add discussion tools

This commit is contained in:
phernandez
2025-01-18 10:23:01 -06:00
parent 0df784c1c9
commit ae9926f54d
11 changed files with 235 additions and 105 deletions
@@ -40,13 +40,13 @@ async def get_memory_context(
"""Get rich context from memory:// URI."""
# add the project name from the config to the url as the "host
# Parse URI
memory_url = MemoryUrl.parse(f"memory://{config.project}/{uri}")
memory_url = MemoryUrl(f"memory://{config.project}/{uri}")
# Parse timeframe
since = parse_timeframe(timeframe)
# Build context
context = await context_service.build_context(str(memory_url), depth=depth, since=since)
context = await context_service.build_context(memory_url, depth=depth, since=since)
primary_entities = [SearchResult(**asdict(r)) for r in context["primary_entities"]]
related_entities = [RelatedResult(**asdict(r)) for r in context["related_entities"]]
+4
View File
@@ -10,6 +10,7 @@ from basic_memory.mcp.tools import knowledge # noqa: F401
from basic_memory.mcp.tools import search # noqa: F401
from basic_memory.mcp.tools import discovery # noqa: F401
from basic_memory.mcp.tools import activity # noqa: F401
from basic_memory.mcp.tools.discussion import get_discussion_context
# Export the tools
from basic_memory.mcp.tools.knowledge import (
@@ -55,4 +56,7 @@ __all__ = [
# Activity tools
"get_recent_activity",
# memory tools
"get_discussion_context"
]
+105
View File
@@ -0,0 +1,105 @@
"""Discussion context tools for Basic Memory MCP server."""
from typing import Optional
from basic_memory.mcp.async_client import client
from basic_memory.mcp.server import mcp
from basic_memory.schemas.memory import GraphContext, MemoryUrl
@mcp.tool(
category="discussion",
description="Get discussion context from a memory:// URI to continue conversations naturally.",
examples=[
{
"name": "Continue Previous Discussion",
"description": "Load context to continue a technical discussion",
"code": """
# Get context for previous discussion about search
context = await get_discussion_context(
url="memory://specs/search-refactor",
depth=2,
timeframe="7d"
)
# Access the context components
primary = context.primary_entities # Main discussion topic
related = context.related_entities # Related concepts
meta = context.metadata # Discussion metadata
# Analyze primary discussion topics
print("\\nPrimary Discussion Topic:")
for entity in primary:
print(f"- {entity.title}")
print(f" Type: {entity.type}")
if "status" in entity.metadata:
print(f" Status: {entity.metadata['status']}")
# Analyze related content
print("\\nRelated Topics:")
for item in related:
print(f"- {item.title}")
if item.relation_type:
print(f" Relation: {item.relation_type}")
"""
},
{
"name": "Pattern Matching and Time Filtering",
"description": "Search for matching discussions within a timeframe",
"code": """
# Find recent discussions matching a pattern
context = await get_discussion_context(
url="memory://design/*", # Match all design documents
depth=1, # Direct relations only
timeframe="3d" # Last 3 days only
)
print(f"Found {context.metadata['matched_entities']} matching discussions")
print(f"Total related items: {context.metadata['total_entities']}")
# Group by document type
by_type = {}
for entity in context.primary_entities:
doc_type = entity.type
if doc_type not in by_type:
by_type[doc_type] = []
by_type[doc_type].append(entity)
# Show summary by type
for doc_type, entities in by_type.items():
print(f"\\n{doc_type.title()}:")
for entity in entities:
print(f"- {entity.title}")
print(f" Added: {entity.metadata.get('created_at', 'Unknown')}")
"""
}
],
)
async def get_discussion_context(
url: MemoryUrl,
depth: Optional[int] = 2,
timeframe: Optional[str] = "7d",
) -> GraphContext:
"""Get context needed to continue a discussion.
This tool enables natural continuation of discussions by loading relevant context
from memory:// URIs. It uses pattern matching to find relevant content and builds
a rich context graph of related information.
Args:
url: memory:// URI pointing to discussion content (e.g. memory://specs/search)
depth: How many relation hops to traverse (default: 2)
timeframe: How far back to look, e.g. "7d", "24h" (default: "7d")
Returns:
GraphContext containing:
- primary_entities: Directly matched content
- related_entities: Connected content via relations
- metadata: Context building info
"""
# Map directly to the memory endpoint
memory_url = MemoryUrl.validate(url)
response = await client.get(
f"/memory/{memory_url.relative_path()}", params={"depth": depth, "timeframe": timeframe}
)
return GraphContext.model_validate(response.json())
+19 -53
View File
@@ -1,79 +1,45 @@
"""Schemas for memory context."""
from typing import Dict, List, Optional, Any
from pydantic import BaseModel, Field
from pydantic import field_validator
from pydantic import AnyUrl, Field, BaseModel
from basic_memory.schemas.search import SearchResult, RelatedResult
from basic_memory.config import config
"""Memory URL schema for knowledge addressing.
The memory:// URL scheme provides a unified way to address knowledge across projects:
memory://project-name/path/to/content
The memory:// URL scheme provides a unified way to address knowledge:
Examples:
memory://basic-memory/specs/search/* # Pattern matching
memory://basic-memory/specs/xyz # Exact permalink
memory://basic-memory/related/sync # Related content
memory://specs/search/* # Pattern matching
memory://specs/xyz # direct reference
"""
class MemoryUrl(BaseModel):
class MemoryUrl(AnyUrl):
"""memory:// URL scheme for knowledge addressing."""
scheme: str = Field(default="memory", frozen=True)
host: str # Project identifier
path: str # Full path
allowed_schemes = {'memory'}
# Query params
params: Dict[str, Any] = Field(default_factory=dict) # For special modes like 'related'
@field_validator("scheme")
@classmethod
def validate_scheme(cls, v: str) -> str:
"""Validate URL scheme."""
if v != "memory":
raise ValueError("URL must use memory:// scheme")
return v
def validate(cls, url: str) -> "MemoryUrl":
"""Validate and construct a MemoryUrl."""
@field_validator("host")
@classmethod
def validate_host(cls, v: Optional[str]) -> str:
"""Validate host (project identifier)."""
if not v:
raise ValueError("URL must include project/context identifier")
return v
memory_url = cls(url)
# if the url host value is not the project name, assume the default project
if memory_url.host != config.project:
memory_url = cls(f"memory://{config.project}/{memory_url.host}{memory_url.path}")
@classmethod
def parse(cls, url_str: str) -> "MemoryUrl":
"""Parse a memory:// URL string."""
# Split scheme and rest
if "://" not in url_str:
raise ValueError("URL must include scheme (memory://)")
scheme, rest = url_str.split("://", 1)
# Split host and path
parts = rest.split("/", 1)
if len(parts) != 2:
raise ValueError("URL must include both host and path")
host, path = parts
path = "/" + path # Add leading slash
# Create base URL
url = cls(scheme=scheme, host=host, path=path)
return url
@property
def project(self) -> str:
"""Get the project/context identifier."""
return self.host
return memory_url
def relative_path(self) -> str:
"""Get the path without leading slash."""
return self.path[1:] if self.path.startswith("/") else self.path
path = self.path
return path[1:] if path.startswith("/") else path
def __str__(self) -> str:
"""Convert back to URL string."""
@@ -101,4 +67,4 @@ class GraphContext(BaseModel):
"total_entities": 8,
"total_relations": 12,
},
)
)
+3 -6
View File
@@ -49,15 +49,12 @@ class ContextService:
async def build_context(
self,
uri: str,
memory_url: MemoryUrl,
depth: int = 2,
since: Optional[datetime] = None,
):
"""Build rich context from a memory:// URI."""
logger.debug(f"Building context for URI {uri}")
# Parse the URI
memory_url = MemoryUrl.parse(uri)
logger.debug(f"Building context for URI {memory_url}")
# Pattern matching - use search
if '*' in memory_url.relative_path():
@@ -85,7 +82,7 @@ class ContextService:
"primary_entities": primary,
"related_entities": related,
"metadata": {
"uri": uri,
"uri": memory_url.relative_path(),
"depth": depth,
"timeframe": since.isoformat() if since else None,
"generated_at": datetime.now(timezone.utc).isoformat(),