refactor: 梳理规则体系并统一记忆与部署流程
This commit is contained in:
@@ -59,16 +59,6 @@ def _patch_repositories(
|
||||
monkeypatch.setattr(store_module, "MessageRepository", _FakeMessageRepository)
|
||||
monkeypatch.setattr(store_module, "AgentChatSessionStatus", _SessionStatus)
|
||||
|
||||
async def _fake_stage_bit_map(self, *, session: object) -> dict[str, int]:
|
||||
del self, session
|
||||
return {"router": 16, "worker": 17, "memory": 18}
|
||||
|
||||
monkeypatch.setattr(
|
||||
store_module.SqlAlchemyEventStore,
|
||||
"_load_stage_visibility_bit_map",
|
||||
_fake_stage_bit_map,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_store_persists_worker_output_with_answer_as_content(
|
||||
@@ -113,7 +103,7 @@ async def test_store_persists_worker_output_with_answer_as_content(
|
||||
assert metadata["agent_output"]["answer"] == "worker-answer"
|
||||
assert metadata["agent_output"]["ui_hints"]["intent"] == "message"
|
||||
assert append_kwargs["cost"] == Decimal("0.123")
|
||||
assert append_kwargs["visibility_mask"] == ((1 << 0) | (1 << 17))
|
||||
assert append_kwargs["visibility_mask"] == ((1 << 0) | (1 << 1))
|
||||
assert captured["message_delta"] == 1
|
||||
assert captured["token_delta"] == 8
|
||||
|
||||
@@ -153,3 +143,65 @@ async def test_store_persists_tool_output_with_summary_as_content(
|
||||
== "status=success batch=1 success=1 failed=0 ids=[event-1]"
|
||||
)
|
||||
assert append_kwargs["visibility_mask"] == (1 << 0)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_store_persists_router_step_output_for_cost_tracking(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
fake_chat_session = SimpleNamespace(state_snapshot={}, message_count=10)
|
||||
_patch_repositories(monkeypatch, captured, fake_chat_session)
|
||||
|
||||
store = store_module.SqlAlchemyEventStore(session_factory=lambda: _FakeSessionCtx())
|
||||
await store.persist(
|
||||
{
|
||||
"type": "STEP_FINISHED",
|
||||
"threadId": "00000000-0000-0000-0000-000000000001",
|
||||
"runId": "run-router-1",
|
||||
"stepName": "router",
|
||||
"_router_persist": {
|
||||
"router_output": {
|
||||
"normalized_task_input": {
|
||||
"user_text": "安排明天会议",
|
||||
"context_summary": "",
|
||||
},
|
||||
"key_entities": [],
|
||||
"constraints": [],
|
||||
"task_typing": {"primary": "scheduling"},
|
||||
"execution_mode": "tool_assisted",
|
||||
"result_typing": {"primary": "execution_report"},
|
||||
"ui": {
|
||||
"ui_mode": "none",
|
||||
"ui_decision_reason": "单任务",
|
||||
},
|
||||
},
|
||||
"response_metadata": {
|
||||
"model": "doubao-seed-1-6-250615",
|
||||
"inputTokens": 12,
|
||||
"outputTokens": 8,
|
||||
"cost": "0.01",
|
||||
"latencyMs": 320,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
append_kwargs = cast(dict[str, Any], captured["append_kwargs"])
|
||||
assert append_kwargs["seq"] == 11
|
||||
assert append_kwargs["content"] == ""
|
||||
assert append_kwargs["model_code"] == "doubao-seed-1-6-250615"
|
||||
assert append_kwargs["input_tokens"] == 12
|
||||
assert append_kwargs["output_tokens"] == 8
|
||||
assert append_kwargs["latency_ms"] == 320
|
||||
assert append_kwargs["cost"] == Decimal("0.01")
|
||||
assert append_kwargs["visibility_mask"] == 0
|
||||
|
||||
metadata = cast(dict[str, Any], append_kwargs["metadata"])
|
||||
assert sorted(metadata.keys()) == ["agent_type", "router_agent_output", "run_id"]
|
||||
assert metadata["agent_type"] == "router"
|
||||
assert metadata["router_agent_output"]["execution_mode"] == "tool_assisted"
|
||||
|
||||
assert captured["message_delta"] == 1
|
||||
assert captured["token_delta"] == 20
|
||||
assert captured["cost_delta"] == Decimal("0.01")
|
||||
|
||||
@@ -114,6 +114,39 @@ def test_build_router_messages_skips_injection_when_context_last_is_user() -> No
|
||||
assert msg.content == existing_context[i].content
|
||||
|
||||
|
||||
def test_build_model_omits_none_generate_kwargs(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
class _FakeOpenAIChatModel:
|
||||
def __init__(self, **kwargs: object) -> None:
|
||||
captured.update(kwargs)
|
||||
|
||||
monkeypatch.setattr(runner_module, "OpenAIChatModel", _FakeOpenAIChatModel)
|
||||
|
||||
runner = AgentScopeRunner()
|
||||
stage_config = runner_module.SystemAgentRuntimeConfig(
|
||||
agent_type=AgentType.ROUTER,
|
||||
model_code="demo",
|
||||
api_base_url="https://example.com",
|
||||
api_key="test",
|
||||
llm_config=runner_module.SystemAgentLLMConfig(
|
||||
temperature=None,
|
||||
max_tokens=None,
|
||||
timeout_seconds=30.0,
|
||||
),
|
||||
)
|
||||
|
||||
model = runner._build_model(stage_config=stage_config)
|
||||
|
||||
assert isinstance(model, runner_module.TrackingChatModel)
|
||||
assert captured["generate_kwargs"] == {
|
||||
"timeout": 30.0,
|
||||
"extra_body": {"enable_thinking": False},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_runtime_client_time_from_forwarded_props() -> None:
|
||||
runner = AgentScopeRunner()
|
||||
|
||||
@@ -27,6 +27,7 @@ class _FakeService:
|
||||
created_request: Any = None
|
||||
created_id: str = field(default_factory=lambda: str(uuid4()))
|
||||
list_calls: list[dict[str, Any]] = field(default_factory=list)
|
||||
deleted_ids: list[str] = field(default_factory=list)
|
||||
|
||||
async def list_paginated(
|
||||
self, *, page: int, page_size: int, query: str | None = None
|
||||
@@ -57,10 +58,20 @@ class _FakeService:
|
||||
metadata=request.metadata,
|
||||
)
|
||||
|
||||
async def delete(self, item_id: UUID) -> None:
|
||||
self.deleted_ids.append(str(item_id))
|
||||
|
||||
async def share(self, item_id: UUID, request: Any) -> None:
|
||||
if not hasattr(self, "share_calls"):
|
||||
self.share_calls = []
|
||||
self.share_calls.append({"item_id": str(item_id), "request": request})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_write_requires_runtime_context() -> None:
|
||||
result = await calendar_module.calendar_write(operations=["create"])
|
||||
result = await calendar_module.calendar_write(
|
||||
operations=[calendar_module.CalendarWriteOperation(action="create")]
|
||||
)
|
||||
payload = _decode_tool_response(result)
|
||||
|
||||
assert payload["status"] == "failure"
|
||||
@@ -77,8 +88,12 @@ async def test_calendar_write_create_requires_start_at(
|
||||
)
|
||||
|
||||
result = await calendar_module.calendar_write(
|
||||
operations=["create"],
|
||||
event_timezones=["Asia/Shanghai"],
|
||||
operations=[
|
||||
calendar_module.CalendarWriteOperation(
|
||||
action="create",
|
||||
event_timezone="Asia/Shanghai",
|
||||
)
|
||||
],
|
||||
session=SimpleNamespace(),
|
||||
owner_id=uuid4(),
|
||||
)
|
||||
@@ -99,8 +114,12 @@ async def test_calendar_write_create_requires_event_timezone(
|
||||
)
|
||||
|
||||
result = await calendar_module.calendar_write(
|
||||
operations=["create"],
|
||||
start_ats=["2026-03-16T09:00:00+08:00"],
|
||||
operations=[
|
||||
calendar_module.CalendarWriteOperation(
|
||||
action="create",
|
||||
start_at="2026-03-16T09:00:00+08:00",
|
||||
)
|
||||
],
|
||||
session=SimpleNamespace(),
|
||||
owner_id=uuid4(),
|
||||
)
|
||||
@@ -121,9 +140,13 @@ async def test_calendar_write_rejects_naive_start_at(
|
||||
)
|
||||
|
||||
result = await calendar_module.calendar_write(
|
||||
operations=["create"],
|
||||
start_ats=["2026-03-16T09:00:00"],
|
||||
event_timezones=["Asia/Shanghai"],
|
||||
operations=[
|
||||
calendar_module.CalendarWriteOperation(
|
||||
action="create",
|
||||
start_at="2026-03-16T09:00:00",
|
||||
event_timezone="Asia/Shanghai",
|
||||
)
|
||||
],
|
||||
session=SimpleNamespace(),
|
||||
owner_id=uuid4(),
|
||||
)
|
||||
@@ -144,11 +167,15 @@ async def test_calendar_write_create_normalizes_to_utc(
|
||||
)
|
||||
|
||||
result = await calendar_module.calendar_write(
|
||||
operations=["create"],
|
||||
titles=["晨会"],
|
||||
start_ats=["2026-03-16T09:00:00+08:00"],
|
||||
end_ats=["2026-03-16T10:00:00+08:00"],
|
||||
event_timezones=["Asia/Shanghai"],
|
||||
operations=[
|
||||
calendar_module.CalendarWriteOperation(
|
||||
action="create",
|
||||
title="晨会",
|
||||
start_at="2026-03-16T09:00:00+08:00",
|
||||
end_at="2026-03-16T10:00:00+08:00",
|
||||
event_timezone="Asia/Shanghai",
|
||||
)
|
||||
],
|
||||
session=SimpleNamespace(),
|
||||
owner_id=uuid4(),
|
||||
)
|
||||
@@ -166,7 +193,7 @@ async def test_calendar_write_create_normalizes_to_utc(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_write_rejects_misaligned_batch_lists(
|
||||
async def test_calendar_write_batch_supports_create_and_delete(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
fake_service = _FakeService()
|
||||
@@ -175,17 +202,26 @@ async def test_calendar_write_rejects_misaligned_batch_lists(
|
||||
)
|
||||
|
||||
result = await calendar_module.calendar_write(
|
||||
operations=["create", "delete"],
|
||||
start_ats=["2026-03-16T09:00:00+08:00"],
|
||||
event_timezones=["Asia/Shanghai", "Asia/Shanghai"],
|
||||
operations=[
|
||||
calendar_module.CalendarWriteOperation(
|
||||
action="create",
|
||||
title="晨会",
|
||||
start_at="2026-03-16T09:00:00+08:00",
|
||||
event_timezone="Asia/Shanghai",
|
||||
),
|
||||
calendar_module.CalendarWriteOperation(
|
||||
action="delete",
|
||||
event_id=str(uuid4()),
|
||||
),
|
||||
],
|
||||
session=SimpleNamespace(),
|
||||
owner_id=uuid4(),
|
||||
)
|
||||
payload = _decode_tool_response(result)
|
||||
|
||||
assert payload["status"] == "failure"
|
||||
assert payload["error"]["code"] == "INVALID_ARGUMENT"
|
||||
assert "长度必须与 operations 一致" in payload["error"]["message"]
|
||||
assert payload["status"] == "success"
|
||||
assert "success=2" in payload["result"]
|
||||
assert len(fake_service.deleted_ids) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -214,3 +250,45 @@ async def test_calendar_read_returns_structured_result_with_ids(
|
||||
assert "status=" in payload["result"]
|
||||
assert fake_service.created_id in payload["result"]
|
||||
assert fake_service.list_calls == [{"page": 1, "page_size": 20, "query": "会议"}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_share_executes_with_valid_invitee(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
fake_service = _FakeService()
|
||||
monkeypatch.setattr(
|
||||
calendar_module, "create_schedule_service", lambda *_: fake_service
|
||||
)
|
||||
target_user_id = str(uuid4())
|
||||
monkeypatch.setattr(
|
||||
calendar_module,
|
||||
"resolve_share_target_phone_map",
|
||||
lambda user_ids: {target_user_id: "+8613900001234"}
|
||||
if target_user_id in user_ids
|
||||
else {},
|
||||
)
|
||||
|
||||
event_id = str(uuid4())
|
||||
result = await calendar_module.calendar_share(
|
||||
event_id=event_id,
|
||||
invitees=[
|
||||
calendar_module.CalendarShareInvitee(
|
||||
userId=target_user_id,
|
||||
permissionView=True,
|
||||
permissionEdit=False,
|
||||
permissionInvite=False,
|
||||
)
|
||||
],
|
||||
session=SimpleNamespace(),
|
||||
owner_id=uuid4(),
|
||||
)
|
||||
payload = _decode_tool_response(result)
|
||||
|
||||
assert payload["status"] == "success"
|
||||
assert payload["result"].startswith("status=success invited_count=1")
|
||||
assert "+8613900001234" in payload["result"]
|
||||
assert len(fake_service.share_calls) == 1
|
||||
share_call = fake_service.share_calls[0]
|
||||
assert share_call["item_id"] == event_id
|
||||
assert share_call["request"].phone == "+8613900001234"
|
||||
|
||||
@@ -60,8 +60,12 @@ def _user_memory():
|
||||
@pytest.mark.asyncio
|
||||
async def test_memory_write_requires_runtime_context() -> None:
|
||||
response = await memory_module.memory_write(
|
||||
memory_type="user",
|
||||
user_content=UserMemoryContent(interests=["跑步"]),
|
||||
operations=[
|
||||
memory_module.MemoryWriteArgs(
|
||||
memory_type=MemoryType.USER,
|
||||
user_content=UserMemoryContent(interests=["跑步"]),
|
||||
)
|
||||
],
|
||||
)
|
||||
payload = _decode_tool_response(response)
|
||||
assert payload["status"] == "failure"
|
||||
@@ -78,15 +82,20 @@ async def test_memory_write_updates_user_content(
|
||||
)
|
||||
|
||||
response = await memory_module.memory_write(
|
||||
memory_type="user",
|
||||
user_content=UserMemoryContent(interests=["阅读"]),
|
||||
operations=[
|
||||
memory_module.MemoryWriteArgs(
|
||||
memory_type=MemoryType.USER,
|
||||
user_content=UserMemoryContent(interests=["阅读"]),
|
||||
)
|
||||
],
|
||||
session=SimpleNamespace(),
|
||||
owner_id=uuid4(),
|
||||
)
|
||||
payload = _decode_tool_response(response)
|
||||
|
||||
assert payload["status"] == "success"
|
||||
assert "memory_type=user" in str(payload["result"])
|
||||
assert "success=1" in str(payload["result"])
|
||||
assert "updated_types=[user]" in str(payload["result"])
|
||||
assert fake_service.updated_user == 1
|
||||
|
||||
|
||||
@@ -101,13 +110,61 @@ async def test_memory_forget_updates_content_paths(
|
||||
)
|
||||
|
||||
response = await memory_module.memory_forget(
|
||||
memory_type="user",
|
||||
forget_paths=["preferences.communication_style"],
|
||||
operations=[
|
||||
memory_module.MemoryForgetArgs(
|
||||
memory_type=MemoryType.USER,
|
||||
forget_paths=["preferences.communication_style"],
|
||||
)
|
||||
],
|
||||
session=SimpleNamespace(),
|
||||
owner_id=uuid4(),
|
||||
)
|
||||
payload = _decode_tool_response(response)
|
||||
|
||||
assert payload["status"] == "success"
|
||||
assert "success=1" in str(payload["result"])
|
||||
assert "forgotten=1" in str(payload["result"])
|
||||
assert fake_service.updated_user == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_memory_write_partial_status_contains_error_details(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
fake_service = _FakeMemoriesService()
|
||||
call_count = 0
|
||||
|
||||
async def _update_user_memory(**kwargs):
|
||||
nonlocal call_count
|
||||
_ = kwargs
|
||||
call_count += 1
|
||||
if call_count == 2:
|
||||
raise ValueError("invalid payload")
|
||||
fake_service.updated_user += 1
|
||||
return SimpleNamespace()
|
||||
|
||||
fake_service.update_user_memory = _update_user_memory # type: ignore[method-assign]
|
||||
monkeypatch.setattr(
|
||||
memory_module, "create_memories_service", lambda **_: fake_service
|
||||
)
|
||||
|
||||
response = await memory_module.memory_write(
|
||||
operations=[
|
||||
memory_module.MemoryWriteArgs(
|
||||
memory_type=MemoryType.USER,
|
||||
user_content=UserMemoryContent(interests=["阅读"]),
|
||||
),
|
||||
memory_module.MemoryWriteArgs(
|
||||
memory_type=MemoryType.USER,
|
||||
user_content=UserMemoryContent(interests=["跑步"]),
|
||||
),
|
||||
],
|
||||
session=SimpleNamespace(),
|
||||
owner_id=uuid4(),
|
||||
)
|
||||
payload = _decode_tool_response(response)
|
||||
|
||||
assert payload["status"] == "partial"
|
||||
assert "status=partial" in str(payload["result"])
|
||||
assert "failed=1" in str(payload["result"])
|
||||
assert _payload_error_code(payload) in {"INVALID_ARGUMENT", "UNKNOWN_ERROR"}
|
||||
|
||||
Reference in New Issue
Block a user