docs(profiles): add comprehensive docstrings for profile backend and handler methods

- Added documentation for get_all_sync, get_by_sync, delete_sync, delete_all_sync methods
- Documented add_sync and add_batch_sync functionality with deduping behavior
- Added docstrings for update_sync and search_sync operations
- Updated ProfileHandler.format_node method with proper documentation
- Refactored private _format_node to public format_node method
- Added comprehensive documentation for profile vector handler operations
- Documented _vector_profile_matches, _get_by_profile_id, _get_by_profile_key helper methods
- Added docstrings for retrieve_profile functionality and formatting methods
This commit is contained in:
方应 2026-04-30 10:10:26 +08:00
parent 03259729c1
commit 26017e4044
4 changed files with 62 additions and 28 deletions

View file

@ -42,11 +42,13 @@ class FileProfileBackend(BaseProfileBackend):
logger.info(f"Saved {len(nodes)} profiles to {self.cache_key}")
def get_all_sync(self) -> list[MemoryNode]:
"""Load all profile nodes from cache, ordered by ``message_time``."""
nodes = self._load_nodes()
nodes.sort(key=lambda n: n.message_time)
return nodes
def get_by_sync(self, *, profile_id: str | None = None, profile_key: str | None = None) -> MemoryNode | None:
"""Return the first node matching ``profile_id`` or ``profile_key``."""
if not profile_id and not profile_key:
raise ValueError("Must provide either profile_id or profile_key")
@ -58,6 +60,7 @@ class FileProfileBackend(BaseProfileBackend):
return None
def delete_sync(self, profile_id: str | list[str]) -> bool | int:
"""Remove one id, many ids, or none; returns bool, count, or 0/false if nothing removed."""
nodes = self._load_nodes()
original_count = len(nodes)
@ -83,6 +86,7 @@ class FileProfileBackend(BaseProfileBackend):
return True
def delete_all_sync(self) -> int:
"""Clear every cached profile for this target; returns how many were stored."""
nodes = self._load_nodes()
count = len(nodes)
self._save_nodes([], apply_limits=False)
@ -90,6 +94,7 @@ class FileProfileBackend(BaseProfileBackend):
return count
def add_sync(self, message_time: str, profile_key: str, profile_value: str, ref_memory_id: str = "") -> MemoryNode:
"""Append a profile row, replacing any existing row with the same key."""
nodes = self._load_nodes()
new_node = MemoryNode(
@ -112,6 +117,7 @@ class FileProfileBackend(BaseProfileBackend):
return new_node
def add_batch_sync(self, profiles: list[dict], ref_memory_id: str = "") -> list[MemoryNode]:
"""Insert many profiles in one write, deduping by key against existing rows."""
if not profiles:
return []
@ -146,6 +152,7 @@ class FileProfileBackend(BaseProfileBackend):
profile_key: str,
profile_value: str,
) -> MemoryNode | None:
"""Update fields for ``profile_id``; return ``None`` if that id is missing."""
nodes = self._load_nodes()
target_node = None
for node in nodes:
@ -165,6 +172,7 @@ class FileProfileBackend(BaseProfileBackend):
return target_node
def search_sync(self, query: str | list[str], limit: int = 5) -> list[MemoryNode]:
"""Simple substring/token match over key and content, best matches first."""
queries = [query] if isinstance(query, str) else query
query_terms = [q.strip().lower() for q in queries if q and q.strip()]
if not query_terms:

View file

@ -115,7 +115,8 @@ class ProfileHandler:
return await self.backend.search(query=query, limit=limit)
@staticmethod
def _format_node(node: MemoryNode, add_profile_id: bool = False, add_history_id: bool = False) -> str:
def format_node(node: MemoryNode, add_profile_id: bool = False, add_history_id: bool = False) -> str:
"""Render a profile ``MemoryNode`` as a single-line string for tools/logs."""
parts = []
profile_key = str(node.metadata.get("profile_key", node.when_to_use))
@ -134,7 +135,7 @@ class ProfileHandler:
async def aread_all(self, add_profile_id: bool = False, add_history_id: bool = False) -> str:
nodes = await self.aget_all()
formatted_profiles = [self._format_node(node, add_profile_id, add_history_id) for node in nodes]
formatted_profiles = [self.format_node(node, add_profile_id, add_history_id) for node in nodes]
logger.info(f"Read {len(formatted_profiles)} profiles from {self.cache_key}")
return "\n".join(formatted_profiles).strip()
@ -146,7 +147,7 @@ class ProfileHandler:
add_history_id: bool = False,
) -> tuple[list[MemoryNode], str]:
nodes = await self.asearch(query=query, limit=limit)
formatted_profiles = [self._format_node(node, add_profile_id, add_history_id) for node in nodes]
formatted_profiles = [self.format_node(node, add_profile_id, add_history_id) for node in nodes]
return nodes, "\n".join(formatted_profiles).strip()
def delete(self, profile_id: str | list[str]) -> bool | int:

View file

