237 lines
7.1 KiB
Python
237 lines
7.1 KiB
Python
|
|
"""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")
|