Skip to content

Commit 392b628

Browse files
tylerslatonmaxkorp
andauthored
Make some behavioral changes to strands (#761)
Co-authored-by: Max Korp <[email protected]>
1 parent dae7c94 commit 392b628

File tree

18 files changed

+3121
-281
lines changed

18 files changed

+3121
-281
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+
}
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/awsStrandsPages/HumanInLoopPage";
3+
4+
test.describe("Human in the Loop Feature", () => {
5+
test("[Strands] 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+
"/aws-strands/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("[Strands] 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+
"/aws-strands/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/e2e/tests/awsStrandsTests/sharedStatePage.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ test.describe("Shared State Feature", () => {
1313
);
1414

1515
await sharedStateAgent.openChat();
16-
await sharedStateAgent.sendMessage('Please give me a pasta recipe of your choosing, but one of the ingredients should be "Pasta"');
16+
await sharedStateAgent.sendMessage('Please give me a pasta recipe of your choosing, but one of the ingredients should be "Pasta". Not a type of pasta, exactly the word "Pasta".');
1717
await sharedStateAgent.loader();
1818
await sharedStateAgent.awaitIngredientCard('Pasta');
1919
await sharedStateAgent.getInstructionItems(
@@ -44,7 +44,7 @@ test.describe("Shared State Feature", () => {
4444
await page.waitForTimeout(1000);
4545

4646
// Ask chat for all ingredients
47-
await sharedStateAgent.sendMessage("Give me all the ingredients");
47+
await sharedStateAgent.sendMessage("Please list all of the ingredients");
4848
await sharedStateAgent.loader();
4949

5050
// Verify chat response includes both existing and new ingredients

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ const ALL_TARGETS = {
117117
name: "Dojo",
118118
cwd: gitRoot,
119119
},
120+
"dojo-dev": {
121+
command: "pnpm install --no-frozen-lockfile && pnpm build --filter=demo-viewer^...",
122+
name: "Dojo (dev)",
123+
cwd: gitRoot,
124+
},
120125
"microsoft-agent-framework-python": {
121126
command: "uv sync",
122127
name: "Microsoft Agent Framework (Python)",
@@ -149,6 +154,10 @@ async function main() {
149154
selectedKeys = selectedKeys.filter((k) => !excludeList.includes(k));
150155
}
151156

157+
if (selectedKeys.includes("dojo") && selectedKeys.includes("dojo-dev")) {
158+
selectedKeys= selectedKeys.filter(x => x != "dojo-dev");
159+
}
160+
152161
// Build procs list, warning on unknown keys
153162
const procs = [];
154163
for (const key of selectedKeys) {

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,31 @@ const ALL_SERVICES = {
184184
NEXT_PUBLIC_CUSTOM_DOMAIN_TITLE: 'cpkdojo.local___CopilotKit Feature Viewer',
185185
},
186186
}],
187+
'dojo-dev': [{
188+
command: 'pnpm run dev --filter=demo-viewer...',
189+
name: 'Dojo (dev)',
190+
cwd: gitRoot,
191+
env: {
192+
PORT: 9999,
193+
SERVER_STARTER_URL: 'http://localhost:8000',
194+
SERVER_STARTER_ALL_FEATURES_URL: 'http://localhost:8001',
195+
AGNO_URL: 'http://localhost:8002',
196+
CREW_AI_URL: 'http://localhost:8003',
197+
LANGGRAPH_FAST_API_URL: 'http://localhost:8004',
198+
LANGGRAPH_PYTHON_URL: 'http://localhost:8005',
199+
LANGGRAPH_TYPESCRIPT_URL: 'http://localhost:8006',
200+
LLAMA_INDEX_URL: 'http://localhost:8007',
201+
MASTRA_URL: 'http://localhost:8008',
202+
PYDANTIC_AI_URL: 'http://localhost:8009',
203+
ADK_MIDDLEWARE_URL: 'http://localhost:8010',
204+
A2A_MIDDLEWARE_BUILDINGS_MANAGEMENT_URL: 'http://localhost:8011',
205+
A2A_MIDDLEWARE_FINANCE_URL: 'http://localhost:8012',
206+
A2A_MIDDLEWARE_IT_URL: 'http://localhost:8013',
207+
A2A_MIDDLEWARE_ORCHESTRATOR_URL: 'http://localhost:8014',
208+
AWS_STRANDS_URL: 'http://localhost:8017',
209+
NEXT_PUBLIC_CUSTOM_DOMAIN_TITLE: 'cpkdojo.local___CopilotKit Feature Viewer',
210+
},
211+
}],
187212
};
188213

189214
function printDryRunServices(procs) {
@@ -214,6 +239,10 @@ async function main() {
214239
selectedKeys = selectedKeys.filter((k) => !excludeList.includes(k));
215240
}
216241

242+
if (selectedKeys.includes("dojo") && selectedKeys.includes("dojo-dev")) {
243+
selectedKeys= selectedKeys.filter(x => x != "dojo-dev");
244+
}
245+
217246
// Build processes, warn for unknown keys
218247
const procs = [];
219248
for (const key of selectedKeys) {

apps/dojo/src/agents.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ export const agentsIntegrations: AgentIntegrationConfig[] = [
463463
backend_tool_rendering: new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/backend-tool-rendering/` }),
464464
agentic_generative_ui: new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/agentic-generative-ui/` }),
465465
shared_state: new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/shared-state/` }),
466+
human_in_the_loop: new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/human-in-the-loop/`, debug: true }),
466467
};
467468
},
468469
},

apps/dojo/src/menu.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ export const menuIntegrations: MenuIntegrationConfig[] = [
210210
"backend_tool_rendering",
211211
"agentic_generative_ui",
212212
"shared_state",
213+
"human_in_the_loop",
213214
],
214215
},
215216
];

integrations/aws-strands/python/examples/poetry.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

integrations/aws-strands/python/examples/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ packages = [{ include = "server" }]
88

99
[tool.poetry.dependencies]
1010
python = "<3.14,>=3.12"
11-
ag-ui-protocol = "^0.1.5"
11+
ag-ui-protocol = "^0.1.10"
1212
fastapi = "^0.115.12"
1313
uvicorn = "^0.34.3"
1414
strands-agents = {extras = ["gemini"], version = "^1.15.0"}
@@ -20,4 +20,4 @@ requires = ["poetry-core"]
2020
build-backend = "poetry.core.masonry.api"
2121

2222
[tool.poetry.scripts]
23-
dev = "server:main"
23+
dev = "server:main"

integrations/aws-strands/python/examples/server/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
agentic_chat_app,
2929
agentic_generative_ui_app,
3030
backend_tool_rendering_app,
31+
human_in_the_loop_app,
3132
shared_state_app,
3233
)
3334

@@ -48,6 +49,7 @@
4849
app.mount('/backend-tool-rendering', backend_tool_rendering_app, 'Backend Tool Rendering')
4950
app.mount('/agentic-generative-ui', agentic_generative_ui_app, 'Agentic Generative UI')
5051
app.mount('/shared-state', shared_state_app, 'Shared State')
52+
app.mount('/human-in-the-loop', human_in_the_loop_app, 'Human in the Loop')
5153

5254
@app.get("/")
5355
def root():
@@ -70,4 +72,3 @@ def main():
7072
main()
7173

7274
__all__ = ["main", "app"]
73-

0 commit comments

Comments
 (0)