quickthink 0.2.1__py3-none-any.whl
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/__init__.py +6 -0
- quickthink/cli.py +187 -0
- quickthink/config.py +108 -0
- quickthink/engine.py +174 -0
- quickthink/inline_protocol.py +18 -0
- quickthink/ollama_client.py +38 -0
- quickthink/plan_grammar.py +24 -0
- quickthink/prompts.py +73 -0
- quickthink/routing.py +65 -0
- quickthink/ui_server.py +1465 -0
- quickthink-0.2.1.dist-info/METADATA +387 -0
- quickthink-0.2.1.dist-info/RECORD +16 -0
- quickthink-0.2.1.dist-info/WHEEL +5 -0
- quickthink-0.2.1.dist-info/entry_points.txt +2 -0
- quickthink-0.2.1.dist-info/licenses/LICENSE +176 -0
- quickthink-0.2.1.dist-info/top_level.txt +1 -0
quickthink/__init__.py
ADDED
quickthink/cli.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from importlib.metadata import version as package_version
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from .config import MODEL_PROFILES, PRESET_PROFILES, SUPPORTED_MODELS, QuickThinkConfig
|
|
12
|
+
from .engine import QuickThinkEngine
|
|
13
|
+
from .ui_server import serve_ui
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="Compressed planning scaffold for local LLMs")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _version_callback(value: bool) -> None:
|
|
19
|
+
if value:
|
|
20
|
+
typer.echo(package_version("quickthink"))
|
|
21
|
+
raise typer.Exit()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.callback()
|
|
25
|
+
def main(
|
|
26
|
+
version: bool = typer.Option(
|
|
27
|
+
False,
|
|
28
|
+
"--version",
|
|
29
|
+
callback=_version_callback,
|
|
30
|
+
is_eager=True,
|
|
31
|
+
help="Show the installed quickthink version and exit.",
|
|
32
|
+
),
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Compressed planning scaffold for local LLMs."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.command()
|
|
38
|
+
def list_models() -> None:
|
|
39
|
+
for model, profile in MODEL_PROFILES.items():
|
|
40
|
+
typer.echo(f"{model} -> {json.dumps(profile)}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command()
|
|
44
|
+
def list_presets() -> None:
|
|
45
|
+
for preset, profile in PRESET_PROFILES.items():
|
|
46
|
+
typer.echo(f"{preset} -> {json.dumps(profile)}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.command()
|
|
50
|
+
def compatibility() -> None:
|
|
51
|
+
for model in SUPPORTED_MODELS:
|
|
52
|
+
typer.echo(model)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command()
|
|
56
|
+
def ask(
|
|
57
|
+
prompt: str = typer.Argument(..., help="User prompt"),
|
|
58
|
+
model: str = typer.Option("qwen2.5:1.5b", help="Ollama model"),
|
|
59
|
+
ollama_url: str = typer.Option("http://localhost:11434", help="Ollama base URL"),
|
|
60
|
+
mode: str = typer.Option("lite", help="Execution mode: lite or two_pass"),
|
|
61
|
+
preset: str = typer.Option("balanced", help="Preset profile: fast, balanced, strict"),
|
|
62
|
+
show_plan: bool = typer.Option(False, help="Show compressed plan in terminal output"),
|
|
63
|
+
show_route: bool = typer.Option(False, help="Show routing diagnostics"),
|
|
64
|
+
log_file: Optional[Path] = typer.Option(None, help="Optional JSONL log file"),
|
|
65
|
+
bypass_short_prompts: bool = typer.Option(True, help="Skip plan stage for short prompts"),
|
|
66
|
+
continuity_hint: Optional[str] = typer.Option(None, help="Optional tiny continuity hint"),
|
|
67
|
+
lane_policy: str = typer.Option("default", help="Lane policy: default or strict_safe"),
|
|
68
|
+
) -> None:
|
|
69
|
+
if mode not in {"lite", "two_pass"}:
|
|
70
|
+
raise typer.BadParameter("mode must be 'lite' or 'two_pass'")
|
|
71
|
+
if preset not in PRESET_PROFILES:
|
|
72
|
+
raise typer.BadParameter("preset must be one of: fast, balanced, strict")
|
|
73
|
+
if lane_policy not in {"default", "strict_safe"}:
|
|
74
|
+
raise typer.BadParameter("lane-policy must be 'default' or 'strict_safe'")
|
|
75
|
+
config = QuickThinkConfig.with_model_profile(model=model, ollama_url=ollama_url)
|
|
76
|
+
config.apply_preset(preset)
|
|
77
|
+
config.bypass_short_prompts = bypass_short_prompts
|
|
78
|
+
config.mode = mode
|
|
79
|
+
config.continuity_hint = continuity_hint
|
|
80
|
+
config.lane_policy = lane_policy
|
|
81
|
+
engine = QuickThinkEngine(config)
|
|
82
|
+
|
|
83
|
+
result = engine.run(prompt)
|
|
84
|
+
|
|
85
|
+
if show_route:
|
|
86
|
+
typer.echo(
|
|
87
|
+
f"[route] mode={result.mode} bypassed={result.bypassed} score={result.route_score} "
|
|
88
|
+
f"plan_budget={result.selected_plan_budget} repaired={result.plan_repaired}"
|
|
89
|
+
)
|
|
90
|
+
if show_plan and result.plan:
|
|
91
|
+
typer.echo(f"[plan] {result.plan}")
|
|
92
|
+
typer.echo(result.answer)
|
|
93
|
+
|
|
94
|
+
if log_file:
|
|
95
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
with log_file.open("a", encoding="utf-8") as fh:
|
|
97
|
+
fh.write(
|
|
98
|
+
json.dumps(
|
|
99
|
+
{
|
|
100
|
+
"prompt": prompt,
|
|
101
|
+
"model": model,
|
|
102
|
+
"preset": config.preset,
|
|
103
|
+
"mode": result.mode,
|
|
104
|
+
"timestamp": datetime.now(timezone.utc).replace(microsecond=0).isoformat(),
|
|
105
|
+
"answer": result.answer,
|
|
106
|
+
"plan": result.plan,
|
|
107
|
+
"bypassed": result.bypassed,
|
|
108
|
+
"route_score": result.route_score,
|
|
109
|
+
"selected_plan_budget": result.selected_plan_budget,
|
|
110
|
+
"plan_repaired": result.plan_repaired,
|
|
111
|
+
"plan_latency_ms": round(result.plan_latency_ms, 2),
|
|
112
|
+
"answer_latency_ms": round(result.answer_latency_ms, 2),
|
|
113
|
+
"total_latency_ms": round(result.total_latency_ms, 2),
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
+ "\n"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@app.command()
|
|
121
|
+
def bench(
|
|
122
|
+
prompt: str = typer.Argument(..., help="Benchmark prompt"),
|
|
123
|
+
model: str = typer.Option("qwen2.5:1.5b", help="Ollama model"),
|
|
124
|
+
ollama_url: str = typer.Option("http://localhost:11434", help="Ollama base URL"),
|
|
125
|
+
runs: int = typer.Option(3, min=1, max=20, help="Number of runs per mode"),
|
|
126
|
+
preset: str = typer.Option("balanced", help="Preset profile: fast, balanced, strict"),
|
|
127
|
+
lane_policy: str = typer.Option("default", help="Lane policy: default or strict_safe"),
|
|
128
|
+
) -> None:
|
|
129
|
+
if preset not in PRESET_PROFILES:
|
|
130
|
+
raise typer.BadParameter("preset must be one of: fast, balanced, strict")
|
|
131
|
+
if lane_policy not in {"default", "strict_safe"}:
|
|
132
|
+
raise typer.BadParameter("lane-policy must be 'default' or 'strict_safe'")
|
|
133
|
+
lite_latencies: list[float] = []
|
|
134
|
+
two_pass_latencies: list[float] = []
|
|
135
|
+
direct_latencies: list[float] = []
|
|
136
|
+
|
|
137
|
+
config_lite = QuickThinkConfig.with_model_profile(model=model, ollama_url=ollama_url)
|
|
138
|
+
config_lite.apply_preset(preset)
|
|
139
|
+
config_lite.mode = "lite"
|
|
140
|
+
config_lite.lane_policy = lane_policy
|
|
141
|
+
engine_lite = QuickThinkEngine(config_lite)
|
|
142
|
+
for _ in range(runs):
|
|
143
|
+
lite_latencies.append(engine_lite.run(prompt).total_latency_ms)
|
|
144
|
+
|
|
145
|
+
config_two_pass = QuickThinkConfig.with_model_profile(model=model, ollama_url=ollama_url)
|
|
146
|
+
config_two_pass.apply_preset(preset)
|
|
147
|
+
config_two_pass.mode = "two_pass"
|
|
148
|
+
config_two_pass.lane_policy = lane_policy
|
|
149
|
+
engine_two_pass = QuickThinkEngine(config_two_pass)
|
|
150
|
+
for _ in range(runs):
|
|
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)
|
|
163
|
+
|
|
164
|
+
avg_lite = sum(lite_latencies) / len(lite_latencies)
|
|
165
|
+
avg_two_pass = sum(two_pass_latencies) / len(two_pass_latencies)
|
|
166
|
+
avg_direct = sum(direct_latencies) / len(direct_latencies)
|
|
167
|
+
|
|
168
|
+
typer.echo(f"model={model}")
|
|
169
|
+
typer.echo(f"preset={preset}")
|
|
170
|
+
typer.echo(f"avg_lite_ms={avg_lite:.2f}")
|
|
171
|
+
typer.echo(f"avg_two_pass_ms={avg_two_pass:.2f}")
|
|
172
|
+
typer.echo(f"avg_direct_ms={avg_direct:.2f}")
|
|
173
|
+
typer.echo(f"lite_overhead_ms={avg_lite-avg_direct:.2f}")
|
|
174
|
+
typer.echo(f"two_pass_overhead_ms={avg_two_pass-avg_direct:.2f}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.command()
|
|
178
|
+
def ui(
|
|
179
|
+
host: str = typer.Option("127.0.0.1", help="Bind host"),
|
|
180
|
+
port: int = typer.Option(7860, min=1, max=65535, help="Bind port"),
|
|
181
|
+
open_browser: bool = typer.Option(True, help="Open UI in browser on startup"),
|
|
182
|
+
) -> None:
|
|
183
|
+
serve_ui(host=host, port=port, open_browser=open_browser)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
app()
|
quickthink/config.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
SUPPORTED_MODELS = (
|
|
6
|
+
"qwen2.5:1.5b",
|
|
7
|
+
"mistral:7b",
|
|
8
|
+
"gemma3:27b",
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
MODEL_PROFILES = {
|
|
12
|
+
"qwen2.5:1.5b": {
|
|
13
|
+
"plan_budget_tokens": 8,
|
|
14
|
+
"min_plan_budget_tokens": 6,
|
|
15
|
+
"max_plan_budget_tokens": 12,
|
|
16
|
+
"temperature": 0.3,
|
|
17
|
+
"top_p": 0.9,
|
|
18
|
+
},
|
|
19
|
+
"mistral:7b": {
|
|
20
|
+
"plan_budget_tokens": 10,
|
|
21
|
+
"min_plan_budget_tokens": 8,
|
|
22
|
+
"max_plan_budget_tokens": 16,
|
|
23
|
+
"temperature": 0.25,
|
|
24
|
+
"top_p": 0.9,
|
|
25
|
+
},
|
|
26
|
+
"gemma3:27b": {
|
|
27
|
+
"plan_budget_tokens": 8,
|
|
28
|
+
"min_plan_budget_tokens": 6,
|
|
29
|
+
"max_plan_budget_tokens": 14,
|
|
30
|
+
"temperature": 0.2,
|
|
31
|
+
"top_p": 0.85,
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
PRESET_PROFILES = {
|
|
36
|
+
"fast": {
|
|
37
|
+
"min_plan_budget_tokens": 4,
|
|
38
|
+
"max_plan_budget_tokens": 8,
|
|
39
|
+
"route_skip_score_threshold": 2,
|
|
40
|
+
"bypass_char_threshold": 180,
|
|
41
|
+
"temperature": 0.2,
|
|
42
|
+
"top_p": 0.85,
|
|
43
|
+
},
|
|
44
|
+
"balanced": {
|
|
45
|
+
"min_plan_budget_tokens": 6,
|
|
46
|
+
"max_plan_budget_tokens": 12,
|
|
47
|
+
"route_skip_score_threshold": 1,
|
|
48
|
+
"bypass_char_threshold": 120,
|
|
49
|
+
"temperature": 0.3,
|
|
50
|
+
"top_p": 0.9,
|
|
51
|
+
},
|
|
52
|
+
"strict": {
|
|
53
|
+
"min_plan_budget_tokens": 8,
|
|
54
|
+
"max_plan_budget_tokens": 16,
|
|
55
|
+
"route_skip_score_threshold": 0,
|
|
56
|
+
"bypass_char_threshold": 80,
|
|
57
|
+
"temperature": 0.15,
|
|
58
|
+
"top_p": 0.8,
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class QuickThinkConfig:
|
|
65
|
+
model: str = "qwen2.5:1.5b"
|
|
66
|
+
ollama_url: str = "http://localhost:11434"
|
|
67
|
+
provider: str = "ollama"
|
|
68
|
+
plan_budget_tokens: int = 8
|
|
69
|
+
min_plan_budget_tokens: int = 6
|
|
70
|
+
max_plan_budget_tokens: int = 12
|
|
71
|
+
temperature: float = 0.3
|
|
72
|
+
top_p: float = 0.9
|
|
73
|
+
bypass_short_prompts: bool = True
|
|
74
|
+
bypass_char_threshold: int = 120
|
|
75
|
+
adaptive_routing: bool = True
|
|
76
|
+
route_skip_score_threshold: int = 1
|
|
77
|
+
mode: str = "lite"
|
|
78
|
+
continuity_hint: str | None = None
|
|
79
|
+
preset: str = "balanced"
|
|
80
|
+
request_timeout_s: float = 180.0
|
|
81
|
+
think: bool | str | None = None
|
|
82
|
+
scaffold_rules: str | None = None
|
|
83
|
+
lane_policy: str = "default"
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def with_model_profile(cls, model: str, ollama_url: str = "http://localhost:11434") -> "QuickThinkConfig":
|
|
87
|
+
profile = MODEL_PROFILES.get(model, {})
|
|
88
|
+
return cls(
|
|
89
|
+
model=model,
|
|
90
|
+
ollama_url=ollama_url,
|
|
91
|
+
plan_budget_tokens=profile.get("plan_budget_tokens", 8),
|
|
92
|
+
min_plan_budget_tokens=profile.get("min_plan_budget_tokens", 6),
|
|
93
|
+
max_plan_budget_tokens=profile.get("max_plan_budget_tokens", 12),
|
|
94
|
+
temperature=profile.get("temperature", 0.3),
|
|
95
|
+
top_p=profile.get("top_p", 0.9),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def apply_preset(self, preset: str) -> None:
|
|
99
|
+
profile = PRESET_PROFILES.get(preset)
|
|
100
|
+
if not profile:
|
|
101
|
+
raise ValueError(f"Unknown preset '{preset}'")
|
|
102
|
+
self.preset = preset
|
|
103
|
+
self.min_plan_budget_tokens = int(profile["min_plan_budget_tokens"])
|
|
104
|
+
self.max_plan_budget_tokens = int(profile["max_plan_budget_tokens"])
|
|
105
|
+
self.route_skip_score_threshold = int(profile["route_skip_score_threshold"])
|
|
106
|
+
self.bypass_char_threshold = int(profile["bypass_char_threshold"])
|
|
107
|
+
self.temperature = float(profile["temperature"])
|
|
108
|
+
self.top_p = float(profile["top_p"])
|
quickthink/engine.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from time import perf_counter
|
|
5
|
+
|
|
6
|
+
from .config import QuickThinkConfig
|
|
7
|
+
from .inline_protocol import extract_plan_and_answer
|
|
8
|
+
from .ollama_client import OllamaClient
|
|
9
|
+
from .plan_grammar import is_valid_plan, normalize_plan
|
|
10
|
+
from .prompts import (
|
|
11
|
+
make_answer_prompt,
|
|
12
|
+
make_inline_plan_answer_prompt,
|
|
13
|
+
make_plan_prompt,
|
|
14
|
+
make_plan_repair_prompt,
|
|
15
|
+
)
|
|
16
|
+
from .routing import infer_task_class, should_bypass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class QuickThinkResult:
|
|
21
|
+
answer: str
|
|
22
|
+
plan: str | None
|
|
23
|
+
mode: str
|
|
24
|
+
bypassed: bool
|
|
25
|
+
route_score: int
|
|
26
|
+
selected_plan_budget: int
|
|
27
|
+
plan_repaired: bool
|
|
28
|
+
plan_latency_ms: float
|
|
29
|
+
answer_latency_ms: float
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def total_latency_ms(self) -> float:
|
|
33
|
+
return self.plan_latency_ms + self.answer_latency_ms
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class QuickThinkEngine:
|
|
37
|
+
def __init__(self, config: QuickThinkConfig) -> None:
|
|
38
|
+
self.config = config
|
|
39
|
+
self.client = OllamaClient(config.ollama_url, timeout_s=config.request_timeout_s)
|
|
40
|
+
|
|
41
|
+
def run(self, prompt: str) -> QuickThinkResult:
|
|
42
|
+
if self.config.lane_policy == "strict_safe" and infer_task_class(prompt) == "strict_format":
|
|
43
|
+
return self._run_direct(prompt=prompt, route_score=-1, selected_budget=self.config.min_plan_budget_tokens)
|
|
44
|
+
|
|
45
|
+
bypass, route_score, selected_budget = should_bypass(prompt, self.config)
|
|
46
|
+
if bypass:
|
|
47
|
+
return self._run_direct(prompt=prompt, route_score=route_score, selected_budget=selected_budget)
|
|
48
|
+
|
|
49
|
+
if self.config.mode == "two_pass":
|
|
50
|
+
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
|
+
return self._run_lite(prompt, route_score, selected_budget)
|
|
54
|
+
|
|
55
|
+
def _run_direct(self, prompt: str, route_score: int, selected_budget: int) -> QuickThinkResult:
|
|
56
|
+
start = perf_counter()
|
|
57
|
+
answer_raw = self.client.generate(
|
|
58
|
+
model=self.config.model,
|
|
59
|
+
prompt=prompt,
|
|
60
|
+
temperature=self.config.temperature,
|
|
61
|
+
top_p=self.config.top_p,
|
|
62
|
+
max_tokens=512,
|
|
63
|
+
think=self.config.think,
|
|
64
|
+
)
|
|
65
|
+
answer_ms = (perf_counter() - start) * 1000
|
|
66
|
+
return QuickThinkResult(
|
|
67
|
+
answer=answer_raw.get("response", "").strip(),
|
|
68
|
+
plan=None,
|
|
69
|
+
mode=self.config.mode,
|
|
70
|
+
bypassed=True,
|
|
71
|
+
route_score=route_score,
|
|
72
|
+
selected_plan_budget=selected_budget,
|
|
73
|
+
plan_repaired=False,
|
|
74
|
+
plan_latency_ms=0.0,
|
|
75
|
+
answer_latency_ms=answer_ms,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def _run_lite(self, prompt: str, route_score: int, selected_budget: int) -> QuickThinkResult:
|
|
79
|
+
inline_prompt = make_inline_plan_answer_prompt(
|
|
80
|
+
prompt,
|
|
81
|
+
selected_budget,
|
|
82
|
+
continuity_hint=self.config.continuity_hint,
|
|
83
|
+
scaffold_rules=self.config.scaffold_rules,
|
|
84
|
+
)
|
|
85
|
+
start = perf_counter()
|
|
86
|
+
raw = self.client.generate(
|
|
87
|
+
model=self.config.model,
|
|
88
|
+
prompt=inline_prompt,
|
|
89
|
+
temperature=self.config.temperature,
|
|
90
|
+
top_p=self.config.top_p,
|
|
91
|
+
max_tokens=768,
|
|
92
|
+
think=self.config.think,
|
|
93
|
+
)
|
|
94
|
+
answer_ms = (perf_counter() - start) * 1000
|
|
95
|
+
|
|
96
|
+
raw_text = raw.get("response", "")
|
|
97
|
+
plan, answer = extract_plan_and_answer(raw_text)
|
|
98
|
+
repaired = False
|
|
99
|
+
|
|
100
|
+
if plan and not is_valid_plan(plan, selected_budget):
|
|
101
|
+
repaired = True
|
|
102
|
+
plan = "g:solve;c:constraints;s:direct_reasoning;r:verify_output"
|
|
103
|
+
if plan is None:
|
|
104
|
+
repaired = True
|
|
105
|
+
|
|
106
|
+
return QuickThinkResult(
|
|
107
|
+
answer=answer.strip(),
|
|
108
|
+
plan=plan,
|
|
109
|
+
mode="lite",
|
|
110
|
+
bypassed=False,
|
|
111
|
+
route_score=route_score,
|
|
112
|
+
selected_plan_budget=selected_budget,
|
|
113
|
+
plan_repaired=repaired,
|
|
114
|
+
plan_latency_ms=0.0,
|
|
115
|
+
answer_latency_ms=answer_ms,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _run_two_pass(self, prompt: str, route_score: int, selected_budget: int) -> QuickThinkResult:
|
|
119
|
+
plan_prompt = make_plan_prompt(prompt, selected_budget)
|
|
120
|
+
start = perf_counter()
|
|
121
|
+
plan_raw = self.client.generate(
|
|
122
|
+
model=self.config.model,
|
|
123
|
+
prompt=plan_prompt,
|
|
124
|
+
temperature=min(self.config.temperature, 0.3),
|
|
125
|
+
top_p=self.config.top_p,
|
|
126
|
+
max_tokens=selected_budget,
|
|
127
|
+
think=self.config.think,
|
|
128
|
+
)
|
|
129
|
+
plan_ms = (perf_counter() - start) * 1000
|
|
130
|
+
plan = normalize_plan(plan_raw.get("response", ""))
|
|
131
|
+
repaired = False
|
|
132
|
+
|
|
133
|
+
if not is_valid_plan(plan, selected_budget):
|
|
134
|
+
repaired = True
|
|
135
|
+
repair_prompt = make_plan_repair_prompt(prompt, plan, selected_budget)
|
|
136
|
+
start = perf_counter()
|
|
137
|
+
repair_raw = self.client.generate(
|
|
138
|
+
model=self.config.model,
|
|
139
|
+
prompt=repair_prompt,
|
|
140
|
+
temperature=0.1,
|
|
141
|
+
top_p=self.config.top_p,
|
|
142
|
+
max_tokens=selected_budget + 8,
|
|
143
|
+
think=self.config.think,
|
|
144
|
+
)
|
|
145
|
+
plan_ms += (perf_counter() - start) * 1000
|
|
146
|
+
repaired_plan = normalize_plan(repair_raw.get("response", ""))
|
|
147
|
+
if is_valid_plan(repaired_plan, selected_budget):
|
|
148
|
+
plan = repaired_plan
|
|
149
|
+
else:
|
|
150
|
+
plan = "g:solve;c:constraints;s:direct_reasoning;r:verify_output"
|
|
151
|
+
|
|
152
|
+
answer_prompt = make_answer_prompt(prompt, plan)
|
|
153
|
+
start = perf_counter()
|
|
154
|
+
answer_raw = self.client.generate(
|
|
155
|
+
model=self.config.model,
|
|
156
|
+
prompt=answer_prompt,
|
|
157
|
+
temperature=self.config.temperature,
|
|
158
|
+
top_p=self.config.top_p,
|
|
159
|
+
max_tokens=768,
|
|
160
|
+
think=self.config.think,
|
|
161
|
+
)
|
|
162
|
+
answer_ms = (perf_counter() - start) * 1000
|
|
163
|
+
|
|
164
|
+
return QuickThinkResult(
|
|
165
|
+
answer=answer_raw.get("response", "").strip(),
|
|
166
|
+
plan=plan,
|
|
167
|
+
mode="two_pass",
|
|
168
|
+
bypassed=False,
|
|
169
|
+
route_score=route_score,
|
|
170
|
+
selected_plan_budget=selected_budget,
|
|
171
|
+
plan_repaired=repaired,
|
|
172
|
+
plan_latency_ms=plan_ms,
|
|
173
|
+
answer_latency_ms=answer_ms,
|
|
174
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from .plan_grammar import normalize_plan
|
|
6
|
+
|
|
7
|
+
_INLINE_RE = re.compile(r"\[P\](.*?)\[A\](.*)", re.DOTALL)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def extract_plan_and_answer(raw: str) -> tuple[str | None, str]:
|
|
11
|
+
text = raw.strip()
|
|
12
|
+
match = _INLINE_RE.search(text)
|
|
13
|
+
if not match:
|
|
14
|
+
return None, text
|
|
15
|
+
|
|
16
|
+
plan = normalize_plan(match.group(1))
|
|
17
|
+
answer = match.group(2).strip()
|
|
18
|
+
return plan, answer
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OllamaClient:
|
|
9
|
+
def __init__(self, base_url: str, timeout_s: float = 180.0) -> None:
|
|
10
|
+
self.base_url = base_url.rstrip("/")
|
|
11
|
+
self.timeout_s = timeout_s
|
|
12
|
+
|
|
13
|
+
def generate(
|
|
14
|
+
self,
|
|
15
|
+
*,
|
|
16
|
+
model: str,
|
|
17
|
+
prompt: str,
|
|
18
|
+
temperature: float,
|
|
19
|
+
top_p: float,
|
|
20
|
+
max_tokens: int,
|
|
21
|
+
think: bool | str | None = None,
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
payload = {
|
|
24
|
+
"model": model,
|
|
25
|
+
"prompt": prompt,
|
|
26
|
+
"stream": False,
|
|
27
|
+
"options": {
|
|
28
|
+
"temperature": temperature,
|
|
29
|
+
"top_p": top_p,
|
|
30
|
+
"num_predict": max_tokens,
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
if think is not None:
|
|
34
|
+
payload["think"] = think
|
|
35
|
+
with httpx.Client(timeout=self.timeout_s) as client:
|
|
36
|
+
response = client.post(f"{self.base_url}/api/generate", json=payload)
|
|
37
|
+
response.raise_for_status()
|
|
38
|
+
return response.json()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
_PLAN_RE = re.compile(
|
|
6
|
+
r"^g:([a-z0-9_,-]{1,64});c:([a-z0-9_,-]{1,64});s:([a-z0-9_,-]{1,64});r:([a-z0-9_,-]{1,64})$"
|
|
7
|
+
)
|
|
8
|
+
_TOKEN_RE = re.compile(r"[a-z0-9_]+")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def estimate_plan_tokens(plan: str) -> int:
|
|
12
|
+
return len(_TOKEN_RE.findall(plan.lower()))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalize_plan(raw: str) -> str:
|
|
16
|
+
line = raw.strip().splitlines()[0] if raw.strip() else ""
|
|
17
|
+
return line.strip().lower().replace(" ", "")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_valid_plan(plan: str, budget_tokens: int) -> bool:
|
|
21
|
+
normalized = normalize_plan(plan)
|
|
22
|
+
if not _PLAN_RE.match(normalized):
|
|
23
|
+
return False
|
|
24
|
+
return estimate_plan_tokens(normalized) <= budget_tokens
|
quickthink/prompts.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def make_plan_prompt(user_prompt: str, budget: int) -> str:
|
|
5
|
+
return (
|
|
6
|
+
"You are a compressed planning module. Produce only one ultra-compact plan line.\n"
|
|
7
|
+
"Rules:\n"
|
|
8
|
+
f"- Max keyword tokens: {budget}\n"
|
|
9
|
+
"- Lowercase only\n"
|
|
10
|
+
"- No spaces\n"
|
|
11
|
+
"- No prose\n"
|
|
12
|
+
"- No markdown\n"
|
|
13
|
+
"- Exactly this format and key order: g:<...>;c:<...>;s:<...>;r:<...>\n"
|
|
14
|
+
"- Use slugs with [a-z0-9_,-]\n\n"
|
|
15
|
+
f"User task:\n{user_prompt}\n"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def make_answer_prompt(user_prompt: str, plan: str) -> str:
|
|
20
|
+
return (
|
|
21
|
+
"You are an assistant. Use the compact plan internally and answer directly.\n"
|
|
22
|
+
"Do not reveal internal planning.\n"
|
|
23
|
+
"Return only the final answer.\n\n"
|
|
24
|
+
f"Internal plan:\n{plan}\n\n"
|
|
25
|
+
f"User task:\n{user_prompt}\n"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def make_plan_repair_prompt(user_prompt: str, bad_plan: str, budget: int) -> str:
|
|
30
|
+
return (
|
|
31
|
+
"Repair the plan to valid compact grammar. Return one line only.\n"
|
|
32
|
+
f"Budget: {budget} keyword tokens max.\n"
|
|
33
|
+
"Required format: g:<...>;c:<...>;s:<...>;r:<...>\n"
|
|
34
|
+
"Rules: lowercase, no spaces, keys in exact order, [a-z0-9_,-] only.\n\n"
|
|
35
|
+
f"User task:\n{user_prompt}\n\n"
|
|
36
|
+
f"Invalid plan:\n{bad_plan}\n"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def make_inline_plan_answer_prompt(
|
|
41
|
+
user_prompt: str,
|
|
42
|
+
budget: int,
|
|
43
|
+
continuity_hint: str | None = None,
|
|
44
|
+
scaffold_rules: str | None = None,
|
|
45
|
+
) -> str:
|
|
46
|
+
hint_line = ""
|
|
47
|
+
if continuity_hint:
|
|
48
|
+
hint_line = (
|
|
49
|
+
"Optional continuity hint is provided below. Use it only if clearly useful.\n"
|
|
50
|
+
f"Hint: {continuity_hint}\n"
|
|
51
|
+
)
|
|
52
|
+
rules_line = ""
|
|
53
|
+
if scaffold_rules:
|
|
54
|
+
rules_line = f"Additional scaffold rules:\n- {scaffold_rules}\n"
|
|
55
|
+
return (
|
|
56
|
+
"Generate a compact internal plan prefix and then the answer in one response.\n"
|
|
57
|
+
"Output format must be exactly:\n"
|
|
58
|
+
"[P]g:<...>;c:<...>;s:<...>;r:<...>\n"
|
|
59
|
+
"[A]<final answer text>\n"
|
|
60
|
+
"Rules for [P]:\n"
|
|
61
|
+
f"- max keyword tokens: {budget}\n"
|
|
62
|
+
"- lowercase\n"
|
|
63
|
+
"- no spaces\n"
|
|
64
|
+
"- grammar keys in order: g,c,s,r\n"
|
|
65
|
+
"- chars allowed: [a-z0-9_,-:;]\n"
|
|
66
|
+
"Rules for [A]:\n"
|
|
67
|
+
"- direct final answer\n"
|
|
68
|
+
"- do not mention internal planning\n"
|
|
69
|
+
"- no extra section headers\n"
|
|
70
|
+
f"{rules_line}"
|
|
71
|
+
f"{hint_line}\n"
|
|
72
|
+
f"User task:\n{user_prompt}\n"
|
|
73
|
+
)
|