MSGEncrypted commited on
Commit
1706cd9
·
1 Parent(s): a31982f

try fix pptx

Browse files
apps/gradio-space/src/gradio_space/tabs/education_pptx.py CHANGED
@@ -4,20 +4,19 @@ from agent.runner import AgentRunner
4
  from gradio_space.model_loading import ensure_model_loaded, get_active_model_key, model_status
5
  from inference.factory import get_backend
6
 
7
-
8
  def generate_lesson_slides(
9
  topic: str,
10
  grade: str,
11
  slide_count: int,
12
- ) -> tuple[str, str | None, str, str]:
13
  model_key = get_active_model_key()
14
  load_error = ensure_model_loaded(model_key)
15
  if load_error:
16
- return load_error, None, "", load_error
17
 
18
  if not topic.strip():
19
  message = "Please enter a lesson topic."
20
- return message, None, "", message
21
 
22
  try:
23
  runner = AgentRunner()
@@ -30,14 +29,24 @@ def generate_lesson_slides(
30
  )
31
  except Exception as exc: # noqa: BLE001 — show agent errors in UI
32
  message = f"Agent error: {exc}"
33
- return message, None, "", message
34
 
 
35
  trace_summary = (
36
  f"Run `{result.trace.run_id}` · skill `{result.trace.skill}` · "
37
  f"model `{result.trace.model}`\n\n"
38
  f"Trace saved: `{result.trace_path}`"
39
  )
40
- return result.markdown_preview, result.pptx_path, trace_summary, result.trace.to_json()
 
 
 
 
 
 
 
 
 
41
 
42
 
43
  def build_education_pptx_tab() -> None:
@@ -73,8 +82,37 @@ the agent then builds a downloadable PowerPoint — no cloud LLM API.
73
 
74
  generate_btn = gr.Button("Generate lesson slides", variant="primary")
75
 
