From 29e304779208b7a00ff462fd5e8b61c79a69c9c4 Mon Sep 17 00:00:00 2001 From: nekenny Date: Fri, 3 Apr 2026 22:25:52 +0500 Subject: [PATCH] feat: protocol and template API endpoints, auto-pipeline on upload Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/api/protocols.py | 103 +++++++++++++++++++++++++++++ backend/app/api/recordings.py | 2 + backend/app/api/templates.py | 89 +++++++++++++++++++++++++ backend/app/main.py | 19 ++++++ backend/app/tasks/transcription.py | 3 + 5 files changed, 216 insertions(+) create mode 100644 backend/app/api/protocols.py create mode 100644 backend/app/api/templates.py diff --git a/backend/app/api/protocols.py b/backend/app/api/protocols.py new file mode 100644 index 0000000..038624e --- /dev/null +++ b/backend/app/api/protocols.py @@ -0,0 +1,103 @@ +import uuid + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from app.api.deps import get_current_user +from app.database import get_db +from app.models.protocol import Protocol +from app.models.user import User +from app.schemas.protocol import ProtocolList, ProtocolRegenerate, ProtocolResponse +from app.tasks.protocol import generate_protocol + +router = APIRouter(prefix="/api/protocols", tags=["protocols"]) + + +@router.get("", response_model=ProtocolList) +async def list_protocols( + skip: int = 0, + limit: int = 50, + db: AsyncSession = Depends(get_db), + user: User = Depends(get_current_user), +): + total = (await db.execute(select(func.count(Protocol.id)))).scalar() + result = await db.execute( + select(Protocol) + .order_by(Protocol.created_at.desc()) + .offset(skip) + .limit(limit) + ) + items = list(result.scalars().all()) + return ProtocolList(items=items, total=total) + + +@router.get("/{protocol_id}", response_model=ProtocolResponse) +async def get_protocol( + protocol_id: uuid.UUID, + db: AsyncSession = Depends(get_db), + user: User = Depends(get_current_user), +): + result = await db.execute( + select(Protocol).where(Protocol.id == protocol_id) + ) + protocol = result.scalar_one_or_none() + if not protocol: + raise HTTPException(404, "Protocol not found") + return protocol + + +@router.post("/{protocol_id}/regenerate", status_code=202) +async def regenerate_protocol( + protocol_id: uuid.UUID, + data: ProtocolRegenerate, + db: AsyncSession = Depends(get_db), + user: User = Depends(get_current_user), +): + result = await db.execute( + select(Protocol).where(Protocol.id == protocol_id) + ) + protocol = result.scalar_one_or_none() + if not protocol: + raise HTTPException(404, "Protocol not found") + + template_id = str(data.template_id) if data.template_id else None + generate_protocol.delay(str(protocol.transcription_id), template_id) + return {"message": "Regeneration started"} + + +@router.put("/{protocol_id}", response_model=ProtocolResponse) +async def update_protocol_content( + protocol_id: uuid.UUID, + content: dict, + db: AsyncSession = Depends(get_db), + user: User = Depends(get_current_user), +): + result = await db.execute( + select(Protocol).where(Protocol.id == protocol_id) + ) + protocol = result.scalar_one_or_none() + if not protocol: + raise HTTPException(404, "Protocol not found") + + protocol.content = content + await db.commit() + await db.refresh(protocol) + return protocol + + +@router.delete("/{protocol_id}", status_code=204) +async def delete_protocol( + protocol_id: uuid.UUID, + db: AsyncSession = Depends(get_db), + user: User = Depends(get_current_user), +): + result = await db.execute( + select(Protocol).where(Protocol.id == protocol_id) + ) + protocol = result.scalar_one_or_none() + if not protocol: + raise HTTPException(404, "Protocol not found") + await db.delete(protocol) + await db.commit() diff --git a/backend/app/api/recordings.py b/backend/app/api/recordings.py index 14e1d01..81c6c6b 100644 --- a/backend/app/api/recordings.py +++ b/backend/app/api/recordings.py @@ -19,6 +19,7 @@ from app.services.recording import ( get_recordings, get_upload_path, ) +from app.tasks.transcription import transcribe_recording router = APIRouter(prefix="/api/recordings", tags=["recordings"]) @@ -57,6 +58,7 @@ async def upload_recording( source=RecordingSource.web, duration=duration, ) + transcribe_recording.delay(str(recording.id)) return recording diff --git a/backend/app/api/templates.py b/backend/app/api/templates.py new file mode 100644 index 0000000..1b9caa0 --- /dev/null +++ b/backend/app/api/templates.py @@ -0,0 +1,89 @@ +import uuid + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.deps import get_current_user +from app.database import get_db +from app.models.prompt_template import PromptTemplate +from app.models.user import User +from app.schemas.prompt_template import TemplateCreate, TemplateResponse, TemplateUpdate + +router = APIRouter(prefix="/api/templates", tags=["templates"]) + + +@router.get("", response_model=list[TemplateResponse]) +async def list_templates( + db: AsyncSession = Depends(get_db), + user: User = Depends(get_current_user), +): + result = await db.execute( + select(PromptTemplate).order_by(PromptTemplate.created_at) + ) + return list(result.scalars().all()) + + +@router.post("", response_model=TemplateResponse, status_code=201) +async def create_template( + data: TemplateCreate, + db: AsyncSession = Depends(get_db), + user: User = Depends(get_current_user), +): + template = PromptTemplate(**data.model_dump()) + db.add(template) + await db.commit() + await db.refresh(template) + return template + + +@router.get("/{template_id}", response_model=TemplateResponse) +async def get_template( + template_id: uuid.UUID, + db: AsyncSession = Depends(get_db), + user: User = Depends(get_current_user), +): + result = await db.execute( + select(PromptTemplate).where(PromptTemplate.id == template_id) + ) + template = result.scalar_one_or_none() + if not template: + raise HTTPException(404, "Template not found") + return template + + +@router.put("/{template_id}", response_model=TemplateResponse) +async def update_template( + template_id: uuid.UUID, + data: TemplateUpdate, + db: AsyncSession = Depends(get_db), + user: User = Depends(get_current_user), +): + result = await db.execute( + select(PromptTemplate).where(PromptTemplate.id == template_id) + ) + template = result.scalar_one_or_none() + if not template: + raise HTTPException(404, "Template not found") + + for field, value in data.model_dump(exclude_unset=True).items(): + setattr(template, field, value) + await db.commit() + await db.refresh(template) + return template + + +@router.delete("/{template_id}", status_code=204) +async def delete_template( + template_id: uuid.UUID, + db: AsyncSession = Depends(get_db), + user: User = Depends(get_current_user), +): + result = await db.execute( + select(PromptTemplate).where(PromptTemplate.id == template_id) + ) + template = result.scalar_one_or_none() + if not template: + raise HTTPException(404, "Template not found") + await db.delete(template) + await db.commit() diff --git a/backend/app/main.py b/backend/app/main.py index 31dcf94..76197c3 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,19 +1,38 @@ from contextlib import asynccontextmanager from fastapi import FastAPI +from sqlalchemy import select from app.api.auth import router as auth_router +from app.api.protocols import router as protocols_router from app.api.recordings import router as recordings_router +from app.api.templates import router as templates_router +from app.database import async_session +from app.models.prompt_template import PromptTemplate +from app.prompts.defaults import DEFAULT_TEMPLATES + + +async def seed_default_templates(): + async with async_session() as db: + result = await db.execute(select(PromptTemplate)) + if result.scalars().first() is not None: + return + for tpl in DEFAULT_TEMPLATES: + db.add(PromptTemplate(**tpl)) + await db.commit() @asynccontextmanager async def lifespan(app: FastAPI): + await seed_default_templates() yield app = FastAPI(title="Meeting Protocol Service", lifespan=lifespan) app.include_router(auth_router) app.include_router(recordings_router) +app.include_router(protocols_router) +app.include_router(templates_router) @app.get("/api/health") diff --git a/backend/app/tasks/transcription.py b/backend/app/tasks/transcription.py index 1ca706d..1cc2b19 100644 --- a/backend/app/tasks/transcription.py +++ b/backend/app/tasks/transcription.py @@ -61,6 +61,9 @@ def transcribe_recording(self, recording_id: str): db.commit() db.refresh(transcription) + from app.tasks.protocol import generate_protocol + generate_protocol.delay(str(transcription.id)) + return {"transcription_id": str(transcription.id), "status": "done"} except Exception as e: