Spaces:
Running on Zero
Running on Zero
Commit ·
1706cd9
1
Parent(s): a31982f
try fix pptx
Browse files- apps/gradio-space/src/gradio_space/tabs/education_pptx.py +56 -9
- libs/agent/pyproject.toml +2 -0
- libs/agent/src/agent/preview.py +249 -0
- libs/agent/src/agent/runner.py +27 -0
- libs/agent/src/agent/tools/__init__.py +2 -1
- libs/agent/src/agent/tools/docx.py +75 -0
- libs/agent/tests/test_runner.py +38 -0
- models.yaml +4 -1
- outputs/traces/5ffe463cd9ff.json +28 -0
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,
|
| 17 |
|
| 18 |
if not topic.strip():
|
| 19 |
message = "Please enter a lesson topic."
|
| 20 |
-
return message, None,
|
| 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,
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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("&", "&")
|
| 72 |
+
.replace("<", "<")
|
| 73 |
+
.replace(">", ">")
|
| 74 |
+
.replace('"', """)
|
| 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:
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|