183 lines
5.6 KiB
Python
183 lines
5.6 KiB
Python
from __future__ import annotations
|
|
|
|
from copy import deepcopy
|
|
from typing import Any, cast
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from src.config import ModelConfig
|
|
from src.llm import tool_loop
|
|
from src.llm.runtime import AttemptPlan
|
|
from src.llm.tool_loop import execute_tool_loop
|
|
from src.llm.types import HonchoLLMCallResponse, ProviderClient
|
|
|
|
|
|
def _make_plan() -> AttemptPlan:
|
|
return AttemptPlan(
|
|
provider="openai",
|
|
model="deepseek-v4-pro",
|
|
client=cast(ProviderClient, object()),
|
|
thinking_budget_tokens=None,
|
|
reasoning_effort=None,
|
|
selected_config=ModelConfig(
|
|
model="deepseek-v4-pro",
|
|
transport="openai",
|
|
),
|
|
attempt=1,
|
|
retry_attempts=1,
|
|
is_fallback=False,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tool_loop_replays_reasoning_content_on_continuation() -> None:
|
|
calls: list[list[dict[str, Any]]] = []
|
|
responses = iter(
|
|
[
|
|
HonchoLLMCallResponse(
|
|
content="",
|
|
output_tokens=5,
|
|
finish_reasons=["tool_calls"],
|
|
tool_calls_made=[
|
|
{
|
|
"id": "call_1",
|
|
"name": "search",
|
|
"input": {"query": "honcho"},
|
|
}
|
|
],
|
|
thinking_content="DeepSeek reasoning",
|
|
),
|
|
HonchoLLMCallResponse(
|
|
content="done",
|
|
output_tokens=3,
|
|
finish_reasons=["stop"],
|
|
tool_calls_made=[],
|
|
),
|
|
]
|
|
)
|
|
|
|
async def fake_call(*_args: Any, **kwargs: Any) -> HonchoLLMCallResponse[Any]:
|
|
calls.append(deepcopy(kwargs["messages"]))
|
|
return next(responses)
|
|
|
|
async def execute_search(_name: str, _input: dict[str, Any]) -> str:
|
|
return "result"
|
|
|
|
with patch.object(tool_loop, "honcho_llm_call_inner", new=fake_call):
|
|
result = await execute_tool_loop(
|
|
prompt="hi",
|
|
max_tokens=64,
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
tools=[
|
|
{
|
|
"name": "search",
|
|
"description": "Search",
|
|
"input_schema": {"type": "object"},
|
|
}
|
|
],
|
|
tool_choice="auto",
|
|
tool_executor=execute_search,
|
|
max_tool_iterations=5,
|
|
response_model=None,
|
|
json_mode=False,
|
|
temperature=None,
|
|
stop_seqs=None,
|
|
verbosity=None,
|
|
enable_retry=False,
|
|
retry_attempts=1,
|
|
max_input_tokens=None,
|
|
get_attempt_plan=_make_plan,
|
|
before_retry_callback=lambda _retry_state: None,
|
|
stream_final=False,
|
|
telemetry=None,
|
|
)
|
|
|
|
assert isinstance(result, HonchoLLMCallResponse)
|
|
assert len(calls) == 2
|
|
assert calls[1][1]["reasoning_content"] == "DeepSeek reasoning"
|
|
assert calls[1][1]["tool_calls"][0]["function"]["name"] == "search"
|
|
assert calls[1][2] == {
|
|
"role": "tool",
|
|
"tool_call_id": "call_1",
|
|
"content": "result",
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tool_loop_replays_null_assistant_content_on_continuation() -> None:
|
|
calls: list[list[dict[str, Any]]] = []
|
|
responses = iter(
|
|
[
|
|
HonchoLLMCallResponse(
|
|
content=None,
|
|
output_tokens=5,
|
|
finish_reasons=["tool_calls"],
|
|
tool_calls_made=[
|
|
{
|
|
"id": "call_1",
|
|
"name": "search",
|
|
"input": {"query": "honcho"},
|
|
}
|
|
],
|
|
reasoning_details=[
|
|
{
|
|
"type": "reasoning.encrypted",
|
|
"data": "opaque",
|
|
"format": "openai-responses-v1",
|
|
"id": "binding",
|
|
"index": 0,
|
|
}
|
|
],
|
|
),
|
|
HonchoLLMCallResponse(
|
|
content="done",
|
|
output_tokens=3,
|
|
finish_reasons=["stop"],
|
|
tool_calls_made=[],
|
|
),
|
|
]
|
|
)
|
|
|
|
async def fake_call(*_args: Any, **kwargs: Any) -> HonchoLLMCallResponse[Any]:
|
|
calls.append(deepcopy(kwargs["messages"]))
|
|
return next(responses)
|
|
|
|
async def execute_search(_name: str, _input: dict[str, Any]) -> str:
|
|
return "result"
|
|
|
|
with patch.object(tool_loop, "honcho_llm_call_inner", new=fake_call):
|
|
result = await execute_tool_loop(
|
|
prompt="hi",
|
|
max_tokens=64,
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
tools=[
|
|
{
|
|
"name": "search",
|
|
"description": "Search",
|
|
"input_schema": {"type": "object"},
|
|
}
|
|
],
|
|
tool_choice="auto",
|
|
tool_executor=execute_search,
|
|
max_tool_iterations=5,
|
|
response_model=None,
|
|
json_mode=False,
|
|
temperature=None,
|
|
stop_seqs=None,
|
|
verbosity=None,
|
|
enable_retry=False,
|
|
retry_attempts=1,
|
|
max_input_tokens=None,
|
|
get_attempt_plan=_make_plan,
|
|
before_retry_callback=lambda _retry_state: None,
|
|
stream_final=False,
|
|
telemetry=None,
|
|
)
|
|
|
|
assert isinstance(result, HonchoLLMCallResponse)
|
|
assert len(calls) == 2
|
|
assert calls[1][1]["content"] is None
|
|
assert calls[1][1]["reasoning_details"][0]["data"] == "opaque"
|
|
assert calls[1][1]["tool_calls"][0]["function"]["name"] == "search"
|