mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-19 08:14:40 +00:00
fix: better snapshot and gate
This commit is contained in:
1 parent
29f661f3c7
commit
3f88dc7b57
12 files changed
+449
-22
No files matched your search
@@ -1367,6 +1367,134 @@ class TestHandleStreaming:
|
||||
# The whole point: the merged payload parses.
|
||||
_json.loads(executed_call.arguments)
|
||||
|
||||
def test_a_distinct_call_reusing_an_index_is_not_treated_as_a_restatement(
|
||||
self, caplog
|
||||
):
|
||||
"""Two complete payloads under one index may be two different calls.
|
||||
|
||||
Some OpenAI-compatible gateways tag sequential parallel calls with the
|
||||
same ``index``. That is indistinguishable from a restatement by payload
|
||||
shape alone, so the identity decides — and it must be read BEFORE the
|
||||
id/name overwrite, which would otherwise erase the evidence. Falling
|
||||
back to concatenation keeps the failure loud: ``tool_action_parser``
|
||||
logs the unparseable prefix, where a silent replace would run one tool
|
||||
with the other's arguments and report the turn complete.
|
||||
"""
|
||||
import logging as _logging
|
||||
|
||||
handler = ConcreteHandler()
|
||||
agent = Mock()
|
||||
agent.llm = Mock()
|
||||
agent.model_id = "test"
|
||||
agent.tools = []
|
||||
agent._check_context_limit = Mock(return_value=False)
|
||||
agent.context_limit_reached = False
|
||||
agent.llm.__class__.__name__ = "MockLLM"
|
||||
agent.tool_executor.check_pause = Mock(return_value=None)
|
||||
|
||||
chunk1 = LLMResponse(
|
||||
content="",
|
||||
tool_calls=[
|
||||
ToolCall(id="call_A", name="memory_view", arguments='{"a":1}', index=0)
|
||||
],
|
||||
finish_reason="",
|
||||
raw_response={},
|
||||
)
|
||||
chunk2 = LLMResponse(
|
||||
content="",
|
||||
tool_calls=[
|
||||
ToolCall(id="call_B", name="memory_write", arguments='{"b":2}', index=0)
|
||||
],
|
||||
finish_reason="tool_calls",
|
||||
raw_response={},
|
||||
)
|
||||
|
||||
handler.parse_response = lambda c: c
|
||||
|
||||
def fake_iterate(response):
|
||||
yield from response
|
||||
|
||||
handler._iterate_stream = fake_iterate
|
||||
|
||||
def fake_execute(tools_dict, call):
|
||||
yield {"type": "tool_call", "data": {"status": "pending"}}
|
||||
return ("result", call.id)
|
||||
|
||||
agent._execute_tool_action = Mock(side_effect=fake_execute)
|
||||
agent.llm.gen_stream = Mock(
|
||||
return_value=[
|
||||
LLMResponse(
|
||||
content="done", tool_calls=[], finish_reason="stop", raw_response={}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
with caplog.at_level(_logging.WARNING, logger="application.llm.handlers.base"):
|
||||
list(
|
||||
handler.handle_streaming(agent, [chunk1, chunk2], {"1": {"name": "t"}}, [])
|
||||
)
|
||||
|
||||
executed_call = agent._execute_tool_action.call_args[0][1]
|
||||
# Concatenated, not silently replaced: call A's payload survives in a
|
||||
# form the parser reports rather than vanishing.
|
||||
assert executed_call.arguments == '{"a":1}{"b":2}'
|
||||
assert "tool_call_index_reused_by_distinct_call" in caplog.text
|
||||
|
||||
def test_a_restatement_carrying_the_same_identity_still_replaces(self):
|
||||
"""The guard must not undo the fix: same id/name means one call."""
|
||||
handler = ConcreteHandler()
|
||||
agent = Mock()
|
||||
agent.llm = Mock()
|
||||
agent.model_id = "test"
|
||||
agent.tools = []
|
||||
agent._check_context_limit = Mock(return_value=False)
|
||||
agent.context_limit_reached = False
|
||||
agent.llm.__class__.__name__ = "MockLLM"
|
||||
agent.tool_executor.check_pause = Mock(return_value=None)
|
||||
|
||||
chunk1 = LLMResponse(
|
||||
content="",
|
||||
tool_calls=[
|
||||
ToolCall(id="c1", name="search", arguments='{"q":"x"}', index=0)
|
||||
],
|
||||
finish_reason="",
|
||||
raw_response={},
|
||||
)
|
||||
# The finish frame repeats the identity as well as the payload.
|
||||
chunk2 = LLMResponse(
|
||||
content="",
|
||||
tool_calls=[
|
||||
ToolCall(id="c1", name="search", arguments='{"q":"x"}', index=0)
|
||||
],
|
||||
finish_reason="tool_calls",
|
||||
raw_response={},
|
||||
)
|
||||
|
||||
handler.parse_response = lambda c: c
|
||||
|
||||
def fake_iterate(response):
|
||||
yield from response
|
||||
|
||||
handler._iterate_stream = fake_iterate
|
||||
|
||||
def fake_execute(tools_dict, call):
|
||||
yield {"type": "tool_call", "data": {"status": "pending"}}
|
||||
return ("result", call.id)
|
||||
|
||||
agent._execute_tool_action = Mock(side_effect=fake_execute)
|
||||
agent.llm.gen_stream = Mock(
|
||||
return_value=[
|
||||
LLMResponse(
|
||||
content="done", tool_calls=[], finish_reason="stop", raw_response={}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
list(handler.handle_streaming(agent, [chunk1, chunk2], {"1": {"name": "t"}}, []))
|
||||
|
||||
executed_call = agent._execute_tool_action.call_args[0][1]
|
||||
assert executed_call.arguments == '{"q":"x"}'
|
||||
|
||||
def test_context_limit_adds_system_message(self):
|
||||
handler = ConcreteHandler()
|
||||
agent = Mock()
|
||||
|
||||
Reference in new issue
Block a user