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_PORT=8080
|
||||
|
||||
MEMORA_API_MCP_ENABLED=true
|
||||
|
||||
# =============================================================================
|
||||
# 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))
|
||||
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
|
||||
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_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__":
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ test = [
|
|||
"pytest-asyncio>=0.21.0",
|
||||
"pytest-timeout>=2.4.0",
|
||||
]
|
||||
mcp = [
|
||||
"fastmcp>=2.0.0",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
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-dev",
|
||||
"memora-langmem",
|
||||
"memora-mcp-server",
|
||||
"memora-openai",
|
||||
]
|
||||
|
||||
|
|
@ -1717,6 +1716,9 @@ dependencies = [
|
|||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
mcp = [
|
||||
{ name = "fastmcp" },
|
||||
]
|
||||
test = [
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
|
|
@ -1728,6 +1730,7 @@ requires-dist = [
|
|||
{ name = "alembic", specifier = ">=1.17.1" },
|
||||
{ name = "asyncpg", specifier = ">=0.29.0" },
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.120.3" },
|
||||
{ name = "fastmcp", marker = "extra == 'mcp'", specifier = ">=2.0.0" },
|
||||
{ name = "greenlet", specifier = ">=3.2.4" },
|
||||
{ name = "httpx", specifier = ">=0.27.0" },
|
||||
{ name = "langchain-text-splitters", specifier = ">=0.3.0" },
|
||||
|
|
@ -1747,7 +1750,7 @@ requires-dist = [
|
|||
{ name = "transformers", specifier = ">=4.30.0" },
|
||||
{ name = "uvicorn", specifier = ">=0.38.0" },
|
||||
]
|
||||
provides-extras = ["test"]
|
||||
provides-extras = ["test", "mcp"]
|
||||
|
||||
[[package]]
|
||||
name = "memora-client"
|
||||
|
|
@ -1814,21 +1817,6 @@ requires-dist = [
|
|||
]
|
||||
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]]
|
||||
name = "memora-openai"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
Loading…
Reference in a new issue