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.
Files changed (30) hide show
  1. deeptutor/agents/visualize/__init__.py +5 -0
  2. deeptutor/agents/visualize/agents/__init__.py +11 -0
  3. deeptutor/agents/visualize/agents/analysis_agent.py +102 -0
  4. deeptutor/agents/visualize/agents/code_generator_agent.py +117 -0
  5. deeptutor/agents/visualize/agents/review_agent.py +75 -0
  6. deeptutor/agents/visualize/capability.py +571 -0
  7. deeptutor/agents/visualize/models.py +120 -0
  8. deeptutor/agents/visualize/pipeline.py +91 -0
  9. deeptutor/agents/visualize/prompts/en/analysis_agent.yaml +132 -0
  10. deeptutor/agents/visualize/prompts/en/code_generator_agent.yaml +189 -0
  11. deeptutor/agents/visualize/prompts/en/review_agent.yaml +38 -0
  12. deeptutor/agents/visualize/prompts/en/visualize.yaml +14 -0
  13. deeptutor/agents/visualize/prompts/zh/analysis_agent.yaml +123 -0
  14. deeptutor/agents/visualize/prompts/zh/code_generator_agent.yaml +161 -0
  15. deeptutor/agents/visualize/prompts/zh/review_agent.yaml +35 -0
  16. deeptutor/agents/visualize/prompts/zh/visualize.yaml +14 -0
  17. deeptutor/agents/visualize/utils.py +210 -0
  18. deeptutor/api/routers/visualizers.py +144 -0
  19. deeptutor/visualizers/README.md +79 -0
  20. deeptutor/visualizers/__init__.py +20 -0
  21. deeptutor/visualizers/builtin.py +369 -0
  22. deeptutor/visualizers/loop_capability.py +125 -0
  23. deeptutor/visualizers/protocol.py +225 -0
  24. deeptutor/visualizers/registry.py +155 -0
  25. deeptutor/visualizers/store.py +260 -0
  26. deeptutor/visualizers/tool.py +150 -0
  27. deeptutor_visualize-1.7.0.dist-info/METADATA +6 -0
  28. deeptutor_visualize-1.7.0.dist-info/RECORD +30 -0
  29. deeptutor_visualize-1.7.0.dist-info/WHEEL +5 -0
  30. deeptutor_visualize-1.7.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ """Visualize agents and pipeline."""
2
+
3
+ from .pipeline import VisualizePipeline
4
+
5
+ __all__ = ["VisualizePipeline"]
@@ -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))