simplify MemoryUrl to be validated string

This commit is contained in:
phernandez
2025-02-01 13:42:48 -06:00
parent 572187a59a
commit 3199ab0217
9 changed files with 89 additions and 87 deletions
+1 -1
View File
@@ -39,4 +39,4 @@ async def exception_handler(request, exc):
logger.exception(
f"An unhandled exception occurred for request '{request.url}', exception: {exc}"
)
return await http_exception_handler(request, HTTPException(status_code=500, detail=exc.args[0]))
return await http_exception_handler(request, HTTPException(status_code=500, detail=str(exc)))
@@ -13,12 +13,11 @@ 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,
RelationSummary,
EntitySummary,
ObservationSummary,
MemoryMetadata,
MemoryMetadata, normalize_memory_url,
)
from basic_memory.schemas.search import SearchItemType
from basic_memory.services.context_service import ContextResultRow
@@ -50,7 +49,7 @@ async def to_graph_context(context, entity_repository: EntityRepository):
permalink=item.permalink,
type=item.type,
from_id=from_entity.permalink,
to_id=to_entity.permalink,
to_id=to_entity.permalink if to_entity else None,
)
primary_results = [await to_summary(r) for r in context["primary_results"]]
@@ -109,7 +108,7 @@ async def get_memory_context(
logger.debug(
f"Getting context for URI: `{uri}` depth: `{depth}` timeframe: `{timeframe}` max_results: `{max_results}`"
)
memory_url = MemoryUrl(f"memory://{uri}")
memory_url = normalize_memory_url(uri)
# Parse timeframe
since = parse(timeframe)
+3 -3
View File
@@ -7,7 +7,7 @@ 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.memory import GraphContext, MemoryUrl, memory_url, memory_url_path, normalize_memory_url
from basic_memory.schemas.base import TimeFrame
@@ -61,10 +61,10 @@ async def build_context(
build_context("memory://features/knowledge-graph", timeframe="3 months ago")
"""
logger.info(f"Building context from {url}")
memory_url = MemoryUrl.validate(url)
url = normalize_memory_url(url)
response = await call_get(
client,
f"/memory/{memory_url.relative_path()}",
f"/memory/{memory_url_path(url)}",
params={"depth": depth, "timeframe": timeframe, "max_results": max_results},
)
return GraphContext.model_validate(response.json())
+46 -41
View File
@@ -1,54 +1,59 @@
"""Schemas for memory context."""
from datetime import datetime
from typing import Dict, List, Any, Optional
from typing import Dict, List, Any, Optional, Annotated
from pydantic import BaseModel, field_validator, Field
import pydantic
from annotated_types import MinLen, MaxLen
from pydantic import BaseModel, field_validator, Field, BeforeValidator, TypeAdapter, AnyUrl
from basic_memory.schemas.search import SearchItemType
class MemoryUrl(BaseModel):
"""memory:// URL scheme for knowledge addressing.
Example URLs:
memory://specs/search # Direct reference
memory://specs/search/* # Pattern matching
memory://related/xyz # Special lookup
def normalize_memory_url(url: str) -> str:
"""Normalize a MemoryUrl string.
Args:
url: A path like "specs/search" or "memory://specs/search"
Returns:
Normalized URL starting with memory://
Examples:
>>> normalize_memory_url("specs/search")
'memory://specs/search'
>>> normalize_memory_url("memory://specs/search")
'memory://specs/search'
"""
url: str
path: str = ""
@field_validator("url")
@classmethod
def validate_url(cls, v: str) -> str:
"""Validate the URL starts with memory://."""
if isinstance(v, MemoryUrl):
return v.url
if not isinstance(v, str):
raise ValueError(f"URL must be a string, got {type(v)}")
if not v.startswith("memory://"):
raise ValueError(f"Invalid memory URL: {v}. Must start with memory://")
return v
def __init__(self, url: str, **data):
if isinstance(url, MemoryUrl):
url = url.url
super().__init__(url=url, **data)
self.path = url.removeprefix("memory://")
@classmethod
def validate(cls, url: str) -> "MemoryUrl":
"""Validate and construct a MemoryUrl."""
return cls(url=url)
clean_path = url.removeprefix("memory://")
return f"memory://{clean_path}"
def relative_path(self) -> str:
"""Get the path."""
return self.path
def __str__(self) -> str:
"""Convert back to URL string."""
return self.url
MemoryUrl = Annotated[
str,
BeforeValidator(str.strip), # Clean whitespace
MinLen(1),
MaxLen(2028),
]
memory_url = TypeAdapter(MemoryUrl)
def memory_url_path(url: memory_url) -> str:
"""
Returns the uri for a url value by removing the prefix "memory://" from a given MemoryUrl.
This function processes a given MemoryUrl by removing the "memory://"
prefix and returns the resulting string. If the provided url does not
begin with "memory://", the function will simply return the input url
unchanged.
:param url: A MemoryUrl object representing the URL with a "memory://" prefix.
:type url: MemoryUrl
:return: A string representing the URL with the "memory://" prefix removed.
:rtype: str
"""
return url.removeprefix("memory://")
class EntitySummary(BaseModel):
@@ -66,7 +71,7 @@ class RelationSummary(BaseModel):
permalink: str
type: str
from_id: str
to_id: str
to_id: Optional[str] = None
class ObservationSummary(BaseModel):
+9 -8
View File
@@ -9,7 +9,7 @@ from sqlalchemy import text
from basic_memory.repository.entity_repository import EntityRepository
from basic_memory.repository.search_repository import SearchRepository
from basic_memory.schemas.memory import MemoryUrl
from basic_memory.schemas.memory import MemoryUrl, memory_url_path
from basic_memory.schemas.search import SearchItemType
@@ -62,17 +62,18 @@ class ContextService:
)
if memory_url:
path = memory_url_path(memory_url)
# Pattern matching - use search
if "*" in memory_url.relative_path():
logger.debug(f"Pattern search for '{memory_url.relative_path()}'")
if "*" in path:
logger.debug(f"Pattern search for '{path}'")
primary = await self.search_repository.search(
permalink_match=memory_url.relative_path()
permalink_match=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())
logger.debug(f"Direct lookup for '{path}'")
primary = await self.search_repository.search(permalink=path)
else:
logger.debug(f"Build context for '{types}'")
primary = await self.search_repository.search(types=types)
@@ -95,7 +96,7 @@ class ContextService:
"primary_results": primary,
"related_results": related,
"metadata": {
"uri": memory_url.relative_path() if memory_url else None,
"uri": memory_url_path(memory_url) if memory_url else None,
"types": types if types else None,
"depth": depth,
"timeframe": since.isoformat() if since else None,