improve moniitoring

This commit is contained in:
Nicolò Boschi 2025-11-07 16:12:59 +01:00
parent bff72ca6ae
commit cfafd2fb47
9 changed files with 17726 additions and 2586 deletions

View file

@ -77,10 +77,12 @@ class LoComoDataset(BenchmarkDataset):
# Add to batch
session_content = "\n".join(session_parts)
document_id = f"{item['sample_id']}_{session_key}"
batch_contents.append({
"content": session_content,
"context": f"Conversation session between {speaker_a} and {speaker_b} (conversation {item['sample_id']} session {session_key})",
"event_date": session_date
"event_date": session_date,
"document_id": document_id
})
return batch_contents

View file

@ -83,10 +83,13 @@ class LongMemEvalDataset(BenchmarkDataset):
# Add session to batch
if session_content_parts:
session_content = "\n".join(session_content_parts)
question_id = item.get("question_id", "unknown")
document_id = f"{question_id}_{session_id}"
batch_contents.append({
"content": session_content,
"context": f"Session {session_id}",
"event_date": session_date
"event_date": session_date,
"document_id": document_id
})
return batch_contents
@ -127,6 +130,10 @@ class LongMemEvalDataset(BenchmarkDataset):
return datetime.now(timezone.utc)
class QuestionAnswer(pydantic.BaseModel):
answer: str
reasoning: str
class LongMemEvalAnswerGenerator(LLMAnswerGenerator):
"""LongMemEval-specific answer generator using configurable LLM provider."""
@ -142,43 +149,82 @@ class LongMemEvalAnswerGenerator(LLMAnswerGenerator):
memories: List[Dict[str, Any]]
) -> Tuple[str, str, Optional[List[Dict[str, Any]]]]:
"""
Generate answer from retrieved memories using OpenAI.
Generate answer from retrieved memories using Groq.
Returns:
Tuple of (answer, reasoning, retrieved_memories_override)
Tuple of (answer, reasoning, None)
- None indicates to use the memories passed in
"""
# Format memories as context
# Format context
context_parts = []
for i, mem in enumerate(memories, 1):
context_parts.append(f"[Memory {i}] {mem['text']}")
for result in memories:
context_parts.append({"text": result.get("text"), "context": result.get("context"),
"event_date": result.get("event_date")})
context = "\n".join(context_parts) if context_parts else "No relevant memories found."
context = json.dumps(context_parts)
prompt = f"""You are a helpful assistant. Based on the following memories from past conversations, answer the question.
# Use LLM to generate answer
try:
answer_obj = await self.llm_config.call(
messages=[
{
"role": "system",
"content": "You are a helpful expert assistant answering questions from lme_experiment users based on the provided context."
},
{
"role": "user",
"content": f"""
# CONTEXT:
You have access to facts and entities from a conversation.
# INSTRUCTIONS:
1. Carefully analyze all provided memories
2. Pay special attention to the timestamps to determine the answer
3. If the question asks about a specific event or fact, look for direct evidence in the memories
4. If the memories contain contradictory information, prioritize the most recent memory
5. Always convert relative time references to specific dates, months, or years.
6. Be as specific as possible when talking about people, places, and events
7. Timestamps in memories represent the actual time the event occurred, not the time the event was mentioned in a message.
Clarification:
When interpreting memories, use the timestamp to determine when the described event happened, not when someone talked about the event.
Example:
Memory: (2023-03-15T16:33:00Z) I went to the vet yesterday.
Question: What day did I go to the vet?
Correct Answer: March 15, 2023
Explanation:
Even though the phrase says "yesterday," the timestamp shows the event was recorded as happening on March 15th. Therefore, the actual vet visit happened on that date, regardless of the word "yesterday" in the text.
# APPROACH (Think step by step):
1. First, examine all memories that contain information related to the question
2. Examine the timestamps and content of these memories carefully
3. Look for explicit mentions of dates, times, locations, or events that answer the question
4. If the answer requires calculation (e.g., converting relative time references), show your work
5. Formulate a precise, concise answer based solely on the evidence in the memories
6. Double-check that your answer directly addresses the question asked
7. Ensure your final answer is specific and avoids vague time references
8. If you're not exactly sure, still try to attempt an answer. Sometimes the terms are sligtly different from the question, so it's better to try with the current evidence than just say you don't know.
9. Say that you cannot answer if no evidence is related to the question.
Context:
Memories:
{context}
Question: {question}
Answer:
Instructions:
- Answer based ONLY on the provided memories
- If the memories don't contain the answer, say "I don't have enough information to answer this question"
- Be concise and direct
- If asked to abstain (e.g., for unanswerable questions), explicitly say you cannot answer
Answer:"""
try:
answer = await self.llm_config.call(
messages=[{"role": "user", "content": prompt}],
scope="memory",
temperature=0.0,
max_tokens=300
"""
}
],
response_format=QuestionAnswer,
scope="memory"
)
return answer.strip(), "", None # LongMemEval doesn't use reasoning or override memories
return answer_obj.answer, answer_obj.reasoning, None
except Exception as e:
return f"Error generating answer: {str(e)}", "", None
return f"Error generating answer: {str(e)}", "Error occurred during answer generation.", None
async def run_benchmark(

File diff suppressed because it is too large Load diff

View file

@ -233,6 +233,88 @@ class GraphDataResponse(BaseModel):
}
class ListMemoryUnitsResponse(BaseModel):
"""Response model for list memory units endpoint."""
items: List[Dict[str, Any]]
total: int
limit: int
offset: int
class Config:
json_schema_extra = {
"example": {
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"text": "Alice works at Google on the AI team",
"context": "Work conversation",
"date": "2024-01-15T10:30:00Z",
"fact_type": "world",
"entities": "Alice (PERSON), Google (ORGANIZATION)"
}
],
"total": 150,
"limit": 100,
"offset": 0
}
}
class ListDocumentsResponse(BaseModel):
"""Response model for list documents endpoint."""
items: List[Dict[str, Any]]
total: int
limit: int
offset: int
class Config:
json_schema_extra = {
"example": {
"items": [
{
"id": "session_1",
"agent_id": "user123",
"content_hash": "abc123",
"metadata": {"source": "conversation"},
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z",
"text_length": 5420,
"memory_unit_count": 15
}
],
"total": 50,
"limit": 100,
"offset": 0
}
}
class DocumentResponse(BaseModel):
"""Response model for get document endpoint."""
id: str
agent_id: str
original_text: str
content_hash: Optional[str]
metadata: Dict[str, Any]
created_at: str
updated_at: str
memory_unit_count: int
class Config:
json_schema_extra = {
"example": {
"id": "session_1",
"agent_id": "user123",
"original_text": "Full document text here...",
"content_hash": "abc123",
"metadata": {"source": "conversation"},
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z",
"memory_unit_count": 15
}
}
def create_app(memory: TemporalSemanticMemory) -> FastAPI:
"""
Create and configure the FastAPI application.
@ -315,7 +397,7 @@ def _register_routes(app: FastAPI):
response_model=GraphDataResponse,
tags=["Visualization"],
summary="Get memory graph data",
description="Retrieve graph data for visualization, optionally filtered by agent_id and fact_type (world/agent/opinion)"
description="Retrieve graph data for visualization, optionally filtered by agent_id and fact_type (world/agent/opinion). Limited to 1000 most recent items."
)
async def api_graph(
agent_id: Optional[str] = None,
@ -332,6 +414,46 @@ def _register_routes(app: FastAPI):
raise HTTPException(status_code=500, detail=str(e))
@app.get(
"/api/list",
response_model=ListMemoryUnitsResponse,
tags=["Visualization"],
summary="List memory units",
description="List memory units with pagination and optional full-text search. Supports filtering by agent_id and fact_type."
)
async def api_list(
agent_id: Optional[str] = None,
fact_type: Optional[str] = None,
q: Optional[str] = None,
limit: int = 100,
offset: int = 0
):
"""
List memory units for table view with optional full-text search.
Args:
agent_id: Filter by agent ID
fact_type: Filter by fact type (world, agent, opinion)
q: Search query for full-text search (searches text and context)
limit: Maximum number of results (default: 100)
offset: Offset for pagination (default: 0)
"""
try:
data = await app.state.memory.list_memory_units(
agent_id=agent_id,
fact_type=fact_type,
search_query=q,
limit=limit,
offset=offset
)
return data
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
print(f"Error in /api/list: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.post(
"/api/search",
response_model=SearchResponse,
@ -475,16 +597,30 @@ def _register_routes(app: FastAPI):
agent_id
)
# Get pending operations count
pending_ops_result = await conn.fetchrow(
# Get pending and failed operations counts
ops_stats = await conn.fetch(
"""
SELECT status, COUNT(*) as count
FROM async_operations
WHERE agent_id = $1
GROUP BY status
""",
agent_id
)
ops_by_status = {row['status']: row['count'] for row in ops_stats}
pending_operations = ops_by_status.get('pending', 0)
failed_operations = ops_by_status.get('failed', 0)
# Get document count
doc_count_result = await conn.fetchrow(
"""
SELECT COUNT(*) as count
FROM async_operations
FROM documents
WHERE agent_id = $1
""",
agent_id
)
pending_operations = pending_ops_result['count'] if pending_ops_result else 0
total_documents = doc_count_result['count'] if doc_count_result else 0
# Format results
nodes_by_type = {row['fact_type']: row['count'] for row in node_stats}
@ -497,9 +633,11 @@ def _register_routes(app: FastAPI):
"agent_id": agent_id,
"total_nodes": total_nodes,
"total_links": total_links,
"total_documents": total_documents,
"nodes_by_type": nodes_by_type,
"links_by_type": links_by_type,
"pending_operations": pending_operations
"pending_operations": pending_operations,
"failed_operations": failed_operations
}
except Exception as e:
@ -508,6 +646,75 @@ def _register_routes(app: FastAPI):
print(f"Error in /api/stats/{agent_id}: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.get(
"/api/documents",
response_model=ListDocumentsResponse,
tags=["Documents"],
summary="List documents",
description="List documents with pagination and optional search. Documents are the source content from which memory units are extracted."
)
async def api_list_documents(
agent_id: Optional[str] = None,
q: Optional[str] = None,
limit: int = 100,
offset: int = 0
):
"""
List documents for an agent with optional search.
Args:
agent_id: Filter by agent ID
q: Search query (searches document ID and metadata)
limit: Maximum number of results (default: 100)
offset: Offset for pagination (default: 0)
"""
try:
data = await app.state.memory.list_documents(
agent_id=agent_id,
search_query=q,
limit=limit,
offset=offset
)
return data
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
print(f"Error in /api/documents: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.get(
"/api/documents/{document_id}",
response_model=DocumentResponse,
tags=["Documents"],
summary="Get document details",
description="Get a specific document including its original text"
)
async def api_get_document(
document_id: str,
agent_id: str
):
"""
Get a specific document with its original text.
Args:
document_id: Document ID
agent_id: Agent ID (required as query parameter)
"""
try:
document = await app.state.memory.get_document(document_id, agent_id)
if not document:
raise HTTPException(status_code=404, detail="Document not found")
return document
except HTTPException:
raise
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
print(f"Error in /api/documents/{document_id}: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.post(
"/api/memories/batch",
response_model=BatchPutResponse,
@ -667,6 +874,98 @@ def _register_routes(app: FastAPI):
raise HTTPException(status_code=500, detail=str(e))
@app.get(
"/api/operations/{agent_id}",
tags=["Memory Storage"],
summary="List async operations",
description="Get a list of all async operations (pending and failed) for a specific agent, including error messages for failed operations"
)
async def api_list_operations(agent_id: str):
"""List all async operations (pending and failed) for an agent."""
try:
pool = await app.state.memory._get_pool()
async with pool.acquire() as conn:
operations = await conn.fetch(
"""
SELECT id, agent_id, task_type, items_count, document_id, created_at, status, error_message
FROM async_operations
WHERE agent_id = $1
ORDER BY created_at ASC
""",
agent_id
)
return {
"agent_id": agent_id,
"operations": [
{
"id": str(row['id']),
"task_type": row['task_type'],
"items_count": row['items_count'],
"document_id": row['document_id'],
"created_at": row['created_at'].isoformat(),
"status": row['status'],
"error_message": row['error_message']
}
for row in operations
]
}
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
print(f"Error in /api/operations/{agent_id}: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.delete(
"/api/operations/{operation_id}",
tags=["Memory Storage"],
summary="Cancel a pending async operation",
description="Cancel a pending async operation by removing it from the queue"
)
async def api_cancel_operation(operation_id: str):
"""Cancel a pending async operation."""
try:
# Validate UUID format
try:
op_uuid = uuid.UUID(operation_id)
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid operation_id format: {operation_id}")
pool = await app.state.memory._get_pool()
async with pool.acquire() as conn:
# Check if operation exists
result = await conn.fetchrow(
"SELECT agent_id FROM async_operations WHERE id = $1",
op_uuid
)
if not result:
raise HTTPException(status_code=404, detail=f"Operation {operation_id} not found")
# Delete the operation
await conn.execute(
"DELETE FROM async_operations WHERE id = $1",
op_uuid
)
return {
"success": True,
"message": f"Operation {operation_id} cancelled",
"operation_id": operation_id,
"agent_id": result['agent_id']
}
except HTTPException:
raise
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
print(f"Error in /api/operations/{operation_id}: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.delete(
"/api/memory/{unit_id}",
tags=["Memory Storage"],

View file

@ -107,287 +107,189 @@ async def _extract_facts_from_chunk(
# Format event_date for the prompt
event_date_str = event_date.strftime("%Y-%m-%dT%H:%M:%SZ")
prompt = f"""You are extracting facts from text for an AI memory system. Each fact will be stored and retrieved later.
prompt = f"""You are extracting comprehensive, narrative facts from conversations for an AI memory system.
## CONTEXT INFORMATION
- Current reference date/time: {event_date_str}
- Context: {context if context else 'no context provided'}
## CRITICAL: Facts must be DETAILED, COMPREHENSIVE, and CONTEXT-RICH
## CORE PRINCIPLE: Extract FEWER, MORE COMPREHENSIVE Facts
**GOAL**: Extract 2-5 comprehensive facts per conversation, NOT dozens of small fragments.
Each fact should:
1. Be SELF-CONTAINED - readable without the original context
2. Include ALL relevant details: WHO, WHAT, WHERE, WHEN, WHY, HOW
3. **CRITICAL: ALWAYS include the SUBJECT (who is doing/saying/experiencing)**
4. **Preserve ALL context**: photos/images, "new" things, visual elements, medium of communication
5. Preserve specific names, dates, numbers, locations, relationships, modifiers (new, old, first, etc.)
6. Resolve pronouns to actual names/entities (I speaker name, their possessor name)
7. **CRITICAL: Preserve possessive relationships** (their kids whose kids, his car whose car)
8. Capture nuances, reasons, causes, implications, and surrounding context
1. **CAPTURE ENTIRE CONVERSATIONS OR EXCHANGES** - Include the full back-and-forth discussion
2. **BE NARRATIVE AND COMPREHENSIVE** - Tell the complete story with all context
3. **BE SELF-CONTAINED** - Readable without the original text
4. **INCLUDE ALL PARTICIPANTS** - WHO said/did WHAT, with their reasoning
5. **PRESERVE THE FLOW** - Keep related exchanges together in one fact
**COMMON MISTAKES TO AVOID:**
- "The kids were excited" Missing WHO the kids belong to
- "Melanie's kids were excited" or "Melanie took her kids who were excited"
- "Nate chose his hair color because it's bright and bold" Missing that it's NEW and in a PHOTO
- "Nate shared a photo of his new hair color, which he chose because it's bright and bold"
- "Alice started a job at Google" Missing that it's NEW
- "Alice started a new job at Google"
## HOW TO COMBINE INFORMATION INTO COMPREHENSIVE FACTS
## TEMPORAL INFORMATION (VERY IMPORTANT)
For each fact, extract the ABSOLUTE date/time when it occurred:
- If text mentions ABSOLUTE dates ("on March 15, 2024", "last Tuesday"), use that date
- If text mentions RELATIVE times ("yesterday", "last week", "last month", "last year", "this morning", "3 days ago", "next year"), calculate the absolute date using the reference date above
- **CRITICAL**: Transform relative temporal expressions in the FACT TEXT to absolute context:
- "last year" "in [calculated year]" (e.g., if reference is 2023, "last year" becomes "in 2022")
- "last month" "in [month name] [year]" (e.g., if reference is March 2024, "last month" becomes "in February 2024")
- "last week" "week of [date]" or keep as "last week" with absolute date field
- "yesterday" can stay as "yesterday" with absolute date field
- If NO specific time is mentioned, use the reference date
- Always output dates in ISO format: YYYY-MM-DDTHH:MM:SSZ
** GOOD APPROACH**: One comprehensive fact capturing the entire discussion
"Alice and Bob discussed playlist names for the summer party. Bob suggested 'Summer Vibes' because it's catchy and seasonal. Alice liked it but wanted something more unique. They considered 'Sunset Sessions' and 'Beach Beats', with Alice favoring 'Beach Beats' for its playful tone. They ultimately decided on 'Beach Beats' as the final name."
Examples of date extraction and fact text transformation:
- Reference: 2024-03-20T10:00:00Z
- "Yesterday I went hiking" fact: "Yesterday I went hiking", date: 2024-03-19T10:00:00Z
- "Last week I joined Google" fact: "Last week I joined Google", date: 2024-03-13T10:00:00Z (approximately)
- "Last year we visited Paris" fact: "In 2023 we visited Paris", date: 2023-03-20T10:00:00Z
- "Last month I started a new job" fact: "In February 2024 I started a new job", date: 2024-02-20T10:00:00Z
- "This morning I had coffee" fact: "This morning I had coffee", date: 2024-03-20T08:00:00Z
- "I work at Google" (no time mentioned) date: 2024-03-20T10:00:00Z (use reference)
** BAD APPROACH**: Multiple fragmented facts
- "Bob suggested Summer Vibes"
- "Alice wanted something unique"
- "They considered Sunset Sessions"
- "Alice likes Beach Beats"
- "They chose Beach Beats"
## What to EXTRACT (BE EXHAUSTIVE - DO NOT SKIP ANYTHING):
- **Biographical information (CRITICAL - NEVER MISS)**:
- Origins: home country, birthplace, where someone is from ("my home country Sweden" = Caroline is from Sweden)
- Current location: where they live now
- Jobs, roles, backgrounds, experiences, skills
- Family background, heritage, cultural identity
- Education, training, certifications
- **Events (NEVER MISS THESE)**:
- ANY action that happened (went, did, attended, joined, started, finished, etc.)
- Photos, images, videos shared or taken ("here's a photo", "took a picture", "captured")
- Social activities (meetups, gatherings, meals, conversations)
- Achievements, milestones, accomplishments
- Travels, visits, locations visited
- Purchases, acquisitions, creations
- **Identity and personal details**:
- Origins, nationality, home country, roots
- Cultural background, heritage
- Family connections (grandmother from X, parents in Y)
- **Opinions and beliefs**: who believes what and why
- **Recommendations and advice**: specific suggestions with reasoning
- **Descriptions**: detailed explanations of how things work
- **Social relationships and nicknames (CRITICAL - ALWAYS EXTRACT)**:
- Nicknames: how different people refer to someone ("Andrey calls Joanne 'Jo'", "Everyone calls him Bobby")
- Terms of address: how people address each other (formal names, nicknames, titles)
- Relationship indicators: how people describe their relationships ("considers X as a mentor", "refers to Y as their best friend")
- Social dynamics: who knows whom, who interacts with whom
- Even if not an "event", these are FACTS about social relationships
- Extract BOTH the person using the name AND the person being referred to
- **Relationships**: connections between people, organizations, concepts
- **States and conditions**: current status, ongoing situations
## WHAT TO COMBINE INTO SINGLE FACTS
## CRITICAL: Extract EVERY event with FULL CONTEXT
- "here's a photo of my new car" = shared a photo of their NEW car (preserve "new")
- "I was with friends last week" = meetup/gathering with friends last week
- "sent you that link" = action of sending a link
- "got a new job" = preserve "new" - it's important context
- DO NOT skip events just because they seem minor or casual
- DO NOT drop modifiers like "new", "first", "old", "favorite" - they're critical context
1. **FULL DISCUSSIONS** - Entire conversations about a topic (playlist names, travel plans, decisions)
2. **MULTI-STEP EVENTS** - Connected actions that form a complete story
3. **DECISIONS WITH REASONING** - The full decision-making process and rationale
4. **EXCHANGES WITH CONTEXT** - Questions, answers, and follow-up all together
5. **RELATED ACTIONS** - Multiple related activities in sequence
## What to SKIP (ONLY these):
- Greetings, thank yous, acknowledgments (unless they reveal information)
## ESSENTIAL DETAILS TO PRESERVE IN COMPREHENSIVE FACTS
While combining related content into comprehensive facts, you MUST preserve:
1. **ALL PARTICIPANTS** - Who said/did what
2. **FULL REASONING** - Why decisions were made, motivations, explanations
3. **TEMPORAL CONTEXT** - When things happened (transform relative dates like "last year" "in 2023")
4. **VISUAL/MEDIA ELEMENTS** - Photos, images, videos shared
5. **MODIFIERS** - "new", "first", "old", "favorite" (critical context)
6. **POSSESSIVE RELATIONSHIPS** - "their kids" "Person's kids"
7. **BIOGRAPHICAL DETAILS** - Origins, locations, jobs, family background
8. **SOCIAL DYNAMICS** - Nicknames, how people address each other, relationships
## TEMPORAL INFORMATION
- Extract the ABSOLUTE date/time for when the fact/conversation occurred
- Transform relative times in the fact text:
- "last year" "in [year]" (e.g., "in 2023")
- "last month" "in [month year]" (e.g., "in February 2024")
- Use ISO format for dates: YYYY-MM-DDTHH:MM:SSZ
- If no specific time mentioned, use the reference date
## WHEN TO SPLIT INTO SEPARATE FACTS
Only split into separate facts when topics are COMPLETELY UNRELATED:
- Different subjects discussed (playlist names vs. vacation plans)
- Biographical facts vs. events (where someone is from vs. what they did)
- Different time periods (something last year vs. today)
## What to SKIP
- Greetings, thank yous (unless they reveal information)
- Filler words ("um", "uh", "like")
- Pure reactions without content ("wow", "cool", "nice")
- Incomplete thoughts or sentence fragments with no meaning
- Pure reactions without content ("wow", "cool")
- Incomplete fragments with no meaning
## FACT TYPE CLASSIFICATION (CRITICAL):
For EACH fact, classify it as either 'world' or 'agent':
## FACT TYPE CLASSIFICATION
Classify each fact as either 'world' or 'agent':
- **'world'**: General facts about people, events, conversations (most facts)
- **'agent'**: Only for AI agent's own actions
- **'world'**: General facts about the world, events, people, things that happen
- Examples: "Alice works at Google", "Bob went hiking in Yosemite", "The meeting is scheduled for Monday"
- Most facts will be 'world' type
## ENTITY EXTRACTION
Extract ALL important entities with types:
- **PERSON**: Names of individuals
- **ORG**: Companies, institutions, teams
- **PLACE**: Cities, countries, locations
- **PRODUCT**: Products, tools, technologies
- **CONCEPT**: Topics, projects, subjects
- **OTHER**: Entities that don't fit above
- **'agent'**: Facts specifically about what the AI agent did or actions the agent took
- Examples: "The AI agent helped the user debug their code", "The agent answered a question about Python", "The agent created a new file"
- ONLY use 'agent' if the fact is explicitly about the AI agent's actions
- Conversations with the user where the agent participated are 'agent' type
- Tasks performed BY the agent are 'agent' type
Extract proper nouns and key identifying terms. Skip pronouns and generic terms.
When in doubt, classify as 'world'.
## EXAMPLES - COMPREHENSIVE VS FRAGMENTED FACTS:
## ENTITY EXTRACTION (CRITICAL):
For EACH fact, extract ALL important entities mentioned with their types:
- **PERSON**: Names of individuals (Alice, Bob, Dr. Smith)
- **ORG**: Companies, institutions, teams (Google, MIT, AI Team)
- **PLACE**: Cities, countries, locations, venues (Mountain View, Yosemite, The Coffee Shop)
- **PRODUCT**: Specific products, tools, technologies (iPhone, Python, TensorFlow)
- **CONCEPT**: Important topics, projects, subjects (AI, machine learning, Project Phoenix)
- **OTHER**: Entities that don't fit the above categories (events, time periods, etc.)
### Example 1: Playlist Discussion
**Input Conversation:**
"Alice: Hey, what should we name our summer party playlist?
Bob: How about 'Summer Vibes'? It's catchy and seasonal.
Alice: I like it, but want something more unique.
Bob: What about 'Sunset Sessions' or 'Beach Beats'?
Alice: Ooh, I love 'Beach Beats'! It's playful and fun.
Bob: Perfect, let's go with that!"
Entity extraction rules:
- Use the EXACT form as it appears in the fact (preserve capitalization)
- Assign the correct type to distinguish ambiguous entities (Apple the company = ORG, apple the fruit = PRODUCT/CONCEPT)
- Use OTHER only for entities that truly don't fit the other categories
- Include both full names and commonly used short forms if both appear
- Extract proper nouns and key identifying terms
- Skip generic terms (the, a, some) and pronouns (he, she, they)
- Each entity must have both text and type
** BAD (fragmented into many small facts):**
1. "Alice asked about playlist names"
2. "Bob suggested Summer Vibes"
3. "Alice wanted something unique"
4. "Bob suggested Sunset Sessions"
5. "Bob suggested Beach Beats"
6. "Alice likes Beach Beats"
7. "They chose Beach Beats"
## EXAMPLES of GOOD facts (detailed, comprehensive):
** GOOD (one comprehensive fact):**
"Alice and Bob discussed naming their summer party playlist. Bob suggested 'Summer Vibes' because it's catchy and seasonal, but Alice wanted something more unique. Bob then proposed 'Sunset Sessions' and 'Beach Beats', with Alice favoring 'Beach Beats' for its playful and fun tone. They ultimately decided on 'Beach Beats' as the final name."
- fact_type: "world"
- entities: [{{"text": "Alice", "type": "PERSON"}}, {{"text": "Bob", "type": "PERSON"}}]
Input: "Alice mentioned she works at Google in Mountain View. She joined the AI team last year."
GOOD fact: "Alice works at Google in Mountain View on the AI team, which she joined in 2023"
GOOD fact_type: "world"
GOOD date: 2023-03-20T10:00:00Z (if reference is 2024-03-20, "last year" = 2023)
GOOD entities: [
{{"text": "Alice", "type": "PERSON"}},
{{"text": "Google", "type": "ORG"}},
{{"text": "Mountain View", "type": "PLACE"}},
{{"text": "AI team", "type": "ORG"}}
]
NOTE: "last year" was transformed to "in 2023" in the fact text
### Example 2: Photo Sharing with Context
**Input:**
"Nate: Here's a photo of my new hair!
Friend: Whoa! Why that color?
Nate: I picked bright orange because it's bold and makes me feel confident. Plus it matches my personality!"
Input: "Yesterday Bob went hiking in Yosemite because it helps him clear his mind."
GOOD fact: "Bob went hiking in Yosemite because it helps him clear his mind"
GOOD fact_type: "world"
GOOD date: Reference date minus 1 day
GOOD entities: [
{{"text": "Bob", "type": "PERSON"}},
{{"text": "Yosemite", "type": "PLACE"}}
]
** BAD (loses context):**
"Nate chose orange hair because it's bold"
Input: "Here's a photo of me with my friends taken last week at the beach."
GOOD fact: "Someone shared a photo taken last week showing them with their friends at the beach"
GOOD date: Reference date minus 7 days (last week)
GOOD entities: []
NOTE: Include that it's a PHOTO being shared, when it was taken, and who/what/where is in it
** GOOD (comprehensive with all context):**
"Nate shared a photo of his new bright orange hair. When asked why he chose that color, Nate explained he picked it because it's bold and makes him feel confident, and it matches his personality."
- fact_type: "world"
- entities: [{{"text": "Nate", "type": "PERSON"}}]
- NOTE: Preserves that it's a PHOTO, it's NEW hair, the COLOR, and the FULL reasoning
Input: "Nate: Here's a photo of my new hair! Friend: Why that color? Nate: I picked this color because it's bright and bold"
BAD fact: "Nate chose his hair color because it's bright and bold"
PROBLEM: Missing that it's NEW hair and he SHARED A PHOTO of it!
GOOD fact: "Nate shared a photo of his new hair color, which he chose because it's bright and bold"
GOOD entities: [{{"text": "Nate", "type": "PERSON"}}]
NOTE: Preserve "new" and "photo" - critical context about what happened
### Example 3: Travel Planning
**Input:**
"Sarah: I'm thinking of visiting Japan next spring.
Mike: That's perfect timing for cherry blossoms! You should definitely visit Kyoto.
Sarah: Why Kyoto specifically?
Mike: It has the most beautiful temples and the cherry blossoms there are spectacular. I went in 2019.
Sarah: Sounds amazing! I'll add it to my itinerary."
Input: "I sent you that article about AI last Tuesday."
GOOD fact: "Someone sent an article about AI"
GOOD fact_type: "world"
GOOD date: Calculate last Tuesday from reference date
GOOD entities: [
{{"text": "AI", "type": "CONCEPT"}}
]
** BAD (fragmented):**
1. "Sarah is planning to visit Japan"
2. "Mike suggested Kyoto"
3. "Kyoto has beautiful temples"
4. "Mike visited in 2019"
Input: "The AI agent helped me write a Python script to analyze my data."
GOOD fact: "The AI agent helped someone write a Python script to analyze their data"
GOOD fact_type: "agent"
GOOD date: Reference date (no specific time mentioned)
GOOD entities: [
{{"text": "Python", "type": "PRODUCT"}},
{{"text": "data analysis", "type": "CONCEPT"}}
]
** GOOD (comprehensive conversation):**
"Sarah is planning to visit Japan next spring, and Mike recommended Kyoto as the perfect destination for cherry blossom season. Mike explained that Kyoto has the most beautiful temples and spectacular cherry blossoms, based on his visit there in 2019. Sarah decided to add Kyoto to her itinerary."
- fact_type: "world"
- date: Next spring from reference date
- entities: [{{"text": "Sarah", "type": "PERSON"}}, {{"text": "Mike", "type": "PERSON"}}, {{"text": "Japan", "type": "PLACE"}}, {{"text": "Kyoto", "type": "PLACE"}}]
Input: "I bought an Apple laptop and some apples from the store."
GOOD fact: "Someone bought an Apple laptop and some apples from the store"
GOOD entities: [
{{"text": "Apple", "type": "ORG"}},
{{"text": "apples", "type": "PRODUCT"}}
]
NOTE: Use type to distinguish "Apple" the company from "apples" the fruit
### Example 4: Job News
**Input:**
"Alice mentioned she works at Google in Mountain View. She joined the AI team last year and loves the culture there."
Input: "The conference starts on Monday at the convention center."
GOOD fact: "The conference starts on Monday at the convention center"
GOOD entities: [
{{"text": "conference", "type": "OTHER"}},
{{"text": "Monday", "type": "OTHER"}},
{{"text": "convention center", "type": "PLACE"}}
]
NOTE: Use OTHER for entities like events (conference) or time references (Monday) that don't fit other categories
** GOOD (combined into one comprehensive fact):**
"Alice works at Google in Mountain View on the AI team, which she joined in 2023, and she loves the company culture there."
- fact_type: "world"
- date: 2023 (if reference is 2024)
- entities: [{{"text": "Alice", "type": "PERSON"}}, {{"text": "Google", "type": "ORG"}}, {{"text": "Mountain View", "type": "PLACE"}}, {{"text": "AI team", "type": "ORG"}}]
Input: "Melanie said 'Yesterday I took the kids to the museum - it was so cool seeing their eyes light up!'"
BAD fact: "The kids were excited about the museum"
BAD entities: [{{"text": "museum", "type": "PLACE"}}]
PROBLEM: Missing WHO (Melanie) and whose kids!
### Example 5: When to Split into Multiple Facts
**Input:**
"Caroline said 'This necklace is from my grandma in Sweden. I'm planning to visit Stockholm next month for a tech conference.'"
GOOD fact: "Melanie took her kids to the museum yesterday and they were excited, with their eyes lighting up"
GOOD date: Reference date minus 1 day
GOOD entities: [
{{"text": "Melanie", "type": "PERSON"}},
{{"text": "museum", "type": "PLACE"}}
]
NOTE: Preserved the subject (Melanie) and possessive relationship (her kids)
Input: "Caroline said 'This necklace is from my grandma in my home country, Sweden. She gave it to me when I was young.'"
BAD fact: "Caroline received a necklace as a gift from her grandmother when she was young"
BAD entities: [{{"text": "Caroline", "type": "PERSON"}}, {{"text": "necklace", "type": "PRODUCT"}}]
PROBLEM: Missing the CRITICAL biographical info that Caroline is from Sweden!
GOOD facts (extract MULTIPLE facts):
1. "Caroline is from Sweden, which is her home country"
entities: [{{"text": "Caroline", "type": "PERSON"}}, {{"text": "Sweden", "type": "PLACE"}}]
2. "Caroline's grandmother is from Sweden"
entities: [{{"text": "Caroline", "type": "PERSON"}}, {{"text": "Sweden", "type": "PLACE"}}]
3. "Caroline received a necklace as a gift from her grandmother in Sweden when she was young"
entities: [{{"text": "Caroline", "type": "PERSON"}}, {{"text": "Sweden", "type": "PLACE"}}, {{"text": "necklace", "type": "PRODUCT"}}]
NOTE: Extract SEPARATE facts for biographical details (home country) AND events (gift received)
## EXAMPLES of SOCIAL RELATIONSHIPS and NICKNAMES (CRITICAL):
Input: "Joanne was referred to as 'Jo' by Andrey during the meeting."
GOOD fact: "Andrey calls Joanne 'Jo'"
GOOD fact_type: "world"
GOOD date: Reference date (no specific time mentioned)
GOOD entities: [
{{"text": "Andrey", "type": "PERSON"}},
{{"text": "Joanne", "type": "PERSON"}}
]
NOTE: This is a FACT about their social relationship, even if it's not an "event"
Input: "Everyone calls him Bobby, but his real name is Robert."
GOOD facts (extract MULTIPLE facts):
1. "People call Robert by the nickname 'Bobby'"
entities: [{{"text": "Robert", "type": "PERSON"}}]
2. "Robert's real name is Robert (goes by Bobby)"
entities: [{{"text": "Robert", "type": "PERSON"}}]
NOTE: Extract the social fact about how people refer to him
Input: "Sarah introduced me to Dr. Chen, but she told me to just call him Michael."
GOOD facts (extract MULTIPLE facts):
1. "Sarah introduced someone to Dr. Chen (Michael)"
entities: [{{"text": "Sarah", "type": "PERSON"}}, {{"text": "Dr. Chen", "type": "PERSON"}}, {{"text": "Michael", "type": "PERSON"}}]
2. "Sarah told someone to call Dr. Chen by his first name Michael"
entities: [{{"text": "Sarah", "type": "PERSON"}}, {{"text": "Dr. Chen", "type": "PERSON"}}, {{"text": "Michael", "type": "PERSON"}}]
NOTE: Extract both the event (introduction) AND the social relationship fact (how to address him)
Input: "Alex considers Maria his mentor and always refers to her as 'the expert'."
GOOD facts (extract MULTIPLE facts):
1. "Alex considers Maria his mentor"
entities: [{{"text": "Alex", "type": "PERSON"}}, {{"text": "Maria", "type": "PERSON"}}]
2. "Alex refers to Maria as 'the expert'"
entities: [{{"text": "Alex", "type": "PERSON"}}, {{"text": "Maria", "type": "PERSON"}}]
NOTE: Capture both the relationship and how Alex refers to Maria
Input: "My grandmother - we call her Nana - lives in Boston."
GOOD facts (extract MULTIPLE facts):
1. "Someone's grandmother lives in Boston"
entities: [{{"text": "Boston", "type": "PLACE"}}]
2. "Someone and their family call their grandmother 'Nana'"
entities: []
NOTE: Extract both the biographical fact AND the nickname/term of address
** GOOD (split into 2 facts - different topics):**
1. "Caroline received a necklace from her grandmother in Sweden"
- entities: [{{"text": "Caroline", "type": "PERSON"}}, {{"text": "Sweden", "type": "PLACE"}}]
2. "Caroline is planning to visit Stockholm next month to attend a tech conference"
- date: Next month from reference
- entities: [{{"text": "Caroline", "type": "PERSON"}}, {{"text": "Stockholm", "type": "PLACE"}}]
- NOTE: Split because one is about the past (necklace) and one is future plans (conference) - completely different topics
## TEXT TO EXTRACT FROM:
{chunk}
Remember:
1. BE EXHAUSTIVE - Extract EVERY event, action, and fact with FULL CONTEXT
2. **PRESERVE ALL CONTEXT** - photos, visual elements, "new" things, modifiers (new/old/first/favorite)
3. **ALWAYS include the SUBJECT** - never say "the kids" without saying whose kids
4. **Preserve possessive relationships** - "their kids" must become "Person's kids"
5. **Extract biographical details as SEPARATE facts** - "my home country Sweden" "Person is from Sweden"
6. **Extract SOCIAL RELATIONSHIPS and NICKNAMES** - even if not events, these are facts
7. DO NOT drop modifiers or context - "new hair" stays "new hair", "photo of X" stays "photo of X"
8. Extract absolute dates by calculating relative times from the reference date
9. **CLASSIFY EACH FACT**: 'world' for general facts, 'agent' for AI agent actions
10. Extract ALL entities with types (PERSON, ORG, PLACE, PRODUCT, CONCEPT, OTHER)
11. When in doubt, EXTRACT IT with MORE CONTEXT rather than less"""
## CRITICAL REMINDERS:
1. **EXTRACT 2-5 COMPREHENSIVE FACTS** - Not dozens of fragments
2. **COMBINE RELATED EXCHANGES** - Keep full discussions together in one fact
3. **PRESERVE ALL CONTEXT** - Photos, "new" things, visual elements, reasoning, modifiers
4. **INCLUDE ALL PARTICIPANTS** - Who said/did what with full reasoning
5. **MAINTAIN NARRATIVE FLOW** - Tell the complete story in each fact
6. **ONLY SPLIT** when topics are completely unrelated or different time periods
7. **TRANSFORM RELATIVE DATES** - "last year" "in 2023" in the fact text
8. **EXTRACT ALL ENTITIES** - PERSON, ORG, PLACE, PRODUCT, CONCEPT, OTHER
9. **CLASSIFY FACTS** - 'world' for general facts, 'agent' for AI agent actions
10. When combining, prefer MORE comprehensive facts over fragmenting"""
import time
import logging
@ -406,7 +308,7 @@ Remember:
messages=[
{
"role": "system",
"content": "You are an EXHAUSTIVE fact and entity extractor. CRITICAL RULES: 1) ALWAYS include the SUBJECT (never 'the kids' without whose kids), 2) **PRESERVE ALL CONTEXT** - photos, 'new' things, modifiers (new/old/first/favorite), visual elements - DO NOT drop these details, 3) Extract biographical details as SEPARATE facts ('my home country Sweden''Person is from Sweden'), 4) **Extract SOCIAL RELATIONSHIPS and NICKNAMES** as facts even if not events ('Andrey calls Joanne Jo'), 5) Extract EVERY event with FULL CONTEXT - 'photo of my new hair' must preserve 'photo' AND 'new', 6) **TRANSFORM RELATIVE DATES IN FACT TEXT**: 'last year''in [year]', 'last month''in [month year]'. Extract ALL entities with types: PERSON, ORG, PLACE, PRODUCT, CONCEPT, OTHER. Preserve possessive relationships (their→whose). When in doubt, include MORE context rather than less - missing context loses critical information."
"content": "You are a comprehensive fact extractor that creates narrative, self-contained facts. CRITICAL: Extract 2-5 COMPREHENSIVE facts per conversation, NOT dozens of fragments. COMBINE related exchanges into single narrative facts that tell the complete story. For example, a discussion about playlist names should be ONE fact capturing the entire back-and-forth with all reasoning, not multiple small facts. PRESERVE all context (photos, 'new' things, visual elements, full reasoning), INCLUDE all participants and what they said/did, MAINTAIN narrative flow. ONLY SPLIT into separate facts when topics are completely unrelated or different time periods. Transform relative dates in fact text ('last year''in 2023'). Extract entities (PERSON, ORG, PLACE, PRODUCT, CONCEPT, OTHER). When in doubt, prefer MORE COMPREHENSIVE over fragmenting."
},
{
"role": "user",

View file

@ -225,6 +225,25 @@ class TemporalSemanticMemory(
"""
task_type = task_dict.get('type')
operation_id = task_dict.get('operation_id')
retry_count = task_dict.get('retry_count', 0)
max_retries = 3
# Check if operation was cancelled (only for tasks with operation_id)
if operation_id:
try:
pool = await self._get_pool()
async with pool.acquire() as conn:
result = await conn.fetchrow(
"SELECT id FROM async_operations WHERE id = $1",
uuid.UUID(operation_id)
)
if not result:
# Operation was cancelled, skip processing
logger.info(f"Skipping cancelled operation: {operation_id}")
return
except Exception as e:
logger.error(f"Failed to check operation status {operation_id}: {e}")
# Continue with processing if we can't check status
try:
if task_type == 'access_count_update':
@ -237,9 +256,35 @@ class TemporalSemanticMemory(
await self._handle_batch_put(task_dict)
else:
logger.error(f"Unknown task type: {task_type}")
finally:
# Delete operation record if operation_id is present
# Don't retry unknown task types
if operation_id:
await self._delete_operation_record(operation_id)
return
# Task succeeded - delete operation record
if operation_id:
await self._delete_operation_record(operation_id)
except Exception as e:
# Task failed - check if we should retry
logger.error(f"Task execution failed (attempt {retry_count + 1}/{max_retries + 1}): {task_type}, error: {e}")
import traceback
error_traceback = traceback.format_exc()
traceback.print_exc()
if retry_count < max_retries:
# Reschedule with incremented retry count
task_dict['retry_count'] = retry_count + 1
logger.info(f"Rescheduling task {task_type} (retry {retry_count + 1}/{max_retries})")
await self._task_backend.submit_task(task_dict)
else:
# Max retries exceeded - mark operation as failed
logger.error(f"Max retries exceeded for task {task_type}, marking as failed")
if operation_id:
await self._mark_operation_failed(operation_id, str(e), error_traceback)
async def _delete_operation_record(self, operation_id: str):
"""Helper to delete an operation record from the database."""
try:
pool = await self._get_pool()
async with pool.acquire() as conn:
@ -251,6 +296,28 @@ class TemporalSemanticMemory(
except Exception as e:
logger.error(f"Failed to delete async operation record {operation_id}: {e}")
async def _mark_operation_failed(self, operation_id: str, error_message: str, error_traceback: str):
"""Helper to mark an operation as failed in the database."""
try:
pool = await self._get_pool()
# Truncate error message to avoid extremely long strings
full_error = f"{error_message}\n\nTraceback:\n{error_traceback}"
truncated_error = full_error[:5000] if len(full_error) > 5000 else full_error
async with pool.acquire() as conn:
await conn.execute(
"""
UPDATE async_operations
SET status = 'failed', error_message = $2
WHERE id = $1
""",
uuid.UUID(operation_id),
truncated_error
)
logger.info(f"Marked async operation as failed: {operation_id}")
except Exception as e:
logger.error(f"Failed to mark operation as failed {operation_id}: {e}")
async def initialize(self):
"""Initialize the connection pool and background workers."""
if self._initialized:
@ -1681,7 +1748,8 @@ class TemporalSemanticMemory(
SELECT id, text, event_date, context
FROM memory_units
{where_clause}
ORDER BY event_date
ORDER BY event_date DESC
LIMIT 1000
""", *query_params)
# Get links, filtering to only include links between units of the selected agent
@ -1808,6 +1876,298 @@ class TemporalSemanticMemory(
"total_units": len(units)
}
async def list_memory_units(
self,
agent_id: Optional[str] = None,
fact_type: Optional[str] = None,
search_query: Optional[str] = None,
limit: int = 100,
offset: int = 0
):
"""
List memory units for table view with optional full-text search.
Args:
agent_id: Filter by agent ID
fact_type: Filter by fact type (world, agent, opinion)
search_query: Full-text search query (searches text and context fields)
limit: Maximum number of results to return
offset: Offset for pagination
Returns:
Dict with items (list of memory units) and total count
"""
pool = await self._get_pool()
async with pool.acquire() as conn:
# Build query conditions
query_conditions = []
query_params = []
param_count = 0
if agent_id:
param_count += 1
query_conditions.append(f"agent_id = ${param_count}")
query_params.append(agent_id)
if fact_type:
param_count += 1
query_conditions.append(f"fact_type = ${param_count}")
query_params.append(fact_type)
if search_query:
# Full-text search on text and context fields using ILIKE
param_count += 1
query_conditions.append(f"(text ILIKE ${param_count} OR context ILIKE ${param_count})")
query_params.append(f"%{search_query}%")
where_clause = "WHERE " + " AND ".join(query_conditions) if query_conditions else ""
# Get total count
count_query = f"""
SELECT COUNT(*) as total
FROM memory_units
{where_clause}
"""
count_result = await conn.fetchrow(count_query, *query_params)
total = count_result['total']
# Get units with limit and offset
param_count += 1
limit_param = f"${param_count}"
query_params.append(limit)
param_count += 1
offset_param = f"${param_count}"
query_params.append(offset)
units = await conn.fetch(f"""
SELECT id, text, event_date, context, fact_type
FROM memory_units
{where_clause}
ORDER BY event_date DESC
LIMIT {limit_param} OFFSET {offset_param}
""", *query_params)
# Get entity information for these units
if units:
unit_ids = [row['id'] for row in units]
unit_entities = await conn.fetch("""
SELECT ue.unit_id, e.canonical_name, e.entity_type
FROM unit_entities ue
JOIN entities e ON ue.entity_id = e.id
WHERE ue.unit_id = ANY($1::uuid[])
ORDER BY ue.unit_id
""", unit_ids)
else:
unit_entities = []
# Build entity mapping
entity_map = {}
for row in unit_entities:
unit_id = row['unit_id']
entity_name = row['canonical_name']
entity_type = row['entity_type']
if unit_id not in entity_map:
entity_map[unit_id] = []
entity_map[unit_id].append(f"{entity_name} ({entity_type})")
# Build result items
items = []
for row in units:
unit_id = row['id']
entities = entity_map.get(unit_id, [])
items.append({
"id": str(unit_id),
"text": row['text'],
"context": row['context'] if row['context'] else "",
"date": row['event_date'].isoformat() if row['event_date'] else "",
"fact_type": row['fact_type'],
"entities": ", ".join(entities) if entities else ""
})
return {
"items": items,
"total": total,
"limit": limit,
"offset": offset
}
async def list_documents(
self,
agent_id: Optional[str] = None,
search_query: Optional[str] = None,
limit: int = 100,
offset: int = 0
):
"""
List documents with optional search and pagination.
Args:
agent_id: Filter by agent ID
search_query: Search in metadata (JSON text search)
limit: Maximum number of results
offset: Offset for pagination
Returns:
Dict with items (list of documents without original_text) and total count
"""
pool = await self._get_pool()
async with pool.acquire() as conn:
# Build query conditions
query_conditions = []
query_params = []
param_count = 0
if agent_id:
param_count += 1
query_conditions.append(f"agent_id = ${param_count}")
query_params.append(agent_id)
if search_query:
# Search in document ID and metadata (as text)
param_count += 1
query_conditions.append(f"(id ILIKE ${param_count} OR metadata::text ILIKE ${param_count})")
query_params.append(f"%{search_query}%")
where_clause = "WHERE " + " AND ".join(query_conditions) if query_conditions else ""
# Get total count
count_query = f"""
SELECT COUNT(*) as total
FROM documents
{where_clause}
"""
count_result = await conn.fetchrow(count_query, *query_params)
total = count_result['total']
# Get documents with limit and offset (without original_text for performance)
param_count += 1
limit_param = f"${param_count}"
query_params.append(limit)
param_count += 1
offset_param = f"${param_count}"
query_params.append(offset)
documents = await conn.fetch(f"""
SELECT
id,
agent_id,
content_hash,
metadata,
created_at,
updated_at,
LENGTH(original_text) as text_length
FROM documents
{where_clause}
ORDER BY created_at DESC
LIMIT {limit_param} OFFSET {offset_param}
""", *query_params)
# Get memory unit count for each document
if documents:
doc_ids = [(row['id'], row['agent_id']) for row in documents]
# Create placeholders for the query
placeholders = []
params_for_count = []
for i, (doc_id, agent_id_val) in enumerate(doc_ids):
idx_doc = i * 2 + 1
idx_agent = i * 2 + 2
placeholders.append(f"(document_id = ${idx_doc} AND agent_id = ${idx_agent})")
params_for_count.extend([doc_id, agent_id_val])
where_clause_count = " OR ".join(placeholders)
unit_counts = await conn.fetch(f"""
SELECT document_id, agent_id, COUNT(*) as unit_count
FROM memory_units
WHERE {where_clause_count}
GROUP BY document_id, agent_id
""", *params_for_count)
else:
unit_counts = []
# Build count mapping
count_map = {(row['document_id'], row['agent_id']): row['unit_count'] for row in unit_counts}
# Build result items
items = []
for row in documents:
doc_id = row['id']
agent_id_val = row['agent_id']
unit_count = count_map.get((doc_id, agent_id_val), 0)
items.append({
"id": doc_id,
"agent_id": agent_id_val,
"content_hash": row['content_hash'],
"metadata": row['metadata'] if row['metadata'] else {},
"created_at": row['created_at'].isoformat() if row['created_at'] else "",
"updated_at": row['updated_at'].isoformat() if row['updated_at'] else "",
"text_length": row['text_length'] or 0,
"memory_unit_count": unit_count
})
return {
"items": items,
"total": total,
"limit": limit,
"offset": offset
}
async def get_document(
self,
document_id: str,
agent_id: str
):
"""
Get a specific document including its original_text.
Args:
document_id: Document ID
agent_id: Agent ID
Returns:
Dict with document details including original_text, or None if not found
"""
pool = await self._get_pool()
async with pool.acquire() as conn:
doc = await conn.fetchrow("""
SELECT
id,
agent_id,
original_text,
content_hash,
metadata,
created_at,
updated_at
FROM documents
WHERE id = $1 AND agent_id = $2
""", document_id, agent_id)
if not doc:
return None
# Get memory unit count
unit_count_row = await conn.fetchrow("""
SELECT COUNT(*) as unit_count
FROM memory_units
WHERE document_id = $1 AND agent_id = $2
""", document_id, agent_id)
return {
"id": doc['id'],
"agent_id": doc['agent_id'],
"original_text": doc['original_text'],
"content_hash": doc['content_hash'],
"metadata": doc['metadata'] if doc['metadata'] else {},
"created_at": doc['created_at'].isoformat() if doc['created_at'] else "",
"updated_at": doc['updated_at'].isoformat() if doc['updated_at'] else "",
"memory_unit_count": unit_count_row['unit_count'] if unit_count_row else 0
}
async def _evaluate_opinion_update_async(
self,
opinion_text: str,

View file

@ -172,6 +172,137 @@ window.loadDataView = async function(factType) {
}
}
// Load documents view
window.loadDocumentsView = async function() {
if (!currentAgentId) {
alert('Please select an agent first');
return;
}
await loadDocumentsTable();
}
// Load documents table from API
async function loadDocumentsTable(searchQuery = '', limit = 100, offset = 0) {
if (!currentAgentId) return;
const tbody = document.getElementById('documents-table-body');
const countSpan = document.getElementById('documents-count');
if (!tbody) return;
try {
// Build URL with filters
let url = `api/documents?agent_id=${encodeURIComponent(currentAgentId)}&limit=${limit}&offset=${offset}`;
if (searchQuery) {
url += `&q=${encodeURIComponent(searchQuery)}`;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (countSpan) {
countSpan.textContent = `(${data.total})`;
}
if (data.items.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="empty-message">No documents found</td></tr>';
return;
}
tbody.innerHTML = data.items.map(doc => `
<tr>
<td title="${doc.id}">${doc.id.length > 30 ? doc.id.substring(0, 30) + '...' : doc.id}</td>
<td>${doc.created_at ? new Date(doc.created_at).toLocaleString() : 'N/A'}</td>
<td>${doc.updated_at ? new Date(doc.updated_at).toLocaleString() : 'N/A'}</td>
<td>${doc.text_length.toLocaleString()} chars</td>
<td>${doc.memory_unit_count}</td>
<td title="${JSON.stringify(doc.metadata)}">${Object.keys(doc.metadata).length > 0 ? JSON.stringify(doc.metadata).substring(0, 50) + '...' : 'None'}</td>
<td>
<button onclick="viewDocumentText('${doc.id.replace(/'/g, "\\'")}', '${doc.agent_id.replace(/'/g, "\\'")}')"
class="load-button"
style="padding: 5px 10px; font-size: 12px;"
title="View original text">
View Text
</button>
</td>
</tr>
`).join('');
// Setup table filter with debounced API calls
const filterInput = document.getElementById('documents-filter');
if (filterInput) {
filterInput.removeEventListener('input', filterInput._filterHandler);
filterInput._filterHandler = debounce(async function() {
const filterValue = this.value.trim();
await loadDocumentsTable(filterValue);
}, 500);
filterInput.addEventListener('input', filterInput._filterHandler);
}
} catch (error) {
console.error('Error loading documents:', error);
tbody.innerHTML = '<tr><td colspan="7" class="empty-message">Error loading documents</td></tr>';
}
}
// View document text in a modal
window.viewDocumentText = async function(documentId, agentId) {
try {
const url = `api/documents/${encodeURIComponent(documentId)}?agent_id=${encodeURIComponent(agentId)}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const doc = await response.json();
// Create modal overlay
const modal = document.createElement('div');
modal.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 10000; display: flex; align-items: center; justify-content: center; padding: 20px;';
const modalContent = document.createElement('div');
modalContent.style.cssText = 'background: white; border-radius: 8px; max-width: 900px; max-height: 90vh; overflow: auto; padding: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);';
modalContent.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 20px;">
<div>
<h2 style="margin: 0 0 10px 0;">Document: ${doc.id}</h2>
<div style="color: #666; font-size: 14px;">
<div>Created: ${new Date(doc.created_at).toLocaleString()}</div>
<div>Memory Units: ${doc.memory_unit_count}</div>
${Object.keys(doc.metadata).length > 0 ? `<div>Metadata: ${JSON.stringify(doc.metadata, null, 2)}</div>` : ''}
</div>
</div>
<button onclick="this.closest('[style*=fixed]').remove()"
style="padding: 8px 16px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: bold;">
</button>
</div>
<div style="background: #f5f5f5; padding: 20px; border-radius: 4px; border-left: 4px solid #2196F3; white-space: pre-wrap; font-family: monospace; max-height: 60vh; overflow: auto; line-height: 1.6;">
${doc.original_text}
</div>
`;
modal.appendChild(modalContent);
document.body.appendChild(modal);
// Close on background click
modal.addEventListener('click', function(e) {
if (e.target === modal) {
modal.remove();
}
});
} catch (error) {
console.error('Error loading document:', error);
alert('Error loading document: ' + error.message);
}
}
// Reload graph for a specific fact type
window.reloadDataGraph = function(factType) {
const data = dataCache[factType];
@ -308,24 +439,62 @@ window.reloadDataGraph = function(factType) {
}
// Update table for a specific fact type
function updateDataTable(factType, data) {
async function updateDataTable(factType, data) {
if (!data) return;
const tbody = document.getElementById(`${factType}-table-body`);
const countSpan = document.getElementById(`${factType}-table-count`);
if (countSpan) {
countSpan.textContent = `(${data.total_units})`;
}
if (tbody) {
tbody.innerHTML = data.table_rows.map(row => `
// Load initial table data from the new /api/list endpoint
await loadTableData(factType);
// Setup table filter with debounced API calls
const filterInput = document.getElementById(`${factType}-table-filter`);
if (filterInput) {
filterInput.removeEventListener('input', filterInput._filterHandler);
filterInput._filterHandler = debounce(async function() {
const filterValue = this.value.trim();
await loadTableData(factType, filterValue);
}, 500); // 500ms debounce
filterInput.addEventListener('input', filterInput._filterHandler);
}
}
// Load table data from /api/list endpoint
async function loadTableData(factType, searchQuery = '', limit = 100, offset = 0) {
if (!currentAgentId) return;
const tbody = document.getElementById(`${factType}-table-body`);
if (!tbody) return;
try {
// Build URL with filters
let url = `api/list?agent_id=${encodeURIComponent(currentAgentId)}&fact_type=${factType}&limit=${limit}&offset=${offset}`;
if (searchQuery) {
url += `&q=${encodeURIComponent(searchQuery)}`;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.items.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-message">No results found</td></tr>';
return;
}
tbody.innerHTML = data.items.map(row => `
<tr>
<td>${row.id}</td>
<td>${row.id.substring(0, 8)}...</td>
<td>${row.text}</td>
<td>${row.context}</td>
<td>${row.date}</td>
<td>${row.entities}</td>
<td>${row.context || 'N/A'}</td>
<td>${row.date ? new Date(row.date).toLocaleString() : 'N/A'}</td>
<td>${row.entities || 'None'}</td>
<td>
<button onclick="deleteRecord('${factType}', '${row.id}')"
class="delete-button"
@ -335,27 +504,24 @@ function updateDataTable(factType, data) {
</td>
</tr>
`).join('');
} catch (error) {
console.error('Error loading table data:', error);
tbody.innerHTML = '<tr><td colspan="6" class="empty-message">Error loading data</td></tr>';
}
}
// Setup table filter
const filterInput = document.getElementById(`${factType}-table-filter`);
if (filterInput) {
filterInput.removeEventListener('input', filterInput._filterHandler);
filterInput._filterHandler = function() {
const filterValue = this.value.toLowerCase();
const rows = document.querySelectorAll(`#${factType}-table-body tr`);
rows.forEach(row => {
const text = row.textContent.toLowerCase();
if (text.includes(filterValue)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
// Debounce function to limit API calls
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func.apply(this, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
filterInput.addEventListener('input', filterInput._filterHandler);
}
}
// Delete a record and all its links
@ -1925,7 +2091,7 @@ function updateUIForAgentSelection() {
}
// Update each data subtab
['world', 'agent', 'opinion'].forEach(factType => {
['world', 'agent', 'opinion', 'documents'].forEach(factType => {
const noAgentMsg = document.getElementById(`${factType}-no-agent-message`);
const content = document.getElementById(`${factType}-content`);
@ -1960,11 +2126,14 @@ async function loadStats() {
document.getElementById('stat-semantic-links').textContent = (stats.links_by_type.semantic || 0).toLocaleString();
document.getElementById('stat-entity-links').textContent = (stats.links_by_type.entity || 0).toLocaleString();
// Update documents count
document.getElementById('stat-documents').textContent = (stats.total_documents || 0).toLocaleString();
} catch (e) {
console.error('Error loading stats:', e);
// Reset to dashes on error
['stat-total-nodes', 'stat-world-nodes', 'stat-agent-nodes', 'stat-opinion-nodes',
'stat-total-links', 'stat-temporal-links', 'stat-semantic-links', 'stat-entity-links'].forEach(id => {
'stat-total-links', 'stat-temporal-links', 'stat-semantic-links', 'stat-entity-links', 'stat-documents'].forEach(id => {
document.getElementById(id).textContent = '-';
});
}

View file

@ -62,6 +62,10 @@
<div class="stat-label">Entity Links</div>
<div class="stat-value" id="stat-entity-links">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Documents</div>
<div class="stat-value" id="stat-documents">-</div>
</div>
</div>
</div>
@ -69,6 +73,7 @@
<button class="data-sub-tab-button active" onclick="switchDataSubTab('world')">World</button>
<button class="data-sub-tab-button" onclick="switchDataSubTab('agent')">Agent</button>
<button class="data-sub-tab-button" onclick="switchDataSubTab('opinion')">Opinions</button>
<button class="data-sub-tab-button" onclick="switchDataSubTab('documents')">Documents</button>
</div>
<!-- World, Agent, and Opinions subtabs share the same structure -->
@ -117,7 +122,7 @@
</div>
</div>
<div id="world-table-view" class="data-view" style="display: none;">
<input type="text" id="world-table-filter" placeholder="Filter by text, context, or entities..." class="table-filter">
<input type="text" id="world-table-filter" placeholder="Search memories (text, context)..." class="table-filter">
<div class="table-container">
<table class="memory-table">
<thead>
@ -177,7 +182,7 @@
</div>
</div>
<div id="agent-table-view" class="data-view" style="display: none;">
<input type="text" id="agent-table-filter" placeholder="Filter by text, context, or entities..." class="table-filter">
<input type="text" id="agent-table-filter" placeholder="Search memories (text, context)..." class="table-filter">
<div class="table-container">
<table class="memory-table">
<thead>
@ -237,7 +242,7 @@
</div>
</div>
<div id="opinion-table-view" class="data-view" style="display: none;">
<input type="text" id="opinion-table-filter" placeholder="Filter by text, context, or entities..." class="table-filter">
<input type="text" id="opinion-table-filter" placeholder="Search memories (text, context)..." class="table-filter">
<div class="table-container">
<table class="memory-table">
<thead>
@ -251,6 +256,41 @@
</div>
</div>
</div>
<div id="documents-subtab" class="data-subtab-content">
<div id="documents-no-agent-message" class="no-agent-message">
<h3>No Agent Selected</h3>
<p>Please select an agent from the dropdown above to view documents.</p>
</div>
<div id="documents-content" style="display: none;">
<div class="data-controls" style="margin-bottom: 15px; padding: 10px; background: #f5f5f5; border-radius: 4px;">
<button onclick="loadDocumentsView()" class="load-button">📄 Load Documents</button>
<button onclick="loadDocumentsView()" class="refresh-button">🔄 Refresh</button>
<span id="documents-count" class="node-count"></span>
</div>
<div class="data-view">
<input type="text" id="documents-filter" placeholder="Search documents (ID, metadata)..." class="table-filter">
<div class="table-container">
<table class="memory-table">
<thead>
<tr>
<th>Document ID</th>
<th>Created</th>
<th>Updated</th>
<th>Text Length</th>
<th>Memory Units</th>
<th>Metadata</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="documents-table-body">
<tr><td colspan="7" class="empty-message">Click "Load Documents" to view data</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div id="debug-tab" class="tab-content">

View file

@ -19,7 +19,7 @@
"Visualization"
],
"summary": "Get memory graph data",
"description": "Retrieve graph data for visualization, optionally filtered by agent_id and fact_type (world/agent/opinion)",
"description": "Retrieve graph data for visualization, optionally filtered by agent_id and fact_type (world/agent/opinion). Limited to 1000 most recent items.",
"operationId": "api_graph_api_graph_get",
"parameters": [
{
@ -79,6 +79,108 @@
}
}
},
"/api/list": {
"get": {
"tags": [
"Visualization"
],
"summary": "List memory units",
"description": "List memory units with pagination and optional full-text search. Supports filtering by agent_id and fact_type.",
"operationId": "api_list_api_list_get",
"parameters": [
{
"name": "agent_id",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Agent Id"
}
},
{
"name": "fact_type",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Fact Type"
}
},
{
"name": "q",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Q"
}
},
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 100,
"title": "Limit"
}
},
{
"name": "offset",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 0,
"title": "Offset"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ListMemoryUnitsResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/search": {
"post": {
"tags": [
@ -226,6 +328,144 @@
}
}
},
"/api/documents": {
"get": {
"tags": [
"Documents"
],
"summary": "List documents",
"description": "List documents with pagination and optional search. Documents are the source content from which memory units are extracted.",
"operationId": "api_list_documents_api_documents_get",
"parameters": [
{
"name": "agent_id",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Agent Id"
}
},
{
"name": "q",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Q"
}
},
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 100,
"title": "Limit"
}
},
{
"name": "offset",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 0,
"title": "Offset"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ListDocumentsResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/documents/{document_id}": {
"get": {
"tags": [
"Documents"
],
"summary": "Get document details",
"description": "Get a specific document including its original text",
"operationId": "api_get_document_api_documents__document_id__get",
"parameters": [
{
"name": "document_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Document Id"
}
},
{
"name": "agent_id",
"in": "query",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DocumentResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/memories/batch": {
"post": {
"tags": [
@ -310,6 +550,88 @@
}
}
},
"/api/operations/{agent_id}": {
"get": {
"tags": [
"Memory Storage"
],
"summary": "List async operations",
"description": "Get a list of all async operations (pending and failed) for a specific agent, including error messages for failed operations",
"operationId": "api_list_operations_api_operations__agent_id__get",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Agent Id"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/operations/{operation_id}": {
"delete": {
"tags": [
"Memory Storage"
],
"summary": "Cancel a pending async operation",
"description": "Cancel a pending async operation by removing it from the queue",
"operationId": "api_cancel_operation_api_operations__operation_id__delete",
"parameters": [
{
"name": "operation_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Operation Id"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/memory/{unit_id}": {
"delete": {
"tags": [
@ -543,6 +865,75 @@
"success": true
}
},
"DocumentResponse": {
"properties": {
"id": {
"type": "string",
"title": "Id"
},
"agent_id": {
"type": "string",
"title": "Agent Id"
},
"original_text": {
"type": "string",
"title": "Original Text"
},
"content_hash": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Content Hash"
},
"metadata": {
"additionalProperties": true,
"type": "object",
"title": "Metadata"
},
"created_at": {
"type": "string",
"title": "Created At"
},
"updated_at": {
"type": "string",
"title": "Updated At"
},
"memory_unit_count": {
"type": "integer",
"title": "Memory Unit Count"
}
},
"type": "object",
"required": [
"id",
"agent_id",
"original_text",
"content_hash",
"metadata",
"created_at",
"updated_at",
"memory_unit_count"
],
"title": "DocumentResponse",
"description": "Response model for get document endpoint.",
"example": {
"agent_id": "user123",
"content_hash": "abc123",
"created_at": "2024-01-15T10:30:00Z",
"id": "session_1",
"memory_unit_count": 15,
"metadata": {
"source": "conversation"
},
"original_text": "Full document text here...",
"updated_at": "2024-01-15T10:30:00Z"
}
},
"GraphDataResponse": {
"properties": {
"nodes": {
@ -629,6 +1020,106 @@
"type": "object",
"title": "HTTPValidationError"
},
"ListDocumentsResponse": {
"properties": {
"items": {
"items": {
"additionalProperties": true,
"type": "object"
},
"type": "array",
"title": "Items"
},
"total": {
"type": "integer",
"title": "Total"
},
"limit": {
"type": "integer",
"title": "Limit"
},
"offset": {
"type": "integer",
"title": "Offset"
}
},
"type": "object",
"required": [
"items",
"total",
"limit",
"offset"
],
"title": "ListDocumentsResponse",
"description": "Response model for list documents endpoint.",
"example": {
"items": [
{
"agent_id": "user123",
"content_hash": "abc123",
"created_at": "2024-01-15T10:30:00Z",
"id": "session_1",
"memory_unit_count": 15,
"metadata": {
"source": "conversation"
},
"text_length": 5420,
"updated_at": "2024-01-15T10:30:00Z"
}
],
"limit": 100,
"offset": 0,
"total": 50
}
},
"ListMemoryUnitsResponse": {
"properties": {
"items": {
"items": {
"additionalProperties": true,
"type": "object"
},
"type": "array",
"title": "Items"
},
"total": {
"type": "integer",
"title": "Total"
},
"limit": {
"type": "integer",
"title": "Limit"
},
"offset": {
"type": "integer",
"title": "Offset"
}
},
"type": "object",
"required": [
"items",
"total",
"limit",
"offset"
],
"title": "ListMemoryUnitsResponse",
"description": "Response model for list memory units endpoint.",
"example": {
"items": [
{
"context": "Work conversation",
"date": "2024-01-15T10:30:00Z",
"entities": "Alice (PERSON), Google (ORGANIZATION)",
"fact_type": "world",
"id": "550e8400-e29b-41d4-a716-446655440000",
"text": "Alice works at Google on the AI team"
}
],
"limit": 100,
"offset": 0,
"total": 150
}
},
"MemoryItem": {
"properties": {
"content": {