Skip to content

Commit ada29f2

Browse files
authored
Merge pull request #2372 from ag-ui-protocol/fix/aws-strands-python-a2ui-recovery-guide
fix(dojo): propagate A2UI catalog through per-agent middleware
2 parents b191a63 + 480de3f commit ada29f2

5 files changed

Lines changed: 157 additions & 22 deletions

File tree

apps/dojo/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"generate-content-json": "npx tsx scripts/generate-content-json.ts",
1212
"test:crewai-config": "tsx --test src/crewai.test.ts",
1313
"run-everything": "./scripts/prep-dojo-everything.js && ./scripts/run-dojo-everything.js",
14-
"local-install": "bash scripts/local-install.sh"
14+
"local-install": "bash scripts/local-install.sh",
15+
"test:a2ui-config": "tsx --test src/a2ui-config.test.ts"
1516
},
1617
"dependencies": {
1718
"@a2a-js/sdk": "0.2.5",
@@ -91,7 +92,8 @@
9192
"tailwindcss-animate": "^1.0.7",
9293
"untruncate-json": "^0.0.1",
9394
"uuid": "^11.1.0",
94-
"zod": "^3.25.75"
95+
"zod": "^3.25.75",
96+
"zod-to-json-schema": "3.25.2"
9597
},
9698
"peerDependencies": {
9799
"@ag-ui/client": "workspace:*",

apps/dojo/src/a2ui-config.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
4+
import {
5+
AbstractAgent,
6+
type BaseEvent,
7+
EventType,
8+
type RunAgentInput,
9+
} from "@ag-ui/client";
10+
import {
11+
A2UIMiddleware,
12+
A2UI_SCHEMA_CONTEXT_DESCRIPTION,
13+
} from "@ag-ui/a2ui-middleware";
14+
import { Observable, firstValueFrom, toArray } from "rxjs";
15+
16+
import { dynamicSchemaCatalog } from "./a2ui-catalog";
17+
import { DOJO_A2UI_MIDDLEWARE_CONFIG } from "./a2ui-config";
18+
19+
class CaptureAgent extends AbstractAgent {
20+
readonly runCalls: RunAgentInput[] = [];
21+
22+
run(input: RunAgentInput): Observable<BaseEvent> {
23+
this.runCalls.push(input);
24+
return new Observable((subscriber) => {
25+
subscriber.next({
26+
type: EventType.RUN_STARTED,
27+
threadId: input.threadId,
28+
runId: input.runId,
29+
});
30+
subscriber.next({
31+
type: EventType.RUN_FINISHED,
32+
threadId: input.threadId,
33+
runId: input.runId,
34+
});
35+
subscriber.complete();
36+
});
37+
}
38+
}
39+
40+
test("Dojo A2UI middleware forwards the rendered dynamic catalog", async () => {
41+
const downstream = new CaptureAgent();
42+
const middleware = new A2UIMiddleware(DOJO_A2UI_MIDDLEWARE_CONFIG);
43+
44+
await firstValueFrom(
45+
middleware
46+
.run(
47+
{
48+
threadId: "test-thread",
49+
runId: "test-run",
50+
tools: [],
51+
context: [],
52+
forwardedProps: {},
53+
state: {},
54+
messages: [],
55+
},
56+
downstream,
57+
)
58+
.pipe(toArray()),
59+
);
60+
61+
assert.equal(downstream.runCalls.length, 1);
62+
const forwarded = downstream.runCalls[0];
63+
const schemaContext = forwarded.context.find(
64+
({ description }) => description === A2UI_SCHEMA_CONTEXT_DESCRIPTION,
65+
);
66+
assert.ok(
67+
schemaContext && typeof schemaContext.value === "string",
68+
"dynamic catalog schema must reach downstream agent context",
69+
);
70+
71+
const schema = JSON.parse(schemaContext.value) as {
72+
catalogId: string;
73+
components: Record<
74+
string,
75+
{ allOf?: Array<{ properties?: Record<string, unknown> }> }
76+
>;
77+
};
78+
assert.equal(schema.catalogId, dynamicSchemaCatalog.id);
79+
assert.deepEqual(Object.keys(schema.components), [
80+
...dynamicSchemaCatalog.components.keys(),
81+
]);
82+
83+
const hotelProperties = schema.components.HotelCard.allOf?.find(
84+
({ properties }) => properties,
85+
)?.properties;
86+
assert.ok(hotelProperties);
87+
assert.ok("pricePerNight" in hotelProperties);
88+
const rating = hotelProperties.rating as {
89+
anyOf?: Array<{ type?: string }>;
90+
};
91+
assert.ok(rating.anyOf?.some(({ type }) => type === "number"));
92+
});

apps/dojo/src/a2ui-config.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { A2UIMiddlewareConfig } from "@ag-ui/a2ui-middleware";
2+
import { zodToJsonSchema } from "zod-to-json-schema";
3+
4+
import {
5+
HotelCardApi,
6+
ProductCardApi,
7+
RowApi,
8+
TeamMemberCardApi,
9+
} from "./a2ui-catalog/apis";
10+
11+
const DOJO_A2UI_CATALOG_ID = "https://a2ui.org/demos/dojo/dynamic_catalog.json";
12+
13+
const dynamicComponentApis = [
14+
RowApi,
15+
HotelCardApi,
16+
ProductCardApi,
17+
TeamMemberCardApi,
18+
];
19+
20+
const components = Object.fromEntries(
21+
dynamicComponentApis.map((componentApi) => {
22+
const schema = zodToJsonSchema(componentApi.schema, {
23+
target: "jsonSchema2019-09",
24+
}) as {
25+
properties?: Record<string, unknown>;
26+
required?: string[];
27+
};
28+
return [
29+
componentApi.name,
30+
{
31+
allOf: [
32+
{ $ref: "common_types.json#/$defs/ComponentCommon" },
33+
{
34+
properties: {
35+
component: { const: componentApi.name },
36+
...(schema.properties ?? {}),
37+
},
38+
required: ["component", ...(schema.required ?? [])],
39+
},
40+
],
41+
},
42+
];
43+
}),
44+
);
45+
46+
export const DOJO_A2UI_MIDDLEWARE_CONFIG = {
47+
injectA2UITool: true,
48+
defaultCatalogId: DOJO_A2UI_CATALOG_ID,
49+
schema: {
50+
catalogId: DOJO_A2UI_CATALOG_ID,
51+
components,
52+
},
53+
} satisfies A2UIMiddlewareConfig;

apps/dojo/src/agents.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,14 @@ import { Ag2Agent } from "@ag-ui/ag2";
4444
import { LangroidHttpAgent } from "@ag-ui/langroid";
4545
import { WatsonxAgent } from "@ag-ui/watsonx";
4646
import { A2UIMiddleware } from "@ag-ui/a2ui-middleware";
47+
import { DOJO_A2UI_MIDDLEWARE_CONFIG } from "./a2ui-config";
4748
import {
4849
CREWAI_CONVERSATIONAL_AGENT_PATHS,
4950
CREWAI_FLOW_AGENT_PATHS,
5051
} from "./crewai";
5152

5253
const envVars = getEnvVars();
5354

54-
// Catalog the dojo's dynamic A2UI demos render against (HotelCard / ProductCard
55-
// / TeamMemberCard / Row).
56-
const A2UI_DOJO_CATALOG_ID = "https://a2ui.org/demos/dojo/dynamic_catalog.json";
57-
5855
// Per-agent A2UI inject whitelist for the adk-middleware integration. These
5956
// subagent demos wire no a2ui tool themselves and rely on the adapter
6057
// auto-injecting `generate_a2ui` when it sees `injectA2UITool`. Injection is
@@ -101,10 +98,7 @@ function createCrewAIIntegrationAgents<const T extends Record<string, string>>(
10198
);
10299
for (const id of CREWAI_A2UI_INJECT_AGENTS) {
103100
(agents as Record<string, AbstractAgent>)[id]?.use(
104-
new A2UIMiddleware({
105-
injectA2UITool: true,
106-
defaultCatalogId: A2UI_DOJO_CATALOG_ID,
107-
}),
101+
new A2UIMiddleware(DOJO_A2UI_MIDDLEWARE_CONFIG),
108102
);
109103
}
110104
return agents;
@@ -155,10 +149,7 @@ export const agentsIntegrations = {
155149
// Whitelist-driven per-agent A2UI injection (see ADK_A2UI_INJECT_AGENTS).
156150
for (const id of ADK_A2UI_INJECT_AGENTS) {
157151
(agents as Record<string, AbstractAgent>)[id]?.use(
158-
new A2UIMiddleware({
159-
injectA2UITool: true,
160-
defaultCatalogId: A2UI_DOJO_CATALOG_ID,
161-
}),
152+
new A2UIMiddleware(DOJO_A2UI_MIDDLEWARE_CONFIG),
162153
);
163154
}
164155
return agents;
@@ -620,10 +611,7 @@ export const agentsIntegrations = {
620611
};
621612
for (const id of STRANDS_A2UI_INJECT_AGENTS) {
622613
(agents as Record<string, AbstractAgent>)[id]?.use(
623-
new A2UIMiddleware({
624-
injectA2UITool: true,
625-
defaultCatalogId: A2UI_DOJO_CATALOG_ID,
626-
}),
614+
new A2UIMiddleware(DOJO_A2UI_MIDDLEWARE_CONFIG),
627615
);
628616
}
629617
return agents;
@@ -669,10 +657,7 @@ export const agentsIntegrations = {
669657
};
670658
for (const id of STRANDS_A2UI_INJECT_AGENTS) {
671659
(agents as Record<string, AbstractAgent>)[id]?.use(
672-
new A2UIMiddleware({
673-
injectA2UITool: true,
674-
defaultCatalogId: A2UI_DOJO_CATALOG_ID,
675-
}),
660+
new A2UIMiddleware(DOJO_A2UI_MIDDLEWARE_CONFIG),
676661
);
677662
}
678663
return agents;

pnpm-lock.yaml

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

0 commit comments

Comments
 (0)