76
- outline_preview = gr.Markdown(label="Outline preview")
77
- pptx_file = gr.File(label="Download PowerPoint", interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  trace_box = gr.Textbox(
79
  label="Agent trace (JSON)",
80
  lines=12,
@@ -88,5 +126,14 @@ the agent then builds a downloadable PowerPoint — no cloud LLM API.
88
  generate_btn.click(
89
  fn=generate_lesson_slides,
90
  inputs=[topic, grade, slide_count],
91
- outputs=[outline_preview, pptx_file, trace_summary, trace_box],
 
 
 
 
 
 
 
 
 
92
  )
 
4
  from gradio_space.model_loading import ensure_model_loaded, get_active_model_key, model_status
5
  from inference.factory import get_backend
6
 
 
7
  def generate_lesson_slides(
8
  topic: str,
9
  grade: str,
10
  slide_count: int,
11
+ ) -> tuple[str, str, list[tuple[str, str]], str | None, str | None, str | None, str, str]:
12
  model_key = get_active_model_key()
13
  load_error = ensure_model_loaded(model_key)
14
  if load_error:
15
+ return load_error, "", [], None, None, None, load_error, load_error
16
 
17
  if not topic.strip():
18
  message = "Please enter a lesson topic."
19
+ return message, "", [], None, None, None, message, message
20
 
21
  try:
22
  runner = AgentRunner()
 
29
  )
30
  except Exception as exc: # noqa: BLE001 — show agent errors in UI
31
  message = f"Agent error: {exc}"
32
+ return message, "", [], None, None, None, message, message
33
 
34
+ gallery = [(path, f"Slide {i}") for i, path in enumerate(result.preview_images)]
35
  trace_summary = (
36
  f"Run `{result.trace.run_id}` · skill `{result.trace.skill}` · "
37
  f"model `{result.trace.model}`\n\n"
38
  f"Trace saved: `{result.trace_path}`"
39
  )
40
+ return (
41
+ result.markdown_preview,
42
+ result.html_preview,
43
+ gallery,
44
+ result.pptx_path,
45
+ result.docx_path,
46
+ result.html_export_path,
47
+ trace_summary,
48
+ result.trace.to_json(),
49
+ )
50
 
51
 
52
  def build_education_pptx_tab() -> None:
 
82
 
83
  generate_btn = gr.Button("Generate lesson slides", variant="primary")
84
 
85
+ with gr.Tabs():
86
+ with gr.Tab("Slide preview"):
87
+ slide_preview = gr.HTML(label="Slides")
88
+ slide_gallery = gr.Gallery(
89
+ label="Slide thumbnails",
90
+ columns=2,
91
+ height="auto",
92
+ object_fit="contain",
93
+ )
94
+ with gr.Tab("Outline"):
95
+ outline_preview = gr.Markdown(label="Outline (markdown)")
96
+
97
+ with gr.Row():
98
+ pptx_file = gr.File(label="Download PowerPoint (.pptx)", interactive=False)
99
+ docx_file = gr.File(
100
+ label="Download Word / Google Docs (.docx)",
101
+ interactive=False,
102
+ )
103
+ html_file = gr.File(
104
+ label="Download HTML (import to Google Docs)",
105
+ interactive=False,
106
+ )
107
+
108
+ gr.Markdown(
109
+ """
110
+ **Open in Google Docs:** download the `.docx` file, upload it to [Google Drive](https://drive.google.com),
111
+ then choose **Open with → Google Docs**. You can also upload the `.html` file via
112
+ **Google Docs → File → Open → Upload**.
113
+ """
114
+ )
115
+
116
  trace_box = gr.Textbox(
117
  label="Agent trace (JSON)",
118
  lines=12,
 
126
  generate_btn.click(
127
  fn=generate_lesson_slides,
128
  inputs=[topic, grade, slide_count],
129
+ outputs=[
130
+ outline_preview,
131
+ slide_preview,
132
+ slide_gallery,
133
+ pptx_file,
134
+ docx_file,
135
+ html_file,
136
+ trace_summary,
137
+ trace_box,
138
+ ],
139
  )
libs/agent/pyproject.toml CHANGED
@@ -9,7 +9,9 @@ authors = [
9
  requires-python = ">=3.12"
10
  dependencies = [
11
  "inference",
 
12
  "pydantic>=2.0.0",
 
13
  "python-pptx>=1.0.0",
14
  "pyyaml>=6.0.2",
15
  ]
 
9
  requires-python = ">=3.12"
10
  dependencies = [
11
  "inference",
12
+ "pillow>=10.0.0",
13
  "pydantic>=2.0.0",
14
+ "python-docx>=1.1.0",
15
  "python-pptx>=1.0.0",
16
  "pyyaml>=6.0.2",
17
  ]
libs/agent/src/agent/preview.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import html
4
+ from pathlib import Path
5
+
6
+ from PIL import Image, ImageDraw, ImageFont
7
+
8
+ from agent.models import SlideOutline
9
+ from agent.tools.pptx import _outputs_dir, _safe_filename
10
+
11
+
12
+ def outline_to_html(outline: SlideOutline) -> str:
13
+ """Render slide-like cards for in-browser preview."""
14
+ slides_html: list[str] = []
15
+ slides_html.append(
16
+ _slide_card_html(
17
+ title=outline.title,
18
+ subtitle="Lesson slides",
19
+ bullets=[],
20
+ speaker_note="",
21
+ index=0,
22
+ is_title=True,
23
+ )
24
+ )
25
+ for index, slide in enumerate(outline.slides, start=1):
26
+ slides_html.append(
27
+ _slide_card_html(
28
+ title=slide.title,
29
+ subtitle="",
30
+ bullets=slide.bullets,
31
+ speaker_note=slide.speaker_note,
32
+ index=index,
33
+ is_title=False,
34
+ )
35
+ )
36
+
37
+ return f"""
38
+ <div class="lesson-deck">
39
+ <style>
40
+ .lesson-deck {{
41
+ font-family: Georgia, "Iowan Old Style", serif;
42
+ display: flex;
43
+ flex-direction: column;
44
+ gap: 16px;
45
+ max-width: 960px;
46
+ }}
47
+ .lesson-slide {{
48
+ border: 2px solid #5a3a22;
49
+ border-radius: 12px;
50
+ background: linear-gradient(180deg, #fbf6e8 0%, #f6efe1 100%);
51
+ box-shadow: 0 4px 0 rgba(58,37,22,0.12);
52
+ padding: 24px 28px;
53
+ min-height: 180px;
54
+ }}
55
+ .lesson-slide.title-slide {{
56
+ background: linear-gradient(135deg, #3a2516 0%, #5a3a22 100%);
57
+ color: #f6efe1;
58
+ min-height: 220px;
59
+ display: flex;
60
+ flex-direction: column;
61
+ justify-content: center;
62
+ }}
63
+ .lesson-slide .slide-index {{
64
+ font-size: 11px;
65
+ letter-spacing: 0.18em;
66
+ text-transform: uppercase;
67
+ color: #8a4a2b;
68
+ margin-bottom: 8px;
69
+ font-weight: 700;
70
+ }}
71
+ .lesson-slide.title-slide .slide-index {{
72
+ color: #e6a85c;
73
+ }}
74
+ .lesson-slide h3 {{
75
+ margin: 0 0 12px 0;
76
+ font-size: 1.5rem;
77
+ line-height: 1.2;
78
+ }}
79
+ .lesson-slide .subtitle {{
80
+ margin: 0;
81
+ opacity: 0.85;
82
+ font-style: italic;
83
+ }}
84
+ .lesson-slide ul {{
85
+ margin: 0;
86
+ padding-left: 1.25rem;
87
+ }}
88
+ .lesson-slide li {{
89
+ margin-bottom: 6px;
90
+ line-height: 1.45;
91
+ }}
92
+ .lesson-slide .speaker-note {{
93
+ margin-top: 14px;
94
+ padding-top: 10px;
95
+ border-top: 1px dashed #8a6a48;
96
+ font-size: 0.9rem;
97
+ color: #5a3a22;
98
+ font-style: italic;
99
+ }}
100
+ </style>
101
+ {''.join(slides_html)}
102
+ </div>
103
+ """
104
+
105
+
106
+ def _slide_card_html(
107
+ *,
108
+ title: str,
109
+ subtitle: str,
110
+ bullets: list[str],
111
+ speaker_note: str,
112
+ index: int,
113
+ is_title: bool,
114
+ ) -> str:
115
+ safe_title = html.escape(title)
116
+ safe_subtitle = html.escape(subtitle)
117
+ klass = "lesson-slide title-slide" if is_title else "lesson-slide"
118
+ label = "Title" if is_title else f"Slide {index}"
119
+
120
+ bullets_html = ""
121
+ if bullets:
122
+ items = "".join(f"<li>{html.escape(b)}</li>" for b in bullets)
123
+ bullets_html = f"<ul>{items}</ul>"
124
+
125
+ note_html = ""
126
+ if speaker_note:
127
+ note_html = f'<div class="speaker-note">Teacher note: {html.escape(speaker_note)}</div>'
128
+
129
+ subtitle_html = f'<p class="subtitle">{safe_subtitle}</p>' if subtitle else ""
130
+
131
+ return f"""
132
+ <article class="{klass}">
133
+ <div class="slide-index">{label}</div>
134
+ <h3>{safe_title}</h3>
135
+ {subtitle_html}
136
+ {bullets_html}
137
+ {note_html}
138
+ </article>
139
+ """
140
+
141
+
142
+ def render_slide_images(outline: SlideOutline, run_id: str) -> list[Path]:
143
+ """Render PNG thumbnails for gr.Gallery preview."""
144
+ out_dir = _outputs_dir() / f"preview_{run_id}"
145
+ out_dir.mkdir(parents=True, exist_ok=True)
146
+
147
+ width, height = 1280, 720
148
+ paths: list[Path] = []
149
+
150
+ title_path = out_dir / "00_title.png"
151
+ _draw_slide_image(
152
+ title_path,
153
+ width,
154
+ height,
155
+ title=outline.title,
156
+ subtitle="Generated lesson slides",
157
+ bullets=[],
158
+ is_title=True,
159
+ )
160
+ paths.append(title_path)
161
+
162
+ for index, slide in enumerate(outline.slides, start=1):
163
+ path = out_dir / f"{index:02d}_{_safe_filename(slide.title)}.png"
164
+ _draw_slide_image(
165
+ path,
166
+ width,
167
+ height,
168
+ title=slide.title,
169
+ subtitle="",
170
+ bullets=slide.bullets,
171
+ is_title=False,
172
+ )
173
+ paths.append(path)
174
+
175
+ return paths
176
+
177
+
178
+ def _load_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
179
+ candidates = [
180
+ "/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf",
181
+ "/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf" if bold else "/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf",
182
+ ]
183
+ for path in candidates:
184
+ if Path(path).exists():
185
+ return ImageFont.truetype(path, size)
186
+ return ImageFont.load_default()
187
+
188
+
189
+ def _draw_slide_image(
190
+ path: Path,
191
+ width: int,
192
+ height: int,
193
+ *,
194
+ title: str,
195
+ subtitle: str,
196
+ bullets: list[str],
197
+ is_title: bool,
198
+ ) -> None:
199
+ if is_title:
200
+ bg = (58, 37, 22)
201
+ fg = (246, 239, 225)
202
+ accent = (230, 168, 92)
203
+ else:
204
+ bg = (251, 246, 232)
205
+ fg = (42, 33, 24)
206
+ accent = (138, 106, 72)
207
+
208
+ image = Image.new("RGB", (width, height), bg)
209
+ draw = ImageDraw.Draw(image)
210
+
211
+ margin = 80
212
+ title_font = _load_font(56 if is_title else 44, bold=True)
213
+ body_font = _load_font(30)
214
+ small_font = _load_font(24)
215
+
216
+ y = margin
217
+ if is_title:
218
+ draw.text((margin, height // 2 - 80), _wrap_text(title, 28), fill=fg, font=title_font)
219
+ if subtitle:
220
+ draw.text((margin, height // 2 + 40), subtitle, fill=accent, font=small_font)
221
+ else:
222
+ draw.text((margin, y), _wrap_text(title, 32), fill=fg, font=title_font)
223
+ y += 90
224
+ for bullet in bullets:
225
+ line = _wrap_text(f"• {bullet}", 48)
226
+ for part in line.split("\n"):
227
+ draw.text((margin + 10, y), part, fill=fg, font=body_font)
228
+ y += 42
229
+ y += 8
230
+
231
+ draw.rectangle([(0, 0), (width, 8)], fill=accent)
232
+ image.save(path)
233
+
234
+
235
+ def _wrap_text(text: str, max_chars: int) -> str:
236
+ words = text.split()
237
+ lines: list[str] = []
238
+ current: list[str] = []
239
+ for word in words:
240
+ candidate = " ".join(current + [word])
241
+ if len(candidate) <= max_chars:
242
+ current.append(word)
243
+ else:
244
+ if current:
245
+ lines.append(" ".join(current))
246
+ current = [word]
247
+ if current:
248
+ lines.append(" ".join(current))
249
+ return "\n".join(lines) if lines else text
libs/agent/src/agent/runner.py CHANGED
@@ -8,6 +8,7 @@ from typing import Any
8
  from inference.base import InferenceBackend
9
 
10
  from agent.models import EducationPptxInput, SlideOutline
 
11
  from agent.prompts import (
12
  education_outline_repair,
13
  education_outline_system,
@@ -15,6 +16,7 @@ from agent.prompts import (
15
  outline_to_markdown,
16
  )
17
  from agent.skills import SkillRegistry
 
18
  from agent.tools_registry import ToolRegistry
19
  from agent.trace import TraceRecorder
20
 
@@ -24,7 +26,11 @@ EDUCATION_PPTX_SKILL = "education-pptx"
24
  @dataclass
25
  class AgentResult:
26
  markdown_preview: str
 
 
27
  pptx_path: str
 
 
28
  trace: TraceRecorder
29
  trace_path: str
30
  outline: SlideOutline
@@ -66,15 +72,36 @@ class AgentRunner:
66
  {"title": outline.title, "slide_count": len(outline.slides)},
67
  pptx_path,
68
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  trace.set_artifact(pptx_path)
70
 
71
  slides_dicts = [s.model_dump() for s in outline.slides]
72
  markdown = outline_to_markdown(outline.title, slides_dicts)
 
 
73
  trace_path = trace.save()
74
 
75
  return AgentResult(
76
  markdown_preview=markdown,
 
 
77
  pptx_path=pptx_path,
 
 
78
  trace=trace,
79
  trace_path=str(trace_path),
80
  outline=outline,
 
8
  from inference.base import InferenceBackend
9
 
10
  from agent.models import EducationPptxInput, SlideOutline
11
+ from agent.preview import outline_to_html, render_slide_images
12
  from agent.prompts import (
13
  education_outline_repair,
14
  education_outline_system,
 
16
  outline_to_markdown,
17
  )
18
  from agent.skills import SkillRegistry
19
+ from agent.tools.docx import create_docx, create_html_export
20
  from agent.tools_registry import ToolRegistry
21
  from agent.trace import TraceRecorder
22
 
 
26
  @dataclass
27
  class AgentResult:
28
  markdown_preview: str
29
+ html_preview: str
30
+ preview_images: list[str]
31
  pptx_path: str
32
+ docx_path: str
33
+ html_export_path: str
34
  trace: TraceRecorder
35
  trace_path: str
36
  outline: SlideOutline
 
72
  {"title": outline.title, "slide_count": len(outline.slides)},
73
  pptx_path,
74
  )
75
+
76
+ docx_path = create_docx(outline, run_id=trace.run_id)
77
+ trace.log_tool(
78
+ "create_docx",
79
+ {"title": outline.title, "slide_count": len(outline.slides)},
80
+ str(docx_path),
81
+ )
82
+
83
+ html_export_path = create_html_export(outline, run_id=trace.run_id)
84
+ trace.log_tool(
85
+ "create_html_export",
86
+ {"title": outline.title},
87
+ str(html_export_path),
88
+ )
89
+
90
  trace.set_artifact(pptx_path)
91
 
92
  slides_dicts = [s.model_dump() for s in outline.slides]
93
  markdown = outline_to_markdown(outline.title, slides_dicts)
94
+ html_preview = outline_to_html(outline)
95
+ preview_images = [str(p) for p in render_slide_images(outline, trace.run_id)]
96
  trace_path = trace.save()
97
 
98
  return AgentResult(
99
  markdown_preview=markdown,
100
+ html_preview=html_preview,
101
+ preview_images=preview_images,
102
  pptx_path=pptx_path,
103
+ docx_path=str(docx_path),
104
+ html_export_path=str(html_export_path),
105
  trace=trace,
106
  trace_path=str(trace_path),
107
  outline=outline,
libs/agent/src/agent/tools/__init__.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from agent.tools.pptx import create_pptx
2
 
3
- __all__ = ["create_pptx"]
 
1
+ from agent.tools.docx import create_docx, create_html_export
2
  from agent.tools.pptx import create_pptx
3
 
4
+ __all__ = ["create_docx", "create_html_export", "create_pptx"]
libs/agent/src/agent/tools/docx.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import uuid
5
+ from pathlib import Path
6
+
7
+ from docx import Document
8
+ from docx.shared import Pt
9
+
10
+ from agent.models import SlideOutline
11
+ from agent.tools.pptx import _outputs_dir, _safe_filename
12
+
13
+
14
+ def create_docx(outline: SlideOutline, run_id: str | None = None) -> Path:
15
+ """Build a Word document from a slide outline (opens in Google Docs when uploaded)."""
16
+ rid = run_id or uuid.uuid4().hex[:12]
17
+ out_dir = _outputs_dir()
18
+ filename = f"{_safe_filename(outline.title)}_{rid}.docx"
19
+ path = out_dir / filename
20
+
21
+ doc = Document()
22
+ title = doc.add_heading(outline.title, level=0)
23
+ title.runs[0].font.size = Pt(28)
24
+ doc.add_paragraph("Generated lesson slides").italic = True
25
+ doc.add_page_break()
26
+
27
+ for index, slide in enumerate(outline.slides, start=1):
28
+ heading = doc.add_heading(f"Slide {index}: {slide.title}", level=1)
29
+ heading.runs[0].font.size = Pt(22)
30
+ for bullet in slide.bullets:
31
+ para = doc.add_paragraph(bullet, style="List Bullet")
32
+ para.runs[0].font.size = Pt(14)
33
+ if slide.speaker_note:
34
+ note = doc.add_paragraph(f"Teacher note: {slide.speaker_note}")
35
+ note.runs[0].italic = True
36
+ note.runs[0].font.size = Pt(11)
37
+ if index < len(outline.slides):
38
+ doc.add_page_break()
39
+
40
+ doc.save(str(path))
41
+ return path
42
+
43
+
44
+ def create_html_export(outline: SlideOutline, run_id: str | None = None) -> Path:
45
+ """Standalone HTML file — import into Google Docs via File → Open → Upload."""
46
+ from agent.preview import outline_to_html
47
+
48
+ rid = run_id or uuid.uuid4().hex[:12]
49
+ out_dir = _outputs_dir()
50
+ filename = f"{_safe_filename(outline.title)}_{rid}.html"
51
+ path = out_dir / filename
52
+
53
+ body = outline_to_html(outline)
54
+ full = f"""<!DOCTYPE html>
55
+ <html lang="en">
56
+ <head>
57
+ <meta charset="utf-8" />
58
+ <title>{_escape_html(outline.title)}</title>
59
+ </head>
60
+ <body>
61
+ {body}
62
+ </body>
63
+ </html>
64
+ """
65
+ path.write_text(full)
66
+ return path
67
+
68
+
69
+ def _escape_html(text: str) -> str:
70
+ return (
71
+ text.replace("&", "&amp;")
72
+ .replace("<", "&lt;")
73
+ .replace(">", "&gt;")
74
+ .replace('"', "&quot;")
75
+ )
libs/agent/tests/test_runner.py CHANGED
@@ -1,5 +1,7 @@
1
  from agent.models import SlideOutline, SlideSpec
 
2
  from agent.runner import AgentRunner
 
3
  from agent.tools.pptx import create_pptx
4
 
5
 
@@ -21,3 +23,39 @@ def test_create_pptx_writes_file(tmp_path, monkeypatch):
21
  path = create_pptx(outline, run_id="test")
22
  assert path.exists()
23
  assert path.suffix == ".pptx"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from agent.models import SlideOutline, SlideSpec
2
+ from agent.preview import outline_to_html, render_slide_images
3
  from agent.runner import AgentRunner
4
+ from agent.tools.docx import create_docx, create_html_export
5
  from agent.tools.pptx import create_pptx
6
 
7
 
 
23
  path = create_pptx(outline, run_id="test")
24
  assert path.exists()
25
  assert path.suffix == ".pptx"
26
+
27
+
28
+ def test_create_docx_writes_file(tmp_path, monkeypatch):
29
+ monkeypatch.setenv("AGENT_OUTPUTS_DIR", str(tmp_path))
30
+ outline = SlideOutline(
31
+ title="Photosynthesis",
32
+ slides=[SlideSpec(title="Intro", bullets=["Sunlight", "Chlorophyll"])],
33
+ )
34
+ path = create_docx(outline, run_id="test")
35
+ assert path.exists()
36
+ assert path.suffix == ".docx"
37
+
38
+
39
+ def test_outline_preview_and_images(tmp_path, monkeypatch):
40
+ monkeypatch.setenv("AGENT_OUTPUTS_DIR", str(tmp_path))
41
+ outline = SlideOutline(
42
+ title="Water Cycle",
43
+ slides=[SlideSpec(title="Evaporation", bullets=["Heat", "Vapor"])],
44
+ )
45
+ html = outline_to_html(outline)
46
+ assert "Water Cycle" in html
47
+ assert "Evaporation" in html
48
+ images = render_slide_images(outline, run_id="prev")
49
+ assert len(images) == 2
50
+ assert all(p.exists() for p in images)
51
+
52
+
53
+ def test_create_html_export(tmp_path, monkeypatch):
54
+ monkeypatch.setenv("AGENT_OUTPUTS_DIR", str(tmp_path))
55
+ outline = SlideOutline(
56
+ title="Fractions",
57
+ slides=[SlideSpec(title="Parts", bullets=["Half", "Quarter"])],
58
+ )
59
+ path = create_html_export(outline, run_id="html")
60
+ assert path.exists()
61
+ assert "Fractions" in path.read_text()
models.yaml CHANGED
@@ -2,7 +2,10 @@
2
  # Select active preset with ACTIVE_MODEL; override any field via .env (see .env.example).
3
 
4
  defaults:
5
- active_model: minicpm5-1b
 
 
 
6
  # Dev: set ALLOW_MODEL_SWITCH=true in .env to expose a dropdown in Gradio.
7
  # Space: keep false so visitors use one pinned model.
8
  allow_model_switch: false
 
2
  # Select active preset with ACTIVE_MODEL; override any field via .env (see .env.example).
3
 
4
  defaults:
5
+ active_model: minicpm-v-4.6
6
+
7
+ # active_model: minicpm5-1b
8
+
9
  # Dev: set ALLOW_MODEL_SWITCH=true in .env to expose a dropdown in Gradio.
10
  # Space: keep false so visitors use one pinned model.
11
  allow_model_switch: false
outputs/traces/5ffe463cd9ff.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "run_id": "5ffe463cd9ff",
3
+ "skill": "education-pptx",
4
+ "model": "minicpm5-1b",
5
+ "input": {
6
+ "topic": "ai agent",
7
+ "grade": "6",
8
+ "slide_count": 5
9
+ },
10
+ "steps": [
11
+ {
12
+ "type": "llm",
13
+ "prompt_hash": "92e1b034dc5f5dca",
14
+ "output": "<think>\nThe user wants a lesson plan for teaching AI agent to grade level 6 students. I need to create a JSON outline with 5 slides. The topic is \"AI Agent\", grade level is 6, and content slides are 5.\n\nI'll structure the JSON with a title and then 5 slides. Each slide should have a title, bullets (short factual statements), and optionally a speaker_note. I'll make sure to keep it simple and age-appropriate for 6th graders.\n\nLet me outline the content:\n\n1. Introduction to AI Agent\n2. What is an AI Agent?\n3. Key features of an AI Agent\n4. How AI Agents work\n5. Real-world applications\n\nI'll make sure each slide has 2-3 bullets for brevity. The speaker_note can be brief if helpful.\n\nNow, I'll generate the JSON accordingly.\n</think>\n\n{\n \"title\": \"AI Agent: Teaching a 6th Grade\",\n \"slides\": [\n {\n \"title\": \"Introduction to AI Agent\",\n \"bullets\": [\n \"An AI Agent is a computer program that can perform tasks for you.\",\n \"It can learn from experience and improve over time.\",\n \"It can be used for many different tasks.\"\n ],\n \"speaker_note\": \"AI agents are like helpful digital assistants that can help with homework, answering questions, and more.\"\n },\n {\n \"title\": \"What is an AI Agent?\",\n \"bullets\": [\n \"An AI Agent is a program that can learn from experience and improve over time.\",\n \"It can be used for many different tasks, such as answering questions, helping with homework, and more.\"\n ],\n \"speaker_note\": \"Think of it as a smart program that can learn from your interactions and become better over time.\"\n },\n {\n \"title\": \"Key Features of an AI Agent\",\n \"bullets\": [\n \"It can learn from experience and improve over time.\",\n \"It can be used for many different tasks, such as answering questions, helping with homework, and more.\"\n ],\n \"speaker_note\": \"AI agents are designed to be helpful and efficient in their tasks.\"\n },\n {\n \"title\": \"How AI Agents Work\",\n \"bullets\": [\n \"An AI Agent uses algorithms and data to make decisions.\",\n \"It learns from user interactions to become more accurate over time.\",\n \"It can be programmed to perform specific tasks.\"\n ],\n \"speaker_note\": \"AI agents use algorithms to make decisions based on data.\"\n },\n {\n \"title\": \"Real-World Applications\",\n \"bullets\": [\n \"AI agents can help with homework, answering questions, and more.\",\n \"They can be used in many different fields, like education, customer service, and more.\"\n ],\n \"speaker_note\": \"AI agents are used in many different fields, including education and customer service.\"\n }\n ]\n}"
15
+ },
16
+ {
17
+ "type": "tool",
18
+ "name": "create_pptx",
19
+ "arguments": {
20
+ "title": "AI Agent: Teaching a 6th Grade",
21
+ "slide_count": 5
22
+ },
23
+ "result": "/tmp/agent_outputs/ai_agent_teaching_a_6th_grade_5ffe463cd9ff.pptx"
24
+ }
25
+ ],
26
+ "artifact": "/tmp/agent_outputs/ai_agent_teaching_a_6th_grade_5ffe463cd9ff.pptx",
27
+ "created_at": "2026-06-11T12:15:49.294920+00:00"
28
+ }