refactor(minimax): use run_with_tool_loop shared helper (68 -> 44 lines)
Task 1.3 of the follow-up track. _send_minimax now uses
run_with_tool_loop with a per-round request_builder callback
that re-reads _minimax_history under _minimax_history_lock.
The plan's Task 1.3 example builds the request once before the
loop. That would break MiniMax tool flows because the API
would not see the tool results appended to _minimax_history
on later rounds. The fix: extend run_with_tool_loop's 2nd arg
to accept Union[OpenAICompatibleRequest, Callable[[int],
OpenAICompatibleRequest]] (backward compatible; static-request
vendors pass a single request). MiniMax now passes a closure
that rebuilds messages from history each round.
Reasoning extraction: MiniMax exposes its chain-of-thought via
response.raw_response.choices[0].message.reasoning_details[0].
get('text'). Lifted to a _extract_minimax_reasoning callback
passed as reasoning_extractor=... (the new parameter added
in the previous commit).
Trim callback: wraps _trim_minimax_history so it can be called
from run_with_tool_loop after each tool-result append.
Green confirmed: 51 vendor + tool tests pass (6 MiniMax + 5
tool_loop core + 1 tool_loop builder + 39 others); the new
test_ai_client_tool_loop_builder.py locks in the per-round
builder contract.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
"""Verify run_with_tool_loop supports a per-round request_builder callback.
|
||||
|
||||
Vendors that mutate their history list (e.g. MiniMax) need to rebuild
|
||||
the messages on each round so the API sees the latest tool results.
|
||||
run_with_tool_loop accepts a callable as the 2nd arg to enable this.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
from src.openai_compatible import NormalizedResponse, OpenAICompatibleRequest
|
||||
from src.ai_client import run_with_tool_loop
|
||||
from src.vendor_capabilities import VendorCapabilities
|
||||
|
||||
def _make_normalized_response(text: str = "ok", tool_calls: list[dict[str, Any]] | None = None) -> NormalizedResponse:
|
||||
return NormalizedResponse(
|
||||
text=text, tool_calls=tool_calls or [],
|
||||
usage_input_tokens=10, usage_output_tokens=5,
|
||||
usage_cache_read_tokens=0, usage_cache_creation_tokens=0,
|
||||
raw_response=None,
|
||||
)
|
||||
|
||||
def test_run_with_tool_loop_calls_request_builder_each_round() -> None:
|
||||
caps = VendorCapabilities(vendor="test", model="test-model", tool_calling=True, context_window=8192)
|
||||
client = MagicMock()
|
||||
tool_response = _make_normalized_response(
|
||||
"first", tool_calls=[{"id": "c1", "type": "function", "function": {"name": "noop", "arguments": "{}"}}]
|
||||
)
|
||||
final = _make_normalized_response("done")
|
||||
builder_calls: list[int] = []
|
||||
def builder(round_idx: int) -> OpenAICompatibleRequest:
|
||||
builder_calls.append(round_idx)
|
||||
return OpenAICompatibleRequest(messages=[{"role": "user", "content": f"round={round_idx}"}], model="m")
|
||||
with patch("src.ai_client.send_openai_compatible", side_effect=[tool_response, final]), \
|
||||
patch("src.ai_client._execute_tool_calls_concurrently", return_value=[("noop", "c1", "r", "")]):
|
||||
result = run_with_tool_loop(
|
||||
client, builder, capabilities=caps,
|
||||
pre_tool_callback=None, qa_callback=None, patch_callback=None,
|
||||
base_dir=".", vendor_name="test", history_lock=None, history=None,
|
||||
)
|
||||
assert result == "done"
|
||||
assert len(builder_calls) >= 2
|
||||
Reference in New Issue
Block a user