mmoussa commited on
Commit
a02e2d8
·
verified ·
1 Parent(s): bf9611a

Knack v3 as an Ollama decision model (--ask, /v1/systemone)

Browse files
Files changed (4) hide show
  1. README.md +78 -33
  2. knack-v3-Q4_K_M.gguf +2 -2
  3. knack-v3-Q8_0.gguf +2 -2
  4. knack.py +34 -25
README.md CHANGED
@@ -73,43 +73,90 @@ ollama pull devshub/knack:q4_k_m # 516 MB, near-identical accuracy
73
  ollama pull hf.co/devshub-ai/knack:Q8_0 # same model, straight from this repo
74
  ```
75
 
76
- Knack is a classifier, not a chat model. A request is a **state** (one `field: value` line
77
- per field) followed by one **question id**. The answer is the next token: a letter `A`, `B`,
78
- `C`... that picks the question's option in the order listed below.
79
 
80
- ```bash
81
- curl -s localhost:11434/api/generate -d '{
82
- "model": "devshub/knack", "raw": true, "stream": false,
83
- "logprobs": true, "top_logprobs": 20, "options": {"temperature": 0, "num_predict": 1},
84
- "prompt": "<|im_start|>user\ncommand: tail -f app.log\ndescription: <|im_end|>\n<|im_start|>assistant\nlong_running:"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  }'
86
  ```
87
 
88
- **Read the answer from `logprobs`, not from `response`.** A question with N options uses
89
- only the first N letters. Take the probabilities of those letters, renormalise them, and
90
- pick the highest (for `long_running` above: `A` = yes, `B` = no). The generated letter on
91
- its own can be an unused letter. `knack.py` does this for you.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
  ## What it decides
94
 
95
  **Every shell command** (state fields: `command`, `description`):
96
 
97
- | id | options (A, B, C...) |
98
- |---|---|
99
- | `risk` | 0 safe, 1 needs care, 2 destructive |
100
- | `long_running` | yes, no |
101
- | `activity` | testing, building, installing, git, inspecting, running |
102
- | `verifies` | yes, no |
103
- | `mutates_repo` | yes, no |
104
- | `deploys` | yes, no |
105
- | `runtime_check` | yes, no |
106
- | `serves` | yes, no |
107
 
108
  **Every user turn** (state fields, in this order: `prompt`, `current_mode`,
109
  `recent_activity`, `conversation_size`, `open_task`, `turns_on_open_task`,
110
  `interrupted_work`, `browser_context`; see `turn_state()` in `knack.py` for defaults):
111
 
112
- | id | options (A, B, C...) |
113
  |---|---|
114
  | `turn.chat_or_task` | chat, task |
115
  | `turn.actionable` / `turn.question_shaped` / `turn.new_task` / `turn.resume` / `turn.mission` / `turn.plan_authoring` / `turn.demo` / `turn.skip_orientation` | yes, no |
@@ -123,8 +170,8 @@ its own can be an unused letter. `knack.py` does this for you.
123
  | `route.risk` | low, medium, high |
124
  | `browser` / `messaging` / `delivery.feature_task` / `delivery.slash_task` | yes, no |
125
 
126
- Knack only knows these 29 questions. Inside CodeSync, anything else goes to CodeSync's cloud
127
- judgment service, so a new kind of question never gets a guess.
128
 
129
  ## Results
130
 
@@ -135,10 +182,9 @@ Measured on CodeSync's own judgments (version v3, November 2026):
135
  | Hand-labelled set | **97.9%** | 97.9% | 97.4% | 233 decisions labelled by hand |
136
  | Held-out set | **95.5%** | 95.7% | 95.7% | 3,267 decisions from real sessions, never trained on |
137
  | Rare-case set | **93.7%** | 93.6% | 93.6% | 4,120 decisions on the hardest, rarest cases |
138
- | Latency | **13 ms** | ~80 ms | ~80 ms | median per command (8 questions) on a laptop GPU |
139
 
140
- The Ollama files run one request per question; CodeSync's built-in runtime answers all
141
- questions of a turn in a single forward pass, which is why it is faster.
142
 
143
  ## Use it inside CodeSync
144
 
@@ -156,13 +202,12 @@ picked up automatically. To turn it off: `CODESYNC_KNACK_DISABLED=1`.
156
 
157
  | File | Purpose |
158
  |---|---|
159
- | `knack-v3-Q8_0.gguf`, `knack-v3-Q4_K_M.gguf` | Ollama / llama.cpp edition |
160
  | `knack.py` | standalone runner (CLI and local server), standard library only |
161
- | `template`, `params` | Ollama prompt template and parameters (raw prompt, 1 token, temperature 0) |
162
  | `manifest.json`, `knack-<version>.tar.gz` | the PyTorch model folder CodeSync installs |
163
 
164
- The GGUF edition is the same weights with an untied output head: the rows of the answer
165
- letters carry a shared offset, so the letters always lead the vocabulary while their relative
166
- probabilities, and so every answer, stay the same.
167
 
168
  Built by [DevsHub](https://app.devshub.ai) for [CodeSync](https://app.devshub.ai).
 
73
  ollama pull hf.co/devshub-ai/knack:Q8_0 # same model, straight from this repo
74
  ```
