"""Agent memory facade — unified API for persistent agent memory. Delegates to the ``agent_memory`` plugin (pgvector semantic search) and provides the canonical function signatures used by the agent framework (``store_agent_memory``, ``retrieve_agent_memory``, ``search_agent_memory``). Used by: - ``app/ai/agent_loop.py`` — memory retrieval during ReAct loops - ``app/plugins/builtins/automation`` — agent memory tools """ from __future__ import annotations import logging import uuid from typing import Any from sqlalchemy.ext.asyncio import AsyncSession logger = logging.getLogger(__name__) # Memory types supported by the agent memory system. MEMORY_TYPES = ("observation", "preference", "fact", "context") def _memory_type(value: str | None) -> str: """Normalize a memory type to a supported value.""" if value in MEMORY_TYPES: return value return "fact" async def store_agent_memory( db: AsyncSession, tenant_id: uuid.UUID, agent_id: uuid.UUID, memory_type: str, content: str, metadata: dict[str, Any] | None = None, ) -> uuid.UUID: """Store a new agent memory with semantic embedding. Args: db: Database session. tenant_id: Tenant UUID. agent_id: Agent UUID. memory_type: One of ``observation``, ``preference``, ``fact``, ``context``. content: Memory content text. metadata: Optional metadata dict (stored as JSONB on the memory row). Returns: The UUID of the created memory. """ from app.plugins.builtins.agent_memory.services import store_memory result = await store_memory( db=db, tenant_id=tenant_id, agent_id=agent_id, content=content, memory_type=_memory_type(memory_type), ) memory_id = uuid.UUID(result["id"]) # Persist optional metadata on the memory row. if metadata: from sqlalchemy import update from app.plugins.builtins.agent_memory.models import AgentMemory await db.execute( update(AgentMemory) .where(AgentMemory.id == memory_id) .values(metadata_=metadata) ) await db.flush() return memory_id async def retrieve_agent_memory( db: AsyncSession, tenant_id: uuid.UUID, agent_id: uuid.UUID, memory_type: str | None = None, limit: int = 10, ) -> list[dict[str, Any]]: """Retrieve recent memories for an agent, optionally filtered by type. Args: db: Database session. tenant_id: Tenant UUID. agent_id: Agent UUID. memory_type: Optional memory type filter. limit: Maximum number of results (default 10). Returns: List of memory dicts, newest first. """ from sqlalchemy import select from app.plugins.builtins.agent_memory.models import AgentMemory stmt = ( select(AgentMemory) .where( AgentMemory.tenant_id == tenant_id, AgentMemory.agent_id == agent_id, AgentMemory.deleted_at.is_(None), ) .order_by(AgentMemory.created_at.desc()) .limit(limit) ) if memory_type: stmt = stmt.where(AgentMemory.memory_type == _memory_type(memory_type)) result = await db.execute(stmt) memories = result.scalars().all() return [ { "id": str(m.id), "agent_id": str(m.agent_id), "memory_type": m.memory_type, "content": m.content, "metadata": getattr(m, "metadata_", None) or {}, "created_at": m.created_at.isoformat() if m.created_at else None, } for m in memories ] async def search_agent_memory( db: AsyncSession, tenant_id: uuid.UUID, agent_id: uuid.UUID, query: str, limit: int = 5, ) -> list[dict[str, Any]]: """Semantic search over agent memories using pgvector embeddings. Falls back to recent-memory retrieval when embedding generation is unavailable (e.g. no embedding model configured). Args: db: Database session. tenant_id: Tenant UUID. agent_id: Agent UUID. query: Natural language query. limit: Maximum number of results (default 5). Returns: List of memory dicts with similarity scores, sorted by relevance. """ from app.plugins.builtins.agent_memory.services import retrieve_relevant_memories return await retrieve_relevant_memories( db=db, tenant_id=tenant_id, agent_id=agent_id, query=query, limit=limit, min_score=0.0, ) def register_agent_memory_tools(registry) -> None: """Register agent memory tools in the global ToolRegistry. Registers ``search_agent_memory`` so AI agents can query their own persistent memory during ReAct loops. """ import json async def search_agent_memory_handler( arguments: dict[str, Any], context: dict[str, Any] ) -> str: """Handle search_agent_memory tool call from an AI agent.""" from app.core.db import get_session_factory query = arguments.get("query", "") limit = arguments.get("limit", 5) agent_id_str = context.get("agent_id", "") tenant_id_str = context.get("tenant_id", "") if not query: return json.dumps({"error": "Missing query"}) try: tenant_id = uuid.UUID(tenant_id_str) if tenant_id_str else uuid.uuid4() agent_id = uuid.UUID(agent_id_str) if agent_id_str else uuid.uuid4() except (ValueError, TypeError): return json.dumps({"error": "Invalid tenant_id or agent_id"}) factory = get_session_factory() async with factory() as db: results = await search_agent_memory( db=db, tenant_id=tenant_id, agent_id=agent_id, query=query, limit=limit, ) return json.dumps({"memories": results, "count": len(results)}, default=str) registry.register( name="search_agent_memory", description=( "Semantische Suche über das persistente Gedächtnis eines Agents. " "Findet relevante frühere Beobachtungen, Fakten und Präferenzen." ), parameters={ "type": "object", "properties": { "query": { "type": "string", "description": "Natürlichsprachliche Suchanfrage", }, "limit": { "type": "integer", "default": 5, "description": "Maximale Anzahl Ergebnisse", }, }, "required": ["query"], }, handler=search_agent_memory_handler, plugin_name="agent_memory", required_permission="agent_memory:read", category="memory", ) logger.info("Agent memory tool 'search_agent_memory' registered") def unregister_agent_memory_tools(registry) -> None: """Unregister agent memory tools from the global ToolRegistry.""" registry.unregister("search_agent_memory") logger.info("Agent memory tool 'search_agent_memory' unregistered")