""" 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: return None 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: 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 []