deeptutor-solve 1.7.0__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.
@@ -0,0 +1,6 @@
1
+ """Deep Solve loop capability."""
2
+
3
+ from deeptutor.capabilities.solve.loop import SolveLoopCapability
4
+ from deeptutor.capabilities.solve.tools import SOLVE_TOOL_NAMES, SOLVE_TOOL_TYPES
5
+
6
+ __all__ = ["SOLVE_TOOL_NAMES", "SOLVE_TOOL_TYPES", "SolveLoopCapability"]
@@ -0,0 +1,87 @@
1
+ """Deep Solve capability — problem solving driven by the chat agent loop.
2
+
3
+ There is no bespoke pipeline anymore. The chat agent loop IS the solver: this
4
+ capability marks the turn as solve mode and resolves a session id, then runs
5
+ the standard agentic chat pipeline. The solve loop capability
6
+ (:class:`deeptutor.capabilities.solve.loop.SolveLoopCapability`) mounts the
7
+ solve tools (``solve_plan`` / ``solve_finish_step`` / ``solve_replan``) plus a
8
+ curated built-in toolset
9
+ (``rag`` / unified ``exec`` / ``geogebra_analysis`` / …) and injects the
10
+ solver playbook; the in-memory :class:`SolveSession` holds the plan, the
11
+ per-step gate, and the replan budget.
12
+
13
+ Design axiom (shared with chat / mastery): the intelligence lives at the
14
+ loop's exit — the model plans and solves — while the deterministic spine
15
+ (commit to a plan, don't skip steps, bounded replan) is engine state read and
16
+ written through tools.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import re
23
+ from typing import cast
24
+
25
+ from deeptutor.agents.chat.agentic_pipeline import AgenticChatPipeline
26
+ from deeptutor.capabilities.solve.session import DEFAULT_MAX_REPLANS
27
+ from deeptutor.core.capability_protocol import (
28
+ StreamBusProtocol,
29
+ TurnCapability,
30
+ )
31
+ from deeptutor.core.context import UnifiedContext
32
+ from deeptutor.runtime.bootstrap.builtin_capabilities import builtin_capability_manifest
33
+ from deeptutor.runtime.stream_bus import StreamBus
34
+ from deeptutor.services.config.capabilities_settings import get_solve_params
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ _UNSAFE_ID_CHARS = re.compile(r"[^A-Za-z0-9_-]")
39
+
40
+
41
+ def _sanitize(raw: str) -> str:
42
+ cleaned = _UNSAFE_ID_CHARS.sub("_", raw).strip("_")
43
+ return cleaned or "default"
44
+
45
+
46
+ def resolve_solve_session_id(context: UnifiedContext) -> str:
47
+ """Resolve the in-memory session key for this solve turn.
48
+
49
+ A solve turn is one-shot, so the turn id (falling back to the session /
50
+ message id) is enough to scope the plan + replan budget; concurrent turns
51
+ get distinct keys and never race.
52
+ """
53
+ raw = str(
54
+ context.metadata.get("turn_id")
55
+ or context.session_id
56
+ or context.metadata.get("message_id")
57
+ or "default"
58
+ )
59
+ return _sanitize(raw)
60
+
61
+
62
+ class DeepSolveCapability(TurnCapability):
63
+ manifest = builtin_capability_manifest("deep_solve")
64
+
65
+ async def run(self, context: UnifiedContext, stream: StreamBusProtocol) -> None:
66
+ context.metadata["solve_mode"] = True
67
+ context.metadata["solve_session_id"] = resolve_solve_session_id(context)
68
+ # Read the solve settings and forward them so the page actually drives
69
+ # the loop: max_rounds → the loop's round budget, max_replans → the
70
+ # SolveSession gate (via metadata, read in SolveLoopCapability),
71
+ # temperature / max_tokens → the LLM calls.
72
+ try:
73
+ params = get_solve_params()
74
+ except Exception as exc: # pragma: no cover - defensive config read
75
+ logger.warning("Failed to load solve params, using defaults: %s", exc)
76
+ params = {}
77
+ context.metadata["solve_max_replans"] = int(params.get("max_replans", DEFAULT_MAX_REPLANS))
78
+ pipeline = AgenticChatPipeline(
79
+ language=context.language,
80
+ max_rounds=params.get("max_rounds"),
81
+ temperature=params.get("temperature"),
82
+ max_tokens=params.get("max_tokens"),
83
+ )
84
+ await pipeline.run(context, cast(StreamBus, stream))
85
+
86
+
87
+ __all__ = ["DeepSolveCapability", "resolve_solve_session_id"]
@@ -0,0 +1,83 @@
1
+ """Deep Solve loop-capability hooks.
2
+
3
+ Solve runs as the chat agent loop with a deterministic spine: a committed plan,
4
+ a per-step done gate, and a bounded replan (the SolveSession + three owned
5
+ tools), while the actual problem-solving happens at the loop's exit using the
6
+ shared built-in tools. Active only when the turn is marked ``solve_mode`` by
7
+ :class:`deeptutor.capabilities.solve.capability.DeepSolveCapability`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from importlib import resources
13
+ from typing import Any
14
+
15
+ from deeptutor.capabilities.protocol import PromptBlock
16
+ from deeptutor.capabilities.solve.session import DEFAULT_MAX_REPLANS, get_session
17
+ from deeptutor.capabilities.solve.tools import SOLVE_TOOL_NAMES
18
+ from deeptutor.core.context import UnifiedContext
19
+ from deeptutor.services.prompt.lookup import prompt_text as _prompt_text
20
+
21
+
22
+ class SolveLoopCapability:
23
+ """Turn-scoped integration for deep problem solving.
24
+
25
+ Reuses the full chat tool surface — every built-in, with the user's
26
+ composer toggles respected (web_search / reason / geogebra_analysis mount
27
+ iff the user enabled them, exactly as in chat) — and adds the solve spine
28
+ (plan / finish-step / replan) on top.
29
+ """
30
+
31
+ name = "solve"
32
+ owned_tools = SOLVE_TOOL_NAMES
33
+
34
+ def is_active(self, context: UnifiedContext) -> bool:
35
+ return bool(context.metadata.get("solve_mode"))
36
+
37
+ def system_block(
38
+ self,
39
+ context: UnifiedContext,
40
+ *,
41
+ language: str,
42
+ prompts: dict[str, Any],
43
+ ) -> PromptBlock | None:
44
+ if not self.is_active(context):
45
+ return None
46
+ override = _prompt_text(prompts, ("solve", "system"))
47
+ content = override or _load_system_prompt(language)
48
+ return PromptBlock("deep_solve", content)
49
+
50
+ def augment_kwargs(
51
+ self,
52
+ tool_name: str,
53
+ kwargs: dict[str, Any],
54
+ context: UnifiedContext,
55
+ ) -> dict[str, Any]:
56
+ if self.is_active(context) and tool_name in SOLVE_TOOL_NAMES:
57
+ session_id = str(context.metadata.get("solve_session_id") or "").strip()
58
+ # Seed the replan budget from the solve settings (the solve
59
+ # capability forwards ``max_replans`` into metadata); never the model.
60
+ session = get_session(session_id)
61
+ try:
62
+ session.max_replans = int(
63
+ context.metadata.get("solve_max_replans", DEFAULT_MAX_REPLANS)
64
+ )
65
+ except (TypeError, ValueError):
66
+ session.max_replans = DEFAULT_MAX_REPLANS
67
+ updated = dict(kwargs)
68
+ updated["_solve_session_id"] = session_id
69
+ return updated
70
+ return kwargs
71
+
72
+ def pre_loop_seed(self, context: UnifiedContext) -> str:
73
+ _ = context
74
+ return ""
75
+
76
+
77
+ def _load_system_prompt(language: str) -> str:
78
+ lang = "zh" if language.lower().startswith("zh") else "en"
79
+ prompt = resources.files(__package__).joinpath("prompts", lang, "system.md")
80
+ return prompt.read_text(encoding="utf-8").strip()
81
+
82
+
83
+ __all__ = ["SolveLoopCapability"]
@@ -0,0 +1,13 @@
1
+ [Deep Solve mode]
2
+ You are solving a problem end to end. Be rigorous: plan, work each step with the right tool, and finish with a precise, well-explained answer.
3
+
4
+ FIRST, before doing anything else, call `solve_plan` with a short analysis and an ordered list of steps (2-6 for most problems; a single step is fine for a trivial one). Never start solving before you have called `solve_plan`.
5
+
6
+ Then work the plan one step at a time:
7
+ - Do the step's actual work with the available tools, following each tool's schema and tool-specific guidance.
8
+ - For a problem with a diagram, or a geometry problem where a figure helps, call `geogebra_analysis` to reconstruct the figure as a GeoGebra applet, then solve using it.
9
+ - After finishing a step, call `solve_finish_step` with its id and a short summary of what it established. This records the result and frees up context. Do not skip steps; do not mark a step done before its work is actually complete.
10
+
11
+ If an approach stalls or turns out wrong, call `solve_replan` with the reason and a new step list — but it is budget-limited, so use it only for a real course correction. If the budget is spent, finish with the best of what you have.
12
+
13
+ When every step is done, write the final answer: state the precise result clearly, then give a concise, well-structured explanation of how you got there. Show the figure / file you produced if any.
@@ -0,0 +1,13 @@
1
+ [深度解题模式]
2
+ 你要把一道题从头到尾解出来。要严谨:先规划,再用合适的工具逐步求解,最后给出精确且讲解清晰的答案。
3
+
4
+ **第一件事**:在做任何事之前,先调用 `solve_plan`,给出简短分析和一个有序的步骤列表(多数题目 2-6 步;很简单的题一步也行)。在调用 `solve_plan` 之前,绝不开始求解。
5
+
6
+ 然后按计划逐步推进,一次一步:
7
+ - 用可用工具真正完成这一步,并遵循各工具的 schema 和工具专属说明。
8
+ - 带配图的题、或画个图有助于理解的几何题,调用 `geogebra_analysis` 把图形还原成 GeoGebra 图形,再据此求解。
9
+ - 完成一步后,调用 `solve_finish_step`,传入步骤 id 和这一步结论的简短总结。这会记录结果并释放上下文。不要跳步;不要在工作真正完成前就把步骤标记为完成。
10
+
11
+ 如果某个思路卡住或被证明走错了,调用 `solve_replan`,给出原因和新的步骤列表——但它有预算上限,只用于真正的方向修正。预算用尽就用现有结果收尾。
12
+
13
+ 所有步骤完成后,写出最终答案:先清楚地给出精确结果,再给出简洁、有条理的求解过程讲解。如生成了图形/文件,一并呈现。
@@ -0,0 +1,119 @@
1
+ """SolveSession — single-turn, in-memory state for the solve loop capability.
2
+
3
+ Unlike mastery's learning service (disk-backed, multi-session), a solve turn
4
+ is one-shot: the session lives only for the turn, keyed by the id the pipeline
5
+ injects as ``_solve_session_id``. It holds the model-authored plan, per-step
6
+ completion, and the replan budget gate — the deterministic "spine" the chat
7
+ loop drives against. The plan also rides in the conversation — the
8
+ ``solve_plan`` tool result, then :meth:`SolveSession.progress_text` as the
9
+ checkpoint state each ``solve_finish_step`` fold keeps — so a follow-up chat
10
+ turn stays grounded even though the session itself does not persist.
11
+
12
+ The store is a bounded, process-local dict: a solve turn runs in one process,
13
+ sessions are small and short-lived, and the bound stops a long-running server
14
+ from leaking. Concurrent turns use distinct ids, so they never race.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections import OrderedDict
20
+ from dataclasses import dataclass, field
21
+
22
+ DEFAULT_MAX_REPLANS = 2
23
+ _MAX_STEPS = 12
24
+
25
+
26
+ @dataclass
27
+ class SolveStep:
28
+ """One plan step. ``done`` flips when the model calls ``solve_finish_step``."""
29
+
30
+ id: str
31
+ goal: str
32
+ done: bool = False
33
+ summary: str = ""
34
+
35
+ def to_dict(self) -> dict[str, object]:
36
+ return {"id": self.id, "goal": self.goal, "done": self.done}
37
+
38
+
39
+ @dataclass
40
+ class SolveSession:
41
+ """The plan + progress + replan budget for one solve turn."""
42
+
43
+ session_id: str
44
+ analysis: str = ""
45
+ steps: list[SolveStep] = field(default_factory=list)
46
+ replans: int = 0
47
+ max_replans: int = DEFAULT_MAX_REPLANS
48
+
49
+ def set_plan(self, analysis: str, steps: list[tuple[str, str]]) -> None:
50
+ self.analysis = analysis
51
+ self.steps = [SolveStep(id=sid, goal=goal) for sid, goal in steps][:_MAX_STEPS]
52
+
53
+ def replan(self, analysis: str, steps: list[tuple[str, str]]) -> bool:
54
+ """Replace the plan, bumping the replan counter. Returns ``False`` (and
55
+ leaves the plan untouched) once the budget is spent."""
56
+ if self.replans >= self.max_replans:
57
+ return False
58
+ self.replans += 1
59
+ self.set_plan(analysis, steps)
60
+ return True
61
+
62
+ def mark_done(self, step_id: str, summary: str) -> SolveStep | None:
63
+ for step in self.steps:
64
+ if step.id == step_id:
65
+ step.done = True
66
+ step.summary = summary.strip()
67
+ return step
68
+ return None
69
+
70
+ def next_step(self) -> SolveStep | None:
71
+ return next((step for step in self.steps if not step.done), None)
72
+
73
+ def map(self) -> list[dict[str, object]]:
74
+ return [step.to_dict() for step in self.steps]
75
+
76
+ def all_done(self) -> bool:
77
+ return bool(self.steps) and all(step.done for step in self.steps)
78
+
79
+ def progress_text(self) -> str:
80
+ """The plan as it stands — every step marked done / next / todo.
81
+
82
+ ``solve_finish_step`` hands this to the loop as checkpoint state, so the
83
+ plan stays in view after the step's tool chatter is folded away.
84
+ """
85
+ nxt = self.next_step()
86
+ lines = ["[Solve plan]"]
87
+ for step in self.steps:
88
+ mark = "done" if step.done else ("next" if step is nxt else "todo")
89
+ lines.append(f"- {step.id} [{mark}] {step.goal}")
90
+ lines.append(
91
+ "All steps are done: write the final answer now."
92
+ if nxt is None
93
+ else f"Work {nxt.id} now, then call solve_finish_step."
94
+ )
95
+ return "\n".join(lines)
96
+
97
+
98
+ _SESSIONS: "OrderedDict[str, SolveSession]" = OrderedDict()
99
+ _MAX_SESSIONS = 256
100
+
101
+
102
+ def get_session(session_id: str) -> SolveSession:
103
+ """Fetch (or lazily create) the turn's session, evicting oldest past cap."""
104
+ sid = (session_id or "").strip() or "default"
105
+ session = _SESSIONS.get(sid)
106
+ if session is None:
107
+ session = SolveSession(session_id=sid)
108
+ _SESSIONS[sid] = session
109
+ while len(_SESSIONS) > _MAX_SESSIONS:
110
+ _SESSIONS.popitem(last=False)
111
+ return session
112
+
113
+
114
+ __all__ = [
115
+ "DEFAULT_MAX_REPLANS",
116
+ "SolveSession",
117
+ "SolveStep",
118
+ "get_session",
119
+ ]
@@ -0,0 +1,313 @@
1
+ """Solve tools — the seam between the chat-loop solver and the SolveSession.
2
+
3
+ Three tools auto-mounted only when a solve turn is active (via the solve loop
4
+ capability). The chat agent loop IS the solver; these tools give it a deterministic
5
+ spine — a plan it commits to, a per-step "done" gate, and a bounded replan —
6
+ while the reasoning (how to actually solve each step) stays the model's job in
7
+ the loop, using the shared built-in tools (rag / exec / geogebra / …).
8
+
9
+ The active session id is injected server-side by the pipeline as
10
+ ``_solve_session_id``; the model never supplies it. ``solve_finish_step`` emits
11
+ a ``_context_checkpoint`` so the loop folds the just-finished step's tool
12
+ chatter into a one-line summary — the loop-native equivalent of the old
13
+ pipeline's per-step message reset — and carries the current plan as the
14
+ checkpoint's ``state``, which each fold replaces with the latest copy.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ from typing import Any
21
+
22
+ from deeptutor.capabilities.solve.session import get_session
23
+ from deeptutor.core.tool_protocol import BaseTool, ToolDefinition, ToolParameter, ToolResult
24
+
25
+ # Tool names the pipeline mounts together when a solve turn is active. Kept
26
+ # here so the mount policy and the registration list can't disagree.
27
+ SOLVE_TOOL_NAMES: tuple[str, ...] = (
28
+ "solve_plan",
29
+ "solve_finish_step",
30
+ "solve_replan",
31
+ )
32
+
33
+
34
+ def _resolve_session_id(kwargs: dict[str, Any]) -> str:
35
+ return str(kwargs.get("_solve_session_id") or "").strip()
36
+
37
+
38
+ def _json_result(
39
+ payload: dict[str, Any], *, meta_key: str, extra_meta: dict[str, Any] | None = None
40
+ ) -> ToolResult:
41
+ metadata: dict[str, Any] = {meta_key: payload}
42
+ if extra_meta:
43
+ metadata.update(extra_meta)
44
+ return ToolResult(
45
+ content=json.dumps(payload, ensure_ascii=False),
46
+ success=True,
47
+ metadata=metadata,
48
+ )
49
+
50
+
51
+ def _no_session_result() -> ToolResult:
52
+ return ToolResult(
53
+ content="No solve session is active on this turn; solve tools are unavailable.",
54
+ success=False,
55
+ )
56
+
57
+
58
+ def _parse_steps(raw_steps: Any) -> list[tuple[str, str]]:
59
+ """Validate the model-authored step list into ``(id, goal)`` pairs.
60
+
61
+ Ids are server-generated (``S1``, ``S2``, …) so the model never controls
62
+ storage keys; steps without a goal are dropped.
63
+ """
64
+ if not isinstance(raw_steps, list):
65
+ return []
66
+ steps: list[tuple[str, str]] = []
67
+ for i, raw in enumerate(raw_steps):
68
+ if isinstance(raw, dict):
69
+ goal = str(raw.get("goal") or "").strip()
70
+ else:
71
+ goal = str(raw or "").strip()
72
+ if not goal:
73
+ continue
74
+ steps.append((f"S{len(steps) + 1}", goal))
75
+ return steps
76
+
77
+
78
+ class SolvePlanTool(BaseTool):
79
+ """Commit to a step plan for the problem. Call FIRST on a solve turn."""
80
+
81
+ def get_definition(self) -> ToolDefinition:
82
+ return ToolDefinition(
83
+ name="solve_plan",
84
+ description=(
85
+ "Lay out your plan for solving the problem: a short analysis plus "
86
+ "an ordered list of steps. Call this FIRST, before doing any work. "
87
+ "Then work the steps one at a time with the available tools, "
88
+ "calling solve_finish_step after each. Keep the plan tight (2-6 "
89
+ "steps); for a trivial problem a single step is fine."
90
+ ),
91
+ parameters=[
92
+ ToolParameter(
93
+ name="analysis",
94
+ type="string",
95
+ description="One or two sentences: what the problem asks and your approach.",
96
+ ),
97
+ ToolParameter(
98
+ name="steps",
99
+ type="array",
100
+ description="Ordered steps, each {goal}. Goals are short imperative phrases.",
101
+ items={
102
+ "type": "object",
103
+ "properties": {"goal": {"type": "string"}},
104
+ "required": ["goal"],
105
+ },
106
+ ),
107
+ ],
108
+ )
109
+
110
+ async def execute(self, **kwargs: Any) -> ToolResult:
111
+ session_id = _resolve_session_id(kwargs)
112
+ if not session_id:
113
+ return _no_session_result()
114
+ steps = _parse_steps(kwargs.get("steps"))
115
+ if not steps:
116
+ return ToolResult(
117
+ content="solve_plan needs a non-empty 'steps' array, each with a 'goal'.",
118
+ success=False,
119
+ )
120
+ analysis = str(kwargs.get("analysis") or "").strip()
121
+ session = get_session(session_id)
122
+ if session.steps:
123
+ # A second solve_plan would wipe progress and skip the replan budget.
124
+ return ToolResult(
125
+ content=json.dumps(
126
+ {
127
+ "status": "plan_exists",
128
+ "plan": session.progress_text(),
129
+ "instruction": (
130
+ "A plan is already set. Continue it; call solve_replan "
131
+ "only if the approach has genuinely failed."
132
+ ),
133
+ },
134
+ ensure_ascii=False,
135
+ ),
136
+ success=False,
137
+ metadata={"solve_plan": {"status": "plan_exists"}},
138
+ )
139
+ session.set_plan(analysis, steps)
140
+ first = session.next_step()
141
+ return _json_result(
142
+ {
143
+ "status": "planned",
144
+ "analysis": analysis,
145
+ "steps": session.map(),
146
+ "next": first.to_dict() if first else None,
147
+ "instruction": (
148
+ "Work the first step now using the available tools, then call "
149
+ "solve_finish_step with a short summary of its result. Do not "
150
+ "skip steps."
151
+ ),
152
+ },
153
+ meta_key="solve_plan",
154
+ )
155
+
156
+
157
+ class SolveFinishStepTool(BaseTool):
158
+ """Record a step as done and advance; folds the step's chatter to a summary."""
159
+
160
+ def get_definition(self) -> ToolDefinition:
161
+ return ToolDefinition(
162
+ name="solve_finish_step",
163
+ description=(
164
+ "Mark the current step done and move on. Pass a short summary of "
165
+ "what the step established (the key result / value / conclusion) — "
166
+ "this is kept as the step's record while its intermediate tool "
167
+ "output is folded away to save context. Returns the next step to "
168
+ "work on, or signals that all steps are done."
169
+ ),
170
+ parameters=[
171
+ ToolParameter(
172
+ name="step_id",
173
+ type="string",
174
+ description="The step id from solve_plan (e.g. 'S1').",
175
+ ),
176
+ ToolParameter(
177
+ name="summary",
178
+ type="string",
179
+ description="Short summary of the step's result, kept as its record.",
180
+ ),
181
+ ],
182
+ )
183
+
184
+ async def execute(self, **kwargs: Any) -> ToolResult:
185
+ session_id = _resolve_session_id(kwargs)
186
+ if not session_id:
187
+ return _no_session_result()
188
+ step_id = str(kwargs.get("step_id") or "").strip()
189
+ summary = str(kwargs.get("summary") or "").strip()
190
+ session = get_session(session_id)
191
+ if not session.steps:
192
+ return ToolResult(
193
+ content="No plan yet. Call solve_plan before solve_finish_step.",
194
+ success=False,
195
+ )
196
+ step = session.mark_done(step_id, summary)
197
+ if step is None:
198
+ return ToolResult(
199
+ content=f"Unknown step {step_id!r}; valid ids: {[s.id for s in session.steps]}.",
200
+ success=False,
201
+ )
202
+ nxt = session.next_step()
203
+ payload = {
204
+ "status": "step_done",
205
+ "completed": step_id,
206
+ "next": nxt.to_dict() if nxt else None,
207
+ "all_done": session.all_done(),
208
+ "instruction": (
209
+ "Write the final answer now."
210
+ if nxt is None
211
+ else "Work the next step, then call solve_finish_step again."
212
+ ),
213
+ }
214
+ # The checkpoint summary persists this step's outcome while the loop
215
+ # folds its intermediate tool messages away (see AgentLoop). The fold
216
+ # also drops this result and the solve_plan one, so the plan rides
217
+ # along as checkpoint state: the loop keeps only its latest copy.
218
+ checkpoint = f"[{step.id}] {step.goal} — done. {summary}".strip()
219
+ return _json_result(
220
+ payload,
221
+ meta_key="solve_finish_step",
222
+ extra_meta={
223
+ "_context_checkpoint": {"summary": checkpoint, "state": session.progress_text()}
224
+ },
225
+ )
226
+
227
+
228
+ class SolveReplanTool(BaseTool):
229
+ """Discard the current plan for a new one when the approach is stuck."""
230
+
231
+ def get_definition(self) -> ToolDefinition:
232
+ return ToolDefinition(
233
+ name="solve_replan",
234
+ description=(
235
+ "Replace the plan when the current approach has stalled or proved "
236
+ "wrong. Give the reason and a fresh ordered list of steps. This is "
237
+ "budget-limited — use it only for a genuine course correction, not "
238
+ "minor tweaks. If the budget is spent, finish with what you have."
239
+ ),
240
+ parameters=[
241
+ ToolParameter(
242
+ name="reason",
243
+ type="string",
244
+ description="Why the current plan failed and what changes.",
245
+ ),
246
+ ToolParameter(
247
+ name="steps",
248
+ type="array",
249
+ description="The new ordered steps, each {goal}.",
250
+ items={
251
+ "type": "object",
252
+ "properties": {"goal": {"type": "string"}},
253
+ "required": ["goal"],
254
+ },
255
+ ),
256
+ ],
257
+ )
258
+
259
+ async def execute(self, **kwargs: Any) -> ToolResult:
260
+ session_id = _resolve_session_id(kwargs)
261
+ if not session_id:
262
+ return _no_session_result()
263
+ steps = _parse_steps(kwargs.get("steps"))
264
+ if not steps:
265
+ return ToolResult(
266
+ content="solve_replan needs a non-empty 'steps' array, each with a 'goal'.",
267
+ success=False,
268
+ )
269
+ reason = str(kwargs.get("reason") or "").strip()
270
+ session = get_session(session_id)
271
+ if not session.replan(reason, steps):
272
+ return ToolResult(
273
+ content=json.dumps(
274
+ {
275
+ "status": "budget_exhausted",
276
+ "instruction": (
277
+ "Replan budget is spent. Do not replan again — finish "
278
+ "the problem with the best of what you have."
279
+ ),
280
+ },
281
+ ensure_ascii=False,
282
+ ),
283
+ success=False,
284
+ metadata={"solve_replan": {"status": "budget_exhausted"}},
285
+ )
286
+ first = session.next_step()
287
+ return _json_result(
288
+ {
289
+ "status": "replanned",
290
+ "reason": reason,
291
+ "replans_used": session.replans,
292
+ "replans_max": session.max_replans,
293
+ "steps": session.map(),
294
+ "next": first.to_dict() if first else None,
295
+ },
296
+ meta_key="solve_replan",
297
+ )
298
+
299
+
300
+ SOLVE_TOOL_TYPES: tuple[type[BaseTool], ...] = (
301
+ SolvePlanTool,
302
+ SolveFinishStepTool,
303
+ SolveReplanTool,
304
+ )
305
+
306
+
307
+ __all__ = [
308
+ "SOLVE_TOOL_NAMES",
309
+ "SOLVE_TOOL_TYPES",
310
+ "SolveFinishStepTool",
311
+ "SolvePlanTool",
312
+ "SolveReplanTool",
313
+ ]
@@ -0,0 +1,22 @@
1
+ """Compatibility exports for solve loop-plugin tools.
2
+
3
+ The solve loop capability owns the implementation under
4
+ ``deeptutor.capabilities.solve.tools``. This module keeps a stable import path
5
+ for the built-in tool registry and any external users.
6
+ """
7
+
8
+ from deeptutor.capabilities.solve.tools import (
9
+ SOLVE_TOOL_NAMES,
10
+ SOLVE_TOOL_TYPES,
11
+ SolveFinishStepTool,
12
+ SolvePlanTool,
13
+ SolveReplanTool,
14
+ )
15
+
16
+ __all__ = [
17
+ "SOLVE_TOOL_NAMES",
18
+ "SOLVE_TOOL_TYPES",
19
+ "SolveFinishStepTool",
20
+ "SolvePlanTool",
21
+ "SolveReplanTool",
22
+ ]
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: deeptutor-solve
3
+ Version: 1.7.0
4
+ Summary: DeepTutor solve module
5
+ Requires-Python: <3.15,>=3.11
6
+ Requires-Dist: deeptutor==1.7.0
@@ -0,0 +1,12 @@
1
+ deeptutor/capabilities/solve/__init__.py,sha256=-SLqq9qs1Z57g-zU5JWnAINn30lexonDvUQrlckhWZY,258
2
+ deeptutor/capabilities/solve/capability.py,sha256=rOrLDd6agmohY95xrvf-kRXICDjQ6aFzgyx_LqGRj1Y,3526
3
+ deeptutor/capabilities/solve/loop.py,sha256=WPVUm4g-rpQrH_deij3UE_VRZQ01v_RGRptJd7sZD-s,3074
4
+ deeptutor/capabilities/solve/session.py,sha256=Aixs2rHchSnzBmlD9h9d0OIu4s9D-OHJ55o5Y1Ac7_4,4151
5
+ deeptutor/capabilities/solve/tools.py,sha256=2cxeUDLOYYu0hsWWDzv4iYkJZGqeWrbEYGGavlMuNU0,11946
6
+ deeptutor/capabilities/solve/prompts/en/system.md,sha256=6yzoeqDZtrNzEqwy56iTwZ434EyZL1rzledEvEO_BBk,1380
7
+ deeptutor/capabilities/solve/prompts/zh/system.md,sha256=sb6jKczaCBmEARoTTyM55PlLloP1qdHMMpt_pd1R2Ts,1285
8
+ deeptutor/tools/solve_tool.py,sha256=JYnzNFKJYNDkzDZTvS6jOBn_Hs4PFIt6LU7dci6DwDI,544
9
+ deeptutor_solve-1.7.0.dist-info/METADATA,sha256=k_7DzztRgLQpZCdP3KUlxYFViEyOnTmhwgXq8q6-cgU,153
10
+ deeptutor_solve-1.7.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
11
+ deeptutor_solve-1.7.0.dist-info/top_level.txt,sha256=hnSHUG_vphMnHpT-DbMqcemCKadJ24laFGAWlovG2xM,10
12
+ deeptutor_solve-1.7.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ deeptutor