Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions backend/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Context management for multi-message conversations with smart summarization."""

from typing import List, Dict, Any
from .openrouter import query_model


async def summarize_older_messages(messages: List[Dict[str, str]]) -> str:
"""Summarize older messages into a concise conversation summary."""
conversation_text = ""
for msg in messages:
role = msg["role"].capitalize()
if msg["role"] == "user":
conversation_text += f"{role}: {msg['content']}\n\n"
else:
if "stage3" in msg and "response" in msg["stage3"]:
conversation_text += f"{role}: {msg['stage3']['response']}\n\n"
elif "content" in msg:
conversation_text += f"{role}: {msg['content']}\n\n"

summary_prompt = f"""Summarize the following conversation concisely in 2-3 sentences. Focus on key topics, questions asked, and important context that would be needed to understand follow-up questions.

Conversation:
{conversation_text}

Concise summary:"""

messages_for_api = [{"role": "user", "content": summary_prompt}]
response = await query_model("google/gemini-2.0-flash-exp:free", messages_for_api, timeout=30.0)

if response is None:
return "Previous conversation: " + conversation_text[:200] + "..."

return response.get('content', '').strip()


def format_assistant_message(assistant_msg: Dict[str, Any]) -> str:
"""Convert council's 3-stage output into clean text for context."""
if "stage3" in assistant_msg and "response" in assistant_msg["stage3"]:
return assistant_msg["stage3"]["response"]

if "content" in assistant_msg:
return assistant_msg["content"]

return "[Assistant response]"


async def build_context_messages(
conversation_messages: List[Dict[str, Any]],
current_query: str,
recent_message_limit: int = 5
) -> List[Dict[str, str]]:
"""Build message history with smart summarization for long conversations."""
if len(conversation_messages) == 0:
return [{"role": "user", "content": current_query}]

num_recent_messages = recent_message_limit * 2

if len(conversation_messages) <= num_recent_messages:
formatted_messages = []
for msg in conversation_messages:
if msg["role"] == "user":
formatted_messages.append({"role": "user", "content": msg["content"]})
else:
content = format_assistant_message(msg)
formatted_messages.append({"role": "assistant", "content": content})

formatted_messages.append({"role": "user", "content": current_query})
return formatted_messages

older_messages = conversation_messages[:-num_recent_messages]
recent_messages = conversation_messages[-num_recent_messages:]

summary = await summarize_older_messages(older_messages)

formatted_messages = [
{"role": "user", "content": f"[Previous conversation summary: {summary}]"},
{"role": "assistant", "content": "I understand the previous conversation context."}
]

for msg in recent_messages:
if msg["role"] == "user":
formatted_messages.append({"role": "user", "content": msg["content"]})
else:
content = format_assistant_message(msg)
formatted_messages.append({"role": "assistant", "content": content})

formatted_messages.append({"role": "user", "content": current_query})
return formatted_messages

24 changes: 10 additions & 14 deletions backend/council.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,16 @@
from .config import COUNCIL_MODELS, CHAIRMAN_MODEL


async def stage1_collect_responses(user_query: str) -> List[Dict[str, Any]]:
async def stage1_collect_responses(messages: List[Dict[str, str]]) -> List[Dict[str, Any]]:
"""
Stage 1: Collect individual responses from all council models.

Args:
user_query: The user's question
messages: Full message history including current query

Returns:
List of dicts with 'model' and 'response' keys
"""
messages = [{"role": "user", "content": user_query}]

# Query all models in parallel
responses = await query_models_parallel(COUNCIL_MODELS, messages)

# Format results
Expand Down Expand Up @@ -293,35 +290,34 @@ async def generate_conversation_title(user_query: str) -> str:
return title


async def run_full_council(user_query: str) -> Tuple[List, List, Dict, Dict]:
async def run_full_council(messages: List[Dict[str, str]]) -> Tuple[List, List, Dict, Dict]:
"""
Run the complete 3-stage council process.
Run the complete 3-stage council process with conversation context.

Args:
user_query: The user's question
messages: Full message history in OpenAI format

Returns:
Tuple of (stage1_results, stage2_results, stage3_result, metadata)
"""
# Stage 1: Collect individual responses
stage1_results = await stage1_collect_responses(user_query)
current_query = messages[-1]["content"]

stage1_results = await stage1_collect_responses(messages)

