Skip to content

Commit 461748b

Browse files
abrookinsclaude
andcommitted
Migrate grounding tests to memory strategy system
Replace extract_discrete_memories with get_memory_strategy("discrete"). Remove legacy extraction code and update all contextual grounding tests to use new memory strategy architecture. Fix regex patterns for pronoun detection and add JSON parsing robustness. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent bf480b4 commit 461748b

File tree

6 files changed

+108
-1942
lines changed

6 files changed

+108
-1942
lines changed

agent_memory_server/docket_tasks.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
from agent_memory_server.config import settings
1010
from agent_memory_server.extraction import (
11-
extract_discrete_memories,
1211
extract_memories_with_strategy,
1312
)
1413
from agent_memory_server.long_term_memory import (
@@ -33,7 +32,6 @@
3332
summarize_session,
3433
index_long_term_memories,
3534
compact_long_term_memories,
36-
extract_discrete_memories,
3735
extract_memories_with_strategy,
3836
promote_working_memory_to_long_term,
3937
delete_long_term_memories,

agent_memory_server/extraction.py

Lines changed: 0 additions & 215 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import json
22
import os
3-
from datetime import datetime
43
from typing import TYPE_CHECKING, Any
54

65
import ulid
@@ -215,220 +214,6 @@ async def handle_extraction(text: str) -> tuple[list[str], list[str]]:
215214
return topics, entities
216215

217216

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-
MULTI-ENTITY HANDLING:
260-
When multiple people are mentioned in the conversation, you MUST extract separate memories for each distinct person and their activities. Do NOT omit any person who is mentioned by name.
261-
262-
Example: If the conversation mentions "John and Sarah are working on a project. He handles backend, she handles frontend. His Python skills complement her React expertise."
263-
You should extract:
264-
- "John works on the backend of a project and has Python skills"
265-
- "Sarah works on the frontend of a project and has React expertise"
266-
- "John and Sarah collaborate effectively on a project"
267-
268-
For each memory, return a JSON object with the following fields:
269-
- type: str -- The memory type, either "episodic" or "semantic"
270-
- text: str -- The actual information to store (with all contextual references grounded)
271-
- topics: list[str] -- The topics of the memory (top {top_k_topics})
272-
- entities: list[str] -- The entities of the memory
273-
274-
Return a list of memories, for example:
275-
{{
276-
"memories": [
277-
{{
278-
"type": "semantic",
279-
"text": "User prefers window seats",
280-
"topics": ["travel", "airline"],
281-
"entities": ["User", "window seat"],
282-
}},
283-
{{
284-
"type": "episodic",
285-
"text": "John works on backend development and has Python programming skills",
286-
"topics": ["programming", "backend"],
287-
"entities": ["John", "Python"],
288-
}},
289-
{{
290-
"type": "episodic",
291-
"text": "Sarah works on frontend integration and has React expertise",
292-
"topics": ["programming", "frontend"],
293-
"entities": ["Sarah", "React"],
294-
}},
295-
]
296-
}}
297-
298-
IMPORTANT RULES:
299-
1. Only extract information that would be genuinely useful for future interactions.
300-
2. Do not extract procedural knowledge - that is handled by the system's built-in tools and prompts.
301-
3. You are a large language model - do not extract facts that you already know.
302-
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.
303-
5. MANDATORY: Replace every instance of "he/she/they/him/her/them/his/hers/theirs" with the actual person's name.
304-
6. MANDATORY: Replace possessive pronouns like "her experience" with "User's experience" (if "her" refers to the user).
305-
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.
306-
8. CRITICAL: When multiple people are mentioned by name, extract memories for EACH person individually. Do not ignore any named person.
307-
308-
Message:
309-
{message}
310-
311-
STEP-BY-STEP PROCESS:
312-
1. First, identify all people mentioned by name in the conversation
313-
2. Identify all pronouns in the text: he, she, they, him, her, them, his, hers, theirs
314-
3. Determine what person each pronoun refers to based on the context
315-
4. Replace every single pronoun with the actual person's name
316-
5. Extract memories for EACH named person and their activities/attributes
317-
6. Extract any additional collaborative or relational memories
318-
7. Ensure NO pronouns remain unresolved
319-
320-
Extracted memories:
321-
"""
322-
323-
324-
async def extract_discrete_memories(
325-
memories: list[MemoryRecord] | None = None,
326-
deduplicate: bool = True,
327-
):
328-
"""
329-
Extract episodic and semantic memories from text using an LLM.
330-
"""
331-
client = await get_model_client(settings.generation_model)
332-
333-
# Use vectorstore adapter to find messages that need discrete memory extraction
334-
# Local imports to avoid circular dependencies:
335-
# long_term_memory imports from extraction, so we import locally here
336-
from agent_memory_server.long_term_memory import index_long_term_memories
337-
from agent_memory_server.vectorstore_factory import get_vectorstore_adapter
338-
339-
adapter = await get_vectorstore_adapter()
340-
341-
if not memories:
342-
# If no memories are provided, search for any messages in long-term memory
343-
# that haven't been processed for discrete extraction
344-
345-
memories = []
346-
offset = 0
347-
while True:
348-
search_result = await adapter.search_memories(
349-
query="", # Empty query to get all messages
350-
memory_type=MemoryType(eq="message"),
351-
discrete_memory_extracted=DiscreteMemoryExtracted(eq="f"),
352-
limit=25,
353-
offset=offset,
354-
)
355-
356-
logger.info(
357-
f"Found {len(search_result.memories)} memories to extract: {[m.id for m in search_result.memories]}"
358-
)
359-
360-
memories += search_result.memories
361-
362-
if len(search_result.memories) < 25:
363-
break
364-
365-
offset += 25
366-
367-
new_discrete_memories = []
368-
updated_memories = []
369-
370-
for memory in memories:
371-
if not memory or not memory.text:
372-
logger.info(f"Deleting memory with no text: {memory}")
373-
await adapter.delete_memories([memory.id])
374-
continue
375-
376-
async for attempt in AsyncRetrying(stop=stop_after_attempt(3)):
377-
with attempt:
378-
response = await client.create_chat_completion(
379-
model=settings.generation_model,
380-
prompt=DISCRETE_EXTRACTION_PROMPT.format(
381-
message=memory.text,
382-
top_k_topics=settings.top_k_topics,
383-
current_datetime=datetime.now().strftime(
384-
"%A, %B %d, %Y at %I:%M %p %Z"
385-
),
386-
),
387-
response_format={"type": "json_object"},
388-
)
389-
try:
390-
new_message = json.loads(response.choices[0].message.content)
391-
except json.JSONDecodeError:
392-
logger.error(
393-
f"Error decoding JSON: {response.choices[0].message.content}"
394-
)
395-
raise
396-
try:
397-
assert isinstance(new_message, dict)
398-
assert isinstance(new_message["memories"], list)
399-
except AssertionError:
400-
logger.error(
401-
f"Invalid response format: {response.choices[0].message.content}"
402-
)
403-
raise
404-
new_discrete_memories.extend(new_message["memories"])
405-
406-
# Update the memory to mark it as processed using the vectorstore adapter
407-
updated_memory = memory.model_copy(update={"discrete_memory_extracted": "t"})
408-
updated_memories.append(updated_memory)
409-
410-
if updated_memories:
411-
await adapter.update_memories(updated_memories)
412-
413-
if new_discrete_memories:
414-
long_term_memories = [
415-
MemoryRecord(
416-
id=str(ulid.ULID()),
417-
text=new_memory["text"],
418-
memory_type=new_memory.get("type", "episodic"),
419-
topics=new_memory.get("topics", []),
420-
entities=new_memory.get("entities", []),
421-
discrete_memory_extracted="t",
422-
)
423-
for new_memory in new_discrete_memories
424-
]
425-
426-
await index_long_term_memories(
427-
long_term_memories,
428-
deduplicate=deduplicate,
429-
)
430-
431-
432217
async def extract_memories_with_strategy(
433218
memories: list[MemoryRecord] | None = None,
434219
deduplicate: bool = True,

0 commit comments

Comments
 (0)