Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -549,13 +549,11 @@ async def test_streaming_tool_call_arguments_done_event_present(
# 4) Agentic loop (StrandsAgentChatModule only)
# ===================================================================

# Tools in the format StrandsAgentChatModule's _extract_tool_names expects:
# nested under a "function" key (Chat Completions-compatible format).
# The tool definition sent to the LLM is sourced from the tool registry,
# not from body_json["tools"] directly.
# Tools in Responses API format ({type, name}).
# The actual tool definition passed to the LLM comes from the tool registry.
_AGENTIC_CALCULATE_TOOL: dict[str, Any] = {
"type": "function",
"function": {"name": "calculate"},
"name": "calculate",
}


Expand Down Expand Up @@ -815,7 +813,7 @@ async def test_unknown_tool_is_silently_skipped(
"model": agentic_factory.model,
"input": "Hi",
"tools": [
{"type": "function", "function": {"name": "nonexistent_tool"}},
{"type": "function", "name": "nonexistent_tool"},
],
}
result = await module.generate_response(_make_request(), body)
Expand Down Expand Up @@ -847,7 +845,7 @@ async def run(self, params: dict[str, Any]) -> Any:
"model": agentic_factory.model,
"input": "Hi",
"tools": [
{"type": "function", "function": {"name": "broken_tool"}},
{"type": "function", "name": "broken_tool"},
],
}
result = await module.generate_response(_make_request(), body)
Expand All @@ -866,7 +864,7 @@ async def test_tool_registry_error_propagates(self, agentic_factory: ModuleFacto
"model": agentic_factory.model,
"input": "Hi",
"tools": [
{"type": "function", "function": {"name": "calculate"}},
{"type": "function", "name": "calculate"},
],
}
with pytest.raises(RuntimeError, match="Registry unavailable"):
Expand Down Expand Up @@ -894,7 +892,7 @@ async def test_tool_invocation_http_error_handled_gracefully(
body = {
"model": agentic_factory.model,
"input": "call tool 'calculate' with '{}'",
"tools": [{"type": "function", "function": {"name": "calculate"}}],
"tools": [{"type": "function", "name": "calculate"}],
}
result = await module.generate_response(_make_request(), body)
assert result.status == "completed"
Expand Down Expand Up @@ -922,7 +920,7 @@ async def test_tool_invocation_success_request_sent_to_tool(
body = {
"model": agentic_factory.model,
"input": "call tool 'calculate' with '{\"expression\": \"2+2\"}'",
"tools": [{"type": "function", "function": {"name": "calculate"}}],
"tools": [{"type": "function", "name": "calculate"}],
}
result = await module.generate_response(_make_request(), body)
assert result.status == "completed"
Expand Down Expand Up @@ -951,8 +949,8 @@ async def test_partial_tools_resolved_when_some_missing(
"model": agentic_factory.model,
"input": "Do stuff",
"tools": [
{"type": "function", "function": {"name": "calculate"}},
{"type": "function", "function": {"name": "missing_tool"}},
{"type": "function", "name": "calculate"},
{"type": "function", "name": "missing_tool"},
],
}
result = await module.generate_response(_make_request(), body)
Expand Down
16 changes: 9 additions & 7 deletions backend/omni/src/modai/modules/chat/openai_agent_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,16 +216,18 @@ def _message_text(msg: Any) -> str:


def _extract_tool_names(body_json: dict[str, Any]) -> list[str]:
"""Extract tool function names from the OpenAI-format request body."""
"""Extract tool function names from the OpenAI Responses API request body.

Expects the flat Responses API format: {type: "function", name: "..."}
"""
tools = body_json.get("tools", [])
names: list[str] = []
for tool in tools:
if isinstance(tool, dict) and tool.get("type") == "function":
fn = tool.get("function", {})
if isinstance(fn, dict):
name = fn.get("name")
if name:
names.append(name)
if not isinstance(tool, dict) or tool.get("type") != "function":
continue
name = tool.get("name")
if name:
names.append(name)
return names


Expand Down
13 changes: 12 additions & 1 deletion e2e_tests/tests_omni_full/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,39 @@ export default defineConfig({
],
webServer: [
{
name: "NanoIDP",
command: "bash scripts/start-nanoidp.sh",
url: "http://localhost:9000/api/health",
reuseExistingServer: !process.env.CI,
gracefulShutdown: { signal: "SIGTERM", timeout: 10000 },
timeout: 60000,
},
{
name: "Backend",
command: "bash scripts/run-backend.sh",
url: "http://localhost:8000/api/health",
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
{
name: "Frontend",
command: "bash scripts/run-frontend.sh",
url: "http://localhost:4173",
reuseExistingServer: !process.env.CI,
},
{
command: "docker container run --rm -p 3001:8000 ghcr.io/modai-systems/llmock:latest",
name: "LLMock",
command: "docker container run --rm -p 3001:8000 -e LLMOCK_DEBUG=true ghcr.io/modai-systems/llmock:latest",
url: "http://localhost:3001/health",
reuseExistingServer: !process.env.CI,
gracefulShutdown: { signal: "SIGTERM", timeout: 5000 },
},
{
name: "Dice Roller",
command: "bash scripts/run-dice-roller.sh",
url: "http://localhost:8001/openapi.json",
reuseExistingServer: !process.env.CI,
gracefulShutdown: { signal: "SIGTERM", timeout: 5000 },
},
],
});
2 changes: 1 addition & 1 deletion e2e_tests/tests_omni_full/scripts/run-backend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_DIR=$(dirname "$(realpath "$0")")
ROOT_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
BACKEND_DIR="$ROOT_DIR/backend/omni"

Expand Down
15 changes: 15 additions & 0 deletions e2e_tests/tests_omni_full/scripts/run-dice-roller.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Starts the dice-roller tool microservice for e2e tests.
#
# Lifecycle managed by Playwright's webServer config:
# - url: http://localhost:8001/openapi.json

set -euo pipefail

SCRIPT_DIR=$(dirname "$(realpath "$0")")
ROOT_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
DICE_ROLLER_DIR="$ROOT_DIR/backend/tools/dice-roller"

cd "$DICE_ROLLER_DIR"

exec uv run uvicorn main:app --port 8001
2 changes: 1 addition & 1 deletion e2e_tests/tests_omni_full/scripts/run-frontend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_DIR=$(dirname "$(realpath "$0")")
ROOT_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
FRONTEND_DIR="$ROOT_DIR/frontend/omni"

Expand Down
28 changes: 27 additions & 1 deletion e2e_tests/tests_omni_full/src/chat.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { test } from "@playwright/test";
import { TEST_USER_PASSWORD, TEST_USERNAME } from "./fixtures";
import { ChatPage, LLMProvidersPage, NanoIdpLoginPage } from "./pages";
import {
ChatPage,
LLMProvidersPage,
NanoIdpLoginPage,
ToolsManagementPage,
} from "./pages";

const BACKEND_URL = "http://localhost:8000";

Expand Down Expand Up @@ -47,4 +52,25 @@ test.describe("Chat", () => {
await chatPage.sendMessage("Hello again");
await chatPage.assertLastResponse("Hello again");
});

test("should call dice-roller tool and return result", async ({ page }) => {
// Enable the dice-roller tool via the Tools management page
const toolsPage = new ToolsManagementPage(page);
await toolsPage.navigateTo();
await toolsPage.enableTool("roll_dice");

const chatPage = new ChatPage(page);
await chatPage.navigateTo();
await chatPage.selectFirstModel();

// llmock trigger: "call tool '<name>' with '<json>'" causes it to return
// a tool_call response. The backend Strands agent then calls the
// dice-roller microservice (port 8001) and sends the result back.
// LLMock responds with "last tool call result is <json>" after the agent
// sends the tool result back.
await chatPage.sendMessage(
"call tool 'roll_dice' with '{\"count\": 1, \"sides\": 6}'",
);
await chatPage.assertLastResponse("last tool call result is", 20000);
});
});
25 changes: 23 additions & 2 deletions e2e_tests/tests_omni_full/src/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,10 @@ export class ChatPage {
await this.page.getByRole("button", { name: "Send" }).click();
}

