Implements a background process that monitors session logs for successful agent action sequences, uses the LLM router to extract reusable skill templates, and stores them in a SQLite database. Discovered skills are surfaced via dashboard notifications (push + WebSocket + event bus) and a new /skills page with HTMX polling. Users can confirm, reject, or archive discovered skills. - src/timmy/skill_discovery.py: Core engine with LLM analysis + heuristic fallback - src/dashboard/routes/skills.py: CRUD routes for skill management - src/dashboard/templates/skills.html: Main skills page - src/dashboard/templates/partials/skills_list.html: HTMX partial - Background scheduler in app.py runs every 10 minutes - 31 unit tests covering DB ops, clustering, parsing, dedup, and scan Fixes #1011 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
83 lines
2.7 KiB
Python
83 lines
2.7 KiB
Python
"""Skill Discovery routes — view and manage auto-discovered skills."""
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Form, HTTPException, Request
|
|
from fastapi.responses import HTMLResponse
|
|
|
|
from dashboard.templating import templates
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/skills", tags=["skills"])
|
|
|
|
|
|
@router.get("", response_class=HTMLResponse)
|
|
async def skills_page(request: Request):
|
|
"""Main skill discovery page."""
|
|
from timmy.skill_discovery import get_skill_discovery_engine
|
|
|
|
engine = get_skill_discovery_engine()
|
|
skills = engine.list_skills(limit=50)
|
|
counts = engine.skill_count()
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"skills.html",
|
|
{"skills": skills, "counts": counts},
|
|
)
|
|
|
|
|
|
@router.get("/list", response_class=HTMLResponse)
|
|
async def skills_list_partial(request: Request, status: str = ""):
|
|
"""HTMX partial: return skill list for polling."""
|
|
from timmy.skill_discovery import get_skill_discovery_engine
|
|
|
|
engine = get_skill_discovery_engine()
|
|
skills = engine.list_skills(status=status or None, limit=50)
|
|
counts = engine.skill_count()
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"partials/skills_list.html",
|
|
{"skills": skills, "counts": counts},
|
|
)
|
|
|
|
|
|
@router.post("/{skill_id}/status", response_class=HTMLResponse)
|
|
async def update_skill_status(request: Request, skill_id: str, status: str = Form(...)):
|
|
"""Update a skill's status (confirm / reject / archive)."""
|
|
from timmy.skill_discovery import get_skill_discovery_engine
|
|
|
|
engine = get_skill_discovery_engine()
|
|
if not engine.update_status(skill_id, status):
|
|
raise HTTPException(status_code=400, detail=f"Invalid status: {status}")
|
|
|
|
skills = engine.list_skills(limit=50)
|
|
counts = engine.skill_count()
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"partials/skills_list.html",
|
|
{"skills": skills, "counts": counts},
|
|
)
|
|
|
|
|
|
@router.post("/scan", response_class=HTMLResponse)
|
|
async def trigger_scan(request: Request):
|
|
"""Manually trigger a skill discovery scan."""
|
|
from timmy.skill_discovery import get_skill_discovery_engine
|
|
|
|
engine = get_skill_discovery_engine()
|
|
try:
|
|
discovered = await engine.scan()
|
|
msg = f"Scan complete: {len(discovered)} new skill(s) found."
|
|
except Exception as exc:
|
|
logger.warning("Manual skill scan failed: %s", exc)
|
|
msg = f"Scan failed: {exc}"
|
|
|
|
skills = engine.list_skills(limit=50)
|
|
counts = engine.skill_count()
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"partials/skills_list.html",
|
|
{"skills": skills, "counts": counts, "scan_message": msg},
|
|
)
|