#!/usr/bin/env python3
"""Robot demo proxy, v2: the "three deliveries" game on the route language.

Sits on 127.0.0.1:18450 behind Caddy (handle /api/robot/*). Talks to the Extella platform with the
account token from the environment; the token never reaches the browser.

  GET  /api/robot/state                    examples, level 1, model, limits, devices
  POST /api/robot/run                      {skill, level: "level1"|"seed<N>", device: vps|mac, law} -> episode
  POST /api/robot/teach                    {wish, session} -> program via the model, checked, saved, run on level 1
  POST /api/robot/teach-program            {program, session, parent} -> same path, no model
  POST /api/robot/session/no-ai            {session} -> forbid generation for this session (server side)
  POST /api/robot/share                    {skill, note, session} -> library entry + per-skill API key
  GET  /api/robot/library                  shared experts (route language only)
  GET  /api/robot/skill/<id>               one expert with its program
  GET  /api/robot/export/<id>              bundle: language handler + expert, as JSON
  POST /api/robot/<skill>/run              header X-Robot-Key -> the visitor's own API for their expert
"""
import hashlib
import json
import os
import secrets
import sys
import threading
import time
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import route_core as core  # noqa: E402

API = "https://api.extella.ai/api"
TOKEN = os.environ["EXTELLA_TOKEN"]
AGENT = os.environ.get("ROBOT_AGENT", "agent_Fgw-hPvLsg9vyn2QDg5GO")
DEVICE = os.environ.get("ROBOT_DEVICE", "85800354-f7b7-449f-b526-9357cd91f780")
DEVICES = {"vps": {"id": DEVICE, "name": "extella-team", "label": "VPS · Linux"},
           "mac": {"id": os.environ.get("ROBOT_DEVICE_MAC", "24f37e45-8c9f-4896-b64f-0dcd0cd8b0e4"), "name": "Anvar's MacBook", "label": "MacBook · macOS"}}
OPENROUTER_KEY = os.environ.get("OPENROUTER_API_KEY", "")
MODEL = os.environ.get("ROBOT_MODEL", "google/gemini-2.5-flash-lite")
PORT = int(os.environ.get("ROBOT_PORT", "18450"))
DAY_LIMIT = int(os.environ.get("ROBOT_DAY_LIMIT", "3000"))
SESSION_LIMIT = int(os.environ.get("ROBOT_SESSION_LIMIT", "10"))
LIBRARY_KEY = "robot_route_library"
REGISTRAR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "demo_register_route.fython")
LAWS = {"clinic_first": {"en": "town law: the clinic (house C) is always served first", "ru": "закон городка: клиника (дом C) обслуживается первой"}}

LOCK = threading.Lock()
# Every model call in this process goes through generate_program(); the provider key exists nowhere else.
# Counting here is therefore a measurement at the only boundary where a model call is possible.
CALLS = threading.local()


def calls_reset():
    CALLS.n = 0


def calls_count():
    return getattr(CALLS, "n", 0)
STATE = {"day": time.strftime("%Y-%m-%d"), "generations_today": 0, "sessions": {}, "rate": {}}

EXAMPLES = [
    {"id": "default", "expert": "skill_route_list", "role": "default",
     "wish": "Visit the houses in list order: A, then B, then C.", "wish_ru": "Объезжать дома по списку: A, потом B, потом C.",
     "program": {"name": "list order", "steps": [{"pick": "list"}]}},
    {"id": "nearest", "expert": "skill_route_nearest",
     "wish": "Deliver to the nearest house first.", "wish_ru": "Сначала доставляй в ближайший дом.",
     "program": {"name": "nearest first", "steps": [{"pick": "nearest"}]}},
    {"id": "shortest", "expert": "skill_route_shortest",
     "wish": "Find the shortest route through all three houses.", "wish_ru": "Найди кратчайший маршрут через все три дома.",
     "program": {"name": "shortest total route", "steps": [{"pick": "shortest_total"}]}},
    {"id": "c_first", "expert": "skill_route_c_first",
     "wish": "Visit C first, then choose the shorter remaining route.", "wish_ru": "Сначала C, потом более короткий из оставшихся маршрутов.",
     "program": {"name": "C first, then nearest", "steps": [{"pick": "house", "house": "C"}, {"pick": "nearest"}]}},
]
EXAMPLE_BY_EXPERT = {e["expert"]: e for e in EXAMPLES}


