Skip to content

Commit c46e7ec

Browse files
committed
feat(dojo): add hitl example, fix backend tool rendering, lint
Signed-off-by: Tyler Slaton <[email protected]>
1 parent d10ebc0 commit c46e7ec

File tree

11 files changed

+372
-48
lines changed

11 files changed

+372
-48
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { Page, Locator, expect } from '@playwright/test';
2+
3+
export class HumanInLoopPage {
4+
readonly page: Page;
5+
readonly planTaskButton: Locator;
6+
readonly chatInput: Locator;
7+
readonly sendButton: Locator;
8+
readonly agentGreeting: Locator;
9+
readonly plan: Locator;
10+
readonly performStepsButton: Locator;
11+
readonly agentMessage: Locator;
12+
readonly userMessage: Locator;
13+
14+
constructor(page: Page) {
15+
this.page = page;
16+
this.planTaskButton = page.getByRole('button', { name: 'Human in the loop Plan a task' });
17+
this.agentGreeting = page.getByText("Hi, I'm an agent specialized in helping you with your tasks. How can I help you?");
18+
this.chatInput = page.getByRole('textbox', { name: 'Type a message...' });
19+
this.sendButton = page.locator('[data-test-id="copilot-chat-ready"]');
20+
this.plan = page.getByTestId('select-steps');
21+
this.performStepsButton = page.getByRole('button', { name: 'Confirm' });
22+
this.agentMessage = page.locator('.copilotKitAssistantMessage');
23+
this.userMessage = page.locator('.copilotKitUserMessage');
24+
}
25+
26+
async openChat() {
27+
await this.agentGreeting.isVisible();
28+
}
29+
30+
async sendMessage(message: string) {
31+
await this.chatInput.click();
32+
await this.chatInput.fill(message);
33+
await this.sendButton.click();
34+
}
35+
36+
async selectItemsInPlanner() {
37+
await expect(this.plan).toBeVisible({ timeout: 10000 });
38+
await this.plan.click();
39+
}
40+
41+
async getPlannerOnClick(name: string | RegExp) {
42+
return this.page.getByRole('button', { name });
43+
}
44+
45+
async uncheckItem(identifier: number | string): Promise<string> {
46+
const plannerContainer = this.page.getByTestId('select-steps');
47+
const items = plannerContainer.getByTestId('step-item');
48+
49+
let item;
50+
if (typeof identifier === 'number') {
51+
item = items.nth(identifier);
52+
} else {
53+
item = items.filter({
54+
has: this.page.getByTestId('step-text').filter({ hasText: identifier })
55+
}).first();
56+
}
57+
const stepTextElement = item.getByTestId('step-text');
58+
const text = await stepTextElement.innerText();
59+
await item.click();
60+
61+
return text;
62+
}
63+
64+
async isStepItemUnchecked(target: number | string): Promise<boolean> {
65+
const plannerContainer = this.page.getByTestId('select-steps');
66+
const items = plannerContainer.getByTestId('step-item');
67+
68+
let item;
69+
if (typeof target === 'number') {
70+
item = items.nth(target);
71+
} else {
72+
item = items.filter({
73+
has: this.page.getByTestId('step-text').filter({ hasText: target })
74+
}).first();
75+
}
76+
const checkbox = item.locator('input[type="checkbox"]');
77+
return !(await checkbox.isChecked());
78+
}
79+
80+
async performSteps() {
81+
await this.performStepsButton.click();
82+
}
83+
84+
async assertAgentReplyVisible(expectedText: RegExp) {
85+
await expect(this.agentMessage.last().getByText(expectedText)).toBeVisible();
86+
}
87+
88+
async assertUserMessageVisible(message: string) {
89+
await expect(this.page.getByText(message)).toBeVisible();
90+
}
91+
}

apps/dojo/e2e/tests/agnoTests/backendToolRenderingPage.spec.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import { test, expect } from "@playwright/test";
22

