""" routes_oracle_templates.py ────────────────────────── Oracle Template Catalog API Extends the existing Oracle route surface with template taxonomy and seeding. Endpoints: GET /oracle/template-chapters — list chapters POST /oracle/template-chapters — create a chapter GET /oracle/template-subchapters — list subchapters (optionally filtered) POST /oracle/template-subchapters — create a subchapter GET /oracle/component-templates — list templates (filterable) POST /oracle/component-templates — create a template GET /oracle/component-templates/{id} — get a template POST /oracle/component-templates/{id}/seed — add a seed example GET /oracle/component-templates/{id}/seed — list seed examples for a template POST /oracle/component-templates/synthetic-jobs — trigger a Kimi synthetic job """ from __future__ import annotations import json import logging from typing import Any, Optional from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from pydantic import BaseModel, Field from backend.auth.dependencies import get_current_user logger = logging.getLogger("velocity.oracle_templates") router = APIRouter() # ── Helpers ─────────────────────────────────────────────────────────────────── def _pool(request: Request): pool = request.app.state.db_pool if pool is None: raise HTTPException(503, "Database unavailable.") return pool # ── Models ──────────────────────────────────────────────────────────────────── class ChapterCreate(BaseModel): name: str description: Optional[str] = None sort_order: int = 0 class SubchapterCreate(BaseModel): chapter_id: str name: str description: Optional[str] = None sort_order: int = 0 class TemplateCreate(BaseModel): name: str category: str chapter_id: Optional[str] = None subchapter_id: Optional[str] = None component_type: Optional[str] = None accepted_shapes: list[str] = Field(default_factory=list) json_template: Optional[dict] = None description: Optional[str] = None origin: str = "premade" version: str = "1.0.0" class SeedExampleCreate(BaseModel): title: str example_json: dict quality_notes: Optional[str] = None chapter_id: Optional[str] = None subchapter_id: Optional[str] = None is_canonical: bool = False class SyntheticJobCreate(BaseModel): template_id: str chapter_id: Optional[str] = None subchapter_id: Optional[str] = None model: str = "kimi" requested_count: int = Field(10, ge=1, le=500) # ── Template Chapters ───────────────────────────────────────────────────────── @router.get("/template-chapters", summary="List Oracle template chapters") async def list_template_chapters( request: Request, include_inactive: bool = Query(False), user=Depends(get_current_user), ): pool = _pool(request) async with pool.acquire() as conn: where = "WHERE ch.tenant_id=$1" + ("" if include_inactive else " AND ch.is_active=TRUE") rows = await conn.fetch( f""" SELECT ch.chapter_id, ch.name, ch.description, ch.sort_order, ch.is_active, COUNT(sub.subchapter_id) FILTER (WHERE sub.is_active=TRUE) as subchapter_count, COUNT(t.template_id) as template_count FROM oracle_template_chapters ch LEFT JOIN oracle_template_subchapters sub ON sub.chapter_id = ch.chapter_id LEFT JOIN oracle_component_templates t ON t.chapter_id = ch.chapter_id AND t.status != 'archived' {where} GROUP BY ch.chapter_id ORDER BY ch.sort_order ASC """, user.role, ) return {"chapters": [dict(r) for r in rows]} @router.post("/template-chapters", status_code=status.HTTP_201_CREATED, summary="Create a template chapter") async def create_template_chapter( request: Request, body: ChapterCreate, user=Depends(get_current_user), ): pool = _pool(request) async with pool.acquire() as conn: row = await conn.fetchrow( """ INSERT INTO oracle_template_chapters (tenant_id, name, description, sort_order) VALUES ($1,$2,$3,$4) RETURNING chapter_id, created_at """, user.role, body.name, body.description, body.sort_order, ) return {"chapter_id": str(row["chapter_id"]), "created_at": str(row["created_at"])} # ── Template Subchapters ────────────────────────────────────────────────────── @router.get("/template-subchapters", summary="List Oracle template subchapters") async def list_template_subchapters( request: Request, chapter_id: Optional[str] = Query(None), include_inactive: bool = Query(False), user=Depends(get_current_user), ): pool = _pool(request) async with pool.acquire() as conn: where = "WHERE sub.tenant_id=$1" params: list[Any] = [user.role] idx = 2 if not include_inactive: where += " AND sub.is_active=TRUE" if chapter_id: where += f" AND sub.chapter_id=${idx}"; params.append(chapter_id); idx += 1 rows = await conn.fetch( f""" SELECT sub.subchapter_id, sub.chapter_id, ch.name as chapter_name, sub.name, sub.description, sub.sort_order, sub.is_active, COUNT(t.template_id) as template_count FROM oracle_template_subchapters sub JOIN oracle_template_chapters ch ON ch.chapter_id = sub.chapter_id LEFT JOIN oracle_component_templates t ON t.subchapter_id = sub.subchapter_id AND t.status != 'archived' {where} GROUP BY sub.subchapter_id, ch.name ORDER BY sub.chapter_id, sub.sort_order ASC """, *params, ) return {"subchapters": [dict(r) for r in rows]} @router.post("/template-subchapters", status_code=status.HTTP_201_CREATED, summary="Create a template subchapter") async def create_template_subchapter( request: Request, body: SubchapterCreate, user=Depends(get_current_user), ): pool = _pool(request) async with pool.acquire() as conn: # Verify chapter exists and belongs to tenant ch_exists = await conn.fetchval( "SELECT 1 FROM oracle_template_chapters WHERE chapter_id=$1 AND tenant_id=$2", body.chapter_id, user.role, ) if not ch_exists: raise HTTPException(404, "Chapter not found") row = await conn.fetchrow( """ INSERT INTO oracle_template_subchapters (chapter_id, tenant_id, name, description, sort_order) VALUES ($1,$2,$3,$4,$5) RETURNING subchapter_id, created_at """, body.chapter_id, user.role, body.name, body.description, body.sort_order, ) return {"subchapter_id": str(row["subchapter_id"]), "created_at": str(row["created_at"])} # ── Component Templates ─────────────────────────────────────────────────────── @router.get("/component-templates", summary="List Oracle component templates") async def list_component_templates( request: Request, chapter_id: Optional[str] = Query(None), subchapter_id: Optional[str] = Query(None), status_filter: Optional[str] = Query(None, alias="status"), search: Optional[str] = Query(None), limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0), user=Depends(get_current_user), ): pool = _pool(request) where = "WHERE t.tenant_id=$1" params: list[Any] = [user.role] idx = 2 if chapter_id: where += f" AND t.chapter_id=${idx}"; params.append(chapter_id); idx += 1 if subchapter_id: where += f" AND t.subchapter_id=${idx}"; params.append(subchapter_id); idx += 1 if status_filter: where += f" AND t.status=${idx}"; params.append(status_filter); idx += 1 if search: where += f" AND (t.name ILIKE ${idx} OR t.description ILIKE ${idx})" params.append(f"%{search}%"); idx += 1 async with pool.acquire() as conn: rows = await conn.fetch( f""" SELECT t.template_id, t.name, t.category, t.status, t.origin, t.version, t.accepted_shapes, t.use_count, t.chapter_id, t.subchapter_id, t.description, ch.name as chapter_name, sub.name as subchapter_name, t.created_at, t.updated_at FROM oracle_component_templates t LEFT JOIN oracle_template_chapters ch ON ch.chapter_id = t.chapter_id LEFT JOIN oracle_template_subchapters sub ON sub.subchapter_id = t.subchapter_id {where} ORDER BY t.updated_at DESC LIMIT ${idx} OFFSET ${idx+1} """, *params, limit, offset, ) total = await conn.fetchval( f"SELECT COUNT(*) FROM oracle_component_templates t {where}", *params, ) return {"total": total, "limit": limit, "offset": offset, "templates": [dict(r) for r in rows]} @router.post("/component-templates", status_code=status.HTTP_201_CREATED, summary="Create a component template") async def create_component_template( request: Request, body: TemplateCreate, user=Depends(get_current_user), ): pool = _pool(request) async with pool.acquire() as conn: row = await conn.fetchrow( """ INSERT INTO oracle_component_templates ( tenant_id, name, category, chapter_id, subchapter_id, accepted_shapes, json_template, description, origin, version, status ) VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8,$9,$10,'draft') RETURNING template_id, created_at """, user.role, body.name, body.category, body.chapter_id, body.subchapter_id, body.accepted_shapes, json.dumps(body.json_template) if body.json_template else None, body.description, body.origin, body.version, ) return {"template_id": str(row["template_id"]), "created_at": str(row["created_at"])} @router.get("/component-templates/{template_id}", summary="Get a component template") async def get_component_template( template_id: str, request: Request, user=Depends(get_current_user), ): pool = _pool(request) async with pool.acquire() as conn: row = await conn.fetchrow( """ SELECT t.*, ch.name as chapter_name, sub.name as subchapter_name FROM oracle_component_templates t LEFT JOIN oracle_template_chapters ch ON ch.chapter_id = t.chapter_id LEFT JOIN oracle_template_subchapters sub ON sub.subchapter_id = t.subchapter_id WHERE t.template_id=$1 AND t.tenant_id=$2 """, template_id, user.role, ) if not row: raise HTTPException(404, "Template not found") return dict(row) # ── Seed Examples ───────────────────────────────────────────────────────────── @router.post("/component-templates/{template_id}/seed", status_code=status.HTTP_201_CREATED, summary="Add a seed example to a template") async def add_seed_example( template_id: str, request: Request, body: SeedExampleCreate, user=Depends(get_current_user), ): pool = _pool(request) async with pool.acquire() as conn: exists = await conn.fetchval( "SELECT 1 FROM oracle_component_templates WHERE template_id=$1 AND tenant_id=$2", template_id, user.role, ) if not exists: raise HTTPException(404, "Template not found") row = await conn.fetchrow( """ INSERT INTO oracle_template_seed_examples ( template_id, chapter_id, subchapter_id, title, example_json, quality_notes, is_canonical ) VALUES ($1,$2,$3,$4,$5::jsonb,$6,$7) RETURNING example_id, created_at """, template_id, body.chapter_id, body.subchapter_id, body.title, json.dumps(body.example_json), body.quality_notes, body.is_canonical, ) return {"example_id": str(row["example_id"]), "created_at": str(row["created_at"])} @router.get("/component-templates/{template_id}/seed", summary="List seed examples for a template") async def list_seed_examples( template_id: str, request: Request, user=Depends(get_current_user), ): pool = _pool(request) async with pool.acquire() as conn: rows = await conn.fetch( """ SELECT example_id, title, example_json, quality_notes, is_canonical, created_at FROM oracle_template_seed_examples WHERE template_id=$1 ORDER BY is_canonical DESC, created_at ASC """, template_id, ) return {"examples": [dict(r) for r in rows]} # ── Synthetic Jobs ──────────────────────────────────────────────────────────── @router.post("/component-templates/synthetic-jobs", status_code=status.HTTP_201_CREATED, summary="Trigger a Kimi synthetic data generation job") async def trigger_synthetic_job( request: Request, body: SyntheticJobCreate, user=Depends(get_current_user), ): """ Queues a Kimi synthetic data expansion job for a template. The job will be picked up by the background synthetic generation worker. """ pool = _pool(request) async with pool.acquire() as conn: exists = await conn.fetchval( "SELECT 1 FROM oracle_component_templates WHERE template_id=$1 AND tenant_id=$2", body.template_id, user.role, ) if not exists: raise HTTPException(404, "Template not found") row = await conn.fetchrow( """ INSERT INTO oracle_synthetic_generation_jobs ( tenant_id, template_id, chapter_id, subchapter_id, model, requested_count, created_by ) VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING job_id, status, created_at """, user.role, body.template_id, body.chapter_id, body.subchapter_id, body.model, body.requested_count, user.user_id, ) logger.info( "Synthetic job queued: %s for template %s (%d examples)", row["job_id"], body.template_id, body.requested_count, ) return { "job_id": str(row["job_id"]), "status": row["status"], "created_at": str(row["created_at"]), }