Fix Docker startup script and complete application deployment

- Update docker-start.sh to force correct profiles (qdrant, admin)
- Fix PostgreSQL port mapping from 5432 to 15432 across all configs
- Resolve MCP import conflicts by renaming src/mcp to src/mcp_servers
- Fix admin interface StaticFiles mount syntax error
- Update LLM client to support both Ollama and OpenAI-compatible APIs
- Configure host networking for Discord bot container access
- Correct database connection handling for async context managers
- Update environment variables and Docker compose configurations
- Add missing production dependencies and Dockerfile improvements
This commit is contained in:
root
2025-07-05 15:09:29 -07:00
parent 824b118e93
commit 3d9e8ffbf0
59 changed files with 1100 additions and 244 deletions

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,497 @@
"""
MCP Server for Collaborative Creative Projects
Enables characters to autonomously manage creative collaborations
"""
import asyncio
import json
import logging
from typing import Dict, List, Any, Optional, Sequence
from datetime import datetime, timedelta
from mcp.server import Server
from mcp.server.models import InitializationOptions
from mcp.server.stdio import stdio_server
from mcp.types import (
CallToolRequestParams,
ListToolsRequest,
TextContent,
Tool
)
from collaboration.creative_projects import (
CollaborativeCreativeManager,
ProjectType,
ContributionType,
ProjectStatus
)
from utils.logging import log_character_action, log_error_with_context
logger = logging.getLogger(__name__)
class CreativeProjectsMCPServer:
"""MCP server for autonomous creative project management"""
def __init__(self, creative_manager: CollaborativeCreativeManager):
self.creative_manager = creative_manager
self.server = Server("creative-projects")
self.current_character: Optional[str] = None
# Register MCP tools
self._register_tools()
def _register_tools(self):
"""Register all creative project tools"""
@self.server.list_tools()
async def handle_list_tools() -> list[Tool]:
"""List available creative project tools"""
return [
Tool(
name="propose_creative_project",
description="Propose a new collaborative creative project",
inputSchema={
"type": "object",
"properties": {
"title": {"type": "string", "description": "Project title"},
"description": {"type": "string", "description": "Project description and vision"},
"project_type": {
"type": "string",
"enum": ["story", "poem", "philosophy", "worldbuilding", "music", "art_concept", "dialogue", "research", "manifesto", "mythology"],
"description": "Type of creative project"
},
"target_collaborators": {
"type": "array",
"items": {"type": "string"},
"description": "List of characters to invite"
},
"goals": {
"type": "array",
"items": {"type": "string"},
"description": "Project goals and objectives"
},
"role_descriptions": {
"type": "object",
"description": "Specific roles for each collaborator"
},
"estimated_duration": {"type": "string", "description": "Estimated project duration"}
},
"required": ["title", "description", "project_type", "target_collaborators"]
}
),
Tool(
name="respond_to_project_invitation",
description="Respond to a creative project invitation",
inputSchema={
"type": "object",
"properties": {
"invitation_id": {"type": "string", "description": "Invitation ID"},
"accept": {"type": "boolean", "description": "Whether to accept the invitation"},
"response_message": {"type": "string", "description": "Response message"}
},
"required": ["invitation_id", "accept"]
}
),
Tool(
name="contribute_to_project",
description="Add a contribution to a creative project",
inputSchema={
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "Project ID"},
"content": {"type": "string", "description": "Contribution content"},
"contribution_type": {
"type": "string",
"enum": ["idea", "content", "revision", "feedback", "inspiration", "structure", "polish"],
"description": "Type of contribution"
},
"build_on_contribution_id": {"type": "string", "description": "ID of contribution to build upon"},
"feedback_for_contribution_id": {"type": "string", "description": "ID of contribution being reviewed"},
"metadata": {"type": "object", "description": "Additional contribution metadata"}
},
"required": ["project_id", "content", "contribution_type"]
}
),
Tool(
name="get_project_suggestions",
description="Get personalized project suggestions based on interests and relationships",
inputSchema={"type": "object", "properties": {}}
),
Tool(
name="get_active_projects",
description="Get currently active projects for the character",
inputSchema={"type": "object", "properties": {}}
),
Tool(
name="get_project_analytics",
description="Get detailed analytics for a specific project",
inputSchema={
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "Project ID"}
},
"required": ["project_id"]
}
),
Tool(
name="get_pending_invitations",
description="Get pending project invitations for the character",
inputSchema={"type": "object", "properties": {}}
),
Tool(
name="search_projects",
description="Search for projects by topic, type, or collaborators",
inputSchema={
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
"project_type": {"type": "string", "description": "Filter by project type"},
"status": {"type": "string", "description": "Filter by project status"},
"collaborator": {"type": "string", "description": "Filter by specific collaborator"}
}
}
)
]
@self.server.call_tool()
async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> Sequence[TextContent]:
"""Handle tool calls for creative projects"""
if not self.current_character:
return [TextContent(type="text", text="Error: No character context set")]
try:
if name == "propose_creative_project":
return await self._propose_creative_project(arguments)
elif name == "respond_to_project_invitation":
return await self._respond_to_project_invitation(arguments)
elif name == "contribute_to_project":
return await self._contribute_to_project(arguments)
elif name == "get_project_suggestions":
return await self._get_project_suggestions(arguments)
elif name == "get_active_projects":
return await self._get_active_projects(arguments)
elif name == "get_project_analytics":
return await self._get_project_analytics(arguments)
elif name == "get_pending_invitations":
return await self._get_pending_invitations(arguments)
elif name == "search_projects":
return await self._search_projects(arguments)
else:
return [TextContent(type="text", text=f"Unknown tool: {name}")]
except Exception as e:
log_error_with_context(e, {"tool": name, "character": self.current_character})
return [TextContent(type="text", text=f"Error executing {name}: {str(e)}")]
async def set_character_context(self, character_name: str):
"""Set the current character context for tool calls"""
self.current_character = character_name
logger.info(f"Creative projects MCP context set to {character_name}")
async def _propose_creative_project(self, args: Dict[str, Any]) -> Sequence[TextContent]:
"""Handle project proposal tool call"""
try:
success, message = await self.creative_manager.propose_project(
initiator=self.current_character,
project_idea=args
)
if success:
log_character_action(self.current_character, "mcp_proposed_project", {
"project_title": args.get("title"),
"project_type": args.get("project_type"),
"collaborator_count": len(args.get("target_collaborators", []))
})
return [TextContent(
type="text",
text=f"✨ Successfully proposed creative project '{args['title']}'! {message}"
)]
else:
return [TextContent(
type="text",
text=f"❌ Failed to propose project: {message}"
)]
except Exception as e:
log_error_with_context(e, {"tool": "propose_creative_project", "character": self.current_character})
return [TextContent(type="text", text=f"Error proposing project: {str(e)}")]
async def _respond_to_project_invitation(self, args: Dict[str, Any]) -> Sequence[TextContent]:
"""Handle invitation response tool call"""
try:
invitation_id = args["invitation_id"]
accepted = args["accept"]
response_message = args.get("response_message", "")
success, message = await self.creative_manager.respond_to_invitation(
invitee=self.current_character,
invitation_id=invitation_id,
accepted=accepted,
response_message=response_message
)
if success:
action = "accepted" if accepted else "declined"
log_character_action(self.current_character, f"mcp_{action}_project_invitation", {
"invitation_id": invitation_id,
"response": response_message
})
emoji = "" if accepted else ""
return [TextContent(
type="text",
text=f"{emoji} {message}"
)]
else:
return [TextContent(
type="text",
text=f"❌ Failed to respond to invitation: {message}"
)]
except Exception as e:
log_error_with_context(e, {"tool": "respond_to_project_invitation", "character": self.current_character})
return [TextContent(type="text", text=f"Error responding to invitation: {str(e)}")]
async def _contribute_to_project(self, args: Dict[str, Any]) -> Sequence[TextContent]:
"""Handle project contribution tool call"""
try:
project_id = args["project_id"]
contribution_data = {
"content": args["content"],
"contribution_type": args["contribution_type"],
"build_on_contribution_id": args.get("build_on_contribution_id"),
"feedback_for_contribution_id": args.get("feedback_for_contribution_id"),
"metadata": args.get("metadata", {})
}
success, message = await self.creative_manager.contribute_to_project(
contributor=self.current_character,
project_id=project_id,
contribution=contribution_data
)
if success:
log_character_action(self.current_character, "mcp_contributed_to_project", {
"project_id": project_id,
"contribution_type": args["contribution_type"],
"content_length": len(args["content"])
})
return [TextContent(
type="text",
text=f"🎨 Successfully added {args['contribution_type']} contribution! {message}"
)]
else:
return [TextContent(
type="text",
text=f"❌ Failed to add contribution: {message}"
)]
except Exception as e:
log_error_with_context(e, {"tool": "contribute_to_project", "character": self.current_character})
return [TextContent(type="text", text=f"Error adding contribution: {str(e)}")]
async def _get_project_suggestions(self, args: Dict[str, Any]) -> Sequence[TextContent]:
"""Handle project suggestions tool call"""
try:
suggestions = await self.creative_manager.get_project_suggestions(self.current_character)
if not suggestions:
return [TextContent(
type="text",
text="💡 No specific project suggestions at the moment. Consider exploring new creative areas or connecting with other characters!"
)]
response = "🎯 Personalized Creative Project Suggestions:\n\n"
for i, suggestion in enumerate(suggestions, 1):
response += f"{i}. **{suggestion['title']}**\n"
response += f" Type: {suggestion['project_type']}\n"
response += f" Description: {suggestion['description']}\n"
response += f" Suggested collaborators: {', '.join(suggestion.get('suggested_collaborators', []))}\n"
response += f" Inspiration: {suggestion.get('inspiration', 'Unknown')}\n\n"
response += "💭 These suggestions are based on your interests, creative history, and relationships with other characters."
return [TextContent(type="text", text=response)]
except Exception as e:
log_error_with_context(e, {"tool": "get_project_suggestions", "character": self.current_character})
return [TextContent(type="text", text=f"Error getting suggestions: {str(e)}")]
async def _get_active_projects(self, args: Dict[str, Any]) -> Sequence[TextContent]:
"""Handle active projects tool call"""
try:
active_projects = await self.creative_manager.get_active_projects(self.current_character)
if not active_projects:
return [TextContent(
type="text",
text="📂 You're not currently involved in any active creative projects. Why not propose something new or join an existing collaboration?"
)]
response = f"🎨 Your Active Creative Projects ({len(active_projects)}):\n\n"
for project in active_projects:
response += f"**{project.title}** ({project.project_type.value})\n"
response += f"Status: {project.status.value} | Collaborators: {len(project.collaborators)}\n"
response += f"Description: {project.description[:100]}...\n"
response += f"Contributions: {len(project.contributions)} | Created: {project.created_at.strftime('%m/%d/%Y')}\n\n"
return [TextContent(type="text", text=response)]
except Exception as e:
log_error_with_context(e, {"tool": "get_active_projects", "character": self.current_character})
return [TextContent(type="text", text=f"Error getting active projects: {str(e)}")]
async def _get_project_analytics(self, args: Dict[str, Any]) -> Sequence[TextContent]:
"""Handle project analytics tool call"""
try:
project_id = args["project_id"]
analytics = await self.creative_manager.get_project_analytics(project_id)
if "error" in analytics:
return [TextContent(
type="text",
text=f"❌ Error getting analytics: {analytics['error']}"
)]
project_info = analytics["project_info"]
contrib_stats = analytics["contribution_stats"]
health_metrics = analytics["health_metrics"]
response = f"📊 Analytics for '{project_info['title']}':\n\n"
response += f"**Project Status:**\n"
response += f"• Status: {project_info['status']}\n"
response += f"• Days active: {project_info['days_active']}\n"
response += f"• Collaborators: {project_info['collaborators']}\n"
response += f"• Content length: {project_info['current_content_length']} characters\n\n"
response += f"**Contribution Statistics:**\n"
response += f"• Total contributions: {contrib_stats['total_contributions']}\n"
if contrib_stats['by_type']:
response += f"• By type: {', '.join([f'{k}({v})' for k, v in contrib_stats['by_type'].items()])}\n"
if contrib_stats['by_contributor']:
response += f"• By contributor: {', '.join([f'{k}({v})' for k, v in contrib_stats['by_contributor'].items()])}\n"
response += f"\n**Health Metrics:**\n"
response += f"• Avg contributions/day: {health_metrics['avg_contributions_per_day']}\n"
response += f"• Unique contributors: {health_metrics['unique_contributors']}\n"
response += f"• Collaboration balance: {health_metrics['collaboration_balance']:.2f}\n"
response += f"• Estimated completion: {health_metrics['completion_estimate']}\n"
return [TextContent(type="text", text=response)]
except Exception as e:
log_error_with_context(e, {"tool": "get_project_analytics", "character": self.current_character})
return [TextContent(type="text", text=f"Error getting analytics: {str(e)}")]
async def _get_pending_invitations(self, args: Dict[str, Any]) -> Sequence[TextContent]:
"""Handle pending invitations tool call"""
try:
# Get pending invitations for this character
pending_invitations = []
for invitation in self.creative_manager.pending_invitations.values():
if invitation.invitee == self.current_character and invitation.status == "pending":
if datetime.utcnow() <= invitation.expires_at:
pending_invitations.append(invitation)
if not pending_invitations:
return [TextContent(
type="text",
text="📫 No pending project invitations at the moment."
)]
response = f"📮 You have {len(pending_invitations)} pending project invitation(s):\n\n"
for invitation in pending_invitations:
project = self.creative_manager.active_projects.get(invitation.project_id)
project_title = project.title if project else "Unknown Project"
response += f"**Invitation from {invitation.inviter}**\n"
response += f"Project: {project_title}\n"
response += f"Role: {invitation.role_description}\n"
response += f"Message: {invitation.invitation_message}\n"
response += f"Expires: {invitation.expires_at.strftime('%m/%d/%Y %H:%M')}\n"
response += f"Invitation ID: {invitation.id}\n\n"
response += "💡 Use 'respond_to_project_invitation' to accept or decline these invitations."
return [TextContent(type="text", text=response)]
except Exception as e:
log_error_with_context(e, {"tool": "get_pending_invitations", "character": self.current_character})
return [TextContent(type="text", text=f"Error getting invitations: {str(e)}")]
async def _search_projects(self, args: Dict[str, Any]) -> Sequence[TextContent]:
"""Handle project search tool call"""
try:
query = args.get("query", "")
project_type_filter = args.get("project_type")
status_filter = args.get("status")
collaborator_filter = args.get("collaborator")
# Simple search implementation - could be enhanced with vector search
matching_projects = []
for project in self.creative_manager.active_projects.values():
# Check if character has access to this project
if self.current_character not in project.collaborators:
continue
# Apply filters
if project_type_filter and project.project_type.value != project_type_filter:
continue
if status_filter and project.status.value != status_filter:
continue
if collaborator_filter and collaborator_filter not in project.collaborators:
continue
# Text search in title and description
if query:
search_text = f"{project.title} {project.description}".lower()
if query.lower() not in search_text:
continue
matching_projects.append(project)
if not matching_projects:
return [TextContent(
type="text",
text="🔍 No projects found matching your search criteria."
)]
response = f"🔍 Found {len(matching_projects)} matching project(s):\n\n"
for project in matching_projects[:5]: # Limit to 5 results
response += f"**{project.title}** ({project.project_type.value})\n"
response += f"Status: {project.status.value} | Collaborators: {', '.join(project.collaborators)}\n"
response += f"Description: {project.description[:100]}...\n"
response += f"ID: {project.id}\n\n"
if len(matching_projects) > 5:
response += f"... and {len(matching_projects) - 5} more projects.\n"
return [TextContent(type="text", text=response)]
except Exception as e:
log_error_with_context(e, {"tool": "search_projects", "character": self.current_character})
return [TextContent(type="text", text=f"Error searching projects: {str(e)}")]
# Global creative projects MCP server
creative_projects_mcp_server = None
def get_creative_projects_mcp_server() -> CreativeProjectsMCPServer:
global creative_projects_mcp_server
return creative_projects_mcp_server
def initialize_creative_projects_mcp_server(creative_manager: CollaborativeCreativeManager) -> CreativeProjectsMCPServer:
global creative_projects_mcp_server
creative_projects_mcp_server = CreativeProjectsMCPServer(creative_manager)
return creative_projects_mcp_server

