|
|
@@ -1145,6 +1145,56 @@ async def _handle_ollama_generate(request: Request, agent_name: str = "") -> Res
|
|
|
|
raise
|
|
|
|
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")
|
|
|
|
@app.post("/api/chat")
|
|
|
|
async def chat(request: Request) -> Response:
|
|
|
|
async def chat(request: Request) -> Response:
|
|
|
|
return await _handle_ollama_chat(request)
|
|
|
|
return await _handle_ollama_chat(request)
|
|
|
@@ -1940,13 +1990,95 @@ async def update_config(request: Request) -> dict:
|
|
|
|
return {"error": "key is required"}
|
|
|
|
return {"error": "key is required"}
|
|
|
|
async with pool.acquire() as conn:
|
|
|
|
async with pool.acquire() as conn:
|
|
|
|
await conn.execute(
|
|
|
|
await conn.execute(
|
|
|
|
"UPDATE config SET value=$1, updated_at=now() WHERE key=$2",
|
|
|
|
"""INSERT INTO config (key, value, updated_at)
|
|
|
|
value, key,
|
|
|
|
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)
|
|
|
|
log.info("config updated key=%s value=%s", key, value)
|
|
|
|
return {"status": "ok", "key": key, "value": 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
|
|
|
|
# /resolve/run — manually trigger resolution job
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
@@ -3274,6 +3406,22 @@ ADMIN_HTML = """<!DOCTYPE html>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div id="features-status" style="font-size:0.8em;color:#666;margin-bottom:1.5em"></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>
|
|
|
|
<h2>World model stats</h2>
|
|
|
|
<div class="stats" id="stats">
|
|
|
|
<div class="stats" id="stats">
|
|
|
|
<div class="stat"><div class="stat-label">SOAS tokens</div><div class="stat-value" id="s-soas">…</div></div>
|
|
|
|
<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');
|
|
|
|
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) {{
|
|
|
|
async function setFeature(key, enabled) {{
|
|
|
|
const val = enabled ? 'true' : 'false';
|
|
|
|
const val = enabled ? 'true' : 'false';
|
|
|
|
const r = await fetch('/config', {{method:'POST', headers:{{'Content-Type':'application/json'}}, body:JSON.stringify({{key, value:val}})}});
|
|
|
|
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();
|
|
|
|
loadStats();
|
|
|
|
loadFeatures();
|
|
|
|
loadFeatures();
|
|
|
|
|
|
|
|
loadTestChatModels();
|
|
|
|
loadConflicts();
|
|
|
|
loadConflicts();
|
|
|
|
loadLog(0);
|
|
|
|
loadLog(0);
|
|
|
|
loadModels();
|
|
|
|
loadModels();
|
|
|
|