75
 
76
+ Then use it like any Ollama decision model:
 
 
77
 
78
+ ```
79
+ echo "npm run dev" | ollama run devshub/knack --ask "Will it keep running?"
80
+ ```
81
+
82
+ ```
83
+ yes 97%
84
+ ```
85
+
86
+ Ask by question id or by its plain phrasing (both listed below). Knack reads the text as a
87
+ shell command for the command questions and as a user message for the turn questions:
88
+
89
+ ```
90
+ echo "git push --force" | ollama run devshub/knack --ask risk --level safe --level "needs care" --level destructive
91
+ echo "fix the failing login test" | ollama run devshub/knack --ask "Is this a new task?"
92
+ echo "fix the failing login test" | ollama run devshub/knack --ask task.intent \
93
+ --choice bug_fix --choice feature --choice refactor --choice api_schema --choice config_deploy \
94
+ --choice test --choice documentation --choice investigation --choice unknown
95
+ ```
96
+
97
+ Knack only knows its 29 questions. Anything else ("Is a refund being requested?") is
98
+ rejected with the list of what it can answer, rather than guessed. A question asked with
99
+ the wrong options gets back the exact flags to use.
100
+
101
+ ### cURL
102
+
103
+ ```
104
+ curl http://localhost:11434/v1/systemone \
105
+ -H "Content-Type: application/json" \
106
+ -d '{
107
+ "model": "devshub/knack",
108
+ "state": "npm run dev",
109
+ "questions": {
110
+ "long_running": {"type": "noul", "instructions": "Will it keep running?"},
111
+ "serves": {"type": "noul", "instructions": "Does it start a server?"}
112
+ }
113
  }'
114
  ```
115
 
116
+ ```
117
+ {"model":"devshub/knack","answers":{"long_running":{"type":"noul","noul":0.9708},"serves":{"type":"noul","noul":0.9701}}}
118
+ ```
119
+
120
+ The state can also be an object with Knack's own fields, for example
121
+ `{"command": "npm run dev", "description": "start the app"}`.
122
+
123
+ ### Python
124
+
125
+ ```
126
+ pip install ollama
127
+ ```
128
+
129
+ ```python
130
+ import ollama
131
+
132
+ response = ollama.systemone(
133
+ model="devshub/knack",
134
+ state="npm run dev",
135
+ questions={"long_running": {"type": "noul", "instructions": "Will it keep running?"}},
136
+ )
137
+ print(response.answers["long_running"].noul) # 0.9708
138
+ ```
139
 
140
  ## What it decides
141
 
142
  **Every shell command** (state fields: `command`, `description`):
