Files
Timmy-time-dashboard/src/dashboard/routes/skills.py
Alexander Whitestone 92c677f029
Some checks failed
Tests / lint (pull_request) Successful in 13s
Tests / test (pull_request) Failing after 6m25s
feat: add automated skill discovery pipeline
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>
2026-03-22 19:56:35 -04:00

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},
)