Skip to content

Commit 3e975a5

Browse files
authored
Merge pull request #57 from redis/feature/flaky-grounding-test
Improve multi-entity contextual grounding in memory extraction
2 parents 4660b18 + 461748b commit 3e975a5

File tree

9 files changed

+262
-1908
lines changed

9 files changed

+262
-1908
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 & 196 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,201 +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-
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-
413217
async def extract_memories_with_strategy(
414218
memories: list[MemoryRecord] | None = None,
415219
deduplicate: bool = True,

0 commit comments

Comments
 (0)