diff --git a/backend/app/api/endpoints/session_resolutions.py b/backend/app/api/endpoints/session_resolutions.py new file mode 100644 index 00000000..3a13dc9b --- /dev/null +++ b/backend/app/api/endpoints/session_resolutions.py @@ -0,0 +1,80 @@ +"""Resolution output endpoints.""" +import logging +from typing import Annotated +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.deps import get_current_active_user, get_db +from app.models.user import User +from app.models.session_resolution_output import SessionResolutionOutput +from app.services.resolution_output_generator import ResolutionOutputGenerator +from app.schemas.session_resolution import ( + ResolutionOutputResponse, + ResolutionOutputEditRequest, + ResolutionOutputPushRequest, + ResolutionOutputPushResponse, + AllResolutionOutputsResponse, +) + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/ai-sessions/{session_id}", tags=["session-resolutions"]) + + +@router.get("/outputs", response_model=AllResolutionOutputsResponse) +async def get_outputs( + session_id: UUID, + current_user: Annotated[User, Depends(get_current_active_user)], + db: Annotated[AsyncSession, Depends(get_db)], +) -> AllResolutionOutputsResponse: + result = await db.execute( + select(SessionResolutionOutput) + .where(SessionResolutionOutput.session_id == session_id) + .order_by(SessionResolutionOutput.output_type) + ) + outputs = result.scalars().all() + return AllResolutionOutputsResponse( + outputs=[ResolutionOutputResponse.model_validate(o) for o in outputs] + ) + + +@router.patch("/outputs/{output_id}", response_model=ResolutionOutputResponse) +async def edit_output( + session_id: UUID, + output_id: UUID, + body: ResolutionOutputEditRequest, + current_user: Annotated[User, Depends(get_current_active_user)], + db: Annotated[AsyncSession, Depends(get_db)], +) -> ResolutionOutputResponse: + gen = ResolutionOutputGenerator(db) + try: + output = await gen.edit_output(output_id, body.edited_content) + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + await db.commit() + return ResolutionOutputResponse.model_validate(output) + + +@router.post("/outputs/{output_id}/push", response_model=ResolutionOutputPushResponse) +async def push_output( + session_id: UUID, + output_id: UUID, + body: ResolutionOutputPushRequest, + current_user: Annotated[User, Depends(get_current_active_user)], + db: Annotated[AsyncSession, Depends(get_db)], +) -> ResolutionOutputPushResponse: + gen = ResolutionOutputGenerator(db) + try: + output = await gen.push_output(output_id, body.destination) + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + await db.commit() + return ResolutionOutputPushResponse( + output_id=output.id, + status=output.status, + pushed_to=output.pushed_to or body.destination, + pushed_reference=output.pushed_reference, + ) diff --git a/backend/app/api/router.py b/backend/app/api/router.py index deafed4a..d588afc9 100644 --- a/backend/app/api/router.py +++ b/backend/app/api/router.py @@ -32,6 +32,7 @@ from app.api.endpoints import script_builder from app.api.endpoints import beta_feedback from app.api.endpoints import session_branches from app.api.endpoints import session_handoffs +from app.api.endpoints import session_resolutions api_router = APIRouter() @@ -79,6 +80,7 @@ api_router.include_router(onboarding.router) api_router.include_router(branding.router) api_router.include_router(supporting_data.router) api_router.include_router(session_handoffs.queue_router) # Must be before ai_sessions to avoid /{session_id} conflict +api_router.include_router(session_resolutions.router) # Must be before ai_sessions to avoid /{session_id} conflict api_router.include_router(ai_sessions.router) api_router.include_router(flow_proposals.router) api_router.include_router(flowpilot_analytics.router) diff --git a/backend/tests/test_session_resolutions_api.py b/backend/tests/test_session_resolutions_api.py new file mode 100644 index 00000000..deac8c70 --- /dev/null +++ b/backend/tests/test_session_resolutions_api.py @@ -0,0 +1,62 @@ +"""API tests for resolution output endpoints.""" +import pytest +from unittest.mock import patch +from httpx import AsyncClient + +from app.models.ai_session import AISession +from app.models.session_resolution_output import SessionResolutionOutput + + +@pytest.mark.asyncio +async def test_get_outputs_empty(client: AsyncClient, test_user, auth_headers, test_db): + session = AISession( + user_id=test_user["user_data"]["id"], + account_id=test_user["user_data"]["account_id"], + session_type="guided", + intake_type="free_text", + intake_content={"text": "test"}, + status="active", + confidence_tier="guided", + conversation_messages=[], + ) + test_db.add(session) + await test_db.commit() + + resp = await client.get(f"/api/v1/ai-sessions/{session.id}/outputs", headers=auth_headers) + assert resp.status_code == 200 + assert resp.json()["outputs"] == [] + + +@pytest.mark.asyncio +async def test_edit_output_api(client: AsyncClient, test_user, auth_headers, test_db): + session = AISession( + user_id=test_user["user_data"]["id"], + account_id=test_user["user_data"]["account_id"], + session_type="guided", + intake_type="free_text", + intake_content={"text": "test"}, + status="resolved", + confidence_tier="guided", + conversation_messages=[], + resolution_summary="Fixed", + ) + test_db.add(session) + await test_db.flush() + + output = SessionResolutionOutput( + session_id=session.id, + output_type="psa_ticket_notes", + generated_content="Original", + status="draft", + generated_by_model="claude-sonnet-4-6", + ) + test_db.add(output) + await test_db.commit() + + resp = await client.patch( + f"/api/v1/ai-sessions/{session.id}/outputs/{output.id}", + headers=auth_headers, + json={"edited_content": "My edited version"}, + ) + assert resp.status_code == 200 + assert resp.json()["edited_content"] == "My edited version"