quickthink 0.2.1__tar.gz → 0.2.2__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.
- {quickthink-0.2.1 → quickthink-0.2.2}/PKG-INFO +21 -12
- {quickthink-0.2.1 → quickthink-0.2.2}/README.md +19 -10
- {quickthink-0.2.1 → quickthink-0.2.2}/pyproject.toml +2 -2
- {quickthink-0.2.1 → quickthink-0.2.2}/src/quickthink/cli.py +77 -31
- {quickthink-0.2.1 → quickthink-0.2.2}/src/quickthink/engine.py +71 -7
- {quickthink-0.2.1 → quickthink-0.2.2}/src/quickthink.egg-info/PKG-INFO +21 -12
- {quickthink-0.2.1 → quickthink-0.2.2}/src/quickthink.egg-info/SOURCES.txt +1 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/src/quickthink.egg-info/requires.txt +0 -1
- quickthink-0.2.2/tests/test_cli_offline.py +118 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/LICENSE +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/setup.cfg +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/src/quickthink/__init__.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/src/quickthink/config.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/src/quickthink/inline_protocol.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/src/quickthink/ollama_client.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/src/quickthink/plan_grammar.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/src/quickthink/prompts.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/src/quickthink/routing.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/src/quickthink/ui_server.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/src/quickthink.egg-info/dependency_links.txt +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/src/quickthink.egg-info/entry_points.txt +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/src/quickthink.egg-info/top_level.txt +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/tests/test_cli_lane_policy.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/tests/test_cli_version.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/tests/test_engine_lane_policy.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/tests/test_eval_harness_safety.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/tests/test_inline_protocol.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/tests/test_make_gate_decision.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/tests/test_plan_grammar.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/tests/test_presets.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/tests/test_prompts.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/tests/test_routing.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/tests/test_ui_lane_policy.py +0 -0
- {quickthink-0.2.1 → quickthink-0.2.2}/tests/test_ui_server_paths.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quickthink
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Compressed planning scaffold for local LLMs — latency-aware routing and structured output reliability
|
|
5
5
|
Author-email: Rolando Bosch <roli@hermes-labs.ai>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -17,13 +17,13 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
22
|
Requires-Python: >=3.9
|
|
22
23
|
Description-Content-Type: text/markdown
|
|
23
24
|
License-File: LICENSE
|
|
24
25
|
Requires-Dist: httpx>=0.27.0
|
|
25
26
|
Requires-Dist: typer>=0.12.0
|
|
26
|
-
Requires-Dist: pydantic>=2.7.0
|
|
27
27
|
Provides-Extra: dev
|
|
28
28
|
Requires-Dist: pytest>=8.2.0; extra == "dev"
|
|
29
29
|
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
@@ -92,8 +92,8 @@ Small/local models are fast but often underperform on multi-step tasks.
|
|
|
92
92
|
Prerequisite: install and start [Ollama](https://ollama.com/) locally.
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
|
-
# 1)
|
|
96
|
-
python -m pip install "quickthink
|
|
95
|
+
# 1) Install the released package
|
|
96
|
+
python -m pip install "quickthink==0.2.2"
|
|
97
97
|
|
|
98
98
|
# 2) Pull one supported model
|
|
99
99
|
ollama pull qwen2.5:1.5b
|
|
@@ -104,12 +104,6 @@ quickthink ask "Give me a 3-step plan to learn SQL basics" --model qwen2.5:1.5b
|
|
|
104
104
|
|
|
105
105
|
If this command works, your local setup is ready.
|
|
106
106
|
|
|
107
|
-
After `v0.2.1` has been published to PyPI, install the release instead:
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
python -m pip install "quickthink==0.2.1"
|
|
111
|
-
```
|
|
112
|
-
|
|
113
107
|
For development, clone the repository and install the editable development extras:
|
|
114
108
|
|
|
115
109
|
```bash
|
|
@@ -199,6 +193,18 @@ Show routing diagnostics:
|
|
|
199
193
|
quickthink ask "Design a robust parser with tradeoffs and a JSON output schema" --show-route --show-plan
|
|
200
194
|
```
|
|
201
195
|
|
|
196
|
+
Skip planning entirely (`direct` mode):
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
quickthink ask "What is the capital of France?" --mode direct
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Inspect routing and the exact prompt(s) without calling Ollama (`--dry-run` works offline):
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
quickthink ask "Design a retry strategy for a flaky payments API: compare exponential backoff versus a circuit breaker, list the tradeoffs, and return a JSON schema for the config" --mode two_pass --dry-run
|
|
206
|
+
```
|
|
207
|
+
|
|
202
208
|
Optional continuity hint (tiny, off by default):
|
|
203
209
|
|
|
204
210
|
```bash
|
|
@@ -351,8 +357,9 @@ Test:
|
|
|
351
357
|
PYTHONPATH=src .venv/bin/pytest -q
|
|
352
358
|
```
|
|
353
359
|
|
|
354
|
-
Lint (
|
|
360
|
+
Lint (same commands as CI):
|
|
355
361
|
```bash
|
|
362
|
+
.venv/bin/ruff check src/ tests/ scripts/
|
|
356
363
|
python -m compileall src tests scripts
|
|
357
364
|
```
|
|
358
365
|
|
|
@@ -384,4 +391,6 @@ Apache-2.0
|
|
|
384
391
|
|
|
385
392
|
## About Hermes Labs
|
|
386
393
|
|
|
387
|
-
Hermes Labs is an
|
|
394
|
+
[Hermes Labs](https://hermes-labs.ai) is an AI reliability engineering studio for product and engineering teams shipping production agents and LLM applications. We find the structural AI failures standard evals miss, then harden retrieval, memory, agents, and the language layers around production AI systems with runtime controls and defensible evidence.
|
|
395
|
+
|
|
396
|
+
Browse the [open-source catalog](https://hermes-labs.ai/open-source) or contact [roli@hermes-labs.ai](mailto:roli@hermes-labs.ai).
|
|
@@ -61,8 +61,8 @@ Small/local models are fast but often underperform on multi-step tasks.
|
|
|
61
61
|
Prerequisite: install and start [Ollama](https://ollama.com/) locally.
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
# 1)
|
|
65
|
-
python -m pip install "quickthink
|
|
64
|
+
# 1) Install the released package
|
|
65
|
+
python -m pip install "quickthink==0.2.2"
|
|
66
66
|
|
|
67
67
|
# 2) Pull one supported model
|
|
68
68
|
ollama pull qwen2.5:1.5b
|
|
@@ -73,12 +73,6 @@ quickthink ask "Give me a 3-step plan to learn SQL basics" --model qwen2.5:1.5b
|
|
|
73
73
|
|
|
74
74
|
If this command works, your local setup is ready.
|
|
75
75
|
|
|
76
|
-
After `v0.2.1` has been published to PyPI, install the release instead:
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
python -m pip install "quickthink==0.2.1"
|
|
80
|
-
```
|
|
81
|
-
|
|
82
76
|
For development, clone the repository and install the editable development extras:
|
|
83
77
|
|
|
84
78
|
```bash
|
|
@@ -168,6 +162,18 @@ Show routing diagnostics:
|
|
|
168
162
|
quickthink ask "Design a robust parser with tradeoffs and a JSON output schema" --show-route --show-plan
|
|
169
163
|
```
|
|
170
164
|
|
|
165
|
+
Skip planning entirely (`direct` mode):
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
quickthink ask "What is the capital of France?" --mode direct
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Inspect routing and the exact prompt(s) without calling Ollama (`--dry-run` works offline):
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
quickthink ask "Design a retry strategy for a flaky payments API: compare exponential backoff versus a circuit breaker, list the tradeoffs, and return a JSON schema for the config" --mode two_pass --dry-run
|
|
175
|
+
```
|
|
176
|
+
|
|
171
177
|
Optional continuity hint (tiny, off by default):
|
|
172
178
|
|
|
173
179
|
```bash
|
|
@@ -320,8 +326,9 @@ Test:
|
|
|
320
326
|
PYTHONPATH=src .venv/bin/pytest -q
|
|
321
327
|
```
|
|
322
328
|
|
|
323
|
-
Lint (
|
|
329
|
+
Lint (same commands as CI):
|
|
324
330
|
```bash
|
|
331
|
+
.venv/bin/ruff check src/ tests/ scripts/
|
|
325
332
|
python -m compileall src tests scripts
|
|
326
333
|
```
|
|
327
334
|
|
|
@@ -353,4 +360,6 @@ Apache-2.0
|
|
|
353
360
|
|
|
354
361
|
## About Hermes Labs
|
|
355
362
|
|
|
356
|
-
Hermes Labs is an
|
|
363
|
+
[Hermes Labs](https://hermes-labs.ai) is an AI reliability engineering studio for product and engineering teams shipping production agents and LLM applications. We find the structural AI failures standard evals miss, then harden retrieval, memory, agents, and the language layers around production AI systems with runtime controls and defensible evidence.
|
|
364
|
+
|
|
365
|
+
Browse the [open-source catalog](https://hermes-labs.ai/open-source) or contact [roli@hermes-labs.ai](mailto:roli@hermes-labs.ai).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "quickthink"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "Compressed planning scaffold for local LLMs — latency-aware routing and structured output reliability"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.9"
|
|
@@ -29,12 +29,12 @@ classifiers = [
|
|
|
29
29
|
"Programming Language :: Python :: 3.10",
|
|
30
30
|
"Programming Language :: Python :: 3.11",
|
|
31
31
|
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
32
33
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
33
34
|
]
|
|
34
35
|
dependencies = [
|
|
35
36
|
"httpx>=0.27.0",
|
|
36
37
|
"typer>=0.12.0",
|
|
37
|
-
"pydantic>=2.7.0",
|
|
38
38
|
]
|
|
39
39
|
|
|
40
40
|
[project.urls]
|
|
@@ -4,8 +4,9 @@ import json
|
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
5
|
from importlib.metadata import version as package_version
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Optional
|
|
7
|
+
from typing import NoReturn, Optional
|
|
8
8
|
|
|
9
|
+
import httpx
|
|
9
10
|
import typer
|
|
10
11
|
|
|
11
12
|
from .config import MODEL_PROFILES, PRESET_PROFILES, SUPPORTED_MODELS, QuickThinkConfig
|
|
@@ -14,6 +15,8 @@ from .ui_server import serve_ui
|
|
|
14
15
|
|
|
15
16
|
app = typer.Typer(help="Compressed planning scaffold for local LLMs")
|
|
16
17
|
|
|
18
|
+
MODES = ("lite", "two_pass", "direct")
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
def _version_callback(value: bool) -> None:
|
|
19
22
|
if value:
|
|
@@ -57,7 +60,7 @@ def ask(
|
|
|
57
60
|
prompt: str = typer.Argument(..., help="User prompt"),
|
|
58
61
|
model: str = typer.Option("qwen2.5:1.5b", help="Ollama model"),
|
|
59
62
|
ollama_url: str = typer.Option("http://localhost:11434", help="Ollama base URL"),
|
|
60
|
-
mode: str = typer.Option("lite", help="Execution mode: lite or
|
|
63
|
+
mode: str = typer.Option("lite", help="Execution mode: lite, two_pass, or direct"),
|
|
61
64
|
preset: str = typer.Option("balanced", help="Preset profile: fast, balanced, strict"),
|
|
62
65
|
show_plan: bool = typer.Option(False, help="Show compressed plan in terminal output"),
|
|
63
66
|
show_route: bool = typer.Option(False, help="Show routing diagnostics"),
|
|
@@ -65,9 +68,14 @@ def ask(
|
|
|
65
68
|
bypass_short_prompts: bool = typer.Option(True, help="Skip plan stage for short prompts"),
|
|
66
69
|
continuity_hint: Optional[str] = typer.Option(None, help="Optional tiny continuity hint"),
|
|
67
70
|
lane_policy: str = typer.Option("default", help="Lane policy: default or strict_safe"),
|
|
71
|
+
dry_run: bool = typer.Option(
|
|
72
|
+
False,
|
|
73
|
+
"--dry-run",
|
|
74
|
+
help="Resolve routing and print the prompt(s) that would be sent to Ollama, without calling it",
|
|
75
|
+
),
|
|
68
76
|
) -> None:
|
|
69
|
-
if mode not in
|
|
70
|
-
raise typer.BadParameter("mode must be
|
|
77
|
+
if mode not in MODES:
|
|
78
|
+
raise typer.BadParameter("mode must be one of: lite, two_pass, direct")
|
|
71
79
|
if preset not in PRESET_PROFILES:
|
|
72
80
|
raise typer.BadParameter("preset must be one of: fast, balanced, strict")
|
|
73
81
|
if lane_policy not in {"default", "strict_safe"}:
|
|
@@ -80,7 +88,21 @@ def ask(
|
|
|
80
88
|
config.lane_policy = lane_policy
|
|
81
89
|
engine = QuickThinkEngine(config)
|
|
82
90
|
|
|
83
|
-
|
|
91
|
+
if dry_run:
|
|
92
|
+
preview = engine.preview(prompt)
|
|
93
|
+
typer.echo(
|
|
94
|
+
f"[route] mode={preview.mode} bypassed={preview.bypassed} score={preview.route_score} "
|
|
95
|
+
f"plan_budget={preview.selected_plan_budget} model_calls={preview.model_calls} model={model}"
|
|
96
|
+
)
|
|
97
|
+
for stage, text in preview.prompts:
|
|
98
|
+
typer.echo(f"[prompt:{stage}]")
|
|
99
|
+
typer.echo(text)
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
result = engine.run(prompt)
|
|
104
|
+
except httpx.HTTPError as exc:
|
|
105
|
+
_ollama_error(exc, ollama_url=ollama_url, model=model)
|
|
84
106
|
|
|
85
107
|
if show_route:
|
|
86
108
|
typer.echo(
|
|
@@ -117,6 +139,42 @@ def ask(
|
|
|
117
139
|
)
|
|
118
140
|
|
|
119
141
|
|
|
142
|
+
def _ollama_error(exc: httpx.HTTPError, *, ollama_url: str, model: str) -> NoReturn:
|
|
143
|
+
typer.echo(
|
|
144
|
+
f"error: Ollama request to {ollama_url} failed ({exc.__class__.__name__}: {exc}).\n"
|
|
145
|
+
f"Start Ollama with `ollama serve` and make sure the model is available with `ollama pull {model}`.",
|
|
146
|
+
err=True,
|
|
147
|
+
)
|
|
148
|
+
raise typer.Exit(code=2)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _bench_modes(
|
|
152
|
+
*,
|
|
153
|
+
prompt: str,
|
|
154
|
+
model: str,
|
|
155
|
+
ollama_url: str,
|
|
156
|
+
runs: int,
|
|
157
|
+
preset: str,
|
|
158
|
+
lane_policy: str,
|
|
159
|
+
lite_latencies: list[float],
|
|
160
|
+
two_pass_latencies: list[float],
|
|
161
|
+
direct_latencies: list[float],
|
|
162
|
+
) -> None:
|
|
163
|
+
for mode, latencies in (("lite", lite_latencies), ("two_pass", two_pass_latencies), ("direct", direct_latencies)):
|
|
164
|
+
config = QuickThinkConfig.with_model_profile(model=model, ollama_url=ollama_url)
|
|
165
|
+
config.apply_preset(preset)
|
|
166
|
+
config.mode = mode
|
|
167
|
+
# Benchmark the configured mode itself: without this, prompts below the preset's
|
|
168
|
+
# bypass threshold or complexity score would silently measure the direct path under
|
|
169
|
+
# a lite/two_pass label. `strict_safe` keeps its own strict-format bypass.
|
|
170
|
+
config.bypass_short_prompts = False
|
|
171
|
+
config.adaptive_routing = False
|
|
172
|
+
config.lane_policy = lane_policy
|
|
173
|
+
engine = QuickThinkEngine(config)
|
|
174
|
+
for _ in range(runs):
|
|
175
|
+
latencies.append(engine.run(prompt).total_latency_ms)
|
|
176
|
+
|
|
177
|
+
|
|
120
178
|
@app.command()
|
|
121
179
|
def bench(
|
|
122
180
|
prompt: str = typer.Argument(..., help="Benchmark prompt"),
|
|
@@ -134,32 +192,20 @@ def bench(
|
|
|
134
192
|
two_pass_latencies: list[float] = []
|
|
135
193
|
direct_latencies: list[float] = []
|
|
136
194
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
two_pass_latencies.append(engine_two_pass.run(prompt).total_latency_ms)
|
|
152
|
-
|
|
153
|
-
config_direct = QuickThinkConfig.with_model_profile(model=model, ollama_url=ollama_url)
|
|
154
|
-
config_direct.apply_preset(preset)
|
|
155
|
-
config_direct.mode = "lite"
|
|
156
|
-
config_direct.lane_policy = lane_policy
|
|
157
|
-
config_direct.bypass_short_prompts = True
|
|
158
|
-
config_direct.adaptive_routing = False
|
|
159
|
-
config_direct.bypass_char_threshold = 100_000
|
|
160
|
-
engine_direct = QuickThinkEngine(config_direct)
|
|
161
|
-
for _ in range(runs):
|
|
162
|
-
direct_latencies.append(engine_direct.run(prompt).total_latency_ms)
|
|
195
|
+
try:
|
|
196
|
+
_bench_modes(
|
|
197
|
+
prompt=prompt,
|
|
198
|
+
model=model,
|
|
199
|
+
ollama_url=ollama_url,
|
|
200
|
+
runs=runs,
|
|
201
|
+
preset=preset,
|
|
202
|
+
lane_policy=lane_policy,
|
|
203
|
+
lite_latencies=lite_latencies,
|
|
204
|
+
two_pass_latencies=two_pass_latencies,
|
|
205
|
+
direct_latencies=direct_latencies,
|
|
206
|
+
)
|
|
207
|
+
except httpx.HTTPError as exc:
|
|
208
|
+
_ollama_error(exc, ollama_url=ollama_url, model=model)
|
|
163
209
|
|
|
164
210
|
avg_lite = sum(lite_latencies) / len(lite_latencies)
|
|
165
211
|
avg_two_pass = sum(two_pass_latencies) / len(two_pass_latencies)
|
|
@@ -33,23 +33,87 @@ class QuickThinkResult:
|
|
|
33
33
|
return self.plan_latency_ms + self.answer_latency_ms
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
@dataclass
|
|
37
|
+
class QuickThinkPreview:
|
|
38
|
+
"""Routing decision plus the prompt(s) that would be sent, produced without a model call."""
|
|
39
|
+
|
|
40
|
+
mode: str
|
|
41
|
+
bypassed: bool
|
|
42
|
+
route_score: int
|
|
43
|
+
selected_plan_budget: int
|
|
44
|
+
prompts: list[tuple[str, str]]
|
|
45
|
+
model_calls_min: int
|
|
46
|
+
model_calls_max: int
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def model_calls(self) -> str:
|
|
50
|
+
"""Human-readable call count, e.g. ``"1"`` or ``"2-3"`` when a plan repair call may occur."""
|
|
51
|
+
if self.model_calls_min == self.model_calls_max:
|
|
52
|
+
return str(self.model_calls_min)
|
|
53
|
+
return f"{self.model_calls_min}-{self.model_calls_max}"
|
|
54
|
+
|
|
55
|
+
|
|
36
56
|
class QuickThinkEngine:
|
|
37
57
|
def __init__(self, config: QuickThinkConfig) -> None:
|
|
38
58
|
self.config = config
|
|
39
59
|
self.client = OllamaClient(config.ollama_url, timeout_s=config.request_timeout_s)
|
|
40
60
|
|
|
41
|
-
def
|
|
61
|
+
def _resolve_route(self, prompt: str) -> tuple[bool, int, int]:
|
|
62
|
+
"""Return (bypass, route_score, selected_budget) without contacting the model."""
|
|
42
63
|
if self.config.lane_policy == "strict_safe" and infer_task_class(prompt) == "strict_format":
|
|
43
|
-
return
|
|
64
|
+
return True, -1, self.config.min_plan_budget_tokens
|
|
65
|
+
return should_bypass(prompt, self.config)
|
|
66
|
+
|
|
67
|
+
def preview(self, prompt: str) -> QuickThinkPreview:
|
|
68
|
+
"""Resolve routing and build the exact prompt(s) that ``run`` would send, without calling Ollama."""
|
|
69
|
+
bypass, route_score, selected_budget = self._resolve_route(prompt)
|
|
70
|
+
if bypass or self.config.mode == "direct":
|
|
71
|
+
return QuickThinkPreview(
|
|
72
|
+
mode=self.config.mode,
|
|
73
|
+
bypassed=True,
|
|
74
|
+
route_score=route_score,
|
|
75
|
+
selected_plan_budget=selected_budget,
|
|
76
|
+
prompts=[("answer", prompt)],
|
|
77
|
+
model_calls_min=1,
|
|
78
|
+
model_calls_max=1,
|
|
79
|
+
)
|
|
80
|
+
if self.config.mode == "two_pass":
|
|
81
|
+
# The answer prompt embeds the plan returned by the first call, so it is shown as a
|
|
82
|
+
# template; an invalid first plan triggers one extra repair call before the answer.
|
|
83
|
+
prompts = [
|
|
84
|
+
("plan", make_plan_prompt(prompt, selected_budget)),
|
|
85
|
+
("answer-template", make_answer_prompt(prompt, "<plan from the first call>")),
|
|
86
|
+
]
|
|
87
|
+
calls_min, calls_max = 2, 3
|
|
88
|
+
else:
|
|
89
|
+
prompts = [
|
|
90
|
+
(
|
|
91
|
+
"plan+answer",
|
|
92
|
+
make_inline_plan_answer_prompt(
|
|
93
|
+
prompt,
|
|
94
|
+
selected_budget,
|
|
95
|
+
continuity_hint=self.config.continuity_hint,
|
|
96
|
+
scaffold_rules=self.config.scaffold_rules,
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
]
|
|
100
|
+
calls_min, calls_max = 1, 1
|
|
101
|
+
return QuickThinkPreview(
|
|
102
|
+
mode=self.config.mode,
|
|
103
|
+
bypassed=False,
|
|
104
|
+
route_score=route_score,
|
|
105
|
+
selected_plan_budget=selected_budget,
|
|
106
|
+
prompts=prompts,
|
|
107
|
+
model_calls_min=calls_min,
|
|
108
|
+
model_calls_max=calls_max,
|
|
109
|
+
)
|
|
44
110
|
|
|
45
|
-
|
|
46
|
-
|
|
111
|
+
def run(self, prompt: str) -> QuickThinkResult:
|
|
112
|
+
bypass, route_score, selected_budget = self._resolve_route(prompt)
|
|
113
|
+
if bypass or self.config.mode == "direct":
|
|
47
114
|
return self._run_direct(prompt=prompt, route_score=route_score, selected_budget=selected_budget)
|
|
48
|
-
|
|
49
115
|
if self.config.mode == "two_pass":
|
|
50
116
|
return self._run_two_pass(prompt, route_score, selected_budget)
|
|
51
|
-
if self.config.mode == "direct":
|
|
52
|
-
return self._run_direct(prompt=prompt, route_score=route_score, selected_budget=selected_budget)
|
|
53
117
|
return self._run_lite(prompt, route_score, selected_budget)
|
|
54
118
|
|
|
55
119
|
def _run_direct(self, prompt: str, route_score: int, selected_budget: int) -> QuickThinkResult:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quickthink
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Compressed planning scaffold for local LLMs — latency-aware routing and structured output reliability
|
|
5
5
|
Author-email: Rolando Bosch <roli@hermes-labs.ai>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -17,13 +17,13 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
22
|
Requires-Python: >=3.9
|
|
22
23
|
Description-Content-Type: text/markdown
|
|
23
24
|
License-File: LICENSE
|
|
24
25
|
Requires-Dist: httpx>=0.27.0
|
|
25
26
|
Requires-Dist: typer>=0.12.0
|
|
26
|
-
Requires-Dist: pydantic>=2.7.0
|
|
27
27
|
Provides-Extra: dev
|
|
28
28
|
Requires-Dist: pytest>=8.2.0; extra == "dev"
|
|
29
29
|
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
@@ -92,8 +92,8 @@ Small/local models are fast but often underperform on multi-step tasks.
|
|
|
92
92
|
Prerequisite: install and start [Ollama](https://ollama.com/) locally.
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
|
-
# 1)
|
|
96
|
-
python -m pip install "quickthink
|
|
95
|
+
# 1) Install the released package
|
|
96
|
+
python -m pip install "quickthink==0.2.2"
|
|
97
97
|
|
|
98
98
|
# 2) Pull one supported model
|
|
99
99
|
ollama pull qwen2.5:1.5b
|
|
@@ -104,12 +104,6 @@ quickthink ask "Give me a 3-step plan to learn SQL basics" --model qwen2.5:1.5b
|
|
|
104
104
|
|
|
105
105
|
If this command works, your local setup is ready.
|
|
106
106
|
|
|
107
|
-
After `v0.2.1` has been published to PyPI, install the release instead:
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
python -m pip install "quickthink==0.2.1"
|
|
111
|
-
```
|
|
112
|
-
|
|
113
107
|
For development, clone the repository and install the editable development extras:
|
|
114
108
|
|
|
115
109
|
```bash
|
|
@@ -199,6 +193,18 @@ Show routing diagnostics:
|
|
|
199
193
|
quickthink ask "Design a robust parser with tradeoffs and a JSON output schema" --show-route --show-plan
|
|
200
194
|
```
|
|
201
195
|
|
|
196
|
+
Skip planning entirely (`direct` mode):
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
quickthink ask "What is the capital of France?" --mode direct
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Inspect routing and the exact prompt(s) without calling Ollama (`--dry-run` works offline):
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
quickthink ask "Design a retry strategy for a flaky payments API: compare exponential backoff versus a circuit breaker, list the tradeoffs, and return a JSON schema for the config" --mode two_pass --dry-run
|
|
206
|
+
```
|
|
207
|
+
|
|
202
208
|
Optional continuity hint (tiny, off by default):
|
|
203
209
|
|
|
204
210
|
```bash
|
|
@@ -351,8 +357,9 @@ Test:
|
|
|
351
357
|
PYTHONPATH=src .venv/bin/pytest -q
|
|
352
358
|
```
|
|
353
359
|
|
|
354
|
-
Lint (
|
|
360
|
+
Lint (same commands as CI):
|
|
355
361
|
```bash
|
|
362
|
+
.venv/bin/ruff check src/ tests/ scripts/
|
|
356
363
|
python -m compileall src tests scripts
|
|
357
364
|
```
|
|
358
365
|
|
|
@@ -384,4 +391,6 @@ Apache-2.0
|
|
|
384
391
|
|
|
385
392
|
## About Hermes Labs
|
|
386
393
|
|
|
387
|
-
Hermes Labs is an
|
|
394
|
+
[Hermes Labs](https://hermes-labs.ai) is an AI reliability engineering studio for product and engineering teams shipping production agents and LLM applications. We find the structural AI failures standard evals miss, then harden retrieval, memory, agents, and the language layers around production AI systems with runtime controls and defensible evidence.
|
|
395
|
+
|
|
396
|
+
Browse the [open-source catalog](https://hermes-labs.ai/open-source) or contact [roli@hermes-labs.ai](mailto:roli@hermes-labs.ai).
|
|
@@ -18,6 +18,7 @@ src/quickthink.egg-info/entry_points.txt
|
|
|
18
18
|
src/quickthink.egg-info/requires.txt
|
|
19
19
|
src/quickthink.egg-info/top_level.txt
|
|
20
20
|
tests/test_cli_lane_policy.py
|
|
21
|
+
tests/test_cli_offline.py
|
|
21
22
|
tests/test_cli_version.py
|
|
22
23
|
tests/test_engine_lane_policy.py
|
|
23
24
|
tests/test_eval_harness_safety.py
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""CLI behaviour that must work without a running Ollama server."""
|
|
2
|
+
|
|
3
|
+
from typer.testing import CliRunner
|
|
4
|
+
|
|
5
|
+
from quickthink.cli import app
|
|
6
|
+
from quickthink.config import QuickThinkConfig
|
|
7
|
+
from quickthink.engine import QuickThinkEngine
|
|
8
|
+
|
|
9
|
+
# Port 9 (discard) is closed on every CI runner, so a connection is refused immediately.
|
|
10
|
+
UNREACHABLE = "http://127.0.0.1:9"
|
|
11
|
+
|
|
12
|
+
# Longer than the balanced preset's 120-char bypass threshold and scores >1 on the routing regexes,
|
|
13
|
+
# so it takes the scaffold path instead of the direct path.
|
|
14
|
+
SCAFFOLD_PROMPT = (
|
|
15
|
+
"Design a retry strategy for a flaky payments API: compare exponential backoff versus a circuit "
|
|
16
|
+
"breaker, list the tradeoffs, and return a JSON schema for the config"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_help_runs_without_ollama() -> None:
|
|
21
|
+
result = CliRunner().invoke(app, ["--help"])
|
|
22
|
+
assert result.exit_code == 0
|
|
23
|
+
assert "ask" in result.output
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_ask_accepts_direct_mode_and_dry_run_makes_no_network_call() -> None:
|
|
27
|
+
result = CliRunner().invoke(
|
|
28
|
+
app,
|
|
29
|
+
["ask", "what is 2 + 2?", "--mode", "direct", "--ollama-url", UNREACHABLE, "--dry-run"],
|
|
30
|
+
)
|
|
31
|
+
assert result.exit_code == 0, result.output
|
|
32
|
+
assert "[route] mode=direct bypassed=True" in result.output
|
|
33
|
+
assert "model_calls=1" in result.output
|
|
34
|
+
assert "[prompt:answer]\nwhat is 2 + 2?" in result.output
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_dry_run_lite_shows_scaffold_prompt() -> None:
|
|
38
|
+
prompt = SCAFFOLD_PROMPT
|
|
39
|
+
result = CliRunner().invoke(app, ["ask", prompt, "--ollama-url", UNREACHABLE, "--dry-run"])
|
|
40
|
+
assert result.exit_code == 0, result.output
|
|
41
|
+
assert "[route] mode=lite bypassed=False" in result.output
|
|
42
|
+
assert "[prompt:plan+answer]" in result.output
|
|
43
|
+
assert "Generate a compact internal plan prefix" in result.output
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_dry_run_two_pass_lists_both_calls() -> None:
|
|
47
|
+
prompt = SCAFFOLD_PROMPT
|
|
48
|
+
result = CliRunner().invoke(
|
|
49
|
+
app, ["ask", prompt, "--mode", "two_pass", "--ollama-url", UNREACHABLE, "--dry-run"]
|
|
50
|
+
)
|
|
51
|
+
assert result.exit_code == 0, result.output
|
|
52
|
+
assert "model_calls=2-3" in result.output
|
|
53
|
+
assert "[prompt:plan]" in result.output
|
|
54
|
+
assert "[prompt:answer-template]" in result.output
|
|
55
|
+
assert "<plan from the first call>" in result.output
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_ask_invalid_mode_rejected() -> None:
|
|
59
|
+
result = CliRunner().invoke(app, ["ask", "hello", "--mode", "turbo"])
|
|
60
|
+
assert result.exit_code != 0
|
|
61
|
+
assert "mode must be one of: lite, two_pass, direct" in result.output
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_ask_reports_unreachable_ollama_cleanly() -> None:
|
|
65
|
+
result = CliRunner().invoke(app, ["ask", "hello", "--ollama-url", UNREACHABLE])
|
|
66
|
+
assert result.exit_code == 2
|
|
67
|
+
assert result.exception is None or isinstance(result.exception, SystemExit)
|
|
68
|
+
assert "error: Ollama request to http://127.0.0.1:9 failed" in result.output
|
|
69
|
+
assert "ollama serve" in result.output
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_engine_preview_matches_run_routing(monkeypatch) -> None:
|
|
73
|
+
cfg = QuickThinkConfig.with_model_profile("qwen2.5:1.5b")
|
|
74
|
+
cfg.mode = "lite"
|
|
75
|
+
cfg.bypass_short_prompts = False
|
|
76
|
+
cfg.adaptive_routing = False
|
|
77
|
+
engine = QuickThinkEngine(cfg)
|
|
78
|
+
|
|
79
|
+
sent: list[str] = []
|
|
80
|
+
|
|
81
|
+
def fake_generate(**kwargs): # type: ignore[no-untyped-def]
|
|
82
|
+
sent.append(str(kwargs.get("prompt", "")))
|
|
83
|
+
return {"response": "[P]g:x;c:y;s:z;r:k\n[A]ok"}
|
|
84
|
+
|
|
85
|
+
monkeypatch.setattr(engine.client, "generate", fake_generate)
|
|
86
|
+
|
|
87
|
+
preview = engine.preview("Compare two retry strategies")
|
|
88
|
+
result = engine.run("Compare two retry strategies")
|
|
89
|
+
|
|
90
|
+
assert preview.bypassed is result.bypassed
|
|
91
|
+
assert preview.route_score == result.route_score
|
|
92
|
+
assert preview.selected_plan_budget == result.selected_plan_budget
|
|
93
|
+
assert [text for _, text in preview.prompts] == sent
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_bench_forces_planned_modes(monkeypatch) -> None:
|
|
97
|
+
"""A short prompt must still be measured under lite and two_pass, not bypassed to direct."""
|
|
98
|
+
from quickthink import cli
|
|
99
|
+
|
|
100
|
+
seen: list[tuple[str, bool]] = []
|
|
101
|
+
|
|
102
|
+
class FakeEngine:
|
|
103
|
+
def __init__(self, config: QuickThinkConfig) -> None:
|
|
104
|
+
self.config = config
|
|
105
|
+
|
|
106
|
+
def run(self, prompt: str): # type: ignore[no-untyped-def]
|
|
107
|
+
real = QuickThinkEngine(self.config).preview(prompt)
|
|
108
|
+
seen.append((self.config.mode, real.bypassed))
|
|
109
|
+
|
|
110
|
+
class R:
|
|
111
|
+
total_latency_ms = 1.0
|
|
112
|
+
|
|
113
|
+
return R()
|
|
114
|
+
|
|
115
|
+
monkeypatch.setattr(cli, "QuickThinkEngine", FakeEngine)
|
|
116
|
+
result = CliRunner().invoke(app, ["bench", "what is 2 + 2?", "--runs", "1"])
|
|
117
|
+
assert result.exit_code == 0, result.output
|
|
118
|
+
assert seen == [("lite", False), ("two_pass", False), ("direct", True)]
|
|
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
|