MSGEncrypted commited on
Commit
b911f86
·
1 Parent(s): 4b65659

education ppxt skills

Browse files
.cursor/plans/skill_agent_pptx_5413e3c2.plan.md CHANGED
@@ -4,13 +4,13 @@ overview: Add a Hermes-style skill agent library on top of your existing Transfo
4
  todos:
5
  - id: agent-lib
6
  content: "Create libs/agent: SkillRegistry, ToolRegistry, AgentRunner, TraceRecorder, pydantic outline models"
7
- status: in_progress
8
  - id: pptx-skill
9
  content: Add skills/education-pptx/SKILL.md and create_pptx tool (python-pptx)
10
- status: pending
11
  - id: gradio-tabs
12
  content: "Refactor Gradio app into Tabs: Education PPTX (primary) + Chat (debug)"
13
- status: pending
14
  - id: docker-workspace
15
  content: Wire agent package into uv workspace, Dockerfile, models.yaml active_model
16
  status: pending
 
4
  todos:
5
  - id: agent-lib
6
  content: "Create libs/agent: SkillRegistry, ToolRegistry, AgentRunner, TraceRecorder, pydantic outline models"
7
+ status: completed
8
  - id: pptx-skill
9
  content: Add skills/education-pptx/SKILL.md and create_pptx tool (python-pptx)
10
+ status: completed
11
  - id: gradio-tabs
12
  content: "Refactor Gradio app into Tabs: Education PPTX (primary) + Chat (debug)"
13
+ status: in_progress
14
  - id: docker-workspace
15
  content: Wire agent package into uv workspace, Dockerfile, models.yaml active_model
16
  status: pending
apps/gradio-space/src/gradio_space/app.py CHANGED
@@ -2,103 +2,11 @@ import os
2
 
3
  import gradio as gr
4
 
5
- from inference.config import get_app_config, get_model_config
6
- from inference.factory import get_backend, reset_backend
 
7
 
8
  _app_config = get_app_config()
9
- _current_model_key: str | None = None
10
- _load_state: dict[str, bool] = {}
11
- _load_errors: dict[str, str] = {}
12
-
13
-
14
- def _ensure_model_loaded(model_key: str) -> str | None:
15
- global _current_model_key
16
-
17
- if model_key != _current_model_key:
18
- reset_backend()
19
- _current_model_key = model_key
20
-
21
- if _load_state.get(model_key):
22
- return None
23
-
24
- if model_key in _load_errors:
25
- return _load_errors[model_key]
26
-
27
- try:
28
- get_backend(model_key).load()
29
- _load_state[model_key] = True
30
- return None
31
- except Exception as exc: # noqa: BLE001 — surface model load failures in the UI
32
- message = f"Failed to load model: {exc}"
33
- _load_errors[model_key] = message
34
- return message
35
-
36
-
37
- def _history_to_messages(history: list) -> list[dict[str, str]]:
38
- messages: list[dict[str, str]] = []
39
- for item in history:
40
- if isinstance(item, dict):
41
- messages.append({"role": item["role"], "content": item["content"]})
42
- else:
43
- user_msg, assistant_msg = item
44
- messages.append({"role": "user", "content": user_msg})
45
- if assistant_msg:
46
- messages.append({"role": "assistant", "content": assistant_msg})
47
- return messages
48
-
49
-
50
- def chat(message: str, history: list, model_key: str) -> str:
51
- load_error = _ensure_model_loaded(model_key)
52
- if load_error:
53
- return load_error
54
-
55
- messages = _history_to_messages(history)
56
- messages.append({"role": "user", "content": message})
57
- return get_backend(model_key).chat(messages)
58
-
59
-
60
- def _runtime_device_hint(model_key: str) -> str:
61
- model = get_model_config(model_key)
62
- if model.backend == "transformers":
63
- try:
64
- import torch
65
-
66
- if torch.cuda.is_available():
67
- return f"GPU ({torch.cuda.get_device_name(0)})"
68
- except ImportError:
69
- pass
70
- return "CPU"
71
- if model.n_gpu_layers > 0:
72
- return f"llama.cpp GPU offload ({model.n_gpu_layers} layers)"
73
- return "CPU"
74
-
75
-
76
- def warmup(model_key: str | None = None) -> str:
77
- key = model_key or _app_config.active_model
78
- model = get_model_config(key)
79
-
80
- if _load_state.get(key):
81
- backend = get_backend(key)
82
- device = (
83
- backend.device_label
84
- if hasattr(backend, "device_label")
85
- else _runtime_device_hint(key)
86
- )
87
- return f"Model ready: {model.label} on {device}"
88
-
89
- if key in _load_errors:
90
- return _load_errors[key]
91
-
92
- device_hint = _runtime_device_hint(key)
93
- return (
94
- f"Preset `{key}` selected ({model.backend}, {device_hint}). "
95
- "Weights load on the first chat message."
96
- )
97
-
98
-
99
- def model_status(model_key: str) -> str:
100
- model = get_model_config(model_key)
101
- return f"**{model.label}**\n\n- Backend: `{model.backend}`\n- {warmup(model_key)}"
102
 
