shorter tool descriptions + time tool

This commit is contained in:
Daniel O'Connell 2025-06-03 13:45:38 +02:00
parent 79567b19f2
commit 0551ddd30c

View File

@ -7,6 +7,7 @@ import pathlib
from datetime import datetime, timezone from datetime import datetime, timezone
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel
from sqlalchemy import Text, func from sqlalchemy import Text, func
from sqlalchemy import cast as sql_cast from sqlalchemy import cast as sql_cast
from sqlalchemy.dialects.postgresql import ARRAY from sqlalchemy.dialects.postgresql import ARRAY
@ -70,22 +71,17 @@ def filter_source_ids(
return 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() @mcp.tool()
async def get_all_tags() -> list[str]: async def get_all_tags() -> list[str]:
""" """
Get all unique tags used across the entire knowledge base. Get all unique tags used across the entire knowledge base.
Returns sorted list of tags from both observations and content.
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"
""" """
with make_session() as session: with make_session() as session:
tags_query = session.query(func.unnest(SourceItem.tags)).distinct() 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]: async def get_all_subjects() -> list[str]:
""" """
Get all unique subjects from observations about the user. Get all unique subjects from observations about the user.
Returns sorted list of subject identifiers used in observations.
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"
""" """
with make_session() as session: with make_session() as session:
return sorted( return sorted(
@ -120,23 +103,8 @@ async def get_all_subjects() -> list[str]:
@mcp.tool() @mcp.tool()
async def get_all_observation_types() -> list[str]: async def get_all_observation_types() -> list[str]:
""" """
Get all unique observation types that have been used. Get all observation types that have been used.
Standard types are belief, preference, behavior, contradiction, general, but there can be more.
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.
""" """
with make_session() as session: with make_session() as session:
return sorted( return sorted(
@ -157,110 +125,19 @@ async def search_knowledge_base(
limit: int = 10, limit: int = 10,
) -> list[dict]: ) -> list[dict]:
""" """
Search through the user's stored knowledge and content. Search user's stored content including emails, documents, articles, books.
Use to find specific information the user has saved or received.
Purpose: Combine with search_observations for complete user context.
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.
Args: Args:
query: Natural language search query. Be descriptive about what you're query: Natural language search query - be descriptive about what you're looking for
looking for. The search understands meaning but also values exact terms. previews: Include actual content in results - when false only a snippet is returned
Examples: modalities: Filter by type: email, blog, book, forum, photo, comic, webpage (empty = all)
- "email about project deadline from last week" tags: Filter by tags - content must have at least one matching tag
- "functional programming articles comparing Haskell and Scala" limit: Max results (1-100)
- "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.
previews: Whether to include content snippets in results. Returns: List of search results with id, score, chunks, content, filename
- True: Returns preview text and image previews (useful for quick scanning) Higher scores (>0.7) indicate strong matches.
- 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
""" """
logger.info(f"MCP search for: {query}") 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] 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() @mcp.tool()
async def observe( async def observe(
content: str, observations: list[RawObservation],
subject: str,
observation_type: str = "general",
confidences: dict[str, float] = {},
evidence: dict | None = None,
tags: list[str] | None = None,
session_id: str | None = None, session_id: str | None = None,
agent_model: str = "unknown", agent_model: str = "unknown",
) -> dict: ) -> 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: RawObservation fields:
This tool is part of a memory system designed to help AI agents build a content (required): Detailed observation text explaining what you observed
deep, persistent understanding of users over time. Use it to record any subject (required): Consistent identifier like "programming_style", "work_habits"
notable information about the user's preferences, beliefs, behaviors, or observation_type: belief, preference, behavior, contradiction, general
characteristics. These observations accumulate to create a comprehensive confidences: Dict of scores (0.0-1.0), e.g. {"observation_accuracy": 0.9}
model of the user that improves future interactions. evidence: Context dict with extra context, e.g. "quote" (exact words) and "context" (situation)
tags: List of categorization tags for organization
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.
Args: Args:
content: The observation itself. Be specific and detailed. Write complete observations: List of RawObservation objects
thoughts that will make sense when read months later without context. session_id: UUID to group observations from same conversation
Bad: "Likes FP" agent_model: AI model making observations (for quality tracking)
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 <aspect>: <confidence>
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"
)
""" """
task = celery_app.send_task( tasks = [
SYNC_OBSERVATION, (
queue="notes", observation,
kwargs={ celery_app.send_task(
"subject": subject, SYNC_OBSERVATION,
"content": content, queue="notes",
"observation_type": observation_type, kwargs={
"confidences": confidences, "subject": observation.subject,
"evidence": evidence, "content": observation.content,
"tags": tags, "observation_type": observation.observation_type,
"session_id": session_id, "confidences": observation.confidences,
"agent_model": agent_model, "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 { return {
"task_id": task.id, "task_ids": {short_content(obs): task.id for obs, task in tasks},
"status": "queued", "status": "queued",
} }
@ -465,107 +237,20 @@ async def search_observations(
limit: int = 10, limit: int = 10,
) -> list[dict]: ) -> list[dict]:
""" """
Search through observations to understand the user better. Search recorded observations about the user.
Use before responding to understand user preferences, patterns, and past insights.
Purpose: Search by meaning - the query matches both content and context.
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
Args: Args:
query: Natural language description of what you're looking for. The search query: Natural language search query describing what you're looking for
matches both meaning and specific terms in observation content. subject: Filter by exact subject identifier (empty = search all subjects)
Examples: tags: Filter by tags (must have at least one matching tag)
- "programming preferences and coding style" observation_types: Filter by: belief, preference, behavior, contradiction, general
- "opinions about artificial intelligence and AI safety" min_confidences: Minimum confidence thresholds, e.g. {"observation_accuracy": 0.8}
- "work habits productivity patterns when does user work best" limit: Max results (1-100)
- "previous projects the user has worked on"
Pro tip: Use natural language but include key terms you expect to find.
subject: Filter by exact subject identifier. Must match subjects used when Returns: List with content, tags, created_at, metadata
creating observations (e.g., "programming_style", "work_habits"). Results sorted by relevance to your query.
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
""" """
semantic_text = observation.generate_semantic_text( semantic_text = observation.generate_semantic_text(
subject=subject or "", subject=subject or "",
@ -614,39 +299,20 @@ async def create_note(
content: str, content: str,
filename: str | None = None, filename: str | None = None,
note_type: str | None = None, note_type: str | None = None,
confidence: float = 0.5, confidences: dict[str, float] = {},
tags: list[str] = [], tags: list[str] = [],
) -> dict: ) -> dict:
""" """
Create a note when the user asks for something to be noted down or when you think Create a note when user asks to save or record something.
something is important to note down. Use when user explicitly requests noting information for future reference.
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
Args: Args:
subject: What the note is about (e.g., "meeting_notes", "idea") subject: What the note is about (used for organization)
content: The actual content to note down, as markdown content: Note content as a markdown string
filename: Optional path relative to notes folder (e.g., "project/ideas.md") filename: Optional path relative to notes folder (e.g., "project/ideas.md")
note_type: Optional categorization of the note note_type: Optional categorization of the note
confidence: How confident you are in the note accuracy (0.0-1.0) confidences: Dict of scores (0.0-1.0), e.g. {"observation_accuracy": 0.9}
tags: Optional tags for organization tags: Organization tags for filtering and discovery
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"]
)
""" """
if filename: if filename:
path = pathlib.Path(filename) path = pathlib.Path(filename)
@ -663,7 +329,7 @@ async def create_note(
"content": content, "content": content,
"filename": filename, "filename": filename,
"note_type": note_type, "note_type": note_type,
"confidence": confidence, "confidences": confidences,
"tags": tags, "tags": tags,
}, },
) )
@ -683,36 +349,14 @@ async def create_note(
@mcp.tool() @mcp.tool()
async def note_files(path: str = "/"): async def note_files(path: str = "/"):
""" """
List all available note files in the user's note storage system. List note files in the user's note storage.
Use to discover existing notes before reading or to help user navigate their collection.
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.
Args: Args:
path: Directory path to search within the notes collection. Use "/" for the path: Directory path to search (e.g., "/", "/projects", "/meetings")
root notes directory, or specify subdirectories like "/projects" or Use "/" for root, or subdirectories to narrow scope
"/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
Examples: Returns: List of file paths relative to notes directory
# 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"]
""" """
root = settings.NOTES_STORAGE_DIR / path.lstrip("/") root = settings.NOTES_STORAGE_DIR / path.lstrip("/")
return [ return [
@ -725,38 +369,15 @@ async def note_files(path: str = "/"):
@mcp.tool() @mcp.tool()
def fetch_file(filename: str): def fetch_file(filename: str):
""" """
Retrieve the raw content of a file from the user's storage system. Read file content from user's storage.
Use when you need to access specific content of a file that's been referenced.
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.
Args: Args:
filename: Path to the file to fetch, relative to the file storage directory. filename: Path to file (e.g., "/notes/project.md", "/documents/report.pdf")
Should start with "/" and use forward slashes. The path structure depends Path should start with "/" and use forward slashes
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
Returns: Returns: Raw bytes content (decode as UTF-8 for text files)
Raw bytes content of the file. For text files (like Markdown notes), you'll Raises FileNotFoundError if file doesn't exist.
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.
""" """
path = settings.FILE_STORAGE_DIR / filename.lstrip("/") path = settings.FILE_STORAGE_DIR / filename.lstrip("/")
if not path.exists(): if not path.exists():