""" Intent Rule Management API. [AC-AISVC-65~AC-AISVC-68] Intent rule CRUD endpoints. [AC-AISVC-96] Intent rule testing endpoint. """ import logging import uuid from typing import Any from fastapi import APIRouter, Depends, Header, HTTPException from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_session from app.models.entities import IntentRuleCreate, IntentRuleUpdate from app.services.intent.rule_service import IntentRuleService from app.services.intent.tester import IntentRuleTester logger = logging.getLogger(__name__) router = APIRouter(prefix="/admin/intent-rules", tags=["Intent Rules"]) def get_tenant_id(x_tenant_id: str = Header(..., alias="X-Tenant-Id")) -> str: """Extract tenant ID from header.""" if not x_tenant_id: raise HTTPException(status_code=400, detail="X-Tenant-Id header is required") return x_tenant_id @router.get("") async def list_rules( tenant_id: str = Depends(get_tenant_id), response_type: str | None = None, is_enabled: bool | None = None, session: AsyncSession = Depends(get_session), ) -> dict[str, Any]: """ [AC-AISVC-66] List all intent rules for a tenant. """ logger.info( f"[AC-AISVC-66] Listing intent rules for tenant={tenant_id}, " f"response_type={response_type}, is_enabled={is_enabled}" ) service = IntentRuleService(session) rules = await service.list_rules(tenant_id, response_type, is_enabled) data = [] for rule in rules: data.append(await service.rule_to_info_dict(rule)) return {"data": data} @router.post("", status_code=201) async def create_rule( body: IntentRuleCreate, tenant_id: str = Depends(get_tenant_id), session: AsyncSession = Depends(get_session), ) -> dict[str, Any]: """ [AC-AISVC-65] Create a new intent rule. """ valid_response_types = ["fixed", "rag", "flow", "transfer"] if body.response_type not in valid_response_types: raise HTTPException( status_code=400, detail=f"Invalid response_type. Must be one of: {valid_response_types}" ) if body.response_type == "rag" and not body.target_kb_ids: logger.warning( f"[AC-AISVC-65] Creating rag rule without target_kb_ids: tenant={tenant_id}" ) if body.response_type == "flow" and not body.flow_id: raise HTTPException( status_code=400, detail="flow_id is required when response_type is 'flow'" ) if body.response_type == "fixed" and not body.fixed_reply: raise HTTPException( status_code=400, detail="fixed_reply is required when response_type is 'fixed'" ) if body.response_type == "transfer" and not body.transfer_message: raise HTTPException( status_code=400, detail="transfer_message is required when response_type is 'transfer'" ) logger.info( f"[AC-AISVC-65] Creating intent rule for tenant={tenant_id}, name={body.name}" ) service = IntentRuleService(session) rule = await service.create_rule(tenant_id, body) return await service.rule_to_info_dict(rule) @router.get("/{rule_id}") async def get_rule( rule_id: uuid.UUID, tenant_id: str = Depends(get_tenant_id), session: AsyncSession = Depends(get_session), ) -> dict[str, Any]: """ [AC-AISVC-66] Get intent rule detail. """ logger.info(f"[AC-AISVC-66] Getting intent rule for tenant={tenant_id}, id={rule_id}") service = IntentRuleService(session) rule = await service.get_rule(tenant_id, rule_id) if not rule: raise HTTPException(status_code=404, detail="Intent rule not found") return await service.rule_to_info_dict(rule) @router.put("/{rule_id}") async def update_rule( rule_id: uuid.UUID, body: IntentRuleUpdate, tenant_id: str = Depends(get_tenant_id), session: AsyncSession = Depends(get_session), ) -> dict[str, Any]: """ [AC-AISVC-67] Update an intent rule. """ valid_response_types = ["fixed", "rag", "flow", "transfer"] if body.response_type is not None and body.response_type not in valid_response_types: raise HTTPException( status_code=400, detail=f"Invalid response_type. Must be one of: {valid_response_types}" ) logger.info(f"[AC-AISVC-67] Updating intent rule for tenant={tenant_id}, id={rule_id}") service = IntentRuleService(session) rule = await service.update_rule(tenant_id, rule_id, body) if not rule: raise HTTPException(status_code=404, detail="Intent rule not found") return await service.rule_to_info_dict(rule) @router.delete("/{rule_id}", status_code=204) async def delete_rule( rule_id: uuid.UUID, tenant_id: str = Depends(get_tenant_id), session: AsyncSession = Depends(get_session), ) -> None: """ [AC-AISVC-68] Delete an intent rule. """ logger.info(f"[AC-AISVC-68] Deleting intent rule for tenant={tenant_id}, id={rule_id}") service = IntentRuleService(session) success = await service.delete_rule(tenant_id, rule_id) if not success: raise HTTPException(status_code=404, detail="Intent rule not found") class IntentRuleTestRequest(BaseModel): """Request body for testing an intent rule.""" message: str @router.post("/{rule_id}/test") async def test_rule( rule_id: uuid.UUID, body: IntentRuleTestRequest, tenant_id: str = Depends(get_tenant_id), session: AsyncSession = Depends(get_session), ) -> dict[str, Any]: """ [AC-AISVC-96] Test an intent rule against a message. Returns match result with conflict detection. """ logger.info( f"[AC-AISVC-96] Testing intent rule for tenant={tenant_id}, " f"rule_id={rule_id}, message='{body.message[:50]}...'" ) service = IntentRuleService(session) rule = await service.get_rule(tenant_id, rule_id) if not rule: raise HTTPException(status_code=404, detail="Intent rule not found") all_rules = await service.get_enabled_rules_for_matching(tenant_id) tester = IntentRuleTester() result = await tester.test_rule(rule, [body.message], all_rules) return result.to_dict()