deeptutor-renderer-manim 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 (32) hide show
  1. deeptutor/agents/math_animator/__init__.py +23 -0
  2. deeptutor/agents/math_animator/agents/__init__.py +15 -0
  3. deeptutor/agents/math_animator/agents/code_generator_agent.py +268 -0
  4. deeptutor/agents/math_animator/agents/concept_analysis_agent.py +97 -0
  5. deeptutor/agents/math_animator/agents/concept_design_agent.py +76 -0
  6. deeptutor/agents/math_animator/agents/summary_agent.py +78 -0
  7. deeptutor/agents/math_animator/agents/visual_review_agent.py +100 -0
  8. deeptutor/agents/math_animator/capability.py +301 -0
  9. deeptutor/agents/math_animator/duration_utils.py +36 -0
  10. deeptutor/agents/math_animator/models.py +97 -0
  11. deeptutor/agents/math_animator/pipeline.py +326 -0
  12. deeptutor/agents/math_animator/prompts/en/code_generator_agent.yaml +95 -0
  13. deeptutor/agents/math_animator/prompts/en/concept_analysis_agent.yaml +26 -0
  14. deeptutor/agents/math_animator/prompts/en/concept_design_agent.yaml +47 -0
  15. deeptutor/agents/math_animator/prompts/en/math_animator.yaml +7 -0
  16. deeptutor/agents/math_animator/prompts/en/summary_agent.yaml +27 -0
  17. deeptutor/agents/math_animator/prompts/en/visual_review_agent.yaml +31 -0
  18. deeptutor/agents/math_animator/prompts/zh/code_generator_agent.yaml +96 -0
  19. deeptutor/agents/math_animator/prompts/zh/concept_analysis_agent.yaml +26 -0
  20. deeptutor/agents/math_animator/prompts/zh/concept_design_agent.yaml +48 -0
  21. deeptutor/agents/math_animator/prompts/zh/math_animator.yaml +7 -0
  22. deeptutor/agents/math_animator/prompts/zh/summary_agent.yaml +27 -0
  23. deeptutor/agents/math_animator/prompts/zh/visual_review_agent.yaml +31 -0
  24. deeptutor/agents/math_animator/renderer.py +288 -0
  25. deeptutor/agents/math_animator/request_config.py +38 -0
  26. deeptutor/agents/math_animator/retry_manager.py +160 -0
  27. deeptutor/agents/math_animator/utils.py +124 -0
  28. deeptutor/agents/math_animator/visual_review.py +154 -0
  29. deeptutor_renderer_manim-1.7.0.dist-info/METADATA +7 -0
  30. deeptutor_renderer_manim-1.7.0.dist-info/RECORD +32 -0
  31. deeptutor_renderer_manim-1.7.0.dist-info/WHEEL +5 -0
  32. deeptutor_renderer_manim-1.7.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,23 @@
