remove activity router, replace with memory/recent

This commit is contained in:
phernandez
2025-01-20 15:35:08 -06:00
parent f61c5970df
commit 14f520c272
24 changed files with 421 additions and 591 deletions
+1 -2
View File
@@ -6,7 +6,7 @@ from fastapi import FastAPI
from loguru import logger
from basic_memory import db
from .routers import knowledge, discovery, search, activity, memory, resource
from .routers import knowledge, discovery, search, memory, resource
@asynccontextmanager
@@ -29,7 +29,6 @@ app = FastAPI(
# Include routers
app.include_router(knowledge.router)
app.include_router(discovery.router)
app.include_router(activity.router)
app.include_router(search.router)
app.include_router(memory.router)
app.include_router(resource.router)
+1 -2
View File
@@ -4,7 +4,6 @@ from . import knowledge_router as knowledge
from . import discovery_router as discovery
from . import memory_router as memory
from . import resource_router as resource
from . import activity_router as activity
from . import search_router as search
__all__ = ["knowledge", "discovery", "memory", "resource", "activity", "search"]
__all__ = ["knowledge", "discovery", "memory", "resource", "search"]
@@ -1,47 +0,0 @@
"""Activity router for tracking recent changes."""
from typing import List, Optional
from fastapi import APIRouter, Depends, Query
from loguru import logger
from basic_memory.deps import get_activity_service
from basic_memory.services.activity_service import ActivityService
from basic_memory.schemas.activity import RecentActivity, ActivityType
router = APIRouter(
prefix="/activity",
tags=["activity"]
)
@router.get(
"/recent",
response_model=RecentActivity,
summary="Get recent activity"
)
async def get_recent_activity(
activity_service: ActivityService = Depends(get_activity_service),
timeframe: str = "1d",
activity_types: Optional[List[ActivityType]] = Query(None),
) -> RecentActivity:
"""
Get recent activity across the knowledge base.
Args:
timeframe: Time window to look back (1h, 1d, 1w, 1m)
activity_types: Optional list of ActivityType values to include
Returns:
RecentActivity with changes and summary
"""
logger.debug(
f"Getting recent activity (timeframe={timeframe}, "
f"types={activity_types})"
)
return await activity_service.get_recent_activity(
timeframe=timeframe,
activity_types=[t.value for t in activity_types] if activity_types else None,
)
+68 -47
View File
@@ -1,14 +1,17 @@
"""Routes for memory:// URI operations."""
from datetime import datetime, timedelta
from typing import Optional
from typing import Optional, List, Annotated
from fastapi import APIRouter
from dateparser import parse
from fastapi import APIRouter, Query
from loguru import logger
from basic_memory.config import config
from basic_memory.deps import ContextServiceDep, EntityRepositoryDep
from basic_memory.repository import EntityRepository
from basic_memory.repository.search_repository import SearchIndexRow
from basic_memory.schemas.base import TimeFrame
from basic_memory.schemas.memory import (
MemoryUrl,
GraphContext,
@@ -23,49 +26,8 @@ from basic_memory.services.context_service import ContextResultRow
router = APIRouter(prefix="/memory", tags=["memory"])
def parse_timeframe(timeframe: str) -> Optional[datetime]:
"""Convert timeframe string to datetime.
Formats:
- 7d: 7 days ago
- 30d: 30 days ago
- None: no time limit
"""
if not timeframe:
return None
if not timeframe.endswith("d"):
raise ValueError("Timeframe must be in days (e.g., '7d')")
days = int(timeframe[:-1])
return datetime.utcnow() - timedelta(days=days)
@router.get("/{uri:path}", response_model=GraphContext)
async def get_memory_context(
context_service: ContextServiceDep,
entity_repository: EntityRepositoryDep,
uri: str,
depth: int = 1,
timeframe: str = "7d",
max_results: int = 10,
) -> GraphContext:
"""Get rich context from memory:// URI."""
# add the project name from the config to the url as the "host
# Parse URI
logger.debug(
f"Getting context for URI: `{uri}` depth: `{depth}` timeframe: `{timeframe}` max_results: `{max_results}`"
)
memory_url = MemoryUrl(f"memory://{config.project}/{uri}")
# Parse timeframe
since = parse_timeframe(timeframe)
# Build context
context = await context_service.build_context(
memory_url, depth=depth, since=since, max_results=max_results
)
async def to_graph_context(context, entity_repository: EntityRepository):
# return results
async def to_summary(item: SearchIndexRow | ContextResultRow):
match item.type:
@@ -81,10 +43,9 @@ async def get_memory_context(
category=item.category, content=item.content, permalink=item.permalink
)
case SearchItemType.RELATION:
from_entity = await entity_repository.find_by_id(item.from_id)
to_entity = await entity_repository.find_by_id(item.to_id)
return RelationSummary(
permalink=item.permalink,
type=item.type,
@@ -96,9 +57,69 @@ async def get_memory_context(
primary_results = [await to_summary(r) for r in context["primary_results"]]
related_results = [await to_summary(r) for r in context["related_results"]]
metadata = MemoryMetadata.model_validate(context["metadata"])
# Transform to GraphContext
return GraphContext(
primary_results=primary_results, related_results=related_results, metadata=metadata
)
@router.get("/recent", response_model=GraphContext)
async def recent(
context_service: ContextServiceDep,
entity_repository: EntityRepositoryDep,
types: Annotated[list[SearchItemType] | None, Query()] = None,
depth: int = 1,
timeframe: TimeFrame = "7d",
max_results: int = 10,
) -> GraphContext:
# return all types by default
types = (
[SearchItemType.ENTITY, SearchItemType.RELATION, SearchItemType.OBSERVATION]
if not types
else types
)
logger.debug(
f"Getting recent context: `{types}` depth: `{depth}` timeframe: `{timeframe}` max_results: `{max_results}`"
)
# Parse timeframe
since = parse(timeframe)
# Build context
context = await context_service.build_context(
types=types, depth=depth, since=since, max_results=max_results
)
return await to_graph_context(context, entity_repository=entity_repository)
# get_memory_context needs to be declared last so other paths can match
@router.get("/{uri:path}", response_model=GraphContext)
async def get_memory_context(
context_service: ContextServiceDep,
entity_repository: EntityRepositoryDep,
uri: str,
depth: int = 1,
timeframe: TimeFrame = "7d",
max_results: int = 10,
) -> GraphContext:
"""Get rich context from memory:// URI."""
# add the project name from the config to the url as the "host
# Parse URI
logger.debug(
f"Getting context for URI: `{uri}` depth: `{depth}` timeframe: `{timeframe}` max_results: `{max_results}`"
)
memory_url = MemoryUrl(f"memory://{config.project}/{uri}")
# Parse timeframe
since = parse(timeframe)
# Build context
context = await context_service.build_context(
memory_url, depth=depth, since=since, max_results=max_results
)
return await to_graph_context(context, entity_repository=entity_repository)
-15
View File
@@ -21,7 +21,6 @@ from basic_memory.services import (
ObservationService,
RelationService,
)
from basic_memory.services.activity_service import ActivityService
from basic_memory.services.context_service import ContextService
from basic_memory.services.file_service import FileService
from basic_memory.services.search_service import SearchService
@@ -166,20 +165,6 @@ async def get_search_service(
SearchServiceDep = Annotated[SearchService, Depends(get_search_service)]
async def get_activity_service(
entity_service: EntityServiceDep,
relation_service: RelationServiceDep,
) -> ActivityService:
"""Create ActivityService with dependencies."""
return ActivityService(
entity_service=entity_service,
relation_service=relation_service,
)
ActivityServiceDep = Annotated[ActivityService, Depends(get_activity_service)]
async def get_knowledge_writer() -> KnowledgeWriter:
return KnowledgeWriter()
+2 -10
View File
@@ -5,15 +5,8 @@ Basic Memory through the MCP protocol. Importing this module registers
all tools with the MCP server.
"""
from basic_memory.mcp.tools import activity # noqa: F401
# Import tools to register them with MCP
from basic_memory.mcp.tools import knowledge # noqa: F401
from basic_memory.mcp.tools import search # noqa: F401
from basic_memory.mcp.tools.activity import (
get_recent_activity,
)
from basic_memory.mcp.tools.memory import build_context
from basic_memory.mcp.tools.memory import build_context, recent_activity
from basic_memory.mcp.tools.ai_edit import ai_edit
# Export the tools
@@ -40,10 +33,9 @@ __all__ = [
"get_entities",
# Search tools
"search",
# Activity tools
"get_recent_activity",
# memory tools
"build_context",
"recent_activity",
# file edit
"ai_edit",
]
-42
View File
@@ -1,42 +0,0 @@
"""Tools for tracking activity and changes in the knowledge base."""
from typing import List, Optional
from loguru import logger
from mcp.server.fastmcp import Context
from basic_memory.mcp.async_client import client
from basic_memory.mcp.server import mcp
from basic_memory.schemas.activity import ActivityType, RecentActivity
@mcp.tool(
description="Track recent changes to documents, entities, and relations",
)
async def get_recent_activity(
context: Context,
timeframe: str = "1d",
activity_types: Optional[List[ActivityType]] = None,
) -> RecentActivity:
"""Track changes across the knowledge base.
Args:
timeframe: Time window to analyze ("1h", "1d", "1w")
activity_types: Optional list of types to filter by
context: MCP context
Returns:
RecentActivity object with changes and summary statistics
"""
context.info(f"Getting recent activity (timeframe={timeframe}, types={activity_types})")
# Build params
params = {
"timeframe": timeframe,
}
if activity_types:
params["activity_types"] = [t.value for t in activity_types]
# Get activity
response = await client.get("/activity/recent", params=params)
return RecentActivity.model_validate(response.json())
+99 -10
View File
@@ -1,19 +1,34 @@
"""Discussion context tools for Basic Memory MCP server."""
from typing import Optional
from typing import Optional, List
from loguru import logger
from basic_memory.mcp.async_client import client
from basic_memory.mcp.server import mcp
from basic_memory.mcp.tools.utils import call_get
from basic_memory.schemas.memory import GraphContext, MemoryUrl
from basic_memory.schemas.search import SearchItemType
from basic_memory.schemas.base import TimeFrame
@mcp.tool(
description="Build context from a memory:// URI to continue conversations naturally.",
description="""Build context from a memory:// URI to continue conversations naturally.
Use this to follow up on previous discussions or explore related topics.
Timeframes use natural language support - examples:
- "2 days ago"
- "last week"
- "today"
- "3 months ago"
Or standard formats like "7d", "24h"
""",
)
async def build_context(
url: MemoryUrl, depth: Optional[int] = 1, timeframe: Optional[str] = "7d", max_results: int = 10
url: MemoryUrl,
depth: Optional[int] = 1,
timeframe: Optional[TimeFrame] = "7d",
max_results: int = 10,
) -> GraphContext:
"""Get context needed to continue a discussion.
@@ -22,23 +37,97 @@ async def build_context(
a rich context graph of related information.
Args:
ctx: MCP context
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")
max_results: The maximum number of results to return (default: 10)
depth: How many relation hops to traverse (1-3 recommended for performance)
timeframe: How far back to look. Supports natural language like "2 days ago", "last week"
max_results: Maximum number of results to return (default: 10)
Returns:
GraphContext containing:
- primary_results: Directly matched content
- primary_results: Content matching the memory:// URI
- related_results: Connected content via relations
- metadata: Context building info
- metadata: Context building details
Examples:
# Continue a specific discussion
build_context("memory://specs/search")
# Get deeper context about a component
build_context("memory://components/memory-service", depth=2)
# Look at recent changes to a specification
build_context("memory://specs/document-format", timeframe="today")
# Research the history of a feature
build_context("memory://features/knowledge-graph", timeframe="3 months ago")
"""
logger.info(f"Building context from {url}")
# Map directly to the memory endpoint
memory_url = MemoryUrl.validate(url)
response = await client.get(
response = await call_get(
client,
f"/memory/{memory_url.relative_path()}",
params={"depth": depth, "timeframe": timeframe, "max_results": max_results},
)
return GraphContext.model_validate(response.json())
@mcp.tool(
description="""Get recent activity from across the knowledge base.
Timeframe supports natural language formats like:
- "2 days ago"
- "last week"
- "3 weeks"
- "2 months"
- "yesterday"
- "today"
Or standard formats like "7d", "24h"
""",
)
async def recent_activity(
types: List[SearchItemType] = None,
depth: Optional[int] = 1,
timeframe: Optional[TimeFrame] = "7d",
max_results: int = 10,
) -> GraphContext:
"""Get recent activity across the knowledge base.
Args:
types: Filter by entity types (["entity", "relation", "observation"]). If None, returns all types.
depth: How many relation hops to traverse when building context (1-3 recommended)
timeframe: How far back to look. Supports natural language like "2 days ago", "last week"
max_results: Maximum number of results to return (default: 10)
Returns:
GraphContext containing:
- primary_results: Latest activities matching the filters
- related_results: Connected content via relations
- metadata: Query details and statistics
Examples:
# Get all activity from last week
recent_activity(timeframe="last week")
# Get only entity changes from yesterday
recent_activity(types=["entity"], timeframe="yesterday")
# Track recent specification changes
recent_activity(types=["entity"], depth=2, timeframe="3 days ago")
# Follow recent relation changes
recent_activity(types=["relation"], timeframe="today")
Notes:
- Higher depth values (>3) may impact performance with large result sets
- For focused queries, consider using build_context with a specific URI
- Max timeframe is 1 year in the past
"""
logger.info(
f"Getting recent activity from {types}, depth={depth}, timeframe={timeframe}, max_results={max_results}"
)
response = await client.get(
"/memory/recent",
params={"depth": depth, "timeframe": timeframe, "max_results": max_results, "types": types},
)
return GraphContext.model_validate(response.json())
@@ -1,6 +1,7 @@
"""Repository for search operations."""
import json
import time
from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional, Any, Dict
@@ -95,6 +96,7 @@ class SearchRepository:
types: List[SearchItemType] = None,
after_date: datetime = None,
entity_types: List[str] = None,
limit: int = 10,
) -> List[SearchIndexRow]:
"""Search across all indexed content with fuzzy matching."""
conditions = []
@@ -131,6 +133,9 @@ class SearchRepository:
params["after_date"] = after_date
conditions.append("datetime(created_at) > datetime(:after_date)")
# set limit on search query
params["limit"] = limit
# Build WHERE clause
where_clause = " AND ".join(conditions) if conditions else "1=1"
@@ -154,6 +159,7 @@ class SearchRepository:
FROM search_index
WHERE {where_clause}
ORDER BY score ASC
LIMIT :limit
"""
#logger.debug(f"Search {sql} params: {params}")
@@ -182,7 +188,7 @@ class SearchRepository:
for row in rows
]
logger.debug(f"Search results: {results}")
#logger.debug(f"Search results: {results}")
return results
async def index_item(
@@ -234,9 +240,12 @@ class SearchRepository:
"""Execute a query asynchronously."""
#logger.debug(f"Executing query: {query}")
async with db.scoped_session(self.session_maker) as session:
start_time = time.perf_counter()
if params:
result = await session.execute(query, params)
else:
result = await session.execute(query)
logger.debug("Query executed successfully")
end_time = time.perf_counter()
elapsed_time = end_time - start_time
logger.debug(f"Query executed successfully in {elapsed_time:.2f}s.")
return result
-85
View File
@@ -1,85 +0,0 @@
from datetime import datetime, timedelta
from enum import Enum
from typing import List, Optional
from pydantic import BaseModel, Field
class TimeFrame:
"""Represents a time period for querying activity."""
def __init__(self, timeframe_str: str):
"""Parse timeframe string (e.g., '1d', '2w', '1m')"""
if not timeframe_str or len(timeframe_str) < 2:
raise ValueError("Invalid timeframe format")
try:
self.value = int(timeframe_str[:-1])
self.unit = timeframe_str[-1]
except ValueError:
raise ValueError("Invalid timeframe format")
if self.unit not in ["h", "d", "w", "m"]:
raise ValueError("Invalid timeframe unit")
if self.value < 1:
raise ValueError("Timeframe value must be positive")
@property
def to_timedelta(self) -> timedelta:
"""Convert to Python timedelta."""
if self.unit == "h":
return timedelta(hours=self.value)
elif self.unit == "d":
return timedelta(days=self.value)
elif self.unit == "w":
return timedelta(weeks=self.value)
elif self.unit == "m":
# Approximate month as 30 days
return timedelta(days=self.value * 30)
else:
raise ValueError(f"Invalid unit: {self.unit}")
class ActivityType(str, Enum):
"""Types of activities that can be tracked."""
ENTITY = "entity"
RELATION = "relation"
class ChangeType(str, Enum):
"""Types of changes that can occur."""
CREATED = "created"
UPDATED = "updated"
DELETED = "deleted"
class ActivityChange(BaseModel):
"""Represents a single change in the system."""
activity_type: ActivityType
change_type: ChangeType
timestamp: datetime
permalink: str
summary: str
content: Optional[str] = None
class ActivitySummary(BaseModel):
"""Summary statistics about recent activity."""
entity_changes: int = Field(default=0, description="Number of entity changes")
relation_changes: int = Field(default=0, description="Number of relation changes")
most_active_paths: List[str] = Field(
default_factory=list, description="List of most frequently changed paths"
)
class RecentActivity(BaseModel):
"""Complete activity report."""
timeframe: str
changes: List[ActivityChange] = Field(default_factory=list)
summary: ActivitySummary
+33
View File
@@ -13,10 +13,13 @@ Key Concepts:
import mimetypes
import re
from datetime import datetime
from enum import Enum
from typing import List, Optional, Annotated, Dict
from annotated_types import MinLen, MaxLen
from dateparser import parse
from pydantic import BaseModel, BeforeValidator, Field, model_validator, ValidationError
from basic_memory.utils import generate_permalink
@@ -81,6 +84,36 @@ class ObservationCategory(str, Enum):
return None
def validate_timeframe(timeframe: str) -> str:
"""Convert human readable timeframes to a duration relative to the current time."""
if not isinstance(timeframe, str):
raise ValueError("Timeframe must be a string")
# Parse relative time expression
parsed = parse(timeframe)
if not parsed:
raise ValueError(f"Could not parse timeframe: {timeframe}")
# Convert to duration
now = datetime.now()
if parsed > now:
raise ValueError("Timeframe cannot be in the future")
# Could format the duration back to our standard format
days = (now - parsed).days
# Could enforce reasonable limits
if days > 365:
raise ValueError("Timeframe should be <= 1 year")
return f"{days}d"
TimeFrame = Annotated[
str,
BeforeValidator(validate_timeframe)
]
PathId = Annotated[str, BeforeValidator(validate_path_format)]
"""Unique identifier in format '{path}/{normalized_name}'."""
+4 -2
View File
@@ -1,11 +1,12 @@
"""Schemas for memory context."""
from datetime import datetime
from typing import Dict, List, Any
from typing import Dict, List, Any, Optional
from pydantic import AnyUrl, Field, BaseModel
from basic_memory.config import config
from basic_memory.schemas.search import SearchItemType
"""Memory URL schema for knowledge addressing.
@@ -77,7 +78,8 @@ class ObservationSummary(BaseModel):
class MemoryMetadata(BaseModel):
"""Simplified response metadata."""
uri: str
uri: Optional[str] = None
types: Optional[List[SearchItemType]] = None
depth: int
timeframe: str
generated_at: datetime
@@ -1,157 +0,0 @@
"""Service for tracking and querying activity across the knowledge base."""
from datetime import datetime, timezone
from typing import List, Optional
from . import EntityService, RelationService
from ..schemas.activity import (
ActivityChange,
ActivitySummary,
ActivityType,
ChangeType,
RecentActivity,
TimeFrame,
)
class ActivityService:
"""Service for tracking and querying activity across the knowledge base."""
def __init__(
self,
entity_service: EntityService,
relation_service: RelationService,
):
"""Initialize with required services."""
self.entity_service = entity_service
self.relation_service = relation_service
async def get_recent_activity(
self,
timeframe: str = "1d",
activity_types: Optional[List[str]] = None,
) -> RecentActivity:
"""Get all recent activity in the knowledge base.
Args:
timeframe: Time window to look back (1h, 1d, 1w, 1m)
activity_types: Optional list of types to include
Returns:
RecentActivity object containing changes and summary
"""
# Parse timeframe and get cutoff date
tf = TimeFrame(timeframe)
since = datetime.now(timezone.utc) - tf.to_timedelta
# Get changes based on requested types
changes = []
types_to_fetch = (
[ActivityType(t) for t in activity_types] if activity_types else list(ActivityType)
)
for activity_type in types_to_fetch:
if activity_type == ActivityType.ENTITY:
changes.extend(await self._get_entity_changes(since))
elif activity_type == ActivityType.RELATION:
changes.extend(await self._get_relation_changes(since))
# Sort all changes by timestamp, ensuring timezone awareness
for change in changes:
if change.timestamp.tzinfo is None:
change.timestamp = change.timestamp.replace(tzinfo=timezone.utc)
changes.sort(key=lambda x: x.timestamp, reverse=True)
# Generate summary
summary = ActivitySummary(
entity_changes=len([c for c in changes if c.activity_type == ActivityType.ENTITY]),
relation_changes=len([c for c in changes if c.activity_type == ActivityType.RELATION]),
most_active_paths=self._get_most_active_paths(changes),
)
return RecentActivity(timeframe=timeframe, changes=changes, summary=summary)
async def _get_entity_changes(self, since: datetime) -> List[ActivityChange]:
"""Get recent entity changes."""
# Query entities updated since the cutoff
entities = await self.entity_service.get_modified_since(since)
changes = []
for entity in entities:
# Ensure timestamps are timezone-aware
created_at = (
entity.created_at.replace(tzinfo=timezone.utc)
if entity.created_at.tzinfo is None
else entity.created_at
)
updated_at = (
entity.updated_at.replace(tzinfo=timezone.utc)
if entity.updated_at.tzinfo is None
else entity.updated_at
)
change_type = ChangeType.CREATED if created_at >= since else ChangeType.UPDATED
changes.append(
ActivityChange(
activity_type=ActivityType.ENTITY,
change_type=change_type,
timestamp=updated_at,
permalink=entity.permalink,
summary=f"{change_type.value.title()} entity: {entity.title}",
content=entity.summary,
)
)
return changes
async def _get_relation_changes(self, since: datetime) -> List[ActivityChange]:
"""Get recent relation changes."""
# Query relations updated since the cutoff
relations = await self.relation_service.get_modified_since(since)
changes = []
for relation in relations:
# Ensure timestamps are timezone-aware
created_at = (
relation.created_at.replace(tzinfo=timezone.utc)
if relation.created_at.tzinfo is None
else relation.created_at
)
updated_at = (
relation.updated_at.replace(tzinfo=timezone.utc)
if relation.updated_at.tzinfo is None
else relation.updated_at
)
change_type = ChangeType.CREATED if created_at >= since else ChangeType.UPDATED
changes.append(
ActivityChange(
activity_type=ActivityType.RELATION,
change_type=change_type,
timestamp=updated_at,
permalink=f"{relation.from_id}->{relation.to_id}",
summary=(
f"{change_type.value.title()} relation: "
f"{relation.from_id} {relation.relation_type} {relation.to_id}"
),
content=relation.context,
)
)
return changes
def _get_most_active_paths(self, changes: List[ActivityChange], limit: int = 5) -> List[str]:
"""Get the most frequently changed paths."""
path_counts = {}
for change in changes:
path_counts[change.permalink] = path_counts.get(change.permalink, 0) + 1
# Sort by count descending and take top paths
sorted_paths = sorted(
path_counts.items(),
key=lambda x: (-x[1], x[0]), # Sort by count desc, then path asc
)
return [path for path, _ in sorted_paths[:limit]]
+21 -15
View File
@@ -50,7 +50,8 @@ class ContextService:
async def build_context(
self,
memory_url: MemoryUrl,
memory_url: MemoryUrl = None,
types: List[SearchItemType] = None,
depth: int = 1,
since: Optional[datetime] = None,
max_results: int = 10,
@@ -60,22 +61,26 @@ class ContextService:
f"Building context for URI: '{memory_url}' depth: '{depth}' since: '{since}' max_results: '{max_results}'"
)
# Pattern matching - use search
if "*" in memory_url.relative_path():
logger.debug(f"Pattern search for '{memory_url.relative_path()}'")
primary = await self.search_repository.search(
permalink_match=memory_url.relative_path()
)
# Direct lookup for exact path
if memory_url:
# Pattern matching - use search
if "*" in memory_url.relative_path():
logger.debug(f"Pattern search for '{memory_url.relative_path()}'")
primary = await self.search_repository.search(
permalink_match=memory_url.relative_path()
)
# Direct lookup for exact path
else:
logger.debug(f"Direct lookup for '{memory_url.relative_path()}'")
primary = await self.search_repository.search(permalink=memory_url.relative_path())
else:
logger.debug(f"Direct lookup for '{memory_url.relative_path()}'")
primary = await self.search_repository.search(permalink=memory_url.relative_path())
logger.debug(f"Build context for '{types}'")
primary = await self.search_repository.search(types=types)
# Get type_id pairs for traversal
type_id_pairs = [(r.type, r.id) for r in primary] if primary else []
logger.debug(f"primary type_id_pairs: {type_id_pairs}")
logger.debug(f"found primary type_id_pairs: {len(type_id_pairs)}")
# Find related content
related = await self.find_related(
@@ -83,14 +88,15 @@ class ContextService:
)
logger.debug(f"Found {len(related)} related results")
for r in related:
logger.debug(f"Found related result: {r}")
logger.debug(f"Found related {r.type}: {r.permalink}")
# Build response
return {
"primary_results": primary,
"related_results": related,
"metadata": {
"uri": memory_url.relative_path(),
"uri": memory_url.relative_path() if memory_url else None,
"types": types if types else None,
"depth": depth,
"timeframe": since.isoformat() if since else None,
"generated_at": datetime.now(timezone.utc).isoformat(),
@@ -117,7 +123,7 @@ class ContextService:
if not type_id_pairs:
return []
logger.debug(f"Finding connected items for {type_id_pairs} with depth {max_depth}")
logger.debug(f"Finding connected items for {len(type_id_pairs)} with depth {max_depth}")
# Build the VALUES clause directly since SQLite doesn't handle parameterized IN well
values = ", ".join([f"('{t}', {i})" for t, i in type_id_pairs])