mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-14 23:21:04 +00:00
199 lines
7.5 KiB
Python
199 lines
7.5 KiB
Python
"""Profile Handler for managing user profiles in local memory"""
|
|
|
|
from pathlib import Path
|
|
|
|
from loguru import logger
|
|
|
|
from ...core.enumeration import MemoryType
|
|
from ...core.schema import MemoryNode
|
|
from ...core.utils import CacheHandler, deduplicate_memories
|
|
|
|
|
|
class ProfileHandler:
|
|
"""User profile CRUD handler"""
|
|
|
|
def __init__(self, profile_path: str | Path, memory_target: str, max_capacity: int = 100):
|
|
"""init"""
|
|
self.memory_target: str = memory_target
|
|
self.cache_key: str = self.memory_target.replace(" ", "_").lower()
|
|
self.cache_handler: CacheHandler = CacheHandler(profile_path)
|
|
self.max_capacity: int = max_capacity
|
|
|
|
def _load_nodes(self) -> list[MemoryNode]:
|
|
"""Load profile nodes"""
|
|
cached_data = self.cache_handler.load(self.cache_key, auto_clean=False)
|
|
if not cached_data:
|
|
return []
|
|
return [MemoryNode(**data) for data in cached_data]
|
|
|
|
def _save_nodes(self, nodes: list[MemoryNode], apply_limits: bool = True):
|
|
"""Save nodes with optional deduplication and capacity enforcement"""
|
|
if apply_limits:
|
|
nodes = deduplicate_memories(nodes)
|
|
|
|
# Enforce capacity limit by removing the oldest profiles
|
|
if len(nodes) > self.max_capacity:
|
|
sorted_nodes = sorted(nodes, key=lambda n: n.message_time)
|
|
removed_count = len(sorted_nodes) - self.max_capacity
|
|
nodes = sorted_nodes[removed_count:]
|
|
logger.info(
|
|
f"Capacity limit reached: removed {removed_count} oldest profiles "
|
|
f"(kept {len(nodes)}/{self.max_capacity})",
|
|
)
|
|
|
|
nodes_data = [node.model_dump(exclude_none=True) for node in nodes]
|
|
self.cache_handler.save(self.cache_key, nodes_data)
|
|
logger.info(f"Saved {len(nodes)} profiles to {self.cache_key}")
|
|
|
|
def delete(self, profile_id: str | list[str]) -> bool | int:
|
|
"""Delete profile by ID(s), returns True/False for single ID or count for batch delete"""
|
|
nodes = self._load_nodes()
|
|
original_count = len(nodes)
|
|
|
|
# Batch delete mode
|
|
if isinstance(profile_id, list):
|
|
profile_ids_set = set(profile_id)
|
|
nodes = [n for n in nodes if n.memory_id not in profile_ids_set]
|
|
deleted_count = original_count - len(nodes)
|
|
|
|
if deleted_count == 0:
|
|
logger.warning(f"No profiles found to delete from {len(profile_id)} IDs")
|
|
return 0
|
|
|
|
self._save_nodes(nodes, apply_limits=False)
|
|
logger.info(f"Batch deleted {deleted_count} profiles")
|
|
return deleted_count
|
|
|
|
# Single delete mode
|
|
nodes = [n for n in nodes if n.memory_id != profile_id]
|
|
|
|
if len(nodes) == original_count:
|
|
logger.warning(f"Profile {profile_id} not found")
|
|
return False
|
|
|
|
self._save_nodes(nodes, apply_limits=False)
|
|
logger.info(f"Deleted profile {profile_id}")
|
|
return True
|
|
|
|
def delete_all(self) -> int:
|
|
"""Delete all profiles, returns count deleted"""
|
|
nodes = self._load_nodes()
|
|
count = len(nodes)
|
|
self._save_nodes([], apply_limits=False)
|
|
logger.info(f"Deleted all {count} profiles")
|
|
return count
|
|
|
|
def add(self, message_time: str, profile_key: str, profile_value: str, ref_memory_id: str = "") -> MemoryNode:
|
|
"""Add new profile, returns created MemoryNode"""
|
|
nodes = self._load_nodes()
|
|
|
|
new_node = MemoryNode(
|
|
memory_type=MemoryType.PERSONAL,
|
|
memory_target=self.memory_target,
|
|
when_to_use=profile_key,
|
|
content=profile_value,
|
|
message_time=message_time,
|
|
ref_memory_id=ref_memory_id,
|
|
)
|
|
|
|
nodes.append(new_node)
|
|
self._save_nodes(nodes)
|
|
logger.info(f"Added profile: {profile_key}={profile_value}")
|
|
return new_node
|
|
|
|
def add_batch(self, profiles: list[dict], ref_memory_id: str = "") -> list[MemoryNode]:
|
|
"""Add multiple profiles in batch, returns list of created MemoryNodes"""
|
|
if not profiles:
|
|
return []
|
|
|
|
nodes = self._load_nodes()
|
|
|
|
new_nodes = [
|
|
MemoryNode(
|
|
memory_type=MemoryType.PERSONAL,
|
|
memory_target=self.memory_target,
|
|
when_to_use=p.get("profile_key", ""),
|
|
content=p.get("profile_value", ""),
|
|
message_time=p.get("message_time", ""),
|
|
ref_memory_id=ref_memory_id,
|
|
)
|
|
for p in profiles
|
|
]
|
|
|
|
nodes.extend(new_nodes)
|
|
self._save_nodes(nodes)
|
|
logger.info(f"Batch added {len(new_nodes)} profiles")
|
|
return new_nodes
|
|
|
|
def update(self, profile_id: str, message_time: str, profile_key: str, profile_value: str) -> MemoryNode | None:
|
|
"""Update profile by ID, returns updated node or None if not found"""
|
|
nodes = self._load_nodes()
|
|
|
|
target_node = None
|
|
for node in nodes:
|
|
if node.memory_id == profile_id:
|
|
node.when_to_use = profile_key
|
|
node.content = profile_value
|
|
node.message_time = message_time
|
|
target_node = node
|
|
break
|
|
|
|
if target_node is None:
|
|
logger.warning(f"Profile {profile_id} not found")
|
|
return None
|
|
|
|
self._save_nodes(nodes, apply_limits=False)
|
|
logger.info(f"Updated profile {profile_id}: {profile_key}={profile_value}")
|
|
return target_node
|
|
|
|
def get_by(self, *, profile_id: str | None = None, profile_key: str | None = None) -> MemoryNode | None:
|
|
"""Get profile by ID or key"""
|
|
if not profile_id and not profile_key:
|
|
raise ValueError("Must provide either profile_id or profile_key")
|
|
|
|
nodes = self._load_nodes()
|
|
for node in nodes:
|
|
if profile_id and node.memory_id == profile_id:
|
|
return node
|
|
if profile_key and node.when_to_use == profile_key:
|
|
return node
|
|
return None
|
|
|
|
def get_by_id(self, profile_id: str) -> MemoryNode | None:
|
|
"""Get profile by ID (convenience method)"""
|
|
return self.get_by(profile_id=profile_id)
|
|
|
|
def get_by_key(self, profile_key: str) -> MemoryNode | None:
|
|
"""Get profile by key (convenience method)"""
|
|
return self.get_by(profile_key=profile_key)
|
|
|
|
def get_all(self) -> list[MemoryNode]:
|
|
"""Get all profiles, sorted by message_time"""
|
|
nodes = self._load_nodes()
|
|
nodes.sort(key=lambda n: n.message_time)
|
|
return nodes
|
|
|
|
@staticmethod
|
|
def _format_node(node: MemoryNode, add_profile_id: bool = False, add_history_id: bool = False) -> str:
|
|
"""Format a single node to string"""
|
|
parts = []
|
|
|
|
if add_profile_id:
|
|
parts.append(f"profile_id={node.memory_id}")
|
|
|
|
if node.message_time:
|
|
parts.append(f"[{node.message_time}]")
|
|
|
|
parts.append(f"{node.when_to_use}: {node.content}")
|
|
|
|
if add_history_id:
|
|
parts.append(f"history_id={node.ref_memory_id}")
|
|
|
|
return " ".join(parts)
|
|
|
|
def read_all(self, add_profile_id: bool = False, add_history_id: bool = False) -> str:
|
|
"""Read all profiles and return formatted string"""
|
|
nodes = self.get_all()
|
|
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()
|