diff --git a/backend/context.py b/backend/context.py new file mode 100644 index 000000000..e413cb0a8 --- /dev/null +++ b/backend/context.py @@ -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 + diff --git a/backend/council.py b/backend/council.py index 5069abec9..a2579fbde 100644 --- a/backend/council.py +++ b/backend/council.py @@ -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 @@ -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 ) diff --git a/backend/main.py b/backend/main.py index e33ce59a6..f36fa7ae9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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") @@ -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, @@ -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 diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 000000000..db2b6f4ba --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1,9 @@ +20 + + + + + + + + diff --git a/frontend/src/components/ChatInterface.css b/frontend/src/components/ChatInterface.css index 0d013003a..715a825fb 100644 --- a/frontend/src/components/ChatInterface.css +++ b/frontend/src/components/ChatInterface.css @@ -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; } diff --git a/frontend/src/components/ChatInterface.jsx b/frontend/src/components/ChatInterface.jsx index 3ae796caa..544cdb98d 100644 --- a/frontend/src/components/ChatInterface.jsx +++ b/frontend/src/components/ChatInterface.jsx @@ -57,7 +57,13 @@ export default function ChatInterface({

Ask a question to consult the LLM Council

) : ( - conversation.messages.map((msg, index) => ( + <> + {conversation.messages.length > 6 && ( +
+ 💭 Using conversation context ({conversation.messages.length} messages) +
+ )} + {conversation.messages.map((msg, index) => (
{msg.role === 'user' ? (
@@ -107,7 +113,8 @@ export default function ChatInterface({
)}
- )) + ))} + )} {isLoading && ( @@ -120,26 +127,28 @@ export default function ChatInterface({
- {conversation.messages.length === 0 && ( -
-