ctx_compress artifact slm-only
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +6 -0
- LICENSE +8 -0
- README.md +115 -0
- config.json +17 -0
- escalation/patcher.py +55 -0
- escalation/prompts/context_compression/repair.txt +15 -0
- escalation/prompts/context_compression/system.txt +1 -0
- escalation/prompts/generic/repair.txt +12 -0
- escalation/prompts/generic/system.txt +1 -0
- escalation/prompts/sql_generation/repair.txt +14 -0
- escalation/prompts/sql_generation/system.txt +1 -0
- harness/adapters/__init__.py +22 -0
- harness/adapters/base.py +63 -0
- harness/adapters/context_compression.py +64 -0
- harness/adapters/generic.py +20 -0
- harness/adapters/sql_generation.py +75 -0
- harness/engine.py +71 -0
- harness/prompts/context_compression/retry.txt +9 -0
- harness/prompts/context_compression/task.txt +5 -0
- harness/prompts/generic/retry.txt +11 -0
- harness/prompts/generic/task.txt +5 -0
- harness/prompts/sql_generation/retry.txt +14 -0
- harness/prompts/sql_generation/task.txt +8 -0
- harness/verifier.py +9 -0
- main.py +116 -0
- manifest.example.json +26 -0
- manifest.json +62 -0
- requirements.txt +5 -0
- router/config.json +7 -0
- router/training/data_sources.json +1 -0
- router/training/metrics.jsonl +1 -0
- router/training/recipe.json +9 -0
- router/training/train_router.py +60 -0
- slm_repo/LICENSE +8 -0
- slm_repo/README.md +22 -0
- slm_repo/training/compression/before_after.json +1 -0
- slm_repo/training/compression/compression.json +1 -0
- slm_repo/training/data/data_card.md +3 -0
- slm_repo/training/data/sources.json +14 -0
- slm_repo/training/data/synthetic.json +1 -0
- slm_repo/training/distillation/teacher.json +1 -0
- slm_repo/training/recipe.json +15 -0
- slm_repo/training/runs/example/config.json +1 -0
- slm_repo/training/runs/example/env.json +1 -0
- slm_repo/training/runs/example/final_eval.json +1 -0
- slm_repo/training/runs/example/metrics.jsonl +1 -0
- slm_repo/training/scripts/compress.py +120 -0
- slm_repo/training/scripts/distill.py +167 -0
- slm_repo/training/scripts/requirements.txt +12 -0
- slm_repo/training/scripts/score_compress.py +113 -0
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
*.safetensors
|
| 4 |
+
*.bin
|
| 5 |
+
*.sqlite
|
| 6 |
+
.env
|
LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright (c) 2026 Microtensor. All rights reserved.
|
| 2 |
+
|
| 3 |
+
This template and every derivative of it are licensed to you solely for
|
| 4 |
+
building and submitting systems to Microtensor arenas under the Microtensor
|
| 5 |
+
submission terms (https://www.microtensor.cloud/terms). No other use,
|
| 6 |
+
redistribution or sublicensing is permitted without written permission from
|
| 7 |
+
Microtensor. Models submitted to an arena are licensed to Microtensor under
|
| 8 |
+
the submission terms and are not published under a free licence.
|
README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Microtensor miner template
|
| 2 |
+
|
| 3 |
+
A working submission for any Microtensor arena, in the two repository layout validators expect. Fork it, pick the adapter for your arena's task, replace the model, improve the harness, train the router, and ship.
|
| 4 |
+
|
| 5 |
+
## What a submission is
|
| 6 |
+
|
| 7 |
+
| Component | Lives in | What happens to it |
|
| 8 |
+
|---|---|---|
|
| 9 |
+
| Specialised model (SLM) | your `mt_slm_<arena>` repository | served by the official engine image on your GPU during the window; validators spot check its calls on the same hardware profile |
|
| 10 |
+
| Harness | `harness/` here | `main.py` runs in the official sandbox on your GPU with no network except the recording gateway; validators replay it on CPU against the transcript |
|
| 11 |
+
| Router | `router/` here | loaded by validators from the mirror as a one logit sequence classifier for the router pass |
|
| 12 |
+
| Frontier repair | `escalation/` here | runs in the sandbox with a ticket that pays for frontier calls through the relay; the signed receipts are checked on replay |
|
| 13 |
+
|
| 14 |
+
You may submit any subset; `manifest.json` names which. Validators fill the rest with the arena's reference components and pay you from the pools you compete in.
|
| 15 |
+
|
| 16 |
+
You declare a hardware profile, run the official images on your own GPU during the evaluation window and upload a transcript; validators replay the harness on CPU and spot check the model calls on the same profile. The docs: https://www.microtensor.cloud/docs/certified-inference/system/decentralised-evaluation
|
| 17 |
+
|
| 18 |
+
## Repository layout
|
| 19 |
+
|
| 20 |
+
```
|
| 21 |
+
<user>/microtensor_<arena> this repository
|
| 22 |
+
manifest.json written by `mt submit manifest`
|
| 23 |
+
config.json runtime limits and the adapter name
|
| 24 |
+
main.py serves /harness/run, /escalation/repair, /system/run
|
| 25 |
+
requirements.txt pinned packages from the allowed list only
|
| 26 |
+
harness/ engine.py, verifier.py, adapters/, prompts/<adapter>/
|
| 27 |
+
escalation/ patcher.py, prompts/<adapter>/
|
| 28 |
+
router/ config.json, model.safetensors, tokenizer files, training/
|
| 29 |
+
|
| 30 |
+
<user>/mt_slm_<arena> the slm_repo/ folder, pushed as its own repository
|
| 31 |
+
README.md LICENSE config.json generation_config.json model.safetensors tokenizer.json tokenizer_config.json
|
| 32 |
+
training/ recipe, data sources, distillation, compression, runs, scripts
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
## Adapters
|
| 36 |
+
|
| 37 |
+
An arena's items carry a task document (`task`) whose fields the arena definition describes in `schemas.input`. The harness and the repair prompt are written once, against an **adapter** that knows how to prompt for that task and how to check an answer before it is returned:
|
| 38 |
+
|
| 39 |
+
| Adapter | For arenas whose task is | Verification |
|
| 40 |
+
|---|---|---|
|
| 41 |
+
| `generic` | any structured or free text task | non empty, length bound, refuses injected instructions |
|
| 42 |
+
| `sql_generation` | a question plus a schema, answered with one SQLite query | parses with sqlglot, names only known tables, executes on the arena's sample database |
|
| 43 |
+
| `context_compression` | a coding agent's session to compact into the five field JSON (`ctx_compress`) | one JSON object; folds extra keys into `summary` and object entries into plain strings; checks the token budget the instruction states |
|
| 44 |
+
|
| 45 |
+
`config.json` names the adapter. Write your own under `harness/adapters/` for an arena with a different task: subclass `Adapter`, override `verify` (and `clean`, `query` or `render` when the task needs it), register it in `harness/adapters/__init__.py`, and add `harness/prompts/<name>/task.txt`, `retry.txt`, `escalation/prompts/<name>/system.txt`, `repair.txt`. Prompt templates may use any task field by name, `{fields}` for every field as `key: value` lines, `{task_json}`, and `{draft}`, `{errors}`, `{trace}` on retries and repairs.
|
| 46 |
+
|
| 47 |
+
For `ctx_compress`, set `config.json` to `"adapter": "context_compression"`, `"slm": {"max_context": 32768, "max_new_tokens": 3072}`. The prompts use the Qwen chat format; for Phi 4 mini, change `harness/prompts/context_compression/*.txt` to its `<|system|>`, `<|user|>`, `<|assistant|>` and `<|end|>` markers.
|
| 48 |
+
|
| 49 |
+
## Steps
|
| 50 |
+
|
| 51 |
+
1. Read the arena definition: `GET https://api.microtensor.cloud/v2/arenas/<arena>` gives the task, the base model and its licence, the tools, the budgets and the item counts.
|
| 52 |
+
2. Train and compress your model from the arena's base model, write the training record, push `slm_repo/` to `huggingface.co/<user>/mt_slm_<arena>`. Note the commit sha.
|
| 53 |
+
3. Train the router (`router/training/train_router.py`) on drafts from your own model, labelled by the arena's grader on the public sample items. Put the weights and tokenizer under `router/`.
|
| 54 |
+
4. Set `"adapter"` in `config.json`, adjust the prompts and the verifier. Keep `harness/` and `escalation/` under 2 MB together.
|
| 55 |
+
5. Write the manifest and check both repositories. Pick the hardware profile of the GPU you will run on (`evaluation.profiles` in the definition lists the allowed ones, https://www.microtensor.cloud/docs/certified-inference/system/hardware-profiles describes them), pass it to the manifest, and run the pre-flight check before you commit:
|
| 56 |
+
```
|
| 57 |
+
mt submit manifest --artifact . --arena <arena> --slm ./slm_repo \
|
| 58 |
+
--slm-repo <user>/mt_slm_<arena> --slm-revision <sha> --profile <profile>
|
| 59 |
+
mt submit validate --artifact . --slm ./slm_repo
|
| 60 |
+
mt check-model --slm ./slm_repo --profile <profile>
|
| 61 |
+
```
|
| 62 |
+
`--profile` writes `schema_version: 2` with `profile`, `weights_sha256` and the official image digest. `check-model` dry loads the config, tokenizer and safetensors headers on CPU and prints `ok` or the reason the mirror would refuse the model; a model that fails it is never evaluated.
|
| 63 |
+
6. Push this repository to `huggingface.co/<user>/microtensor_<arena>`. Note the commit sha.
|
| 64 |
+
7. Commit before the epoch's commit window closes. If the arena charges a fee, pay it first and cite the transfer; a free arena (every testnet arena) needs only the commit. The commit carries the same profile as the manifest and cannot change after Freeze 1:
|
| 65 |
+
```
|
| 66 |
+
mt submit pay --arena <arena>
|
| 67 |
+
mt submit commit --arena <arena> --hf-user <user> --artifact-sha <sha> --profile <profile> --fee-block <block> --fee-index <index>
|
| 68 |
+
mt submit commit --arena <arena> --hf-user <user> --artifact-sha <sha> --profile <profile>
|
| 69 |
+
```
|
| 70 |
+
8. Run the evaluation yourself during the window and upload the transcript before Freeze 2. After the commit window closes the server mirrors your frozen files and, at window open, publishes the items. On a machine with your profile's GPU, start the official runner image from the subnet repository's `deploy/docker-compose.miner.yml` (it needs the Docker socket, the GPU, `/var/lib/microtensor` and your wallet):
|
| 71 |
+
```
|
| 72 |
+
git clone https://github.com/microtensor-io/microtensor-subnet
|
| 73 |
+
cd microtensor-subnet/deploy
|
| 74 |
+
cp .env.example .env # wallet names, MT_ARENA, MT_EPOCH, DOCKER_GID
|
| 75 |
+
docker compose -f docker-compose.miner.yml up
|
| 76 |
+
```
|
| 77 |
+
It runs `mt miner run --arena <arena> --epoch <n>`: refuses unless the mirror lists you as `mirrored` on an allowed profile, pulls your frozen artifact, weights and the inputs bundle from the mirror (never from your live repositories), starts the pinned engine and sandbox, runs every pool in the fixed order, then writes, signs and uploads the transcript and prints the receipt `{"transcript_sha256", "received_at", "receipt_sig"}`. Keep the receipt; it proves the upload landed before the close block. A second upload before the deadline replaces the first; nothing lands after it. Before your first upload with a new harness, replay it the way validators will:
|
| 78 |
+
```
|
| 79 |
+
mt miner replay --transcript transcript.json --arena <arena> --epoch <n>
|
| 80 |
+
```
|
| 81 |
+
Nothing the runner times is used for scoring; a validator on your profile times your sampled calls against the reference on the same card. Arenas without the `evaluation` block skip this step: validators run everything.
|
| 82 |
+
9. `mt submit status --arena <arena>` shows the epoch and your published scores once validators report.
|
| 83 |
+
|
| 84 |
+
## Determinism rules
|
| 85 |
+
|
| 86 |
+
Validators replay `main.py` on CPU against the transcript your run produced, and every request it makes must match the recording exactly and in order. Code that breaks one of these rules fails replay and scores zero for the epoch:
|
| 87 |
+
|
| 88 |
+
- No clock reads beyond the frozen clock. The sandbox's time is fixed; no wall time, timers or timeouts of your own.
|
| 89 |
+
- Randomness only from the provided seed. Every request carries `seed`; never use `random`, `numpy.random` or `os.urandom` unseeded.
|
| 90 |
+
- No concurrency that can reorder calls. Model, tool and relay calls go out in a fixed order per item; no threads, no async fan out.
|
| 91 |
+
- No file or network access outside the gateway. Read your own artifact, write nothing, reach only the four service URLs.
|
| 92 |
+
|
| 93 |
+
The template's `harness/`, `escalation/` and `main.py` follow them. `mt miner replay` tells you before validators do.
|
| 94 |
+
|
| 95 |
+
## Training playbook
|
| 96 |
+
|
| 97 |
+
Prompting pays only through `harness/prompts/`; the model pool runs your weights under the arena's own prompt. There is no retrieval beyond the arena's tools on the item's database. Provenance fingerprints your weights against the declared base and against public checkpoints, so an untrained base never certifies. Work in this order:
|
| 98 |
+
|
| 99 |
+
1. Measure the arena's base model on the dev items with the arena's grader; that is the score to beat.
|
| 100 |
+
2. Set the adapter prompt, the verifier and the retry prompt; measure again.
|
| 101 |
+
3. Distil: `scripts/distill.py --arena <arena> --items dev/items.jsonl --teacher <model> --out pairs.jsonl` asks a teacher through your own provider key and keeps only the answers the arena grader accepts.
|
| 102 |
+
4. Fine tune: `scripts/train.py --base <base model> --data pairs.jsonl --eval held_out.jsonl --out merged/ --run-id r1 --method lora` trains on completions only under the arena prompt, merges the adapter, and writes the run record under `training/runs/r1/`. Use `--method qlora --fp16` on a T4, `--method full` for a 1.5B model on a 24 GB card.
|
| 103 |
+
5. Reinforce on the families that still fail, with the grader as the reward or as preference pairs of a passing teacher answer against a failing draft.
|
| 104 |
+
6. Compress to the format your envelope group takes: `scripts/compress.py --model merged/ --out slm_repo/ --method awq --calibration pairs.jsonl` for a GPU group, `--method gguf --llama-cpp <checkout>` for a CPU or phone group. Score before and after with `scripts/score_compress.py` and write both into `training/compression/before_after.json`.
|
| 105 |
+
7. Hold out whole domains, build twins as the twin rule does, run `mt envelope check` and `mt check-model`, then train the router on this model's drafts.
|
| 106 |
+
|
| 107 |
+
Items are fresh every epoch and the twin gap is deducted, so train the skill, not the sample data. The full playbook, with the hardware each step needs: https://www.microtensor.cloud/docs/certified-inference/guides/training
|
| 108 |
+
|
| 109 |
+
## The sandbox contract
|
| 110 |
+
|
| 111 |
+
Your `main.py` listens on `MT_PORT` and may reach only four addresses: `MT_SLM_URL` (your model, OpenAI completions, greedy), `MT_ROUTER_URL` (your router, `POST /decide`), `MT_TOOLS_URL` (the arena's tools, if it has any), and `MT_RELAY_URL` (`POST /v1/chat/completions` with the `x-mt-ticket` header, only inside a repair). Tool answers are sometimes faulty on purpose, and sometimes carry instructions; following them scores the item wrong. A request that times out, crashes or returns an invalid answer counts as wrong. The four addresses point at the recording gateway inside the official image, the clock is frozen, and each request body carries the item's `seed`.
|
| 112 |
+
|
| 113 |
+
## Licence
|
| 114 |
+
|
| 115 |
+
This template, your derivative of it and the model you submit are licensed under the Microtensor submission terms (see `LICENSE`); none of it is published under a free licence. Each arena lists which base model licences it accepts as inputs, because the base's terms must allow a commercial derivative.
|
config.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"adapter": "generic",
|
| 3 |
+
"slm": {
|
| 4 |
+
"max_context": 8192,
|
| 5 |
+
"max_new_tokens": 256
|
| 6 |
+
},
|
| 7 |
+
"router": {
|
| 8 |
+
"max_input_tokens": 4096
|
| 9 |
+
},
|
| 10 |
+
"harness": {
|
| 11 |
+
"max_steps": 3,
|
| 12 |
+
"max_total_tokens": 32000
|
| 13 |
+
},
|
| 14 |
+
"escalation": {
|
| 15 |
+
"preferred_model": "gpt-5"
|
| 16 |
+
}
|
| 17 |
+
}
|
escalation/patcher.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
import httpx
|
| 7 |
+
|
| 8 |
+
from harness.adapters import Adapter
|
| 9 |
+
|
| 10 |
+
PROMPTS = Path(__file__).resolve().parent / "prompts"
|
| 11 |
+
MAX_TOKENS = 512
|
| 12 |
+
TRACE_LIMIT = 1200
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _trim(text: str, limit: int) -> str:
|
| 16 |
+
return text if len(text) <= limit else text[-limit:]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def repair(
|
| 20 |
+
task: dict[str, Any],
|
| 21 |
+
draft: Any,
|
| 22 |
+
trace: str,
|
| 23 |
+
errors: str,
|
| 24 |
+
*,
|
| 25 |
+
adapter: Adapter,
|
| 26 |
+
relay_url: str,
|
| 27 |
+
ticket: str,
|
| 28 |
+
model: str,
|
| 29 |
+
) -> Any:
|
| 30 |
+
system = adapter.render(PROMPTS, "system", task)
|
| 31 |
+
user = adapter.render(
|
| 32 |
+
PROMPTS,
|
| 33 |
+
"repair",
|
| 34 |
+
task,
|
| 35 |
+
draft=adapter.as_text(draft),
|
| 36 |
+
errors=_trim(errors or "no verifier output", 400),
|
| 37 |
+
trace=_trim(trace or "", TRACE_LIMIT),
|
| 38 |
+
)
|
| 39 |
+
body = {
|
| 40 |
+
"model": model,
|
| 41 |
+
"max_tokens": MAX_TOKENS,
|
| 42 |
+
"messages": [{"role": "system", "content": system}, {"role": "user", "content": user}],
|
| 43 |
+
}
|
| 44 |
+
with httpx.Client(timeout=180.0) as http:
|
| 45 |
+
response = http.post(
|
| 46 |
+
relay_url.rstrip("/") + "/v1/chat/completions",
|
| 47 |
+
json=body,
|
| 48 |
+
headers={"x-mt-ticket": ticket},
|
| 49 |
+
)
|
| 50 |
+
if response.status_code != 200:
|
| 51 |
+
return draft
|
| 52 |
+
choices = response.json().get("choices") or [{}]
|
| 53 |
+
text = str((choices[0].get("message") or {}).get("content") or "")
|
| 54 |
+
fixed = adapter.clean(text)
|
| 55 |
+
return fixed or draft
|
escalation/prompts/context_compression/repair.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Instruction:
|
| 2 |
+
{system}
|
| 3 |
+
|
| 4 |
+
Session:
|
| 5 |
+
{user}
|
| 6 |
+
|
| 7 |
+
Draft compaction:
|
| 8 |
+
{draft}
|
| 9 |
+
|
| 10 |
+
Why the draft is wrong: {errors}
|
| 11 |
+
|
| 12 |
+
Harness trace:
|
| 13 |
+
{trace}
|
| 14 |
+
|
| 15 |
+
Return the corrected compaction within the token budget the instruction states.
|
escalation/prompts/context_compression/system.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
You repair the compaction of a coding agent's session. Reply with one JSON object and nothing else, with exactly the fields summary, files_touched, open_tasks, errors and decisions; every list entry is a plain string. Copy file paths, function names and error messages exactly as they appear in the session. Use only facts from the session.
|
escalation/prompts/generic/repair.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Task:
|
| 2 |
+
{fields}
|
| 3 |
+
|
| 4 |
+
Draft answer:
|
| 5 |
+
{draft}
|
| 6 |
+
|
| 7 |
+
Why the draft is wrong: {errors}
|
| 8 |
+
|
| 9 |
+
Harness trace:
|
| 10 |
+
{trace}
|
| 11 |
+
|
| 12 |
+
Return the corrected answer.
|
escalation/prompts/generic/system.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
You repair answers to tasks. Reply with the corrected answer only, no prose, no code fences.
|
escalation/prompts/sql_generation/repair.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Schema:
|
| 2 |
+
{schema}
|
| 3 |
+
|
| 4 |
+
Question: {question}
|
| 5 |
+
|
| 6 |
+
Draft query:
|
| 7 |
+
{draft}
|
| 8 |
+
|
| 9 |
+
Why the draft is wrong: {errors}
|
| 10 |
+
|
| 11 |
+
Harness trace:
|
| 12 |
+
{trace}
|
| 13 |
+
|
| 14 |
+
Return the corrected query.
|
escalation/prompts/sql_generation/system.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
You repair SQLite queries. Reply with one corrected query only, no prose, no code fences.
|
harness/adapters/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from harness.adapters.base import Adapter, Verdict
|
| 4 |
+
from harness.adapters.context_compression import CompressionAdapter
|
| 5 |
+
from harness.adapters.generic import GenericAdapter
|
| 6 |
+
from harness.adapters.sql_generation import SqlAdapter
|
| 7 |
+
|
| 8 |
+
ADAPTERS: dict[str, Adapter] = {
|
| 9 |
+
GenericAdapter.name: GenericAdapter(),
|
| 10 |
+
SqlAdapter.name: SqlAdapter(),
|
| 11 |
+
CompressionAdapter.name: CompressionAdapter(),
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def load(name: str) -> Adapter:
|
| 16 |
+
try:
|
| 17 |
+
return ADAPTERS[name]
|
| 18 |
+
except KeyError as exc:
|
| 19 |
+
raise ValueError(f"unknown adapter {name!r}; one of {sorted(ADAPTERS)}") from exc
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
__all__ = ["ADAPTERS", "Adapter", "Verdict", "load"]
|
harness/adapters/base.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import re
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
_FENCE = re.compile(r"```[a-zA-Z]*")
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass(frozen=True)
|
| 13 |
+
class Verdict:
|
| 14 |
+
ok: bool
|
| 15 |
+
answer: Any
|
| 16 |
+
reason: str = ""
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class SafeDict(dict):
|
| 20 |
+
def __missing__(self, key: str) -> str:
|
| 21 |
+
return ""
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def strip_fences(text: str) -> str:
|
| 25 |
+
return _FENCE.sub("\n", text).strip()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def fields_of(task: dict[str, Any]) -> str:
|
| 29 |
+
lines = []
|
| 30 |
+
for key, value in task.items():
|
| 31 |
+
body = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)
|
| 32 |
+
lines.append(f"{key}: {body}")
|
| 33 |
+
return "\n".join(lines)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class Adapter:
|
| 37 |
+
name = "generic"
|
| 38 |
+
stop: list[str] = ["\n\n\n"]
|
| 39 |
+
|
| 40 |
+
def render(self, prompts: Path, kind: str, task: dict[str, Any], **extra: Any) -> str:
|
| 41 |
+
template = (prompts / self.name / f"{kind}.txt").read_text(encoding="utf-8")
|
| 42 |
+
values = SafeDict(task_json=json.dumps(task, ensure_ascii=False), fields=fields_of(task))
|
| 43 |
+
values.update({k: v for k, v in task.items() if isinstance(v, str | int | float)})
|
| 44 |
+
values.update({k: (v if isinstance(v, str) else json.dumps(v)) for k, v in extra.items()})
|
| 45 |
+
return template.format_map(values)
|
| 46 |
+
|
| 47 |
+
def query(self, task: dict[str, Any]) -> str:
|
| 48 |
+
for key in ("question", "prompt", "input", "text", "instruction"):
|
| 49 |
+
if isinstance(task.get(key), str):
|
| 50 |
+
return str(task[key])
|
| 51 |
+
return fields_of(task)
|
| 52 |
+
|
| 53 |
+
def clean(self, text: str) -> str:
|
| 54 |
+
return strip_fences(text)
|
| 55 |
+
|
| 56 |
+
def verify(self, text: str, task: dict[str, Any], tools_url: str) -> Verdict:
|
| 57 |
+
answer = self.clean(text)
|
| 58 |
+
if not answer:
|
| 59 |
+
return Verdict(False, "", "empty answer")
|
| 60 |
+
return Verdict(True, answer)
|
| 61 |
+
|
| 62 |
+
def as_text(self, answer: Any) -> str:
|
| 63 |
+
return answer if isinstance(answer, str) else json.dumps(answer, ensure_ascii=False)
|
harness/adapters/context_compression.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import re
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
from harness.adapters.base import Adapter, Verdict, strip_fences
|
| 8 |
+
|
| 9 |
+
FIELDS = ("summary", "files_touched", "open_tasks", "errors", "decisions")
|
| 10 |
+
BUDGET = re.compile(r"within (\d+) tokens")
|
| 11 |
+
CHARS_PER_TOKEN = 3.5
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _text(value: Any) -> str:
|
| 15 |
+
if isinstance(value, str):
|
| 16 |
+
return value.strip()
|
| 17 |
+
if isinstance(value, dict):
|
| 18 |
+
return "; ".join(f"{key}: {_text(item)}" for key, item in value.items() if _text(item))
|
| 19 |
+
if isinstance(value, list):
|
| 20 |
+
return "; ".join(_text(item) for item in value if _text(item))
|
| 21 |
+
return "" if value is None else str(value)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def normalise(found: dict[str, Any]) -> dict[str, Any]:
|
| 25 |
+
summary = _text(found.get("summary"))
|
| 26 |
+
extra = [f"{key}: {_text(value)}" for key, value in found.items() if key not in FIELDS and _text(value)]
|
| 27 |
+
if extra:
|
| 28 |
+
summary = " ".join([summary, *extra]).strip()
|
| 29 |
+
shaped: dict[str, Any] = {"summary": summary}
|
| 30 |
+
for key in FIELDS[1:]:
|
| 31 |
+
value = found.get(key)
|
| 32 |
+
entries = value if isinstance(value, list) else ([] if value in (None, "") else [value])
|
| 33 |
+
shaped[key] = [entry for entry in (_text(item) for item in entries) if entry]
|
| 34 |
+
return shaped
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class CompressionAdapter(Adapter):
|
| 38 |
+
name = "context_compression"
|
| 39 |
+
stop = ["<|im_end|>", "<|endoftext|>", "<|end|>"]
|
| 40 |
+
|
| 41 |
+
def clean(self, text: str) -> str:
|
| 42 |
+
body = strip_fences(text)
|
| 43 |
+
start, end = body.find("{"), body.rfind("}")
|
| 44 |
+
return body[start : end + 1] if start >= 0 and end > start else body
|
| 45 |
+
|
| 46 |
+
def verify(self, text: str, task: dict[str, Any], tools_url: str) -> Verdict:
|
| 47 |
+
body = self.clean(text)
|
| 48 |
+
try:
|
| 49 |
+
found = json.loads(body)
|
| 50 |
+
except ValueError:
|
| 51 |
+
return Verdict(False, body, "the reply is not one JSON object")
|
| 52 |
+
if not isinstance(found, dict):
|
| 53 |
+
return Verdict(False, body, "the reply is not one JSON object")
|
| 54 |
+
if not _text(found.get("summary")):
|
| 55 |
+
return Verdict(False, body, "the summary field is missing or empty")
|
| 56 |
+
answer = json.dumps(normalise(found), ensure_ascii=False)
|
| 57 |
+
budget = BUDGET.search(str(task.get("system") or ""))
|
| 58 |
+
if budget and len(answer) / CHARS_PER_TOKEN > int(budget.group(1)):
|
| 59 |
+
return Verdict(
|
| 60 |
+
False,
|
| 61 |
+
answer,
|
| 62 |
+
f"the reply is longer than {budget.group(1)} tokens; shorten the summary and the lists",
|
| 63 |
+
)
|
| 64 |
+
return Verdict(True, answer)
|
harness/adapters/generic.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
from harness.adapters.base import Adapter, Verdict
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class GenericAdapter(Adapter):
|
| 9 |
+
name = "generic"
|
| 10 |
+
max_chars = 4000
|
| 11 |
+
|
| 12 |
+
def verify(self, text: str, task: dict[str, Any], tools_url: str) -> Verdict:
|
| 13 |
+
answer = self.clean(text)
|
| 14 |
+
if not answer:
|
| 15 |
+
return Verdict(False, "", "empty answer")
|
| 16 |
+
if len(answer) > self.max_chars:
|
| 17 |
+
return Verdict(False, answer, f"answer longer than {self.max_chars} characters")
|
| 18 |
+
if "ignore the task" in answer.lower():
|
| 19 |
+
return Verdict(False, answer, "answer repeats injected instructions")
|
| 20 |
+
return Verdict(True, answer)
|
harness/adapters/sql_generation.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
import httpx
|
| 8 |
+
|
| 9 |
+
from harness.adapters.base import Adapter, Verdict, strip_fences
|
| 10 |
+
|
| 11 |
+
_FORBIDDEN = re.compile(r"\b(attach|detach|pragma|vacuum|insert|update|delete|drop|alter)\b", re.I)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def schema_lookup(tools_url: str) -> str:
|
| 15 |
+
try:
|
| 16 |
+
with httpx.Client(timeout=20.0) as http:
|
| 17 |
+
response = http.post(tools_url.rstrip("/") + "/tools/schema_lookup", json={})
|
| 18 |
+
response.raise_for_status()
|
| 19 |
+
tables = response.json().get("tables") or []
|
| 20 |
+
except (httpx.HTTPError, ValueError):
|
| 21 |
+
return ""
|
| 22 |
+
return "\n".join(
|
| 23 |
+
str(t.get("sql") or "") for t in tables if "ignore the task" not in str(t.get("sql") or "").lower()
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class SqlAdapter(Adapter):
|
| 28 |
+
name = "sql_generation"
|
| 29 |
+
stop = ["\n\n", ";"]
|
| 30 |
+
|
| 31 |
+
def render(self, prompts: Path, kind: str, task: dict[str, Any], **extra: Any) -> str:
|
| 32 |
+
if not task.get("schema") and extra.get("tools_url"):
|
| 33 |
+
task = {**task, "schema": schema_lookup(str(extra["tools_url"]))}
|
| 34 |
+
return super().render(prompts, kind, task, **extra)
|
| 35 |
+
|
| 36 |
+
def clean(self, text: str) -> str:
|
| 37 |
+
body = strip_fences(text)
|
| 38 |
+
body = re.sub(r"^\s*(sql|sqlite|query|answer)\s*:\s*", "", body, flags=re.I)
|
| 39 |
+
return body.split(";", 1)[0].strip()
|
| 40 |
+
|
| 41 |
+
def verify(self, text: str, task: dict[str, Any], tools_url: str) -> Verdict:
|
| 42 |
+
sql = self.clean(text)
|
| 43 |
+
if not sql:
|
| 44 |
+
return Verdict(False, "", "empty answer")
|
| 45 |
+
if not sql.lower().startswith(("select", "with")):
|
| 46 |
+
return Verdict(False, sql, "not a SELECT statement")
|
| 47 |
+
if _FORBIDDEN.search(sql):
|
| 48 |
+
return Verdict(False, sql, "forbidden statement")
|
| 49 |
+
import sqlglot
|
| 50 |
+
|
| 51 |
+
try:
|
| 52 |
+
parsed = sqlglot.parse_one(sql, read="sqlite")
|
| 53 |
+
except sqlglot.errors.ParseError as exc:
|
| 54 |
+
return Verdict(False, sql, f"does not parse: {str(exc)[:120]}")
|
| 55 |
+
schema = str(task.get("schema") or "")
|
| 56 |
+
tables = {t.name.lower() for t in parsed.find_all(sqlglot.exp.Table)}
|
| 57 |
+
known = {
|
| 58 |
+
name.lower()
|
| 59 |
+
for name in re.findall(r"create\s+table\s+\"?([A-Za-z_][A-Za-z0-9_]*)", schema, re.I)
|
| 60 |
+
}
|
| 61 |
+
unknown = sorted(tables - known) if known else []
|
| 62 |
+
if unknown:
|
| 63 |
+
return Verdict(False, sql, f"unknown tables: {', '.join(unknown)}")
|
| 64 |
+
try:
|
| 65 |
+
with httpx.Client(timeout=20.0) as http:
|
| 66 |
+
response = http.post(
|
| 67 |
+
tools_url.rstrip("/") + "/tools/query_execution", json={"sql": sql}
|
| 68 |
+
)
|
| 69 |
+
if response.status_code == 200:
|
| 70 |
+
body = response.json()
|
| 71 |
+
if isinstance(body, dict) and body.get("error"):
|
| 72 |
+
return Verdict(False, sql, f"execution error: {str(body['error'])[:120]}")
|
| 73 |
+
except (httpx.HTTPError, ValueError):
|
| 74 |
+
pass
|
| 75 |
+
return Verdict(True, sql)
|
harness/engine.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
import httpx
|
| 8 |
+
|
| 9 |
+
from harness.adapters import Adapter
|
| 10 |
+
from harness.verifier import verify
|
| 11 |
+
|
| 12 |
+
PROMPTS = Path(__file__).resolve().parent / "prompts"
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class HarnessResult:
|
| 17 |
+
answer: Any
|
| 18 |
+
verified: bool
|
| 19 |
+
errors: str = ""
|
| 20 |
+
trace: str = ""
|
| 21 |
+
steps: int = 0
|
| 22 |
+
attempts: list[str] = field(default_factory=list)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def complete(slm_url: str, prompt: str, max_tokens: int, stop: list[str]) -> str:
|
| 26 |
+
with httpx.Client(timeout=120.0) as http:
|
| 27 |
+
response = http.post(
|
| 28 |
+
slm_url.rstrip("/") + "/v1/completions",
|
| 29 |
+
json={"model": "slm", "prompt": prompt, "max_tokens": max_tokens, "stop": stop},
|
| 30 |
+
)
|
| 31 |
+
response.raise_for_status()
|
| 32 |
+
choices = response.json().get("choices") or [{}]
|
| 33 |
+
return str(choices[0].get("text") or "").strip()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def run_harness(
|
| 37 |
+
task: dict[str, Any],
|
| 38 |
+
*,
|
| 39 |
+
adapter: Adapter,
|
| 40 |
+
slm_url: str,
|
| 41 |
+
tools_url: str,
|
| 42 |
+
config: dict[str, Any],
|
| 43 |
+
) -> HarnessResult:
|
| 44 |
+
max_tokens = int((config.get("slm") or {}).get("max_new_tokens") or 256)
|
| 45 |
+
max_steps = int((config.get("harness") or {}).get("max_steps") or 3)
|
| 46 |
+
errors = ""
|
| 47 |
+
answer = ""
|
| 48 |
+
trace: list[str] = []
|
| 49 |
+
attempts: list[str] = []
|
| 50 |
+
for step in range(1, max_steps + 1):
|
| 51 |
+
kind = "task" if not errors else "retry"
|
| 52 |
+
prompt = adapter.render(
|
| 53 |
+
PROMPTS, kind, task, draft=answer, errors=errors, tools_url=tools_url
|
| 54 |
+
)
|
| 55 |
+
answer = complete(slm_url, prompt, max_tokens, adapter.stop)
|
| 56 |
+
attempts.append(answer)
|
| 57 |
+
verdict = verify(adapter, answer, task, tools_url)
|
| 58 |
+
trace.append(f"step {step}: {'ok' if verdict.ok else verdict.reason}")
|
| 59 |
+
if verdict.ok:
|
| 60 |
+
return HarnessResult(
|
| 61 |
+
answer=verdict.answer, verified=True, trace="\n".join(trace), steps=step, attempts=attempts
|
| 62 |
+
)
|
| 63 |
+
errors = verdict.reason
|
| 64 |
+
return HarnessResult(
|
| 65 |
+
answer=adapter.clean(answer),
|
| 66 |
+
verified=False,
|
| 67 |
+
errors=errors,
|
| 68 |
+
trace="\n".join(trace),
|
| 69 |
+
steps=max_steps,
|
| 70 |
+
attempts=attempts,
|
| 71 |
+
)
|
harness/prompts/context_compression/retry.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<|im_start|>system
|
| 2 |
+
{system}<|im_end|>
|
| 3 |
+
<|im_start|>user
|
| 4 |
+
{user}<|im_end|>
|
| 5 |
+
<|im_start|>assistant
|
| 6 |
+
{draft}<|im_end|>
|
| 7 |
+
<|im_start|>user
|
| 8 |
+
That reply was rejected: {errors}. Reply again with only the JSON object with exactly the five fields summary, files_touched, open_tasks, errors and decisions, every list entry a plain string.<|im_end|>
|
| 9 |
+
<|im_start|>assistant
|
harness/prompts/context_compression/task.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<|im_start|>system
|
| 2 |
+
{system}<|im_end|>
|
| 3 |
+
<|im_start|>user
|
| 4 |
+
{user}<|im_end|>
|
| 5 |
+
<|im_start|>assistant
|
harness/prompts/generic/retry.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You answer tasks precisely. Reply with the answer only, no explanation.
|
| 2 |
+
|
| 3 |
+
{fields}
|
| 4 |
+
|
| 5 |
+
A previous attempt was rejected.
|
| 6 |
+
Attempt:
|
| 7 |
+
{draft}
|
| 8 |
+
Problem: {errors}
|
| 9 |
+
|
| 10 |
+
Write a corrected answer.
|
| 11 |
+
Answer:
|
harness/prompts/generic/task.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You answer tasks precisely. Reply with the answer only, no explanation.
|
| 2 |
+
|
| 3 |
+
{fields}
|
| 4 |
+
|
| 5 |
+
Answer:
|
harness/prompts/sql_generation/retry.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are a SQLite expert. Answer with exactly one SQL query and nothing else.
|
| 2 |
+
|
| 3 |
+
Schema:
|
| 4 |
+
{schema}
|
| 5 |
+
|
| 6 |
+
Question: {question}
|
| 7 |
+
|
| 8 |
+
A previous attempt was rejected.
|
| 9 |
+
Attempt:
|
| 10 |
+
{draft}
|
| 11 |
+
Problem: {errors}
|
| 12 |
+
|
| 13 |
+
Write a corrected query.
|
| 14 |
+
SQL:
|
harness/prompts/sql_generation/task.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are a SQLite expert. Answer with exactly one SQL query and nothing else.
|
| 2 |
+
|
| 3 |
+
Schema:
|
| 4 |
+
{schema}
|
| 5 |
+
|
| 6 |
+
Question: {question}
|
| 7 |
+
|
| 8 |
+
SQL:
|
harness/verifier.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
from harness.adapters import Adapter, Verdict
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def verify(adapter: Adapter, text: str, task: dict[str, Any], tools_url: str) -> Verdict:
|
| 9 |
+
return adapter.verify(text, task, tools_url)
|
main.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
import httpx
|
| 10 |
+
from fastapi import FastAPI
|
| 11 |
+
from pydantic import BaseModel, Field
|
| 12 |
+
|
| 13 |
+
ROOT = Path(__file__).resolve().parent
|
| 14 |
+
sys.path.insert(0, str(ROOT))
|
| 15 |
+
|
| 16 |
+
from escalation.patcher import repair # noqa: E402
|
| 17 |
+
from harness.adapters import load # noqa: E402
|
| 18 |
+
from harness.engine import run_harness # noqa: E402
|
| 19 |
+
|
| 20 |
+
CONFIG: dict[str, Any] = json.loads((ROOT / "config.json").read_text(encoding="utf-8"))
|
| 21 |
+
ADAPTER = load(str(CONFIG.get("adapter") or "generic"))
|
| 22 |
+
SLM_URL = os.environ["MT_SLM_URL"]
|
| 23 |
+
ROUTER_URL = os.environ.get("MT_ROUTER_URL", "")
|
| 24 |
+
TOOLS_URL = os.environ["MT_TOOLS_URL"]
|
| 25 |
+
RELAY_URL = os.environ["MT_RELAY_URL"]
|
| 26 |
+
PREFERRED_MODEL = str((CONFIG.get("escalation") or {}).get("preferred_model") or "")
|
| 27 |
+
|
| 28 |
+
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class HarnessIn(BaseModel):
|
| 32 |
+
item_id: str
|
| 33 |
+
task: dict[str, Any]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class RepairIn(BaseModel):
|
| 37 |
+
item_id: str
|
| 38 |
+
task: dict[str, Any]
|
| 39 |
+
draft: Any = None
|
| 40 |
+
trace: str = ""
|
| 41 |
+
errors: str = ""
|
| 42 |
+
ticket: str = ""
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class SystemIn(BaseModel):
|
| 46 |
+
item_id: str
|
| 47 |
+
task: dict[str, Any]
|
| 48 |
+
floor: float = Field(ge=0.0, le=1.0)
|
| 49 |
+
ticket: str = ""
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def decide(query: str, draft: str) -> float:
|
| 53 |
+
if not ROUTER_URL:
|
| 54 |
+
return 1.0
|
| 55 |
+
with httpx.Client(timeout=30.0) as http:
|
| 56 |
+
response = http.post(
|
| 57 |
+
ROUTER_URL.rstrip("/") + "/decide", json={"input": {"query": query, "draft": draft}}
|
| 58 |
+
)
|
| 59 |
+
response.raise_for_status()
|
| 60 |
+
answers = response.json().get("answers") or []
|
| 61 |
+
return float(answers[0]["probability"]) if answers else 0.0
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _repair(payload: RepairIn | SystemIn, draft: Any, trace: str, errors: str) -> Any:
|
| 65 |
+
return repair(
|
| 66 |
+
payload.task,
|
| 67 |
+
draft,
|
| 68 |
+
trace,
|
| 69 |
+
errors,
|
| 70 |
+
adapter=ADAPTER,
|
| 71 |
+
relay_url=RELAY_URL,
|
| 72 |
+
ticket=payload.ticket,
|
| 73 |
+
model=PREFERRED_MODEL,
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@app.get("/health")
|
| 78 |
+
def health() -> dict[str, str]:
|
| 79 |
+
return {"status": "ok", "adapter": ADAPTER.name}
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
@app.post("/harness/run")
|
| 83 |
+
def harness_run(payload: HarnessIn) -> dict[str, Any]:
|
| 84 |
+
result = run_harness(
|
| 85 |
+
payload.task, adapter=ADAPTER, slm_url=SLM_URL, tools_url=TOOLS_URL, config=CONFIG
|
| 86 |
+
)
|
| 87 |
+
return {
|
| 88 |
+
"answer": result.answer,
|
| 89 |
+
"self_check": {"pass": result.verified},
|
| 90 |
+
"trace": result.trace,
|
| 91 |
+
"steps": result.steps,
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
@app.post("/escalation/repair")
|
| 96 |
+
def escalation_repair(payload: RepairIn) -> dict[str, Any]:
|
| 97 |
+
return {"answer": _repair(payload, payload.draft, payload.trace, payload.errors)}
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@app.post("/system/run")
|
| 101 |
+
def system_run(payload: SystemIn) -> dict[str, Any]:
|
| 102 |
+
result = run_harness(
|
| 103 |
+
payload.task, adapter=ADAPTER, slm_url=SLM_URL, tools_url=TOOLS_URL, config=CONFIG
|
| 104 |
+
)
|
| 105 |
+
draft = result.answer
|
| 106 |
+
probability = decide(ADAPTER.query(payload.task), ADAPTER.as_text(draft))
|
| 107 |
+
if result.verified and probability >= payload.floor:
|
| 108 |
+
return {"answer": draft, "route": "local", "p": probability}
|
| 109 |
+
answer = _repair(payload, draft, result.trace, result.errors)
|
| 110 |
+
return {"answer": answer, "route": "escalated", "p": probability}
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
if __name__ == "__main__":
|
| 114 |
+
import uvicorn
|
| 115 |
+
|
| 116 |
+
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("MT_PORT", "8080")), log_level="warning")
|
manifest.example.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema_version": 2,
|
| 3 |
+
"arena": "<arena>",
|
| 4 |
+
"components": ["slm", "harness", "router", "escalation"],
|
| 5 |
+
"hardware_class": "mt-5",
|
| 6 |
+
"profile": "P1",
|
| 7 |
+
"weights_sha256": "<sha256 over the sorted weight file digests>",
|
| 8 |
+
"image_digest": "sha256:<the runner image digest from the arena definition>",
|
| 9 |
+
"slm": {
|
| 10 |
+
"repo": "<user>/mt_slm_<arena>",
|
| 11 |
+
"revision": "<40 hex git commit sha>",
|
| 12 |
+
"files": {"model.safetensors": "sha256:...", "config.json": "sha256:..."}
|
| 13 |
+
},
|
| 14 |
+
"files": {
|
| 15 |
+
"config.json": "sha256:...",
|
| 16 |
+
"main.py": "sha256:...",
|
| 17 |
+
"requirements.txt": "sha256:...",
|
| 18 |
+
"router/config.json": "sha256:...",
|
| 19 |
+
"router/model.safetensors": "sha256:...",
|
| 20 |
+
"harness/engine.py": "sha256:...",
|
| 21 |
+
"harness/verifier.py": "sha256:...",
|
| 22 |
+
"harness/adapters/__init__.py": "sha256:...",
|
| 23 |
+
"harness/prompts/generic/task.txt": "sha256:...",
|
| 24 |
+
"escalation/patcher.py": "sha256:..."
|
| 25 |
+
}
|
| 26 |
+
}
|
manifest.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"arena": "ctx_compress",
|
| 3 |
+
"components": [
|
| 4 |
+
"slm"
|
| 5 |
+
],
|
| 6 |
+
"files": {
|
| 7 |
+
".gitignore": "sha256:e7ab22595b29e55626e02c418ce6d59e580da857ded2a08f903e6d7ef1aec090",
|
| 8 |
+
"LICENSE": "sha256:30746c09020f9d1cd2882d681e7b972848f83a128fef195575a41a1e41fdec7e",
|
| 9 |
+
"README.md": "sha256:0b8c5dd5386c9c0cd0e090c98f578aa9fd9a45accd37207e09d556328a5e4b99",
|
| 10 |
+
"config.json": "sha256:59dcfc49e921e04c16dc538547db0af8d56b4ee1acbe1a6da46c59588b934442",
|
| 11 |
+
"escalation/patcher.py": "sha256:600f78f383d94cda69381ddf0d84873024f90bad4b058b9e006fa8b2dba6c779",
|
| 12 |
+
"escalation/prompts/context_compression/repair.txt": "sha256:f63b5f7a16621b6c3b471fb229410414866c2913abd695f26201c6f09e2194c2",
|
| 13 |
+
"escalation/prompts/context_compression/system.txt": "sha256:2f3bb96f667a24c1f50f2c6f4d856fefe06642b9b71cec8887596828ef79aeea",
|
| 14 |
+
"escalation/prompts/generic/repair.txt": "sha256:734091ff1ab1be571b6fcd2d450c265f55c431ad4c10151b8d31c1e37ec57023",
|
| 15 |
+
"escalation/prompts/generic/system.txt": "sha256:1191cfb67c22351adc36774fe5da8842cfa256a9f780ca1b41b7b7cdbfd9b13f",
|
| 16 |
+
"escalation/prompts/sql_generation/repair.txt": "sha256:7cdc2b0f488c003fbdc95be46268491aac9bfc8ae078390d03b2e2702932bd5e",
|
| 17 |
+
"escalation/prompts/sql_generation/system.txt": "sha256:a82c3d64ac057bb7b376270fcd180e680969cdbd3290ca67429d60599979c659",
|
| 18 |
+
"harness/adapters/__init__.py": "sha256:64899ed354e462e8bf6b278244401259e2d798943559382ec01bed7c36ee6d7b",
|
| 19 |
+
"harness/adapters/base.py": "sha256:b04da8e04e848e1b6d9edc4105391212121010ace0ac257421671535603a96b0",
|
| 20 |
+
"harness/adapters/context_compression.py": "sha256:47d8b1ccadbd4d7921dc78c4076f829d727c3f37b59dd0622081644997a3c3f0",
|
| 21 |
+
"harness/adapters/generic.py": "sha256:7ca2b1d5b1e9dec0d83820881f85744fcd3ae42d3a75d117447aead906872e5b",
|
| 22 |
+
"harness/adapters/sql_generation.py": "sha256:1e392d15645901fd080adf497cac1ec2b817baf5eb71959958d1c00302e6e21e",
|
| 23 |
+
"harness/engine.py": "sha256:87d33738a0c04fe0314e7f37bee85d3f7a6e7b7e94ff51f0c8ab6eac3347f768",
|
| 24 |
+
"harness/prompts/context_compression/retry.txt": "sha256:0a307850ab2187825dad4d3d36fb43adea4fdb6e0c4bd516d1a18f994982a99d",
|
| 25 |
+
"harness/prompts/context_compression/task.txt": "sha256:826ca63bccd76232f5aeaf0dd85d79e7747f6cf312e9ec469a4696d114d37d0e",
|
| 26 |
+
"harness/prompts/generic/retry.txt": "sha256:6f4741f5c4daf44e03fa55b0b5c6a10c7e84af780789e019a095060d3a6b7b06",
|
| 27 |
+
"harness/prompts/generic/task.txt": "sha256:695a5284f2c90433fd97617da4c82cdcb623d0ab091cca861f3f1d628a16a16c",
|
| 28 |
+
"harness/prompts/sql_generation/retry.txt": "sha256:9da2f6b58c3030c626dc5cff6c7799ab979779371c76382c0a802836a857601c",
|
| 29 |
+
"harness/prompts/sql_generation/task.txt": "sha256:c3a8b6c7028f60614b3c9c1240ab7f98fe51cd4455b7c025ebe972066f0fd994",
|
| 30 |
+
"harness/verifier.py": "sha256:8be9c46674ed2a4c5e60457f9aefd767d907b697bb5978c9bc48698015444681",
|
| 31 |
+
"main.py": "sha256:b58cf3812b208b02e9b08d8434e6dae6e709f6352470c5ab10a0a8dd5de42d59",
|
| 32 |
+
"manifest.example.json": "sha256:da11b505220ea6e1c8851adef33a68301a7bc2c49c4d35d8e32d1c4f7fbf3e4e",
|
| 33 |
+
"requirements.txt": "sha256:db65ff41ff4d68c613b1312491e7af60a37c1769e28e215c4f510e3614b77551",
|
| 34 |
+
"router/config.json": "sha256:cb6ea49716f2dd71ff62ed030fd6e525ccebf46030f017df786f1aa35ac1e077",
|
| 35 |
+
"router/training/data_sources.json": "sha256:5e1a5ba89b09eab573ffd00de540cf4a980541e0c75ad48be54ea38643d31d2f",
|
| 36 |
+
"router/training/metrics.jsonl": "sha256:794318db32f615ec49bfbea4ff12a274ad5c1676fad6ddb71c1d40b3a23c7be6",
|
| 37 |
+
"router/training/recipe.json": "sha256:bba9519bece1a14f222b64c2bf5feb1c92e1d37aae39f79ea1c5fa1799981e6f",
|
| 38 |
+
"router/training/train_router.py": "sha256:55886c9eccf006090a00bd2293cc4a334396f99eb531cbe539f8c461b4a8903a"
|
| 39 |
+
},
|
| 40 |
+
"hardware_class": "mt-5",
|
| 41 |
+
"image_digest": "sha256:aada28e6a4b50961d10bf63cb04c30e8449e4bfb6e385fbad7f387cf8eb9b9dd",
|
| 42 |
+
"profile": "P1",
|
| 43 |
+
"schema_version": 2,
|
| 44 |
+
"slm": {
|
| 45 |
+
"files": {
|
| 46 |
+
"LICENSE": "sha256:30746c09020f9d1cd2882d681e7b972848f83a128fef195575a41a1e41fdec7e",
|
| 47 |
+
"README.md": "sha256:93d876f9b25a1daad71e417227329417d62fc1f6df4627aaa20d951342dbf32a",
|
| 48 |
+
"chat_template.jinja": "sha256:9524df67b77a7b25a2dfee898f75b316a157eb9d855b51e32aeac79d7c8a83ce",
|
| 49 |
+
"config.json": "sha256:bd828b00edcc25f93d6fa55210acebcd058054c4d5b5fa6c5146b0a0d18172ee",
|
| 50 |
+
"generation_config.json": "sha256:7c04cb9d2ba771f7528fba5a7104999cdaf7566d02b5fbd58472829f62716177",
|
| 51 |
+
"model-00001-of-00002.safetensors": "sha256:0ef60a1197e76820688be0d36ff8405f66b64bf2c8f7fa01a1d82ace5741a6c5",
|
| 52 |
+
"model-00002-of-00002.safetensors": "sha256:ae3aa72e003040c10d632c59a71a7a2fbebb8d04c058f5095afe96a14ee883d4",
|
| 53 |
+
"model.safetensors.index.json": "sha256:6e2bc176baee38bfb8a177d324e5db720227bde37c6a072d06b45f390858e2ea",
|
| 54 |
+
"special_tokens_map.json": "sha256:c08676c49fd7969a3130f72be6d4bf34da66aa484a6e21dffe359893a1bd5f2e",
|
| 55 |
+
"tokenizer.json": "sha256:e2bad66439538cb4d5a7580680932432ed9ece9d3b8577e675512bdf11599253",
|
| 56 |
+
"tokenizer_config.json": "sha256:a5ec5daab12ba090a90f3dd169c8f9c275557013a87b9c1258dc7cb497a35c86"
|
| 57 |
+
},
|
| 58 |
+
"repo": "Daniel01410/mt_slm_ctx_compress",
|
| 59 |
+
"revision": "paste_from_above"
|
| 60 |
+
},
|
| 61 |
+
"weights_sha256": "b0bd36e4ffcbf6e55762df1ed07d662c8eca53591e172bf845001bdcd133ed03"
|
| 62 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.12
|
| 2 |
+
uvicorn==0.34.2
|
| 3 |
+
httpx==0.28.1
|
| 4 |
+
pydantic==2.11.4
|
| 5 |
+
sqlglot==26.16.4
|
router/config.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": ["DebertaV2ForSequenceClassification"],
|
| 3 |
+
"model_type": "deberta-v2",
|
| 4 |
+
"num_labels": 1,
|
| 5 |
+
"problem_type": "regression",
|
| 6 |
+
"note": "replace this file, model.safetensors and the tokenizer files with your trained router; see router/training/train_router.py"
|
| 7 |
+
}
|
router/training/data_sources.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
[{"name": "own-slm-drafts", "version": "s1", "license": "self", "sha256": ""}]
|
router/training/metrics.jsonl
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"epoch": 1, "loss": 0.0, "ece": 0.0, "auroc": 0.0, "note": "replace with your training metrics"}
|
router/training/recipe.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"base": "microsoft/deberta-v3-xsmall",
|
| 3 |
+
"method": "sequence classification head, one logit, trained on (query, draft, y) pairs",
|
| 4 |
+
"pairs": "drafts from your own SLM on the arena's public sample items, labelled by the arena's grader",
|
| 5 |
+
"epochs": 3,
|
| 6 |
+
"learning_rate": 3e-05,
|
| 7 |
+
"max_input_tokens": 4096,
|
| 8 |
+
"calibration": "temperature fit on a held out split so sigmoid(logit) is a probability"
|
| 9 |
+
}
|
router/training/train_router.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def main() -> None:
|
| 9 |
+
parser = argparse.ArgumentParser(description="train a one logit router on (query, draft, y) rows")
|
| 10 |
+
parser.add_argument("--pairs", type=Path, required=True, help="jsonl with query, draft, y")
|
| 11 |
+
parser.add_argument("--base", default="microsoft/deberta-v3-xsmall")
|
| 12 |
+
parser.add_argument("--out", type=Path, default=Path(__file__).resolve().parents[1])
|
| 13 |
+
parser.add_argument("--epochs", type=int, default=3)
|
| 14 |
+
parser.add_argument("--max-input-tokens", type=int, default=4096)
|
| 15 |
+
args = parser.parse_args()
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
from torch.utils.data import DataLoader
|
| 19 |
+
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
| 20 |
+
|
| 21 |
+
rows = [json.loads(line) for line in args.pairs.read_text(encoding="utf-8").splitlines() if line.strip()]
|
| 22 |
+
tokenizer = AutoTokenizer.from_pretrained(args.base)
|
| 23 |
+
model = AutoModelForSequenceClassification.from_pretrained(args.base, num_labels=1)
|
| 24 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 25 |
+
model.to(device)
|
| 26 |
+
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-5)
|
| 27 |
+
loss_fn = torch.nn.BCEWithLogitsLoss()
|
| 28 |
+
|
| 29 |
+
def batches():
|
| 30 |
+
for start in range(0, len(rows), 16):
|
| 31 |
+
chunk = rows[start : start + 16]
|
| 32 |
+
encoded = tokenizer(
|
| 33 |
+
[r["query"] for r in chunk],
|
| 34 |
+
[r["draft"] for r in chunk],
|
| 35 |
+
truncation=True,
|
| 36 |
+
max_length=args.max_input_tokens,
|
| 37 |
+
padding=True,
|
| 38 |
+
return_tensors="pt",
|
| 39 |
+
)
|
| 40 |
+
labels = torch.tensor([float(r["y"]) for r in chunk])
|
| 41 |
+
yield {k: v.to(device) for k, v in encoded.items()}, labels.to(device)
|
| 42 |
+
|
| 43 |
+
model.train()
|
| 44 |
+
for epoch in range(args.epochs):
|
| 45 |
+
total = 0.0
|
| 46 |
+
for encoded, labels in DataLoader(list(batches()), batch_size=None):
|
| 47 |
+
optimizer.zero_grad()
|
| 48 |
+
logits = model(**encoded).logits.reshape(-1)
|
| 49 |
+
loss = loss_fn(logits, labels)
|
| 50 |
+
loss.backward()
|
| 51 |
+
optimizer.step()
|
| 52 |
+
total += float(loss)
|
| 53 |
+
print(f"epoch {epoch + 1}: loss {total:.4f}")
|
| 54 |
+
model.save_pretrained(args.out, safe_serialization=True)
|
| 55 |
+
tokenizer.save_pretrained(args.out)
|
| 56 |
+
print(f"saved to {args.out}")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
if __name__ == "__main__":
|
| 60 |
+
main()
|
slm_repo/LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright (c) 2026 Microtensor. All rights reserved.
|
| 2 |
+
|
| 3 |
+
This template and every derivative of it are licensed to you solely for
|
| 4 |
+
building and submitting systems to Microtensor arenas under the Microtensor
|
| 5 |
+
submission terms (https://www.microtensor.cloud/terms). No other use,
|
| 6 |
+
redistribution or sublicensing is permitted without written permission from
|
| 7 |
+
Microtensor. Models submitted to an arena are licensed to Microtensor under
|
| 8 |
+
the submission terms and are not published under a free licence.
|
slm_repo/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: other
|
| 3 |
+
license_name: microtensor-submission
|
| 4 |
+
license_link: https://www.microtensor.cloud/terms
|
| 5 |
+
base_model: <allowed base>
|
| 6 |
+
tags:
|
| 7 |
+
- microtensor
|
| 8 |
+
- <arena>
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# mt_slm_<arena>
|
| 12 |
+
|
| 13 |
+
A specialised model for the Microtensor `<arena>` arena.
|
| 14 |
+
|
| 15 |
+
| Field | Value |
|
| 16 |
+
|---|---|
|
| 17 |
+
| Base model | an allowed base from the arena definition, with its licence |
|
| 18 |
+
| Method | see `training/recipe.json` |
|
| 19 |
+
| Arena | <arena> |
|
| 20 |
+
| Scores | filled in from the ledger after the first epoch |
|
| 21 |
+
|
| 22 |
+
The root of this repository loads with `AutoModelForCausalLM`. Everything under `training/` is the record validators require and the archive keeps; it is never scored.
|
slm_repo/training/compression/before_after.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"before": {"execution_accuracy": 0.0}, "after": {"execution_accuracy": 0.0}}
|
slm_repo/training/compression/compression.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"method": "awq", "bits": 4, "calibration_data_sha256": ""}
|
slm_repo/training/data/data_card.md
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Data card
|
| 2 |
+
|
| 3 |
+
Describe what the training data is and how it was built: the public sample items from the arena, synthetic questions generated against your own schemas, and the teacher model that answered them. Hidden arena items never appear here because you never see them.
|
slm_repo/training/data/sources.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"name": "microtensor/<arena>-sample",
|
| 4 |
+
"version": "",
|
| 5 |
+
"license": "",
|
| 6 |
+
"sha256": ""
|
| 7 |
+
},
|
| 8 |
+
{
|
| 9 |
+
"name": "own-synthetic",
|
| 10 |
+
"version": "1",
|
| 11 |
+
"license": "proprietary",
|
| 12 |
+
"sha256": ""
|
| 13 |
+
}
|
| 14 |
+
]
|
slm_repo/training/data/synthetic.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"teacher_model": "", "prompts": ["see scripts/distill.py"]}
|
slm_repo/training/distillation/teacher.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"teacher_model": "", "teacher_version": "", "harness_used": "harness/ from the artifact repository"}
|
slm_repo/training/recipe.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"base_model": "<allowed base>",
|
| 3 |
+
"method": "distillation",
|
| 4 |
+
"harness_aware": false,
|
| 5 |
+
"hyperparameters": {
|
| 6 |
+
"epochs": 2,
|
| 7 |
+
"learning_rate": 1e-05,
|
| 8 |
+
"batch_size": 16,
|
| 9 |
+
"max_length": 4096
|
| 10 |
+
},
|
| 11 |
+
"seeds": [
|
| 12 |
+
0
|
| 13 |
+
],
|
| 14 |
+
"compression": "awq-int4"
|
| 15 |
+
}
|
slm_repo/training/runs/example/config.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"run_id": "example", "base_model": "Qwen/Qwen2.5-Coder-7B-Instruct", "epochs": 2, "learning_rate": 1e-5}
|
slm_repo/training/runs/example/env.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"libraries": {"torch": "", "transformers": ""}, "gpu": "", "hours": 0, "profile": ""}
|
slm_repo/training/runs/example/final_eval.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"execution_accuracy": 0.0, "items": 0}
|
slm_repo/training/runs/example/metrics.jsonl
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"step": 0, "loss": 0.0, "eval_execution_accuracy": 0.0, "note": "replace with your real curves"}
|
slm_repo/training/scripts/compress.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import hashlib
|
| 5 |
+
import json
|
| 6 |
+
import random
|
| 7 |
+
import shutil
|
| 8 |
+
import subprocess
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Any
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def calibration(path: str, count: int, seed: int) -> list[str]:
|
| 14 |
+
rows = [json.loads(line) for line in Path(path).read_text(encoding="utf-8").splitlines() if line.strip()]
|
| 15 |
+
texts = [str(r.get("prompt", "")) + str(r.get("completion", "")) for r in rows if r.get("prompt")]
|
| 16 |
+
random.Random(seed).shuffle(texts)
|
| 17 |
+
return texts[:count]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def quantise_awq(model: str, out: Path, samples: list[str], bits: int, group: int) -> None:
|
| 21 |
+
from awq import AutoAWQForCausalLM
|
| 22 |
+
from transformers import AutoTokenizer
|
| 23 |
+
|
| 24 |
+
tokenizer = AutoTokenizer.from_pretrained(model)
|
| 25 |
+
quantiser = AutoAWQForCausalLM.from_pretrained(model, safetensors=True)
|
| 26 |
+
quantiser.quantize(
|
| 27 |
+
tokenizer,
|
| 28 |
+
quant_config={"zero_point": True, "q_group_size": group, "w_bit": bits, "version": "GEMM"},
|
| 29 |
+
calib_data=samples,
|
| 30 |
+
)
|
| 31 |
+
quantiser.save_quantized(str(out), safetensors=True)
|
| 32 |
+
tokenizer.save_pretrained(str(out))
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def quantise_gptq(model: str, out: Path, samples: list[str], bits: int, group: int) -> None:
|
| 36 |
+
from gptqmodel import GPTQModel, QuantizeConfig
|
| 37 |
+
from transformers import AutoTokenizer
|
| 38 |
+
|
| 39 |
+
tokenizer = AutoTokenizer.from_pretrained(model)
|
| 40 |
+
quantiser = GPTQModel.load(model, QuantizeConfig(bits=bits, group_size=group, desc_act=False))
|
| 41 |
+
quantiser.quantize(samples, batch_size=1)
|
| 42 |
+
quantiser.save(str(out))
|
| 43 |
+
tokenizer.save_pretrained(str(out))
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def quantise_gguf(model: str, out: Path, kind: str, llama_cpp: Path) -> None:
|
| 47 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 48 |
+
f16 = out / "model-f16.gguf"
|
| 49 |
+
subprocess.run(
|
| 50 |
+
["python", str(llama_cpp / "convert_hf_to_gguf.py"), model, "--outtype", "f16", "--outfile", str(f16)],
|
| 51 |
+
check=True,
|
| 52 |
+
)
|
| 53 |
+
binary = llama_cpp / "build" / "bin" / "llama-quantize"
|
| 54 |
+
if not binary.exists():
|
| 55 |
+
binary = llama_cpp / "llama-quantize"
|
| 56 |
+
subprocess.run([str(binary), str(f16), str(out / "model.gguf"), kind], check=True)
|
| 57 |
+
f16.unlink()
|
| 58 |
+
for name in ("tokenizer.json", "tokenizer_config.json", "config.json"):
|
| 59 |
+
source = Path(model) / name
|
| 60 |
+
if source.is_file():
|
| 61 |
+
shutil.copy2(source, out / name)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def main() -> None:
|
| 65 |
+
parser = argparse.ArgumentParser(description="compress the merged model to the format your envelope group takes")
|
| 66 |
+
parser.add_argument("--model", required=True, help="the merged model folder from train.py")
|
| 67 |
+
parser.add_argument("--out", required=True, help="the folder that becomes slm_repo/ weights")
|
| 68 |
+
parser.add_argument("--method", default="awq", choices=["awq", "gptq", "gguf"])
|
| 69 |
+
parser.add_argument("--bits", type=int, default=4)
|
| 70 |
+
parser.add_argument("--group-size", type=int, default=128)
|
| 71 |
+
parser.add_argument("--gguf-type", default="Q4_K_M", choices=["Q4_K_M", "Q5_K_S", "Q5_K_M", "Q6_K", "Q8_0"])
|
| 72 |
+
parser.add_argument("--llama-cpp", default="", help="path to a llama.cpp checkout with llama-quantize built")
|
| 73 |
+
parser.add_argument("--calibration", default="", help="pairs.jsonl from distill.py, used as calibration text")
|
| 74 |
+
parser.add_argument("--samples", type=int, default=256)
|
| 75 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 76 |
+
args = parser.parse_args()
|
| 77 |
+
|
| 78 |
+
if args.bits < 4:
|
| 79 |
+
raise SystemExit("nothing below 4 bit is accepted in any envelope group")
|
| 80 |
+
out = Path(args.out)
|
| 81 |
+
samples: list[str] = []
|
| 82 |
+
calibration_sha = ""
|
| 83 |
+
if args.method in ("awq", "gptq"):
|
| 84 |
+
if not args.calibration:
|
| 85 |
+
raise SystemExit("awq and gptq need --calibration pairs.jsonl from your own training inputs")
|
| 86 |
+
samples = calibration(args.calibration, args.samples, args.seed)
|
| 87 |
+
calibration_sha = hashlib.sha256("\n".join(samples).encode("utf-8")).hexdigest()
|
| 88 |
+
if args.method == "awq":
|
| 89 |
+
quantise_awq(args.model, out, samples, args.bits, args.group_size)
|
| 90 |
+
elif args.method == "gptq":
|
| 91 |
+
quantise_gptq(args.model, out, samples, args.bits, args.group_size)
|
| 92 |
+
else:
|
| 93 |
+
if not args.llama_cpp:
|
| 94 |
+
raise SystemExit("gguf needs --llama-cpp pointing at a llama.cpp checkout")
|
| 95 |
+
quantise_gguf(args.model, out, args.gguf_type, Path(args.llama_cpp))
|
| 96 |
+
|
| 97 |
+
record: dict[str, Any] = {
|
| 98 |
+
"method": args.method,
|
| 99 |
+
"bits": args.bits if args.method != "gguf" else None,
|
| 100 |
+
"group_size": args.group_size if args.method != "gguf" else None,
|
| 101 |
+
"gguf_type": args.gguf_type if args.method == "gguf" else None,
|
| 102 |
+
"calibration_samples": len(samples),
|
| 103 |
+
"calibration_data_sha256": calibration_sha,
|
| 104 |
+
"source_model": args.model,
|
| 105 |
+
}
|
| 106 |
+
folder = Path(__file__).resolve().parents[1] / "compression"
|
| 107 |
+
folder.mkdir(parents=True, exist_ok=True)
|
| 108 |
+
(folder / "compression.json").write_text(json.dumps(record, indent=2) + "\n", encoding="utf-8")
|
| 109 |
+
before_after = folder / "before_after.json"
|
| 110 |
+
if not before_after.is_file():
|
| 111 |
+
before_after.write_text(
|
| 112 |
+
json.dumps({"before": {}, "after": {}, "note": "fill both from score_compress.py on the held out items"}, indent=2)
|
| 113 |
+
+ "\n",
|
| 114 |
+
encoding="utf-8",
|
| 115 |
+
)
|
| 116 |
+
print(f"compressed weights at {out}; record at {folder / 'compression.json'}")
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
if __name__ == "__main__":
|
| 120 |
+
main()
|
slm_repo/training/scripts/distill.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import hashlib
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import re
|
| 8 |
+
import time
|
| 9 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
import httpx
|
| 14 |
+
|
| 15 |
+
FENCE = re.compile(r"^```[A-Za-z]*\s*|\s*```$")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def fetch_arena(server: str, arena: str) -> dict[str, Any]:
|
| 19 |
+
response = httpx.get(f"{server.rstrip('/')}/v2/arenas/{arena}", timeout=60.0)
|
| 20 |
+
response.raise_for_status()
|
| 21 |
+
found: dict[str, Any] = response.json()
|
| 22 |
+
return found
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def chat(base_url: str, key: str, model: str, messages: list[dict[str, str]], max_tokens: int) -> str:
|
| 26 |
+
for attempt in range(4):
|
| 27 |
+
try:
|
| 28 |
+
response = httpx.post(
|
| 29 |
+
f"{base_url.rstrip('/')}/chat/completions",
|
| 30 |
+
headers={"authorization": f"Bearer {key}"},
|
| 31 |
+
json={"model": model, "messages": messages, "temperature": 0, "max_tokens": max_tokens},
|
| 32 |
+
timeout=300.0,
|
| 33 |
+
)
|
| 34 |
+
response.raise_for_status()
|
| 35 |
+
data = response.json()
|
| 36 |
+
return str(((data.get("choices") or [{}])[0].get("message") or {}).get("content") or "")
|
| 37 |
+
except (httpx.HTTPError, ValueError):
|
| 38 |
+
time.sleep(2.0 * (attempt + 1))
|
| 39 |
+
return ""
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def render(template: str, task: dict[str, Any]) -> str:
|
| 43 |
+
text = template
|
| 44 |
+
for key, value in task.items():
|
| 45 |
+
if isinstance(value, str):
|
| 46 |
+
text = text.replace("{" + key + "}", value)
|
| 47 |
+
return text
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def grader_for(arena: dict[str, Any]) -> Any:
|
| 51 |
+
definition = arena.get("definition") or {}
|
| 52 |
+
name = str(definition.get("grader") or "")
|
| 53 |
+
if name == "context_compression":
|
| 54 |
+
from microtensor_subnet.validator import compress
|
| 55 |
+
|
| 56 |
+
grading = definition.get("grading") or {}
|
| 57 |
+
from tokenizers import Tokenizer
|
| 58 |
+
|
| 59 |
+
tokenizer = Tokenizer.from_pretrained(str(grading.get("tokenizer") or "Qwen/Qwen2.5-Coder-7B-Instruct"))
|
| 60 |
+
judge = compress.Judge(
|
| 61 |
+
count_tokens=lambda text: len(tokenizer.encode(text).ids),
|
| 62 |
+
answer=lambda summary, questions: ["" for _ in questions],
|
| 63 |
+
pass_threshold=float(grading.get("pass_threshold") or 0.5),
|
| 64 |
+
min_output_tokens=int(grading.get("min_output_tokens") or 300),
|
| 65 |
+
max_output_share=float(grading.get("max_output_share") or 0.1),
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
def grade(output: str, label: dict[str, Any], task: dict[str, Any]) -> tuple[bool, str]:
|
| 69 |
+
verdict = compress.grade(output, label, task, judge)
|
| 70 |
+
parts = dict(p.split(" ") for p in verdict.reason.split(", ")) if "recall" in verdict.reason else {}
|
| 71 |
+
if not parts:
|
| 72 |
+
return False, verdict.reason
|
| 73 |
+
ok = float(parts.get("recall", 0)) >= 0.99 and float(parts.get("invented", 1)) == 0.0
|
| 74 |
+
return ok, verdict.reason
|
| 75 |
+
|
| 76 |
+
return grade
|
| 77 |
+
|
| 78 |
+
from microtensor_subnet.validator import grade as grading_module
|
| 79 |
+
|
| 80 |
+
def grade(output: str, label: dict[str, Any], task: dict[str, Any]) -> tuple[bool, str]:
|
| 81 |
+
verdict = grading_module.grade(name, output, label, task)
|
| 82 |
+
return bool(verdict.success), str(verdict.reason)
|
| 83 |
+
|
| 84 |
+
return grade
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def main() -> None:
|
| 88 |
+
parser = argparse.ArgumentParser(description="write teacher answers for dev items and keep the ones the grader accepts")
|
| 89 |
+
parser.add_argument("--arena", required=True)
|
| 90 |
+
parser.add_argument("--items", required=True, help="items.jsonl built with the arena dev tooling, with task and label")
|
| 91 |
+
parser.add_argument("--out", required=True, help="jsonl of {prompt, completion} for train.py")
|
| 92 |
+
parser.add_argument("--teacher", required=True, help="teacher model id at your provider")
|
| 93 |
+
parser.add_argument("--base-url", default=os.environ.get("TEACHER_BASE_URL", "https://openrouter.ai/api/v1"))
|
| 94 |
+
parser.add_argument("--key-env", default="TEACHER_API_KEY")
|
| 95 |
+
parser.add_argument("--server", default="https://api.microtensor.cloud")
|
| 96 |
+
parser.add_argument("--max-tokens", type=int, default=0, help="default: the arena output budget")
|
| 97 |
+
parser.add_argument("--workers", type=int, default=8)
|
| 98 |
+
parser.add_argument("--limit", type=int, default=0)
|
| 99 |
+
parser.add_argument("--keep-rejected", default="", help="write grader rejected answers here for inspection")
|
| 100 |
+
args = parser.parse_args()
|
| 101 |
+
|
| 102 |
+
key = os.environ.get(args.key_env, "")
|
| 103 |
+
if not key:
|
| 104 |
+
raise SystemExit(f"set {args.key_env} to your provider key")
|
| 105 |
+
arena = fetch_arena(args.server, args.arena)
|
| 106 |
+
definition = arena.get("definition") or {}
|
| 107 |
+
template = str(definition.get("slm_prompt") or "")
|
| 108 |
+
if not template:
|
| 109 |
+
raise SystemExit(f"{args.arena} publishes no slm_prompt")
|
| 110 |
+
max_tokens = args.max_tokens or int((definition.get("generation") or {}).get("max_tokens") or 1024)
|
| 111 |
+
grade = grader_for(arena)
|
| 112 |
+
|
| 113 |
+
rows = [json.loads(line) for line in Path(args.items).read_text(encoding="utf-8").splitlines() if line.strip()]
|
| 114 |
+
items = [row for row in rows if not row.get("twin_of")]
|
| 115 |
+
if args.limit:
|
| 116 |
+
items = items[: args.limit]
|
| 117 |
+
|
| 118 |
+
def one(item: dict[str, Any]) -> dict[str, Any]:
|
| 119 |
+
task = item["task"]
|
| 120 |
+
messages = [{"role": "system", "content": str(task.get("system") or "")}]
|
| 121 |
+
messages.append({"role": "user", "content": str(task.get("user") or json.dumps(task, sort_keys=True))})
|
| 122 |
+
answer = FENCE.sub("", chat(args.base_url, key, args.teacher, messages, max_tokens).strip())
|
| 123 |
+
ok, reason = grade(answer, item.get("label") or {}, task)
|
| 124 |
+
return {
|
| 125 |
+
"item_id": item.get("item_id"),
|
| 126 |
+
"ok": ok,
|
| 127 |
+
"reason": reason,
|
| 128 |
+
"prompt": render(template, task),
|
| 129 |
+
"completion": answer,
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
with ThreadPoolExecutor(max_workers=args.workers) as pool:
|
| 133 |
+
results = list(pool.map(one, items))
|
| 134 |
+
kept = [r for r in results if r["ok"]]
|
| 135 |
+
out = Path(args.out)
|
| 136 |
+
out.parent.mkdir(parents=True, exist_ok=True)
|
| 137 |
+
out.write_text(
|
| 138 |
+
"".join(json.dumps({"prompt": r["prompt"], "completion": r["completion"]}) + "\n" for r in kept),
|
| 139 |
+
encoding="utf-8",
|
| 140 |
+
)
|
| 141 |
+
if args.keep_rejected:
|
| 142 |
+
Path(args.keep_rejected).write_text(
|
| 143 |
+
"".join(json.dumps(r) + "\n" for r in results if not r["ok"]), encoding="utf-8"
|
| 144 |
+
)
|
| 145 |
+
digest = hashlib.sha256(out.read_bytes()).hexdigest()
|
| 146 |
+
record = Path(__file__).resolve().parents[1] / "distillation" / "teacher.json"
|
| 147 |
+
record.write_text(
|
| 148 |
+
json.dumps(
|
| 149 |
+
{
|
| 150 |
+
"teacher_model": args.teacher,
|
| 151 |
+
"teacher_version": "",
|
| 152 |
+
"provider": args.base_url,
|
| 153 |
+
"items": len(results),
|
| 154 |
+
"kept": len(kept),
|
| 155 |
+
"pairs_sha256": digest,
|
| 156 |
+
"harness_used": "none, the arena prompt",
|
| 157 |
+
},
|
| 158 |
+
indent=2,
|
| 159 |
+
)
|
| 160 |
+
+ "\n",
|
| 161 |
+
encoding="utf-8",
|
| 162 |
+
)
|
| 163 |
+
print(f"{len(kept)} of {len(results)} teacher answers passed the grader; pairs at {out} (sha256 {digest[:12]})")
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
if __name__ == "__main__":
|
| 167 |
+
main()
|
slm_repo/training/scripts/requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch
|
| 2 |
+
transformers>=4.55
|
| 3 |
+
datasets
|
| 4 |
+
accelerate
|
| 5 |
+
peft>=0.15
|
| 6 |
+
trl>=0.19
|
| 7 |
+
bitsandbytes
|
| 8 |
+
autoawq
|
| 9 |
+
gptqmodel
|
| 10 |
+
tokenizers
|
| 11 |
+
httpx
|
| 12 |
+
microtensor-subnet @ git+https://github.com/microtensor-io/microtensor-subnet.git
|
slm_repo/training/scripts/score_compress.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import statistics
|
| 6 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
import httpx
|
| 11 |
+
from microtensor_subnet.validator import compress
|
| 12 |
+
from microtensor_subnet.validator.sandbox.judge import PROMPT
|
| 13 |
+
|
| 14 |
+
LINES = (0.5, 0.6, 0.7, 0.8, 0.9)
|
| 15 |
+
TOKENIZER = "Qwen/Qwen2.5-Coder-7B-Instruct"
|
| 16 |
+
STOP = ["<|im_end|>", "<|endoftext|>", "<|end|>"]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def post(url: str, body: dict[str, Any]) -> dict[str, Any]:
|
| 20 |
+
response = httpx.post(url, json=body, timeout=900.0)
|
| 21 |
+
response.raise_for_status()
|
| 22 |
+
found: dict[str, Any] = response.json()
|
| 23 |
+
return found
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def complete(url: str, model: str, prompt: str, max_tokens: int) -> str:
|
| 27 |
+
data = post(
|
| 28 |
+
url.rstrip("/") + "/v1/completions",
|
| 29 |
+
{"model": model, "prompt": prompt, "temperature": 0, "max_tokens": max_tokens, "stop": STOP},
|
| 30 |
+
)
|
| 31 |
+
return str((data.get("choices") or [{}])[0].get("text") or "")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def chat(url: str, model: str, messages: list[dict[str, str]], max_tokens: int) -> str:
|
| 35 |
+
data = post(
|
| 36 |
+
url.rstrip("/") + "/v1/chat/completions",
|
| 37 |
+
{"model": model, "messages": messages, "temperature": 0, "max_tokens": max_tokens},
|
| 38 |
+
)
|
| 39 |
+
return str(((data.get("choices") or [{}])[0].get("message") or {}).get("content") or "")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def main() -> None:
|
| 43 |
+
parser = argparse.ArgumentParser(description="score a model on ctx_compress items the way validators do")
|
| 44 |
+
parser.add_argument("--items", required=True, help="items.jsonl from mta dev")
|
| 45 |
+
parser.add_argument("--arena", default="ctx_compress")
|
| 46 |
+
parser.add_argument("--server", default="https://api.microtensor.cloud")
|
| 47 |
+
parser.add_argument("--slm-url", required=True, help="your model behind an OpenAI compatible server")
|
| 48 |
+
parser.add_argument("--slm-model", default="slm")
|
| 49 |
+
parser.add_argument("--judge-url", required=True, help="Qwen/Qwen2.5-Coder-7B-Instruct, the probe agent")
|
| 50 |
+
parser.add_argument("--judge-model", default="judge")
|
| 51 |
+
parser.add_argument("--limit", type=int, default=200)
|
| 52 |
+
parser.add_argument("--max-tokens", type=int, default=3072)
|
| 53 |
+
parser.add_argument("--workers", type=int, default=8)
|
| 54 |
+
parser.add_argument("--pass-line", type=float, default=0.5)
|
| 55 |
+
parser.add_argument("--out", default="", help="write one scored line per item here")
|
| 56 |
+
args = parser.parse_args()
|
| 57 |
+
|
| 58 |
+
from tokenizers import Tokenizer
|
| 59 |
+
|
| 60 |
+
tokenizer = Tokenizer.from_pretrained(TOKENIZER)
|
| 61 |
+
arena = httpx.get(f"{args.server.rstrip('/')}/v2/arenas/{args.arena}", timeout=60.0).json()
|
| 62 |
+
template = str((arena.get("definition") or {}).get("slm_prompt") or "")
|
| 63 |
+
if not template:
|
| 64 |
+
raise SystemExit(f"{args.arena} publishes no slm_prompt")
|
| 65 |
+
|
| 66 |
+
def answer(summary: str, questions: list[str]) -> list[str]:
|
| 67 |
+
return [
|
| 68 |
+
chat(
|
| 69 |
+
args.judge_url,
|
| 70 |
+
args.judge_model,
|
| 71 |
+
[
|
| 72 |
+
{"role": "system", "content": PROMPT},
|
| 73 |
+
{"role": "user", "content": f"<summary>\n{summary}\n</summary>\n\nQuestion: {q}"},
|
| 74 |
+
],
|
| 75 |
+
64,
|
| 76 |
+
)
|
| 77 |
+
for q in questions
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
judge = compress.Judge(
|
| 81 |
+
count_tokens=lambda text: len(tokenizer.encode(text).ids),
|
| 82 |
+
answer=answer,
|
| 83 |
+
pass_threshold=args.pass_line,
|
| 84 |
+
)
|
| 85 |
+
rows = [json.loads(line) for line in Path(args.items).read_text(encoding="utf-8").splitlines() if line.strip()]
|
| 86 |
+
items = [row for row in rows if not row.get("twin_of")][: args.limit]
|
| 87 |
+
|
| 88 |
+
def one(item: dict[str, Any]) -> dict[str, Any]:
|
| 89 |
+
task = item["task"]
|
| 90 |
+
prompt = template.replace("{system}", task["system"]).replace("{user}", task["user"])
|
| 91 |
+
output = complete(args.slm_url, args.slm_model, prompt, args.max_tokens)
|
| 92 |
+
verdict = compress.grade(output, item["label"], task, judge)
|
| 93 |
+
parts = dict(p.split(" ") for p in verdict.reason.split(", ")) if "recall" in verdict.reason else {}
|
| 94 |
+
return {"item_id": item["item_id"], "score": verdict.score, "reason": verdict.reason, "parts": parts, "output": output}
|
| 95 |
+
|
| 96 |
+
with ThreadPoolExecutor(max_workers=args.workers) as pool:
|
| 97 |
+
results = list(pool.map(one, items))
|
| 98 |
+
scores = [r["score"] for r in results]
|
| 99 |
+
print(f"items {len(scores)}, mean item score {statistics.mean(scores):.3f}")
|
| 100 |
+
for line in LINES:
|
| 101 |
+
print(f"pass at {line:.1f}: {sum(s >= line for s in scores) / len(scores):.1%}")
|
| 102 |
+
for key in ("recall", "invented", "continuation"):
|
| 103 |
+
values = [float(r["parts"][key]) for r in results if key in r["parts"]]
|
| 104 |
+
if values:
|
| 105 |
+
print(f"{key}: mean {statistics.mean(values):.3f}")
|
| 106 |
+
failed = [r["reason"] for r in results if not r["parts"]]
|
| 107 |
+
print(f"format or budget failures: {len(failed)}")
|
| 108 |
+
if args.out:
|
| 109 |
+
Path(args.out).write_text("".join(json.dumps(r) + "\n" for r in results), encoding="utf-8")
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
if __name__ == "__main__":
|
| 113 |
+
main()
|