- Added new modules for advisor compliance, KYC sessions, and script templates, enhancing the advisor agent's capabilities. - Implemented a comprehensive API structure under the `/api/advisor-agent` prefix, ensuring clear organization and access to new features. - Established database models and repositories for compliance rules and KYC sessions, facilitating robust data management. - Integrated exception handling and response models to improve error management and user feedback. - Updated settings to include new configurations for compliance and KYC features, ensuring flexibility and adaptability. This update significantly expands the advisor agent's functionality, providing essential tools for compliance and customer interaction while maintaining a structured API design.
151 lines
5.0 KiB
Python
151 lines
5.0 KiB
Python
"""AI semantic compliance detection with safe degradation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Protocol
|
|
|
|
import httpx
|
|
from pydantic import BaseModel, ValidationError
|
|
|
|
from app.config.settings import settings
|
|
from app.model.advisor_schemas import ComplianceAiAnalysis
|
|
|
|
PROMPT_VERSION = "compliance-semantic-v1"
|
|
DEGRADED_SUGGESTION = "AI 语义检测暂不可用,请人工确认后再继续复制或导出。"
|
|
|
|
|
|
class LLMClient(Protocol):
|
|
def complete(self, prompt: str, *, timeout_seconds: float) -> str:
|
|
pass
|
|
|
|
|
|
class SemanticLLMUnavailableError(RuntimeError):
|
|
def __init__(self, reason: str) -> None:
|
|
self.reason = reason
|
|
super().__init__(reason)
|
|
|
|
|
|
class SemanticLLMResult(BaseModel):
|
|
risk_level: str
|
|
reason: str
|
|
suggestion: str | None = None
|
|
|
|
|
|
class DeepSeekLLMClient:
|
|
def complete(self, prompt: str, *, timeout_seconds: float) -> str:
|
|
if not settings.deepseek_api_key:
|
|
raise SemanticLLMUnavailableError("missing_api_key")
|
|
|
|
response = httpx.post(
|
|
f"{settings.deepseek_base_url.rstrip('/')}/chat/completions",
|
|
headers={"Authorization": f"Bearer {settings.deepseek_api_key}"},
|
|
json={
|
|
"model": settings.deepseek_model,
|
|
"messages": [
|
|
{
|
|
"role": "system",
|
|
"content": "你是金融投顾话术合规审核助手,只返回 JSON。",
|
|
},
|
|
{"role": "user", "content": prompt},
|
|
],
|
|
"temperature": 0,
|
|
"response_format": {"type": "json_object"},
|
|
},
|
|
timeout=timeout_seconds,
|
|
)
|
|
response.raise_for_status()
|
|
body = response.json()
|
|
return body["choices"][0]["message"]["content"]
|
|
|
|
|
|
class ComplianceSemanticService:
|
|
def __init__(
|
|
self,
|
|
llm_client: LLMClient | None = None,
|
|
*,
|
|
enabled: bool | None = None,
|
|
timeout_seconds: float | None = None,
|
|
) -> None:
|
|
self.llm_client = llm_client or DeepSeekLLMClient()
|
|
self.enabled = settings.compliance_ai_enabled if enabled is None and llm_client is None else enabled is not False
|
|
self.timeout_seconds = timeout_seconds or settings.compliance_ai_timeout_seconds
|
|
|
|
def analyze(
|
|
self,
|
|
*,
|
|
text: str,
|
|
scene: str | None = None,
|
|
customer_risk_level: str | None = None,
|
|
) -> ComplianceAiAnalysis | None:
|
|
if not self.enabled:
|
|
return None
|
|
|
|
prompt = self._build_prompt(text=text, scene=scene, customer_risk_level=customer_risk_level)
|
|
try:
|
|
raw_output = self.llm_client.complete(prompt, timeout_seconds=self.timeout_seconds)
|
|
return self._parse_output(raw_output)
|
|
except TimeoutError:
|
|
return self._degraded("timeout")
|
|
except SemanticLLMUnavailableError as exc:
|
|
return self._degraded(exc.reason)
|
|
except (httpx.HTTPError, KeyError, IndexError, TypeError, ValueError):
|
|
return self._degraded("malformed_output")
|
|
|
|
def _parse_output(self, raw_output: str) -> ComplianceAiAnalysis:
|
|
if not raw_output.strip():
|
|
return self._degraded("empty_output")
|
|
|
|
try:
|
|
parsed = json.loads(raw_output)
|
|
except json.JSONDecodeError:
|
|
return self._degraded("malformed_output")
|
|
|
|
if parsed.get("refusal"):
|
|
return self._degraded("refusal")
|
|
|
|
try:
|
|
result = SemanticLLMResult.model_validate(parsed)
|
|
except ValidationError:
|
|
return self._degraded("malformed_output")
|
|
|
|
risk_level = result.risk_level.upper()
|
|
if risk_level not in {"BLOCK", "WARN", "INFO"}:
|
|
return self._degraded("malformed_output")
|
|
|
|
return ComplianceAiAnalysis(
|
|
risk_level=risk_level,
|
|
reason=result.reason,
|
|
suggestion=result.suggestion,
|
|
degraded=False,
|
|
prompt_version=PROMPT_VERSION,
|
|
model=settings.deepseek_model,
|
|
)
|
|
|
|
@staticmethod
|
|
def _build_prompt(
|
|
*,
|
|
text: str,
|
|
scene: str | None = None,
|
|
customer_risk_level: str | None = None,
|
|
) -> str:
|
|
return (
|
|
f"prompt_version={PROMPT_VERSION}\n"
|
|
"请判断以下金融投顾话术是否存在隐性违规风险,只返回 JSON:"
|
|
"{\"risk_level\":\"INFO|WARN|BLOCK\",\"reason\":\"...\",\"suggestion\":\"...\"}\n"
|
|
f"scene={scene or 'general'}\n"
|
|
f"customer_risk_level={customer_risk_level or 'unknown'}\n"
|
|
f"text={text}"
|
|
)
|
|
|
|
@staticmethod
|
|
def _degraded(reason: str) -> ComplianceAiAnalysis:
|
|
return ComplianceAiAnalysis(
|
|
risk_level="WARN",
|
|
reason=f"AI semantic detection degraded: {reason}",
|
|
suggestion=DEGRADED_SUGGESTION,
|
|
degraded=True,
|
|
prompt_version=PROMPT_VERSION,
|
|
model=settings.deepseek_model,
|
|
)
|