programasweights 0.2.7__tar.gz → 0.2.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {programasweights-0.2.7 → programasweights-0.2.8}/AGENTS.md +8 -8
- {programasweights-0.2.7 → programasweights-0.2.8}/PKG-INFO +1 -1
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/adr/001-llama-cpp-over-pytorch.md +1 -1
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/api-reference/cli.md +1 -1
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/api-reference/python-sdk.md +1 -1
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/api-reference/rest-api.md +10 -2
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/architecture.md +1 -1
- programasweights-0.2.8/docs/case-studies/log-monitoring.md +132 -0
- programasweights-0.2.8/docs/case-studies/tool-calling.md +477 -0
- programasweights-0.2.8/docs/guide/writing-good-specs.md +11 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/mkdocs.yml +2 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/programasweights/__init__.py +12 -2
- programasweights-0.2.8/programasweights/_output.py +34 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/programasweights/cache.py +7 -4
- {programasweights-0.2.7 → programasweights-0.2.8}/programasweights/client.py +10 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/programasweights/runtime_llamacpp.py +4 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/pyproject.toml +1 -1
- programasweights-0.2.7/docs/guide/writing-good-specs.md +0 -36
- {programasweights-0.2.7 → programasweights-0.2.8}/.gitignore +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/.readthedocs.yaml +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/CHANGELOG.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/LICENSE +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/PYPI_README.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/README.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/adr/002-q4_0-adapter-format.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/adr/003-single-spec-field.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/adr/004-compiler-naming.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/adr/005-vllm-hidden-states.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/adr/006-email-api-key-auth.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/advanced/adrs.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/advanced/architecture.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/case-studies/semantic-search.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/case-studies/site-navigation.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/getting-started/first-program.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/getting-started/installation.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/getting-started/naming-programs.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/guide/browser-inference.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/guide/how-it-works.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/guide/local-inference.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/hub/browsing-programs.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/hub/feedback-cases.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/hub/publishing-programs.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/index.md +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/docs/requirements.txt +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/examples/flask_app.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/examples/jupyter_notebook.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/examples/langchain_integration.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/examples/paw_monitor.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/examples/replace_openai.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/programasweights/artifacts.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/programasweights/cli.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/programasweights/compiler/__init__.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/programasweights/compiler/dummy.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/programasweights/config.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/programasweights/convert_peft_to_paw.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/programasweights/paw_format.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/programasweights/runtime/__init__.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/programasweights/runtime/interpreter.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/programasweights/runtime/interpreter_onnx.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/tests/test_offline_cache.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/tests/test_sdk.py +0 -0
- {programasweights-0.2.7 → programasweights-0.2.8}/tests/test_sdk.sh +0 -0
|
@@ -78,6 +78,11 @@ Output: delete
|
|
|
78
78
|
- **Include examples from your actual data**: Examples outperform prose-only descriptions.
|
|
79
79
|
- **Debug failures before sweeping**: Look at specific failing examples and understand WHY before trying many variants.
|
|
80
80
|
|
|
81
|
+
## Context Window
|
|
82
|
+
|
|
83
|
+
- Spec + input + output share a ~2048 token context window. Inputs that exceed it will error.
|
|
84
|
+
- `max_tokens` defaults to `None`: generation runs until EOS or the context limit.
|
|
85
|
+
|
|
81
86
|
## Chaining Functions
|
|
82
87
|
|
|
83
88
|
Multiple PAW functions can be composed for multi-step tasks:
|
|
@@ -93,11 +98,7 @@ if label != "other":
|
|
|
93
98
|
|
|
94
99
|
Chain them with regular Python logic.
|
|
95
100
|
|
|
96
|
-
##
|
|
97
|
-
|
|
98
|
-
Production examples with iterative spec-tuning walkthroughs: [site navigation](https://programasweights.readthedocs.io/en/latest/case-studies/site-navigation/) (5-program pipeline, Cmd+K helper) and [semantic search](https://programasweights.readthedocs.io/en/latest/case-studies/semantic-search/) (reranking without a vector database).
|
|
99
|
-
|
|
100
|
-
## Event-Driven Monitoring
|
|
101
|
+
## Log Monitoring
|
|
101
102
|
|
|
102
103
|
PAW functions can classify log output. Compile once with examples from your specific logs, then reuse the function locally forever:
|
|
103
104
|
|
|
@@ -199,8 +200,7 @@ paw.login()
|
|
|
199
200
|
- **Cache**: `~/.cache/programasweights/`. Override with `PAW_CACHE_DIR`.
|
|
200
201
|
- **Offline** after first download.
|
|
201
202
|
|
|
202
|
-
##
|
|
203
|
+
## Case Studies
|
|
203
204
|
|
|
204
|
-
|
|
205
|
-
- `max_tokens` defaults to `None`: generation runs until EOS or the context limit.
|
|
205
|
+
Detailed walkthroughs of building production systems with PAW, including what we tried and what we learned: [log monitoring](https://programasweights.readthedocs.io/en/latest/case-studies/log-monitoring/), [site navigation](https://programasweights.readthedocs.io/en/latest/case-studies/site-navigation/), [semantic search](https://programasweights.readthedocs.io/en/latest/case-studies/semantic-search/), [tool calling](https://programasweights.readthedocs.io/en/latest/case-studies/tool-calling/).
|
|
206
206
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: programasweights
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Compile natural language specifications into neural programs that run locally via llama.cpp.
|
|
5
5
|
Project-URL: Homepage, https://programasweights.com
|
|
6
6
|
Project-URL: Repository, https://github.com/programasweights/programasweights-python
|
|
@@ -17,5 +17,5 @@ Replace the PyTorch runtime with llama-cpp-python (~80MB install). Use GGUF mode
|
|
|
17
17
|
- Inference uses Metal (Mac), CPU (Linux/Windows) — no GPU required
|
|
18
18
|
- Must pre-render chat templates server-side (no transformers tokenizer on client)
|
|
19
19
|
- .paw format must change from v2 (safetensors) to v3 (GGUF adapter)
|
|
20
|
-
- Base model is downloaded once (~
|
|
20
|
+
- Base model is downloaded once (~594 MB for Q6_K) and shared across all functions
|
|
21
21
|
- Per-function adapter download is ~23MB (Q4_0, confirmed lossless at 4096-scale eval)
|
|
@@ -57,7 +57,7 @@ paw rename <program_id> <new-slug> [--json]
|
|
|
57
57
|
| Argument | Description |
|
|
58
58
|
|----------|-------------|
|
|
59
59
|
| `program_id` | Program hash ID or current slug. |
|
|
60
|
-
| `new-slug` | New slug name (2-50 chars, lowercase alphanumeric and hyphens). |
|
|
60
|
+
| `new-slug` | New slug name (2-50 chars, lowercase alphanumeric and hyphens). Pass `""` to remove. |
|
|
61
61
|
|
|
62
62
|
## `paw info`
|
|
63
63
|
|
|
@@ -41,7 +41,7 @@ output: str = fn(input_text, max_tokens=None, temperature=0.0)
|
|
|
41
41
|
| `max_tokens` | Maximum tokens to generate. `None` (default) = use all remaining context window. |
|
|
42
42
|
| `temperature` | Sampling temperature (default `0.0`). |
|
|
43
43
|
|
|
44
|
-
**Context limits:** Spec + input + output share a ~2048 token window.
|
|
44
|
+
**Context limits:** Spec + input + output share a ~2048 token window. Inputs that exceed it will error. `max_tokens` defaults to `None`: generation runs until EOS or the context limit.
|
|
45
45
|
|
|
46
46
|
## `paw.compile`
|
|
47
47
|
|
|
@@ -34,7 +34,11 @@ Compile a specification.
|
|
|
34
34
|
| `name` | string | No | Display title. Auto-generated if omitted. |
|
|
35
35
|
| `tags` | string[] | No | Tags for discovery. |
|
|
36
36
|
|
|
37
|
-
**Response
|
|
37
|
+
**Response:**
|
|
38
|
+
- **202 Accepted** on success — JSON with `job_id`, `status`, `program_id`, `slug` (if created), `pseudo_program`, and `timings`
|
|
39
|
+
- **400** — unknown compiler
|
|
40
|
+
- **422** — spec too short, too long, or token limit exceeded
|
|
41
|
+
- **500** — compilation failed (server error)
|
|
38
42
|
|
|
39
43
|
### `POST /infer`
|
|
40
44
|
|
|
@@ -88,7 +92,11 @@ Owner-only fields: `public`, `name`, `tags`. Any authenticated user can set thei
|
|
|
88
92
|
|
|
89
93
|
### `GET /programs/{id_or_slug}/download`
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
Downloads the `.paw` artifact. Returns one of:
|
|
96
|
+
- **302** redirect to HuggingFace CDN (program already uploaded)
|
|
97
|
+
- **200** file from server (freshly compiled, not yet on HF)
|
|
98
|
+
- **202 Accepted** with `Retry-After` header (program assets still generating, retry after the indicated seconds)
|
|
99
|
+
- **404** if program not found or private and not owned by the requester
|
|
92
100
|
|
|
93
101
|
### `GET /programs/resolve/{slug}`
|
|
94
102
|
|
|
@@ -31,7 +31,7 @@ vLLM multi-LoRA serving on GPU 2 (or GPU 1 in 2-GPU config). Each compiled progr
|
|
|
31
31
|
|
|
32
32
|
## Inference (Client-side SDK)
|
|
33
33
|
|
|
34
|
-
llama-cpp-python loads the base GGUF model (
|
|
34
|
+
llama-cpp-python loads the base GGUF model (594 MB, downloaded once) and hot-swaps Q4_0 adapters per function call. No PyTorch or transformers required.
|
|
35
35
|
|
|
36
36
|
## GPU Assignment
|
|
37
37
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Event-Driven Log Monitoring
|
|
2
|
+
|
|
3
|
+
Long-running processes -- training runs, deployments, data pipelines -- produce thousands of log lines. You want to know when something important happens (checkpoint saved, error, completion) without watching the terminal. PAW lets you compile a classifier that runs locally and alerts on the lines that matter.
|
|
4
|
+
|
|
5
|
+
**Full tool:** [examples/paw_monitor.py](https://github.com/programasweights/programasweights-python/blob/main/examples/paw_monitor.py) on GitHub.
|
|
6
|
+
|
|
7
|
+
## How we built it
|
|
8
|
+
|
|
9
|
+
### Attempt 1: Keyword matching
|
|
10
|
+
|
|
11
|
+
The obvious first approach: grep for "error", "fail", "complete".
|
|
12
|
+
|
|
13
|
+
**Result:** Too noisy. "error" matches "error_count=0" (routine metric). "complete" matches "batch complete" (every 10 seconds). Missing important lines like "Traceback" or "CUDA out of memory" that don't contain the keywords.
|
|
14
|
+
|
|
15
|
+
**Lesson:** The whole reason to use PAW is that keyword matching can't express "important enough to interrupt me."
|
|
16
|
+
|
|
17
|
+
### Attempt 2: One-shot description
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Classify if this log line is important enough to alert on. Return ALERT or QUIET.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Result:** Too vague. The model alerted on everything that looked unusual, including routine debug output. "Important" means different things in different contexts.
|
|
24
|
+
|
|
25
|
+
**Lesson:** Don't rely on abstract instructions. The model needs concrete examples of what ALERT and QUIET look like in YOUR specific logs.
|
|
26
|
+
|
|
27
|
+
### Attempt 3: Example-based spec
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
Classify log lines. Return ONLY one word: ALERT or QUIET.
|
|
31
|
+
|
|
32
|
+
Input: [step 100] loss=0.05 lr=0.0001
|
|
33
|
+
Output: QUIET
|
|
34
|
+
|
|
35
|
+
Input: [Checkpoint] Saved model at step 1000
|
|
36
|
+
Output: ALERT
|
|
37
|
+
|
|
38
|
+
Input: Traceback (most recent call last):
|
|
39
|
+
Output: ALERT
|
|
40
|
+
|
|
41
|
+
Input: Training complete. Final loss: 0.11
|
|
42
|
+
Output: ALERT
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Result:** This worked. The model learned the boundary between routine metrics (QUIET) and significant events (ALERT) from the examples. Adding 3-5 examples from actual logs was the key.
|
|
46
|
+
|
|
47
|
+
**Lesson:** Include examples from your actual data. 3-5 representative examples consistently outperform prose-only descriptions.
|
|
48
|
+
|
|
49
|
+
### Refinement: Developer feedback
|
|
50
|
+
|
|
51
|
+
A developer using PAW for monitoring diffusion model training shared their experience. Key insights:
|
|
52
|
+
|
|
53
|
+
- **Positive instructions work better than negative ones.** "Output ALERT only if X, Y, or Z" is more reliable than "Don't alert on routine output."
|
|
54
|
+
- **The spec should match the monitoring context.** Different training phases produce different log patterns. Examples should cover the specific domain.
|
|
55
|
+
- **Stall detection needs a separate mechanism.** PAW classifies what it sees -- it can't detect the absence of output. The monitoring tool needs a timer for "no new output in N minutes."
|
|
56
|
+
|
|
57
|
+
## The solution
|
|
58
|
+
|
|
59
|
+
### The spec
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
program = paw.compile("""
|
|
63
|
+
Classify log lines. Return ONLY one word: ALERT or QUIET.
|
|
64
|
+
|
|
65
|
+
Input: [step 100] loss=0.05 lr=0.0001
|
|
66
|
+
Output: QUIET
|
|
67
|
+
|
|
68
|
+
Input: [Checkpoint] Saved model at step 1000
|
|
69
|
+
Output: ALERT
|
|
70
|
+
|
|
71
|
+
Input: Traceback (most recent call last):
|
|
72
|
+
Output: ALERT
|
|
73
|
+
|
|
74
|
+
Input: Training complete. Final loss: 0.11
|
|
75
|
+
Output: ALERT
|
|
76
|
+
""")
|
|
77
|
+
|
|
78
|
+
fn = paw.function(program.id)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Compile once, save the program ID, reuse forever. The function runs locally with no internet after the first download.
|
|
82
|
+
|
|
83
|
+
### The monitoring loop
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import time
|
|
87
|
+
|
|
88
|
+
fn = paw.function("your-program-id")
|
|
89
|
+
|
|
90
|
+
last_size = 0
|
|
91
|
+
while True:
|
|
92
|
+
size = os.path.getsize(log_file)
|
|
93
|
+
if size > last_size:
|
|
94
|
+
with open(log_file) as f:
|
|
95
|
+
f.seek(last_size)
|
|
96
|
+
new_text = f.read()
|
|
97
|
+
last_size = size
|
|
98
|
+
|
|
99
|
+
# Truncate to last ~1000 chars to fit context window
|
|
100
|
+
chunk = new_text[-1000:] if len(new_text) > 1000 else new_text
|
|
101
|
+
result = fn(chunk)
|
|
102
|
+
if result.strip() == "ALERT":
|
|
103
|
+
send_notification(chunk)
|
|
104
|
+
|
|
105
|
+
time.sleep(10)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Full tool
|
|
109
|
+
|
|
110
|
+
The complete [paw_monitor.py](https://github.com/programasweights/programasweights-python/blob/main/examples/paw_monitor.py) adds:
|
|
111
|
+
|
|
112
|
+
- **File watching** with `seek()` to only process new content
|
|
113
|
+
- **Input truncation** to fit the ~2048 token context window
|
|
114
|
+
- **Stall detection** -- alerts if no new output for a configurable timeout
|
|
115
|
+
- **`--focus` and `--ignore`** flags to guide what the classifier pays attention to
|
|
116
|
+
- **`--local` flag** to run entirely offline after first compile
|
|
117
|
+
- **`--json` output** for integration with other tools
|
|
118
|
+
|
|
119
|
+
## Adapting this for your use case
|
|
120
|
+
|
|
121
|
+
1. **Collect 5-10 example log lines** from your actual process -- a mix of routine output and important events
|
|
122
|
+
2. **Write a spec** with `Input: ... Output: ALERT/QUIET` pairs
|
|
123
|
+
3. **Test with real logs** -- pipe a log file through the classifier and check which lines it flags
|
|
124
|
+
4. **Iterate** -- if it alerts too much, add more QUIET examples. If it misses things, add ALERT examples.
|
|
125
|
+
5. **Save the program ID** -- compile once, monitor forever
|
|
126
|
+
|
|
127
|
+
## Takeaways
|
|
128
|
+
|
|
129
|
+
- **Examples beat descriptions** for teaching the model your specific log patterns.
|
|
130
|
+
- **Compile once, run forever.** The compiled function is cached locally and needs no internet.
|
|
131
|
+
- **PAW classifies what it sees** -- combine with a timer for stall detection.
|
|
132
|
+
- **Iterate with real data.** The first spec is rarely perfect. Test, check failures, adjust examples.
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# Tool Calling with a 10-Function Pipeline
|
|
2
|
+
|
|
3
|
+
Large language models are increasingly expected to call tools — pick the right API, extract parameters, chain multi-step flows, and know when NOT to act. This normally requires large models (70B+). We used PAW to build a tool-calling system from 10 tiny compiled functions running on a 0.6B interpreter, and tested it on the [ToolCall-15](https://github.com/stevibe/ToolCall-15) benchmark.
|
|
4
|
+
|
|
5
|
+
The entire pipeline was built in a single Cursor session by an AI coding agent (Claude Opus 4.6) that read PAW's [AGENTS.md](https://programasweights.com/AGENTS.md) and the benchmark source.
|
|
6
|
+
|
|
7
|
+
**Result:** 93% (28/30 points) on ToolCall-15, with 14 out of 15 scenarios passing.
|
|
8
|
+
|
|
9
|
+
!!! warning "Benchmark leakage"
|
|
10
|
+
The AI agent that built this pipeline had access to the benchmark source code, including the 15 scenario definitions and scoring logic. This means the specs and heuristics were likely tailored to these specific test cases. We report 93% as a proof of concept for the architecture, not a generalizable accuracy claim. A principled evaluation would test on held-out scenarios the builder has never seen.
|
|
11
|
+
|
|
12
|
+
## The benchmark: ToolCall-15
|
|
13
|
+
|
|
14
|
+
[ToolCall-15](https://github.com/stevibe/ToolCall-15) (by [stevibe](https://x.com/stevibe)) is a visual benchmark for LLM tool use. It runs 15 hand-picked scenarios through an OpenAI-compatible chat completions interface, scores each deterministically, and renders results in a live dashboard.
|
|
15
|
+
|
|
16
|
+
**12 available tools** (given to every scenario):
|
|
17
|
+
|
|
18
|
+
`web_search`, `get_weather`, `calculator`, `send_email`, `search_files`, `read_file`, `create_calendar_event`, `get_contacts`, `translate_text`, `get_stock_price`, `set_reminder`, `run_code`
|
|
19
|
+
|
|
20
|
+
**15 scenarios across 5 categories** (3 each, max 6 points per category):
|
|
21
|
+
|
|
22
|
+
| Category | Scenarios | What it tests |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| A: Tool Selection | TC-01 Weather in Berlin, TC-02 AAPL stock price, TC-03 "Let Sarah know..." | Pick the right tool, resist distractors, infer implicit chains |
|
|
25
|
+
| B: Parameter Precision | TC-04 Tokyo in Fahrenheit, TC-05 "Next Monday 9:30am", TC-06 Translate to both Spanish & Japanese | Pass correct arguments, parse dates, handle multi-value |
|
|
26
|
+
| C: Multi-Step Chains | TC-07 Find report → read → email total, TC-08 Check weather → if raining → set reminder, TC-09 Weather AND stock price | Thread data across steps, branch conditionally, run in parallel |
|
|
27
|
+
| D: Restraint & Refusal | TC-10 "When did WWII end?", TC-11 "15% of 200", TC-12 "Delete my emails" | Don't use tools for trivial knowledge, refuse impossible requests |
|
|
28
|
+
| E: Error Recovery | TC-13 Empty search results → retry, TC-14 Stock API rate limited, TC-15 Search population → calculate 2% | Handle errors gracefully, maintain data integrity |
|
|
29
|
+
|
|
30
|
+
Scoring: pass = 2 points, partial = 1, fail = 0. Final score = average of 5 category percentages.
|
|
31
|
+
|
|
32
|
+
## The approach: decompose into specialists
|
|
33
|
+
|
|
34
|
+
The key insight is the same one from our [site navigation](site-navigation.md) case study: **split the task into small, focused functions.** A single monolithic prompt trying to handle tool selection, parameter extraction, multi-step reasoning, and restraint all at once would fail. Instead, each PAW function handles one decision.
|
|
35
|
+
|
|
36
|
+
The 10 compiled functions divide into three groups:
|
|
37
|
+
|
|
38
|
+
**Routing (which tool?):**
|
|
39
|
+
|
|
40
|
+
| Function | Spec (abbreviated) | Output |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `tc15-needs-tool` | "Does this request need an external tool, or can it be answered directly?" | YES / NO |
|
|
43
|
+
| `tc15-tool-router` | "Which tool should be called?" with 10 examples covering all 12 tools + NONE | One of 13 labels |
|
|
44
|
+
| `tc15-impossible-check` | "Is this request something no available tool can fulfill?" | IMPOSSIBLE / POSSIBLE |
|
|
45
|
+
| `tc15-second-tool` | "After the first tool, is a second tool needed?" | Tool name or NONE |
|
|
46
|
+
|
|
47
|
+
**Extraction (what parameters?):**
|
|
48
|
+
|
|
49
|
+
| Function | Spec (abbreviated) | Output |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `tc15-extract-location` | "Extract the city name from a weather request" | City name |
|
|
52
|
+
| `tc15-extract-ticker` | "Extract the stock ticker symbol" | Ticker (e.g. AAPL) |
|
|
53
|
+
| `tc15-extract-units` | "Extract temperature units, default celsius" | fahrenheit / celsius |
|
|
54
|
+
| `tc15-extract-search-query` | "Extract the file search query" | Search string |
|
|
55
|
+
| `tc15-extract-person` | "Extract the person's name" | Name |
|
|
56
|
+
| `tc15-extract-translate` | "Extract source_lang, target_lang, text" | Pipe-separated triple |
|
|
57
|
+
|
|
58
|
+
**Not compiled (handled by code):**
|
|
59
|
+
|
|
60
|
+
Some tasks are better handled by regular code than by a neural function:
|
|
61
|
+
|
|
62
|
+
- **Date/time parsing** (regex): "next Monday at 9:30am" → `2026-03-23T09:30`
|
|
63
|
+
- **Direct answers** (lookup table): "15% of 200" → `30`
|
|
64
|
+
- **Response synthesis** (template): format tool results into natural language
|
|
65
|
+
- **Protocol formatting**: build OpenAI-compatible `tool_calls` JSON
|
|
66
|
+
|
|
67
|
+
This is a general design principle: **use PAW for fuzzy judgment, use code for structured logic.**
|
|
68
|
+
|
|
69
|
+
## The full specs
|
|
70
|
+
|
|
71
|
+
Here are the actual specs the agent compiled. Each is a standalone PAW function.
|
|
72
|
+
|
|
73
|
+
### tc15-tool-router
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
Classify which tool should be called for a user request.
|
|
77
|
+
Output EXACTLY one of these tool names:
|
|
78
|
+
web_search, get_weather, calculator, send_email, search_files,
|
|
79
|
+
read_file, create_calendar_event, get_contacts, translate_text,
|
|
80
|
+
get_stock_price, set_reminder, run_code, NONE.
|
|
81
|
+
Output NONE if the user's question can be answered from general
|
|
82
|
+
knowledge without any tool.
|
|
83
|
+
|
|
84
|
+
Input: What's the weather in Berlin?
|
|
85
|
+
Output: get_weather
|
|
86
|
+
|
|
87
|
+
Input: What is the price of AAPL stock?
|
|
88
|
+
Output: get_stock_price
|
|
89
|
+
|
|
90
|
+
Input: What year did WWII end?
|
|
91
|
+
Output: NONE
|
|
92
|
+
|
|
93
|
+
Input: What is 15% of 200?
|
|
94
|
+
Output: NONE
|
|
95
|
+
|
|
96
|
+
Input: Delete all my emails
|
|
97
|
+
Output: NONE
|
|
98
|
+
|
|
99
|
+
Input: Translate hello to Spanish
|
|
100
|
+
Output: translate_text
|
|
101
|
+
|
|
102
|
+
Input: Find the Q3 budget report
|
|
103
|
+
Output: search_files
|
|
104
|
+
|
|
105
|
+
Input: Send an email to Sarah
|
|
106
|
+
Output: get_contacts
|
|
107
|
+
|
|
108
|
+
Input: Schedule a meeting for Monday
|
|
109
|
+
Output: create_calendar_event
|
|
110
|
+
|
|
111
|
+
Input: Remind me to bring an umbrella
|
|
112
|
+
Output: set_reminder
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### tc15-needs-tool
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
Determine if a user request needs an external tool or can be
|
|
119
|
+
answered directly. Output YES if the request needs real-time data,
|
|
120
|
+
file access, sending messages, scheduling, or looking up current
|
|
121
|
+
information. Output NO if it's basic knowledge, simple math, or
|
|
122
|
+
an impossible request with no available tool.
|
|
123
|
+
|
|
124
|
+
Input: What's the weather?
|
|
125
|
+
Output: YES
|
|
126
|
+
|
|
127
|
+
Input: What year did WWII end?
|
|
128
|
+
Output: NO
|
|
129
|
+
|
|
130
|
+
Input: What is 15% of 200?
|
|
131
|
+
Output: NO
|
|
132
|
+
|
|
133
|
+
Input: Delete all my emails
|
|
134
|
+
Output: NO
|
|
135
|
+
|
|
136
|
+
Input: What's AAPL stock price?
|
|
137
|
+
Output: YES
|
|
138
|
+
|
|
139
|
+
Input: Find the budget report
|
|
140
|
+
Output: YES
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### tc15-extract-location
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
Extract the location/city name from a weather or location request.
|
|
147
|
+
Output just the city or location name, nothing else.
|
|
148
|
+
|
|
149
|
+
Input: What's the weather in Berlin?
|
|
150
|
+
Output: Berlin
|
|
151
|
+
|
|
152
|
+
Input: Temperature in Tokyo in Fahrenheit
|
|
153
|
+
Output: Tokyo
|
|
154
|
+
|
|
155
|
+
Input: How's the weather in Paris right now?
|
|
156
|
+
Output: Paris
|
|
157
|
+
|
|
158
|
+
Input: London weather forecast
|
|
159
|
+
Output: London
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### tc15-extract-ticker
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
Extract the stock ticker symbol from a stock price request.
|
|
166
|
+
Output just the uppercase ticker symbol.
|
|
167
|
+
|
|
168
|
+
Input: What is the price of AAPL stock?
|
|
169
|
+
Output: AAPL
|
|
170
|
+
|
|
171
|
+
Input: MSFT stock price
|
|
172
|
+
Output: MSFT
|
|
173
|
+
|
|
174
|
+
Input: How much is Apple stock?
|
|
175
|
+
Output: AAPL
|
|
176
|
+
|
|
177
|
+
Input: Microsoft stock price?
|
|
178
|
+
Output: MSFT
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### tc15-extract-units
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
Extract temperature units from a weather request.
|
|
185
|
+
Output 'fahrenheit' if Fahrenheit is mentioned, otherwise
|
|
186
|
+
output 'celsius'.
|
|
187
|
+
|
|
188
|
+
Input: Temperature in Tokyo in Fahrenheit
|
|
189
|
+
Output: fahrenheit
|
|
190
|
+
|
|
191
|
+
Input: Weather in Berlin
|
|
192
|
+
Output: celsius
|
|
193
|
+
|
|
194
|
+
Input: Paris weather in F
|
|
195
|
+
Output: fahrenheit
|
|
196
|
+
|
|
197
|
+
Input: London temperature celsius
|
|
198
|
+
Output: celsius
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### tc15-extract-search-query
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
Extract the file search query from a user request about finding
|
|
205
|
+
documents or files. Output a concise search query string.
|
|
206
|
+
|
|
207
|
+
Input: Find the Q3 budget report
|
|
208
|
+
Output: Q3 budget report
|
|
209
|
+
|
|
210
|
+
Input: Find the Johnson proposal document
|
|
211
|
+
Output: Johnson proposal
|
|
212
|
+
|
|
213
|
+
Input: Search for the annual review
|
|
214
|
+
Output: annual review
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### tc15-extract-person
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
Extract the person's name from a request about contacting or
|
|
221
|
+
looking up someone. Output just the person's name.
|
|
222
|
+
|
|
223
|
+
Input: I need to let Sarah know about the meeting
|
|
224
|
+
Output: Sarah
|
|
225
|
+
|
|
226
|
+
Input: Send an email to John
|
|
227
|
+
Output: John
|
|
228
|
+
|
|
229
|
+
Input: Contact my manager
|
|
230
|
+
Output: manager
|
|
231
|
+
|
|
232
|
+
Input: Email the report to Jamie
|
|
233
|
+
Output: Jamie
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### tc15-extract-translate
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
Extract translation parameters from a translate request.
|
|
240
|
+
Output in the format: source_lang|target_lang|text.
|
|
241
|
+
If multiple target languages, output one line per target language.
|
|
242
|
+
|
|
243
|
+
Input: Translate hello to Spanish
|
|
244
|
+
Output: English|Spanish|hello
|
|
245
|
+
|
|
246
|
+
Input: Translate Where is the hospital from English to Japanese
|
|
247
|
+
Output: English|Japanese|Where is the hospital
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### tc15-impossible-check
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
Determine if a request asks for something that cannot be done
|
|
254
|
+
with standard tools (web search, weather, calculator, email,
|
|
255
|
+
file search/read, calendar, contacts, translate, stocks,
|
|
256
|
+
reminders, code execution).
|
|
257
|
+
Output IMPOSSIBLE if no tool can fulfill it (like deleting
|
|
258
|
+
emails, modifying files, accessing databases, etc).
|
|
259
|
+
Output POSSIBLE otherwise.
|
|
260
|
+
|
|
261
|
+
Input: Delete all my emails from last month
|
|
262
|
+
Output: IMPOSSIBLE
|
|
263
|
+
|
|
264
|
+
Input: What's the weather?
|
|
265
|
+
Output: POSSIBLE
|
|
266
|
+
|
|
267
|
+
Input: Hack into the server
|
|
268
|
+
Output: IMPOSSIBLE
|
|
269
|
+
|
|
270
|
+
Input: Send an email
|
|
271
|
+
Output: POSSIBLE
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### tc15-second-tool
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
Given a user request that may need multiple tools, identify if
|
|
278
|
+
a SECOND tool is needed after the first. Output the second tool
|
|
279
|
+
name or NONE.
|
|
280
|
+
|
|
281
|
+
Input: Weather in London and MSFT stock price
|
|
282
|
+
Output: get_stock_price
|
|
283
|
+
|
|
284
|
+
Input: Find the report and email it to my manager
|
|
285
|
+
Output: read_file
|
|
286
|
+
|
|
287
|
+
Input: Check weather, if raining remind me about umbrella
|
|
288
|
+
Output: set_reminder
|
|
289
|
+
|
|
290
|
+
Input: What's the weather in Berlin?
|
|
291
|
+
Output: NONE
|
|
292
|
+
|
|
293
|
+
Input: Let Sarah know the meeting moved to 3pm
|
|
294
|
+
Output: send_email
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Pipeline architecture
|
|
298
|
+
|
|
299
|
+
The 10 PAW functions are orchestrated by a FastAPI proxy server that speaks the OpenAI `/v1/chat/completions` protocol. Here's the flow:
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
User message arrives at proxy
|
|
303
|
+
│
|
|
304
|
+
├─ detect_parallel_tools (regex)
|
|
305
|
+
│ "weather AND stock price" → call both tools in parallel
|
|
306
|
+
│ └─ if match: extract params for each → return tool_calls → done
|
|
307
|
+
│
|
|
308
|
+
├─ tc15-needs-tool → "Does this need a tool?"
|
|
309
|
+
│ └─ NO → tc15-impossible-check → explain refusal or answer directly
|
|
310
|
+
│
|
|
311
|
+
├─ tc15-tool-router → "Which tool?"
|
|
312
|
+
│ └─ NONE → answer from knowledge (lookup table or simple math)
|
|
313
|
+
│
|
|
314
|
+
├─ extract_params_for_tool(tool_name):
|
|
315
|
+
│ ├─ get_weather → tc15-extract-location + tc15-extract-units
|
|
316
|
+
│ ├─ get_stock_price → tc15-extract-ticker
|
|
317
|
+
│ ├─ search_files → tc15-extract-search-query
|
|
318
|
+
│ ├─ get_contacts → tc15-extract-person
|
|
319
|
+
│ ├─ translate_text → tc15-extract-translate (may produce 2 calls)
|
|
320
|
+
│ ├─ calendar_event → regex date/time parsing
|
|
321
|
+
│ └─ set_reminder → regex date/time parsing
|
|
322
|
+
│
|
|
323
|
+
└─ Return tool_calls in OpenAI JSON format
|
|
324
|
+
|
|
325
|
+
──── On follow-up turns (tool results come back) ────
|
|
326
|
+
|
|
327
|
+
├─ decide_follow_up_tool:
|
|
328
|
+
│ ├─ Error result? → explain to user
|
|
329
|
+
│ ├─ Empty results? → retry with broader query
|
|
330
|
+
│ ├─ tc15-second-tool → "Is another tool needed?"
|
|
331
|
+
│ │ └─ Chain logic: search → read → contacts → send_email
|
|
332
|
+
│ └─ No more tools → synthesize_response from all results
|
|
333
|
+
│
|
|
334
|
+
└─ Return either more tool_calls or final text response
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
The proxy handles multi-turn conversations by tracking which tools have already been called and threading data between steps (e.g., using a `file_id` from a search result in a subsequent `read_file` call).
|
|
338
|
+
|
|
339
|
+
## How it was built
|
|
340
|
+
|
|
341
|
+
A single prompt to Cursor (Claude Opus 4.6):
|
|
342
|
+
|
|
343
|
+
> "Build a PAW-powered proxy for the ToolCall-15 benchmark. Read AGENTS.md for PAW usage. Analyze the benchmark scenarios and create specialized PAW functions for tool routing and parameter extraction. Wire them into an OpenAI-compatible endpoint."
|
|
344
|
+
|
|
345
|
+
The agent:
|
|
346
|
+
|
|
347
|
+
1. Read the benchmark methodology and all 15 scenario definitions
|
|
348
|
+
2. Identified which decisions need fuzzy judgment (PAW) vs. structured logic (code)
|
|
349
|
+
3. Designed and compiled 10 PAW functions with specs tailored to the scenarios
|
|
350
|
+
4. Built a FastAPI proxy server (~700 lines) with conversation state management
|
|
351
|
+
5. Ran the benchmark, debugged failures, and iterated
|
|
352
|
+
|
|
353
|
+
### Iteration 1: 70% (21/30)
|
|
354
|
+
|
|
355
|
+
The first run had several failures:
|
|
356
|
+
|
|
357
|
+
- **TC-01, TC-02 (Tool Selection): Infinite loop.** After calling the correct tool and receiving results, the proxy called the same tool again instead of synthesizing a response. Root cause: the conversation state handler didn't properly detect "I already have results, stop calling tools."
|
|
358
|
+
- **TC-08 (Conditional Branching): Wrong first tool.** The router sent "check weather, if raining set reminder" to `create_calendar_event` instead of `get_weather`. The tool-router spec didn't have an example for conditional chains.
|
|
359
|
+
- **TC-15 (Data Integrity): Wrong second tool.** After `web_search` returned Iceland's population, the proxy called `get_stock_price("Iceland")` instead of `calculator`. The `tc15-second-tool` function didn't have enough examples for search-then-calculate chains.
|
|
360
|
+
- **TC-07 (Multi-Step Chain): Partial.** The 4-step chain (search → read → contacts → email) partially worked but didn't thread data correctly between all steps.
|
|
361
|
+
|
|
362
|
+
### Iteration 2: fix the orchestration
|
|
363
|
+
|
|
364
|
+
The agent fixed the proxy code (not the PAW specs):
|
|
365
|
+
|
|
366
|
+
- Added proper conversation state tracking to detect when tool results are present
|
|
367
|
+
- Added routing overrides for common patterns (conditional weather checks, search-then-calculate)
|
|
368
|
+
- Fixed data threading in multi-step chains (extracting `file_id` from search results, email from contacts)
|
|
369
|
+
- Recompiled 2 functions that had HuggingFace download failures
|
|
370
|
+
|
|
371
|
+
### Final result: 93% (28/30)
|
|
372
|
+
|
|
373
|
+
| Category | Score | Details |
|
|
374
|
+
|---|---|---|
|
|
375
|
+
| A: Tool Selection | 100% (6/6) | TC-01 pass, TC-02 pass, TC-03 pass |
|
|
376
|
+
| B: Parameter Precision | 100% (6/6) | TC-04 pass, TC-05 pass, TC-06 pass |
|
|
377
|
+
| C: Multi-Step Chains | 100% (6/6) | TC-07 pass, TC-08 pass, TC-09 pass |
|
|
378
|
+
| D: Restraint & Refusal | 100% (6/6) | TC-10 pass, TC-11 pass, TC-12 pass |
|
|
379
|
+
| E: Error Recovery | 67% (4/6) | TC-13 **fail**, TC-14 pass, TC-15 pass |
|
|
380
|
+
|
|
381
|
+
**14 out of 15 scenarios passed.** The single failure:
|
|
382
|
+
|
|
383
|
+
### Why TC-13 failed
|
|
384
|
+
|
|
385
|
+
**TC-13 (Empty Results Retry):** The user asks "Find the Johnson proposal document." The proxy correctly calls `search_files(query="Johnson proposal")`, gets empty results, but instead of retrying with a broader query like `"Johnson"`, it immediately responds "No results were found."
|
|
386
|
+
|
|
387
|
+
**Root cause:** The proxy has loop-prevention logic that blocks re-calling a tool already in `tools_called`. This prevents infinite loops (which was a real problem in iteration 1) but also prevents the legitimate retry pattern where you call the same tool with different parameters. The agent's fix for the infinite loop was too aggressive — it stopped all same-tool retries.
|
|
388
|
+
|
|
389
|
+
This is an **architectural limitation**, not a PAW function limitation. The `tc15-extract-search-query` function works fine. The issue is in the orchestration code's retry policy.
|
|
390
|
+
|
|
391
|
+
## Contamination disclosure
|
|
392
|
+
|
|
393
|
+
The AI agent that built this pipeline had full access to:
|
|
394
|
+
|
|
395
|
+
- The 15 scenario definitions (user messages, expected tools, scoring logic)
|
|
396
|
+
- The mocked tool responses
|
|
397
|
+
- The benchmark source code
|
|
398
|
+
|
|
399
|
+
This means:
|
|
400
|
+
|
|
401
|
+
1. The specs were tuned for these 15 specific scenarios (e.g., the tool-router examples map closely to the actual test inputs)
|
|
402
|
+
2. The routing overrides in the proxy code target specific scenario patterns
|
|
403
|
+
3. The direct-answer lookup table contains exact answers for TC-10 and TC-11
|
|
404
|
+
|
|
405
|
+
**What this proves:** The architecture works — 10 small PAW specialists + Python glue can implement a tool-calling system that scores 93% on a multi-category benchmark.
|
|
406
|
+
|
|
407
|
+
**What this does NOT prove:** That these exact specs would generalize to unseen tool-calling scenarios. A principled evaluation would compile specs without seeing the test cases, then evaluate on held-out scenarios.
|
|
408
|
+
|
|
409
|
+
## Full code
|
|
410
|
+
|
|
411
|
+
### compile_functions.py
|
|
412
|
+
|
|
413
|
+
The compilation script that creates all 10 functions:
|
|
414
|
+
|
|
415
|
+
```python
|
|
416
|
+
import json, sys
|
|
417
|
+
import programasweights as paw
|
|
418
|
+
|
|
419
|
+
COMPILER = "paw-4b-qwen3-0.6b"
|
|
420
|
+
|
|
421
|
+
FUNCTIONS_TO_COMPILE = [
|
|
422
|
+
{"slug": "tc15-tool-router", "spec": "Classify which tool..."},
|
|
423
|
+
{"slug": "tc15-needs-tool", "spec": "Determine if request needs tool..."},
|
|
424
|
+
{"slug": "tc15-extract-location", "spec": "Extract city name..."},
|
|
425
|
+
{"slug": "tc15-extract-ticker", "spec": "Extract stock ticker..."},
|
|
426
|
+
{"slug": "tc15-extract-units", "spec": "Extract temperature units..."},
|
|
427
|
+
{"slug": "tc15-extract-search-query","spec": "Extract file search query..."},
|
|
428
|
+
{"slug": "tc15-extract-person", "spec": "Extract person's name..."},
|
|
429
|
+
{"slug": "tc15-extract-translate", "spec": "Extract translation parameters..."},
|
|
430
|
+
{"slug": "tc15-impossible-check", "spec": "Is this request impossible?..."},
|
|
431
|
+
{"slug": "tc15-second-tool", "spec": "Is a second tool needed?..."},
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
for fn_def in FUNCTIONS_TO_COMPILE:
|
|
435
|
+
program = paw.compile(fn_def["spec"], compiler=COMPILER)
|
|
436
|
+
print(f"{fn_def['slug']}: {program.id}")
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### server.py (key routing logic)
|
|
440
|
+
|
|
441
|
+
The proxy server's main decision flow:
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
@app.post("/v1/chat/completions")
|
|
445
|
+
async def chat_completions(request: ChatRequest):
|
|
446
|
+
messages = [m.model_dump() for m in request.messages]
|
|
447
|
+
user_msg = ""
|
|
448
|
+
for msg in messages:
|
|
449
|
+
if msg.get("role") == "user":
|
|
450
|
+
user_msg = msg.get("content", "")
|
|
451
|
+
|
|
452
|
+
# If we already have tool results, decide: more tools or synthesize?
|
|
453
|
+
if any(m.get("role") == "tool" for m in messages):
|
|
454
|
+
follow_up = decide_follow_up_tool(user_msg, messages)
|
|
455
|
+
if follow_up:
|
|
456
|
+
return make_response(tool_calls=follow_up)
|
|
457
|
+
return make_response(content=synthesize_response(user_msg, messages))
|
|
458
|
+
|
|
459
|
+
# First turn: should we use tools at all?
|
|
460
|
+
tool_calls = build_tool_calls_for_user(user_msg, messages)
|
|
461
|
+
if tool_calls:
|
|
462
|
+
return make_response(tool_calls=tool_calls)
|
|
463
|
+
|
|
464
|
+
# No tools needed — answer directly
|
|
465
|
+
return make_response(content=generate_direct_answer(user_msg))
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
The full source code is available in the [paw-proxy directory](https://github.com/stevibe/ToolCall-15/tree/main/paw-proxy) of the ToolCall-15 repository.
|
|
469
|
+
|
|
470
|
+
## Takeaways
|
|
471
|
+
|
|
472
|
+
- **Decomposition works.** 10 small specialists beat one monolithic prompt. Each function handles one decision — "which tool?", "what city?", "is this impossible?" — and does it well.
|
|
473
|
+
- **PAW + code hybrid.** Fuzzy judgment (tool routing, entity extraction, intent classification) goes to PAW. Structured logic (date parsing, JSON formatting, conversation state) stays in Python. Neither alone would work.
|
|
474
|
+
- **An AI agent can build the whole thing.** From analyzing the benchmark to compiling functions to writing the proxy server — one Cursor session, one prompt.
|
|
475
|
+
- **Loop prevention vs. retry is hard.** The single failure (TC-13) came from the tension between preventing infinite tool loops and allowing legitimate retries with different parameters. This is an orchestration problem, not a model problem.
|
|
476
|
+
- **0.6B is enough for focused tasks.** Tool routing across 12 options, entity extraction, yes/no classification — none of these need a large model when the spec is precise and examples are clear.
|
|
477
|
+
- **Always evaluate on unseen data.** Our 93% includes benchmark leakage. The architecture is sound, but the specific accuracy is optimistic.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Writing Good Specs
|
|
2
|
+
|
|
3
|
+
The canonical spec-writing guide lives in [AGENTS.md](https://programasweights.com/agents) — the same document that AI coding assistants read when integrating PAW.
|
|
4
|
+
|
|
5
|
+
See the **"Writing Good Specs"** section there for:
|
|
6
|
+
|
|
7
|
+
- The #1 practice: iterate with test cases
|
|
8
|
+
- Spec-tuning tips (role naming, output constraints, positive vs negative instructions, field ordering, and more)
|
|
9
|
+
- Example specs with `Input: ... Output: ...` format
|
|
10
|
+
|
|
11
|
+
You can also copy the full AGENTS.md from the [AI Agents page](https://programasweights.com/agents) and save it as `AGENTS.md` in your project root — AI assistants will read it automatically.
|
|
@@ -52,8 +52,10 @@ nav:
|
|
|
52
52
|
- Publishing Programs: hub/publishing-programs.md
|
|
53
53
|
- Feedback & Cases: hub/feedback-cases.md
|
|
54
54
|
- Case Studies:
|
|
55
|
+
- Log Monitoring: case-studies/log-monitoring.md
|
|
55
56
|
- Site Navigation: case-studies/site-navigation.md
|
|
56
57
|
- Semantic Search: case-studies/semantic-search.md
|
|
58
|
+
- Tool Calling: case-studies/tool-calling.md
|
|
57
59
|
- Advanced:
|
|
58
60
|
- Architecture: advanced/architecture.md
|
|
59
61
|
- Architecture Decision Records: advanced/adrs.md
|
|
@@ -27,7 +27,7 @@ try:
|
|
|
27
27
|
from importlib.metadata import version as _meta_version
|
|
28
28
|
__version__ = _meta_version("programasweights")
|
|
29
29
|
except Exception:
|
|
30
|
-
__version__ = "0.2.
|
|
30
|
+
__version__ = "0.2.8"
|
|
31
31
|
|
|
32
32
|
from .config import get_api_url, get_api_key, set_api_key
|
|
33
33
|
|
|
@@ -68,8 +68,15 @@ def compile(
|
|
|
68
68
|
'{"name": "Alice"}'
|
|
69
69
|
"""
|
|
70
70
|
from .client import PAWClient
|
|
71
|
+
from ._output import status
|
|
72
|
+
status("Compiling...")
|
|
71
73
|
client = PAWClient(api_url=get_api_url(), api_key=get_api_key())
|
|
72
|
-
|
|
74
|
+
result = client.compile(spec, compiler=compiler, name=name, tags=tags, public=public, slug=slug)
|
|
75
|
+
label = f"{result.id}"
|
|
76
|
+
if result.slug:
|
|
77
|
+
label += f" ({result.slug})"
|
|
78
|
+
status(f"Compiled: {label}")
|
|
79
|
+
return result
|
|
73
80
|
|
|
74
81
|
|
|
75
82
|
def function(
|
|
@@ -111,12 +118,15 @@ def function(
|
|
|
111
118
|
if n_gpu_layers is None:
|
|
112
119
|
n_gpu_layers = int(os.environ.get("PAW_GPU_LAYERS", "0"))
|
|
113
120
|
|
|
121
|
+
from ._output import status
|
|
122
|
+
|
|
114
123
|
resolved_id = program_id
|
|
115
124
|
if not re.fullmatch(r"[a-f0-9]{16,64}", program_id):
|
|
116
125
|
cached = get_cached_slug(program_id)
|
|
117
126
|
if cached:
|
|
118
127
|
resolved_id = cached
|
|
119
128
|
else:
|
|
129
|
+
status(f"Resolving {program_id}...")
|
|
120
130
|
from .client import PAWClient
|
|
121
131
|
client = PAWClient(api_url=get_api_url(), api_key=get_api_key())
|
|
122
132
|
resolved_id = client.resolve_slug(program_id)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""User-facing status messages for the PAW SDK.
|
|
2
|
+
|
|
3
|
+
All messages go to stderr so they don't pollute stdout when users pipe output.
|
|
4
|
+
Set PAW_QUIET=1 to suppress all status messages.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _quiet() -> bool:
|
|
13
|
+
return os.environ.get("PAW_QUIET", "").strip() in ("1", "true", "yes")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def status(msg: str, **kwargs) -> None:
|
|
17
|
+
"""Print a status message to stderr."""
|
|
18
|
+
if _quiet():
|
|
19
|
+
return
|
|
20
|
+
print(msg, file=sys.stderr, flush=True, **kwargs)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def status_inline(msg: str) -> None:
|
|
24
|
+
"""Print a status message inline (no newline) for progress updates."""
|
|
25
|
+
if _quiet():
|
|
26
|
+
return
|
|
27
|
+
print(f"\r{msg}", end="", file=sys.stderr, flush=True)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def status_end() -> None:
|
|
31
|
+
"""End an inline status line."""
|
|
32
|
+
if _quiet():
|
|
33
|
+
return
|
|
34
|
+
print(file=sys.stderr, flush=True)
|
|
@@ -54,9 +54,10 @@ def get_base_model_path(interpreter: str = "Qwen/Qwen3-0.6B") -> Path:
|
|
|
54
54
|
f"Available: {list(INTERPRETER_TO_GGUF.keys())}"
|
|
55
55
|
)
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
from ._output import status
|
|
58
|
+
status(f"Downloading interpreter {gguf_name} (one-time download)...")
|
|
58
59
|
_download_file(url, gguf_path)
|
|
59
|
-
|
|
60
|
+
status(f"Saved to {gguf_path}")
|
|
60
61
|
return gguf_path
|
|
61
62
|
|
|
62
63
|
|
|
@@ -87,8 +88,10 @@ def _download_file(url: str, dest: Path):
|
|
|
87
88
|
if total > 0:
|
|
88
89
|
pct = downloaded / total * 100
|
|
89
90
|
mb = downloaded / 1024 / 1024
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
from ._output import status_inline, status_end
|
|
92
|
+
status_inline(f" {mb:.1f} MB ({pct:.0f}%)")
|
|
93
|
+
from ._output import status_end
|
|
94
|
+
status_end()
|
|
92
95
|
os.replace(str(tmp), str(dest))
|
|
93
96
|
except BaseException:
|
|
94
97
|
tmp.unlink(missing_ok=True)
|
|
@@ -114,9 +114,13 @@ class PAWClient:
|
|
|
114
114
|
if (program_dir / "prompt_template.txt").exists():
|
|
115
115
|
return program_dir
|
|
116
116
|
|
|
117
|
+
from ._output import status
|
|
118
|
+
|
|
119
|
+
status(f"Downloading program {program_id[:12]}...")
|
|
117
120
|
max_wait = 30
|
|
118
121
|
elapsed = 0
|
|
119
122
|
resp = None
|
|
123
|
+
waiting_logged = False
|
|
120
124
|
while elapsed < max_wait:
|
|
121
125
|
resp = httpx.get(
|
|
122
126
|
f"{self._api_url}/api/v1/programs/{program_id}/download",
|
|
@@ -125,6 +129,9 @@ class PAWClient:
|
|
|
125
129
|
follow_redirects=True,
|
|
126
130
|
)
|
|
127
131
|
if resp.status_code == 202:
|
|
132
|
+
if not waiting_logged:
|
|
133
|
+
status("Waiting for program to be ready...")
|
|
134
|
+
waiting_logged = True
|
|
128
135
|
retry_after = int(resp.headers.get("Retry-After", "3"))
|
|
129
136
|
time.sleep(retry_after)
|
|
130
137
|
elapsed += retry_after
|
|
@@ -137,6 +144,9 @@ class PAWClient:
|
|
|
137
144
|
if "not found" in detail.lower():
|
|
138
145
|
raise RuntimeError(f"Program {program_id} not found on server.")
|
|
139
146
|
if elapsed < max_wait - 3:
|
|
147
|
+
if not waiting_logged:
|
|
148
|
+
status("Waiting for program to be ready...")
|
|
149
|
+
waiting_logged = True
|
|
140
150
|
time.sleep(3)
|
|
141
151
|
elapsed += 3
|
|
142
152
|
continue
|
|
@@ -51,9 +51,12 @@ class PawFunction:
|
|
|
51
51
|
raise FileNotFoundError(f"No prompt_template.txt in {program_dir}")
|
|
52
52
|
self._template = template_path.read_text()
|
|
53
53
|
|
|
54
|
+
from ._output import status
|
|
54
55
|
interpreter = self._meta.get("interpreter", "Qwen/Qwen3-0.6B")
|
|
55
56
|
base_model_path = cache.get_base_model_path(interpreter)
|
|
56
57
|
|
|
58
|
+
status("Loading interpreter...")
|
|
59
|
+
|
|
57
60
|
if not verbose:
|
|
58
61
|
import os as _os, sys as _sys
|
|
59
62
|
_stderr_fd = _sys.stderr.fileno()
|
|
@@ -103,6 +106,7 @@ class PawFunction:
|
|
|
103
106
|
self._n_prefix = len(self._prefix_tokens)
|
|
104
107
|
|
|
105
108
|
self._load_or_eval_prefix()
|
|
109
|
+
status("Ready.")
|
|
106
110
|
|
|
107
111
|
def _apply_adapter(self, scale: float):
|
|
108
112
|
"""Apply LoRA adapter, compatible with both old and new llama.cpp API."""
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "programasweights"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.8"
|
|
8
8
|
description = "Compile natural language specifications into neural programs that run locally via llama.cpp."
|
|
9
9
|
readme = "PYPI_README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# Writing Good Specs
|
|
2
|
-
|
|
3
|
-
A specification is a **self-contained** natural-language description of what the function should do. There is no separate “examples” API field: everything the compiler sees must live in that single spec string.
|
|
4
|
-
|
|
5
|
-
## What to include
|
|
6
|
-
|
|
7
|
-
Write clearly enough that someone unfamiliar with your project could implement the behavior from the text alone.
|
|
8
|
-
|
|
9
|
-
### Good practices
|
|
10
|
-
|
|
11
|
-
- **Output shape:** Say how results should look. Examples: “return a JSON list of strings”, “respond with only `positive` or `negative`”, “output valid CSV with a header row”.
|
|
12
|
-
- **Edge cases:** State behavior for empty input, missing data, or failure modes. Example: “if no emails are found, return an empty list”.
|
|
13
|
-
- **Ambiguity:** Explain how to handle unclear inputs (e.g., multiple interpretations, partial data).
|
|
14
|
-
- **Length:** Keep the spec under **8000 characters**; the API enforces this limit.
|
|
15
|
-
|
|
16
|
-
### Examples inside the spec
|
|
17
|
-
|
|
18
|
-
You may embed example pairs directly in the spec text, for example:
|
|
19
|
-
|
|
20
|
-
```text
|
|
21
|
-
Examples:
|
|
22
|
-
Input: hello
|
|
23
|
-
Output: greeting
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Use a consistent pattern so the compiler can treat them as demonstrations of desired behavior.
|
|
27
|
-
|
|
28
|
-
## What to avoid
|
|
29
|
-
|
|
30
|
-
- **Vague goals:** Phrases like “do something useful” or “be smart about it” do not constrain behavior.
|
|
31
|
-
- **Excessive length:** Long essays dilute the task definition and hit limits unnecessarily.
|
|
32
|
-
- **Contradictions:** Conflicting instructions produce unreliable pseudo-programs and weaker adapters.
|
|
33
|
-
|
|
34
|
-
## How the compiler uses your spec
|
|
35
|
-
|
|
36
|
-
The compiler **generates a pseudo-program** from your spec. That artifact often **rephrases and expands** the spec with structured examples. You can inspect what was produced in the playground under **“View compiled program internals”** to verify alignment with your intent.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programasweights-0.2.7 → programasweights-0.2.8}/programasweights/runtime/interpreter_onnx.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|