refactor: 梳理规则体系并统一记忆与部署流程

This commit is contained in:
qzl
2026-03-23 17:57:24 +08:00
parent 2a14ad1d8e
commit f4b7eb7e09
39 changed files with 2091 additions and 1454 deletions
@@ -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"}