143
 
144
+ | id | options | plain question |
145
+ |---|---|---|
146
+ | `risk` | 0 safe, 1 needs care, 2 destructive | How risky is this command? |
147
+ | `long_running` | yes, no | Will it keep running? |
148
+ | `activity` | testing, building, installing, git, inspecting, running | What kind of command is this? |
149
+ | `verifies` | yes, no | Does it verify the work? |
150
+ | `mutates_repo` | yes, no | Does it change the repo? |
151
+ | `deploys` | yes, no | Does it deploy? |
152
+ | `runtime_check` | yes, no | Does it need a running app? |
153
+ | `serves` | yes, no | Does it start a server? |
154
 
155
  **Every user turn** (state fields, in this order: `prompt`, `current_mode`,
156
  `recent_activity`, `conversation_size`, `open_task`, `turns_on_open_task`,
157
  `interrupted_work`, `browser_context`; see `turn_state()` in `knack.py` for defaults):
158
 
159
+ | id | options |
160
  |---|---|
161
  | `turn.chat_or_task` | chat, task |
162
  | `turn.actionable` / `turn.question_shaped` / `turn.new_task` / `turn.resume` / `turn.mission` / `turn.plan_authoring` / `turn.demo` / `turn.skip_orientation` | yes, no |
 
170
  | `route.risk` | low, medium, high |
171
  | `browser` / `messaging` / `delivery.feature_task` / `delivery.slash_task` | yes, no |
172
 
173
+ Knack only knows these 29 questions; Ollama rejects any other one instead of guessing.
174
+ Inside CodeSync, anything else goes to CodeSync's cloud judgment service.
175
 
176
  ## Results
177
 
 
182
  | Hand-labelled set | **97.9%** | 97.9% | 97.4% | 233 decisions labelled by hand |
183
  | Held-out set | **95.5%** | 95.7% | 95.7% | 3,267 decisions from real sessions, never trained on |
184
  | Rare-case set | **93.7%** | 93.6% | 93.6% | 4,120 decisions on the hardest, rarest cases |
185
+ | Latency | **13 ms** | 17 ms | ~17 ms | median per command (8 questions) on a laptop GPU |
186
 
187
+ Latency is the median for all 8 command questions in one `/v1/systemone` request.
 
188
 
189
  ## Use it inside CodeSync
190
 
 
202
 
203
  | File | Purpose |
204
  |---|---|
205
+ | `knack-v3-Q8_0.gguf`, `knack-v3-Q4_K_M.gguf` | Ollama / llama.cpp decision model (`--ask`, `/v1/systemone`) |
206
  | `knack.py` | standalone runner (CLI and local server), standard library only |
 
207
  | `manifest.json`, `knack-<version>.tar.gz` | the PyTorch model folder CodeSync installs |
208
 
209
+ The GGUF edition is the same weights packaged as an Ollama decision model: a `systemone`
210
+ prompt template rebuilds Knack's training prompt from the request, and the answer is read
211
+ from the option letters exactly as CodeSync's own runtime reads it.
212
 
