mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-04-30 06:45:55 +00:00
Extend `_merge_consecutive` so the three invariants from `LLMProvider._enforce_role_alternation` all hold for Anthropic: 1. collapse consecutive same-role turns (unchanged) 2. no trailing assistant — Anthropic rejects prefill (unchanged) 3. no leading assistant — Anthropic requires the first turn be user 4. non-empty messages array — recover the last stripped assistant as a user turn when every turn got stripped, so callers don't hit a secondary "messages array empty" 400 Anthropic-specific wrinkle: `tool_use` blocks live inside `content` (not a separate `tool_calls` field) and are illegal inside user turns, so both recovery paths skip any message carrying them rather than silently producing a malformed request. Adds 4 unit tests covering the new branches, including the tool_use opt-outs, and updates the existing `test_single_assistant_stripped` to reflect the new rerouting contract. Made-with: Cursor
140 lines
5.6 KiB
Python
140 lines
5.6 KiB
Python
"""Tests for AnthropicProvider._merge_consecutive."""
|
|
|
|
from nanobot.providers.anthropic_provider import AnthropicProvider
|
|
|
|
|
|
class TestMergeConsecutive:
|
|
"""Verify role alternation and trailing-assistant stripping."""
|
|
|
|
def test_basic_alternation(self):
|
|
msgs = [
|
|
{"role": "user", "content": "hello"},
|
|
{"role": "assistant", "content": "hi"},
|
|
{"role": "user", "content": "bye"},
|
|
]
|
|
result = AnthropicProvider._merge_consecutive(msgs)
|
|
assert len(result) == 3
|
|
assert [m["role"] for m in result] == ["user", "assistant", "user"]
|
|
|
|
def test_consecutive_same_role_merged(self):
|
|
msgs = [
|
|
{"role": "user", "content": "a"},
|
|
{"role": "user", "content": "b"},
|
|
{"role": "assistant", "content": "reply"},
|
|
]
|
|
result = AnthropicProvider._merge_consecutive(msgs)
|
|
# Two user messages merged into one, trailing assistant stripped
|
|
assert len(result) == 1
|
|
assert result[0]["role"] == "user"
|
|
|
|
def test_trailing_assistant_stripped(self):
|
|
"""Anthropic rejects prefill — trailing assistant must be removed."""
|
|
msgs = [
|
|
{"role": "user", "content": "hello"},
|
|
{"role": "assistant", "content": "hi"},
|
|
]
|
|
result = AnthropicProvider._merge_consecutive(msgs)
|
|
assert len(result) == 1
|
|
assert result[0]["role"] == "user"
|
|
assert result[0]["content"] == "hello"
|
|
|
|
def test_multiple_trailing_assistant_stripped(self):
|
|
msgs = [
|
|
{"role": "user", "content": "hello"},
|
|
{"role": "assistant", "content": "a"},
|
|
{"role": "user", "content": "ok"},
|
|
{"role": "assistant", "content": "b"},
|
|
{"role": "assistant", "content": "c"},
|
|
]
|
|
result = AnthropicProvider._merge_consecutive(msgs)
|
|
# b+c merged into one assistant, then stripped as trailing
|
|
assert len(result) == 3
|
|
assert result[-1]["role"] == "user"
|
|
assert result[-1]["content"] == "ok"
|
|
|
|
def test_empty_messages(self):
|
|
assert AnthropicProvider._merge_consecutive([]) == []
|
|
|
|
def test_single_user_message(self):
|
|
msgs = [{"role": "user", "content": "hi"}]
|
|
result = AnthropicProvider._merge_consecutive(msgs)
|
|
assert len(result) == 1
|
|
|
|
def test_single_assistant_rerouted_to_user(self):
|
|
"""When stripping leaves nothing, the last assistant is rerouted to
|
|
``user`` so we don't produce an empty messages array."""
|
|
msgs = [{"role": "assistant", "content": "hi"}]
|
|
result = AnthropicProvider._merge_consecutive(msgs)
|
|
assert len(result) == 1
|
|
assert result[0]["role"] == "user"
|
|
assert result[0]["content"] == "hi"
|
|
|
|
def test_all_assistants_collapse_then_rerouted(self):
|
|
"""Consecutive trailing assistants merge into one, which is then
|
|
rerouted as a user turn carrying the merged content."""
|
|
msgs = [
|
|
{"role": "assistant", "content": "a"},
|
|
{"role": "assistant", "content": "b"},
|
|
]
|
|
result = AnthropicProvider._merge_consecutive(msgs)
|
|
assert len(result) == 1
|
|
assert result[0]["role"] == "user"
|
|
# "b" was merged into "a"'s block list during the merge pass.
|
|
assert result[0]["content"] == [
|
|
{"type": "text", "text": "a"},
|
|
{"type": "text", "text": "b"},
|
|
]
|
|
|
|
def test_assistant_with_tool_use_not_rerouted(self):
|
|
"""A trailing assistant carrying ``tool_use`` blocks cannot become a
|
|
user turn (Anthropic rejects ``tool_use`` inside user messages), so
|
|
the method returns an empty list rather than forging a bad request."""
|
|
msgs = [
|
|
{
|
|
"role": "assistant",
|
|
"content": [
|
|
{"type": "text", "text": "let me search"},
|
|
{"type": "tool_use", "id": "t1", "name": "search", "input": {}},
|
|
],
|
|
}
|
|
]
|
|
result = AnthropicProvider._merge_consecutive(msgs)
|
|
assert result == []
|
|
|
|
def test_leading_assistant_gets_synthetic_user(self):
|
|
"""If the first turn is a bare assistant (e.g. history truncation
|
|
dropped the original user request), prepend a synthetic opener so
|
|
the conversation still starts with ``user``."""
|
|
msgs = [
|
|
{"role": "assistant", "content": "hi"},
|
|
{"role": "user", "content": "ok"},
|
|
{"role": "assistant", "content": "reply"},
|
|
]
|
|
result = AnthropicProvider._merge_consecutive(msgs)
|
|
assert [m["role"] for m in result] == ["user", "assistant", "user"]
|
|
assert result[0]["content"] == "(conversation continued)"
|
|
assert result[1]["content"] == "hi"
|
|
assert result[2]["content"] == "ok"
|
|
|
|
def test_leading_assistant_with_tool_use_left_alone(self):
|
|
"""Don't prepend a synthetic opener before an assistant carrying
|
|
``tool_use``; doing so would orphan the paired ``tool_result`` that
|
|
follows. The caller will see the original 400 rather than a
|
|
harder-to-diagnose tool-pair mismatch."""
|
|
msgs = [
|
|
{
|
|
"role": "assistant",
|
|
"content": [
|
|
{"type": "tool_use", "id": "t1", "name": "search", "input": {}},
|
|
],
|
|
},
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "tool_result", "tool_use_id": "t1", "content": "ok"},
|
|
],
|
|
},
|
|
]
|
|
result = AnthropicProvider._merge_consecutive(msgs)
|
|
assert [m["role"] for m in result] == ["assistant", "user"]
|