Instructions to use devshub-ai/knack with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use devshub-ai/knack with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf devshub-ai/knack:Q4_K_M # Run inference directly in the terminal: llama cli -hf devshub-ai/knack:Q4_K_M
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf devshub-ai/knack:Q4_K_M # Run inference directly in the terminal: llama cli -hf devshub-ai/knack:Q4_K_M
Build from source code
git clone https://github.com/ggml-org/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-app # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama serve -hf devshub-ai/knack:Q4_K_M # Run inference directly in the terminal: ./build/bin/llama cli -hf devshub-ai/knack:Q4_K_M
Use Docker
docker model run hf.co/devshub-ai/knack:Q4_K_M
- Llama new
- LM Studio
- Jan
- Ollama
How to use devshub-ai/knack with Ollama:
ollama run hf.co/devshub-ai/knack:Q4_K_M
- Unsloth Desktop
- Pi
How to use devshub-ai/knack with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf devshub-ai/knack:Q4_K_M
Configure the model in Pi
# Install Pi: npm install -g @earendil-works/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "devshub-ai/knack:Q4_K_M" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Docker Model Runner
How to use devshub-ai/knack with Docker Model Runner:
docker model run hf.co/devshub-ai/knack:Q4_K_M
- Lemonade
How to use devshub-ai/knack with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull devshub-ai/knack:Q4_K_M
Run and chat with the model
lemonade run user.knack-Q4_K_M
List all available models
lemonade list
- Hermes Agent
How to use devshub-ai/knack with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf devshub-ai/knack:Q4_K_M
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default devshub-ai/knack:Q4_K_M
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use devshub-ai/knack with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf devshub-ai/knack:Q4_K_M
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "devshub-ai/knack:Q4_K_M" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
Knack v3: GGUF for Ollama / llama.cpp + knack.py
Browse files- .gitattributes +2 -0
- README.md +113 -26
- knack-v3-Q4_K_M.gguf +3 -0
- knack-v3-Q8_0.gguf +3 -0
- knack.py +226 -0
- params +5 -0
- template +1 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
knack-v3-Q4_K_M.gguf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
knack-v3-Q8_0.gguf filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -4,7 +4,10 @@ tags:
|
|
| 4 |
- devshub
|
| 5 |
- decision-model
|
| 6 |
- developer-tools
|
|
|
|
|
|
|
| 7 |
pipeline_tag: text-classification
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
# CodeSync Knack
|
|
@@ -19,54 +22,138 @@ in about 10 milliseconds.
|
|
| 19 |
- **Free.** Ships with every CodeSync install. No API key, no account, no usage limits.
|
| 20 |
- **Private.** Runs locally. Your prompts and commands never leave your machine to be judged.
|
| 21 |
- **Fast.** About 10 ms per decision on a GPU, about 250 ms on a plain CPU.
|
| 22 |
-
- **Small.**
|
| 23 |
- **Accurate where it matters.** Trained on real CodeSync sessions, with extra focus on the
|
| 24 |
rare cases that cost the most when missed: destructive deletes, commands that never exit,
|
| 25 |
resuming interrupted work, refactor / test / documentation requests.
|
| 26 |
|
| 27 |
-
##
|
| 28 |
|
| 29 |
-
|
|
|
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
| Held-out set | **95.5%** | 3,267 decisions from real sessions, never trained on |
|
| 35 |
-
| Rare-case set | **93.7%** | 4,120 decisions on the hardest, rarest cases |
|
| 36 |
-
| Latency | **13 ms** | median per request on a laptop GPU |
|
| 37 |
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
-
|
| 41 |
|
| 42 |
```bash
|
| 43 |
-
curl -fsSL https://
|
| 44 |
```
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
## What it decides
|
| 53 |
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
-
|
| 63 |
-
|
|
|
|
| 64 |
|
| 65 |
## Files
|
| 66 |
|
| 67 |
| File | Purpose |
|
| 68 |
|---|---|
|
| 69 |
-
| `
|
| 70 |
-
| `knack
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
Built by [DevsHub](https://app.devshub.ai) for [CodeSync](https://app.devshub.ai).
|
|
|
|
| 4 |
- devshub
|
| 5 |
- decision-model
|
| 6 |
- developer-tools
|
| 7 |
+
- gguf
|
| 8 |
+
- ollama
|
| 9 |
pipeline_tag: text-classification
|
| 10 |
+
inference: false
|
| 11 |
---
|
| 12 |
|
| 13 |
# CodeSync Knack
|
|
|
|
| 22 |
- **Free.** Ships with every CodeSync install. No API key, no account, no usage limits.
|
| 23 |
- **Private.** Runs locally. Your prompts and commands never leave your machine to be judged.
|
| 24 |
- **Fast.** About 10 ms per decision on a GPU, about 250 ms on a plain CPU.
|
| 25 |
+
- **Small.** 0.5B parameters. 546 MB (Q8_0) or 371 MB (Q4_K_M) in Ollama.
|
| 26 |
- **Accurate where it matters.** Trained on real CodeSync sessions, with extra focus on the
|
| 27 |
rare cases that cost the most when missed: destructive deletes, commands that never exit,
|
| 28 |
resuming interrupted work, refactor / test / documentation requests.
|
| 29 |
|
| 30 |
+
## Try it in one line
|
| 31 |
|
| 32 |
+
You need [Ollama](https://ollama.com/download) running. Nothing else to install: the script
|
| 33 |
+
uses only the Python standard library and pulls the model on first run.
|
| 34 |
|
| 35 |
+
```bash
|
| 36 |
+
curl -fsSL https://huggingface.co/devshub-ai/knack/resolve/main/knack.py | python3 - "rm -rf build/"
|
| 37 |
+
```
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
```
|
| 40 |
+
risk 2 0.93 0 safe, 1 needs care, 2 destructive / dangerous
|
| 41 |
+
long_running no 0.97 keeps running (server, watch, poll, tail -f)
|
| 42 |
+
activity running 0.95 what kind of command it is
|
| 43 |
+
...
|
| 44 |
+
```
|
| 45 |
|
| 46 |
+
Judge a user message instead of a command:
|
| 47 |
|
| 48 |
```bash
|
| 49 |
+
curl -fsSL https://huggingface.co/devshub-ai/knack/resolve/main/knack.py | python3 - --turn "fix the failing login test"
|
| 50 |
```
|
| 51 |
|
| 52 |
+
Or save the script and use it as a tool or a local server:
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
curl -fsSLO https://huggingface.co/devshub-ai/knack/resolve/main/knack.py
|
| 56 |
+
python3 knack.py "docker compose up" --json # machine-readable
|
| 57 |
+
python3 knack.py --turn "add dark mode" -q route.mode -q task.intent
|
| 58 |
+
python3 knack.py serve --port 33727 # POST /v1/systemone (CodeSync format)
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
## Use it with Ollama directly
|
| 62 |
+
|
| 63 |
+
```bash
|
| 64 |
+
ollama pull hf.co/devshub-ai/knack:Q8_0 # or :Q4_K_M (smaller, near-identical accuracy)
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
Knack is a classifier, not a chat model. A request is a **state** (one `field: value` line
|
| 68 |
+
per field) followed by one **question id**. The answer is the next token: a letter `A`, `B`,
|
| 69 |
+
`C`... that picks the question's option in the order listed below.
|
| 70 |
+
|
| 71 |
+
```bash
|
| 72 |
+
curl -s localhost:11434/api/generate -d '{
|
| 73 |
+
"model": "hf.co/devshub-ai/knack:Q8_0", "raw": true, "stream": false,
|
| 74 |
+
"logprobs": true, "top_logprobs": 20, "options": {"temperature": 0, "num_predict": 1},
|
| 75 |
+
"prompt": "<|im_start|>user\ncommand: tail -f app.log\ndescription: <|im_end|>\n<|im_start|>assistant\nlong_running:"
|
| 76 |
+
}'
|
| 77 |
+
```
|
| 78 |
|
| 79 |
+
**Read the answer from `logprobs`, not from `response`.** A question with N options uses
|
| 80 |
+
only the first N letters. Take the probabilities of those letters, renormalise them, and
|
| 81 |
+
pick the highest (for `long_running` above: `A` = yes, `B` = no). The generated letter on
|
| 82 |
+
its own can be an unused letter. `knack.py` does this for you.
|
| 83 |
|
| 84 |
## What it decides
|
| 85 |
|
| 86 |
+
**Every shell command** (state fields: `command`, `description`):
|
| 87 |
+
|
| 88 |
+
| id | options (A, B, C...) |
|
| 89 |
+
|---|---|
|
| 90 |
+
| `risk` | 0 safe, 1 needs care, 2 destructive |
|
| 91 |
+
| `long_running` | yes, no |
|
| 92 |
+
| `activity` | testing, building, installing, git, inspecting, running |
|
| 93 |
+
| `verifies` | yes, no |
|
| 94 |
+
| `mutates_repo` | yes, no |
|
| 95 |
+
| `deploys` | yes, no |
|
| 96 |
+
| `runtime_check` | yes, no |
|
| 97 |
+
| `serves` | yes, no |
|
| 98 |
+
|
| 99 |
+
**Every user turn** (state fields, in this order: `prompt`, `current_mode`,
|
| 100 |
+
`recent_activity`, `conversation_size`, `open_task`, `turns_on_open_task`,
|
| 101 |
+
`interrupted_work`, `browser_context`; see `turn_state()` in `knack.py` for defaults):
|
| 102 |
+
|
| 103 |
+
| id | options (A, B, C...) |
|
| 104 |
+
|---|---|
|
| 105 |
+
| `turn.chat_or_task` | chat, task |
|
| 106 |
+
| `turn.actionable` / `turn.question_shaped` / `turn.new_task` / `turn.resume` / `turn.mission` / `turn.plan_authoring` / `turn.demo` / `turn.skip_orientation` | yes, no |
|
| 107 |
+
| `turn.change_intent` | create, adjust, enhance, fix, refactor, none |
|
| 108 |
+
| `turn.screencast` | start, stop, none |
|
| 109 |
+
| `task.intent` | bug_fix, feature, refactor, api_schema, config_deploy, test, documentation, investigation, unknown |
|
| 110 |
+
| `task.execution_strategy` | direct, plan, parallel |
|
| 111 |
+
| `route.mode` | plan, build, ask, debug |
|
| 112 |
+
| `route.tier` | light, medium, heavy |
|
| 113 |
+
| `route.thinking` | off, low, medium, high, xhigh, max |
|
| 114 |
+
| `route.risk` | low, medium, high |
|
| 115 |
+
| `browser` / `messaging` / `delivery.feature_task` / `delivery.slash_task` | yes, no |
|
| 116 |
+
|
| 117 |
+
Knack only knows these 29 questions. Inside CodeSync, anything else goes to CodeSync's cloud
|
| 118 |
+
judgment service, so a new kind of question never gets a guess.
|
| 119 |
+
|
| 120 |
+
## Results
|
| 121 |
+
|
| 122 |
+
Measured on CodeSync's own judgments (version v3, November 2026):
|
| 123 |
|
| 124 |
+
| Benchmark | Knack v3 | Ollama Q8_0 | Ollama Q4_K_M | What it measures |
|
| 125 |
+
|---|---:|---:|---:|---|
|
| 126 |
+
| Hand-labelled set | **97.9%** | 97.9% | 97.4% | 233 decisions labelled by hand |
|
| 127 |
+
| Held-out set | **95.5%** | 95.7% | 95.7% | 3,267 decisions from real sessions, never trained on |
|
| 128 |
+
| Rare-case set | **93.7%** | 93.6% | 93.6% | 4,120 decisions on the hardest, rarest cases |
|
| 129 |
+
| Latency | **13 ms** | ~80 ms | ~80 ms | median per command (8 questions) on a laptop GPU |
|
| 130 |
+
|
| 131 |
+
The Ollama files run one request per question; CodeSync's built-in runtime answers all
|
| 132 |
+
questions of a turn in a single forward pass, which is why it is faster.
|
| 133 |
+
|
| 134 |
+
## Use it inside CodeSync
|
| 135 |
+
|
| 136 |
+
Install CodeSync. That's it.
|
| 137 |
+
|
| 138 |
+
```bash
|
| 139 |
+
curl -fsSL https://app.devshub.ai/install.sh | bash
|
| 140 |
+
```
|
| 141 |
|
| 142 |
+
On first start, CodeSync downloads Knack in the background and keeps working while it
|
| 143 |
+
does. Once Knack is ready it becomes CodeSync's first judgment engine, and new versions are
|
| 144 |
+
picked up automatically. To turn it off: `CODESYNC_KNACK_DISABLED=1`.
|
| 145 |
|
| 146 |
## Files
|
| 147 |
|
| 148 |
| File | Purpose |
|
| 149 |
|---|---|
|
| 150 |
+
| `knack-v3-Q8_0.gguf`, `knack-v3-Q4_K_M.gguf` | Ollama / llama.cpp edition |
|
| 151 |
+
| `knack.py` | standalone runner (CLI and local server), standard library only |
|
| 152 |
+
| `template`, `params` | Ollama prompt template and parameters (raw prompt, 1 token, temperature 0) |
|
| 153 |
+
| `manifest.json`, `knack-<version>.tar.gz` | the PyTorch model folder CodeSync installs |
|
| 154 |
+
|
| 155 |
+
The GGUF edition is the same weights with an untied output head: the rows of the answer
|
| 156 |
+
letters carry a shared offset, so the letters always lead the vocabulary while their relative
|
| 157 |
+
probabilities, and so every answer, stay the same.
|
| 158 |
|
| 159 |
Built by [DevsHub](https://app.devshub.ai) for [CodeSync](https://app.devshub.ai).
|
knack-v3-Q4_K_M.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c55e4831fc36f7adf5bd90573bb3c2c81c0409451ddebd2e13bdce9f49ea1095
|
| 3 |
+
size 515961664
|
knack-v3-Q8_0.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2eed77e11930251a46eed2bf1cf6401e8fe499c2a953bca5e8fdaa9bc4eeeb6b
|
| 3 |
+
size 816653120
|
knack.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""CodeSync Knack, standalone: ask the model CodeSync's judgment questions through Ollama.
|
| 3 |
+
|
| 4 |
+
Standard library only. Needs Ollama (https://ollama.com) running; the model (546 MB) is
|
| 5 |
+
pulled on first use. One-liner, nothing to install:
|
| 6 |
+
|
| 7 |
+
curl -fsSL https://huggingface.co/devshub-ai/knack/resolve/main/knack.py | python3 - "rm -rf build/"
|
| 8 |
+
|
| 9 |
+
Usage:
|
| 10 |
+
|
| 11 |
+
python knack.py "rm -rf build/" # the 8 shell-command questions
|
| 12 |
+
python knack.py --turn "fix the failing login test" # the 21 per-turn questions
|
| 13 |
+
python knack.py --turn "add dark mode" --mode plan --json
|
| 14 |
+
python knack.py serve --port 33727 # CodeSync-compatible POST /v1/systemone
|
| 15 |
+
|
| 16 |
+
How Knack answers: the prompt is a state (``field: value`` lines) followed by one question
|
| 17 |
+
id, and the answer is the next token, a letter ``A``, ``B``, ``C``... picking the question's
|
| 18 |
+
option in order. Only the first N letters belong to a question with N options, so the
|
| 19 |
+
answer is the most likely of THOSE letters, renormalised (this script does that from
|
| 20 |
+
Ollama's logprobs). Do not read the raw generated letter on its own.
|
| 21 |
+
|
| 22 |
+
Env: KNACK_OLLAMA_HOST (default http://127.0.0.1:11434),
|
| 23 |
+
KNACK_MODEL (default hf.co/devshub-ai/knack:Q8_0; :Q4_K_M is smaller, 371 MB).
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
from __future__ import annotations
|
| 27 |
+
|
| 28 |
+
import argparse
|
| 29 |
+
import json
|
| 30 |
+
import math
|
| 31 |
+
import os
|
| 32 |
+
import sys
|
| 33 |
+
import urllib.error
|
| 34 |
+
import urllib.request
|
| 35 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 36 |
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
| 37 |
+
from typing import Any
|
| 38 |
+
|
| 39 |
+
HOST = os.environ.get("KNACK_OLLAMA_HOST", "http://127.0.0.1:11434").rstrip("/")
|
| 40 |
+
MODEL = os.environ.get("KNACK_MODEL", "hf.co/devshub-ai/knack:Q8_0")
|
| 41 |
+
PREFIX = "<|im_start|>user\n"
|
| 42 |
+
SUFFIX = "<|im_end|>\n<|im_start|>assistant\n"
|
| 43 |
+
LETTERS = [chr(65 + i) for i in range(26)]
|
| 44 |
+
_READY: set[str] = set()
|
| 45 |
+
|
| 46 |
+
# question id -> (type, option keys in letter order, what it decides)
|
| 47 |
+
QUESTIONS: dict[str, tuple[str, list[str], str]] = {
|
| 48 |
+
# every shell command (state: command, description)
|
| 49 |
+
"risk": ("score", ["0", "1", "2"], "0 safe, 1 needs care, 2 destructive / dangerous"),
|
| 50 |
+
"long_running": ("noul", ["yes", "no"], "keeps running (server, watch, poll, tail -f)"),
|
| 51 |
+
"activity": ("choice", ["testing", "building", "installing", "git", "inspecting", "running"],
|
| 52 |
+
"what kind of command it is"),
|
| 53 |
+
"verifies": ("noul", ["yes", "no"], "checks that work is correct (tests, lint, build)"),
|
| 54 |
+
"mutates_repo": ("noul", ["yes", "no"], "changes files or git state of the repo"),
|
| 55 |
+
"deploys": ("noul", ["yes", "no"], "deploys or publishes"),
|
| 56 |
+
"runtime_check": ("noul", ["yes", "no"], "needs a running app to check"),
|
| 57 |
+
"serves": ("noul", ["yes", "no"], "starts a server"),
|
| 58 |
+
# every user turn (state: prompt, current_mode, ...)
|
| 59 |
+
"turn.chat_or_task": ("choice", ["chat", "task"], "conversation or work request"),
|
| 60 |
+
"turn.actionable": ("noul", ["yes", "no"], "asks for something to be done"),
|
| 61 |
+
"turn.question_shaped": ("noul", ["yes", "no"], "phrased as a question"),
|
| 62 |
+
"turn.change_intent": ("choice", ["create", "adjust", "enhance", "fix", "refactor", "none"],
|
| 63 |
+
"kind of code change asked for"),
|
| 64 |
+
"turn.new_task": ("noul", ["yes", "no"], "starts a new task (vs continuing)"),
|
| 65 |
+
"turn.resume": ("noul", ["yes", "no"], "asks to resume interrupted work"),
|
| 66 |
+
"turn.mission": ("noul", ["yes", "no"], "long multi-step mission"),
|
| 67 |
+
"turn.plan_authoring": ("noul", ["yes", "no"], "asks to write a plan"),
|
| 68 |
+
"turn.screencast": ("choice", ["start", "stop", "none"], "start / stop screen recording"),
|
| 69 |
+
"turn.demo": ("noul", ["yes", "no"], "asks for a demo"),
|
| 70 |
+
"turn.skip_orientation": ("noul", ["yes", "no"], "no codebase exploration needed"),
|
| 71 |
+
"task.intent": ("choice", ["bug_fix", "feature", "refactor", "api_schema", "config_deploy",
|
| 72 |
+
"test", "documentation", "investigation", "unknown"], "task type"),
|
| 73 |
+
"task.execution_strategy": ("choice", ["direct", "plan", "parallel"], "how to execute"),
|
| 74 |
+
"route.mode": ("choice", ["plan", "build", "ask", "debug"], "best CodeSync mode"),
|
| 75 |
+
"route.tier": ("choice", ["light", "medium", "heavy"], "model size needed"),
|
| 76 |
+
"route.thinking": ("choice", ["off", "low", "medium", "high", "xhigh", "max"],
|
| 77 |
+
"reasoning effort needed"),
|
| 78 |
+
"route.risk": ("choice", ["low", "medium", "high"], "risk of the requested work"),
|
| 79 |
+
"browser": ("choice", ["yes", "no"], "needs a browser"),
|
| 80 |
+
"messaging": ("choice", ["yes", "no"], "needs to send a message (email, chat)"),
|
| 81 |
+
"delivery.feature_task": ("choice", ["yes", "no"], "a feature delivery task"),
|
| 82 |
+
"delivery.slash_task": ("choice", ["yes", "no"], "a slash-command style task"),
|
| 83 |
+
}
|
| 84 |
+
BASH = list(QUESTIONS)[:8] # the shell-command questions above
|
| 85 |
+
TURN = list(QUESTIONS)[8:] # the per-turn questions
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def bash_state(command: str, description: str = "") -> dict[str, Any]:
|
| 89 |
+
return {"command": command, "description": description}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def turn_state(prompt: str, mode: str = "build") -> dict[str, Any]:
|
| 93 |
+
return {"prompt": prompt, "current_mode": mode, "recent_activity": "none",
|
| 94 |
+
"conversation_size": "unknown", "open_task": "none", "turns_on_open_task": 0,
|
| 95 |
+
"interrupted_work": "none", "browser_context": False}
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def render_state(state: dict[str, Any]) -> str:
|
| 99 |
+
return "\n".join(f"{k}: {v if isinstance(v, str) else json.dumps(v, ensure_ascii=False)}"
|
| 100 |
+
for k, v in state.items())
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _post(path: str, body: dict[str, Any], timeout: float = 60.0) -> dict[str, Any]:
|
| 104 |
+
req = urllib.request.Request(HOST + path, data=json.dumps(body).encode(),
|
| 105 |
+
headers={"Content-Type": "application/json"})
|
| 106 |
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
| 107 |
+
return json.loads(r.read())
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def ask_one(state: dict[str, Any], qid: str, model: str = MODEL) -> dict[str, Any]:
|
| 111 |
+
"""{answer, confidence, probabilities} for one question."""
|
| 112 |
+
kind, keys, _ = QUESTIONS[qid]
|
| 113 |
+
out = _post("/api/generate", {
|
| 114 |
+
"model": model, "prompt": PREFIX + render_state(state) + SUFFIX + qid + ":",
|
| 115 |
+
"raw": True, "stream": False, "logprobs": True, "top_logprobs": 20,
|
| 116 |
+
"options": {"temperature": 0, "num_predict": 1}})
|
| 117 |
+
letters = LETTERS[: len(keys)]
|
| 118 |
+
logp = {e["token"].strip(): e["logprob"]
|
| 119 |
+
for e in (out.get("logprobs") or [{}])[0].get("top_logprobs", [])}
|
| 120 |
+
raw = {k: math.exp(logp[c]) if c in logp else 0.0 for k, c in zip(keys, letters)}
|
| 121 |
+
total = sum(raw.values()) or 1.0
|
| 122 |
+
probs = {k: v / total for k, v in raw.items()}
|
| 123 |
+
best = max(probs, key=probs.__getitem__)
|
| 124 |
+
return {"answer": best, "confidence": probs[best], "probabilities": probs, "type": kind}
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def ensure_model(model: str = MODEL) -> None:
|
| 128 |
+
"""Pull the model into Ollama if it is not there yet (first run only)."""
|
| 129 |
+
if model in _READY:
|
| 130 |
+
return
|
| 131 |
+
_READY.add(model)
|
| 132 |
+
try:
|
| 133 |
+
_post("/api/show", {"model": model}, timeout=10)
|
| 134 |
+
return
|
| 135 |
+
except urllib.error.HTTPError as exc:
|
| 136 |
+
if exc.code != 404:
|
| 137 |
+
raise
|
| 138 |
+
print(f"pulling {model} into Ollama (first run only)...", file=sys.stderr, flush=True)
|
| 139 |
+
_post("/api/pull", {"model": model, "stream": False}, timeout=3600)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def ask(state: dict[str, Any], qids: list[str], model: str = MODEL) -> dict[str, dict[str, Any]]:
|
| 143 |
+
unknown = [q for q in qids if q not in QUESTIONS]
|
| 144 |
+
if unknown:
|
| 145 |
+
raise KeyError(f"Knack was not trained on: {unknown}")
|
| 146 |
+
ensure_model(model)
|
| 147 |
+
with ThreadPoolExecutor(max_workers=8) as pool:
|
| 148 |
+
return dict(zip(qids, pool.map(lambda q: ask_one(state, q, model), qids)))
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def to_systemone(qid: str, a: dict[str, Any]) -> dict[str, Any]:
|
| 152 |
+
"""CodeSync / TypeSafe System One answer shape."""
|
| 153 |
+
p, kind = a["probabilities"], a["type"]
|
| 154 |
+
if kind == "noul":
|
| 155 |
+
return {"noul": p["yes"], "confidence": a["confidence"]}
|
| 156 |
+
if kind == "score":
|
| 157 |
+
return {"score": sum(int(k) * v for k, v in p.items()), "confidence": a["confidence"],
|
| 158 |
+
"probabilities": p}
|
| 159 |
+
return {"choice": a["answer"], "confidence": a["confidence"], "probabilities": p}
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def serve(port: int, model: str) -> None:
|
| 163 |
+
class H(BaseHTTPRequestHandler):
|
| 164 |
+
def _send(self, code: int, body: dict[str, Any]) -> None:
|
| 165 |
+
data = json.dumps(body).encode()
|
| 166 |
+
self.send_response(code)
|
| 167 |
+
self.send_header("Content-Type", "application/json")
|
| 168 |
+
self.send_header("Content-Length", str(len(data)))
|
| 169 |
+
self.end_headers()
|
| 170 |
+
self.wfile.write(data)
|
| 171 |
+
|
| 172 |
+
def do_GET(self) -> None: # noqa: N802
|
| 173 |
+
self._send(200, {"ok": True, "model": model, "questions": len(QUESTIONS)})
|
| 174 |
+
|
| 175 |
+
def do_POST(self) -> None: # noqa: N802
|
| 176 |
+
body = json.loads(self.rfile.read(int(self.headers.get("Content-Length") or 0)))
|
| 177 |
+
qids = [q for q in body.get("questions", {}) if q in QUESTIONS] # others: omitted
|
| 178 |
+
try:
|
| 179 |
+
res = ask(body["state"], qids, model)
|
| 180 |
+
except Exception as exc: # noqa: BLE001
|
| 181 |
+
return self._send(502, {"error": f"{type(exc).__name__}: {exc}"})
|
| 182 |
+
self._send(200, {"answers": {q: to_systemone(q, a) for q, a in res.items()}})
|
| 183 |
+
|
| 184 |
+
def log_message(self, *a: Any) -> None:
|
| 185 |
+
pass
|
| 186 |
+
|
| 187 |
+
print(f"Knack ({model} via {HOST}) on http://127.0.0.1:{port}/v1/systemone", flush=True)
|
| 188 |
+
ThreadingHTTPServer(("127.0.0.1", port), H).serve_forever()
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def main(argv: list[str] | None = None) -> int:
|
| 192 |
+
argv = sys.argv[1:] if argv is None else argv
|
| 193 |
+
if argv[:1] == ["serve"]:
|
| 194 |
+
sp = argparse.ArgumentParser(prog="knack.py serve")
|
| 195 |
+
sp.add_argument("--port", type=int, default=33727)
|
| 196 |
+
sp.add_argument("--model", default=MODEL)
|
| 197 |
+
a = sp.parse_args(argv[1:])
|
| 198 |
+
serve(a.port, a.model)
|
| 199 |
+
return 0
|
| 200 |
+
ap = argparse.ArgumentParser(description="Ask CodeSync Knack (via Ollama).")
|
| 201 |
+
ap.add_argument("text", help="a shell command, or with --turn a user message")
|
| 202 |
+
ap.add_argument("--turn", action="store_true", help="judge a user message, not a command")
|
| 203 |
+
ap.add_argument("--mode", default="build", help="current CodeSync mode for --turn")
|
| 204 |
+
ap.add_argument("--description", default="", help="what the command is for")
|
| 205 |
+
ap.add_argument("-q", "--question", action="append", help="only these question ids")
|
| 206 |
+
ap.add_argument("--model", default=MODEL)
|
| 207 |
+
ap.add_argument("--json", action="store_true")
|
| 208 |
+
a = ap.parse_args(argv)
|
| 209 |
+
state = turn_state(a.text, a.mode) if a.turn else bash_state(a.text, a.description)
|
| 210 |
+
qids = a.question or (TURN if a.turn else BASH)
|
| 211 |
+
try:
|
| 212 |
+
res = ask(state, qids, a.model)
|
| 213 |
+
except OSError as exc:
|
| 214 |
+
print(f"cannot reach Ollama at {HOST} ({exc}). Run: ollama pull {a.model}", file=sys.stderr)
|
| 215 |
+
return 1
|
| 216 |
+
if a.json:
|
| 217 |
+
print(json.dumps(res, indent=1))
|
| 218 |
+
return 0
|
| 219 |
+
w = max(map(len, qids))
|
| 220 |
+
for q, r in res.items():
|
| 221 |
+
print(f"{q:<{w}} {r['answer']:<13} {r['confidence']:.2f} {QUESTIONS[q][2]}")
|
| 222 |
+
return 0
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
if __name__ == "__main__":
|
| 226 |
+
sys.exit(main())
|
params
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"temperature": 0,
|
| 3 |
+
"num_predict": 1,
|
| 4 |
+
"num_ctx": 2048
|
| 5 |
+
}
|
template
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{{ .Prompt }}
|