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:
80
backend/app/api/endpoints/session_resolutions.py
Normal file
80
backend/app/api/endpoints/session_resolutions.py
Normal 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,
|
||||||
|
)
|
||||||
@@ -32,6 +32,7 @@ from app.api.endpoints import script_builder
|
|||||||
from app.api.endpoints import beta_feedback
|
from app.api.endpoints import beta_feedback
|
||||||
from app.api.endpoints import session_branches
|
from app.api.endpoints import session_branches
|
||||||
from app.api.endpoints import session_handoffs
|
from app.api.endpoints import session_handoffs
|
||||||
|
from app.api.endpoints import session_resolutions
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ api_router.include_router(onboarding.router)
|
|||||||
api_router.include_router(branding.router)
|
api_router.include_router(branding.router)
|
||||||
api_router.include_router(supporting_data.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_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(ai_sessions.router)
|
||||||
api_router.include_router(flow_proposals.router)
|
api_router.include_router(flow_proposals.router)
|
||||||
api_router.include_router(flowpilot_analytics.router)
|
api_router.include_router(flowpilot_analytics.router)
|
||||||
|
|||||||
62
backend/tests/test_session_resolutions_api.py
Normal file
62
backend/tests/test_session_resolutions_api.py
Normal 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"
|
||||||
Reference in New Issue
Block a user