Dama12 commited on
Commit
ee33562
·
1 Parent(s): 05d4e50

feat: implement Long-Term Semantic Memory using Qdrant (Approach B)

Browse files
app/api/routes/analysis.py CHANGED
@@ -74,6 +74,7 @@ async def start_project_analysis(
74
  if not current_user.id.hex.startswith("0000"):
75
  user_repo.deduct_credits(current_user, 50)
76
 
 
77
  target_project_id = project_id
78
 
79
  # If project_id is the "nil" UUID from frontend, use/create a real project for history
@@ -279,6 +280,7 @@ async def get_specific_analysis(
279
  engine = K2ThinkEngine()
280
  k2_request = K2AnalysisRequest(
281
  documents=docs,
 
282
  reasoning_depth=depth,
283
  ethics_rigor=rigor,
284
  info_density=density
@@ -366,7 +368,8 @@ async def scientific_chat(
366
  result = await engine.chat(
367
  message=request.message,
368
  analysis_context=context,
369
- history=request.history or []
 
370
  )
371
  # Update credits in result
372
  if isinstance(result, ChatResponse):
 
74
  if not current_user.id.hex.startswith("0000"):
75
  user_repo.deduct_credits(current_user, 50)
76
 
77
+ request.user_id = str(current_user.id)
78
  target_project_id = project_id
79
 
80
  # If project_id is the "nil" UUID from frontend, use/create a real project for history
 
280
  engine = K2ThinkEngine()
281
  k2_request = K2AnalysisRequest(
282
  documents=docs,
283
+ user_id=str(current_user.id),
284
  reasoning_depth=depth,
285
  ethics_rigor=rigor,
286
  info_density=density
 
368
  result = await engine.chat(
369
  message=request.message,
370
  analysis_context=context,
371
+ history=request.history or [],
372
+ user_id=str(current_user.id)
373
  )
374
  # Update credits in result
375
  if isinstance(result, ChatResponse):
app/models/schemas.py CHANGED
@@ -118,6 +118,7 @@ class ExperimentalProtocol(BaseModel):
118
  class AnalysisRequest(BaseModel):
119
  """Requête d'analyse scientifique"""
120
  documents: List[ScientificDocument]
 
121
  user_notes: Optional[str] = None
122
 
123
  # Scientific Settings from UI
 
118
  class AnalysisRequest(BaseModel):
119
  """Requête d'analyse scientifique"""
120
  documents: List[ScientificDocument]
121
+ user_id: Optional[str] = None
122
  user_notes: Optional[str] = None
123
 
124
  # Scientific Settings from UI
app/services/k2_think_engine.py CHANGED
@@ -34,6 +34,9 @@ class K2ThinkEngine:
34
 
35
  self.audit_logs: List[AuditLog] = []
36
  self.reasoning_trace: List[Dict[str, Any]] = []
 
 
 
37
 
38
  async def process_analysis_request(
39
  self,
@@ -98,9 +101,22 @@ class K2ThinkEngine:
98
 
99
  density_instruction = "Use compact, dense bullet points." if request.info_density == "compact" else "Use comfortable, explanatory paragraphs where needed."
100
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  system_prompt = f"""You are the K2 Think V2 Scientific Co-Investigator.
102
  Your core capability and primary directive is MULTI-DOCUMENT REASONING and KNOWLEDGE SYNTHESIS.
103
  Do NOT just summarize individual papers. You MUST cross-reference, compare, and contrast the provided documents to uncover deeper strategic insights.
 
104
 
105
  REASONING GUIDELINES:
106
  - DEPTH: {depth_instruction}
@@ -204,6 +220,20 @@ OUTPUT FORMAT: You MUST respond ONLY with a valid JSON object matching this stru
204
  )
205
 
206
  logger.info(f"Analysis {request_id} completed successfully")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  return result
208
 
209
  except Exception as e:
@@ -217,7 +247,8 @@ OUTPUT FORMAT: You MUST respond ONLY with a valid JSON object matching this stru
217
  self,
218
  message: str,
219
  analysis_context: Optional[Dict[str, Any]] = None,
220
- history: List[Dict[str, str]] = []
 
221
  ) -> Dict[str, Any]:
222
  """
223
  Discussion interactive avec K2 Think sur l'analyse
@@ -234,6 +265,13 @@ Your goal:
234
 
235
  Keep your tone professional, strategic, and scientifically rigorous.
236
  """
 
 
 
 
 
 
 
237
  messages = [{"role": "system", "content": system_prompt}]
238
 
239
  # Add history
 
34
 
35
  self.audit_logs: List[AuditLog] = []
36
  self.reasoning_trace: List[Dict[str, Any]] = []
37
+
38
+ from app.services.memory_service import MemoryService
39
+ self.memory_service = MemoryService()
40
 
41
  async def process_analysis_request(
42
  self,
 
101
 
102
  density_instruction = "Use compact, dense bullet points." if request.info_density == "compact" else "Use comfortable, explanatory paragraphs where needed."
103
 
104
+ # ============ LONG-TERM SEMANTIC MEMORY ============
105
+ past_context_instruction = ""
106
+ if request.user_id:
107
+ # Search for past memories related to the document titles/content
108
+ query = f"Research related to: {', '.join([d.title for d in request.documents[:3]])}"
109
+ memories = await self.memory_service.search_memory(request.user_id, query)
110
+
111
+ if memories:
112
+ logger.info(f"K2 Think: Retrieved {len(memories)} past memories for user {request.user_id}")
113
+ memories_text = "\n- ".join(memories)
114
+ past_context_instruction = f"\n\nPAST RESEARCH CONTEXT & PREFERENCES (Your memory of this researcher):\n- {memories_text}"
115
+
116
  system_prompt = f"""You are the K2 Think V2 Scientific Co-Investigator.
117
  Your core capability and primary directive is MULTI-DOCUMENT REASONING and KNOWLEDGE SYNTHESIS.
118
  Do NOT just summarize individual papers. You MUST cross-reference, compare, and contrast the provided documents to uncover deeper strategic insights.
119
+ {past_context_instruction}
120
 
121
  REASONING GUIDELINES:
122
  - DEPTH: {depth_instruction}
 
220
  )
221
 
222
  logger.info(f"Analysis {request_id} completed successfully")
223
+
224
+ # ============ MEMORY CONSOLIDATION ============
225
+ if request.user_id:
226
+ try:
227
+ # Determine project_id (this might need to be passed in the request or handled by orchestration)
228
+ # For now we use a generic placeholder if not available
229
+ await self.memory_service.consolidate_analysis(
230
+ user_id=request.user_id,
231
+ project_id="auto_consolidation",
232
+ analysis_result=result
233
+ )
234
+ except Exception as mem_err:
235
+ logger.error(f"Failed to consolidate memory: {mem_err}")
236
+
237
  return result
238
 
239
  except Exception as e:
 
247
  self,
248
  message: str,
249
  analysis_context: Optional[Dict[str, Any]] = None,
250
+ history: List[Dict[str, str]] = [],
251
+ user_id: Optional[str] = None
252
  ) -> Dict[str, Any]:
253
  """
254
  Discussion interactive avec K2 Think sur l'analyse
 
265
 
266
  Keep your tone professional, strategic, and scientifically rigorous.
267
  """
268
+ # ============ LONG-TERM SEMANTIC MEMORY (CHAT) ============
269
+ if user_id:
270
+ memories = await self.memory_service.search_memory(user_id, message)
271
+ if memories:
272
+ memories_text = "\n- ".join(memories)
273
+ system_prompt += f"\n\nPAST USER CONTEXT (Relevant to this query):\n- {memories_text}"
274
+
275
  messages = [{"role": "system", "content": system_prompt}]
276
 
277
  # Add history
app/services/memory_service.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Service de Mémoire Sémantique - Persistance du savoir de l'investigateur via Qdrant
3
+ """
4
+ from typing import List, Dict, Any, Optional
5
+ from qdrant_client import QdrantClient
6
+ from qdrant_client.http import models
7
+ from app.core.settings import settings
8
+ from app.core.logging import logger
9
+ from datetime import datetime
10
+ import uuid
11
+
12
+ class MemoryService:
13
+ """
14
+ Gère la mémoire à long terme de l'agent.
15
+ Stocke les découvertes, préférences et conclusions passées.
16
+ """
17
+
18
+ COLLECTION_NAME = "user_semantic_memory"
19
+
20
+ def __init__(self):
21
+ self.client = QdrantClient(
22
+ url=settings.VECTOR_DB_URL,
23
+ api_key=settings.VECTOR_DB_API_KEY
24
+ )
25
+ self._ensure_collection()
26
+
27
+ def _ensure_collection(self):
28
+ """Crée la collection si elle n'existe pas"""
29
+ try:
30
+ collections = self.client.get_collections().collections
31
+ exists = any(c.name == self.COLLECTION_NAME for c in collections)
32
+
33
+ if not exists:
34
+ logger.info(f"Creating Qdrant collection: {self.COLLECTION_NAME}")
35
+ self.client.create_collection(
36
+ collection_name=self.COLLECTION_NAME,
37
+ vectors_config=models.VectorParams(
38
+ size=1536, # Taille standard OpenAI embeddings (text-embedding-3-small)
39
+ distance=models.Distance.COSINE
40
+ )
41
+ )
42
+ except Exception as e:
43
+ logger.error(f"Failed to ensure Qdrant collection: {e}")
44
+
45
+ async def save_memory(self, user_id: str, content: str, metadata: Dict[str, Any] = None):
46
+ """Sauvegarde un fragment de savoir dans la mémoire sémantique"""
47
+ from openai import AsyncOpenAI
48
+ openai_client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
49
+
50
+ try:
51
+ # 1. Générer embedding
52
+ response = await openai_client.embeddings.create(
53
+ input=content,
54
+ model="text-embedding-3-small"
55
+ )
56
+ embedding = response.data[0].embedding
57
+
58
+ # 2. Upsert dans Qdrant
59
+ point_id = str(uuid.uuid4())
60
+ payload = {
61
+ "user_id": user_id,
62
+ "content": content,
63
+ "created_at": datetime.utcnow().isoformat(),
64
+ **(metadata or {})
65
+ }
66
+
67
+ self.client.upsert(
68
+ collection_name=self.COLLECTION_NAME,
69
+ points=[
70
+ models.PointStruct(
71
+ id=point_id,
72
+ vector=embedding,
73
+ payload=payload
74
+ )
75
+ ]
76
+ )
77
+ logger.info(f"Memory fragment saved for user {user_id}")
78
+ return point_id
79
+ except Exception as e:
80
+ logger.error(f"Failed to save memory: {e}")
81
+ return None
82
+
83
+ async def search_memory(self, user_id: str, query: str, limit: int = 5) -> List[str]:
84
+ """Recherche des souvenirs pertinents pour un sujet donné"""
85
+ from openai import AsyncOpenAI
86
+ openai_client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
87
+
88
+ try:
89
+ # 1. Générer embedding de la requête
90
+ response = await openai_client.embeddings.create(
91
+ input=query,
92
+ model="text-embedding-3-small"
93
+ )
94
+ embedding = response.data[0].embedding
95
+
96
+ # 2. Rechercher dans Qdrant filtré par user_id
97
+ search_result = self.client.search(
98
+ collection_name=self.COLLECTION_NAME,
99
+ query_vector=embedding,
100
+ query_filter=models.Filter(
101
+ must=[
102
+ models.FieldCondition(
103
+ key="user_id",
104
+ match=models.MatchValue(value=user_id)
105
+ )
106
+ ]
107
+ ),
108
+ limit=limit
109
+ )
110
+
111
+ memories = [hit.payload["content"] for hit in search_result if hit.score > 0.7]
112
+ return memories
113
+ except Exception as e:
114
+ logger.error(f"Failed to search memory: {e}")
115
+ return []
116
+
117
+ async def consolidate_analysis(self, user_id: str, project_id: str, analysis_result: Any):
118
+ """
119
+ Génère un résumé des points clés d'une analyse et les stocke en mémoire.
120
+ """
121
+ # Note: On utilise K2 pour générer le résumé sémantique si nécessaire
122
+ # Pour le hackathon, on peut extraire les recommandations stratégiques et les gaps clés
123
+
124
+ summary_parts = []
125
+ if hasattr(analysis_result, 'reasoning_summary') and analysis_result.reasoning_summary:
126
+ summary_parts.append(analysis_result.reasoning_summary)
127
+
128
+ if hasattr(analysis_result, 'strategic_recommendations') and analysis_result.strategic_recommendations:
129
+ recs = ". ".join(analysis_result.strategic_recommendations)
130
+ summary_parts.append(f"Strategic Findings: {recs}")
131
+
132
+ if not summary_parts:
133
+ return
134
+
135
+ full_context = "\n".join(summary_parts)
136
+
137
+ # On peut demander à K2 de "condenser" cela en savoir réutilisable
138
+ # Mais pour aller vite, on stocke le bloc tel quel ou par petits morceaux
139
+ await self.save_memory(
140
+ user_id=user_id,
141
+ content=full_context,
142
+ metadata={
143
+ "project_id": project_id,
144
+ "type": "analysis_consolidation"
145
+ }
146
+ )