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.
- deeptutor/agents/math_animator/__init__.py +23 -0
- deeptutor/agents/math_animator/agents/__init__.py +15 -0
- deeptutor/agents/math_animator/agents/code_generator_agent.py +268 -0
- deeptutor/agents/math_animator/agents/concept_analysis_agent.py +97 -0
- deeptutor/agents/math_animator/agents/concept_design_agent.py +76 -0
- deeptutor/agents/math_animator/agents/summary_agent.py +78 -0
- deeptutor/agents/math_animator/agents/visual_review_agent.py +100 -0
- deeptutor/agents/math_animator/capability.py +301 -0
- deeptutor/agents/math_animator/duration_utils.py +36 -0
- deeptutor/agents/math_animator/models.py +97 -0
- deeptutor/agents/math_animator/pipeline.py +326 -0
- deeptutor/agents/math_animator/prompts/en/code_generator_agent.yaml +95 -0
- deeptutor/agents/math_animator/prompts/en/concept_analysis_agent.yaml +26 -0
- deeptutor/agents/math_animator/prompts/en/concept_design_agent.yaml +47 -0
- deeptutor/agents/math_animator/prompts/en/math_animator.yaml +7 -0
- deeptutor/agents/math_animator/prompts/en/summary_agent.yaml +27 -0
- deeptutor/agents/math_animator/prompts/en/visual_review_agent.yaml +31 -0
- deeptutor/agents/math_animator/prompts/zh/code_generator_agent.yaml +96 -0
- deeptutor/agents/math_animator/prompts/zh/concept_analysis_agent.yaml +26 -0
- deeptutor/agents/math_animator/prompts/zh/concept_design_agent.yaml +48 -0
- deeptutor/agents/math_animator/prompts/zh/math_animator.yaml +7 -0
- deeptutor/agents/math_animator/prompts/zh/summary_agent.yaml +27 -0
- deeptutor/agents/math_animator/prompts/zh/visual_review_agent.yaml +31 -0
- deeptutor/agents/math_animator/renderer.py +288 -0
- deeptutor/agents/math_animator/request_config.py +38 -0
- deeptutor/agents/math_animator/retry_manager.py +160 -0
- deeptutor/agents/math_animator/utils.py +124 -0
- deeptutor/agents/math_animator/visual_review.py +154 -0
- deeptutor_renderer_manim-1.7.0.dist-info/METADATA +7 -0
- deeptutor_renderer_manim-1.7.0.dist-info/RECORD +32 -0
- deeptutor_renderer_manim-1.7.0.dist-info/WHEEL +5 -0
- 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"]
|