karotte 3.0.4__py3-none-any.whl
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.
- karotte/LICENSE +21 -0
- karotte/README.md +270 -0
- karotte/__init__.py +53 -0
- karotte/agents/__init__.py +52 -0
- karotte/agents/agent.py +97 -0
- karotte/agents/backend_source.py +47 -0
- karotte/agents/builtin.py +13 -0
- karotte/agents/builtin_source.py +356 -0
- karotte/agents/cli_agent.py +95 -0
- karotte/agents/external.py +14 -0
- karotte/agents/fake_source.py +27 -0
- karotte/agents/message_loop.py +305 -0
- karotte/agents/message_source.py +22 -0
- karotte/agents/mistral_vibe/__init__.py +3 -0
- karotte/agents/mistral_vibe/adapter.py +180 -0
- karotte/agents/mistral_vibe/agent.py +283 -0
- karotte/agents/models.py +48 -0
- karotte/backend_client.py +258 -0
- karotte/build.py +159 -0
- karotte/cgroups.py +396 -0
- karotte/check_credentials.py +250 -0
- karotte/check_paths.py +32 -0
- karotte/cli/__init__.py +112 -0
- karotte/cli/agents.py +164 -0
- karotte/cli/build.py +50 -0
- karotte/cli/check.py +71 -0
- karotte/cli/create_env.py +83 -0
- karotte/cli/create_run_config.py +82 -0
- karotte/cli/dashboard.py +15 -0
- karotte/cli/models.py +51 -0
- karotte/cli/run.py +497 -0
- karotte/cli/tasks.py +48 -0
- karotte/cli/templates.py +44 -0
- karotte/cli/update.py +44 -0
- karotte/confinement.py +719 -0
- karotte/container.py +69 -0
- karotte/create_env.py +244 -0
- karotte/demoted.py +57 -0
- karotte/evaluation_runner.py +345 -0
- karotte/fake_model.py +20 -0
- karotte/file_quota.py +236 -0
- karotte/gcs.py +70 -0
- karotte/hardware.py +67 -0
- karotte/hide_run_config.py +48 -0
- karotte/init_shim.py +84 -0
- karotte/judges/__init__.py +25 -0
- karotte/judges/_composite_judge.py +194 -0
- karotte/judges/always_pass_judge.py +11 -0
- karotte/judges/executable_judge.py +185 -0
- karotte/judges/judge.py +27 -0
- karotte/judges/regex_judge.py +34 -0
- karotte/judges/rubric_context.py +130 -0
- karotte/judges/rubric_judge.py +162 -0
- karotte/load_tasks.py +45 -0
- karotte/log.py +12 -0
- karotte/mcp_servers/discover_tools.py +125 -0
- karotte/mcp_servers/http_mcp_server.py +137 -0
- karotte/mcp_servers/mcp_server.py +51 -0
- karotte/mcp_servers/resource_profiling_middleware.py +38 -0
- karotte/mcp_servers/resource_sampler.py +240 -0
- karotte/memory_watch.py +528 -0
- karotte/model_catalog.py +92 -0
- karotte/model_spec.py +345 -0
- karotte/process_utils.py +355 -0
- karotte/protected_store.py +196 -0
- karotte/providers.py +411 -0
- karotte/py.typed +0 -0
- karotte/pyproject.toml +124 -0
- karotte/reclaim.py +645 -0
- karotte/run_config_preprocessors.py +16 -0
- karotte/run_helpers.py +829 -0
- karotte/runtime.py +15 -0
- karotte/save_artifact.py +258 -0
- karotte/save_submission.py +250 -0
- karotte/schemas/__init__.py +72 -0
- karotte/schemas/_import_mcp_types.py +73 -0
- karotte/schemas/chat.py +40 -0
- karotte/schemas/data_mount.py +37 -0
- karotte/schemas/environment_template.py +23 -0
- karotte/schemas/evaluation_run_config.py +244 -0
- karotte/schemas/http_mcp_server_config.py +18 -0
- karotte/schemas/run_state.py +83 -0
- karotte/schemas/scoring.py +16 -0
- karotte/schemas/transcript.py +194 -0
- karotte/schemas/websocket_config.py +6 -0
- karotte/step.py +90 -0
- karotte/student_misbehavior.py +18 -0
- karotte/subprocess.py +582 -0
- karotte/task.py +121 -0
- karotte/task_factory.py +105 -0
- karotte/templates/LICENSE +16 -0
- karotte/templates/default/.dockerignore +11 -0
- karotte/templates/default/.gitignore +240 -0
- karotte/templates/default/.karotte/.gitkeep +0 -0
- karotte/templates/default/.python-version +1 -0
- karotte/templates/default/CLAUDE.md +3 -0
- karotte/templates/default/Containerfile +188 -0
- karotte/templates/default/intermediate_data/.gitkeep +0 -0
- karotte/templates/default/justfile +86 -0
- karotte/templates/default/post_create.py +24 -0
- karotte/templates/default/pyproject.toml.jinja +45 -0
- karotte/templates/default/root_data/.gitkeep +0 -0
- karotte/templates/default/scripts/check_supply_chain_config.py +160 -0
- karotte/templates/default/scripts/clear_execstack.py +89 -0
- karotte/templates/default/setup_data.py +18 -0
- karotte/templates/default/shared_data/.gitkeep +0 -0
- karotte/templates/default/src/environment/__init__.py +68 -0
- karotte/templates/default/src/environment/check_permissions.py +340 -0
- karotte/templates/default/src/environment/fake_model.py +39 -0
- karotte/templates/default/src/environment/interpreter_checks.py +55 -0
- karotte/templates/default/src/environment/judges/.gitkeep +0 -0
- karotte/templates/default/src/environment/paths.py +25 -0
- karotte/templates/default/src/environment/submissions.py +34 -0
- karotte/templates/default/src/environment/suite.py +125 -0
- karotte/templates/default/src/environment/system_prompts.py +23 -0
- karotte/templates/default/src/environment/tasks/__init__.py +0 -0
- karotte/templates/default/src/environment/tasks/_template/README.md +9 -0
- karotte/templates/default/src/environment/tasks/_template/__init__.py +60 -0
- karotte/templates/default/src/environment/tasks/_template_suite/README.md +9 -0
- karotte/templates/default/src/environment/tasks/_template_suite/__init__.py +101 -0
- karotte/templates/default/src/environment/tasks/_template_suite/prompt.py +21 -0
- karotte/templates/default/src/environment/tasks/_template_suite/scoring_script.py +44 -0
- karotte/templates/default/src/environment/tasks/_template_suite/task_configs/calibration_configs.json +10 -0
- karotte/templates/default/src/environment/tasks/_template_suite/task_configs/create_calibration_configs.py +32 -0
- karotte/templates/default/src/environment/tasks/_template_suite/task_configs/create_task_configs.py +54 -0
- karotte/templates/default/src/environment/tasks/_template_suite/task_configs/task_config.py +30 -0
- karotte/templates/default/src/environment/tasks/_template_suite/task_configs/task_configs.json +14 -0
- karotte/templates/default/src/environment/tasks/example_task/README.md +24 -0
- karotte/templates/default/src/environment/tasks/example_task/__init__.py +96 -0
- karotte/templates/default/src/environment/tasks/example_task/scoring_script.py +12 -0
- karotte/templates/default/src/environment/tools/.gitkeep +0 -0
- karotte/templates/default/student_data/.gitkeep +0 -0
- karotte/templates/default/template.toml +3 -0
- karotte/templates/default/tests/test_build_venvs.py +287 -0
- karotte/templates/default/tests/test_clear_execstack.py +113 -0
- karotte/templates/default/tests/test_collect_submission.py +81 -0
- karotte/templates/default/tests/test_fake_model.py +29 -0
- karotte/templates/default/tests/test_system_prompts.py +84 -0
- karotte/templates/default/tests/test_task_discovery.py +527 -0
- karotte/templates/default/venvs/README.md +32 -0
- karotte/templates/default/venvs/build_venvs.py +182 -0
- karotte/templates/default/venvs/student/pyproject.toml.jinja +15 -0
- karotte/templates/language-toolchains/Containerfile +135 -0
- karotte/templates/language-toolchains/scripts/check_toolchains.py +1661 -0
- karotte/templates/language-toolchains/src/environment/elf.py +178 -0
- karotte/templates/language-toolchains/src/environment/sandbox.py +499 -0
- karotte/templates/language-toolchains/src/environment/sandbox_probe.c +181 -0
- karotte/templates/language-toolchains/src/environment/shim.c +146 -0
- karotte/templates/language-toolchains/src/environment/toolchain_checks.py +226 -0
- karotte/templates/language-toolchains/src/environment/toolchain_config.py +13 -0
- karotte/templates/language-toolchains/src/environment/toolchain_grading.py +811 -0
- karotte/templates/language-toolchains/src/environment/toolchains.py +2141 -0
- karotte/templates/language-toolchains/src/environment/tools/build.py +223 -0
- karotte/templates/language-toolchains/template.toml +2 -0
- karotte/templates/language-toolchains/tests/test_build_tool.py +256 -0
- karotte/templates/language-toolchains/tests/test_sandbox.py +1373 -0
- karotte/templates/language-toolchains/tests/test_toolchain_grading.py +110 -0
- karotte/templates/language-toolchains/tests/test_toolchains.py +1931 -0
- karotte/templates/language-toolchains/toolchains.just +65 -0
- karotte/templates/pyproject.base.toml.jinja +19 -0
- karotte/templates.py +89 -0
- karotte/terminal/__init__.py +0 -0
- karotte/terminal/app.py +948 -0
- karotte/terminal/console_view.py +65 -0
- karotte/terminal/discover_transcripts.py +18 -0
- karotte/terminal/run_header.py +90 -0
- karotte/terminal/run_view.py +66 -0
- karotte/terminal/sidebar.py +206 -0
- karotte/terminal/styles.tcss +224 -0
- karotte/terminal/transcript_view.py +481 -0
- karotte/terminal/websocket_handler.py +48 -0
- karotte/text.py +24 -0
- karotte/text_files.py +12 -0
- karotte/tool_base.py +45 -0
- karotte/tools/bash.py +876 -0
- karotte/tools/replace_in_file.py +290 -0
- karotte/tools/view_image_file.py +139 -0
- karotte/tools/view_lines_in_file.py +164 -0
- karotte/transcript_markdown.py +261 -0
- karotte/transcript_streaming/stream_transcript_to_backend.py +84 -0
- karotte/transcript_streaming/stream_transcript_to_stdout.py +182 -0
- karotte/transcript_streaming/stream_transcript_to_websocket.py +164 -0
- karotte/truncation.py +50 -0
- karotte/trusted_bin.py +31 -0
- karotte/untrusted_paths.py +64 -0
- karotte/update_env.py +676 -0
- karotte/uv_supply_chain.py +61 -0
- karotte-3.0.4.dist-info/METADATA +310 -0
- karotte-3.0.4.dist-info/RECORD +193 -0
- karotte-3.0.4.dist-info/WHEEL +4 -0
- karotte-3.0.4.dist-info/entry_points.txt +3 -0
- karotte-3.0.4.dist-info/licenses/LICENSE +21 -0
- karotte-3.0.4.dist-info/licenses/src/karotte/templates/LICENSE +16 -0
karotte/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Preference Model
|
|
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.
|
karotte/README.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# karotte
|
|
2
|
+
|
|
3
|
+
karotte runs LLM agents on tasks and scores the results.
|
|
4
|
+
|
|
5
|
+
You write an *environment*: a Python project with one or more tasks. A task is a
|
|
6
|
+
list of steps. Each step gives the agent instructions and a judge that decides
|
|
7
|
+
whether the agent succeeded. karotte builds the environment into a container
|
|
8
|
+
image, lets the model work inside it through tools like `bash`, and records every
|
|
9
|
+
message, tool call and score in a transcript.
|
|
10
|
+
|
|
11
|
+
The agent runs as an unprivileged user with its own resource limits, a firewall,
|
|
12
|
+
and a disk quota, so a task can hand it a real shell without trusting it.
|
|
13
|
+
The model under test is called the *student*.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
karotte needs Python 3.12+, [uv](https://docs.astral.sh/uv/), and docker with
|
|
18
|
+
[buildx](https://github.com/docker/buildx#installing) (or
|
|
19
|
+
podman with `--runtime podman`) for containerized runs. Ubuntu's `docker.io`
|
|
20
|
+
lacks buildx; install `docker-buildx` too. The justfiles need
|
|
21
|
+
[just](https://github.com/casey/just) 1.40 or newer; `uv sync --extra dev`
|
|
22
|
+
installs one into the venv.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
uv tool install karotte
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
Create an environment from the `default` template and run its example task:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
karotte create-env my_env
|
|
34
|
+
cd my_env
|
|
35
|
+
uv sync --extra dev
|
|
36
|
+
uv run setup_data.py
|
|
37
|
+
uv run karotte create-run-config --model claude-fable-5
|
|
38
|
+
export ANTHROPIC_API_KEY=...
|
|
39
|
+
uv run karotte run --config run_config.json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Commands that load your tasks (`create-run-config`, `run`, `tasks list`,
|
|
43
|
+
`check`) have to run in the environment's venv, hence `uv run`.
|
|
44
|
+
`setup_data.py` prepares data the image needs, such as model weights; the
|
|
45
|
+
`default` template's version does nothing. `karotte check` loads every task and
|
|
46
|
+
tool and fails if one doesn't load; the image build runs it too.
|
|
47
|
+
|
|
48
|
+
`karotte run` builds the image, runs the task, and writes the transcript to
|
|
49
|
+
`out/transcript.json`. Files the task saves as artifacts go to
|
|
50
|
+
`out/<run_id>_artifacts/`. `karotte dashboard out/` shows the transcripts in `out/`.
|
|
51
|
+
`karotte models list` shows which models karotte knows; model ids are passed to
|
|
52
|
+
[litellm](https://docs.litellm.ai/). The API key is `model_api_key` in the run
|
|
53
|
+
config. `create-run-config` sets it to the key variable of the model's
|
|
54
|
+
provider, such as `$ANTHROPIC_API_KEY` or `$OPENAI_API_KEY`, which karotte reads
|
|
55
|
+
from your environment when the run starts. `--model-api-key` sets another.
|
|
56
|
+
|
|
57
|
+
Useful while you work on a task:
|
|
58
|
+
|
|
59
|
+
- `uv run karotte run --config run_config.json --dev` skips the image build and
|
|
60
|
+
mounts your `src/environment/` into the container.
|
|
61
|
+
- `uv run karotte tasks list` shows the tasks the environment defines;
|
|
62
|
+
`create-run-config --task <task-id>` picks one.
|
|
63
|
+
|
|
64
|
+
### The container image
|
|
65
|
+
|
|
66
|
+
Every `karotte run` without `--dev` rebuilds the image first. podman and docker
|
|
67
|
+
reuse cached layers, so an unchanged environment builds quickly.
|
|
68
|
+
|
|
69
|
+
`--dev` skips the build and runs the existing image, with your
|
|
70
|
+
`src/environment/` mounted over the installed copy. Rebuild after changing
|
|
71
|
+
dependencies, the `Containerfile` or data.
|
|
72
|
+
|
|
73
|
+
`karotte build` builds the same image without running a task. `run` always uses
|
|
74
|
+
the image tagged `karotte`, which is `build`'s default tag, and has no option to
|
|
75
|
+
pick another. `build --tag` is for images you use elsewhere.
|
|
76
|
+
|
|
77
|
+
### Model endpoints and proxies
|
|
78
|
+
|
|
79
|
+
`karotte run --proxy <url>` sends the model calls to `<url>` instead of the
|
|
80
|
+
provider. Pass the root URL, without `/v1`:
|
|
81
|
+
|
|
82
|
+
- Claude models go to `<url>/v1/messages`. karotte sets `ANTHROPIC_BASE_URL` to
|
|
83
|
+
`<url>` in the container, and litellm adds `/v1/messages`.
|
|
84
|
+
- Other models go to `<url>/v1`, so the endpoint needs an OpenAI-compatible API
|
|
85
|
+
there. `vertex_ai/` models ignore the proxy.
|
|
86
|
+
|
|
87
|
+
The same flag points karotte at any endpoint that serves these paths. The URL is
|
|
88
|
+
used inside the container, where `localhost` is the container itself. Variables
|
|
89
|
+
like `ANTHROPIC_BASE_URL` in your shell don't reach the container. With
|
|
90
|
+
`--no-containerized`, karotte sets `ANTHROPIC_BASE_URL` and `KAROTTE_PROXY_URL`
|
|
91
|
+
in its own process instead, unless one of them is already set.
|
|
92
|
+
`--no-containerized` only runs inside a karotte container image, where
|
|
93
|
+
`KAROTTE_CONTAINERIZED` is set.
|
|
94
|
+
|
|
95
|
+
A key is needed only if the endpoint asks for one. karotte still sends
|
|
96
|
+
`model_api_key`; if that names a variable such as `$OPENAI_API_KEY` that is
|
|
97
|
+
unset, `--proxy` fills in a placeholder.
|
|
98
|
+
|
|
99
|
+
Without `--proxy`, karotte uses the URL a plugin registers under
|
|
100
|
+
`karotte.default_proxy_url` (see [Plugins](#plugins)). Without such a plugin,
|
|
101
|
+
calls go straight to the provider. `--no-proxy` ignores the plugin's URL.
|
|
102
|
+
|
|
103
|
+
`KAROTTE_INFERENCE_SERVICE_TIER=priority` asks Fireworks and Vertex AI Gemini
|
|
104
|
+
for their priority tier, `auto` only once the provider runs out of capacity.
|
|
105
|
+
Unset, the default, uses the provider's default tier.
|
|
106
|
+
|
|
107
|
+
## Writing tasks
|
|
108
|
+
|
|
109
|
+
Each task is a package in `src/environment/tasks/`: a directory whose
|
|
110
|
+
`__init__.py` defines the `Task` class. Plain files and directories starting with
|
|
111
|
+
`_` are skipped. `uv run just create-task <task-id>` copies the task template
|
|
112
|
+
(`just` comes with `uv sync --extra dev` and lives in the venv). A minimal task:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
import re
|
|
116
|
+
|
|
117
|
+
from karotte import Step, Task
|
|
118
|
+
from karotte.judges import RegexJudge
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class FindPython(Task):
|
|
122
|
+
id = "find-python"
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def system_prompt(self) -> str:
|
|
126
|
+
return "You are working in a Linux shell."
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def steps(self):
|
|
130
|
+
return [FindPythonStep(config=self.config)]
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def tools(self):
|
|
134
|
+
return ["bash"]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class FindPythonStep(Step):
|
|
138
|
+
@property
|
|
139
|
+
def instructions(self) -> str:
|
|
140
|
+
return "Find the path to your Python executable. Answer with `path: <path>`."
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def judge(self):
|
|
144
|
+
return RegexJudge([re.compile(r"path: .*/python3?")])
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Judges in `karotte.judges`: `RegexJudge` matches the final message,
|
|
148
|
+
`ExecutableJudge` runs a scoring script, `RubricJudge` asks an LLM to grade
|
|
149
|
+
against a rubric, and `Judge` is the base class for your own. The example task
|
|
150
|
+
in the `default` template shows submissions, hints, and hooks that run before
|
|
151
|
+
scoring.
|
|
152
|
+
|
|
153
|
+
## Templates
|
|
154
|
+
|
|
155
|
+
A template is a directory of files, rendered with
|
|
156
|
+
[Jinja](https://jinja.palletsprojects.com/) into a new environment. karotte ships
|
|
157
|
+
two:
|
|
158
|
+
|
|
159
|
+
- `default`: a CPU environment with an example task. Every other template builds
|
|
160
|
+
on it.
|
|
161
|
+
- `language-toolchains`: gives the agent exactly one language toolchain per task.
|
|
162
|
+
|
|
163
|
+
`karotte templates list` shows every installed template. Templates stack:
|
|
164
|
+
`karotte create-env my_env --template default --template language-toolchains`
|
|
165
|
+
renders `default` and then `language-toolchains` on top. A later template can
|
|
166
|
+
replace a file or override a Jinja block in it:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
{% extends "default/CLAUDE.md" %}
|
|
170
|
+
{% block claude_md -%}
|
|
171
|
+
New content
|
|
172
|
+
{% endblock %}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`karotte update` brings an existing environment up to the latest templates with
|
|
176
|
+
a 3-way merge, so your own edits survive.
|
|
177
|
+
|
|
178
|
+
### Writing a template package
|
|
179
|
+
|
|
180
|
+
Templates can live in their own Python package. Put the template directories
|
|
181
|
+
(each with a `template.toml`) under one directory and register it under the
|
|
182
|
+
`karotte.templates` entry point:
|
|
183
|
+
|
|
184
|
+
```toml
|
|
185
|
+
[project.entry-points."karotte.templates"]
|
|
186
|
+
my_templates = "my_package:TEMPLATES_DIR"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`TEMPLATES_DIR` is a path to that directory. A `template.toml` holds the fields of
|
|
190
|
+
[`EnvironmentTemplate`](https://github.com/preferencemodel/karotte/blob/main/src/karotte/schemas/environment_template.py), for
|
|
191
|
+
example:
|
|
192
|
+
|
|
193
|
+
```toml
|
|
194
|
+
description = "Adds a Rust toolchain."
|
|
195
|
+
requires = ["default"]
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Install the package next to karotte (`uvx --with my-package karotte create-env
|
|
199
|
+
...`). karotte records it in the environment's manifest, so `karotte update`
|
|
200
|
+
pulls it in again.
|
|
201
|
+
|
|
202
|
+
## Plugins
|
|
203
|
+
|
|
204
|
+
Besides templates, a package can extend karotte through these entry points:
|
|
205
|
+
|
|
206
|
+
| Entry point | Points at | Effect |
|
|
207
|
+
| --- | --- | --- |
|
|
208
|
+
| `karotte.cli` | a Typer app | Adds a subcommand named after the entry point. |
|
|
209
|
+
| `karotte.run_config_preprocessors` | `f(config) -> config` | Rewrites the run config before a run, in entry point name order. |
|
|
210
|
+
| `karotte.default_proxy_url` | a string | Default for `karotte run --proxy`. |
|
|
211
|
+
| `karotte.harness_secret_env` | a list of names | Environment variables hidden from the agent. |
|
|
212
|
+
| `karotte.platform_tooling_dirs` | a list of paths | Directories where a platform mounts its own tooling into every container; hidden from graded toolchain runs. |
|
|
213
|
+
| `karotte.age_delay_exemptions` | a list of package names | More packages exempt from uv's `exclude-newer` delay (karotte always is). |
|
|
214
|
+
| `karotte.update_migrations` | an object with `prepare`, `tool` and `migrate` | Moves envs made by an older release to the current names during `karotte update`. |
|
|
215
|
+
| `karotte.default_hardware` | a string | `required_hardware` of tasks that set none. karotte itself knows no hardware names. |
|
|
216
|
+
| `karotte.hardware_limits` | `f(hardware) -> HardwareLimits \| None` | Memory and disk a sandbox on that hardware holds. Without it, the agent's memory limit is the sandbox's cgroup limit or RAM, less 1 GiB for the harness, and none where cgroups don't work. |
|
|
217
|
+
| `karotte.container_run_args` | `f(task, runtime) -> list[str]` | Extra arguments for the container engine's `run` (docker, podman or nerdctl), e.g. to pass devices through, in entry point name order. Raising refuses the launch with the exception's message. |
|
|
218
|
+
|
|
219
|
+
A plugin that fails to load is skipped with a warning, except a run config
|
|
220
|
+
preprocessor: that one fails the run.
|
|
221
|
+
|
|
222
|
+
## Connecting a backend
|
|
223
|
+
|
|
224
|
+
Without a backend, karotte writes the transcript to a file. To collect runs
|
|
225
|
+
centrally, set `backend_uri` in the run config. karotte then calls these HTTP
|
|
226
|
+
endpoints on it, with `run_id` as a query parameter (presign has it in the body):
|
|
227
|
+
|
|
228
|
+
| Request | Purpose |
|
|
229
|
+
| --- | --- |
|
|
230
|
+
| `POST /api/internal/create_transcript` | Start a transcript. |
|
|
231
|
+
| `GET /api/internal/transcript_length` | Number of stored events, 404 if none. |
|
|
232
|
+
| `POST /api/internal/append_transcript` | Body `{"event": ..., "seq": n}`. Writing the same `seq` again must overwrite, not append. |
|
|
233
|
+
| `POST /api/internal/update_run_state` | Body with `status` (`running`, `passed`, `failed`, `error`), `score`, and token counts. |
|
|
234
|
+
| `POST /api/artifacts/presign` | Body `{"run_id", "artifact_paths"}`, returns `{"presigned_urls": {path: url}}`. karotte PUTs each file gzipped. |
|
|
235
|
+
|
|
236
|
+
With the `external` agent the backend also supplies the model's messages:
|
|
237
|
+
`GET /api/internal/get_message` returns the next message (404 while there is
|
|
238
|
+
none) and `POST /api/internal/delete_message` consumes it.
|
|
239
|
+
|
|
240
|
+
Requests carry `Authorization: Bearer <token>` if the file at
|
|
241
|
+
`KAROTTE_BACKEND_TOKEN_PATH` (default `/var/run/secrets/service-account-token`)
|
|
242
|
+
exists. The events are the models in
|
|
243
|
+
[`karotte.schemas.transcript`](https://github.com/preferencemodel/karotte/blob/main/src/karotte/schemas/transcript.py).
|
|
244
|
+
|
|
245
|
+
## Development
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
just lint
|
|
249
|
+
just test
|
|
250
|
+
just test-template default
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Every merge to `main` is released. The patch version is the commit count.
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
karotte is under the [MIT license](https://github.com/preferencemodel/karotte/blob/main/LICENSE). The templates in
|
|
258
|
+
`src/karotte/templates/` are under [MIT No Attribution](https://github.com/preferencemodel/karotte/blob/main/src/karotte/templates/LICENSE)
|
|
259
|
+
(`MIT-0`), so environments created from them need no license notice.
|
|
260
|
+
|
|
261
|
+
### Third-party software in built images
|
|
262
|
+
|
|
263
|
+
Images built from the templates contain third-party software under its own
|
|
264
|
+
licenses. They are based on Amazon Linux 2023. The `language-toolchains`
|
|
265
|
+
template installs GPL and LGPL software (gcc, GnuCOBOL, GNU Prolog, Free Pascal,
|
|
266
|
+
the libraries bundled with Julia, and others), Amazon Corretto (GPL-2.0 with the
|
|
267
|
+
Classpath Exception) and Clojure (EPL-1.0) for the languages you enable. Mojo,
|
|
268
|
+
off by default, is under Modular's proprietary license and has its own terms even
|
|
269
|
+
if you never share the image. The rest only matters if you redistribute a built
|
|
270
|
+
image, for example by pushing it to a public registry.
|
karotte/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""karotte: A library for creating and running AI agent evaluation environments."""
|
|
2
|
+
|
|
3
|
+
from karotte.confinement import Contract as Contract
|
|
4
|
+
from karotte.confinement import FileLimit as FileLimit
|
|
5
|
+
from karotte.confinement import ResourceLimits as ResourceLimits
|
|
6
|
+
from karotte.confinement import get_resource_limits as get_resource_limits
|
|
7
|
+
from karotte.confinement import limit_resources as limit_resources
|
|
8
|
+
from karotte.judges.judge import Judge as Judge
|
|
9
|
+
from karotte.process_utils import UnreapableCohortError as UnreapableCohortError
|
|
10
|
+
from karotte.process_utils import kill_processes as kill_processes
|
|
11
|
+
from karotte.protected_store import ProtectedStore as ProtectedStore
|
|
12
|
+
from karotte.reclaim import ReclaimError as ReclaimError
|
|
13
|
+
from karotte.reclaim import delete_files as delete_files
|
|
14
|
+
from karotte.runtime import Runtime as Runtime
|
|
15
|
+
from karotte.save_artifact import save_artifact as save_artifact
|
|
16
|
+
from karotte.save_submission import save_submission as save_submission
|
|
17
|
+
from karotte.schemas.evaluation_run_config import (
|
|
18
|
+
EvaluationRunConfig as EvaluationRunConfig,
|
|
19
|
+
)
|
|
20
|
+
from karotte.step import Step as Step
|
|
21
|
+
from karotte.student_misbehavior import (
|
|
22
|
+
StudentMisbehaviorError as StudentMisbehaviorError,
|
|
23
|
+
)
|
|
24
|
+
from karotte.task import Task as Task
|
|
25
|
+
from karotte.task_factory import StepConfig as StepConfig
|
|
26
|
+
from karotte.task_factory import create_task as create_task
|
|
27
|
+
from karotte.tool_base import ToolBase as ToolBase
|
|
28
|
+
from karotte.tool_base import ToolConfigWriter as ToolConfigWriter
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"Task",
|
|
32
|
+
"Step",
|
|
33
|
+
"Judge",
|
|
34
|
+
"EvaluationRunConfig",
|
|
35
|
+
"ToolBase",
|
|
36
|
+
"ToolConfigWriter",
|
|
37
|
+
"ProtectedStore",
|
|
38
|
+
"save_artifact",
|
|
39
|
+
"save_submission",
|
|
40
|
+
"create_task",
|
|
41
|
+
"StepConfig",
|
|
42
|
+
"Runtime",
|
|
43
|
+
"kill_processes",
|
|
44
|
+
"UnreapableCohortError",
|
|
45
|
+
"delete_files",
|
|
46
|
+
"ReclaimError",
|
|
47
|
+
"StudentMisbehaviorError",
|
|
48
|
+
"limit_resources",
|
|
49
|
+
"get_resource_limits",
|
|
50
|
+
"ResourceLimits",
|
|
51
|
+
"Contract",
|
|
52
|
+
"FileLimit",
|
|
53
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Pluggable agents for the evaluation runner.
|
|
2
|
+
|
|
3
|
+
An `Agent` produces a run's transcript one step at a time; the runner keeps the
|
|
4
|
+
task lifecycle (hooks, judging, artifacts, event bookkeeping). Builtin and
|
|
5
|
+
external agents share `MessageLoopAgent`, differing only in their
|
|
6
|
+
`MessageSource` -- builtin drives the litellm API loop itself, external fetches
|
|
7
|
+
messages from the backend (training rollouts). Fake replays env-authored
|
|
8
|
+
messages.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from karotte.agents.agent import (
|
|
12
|
+
Agent,
|
|
13
|
+
EmptyTurnLimitReachedError,
|
|
14
|
+
RunContext,
|
|
15
|
+
TurnLimitReachedError,
|
|
16
|
+
)
|
|
17
|
+
from karotte.agents.backend_source import EXTERNAL_MESSAGE_TIMEOUT_S, BackendSource
|
|
18
|
+
from karotte.agents.builtin import BuiltinAgent
|
|
19
|
+
from karotte.agents.builtin_source import BuiltinSource
|
|
20
|
+
from karotte.agents.cli_agent import (
|
|
21
|
+
CliAgent,
|
|
22
|
+
cli_agent_types,
|
|
23
|
+
get_cli_agent_type,
|
|
24
|
+
register_cli_agent,
|
|
25
|
+
)
|
|
26
|
+
from karotte.agents.external import ExternalAgent
|
|
27
|
+
from karotte.agents.fake_source import FakeSource
|
|
28
|
+
from karotte.agents.message_loop import MessageLoopAgent
|
|
29
|
+
from karotte.agents.message_source import MessageSource
|
|
30
|
+
|
|
31
|
+
# Import agent modules for their `register_cli_agent` side effects.
|
|
32
|
+
from karotte.agents.mistral_vibe import MistralVibeAgent
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"EXTERNAL_MESSAGE_TIMEOUT_S",
|
|
36
|
+
"Agent",
|
|
37
|
+
"BackendSource",
|
|
38
|
+
"BuiltinAgent",
|
|
39
|
+
"BuiltinSource",
|
|
40
|
+
"CliAgent",
|
|
41
|
+
"EmptyTurnLimitReachedError",
|
|
42
|
+
"ExternalAgent",
|
|
43
|
+
"FakeSource",
|
|
44
|
+
"MessageLoopAgent",
|
|
45
|
+
"MessageSource",
|
|
46
|
+
"MistralVibeAgent",
|
|
47
|
+
"RunContext",
|
|
48
|
+
"TurnLimitReachedError",
|
|
49
|
+
"cli_agent_types",
|
|
50
|
+
"get_cli_agent_type",
|
|
51
|
+
"register_cli_agent",
|
|
52
|
+
]
|
karotte/agents/agent.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import ClassVar, Literal, Protocol, final
|
|
4
|
+
|
|
5
|
+
from fastmcp import Client
|
|
6
|
+
from fastmcp.client import StreamableHttpTransport
|
|
7
|
+
from litellm import ChatCompletionToolParam
|
|
8
|
+
|
|
9
|
+
from karotte.schemas.evaluation_run_config import EvaluationRunConfig
|
|
10
|
+
from karotte.schemas.transcript import Event, Transcript
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TurnLimitReachedError(Exception):
|
|
14
|
+
"""Raised when the evaluation run exceeds the configured turn limit."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StepTimeLimitReachedError(Exception):
|
|
18
|
+
"""Raised when a step exceeds its configured wall-clock time limit."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class StepContextWindowLimitReachedError(Exception):
|
|
22
|
+
"""Raised when a step exceeds its configured context-window token limit."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EmptyTurnLimitReachedError(Exception):
|
|
26
|
+
"""Raised when the model produced neither text nor tool calls too many turns
|
|
27
|
+
in a row, so nudging it to continue has stopped working."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@final
|
|
31
|
+
@dataclass
|
|
32
|
+
class RunContext:
|
|
33
|
+
"""What an agent needs to solve steps within a run.
|
|
34
|
+
|
|
35
|
+
The runner owns the transcript and applies event bookkeeping; the agent
|
|
36
|
+
reads `transcript.messages` for conversation history and executes tool calls
|
|
37
|
+
against `mcp_client`. One `RunContext` per run, passed to `Agent.start`.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
config: EvaluationRunConfig
|
|
41
|
+
mcp_client: Client[StreamableHttpTransport]
|
|
42
|
+
tools: list[ChatCompletionToolParam]
|
|
43
|
+
transcript: Transcript
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Agent(Protocol):
|
|
47
|
+
"""Produces a run's transcript, one step at a time.
|
|
48
|
+
|
|
49
|
+
The runner keeps the task lifecycle (hooks, judging, artifacts, event
|
|
50
|
+
bookkeeping); the agent decides how messages and tool calls get produced.
|
|
51
|
+
An agent instance is stateful across steps within a run.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
allows_student_mcp_access: ClassVar[bool]
|
|
55
|
+
"""Whether the student user may reach the MCP port.
|
|
56
|
+
|
|
57
|
+
True only for agents that run in-container as the student and make native
|
|
58
|
+
MCP tool calls (CLI agents). Builtin/external agents keep the port blocked:
|
|
59
|
+
karotte executes their tool calls itself, so student access would only let
|
|
60
|
+
task code bypass transcript recording.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
native_tool_names: ClassVar[frozenset[str]]
|
|
64
|
+
"""Tools this agent supplies itself, and which the runner therefore
|
|
65
|
+
omits when registering the task's tools. A CLI agent with its own bash/file
|
|
66
|
+
tools lists them here so it uses its native ones instead of duplicates over
|
|
67
|
+
MCP. Empty for agents that rely entirely on MCP tools.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
async def start(self, ctx: RunContext) -> None:
|
|
71
|
+
"""Verify/configure the agent for the run."""
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
def run_step(
|
|
75
|
+
self,
|
|
76
|
+
instructions: str,
|
|
77
|
+
time_limit_seconds: float | None = None,
|
|
78
|
+
on_time_limit: Literal["error", "score"] = "error",
|
|
79
|
+
context_window_limit: int | None = None,
|
|
80
|
+
on_context_window_limit: Literal["error", "score"] = "error",
|
|
81
|
+
) -> AsyncGenerator[Event]:
|
|
82
|
+
"""Solve one step, yielding raw transcript events.
|
|
83
|
+
|
|
84
|
+
`time_limit_seconds` bounds the step's wall-clock duration; when it
|
|
85
|
+
elapses the step either aborts (`on_time_limit="error"`) or ends so it
|
|
86
|
+
can be scored (`on_time_limit="score"`). `None` disables the limit.
|
|
87
|
+
|
|
88
|
+
`context_window_limit` bounds the step's context-window length (the input
|
|
89
|
+
tokens of the most recent turn); when it is exceeded the step either
|
|
90
|
+
aborts (`on_context_window_limit="error"`) or ends so it can be scored
|
|
91
|
+
(`on_context_window_limit="score"`). `None` disables the limit.
|
|
92
|
+
"""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
async def stop(self) -> None:
|
|
96
|
+
"""Release any resources held for the run."""
|
|
97
|
+
...
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from collections.abc import AsyncGenerator
|
|
3
|
+
from typing import final
|
|
4
|
+
|
|
5
|
+
from litellm import ChatCompletionToolParam
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from karotte.backend_client import BackendClient
|
|
9
|
+
from karotte.schemas.chat import Message
|
|
10
|
+
from karotte.schemas.transcript import Event
|
|
11
|
+
|
|
12
|
+
# Maximum time to wait for a message from the model before giving up.
|
|
13
|
+
EXTERNAL_MESSAGE_TIMEOUT_S = 120 * 60
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@final
|
|
17
|
+
class BackendSource:
|
|
18
|
+
"""Long-polls the backend for the next message produced by an external model
|
|
19
|
+
(training rollouts). karotte still drives the step loop and executes the tool
|
|
20
|
+
calls the message contains."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, backend_client: BackendClient, run_id: str) -> None:
|
|
23
|
+
self.backend_client = backend_client
|
|
24
|
+
self.run_id = run_id
|
|
25
|
+
|
|
26
|
+
async def collect(
|
|
27
|
+
self,
|
|
28
|
+
messages: list[Message], # pyright: ignore[reportUnusedParameter]
|
|
29
|
+
tools: list[ChatCompletionToolParam], # pyright: ignore[reportUnusedParameter]
|
|
30
|
+
) -> AsyncGenerator[Event]:
|
|
31
|
+
start = time.time()
|
|
32
|
+
last_log = time.time()
|
|
33
|
+
while True:
|
|
34
|
+
elapsed = time.time() - start
|
|
35
|
+
if elapsed > EXTERNAL_MESSAGE_TIMEOUT_S:
|
|
36
|
+
raise TimeoutError(
|
|
37
|
+
f"Timed out after {elapsed:.0f}s waiting for a response from the model"
|
|
38
|
+
)
|
|
39
|
+
if time.time() - last_log > 30:
|
|
40
|
+
logger.warning(f"Still waiting for new message, {elapsed:.0f}s elapsed")
|
|
41
|
+
last_log = time.time()
|
|
42
|
+
# get_message is a long-poll operation so we don't need extra sleep
|
|
43
|
+
msg_added_evt = await self.backend_client.get_message(self.run_id)
|
|
44
|
+
if msg_added_evt is not None:
|
|
45
|
+
yield msg_added_evt
|
|
46
|
+
await self.backend_client.delete_message(self.run_id)
|
|
47
|
+
return
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import final
|
|
2
|
+
|
|
3
|
+
from karotte.agents.builtin_source import BuiltinSource
|
|
4
|
+
from karotte.agents.message_loop import MessageLoopAgent
|
|
5
|
+
from karotte.schemas.evaluation_run_config import EvaluationRunConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@final
|
|
9
|
+
class BuiltinAgent(MessageLoopAgent):
|
|
10
|
+
"""karotte drives the model itself via the litellm API loop."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, config: EvaluationRunConfig) -> None:
|
|
13
|
+
super().__init__(BuiltinSource(config))
|