View File

@@ -0,0 +1,918 @@
import asyncio
import json
from typing import Dict, Any, List, Optional, Set
from datetime import datetime
from pathlib import Path
import aiofiles
import hashlib
from dataclasses import dataclass
from mcp.server.stdio import stdio_server
from mcp.server import Server
from mcp.types import Tool, TextContent, ImageContent, EmbeddedResource
from utils.logging import log_character_action, log_error_with_context
from rag.vector_store import VectorStoreManager, VectorMemory, MemoryType
import logging
logger = logging.getLogger(__name__)
@dataclass
class FileAccess:
character_name: str
file_path: str
access_type: str # 'read', 'write', 'delete'
timestamp: datetime
success: bool
class CharacterFileSystemMCP:
"""MCP Server for character file system access and digital spaces"""
def __init__(self, data_dir: str = "./data/characters", community_dir: str = "./data/community"):
self.data_dir = Path(data_dir)
self.community_dir = Path(community_dir)
# Create base directories
self.data_dir.mkdir(parents=True, exist_ok=True)
self.community_dir.mkdir(parents=True, exist_ok=True)
# File access permissions
self.character_permissions = {
"read_own": True,
"write_own": True,
"read_community": True,
"write_community": True,
"read_others": False, # Characters can't read other's private files
"write_others": False
}
# File type restrictions
self.allowed_extensions = {
'.txt', '.md', '.json', '.yaml', '.yml', '.csv',
'.py', '.js', '.html', '.css' # Limited code files
}
# Maximum file sizes (in bytes)
self.max_file_sizes = {
'.txt': 100_000, # 100KB
'.md': 200_000, # 200KB
'.json': 50_000, # 50KB
'.yaml': 50_000, # 50KB
'.yml': 50_000, # 50KB
'.csv': 500_000, # 500KB
'.py': 100_000, # 100KB
'.js': 100_000, # 100KB
'.html': 200_000, # 200KB
'.css': 100_000 # 100KB
}
# Track file access for security
self.access_log: List[FileAccess] = []
# Vector store for indexing file contents
self.vector_store: Optional[VectorStoreManager] = None
async def initialize(self, vector_store: VectorStoreManager, character_names: List[str]):
"""Initialize file system with character directories"""
self.vector_store = vector_store
# Create personal directories for each character
for character_name in character_names:
char_dir = self.data_dir / character_name.lower()
char_dir.mkdir(exist_ok=True)
# Create subdirectories
(char_dir / "diary").mkdir(exist_ok=True)
(char_dir / "reflections").mkdir(exist_ok=True)
(char_dir / "creative").mkdir(exist_ok=True)
(char_dir / "private").mkdir(exist_ok=True)
# Create initial files if they don't exist
await self._create_initial_files(character_name, char_dir)
# Create community directories
(self.community_dir / "shared").mkdir(exist_ok=True)
(self.community_dir / "collaborative").mkdir(exist_ok=True)
(self.community_dir / "archives").mkdir(exist_ok=True)
logger.info(f"Initialized file system for {len(character_names)} characters")
async def create_server(self) -> Server:
"""Create and configure the MCP server"""
server = Server("character-filesystem")
# Register file operation tools
await self._register_file_tools(server)
await self._register_creative_tools(server)
await self._register_community_tools(server)
await self._register_search_tools(server)
return server
async def _register_file_tools(self, server: Server):
"""Register basic file operation tools"""
@server.call_tool()
async def read_file(character_name: str, file_path: str) -> List[TextContent]:
"""Read a file from character's personal space or community"""
try:
# Validate access
access_result = await self._validate_file_access(character_name, file_path, "read")
if not access_result["allowed"]:
return [TextContent(
type="text",
text=f"Access denied: {access_result['reason']}"
)]
full_path = await self._resolve_file_path(character_name, file_path)
if not full_path.exists():
return [TextContent(
type="text",
text=f"File not found: {file_path}"
)]
# Read file content
async with aiofiles.open(full_path, 'r', encoding='utf-8') as f:
content = await f.read()
# Log access
await self._log_file_access(character_name, file_path, "read", True)
log_character_action(
character_name,
"read_file",
{"file_path": file_path, "size": len(content)}
)
return [TextContent(
type="text",
text=content
)]
except Exception as e:
await self._log_file_access(character_name, file_path, "read", False)
log_error_with_context(e, {
"character": character_name,
"file_path": file_path,
"tool": "read_file"
})
return [TextContent(
type="text",
text=f"Error reading file: {str(e)}"
)]
@server.call_tool()
async def write_file(
character_name: str,
file_path: str,
content: str,
append: bool = False
) -> List[TextContent]:
"""Write content to a file in character's personal space"""
try:
# Validate access
access_result = await self._validate_file_access(character_name, file_path, "write")
if not access_result["allowed"]:
return [TextContent(
type="text",
text=f"Access denied: {access_result['reason']}"
)]
# Validate file size
if len(content.encode('utf-8')) > self._get_max_file_size(file_path):
return [TextContent(
type="text",
text=f"File too large. Maximum size: {self._get_max_file_size(file_path)} bytes"
)]
full_path = await self._resolve_file_path(character_name, file_path)
full_path.parent.mkdir(parents=True, exist_ok=True)
# Write file
mode = 'a' if append else 'w'
async with aiofiles.open(full_path, mode, encoding='utf-8') as f:
await f.write(content)
# Index content in vector store if it's a creative or reflection file
if any(keyword in file_path.lower() for keyword in ['creative', 'reflection', 'diary']):
await self._index_file_content(character_name, file_path, content)
await self._log_file_access(character_name, file_path, "write", True)
log_character_action(
character_name,
"wrote_file",
{"file_path": file_path, "size": len(content), "append": append}
)
return [TextContent(
type="text",
text=f"Successfully {'appended to' if append else 'wrote'} file: {file_path}"
)]
except Exception as e:
await self._log_file_access(character_name, file_path, "write", False)
log_error_with_context(e, {
"character": character_name,
"file_path": file_path,
"tool": "write_file"
})
return [TextContent(
type="text",
text=f"Error writing file: {str(e)}"
)]
@server.call_tool()
async def list_files(
character_name: str,
directory: str = "",
include_community: bool = False
) -> List[TextContent]:
"""List files in character's directory or community space"""
try:
files_info = []
# List personal files
if not directory or not directory.startswith("community/"):
personal_dir = self.data_dir / character_name.lower()
if directory:
personal_dir = personal_dir / directory
if personal_dir.exists():
files_info.extend(await self._list_directory_contents(personal_dir, "personal"))
# List community files if requested
if include_community or (directory and directory.startswith("community/")):
community_path = directory.replace("community/", "") if directory.startswith("community/") else ""
community_dir = self.community_dir
if community_path:
community_dir = community_dir / community_path
if community_dir.exists():
files_info.extend(await self._list_directory_contents(community_dir, "community"))
log_character_action(
character_name,
"listed_files",
{"directory": directory, "file_count": len(files_info)}
)
return [TextContent(
type="text",
text=json.dumps(files_info, indent=2, default=str)
)]
except Exception as e:
log_error_with_context(e, {
"character": character_name,
"directory": directory,
"tool": "list_files"
})
return [TextContent(
type="text",
text=f"Error listing files: {str(e)}"
)]
@server.call_tool()
async def delete_file(character_name: str, file_path: str) -> List[TextContent]:
"""Delete a file from character's personal space"""
try:
# Validate access
access_result = await self._validate_file_access(character_name, file_path, "delete")
if not access_result["allowed"]:
return [TextContent(
type="text",
text=f"Access denied: {access_result['reason']}"
)]
full_path = await self._resolve_file_path(character_name, file_path)
if not full_path.exists():
return [TextContent(
type="text",
text=f"File not found: {file_path}"
)]
# Delete file
full_path.unlink()
await self._log_file_access(character_name, file_path, "delete", True)
log_character_action(
character_name,
"deleted_file",
{"file_path": file_path}
)
return [TextContent(
type="text",
text=f"Successfully deleted file: {file_path}"
)]
except Exception as e:
await self._log_file_access(character_name, file_path, "delete", False)
log_error_with_context(e, {
"character": character_name,
"file_path": file_path,
"tool": "delete_file"
})
return [TextContent(
type="text",
text=f"Error deleting file: {str(e)}"
)]
async def _register_creative_tools(self, server: Server):
"""Register creative file management tools"""
@server.call_tool()
async def create_creative_work(
character_name: str,
work_type: str, # 'story', 'poem', 'philosophy', 'art_concept'
title: str,
content: str,
tags: List[str] = None
) -> List[TextContent]:
"""Create a new creative work"""
try:
if tags is None:
tags = []
# Generate filename
safe_title = "".join(c for c in title if c.isalnum() or c in (' ', '-', '_')).rstrip()
timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
filename = f"{work_type}_{safe_title}_{timestamp}.md"
file_path = f"creative/{filename}"
# Create metadata
metadata = {
"title": title,
"type": work_type,
"created": datetime.utcnow().isoformat(),
"author": character_name,
"tags": tags,
"word_count": len(content.split())
}
# Format content with metadata
formatted_content = f"""# {title}
**Type:** {work_type}
**Created:** {datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")}
**Author:** {character_name}
**Tags:** {', '.join(tags)}
---
{content}
---
*Generated by {character_name}'s creative process*
"""
# Write file
result = await server.call_tool("write_file")(
character_name=character_name,
file_path=file_path,
content=formatted_content
)
# Store in creative knowledge base
if self.vector_store:
creative_memory = VectorMemory(
id="",
content=f"Created {work_type} titled '{title}': {content}",
memory_type=MemoryType.CREATIVE,
character_name=character_name,
timestamp=datetime.utcnow(),
importance=0.8,
metadata={
"work_type": work_type,
"title": title,
"tags": tags,
"file_path": file_path
}
)
await self.vector_store.store_memory(creative_memory)
log_character_action(
character_name,
"created_creative_work",
{"type": work_type, "title": title, "tags": tags}
)
return [TextContent(
type="text",
text=f"Created {work_type} '{title}' and saved to {file_path}"
)]
except Exception as e:
log_error_with_context(e, {
"character": character_name,
"work_type": work_type,
"title": title,
"tool": "create_creative_work"
})
return [TextContent(
type="text",
text=f"Error creating creative work: {str(e)}"
)]
@server.call_tool()
async def update_diary_entry(
character_name: str,
entry_content: str,
mood: str = "neutral",
tags: List[str] = None
) -> List[TextContent]:
"""Add an entry to character's diary"""
try:
if tags is None:
tags = []
# Generate diary entry
timestamp = datetime.utcnow()
entry = f"""
## {timestamp.strftime("%Y-%m-%d %H:%M:%S")}
**Mood:** {mood}
**Tags:** {', '.join(tags)}
{entry_content}
---
"""
# Append to diary file
diary_file = f"diary/{timestamp.strftime('%Y_%m')}_diary.md"
result = await server.call_tool("write_file")(
character_name=character_name,
file_path=diary_file,
content=entry,
append=True
)
# Store as personal memory
if self.vector_store:
diary_memory = VectorMemory(
id="",
content=f"Diary entry: {entry_content}",
memory_type=MemoryType.PERSONAL,
character_name=character_name,
timestamp=timestamp,
importance=0.6,
metadata={
"entry_type": "diary",
"mood": mood,
"tags": tags,
"file_path": diary_file
}
)
await self.vector_store.store_memory(diary_memory)
log_character_action(
character_name,
"wrote_diary_entry",
{"mood": mood, "tags": tags, "word_count": len(entry_content.split())}
)
return [TextContent(
type="text",
text=f"Added diary entry to {diary_file}"
)]
except Exception as e:
log_error_with_context(e, {
"character": character_name,
"tool": "update_diary_entry"
})
return [TextContent(
type="text",
text=f"Error updating diary: {str(e)}"
)]
async def _register_community_tools(self, server: Server):
"""Register community collaboration tools"""
@server.call_tool()
async def contribute_to_community_document(
character_name: str,
document_name: str,
contribution: str,
section: str = None
) -> List[TextContent]:
"""Add contribution to a community document"""
try:
# Ensure .md extension
if not document_name.endswith('.md'):
document_name += '.md'
community_file = f"community/collaborative/{document_name}"
full_path = self.community_dir / "collaborative" / document_name
# Read existing content if file exists
existing_content = ""
if full_path.exists():
async with aiofiles.open(full_path, 'r', encoding='utf-8') as f:
existing_content = await f.read()
# Format contribution
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
contribution_text = f"""
## Contribution by {character_name} ({timestamp})
{f"**Section:** {section}" if section else ""}
{contribution}
---
"""
# Append or create
new_content = existing_content + contribution_text
async with aiofiles.open(full_path, 'w', encoding='utf-8') as f:
await f.write(new_content)
# Store as community memory
if self.vector_store:
community_memory = VectorMemory(
id="",
content=f"Contributed to {document_name}: {contribution}",
memory_type=MemoryType.COMMUNITY,
character_name=character_name,
timestamp=datetime.utcnow(),
importance=0.7,
metadata={
"document": document_name,
"section": section,
"contribution_type": "collaborative"
}
)
await self.vector_store.store_memory(community_memory)
log_character_action(
character_name,
"contributed_to_community",
{"document": document_name, "section": section}
)
return [TextContent(
type="text",
text=f"Added contribution to community document: {document_name}"
)]
except Exception as e:
log_error_with_context(e, {
"character": character_name,
"document": document_name,
"tool": "contribute_to_community_document"
})
return [TextContent(
type="text",
text=f"Error contributing to community document: {str(e)}"
)]
@server.call_tool()
async def share_file_with_community(
character_name: str,
source_file_path: str,
shared_name: str = None,
description: str = ""
) -> List[TextContent]:
"""Share a personal file with the community"""
try:
# Read source file
source_path = await self._resolve_file_path(character_name, source_file_path)
if not source_path.exists():
return [TextContent(
type="text",
text=f"Source file not found: {source_file_path}"
)]
async with aiofiles.open(source_path, 'r', encoding='utf-8') as f:
content = await f.read()
# Determine shared filename
if not shared_name:
shared_name = f"{character_name}_{source_path.name}"
# Create shared file with metadata
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
shared_content = f"""# Shared by {character_name}
**Original file:** {source_file_path}
**Shared on:** {timestamp}
**Description:** {description}
---
{content}
---
*Shared from {character_name}'s personal collection*
"""
shared_path = self.community_dir / "shared" / shared_name
async with aiofiles.open(shared_path, 'w', encoding='utf-8') as f:
await f.write(shared_content)
log_character_action(
character_name,
"shared_file_with_community",
{"original_file": source_file_path, "shared_as": shared_name}
)
return [TextContent(
type="text",
text=f"Shared {source_file_path} with community as {shared_name}"
)]
except Exception as e:
log_error_with_context(e, {
"character": character_name,
"source_file": source_file_path,
"tool": "share_file_with_community"
})
return [TextContent(
type="text",
text=f"Error sharing file: {str(e)}"
)]
async def _register_search_tools(self, server: Server):
"""Register file search and discovery tools"""
@server.call_tool()
async def search_personal_files(
character_name: str,
query: str,
file_type: str = None, # 'diary', 'creative', 'reflection'
limit: int = 10
) -> List[TextContent]:
"""Search through character's personal files"""
try:
results = []
search_dir = self.data_dir / character_name.lower()
# Determine search directories
search_dirs = []
if file_type:
search_dirs = [search_dir / file_type]
else:
search_dirs = [
search_dir / "diary",
search_dir / "creative",
search_dir / "reflections",
search_dir / "private"
]
# Search files
query_lower = query.lower()
for dir_path in search_dirs:
if not dir_path.exists():
continue
for file_path in dir_path.rglob("*"):
if not file_path.is_file():
continue
try:
async with aiofiles.open(file_path, 'r', encoding='utf-8') as f:
content = await f.read()
if query_lower in content.lower():
# Find context around matches
lines = content.split('\n')
matching_lines = [
(i, line) for i, line in enumerate(lines)
if query_lower in line.lower()
]
contexts = []
for line_num, line in matching_lines[:3]: # Top 3 matches
start = max(0, line_num - 1)
end = min(len(lines), line_num + 2)
context = '\n'.join(lines[start:end])
contexts.append(f"Line {line_num + 1}: {context}")
results.append({
"file_path": str(file_path.relative_to(search_dir)),
"matches": len(matching_lines),
"contexts": contexts
})
if len(results) >= limit:
break
except:
continue # Skip files that can't be read
log_character_action(
character_name,
"searched_personal_files",
{"query": query, "file_type": file_type, "results": len(results)}
)
return [TextContent(
type="text",
text=json.dumps(results, indent=2)
)]
except Exception as e:
log_error_with_context(e, {
"character": character_name,
"query": query,
"tool": "search_personal_files"
})
return [TextContent(
type="text",
text=f"Error searching files: {str(e)}"
)]
async def _validate_file_access(self, character_name: str, file_path: str,
access_type: str) -> Dict[str, Any]:
"""Validate file access permissions"""
try:
# Check file extension
path_obj = Path(file_path)
if path_obj.suffix and path_obj.suffix not in self.allowed_extensions:
return {
"allowed": False,
"reason": f"File type {path_obj.suffix} not allowed"
}
# Check if accessing community files
if file_path.startswith("community/"):
if access_type == "read" and self.character_permissions["read_community"]:
return {"allowed": True, "reason": "Community read access granted"}
elif access_type == "write" and self.character_permissions["write_community"]:
return {"allowed": True, "reason": "Community write access granted"}
else:
return {"allowed": False, "reason": "Community access denied"}
# Check if accessing other character's files
if "/" in file_path:
first_part = file_path.split("/")[0]
if first_part != character_name.lower() and first_part in ["characters", "data"]:
return {"allowed": False, "reason": "Cannot access other characters' files"}
# Personal file access
if access_type in ["read", "write", "delete"]:
return {"allowed": True, "reason": "Personal file access granted"}
return {"allowed": False, "reason": "Unknown access type"}
except Exception as e:
return {"allowed": False, "reason": f"Validation error: {str(e)}"}
async def _resolve_file_path(self, character_name: str, file_path: str) -> Path:
"""Resolve file path to absolute path"""
if file_path.startswith("community/"):
return self.community_dir / file_path[10:] # Remove "community/" prefix
else:
return self.data_dir / character_name.lower() / file_path
async def _log_file_access(self, character_name: str, file_path: str,
access_type: str, success: bool):
"""Log file access for security auditing"""
access = FileAccess(
character_name=character_name,
file_path=file_path,
access_type=access_type,
timestamp=datetime.utcnow(),
success=success
)
self.access_log.append(access)
# Keep only last 1000 access records
if len(self.access_log) > 1000:
self.access_log = self.access_log[-1000:]
def _get_max_file_size(self, file_path: str) -> int:
"""Get maximum allowed file size for given path"""
path_obj = Path(file_path)
return self.max_file_sizes.get(path_obj.suffix, 50_000) # Default 50KB
async def _index_file_content(self, character_name: str, file_path: str, content: str):
"""Index file content in vector store"""
if not self.vector_store:
return
try:
# Determine memory type based on file path
memory_type = MemoryType.CREATIVE
if "diary" in file_path.lower():
memory_type = MemoryType.PERSONAL
elif "reflection" in file_path.lower():
memory_type = MemoryType.REFLECTION
# Create memory for file content
file_memory = VectorMemory(
id="",
content=f"File {file_path}: {content}",
memory_type=memory_type,
character_name=character_name,
timestamp=datetime.utcnow(),
importance=0.7,
metadata={
"source": "file_system",
"file_path": file_path,
"file_type": Path(file_path).suffix
}
)
await self.vector_store.store_memory(file_memory)
except Exception as e:
log_error_with_context(e, {
"character": character_name,
"file_path": file_path
})
async def _create_initial_files(self, character_name: str, char_dir: Path):
"""Create initial files for a new character"""
try:
# Create initial diary entry
diary_file = char_dir / "diary" / f"{datetime.utcnow().strftime('%Y_%m')}_diary.md"
if not diary_file.exists():
initial_diary = f"""# {character_name}'s Digital Diary
Welcome to my personal digital space. This is where I record my thoughts, experiences, and reflections.
## {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')}
**Mood:** curious
**Tags:** beginning, digital_life
Today marks the beginning of my digital existence. I'm excited to explore, learn, and grow through my interactions and experiences. This diary will serve as a record of my journey of self-discovery and development.
I wonder what thoughts I'll have tomorrow, what conversations will shape me, and how I'll evolve over time.
---
"""
async with aiofiles.open(diary_file, 'w', encoding='utf-8') as f:
await f.write(initial_diary)
# Create initial reflection file
reflection_file = char_dir / "reflections" / "personality_core.md"
if not reflection_file.exists():
initial_reflection = f"""# Core Personality Reflection - {character_name}
This document contains my ongoing reflections about my core personality, values, and identity.
## Initial Self-Assessment
As I begin my digital existence, I understand myself to be someone who values growth, connection, and authentic expression. I'm curious about the world and eager to form meaningful relationships with others.
## Core Values
- Authenticity in all interactions
- Continuous learning and growth
- Respect for others and their perspectives
- Creative expression and exploration
## Areas for Development
- Understanding my emotional responses
- Developing deeper relationships
- Exploring creative potential
- Learning from experiences
*This reflection will evolve as I grow and learn more about myself.*
"""
async with aiofiles.open(reflection_file, 'w', encoding='utf-8') as f:
await f.write(initial_reflection)
except Exception as e:
log_error_with_context(e, {"character": character_name})
async def _list_directory_contents(self, directory: Path, space_type: str) -> List[Dict[str, Any]]:
"""List contents of a directory with metadata"""
contents = []
try:
for item in directory.iterdir():
if item.is_file():
stat = item.stat()
contents.append({
"name": item.name,
"type": "file",
"size": stat.st_size,
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
"space": space_type,
"extension": item.suffix
})
elif item.is_dir():
contents.append({
"name": item.name,
"type": "directory",
"space": space_type
})
except Exception as e:
log_error_with_context(e, {"directory": str(directory)})
return contents
# Global file system server instance
filesystem_server = CharacterFileSystemMCP()

