add docs and some fixes
This commit is contained in:
parent
2bedc0003e
commit
4f8e4b83ed
7 changed files with 81 additions and 881 deletions
518
README.md
518
README.md
|
|
@ -1,512 +1,64 @@
|
|||
# Memora - Entity-Aware Memory System for AI Agents
|
||||
# Memora
|
||||
|
||||
A temporal-semantic-entity memory system that enables AI agents to store, retrieve, and reason over memories using graph-based spreading activation search.
|
||||
**Long-term memory for AI agents.**
|
||||
|
||||
## Architecture
|
||||
AI assistants forget everything between sessions. Memora fixes that with a memory system that handles temporal reasoning, entity connections, and personality-aware responses.
|
||||
|
||||
**See [architecture.md](architecture.md) for comprehensive technical documentation.**
|
||||
## Why Memora?
|
||||
|
||||
The system provides:
|
||||
- **Three Memory Networks**: Separate world knowledge, agent experiences, and formed opinions
|
||||
- **Multi-Strategy Retrieval**: 4-way parallel search (semantic, keyword, graph, temporal-graph)
|
||||
- **Entity Resolution**: Automatic entity disambiguation and linking
|
||||
- **Personality Framework**: Big Five traits influencing opinion formation
|
||||
- **Neural Reranking**: Optional cross-encoder for precision refinement
|
||||
- **Temporal queries** — "What did Alice do last spring?" requires more than vector search
|
||||
- **Entity connections** — Knowing "Alice works at Google" + "Google is in Mountain View" = "Alice works in Mountain View"
|
||||
- **Agent opinions** — Agents form and recall beliefs with confidence scores
|
||||
- **Personality** — Big Five traits influence how agents process and respond to information
|
||||
|
||||
### Quick Architecture Overview
|
||||
## 5-Minute Setup
|
||||
|
||||
**Three Memory Networks**:
|
||||
1. **World Network**: General knowledge ("Alice works at Google")
|
||||
2. **Agent Network**: Agent's own actions ("I recommended Yosemite to Alice")
|
||||
3. **Opinion Network**: Formed opinions with confidence scores ("Python is better for data science [0.85]")
|
||||
|
||||
**Retrieval Pipeline**:
|
||||
```
|
||||
Query → [Semantic + Keyword + Graph + Temporal] → RRF Merge → Cross-Encoder Reranking → MMR → Results
|
||||
```
|
||||
- 4-way parallel retrieval for high recall
|
||||
- Neural cross-encoder reranking for precision
|
||||
- MMR diversification to avoid redundancy
|
||||
|
||||
**Key Features**:
|
||||
- Entity resolution links memories through shared people/places/things
|
||||
- Graph spreading activation discovers indirect connections
|
||||
- Temporal queries: "What did Alice do last spring?"
|
||||
- Personality traits (Big Five model) influence opinion formation
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
2. Configure environment file:
|
||||
|
||||
Create `.env` file:
|
||||
```bash
|
||||
cat > .env << 'EOF'
|
||||
# API Service Configuration
|
||||
MEMORA_API_DATABASE_URL=postgresql://memora:memora_dev@localhost:5432/memora
|
||||
|
||||
# LLM Provider: "openai", "groq", or "ollama"
|
||||
MEMORA_API_LLM_PROVIDER=groq
|
||||
|
||||
# API Key (not needed for ollama)
|
||||
MEMORA_API_LLM_API_KEY=your_api_key_here
|
||||
|
||||
# LLM Model
|
||||
MEMORA_API_LLM_MODEL=openai/gpt-oss-120b
|
||||
|
||||
# Optional: Custom base URL (for ollama or custom endpoints)
|
||||
# MEMORA_API_LLM_BASE_URL=http://localhost:11434/v1
|
||||
|
||||
# Control Plane Configuration
|
||||
MEMORA_CP_DATAPLANE_API_URL=http://localhost:8080
|
||||
EOF
|
||||
```
|
||||
|
||||
### LLM Provider Configuration
|
||||
|
||||
The system supports multiple LLM providers with separate configuration for main operations and benchmark evaluation:
|
||||
|
||||
#### Main LLM (for memory operations)
|
||||
|
||||
**Groq** (default, fast inference):
|
||||
```bash
|
||||
LLM_PROVIDER=groq
|
||||
LLM_API_KEY=your_groq_api_key
|
||||
```
|
||||
|
||||
**OpenAI**:
|
||||
```bash
|
||||
LLM_PROVIDER=openai
|
||||
LLM_API_KEY=your_openai_api_key
|
||||
```
|
||||
|
||||
**Ollama** (local, no API key needed):
|
||||
```bash
|
||||
LLM_PROVIDER=ollama
|
||||
LLM_BASE_URL=http://localhost:11434/v1 # Default, can be customized
|
||||
```
|
||||
|
||||
#### Judge LLM (for benchmark evaluation)
|
||||
|
||||
Benchmarks can use a separate LLM for evaluation (e.g., using Groq for fast answer generation but OpenAI GPT-4 for accurate judging):
|
||||
### 1. Start the server
|
||||
|
||||
```bash
|
||||
# If not set, falls back to main LLM configuration
|
||||
JUDGE_LLM_PROVIDER=openai
|
||||
JUDGE_LLM_API_KEY=your_openai_api_key
|
||||
# JUDGE_LLM_BASE_URL=https://api.custom.com/v1 # Optional
|
||||
```
|
||||
|
||||
**Example: Fast generation, accurate judging**:
|
||||
```bash
|
||||
# Main LLM - Groq for speed
|
||||
LLM_PROVIDER=groq
|
||||
LLM_API_KEY=your_groq_key
|
||||
|
||||
# Judge LLM - OpenAI GPT-4 for accuracy
|
||||
JUDGE_LLM_PROVIDER=openai
|
||||
JUDGE_LLM_API_KEY=your_openai_key
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
# Start all services with Docker (PostgreSQL, API, Control Plane)
|
||||
cd ../docker
|
||||
# Clone and start with Docker
|
||||
git clone https://github.com/anthropics/memora.git
|
||||
cd memora/docker
|
||||
./start.sh
|
||||
|
||||
# Or start services individually:
|
||||
# 1. Start PostgreSQL only
|
||||
# (then migrations run automatically when API starts)
|
||||
# 2. Start the server with local environment
|
||||
./scripts/start-server.sh --env local
|
||||
|
||||
# Stop all Docker services
|
||||
cd ../docker
|
||||
./stop.sh
|
||||
|
||||
# Erase all data and containers
|
||||
cd ../docker
|
||||
./clean.sh
|
||||
```
|
||||
|
||||
The server will start at http://localhost:8080
|
||||
Server runs at `http://localhost:8080`
|
||||
|
||||
**API Endpoints**:
|
||||
- `GET /` - Interactive visualization UI
|
||||
- `POST /api/memories/batch` - Store memories
|
||||
- `POST /api/search` - Search all networks
|
||||
- `POST /api/world_search` - Search world facts only
|
||||
- `POST /api/agent_search` - Search agent facts only
|
||||
- `POST /api/opinion_search` - Search opinions only
|
||||
- `POST /api/think` - Think and generate contextual answers
|
||||
- `GET /api/graph` - Get graph data for visualization
|
||||
- `GET /api/agents` - List all agents
|
||||
- `PUT /api/agents/{agent_id}` - Create/update agent with personality
|
||||
- `GET /api/agents/{agent_id}/profile` - Get agent profile
|
||||
- `PUT /api/agents/{agent_id}/profile` - Update personality traits
|
||||
- `POST /api/agents/{agent_id}/background` - Merge agent background
|
||||
|
||||
## API Examples (curl)
|
||||
|
||||
### Create/Update Agent
|
||||
### 2. Install the Python client
|
||||
|
||||
```bash
|
||||
# Create or update an agent with personality and background
|
||||
curl -X PUT http://localhost:8080/api/agents/alice_agent \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"personality": {
|
||||
"openness": 0.8,
|
||||
"conscientiousness": 0.6,
|
||||
"extraversion": 0.5,
|
||||
"agreeableness": 0.7,
|
||||
"neuroticism": 0.3,
|
||||
"bias_strength": 0.7
|
||||
},
|
||||
"background": "I am a creative software engineer with 10 years of startup experience"
|
||||
}'
|
||||
|
||||
# Create agent with just background (personality defaults to 0.5 for all traits)
|
||||
curl -X PUT http://localhost:8080/api/agents/bob_agent \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"background": "I am a data scientist interested in machine learning"
|
||||
}'
|
||||
pip install memora-client
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"agent_id": "alice_agent",
|
||||
"personality": {
|
||||
"openness": 0.8,
|
||||
"conscientiousness": 0.6,
|
||||
"extraversion": 0.5,
|
||||
"agreeableness": 0.7,
|
||||
"neuroticism": 0.3,
|
||||
"bias_strength": 0.7
|
||||
},
|
||||
"background": "I am a creative software engineer with 10 years of startup experience"
|
||||
}
|
||||
```
|
||||
### 3. Use it
|
||||
|
||||
### Store Memories
|
||||
```python
|
||||
from memora_client import Memora
|
||||
|
||||
```bash
|
||||
# Store memories for an agent
|
||||
curl -X POST http://localhost:8080/api/memories/batch \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"agent_id": "alice_agent",
|
||||
"items": [
|
||||
{
|
||||
"content": "Alice works at Google as a software engineer. She joined last year and focuses on machine learning infrastructure.",
|
||||
"context": "career discussion",
|
||||
"event_date": "2024-01-15T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"content": "Alice loves hiking in Yosemite National Park. She goes every weekend and has climbed Half Dome three times.",
|
||||
"context": "hobby conversation"
|
||||
}
|
||||
],
|
||||
"document_id": "conversation_001"
|
||||
}'
|
||||
```
|
||||
client = Memora(base_url="http://localhost:8080")
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Successfully stored 2 memory items",
|
||||
"agent_id": "alice_agent",
|
||||
"document_id": "conversation_001",
|
||||
"items_count": 2
|
||||
}
|
||||
```
|
||||
|
||||
### Search Memories
|
||||
|
||||
```bash
|
||||
# Search across all networks
|
||||
curl -X POST http://localhost:8080/api/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"agent_id": "alice_agent",
|
||||
"query": "What does Alice do?",
|
||||
"thinking_budget": 100,
|
||||
"top_k": 10,
|
||||
"trace": false
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"text": "Alice works at Google as a software engineer",
|
||||
"context": "career discussion",
|
||||
"event_date": "2024-01-15T10:00:00Z",
|
||||
"weight": 0.95,
|
||||
"fact_type": "world"
|
||||
},
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"text": "Alice joined Google last year",
|
||||
"weight": 0.87,
|
||||
"fact_type": "world"
|
||||
}
|
||||
],
|
||||
"trace": null
|
||||
}
|
||||
```
|
||||
|
||||
### Temporal Queries
|
||||
|
||||
The system automatically detects temporal constraints and activates temporal graph retrieval:
|
||||
|
||||
```bash
|
||||
# Temporal query - automatically uses 4-way retrieval with temporal graph
|
||||
curl -X POST http://localhost:8080/api/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"agent_id": "alice_agent",
|
||||
"query": "What did Alice do last spring?",
|
||||
"thinking_budget": 100,
|
||||
"top_k": 10
|
||||
}'
|
||||
```
|
||||
|
||||
Supported temporal expressions:
|
||||
- **Seasons**: "last spring", "this summer", "winter 2024"
|
||||
- **Months**: "in June", "last March", "this November"
|
||||
- **Relative**: "last year", "last month", "last week"
|
||||
- **Ranges**: "between March and May"
|
||||
|
||||
### Think and Generate Answer
|
||||
|
||||
```bash
|
||||
# Think operation: combines agent identity, world knowledge, and opinions
|
||||
curl -X POST http://localhost:8080/api/think \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"agent_id": "alice_agent",
|
||||
"query": "What do you know about Alice?",
|
||||
"thinking_budget": 50,
|
||||
"top_k": 10
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"text": "Alice is a software engineer at Google who joined last year. She specializes in machine learning infrastructure. In her free time, she's an avid hiker who frequents Yosemite National Park on weekends and has climbed Half Dome three times.",
|
||||
"based_on": {
|
||||
"world": [
|
||||
{
|
||||
"text": "Alice works at Google as a software engineer",
|
||||
"weight": 0.95,
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
},
|
||||
{
|
||||
"text": "Alice loves hiking in Yosemite National Park",
|
||||
"weight": 0.89,
|
||||
"id": "550e8400-e29b-41d4-a716-446655440002"
|
||||
}
|
||||
],
|
||||
"agent": [],
|
||||
"opinion": []
|
||||
},
|
||||
"new_opinions": []
|
||||
}
|
||||
```
|
||||
|
||||
## CLI Usage
|
||||
|
||||
The Memora CLI provides command-line access to memory operations and agent management:
|
||||
|
||||
**Memory Operations**:
|
||||
```bash
|
||||
# Store a memory
|
||||
memora put <agent_id> "Alice works at Google"
|
||||
# Store memories
|
||||
client.store(agent_id="my-agent", content="Alice works at Google")
|
||||
client.store(agent_id="my-agent", content="Bob prefers Python over JavaScript")
|
||||
|
||||
# Search memories
|
||||
memora search <agent_id> "What does Alice do?" --budget 100
|
||||
results = client.search(agent_id="my-agent", query="What does Alice do?")
|
||||
for r in results:
|
||||
print(f"{r['text']} ({r['weight']:.2f})")
|
||||
|
||||
# Think (reasoning with opinions)
|
||||
memora think <agent_id> "What do you think about remote work?" -v
|
||||
# Generate personality-aware responses
|
||||
answer = client.think(agent_id="my-agent", query="Tell me about Alice")
|
||||
print(answer["text"])
|
||||
```
|
||||
|
||||
**Agent Management**:
|
||||
```bash
|
||||
# View agent profile
|
||||
memora profile <agent_id>
|
||||
## Documentation
|
||||
|
||||
# Update personality traits (all required)
|
||||
memora set-personality <agent_id> \
|
||||
--openness 0.8 \
|
||||
--conscientiousness 0.6 \
|
||||
--extraversion 0.5 \
|
||||
--agreeableness 0.7 \
|
||||
--neuroticism 0.3 \
|
||||
--bias-strength 0.7
|
||||
Full documentation: [memora-docs](./memora-docs)
|
||||
|
||||
# Add/merge background
|
||||
memora background <agent_id> "I was born in Texas"
|
||||
|
||||
# List all agents
|
||||
memora agents
|
||||
```
|
||||
|
||||
**Output Formats**:
|
||||
```bash
|
||||
# Pretty output (default)
|
||||
memora search <agent_id> "query"
|
||||
|
||||
# JSON output
|
||||
memora search <agent_id> "query" -o json
|
||||
|
||||
# YAML output
|
||||
memora search <agent_id> "query" -o yaml
|
||||
|
||||
# Verbose mode (show requests/responses)
|
||||
memora search <agent_id> "query" -v
|
||||
```
|
||||
|
||||
## OpenAI Client Wrapper (`memora-openai`)
|
||||
|
||||
The `memora-openai` package provides a drop-in replacement for the OpenAI Python client that automatically integrates with Memora. It transparently stores conversations and injects relevant memories into prompts.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
cd memora-openai
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```python
|
||||
from memora_openai import configure, OpenAI
|
||||
|
||||
# Configure Memora integration once
|
||||
configure(
|
||||
memora_api_url="http://localhost:8000",
|
||||
agent_id="my-agent",
|
||||
store_conversations=True, # Store conversations to Memora
|
||||
inject_memories=True, # Inject relevant memories into prompts
|
||||
)
|
||||
|
||||
# Use OpenAI client as normal - Memora integration happens automatically
|
||||
client = OpenAI(api_key="sk-...")
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "What did we discuss about AI?"}]
|
||||
)
|
||||
```
|
||||
|
||||
### Async Support
|
||||
|
||||
```python
|
||||
from memora_openai import configure, AsyncOpenAI
|
||||
|
||||
configure(
|
||||
memora_api_url="http://localhost:8000",
|
||||
agent_id="my-agent",
|
||||
)
|
||||
|
||||
client = AsyncOpenAI(api_key="sk-...")
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "Tell me about my preferences"}]
|
||||
)
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
- **Automatic Memory Injection**: Relevant memories are automatically retrieved and injected as system messages before each API call
|
||||
- **Conversation Storage**: All conversations are automatically stored to Memora for future retrieval
|
||||
- **Zero Code Changes**: Works as a drop-in replacement for `openai.OpenAI` and `openai.AsyncOpenAI`
|
||||
- **Configurable**: Control memory search budget, context window, and enable/disable features
|
||||
- **Transparent**: Original OpenAI API responses are returned unchanged
|
||||
|
||||
### Configuration Options
|
||||
|
||||
```python
|
||||
configure(
|
||||
memora_api_url="http://localhost:8000", # Memora API URL
|
||||
agent_id="my-agent", # Agent identifier (required)
|
||||
store_conversations=True, # Store conversations
|
||||
inject_memories=True, # Inject memories
|
||||
document_id="session-123", # Optional: Group conversations by document ID
|
||||
enabled=True, # Master switch
|
||||
)
|
||||
```
|
||||
|
||||
See [memora-openai/README.md](memora-openai/README.md) for full documentation and examples.
|
||||
|
||||
## Running Benchmarks
|
||||
|
||||
The system includes two benchmarks for evaluating memory retrieval quality:
|
||||
|
||||
### LoComo Benchmark
|
||||
|
||||
Long-term Conversational Memory benchmark - evaluates multi-turn conversation understanding:
|
||||
|
||||
```bash
|
||||
# Run full benchmark with think API (uses local env by default)
|
||||
./scripts/benchmarks/run-locomo.sh --use-think
|
||||
|
||||
# Run with dev environment
|
||||
./scripts/benchmarks/run-locomo.sh --use-think --env dev
|
||||
|
||||
# Run with limits for quick testing
|
||||
./scripts/benchmarks/run-locomo.sh --use-think --max-conversations 5 --max-questions 3
|
||||
|
||||
# Skip ingestion (use existing data)
|
||||
./scripts/benchmarks/run-locomo.sh --use-think --skip-ingestion
|
||||
```
|
||||
|
||||
### LongMemEval Benchmark
|
||||
|
||||
Long-term Memory Evaluation benchmark - tests memory retention and retrieval:
|
||||
|
||||
```bash
|
||||
# Run full benchmark (uses local env by default)
|
||||
./scripts/benchmarks/run-longmemeval.sh
|
||||
|
||||
# Run with dev environment
|
||||
./scripts/benchmarks/run-longmemeval.sh --env dev
|
||||
|
||||
# Run with arguments (pass any args directly)
|
||||
./scripts/benchmarks/run-longmemeval.sh --max-instances 10 --max-questions 5
|
||||
|
||||
# Skip ingestion
|
||||
./scripts/benchmarks/run-longmemeval.sh --skip-ingestion
|
||||
```
|
||||
|
||||
### Visualizer
|
||||
|
||||
View benchmark results in an interactive web interface:
|
||||
|
||||
```bash
|
||||
# Start the visualizer server
|
||||
./scripts/benchmarks/start-visualizer.sh
|
||||
```
|
||||
|
||||
The visualizer will be available at http://localhost:8001
|
||||
|
||||
**Benchmark Results**: Results are saved to `benchmark_results.json` in each benchmark directory with metrics including accuracy, F1 score, and per-question performance.
|
||||
- [Architecture](./memora-docs/docs/developer/architecture.md) — How ingestion, storage, and retrieval work
|
||||
- [Python Client](./memora-docs/docs/sdks/python.md) — Full API reference
|
||||
- [API Reference](./memora-docs/docs/api-reference/index.md) — REST API endpoints
|
||||
- [Personality](./memora-docs/docs/developer/personality.md) — Big Five traits and opinion formation
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -1,132 +0,0 @@
|
|||
# OpenAPI Client Generator Comparison
|
||||
|
||||
## Current: openapi-python-client
|
||||
**Pros:**
|
||||
- Python-native (no Java required)
|
||||
- Lightweight
|
||||
- Good type hints
|
||||
- Uses httpx (modern)
|
||||
|
||||
**Cons:**
|
||||
- Functional style (not OOP)
|
||||
- Verbose imports
|
||||
- Awkward API (need to pass client everywhere)
|
||||
|
||||
## Option 1: openapi-generator (Recommended)
|
||||
**Command:** `openapi-generator-cli generate -i openapi.json -g python -o memora-clients/python`
|
||||
|
||||
**Pros:**
|
||||
- ✅ **OOP style** - generates `client.search_memories()` not `search_memories.sync(client=...)`
|
||||
- ✅ Widely used (industry standard)
|
||||
- ✅ Active development
|
||||
- ✅ Generates proper SDK with clean imports
|
||||
- ✅ Built-in retry, timeout handling
|
||||
|
||||
**Cons:**
|
||||
- Requires Java Runtime (but can use Docker)
|
||||
- Larger generated code
|
||||
- Some boilerplate
|
||||
|
||||
**Example Generated Code:**
|
||||
```python
|
||||
from memora_client import ApiClient, Configuration, MemoryOperationsApi
|
||||
|
||||
config = Configuration(host="http://localhost:8000")
|
||||
client = ApiClient(config)
|
||||
api = MemoryOperationsApi(client)
|
||||
|
||||
# Clean method calls!
|
||||
results = api.search_memories(
|
||||
agent_id="alice",
|
||||
search_request=SearchRequest(query="...")
|
||||
)
|
||||
```
|
||||
|
||||
## Option 2: fern
|
||||
**Command:** `fern generate`
|
||||
|
||||
**Pros:**
|
||||
- ✅ Modern, best-in-class DX
|
||||
- ✅ Beautiful generated code
|
||||
- ✅ Excellent type hints
|
||||
- ✅ Async-first
|
||||
- ✅ Pydantic v2 models
|
||||
|
||||
**Cons:**
|
||||
- Requires `fern.config.yml` setup
|
||||
- Less mature than openapi-generator
|
||||
- Config-heavy
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
from memora import Memora
|
||||
|
||||
client = Memora(base_url="http://localhost:8000")
|
||||
results = client.search_memories(agent_id="alice", query="...")
|
||||
```
|
||||
|
||||
## Option 3: speakeasy
|
||||
**Command:** `speakeasy generate sdk`
|
||||
|
||||
**Pros:**
|
||||
- ✅ Very clean generated code
|
||||
- ✅ Great DX
|
||||
- ✅ SDK versioning built-in
|
||||
|
||||
**Cons:**
|
||||
- Commercial (free tier available)
|
||||
- Requires account
|
||||
- Less control
|
||||
|
||||
## Recommendation: openapi-generator
|
||||
|
||||
Use **openapi-generator** because it:
|
||||
1. Generates proper OOP-style APIs
|
||||
2. Industry standard with great support
|
||||
3. Can run via Docker (no Java install needed)
|
||||
4. Will give you `api.search_memories()` style calls
|
||||
|
||||
### Migration Steps:
|
||||
|
||||
1. **Install via Docker:**
|
||||
```bash
|
||||
alias openapi-generator='docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli'
|
||||
```
|
||||
|
||||
2. **Generate config:**
|
||||
```bash
|
||||
openapi-generator config-help -g python
|
||||
```
|
||||
|
||||
3. **Create config file:** `openapi-generator-config.yaml`
|
||||
```yaml
|
||||
packageName: memora_client
|
||||
projectName: memora-client
|
||||
packageVersion: 0.0.7
|
||||
library: urllib3 # or 'asyncio' for async
|
||||
```
|
||||
|
||||
4. **Generate:**
|
||||
```bash
|
||||
openapi-generator generate \
|
||||
-i openapi.json \
|
||||
-g python \
|
||||
-o memora-clients/python \
|
||||
-c openapi-generator-config.yaml
|
||||
```
|
||||
|
||||
This will generate code like:
|
||||
```python
|
||||
import memora_client
|
||||
from memora_client.api import memory_operations_api
|
||||
|
||||
config = memora_client.Configuration(host="http://localhost:8000")
|
||||
with memora_client.ApiClient(config) as api_client:
|
||||
api = memory_operations_api.MemoryOperationsApi(api_client)
|
||||
response = api.search_memories(
|
||||
agent_id="alice",
|
||||
search_request=SearchRequest(query="...")
|
||||
)
|
||||
```
|
||||
|
||||
Then we add our thin `Memora` wrapper on top for even simpler usage!
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Memora-LangMem: Drop-in Semantic Memory for LangGraph\n",
|
||||
"\n",
|
||||
"Replace your LangGraph memory store in one line and get advanced semantic capabilities."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## What is Memora-LangMem?\n",
|
||||
"\n",
|
||||
"`memora-langmem` implements LangGraph's `BaseStore` interface using Memora as the backend.\n",
|
||||
"\n",
|
||||
"### What You Get vs Standard LangGraph Memory\n",
|
||||
"\n",
|
||||
"| Feature | Standard Memory | Memora-LangMem |\n",
|
||||
"|---------|-----------------|----------------|\n",
|
||||
"| Basic Key-Value Storage | ✅ | ✅ |\n",
|
||||
"| Semantic Search | ✅ Basic | ✅ **Enhanced with spreading activation** |\n",
|
||||
"| Namespace Support | ✅ | ✅ |\n",
|
||||
"| **Personality-Driven Retrieval** | ❌ | ✅ |\n",
|
||||
"| **Automatic Fact Extraction** | ❌ | ✅ |\n",
|
||||
"| **Entity Recognition** | ❌ | ✅ |\n",
|
||||
"| **Temporal Reasoning** | ❌ | ✅ |\n",
|
||||
"| **Opinion Formation** | ❌ | ✅ |\n",
|
||||
"| **Background Knowledge** | ❌ | ✅ |\n",
|
||||
"| **Thinking/Reasoning API** | ❌ | ✅ |\n",
|
||||
"\n",
|
||||
"### When to Use\n",
|
||||
"- Conversational agents needing long-term memory\n",
|
||||
"- Personalized AI with context-aware responses \n",
|
||||
"- Multi-agent systems with distinct personalities\n",
|
||||
"- Knowledge management with semantic search"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Installation\n",
|
||||
"\n",
|
||||
"```bash\n",
|
||||
"uv pip install -e /path/to/memora-langmem\n",
|
||||
"export MEMORA_API_URL=http://localhost:8000\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## The Drop-in Replacement\n",
|
||||
"\n",
|
||||
"### Before: Standard LangGraph Memory"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langmem import create_manage_memory_tool, create_search_memory_tool\n",
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
"from langgraph.store.memory import InMemoryStore\n",
|
||||
"\n",
|
||||
"# Standard store - basic key-value with optional vector search\n",
|
||||
"store = InMemoryStore()\n",
|
||||
"\n",
|
||||
"agent = create_react_agent(\n",
|
||||
" \"anthropic:claude-3-5-sonnet-latest\",\n",
|
||||
" tools=[\n",
|
||||
" create_manage_memory_tool(namespace=(\"memories\",)),\n",
|
||||
" create_search_memory_tool(namespace=(\"memories\",)),\n",
|
||||
" ],\n",
|
||||
" store=store\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### After: With Memora-LangMem\n",
|
||||
"\n",
|
||||
"**Just change one line!**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"from langmem import create_manage_memory_tool, create_search_memory_tool\n",
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
"from memora_langmem import MemoraStore # ← Only import change!\n",
|
||||
"\n",
|
||||
"# Replace InMemoryStore with MemoraStore\n",
|
||||
"base_url = os.getenv(\"MEMORA_API_URL\", \"http://localhost:8000\")\n",
|
||||
"store = MemoraStore(base_url=base_url, default_agent_id=\"my_agent\") # ← One line change!\n",
|
||||
"\n",
|
||||
"# Everything else stays exactly the same\n",
|
||||
"agent = create_react_agent(\n",
|
||||
" \"anthropic:claude-3-5-sonnet-latest\",\n",
|
||||
" tools=[\n",
|
||||
" create_manage_memory_tool(namespace=(\"memories\",)),\n",
|
||||
" create_search_memory_tool(namespace=(\"memories\",)),\n",
|
||||
" ],\n",
|
||||
" store=store # ← Now using Memora with enhanced capabilities!\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(\"✅ Agent created with Memora-powered memory\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Example: Conversational Memory in Action"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import time\n",
|
||||
"\n",
|
||||
"# Store information\n",
|
||||
"result1 = agent.invoke({\n",
|
||||
" \"messages\": [{\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": \"\"\"Remember: I'm David, a software engineer working on AI projects. \n",
|
||||
" I love Python and machine learning. Currently building a chatbot with LangGraph.\"\"\"\n",
|
||||
" }]\n",
|
||||
"})\n",
|
||||
"print(\"Agent:\", result1[\"messages\"][-1].content)\n",
|
||||
"\n",
|
||||
"time.sleep(2)\n",
|
||||
"\n",
|
||||
"# Recall information\n",
|
||||
"result2 = agent.invoke({\n",
|
||||
" \"messages\": [{\"role\": \"user\", \"content\": \"What do you remember about me?\"}]\n",
|
||||
"})\n",
|
||||
"print(\"\\nAgent:\", result2[\"messages\"][-1].content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## What Happens Behind the Scenes\n",
|
||||
"\n",
|
||||
"When your agent stores memories with Memora, automatically:\n",
|
||||
"\n",
|
||||
"1. **Fact Extraction**: Natural language → structured facts\n",
|
||||
"2. **Entity Recognition**: Identifies people, places, concepts\n",
|
||||
"3. **Semantic Indexing**: Spreading activation for better retrieval\n",
|
||||
"4. **Temporal Awareness**: Event dates tracked for time queries\n",
|
||||
"5. **Opinion Formation**: Agent develops perspectives over time\n",
|
||||
"6. **Personality Influence**: Memory retrieval shaped by personality traits\n",
|
||||
"\n",
|
||||
"**You use the standard LangGraph API - Memora does the rest!**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Multi-Agent Support\n",
|
||||
"\n",
|
||||
"Each agent gets isolated memory and can develop unique personality:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Different agents with different personalities\n",
|
||||
"creative_store = MemoraStore(base_url=base_url, default_agent_id=\"creative_writer\")\n",
|
||||
"analyst_store = MemoraStore(base_url=base_url, default_agent_id=\"data_analyst\")\n",
|
||||
"\n",
|
||||
"creative_agent = create_react_agent(\n",
|
||||
" \"anthropic:claude-3-5-sonnet-latest\",\n",
|
||||
" tools=[\n",
|
||||
" create_manage_memory_tool(namespace=(\"creative\",)),\n",
|
||||
" create_search_memory_tool(namespace=(\"creative\",))\n",
|
||||
" ],\n",
|
||||
" store=creative_store\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"analyst_agent = create_react_agent(\n",
|
||||
" \"anthropic:claude-3-5-sonnet-latest\",\n",
|
||||
" tools=[\n",
|
||||
" create_manage_memory_tool(namespace=(\"analysis\",)),\n",
|
||||
" create_search_memory_tool(namespace=(\"analysis\",))\n",
|
||||
" ],\n",
|
||||
" store=analyst_store\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(\"✅ Two agents with isolated memories and distinct personalities\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Summary\n",
|
||||
"\n",
|
||||
"### The Change\n",
|
||||
"```python\n",
|
||||
"# Before\n",
|
||||
"store = InMemoryStore()\n",
|
||||
"\n",
|
||||
"# After \n",
|
||||
"store = MemoraStore(base_url=\"http://localhost:8000\", default_agent_id=\"my_agent\")\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"### What You Get\n",
|
||||
"- ✅ Semantic search with spreading activation\n",
|
||||
"- ✅ Automatic fact extraction from conversations\n",
|
||||
"- ✅ Entity recognition and linking\n",
|
||||
"- ✅ Temporal reasoning (time-aware queries)\n",
|
||||
"- ✅ Personality-driven memory retrieval\n",
|
||||
"- ✅ Opinion formation over time\n",
|
||||
"- ✅ Multi-agent support with isolated memories\n",
|
||||
"\n",
|
||||
"**Same LangGraph API. Smarter memory. Zero code changes (except the store line).**"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
|
|
@ -19,6 +19,22 @@ from pydantic import BaseModel, Field
|
|||
from memora import TemporalSemanticMemory
|
||||
|
||||
|
||||
class MetadataFilter(BaseModel):
|
||||
"""Filter for metadata fields. Matches records where (key=value) OR (key not set) when match_unset=True."""
|
||||
key: str = Field(description="Metadata key to filter on")
|
||||
value: Optional[str] = Field(default=None, description="Value to match. If None with match_unset=True, matches any record where key is not set.")
|
||||
match_unset: bool = Field(default=True, description="If True, also match records where this metadata key is not set")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"key": "source",
|
||||
"value": "slack",
|
||||
"match_unset": True
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SearchRequest(BaseModel):
|
||||
"""Request model for search endpoint."""
|
||||
query: str
|
||||
|
|
@ -27,6 +43,7 @@ class SearchRequest(BaseModel):
|
|||
max_tokens: int = 4096
|
||||
trace: bool = False
|
||||
question_date: Optional[str] = None # ISO format date string (e.g., "2023-05-30T23:40:00")
|
||||
metadata_filter: Optional[List[MetadataFilter]] = Field(default=None, description="Filter by metadata. Multiple filters are ANDed together.")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
|
|
@ -36,7 +53,8 @@ class SearchRequest(BaseModel):
|
|||
"thinking_budget": 100,
|
||||
"max_tokens": 4096,
|
||||
"trace": True,
|
||||
"question_date": "2023-05-30T23:40:00"
|
||||
"question_date": "2023-05-30T23:40:00",
|
||||
"metadata_filter": [{"key": "source", "value": "slack", "match_unset": True}]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +70,8 @@ class SearchResult(BaseModel):
|
|||
"type": "world",
|
||||
"context": "work info",
|
||||
"event_date": "2024-01-15T10:30:00Z",
|
||||
"document_id": "session_abc123"
|
||||
"document_id": "session_abc123",
|
||||
"metadata": {"source": "slack"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +82,7 @@ class SearchResult(BaseModel):
|
|||
context: Optional[str] = None
|
||||
event_date: Optional[str] = None # ISO format date string
|
||||
document_id: Optional[str] = None # Document this memory belongs to
|
||||
metadata: Optional[Dict[str, str]] = None # User-defined metadata
|
||||
|
||||
|
||||
class SearchResponse(BaseModel):
|
||||
|
|
@ -96,13 +116,15 @@ class MemoryItem(BaseModel):
|
|||
content: str
|
||||
event_date: Optional[datetime] = None
|
||||
context: Optional[str] = None
|
||||
metadata: Optional[Dict[str, 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"
|
||||
"context": "team meeting",
|
||||
"metadata": {"source": "slack", "channel": "engineering"}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -177,13 +199,15 @@ class ThinkRequest(BaseModel):
|
|||
query: str
|
||||
thinking_budget: int = 50
|
||||
context: Optional[str] = None
|
||||
metadata_filter: Optional[List[MetadataFilter]] = Field(default=None, description="Filter by metadata. Multiple filters are ANDed together.")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"query": "What do you think about artificial intelligence?",
|
||||
"thinking_budget": 50,
|
||||
"context": "This is for a research paper on AI ethics"
|
||||
"context": "This is for a research paper on AI ethics",
|
||||
"metadata_filter": [{"key": "source", "value": "slack", "match_unset": True}]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ class MemoryUnit(Base):
|
|||
fact_type: Mapped[str] = mapped_column(Text, nullable=False, server_default="world")
|
||||
confidence_score: Mapped[Optional[float]] = mapped_column(Float)
|
||||
access_count: Mapped[int] = mapped_column(Integer, server_default="0")
|
||||
unit_metadata: Mapped[dict] = mapped_column("metadata", JSONB, server_default=sql_text("'{}'::jsonb")) # User-defined metadata (str->str)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True), server_default=func.now()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class MemoryFact(BaseModel):
|
|||
occurred_end: Optional[str] = Field(None, description="ISO format date when the event ended occurring")
|
||||
mentioned_at: Optional[str] = Field(None, description="ISO format date when the fact was mentioned/learned")
|
||||
document_id: Optional[str] = Field(None, description="ID of the document this memory belongs to")
|
||||
metadata: Optional[Dict[str, str]] = Field(None, description="User-defined metadata")
|
||||
|
||||
# Internal metrics (used by system but may not be exposed in API)
|
||||
activation: Optional[float] = Field(None, description="Internal activation score")
|
||||
|
|
@ -39,6 +40,7 @@ class MemoryFact(BaseModel):
|
|||
"context": "work info",
|
||||
"event_date": "2024-01-15T10:30:00Z",
|
||||
"document_id": "session_abc123",
|
||||
"metadata": {"source": "slack"},
|
||||
"activation": 0.95
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -744,12 +744,13 @@ class TemporalSemanticMemory(
|
|||
content = item["content"]
|
||||
context = item.get("context", "")
|
||||
event_date = item.get("event_date") or utcnow()
|
||||
metadata = item.get("metadata") or {}
|
||||
|
||||
task = extract_facts(content, event_date, context, llm_config=self._llm_config, agent_name=agent_name, extract_opinions=extract_opinions)
|
||||
fact_extraction_tasks.append((task, event_date, context))
|
||||
fact_extraction_tasks.append((task, event_date, context, metadata))
|
||||
|
||||
# Wait for all fact extractions to complete
|
||||
all_fact_results = await asyncio.gather(*[task for task, _, _ in fact_extraction_tasks])
|
||||
all_fact_results = await asyncio.gather(*[task for task, _, _, _ in fact_extraction_tasks])
|
||||
log_buffer.append(f"[1] Extract facts (parallel): {len(fact_extraction_tasks)} contents in {time.time() - step_start:.3f}s")
|
||||
|
||||
# Flatten and track which facts belong to which content
|
||||
|
|
@ -762,10 +763,11 @@ class TemporalSemanticMemory(
|
|||
all_fact_entities = [] # NEW: Store LLM-extracted entities per fact
|
||||
all_fact_types = [] # Store fact type (world or agent)
|
||||
all_causal_relations = [] # NEW: Store causal relationships per fact
|
||||
all_metadata = [] # User-defined metadata for each fact
|
||||
content_boundaries = [] # [(start_idx, end_idx), ...]
|
||||
|
||||
current_idx = 0
|
||||
for i, ((_, event_date, context), fact_dicts) in enumerate(zip(fact_extraction_tasks, all_fact_results)):
|
||||
for i, ((_, event_date, context, metadata), fact_dicts) in enumerate(zip(fact_extraction_tasks, all_fact_results)):
|
||||
start_idx = current_idx
|
||||
|
||||
for fact_dict in fact_dicts:
|
||||
|
|
@ -813,6 +815,8 @@ class TemporalSemanticMemory(
|
|||
adjusted_rel['target_fact_index'] = start_idx + rel['target_fact_index']
|
||||
adjusted_relations.append(adjusted_rel)
|
||||
all_causal_relations.append(adjusted_relations)
|
||||
# Each fact inherits metadata from its source content item
|
||||
all_metadata.append(metadata)
|
||||
|
||||
end_idx = current_idx + len(fact_dicts)
|
||||
content_boundaries.append((start_idx, end_idx))
|
||||
|
|
@ -959,6 +963,7 @@ class TemporalSemanticMemory(
|
|||
filtered_contexts = [c for c, is_dup in zip(all_contexts, all_is_duplicate) if not is_dup]
|
||||
filtered_entities = [ents for ents, is_dup in zip(all_fact_entities, all_is_duplicate) if not is_dup]
|
||||
filtered_fact_types = [ft for ft, is_dup in zip(all_fact_types, all_is_duplicate) if not is_dup]
|
||||
filtered_metadata = [m for m, is_dup in zip(all_metadata, all_is_duplicate) if not is_dup]
|
||||
|
||||
# Build index mapping from old indices to new indices (accounting for removed duplicates)
|
||||
old_to_new_index = {}
|
||||
|
|
@ -999,10 +1004,13 @@ class TemporalSemanticMemory(
|
|||
else None
|
||||
for ft in filtered_fact_types
|
||||
]
|
||||
# Convert metadata dicts to JSON strings for asyncpg
|
||||
import json
|
||||
filtered_metadata_json = [json.dumps(m) if m else '{}' for m in filtered_metadata]
|
||||
results = await conn.fetch(
|
||||
"""
|
||||
INSERT INTO memory_units (agent_id, document_id, text, context, embedding, event_date, occurred_start, occurred_end, mentioned_at, fact_type, confidence_score, access_count)
|
||||
SELECT * FROM unnest($1::text[], $2::text[], $3::text[], $4::text[], $5::vector[], $6::timestamptz[], $7::timestamptz[], $8::timestamptz[], $9::timestamptz[], $10::text[], $11::float[], $12::integer[])
|
||||
INSERT INTO memory_units (agent_id, document_id, text, context, embedding, event_date, occurred_start, occurred_end, mentioned_at, fact_type, confidence_score, access_count, metadata)
|
||||
SELECT * FROM unnest($1::text[], $2::text[], $3::text[], $4::text[], $5::vector[], $6::timestamptz[], $7::timestamptz[], $8::timestamptz[], $9::timestamptz[], $10::text[], $11::float[], $12::integer[], $13::jsonb[])
|
||||
RETURNING id
|
||||
""",
|
||||
[agent_id] * len(filtered_sentences),
|
||||
|
|
@ -1016,7 +1024,8 @@ class TemporalSemanticMemory(
|
|||
filtered_mentioned_ats,
|
||||
filtered_fact_types,
|
||||
confidence_scores,
|
||||
[0] * len(filtered_sentences)
|
||||
[0] * len(filtered_sentences),
|
||||
filtered_metadata_json
|
||||
)
|
||||
|
||||
created_unit_ids = [str(row['id']) for row in results]
|
||||
|
|
|
|||
Loading…
Reference in a new issue