Skip to content

Commit dae7c94

Browse files
UmarMajeed-RanaRana Umar MajeedRana Umar Majeed
authored
Add the Strands Integration to AG-UI (#675)
feat: Strands integration --------- Co-authored-by: Rana Umar Majeed <[email protected]> Co-authored-by: Rana Umar Majeed <[email protected]>
1 parent d0e095f commit dae7c94

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+5763
-83
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Page, Locator, expect } from '@playwright/test';
2+
3+
export class AgenticGenUIPage {
4+
readonly page: Page;
5+
readonly chatInput: Locator;
6+
readonly planTaskButton: Locator;
7+
readonly agentMessage: Locator;
8+
readonly userMessage: Locator;
9+
readonly agentGreeting: Locator;
10+
readonly agentPlannerContainer: Locator;
11+
readonly sendButton: Locator;
12+
13+
constructor(page: Page) {
14+
this.page = page;
15+
this.planTaskButton = page.getByRole('button', { name: 'Agentic Generative UI' });
16+
this.chatInput = page.getByRole('textbox', { name: 'Type a message...' });
17+
this.sendButton = page.locator('[data-test-id="copilot-chat-ready"]');
18+
this.agentMessage = page.locator('.copilotKitAssistantMessage');
19+
this.userMessage = page.locator('.copilotKitUserMessage');
20+
this.agentGreeting = page.getByText('This agent demonstrates');
21+
this.agentPlannerContainer = page.getByTestId('task-progress');
22+
}
23+
24+
async plan() {
25+
const stepItems = this.agentPlannerContainer.getByTestId('task-step-text');
26+
const count = await stepItems.count();
27+
expect(count).toBeGreaterThan(0);
28+
for (let i = 0; i < count; i++) {
29+
const stepText = await stepItems.nth(i).textContent();
30+
console.log(`Step ${i + 1}: ${stepText?.trim()}`);
31+
await expect(stepItems.nth(i)).toBeVisible();
32+
}
33+
}
34+
35+
async openChat() {
36+
await this.planTaskButton.isVisible();
37+
}
38+
39+
async sendMessage(message: string) {
40+
await this.chatInput.fill(message);
41+
await this.page.waitForTimeout(5000)
42+
}
43+
44+
getPlannerButton(name: string | RegExp) {
45+
return this.page.getByRole('button', { name });
46+
}
47+
48+
async assertAgentReplyVisible(expectedText: RegExp) {
49+
await expect(this.agentMessage.last().getByText(expectedText)).toBeVisible();
50+
}
51+
52+
async getUserText(textOrRegex) {
53+
return await this.page.getByText(textOrRegex).isVisible();
54+
}
55+
56+
async assertUserMessageVisible(message: string) {
57+
await expect(this.userMessage.getByText(message)).toBeVisible();
58+
}
59+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import {
2+
test,
3+
expect,
4+
waitForAIResponse,
5+
retryOnAIFailure,
6+
} from "../../test-isolation-helper";
7+
import { AgenticChatPage } from "../../featurePages/AgenticChatPage";
8+
9+
test("[Strands] Agentic Chat sends and receives a message", async ({
10+
page,
11+
}) => {
12+
await retryOnAIFailure(async () => {
13+
await page.goto(
14+
"/aws-strands/feature/agentic_chat"
15+
);
16+
17+
const chat = new AgenticChatPage(page);
18+
19+
await chat.openChat();
20+
await chat.agentGreeting.isVisible;
21+
await chat.sendMessage("Hi, I am duaa");
22+
23+
await waitForAIResponse(page);
24+
await chat.assertUserMessageVisible("Hi, I am duaa");
25+
await chat.assertAgentReplyVisible(/Hello/i);
26+
});
27+
});
28+
29+
test("[Strands] Agentic Chat changes background on message and reset", async ({
30+
page,
31+
}) => {
32+
await retryOnAIFailure(async () => {
33+
await page.goto(
34+
"/aws-strands/feature/agentic_chat"
35+
);
36+
37+
const chat = new AgenticChatPage(page);
38+
39+
await chat.openChat();
40+
await chat.agentGreeting.waitFor({ state: "visible" });
41+
42+
// Store initial background color
43+
const backgroundContainer = page.locator('[data-testid="background-container"]')
44+
const initialBackground = await backgroundContainer.evaluate(el => getComputedStyle(el).backgroundColor);
45+
console.log("Initial background color:", initialBackground);
46+
47+
// 1. Send message to change background to blue
48+
await chat.sendMessage("Hi change the background color to blue");
49+
await chat.assertUserMessageVisible(
50+
"Hi change the background color to blue"
51+
);
52+
await waitForAIResponse(page);
53+
54+
await expect(backgroundContainer).not.toHaveCSS('background-color', initialBackground, { timeout: 7000 });
55+
const backgroundBlue = await backgroundContainer.evaluate(el => getComputedStyle(el).backgroundColor);
56+
// Check if background is blue (string color name or contains blue)
57+
expect(backgroundBlue.toLowerCase()).toMatch(/blue|rgb\(.*,.*,.*\)|#[0-9a-f]{6}/);
58+
59+
// 2. Change to pink
60+
await chat.sendMessage("Hi change the background color to pink");
61+
await chat.assertUserMessageVisible(
62+
"Hi change the background color to pink"
63+
);
64+
await waitForAIResponse(page);
65+
66+
await expect(backgroundContainer).not.toHaveCSS('background-color', backgroundBlue, { timeout: 7000 });
67+
const backgroundPink = await backgroundContainer.evaluate(el => getComputedStyle(el).backgroundColor);
68+
// Check if background is pink (string color name or contains pink)
69+
expect(backgroundPink.toLowerCase()).toMatch(/pink|rgb\(.*,.*,.*\)|#[0-9a-f]{6}/);
70+
});
71+
});
72+
73+
test("[Strands] Agentic Chat retains memory of user messages during a conversation", async ({
74+
page,
75+
}) => {
76+
await retryOnAIFailure(async () => {
77+
await page.goto(
78+
"/aws-strands/feature/agentic_chat"
79+
);
80+
81+
const chat = new AgenticChatPage(page);
82+
await chat.openChat();
83+
await chat.agentGreeting.click();
84+
85+
await chat.sendMessage("Hey there");
86+
await chat.assertUserMessageVisible("Hey there");
87+
await waitForAIResponse(page);
88+
await chat.assertAgentReplyVisible(/how can I assist you/i);
89+
90+
const favFruit = "Mango";
91+
await chat.sendMessage(`My favorite fruit is ${favFruit}`);
92+
await chat.assertUserMessageVisible(`My favorite fruit is ${favFruit}`);
93+
await waitForAIResponse(page);
94+
await chat.assertAgentReplyVisible(new RegExp(favFruit, "i"));
95+
96+
await chat.sendMessage("and I love listening to Kaavish");
97+
await chat.assertUserMessageVisible("and I love listening to Kaavish");
98+
await waitForAIResponse(page);
99+
await chat.assertAgentReplyVisible(/Kaavish/i);
100+
101+
await chat.sendMessage("tell me an interesting fact about Moon");
102+
await chat.assertUserMessageVisible(
103+
"tell me an interesting fact about Moon"
104+
);
105+
await waitForAIResponse(page);
106+
await chat.assertAgentReplyVisible(/Moon/i);
107+
108+
await chat.sendMessage("Can you remind me what my favorite fruit is?");
109+
await chat.assertUserMessageVisible(
110+
"Can you remind me what my favorite fruit is?"
111+
);
112+
await waitForAIResponse(page);
113+
await chat.assertAgentReplyVisible(new RegExp(favFruit, "i"));
114+
});
115+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { test, expect } from "@playwright/test";
2+
import { AgenticGenUIPage } from "../../pages/awsStrandsPages/AgenticUIGenPage";
3+
4+
test.describe("Agent Generative UI Feature", () => {
5+
// Flaky. Sometimes the steps render but never process.
6+
test("[Strands] should interact with the chat to get a planner on prompt", async ({
7+
page,
8+
}) => {
9+
const genUIAgent = new AgenticGenUIPage(page);
10+
11+
await page.goto(
12+
"/aws-strands/feature/agentic_generative_ui"
13+
);
14+
15+
await genUIAgent.openChat();
16+
await genUIAgent.sendMessage("Hi");
17+
await genUIAgent.sendButton.click();
18+
await genUIAgent.assertAgentReplyVisible(/Hello/);
19+
20+
await genUIAgent.sendMessage("give me a plan to make brownies");
21+
await genUIAgent.sendButton.click();
22+
await expect(genUIAgent.agentPlannerContainer).toBeVisible({ timeout: 15000 });
23+
await genUIAgent.plan();
24+
25+
await page.waitForFunction(
26+
() => {
27+
const messages = Array.from(document.querySelectorAll('.copilotKitAssistantMessage'));
28+
const lastMessage = messages[messages.length - 1];
29+
const content = lastMessage?.textContent?.trim() || '';
30+
31+
return messages.length >= 3 && content.length > 0;
32+
},
33+
{ timeout: 30000 }
34+
);
35+
});
36+
37+
test("[Strands] should interact with the chat using predefined prompts and perform steps", async ({
38+
page,
39+
}) => {
40+
const genUIAgent = new AgenticGenUIPage(page);
41+
42+
await page.goto(
43+
"/aws-strands/feature/agentic_generative_ui"
44+
);
45+
46+
await genUIAgent.openChat();
47+
await genUIAgent.sendMessage("Hi");
48+
await genUIAgent.sendButton.click();
49+
await genUIAgent.assertAgentReplyVisible(/Hello/);
50+
51+
await genUIAgent.sendMessage("Go to Mars");
52+
await genUIAgent.sendButton.click();
53+
54+
await expect(genUIAgent.agentPlannerContainer).toBeVisible({ timeout: 15000 });
55+
await genUIAgent.plan();
56+
57+
await page.waitForFunction(
58+
() => {
59+
const messages = Array.from(document.querySelectorAll('.copilotKitAssistantMessage'));
60+
const lastMessage = messages[messages.length - 1];
61+
const content = lastMessage?.textContent?.trim() || '';
62+
63+
return messages.length >= 3 && content.length > 0;
64+
},
65+
{ timeout: 30000 }
66+
);
67+
});
68+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
test("[Strands] Backend Tool Rendering displays weather cards", async ({ page }) => {
4+
// Set shorter default timeout for this test
5+
test.setTimeout(30000); // 30 seconds total
6+
7+
await page.goto("/aws-strands/feature/backend_tool_rendering");
8+
9+
// Verify suggestion buttons are visible
10+
await expect(page.getByRole("button", { name: "Weather in San Francisco" })).toBeVisible({
11+
timeout: 5000,
12+
});
13+
14+
// Click first suggestion and verify weather card appears
15+
await page.getByRole("button", { name: "Weather in San Francisco" }).click();
16+
17+
// Wait for either test ID or fallback to "Current Weather" text
18+
const weatherCard = page.getByTestId("weather-card");
19+
const currentWeatherText = page.getByText("Current Weather");
20+
21+
// Try test ID first, fallback to text
22+
try {
23+
await expect(weatherCard).toBeVisible({ timeout: 10000 });
24+
} catch (e) {
25+
// Fallback to checking for "Current Weather" text
26+
await expect(currentWeatherText.first()).toBeVisible({ timeout: 10000 });
27+
}
28+
29+
// Verify weather content is present (use flexible selectors)
30+
const hasHumidity = await page
31+
.getByText("Humidity")
32+
.isVisible()
33+
.catch(() => false);
34+
const hasWind = await page
35+
.getByText("Wind")
36+
.isVisible()
37+
.catch(() => false);
38+
const hasCityName = await page
39+
.locator("h3")
40+
.filter({ hasText: /San Francisco/i })
41+
.isVisible()
42+
.catch(() => false);
43+
44+
// At least one of these should be true
45+
expect(hasHumidity || hasWind || hasCityName).toBeTruthy();
46+
47+
// Click second suggestion
48+
await page.getByRole("button", { name: "Weather in New York" }).click();
49+
await page.waitForTimeout(2000);
50+
51+
// Verify at least one weather-related element is still visible
52+
const weatherElements = await page.getByText(/Weather|Humidity|Wind|Temperature/i).count();
53+
expect(weatherElements).toBeGreaterThan(0);
54+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { test, expect } from "@playwright/test";
2+
import { SharedStatePage } from "../../featurePages/SharedStatePage";
3+
4+
test.describe("Shared State Feature", () => {
5+
test("[Strands] should interact with the chat to get a recipe on prompt", async ({
6+
page,
7+
}) => {
8+
const sharedStateAgent = new SharedStatePage(page);
9+
10+
// Update URL to new domain
11+
await page.goto(
12+
"/aws-strands/feature/shared_state"
13+
);
14+
15+
await sharedStateAgent.openChat();
16+
await sharedStateAgent.sendMessage('Please give me a pasta recipe of your choosing, but one of the ingredients should be "Pasta"');
17+
await sharedStateAgent.loader();
18+
await sharedStateAgent.awaitIngredientCard('Pasta');
19+
await sharedStateAgent.getInstructionItems(
20+
sharedStateAgent.instructionsContainer
21+
);
22+
});
23+
24+
test("[Strands] should share state between UI and chat", async ({
25+
page,
26+
}) => {
27+
const sharedStateAgent = new SharedStatePage(page);
28+
29+
await page.goto(
30+
"/aws-strands/feature/shared_state"
31+
);
32+
33+
await sharedStateAgent.openChat();
34+
35+
// Add new ingredient via UI
36+
await sharedStateAgent.addIngredient.click();
37+
38+
// Fill in the new ingredient details
39+
const newIngredientCard = page.locator('.ingredient-card').last();
40+
await newIngredientCard.locator('.ingredient-name-input').fill('Potatoes');
41+
await newIngredientCard.locator('.ingredient-amount-input').fill('12');
42+
43+
// Wait for UI to update
44+
await page.waitForTimeout(1000);
45+
46+
// Ask chat for all ingredients
47+
await sharedStateAgent.sendMessage("Give me all the ingredients");
48+
await sharedStateAgent.loader();
49+
50+
// Verify chat response includes both existing and new ingredients
51+
await expect(sharedStateAgent.agentMessage.getByText(/Potatoes/)).toBeVisible();
52+
await expect(sharedStateAgent.agentMessage.getByText(/12/)).toBeVisible();
53+
await expect(sharedStateAgent.agentMessage.getByText(/Carrots/)).toBeVisible();
54+
await expect(sharedStateAgent.agentMessage.getByText(/All-Purpose Flour/)).toBeVisible();
55+
});
56+
});

apps/dojo/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@ag-ui/server-starter-all-features": "workspace:*",
2828
"@ag-ui/spring-ai": "workspace:*",
2929
"@ag-ui/vercel-ai-sdk": "workspace:*",
30+
"@ag-ui/aws-strands-integration": "workspace:*",
3031
"@ai-sdk/openai": "^2.0.42",
3132
"@copilotkit/react-core": "1.10.6",
3233
"@copilotkit/react-ui": "1.10.6",

apps/dojo/scripts/prep-dojo-everything.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ const ALL_TARGETS = {
9797
name: "Pydantic AI",
9898
cwd: path.join(integrationsRoot, "pydantic-ai/python/examples"),
9999
},
100+
"aws-strands": {
101+
command: "poetry install",
102+
name: "AWS Strands",
103+
cwd: path.join(integrationsRoot, "aws-strands/python/examples"),
104+
},
100105
"adk-middleware": {
101106
command: "uv sync",
102107
name: "ADK Middleware",

apps/dojo/scripts/run-dojo-everything.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ const ALL_SERVICES = {
111111
cwd: path.join(integrationsRoot, 'pydantic-ai/python/examples'),
112112
env: { PORT: 8009 },
113113
}],
114+
'aws-strands': [{
115+
command: 'poetry run dev',
116+
name: 'AWS Strands',
117+
cwd: path.join(integrationsRoot, 'aws-strands/python/examples'),
118+
env: { PORT: 8017 },
119+
}],
114120
'adk-middleware': [{
115121
command: 'uv run dev',
116122
name: 'ADK Middleware',
@@ -174,6 +180,7 @@ const ALL_SERVICES = {
174180
A2A_MIDDLEWARE_FINANCE_URL: 'http://localhost:8012',
175181
A2A_MIDDLEWARE_IT_URL: 'http://localhost:8013',
176182
A2A_MIDDLEWARE_ORCHESTRATOR_URL: 'http://localhost:8014',
183+
AWS_STRANDS_URL: 'http://localhost:8017',
177184
NEXT_PUBLIC_CUSTOM_DOMAIN_TITLE: 'cpkdojo.local___CopilotKit Feature Viewer',
178185
},
179186
}],

0 commit comments

Comments
 (0)