deeptutor-visualize 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/agents/visualize/__init__.py +5 -0
- deeptutor/agents/visualize/agents/__init__.py +11 -0
- deeptutor/agents/visualize/agents/analysis_agent.py +102 -0
- deeptutor/agents/visualize/agents/code_generator_agent.py +117 -0
- deeptutor/agents/visualize/agents/review_agent.py +75 -0
- deeptutor/agents/visualize/capability.py +571 -0
- deeptutor/agents/visualize/models.py +120 -0
- deeptutor/agents/visualize/pipeline.py +91 -0
- deeptutor/agents/visualize/prompts/en/analysis_agent.yaml +132 -0
- deeptutor/agents/visualize/prompts/en/code_generator_agent.yaml +189 -0
- deeptutor/agents/visualize/prompts/en/review_agent.yaml +38 -0
- deeptutor/agents/visualize/prompts/en/visualize.yaml +14 -0
- deeptutor/agents/visualize/prompts/zh/analysis_agent.yaml +123 -0
- deeptutor/agents/visualize/prompts/zh/code_generator_agent.yaml +161 -0
- deeptutor/agents/visualize/prompts/zh/review_agent.yaml +35 -0
- deeptutor/agents/visualize/prompts/zh/visualize.yaml +14 -0
- deeptutor/agents/visualize/utils.py +210 -0
- deeptutor/api/routers/visualizers.py +144 -0
- deeptutor/visualizers/README.md +79 -0
- deeptutor/visualizers/__init__.py +20 -0
- deeptutor/visualizers/builtin.py +369 -0
- deeptutor/visualizers/loop_capability.py +125 -0
- deeptutor/visualizers/protocol.py +225 -0
- deeptutor/visualizers/registry.py +155 -0
- deeptutor/visualizers/store.py +260 -0
- deeptutor/visualizers/tool.py +150 -0
- deeptutor_visualize-1.7.0.dist-info/METADATA +6 -0
- deeptutor_visualize-1.7.0.dist-info/RECORD +30 -0
- deeptutor_visualize-1.7.0.dist-info/WHEEL +5 -0
- deeptutor_visualize-1.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Agent building blocks for the visualize capability."""
|
|
2
|
+
|
|
3
|
+
from .analysis_agent import AnalysisAgent
|
|
4
|
+
from .code_generator_agent import CodeGeneratorAgent
|
|
5
|
+
from .review_agent import ReviewAgent
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AnalysisAgent",
|
|
9
|
+
"CodeGeneratorAgent",
|
|
10
|
+
"ReviewAgent",
|
|
11
|
+
]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Analysis stage: decide SVG vs Chart.js and produce a structured brief."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from deeptutor.agents.base_agent import BaseAgent
|
|
6
|
+
from deeptutor.core.context import Attachment
|
|
7
|
+
from deeptutor.core.trace import build_trace_metadata, new_call_id
|
|
8
|
+
|
|
9
|
+
from ..models import VisualizationAnalysis
|
|
10
|
+
from ..utils import extract_json_object
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AnalysisAgent(BaseAgent):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
api_key: str | None = None,
|
|
17
|
+
base_url: str | None = None,
|
|
18
|
+
api_version: str | None = None,
|
|
19
|
+
language: str = "zh",
|
|
20
|
+
) -> None:
|
|
21
|
+
super().__init__(
|
|
22
|
+
module_name="visualize",
|
|
23
|
+
agent_name="analysis_agent",
|
|
24
|
+
api_key=api_key,
|
|
25
|
+
base_url=base_url,
|
|
26
|
+
api_version=api_version,
|
|
27
|
+
language=language,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
async def process(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
user_input: str,
|
|
34
|
+
history_context: str,
|
|
35
|
+
render_mode: str = "auto",
|
|
36
|
+
attachments: list[Attachment] | None = None,
|
|
37
|
+
) -> VisualizationAnalysis:
|
|
38
|
+
# Manim modes short-circuit the LLM call entirely — MathAnimatorPipeline
|
|
39
|
+
# has its own ConceptAnalysisAgent that produces the manim-specific
|
|
40
|
+
# design brief. We only need a stub VisualizationAnalysis here so the
|
|
41
|
+
# capability runner can dispatch on render_type.
|
|
42
|
+
if render_mode in ("manim_video", "manim_image"):
|
|
43
|
+
return VisualizationAnalysis(
|
|
44
|
+
render_type=render_mode, # type: ignore[arg-type]
|
|
45
|
+
description="",
|
|
46
|
+
data_description="",
|
|
47
|
+
chart_type="",
|
|
48
|
+
visual_elements=[],
|
|
49
|
+
rationale=f"User selected {render_mode} explicitly.",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if render_mode in ("svg", "chartjs", "mermaid", "mindmap", "html"):
|
|
53
|
+
system_prompt = self.get_prompt("system_fixed")
|
|
54
|
+
user_template = self.get_prompt("user_template_fixed")
|
|
55
|
+
elif render_mode == "figure":
|
|
56
|
+
# Constrained-auto mode: LLM picks one of svg/chartjs/mermaid
|
|
57
|
+
# (html is excluded). Used by the Book figure block.
|
|
58
|
+
system_prompt = self.get_prompt("system_figure")
|
|
59
|
+
user_template = self.get_prompt("user_template_figure")
|
|
60
|
+
else:
|
|
61
|
+
system_prompt = self.get_prompt("system")
|
|
62
|
+
user_template = self.get_prompt("user_template")
|
|
63
|
+
if not system_prompt or not user_template:
|
|
64
|
+
raise ValueError("AnalysisAgent prompts are not configured.")
|
|
65
|
+
|
|
66
|
+
format_kwargs: dict[str, str] = {
|
|
67
|
+
"user_input": user_input.strip(),
|
|
68
|
+
"history_context": history_context.strip() or "(none)",
|
|
69
|
+
}
|
|
70
|
+
if render_mode in ("svg", "chartjs", "mermaid", "mindmap", "html"):
|
|
71
|
+
format_kwargs["render_type"] = render_mode
|
|
72
|
+
|
|
73
|
+
user_prompt = user_template.format(**format_kwargs)
|
|
74
|
+
|
|
75
|
+
# Blocking rather than streamed: nothing consumes the partial JSON, and
|
|
76
|
+
# a reasoning model's <think> prelude never reaches the parser this way.
|
|
77
|
+
raw = await self.call_llm(
|
|
78
|
+
user_prompt=user_prompt,
|
|
79
|
+
system_prompt=system_prompt,
|
|
80
|
+
response_format={"type": "json_object"},
|
|
81
|
+
stage="analyzing",
|
|
82
|
+
attachments=attachments,
|
|
83
|
+
trace_meta=build_trace_metadata(
|
|
84
|
+
call_id=new_call_id("viz-analysis"),
|
|
85
|
+
phase="analyzing",
|
|
86
|
+
label="Visualization analysis",
|
|
87
|
+
call_kind="viz_analysis",
|
|
88
|
+
trace_role="analyze",
|
|
89
|
+
trace_kind="llm_output",
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
result = VisualizationAnalysis.model_validate(extract_json_object(raw))
|
|
93
|
+
if render_mode in ("svg", "chartjs", "mermaid", "mindmap", "html"):
|
|
94
|
+
result.render_type = render_mode # type: ignore[assignment]
|
|
95
|
+
elif render_mode == "figure" and result.render_type not in (
|
|
96
|
+
"svg",
|
|
97
|
+
"chartjs",
|
|
98
|
+
"mermaid",
|
|
99
|
+
):
|
|
100
|
+
# Defensive: if the LLM ignored the constraint, force a safe default.
|
|
101
|
+
result.render_type = "svg" # type: ignore[assignment]
|
|
102
|
+
return result
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Code generation stage: produce SVG or Chart.js code from the analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from deeptutor.agents.base_agent import BaseAgent
|
|
8
|
+
from deeptutor.core.trace import build_trace_metadata, new_call_id
|
|
9
|
+
|
|
10
|
+
from ..models import VisualizationAnalysis
|
|
11
|
+
from ..utils import extract_code_block
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CodeGeneratorAgent(BaseAgent):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
api_key: str | None = None,
|
|
18
|
+
base_url: str | None = None,
|
|
19
|
+
api_version: str | None = None,
|
|
20
|
+
language: str = "zh",
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__(
|
|
23
|
+
module_name="visualize",
|
|
24
|
+
agent_name="code_generator_agent",
|
|
25
|
+
api_key=api_key,
|
|
26
|
+
base_url=base_url,
|
|
27
|
+
api_version=api_version,
|
|
28
|
+
language=language,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
async def process(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
user_input: str,
|
|
35
|
+
history_context: str,
|
|
36
|
+
analysis: VisualizationAnalysis,
|
|
37
|
+
) -> str:
|
|
38
|
+
# Structured prompt assembly: every render type loads the shared
|
|
39
|
+
# base + general rules, plus exactly one format-specific rule block.
|
|
40
|
+
# This keeps the per-call prompt dense with the rules that matter for
|
|
41
|
+
# *this* format without ever paying for the union of all four.
|
|
42
|
+
# render_type here is always one of svg/chartjs/mermaid/html — manim
|
|
43
|
+
# is dispatched away in the capability layer before code generation.
|
|
44
|
+
system_parts = (
|
|
45
|
+
self.get_prompt("system_base"),
|
|
46
|
+
self.get_prompt("rules_general"),
|
|
47
|
+
self.get_prompt(f"rules_{analysis.render_type}"),
|
|
48
|
+
)
|
|
49
|
+
system_prompt = "\n\n".join(part.strip() for part in system_parts if part and part.strip())
|
|
50
|
+
user_template = self.get_prompt("user_template")
|
|
51
|
+
if not system_prompt or not user_template:
|
|
52
|
+
raise ValueError("CodeGeneratorAgent prompts are not configured.")
|
|
53
|
+
|
|
54
|
+
user_prompt = user_template.format(
|
|
55
|
+
user_input=user_input.strip(),
|
|
56
|
+
history_context=history_context.strip() or "(none)",
|
|
57
|
+
render_type=analysis.render_type,
|
|
58
|
+
analysis_json=json.dumps(analysis.model_dump(), ensure_ascii=False, indent=2),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Blocking rather than streamed: the fenced block is only usable whole.
|
|
62
|
+
response = await self.call_llm(
|
|
63
|
+
user_prompt=user_prompt,
|
|
64
|
+
system_prompt=system_prompt,
|
|
65
|
+
stage="generating",
|
|
66
|
+
trace_meta=build_trace_metadata(
|
|
67
|
+
call_id=new_call_id("viz-codegen"),
|
|
68
|
+
phase="generating",
|
|
69
|
+
label="Code generation",
|
|
70
|
+
call_kind="viz_code_generation",
|
|
71
|
+
trace_role="generate",
|
|
72
|
+
trace_kind="llm_output",
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if analysis.render_type == "svg":
|
|
77
|
+
lang_hint = "svg"
|
|
78
|
+
elif analysis.render_type in ("mermaid", "mindmap"):
|
|
79
|
+
lang_hint = "mermaid"
|
|
80
|
+
elif analysis.render_type == "html":
|
|
81
|
+
lang_hint = "html"
|
|
82
|
+
else:
|
|
83
|
+
lang_hint = "javascript"
|
|
84
|
+
|
|
85
|
+
extracted = extract_code_block(response, lang_hint) or extract_code_block(response)
|
|
86
|
+
|
|
87
|
+
# For html, the model sometimes returns the full document with no fence.
|
|
88
|
+
# `extract_code_block` will then return the trimmed raw response — accept
|
|
89
|
+
# it as long as it looks like an HTML document.
|
|
90
|
+
if analysis.render_type == "html" and not extracted:
|
|
91
|
+
stripped = (response or "").strip()
|
|
92
|
+
lowered = stripped.lower()
|
|
93
|
+
if lowered.startswith("<!doctype") or lowered.startswith("<html"):
|
|
94
|
+
return stripped
|
|
95
|
+
|
|
96
|
+
# The renderers expect their payload to start cleanly with the right
|
|
97
|
+
# root tag. Models often wrap output with prose ("Here you go: <svg>…")
|
|
98
|
+
# or emit a code fence on the same line as the closing tag, which
|
|
99
|
+
# `extract_code_block`'s regex (requiring a leading \n before ```) does
|
|
100
|
+
# not strip. Trim to the outermost root tags as a defensive net.
|
|
101
|
+
if extracted:
|
|
102
|
+
if analysis.render_type == "svg":
|
|
103
|
+
lower = extracted.lower()
|
|
104
|
+
start = lower.find("<svg")
|
|
105
|
+
end = lower.rfind("</svg>")
|
|
106
|
+
if start != -1 and end != -1 and end > start:
|
|
107
|
+
extracted = extracted[start : end + len("</svg>")]
|
|
108
|
+
elif analysis.render_type == "html":
|
|
109
|
+
lower = extracted.lower()
|
|
110
|
+
start = lower.find("<!doctype")
|
|
111
|
+
if start == -1:
|
|
112
|
+
start = lower.find("<html")
|
|
113
|
+
end = lower.rfind("</html>")
|
|
114
|
+
if start != -1 and end != -1 and end > start:
|
|
115
|
+
extracted = extracted[start : end + len("</html>")]
|
|
116
|
+
|
|
117
|
+
return extracted
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Repair stage: targeted fix after deterministic local validation fails."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from deeptutor.agents.base_agent import BaseAgent
|
|
8
|
+
from deeptutor.core.trace import build_trace_metadata, new_call_id
|
|
9
|
+
|
|
10
|
+
from ..models import ReviewResult, VisualizationAnalysis
|
|
11
|
+
from ..utils import extract_json_object
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ReviewAgent(BaseAgent):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
api_key: str | None = None,
|
|
18
|
+
base_url: str | None = None,
|
|
19
|
+
api_version: str | None = None,
|
|
20
|
+
language: str = "zh",
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__(
|
|
23
|
+
module_name="visualize",
|
|
24
|
+
agent_name="review_agent",
|
|
25
|
+
api_key=api_key,
|
|
26
|
+
base_url=base_url,
|
|
27
|
+
api_version=api_version,
|
|
28
|
+
language=language,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
async def process(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
user_input: str,
|
|
35
|
+
analysis: VisualizationAnalysis,
|
|
36
|
+
code: str,
|
|
37
|
+
error: str,
|
|
38
|
+
) -> ReviewResult:
|
|
39
|
+
"""Targeted repair pass — this agent's single job.
|
|
40
|
+
|
|
41
|
+
Only invoked *after* the deterministic local validation has failed,
|
|
42
|
+
and handed the concrete error so the model fixes one specific defect
|
|
43
|
+
instead of re-judging everything (chat and Book share this path).
|
|
44
|
+
"""
|
|
45
|
+
system_prompt = self.get_prompt("repair_system")
|
|
46
|
+
user_template = self.get_prompt("repair_user_template")
|
|
47
|
+
if not system_prompt or not user_template:
|
|
48
|
+
raise ValueError("ReviewAgent repair prompts are not configured.")
|
|
49
|
+
|
|
50
|
+
user_prompt = user_template.format(
|
|
51
|
+
user_input=user_input.strip(),
|
|
52
|
+
render_type=analysis.render_type,
|
|
53
|
+
error=error.strip() or "(unspecified)",
|
|
54
|
+
analysis_json=json.dumps(analysis.model_dump(), ensure_ascii=False, indent=2),
|
|
55
|
+
code=code,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
chunks: list[str] = []
|
|
59
|
+
async for chunk in self.stream_llm(
|
|
60
|
+
user_prompt=user_prompt,
|
|
61
|
+
system_prompt=system_prompt,
|
|
62
|
+
response_format={"type": "json_object"},
|
|
63
|
+
stage="reviewing",
|
|
64
|
+
trace_meta=build_trace_metadata(
|
|
65
|
+
call_id=new_call_id("viz-repair"),
|
|
66
|
+
phase="reviewing",
|
|
67
|
+
label="Code repair",
|
|
68
|
+
call_kind="viz_code_repair",
|
|
69
|
+
trace_role="review",
|
|
70
|
+
trace_kind="llm_output",
|
|
71
|
+
),
|
|
72
|
+
):
|
|
73
|
+
chunks.append(chunk)
|
|
74
|
+
response = "".join(chunks)
|
|
75
|
+
return ReviewResult.model_validate(extract_json_object(response))
|