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
91 changes: 91 additions & 0 deletions apps/dojo/e2e/pages/awsStrandsPages/HumanInLoopPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Page, Locator, expect } from '@playwright/test';

export class HumanInLoopPage {
readonly page: Page;
readonly planTaskButton: Locator;
readonly chatInput: Locator;
readonly sendButton: Locator;
readonly agentGreeting: Locator;
readonly plan: Locator;
readonly performStepsButton: Locator;
readonly agentMessage: Locator;
readonly userMessage: Locator;

constructor(page: Page) {
this.page = page;
this.planTaskButton = page.getByRole('button', { name: 'Human in the loop Plan a task' });
this.agentGreeting = page.getByText("Hi, I'm an agent specialized in helping you with your tasks. How can I help you?");
this.chatInput = page.getByRole('textbox', { name: 'Type a message...' });
this.sendButton = page.locator('[data-test-id="copilot-chat-ready"]');
this.plan = page.getByTestId('select-steps');
this.performStepsButton = page.getByRole('button', { name: 'Confirm' });
this.agentMessage = page.locator('.copilotKitAssistantMessage');
this.userMessage = page.locator('.copilotKitUserMessage');
}

async openChat() {
await this.agentGreeting.isVisible();
}

async sendMessage(message: string) {
await this.chatInput.click();
await this.chatInput.fill(message);
await this.sendButton.click();
}

async selectItemsInPlanner() {
await expect(this.plan).toBeVisible({ timeout: 10000 });
await this.plan.click();
}

async getPlannerOnClick(name: string | RegExp) {
return this.page.getByRole('button', { name });
}

async uncheckItem(identifier: number | string): Promise<string> {
const plannerContainer = this.page.getByTestId('select-steps');
const items = plannerContainer.getByTestId('step-item');

let item;
if (typeof identifier === 'number') {
item = items.nth(identifier);
} else {
item = items.filter({
has: this.page.getByTestId('step-text').filter({ hasText: identifier })
}).first();
}
const stepTextElement = item.getByTestId('step-text');
const text = await stepTextElement.innerText();
await item.click();

return text;
}

async isStepItemUnchecked(target: number | string): Promise<boolean> {
const plannerContainer = this.page.getByTestId('select-steps');
const items = plannerContainer.getByTestId('step-item');

let item;
if (typeof target === 'number') {
item = items.nth(target);
} else {
item = items.filter({
has: this.page.getByTestId('step-text').filter({ hasText: target })
}).first();
}
const checkbox = item.locator('input[type="checkbox"]');
return !(await checkbox.isChecked());
}

async performSteps() {
await this.performStepsButton.click();
}

async assertAgentReplyVisible(expectedText: RegExp) {
await expect(this.agentMessage.last().getByText(expectedText)).toBeVisible();
}

async assertUserMessageVisible(message: string) {
await expect(this.page.getByText(message)).toBeVisible();
}
}
91 changes: 91 additions & 0 deletions apps/dojo/e2e/tests/awsStrandsTests/humanInTheLoopPage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { test, expect, waitForAIResponse, retryOnAIFailure } from "../../test-isolation-helper";
import { HumanInLoopPage } from "../../pages/awsStrandsPages/HumanInLoopPage";

