@@ -117,6 +117,32 @@ def __init__(self, config: Dict[str, Any], verbose: int = 0):
117
117
self .use_rag = (self .provider .lower () == "rag" ) and CHROMADB_AVAILABLE and self .cfg .get ("use_embedding" , False )
118
118
self .graph_enabled = False # Initialize graph support flag
119
119
120
+ # Initialize session summary configuration
121
+ self .session_summary_config = self .cfg .get ("session_summary_config" , {})
122
+ self .session_enabled = self .session_summary_config .get ("enabled" , False )
123
+ self .update_after_n_turns = self .session_summary_config .get ("update_after_n_turns" , 5 )
124
+ self .summary_model = self .session_summary_config .get ("model" , "gpt-4o-mini" )
125
+ self .include_in_context = self .session_summary_config .get ("include_in_context" , True )
126
+
127
+ # Initialize agentic memory configuration
128
+ self .agentic_config = self .cfg .get ("agentic_config" , {})
129
+ self .agentic_enabled = self .agentic_config .get ("enabled" , False )
130
+ self .auto_classify = self .agentic_config .get ("auto_classify" , True )
131
+ self .confidence_threshold = self .agentic_config .get ("confidence_threshold" , 0.7 )
132
+ self .management_model = self .agentic_config .get ("management_model" , "gpt-4o" )
133
+
134
+ # Initialize memory reference configuration
135
+ self .reference_config = self .cfg .get ("reference_config" , {})
136
+ self .include_references = self .reference_config .get ("include_references" , False )
137
+ self .reference_format = self .reference_config .get ("reference_format" , "inline" )
138
+ self .max_references = self .reference_config .get ("max_references" , 5 )
139
+ self .show_confidence = self .reference_config .get ("show_confidence" , False )
140
+
141
+ # Session tracking for summaries
142
+ self .turn_counter = 0
143
+ self .session_history = []
144
+ self .current_session_summary = None
145
+
120
146
# Extract embedding model from config
121
147
self .embedder_config = self .cfg .get ("embedder" , {})
122
148
if isinstance (self .embedder_config , dict ):
@@ -1144,3 +1170,311 @@ def search_with_quality(
1144
1170
logger .info (f"After quality filter: { len (filtered )} results" )
1145
1171
1146
1172
return filtered
1173
+
1174
+ # -------------------------------------------------------------------------
1175
+ # Session Summary Methods
1176
+ # -------------------------------------------------------------------------
1177
+ def add_to_session (self , role : str , content : str ) -> None :
1178
+ """Add a conversation turn to the session history"""
1179
+ if not self .session_enabled :
1180
+ return
1181
+
1182
+ self .session_history .append ({
1183
+ "role" : role ,
1184
+ "content" : content ,
1185
+ "timestamp" : time .time ()
1186
+ })
1187
+ self .turn_counter += 1
1188
+
1189
+ # Check if we need to update the session summary
1190
+ if self .turn_counter % self .update_after_n_turns == 0 :
1191
+ self ._update_session_summary ()
1192
+
1193
+ def _update_session_summary (self ) -> None :
1194
+ """Update the session summary using the configured model"""
1195
+ if not self .session_history :
1196
+ return
1197
+
1198
+ # Create conversation text for summarization
1199
+ conversation_text = "\n " .join ([
1200
+ f"{ turn ['role' ]} : { turn ['content' ]} "
1201
+ for turn in self .session_history [- self .update_after_n_turns :]
1202
+ ])
1203
+
1204
+ summary_prompt = f"""
1205
+ Summarize the following conversation, focusing on:
1206
+ 1. Key topics discussed
1207
+ 2. Important decisions made
1208
+ 3. Relevant context for future conversations
1209
+ 4. User preferences and requirements mentioned
1210
+
1211
+ Conversation:
1212
+ { conversation_text }
1213
+
1214
+ Provide a concise summary in JSON format with keys: "text", "topics", "key_points"
1215
+ """
1216
+
1217
+ try :
1218
+ if LITELLM_AVAILABLE :
1219
+ import litellm
1220
+ response = litellm .completion (
1221
+ model = self .summary_model ,
1222
+ messages = [{"role" : "user" , "content" : summary_prompt }],
1223
+ response_format = {"type" : "json_object" },
1224
+ temperature = 0.3
1225
+ )
1226
+ summary_data = json .loads (response .choices [0 ].message .content )
1227
+ elif OPENAI_AVAILABLE :
1228
+ from openai import OpenAI
1229
+ client = OpenAI ()
1230
+ response = client .chat .completions .create (
1231
+ model = self .summary_model ,
1232
+ messages = [{"role" : "user" , "content" : summary_prompt }],
1233
+ response_format = {"type" : "json_object" },
1234
+ temperature = 0.3
1235
+ )
1236
+ summary_data = json .loads (response .choices [0 ].message .content )
1237
+ else :
1238
+ self ._log_verbose ("No LLM available for session summary" , logging .WARNING )
1239
+ return
1240
+
1241
+ self .current_session_summary = summary_data
1242
+
1243
+ # Store summary in long-term memory if enabled
1244
+ if self .include_in_context :
1245
+ self .store_long_term (
1246
+ text = summary_data .get ("text" , "" ),
1247
+ metadata = {
1248
+ "type" : "session_summary" ,
1249
+ "topics" : summary_data .get ("topics" , []),
1250
+ "key_points" : summary_data .get ("key_points" , []),
1251
+ "turn_count" : self .turn_counter
1252
+ }
1253
+ )
1254
+
1255
+ except Exception as e :
1256
+ self ._log_verbose (f"Error updating session summary: { e } " , logging .ERROR )
1257
+
1258
+ async def aget_session_summary (self ) -> Optional [Dict [str , Any ]]:
1259
+ """Get the current session summary (async version)"""
1260
+ return self .current_session_summary
1261
+
1262
+ def get_session_summary (self ) -> Optional [Dict [str , Any ]]:
1263
+ """Get the current session summary"""
1264
+ return self .current_session_summary
1265
+
1266
+ # -------------------------------------------------------------------------
1267
+ # Agentic Memory Management Methods
1268
+ # -------------------------------------------------------------------------
1269
+ def remember (self , fact : str , metadata : Optional [Dict [str , Any ]] = None ) -> bool :
1270
+ """Store important information with agentic classification"""
1271
+ if not self .agentic_enabled :
1272
+ # Fallback to regular long-term storage
1273
+ self .store_long_term (fact , metadata = metadata )
1274
+ return True
1275
+
1276
+ # Auto-classify the importance if enabled
1277
+ if self .auto_classify :
1278
+ importance_score = self ._classify_importance (fact )
1279
+ if importance_score < self .confidence_threshold :
1280
+ self ._log_verbose (f"Fact importance { importance_score } below threshold { self .confidence_threshold } " )
1281
+ return False
1282
+
1283
+ # Store with agentic metadata
1284
+ agentic_metadata = metadata or {}
1285
+ agentic_metadata .update ({
1286
+ "stored_by" : "agentic_memory" ,
1287
+ "importance_score" : importance_score if self .auto_classify else 1.0 ,
1288
+ "auto_classified" : self .auto_classify
1289
+ })
1290
+
1291
+ self .store_long_term (fact , metadata = agentic_metadata )
1292
+ return True
1293
+
1294
+ def update_memory (self , memory_id : str , new_fact : str ) -> bool :
1295
+ """Update existing memory by ID"""
1296
+ try :
1297
+ # Update in SQLite
1298
+ conn = sqlite3 .connect (self .long_db )
1299
+ c = conn .cursor ()
1300
+ c .execute (
1301
+ "UPDATE long_mem SET content = ?, meta = ? WHERE id = ?" ,
1302
+ (new_fact , json .dumps ({"updated" : True , "updated_at" : time .time ()}), memory_id )
1303
+ )
1304
+ updated = c .rowcount > 0
1305
+ conn .commit ()
1306
+ conn .close ()
1307
+
1308
+ # Update in vector store if available
1309
+ if self .use_rag and hasattr (self , "chroma_col" ):
1310
+ try :
1311
+ # ChromaDB doesn't support direct updates, so we delete and re-add
1312
+ self .chroma_col .delete (ids = [memory_id ])
1313
+ if LITELLM_AVAILABLE :
1314
+ import litellm
1315
+ response = litellm .embedding (
1316
+ model = self .embedding_model ,
1317
+ input = new_fact
1318
+ )
1319
+ embedding = response .data [0 ]["embedding" ]
1320
+ elif OPENAI_AVAILABLE :
1321
+ from openai import OpenAI
1322
+ client = OpenAI ()
1323
+ response = client .embeddings .create (
1324
+ input = new_fact ,
1325
+ model = self .embedding_model
1326
+ )
1327
+ embedding = response .data [0 ].embedding
1328
+ else :
1329
+ return updated
1330
+
1331
+ self .chroma_col .add (
1332
+ documents = [new_fact ],
1333
+ metadatas = [{"updated" : True , "updated_at" : time .time ()}],
1334
+ ids = [memory_id ],
1335
+ embeddings = [embedding ]
1336
+ )
1337
+ except Exception as e :
1338
+ self ._log_verbose (f"Error updating in ChromaDB: { e } " , logging .ERROR )
1339
+
1340
+ return updated
1341
+
1342
+ except Exception as e :
1343
+ self ._log_verbose (f"Error updating memory: { e } " , logging .ERROR )
1344
+ return False
1345
+
1346
+ def forget (self , memory_id : str ) -> bool :
1347
+ """Remove a memory by ID"""
1348
+ try :
1349
+ # Delete from SQLite
1350
+ conn = sqlite3 .connect (self .long_db )
1351
+ c = conn .cursor ()
1352
+ c .execute ("DELETE FROM long_mem WHERE id = ?" , (memory_id ,))
1353
+ deleted = c .rowcount > 0
1354
+ conn .commit ()
1355
+ conn .close ()
1356
+
1357
+ # Delete from vector store if available
1358
+ if self .use_rag and hasattr (self , "chroma_col" ):
1359
+ try :
1360
+ self .chroma_col .delete (ids = [memory_id ])
1361
+ except Exception as e :
1362
+ self ._log_verbose (f"Error deleting from ChromaDB: { e } " , logging .ERROR )
1363
+
1364
+ return deleted
1365
+
1366
+ except Exception as e :
1367
+ self ._log_verbose (f"Error forgetting memory: { e } " , logging .ERROR )
1368
+ return False
1369
+
1370
+ def search_memories (self , query : str , limit : int = 5 , ** kwargs ) -> List [Dict [str , Any ]]:
1371
+ """Search memories with agentic filtering"""
1372
+ # Use existing search method but add agentic filtering
1373
+ results = self .search_long_term (query , limit = limit , ** kwargs )
1374
+
1375
+ # Filter by agentic metadata if enabled
1376
+ if self .agentic_enabled :
1377
+ results = [
1378
+ r for r in results
1379
+ if r .get ("metadata" , {}).get ("stored_by" ) == "agentic_memory"
1380
+ ]
1381
+
1382
+ return results
1383
+
1384
+ def _classify_importance (self , fact : str ) -> float :
1385
+ """Classify the importance of a fact using LLM"""
1386
+ classification_prompt = f"""
1387
+ Rate the importance of storing this information in long-term memory on a scale of 0.0 to 1.0:
1388
+ - 1.0: Critical information (user preferences, key decisions, important facts)
1389
+ - 0.7: Important information (useful context, relevant details)
1390
+ - 0.5: Moderate information (might be useful later)
1391
+ - 0.3: Low importance (casual conversation, temporary info)
1392
+ - 0.0: Not worth storing (greetings, small talk)
1393
+
1394
+ Information: { fact }
1395
+
1396
+ Return only a number between 0.0 and 1.0.
1397
+ """
1398
+
1399
+ try :
1400
+ if LITELLM_AVAILABLE :
1401
+ import litellm
1402
+ response = litellm .completion (
1403
+ model = self .management_model ,
1404
+ messages = [{"role" : "user" , "content" : classification_prompt }],
1405
+ temperature = 0.1
1406
+ )
1407
+ score_text = response .choices [0 ].message .content .strip ()
1408
+ elif OPENAI_AVAILABLE :
1409
+ from openai import OpenAI
1410
+ client = OpenAI ()
1411
+ response = client .chat .completions .create (
1412
+ model = self .management_model ,
1413
+ messages = [{"role" : "user" , "content" : classification_prompt }],
1414
+ temperature = 0.1
1415
+ )
1416
+ score_text = response .choices [0 ].message .content .strip ()
1417
+ else :
1418
+ return 0.5 # Default moderate importance
1419
+
1420
+ return float (score_text )
1421
+
1422
+ except Exception as e :
1423
+ self ._log_verbose (f"Error classifying importance: { e } " , logging .ERROR )
1424
+ return 0.5 # Default moderate importance
1425
+
1426
+ # -------------------------------------------------------------------------
1427
+ # Memory Reference Methods
1428
+ # -------------------------------------------------------------------------
1429
+ def search_with_references (self , query : str , limit : int = 5 , ** kwargs ) -> Dict [str , Any ]:
1430
+ """Search with memory references included"""
1431
+ results = self .search_long_term (query , limit = limit , ** kwargs )
1432
+
1433
+ if not self .include_references or not results :
1434
+ return {
1435
+ "content" : "" ,
1436
+ "references" : []
1437
+ }
1438
+
1439
+ # Format results with references
1440
+ content_parts = []
1441
+ references = []
1442
+
1443
+ for i , result in enumerate (results [:self .max_references ], 1 ):
1444
+ text = result .get ("text" , "" )
1445
+ metadata = result .get ("metadata" , {})
1446
+ confidence = result .get ("score" , 0.0 )
1447
+
1448
+ if self .reference_format == "inline" :
1449
+ content_parts .append (f"{ text } [{ i } ]" )
1450
+ elif self .reference_format == "footnote" :
1451
+ content_parts .append (f"{ text } " )
1452
+ else : # metadata format
1453
+ content_parts .append (text )
1454
+
1455
+ ref_entry = {
1456
+ "id" : i ,
1457
+ "text" : text ,
1458
+ "metadata" : metadata
1459
+ }
1460
+
1461
+ if self .show_confidence :
1462
+ ref_entry ["confidence" ] = confidence
1463
+
1464
+ references .append (ref_entry )
1465
+
1466
+ content = " " .join (content_parts )
1467
+
1468
+ # Add footnotes if using footnote format
1469
+ if self .reference_format == "footnote" :
1470
+ footnotes = [
1471
+ f"[{ ref ['id' ]} ] { ref ['text' ]} " +
1472
+ (f" (confidence: { ref ['confidence' ]:.2f} )" if self .show_confidence else "" )
1473
+ for ref in references
1474
+ ]
1475
+ content += "\n \n References:\n " + "\n " .join (footnotes )
1476
+
1477
+ return {
1478
+ "content" : content ,
1479
+ "references" : references
1480
+ }
0 commit comments