feat: flow export/import + procedural Flow Assist #96
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 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)
|
||||
|
||||
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