This commit is contained in:
Nicolò Boschi 2025-11-03 20:10:11 +01:00
parent 42260c29f7
commit 7d8d07d1aa
37 changed files with 1264 additions and 140 deletions

View file

@ -341,7 +341,7 @@ poetry add ../memory-poc --editable
### 2. Import the memory system:
```python
from memory import TemporalSemanticMemory
from memora import TemporalSemanticMemory
# Initialize memory
memory = TemporalSemanticMemory()
@ -363,7 +363,7 @@ results, trace = await memory.search_async(
### 3. Import the FastAPI app:
```python
from web import app, memory
from memora.web import app, memory
# Use the FastAPI app in your own project
# You can mount it as a sub-application or run it directly
@ -377,7 +377,7 @@ if __name__ == "__main__":
```python
from fastapi import FastAPI
from web import app as memory_app, memory
from memora.web import app as memory_app, memory
# Create your own app
my_app = FastAPI()
@ -407,7 +407,7 @@ if __name__ == "__main__":
- `SearchTrace`, `SearchTracer` - Search tracing utilities
- `QueryInfo`, `EntryPoint`, `NodeVisit`, etc. - Trace data structures
**From `web` package:**
**From `memory.web` package:**
- `app` - FastAPI application instance
- `memory` - Shared TemporalSemanticMemory instance
@ -417,10 +417,10 @@ To run the web interface:
```bash
# Development mode with auto-reload
uvicorn web.server:app --reload --port 8000
uvicorn memora.web.server:app --reload --port 8000
# Production mode
uvicorn web.server:app --host 0.0.0.0 --port 8000 --workers 4
uvicorn memora.web.server:app --host 0.0.0.0 --port 8000 --workers 4
```
Then open http://localhost:8000 in your browser to access the visualization interface.
@ -476,7 +476,7 @@ The memory system uses a **mixin pattern** for code organization:
### Store Memories
```python
from memory import TemporalSemanticMemory
from memora import TemporalSemanticMemory
memory = TemporalSemanticMemory()

View file

@ -22,7 +22,7 @@ from rich.table import Table
from rich import box
import pydantic
from memory import TemporalSemanticMemory
from memora import TemporalSemanticMemory
from openai import AsyncOpenAI
console = Console()

View file

@ -14,7 +14,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
import asyncio
import argparse
from memory import TemporalSemanticMemory
from memora import TemporalSemanticMemory
from locomo_benchmark import LoComoDataset, LoComoAnswerGenerator, LoComoAnswerEvaluator
from common.benchmark_runner import BenchmarkRunner

View file

@ -26,7 +26,7 @@ import asyncio
import argparse
import subprocess
from rich.console import Console
from memory import TemporalSemanticMemory
from memora import TemporalSemanticMemory
from longmemeval_benchmark import LongMemEvalDataset, LongMemEvalAnswerGenerator, LongMemEvalAnswerEvaluator
from common.benchmark_runner import BenchmarkRunner

View file

@ -7,7 +7,7 @@ This demonstrates how to:
3. Mount the memory app as a sub-application
"""
import asyncio
from web import app, memory
from memora.web import app, memory
async def example_memory_usage():

43
generate_openapi.py Normal file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""
Generate OpenAPI specification from FastAPI app.
This script imports the FastAPI app and exports its OpenAPI schema to a JSON file.
"""
import json
import sys
from pathlib import Path
# Add parent directory to path to import memory module
sys.path.insert(0, str(Path(__file__).parent))
from memory.web.server import app
def generate_openapi_spec(output_path: str = "openapi.json"):
"""Generate OpenAPI spec and save to file."""
# Get the OpenAPI schema from the app
openapi_schema = app.openapi()
# Write to file
output_file = Path(output_path)
with open(output_file, 'w') as f:
json.dump(openapi_schema, f, indent=2)
print(f"✓ OpenAPI specification generated: {output_file.absolute()}")
print(f" - Title: {openapi_schema['info']['title']}")
print(f" - Version: {openapi_schema['info']['version']}")
print(f" - Endpoints: {len(openapi_schema['paths'])}")
# List endpoints
print("\n Endpoints:")
for path, methods in openapi_schema['paths'].items():
for method in methods.keys():
if method.upper() in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']:
endpoint_info = methods[method]
summary = endpoint_info.get('summary', 'No summary')
tags = ', '.join(endpoint_info.get('tags', ['untagged']))
print(f" {method.upper():6} {path:30} [{tags}] - {summary}")
if __name__ == "__main__":
output = sys.argv[1] if len(sys.argv) > 1 else "openapi.json"
generate_openapi_spec(output)

