- Agent dropdown UI (chip button + hidden select) in composer header - Session.agent field persists agent selection across refresh - soul.md loaded per-agent via ephemeral_system_prompt injection - ChromaDB memory filtered by agent topic (lotus/, sunflower/, etc.) - Fixed streaming.py: agent→_ai_agent variable shadowing (lines 1161, 1163) - New API endpoints: /api/agents/topology, /api/agents/memory/search - Agent metadata registry with emoji, name, description per Tier-2 agent
130 lines
4.3 KiB
Python
130 lines
4.3 KiB
Python
"""
|
|
Phase 5 — Memory Search (ChromaDB)
|
|
Appended to agents.py functions for Memory Search.
|
|
"""
|
|
import chromadb
|
|
import os
|
|
from pathlib import Path
|
|
|
|
HERMES_HOME = Path(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")))
|
|
|
|
|
|
def _get_agent_soul(agent_id: str) -> str | None:
|
|
"""
|
|
Load soul.md for a specific agent.
|
|
|
|
Searches in this order:
|
|
1. ~/.hermes/agents/{agent_id}/soul.md
|
|
2. ~/.hermes/agents/{agent_id}/SOUL.md
|
|
|
|
Returns None if not found.
|
|
"""
|
|
if not agent_id or agent_id == "rose":
|
|
return None # Rose uses the global HERMES_HOME/SOUL.md
|
|
|
|
for fname in ("soul.md", "SOUL.md"):
|
|
path = HERMES_HOME / "agents" / agent_id / fname
|
|
if path.exists():
|
|
try:
|
|
content = path.read_text(encoding="utf-8").strip()
|
|
if content:
|
|
return content
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def _get_agent_memory_context(agent_id: str, query: str, limit: int = 5) -> str | None:
|
|
"""
|
|
Build a memory context string by searching ChromaDB for the agent's memories.
|
|
|
|
Searches rose_memory collection filtered by topic matching "{agent_id}/".
|
|
Returns formatted text block or None if nothing found.
|
|
"""
|
|
if not agent_id or agent_id == "rose":
|
|
return None
|
|
|
|
matches = _search_agent_memory(agent_id, query, limit=limit)
|
|
if not matches:
|
|
return None
|
|
|
|
blocks = []
|
|
for m in matches:
|
|
blocks.append(f"## {m['topic']}\n{m['content'][:300]}")
|
|
return "\n\n".join(blocks) if blocks else None
|
|
|
|
|
|
def _get_chroma_client():
|
|
"""Get or create the shared ChromaDB HTTP client (thread-safe singleton)."""
|
|
if not hasattr(_get_chroma_client, "_client"):
|
|
_get_chroma_client._client = chromadb.HttpClient(host="127.0.0.1", port=8000)
|
|
return _get_chroma_client._client
|
|
|
|
|
|
def _search_agent_memory(agent_id: str, query: str, limit: int = 10) -> list:
|
|
"""
|
|
Search memory for a specific agent.
|
|
Searches the rose_memory collection filtered by topic matching agent_id.
|
|
"""
|
|
try:
|
|
client = _get_chroma_client()
|
|
coll = client.get_collection(name="rose_memory")
|
|
results = coll.query(
|
|
query_texts=[query],
|
|
n_results=limit,
|
|
include=["metadatas", "documents"],
|
|
)
|
|
matches = []
|
|
for i, doc in enumerate(results.get("documents", [[]])[0] or []):
|
|
meta = (results.get("metadatas", [[{}]])[0] or [{}])[i] or {}
|
|
topic = meta.get("topic", "")
|
|
# Filter: only docs that belong to this agent (topic starts with agent_id)
|
|
if not topic.startswith(agent_id):
|
|
continue
|
|
matches.append({
|
|
"id": (results.get("ids", [["?"]])[0] or ["?"])[i],
|
|
"topic": topic,
|
|
"content": doc,
|
|
"confidence": float(meta.get("confidence", 0.0)),
|
|
"tags": meta.get("tags", ""),
|
|
"vault_path": meta.get("vault_path", ""),
|
|
})
|
|
return matches
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
def _search_all_agents_memory(query: str, limit: int = 20) -> list:
|
|
"""
|
|
Search across all agent memories in ChromaDB.
|
|
Returns matches with agent attribution from topic.
|
|
Topic format: "agent-name/fact-name" or flat topic name.
|
|
"""
|
|
try:
|
|
client = _get_chroma_client()
|
|
coll = client.get_collection(name="rose_memory")
|
|
results = coll.query(
|
|
query_texts=[query],
|
|
n_results=limit,
|
|
include=["metadatas", "documents"],
|
|
)
|
|
matches = []
|
|
for i, doc in enumerate(results.get("documents", [[]])[0] or []):
|
|
meta = (results.get("metadatas", [[{}]])[0] or [{}])[i] or {}
|
|
topic = meta.get("topic", "")
|
|
# Extract agent from topic: "agent-name/ffact" -> agent
|
|
parts = topic.split("/")
|
|
agent = parts[0] if len(parts) > 1 else topic
|
|
matches.append({
|
|
"id": (results.get("ids", [["?"]])[0] or ["?"])[i],
|
|
"topic": topic,
|
|
"agent": agent,
|
|
"content": doc,
|
|
"confidence": float(meta.get("confidence", 0.0)),
|
|
"tags": meta.get("tags", ""),
|
|
"vault_path": meta.get("vault_path", ""),
|
|
})
|
|
return matches
|
|
except Exception:
|
|
return []
|