""" 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 import os 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() _DEFAULT_TENANT_ID = os.getenv("ORACLE_DEFAULT_TENANT_ID", "tenant_velocity") # ── Helpers ─────────────────────────────────────────────────────────────────── def _pool(request: Request): pool = request.app.state.db_pool if pool is None: raise HTTPException(503, "Database unavailable.") return pool def _tenant_id() -> str: return _DEFAULT_TENANT_ID # ── 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 """, _tenant_id(), ) 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 """, _tenant_id(), 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] = [_tenant_id()] 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, _tenant_id(), ) 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, _tenant_id(), 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] = [_tenant_id()] 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 """, _tenant_id(), 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, _tenant_id(), ) 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, _tenant_id(), ) 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, _tenant_id(), ) 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 """, _tenant_id(), 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"]), }