mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
add discussion tools
This commit is contained in:
@@ -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"]]
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
@@ -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())
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
)
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user