okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petgleam 1.19: reformat constants, require 1.19, build it in nix until nixpkgs-unstable has it5f9679f

main
6.8 KiB169 linesraw
1//// Opt-in live integration: gleam run -m manual/coding
2//// Uses ALBEDO_BENCH_URL/MODEL/KEY and ALBEDO_LIVE_WORKSPACE/DATABASE/PROTOCOL.
3//// No credentials are retained in the kernel environment or output.
4
5import albedo/harness/extensions/work/ledger as work
6import albedo/harness/runtime
7import albedo/openai_api as openai
8import albedo/openai_api/types
9import gleam/io
10import gleam/json
11import gleam/list
12import gleam/option.{Some}
13import gleam/result
14import gleam/string
15
16@external(erlang, "albedo_openai_bench", "env")
17fn env(name: String) -> String
18
19@external(erlang, "albedo_live_coding", "take_key")
20fn take_key() -> String
21
22@external(erlang, "albedo_live_coding", "now")
23fn now() -> Int
24
25@external(erlang, "albedo_live_coding", "with_runtime")
26fn with_runtime(host: runtime.Runtime, run: fn() -> a) -> a
27
28pub fn main() -> Nil {
29  let key = take_key()
30  let workspace = env("ALBEDO_LIVE_WORKSPACE")
31  case
32    key == ""
33    || workspace == ""
34    || env("ALBEDO_BENCH_URL") == ""
35    || env("ALBEDO_BENCH_MODEL") == ""
36    || env("ALBEDO_LIVE_DATABASE") == ""
37  {
38    True -> io.println("missing live-test environment; no requests made")
39    False -> {
40      let protocol = case env("ALBEDO_LIVE_PROTOCOL") {
41        "chat_completions" -> types.ChatCompletions
42        _ -> types.Responses
43      }
44      let client =
45        types.Client(
46          ..openai.client(protocol, env("ALBEDO_BENCH_URL"), key),
47          timeout_ms: 90_000,
48        )
49      let assert Ok(host) = runtime.start(env("ALBEDO_LIVE_DATABASE"))
50      use <- with_runtime(host)
51      let assert Ok(session) = runtime.open_session(host, "coding", workspace)
52      let request =
53        types.Request(
54          ..openai.request(env("ALBEDO_BENCH_MODEL"), [types.User(task)]),
55          instructions: Some(instructions),
56          tools: runtime.tools(session),
57          max_output_tokens: Some(4096),
58        )
59      let start = now()
60      let result = run(client, request, host, session, 1, 0)
61      let ledger =
62        work.list(runtime.ledger(host), workspace, 0, 50) |> result.unwrap([])
63      log([
64        #("event", json.string("summary")),
65        #("elapsed_ms", json.int(now() - start)),
66        #(
67          "result",
68          json.string(
69            string.inspect(result) |> string.replace(key, "[redacted]"),
70          ),
71        ),
72        #("ledger", json.array(ledger, work.to_json)),
73      ])
74      let assert Ok(_) = result
75      Nil
76    }
77  }
78}
79
80fn run(
81  client: types.Client,
82  request: types.Request,
83  host: runtime.Runtime,
84  session: runtime.Session,
85  step: Int,
86  tools: Int,
87) -> Result(#(Int, Int), String) {
88  case step > 20 || tools > 30 {
89    True -> Error("live test turn/tool limit reached")
90    False -> {
91      let start = now()
92      use turn <- result.try(
93        openai.stream(client, request, fn(_) { types.Continue })
94        |> result.map_error(fn(error) {
95          string.inspect(error) |> string.replace(client.api_key, "[redacted]")
96        }),
97      )
98      log([
99        #("event", json.string("model_turn")),
100        #("step", json.int(step)),
101        #("elapsed_ms", json.int(now() - start)),
102        #("calls", json.int(list.length(turn.tool_calls))),
103        #("finish", json.string(string.inspect(turn.finish))),
104        #("usage", json.string(string.inspect(turn.usage))),
105      ])
106      case turn.tool_calls {
107        [] -> {
108          log([
109            #("event", json.string("final")),
110            #("output", json.array(turn.output, types.replay_json)),
111          ])
112          case turn.finish {
113            types.Complete -> Ok(#(step, tools))
114            _ -> Error("model stopped without completion")
115          }
116        }
117        calls -> {
118          use outputs <- result.try(
119            list.try_map(calls, fn(call) {
120              log([
121                #("event", json.string("tool_call")),
122                #("id", json.string(call.id)),
123                #("arguments", json.string(call.arguments)),
124              ])
125              use output <- result.try(runtime.invoke(
126                host,
127                session,
128                call,
129                types.any_images,
130              ))
131              case output {
132                types.ToolOutput(_, text, _) ->
133                  log([
134                    #("event", json.string("tool_result")),
135                    #("body", json.string(text)),
136                  ])
137                _ -> Nil
138              }
139              Ok(output)
140            }),
141          )
142          let input =
143            list.append(
144              request.input,
145              list.append(list.map(turn.output, types.Replay), outputs),
146            )
147          run(
148            client,
149            types.Request(..request, input: input),
150            host,
151            session,
152            step + 1,
153            tools + list.length(calls),
154          )
155        }
156      }
157    }
158  }
159}
160
161fn log(fields: List(#(String, json.Json))) -> Nil {
162  fields |> json.object |> json.to_string |> io.println
163}
164
165const instructions =
166  "You are testing albedo by solving a real coding task. Your only tool is python. Its namespace persists across calls, and top-level await is supported. Use pathlib for files. await work.create(title, notes=...) returns a dict with id and revision; await work.update(id, revision=..., status=...) uses optimistic revisions. These methods are coroutines: always await them, including when assigning their result. run(program, *args, timeout=seconds) starts a job without a shell; await its handle and print(job.tail()) to see results. Use cells.read/cells.run if a cell fails. Work only in your current workspace. Do not inspect credentials, environment variables, other directories, or network services. Do not modify any README. You must actually create files and run tests, not merely describe code. Use at least two python calls and retain a variable between calls. Once your tests pass, update the work item to done and finish with a brief account of what passed. Do not keep adding unrelated tests after success."
167
168const task =
169  "Implement interval_set.py using only the Python standard library. Public functions normalize(intervals), union(left, right), intersection(left, right), and difference(left, right) return canonical lists of (start, end) integer tuples for half-open intervals [start,end). Inputs may be unsorted, overlap, contain duplicates or empty intervals, and may be one-shot generators. Drop empty intervals; reject any reversed interval (start > end) with ValueError. Canonical output is sorted, nonempty, disjoint, and merges touching intervals. Do not mutate caller input. difference means left minus right. Do not enumerate integer points: endpoints can be arbitrarily large. Aim for sorting plus linear sweeps. Create a work item and mark it done only after tests pass. Write test_interval_set.py with edge cases and deterministic randomized checks, then execute it through bash from the python tool. Do not install dependencies. Keep the implementation readable."