feat: add AI suggestion audit trail endpoints
Create/list/resolve endpoints for tracking AI-applied changes to flows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
79
backend/app/api/endpoints/ai_suggestions.py
Normal file
79
backend/app/api/endpoints/ai_suggestions.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
"""AI Suggestion audit trail endpoints."""
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from app.api.deps import get_current_active_user, get_db
|
||||||
|
from app.models.user import User
|
||||||
|
from app.models.ai_suggestion import AISuggestion
|
||||||
|
from app.schemas.ai_suggestion import (
|
||||||
|
AISuggestionCreate,
|
||||||
|
AISuggestionResponse,
|
||||||
|
AISuggestionResolve,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/ai/suggestions", tags=["ai-suggestions"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tree/{tree_id}", response_model=list[AISuggestionResponse])
|
||||||
|
async def list_suggestions(
|
||||||
|
tree_id: UUID,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
):
|
||||||
|
"""List all suggestions for a flow, filtered to current user."""
|
||||||
|
result = await db.execute(
|
||||||
|
select(AISuggestion)
|
||||||
|
.where(AISuggestion.tree_id == tree_id, AISuggestion.user_id == current_user.id)
|
||||||
|
.order_by(AISuggestion.created_at.desc())
|
||||||
|
)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=AISuggestionResponse, status_code=201)
|
||||||
|
async def create_suggestion(
|
||||||
|
data: AISuggestionCreate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
):
|
||||||
|
"""Record a new AI suggestion."""
|
||||||
|
suggestion = AISuggestion(
|
||||||
|
tree_id=data.tree_id,
|
||||||
|
user_id=current_user.id,
|
||||||
|
session_id=data.session_id,
|
||||||
|
action_type=data.action_type,
|
||||||
|
target_node_id=data.target_node_id,
|
||||||
|
changes_json=data.changes_json,
|
||||||
|
)
|
||||||
|
db.add(suggestion)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(suggestion)
|
||||||
|
return suggestion
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{suggestion_id}", response_model=AISuggestionResponse)
|
||||||
|
async def resolve_suggestion(
|
||||||
|
suggestion_id: UUID,
|
||||||
|
data: AISuggestionResolve,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
):
|
||||||
|
"""Accept or dismiss a suggestion."""
|
||||||
|
result = await db.execute(
|
||||||
|
select(AISuggestion).where(
|
||||||
|
AISuggestion.id == suggestion_id,
|
||||||
|
AISuggestion.user_id == current_user.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
suggestion = result.scalar_one_or_none()
|
||||||
|
if not suggestion:
|
||||||
|
raise HTTPException(status_code=404, detail="Suggestion not found")
|
||||||
|
|
||||||
|
suggestion.status = data.status
|
||||||
|
suggestion.resolved_at = datetime.now(timezone.utc)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(suggestion)
|
||||||
|
return suggestion
|
||||||
@@ -13,6 +13,7 @@ from app.api.endpoints import assistant_chat
|
|||||||
from app.api.endpoints import survey
|
from app.api.endpoints import survey
|
||||||
from app.api.endpoints import admin_survey
|
from app.api.endpoints import admin_survey
|
||||||
from app.api.endpoints import tree_transfer
|
from app.api.endpoints import tree_transfer
|
||||||
|
from app.api.endpoints import ai_suggestions
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
|
|
||||||
@@ -50,3 +51,4 @@ api_router.include_router(assistant_chat.router)
|
|||||||
api_router.include_router(survey.router)
|
api_router.include_router(survey.router)
|
||||||
api_router.include_router(admin_survey.router)
|
api_router.include_router(admin_survey.router)
|
||||||
api_router.include_router(tree_transfer.router)
|
api_router.include_router(tree_transfer.router)
|
||||||
|
api_router.include_router(ai_suggestions.router)
|
||||||
|
|||||||
33
backend/app/schemas/ai_suggestion.py
Normal file
33
backend/app/schemas/ai_suggestion.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"""Schemas for AI suggestion audit trail."""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class AISuggestionCreate(BaseModel):
|
||||||
|
tree_id: UUID
|
||||||
|
session_id: Optional[UUID] = None
|
||||||
|
action_type: str
|
||||||
|
target_node_id: Optional[str] = None
|
||||||
|
changes_json: dict = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class AISuggestionResponse(BaseModel):
|
||||||
|
id: UUID
|
||||||
|
tree_id: UUID
|
||||||
|
user_id: UUID
|
||||||
|
session_id: Optional[UUID]
|
||||||
|
action_type: str
|
||||||
|
target_node_id: Optional[str]
|
||||||
|
changes_json: dict
|
||||||
|
status: str
|
||||||
|
created_at: datetime
|
||||||
|
resolved_at: Optional[datetime]
|
||||||
|
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
|
class AISuggestionResolve(BaseModel):
|
||||||
|
status: str = Field(..., pattern="^(accepted|dismissed)$")
|
||||||
41
backend/tests/test_ai_suggestions.py
Normal file
41
backend/tests/test_ai_suggestions.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
"""Tests for AI suggestion endpoints."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_and_list_suggestions(client, auth_headers, test_tree):
|
||||||
|
"""Can create and list suggestions for a tree."""
|
||||||
|
tree_id = test_tree["id"]
|
||||||
|
|
||||||
|
# Create suggestion
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/v1/ai/suggestions",
|
||||||
|
json={
|
||||||
|
"tree_id": tree_id,
|
||||||
|
"action_type": "generate_branch",
|
||||||
|
"target_node_id": "some-node",
|
||||||
|
"changes_json": {"before": {}, "after": {"id": "new-node"}},
|
||||||
|
},
|
||||||
|
headers=auth_headers,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
suggestion_id = resp.json()["id"]
|
||||||
|
assert resp.json()["status"] == "pending"
|
||||||
|
|
||||||
|
# List suggestions
|
||||||
|
resp = await client.get(
|
||||||
|
f"/api/v1/ai/suggestions/tree/{tree_id}",
|
||||||
|
headers=auth_headers,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert len(resp.json()) >= 1
|
||||||
|
|
||||||
|
# Resolve suggestion
|
||||||
|
resp = await client.patch(
|
||||||
|
f"/api/v1/ai/suggestions/{suggestion_id}",
|
||||||
|
json={"status": "accepted"},
|
||||||
|
headers=auth_headers,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["status"] == "accepted"
|
||||||
|
assert resp.json()["resolved_at"] is not None
|
||||||
Reference in New Issue
Block a user