mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
c5c70cb0f4
Signed-off-by: phernandez <paul@basicmachines.co>
198 lines
5.7 KiB
Python
198 lines
5.7 KiB
Python
"""Schemas for memory context."""
|
|
|
|
from datetime import datetime
|
|
from typing import List, Optional, Annotated, Sequence
|
|
|
|
from annotated_types import MinLen, MaxLen
|
|
from pydantic import BaseModel, Field, BeforeValidator, TypeAdapter
|
|
|
|
from basic_memory.schemas.search import SearchItemType
|
|
|
|
|
|
def validate_memory_url_path(path: str) -> bool:
|
|
"""Validate that a memory URL path is well-formed.
|
|
|
|
Args:
|
|
path: The path part of a memory URL (without memory:// prefix)
|
|
|
|
Returns:
|
|
True if the path is valid, False otherwise
|
|
|
|
Examples:
|
|
>>> validate_memory_url_path("specs/search")
|
|
True
|
|
>>> validate_memory_url_path("memory//test") # Double slash
|
|
False
|
|
>>> validate_memory_url_path("invalid://test") # Contains protocol
|
|
False
|
|
"""
|
|
if not path or not path.strip():
|
|
return False
|
|
|
|
# Check for invalid protocol schemes within the path first (more specific)
|
|
if "://" in path:
|
|
return False
|
|
|
|
# Check for double slashes (except at the beginning for absolute paths)
|
|
if "//" in path:
|
|
return False
|
|
|
|
# Check for invalid characters (excluding * which is used for pattern matching)
|
|
invalid_chars = {"<", ">", '"', "|", "?"}
|
|
if any(char in path for char in invalid_chars):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def normalize_memory_url(url: str | None) -> str:
|
|
"""Normalize a MemoryUrl string with validation.
|
|
|
|
Args:
|
|
url: A path like "specs/search" or "memory://specs/search"
|
|
|
|
Returns:
|
|
Normalized URL starting with memory://
|
|
|
|
Raises:
|
|
ValueError: If the URL path is malformed
|
|
|
|
Examples:
|
|
>>> normalize_memory_url("specs/search")
|
|
'memory://specs/search'
|
|
>>> normalize_memory_url("memory://specs/search")
|
|
'memory://specs/search'
|
|
>>> normalize_memory_url("memory//test")
|
|
Traceback (most recent call last):
|
|
...
|
|
ValueError: Invalid memory URL path: 'memory//test' contains double slashes
|
|
"""
|
|
if not url:
|
|
return ""
|
|
|
|
clean_path = url.removeprefix("memory://")
|
|
|
|
# Validate the extracted path
|
|
if not validate_memory_url_path(clean_path):
|
|
# Provide specific error messages for common issues
|
|
if "://" in clean_path:
|
|
raise ValueError(f"Invalid memory URL path: '{clean_path}' contains protocol scheme")
|
|
elif "//" in clean_path:
|
|
raise ValueError(f"Invalid memory URL path: '{clean_path}' contains double slashes")
|
|
elif not clean_path.strip():
|
|
raise ValueError("Memory URL path cannot be empty or whitespace")
|
|
else:
|
|
raise ValueError(f"Invalid memory URL path: '{clean_path}' contains invalid characters")
|
|
|
|
return f"memory://{clean_path}"
|
|
|
|
|
|
MemoryUrl = Annotated[
|
|
str,
|
|
BeforeValidator(str.strip), # Clean whitespace
|
|
BeforeValidator(normalize_memory_url), # Validate and normalize the URL
|
|
MinLen(1),
|
|
MaxLen(2028),
|
|
]
|
|
|
|
memory_url = TypeAdapter(MemoryUrl)
|
|
|
|
|
|
def memory_url_path(url: memory_url) -> str: # pyright: ignore
|
|
"""
|
|
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):
|
|
"""Simplified entity representation."""
|
|
|
|
type: str = "entity"
|
|
permalink: Optional[str]
|
|
title: str
|
|
content: Optional[str] = None
|
|
file_path: str
|
|
created_at: datetime
|
|
|
|
|
|
class RelationSummary(BaseModel):
|
|
"""Simplified relation representation."""
|
|
|
|
type: str = "relation"
|
|
title: str
|
|
file_path: str
|
|
permalink: str
|
|
relation_type: str
|
|
from_entity: str
|
|
to_entity: Optional[str] = None
|
|
created_at: datetime
|
|
|
|
|
|
class ObservationSummary(BaseModel):
|
|
"""Simplified observation representation."""
|
|
|
|
type: str = "observation"
|
|
title: str
|
|
file_path: str
|
|
permalink: str
|
|
category: str
|
|
content: str
|
|
created_at: datetime
|
|
|
|
|
|
class MemoryMetadata(BaseModel):
|
|
"""Simplified response metadata."""
|
|
|
|
uri: Optional[str] = None
|
|
types: Optional[List[SearchItemType]] = None
|
|
depth: int
|
|
timeframe: Optional[str] = None
|
|
generated_at: datetime
|
|
primary_count: Optional[int] = None # Changed field name
|
|
related_count: Optional[int] = None # Changed field name
|
|
total_results: Optional[int] = None # For backward compatibility
|
|
total_relations: Optional[int] = None
|
|
total_observations: Optional[int] = None
|
|
|
|
|
|
class ContextResult(BaseModel):
|
|
"""Context result containing a primary item with its observations and related items."""
|
|
|
|
primary_result: EntitySummary | RelationSummary | ObservationSummary = Field(
|
|
description="Primary item"
|
|
)
|
|
|
|
observations: Sequence[ObservationSummary] = Field(
|
|
description="Observations belonging to this entity", default_factory=list
|
|
)
|
|
|
|
related_results: Sequence[EntitySummary | RelationSummary | ObservationSummary] = Field(
|
|
description="Related items", default_factory=list
|
|
)
|
|
|
|
|
|
class GraphContext(BaseModel):
|
|
"""Complete context response."""
|
|
|
|
# hierarchical results
|
|
results: Sequence[ContextResult] = Field(
|
|
description="Hierarchical results with related items nested", default_factory=list
|
|
)
|
|
|
|
# Context metadata
|
|
metadata: MemoryMetadata
|
|
|
|
page: Optional[int] = None
|
|
page_size: Optional[int] = None
|