import json from collections.abc import AsyncIterator from typing import Any from fastapi import APIRouter, Response from fastapi.responses import StreamingResponse from src.api.dependencies import DbSessionDep, GuestSessionHeaderDep, OptionalCurrentUserDep from src.api.errors import error_response from src.models.schemas import ( DraftCheckRequest, DraftCheckResponse, DraftExportRequest, DraftPatchRequest, DraftPatchResponse, DraftPlanningResult, DraftRequest, DraftResponse, ) from src.services.auth import AuthServiceError from src.services.drafting import ( check_draft_grounding, export_docx, generate_draft, generate_draft_stream, patch_draft_selection, plan_draft, ) from src.services.guest_quota import ensure_guest_session_id from src.services.retrieval_access import RetrievalAccessCoordinator from src.services.usage_tracking import UsageTrackingService router = APIRouter(prefix="/drafts", tags=["drafts"]) @router.post("/plan", response_model=DraftPlanningResult) async def plan_draft_request(request: DraftRequest) -> DraftPlanningResult | dict: try: return plan_draft(request) except Exception as e: return error_response( status_code=500, code="draft_planning_failed", message=str(e), ) @router.post("", response_model=DraftResponse) async def create_draft( request: DraftRequest, response: Response, db: DbSessionDep, current_user: OptionalCurrentUserDep, guest_session_id: GuestSessionHeaderDep = None, ) -> DraftResponse | dict: resolved_guest_session_id: str | None = None try: if current_user is None: resolved_guest_session_id = ensure_guest_session_id(guest_session_id) response.headers["X-Guest-Session-Id"] = resolved_guest_session_id coordinator = RetrievalAccessCoordinator(db) draft_response = generate_draft( request=request, coordinator=coordinator, current_user=current_user, guest_session_id=resolved_guest_session_id or guest_session_id, ) if current_user is not None and draft_response.html: UsageTrackingService.record_event_safe( event_type="draft_created", user_id=current_user.id, source="draft", metadata={"template_key": request.template_key.value}, ) return draft_response except AuthServiceError as exc: return error_response( status_code=exc.status_code, code=exc.code, message=exc.message, ) except Exception as e: return error_response( status_code=500, code="draft_generation_failed", message=str(e), ) @router.post("/stream") async def create_draft_stream( request: DraftRequest, response: Response, db: DbSessionDep, current_user: OptionalCurrentUserDep, guest_session_id: GuestSessionHeaderDep = None, ) -> StreamingResponse: resolved_guest_session_id: str | None = None try: if current_user is None: resolved_guest_session_id = ensure_guest_session_id(guest_session_id) coordinator = RetrievalAccessCoordinator(db) async def event_generator() -> AsyncIterator[str]: tracked = False try: async for frame in generate_draft_stream( request=request, coordinator=coordinator, current_user=current_user, guest_session_id=resolved_guest_session_id or guest_session_id, ): if ( not tracked and isinstance(frame, dict) and frame.get("event") == "done" and isinstance(frame.get("data"), dict) and frame["data"].get("html") and current_user is not None ): UsageTrackingService.record_event_safe( event_type="draft_created", user_id=current_user.id, source="draft_stream", metadata={"template_key": request.template_key.value}, ) tracked = True yield f"event: {frame['event']}\ndata: {json.dumps(frame['data'], ensure_ascii=False)}\n\n" except Exception as exc: yield f"event: error\ndata: {json.dumps({'message': str(exc)})}\n\n" headers = {"Cache-Control": "no-cache", "X-Accel-Buffering": "no"} if resolved_guest_session_id: headers["X-Guest-Session-Id"] = resolved_guest_session_id return StreamingResponse( event_generator(), media_type="text/event-stream", headers=headers, ) except AuthServiceError as exc: return error_response(status_code=exc.status_code, code=exc.code, message=exc.message) except Exception as e: return error_response(status_code=500, code="draft_stream_failed", message=str(e)) @router.post("/check", response_model=DraftCheckResponse) async def check_draft(request: DraftCheckRequest) -> DraftCheckResponse | dict: try: checked_html, uncertain_count, issues = check_draft_grounding( request.html, request.requirement, request.citations, request.template_key.value ) return DraftCheckResponse(checked_html=checked_html, uncertain_count=uncertain_count, issues=issues) except Exception as e: return error_response(status_code=500, code="draft_check_failed", message=str(e)) @router.post("/patch", response_model=DraftPatchResponse) async def patch_draft(request: DraftPatchRequest) -> DraftPatchResponse | dict: try: replacement = patch_draft_selection(request.selected_html, request.instruction) return DraftPatchResponse(replacement_html=replacement) except Exception as e: return error_response(status_code=500, code="draft_patch_failed", message=str(e)) @router.post("/export.docx") async def export_draft_docx( request: DraftExportRequest, current_user: OptionalCurrentUserDep, ) -> Any: try: buffer = export_docx(request) headers = { "Content-Disposition": "attachment; filename=draft_export.docx" } return StreamingResponse( buffer, media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", headers=headers ) except Exception as e: return error_response( status_code=500, code="docx_export_failed", message=str(e), )