# ---------------------------------------------------------------- platform

def platform(path, body, timeout=120):
    req = urllib.request.Request(API + path, data=json.dumps(body, ensure_ascii=False).encode(),
                                 headers={"X-Auth-Token": TOKEN, "X-Profile-Id": "default",
                                          "X-Agent-Id": AGENT, "Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return json.loads(r.read())


def unwrap(out):
    import ast
    res = out.get("result", out) if isinstance(out, dict) else out
    if isinstance(res, dict) and "result" in res:
        res = res["result"]
    if isinstance(res, str):
        for loader in (json.loads, ast.literal_eval):
            try:
                return loader(res)
            except Exception:
                pass
    return res


def run_skill(expert, params, device="vps"):
    dev = DEVICES.get(device, DEVICES["vps"])
    t0 = time.time()
    try:
        out = platform("/expert/run", {"name": expert, "params": params, "targets": [dev["id"]]}, timeout=150)
    except urllib.error.HTTPError as e:
        body = e.read().decode("utf-8", "ignore")[:200]
        return {"status": "error", "reason": ("device %s is offline right now" % dev["name"]) if "unavailable" in body.lower() else "platform error: " + body,
                "platform_seconds": round(time.time() - t0, 1), "device": dev["name"], "device_key": device}
    task = out.get("task_id") if isinstance(out, dict) else None
    while task:
        time.sleep(2)
        chk = platform("/tasks/check", {"task_id": task}, timeout=30)
        if str(chk.get("status")) in ("completed", "done", "success", "finished", "error", "failed"):
            out = chk
            break
    res = unwrap(out)
    if not isinstance(res, dict):
        res = {"status": "error", "reason": str(res)[:200]}
    res["platform_seconds"] = round(time.time() - t0, 1)
    res["device"] = res.get("host") or dev["name"]
    res["device_key"] = device
    if isinstance(res.get("reason"), str) and "unavailable" in res["reason"].lower():
        res["status"] = "error"
        res["reason"] = "device %s is offline right now" % dev["name"]
    return res


def save_skill(name, program, description):
    return platform("/expert/save", {"name": name, "description": description[:300],
                                     "code": json.dumps(program, ensure_ascii=False, indent=1),
                                     "cspl": "route", "kwargs": {"seed": 0, "mode": "run"}, "global": False}, timeout=60)


def kv_get(key):
    try:
        out = platform("/kv/get", {"key": key}, timeout=30)
    except urllib.error.HTTPError:
        return None
    val = out.get("value") if isinstance(out, dict) else None
    if isinstance(val, str):
        try:
            return json.loads(val)
        except Exception:
            return None
    return val


def kv_set(key, value, description=""):
    return platform("/kv/set", {"key": key, "value": json.dumps(value, ensure_ascii=False), "description": description}, timeout=30)


# ---------------------------------------------------------------- generation

SYSTEM_PROMPT = """You write programs in the route language for a delivery robot. Output ONLY a JSON object, nothing else.

The robot starts with three parcels for houses A, B and C on a small grid. The program chooses the ORDER of the three deliveries; the engine walks the shortest path between houses. One decision per step; the last step repeats until all houses are delivered.

Grammar:
{"name": "<short name, 2-4 words>", "steps": [ {"pick": "<list|nearest|farthest|shortest_total|house>", "house": "<A|B|C, only with pick=house>"} ]}
- list: the first undelivered house in the order A, B, C
- nearest: the nearest undelivered house from where the robot stands
- farthest: the farthest undelivered house
- shortest_total: the first house of the shortest total route through the remaining houses
- house + "house": go to that specific house (if it is still undelivered)
At most 3 steps. Use ONLY these words. Do not invent fields.

Examples:
"nearest house first" -> {"name": "nearest first", "steps": [{"pick": "nearest"}]}
"shortest route overall" -> {"name": "shortest total route", "steps": [{"pick": "shortest_total"}]}
"C first, then whatever is closer" -> {"name": "C first, then nearest", "steps": [{"pick": "house", "house": "C"}, {"pick": "nearest"}]}
"B then A then C" -> {"name": "B, A, C", "steps": [{"pick": "house", "house": "B"}, {"pick": "house", "house": "A"}, {"pick": "house", "house": "C"}]}
"""


def generate_program(wish, previous=None, failure=None):
    user = "Wish: " + wish
    if previous is not None:
        user += "\nYour previous program was rejected: " + str(failure)[:300] + "\nPrevious program: " + json.dumps(previous) + "\nFix it using only the grammar."
    body = {"model": MODEL, "temperature": 0.2, "max_tokens": 300,
            "response_format": {"type": "json_object"},
            "messages": [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user}]}
    req = urllib.request.Request("https://openrouter.ai/api/v1/chat/completions", data=json.dumps(body).encode(),
                                 headers={"Authorization": "Bearer " + OPENROUTER_KEY, "Content-Type": "application/json",
                                          "HTTP-Referer": "https://extella.dev/robot/", "X-Title": "Extella robot demo"})
    t0 = time.time()
    CALLS.n = calls_count() + 1
    with urllib.request.urlopen(req, timeout=60) as r:
        out = json.loads(r.read())
    text = out["choices"][0]["message"]["content"].strip()
    if text.startswith("```"):
        text = text.strip("`")
        text = text[text.find("{"):]
    usage = out.get("usage", {})
    return json.loads(text), {"seconds": round(time.time() - t0, 1), "tokens": usage.get("total_tokens"), "model": out.get("model", MODEL)}


def registrar_sha():
    """sha256 of the deployed handler text without its HANDLER_SHA line; must equal handler_sha reported by runs."""
    try:
        reg = open(REGISTRAR_FILE, encoding="utf-8").read()
        body = reg.split("'" * 3)[1]
        lines = [ln for ln in body.split(chr(10)) if not ln.startswith("HANDLER_SHA = ")]
        return hashlib.sha256(chr(10).join(lines).encode("utf-8")).hexdigest()
    except Exception:
        return None


def skill_id(program, session):
    raw = json.dumps(program, sort_keys=True) + "|" + session
    return "skill_" + hashlib.sha256(raw.encode()).hexdigest()[:10]


# ---------------------------------------------------------------- limits & sessions

def allowed(ip):
    now = time.time()
    with LOCK:
        hits = [t for t in STATE["rate"].get(ip, []) if now - t < 60]
        hits.append(now)
        STATE["rate"][ip] = hits
        return len(hits) <= 60


def session(sid):
    with LOCK:
        if STATE["day"] != time.strftime("%Y-%m-%d"):
            STATE["day"] = time.strftime("%Y-%m-%d")
            STATE["generations_today"] = 0
        return STATE["sessions"].setdefault(sid, {"no_ai": False, "generations": 0})


def take_generation(sid):
    s = session(sid)
    with LOCK:
        if s["no_ai"]:
            return "AI is switched off for this session. Edit the rule to switch it on again."
        if STATE["generations_today"] >= DAY_LIMIT:
            return "today's generation budget is spent; try tomorrow or run the examples"
        if s["generations"] >= SESSION_LIMIT:
            return "this session used its %d generations" % SESSION_LIMIT
        s["generations"] += 1
        STATE["generations_today"] += 1
    return None


# ---------------------------------------------------------------- the teach path

def teach_path(program, sid, parent="", wish="", steps=None, device="vps"):
    """grammar -> test levels -> save expert -> run on level 1 through the platform."""
    steps = steps if steps is not None else []
    ok, reason = core.validate(program)
    steps.append({"step": "grammar", "ok": ok, "seconds": 0.0, "detail": "every word is in the route language" if ok else reason})
    if not ok:
        return {"status": "failed", "steps": steps, "program": program}
    t0 = time.time()
    ok, report = core.check_program(program)
    steps.append({"step": "test_levels", "ok": ok, "seconds": round(time.time() - t0, 2),
                  "detail": ("%d levels, all three houses delivered each time" % len(report.get("levels", []))) if ok else report})
    if not ok:
        return {"status": "failed", "steps": steps, "program": program}
    sid_skill = skill_id(program, sid)
    t0 = time.time()
    desc = "Route expert taught on the page. Wish: %s. Rule: %s" % (wish[:120], core.describe(program))
    out = save_skill(sid_skill, program, desc)
    ok = out.get("status") == "success"
    steps.append({"step": "save_expert", "ok": ok, "seconds": round(time.time() - t0, 1), "detail": sid_skill if ok else json.dumps(out)[:200]})
    if not ok:
        return {"status": "failed", "steps": steps, "program": program}
    ep = run_skill(sid_skill, {"level": "level1", "mode": "run"}, device)
    ok = ep.get("status") == "success"
    steps.append({"step": "run_on_platform", "ok": ok, "seconds": ep.get("platform_seconds"),
                  "detail": ("%s · %s · %d moves · model calls 0" % (ep.get("host"), "".join(ep.get("order", [])), ep.get("moves", 0))) if ok else ep.get("reason")})
    if not ok:
        return {"status": "failed", "steps": steps, "program": program}
    return {"status": "success", "skill": sid_skill, "program": program, "rule_text": core.describe(program), "episode": ep, "steps": steps}


# ---------------------------------------------------------------- http

class Handler(BaseHTTPRequestHandler):
    server_version = "extella-robot/2"

    def _send(self, code, obj):
        data = json.dumps(obj, ensure_ascii=False).encode()
        self.send_response(code)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(data)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(data)

    def _body(self):
        n = int(self.headers.get("Content-Length") or 0)
        if n > 20000:
            return None
        try:
            return json.loads(self.rfile.read(n) or b"{}")
        except Exception:
            return None

    def _ip(self):
        return self.headers.get("X-Forwarded-For", self.client_address[0]).split(",")[0].strip()

    def log_message(self, fmt, *args):
        pass

    def do_GET(self):
        calls_reset()
        if not allowed(self._ip()):
            return self._send(429, {"status": "error", "reason": "too many requests, slow down"})
        path = self.path.split("?")[0]
        if path == "/api/robot/state":
            session("_probe")
            return self._send(200, {"status": "success", "examples": EXAMPLES, "level1": core.LEVEL1, "model": MODEL if OPENROUTER_KEY else None,
                                    "generations_left_today": max(0, DAY_LIMIT - STATE["generations_today"]), "per_session": SESSION_LIMIT,
                                    "devices": {k: {"name": v["name"], "label": v["label"]} for k, v in DEVICES.items()},
                                    "laws": LAWS, "language": "route"})
        if path == "/api/robot/library":
            lib = kv_get(LIBRARY_KEY) or []
            return self._send(200, {"status": "success", "skills": [{k: v for k, v in s.items() if k != "key_hash"} for s in lib]})
        if path.startswith("/api/robot/skill/"):
            sid = path.split("/")[4]
            lib = kv_get(LIBRARY_KEY) or []
            entry = next((s for s in lib if s["id"] == sid), None)
            if not entry:
                return self._send(404, {"status": "error", "reason": "no such expert in the library"})
            return self._send(200, {"status": "success", "skill": {k: v for k, v in entry.items() if k != "key_hash"}})
        if path.startswith("/api/robot/export/"):
            sid = path.split("/")[4]
            if not sid.startswith("skill_"):
                return self._send(400, {"status": "error", "reason": "unknown expert"})
            try:
                ex = platform("/expert/get", {"name": sid}, timeout=30)
            except urllib.error.HTTPError:
                return self._send(404, {"status": "error", "reason": "no such expert"})
            if ex.get("status") != "success" or not ex.get("expert_code"):
                return self._send(404, {"status": "error", "reason": "no such expert"})
            try:
                registrar = open(REGISTRAR_FILE, encoding="utf-8").read()
            except OSError:
                return self._send(500, {"status": "error", "reason": "language handler file is missing on the server"})
            bundle = {"format": "extella-expert-bundle/1", "handler_sha": registrar_sha(),
                      "verify": "take the handler text between the triple quotes, delete the line starting with HANDLER_SHA, sha256 the rest; every run reports the same value as handler_sha", "exported_at": time.strftime("%Y-%m-%d %H:%M UTC", time.gmtime()), "from": "https://extella.dev/robot/",
                      "experts": [
                          {"name": "demo_register_route", "cspl": "fython", "code": registrar, "kwargs": {},
                           "description": "Registers the route language handler on the device it runs on. Run once per device.", "role": "language handler (run once on your device)"},
                          {"name": sid, "cspl": "route", "code": ex["expert_code"], "kwargs": {"seed": 0, "mode": "run"},
                           "description": (ex.get("expert_description") or "")[:300], "role": "the expert (a route program)"}],
                      "how_to": ["save both experts into your account: POST https://api.extella.ai/api/expert/save with headers X-Auth-Token, X-Profile-Id: default, X-Agent-Id",
                                 "run demo_register_route once with targets = [your device id]",
                                 "run " + sid + " with params {level: level1 | seed7, mode: run} and the same targets",
                                 "or simply: python3 take_home.py https://extella.dev/api/robot/export/" + sid]}
            return self._send(200, bundle)
        return self._send(404, {"status": "error", "reason": "unknown path"})

    def do_POST(self):
        calls_reset()
        if not allowed(self._ip()):
            return self._send(429, {"status": "error", "reason": "too many requests, slow down"})
        path = self.path.split("?")[0]
        body = self._body()
        if body is None:
            return self._send(400, {"status": "error", "reason": "bad json"})

        if path == "/api/robot/run":
            skill = str(body.get("skill", ""))
            skill = next((e["expert"] for e in EXAMPLES if e["id"] == skill), skill)
            if not skill.startswith("skill_"):
                return self._send(400, {"status": "error", "reason": "unknown expert"})
            device = str(body.get("device") or "vps")
            if device not in DEVICES:
                return self._send(400, {"status": "error", "reason": "unknown device"})
            level = str(body.get("level") or "level1")
            if level != "level1" and not (level.startswith("seed") and level[4:].isdigit() and 0 < int(level[4:]) < 10 ** 6):
                return self._send(400, {"status": "error", "reason": "level must be level1 or seed<N>"})
            law = body.get("law") or None
            if law is not None and law not in LAWS:
                return self._send(400, {"status": "error", "reason": "unknown law"})
            params = {"level": level, "mode": "run"}
            if law:
                params["law"] = law
            res = run_skill(skill, params, device)
            res["model_calls_measured"] = calls_count()     # provider calls made by the proxy while serving THIS request
            res["handler_sha_expected"] = registrar_sha()
            if res.get("status") == "success":
                self._bump_runs(skill)
            return self._send(200, res)

        if path == "/api/robot/teach":
            wish = str(body.get("wish", "")).strip()[:200]
            sid = str(body.get("session", ""))[:64] or self._ip()
            if len(wish) < 3:
                return self._send(400, {"status": "error", "reason": "say what the robot should do, in a sentence"})
            if not OPENROUTER_KEY:
                return self._send(503, {"status": "error", "reason": "generation is not configured on this server; pick an example"})
            refusal = take_generation(sid)
            if refusal:
                return self._send(403, {"status": "no_ai", "reason": refusal})
            steps = []
            t0 = time.time()
            try:
                program, meta = generate_program(wish)
            except Exception as err:
                steps.append({"step": "generate", "ok": False, "seconds": round(time.time() - t0, 1), "detail": "model call failed: " + str(err)[:120]})
                return self._send(200, {"status": "failed", "steps": steps})
            steps.append({"step": "generate", "ok": True, "seconds": meta["seconds"], "detail": "%s · %s tokens" % (meta["model"], meta["tokens"])})
            out = teach_path(program, sid, wish=wish, steps=steps)
            if out["status"] != "success" and out["steps"] and out["steps"][1]["step"] in ("grammar", "test_levels") and not out["steps"][1]["ok"]:
                try:
                    program2, meta2 = generate_program(wish, program, out["steps"][1]["detail"])
                    steps2 = [{"step": "generate", "ok": True, "seconds": meta2["seconds"], "detail": "second attempt · %s tokens" % meta2["tokens"]}]
                    out2 = teach_path(program2, sid, wish=wish, steps=steps2)
                    out2["steps"] = out["steps"] + out2["steps"]
                    out = out2
                except Exception as err:
                    out["steps"].append({"step": "generate", "ok": False, "seconds": 0, "detail": "second attempt failed: " + str(err)[:120]})
            out["wish"] = wish
            out["model_calls_create"] = calls_count()       # provider calls made while creating this expert (1 or 2)
            if out.get("episode"):
                out["episode"]["model_calls_measured"] = 0  # the run inside teach_path is a platform call; provider calls are counted above
                out["episode"]["handler_sha_expected"] = registrar_sha()
            return self._send(200, out)

        if path == "/api/robot/teach-program":
            program = body.get("program")
            sid = str(body.get("session", ""))[:64] or self._ip()
            if not isinstance(program, dict):
                return self._send(400, {"status": "error", "reason": "program must be an object"})
            out = teach_path(program, sid, parent=str(body.get("parent", ""))[:40], wish=str(body.get("wish", ""))[:200])
            return self._send(200, out)

        if path == "/api/robot/session/no-ai":
            sid = str(body.get("session", ""))[:64] or self._ip()
            s = session(sid)
            with LOCK:
                s["no_ai"] = bool(body.get("on", True))
            return self._send(200, {"status": "success", "no_ai": s["no_ai"]})

        if path == "/api/robot/share":
            sid_skill = str(body.get("skill", ""))
            if not sid_skill.startswith("skill_") or sid_skill in EXAMPLE_BY_EXPERT:
                return self._send(400, {"status": "error", "reason": "teach your own expert first"})
            program = body.get("program")
            ok, reason = core.validate(program) if isinstance(program, dict) else (False, "program missing")
            if not ok:
                return self._send(400, {"status": "error", "reason": reason})
            key = secrets.token_urlsafe(18)
            level = str(body.get("level") or "level1")
            if level != "level1" and not (level.startswith("seed") and level[4:].isdigit()):
                level = "level1"
            entry = {"id": sid_skill, "wish": str(body.get("wish", ""))[:200], "note": str(body.get("note", ""))[:140], "level": level,
                     "program": program, "rule_text": core.describe(program), "parent": str(body.get("parent", ""))[:40],
                     "created": time.strftime("%Y-%m-%d"), "runs": 0, "forks": 0, "key_hash": hashlib.sha256(key.encode()).hexdigest()}
            with LOCK:
                lib = kv_get(LIBRARY_KEY) or []
                if any(s["id"] == sid_skill for s in lib):
                    return self._send(409, {"status": "error", "reason": "already shared"})
                if entry["parent"]:
                    for s in lib:
                        if s["id"] == entry["parent"]:
                            s["forks"] = s.get("forks", 0) + 1
                lib.append(entry)
                kv_set(LIBRARY_KEY, lib[-200:], "Robot demo: shared route experts")
            return self._send(200, {"status": "success", "skill": sid_skill, "api_key": key,
                                    "api": {"method": "POST", "path": "/api/robot/%s/run" % sid_skill, "header": "X-Robot-Key: <key>", "body": {"level": "seed7"}}})

        if path.startswith("/api/robot/skill_") and path.endswith("/run"):
            sid_skill = path.split("/")[3]
            key = self.headers.get("X-Robot-Key", "")
            lib = kv_get(LIBRARY_KEY) or []
            entry = next((s for s in lib if s["id"] == sid_skill), None)
            if not entry or hashlib.sha256(key.encode()).hexdigest() != entry.get("key_hash"):
                return self._send(403, {"status": "error", "reason": "bad expert or key"})
            level = str(body.get("level") or "seed%d" % (int(time.time()) % 100000 + 1))
            if level != "level1" and not (level.startswith("seed") and level[4:].isdigit()):
                return self._send(400, {"status": "error", "reason": "level must be level1 or seed<N>"})
            res = run_skill(sid_skill, {"level": level, "mode": "run"})
            self._bump_runs(sid_skill)
            res["via"] = "api"
            return self._send(200, res)

        return self._send(404, {"status": "error", "reason": "unknown path"})

    def _bump_runs(self, sid_skill):
        if sid_skill in EXAMPLE_BY_EXPERT:
            return
        try:
            with LOCK:
                lib = kv_get(LIBRARY_KEY) or []
                for s in lib:
                    if s["id"] == sid_skill:
                        s["runs"] = s.get("runs", 0) + 1
                        kv_set(LIBRARY_KEY, lib, "Robot demo: shared route experts")
                        break
        except Exception:
            pass


if __name__ == "__main__":
    print("robot proxy v2 (route) on 127.0.0.1:%d, model %s" % (PORT, MODEL if OPENROUTER_KEY else "none"))
    ThreadingHTTPServer(("127.0.0.1", PORT), Handler).serve_forever()
