glasskit.ai 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 @tash-2s
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,596 @@
1
+ Metadata-Version: 2.4
2
+ Name: glasskit.ai
3
+ Version: 0.1.0
4
+ Summary: Recorded-video eval tools for smart-glasses apps
5
+ Keywords: ai,cli,evals,smart-glasses,testing,video
6
+ Author: RealComputer
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Multimedia :: Video
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Classifier: Topic :: Utilities
18
+ Requires-Dist: av
19
+ Requires-Dist: pillow
20
+ Requires-Dist: pydantic>=2
21
+ Requires-Dist: pyyaml
22
+ Requires-Dist: rich
23
+ Requires-Dist: typer
24
+ Requires-Python: >=3.12
25
+ Project-URL: Homepage, https://glasskit.ai
26
+ Project-URL: Documentation, https://glasskit.ai/docs
27
+ Project-URL: Repository, https://github.com/RealComputer/GlassKit
28
+ Project-URL: Issues, https://github.com/RealComputer/GlassKit/issues
29
+ Description-Content-Type: text/markdown
30
+
31
+ # GlassKit CLI
32
+
33
+ This is the GlassKit command-line package. Its first command family is `glasskit eval`, a recorded-video evaluator for smart-glasses apps.
34
+
35
+ Smart-glasses apps are hard to test by hand because the input is physical, visual, and timing-sensitive. Given a recording of a workflow, `glasskit eval` lets you label the important moments and rerun the same checks whenever your prompts, model, parser, or app logic changes. Your adapter owns the app-specific call; the CLI handles video decoding, timestamp sampling, comparisons, reports, failure artifacts, and quality gates.
36
+
37
+ The current implementation assumes you use a `uv`-managed Python pipeline. If your use case does not fit the current model, please open an issue. We want to expand support based on real app needs.
38
+
39
+ ## Why Use `glasskit eval`?
40
+
41
+ - Turn real camera recordings into repeatable tests instead of relying on memory, screenshots, or manual replay.
42
+ - Test the vision path that users actually depend on: frames, prompts, model calls, response parsing, app logic, and thresholds.
43
+ - Label only stable moments in the video and skip ambiguous transitions that would make a test noisy.
44
+ - Keep app-specific behavior in your adapter while reusing the CLI for video handling, comparison modes, JSON reports, and failure images.
45
+ - Run the same suite locally and in CI with exit codes that distinguish setup errors from quality-gate failures.
46
+
47
+ ## What You Need
48
+
49
+ - A short recording of the workflow you want to evaluate.
50
+ - An `expected.yaml` file that says which timestamps or ranges should be checked and what each result should be.
51
+ - A Python adapter function or object that receives decoded frames and returns JSON-like observations.
52
+ - A `uv` command environment that can install `glasskit.ai` and your app's runtime dependencies.
53
+
54
+ ## Install
55
+
56
+ Use the CLI from the app repository that contains the eval suite, adapter, and app dependencies. Running from that directory keeps imports and relative paths predictable. If your app uses a `.env` file, pass it through the command runner, for example with `uv run --env-file .env`; the CLI does not load `.env` files by itself. With `uv`, install the published `glasskit.ai` package into the command environment and invoke the `glasskit` console script in one command:
57
+
58
+ ```bash
59
+ cd path/to/your-app
60
+ uv run --with glasskit.ai glasskit eval --help
61
+ ```
62
+
63
+ If you already installed the command another way (`uv add --dev glasskit.ai`), you can drop the `uv run --with glasskit.ai` prefix and run `glasskit eval ...` directly.
64
+
65
+ Run help when you need the exact options for the installed version:
66
+
67
+ ```bash
68
+ uv run --with glasskit.ai glasskit --help
69
+ uv run --with glasskit.ai glasskit eval --help
70
+ uv run --with glasskit.ai glasskit eval run --help
71
+ ```
72
+
73
+ ## Quick Start
74
+
75
+ Run these commands from your app repository so local imports, adapter files, and relative asset paths resolve the same way they do in your app. Add `--env-file .env` to `uv run` if your adapter expects environment variables from that file.
76
+
77
+ ### 1. Create a Case from a Recording
78
+
79
+ ```bash
80
+ uv run --with glasskit.ai glasskit eval init-case \
81
+ --suite eval-suite \
82
+ --case fold-step-001 \
83
+ --video path/to/recording.mp4 \
84
+ --target step_1 \
85
+ --label "Step 1"
86
+ ```
87
+
88
+ This creates `eval-suite/fold-step-001/`, copies the recording into the case directory, and writes a starter `expected.yaml`.
89
+
90
+ ### 2. Label the Moments You Care About
91
+
92
+ Edit `eval-suite/fold-step-001/expected.yaml` so the timestamps and expected values match the video. Start with one or two unambiguous samples:
93
+
94
+ ```yaml
95
+ version: 1
96
+ video: "video.mp4"
97
+ sampling:
98
+ every_s: 0.5
99
+ targets:
100
+ step_1:
101
+ label: Step 1
102
+ samples:
103
+ - at: 2.0
104
+ field: matches
105
+ expect: true
106
+ - range: [3.0, 5.0]
107
+ field: matches
108
+ expect: true
109
+ ```
110
+
111
+ Use `at` for a single moment and `range` for a stable window. Avoid transition frames until you specifically want to measure transition behavior.
112
+
113
+ ### 3. Check the Wiring with a Fake Adapter
114
+
115
+ Create `eval_adapter.py` in your app repository:
116
+
117
+ ```python
118
+ def evaluate_sample(sample, target):
119
+ return {
120
+ "target": target.id,
121
+ "timestamp_s": sample.timestamp_s,
122
+ "matches": target.id == "step_1",
123
+ }
124
+ ```
125
+
126
+ This adapter does not judge the image. It only proves that the suite, video decoding, field extraction, comparison, and command wiring work before you connect a model backend.
127
+
128
+ ### 4. Validate and Inspect the Schedule
129
+
130
+ ```bash
131
+ uv run --with glasskit.ai glasskit eval validate --suite eval-suite --adapter eval_adapter.py:evaluate_sample
132
+ uv run --with glasskit.ai glasskit eval list-samples --suite eval-suite
133
+ ```
134
+
135
+ ### 5. Run the Eval
136
+
137
+ Run the fake adapter first:
138
+
139
+ ```bash
140
+ uv run --with glasskit.ai glasskit eval run \
141
+ --adapter eval_adapter.py:evaluate_sample \
142
+ --suite eval-suite \
143
+ --min-pass-rate 1.0
144
+ ```
145
+
146
+ After that passes, replace the fake adapter body with the real call into your app or model backend and run with the options you want for local debugging or CI:
147
+
148
+ ```bash
149
+ uv run --with glasskit.ai glasskit eval run \
150
+ --adapter eval_adapter.py:create_evaluator \
151
+ --suite eval-suite \
152
+ --min-pass-rate 0.9 \
153
+ --output-json tmp/eval-results.json \
154
+ --save-failures \
155
+ --artifacts-dir tmp/eval-artifacts
156
+ ```
157
+
158
+ The command exits `0` when all quality gates pass, `1` when the eval ran but one or more gates failed, and `2` for setup or runtime errors such as invalid YAML, unreadable videos, or adapter failures that are not being collected with `--keep-going`.
159
+
160
+ ## Recommended App Repo Layout
161
+
162
+ Keep the eval suite next to the adapter and app code that it exercises. This makes imports and relative asset paths predictable, and keeps environment handling close to the code that needs it.
163
+
164
+ ```text
165
+ your-app/
166
+ eval_adapter.py
167
+ adapter-config.yaml
168
+ eval-suite/
169
+ fold-step-001/
170
+ video.mp4
171
+ expected.yaml
172
+ fold-step-002/
173
+ video.mp4
174
+ expected.yaml
175
+ ```
176
+
177
+ Commit the suite files that your team should share. Keep secrets in environment variables or uncommitted environment files, not in `expected.yaml` or `adapter-config.yaml`.
178
+
179
+ ## Eval Suite Layout
180
+
181
+ An eval suite is a directory containing one or more case directories. Each case has an `expected.yaml` file and either one video file in the case directory or a `video:` path in `expected.yaml`.
182
+
183
+ ```text
184
+ eval-suite/
185
+ fold-step-001/
186
+ video.mp4
187
+ expected.yaml
188
+ fold-step-002/
189
+ camera-recording.mov
190
+ expected.yaml
191
+ ```
192
+
193
+ A single-case suite is also supported by placing `expected.yaml` directly in the suite directory.
194
+
195
+ Supported video suffixes are `.mp4`, `.mov`, `.m4v`, `.webm`, and `.mkv`. Timestamps in `expected.yaml` are seconds from the start of the clip, even when the container stores non-zero presentation timestamps internally.
196
+
197
+ ## Writing `expected.yaml`
198
+
199
+ Here is a representative case file:
200
+
201
+ ```yaml
202
+ version: 1
203
+ video: video.mp4
204
+ description: Fold step 1 should be detected after the crease is completed.
205
+ sampling:
206
+ every_s: 0.5
207
+ workflow:
208
+ targets:
209
+ - id: step_1
210
+ label: Step 1
211
+ prompt_id: origami.step_1
212
+ targets:
213
+ step_1:
214
+ label: Step 1
215
+ config:
216
+ reference_image: assets/step_1.png
217
+ samples:
218
+ - range: [0.0, 6.8]
219
+ expect: false
220
+ - range: [7.4, 11.8]
221
+ expect: true
222
+ step_2:
223
+ label: Step 2
224
+ samples:
225
+ - at: [4.0, 6.0]
226
+ expect: false
227
+ thresholds:
228
+ min_pass_rate: 0.9
229
+ max_failures: 2
230
+ per_target:
231
+ step_1:
232
+ min_pass_rate: 0.95
233
+ ```
234
+
235
+ Ranges are interpreted as `[start, end)`. With `sampling.every_s: 0.5`, `range: [7.4, 8.6]` expands to samples at `7.4`, `7.9`, and `8.4` seconds. Only declared `range` and `at` samples are evaluated; unlabeled gaps are skipped. A sample block must contain exactly one of `range` or `at`.
236
+
237
+ Use ranges for stable windows where the expected answer should be unchanged. Use `at` for isolated moments or when a transition is too short to sample safely. Avoid labeling ambiguous transition frames unless the ambiguity is exactly what you want to measure.
238
+
239
+ ### Case Fields
240
+
241
+ - `version` must be `1`.
242
+ - `video` is an optional path to the case video, resolved relative to the case directory. If it is omitted, the case directory must contain exactly one supported video file.
243
+ - `description` is optional and only for humans.
244
+ - `sampling.every_s` sets the default sample interval for `range` blocks in the case. The default is `0.5` seconds.
245
+ - `workflow.targets` is optional metadata matched to targets by each entry's `id`. Each entry must have `id`; `label` is optional; extra fields are passed to the adapter as `target.config` unless overridden by `targets.<id>.config`.
246
+ - `targets.<target_id>.label` is optional display text for reports.
247
+ - `targets.<target_id>.config` is optional adapter-specific metadata for that target. This is where you can put prompt ids, reference image paths, class names, or other app-level data that the core CLI should not know about.
248
+ - `targets.<target_id>.samples` is the required list of labeled sample blocks.
249
+ - `thresholds` is optional case-level gating. It can contain `min_pass_rate`, `max_failures`, and `per_target.<target_id>.min_pass_rate`.
250
+
251
+ ## Expected Values and Comparison
252
+
253
+ The adapter returns a JSON-like value: `null`, boolean, number, string, array, or object. Each sample's `expect` value is compared with that returned value.
254
+
255
+ By default, booleans, strings, and `null` use exact comparison. Numbers use numeric comparison with zero tolerance unless you set one. Arrays and objects use exact comparison unless you choose another mode.
256
+
257
+ Use `field` when the adapter returns a structured object but the sample only cares about one nested value:
258
+
259
+ ```yaml
260
+ targets:
261
+ detector:
262
+ samples:
263
+ - at: 2.0
264
+ field: result.matches
265
+ expect: true
266
+ ```
267
+
268
+ Field paths are dot-separated. Mapping keys are matched by name, and list indexes can be addressed with non-negative numeric path parts such as `detections.0.label`.
269
+
270
+ Supported `compare.mode` values are:
271
+
272
+ ```yaml
273
+ targets:
274
+ score:
275
+ samples:
276
+ - at: 1.0
277
+ expect: 0.75
278
+ compare:
279
+ mode: numeric
280
+ tolerance: 0.05
281
+ metadata:
282
+ samples:
283
+ - at: 1.0
284
+ expect:
285
+ result:
286
+ matches: true
287
+ compare:
288
+ mode: json_subset
289
+ objects:
290
+ samples:
291
+ - at: 1.0
292
+ expect: ["paper", "crease"]
293
+ compare:
294
+ mode: set_contains_all
295
+ ```
296
+
297
+ - `exact` requires the observed value to equal `expect`.
298
+ - `numeric` requires both values to be numbers and allows `tolerance`.
299
+ - `json_subset` requires every key and value in `expect` to be present in the observed object. For arrays, each expected item must match at least one observed item.
300
+ - `set_equals` compares arrays as unordered sets.
301
+ - `set_contains_any` passes when at least one expected array item is present in the observed array.
302
+ - `set_contains_all` passes when every expected array item is present in the observed array.
303
+
304
+ ## Suite-Level Thresholds
305
+
306
+ Put thresholds that should apply to the selected run as a whole in `suite.yaml` at the suite root. Suite-level `min_pass_rate` and `max_failures` gates are evaluated against the combined selected results. Suite-level `per_target` entries apply across the selected samples for each target id:
307
+
308
+ ```yaml
309
+ thresholds:
310
+ min_pass_rate: 0.9
311
+ max_failures: 5
312
+ per_target:
313
+ step_1:
314
+ min_pass_rate: 0.95
315
+ step_2:
316
+ min_pass_rate: 0.85
317
+ ```
318
+
319
+ Every run also includes an `adapter_errors` gate. The run only succeeds if the adapter produced no runtime or comparison errors and every configured quality gate passed.
320
+
321
+ Failed comparisons are intentionally controlled by quality gates instead of a built-in default failure policy. If you run without `--min-pass-rate`, `--min-target-pass-rate`, `--max-failures`, or YAML thresholds, failed comparisons are reported in the summary and JSON output but do not make the command exit nonzero; adapter, runtime, or comparison errors still fail through the `adapter_errors` gate. Configure a pass-rate or max-failures gate for CI or any run where failed observations should fail the command.
322
+
323
+ CLI gates are useful for one-off CI jobs or local experiments:
324
+
325
+ ```bash
326
+ uv run --with glasskit.ai glasskit eval run \
327
+ --adapter eval_adapter.py:create_evaluator \
328
+ --suite eval-suite \
329
+ --min-pass-rate 0.9 \
330
+ --min-target-pass-rate 0.85 \
331
+ --max-failures 3
332
+ ```
333
+
334
+ `--min-pass-rate` and `--max-failures` override suite-level YAML values for the run. Because those flags define a run-level pass/fail policy, case-level YAML gates are not applied when either flag is set. `--min-target-pass-rate` applies the same target pass-rate gate to every target present in the selected results; when it is set, suite-level `per_target` gates are replaced by the uniform CLI gate. With `--case`, suite-level per-target gates for targets outside the selected case are skipped.
335
+
336
+ ## Commands
337
+
338
+ ### `glasskit eval init-case`
339
+
340
+ `init-case` creates a case directory, copies the source video into it when needed, and writes a starter `expected.yaml`:
341
+
342
+ ```bash
343
+ uv run --with glasskit.ai glasskit eval init-case \
344
+ --suite eval-suite \
345
+ --case fold-step-001 \
346
+ --video recordings/fold-step-001.mp4 \
347
+ --target step_1 \
348
+ --label "Step 1"
349
+ ```
350
+
351
+ The case name must be a single directory name under the suite. If the source video is already inside the case directory, the generated `video:` path is written relative to the case directory. Use `--force` to overwrite an existing `expected.yaml` or case video.
352
+
353
+ ### `glasskit eval validate`
354
+
355
+ `validate` checks suite structure, YAML schema, video readability, sample timestamps, and optional adapter importability:
356
+
357
+ ```bash
358
+ uv run --with glasskit.ai glasskit eval validate --suite eval-suite
359
+ uv run --with glasskit.ai glasskit eval validate --suite eval-suite --adapter eval_adapter.py:create_evaluator
360
+ uv run --with glasskit.ai glasskit eval validate --suite eval-suite --case fold-step-001
361
+ ```
362
+
363
+ Use validation before long or paid model evals. It catches most local mistakes without decoding sample frames or calling `evaluate`. Passing `--adapter` imports, constructs, and closes the adapter, so adapter setup side effects can still run.
364
+
365
+ ### `glasskit eval list-samples`
366
+
367
+ `list-samples` prints the expanded sample schedule:
368
+
369
+ ```bash
370
+ uv run --with glasskit.ai glasskit eval list-samples --suite eval-suite
371
+ uv run --with glasskit.ai glasskit eval list-samples --suite eval-suite --case fold-step-001
372
+ ```
373
+
374
+ This is the quickest way to confirm that your ranges, point samples, fields, and explicit comparison modes expand as intended. When a sample omits `compare.mode`, the Mode column is blank because the default mode is inferred when the sample is evaluated.
375
+
376
+ ### `glasskit eval run`
377
+
378
+ `run` decodes sample frames, calls the adapter, compares results, applies gates, prints a summary, and optionally writes JSON and failure artifacts:
379
+
380
+ ```bash
381
+ uv run --with glasskit.ai glasskit eval run \
382
+ --adapter eval_adapter.py:create_evaluator \
383
+ --suite eval-suite \
384
+ --case fold-step-001 \
385
+ --adapter-config adapter-config.yaml \
386
+ --keep-going \
387
+ --verbose \
388
+ --output-json tmp/eval-results.json \
389
+ --save-failures \
390
+ --artifacts-dir tmp/eval-artifacts
391
+ ```
392
+
393
+ - `--case` limits the run to one case directory by name.
394
+ - `--adapter-config` reads a YAML or JSON object and passes it to the adapter factory.
395
+ - `--keep-going` records adapter or comparison errors as errored sample results instead of aborting the run on the first error.
396
+ - `--verbose` prints every sample result as it is produced and sets `AdapterConfig.verbose` for the adapter.
397
+ - `--output-json` writes a machine-readable report with summary counts, elapsed run duration, gate results, and per-sample observations. The final console summary also shows the elapsed duration.
398
+ - `--save-failures` saves failed sample frames and per-result JSON files. If `--artifacts-dir` is omitted, artifacts are written under `.glasskit-artifacts` in the suite directory.
399
+ - `--allow-empty` allows suites or cases with no samples. This is mainly useful while drafting a suite, not for real quality gates.
400
+
401
+ ## Writing an Adapter
402
+
403
+ An adapter is the bridge between the generic CLI and your app. It receives decoded video frames plus target metadata, calls your app or model backend, and returns a JSON-like observation for each sample.
404
+
405
+ Pass an adapter as `<module-or-file>:<callable>`. The module side can be a Python import path such as `my_app.eval_adapter` or a file path such as `eval_adapter.py`. The callable side can name a function, class, or nested attribute such as `create_evaluator` or `EvalAdapters.fold_checker`.
406
+
407
+ The recommended shape is a factory that accepts one required `AdapterConfig` argument and returns an evaluator object:
408
+
409
+ ```python
410
+ from __future__ import annotations
411
+
412
+ import os
413
+ from typing import Any
414
+
415
+
416
+ def create_evaluator(config: Any) -> "FoldEvaluator":
417
+ settings = dict(config.config)
418
+ return FoldEvaluator(
419
+ api_key=os.environ["MODEL_API_KEY"],
420
+ model=settings.get("model", "default-model"),
421
+ verbose=bool(config.verbose),
422
+ )
423
+
424
+
425
+ class FoldEvaluator:
426
+ def __init__(self, *, api_key: str, model: str, verbose: bool) -> None:
427
+ self._api_key = api_key
428
+ self._model = model
429
+ self._verbose = verbose
430
+
431
+ async def evaluate(self, sample: Any, target: Any) -> bool:
432
+ image = sample.image
433
+ target_id = target.id
434
+ prompt_id = target.config.get("prompt_id", target_id)
435
+ return await call_model_backend(
436
+ api_key=self._api_key,
437
+ model=self._model,
438
+ image=image,
439
+ prompt_id=prompt_id,
440
+ timestamp_s=sample.timestamp_s,
441
+ )
442
+
443
+ async def close(self) -> None:
444
+ await close_model_client()
445
+ ```
446
+
447
+ No-argument factories and evaluator classes are also supported, but they will not receive `AdapterConfig`. If the factory needs `--adapter-config`, `--artifacts-dir`, `--verbose`, or the suite path, give it one required argument.
448
+
449
+ `evaluate(sample, target)` may be synchronous or asynchronous. If the evaluator also implements `evaluate_many(samples, target)`, the runner calls it once per target and uses the returned list as the observations for that target's samples. `evaluate_many` must return exactly one observation for each input sample in the same order.
450
+
451
+ `close()` is optional and may be synchronous or asynchronous. Use it to close HTTP clients, model sessions, or temporary resources.
452
+
453
+ ### Adapter Inputs
454
+
455
+ The factory receives `AdapterConfig` when it declares one required argument. `AdapterConfig` has these fields:
456
+
457
+ - `suite_path` is the resolved path to the eval suite.
458
+ - `config` is the object loaded from `--adapter-config`; it is an empty mapping when the option is omitted.
459
+ - `artifacts_dir` is the path from `--artifacts-dir`, or `None` when the option is omitted.
460
+ - `verbose` mirrors `--verbose`.
461
+
462
+ The evaluator receives a `sample` object with these fields:
463
+
464
+ - `image` is a decoded RGB `PIL.Image.Image` for the requested timestamp.
465
+ - `timestamp_s` is the requested sample timestamp in seconds from the start of the clip.
466
+ - `frame_index` is the decoded video frame index chosen for that timestamp.
467
+ - `sample_index` is the case-local sample index.
468
+ - `video_path` is the source video path as a string.
469
+ - `case_name` is the case directory name.
470
+
471
+ The evaluator also receives a `target` object with these fields:
472
+
473
+ - `id` is the target id from `expected.yaml`.
474
+ - `index` is the target's zero-based order in the case file.
475
+ - `label` is the optional target label.
476
+ - `config` is the merged target metadata from `workflow.targets` and `targets.<id>.config`.
477
+
478
+ ### Simple Function Adapters
479
+
480
+ For smoke checks or fake local evals, the adapter target can be a function whose first two positional arguments are either `image, target_id` or `sample, target`:
481
+
482
+ ```python
483
+ def evaluate_frame(image, target_id):
484
+ return target_id == "step_1"
485
+ ```
486
+
487
+ ```python
488
+ async def evaluate_sample(sample, target):
489
+ return {
490
+ "target": target.id,
491
+ "bright": sample.image.convert("L").getextrema()[1] > 180,
492
+ }
493
+ ```
494
+
495
+ Function adapters are useful for testing suite wiring because they do not need a model backend. Production adapters should usually use the object shape so they can reuse clients and close resources cleanly.
496
+
497
+ ### Adapter Config Files
498
+
499
+ Use `--adapter-config` for values that should not live in `expected.yaml`, such as backend URLs, model names, thresholds owned by the adapter, or local asset paths:
500
+
501
+ ```yaml
502
+ api_url: "https://example.test/v1"
503
+ model: "vision-checker"
504
+ jpeg_quality: 90
505
+ ```
506
+
507
+ The CLI only parses the file as YAML or JSON and passes the resulting object to `AdapterConfig.config`; it does not expand environment variables inside the file. Read secrets directly from environment variables in the adapter and use `--adapter-config` for non-secret runtime settings.
508
+
509
+ ### Adapter Return Values
510
+
511
+ Return the smallest stable value that answers the target. For a binary detector, return `true` or `false`. For a classifier, return a string label. For richer workflows, return an object and use `field` or `json_subset` in `expected.yaml`.
512
+
513
+ Good observations are deterministic, JSON-like, and easy to inspect in the JSON report:
514
+
515
+ ```python
516
+ return {
517
+ "result": {
518
+ "matches": True,
519
+ "confidence": 0.94,
520
+ "label": "folded",
521
+ }
522
+ }
523
+ ```
524
+
525
+ Avoid returning SDK objects, dataclasses, images, bytes, or other values that cannot be serialized to JSON. They make reports harder to read and may fail when written to `--output-json`.
526
+
527
+ ## Practical Adapter Pattern
528
+
529
+ A model-backed adapter usually follows this flow:
530
+
531
+ ```python
532
+ import os
533
+
534
+
535
+ def create_evaluator(config):
536
+ settings = dict(config.config)
537
+ return MyEvaluator(
538
+ api_key=settings.get("api_key") or os.environ["MODEL_API_KEY"],
539
+ api_url=settings.get("api_url", "https://api.example.test"),
540
+ model=settings.get("model", "vision-model"),
541
+ )
542
+
543
+
544
+ class MyEvaluator:
545
+ def __init__(self, *, api_key, api_url, model):
546
+ self._client = make_async_client(api_key=api_key, base_url=api_url)
547
+ self._model = model
548
+
549
+ async def evaluate_many(self, samples, target):
550
+ return [await self.evaluate(sample, target) for sample in samples]
551
+
552
+ async def evaluate(self, sample, target):
553
+ prompt = target.config.get("prompt", f"Check {target.id}.")
554
+ image_payload = encode_image(sample.image)
555
+ response = await self._client.check(
556
+ model=self._model,
557
+ prompt=prompt,
558
+ image=image_payload,
559
+ )
560
+ return parse_response(response)
561
+
562
+ async def close(self):
563
+ await self._client.aclose()
564
+ ```
565
+
566
+ Keep retries, response parsing, prompt construction, and backend-specific error handling in the adapter. Keep generic eval semantics in `expected.yaml` and the CLI.
567
+
568
+ ## Debugging Failed Runs
569
+
570
+ Start with validation:
571
+
572
+ ```bash
573
+ uv run --with glasskit.ai glasskit eval validate --suite eval-suite --adapter eval_adapter.py:create_evaluator
574
+ ```
575
+
576
+ Then list samples and run one case:
577
+
578
+ ```bash
579
+ uv run --with glasskit.ai glasskit eval list-samples --suite eval-suite --case fold-step-001
580
+ uv run --with glasskit.ai glasskit eval run --suite eval-suite --case fold-step-001 --adapter eval_adapter.py:create_evaluator --verbose
581
+ ```
582
+
583
+ If the adapter is unstable or expensive, add `--keep-going --save-failures --output-json tmp/eval-results.json`. The saved failure images show exactly what frame the adapter saw, and the JSON report includes the raw observation, extracted field, comparison mode, and reason for each sample.
584
+
585
+ Common issues:
586
+
587
+ - `adapter target not found` means the `<module-or-file>:<callable>` path imported successfully but the callable name could not be resolved.
588
+ - `adapter import failed` usually means the working directory, `PYTHONPATH`, or app environment does not include the adapter's dependencies.
589
+ - `video file does not exist` means the `video:` path is wrong or is being resolved from the case directory differently than expected.
590
+ - `sample ... exceeds video duration` means a timestamp is beyond the readable video duration. Check the recording length and the units in `expected.yaml`.
591
+ - `missing field` means the adapter returned a value that does not contain the sample's `field` path.
592
+ - `invalid_observation: adapter returned null` means the adapter returned `None` for a sample whose expected value was not `null`.
593
+
594
+ ## Technical Details
595
+
596
+ For contributor-oriented implementation notes, see [AGENTS.md](AGENTS.md).