test.describe("Human in the Loop Feature", () => {
test("[Strands] should interact with the chat and perform steps", async ({
page,
}) => {
await retryOnAIFailure(async () => {
const humanInLoop = new HumanInLoopPage(page);

await page.goto(
"/aws-strands/feature/human_in_the_loop"
);

await humanInLoop.openChat();

await humanInLoop.sendMessage("Hi");
await humanInLoop.agentGreeting.isVisible();

await humanInLoop.sendMessage(
"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"
);
await waitForAIResponse(page);
await expect(humanInLoop.plan).toBeVisible({ timeout: 10000 });

const itemText = "eggs";
await page.waitForTimeout(5000);
await humanInLoop.uncheckItem(itemText);
await humanInLoop.performSteps();

await page.waitForFunction(
() => {
const messages = Array.from(document.querySelectorAll('.copilotKitAssistantMessage'));
const lastMessage = messages[messages.length - 1];
const content = lastMessage?.textContent?.trim() || '';
return messages.length >= 3 && content.length > 0;
},
{ timeout: 30000 }
);

await humanInLoop.sendMessage(
`Does the planner include ${itemText}? ⚠️ Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`
);
await waitForAIResponse(page);
});
});

test("[Strands] should interact with the chat using predefined prompts and perform steps", async ({
page,
}) => {
await retryOnAIFailure(async () => {
const humanInLoop = new HumanInLoopPage(page);

await page.goto(
"/aws-strands/feature/human_in_the_loop"
);

await humanInLoop.openChat();

await humanInLoop.sendMessage("Hi");
await humanInLoop.agentGreeting.isVisible();
await humanInLoop.sendMessage(
"Plan a mission to Mars with the first step being Start The Planning"
);
await waitForAIResponse(page);
await expect(humanInLoop.plan).toBeVisible({ timeout: 10000 });

const uncheckedItem = "Start The Planning";

await page.waitForTimeout(5000);
await humanInLoop.uncheckItem(uncheckedItem);
await humanInLoop.performSteps();

await page.waitForFunction(
() => {
const messages = Array.from(document.querySelectorAll('.copilotKitAssistantMessage'));
const lastMessage = messages[messages.length - 1];
const content = lastMessage?.textContent?.trim() || '';

return messages.length >= 3 && content.length > 0;
},
{ timeout: 30000 }
);

await humanInLoop.sendMessage(
`Does the planner include ${uncheckedItem}? ⚠️ Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`
);
await waitForAIResponse(page);
});
});
});
4 changes: 2 additions & 2 deletions apps/dojo/e2e/tests/awsStrandsTests/sharedStatePage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ test.describe("Shared State Feature", () => {
);

await sharedStateAgent.openChat();
await sharedStateAgent.sendMessage('Please give me a pasta recipe of your choosing, but one of the ingredients should be "Pasta"');
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".');
await sharedStateAgent.loader();
await sharedStateAgent.awaitIngredientCard('Pasta');
await sharedStateAgent.getInstructionItems(
Expand Down Expand Up @@ -44,7 +44,7 @@ test.describe("Shared State Feature", () => {
await page.waitForTimeout(1000);

// Ask chat for all ingredients
await sharedStateAgent.sendMessage("Give me all the ingredients");
await sharedStateAgent.sendMessage("Please list all of the ingredients");
await sharedStateAgent.loader();

// Verify chat response includes both existing and new ingredients
Expand Down
9 changes: 9 additions & 0 deletions apps/dojo/scripts/prep-dojo-everything.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ const ALL_TARGETS = {
name: "Dojo",
cwd: gitRoot,
},
"dojo-dev": {
command: "pnpm install --no-frozen-lockfile && pnpm build --filter=demo-viewer^...",
name: "Dojo (dev)",
cwd: gitRoot,
},
"microsoft-agent-framework-python": {
command: "uv sync",
name: "Microsoft Agent Framework (Python)",
Expand Down Expand Up @@ -149,6 +154,10 @@ async function main() {
selectedKeys = selectedKeys.filter((k) => !excludeList.includes(k));
}

if (selectedKeys.includes("dojo") && selectedKeys.includes("dojo-dev")) {
selectedKeys= selectedKeys.filter(x => x != "dojo-dev");
}

// Build procs list, warning on unknown keys
const procs = [];
for (const key of selectedKeys) {
Expand Down
29 changes: 29 additions & 0 deletions apps/dojo/scripts/run-dojo-everything.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,31 @@ const ALL_SERVICES = {
NEXT_PUBLIC_CUSTOM_DOMAIN_TITLE: 'cpkdojo.local___CopilotKit Feature Viewer',
},
}],
'dojo-dev': [{
command: 'pnpm run dev --filter=demo-viewer...',
name: 'Dojo (dev)',
cwd: gitRoot,
env: {
PORT: 9999,
SERVER_STARTER_URL: 'http://localhost:8000',
SERVER_STARTER_ALL_FEATURES_URL: 'http://localhost:8001',
AGNO_URL: 'http://localhost:8002',
CREW_AI_URL: 'http://localhost:8003',
LANGGRAPH_FAST_API_URL: 'http://localhost:8004',
LANGGRAPH_PYTHON_URL: 'http://localhost:8005',
LANGGRAPH_TYPESCRIPT_URL: 'http://localhost:8006',
LLAMA_INDEX_URL: 'http://localhost:8007',
MASTRA_URL: 'http://localhost:8008',
PYDANTIC_AI_URL: 'http://localhost:8009',
ADK_MIDDLEWARE_URL: 'http://localhost:8010',
A2A_MIDDLEWARE_BUILDINGS_MANAGEMENT_URL: 'http://localhost:8011',
A2A_MIDDLEWARE_FINANCE_URL: 'http://localhost:8012',
A2A_MIDDLEWARE_IT_URL: 'http://localhost:8013',
A2A_MIDDLEWARE_ORCHESTRATOR_URL: 'http://localhost:8014',
AWS_STRANDS_URL: 'http://localhost:8017',
NEXT_PUBLIC_CUSTOM_DOMAIN_TITLE: 'cpkdojo.local___CopilotKit Feature Viewer',
},
}],
};

function printDryRunServices(procs) {
Expand Down Expand Up @@ -214,6 +239,10 @@ async function main() {
selectedKeys = selectedKeys.filter((k) => !excludeList.includes(k));
}

if (selectedKeys.includes("dojo") && selectedKeys.includes("dojo-dev")) {
selectedKeys= selectedKeys.filter(x => x != "dojo-dev");
}

// Build processes, warn for unknown keys
const procs = [];
for (const key of selectedKeys) {
Expand Down
1 change: 1 addition & 0 deletions apps/dojo/src/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,7 @@ export const agentsIntegrations: AgentIntegrationConfig[] = [
backend_tool_rendering: new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/backend-tool-rendering/` }),
agentic_generative_ui: new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/agentic-generative-ui/` }),
shared_state: new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/shared-state/` }),
human_in_the_loop: new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/human-in-the-loop/`, debug: true }),
};
},
},
Expand Down
1 change: 1 addition & 0 deletions apps/dojo/src/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export const menuIntegrations: MenuIntegrationConfig[] = [
"backend_tool_rendering",
"agentic_generative_ui",
"shared_state",
"human_in_the_loop",
],
},
];
14 changes: 7 additions & 7 deletions integrations/aws-strands/python/examples/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions integrations/aws-strands/python/examples/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ packages = [{ include = "server" }]

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

[tool.poetry.scripts]
dev = "server:main"
dev = "server:main"
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
agentic_chat_app,
agentic_generative_ui_app,
backend_tool_rendering_app,
human_in_the_loop_app,
shared_state_app,
)

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

@app.get("/")
def root():
Expand All @@ -70,4 +72,3 @@ def main():
main()

__all__ = ["main", "app"]

Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
from .agentic_chat import app as agentic_chat_app
from .agentic_generative_ui import app as agentic_generative_ui_app
from .backend_tool_rendering import app as backend_tool_rendering_app
from .human_in_the_loop import app as human_in_the_loop_app
from .shared_state import app as shared_state_app

__all__ = [
"agentic_chat_app",
"agentic_generative_ui_app",
"backend_tool_rendering_app",
"human_in_the_loop_app",
"shared_state_app",
]

Loading
Loading