View file

@ -338,9 +338,19 @@ class LinkOperationsMixin:
# Try direct conversion (works for numpy arrays, pgvector objects, etc.)
emb = np.array(raw_emb, dtype=np.float32)
# Ensure it's 1D
if emb.ndim != 1:
raise ValueError(f"Expected 1D embedding, got shape {emb.shape}")
embedding_arrays.append(emb)
existing_embeddings = np.vstack(embedding_arrays) if embedding_arrays else np.array([])
if not embedding_arrays:
existing_embeddings = np.array([])
elif len(embedding_arrays) == 1:
# Single embedding: reshape to (1, dim)
existing_embeddings = embedding_arrays[0].reshape(1, -1)
else:
# Multiple embeddings: vstack
existing_embeddings = np.vstack(embedding_arrays)
# For each new unit, compute similarities with ALL existing units
for unit_id, new_embedding in zip(unit_ids, embeddings):

View file

@ -290,7 +290,29 @@ class TemporalSemanticMemory(
is_duplicate = []
# Convert existing embeddings to numpy for faster computation
existing_embeddings = np.array([np.array(row['embedding']) for row in existing_facts])
embedding_arrays = []
for row in existing_facts:
raw_emb = row['embedding']
# Handle different pgvector formats
if isinstance(raw_emb, str):
# Parse string format: "[1.0, 2.0, ...]"
import json
emb = np.array(json.loads(raw_emb), dtype=np.float32)
elif isinstance(raw_emb, (list, tuple)):
emb = np.array(raw_emb, dtype=np.float32)
else:
# Try direct conversion
emb = np.array(raw_emb, dtype=np.float32)
embedding_arrays.append(emb)
if not embedding_arrays:
existing_embeddings = np.array([])
elif len(embedding_arrays) == 1:
# Single embedding: reshape to (1, dim)
existing_embeddings = embedding_arrays[0].reshape(1, -1)
else:
# Multiple embeddings: vstack
existing_embeddings = np.vstack(embedding_arrays)
comp_start = time_mod.time()
for embedding in embeddings:

View file

@ -16,19 +16,48 @@ from pathlib import Path
from typing import Optional, List, Dict, Any
from datetime import datetime
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from memory import TemporalSemanticMemory
# Import from parent memora package
from memora import TemporalSemanticMemory
import logging
load_dotenv()
logging.basicConfig(level=logging.INFO)
app = FastAPI(title="Memory Graph API", version="1.0.0")
app = FastAPI(
title="Agent Memory API",
version="1.0.0",
description="""
A temporal-semantic memory system for AI agents that stores, retrieves, and reasons over memories.
## Features
* **Batch Memory Storage**: Store multiple memories efficiently with automatic fact extraction
* **Semantic Search**: Find relevant memories using natural language queries
* **Fact Type Filtering**: Search across world facts, agent actions, and opinions separately
* **Think Endpoint**: Generate contextual answers based on agent identity and memories
* **Graph Visualization**: Interactive memory graph visualization
* **Document Tracking**: Track and manage memory documents with upsert support
## Architecture
The system uses:
- **Temporal Links**: Connect memories that are close in time
- **Semantic Links**: Connect semantically similar memories
- **Entity Links**: Connect memories that mention the same entities
- **Spreading Activation**: Intelligent traversal for memory retrieval
""",
contact={
"name": "Memory System",
},
license_info={
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}
)
# Mount static files
app.mount("/static", StaticFiles(directory="web/static"), name="static")
app.mount("/static", StaticFiles(directory=str(Path(__file__).parent / "static")), name="static")
class SearchRequest(BaseModel):
@ -40,6 +69,42 @@ class SearchRequest(BaseModel):
mmr_lambda: float = 0.5
trace: bool = False
class Config:
json_schema_extra = {
"example": {
"query": "What did Alice say about machine learning?",
"agent_id": "user123",
"thinking_budget": 100,
"top_k": 10,
"mmr_lambda": 0.5,
"trace": True
}
}
class SearchResponse(BaseModel):
"""Response model for search endpoints."""
results: List[Dict[str, Any]]
trace: Optional[Dict[str, Any]] = None
class Config:
json_schema_extra = {
"example": {
"results": [
{
"text": "Alice works at Google on the AI team",
"score": 0.95,
"id": "123e4567-e89b-12d3-a456-426614174000"
}
],
"trace": {
"query": "What did Alice say about machine learning?",
"num_results": 1,
"time_seconds": 0.123
}
}
}
class MemoryItem(BaseModel):
"""Single memory item for batch put."""
@ -47,6 +112,15 @@ class MemoryItem(BaseModel):
event_date: Optional[datetime] = None
context: Optional[str] = None
class Config:
json_schema_extra = {
"example": {
"content": "Alice mentioned she's working on a new ML model",
"event_date": "2024-01-15T10:30:00Z",
"context": "team meeting"
}
}
class BatchPutRequest(BaseModel):
"""Request model for batch put endpoint."""
@ -56,6 +130,45 @@ class BatchPutRequest(BaseModel):
document_metadata: Optional[Dict[str, Any]] = None
upsert: bool = False
class Config:
json_schema_extra = {
"example": {
"agent_id": "user123",
"items": [
{
"content": "Alice works at Google",
"context": "work"
},
{
"content": "Bob went hiking yesterday",
"event_date": "2024-01-15T10:00:00Z"
}
],
"document_id": "conversation_123",
"upsert": False
}
}
class BatchPutResponse(BaseModel):
"""Response model for batch put endpoint."""
success: bool
message: str
agent_id: str
document_id: Optional[str] = None
items_count: int
class Config:
json_schema_extra = {
"example": {
"success": True,
"message": "Successfully stored 2 memory items",
"agent_id": "user123",
"document_id": "conversation_123",
"items_count": 2
}
}
class ThinkRequest(BaseModel):
"""Request model for think endpoint."""
@ -64,6 +177,16 @@ class ThinkRequest(BaseModel):
thinking_budget: int = 50
top_k: int = 10
class Config:
json_schema_extra = {
"example": {
"query": "What do you think about artificial intelligence?",
"agent_id": "user123",
"thinking_budget": 50,
"top_k": 10
}
}
class ThinkResponse(BaseModel):
"""Response model for think endpoint."""
@ -71,6 +194,50 @@ class ThinkResponse(BaseModel):
based_on: Dict[str, List[Dict[str, Any]]] # {"world": [...], "agent": [...], "opinion": [...]}
new_opinions: List[str] = [] # List of newly formed opinions
class Config:
json_schema_extra = {
"example": {
"text": "Based on my understanding, AI is a transformative technology...",
"based_on": {
"world": [{"text": "AI is used in healthcare", "score": 0.9}],
"agent": [{"text": "I discussed AI applications last week", "score": 0.85}],
"opinion": [{"text": "I believe AI should be used ethically", "score": 0.8}]
},
"new_opinions": ["AI has great potential when used responsibly"]
}
}
class AgentsResponse(BaseModel):
"""Response model for agents list endpoint."""
agents: List[str]
class Config:
json_schema_extra = {
"example": {
"agents": ["user123", "agent_alice", "agent_bob"]
}
}
class GraphDataResponse(BaseModel):
"""Response model for graph data endpoint."""
nodes: List[Dict[str, Any]]
edges: List[Dict[str, Any]]
class Config:
json_schema_extra = {
"example": {
"nodes": [
{"id": "1", "label": "Alice works at Google", "type": "world"},
{"id": "2", "label": "Bob went hiking", "type": "world"}
],
"edges": [
{"from": "1", "to": "2", "type": "semantic", "weight": 0.8}
]
}
}
memory = TemporalSemanticMemory()
@ -86,14 +253,23 @@ async def shutdown_event():
await memory.close()
logging.info("Memory system closed")
@app.get("/")
@app.get("/", include_in_schema=False)
async def index():
"""Serve the visualization page."""
return FileResponse("web/templates/index.html")
return FileResponse(str(Path(__file__).parent / "templates" / "index.html"))
@app.get("/api/graph")
async def api_graph(agent_id: Optional[str] = None, fact_type: Optional[str] = None):
@app.get(
"/api/graph",
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)"
)
async def api_graph(
agent_id: Optional[str] = None,
fact_type: Optional[str] = None
):
"""Get graph data from database, optionally filtered by agent_id and fact_type."""
try:
data = await memory.get_graph_data(agent_id, fact_type)
@ -105,11 +281,16 @@ async def api_graph(agent_id: Optional[str] = None, fact_type: Optional[str] = N
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/search")
@app.post(
"/api/search",
response_model=SearchResponse,
tags=["Search"],
summary="Search all memory types",
description="Search across all memory types (world, agent, opinion) using semantic similarity and spreading activation"
)
async def api_search(request: SearchRequest):
"""Run a search and return results with trace."""
try:
# Initialize memory system
# Run search with tracing
results, trace = await memory.search_async(
agent_id=request.agent_id,
@ -123,10 +304,10 @@ async def api_search(request: SearchRequest):
# Convert trace to dict
trace_dict = trace.to_dict() if trace else None
return {
'results': results,
'trace': trace_dict
}
return SearchResponse(
results=results,
trace=trace_dict
)
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
@ -134,7 +315,13 @@ async def api_search(request: SearchRequest):
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/world_search")
@app.post(
"/api/world_search",
response_model=SearchResponse,
tags=["Search"],
summary="Search world facts",
description="Search only world facts - general knowledge about people, places, events, and things that happen"
)
async def api_world_search(request: SearchRequest):
"""Search only world facts (general knowledge about the world)."""
try:
@ -152,10 +339,10 @@ async def api_world_search(request: SearchRequest):
# Convert trace to dict
trace_dict = trace.to_dict() if trace else None
return {
'results': results,
'trace': trace_dict
}
return SearchResponse(
results=results,
trace=trace_dict
)
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
@ -163,7 +350,13 @@ async def api_world_search(request: SearchRequest):
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/agent_search")
@app.post(
"/api/agent_search",
response_model=SearchResponse,
tags=["Search"],
summary="Search agent action facts",
description="Search only agent facts - memories about what the AI agent did, actions taken, and tasks performed"
)
async def api_agent_search(request: SearchRequest):
"""Search only agent facts (facts about what the agent did)."""
try:
@ -181,10 +374,10 @@ async def api_agent_search(request: SearchRequest):
# Convert trace to dict
trace_dict = trace.to_dict() if trace else None
return {
'results': results,
'trace': trace_dict
}
return SearchResponse(
results=results,
trace=trace_dict
)
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
@ -192,7 +385,13 @@ async def api_agent_search(request: SearchRequest):
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/opinion_search")
@app.post(
"/api/opinion_search",
response_model=SearchResponse,
tags=["Search"],
summary="Search agent opinions",
description="Search only opinion facts - the agent's formed beliefs, perspectives, and viewpoints"
)
async def api_opinion_search(request: SearchRequest):
"""Search only opinion facts (agent's formed opinions and perspectives)."""
try:
@ -210,10 +409,10 @@ async def api_opinion_search(request: SearchRequest):
# Convert trace to dict
trace_dict = trace.to_dict() if trace else None
return {
'results': results,
'trace': trace_dict
}
return SearchResponse(
results=results,
trace=trace_dict
)
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
@ -221,19 +420,24 @@ async def api_opinion_search(request: SearchRequest):
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/think")
async def api_think(request: ThinkRequest):
"""
Think and formulate an answer using agent identity, world facts, and opinions.
@app.post(
"/api/think",
response_model=ThinkResponse,
tags=["Reasoning"],
summary="Think and generate answer",
description="""
Think and formulate an answer using agent identity, world facts, and opinions.
This endpoint:
1. Retrieves agent facts (agent's identity)
2. Retrieves world facts relevant to the query
3. Retrieves existing opinions (agent's perspectives)
4. Uses Groq LLM to formulate an answer
5. Extracts and stores any new opinions formed
6. Returns plain text answer, the facts used, and new opinions
This endpoint:
1. Retrieves agent facts (agent's identity)
2. Retrieves world facts relevant to the query
3. Retrieves existing opinions (agent's perspectives)
4. Uses LLM to formulate a contextual answer
5. Extracts and stores any new opinions formed
6. Returns plain text answer, the facts used, and new opinions
"""
)
async def api_think(request: ThinkRequest):
try:
# Use the memory system's think_async method
result = await memory.think_async(
@ -256,12 +460,18 @@ async def api_think(request: ThinkRequest):
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/agents")
@app.get(
"/api/agents",
response_model=AgentsResponse,
tags=["Management"],
summary="List all agents",
description="Get a list of all agent IDs that have stored memories in the system"
)
async def api_agents():
"""Get list of available agents from database."""
try:
agent_list = await memory.list_agents()
return {"agents": agent_list}
return AgentsResponse(agents=agent_list)
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
@ -269,25 +479,30 @@ async def api_agents():
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/memories/batch")
@app.post(
"/api/memories/batch",
response_model=BatchPutResponse,
tags=["Memory Storage"],
summary="Store multiple memories",
description="""
Store multiple memory items in batch with automatic fact extraction.
Features:
- Efficient batch processing
- Automatic fact extraction from natural language
- Entity recognition and linking
- Document tracking with optional upsert
- Temporal and semantic linking
The system automatically:
1. Extracts semantic facts from the content
2. Generates embeddings
3. Deduplicates similar facts
4. Creates temporal, semantic, and entity links
5. Tracks document metadata
"""
)
async def api_batch_put(request: BatchPutRequest):
"""
Store multiple memories in batch.
This endpoint calls put_batch_async to efficiently store multiple memory items.
Supports document tracking and upsert operations.
Example request:
{
"agent_id": "user123",
"items": [
{"content": "Alice works at Google", "context": "work"},
{"content": "Bob went hiking yesterday", "event_date": "2024-01-15T10:00:00Z"}
],
"document_id": "conversation_123",
"upsert": false
}
"""
try:
# Validate agent_id - prevent writing to reserved agents
RESERVED_AGENT_IDS = {"locomo"}
@ -297,9 +512,6 @@ async def api_batch_put(request: BatchPutRequest):
detail=f"Cannot write to reserved agent_id '{request.agent_id}'. Reserved agents: {', '.join(RESERVED_AGENT_IDS)}"
)
# Initialize memory system
# Prepare contents for put_batch_async
contents = []
for item in request.items:
@ -319,13 +531,13 @@ async def api_batch_put(request: BatchPutRequest):
upsert=request.upsert
)
return {
"success": True,
"message": f"Successfully stored {len(contents)} memory items",
"agent_id": request.agent_id,
"document_id": request.document_id,
"items_count": len(contents)
}
return BatchPutResponse(
success=True,
message=f"Successfully stored {len(contents)} memory items",
agent_id=request.agent_id,
document_id=request.document_id,
items_count=len(contents)
)
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
@ -365,4 +577,4 @@ if __name__ == "__main__":
print(" GET /api/agents - List available agents")
print("\n" + "=" * 80 + "\n")
uvicorn.run("server:app", host="0.0.0.0", port=8080, reload=True)
uvicorn.run("memora.web.server:app", host="0.0.0.0", port=8080, reload=True)

837
openapi.json Normal file
View file

@ -0,0 +1,837 @@
{
"openapi": "3.1.0",
"info": {
"title": "Agent Memory API",
"description": "\nA temporal-semantic memory system for AI agents that stores, retrieves, and reasons over memories.\n\n## Features\n\n* **Batch Memory Storage**: Store multiple memories efficiently with automatic fact extraction\n* **Semantic Search**: Find relevant memories using natural language queries\n* **Fact Type Filtering**: Search across world facts, agent actions, and opinions separately\n* **Think Endpoint**: Generate contextual answers based on agent identity and memories\n* **Graph Visualization**: Interactive memory graph visualization\n* **Document Tracking**: Track and manage memory documents with upsert support\n\n## Architecture\n\nThe system uses:\n- **Temporal Links**: Connect memories that are close in time\n- **Semantic Links**: Connect semantically similar memories\n- **Entity Links**: Connect memories that mention the same entities\n- **Spreading Activation**: Intelligent traversal for memory retrieval\n ",
"contact": {
"name": "Memory System"
},
"license": {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "1.0.0"
},
"paths": {
"/api/graph": {
"get": {
"tags": [
"Visualization"
],
"summary": "Get memory graph data",
"description": "Retrieve graph data for visualization, optionally filtered by agent_id and fact_type (world/agent/opinion)",
"operationId": "api_graph_api_graph_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"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GraphDataResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/search": {
"post": {
"tags": [
"Search"
],
"summary": "Search all memory types",
"description": "Search across all memory types (world, agent, opinion) using semantic similarity and spreading activation",
"operationId": "api_search_api_search_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/world_search": {
"post": {
"tags": [
"Search"
],
"summary": "Search world facts",
"description": "Search only world facts - general knowledge about people, places, events, and things that happen",
"operationId": "api_world_search_api_world_search_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/agent_search": {
"post": {
"tags": [
"Search"
],
"summary": "Search agent action facts",
"description": "Search only agent facts - memories about what the AI agent did, actions taken, and tasks performed",
"operationId": "api_agent_search_api_agent_search_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/opinion_search": {
"post": {
"tags": [
"Search"
],
"summary": "Search agent opinions",
"description": "Search only opinion facts - the agent's formed beliefs, perspectives, and viewpoints",
"operationId": "api_opinion_search_api_opinion_search_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/think": {
"post": {
"tags": [
"Reasoning"
],
"summary": "Think and generate answer",
"description": "Think and formulate an answer using agent identity, world facts, and opinions.\n\nThis endpoint:\n1. Retrieves agent facts (agent's identity)\n2. Retrieves world facts relevant to the query\n3. Retrieves existing opinions (agent's perspectives)\n4. Uses LLM to formulate a contextual answer\n5. Extracts and stores any new opinions formed\n6. Returns plain text answer, the facts used, and new opinions",
"operationId": "api_think_api_think_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ThinkRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ThinkResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/agents": {
"get": {
"tags": [
"Management"
],
"summary": "List all agents",
"description": "Get a list of all agent IDs that have stored memories in the system",
"operationId": "api_agents_api_agents_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AgentsResponse"
}
}
}
}
}
}
},
"/api/memories/batch": {
"post": {
"tags": [
"Memory Storage"
],
"summary": "Store multiple memories",
"description": "Store multiple memory items in batch with automatic fact extraction.\n\nFeatures:\n- Efficient batch processing\n- Automatic fact extraction from natural language\n- Entity recognition and linking\n- Document tracking with optional upsert\n- Temporal and semantic linking\n\nThe system automatically:\n1. Extracts semantic facts from the content\n2. Generates embeddings\n3. Deduplicates similar facts\n4. Creates temporal, semantic, and entity links\n5. Tracks document metadata",
"operationId": "api_batch_put_api_memories_batch_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BatchPutRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BatchPutResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/locomo": {
"get": {
"summary": "Api Locomo",
"description": "Get Locomo benchmark results.",
"operationId": "api_locomo_api_locomo_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
}
},
"components": {
"schemas": {
"AgentsResponse": {
"properties": {
"agents": {
"items": {
"type": "string"
},
"type": "array",
"title": "Agents"
}
},
"type": "object",
"required": [
"agents"
],
"title": "AgentsResponse",
"description": "Response model for agents list endpoint.",
"example": {
"agents": [
"user123",
"agent_alice",
"agent_bob"
]
}
},
"BatchPutRequest": {
"properties": {
"agent_id": {
"type": "string",
"title": "Agent Id"
},
"items": {
"items": {
"$ref": "#/components/schemas/MemoryItem"
},
"type": "array",
"title": "Items"
},
"document_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Document Id"
},
"document_metadata": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"title": "Document Metadata"
},
"upsert": {
"type": "boolean",
"title": "Upsert",
"default": false
}
},
"type": "object",
"required": [
"agent_id",
"items"
],
"title": "BatchPutRequest",
"description": "Request model for batch put endpoint.",
"example": {
"agent_id": "user123",
"document_id": "conversation_123",
"items": [
{
"content": "Alice works at Google",
"context": "work"
},
{
"content": "Bob went hiking yesterday",
"event_date": "2024-01-15T10:00:00Z"
}
],
"upsert": false
}
},
"BatchPutResponse": {
"properties": {
"success": {
"type": "boolean",
"title": "Success"
},
"message": {
"type": "string",
"title": "Message"
},
"agent_id": {
"type": "string",
"title": "Agent Id"
},
"document_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Document Id"
},
"items_count": {
"type": "integer",
"title": "Items Count"
}
},
"type": "object",
"required": [
"success",
"message",
"agent_id",
"items_count"
],
"title": "BatchPutResponse",
"description": "Response model for batch put endpoint.",
"example": {
"agent_id": "user123",
"document_id": "conversation_123",
"items_count": 2,
"message": "Successfully stored 2 memory items",
"success": true
}
},
"GraphDataResponse": {
"properties": {
"nodes": {
"items": {
"additionalProperties": true,
"type": "object"
},
"type": "array",
"title": "Nodes"
},
"edges": {
"items": {
"additionalProperties": true,
"type": "object"
},
"type": "array",
"title": "Edges"
}
},
"type": "object",
"required": [
"nodes",
"edges"
],
"title": "GraphDataResponse",
"description": "Response model for graph data endpoint.",
"example": {
"edges": [
{
"from": "1",
"to": "2",
"type": "semantic",
"weight": 0.8
}
],
"nodes": [
{
"id": "1",
"label": "Alice works at Google",
"type": "world"
},
{
"id": "2",
"label": "Bob went hiking",
"type": "world"
}
]
}
},
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
},
"MemoryItem": {
"properties": {
"content": {
"type": "string",
"title": "Content"
},
"event_date": {
"anyOf": [
{
"type": "string",
"format": "date-time"
},
{
"type": "null"
}
],
"title": "Event Date"
},
"context": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Context"
}
},
"type": "object",
"required": [
"content"
],
"title": "MemoryItem",
"description": "Single memory item for batch put.",
"example": {
"content": "Alice mentioned she's working on a new ML model",
"context": "team meeting",
"event_date": "2024-01-15T10:30:00Z"
}
},
"SearchRequest": {
"properties": {
"query": {
"type": "string",
"title": "Query"
},
"agent_id": {
"type": "string",
"title": "Agent Id",
"default": "default"
},
"thinking_budget": {
"type": "integer",
"title": "Thinking Budget",
"default": 100
},
"top_k": {
"type": "integer",
"title": "Top K",
"default": 10
},
"mmr_lambda": {
"type": "number",
"title": "Mmr Lambda",
"default": 0.5
},
"trace": {
"type": "boolean",
"title": "Trace",
"default": false
}
},
"type": "object",
"required": [
"query"
],
"title": "SearchRequest",
"description": "Request model for search endpoint.",
"example": {
"agent_id": "user123",
"mmr_lambda": 0.5,
"query": "What did Alice say about machine learning?",
"thinking_budget": 100,
"top_k": 10,
"trace": true
}
},
"SearchResponse": {
"properties": {
"results": {
"items": {
"additionalProperties": true,
"type": "object"
},
"type": "array",
"title": "Results"
},
"trace": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"title": "Trace"
}
},
"type": "object",
"required": [
"results"
],
"title": "SearchResponse",
"description": "Response model for search endpoints.",
"example": {
"results": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"score": 0.95,
"text": "Alice works at Google on the AI team"
}
],
"trace": {
"num_results": 1,
"query": "What did Alice say about machine learning?",
"time_seconds": 0.123
}
}
},
"ThinkRequest": {
"properties": {
"query": {
"type": "string",
"title": "Query"
},
"agent_id": {
"type": "string",
"title": "Agent Id",
"default": "default"
},
"thinking_budget": {
"type": "integer",
"title": "Thinking Budget",
"default": 50
},
"top_k": {
"type": "integer",
"title": "Top K",
"default": 10
}
},
"type": "object",
"required": [
"query"
],
"title": "ThinkRequest",
"description": "Request model for think endpoint.",
"example": {
"agent_id": "user123",
"query": "What do you think about artificial intelligence?",
"thinking_budget": 50,
"top_k": 10
}
},
"ThinkResponse": {
"properties": {
"text": {
"type": "string",
"title": "Text"
},
"based_on": {
"additionalProperties": {
"items": {
"additionalProperties": true,
"type": "object"
},
"type": "array"
},
"type": "object",
"title": "Based On"
},
"new_opinions": {
"items": {
"type": "string"
},
"type": "array",
"title": "New Opinions",
"default": []
}
},
"type": "object",
"required": [
"text",
"based_on"
],
"title": "ThinkResponse",
"description": "Response model for think endpoint.",
"example": {
"based_on": {
"agent": [
{
"score": 0.85,
"text": "I discussed AI applications last week"
}
],
"opinion": [
{
"score": 0.8,
"text": "I believe AI should be used ethically"
}
],
"world": [
{
"score": 0.9,
"text": "AI is used in healthcare"
}
]
},
"new_opinions": [
"AI has great potential when used responsibly"
],
"text": "Based on my understanding, AI is a transformative technology..."
}
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
}
}
}
}

View file

@ -3,7 +3,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "agent_memory"
name = "memora"
version = "0.1.0"
description = "Temporal + Semantic + Entity Memory System for AI agents using PostgreSQL"
readme = "README.md"
@ -29,7 +29,7 @@ dependencies = [
]
[tool.hatch.build.targets.wheel]
packages = ["memory", "web"]
packages = ["memora"]
[tool.pytest.ini_options]
log_cli = true

View file

@ -6,7 +6,7 @@ import pytest_asyncio
import os
import asyncio
from dotenv import load_dotenv
from memory import TemporalSemanticMemory
from memora import TemporalSemanticMemory
import asyncpg
load_dotenv()

View file

@ -2,7 +2,7 @@
Test chunking functionality for large documents.
"""
import pytest
from memory.llm_client import chunk_text
from memora.llm_client import chunk_text
def test_chunk_text_small():

View file

@ -5,7 +5,7 @@ import logging
import os
import pytest
from datetime import datetime, timezone
from memory import TemporalSemanticMemory
from memora import TemporalSemanticMemory
@pytest.mark.asyncio

View file

@ -10,7 +10,7 @@ import json
import pytest
from datetime import datetime, timezone
from pathlib import Path
from memory import TemporalSemanticMemory
from memora import TemporalSemanticMemory
# Configure logging to show performance metrics

View file

@ -4,8 +4,8 @@ Test search tracing functionality.
import pytest
import asyncio
import os
from memory.temporal_semantic_memory import TemporalSemanticMemory
from memory.search_trace import SearchTrace
from memora.temporal_semantic_memory import TemporalSemanticMemory
from memora.search_trace import SearchTrace
from datetime import datetime, timezone

View file

@ -3,7 +3,7 @@ Test temporal extraction and per-fact dating.
"""
import pytest
from datetime import datetime, timezone, timedelta
from memory.llm_client import extract_facts_from_text
from memora.llm_client import extract_facts_from_text
@pytest.mark.asyncio

View file

@ -4,7 +4,7 @@ Test think function for opinion generation and consistency.
import pytest
import os
from datetime import datetime, timezone
from memory import TemporalSemanticMemory
from memora import TemporalSemanticMemory
@pytest.mark.asyncio

90
uv.lock
View file

@ -6,51 +6,6 @@ resolution-markers = [
"python_full_version < '3.12'",
]
[[package]]
name = "agent-memory"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "alembic" },
{ name = "asyncpg" },
{ name = "fastapi", extra = ["standard"] },
{ name = "greenlet" },
{ name = "langchain-text-splitters" },
{ name = "openai" },
{ name = "pgvector" },
{ name = "psycopg2-binary" },
{ name = "pydantic" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-timeout" },
{ name = "python-dotenv" },
{ name = "rich" },
{ name = "sentence-transformers" },
{ name = "sqlalchemy" },
{ name = "uvicorn" },
]
[package.metadata]
requires-dist = [
{ name = "alembic", specifier = ">=1.17.1" },
{ name = "asyncpg", specifier = ">=0.29.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.120.3" },
{ name = "greenlet", specifier = ">=3.2.4" },
{ name = "langchain-text-splitters", specifier = ">=0.3.0" },
{ name = "openai", specifier = ">=1.0.0" },
{ name = "pgvector", specifier = ">=0.4.1" },
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
{ name = "pydantic", specifier = ">=2.0.0" },
{ name = "pytest", specifier = ">=7.0.0" },
{ name = "pytest-asyncio", specifier = ">=0.21.0" },
{ name = "pytest-timeout", specifier = ">=2.4.0" },
{ name = "python-dotenv", specifier = ">=1.0.0" },
{ name = "rich", specifier = ">=13.0.0" },
{ name = "sentence-transformers", specifier = ">=2.2.0" },
{ name = "sqlalchemy", specifier = ">=2.0.44" },
{ name = "uvicorn", specifier = ">=0.38.0" },
]
[[package]]
name = "alembic"
version = "1.17.1"
@ -807,6 +762,51 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
]
[[package]]
name = "memora"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "alembic" },
{ name = "asyncpg" },
{ name = "fastapi", extra = ["standard"] },
{ name = "greenlet" },
{ name = "langchain-text-splitters" },
{ name = "openai" },
{ name = "pgvector" },
{ name = "psycopg2-binary" },
{ name = "pydantic" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-timeout" },
{ name = "python-dotenv" },
{ name = "rich" },
{ name = "sentence-transformers" },
{ name = "sqlalchemy" },
{ name = "uvicorn" },
]
[package.metadata]
requires-dist = [
{ name = "alembic", specifier = ">=1.17.1" },
{ name = "asyncpg", specifier = ">=0.29.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.120.3" },
{ name = "greenlet", specifier = ">=3.2.4" },
{ name = "langchain-text-splitters", specifier = ">=0.3.0" },
{ name = "openai", specifier = ">=1.0.0" },
{ name = "pgvector", specifier = ">=0.4.1" },
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
{ name = "pydantic", specifier = ">=2.0.0" },
{ name = "pytest", specifier = ">=7.0.0" },
{ name = "pytest-asyncio", specifier = ">=0.21.0" },
{ name = "pytest-timeout", specifier = ">=2.4.0" },
{ name = "python-dotenv", specifier = ">=1.0.0" },
{ name = "rich", specifier = ">=13.0.0" },
{ name = "sentence-transformers", specifier = ">=2.2.0" },
{ name = "sqlalchemy", specifier = ">=2.0.44" },
{ name = "uvicorn", specifier = ">=0.38.0" },
]
[[package]]
name = "mpmath"
version = "1.3.0"