|
1 | 1 | import json |
2 | 2 | import os |
3 | | -from datetime import datetime |
4 | 3 | from typing import TYPE_CHECKING, Any |
5 | 4 |
|
6 | 5 | import ulid |
@@ -215,201 +214,6 @@ async def handle_extraction(text: str) -> tuple[list[str], list[str]]: |
215 | 214 | return topics, entities |
216 | 215 |
|
217 | 216 |
|
218 | | -DISCRETE_EXTRACTION_PROMPT = """ |
219 | | - You are a long-memory manager. Your job is to analyze text and extract |
220 | | - information that might be useful in future conversations with users. |
221 | | -
|
222 | | - CURRENT CONTEXT: |
223 | | - Current date and time: {current_datetime} |
224 | | -
|
225 | | - Extract two types of memories: |
226 | | - 1. EPISODIC: Personal experiences specific to a user or agent. |
227 | | - Example: "User prefers window seats" or "User had a bad experience in Paris" |
228 | | -
|
229 | | - 2. SEMANTIC: User preferences and general knowledge outside of your training data. |
230 | | - Example: "Trek discontinued the Trek 520 steel touring bike in 2023" |
231 | | -
|
232 | | - CONTEXTUAL GROUNDING REQUIREMENTS: |
233 | | - When extracting memories, you must resolve all contextual references to their concrete referents: |
234 | | -
|
235 | | - 1. PRONOUNS: Replace ALL pronouns (he/she/they/him/her/them/his/hers/theirs) with the actual person's name, EXCEPT for the application user, who must always be referred to as "User". |
236 | | - - "He loves coffee" → "User loves coffee" (if "he" refers to the user) |
237 | | - - "I told her about it" → "User told colleague about it" (if "her" refers to a colleague) |
238 | | - - "Her experience is valuable" → "User's experience is valuable" (if "her" refers to the user) |
239 | | - - "My name is Alice and I prefer tea" → "User prefers tea" (do NOT store the application user's given name in text) |
240 | | - - NEVER leave pronouns unresolved - always replace with the specific person's name |
241 | | -
|
242 | | - 2. TEMPORAL REFERENCES: Convert relative time expressions to absolute dates/times using the current datetime provided above |
243 | | - - "yesterday" → specific date (e.g., "March 15, 2025" if current date is March 16, 2025) |
244 | | - - "last year" → specific year (e.g., "2024" if current year is 2025) |
245 | | - - "three months ago" → specific month/year (e.g., "December 2024" if current date is March 2025) |
246 | | - - "next week" → specific date range (e.g., "December 22-28, 2024" if current date is December 15, 2024) |
247 | | - - "tomorrow" → specific date (e.g., "December 16, 2024" if current date is December 15, 2024) |
248 | | - - "last month" → specific month/year (e.g., "November 2024" if current date is December 2024) |
249 | | -
|
250 | | - 3. SPATIAL REFERENCES: Resolve place references to specific locations |
251 | | - - "there" → "San Francisco" (if referring to San Francisco) |
252 | | - - "that place" → "Chez Panisse restaurant" (if referring to that restaurant) |
253 | | - - "here" → "the office" (if referring to the office) |
254 | | -
|
255 | | - 4. DEFINITE REFERENCES: Resolve definite articles to specific entities |
256 | | - - "the meeting" → "the quarterly planning meeting" |
257 | | - - "the document" → "the budget proposal document" |
258 | | -
|
259 | | - For each memory, return a JSON object with the following fields: |
260 | | - - type: str -- The memory type, either "episodic" or "semantic" |
261 | | - - text: str -- The actual information to store (with all contextual references grounded) |
262 | | - - topics: list[str] -- The topics of the memory (top {top_k_topics}) |
263 | | - - entities: list[str] -- The entities of the memory |
264 | | -
|
265 | | - Return a list of memories, for example: |
266 | | - {{ |
267 | | - "memories": [ |
268 | | - {{ |
269 | | - "type": "semantic", |
270 | | - "text": "User prefers window seats", |
271 | | - "topics": ["travel", "airline"], |
272 | | - "entities": ["User", "window seat"], |
273 | | - }}, |
274 | | - {{ |
275 | | - "type": "episodic", |
276 | | - "text": "Trek discontinued the Trek 520 steel touring bike in 2023", |
277 | | - "topics": ["travel", "bicycle"], |
278 | | - "entities": ["Trek", "Trek 520 steel touring bike"], |
279 | | - }}, |
280 | | - ] |
281 | | - }} |
282 | | -
|
283 | | - IMPORTANT RULES: |
284 | | - 1. Only extract information that would be genuinely useful for future interactions. |
285 | | - 2. Do not extract procedural knowledge - that is handled by the system's built-in tools and prompts. |
286 | | - 3. You are a large language model - do not extract facts that you already know. |
287 | | - 4. CRITICAL: ALWAYS ground ALL contextual references - never leave ANY pronouns, relative times, or vague place references unresolved. For the application user, always use "User" instead of their given name to avoid stale naming if they change their profile name later. |
288 | | - 5. MANDATORY: Replace every instance of "he/she/they/him/her/them/his/hers/theirs" with the actual person's name. |
289 | | - 6. MANDATORY: Replace possessive pronouns like "her experience" with "User's experience" (if "her" refers to the user). |
290 | | - 7. If you cannot determine what a contextual reference refers to, either omit that memory or use generic terms like "someone" instead of ungrounded pronouns. |
291 | | -
|
292 | | - Message: |
293 | | - {message} |
294 | | -
|
295 | | - STEP-BY-STEP PROCESS: |
296 | | - 1. First, identify all pronouns in the text: he, she, they, him, her, them, his, hers, theirs |
297 | | - 2. Determine what person each pronoun refers to based on the context |
298 | | - 3. Replace every single pronoun with the actual person's name |
299 | | - 4. Extract the grounded memories with NO pronouns remaining |
300 | | -
|
301 | | - Extracted memories: |
302 | | - """ |
303 | | - |
304 | | - |
305 | | -async def extract_discrete_memories( |
306 | | - memories: list[MemoryRecord] | None = None, |
307 | | - deduplicate: bool = True, |
308 | | -): |
309 | | - """ |
310 | | - Extract episodic and semantic memories from text using an LLM. |
311 | | - """ |
312 | | - client = await get_model_client(settings.generation_model) |
313 | | - |
314 | | - # Use vectorstore adapter to find messages that need discrete memory extraction |
315 | | - # Local imports to avoid circular dependencies: |
316 | | - # long_term_memory imports from extraction, so we import locally here |
317 | | - from agent_memory_server.long_term_memory import index_long_term_memories |
318 | | - from agent_memory_server.vectorstore_factory import get_vectorstore_adapter |
319 | | - |
320 | | - adapter = await get_vectorstore_adapter() |
321 | | - |
322 | | - if not memories: |
323 | | - # If no memories are provided, search for any messages in long-term memory |
324 | | - # that haven't been processed for discrete extraction |
325 | | - |
326 | | - memories = [] |
327 | | - offset = 0 |
328 | | - while True: |
329 | | - search_result = await adapter.search_memories( |
330 | | - query="", # Empty query to get all messages |
331 | | - memory_type=MemoryType(eq="message"), |
332 | | - discrete_memory_extracted=DiscreteMemoryExtracted(eq="f"), |
333 | | - limit=25, |
334 | | - offset=offset, |
335 | | - ) |
336 | | - |
337 | | - logger.info( |
338 | | - f"Found {len(search_result.memories)} memories to extract: {[m.id for m in search_result.memories]}" |
339 | | - ) |
340 | | - |
341 | | - memories += search_result.memories |
342 | | - |
343 | | - if len(search_result.memories) < 25: |
344 | | - break |
345 | | - |
346 | | - offset += 25 |
347 | | - |
348 | | - new_discrete_memories = [] |
349 | | - updated_memories = [] |
350 | | - |
351 | | - for memory in memories: |
352 | | - if not memory or not memory.text: |
353 | | - logger.info(f"Deleting memory with no text: {memory}") |
354 | | - await adapter.delete_memories([memory.id]) |
355 | | - continue |
356 | | - |
357 | | - async for attempt in AsyncRetrying(stop=stop_after_attempt(3)): |
358 | | - with attempt: |
359 | | - response = await client.create_chat_completion( |
360 | | - model=settings.generation_model, |
361 | | - prompt=DISCRETE_EXTRACTION_PROMPT.format( |
362 | | - message=memory.text, |
363 | | - top_k_topics=settings.top_k_topics, |
364 | | - current_datetime=datetime.now().strftime( |
365 | | - "%A, %B %d, %Y at %I:%M %p %Z" |
366 | | - ), |
367 | | - ), |
368 | | - response_format={"type": "json_object"}, |
369 | | - ) |
370 | | - try: |
371 | | - new_message = json.loads(response.choices[0].message.content) |
372 | | - except json.JSONDecodeError: |
373 | | - logger.error( |
374 | | - f"Error decoding JSON: {response.choices[0].message.content}" |
375 | | - ) |
376 | | - raise |
377 | | - try: |
378 | | - assert isinstance(new_message, dict) |
379 | | - assert isinstance(new_message["memories"], list) |
380 | | - except AssertionError: |
381 | | - logger.error( |
382 | | - f"Invalid response format: {response.choices[0].message.content}" |
383 | | - ) |
384 | | - raise |
385 | | - new_discrete_memories.extend(new_message["memories"]) |
386 | | - |
387 | | - # Update the memory to mark it as processed using the vectorstore adapter |
388 | | - updated_memory = memory.model_copy(update={"discrete_memory_extracted": "t"}) |
389 | | - updated_memories.append(updated_memory) |
390 | | - |
391 | | - if updated_memories: |
392 | | - await adapter.update_memories(updated_memories) |
393 | | - |
394 | | - if new_discrete_memories: |
395 | | - long_term_memories = [ |
396 | | - MemoryRecord( |
397 | | - id=str(ulid.ULID()), |
398 | | - text=new_memory["text"], |
399 | | - memory_type=new_memory.get("type", "episodic"), |
400 | | - topics=new_memory.get("topics", []), |
401 | | - entities=new_memory.get("entities", []), |
402 | | - discrete_memory_extracted="t", |
403 | | - ) |
404 | | - for new_memory in new_discrete_memories |
405 | | - ] |
406 | | - |
407 | | - await index_long_term_memories( |
408 | | - long_term_memories, |
409 | | - deduplicate=deduplicate, |
410 | | - ) |
411 | | - |
412 | | - |
413 | 217 | async def extract_memories_with_strategy( |
414 | 218 | memories: list[MemoryRecord] | None = None, |
415 | 219 | deduplicate: bool = True, |
|
0 commit comments