Phase 4-6: Message Bus, Memory Search (ChromaDB), Token Tracking, Topology Graph

Phase 4: Message Bus Viewer
- Backend: get_message_bus_status(), send_bus_message() in agents.py
- Route: GET /api/agents/message-bus, POST /api/agents/{id}/bus-message
- Frontend: Message Bus tab in agent detail overlay

Phase 5: Memory Search (ChromaDB)
- Backend: _search_agent_memory(), _search_all_agents_memory() via ChromaDB rose_memory collection
- Route: GET /api/agents/memory/search, GET /api/agents/{id}/memory/search
- Frontend: Search bar added to Memory tab, renders confidence scores + topics

Phase 6: Token Tracking + Topology Graph
- Backend: get_agent_usage() reads ~/.hermes/agents/{id}/usage.json
- Route: GET /api/agents/{id}/usage
- Frontend: Usage tab with today/week/month token counts and cost
- Frontend: Topology tab with SVG radial graph of agent network
This commit is contained in:
Rose
2026-04-20 14:45:11 +02:00
parent 8b8a507ace
commit 00045314f8
5 changed files with 812 additions and 18 deletions

View File

@@ -859,6 +859,13 @@ def handle_get(handler, parsed) -> bool:
limit = int(parse_qs(parsed.query).get("limit", ["20"])[0])
return j(handler, _agents.get_agent_errors(agent_id, limit=limit))
# GET /api/agents/{id}/usage
if parsed.path.startswith("/api/agents/") and "/usage" in parsed.path:
parts = parsed.path.split("/")
if len(parts) == 5 and parts[4] == "usage":
agent_id = parts[3]
return j(handler, _agents.get_agent_usage(agent_id))
# GET /api/agents/{id}/chat-history
if parsed.path.startswith("/api/agents/") and "/chat-history" in parsed.path:
parts = parsed.path.split("/")
@@ -881,6 +888,38 @@ def handle_get(handler, parsed) -> bool:
agent_id = parts[3]
return j(handler, _agents.get_agent_tasks(agent_id))
# GET /api/agents/message-bus — all inboxes at once
if parsed.path == "/api/agents/message-bus":
return j(handler, _agents.get_message_bus_status())
# POST /api/agents/{id}/bus-message — send message to agent via bus
if parsed.path.startswith("/api/agents/") and "/bus-message" in parsed.path:
parts = parsed.path.split("/")
if len(parts) == 5 and parts[4] == "bus-message":
agent_id = parts[3]
data = read_body(handler)
result = _agents.send_bus_message(
to_agent=agent_id,
from_agent=data.get("from_agent", "rose"),
subject=data.get("subject", ""),
content=data.get("content", ""),
msg_type=data.get("type", "request"),
)
return j(handler, result)
# GET /api/agents/memory/search?q= — search all agents
if parsed.path == "/api/agents/memory/search":
return _handle_memory_search(handler, parsed, agent_id=None)
# GET /api/agents/{id}/memory/search?q= — search specific agent
_mem_match = None
if parsed.path.startswith("/api/agents/") and "/memory/search" in parsed.path:
parts = parsed.path.split("/")
if len(parts) == 6 and parts[5] == "search":
_mem_match = parts[3]
if _mem_match:
return _handle_memory_search(handler, parsed, agent_id=_mem_match)
# ── Profile API (GET) ──
if parsed.path == "/api/profiles":
from api.profiles import list_profiles_api, get_active_profile_name
@@ -3350,3 +3389,25 @@ def _handle_logs_read(handler, log_name):
"line_count": len(lines),
"tail_count": len(tail),
})
# ── Memory Search ─────────────────────────────────────────────────────────────
def _handle_memory_search(handler, parsed, agent_id=None) -> bytes:
"""
GET /api/agents/memory/search?q=query — all agents
GET /api/agents/{id}/memory/search?q=query — specific agent
"""
try:
qs = parse_qs(parsed.query)
query = " ".join(qs.get("q", [""])).strip()
limit = int(" ".join(qs.get("limit", ["20"])).strip())
if not query:
return bad(handler, "q parameter required")
if agent_id:
results = _agents._search_agent_memory(agent_id, query, limit=limit)
else:
results = _agents._search_all_agents_memory(query, limit=limit)
return j(handler, {"query": query, "results": results, "count": len(results)})
except Exception as e:
return bad(handler, str(e))