1
+ """Math animator agents and pipeline."""
2
+
3
+ from importlib import import_module
4
+ from typing import Any
5
+
6
+ from .request_config import (
7
+ MathAnimatorRequestConfig,
8
+ validate_math_animator_request_config,
9
+ )
10
+
11
+ __all__ = [
12
+ "MathAnimatorPipeline",
13
+ "MathAnimatorRequestConfig",
14
+ "validate_math_animator_request_config",
15
+ ]
16
+
17
+
18
+ def __getattr__(name: str) -> Any:
19
+ if name == "MathAnimatorPipeline":
20
+ value = import_module(f"{__name__}.pipeline").MathAnimatorPipeline
21
+ globals()[name] = value
22
+ return value
23
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,15 @@
1
+ """Agent building blocks for the math animator capability."""
2
+
3
+ from .code_generator_agent import CodeGeneratorAgent
4
+ from .concept_analysis_agent import ConceptAnalysisAgent
5
+ from .concept_design_agent import ConceptDesignAgent
6
+ from .summary_agent import SummaryAgent
7
+ from .visual_review_agent import VisualReviewAgent
8
+
9
+ __all__ = [
10
+ "CodeGeneratorAgent",
11
+ "ConceptAnalysisAgent",
12
+ "ConceptDesignAgent",
13
+ "SummaryAgent",
14
+ "VisualReviewAgent",
15
+ ]
@@ -0,0 +1,268 @@
1
+ """Code generation and repair stages for math animator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ from typing import Any
8
+
9
+ from deeptutor.agents.base_agent import BaseAgent
10
+ from deeptutor.core.trace import build_trace_metadata, new_call_id
11
+ from deeptutor.services.llm import StreamOutcome
12
+ from deeptutor.services.llm.reasoning_params import RETRY_REASONING_EFFORT
13
+
14
+ from ..models import ConceptAnalysis, GeneratedCode, SceneDesign
15
+ from ..utils import (
16
+ build_repair_error_message,
17
+ describe_unusable_output,
18
+ escalated_max_tokens,
19
+ extract_json_object,
20
+ )
21
+
22
+ #: How much of a failed response to keep in the debug log. Enough to see where
23
+ #: a truncated generation stopped, short enough to stay readable (#1545).
24
+ _RAW_EXCERPT_CHARS = 200
25
+
26
+
27
+ class GeneratedCodeOutputError(ValueError):
28
+ """The model exhausted its retries without returning runnable code."""
29
+
30
+
31
+ class CodeGeneratorAgent(BaseAgent):
32
+ def __init__(
33
+ self,
34
+ api_key: str | None = None,
35
+ base_url: str | None = None,
36
+ api_version: str | None = None,
37
+ language: str = "zh",
38
+ ) -> None:
39
+ super().__init__(
40
+ module_name="math_animator",
41
+ agent_name="code_generator_agent",
42
+ api_key=api_key,
43
+ base_url=base_url,
44
+ api_version=api_version,
45
+ language=language,
46
+ )
47
+
48
+ async def process(
49
+ self,
50
+ *,
51
+ user_input: str,
52
+ output_mode: str,
53
+ analysis: ConceptAnalysis,
54
+ design: SceneDesign,
55
+ duration_target_seconds: float | None = None,
56
+ ) -> GeneratedCode:
57
+ """BaseAgent-compatible entrypoint for the default generation path."""
58
+ return await self.generate(
59
+ user_input=user_input,
60
+ output_mode=output_mode,
61
+ analysis=analysis,
62
+ design=design,
63
+ duration_target_seconds=duration_target_seconds,
64
+ )
65
+
66
+ async def generate(
67
+ self,
68
+ *,
69
+ user_input: str,
70
+ output_mode: str,
71
+ analysis: ConceptAnalysis,
72
+ design: SceneDesign,
73
+ duration_target_seconds: float | None = None,
74
+ ) -> GeneratedCode:
75
+ system_prompt = self.get_prompt("generate_system")
76
+ user_template = self.get_prompt("generate_user_template")
77
+ if not system_prompt or not user_template:
78
+ raise ValueError("CodeGeneratorAgent generation prompts are not configured.")
79
+
80
+ user_prompt = user_template.format(
81
+ user_input=user_input.strip(),
82
+ output_mode=output_mode,
83
+ duration_requirement=(
84
+ f"用户明确目标时长约 {duration_target_seconds:.1f} 秒,生成代码必须围绕该时长做节奏预算。"
85
+ if duration_target_seconds is not None
86
+ else "用户未给出明确秒数时长,可按标准教学节奏生成。"
87
+ ),
88
+ analysis_json=json.dumps(analysis.model_dump(), ensure_ascii=False, indent=2),
89
+ design_json=json.dumps(design.model_dump(), ensure_ascii=False, indent=2),
90
+ )
91
+ return await self._request_generated_code(
92
+ user_prompt=user_prompt,
93
+ system_prompt=system_prompt,
94
+ stage="code_generation",
95
+ call_id_prefix="math-codegen",
96
+ trace_meta={
97
+ "phase": "code_generation",
98
+ "label": "Code generation",
99
+ "call_kind": "math_code_generation",
100
+ "trace_role": "generate",
101
+ "trace_kind": "llm_output",
102
+ },
103
+ )
104
+
105
+ async def repair(
106
+ self,
107
+ *,
108
+ user_input: str,
109
+ output_mode: str,
110
+ current_code: str,
111
+ error_message: str,
112
+ attempt: int,
113
+ duration_target_seconds: float | None = None,
114
+ ) -> GeneratedCode:
115
+ system_prompt = self.get_prompt("retry_system")
116
+ user_template = self.get_prompt("retry_user_template")
117
+ if not system_prompt or not user_template:
118
+ raise ValueError("CodeGeneratorAgent retry prompts are not configured.")
119
+
120
+ user_prompt = user_template.format(
121
+ user_input=user_input.strip(),
122
+ output_mode=output_mode,
123
+ attempt=attempt,
124
+ duration_requirement=(
125
+ f"目标时长约 {duration_target_seconds:.1f} 秒,修复后仍需保持接近该时长。"
126
+ if duration_target_seconds is not None
127
+ else "无明确目标时长。"
128
+ ),
129
+ error_message=build_repair_error_message(error_message),
130
+ current_code=current_code,
131
+ )
132
+ return await self._request_generated_code(
133
+ user_prompt=user_prompt,
134
+ system_prompt=system_prompt,
135
+ stage="code_retry",
136
+ call_id_prefix="math-retry",
137
+ trace_meta={
138
+ "phase": "code_retry",
139
+ "label": f"Code retry #{attempt}",
140
+ "call_kind": "math_code_retry",
141
+ "trace_role": "repair",
142
+ "trace_kind": "llm_output",
143
+ "attempt": attempt,
144
+ },
145
+ )
146
+
147
+ async def _request_generated_code(
148
+ self,
149
+ *,
150
+ user_prompt: str,
151
+ system_prompt: str,
152
+ stage: str,
153
+ call_id_prefix: str,
154
+ trace_meta: dict[str, Any],
155
+ ) -> GeneratedCode:
156
+ """Retry model-success responses that contain no usable code.
157
+
158
+ Provider retries already cover transport failures. This second,
159
+ deliberately narrow boundary covers a successful response whose
160
+ content is blank, reasoning-only, or malformed JSON (#1202). Parsing
161
+ stays strict and callers never proceed to the renderer with ``code=''``.
162
+
163
+ A retry is not a repeat. The provider reports whether it stopped at the
164
+ output cap, and a reasoning model that spent the whole budget on
165
+ chain-of-thought fails that way every time on the same request (#1547),
166
+ so a truncated attempt is retried with a larger budget and lower
167
+ reasoning effort. Whichever way the attempt failed, the
168
+ reason reaches both the log and the raised error (#1545).
169
+ """
170
+
171
+ max_retries = max(0, int(self.get_max_retries()))
172
+ base_max_tokens = max(0, int(self.get_max_tokens() or 0))
173
+ attempts = max_retries + 1
174
+ last_error: Exception | None = None
175
+ last_failure = ""
176
+ truncations = 0
177
+ for structured_attempt in range(attempts):
178
+ max_tokens = escalated_max_tokens(base_max_tokens, truncations)
179
+ retry_instruction = self._retry_instruction(
180
+ attempt=structured_attempt, after_truncation=truncations > 0
181
+ )
182
+ outcome = StreamOutcome()
183
+ chunks: list[str] = []
184
+ async for chunk in self.stream_llm(
185
+ user_prompt=user_prompt + retry_instruction,
186
+ system_prompt=system_prompt,
187
+ response_format={"type": "json_object"},
188
+ max_tokens=max_tokens or None,
189
+ reasoning_effort=(RETRY_REASONING_EFFORT if structured_attempt else None),
190
+ stage=stage,
191
+ trace_meta=build_trace_metadata(
192
+ call_id=new_call_id(call_id_prefix),
193
+ **trace_meta,
194
+ structured_attempt=structured_attempt + 1,
195
+ ),
196
+ outcome=outcome,
197
+ ):
198
+ chunks.append(chunk)
199
+ raw_response = "".join(chunks)
200
+
201
+ try:
202
+ generated = GeneratedCode.model_validate(extract_json_object(raw_response))
203
+ if not generated.code.strip():
204
+ raise ValueError("structured response has an empty code field")
205
+ return generated
206
+ except (json.JSONDecodeError, ValueError) as exc:
207
+ last_error = exc
208
+ last_failure = describe_unusable_output(
209
+ error=exc,
210
+ raw_response=raw_response,
211
+ outcome=outcome,
212
+ max_tokens=max_tokens,
213
+ )
214
+ if outcome.truncated:
215
+ truncations += 1
216
+ self.logger.warning(
217
+ "Math animator %s attempt %d/%d produced no usable code: %s",
218
+ stage,
219
+ structured_attempt + 1,
220
+ attempts,
221
+ last_failure,
222
+ )
223
+ self.logger.debug(
224
+ "Math animator %s attempt %d raw response head=%r tail=%r",
225
+ stage,
226
+ structured_attempt + 1,
227
+ raw_response[:_RAW_EXCERPT_CHARS],
228
+ raw_response[-_RAW_EXCERPT_CHARS:],
229
+ )
230
+ if structured_attempt >= max_retries:
231
+ break
232
+ # Once the budget cannot grow, another truncated call repeats
233
+ # the same failure instead of repairing it (#1914).
234
+ if (
235
+ outcome.truncated
236
+ and escalated_max_tokens(base_max_tokens, truncations) <= max_tokens
237
+ ):
238
+ break
239
+ await asyncio.sleep(min(0.25 * (2**structured_attempt), 2.0))
240
+
241
+ raise GeneratedCodeOutputError(
242
+ f"Math animator {stage} returned no usable code after {structured_attempt + 1} attempts. "
243
+ f"Last attempt: {last_failure}"
244
+ ) from last_error
245
+
246
+ @staticmethod
247
+ def _retry_instruction(*, attempt: int, after_truncation: bool) -> str:
248
+ """The nudge appended to the prompt on retry, matched to the failure."""
249
+
250
+ if not attempt:
251
+ return ""
252
+ if after_truncation:
253
+ # Asking again for "structured code" is useless when the previous
254
+ # response was a complete thought and an incomplete answer: the
255
+ # budget, not the format, is what ran out (#1547).
256
+ return (
257
+ "\n\nYour previous response hit the output token limit before the JSON "
258
+ "was complete. Keep your internal reasoning to a few sentences and spend "
259
+ "the budget on the code itself. Return exactly one JSON object with a "
260
+ "non-empty `code` field and nothing else."
261
+ )
262
+ return (
263
+ "\n\nYour previous response contained no usable structured code. "
264
+ "Return exactly one JSON object with a non-empty `code` field."
265
+ )
266
+
267
+
268
+ __all__ = ["CodeGeneratorAgent", "GeneratedCodeOutputError"]
@@ -0,0 +1,97 @@
1
+ """Concept analysis stage for math animator."""
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
+ from deeptutor.services.llm.structured_retry import json_with_reasoning_retry
9
+ from deeptutor.services.prompt import get_prompt_manager
10
+
11
+ from ..models import ConceptAnalysis
12
+
13
+
14
+ class ConceptAnalysisAgent(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="math_animator",
24
+ agent_name="concept_analysis_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
+ output_mode: str,
37
+ style_hint: str,
38
+ attachments: list[Attachment],
39
+ ) -> ConceptAnalysis:
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
+ # Retry once: a worker that looked the prompts up before the
44
+ # package finished installing used to cache the empty result
45
+ # forever. PromptManager no longer caches misses, so a reload can
46
+ # now actually recover — and it stays the one place that knows how
47
+ # to find a prompt file.
48
+ self.prompts = get_prompt_manager().reload_prompts(
49
+ "math_animator", "concept_analysis_agent", self.language
50
+ )
51
+ system_prompt = self.get_prompt("system")
52
+ user_template = self.get_prompt("user_template")
53
+
54
+ if not system_prompt or not user_template:
55
+ raise ValueError("ConceptAnalysisAgent prompts are not configured.")
56
+
57
+ reference_count = sum(1 for item in attachments if item.type == "image")
58
+ user_prompt = user_template.format(
59
+ user_input=user_input.strip(),
60
+ history_context=history_context.strip() or "(none)",
61
+ output_mode=output_mode,
62
+ style_hint=style_hint.strip() or "(none)",
63
+ reference_count=reference_count,
64
+ )
65
+ messages = [
66
+ {"role": "system", "content": system_prompt},
67
+ {"role": "user", "content": user_prompt},
68
+ ]
69
+
70
+ async def _run(reasoning_effort: str | None) -> str:
71
+ chunks: list[str] = []
72
+ async for chunk in self.stream_llm(
73
+ user_prompt=user_prompt,
74
+ system_prompt=system_prompt,
75
+ messages=messages,
76
+ attachments=attachments,
77
+ response_format={"type": "json_object"},
78
+ reasoning_effort=reasoning_effort,
79
+ stage="concept_analysis",
80
+ trace_meta=build_trace_metadata(
81
+ call_id=new_call_id("math-analysis"),
82
+ phase="concept_analysis",
83
+ label="Concept analysis",
84
+ call_kind="math_concept_analysis",
85
+ trace_role="analyze",
86
+ trace_kind="llm_output",
87
+ ),
88
+ ):
89
+ chunks.append(chunk)
90
+ return "".join(chunks)
91
+
92
+ payload = await json_with_reasoning_retry(
93
+ _run, expected_key="learning_goal", logger_instance=self.logger
94
+ )
95
+ if not payload.get("learning_goal"):
96
+ raise ValueError("Math animator concept analysis returned no learning goal.")
97
+ return ConceptAnalysis.model_validate(payload)
@@ -0,0 +1,76 @@
1
+ """Concept design stage for math animator."""
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
+ from deeptutor.services.llm.structured_retry import json_with_reasoning_retry
10
+
11
+ from ..models import ConceptAnalysis, SceneDesign
12
+
13
+
14
+ class ConceptDesignAgent(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="math_animator",
24
+ agent_name="concept_design_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
+ output_mode: str,
36
+ analysis: ConceptAnalysis,
37
+ style_hint: str,
38
+ ) -> SceneDesign:
39
+ system_prompt = self.get_prompt("system")
40
+ user_template = self.get_prompt("user_template")
41
+ if not system_prompt or not user_template:
42
+ raise ValueError("ConceptDesignAgent prompts are not configured.")
43
+
44
+ user_prompt = user_template.format(
45
+ user_input=user_input.strip(),
46
+ output_mode=output_mode,
47
+ style_hint=style_hint.strip() or "(none)",
48
+ analysis_json=json.dumps(analysis.model_dump(), ensure_ascii=False, indent=2),
49
+ )
50
+
51
+ async def _run(reasoning_effort: str | None) -> str:
52
+ chunks: list[str] = []
53
+ async for chunk in self.stream_llm(
54
+ user_prompt=user_prompt,
55
+ system_prompt=system_prompt,
56
+ response_format={"type": "json_object"},
57
+ reasoning_effort=reasoning_effort,
58
+ stage="concept_design",
59
+ trace_meta=build_trace_metadata(
60
+ call_id=new_call_id("math-design"),
61
+ phase="concept_design",
62
+ label="Concept design",
63
+ call_kind="math_concept_design",
64
+ trace_role="design",
65
+ trace_kind="llm_output",
66
+ ),
67
+ ):
68
+ chunks.append(chunk)
69
+ return "".join(chunks)
70
+
71
+ payload = await json_with_reasoning_retry(
72
+ _run, expected_key="scene_outline", logger_instance=self.logger
73
+ )
74
+ if not payload.get("scene_outline"):
75
+ raise ValueError("Math animator concept design returned no scene outline.")
76
+ return SceneDesign.model_validate(payload)
@@ -0,0 +1,78 @@
1
+ """Summary stage for math animator."""
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
+ from deeptutor.services.llm.structured_retry import json_with_reasoning_retry
10
+
11
+ from ..models import ConceptAnalysis, RenderResult, SceneDesign, SummaryPayload
12
+
13
+
14
+ class SummaryAgent(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="math_animator",
24
+ agent_name="summary_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
+ output_mode: str,
36
+ analysis: ConceptAnalysis,
37
+ design: SceneDesign,
38
+ render_result: RenderResult,
39
+ ) -> SummaryPayload:
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("SummaryAgent prompts are not configured.")
44
+
45
+ user_prompt = user_template.format(
46
+ user_input=user_input.strip(),
47
+ output_mode=output_mode,
48
+ analysis_json=json.dumps(analysis.model_dump(), ensure_ascii=False, indent=2),
49
+ design_json=json.dumps(design.model_dump(), ensure_ascii=False, indent=2),
50
+ render_json=json.dumps(render_result.model_dump(), ensure_ascii=False, indent=2),
51
+ )
52
+
53
+ async def _run(reasoning_effort: str | None) -> str:
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
+ reasoning_effort=reasoning_effort,
60
+ stage="summary",
61
+ trace_meta=build_trace_metadata(
62
+ call_id=new_call_id("math-summary"),
63
+ phase="summary",
64
+ label="Summarize result",
65
+ call_kind="math_summary",
66
+ trace_role="summarize",
67
+ trace_kind="llm_output",
68
+ ),
69
+ ):
70
+ chunks.append(chunk)
71
+ return "".join(chunks)
72
+
73
+ payload = await json_with_reasoning_retry(
74
+ _run, expected_key="summary_text", logger_instance=self.logger
75
+ )
76
+ # Rendering has already succeeded. Keep returning its artifacts if the
77
+ # summary remains empty after the low-effort retry.
78
+ return SummaryPayload.model_validate(payload)
@@ -0,0 +1,100 @@
1
+ """Visual quality review stage for math animator outputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from deeptutor.agents.base_agent import BaseAgent
8
+ from deeptutor.core.context import Attachment
9
+ from deeptutor.core.trace import build_trace_metadata, new_call_id
10
+ from deeptutor.services.llm import supports_vision
11
+
12
+ from ..models import RenderResult, VisualReviewResult
13
+ from ..utils import extract_json_object
14
+
15
+
16
+ class VisualReviewAgent(BaseAgent):
17
+ def __init__(
18
+ self,
19
+ api_key: str | None = None,
20
+ base_url: str | None = None,
21
+ api_version: str | None = None,
22
+ language: str = "zh",
23
+ ) -> None:
24
+ super().__init__(
25
+ module_name="math_animator",
26
+ agent_name="visual_review_agent",
27
+ api_key=api_key,
28
+ base_url=base_url,
29
+ api_version=api_version,
30
+ language=language,
31
+ )
32
+
33
+ async def process(
34
+ self,
35
+ *,
36
+ user_input: str,
37
+ output_mode: str,
38
+ current_code: str,
39
+ render_result: RenderResult,
40
+ attachments: list[Attachment],
41
+ ) -> VisualReviewResult:
42
+ if not attachments:
43
+ return VisualReviewResult(
44
+ passed=True,
45
+ summary="Visual review skipped because no review frames were available.",
46
+ reviewed_frames=0,
47
+ )
48
+
49
+ model = self.get_model()
50
+ if not supports_vision(self.binding, model):
51
+ return VisualReviewResult(
52
+ passed=True,
53
+ summary="Visual review skipped because the current model does not support image inspection.",
54
+ reviewed_frames=len(attachments),
55
+ )
56
+
57
+ system_prompt = self.get_prompt("system")
58
+ user_template = self.get_prompt("user_template")
59
+ if not system_prompt or not user_template:
60
+ raise ValueError("VisualReviewAgent prompts are not configured.")
61
+
62
+ user_prompt = user_template.format(
63
+ user_input=user_input.strip(),
64
+ output_mode=output_mode,
65
+ reviewed_frames=len(attachments),
66
+ render_json=json.dumps(
67
+ render_result.model_dump(exclude={"visual_review"}), ensure_ascii=False, indent=2
68
+ ),
69
+ current_code=current_code,
70
+ )
71
+ messages = [
72
+ {"role": "system", "content": system_prompt},
73
+ {"role": "user", "content": user_prompt},
74
+ ]
75
+ _chunks: list[str] = []
76
+ async for _c in self.stream_llm(
77
+ user_prompt=user_prompt,
78
+ system_prompt=system_prompt,
79
+ messages=messages,
80
+ attachments=attachments,
81
+ response_format={"type": "json_object"},
82
+ model=model,
83
+ stage="render_output",
84
+ trace_meta=build_trace_metadata(
85
+ call_id=new_call_id("math-visual-review"),
86
+ phase="render_output",
87
+ label="Visual quality review",
88
+ call_kind="math_visual_review",
89
+ trace_role="review",
90
+ trace_kind="llm_output",
91
+ ),
92
+ ):
93
+ _chunks.append(_c)
94
+ response = "".join(_chunks)
95
+ payload = extract_json_object(response)
96
+ payload.setdefault("reviewed_frames", len(attachments))
97
+ return VisualReviewResult.model_validate(payload)
98
+
99
+
100
+ __all__ = ["VisualReviewAgent"]