From 0551ddd30c5308f8d2ea65b11fdad749c7212269 Mon Sep 17 00:00:00 2001 From: Daniel O'Connell Date: Tue, 3 Jun 2025 13:45:38 +0200 Subject: [PATCH] shorter tool descriptions + time tool --- src/memory/api/MCP/tools.py | 581 +++++++----------------------------- 1 file changed, 101 insertions(+), 480 deletions(-) diff --git a/src/memory/api/MCP/tools.py b/src/memory/api/MCP/tools.py index 15951bb..891f77d 100644 --- a/src/memory/api/MCP/tools.py +++ b/src/memory/api/MCP/tools.py @@ -7,6 +7,7 @@ import pathlib from datetime import datetime, timezone from mcp.server.fastmcp import FastMCP +from pydantic import BaseModel from sqlalchemy import Text, func from sqlalchemy import cast as sql_cast from sqlalchemy.dialects.postgresql import ARRAY @@ -70,22 +71,17 @@ def filter_source_ids( return source_ids +@mcp.tool() +async def get_current_time() -> dict: + """Get the current time in UTC.""" + return {"current_time": datetime.now(timezone.utc).isoformat()} + + @mcp.tool() async def get_all_tags() -> list[str]: """ Get all unique tags used across the entire knowledge base. - - Purpose: - This tool retrieves all tags that have been used in the system, both from - AI observations (created with 'observe') and other content. Use it to - understand the tag taxonomy, ensure consistency, or discover related topics. - - Returns: - Sorted list of all unique tags in the system. Tags follow patterns like: - - Topics: "machine-learning", "functional-programming" - - Projects: "project:website-redesign" - - Contexts: "context:work", "context:late-night" - - Domains: "domain:finance" + Returns sorted list of tags from both observations and content. """ with make_session() as session: tags_query = session.query(func.unnest(SourceItem.tags)).distinct() @@ -96,20 +92,7 @@ async def get_all_tags() -> list[str]: async def get_all_subjects() -> list[str]: """ Get all unique subjects from observations about the user. - - Purpose: - This tool retrieves all subject identifiers that have been used in - observations (created with 'observe'). Subjects are the consistent - identifiers for what observations are about. Use this to understand - what aspects of the user have been tracked and ensure consistency. - - Returns: - Sorted list of all unique subjects. Common patterns include: - - "programming_style", "programming_philosophy" - - "work_habits", "work_schedule" - - "ai_beliefs", "ai_safety_beliefs" - - "learning_preferences" - - "communication_style" + Returns sorted list of subject identifiers used in observations. """ with make_session() as session: return sorted( @@ -120,23 +103,8 @@ async def get_all_subjects() -> list[str]: @mcp.tool() async def get_all_observation_types() -> list[str]: """ - Get all unique observation types that have been used. - - Purpose: - This tool retrieves the distinct observation types that have been recorded - in the system. While the standard types are predefined (belief, preference, - behavior, contradiction, general), this shows what's actually been used. - Helpful for understanding the distribution of observation types. - - Standard types: - - "belief": Opinions or beliefs the user holds - - "preference": Things they prefer or favor - - "behavior": Patterns in how they act or work - - "contradiction": Noted inconsistencies - - "general": Observations that don't fit other categories - - Returns: - List of observation types that have actually been used in the system. + Get all observation types that have been used. + Standard types are belief, preference, behavior, contradiction, general, but there can be more. """ with make_session() as session: return sorted( @@ -157,110 +125,19 @@ async def search_knowledge_base( limit: int = 10, ) -> list[dict]: """ - Search through the user's stored knowledge and content. - - Purpose: - This tool searches the user's personal knowledge base - a collection of - their saved content including emails, documents, blog posts, books, and - more. Use this alongside 'search_observations' to build a complete picture: - - search_knowledge_base: Finds user's actual content and information - - search_observations: Finds AI-generated insights about the user - Together they enable deeply personalized, context-aware assistance. - - When to use: - - User asks about something they've read/written/received - - You need to find specific content the user has saved - - User references a document, email, or article - - To provide quotes or information from user's sources - - To understand context from user's past communications - - When user says "that article about..." or similar references - - How it works: - Uses hybrid search combining semantic understanding with keyword matching. - This means it finds content based on meaning AND specific terms, giving - you the best of both approaches. Results are ranked by relevance. + Search user's stored content including emails, documents, articles, books. + Use to find specific information the user has saved or received. + Combine with search_observations for complete user context. Args: - query: Natural language search query. Be descriptive about what you're - looking for. The search understands meaning but also values exact terms. - Examples: - - "email about project deadline from last week" - - "functional programming articles comparing Haskell and Scala" - - "that blog post about AI safety and alignment" - - "recipe for chocolate cake Sarah sent me" - Pro tip: Include both concepts and specific keywords for best results. + query: Natural language search query - be descriptive about what you're looking for + previews: Include actual content in results - when false only a snippet is returned + modalities: Filter by type: email, blog, book, forum, photo, comic, webpage (empty = all) + tags: Filter by tags - content must have at least one matching tag + limit: Max results (1-100) - previews: Whether to include content snippets in results. - - True: Returns preview text and image previews (useful for quick scanning) - - False: Returns just metadata (faster, less data) - Default is False. - - modalities: Types of content to search. Leave empty to search all. - Available types: - - 'email': Email messages - - 'blog': Blog posts and articles - - 'book': Book sections and ebooks - - 'forum': Forum posts (e.g., LessWrong, Reddit) - - 'observation': AI observations (use search_observations instead) - - 'photo': Images with extracted text - - 'comic': Comics and graphic content - - 'webpage': General web pages - Examples: - - ["email"] - only emails - - ["blog", "forum"] - articles and forum posts - - [] - search everything - - limit: Maximum results to return (1-100). Default 10. - Increase for comprehensive searches, decrease for quick lookups. - - Returns: - List of search results ranked by relevance, each containing: - - id: Unique identifier for the source item - - score: Relevance score (0-1, higher is better) - - chunks: Matching content segments with metadata - - content: Full details including: - - For emails: sender, recipient, subject, date - - For blogs: author, title, url, publish date - - For books: title, author, chapter info - - Type-specific fields for each modality - - filename: Path to file if content is stored on disk - - Examples: - # Find specific email - results = await search_knowledge_base( - query="Sarah deadline project proposal next Friday", - modalities=["email"], - previews=True, - limit=5 - ) - - # Search for technical articles - results = await search_knowledge_base( - query="functional programming monads category theory", - modalities=["blog", "book"], - limit=20 - ) - - # Find everything about a topic - results = await search_knowledge_base( - query="machine learning deployment kubernetes docker", - previews=True - ) - - # Quick lookup of a remembered document - results = await search_knowledge_base( - query="tax forms 2023 accountant recommendations", - modalities=["email"], - limit=3 - ) - - Best practices: - - Include context in queries ("email from Sarah" vs just "Sarah") - - Use modalities to filter when you know the content type - - Enable previews when you need to verify content before using - - Combine with search_observations for complete context - - Higher scores (>0.7) indicate strong matches - - If no results, try broader queries or different phrasing + Returns: List of search results with id, score, chunks, content, filename + Higher scores (>0.7) indicate strong matches. """ logger.info(f"MCP search for: {query}") @@ -282,175 +159,70 @@ async def search_knowledge_base( ), ) - # Convert SearchResult objects to dictionaries for MCP return [result.model_dump() for result in results] +class RawObservation(BaseModel): + subject: str + content: str + observation_type: str = "general" + confidences: dict[str, float] = {} + evidence: dict | None = None + tags: list[str] = [] + + @mcp.tool() async def observe( - content: str, - subject: str, - observation_type: str = "general", - confidences: dict[str, float] = {}, - evidence: dict | None = None, - tags: list[str] | None = None, + observations: list[RawObservation], session_id: str | None = None, agent_model: str = "unknown", ) -> dict: """ - Record an observation about the user to build long-term understanding. + Record observations about the user for long-term understanding. + Use proactively when user expresses preferences, behaviors, beliefs, or contradictions. + Be specific and detailed - observations should make sense months later. - Purpose: - This tool is part of a memory system designed to help AI agents build a - deep, persistent understanding of users over time. Use it to record any - notable information about the user's preferences, beliefs, behaviors, or - characteristics. These observations accumulate to create a comprehensive - model of the user that improves future interactions. - - Quick Reference: - # Most common patterns: - observe(content="User prefers X over Y because...", subject="preferences", observation_type="preference") - observe(content="User always/often does X when Y", subject="work_habits", observation_type="behavior") - observe(content="User believes/thinks X about Y", subject="beliefs_on_topic", observation_type="belief") - observe(content="User said X but previously said Y", subject="topic", observation_type="contradiction") - - When to use: - - User expresses a preference or opinion - - You notice a behavioral pattern - - User reveals information about their work/life/interests - - You spot a contradiction with previous statements - - Any insight that would help understand the user better in future - - Important: Be an active observer. Don't wait to be asked - proactively record - observations throughout conversations to build understanding. + RawObservation fields: + content (required): Detailed observation text explaining what you observed + subject (required): Consistent identifier like "programming_style", "work_habits" + observation_type: belief, preference, behavior, contradiction, general + confidences: Dict of scores (0.0-1.0), e.g. {"observation_accuracy": 0.9} + evidence: Context dict with extra context, e.g. "quote" (exact words) and "context" (situation) + tags: List of categorization tags for organization Args: - content: The observation itself. Be specific and detailed. Write complete - thoughts that will make sense when read months later without context. - Bad: "Likes FP" - Good: "User strongly prefers functional programming paradigms, especially - pure functions and immutability, considering them more maintainable" - - subject: A consistent identifier for what this observation is about. Use - snake_case and be consistent across observations to enable tracking. - Examples: - - "programming_style" (not "coding" or "development") - - "work_habits" (not "productivity" or "work_patterns") - - "ai_safety_beliefs" (not "AI" or "artificial_intelligence") - - observation_type: Categorize the observation: - - "belief": An opinion or belief the user holds - - "preference": Something they prefer or favor - - "behavior": A pattern in how they act or work - - "contradiction": An inconsistency with previous observations - - "general": Doesn't fit other categories - - confidences: How certain you are (0.0-1.0) in a given aspect of the observation: - - 1.0: User explicitly stated this - - 0.9: Strongly implied or demonstrated repeatedly - - 0.8: Inferred with high confidence (default) - - 0.7: Probable but with some uncertainty - - 0.6 or below: Speculative, use sparingly - Provided as a mapping of : - Examples: - - {"observation_accuracy": 0.95} - - {"observation_accuracy": 0.8, "interpretation": 0.5} - - evidence: Supporting context as a dict. Include relevant details: - - "quote": Exact words from the user - - "context": What prompted this observation - - "timestamp": When this was observed - - "related_to": Connection to other topics - Example: { - "quote": "I always refactor to pure functions", - "context": "Discussing code review practices" - } - - tags: Categorization labels. Use lowercase with hyphens. Common patterns: - - Topics: "machine-learning", "web-development", "philosophy" - - Projects: "project:website-redesign", "project:thesis" - - Contexts: "context:work", "context:personal", "context:late-night" - - Domains: "domain:finance", "domain:healthcare" - - session_id: UUID string to group observations from the same conversation. - Generate one UUID per conversation and reuse it for all observations - in that conversation. Format: "550e8400-e29b-41d4-a716-446655440000" - - agent_model: Which AI model made this observation (e.g., "claude-3-opus", - "gpt-4", "claude-3.5-sonnet"). Helps track observation quality. - - Returns: - Dict with created observation details: - - id: Unique identifier for reference - - created_at: Timestamp of creation - - subject: The subject as stored - - observation_type: The type as stored - - confidence: The confidence score - - tags: List of applied tags - - Examples: - # After user mentions their coding philosophy - await observe( - content="User believes strongly in functional programming principles, " - "particularly avoiding mutable state which they call 'the root " - "of all evil'. They prioritize code purity over performance.", - subject="programming_philosophy", - observation_type="belief", - confidences={"observation_accuracy": 0.95}, - evidence={ - "quote": "State is the root of all evil in programming", - "context": "Discussing why they chose Haskell for their project" - }, - tags=["programming", "functional-programming", "philosophy"], - session_id="550e8400-e29b-41d4-a716-446655440000", - agent_model="claude-3-opus" - ) - - # Noticing a work pattern - await observe( - content="User frequently works on complex problems late at night, " - "typically between 11pm and 3am, claiming better focus", - subject="work_schedule", - observation_type="behavior", - confidences={"observation_accuracy": 0.85}, - evidence={ - "context": "Mentioned across multiple conversations over 2 weeks" - }, - tags=["behavior", "work-habits", "productivity", "context:late-night"], - agent_model="claude-3-opus" - ) - - # Recording a contradiction - await observe( - content="User now advocates for microservices architecture, but " - "previously argued strongly for monoliths in similar contexts", - subject="architecture_preferences", - observation_type="contradiction", - confidences={"observation_accuracy": 0.9}, - evidence={ - "quote": "Microservices are definitely the way to go", - "context": "Designing a new system similar to one from 3 months ago" - }, - tags=["architecture", "contradiction", "software-design"], - agent_model="gpt-4" - ) + observations: List of RawObservation objects + session_id: UUID to group observations from same conversation + agent_model: AI model making observations (for quality tracking) """ - task = celery_app.send_task( - SYNC_OBSERVATION, - queue="notes", - kwargs={ - "subject": subject, - "content": content, - "observation_type": observation_type, - "confidences": confidences, - "evidence": evidence, - "tags": tags, - "session_id": session_id, - "agent_model": agent_model, - }, - ) + tasks = [ + ( + observation, + celery_app.send_task( + SYNC_OBSERVATION, + queue="notes", + kwargs={ + "subject": observation.subject, + "content": observation.content, + "observation_type": observation.observation_type, + "confidences": observation.confidences, + "evidence": observation.evidence, + "tags": observation.tags, + "session_id": session_id, + "agent_model": agent_model, + }, + ), + ) + for observation in observations + ] + + def short_content(obs: RawObservation) -> str: + if len(obs.content) > 50: + return obs.content[:47] + "..." + return obs.content + return { - "task_id": task.id, + "task_ids": {short_content(obs): task.id for obs, task in tasks}, "status": "queued", } @@ -465,107 +237,20 @@ async def search_observations( limit: int = 10, ) -> list[dict]: """ - Search through observations to understand the user better. - - Purpose: - This tool searches through all observations recorded about the user using - the 'observe' tool. Use it to recall past insights, check for patterns, - find contradictions, or understand the user's preferences before responding. - The more you use this tool, the more personalized and insightful your - responses can be. - - When to use: - - Before answering questions where user preferences might matter - - When the user references something from the past - - To check if current behavior aligns with past patterns - - To find related observations on a topic - - To build context about the user's expertise or interests - - Whenever personalization would improve your response + Search recorded observations about the user. + Use before responding to understand user preferences, patterns, and past insights. + Search by meaning - the query matches both content and context. Args: - query: Natural language description of what you're looking for. The search - matches both meaning and specific terms in observation content. - Examples: - - "programming preferences and coding style" - - "opinions about artificial intelligence and AI safety" - - "work habits productivity patterns when does user work best" - - "previous projects the user has worked on" - Pro tip: Use natural language but include key terms you expect to find. + query: Natural language search query describing what you're looking for + subject: Filter by exact subject identifier (empty = search all subjects) + tags: Filter by tags (must have at least one matching tag) + observation_types: Filter by: belief, preference, behavior, contradiction, general + min_confidences: Minimum confidence thresholds, e.g. {"observation_accuracy": 0.8} + limit: Max results (1-100) - subject: Filter by exact subject identifier. Must match subjects used when - creating observations (e.g., "programming_style", "work_habits"). - Leave empty to search all subjects. Use this when you know the exact - subject category you want. - - tags: Filter results to only observations with these tags. Observations must - have at least one matching tag. Use the same format as when creating: - - ["programming", "functional-programming"] - - ["context:work", "project:thesis"] - - ["domain:finance", "machine-learning"] - - observation_types: Filter by type of observation: - - "belief": Opinions or beliefs the user holds - - "preference": Things they prefer or favor - - "behavior": Patterns in how they act or work - - "contradiction": Noted inconsistencies - - "general": Other observations - Leave as None to search all types. - - min_confidences: Only return observations with confidence >= this value, e.g. - {"observation_accuracy": 0.8, "interpretation": 0.5} facts where you were confident - that you observed the fact but are not necessarily sure about the interpretation. - Range: 0.0 to 1.0 - - limit: Maximum results to return (1-100). Default 10. Increase when you - need comprehensive understanding of a topic. - - Returns: - List of observations sorted by relevance, each containing: - - subject: What the observation is about - - content: The full observation text - - observation_type: Type of observation - - evidence: Supporting context/quotes if provided - - confidence: How certain the observation is (0-1) - - agent_model: Which AI model made the observation - - tags: All tags on this observation - - created_at: When it was observed (if available) - - Examples: - # Before discussing code architecture - results = await search_observations( - query="software architecture preferences microservices monoliths", - tags=["architecture"], - min_confidence=0.7 - ) - - # Understanding work style for scheduling - results = await search_observations( - query="when does user work best productivity schedule", - observation_types=["behavior", "preference"], - subject="work_schedule" - ) - - # Check for AI safety views before discussing AI - results = await search_observations( - query="artificial intelligence safety alignment concerns", - observation_types=["belief"], - min_confidence=0.8, - limit=20 - ) - - # Find contradictions on a topic - results = await search_observations( - query="testing methodology unit tests integration", - observation_types=["contradiction"], - tags=["testing", "software-development"] - ) - - Best practices: - - Search before making assumptions about user preferences - - Use broad queries first, then filter with tags/types if too many results - - Check for contradictions when user says something unexpected - - Higher confidence observations are more reliable - - Recent observations may override older ones on same topic + Returns: List with content, tags, created_at, metadata + Results sorted by relevance to your query. """ semantic_text = observation.generate_semantic_text( subject=subject or "", @@ -614,39 +299,20 @@ async def create_note( content: str, filename: str | None = None, note_type: str | None = None, - confidence: float = 0.5, + confidences: dict[str, float] = {}, tags: list[str] = [], ) -> dict: """ - Create a note when the user asks for something to be noted down or when you think - something is important to note down. - - Purpose: - Use this tool when the user explicitly asks to note, save, or record - something for later reference. Notes don't have to be really short - long - markdown docs are fine, as long as that was what was asked for. - You can also use this tool to note down things that are important to you. - - When to use: - - User says "note down that..." or "please save this" - - User asks to record information for future reference - - User wants to remember something specific + Create a note when user asks to save or record something. + Use when user explicitly requests noting information for future reference. Args: - subject: What the note is about (e.g., "meeting_notes", "idea") - content: The actual content to note down, as markdown + subject: What the note is about (used for organization) + content: Note content as a markdown string filename: Optional path relative to notes folder (e.g., "project/ideas.md") note_type: Optional categorization of the note - confidence: How confident you are in the note accuracy (0.0-1.0) - tags: Optional tags for organization - - Example: - # User: "Please note down that we decided to use React for the frontend" - await create_note( - subject="project_decisions", - content="Decided to use React for the frontend", - tags=["project", "frontend"] - ) + confidences: Dict of scores (0.0-1.0), e.g. {"observation_accuracy": 0.9} + tags: Organization tags for filtering and discovery """ if filename: path = pathlib.Path(filename) @@ -663,7 +329,7 @@ async def create_note( "content": content, "filename": filename, "note_type": note_type, - "confidence": confidence, + "confidences": confidences, "tags": tags, }, ) @@ -683,36 +349,14 @@ async def create_note( @mcp.tool() async def note_files(path: str = "/"): """ - List all available note files in the user's note storage system. - - Purpose: - This tool provides a way to discover and browse the user's organized note - collection. Notes are stored as Markdown files and can be created either - through the 'create_note' tool or by the user directly. Use this tool to - understand what notes exist before reading or referencing them, or to help - the user navigate their note collection. + List note files in the user's note storage. + Use to discover existing notes before reading or to help user navigate their collection. Args: - path: Directory path to search within the notes collection. Use "/" for the - root notes directory, or specify subdirectories like "/projects" or - "/meetings". The path should start with "/" and use forward slashes. - Examples: - - "/" - List all notes in the entire collection - - "/projects" - Only notes in the projects folder - - "/meetings/2024" - Notes in a specific year's meetings folder + path: Directory path to search (e.g., "/", "/projects", "/meetings") + Use "/" for root, or subdirectories to narrow scope - Examples: - # List all notes - all_notes = await note_files("/") - # Returns: ["/notes/project_ideas.md", "/notes/meetings/daily_standup.md", ...] - - # List notes in a specific folder - project_notes = await note_files("/projects") - # Returns: ["/notes/projects/website_redesign.md", "/notes/projects/mobile_app.md"] - - # Check for meeting notes - meeting_notes = await note_files("/meetings") - # Returns: ["/notes/meetings/2024-01-15.md", "/notes/meetings/weekly_review.md"] + Returns: List of file paths relative to notes directory """ root = settings.NOTES_STORAGE_DIR / path.lstrip("/") return [ @@ -725,38 +369,15 @@ async def note_files(path: str = "/"): @mcp.tool() def fetch_file(filename: str): """ - Retrieve the raw content of a file from the user's storage system. - - Purpose: - This tool allows you to read the actual content of files stored in the - user's file system, including notes, documents, images, and other files. - Use this when you need to access the specific content of a file that has - been referenced or when the user asks you to read/examine a particular file. + Read file content from user's storage. + Use when you need to access specific content of a file that's been referenced. Args: - filename: Path to the file to fetch, relative to the file storage directory. - Should start with "/" and use forward slashes. The path structure depends - on how files are organized in the storage system. - Examples: - - "/notes/project_ideas.md" - A note file - - "/documents/report.pdf" - A PDF document - - "/images/diagram.png" - An image file - - "/emails/important_thread.txt" - Saved email content + filename: Path to file (e.g., "/notes/project.md", "/documents/report.pdf") + Path should start with "/" and use forward slashes - Returns: - Raw bytes content of the file. For text files (like Markdown notes), you'll - typically want to decode this as UTF-8 to get readable text: - ```python - content_bytes = await fetch_file("/notes/my_note.md") - content_text = content_bytes.decode('utf-8') - ``` - - Raises: - FileNotFoundError: If the specified file doesn't exist at the given path. - - Security note: - This tool only accesses files within the configured storage directory, - ensuring it cannot read arbitrary system files. + Returns: Raw bytes content (decode as UTF-8 for text files) + Raises FileNotFoundError if file doesn't exist. """ path = settings.FILE_STORAGE_DIR / filename.lstrip("/") if not path.exists():