Spaces:
Sleeping
Sleeping
Commit ·
4b65659
1
Parent(s): d10324f
skills prompt and tools
Browse files
libs/agent/src/agent/prompts.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
from agent.models import EducationPptxInput
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def education_outline_system(skill_body: str) -> str:
|
| 9 |
+
return f"""You are a lesson-planning assistant for teachers.
|
| 10 |
+
Follow the skill workflow below and output ONLY valid JSON (no markdown fences).
|
| 11 |
+
|
| 12 |
+
Skill workflow:
|
| 13 |
+
{skill_body}
|
| 14 |
+
|
| 15 |
+
JSON schema:
|
| 16 |
+
{{
|
| 17 |
+
"title": "string — presentation title",
|
| 18 |
+
"slides": [
|
| 19 |
+
{{
|
| 20 |
+
"title": "string — slide heading",
|
| 21 |
+
"bullets": ["string", "..."],
|
| 22 |
+
"speaker_note": "string — one sentence for the teacher"
|
| 23 |
+
}}
|
| 24 |
+
]
|
| 25 |
+
}}
|
| 26 |
+
|
| 27 |
+
Rules:
|
| 28 |
+
- Use exactly the requested number of content slides (title slide is added separately by the tool).
|
| 29 |
+
- Bullets should be short, age-appropriate, and factual.
|
| 30 |
+
- speaker_note is optional but helpful for each slide.
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def education_outline_user(req: EducationPptxInput) -> str:
|
| 35 |
+
return (
|
| 36 |
+
f"Topic: {req.topic}\n"
|
| 37 |
+
f"Grade level: {req.grade}\n"
|
| 38 |
+
f"Number of content slides: {req.slide_count}\n"
|
| 39 |
+
"Return JSON only."
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def education_outline_repair(invalid_output: str, error: str) -> str:
|
| 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 |
+
)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def outline_to_markdown(title: str, slides: list[dict]) -> str:
|
| 53 |
+
lines = [f"# {title}", ""]
|
| 54 |
+
for index, slide in enumerate(slides, start=1):
|
| 55 |
+
lines.append(f"## Slide {index}: {slide.get('title', 'Untitled')}")
|
| 56 |
+
for bullet in slide.get("bullets", []):
|
| 57 |
+
lines.append(f"- {bullet}")
|
| 58 |
+
note = slide.get("speaker_note", "")
|
| 59 |
+
if note:
|
| 60 |
+
lines.append(f"\n*Teacher note:* {note}")
|
| 61 |
+
lines.append("")
|
| 62 |
+
return "\n".join(lines).strip()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def outline_json_example(slide_count: int) -> str:
|
| 66 |
+
example = {
|
| 67 |
+
"title": "Example Lesson",
|
| 68 |
+
"slides": [
|
| 69 |
+
{
|
| 70 |
+
"title": f"Slide {i}",
|
| 71 |
+
"bullets": ["Key point A", "Key point B"],
|
| 72 |
+
"speaker_note": "Brief teaching tip.",
|
| 73 |
+
}
|
| 74 |
+
for i in range(1, slide_count + 1)
|
| 75 |
+
],
|
| 76 |
+
}
|
| 77 |
+
return json.dumps(example, indent=2)
|
libs/agent/src/agent/skills.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
import yaml
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@dataclass(frozen=True)
|
| 12 |
+
class Skill:
|
| 13 |
+
name: str
|
| 14 |
+
description: str
|
| 15 |
+
task: str
|
| 16 |
+
tools: list[str]
|
| 17 |
+
model_hints: list[str]
|
| 18 |
+
body: str
|
| 19 |
+
path: Path
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _find_skills_root() -> Path | None:
|
| 23 |
+
import os
|
| 24 |
+
|
| 25 |
+
env_path = os.environ.get("SKILLS_DIR")
|
| 26 |
+
if env_path:
|
| 27 |
+
root = Path(env_path)
|
| 28 |
+
if root.is_dir():
|
| 29 |
+
return root.resolve()
|
| 30 |
+
|
| 31 |
+
for base in (Path.cwd(), *Path.cwd().parents):
|
| 32 |
+
candidate = base / "skills"
|
| 33 |
+
if candidate.is_dir():
|
| 34 |
+
return candidate.resolve()
|
| 35 |
+
return None
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _parse_skill_md(path: Path) -> Skill:
|
| 39 |
+
text = path.read_text()
|
| 40 |
+
match = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)$", text, re.DOTALL)
|
| 41 |
+
if not match:
|
| 42 |
+
raise ValueError(f"{path}: expected YAML frontmatter delimited by ---")
|
| 43 |
+
|
| 44 |
+
meta: dict[str, Any] = yaml.safe_load(match.group(1)) or {}
|
| 45 |
+
body = match.group(2).strip()
|
| 46 |
+
|
| 47 |
+
return Skill(
|
| 48 |
+
name=str(meta.get("name", path.parent.name)),
|
| 49 |
+
description=str(meta.get("description", "")),
|
| 50 |
+
task=str(meta.get("task", "")),
|
| 51 |
+
tools=[str(t) for t in meta.get("tools", [])],
|
| 52 |
+
model_hints=[str(m) for m in meta.get("model_hints", [])],
|
| 53 |
+
body=body,
|
| 54 |
+
path=path,
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class SkillRegistry:
|
| 59 |
+
def __init__(self, skills_root: Path | None = None) -> None:
|
| 60 |
+
self._root = skills_root or _find_skills_root()
|
| 61 |
+
self._skills: dict[str, Skill] = {}
|
| 62 |
+
if self._root is not None:
|
| 63 |
+
self._load_all()
|
| 64 |
+
|
| 65 |
+
@property
|
| 66 |
+
def root(self) -> Path | None:
|
| 67 |
+
return self._root
|
| 68 |
+
|
| 69 |
+
def _load_all(self) -> None:
|
| 70 |
+
assert self._root is not None
|
| 71 |
+
for skill_md in sorted(self._root.glob("*/SKILL.md")):
|
| 72 |
+
skill = _parse_skill_md(skill_md)
|
| 73 |
+
self._skills[skill.name] = skill
|
| 74 |
+
|
| 75 |
+
def get(self, name: str) -> Skill:
|
| 76 |
+
if name not in self._skills:
|
| 77 |
+
known = ", ".join(sorted(self._skills)) or "(none found)"
|
| 78 |
+
raise KeyError(f"Unknown skill {name!r}. Known: {known}")
|
| 79 |
+
return self._skills[name]
|
| 80 |
+
|
| 81 |
+
def list_by_task(self, task: str) -> list[Skill]:
|
| 82 |
+
return [s for s in self._skills.values() if s.task == task]
|
| 83 |
+
|
| 84 |
+
def all(self) -> list[Skill]:
|
| 85 |
+
return list(self._skills.values())
|
libs/agent/src/agent/tools/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from agent.tools.pptx import create_pptx
|
| 2 |
+
|
| 3 |
+
__all__ = ["create_pptx"]
|
libs/agent/src/agent/tools/pptx.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
import uuid
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from pptx import Presentation
|
| 8 |
+
from pptx.util import Inches, Pt
|
| 9 |
+
|
| 10 |
+
from agent.models import SlideOutline
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _outputs_dir() -> Path:
|
| 14 |
+
import os
|
| 15 |
+
import tempfile
|
| 16 |
+
|
| 17 |
+
env = os.environ.get("AGENT_OUTPUTS_DIR")
|
| 18 |
+
if env:
|
| 19 |
+
path = Path(env)
|
| 20 |
+
else:
|
| 21 |
+
path = Path(tempfile.gettempdir()) / "agent_outputs"
|
| 22 |
+
path.mkdir(parents=True, exist_ok=True)
|
| 23 |
+
return path
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _safe_filename(title: str) -> str:
|
| 27 |
+
slug = re.sub(r"[^a-zA-Z0-9]+", "_", title.strip().lower()).strip("_")
|
| 28 |
+
return slug[:60] or "lesson"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def create_pptx(outline: SlideOutline, run_id: str | None = None) -> Path:
|
| 32 |
+
"""Build a .pptx from a validated slide outline."""
|
| 33 |
+
rid = run_id or uuid.uuid4().hex[:12]
|
| 34 |
+
out_dir = _outputs_dir()
|
| 35 |
+
filename = f"{_safe_filename(outline.title)}_{rid}.pptx"
|
| 36 |
+
path = out_dir / filename
|
| 37 |
+
|
| 38 |
+
prs = Presentation()
|
| 39 |
+
prs.slide_width = Inches(10)
|
| 40 |
+
prs.slide_height = Inches(7.5)
|
| 41 |
+
|
| 42 |
+
title_layout = prs.slide_layouts[0]
|
| 43 |
+
title_slide = prs.slides.add_slide(title_layout)
|
| 44 |
+
title_slide.shapes.title.text = outline.title
|
| 45 |
+
if title_slide.placeholders[1].text_frame:
|
| 46 |
+
title_slide.placeholders[1].text = "Generated lesson slides"
|
| 47 |
+
|
| 48 |
+
bullet_layout = prs.slide_layouts[1]
|
| 49 |
+
for slide_spec in outline.slides:
|
| 50 |
+
slide = prs.slides.add_slide(bullet_layout)
|
| 51 |
+
slide.shapes.title.text = slide_spec.title
|
| 52 |
+
body = slide.placeholders[1].text_frame
|
| 53 |
+
body.clear()
|
| 54 |
+
for index, bullet in enumerate(slide_spec.bullets):
|
| 55 |
+
if index == 0:
|
| 56 |
+
p = body.paragraphs[0]
|
| 57 |
+
else:
|
| 58 |
+
p = body.add_paragraph()
|
| 59 |
+
p.text = bullet
|
| 60 |
+
p.level = 0
|
| 61 |
+
p.font.size = Pt(20)
|
| 62 |
+
if slide_spec.speaker_note:
|
| 63 |
+
notes = slide.notes_slide.notes_text_frame
|
| 64 |
+
notes.text = slide_spec.speaker_note
|
| 65 |
+
|
| 66 |
+
prs.save(str(path))
|
| 67 |
+
return path
|
libs/agent/src/agent/tools_registry.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from collections.abc import Callable
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
from agent.models import SlideOutline
|
| 8 |
+
from agent.tools.pptx import create_pptx
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@dataclass(frozen=True)
|
| 12 |
+
class ToolSpec:
|
| 13 |
+
name: str
|
| 14 |
+
description: str
|
| 15 |
+
handler: Callable[..., Any]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ToolRegistry:
|
| 19 |
+
def __init__(self) -> None:
|
| 20 |
+
self._tools: dict[str, ToolSpec] = {}
|
| 21 |
+
self.register(
|
| 22 |
+
"create_pptx",
|
| 23 |
+
"Create a PowerPoint file from a validated SlideOutline",
|
| 24 |
+
self._handle_create_pptx,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
def register(self, name: str, description: str, handler: Callable[..., Any]) -> None:
|
| 28 |
+
self._tools[name] = ToolSpec(name=name, description=description, handler=handler)
|
| 29 |
+
|
| 30 |
+
def get(self, name: str) -> ToolSpec:
|
| 31 |
+
if name not in self._tools:
|
| 32 |
+
raise KeyError(f"Unknown tool {name!r}")
|
| 33 |
+
return self._tools[name]
|
| 34 |
+
|
| 35 |
+
def _handle_create_pptx(self, outline: SlideOutline, run_id: str | None = None) -> str:
|
| 36 |
+
path = create_pptx(outline, run_id=run_id)
|
| 37 |
+
return str(path)
|