Aniket2006's picture
feat(copilot): complete copilot integration and resolve CSS styling compliance
f70ac6a
Raw
History Blame Contribute Delete
8.03 kB
"""
MemoryAgent - Conversation context management
Stores and retrieves conversation history
Resolves coreferences (this account, that alert, etc)
"""
import json
import re
import sqlite3
from typing import Dict, List
from pathlib import Path
from .base import Agent, AgentConfig, AgentResult
import time
class MemoryAgent(Agent):
"""
Manages conversation memory using SQLite
Maintains context across turns within a conversation
"""
DB_PATH = Path(__file__).parent.parent.parent.parent / "data" / "copilot_memory.db"
def __init__(self, api_pool):
config = AgentConfig(
name="MemoryAgent",
model="llama-3.1-8b-instant",
temperature=0.0,
max_tokens=500,
timeout_ms=5000,
)
super().__init__(config, api_pool)
self._init_db()
def _init_db(self):
"""Initialize SQLite database for conversation memory"""
self.DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(self.DB_PATH))
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS conversation_turns (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id TEXT NOT NULL,
turn_number INTEGER NOT NULL,
user_message TEXT NOT NULL,
assistant_response TEXT,
intent TEXT,
extracted_entities TEXT,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(conversation_id, turn_number)
)
""")
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_conversation_id
ON conversation_turns(conversation_id)
""")
conn.commit()
conn.close()
def _build_prompt(self, **inputs) -> str:
return ""
def _parse_response(self, response_text: str) -> Dict:
return {}
async def invoke(
self,
conversation_id: str,
user_message: str = None,
limit: int = 5,
**kwargs,
) -> AgentResult:
"""
Retrieve conversation history and resolve coreferences
"""
start_time = time.time()
try:
# Get conversation history
history = self._get_conversation_history(conversation_id, limit=limit)
# Extract current context from history
current_context = self._extract_current_context(history)
# Resolve coreferences in current message
resolved_message = user_message
if user_message and current_context:
resolved_message = self._resolve_coreferences(user_message, current_context)
result_data = {
"conversation_id": conversation_id,
"history": history,
"current_context": current_context,
"resolved_message": resolved_message,
"turn_count": len(history),
}
self.logger.info(
f"[OK] {self.config.name}: Retrieved {len(history)} turns "
f"for {conversation_id}"
)
return await self._create_result(
success=True,
data=result_data,
tokens_input=0,
tokens_output=self._estimate_tokens(str(result_data)),
start_time=start_time,
)
except Exception as e:
self.logger.error(f"[FAIL] {self.config.name}: {e}")
return await self._create_result(
success=False,
data={"history": [], "current_context": {}},
error=str(e),
start_time=start_time,
)
def _get_conversation_history(self, conversation_id: str, limit: int = 5) -> List[Dict]:
"""Fetch recent turns for a conversation"""
conn = sqlite3.connect(str(self.DB_PATH))
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM conversation_turns
WHERE conversation_id = ?
ORDER BY turn_number DESC
LIMIT ?
""", (conversation_id, limit))
rows = cursor.fetchall()
conn.close()
turns = []
for row in rows:
turn = dict(row)
# Parse extracted entities JSON
if turn.get("extracted_entities"):
try:
turn["extracted_entities"] = json.loads(turn["extracted_entities"])
except json.JSONDecodeError:
turn["extracted_entities"] = {}
turns.append(turn)
# Reverse to chronological order
turns.reverse()
return turns
def _extract_current_context(self, history: List[Dict]) -> Dict:
"""Extract current context from conversation history"""
context = {
"current_account_id": None,
"current_alert_id": None,
"current_typology": None,
"last_intent": None,
"topics_discussed": [],
}
for turn in history:
entities = turn.get("extracted_entities", {})
if isinstance(entities, dict):
# Update with most recent values
if entities.get("account_id"):
context["current_account_id"] = entities["account_id"]
if entities.get("typologies"):
context["current_typology"] = entities["typologies"][0] if entities["typologies"] else None
context["last_intent"] = turn.get("intent")
# Track topics
if turn.get("intent"):
context["topics_discussed"].append(turn["intent"])
return context
def _resolve_coreferences(self, message: str, context: Dict) -> str:
"""Resolve pronouns and references in message"""
if not message:
return message
resolved = message
message_lower = message.lower()
# Coreferences to resolve
replacements = {
"this account": context.get("current_account_id"),
"that account": context.get("current_account_id"),
"the account": context.get("current_account_id"),
"this alert": context.get("current_alert_id"),
"that alert": context.get("current_alert_id"),
"this typology": context.get("current_typology"),
"that typology": context.get("current_typology"),
}
for pronoun, entity in replacements.items():
if entity and pronoun in message_lower:
# Case-insensitive replacement preserving original
pattern = re.compile(re.escape(pronoun), re.IGNORECASE)
resolved = pattern.sub(f"{pronoun} ({entity})", resolved)
return resolved
def store_turn(
self,
conversation_id: str,
user_message: str,
assistant_response: str = "",
intent: str = "GENERAL",
extracted_entities: Dict = None,
) -> int:
"""Store a conversation turn in the database"""
conn = sqlite3.connect(str(self.DB_PATH))
cursor = conn.cursor()
# Get next turn number
cursor.execute(
"SELECT COALESCE(MAX(turn_number), 0) + 1 FROM conversation_turns WHERE conversation_id = ?",
(conversation_id,)
)
turn_number = cursor.fetchone()[0]
# Insert turn
cursor.execute("""
INSERT INTO conversation_turns
(conversation_id, turn_number, user_message, assistant_response, intent, extracted_entities)
VALUES (?, ?, ?, ?, ?, ?)
""", (
conversation_id,
turn_number,
user_message,
assistant_response,
intent,
json.dumps(extracted_entities or {}),
))
conn.commit()
conn.close()
return turn_number