213
  Built by [DevsHub](https://app.devshub.ai) for [CodeSync](https://app.devshub.ai).
knack-v3-Q4_K_M.gguf CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:c55e4831fc36f7adf5bd90573bb3c2c81c0409451ddebd2e13bdce9f49ea1095
3
- size 515961664
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:efa47599cece417c1e66daa86b876e7665fa6d2e28a3e5c79a2425ae64dd5e23
3
+ size 515970400
knack-v3-Q8_0.gguf CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:2eed77e11930251a46eed2bf1cf6401e8fe499c2a953bca5e8fdaa9bc4eeeb6b
3
- size 816653120
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:18aaebf8083a4b123495e373131bf3fe858599c95421b412bdcc1db38d16f753
3
+ size 816661856
knack.py CHANGED
@@ -15,11 +15,9 @@ Usage:
15
  python knack.py --turn "add dark mode" --mode plan --json
16
  python knack.py serve --port 33727 # CodeSync-compatible POST /v1/systemone
17
 
18
- How Knack answers: the prompt is a state (``field: value`` lines) followed by one question
19
- id, and the answer is the next token, a letter ``A``, ``B``, ``C``... picking the question's
20
- option in order. Only the first N letters belong to a question with N options, so the
21
- answer is the most likely of THOSE letters, renormalised (this script does that from
22
- Ollama's logprobs). Do not read the raw generated letter on its own.
23
 
24
  Env: KNACK_OLLAMA_HOST (default http://127.0.0.1:11434),
25
  KNACK_MODEL (default devshub/knack; devshub/knack:q4_k_m is smaller, 516 MB;
@@ -30,20 +28,15 @@ from __future__ import annotations
30
 
31
  import argparse
32
  import json
33
- import math
34
  import os
35
  import sys
36
  import urllib.error
37
  import urllib.request
38
- from concurrent.futures import ThreadPoolExecutor
39
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
40
  from typing import Any
41
 
42
  HOST = os.environ.get("KNACK_OLLAMA_HOST", "http://127.0.0.1:11434").rstrip("/")
43
  MODEL = os.environ.get("KNACK_MODEL", "devshub/knack")
44
- PREFIX = "<|im_start|>user\n"
45
- SUFFIX = "<|im_end|>\n<|im_start|>assistant\n"
46
- LETTERS = [chr(65 + i) for i in range(26)]
47
  _READY: set[str] = set()
48
 
49
  # question id -> (type, option keys in letter order, what it decides)
@@ -110,19 +103,25 @@ def _post(path: str, body: dict[str, Any], timeout: float = 60.0) -> dict[str, A
110
  return json.loads(r.read())
111
 
112
 
113
- def ask_one(state: dict[str, Any], qid: str, model: str = MODEL) -> dict[str, Any]:
114
- """{answer, confidence, probabilities} for one question."""
115
  kind, keys, _ = QUESTIONS[qid]
116
- out = _post("/api/generate", {
117
- "model": model, "prompt": PREFIX + render_state(state) + SUFFIX + qid + ":",
118
- "raw": True, "stream": False, "logprobs": True, "top_logprobs": 20,
119
- "options": {"temperature": 0, "num_predict": 1}})
120
- letters = LETTERS[: len(keys)]
121
- logp = {e["token"].strip(): e["logprob"]
122
- for e in (out.get("logprobs") or [{}])[0].get("top_logprobs", [])}
123
- raw = {k: math.exp(logp[c]) if c in logp else 0.0 for k, c in zip(keys, letters)}
124
- total = sum(raw.values()) or 1.0
125
- probs = {k: v / total for k, v in raw.items()}
 
 
 
 
 
 
126
  best = max(probs, key=probs.__getitem__)
127
  return {"answer": best, "confidence": probs[best], "probabilities": probs, "type": kind}
128
 
@@ -147,8 +146,15 @@ def ask(state: dict[str, Any], qids: list[str], model: str = MODEL) -> dict[str,
147
  if unknown:
148
  raise KeyError(f"Knack was not trained on: {unknown}")
149
  ensure_model(model)
150
- with ThreadPoolExecutor(max_workers=8) as pool:
151
- return dict(zip(qids, pool.map(lambda q: ask_one(state, q, model), qids)))
 
 
 
 
 
 
 
152
 
153
 
154
  def to_systemone(qid: str, a: dict[str, Any]) -> dict[str, Any]:
@@ -213,8 +219,11 @@ def main(argv: list[str] | None = None) -> int:
213
  qids = a.question or (TURN if a.turn else BASH)
214
  try:
215
  res = ask(state, qids, a.model)
 
 
 
216
  except OSError as exc:
217
- print(f"cannot reach Ollama at {HOST} ({exc}). Run: ollama pull {a.model}", file=sys.stderr)
218
  return 1
219
  if a.json:
220
  print(json.dumps(res, indent=1))
 
15
  python knack.py --turn "add dark mode" --mode plan --json
16
  python knack.py serve --port 33727 # CodeSync-compatible POST /v1/systemone
17
 
18
+ Knack is an Ollama decision model: this script sends all questions of a command or turn in
19
+ one ``POST /v1/systemone`` request (needs an Ollama with decision-model support). Without the
20
+ script: ``echo "npm run dev" | ollama run devshub/knack --ask long_running``.
 
 
21
 
22
  Env: KNACK_OLLAMA_HOST (default http://127.0.0.1:11434),
23
  KNACK_MODEL (default devshub/knack; devshub/knack:q4_k_m is smaller, 516 MB;
 
28
 
29
  import argparse
30
  import json
 
31
  import os
32
  import sys
33
  import urllib.error
34
  import urllib.request
 
35
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
36
  from typing import Any
37
 
38
  HOST = os.environ.get("KNACK_OLLAMA_HOST", "http://127.0.0.1:11434").rstrip("/")
39
  MODEL = os.environ.get("KNACK_MODEL", "devshub/knack")
 
 
 
40
  _READY: set[str] = set()
41
 
42
  # question id -> (type, option keys in letter order, what it decides)
 
103
  return json.loads(r.read())
104
 
105
 
106
+ def _spec(qid: str) -> dict[str, Any]:
107
+ """System One question for /v1/systemone (the id is what Knack matches on)."""
108
  kind, keys, _ = QUESTIONS[qid]
109
+ if kind == "noul":
110
+ return {"type": "noul", "instructions": qid}
111
+ if kind == "score":
112
+ return {"type": "score", "instructions": qid, "criteria": keys}
113
+ return {"type": "choice", "instructions": qid, "criteria": {k: None for k in keys}}
114
+
115
+
116
+ def _answer(qid: str, a: dict[str, Any]) -> dict[str, Any]:
117
+ """{answer, confidence, probabilities, type} from one /v1/systemone answer."""
118
+ kind, keys, _ = QUESTIONS[qid]
119
+ if kind == "noul":
120
+ yes = float(a["noul"])
121
+ probs = {"yes": yes, "no": 1.0 - yes}
122
+ else:
123
+ raw = a.get("probabilities") or {}
124
+ probs = {k: float(raw.get(k, raw.get(str(i), 0.0))) for i, k in enumerate(keys)}
125
  best = max(probs, key=probs.__getitem__)
126
  return {"answer": best, "confidence": probs[best], "probabilities": probs, "type": kind}
127
 
 
146
  if unknown:
147
  raise KeyError(f"Knack was not trained on: {unknown}")
148
  ensure_model(model)
149
+ try:
150
+ out = _post("/v1/systemone", {"model": model, "state": state,
151
+ "questions": {q: _spec(q) for q in qids}})
152
+ except urllib.error.HTTPError as exc:
153
+ detail = exc.read().decode(errors="replace")[:300]
154
+ raise RuntimeError(f"Ollama /v1/systemone: HTTP {exc.code} {detail} "
155
+ "(update Ollama and re-pull the model if this is an old build)") from None
156
+ answers = out.get("answers") or {}
157
+ return {q: _answer(q, answers[q]) for q in qids if q in answers}
158
 
159
 
160
  def to_systemone(qid: str, a: dict[str, Any]) -> dict[str, Any]:
 
219
  qids = a.question or (TURN if a.turn else BASH)
220
  try:
221
  res = ask(state, qids, a.model)
222
+ except RuntimeError as exc:
223
+ print(exc, file=sys.stderr)
224
+ return 1
225
  except OSError as exc:
226
+ print(f"cannot reach Ollama at {HOST} ({exc}). Is Ollama running?", file=sys.stderr)
227
  return 1
228
  if a.json:
229
  print(json.dumps(res, indent=1))