72 lines
2.9 KiB
Python
72 lines
2.9 KiB
Python
"""统一意图分类出口,业务 Agent 不得自行解析模型分类结果。"""
|
|
|
|
import json
|
|
from typing import Any, Protocol
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
|
|
from app.core.contracts import IntentResult
|
|
from app.core.errors import RecoverableAgentError, ValidationAgentError
|
|
from app.service.model_gateway import ModelGenerationService
|
|
|
|
|
|
class IntentEndpointResolver(Protocol):
|
|
async def resolve(self, *, agent_type: str, task_type: str) -> list[Any]: ...
|
|
|
|
|
|
class _IntentPayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
intent: str = Field(min_length=1, max_length=64)
|
|
confidence: float = Field(ge=0, le=1)
|
|
|
|
|
|
class IntentClassifier:
|
|
"""使用已路由模型输出严格 JSON,并绑定 Agent 声明的意图集合。"""
|
|
|
|
def __init__(self, model_service: ModelGenerationService, *, threshold: float = 0.65) -> None:
|
|
if not 0 <= threshold <= 1:
|
|
raise ValueError("intent threshold must be between 0 and 1")
|
|
self.model_service = model_service
|
|
self.threshold = threshold
|
|
|
|
async def classify(
|
|
self, *, message: str, supported_intents: tuple[str, ...], endpoints: list[Any],
|
|
intent_descriptions: dict[str, str] | None = None,
|
|
) -> IntentResult:
|
|
if not message.strip():
|
|
raise ValidationAgentError("待分类消息不能为空")
|
|
if not supported_intents:
|
|
raise ValidationAgentError("Agent 未声明支持的意图")
|
|
descriptions = intent_descriptions or {}
|
|
intent_options = "\n".join(
|
|
f"- {intent}: {descriptions.get(intent, intent)}" for intent in supported_intents
|
|
)
|
|
prompt = (
|
|
"请仅输出 JSON,不要 Markdown。字段必须为 intent 和 confidence。"
|
|
"intent 只能从以下候选项中选择,confidence 为 0 到 1 的数字:\n"
|
|
f"{intent_options}\n"
|
|
f"用户消息:{message}"
|
|
)
|
|
execution = await self.model_service.generate(endpoints, prompt)
|
|
payload = self._parse(execution.text)
|
|
if payload.intent not in supported_intents:
|
|
raise ValidationAgentError("模型返回了未声明的意图")
|
|
return IntentResult(
|
|
intent=payload.intent,
|
|
confidence=payload.confidence,
|
|
needs_clarification=payload.confidence < self.threshold,
|
|
)
|
|
|
|
@staticmethod
|
|
def _parse(text: str) -> _IntentPayload:
|
|
candidate = text.strip()
|
|
if candidate.startswith("```"):
|
|
candidate = candidate.removeprefix("```")
|
|
candidate = candidate.removeprefix("json").removesuffix("```").strip()
|
|
try:
|
|
value = json.loads(candidate)
|
|
return _IntentPayload.model_validate(value)
|
|
except (json.JSONDecodeError, ValidationError, TypeError) as exc:
|
|
raise RecoverableAgentError("模型意图输出不是有效 JSON") from exc
|