Files
Meeting-AI-Orchestration-Demo/backend/app/api/routes.py
2026-06-27 13:50:11 +03:00

204 lines
7.4 KiB
Python

from datetime import datetime
from typing import Annotated
from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, Request, UploadFile, WebSocket, WebSocketDisconnect
from sqlalchemy.orm import Session
from app.api.dependencies import context_builder, event_publisher, memory_manager, orchestrator, tool_registry
from app.domain.schemas import (
ActionStatus,
DashboardRead,
EventType,
NoteCreate,
NoteRead,
PendingActionRead,
PendingActionUpdate,
ReservationCreate,
ReservationPatch,
ReservationRead,
TaskCreate,
TaskPatch,
TaskRead,
TranscriptChunk,
TranscriptionJobRead,
)
from app.executor.action_executor import ActionExecutor
from app.infrastructure.database import Note, PendingAction, Reservation, Task, get_session
from app.infrastructure.soniox import SonioxTranscriptionService, jobs
router = APIRouter()
DbSession = Annotated[Session, Depends(get_session)]
@router.post("/tasks", response_model=TaskRead, operation_id="create_task", summary="Create task")
def create_task(payload: TaskCreate, session: DbSession) -> Task:
task = Task(**payload.model_dump())
session.add(task)
session.commit()
session.refresh(task)
return task
@router.patch("/tasks/{task_id}", response_model=TaskRead, operation_id="update_task", summary="Update task")
def update_task(task_id: int, payload: TaskPatch, session: DbSession) -> Task:
task = session.get(Task, task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
for key, value in payload.model_dump(exclude_unset=True).items():
setattr(task, key, value)
session.commit()
session.refresh(task)
return task
@router.post("/notes", response_model=NoteRead, operation_id="create_note", summary="Create note")
def create_note(payload: NoteCreate, session: DbSession) -> Note:
note = Note(**payload.model_dump())
session.add(note)
session.commit()
session.refresh(note)
return note
@router.post("/reservations", response_model=ReservationRead, operation_id="create_reservation", summary="Create reservation")
def create_reservation(payload: ReservationCreate, session: DbSession) -> Reservation:
reservation = Reservation(**payload.model_dump())
session.add(reservation)
session.commit()
session.refresh(reservation)
return reservation
@router.patch(
"/reservations/{reservation_id}",
response_model=ReservationRead,
operation_id="update_reservation",
summary="Update reservation",
)
def update_reservation(reservation_id: int, payload: ReservationPatch, session: DbSession) -> Reservation:
reservation = session.get(Reservation, reservation_id)
if not reservation:
raise HTTPException(status_code=404, detail="Reservation not found")
for key, value in payload.model_dump(exclude_unset=True).items():
setattr(reservation, key, value)
session.commit()
session.refresh(reservation)
return reservation
@router.get("/pending-actions", response_model=list[PendingActionRead])
def list_pending_actions(session: DbSession) -> list[PendingAction]:
return session.query(PendingAction).order_by(PendingAction.created_at.desc()).all()
@router.patch("/pending-actions/{action_id}", response_model=PendingActionRead)
def edit_pending_action(action_id: int, payload: PendingActionUpdate, session: DbSession) -> PendingAction:
action = session.get(PendingAction, action_id)
if not action:
raise HTTPException(status_code=404, detail="Pending action not found")
if payload.payload is not None:
action.payload = payload.payload
session.commit()
session.refresh(action)
return action
@router.post("/pending-actions/{action_id}/approve", response_model=PendingActionRead)
async def approve_action(action_id: int, session: DbSession, request: Request) -> PendingAction:
action = session.get(PendingAction, action_id)
if not action:
raise HTTPException(status_code=404, detail="Pending action not found")
action.status = ActionStatus.APPROVED
session.commit()
session.refresh(action)
generated = await ActionExecutor(tool_registry(request)).execute(session, action)
event_publisher().publish(
EventType.API_CALL_GENERATED,
f"approve-{action_id}",
{"action_id": action_id, "status": generated.status, "api_call": generated.execution_result},
)
return generated
@router.post("/pending-actions/{action_id}/reject", response_model=PendingActionRead)
def reject_action(action_id: int, session: DbSession) -> PendingAction:
action = session.get(PendingAction, action_id)
if not action:
raise HTTPException(status_code=404, detail="Pending action not found")
action.status = ActionStatus.REJECTED
session.commit()
session.refresh(action)
event_publisher().publish(EventType.ACTION_REJECTED, f"reject-{action_id}", {"action_id": action_id})
return action
@router.get("/dashboard", response_model=DashboardRead)
def dashboard(session: DbSession):
return DashboardRead(
transcript=context_builder().transcript(),
context=context_builder().snapshot(
{
"tasks": session.query(Task).count(),
"notes": session.query(Note).count(),
"reservations": session.query(Reservation).count(),
}
),
memories=memory_manager().retrieve("wedding venue flowers tasks", limit=6),
pending_actions=session.query(PendingAction).order_by(PendingAction.created_at.desc()).all(),
tasks=session.query(Task).order_by(Task.created_at.desc()).all(),
notes=session.query(Note).order_by(Note.created_at.desc()).all(),
reservations=session.query(Reservation).order_by(Reservation.created_at.desc()).all(),
events=event_publisher().recent(),
)
@router.get("/tools")
def tools(request: Request):
return [tool.model_dump() for tool in tool_registry(request).list()]
@router.post("/transcriptions", response_model=TranscriptionJobRead, status_code=202)
async def create_transcription_job(
request: Request,
background_tasks: BackgroundTasks,
audio: UploadFile = File(...),
) -> TranscriptionJobRead:
payload = await audio.read()
if not payload:
raise HTTPException(status_code=400, detail="No audio file provided.")
job = jobs.create()
background_tasks.add_task(
SonioxTranscriptionService().process_upload,
job.job_id,
payload,
audio.filename or "recording.webm",
request,
)
return job
@router.get("/transcriptions/{job_id}", response_model=TranscriptionJobRead)
def get_transcription_job(job_id: str) -> TranscriptionJobRead:
job = jobs.get(job_id)
if not job:
raise HTTPException(status_code=404, detail="Transcription job not found.")
return job
@router.websocket("/ws/transcript")
async def transcript_socket(websocket: WebSocket):
await websocket.accept()
db = next(get_session())
try:
while True:
raw = await websocket.receive_json()
if "timestamp" not in raw:
raw["timestamp"] = datetime.utcnow().isoformat()
chunk = TranscriptChunk.model_validate(raw)
action = await orchestrator(websocket).ingest(db, chunk) # type: ignore[arg-type]
await websocket.send_json({"ok": True, "pending_action_id": action.id if action else None})
except WebSocketDisconnect:
return
finally:
db.close()