2026-09-09 21:55:37 +08:00
|
|
|
import asyncio
|
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
|
from collections.abc import AsyncIterator
|
|
|
|
|
|
|
|
|
|
from app.core.contracts import (
|
|
|
|
|
AgentDefinition,
|
|
|
|
|
AgentRequest,
|
|
|
|
|
AgentResult,
|
|
|
|
|
CoreResult,
|
|
|
|
|
IntentResult,
|
|
|
|
|
RecalledMemory,
|
|
|
|
|
RequestContext,
|
|
|
|
|
ResolvedAgentConfig,
|
|
|
|
|
RunProgressEvent,
|
|
|
|
|
SourceReference,
|
|
|
|
|
ToolCallRecord,
|
|
|
|
|
)
|
2026-09-10 15:55:54 +08:00
|
|
|
from app.core.errors import RecoverableAgentError, UpstreamTimeoutError
|
2026-09-09 21:55:37 +08:00
|
|
|
from app.service.agent.authorizer import AgentAuthorizer
|
|
|
|
|
from app.service.agent.governance import AgentGovernance
|
|
|
|
|
from app.service.intent_classifier import IntentClassifier, IntentEndpointResolver
|
|
|
|
|
from app.service.model_gateway import ModelExecution, ModelGenerationService
|
|
|
|
|
from app.service.tool_executor import ToolExecutor
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BaseAgent(ABC):
|
|
|
|
|
definition: AgentDefinition
|
|
|
|
|
|
|
|
|
|
def __init__(self, definition: AgentDefinition) -> None:
|
|
|
|
|
self.definition = definition
|
|
|
|
|
self._governance: AgentGovernance | None = None
|
|
|
|
|
self.config: ResolvedAgentConfig | None = None
|
|
|
|
|
self.memories: tuple[RecalledMemory, ...] = ()
|
|
|
|
|
self._model_service: ModelGenerationService | None = None
|
|
|
|
|
self._tool_executor: ToolExecutor | None = None
|
|
|
|
|
self._tool_records: list[ToolCallRecord] = []
|
|
|
|
|
self._tool_references: list[SourceReference] = []
|
|
|
|
|
self._intent_classifier: IntentClassifier | None = None
|
|
|
|
|
self._intent_endpoint_resolver: IntentEndpointResolver | None = None
|
|
|
|
|
self._classified_intent: IntentResult | None = None
|
|
|
|
|
|
|
|
|
|
def bind_governance(self, governance: AgentGovernance) -> None:
|
|
|
|
|
if self._governance is not None:
|
|
|
|
|
raise TypeError("Agent instances must not be reused")
|
|
|
|
|
self._governance = governance
|
|
|
|
|
|
|
|
|
|
def bind_model_service(self, service: ModelGenerationService) -> None:
|
|
|
|
|
if self._model_service is not None:
|
|
|
|
|
raise TypeError("model service is already bound")
|
|
|
|
|
self._model_service = service
|
|
|
|
|
|
|
|
|
|
def bind_tool_executor(self, executor: ToolExecutor) -> None:
|
|
|
|
|
if self._tool_executor is not None:
|
|
|
|
|
raise TypeError("tool executor is already bound")
|
|
|
|
|
self._tool_executor = executor
|
|
|
|
|
|
|
|
|
|
def bind_intent_classifier(
|
|
|
|
|
self, classifier: IntentClassifier, resolver: IntentEndpointResolver
|
|
|
|
|
) -> None:
|
|
|
|
|
if self._intent_classifier is not None:
|
|
|
|
|
raise TypeError("intent classifier is already bound")
|
|
|
|
|
self._intent_classifier = classifier
|
|
|
|
|
self._intent_endpoint_resolver = resolver
|
|
|
|
|
|
|
|
|
|
async def call_tool(
|
|
|
|
|
self, name: str, arguments: dict[str, object], *, intent: str,
|
|
|
|
|
context: RequestContext,
|
|
|
|
|
) -> object:
|
|
|
|
|
if self._tool_executor is None or self.config is None:
|
|
|
|
|
raise RecoverableAgentError("工具执行器未由工厂注入")
|
|
|
|
|
execution = await self._tool_executor.execute(
|
|
|
|
|
name=name, arguments=arguments, intent=intent,
|
|
|
|
|
configured_tools=self.config.allowed_tools_by_intent, context=context,
|
|
|
|
|
)
|
|
|
|
|
self._tool_records.append(execution.record)
|
|
|
|
|
self._tool_references.extend(execution.references)
|
|
|
|
|
return execution.output
|
|
|
|
|
|
|
|
|
|
async def generate_with_model(
|
|
|
|
|
self, endpoints: list[object], prompt: str, *, max_attempts: int = 2
|
|
|
|
|
) -> ModelExecution:
|
|
|
|
|
if self._model_service is None:
|
|
|
|
|
raise RecoverableAgentError("模型服务未由工厂注入")
|
|
|
|
|
return await self._model_service.generate(endpoints, prompt, max_attempts=max_attempts)
|
|
|
|
|
|
|
|
|
|
def __init_subclass__(cls, **kwargs: object) -> None:
|
|
|
|
|
super().__init_subclass__(**kwargs)
|
|
|
|
|
forbidden = {"execute", "validate_input", "validate_access", "resolve_config",
|
|
|
|
|
"recall_memory", "check_compliance", "_execute_governed",
|
|
|
|
|
"bind_governance", "bind_model_service", "generate_with_model",
|
|
|
|
|
"bind_tool_executor", "call_tool", "bind_intent_classifier",
|
|
|
|
|
"classify_intent"}
|
|
|
|
|
overridden = forbidden.intersection(cls.__dict__)
|
|
|
|
|
if overridden:
|
|
|
|
|
raise TypeError(f"Agent cannot override governance methods: {sorted(overridden)}")
|
|
|
|
|
|
|
|
|
|
async def execute(
|
|
|
|
|
self, request: AgentRequest, context: RequestContext, run_id: str
|
|
|
|
|
) -> AsyncIterator[RunProgressEvent]:
|
|
|
|
|
self.validate_input(request)
|
|
|
|
|
await self.validate_access(request, context)
|
|
|
|
|
await self.resolve_config(context)
|
|
|
|
|
await self.recall_memory(request, context)
|
|
|
|
|
await self.classify_intent(request)
|
|
|
|
|
governance, config, memories = self._governance, self.config, self.memories
|
|
|
|
|
if governance is None or config is None:
|
|
|
|
|
raise RecoverableAgentError("治理初始化失败")
|
|
|
|
|
yield RunProgressEvent(event_type="start", run_id=run_id)
|
|
|
|
|
result = await self._execute_governed(request, context, run_id)
|
|
|
|
|
# Capture the trusted snapshot before entering business code.
|
2026-09-11 15:06:44 +08:00
|
|
|
# 传 `agent_type` 让治理层判断"这条输出是否面向客户":门禁 F5(面向客户输出 100%
|
|
|
|
|
# 附固定话术)只对面向客户的 Agent 生效,内部 Agent(风控)的输出是字段化摘要,
|
|
|
|
|
# 追加话术会破坏其字段契约。类型从这里传最可靠——它是定义的一部分,不需要查库。
|
|
|
|
|
result = await governance.review(
|
|
|
|
|
result, context, config, memories, agent_type=self.definition.agent_type
|
|
|
|
|
)
|
2026-09-09 21:55:37 +08:00
|
|
|
yield RunProgressEvent(
|
|
|
|
|
event_type="done", run_id=run_id,
|
|
|
|
|
payload={"result": result.model_dump(mode="json")},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def validate_input(self, request: AgentRequest) -> None:
|
|
|
|
|
if request.agent_type != self.definition.agent_type:
|
|
|
|
|
raise ValueError("request agent_type does not match definition")
|
|
|
|
|
|
|
|
|
|
async def validate_access(self, request: AgentRequest, context: RequestContext) -> None:
|
|
|
|
|
AgentAuthorizer.ensure_allowed(self.definition, context)
|
|
|
|
|
|
|
|
|
|
async def resolve_config(self, context: RequestContext) -> None:
|
|
|
|
|
if self._governance is None:
|
|
|
|
|
raise RecoverableAgentError("Agent 未由工厂注入治理依赖")
|
|
|
|
|
self.config = await self._governance.resolve(self.definition, context)
|
|
|
|
|
|
|
|
|
|
async def recall_memory(self, request: AgentRequest, context: RequestContext) -> None:
|
|
|
|
|
if self._governance is None:
|
|
|
|
|
raise RecoverableAgentError("缺少记忆治理依赖")
|
2026-09-11 16:11:30 +08:00
|
|
|
# 公共召回是长期/画像记忆,不是客服二期的会话短期上下文;定义未授权时不得读取。
|
|
|
|
|
if not self.definition.recalls_customer_memory or "visitor" in context.roles:
|
2026-09-10 17:36:39 +08:00
|
|
|
self.memories = ()
|
|
|
|
|
return
|
2026-09-09 21:55:37 +08:00
|
|
|
self.memories = await self._governance.recall(context)
|
|
|
|
|
if any(memory.customer_id != context.user_id for memory in self.memories):
|
|
|
|
|
raise RecoverableAgentError("记忆召回越过客户范围")
|
|
|
|
|
|
|
|
|
|
async def classify_intent(self, request: AgentRequest) -> IntentResult | None:
|
2026-09-10 17:36:39 +08:00
|
|
|
if not self.definition.requires_model_intent_classification:
|
|
|
|
|
return None
|
2026-09-09 21:55:37 +08:00
|
|
|
if self._intent_classifier is None or self._intent_endpoint_resolver is None:
|
|
|
|
|
return None
|
|
|
|
|
endpoints = await self._intent_endpoint_resolver.resolve(
|
|
|
|
|
agent_type=self.definition.agent_type, task_type="intent_classification"
|
|
|
|
|
)
|
|
|
|
|
self._classified_intent = await self._intent_classifier.classify(
|
|
|
|
|
message=request.message,
|
|
|
|
|
supported_intents=self.definition.supported_intents,
|
|
|
|
|
endpoints=endpoints,
|
2026-09-10 15:55:54 +08:00
|
|
|
# 按 agent_type 读取该 Agent 当前生效的意图配置(描述/示例/阈值)。
|
|
|
|
|
agent_type=self.definition.agent_type,
|
2026-09-09 21:55:37 +08:00
|
|
|
)
|
|
|
|
|
return self._classified_intent
|
|
|
|
|
|
|
|
|
|
async def check_compliance(self, result: AgentResult, context: RequestContext) -> AgentResult:
|
|
|
|
|
if self._governance is None or self.config is None:
|
|
|
|
|
raise RecoverableAgentError("缺少合规治理依赖")
|
|
|
|
|
return await self._governance.review(result, context, self.config, self.memories)
|
|
|
|
|
|
|
|
|
|
async def _execute_governed(
|
|
|
|
|
self, request: AgentRequest, context: RequestContext, run_id: str
|
|
|
|
|
) -> AgentResult:
|
|
|
|
|
if self.config is None:
|
|
|
|
|
raise RecoverableAgentError("缺少运行配置")
|
|
|
|
|
try:
|
|
|
|
|
async with asyncio.timeout(self.config.timeout_seconds):
|
|
|
|
|
result = await self.handle(request, context)
|
|
|
|
|
except TimeoutError as exc:
|
2026-09-10 15:55:54 +08:00
|
|
|
raise UpstreamTimeoutError("Agent 执行超时") from exc
|
2026-09-09 21:55:37 +08:00
|
|
|
result = result.model_copy(update={
|
|
|
|
|
"intent": result.intent or self._classified_intent,
|
|
|
|
|
"tool_calls": tuple(self._tool_records),
|
|
|
|
|
"source_references": tuple(result.source_references) + tuple(self._tool_references),
|
|
|
|
|
})
|
|
|
|
|
return AgentResult(run_id=run_id, result=result)
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
async def handle(self, request: AgentRequest, context: RequestContext) -> CoreResult:
|
|
|
|
|
"""Implement domain-specific intent handling here."""
|