Zoo

← back to the zoo

14. Ask a model

Any app can ask a language model a question without holding a key.

The ai app holds the key; an app calls it with rc_app_headers("ai", true) and gets an answer marked who asked, for whom.

This is the code serving the zoo right now: read from disk for this request, from commit 0c1c124a02 (live). The zoo's own self-check fetches this page and compares it byte for byte with the file it runs.

exhibits/14-ask-a-model.php sha256 853ce0232029 · raw

1<?php
2// Exhibit 14. The zoo has no model key. It asks the ai app, over HTTPS, for the signed-in person:
3//     curl POST rc_app_url("ai") . "/?api=complete"   with rc_app_headers("ai", true)
4//     body: {"model": "fast", "max_tokens": 400, "messages": [{"role": "user", "content": "..."}]}
5// The answer is the Messages shape plus rc: {via, app, user}, which is what the card prints.
6
7function ask_ai(string $question, int $max = 400): array {
8    $body = json_encode(["model" => "fast", "max_tokens" => $max, "messages" => [["role" => "user", "content" => $question]]]);
9    [$s, $out] = call_app("ai", "/?api=complete", true, "POST", $body, ["Content-Type: application/json"], 60);
10    $r = json_decode($out, true);
11    if ($s !== 200 || !is_array($r)) return ["error" => ($r["error"]["message"] ?? "ai answered $s"), "status" => $s === 429 ? 429 : 502];
12    $text = trim(implode("", array_map(fn($b) => $b["type"] === "text" ? $b["text"] : "", $r["content"] ?? [])));
13    return ["answer" => $text, "via" => $r["rc"]["via"] ?? "", "for" => $r["rc"]["app"] ?? "", "model" => $r["model"] ?? "",
14            "tokens" => ($r["usage"]["input_tokens"] ?? 0) + ($r["usage"]["output_tokens"] ?? 0)];
15}
16
17return [
18    "n" => 14, "try" => "ask the model a question", "wing" => "Intelligence", "kind" => "self",
19    "title" => "Ask a model",
20    "promise" => "Any app can ask a language model a question without holding a key.",
21    "block" => 'The ai app holds the key; an app calls it with rc_app_headers("ai", true) and gets an answer marked who asked, for whom.',
22    "show" => function (?array $me): string {
23        if (!$me) return '<p class="muted"><a href="' . h(rc_signin_url()) . '">Sign in</a> to ask the model something.</p>';
24        return '<p class="muted small">One question at a time, up to 500 characters; each answer goes on your ledger (exhibit 15).</p><form class="row" id="ask-form"><input name="q" maxlength="500" placeholder="Try: name three Swedish lakes" required><button class="primary">Ask</button></form>'
25             . '<div id="ask-out" class="note"></div>';
26    },
27    "api" => function (string $do, ?array $me, array $in, bool $post): ?array {
28        if ($do !== "ask" || !$post) return null;
29        if (!$me) return ["error" => "sign in first", "status" => 401];
30        $q = trim(mb_substr((string)($in["question"] ?? ""), 0, 500));
31        if ($q === "") return ["error" => "ask something", "status" => 400];
32        return ask_ai($q, min(400, max(1, (int)($in["max_tokens"] ?? 400))));
33    },
34    // The robot asks a fixed cheap question; and nothing key-shaped may sit in what we serve.
35    "check" => function (): array {
36        require_once ZOO_ROOT . "/lib/robot.php";
37        [$code, , $j] = robot_at_zoo()->post("https://" . env("RC_HOST") . "/api/14/ask", ["question" => "Reply with the single word: pong", "max_tokens" => 20]);
38        if ($code !== 200 || !is_array($j)) return [false, "the question failed ($code): " . (is_array($j) ? ($j["error"] ?? "") : substr((string)$j, 0, 120))];
39        if ($j["answer"] === "") return [false, "ai answered, but with nothing"];
40        if ($j["via"] !== "ai" || $j["for"] !== "zoo") return [false, "the answer is not marked 'via ai, for zoo' (via '{$j['via']}', for '{$j['for']}')"];
41        $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(ZOO_ROOT, FilesystemIterator::SKIP_DOTS));
42        foreach ($it as $f) {
43            if (!preg_match('/\.(php|js|css|conf|md)$/', $f->getFilename()) || str_contains($f->getPathname(), "/.git/")) continue;
44            if (preg_match('/\bsk-[A-Za-z0-9_-]{20,}|\b[0-9a-f]{32}\.[A-Za-z0-9]{12,}|-----BEGIN [A-Z ]*PRIVATE KEY/', (string)file_get_contents($f->getPathname()))) return [false, "something key-shaped is in " . substr($f->getPathname(), strlen(ZOO_ROOT) + 1)];
45        }
46        return [true, "asked the fast model through ai as the robot: \"" . mb_substr($j["answer"], 0, 40) . "\" ({$j['tokens']} tokens); no key in the zoo's files", ["tokens" => $j["tokens"]]];
47    },
48    "script" => <<<'JS'
49document.getElementById("ask-form")?.addEventListener("submit", async (ev) => {
50  ev.preventDefault();
51  const out = document.getElementById("ask-out"), btn = ev.target.querySelector("button");
52  btn.disabled = true; out.textContent = "thinking…";
53  const j = await zoo.call("/api/14/ask", { question: ev.target.q.value });
54  btn.disabled = false;
55  if (j.error) { out.textContent = j.error; return; }
56  out.innerHTML = '<div class="answer">' + zoo.md(j.answer) + '</div><p class="muted">answered via ' + zoo.esc(j.via) + ', for ' + zoo.esc(j.for) + ', on your behalf (' + zoo.esc(j.model) + ', ' + j.tokens + ' tokens)</p>';
57  window.dispatchEvent(new Event("zoo-asked"));
58});
59
60JS,
61];