mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-18 10:00:40 +08:00
chore: sync master into dev
This commit is contained in:
@@ -137,6 +137,41 @@ class MockMixedContentToolExecutor:
|
||||
yield result
|
||||
|
||||
|
||||
class VaryingUsageProvider(MockProvider):
|
||||
"""Return distinct token usage values for each tool-loop request."""
|
||||
|
||||
async def text_chat(self, **kwargs) -> LLMResponse:
|
||||
self.call_count += 1
|
||||
usage = TokenUsage(
|
||||
input_other=self.call_count * 100,
|
||||
input_cached=self.call_count * 10,
|
||||
output=self.call_count,
|
||||
)
|
||||
if self.call_count == 1:
|
||||
return LLMResponse(
|
||||
role="assistant",
|
||||
tools_call_name=["test_tool"],
|
||||
tools_call_args=[{"query": "test"}],
|
||||
tools_call_ids=["call_varying_usage"],
|
||||
usage=usage,
|
||||
)
|
||||
return LLMResponse(
|
||||
role="assistant",
|
||||
completion_text="final",
|
||||
usage=usage,
|
||||
)
|
||||
|
||||
|
||||
class MissingFinalUsageProvider(VaryingUsageProvider):
|
||||
"""Omit usage from the final response after reporting an earlier request."""
|
||||
|
||||
async def text_chat(self, **kwargs) -> LLMResponse:
|
||||
response = await super().text_chat(**kwargs)
|
||||
if self.call_count == 2:
|
||||
response.usage = None
|
||||
return response
|
||||
|
||||
|
||||
class MockFailingProvider(MockProvider):
|
||||
async def text_chat(self, **kwargs) -> LLMResponse:
|
||||
self.call_count += 1
|
||||
@@ -587,6 +622,114 @@ async def test_normal_completion_without_max_step(
|
||||
assert runner.req.func_tool is not None, "正常完成时工具不应该被禁用"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("streaming", [False, True])
|
||||
async def test_stats_separate_latest_context_from_cumulative_usage(
|
||||
runner, provider_request, mock_tool_executor, mock_hooks, streaming
|
||||
):
|
||||
"""Context occupancy uses the latest input while usage remains cumulative."""
|
||||
provider = VaryingUsageProvider()
|
||||
await runner.reset(
|
||||
provider=provider,
|
||||
request=provider_request,
|
||||
run_context=ContextWrapper(context=None),
|
||||
tool_executor=mock_tool_executor,
|
||||
agent_hooks=mock_hooks,
|
||||
streaming=streaming,
|
||||
)
|
||||
|
||||
async for _ in runner.step_until_done(3):
|
||||
pass
|
||||
|
||||
assert provider.call_count == 2
|
||||
assert runner.stats.token_usage == TokenUsage(
|
||||
input_other=300,
|
||||
input_cached=30,
|
||||
output=3,
|
||||
)
|
||||
assert runner.stats.current_context_tokens == 220
|
||||
assert runner.stats.to_dict()["current_context_tokens"] == 220
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("streaming", [False, True])
|
||||
async def test_stats_emit_update_after_each_completed_llm_request(
|
||||
runner, provider_request, mock_tool_executor, mock_hooks, streaming
|
||||
):
|
||||
"""Emit one stats update for every completed LLM request."""
|
||||
provider = VaryingUsageProvider()
|
||||
await runner.reset(
|
||||
provider=provider,
|
||||
request=provider_request,
|
||||
run_context=ContextWrapper(context=None),
|
||||
tool_executor=mock_tool_executor,
|
||||
agent_hooks=mock_hooks,
|
||||
streaming=streaming,
|
||||
)
|
||||
|
||||
responses = [response async for response in runner.step_until_done(3)]
|
||||
stats_responses = [
|
||||
response for response in responses if response.type == "agent_stats"
|
||||
]
|
||||
|
||||
assert provider.call_count == 2
|
||||
assert len(stats_responses) == 2
|
||||
assert [response.data["chain"].type for response in stats_responses] == [
|
||||
"agent_stats",
|
||||
"agent_stats",
|
||||
]
|
||||
stats_snapshots = [
|
||||
response.data["chain"].chain[0].data for response in stats_responses
|
||||
]
|
||||
assert [snapshot["current_context_tokens"] for snapshot in stats_snapshots] == [
|
||||
110,
|
||||
220,
|
||||
]
|
||||
assert stats_snapshots[0]["token_usage"]["input_other"] == 100
|
||||
assert stats_snapshots[0]["token_usage"]["input_cached"] == 10
|
||||
assert stats_snapshots[1]["token_usage"]["input_other"] == 300
|
||||
assert stats_snapshots[1]["token_usage"]["input_cached"] == 30
|
||||
|
||||
# Emitted events keep their own snapshots even if live stats mutate later.
|
||||
runner.stats.token_usage.input_other = 999
|
||||
assert stats_snapshots[0]["token_usage"]["input_other"] == 100
|
||||
assert stats_snapshots[1]["token_usage"]["input_other"] == 300
|
||||
|
||||
assert responses.index(stats_responses[0]) < next(
|
||||
index
|
||||
for index, response in enumerate(responses)
|
||||
if response.type == "tool_call"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("streaming", [False, True])
|
||||
async def test_stats_clear_current_context_when_latest_usage_is_missing(
|
||||
runner, provider_request, mock_tool_executor, mock_hooks, streaming
|
||||
):
|
||||
"""Do not expose stale context occupancy when the latest usage is unknown."""
|
||||
provider = MissingFinalUsageProvider()
|
||||
await runner.reset(
|
||||
provider=provider,
|
||||
request=provider_request,
|
||||
run_context=ContextWrapper(context=None),
|
||||
tool_executor=mock_tool_executor,
|
||||
agent_hooks=mock_hooks,
|
||||
streaming=streaming,
|
||||
)
|
||||
|
||||
async for _ in runner.step_until_done(3):
|
||||
pass
|
||||
|
||||
assert runner.stats.token_usage == TokenUsage(
|
||||
input_other=100,
|
||||
input_cached=10,
|
||||
output=1,
|
||||
)
|
||||
assert runner.stats.current_context_tokens == 0
|
||||
assert runner.stats.to_dict()["current_context_tokens"] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_max_step_with_streaming(
|
||||
runner, mock_provider, provider_request, mock_tool_executor, mock_hooks
|
||||
@@ -1144,6 +1287,8 @@ async def test_stop_interrupts_pending_subagent_handoff(mock_hooks):
|
||||
|
||||
step_iter = runner.step()
|
||||
first_resp = await step_iter.__anext__()
|
||||
if first_resp.type == "agent_stats":
|
||||
first_resp = await step_iter.__anext__()
|
||||
assert first_resp.type == "tool_call"
|
||||
assert provider.abort_signal is not None
|
||||
assert provider.abort_signal.is_set() is False
|
||||
@@ -1194,6 +1339,8 @@ async def test_stop_interrupts_pending_regular_tool(mock_hooks):
|
||||
|
||||
step_iter = runner.step()
|
||||
first_resp = await step_iter.__anext__()
|
||||
if first_resp.type == "agent_stats":
|
||||
first_resp = await step_iter.__anext__()
|
||||
assert first_resp.type == "tool_call"
|
||||
assert provider.abort_signal is not None
|
||||
assert provider.abort_signal.is_set() is False
|
||||
|
||||
Reference in New Issue
Block a user