feat: add resolution output API endpoints

Adds GET /outputs, PATCH /outputs/{id}, and POST /outputs/{id}/push
endpoints under /ai-sessions/{session_id}/, plus integration tests.
Router registered before ai_sessions to avoid path conflict.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-24 08:47:09 +00:00
parent 5f3169bad4
commit a928901a2f
3 changed files with 144 additions and 0 deletions

View File

@@ -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,
)

View File

@@ -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)

View File

@@ -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"