diff --git a/README.md b/README.md index d81c47b9..c6f12cff 100644 --- a/README.md +++ b/README.md @@ -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() diff --git a/benchmarks/common/benchmark_runner.py b/benchmarks/common/benchmark_runner.py index 8834470d..0f7148f6 100644 --- a/benchmarks/common/benchmark_runner.py +++ b/benchmarks/common/benchmark_runner.py @@ -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() diff --git a/benchmarks/locomo/run_benchmark.py b/benchmarks/locomo/run_benchmark.py index 12f08375..5e71bc68 100644 --- a/benchmarks/locomo/run_benchmark.py +++ b/benchmarks/locomo/run_benchmark.py @@ -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 diff --git a/benchmarks/longmemeval/run_benchmark.py b/benchmarks/longmemeval/run_benchmark.py index e4041649..f4fd53ba 100644 --- a/benchmarks/longmemeval/run_benchmark.py +++ b/benchmarks/longmemeval/run_benchmark.py @@ -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 diff --git a/examples/library_usage_example.py b/examples/library_usage_example.py index a3a15cc9..eb2532d2 100644 --- a/examples/library_usage_example.py +++ b/examples/library_usage_example.py @@ -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(): diff --git a/generate_openapi.py b/generate_openapi.py new file mode 100644 index 00000000..ecd7ea73 --- /dev/null +++ b/generate_openapi.py @@ -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) diff --git a/memory/__init__.py b/memora/__init__.py similarity index 100% rename from memory/__init__.py rename to memora/__init__.py diff --git a/memory/entity_resolver.py b/memora/entity_resolver.py similarity index 100% rename from memory/entity_resolver.py rename to memora/entity_resolver.py diff --git a/memory/llm_client.py b/memora/llm_client.py similarity index 100% rename from memory/llm_client.py rename to memora/llm_client.py diff --git a/memory/models.py b/memora/models.py similarity index 100% rename from memory/models.py rename to memora/models.py diff --git a/memory/operations/README.md b/memora/operations/README.md similarity index 100% rename from memory/operations/README.md rename to memora/operations/README.md diff --git a/memory/operations/__init__.py b/memora/operations/__init__.py similarity index 100% rename from memory/operations/__init__.py rename to memora/operations/__init__.py diff --git a/memory/operations/batch_operations.py b/memora/operations/batch_operations.py similarity index 100% rename from memory/operations/batch_operations.py rename to memora/operations/batch_operations.py diff --git a/memory/operations/embedding_operations.py b/memora/operations/embedding_operations.py similarity index 100% rename from memory/operations/embedding_operations.py rename to memora/operations/embedding_operations.py diff --git a/memory/operations/link_operations.py b/memora/operations/link_operations.py similarity index 96% rename from memory/operations/link_operations.py rename to memora/operations/link_operations.py index 054fe421..7d472246 100644 --- a/memory/operations/link_operations.py +++ b/memora/operations/link_operations.py @@ -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): diff --git a/memory/operations/search_operations.py b/memora/operations/search_operations.py similarity index 100% rename from memory/operations/search_operations.py rename to memora/operations/search_operations.py diff --git a/memory/operations/think_operations.py b/memora/operations/think_operations.py similarity index 100% rename from memory/operations/think_operations.py rename to memora/operations/think_operations.py diff --git a/memory/search_trace.py b/memora/search_trace.py similarity index 100% rename from memory/search_trace.py rename to memora/search_trace.py diff --git a/memory/search_tracer.py b/memora/search_tracer.py similarity index 100% rename from memory/search_tracer.py rename to memora/search_tracer.py diff --git a/memory/temporal_semantic_memory.py b/memora/temporal_semantic_memory.py similarity index 98% rename from memory/temporal_semantic_memory.py rename to memora/temporal_semantic_memory.py index 148d4b1c..e4e447f0 100644 --- a/memory/temporal_semantic_memory.py +++ b/memora/temporal_semantic_memory.py @@ -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: diff --git a/memory/utils.py b/memora/utils.py similarity index 100% rename from memory/utils.py rename to memora/utils.py diff --git a/web/__init__.py b/memora/web/__init__.py similarity index 100% rename from web/__init__.py rename to memora/web/__init__.py diff --git a/web/server.py b/memora/web/server.py similarity index 51% rename from web/server.py rename to memora/web/server.py index a056382a..c6d036a7 100644 --- a/web/server.py +++ b/memora/web/server.py @@ -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) diff --git a/web/static/css/styles.css b/memora/web/static/css/styles.css similarity index 100% rename from web/static/css/styles.css rename to memora/web/static/css/styles.css diff --git a/web/static/js/app.js b/memora/web/static/js/app.js similarity index 100% rename from web/static/js/app.js rename to memora/web/static/js/app.js diff --git a/web/static/js/locomo.js b/memora/web/static/js/locomo.js similarity index 100% rename from web/static/js/locomo.js rename to memora/web/static/js/locomo.js diff --git a/web/templates/index.html b/memora/web/templates/index.html similarity index 100% rename from web/templates/index.html rename to memora/web/templates/index.html diff --git a/openapi.json b/openapi.json new file mode 100644 index 00000000..03b342d6 --- /dev/null +++ b/openapi.json @@ -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" + } + } + } +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 33d5d9f2..f4afe7a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index 96a40091..1bf035a4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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() diff --git a/tests/test_chunking.py b/tests/test_chunking.py index 3ebc973c..59d4b368 100644 --- a/tests/test_chunking.py +++ b/tests/test_chunking.py @@ -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(): diff --git a/tests/test_document_tracking.py b/tests/test_document_tracking.py index 8250d541..b7d18154 100644 --- a/tests/test_document_tracking.py +++ b/tests/test_document_tracking.py @@ -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 diff --git a/tests/test_performance_tuning.py b/tests/test_performance_tuning.py index 6d9e180e..471ee415 100644 --- a/tests/test_performance_tuning.py +++ b/tests/test_performance_tuning.py @@ -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 diff --git a/tests/test_search_trace.py b/tests/test_search_trace.py index ef26eeae..7dcf0d7b 100644 --- a/tests/test_search_trace.py +++ b/tests/test_search_trace.py @@ -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 diff --git a/tests/test_temporal_extraction.py b/tests/test_temporal_extraction.py index 458960b4..5aa8c580 100644 --- a/tests/test_temporal_extraction.py +++ b/tests/test_temporal_extraction.py @@ -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 diff --git a/tests/test_think.py b/tests/test_think.py index 8da4c5c5..b54deb8c 100644 --- a/tests/test_think.py +++ b/tests/test_think.py @@ -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 diff --git a/uv.lock b/uv.lock index 4cd01958..7413e996 100644 --- a/uv.lock +++ b/uv.lock @@ -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"