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.
- deeptutor/capabilities/solve/__init__.py +6 -0
- deeptutor/capabilities/solve/capability.py +87 -0
- deeptutor/capabilities/solve/loop.py +83 -0
- deeptutor/capabilities/solve/prompts/en/system.md +13 -0
- deeptutor/capabilities/solve/prompts/zh/system.md +13 -0
- deeptutor/capabilities/solve/session.py +119 -0
- deeptutor/capabilities/solve/tools.py +313 -0
- deeptutor/tools/solve_tool.py +22 -0
- deeptutor_solve-1.7.0.dist-info/METADATA +6 -0
- deeptutor_solve-1.7.0.dist-info/RECORD +12 -0
- deeptutor_solve-1.7.0.dist-info/WHEEL +5 -0
- deeptutor_solve-1.7.0.dist-info/top_level.txt +1 -0
|
@@ -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,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 @@
|
|
|
1
|
+
deeptutor
|