Files
windmill/integration_tests/ai_agent_tests/test_output_schema.py
centdix 18d85f1412 fix: remove $schema field from Google AI output schema requests (#7765)
* 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>
2026-02-03 01:04:36 +00:00

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"])