ai-robot-core/ai-service/app/api/admin/intent_rules.py

207 lines
6.1 KiB
Python

"""
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()