async assertLastResponse(content: string): Promise<void> {
async assertLastResponse(content: string, timeout = 5000): Promise<void> {
await expect(
this.page.locator(".bg-muted.rounded-2xl").last(),
).toContainText(content, { timeout: 5000 });
).toContainText(content, { timeout });
}
}

Expand Down Expand Up @@ -202,3 +202,24 @@ export class Sidebar {
}
}
}

export class ToolsManagementPage {
constructor(private page: Page) {}

async navigateTo(): Promise<void> {
const sidebar = new Sidebar(this.page);
await sidebar.navigateTo("Tools");
await expect(this.page).toHaveURL("/tools");
}

async enableTool(toolName: string): Promise<void> {
const toggle = this.page.getByLabel(`Toggle tool ${toolName}`, {
exact: true,
});
await toggle.waitFor({ state: "visible", timeout: 10000 });
const isChecked = await toggle.getAttribute("data-state");
if (isChecked !== "checked") {
await toggle.click();
}
}
}
2 changes: 2 additions & 0 deletions e2e_tests/tests_omni_light/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ export default defineConfig({
],
webServer: [
{
name: "Frontend",
command: "cd ../../frontend/omni && ln -sf modules_browser_only.json public/modules.json && pnpm build && pnpm preview",
url: "http://localhost:4173",
reuseExistingServer: !process.env.CI,
},
{
name: "LLMock",
command:
"docker container run --rm --platform linux/amd64 -p 3001:8000 -e LLMOCK_CORS_ALLOW_ORIGINS='[\"http://localhost:4173\"]' ghcr.io/modai-systems/llmock:latest",
url: "http://localhost:3001/health",
Expand Down
35 changes: 32 additions & 3 deletions frontend/omni/public/modules_browser_only.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
"type": "MainApp",
"path": "@/modules/router/AppRouter",
"dependencies": {
"module:routes": ["chat-route", "providers-route", "chat-fallback-route", "sidebar-layout-route"]
"module:routes": ["chat-route", "providers-route", "tools-route", "chat-fallback-route", "sidebar-layout-route"]
}
},
{
"id": "sidebar-layout",
"type": "MainApp",
"path": "@/modules/main-app-sidebar-based/MainApp",
"dependencies": {
"module:sidebarTopItems": ["chat-navigation-item", "providers-navigation-item"]
"module:sidebarTopItems": ["chat-navigation-item", "providers-navigation-item", "tools-navigation-item"]
}
},
{
Expand Down Expand Up @@ -57,7 +57,8 @@
"path": "@/modules/chat/ChatComponent",
"dependencies": {
"module:chatService": "chat-service",
"module:llmProviderService": "llm-provider-service"
"module:llmProviderService": "llm-provider-service",
"module:toolsService": "tools-service"
}
},
{
Expand Down Expand Up @@ -90,19 +91,47 @@
"module:llmProviderService": "llm-provider-service"
}
},
{
"id": "tools-service",
"type": "ToolsService",
"path": "@/modules/tools-service/emptyToolsService/create",
"dependencies": {}
},
{
"id": "tools-management",
"type": "ToolsManagementComponent",
"path": "@/modules/tools-management/ToolsManagementComponent",
"dependencies": {
"module:toolsService": "tools-service"
}
},
{
"id": "providers-navigation-item",
"type": "SidebarTopItem",
"path": "@/modules/llm-provider-management/providersNavigationItem",
"dependencies": {}
},
{
"id": "tools-navigation-item",
"type": "SidebarTopItem",
"path": "@/modules/tools-management/toolsNavigationItem",
"dependencies": {}
},
{
"id": "providers-route",
"type": "Route",
"path": "@/modules/llm-provider-management/providersRouteDefinition/create",
"dependencies": {
"module:providerManagement": ["llm-provider-management"]
}
},
{
"id": "tools-route",
"type": "Route",
"path": "@/modules/tools-management/toolsRouteDefinition/create",
"dependencies": {
"module:toolsManagement": ["tools-management"]
}
}
]
}
Loading
Loading