feat: preserve isolated visitor agent runtime
This commit is contained in:
@@ -60,6 +60,7 @@ class AgentDefinition(BaseModel):
|
||||
allowed_roles: tuple[str, ...] = ()
|
||||
allowed_portals: tuple[str, ...] = ()
|
||||
supported_intents: tuple[str, ...] = ("general",)
|
||||
requires_model_intent_classification: bool = True
|
||||
|
||||
|
||||
class ResolvedAgentConfig(BaseModel):
|
||||
|
||||
@@ -92,6 +92,6 @@ class JwtAuthenticator:
|
||||
if claims.get("visitor") is True:
|
||||
return RequestContext(
|
||||
user_id=str(subject), trace_id=str(uuid4()), roles=("visitor",),
|
||||
permissions=("agent:run",),
|
||||
permissions=("agent:run",), data_scope="public",
|
||||
)
|
||||
return RequestContext(user_id=str(claims["sub"]), trace_id=str(uuid4()))
|
||||
|
||||
@@ -129,11 +129,16 @@ class BaseAgent(ABC):
|
||||
async def recall_memory(self, request: AgentRequest, context: RequestContext) -> None:
|
||||
if self._governance is None:
|
||||
raise RecoverableAgentError("缺少记忆治理依赖")
|
||||
if "visitor" in context.roles:
|
||||
self.memories = ()
|
||||
return
|
||||
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:
|
||||
if not self.definition.requires_model_intent_classification:
|
||||
return None
|
||||
if self._intent_classifier is None or self._intent_endpoint_resolver is None:
|
||||
return None
|
||||
endpoints = await self._intent_endpoint_resolver.resolve(
|
||||
|
||||
@@ -59,6 +59,10 @@ class AgentFactory:
|
||||
agent.bind_model_service(self._model_service)
|
||||
if self._tool_executor is not None:
|
||||
agent.bind_tool_executor(self._tool_executor)
|
||||
if self._intent_classifier is not None and self._intent_endpoint_resolver is not None:
|
||||
if (
|
||||
agent.definition.requires_model_intent_classification
|
||||
and self._intent_classifier is not None
|
||||
and self._intent_endpoint_resolver is not None
|
||||
):
|
||||
agent.bind_intent_classifier(self._intent_classifier, self._intent_endpoint_resolver)
|
||||
return agent
|
||||
|
||||
@@ -118,7 +118,11 @@ class AgentRunApplicationService:
|
||||
await OutboxRepository(self.session).append(DomainEvent(
|
||||
event_id=str(uuid4()), event_type="agent.run_requested", aggregate_type="agent_run",
|
||||
aggregate_id=run_id, trace_id=trace_id,
|
||||
payload={"run_id": run_id, "metadata": request.metadata.model_dump(mode="json")},
|
||||
payload={
|
||||
"run_id": run_id,
|
||||
"actor_type": "visitor" if "visitor" in context.roles else "authenticated",
|
||||
"metadata": request.metadata.model_dump(mode="json"),
|
||||
},
|
||||
occurred_at=now,
|
||||
))
|
||||
return RunAccepted(run_id, trace_id)
|
||||
|
||||
+43
-14
@@ -102,6 +102,35 @@ class WorkerRuntime:
|
||||
# episode 聚合是低频批处理,按轮次节流而不是每轮都查。
|
||||
self._episode_rounds = 0
|
||||
|
||||
async def restore_context(
|
||||
self, *, actor_type: str, actor_id: str, trace_id: str
|
||||
) -> RequestContext:
|
||||
"""按已验证的内部事件身份恢复执行上下文。"""
|
||||
identity = RequestContext(user_id=actor_id, trace_id=trace_id)
|
||||
if actor_type == "visitor":
|
||||
return identity.model_copy(update={
|
||||
"roles": ("visitor",),
|
||||
"permissions": ("agent:run",),
|
||||
"data_scope": "public",
|
||||
})
|
||||
return await self.resolve_identity(identity)
|
||||
|
||||
@staticmethod
|
||||
def should_request_memory_extraction(
|
||||
*, context: RequestContext, message: str, result: AgentResult,
|
||||
business_events: tuple[str, ...] | list[str],
|
||||
) -> bool:
|
||||
"""只允许已登录用户的明确业务事实进入客户记忆抽取队列。"""
|
||||
if "visitor" in context.roles:
|
||||
return False
|
||||
return MemoryService.should_extract_memory(
|
||||
conversation_content=message,
|
||||
role="user",
|
||||
tool_result=any(call.status == "succeeded" for call in result.result.tool_calls),
|
||||
event_type=business_events[0] if business_events else None,
|
||||
signals=MemoryService.detect_memory_signals(message),
|
||||
)
|
||||
|
||||
async def dispatch_one(self, *, run_id: str | None = None) -> bool:
|
||||
# Outbox acknowledges a durable SQL queue entry, not an in-memory task.
|
||||
async with SessionFactory() as session:
|
||||
@@ -428,9 +457,15 @@ class WorkerRuntime:
|
||||
idempotency_key=idem.idempotency_key,
|
||||
metadata=AgentRequestMetadata.model_validate(metadata),
|
||||
)
|
||||
identity = RequestContext(user_id=str(run.user_id), trace_id=run.trace_id)
|
||||
# Re-check account and permissions at execution time, including delayed jobs.
|
||||
context = await self.resolve_identity(identity)
|
||||
actor_type = (
|
||||
str(event.payload.get("actor_type", "authenticated"))
|
||||
if event else "authenticated"
|
||||
)
|
||||
actor_id = str(run.user_id)
|
||||
trace_id = run.trace_id
|
||||
context = await self.restore_context(
|
||||
actor_type=actor_type, actor_id=actor_id, trace_id=trace_id
|
||||
)
|
||||
result: AgentResult | None = None
|
||||
async for event_data in AgentExecutor(self.factory).execute(
|
||||
request.agent_type, request, context, run_id
|
||||
@@ -452,17 +487,11 @@ class WorkerRuntime:
|
||||
async with SessionFactory() as session:
|
||||
await AgentPersistenceService(session).complete_run(
|
||||
run_id, result, worker_id=worker_id,
|
||||
memory_extraction_requested=MemoryService.should_extract_memory(
|
||||
conversation_content=request.message,
|
||||
role="user",
|
||||
# 工具产出的权威事实同样构成持久记忆(工具调用记录来自终态结果)。
|
||||
tool_result=any(
|
||||
call.status == "succeeded" for call in result.result.tool_calls
|
||||
),
|
||||
# 本 run 落库的业务事件(风险评估完成、交易完成等)。
|
||||
event_type=business_events[0] if business_events else None,
|
||||
# 用户明确陈述的偏好/约束/身份/目标,命中才触发抽取。
|
||||
signals=MemoryService.detect_memory_signals(request.message),
|
||||
memory_extraction_requested=self.should_request_memory_extraction(
|
||||
context=context,
|
||||
message=request.message,
|
||||
result=result,
|
||||
business_events=business_events,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user