Improve AI logic
This commit is contained in:
1
services/ai_insights/app/api/__init__.py
Normal file
1
services/ai_insights/app/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""API modules for AI Insights Service."""
|
||||
323
services/ai_insights/app/api/insights.py
Normal file
323
services/ai_insights/app/api/insights.py
Normal file
@@ -0,0 +1,323 @@
|
||||
"""API endpoints for AI Insights."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
import math
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.repositories.insight_repository import InsightRepository
|
||||
from app.repositories.feedback_repository import FeedbackRepository
|
||||
from app.schemas.insight import (
|
||||
AIInsightCreate,
|
||||
AIInsightUpdate,
|
||||
AIInsightResponse,
|
||||
AIInsightList,
|
||||
InsightMetrics,
|
||||
InsightFilters
|
||||
)
|
||||
from app.schemas.feedback import InsightFeedbackCreate, InsightFeedbackResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/tenants/{tenant_id}/insights", response_model=AIInsightResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_insight(
|
||||
tenant_id: UUID,
|
||||
insight_data: AIInsightCreate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Create a new AI Insight."""
|
||||
# Ensure tenant_id matches
|
||||
if insight_data.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Tenant ID mismatch"
|
||||
)
|
||||
|
||||
repo = InsightRepository(db)
|
||||
insight = await repo.create(insight_data)
|
||||
await db.commit()
|
||||
|
||||
return insight
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/insights", response_model=AIInsightList)
|
||||
async def get_insights(
|
||||
tenant_id: UUID,
|
||||
category: Optional[str] = Query(None),
|
||||
priority: Optional[str] = Query(None),
|
||||
status: Optional[str] = Query(None),
|
||||
actionable_only: bool = Query(False),
|
||||
min_confidence: int = Query(0, ge=0, le=100),
|
||||
source_service: Optional[str] = Query(None),
|
||||
from_date: Optional[datetime] = Query(None),
|
||||
to_date: Optional[datetime] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get insights for a tenant with filters and pagination."""
|
||||
filters = InsightFilters(
|
||||
category=category,
|
||||
priority=priority,
|
||||
status=status,
|
||||
actionable_only=actionable_only,
|
||||
min_confidence=min_confidence,
|
||||
source_service=source_service,
|
||||
from_date=from_date,
|
||||
to_date=to_date
|
||||
)
|
||||
|
||||
repo = InsightRepository(db)
|
||||
skip = (page - 1) * page_size
|
||||
|
||||
insights, total = await repo.get_by_tenant(tenant_id, filters, skip, page_size)
|
||||
|
||||
total_pages = math.ceil(total / page_size) if total > 0 else 0
|
||||
|
||||
return AIInsightList(
|
||||
items=insights,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_pages=total_pages
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/insights/orchestration-ready")
|
||||
async def get_orchestration_ready_insights(
|
||||
tenant_id: UUID,
|
||||
target_date: datetime = Query(...),
|
||||
min_confidence: int = Query(70, ge=0, le=100),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get actionable insights for orchestration workflow."""
|
||||
repo = InsightRepository(db)
|
||||
categorized_insights = await repo.get_orchestration_ready_insights(
|
||||
tenant_id, target_date, min_confidence
|
||||
)
|
||||
|
||||
return categorized_insights
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/insights/{insight_id}", response_model=AIInsightResponse)
|
||||
async def get_insight(
|
||||
tenant_id: UUID,
|
||||
insight_id: UUID,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get a single insight by ID."""
|
||||
repo = InsightRepository(db)
|
||||
insight = await repo.get_by_id(insight_id)
|
||||
|
||||
if not insight:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Insight not found"
|
||||
)
|
||||
|
||||
if insight.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied"
|
||||
)
|
||||
|
||||
return insight
|
||||
|
||||
|
||||
@router.patch("/tenants/{tenant_id}/insights/{insight_id}", response_model=AIInsightResponse)
|
||||
async def update_insight(
|
||||
tenant_id: UUID,
|
||||
insight_id: UUID,
|
||||
update_data: AIInsightUpdate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Update an insight (typically status changes)."""
|
||||
repo = InsightRepository(db)
|
||||
|
||||
# Verify insight exists and belongs to tenant
|
||||
insight = await repo.get_by_id(insight_id)
|
||||
if not insight:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Insight not found"
|
||||
)
|
||||
|
||||
if insight.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied"
|
||||
)
|
||||
|
||||
updated_insight = await repo.update(insight_id, update_data)
|
||||
await db.commit()
|
||||
|
||||
return updated_insight
|
||||
|
||||
|
||||
@router.delete("/tenants/{tenant_id}/insights/{insight_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def dismiss_insight(
|
||||
tenant_id: UUID,
|
||||
insight_id: UUID,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Dismiss an insight (soft delete)."""
|
||||
repo = InsightRepository(db)
|
||||
|
||||
# Verify insight exists and belongs to tenant
|
||||
insight = await repo.get_by_id(insight_id)
|
||||
if not insight:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Insight not found"
|
||||
)
|
||||
|
||||
if insight.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied"
|
||||
)
|
||||
|
||||
await repo.delete(insight_id)
|
||||
await db.commit()
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/insights/metrics/summary", response_model=InsightMetrics)
|
||||
async def get_insights_metrics(
|
||||
tenant_id: UUID,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get aggregate metrics for insights."""
|
||||
repo = InsightRepository(db)
|
||||
metrics = await repo.get_metrics(tenant_id)
|
||||
|
||||
return InsightMetrics(**metrics)
|
||||
|
||||
|
||||
@router.post("/tenants/{tenant_id}/insights/{insight_id}/apply")
|
||||
async def apply_insight(
|
||||
tenant_id: UUID,
|
||||
insight_id: UUID,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Apply an insight recommendation (trigger action)."""
|
||||
repo = InsightRepository(db)
|
||||
|
||||
# Verify insight exists and belongs to tenant
|
||||
insight = await repo.get_by_id(insight_id)
|
||||
if not insight:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Insight not found"
|
||||
)
|
||||
|
||||
if insight.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied"
|
||||
)
|
||||
|
||||
if not insight.actionable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="This insight is not actionable"
|
||||
)
|
||||
|
||||
# Update status to in_progress
|
||||
update_data = AIInsightUpdate(status='in_progress', applied_at=datetime.utcnow())
|
||||
await repo.update(insight_id, update_data)
|
||||
await db.commit()
|
||||
|
||||
# TODO: Route to appropriate service based on recommendation_actions
|
||||
# This will be implemented when service clients are added
|
||||
|
||||
return {
|
||||
"message": "Insight application initiated",
|
||||
"insight_id": str(insight_id),
|
||||
"actions": insight.recommendation_actions
|
||||
}
|
||||
|
||||
|
||||
@router.post("/tenants/{tenant_id}/insights/{insight_id}/feedback", response_model=InsightFeedbackResponse)
|
||||
async def record_feedback(
|
||||
tenant_id: UUID,
|
||||
insight_id: UUID,
|
||||
feedback_data: InsightFeedbackCreate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Record feedback for an applied insight."""
|
||||
insight_repo = InsightRepository(db)
|
||||
|
||||
# Verify insight exists and belongs to tenant
|
||||
insight = await insight_repo.get_by_id(insight_id)
|
||||
if not insight:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Insight not found"
|
||||
)
|
||||
|
||||
if insight.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied"
|
||||
)
|
||||
|
||||
# Ensure feedback is for this insight
|
||||
if feedback_data.insight_id != insight_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Insight ID mismatch"
|
||||
)
|
||||
|
||||
feedback_repo = FeedbackRepository(db)
|
||||
feedback = await feedback_repo.create(feedback_data)
|
||||
|
||||
# Update insight status based on feedback
|
||||
new_status = 'applied' if feedback.success else 'dismissed'
|
||||
update_data = AIInsightUpdate(status=new_status)
|
||||
await insight_repo.update(insight_id, update_data)
|
||||
|
||||
await db.commit()
|
||||
|
||||
return feedback
|
||||
|
||||
|
||||
@router.post("/tenants/{tenant_id}/insights/refresh")
|
||||
async def refresh_insights(
|
||||
tenant_id: UUID,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Trigger insight refresh (expire old, generate new)."""
|
||||
repo = InsightRepository(db)
|
||||
|
||||
# Expire old insights
|
||||
expired_count = await repo.expire_old_insights()
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"message": "Insights refreshed",
|
||||
"expired_count": expired_count
|
||||
}
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/insights/export")
|
||||
async def export_insights(
|
||||
tenant_id: UUID,
|
||||
format: str = Query("json", regex="^(json|csv)$"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Export insights to JSON or CSV."""
|
||||
repo = InsightRepository(db)
|
||||
insights, _ = await repo.get_by_tenant(tenant_id, filters=None, skip=0, limit=1000)
|
||||
|
||||
if format == "json":
|
||||
return {"insights": [AIInsightResponse.model_validate(i) for i in insights]}
|
||||
|
||||
# CSV export would be implemented here
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="CSV export not yet implemented"
|
||||
)
|
||||
Reference in New Issue
Block a user