emb abstraction
This commit is contained in:
parent
7d8d07d1aa
commit
992faa4efe
6 changed files with 90 additions and 131 deletions
|
|
@ -11,7 +11,7 @@ from pathlib import Path
|
||||||
# Add parent directory to path to import memory module
|
# Add parent directory to path to import memory module
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
from memory.web.server import app
|
from memora.web.server import app
|
||||||
|
|
||||||
def generate_openapi_spec(output_path: str = "openapi.json"):
|
def generate_openapi_spec(output_path: str = "openapi.json"):
|
||||||
"""Generate OpenAPI spec and save to file."""
|
"""Generate OpenAPI spec and save to file."""
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ from .search_trace import (
|
||||||
SearchPhaseMetrics,
|
SearchPhaseMetrics,
|
||||||
)
|
)
|
||||||
from .search_tracer import SearchTracer
|
from .search_tracer import SearchTracer
|
||||||
|
from .embeddings import Embeddings, SentenceTransformersEmbeddings
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"TemporalSemanticMemory",
|
"TemporalSemanticMemory",
|
||||||
|
|
@ -29,5 +30,7 @@ __all__ = [
|
||||||
"PruningDecision",
|
"PruningDecision",
|
||||||
"SearchSummary",
|
"SearchSummary",
|
||||||
"SearchPhaseMetrics",
|
"SearchPhaseMetrics",
|
||||||
|
"Embeddings",
|
||||||
|
"SentenceTransformersEmbeddings",
|
||||||
]
|
]
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.1.0"
|
||||||
|
|
|
||||||
|
|
@ -5,82 +5,48 @@ Embedding generation operations for memory units.
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
from typing import List
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Global process pool for parallel embedding generation
|
|
||||||
_PROCESS_POOL = None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_worker_model():
|
|
||||||
"""Get or load the embedding model in worker process."""
|
|
||||||
from sentence_transformers import SentenceTransformer
|
|
||||||
global _worker_model
|
|
||||||
if '_worker_model' not in globals():
|
|
||||||
globals()['_worker_model'] = SentenceTransformer("BAAI/bge-small-en-v1.5")
|
|
||||||
return globals()['_worker_model']
|
|
||||||
|
|
||||||
|
|
||||||
def _encode_batch_worker(texts: List[str]) -> List[List[float]]:
|
|
||||||
"""
|
|
||||||
Worker function for process pool - encodes texts to embeddings.
|
|
||||||
|
|
||||||
This function runs in a separate process and loads its own model.
|
|
||||||
"""
|
|
||||||
model = _get_worker_model()
|
|
||||||
embeddings = model.encode(texts, convert_to_numpy=True, show_progress_bar=False)
|
|
||||||
return [emb.tolist() for emb in embeddings]
|
|
||||||
|
|
||||||
|
|
||||||
def _get_process_pool():
|
|
||||||
"""Get or create the global process pool."""
|
|
||||||
global _PROCESS_POOL
|
|
||||||
if _PROCESS_POOL is None:
|
|
||||||
# Use 4 worker processes for true parallelism
|
|
||||||
_PROCESS_POOL = ProcessPoolExecutor(max_workers=4)
|
|
||||||
return _PROCESS_POOL
|
|
||||||
|
|
||||||
|
|
||||||
class EmbeddingOperationsMixin:
|
class EmbeddingOperationsMixin:
|
||||||
"""Mixin class for embedding operations."""
|
"""Mixin class for embedding operations."""
|
||||||
|
|
||||||
def _generate_embedding(self, text: str) -> List[float]:
|
def _generate_embedding(self, text: str) -> List[float]:
|
||||||
"""
|
"""
|
||||||
Generate embedding for text using local SentenceTransformer model.
|
Generate embedding for text using the configured embeddings backend.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text: Text to embed
|
text: Text to embed
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
384-dimensional embedding vector (bge-small-en-v1.5)
|
Embedding vector (dimension depends on embeddings backend)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
embedding = self.embedding_model.encode(text, convert_to_numpy=True, show_progress_bar=False)
|
embeddings = self.embeddings.encode([text])
|
||||||
return embedding.tolist()
|
return embeddings[0]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Failed to generate embedding: {str(e)}")
|
raise Exception(f"Failed to generate embedding: {str(e)}")
|
||||||
|
|
||||||
async def _generate_embeddings_batch(self, texts: List[str]) -> List[List[float]]:
|
async def _generate_embeddings_batch(self, texts: List[str]) -> List[List[float]]:
|
||||||
"""
|
"""
|
||||||
Generate embeddings for multiple texts using local model in parallel.
|
Generate embeddings for multiple texts using the configured embeddings backend.
|
||||||
|
|
||||||
Uses a ProcessPoolExecutor to achieve TRUE parallelism for CPU-bound
|
Runs the embedding generation in a thread pool to avoid blocking the event loop
|
||||||
embedding generation. Each worker process loads its own model copy.
|
for CPU-bound operations.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
texts: List of texts to embed
|
texts: List of texts to embed
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of 384-dimensional embeddings in same order as input texts
|
List of embeddings in same order as input texts
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Run in process pool for true parallelism
|
# Run embeddings in thread pool to avoid blocking event loop
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
pool = _get_process_pool()
|
|
||||||
embeddings = await loop.run_in_executor(
|
embeddings = await loop.run_in_executor(
|
||||||
pool,
|
None, # Use default thread pool
|
||||||
_encode_batch_worker,
|
self.embeddings.encode,
|
||||||
texts
|
texts
|
||||||
)
|
)
|
||||||
return embeddings
|
return embeddings
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,10 @@ import os
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
import asyncpg
|
import asyncpg
|
||||||
from sentence_transformers import SentenceTransformer
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from .embeddings import Embeddings, SentenceTransformersEmbeddings
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import uuid
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -38,44 +37,6 @@ def utcnow():
|
||||||
# Logger for memory system
|
# Logger for memory system
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Global process pool for parallel embedding generation
|
|
||||||
# Each process loads its own copy of the embedding model
|
|
||||||
# This provides TRUE parallelism for CPU-bound embedding operations
|
|
||||||
_PROCESS_POOL = None
|
|
||||||
_EMBEDDING_MODEL_NAME = "BAAI/bge-small-en-v1.5"
|
|
||||||
|
|
||||||
# Process-local model cache (one per worker process)
|
|
||||||
_worker_model = None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_worker_model():
|
|
||||||
"""Get or load the embedding model in worker process."""
|
|
||||||
global _worker_model
|
|
||||||
if _worker_model is None:
|
|
||||||
_worker_model = SentenceTransformer(_EMBEDDING_MODEL_NAME)
|
|
||||||
return _worker_model
|
|
||||||
|
|
||||||
|
|
||||||
def _encode_batch_worker(texts: List[str]) -> List[List[float]]:
|
|
||||||
"""
|
|
||||||
Worker function for process pool - encodes texts to embeddings.
|
|
||||||
|
|
||||||
This function runs in a separate process and loads its own model.
|
|
||||||
"""
|
|
||||||
model = _get_worker_model()
|
|
||||||
embeddings = model.encode(texts, convert_to_numpy=True, show_progress_bar=False)
|
|
||||||
return [emb.tolist() for emb in embeddings]
|
|
||||||
|
|
||||||
|
|
||||||
def _get_process_pool():
|
|
||||||
"""Get or create the global process pool."""
|
|
||||||
global _PROCESS_POOL
|
|
||||||
if _PROCESS_POOL is None:
|
|
||||||
# Use 4 worker processes for true parallelism
|
|
||||||
# Adjust based on your CPU cores (each process loads ~500MB model)
|
|
||||||
_PROCESS_POOL = ProcessPoolExecutor(max_workers=4)
|
|
||||||
return _PROCESS_POOL
|
|
||||||
|
|
||||||
|
|
||||||
class TemporalSemanticMemory(
|
class TemporalSemanticMemory(
|
||||||
EmbeddingOperationsMixin,
|
EmbeddingOperationsMixin,
|
||||||
|
|
@ -92,14 +53,16 @@ class TemporalSemanticMemory(
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
db_url: Optional[str] = None,
|
db_url: Optional[str] = None,
|
||||||
embedding_model: str = "BAAI/bge-small-en-v1.5",
|
embeddings: Optional[Embeddings] = None,
|
||||||
|
embedding_model: Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Initialize the temporal + semantic memory system.
|
Initialize the temporal + semantic memory system.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db_url: PostgreSQL connection URL (postgresql://user:pass@host:port/dbname)
|
db_url: PostgreSQL connection URL (postgresql://user:pass@host:port/dbname)
|
||||||
embedding_model: Name of the SentenceTransformer model to use
|
embeddings: Embeddings implementation to use. If not provided, uses SentenceTransformersEmbeddings
|
||||||
|
embedding_model: (Deprecated) Name of the SentenceTransformer model to use. Use embeddings parameter instead.
|
||||||
"""
|
"""
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
|
@ -118,10 +81,13 @@ class TemporalSemanticMemory(
|
||||||
# Initialize entity resolver (will be created in initialize())
|
# Initialize entity resolver (will be created in initialize())
|
||||||
self.entity_resolver = None
|
self.entity_resolver = None
|
||||||
|
|
||||||
# Initialize local embedding model (384 dimensions)
|
# Initialize embeddings
|
||||||
logger.info(f"Loading embedding model: {embedding_model}...")
|
if embeddings is not None:
|
||||||
self.embedding_model = SentenceTransformer(embedding_model)
|
self.embeddings = embeddings
|
||||||
logger.info(f"Model loaded (embedding dim: {self.embedding_model.get_sentence_embedding_dimension()})")
|
else:
|
||||||
|
# Default to SentenceTransformersEmbeddings
|
||||||
|
model_name = embedding_model or "BAAI/bge-small-en-v1.5"
|
||||||
|
self.embeddings = SentenceTransformersEmbeddings(model_name)
|
||||||
|
|
||||||
# Background queue for access count updates (to avoid blocking searches)
|
# Background queue for access count updates (to avoid blocking searches)
|
||||||
self._access_count_queue = asyncio.Queue()
|
self._access_count_queue = asyncio.Queue()
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,6 @@ Web interface for memory system.
|
||||||
|
|
||||||
Provides FastAPI app and visualization interface.
|
Provides FastAPI app and visualization interface.
|
||||||
"""
|
"""
|
||||||
from .server import app, memory
|
from .server import app, create_app
|
||||||
|
|
||||||
__all__ = ["app", "memory"]
|
__all__ = ["app", "create_app"]
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,30 @@ from datetime import datetime
|
||||||
|
|
||||||
# Import from parent memora package
|
# Import from parent memora package
|
||||||
from memora import TemporalSemanticMemory
|
from memora import TemporalSemanticMemory
|
||||||
|
from memora.embeddings import Embeddings
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
app = FastAPI(
|
|
||||||
title="Agent Memory API",
|
def create_app(embeddings: Optional[Embeddings] = None, db_url: Optional[str] = None) -> FastAPI:
|
||||||
version="1.0.0",
|
"""
|
||||||
description="""
|
Create and configure the FastAPI application.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
embeddings: Optional custom embeddings implementation. If not provided,
|
||||||
|
uses default SentenceTransformersEmbeddings.
|
||||||
|
db_url: Optional database URL. If not provided, uses DATABASE_URL env var.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configured FastAPI application
|
||||||
|
"""
|
||||||
|
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.
|
A temporal-semantic memory system for AI agents that stores, retrieves, and reasons over memories.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
@ -46,18 +60,42 @@ The system uses:
|
||||||
- **Semantic Links**: Connect semantically similar memories
|
- **Semantic Links**: Connect semantically similar memories
|
||||||
- **Entity Links**: Connect memories that mention the same entities
|
- **Entity Links**: Connect memories that mention the same entities
|
||||||
- **Spreading Activation**: Intelligent traversal for memory retrieval
|
- **Spreading Activation**: Intelligent traversal for memory retrieval
|
||||||
""",
|
""",
|
||||||
contact={
|
contact={
|
||||||
"name": "Memory System",
|
"name": "Memory System",
|
||||||
},
|
},
|
||||||
license_info={
|
license_info={
|
||||||
"name": "Apache 2.0",
|
"name": "Apache 2.0",
|
||||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
|
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mount static files
|
# Mount static files
|
||||||
app.mount("/static", StaticFiles(directory=str(Path(__file__).parent / "static")), name="static")
|
app.mount("/static", StaticFiles(directory=str(Path(__file__).parent / "static")), name="static")
|
||||||
|
|
||||||
|
# Initialize memory system with custom embeddings if provided
|
||||||
|
memory = TemporalSemanticMemory(db_url=db_url, embeddings=embeddings)
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
"""Initialize memory system on startup."""
|
||||||
|
await memory.initialize()
|
||||||
|
logging.info("Memory system initialized")
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown_event():
|
||||||
|
"""Cleanup memory system on shutdown."""
|
||||||
|
await memory.close()
|
||||||
|
logging.info("Memory system closed")
|
||||||
|
|
||||||
|
# Store memory instance on app for route handlers to access
|
||||||
|
app.state.memory = memory
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
# Create default app instance with default embeddings
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
|
||||||
class SearchRequest(BaseModel):
|
class SearchRequest(BaseModel):
|
||||||
|
|
@ -239,20 +277,6 @@ class GraphDataResponse(BaseModel):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
memory = TemporalSemanticMemory()
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
|
||||||
async def startup_event():
|
|
||||||
"""Initialize memory system on startup."""
|
|
||||||
await memory.initialize()
|
|
||||||
logging.info("Memory system initialized")
|
|
||||||
|
|
||||||
@app.on_event("shutdown")
|
|
||||||
async def shutdown_event():
|
|
||||||
"""Cleanup memory system on shutdown."""
|
|
||||||
await memory.close()
|
|
||||||
logging.info("Memory system closed")
|
|
||||||
|
|
||||||
@app.get("/", include_in_schema=False)
|
@app.get("/", include_in_schema=False)
|
||||||
async def index():
|
async def index():
|
||||||
"""Serve the visualization page."""
|
"""Serve the visualization page."""
|
||||||
|
|
@ -272,7 +296,7 @@ async def api_graph(
|
||||||
):
|
):
|
||||||
"""Get graph data from database, optionally filtered by agent_id and fact_type."""
|
"""Get graph data from database, optionally filtered by agent_id and fact_type."""
|
||||||
try:
|
try:
|
||||||
data = await memory.get_graph_data(agent_id, fact_type)
|
data = await app.state.memory.get_graph_data(agent_id, fact_type)
|
||||||
return data
|
return data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
@ -292,7 +316,7 @@ async def api_search(request: SearchRequest):
|
||||||
"""Run a search and return results with trace."""
|
"""Run a search and return results with trace."""
|
||||||
try:
|
try:
|
||||||
# Run search with tracing
|
# Run search with tracing
|
||||||
results, trace = await memory.search_async(
|
results, trace = await app.state.memory.search_async(
|
||||||
agent_id=request.agent_id,
|
agent_id=request.agent_id,
|
||||||
query=request.query,
|
query=request.query,
|
||||||
thinking_budget=request.thinking_budget,
|
thinking_budget=request.thinking_budget,
|
||||||
|
|
@ -326,7 +350,7 @@ async def api_world_search(request: SearchRequest):
|
||||||
"""Search only world facts (general knowledge about the world)."""
|
"""Search only world facts (general knowledge about the world)."""
|
||||||
try:
|
try:
|
||||||
# Run search with fact_type filter for 'world'
|
# Run search with fact_type filter for 'world'
|
||||||
results, trace = await memory.search_async(
|
results, trace = await app.state.memory.search_async(
|
||||||
agent_id=request.agent_id,
|
agent_id=request.agent_id,
|
||||||
query=request.query,
|
query=request.query,
|
||||||
thinking_budget=request.thinking_budget,
|
thinking_budget=request.thinking_budget,
|
||||||
|
|
@ -361,7 +385,7 @@ async def api_agent_search(request: SearchRequest):
|
||||||
"""Search only agent facts (facts about what the agent did)."""
|
"""Search only agent facts (facts about what the agent did)."""
|
||||||
try:
|
try:
|
||||||
# Run search with fact_type filter for 'agent'
|
# Run search with fact_type filter for 'agent'
|
||||||
results, trace = await memory.search_async(
|
results, trace = await app.state.memory.search_async(
|
||||||
agent_id=request.agent_id,
|
agent_id=request.agent_id,
|
||||||
query=request.query,
|
query=request.query,
|
||||||
thinking_budget=request.thinking_budget,
|
thinking_budget=request.thinking_budget,
|
||||||
|
|
@ -396,7 +420,7 @@ async def api_opinion_search(request: SearchRequest):
|
||||||
"""Search only opinion facts (agent's formed opinions and perspectives)."""
|
"""Search only opinion facts (agent's formed opinions and perspectives)."""
|
||||||
try:
|
try:
|
||||||
# Run search with fact_type filter for 'opinion'
|
# Run search with fact_type filter for 'opinion'
|
||||||
results, trace = await memory.search_async(
|
results, trace = await app.state.memory.search_async(
|
||||||
agent_id=request.agent_id,
|
agent_id=request.agent_id,
|
||||||
query=request.query,
|
query=request.query,
|
||||||
thinking_budget=request.thinking_budget,
|
thinking_budget=request.thinking_budget,
|
||||||
|
|
@ -440,7 +464,7 @@ This endpoint:
|
||||||
async def api_think(request: ThinkRequest):
|
async def api_think(request: ThinkRequest):
|
||||||
try:
|
try:
|
||||||
# Use the memory system's think_async method
|
# Use the memory system's think_async method
|
||||||
result = await memory.think_async(
|
result = await app.state.memory.think_async(
|
||||||
agent_id=request.agent_id,
|
agent_id=request.agent_id,
|
||||||
query=request.query,
|
query=request.query,
|
||||||
thinking_budget=request.thinking_budget,
|
thinking_budget=request.thinking_budget,
|
||||||
|
|
@ -470,7 +494,7 @@ async def api_think(request: ThinkRequest):
|
||||||
async def api_agents():
|
async def api_agents():
|
||||||
"""Get list of available agents from database."""
|
"""Get list of available agents from database."""
|
||||||
try:
|
try:
|
||||||
agent_list = await memory.list_agents()
|
agent_list = await app.state.memory.list_agents()
|
||||||
return AgentsResponse(agents=agent_list)
|
return AgentsResponse(agents=agent_list)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
@ -523,7 +547,7 @@ async def api_batch_put(request: BatchPutRequest):
|
||||||
contents.append(content_dict)
|
contents.append(content_dict)
|
||||||
|
|
||||||
# Call put_batch_async
|
# Call put_batch_async
|
||||||
result = await memory.put_batch_async(
|
result = await app.state.memory.put_batch_async(
|
||||||
agent_id=request.agent_id,
|
agent_id=request.agent_id,
|
||||||
contents=contents,
|
contents=contents,
|
||||||
document_id=request.document_id,
|
document_id=request.document_id,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue