File size: 8,025 Bytes
f70ac6a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
"""
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