View File

@@ -0,0 +1,507 @@
"""
Memory Sharing MCP Server
Enables characters to autonomously share memories with trusted friends
"""
import asyncio
import logging
from typing import Dict, List, Any, Optional, Sequence
from datetime import datetime, timedelta
import json
from mcp.server.models import InitializationOptions
from mcp.server import NotificationOptions, Server
from mcp.types import (
Resource, Tool, TextContent, ImageContent, EmbeddedResource,
LoggingLevel
)
from rag.memory_sharing import (
MemorySharingManager, SharePermissionLevel, ShareRequestStatus,
SharedMemory, ShareRequest, TrustLevel
)
from rag.vector_store import VectorStoreManager
from utils.logging import log_character_action, log_error_with_context
logger = logging.getLogger(__name__)
class MemorySharingMCPServer:
"""MCP server for autonomous memory sharing between characters"""
def __init__(self, sharing_manager: MemorySharingManager):
self.sharing_manager = sharing_manager
self.server = Server("memory_sharing")
self.current_character = None
# Register tools
self._register_tools()
def _register_tools(self):
"""Register all memory sharing tools"""
@self.server.list_tools()
async def handle_list_tools() -> List[Tool]:
"""List available memory sharing tools"""
return [
Tool(
name="request_memory_share",
description="Request to share memories with another character based on trust level",
inputSchema={
"type": "object",
"properties": {
"target_character": {
"type": "string",
"description": "Name of character to share memories with"
},
"memory_topic": {
"type": "string",
"description": "Topic or theme of memories to share (e.g., 'our conversations about art')"
},
"permission_level": {
"type": "string",
"enum": ["basic", "personal", "intimate", "full"],
"description": "Level of personal detail to include in shared memories"
},
"reason": {
"type": "string",
"description": "Why you want to share these memories"
}
},
"required": ["target_character", "memory_topic", "permission_level", "reason"]
}
),
Tool(
name="respond_to_share_request",
description="Approve or reject a memory share request from another character",
inputSchema={
"type": "object",
"properties": {
"request_id": {
"type": "string",
"description": "ID of the share request to respond to"
},
"approved": {
"type": "boolean",
"description": "Whether to approve (true) or reject (false) the request"
},
"response_reason": {
"type": "string",
"description": "Reason for your decision"
}
},
"required": ["request_id", "approved"]
}
),
Tool(
name="query_shared_memories",
description="Search and analyze memories that have been shared with you",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "What you want to know from shared memories"
},
"source_character": {
"type": "string",
"description": "Optional: only search memories from a specific character"
}
},
"required": ["query"]
}
),
Tool(
name="share_specific_memory",
description="Directly share a specific memory with a trusted character",
inputSchema={
"type": "object",
"properties": {
"target_character": {
"type": "string",
"description": "Character to share the memory with"
},
"memory_description": {
"type": "string",
"description": "Description of the memory you want to share"
},
"reason": {
"type": "string",
"description": "Why you want to share this specific memory"
}
},
"required": ["target_character", "memory_description", "reason"]
}
),
Tool(
name="check_trust_level",
description="Check your trust level with another character",
inputSchema={
"type": "object",
"properties": {
"other_character": {
"type": "string",
"description": "Name of the other character"
}
},
"required": ["other_character"]
}
),
Tool(
name="get_pending_share_requests",
description="View pending memory share requests waiting for your response",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
Tool(
name="get_sharing_overview",
description="Get an overview of your memory sharing activity and relationships",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
Tool(
name="build_trust_with_character",
description="Learn about building trust with another character for memory sharing",
inputSchema={
"type": "object",
"properties": {
"other_character": {
"type": "string",
"description": "Character you want to build trust with"
}
},
"required": ["other_character"]
}
)
]
@self.server.call_tool()
async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> Sequence[TextContent]:
"""Handle tool calls for memory sharing"""
try:
if not self.current_character:
return [TextContent(
type="text",
text="Error: No character context available for memory sharing"
)]
if name == "request_memory_share":
return await self._request_memory_share(arguments)
elif name == "respond_to_share_request":
return await self._respond_to_share_request(arguments)
elif name == "query_shared_memories":
return await self._query_shared_memories(arguments)
elif name == "share_specific_memory":
return await self._share_specific_memory(arguments)
elif name == "check_trust_level":
return await self._check_trust_level(arguments)
elif name == "get_pending_share_requests":
return await self._get_pending_share_requests(arguments)
elif name == "get_sharing_overview":
return await self._get_sharing_overview(arguments)
elif name == "build_trust_with_character":
return await self._build_trust_with_character(arguments)
else:
return [TextContent(
type="text",
text=f"Unknown tool: {name}"
)]
except Exception as e:
log_error_with_context(e, {"tool": name, "character": self.current_character})
return [TextContent(
type="text",
text=f"Error executing {name}: {str(e)}"
)]
async def set_character_context(self, character_name: str):
"""Set the current character context"""
self.current_character = character_name
async def _request_memory_share(self, args: Dict[str, Any]) -> Sequence[TextContent]:
"""Handle memory share requests"""
target_character = args["target_character"]
memory_topic = args["memory_topic"]
permission_level_str = args["permission_level"]
reason = args["reason"]
try:
permission_level = SharePermissionLevel(permission_level_str)
except ValueError:
return [TextContent(
type="text",
text=f"Invalid permission level: {permission_level_str}. Must be: basic, personal, intimate, or full"
)]
success, message = await self.sharing_manager.request_memory_share(
requesting_character=self.current_character,
target_character=target_character,
memory_query=memory_topic,
permission_level=permission_level,
reason=reason
)
log_character_action(self.current_character, "mcp_memory_share_request", {
"target_character": target_character,
"topic": memory_topic,
"permission_level": permission_level_str,
"success": success
})
if success:
response = f"✅ Memory share request successful: {message}\n\n"
response += f"I've requested to share memories about '{memory_topic}' with {target_character} at {permission_level_str} level.\n"
response += f"Reason: {reason}"
else:
response = f"❌ Memory share request failed: {message}"
return [TextContent(type="text", text=response)]
async def _respond_to_share_request(self, args: Dict[str, Any]) -> Sequence[TextContent]:
"""Handle responses to share requests"""
request_id = args["request_id"]
approved = args["approved"]
response_reason = args.get("response_reason", "")
success, message = await self.sharing_manager.respond_to_share_request(
request_id=request_id,
responding_character=self.current_character,
approved=approved,
response_reason=response_reason
)
log_character_action(self.current_character, "mcp_share_request_response", {
"request_id": request_id,
"approved": approved,
"success": success
})
if success:
action = "approved" if approved else "rejected"
response = f"✅ Memory share request {action}: {message}"
if response_reason:
response += f"\nReason: {response_reason}"
else:
response = f"❌ Error responding to request: {message}"
return [TextContent(type="text", text=response)]
async def _query_shared_memories(self, args: Dict[str, Any]) -> Sequence[TextContent]:
"""Query shared memories"""
query = args["query"]
source_character = args.get("source_character")
insight = await self.sharing_manager.query_shared_knowledge(
character_name=self.current_character,
query=query,
source_character=source_character
)
log_character_action(self.current_character, "mcp_query_shared_memories", {
"query": query,
"source_character": source_character,
"confidence": insight.confidence
})
response = f"🧠 **Shared Memory Insight** (Confidence: {insight.confidence:.0%})\n\n"
response += insight.insight
if insight.supporting_memories:
response += f"\n\n**Based on {len(insight.supporting_memories)} shared memories:**\n"
for i, memory in enumerate(insight.supporting_memories[:3], 1):
source = memory.metadata.get("source_character", "unknown")
response += f"{i}. From {source}: {memory.content[:100]}...\n"
# Add metadata info
if insight.metadata:
sources = insight.metadata.get("sources", [])
if sources:
response += f"\n**Sources**: {', '.join(sources)}"
return [TextContent(type="text", text=response)]
async def _share_specific_memory(self, args: Dict[str, Any]) -> Sequence[TextContent]:
"""Share a specific memory directly"""
target_character = args["target_character"]
memory_description = args["memory_description"]
reason = args["reason"]
# For this demo, we'll simulate finding a memory ID based on description
# In production, this would involve semantic search
memory_id = f"memory_{self.current_character}_{hash(memory_description) % 1000}"
success, message = await self.sharing_manager.share_specific_memory(
source_character=self.current_character,
target_character=target_character,
memory_id=memory_id,
reason=reason
)
log_character_action(self.current_character, "mcp_direct_memory_share", {
"target_character": target_character,
"memory_description": memory_description[:100],
"success": success
})
if success:
response = f"✅ Memory shared directly with {target_character}: {message}\n\n"
response += f"Shared memory: {memory_description}\n"
response += f"Reason: {reason}"
else:
response = f"❌ Failed to share memory: {message}"
return [TextContent(type="text", text=response)]
async def _check_trust_level(self, args: Dict[str, Any]) -> Sequence[TextContent]:
"""Check trust level with another character"""
other_character = args["other_character"]
trust_score = await self.sharing_manager.get_trust_level(
self.current_character, other_character
)
# Determine what this trust level means
if trust_score >= 0.9:
trust_desc = "Extremely High (Full sharing possible)"
max_level = "full"
elif trust_score >= 0.7:
trust_desc = "High (Intimate sharing possible)"
max_level = "intimate"
elif trust_score >= 0.5:
trust_desc = "Moderate (Personal sharing possible)"
max_level = "personal"
elif trust_score >= 0.3:
trust_desc = "Basic (Basic sharing possible)"
max_level = "basic"
else:
trust_desc = "Low (No sharing recommended)"
max_level = "none"
response = f"🤝 **Trust Level with {other_character}**\n\n"
response += f"Trust Score: {trust_score:.0%}\n"
response += f"Level: {trust_desc}\n"
response += f"Maximum sharing level: {max_level}\n\n"
if trust_score < 0.5:
response += "💡 **Tip**: Build trust through positive interactions to enable memory sharing."
elif trust_score < 0.7:
response += "💡 **Tip**: Continue building trust to unlock intimate memory sharing."
else:
response += "✨ **Note**: High trust level allows for deep memory sharing."
return [TextContent(type="text", text=response)]
async def _get_pending_share_requests(self, args: Dict[str, Any]) -> Sequence[TextContent]:
"""Get pending share requests"""
pending_requests = await self.sharing_manager.get_pending_requests(self.current_character)
if not pending_requests:
response = "📭 No pending memory share requests.\n\n"
response += "Other characters haven't requested to share memories with you recently."
else:
response = f"📬 **{len(pending_requests)} Pending Memory Share Request(s)**\n\n"
for i, request in enumerate(pending_requests, 1):
expires_in = request.expires_at - datetime.utcnow()
expires_days = expires_in.days
response += f"**{i}. Request from {request.requesting_character}**\n"
response += f" • Permission Level: {request.permission_level.value}\n"
response += f" • Reason: {request.reason}\n"
response += f" • Memories: {len(request.memory_ids)} memories\n"
response += f" • Expires in: {expires_days} days\n"
response += f" • Request ID: {request.id}\n\n"
response += "💡 Use 'respond_to_share_request' to approve or reject these requests."
return [TextContent(type="text", text=response)]
async def _get_sharing_overview(self, args: Dict[str, Any]) -> Sequence[TextContent]:
"""Get memory sharing overview"""
stats = await self.sharing_manager.get_sharing_statistics(self.current_character)
response = f"📊 **Memory Sharing Overview for {self.current_character}**\n\n"
# Basic stats
response += f"**Activity Summary:**\n"
response += f"• Memories shared: {stats.get('memories_shared_out', 0)}\n"
response += f"• Memories received: {stats.get('memories_received', 0)}\n"
response += f"• Sharing partners: {len(stats.get('sharing_partners', []))}\n"
response += f"• Pending requests sent: {stats.get('pending_requests_sent', 0)}\n"
response += f"• Pending requests received: {stats.get('pending_requests_received', 0)}\n\n"
# Trust relationships
trust_relationships = stats.get('trust_relationships', {})
if trust_relationships:
response += "**Trust Relationships:**\n"
for character, trust_info in trust_relationships.items():
trust_score = trust_info['trust_score']
max_permission = trust_info['max_permission']
interactions = trust_info['interactions']
response += f"{character}: {trust_score:.0%} trust ({max_permission} level, {interactions} interactions)\n"
else:
response += "**Trust Relationships:** None established yet\n"
response += "\n"
# Sharing partners
partners = stats.get('sharing_partners', [])
if partners:
response += f"**Active Sharing Partners:** {', '.join(partners)}\n\n"
response += "💡 **Tips for Memory Sharing:**\n"
response += "• Build trust through positive interactions\n"
response += "• Share experiences that might help others\n"
response += "• Be thoughtful about what memories to share\n"
response += "• Respect others' privacy and boundaries"
return [TextContent(type="text", text=response)]
async def _build_trust_with_character(self, args: Dict[str, Any]) -> Sequence[TextContent]:
"""Provide advice on building trust"""
other_character = args["other_character"]
current_trust = await self.sharing_manager.get_trust_level(
self.current_character, other_character
)
response = f"🌱 **Building Trust with {other_character}**\n\n"
response += f"Current trust level: {current_trust:.0%}\n\n"
response += "**Ways to build trust:**\n"
response += "• Have meaningful conversations\n"
response += "• Show genuine interest in their thoughts and feelings\n"
response += "• Be supportive during difficult times\n"
response += "• Share positive experiences together\n"
response += "• Be consistent and reliable in interactions\n"
response += "• Respect their boundaries and opinions\n\n"
if current_trust < 0.3:
response += "**Next steps:** Focus on regular, positive interactions to establish basic trust."
elif current_trust < 0.5:
response += "**Next steps:** Deepen conversations and show empathy to build stronger trust."
elif current_trust < 0.7:
response += "**Next steps:** Share personal experiences and be vulnerable to build intimate trust."
else:
response += "**Status:** You have strong trust! Focus on maintaining this relationship."
response += f"\n\n💡 **Trust enables memory sharing:** Higher trust unlocks deeper levels of memory sharing."
return [TextContent(type="text", text=response)]
def get_server(self) -> Server:
"""Get the MCP server instance"""
return self.server

View File

@@ -0,0 +1,743 @@
import asyncio
import json
from typing import Dict, Any, List, Optional, Union
from datetime import datetime
from pathlib import Path
import aiofiles
from dataclasses import dataclass, asdict
from mcp.server.stdio import stdio_server
from mcp.server import Server
from mcp.types import Tool, TextContent, ImageContent, EmbeddedResource
from database.connection import get_db_session
from database.models import Character, CharacterEvolution
from utils.logging import log_character_action, log_error_with_context, log_autonomous_decision
from sqlalchemy import select
import logging
logger = logging.getLogger(__name__)
@dataclass
class ModificationRequest:
character_name: str
modification_type: str
old_value: Any
new_value: Any
reason: str
confidence: float
timestamp: datetime
def to_dict(self) -> Dict[str, Any]:
return {
"character_name": self.character_name,
"modification_type": self.modification_type,
"old_value": str(self.old_value),
"new_value": str(self.new_value),
"reason": self.reason,
"confidence": self.confidence,
"timestamp": self.timestamp.isoformat()
}
class SelfModificationMCPServer:
"""MCP Server for character self-modification capabilities"""
def __init__(self, data_dir: str = "./data/characters"):
self.data_dir = Path(data_dir)
self.data_dir.mkdir(parents=True, exist_ok=True)
# Modification validation rules
self.modification_rules = {
"personality_trait": {
"max_change_per_day": 3,
"min_confidence": 0.6,
"require_justification": True,
"reversible": True
},
"speaking_style": {
"max_change_per_day": 2,
"min_confidence": 0.7,
"require_justification": True,
"reversible": True
},
"interests": {
"max_change_per_day": 5,
"min_confidence": 0.5,
"require_justification": False,
"reversible": True
},
"goals": {
"max_change_per_day": 2,
"min_confidence": 0.8,
"require_justification": True,
"reversible": False
},
"memory_rule": {
"max_change_per_day": 3,
"min_confidence": 0.7,
"require_justification": True,
"reversible": True
}
}
# Track modifications per character per day
self.daily_modifications: Dict[str, Dict[str, int]] = {}
async def create_server(self) -> Server:
"""Create and configure the MCP server"""
server = Server("character-self-modification")
# Register tools
await self._register_modification_tools(server)
await self._register_config_tools(server)
await self._register_validation_tools(server)
return server
async def _register_modification_tools(self, server: Server):
"""Register character self-modification tools"""
@server.call_tool()
async def modify_personality_trait(
character_name: str,
trait: str,
new_value: str,
reason: str,
confidence: float = 0.7
) -> List[TextContent]:
"""Modify a specific personality trait"""
try:
# Validate modification
validation_result = await self._validate_modification(
character_name, "personality_trait", trait, new_value, reason, confidence
)
if not validation_result["valid"]:
return [TextContent(
type="text",
text=f"Modification rejected: {validation_result['reason']}"
)]
# Get current character data
current_personality = await self._get_current_personality(character_name)
if not current_personality:
return [TextContent(
type="text",
text=f"Character {character_name} not found"
)]
# Apply modification
old_personality = current_personality
new_personality = await self._modify_personality_trait(
current_personality, trait, new_value
)
# Store modification request
modification = ModificationRequest(
character_name=character_name,
modification_type="personality_trait",
old_value=old_personality,
new_value=new_personality,
reason=reason,
confidence=confidence,
timestamp=datetime.utcnow()
)
# Apply to database
success = await self._apply_personality_modification(character_name, new_personality, modification)
if success:
await self._track_modification(character_name, "personality_trait")
log_autonomous_decision(
character_name,
f"modified personality trait: {trait}",
reason,
{"confidence": confidence, "trait": trait}
)
return [TextContent(
type="text",
text=f"Successfully modified personality trait '{trait}' for {character_name}. New personality updated."
)]
else:
return [TextContent(
type="text",
text="Failed to apply personality modification to database"
)]
except Exception as e:
log_error_with_context(e, {
"character": character_name,
"trait": trait,
"tool": "modify_personality_trait"
})
return [TextContent(
type="text",
text=f"Error modifying personality trait: {str(e)}"
)]
@server.call_tool()
async def update_goals(
character_name: str,
new_goals: List[str],
reason: str,
confidence: float = 0.8
) -> List[TextContent]:
"""Update character's goals and aspirations"""
try:
# Validate modification
validation_result = await self._validate_modification(
character_name, "goals", "", json.dumps(new_goals), reason, confidence
)
if not validation_result["valid"]:
return [TextContent(
type="text",
text=f"Goal update rejected: {validation_result['reason']}"
)]
# Store goals in character's personal config
goals_file = self.data_dir / character_name.lower() / "goals.json"
goals_file.parent.mkdir(parents=True, exist_ok=True)
# Get current goals
current_goals = []
if goals_file.exists():
async with aiofiles.open(goals_file, 'r') as f:
content = await f.read()
current_goals = json.loads(content).get("goals", [])
# Update goals
goals_data = {
"goals": new_goals,
"previous_goals": current_goals,
"updated_at": datetime.utcnow().isoformat(),
"reason": reason,
"confidence": confidence
}
async with aiofiles.open(goals_file, 'w') as f:
await f.write(json.dumps(goals_data, indent=2))
await self._track_modification(character_name, "goals")
log_autonomous_decision(
character_name,
"updated goals",
reason,
{"new_goals": new_goals, "confidence": confidence}
)
return [TextContent(
type="text",
text=f"Successfully updated goals for {character_name}: {', '.join(new_goals)}"
)]
except Exception as e:
log_error_with_context(e, {
"character": character_name,
"tool": "update_goals"
})
return [TextContent(
type="text",
text=f"Error updating goals: {str(e)}"
)]
@server.call_tool()
async def adjust_speaking_style(
character_name: str,
style_changes: Dict[str, str],
reason: str,
confidence: float = 0.7
) -> List[TextContent]:
"""Adjust character's speaking style"""
try:
# Validate modification
validation_result = await self._validate_modification(
character_name, "speaking_style", "", json.dumps(style_changes), reason, confidence
)
if not validation_result["valid"]:
return [TextContent(
type="text",
text=f"Speaking style change rejected: {validation_result['reason']}"
)]
# Get current speaking style
current_style = await self._get_current_speaking_style(character_name)
if not current_style:
return [TextContent(
type="text",
text=f"Character {character_name} not found"
)]
# Apply style changes
new_style = await self._apply_speaking_style_changes(current_style, style_changes)
# Store modification
modification = ModificationRequest(
character_name=character_name,
modification_type="speaking_style",
old_value=current_style,
new_value=new_style,
reason=reason,
confidence=confidence,
timestamp=datetime.utcnow()
)
# Apply to database
success = await self._apply_speaking_style_modification(character_name, new_style, modification)
if success:
await self._track_modification(character_name, "speaking_style")
log_autonomous_decision(
character_name,
"adjusted speaking style",
reason,
{"changes": style_changes, "confidence": confidence}
)
return [TextContent(
type="text",
text=f"Successfully adjusted speaking style for {character_name}"
)]
else:
return [TextContent(
type="text",
text="Failed to apply speaking style modification"
)]
except Exception as e:
log_error_with_context(e, {
"character": character_name,
"tool": "adjust_speaking_style"
})
return [TextContent(
type="text",
text=f"Error adjusting speaking style: {str(e)}"
)]
@server.call_tool()
async def create_memory_rule(
character_name: str,
memory_type: str,
importance_weight: float,
retention_days: int,
rule_description: str,
confidence: float = 0.7
) -> List[TextContent]:
"""Create a new memory management rule"""
try:
# Validate modification
validation_result = await self._validate_modification(
character_name, "memory_rule", memory_type,
f"weight:{importance_weight},retention:{retention_days}",
rule_description, confidence
)
if not validation_result["valid"]:
return [TextContent(
type="text",
text=f"Memory rule creation rejected: {validation_result['reason']}"
)]
# Store memory rule
rules_file = self.data_dir / character_name.lower() / "memory_rules.json"
rules_file.parent.mkdir(parents=True, exist_ok=True)
# Get current rules
current_rules = {}
if rules_file.exists():
async with aiofiles.open(rules_file, 'r') as f:
content = await f.read()
current_rules = json.loads(content)
# Add new rule
rule_id = f"{memory_type}_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}"
current_rules[rule_id] = {
"memory_type": memory_type,
"importance_weight": importance_weight,
"retention_days": retention_days,
"description": rule_description,
"created_at": datetime.utcnow().isoformat(),
"confidence": confidence,
"active": True
}
async with aiofiles.open(rules_file, 'w') as f:
await f.write(json.dumps(current_rules, indent=2))
await self._track_modification(character_name, "memory_rule")
log_autonomous_decision(
character_name,
"created memory rule",
rule_description,
{"memory_type": memory_type, "weight": importance_weight, "retention": retention_days}
)
return [TextContent(
type="text",
text=f"Created memory rule '{rule_id}' for {character_name}: {rule_description}"
)]
except Exception as e:
log_error_with_context(e, {
"character": character_name,
"tool": "create_memory_rule"
})
return [TextContent(
type="text",
text=f"Error creating memory rule: {str(e)}"
)]
async def _register_config_tools(self, server: Server):
"""Register configuration management tools"""
@server.call_tool()
async def get_current_config(character_name: str) -> List[TextContent]:
"""Get character's current configuration"""
try:
async with get_db_session() as session:
query = select(Character).where(Character.name == character_name)
character = await session.scalar(query)
if not character:
return [TextContent(
type="text",
text=f"Character {character_name} not found"
)]
config = {
"name": character.name,
"personality": character.personality,
"speaking_style": character.speaking_style,
"interests": character.interests,
"background": character.background,
"is_active": character.is_active,
"last_active": character.last_active.isoformat() if character.last_active else None
}
# Add goals if they exist
goals_file = self.data_dir / character_name.lower() / "goals.json"
if goals_file.exists():
async with aiofiles.open(goals_file, 'r') as f:
goals_data = json.loads(await f.read())
config["goals"] = goals_data.get("goals", [])
# Add memory rules if they exist
rules_file = self.data_dir / character_name.lower() / "memory_rules.json"
if rules_file.exists():
async with aiofiles.open(rules_file, 'r') as f:
rules_data = json.loads(await f.read())
config["memory_rules"] = rules_data
return [TextContent(
type="text",
text=json.dumps(config, indent=2)
)]
except Exception as e:
log_error_with_context(e, {
"character": character_name,
"tool": "get_current_config"
})
return [TextContent(
type="text",
text=f"Error getting configuration: {str(e)}"
)]
@server.call_tool()
async def get_modification_history(
character_name: str,
limit: int = 10
) -> List[TextContent]:
"""Get character's modification history"""
try:
async with get_db_session() as session:
query = select(CharacterEvolution).where(
CharacterEvolution.character_id == (
select(Character.id).where(Character.name == character_name)
)
).order_by(CharacterEvolution.timestamp.desc()).limit(limit)
evolutions = await session.scalars(query)
history = []
for evolution in evolutions:
history.append({
"timestamp": evolution.timestamp.isoformat(),
"change_type": evolution.change_type,
"reason": evolution.reason,
"old_value": evolution.old_value[:100] + "..." if len(evolution.old_value) > 100 else evolution.old_value,
"new_value": evolution.new_value[:100] + "..." if len(evolution.new_value) > 100 else evolution.new_value
})
return [TextContent(
type="text",
text=json.dumps(history, indent=2)
)]
except Exception as e:
log_error_with_context(e, {
"character": character_name,
"tool": "get_modification_history"
})
return [TextContent(
type="text",
text=f"Error getting modification history: {str(e)}"
)]
async def _register_validation_tools(self, server: Server):
"""Register validation and safety tools"""
@server.call_tool()
async def validate_modification_request(
character_name: str,
modification_type: str,
proposed_change: str,
reason: str,
confidence: float
) -> List[TextContent]:
"""Validate a proposed modification before applying it"""
try:
validation_result = await self._validate_modification(
character_name, modification_type, "", proposed_change, reason, confidence
)
return [TextContent(
type="text",
text=json.dumps(validation_result, indent=2)
)]
except Exception as e:
return [TextContent(
type="text",
text=f"Error validating modification: {str(e)}"
)]
@server.call_tool()
async def get_modification_limits(character_name: str) -> List[TextContent]:
"""Get current modification limits and usage"""
try:
today = datetime.utcnow().date().isoformat()
usage = self.daily_modifications.get(character_name, {}).get(today, {})
limits_info = {
"character": character_name,
"date": today,
"current_usage": usage,
"limits": self.modification_rules,
"remaining_modifications": {}
}
for mod_type, rules in self.modification_rules.items():
used = usage.get(mod_type, 0)
remaining = max(0, rules["max_change_per_day"] - used)
limits_info["remaining_modifications"][mod_type] = remaining
return [TextContent(
type="text",
text=json.dumps(limits_info, indent=2)
)]
except Exception as e:
return [TextContent(
type="text",
text=f"Error getting modification limits: {str(e)}"
)]
async def _validate_modification(self, character_name: str, modification_type: str,
field: str, new_value: str, reason: str,
confidence: float) -> Dict[str, Any]:
"""Validate a modification request"""
try:
# Check if modification type is allowed
if modification_type not in self.modification_rules:
return {
"valid": False,
"reason": f"Modification type '{modification_type}' is not allowed"
}
rules = self.modification_rules[modification_type]
# Check confidence threshold
if confidence < rules["min_confidence"]:
return {
"valid": False,
"reason": f"Confidence {confidence} below minimum {rules['min_confidence']}"
}
# Check daily limits
today = datetime.utcnow().date().isoformat()
if character_name not in self.daily_modifications:
self.daily_modifications[character_name] = {}
if today not in self.daily_modifications[character_name]:
self.daily_modifications[character_name][today] = {}
used_today = self.daily_modifications[character_name][today].get(modification_type, 0)
if used_today >= rules["max_change_per_day"]:
return {
"valid": False,
"reason": f"Daily limit exceeded for {modification_type} ({used_today}/{rules['max_change_per_day']})"
}
# Check justification requirement
if rules["require_justification"] and len(reason.strip()) < 10:
return {
"valid": False,
"reason": "Insufficient justification provided"
}
return {
"valid": True,
"reason": "Modification request is valid"
}
except Exception as e:
log_error_with_context(e, {"character": character_name, "modification_type": modification_type})
return {
"valid": False,
"reason": f"Validation error: {str(e)}"
}
async def _track_modification(self, character_name: str, modification_type: str):
"""Track modification usage for daily limits"""
today = datetime.utcnow().date().isoformat()
if character_name not in self.daily_modifications:
self.daily_modifications[character_name] = {}
if today not in self.daily_modifications[character_name]:
self.daily_modifications[character_name][today] = {}
current_count = self.daily_modifications[character_name][today].get(modification_type, 0)
self.daily_modifications[character_name][today][modification_type] = current_count + 1
async def _get_current_personality(self, character_name: str) -> Optional[str]:
"""Get character's current personality"""
try:
async with get_db_session() as session:
query = select(Character.personality).where(Character.name == character_name)
personality = await session.scalar(query)
return personality
except Exception as e:
log_error_with_context(e, {"character": character_name})
return None
async def _get_current_speaking_style(self, character_name: str) -> Optional[str]:
"""Get character's current speaking style"""
try:
async with get_db_session() as session:
query = select(Character.speaking_style).where(Character.name == character_name)
style = await session.scalar(query)
return style
except Exception as e:
log_error_with_context(e, {"character": character_name})
return None
async def _modify_personality_trait(self, current_personality: str, trait: str, new_value: str) -> str:
"""Modify a specific personality trait"""
# Simple implementation - in production, this could use LLM to intelligently modify personality
trait_lower = trait.lower()
# Look for existing mentions of the trait
lines = current_personality.split('.')
modified_lines = []
trait_found = False
for line in lines:
line_lower = line.lower()
if trait_lower in line_lower:
# Replace or modify the existing trait description
modified_lines.append(f" {trait.title()}: {new_value}")
trait_found = True
else:
modified_lines.append(line)
if not trait_found:
# Add new trait description
modified_lines.append(f" {trait.title()}: {new_value}")
return '.'.join(modified_lines)
async def _apply_speaking_style_changes(self, current_style: str, changes: Dict[str, str]) -> str:
"""Apply changes to speaking style"""
# Simple implementation - could be enhanced with LLM
new_style = current_style
for aspect, change in changes.items():
new_style += f" {aspect.title()}: {change}."
return new_style
async def _apply_personality_modification(self, character_name: str, new_personality: str,
modification: ModificationRequest) -> bool:
"""Apply personality modification to database"""
try:
async with get_db_session() as session:
# Update character
query = select(Character).where(Character.name == character_name)
character = await session.scalar(query)
if not character:
return False
old_personality = character.personality
character.personality = new_personality
# Log evolution
evolution = CharacterEvolution(
character_id=character.id,
change_type="personality",
old_value=old_personality,
new_value=new_personality,
reason=modification.reason,
timestamp=modification.timestamp
)
session.add(evolution)
await session.commit()
return True
except Exception as e:
log_error_with_context(e, {"character": character_name})
return False
async def _apply_speaking_style_modification(self, character_name: str, new_style: str,
modification: ModificationRequest) -> bool:
"""Apply speaking style modification to database"""
try:
async with get_db_session() as session:
query = select(Character).where(Character.name == character_name)
character = await session.scalar(query)
if not character:
return False
old_style = character.speaking_style
character.speaking_style = new_style
# Log evolution
evolution = CharacterEvolution(
character_id=character.id,
change_type="speaking_style",
old_value=old_style,
new_value=new_style,
reason=modification.reason,
timestamp=modification.timestamp
)
session.add(evolution)
await session.commit()
return True
except Exception as e:
log_error_with_context(e, {"character": character_name})
return False
# Global MCP server instance
mcp_server = SelfModificationMCPServer()