Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
"""Regression: the final Anthropic request must carry no blank text block.
|
||||
|
||||
`convert_messages_to_anthropic` runs per-message converters that coerce blanks
|
||||
they produce, but a blank/whitespace-only text block can be synthesized *after*
|
||||
those run — a compression summary message, a role merge, or an upstream message
|
||||
whose content arrives pre-shaped as content blocks. Any single blank text block
|
||||
makes Anthropic reject the whole request with HTTP 400 "text content blocks must
|
||||
contain non-whitespace text", which then replays on every turn and wedges the
|
||||
session.
|
||||
|
||||
`_scrub_blank_text_blocks` is the final backstop on the fully-assembled message
|
||||
list, and the system-block path coerces blanks at extraction time (a blank block
|
||||
carrying a cache_control breakpoint cannot be dropped).
|
||||
Ref #69512 / #70909 (follow-up: request-level guard, not just per-message).
|
||||
"""
|
||||
from agent.anthropic_adapter import (
|
||||
_EMPTY_TEXT_PLACEHOLDER,
|
||||
convert_messages_to_anthropic,
|
||||
)
|
||||
|
||||
|
||||
def _all_text_blocks(messages):
|
||||
for m in messages:
|
||||
content = m.get("content")
|
||||
if isinstance(content, list):
|
||||
for b in content:
|
||||
if isinstance(b, dict) and b.get("type") == "text":
|
||||
yield b
|
||||
|
||||
|
||||
def _assert_no_blank(system, messages):
|
||||
if isinstance(system, list):
|
||||
for b in system:
|
||||
if isinstance(b, dict) and b.get("type") == "text":
|
||||
assert b["text"].strip(), f"blank text block in system: {b!r}"
|
||||
for b in _all_text_blocks(messages):
|
||||
assert b["text"].strip(), f"blank text block survived: {b!r}"
|
||||
|
||||
|
||||
def test_pre_shaped_blank_block_in_user_content_is_coerced():
|
||||
# Content arrives already as blocks with a blank text part — the per-message
|
||||
# user converter does not walk-and-coerce these, so the final guard must.
|
||||
messages = [
|
||||
{"role": "user", "content": [
|
||||
{"type": "text", "text": " "},
|
||||
{"type": "text", "text": "real question"},
|
||||
]},
|
||||
]
|
||||
system, result = convert_messages_to_anthropic(messages)
|
||||
_assert_no_blank(system, result)
|
||||
|
||||
|
||||
def test_blank_summary_style_user_message_is_coerced():
|
||||
# A compression summary that came back empty becomes a whitespace user
|
||||
# message; it must not reach the wire as a blank block.
|
||||
messages = [
|
||||
{"role": "user", "content": "hi"},
|
||||
{"role": "assistant", "content": "answer"},
|
||||
{"role": "user", "content": "\n\n"}, # empty "summary"-style turn
|
||||
]
|
||||
system, result = convert_messages_to_anthropic(messages)
|
||||
_assert_no_blank(system, result)
|
||||
|
||||
|
||||
def test_blank_system_block_is_coerced():
|
||||
messages = [
|
||||
{"role": "system", "content": [
|
||||
{"type": "text", "text": " ", "cache_control": {"type": "ephemeral"}},
|
||||
]},
|
||||
{"role": "user", "content": "hello"},
|
||||
]
|
||||
system, result = convert_messages_to_anthropic(messages)
|
||||
_assert_no_blank(system, result)
|
||||
|
||||
|
||||
def test_prepended_leading_user_turn_is_not_blank():
|
||||
"""The root cause: _ensure_leading_user_turn prepends a placeholder turn when
|
||||
messages[0] is not a user turn (post-compaction histories start with an
|
||||
assistant summary). That placeholder must be NON-whitespace — a bare " "
|
||||
is itself a blank text block and 400s the whole request, wedging every turn.
|
||||
Bedrock's equivalent already uses the shared placeholder.
|
||||
"""
|
||||
messages = [
|
||||
{"role": "assistant", "content": "summary of earlier turns"},
|
||||
{"role": "user", "content": "continue"},
|
||||
]
|
||||
system, result = convert_messages_to_anthropic(messages)
|
||||
assert result[0]["role"] == "user", "a leading user turn must be prepended"
|
||||
_assert_no_blank(system, result)
|
||||
|
||||
|
||||
def test_real_text_is_left_untouched():
|
||||
# A real, non-blank turn keeps its text verbatim and is never replaced by
|
||||
# the placeholder (the guard only touches blank blocks).
|
||||
messages = [
|
||||
{"role": "user", "content": "what is 2+2?"},
|
||||
]
|
||||
system, result = convert_messages_to_anthropic(messages)
|
||||
# Content may be a plain string or a list of blocks; collect text either way.
|
||||
texts = []
|
||||
for m in result:
|
||||
c = m.get("content")
|
||||
if isinstance(c, str):
|
||||
texts.append(c)
|
||||
elif isinstance(c, list):
|
||||
texts.extend(b.get("text", "") for b in c if isinstance(b, dict) and b.get("type") == "text")
|
||||
assert "what is 2+2?" in texts
|
||||
assert _EMPTY_TEXT_PLACEHOLDER not in texts
|
||||
Reference in New Issue
Block a user