mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
b0ddcf31e4
* ci: add path-gated AI agent integration tests workflow Runs integration_tests/ai_agent_tests against real LLM providers (Anthropic/OpenAI/Google) only when AI-agent backend code or the tests change, since runs make paid LLM calls. Adds a conftest fixture that skips provider-parametrized cases whose API keys are absent, so CI exercises only the providers it has secrets for. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: add path-gated ai_evals global-mode smoke workflow Runs the global AI chat eval (global-test1) across one cheap model per provider (Anthropic/OpenAI/Google/DeepSeek) only when the eval harness or copilot chat code change, since runs make paid LLM calls. Builds Windmill CE from source as the AI proxy; global tools/drafts run in the Vitest bridge. Gates on the deterministic draft pipeline (run succeeded + produced a draft + used write_script), not the variable LLM judge score. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: run AI smokes on PR ready-for-review instead of every push Switch the pull_request trigger from `synchronize` (every commit) to `ready_for_review`, with a job guard skipping draft PRs, so the paid LLM runs only fire when a PR is marked ready to merge (plus push-to-main and manual dispatch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ai_evals): lazily load cli mode so non-cli evals skip the cli toolchain The entrypoint eagerly imported modes/cli, which pulls the wmill CLI guidance modules and their JSR deps (@cliffy/*). Global/flow/script/app runs then crashed with "Cannot find module '@cliffy/ansi/colors'" when the cli workspace deps were not installed. Import createCliModeRunner dynamically inside runCliBenchmark instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ai_agent): raise low max_completion_tokens to OpenAI's 16 minimum OpenAI's /v1/responses rejects max_output_tokens < 16 with a 400, failing test_low_max_tokens for openai. 16 still exercises a truncated response. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: run ai_evals workflow on Node 22 for the frontend undici 8.x dep The Vitest bridge loads frontend/node_modules/undici@8.x, which requires Node >=22.19; Node 20 failed with "webidl.util.markAsUncloneable is not a function" when loading vitest.config.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ai_evals): run frontend evals autonomously + give global-test1 more turns Frontend evals (flow/script/app/global) ran the production chat prompt, which assumes an interactive human — so cheaper models burned their turn budget asking for confirmation, waiting for approval, or presenting a plan, sometimes hitting maxTurns without producing a draft. Append a shared autonomy note in baseEvalRunner (the path all frontend modes share, mirroring cli mode): act directly on clear requests; only ask on genuinely ambiguous ones (preserving the askUserQuestion cases). Also raise global-test1's maxTurns 8 -> 10 so a model that over-explores still converges. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(ai_evals): watch draft/prompt deps outside copilot/ The global eval runs production frontend code in-process, so the smoke's behavior depends on files outside frontend/src/lib/components/copilot/**: the draft model (userDraft.svelte.ts, userDraftDbSyncer.svelte.ts), script inference (infer.ts), and the chat system prompts ($system_prompts -> system_prompts/auto-generated). Add them to both push and PR path filters so a change there actually triggers the smoke that gates on draft production. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: skip direct provider tests without credentials * feat: add ai evals skip judge flag * fix: simplify ai evals ci gate * fix: simplify ai evals smoke gate * fix: handle ai eval workflow triggers --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
252 lines
7.9 KiB
Python
252 lines
7.9 KiB
Python
"""
|
|
Tool calling tests for AI agents.
|
|
|
|
Tests AI agent tool calling with different tool types:
|
|
- Rawscript tools (inline Bun/TypeScript)
|
|
- Workspace script tools (scripts deployed to the workspace)
|
|
- MCP tools (external MCP servers)
|
|
- Websearch tools (built-in web search)
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from .conftest import AIAgentTestClient, create_ai_agent_flow, create_rawscript_tool, create_script_tool
|
|
from .providers import ALL_PROVIDERS, ANTHROPIC, GOOGLE_AI, OPENAI, make_provider_input_transform
|
|
|
|
|
|
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;
|
|
}
|
|
"""
|
|
|
|
GOOGLE_AI_GEMINI_3 = {
|
|
"name": "google_ai_gemini_3",
|
|
"input_transform": make_provider_input_transform(
|
|
kind="googleai",
|
|
model="gemini-3-flash-preview",
|
|
resource_path="u/admin/googleai",
|
|
),
|
|
}
|
|
|
|
|
|
class TestToolCalling:
|
|
"""Test AI agent tool calling with different tool types."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"provider_config",
|
|
ALL_PROVIDERS,
|
|
ids=get_provider_ids(ALL_PROVIDERS),
|
|
)
|
|
def test_sum_tool(
|
|
self,
|
|
client: AIAgentTestClient,
|
|
setup_providers,
|
|
provider_config,
|
|
):
|
|
"""
|
|
Test that an AI agent can call a rawscript tool to add numbers.
|
|
"""
|
|
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.",
|
|
tools=tools,
|
|
)
|
|
|
|
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
|
|
result_str = str(result)
|
|
assert "12" in result_str, f"Expected '12' in result: {result}"
|
|
print(f"Sum tool result from {provider_config['name']}: {result}")
|
|
|
|
@pytest.mark.parametrize(
|
|
"provider_config",
|
|
ALL_PROVIDERS,
|
|
ids=get_provider_ids(ALL_PROVIDERS),
|
|
)
|
|
def test_workspace_script_tool(
|
|
self,
|
|
client: AIAgentTestClient,
|
|
setup_providers,
|
|
provider_config,
|
|
):
|
|
"""
|
|
Test that an AI agent can call a workspace script tool to add numbers.
|
|
|
|
This test uses a script that was deployed to the workspace (u/admin/sum_script)
|
|
rather than an inline rawscript.
|
|
"""
|
|
tools = [
|
|
create_script_tool(
|
|
tool_id="sum_numbers",
|
|
script_path="u/admin/sum_script",
|
|
params=["a", "b"],
|
|
)
|
|
]
|
|
|
|
flow_value = create_ai_agent_flow(
|
|
provider_input_transform=provider_config["input_transform"],
|
|
system_prompt="You are a helpful assistant. Use the sum_numbers tool to perform arithmetic.",
|
|
tools=tools,
|
|
)
|
|
|
|
result = client.run_preview_flow(
|
|
flow_value=flow_value,
|
|
args={"user_message": "What is 8 + 15? Use the sum_numbers tool."},
|
|
)
|
|
|
|
assert result is not None
|
|
result_str = str(result)
|
|
assert "23" in result_str, f"Expected '23' in result: {result}"
|
|
print(f"Workspace script tool result from {provider_config['name']}: {result}")
|
|
|
|
@pytest.mark.requires_provider("google_ai")
|
|
def test_nested_ai_agent_tool_with_gemini_3(
|
|
self,
|
|
client: AIAgentTestClient,
|
|
setup_providers,
|
|
):
|
|
"""
|
|
Test that a Gemini agent can call another Gemini AI agent as a tool.
|
|
"""
|
|
nested_ai_agent_tool = {
|
|
"id": "delegate_agent",
|
|
"summary": "delegate_agent",
|
|
"value": {
|
|
"tool_type": "flowmodule",
|
|
"type": "aiagent",
|
|
"input_transforms": {
|
|
"provider": GOOGLE_AI_GEMINI_3["input_transform"],
|
|
"system_prompt": {
|
|
"type": "static",
|
|
"value": "You are a concise arithmetic helper. Return only the numeric answer.",
|
|
},
|
|
"user_message": {"type": "ai"},
|
|
"output_type": {"type": "static", "value": "text"},
|
|
},
|
|
"tools": [],
|
|
},
|
|
}
|
|
|
|
flow_value = create_ai_agent_flow(
|
|
provider_input_transform=GOOGLE_AI_GEMINI_3["input_transform"],
|
|
system_prompt="You are a coordinator. Use delegate_agent for arithmetic before answering.",
|
|
tools=[nested_ai_agent_tool],
|
|
output_type="text",
|
|
)
|
|
|
|
result = client.run_preview_flow(
|
|
flow_value=flow_value,
|
|
args={"user_message": "Ask delegate_agent what 13 + 29 is, then tell me the result."},
|
|
)
|
|
|
|
assert result is not None
|
|
result_str = str(result)
|
|
assert "42" in result_str, f"Expected '42' in result: {result}"
|
|
|
|
messages = result.get("messages", [])
|
|
assert any(
|
|
tool_call.get("function", {}).get("name") == "delegate_agent"
|
|
for message in messages
|
|
for tool_call in message.get("tool_calls", [])
|
|
), f"Expected delegate_agent tool call in messages: {messages}"
|
|
print(f"Nested AI agent tool result from {GOOGLE_AI_GEMINI_3['name']}: {result}")
|
|
|
|
@pytest.mark.parametrize(
|
|
"provider_config",
|
|
ALL_PROVIDERS,
|
|
ids=get_provider_ids(ALL_PROVIDERS),
|
|
)
|
|
def test_mcp_tool(
|
|
self,
|
|
client: AIAgentTestClient,
|
|
setup_providers,
|
|
provider_config,
|
|
):
|
|
"""
|
|
Test that an AI agent can call an MCP tool (DeepWiki).
|
|
"""
|
|
tools = [
|
|
{
|
|
"id": "deepwiki",
|
|
"value": {
|
|
"tool_type": "mcp",
|
|
"resource_path": "$res:u/admin/deepwiki",
|
|
},
|
|
}
|
|
]
|
|
|
|
flow_value = create_ai_agent_flow(
|
|
provider_input_transform=provider_config["input_transform"],
|
|
system_prompt="You are a helpful assistant. Use the available tools to answer questions.",
|
|
tools=tools,
|
|
)
|
|
|
|
result = client.run_preview_flow(
|
|
flow_value=flow_value,
|
|
args={"user_message": "Use the read_wiki_structure tool to get the structure of the sveltejs/svelte repository."},
|
|
)
|
|
|
|
assert result is not None
|
|
print(f"MCP tool result from {provider_config['name']}: {result}")
|
|
|
|
@pytest.mark.parametrize(
|
|
"provider_config",
|
|
[OPENAI, ANTHROPIC, GOOGLE_AI],
|
|
ids=get_provider_ids([OPENAI, ANTHROPIC, GOOGLE_AI]),
|
|
)
|
|
def test_websearch_tool(
|
|
self,
|
|
client: AIAgentTestClient,
|
|
setup_providers,
|
|
provider_config,
|
|
):
|
|
"""
|
|
Test that an AI agent can use the websearch tool.
|
|
"""
|
|
tools = [
|
|
{
|
|
"id": "websearch",
|
|
"value": {
|
|
"tool_type": "websearch",
|
|
},
|
|
}
|
|
]
|
|
|
|
flow_value = create_ai_agent_flow(
|
|
provider_input_transform=provider_config["input_transform"],
|
|
system_prompt="You are a helpful assistant. Use websearch to find current information.",
|
|
tools=tools,
|
|
)
|
|
|
|
result = client.run_preview_flow(
|
|
flow_value=flow_value,
|
|
args={"user_message": "What is the current version of Svelte? Use websearch."},
|
|
)
|
|
|
|
assert result is not None
|
|
print(f"Websearch tool result from {provider_config['name']}: {result}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v", "-s"])
|