# If no models responded successfully, return error
if not stage1_results:
return [], [], {
"model": "error",
"response": "All models failed to respond. Please try again."
}, {}

# Stage 2: Collect rankings
stage2_results, label_to_model = await stage2_collect_rankings(user_query, stage1_results)
stage2_results, label_to_model = await stage2_collect_rankings(current_query, stage1_results)

# Calculate aggregate rankings
aggregate_rankings = calculate_aggregate_rankings(stage2_results, label_to_model)

# Stage 3: Synthesize final answer
stage3_result = await stage3_synthesize_final(
user_query,
current_query,
stage1_results,
stage2_results
)
Expand Down
14 changes: 11 additions & 3 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from . import storage
from .council import run_full_council, generate_conversation_title, stage1_collect_responses, stage2_collect_rankings, stage3_synthesize_final, calculate_aggregate_rankings
from .context import build_context_messages

app = FastAPI(title="LLM Council API")

Expand Down Expand Up @@ -101,11 +102,13 @@ async def send_message(conversation_id: str, request: SendMessageRequest):
title = await generate_conversation_title(request.content)
storage.update_conversation_title(conversation_id, title)

# Run the 3-stage council process
stage1_results, stage2_results, stage3_result, metadata = await run_full_council(
messages = await build_context_messages(
conversation["messages"][:-1],
request.content
)

stage1_results, stage2_results, stage3_result, metadata = await run_full_council(messages)

# Add assistant message with all stages
storage.add_assistant_message(
conversation_id,
Expand Down Expand Up @@ -147,9 +150,14 @@ async def event_generator():
if is_first_message:
title_task = asyncio.create_task(generate_conversation_title(request.content))

messages = await build_context_messages(
conversation["messages"][:-1],
request.content
)

# Stage 1: Collect responses
yield f"data: {json.dumps({'type': 'stage1_start'})}\n\n"
stage1_results = await stage1_collect_responses(request.content)
stage1_results = await stage1_collect_responses(messages)
yield f"data: {json.dumps({'type': 'stage1_complete', 'data': stage1_results})}\n\n"

# Stage 2: Collect rankings
Expand Down
9 changes: 9 additions & 0 deletions frontend/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
20








15 changes: 15 additions & 0 deletions frontend/src/components/ChatInterface.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@
font-size: 16px;
}

.context-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
margin-bottom: 20px;
background: linear-gradient(135deg, #f8f9ff 0%, #f0f7ff 100%);
border: 1px solid #d0e7ff;
border-radius: 8px;
color: #4a90e2;
font-size: 13px;
font-weight: 500;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}

.message-group {
margin-bottom: 32px;
}
Expand Down
53 changes: 31 additions & 22 deletions frontend/src/components/ChatInterface.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,13 @@ export default function ChatInterface({
<p>Ask a question to consult the LLM Council</p>
</div>
) : (
conversation.messages.map((msg, index) => (
<>
{conversation.messages.length > 6 && (
<div className="context-indicator">
💭 Using conversation context ({conversation.messages.length} messages)
</div>
)}
{conversation.messages.map((msg, index) => (
<div key={index} className="message-group">
{msg.role === 'user' ? (
<div className="user-message">
Expand Down Expand Up @@ -107,7 +113,8 @@ export default function ChatInterface({
</div>
)}
</div>
))
))}
</>
)}

{isLoading && (
Expand All @@ -120,26 +127,28 @@ export default function ChatInterface({
<div ref={messagesEndRef} />
</div>

{conversation.messages.length === 0 && (
<form className="input-form" onSubmit={handleSubmit}>
<textarea
className="message-input"
placeholder="Ask your question... (Shift+Enter for new line, Enter to send)"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
disabled={isLoading}
rows={3}
/>
<button
type="submit"
className="send-button"
disabled={!input.trim() || isLoading}
>
Send
</button>
</form>
)}
<form className="input-form" onSubmit={handleSubmit}>
<textarea
className="message-input"
placeholder={
conversation.messages.length === 0
? "Ask your question... (Shift+Enter for new line, Enter to send)"
: "Ask a follow-up question... (Shift+Enter for new line, Enter to send)"
}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
disabled={isLoading}
rows={3}
/>
<button
type="submit"
className="send-button"
disabled={!input.trim() || isLoading}
>
Send
</button>
</form>
</div>
);
}