Spaces:
Sleeping
Sleeping
Commit ·
a822dca
1
Parent(s): 1706cd9
model loading + fix preview + tracing + pptx skills
Browse files- .gitignore +2 -0
- apps/gradio-space/src/gradio_space/app.py +5 -3
- apps/gradio-space/src/gradio_space/model_loading.py +14 -1
- apps/gradio-space/src/gradio_space/tabs/chat.py +1 -1
- apps/gradio-space/src/gradio_space/tabs/education_pptx.py +31 -9
- libs/agent/src/agent/preview.py +8 -3
- libs/agent/src/agent/prompts.py +10 -1
- libs/agent/src/agent/runner.py +77 -17
- libs/agent/src/agent/tools/pptx.py +5 -0
- libs/agent/src/agent/trace.py +3 -0
- libs/agent/tests/test_runner.py +27 -0
- uv.lock +17 -0
.gitignore
CHANGED
|
@@ -9,3 +9,5 @@ models/
|
|
| 9 |
*.egg-info/
|
| 10 |
dist/
|
| 11 |
build/
|
|
|
|
|
|
|
|
|
| 9 |
*.egg-info/
|
| 10 |
dist/
|
| 11 |
build/
|
| 12 |
+
|
| 13 |
+
outputs/traces
|
apps/gradio-space/src/gradio_space/app.py
CHANGED
|
@@ -2,7 +2,9 @@ import os
|
|
| 2 |
|
| 3 |
import gradio as gr
|
| 4 |
|
|
|
|
| 5 |
from gradio_space.tabs import build_chat_tab, build_education_pptx_tab
|
|
|
|
| 6 |
from inference.config import get_app_config
|
| 7 |
|
| 8 |
_app_config = get_app_config()
|
|
@@ -40,13 +42,13 @@ Part of the [Build Small Hackathon](https://huggingface.co/build-small-hackathon
|
|
| 40 |
return demo
|
| 41 |
|
| 42 |
|
| 43 |
-
demo = build_demo()
|
| 44 |
-
|
| 45 |
-
|
| 46 |
def main() -> None:
|
|
|
|
|
|
|
| 47 |
demo.launch(
|
| 48 |
server_name="0.0.0.0",
|
| 49 |
server_port=int(os.environ.get("PORT", "7860")),
|
|
|
|
| 50 |
)
|
| 51 |
|
| 52 |
|
|
|
|
| 2 |
|
| 3 |
import gradio as gr
|
| 4 |
|
| 5 |
+
from gradio_space.model_loading import preload_active_model
|
| 6 |
from gradio_space.tabs import build_chat_tab, build_education_pptx_tab
|
| 7 |
+
from gradio_space.tabs.education_pptx import gradio_allowed_paths
|
| 8 |
from inference.config import get_app_config
|
| 9 |
|
| 10 |
_app_config = get_app_config()
|
|
|
|
| 42 |
return demo
|
| 43 |
|
| 44 |
|
|
|
|
|
|
|
|
|
|
| 45 |
def main() -> None:
|
| 46 |
+
preload_active_model()
|
| 47 |
+
demo = build_demo()
|
| 48 |
demo.launch(
|
| 49 |
server_name="0.0.0.0",
|
| 50 |
server_port=int(os.environ.get("PORT", "7860")),
|
| 51 |
+
allowed_paths=gradio_allowed_paths(),
|
| 52 |
)
|
| 53 |
|
| 54 |
|
apps/gradio-space/src/gradio_space/model_loading.py
CHANGED
|
@@ -69,10 +69,23 @@ def warmup(model_key: str | None = None) -> str:
|
|
| 69 |
device_hint = runtime_device_hint(key)
|
| 70 |
return (
|
| 71 |
f"Preset `{key}` selected ({model.backend}, {device_hint}). "
|
| 72 |
-
"
|
| 73 |
)
|
| 74 |
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
def model_status(model_key: str) -> str:
|
| 77 |
model = get_model_config(model_key)
|
| 78 |
return f"**{model.label}**\n\n- Backend: `{model.backend}`\n- {warmup(model_key)}"
|
|
|
|
| 69 |
device_hint = runtime_device_hint(key)
|
| 70 |
return (
|
| 71 |
f"Preset `{key}` selected ({model.backend}, {device_hint}). "
|
| 72 |
+
"Loading weights…"
|
| 73 |
)
|
| 74 |
|
| 75 |
|
| 76 |
+
def preload_active_model() -> str:
|
| 77 |
+
"""Load the active preset at startup so the first request is fast."""
|
| 78 |
+
key = get_active_model_key()
|
| 79 |
+
print(f"[startup] Loading model preset `{key}`…", flush=True)
|
| 80 |
+
error = ensure_model_loaded(key)
|
| 81 |
+
if error:
|
| 82 |
+
print(f"[startup] {error}", flush=True)
|
| 83 |
+
return error
|
| 84 |
+
status = warmup(key)
|
| 85 |
+
print(f"[startup] {status}", flush=True)
|
| 86 |
+
return status
|
| 87 |
+
|
| 88 |
+
|
| 89 |
def model_status(model_key: str) -> str:
|
| 90 |
model = get_model_config(model_key)
|
| 91 |
return f"**{model.label}**\n\n- Backend: `{model.backend}`\n- {warmup(model_key)}"
|
apps/gradio-space/src/gradio_space/tabs/chat.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
|
| 3 |
-
from gradio_space.model_loading import chat, model_status
|
| 4 |
from inference.config import get_app_config
|
| 5 |
|
| 6 |
_app_config = get_app_config()
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
|
| 3 |
+
from gradio_space.model_loading import chat, model_status
|
| 4 |
from inference.config import get_app_config
|
| 5 |
|
| 6 |
_app_config = get_app_config()
|
apps/gradio-space/src/gradio_space/tabs/education_pptx.py
CHANGED
|
@@ -1,22 +1,37 @@
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
|
| 3 |
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 |
def generate_lesson_slides(
|
| 8 |
topic: str,
|
| 9 |
grade: str,
|
| 10 |
slide_count: int,
|
| 11 |
-
) -> tuple[str, str, list[
|
| 12 |
model_key = get_active_model_key()
|
| 13 |
load_error = ensure_model_loaded(model_key)
|
| 14 |
if load_error:
|
| 15 |
-
return load_error,
|
| 16 |
|
| 17 |
if not topic.strip():
|
| 18 |
message = "Please enter a lesson topic."
|
| 19 |
-
return message,
|
| 20 |
|
| 21 |
try:
|
| 22 |
runner = AgentRunner()
|
|
@@ -29,9 +44,9 @@ def generate_lesson_slides(
|
|
| 29 |
)
|
| 30 |
except Exception as exc: # noqa: BLE001 — show agent errors in UI
|
| 31 |
message = f"Agent error: {exc}"
|
| 32 |
-
return message,
|
| 33 |
|
| 34 |
-
gallery = [(
|
| 35 |
trace_summary = (
|
| 36 |
f"Run `{result.trace.run_id}` · skill `{result.trace.skill}` · "
|
| 37 |
f"model `{result.trace.model}`\n\n"
|
|
@@ -41,9 +56,9 @@ def generate_lesson_slides(
|
|
| 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 |
)
|
|
@@ -88,8 +103,9 @@ the agent then builds a downloadable PowerPoint — no cloud LLM API.
|
|
| 88 |
slide_gallery = gr.Gallery(
|
| 89 |
label="Slide thumbnails",
|
| 90 |
columns=2,
|
| 91 |
-
height=
|
| 92 |
object_fit="contain",
|
|
|
|
| 93 |
)
|
| 94 |
with gr.Tab("Outline"):
|
| 95 |
outline_preview = gr.Markdown(label="Outline (markdown)")
|
|
@@ -137,3 +153,9 @@ then choose **Open with → Google Docs**. You can also upload the `.html` file
|
|
| 137 |
trace_box,
|
| 138 |
],
|
| 139 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
import gradio as gr
|
| 4 |
|
| 5 |
from agent.runner import AgentRunner
|
| 6 |
+
from agent.tools.pptx import get_outputs_dir
|
| 7 |
from gradio_space.model_loading import ensure_model_loaded, get_active_model_key, model_status
|
| 8 |
from inference.factory import get_backend
|
| 9 |
|
| 10 |
+
def _error_html(message: str) -> str:
|
| 11 |
+
safe = (
|
| 12 |
+
message.replace("&", "&")
|
| 13 |
+
.replace("<", "<")
|
| 14 |
+
.replace(">", ">")
|
| 15 |
+
)
|
| 16 |
+
return (
|
| 17 |
+
f'<div style="padding:12px;border:1px solid #c44;border-radius:8px;'
|
| 18 |
+
f'background:#fff5f5;color:#8a1f1f;">{safe}</div>'
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
def generate_lesson_slides(
|
| 23 |
topic: str,
|
| 24 |
grade: str,
|
| 25 |
slide_count: int,
|
| 26 |
+
) -> tuple[str, str, list[str], str | None, str | None, str | None, str, str]:
|
| 27 |
model_key = get_active_model_key()
|
| 28 |
load_error = ensure_model_loaded(model_key)
|
| 29 |
if load_error:
|
| 30 |
+
return load_error, _error_html(load_error), [], None, None, None, load_error, load_error
|
| 31 |
|
| 32 |
if not topic.strip():
|
| 33 |
message = "Please enter a lesson topic."
|
| 34 |
+
return message, _error_html(message), [], None, None, None, message, message
|
| 35 |
|
| 36 |
try:
|
| 37 |
runner = AgentRunner()
|
|
|
|
| 44 |
)
|
| 45 |
except Exception as exc: # noqa: BLE001 — show agent errors in UI
|
| 46 |
message = f"Agent error: {exc}"
|
| 47 |
+
return message, _error_html(message), [], None, None, None, message, message
|
| 48 |
|
| 49 |
+
gallery = [str(Path(p).resolve()) for p in result.preview_images]
|
| 50 |
trace_summary = (
|
| 51 |
f"Run `{result.trace.run_id}` · skill `{result.trace.skill}` · "
|
| 52 |
f"model `{result.trace.model}`\n\n"
|
|
|
|
| 56 |
result.markdown_preview,
|
| 57 |
result.html_preview,
|
| 58 |
gallery,
|
| 59 |
+
str(Path(result.pptx_path).resolve()),
|
| 60 |
+
str(Path(result.docx_path).resolve()),
|
| 61 |
+
str(Path(result.html_export_path).resolve()),
|
| 62 |
trace_summary,
|
| 63 |
result.trace.to_json(),
|
| 64 |
)
|
|
|
|
| 103 |
slide_gallery = gr.Gallery(
|
| 104 |
label="Slide thumbnails",
|
| 105 |
columns=2,
|
| 106 |
+
height=420,
|
| 107 |
object_fit="contain",
|
| 108 |
+
preview=True,
|
| 109 |
)
|
| 110 |
with gr.Tab("Outline"):
|
| 111 |
outline_preview = gr.Markdown(label="Outline (markdown)")
|
|
|
|
| 153 |
trace_box,
|
| 154 |
],
|
| 155 |
)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def gradio_allowed_paths() -> list[str]:
|
| 159 |
+
"""Paths Gradio must be allowed to read for previews and downloads."""
|
| 160 |
+
root = get_outputs_dir().resolve()
|
| 161 |
+
return [str(root)]
|
libs/agent/src/agent/preview.py
CHANGED
|
@@ -215,12 +215,17 @@ def _draw_slide_image(
|
|
| 215 |
|
| 216 |
y = margin
|
| 217 |
if is_title:
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
| 219 |
if subtitle:
|
| 220 |
draw.text((margin, height // 2 + 40), subtitle, fill=accent, font=small_font)
|
| 221 |
else:
|
| 222 |
-
|
| 223 |
-
|
|
|
|
|
|
|
| 224 |
for bullet in bullets:
|
| 225 |
line = _wrap_text(f"• {bullet}", 48)
|
| 226 |
for part in line.split("\n"):
|
|
|
|
| 215 |
|
| 216 |
y = margin
|
| 217 |
if is_title:
|
| 218 |
+
y_title = height // 2 - 80
|
| 219 |
+
for part in _wrap_text(title, 28).split("\n"):
|
| 220 |
+
draw.text((margin, y_title), part, fill=fg, font=title_font)
|
| 221 |
+
y_title += 64
|
| 222 |
if subtitle:
|
| 223 |
draw.text((margin, height // 2 + 40), subtitle, fill=accent, font=small_font)
|
| 224 |
else:
|
| 225 |
+
for part in _wrap_text(title, 32).split("\n"):
|
| 226 |
+
draw.text((margin, y), part, fill=fg, font=title_font)
|
| 227 |
+
y += 52
|
| 228 |
+
y += 20
|
| 229 |
for bullet in bullets:
|
| 230 |
line = _wrap_text(f"• {bullet}", 48)
|
| 231 |
for part in line.split("\n"):
|
libs/agent/src/agent/prompts.py
CHANGED
|
@@ -40,10 +40,19 @@ def education_outline_user(req: EducationPptxInput) -> str:
|
|
| 40 |
)
|
| 41 |
|
| 42 |
|
| 43 |
-
def education_outline_repair(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
return (
|
| 45 |
"The previous response was invalid JSON or did not match the schema.\n"
|
| 46 |
f"Validation error: {error}\n"
|
|
|
|
| 47 |
f"Previous output:\n{invalid_output}\n\n"
|
| 48 |
"Return corrected JSON only, no explanation."
|
| 49 |
)
|
|
|
|
| 40 |
)
|
| 41 |
|
| 42 |
|
| 43 |
+
def education_outline_repair(
|
| 44 |
+
invalid_output: str,
|
| 45 |
+
error: str,
|
| 46 |
+
*,
|
| 47 |
+
expected_slides: int | None = None,
|
| 48 |
+
) -> str:
|
| 49 |
+
count_line = ""
|
| 50 |
+
if expected_slides is not None:
|
| 51 |
+
count_line = f"\nYou must include exactly {expected_slides} items in the slides array.\n"
|
| 52 |
return (
|
| 53 |
"The previous response was invalid JSON or did not match the schema.\n"
|
| 54 |
f"Validation error: {error}\n"
|
| 55 |
+
f"{count_line}"
|
| 56 |
f"Previous output:\n{invalid_output}\n\n"
|
| 57 |
"Return corrected JSON only, no explanation."
|
| 58 |
)
|
libs/agent/src/agent/runner.py
CHANGED
|
@@ -7,7 +7,7 @@ from typing import Any
|
|
| 7 |
|
| 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,
|
|
@@ -125,34 +125,94 @@ class AgentRunner:
|
|
| 125 |
trace.log_llm(prompt_text, raw)
|
| 126 |
|
| 127 |
try:
|
| 128 |
-
return self._parse_outline(raw, req.slide_count)
|
| 129 |
except (json.JSONDecodeError, ValueError) as first_error:
|
| 130 |
repair_messages = messages + [
|
| 131 |
{"role": "assistant", "content": raw},
|
| 132 |
{
|
| 133 |
"role": "user",
|
| 134 |
-
"content": education_outline_repair(
|
|
|
|
|
|
|
| 135 |
},
|
| 136 |
]
|
|
|
|
|
|
|
|
|
|
| 137 |
repaired = backend.chat(repair_messages, max_tokens=2048, temperature=0.1)
|
| 138 |
-
trace.log_llm(
|
| 139 |
-
return self._parse_outline(repaired, req.slide_count)
|
| 140 |
|
| 141 |
-
def _parse_outline(
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
outline = SlideOutline.model_validate(data)
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
)
|
| 154 |
return outline
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
@staticmethod
|
| 157 |
def _extract_json(text: str) -> dict[str, Any]:
|
| 158 |
cleaned = text.strip()
|
|
|
|
| 7 |
|
| 8 |
from inference.base import InferenceBackend
|
| 9 |
|
| 10 |
+
from agent.models import EducationPptxInput, SlideOutline, SlideSpec
|
| 11 |
from agent.preview import outline_to_html, render_slide_images
|
| 12 |
from agent.prompts import (
|
| 13 |
education_outline_repair,
|
|
|
|
| 125 |
trace.log_llm(prompt_text, raw)
|
| 126 |
|
| 127 |
try:
|
| 128 |
+
return self._parse_outline(raw, req.slide_count, trace)
|
| 129 |
except (json.JSONDecodeError, ValueError) as first_error:
|
| 130 |
repair_messages = messages + [
|
| 131 |
{"role": "assistant", "content": raw},
|
| 132 |
{
|
| 133 |
"role": "user",
|
| 134 |
+
"content": education_outline_repair(
|
| 135 |
+
raw, str(first_error), expected_slides=req.slide_count
|
| 136 |
+
),
|
| 137 |
},
|
| 138 |
]
|
| 139 |
+
repair_prompt = education_outline_repair(
|
| 140 |
+
raw, str(first_error), expected_slides=req.slide_count
|
| 141 |
+
)
|
| 142 |
repaired = backend.chat(repair_messages, max_tokens=2048, temperature=0.1)
|
| 143 |
+
trace.log_llm(repair_prompt, repaired)
|
| 144 |
+
return self._parse_outline(repaired, req.slide_count, trace)
|
| 145 |
|
| 146 |
+
def _parse_outline(
|
| 147 |
+
self,
|
| 148 |
+
raw: str,
|
| 149 |
+
expected_slides: int,
|
| 150 |
+
trace: TraceRecorder | None = None,
|
| 151 |
+
) -> SlideOutline:
|
| 152 |
+
data = self._sanitize_outline_data(self._extract_json(raw))
|
| 153 |
outline = SlideOutline.model_validate(data)
|
| 154 |
+
original_count = len(outline.slides)
|
| 155 |
+
outline = self._normalize_slide_count(outline, expected_slides)
|
| 156 |
+
if trace and original_count != expected_slides:
|
| 157 |
+
trace.log_note(
|
| 158 |
+
"Adjusted slide count to match request",
|
| 159 |
+
requested=expected_slides,
|
| 160 |
+
model_returned=original_count,
|
| 161 |
+
final=len(outline.slides),
|
| 162 |
+
)
|
|
|
|
| 163 |
return outline
|
| 164 |
|
| 165 |
+
@staticmethod
|
| 166 |
+
def _sanitize_outline_data(data: dict[str, Any]) -> dict[str, Any]:
|
| 167 |
+
title = str(data.get("title") or "Lesson").strip() or "Lesson"
|
| 168 |
+
slides_in = data.get("slides") or []
|
| 169 |
+
slides_out: list[dict[str, Any]] = []
|
| 170 |
+
for index, slide in enumerate(slides_in):
|
| 171 |
+
if not isinstance(slide, dict):
|
| 172 |
+
continue
|
| 173 |
+
slide_title = str(slide.get("title") or f"Slide {index + 1}").strip()
|
| 174 |
+
bullets_raw = slide.get("bullets") or []
|
| 175 |
+
if isinstance(bullets_raw, str):
|
| 176 |
+
bullets_raw = [bullets_raw]
|
| 177 |
+
bullets = [str(b).strip() for b in bullets_raw if str(b).strip()]
|
| 178 |
+
if not bullets:
|
| 179 |
+
bullets = ["Discuss this topic with the class"]
|
| 180 |
+
slides_out.append(
|
| 181 |
+
{
|
| 182 |
+
"title": slide_title or f"Slide {index + 1}",
|
| 183 |
+
"bullets": bullets,
|
| 184 |
+
"speaker_note": str(slide.get("speaker_note") or ""),
|
| 185 |
+
}
|
| 186 |
+
)
|
| 187 |
+
if not slides_out:
|
| 188 |
+
slides_out.append(
|
| 189 |
+
{
|
| 190 |
+
"title": "Introduction",
|
| 191 |
+
"bullets": ["Overview of the topic", "Why it matters"],
|
| 192 |
+
"speaker_note": "",
|
| 193 |
+
}
|
| 194 |
+
)
|
| 195 |
+
return {"title": title, "slides": slides_out}
|
| 196 |
+
|
| 197 |
+
@staticmethod
|
| 198 |
+
def _normalize_slide_count(outline: SlideOutline, expected: int) -> SlideOutline:
|
| 199 |
+
slides = list(outline.slides)
|
| 200 |
+
if len(slides) > expected:
|
| 201 |
+
slides = slides[:expected]
|
| 202 |
+
while len(slides) < expected:
|
| 203 |
+
number = len(slides) + 1
|
| 204 |
+
slides.append(
|
| 205 |
+
SlideSpec(
|
| 206 |
+
title=f"More about {outline.title}",
|
| 207 |
+
bullets=[
|
| 208 |
+
"Key idea to expand in class",
|
| 209 |
+
"Question for students",
|
| 210 |
+
],
|
| 211 |
+
speaker_note="Add details for this slide during the lesson.",
|
| 212 |
+
)
|
| 213 |
+
)
|
| 214 |
+
return SlideOutline(title=outline.title, slides=slides)
|
| 215 |
+
|
| 216 |
@staticmethod
|
| 217 |
def _extract_json(text: str) -> dict[str, Any]:
|
| 218 |
cleaned = text.strip()
|
libs/agent/src/agent/tools/pptx.py
CHANGED
|
@@ -10,6 +10,11 @@ from pptx.util import Inches, Pt
|
|
| 10 |
from agent.models import SlideOutline
|
| 11 |
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
def _outputs_dir() -> Path:
|
| 14 |
import os
|
| 15 |
import tempfile
|
|
|
|
| 10 |
from agent.models import SlideOutline
|
| 11 |
|
| 12 |
|
| 13 |
+
def get_outputs_dir() -> Path:
|
| 14 |
+
"""Directory for generated artifacts (pptx, docx, preview images)."""
|
| 15 |
+
return _outputs_dir()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
def _outputs_dir() -> Path:
|
| 19 |
import os
|
| 20 |
import tempfile
|
libs/agent/src/agent/trace.py
CHANGED
|
@@ -32,6 +32,9 @@ class TraceRecorder:
|
|
| 32 |
}
|
| 33 |
)
|
| 34 |
|
|
|
|
|
|
|
|
|
|
| 35 |
def log_tool(self, name: str, arguments: dict[str, Any], result: str) -> None:
|
| 36 |
self.steps.append(
|
| 37 |
{
|
|
|
|
| 32 |
}
|
| 33 |
)
|
| 34 |
|
| 35 |
+
def log_note(self, message: str, **details: Any) -> None:
|
| 36 |
+
self.steps.append({"type": "note", "message": message, **details})
|
| 37 |
+
|
| 38 |
def log_tool(self, name: str, arguments: dict[str, Any], result: str) -> None:
|
| 39 |
self.steps.append(
|
| 40 |
{
|
libs/agent/tests/test_runner.py
CHANGED
|
@@ -5,6 +5,33 @@ from agent.tools.docx import create_docx, create_html_export
|
|
| 5 |
from agent.tools.pptx import create_pptx
|
| 6 |
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
def test_extract_json_from_fenced_block():
|
| 9 |
raw = '```json\n{"title": "T", "slides": [{"title": "S", "bullets": ["a"]}]}\n```'
|
| 10 |
data = AgentRunner._extract_json(raw)
|
|
|
|
| 5 |
from agent.tools.pptx import create_pptx
|
| 6 |
|
| 7 |
|
| 8 |
+
def test_parse_outline_pads_when_model_returns_too_few():
|
| 9 |
+
runner = AgentRunner()
|
| 10 |
+
raw = (
|
| 11 |
+
'{"title": "AI Agents", "slides": ['
|
| 12 |
+
'{"title": "Intro", "bullets": ["What is an agent?"]},'
|
| 13 |
+
'{"title": "Uses", "bullets": ["Automation"]}'
|
| 14 |
+
"]}"
|
| 15 |
+
)
|
| 16 |
+
outline = runner._parse_outline(raw, expected_slides=5)
|
| 17 |
+
assert len(outline.slides) == 5
|
| 18 |
+
assert outline.title == "AI Agents"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_parse_outline_trims_when_model_returns_too_many():
|
| 22 |
+
runner = AgentRunner()
|
| 23 |
+
raw = (
|
| 24 |
+
'{"title": "Topic", "slides": ['
|
| 25 |
+
'{"title": "A", "bullets": ["a"]},'
|
| 26 |
+
'{"title": "B", "bullets": ["b"]},'
|
| 27 |
+
'{"title": "C", "bullets": ["c"]},'
|
| 28 |
+
'{"title": "D", "bullets": ["d"]}'
|
| 29 |
+
"]}"
|
| 30 |
+
)
|
| 31 |
+
outline = runner._parse_outline(raw, expected_slides=3)
|
| 32 |
+
assert len(outline.slides) == 3
|
| 33 |
+
|
| 34 |
+
|
| 35 |
def test_extract_json_from_fenced_block():
|
| 36 |
raw = '```json\n{"title": "T", "slides": [{"title": "S", "bullets": ["a"]}]}\n```'
|
| 37 |
data = AgentRunner._extract_json(raw)
|
uv.lock
CHANGED
|
@@ -45,7 +45,9 @@ version = "0.1.0"
|
|
| 45 |
source = { editable = "libs/agent" }
|
| 46 |
dependencies = [
|
| 47 |
{ name = "inference" },
|
|
|
|
| 48 |
{ name = "pydantic" },
|
|
|
|
| 49 |
{ name = "python-pptx" },
|
| 50 |
{ name = "pyyaml" },
|
| 51 |
]
|
|
@@ -53,7 +55,9 @@ dependencies = [
|
|
| 53 |
[package.metadata]
|
| 54 |
requires-dist = [
|
| 55 |
{ name = "inference", editable = "libs/inference" },
|
|
|
|
| 56 |
{ name = "pydantic", specifier = ">=2.0.0" },
|
|
|
|
| 57 |
{ name = "python-pptx", specifier = ">=1.0.0" },
|
| 58 |
{ name = "pyyaml", specifier = ">=6.0.2" },
|
| 59 |
]
|
|
@@ -1324,6 +1328,19 @@ wheels = [
|
|
| 1324 |
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
| 1325 |
]
|
| 1326 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1327 |
[[package]]
|
| 1328 |
name = "python-multipart"
|
| 1329 |
version = "0.0.32"
|
|
|
|
| 45 |
source = { editable = "libs/agent" }
|
| 46 |
dependencies = [
|
| 47 |
{ name = "inference" },
|
| 48 |
+
{ name = "pillow" },
|
| 49 |
{ name = "pydantic" },
|
| 50 |
+
{ name = "python-docx" },
|
| 51 |
{ name = "python-pptx" },
|
| 52 |
{ name = "pyyaml" },
|
| 53 |
]
|
|
|
|
| 55 |
[package.metadata]
|
| 56 |
requires-dist = [
|
| 57 |
{ name = "inference", editable = "libs/inference" },
|
| 58 |
+
{ name = "pillow", specifier = ">=10.0.0" },
|
| 59 |
{ name = "pydantic", specifier = ">=2.0.0" },
|
| 60 |
+
{ name = "python-docx", specifier = ">=1.1.0" },
|
| 61 |
{ name = "python-pptx", specifier = ">=1.0.0" },
|
| 62 |
{ name = "pyyaml", specifier = ">=6.0.2" },
|
| 63 |
]
|
|
|
|
| 1328 |
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
| 1329 |
]
|
| 1330 |
|
| 1331 |
+
[[package]]
|
| 1332 |
+
name = "python-docx"
|
| 1333 |
+
version = "1.2.0"
|
| 1334 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1335 |
+
dependencies = [
|
| 1336 |
+
{ name = "lxml" },
|
| 1337 |
+
{ name = "typing-extensions" },
|
| 1338 |
+
]
|
| 1339 |
+
sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" }
|
| 1340 |
+
wheels = [
|
| 1341 |
+
{ url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" },
|
| 1342 |
+
]
|
| 1343 |
+
|
| 1344 |
[[package]]
|
| 1345 |
name = "python-multipart"
|
| 1346 |
version = "0.0.32"
|