3-
test("[Agno] Backend Tool Rendering displays weather cards", async ({ page }) => {
3+
test("[Agno] Backend Tool Rendering displays weather cards", async ({
4+
page,
5+
}) => {
46
// Set shorter default timeout for this test
57
test.setTimeout(30000); // 30 seconds total
68

79
await page.goto("/agno/feature/backend_tool_rendering");
810

911
// Verify suggestion buttons are visible
10-
await expect(page.getByRole("button", { name: "Weather in San Francisco" })).toBeVisible({
12+
await expect(
13+
page.getByRole("button", { name: "Weather in San Francisco" }),
14+
).toBeVisible({
1115
timeout: 5000,
1216
});
1317

@@ -26,29 +30,45 @@ test("[Agno] Backend Tool Rendering displays weather cards", async ({ page }) =>
2630
await expect(currentWeatherText.first()).toBeVisible({ timeout: 10000 });
2731
}
2832

29-
// Verify weather content is present (use flexible selectors)
33+
// Verify all weather data fields are present and correctly displayed
3034
const hasHumidity = await page
31-
.getByText("Humidity")
35+
.getByTestId("weather-humidity")
3236
.isVisible()
3337
.catch(() => false);
3438
const hasWind = await page
35-
.getByText("Wind")
39+
.getByTestId("weather-wind")
40+
.isVisible()
41+
.catch(() => false);
42+
const hasFeelsLike = await page
43+
.getByTestId("weather-feels-like")
3644
.isVisible()
3745
.catch(() => false);
3846
const hasCityName = await page
39-
.locator("h3")
47+
.getByTestId("weather-city")
4048
.filter({ hasText: /San Francisco/i })
4149
.isVisible()
4250
.catch(() => false);
4351

44-
// At least one of these should be true
45-
expect(hasHumidity || hasWind || hasCityName).toBeTruthy();
52+
// Verify all critical fields are present
53+
expect(hasHumidity).toBeTruthy();
54+
expect(hasWind).toBeTruthy();
55+
expect(hasFeelsLike).toBeTruthy();
56+
expect(hasCityName).toBeTruthy();
57+
58+
// Verify temperature is displayed (should show both C and F)
59+
const temperatureText = await page
60+
.locator("text=/\\d+°\\s*C/")
61+
.isVisible()
62+
.catch(() => false);
63+
expect(temperatureText).toBeTruthy();
4664

4765
// Click second suggestion
4866
await page.getByRole("button", { name: "Weather in New York" }).click();
4967
await page.waitForTimeout(2000);
5068

5169
// Verify at least one weather-related element is still visible
52-
const weatherElements = await page.getByText(/Weather|Humidity|Wind|Temperature/i).count();
70+
const weatherElements = await page
71+
.getByText(/Weather|Humidity|Wind|Temperature/i)
72+
.count();
5373
expect(weatherElements).toBeGreaterThan(0);
5474
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { test, expect, waitForAIResponse, retryOnAIFailure } from "../../test-isolation-helper";
2+
import { HumanInLoopPage } from "../../pages/agnoPages/HumanInLoopPage";
3+
4+
test.describe("Human in the Loop Feature", () => {
5+
test("[Agno] should interact with the chat and perform steps", async ({
6+
page,
7+
}) => {
8+
await retryOnAIFailure(async () => {
9+
const humanInLoop = new HumanInLoopPage(page);
10+
11+
await page.goto(
12+
"/agno/feature/human_in_the_loop"
13+
);
14+
15+
await humanInLoop.openChat();
16+
17+
await humanInLoop.sendMessage("Hi");
18+
await humanInLoop.agentGreeting.isVisible();
19+
20+
await humanInLoop.sendMessage(
21+
"Give me a plan to make brownies, there should be only one step with eggs and one step with oven, this is a strict requirement so adhere"
22+
);
23+
await waitForAIResponse(page);
24+
await expect(humanInLoop.plan).toBeVisible({ timeout: 10000 });
25+
26+
const itemText = "eggs";
27+
await page.waitForTimeout(5000);
28+
await humanInLoop.uncheckItem(itemText);
29+
await humanInLoop.performSteps();
30+
31+
await page.waitForFunction(
32+
() => {
33+
const messages = Array.from(document.querySelectorAll('.copilotKitAssistantMessage'));
34+
const lastMessage = messages[messages.length - 1];
35+
const content = lastMessage?.textContent?.trim() || '';
36+
return messages.length >= 3 && content.length > 0;
37+
},
38+
{ timeout: 30000 }
39+
);
40+
41+
await humanInLoop.sendMessage(
42+
`Does the planner include ${itemText}? ⚠️ Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`
43+
);
44+
await waitForAIResponse(page);
45+
});
46+
});
47+
48+
test("[Agno] should interact with the chat using predefined prompts and perform steps", async ({
49+
page,
50+
}) => {
51+
await retryOnAIFailure(async () => {
52+
const humanInLoop = new HumanInLoopPage(page);
53+
54+
await page.goto(
55+
"/agno/feature/human_in_the_loop"
56+
);
57+
58+
await humanInLoop.openChat();
59+
60+
await humanInLoop.sendMessage("Hi");
61+
await humanInLoop.agentGreeting.isVisible();
62+
await humanInLoop.sendMessage(
63+
"Plan a mission to Mars with the first step being Start The Planning"
64+
);
65+
await waitForAIResponse(page);
66+
await expect(humanInLoop.plan).toBeVisible({ timeout: 10000 });
67+
68+
const uncheckedItem = "Start The Planning";
69+
70+
await page.waitForTimeout(5000);
71+
await humanInLoop.uncheckItem(uncheckedItem);
72+
await humanInLoop.performSteps();
73+
74+
await page.waitForFunction(
75+
() => {
76+
const messages = Array.from(document.querySelectorAll('.copilotKitAssistantMessage'));
77+
const lastMessage = messages[messages.length - 1];
78+
const content = lastMessage?.textContent?.trim() || '';
79+
80+
return messages.length >= 3 && content.length > 0;
81+
},
82+
{ timeout: 30000 }
83+
);
84+
85+
await humanInLoop.sendMessage(
86+
`Does the planner include ${uncheckedItem}? ⚠️ Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`
87+
);
88+
await waitForAIResponse(page);
89+
});
90+
});
91+
});

apps/dojo/src/agents.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,9 @@ export const agentsIntegrations: AgentIntegrationConfig[] = [
273273
backend_tool_rendering: new AgnoAgent({
274274
url: `${envVars.agnoUrl}/backend_tool_rendering/agui`,
275275
}),
276+
human_in_the_loop: new AgnoAgent({
277+
url: `${envVars.agnoUrl}/human_in_the_loop/agui`,
278+
}),
276279
};
277280
},
278281
},

0 commit comments

Comments
 (0)