This commit is contained in:
2026-05-03 11:24:53 +02:00
parent e8301fb2bf
commit 9a34e0005e
6 changed files with 218 additions and 6 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"updated_at": "2026-05-03T05:59:27.815628",
"updated_at": "2026-05-03T07:49:54.497466",
"platforms": {
"telegram": [],
"discord": [],
+1 -1
View File
@@ -1 +1 @@
{"pid": 7, "kind": "hermes-gateway", "argv": ["/opt/hermes/.venv/bin/hermes", "gateway", "run"], "start_time": 57263427}
{"pid": 7, "kind": "hermes-gateway", "argv": ["/opt/hermes/.venv/bin/hermes", "gateway", "run"], "start_time": 57925761}
+1 -1
View File
@@ -1 +1 @@
{"pid": 7, "kind": "hermes-gateway", "argv": ["/opt/hermes/.venv/bin/hermes", "gateway", "run"], "start_time": 57263427}
{"pid": 7, "kind": "hermes-gateway", "argv": ["/opt/hermes/.venv/bin/hermes", "gateway", "run"], "start_time": 57925761}
+1 -1
View File
@@ -1 +1 @@
{"pid": 7, "kind": "hermes-gateway", "argv": ["/opt/hermes/.venv/bin/hermes", "gateway", "run"], "start_time": 57263427, "gateway_state": "running", "exit_reason": null, "restart_requested": false, "active_agents": 0, "platforms": {}, "updated_at": "2026-05-03T05:59:27.802648+00:00"}
{"pid": 7, "kind": "hermes-gateway", "argv": ["/opt/hermes/.venv/bin/hermes", "gateway", "run"], "start_time": 57925761, "gateway_state": "running", "exit_reason": null, "restart_requested": false, "active_agents": 0, "platforms": {}, "updated_at": "2026-05-03T07:49:54.488441+00:00"}
+213 -2
View File
@@ -1145,6 +1145,56 @@ async def _handle_ollama_generate(request: Request, agent_name: str = "") -> Res
raise
# ---------------------------------------------------------------------------
# /chat — gnommoweb integration endpoint
# Accepts a conversation turn from gnommoweb and returns an agent reply.
# Currently a stub: echoes back a placeholder until full agent routing is wired.
# ---------------------------------------------------------------------------
@app.post("/chat")
async def gnommoweb_chat(request: Request) -> dict:
"""
Entry point for gnommoweb agent chat.
Expected body:
{
"agent_id": <int>,
"conversation_id": <int|null>,
"context_id": <str|null>,
"user_id": <int>,
"message": <str>,
"history": [{"role": "user"|"assistant", "content": <str>}]
}
Returns:
{
"message": <str>,
"pose": <str>,
"context_id": <str|null>
}
"""
data = await request.json()
agent_id = data.get("agent_id")
conversation_id = data.get("conversation_id")
context_id = data.get("context_id")
user_id = data.get("user_id")
message = data.get("message", "")
history = data.get("history", [])
log.info(
"gnommoweb_chat agent_id=%s conv=%s user=%s msg_len=%d hist=%d",
agent_id, conversation_id, user_id, len(message), len(history),
)
# TODO: route to agent framework (Agent Zero, etc.) based on agent config
# For now return a stub so gnommoweb has a working endpoint to call
return {
"message": f"[festinger stub] agent_id={agent_id} received: {message[:80]}",
"pose": "neutral",
"context_id": context_id,
}
@app.post("/api/chat")
async def chat(request: Request) -> Response:
return await _handle_ollama_chat(request)
@@ -1940,13 +1990,95 @@ async def update_config(request: Request) -> dict:
return {"error": "key is required"}
async with pool.acquire() as conn:
await conn.execute(
"UPDATE config SET value=$1, updated_at=now() WHERE key=$2",
value, key,
"""INSERT INTO config (key, value, updated_at)
VALUES ($1, $2, now())
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = now()""",
key, value,
)
log.info("config updated key=%s value=%s", key, value)
return {"status": "ok", "key": key, "value": value}
# ---------------------------------------------------------------------------
# /test-chat — diagnostic: send a single message to upstream_openai and return
# the raw response + timing so the admin can verify the LLM connection works.
# ---------------------------------------------------------------------------
@app.get("/test-chat/models")
async def test_chat_models(request: Request) -> dict:
"""Return the list of models currently available in upstream_openai (Ollama)."""
cfg = request.app.state.yaml_config
upstream = cfg.get("upstream_openai", "")
if not upstream:
return {"ok": False, "error": "upstream_openai not configured", "models": []}
url = f"{upstream.rstrip('/')}/v1/models"
try:
async with httpx.AsyncClient(timeout=8.0) as client:
r = await client.get(url)
if not r.is_success:
return {"ok": False, "error": f"HTTP {r.status_code} from {url}", "models": []}
ids = sorted(m.get("id", "") for m in r.json().get("data", []))
return {"ok": True, "models": ids, "upstream": upstream}
except httpx.ConnectError:
return {"ok": False, "error": f"Connection refused at {url} — is Ollama running?", "models": []}
except Exception as exc:
return {"ok": False, "error": str(exc), "models": []}
@app.post("/test-chat")
async def test_chat(request: Request) -> dict:
cfg = request.app.state.yaml_config
data = await request.json()
message = (data.get("message") or "Say hello in one sentence.").strip()
model = (data.get("model") or "").strip()
upstream = cfg.get("upstream_openai", "")
if not upstream:
return {"ok": False, "error": "upstream_openai not configured"}
# Always fetch the available model list so the UI can show it
available: list[str] = []
try:
async with httpx.AsyncClient(timeout=8.0) as client:
mr = await client.get(f"{upstream.rstrip('/')}/v1/models")
if mr.is_success:
available = sorted(m.get("id", "") for m in mr.json().get("data", []))
except Exception:
pass
if not model:
if not available:
return {"ok": False, "error": f"No model specified and none found at {upstream}/v1/models", "available": []}
model = available[0]
url = f"{upstream.rstrip('/')}/v1/chat/completions"
body = {
"model": model,
"stream": False,
"messages": [{"role": "user", "content": message}],
}
t0 = time.perf_counter()
try:
async with httpx.AsyncClient(timeout=60.0) as client:
r = await client.post(url, json=body)
ms = int((time.perf_counter() - t0) * 1000)
if not r.is_success:
return {"ok": False, "error": f"HTTP {r.status_code}: {r.text[:400]}", "url": url, "model": model, "ms": ms, "available": available}
resp = r.json()
reply = resp.get("choices", [{}])[0].get("message", {}).get("content", "(empty)")
return {"ok": True, "reply": reply, "model": model, "url": url, "ms": ms, "available": available}
except httpx.ConnectError as exc:
ms = int((time.perf_counter() - t0) * 1000)
return {"ok": False, "error": f"Connection refused — is Ollama running at {upstream}? ({exc})", "url": url, "ms": ms, "available": available}
except httpx.TimeoutException:
ms = int((time.perf_counter() - t0) * 1000)
return {"ok": False, "error": f"Request timed out after {ms}ms", "url": url, "ms": ms, "available": available}
except Exception as exc:
ms = int((time.perf_counter() - t0) * 1000)
return {"ok": False, "error": str(exc), "url": url, "ms": ms, "available": available}
# ---------------------------------------------------------------------------
# /resolve/run — manually trigger resolution job
# ---------------------------------------------------------------------------
@@ -3274,6 +3406,22 @@ ADMIN_HTML = """<!DOCTYPE html>
</div>
<div id="features-status" style="font-size:0.8em;color:#666;margin-bottom:1.5em"></div>
<h2>Test Chat</h2>
<p style="font-size:0.83em;color:#666;margin-bottom:0.8em">
Send a message directly to <code>upstream_openai</code> (Ollama) to verify the connection end-to-end.
</p>
<div id="tc-models-row" style="font-size:0.82em;color:#555;margin-bottom:0.6em">
<span id="tc-models-label">Loading available models…</span>
</div>
<div style="display:flex;gap:0.6em;align-items:flex-end;flex-wrap:wrap;margin-bottom:0.6em">
<select id="tc-model" style="font-family:monospace;padding:5px 8px;border:1px solid #ccc;border-radius:3px;min-width:220px">
<option value="">— loading —</option>
</select>
<input id="tc-msg" type="text" value="Say hello in one sentence." style="font-family:monospace;padding:5px 8px;border:1px solid #ccc;border-radius:3px;flex:1;min-width:200px">
<button onclick="sendTestChat(this)" class="primary" style="height:32px;white-space:nowrap">Send</button>
</div>
<div id="tc-result" style="font-size:0.85em;margin-bottom:1.5em"></div>
<h2>World model stats</h2>
<div class="stats" id="stats">
<div class="stat"><div class="stat-label">SOAS tokens</div><div class="stat-value" id="s-soas">…</div></div>
@@ -3446,6 +3594,68 @@ ADMIN_HTML = """<!DOCTYPE html>
document.getElementById('feat-loop-detection').checked = isEnabled('feature_loop_detection');
}}
async function loadTestChatModels() {{
const sel = document.getElementById('tc-model');
const label = document.getElementById('tc-models-label');
try {{
const r = await fetch('/test-chat/models');
const d = await r.json();
if (d.ok && d.models.length) {{
sel.innerHTML = d.models.map(m => `<option value="${{m}}">${{m}}</option>`).join('');
label.innerHTML = `<strong>${{d.models.length}}</strong> model${{d.models.length===1?'':'s'}} available at <code>${{d.upstream}}</code>: ${{d.models.join(', ')}}`;
label.style.color = '#2a7a2a';
}} else {{
sel.innerHTML = '<option value="">— none found —</option>';
label.textContent = d.error || 'No models found';
label.style.color = '#b00';
}}
}} catch(e) {{
sel.innerHTML = '<option value="">— error —</option>';
label.textContent = `Failed to reach /test-chat/models: ${{e}}`;
label.style.color = '#b00';
}}
}}
async function sendTestChat(btn) {{
const model = document.getElementById('tc-model').value.trim();
const message = document.getElementById('tc-msg').value.trim() || 'Say hello in one sentence.';
const out = document.getElementById('tc-result');
btn.disabled = true;
btn.textContent = 'Sending…';
out.innerHTML = '<span style="color:#888">Waiting for response…</span>';
try {{
const r = await fetch('/test-chat', {{
method: 'POST',
headers: {{'Content-Type': 'application/json'}},
body: JSON.stringify({{message, model: model || undefined}}),
}});
const d = await r.json();
// Refresh model dropdown if the response includes an updated list
if (d.available && d.available.length) {{
const sel = document.getElementById('tc-model');
const current = sel.value;
sel.innerHTML = d.available.map(m => `<option value="${{m}}"${{m===d.model?' selected':''}}>${{m}}</option>`).join('');
document.getElementById('tc-models-label').innerHTML =
`<strong>${{d.available.length}}</strong> model${{d.available.length===1?'':'s'}} available: ${{d.available.join(', ')}}`;
document.getElementById('tc-models-label').style.color = '#2a7a2a';
}}
if (d.ok) {{
out.innerHTML =
`<div style="color:#2a7a2a;margin-bottom:0.3em">✓ <strong>${{d.model}}</strong> — ${{d.ms}}ms</div>` +
`<pre style="margin:0;white-space:pre-wrap;background:#f4f9f4;border-color:#c0e0c0">${{d.reply}}</pre>`;
}} else {{
out.innerHTML =
`<div style="color:#b00;margin-bottom:0.3em">✗ ${{d.ms != null ? d.ms+'ms' : ''}}</div>` +
`<pre style="margin:0;white-space:pre-wrap;background:#fdf4f4;border-color:#e0c0c0">${{d.error}}</pre>`;
}}
}} catch(e) {{
out.innerHTML = `<span style="color:#b00">Request failed: ${{e}}</span>`;
}} finally {{
btn.disabled = false;
btn.textContent = 'Send';
}}
}}
async function setFeature(key, enabled) {{
const val = enabled ? 'true' : 'false';
const r = await fetch('/config', {{method:'POST', headers:{{'Content-Type':'application/json'}}, body:JSON.stringify({{key, value:val}})}});
@@ -3846,6 +4056,7 @@ ADMIN_HTML = """<!DOCTYPE html>
loadStats();
loadFeatures();
loadTestChatModels();
loadConflicts();
loadLog(0);
loadModels();
+1
View File
@@ -17,4 +17,5 @@ exec autossh -M 0 -N \
-R 0.0.0.0:50005:rind:80 \
-R 0.0.0.0:50006:abyssinthia:80 \
-R 0.0.0.0:50007:gerhard-dashboard:9119 \
-R 0.0.0.0:11435:festinger:11434 \
tunnel@glitch.university