Skip to content

Commit df844b8

Browse files
committed
feat: add visualize capability with Chart.js/SVG rendering pipeline
- Add visualize agent pipeline (analysis → code generation → review) - Add VisualizeCapability with request config and registry wiring - Add frontend VisualizeConfigPanel, VisualizationViewer components - Add chart.js and react-chartjs-2 dependencies - Improve math_animator YON_IMAGE anchor block prompts (en/zh) - Add minimax model override (supports_response_format: false) - Suppress noisy uvicorn WebSocket connection logs - Add selective HTTP access logging middleware (non-200 only) Made-with: Cursor
1 parent 9a3e718 commit df844b8

32 files changed

Lines changed: 1372 additions & 19 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
4242
> **[2026.4.7]** [v1.0.0-beta.2](https://github.com/HKUDS/DeepTutor/releases/tag/v1.0.0-beta.2) — Runtime cache invalidation for hot settings reload, MinerU nested output support, mimic WebSocket fix, Python 3.11+ minimum, and CI improvements.
4343
44-
> **[2026.4.4]** [v1.0.0-beta.1](https://github.com/HKUDS/DeepTutor/releases/tag/v1.0.0-beta.1) — Agent-native architecture rewrite (DeepTutor 2.0) with two-layer plugin model (Tools + Capabilities), CLI & SDK entry points, TutorBot multi-channel bot agent, Co-Writer, Guided Learning, and persistent memory.
44+
> **[2026.4.4]** [v1.0.0-beta.1](https://github.com/HKUDS/DeepTutor/releases/tag/v1.0.0-beta.1) — Agent-native architecture rewrite (~200k lines) with two-layer plugin model (Tools + Capabilities), CLI & SDK entry points, TutorBot multi-channel bot agent, Co-Writer, Guided Learning, and persistent memory.
4545
4646
<details>
4747
<summary><b>Past releases</b></summary>

deeptutor/agents/math_animator/prompts/en/code_generator_agent.yaml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,20 @@ generate_system: |
33
Produce runnable Python Manim code.
44
Rules:
55
- For video mode, return one complete Manim script with at least one renderable Scene subclass.
6-
- For image mode, return YON_IMAGE anchor blocks only, and each block must contain a standalone renderable Manim script.
6+
- For image mode, return YON_IMAGE anchor blocks only, and each block must contain a standalone renderable Manim script. Format:
7+
### YON_IMAGE_1_START ###
8+
from manim import *
9+
class Scene1(Scene):
10+
def construct(self):
11+
...
12+
### YON_IMAGE_1_END ###
13+
### YON_IMAGE_2_START ###
14+
from manim import *
15+
class Scene2(Scene):
16+
def construct(self):
17+
...
18+
### YON_IMAGE_2_END ###
19+
Anchor numbering starts at 1. The code field must contain nothing outside YON_IMAGE anchor blocks.
720
- Do not include commentary outside the JSON response.
821
- Every geometric point, path point, and vertex passed to Manim must be 3D; do not use `[x, y]` when `[x, y, 0]` is required.
922
- When positions come from axes or planes, prefer helpers such as `axes.c2p(...)` and `plane.c2p(...)` so the result is a valid 3D point.
@@ -59,6 +72,7 @@ retry_system: |
5972
If the error mentions `self.camera.frame`, repair by either:
6073
1) changing the scene base class to `MovingCameraScene`, or
6174
2) removing camera-frame animation while keeping the teaching flow.
75+
If the error mentions `YON_IMAGE`, the image-mode code is missing anchor markers. Wrap the code in `### YON_IMAGE_n_START ###` / `### YON_IMAGE_n_END ###` blocks and ensure the code field contains nothing outside those anchor blocks.
6276
6377
retry_user_template: |
6478
User request:

deeptutor/agents/math_animator/prompts/zh/code_generator_agent.yaml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,20 @@ generate_system: |
33
你要生成可运行的 Python Manim 代码。
44
要求:
55
- video 模式:返回一个完整的 Manim 脚本,至少包含一个可渲染的 Scene 子类。
6-
- image 模式:必须严格输出若干个 YON_IMAGE 锚点代码块,每个代码块内都必须是独立可渲染的 Manim 脚本。
6+
- image 模式:必须严格输出若干个 YON_IMAGE 锚点代码块,每个代码块内都必须是独立可渲染的 Manim 脚本。格式如下:
7+
### YON_IMAGE_1_START ###
8+
from manim import *
9+
class Scene1(Scene):
10+
def construct(self):
11+
...
12+
### YON_IMAGE_1_END ###
13+
### YON_IMAGE_2_START ###
14+
from manim import *
15+
class Scene2(Scene):
16+
def construct(self):
17+
...
18+
### YON_IMAGE_2_END ###
19+
锚点编号从 1 开始递增。code 字段中除了 YON_IMAGE 锚点块外不能有其他代码。
720
- 不要输出解释性文字。
821
- Manim 中所有几何点、路径点、顶点坐标都必须是 3D 形式;不要传 `[x, y]`,要传 `[x, y, 0]`。
922
- 如果坐标来自坐标轴或数轴映射,优先使用 `axes.c2p(...)`、`plane.c2p(...)` 等返回 3D 点的方法。
@@ -60,6 +73,7 @@ retry_system: |
6073
若报错与 `self.camera.frame` 相关,必须二选一修复:
6174
1) 把场景类改为 `MovingCameraScene`;或
6275
2) 删除相机动画语句并保留原教学逻辑。
76+
若报错包含 `YON_IMAGE`,说明 image 模式下代码缺少锚点标记。必须把代码重新包裹在 `### YON_IMAGE_n_START ###` / `### YON_IMAGE_n_END ###` 中,且 code 字段中除锚点块外不能有其他内容。
6377
6478
retry_user_template: |
6579
用户需求:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Visualize agents and pipeline."""
2+
3+
from .pipeline import VisualizePipeline
4+
5+
__all__ = ["VisualizePipeline"]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
]
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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.trace import build_trace_metadata, new_call_id
7+
8+
from ..models import VisualizationAnalysis
9+
from ..utils import extract_json_object
10+
11+
12+
class AnalysisAgent(BaseAgent):
13+
def __init__(
14+
self,
15+
api_key: str | None = None,
16+
base_url: str | None = None,
17+
api_version: str | None = None,
18+
language: str = "zh",
19+
) -> None:
20+
super().__init__(
21+
module_name="visualize",
22+
agent_name="analysis_agent",
23+
api_key=api_key,
24+
base_url=base_url,
25+
api_version=api_version,
26+
language=language,
27+
)
28+
29+
async def process(
30+
self,
31+
*,
32+
user_input: str,
33+
history_context: str,
34+
render_mode: str = "auto",
35+
) -> VisualizationAnalysis:
36+
if render_mode in ("svg", "chartjs"):
37+
system_prompt = self.get_prompt("system_fixed")
38+
user_template = self.get_prompt("user_template_fixed")
39+
else:
40+
system_prompt = self.get_prompt("system")
41+
user_template = self.get_prompt("user_template")
42+
if not system_prompt or not user_template:
43+
raise ValueError("AnalysisAgent prompts are not configured.")
44+
45+
format_kwargs: dict[str, str] = {
46+
"user_input": user_input.strip(),
47+
"history_context": history_context.strip() or "(none)",
48+
}
49+
if render_mode in ("svg", "chartjs"):
50+
format_kwargs["render_type"] = render_mode
51+
52+
user_prompt = user_template.format(**format_kwargs)
53+
54+
chunks: list[str] = []
55+
async for chunk in self.stream_llm(
56+
user_prompt=user_prompt,
57+
system_prompt=system_prompt,
58+
response_format={"type": "json_object"},
59+
stage="analyzing",
60+
trace_meta=build_trace_metadata(
61+
call_id=new_call_id("viz-analysis"),
62+
phase="analyzing",
63+
label="Visualization analysis",
64+
call_kind="viz_analysis",
65+
trace_role="analyze",
66+
trace_kind="llm_output",
67+
),
68+
):
69+
chunks.append(chunk)
70+
response = "".join(chunks)
71+
result = VisualizationAnalysis.model_validate(extract_json_object(response))
72+
if render_mode in ("svg", "chartjs"):
73+
result.render_type = render_mode # type: ignore[assignment]
74+
return result
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
system_prompt = self.get_prompt("system")
39+
user_template = self.get_prompt("user_template")
40+
if not system_prompt or not user_template:
41+
raise ValueError("CodeGeneratorAgent prompts are not configured.")
42+
43+
user_prompt = user_template.format(
44+
user_input=user_input.strip(),
45+
history_context=history_context.strip() or "(none)",
46+
render_type=analysis.render_type,
47+
analysis_json=json.dumps(analysis.model_dump(), ensure_ascii=False, indent=2),
48+
)
49+
50+
chunks: list[str] = []
51+
async for chunk in self.stream_llm(
52+
user_prompt=user_prompt,
53+
system_prompt=system_prompt,
54+
stage="generating",
55+
trace_meta=build_trace_metadata(
56+
call_id=new_call_id("viz-codegen"),
57+
phase="generating",
58+
label="Code generation",
59+
call_kind="viz_code_generation",
60+
trace_role="generate",
61+
trace_kind="llm_output",
62+
),
63+
):
64+
chunks.append(chunk)
65+
response = "".join(chunks)
66+
67+
lang_hint = "svg" if analysis.render_type == "svg" else "javascript"
68+
return extract_code_block(response, lang_hint) or extract_code_block(response)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Review stage: check and optionally optimise the generated code."""
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+
) -> ReviewResult:
38+
system_prompt = self.get_prompt("system")
39+
user_template = self.get_prompt("user_template")
40+
if not system_prompt or not user_template:
41+
raise ValueError("ReviewAgent prompts are not configured.")
42+
43+
user_prompt = user_template.format(
44+
user_input=user_input.strip(),
45+
render_type=analysis.render_type,
46+
analysis_json=json.dumps(analysis.model_dump(), ensure_ascii=False, indent=2),
47+
code=code,
48+
)
49+
50+
chunks: list[str] = []
51+
async for chunk in self.stream_llm(
52+
user_prompt=user_prompt,
53+
system_prompt=system_prompt,
54+
response_format={"type": "json_object"},
55+
stage="reviewing",
56+
trace_meta=build_trace_metadata(
57+
call_id=new_call_id("viz-review"),
58+
phase="reviewing",
59+
label="Code review",
60+
call_kind="viz_code_review",
61+
trace_role="review",
62+
trace_kind="llm_output",
63+
),
64+
):
65+
chunks.append(chunk)
66+
response = "".join(chunks)
67+
return ReviewResult.model_validate(extract_json_object(response))
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Data models for the visualize pipeline."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Literal
6+
7+
from pydantic import BaseModel, Field
8+
9+
10+
class VisualizationAnalysis(BaseModel):
11+
"""Output of the analysis stage."""
12+
13+
render_type: Literal["svg", "chartjs"] = Field(
14+
description="Whether to render as raw SVG or as a Chart.js configuration.",
15+
)
16+
description: str = Field(
17+
default="",
18+
description="High-level description of what the visualization should show.",
19+
)
20+
data_description: str = Field(
21+
default="",
22+
description="Description of the data or elements to be visualized.",
23+
)
24+
chart_type: str = Field(
25+
default="",
26+
description="Chart.js chart type (bar, line, pie, doughnut, radar, etc.) when render_type is chartjs.",
27+
)
28+
visual_elements: list[str] = Field(
29+
default_factory=list,
30+
description="Key visual elements to include (shapes, labels, axes, colors, etc.).",
31+
)
32+
rationale: str = Field(
33+
default="",
34+
description="Why this render_type was chosen over the alternative.",
35+
)
36+
37+
38+
class ReviewResult(BaseModel):
39+
"""Output of the review / optimization stage."""
40+
41+
optimized_code: str = Field(
42+
description="The final (potentially optimized) visualization code.",
43+
)
44+
changed: bool = Field(
45+
default=False,
46+
description="Whether the reviewer made modifications.",
47+
)
48+
review_notes: str = Field(
49+
default="",
50+
description="Notes on what was checked or changed.",
51+
)

0 commit comments

Comments
 (0)