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:
chihlasm
2026-03-07 00:20:32 -05:00
parent 270c20912e
commit dcfc70b1e6
4 changed files with 155 additions and 0 deletions

View 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

View File

@@ -13,6 +13,7 @@ from app.api.endpoints import assistant_chat
from app.api.endpoints import survey
from app.api.endpoints import admin_survey
from app.api.endpoints import tree_transfer
from app.api.endpoints import ai_suggestions
api_router = APIRouter()
@@ -50,3 +51,4 @@ api_router.include_router(assistant_chat.router)
api_router.include_router(survey.router)
api_router.include_router(admin_survey.router)
api_router.include_router(tree_transfer.router)
api_router.include_router(ai_suggestions.router)

View 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)$")

View 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