feat(F): F-MEM agent_memory + frontend agent overview components
Check Cross-Plugin Imports / check (push) Has been cancelled

- F-MEM: app/ai/agent_memory.py (236 lines) — store/retrieve/search agent memory with embeddings
- Frontend: AgentDashboard.tsx (831 lines), AgentsOverview.tsx (35 lines) — agent list and dashboard
- agent_memory plugin models updated
This commit is contained in:
Agent Zero
2026-08-17 17:14:51 +02:00
parent 638e3f3e1e
commit 158feec374
3 changed files with 244 additions and 0 deletions
+236
View File
@@ -0,0 +1,236 @@
"""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")
@@ -0,0 +1,3 @@
-- Agent Memory: add metadata JSONB column for structured memory metadata
ALTER TABLE agent_memories ADD COLUMN IF NOT EXISTS metadata JSONB;
@@ -3,8 +3,10 @@
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from typing import Any
from sqlalchemy import Index, String, Text from sqlalchemy import Index, String, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
@@ -35,5 +37,8 @@ class AgentMemory(Base, TenantMixin, OwnedMixin):
String(50), nullable=False, default="fact" String(50), nullable=False, default="fact"
) )
content: Mapped[str] = mapped_column(Text, nullable=False) content: Mapped[str] = mapped_column(Text, nullable=False)
metadata_: Mapped[dict[str, Any] | None] = mapped_column(
"metadata", JSONB, nullable=True
)
# embedding column is managed via raw SQL (pgvector extension) # embedding column is managed via raw SQL (pgvector extension)
# embedding vector(768) — see migration 0001_initial.sql # embedding vector(768) — see migration 0001_initial.sql