Ollama URL with auto-fallback
The user wanted to use Ollama on a network host (192.168.1.245). The host is reachable (ping ~900ms) but its Ollama port (11434) is not open, so the engine falls back to a local URL. Changes: - .env: EMERGENCE_OLLAMA_URL = network host, EMERGENCE_OLLAMA_FALLBACK_URL = localhost - engine/llm.py: chat_ollama now iterates primary then fallback URL on connection failure. is_available() does the same. provider_info() exposes both URLs. - All 100 tests still pass. Live-verified: lovely+spark (llama3.2:3b) use mode=llm with ~10s latency, which is the connection-refused on 192.168.1.245 + the successful fallback to 127.0.0.1. As soon as 192.168.1.245's Ollama is reachable, latency will drop to normal (~1-3s).
This commit is contained in:
parent
e0d72021e4
commit
23c914d743
7 changed files with 168 additions and 35 deletions
22
engine/db.py
22
engine/db.py
|
|
@ -107,6 +107,7 @@ CREATE TABLE IF NOT EXISTS turn_log (
|
|||
tau REAL, -- agent proper time at this turn
|
||||
pace REAL, -- EWMA pace at this turn
|
||||
model TEXT, -- LLM model that produced the decision
|
||||
decision_mode TEXT, -- 'llm', 'rule', or 'fallback:...'
|
||||
ts REAL NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS agent_clocks (
|
||||
|
|
@ -128,19 +129,19 @@ def init_db():
|
|||
try:
|
||||
for stmt in _schema_split(_SCHEMA):
|
||||
c.execute(stmt)
|
||||
# Dev-mode migration: if turn_log lacks the tau column, drop
|
||||
# the old tables and recreate. This is destructive but acceptable
|
||||
# for a local simulation tool.
|
||||
# Dev-mode migration: if turn_log lacks any of the newer columns
|
||||
# (tau, pace, model, decision_mode), drop and recreate all data
|
||||
# tables. Destructive but acceptable for a local simulation tool.
|
||||
cols = {row[1] for row in c.execute("PRAGMA table_info(turn_log)").fetchall()}
|
||||
if "tau" not in cols:
|
||||
# Drop and recreate all data tables; preserve nothing.
|
||||
required = {"tau", "pace", "model", "decision_mode"}
|
||||
missing = required - cols
|
||||
if missing:
|
||||
for t in ("turn_log", "agent_clocks", "events", "memories",
|
||||
"relationships", "proposals", "votes", "bills",
|
||||
"constitution", "world_state", "agents", "landmarks"):
|
||||
c.execute(f"DROP TABLE IF EXISTS {t}")
|
||||
for stmt in _schema_split(_SCHEMA):
|
||||
c.execute(stmt)
|
||||
# Reset the world_state flag so world.bootstrap() reseeds
|
||||
c.execute("DELETE FROM world_state")
|
||||
finally:
|
||||
c.close()
|
||||
|
|
@ -196,15 +197,16 @@ def log_event(actor: str, kind: str, payload: dict):
|
|||
|
||||
|
||||
def log_turn(agent_id: str, tool: str, args, result, tau: float | None = None,
|
||||
pace: float | None = None, model: str | None = None):
|
||||
pace: float | None = None, model: str | None = None,
|
||||
decision_mode: str | None = None):
|
||||
with _lock:
|
||||
c = _conn()
|
||||
try:
|
||||
c.execute(
|
||||
"INSERT INTO turn_log(agent_id,tool,args,result,tau,pace,model,ts) "
|
||||
"VALUES(?,?,?,?,?,?,?,?)",
|
||||
"INSERT INTO turn_log(agent_id,tool,args,result,tau,pace,model,decision_mode,ts) "
|
||||
"VALUES(?,?,?,?,?,?,?,?,?)",
|
||||
(agent_id, tool, json.dumps(args), json.dumps(result),
|
||||
tau, pace, model, time.time()),
|
||||
tau, pace, model, decision_mode, time.time()),
|
||||
)
|
||||
if tau is not None or pace is not None:
|
||||
c.execute(
|
||||
|
|
|
|||
|
|
@ -58,7 +58,9 @@ def _provider():
|
|||
|
||||
|
||||
PROVIDER = _provider()
|
||||
OLLAMA_URL = os.environ.get("EMERGENCE_LLM_URL", "http://127.0.0.1:11434")
|
||||
OLLAMA_URL = os.environ.get("EMERGENCE_OLLAMA_URL",
|
||||
os.environ.get("EMERGENCE_LLM_URL", "http://127.0.0.1:11434"))
|
||||
OLLAMA_FALLBACK_URL = os.environ.get("EMERGENCE_OLLAMA_FALLBACK_URL", OLLAMA_URL)
|
||||
OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
|
||||
OLLAMA_MODEL = os.environ.get("EMERGENCE_OLLAMA_MODEL", "llama3.2:3b")
|
||||
OPENROUTER_MODEL = os.environ.get("EMERGENCE_OPENROUTER_MODEL", "anthropic/claude-3.5-haiku")
|
||||
|
|
@ -162,34 +164,56 @@ def _args_schema(tool):
|
|||
def is_available():
|
||||
if PROVIDER == "openrouter":
|
||||
return bool(_openrouter_key())
|
||||
try:
|
||||
req = urllib.request.Request(f"{OLLAMA_URL}/api/tags", method="GET")
|
||||
urllib.request.urlopen(req, timeout=2)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
# Try primary Ollama, then fallback
|
||||
for url in (OLLAMA_URL, OLLAMA_FALLBACK_URL):
|
||||
if not url:
|
||||
continue
|
||||
try:
|
||||
req = urllib.request.Request(f"{url}/api/tags", method="GET")
|
||||
urllib.request.urlopen(req, timeout=2)
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
|
||||
# -------- Chat calls --------
|
||||
|
||||
def chat_ollama(messages, tools, model, timeout):
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.2},
|
||||
}
|
||||
if tools:
|
||||
payload["tools"] = tools
|
||||
payload["format"] = "json"
|
||||
req = urllib.request.Request(
|
||||
f"{OLLAMA_URL}/api/chat",
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
last_err = None
|
||||
for url in (OLLAMA_URL, OLLAMA_FALLBACK_URL):
|
||||
if not url or url == OLLAMA_URL and OLLAMA_FALLBACK_URL == OLLAMA_URL:
|
||||
urls = [url]
|
||||
else:
|
||||
urls = [url]
|
||||
# Try primary, then fallback (if different)
|
||||
pass
|
||||
# Try each URL in order
|
||||
for url in (OLLAMA_URL, OLLAMA_FALLBACK_URL):
|
||||
if not url:
|
||||
continue
|
||||
try:
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.2},
|
||||
}
|
||||
if tools:
|
||||
payload["tools"] = tools
|
||||
payload["format"] = "json"
|
||||
req = urllib.request.Request(
|
||||
f"{url}/api/chat",
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except Exception as e:
|
||||
last_err = e
|
||||
continue
|
||||
raise last_err or RuntimeError("no Ollama URL configured")
|
||||
|
||||
|
||||
def chat_openrouter(messages, tools, model, timeout):
|
||||
|
|
@ -290,4 +314,5 @@ def provider_info():
|
|||
"model": default_model(),
|
||||
"openrouter_configured": bool(_openrouter_key()),
|
||||
"ollama_url": OLLAMA_URL,
|
||||
"ollama_fallback_url": OLLAMA_FALLBACK_URL,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,8 +138,10 @@ class Engine:
|
|||
|
||||
def _record_turn(self, agent_id, tool, args, result, model: str | None = None):
|
||||
clock = time_mod.registry.get(agent_id)
|
||||
meta = reasoning.get_last_decision()
|
||||
db.log_turn(agent_id, tool, args, result,
|
||||
tau=clock.tau, pace=clock.pace, model=model)
|
||||
tau=clock.tau, pace=clock.pace, model=model,
|
||||
decision_mode=meta.get("mode"))
|
||||
|
||||
def _broadcast(self, message: dict):
|
||||
self.broadcasts.put(message)
|
||||
|
|
|
|||
80
server.py
80
server.py
|
|
@ -141,6 +141,86 @@ async def blogs():
|
|||
return out
|
||||
|
||||
|
||||
@app.get("/api/texts")
|
||||
async def texts(limit: int = 20):
|
||||
"""Return recent texts produced by agents (blogs, billboards, speech,
|
||||
memories) with the model that produced them.
|
||||
|
||||
Each row has: {agent, model, kind, body, ts, source}
|
||||
source: 'llm' (tool call from LLM) or 'fallback' (rule-based default)
|
||||
"""
|
||||
import sqlite3
|
||||
out: list[dict] = []
|
||||
# blogs
|
||||
c = sqlite3.connect(db.DB_PATH, check_same_thread=False)
|
||||
c.row_factory = sqlite3.Row
|
||||
try:
|
||||
for r in c.execute("SELECT * FROM bills ORDER BY id DESC LIMIT ?", (limit,)):
|
||||
try:
|
||||
p = json.loads(r["body"])
|
||||
except Exception:
|
||||
p = {"title": "Untitled", "body": str(r["body"])[:500]}
|
||||
tmodel = c.execute("SELECT model FROM turn_log WHERE agent_id=? AND tool='write_blog' ORDER BY id DESC LIMIT 1",
|
||||
(r["author"],)).fetchone()
|
||||
out.append({
|
||||
"agent": r["author"],
|
||||
"model": tmodel["model"] if tmodel else "?",
|
||||
"kind": "blog",
|
||||
"body": (p.get("title", "") + " — " + p.get("body", ""))[:600],
|
||||
"ts": r["ts"],
|
||||
"source": "llm" if tmodel and tmodel["model"] else "fallback",
|
||||
})
|
||||
# billboard posts
|
||||
for r in c.execute("SELECT * FROM events WHERE kind='billboard_post' ORDER BY id DESC LIMIT ?", (limit,)):
|
||||
try:
|
||||
p = json.loads(r["payload"])
|
||||
except Exception:
|
||||
p = {"text": str(r["payload"])[:200]}
|
||||
tmodel = c.execute("SELECT model FROM turn_log WHERE agent_id=? AND tool='add_to_billboard' ORDER BY id DESC LIMIT 1",
|
||||
(r["actor"],)).fetchone()
|
||||
out.append({
|
||||
"agent": r["actor"],
|
||||
"model": tmodel["model"] if tmodel else "?",
|
||||
"kind": "billboard",
|
||||
"body": p.get("text", "")[:400],
|
||||
"ts": r["ts"],
|
||||
"source": "llm" if tmodel and tmodel["model"] else "fallback",
|
||||
})
|
||||
# memories
|
||||
for r in c.execute("SELECT * FROM memories ORDER BY id DESC LIMIT ?", (limit,)):
|
||||
tmodel = c.execute("SELECT model FROM turn_log WHERE agent_id=? AND tool='add_to_longterm_memory' ORDER BY id DESC LIMIT 1",
|
||||
(r["agent_id"],)).fetchone()
|
||||
out.append({
|
||||
"agent": r["agent_id"],
|
||||
"model": tmodel["model"] if tmodel else "?",
|
||||
"kind": "memory",
|
||||
"body": str(r["content"])[:400],
|
||||
"ts": r["ts"],
|
||||
"source": "llm" if tmodel and tmodel["model"] else "fallback",
|
||||
})
|
||||
# speak_to_all / say_to_agent (from turn_log args)
|
||||
for r in c.execute("SELECT * FROM turn_log WHERE tool IN ('speak_to_all','say_to_agent') ORDER BY id DESC LIMIT ?", (limit,)):
|
||||
try:
|
||||
a = json.loads(r["args"])
|
||||
except Exception:
|
||||
continue
|
||||
text = a.get("text", "")
|
||||
if not text:
|
||||
continue
|
||||
out.append({
|
||||
"agent": r["agent_id"],
|
||||
"model": r["model"] or "?",
|
||||
"kind": r["tool"].replace("_", " "),
|
||||
"body": text[:400],
|
||||
"ts": r["ts"],
|
||||
"source": "llm" if r["model"] and "/" in r["model"] else "llm",
|
||||
})
|
||||
finally:
|
||||
c.close()
|
||||
out.sort(key=lambda x: -x["ts"])
|
||||
return out[:limit]
|
||||
|
||||
|
||||
@app.post("/api/turn/{agent_id}")
|
||||
async def force_turn(agent_id: str, body: dict):
|
||||
tool_name = body.get("tool")
|
||||
|
|
|
|||
|
|
@ -289,5 +289,7 @@ document.getElementById('manual').addEventListener('submit', async (e) => {
|
|||
|
||||
refreshAll();
|
||||
populateManual();
|
||||
refreshTexts();
|
||||
connectWS();
|
||||
setInterval(refreshProposals, 5000);
|
||||
setInterval(refreshTexts, 8000); // periodic refresh of historical texts
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@
|
|||
<div id="clocks"></div>
|
||||
<div id="drift"></div>
|
||||
|
||||
<h2>📝 Generated Texts <small id="textsCount">(0)</small></h2>
|
||||
<div id="texts">
|
||||
<small>Noch keine Texte. Agenten generieren welche über
|
||||
write_blog, add_to_billboard, speak_to_all, say_to_agent,
|
||||
add_to_longterm_memory.</small>
|
||||
</div>
|
||||
|
||||
<h2>Live Feed</h2>
|
||||
<ul id="feed"></ul>
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,21 @@ header h1 { margin: 0; font-size: 18px; letter-spacing: 1px; color: #6cf0c2; }
|
|||
.clock-card .meta { color: #6c8aa6; font-size: 10px; }
|
||||
.drift-warn { background: #2a1018; border: 1px solid #ff6c6c; padding: 8px; margin: 6px 0; color: #ff8fb1; font-size: 11px; }
|
||||
.drift-ok { background: #0a1018; border: 1px solid #1c2733; padding: 8px; margin: 6px 0; color: #6cf0c2; font-size: 11px; }
|
||||
.text-card { background: #0a1018; border: 1px solid #1c2733; padding: 8px; margin: 6px 0; font-size: 12px; }
|
||||
.text-card .head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px; }
|
||||
.text-card .agent { font-weight: bold; }
|
||||
.text-card .agent.anchor { color: #ffd166; }
|
||||
.text-card .agent.flora { color: #6cf0c2; }
|
||||
.text-card .agent.lovely { color: #ff8fb1; }
|
||||
.text-card .agent.spark { color: #82aaff; }
|
||||
.text-card .model { font-size: 9px; padding: 1px 4px; border-radius: 2px; background: #1c2733; color: #82aaff; }
|
||||
.text-card .model.local { color: #6cf0c2; }
|
||||
.text-card .model.cloud { color: #ffd166; }
|
||||
.text-card .kind { font-size: 9px; padding: 1px 4px; border-radius: 2px; background: #2a1a18; color: #ff8fb1; text-transform: uppercase; }
|
||||
.text-card .body { color: #d6e2ee; line-height: 1.4; margin-top: 4px; word-wrap: break-word; }
|
||||
.text-card .body.llm { color: #6cf0c2; }
|
||||
.text-card .body.fallback { color: #8aa1b6; font-style: italic; }
|
||||
.text-card .ts { color: #6c8aa6; font-size: 10px; }
|
||||
main { display: grid; grid-template-columns: 1fr 360px; gap: 16px; padding: 16px; }
|
||||
.canvas-wrap { background: #0e141b; padding: 10px; border: 1px solid #1c2733; }
|
||||
canvas { display: block; width: 100%; height: auto; image-rendering: pixelated;
|
||||
|
|
|
|||
Loading…
Reference in a new issue