@ -39,6 +39,7 @@ class ProfileVectorHandler:
return hash_obj.hexdigest()[:16]
def _base_filters(self) -> dict:
"""Filters shared by all profile rows in the vector collection."""
return {
"memory_type": MemoryType.IDENTITY.value,
"memory_target": self.memory_target,
@ -46,6 +47,7 @@ class ProfileVectorHandler:
}
def _build_profile_node(self, profile: dict, ref_memory_id: str = "") -> MemoryNode:
"""Turn a profile dict into a ``MemoryNode`` for upsert into the vector store."""
profile_key = profile.get("profile_key", "").strip()
profile_value = profile.get("profile_value", "").strip()
message_time = profile.get("message_time", "")
@ -69,7 +71,41 @@ class ProfileVectorHandler:
metadata=metadata,
)
def _vector_profile_matches(self, memory_node: MemoryNode) -> bool:
"""True if ``memory_node`` belongs to this handler's target and profile kind."""
if memory_node.memory_target != self.memory_target:
return False
if memory_node.memory_type is not MemoryType.IDENTITY:
return False
if memory_node.metadata.get("profile_kind") != self.PROFILE_KIND:
return False
return True
async def _get_by_profile_id(self, profile_id: str) -> MemoryNode | None:
"""Load by vector id and validate filters."""
try:
vector_node = await self.vector_store.get(profile_id)
except KeyError:
logger.warning(f"Profile {profile_id} not found in vector store")
return None
if vector_node is None:
logger.warning(f"Profile {profile_id} not found in vector store")
return None
memory_node = MemoryNode.from_vector_node(vector_node)
if not self._vector_profile_matches(memory_node):
return None
return memory_node
async def _get_by_profile_key(self, profile_key: str) -> MemoryNode | None:
"""Load the single row matching ``profile_key`` under base filters."""
filters = {**self._base_filters(), "profile_key": profile_key}
vector_nodes = await self.vector_store.list(filters=filters, limit=1)
if not vector_nodes:
return None
return MemoryNode.from_vector_node(vector_nodes[0])
async def get_all(self) -> list[MemoryNode]:
"""List every profile row for this memory target, sorted by store."""
vector_nodes = await self.vector_store.list(
filters=self._base_filters(),
sort_key="message_time",
@ -78,34 +114,15 @@ class ProfileVectorHandler:
return [MemoryNode.from_vector_node(node) for node in vector_nodes]
async def get_by(self, *, profile_id: str | None = None, profile_key: str | None = None) -> MemoryNode | None:
"""Return one profile by stable id or by logical profile key."""
if not profile_id and not profile_key:
raise ValueError("Must provide either profile_id or profile_key")
if profile_id:
try:
vector_node = await self.vector_store.get(profile_id)
except KeyError:
logger.warning(f"Profile {profile_id} not found in vector store")
return None
if vector_node is None:
logger.warning(f"Profile {profile_id} not found in vector store")
return None
memory_node = MemoryNode.from_vector_node(vector_node)
if memory_node.memory_target != self.memory_target:
return None
if memory_node.memory_type is not MemoryType.IDENTITY:
return None
if memory_node.metadata.get("profile_kind") != self.PROFILE_KIND:
return None
return memory_node
profile_key = profile_key or ""
vector_nodes = await self.vector_store.list(filters={**self._base_filters(), "profile_key": profile_key}, limit=1)
if not vector_nodes:
return None
return MemoryNode.from_vector_node(vector_nodes[0])
return await self._get_by_profile_id(profile_id)
return await self._get_by_profile_key(profile_key or "")
async def delete(self, profile_id: str | list[str]) -> bool | int:
"""Delete one id, many ids, or report zero/false when nothing matched."""
if isinstance(profile_id, list):
profile_ids = list(dict.fromkeys(pid for pid in profile_id if pid))
if not profile_ids:
@ -127,6 +144,7 @@ class ProfileVectorHandler:
return True
async def delete_all(self) -> int:
"""Remove all profile vectors for this target; returns how many were deleted."""
nodes = await self.get_all()
if not nodes:
return 0
@ -134,6 +152,7 @@ class ProfileVectorHandler:
return len(nodes)
async def add_batch(self, profiles: list[dict], ref_memory_id: str = "") -> list[MemoryNode]:
"""Upsert many profiles at once (last dict wins per key), then enforce capacity."""
if not profiles:
return []
@ -144,7 +163,9 @@ class ProfileVectorHandler:
continue
deduped_profiles[profile_key] = profile
new_nodes = [self._build_profile_node(profile, ref_memory_id=ref_memory_id) for profile in deduped_profiles.values()]
new_nodes = [
self._build_profile_node(profile, ref_memory_id=ref_memory_id) for profile in deduped_profiles.values()
]
if not new_nodes:
return []
@ -154,6 +175,7 @@ class ProfileVectorHandler:
return new_nodes
async def add(self, message_time: str, profile_key: str, profile_value: str, ref_memory_id: str = "") -> MemoryNode:
"""Insert or replace a single profile row."""
nodes = await self.add_batch(
[
{
@ -173,6 +195,7 @@ class ProfileVectorHandler:
profile_key: str,
profile_value: str,
) -> MemoryNode | None:
"""Replace content and key for ``profile_id``; return ``None`` if missing."""
existing_node = await self.get_by(profile_id=profile_id)
if existing_node is None:
return None
@ -197,6 +220,7 @@ class ProfileVectorHandler:
return new_node
async def search(self, query: str | list[str], limit: int = 5) -> list[MemoryNode]:
"""Semantic search with de-duplication across multiple query strings."""
queries = [query] if isinstance(query, str) else query
seen_nodes: dict[str, MemoryNode] = {}
for item in queries:
@ -211,6 +235,7 @@ class ProfileVectorHandler:
return nodes[:limit]
async def enforce_capacity(self):
"""Drop oldest rows when count exceeds ``max_capacity``."""
nodes = await self.get_all()
overflow = len(nodes) - self.max_capacity
if overflow <= 0:

View file

@ -95,7 +95,7 @@ class RetrieveProfile(BaseMemoryTool):
output = "No new profiles found."
else:
output = "\n".join(
[ProfileHandler._format_node(node, add_profile_id=True, add_history_id=True) for node in new_nodes],
[ProfileHandler.format_node(node, add_profile_id=True, add_history_id=True) for node in new_nodes],
)
logger.info(f"Retrieved {len(profile_nodes)} profiles, {len(new_nodes)} new after deduplication")