* fix: remove $schema field from Google AI output schema requests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: add $schema field to all output schema integration tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: remove $schema field from Google AI tool parameter schemas Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: add workspace script tool test for AI agents Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
461 lines
15 KiB
Python
461 lines
15 KiB
Python
"""
|
|
Output schema tests for AI agents.
|
|
|
|
Tests that AI agents correctly handle structured output with output_schema:
|
|
- With a tool (agent uses tool then returns structured output)
|
|
- Without a tool (agent returns structured output directly)
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from .conftest import AIAgentTestClient, create_ai_agent_flow, create_rawscript_tool
|
|
from .providers import ALL_PROVIDERS
|
|
|
|
|
|
def get_provider_ids(providers: list) -> list[str]:
|
|
"""Get provider names for pytest parametrization IDs."""
|
|
return [p["name"] for p in providers]
|
|
|
|
|
|
# Inline script for sum tool (Bun/TypeScript)
|
|
ADD_NUMBERS_SCRIPT = """
|
|
export function main(a: number, b: number): number {
|
|
return a + b;
|
|
}
|
|
"""
|
|
|
|
# Output schema for structured result
|
|
RESULT_SCHEMA = {
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object",
|
|
"properties": {
|
|
"sum": {"type": "number"}
|
|
},
|
|
"required": ["sum"]
|
|
}
|
|
|
|
|
|
class TestOutputSchema:
|
|
"""Test AI agent output_schema handling."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"provider_config",
|
|
ALL_PROVIDERS,
|
|
ids=get_provider_ids(ALL_PROVIDERS),
|
|
)
|
|
def test_output_schema_with_tool(
|
|
self,
|
|
client: AIAgentTestClient,
|
|
setup_providers,
|
|
provider_config,
|
|
):
|
|
"""
|
|
Test output_schema with a tool.
|
|
|
|
The agent should use the add_numbers tool and return structured output.
|
|
"""
|
|
if provider_config["name"] == "google_ai":
|
|
pytest.xfail("Google AI does not support output_schema with tools")
|
|
|
|
tools = [
|
|
create_rawscript_tool(
|
|
tool_id="add_numbers",
|
|
content=ADD_NUMBERS_SCRIPT,
|
|
params=["a", "b"],
|
|
language="bun",
|
|
)
|
|
]
|
|
|
|
flow_value = create_ai_agent_flow(
|
|
provider_input_transform=provider_config["input_transform"],
|
|
system_prompt="You are a helpful assistant. Use the add_numbers tool to perform arithmetic. Return the result in the structured format.",
|
|
tools=tools,
|
|
output_schema=RESULT_SCHEMA,
|
|
)
|
|
|
|
result = client.run_preview_flow(
|
|
flow_value=flow_value,
|
|
args={"user_message": "What is 5 + 7? Use the add_numbers tool."},
|
|
)
|
|
|
|
assert result is not None
|
|
# The result should be structured with a "result" field
|
|
assert "sum" in result or "12" in str(result), f"Expected structured result with '12': {result}"
|
|
|
|
# For Anthropic and Bedrock, verify structured_output tool was used
|
|
if provider_config["name"] in ("anthropic", "bedrock"):
|
|
messages = result.get("messages", [])
|
|
has_structured_output_msg = any(
|
|
msg.get("role") == "tool" and
|
|
msg.get("content") == "Successfully ran structured_output tool"
|
|
for msg in messages
|
|
)
|
|
assert has_structured_output_msg, (
|
|
f"Expected 'Successfully ran structured_output tool' message for {provider_config['name']}: {messages}"
|
|
)
|
|
|
|
print(f"Output schema with tool result from {provider_config['name']}: {result}")
|
|
|
|
@pytest.mark.parametrize(
|
|
"provider_config",
|
|
ALL_PROVIDERS,
|
|
ids=get_provider_ids(ALL_PROVIDERS),
|
|
)
|
|
def test_output_schema_without_tool(
|
|
self,
|
|
client: AIAgentTestClient,
|
|
setup_providers,
|
|
provider_config,
|
|
):
|
|
"""
|
|
Test output_schema without a tool.
|
|
|
|
The agent should compute the sum and return structured output directly.
|
|
"""
|
|
flow_value = create_ai_agent_flow(
|
|
provider_input_transform=provider_config["input_transform"],
|
|
system_prompt="You are a helpful assistant. Compute arithmetic and return the result in the structured format.",
|
|
tools=[],
|
|
output_schema=RESULT_SCHEMA,
|
|
)
|
|
|
|
result = client.run_preview_flow(
|
|
flow_value=flow_value,
|
|
args={"user_message": "What is 5 + 7?"},
|
|
)
|
|
|
|
assert result is not None
|
|
# The result should be structured with a "result" field
|
|
assert "sum" in result or "12" in str(result), f"Expected structured result with '12': {result}"
|
|
|
|
# For Anthropic and Bedrock, verify structured_output tool was used
|
|
if provider_config["name"] in ("anthropic", "bedrock"):
|
|
messages = result.get("messages", [])
|
|
has_structured_output_msg = any(
|
|
msg.get("role") == "tool" and
|
|
msg.get("content") == "Successfully ran structured_output tool"
|
|
for msg in messages
|
|
)
|
|
assert has_structured_output_msg, (
|
|
f"Expected 'Successfully ran structured_output tool' message for {provider_config['name']}: {messages}"
|
|
)
|
|
|
|
print(f"Output schema without tool result from {provider_config['name']}: {result}")
|
|
|
|
|
|
class TestSchemaVariations:
|
|
"""
|
|
Test various schema features across all providers.
|
|
|
|
These tests verify that different JSON Schema features are correctly
|
|
processed by make_strict() and accepted by providers.
|
|
"""
|
|
|
|
@pytest.mark.parametrize(
|
|
"provider_config",
|
|
ALL_PROVIDERS,
|
|
ids=get_provider_ids(ALL_PROVIDERS),
|
|
)
|
|
def test_nested_objects_schema(
|
|
self,
|
|
client: AIAgentTestClient,
|
|
setup_providers,
|
|
provider_config,
|
|
):
|
|
"""Test deeply nested object structure."""
|
|
schema = {
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object",
|
|
"properties": {
|
|
"user": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {"type": "string"},
|
|
"age": {"type": "integer"}
|
|
}
|
|
}
|
|
},
|
|
"required": ["user"]
|
|
}
|
|
|
|
flow_value = create_ai_agent_flow(
|
|
provider_input_transform=provider_config["input_transform"],
|
|
system_prompt="Extract user info. Return structured data.",
|
|
tools=[],
|
|
output_schema=schema,
|
|
)
|
|
|
|
result = client.run_preview_flow(
|
|
flow_value=flow_value,
|
|
args={"user_message": "User John is 25 years old"},
|
|
)
|
|
|
|
assert result is not None
|
|
assert "user" in result or "John" in str(result)
|
|
print(f"Nested objects result from {provider_config['name']}: {result}")
|
|
|
|
@pytest.mark.parametrize(
|
|
"provider_config",
|
|
ALL_PROVIDERS,
|
|
ids=get_provider_ids(ALL_PROVIDERS),
|
|
)
|
|
def test_array_of_objects_schema(
|
|
self,
|
|
client: AIAgentTestClient,
|
|
setup_providers,
|
|
provider_config,
|
|
):
|
|
"""Test array with object items."""
|
|
schema = {
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object",
|
|
"properties": {
|
|
"items": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {"type": "integer"},
|
|
"label": {"type": "string"}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"required": ["items"]
|
|
}
|
|
|
|
flow_value = create_ai_agent_flow(
|
|
provider_input_transform=provider_config["input_transform"],
|
|
system_prompt="Create a list of items. Return structured data.",
|
|
tools=[],
|
|
output_schema=schema,
|
|
)
|
|
|
|
result = client.run_preview_flow(
|
|
flow_value=flow_value,
|
|
args={"user_message": "Create 2 items: Apple (id 1), Banana (id 2)"},
|
|
)
|
|
|
|
assert result is not None
|
|
assert "items" in result or "Apple" in str(result)
|
|
print(f"Array of objects result from {provider_config['name']}: {result}")
|
|
|
|
@pytest.mark.parametrize(
|
|
"provider_config",
|
|
ALL_PROVIDERS,
|
|
ids=get_provider_ids(ALL_PROVIDERS),
|
|
)
|
|
def test_enum_schema(
|
|
self,
|
|
client: AIAgentTestClient,
|
|
setup_providers,
|
|
provider_config,
|
|
):
|
|
"""Test enum constraints."""
|
|
schema = {
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object",
|
|
"properties": {
|
|
"status": {
|
|
"type": "string",
|
|
"enum": ["pending", "approved", "rejected"]
|
|
}
|
|
},
|
|
"required": ["status"]
|
|
}
|
|
|
|
flow_value = create_ai_agent_flow(
|
|
provider_input_transform=provider_config["input_transform"],
|
|
system_prompt="Classify the request status. Return structured data.",
|
|
tools=[],
|
|
output_schema=schema,
|
|
)
|
|
|
|
result = client.run_preview_flow(
|
|
flow_value=flow_value,
|
|
args={"user_message": "The request was accepted"},
|
|
)
|
|
|
|
assert result is not None
|
|
status = result.get("status") if isinstance(result, dict) else None
|
|
assert status in ["pending", "approved", "rejected"] or "approved" in str(result)
|
|
print(f"Enum result from {provider_config['name']}: {result}")
|
|
|
|
@pytest.mark.parametrize(
|
|
"provider_config",
|
|
ALL_PROVIDERS,
|
|
ids=get_provider_ids(ALL_PROVIDERS),
|
|
)
|
|
def test_optional_fields_schema(
|
|
self,
|
|
client: AIAgentTestClient,
|
|
setup_providers,
|
|
provider_config,
|
|
):
|
|
"""Test that optional fields are handled correctly (made nullable)."""
|
|
schema = {
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {"type": "string"},
|
|
"nickname": {"type": "string"} # Not in required - should be nullable
|
|
},
|
|
"required": ["name"]
|
|
}
|
|
|
|
flow_value = create_ai_agent_flow(
|
|
provider_input_transform=provider_config["input_transform"],
|
|
system_prompt="Extract name info. Return structured data.",
|
|
tools=[],
|
|
output_schema=schema,
|
|
)
|
|
|
|
result = client.run_preview_flow(
|
|
flow_value=flow_value,
|
|
args={"user_message": "The person is called Alice"},
|
|
)
|
|
|
|
assert result is not None
|
|
assert "name" in result or "Alice" in str(result)
|
|
print(f"Optional fields result from {provider_config['name']}: {result}")
|
|
|
|
@pytest.mark.parametrize(
|
|
"provider_config",
|
|
ALL_PROVIDERS,
|
|
ids=get_provider_ids(ALL_PROVIDERS),
|
|
)
|
|
def test_number_constraints_schema(
|
|
self,
|
|
client: AIAgentTestClient,
|
|
setup_providers,
|
|
provider_config,
|
|
):
|
|
"""Test min/max constraints on numbers."""
|
|
schema = {
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object",
|
|
"properties": {
|
|
"rating": {
|
|
"type": "number",
|
|
"minimum": 1,
|
|
"maximum": 5
|
|
}
|
|
},
|
|
"required": ["rating"]
|
|
}
|
|
|
|
flow_value = create_ai_agent_flow(
|
|
provider_input_transform=provider_config["input_transform"],
|
|
system_prompt="Provide a rating from 1 to 5. Return structured data.",
|
|
tools=[],
|
|
output_schema=schema,
|
|
)
|
|
|
|
result = client.run_preview_flow(
|
|
flow_value=flow_value,
|
|
args={"user_message": "This is excellent, rate it highly"},
|
|
)
|
|
|
|
assert result is not None
|
|
rating = result.get("rating") if isinstance(result, dict) else None
|
|
if rating is not None:
|
|
assert 1 <= rating <= 5, f"Rating {rating} out of bounds"
|
|
print(f"Number constraints result from {provider_config['name']}: {result}")
|
|
|
|
@pytest.mark.parametrize(
|
|
"provider_config",
|
|
ALL_PROVIDERS,
|
|
ids=get_provider_ids(ALL_PROVIDERS),
|
|
)
|
|
def test_definitions_ref_schema(
|
|
self,
|
|
client: AIAgentTestClient,
|
|
setup_providers,
|
|
provider_config,
|
|
):
|
|
"""Test $ref with definitions."""
|
|
if provider_config["name"] == "google_ai":
|
|
pytest.xfail("Google AI does not support $ref with definitions in output_schema")
|
|
|
|
schema = {
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object",
|
|
"properties": {
|
|
"primary": {"$ref": "#/definitions/Color"},
|
|
"secondary": {"$ref": "#/definitions/Color"}
|
|
},
|
|
"required": ["primary", "secondary"],
|
|
"definitions": {
|
|
"Color": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {"type": "string"},
|
|
"hex": {"type": "string"}
|
|
},
|
|
"required": ["name", "hex"]
|
|
}
|
|
}
|
|
}
|
|
|
|
flow_value = create_ai_agent_flow(
|
|
provider_input_transform=provider_config["input_transform"],
|
|
system_prompt="Provide color information. Return structured data with primary and secondary colors.",
|
|
tools=[],
|
|
output_schema=schema,
|
|
)
|
|
|
|
result = client.run_preview_flow(
|
|
flow_value=flow_value,
|
|
args={"user_message": "Primary color is red (#FF0000), secondary is blue (#0000FF)"},
|
|
)
|
|
|
|
assert result is not None
|
|
assert "primary" in result or "red" in str(result).lower()
|
|
print(f"Definitions/ref result from {provider_config['name']}: {result}")
|
|
|
|
@pytest.mark.parametrize(
|
|
"provider_config",
|
|
ALL_PROVIDERS,
|
|
ids=get_provider_ids(ALL_PROVIDERS),
|
|
)
|
|
def test_anyof_schema(
|
|
self,
|
|
client: AIAgentTestClient,
|
|
setup_providers,
|
|
provider_config,
|
|
):
|
|
"""Test anyOf for union types."""
|
|
schema = {
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"type": "object",
|
|
"properties": {
|
|
"value": {
|
|
"anyOf": [
|
|
{"type": "string"},
|
|
{"type": "number"}
|
|
]
|
|
}
|
|
},
|
|
"required": ["value"]
|
|
}
|
|
|
|
flow_value = create_ai_agent_flow(
|
|
provider_input_transform=provider_config["input_transform"],
|
|
system_prompt="Extract the value. Return structured data.",
|
|
tools=[],
|
|
output_schema=schema,
|
|
)
|
|
|
|
result = client.run_preview_flow(
|
|
flow_value=flow_value,
|
|
args={"user_message": "The answer is 42"},
|
|
)
|
|
|
|
assert result is not None
|
|
assert "value" in result or "42" in str(result)
|
|
print(f"anyOf result from {provider_config['name']}: {result}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v", "-s"])
|