abhishekrn's picture
history, follow-up
9a6a5aa
from typing import List, Dict, Any, Optional
import json
import re
from src.domain.models.conversation import ConversationContext, ConversationTurn, QueryClassification
from src.application.services.conversation_manager import ConversationManager
from src.infrastructure.providers.llm_provider import LLMClient
from config import settings
from config.conversation_config import conversation_config
class HistoryQueryHandler:
"""Handles queries that can be answered from conversation history."""
def __init__(self, conversation_manager: ConversationManager):
self.conversation_manager = conversation_manager
self.llm_client = LLMClient()
async def handle_history_query(self, user_message: str, session_id: str,
classification: QueryClassification) -> str:
"""Handle a query that references conversation history."""
conversation = self.conversation_manager.get_or_create_conversation(session_id)
# Find relevant historical context
relevant_context = await self._find_relevant_context(user_message, conversation, classification)
if not relevant_context:
return await self._handle_no_context(user_message)
# Generate response using LLM with historical context
return await self._generate_history_response(user_message, relevant_context)
async def _find_relevant_context(self, user_message: str, conversation: ConversationContext,
classification: QueryClassification) -> Optional[Dict[str, Any]]:
"""Find relevant context from conversation history."""
context = {
"relevant_turns": [],
"entity_contexts": {},
"recent_summary": "",
"confidence": classification.confidence
}
# 1. Search for relevant turns based on query content
max_turns = conversation_config.history_handler.max_turns_in_prompt
relevant_turns = self.conversation_manager.search_conversation_history(
conversation.session_id, user_message, limit=max_turns
)
context["relevant_turns"] = relevant_turns
# 2. Get context for referenced entities
for entity_ref in classification.referenced_entities:
entity_context = self._find_entity_context(entity_ref, conversation)
if entity_context:
context["entity_contexts"][entity_ref] = entity_context
# 3. Handle specific reference patterns
context.update(await self._handle_specific_references(user_message, conversation))
# 4. Get recent conversation summary
max_summary_turns = conversation_config.memory.max_context_summary_turns
context["recent_summary"] = conversation.get_context_summary(max_turns=max_summary_turns)
return context if (relevant_turns or context["entity_contexts"] or context.get("specific_data")) else None
def _find_entity_context(self, entity_ref: str, conversation: ConversationContext) -> Optional[Dict[str, Any]]:
"""Find context for a specific entity reference."""
# Try to find entity by different patterns
entity_patterns = [
("user_handle", r'\b(user|member|handle)\b'),
("challenge_id", r'\b(challenge|contest)\b'),
("skill_name", r'\b(skill|technology)\b'),
]
for entity_type, pattern in entity_patterns:
if re.search(pattern, entity_ref, re.IGNORECASE):
entity = conversation.find_entity(entity_type)
if entity:
context_str = self.conversation_manager.get_context_for_entity(
conversation.session_id, entity_type, entity.name
)
return {
"entity": entity,
"context": context_str,
"type": entity_type
}
return None
async def _handle_specific_references(self, user_message: str, conversation: ConversationContext) -> Dict[str, Any]:
"""Handle specific reference patterns in the user message."""
result = {}
message_lower = user_message.lower()
# "Last/Previous" references
if re.search(r'\b(last|previous|recent)\b', message_lower):
max_recent = conversation_config.memory.max_recent_turns_for_context
recent_turns = conversation.get_recent_turns(max_recent)
tool_turns = [turn for turn in recent_turns if turn.tool_used]
if tool_turns:
last_tool_turn = tool_turns[-1]
result["specific_data"] = {
"type": "last_result",
"turn": last_tool_turn,
"description": f"Last {last_tool_turn.tool_used} result"
}
# "That/It" references
elif re.search(r'\b(that|it|this)\b', message_lower):
# Use smaller window for direct references
recent_turns = conversation.get_recent_turns(2)
if recent_turns:
last_turn = recent_turns[-1]
result["specific_data"] = {
"type": "reference",
"turn": last_turn,
"description": "Referenced item from recent conversation"
}
# Specific count/number questions
elif re.search(r'\b(how many|count|total|number)\b', message_lower):
# Look for turns with list results
for turn in reversed(conversation.turns):
if turn.tool_used and "query" in turn.tool_used:
# Try to extract count information
count_info = self._extract_count_from_turn(turn)
if count_info:
result["specific_data"] = {
"type": "count",
"turn": turn,
"count_info": count_info,
"description": f"Count information from {turn.tool_used}"
}
break
return result
def _extract_count_from_turn(self, turn: ConversationTurn) -> Optional[Dict[str, Any]]:
"""Extract count information from a conversation turn."""
if not turn.full_response:
return None
try:
# Try to parse as JSON to get list length
if turn.full_response.startswith('[') or turn.full_response.startswith('{'):
data = json.loads(turn.full_response)
if isinstance(data, list):
return {"count": len(data), "items": "results"}
elif isinstance(data, dict) and "result" in data:
result = data["result"]
if isinstance(result, list):
return {"count": len(result), "items": "results"}
except json.JSONDecodeError:
pass
# Try to extract count from response summary
count_match = re.search(r'(\d+)\s*(challenges?|members?|skills?|results?)',
turn.response_summary, re.IGNORECASE)
if count_match:
return {
"count": int(count_match.group(1)),
"items": count_match.group(2)
}
return None
async def _generate_history_response(self, user_message: str, context: Dict[str, Any]) -> str:
"""Generate a response using LLM with historical context."""
if not settings.HF_TOKEN:
return self._generate_simple_history_response(context)
# Prepare context for LLM
context_prompt = self._build_context_prompt(user_message, context)
try:
messages = [
{
"role": "system",
"content": "You are a helpful Topcoder assistant. Answer the user's question using only the provided conversation history and context. Be conversational and helpful."
},
{
"role": "user",
"content": context_prompt
}
]
response = await self.llm_client.chat(messages)
return response
except Exception as e:
print(f"LLM history response failed: {e}")
return self._generate_simple_history_response(context)
def _build_context_prompt(self, user_message: str, context: Dict[str, Any]) -> str:
"""Build a prompt for the LLM with historical context."""
prompt_parts = [
f"USER'S QUESTION: {user_message}",
"",
"CONVERSATION HISTORY:"
]
# Add relevant turns
if context["relevant_turns"]:
for i, turn in enumerate(context["relevant_turns"], 1):
prompt_parts.append(f"Turn {i}:")
prompt_parts.append(f" User asked: {turn.user_message}")
if turn.tool_used:
prompt_parts.append(f" Tool used: {turn.tool_used}")
if turn.tool_params:
prompt_parts.append(f" Parameters: {json.dumps(turn.tool_params)}")
prompt_parts.append(f" Result: {turn.response_summary}")
else:
max_chars = conversation_config.history_handler.max_response_chars_for_display
prompt_parts.append(f" Response: {turn.full_response[:max_chars]}")
prompt_parts.append("")
# Add entity contexts
if context["entity_contexts"]:
prompt_parts.append("ENTITY CONTEXT:")
for entity_ref, entity_context in context["entity_contexts"].items():
prompt_parts.append(f" {entity_ref}: {entity_context['context']}")
prompt_parts.append("")
# Add specific data
if context.get("specific_data"):
specific = context["specific_data"]
prompt_parts.append("SPECIFIC REFERENCE:")
prompt_parts.append(f" Type: {specific['type']}")
prompt_parts.append(f" Description: {specific['description']}")
if specific["type"] == "count" and "count_info" in specific:
count_info = specific["count_info"]
prompt_parts.append(f" Count: {count_info['count']} {count_info['items']}")
elif "turn" in specific:
turn = specific["turn"]
prompt_parts.append(f" From: {turn.user_message}")
prompt_parts.append(f" Result: {turn.response_summary}")
prompt_parts.append("")
prompt_parts.extend([
"INSTRUCTIONS:",
"- Answer the user's question using only the information above",
"- Be conversational and helpful",
"- If the question can't be fully answered from the history, say so",
"- Reference specific results when appropriate",
"- If asking about counts or numbers, provide the specific count if available"
])
return "\n".join(prompt_parts)
def _generate_simple_history_response(self, context: Dict[str, Any]) -> str:
"""Generate a simple response without LLM when no token available."""
if context.get("specific_data"):
specific = context["specific_data"]
if specific["type"] == "count" and "count_info" in specific:
count_info = specific["count_info"]
return f"Based on our previous search, there were {count_info['count']} {count_info['items']} found."
elif specific["type"] == "last_result":
turn = specific["turn"]
return f"In our last {turn.tool_used} search: {turn.response_summary}"
elif specific["type"] == "reference":
turn = specific["turn"]
if turn.tool_used:
return f"From the previous {turn.tool_used} result: {turn.response_summary}"
else:
return turn.full_response[:300] + "..." if len(turn.full_response) > 300 else turn.full_response
elif context["relevant_turns"]:
last_relevant = context["relevant_turns"][-1]
if last_relevant.tool_used:
return f"From our previous {last_relevant.tool_used} search: {last_relevant.response_summary}"
else:
max_chars = conversation_config.history_handler.max_response_chars_for_display
return f"As discussed earlier: {last_relevant.full_response[:max_chars]}..."
return conversation_config.history_handler.no_context_message
async def _handle_no_context(self, user_message: str) -> str:
"""Handle cases where no relevant context is found."""
# Check if this might be a misclassified query
if any(word in user_message.lower() for word in ['new', 'different', 'other', 'more']):
return conversation_config.history_handler.misclassified_query_message
return conversation_config.history_handler.no_context_message