Compare commits
6 Commits
9814d18e8c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 07329d8672 | |||
| 1006d4490a | |||
| 6a259e1ef7 | |||
| bb2a4e2293 | |||
| d74d1a74f8 | |||
| 8704404c40 |
@@ -268,60 +268,50 @@ async def handle_agent0_mcp(
|
|||||||
valid_poses: list[str],
|
valid_poses: list[str],
|
||||||
) -> ChatResponse:
|
) -> ChatResponse:
|
||||||
"""
|
"""
|
||||||
Send a message to a live Agent Zero instance via its MCP streamable-http
|
Send a message to a live Agent Zero instance via its REST API
|
||||||
server and return the response as a ChatResponse.
|
(POST /api/api_message, X-API-KEY header).
|
||||||
|
|
||||||
MCP URL format: {endpoint}/mcp/t-{mcp_key}/http
|
Conversation continuity is maintained via Agent Zero's context_id, which
|
||||||
Tool called: send_message (built into every Agent Zero instance)
|
maps to a gnommoweb conversation_id in _a0_sessions (in-memory).
|
||||||
|
|
||||||
Conversation continuity is maintained via Agent Zero's chat_id, which maps
|
|
||||||
to a gnommoweb conversation_id in _a0_sessions (in-memory).
|
|
||||||
"""
|
"""
|
||||||
from mcp.client.streamable_http import streamablehttp_client
|
endpoint = (model.endpoint or "").strip().rstrip("/")
|
||||||
from mcp import ClientSession
|
api_key = model.mcp_key or "" # stored in mcp_key field; used as X-API-KEY
|
||||||
|
api_url = f"{endpoint}/api/api_message"
|
||||||
|
|
||||||
endpoint = (model.endpoint or "").rstrip("/")
|
# Retrieve existing Agent0 context_id for this conversation (if any)
|
||||||
mcp_key = model.mcp_key or ""
|
a0_context_id = _a0_sessions.get(req.conversation_id) if req.conversation_id else None
|
||||||
mcp_url = f"{endpoint}/mcp/t-{mcp_key}/http"
|
|
||||||
|
|
||||||
# Retrieve existing Agent0 chat_id for this conversation (if any)
|
|
||||||
a0_chat_id = _a0_sessions.get(req.conversation_id) if req.conversation_id else None
|
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
f"[{agent.name}] → Agent0 MCP url={mcp_url} "
|
f"[{agent.name}] → Agent0 REST url={api_url} "
|
||||||
f"conv={req.conversation_id} a0_chat={a0_chat_id or 'new'} "
|
f"conv={req.conversation_id} a0_ctx={a0_context_id or 'new'} "
|
||||||
f"msg='{req.message[:60]}'"
|
f"msg='{req.message[:60]}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with streamablehttp_client(mcp_url) as (read, write, _):
|
payload: dict = {
|
||||||
async with ClientSession(read, write) as session:
|
"message": req.message,
|
||||||
await session.initialize()
|
"lifetime_hours": 24,
|
||||||
|
}
|
||||||
|
if a0_context_id:
|
||||||
|
payload["context_id"] = a0_context_id
|
||||||
|
|
||||||
tool_args: dict = {
|
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||||
"message": req.message,
|
r = await client.post(
|
||||||
"persistent_chat": True,
|
api_url,
|
||||||
}
|
headers={"X-API-KEY": api_key, "Content-Type": "application/json"},
|
||||||
if a0_chat_id:
|
json=payload,
|
||||||
tool_args["chat_id"] = a0_chat_id
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
result = await session.call_tool("send_message", tool_args)
|
data = r.json()
|
||||||
|
log.info(f"[{agent.name}] ← Agent0 REST: {str(data)[:300]}")
|
||||||
|
|
||||||
# The tool returns a JSON-serialised ToolResponse / ToolError
|
response_text = data.get("response", "")
|
||||||
raw = result.content[0].text if result.content else "{}"
|
new_context_id = data.get("context_id", "")
|
||||||
log.info(f"[{agent.name}] ← Agent0 MCP raw: {raw[:300]}")
|
|
||||||
|
|
||||||
parsed = json.loads(raw)
|
# Persist context_id for conversation continuity
|
||||||
|
if new_context_id and req.conversation_id is not None:
|
||||||
if parsed.get("status") == "error":
|
_a0_sessions[req.conversation_id] = new_context_id
|
||||||
raise RuntimeError(parsed.get("error", "Unknown Agent0 error"))
|
|
||||||
|
|
||||||
response_text = parsed.get("response", "")
|
|
||||||
new_chat_id = parsed.get("chat_id", "")
|
|
||||||
|
|
||||||
# Persist the Agent0 chat_id so the next turn continues the same context
|
|
||||||
if new_chat_id and req.conversation_id is not None:
|
|
||||||
_a0_sessions[req.conversation_id] = new_chat_id
|
|
||||||
|
|
||||||
pose = pick_pose(response_text, valid_poses)
|
pose = pick_pose(response_text, valid_poses)
|
||||||
|
|
||||||
@@ -334,7 +324,7 @@ async def handle_agent0_mcp(
|
|||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"[{agent.name}] Agent0 MCP error: {e}")
|
log.error(f"[{agent.name}] Agent0 REST error: {e}")
|
||||||
fallback_pose = next(
|
fallback_pose = next(
|
||||||
(p for p in ("sorry", "annoyed", "neutral") if p in valid_poses),
|
(p for p in ("sorry", "annoyed", "neutral") if p in valid_poses),
|
||||||
valid_poses[0],
|
valid_poses[0],
|
||||||
@@ -362,8 +352,10 @@ async def _fetch_hermes_token(endpoint: str) -> str:
|
|||||||
if cached:
|
if cached:
|
||||||
return cached
|
return cached
|
||||||
|
|
||||||
|
url = f"{endpoint}/"
|
||||||
|
log.info(f"[hermes] fetching token — GET {url!r} (len={len(url)}, bytes={url.encode()!r})")
|
||||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||||
resp = await client.get(f"{endpoint}/")
|
resp = await client.get(url)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
html = resp.text
|
html = resp.text
|
||||||
|
|
||||||
@@ -405,7 +397,7 @@ async def handle_hermes(
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
from websockets.client import connect as ws_connect # type: ignore
|
from websockets.client import connect as ws_connect # type: ignore
|
||||||
|
|
||||||
endpoint = (model.endpoint or "").rstrip("/")
|
endpoint = (model.endpoint or "").strip().rstrip("/")
|
||||||
hermes_session_id = _hermes_sessions.get(req.conversation_id) if req.conversation_id else None
|
hermes_session_id = _hermes_sessions.get(req.conversation_id) if req.conversation_id else None
|
||||||
|
|
||||||
token = await _fetch_hermes_token(endpoint)
|
token = await _fetch_hermes_token(endpoint)
|
||||||
@@ -509,6 +501,9 @@ async def handle_hermes(
|
|||||||
if "401" in err_str or "403" in err_str or "Unauthorized" in err_str.lower():
|
if "401" in err_str or "403" in err_str or "Unauthorized" in err_str.lower():
|
||||||
log.warning(f"[{agent.name}] Hermes token rejected, re-fetching…")
|
log.warning(f"[{agent.name}] Hermes token rejected, re-fetching…")
|
||||||
_hermes_token_cache.pop(endpoint, None)
|
_hermes_token_cache.pop(endpoint, None)
|
||||||
|
if req.conversation_id is not None:
|
||||||
|
_hermes_sessions.pop(req.conversation_id, None)
|
||||||
|
hermes_session_id = None # force session.create on retry
|
||||||
try:
|
try:
|
||||||
token = await _fetch_hermes_token(endpoint)
|
token = await _fetch_hermes_token(endpoint)
|
||||||
return await _do_chat(token)
|
return await _do_chat(token)
|
||||||
@@ -558,6 +553,12 @@ async def agent_chat(req: ChatRequest, authorization: str = Header(default="")):
|
|||||||
valid_poses = agent.poses if agent.poses else ["neutral"]
|
valid_poses = agent.poses if agent.poses else ["neutral"]
|
||||||
model = select_model(req.models)
|
model = select_model(req.models)
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
f"[route] agent={agent.name!r} agent_type={agent.agent_type!r} "
|
||||||
|
f"models_payload={req.models} "
|
||||||
|
f"selected_model={model}"
|
||||||
|
)
|
||||||
|
|
||||||
# ── Route: Hermes Agent ───────────────────────────────────────────────────
|
# ── Route: Hermes Agent ───────────────────────────────────────────────────
|
||||||
if agent.agent_type == "hermes":
|
if agent.agent_type == "hermes":
|
||||||
if not model or not model.endpoint:
|
if not model or not model.endpoint:
|
||||||
@@ -569,7 +570,12 @@ async def agent_chat(req: ChatRequest, authorization: str = Header(default="")):
|
|||||||
pose=valid_poses[0],
|
pose=valid_poses[0],
|
||||||
conversation_id=req.conversation_id,
|
conversation_id=req.conversation_id,
|
||||||
)
|
)
|
||||||
return await handle_hermes(req, agent, model, valid_poses)
|
try:
|
||||||
|
return await handle_hermes(req, agent, model, valid_poses)
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
log.error(f"[hermes] unhandled exception:\n{traceback.format_exc()}")
|
||||||
|
raise
|
||||||
|
|
||||||
# ── Route: Agent0 MCP ─────────────────────────────────────────────────────
|
# ── Route: Agent0 MCP ─────────────────────────────────────────────────────
|
||||||
if agent.agent_type == "agent0" or (model and model.type == "agent0"):
|
if agent.agent_type == "agent0" or (model and model.type == "agent0"):
|
||||||
|
|||||||
+33
-13
@@ -21,8 +21,8 @@ Configuration via environment variables (or a local .env file):
|
|||||||
LM_STUDIO_URL — LM Studio base URL (default: http://localhost:1234)
|
LM_STUDIO_URL — LM Studio base URL (default: http://localhost:1234)
|
||||||
LM_STUDIO_MODEL — model to use (default: first available)
|
LM_STUDIO_MODEL — model to use (default: first available)
|
||||||
HERMES_URL — Hermes dashboard URL (default: http://localhost:50007)
|
HERMES_URL — Hermes dashboard URL (default: http://localhost:50007)
|
||||||
AGENT0_URL — Agent Zero base URL
|
AGENT0_URL — Agent Zero base URL (e.g. http://localhost:50003)
|
||||||
AGENT0_MCP_KEY — Agent Zero MCP token
|
AGENT0_API_KEY — Agent Zero X-API-KEY from the agent's dashboard
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python test_connectivity.py
|
python test_connectivity.py
|
||||||
@@ -287,27 +287,47 @@ async def test_hermes():
|
|||||||
# ── Agent Zero ────────────────────────────────────────────────────────────────
|
# ── Agent Zero ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async def test_agent0():
|
async def test_agent0():
|
||||||
section("Agent Zero (MCP)")
|
section("Agent Zero (REST)")
|
||||||
base_url = os.environ.get("AGENT0_URL", "")
|
base_url = os.environ.get("AGENT0_URL", "")
|
||||||
mcp_key = os.environ.get("AGENT0_MCP_KEY", "")
|
api_key = os.environ.get("AGENT0_API_KEY", "")
|
||||||
|
|
||||||
if not base_url or not mcp_key:
|
if not base_url or not api_key:
|
||||||
skip("MCP endpoint", "AGENT0_URL and AGENT0_MCP_KEY not set")
|
skip("api_message endpoint", "AGENT0_URL and AGENT0_API_KEY not set")
|
||||||
return
|
return
|
||||||
|
|
||||||
mcp_url = f"{base_url.rstrip('/')}/mcp/t-{mcp_key}/http"
|
url = f"{base_url.rstrip('/')}/api/api_message"
|
||||||
|
|
||||||
|
health_url = f"{base_url.rstrip('/')}/api/health"
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
try:
|
try:
|
||||||
|
# Step 1: health check (no auth needed, fast)
|
||||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||||
r = await client.get(mcp_url, headers={"Accept": "application/json"})
|
rh = await client.get(health_url)
|
||||||
|
|
||||||
if r.status_code in (200, 405):
|
if rh.status_code != 200:
|
||||||
ok("MCP endpoint reachable", f"HTTP {r.status_code} at {mcp_url}")
|
fail("Health endpoint", f"HTTP {rh.status_code}")
|
||||||
elif r.status_code == 401:
|
return
|
||||||
fail("Bad MCP key", f"HTTP {r.status_code}")
|
|
||||||
|
info = rh.json()
|
||||||
|
version = info.get("gitinfo", {}).get("version", "unknown")
|
||||||
|
ok(f"Reachable ({version})", health_url)
|
||||||
|
|
||||||
|
# Step 2: verify API key is accepted via api_reset_chat (fast, no LLM call)
|
||||||
|
reset_url = f"{base_url.rstrip('/')}/api/api_reset_chat"
|
||||||
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||||
|
rk = await client.post(
|
||||||
|
reset_url,
|
||||||
|
headers={"X-API-KEY": api_key, "Content-Type": "application/json"},
|
||||||
|
json={},
|
||||||
|
)
|
||||||
|
|
||||||
|
if rk.status_code == 200:
|
||||||
|
ok("API key accepted", f"HTTP {rk.status_code}")
|
||||||
|
elif rk.status_code in (401, 403):
|
||||||
|
fail("Bad API key", f"HTTP {rk.status_code}")
|
||||||
else:
|
else:
|
||||||
ok("MCP endpoint reachable", f"HTTP {r.status_code}")
|
ok("API key accepted", f"HTTP {rk.status_code}")
|
||||||
|
|
||||||
except httpx.ConnectError as e:
|
except httpx.ConnectError as e:
|
||||||
fail("Not reachable", str(e)[:120])
|
fail("Not reachable", str(e)[:120])
|
||||||
|
|||||||
Reference in New Issue
Block a user