Odysseus v1.0

This commit is contained in:
pewdiepie-archdaemon
2026-05-31 23:58:26 +09:00
commit e5c99a5eee
421 changed files with 271349 additions and 0 deletions
View File
+18
View File
@@ -0,0 +1,18 @@
"""
_common.py
Shared constants and helpers for built-in MCP servers.
"""
MAX_OUTPUT_CHARS = 10_000
MAX_READ_CHARS = 20_000
SHELL_TIMEOUT = 60
PYTHON_TIMEOUT = 30
SEARCH_TIMEOUT = 30
def truncate(text: str, limit: int = MAX_OUTPUT_CHARS) -> str:
"""Truncate text to *limit* characters with a suffix note."""
if len(text) > limit:
return text[:limit] + f"\n... (truncated, {len(text)} chars total)"
return text
File diff suppressed because it is too large Load Diff
+166
View File
@@ -0,0 +1,166 @@
"""
image_gen_server.py
MCP server exposing image generation via OpenAI-compatible APIs.
"""
import asyncio
import base64
import sys
import uuid
from pathlib import Path
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
server = Server("image_gen")
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="generate_image",
description="Generate an image using an image-capable model (e.g. gpt-image-1)",
inputSchema={
"type": "object",
"properties": {
"prompt": {"type": "string", "description": "Image description prompt"},
"model": {"type": "string", "description": "Model name (auto-detects if omitted)"},
"size": {"type": "string", "description": "Image size (default 1024x1024)"},
"quality": {"type": "string", "description": "Quality: low, medium, high, auto (default medium)"},
},
"required": ["prompt"],
},
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name != "generate_image":
return [TextContent(type="text", text=f"Unknown tool: {name}")]
prompt = arguments.get("prompt", "")
model_spec = arguments.get("model", "")
size = arguments.get("size", "1024x1024")
quality = arguments.get("quality", "medium")
if not prompt:
return [TextContent(type="text", text="Error: Image prompt is required")]
try:
import httpx
from src.settings import load_settings, get_setting
from src.ai_interaction import _resolve_model
if not get_setting("image_gen_enabled", True):
return [TextContent(type="text", text="Error: Image generation is disabled by the administrator.")]
_settings = load_settings()
if not model_spec:
model_spec = _settings.get("image_model", "")
if quality == "medium" and _settings.get("image_quality"):
quality = _settings["image_quality"]
# Auto-detect best available image model
if not model_spec:
for candidate in ("gpt-image-1.5", "gpt-image-1", "dall-e-3"):
try:
_resolve_model(candidate)
model_spec = candidate
break
except ValueError:
continue
if not model_spec:
return [TextContent(type="text", text="Error: No image model found. Configure one in Admin.")]
url, model_id, headers = _resolve_model(model_spec)
is_gpt_image = "gpt-image" in model_id.lower()
base_url = url.replace("/chat/completions", "").replace("/v1/messages", "").rstrip("/")
images_url = base_url + "/images/generations"
valid_gpt_sizes = {"1024x1024", "1024x1536", "1536x1024", "auto"}
valid_dalle3_sizes = {"1024x1024", "1024x1792", "1792x1024"}
if is_gpt_image and size not in valid_gpt_sizes:
size = "1024x1024"
elif not is_gpt_image and size not in valid_dalle3_sizes:
size = "1024x1024"
payload = {"model": model_id, "prompt": prompt, "n": 1, "size": size}
if is_gpt_image:
payload["quality"] = quality if quality in ("low", "medium", "high", "auto") else "medium"
async with httpx.AsyncClient(timeout=httpx.Timeout(connect=30.0, read=300.0, write=30.0, pool=30.0)) as client:
resp = await client.post(images_url, json=payload, headers=headers)
if resp.status_code != 200:
error_text = resp.text[:500]
try:
err_json = resp.json()
error_text = err_json.get("error", {}).get("message", error_text) if isinstance(err_json.get("error"), dict) else str(err_json.get("error", error_text))
except Exception:
pass
return [TextContent(type="text", text=f"Error: Image generation failed ({resp.status_code}): {error_text}")]
data = resp.json()
images = data.get("data", [])
if not images:
return [TextContent(type="text", text="Error: No images returned from API")]
img = images[0]
image_url = None
if img.get("b64_json"):
img_dir = Path("data/generated_images")
img_dir.mkdir(parents=True, exist_ok=True)
filename = f"{uuid.uuid4().hex[:12]}.png"
img_path = img_dir / filename
img_path.write_bytes(base64.b64decode(img["b64_json"]))
image_url = f"/api/generated-image/{filename}"
# Save to gallery
try:
from src.database import SessionLocal, GalleryImage
db = SessionLocal()
db.add(GalleryImage(
id=str(uuid.uuid4()),
filename=filename,
prompt=prompt,
model=model_id,
size=size,
quality=payload.get("quality", "medium"),
))
db.commit()
db.close()
except Exception:
pass
elif img.get("url"):
image_url = img["url"]
else:
return [TextContent(type="text", text="Error: Unexpected image API response format")]
result = f"Generated image for: {prompt[:100]}\nimage_url: {image_url}\nmodel: {model_id}\nsize: {size}"
return [TextContent(type="text", text=result)]
except httpx.TimeoutException:
return [TextContent(type="text", text="Error: Image generation timed out (300s)")]
except ValueError as e:
return [TextContent(type="text", text=f"Error: {e}")]
except Exception as e:
return [TextContent(type="text", text=f"Error: {e}")]
async def run():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, server.create_initialization_options())
if __name__ == "__main__":
asyncio.run(run())
+208
View File
@@ -0,0 +1,208 @@
"""
memory_server.py
MCP server exposing memory management (list, add, edit, delete, search).
Imports MemoryManager and MemoryVectorStore from the Odysseus codebase.
"""
import asyncio
import sys
import time
from pathlib import Path
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
server = Server("memory")
# Late-initialized managers (set during first tool call)
_memory_manager = None
_memory_vector = None
_initialized = False
def _ensure_init():
"""Lazy-init memory managers on first use."""
global _memory_manager, _memory_vector, _initialized
if _initialized:
return
_initialized = True
from src.constants import DATA_DIR
from src.memory import MemoryManager
_memory_manager = MemoryManager(DATA_DIR)
try:
from src.memory_vector import MemoryVectorStore
_memory_vector = MemoryVectorStore(DATA_DIR)
if not _memory_vector.healthy:
_memory_vector = None
except Exception:
_memory_vector = None
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="manage_memory",
description="Manage the user's memory system: list, add, edit, delete, or search memories.",
inputSchema={
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["list", "add", "edit", "delete", "search"],
"description": "The action to perform",
},
"text": {"type": "string", "description": "Memory text (add/edit) or search query (search)"},
"memory_id": {"type": "string", "description": "Memory ID (edit/delete)"},
"category": {
"type": "string",
"enum": ["fact", "event", "contact", "preference"],
"description": "Memory category (add/list filter)",
},
},
"required": ["action"],
},
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name != "manage_memory":
return [TextContent(type="text", text=f"Unknown tool: {name}")]
_ensure_init()
if not _memory_manager:
return [TextContent(type="text", text="Error: Memory manager not available")]
action = arguments.get("action", "")
if action == "list":
category_filter = arguments.get("category", "")
memories = _memory_manager.load()
if category_filter:
memories = [m for m in memories if m.get("category", "").lower() == category_filter.lower()]
if not memories:
msg = "No memories found"
if category_filter:
msg += f" in category '{category_filter}'"
return [TextContent(type="text", text=msg + ".")]
lines = [f"Found {len(memories)} memory entries:\n"]
for m in memories[:100]:
cat = m.get("category", "fact")
mid = m.get("id", "?")[:8]
text = m.get("text", "")
if len(text) > 150:
text = text[:150] + "..."
lines.append(f"- [{cat}] `{mid}` — {text}")
if len(memories) > 100:
lines.append(f"... and {len(memories) - 100} more")
return [TextContent(type="text", text="\n".join(lines))]
elif action == "add":
text = arguments.get("text", "")
category = arguments.get("category", "fact")
if not text:
return [TextContent(type="text", text="Error: Memory text cannot be empty")]
entry = _memory_manager.add_entry(text, source="ai_agent", category=category)
memories = _memory_manager.load_all()
memories.append(entry)
_memory_manager.save(memories)
if _memory_vector and _memory_vector.healthy:
try:
_memory_vector.add(entry["id"], text)
except Exception:
pass
return [TextContent(type="text", text=f"Memory added: [{category}] {text} (id: {entry['id'][:8]})")]
elif action == "edit":
memory_id = arguments.get("memory_id", "")
new_text = arguments.get("text", "")
if not memory_id or not new_text:
return [TextContent(type="text", text="Error: edit needs memory_id and text")]
memories = _memory_manager.load_all()
found = False
full_id = None
for m in memories:
if m.get("id", "").startswith(memory_id):
m["text"] = new_text
m["timestamp"] = int(time.time())
found = True
full_id = m["id"]
break
if not found:
return [TextContent(type="text", text=f"Error: Memory '{memory_id}' not found")]
_memory_manager.save(memories)
if _memory_vector and _memory_vector.healthy and full_id:
try:
_memory_vector.remove(full_id)
_memory_vector.add(full_id, new_text)
except Exception:
pass
return [TextContent(type="text", text=f"Memory updated: {new_text}")]
elif action == "delete":
memory_id = arguments.get("memory_id", "")
if not memory_id:
return [TextContent(type="text", text="Error: delete needs memory_id")]
memories = _memory_manager.load_all()
full_id = None
deleted_text = ""
deleted_category = ""
for m in memories:
if m.get("id", "").startswith(memory_id):
full_id = m["id"]
deleted_text = m.get("text", "")
deleted_category = m.get("category", "")
break
original_len = len(memories)
memories = [m for m in memories if not m.get("id", "").startswith(memory_id)]
if len(memories) == original_len:
return [TextContent(type="text", text=f"Error: Memory '{memory_id}' not found")]
_memory_manager.save(memories)
if _memory_vector and _memory_vector.healthy and full_id:
try:
_memory_vector.remove(full_id)
except Exception:
pass
cat = f"[{deleted_category}] " if deleted_category else ""
snippet = deleted_text if len(deleted_text) <= 120 else deleted_text[:117] + "..."
return [TextContent(type="text", text=f"Memory deleted: {cat}{snippet} (id: {memory_id})")]
elif action == "search":
query = arguments.get("text", "")
if not query:
return [TextContent(type="text", text="Error: search needs text (query)")]
memories = _memory_manager.load()
if hasattr(_memory_manager, 'get_relevant_memories'):
results = _memory_manager.get_relevant_memories(query, memories, threshold=0.05, max_items=20)
else:
query_lower = query.lower()
results = [m for m in memories if query_lower in m.get("text", "").lower()][:20]
if not results:
return [TextContent(type="text", text=f"No memories found matching '{query}'.")]
lines = [f"Found {len(results)} matching memories:\n"]
for m in results:
cat = m.get("category", "fact")
mid = m.get("id", "?")[:8]
text = m.get("text", "")
lines.append(f"- [{cat}] `{mid}` — {text}")
return [TextContent(type="text", text="\n".join(lines))]
else:
return [TextContent(type="text", text=f"Error: Unknown action '{action}'. Use: list, add, edit, delete, search")]
async def run():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, server.create_initialization_options())
if __name__ == "__main__":
asyncio.run(run())
+144
View File
@@ -0,0 +1,144 @@
"""
rag_server.py
MCP server exposing RAG document management (list, add_directory, remove_directory).
"""
import asyncio
import os
import sys
from pathlib import Path
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
server = Server("rag")
_rag_manager = None
_personal_docs_manager = None
_initialized = False
def _ensure_init():
"""Lazy-init RAG managers on first use."""
global _rag_manager, _personal_docs_manager, _initialized
if _initialized:
return
_initialized = True
try:
from src.rag_singleton import get_rag_manager
_rag_manager = get_rag_manager()
except Exception:
pass
try:
from src.constants import PERSONAL_DIR
from src.personal_docs import PersonalDocsManager
_personal_docs_manager = PersonalDocsManager(PERSONAL_DIR, _rag_manager)
except Exception:
pass
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="manage_rag",
description="Manage RAG indexed documents. List indexed files, add directories, or remove directories.",
inputSchema={
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["list", "add_directory", "remove_directory"],
"description": "The action to perform",
},
"directory": {"type": "string", "description": "Directory path (for add/remove)"},
},
"required": ["action"],
},
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name != "manage_rag":
return [TextContent(type="text", text=f"Unknown tool: {name}")]
_ensure_init()
action = arguments.get("action", "")
if action == "list":
if not _personal_docs_manager:
return [TextContent(type="text", text="Personal docs manager not available. RAG may not be configured.")]
try:
files = getattr(_personal_docs_manager, 'index', None) or []
dirs = []
if hasattr(_personal_docs_manager, 'get_indexed_directories'):
dirs = _personal_docs_manager.get_indexed_directories()
lines = []
if dirs:
lines.append(f"**Indexed directories ({len(dirs)}):**")
for d in dirs:
lines.append(f" - `{d}`")
if files:
lines.append(f"\n**Indexed files ({len(files)}):**")
for f in files[:50]:
fname = f.get("name", str(f)) if isinstance(f, dict) else str(f)
lines.append(f" - {fname}")
if len(files) > 50:
lines.append(f" ... and {len(files) - 50} more")
if not lines:
return [TextContent(type="text", text="No files or directories indexed in RAG.")]
return [TextContent(type="text", text="\n".join(lines))]
except Exception as e:
return [TextContent(type="text", text=f"Error: {e}")]
elif action == "add_directory":
directory = arguments.get("directory", "").strip()
if not directory:
return [TextContent(type="text", text="Error: add_directory needs a directory path")]
directory = os.path.expanduser(directory)
if not os.path.isdir(directory):
return [TextContent(type="text", text=f"Error: Directory not found: {directory}")]
if not _rag_manager:
return [TextContent(type="text", text="Error: RAG manager not available")]
try:
result = _rag_manager.index_personal_documents(directory)
indexed = result.get("indexed_count", 0) if isinstance(result, dict) else 0
return [TextContent(type="text", text=f"Directory '{directory}' added to RAG index ({indexed} chunks indexed)")]
except Exception as e:
return [TextContent(type="text", text=f"Error: Failed to index directory: {e}")]
elif action == "remove_directory":
directory = arguments.get("directory", "").strip()
if not directory:
return [TextContent(type="text", text="Error: remove_directory needs a directory path")]
if not _personal_docs_manager:
return [TextContent(type="text", text="Error: Personal docs manager not available")]
try:
if hasattr(_personal_docs_manager, 'remove_directory'):
_personal_docs_manager.remove_directory(directory)
if _rag_manager and hasattr(_rag_manager, 'remove_directory'):
_rag_manager.remove_directory(directory)
return [TextContent(type="text", text=f"Directory '{directory}' removed from RAG index")]
except Exception as e:
return [TextContent(type="text", text=f"Error: Failed to remove directory: {e}")]
else:
return [TextContent(type="text", text=f"Error: Unknown action '{action}'. Use: list, add_directory, remove_directory")]
async def run():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, server.create_initialization_options())
if __name__ == "__main__":
asyncio.run(run())