merge mcp
This commit is contained in:
parent
99a54aec90
commit
536da41775
27 changed files with 531 additions and 1002 deletions
|
|
@ -27,6 +27,8 @@ MEMORA_API_LLM_API_KEY=your_api_key_here
|
||||||
# MEMORA_API_HOST=0.0.0.0
|
# MEMORA_API_HOST=0.0.0.0
|
||||||
# MEMORA_API_PORT=8080
|
# MEMORA_API_PORT=8080
|
||||||
|
|
||||||
|
MEMORA_API_MCP_ENABLED=true
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# CONTROL PLANE SERVICE (MEMORA_CP_*)
|
# CONTROL PLANE SERVICE (MEMORA_CP_*)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -278,6 +278,132 @@ class Memora:
|
||||||
response = _run_async(self._agent_api.create_or_update_agent(agent_id, request_obj))
|
response = _run_async(self._agent_api.create_or_update_agent(agent_id, request_obj))
|
||||||
return response.to_dict() if hasattr(response, 'to_dict') else response
|
return response.to_dict() if hasattr(response, 'to_dict') else response
|
||||||
|
|
||||||
|
# Async methods (native async, no _run_async wrapper)
|
||||||
|
|
||||||
|
async def aput_batch(
|
||||||
|
self,
|
||||||
|
agent_id: str,
|
||||||
|
items: List[Dict[str, Any]],
|
||||||
|
document_id: Optional[str] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Store multiple memories in batch (async).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agent_id: The agent ID
|
||||||
|
items: List of memory items with 'content' and optional 'event_date', 'context'
|
||||||
|
document_id: Optional document ID for grouping memories
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response with success status and item count
|
||||||
|
"""
|
||||||
|
memory_items = [
|
||||||
|
memory_item.MemoryItem(
|
||||||
|
content=item["content"],
|
||||||
|
event_date=item.get("event_date"),
|
||||||
|
context=item.get("context"),
|
||||||
|
)
|
||||||
|
for item in items
|
||||||
|
]
|
||||||
|
|
||||||
|
request_obj = batch_put_request.BatchPutRequest(
|
||||||
|
items=memory_items,
|
||||||
|
document_id=document_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await self._memory_api.batch_put_memories(agent_id, request_obj)
|
||||||
|
return response.to_dict() if hasattr(response, 'to_dict') else response
|
||||||
|
|
||||||
|
async def aput(
|
||||||
|
self,
|
||||||
|
agent_id: str,
|
||||||
|
content: str,
|
||||||
|
event_date: Optional[datetime] = None,
|
||||||
|
context: Optional[str] = None,
|
||||||
|
document_id: Optional[str] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Store a single memory (async).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agent_id: The agent ID
|
||||||
|
content: Memory content
|
||||||
|
event_date: Optional event timestamp
|
||||||
|
context: Optional context description
|
||||||
|
document_id: Optional document ID for grouping
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response with success status
|
||||||
|
"""
|
||||||
|
return await self.aput_batch(
|
||||||
|
agent_id=agent_id,
|
||||||
|
items=[{"content": content, "event_date": event_date, "context": context}],
|
||||||
|
document_id=document_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def asearch(
|
||||||
|
self,
|
||||||
|
agent_id: str,
|
||||||
|
query: str,
|
||||||
|
fact_type: Optional[List[str]] = None,
|
||||||
|
max_tokens: int = 4096,
|
||||||
|
thinking_budget: int = 100,
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Search memories using semantic similarity (async).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agent_id: The agent ID
|
||||||
|
query: Search query
|
||||||
|
fact_type: Optional list of fact types to filter (world, agent, opinion)
|
||||||
|
max_tokens: Maximum tokens in results (default: 4096)
|
||||||
|
thinking_budget: Token budget for search (default: 100)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of search results
|
||||||
|
"""
|
||||||
|
request_obj = search_request.SearchRequest(
|
||||||
|
query=query,
|
||||||
|
fact_type=fact_type,
|
||||||
|
thinking_budget=thinking_budget,
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
trace=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await self._memory_api.search_memories(agent_id, request_obj)
|
||||||
|
|
||||||
|
if hasattr(response, 'results'):
|
||||||
|
return [r.to_dict() if hasattr(r, 'to_dict') else r for r in response.results]
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def athink(
|
||||||
|
self,
|
||||||
|
agent_id: str,
|
||||||
|
query: str,
|
||||||
|
thinking_budget: int = 50,
|
||||||
|
context: Optional[str] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Generate a contextual answer based on agent identity and memories (async).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agent_id: The agent ID
|
||||||
|
query: The question or prompt
|
||||||
|
thinking_budget: Token budget for thinking (default: 50)
|
||||||
|
context: Optional additional context
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response with answer text, facts used, and new opinions
|
||||||
|
"""
|
||||||
|
request_obj = think_request.ThinkRequest(
|
||||||
|
query=query,
|
||||||
|
thinking_budget=thinking_budget,
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await self._reasoning_api.think(agent_id, request_obj)
|
||||||
|
return response.to_dict() if hasattr(response, 'to_dict') else response
|
||||||
|
|
||||||
|
|
||||||
# Alias for backward compatibility
|
# Alias for backward compatibility
|
||||||
MemoraClient = Memora
|
MemoraClient = Memora
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
# Memora MCP Server
|
|
||||||
|
|
||||||
Remote MCP server for integrating Memora memory capabilities with Claude Desktop and other MCP clients.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Required environment variables:
|
|
||||||
- `MEMORA_AGENT_ID`: The agent ID to use for all operations
|
|
||||||
- `MEMORA_API_URL`: Memora API endpoint (default: http://localhost:8080)
|
|
||||||
- `MEMORA_API_KEY`: API key for authentication (optional)
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Start the HTTP/SSE Server
|
|
||||||
```bash
|
|
||||||
export MEMORA_AGENT_ID=your-agent-id
|
|
||||||
export MEMORA_API_URL=http://localhost:8080
|
|
||||||
export PORT=8765 # optional, default is 8765
|
|
||||||
export HOST=127.0.0.1 # optional, default is 127.0.0.1
|
|
||||||
uv run memora-mcp-server
|
|
||||||
```
|
|
||||||
|
|
||||||
The server will start on `http://127.0.0.1:8765`
|
|
||||||
|
|
||||||
### Claude Desktop Integration
|
|
||||||
|
|
||||||
Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"memora": {
|
|
||||||
"url": "http://127.0.0.1:8765/sse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure the Memora MCP server is running before starting Claude Desktop.
|
|
||||||
|
|
||||||
## Available Tools
|
|
||||||
|
|
||||||
- `memora_put`: Store facts/memories with required context
|
|
||||||
- `memora_search`: Search through memories using semantic search
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
"""Memora MCP Server - Remote MCP server for Memora memory system."""
|
|
||||||
|
|
||||||
__version__ = "0.0.1"
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
"""Memora API client wrapper."""
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class MemoraClient:
|
|
||||||
"""Client for interacting with Memora API."""
|
|
||||||
|
|
||||||
def __init__(self, api_url: str, agent_id: str, api_key: str | None = None):
|
|
||||||
self.api_url = api_url.rstrip("/")
|
|
||||||
self.agent_id = agent_id
|
|
||||||
self.headers = {}
|
|
||||||
if api_key:
|
|
||||||
self.headers["Authorization"] = f"Bearer {api_key}"
|
|
||||||
|
|
||||||
async def remember(
|
|
||||||
self, content: str, context: str
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Store a memory using batch endpoint."""
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
payload = {
|
|
||||||
"agent_id": self.agent_id,
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"content": content,
|
|
||||||
"context": context,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
response = await client.post(
|
|
||||||
f"{self.api_url}/api/memories/batch",
|
|
||||||
json=payload,
|
|
||||||
headers=self.headers,
|
|
||||||
timeout=30.0,
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
async def search(
|
|
||||||
self, query: str, max_tokens: int = 4096
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Search memories using search endpoint."""
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
payload = {
|
|
||||||
"agent_id": self.agent_id,
|
|
||||||
"query": query,
|
|
||||||
"thinking_budget": 100,
|
|
||||||
"max_tokens": max_tokens,
|
|
||||||
"reranker": "heuristic",
|
|
||||||
"trace": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
response = await client.post(
|
|
||||||
f"{self.api_url}/api/search",
|
|
||||||
json=payload,
|
|
||||||
headers=self.headers,
|
|
||||||
timeout=30.0,
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
"""Configuration management for Memora MCP Server."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Config:
|
|
||||||
"""MCP Server configuration."""
|
|
||||||
|
|
||||||
agent_id: str
|
|
||||||
api_url: str = "http://localhost:8080"
|
|
||||||
api_key: str | None = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_env(cls) -> "Config":
|
|
||||||
"""Load configuration from environment variables."""
|
|
||||||
agent_id = os.getenv("MEMORA_AGENT_ID")
|
|
||||||
if not agent_id:
|
|
||||||
raise ValueError("MEMORA_AGENT_ID environment variable is required")
|
|
||||||
|
|
||||||
return cls(
|
|
||||||
agent_id=agent_id,
|
|
||||||
api_url=os.getenv("MEMORA_API_URL", "http://localhost:8080"),
|
|
||||||
api_key=os.getenv("MEMORA_API_KEY"),
|
|
||||||
)
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
[project]
|
|
||||||
name = "memora-mcp-server"
|
|
||||||
version = "0.0.1"
|
|
||||||
description = "Remote MCP server for Memora memory system"
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.11"
|
|
||||||
dependencies = [
|
|
||||||
"fastmcp>=0.7.0",
|
|
||||||
"httpx>=0.28.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[project.scripts]
|
|
||||||
memora-mcp-server = "server:main"
|
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
|
||||||
packages = ["."]
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["hatchling"]
|
|
||||||
build-backend = "hatchling.build"
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
"""Memora MCP Server implementation using FastMCP."""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
from fastmcp import FastMCP
|
|
||||||
|
|
||||||
from config import Config
|
|
||||||
from client import MemoraClient
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Load config
|
|
||||||
config = Config.from_env()
|
|
||||||
client = MemoraClient(
|
|
||||||
api_url=config.api_url,
|
|
||||||
agent_id=config.agent_id,
|
|
||||||
api_key=config.api_key,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create FastMCP server
|
|
||||||
mcp = FastMCP("memora-mcp-server")
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def memora_put(content: str, context: str) -> str:
|
|
||||||
"""
|
|
||||||
**CRITICAL: Store important user information to long-term memory.**
|
|
||||||
|
|
||||||
Use this tool PROACTIVELY whenever the user shares:
|
|
||||||
- Personal facts, preferences, or interests (e.g., "I love hiking", "I'm a vegetarian")
|
|
||||||
- Important events or milestones (e.g., "I got promoted", "My birthday is June 15")
|
|
||||||
- User history, experiences, or background (e.g., "I used to work at Google", "I studied CS at MIT")
|
|
||||||
- Decisions, opinions, or stated preferences (e.g., "I prefer Python over JavaScript")
|
|
||||||
- Goals, plans, or future intentions (e.g., "I'm planning to visit Japan next year")
|
|
||||||
- Relationships or people mentioned (e.g., "My manager Sarah", "My wife Alice")
|
|
||||||
- Work context, projects, or responsibilities
|
|
||||||
- Any other information the user would want remembered for future conversations
|
|
||||||
|
|
||||||
**When to use**: Immediately after user shares personal information. Don't ask permission - just store it naturally.
|
|
||||||
|
|
||||||
**Context guidelines**: Use descriptive contexts like "personal_preferences", "work_history", "family", "hobbies",
|
|
||||||
"career_goals", "project_details", etc. This helps organize and retrieve related memories later.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
content: The fact/memory to store (be specific and include relevant details)
|
|
||||||
context: Categorize the memory (e.g., 'personal_preferences', 'work_history', 'hobbies', 'family')
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
result = await client.remember(content=content, context=context)
|
|
||||||
return f"Fact stored successfully: {result.get('message', 'Success')}"
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error storing fact: {e}", exc_info=True)
|
|
||||||
return f"Error: {str(e)}"
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def memora_search(query: str, max_tokens: int = 4096) -> str:
|
|
||||||
"""
|
|
||||||
**CRITICAL: Search user's memory to provide personalized, context-aware responses.**
|
|
||||||
|
|
||||||
Use this tool PROACTIVELY at the start of conversations or when making recommendations to:
|
|
||||||
- Check user's preferences before making suggestions (e.g., "what foods does the user like?")
|
|
||||||
- Recall user's history to provide continuity (e.g., "what projects has the user worked on?")
|
|
||||||
- Remember user's goals and context (e.g., "what is the user trying to accomplish?")
|
|
||||||
- Avoid repeating information or asking questions you should already know
|
|
||||||
- Personalize responses based on user's background, interests, and past interactions
|
|
||||||
- Reference past conversations or events the user mentioned
|
|
||||||
|
|
||||||
**When to use**:
|
|
||||||
- Start of conversation: Search for relevant context about the user
|
|
||||||
- Before recommendations: Check user preferences and past experiences
|
|
||||||
- When user asks about something they may have mentioned before
|
|
||||||
- To provide continuity across conversations
|
|
||||||
|
|
||||||
**Search tips**: Use natural language queries like "user's programming language preferences",
|
|
||||||
"user's work experience", "user's dietary restrictions", "what does the user know about X?"
|
|
||||||
|
|
||||||
Args:
|
|
||||||
query: Natural language search query to find relevant memories
|
|
||||||
max_tokens: Maximum tokens for search context (default: 4096)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
result = await client.search(query=query, max_tokens=max_tokens)
|
|
||||||
return json.dumps(result, indent=2)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error searching: {e}", exc_info=True)
|
|
||||||
return f"Error: {str(e)}"
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main entry point."""
|
|
||||||
port = int(os.getenv("PORT", "8765"))
|
|
||||||
host = os.getenv("HOST", "127.0.0.1")
|
|
||||||
|
|
||||||
logger.info(f"Starting Memora MCP Server for agent: {config.agent_id}")
|
|
||||||
logger.info(f"MCP server starting on http://{host}:{port}")
|
|
||||||
|
|
||||||
mcp.run(transport="sse", host=host, port=port)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
101
memora/memora/api/__init__.py
Normal file
101
memora/memora/api/__init__.py
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
"""
|
||||||
|
Unified API module for Memora.
|
||||||
|
|
||||||
|
Provides both HTTP REST API and MCP (Model Context Protocol) server.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from memora import TemporalSemanticMemory
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(
|
||||||
|
memory: TemporalSemanticMemory,
|
||||||
|
http_api_enabled: bool = True,
|
||||||
|
mcp_api_enabled: bool = False,
|
||||||
|
mcp_mount_path: str = "/mcp",
|
||||||
|
run_migrations: bool = True,
|
||||||
|
initialize_memory: bool = True
|
||||||
|
) -> FastAPI:
|
||||||
|
"""
|
||||||
|
Create and configure the unified Memora API application.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
memory: TemporalSemanticMemory instance (already initialized with required parameters)
|
||||||
|
http_api_enabled: Whether to enable HTTP REST API endpoints (default: True)
|
||||||
|
mcp_api_enabled: Whether to enable MCP server (default: False)
|
||||||
|
mcp_mount_path: Path to mount MCP server (default: /mcp)
|
||||||
|
run_migrations: Whether to run database migrations on startup (default: True)
|
||||||
|
initialize_memory: Whether to initialize memory system on startup (default: True)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configured FastAPI application with enabled APIs
|
||||||
|
|
||||||
|
Example:
|
||||||
|
# HTTP only
|
||||||
|
app = create_app(memory)
|
||||||
|
|
||||||
|
# MCP only
|
||||||
|
app = create_app(memory, http_api_enabled=False, mcp_api_enabled=True)
|
||||||
|
|
||||||
|
# Both HTTP and MCP
|
||||||
|
app = create_app(memory, mcp_api_enabled=True)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Import and create HTTP API if enabled
|
||||||
|
if http_api_enabled:
|
||||||
|
from .http import create_app as create_http_app
|
||||||
|
app = create_http_app(
|
||||||
|
memory=memory,
|
||||||
|
run_migrations=run_migrations,
|
||||||
|
initialize_memory=initialize_memory
|
||||||
|
)
|
||||||
|
logger.info("HTTP REST API enabled")
|
||||||
|
else:
|
||||||
|
# Create minimal FastAPI app
|
||||||
|
app = FastAPI(title="Memora API", version="0.0.7")
|
||||||
|
logger.info("HTTP REST API disabled")
|
||||||
|
|
||||||
|
# Mount MCP server if enabled
|
||||||
|
if mcp_api_enabled:
|
||||||
|
try:
|
||||||
|
from .mcp import create_mcp_server
|
||||||
|
|
||||||
|
# Create MCP server with shared memory instance
|
||||||
|
mcp_server = create_mcp_server(memory=memory)
|
||||||
|
|
||||||
|
# Mount at specified path
|
||||||
|
app.mount(mcp_mount_path, mcp_server.sse_app())
|
||||||
|
logger.info(f"MCP server enabled at {mcp_mount_path}/sse")
|
||||||
|
except ImportError as e:
|
||||||
|
logger.error(f"MCP server requested but dependencies not available: {e}")
|
||||||
|
logger.error("Install with: pip install memora[mcp]")
|
||||||
|
raise
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
# Re-export commonly used items for backwards compatibility
|
||||||
|
from .http import (
|
||||||
|
SearchRequest,
|
||||||
|
SearchResult,
|
||||||
|
SearchResponse,
|
||||||
|
MemoryItem,
|
||||||
|
BatchPutRequest,
|
||||||
|
ThinkRequest,
|
||||||
|
ThinkResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"create_app",
|
||||||
|
"SearchRequest",
|
||||||
|
"SearchResult",
|
||||||
|
"SearchResponse",
|
||||||
|
"MemoryItem",
|
||||||
|
"BatchPutRequest",
|
||||||
|
"ThinkRequest",
|
||||||
|
"ThinkResponse",
|
||||||
|
]
|
||||||
130
memora/memora/api/mcp.py
Normal file
130
memora/memora/api/mcp.py
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
"""Memora MCP Server implementation using FastMCP."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from fastmcp import FastMCP
|
||||||
|
from memora import TemporalSemanticMemory
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_mcp_server(memory: TemporalSemanticMemory) -> FastMCP:
|
||||||
|
"""
|
||||||
|
Create and configure the Memora MCP server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
memory: TemporalSemanticMemory instance (required)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configured FastMCP server instance
|
||||||
|
"""
|
||||||
|
# Create FastMCP server
|
||||||
|
mcp = FastMCP("memora-mcp-server")
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def memora_put(agent_id: str, content: str, context: str, explanation: str = "") -> str:
|
||||||
|
"""
|
||||||
|
**CRITICAL: Store important user information to long-term memory.**
|
||||||
|
|
||||||
|
Use this tool PROACTIVELY whenever the user shares:
|
||||||
|
- Personal facts, preferences, or interests (e.g., "I love hiking", "I'm a vegetarian")
|
||||||
|
- Important events or milestones (e.g., "I got promoted", "My birthday is June 15")
|
||||||
|
- User history, experiences, or background (e.g., "I used to work at Google", "I studied CS at MIT")
|
||||||
|
- Decisions, opinions, or stated preferences (e.g., "I prefer Python over JavaScript")
|
||||||
|
- Goals, plans, or future intentions (e.g., "I'm planning to visit Japan next year")
|
||||||
|
- Relationships or people mentioned (e.g., "My manager Sarah", "My wife Alice")
|
||||||
|
- Work context, projects, or responsibilities
|
||||||
|
- Any other information the user would want remembered for future conversations
|
||||||
|
|
||||||
|
**When to use**: Immediately after user shares personal information. Don't ask permission - just store it naturally.
|
||||||
|
|
||||||
|
**Context guidelines**: Use descriptive contexts like "personal_preferences", "work_history", "family", "hobbies",
|
||||||
|
"career_goals", "project_details", etc. This helps organize and retrieve related memories later.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agent_id: The unique identifier for the agent/user storing the memory
|
||||||
|
content: The fact/memory to store (be specific and include relevant details)
|
||||||
|
context: Categorize the memory (e.g., 'personal_preferences', 'work_history', 'hobbies', 'family')
|
||||||
|
explanation: Optional explanation for why this memory is being stored
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Log explanation if provided
|
||||||
|
if explanation:
|
||||||
|
logger.debug(f"Explanation: {explanation}")
|
||||||
|
|
||||||
|
# Store memory using put_batch_async
|
||||||
|
await memory.put_batch_async(
|
||||||
|
agent_id=agent_id,
|
||||||
|
contents=[{"content": content, "context": context}]
|
||||||
|
)
|
||||||
|
return f"Fact stored successfully"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error storing fact: {e}", exc_info=True)
|
||||||
|
return f"Error: {str(e)}"
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def memora_search(agent_id: str, query: str, max_tokens: int = 4096, explanation: str = "") -> str:
|
||||||
|
"""
|
||||||
|
**CRITICAL: Search user's memory to provide personalized, context-aware responses.**
|
||||||
|
|
||||||
|
Use this tool PROACTIVELY at the start of conversations or when making recommendations to:
|
||||||
|
- Check user's preferences before making suggestions (e.g., "what foods does the user like?")
|
||||||
|
- Recall user's history to provide continuity (e.g., "what projects has the user worked on?")
|
||||||
|
- Remember user's goals and context (e.g., "what is the user trying to accomplish?")
|
||||||
|
- Avoid repeating information or asking questions you should already know
|
||||||
|
- Personalize responses based on user's background, interests, and past interactions
|
||||||
|
- Reference past conversations or events the user mentioned
|
||||||
|
|
||||||
|
**When to use**:
|
||||||
|
- Start of conversation: Search for relevant context about the user
|
||||||
|
- Before recommendations: Check user preferences and past experiences
|
||||||
|
- When user asks about something they may have mentioned before
|
||||||
|
- To provide continuity across conversations
|
||||||
|
|
||||||
|
**Search tips**: Use natural language queries like "user's programming language preferences",
|
||||||
|
"user's work experience", "user's dietary restrictions", "what does the user know about X?"
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agent_id: The unique identifier for the agent/user whose memories to search
|
||||||
|
query: Natural language search query to find relevant memories
|
||||||
|
max_tokens: Maximum tokens for search context (default: 4096)
|
||||||
|
explanation: Optional explanation for why this search is being performed
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Log all parameters for debugging
|
||||||
|
logger.info(f"memora_search called with: query={query!r}, max_tokens={max_tokens}, explanation={explanation!r}")
|
||||||
|
|
||||||
|
# Log explanation if provided
|
||||||
|
if explanation:
|
||||||
|
logger.debug(f"Explanation: {explanation}")
|
||||||
|
|
||||||
|
# Search using search_async
|
||||||
|
search_result = await memory.search_async(
|
||||||
|
agent_id=agent_id,
|
||||||
|
query=query,
|
||||||
|
fact_type=["world", "agent", "opinion"], # Search all fact types
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
thinking_budget=100
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert results to dict format
|
||||||
|
results = [
|
||||||
|
{
|
||||||
|
"id": fact.id,
|
||||||
|
"text": fact.text,
|
||||||
|
"type": fact.fact_type,
|
||||||
|
"context": fact.context,
|
||||||
|
"event_date": fact.event_date, # Already a string from the database
|
||||||
|
"document_id": fact.document_id
|
||||||
|
}
|
||||||
|
for fact in search_result.results
|
||||||
|
]
|
||||||
|
|
||||||
|
return json.dumps({"results": results}, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error searching: {e}", exc_info=True)
|
||||||
|
return json.dumps({"error": str(e), "results": []})
|
||||||
|
|
||||||
|
return mcp
|
||||||
|
|
@ -22,7 +22,17 @@ _memory = TemporalSemanticMemory(
|
||||||
memory_llm_model=os.getenv("MEMORA_API_LLM_MODEL", "openai/gpt-oss-120b"),
|
memory_llm_model=os.getenv("MEMORA_API_LLM_MODEL", "openai/gpt-oss-120b"),
|
||||||
memory_llm_base_url=os.getenv("MEMORA_API_LLM_BASE_URL") or None,
|
memory_llm_base_url=os.getenv("MEMORA_API_LLM_BASE_URL") or None,
|
||||||
)
|
)
|
||||||
app = create_app(_memory)
|
|
||||||
|
# Check if MCP should be enabled
|
||||||
|
mcp_enabled = os.getenv("MEMORA_API_MCP_ENABLED", "true").lower() == "true"
|
||||||
|
|
||||||
|
# Create unified app with both HTTP and optionally MCP
|
||||||
|
app = create_app(
|
||||||
|
memory=_memory,
|
||||||
|
http_api_enabled=True,
|
||||||
|
mcp_api_enabled=mcp_enabled,
|
||||||
|
mcp_mount_path="/mcp"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ test = [
|
||||||
"pytest-asyncio>=0.21.0",
|
"pytest-asyncio>=0.21.0",
|
||||||
"pytest-timeout>=2.4.0",
|
"pytest-timeout>=2.4.0",
|
||||||
]
|
]
|
||||||
|
mcp = [
|
||||||
|
"fastmcp>=2.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["memora"]
|
packages = ["memora"]
|
||||||
|
|
|
||||||
153
memora/tests/mcp_server/test_server.py
Normal file
153
memora/tests/mcp_server/test_server.py
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
"""Test MCP server with real server and client."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
from mcp import ClientSession
|
||||||
|
from mcp.client.sse import sse_client
|
||||||
|
|
||||||
|
|
||||||
|
# Note: MCP server tests now require the full web server to be running
|
||||||
|
# with MEMORA_API_MCP_ENABLED=true since there's no standalone MCP server anymore.
|
||||||
|
# These tests are kept for documentation but may need manual server setup.
|
||||||
|
|
||||||
|
pytest.skip("MCP server is now integrated with web server. Run web server with MEMORA_API_MCP_ENABLED=true to test.", allow_module_level=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mcp_server_tools_via_sse(mcp_server):
|
||||||
|
"""Test MCP server tools via SSE transport using proper MCP client."""
|
||||||
|
sse_url = mcp_server
|
||||||
|
|
||||||
|
async with sse_client(sse_url) as (read, write):
|
||||||
|
async with ClientSession(read, write) as session:
|
||||||
|
await session.initialize()
|
||||||
|
|
||||||
|
# Test 1: List tools
|
||||||
|
tools_list = await session.list_tools()
|
||||||
|
print(f"Tools: {tools_list}")
|
||||||
|
tool_names = [t.name for t in tools_list.tools]
|
||||||
|
assert "memora_search" in tool_names
|
||||||
|
assert "memora_put" in tool_names
|
||||||
|
|
||||||
|
# Test 2: Call memora_put
|
||||||
|
put_result = await session.call_tool(
|
||||||
|
"memora_put",
|
||||||
|
arguments={
|
||||||
|
"content": "User loves Python programming",
|
||||||
|
"context": "programming_preferences",
|
||||||
|
"explanation": "Storing user's programming language preference"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
print(f"Put result: {put_result}")
|
||||||
|
assert put_result is not None
|
||||||
|
|
||||||
|
# Wait a bit for indexing
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
# Test 3: Call memora_search
|
||||||
|
search_result = await session.call_tool(
|
||||||
|
"memora_search",
|
||||||
|
arguments={
|
||||||
|
"query": "What programming languages does the user like?",
|
||||||
|
"max_tokens": 4096,
|
||||||
|
"explanation": "Searching for programming preferences"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
print(f"Search result: {search_result}")
|
||||||
|
assert search_result is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_multiple_concurrent_requests(mcp_server):
|
||||||
|
"""Test multiple concurrent requests from a single session."""
|
||||||
|
sse_url = mcp_server
|
||||||
|
|
||||||
|
async with sse_client(sse_url) as (read, write):
|
||||||
|
async with ClientSession(read, write) as session:
|
||||||
|
await session.initialize()
|
||||||
|
|
||||||
|
# Fire off 10 concurrent search requests from same session
|
||||||
|
async def make_search(idx):
|
||||||
|
try:
|
||||||
|
result = await session.call_tool(
|
||||||
|
"memora_search",
|
||||||
|
arguments={
|
||||||
|
"query": f"test query {idx}",
|
||||||
|
"explanation": f"Concurrent test {idx}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return idx, "success", result
|
||||||
|
except Exception as e:
|
||||||
|
return idx, "error", str(e)
|
||||||
|
|
||||||
|
tasks = [make_search(i) for i in range(10)]
|
||||||
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
# Check results
|
||||||
|
successes = 0
|
||||||
|
failures = 0
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
if isinstance(result, Exception):
|
||||||
|
print(f"Request failed with exception: {result}")
|
||||||
|
failures += 1
|
||||||
|
else:
|
||||||
|
idx, status, data = result
|
||||||
|
if status == "success":
|
||||||
|
successes += 1
|
||||||
|
else:
|
||||||
|
print(f"Request {idx} failed: {data}")
|
||||||
|
failures += 1
|
||||||
|
|
||||||
|
print(f"Successes: {successes}, Failures: {failures}")
|
||||||
|
|
||||||
|
# We expect all requests to succeed
|
||||||
|
assert successes >= 8, f"Too many failures: {failures}/10"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_race_condition_with_rapid_requests(mcp_server):
|
||||||
|
"""Test rapid-fire requests with multiple sessions to trigger race condition."""
|
||||||
|
sse_url = mcp_server
|
||||||
|
|
||||||
|
async def rapid_session_search(idx):
|
||||||
|
"""Create a new session and immediately make a request."""
|
||||||
|
try:
|
||||||
|
async with sse_client(sse_url) as (read, write):
|
||||||
|
async with ClientSession(read, write) as session:
|
||||||
|
await session.initialize()
|
||||||
|
|
||||||
|
# Make request immediately after initialization
|
||||||
|
result = await session.call_tool(
|
||||||
|
"memora_search",
|
||||||
|
arguments={
|
||||||
|
"query": f"rapid query {idx}",
|
||||||
|
"max_tokens": 2048
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return idx, "success", result
|
||||||
|
except Exception as e:
|
||||||
|
return idx, "error", str(e)
|
||||||
|
|
||||||
|
# Fire 20 requests with minimal delay, each with its own session
|
||||||
|
tasks = [rapid_session_search(i) for i in range(20)]
|
||||||
|
results = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
# Analyze results
|
||||||
|
errors = []
|
||||||
|
for idx, status, data in results:
|
||||||
|
if status == "error":
|
||||||
|
errors.append((idx, data))
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print(f"Found {len(errors)} errors:")
|
||||||
|
for idx, error_msg in errors:
|
||||||
|
print(f" Request {idx}: {error_msg}")
|
||||||
|
|
||||||
|
# Most requests should succeed
|
||||||
|
assert len(errors) < 5, f"Too many errors: {len(errors)}/20"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-v", "-s"])
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
# Build artifacts
|
|
||||||
**/*.pyc
|
|
||||||
**/__pycache__/
|
|
||||||
**/.pytest_cache/
|
|
||||||
**/.venv/
|
|
||||||
**/venv/
|
|
||||||
**/*.egg-info/
|
|
||||||
**/dist/
|
|
||||||
**/build/
|
|
||||||
|
|
||||||
# Node
|
|
||||||
**/node_modules/
|
|
||||||
**/.next/
|
|
||||||
**/npm-debug.log
|
|
||||||
**/.turbo/
|
|
||||||
|
|
||||||
# Environment files
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!standalone/.env.standalone
|
|
||||||
|
|
||||||
# Git
|
|
||||||
.git/
|
|
||||||
.gitignore
|
|
||||||
.gitattributes
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# Test and dev files
|
|
||||||
**/tests/
|
|
||||||
local-db/
|
|
||||||
logs/
|
|
||||||
|
|
||||||
# Documentation (except standalone README)
|
|
||||||
README.md
|
|
||||||
!standalone/README.md
|
|
||||||
|
|
||||||
# Standalone files
|
|
||||||
standalone/build-docker.sh
|
|
||||||
standalone/.dockerignore
|
|
||||||
standalone/.env.example
|
|
||||||
standalone/docker-compose.yml
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
# Environment variables for docker-compose
|
|
||||||
# Copy this file to .env and customize as needed
|
|
||||||
|
|
||||||
# Optional: OpenAI API key
|
|
||||||
# OPENAI_API_KEY=your-api-key-here
|
|
||||||
|
|
||||||
# Optional: Custom embedding model
|
|
||||||
# EMBEDDING_MODEL_NAME=sentence-transformers/all-MiniLM-L6-v2
|
|
||||||
# EMBEDDING_DIM=384
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
# Standalone environment configuration
|
|
||||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/memora
|
|
||||||
DATAPLANE_API_URL=http://localhost:8080
|
|
||||||
|
|
||||||
# Embedding configuration
|
|
||||||
EMBEDDING_MODEL_NAME=sentence-transformers/all-MiniLM-L6-v2
|
|
||||||
EMBEDDING_DIM=384
|
|
||||||
|
|
||||||
# LLM Provider (set to "none" to disable LLM features)
|
|
||||||
LLM_PROVIDER=none
|
|
||||||
|
|
||||||
# Optional: LLM API Keys
|
|
||||||
# OPENAI_API_KEY=your-openai-key-here
|
|
||||||
# ANTHROPIC_API_KEY=your-anthropic-key-here
|
|
||||||
# GROQ_API_KEY=your-groq-key-here
|
|
||||||
5
standalone/.gitignore
vendored
5
standalone/.gitignore
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
# Environment files
|
|
||||||
.env
|
|
||||||
|
|
||||||
# Docker volumes
|
|
||||||
*.log
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
FROM node:20-alpine AS control-plane-builder
|
|
||||||
|
|
||||||
# Build control plane
|
|
||||||
WORKDIR /app/memora-control-plane
|
|
||||||
COPY memora-control-plane/package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
COPY memora-control-plane/ ./
|
|
||||||
# Set env to skip font optimization during build
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
|
||||||
RUN npm run build || (echo "Build failed, retrying..." && npm run build)
|
|
||||||
|
|
||||||
# Python source stage - just copy files, don't build venv yet
|
|
||||||
FROM python:3.12-slim AS dataplane-source
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
COPY pyproject.toml uv.lock ./
|
|
||||||
COPY memora/ ./memora/
|
|
||||||
COPY memora-dev/ ./memora-dev/
|
|
||||||
|
|
||||||
# Final runtime image
|
|
||||||
FROM python:3.12-slim
|
|
||||||
|
|
||||||
# Install system dependencies and PostgreSQL
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
gnupg \
|
|
||||||
lsb-release \
|
|
||||||
wget \
|
|
||||||
curl \
|
|
||||||
ca-certificates \
|
|
||||||
&& mkdir -p /etc/apt/keyrings \
|
|
||||||
&& wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /etc/apt/keyrings/pgdg.gpg \
|
|
||||||
&& echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
|
||||||
&& apt-get update && apt-get install -y \
|
|
||||||
postgresql-15 \
|
|
||||||
postgresql-15-pgvector \
|
|
||||||
postgresql-contrib-15 \
|
|
||||||
nodejs \
|
|
||||||
npm \
|
|
||||||
supervisor \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install uv
|
|
||||||
RUN pip install uv
|
|
||||||
|
|
||||||
# Create app directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy dataplane source from builder
|
|
||||||
COPY --from=dataplane-source /build /app
|
|
||||||
|
|
||||||
# Build venv in the final stage to ensure compatibility
|
|
||||||
RUN cd /app && uv sync --frozen
|
|
||||||
|
|
||||||
# Copy control plane from builder
|
|
||||||
COPY --from=control-plane-builder /app/memora-control-plane/.next/standalone /app/memora-control-plane
|
|
||||||
COPY --from=control-plane-builder /app/memora-control-plane/.next/static /app/memora-control-plane/.next/static
|
|
||||||
COPY memora-control-plane/start-server.sh /app/memora-control-plane/start-server.sh
|
|
||||||
RUN chmod +x /app/memora-control-plane/start-server.sh
|
|
||||||
|
|
||||||
# Copy standalone configuration
|
|
||||||
COPY standalone/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
|
||||||
COPY standalone/init.sh /app/init.sh
|
|
||||||
COPY standalone/.env.standalone /app/.env
|
|
||||||
|
|
||||||
RUN chmod +x /app/init.sh
|
|
||||||
|
|
||||||
# PostgreSQL setup
|
|
||||||
RUN mkdir -p /var/lib/postgresql/data && \
|
|
||||||
chown -R postgres:postgres /var/lib/postgresql && \
|
|
||||||
mkdir -p /var/run/postgresql && \
|
|
||||||
chown -R postgres:postgres /var/run/postgresql
|
|
||||||
|
|
||||||
# Initialize PostgreSQL as postgres user
|
|
||||||
USER postgres
|
|
||||||
RUN /usr/lib/postgresql/15/bin/initdb -D /var/lib/postgresql/data
|
|
||||||
|
|
||||||
USER root
|
|
||||||
|
|
||||||
# Expose ports
|
|
||||||
# 5432: PostgreSQL
|
|
||||||
# 8080: Dataplane API
|
|
||||||
# 3000: Control Plane
|
|
||||||
EXPOSE 5432 8080 3000
|
|
||||||
|
|
||||||
# Start supervisor
|
|
||||||
CMD ["/app/init.sh"]
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
# Memora Standalone - Quick Start
|
|
||||||
|
|
||||||
## What is this?
|
|
||||||
|
|
||||||
A single Docker image containing everything you need to run Memora:
|
|
||||||
- ✅ PostgreSQL database
|
|
||||||
- ✅ Dataplane API (FastAPI backend)
|
|
||||||
- ✅ Control Plane (Next.js web UI)
|
|
||||||
|
|
||||||
## Fastest Start (Docker Compose)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd standalone
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Access the UI at: **http://localhost:3000**
|
|
||||||
|
|
||||||
## Manual Docker Build & Run
|
|
||||||
|
|
||||||
### Build the image:
|
|
||||||
```bash
|
|
||||||
./standalone/build-docker.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run with the helper script:
|
|
||||||
```bash
|
|
||||||
./standalone/run-docker.sh --persist
|
|
||||||
```
|
|
||||||
|
|
||||||
### Or run directly:
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name memora \
|
|
||||||
-p 3000:3000 \
|
|
||||||
-p 8080:8080 \
|
|
||||||
-p 5432:5432 \
|
|
||||||
-v memora-data:/var/lib/postgresql/data \
|
|
||||||
memora-standalone:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Access Points
|
|
||||||
|
|
||||||
| Service | URL | Purpose |
|
|
||||||
|---------|-----|---------|
|
|
||||||
| **Control Plane** | http://localhost:3000 | Web UI |
|
|
||||||
| **Dataplane API** | http://localhost:8080 | REST API |
|
|
||||||
| **PostgreSQL** | localhost:5432 | Database |
|
|
||||||
|
|
||||||
## View Logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs -f memora-standalone
|
|
||||||
```
|
|
||||||
|
|
||||||
## Stop & Remove
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Stop
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Stop and remove data
|
|
||||||
docker-compose down -v
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
Set in `docker-compose.yml` or pass with `-e`:
|
|
||||||
|
|
||||||
- `MEMORA_API_LLM_PROVIDER` - LLM provider (openai, groq, ollama, none) (default: none)
|
|
||||||
- `MEMORA_API_LLM_API_KEY` - API key for LLM provider
|
|
||||||
- `MEMORA_API_LLM_MODEL` - LLM model name (default: openai/gpt-oss-120b)
|
|
||||||
- `MEMORA_API_LLM_BASE_URL` - Optional custom LLM endpoint
|
|
||||||
- `MEMORA_CP_DATAPLANE_API_URL` - Dataplane API URL (default: http://localhost:8080)
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
**Container won't start:**
|
|
||||||
```bash
|
|
||||||
docker logs memora-standalone
|
|
||||||
```
|
|
||||||
|
|
||||||
**Database issues:**
|
|
||||||
```bash
|
|
||||||
docker exec -it memora-standalone su - postgres -c "psql memora"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Reset everything:**
|
|
||||||
```bash
|
|
||||||
docker-compose down -v
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Production Notes
|
|
||||||
|
|
||||||
This standalone image is ideal for:
|
|
||||||
- ✅ Development
|
|
||||||
- ✅ Demos
|
|
||||||
- ✅ Testing
|
|
||||||
- ✅ Small deployments
|
|
||||||
|
|
||||||
For production, consider:
|
|
||||||
- Separate containers for each service
|
|
||||||
- External PostgreSQL database
|
|
||||||
- Kubernetes/Docker Swarm orchestration
|
|
||||||
- Environment-specific configurations
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
# Memora Standalone Docker Image
|
|
||||||
|
|
||||||
This directory contains the configuration to build a standalone Docker image that includes all Memora components in a single container:
|
|
||||||
|
|
||||||
- **PostgreSQL**: Database backend
|
|
||||||
- **Dataplane**: FastAPI backend service
|
|
||||||
- **Control Plane**: Next.js web interface
|
|
||||||
|
|
||||||
## Quick Start with Docker Compose
|
|
||||||
|
|
||||||
The easiest way to run the standalone image:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd standalone
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
This will build and start all services with persistent data storage.
|
|
||||||
|
|
||||||
To stop:
|
|
||||||
```bash
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
To remove data and start fresh:
|
|
||||||
```bash
|
|
||||||
docker-compose down -v
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building Manually
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./standalone/build-docker.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
With custom options:
|
|
||||||
```bash
|
|
||||||
./standalone/build-docker.sh --name my-memora --tag v1.0.0
|
|
||||||
./standalone/build-docker.sh --registry docker.io/myuser --tag latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running Manually
|
|
||||||
|
|
||||||
Using the run script (recommended):
|
|
||||||
```bash
|
|
||||||
./standalone/run-docker.sh --persist
|
|
||||||
```
|
|
||||||
|
|
||||||
With custom ports:
|
|
||||||
```bash
|
|
||||||
./standalone/run-docker.sh --persist --port-control 3001 --port-api 8081
|
|
||||||
```
|
|
||||||
|
|
||||||
Direct docker run:
|
|
||||||
```bash
|
|
||||||
docker run -p 3000:3000 -p 8080:8080 memora-standalone:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
With persistent data:
|
|
||||||
```bash
|
|
||||||
docker run -p 3000:3000 -p 8080:8080 \
|
|
||||||
-v memora-data:/var/lib/postgresql/data \
|
|
||||||
memora-standalone:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
With custom environment variables:
|
|
||||||
```bash
|
|
||||||
docker run -p 3000:3000 -p 8080:8080 \
|
|
||||||
-e MEMORA_API_LLM_PROVIDER=groq \
|
|
||||||
-e MEMORA_API_LLM_API_KEY=your-key \
|
|
||||||
-e MEMORA_API_LLM_MODEL=openai/gpt-oss-120b \
|
|
||||||
memora-standalone:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Accessing Services
|
|
||||||
|
|
||||||
Once running, services are available at:
|
|
||||||
|
|
||||||
- **Control Plane**: http://localhost:3000
|
|
||||||
- **Dataplane API**: http://localhost:8080
|
|
||||||
- **PostgreSQL**: localhost:5432 (username: postgres, password: postgres, database: memora)
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
The container uses `supervisord` to manage three processes:
|
|
||||||
1. PostgreSQL (started first)
|
|
||||||
2. Dataplane API (started after PostgreSQL)
|
|
||||||
3. Control Plane (started after dataplane)
|
|
||||||
|
|
||||||
The `init.sh` script handles:
|
|
||||||
- PostgreSQL initialization
|
|
||||||
- Database creation
|
|
||||||
- Running migrations
|
|
||||||
- Starting all services via supervisord
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
| Variable | Default | Description |
|
|
||||||
|----------|---------|-------------|
|
|
||||||
| `MEMORA_API_DATABASE_URL` | `postgresql://postgres:postgres@localhost:5432/memora` | PostgreSQL connection string |
|
|
||||||
| `MEMORA_CP_DATAPLANE_API_URL` | `http://localhost:8080` | Dataplane API URL for control plane |
|
|
||||||
| `MEMORA_API_LLM_PROVIDER` | `none` | LLM provider (openai, groq, ollama, none) |
|
|
||||||
| `MEMORA_API_LLM_API_KEY` | - | API key for LLM provider |
|
|
||||||
| `MEMORA_API_LLM_MODEL` | `openai/gpt-oss-120b` | LLM model name |
|
|
||||||
| `MEMORA_API_LLM_BASE_URL` | - | Optional custom LLM endpoint |
|
|
||||||
|
|
||||||
## Logs
|
|
||||||
|
|
||||||
View logs from all services:
|
|
||||||
```bash
|
|
||||||
docker logs -f <container-id>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Production Considerations
|
|
||||||
|
|
||||||
This standalone image is designed for:
|
|
||||||
- Development environments
|
|
||||||
- Demos and testing
|
|
||||||
- Small deployments
|
|
||||||
|
|
||||||
For production use, consider:
|
|
||||||
- Using separate containers for each service
|
|
||||||
- External PostgreSQL database
|
|
||||||
- Load balancing for the control plane
|
|
||||||
- Persistent volume for PostgreSQL data
|
|
||||||
- Environment-specific configurations
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
|
|
||||||
# Default values
|
|
||||||
IMAGE_NAME="memora-standalone"
|
|
||||||
IMAGE_TAG="latest"
|
|
||||||
REGISTRY=""
|
|
||||||
|
|
||||||
# Parse command line arguments
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
--name)
|
|
||||||
IMAGE_NAME="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--tag)
|
|
||||||
IMAGE_TAG="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--registry)
|
|
||||||
REGISTRY="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--help)
|
|
||||||
echo "Usage: $0 [OPTIONS]"
|
|
||||||
echo ""
|
|
||||||
echo "Options:"
|
|
||||||
echo " --name NAME Docker image name (default: memora-standalone)"
|
|
||||||
echo " --tag TAG Docker image tag (default: latest)"
|
|
||||||
echo " --registry REG Docker registry URL (optional)"
|
|
||||||
echo " --help Show this help message"
|
|
||||||
echo ""
|
|
||||||
echo "Example:"
|
|
||||||
echo " $0 --name myapp --tag v1.0.0"
|
|
||||||
echo " $0 --registry docker.io/myuser --name memora-standalone --tag v1.0.0"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option: $1"
|
|
||||||
echo "Use --help for usage information"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Construct full image name
|
|
||||||
if [ -n "$REGISTRY" ]; then
|
|
||||||
FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
|
|
||||||
else
|
|
||||||
FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Building Memora Standalone Docker image: ${FULL_IMAGE_NAME}"
|
|
||||||
echo "============================================================="
|
|
||||||
echo "This image includes:"
|
|
||||||
echo " - PostgreSQL database"
|
|
||||||
echo " - Dataplane API (FastAPI)"
|
|
||||||
echo " - Control Plane (Next.js)"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Build the Docker image
|
|
||||||
docker build -f standalone/Dockerfile -t "${FULL_IMAGE_NAME}" .
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Build completed successfully!"
|
|
||||||
echo "Image: ${FULL_IMAGE_NAME}"
|
|
||||||
echo ""
|
|
||||||
echo "To run the container:"
|
|
||||||
echo " docker run -p 3000:3000 -p 8080:8080 ${FULL_IMAGE_NAME}"
|
|
||||||
echo ""
|
|
||||||
echo "Services will be available at:"
|
|
||||||
echo " - Control Plane: http://localhost:3000"
|
|
||||||
echo " - Dataplane API: http://localhost:8080"
|
|
||||||
echo " - PostgreSQL: localhost:5432"
|
|
||||||
echo ""
|
|
||||||
echo "For persistent data, mount a volume:"
|
|
||||||
echo " docker run -p 3000:3000 -p 8080:8080 \\"
|
|
||||||
echo " -v memora-data:/var/lib/postgresql/data \\"
|
|
||||||
echo " ${FULL_IMAGE_NAME}"
|
|
||||||
echo ""
|
|
||||||
if [ -n "$REGISTRY" ]; then
|
|
||||||
echo "To push to registry:"
|
|
||||||
echo " docker push ${FULL_IMAGE_NAME}"
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
memora-standalone:
|
|
||||||
build:
|
|
||||||
context: ..
|
|
||||||
dockerfile: standalone/Dockerfile
|
|
||||||
ports:
|
|
||||||
- "3000:3000" # Control Plane
|
|
||||||
- "8080:8080" # Dataplane API
|
|
||||||
- "5432:5432" # PostgreSQL
|
|
||||||
environment:
|
|
||||||
- EMBEDDING_MODEL_NAME=sentence-transformers/all-MiniLM-L6-v2
|
|
||||||
- EMBEDDING_DIM=384
|
|
||||||
# - OPENAI_API_KEY=${OPENAI_API_KEY} # Uncomment if needed
|
|
||||||
volumes:
|
|
||||||
- memora-data:/var/lib/postgresql/data
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
memora-data:
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "🚀 Starting Memora Standalone Container..."
|
|
||||||
echo "==========================================="
|
|
||||||
|
|
||||||
# Start PostgreSQL temporarily for initialization
|
|
||||||
echo "📦 Starting PostgreSQL for initialization..."
|
|
||||||
su - postgres -c "/usr/lib/postgresql/15/bin/pg_ctl -D /var/lib/postgresql/data -l /tmp/postgresql-init.log start"
|
|
||||||
|
|
||||||
# Wait for PostgreSQL to be ready
|
|
||||||
echo "⏳ Waiting for PostgreSQL to be ready..."
|
|
||||||
for i in {1..30}; do
|
|
||||||
if su - postgres -c "psql -lqt" &>/dev/null; then
|
|
||||||
echo "✅ PostgreSQL is ready"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [ $i -eq 30 ]; then
|
|
||||||
echo "❌ PostgreSQL failed to start"
|
|
||||||
cat /tmp/postgresql-init.log
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
# Create database if it doesn't exist
|
|
||||||
echo "📊 Setting up database..."
|
|
||||||
su - postgres -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname = 'memora'\" | grep -q 1 || psql -c 'CREATE DATABASE memora;'"
|
|
||||||
|
|
||||||
# Run initial migrations
|
|
||||||
# Note: The API also runs migrations automatically on startup.
|
|
||||||
# We run them here during initialization to ensure the database
|
|
||||||
# schema is ready before handing off to supervisord.
|
|
||||||
echo "🔄 Running initial database migrations..."
|
|
||||||
cd /app/memora
|
|
||||||
|
|
||||||
# Export environment variables
|
|
||||||
set -a
|
|
||||||
source /app/.env
|
|
||||||
set +a
|
|
||||||
|
|
||||||
/app/.venv/bin/python -m alembic upgrade head
|
|
||||||
|
|
||||||
# Stop PostgreSQL so supervisord can start it cleanly
|
|
||||||
echo "🔄 Stopping PostgreSQL to hand off to supervisord..."
|
|
||||||
su - postgres -c "/usr/lib/postgresql/15/bin/pg_ctl -D /var/lib/postgresql/data stop -m fast"
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
echo "✅ Initialization complete"
|
|
||||||
echo ""
|
|
||||||
echo "Starting services via supervisord..."
|
|
||||||
echo " - PostgreSQL: localhost:5432"
|
|
||||||
echo " - Dataplane API: http://localhost:8080"
|
|
||||||
echo " - Control Plane: http://localhost:3000"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Start supervisor to manage all services
|
|
||||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Default values
|
|
||||||
IMAGE_NAME="memora-standalone:latest"
|
|
||||||
CONTAINER_NAME="memora-standalone"
|
|
||||||
PERSIST_DATA=false
|
|
||||||
PORT_CONTROL=3000
|
|
||||||
PORT_API=8080
|
|
||||||
PORT_DB=5432
|
|
||||||
|
|
||||||
# Parse command line arguments
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
--image)
|
|
||||||
IMAGE_NAME="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--name)
|
|
||||||
CONTAINER_NAME="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--persist)
|
|
||||||
PERSIST_DATA=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--port-control)
|
|
||||||
PORT_CONTROL="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--port-api)
|
|
||||||
PORT_API="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--port-db)
|
|
||||||
PORT_DB="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--help)
|
|
||||||
echo "Usage: $0 [OPTIONS]"
|
|
||||||
echo ""
|
|
||||||
echo "Options:"
|
|
||||||
echo " --image NAME Docker image name (default: memora-standalone:latest)"
|
|
||||||
echo " --name NAME Container name (default: memora-standalone)"
|
|
||||||
echo " --persist Use persistent volume for data"
|
|
||||||
echo " --port-control PORT Control plane port (default: 3000)"
|
|
||||||
echo " --port-api PORT Dataplane API port (default: 8080)"
|
|
||||||
echo " --port-db PORT PostgreSQL port (default: 5432)"
|
|
||||||
echo " --help Show this help message"
|
|
||||||
echo ""
|
|
||||||
echo "Example:"
|
|
||||||
echo " $0 --persist --port-control 3001"
|
|
||||||
echo ""
|
|
||||||
echo "To stop the container:"
|
|
||||||
echo " docker stop ${CONTAINER_NAME}"
|
|
||||||
echo ""
|
|
||||||
echo "To remove the container:"
|
|
||||||
echo " docker rm ${CONTAINER_NAME}"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option: $1"
|
|
||||||
echo "Use --help for usage information"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check if container already exists
|
|
||||||
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
|
||||||
echo "⚠️ Container '${CONTAINER_NAME}' already exists"
|
|
||||||
echo ""
|
|
||||||
read -p "Do you want to remove it and create a new one? (y/N): " -n 1 -r
|
|
||||||
echo
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
echo "🗑️ Removing existing container..."
|
|
||||||
docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
|
|
||||||
else
|
|
||||||
echo "Exiting..."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "🚀 Starting Memora Standalone Container"
|
|
||||||
echo "========================================"
|
|
||||||
echo "Image: ${IMAGE_NAME}"
|
|
||||||
echo "Container: ${CONTAINER_NAME}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Build docker run command
|
|
||||||
DOCKER_CMD="docker run -d --name ${CONTAINER_NAME}"
|
|
||||||
DOCKER_CMD="${DOCKER_CMD} -p ${PORT_CONTROL}:3000"
|
|
||||||
DOCKER_CMD="${DOCKER_CMD} -p ${PORT_API}:8080"
|
|
||||||
DOCKER_CMD="${DOCKER_CMD} -p ${PORT_DB}:5432"
|
|
||||||
|
|
||||||
if [ "$PERSIST_DATA" = true ]; then
|
|
||||||
DOCKER_CMD="${DOCKER_CMD} -v memora-data:/var/lib/postgresql/data"
|
|
||||||
echo "📦 Using persistent volume: memora-data"
|
|
||||||
fi
|
|
||||||
|
|
||||||
DOCKER_CMD="${DOCKER_CMD} ${IMAGE_NAME}"
|
|
||||||
|
|
||||||
# Run the container
|
|
||||||
eval $DOCKER_CMD
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "✅ Container started successfully!"
|
|
||||||
echo ""
|
|
||||||
echo "Services are available at:"
|
|
||||||
echo " - Control Plane: http://localhost:${PORT_CONTROL}"
|
|
||||||
echo " - Dataplane API: http://localhost:${PORT_API}"
|
|
||||||
echo " - PostgreSQL: localhost:${PORT_DB}"
|
|
||||||
echo ""
|
|
||||||
echo "View logs:"
|
|
||||||
echo " docker logs -f ${CONTAINER_NAME}"
|
|
||||||
echo ""
|
|
||||||
echo "Stop container:"
|
|
||||||
echo " docker stop ${CONTAINER_NAME}"
|
|
||||||
echo ""
|
|
||||||
echo "Remove container:"
|
|
||||||
echo " docker rm -f ${CONTAINER_NAME}"
|
|
||||||
echo ""
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
[supervisord]
|
|
||||||
nodaemon=true
|
|
||||||
user=root
|
|
||||||
logfile=/var/log/supervisor/supervisord.log
|
|
||||||
pidfile=/var/run/supervisord.pid
|
|
||||||
|
|
||||||
[program:postgresql]
|
|
||||||
command=/usr/lib/postgresql/15/bin/postgres -D /var/lib/postgresql/data
|
|
||||||
user=postgres
|
|
||||||
autostart=true
|
|
||||||
autorestart=true
|
|
||||||
stdout_logfile=/dev/stdout
|
|
||||||
stdout_logfile_maxbytes=0
|
|
||||||
stderr_logfile=/dev/stderr
|
|
||||||
stderr_logfile_maxbytes=0
|
|
||||||
priority=1
|
|
||||||
|
|
||||||
[program:dataplane]
|
|
||||||
command=/app/.venv/bin/python -m memora.web.server --host 0.0.0.0 --port 8080
|
|
||||||
directory=/app/memora
|
|
||||||
environment=PATH="/app/.venv/bin:%(ENV_PATH)s",MEMORA_API_DATABASE_URL="postgresql://postgres:postgres@localhost:5432/memora",MEMORA_API_LLM_PROVIDER="none"
|
|
||||||
autostart=true
|
|
||||||
autorestart=true
|
|
||||||
stdout_logfile=/dev/stdout
|
|
||||||
stdout_logfile_maxbytes=0
|
|
||||||
stderr_logfile=/dev/stderr
|
|
||||||
stderr_logfile_maxbytes=0
|
|
||||||
startsecs=10
|
|
||||||
priority=10
|
|
||||||
|
|
||||||
[program:memora-control-plane]
|
|
||||||
command=/app/memora-control-plane/start-server.sh
|
|
||||||
directory=/app/memora-control-plane
|
|
||||||
environment=NODE_ENV="production",MEMORA_CP_PORT="3000",MEMORA_CP_HOSTNAME="0.0.0.0",MEMORA_CP_DATAPLANE_API_URL="http://localhost:8080"
|
|
||||||
autostart=true
|
|
||||||
autorestart=true
|
|
||||||
stdout_logfile=/dev/stdout
|
|
||||||
stdout_logfile_maxbytes=0
|
|
||||||
stderr_logfile=/dev/stderr
|
|
||||||
stderr_logfile_maxbytes=0
|
|
||||||
startsecs=5
|
|
||||||
priority=20
|
|
||||||
22
uv.lock
22
uv.lock
|
|
@ -13,7 +13,6 @@ members = [
|
||||||
"memora-client",
|
"memora-client",
|
||||||
"memora-dev",
|
"memora-dev",
|
||||||
"memora-langmem",
|
"memora-langmem",
|
||||||
"memora-mcp-server",
|
|
||||||
"memora-openai",
|
"memora-openai",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1717,6 +1716,9 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
|
mcp = [
|
||||||
|
{ name = "fastmcp" },
|
||||||
|
]
|
||||||
test = [
|
test = [
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "pytest-asyncio" },
|
{ name = "pytest-asyncio" },
|
||||||
|
|
@ -1728,6 +1730,7 @@ requires-dist = [
|
||||||
{ name = "alembic", specifier = ">=1.17.1" },
|
{ name = "alembic", specifier = ">=1.17.1" },
|
||||||
{ name = "asyncpg", specifier = ">=0.29.0" },
|
{ name = "asyncpg", specifier = ">=0.29.0" },
|
||||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.120.3" },
|
{ name = "fastapi", extras = ["standard"], specifier = ">=0.120.3" },
|
||||||
|
{ name = "fastmcp", marker = "extra == 'mcp'", specifier = ">=2.0.0" },
|
||||||
{ name = "greenlet", specifier = ">=3.2.4" },
|
{ name = "greenlet", specifier = ">=3.2.4" },
|
||||||
{ name = "httpx", specifier = ">=0.27.0" },
|
{ name = "httpx", specifier = ">=0.27.0" },
|
||||||
{ name = "langchain-text-splitters", specifier = ">=0.3.0" },
|
{ name = "langchain-text-splitters", specifier = ">=0.3.0" },
|
||||||
|
|
@ -1747,7 +1750,7 @@ requires-dist = [
|
||||||
{ name = "transformers", specifier = ">=4.30.0" },
|
{ name = "transformers", specifier = ">=4.30.0" },
|
||||||
{ name = "uvicorn", specifier = ">=0.38.0" },
|
{ name = "uvicorn", specifier = ">=0.38.0" },
|
||||||
]
|
]
|
||||||
provides-extras = ["test"]
|
provides-extras = ["test", "mcp"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memora-client"
|
name = "memora-client"
|
||||||
|
|
@ -1814,21 +1817,6 @@ requires-dist = [
|
||||||
]
|
]
|
||||||
provides-extras = ["test"]
|
provides-extras = ["test"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "memora-mcp-server"
|
|
||||||
version = "0.0.1"
|
|
||||||
source = { editable = "memora-mcp-server" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "fastmcp" },
|
|
||||||
{ name = "httpx" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [
|
|
||||||
{ name = "fastmcp", specifier = ">=0.7.0" },
|
|
||||||
{ name = "httpx", specifier = ">=0.28.1" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memora-openai"
|
name = "memora-openai"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue