feat: Add presence heartbeat for Matrix online status

- matrix-gateway: POST /internal/matrix/presence/online endpoint
- usePresenceHeartbeat hook with activity tracking
- Auto away after 5 min inactivity
- Offline on page close/visibility change
- Integrated in MatrixChatRoom component
This commit is contained in:
Apple
2025-11-27 00:19:40 -08:00
parent 5bed515852
commit 3de3c8cb36
6371 changed files with 1317450 additions and 932 deletions

View File

@@ -0,0 +1,24 @@
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY . .
# Expose port
EXPOSE 7008
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import httpx; httpx.get('http://localhost:7008/health').raise_for_status()"
# Run
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7008"]

View File

@@ -0,0 +1,317 @@
# Memory Orchestrator Service
**Port:** 7008
**Purpose:** Unified memory API for DAARION agents
## Features
**Multi-layer memory:**
- Short-term: Recent conversations & events (PostgreSQL)
- Mid-term: Semantic memory with RAG (Vector store)
- Long-term: Knowledge base (Docs, roadmaps)
**Semantic search:**
- Vector embeddings (BGE-M3 or similar)
- Cosine similarity search
- Fallback to text search if pgvector unavailable
**Flexible storage:**
- PostgreSQL for structured data
- pgvector for embeddings (optional)
- Filesystem for knowledge base (Phase 3 stub)
## API
### POST /internal/agent-memory/query
**Request:**
```json
{
"agent_id": "agent:sofia",
"microdao_id": "microdao:7",
"channel_id": "optional-channel-uuid",
"query": "What were recent changes in this microDAO?",
"limit": 5,
"kind_filter": ["conversation", "dao-event"]
}
```
**Response:**
```json
{
"items": [
{
"id": "mem-uuid",
"kind": "conversation",
"score": 0.87,
"content": "User discussed Phase 3 implementation...",
"meta": {
"source": "channel:...",
"timestamp": "..."
},
"created_at": "2025-11-24T12:34:56Z"
}
],
"total": 3,
"query": "What were recent changes..."
}
```
### POST /internal/agent-memory/store
**Request:**
```json
{
"agent_id": "agent:sofia",
"microdao_id": "microdao:7",
"channel_id": "optional",
"kind": "conversation",
"content": {
"user_message": "How do I start Phase 3?",
"agent_reply": "First, copy PHASE3_MASTER_TASK.md..."
},
"metadata": {
"channel_name": "dev-updates",
"message_id": "msg-123"
}
}
```
**Response:**
```json
{
"ok": true,
"id": "mem-uuid-123"
}
```
### POST /internal/agent-memory/summarize
**Request:**
```json
{
"agent_id": "agent:sofia",
"microdao_id": "microdao:7",
"limit": 10
}
```
**Response (Phase 3 stub):**
```json
{
"summary": "Recent activity summary: 10 memories retrieved. [Full summary in Phase 4]",
"items_processed": 10
}
```
## Setup
### Prerequisites
**PostgreSQL with pgvector (optional):**
```sql
CREATE EXTENSION IF NOT EXISTS vector;
```
**Or without pgvector:**
- Service will work with fallback text search
### Environment Variables
```bash
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/daarion
EMBEDDING_ENDPOINT=http://localhost:8001/embed # Optional
MEMORY_ORCHESTRATOR_SECRET=dev-secret-token
```
### Local Development
```bash
cd services/memory-orchestrator
# Install
pip install -r requirements.txt
# Run
python main.py
```
### Docker
```bash
docker build -t memory-orchestrator .
docker run -p 7008:7008 \
-e DATABASE_URL="postgresql://..." \
memory-orchestrator
```
## Memory Types
### Short-term (7 days retention)
- Recent channel messages
- Event log
- Quick lookup by time
**Storage:** `agent_memories_short` table
### Mid-term (90 days retention)
- Semantic search
- Conversation context
- Task history
**Storage:** `agent_memories_vector` table (with embeddings)
### Long-term (permanent)
- Knowledge base
- Docs & roadmaps
- Structured knowledge
**Storage:** Filesystem (Phase 3 stub)
## Integration with agent-runtime
```python
import httpx
async def get_agent_context(agent_id, query):
async with httpx.AsyncClient() as client:
response = await client.post(
"http://memory-orchestrator:7008/internal/agent-memory/query",
headers={"X-Internal-Secret": "dev-secret-token"},
json={
"agent_id": agent_id,
"microdao_id": "microdao:7",
"query": query,
"limit": 5
}
)
return response.json()
async def save_conversation(agent_id, user_msg, agent_reply):
async with httpx.AsyncClient() as client:
await client.post(
"http://memory-orchestrator:7008/internal/agent-memory/store",
headers={"X-Internal-Secret": "dev-secret-token"},
json={
"agent_id": agent_id,
"microdao_id": "microdao:7",
"kind": "conversation",
"content": {
"user_message": user_msg,
"agent_reply": agent_reply
}
}
)
```
## Embedding Service
### Option 1: Local Embedding (Recommended for Phase 3)
Use sentence-transformers or similar:
```python
# Simple embedding server (Flask/FastAPI)
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('BAAI/bge-m3')
@app.post("/embed")
def embed(text: str):
embedding = model.encode(text).tolist()
return {"embedding": embedding}
```
### Option 2: OpenAI Embeddings
```python
# Update embedding_client.py to use OpenAI
import openai
embedding = openai.embeddings.create(
input=text,
model="text-embedding-ada-002"
)
```
### Option 3: No Embedding (Fallback)
Service will use simple text search (ILIKE) if embedding service unavailable.
## Database Schema
### agent_memories_short
```sql
CREATE TABLE agent_memories_short (
id UUID PRIMARY KEY,
agent_id TEXT NOT NULL,
microdao_id TEXT NOT NULL,
channel_id TEXT,
kind TEXT NOT NULL,
content JSONB NOT NULL,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
### agent_memories_vector
```sql
CREATE TABLE agent_memories_vector (
id UUID PRIMARY KEY,
agent_id TEXT NOT NULL,
microdao_id TEXT NOT NULL,
channel_id TEXT,
kind TEXT NOT NULL,
content TEXT NOT NULL,
content_json JSONB,
embedding vector(1024), -- pgvector
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
## Roadmap
### Phase 3 (Current):
- ✅ Short-term memory (PostgreSQL)
- ✅ Mid-term memory (Vector search)
- ✅ Long-term stub (KB filesystem)
- ✅ Semantic search with embeddings
- ✅ Fallback to text search
### Phase 3.5:
- 🔜 LLM-based summarization
- 🔜 Memory consolidation
- 🔜 Context pruning
- 🔜 Advanced RAG strategies
### Phase 4:
- 🔜 Knowledge base indexing
- 🔜 Multi-modal memory (images, files)
- 🔜 Memory sharing across agents
- 🔜 Forgetting mechanisms
## Troubleshooting
**pgvector not available?**
- Service will work with text search fallback
- To enable: `CREATE EXTENSION vector;` in PostgreSQL
**Embeddings not working?**
- Check embedding service: `curl http://localhost:8001/embed`
- Service will use fallback if unavailable
**Slow queries?**
- Check indexes: `EXPLAIN ANALYZE SELECT ...`
- Tune ivfflat index parameters
- Consider increasing `lists` parameter
---
**Status:** ✅ Phase 3 Ready
**Version:** 1.0.0
**Last Updated:** 2025-11-24

View File

@@ -0,0 +1,9 @@
from .short_term_pg import ShortTermBackend
from .vector_store_pg import VectorStoreBackend
from .kb_filesystem import KnowledgeBaseBackend
__all__ = ['ShortTermBackend', 'VectorStoreBackend', 'KnowledgeBaseBackend']

View File

@@ -0,0 +1,75 @@
import os
import json
from typing import Optional
from models import MemoryItem
from datetime import datetime
class KnowledgeBaseBackend:
"""
Long-term knowledge base (filesystem)
Phase 3: Stub implementation
Stores docs, roadmaps, and structured knowledge
"""
def __init__(self, kb_path: str = "/data/kb"):
self.kb_path = kb_path
async def initialize(self):
"""Create KB directory"""
if not os.path.exists(self.kb_path):
try:
os.makedirs(self.kb_path, exist_ok=True)
print(f"✅ KB directory created: {self.kb_path}")
except Exception as e:
print(f"⚠️ Failed to create KB directory: {e}")
print(" Using in-memory stub")
async def query(
self,
agent_id: str,
query_text: str,
limit: int = 5
) -> list[MemoryItem]:
"""
Query knowledge base
Phase 3: Returns stub/empty results
Phase 4: Implement proper KB indexing and search
"""
# Stub implementation for Phase 3
print(f" KB query (stub): {query_text[:50]}...")
# Return empty results for now
# In Phase 4, this would:
# 1. Index docs/roadmaps with embeddings
# 2. Perform semantic search
# 3. Return relevant knowledge chunks
return []
async def store(
self,
agent_id: str,
microdao_id: str,
kind: str,
content: dict,
metadata: Optional[dict] = None
) -> str:
"""
Store knowledge base entry
Phase 3: Stub implementation
"""
# Stub for Phase 3
entry_id = f"kb-{datetime.now().timestamp()}"
print(f" KB store (stub): {entry_id}")
# In Phase 4, would write to filesystem or DB
# with proper indexing
return entry_id

View File

@@ -0,0 +1,109 @@
import asyncpg
import json
from datetime import datetime
from uuid import uuid4
from typing import Optional
from models import MemoryItem
class ShortTermBackend:
"""
Short-term memory backend (PostgreSQL)
Stores recent conversations and events for quick retrieval
"""
def __init__(self, pool: asyncpg.Pool):
self.pool = pool
async def initialize(self):
"""Create tables if not exist"""
async with self.pool.acquire() as conn:
await conn.execute("""
CREATE TABLE IF NOT EXISTS agent_memories_short (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
agent_id TEXT NOT NULL,
microdao_id TEXT NOT NULL,
channel_id TEXT,
kind TEXT NOT NULL,
content JSONB NOT NULL,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_short_agent_time
ON agent_memories_short (agent_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_short_microdao
ON agent_memories_short (microdao_id);
""")
print("✅ Short-term memory table initialized")
async def store(
self,
agent_id: str,
microdao_id: str,
kind: str,
content: dict,
channel_id: Optional[str] = None,
metadata: Optional[dict] = None
) -> str:
"""Store a memory entry"""
memory_id = str(uuid4())
async with self.pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO agent_memories_short
(id, agent_id, microdao_id, channel_id, kind, content, metadata, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
""",
memory_id, agent_id, microdao_id, channel_id, kind,
json.dumps(content), json.dumps(metadata or {}), datetime.now()
)
return memory_id
async def query(
self,
agent_id: str,
limit: int = 10,
kind_filter: Optional[list[str]] = None
) -> list[MemoryItem]:
"""Query recent memories (simple time-based retrieval)"""
query = """
SELECT id, kind, content, metadata, created_at
FROM agent_memories_short
WHERE agent_id = $1
"""
params = [agent_id]
if kind_filter:
query += f" AND kind = ANY($2)"
params.append(kind_filter)
query += " ORDER BY created_at DESC LIMIT $" + str(len(params) + 1)
params.append(limit)
async with self.pool.acquire() as conn:
rows = await conn.fetch(query, *params)
items = []
for row in rows:
content_dict = row['content']
# Convert content dict to string for MemoryItem
content_str = json.dumps(content_dict) if isinstance(content_dict, dict) else str(content_dict)
items.append(MemoryItem(
id=str(row['id']),
kind=row['kind'],
score=1.0, # Time-based, no relevance score
content=content_str,
meta=row['metadata'] or {},
created_at=row['created_at']
))
return items

View File

@@ -0,0 +1,185 @@
import asyncpg
import json
from datetime import datetime
from uuid import uuid4
from typing import Optional
from models import MemoryItem
from embedding_client import EmbeddingClient
class VectorStoreBackend:
"""
Mid-term memory backend with vector search (PostgreSQL + pgvector)
For Phase 3: Uses simple stub if pgvector not available
"""
def __init__(self, pool: asyncpg.Pool, embedding_client: EmbeddingClient):
self.pool = pool
self.embedding_client = embedding_client
self.pgvector_available = False
async def initialize(self):
"""Create tables if not exist"""
async with self.pool.acquire() as conn:
# Try to enable pgvector extension
try:
await conn.execute("CREATE EXTENSION IF NOT EXISTS vector;")
self.pgvector_available = True
print("✅ pgvector extension enabled")
except Exception as e:
print(f"⚠️ pgvector not available: {e}")
print(" Will use fallback (simple text search)")
# Create table (with or without vector column)
if self.pgvector_available:
await conn.execute("""
CREATE TABLE IF NOT EXISTS agent_memories_vector (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
agent_id TEXT NOT NULL,
microdao_id TEXT NOT NULL,
channel_id TEXT,
kind TEXT NOT NULL,
content TEXT NOT NULL,
content_json JSONB,
embedding vector(1024),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_vector_agent
ON agent_memories_vector (agent_id);
CREATE INDEX IF NOT EXISTS idx_vector_embedding
ON agent_memories_vector USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
""")
else:
# Fallback table without vector column
await conn.execute("""
CREATE TABLE IF NOT EXISTS agent_memories_vector (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
agent_id TEXT NOT NULL,
microdao_id TEXT NOT NULL,
channel_id TEXT,
kind TEXT NOT NULL,
content TEXT NOT NULL,
content_json JSONB,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_vector_agent
ON agent_memories_vector (agent_id);
""")
print("✅ Vector memory table initialized")
async def store(
self,
agent_id: str,
microdao_id: str,
kind: str,
content: dict,
channel_id: Optional[str] = None,
metadata: Optional[dict] = None
) -> str:
"""Store a memory with embedding"""
memory_id = str(uuid4())
# Convert content to text for embedding
content_text = json.dumps(content)
# Generate embedding
embedding = await self.embedding_client.embed(content_text)
async with self.pool.acquire() as conn:
if self.pgvector_available:
await conn.execute(
"""
INSERT INTO agent_memories_vector
(id, agent_id, microdao_id, channel_id, kind, content, content_json, embedding, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::vector, $9)
""",
memory_id, agent_id, microdao_id, channel_id, kind,
content_text, json.dumps(content), embedding, json.dumps(metadata or {})
)
else:
# Fallback without embedding
await conn.execute(
"""
INSERT INTO agent_memories_vector
(id, agent_id, microdao_id, channel_id, kind, content, content_json, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
""",
memory_id, agent_id, microdao_id, channel_id, kind,
content_text, json.dumps(content), json.dumps(metadata or {})
)
return memory_id
async def query(
self,
agent_id: str,
query_text: str,
limit: int = 5,
kind_filter: Optional[list[str]] = None
) -> list[MemoryItem]:
"""Query memories by semantic similarity"""
if self.pgvector_available:
# Vector search
query_embedding = await self.embedding_client.embed(query_text)
query_sql = """
SELECT id, kind, content, metadata, created_at,
1 - (embedding <=> $2::vector) as score
FROM agent_memories_vector
WHERE agent_id = $1
"""
params = [agent_id, query_embedding]
if kind_filter:
query_sql += f" AND kind = ANY($3)"
params.append(kind_filter)
query_sql += f" ORDER BY embedding <=> $2::vector LIMIT ${len(params) + 1}"
params.append(limit)
async with self.pool.acquire() as conn:
rows = await conn.fetch(query_sql, *params)
else:
# Fallback: simple text search (ILIKE)
query_sql = """
SELECT id, kind, content, metadata, created_at, 0.5 as score
FROM agent_memories_vector
WHERE agent_id = $1 AND content ILIKE $2
"""
params = [agent_id, f"%{query_text}%"]
if kind_filter:
query_sql += f" AND kind = ANY($3)"
params.append(kind_filter)
query_sql += f" ORDER BY created_at DESC LIMIT ${len(params) + 1}"
params.append(limit)
async with self.pool.acquire() as conn:
rows = await conn.fetch(query_sql, *params)
items = []
for row in rows:
items.append(MemoryItem(
id=str(row['id']),
kind=row['kind'],
score=float(row['score']),
content=row['content'],
meta=row['metadata'] or {},
created_at=row['created_at']
))
return items

View File

@@ -0,0 +1,32 @@
backends:
short_term:
type: "postgresql"
table: "agent_memories_short"
retention_days: 7
mid_term:
type: "vector"
table: "agent_memories_vector"
embedding_dim: 1024 # BGE-M3 default
retention_days: 90
long_term:
type: "filesystem" # Phase 3 stub
path: "/data/kb"
embedding:
provider: "local" # or "openai"
model: "bge-m3"
endpoint: "http://localhost:8001/embed" # Placeholder
database:
url_env: "DATABASE_URL"
pool_size: 10
limits:
max_memories_per_agent: 10000
max_query_results: 50

View File

@@ -0,0 +1,52 @@
import httpx
from typing import Optional
class EmbeddingClient:
"""Simple embedding client for Phase 3"""
def __init__(self, endpoint: str = "http://localhost:8001/embed", provider: str = "local"):
self.endpoint = endpoint
self.provider = provider
self.client = httpx.AsyncClient(timeout=30.0)
async def embed(self, text: str) -> list[float]:
"""
Generate embedding for text
For Phase 3: Returns stub embeddings if service unavailable
"""
try:
response = await self.client.post(
self.endpoint,
json={"text": text}
)
response.raise_for_status()
data = response.json()
return data.get("embedding", [])
except (httpx.ConnectError, httpx.HTTPStatusError):
# Embedding service not available - return stub
print(f"⚠️ Embedding service not available at {self.endpoint}, using stub")
# Return zero vector as stub (1024 dimensions for BGE-M3)
return [0.0] * 1024
except Exception as e:
print(f"❌ Embedding error: {e}")
return [0.0] * 1024
async def embed_batch(self, texts: list[str]) -> list[list[float]]:
"""Generate embeddings for multiple texts"""
# For Phase 3: simple sequential processing
embeddings = []
for text in texts:
emb = await self.embed(text)
embeddings.append(emb)
return embeddings
async def close(self):
"""Close HTTP client"""
await self.client.aclose()

View File

@@ -0,0 +1,244 @@
"""
DAARION Memory Orchestrator Service
Port: 7008
Unified memory API for agent intelligence (short/mid/long-term)
"""
import os
import asyncpg
from fastapi import FastAPI, HTTPException, Header
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from models import (
MemoryQueryRequest, MemoryQueryResponse,
MemoryStoreRequest, MemoryStoreResponse,
MemorySummarizeRequest, MemorySummarizeResponse
)
from embedding_client import EmbeddingClient
from backends import ShortTermBackend, VectorStoreBackend, KnowledgeBaseBackend
# ============================================================================
# Global State
# ============================================================================
db_pool: asyncpg.Pool | None = None
embedding_client: EmbeddingClient | None = None
short_term: ShortTermBackend | None = None
mid_term: VectorStoreBackend | None = None
long_term: KnowledgeBaseBackend | None = None
# ============================================================================
# App Setup
# ============================================================================
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Startup and shutdown"""
global db_pool, embedding_client, short_term, mid_term, long_term
# Startup
print("🚀 Starting Memory Orchestrator service...")
# Database connection
database_url = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@localhost:5432/daarion")
db_pool = await asyncpg.create_pool(database_url, min_size=2, max_size=10)
print("✅ Database pool created")
# Embedding client
embedding_endpoint = os.getenv("EMBEDDING_ENDPOINT", "http://localhost:8001/embed")
embedding_client = EmbeddingClient(endpoint=embedding_endpoint)
print("✅ Embedding client initialized")
# Initialize backends
short_term = ShortTermBackend(db_pool)
await short_term.initialize()
mid_term = VectorStoreBackend(db_pool, embedding_client)
await mid_term.initialize()
long_term = KnowledgeBaseBackend()
await long_term.initialize()
print("✅ Memory Orchestrator ready")
yield
# Shutdown
print("🛑 Shutting down Memory Orchestrator...")
if db_pool:
await db_pool.close()
if embedding_client:
await embedding_client.close()
app = FastAPI(
title="DAARION Memory Orchestrator",
version="1.0.0",
description="Unified memory API for agent intelligence",
lifespan=lifespan
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ============================================================================
# API Endpoints
# ============================================================================
@app.post("/internal/agent-memory/query", response_model=MemoryQueryResponse)
async def query_memory(
request: MemoryQueryRequest,
x_internal_secret: str = Header(None, alias="X-Internal-Secret")
):
"""
Query agent memory (semantic search)
Searches across short-term, mid-term, and long-term memory
"""
# Simple auth check
expected_secret = os.getenv("MEMORY_ORCHESTRATOR_SECRET", "dev-secret-token")
if x_internal_secret != expected_secret:
raise HTTPException(401, "Invalid or missing X-Internal-Secret header")
all_items = []
try:
# Query mid-term (vector search) - primary source
mid_items = await mid_term.query(
agent_id=request.agent_id,
query_text=request.query,
limit=request.limit,
kind_filter=request.kind_filter
)
all_items.extend(mid_items)
# If not enough results, query short-term (recent context)
if len(all_items) < request.limit:
short_items = await short_term.query(
agent_id=request.agent_id,
limit=request.limit - len(all_items),
kind_filter=request.kind_filter
)
all_items.extend(short_items)
# Query long-term (knowledge base) - optional
if len(all_items) < request.limit:
kb_items = await long_term.query(
agent_id=request.agent_id,
query_text=request.query,
limit=request.limit - len(all_items)
)
all_items.extend(kb_items)
# Sort by score and limit
all_items.sort(key=lambda x: x.score, reverse=True)
all_items = all_items[:request.limit]
return MemoryQueryResponse(
items=all_items,
total=len(all_items),
query=request.query
)
except Exception as e:
raise HTTPException(500, f"Memory query failed: {str(e)}")
@app.post("/internal/agent-memory/store", response_model=MemoryStoreResponse)
async def store_memory(
request: MemoryStoreRequest,
x_internal_secret: str = Header(None, alias="X-Internal-Secret")
):
"""
Store a new memory entry
Stores in both short-term (quick access) and mid-term (vector search)
"""
expected_secret = os.getenv("MEMORY_ORCHESTRATOR_SECRET", "dev-secret-token")
if x_internal_secret != expected_secret:
raise HTTPException(401, "Invalid or missing X-Internal-Secret header")
try:
# Store in short-term (always)
memory_id = await short_term.store(
agent_id=request.agent_id,
microdao_id=request.microdao_id,
kind=request.kind,
content=request.content,
channel_id=request.channel_id,
metadata=request.metadata
)
# Store in mid-term (with embedding) for conversations
if request.kind in ["conversation", "task", "dao-event"]:
await mid_term.store(
agent_id=request.agent_id,
microdao_id=request.microdao_id,
kind=request.kind,
content=request.content,
channel_id=request.channel_id,
metadata=request.metadata
)
return MemoryStoreResponse(ok=True, id=memory_id)
except Exception as e:
raise HTTPException(500, f"Memory store failed: {str(e)}")
@app.post("/internal/agent-memory/summarize", response_model=MemorySummarizeResponse)
async def summarize_memory(
request: MemorySummarizeRequest,
x_internal_secret: str = Header(None, alias="X-Internal-Secret")
):
"""
Summarize recent memories (stub for Phase 3)
Phase 4: Will use LLM to generate summaries
"""
expected_secret = os.getenv("MEMORY_ORCHESTRATOR_SECRET", "dev-secret-token")
if x_internal_secret != expected_secret:
raise HTTPException(401, "Invalid or missing X-Internal-Secret header")
# Stub implementation for Phase 3
# Phase 4: Call LLM Proxy to generate summary
try:
items = await short_term.query(
agent_id=request.agent_id,
limit=request.limit
)
summary = f"Recent activity summary for {request.agent_id}: {len(items)} memories retrieved. [Summary generation coming in Phase 4]"
return MemorySummarizeResponse(
summary=summary,
items_processed=len(items)
)
except Exception as e:
raise HTTPException(500, f"Memory summarize failed: {str(e)}")
@app.get("/health")
async def health():
"""Health check"""
return {
"status": "ok",
"service": "memory-orchestrator",
"database": "connected" if db_pool else "disconnected",
"embedding": "ready" if embedding_client else "not ready"
}
# ============================================================================
# Run
# ============================================================================
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7008)

View File

@@ -0,0 +1,51 @@
from pydantic import BaseModel, Field
from typing import Literal, Optional, Dict, Any
from datetime import datetime
from uuid import UUID
class MemoryQueryRequest(BaseModel):
agent_id: str = Field(..., description="Agent ID, e.g., agent:sofia")
microdao_id: str = Field(..., description="MicroDAO ID")
channel_id: Optional[str] = None
query: str = Field(..., min_length=1, description="Query text for semantic search")
limit: int = Field(5, ge=1, le=50, description="Max results to return")
kind_filter: Optional[list[str]] = Field(None, description="Filter by memory kind")
class MemoryItem(BaseModel):
id: str
kind: Literal["conversation", "kb", "task", "dao-event", "channel-context"]
score: float = Field(..., ge=0.0, le=1.0, description="Relevance score")
content: str
meta: Dict[str, Any] = Field(default_factory=dict)
created_at: datetime
class MemoryQueryResponse(BaseModel):
items: list[MemoryItem]
total: int
query: str
class MemoryStoreRequest(BaseModel):
agent_id: str
microdao_id: str
channel_id: Optional[str] = None
kind: Literal["conversation", "kb", "task", "dao-event", "channel-context"]
content: Dict[str, Any] = Field(..., description="Structured content to store")
metadata: Optional[Dict[str, Any]] = Field(default_factory=dict)
class MemoryStoreResponse(BaseModel):
ok: bool
id: str
class MemorySummarizeRequest(BaseModel):
agent_id: str
microdao_id: str
channel_id: Optional[str] = None
limit: int = Field(10, ge=1, le=100, description="Number of recent items to summarize")
class MemorySummarizeResponse(BaseModel):
summary: str
items_processed: int

View File

@@ -0,0 +1,11 @@
fastapi==0.109.0
uvicorn[standard]==0.27.0
pydantic==2.5.3
asyncpg==0.29.0
httpx==0.26.0
pyyaml==6.0.1
python-multipart==0.0.6