103
 
104
  def build_demo() -> gr.Blocks:
@@ -109,14 +17,14 @@ def build_demo() -> gr.Blocks:
109
  else "Using built-in presets (models.yaml not found)."
110
  )
111
 
112
- with gr.Blocks(title="Small Model Hackathon") as demo:
113
  gr.Markdown(
114
  f"""
115
- # Small Model Chat
116
 
117
- Local inference with preset-based configuration.
118
 
119
- - **Default preset:** `{active.key}` — {active.label}
120
  - **Backend:** `{active.backend}`
121
  - {presets_note}
122
 
@@ -124,36 +32,13 @@ Part of the [Build Small Hackathon](https://huggingface.co/build-small-hackathon
124
  """
125
  )
126
 
127
- if _app_config.allow_model_switch and len(_app_config.models) > 1:
128
- model_dropdown = gr.Dropdown(
129
- choices=_app_config.model_choices(),
130
- value=_app_config.active_model,
131
- label="Model preset",
132
- info="Switch presets for local testing. Each preset loads on first use.",
133
- )
134
- status = gr.Markdown(model_status(_app_config.active_model))
135
-
136
- model_dropdown.change(
137
- fn=model_status,
138
- inputs=model_dropdown,
139
- outputs=status,
140
- )
141
-
142
- gr.ChatInterface(
143
- fn=chat,
144
- additional_inputs=[model_dropdown],
145
- examples=[
146
- ["Hello! What can you help me with?", _app_config.active_model],
147
- ["Explain llama.cpp in one sentence.", _app_config.active_model],
148
- ],
149
- )
150
- else:
151
- status = gr.Markdown(model_status(_app_config.active_model))
152
- gr.ChatInterface(
153
- fn=lambda message, history: chat(message, history, _app_config.active_model),
154
- examples=["Hello! What can you help me with?", "Explain llama.cpp in one sentence."],
155
- )
156
- demo.load(lambda: warmup(_app_config.active_model), outputs=status)
157
 
158
  return demo
159
 
 
2
 
3
  import gradio as gr
4
 
5
+ from gradio_space.model_loading import warmup
6
+ from gradio_space.tabs import build_chat_tab, build_education_pptx_tab
7
+ from inference.config import get_app_config
8
 
9
  _app_config = get_app_config()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
 
12
  def build_demo() -> gr.Blocks:
 
17
  else "Using built-in presets (models.yaml not found)."
18
  )
19
 
20
+ with gr.Blocks(title="Lesson Agent — Build Small Hackathon") as demo:
21
  gr.Markdown(
22
  f"""
23
+ # Lesson Agent
24
 
25
+ Local skill-based agent for teachers — **topic in, PowerPoint out**.
26
 
27
+ - **Model:** `{active.key}` — {active.label}
28
  - **Backend:** `{active.backend}`
29
  - {presets_note}
30
 
 
32
  """
33
  )
