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 ADDED
@@ -0,0 +1,6 @@
1
+ """quickthink package."""
2
+
3
+ from .config import QuickThinkConfig
4
+ from .engine import QuickThinkEngine
5
+
6
+ __all__ = ["QuickThinkConfig", "QuickThinkEngine"]
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
+ )