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:
0
src/mcp_servers/__init__.py
Normal file
0
src/mcp_servers/__init__.py
Normal file
1280
src/mcp_servers/calendar_server.py
Normal file
1280
src/mcp_servers/calendar_server.py
Normal file
File diff suppressed because it is too large
Load Diff
497
src/mcp_servers/creative_projects_server.py
Normal file
497
src/mcp_servers/creative_projects_server.py
Normal 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
|
||||
918
src/mcp_servers/file_system_server.py
Normal file
918
src/mcp_servers/file_system_server.py
Normal 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()
|
||||
507
src/mcp_servers/memory_sharing_server.py
Normal file
507
src/mcp_servers/memory_sharing_server.py
Normal 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
|
||||
743
src/mcp_servers/self_modification_server.py
Normal file
743
src/mcp_servers/self_modification_server.py
Normal 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()
|
||||
Reference in New Issue
Block a user