34
 
35
+ with gr.Tabs():
36
+ with gr.Tab("Lesson slides"):
37
+ build_education_pptx_tab()
38
+ with gr.Tab("Chat (debug)"):
39
+ build_chat_tab()
40
+
41
+ demo.load(lambda: warmup(_app_config.active_model))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
  return demo
44
 
apps/gradio-space/src/gradio_space/model_loading.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from inference.config import get_app_config, get_model_config
2
+ from inference.factory import get_backend, reset_backend
3
+
4
+ _app_config = get_app_config()
5
+ _current_model_key: str | None = None
6
+ _load_state: dict[str, bool] = {}
7
+ _load_errors: dict[str, str] = {}
8
+
9
+
10
+ def get_active_model_key() -> str:
11
+ return _app_config.active_model
12
+
13
+
14
+ def ensure_model_loaded(model_key: str) -> str | None:
15
+ global _current_model_key
16
+
17
+ if model_key != _current_model_key:
18
+ reset_backend()
19
+ _current_model_key = model_key
20
+
21
+ if _load_state.get(model_key):
22
+ return None
23
+
24
+ if model_key in _load_errors:
25
+ return _load_errors[model_key]
26
+
27
+ try:
28
+ get_backend(model_key).load()
29
+ _load_state[model_key] = True
30
+ return None
31
+ except Exception as exc: # noqa: BLE001 — surface model load failures in the UI
32
+ message = f"Failed to load model: {exc}"
33
+ _load_errors[model_key] = message
34
+ return message
35
+
36
+
37
+ def runtime_device_hint(model_key: str) -> str:
38
+ model = get_model_config(model_key)
39
+ if model.backend == "transformers":
40
+ try:
41
+ import torch
42
+
43
+ if torch.cuda.is_available():
44
+ return f"GPU ({torch.cuda.get_device_name(0)})"
45
+ except ImportError:
46
+ pass
47
+ return "CPU"
48
+ if model.n_gpu_layers > 0:
49
+ return f"llama.cpp GPU offload ({model.n_gpu_layers} layers)"
50
+ return "CPU"
51
+
52
+
53
+ def warmup(model_key: str | None = None) -> str:
54
+ key = model_key or _app_config.active_model
55
+ model = get_model_config(key)
56
+
57
+ if _load_state.get(key):
58
+ backend = get_backend(key)
59
+ device = (
60
+ backend.device_label
61
+ if hasattr(backend, "device_label")
62
+ else runtime_device_hint(key)
63
+ )
64
+ return f"Model ready: {model.label} on {device}"
65
+
66
+ if key in _load_errors:
67
+ return _load_errors[key]
68
+
69
+ device_hint = runtime_device_hint(key)
70
+ return (
71
+ f"Preset `{key}` selected ({model.backend}, {device_hint}). "
72
+ "Weights load on the first request."
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)}"
apps/gradio-space/src/gradio_space/tabs/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from gradio_space.tabs.chat import build_chat_tab
2
+ from gradio_space.tabs.education_pptx import build_education_pptx_tab
3
+
4
+ __all__ = ["build_chat_tab", "build_education_pptx_tab"]
apps/gradio-space/src/gradio_space/tabs/chat.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ from gradio_space.model_loading import (
4
+ chat as chat_fn,
5
+ ensure_model_loaded,
6
+ get_active_model_key,
7
+ model_status,
8
+ warmup,
9
+ )
10
+ from inference.config import get_app_config
11
+
12
+ _app_config = get_app_config()
13
+
14
+
15
+ def _history_to_messages(history: list) -> list[dict[str, str]]:
16
+ messages: list[dict[str, str]] = []
17
+ for item in history:
18
+ if isinstance(item, dict):
19
+ messages.append({"role": item["role"], "content": item["content"]})
20
+ else:
21
+ user_msg, assistant_msg = item
22
+ messages.append({"role": "user", "content": user_msg})
23
+ if assistant_msg:
24
+ messages.append({"role": "assistant", "content": assistant_msg})
25
+ return messages
26
+
27
+
28
+ def chat(message: str, history: list, model_key: str) -> str:
29
+ load_error = ensure_model_loaded(model_key)
30
+ if load_error:
31
+ return load_error
32
+ return chat_fn(message, history, model_key)
33
+
34
+
35
+ def build_chat_tab() -> None:
36
+ gr.Markdown(
37
+ """
38
+ ### Model chat (debug)
39
+
40
+ Test the active local model with a simple chat interface.
41
+ """
42
+ )
43
+
44
+ model_key = get_active_model_key()
45
+
46
+ if _app_config.allow_model_switch and len(_app_config.models) > 1:
47
+ model_dropdown = gr.Dropdown(
48
+ choices=_app_config.model_choices(),
49
+ value=_app_config.active_model,
50
+ label="Model preset",
51
+ )
52
+ status = gr.Markdown(model_status(model_key))
53
+ model_dropdown.change(fn=model_status, inputs=model_dropdown, outputs=status)
54
+ gr.ChatInterface(
55
+ fn=chat,
56
+ additional_inputs=[model_dropdown],
57
+ examples=[
58
+ ["Hello! What can you help me with?", _app_config.active_model],
59
+ ["Explain photosynthesis in one sentence.", _app_config.active_model],
60
+ ],
61
+ )
62
+ else:
63
+ status = gr.Markdown(model_status(model_key))
64
+ gr.ChatInterface(
65
+ fn=lambda message, history: chat(message, history, model_key),
66
+ examples=[
67
+ "Hello! What can you help me with?",
68
+ "Explain photosynthesis in one sentence.",
69
+ ],
70
+ )
71
+ gr.on(fn=lambda: warmup(model_key), outputs=status)
apps/gradio-space/src/gradio_space/tabs/education_pptx.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
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()
24
+ result = runner.run_education_pptx(
25
+ topic=topic,
26
+ grade=grade,
27
+ slide_count=int(slide_count),
28
+ model_key=model_key,
29
+ backend=get_backend(model_key),
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:
44
+ model_key = get_active_model_key()
45
+
46
+ gr.Markdown(
47
+ """
48
+ ### Lesson slide builder
49
+
50
+ Enter a topic and grade level. A **local small model** drafts the outline;
51
+ the agent then builds a downloadable PowerPoint — no cloud LLM API.
52
+ """
53
+ )
54
+ gr.Markdown(model_status(model_key))
55
+
56
+ with gr.Row():
57
+ topic = gr.Textbox(
58
+ label="Lesson topic",
59
+ placeholder="e.g. Photosynthesis, Fractions, The water cycle",
60
+ )
61
+ grade = gr.Dropdown(
62
+ label="Grade level",
63
+ choices=["K", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "Adult"],
64
+ value="6",
65
+ )
66
+ slide_count = gr.Slider(
67
+ minimum=3,
68
+ maximum=8,
69
+ step=1,
70
+ value=5,
71
+ label="Content slides",
72
+ )
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,
81
+ max_lines=20,
82
+ interactive=False,
83
+ )
84
+
85
+ with gr.Accordion("Trace summary", open=False):
86
+ trace_summary = gr.Markdown()
87
+
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
+ )
libs/agent/src/agent/runner.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
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,
14
+ education_outline_user,
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
+
21
+ EDUCATION_PPTX_SKILL = "education-pptx"
22
+
23
+
24
+ @dataclass
25
+ class AgentResult:
26
+ markdown_preview: str
27
+ pptx_path: str
28
+ trace: TraceRecorder
29
+ trace_path: str
30
+ outline: SlideOutline
31
+
32
+
33
+ class AgentRunner:
34
+ def __init__(
35
+ self,
36
+ skills: SkillRegistry | None = None,
37
+ tools: ToolRegistry | None = None,
38
+ ) -> None:
39
+ self._skills = skills or SkillRegistry()
40
+ self._tools = tools or ToolRegistry()
41
+
42
+ def run_education_pptx(
43
+ self,
44
+ *,
45
+ topic: str,
46
+ grade: str,
47
+ slide_count: int,
48
+ model_key: str,
49
+ backend: InferenceBackend,
50
+ ) -> AgentResult:
51
+ skill = self._skills.get(EDUCATION_PPTX_SKILL)
52
+ req = EducationPptxInput(topic=topic.strip(), grade=grade, slide_count=slide_count)
53
+
54
+ trace = TraceRecorder(
55
+ skill=skill.name,
56
+ model=model_key,
57
+ user_input=req.model_dump(),
58
+ )
59
+
60
+ backend.load()
61
+ outline = self._generate_outline(skill, req, backend, trace)
62
+ tool = self._tools.get("create_pptx")
63
+ pptx_path = tool.handler(outline, run_id=trace.run_id)
64
+ trace.log_tool(
65
+ "create_pptx",
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,
81
+ )
82
+
83
+ def _generate_outline(
84
+ self,
85
+ skill: Any,
86
+ req: EducationPptxInput,
87
+ backend: InferenceBackend,
88
+ trace: TraceRecorder,
89
+ ) -> SlideOutline:
90
+ system = education_outline_system(skill.body)
91
+ user = education_outline_user(req)
92
+ messages = [
93
+ {"role": "system", "content": system},
94
+ {"role": "user", "content": user},
95
+ ]
96
+ prompt_text = system + "\n\n" + user
97
+ raw = backend.chat(messages, max_tokens=2048, temperature=0.3)
98
+ trace.log_llm(prompt_text, raw)
99
+
100
+ try:
101
+ return self._parse_outline(raw, req.slide_count)
102
+ except (json.JSONDecodeError, ValueError) as first_error:
103
+ repair_messages = messages + [
104
+ {"role": "assistant", "content": raw},
105
+ {
106
+ "role": "user",
107
+ "content": education_outline_repair(raw, str(first_error)),
108
+ },
109
+ ]
110
+ repaired = backend.chat(repair_messages, max_tokens=2048, temperature=0.1)
111
+ trace.log_llm(education_outline_repair(raw, str(first_error)), repaired)
112
+ return self._parse_outline(repaired, req.slide_count)
113
+
114
+ def _parse_outline(self, raw: str, expected_slides: int) -> SlideOutline:
115
+ data = self._extract_json(raw)
116
+ outline = SlideOutline.model_validate(data)
117
+ if len(outline.slides) != expected_slides:
118
+ if len(outline.slides) > expected_slides:
119
+ outline = SlideOutline(
120
+ title=outline.title,
121
+ slides=outline.slides[:expected_slides],
122
+ )
123
+ else:
124
+ raise ValueError(
125
+ f"Expected {expected_slides} slides, got {len(outline.slides)}"
126
+ )
127
+ return outline
128
+
129
+ @staticmethod
130
+ def _extract_json(text: str) -> dict[str, Any]:
131
+ cleaned = text.strip()
132
+ fence = re.search(r"```(?:json)?\s*(\{.*\})\s*```", cleaned, re.DOTALL)
133
+ if fence:
134
+ cleaned = fence.group(1)
135
+ else:
136
+ start = cleaned.find("{")
137
+ end = cleaned.rfind("}")
138
+ if start >= 0 and end > start:
139
+ cleaned = cleaned[start : end + 1]
140
+ return json.loads(cleaned)
skills/education-pptx/SKILL.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: education-pptx
3
+ description: Create a short lesson PowerPoint from a topic and grade level
4
+ task: education
5
+ tools:
6
+ - create_pptx
7
+ model_hints:
8
+ - minicpm5-1b
9
+ - qwen3b-gguf
10
+ ---
11
+
12
+ ## Workflow
13
+
14
+ 1. Ask for topic, audience grade, and slide count (3–8 content slides).
15
+ 2. Produce a JSON outline with `title` and `slides` (each slide has `title`, `bullets`, `speaker_note`).
16
+ 3. Call `create_pptx` with the validated outline.
17
+ 4. Return a download link and markdown preview for the teacher.