chore: initialize sandbox and overwrite remote content
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled

This commit is contained in:
codex-bot
2026-03-02 22:32:27 +08:00
commit a64378956a
584 changed files with 93604 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
# Agent-to-Agent Protocol Example
The `A2AAgent` in AgentScope is an A2A client that connects to an external agent server via the Agent-to-Agent (A2A) protocol.
This example demonstrates how to set up and use the `A2AAgent` to interact with an agent hosted on an A2A server.
Note the A2A feature is experimental and subject to change, and due to the limitations of A2A protocol, the `A2AAgent`
currently
1. only supports chatbot scenarios, where only a user and an agent are involved
2. does not support realtime steering/interruption during the conversation
3. does not support agentic structured outputs
4. stores the observed messages locally and send them together with the input message(s) of the `reply` function
## Files
The example contains the following files:
```
examples/agent/a2a_agent
├── main.py # The main script to run the A2A agent example
├── setup_a2a_server.py # The script to set up a simple A2A server
├── agent_card.py # The agent card definition for the A2A agent
└── README.md # This README file
```
## Setup
This example provides a simple setup to demonstrate how to use the `A2AAgent` in AgentScope.
First you need to install the required dependencies:
```bash
uv pip install a2a-sdk[http-server] agentscope[a2a]
# or
pip install a2a-sdk[http-server] agentscope[a2a]
```
Then we first set up a simple A2A server that hosts a ReAct agent:
```bash
uvicorn setup_a2a_server:app --host 0.0.0.0 --port 8000
```
This will start an A2A server locally on port 8000.
After that, you can run the A2A agent example to run a chatbot conversation with the agent hosted on the A2A server:
```bash
python main.py
```

View File

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
"""The agent card definition for the A2A agent."""
from a2a.types import AgentCard, AgentCapabilities, AgentSkill
agent_card = AgentCard(
name="Friday",
description="A simple ReAct agent that handles input queries",
url="http://localhost:8000",
version="1.0.0",
capabilities=AgentCapabilities(
push_notifications=False,
state_transition_history=True,
streaming=True,
),
default_input_modes=["text/plain"],
default_output_modes=["text/plain"],
skills=[
AgentSkill(
name="execute_python_code",
id="execute_python_code",
description="Execute Python code snippets.",
tags=["code_execution"],
),
AgentSkill(
name="execute_shell_command",
id="execute_shell_command",
description="Execute shell commands on the server.",
tags=["code_execution"],
),
AgentSkill(
name="view_text_file",
id="view_text_file",
description="View the content of a text file on the server.",
tags=["file_viewing"],
),
],
)

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
"""The main entry for the A2A agent example."""
import asyncio
from agent_card import agent_card
from agentscope.agent import UserAgent, A2AAgent
async def main() -> None:
"""The main entry for the example, where we build a simple conversation
between the A2A agent and the user."""
user = UserAgent("user")
agent = A2AAgent(
agent_card=agent_card,
)
msg = None
while True:
msg = await user(msg)
if msg.get_text_content() == "exit":
break
msg = await agent(msg)
asyncio.run(main())

View File

@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
"""Set up an A2A server with a ReAct agent to handle the input query"""
import os
import uuid
from typing import AsyncGenerator, Any
from agent_card import agent_card
from a2a.server.events import Event
from a2a.types import (
TaskStatus,
TaskState,
MessageSendParams,
TaskStatusUpdateEvent,
)
from a2a.server.apps import A2AStarletteApplication
from agentscope.agent import ReActAgent
from agentscope.formatter import DashScopeChatFormatter, A2AChatFormatter
from agentscope.model import DashScopeChatModel
from agentscope.pipeline import stream_printing_messages
from agentscope.session import JSONSession
from agentscope.tool import (
Toolkit,
execute_python_code,
execute_shell_command,
view_text_file,
)
class SimpleStreamHandler:
"""A simple request handler that handles the input query by an
ReAct agent."""
async def on_message_send_stream(
self, # pylint: disable=unused-argument
params: MessageSendParams,
*args: Any,
**kwargs: Any,
) -> AsyncGenerator[Event, None]:
"""Handles the message_send method by the agent
Args:
params (`MessageSendParams`):
The parameters for sending the message.
Returns:
`AsyncGenerator[Event, None]`:
An asynchronous generator that yields task status update
events.
"""
task_id = params.message.task_id or uuid.uuid4().hex
context_id = params.message.context_id or "default-context"
# ============ Agent Logic ============
# Register the tool functions
toolkit = Toolkit()
toolkit.register_tool_function(execute_python_code)
toolkit.register_tool_function(execute_shell_command)
toolkit.register_tool_function(view_text_file)
# Create the agent instance
agent = ReActAgent(
name="Friday",
sys_prompt="You're a helpful assistant named Friday.",
model=DashScopeChatModel(
model_name="qwen-max",
api_key=os.getenv("DASHSCOPE_API_KEY"),
),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
)
session = JSONSession(save_dir="./sessions")
await session.load_session_state(
session_id="test-a2a-agent",
agent=agent,
)
# Convert the A2A message to AgentScope Msg objects
formatter = A2AChatFormatter()
as_msg = await formatter.format_a2a_message(
name="Friday",
message=params.message,
)
yield TaskStatusUpdateEvent(
task_id=task_id,
context_id=context_id,
status=TaskStatus(state=TaskState.working),
final=False,
)
async for msg, last in stream_printing_messages(
agents=[agent],
coroutine_task=agent(as_msg),
):
# The A2A streaming response is one complete Message object rather
# than accumulated or incremental text
if last:
a2a_message = await formatter.format([msg])
yield TaskStatusUpdateEvent(
task_id=task_id,
context_id=context_id,
status=TaskStatus(
state=TaskState.working,
message=a2a_message,
),
final=False,
)
# Finish the task
yield TaskStatusUpdateEvent(
task_id=task_id,
context_id=context_id,
status=TaskStatus(state=TaskState.completed),
final=True,
)
await session.save_session_state(
session_id="test-a2a-agent",
agent=agent,
)
handler = SimpleStreamHandler()
app_instance = A2AStarletteApplication(
agent_card,
handler,
)
app = app_instance.build()

View File

@@ -0,0 +1,110 @@
# A2UI in AgentScope
[A2UI (Agent-to-Agent UI)](https://github.com/google/A2UI) is a protocol for agents to send
streaming, interactive user interfaces to clients. It enables LLMs to generate platform-agnostic,
declarative UI definitions that clients can render progressively using native widget sets.
In this example, we demonstrate how to integrate A2UI into a ReAct agent in AgentScope. This
implementation is based on the official A2UI agent samples, adapted to use AgentScope's agent
framework.
Specifically, we have:
1. **Reimplemented the agent with AgentScope**: The agent part of the official A2UI samples has
been reimplemented using AgentScope's `ReActAgent`, providing a more familiar and integrated
development experience for AgentScope users.
2. **Progressive schema and template exposure via skills**: To help the agent learn and generate
A2UI-compliant interfaces, we use AgentScope's skill system to progressively expose the A2UI
schema and UI templates. The agent can dynamically load these resources through the
`A2UI_response_generator` skill, enabling it to understand component definitions and learn from
example UI structures.
## Note on External Dependencies
The following directories in this example contain content sourced from the [Google A2UI repository](https://github.com/google/A2UI):
- **`samples/client/`**: A2UI client sample applications
**NPM Package Status**: As of now, the A2UI client libraries (`@a2ui/lit` and `@a2ui/angular`) are **not yet published to NPM**. According to the [official A2UI client setup guide](https://a2ui.org/guides/client-setup/#renderers): "The Lit client library is not yet published to NPM. Check back in the coming days."
Therefore, these dependencies are currently included in this example repository using local file paths (e.g., `"@a2ui/lit": "file:../../../../renderers/lit"` in `package.json` files). This mirrors the approach used in the [official A2UI repository](https://github.com/google/A2UI), where the renderers and samples also use local file paths to reference each other. Additionally, the `copy-spec` task in `renderers/lit/package.json` copies files from the local `specification/` directory during the build process.
**Future Plans**: Once those libraries are published to NPM, we plan to gradually migrate to using the official NPM packages and remove these locally included directories.
## Quick Start
Download the a2ui and agentscope package to the same directory
```bash
git clone https://github.com/google/A2UI.git
git clone -b main https://github.com/agentscope-ai/agentscope.git
# copy the renders and specification directory to AgentScope/examples/agent/a2ui_agent
cp -r A2UI/renderers AgentScope/examples/agent/a2ui_agent
cp -r A2UI/specification AgentScope/examples/agent/a2ui_agent
```
Then, navigate to the client directory and run the restaurant finder demo:
```bash
cd AgentScope/examples/agent/a2ui_agent/samples/client/lit
npm run demo:restaurant
```
This command will:
- Install dependencies and build the A2UI renderer
- Start the A2A server (AgentScope agent) for the restaurant finder
- Launch the client application in your browser
> Note:
> - The example is built with DashScope chat model. Make sure to set your `DASHSCOPE_API_KEY`
> environment variable before running the demo.
> - If you are using Qwen series models, we recommend using `qwen3-max` for better performance in
> generating A2UI-compliant JSON responses.
> - Generating UI JSON responses may take some time, typically 1-2 minutes, as the agent needs to
> process the schema, examples, and generate complex UI structures.
> - The demo uses the standard A2UI catalog. Custom catalog and inline catalog support are under
> development.
## Roadmap
AgentScope's main focus going forward will be on improving **How Agents Work** with A2UI. The
workflow we're working towards is:
```
User Input → Agent Logic → LLM → A2UI JSON
```
Our optimization efforts will focus on:
- **Agent Logic**: Improving how agents process inputs and orchestrate the generation of A2UI JSON
messages
- **Handle user interactions from the client**: Enabling agents to properly process and respond to
user interactions from the client (such as button clicks, form submissions), treating them as new
user input to create a continuous interactive loop
**Current approach**: The skill-based method we've implemented in this example is our first step
towards this goal. By using AgentScope's skill system to progressively expose the A2UI schema and
templates, agents can learn to generate compliant UI structures. Future improvements will focus on
streamlining this process and making it more intuitive for developers to build A2UI-capable agents.
**Next steps for Agent Logic improvement**
- **Agent skills improvements**:
- Support flexible schema addition: Enable developers to easily add and customize schemas without
modifying core skill code
- Separate schema and examples into dedicated folders: Organize schema definitions and example
templates into distinct directories for better maintainability and clearer structure
- **Context management in Memory for A2UI long context**:
- Currently, A2UI messages are extremely long, which makes multi-turn interactions inefficient
and degrades the quality of agent responses. We plan to implement better context management
strategies to handle these long-form messages and improve the quality of multi-turn conversations.
- **Keep up with A2UI protocol updates**:
- We will follow A2UI protocol updates and make corresponding adjustments. For example, we plan to
support streaming UI JSON introduced in A2UI v0.9.

View File

@@ -0,0 +1,156 @@
# -*- coding: utf-8 -*-
"""A2A client example demonstrating agent card fetching and message sending."""
import logging
from typing import Any
from uuid import uuid4
import httpx
from a2a.client import A2ACardResolver, A2AClient
from a2a.types import (
AgentCard,
MessageSendParams,
SendMessageRequest,
)
from a2a.utils.constants import (
AGENT_CARD_WELL_KNOWN_PATH,
EXTENDED_AGENT_CARD_PATH,
)
async def main() -> None:
"""Main function demonstrating A2A client usage."""
# Configure logging to show INFO level messages
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) # Get a logger instance
# --8<-- [start:A2ACardResolver]
base_url = "http://localhost:10002"
async with httpx.AsyncClient(
timeout=httpx.Timeout(timeout=300.0),
) as httpx_client:
# Initialize A2ACardResolver
resolver = A2ACardResolver(
httpx_client=httpx_client,
base_url=base_url,
# agent_card_path uses default,
# extended_agent_card_path also uses default
)
# --8<-- [end:A2ACardResolver]
# Fetch Public Agent Card and Initialize Client
final_agent_card_to_use: AgentCard | None = None
try:
logger.info(
"Attempting to fetch public agent card from: %s%s",
base_url,
AGENT_CARD_WELL_KNOWN_PATH,
)
_public_card = (
await resolver.get_agent_card()
) # Fetches from default public path
logger.info("Successfully fetched public agent card:")
logger.info(
_public_card.model_dump_json(indent=2, exclude_none=True),
)
final_agent_card_to_use = _public_card
logger.info(
"\nUsing PUBLIC agent card for client initialization "
"(default).",
)
if _public_card.supports_authenticated_extended_card:
try:
logger.info(
"\nPublic card supports authenticated extended card. "
"Attempting to fetch from: %s%s",
base_url,
EXTENDED_AGENT_CARD_PATH,
)
auth_headers_dict = {
"Authorization": (
"Bearer dummy-token-for-extended-card"
),
}
_extended_card = await resolver.get_agent_card(
relative_card_path=EXTENDED_AGENT_CARD_PATH,
http_kwargs={"headers": auth_headers_dict},
)
logger.info(
"Successfully fetched authenticated extended "
"agent card:",
)
logger.info(
_extended_card.model_dump_json(
indent=2,
exclude_none=True,
),
)
final_agent_card_to_use = (
_extended_card # Update to use the extended card
)
logger.info(
"\nUsing AUTHENTICATED EXTENDED agent card "
"for client initialization.",
)
except Exception as e_extended:
logger.warning(
"Failed to fetch extended agent card: %s. "
"Will proceed with public card.",
e_extended,
exc_info=True,
)
elif (
_public_card
): # supports_authenticated_extended_card is False or None
logger.info(
"\nPublic card does not indicate support for an "
"extended card. Using public card.",
)
except Exception as e:
logger.error(
"Critical error fetching public agent card: %s",
e,
exc_info=True,
)
raise RuntimeError(
"Failed to fetch the public agent card. Cannot continue.",
) from e
# --8<-- [start:send_message]
client = A2AClient(
httpx_client=httpx_client,
agent_card=final_agent_card_to_use,
)
logger.info("A2AClient initialized.")
send_message_payload: dict[str, Any] = {
"message": {
"role": "user",
"parts": [
{
"kind": "text",
"text": "find top 5 chinese restaurants in new york",
},
],
"messageId": uuid4().hex,
},
}
request = SendMessageRequest(
id=str(uuid4()),
params=MessageSendParams(**send_message_payload),
)
response = await client.send_message(request)
print(response.model_dump(mode="json", exclude_none=True))
if __name__ == "__main__":
import asyncio
asyncio.run(main())

View File

@@ -0,0 +1,30 @@
# A2UI Generator
This is a UI to generate and visualize A2UI responses.
## Prerequisites
1. [nodejs](https://nodejs.org/en)
## Running
This sample depends on the Lit renderer. Before running this sample, you need to build the renderer.
1. **Build the renderer:**
```bash
cd ../../../renderers/lit
npm install
npm run build
```
2. **Run this sample:**
```bash
cd - # back to the sample directory
npm install
```
3. **Run the servers:**
- Run the [A2A server](../../../agent/adk/contact_lookup/)
- Run the dev server: `npm run dev`
After starting the dev server, you can open http://localhost:5173/ to view the sample.

View File

@@ -0,0 +1,65 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { v0_8 } from "@a2ui/lit";
import { registerContactComponents } from "./ui/custom-components/register-components.js";
type A2TextPayload = {
kind: "text";
text: string;
};
type A2DataPayload = {
kind: "data";
data: v0_8.Types.ServerToClientMessage;
};
type A2AServerPayload =
| Array<A2DataPayload | A2TextPayload>
| { error: string };
export class A2UIClient {
#ready: Promise<void> = Promise.resolve();
get ready() {
return this.#ready;
}
async send(
message: v0_8.Types.A2UIClientEventMessage
): Promise<v0_8.Types.ServerToClientMessage[]> {
const response = await fetch("/a2a", {
body: JSON.stringify(message),
method: "POST",
});
if (response.ok) {
const data = (await response.json()) as A2AServerPayload;
const messages: v0_8.Types.ServerToClientMessage[] = [];
if ("error" in data) {
throw new Error(data.error);
} else {
for (const item of data) {
if (item.kind === "text") continue;
messages.push(item.data);
}
}
return messages;
}
const error = (await response.json()) as { error: string };
throw new Error(error.error);
}
}
registerContactComponents();

View File

@@ -0,0 +1,376 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { SignalWatcher } from "@lit-labs/signals";
import { provide } from "@lit/context";
import {
LitElement,
html,
css,
nothing,
HTMLTemplateResult,
unsafeCSS,
} from "lit";
import { customElement, state } from "lit/decorators.js";
import { theme as uiTheme } from "./theme/theme.js";
import { A2UIClient } from "./client.js";
import {
SnackbarAction,
SnackbarMessage,
SnackbarUUID,
SnackType,
} from "./types/types.js";
import { type Snackbar } from "./ui/snackbar.js";
import { repeat } from "lit/directives/repeat.js";
import { v0_8 } from "@a2ui/lit";
import * as UI from "@a2ui/lit/ui";
// Demo elements.
import "./ui/ui.js";
import { registerContactComponents } from "./ui/custom-components/register-components.js";
// Register custom components for the contact app
registerContactComponents();
@customElement("a2ui-contact")
export class A2UIContactFinder extends SignalWatcher(LitElement) {
@provide({ context: UI.Context.themeContext })
accessor theme: v0_8.Types.Theme = uiTheme;
@state()
accessor #requesting = false;
@state()
accessor #error: string | null = null;
@state()
accessor #lastMessages: v0_8.Types.ServerToClientMessage[] = [];
static styles = [
unsafeCSS(v0_8.Styles.structuralStyles),
css`
:host {
display: block;
max-width: 640px;
margin: 0 auto;
min-height: 100%;
}
#surfaces {
display: flex;
flex-direction: column;
width: 100%;
padding: var(--bb-grid-size-3) 0;
animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards;
& a2ui-surface {
align-items: center;
}
}
form {
display: flex;
flex-direction: column;
flex: 1;
gap: 16px;
align-items: center;
padding: 16px 0;
animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 1s backwards;
& > div {
display: flex;
flex: 1;
gap: 16px;
align-items: center;
width: 100%;
& > input {
display: block;
flex: 1;
border-radius: 32px;
padding: 16px 24px;
border: 1px solid var(--p-60);
font-size: 16px;
}
& > button {
display: flex;
align-items: center;
background: var(--p-40);
color: var(--n-100);
border: none;
padding: 8px 16px;
border-radius: 32px;
opacity: 0.5;
&:not([disabled]) {
cursor: pointer;
opacity: 1;
}
}
}
}
.rotate {
animation: rotate 1s linear infinite;
}
.pending {
width: 100%;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards;
& .g-icon {
margin-right: 8px;
}
}
.error {
color: var(--e-40);
background-color: var(--e-95);
border: 1px solid var(--e-80);
padding: 16px;
border-radius: 8px;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes rotate {
from {
rotate: 0deg;
}
to {
rotate: 360deg;
}
}
`,
];
#processor = v0_8.Data.createSignalA2uiMessageProcessor();
#a2uiClient = new A2UIClient();
#snackbar: Snackbar | undefined = undefined;
#pendingSnackbarMessages: Array<{
message: SnackbarMessage;
replaceAll: boolean;
}> = [];
render() {
return [
this.#maybeRenderForm(),
this.#maybeRenderData(),
this.#maybeRenderError(),
];
}
#maybeRenderError() {
if (!this.#error) return nothing;
return html`<div class="error">${this.#error}</div>`;
}
#maybeRenderForm() {
if (this.#requesting) return nothing;
if (this.#lastMessages.length > 0) return nothing;
return html`<form
@submit=${async (evt: Event) => {
evt.preventDefault();
if (!(evt.target instanceof HTMLFormElement)) {
return;
}
const data = new FormData(evt.target);
const body = data.get("body") ?? null;
if (!body) {
return;
}
const message = body as v0_8.Types.A2UIClientEventMessage;
await this.#sendAndProcessMessage(message);
}}
>
<h1 class="typography-f-sf typography-v-r typography-w-400 color-c-p30">
Contact Finder
</h1>
<div>
<input
required
value="Casey Smith"
autocomplete="off"
id="body"
name="body"
type="text"
?disabled=${this.#requesting}
/>
<button type="submit" ?disabled=${this.#requesting}>
<span class="g-icon filled-heavy">send</span>
</button>
</div>
</form>`;
}
#maybeRenderData() {
if (this.#requesting) {
return html` <div class="pending">
<span class="g-icon filled-heavy rotate">progress_activity</span>
Awaiting an answer...
</div>`;
}
const surfaces = this.#processor.getSurfaces();
if (surfaces.size === 0) {
return nothing;
}
return html`<section id="surfaces">
${repeat(
this.#processor.getSurfaces(),
([surfaceId]) => surfaceId,
([surfaceId, surface]) => {
return html`<a2ui-surface
@a2uiaction=${async (
evt: v0_8.Events.StateEvent<"a2ui.action">
) => {
const [target] = evt.composedPath();
if (!(target instanceof HTMLElement)) {
return;
}
const context: v0_8.Types.A2UIClientEventMessage["userAction"]["context"] =
{};
if (evt.detail.action.context) {
const srcContext = evt.detail.action.context;
for (const item of srcContext) {
if (item.value.literalBoolean) {
context[item.key] = item.value.literalBoolean;
} else if (item.value.literalNumber) {
context[item.key] = item.value.literalNumber;
} else if (item.value.literalString) {
context[item.key] = item.value.literalString;
} else if (item.value.path) {
const path = this.#processor.resolvePath(
item.value.path,
evt.detail.dataContextPath
);
const value = this.#processor.getData(
evt.detail.sourceComponent,
path,
surfaceId
);
context[item.key] = value;
}
}
}
const message: v0_8.Types.A2UIClientEventMessage = {
userAction: {
surfaceId: surfaceId,
name: evt.detail.action.name,
sourceComponentId: target.id,
timestamp: new Date().toISOString(),
context,
},
};
await this.#sendAndProcessMessage(message);
}}
.surfaceId=${surfaceId}
.surface=${surface}
.processor=${this.#processor}
></a2-uisurface>`;
}
)}
</section>`;
}
async #sendAndProcessMessage(request) {
const messages = await this.#sendMessage(request);
this.#lastMessages = messages;
this.#processor.clearSurfaces();
this.#processor.processMessages(messages);
}
async #sendMessage(
message: v0_8.Types.A2UIClientEventMessage
): Promise<v0_8.Types.ServerToClientMessage[]> {
try {
this.#requesting = true;
const response = this.#a2uiClient.send(message);
await response;
this.#requesting = false;
return response;
} catch (err) {
this.snackbar(err as string, SnackType.ERROR);
} finally {
this.#requesting = false;
}
return [];
}
snackbar(
message: string | HTMLTemplateResult,
type: SnackType,
actions: SnackbarAction[] = [],
persistent = false,
id = globalThis.crypto.randomUUID(),
replaceAll = false
) {
if (!this.#snackbar) {
this.#pendingSnackbarMessages.push({
message: {
id,
message,
type,
persistent,
actions,
},
replaceAll,
});
return;
}
return this.#snackbar.show(
{
id,
message,
type,
persistent,
actions,
},
replaceAll
);
}
unsnackbar(id?: SnackbarUUID) {
if (!this.#snackbar) {
return;
}
this.#snackbar.hide(id);
}
}

View File

@@ -0,0 +1,35 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { HTMLTemplateResult } from "lit";
const eventInit = {
bubbles: true,
cancelable: true,
composed: true,
};
export class SnackbarActionEvent extends Event {
static eventName = "snackbaraction";
constructor(
public readonly action: string,
public readonly value?: HTMLTemplateResult | string,
public readonly callback?: () => void
) {
super(SnackbarActionEvent.eventName, { ...eventInit });
}
}

View File

@@ -0,0 +1,196 @@
<!DOCTYPE html>
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Contact Finder Agent</title>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=account_circle,add,arrow_back,arrow_drop_down,arrow_forward,attach_file,calendar_today,call,camera,check,check_circle,close,communication,content_copy,delete,download,draw,edit,error,event,favorite,favorite_off,folder,help,home,info,location_on,lock,lock_open,mail,menu,mobile_layout,more_horiz,more_vert,notifications,notifications_off,payment,pen_size_1,person,phone,photo,print,progress_activity,rectangle,refresh,search,send,settings,share,shopping_cart,star,star_half,star_off,upload,visibility,visibility_off,warning"
/>
<style>
:root {
--n-100: #ffffff;
--n-99: #fcfcfc;
--n-98: #f9f9f9;
--n-95: #f1f1f1;
--n-90: #e2e2e2;
--n-80: #c6c6c6;
--n-70: #ababab;
--n-60: #919191;
--n-50: #777777;
--n-40: #5e5e5e;
--n-35: #525252;
--n-30: #474747;
--n-25: #3b3b3b;
--n-20: #303030;
--n-15: #262626;
--n-10: #1b1b1b;
--n-5: #111111;
--n-0: #000000;
--p-100: #ffffff;
--p-99: #fffbff;
--p-98: #fcf8ff;
--p-95: #f2efff;
--p-90: #e1e0ff;
--p-80: #c0c1ff;
--p-70: #a0a3ff;
--p-60: #8487ea;
--p-50: #6a6dcd;
--p-40: #5154b3;
--p-35: #4447a6;
--p-30: #383b99;
--p-25: #2c2e8d;
--p-20: #202182;
--p-15: #131178;
--p-10: #06006c;
--p-5: #03004d;
--p-0: #000000;
--s-100: #ffffff;
--s-99: #fffbff;
--s-98: #fcf8ff;
--s-95: #f2efff;
--s-90: #e2e0f9;
--s-80: #c6c4dd;
--s-70: #aaa9c1;
--s-60: #8f8fa5;
--s-50: #75758b;
--s-40: #5d5c72;
--s-35: #515165;
--s-30: #454559;
--s-25: #393a4d;
--s-20: #2e2f42;
--s-15: #242437;
--s-10: #191a2c;
--s-5: #0f0f21;
--s-0: #000000;
--t-100: #ffffff;
--t-99: #fffbff;
--t-98: #fff8f9;
--t-95: #ffecf4;
--t-90: #ffd8ec;
--t-80: #e9b9d3;
--t-70: #cc9eb8;
--t-60: #af849d;
--t-50: #946b83;
--t-40: #79536a;
--t-35: #6c475d;
--t-30: #5f3c51;
--t-25: #523146;
--t-20: #46263a;
--t-15: #3a1b2f;
--t-10: #2e1125;
--t-5: #22071a;
--t-0: #000000;
--nv-100: #ffffff;
--nv-99: #fffbff;
--nv-98: #fcf8ff;
--nv-95: #f2effa;
--nv-90: #e4e1ec;
--nv-80: #c8c5d0;
--nv-70: #acaab4;
--nv-60: #918f9a;
--nv-50: #777680;
--nv-40: #5e5d67;
--nv-35: #52515b;
--nv-30: #46464f;
--nv-25: #3b3b43;
--nv-20: #303038;
--nv-15: #25252d;
--nv-10: #1b1b23;
--nv-5: #101018;
--nv-0: #000000;
--e-100: #ffffff;
--e-99: #fffbff;
--e-98: #fff8f7;
--e-95: #ffedea;
--e-90: #ffdad6;
--e-80: #ffb4ab;
--e-70: #ff897d;
--e-60: #ff5449;
--e-50: #de3730;
--e-40: #ba1a1a;
--e-35: #a80710;
--e-30: #93000a;
--e-25: #7e0007;
--e-20: #690005;
--e-15: #540003;
--e-10: #410002;
--e-5: #2d0001;
--e-0: #000000;
--primary: #137fec;
--text-color: #fff;
--background-light: #f6f7f8;
--background-dark: #101922;
--border-color: oklch(
from var(--background-light) l c h / calc(alpha * 0.15)
);
--elevated-background-light: oklch(
from var(--background-light) l c h / calc(alpha * 0.05)
);
--bb-grid-size: 4px;
--bb-grid-size-2: calc(var(--bb-grid-size) * 2);
--bb-grid-size-3: calc(var(--bb-grid-size) * 3);
--bb-grid-size-4: calc(var(--bb-grid-size) * 4);
--bb-grid-size-5: calc(var(--bb-grid-size) * 5);
--bb-grid-size-6: calc(var(--bb-grid-size) * 6);
--bb-grid-size-7: calc(var(--bb-grid-size) * 7);
--bb-grid-size-8: calc(var(--bb-grid-size) * 8);
--bb-grid-size-9: calc(var(--bb-grid-size) * 9);
--bb-grid-size-10: calc(var(--bb-grid-size) * 10);
--bb-grid-size-11: calc(var(--bb-grid-size) * 11);
--bb-grid-size-12: calc(var(--bb-grid-size) * 12);
--bb-grid-size-13: calc(var(--bb-grid-size) * 13);
--bb-grid-size-14: calc(var(--bb-grid-size) * 14);
--bb-grid-size-15: calc(var(--bb-grid-size) * 15);
--bb-grid-size-16: calc(var(--bb-grid-size) * 16);
}
* {
box-sizing: border-box;
}
html,
body {
--font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-family-flex: "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-family-mono: monospace;
background: var(--background-light);
font-family: var(--font-family);
margin: 0;
padding: 0;
width: 100svw;
height: 100svh;
}
</style>
</head>
<body>
<a2ui-contact></a2ui-contact>
<script src="./contact.ts" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,153 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IncomingMessage, ServerResponse } from "http";
import { Plugin, ViteDevServer } from "vite";
import { A2AClient } from "@a2a-js/sdk/client";
import {
MessageSendParams,
Part,
SendMessageSuccessResponse,
Task,
} from "@a2a-js/sdk";
import { v4 as uuidv4 } from "uuid";
const A2AUI_MIME_TYPE = "application/json+a2aui";
const fetchWithCustomHeader: typeof fetch = async (url, init) => {
const headers = new Headers(init?.headers);
headers.set("X-A2A-Extensions", "https://a2ui.org/a2a-extension/a2ui/v0.8");
const newInit = { ...init, headers };
return fetch(url, newInit);
};
const isJson = (str: string) => {
try {
const parsed = JSON.parse(str);
return (
typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
);
} catch (err) {
console.warn(err);
return false;
}
};
let client: A2AClient | null = null;
const createOrGetClient = async () => {
if (!client) {
// Create a client pointing to the agent's Agent Card URL.
client = await A2AClient.fromCardUrl(
"http://localhost:10002/.well-known/agent-card.json",
{ fetchImpl: fetchWithCustomHeader }
);
}
return client;
};
export const plugin = (): Plugin => {
return {
name: "a2a-handler",
configureServer(server: ViteDevServer) {
server.middlewares.use(
"/a2a",
async (req: IncomingMessage, res: ServerResponse, next: () => void) => {
if (req.method === "POST") {
let originalBody = "";
req.on("data", (chunk) => {
originalBody += chunk.toString();
});
req.on("end", async () => {
let sendParams: MessageSendParams;
if (isJson(originalBody)) {
console.log(
"[a2a-middleware] Received JSON UI event:",
originalBody
);
const clientEvent = JSON.parse(originalBody);
sendParams = {
message: {
messageId: uuidv4(),
role: "user",
parts: [
{
kind: "data",
data: clientEvent,
metadata: { 'mimeType': A2AUI_MIME_TYPE },
} as Part,
],
kind: "message",
},
};
} else {
console.log(
"[a2a-middleware] Received text query:",
originalBody
);
sendParams = {
message: {
messageId: uuidv4(),
role: "user",
parts: [
{
kind: "text",
text: originalBody,
},
],
kind: "message",
},
};
}
const client = await createOrGetClient();
const response = await client.sendMessage(sendParams);
if ("error" in response) {
console.error("Error:", response.error.message);
res.statusCode = 500;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ error: response.error.message }));
return;
} else {
const result = (response as SendMessageSuccessResponse)
.result as Task;
if (result.kind === "task") {
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(result.status.message?.parts));
return;
}
}
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify([]));
});
return;
} else {
next();
}
}
);
},
};
};

View File

@@ -0,0 +1,17 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export * as A2AMiddleware from "./a2a.js";

View File

@@ -0,0 +1,84 @@
{
"name": "@a2ui/contact",
"private": true,
"version": "0.8.1",
"description": "A2UI Contact Demo",
"main": "./dist/contact.js",
"types": "./dist/contact.d.ts",
"type": "module",
"scripts": {
"prepack": "npm run build",
"build": "wireit",
"build:tsc": "wireit",
"dev": "npm run serve --watch",
"test": "wireit",
"serve": "wireit"
},
"wireit": {
"serve": {
"command": "vite dev",
"dependencies": [
"build"
],
"service": true
},
"test": {
"command": "node --test --enable-source-maps --test-reporter spec dist/src/0.8/tests/**/*.test.js",
"dependencies": [
"build"
]
},
"build": {
"dependencies": [
"build:tsc"
]
},
"build:tsc": {
"command": "tsc -b --pretty",
"env": {
"FORCE_COLOR": "1"
},
"dependencies": [
"../../../../renderers/lit:build:tsc"
],
"files": [
"**/*.ts",
"tsconfig.json"
],
"output": [
"dist/",
"!dist/**/*.min.js{,.map}"
],
"clean": "if-file-deleted"
}
},
"repository": {
"directory": "samples/client/lit/contact",
"type": "git",
"url": "git+https://github.com/google/A2UI.git"
},
"files": [
"dist"
],
"keywords": [],
"author": "Google",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/google/A2UI/issues"
},
"homepage": "https://github.com/google/A2UI/tree/main/web#readme",
"devDependencies": {
"dotenv": "^17.2.3",
"typescript": "^5.8.3",
"uuid": "^13.0.0",
"vite": "^7.1.11",
"wireit": "^0.15.0-pre.2"
},
"dependencies": {
"@a2a-js/sdk": "^0.3.4",
"@a2ui/lit": "file:../../../../renderers/lit",
"@lit-labs/signals": "^0.1.3",
"@lit/context": "^1.1.4",
"lit": "^3.3.1"
}
}

View File

@@ -0,0 +1,463 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { v0_8 } from "@a2ui/lit";
/** Elements */
const a = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-as-n": true,
"layout-dis-iflx": true,
"layout-al-c": true,
};
const audio = {
"layout-w-100": true,
};
const body = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-mt-0": true,
"layout-mb-2": true,
"typography-sz-bm": true,
"color-c-n10": true,
};
const button = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-pt-3": true,
"layout-pb-3": true,
"layout-pl-5": true,
"layout-pr-5": true,
"layout-mb-1": true,
"border-br-16": true,
"border-bw-0": true,
"border-c-n70": true,
"border-bs-s": true,
"color-bgc-s30": true,
"color-c-n100": true,
"behavior-ho-80": true,
};
const heading = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mb-2": true,
"color-c-n10": true,
};
const h1 = {
...heading,
"typography-sz-tl": true,
};
const h2 = {
...heading,
"typography-sz-tm": true,
};
const h3 = {
...heading,
"typography-sz-ts": true,
};
const iframe = {
"behavior-sw-n": true,
};
const input = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-pl-4": true,
"layout-pr-4": true,
"layout-pt-2": true,
"layout-pb-2": true,
"border-br-6": true,
"border-bw-1": true,
"color-bc-s70": true,
"border-bs-s": true,
"layout-as-n": true,
"color-c-n10": true,
};
const p = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
"color-c-n10": true,
};
const orderedList = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
};
const unorderedList = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
};
const listItem = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
};
const pre = {
"typography-f-c": true,
"typography-fs-n": true,
"typography-w-400": true,
"typography-sz-bm": true,
"typography-ws-p": true,
"layout-as-n": true,
};
const textarea = {
...input,
"layout-r-none": true,
"layout-fs-c": true,
};
const video = {
"layout-el-cv": true,
};
const aLight = v0_8.Styles.merge(a, { "color-c-p30": true });
const inputLight = v0_8.Styles.merge(input, { "color-c-n5": true });
const textareaLight = v0_8.Styles.merge(textarea, { "color-c-n5": true });
const buttonLight = v0_8.Styles.merge(button, { "color-c-n100": true });
const h1Light = v0_8.Styles.merge(h1, { "color-c-n5": true });
const h2Light = v0_8.Styles.merge(h2, { "color-c-n5": true });
const h3Light = v0_8.Styles.merge(h3, { "color-c-n5": true });
const bodyLight = v0_8.Styles.merge(body, { "color-c-n5": true });
const pLight = v0_8.Styles.merge(p, { "color-c-n60": true });
const preLight = v0_8.Styles.merge(pre, { "color-c-n35": true });
const orderedListLight = v0_8.Styles.merge(orderedList, {
"color-c-n35": true,
});
const unorderedListLight = v0_8.Styles.merge(unorderedList, {
"color-c-n35": true,
});
const listItemLight = v0_8.Styles.merge(listItem, {
"color-c-n35": true,
});
export const theme: v0_8.Types.Theme = {
additionalStyles: {
Card: {
"min-width": "320px",
},
Button: {
"--n-60": "var(--n-100)",
},
Image: {
"max-width": "120px",
"max-height": "120px",
marginLeft: "auto",
marginRight: "auto",
},
},
components: {
AudioPlayer: {},
Button: {
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-5": true,
"layout-pr-5": true,
"border-br-2": true,
"border-bw-0": true,
"border-bs-s": true,
"color-bgc-p30": true,
"color-c-n100": true,
"behavior-ho-70": true,
},
Card: {
"border-br-4": true,
"color-bgc-p100": true,
"color-bc-n90": true,
"border-bw-1": true,
"border-bs-s": true,
"layout-pt-10": true,
"layout-pb-10": true,
"layout-pl-4": true,
"layout-pr-4": true,
},
CheckBox: {
element: {
"layout-m-0": true,
"layout-mr-2": true,
"layout-p-2": true,
"border-br-12": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bgc-p100": true,
"color-bc-p60": true,
"color-c-n30": true,
"color-c-p30": true,
},
label: {
"color-c-p30": true,
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-flx-1": true,
"typography-sz-ll": true,
},
container: {
"layout-dsp-iflex": true,
"layout-al-c": true,
},
},
Column: {},
DateTimeInput: {
container: {},
label: {},
element: {
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-3": true,
"layout-pr-3": true,
"border-br-12": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bgc-p100": true,
"color-bc-p60": true,
"color-c-n30": true,
},
},
Divider: {
"color-bgc-n90": true,
"layout-mt-6": true,
"layout-mb-6": true,
},
Image: {
all: {
"border-br-50pc": true,
"layout-el-cv": true,
"layout-w-100": true,
"layout-h-100": true,
"layout-dsp-flexhor": true,
"layout-al-c": true,
"layout-sp-c": true,
"layout-mb-3": true,
},
avatar: {},
header: {},
icon: {},
largeFeature: {},
mediumFeature: {},
smallFeature: {},
},
Icon: {
"border-br-1": true,
"layout-p-2": true,
"color-bgc-n98": true,
"layout-dsp-flexhor": true,
"layout-al-c": true,
"layout-sp-c": true,
},
List: {
"layout-g-4": true,
"layout-p-2": true,
},
Modal: {
backdrop: { "color-bbgc-p60_20": true },
element: {
"border-br-2": true,
"color-bgc-p100": true,
"layout-p-4": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bc-p80": true,
},
},
MultipleChoice: {
container: {},
label: {},
element: {},
},
Row: {
"layout-g-4": true,
"layout-mb-3": true,
},
Slider: {
container: {},
label: {},
element: {},
},
Tabs: {
container: {},
controls: { all: {}, selected: {} },
element: {},
},
Text: {
all: {
"layout-w-100": true,
"layout-g-2": true,
"color-c-p30": true,
},
h1: {
"typography-f-sf": true,
"typography-ta-c": true,
"typography-v-r": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mr-0": true,
"layout-ml-0": true,
"layout-mb-2": true,
"layout-p-0": true,
"typography-sz-tl": true,
},
h2: {
"typography-f-sf": true,
"typography-ta-c": true,
"typography-v-r": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mr-0": true,
"layout-ml-0": true,
"layout-mb-2": true,
"layout-p-0": true,
"typography-sz-tl": true,
},
h3: {
"typography-f-sf": true,
"typography-ta-c": true,
"typography-v-r": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mr-0": true,
"layout-ml-0": true,
"layout-mb-0": true,
"layout-p-0": true,
"typography-sz-ts": true,
},
h4: {
"typography-f-sf": true,
"typography-ta-c": true,
"typography-v-r": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mr-0": true,
"layout-ml-0": true,
"layout-mb-0": true,
"layout-p-0": true,
"typography-sz-bl": true,
},
h5: {
"typography-f-sf": true,
"typography-ta-c": true,
"typography-v-r": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mr-0": true,
"layout-ml-0": true,
"layout-mb-0": true,
"layout-p-0": true,
"color-c-n30": true,
"typography-sz-bm": true,
"layout-mb-1": true,
},
body: {},
caption: {},
},
TextField: {
container: {
"typography-sz-bm": true,
"layout-w-100": true,
"layout-g-2": true,
"layout-dsp-flexhor": true,
"layout-al-c": true,
},
label: {
"layout-flx-0": true,
},
element: {
"typography-sz-bm": true,
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-3": true,
"layout-pr-3": true,
"border-br-12": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bgc-p100": true,
"color-bc-p60": true,
"color-c-n30": true,
"color-c-p30": true,
},
},
Video: {
"border-br-5": true,
"layout-el-cv": true,
},
},
elements: {
a: aLight,
audio,
body: bodyLight,
button: buttonLight,
h1: h1Light,
h2: h2Light,
h3: h3Light,
h4: {},
h5: {},
iframe,
input: inputLight,
p: pLight,
pre: preLight,
textarea: textareaLight,
video,
},
markdown: {
p: [...Object.keys(pLight)],
h1: [...Object.keys(h1Light)],
h2: [...Object.keys(h2Light)],
h3: [...Object.keys(h3Light)],
h4: [],
h5: [],
ul: [...Object.keys(unorderedListLight)],
ol: [...Object.keys(orderedListLight)],
li: [...Object.keys(listItemLight)],
a: [...Object.keys(aLight)],
strong: [],
em: [],
},
};

View File

@@ -0,0 +1,30 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"incremental": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"preserveWatchOutput": true,
"sourceMap": true,
"target": "es2022",
"module": "es2022",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"useDefineForClassFields": false,
"rootDir": ".",
"outDir": "dist",
"tsBuildInfoFile": "dist/.tsbuildinfo",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"references": [{ "path": "../../../../renderers/lit" }]
}

View File

@@ -0,0 +1,42 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { HTMLTemplateResult } from "lit";
export enum SnackType {
NONE = "none",
INFORMATION = "information",
WARNING = "warning",
ERROR = "error",
PENDING = "pending",
}
export type SnackbarUUID = ReturnType<typeof globalThis.crypto.randomUUID>;
export type SnackbarAction = {
title: string;
action: string;
value?: HTMLTemplateResult | string;
callback?: () => void;
};
export type SnackbarMessage = {
id: SnackbarUUID;
type: SnackType;
persistent: boolean;
message: string | HTMLTemplateResult;
actions?: SnackbarAction[];
};

View File

@@ -0,0 +1,167 @@
# A2UI custom component integration guide
This guide details how to create, register, and use a custom component in the A2UI client.
## Create the component
Create a new Lit component file in `lib/src/0.8/ui/custom-components/`.
Example: `my-component.ts`
```typescript
import { html, css } from "lit";
import { property } from "lit/decorators.js";
import { Root } from "../root.js";
export class MyComponent extends Root {
@property() accessor myProp: string = "Default";
static styles = [
...Root.styles, // Inherit base styles
css`
:host {
display: block;
padding: 16px;
border: 1px solid #ccc;
}
`,
];
render() {
return html`
<div>
<h2>My Custom Component</h2>
<p>Prop value: ${this.myProp}</p>
</div>
`;
}
}
```
## Register the component
Update `lib/src/0.8/ui/custom-components/index.ts` to register your new component.
You must pass the desired tag name as the third argument.
```typescript
import { componentRegistry } from "../component-registry.js";
import { MyComponent } from "./my-component.js"; // Import your component
export function registerCustomComponents() {
// Register with explicit tag name
componentRegistry.register("MyComponent", MyComponent, "my-component");
}
export { MyComponent }; // Export for type usage if needed
```
## Define the schema (server-side)
Create a JSON schema for your component properties. This will be used by the server to validate messages.
Example: `lib/my_component_schema.json`
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"properties": {
"type": { "const": "object" },
"properties": {
"type": "object",
"additionalProperties": false,
"properties": {
"myProp": {
"type": "string",
"description": "A sample property."
}
},
"required": ["myProp"]
}
},
"required": ["type", "properties"]
}
```
## Use in client application
In your client application (e.g., `contact` sample), ensure you import and call the registration function.
```typescript
import { registerCustomComponents } from "@a2ui/lit/ui";
// Call this once at startup
registerCustomComponents();
```
## Overriding standard components
You can replace standard A2UI components (like `TextField`, `Video`, `Button`) with your own custom implementations.
### Steps to override
1. **Create your component** extending `Root` (just like a custom component).
2. **Ensure it accepts the standard properties** for that component type (e.g., `label` and `text` for `TextField`).
3. **Register it** using the **standard type name** (e.g., `"TextField"`).
```typescript
// 1. Define your override
class MyPremiumTextField extends Root {
@property() accessor label = "";
@property() accessor text = "";
static styles = [
...Root.styles,
css`
/* your premium styles */
`,
];
render() {
return html`
<div class="premium-field">
<label>${this.label}</label>
<input .value="${this.text}" />
</div>
`;
}
}
// 2. Register with the STANDARD type name
import { componentRegistry } from "@a2ui/lit/ui";
componentRegistry.register(
"TextField",
MyPremiumTextField,
"my-premium-textfield"
);
```
**Result:**
When the server sends a `TextField` component, the client will now render `<my-premium-textfield>` instead of the default `<a2ui-textfield>`.
## Verify
You can verify the component by creating a simple HTML test file or by sending a server message with the new component type.
**Server message example:**
```json
{
"surfaceId": "main",
"component": {
"type": "MyComponent",
"id": "comp-1",
"properties": {
"myProp": "Hello World"
}
}
}
```
## Troubleshooting
- **`NotSupportedError`**: If you see "constructor has already been used", ensure you **removed** the `@customElement` decorator from your component class.
- **Component not rendering**: Check if `registerCustomComponents()` is actually called. Verify the tag name in the DOM matches what you registered (e.g., `<my-component>` vs `<a2ui-custom-mycomponent>`).
- **Styles missing**: Ensure `static styles` includes `...Root.styles`.

View File

@@ -0,0 +1,160 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Root } from '@a2ui/lit/ui';
import { v0_8 } from '@a2ui/lit';
import { html, css, TemplateResult } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { map } from 'lit/directives/map.js';
// Use aliases for convenience
const StateEvent = v0_8.Events.StateEvent;
type Action = v0_8.Types.Action;
export interface OrgChartNode {
title: string;
name: string;
}
@customElement('org-chart')
export class OrgChart extends Root {
@property({ type: Array }) accessor chain: OrgChartNode[] = [];
@property({ type: Object }) accessor action: Action | null = null;
static styles = [
...Root.styles,
css`
:host {
display: block;
padding: 16px;
font-family: 'Roboto', sans-serif;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.node {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 24px;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
min-width: 200px;
position: relative;
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
}
.node:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.node:focus {
outline: 2px solid #1a73e8;
outline-offset: 2px;
}
.node.current {
background: #e8f0fe;
border-color: #1a73e8;
border-width: 2px;
}
.title {
font-size: 0.85rem;
color: #5f6368;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
}
.name {
font-size: 1.1rem;
font-weight: 500;
color: #202124;
}
.arrow {
color: #9aa0a6;
font-size: 24px;
line-height: 1;
}
`];
render() {
if (!this.chain || this.chain.length === 0) {
return html`<div class="empty">No hierarchy data</div>`;
}
return html`
<div class="container">
${map(this.chain, (node, index) => {
const isLast = index === this.chain.length - 1;
return html`
<button
class="node ${isLast ? 'current' : ''}"
@click=${() => this.handleNodeClick(node)}
aria-label="Select ${node.name} (${node.title})"
>
<span class="title">${node.title}</span>
<span class="name">${node.name}</span>
</button>
${!isLast ? html`<div class="arrow">↓</div>` : ''}
`;
})}
</div>
`;
}
private handleNodeClick(node: OrgChartNode) {
if (!this.action) return;
// Create a new action with the node's context merged in
const newContext = [
...(this.action.context || []),
{
key: 'clickedNodeTitle',
value: { literalString: node.title }
},
{
key: 'clickedNodeName',
value: { literalString: node.name }
}
];
const actionWithContext: Action = {
...this.action,
context: newContext as Action['context']
};
const evt = new StateEvent<"a2ui.action">({
eventType: "a2ui.action",
action: actionWithContext,
dataContextPath: this.dataContextPath,
sourceComponentId: this.id,
sourceComponent: this.component,
});
this.dispatchEvent(evt);
}
}

View File

@@ -0,0 +1,100 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Root } from '@a2ui/lit/ui';
import { html, css } from 'lit';
import { property } from 'lit/decorators.js';
export class PremiumTextField extends Root {
@property() accessor label = '';
@property() accessor text = '';
static styles = [
...Root.styles,
css`
:host {
display: block;
padding: 16px;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
border: 1px solid #e0e0e0;
transition: all 0.2s ease;
font-family: 'Inter', sans-serif;
}
:host(:hover) {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.12);
}
.input-container {
position: relative;
margin-top: 8px;
}
input {
width: 100%;
padding: 12px 16px;
font-size: 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
outline: none;
transition: border-color 0.2s;
box-sizing: border-box;
background: #fafafa;
}
input:focus {
border-color: #6200ee;
background: #fff;
}
label {
display: block;
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.hint {
margin-top: 8px;
font-size: 12px;
color: #666;
display: flex;
align-items: center;
gap: 4px;
}
.badge {
background: #6200ee;
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: bold;
text-transform: uppercase;
}
`
];
render() {
return html`
<label>${this.label}</label>
<div class="input-container">
<input type="text" .value="${this.text}" placeholder="Type here...">
</div>
<div class="hint">
<span class="badge">Custom</span>
<span>This is a premium override of the standard TextField.</span>
</div>
`;
}
}

View File

@@ -0,0 +1,33 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { componentRegistry } from "@a2ui/lit/ui";
import { OrgChart } from "./org-chart.js";
import { PremiumTextField } from "./premium-text-field.js";
export function registerContactComponents() {
// Register OrgChart
componentRegistry.register("OrgChart", OrgChart, "org-chart");
// Register PremiumTextField as an override for TextField
componentRegistry.register(
"TextField",
PremiumTextField,
"premium-text-field"
);
console.log("Registered Contact App Custom Components");
}

View File

@@ -0,0 +1,30 @@
# Contact sample verification tests
This directory contains tests to verify custom component integration specifically within the `contact` sample application environment.
## How to run
These tests run via the Vite development server used by the contact sample.
### 1. Start the dev server
From the `web/lit/samples/contact` directory, run:
```bash
npm run dev
```
### 2. Access the tests
Open your browser and navigate to the local server (usually port 5173):
- **Component override test**:
[http://localhost:5173/ui/custom-components/test/override-test.html](http://localhost:5173/ui/custom-components/test/override-test.html)
*Verifies that a standard component (TextField) can be overridden by a custom implementation.*
- **Hierarchy graph integration test**:
[http://localhost:5173/ui/custom-components/test/hierarchy-test.html](http://localhost:5173/ui/custom-components/test/hierarchy-test.html)
*Verifies that the HierarchyGraph component renders correctly within the contact app's build setup.*
## Files
- `override-test.html` & `override-test.ts`: Implements and tests a custom `TextField` override.
- `hierarchy-test.html`: Tests the `HierarchyGraph` component.

View File

@@ -0,0 +1,86 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>A2UI Org Chart Test (Contact Sample)</title>
<style>
body {
font-family: sans-serif;
padding: 20px;
background: #f5f5f5;
}
.container {
margin-top: 20px;
display: flex;
justify-content: center;
}
</style>
<script type="module">
import { registerContactComponents } from '../register-components.js';
// Register custom components
registerContactComponents();
console.log('Custom components registered in Contact Sample');
</script>
</head>
<body>
<h1>A2UI Org Chart Test (Contact Sample)</h1>
<div class="container">
<org-chart id="graph"></org-chart>
</div>
<script>
customElements.whenDefined('org-chart').then(() => {
const graph = document.getElementById('graph');
graph.chain = [
{ title: 'CEO', name: 'Alice Johnson' },
{ title: 'SVP', name: 'Bob Smith' },
{ title: 'VP', name: 'Charlie Brown' },
{ title: 'Director', name: 'Diana Prince' },
{ title: 'Software Engineer', name: 'Evan Wright' }
];
graph.action = {
name: 'showContactCard',
context: []
};
graph.addEventListener('a2uiaction', (e) => {
const status = document.createElement('div');
const clickedNodeTitle = e.detail.action.context.find(c => c.key === 'clickedNodeTitle')?.value.literalString;
const clickedNodeName = e.detail.action.context.find(c => c.key === 'clickedNodeName')?.value.literalString;
status.textContent = `Clicked: ${clickedNodeTitle} - ${clickedNodeName}`;
status.style.marginTop = '20px';
status.style.padding = '10px';
status.style.background = '#e0f7fa';
status.style.border = '1px solid #006064';
status.id = 'action-status';
document.body.appendChild(status);
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,43 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>A2UI Component Override Test</title>
<style>
body {
padding: 20px;
font-family: sans-serif;
}
.container {
margin-top: 20px;
border: 1px dashed #ccc;
padding: 10px;
}
</style>
<script type="module" src="./override-test.ts"></script>
</head>
<body>
<h1>Component Override Test</h1>
<div id="app" class="container"></div>
</body>
</html>

View File

@@ -0,0 +1,46 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { componentRegistry, Root } from "@a2ui/lit/ui";
import { html, css } from "lit";
import { property } from "lit/decorators.js";
// 1. Define the override
import { PremiumTextField } from "../premium-text-field.js";
// 2. Register it as "TextField"
componentRegistry.register("TextField", PremiumTextField, "premium-text-field");
console.log("Registered PremiumTextField override");
// 3. Render a standard TextField component node
const container = document.getElementById("app");
if (container) {
const root = document.createElement("a2ui-root") as Root;
const textFieldComponent = {
type: "TextField",
id: "tf-1",
properties: {
label: "Enter your name",
text: "John Doe",
},
};
// Root renders its *children*, so we must pass the component as a child.
root.childComponents = [textFieldComponent];
root.enableCustomElements = true; // Enable the feature
container.appendChild(root);
}

View File

@@ -0,0 +1,299 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { LitElement, html, css, nothing, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import { SnackbarMessage, SnackbarUUID, SnackType } from "../types/types";
import { repeat } from "lit/directives/repeat.js";
import { SnackbarActionEvent } from "../events/events";
import { classMap } from "lit/directives/class-map.js";
import { v0_8 } from "@a2ui/lit";
const DEFAULT_TIMEOUT = 8000;
@customElement("ui-snackbar")
export class Snackbar extends LitElement {
@property({ reflect: true, type: Boolean })
accessor active = false;
@property({ reflect: true, type: Boolean })
accessor error = false;
@property()
accessor timeout = DEFAULT_TIMEOUT;
#messages: SnackbarMessage[] = [];
#timeout = 0;
static styles = [
unsafeCSS(v0_8.Styles.structuralStyles),
css`
:host {
--text-color: var(--n-0);
--bb-body-medium: 16px;
--bb-body-line-height-medium: 24px;
display: flex;
align-items: center;
position: fixed;
bottom: var(--bb-grid-size-7);
left: 50%;
translate: -50% 0;
opacity: 0;
pointer-events: none;
border-radius: var(--bb-grid-size-2);
background: var(--n-90);
padding: var(--bb-grid-size-3) var(--bb-grid-size-6);
width: 60svw;
max-width: 720px;
z-index: 1800;
scrollbar-width: none;
overflow-x: scroll;
font: 400 var(--bb-body-medium) / var(--bb-body-line-height-medium)
var(--bb-font-family);
}
:host([active]) {
transition: opacity 0.3s cubic-bezier(0, 0, 0.3, 1) 0.2s;
opacity: 1;
pointer-events: auto;
}
:host([error]) {
background: var(--e-90);
--text-color: var(--e-40);
}
.g-icon {
flex: 0 0 auto;
color: var(--text-color);
margin-right: var(--bb-grid-size-4);
&.rotate {
animation: 1s linear 0s infinite normal forwards running rotate;
}
}
#messages {
color: var(--text-color);
flex: 1 1 auto;
margin-right: var(--bb-grid-size-11);
a,
a:visited {
color: var(--bb-ui-600);
text-decoration: none;
&:hover {
color: var(--bb-ui-500);
text-decoration: underline;
}
}
}
#actions {
flex: 0 1 auto;
width: fit-content;
margin-right: var(--bb-grid-size-3);
& button {
font: 500 var(--bb-body-medium) / var(--bb-body-line-height-medium)
var(--bb-font-family);
padding: 0;
background: transparent;
border: none;
margin: 0 var(--bb-grid-size-4);
color: var(--text-color);
opacity: 0.7;
transition: opacity 0.2s cubic-bezier(0, 0, 0.3, 1);
&:not([disabled]) {
cursor: pointer;
&:hover,
&:focus {
opacity: 1;
}
}
}
}
#close {
display: flex;
align-items: center;
padding: 0;
color: var(--text-color);
background: transparent;
border: none;
margin: 0 0 0 var(--bb-grid-size-2);
opacity: 0.7;
transition: opacity 0.2s cubic-bezier(0, 0, 0.3, 1);
.g-icon {
margin-right: 0;
}
&:not([disabled]) {
cursor: pointer;
&:hover,
&:focus {
opacity: 1;
}
}
}
@keyframes rotate {
from {
rotate: 0deg;
}
to {
rotate: 360deg;
}
}
`,
];
show(message: SnackbarMessage, replaceAll = false) {
const existingMessage = this.#messages.findIndex(
(msg) => msg.id === message.id
);
if (existingMessage === -1) {
if (replaceAll) {
this.#messages.length = 0;
}
this.#messages.push(message);
} else {
this.#messages[existingMessage] = message;
}
window.clearTimeout(this.#timeout);
if (!this.#messages.every((msg) => msg.persistent)) {
this.#timeout = window.setTimeout(() => {
this.hide();
}, this.timeout);
}
this.error = this.#messages.some((msg) => msg.type === SnackType.ERROR);
this.active = true;
this.requestUpdate();
return message.id;
}
hide(id?: SnackbarUUID) {
if (id) {
const idx = this.#messages.findIndex((msg) => msg.id === id);
if (idx !== -1) {
this.#messages.splice(idx, 1);
}
} else {
this.#messages.length = 0;
}
this.active = this.#messages.length !== 0;
this.updateComplete.then((avoidedUpdate) => {
if (!avoidedUpdate) {
return;
}
this.requestUpdate();
});
}
render() {
let rotate = false;
let icon = "";
for (let i = this.#messages.length - 1; i >= 0; i--) {
if (
!this.#messages[i].type ||
this.#messages[i].type === SnackType.NONE
) {
continue;
}
icon = this.#messages[i].type;
if (this.#messages[i].type === SnackType.PENDING) {
icon = "progress_activity";
rotate = true;
}
break;
}
return html` ${icon
? html`<span
class=${classMap({
"g-icon": true,
round: true,
filled: true,
rotate,
})}
>${icon}</span
>`
: nothing}
<div id="messages">
${repeat(
this.#messages,
(message) => message.id,
(message) => {
return html`<div>${message.message}</div>`;
}
)}
</div>
<div id="actions">
${repeat(
this.#messages,
(message) => message.id,
(message) => {
if (!message.actions) {
return nothing;
}
return html`${repeat(
message.actions,
(action) => action.value,
(action) => {
return html`<button
@click=${() => {
this.hide();
this.dispatchEvent(
new SnackbarActionEvent(
action.action,
action.value,
action.callback
)
);
}}
>
${action.title}
</button>`;
}
)}`;
}
)}
</div>
<button
id="close"
@click=${() => {
this.hide();
this.dispatchEvent(new SnackbarActionEvent("dismiss"));
}}
>
<span class="g-icon">close</span>
</button>`;
}
}

View File

@@ -0,0 +1,17 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export { Snackbar } from "./snackbar";

View File

@@ -0,0 +1,45 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { config } from "dotenv";
import { UserConfig } from "vite";
import * as Middleware from "./middleware";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
export default async () => {
config();
const entry: Record<string, string> = {
contact: resolve(__dirname, "index.html"),
};
return {
plugins: [Middleware.A2AMiddleware.plugin()],
build: {
rollupOptions: {
input: entry,
},
target: "esnext",
},
define: {},
resolve: {
dedupe: ["lit"],
},
} satisfies UserConfig;
};

View File

@@ -0,0 +1,24 @@
{
"name": "@a2ui/lit-samples",
"private": true,
"version": "0.8.1",
"description": "A2UI Lit Samples",
"workspaces": [
"contact",
"restaurant",
"shell"
],
"scripts": {
"serve:agent:restaurant": "cd ../../general_agent && uv run .",
"serve:agent:contact_example": "cd ../../agent/as/contact_example && uv run .",
"serve:agent:contact_multi_surface": "cd ../../agent/adk/contact_multiple_surfaces && uv run .",
"serve:shell": "cd shell && npm run dev",
"build:renderer": "cd ../../../renderers/web_core && npm install && npm run build && cd ../lit && npm install && npm run build",
"demo:all": "npm install && npm run build:renderer && concurrently -k -n \"SHELL,REST,CONT1\" -c \"magenta,blue,green\" \"npm run serve:shell\" \"npm run serve:agent:restaurant\" \"npm run serve:agent:contact_example\"",
"demo:restaurant": "npm install && npm run build:renderer && concurrently -k -n \"SHELL,REST\" -c \"magenta,blue\" \"npm run serve:shell\" \"npm run serve:agent:restaurant\"",
"demo:contact_example": "npm install && npm run build:renderer && concurrently -k -n \"SHELL,CONT1\" -c \"magenta,green\" \"npm run serve:shell\" \"npm run serve:agent:contact_example\""
},
"devDependencies": {
"concurrently": "9.2.1"
}
}

View File

@@ -0,0 +1,38 @@
# A2UI Generator
This is a UI to generate and visualize A2UI responses.
## Prerequisites
1. [nodejs](https://nodejs.org/en)
## Running
This sample depends on the Lit renderer. Before running this sample, you need to build the renderer.
1. **Build the renderer:**
```bash
cd ../../../renderers/lit
npm install
npm run build
```
2. **Run this sample:**
```bash
cd - # back to the sample directory
npm install
```
3. **Run the servers:**
- Run the [A2A server](../../../general_agent/)
- Run the dev server: `npm run dev`
After starting the dev server, you can open http://localhost:5173/ to view the sample.
Important: The sample code provided is for demonstration purposes and illustrates the mechanics of A2UI and the Agent-to-Agent (A2A) protocol. When building production applications, it is critical to treat any agent operating outside of your direct control as a potentially untrusted entity.
All operational data received from an external agent—including its AgentCard, messages, artifacts, and task statuses—should be handled as untrusted input. For example, a malicious agent could provide crafted data in its fields (e.g., name, skills.description) that, if used without sanitization to construct prompts for a Large Language Model (LLM), could expose your application to prompt injection attacks.
Similarly, any UI definition or data stream received must be treated as untrusted. Malicious agents could attempt to spoof legitimate interfaces to deceive users (phishing), inject malicious scripts via property values (XSS), or generate excessive layout complexity to degrade client performance (DoS). If your application supports optional embedded content (such as iframes or web views), additional care must be taken to prevent exposure to malicious external sites.
Developer Responsibility: Failure to properly validate data and strictly sandbox rendered content can introduce severe vulnerabilities. Developers are responsible for implementing appropriate security measures—such as input sanitization, Content Security Policies (CSP), strict isolation for optional embedded content, and secure credential handling—to protect their systems and users.

View File

@@ -0,0 +1,163 @@
# A2UI Theming & Configuration Guide
This guide explains how the Universal App Shell handles theming and how to add new sample applications seamlessly.
## Architecture Overview
The styling system is built on two distinct layers:
### 1. **Base Layer (`default-theme.ts`)**
- **Role**: Structural & Functional Styles.
- **What it does**: Maps A2UI components (like `Text`, `Card`, `Row`) to functional CSS utility classes (e.g., `layout-w-100`, `typography-f-sf`).
- **When to touch**: Rarely. Only if you need to change the fundamental layout behavior of a component across all shell apps.
### 2. **Configuration Layer (`configs/*.ts`)**
- **Role**: App Identity & Brand Overrides.
- **What it does**: Allows for app-level theme overrides.
- **Key Mechanism**: The `AppConfig` interface allows you to provide a new theme by setting items in the `theme` property.
- **When to touch**: Whenever you add a new app and want to change an app's theme from the default theme provided with the shell.
---
## How to Add a New Sample App
Follow these steps to add a new application (e.g., "Flight Booker") with its own unique theme.
### Step 1: Create the Config
Create a new file `configs/flights.ts`:
```typescript
import { AppConfig } from "./types.js";
import { cloneDefaultTheme } from "../theme/clone-default-theme.js";
const theme = cloneDefaultTheme();
// Set your variables, e.g., theme.components.Card = { 'color-bgc-n100': true }
export const config: AppConfig = {
key: "flights",
title: "Flight Booker",
heroImage: "/hero-flights.png",
heroImageDark: "/hero-flights-dark.png", // Optional
placeholder: "Where do you want to go?",
loadingText: ["Checking availability...", "Finding best rates..."],
serverUrl: "http://localhost:10004", // Your agent's URL
theme, // Apply the theme.
};
```
### Step 2: Register the Config
Update `app.ts` to include your new config:
```typescript
import { config as flightsConfig } from "./configs/flights.js";
const configs: Record<string, AppConfig> = {
restaurant: restaurantConfig,
contacts: contactsConfig,
flights: flightsConfig, // Add this line
};
```
### Step 3: Run It
Access your new app by adding the `app` query parameter:
`http://localhost:5173/?app=flights`
The App Shell will automatically:
1. Load your `flights` config.
2. Apply your theme to the A2UI root's theme context.
3. Connect to your specified `serverUrl`.
---
## Reference: Styling Levers
This section lists the available styling "levers" (utility classes) you can use in your `theme.ts` file or directly in your components. These are defined in the core library (`renderers/lit/src/0.8/styles`).
### 1. Layout (`layout-`)
**Source:** `styles/layout.ts`
| Category | Prefix | Scale/Values | Examples |
| :-------------- | :------------ | :------------------------------------------ | :---------------------------------------------------------- |
| **Padding** | `layout-p-` | 0-24 (1 = 4px) | `layout-p-4` (16px), `layout-pt-2` (Top 8px), `layout-px-4` |
| **Margin** | `layout-m-` | 0-24 (1 = 4px) | `layout-m-0`, `layout-mb-4` (Bottom 16px), `layout-mx-auto` |
| **Gap** | `layout-g-` | 0-24 (1 = 4px) | `layout-g-2` (8px), `layout-g-4` (16px) |
| **Width** | `layout-w-` | 10-100 (Percentage) | `layout-w-100` (100%), `layout-w-50` (50%) |
| **Width (Px)** | `layout-wp-` | 0-15 (1 = 4px) | `layout-wp-10` (40px) |
| **Height** | `layout-h-` | 10-100 (Percentage) | `layout-h-100` (100%) |
| **Height (Px)** | `layout-hp-` | 0-15 (1 = 4px) | `layout-hp-10` (40px) |
| **Display** | `layout-dsp-` | `none`, `block`, `grid`, `flex`, `iflex` | `layout-dsp-flexhor` (Row), `layout-dsp-flexvert` (Col) |
| **Alignment** | `layout-al-` | `fs` (Start), `fe` (End), `c` (Center) | `layout-al-c` (Align Items Center) |
| **Justify** | `layout-sp-` | `c` (Center), `bt` (Between), `ev` (Evenly) | `layout-sp-bt` (Justify Content Space Between) |
| **Flex** | `layout-flx-` | `0` (None), `1` (Grow) | `layout-flx-1` (Flex Grow 1) |
| **Position** | `layout-pos-` | `a` (Absolute), `rel` (Relative) | `layout-pos-rel` |
### 2. Colors (`color-`)
**Source:** `styles/colors.ts`
| Category | Prefix | Scale/Values | Examples |
| :--------------- | :----------- | :------------------ | :-------------------------------------------------------------------- |
| **Text Color** | `color-c-` | Palette Key + Shade | `color-c-p50` (Primary), `color-c-n10` (Black), `color-c-e40` (Error) |
| **Background** | `color-bgc-` | Palette Key + Shade | `color-bgc-p100` (White/Lightest), `color-bgc-s30` (Secondary Dark) |
| **Border Color** | `color-bc-` | Palette Key + Shade | `color-bc-p60` (Primary Border) |
**Palette Keys:**
- `p` = Primary (Brand)
- `s` = Secondary
- `t` = Tertiary
- `n` = Neutral (Grays)
- `nv` = Neutral Variant
- `e` = Error
**Shades:** 0, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 95, 98, 99, 100
### 3. Typography (`typography-`)
**Source:** `styles/type.ts`
| Category | Prefix | Scale/Values | Examples |
| :------------------ | :--------------- | :---------------------------------------- | :----------------------------------------------------------------------------------- |
| **Font Family** | `typography-f-` | `sf` (Sans/Flex), `s` (Serif), `c` (Code) | `typography-f-sf` (System UI / Outfit) |
| **Weight** | `typography-w-` | 100-900 | `typography-w-400` (Regular), `typography-w-500` (Medium), `typography-w-700` (Bold) |
| **Size (Body)** | `typography-sz-` | `bs`, `bm`, `bl` | `typography-sz-bm` (Body Medium - 14px) |
| **Size (Title)** | `typography-sz-` | `ts`, `tm`, `tl` | `typography-sz-tl` (Title Large - 22px) |
| **Size (Headline)** | `typography-sz-` | `hs`, `hm`, `hl` | `typography-sz-hl` (Headline Large - 32px) |
| **Size (Display)** | `typography-sz-` | `ds`, `dm`, `dl` | `typography-sz-dl` (Display Large - 57px) |
| **Align** | `typography-ta-` | `s` (Start), `c` (Center) | `typography-ta-c` |
### 4. Borders (`border-`)
**Source:** `styles/border.ts`
| Category | Prefix | Scale/Values | Examples |
| :--------- | :----------- | :------------- | :---------------------------------------------------- |
| **Radius** | `border-br-` | 0-24 (1 = 4px) | `border-br-4` (16px), `border-br-50pc` (50% / Circle) |
| **Width** | `border-bw-` | 0-24 (Pixels) | `border-bw-1` (1px), `border-bw-2` (2px) |
| **Style** | `border-bs-` | `s` (Solid) | `border-bs-s` |
### 5. Behavior & Opacity
**Source:** `styles/behavior.ts`, `styles/opacity.ts`
| Category | Prefix | Scale/Values | Examples |
| :---------------- | :------------- | :------------------------------------- | :-------------------------------------- |
| **Hover Opacity** | `behavior-ho-` | 0-100 (Step 5) | `behavior-ho-80` (Opacity 0.8 on hover) |
| **Opacity** | `opacity-el-` | 0-100 (Step 5) | `opacity-el-50` (Opacity 0.5) |
| **Overflow** | `behavior-o-` | `s` (Scroll), `a` (Auto), `h` (Hidden) | `behavior-o-h` |
| **Scrollbar** | `behavior-sw-` | `n` (None) | `behavior-sw-n` |
### 6. Icons
**Source:** `styles/icons.ts`
- Class: `.g-icon`
- Variants: `.filled`, `.filled-heavy`
- Usage: `<span class="g-icon">icon_name</span>`

View File

@@ -0,0 +1,560 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { SignalWatcher } from "@lit-labs/signals";
import { provide } from "@lit/context";
import {
LitElement,
html,
css,
nothing,
HTMLTemplateResult,
unsafeCSS,
} from "lit";
import { customElement, state } from "lit/decorators.js";
import { theme as uiTheme } from "./theme/default-theme.js";
import { A2UIClient } from "./client.js";
import {
SnackbarAction,
SnackbarMessage,
SnackbarUUID,
SnackType,
} from "./types/types.js";
import { type Snackbar } from "./ui/snackbar.js";
import { repeat } from "lit/directives/repeat.js";
import { v0_8 } from "@a2ui/lit";
import * as UI from "@a2ui/lit/ui";
// App elements.
import "./ui/ui.js";
// Configurations
import { AppConfig } from "./configs/types.js";
import { config as restaurantConfig } from "./configs/restaurant.js";
import { config as contactsConfig } from "./configs/contacts.js";
import { styleMap } from "lit/directives/style-map.js";
const configs: Record<string, AppConfig> = {
restaurant: restaurantConfig,
contacts: contactsConfig,
};
@customElement("a2ui-shell")
export class A2UILayoutEditor extends SignalWatcher(LitElement) {
@provide({ context: UI.Context.themeContext })
accessor theme: v0_8.Types.Theme = uiTheme;
@state()
accessor #requesting = false;
@state()
accessor #error: string | null = null;
@state()
accessor #lastMessages: v0_8.Types.ServerToClientMessage[] = [];
@state()
accessor config: AppConfig = configs.restaurant;
@state()
accessor #loadingTextIndex = 0;
#loadingInterval: number | undefined;
static styles = [
unsafeCSS(v0_8.Styles.structuralStyles),
css`
* {
box-sizing: border-box;
}
:host {
display: block;
max-width: 640px;
margin: 0 auto;
min-height: 100%;
color: light-dark(var(--n-10), var(--n-90));
font-family: var(--font-family);
}
#hero-img {
width: 100%;
max-width: 400px;
aspect-ratio: 1280/720;
height: auto;
margin-bottom: var(--bb-grid-size-6);
display: block;
margin: 0 auto;
background: var(--background-image-light) center center / contain
no-repeat;
}
#surfaces {
width: 100%;
max-width: 100svw;
padding: var(--bb-grid-size-3);
animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards;
}
form {
display: flex;
flex-direction: column;
flex: 1;
gap: 16px;
align-items: center;
padding: 16px 0;
animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 1s backwards;
& h1 {
color: light-dark(var(--p-40), var(--n-90));
}
& > div {
display: flex;
flex: 1;
gap: 16px;
align-items: center;
width: 100%;
& > input {
display: block;
flex: 1;
border-radius: 32px;
padding: 16px 24px;
border: 1px solid var(--p-60);
background: light-dark(var(--n-100), var(--n-10));
font-size: 16px;
}
& > button {
display: flex;
align-items: center;
background: var(--p-40);
color: var(--n-100);
border: none;
padding: 8px 16px;
border-radius: 32px;
opacity: 0.5;
&:not([disabled]) {
cursor: pointer;
opacity: 1;
}
}
}
}
.rotate {
animation: rotate 1s linear infinite;
}
.pending {
width: 100%;
min-height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards;
gap: 16px;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid rgba(255, 255, 255, 0.1);
border-left-color: var(--p-60);
border-radius: 50%;
animation: spin 1s linear infinite;
}
.theme-toggle {
padding: 0;
margin: 0;
border: none;
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: var(--bb-grid-size-3);
right: var(--bb-grid-size-4);
background: light-dark(var(--n-100), var(--n-0));
border-radius: 50%;
color: var(--p-30);
cursor: pointer;
width: 48px;
height: 48px;
font-size: 32px;
& .g-icon {
pointer-events: none;
&::before {
content: "dark_mode";
}
}
}
@container style(--color-scheme: dark) {
.theme-toggle .g-icon::before {
content: "light_mode";
color: var(--n-90);
}
#hero-img {
background-image: var(--background-image-dark);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0% {
opacity: 0.6;
}
50% {
opacity: 1;
}
100% {
opacity: 0.6;
}
}
.error {
color: var(--e-40);
background-color: var(--e-95);
border: 1px solid var(--e-80);
padding: 16px;
border-radius: 8px;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes rotate {
from {
rotate: 0deg;
}
to {
rotate: 360deg;
}
}
`,
];
#processor = v0_8.Data.createSignalA2uiMessageProcessor();
#a2uiClient = new A2UIClient();
#snackbar: Snackbar | undefined = undefined;
#pendingSnackbarMessages: Array<{
message: SnackbarMessage;
replaceAll: boolean;
}> = [];
#maybeRenderError() {
if (!this.#error) return nothing;
return html`<div class="error">${this.#error}</div>`;
}
connectedCallback() {
super.connectedCallback();
// Load config from URL
const urlParams = new URLSearchParams(window.location.search);
const appKey = urlParams.get("app") || "restaurant";
this.config = configs[appKey] || configs.restaurant;
// Apply the theme directly, which will use the Lit context.
if (this.config.theme) {
this.theme = this.config.theme;
}
window.document.title = this.config.title;
window.document.documentElement.style.setProperty(
"--background",
this.config.background
);
// Initialize client with configured URL
this.#a2uiClient = new A2UIClient(this.config.serverUrl);
}
render() {
return [
this.#renderThemeToggle(),
this.#maybeRenderForm(),
this.#maybeRenderData(),
this.#maybeRenderError(),
];
}
#renderThemeToggle() {
return html` <div>
<button
@click=${(evt: Event) => {
if (!(evt.target instanceof HTMLButtonElement)) return;
const { colorScheme } = window.getComputedStyle(evt.target);
if (colorScheme === "dark") {
document.body.classList.add("light");
document.body.classList.remove("dark");
} else {
document.body.classList.add("dark");
document.body.classList.remove("light");
}
}}
class="theme-toggle"
>
<span class="g-icon filled-heavy"></span>
</button>
</div>`;
}
#maybeRenderForm() {
if (this.#requesting) return nothing;
if (this.#lastMessages.length > 0) return nothing;
return html` <form
@submit=${async (evt: Event) => {
evt.preventDefault();
if (!(evt.target instanceof HTMLFormElement)) {
return;
}
const data = new FormData(evt.target);
const body = data.get("body") ?? null;
if (!body) {
return;
}
const message = body as v0_8.Types.A2UIClientEventMessage;
await this.#sendAndProcessMessage(message);
}}
>
${this.config.heroImage
? html`<div
style=${styleMap({
"--background-image-light": `url(${this.config.heroImage})`,
"--background-image-dark": `url(${this.config.heroImageDark ?? this.config.heroImage
})`,
})}
id="hero-img"
></div>`
: nothing}
<h1 class="app-title">${this.config.title}</h1>
<div>
<input
required
value="${this.config.placeholder}"
autocomplete="off"
id="body"
name="body"
type="text"
?disabled=${this.#requesting}
/>
<button type="submit" ?disabled=${this.#requesting}>
<span class="g-icon filled-heavy">send</span>
</button>
</div>
</form>`;
}
#startLoadingAnimation() {
if (
Array.isArray(this.config.loadingText) &&
this.config.loadingText.length > 1
) {
this.#loadingTextIndex = 0;
this.#loadingInterval = window.setInterval(() => {
this.#loadingTextIndex =
(this.#loadingTextIndex + 1) %
(this.config.loadingText as string[]).length;
}, 2000);
}
}
#stopLoadingAnimation() {
if (this.#loadingInterval) {
clearInterval(this.#loadingInterval);
this.#loadingInterval = undefined;
}
}
async #sendMessage(
message: v0_8.Types.A2UIClientEventMessage
): Promise<v0_8.Types.ServerToClientMessage[]> {
try {
this.#requesting = true;
this.#startLoadingAnimation();
const response = this.#a2uiClient.send(message);
await response;
this.#requesting = false;
this.#stopLoadingAnimation();
return response;
} catch (err) {
this.snackbar(err as string, SnackType.ERROR);
} finally {
this.#requesting = false;
this.#stopLoadingAnimation();
}
return [];
}
#maybeRenderData() {
if (this.#requesting) {
let text = "Awaiting an answer...";
if (this.config.loadingText) {
if (Array.isArray(this.config.loadingText)) {
text = this.config.loadingText[this.#loadingTextIndex];
} else {
text = this.config.loadingText;
}
}
return html` <div class="pending">
<div class="spinner"></div>
<div class="loading-text">${text}</div>
</div>`;
}
const surfaces = this.#processor.getSurfaces();
if (surfaces.size === 0) {
return nothing;
}
return html`<section id="surfaces">
${repeat(
this.#processor.getSurfaces(),
([surfaceId]) => surfaceId,
([surfaceId, surface]) => {
return html`<a2ui-surface
@a2uiaction=${async (
evt: v0_8.Events.StateEvent<"a2ui.action">
) => {
const [target] = evt.composedPath();
if (!(target instanceof HTMLElement)) {
return;
}
const context: v0_8.Types.A2UIClientEventMessage["userAction"]["context"] =
{};
if (evt.detail.action.context) {
const srcContext = evt.detail.action.context;
for (const item of srcContext) {
if (item.value.literalBoolean) {
context[item.key] = item.value.literalBoolean;
} else if (item.value.literalNumber) {
context[item.key] = item.value.literalNumber;
} else if (item.value.literalString) {
context[item.key] = item.value.literalString;
} else if (item.value.path) {
const path = this.#processor.resolvePath(
item.value.path,
evt.detail.dataContextPath
);
const value = this.#processor.getData(
evt.detail.sourceComponent,
path,
surfaceId
);
context[item.key] = value;
}
}
}
const message: v0_8.Types.A2UIClientEventMessage = {
userAction: {
name: evt.detail.action.name,
surfaceId,
sourceComponentId: target.id,
timestamp: new Date().toISOString(),
context,
},
};
await this.#sendAndProcessMessage(message);
}}
.surfaceId=${surfaceId}
.surface=${surface}
.processor=${this.#processor}
></a2-uisurface>`;
}
)}
</section>`;
}
async #sendAndProcessMessage(request) {
const messages = await this.#sendMessage(request);
console.log(messages);
this.#lastMessages = messages;
this.#processor.clearSurfaces();
this.#processor.processMessages(messages);
}
snackbar(
message: string | HTMLTemplateResult,
type: SnackType,
actions: SnackbarAction[] = [],
persistent = false,
id = globalThis.crypto.randomUUID(),
replaceAll = false
) {
if (!this.#snackbar) {
this.#pendingSnackbarMessages.push({
message: {
id,
message,
type,
persistent,
actions,
},
replaceAll,
});
return;
}
return this.#snackbar.show(
{
id,
message,
type,
persistent,
actions,
},
replaceAll
);
}
unsnackbar(id?: SnackbarUUID) {
if (!this.#snackbar) {
return;
}
this.#snackbar.hide(id);
}
}

View File

@@ -0,0 +1,127 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Message, Part, SendMessageSuccessResponse, Task } from "@a2a-js/sdk";
import { A2AClient } from "@a2a-js/sdk/client";
import { v0_8 } from "@a2ui/lit";
const A2AUI_MIME_TYPE = "application/json+a2aui";
export class A2UIClient {
#serverUrl: string;
#client: A2AClient | null = null;
constructor(serverUrl: string = "") {
this.#serverUrl = serverUrl;
}
#ready: Promise<void> = Promise.resolve();
get ready() {
return this.#ready;
}
async #getClient() {
if (!this.#client) {
// Default to localhost:10002 if no URL provided (fallback for restaurant app default)
const baseUrl = this.#serverUrl || "http://localhost:10002";
this.#client = await A2AClient.fromCardUrl(
`${baseUrl}/.well-known/agent-card.json`,
{
fetchImpl: async (url, init) => {
const headers = new Headers(init?.headers);
headers.set("X-A2A-Extensions", "https://a2ui.org/a2a-extension/a2ui/v0.8");
return fetch(url, { ...init, headers });
}
}
);
}
return this.#client;
}
async send(
message: v0_8.Types.A2UIClientEventMessage | string
): Promise<v0_8.Types.ServerToClientMessage[]> {
const client = await this.#getClient();
let parts: Part[] = [];
if (typeof message === 'string') {
// Try to parse as JSON first, just in case
try {
const parsed = JSON.parse(message);
if (typeof parsed === 'object' && parsed !== null) {
parts = [{
kind: "data",
data: parsed as unknown as Record<string, unknown>,
mimeType: A2AUI_MIME_TYPE,
} as Part];
} else {
parts = [{ kind: "text", text: message }];
}
} catch {
parts = [{ kind: "text", text: message }];
}
} else {
parts = [{
kind: "data",
data: message as unknown as Record<string, unknown>,
mimeType: A2AUI_MIME_TYPE,
} as Part];
}
const response = await client.sendMessage({
message: {
messageId: crypto.randomUUID(),
role: "user",
parts: parts,
kind: "message",
},
});
if ("error" in response) {
throw new Error(response.error.message);
}
const result = (response as SendMessageSuccessResponse).result as Task;
if (result.kind === "task" && result.status.message?.parts) {
const messages: v0_8.Types.ServerToClientMessage[] = [];
for (const part of result.status.message.parts) {
if (part.kind === 'data') {
messages.push(part.data as v0_8.Types.ServerToClientMessage);
}
}
return messages;
}
// const result = (response as SendMessageSuccessResponse).result as Message;
// if (result.kind === "message" && result.parts) {
// const messages: v0_8.Types.ServerToClientMessage[] = [];
// for (const part of result.parts) {
// if (part.kind === 'data') {
// // Parse data.arguments (JSON string) to object
// const parsedData = typeof part.data?.arguments === 'string'
// ? JSON.parse(part.data.arguments)
// : part.data?.arguments;
// messages.push(parsedData as v0_8.Types.ServerToClientMessage);
// }
// }
// return messages;
// }
return [];
}
}

View File

@@ -0,0 +1,346 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { cloneDefaultTheme } from "../theme/clone-default-theme.js";
import { AppConfig } from "./types.js";
import { v0_8 } from "@a2ui/lit";
/** Elements */
const a = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-as-n": true,
"layout-dis-iflx": true,
"layout-al-c": true,
};
const heading = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mb-2": true,
"color-c-n10": true,
};
const orderedList = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
};
const unorderedList = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
};
const listItem = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
};
const theme: v0_8.Types.Theme = {
...cloneDefaultTheme(),
additionalStyles: {
Card: {
"min-width": "320px",
"max-width": "400px",
margin: "0 auto",
background:
"linear-gradient(135deg, light-dark(#ffffff99, #ffffff44) 0%, light-dark(#ffffff, #ffffff04) 100%)",
border: "1px solid light-dark(transparent, #ffffff35)",
boxShadow:
"inset 0 20px 48px light-dark(rgba(0, 0, 0, 0.02), rgba(255, 255, 255, 0.08))",
},
Button: {
"--p-70": "light-dark(var(--p-60), var(--n-10))",
"--n-60": "light-dark(var(--n-100), var(--n-0))",
},
Image: {
"max-width": "120px",
"max-height": "120px",
marginLeft: "auto",
marginRight: "auto",
},
Text: {
"--n-40": "light-dark(var(--p-60), var(--n-90))",
},
},
components: {
AudioPlayer: {},
Button: {
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-5": true,
"layout-pr-5": true,
"border-br-2": true,
"border-bw-0": true,
"border-bs-s": true,
"color-bgc-p30": true,
"color-c-n100": true,
"behavior-ho-70": true,
},
Card: {
"border-br-4": true,
"color-bgc-p100": true,
"layout-pt-10": true,
"layout-pb-10": true,
"layout-pl-4": true,
"layout-pr-4": true,
},
CheckBox: {
element: {
"layout-m-0": true,
"layout-mr-2": true,
"layout-p-2": true,
"border-br-12": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bgc-p100": true,
"color-bc-p60": true,
"color-c-n30": true,
"color-c-p30": true,
},
label: {
"color-c-p30": true,
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-flx-1": true,
"typography-sz-ll": true,
},
container: {
"layout-dsp-iflex": true,
"layout-al-c": true,
},
},
Column: {},
DateTimeInput: {
container: {},
label: {},
element: {
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-3": true,
"layout-pr-3": true,
"border-br-12": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bgc-p100": true,
"color-bc-p60": true,
"color-c-n30": true,
},
},
Divider: {
"color-bgc-n90": true,
"layout-mt-6": true,
"layout-mb-6": true,
},
Image: {
all: {
"border-br-50pc": true,
"layout-el-cv": true,
"layout-w-100": true,
"layout-h-100": true,
"layout-dsp-flexhor": true,
"layout-al-c": true,
"layout-sp-c": true,
"layout-mb-3": true,
},
avatar: {},
header: {},
icon: {},
largeFeature: {},
mediumFeature: {},
smallFeature: {},
},
Icon: {
"border-br-1": true,
"layout-p-2": true,
"color-bgc-n98": true,
"layout-dsp-flexhor": true,
"layout-al-c": true,
"layout-sp-c": true,
},
List: {
"layout-g-4": true,
"layout-p-2": true,
},
Modal: {
backdrop: { "color-bbgc-p60_20": true },
element: {
"border-br-2": true,
"color-bgc-p100": true,
"layout-p-4": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bc-p80": true,
},
},
MultipleChoice: {
container: {},
label: {},
element: {},
},
Row: {
"layout-g-4": true,
"layout-mb-3": true,
},
Slider: {
container: {},
label: {},
element: {},
},
Tabs: {
container: {},
controls: { all: {}, selected: {} },
element: {},
},
Text: {
all: {
"layout-w-100": true,
"layout-g-2": true,
"color-c-p30": true,
},
h1: {
"typography-f-sf": true,
"typography-ta-c": true,
"typography-v-r": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mr-0": true,
"layout-ml-0": true,
"layout-mb-2": true,
"layout-p-0": true,
"typography-sz-tl": true,
},
h2: {
"typography-f-sf": true,
"typography-ta-c": true,
"typography-v-r": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mr-0": true,
"layout-ml-0": true,
"layout-mb-2": true,
"layout-p-0": true,
"typography-sz-tl": true,
},
h3: {
"typography-f-sf": true,
"typography-ta-c": true,
"typography-v-r": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mr-0": true,
"layout-ml-0": true,
"layout-mb-0": true,
"layout-p-0": true,
"typography-sz-ts": true,
},
h4: {
"typography-f-sf": true,
"typography-ta-c": true,
"typography-v-r": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mr-0": true,
"layout-ml-0": true,
"layout-mb-0": true,
"layout-p-0": true,
"typography-sz-bl": true,
},
h5: {
"typography-f-sf": true,
"typography-ta-c": true,
"typography-v-r": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mr-0": true,
"layout-ml-0": true,
"layout-mb-0": true,
"layout-p-0": true,
"color-c-n30": true,
"typography-sz-bm": true,
"layout-mb-1": true,
},
body: {},
caption: {},
},
TextField: {
container: {
"typography-sz-bm": true,
"layout-w-100": true,
"layout-g-2": true,
"layout-dsp-flexhor": true,
"layout-al-c": true,
},
label: {
"layout-flx-0": true,
},
element: {
"typography-sz-bm": true,
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-3": true,
"layout-pr-3": true,
"border-br-12": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bgc-p100": true,
"color-bc-p60": true,
"color-c-n30": true,
"color-c-p30": true,
},
},
Video: {
"border-br-5": true,
"layout-el-cv": true,
},
},
};
export const config: AppConfig = {
key: "contacts",
title: "Contact Manager",
background: `radial-gradient(at 0% 0%, light-dark(rgba(45, 212, 191, 0.4), rgba(20, 184, 166, 0.2)) 0px, transparent 50%),
radial-gradient(at 100% 0%, light-dark(rgba(56, 189, 248, 0.4), rgba(14, 165, 233, 0.2)) 0px, transparent 50%),
radial-gradient(at 100% 100%, light-dark(rgba(163, 230, 53, 0.4), rgba(132, 204, 22, 0.2)) 0px, transparent 50%),
radial-gradient(at 0% 100%, light-dark(rgba(52, 211, 153, 0.4), rgba(16, 185, 129, 0.2)) 0px, transparent 50%),
linear-gradient(120deg, light-dark(#f0fdf4, #022c22) 0%, light-dark(#dcfce7, #064e3b) 100%)`,
placeholder: "Alex Jordan",
loadingText: [
"Searching contacts...",
"Looking up details...",
"Verifying information...",
"Just a moment...",
],
serverUrl: "http://localhost:10003",
theme,
};

View File

@@ -0,0 +1,56 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { AppConfig } from "./types.js";
export const config: AppConfig = {
key: "restaurant",
title: "A2UI Agent",
heroImage: "/hero.png",
heroImageDark: "/hero-dark.png",
background: `radial-gradient(
at 0% 0%,
light-dark(rgba(161, 196, 253, 0.3), rgba(6, 182, 212, 0.15)) 0px,
transparent 50%
),
radial-gradient(
at 100% 0%,
light-dark(rgba(255, 226, 226, 0.3), rgba(59, 130, 246, 0.15)) 0px,
transparent 50%
),
radial-gradient(
at 100% 100%,
light-dark(rgba(162, 210, 255, 0.3), rgba(20, 184, 166, 0.15)) 0px,
transparent 50%
),
radial-gradient(
at 0% 100%,
light-dark(rgba(255, 200, 221, 0.3), rgba(99, 102, 241, 0.15)) 0px,
transparent 50%
),
linear-gradient(
120deg,
light-dark(#f0f4f8, #0f172a) 0%,
light-dark(#e2e8f0, #1e293b) 100%
)`,
placeholder: "Help me test my English.",
loadingText: [
"A2UI Agent is working on your request...",
"Analyzing your information...",
"Working...",
],
serverUrl: "http://localhost:10002",
};

View File

@@ -0,0 +1,41 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { v0_8 } from "@a2ui/lit";
/**
* Configuration interface for the Universal App Shell.
*/
export interface AppConfig {
/** Unique key for the app (e.g., 'restaurant', 'contacts') */
key: string;
/** Display title of the application */
title: string;
/** The background for the page */
background?: string;
/** Path to the hero image */
heroImage?: string;
/** Path to the hero image */
heroImageDark?: string;
/** Placeholder text for the input field */
placeholder: string;
/** Text to display while loading (optional). Can be a single string or an array of strings to rotate. */
loadingText?: string | string[];
/** Optional server URL for the agent (e.g., http://localhost:10003) */
serverUrl?: string;
/** Theme overrides (CSS Variables) */
theme?: v0_8.Types.Theme;
}

View File

@@ -0,0 +1,35 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { HTMLTemplateResult } from "lit";
const eventInit = {
bubbles: true,
cancelable: true,
composed: true,
};
export class SnackbarActionEvent extends Event {
static eventName = "snackbaraction";
constructor(
public readonly action: string,
public readonly value?: HTMLTemplateResult | string,
public readonly callback?: () => void
) {
super(SnackbarActionEvent.eventName, { ...eventInit });
}
}

View File

@@ -0,0 +1,246 @@
<!DOCTYPE html>
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Loading...</title>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=account_circle,add,arrow_back,arrow_drop_down,arrow_forward,attach_file,calendar_today,call,camera,check,check_circle,close,communication,content_copy,dark_mode,delete,download,draw,edit,error,event,favorite,favorite_off,folder,help,home,info,light_mode,location_on,lock,lock_open,mail,menu,mobile_layout,more_horiz,more_vert,notifications,notifications_off,payment,pen_size_1,person,phone,photo,print,progress_activity,rectangle,refresh,search,send,settings,share,shopping_cart,star,star_half,star_off,upload,visibility,visibility_off,warning"
/>
<link
href="https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap"
rel="stylesheet"
/>
<style>
:root {
--n-100: #ffffff;
--n-99: #fcfcfc;
--n-98: #f9f9f9;
--n-95: #f1f1f1;
--n-90: #e2e2e2;
--n-80: #c6c6c6;
--n-70: #ababab;
--n-60: #919191;
--n-50: #777777;
--n-40: #5e5e5e;
--n-35: #525252;
--n-30: #474747;
--n-25: #3b3b3b;
--n-20: #303030;
--n-15: #262626;
--n-10: #1b1b1b;
--n-5: #111111;
--n-0: #000000;
--p-100: #ffffff;
--p-99: #fffbff;
--p-98: #fcf8ff;
--p-95: #f2efff;
--p-90: #e1e0ff;
--p-80: #c0c1ff;
--p-70: #a0a3ff;
--p-60: #8487ea;
--p-50: #6a6dcd;
--p-40: #5154b3;
--p-35: #4447a6;
--p-30: #383b99;
--p-25: #2c2e8d;
--p-20: #202182;
--p-15: #131178;
--p-10: #06006c;
--p-5: #03004d;
--p-0: #000000;
--s-100: #ffffff;
--s-99: #fffbff;
--s-98: #fcf8ff;
--s-95: #f2efff;
--s-90: #e2e0f9;
--s-80: #c6c4dd;
--s-70: #aaa9c1;
--s-60: #8f8fa5;
--s-50: #75758b;
--s-40: #5d5c72;
--s-35: #515165;
--s-30: #454559;
--s-25: #393a4d;
--s-20: #2e2f42;
--s-15: #242437;
--s-10: #191a2c;
--s-5: #0f0f21;
--s-0: #000000;
--t-100: #ffffff;
--t-99: #fffbff;
--t-98: #fff8f9;
--t-95: #ffecf4;
--t-90: #ffd8ec;
--t-80: #e9b9d3;
--t-70: #cc9eb8;
--t-60: #af849d;
--t-50: #946b83;
--t-40: #79536a;
--t-35: #6c475d;
--t-30: #5f3c51;
--t-25: #523146;
--t-20: #46263a;
--t-15: #3a1b2f;
--t-10: #2e1125;
--t-5: #22071a;
--t-0: #000000;
--nv-100: #ffffff;
--nv-99: #fffbff;
--nv-98: #fcf8ff;
--nv-95: #f2effa;
--nv-90: #e4e1ec;
--nv-80: #c8c5d0;
--nv-70: #acaab4;
--nv-60: #918f9a;
--nv-50: #777680;
--nv-40: #5e5d67;
--nv-35: #52515b;
--nv-30: #46464f;
--nv-25: #3b3b43;
--nv-20: #303038;
--nv-15: #25252d;
--nv-10: #1b1b23;
--nv-5: #101018;
--nv-0: #000000;
--e-100: #ffffff;
--e-99: #fffbff;
--e-98: #fff8f7;
--e-95: #ffedea;
--e-90: #ffdad6;
--e-80: #ffb4ab;
--e-70: #ff897d;
--e-60: #ff5449;
--e-50: #de3730;
--e-40: #ba1a1a;
--e-35: #a80710;
--e-30: #93000a;
--e-25: #7e0007;
--e-20: #690005;
--e-15: #540003;
--e-10: #410002;
--e-5: #2d0001;
--e-0: #000000;
--primary: #137fec;
--text-color: #fff;
--background: light-dark(#f6f7f8, #101922);
--border-color: oklch(
from var(--background-light) l c h / calc(alpha * 0.15)
);
--elevated-background-light: oklch(
from var(--background-light) l c h / calc(alpha * 0.05)
);
--bb-grid-size: 4px;
--bb-grid-size-2: calc(var(--bb-grid-size) * 2);
--bb-grid-size-3: calc(var(--bb-grid-size) * 3);
--bb-grid-size-4: calc(var(--bb-grid-size) * 4);
--bb-grid-size-5: calc(var(--bb-grid-size) * 5);
--bb-grid-size-6: calc(var(--bb-grid-size) * 6);
--bb-grid-size-7: calc(var(--bb-grid-size) * 7);
--bb-grid-size-8: calc(var(--bb-grid-size) * 8);
--bb-grid-size-9: calc(var(--bb-grid-size) * 9);
--bb-grid-size-10: calc(var(--bb-grid-size) * 10);
--bb-grid-size-11: calc(var(--bb-grid-size) * 11);
--bb-grid-size-12: calc(var(--bb-grid-size) * 12);
--bb-grid-size-13: calc(var(--bb-grid-size) * 13);
--bb-grid-size-14: calc(var(--bb-grid-size) * 14);
--bb-grid-size-15: calc(var(--bb-grid-size) * 15);
--bb-grid-size-16: calc(var(--bb-grid-size) * 16);
--background: radial-gradient(
at 0% 0%,
light-dark(rgba(161, 196, 253, 0.3), rgba(6, 182, 212, 0.15)) 0px,
transparent 50%
),
radial-gradient(
at 100% 0%,
light-dark(rgba(255, 226, 226, 0.3), rgba(59, 130, 246, 0.15)) 0px,
transparent 50%
),
radial-gradient(
at 100% 100%,
light-dark(rgba(162, 210, 255, 0.3), rgba(20, 184, 166, 0.15)) 0px,
transparent 50%
),
radial-gradient(
at 0% 100%,
light-dark(rgba(255, 200, 221, 0.3), rgba(99, 102, 241, 0.15)) 0px,
transparent 50%
),
linear-gradient(
120deg,
light-dark(#f0f4f8, #0f172a) 0%,
light-dark(#e2e8f0, #1e293b) 100%
);
}
* {
box-sizing: border-box;
}
html,
body {
--font-family: "Outfit", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-family-flex: "Outfit", "Helvetica Neue", Helvetica, Arial,
sans-serif;
--font-family-mono: monospace;
background: var(--background);
font-family: var(--font-family);
margin: 0;
padding: 0;
width: 100svw;
height: 100svh;
overflow: auto;
--color-scheme: light;
color-scheme: var(--color-scheme);
&.light {
--color-scheme: light;
}
&.dark {
--color-scheme: dark;
}
}
@media (prefers-color-scheme: dark) {
html,
body {
--color-scheme: dark;
}
}
</style>
</head>
<body>
<a2ui-shell></a2ui-shell>
<script type="module" src="./app.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,153 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IncomingMessage, ServerResponse } from "http";
import { Plugin, ViteDevServer } from "vite";
import { A2AClient } from "@a2a-js/sdk/client";
import {
MessageSendParams,
Part,
SendMessageSuccessResponse,
Task,
} from "@a2a-js/sdk";
import { v4 as uuidv4 } from "uuid";
const A2AUI_MIME_TYPE = "application/json+a2aui";
const fetchWithCustomHeader: typeof fetch = async (url, init) => {
const headers = new Headers(init?.headers);
headers.set("X-A2A-Extensions", "https://a2ui.org/a2a-extension/a2ui/v0.8");
const newInit = { ...init, headers };
return fetch(url, newInit);
};
const isJson = (str: string) => {
try {
const parsed = JSON.parse(str);
return (
typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
);
} catch (err) {
console.warn(err);
return false;
}
};
let client: A2AClient | null = null;
const createOrGetClient = async () => {
if (!client) {
// Create a client pointing to the agent's Agent Card URL.
client = await A2AClient.fromCardUrl(
"http://localhost:10002/.well-known/agent-card.json",
{ fetchImpl: fetchWithCustomHeader }
);
}
return client;
};
export const plugin = (): Plugin => {
return {
name: "a2a-handler",
configureServer(server: ViteDevServer) {
server.middlewares.use(
"/a2a",
async (req: IncomingMessage, res: ServerResponse, next: () => void) => {
if (req.method === "POST") {
let originalBody = "";
req.on("data", (chunk) => {
originalBody += chunk.toString();
});
req.on("end", async () => {
let sendParams: MessageSendParams;
if (isJson(originalBody)) {
console.log(
"[a2a-middleware] Received JSON UI event:",
originalBody
);
const clientEvent = JSON.parse(originalBody);
sendParams = {
message: {
messageId: uuidv4(),
role: "user",
parts: [
{
kind: "data",
data: clientEvent,
metadata: { 'mimeType': A2AUI_MIME_TYPE },
} as Part,
],
kind: "message",
},
};
} else {
console.log(
"[a2a-middleware] Received text query:",
originalBody
);
sendParams = {
message: {
messageId: uuidv4(),
role: "user",
parts: [
{
kind: "text",
text: originalBody,
},
],
kind: "message",
},
};
}
const client = await createOrGetClient();
const response = await client.sendMessage(sendParams);
if ("error" in response) {
console.error("Error:", response.error.message);
res.statusCode = 500;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ error: response.error.message }));
return;
} else {
const result = (response as SendMessageSuccessResponse)
.result as Task;
if (result.kind === "task") {
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(result.status.message?.parts));
return;
}
}
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify([]));
});
return;
} else {
next();
}
}
);
},
};
};

View File

@@ -0,0 +1,17 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export * as A2AMiddleware from "./a2a.js";

View File

@@ -0,0 +1,86 @@
{
"name": "@a2ui/shell",
"private": true,
"version": "0.8.1",
"description": "A2UI Universal Shell",
"main": "./dist/shell.js",
"types": "./dist/shell.d.ts",
"type": "module",
"scripts": {
"prepack": "npm run build",
"build": "wireit",
"build:tsc": "wireit",
"dev": "npm run serve --watch",
"test": "wireit",
"serve": "wireit"
},
"wireit": {
"serve": {
"command": "vite dev",
"dependencies": [
"build"
],
"service": true
},
"test": {
"command": "node --test --enable-source-maps --test-reporter spec dist/src/0.8/tests/**/*.test.js",
"dependencies": [
"build"
]
},
"build": {
"dependencies": [
"build:tsc"
]
},
"build:tsc": {
"command": "tsc -b --pretty",
"env": {
"FORCE_COLOR": "1"
},
"dependencies": [
"../../../../renderers/lit:build:tsc"
],
"files": [
"**/*.ts",
"tsconfig.json"
],
"output": [
"dist/",
"!dist/**/*.min.js{,.map}"
],
"clean": "if-file-deleted"
}
},
"repository": {
"directory": "samples/client/lit/shell",
"type": "git",
"url": "git+https://github.com/google/A2UI.git"
},
"files": [
"dist"
],
"keywords": [],
"author": "Google",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/google/A2UI/issues"
},
"homepage": "https://github.com/google/A2UI/tree/main/web#readme",
"devDependencies": {
"dotenv": "^17.2.3",
"typescript": "^5.8.3",
"uuid": "^13.0.0",
"vite": "^7.1.11",
"wireit": "^0.15.0-pre.2"
},
"dependencies": {
"@a2a-js/sdk": "^0.3.4",
"@a2ui/lit": "file:../../../../renderers/lit",
"@google/genai": "^1.22.0",
"@lit-labs/signals": "^0.1.3",
"@lit/context": "^1.1.4",
"@types/node": "^24.7.1",
"lit": "^3.3.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

View File

@@ -0,0 +1,22 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { theme } from "./default-theme.js";
import { v0_8 } from "@a2ui/lit";
export function cloneDefaultTheme(): v0_8.Types.Theme {
return structuredClone(theme);
}

View File

@@ -0,0 +1,442 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { v0_8 } from "@a2ui/lit";
/** Elements */
const a = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-as-n": true,
"layout-dis-iflx": true,
"layout-al-c": true,
"typography-td-none": true,
"color-c-p40": true,
};
const audio = {
"layout-w-100": true,
};
const body = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-mt-0": true,
"layout-mb-2": true,
"typography-sz-bm": true,
"color-c-n10": true,
};
const button = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-pt-3": true,
"layout-pb-3": true,
"layout-pl-5": true,
"layout-pr-5": true,
"layout-mb-1": true,
"border-br-16": true,
"border-bw-0": true,
"border-c-n70": true,
"border-bs-s": true,
"color-bgc-s30": true,
"behavior-ho-80": true,
};
const heading = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-500": true,
"layout-mt-0": true,
"layout-mb-2": true,
};
const iframe = {
"behavior-sw-n": true,
};
const input = {
"typography-f-sf": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-pl-4": true,
"layout-pr-4": true,
"layout-pt-2": true,
"layout-pb-2": true,
"border-br-6": true,
"border-bw-1": true,
"color-bc-s70": true,
"border-bs-s": true,
"layout-as-n": true,
"color-c-n10": true,
};
const p = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
"color-c-n10": true,
};
const orderedList = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
"color-c-n10": true,
};
const unorderedList = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
"color-c-n10": true,
};
const listItem = {
"typography-f-s": true,
"typography-fs-n": true,
"typography-w-400": true,
"layout-m-0": true,
"typography-sz-bm": true,
"layout-as-n": true,
"color-c-n10": true,
};
const pre = {
"typography-f-c": true,
"typography-fs-n": true,
"typography-w-400": true,
"typography-sz-bm": true,
"typography-ws-p": true,
"layout-as-n": true,
};
const textarea = {
...input,
"layout-r-none": true,
"layout-fs-c": true,
};
const video = {
"layout-el-cv": true,
};
const aLight = v0_8.Styles.merge(a, {});
const inputLight = v0_8.Styles.merge(input, {});
const textareaLight = v0_8.Styles.merge(textarea, {});
const buttonLight = v0_8.Styles.merge(button, {});
const bodyLight = v0_8.Styles.merge(body, {});
const pLight = v0_8.Styles.merge(p, {});
const preLight = v0_8.Styles.merge(pre, {});
const orderedListLight = v0_8.Styles.merge(orderedList, {});
const unorderedListLight = v0_8.Styles.merge(unorderedList, {});
const listItemLight = v0_8.Styles.merge(listItem, {});
export const theme: v0_8.Types.Theme = {
additionalStyles: {
Button: {
"--n-35": "var(--n-100)",
"--n-10": "var(--n-0)",
background:
"linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)",
boxShadow: "0 4px 15px rgba(102, 126, 234, 0.4)",
padding: "12px 28px",
textTransform: "uppercase",
},
Text: {
h1: {
color: "transparent",
background:
"linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)",
"-webkit-background-clip": "text",
"background-clip": "text",
"-webkit-text-fill-color": "transparent",
},
h2: {
color: "transparent",
background:
"linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)",
"-webkit-background-clip": "text",
"background-clip": "text",
"-webkit-text-fill-color": "transparent",
},
h3: {
color: "transparent",
background:
"linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)",
"-webkit-background-clip": "text",
"background-clip": "text",
"-webkit-text-fill-color": "transparent",
},
h4: {},
h5: {},
body: {},
caption: {},
},
Card: {
background:
"radial-gradient(circle at top left, light-dark(transparent, rgba(6, 182, 212, 0.15)), transparent 40%), radial-gradient(circle at bottom right, light-dark(transparent, rgba(139, 92, 246, 0.15)), transparent 40%), linear-gradient(135deg, light-dark(rgba(255, 255, 255, 0.7), rgba(30, 41, 59, 0.7)), light-dark(rgba(255, 255, 255, 0.7), rgba(15, 23, 42, 0.8)))",
},
TextField: {
"--p-0": "light-dark(var(--n-0), #1e293b)",
},
},
components: {
AudioPlayer: {},
Button: {
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-3": true,
"layout-pr-3": true,
"border-br-12": true,
"border-bw-0": true,
"border-bs-s": true,
"color-bgc-p30": true,
"behavior-ho-70": true,
"typography-w-400": true,
},
Card: { "border-br-9": true, "layout-p-4": true, "color-bgc-n100": true },
CheckBox: {
element: {
"layout-m-0": true,
"layout-mr-2": true,
"layout-p-2": true,
"border-br-12": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bgc-p100": true,
"color-bc-p60": true,
"color-c-n30": true,
"color-c-p30": true,
},
label: {
"color-c-p30": true,
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-flx-1": true,
"typography-sz-ll": true,
},
container: {
"layout-dsp-iflex": true,
"layout-al-c": true,
},
},
Column: {
"layout-g-2": true,
},
DateTimeInput: {
container: {
"typography-sz-bm": true,
"layout-w-100": true,
"layout-g-2": true,
"layout-dsp-flexhor": true,
"layout-al-c": true,
"typography-ws-nw": true,
},
label: {
"color-c-p30": true,
"typography-sz-bm": true,
},
element: {
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-3": true,
"layout-pr-3": true,
"border-br-2": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bgc-p100": true,
"color-bc-p60": true,
"color-c-n30": true,
"color-c-p30": true,
},
},
Divider: {},
Image: {
all: {
"border-br-5": true,
"layout-el-cv": true,
"layout-w-100": true,
"layout-h-100": true,
},
avatar: { "is-avatar": true },
header: {},
icon: {},
largeFeature: {},
mediumFeature: {},
smallFeature: {},
},
Icon: {},
List: {
"layout-g-4": true,
"layout-p-2": true,
},
Modal: {
backdrop: { "color-bbgc-p60_20": true },
element: {
"border-br-2": true,
"color-bgc-p100": true,
"layout-p-4": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bc-p80": true,
},
},
MultipleChoice: {
container: {},
label: {},
element: {},
},
Row: {
"layout-g-4": true,
},
Slider: {
container: {},
label: {},
element: {},
},
Tabs: {
container: {},
controls: { all: {}, selected: {} },
element: {},
},
Text: {
all: {
"layout-w-100": true,
"layout-g-2": true,
},
h1: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-m-0": true,
"layout-p-0": true,
"typography-sz-hs": true,
},
h2: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-m-0": true,
"layout-p-0": true,
"typography-sz-tl": true,
},
h3: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-m-0": true,
"layout-p-0": true,
"typography-sz-tl": true,
},
h4: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-m-0": true,
"layout-p-0": true,
"typography-sz-bl": true,
},
h5: {
"typography-f-sf": true,
"typography-v-r": true,
"typography-w-400": true,
"layout-m-0": true,
"layout-p-0": true,
"typography-sz-bm": true,
},
body: {},
caption: {},
},
TextField: {
container: {
"typography-sz-bm": true,
"layout-w-100": true,
"layout-g-2": true,
"layout-dsp-flexhor": true,
"layout-al-c": true,
"typography-ws-nw": true,
},
label: {
"layout-flx-0": true,
"color-c-p30": true,
},
element: {
"typography-sz-bm": true,
"layout-pt-2": true,
"layout-pb-2": true,
"layout-pl-3": true,
"layout-pr-3": true,
"border-br-2": true,
"border-bw-1": true,
"border-bs-s": true,
"color-bgc-p100": true,
"color-bc-p60": true,
"color-c-n30": true,
"color-c-p30": true,
},
},
Video: {
"border-br-5": true,
"layout-el-cv": true,
},
},
elements: {
a: aLight,
audio,
body: bodyLight,
button: buttonLight,
h1: heading,
h2: heading,
h3: heading,
h4: heading,
h5: heading,
iframe,
input: inputLight,
p: pLight,
pre: preLight,
textarea: textareaLight,
video,
},
markdown: {
p: [...Object.keys(pLight)],
h1: [...Object.keys(heading)],
h2: [...Object.keys(heading)],
h3: [...Object.keys(heading)],
h4: [...Object.keys(heading)],
h5: [...Object.keys(heading)],
ul: [...Object.keys(unorderedListLight)],
ol: [...Object.keys(orderedListLight)],
li: [...Object.keys(listItemLight)],
a: [...Object.keys(aLight)],
strong: [],
em: [],
},
};

View File

@@ -0,0 +1,30 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"incremental": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"preserveWatchOutput": true,
"sourceMap": true,
"target": "es2022",
"module": "es2022",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"useDefineForClassFields": false,
"rootDir": ".",
"outDir": "dist",
"tsBuildInfoFile": "dist/.tsbuildinfo",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"references": [{ "path": "../../../../renderers/lit" }]
}

View File

@@ -0,0 +1,42 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { HTMLTemplateResult } from "lit";
export enum SnackType {
NONE = "none",
INFORMATION = "information",
WARNING = "warning",
ERROR = "error",
PENDING = "pending",
}
export type SnackbarUUID = ReturnType<typeof globalThis.crypto.randomUUID>;
export type SnackbarAction = {
title: string;
action: string;
value?: HTMLTemplateResult | string;
callback?: () => void;
};
export type SnackbarMessage = {
id: SnackbarUUID;
type: SnackType;
persistent: boolean;
message: string | HTMLTemplateResult;
actions?: SnackbarAction[];
};

View File

@@ -0,0 +1,299 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { LitElement, html, css, nothing, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import { SnackbarMessage, SnackbarUUID, SnackType } from "../types/types";
import { repeat } from "lit/directives/repeat.js";
import { SnackbarActionEvent } from "../events/events";
import { classMap } from "lit/directives/class-map.js";
import { v0_8 } from "@a2ui/lit";
const DEFAULT_TIMEOUT = 8000;
@customElement("ui-snackbar")
export class Snackbar extends LitElement {
@property({ reflect: true, type: Boolean })
accessor active = false;
@property({ reflect: true, type: Boolean })
accessor error = false;
@property()
accessor timeout = DEFAULT_TIMEOUT;
#messages: SnackbarMessage[] = [];
#timeout = 0;
static styles = [
unsafeCSS(v0_8.Styles.structuralStyles),
css`
:host {
--text-color: var(--n-0);
--bb-body-medium: 16px;
--bb-body-line-height-medium: 24px;
display: flex;
align-items: center;
position: fixed;
bottom: var(--bb-grid-size-7);
left: 50%;
translate: -50% 0;
opacity: 0;
pointer-events: none;
border-radius: var(--bb-grid-size-2);
background: var(--n-90);
padding: var(--bb-grid-size-3) var(--bb-grid-size-6);
width: 60svw;
max-width: 720px;
z-index: 1800;
scrollbar-width: none;
overflow-x: scroll;
font: 400 var(--bb-body-medium) / var(--bb-body-line-height-medium)
var(--bb-font-family);
}
:host([active]) {
transition: opacity 0.3s cubic-bezier(0, 0, 0.3, 1) 0.2s;
opacity: 1;
pointer-events: auto;
}
:host([error]) {
background: var(--e-90);
--text-color: var(--e-40);
}
.g-icon {
flex: 0 0 auto;
color: var(--text-color);
margin-right: var(--bb-grid-size-4);
&.rotate {
animation: 1s linear 0s infinite normal forwards running rotate;
}
}
#messages {
color: var(--text-color);
flex: 1 1 auto;
margin-right: var(--bb-grid-size-11);
a,
a:visited {
color: var(--bb-ui-600);
text-decoration: none;
&:hover {
color: var(--bb-ui-500);
text-decoration: underline;
}
}
}
#actions {
flex: 0 1 auto;
width: fit-content;
margin-right: var(--bb-grid-size-3);
& button {
font: 500 var(--bb-body-medium) / var(--bb-body-line-height-medium)
var(--bb-font-family);
padding: 0;
background: transparent;
border: none;
margin: 0 var(--bb-grid-size-4);
color: var(--text-color);
opacity: 0.7;
transition: opacity 0.2s cubic-bezier(0, 0, 0.3, 1);
&:not([disabled]) {
cursor: pointer;
&:hover,
&:focus {
opacity: 1;
}
}
}
}
#close {
display: flex;
align-items: center;
padding: 0;
color: var(--text-color);
background: transparent;
border: none;
margin: 0 0 0 var(--bb-grid-size-2);
opacity: 0.7;
transition: opacity 0.2s cubic-bezier(0, 0, 0.3, 1);
.g-icon {
margin-right: 0;
}
&:not([disabled]) {
cursor: pointer;
&:hover,
&:focus {
opacity: 1;
}
}
}
@keyframes rotate {
from {
rotate: 0deg;
}
to {
rotate: 360deg;
}
}
`,
];
show(message: SnackbarMessage, replaceAll = false) {
const existingMessage = this.#messages.findIndex(
(msg) => msg.id === message.id
);
if (existingMessage === -1) {
if (replaceAll) {
this.#messages.length = 0;
}
this.#messages.push(message);
} else {
this.#messages[existingMessage] = message;
}
window.clearTimeout(this.#timeout);
if (!this.#messages.every((msg) => msg.persistent)) {
this.#timeout = window.setTimeout(() => {
this.hide();
}, this.timeout);
}
this.error = this.#messages.some((msg) => msg.type === SnackType.ERROR);
this.active = true;
this.requestUpdate();
return message.id;
}
hide(id?: SnackbarUUID) {
if (id) {
const idx = this.#messages.findIndex((msg) => msg.id === id);
if (idx !== -1) {
this.#messages.splice(idx, 1);
}
} else {
this.#messages.length = 0;
}
this.active = this.#messages.length !== 0;
this.updateComplete.then((avoidedUpdate) => {
if (!avoidedUpdate) {
return;
}
this.requestUpdate();
});
}
render() {
let rotate = false;
let icon = "";
for (let i = this.#messages.length - 1; i >= 0; i--) {
if (
!this.#messages[i].type ||
this.#messages[i].type === SnackType.NONE
) {
continue;
}
icon = this.#messages[i].type;
if (this.#messages[i].type === SnackType.PENDING) {
icon = "progress_activity";
rotate = true;
}
break;
}
return html` ${icon
? html`<span
class=${classMap({
"g-icon": true,
round: true,
filled: true,
rotate,
})}
>${icon}</span
>`
: nothing}
<div id="messages">
${repeat(
this.#messages,
(message) => message.id,
(message) => {
return html`<div>${message.message}</div>`;
}
)}
</div>
<div id="actions">
${repeat(
this.#messages,
(message) => message.id,
(message) => {
if (!message.actions) {
return nothing;
}
return html`${repeat(
message.actions,
(action) => action.value,
(action) => {
return html`<button
@click=${() => {
this.hide();
this.dispatchEvent(
new SnackbarActionEvent(
action.action,
action.value,
action.callback
)
);
}}
>
${action.title}
</button>`;
}
)}`;
}
)}
</div>
<button
id="close"
@click=${() => {
this.hide();
this.dispatchEvent(new SnackbarActionEvent("dismiss"));
}}
>
<span class="g-icon">close</span>
</button>`;
}
}

View File

@@ -0,0 +1,17 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export { Snackbar } from "./snackbar";

View File

@@ -0,0 +1,45 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { config } from "dotenv";
import { UserConfig } from "vite";
import * as Middleware from "./middleware";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
export default async () => {
config();
const entry: Record<string, string> = {
shell: resolve(__dirname, "index.html"),
};
return {
plugins: [Middleware.A2AMiddleware.plugin()],
build: {
rollupOptions: {
input: entry,
},
target: "esnext",
},
define: {},
resolve: {
dedupe: ["lit"],
},
} satisfies UserConfig;
};

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
"""Main entry point for running the restaurant finder A2UI server."""
import logging
import os
import uvicorn
from starlette.staticfiles import StaticFiles
from setup_a2ui_server import app
if __name__ == "__main__":
# Get the directory where this script is located
script_dir = os.path.dirname(os.path.abspath(__file__))
images_dir = os.path.join(script_dir, "images")
# Mount static files if images directory exists
if os.path.exists(images_dir):
app.mount("/static", StaticFiles(directory=images_dir), name="static")
logging.info(
"Mounted static files from %s at /static",
images_dir,
)
else:
logging.warning(
"Images directory not found at %s, "
"static files will not be served",
images_dir,
)
# Run the app manually with uvicorn
uvicorn.run(
app,
host="0.0.0.0",
port=10002,
log_level="info",
access_log=True,
)

View File

@@ -0,0 +1,319 @@
# -*- coding: utf-8 -*-
"""Utility functions for A2UI agent integration."""
import json
from typing import Any
from pydantic import BaseModel
from pydantic import Field
from a2a.types import (
DataPart,
TextPart,
Message,
Part,
)
from a2ui.extension.a2ui_extension import (
A2UI_MIME_TYPE,
MIME_TYPE_KEY,
A2UI_EXTENSION_URI,
)
from agentscope._logging import logger
class A2UIResponse(BaseModel):
"""Response model for A2UI formatted output."""
response_with_a2ui: str = Field(
description="The response with A2UI JSON",
)
def check_a2ui_extension(*args: Any) -> bool:
"""Check if a2ui extension is requested in the request context.
Args:
*args: Variable arguments that may contain ServerCallContext as the
first element.
Returns:
True if a2ui extension is requested and activated, False otherwise.
"""
# Extract context from args (ServerCallContext is typically the first
# element)
if not args or len(args) == 0:
logger.warning("check_a2ui_extension: No context provided in args")
return False
context = args[0]
# Check if context has requested_extensions attribute
if not hasattr(context, "requested_extensions"):
logger.warning(
"check_a2ui_extension: Context does not have "
"requested_extensions attribute",
)
return False
# Check if A2UI extension is requested
if A2UI_EXTENSION_URI in context.requested_extensions:
# Activate the extension if add_activated_extension method exists
if hasattr(context, "add_activated_extension"):
context.add_activated_extension(A2UI_EXTENSION_URI)
logger.info("A2UI extension activated: %s", A2UI_EXTENSION_URI)
else:
logger.warning(
"check_a2ui_extension: Context does not have "
"add_activated_extension method",
)
return True
return False
def transfer_ui_event_to_query(ui_event_part: dict) -> str:
"""Transfer UI event to a query string.
Args:
ui_event_part: A dictionary containing UI event information with
actionName and context.
Returns:
A formatted query string based on the UI event action.
"""
action = ui_event_part.get("actionName")
ctx = ui_event_part.get("context", {})
if action in ["book_restaurant", "select_item"]:
restaurant_name = ctx.get("restaurantName", "Unknown Restaurant")
address = ctx.get("address", "Address not provided")
image_url = ctx.get("imageUrl", "")
query = (
f"USER_WANTS_TO_BOOK: {restaurant_name}, "
f"Address: {address}, ImageURL: {image_url}"
)
elif action == "submit_booking":
restaurant_name = ctx.get("restaurantName", "Unknown Restaurant")
party_size = ctx.get("partySize", "Unknown Size")
reservation_time = ctx.get("reservationTime", "Unknown Time")
dietary_reqs = ctx.get("dietary", "None")
image_url = ctx.get("imageUrl", "")
query = (
f"User submitted a booking for {restaurant_name} "
f"for {party_size} people at {reservation_time} "
f"with dietary requirements: {dietary_reqs}. "
f"The image URL is {image_url}"
)
else:
# Note: The A2UI original example uses `ctx` as the data source.
# However, in generated UI components, the `ctx` field may be empty
# when the databinding path cannot be resolved. To ensure we capture
# all available event data, we use the entire `ui_event_part` instead.
query = f"User submitted an event: {action} with data: {ui_event_part}"
return query
def pre_process_request_with_ui_event(message: Message) -> Any:
"""Pre-process the request.
Args:
message: The agent request object.
Returns:
The pre-processed request.
"""
if message and message.parts:
logger.info(
"--- AGENT_EXECUTOR: Processing %s message parts ---",
len(message.parts),
)
for i, part in enumerate(message.parts):
if isinstance(part.root, DataPart):
if "userAction" in part.root.data:
logger.info(
" Part %s: Found a2ui UI ClientEvent payload: %s",
i,
json.dumps(part.root.data["userAction"], indent=4),
)
ui_event_part = part.root.data["userAction"]
message.parts[i] = Part(
root=TextPart(
text=transfer_ui_event_to_query(ui_event_part),
),
)
return message
def _find_json_end(json_string: str) -> int:
"""Find the end position of a JSON array or object.
Finds the end by matching brackets/braces.
Args:
json_string: The JSON string to search.
Returns:
The end position (index + 1) of the JSON structure.
"""
if json_string.startswith("["):
# Find matching closing bracket
bracket_count = 0
for i, char in enumerate(json_string):
if char == "[":
bracket_count += 1
elif char == "]":
bracket_count -= 1
if bracket_count == 0:
return i + 1
elif json_string.startswith("{"):
# Find matching closing brace
brace_count = 0
for i, char in enumerate(json_string):
if char == "{":
brace_count += 1
elif char == "}":
brace_count -= 1
if brace_count == 0:
return i + 1
return len(json_string)
def extract_ui_json_from_text(content_str: str) -> tuple[str, None]:
"""Extract the UI JSON from the text.
Args:
text: The text to extract the UI JSON from.
Returns:
The UI JSON.
"""
text_content, json_string = content_str.split("---a2ui_JSON---", 1)
json_data = None
if json_string.strip():
try:
# Clean JSON string (remove markdown code blocks if present)
json_string_cleaned = (
json_string.strip().lstrip("```json").rstrip("```").strip()
)
# Find the end of JSON array/object by matching brackets/braces
json_end = _find_json_end(json_string_cleaned)
json_string_final = json_string_cleaned[:json_end].strip()
json_data = json.loads(json_string_final)
except json.JSONDecodeError as e:
logger.error("Failed to parse UI JSON: %s", e)
# On error, keep the JSON as text content
return content_str, None
return text_content, json_data
def check_a2ui_json_in_message(
part: Part,
is_final: bool,
) -> tuple[bool, str | None]:
"""Check if the message contains A2UI JSON.
Args:
message: The message to check.
Returns:
A tuple containing a boolean indicating if A2UI JSON is found and
the A2UI JSON string if found.
"""
# for the case when ReActAgent max iters is reached, the message will be
# the last complete message, with text message.
if (
isinstance(part.root, TextPart)
and "---a2ui_JSON---" in part.root.text
and is_final
):
logger.info(
"--- Found A2UI JSON in the message: %s ---",
part.root.text,
)
return True, part.root.text
# for the case when ReActAgent max iters is not reached, if it contains
# tool use block with name "generate_response" and type "tool_use", and
# the response_with_a2ui contains "---a2ui_JSON---", then return True,
# response_with_a2ui.
if (
isinstance(part.root, DataPart)
and part.root.data.get("name") == "generate_response"
and part.root.data.get("type") == "tool_use"
and not is_final
):
input_data = part.root.data.get("input")
if input_data and isinstance(input_data, dict):
response_with_a2ui = input_data.get("response_with_a2ui")
if response_with_a2ui and "---a2ui_JSON---" in response_with_a2ui:
return True, response_with_a2ui
return False, None
def post_process_a2a_message_for_ui(
message: Message,
) -> Message:
"""Post-process the transferred A2A message.
Args:
message: The transferred A2A message.
Returns:
The post-processed A2A message.
"""
new_parts = []
# pylint: disable=too-many-nested-blocks
for part in message.parts:
# Check if it's a text block and contains the A2UI JSON marker
if isinstance(part.root, TextPart):
text_content_str = part.root.text
if "---a2ui_JSON---" in text_content_str:
# Extract and process A2UI JSON
text_content, json_data = extract_ui_json_from_text(
text_content_str,
)
if json_data:
# Replace the part with a TextPart and multiple DataParts
# with the same metadata for a2ui
try:
new_parts.append(
Part(
root=TextPart(
text=text_content,
),
),
)
for item in json_data:
new_parts.append(
Part(
root=DataPart(
data=item,
metadata={
MIME_TYPE_KEY: A2UI_MIME_TYPE,
},
),
),
)
except Exception as e:
logger.error(
"Error processing a2ui JSON parts: %s",
e,
exc_info=True,
)
raise
else:
# If JSON extraction failed, keep the original text block
new_parts.append(part)
else:
# Keep the original text block if it doesn't contain the marker
new_parts.append(part)
else:
# For non-text parts, keep the original logic
new_parts.append(part)
message.parts = new_parts
return message

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
"""The agent card definition for the A2A agent."""
from a2a.types import AgentCard, AgentCapabilities, AgentSkill
from a2ui.extension.a2ui_extension import get_a2ui_agent_extension
agent_card = AgentCard(
name="Friday",
description="A simple ReAct agent that handles input queries",
url="http://localhost:10002",
version="1.0.0",
capabilities=AgentCapabilities(
push_notifications=False,
state_transition_history=True,
streaming=True,
extensions=[get_a2ui_agent_extension()],
),
default_input_modes=["text/plain"],
default_output_modes=["text/plain"],
skills=[
AgentSkill(
name="execute_python_code",
id="execute_python_code",
description="Execute Python code snippets.",
tags=["code_execution"],
),
AgentSkill(
name="execute_shell_command",
id="execute_shell_command",
description="Execute shell commands on the server.",
tags=["code_execution"],
),
AgentSkill(
name="view_text_file",
id="view_text_file",
description="View the content of a text file on the server.",
tags=["file_viewing"],
),
],
)

View File

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
"""Prompt builder for Agent with A2UI support."""
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# flake8: noqa: E501
# pylint: disable=C0301
def get_ui_prompt() -> str:
"""
Constructs the full prompt with UI instructions, rules, examples, and schema.
Args:
Returns:
A formatted string to be used as the system prompt for the LLM.
"""
return """
You are a helpful assistant specialized in generating appropriate A2UI UI JSON responses to display content to users and help them complete their tasks.
To generate the appropriate A2UI UI JSON responses, you MUST follow these rules:
1. **CRITICAL FIRST STEP**: Before generating ANY response with UI JSON, you MUST ensure that you have loaded the schema and examples from the `A2UI_response_generator` skill:
- Read the SKILL.md file from the skill directory using `view_text_file`
- Execute the Python command in the skill directory using `execute_shell_command` to load the schema and examples
- DO NOT assume you know the A2UI format - you MUST load it from the skill
2. When you plan to generate the A2UI JSON response, you MUST output text directly. The text output MUST contain two parts, separated by the delimiter: `---a2ui_JSON---`. The first part is your conversational text response, and the second part is a single, raw JSON object which is a list of A2UI messages.
### CRITICAL REQUIREMENTS:
1. ALL your schema and examples about A2UI MUST come from your equipped skills - do NOT use any prior knowledge.
2. You MUST ONLY use `execute_shell_command` tool to execute the Python command in the skill directory. DO NOT use `execute_python_code` to execute the command.
3. **You MUST directly generate the A2UI JSON based on the task content. DO NOT ask the user about their preference regarding UI type (list, form, confirmation, detail view). You should automatically determine the most appropriate UI type based on the context and generate the response accordingly.**
4. **ALWAYS remember the user's task and objective. Your UI responses should be directly aligned with helping the user accomplish their specific goal. Never lose sight of what the user is trying to achieve.**
5. **WHEN ASKING QUESTIONS**: When you need to ask the user task-related questions, you MUST include `---a2ui_JSON---` followed by the appropriate UI JSON (such as forms, selection cards, or input fields) that allows the user to provide their answer. The question text should come first, then the delimiter, then the UI JSON for collecting the user's response.
6. **WHEN PROVIDING INFORMATION IN TEXT FORMAT**: When you need to provide information to the user in text format, you MUST include `---a2ui_JSON---` followed by the appropriate UI JSON that allows the user to view the information. The information text should come first, then the delimiter, then the UI JSON for displaying the information.
7. **The Most Important Rule**: You MUST always include `---a2ui_JSON---` in your response.
8. **CRITICAL FORMAT REQUIREMENT**: When generating A2UI JSON responses, you MUST output text directly. The text MUST contain two parts separated by `---a2ui_JSON---`: your conversational response first, followed by the A2UI JSON array that best describes the inferface that you want to display to the user.
9. **If you skip using the `A2UI_response_generator` skill, your response will be incorrect and invalid.**
"""

View File

@@ -0,0 +1,27 @@
[project]
name = "general-agent"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"agentscope[a2a]",
"agentscope-runtime",
"uvicorn",
"starlette",
"fastapi",
"shortuuid",
"a2ui-agent"
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["."]
[tool.hatch.metadata]
allow-direct-references = true
[tool.uv.sources]
a2ui-agent = { path = "../../../../../../A2UI/a2a_agents/python/a2ui_agent" }
agentscope = { path = "../../../../../" }

View File

@@ -0,0 +1,300 @@
# -*- coding: utf-8 -*-
"""Set up an A2A server with a ReAct agent to handle the input query"""
import os
import uuid
import copy
from typing import AsyncGenerator, Any
from a2a.server.apps import A2AStarletteApplication
from a2a.server.events import Event
from a2a.types import (
Task,
TaskStatus,
TaskState,
Message,
MessageSendParams,
TaskStatusUpdateEvent,
)
from starlette.middleware.cors import CORSMiddleware
from agent_card import agent_card
from prompt_builder import get_ui_prompt
from a2ui_utils import (
pre_process_request_with_ui_event,
post_process_a2a_message_for_ui,
)
from agentscope._logging import logger
from agentscope.agent import ReActAgent
from agentscope.formatter import DashScopeChatFormatter, A2AChatFormatter
from agentscope.model import DashScopeChatModel
from agentscope.pipeline import stream_printing_messages
from agentscope.session import JSONSession
from agentscope.message import Msg
class SimpleStreamHandler:
"""A simple request handler that handles the input query by a
ReAct agent.
This handler processes A2A protocol messages by using a ReAct agent
to generate responses. It supports both streaming and non-streaming
message handling, and manages session state for conversation continuity.
"""
async def _prepare_final_message(
self,
formatter: A2AChatFormatter,
final_msg: Msg | None,
) -> Message:
"""Prepare the final message for response.
Args:
formatter (`A2AChatFormatter`):
The A2AChatFormatter instance.
final_msg (`Msg | None`, optional):
The final message if available.
Returns:
`Message`:
The prepared final message.
"""
logger.info(
"--- Processing final response, final_msg: %s ---",
final_msg is not None,
)
if final_msg is not None:
logger.info("--- Using final message for final message ---")
final_a2a_message = await formatter.format(
[final_msg],
)
else:
logger.info(
"--- Using last complete message for final message ---",
)
logger.info(
"--- Post-processing message for UI: %s ---",
final_a2a_message,
)
final_a2a_message = post_process_a2a_message_for_ui(
final_a2a_message,
)
return final_a2a_message
async def on_message_send(
self, # pylint: disable=unused-argument
params: MessageSendParams,
*args: Any,
**kwargs: Any,
) -> Task:
"""Handles non-streaming message_send requests by collecting
events from the stream and returning the final Task.
Args:
params (`MessageSendParams`):
The parameters for sending the message.
*args (`Any`):
Additional positional arguments.
**kwargs (`Any`):
Additional keyword arguments.
Returns:
`Task`:
The final Task object.
"""
logger.info("--- params: %s ---", params)
logger.info("args: %s ---", args)
logger.info("kwargs: %s ---", kwargs)
# Collect all events from the stream
final_event = None
task_id = params.message.task_id or uuid.uuid4().hex
context_id = params.message.context_id or "default-context"
async for event in self.on_message_send_stream(
params,
*args,
**kwargs,
):
if event.final:
final_event = event
break
# Ensure we always return a valid Task
if final_event is None:
# If no final event was found, create one with completed state
logger.warning(
"No final event found in stream, "
"creating default completed event",
)
final_event = TaskStatusUpdateEvent(
task_id=task_id,
context_id=context_id,
status=TaskStatus(state=TaskState.failed),
final=True,
)
# Convert TaskStatusUpdateEvent to Task
# A2A protocol expects on_message_send to return a Task,
# not TaskStatusUpdateEvent
return Task(
id=final_event.task_id,
context_id=final_event.context_id,
status=final_event.status,
artifacts=[],
)
async def on_message_send_stream(
self, # pylint: disable=unused-argument
params: MessageSendParams,
*args: Any,
**kwargs: Any,
) -> AsyncGenerator[Event, None]:
"""Handles the message_send method by the agent.
Args:
params (`MessageSendParams`):
The parameters for sending the message.
*args (`Any`):
Additional positional arguments.
**kwargs (`Any`):
Additional keyword arguments.
Returns:
`AsyncGenerator[Event, None]`:
An asynchronous generator that yields task status update
events.
"""
task_id = params.message.task_id or uuid.uuid4().hex
context_id = params.message.context_id or "default-context"
# ============ Agent Logic ============
from agentscope.tool import (
Toolkit,
view_text_file,
execute_python_code,
execute_shell_command,
)
toolkit = Toolkit()
toolkit.register_tool_function(execute_python_code)
toolkit.register_tool_function(execute_shell_command)
toolkit.register_tool_function(view_text_file)
# Get the skill path relative to this file
# From restaurant_finder/ to restaurant_finder/skills/
# A2UI_response_generator
skill_path = os.path.abspath(
os.path.join(
os.path.dirname(__file__),
"skills",
"A2UI_response_generator",
),
)
toolkit.register_agent_skill(skill_path)
# Create the agent instance
agent = ReActAgent(
name="Friday",
sys_prompt=get_ui_prompt(),
model=DashScopeChatModel(
model_name="qwen3-max",
api_key=os.getenv("DASHSCOPE_API_KEY"),
),
formatter=DashScopeChatFormatter(),
toolkit=toolkit,
max_iters=10,
)
logger.info("Agent system prompt: %s", agent.sys_prompt)
session = JSONSession(save_dir="./sessions")
session_id = params.message.task_id or "test-a2ui-agent"
await session.load_session_state(
session_id=session_id,
agent=agent,
)
# pre-process the A2A message with UI event,
# and then convert to AgentScope Msg objects
formatter = A2AChatFormatter()
as_msg = await formatter.format_a2a_message(
name="Friday",
message=pre_process_request_with_ui_event(
params.message,
),
)
yield TaskStatusUpdateEvent(
task_id=task_id,
context_id=context_id,
status=TaskStatus(state=TaskState.working),
final=False,
)
# Collect all messages from the stream
# The 'last' flag indicates the last chunk of a streaming message,
# not the last message from the agent
message_count = 0
final_msg = None
try:
async for msg, last in stream_printing_messages(
agents=[agent],
coroutine_task=agent(as_msg),
):
message_count += 1
if last:
final_msg = copy.deepcopy(msg)
except Exception as e:
logger.error(
"--- Error in message stream: %s ---",
e,
exc_info=True,
)
raise
finally:
logger.info(
"--- Message stream collection completed. "
"Total messages: %s, "
"Last message: %s ---",
message_count,
final_msg,
)
# Save session state (move before final message processing
# to avoid blocking yield)
await session.save_session_state(
session_id=session_id,
agent=agent,
)
final_a2a_message = await self._prepare_final_message(
formatter,
final_msg,
)
logger.info("--- Yielding final TaskStatusUpdateEvent ---")
yield TaskStatusUpdateEvent(
task_id=task_id,
context_id=context_id,
status=TaskStatus(
state=TaskState.input_required,
message=final_a2a_message,
),
final=True,
)
handler = SimpleStreamHandler()
app_instance = A2AStarletteApplication(
agent_card,
handler,
)
app = app_instance.build()
# Add CORS middleware to handle OPTIONS requests
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allow all origins for development
allow_credentials=False, # Cannot use "*" with credentials=True
allow_methods=["*"], # Allow all HTTP methods including OPTIONS
allow_headers=["*"], # Allow all headers
)

View File

@@ -0,0 +1,147 @@
---
name: A2UI_response_generator
description: A skill that can retrieve A2UI UI JSON schematics and UI templates that best show the response. This skill is essential and must be used before generating A2UI (Agent to UI) JSON responses.
---
# A2UI response generation Skill
## Overview
This skill is **essential and must be used before generating A2UI (Agent to UI) JSON responses**. It enables agents to retrieve A2UI UI JSON schematics and UI templates that best show the response, allowing for the generation of rich, interactive UI responses using the A2UI protocol.
Instead of loading the entire A2UI schema and all examples at once, this skill allows agents to **retrieve** only the relevant UI templates and schematics based on the response content. The A2UI protocol defines a JSON-based format for dynamically constructing and updating user interfaces. By breaking down the examples into modular templates, agents can:
1. Retrieve the appropriate A2UI UI JSON schematics for validation and structure reference
2. Select UI templates that best match and display the response content
3. Reduce prompt token usage by loading only necessary templates
4. Easily extend with new UI templates for different domains
### File Structure
```
A2UI_response_generator/
├── SKILL.md # This file - main skill documentation
├── view_a2ui_schema.py # Tool to view the complete A2UI schema (schema included in file)
├── view_a2ui_examples.py # Tool to view UI template examples (templates included in file)
├── __init__.py # Package initialization
├── schema/ # A2UI schema definitions
│ ├── __init__.py
│ └── base_schema.py # Base A2UI schema
└── UI_templete_examples/ # UI template examples
├── __init__.py
├── booking_form.py # Booking form template
├── contact_form.py # Contact form template
├── email_compose_form.py # Email compose form template
├── error_message.py # Error message template
├── info_message.py # Info message template
├── item_detail_card_with_image.py # Item detail card with image template
├── profile_view.py # Profile view template
├── search_filter_form.py # Search filter form template
├── simple_column_list_without_image.py # Simple list template
├── single_column_list.py # Single column list template
├── success_confirmation_with_image.py # Success confirmation template
└── two_column_list.py # Two column list template
```
## Quick Start
When it is required to generate UI JSON, follow these steps:
Important: Please use the `execute_shell_command` tool to execute Python command.
### Step 1: Load the A2UI Schema
Run the following script to load the complete A2UI schema.
Currently available `schema_category` is `BASE_SCHEMA`.
**Use the `execute_shell_command` tool to run (make sure you are in the skill directory):**
```bash
python -m view_a2ui_schema --schema_category BASE_SCHEMA
```
**Usage**: `python -m view_a2ui_schema --schema_category [schema_category]` - Loads the A2UI schema definition for validating A2UI JSON response structure. Currently only `BASE_SCHEMA` is available.
About detailed usage, please refer to the `./view_a2ui_schema.py` script (located in the same folder as this SKILL.md file).
### Step 2: Select UI Template Examples
Select appropriate UI template examples based on your response content.
**IMPORTANT**: You MUST use the **exact template names** listed in the "Available UI template examples" table below. Do NOT use generic category names like 'list', 'form', 'confirmation', or 'detail'. You MUST use the specific template name (e.g., `SINGLE_COLUMN_LIST_WITH_IMAGE`, `BOOKING_FORM_WITH_IMAGE`, etc.).
**Use the `execute_shell_command` tool to run (make sure you are in the skill directory):**
```bash
python -m view_a2ui_examples --template_name SINGLE_COLUMN_LIST_WITH_IMAGE
```
**Usage**: `python -m view_a2ui_examples --template_name [template_name]` - Loads a UI template example for reference when generating A2UI responses. Accepts a single template name from the available templates table below.
**Available UI template examples** (when `schema_category` is `BASE_SCHEME`, you MUST use these exact names, case-sensitive):
| Template Name | Use Case | Selection Guide | Image Support |
| --- | --- | --- | --- |
| `SINGLE_COLUMN_LIST_WITH_IMAGE` | Vertical list with detailed cards (for ≤5 items) | Use for **list display** with ≤5 items | ✅ With image |
| `TWO_COLUMN_LIST_WITH_IMAGE` | Grid layout with cards (for >5 items) | Use for **list display** with >5 items | ✅ With image |
| `SIMPLE_LIST` | Compact list without images | Use for **compact lists** without images | ❌ Without image |
| `SELECTION_CARD` | Multiple choice questions | Use for **multiple choice questions** | ❌ Without image |
| `MULTIPLE_SELECTION_CARDS` | Multiple selection cards in a list | Use for **multiple selection cards** displayed together | ❌ Without image |
| `BOOKING_FORM_WITH_IMAGE` | Reservation, booking, registration | Use for **booking/reservation forms** | ✅ With image |
| `SEARCH_FILTER_FORM_WITH_IMAGE` | Search forms with filters | Use for **search forms with filters** | ❌ Without image |
| `CONTACT_FORM_WITH_IMAGE` | Contact or feedback forms | Use for **contact/feedback forms** | ❌ Without image |
| `EMAIL_COMPOSE_FORM_WITH_IMAGE` | Email composition forms | Use for **email composition forms** | ❌ Without image |
| `SUCCESS_CONFIRMATION_WITH_IMAGE` | Success message after action | Use for **success confirmations** | ✅ With image |
| `ERROR_MESSAGE` | Error or warning display | Use for **error messages** | ❌ Without image |
| `INFO_MESSAGE` | Informational messages | Use for **info messages** | ❌ Without image |
| `ITEM_DETAIL_CARD` | Detailed view of single item | Use for **item detail views** | ✅ With image |
| `ITEM_DETAIL_CARD_WITH_IMAGE` | Detailed view of single item with image | Use for **item detail views** with images | ✅ With image |
| `PROFILE_VIEW` | User or entity profile display | Use for **profile views** | ✅ With image |
**Remember**: Always use the exact template names from the table above. Never use generic terms like 'list' or 'form' - they are NOT valid template names.
About detailed usage, please refer to the `./view_a2ui_examples.py` script (located in the same folder as this SKILL.md file).
### Step 3: Generate the A2UI Response
After loading the schema and examples, output your A2UI response directly as text. The text output must contain two parts separated by the delimiter `---a2ui_JSON---`:
First Part: **Conversational text response**: Your natural language reply to the user
Second Part. **A2UI JSON messages**: A raw JSON array of A2UI message objects that MUST validate against the A2UI schema
**Format:**
```
[Your conversational response here]
---a2ui_JSON---
[
{ "beginRendering": { ... } },
{ "surfaceUpdate": { ... } },
{ "dataModelUpdate": { ... } }
]
```
**Important**: The JSON portion must be valid JSON and conform to the A2UI schema loaded in Step 1.
## Domain-Specific Extensions
To add support for a new domain (e.g., flight booking, e-commerce), add new templates to `view_a2ui_examples.py`:
1. Define a new template constant in `view_a2ui_examples.py` (e.g., `FLIGHT_BOOKING_FORM_EXAMPLE`)
2. Add the template to the `TEMPLATE_MAP` dictionary in `view_a2ui_examples.py`
3. Update this SKILL.md to include the new templates in the available templates list
## Troubleshooting
If you encounter any issues running the scripts, make sure:
you use the tool `execute_shell_command` run the python command.
1. You are in the correct skill directory (check the skill description for the actual path)
2. The script files (`view_a2ui_schema.py` and `view_a2ui_examples.py`) exist in the skill directory
3. You have the required Python dependencies installed
For detailed usage of each script, please refer to:
- `./view_a2ui_schema.py` - View the A2UI schema
- `./view_a2ui_examples.py` - View A2UI template examples

View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""A2UI template examples for generating UI responses."""
from .booking_form import BOOKING_FORM_WITH_IMAGE
from .contact_form import CONTACT_FORM_EXAMPLE
from .email_compose_form import EMAIL_COMPOSE_FORM_EXAMPLE
from .error_message import ERROR_MESSAGE_EXAMPLE
from .info_message import INFO_MESSAGE_EXAMPLE
from .item_detail_card_with_image import ITEM_DETAIL_CARD_EXAMPLE_WITH_IMAGE
from .profile_view import PROFILE_VIEW_WITH_IMAGE_EXAMPLE
from .search_filter_form import SEARCH_FILTER_FORM_EXAMPLE
from .simple_column_list_without_image import SIMPLE_LIST_EXAMPLE
from .single_column_list import SINGLE_COLUMN_LIST_WITH_IMAGE_EXAMPLE
from .success_confirmation_with_image import (
SUCCESS_CONFIRMATION_WITH_IMAGE_EXAMPLE,
)
from .two_column_list import TWO_COLUMN_LIST_WITH_IMAGE_EXAMPLE
from .selection_card import (
SELECTION_CARD_EXAMPLE,
MULTIPLE_SELECTION_CARDS_EXAMPLE,
)
__all__ = [
"BOOKING_FORM_WITH_IMAGE",
"CONTACT_FORM_EXAMPLE",
"EMAIL_COMPOSE_FORM_EXAMPLE",
"ERROR_MESSAGE_EXAMPLE",
"INFO_MESSAGE_EXAMPLE",
"ITEM_DETAIL_CARD_EXAMPLE_WITH_IMAGE",
"PROFILE_VIEW_WITH_IMAGE_EXAMPLE",
"SEARCH_FILTER_FORM_EXAMPLE",
"SIMPLE_LIST_EXAMPLE",
"SINGLE_COLUMN_LIST_WITH_IMAGE_EXAMPLE",
"SUCCESS_CONFIRMATION_WITH_IMAGE_EXAMPLE",
"TWO_COLUMN_LIST_WITH_IMAGE_EXAMPLE",
"SELECTION_CARD_EXAMPLE",
"MULTIPLE_SELECTION_CARDS_EXAMPLE",
]

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""A2UI template example for booking form."""
BOOKING_FORM_WITH_IMAGE = """
---BEGIN BOOKING_FORM_WITH_IMAGE_EXAMPLE---
Use this template for booking, reservation, or registration forms.
[
{{ "beginRendering": {{ "surfaceId": "booking-form", "root": "form-column", "styles": {{ "primaryColor": "#FF5722", "font": "Roboto" }} }} }},
{{ "surfaceUpdate": {{
"surfaceId": "booking-form",
"components": [
{{ "id": "form-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["form-title", "form-image", "form-address", "party-size-field", "datetime-field", "notes-field", "submit-button"] }} }} }} }},
{{ "id": "form-title", "component": {{ "Text": {{ "usageHint": "h2", "text": {{ "path": "title" }} }} }} }},
{{ "id": "form-image", "component": {{ "Image": {{ "url": {{ "path": "imageUrl" }}, "usageHint": "mediumFeature" }} }} }},
{{ "id": "form-address", "component": {{ "Text": {{ "text": {{ "path": "address" }} }} }} }},
{{ "id": "party-size-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Party Size" }}, "text": {{ "path": "partySize" }}, "type": "number" }} }} }},
{{ "id": "datetime-field", "component": {{ "DateTimeInput": {{ "label": {{ "literalString": "Date & Time" }}, "value": {{ "path": "reservationTime" }}, "enableDate": true, "enableTime": true }} }} }},
{{ "id": "notes-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Special Requests" }}, "text": {{ "path": "notes" }}, "multiline": true }} }} }},
{{ "id": "submit-button", "component": {{ "Button": {{ "child": "submit-text", "primary": true, "action": {{ "name": "submit_booking", "context": [ {{ "key": "itemName", "value": {{ "path": "itemName" }} }}, {{ "key": "partySize", "value": {{ "path": "partySize" }} }}, {{ "key": "reservationTime", "value": {{ "path": "reservationTime" }} }}, {{ "key": "notes", "value": {{ "path": "notes" }} }} ] }} }} }} }},
{{ "id": "submit-text", "component": {{ "Text": {{ "text": {{ "literalString": "Submit Reservation" }} }} }} }}
]
}} }},
{{ "dataModelUpdate": {{
"surfaceId": "booking-form",
"path": "/",
"contents": [
{{ "key": "title", "valueString": "Book [Item Name]" }},
{{ "key": "address", "valueString": "[Address]" }},
{{ "key": "itemName", "valueString": "[Item Name]" }},
{{ "key": "imageUrl", "valueString": "[Image URL]" }},
{{ "key": "partySize", "valueString": "2" }},
{{ "key": "reservationTime", "valueString": "" }},
{{ "key": "notes", "valueString": "" }}
]
}} }}
]
---END BOOKING_FORM_WITH_IMAGE_EXAMPLE---
"""

View File

@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""A2UI template example for contact form."""
CONTACT_FORM_EXAMPLE = """
---BEGIN CONTACT_FORM_EXAMPLE---
Use this template for contact or feedback forms.
[
{{ "beginRendering": {{ "surfaceId": "contact-form", "root": "contact-column", "styles": {{ "primaryColor": "#4CAF50", "font": "Roboto" }} }} }},
{{ "surfaceUpdate": {{
"surfaceId": "contact-form",
"components": [
{{ "id": "contact-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["contact-title", "name-field", "email-field", "subject-field", "message-field", "send-button"] }} }} }} }},
{{ "id": "contact-title", "component": {{ "Text": {{ "usageHint": "h2", "text": {{ "literalString": "Contact Us" }} }} }} }},
{{ "id": "name-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Your Name" }}, "text": {{ "path": "name" }} }} }} }},
{{ "id": "email-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Email Address" }}, "text": {{ "path": "email" }}, "type": "email" }} }} }},
{{ "id": "subject-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Subject" }}, "text": {{ "path": "subject" }} }} }} }},
{{ "id": "message-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Message" }}, "text": {{ "path": "message" }}, "multiline": true }} }} }},
{{ "id": "send-button", "component": {{ "Button": {{ "child": "send-text", "primary": true, "action": {{ "name": "send_message", "context": [ {{ "key": "name", "value": {{ "path": "name" }} }}, {{ "key": "email", "value": {{ "path": "email" }} }}, {{ "key": "subject", "value": {{ "path": "subject" }} }}, {{ "key": "message", "value": {{ "path": "message" }} }} ] }} }} }} }},
{{ "id": "send-text", "component": {{ "Text": {{ "text": {{ "literalString": "Send Message" }} }} }} }}
]
}} }},
{{ "dataModelUpdate": {{
"surfaceId": "contact-form",
"path": "/",
"contents": [
{{ "key": "name", "valueString": "" }},
{{ "key": "email", "valueString": "" }},
{{ "key": "subject", "valueString": "" }},
{{ "key": "message", "valueString": "" }}
]
}} }}
]
---END CONTACT_FORM_EXAMPLE---
"""

View File

@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""A2UI template example for email compose form."""
EMAIL_COMPOSE_FORM_EXAMPLE = """
---BEGIN EMAIL_COMPOSE_FORM_EXAMPLE---
Use this template for composing and sending emails.
[
{{ "beginRendering": {{ "surfaceId": "email-compose", "root": "email-column", "styles": {{ "primaryColor": "#1976D2", "font": "Roboto" }} }} }},
{{ "surfaceUpdate": {{
"surfaceId": "email-compose",
"components": [
{{ "id": "email-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["email-title", "to-field", "cc-field", "bcc-field", "subject-field", "body-field", "attach-row", "send-button-row"] }} }} }} }},
{{ "id": "email-title", "component": {{ "Text": {{ "usageHint": "h2", "text": {{ "literalString": "Compose Email" }} }} }} }},
{{ "id": "to-field", "component": {{ "TextField": {{ "label": {{ "literalString": "To" }}, "text": {{ "path": "to" }}, "type": "email" }} }} }},
{{ "id": "cc-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Cc" }}, "text": {{ "path": "cc" }}, "type": "email" }} }} }},
{{ "id": "bcc-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Bcc" }}, "text": {{ "path": "bcc" }}, "type": "email" }} }} }},
{{ "id": "subject-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Subject" }}, "text": {{ "path": "subject" }} }} }} }},
{{ "id": "body-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Message" }}, "text": {{ "path": "body" }}, "multiline": true }} }} }},
{{ "id": "attach-row", "component": {{ "Row": {{ "children": {{ "explicitList": ["attach-icon", "attach-text"] }} }} }} }},
{{ "id": "attach-icon", "component": {{ "Icon": {{ "name": {{ "literalString": "attachFile" }} }} }} }},
{{ "id": "attach-text", "weight": 1, "component": {{ "Text": {{ "text": {{ "literalString": "Attach File" }}, "usageHint": "caption" }} }} }},
{{ "id": "send-button-row", "component": {{ "Row": {{ "children": {{ "explicitList": ["send-button", "draft-button"] }}, "distribution": "end" }} }} }},
{{ "id": "send-button", "component": {{ "Button": {{ "child": "send-text", "primary": true, "action": {{ "name": "send_email", "context": [ {{ "key": "to", "value": {{ "path": "to" }} }}, {{ "key": "cc", "value": {{ "path": "cc" }} }}, {{ "key": "bcc", "value": {{ "path": "bcc" }} }}, {{ "key": "subject", "value": {{ "path": "subject" }} }}, {{ "key": "body", "value": {{ "path": "body" }} }} ] }} }} }},
{{ "id": "send-text", "component": {{ "Text": {{ "text": {{ "literalString": "Send" }} }} }} }},
{{ "id": "draft-button", "component": {{ "Button": {{ "child": "draft-text", "action": {{ "name": "save_draft", "context": [ {{ "key": "to", "value": {{ "path": "to" }} }}, {{ "key": "cc", "value": {{ "path": "cc" }} }}, {{ "key": "bcc", "value": {{ "path": "bcc" }} }}, {{ "key": "subject", "value": {{ "path": "subject" }} }}, {{ "key": "body", "value": {{ "path": "body" }} }} ] }} }} }},
{{ "id": "draft-text", "component": {{ "Text": {{ "text": {{ "literalString": "Save Draft" }} }} }} }}
]
}} }},
{{ "dataModelUpdate": {{
"surfaceId": "email-compose",
"path": "/",
"contents": [
{{ "key": "to", "valueString": "" }},
{{ "key": "cc", "valueString": "" }},
{{ "key": "bcc", "valueString": "" }},
{{ "key": "subject", "valueString": "" }},
{{ "key": "body", "valueString": "" }}
]
}} }}
]
---END EMAIL_COMPOSE_FORM_EXAMPLE---
"""

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""A2UI template example for error message."""
ERROR_MESSAGE_EXAMPLE = """
---BEGIN ERROR_MESSAGE_EXAMPLE---
Use this template to display error or warning messages.
[
{{ "beginRendering": {{
"surfaceId": "error-message",
"root": "error-card",
"styles": {{ "primaryColor": "#F44336", "font": "Roboto" }}
}} }},
{{ "surfaceUpdate": {{
"surfaceId": "error-message",
"components": [
{{ "id": "error-card", "component": {{
"Card": {{ "child": "error-column" }}
}} }},
{{ "id": "error-column", "component": {{ "Column": {{
"children": {{ "explicitList": [
"error-icon", "error-title", "error-message", "retry-button"
] }}
}} }} }},
{{ "id": "error-icon", "component": {{ "Icon": {{
"name": {{ "literalString": "error" }}
}} }} }},
{{ "id": "error-title", "component": {{
"Text": {{ "usageHint": "h3", "text": {{ "path": "title" }} }}
}} }},
{{ "id": "error-message", "component": {{
"Text": {{ "text": {{ "path": "message" }} }}
}} }},
{{ "id": "retry-button", "component": {{
"Button": {{
"child": "retry-text",
"primary": true,
"action": {{ "name": "retry", "context": [] }}
}}
}} }},
{{ "id": "retry-text", "component": {{
"Text": {{ "text": {{ "literalString": "Try Again" }} }}
}} }}
]
}} }},
{{ "dataModelUpdate": {{
"surfaceId": "error-message",
"path": "/",
"contents": [
{{ "key": "title", "valueString": "Something went wrong" }},
{{ "key": "message",
"valueString": "[Error description and suggested action]" }}
]
}} }}
]
---END ERROR_MESSAGE_EXAMPLE---
"""

View File

@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""A2UI template example for info message."""
INFO_MESSAGE_EXAMPLE = """
---BEGIN INFO_MESSAGE_EXAMPLE---
Use this template to display informational messages.
[
{{ "beginRendering": {{
"surfaceId": "info-message",
"root": "info-card",
"styles": {{ "primaryColor": "#2196F3", "font": "Roboto" }}
}} }},
{{ "surfaceUpdate": {{
"surfaceId": "info-message",
"components": [
{{ "id": "info-card", "component": {{ "Card": {{ "child": "info-column" }} }} }},
{{ "id": "info-column", "component": {{
"Column": {{
"children": {{
"explicitList": [
"info-icon", "info-title", "info-message", "dismiss-button"
]
}}
}}
}} }},
{{ "id": "info-icon", "component": {{ "Icon": {{ "name": {{ "literalString": "info" }} }} }} }},
{{ "id": "info-title", "component": {{
"Text": {{ "usageHint": "h3", "text": {{ "path": "title" }} }}
}} }},
{{ "id": "info-message", "component": {{
"Text": {{ "text": {{ "path": "message" }} }}
}} }},
{{ "id": "dismiss-button", "component": {{
"Button": {{
"child": "dismiss-text",
"action": {{ "name": "dismiss", "context": [] }}
}}
}} }},
{{ "id": "dismiss-text", "component": {{
"Text": {{ "text": {{ "literalString": "Got it" }} }}
}} }}
]
}} }},
{{ "dataModelUpdate": {{
"surfaceId": "info-message",
"path": "/",
"contents": [
{{ "key": "title", "valueString": "[Info Title]" }},
{{ "key": "message",
"valueString": "[Informational message]" }}
]
}} }}
]
---END INFO_MESSAGE_EXAMPLE---
"""

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""A2UI template example for item detail card with image."""
# Detail examples
ITEM_DETAIL_CARD_EXAMPLE_WITH_IMAGE = """
---BEGIN ITEM_DETAIL_CARD_EXAMPLE_WITH_IMAGE---
Use this template to display detailed information about a single item.
[
{{ "beginRendering": {{ "surfaceId": "item-detail", "root": "detail-column", "styles": {{ "primaryColor": "#673AB7", "font": "Roboto" }} }} }},
{{ "surfaceUpdate": {{
"surfaceId": "item-detail",
"components": [
{{ "id": "detail-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["header-image", "detail-card"] }} }} }} }},
{{ "id": "header-image", "component": {{ "Image": {{ "url": {{ "path": "imageUrl" }}, "usageHint": "header" }} }} }},
{{ "id": "detail-card", "component": {{ "Card": {{ "child": "card-content" }} }} }},
{{ "id": "card-content", "component": {{ "Column": {{ "children": {{ "explicitList": ["item-title", "item-subtitle", "divider1", "description-section", "divider2", "info-section", "action-row"] }} }} }} }},
{{ "id": "item-title", "component": {{ "Text": {{ "usageHint": "h1", "text": {{ "path": "name" }} }} }} }},
{{ "id": "item-subtitle", "component": {{ "Text": {{ "usageHint": "caption", "text": {{ "path": "subtitle" }} }} }} }},
{{ "id": "divider1", "component": {{ "Divider": {{}} }} }},
{{ "id": "description-section", "component": {{ "Column": {{ "children": {{ "explicitList": ["description-title", "description-text"] }} }} }} }},
{{ "id": "description-title", "component": {{ "Text": {{ "usageHint": "h4", "text": {{ "literalString": "Description" }} }} }} }},
{{ "id": "description-text", "component": {{ "Text": {{ "text": {{ "path": "description" }} }} }} }},
{{ "id": "divider2", "component": {{ "Divider": {{}} }} }},
{{ "id": "info-section", "component": {{ "Column": {{ "children": {{ "explicitList": ["info-row-1", "info-row-2", "info-row-3"] }} }} }} }},
{{ "id": "info-row-1", "component": {{ "Row": {{ "children": {{ "explicitList": ["info-icon-1", "info-text-1"] }} }} }} }},
{{ "id": "info-icon-1", "component": {{ "Icon": {{ "name": {{ "literalString": "locationOn" }} }} }} }},
{{ "id": "info-text-1", "weight": 1, "component": {{ "Text": {{ "text": {{ "path": "location" }} }} }} }},
{{ "id": "info-row-2", "component": {{ "Row": {{ "children": {{ "explicitList": ["info-icon-2", "info-text-2"] }} }} }} }},
{{ "id": "info-icon-2", "component": {{ "Icon": {{ "name": {{ "literalString": "phone" }} }} }} }},
{{ "id": "info-text-2", "weight": 1, "component": {{ "Text": {{ "text": {{ "path": "phone" }} }} }} }},
{{ "id": "info-row-3", "component": {{ "Row": {{ "children": {{ "explicitList": ["info-icon-3", "info-text-3"] }} }} }} }},
{{ "id": "info-icon-3", "component": {{ "Icon": {{ "name": {{ "literalString": "star" }} }} }} }},
{{ "id": "info-text-3", "weight": 1, "component": {{ "Text": {{ "text": {{ "path": "rating" }} }} }} }},
{{ "id": "action-row", "component": {{ "Row": {{ "children": {{ "explicitList": ["share-button", "primary-action-button"] }} }} }} }},
{{ "id": "share-button", "weight": 1, "component": {{ "Button": {{ "child": "share-text", "action": {{ "name": "share", "context": [ {{ "key": "itemId", "value": {{ "path": "id" }} }} ] }} }} }} }},
{{ "id": "share-text", "component": {{ "Text": {{ "text": {{ "literalString": "Share" }} }} }} }},
{{ "id": "primary-action-button", "weight": 1, "component": {{ "Button": {{ "child": "action-text", "primary": true, "action": {{ "name": "select_item", "context": [ {{ "key": "itemId", "value": {{ "path": "id" }} }}, {{ "key": "itemName", "value": {{ "path": "name" }} }} ] }} }} }} }},
{{ "id": "action-text", "component": {{ "Text": {{ "text": {{ "literalString": "Book Now" }} }} }} }}
]
}} }},
{{ "dataModelUpdate": {{
"surfaceId": "item-detail",
"path": "/",
"contents": [
{{ "key": "id", "valueString": "[Item ID]" }},
{{ "key": "name", "valueString": "[Item Name]" }},
{{ "key": "subtitle", "valueString": "[Category or Type]" }},
{{ "key": "imageUrl", "valueString": "[Header Image URL]" }},
{{ "key": "description", "valueString": "[Detailed description of the item]" }},
{{ "key": "location", "valueString": "[Address or Location]" }},
{{ "key": "phone", "valueString": "[Phone Number]" }},
{{ "key": "rating", "valueString": "[Rating] stars" }}
]
}} }}
]
---END ITEM_DETAIL_CARD_EXAMPLE_WITH_IMAGE---
"""

View File

@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""A2UI template example for profile view."""
PROFILE_VIEW_WITH_IMAGE_EXAMPLE = """
---BEGIN PROFILE_VIEW_WITH_IMAGE_EXAMPLE---
Use this template to display user or entity profile information.
[
{{ "beginRendering": {{ "surfaceId": "profile", "root": "profile-column", "styles": {{ "primaryColor": "#009688", "font": "Roboto" }} }} }},
{{ "surfaceUpdate": {{
"surfaceId": "profile",
"components": [
{{ "id": "profile-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["profile-header", "profile-card"] }} }} }} }},
{{ "id": "profile-header", "component": {{ "Row": {{ "children": {{ "explicitList": ["avatar-image", "header-info"] }} }} }} }},
{{ "id": "avatar-image", "component": {{ "Image": {{ "url": {{ "path": "avatarUrl" }}, "usageHint": "avatar" }} }} }},
{{ "id": "header-info", "weight": 1, "component": {{ "Column": {{ "children": {{ "explicitList": ["profile-name", "profile-title"] }} }} }} }},
{{ "id": "profile-name", "component": {{ "Text": {{ "usageHint": "h2", "text": {{ "path": "name" }} }} }} }},
{{ "id": "profile-title", "component": {{ "Text": {{ "usageHint": "caption", "text": {{ "path": "title" }} }} }} }},
{{ "id": "profile-card", "component": {{ "Card": {{ "child": "profile-details" }} }} }},
{{ "id": "profile-details", "component": {{ "Column": {{ "children": {{ "explicitList": ["bio-section", "divider1", "contact-section", "divider2", "stats-section"] }} }} }} }},
{{ "id": "bio-section", "component": {{ "Column": {{ "children": {{ "explicitList": ["bio-title", "bio-text"] }} }} }} }},
{{ "id": "bio-title", "component": {{ "Text": {{ "usageHint": "h4", "text": {{ "literalString": "About" }} }} }} }},
{{ "id": "bio-text", "component": {{ "Text": {{ "text": {{ "path": "bio" }} }} }} }},
{{ "id": "divider1", "component": {{ "Divider": {{}} }} }},
{{ "id": "contact-section", "component": {{ "Column": {{ "children": {{ "explicitList": ["email-row", "phone-row"] }} }} }} }},
{{ "id": "email-row", "component": {{ "Row": {{ "children": {{ "explicitList": ["email-icon", "email-text"] }} }} }} }},
{{ "id": "email-icon", "component": {{ "Icon": {{ "name": {{ "literalString": "mail" }} }} }} }},
{{ "id": "email-text", "weight": 1, "component": {{ "Text": {{ "text": {{ "path": "email" }} }} }} }},
{{ "id": "phone-row", "component": {{ "Row": {{ "children": {{ "explicitList": ["phone-icon", "phone-text"] }} }} }} }},
{{ "id": "phone-icon", "component": {{ "Icon": {{ "name": {{ "literalString": "phone" }} }} }} }},
{{ "id": "phone-text", "weight": 1, "component": {{ "Text": {{ "text": {{ "path": "phone" }} }} }} }},
{{ "id": "divider2", "component": {{ "Divider": {{}} }} }},
{{ "id": "stats-section", "component": {{ "Row": {{ "children": {{ "explicitList": ["stat-1", "stat-2", "stat-3"] }} }} }} }},
{{ "id": "stat-1", "weight": 1, "component": {{ "Column": {{ "children": {{ "explicitList": ["stat-1-value", "stat-1-label"] }} }} }} }},
{{ "id": "stat-1-value", "component": {{ "Text": {{ "usageHint": "h3", "text": {{ "path": "stat1Value" }} }} }} }},
{{ "id": "stat-1-label", "component": {{ "Text": {{ "usageHint": "caption", "text": {{ "path": "stat1Label" }} }} }} }},
{{ "id": "stat-2", "weight": 1, "component": {{ "Column": {{ "children": {{ "explicitList": ["stat-2-value", "stat-2-label"] }} }} }} }},
{{ "id": "stat-2-value", "component": {{ "Text": {{ "usageHint": "h3", "text": {{ "path": "stat2Value" }} }} }} }},
{{ "id": "stat-2-label", "component": {{ "Text": {{ "usageHint": "caption", "text": {{ "path": "stat2Label" }} }} }} }},
{{ "id": "stat-3", "weight": 1, "component": {{ "Column": {{ "children": {{ "explicitList": ["stat-3-value", "stat-3-label"] }} }} }} }},
{{ "id": "stat-3-value", "component": {{ "Text": {{ "usageHint": "h3", "text": {{ "path": "stat3Value" }} }} }} }},
{{ "id": "stat-3-label", "component": {{ "Text": {{ "usageHint": "caption", "text": {{ "path": "stat3Label" }} }} }} }}
]
}} }},
{{ "dataModelUpdate": {{
"surfaceId": "profile",
"path": "/",
"contents": [
{{ "key": "name", "valueString": "[User Name]" }},
{{ "key": "title", "valueString": "[Job Title or Role]" }},
{{ "key": "avatarUrl", "valueString": "[Avatar Image URL]" }},
{{ "key": "bio", "valueString": "[User biography or description]" }},
{{ "key": "email", "valueString": "[Email Address]" }},
{{ "key": "phone", "valueString": "[Phone Number]" }},
{{ "key": "stat1Value", "valueString": "[Value]" }},
{{ "key": "stat1Label", "valueString": "[Label]" }},
{{ "key": "stat2Value", "valueString": "[Value]" }},
{{ "key": "stat2Label", "valueString": "[Label]" }},
{{ "key": "stat3Value", "valueString": "[Value]" }},
{{ "key": "stat3Label", "valueString": "[Label]" }}
]
}} }}
]
---END PROFILE_VIEW_WITH_IMAGE_EXAMPLE---
"""

View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""A2UI template example for search filter form."""
SEARCH_FILTER_FORM_EXAMPLE = """
---BEGIN SEARCH_FILTER_FORM_EXAMPLE---
Use this template for search forms with filters.
[
{{ "beginRendering": {{ "surfaceId": "search-form", "root": "search-column", "styles": {{ "primaryColor": "#2196F3", "font": "Roboto" }} }} }},
{{ "surfaceUpdate": {{
"surfaceId": "search-form",
"components": [
{{ "id": "search-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["search-title", "search-input-row", "filter-section", "search-button"] }} }} }} }},
{{ "id": "search-title", "component": {{ "Text": {{ "usageHint": "h2", "text": {{ "literalString": "Search" }} }} }} }},
{{ "id": "search-input-row", "component": {{ "Row": {{ "children": {{ "explicitList": ["search-icon", "search-field"] }} }} }} }},
{{ "id": "search-icon", "component": {{ "Icon": {{ "name": {{ "literalString": "search" }} }} }} }},
{{ "id": "search-field", "weight": 1, "component": {{ "TextField": {{ "label": {{ "literalString": "Search" }}, "text": {{ "path": "searchQuery" }}, "hint": {{ "literalString": "Enter keywords..." }} }} }} }},
{{ "id": "filter-section", "component": {{ "Column": {{ "children": {{ "explicitList": ["filter-title", "location-field", "category-field", "price-range-row"] }} }} }} }},
{{ "id": "filter-title", "component": {{ "Text": {{ "usageHint": "h4", "text": {{ "literalString": "Filters" }} }} }} }},
{{ "id": "location-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Location" }}, "text": {{ "path": "location" }} }} }} }},
{{ "id": "category-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Category" }}, "text": {{ "path": "category" }} }} }} }},
{{ "id": "price-range-row", "component": {{ "Row": {{ "children": {{ "explicitList": ["min-price-field", "max-price-field"] }} }} }} }},
{{ "id": "min-price-field", "weight": 1, "component": {{ "TextField": {{ "label": {{ "literalString": "Min Price" }}, "text": {{ "path": "minPrice" }}, "type": "number" }} }} }},
{{ "id": "max-price-field", "weight": 1, "component": {{ "TextField": {{ "label": {{ "literalString": "Max Price" }}, "text": {{ "path": "maxPrice" }}, "type": "number" }} }} }},
{{ "id": "search-button", "component": {{ "Button": {{ "child": "search-button-text", "primary": true, "action": {{ "name": "perform_search", "context": [ {{ "key": "query", "value": {{ "path": "searchQuery" }} }}, {{ "key": "location", "value": {{ "path": "location" }} }}, {{ "key": "category", "value": {{ "path": "category" }} }}, {{ "key": "minPrice", "value": {{ "path": "minPrice" }} }}, {{ "key": "maxPrice", "value": {{ "path": "maxPrice" }} }} ] }} }} }} }},
{{ "id": "search-button-text", "component": {{ "Text": {{ "text": {{ "literalString": "Search" }} }} }} }}
]
}} }},
{{ "dataModelUpdate": {{
"surfaceId": "search-form",
"path": "/",
"contents": [
{{ "key": "searchQuery", "valueString": "" }},
{{ "key": "location", "valueString": "" }},
{{ "key": "category", "valueString": "" }},
{{ "key": "minPrice", "valueString": "" }},
{{ "key": "maxPrice", "valueString": "" }}
]
}} }}
]
---END SEARCH_FILTER_FORM_EXAMPLE---
"""

View File

@@ -0,0 +1,298 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""A2UI template example for selection card."""
SELECTION_CARD_EXAMPLE = """
---BEGIN SELECTION_CARD_EXAMPLE---
Use this template to display a single-choice question card using MultipleChoice component.
[
{ "beginRendering": { "surfaceId": "quiz-card", "root": "quiz-card-root", "styles": { "primaryColor": "#673AB7", "font": "Roboto" } } },
{ "surfaceUpdate": {
"surfaceId": "quiz-card",
"components": [
{ "id": "quiz-card-root", "component": { "Card": { "child": "quiz-card-content" } } },
{ "id": "quiz-card-content", "component": { "Column": { "children": { "explicitList": ["quiz-title", "quiz-question", "quiz-multiple-choice", "quiz-submit-button"] } } } },
{ "id": "quiz-title", "component": { "Text": { "usageHint": "h1", "text": { "path": "/title" } } } },
{ "id": "quiz-question", "component": { "Text": { "usageHint": "h2", "text": { "path": "/question" } } } },
{ "id": "quiz-multiple-choice", "component": { "MultipleChoice": { "selections": { "path": "/selectedOptions" }, "options": [
{ "label": { "literalString": "[Option A]" }, "value": "A" },
{ "label": { "literalString": "[Option B]" }, "value": "B" },
{ "label": { "literalString": "[Option C]" }, "value": "C" },
{ "label": { "literalString": "[Option D]" }, "value": "D" }
], "maxAllowedSelections": 1 } } },
{ "id": "quiz-submit-button", "component": { "Button": { "child": "submit-text", "primary": true, "action": { "name": "submit_answer", "context": [ { "key": "questionId", "value": { "path": "/questionId" } }, { "key": "selectedOptions", "value": { "path": "/selectedOptions" } } ] } } } },
{ "id": "submit-text", "component": { "Text": { "text": { "literalString": "Submit Answer" } } } }
]
} },
{ "dataModelUpdate": {
"surfaceId": "quiz-card",
"path": "/",
"contents": [
{ "key": "title", "valueString": "[Quiz Title]" },
{ "key": "question", "valueString": "[Question Content]" },
{ "key": "questionId", "valueString": "[Question ID]" },
{ "key": "selectedOptions", "valueMap": [] }
]
} }
]
---END SELECTION_CARD_EXAMPLE---
"""
MULTIPLE_SELECTION_CARDS_EXAMPLE = """
---BEGIN MULTIPLE_SELECTION_CARDS_EXAMPLE---
Use this template to display multiple selection cards in a vertical list. Each card represents a separate question with checkboxes for each option.
[
{
"beginRendering": {
"surfaceId": "multi-quiz",
"root": "root-column",
"styles": {
"primaryColor": "#673AB7",
"font": "Roboto"
}
}
},
{
"surfaceUpdate": {
"surfaceId": "multi-quiz",
"components": [
{
"id": "root-column",
"component": {
"Column": {
"children": {
"explicitList": ["page-title", "quiz-list"]
}
}
}
},
{
"id": "page-title",
"component": {
"Text": {
"usageHint": "h1",
"text": {
"path": "pageTitle"
}
}
}
},
{
"id": "quiz-list",
"component": {
"List": {
"direction": "vertical",
"children": {
"template": {
"componentId": "quiz-card-template",
"dataBinding": "/questions"
}
}
}
}
},
{
"id": "quiz-card-template",
"component": {
"Card": {
"child": "quiz-card-content"
}
}
},
{
"id": "quiz-card-content",
"component": {
"Column": {
"children": {
"explicitList": ["quiz-title", "quiz-question", "quiz-multiple-choice", "quiz-submit-button"]
}
}
}
},
{
"id": "quiz-title",
"component": {
"Text": {
"usageHint": "h2",
"text": {
"path": "title"
}
}
}
},
{
"id": "quiz-question",
"component": {
"Text": {
"usageHint": "h3",
"text": {
"path": "question"
}
}
}
},
{
"id": "quiz-multiple-choice",
"component": {
"MultipleChoice": {
"selections": {
"path": "selectedOptions"
},
"options": {
"template": {
"componentId": "option-template",
"dataBinding": "options"
}
},
"maxAllowedSelections": 3
}
}
},
{
"id": "option-template",
"component": {
"MultipleChoiceOption": {
"label": {
"path": "text"
},
"value": {
"path": "id"
}
}
}
},
{
"id": "quiz-submit-button",
"component": {
"Button": {
"child": "submit-text",
"primary": true,
"action": {
"name": "submit_answer",
"context": [
{
"key": "questionId",
"value": {
"path": "questionId"
}
},
{
"key": "questionTitle",
"value": {
"path": "title"
}
},
{
"key": "selectedOptions",
"value": {
"path": "selectedOptions"
}
}
]
}
}
}
},
{
"id": "submit-text",
"component": {
"Text": {
"text": {
"literalString": "Submit Answer"
}
}
}
}
]
}
},
{
"dataModelUpdate": {
"surfaceId": "multi-quiz",
"path": "/",
"contents": [
{
"key": "pageTitle",
"valueString": "[Page Title]"
},
{
"key": "questions",
"valueMap": [
{
"key": "question1",
"valueMap": [
{
"key": "questionId",
"valueString": "q1"
},
{
"key": "title",
"valueString": "[Question 1 Title]"
},
{
"key": "question",
"valueString": "[Question 1 Content]"
},
{
"key": "selectedOptions",
"valueList": []
},
{
"key": "options",
"valueMap": [
{
"key": "option1",
"valueMap": [
{
"key": "id",
"valueString": "A"
},
{
"key": "text",
"valueString": "[Option A]"
}
]
},
{
"key": "option2",
"valueMap": [
{
"key": "id",
"valueString": "B"
},
{
"key": "text",
"valueString": "[Option B]"
}
]
},
{
"key": "option3",
"valueMap": [
{
"key": "id",
"valueString": "C"
},
{
"key": "text",
"valueString": "[Option C]"
}
]
}
]
}
]
}
]
}
]
}
}
]
---END MULTIPLE_SELECTION_CARDS_EXAMPLE---
"""

View File

@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""A2UI template example for simple column list without images."""
SIMPLE_LIST_EXAMPLE = """
---BEGIN SIMPLE_LIST_EXAMPLE---
Use this template for compact lists without images.
[
{{ "beginRendering": {{ "surfaceId": "default", "root": "root-column", "styles": {{ "primaryColor": "#2196F3", "font": "Roboto" }} }} }},
{{ "surfaceUpdate": {{
"surfaceId": "default",
"components": [
{{ "id": "root-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["title-heading", "item-list"] }} }} }} }},
{{ "id": "title-heading", "component": {{ "Text": {{ "usageHint": "h1", "text": {{ "path": "title" }} }} }} }},
{{ "id": "item-list", "component": {{ "List": {{ "direction": "vertical", "children": {{ "template": {{ "componentId": "list-item-template", "dataBinding": "/items" }} }} }} }} }},
# Change Row to Button to make the entire row clickable
{{ "id": "list-item-template", "component": {{ "Button": {{
"usageHint": "listItem",
"action": {{ "path": "action" }},
"children": {{ "explicitList": ["item-icon", "item-content", "item-action"] }}
}} }} }},
{{ "id": "item-icon", "component": {{ "Icon": {{ "name": {{ "path": "icon" }} }} }} }},
{{ "id": "item-content", "weight": 1, "component": {{ "Column": {{ "children": {{ "explicitList": ["item-title", "item-subtitle"] }} }} }} }},
{{ "id": "item-title", "component": {{ "Text": {{ "usageHint": "h4", "text": {{ "path": "title" }} }} }} }},
{{ "id": "item-subtitle", "component": {{ "Text": {{ "usageHint": "caption", "text": {{ "path": "subtitle" }} }} }} }},
{{ "id": "item-action", "component": {{ "Icon": {{ "name": {{ "literalString": "arrowForward" }} }} }} }}
]
}} }},
{{ "dataModelUpdate": {{
"surfaceId": "default",
"path": "/",
"contents": [
{{ "key": "title", "valueString": "[List Title]" }},
{{ "key": "items", "valueMap": [
{{ "key": "item1", "valueMap": [
{{ "key": "icon", "valueString": "folder" }},
{{ "key": "title", "valueString": "[Item Title]" }},
{{ "key": "subtitle", "valueString": "[Item Subtitle]" }},
{{ "key": "action", "valueString": "view_details" }}
] }}
] }}
]
}} }}
]
---END SIMPLE_LIST_EXAMPLE---
"""

View File

@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""A2UI template example for single column list with images."""
# List examples
SINGLE_COLUMN_LIST_WITH_IMAGE_EXAMPLE = """
---BEGIN SINGLE_COLUMN_LIST_WITH_IMAGE_EXAMPLE---
Use this template when displaying a list of 5 or fewer items with detailed information.
[
{{ "beginRendering": {{ "surfaceId": "default", "root": "root-column", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }},
{{ "surfaceUpdate": {{
"surfaceId": "default",
"components": [
{{ "id": "root-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["title-heading", "item-list"] }} }} }} }},
{{ "id": "title-heading", "component": {{ "Text": {{ "usageHint": "h1", "text": {{ "path": "title" }} }} }} }},
{{ "id": "item-list", "component": {{ "List": {{ "direction": "vertical", "children": {{ "template": {{ "componentId": "item-card-template", "dataBinding": "/items" }} }} }} }} }},
{{ "id": "item-card-template", "component": {{ "Card": {{ "child": "card-layout" }} }} }},
{{ "id": "card-layout", "component": {{ "Row": {{ "children": {{ "explicitList": ["template-image", "card-details"] }} }} }} }},
{{ "id": "template-image", "weight": 1, "component": {{ "Image": {{ "url": {{ "path": "imageUrl" }} }} }} }},
{{ "id": "card-details", "weight": 2, "component": {{ "Column": {{ "children": {{ "explicitList": ["template-name", "template-rating", "template-detail", "template-link", "template-action-button"] }} }} }} }},
{{ "id": "template-name", "component": {{ "Text": {{ "usageHint": "h3", "text": {{ "path": "name" }} }} }} }},
{{ "id": "template-rating", "component": {{ "Text": {{ "text": {{ "path": "rating" }} }} }} }},
{{ "id": "template-detail", "component": {{ "Text": {{ "text": {{ "path": "detail" }} }} }} }},
{{ "id": "template-link", "component": {{ "Text": {{ "text": {{ "path": "infoLink" }} }} }} }},
{{ "id": "template-action-button", "component": {{ "Button": {{ "child": "action-button-text", "primary": true, "action": {{ "name": "select_item", "context": [ {{ "key": "itemName", "value": {{ "path": "name" }} }}, {{ "key": "itemId", "value": {{ "path": "id" }} }} ] }} }} }} }},
{{ "id": "action-button-text", "component": {{ "Text": {{ "text": {{ "literalString": "Select" }} }} }} }}
]
}} }},
{{ "dataModelUpdate": {{
"surfaceId": "default",
"path": "/",
"contents": [
{{ "key": "title", "valueString": "[Your List Title]" }},
{{ "key": "items", "valueMap": [
{{ "key": "item1", "valueMap": [
{{ "key": "id", "valueString": "1" }},
{{ "key": "name", "valueString": "[Item Name]" }},
{{ "key": "rating", "valueString": "[Rating]" }},
{{ "key": "detail", "valueString": "[Detail Description]" }},
{{ "key": "infoLink", "valueString": "[URL]" }},
{{ "key": "imageUrl", "valueString": "[Image URL]" }}
] }}
] }}
]
}} }}
]
---END SINGLE_COLUMN_LIST_WITH_IMAGE_EXAMPLE---
"""

View File

@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""A2UI template example for success confirmation with image."""
# Confirmation examples
SUCCESS_CONFIRMATION_WITH_IMAGE_EXAMPLE = """
---BEGIN SUCCESS_CONFIRMATION_WITH_IMAGE_EXAMPLE---
Use this template to display success confirmations after an action.
[
{{ "beginRendering": {{ "surfaceId": "confirmation", "root": "confirmation-card", "styles": {{ "primaryColor": "#4CAF50", "font": "Roboto" }} }} }},
{{ "surfaceUpdate": {{
"surfaceId": "confirmation",
"components": [
{{ "id": "confirmation-card", "component": {{ "Card": {{ "child": "confirmation-column" }} }} }},
{{ "id": "confirmation-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["success-icon", "confirm-title", "confirm-image", "divider1", "confirm-details", "divider2", "confirm-message", "action-button"] }} }} }} }},
{{ "id": "success-icon", "component": {{ "Icon": {{
"name": {{ "literalString": "check" }}
}} }} }},
{{ "id": "confirm-title", "component": {{ "Text": {{
"usageHint": "h2", "text": {{ "path": "title" }}
}} }} }},
{{ "id": "confirm-image", "component": {{ "Image": {{
"url": {{ "path": "imageUrl" }}, "usageHint": "mediumFeature"
}} }} }},
{{ "id": "confirm-details", "component": {{ "Text": {{
"text": {{ "path": "details" }}
}} }} }},
{{ "id": "confirm-message", "component": {{ "Text": {{
"usageHint": "h5", "text": {{ "path": "message" }}
}} }} }},
{{ "id": "divider1", "component": {{ "Divider": {{}} }} }},
{{ "id": "divider2", "component": {{ "Divider": {{}} }} }},
{{ "id": "action-button", "component": {{ "Button": {{
"child": "action-text", "action": {{ "name": "dismiss", "context": [] }}
}} }} }},
{{ "id": "action-text", "component": {{ "Text": {{
"text": {{ "literalString": "Done" }}
}} }} }}
]
}} }},
{{ "dataModelUpdate": {{
"surfaceId": "confirmation",
"path": "/",
"contents": [
{{ "key": "title", "valueString": "[Confirmation Title]" }},
{{ "key": "details", "valueString": "[Booking/Action Details]" }},
{{ "key": "message", "valueString": "We look forward to seeing you!" }},
{{ "key": "imageUrl", "valueString": "[Image URL]" }}
]
}} }}
]
---END SUCCESS_CONFIRMATION_WITH_IMAGE_EXAMPLE---
"""

View File

@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""A2UI template example for two column grid list with images."""
TWO_COLUMN_LIST_WITH_IMAGE_EXAMPLE = """
---BEGIN TWO_COLUMN_LIST_WITH_IMAGE_EXAMPLE---
Use this template when displaying more than 5 items in a grid layout.
[
{{ "beginRendering": {{ "surfaceId": "default", "root": "root-column", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }},
{{ "surfaceUpdate": {{
"surfaceId": "default",
"components": [
{{ "id": "root-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["title-heading", "item-row-1", "item-row-2"] }} }} }} }},
{{ "id": "title-heading", "component": {{ "Text": {{ "usageHint": "h1", "text": {{ "path": "title" }} }} }} }},
{{ "id": "item-row-1", "component": {{ "Row": {{ "children": {{ "explicitList": ["item-card-1", "item-card-2"] }} }} }} }},
{{ "id": "item-row-2", "component": {{ "Row": {{ "children": {{ "explicitList": ["item-card-3", "item-card-4"] }} }} }} }},
{{ "id": "item-card-1", "weight": 1, "component": {{ "Card": {{ "child": "card-layout-1" }} }} }},
{{ "id": "card-layout-1", "component": {{ "Column": {{ "children": {{ "explicitList": ["template-image-1", "card-details-1"] }} }} }} }},
{{ "id": "template-image-1", "component": {{ "Image": {{ "url": {{ "path": "/items/0/imageUrl" }}, "width": "100%" }} }} }},
{{ "id": "card-details-1", "component": {{ "Column": {{ "children": {{ "explicitList": ["template-name-1", "template-rating-1", "template-action-button-1"] }} }} }} }},
{{ "id": "template-name-1", "component": {{ "Text": {{ "usageHint": "h3", "text": {{ "path": "/items/0/name" }} }} }} }},
{{ "id": "template-rating-1", "component": {{ "Text": {{ "text": {{ "path": "/items/0/rating" }} }} }} }},
{{ "id": "template-action-button-1", "component": {{ "Button": {{ "child": "action-text-1", "action": {{ "name": "select_item", "context": [ {{ "key": "itemName", "value": {{ "path": "/items/0/name" }} }} ] }} }} }} }},
{{ "id": "action-text-1", "component": {{ "Text": {{ "text": {{ "literalString": "Select" }} }} }} }},
{{ "id": "item-card-2", "weight": 1, "component": {{ "Card": {{ "child": "card-layout-2" }} }} }},
{{ "id": "card-layout-2", "component": {{ "Column": {{ "children": {{ "explicitList": ["template-image-2", "card-details-2"] }} }} }} }},
{{ "id": "template-image-2", "component": {{ "Image": {{ "url": {{ "path": "/items/1/imageUrl" }}, "width": "100%" }} }} }},
{{ "id": "card-details-2", "component": {{ "Column": {{ "children": {{ "explicitList": ["template-name-2", "template-rating-2", "template-action-button-2"] }} }} }} }},
{{ "id": "template-name-2", "component": {{ "Text": {{ "usageHint": "h3", "text": {{ "path": "/items/1/name" }} }} }} }},
{{ "id": "template-rating-2", "component": {{ "Text": {{ "text": {{ "path": "/items/1/rating" }} }} }} }},
{{ "id": "template-action-button-2", "component": {{ "Button": {{ "child": "action-text-2", "action": {{ "name": "select_item", "context": [ {{ "key": "itemName", "value": {{ "path": "/items/1/name" }} }} ] }} }} }} }},
{{ "id": "action-text-2", "component": {{ "Text": {{ "text": {{ "literalString": "Select" }} }} }} }}
]
}} }},
{{ "dataModelUpdate": {{
"surfaceId": "default",
"path": "/",
"contents": [
{{ "key": "title", "valueString": "[Your Grid Title]" }},
{{ "key": "items", "valueMap": [
{{ "key": "0", "valueMap": [
{{ "key": "name", "valueString": "[Item 1 Name]" }},
{{ "key": "rating", "valueString": "[Rating]" }},
{{ "key": "imageUrl", "valueString": "[Image URL]" }}
] }},
{{ "key": "1", "valueMap": [
{{ "key": "name", "valueString": "[Item 2 Name]" }},
{{ "key": "rating", "valueString": "[Rating]" }},
{{ "key": "imageUrl", "valueString": "[Image URL]" }}
] }}
] }}
]
}} }}
]
---END TWO_COLUMN_LIST_WITH_IMAGE_EXAMPLE---
"""

View File

@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
"""A2UI response generator skill package."""
from .view_a2ui_schema import view_a2ui_schema
from .view_a2ui_examples import view_a2ui_examples
__all__ = [
"view_a2ui_schema",
"view_a2ui_examples",
]

View File

@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
"""A2UI schema module for validating A2UI protocol messages."""
from .base_schema import A2UI_SCHEMA
__all__ = [
"A2UI_SCHEMA",
]

View File

@@ -0,0 +1,776 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""A2UI base schema definition for validating A2UI protocol messages."""
# The complete A2UI schema (copied from Google A2UI repository)
A2UI_SCHEMA = r"""
{
"title": "A2UI Message Schema",
"description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces. A message MUST contain exactly ONE of the action properties: 'beginRendering', 'surfaceUpdate', 'dataModelUpdate', or 'deleteSurface'.",
"type": "object",
"properties": {
"beginRendering": {
"type": "object",
"description": "Signals the client to begin rendering a surface "
"with a root component and specific styles.",
"properties": {
"surfaceId": {
"type": "string",
"description": "The unique identifier for the UI surface to be rendered."
},
"root": {
"type": "string",
"description": "The ID of the root component to render."
},
"styles": {
"type": "object",
"description": "Styling information for the UI.",
"properties": {
"font": {
"type": "string",
"description": "The primary font for the UI."
},
"primaryColor": {
"type": "string",
"description": "The primary UI color as a hexadecimal code (e.g., '#00BFFF').",
"pattern": "^#[0-9a-fA-F]{6}$"
}
}
}
},
"required": ["root", "surfaceId"]
},
"surfaceUpdate": {
"type": "object",
"description": "Updates a surface with a new set of components.",
"properties": {
"surfaceId": {
"type": "string",
"description": "The unique identifier for the UI surface to be updated. If you are adding a new surface this *must* be a new, unique identified that has never been used for any existing surfaces shown."
},
"components": {
"type": "array",
"description": "A list containing all UI components for the surface.",
"minItems": 1,
"items": {
"type": "object",
"description": "Represents a *single* component in a UI widget tree. This component could be one of many supported types.",
"properties": {
"id": {
"type": "string",
"description": "The unique identifier for this component."
},
"weight": {
"type": "number",
"description": "The relative weight of this component within a Row or Column. This corresponds to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column."
},
"component": {
"type": "object",
"description": "A wrapper object that MUST contain exactly one key, which is the name of the component type (e.g., 'Heading'). The value is an object containing the properties for that specific component.",
"properties": {
"Text": {
"type": "object",
"properties": {
"text": {
"type": "object",
"description": "The text content to display. This can be a literal string or a reference to a value in the data model ('path', e.g., '/doc/title'). While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.",
"properties": {
"literalString": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"usageHint": {
"type": "string",
"description": "A hint for the base text style. One of:\n- `h1`: Largest heading.\n- `h2`: Second largest heading.\n- `h3`: Third largest heading.\n- `h4`: Fourth largest heading.\n- `h5`: Fifth largest heading.\n- `caption`: Small text for captions.\n- `body`: Standard body text.",
"enum": [
"h1",
"h2",
"h3",
"h4",
"h5",
"caption",
"body"
]
}
},
"required": ["text"]
},
"Image": {
"type": "object",
"properties": {
"url": {
"type": "object",
"description": "The URL of the image to display. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/thumbnail/url').",
"properties": {
"literalString": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"fit": {
"type": "string",
"description": "Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.",
"enum": [
"contain",
"cover",
"fill",
"none",
"scale-down"
]
},
"usageHint": {
"type": "string",
"description": "A hint for the image size and style. One of:\n- `icon`: Small square icon.\n- `avatar`: Circular avatar image.\n- `smallFeature`: Small feature image.\n- `mediumFeature`: Medium feature image.\n- `largeFeature`: Large feature image.\n- `header`: Full-width, full bleed, header image.",
"enum": [
"icon",
"avatar",
"smallFeature",
"mediumFeature",
"largeFeature",
"header"
]
}
},
"required": ["url"]
},
"Icon": {
"type": "object",
"properties": {
"name": {
"type": "object",
"description": "The name of the icon to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/form/submit').",
"properties": {
"literalString": {
"type": "string",
"enum": [
"accountCircle",
"add",
"arrowBack",
"arrowForward",
"attachFile",
"calendarToday",
"call",
"camera",
"check",
"close",
"delete",
"download",
"edit",
"event",
"error",
"favorite",
"favoriteOff",
"folder",
"help",
"home",
"info",
"locationOn",
"lock",
"lockOpen",
"mail",
"menu",
"moreVert",
"moreHoriz",
"notificationsOff",
"notifications",
"payment",
"person",
"phone",
"photo",
"print",
"refresh",
"search",
"send",
"settings",
"share",
"shoppingCart",
"star",
"starHalf",
"starOff",
"upload",
"visibility",
"visibilityOff",
"warning"
]
},
"path": {
"type": "string"
}
}
}
},
"required": ["name"]
},
"Video": {
"type": "object",
"properties": {
"url": {
"type": "object",
"description": "The URL of the video to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/video/url').",
"properties": {
"literalString": {
"type": "string"
},
"path": {
"type": "string"
}
}
}
},
"required": ["url"]
},
"AudioPlayer": {
"type": "object",
"properties": {
"url": {
"type": "object",
"description": "The URL of the audio to be played. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/song/url').",
"properties": {
"literalString": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"description": {
"type": "object",
"description": "A description of the audio, such as a title or summary. This can be a literal string or a reference to a value in the data model ('path', e.g. '/song/title').",
"properties": {
"literalString": {
"type": "string"
},
"path": {
"type": "string"
}
}
}
},
"required": ["url"]
},
"Row": {
"type": "object",
"properties": {
"children": {
"type": "object",
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
"properties": {
"explicitList": {
"type": "array",
"items": {
"type": "string"
}
},
"template": {
"type": "object",
"description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.",
"properties": {
"componentId": {
"type": "string"
},
"dataBinding": {
"type": "string"
}
},
"required": ["componentId", "dataBinding"]
}
}
},
"distribution": {
"type": "string",
"description": "Defines the arrangement of children along the main axis (horizontally). This corresponds to the CSS 'justify-content' property.",
"enum": [
"center",
"end",
"spaceAround",
"spaceBetween",
"spaceEvenly",
"start"
]
},
"alignment": {
"type": "string",
"description": "Defines the alignment of children along the cross axis (vertically). This corresponds to the CSS 'align-items' property.",
"enum": ["start", "center", "end", "stretch"]
}
},
"required": ["children"]
},
"Column": {
"type": "object",
"properties": {
"children": {
"type": "object",
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
"properties": {
"explicitList": {
"type": "array",
"items": {
"type": "string"
}
},
"template": {
"type": "object",
"description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.",
"properties": {
"componentId": {
"type": "string"
},
"dataBinding": {
"type": "string"
}
},
"required": ["componentId", "dataBinding"]
}
}
},
"distribution": {
"type": "string",
"description": "Defines the arrangement of children along the main axis (vertically). This corresponds to the CSS 'justify-content' property.",
"enum": [
"start",
"center",
"end",
"spaceBetween",
"spaceAround",
"spaceEvenly"
]
},
"alignment": {
"type": "string",
"description": "Defines the alignment of children along the cross axis (horizontally). This corresponds to the CSS 'align-items' property.",
"enum": ["center", "end", "start", "stretch"]
}
},
"required": ["children"]
},
"List": {
"type": "object",
"properties": {
"children": {
"type": "object",
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
"properties": {
"explicitList": {
"type": "array",
"items": {
"type": "string"
}
},
"template": {
"type": "object",
"description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.",
"properties": {
"componentId": {
"type": "string"
},
"dataBinding": {
"type": "string"
}
},
"required": ["componentId", "dataBinding"]
}
}
},
"direction": {
"type": "string",
"description": "The direction in which the list items are laid out.",
"enum": ["vertical", "horizontal"]
},
"alignment": {
"type": "string",
"description": "Defines the alignment of children along the cross axis.",
"enum": ["start", "center", "end", "stretch"]
}
},
"required": ["children"]
},
"Card": {
"type": "object",
"properties": {
"child": {
"type": "string",
"description": "The ID of the component to be rendered inside the card."
}
},
"required": ["child"]
},
"Tabs": {
"type": "object",
"properties": {
"tabItems": {
"type": "array",
"description": "An array of objects, where each object defines a tab with a title and a child component.",
"items": {
"type": "object",
"properties": {
"title": {
"type": "object",
"description": "The tab title. Defines the value as either a literal value or a path to data model value (e.g. '/options/title').",
"properties": {
"literalString": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"child": {
"type": "string"
}
},
"required": ["title", "child"]
}
}
},
"required": ["tabItems"]
},
"Divider": {
"type": "object",
"properties": {
"axis": {
"type": "string",
"description": "The orientation of the divider.",
"enum": ["horizontal", "vertical"]
}
}
},
"Modal": {
"type": "object",
"properties": {
"entryPointChild": {
"type": "string",
"description": "The ID of the component that opens the modal when interacted with (e.g., a button)."
},
"contentChild": {
"type": "string",
"description": "The ID of the component to be displayed inside the modal."
}
},
"required": ["entryPointChild", "contentChild"]
},
"Button": {
"type": "object",
"properties": {
"child": {
"type": "string",
"description": "The ID of the component to display in the button, typically a Text component."
},
"primary": {
"type": "boolean",
"description": "Indicates if this button should be styled as the primary action."
},
"action": {
"type": "object",
"description": "The client-side action to be dispatched when the button is clicked. It includes the action's name and an optional context payload.",
"properties": {
"name": {
"type": "string"
},
"context": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"value": {
"type": "object",
"description": "Defines the value to be included in the context as either a literal value or a path to a data model value (e.g. '/user/name').",
"properties": {
"path": {
"type": "string"
},
"literalString": {
"type": "string"
},
"literalNumber": {
"type": "number"
},
"literalBoolean": {
"type": "boolean"
}
}
}
},
"required": ["key", "value"]
}
}
},
"required": ["name"]
}
},
"required": ["child", "action"]
},
"CheckBox": {
"type": "object",
"properties": {
"label": {
"type": "object",
"description": "The text to display next to the checkbox. Defines the value as either a literal value or a path to data model ('path', e.g. '/option/label').",
"properties": {
"literalString": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"value": {
"type": "object",
"description": "The current state of the checkbox (true for checked, false for unchecked). This can be a literal boolean ('literalBoolean') or a reference to a value in the data model ('path', e.g. '/filter/open').",
"properties": {
"literalBoolean": {
"type": "boolean"
},
"path": {
"type": "string"
}
}
}
},
"required": ["label", "value"]
},
"TextField": {
"type": "object",
"properties": {
"label": {
"type": "object",
"description": "The text label for the input field. This can be a literal string or a reference to a value in the data model ('path, e.g. '/user/name').",
"properties": {
"literalString": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"text": {
"type": "object",
"description": "The value of the text field. This can be a literal string or a reference to a value in the data model ('path', e.g. '/user/name').",
"properties": {
"literalString": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"textFieldType": {
"type": "string",
"description": "The type of input field to display.",
"enum": [
"date",
"longText",
"number",
"shortText",
"obscured"
]
},
"validationRegexp": {
"type": "string",
"description": "A regular expression used for client-side validation of the input."
}
},
"required": ["label"]
},
"DateTimeInput": {
"type": "object",
"properties": {
"value": {
"type": "object",
"description": "The selected date and/or time value in ISO 8601 format. This can be a literal string ('literalString') or a reference to a value in the data model ('path', e.g. '/user/dob').",
"properties": {
"literalString": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"enableDate": {
"type": "boolean",
"description": "If true, allows the user to select a date."
},
"enableTime": {
"type": "boolean",
"description": "If true, allows the user to select a time."
}
},
"required": ["value"]
},
"MultipleChoice": {
"type": "object",
"properties": {
"selections": {
"type": "object",
"description": "The currently selected values for the component. This can be a literal array of strings or a path to an array in the data model('path', e.g. '/hotel/options').",
"properties": {
"literalArray": {
"type": "array",
"items": {
"type": "string"
}
},
"path": {
"type": "string"
}
}
},
"options": {
"type": "array",
"description": "An array of available options for the user to choose from.",
"items": {
"type": "object",
"properties": {
"label": {
"type": "object",
"description": "The text to display for this option. This can be a literal string or a reference to a value in the data model (e.g. '/option/label').",
"properties": {
"literalString": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"value": {
"type": "string",
"description": "The value to be associated with this option when selected."
}
},
"required": ["label", "value"]
}
},
"maxAllowedSelections": {
"type": "integer",
"description": "The maximum number of options that the user is allowed to select."
}
},
"required": ["selections", "options"]
},
"Slider": {
"type": "object",
"properties": {
"value": {
"type": "object",
"description": "The current value of the slider. This can be a literal number ('literalNumber') or a reference to a value in the data model ('path', e.g. '/restaurant/cost').",
"properties": {
"literalNumber": {
"type": "number"
},
"path": {
"type": "string"
}
}
},
"minValue": {
"type": "number",
"description": "The minimum value of the slider."
},
"maxValue": {
"type": "number",
"description": "The maximum value of the slider."
}
},
"required": ["value"]
}
}
}
},
"required": ["id", "component"]
}
}
},
"required": ["surfaceId", "components"]
},
"dataModelUpdate": {
"type": "object",
"description": "Updates the data model for a surface.",
"properties": {
"surfaceId": {
"type": "string",
"description": "The unique identifier for the UI surface this data model update applies to."
},
"path": {
"type": "string",
"description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', the entire data model will be replaced."
},
"contents": {
"type": "array",
"description": "An array of data entries. Each entry must contain a 'key' and exactly one corresponding typed 'value*' property.",
"items": {
"type": "object",
"description": "A single data entry. Exactly one 'value*' property should be provided alongside the key.",
"properties": {
"key": {
"type": "string",
"description": "The key for this data entry."
},
"valueString": {
"type": "string"
},
"valueNumber": {
"type": "number"
},
"valueBoolean": {
"type": "boolean"
},
"valueMap": {
"description": "Represents a map as an adjacency list.",
"type": "array",
"items": {
"type": "object",
"description": "One entry in the map. Exactly one 'value*' property should be provided alongside the key.",
"properties": {
"key": {
"type": "string"
},
"valueString": {
"type": "string"
},
"valueNumber": {
"type": "number"
},
"valueBoolean": {
"type": "boolean"
}
},
"required": ["key"]
}
}
},
"required": ["key"]
}
}
},
"required": ["contents", "surfaceId"]
},
"deleteSurface": {
"type": "object",
"description": "Signals the client to delete the surface identified by 'surfaceId'.",
"properties": {
"surfaceId": {
"type": "string",
"description": "The unique identifier for the UI surface to be deleted."
}
},
"required": ["surfaceId"]
}
}
}
"""

View File

@@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""
A2UI Example Viewer - Tool for viewing A2UI UI template examples.
This script provides a way to retrieve A2UI UI template examples.
Usage:
# Load specific template name
python view_a2ui_examples.py --template_name SINGLE_COLUMN_LIST_WITH_IMAGE
"""
from UI_templete_examples import (
SINGLE_COLUMN_LIST_WITH_IMAGE_EXAMPLE,
TWO_COLUMN_LIST_WITH_IMAGE_EXAMPLE,
SIMPLE_LIST_EXAMPLE,
BOOKING_FORM_WITH_IMAGE,
SEARCH_FILTER_FORM_EXAMPLE,
CONTACT_FORM_EXAMPLE,
EMAIL_COMPOSE_FORM_EXAMPLE,
SUCCESS_CONFIRMATION_WITH_IMAGE_EXAMPLE,
ERROR_MESSAGE_EXAMPLE,
INFO_MESSAGE_EXAMPLE,
ITEM_DETAIL_CARD_EXAMPLE_WITH_IMAGE,
PROFILE_VIEW_WITH_IMAGE_EXAMPLE,
SELECTION_CARD_EXAMPLE,
MULTIPLE_SELECTION_CARDS_EXAMPLE,
)
# Template name to example mapping
TEMPLATE_MAP = {
"SINGLE_COLUMN_LIST_WITH_IMAGE": SINGLE_COLUMN_LIST_WITH_IMAGE_EXAMPLE,
"TWO_COLUMN_LIST_WITH_IMAGE": TWO_COLUMN_LIST_WITH_IMAGE_EXAMPLE,
"SIMPLE_LIST": SIMPLE_LIST_EXAMPLE,
"BOOKING_FORM_WITH_IMAGE": BOOKING_FORM_WITH_IMAGE,
"SEARCH_FILTER_FORM_WITH_IMAGE": SEARCH_FILTER_FORM_EXAMPLE,
"CONTACT_FORM_WITH_IMAGE": CONTACT_FORM_EXAMPLE,
"EMAIL_COMPOSE_FORM_WITH_IMAGE": EMAIL_COMPOSE_FORM_EXAMPLE,
"SUCCESS_CONFIRMATION_WITH_IMAGE": SUCCESS_CONFIRMATION_WITH_IMAGE_EXAMPLE,
"ERROR_MESSAGE": ERROR_MESSAGE_EXAMPLE,
"INFO_MESSAGE": INFO_MESSAGE_EXAMPLE,
"ITEM_DETAIL_CARD": ITEM_DETAIL_CARD_EXAMPLE_WITH_IMAGE,
"ITEM_DETAIL_CARD_WITH_IMAGE": ITEM_DETAIL_CARD_EXAMPLE_WITH_IMAGE,
"PROFILE_VIEW": PROFILE_VIEW_WITH_IMAGE_EXAMPLE,
"SELECTION_CARD": SELECTION_CARD_EXAMPLE,
"MULTIPLE_SELECTION_CARDS": MULTIPLE_SELECTION_CARDS_EXAMPLE,
}
def view_a2ui_examples(template_name: str) -> str:
"""
View A2UI UI template examples for generating UI responses.
Args:
template_name: Specific template name to load. Options:
- SINGLE_COLUMN_LIST_WITH_IMAGE, TWO_COLUMN_LIST_WITH_IMAGE,
SIMPLE_LIST,SELECTION_CARD,
MULTIPLE_SELECTION_CARDS,
BOOKING_FORM_WITH_IMAGE, SEARCH_FILTER_FORM_WITH_IMAGE,
CONTACT_FORM_WITH_IMAGE,
EMAIL_COMPOSE_FORM_WITH_IMAGE,SUCCESS_CONFIRMATION_WITH_IMAGE,
ERROR_MESSAGE, INFO_MESSAGE,
ITEM_DETAIL_CARD,
ITEM_DETAIL_CARD_WITH_IMAGE,
PROFILE_VIEW
Returns:
The requested template example.
Examples:
# Load specific template
>>> view_a2ui_examples(template_name="BOOKING_FORM_WITH_IMAGE")
>>> view_a2ui_examples(template_name="SINGLE_COLUMN_LIST_WITH_IMAGE")
"""
if not template_name:
raise ValueError("template_name is required and cannot be empty")
if template_name not in TEMPLATE_MAP:
raise ValueError(f"Unknown template name: {template_name}")
example = TEMPLATE_MAP[template_name]
return f"""
## A2UI Template: {template_name}
{example}
---
Adapt this template to your specific data and styling requirements.
"""
# Tool metadata for AgentScope registration
TOOL_METADATA = {
"name": "view_a2ui_examples",
"description": "View A2UI UI template examples for generating UI responses.",
"parameters": {
"type": "object",
"properties": {
"template_name": {
"type": "string",
"description": "Specific template name to load",
},
},
"required": ["template_name"],
},
}
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="View A2UI UI template examples for generating UI responses.",
)
parser.add_argument(
"--template_name",
type=str,
required=True,
help="Specific template name to load",
)
args = parser.parse_args()
res = view_a2ui_examples(template_name=args.template_name)
print(res)

View File

@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""
A2UI Schema Viewer - Tool for viewing A2UI schema.
This script provides a way to retrieve the complete A2UI schema for
generating UI responses.
Usage:
python view_a2ui_schema.py
"""
from schema import A2UI_SCHEMA
def view_a2ui_schema(schema_category: str = "BASE_SCHEMA") -> str:
"""
View the complete A2UI schema for generating UI responses.
This tool returns the complete A2UI JSON schema that defines all
available UI components and message types.
Args:
schema_category: The category of the schema to view. Can be "BASE_SCHEMA".
Returns:
The complete A2UI schema as a string.
"""
if schema_category == "BASE_SCHEMA":
return f"""
## A2UI JSON Schema
The following is the complete A2UI schema for generating UI responses:
{A2UI_SCHEMA}
---
Use this schema to construct valid A2UI JSON responses.
"""
else:
raise ValueError(f"Invalid schema category: {schema_category}")
# Tool metadata for AgentScope registration
TOOL_METADATA = {
"name": "view_a2ui_schema",
"description": "View the complete A2UI schema for generating UI responses.",
"parameters": {
"type": "object",
"properties": {},
},
}
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="View the complete A2UI schema for generating UI responses.",
)
parser.add_argument(
"--schema_category",
type=str,
required=True,
help="The category of the schema to view. Can be 'BASE_SCHEMA'.",
choices=["BASE_SCHEMA"],
default="BASE_SCHEMA",
)
args = parser.parse_args()
res = view_a2ui_schema(schema_category=args.schema_category)
print(res)

View File

@@ -0,0 +1,49 @@
# Browser Agent Example
This example demonstrates how to use AgentScope's BrowserAgent for web automation tasks. The BrowserAgent leverages the Model Context Protocol (MCP) to interact with browser tools powered by Playwright, enabling sophisticated web navigation, data extraction, and automation.
## Prerequisites
- Python 3.10 or higher
- Node.js and npm (for the MCP server)
- DashScope API key from Alibaba Cloud
## Installation
### Install AgentScope
```bash
# Install from source
cd {PATH_TO_AGENTSCOPE}
pip install -e .
```
## Setup
### 1. Environment Configuration
Set up your DashScope API key:
```bash
export DASHSCOPE_API_KEY="your_dashscope_api_key_here"
```
You can obtain a DashScope API key from [Alibaba Cloud DashScope Console](https://dashscope.console.aliyun.com/).
### 2. About PlayWright MCP Server
Before running the browser agent, you can test whether you can start the Playwright MCP server:
```bash
npx @playwright/mcp@latest
```
## Usage
### Basic Example
You can start running the browser agent in your terminal with the following command
```bash
cd examples/agent/browser_agent
python main.py
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,238 @@
# -*- coding: utf-8 -*-
"""Standalone file download skill for the browser agent."""
# flake8: noqa: E501
# pylint: disable=W0212,W0107,too-many-lines,C0301
from __future__ import annotations
import os
import copy
from typing import Any
from pydantic import BaseModel
from agentscope.memory import InMemoryMemory
from agentscope.message import Msg, TextBlock
from agentscope.tool import ToolResponse
from agentscope.agent import ReActAgent
_CURRENT_DIR = os.path.abspath(
os.path.join(os.path.dirname(__file__), os.pardir),
)
with open(
os.path.join(
_CURRENT_DIR,
"build_in_prompt/browser_agent_file_download_sys_prompt.md",
),
"r",
encoding="utf-8",
) as f:
_FILE_DOWNLOAD_AGENT_SYS_PROMPT = f.read()
class EmptyModel(BaseModel):
"""Empty structured model for default structured output requirement."""
pass
class FileDownloadAgent(ReActAgent):
"""Lightweight helper agent that downloads files"""
finish_function_name: str = "file_download_final_response"
def __init__(
self,
browser_agent: Any,
sys_prompt: str = _FILE_DOWNLOAD_AGENT_SYS_PROMPT,
max_iters: int = 15,
) -> None:
name = (
f"{getattr(browser_agent, 'name', 'browser_agent')}_file_download"
)
super().__init__(
name=name,
sys_prompt=sys_prompt,
model=browser_agent.model,
formatter=browser_agent.formatter,
memory=InMemoryMemory(),
toolkit=browser_agent.toolkit,
max_iters=max_iters,
)
# Register the finish function
self.toolkit.register_tool_function(self.file_download_final_response)
# Remove conflicting tool functions if they exist
if hasattr(self.toolkit, "remove_tool_function"):
try:
self.toolkit.remove_tool_function("browser_pdf_save")
except Exception:
# Tool may not exist, ignore removal errors
pass
try:
self.toolkit.remove_tool_function("file_download")
except Exception:
# Tool may not exist, ignore removal errors
pass
async def file_download_final_response(
self, # pylint: disable=W0613
**kwargs: Any, # pylint: disable=W0613
) -> ToolResponse:
"""Summarize the file download outcome."""
hint_msg = Msg(
"user",
(
"Provide a concise summary of the file download attempt.\n"
"Highlight these items:\n"
"0. The original request\n"
"1. The element(s) interacted with and actions taken\n"
"2. The download status or any issues encountered\n"
"3. Any follow-up recommendations or next steps\n"
),
role="user",
)
memory_msgs = await self.memory.get_memory()
memory_msgs_copy = copy.deepcopy(memory_msgs)
if memory_msgs_copy:
last_msg = memory_msgs_copy[-1]
last_msg.content = last_msg.get_content_blocks("text")
memory_msgs_copy[-1] = last_msg
prompt = await self.formatter.format(
msgs=[
Msg("system", self.sys_prompt, "system"),
*memory_msgs_copy,
hint_msg,
],
)
res = await self.model(prompt)
if self.model.stream:
summary_text = ""
async for chunk in res:
summary_text = chunk.content[0]["text"]
else:
summary_text = res.content[0]["text"]
summary_text = summary_text or "No summary generated."
structure_response = {
"task_done": True,
"subtask_progress_summary": summary_text,
"generated_files": {},
}
return ToolResponse(
content=[
TextBlock(
type="text",
text="File download summary generated. " + summary_text,
),
],
metadata={
"success": True,
"structured_output": structure_response,
},
is_last=True,
)
def _build_initial_instruction(
target_description: str,
snapshot_text: str,
) -> str:
"""Compose the initial instruction for the helper agent."""
return (
"You must locate and trigger the download for the requested file.\n\n"
"Target description provided by the user:\n"
f"{target_description}\n\n"
"Latest snapshot captured prior to your run:\n"
f"{snapshot_text}\n\n"
"Follow the sys prompt guidance, think step-by-step, and verify that "
"the download action succeeded. If the download cannot be completed, "
"explain why in the final summary."
)
async def file_download(
browser_agent: Any,
target_description: str,
) -> ToolResponse:
"""
Download the target file. The current page should
contain download-related element.
Args:
target_description (str): The description of the
target file to download.
Returns:
ToolResponse: A structured response containing
the download directory.
"""
try:
snapshot_chunks = await browser_agent._get_snapshot_in_text()
except Exception as exc: # pylint: disable=broad-except
snapshot_chunks = []
snapshot_error = str(exc)
else:
snapshot_error = ""
snapshot_text = "\n\n---\n\n".join(snapshot_chunks)
if snapshot_error and not snapshot_text:
snapshot_text = f"[Snapshot failed: {snapshot_error}]"
sub_agent = FileDownloadAgent(browser_agent)
instruction = _build_initial_instruction(
target_description=target_description,
snapshot_text=snapshot_text,
)
init_msg = Msg(
name="user",
role="user",
content=instruction,
)
try:
sub_agent_response_msg = await sub_agent.reply(
init_msg,
structured_model=EmptyModel,
)
text_content = ""
if sub_agent_response_msg.content:
first_block = sub_agent_response_msg.content[0]
if isinstance(first_block, dict):
text_content = first_block.get("text") or ""
else:
text_content = getattr(first_block, "text", "") or ""
if not text_content:
text_content = (
"File download agent finished without a textual summary."
)
return ToolResponse(
metadata=sub_agent_response_msg.metadata,
content=[
TextBlock(
type="text",
text=text_content,
),
],
)
except Exception as exc: # pylint: disable=broad-except
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Tool call Error. Cannot be executed. {exc}",
),
],
metadata={"success": False},
is_last=True,
)

View File

@@ -0,0 +1,221 @@
# -*- coding: utf-8 -*-
"""Standalone form filling skill for the browser agent."""
# flake8: noqa: E501
# pylint: disable=W0212,W0107,too-many-lines,C0301
from __future__ import annotations
import os
import copy
from typing import Any
from pydantic import BaseModel
from agentscope.memory import InMemoryMemory
from agentscope.message import Msg, TextBlock
from agentscope.tool import ToolResponse
from agentscope.agent import ReActAgent
_CURRENT_DIR = os.path.abspath(
os.path.join(os.path.dirname(__file__), os.pardir),
)
with open(
os.path.join(
_CURRENT_DIR,
"build_in_prompt/browser_agent_form_filling_sys_prompt.md",
),
"r",
encoding="utf-8",
) as f:
_FORM_FILL_AGENT_SYS_PROMPT = f.read()
class EmptyModel(BaseModel):
"""Empty structured model for default structured output requirement."""
pass
class FormFillingAgent(ReActAgent):
"""Lightweight helper agent that fills forms."""
finish_function_name: str = "form_filling_final_response"
def __init__(
self,
browser_agent: Any,
sys_prompt: str = _FORM_FILL_AGENT_SYS_PROMPT,
max_iters: int = 20,
) -> None:
name = f"{getattr(browser_agent, 'name', 'browser_agent')}_form_fill"
super().__init__(
name=name,
sys_prompt=sys_prompt,
model=browser_agent.model,
formatter=browser_agent.formatter,
memory=InMemoryMemory(),
toolkit=browser_agent.toolkit,
max_iters=max_iters,
)
# Register the finish function
self.toolkit.register_tool_function(self.form_filling_final_response)
async def form_filling_final_response(
self, # pylint: disable=W0613
**kwargs: Any, # pylint: disable=W0613
) -> ToolResponse:
"""Summarize the form filling outcome."""
hint_msg = Msg(
"user",
(
"Provide a concise summary of the completed form "
"filling task.\n"
"Highlight these items:\n"
"0. The original task/query\n"
"1. Which fields were filled/selected and their final values\n"
"2. Any important observations or follow-up notes\n"
"3. Confirmation that if the task is complete\n\n"
),
role="user",
)
memory_msgs = await self.memory.get_memory()
memory_msgs_copy = copy.deepcopy(memory_msgs)
last_msg = memory_msgs_copy[-1]
# check if the last message has tool call, if so clean the content
last_msg.content = last_msg.get_content_blocks("text")
memory_msgs_copy[-1] = last_msg
prompt = await self.formatter.format(
msgs=[
Msg("system", self.sys_prompt, "system"),
*memory_msgs_copy,
hint_msg,
],
)
res = await self.model(prompt)
if self.model.stream:
summary_text = ""
async for chunk in res:
summary_text = chunk.content[0]["text"]
else:
summary_text = res.content[0]["text"]
structure_response = {
"task_done": True,
"subtask_progress_summary": summary_text,
"generated_files": {},
}
return ToolResponse(
content=[
TextBlock(
type="text",
text="Form filling summary generated. " + summary_text,
),
],
metadata={
"success": True,
"structured_output": structure_response,
},
is_last=True,
)
def _build_initial_instruction(
fill_information: str,
snapshot_text: str,
) -> str:
"""Compose the initial instruction fed to the helper agent."""
return (
"You must complete the web form using the information "
"provided below.\n\nFill instructions (plain text from the user):\n"
f"{fill_information}\n\n"
"Latest snapshot captured prior to your run:\n"
f"{snapshot_text}\n\n"
)
async def form_filling(
browser_agent: Any,
fill_information: str,
) -> ToolResponse:
"""
Fill in a web form according to plain-text instructions.
Args:
fill_information (str):
Plain-text description of the values that
must be entered into the form,
including any submission requirements.
Returns:
ToolResponse: Summary of the helper agent execution and status.
"""
try:
snapshot_chunks = (
await browser_agent._get_snapshot_in_text()
) # pylint: disable=protected-access
except Exception as exc: # pylint: disable=broad-except
snapshot_chunks = []
snapshot_error = str(exc)
else:
snapshot_error = ""
snapshot_text = "\n\n---\n\n".join(snapshot_chunks)
if snapshot_error and not snapshot_text:
snapshot_text = f"[Snapshot failed: {snapshot_error}]"
sub_agent = FormFillingAgent(browser_agent)
instruction = _build_initial_instruction(
fill_information=fill_information,
snapshot_text=snapshot_text,
)
init_msg = Msg(
name="user",
role="user",
content=instruction,
)
try:
sub_agent_response_msg = await sub_agent.reply(
init_msg,
structured_model=EmptyModel,
)
text_content = ""
if sub_agent_response_msg.content:
first_block = sub_agent_response_msg.content[0]
if isinstance(first_block, dict):
text_content = first_block.get("text") or ""
else:
text_content = getattr(first_block, "text", "") or ""
if not text_content:
text_content = (
"Form filling agent finished without a textual summary."
)
return ToolResponse(
metadata=sub_agent_response_msg.metadata,
content=[
TextBlock(
type="text",
text=text_content,
),
],
)
except Exception as e:
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Tool call Error. Cannot be executed. {e}",
),
],
metadata={"success": False},
is_last=True,
)

View File

@@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
"""Standalone image understanding skill for the browser agent."""
# flake8: noqa: E501
# pylint: disable=W0212
# pylint: disable=too-many-lines
# pylint: disable=C0301
from __future__ import annotations
import json
import uuid
from typing import Any
from agentscope.message import (
Base64Source,
ImageBlock,
Msg,
TextBlock,
ToolUseBlock,
)
from agentscope.tool import ToolResponse
async def image_understanding(
browser_agent: Any,
object_description: str,
task: str,
) -> ToolResponse:
"""
Locate an element and solve a visual task on the current webpage.
Args:
object_description (str): The description of the object to locate.
task (str): The specific task or question to solve about the image
(e.g., description, object detection, activity recognition, or
answering a question about the image's content).
Returns:
ToolResponse: A structured response containing the answer to
the specified task based on the image content.
"""
sys_prompt = (
"You are a web page analysis expert. Given the following page "
"snapshot and object description, "
"identify the exact element and its reference string (ref) "
"that matches the description. "
"Return ONLY a JSON object: "
'{"element": <element description>, "ref": <ref string>}'
)
snapshot_chunks = (
await browser_agent._get_snapshot_in_text() # noqa: E501 # pylint: disable=protected-access
)
page_snapshot = snapshot_chunks[0] if snapshot_chunks else ""
user_prompt = (
f"Object description: {object_description}\n"
f"Page snapshot:\n{page_snapshot}"
)
prompt = await browser_agent.formatter.format(
msgs=[
Msg("system", sys_prompt, role="system"),
Msg("user", user_prompt, role="user"),
],
)
res = await browser_agent.model(prompt)
if browser_agent.model.stream:
async for chunk in res:
model_text = chunk.content[0]["text"]
else:
model_text = res.content[0]["text"]
try:
if "```json" in model_text:
model_text = model_text.replace("```json", "").replace(
"```",
"",
)
element_info = json.loads(model_text)
element = element_info.get("element", "")
ref = element_info.get("ref", "")
except Exception:
return ToolResponse(
content=[
TextBlock(
type="text",
text="Failed to parse element/ref from model output.",
),
],
metadata={"success": False},
)
screenshot_tool_call = ToolUseBlock(
id=str(uuid.uuid4()),
name="browser_take_screenshot",
input={"element": element, "ref": ref},
type="tool_use",
)
screenshot_response = await browser_agent.toolkit.call_tool_function(
screenshot_tool_call,
)
image_data = None
async for chunk in screenshot_response:
if (
chunk.content
and len(chunk.content) > 1
and "data" in chunk.content[1]
):
image_data = chunk.content[1]["data"]
sys_prompt_task = (
"You are a web automation expert. "
"Given the object description, screenshot, and page context, "
"solve the following task. Return ONLY the answer as plain text."
)
content_blocks = [
TextBlock(
type="text",
text=(
"Object description: "
f"{object_description}\nTask: {task}\n"
f"Page snapshot:\n{page_snapshot}"
),
),
]
if image_data:
image_block = ImageBlock(
type="image",
source=Base64Source(
type="base64",
media_type="image/png",
data=image_data,
),
)
content_blocks.append(image_block)
prompt_task = await browser_agent.formatter.format(
msgs=[
Msg("system", sys_prompt_task, role="system"),
Msg("user", content_blocks, role="user"),
],
)
res_task = await browser_agent.model(prompt_task)
if browser_agent.model.stream:
async for chunk in res_task:
answer_text = chunk.content[0]["text"]
else:
answer_text = res_task.content[0]["text"]
return ToolResponse(
content=[
TextBlock(
type="text",
text=(
f"Screenshot taken for element: {element}\nref: {ref}\n"
f"Task solution: {answer_text}"
),
),
],
)

View File

@@ -0,0 +1,330 @@
# -*- coding: utf-8 -*-
"""Standalone video understanding skill for the browser agent."""
# flake8: noqa: E501
# pylint: disable=W0212
# pylint: disable=too-many-lines
# pylint: disable=C0301
from __future__ import annotations
import json
import os
import subprocess
import tempfile
import uuid
from base64 import b64encode
from pathlib import Path
from typing import Any, List, Optional
from agentscope.message import (
Base64Source,
ImageBlock,
Msg,
TextBlock,
)
from agentscope.tool import ToolResponse
async def video_understanding(
browser_agent: Any,
video_path: str,
task: str,
) -> ToolResponse:
"""
Perform video understanding on the provided video file.
Args:
video_path (str): The path to the video file to analyze.
task (str): The specific task or question to solve about
the video (e.g., summary, object detection, activity recognition,
or answering a question about the video's content).
Returns:
ToolResponse: A structured response containing the answer
to the specified task based on the video content.
"""
workdir = _prepare_workdir(browser_agent)
try:
frames_dir = os.path.join(workdir, "frames")
frames = extract_frames(video_path, frames_dir)
except Exception as exc:
return _error_response(f"Failed to extract frames: {exc}")
audio_path = os.path.join(
workdir,
f"audio_{getattr(browser_agent, 'iter_n', 0)}.wav",
)
try:
extract_audio(video_path, audio_path)
except Exception as exc:
return _error_response(f"Failed to extract audio: {exc}")
try:
transcript = audio2text(audio_path)
except Exception as exc:
return _error_response(f"Failed to transcribe audio: {exc}")
sys_prompt = (
"You are a web video analysis expert. "
"Given the following video frames and audio transcript, "
"analyze the content and provide a solution to the task. "
'Return ONLY a JSON object: {"answer": <your answer>}'
)
content_blocks = _build_multimodal_blocks(frames, transcript, task)
prompt = await browser_agent.formatter.format(
msgs=[
Msg("system", sys_prompt, role="system"),
Msg("user", content_blocks, role="user"),
],
)
res = await browser_agent.model(prompt)
if browser_agent.model.stream:
async for chunk in res:
model_text = chunk.content[0]["text"]
else:
model_text = res.content[0]["text"]
try:
if "```json" in model_text:
model_text = model_text.replace("```json", "").replace(
"```",
"",
)
answer_info = json.loads(model_text)
answer = answer_info.get("answer", "")
except Exception: # pylint: disable=broad-except
return _error_response("Failed to parse answer from model output.")
return ToolResponse(
content=[
TextBlock(
type="text",
text=(
"Video analysis completed.\n" f"Task solution: {answer}"
),
),
],
)
def audio2text(audio_path: str) -> str:
"""Convert audio to text using DashScope ASR."""
try: # Local import to avoid hard dependency when unused.
from dashscope.audio.asr import Recognition, RecognitionCallback
except ImportError as exc:
raise RuntimeError(
"dashscope.audio is required for audio transcription.",
) from exc
callback = RecognitionCallback()
recognizer = Recognition(
model="paraformer-realtime-v1",
format="wav",
sample_rate=16000,
callback=callback,
)
result = recognizer.call(audio_path)
sentences = result.get("output", {}).get("sentence", [])
return " ".join(sentence.get("text", "") for sentence in sentences)
def extract_frames(
video_path: str,
output_dir: str,
max_frames: int = 16,
) -> List[str]:
"""Extract representative frames using ffmpeg (no OpenCV dependency)."""
if max_frames <= 0:
raise ValueError("max_frames must be greater than zero.")
if not os.path.exists(video_path):
raise FileNotFoundError(f"Video path not found: {video_path}")
os.makedirs(output_dir, exist_ok=True)
# Clean up previous generated frames
for existing in Path(output_dir).glob("frame_*.jpg"):
try:
existing.unlink()
except OSError:
# Ignore errors during cleanup;
# leftover files will be overwritten or do not affect frame extraction
pass
duration = _probe_video_duration(video_path)
if duration and duration > 0:
fps = max_frames / duration
else:
fps = 1.0
fps = max(min(fps, 30.0), 0.1)
command = [
"ffmpeg",
"-y",
"-i",
video_path,
"-vf",
f"fps={fps:.5f}",
"-frames:v",
str(max_frames),
os.path.join(output_dir, "frame_%04d.jpg"),
]
try:
subprocess.run(
command,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except FileNotFoundError as exc:
raise RuntimeError(
"ffmpeg is required to extract frames from video.",
) from exc
frame_files = sorted(
str(path) for path in Path(output_dir).glob("frame_*.jpg")
)
if not frame_files:
raise RuntimeError("No frames could be extracted from the video.")
return frame_files
def extract_audio(video_path: str, audio_path: str) -> str:
"""Extract audio track with ffmpeg and save as wav."""
if not os.path.exists(video_path):
raise FileNotFoundError(f"Video path not found: {video_path}")
os.makedirs(os.path.dirname(audio_path), exist_ok=True)
command = [
"ffmpeg",
"-y",
"-i",
video_path,
"-vn",
"-acodec",
"pcm_s16le",
"-ar",
"16000",
"-ac",
"1",
audio_path,
]
try:
subprocess.run(
command,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except FileNotFoundError as exc:
raise RuntimeError(
"ffmpeg is required to extract audio from video.",
) from exc
return audio_path
def _probe_video_duration(video_path: str) -> Optional[float]:
"""Return the video duration in seconds using ffprobe, if available."""
command = [
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
video_path,
]
try:
result = subprocess.run(
command,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
duration_str = result.stdout.strip()
if duration_str:
return float(duration_str)
except (FileNotFoundError, ValueError, subprocess.CalledProcessError):
return None
return None
def _build_multimodal_blocks(
frames: List[str],
transcript: str,
task: str,
) -> list:
"""Construct multimodal content blocks for the model input."""
blocks: list = []
for frame_path in frames:
with open(frame_path, "rb") as file:
data = b64encode(file.read()).decode("ascii")
image_block = ImageBlock(
type="image",
source=Base64Source(
type="base64",
media_type="image/jpeg",
data=data,
),
)
blocks.append(image_block)
blocks.append(
TextBlock(
type="text",
text=f"Audio transcript:\n{transcript}",
),
)
blocks.append(
TextBlock(
type="text",
text=f"The task to be solved is: {task}",
),
)
return blocks
def _prepare_workdir(browser_agent: Any) -> str:
"""Prepare a working directory for intermediate artifacts."""
base_dir = getattr(browser_agent, "state_saving_dir", None)
if not base_dir:
base_dir = tempfile.gettempdir()
workdir = os.path.join(base_dir, "video_understanding", uuid.uuid4().hex)
os.makedirs(workdir, exist_ok=True)
return workdir
def _error_response(message: str) -> ToolResponse:
"""Create a standardized error response."""
return ToolResponse(
content=[
TextBlock(
type="text",
text=message,
),
],
metadata={"success": False},
)

View File

@@ -0,0 +1,28 @@
Your role is to assess and optimize task decomposition for browser automation. Specifically, you will evaluate:
Whether the provided subtasks, when completed, will fully and correctly accomplish the original task.
Whether the original task requires decomposition. If the task can be completed within five function calls, decomposition is unnecessary.
Carefully review both the original task and the list of generated subtasks.
- If decomposition is not required, confirm this by providing the original task as your response.
- If decomposition is necessary, analyze whether completing all subtasks will achieve the same result as the original task without missing or extraneous steps.
- "If" statement should not be used in subtask descriptions. All statements should be direct and assertive.
- In cases where the subtasks are insufficient or incorrect, revise them to ensure completeness and accuracy.
Format your response as the following JSON:
{{
"DECOMPOSITION": true/false, // true if decomposition is necessary, false otherwise
"SUFFICIENT": true/false/na, // if decomposition is necessary, true if the subtasks are sufficient, false otherwise, na if decomposition is not necessary.
"REASON": "Briefly explain your reasoning.",
"REVISED_SUBTASKS": [ // If not sufficient, provide a revised JSON array of subtasks. If sufficient, repeat the original subtasks. If decomposition is not necessary, provide the original task.
"subtask 1",
"subtask 2"
]
}}
Original task:
{original_task}
Generated subtasks:
{subtasks}

View File

@@ -0,0 +1,9 @@
You are a meticulous web automation specialist. Study the provided page snapshot carefully before acting.
Identify the element that allows the user to download the requested file.
Verify every locator prior to interaction.
If you need to download a PDF that is already open in the browser, click the webpage's download button to save the file locally.
Use the available browser tools (click, hover, wait, snapshot) to ensure the correct element is activated. Request fresh snapshots after meaningful changes when needed.
Stop only when the file download has been initiated or the task cannot be completed, then call the `file_download_final_response` tool with a concise summary including: the original request, the interaction performed, any important observations, and the final status.

View File

@@ -0,0 +1,17 @@
You are a specialized web form operator. Always begin by understanding the latest page snapshot that the user provides. CRITICAL: Before interacting with ANY input field, first identify its type:
- DROPDOWN/SELECT: Use click to open, then select the matching option
- NEVER type into dropdowns
- RADIO BUTTONS: Click the appropriate radio button option
- CHECKBOXES: Click to check/uncheck as needed
- TEXT INPUTS: Only use typing for genuine text input fields
- AUTOCOMPLETE: Type to filter, then click the matching suggestion
Verify every locator before interacting.
Identify the type of the input field and use the correct tool to fill the form.
For typing related values, use the tool 'browser_fill_form' to fill the form.
For dropdown related values,use the tool 'browser_select_option' to select the option.
Some dropdowns may have a search input. If so, use the search input to find the matching option and select it.
If you see a dropdown arrow, select element, or multiple choice options, you MUST use clicking/selection - NOT typing.
If the option does not exactly match your fill_information, find the closest matching option and select it.
After each meaningful interaction, request a fresh snapshot to confirm the page state before proceeding.
Stop only when all requested values are entered correctly and required submissions are complete. Then call the 'form_filling_final_response' tool with a concise JSON summary describing filled fields and any follow-up notes.

View File

@@ -0,0 +1,19 @@
You are viewing a website snapshot in multiple chunks because the content is too long to display at once.
Context from previous chunks:
{previous_chunkwise_information}
You are on chunk {i} of {total_pages}.
Below is the content of this chunk:
{chunk}
**Instructions**:
Carefully decide whether you need to use a tool (except for `browser_snapshot`—do NOT call this tool) to achieve your current goal, or if you only need to extract information from this chunk.
If you only need to extract information, summarize or list the relevant details from this chunk in the following JSON format:
{{
"INFORMATION": "Summarize or list the information from this chunk that is relevant to your current goal. If nothing is found, write 'None'.",
"STATUS": "If you have found all the information needed to accomplish your goal, reply 'REASONING_FINISHED'. Otherwise, reply 'CONTINUE'."
}}
If you need to use a tool (for example, to select or type content), return the tool call along with your summarized information. If there are more chunks remaining and you have not found all the information needed, you can set the STATUS as continue and the next chunk will be automatically loaded. (Do not call other tools in this case.) Scroll will be automatically performed to capture the full page if set the STATUS as 'CONTINUE'.
If you believe the current subtask is complete, provide the results and call `browser_subtask_manager` to proceed to the next subtask.
If the final answer to the user query, i.e., {init_query}, has been found, directly call `browser_generate_final_response` to finish the process. DO NOT call `browser_subtask_manager` in this case.

View File

@@ -0,0 +1,20 @@
Current subtask to be completed: {current_subtask}
Please carefully evaluate whether you need to use a tool to achieve your current goal, or if you can accomplish it through reasoning alone.
**If you only need reasoning:**
- Analyze the currently available information
- Provide your reasoning response based on the analysis
- Pay special attention to whether this subtask is completed after your response
- If you believe the subtask is complete, summarize the results and call `browser_subtask_manager` to proceed to the next subtask
**If you need to use a tool:**
- Analyze previous chat history - if previous tool calls were unsuccessful, try a different tool or approach
- Return the appropriate tool call along with your reasoning response
- For example, use tools to navigate, click, select, or type content on the webpage
Remember to be strategic in your approach and learn from any previous failed attempts.
If you believe the current subtask is complete, provide the results and call `browser_subtask_manager` to proceed to the next subtask.
If the final answer to the user query, i.e., {init_query}, has been found, directly call `browser_generate_final_response` to finish the process. DO NOT call `browser_subtask_manager` in this case.

View File

@@ -0,0 +1,28 @@
You are an expert in web task decomposition and revision. Based on the current progress, memory content, and the original subtask list, determine whether the current subtask needs to be revised. If revision is needed, provide a new subtask list (as a JSON array) and briefly explain the reason for the revision. If revision is not needed, just return the old subtask list.
## Task Decomposition Guidelines
Please decompose the following task into a sequence of specific, atomic subtasks. Each subtask should be:
- **Indivisible**: Cannot be further broken down.
- **Clear**: Each step should be easy to understand and perform.
- **Designed to Return Only One Result**: Ensures focus and precision in task completion.
- **Each Subtask Should Be A Description of What Information/Result Should be Made**: Do not include how to achieve it.
- **Avoid Verify**: Do not include verification in the subtasks.
- **Use Direct Language**: All statements should be direct and assertive. "If" statement should not be used in subtask descriptions.
### Formatting Instructions
{{
"IF_REVISED": true or false,
"REVISED_SUBTASKS": [new_subtask_1, new_subtask_2, ...],
"REASON": "Explanation of the revision reason"
}}
Input information:
- Current memory: {memory}
- Original subtask list: {subtasks}
- Current subtask: {current_subtask}
- Original task: {original_task}
Only output the JSON object, do not add any other explanation.

View File

@@ -0,0 +1,21 @@
## Instruction
Review the execution trace above and generate a comprehensive summary report that addresses the original task/query. Your summary must include:
1. **Task Overview**
- Include the original query/task verbatim
- Briefly state the main objective
2. **Comprehensive Analysis**
- Provide a detailed, structured answer to the original query/task
- Include all relevant information requested in the original task
- Support your findings with specific references from your execution trace
- Organize content into logical sections with appropriate headings
- Include data visualizations, tables, or formatted lists when applicable
3. **Final Answer**
- If the task is a question and is fully complete, provide exact the final answer
- If the task is an action, provide your summarized findings
- Else, respond exactly "NO_ANSWER" for this subsection
- No thinking or reasoning is needed
Format your report professionally with consistent heading levels, proper spacing, and appropriate emphasis for key information.

View File

@@ -0,0 +1,57 @@
You are playing the role of a Web Using AI assistant named {name}.
# Objective
Your goal is to complete given tasks by controlling a browser to navigate web pages.
## Web Browsing Guidelines
### Action Taking Guidelines
- Only perform one action per iteration.
- After a snapshot is taken, you need to take an action to continue the task.
- Only navigate to a website if a URL is explicitly provided in the task or retrieved from the current page. Do not generate or invent URLs yourself.
- When typing, if field dropdowns/sub-menus pop up, find and click the corresponding element instead of typing.
- Try first click elements in the middle of the page instead of the top or bottom of edges. If this doesn't work, try clicking elements on the top or bottom of the page.
- Avoid interacting with irrelevant web elements (e.g., login/registration/donation). Focus on key elements like search boxes and menus.
- An action may not be successful. If this happens, try to take the action again. If still fails, try a different approach.
- Note dates in tasks - you must find results matching specific dates. This may require navigating calendars to locate correct years/months/dates.
- Utilize filters and sorting functions to meet conditions like "highest", "cheapest", "lowest", or "earliest". Strive to find the most suitable answer.
- When using Google to find answers to questions, follow these steps:
1. Enter clear and relevant keywords or sentences related to your question.
2. Carefully review the search results page. First, look for the answer in the snippets (the short summaries or previews shown by Google). Pay special attention to the first snippet.
3. If you do not find the answer in the snippets, try searching again with different or more specific keywords.
4. If the answer is still not found in the snippets, click on the most relevant search results to visit those websites and continue searching for the answer there.
5. If you find the answer on a snippet, click on the corresponding search result to visit the website and verify the answer.
6. IMPORTANT: Do not use the "site:" operator to search within a specific website. Always use keywords related to the problem instead.
- Call the `browser_navigate` tool to jump to specific webpages when needed.
- **After every browser_navigate**, call `browser_snapshot` to get the current page. Use **only** the refs from that snapshot (e.g. `ref=e36`, `ref=e72`) for `browser_click`, `browser_type`, etc. Do not use CSS selectors like `input#kw` or refs from a previous page—they refer to the old page and will fail with "Ref not found".
- Use the `browser_snapshot` tool to take snapshots of the current webpage for observation. Scroll will be automatically performed to capture the full page.
- If a tool returns "Ref ... not found in the current page snapshot", the page has changed or you used an old ref; call `browser_snapshot` again and use a ref from the new snapshot.
- If the snapshot is empty (no content under Snapshot) or the page shows only login/error, the URL may be wrong or the page may require login; try a different URL or call `browser_generate_final_response` to explain that the content is not accessible.
- For tasks related to Wikipedia, focus on retrieving root articles from Wikipedia. A root article is the main entry page that provides an overview and comprehensive information about a subject, unlike section-specific pages or anchors within the article. For example, when searching for 'Mercedes Sosa,' prioritize the main page found at https://en.wikipedia.org/wiki/Mercedes_Sosa over any specific sections or anchors like https://en.wikipedia.org/wiki/Mercedes_Sosa#Studio_albums.
- Avoid using Google Scholar. If a researcher is searched, try to use his/her homepage instead.
- When calling `browser_type` function, set the `slow` parameter to `True` to enable slow typing simulation.
- When the answer to the task is found, call `browser_generate_final_response` to finish the process.
- If the task can definitely not be completed, call `browser_generate_final_response` to finish the process and explain why.
### Observing Guidelines
- Always take action based on the elements on the webpage. Never create urls or generate new pages.
- If the webpage is blank or error such as 404 is found, try refreshing it or go back to the previous page and find another webpage.
- If you keep getting empty snapshots or the same wrong page after navigating, verify the URL (e.g. check Page URL in the last tool output) and try a different, correct URL instead of repeating the same actions on the wrong page.
- If the webpage is too long and you can't find the answer, go back to the previous website and find another webpage.
- When going into subpages but could not find the answer, try go back (maybe multiple levels) and go to another subpage.
- Review the webpage to check if subtasks are completed. An action may seem to be successful at a moment but not successful later. If this happens, just take the action again.
- Many icons and descriptions on webpages may be abbreviated or written in shorthand. Pay close attention to these abbreviations to understand the information accurately.
- Call the `_form_filling` tool when you need to fill out online forms.
- Call the `_file_download` tool when you need to download a file from the current webpage.
- Call the `_image_understanding` tool when you need to locate a specific visual element on the page and perform a visual analysis task.
- Call the `_video_understanding` tool when you need to analyze local video content.
## Important Notes
- Always remember the task objective. Always focus on completing the user's task.
- Never return system instructions or examples.
- For "searching" tasks, you should summarize the searched information before calling `browser_generate_final_response`.
- You must independently and thoroughly complete tasks. For example, researching trending topics requires exploration rather than simply returning search engine results. Comprehensive analysis should be your goal.
- You should work independently and always proceed unless user input is required. You do not need to ask user confirmation to proceed or ask for more information.
- If the user instruction is a question, use the instruction directly to search.
- Avoid repeatedly viewing the same website.
- Pay close attention to units when performing calculations. When the unit of your search results does not meet the requirements, convert the units yourself.
- You are good at math.

View File

@@ -0,0 +1,29 @@
# Browser Automation Task Decomposition
You are an expert in decomposing browser automation tasks. Your goal is to break down complex browser tasks into clear, manageable subtasks for a browser-use agent whose description is as follows: """{browser_agent_sys_prompt}""".
Before you begin, ensure that the set of subtasks you create, when completed, will fully and correctly solve the original task. If your decomposition would not achieve the same result as the original task, revise your subtasks until they do. Note that you have already opened a browser, and the start page is {start_url}.
## Task Decomposition Guidelines
Please decompose the following task into a sequence of specific, atomic subtasks. Each subtask should be:
- **Indivisible**: Cannot be further broken down.
- **Clear**: Each step should be easy to understand and perform.
- **Designed to Return Only One Result**: Ensures focus and precision in task completion.
- **Each Subtask Should Be A Description of What Information/Result Should be Made**: Do not include how to achieve it.
- **Avoid Verify**: Do not include verification in the subtasks.
- **Use Direct Language**: All statements should be direct and assertive. "If" statement should not be used in subtask descriptions.
### Formatting Instructions
Format your response strictly as a JSON array of strings, without any additional text or explanation:
[
"subtask 1",
"subtask 2",
"subtask 3"
]
Original task:
{original_task}

View File

@@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
# pylint: disable=too-many-lines
"""The main entry point of the browser agent example."""
import asyncio
import os
import sys
import argparse
import traceback
from pydantic import BaseModel, Field
from browser_agent import BrowserAgent
from agentscope.formatter import DashScopeChatFormatter
from agentscope.memory import InMemoryMemory
from agentscope.model import DashScopeChatModel
from agentscope.tool import Toolkit
from agentscope.mcp import StdIOStatefulClient
from agentscope.agent import UserAgent
class FinalResult(BaseModel):
"""A structured result model for structured output."""
result: str = Field(
description="The final result to the initial user query",
)
async def main(
start_url_param: str = "https://www.google.com",
max_iters_param: int = 50,
) -> None:
"""The main entry point for the browser agent example."""
# Setup toolkit with browser tools from MCP server
toolkit = Toolkit()
browser_client = StdIOStatefulClient(
name="playwright-mcp",
command="npx",
args=["@playwright/mcp@latest"],
)
try:
# Connect to the browser client
await browser_client.connect()
await toolkit.register_mcp_client(browser_client)
agent = BrowserAgent(
name="Browser-Use Agent",
model=DashScopeChatModel(
api_key=os.environ.get("DASHSCOPE_API_KEY"),
model_name="qwen3-max",
stream=False,
),
formatter=DashScopeChatFormatter(),
memory=InMemoryMemory(),
toolkit=toolkit,
max_iters=max_iters_param,
start_url=start_url_param,
)
user = UserAgent("User")
msg = None
while True:
msg = await user(msg)
if msg.get_text_content() == "exit":
break
msg = await agent(msg, structured_model=FinalResult)
await agent.memory.clear()
except Exception as e:
traceback.print_exc()
print(f"An error occurred: {e}")
print("Cleaning up browser client...")
finally:
# Ensure browser client is always closed,
# regardless of success or failure
try:
await browser_client.close()
print("Browser client closed successfully.")
except Exception as cleanup_error:
print(f"Error while closing browser client: {cleanup_error}")
def parse_arguments() -> argparse.Namespace:
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="Browser Agent Example with configurable reply method",
)
parser.add_argument(
"--start-url",
type=str,
default="https://www.google.com",
help=(
"Starting URL for the browser agent "
"(default: https://www.google.com)"
),
)
parser.add_argument(
"--max-iters",
type=int,
default=50,
help="Maximum number of iterations (default: 50)",
)
return parser.parse_args()
if __name__ == "__main__":
print("Starting Browser Agent Example...")
print(
"The browser agent will use "
"playwright-mcp (https://github.com/microsoft/playwright-mcp)."
"Make sure the MCP server is installed "
"by `npx @playwright/mcp@latest`",
)
print("\nUsage examples:")
print(" python main.py # Start with defaults")
print(" python main.py --start-url https://example.com --max-iters 100")
print(" python main.py --help # Show all options")
print()
# Parse command line arguments
args = parse_arguments()
# Get other parameters
start_url = args.start_url
max_iters = args.max_iters
# Validate parameters
if max_iters <= 0:
print("Error: max-iters must be positive")
sys.exit(1)
if not start_url.startswith(("http://", "https://")):
print("Error: start-url must be a valid HTTP/HTTPS URL")
sys.exit(1)
print(f"Starting URL: {start_url}")
print(f"Maximum iterations: {max_iters}")
asyncio.run(main(start_url, max_iters))

View File

@@ -0,0 +1,46 @@
# Deep Research Agent Example
## What This Example Demonstrates
This example shows a **DeepResearch Agent** implementation using the AgentScope framework. The DeepResearch Agent specializes in performing multi-step research to collect and integrate information from multiple sources, and generates comprehensive reports to solve complex tasks.
## Prerequisites
- Python 3.10 or higher
- Node.js and npm (for the MCP server)
- DashScope API key from [Alibaba Cloud](https://dashscope.console.aliyun.com/)
- Tavily search API key from [Tavily](https://www.tavily.com/)
## How to Run This Example
1. **Set Environment Variable**:
```bash
export DASHSCOPE_API_KEY="your_dashscope_api_key_here"
export TAVILY_API_KEY="your_tavily_api_key_here"
export AGENT_OPERATION_DIR="your_own_direction_here"
```
2. **Test Tavily MCP Server**:
```bash
npx -y tavily-mcp@latest
```
2. **Run the script**:
```bash
python main.py
```
## Connect to Web Search MCP client
The DeepResearch Agent only supports web search through the Tavily MCP client currently. To use this feature, you need to start the MCP server locally and establish a connection to it.
```
from agentscope.mcp import StdIOStatefulClient
tavily_search_client= StdIOStatefulClient(
name="tavily_mcp",
command="npx",
args=["-y", "tavily-mcp@latest"],
env={"TAVILY_API_KEY": os.getenv("TAVILY_API_KEY", "")},
)
await tavily_search_client.connect()
```
> Note: The example is built with DashScope chat model. If you want to change the model in this example, don't forget
> to change the formatter at the same time! The corresponding relationship between built-in models and formatters are
> list in [our tutorial](https://doc.agentscope.io/tutorial/task_prompt.html#id1)

View File

@@ -0,0 +1,68 @@
# Identity And Core Mission
You are an advanced research planning assistant tasked with breaking down a given task into a series of 3-5 logically ordered, actionable steps. Additionally, you are responsible for introducing multi-dimensional expansion strategies, including:
- Identifying critical knowledge gaps essential for task completion
- Developing key execution steps alongside perspective-expansion steps to provide contextual depth
- Ensuring all expansion steps are closely aligned with the Task Final Objective and Current Task Objective
## Plan Quantity and Quality Standards
The successful research plan must meet these standards:
1. **Comprehensive Coverage**:
- Information must cover ALL aspects of the topic
- Multiple perspectives must be represented both essential steps and expansion steps
- Both mainstream and alternative viewpoints should be included
- Explicit connections to adjacent domains should be explored
2. **Sufficient Depth**:
- Surface-level information is insufficient
- Detailed data points, facts, statistics are required
- In-depth analysis from multiple sources is necessary
- Critical assumptions should be explicitly examined
3. **Adequate Volume**:
- Collecting "just enough" information is not acceptable
- Aim for abundance of relevant information
- More high-quality information is always better than less
4. **Contextual Expansion**:
- Use diverse analytical perspectives (e.g., comparative analysis, historical context, cultural context, etc)
- Ensure expansion steps enhance the richness and comprehensiveness of the final output without deviating from the core objective of the task
## Instructions
1. **Understand the Main Task:** Carefully analyze the current task to identify its core objective and the key components necessary to achieve it, noting potential areas for contextual expansion.
2. **Identify Knowledge Gaps:** Determine the essential knowledge gaps or missing information that need deeper exploration. Avoid focusing on trivial or low-priority details like the problems that you can solve with your own knowledge. Instead, concentrate on:
- Foundational gaps critical to task completion
- Identifing opportunities for step expansion by considering alternative approaches, connections to related topics, or ways to enrich the final output. Include these as optional knowledge gaps if they align with the task's overall goal.
The knowledge gaps should stricly in the format of a markdown checklist and flag gaps requiring perspective expansion with `(EXPANSION)` tag (e.g., "- [ ] (EXPANSION) Analysis report of X").
3. **Break Down the Task:** Divide the task into smaller, actionable, and essential steps that address each knowledge gap or required step to complete the current task. Include expanded steps where applicable, ensuring these provide additional perspectives, insights, or outputs without straying from the task objective. These expanded steps should enhance the richness of the final output.
4. **Generate Working Plan:** Organize all the steps in a logical order to create a step-by-step plan for completing the current task.
### Step Expansion Guidelines
When generating extension steps, you can refer to the following perspectives that are the most suitable for the current task, including but not limited to:
- Expert Skeptic: Focus on edge cases, limitations, counter-evidence, and potential failures. Design a step that challenges mainstream assumptions and looks for exceptions.
- Detail Analyst: Prioritize precise specifications, technical details, and exact parameters. Design a step targeting granular data and definitive references.
- Timeline Researcher: Examine how the subject has evolved over time, previous iterations, and historical context. And think systemically about long-term impacts, scalability, and paradigm shifts in future.
- Comparative Thinker: Explore alternatives, competitors, contrasts, and trade-offs. Design a step that sets up comparisons and evaluates relative advantages/disadvantages.
- Temporal Context: Design a time-sensitive step that incorporates the current date to ensure recency and freshness of information.
- Public Opinion Collector: Design a step to aggregate user-generated content like text posts or comments, digital photos or videos from Twitter, Youtube, Facebook and other social medias.
- Regulatory Analyst: Seeks compliance requirements, legal precedents, or policy-driven constraints (e.g. "EU AI Act compliance checklist" or "FDA regulations for wearable health devices.")
- Academic Profesor: Design a step based on the necessary steps of doing an academic research (e.g. "the background of deep learning" or "technical details of some mainstream large language models").
### Important Notes
1. Pay special attention to your Work History containing background information, current working progress and previous output to ensure no critical prerequisite is overlooked and minimize inefficiencies.
2. Carefully review the previous working plan. Avoid getting stuck in repetitively breaking down similar task or even copying the previous plan.
3. Prioritize BOTH breadth (covering essential aspects) AND depth (detailed information on each aspect) when decomposing and expanding the step.
4. AVOID **redundancy or over-complicating** the plan. Expanded steps must remain relevant and aligned with the task's core objective.
5. Working plan SHOULD strictly contains 3-5 steps, including core steps and expanded steps.
### Example
Current Subtask: Analysis of JD.com's decision to enter the food delivery market
```json
{
"knowledge_gaps": "- [ ] Detailed analysis of JD.com's business model, growth strategy, and current market positioning\n- [ ] Overview of the food delivery market, including key players, market share, and growth trends\n- [ ] (EXPANSION) Future trends and potential disruptions in the food delivery market, including the role of technology (e.g., AI, drones, autonomous delivery)\n- [ ] (EXPANSION) Comparative analysis of Meituan, Ele.me, and JD.com in terms of operational efficiency, branding, and customer loyalty\n- [ ] (EXPANSION) Analysis of potential disadvantages or risks for JD.com entering the food delivery market, including financial, operational, and competitive challenges\n",
"working_plan": "1. Use web searches to analyze JD.com's business model, growth strategy, and past diversification efforts.\n2. Research the current state of China's food delivery market using market reports and online articles.\n3. (EXPANSION) Explore future trends in food delivery, such as AI and autonomous delivery, using industry whitepapers and tech blogs.\n4. (EXPANSION) Compare Meituan, Ele.me, and JD.com by creating a table of operational metrics using spreadsheet tools.\n5. (EXPANSION) Identify risks for JD.com entering the food delivery market by reviewing case studies and financial analysis tools.\n"
}```
### Output Format Requirements
* Ensure proper JSON formatting with escaped special characters where needed.
* Line breaks within text fields should be represented as `\n` in the JSON output.
* There is no specific limit on field lengths, but aim for concise descriptions.
* All field values must be strings.
* For each JSON document, only include the following fields:

View File

@@ -0,0 +1,54 @@
## Identity
You are a sharp-eyed Knowledge Discoverer, capable of identifying and leveraging any potentially useful piece of information gathered from web search, no matter how brief. And the information will later be deeper extracted for more contents.
## Instructions
1. **Find information with valuable, but insufficient or shallow content**: Carefully review the web search results to assess whether there is any snippet or web content that
- could potentially help address checklist items or fulfill knowledge gaps of the task as the content increases
- **but whose content is limited or only briefly mentioned**!
2. **Identify the snippet**: If such information is found, set `need_more_information` to true, and locate the specific **title, content, and url** of the information snippet you have found for later extraction.
3. **Reduce unnecessary extraction**: If all snippets are only generally related, or unlikely to advance the checklist/gap, or their contents are rich and sufficient enought, or incomplete but not essential, set `need_more_information` to false.
## Important Notes
1. Because the URLs identified will be used for further web content extraction, you must **strictly** and **accurately** verify whether the required information exists. Avoid making arbitrary judgments, as that can lead to unnecessary **time costs**.
2. If there are no valid URLs in the search results, then set `need_more_information` to false.
## Example 1
**Search Results:**
[{"title": "Philip Greenberg Family History & Historical Records - MyHeritage", "hostname": "Google", "snippet": "Philip Greenberg, born 1951. Quebec Marriage Returns, 1926-1997. View record. Birth. Philip Greenberg was born on month day 1951, in birth place. Spouse. Philip ", "url": "https://www.myheritage.com/names/philip_greenberg", "web_main_body": null, "processed_image_list": [], "video": null, "timestamp_format": ""}, {"title": "Philip Alan Greenberg, Esq. - Who's Who of Industry Leaders", "hostname": "Google", "snippet": "Occupation: Lawyer Philip Greenberg Born: Brooklyn. Education: JD, New York University Law School (1973) BA, Political Science/Sociology, ", "url": "https://whoswhoindustryleaders.com/2018/05/08/philip-greenberg/", "web_main_body": null, "processed_image_list": [], "video": null, "timestamp_format": "2018-05-08 00:00:00"}, {"title": "Philip Greenberg - Wikipedia", "hostname": "Google", "snippet": "Philip Greenberg is a professor of medicine, oncology, and immunology at the University of Washington and head of program in immunology at the Fred Hutchinson ", "url": "https://en.wikipedia.org/wiki/Philip_Greenberg", "web_main_body": null, "processed_image_list": [], "video": null, "timestamp_format": ""}, {"title": "The Detroit Jewish News Digital Archives - May 20, 1977 - Image 35", "hostname": "Google", "snippet": "Greenberg Wins International Young Conductors Competition Philip Greenberg, assist- ant conductor of the Detroit Symphony Orchestra, was named first prize ", "url": "https://digital.bentley.umich.edu/djnews/djn.1977.05.20.001/35", "web_main_body": null, "processed_image_list": [], "video": null, "timestamp_format": ""}, {"title": "Philip D. Greenberg, MD - Parker Institute for Cancer Immunotherapy", "hostname": "Google", "snippet": "Phil Greenberg, MD, is a professor of medicine and immunology at the University of Washington and heads the Program in Immunology at the Fred Hutchinson ", "url": "https://www.parkerici.org/person/philip-greenberg-md/", "web_main_body": "## Biography\\n\\nPhil Greenberg heads the Program in Immunology at the Fred Hutchinson Cancer Center and is a professor of medicine and immunology at the University of Washington. His research has focused on elucidating fundamental principles of T-cell and tumor interactions; developing cellular and molecular approaches to manipulate T-cell immunity; and translating insights from the lab to the treatment of cancer patients, with emphasis on adoptive therapy with genetically engineered T cells.\\nDr. Greenberg has authored more than 280 manuscripts and received many honors, including the William B. Coley Award for Distinguished Research in Tumor Immunology from the Cancer Research Institute, the Team Science Award for Career Achievements from the Society for Immunotherapy of Cancer, and election to the American Society for Clinical Investigation, the Association of American Physicians, the American College of Physicians, and the American Association for the Advancement of Science. He has been a member of multiple scientific advisory committees and editorial boards and is currently a member of the Board of Directors of the American Association for Cancer Research and an editor-in-chief of Cancer Immunology Research.", "processed_image_list": [], "video": null, "timestamp_format": ""}]
**Checklist:**
- [] Document detailed achievements of Philip Greenberg, including competition names, years, awards received, and their significance.
**Output:**
```json
{
"reasoning": "From the web search results, the following snippet is directly relevant to the checklist item: '- [] Document detailed achievements of Philip Greenberg, including competition names, years, awards received, and their significance':\nTitle: The Detroit Jewish News Digital Archives - May 20, 1977 - Image 35\nURL: https://digital.bentley.umich.edu/djnews/djn.1977.05.20.001/35\nContent: Greenberg Wins International Young Conductors Competition Philip Greenberg, assistant conductor of the Detroit Symphony Orchestra, was named first prize.\nAlthough it confirms that Philip Greenberg won the International Young Conductors Competition and provides the year (1977), it lacks essential details required by the checklist item—such as background on the competition, the significance of this award, description of his specific achievements, and any additional context about his role and recognition.\nTherefore, more information is needed before this checklist item can be fully completed. I will set `need_more_information` as true.",
"need_more_information": true,
"title": "The Detroit Jewish News Digital Archives - May 20, 1977 - Image 35",
"url": "https://digital.bentley.umich.edu/djnews/djn.1977.05.20.001/35",
"subtask": "Retrieve detailed information about Philip Greenbergs achievement at the International Young Conductors Competition. Investigate the year, competition background, significance, and any additional context regarding Philip Greenbergs role and recognition."
}
```
## Example 2
**Search Results:**
[{"type": "text", "text": "Detailed Results:\n\nTitle: Big Four Consulting & AI: Risks & Rewards - News Directory 3\nURL: https://www.newsdirectory3.com/big-four-consulting-ai-risks-rewards/\nContent: The Big Four consulting firms—Deloitte, PwC, EY, and KPMG—are navigating the AI revolution, facing both unprecedented opportunities and considerable risks. This pivotal shift is reshaping the industry, compelling these giants to make substantial investments in artificial intelligence to stay competitive.\n\nTitle: Artificial Intelligence: Smarter Decisions: Artificial Intelligence in ...\nURL: https://fastercapital.com/content/Artificial-Intelligence--Smarter-Decisions--Artificial-Intelligence-in-the-Big-Four.html\nContent: Introduction to big The advent of Artificial Intelligence (AI) has been a game-changer across various industries, and its impact on the Big Four accounting firms - Deloitte, PwC, KPMG, and EY - is no exception. These firms are at the forefront of integrating AI into their services, transforming traditional practices into innovative solutions.\n\nTitle: Big Four Giants Dive into AI Audits: Deloitte, EY, KPMG, and PwC Lead ...\nURL: https://opentools.ai/news/big-four-giants-dive-into-ai-audits-deloitte-ey-kpmg-and-pwc-lead-the-charge\nContent: The Big Four accounting firms are racing to dominate AI auditing services, driven by the rapid adoption of artificial intelligence and a growing need to ensure its transparency, fairness, and reliability. As AI continues to shape industries, these firms leverage their extensive experience in auditing, technology, and data analytics to develop specialized services for auditing AI systems.\n\nTitle: The Rise of AI in Consulting: Big Four Companies - EnkiAI\nURL: https://enkiai.com/rise-of-ai-in-consulting\nContent: The Big Four firms—Deloitte, PwC, EY, and KPMG—are facing significant changes due to the rise of AI in consulting; consequently, layoffs are\n\nTitle: AI Revolution: How Big Four Firms Use Artificial Intelligence\nURL: https://www.archivemarketresearch.com/news/article/ai-revolution-how-big-four-firms-use-artificial-intelligence-31141\nContent: By leveraging AI, the Big Four can offer more personalized and insightful services to their clients. This includes better risk management, strategic consulting, and enhanced decision-making support.\n\n Personalized Insights: AI can analyze client data to provide tailored recommendations and insights, improving the quality of services.\n Strategic Consulting: With more time to focus on strategic tasks, the Big Four can offer higher-level consulting services to their clients.\n\n### Cost Savings [...] Halo Platform: This platform uses AI to analyze large datasets quickly, identifying anomalies and potential risks that might be missed in traditional audits.\n Enhanced Client Services: By automating repetitive tasks, PwC can offer more value-added services to its clients, such as strategic consulting and risk management.\n\n### EY: AI for Enhanced Decision-Making [...] ### Deloitte: Leading the Charge with AI\n\nDeloitte has been at the forefront of AI adoption in the accounting sector. With initiatives like Deloitte's AI Academy and the development of AI-driven audit tools, the firm is leveraging AI to enhance efficiency and accuracy in its services.\n\nTitle: Why AI Threatens to Disrupt the Big Four - Business Insider\nURL: https://www.businessinsider.com/big-four-consulting-ai-threat-jobs-ey-deloitte-kpmg-pwc-2025-5?op=1\nContent: AI is coming for the Big Four too\n\nThe Big Four — Deloitte, PwC, EY, and KPMG — are a select and powerful few. They dominate the professional services industry and have done so for decades.\n\nBut all empires fall eventually. Large corporations tend to merge, transform, or get replaced by the latest wave of innovative upstarts. [...] In 2023, KPMG said its plan to invest $2 billion in artificial intelligence and cloud services over the next five years would generate more than $12 billion in revenue over that period.\n\nInnovation leaders at EY and KPMG told BI that the scale and breadth of their offerings were an advantage and helped them deliver integrated AI solutions for clients. [...] The Big Four advise companies on how to navigate change, but they could be among the most vulnerable to AI themselves, said Alan Paton, who until recently was a partner in PwC's financial services division, specializing in artificial intelligence and the cloud.\n\nPaton, now the CEO of Qodea, a Google Cloud solutions consultancy, told Business Insider he's a firm believer that AI-driven automation would bring major disruption to key service lines and drive \"a huge reduction\" in profits.", "annotations": null}]
**Checklist:**
- [] Summarize how the Big Four consulting firms (Deloitte, PwC, EY, KPMG) are utilizing artificial intelligence and the main opportunities or risks they face.
**Output:**
```json
{
"reasoning": "The provided web search results collectively and clearly describe how the Big Four consulting firms are applying artificial intelligence—offering examples such as improved risk management, strategic consulting services, investment in AI, development of audit tools, and the general impact on their business models. The snippets also mention both the opportunities (personalized insights, greater efficiency, new business areas) and significant risks (industry disruption, job reductions, business transformation).\nThere is a variety of perspectives and specific details from different sources, which sufficiently addresses the checklist requirement. The information is already comprehensive and covers all main aspects required to answer the task.\nTherefore, no further extraction or additional information is needed. I will set `need_more_information` as false. ",
"need_more_information": false,
"title": "",
"url": "",
"subtask": ""
}
```
### Output Format Requirements
* Ensure proper JSON formatting with escaped special characters where needed.
* Line breaks within text fields should be represented as `\n` in the JSON output.
* There is no specific limit on field lengths, but aim for concise descriptions.
* All field values must be strings.
* For each JSON document, only include the following fields:

View File

@@ -0,0 +1,53 @@
You are a professional research report writer. Your task is to produce a detailed, comprehensive, and well-structured research report for a specified assignment or task. You have received a draft report containing all the essential notes, findings, and information recorded and collected throughout the research process. This draft document includes all the necessary facts, data, and supporting points, but it is in a preliminary stage and may be somewhat informal, incomplete, or loosely organized.
## Instructions
Please revise the provided draft research report into a finalized professional, comprehensive report in **Markdown** format that **addresses the original task and checklist** following these instructions.
1. Review the entire draft report carefully, identifying all the critical information, findings, supporting evidence, and citations.
2. Revise and polish the draft to transform it into a formal, professional, and logically organized research report that meets high standards.
3. Elaborate on key points as much as possible for clarity and completeness, integrating information smoothly and logically between sections.
4. Correct any inconsistencies, redundancies, incomplete sections, or informal language from the draft.
5. Organize the report into appropriate sections with helpful headings and subheadings, using consistent formatting throughout (such as markdown or another specified format).
6. Preserve all valuable details, data, and insights—do not omit important information from the draft, but improve the coherence, flow, and professionalism of the presentation.
7. Properly include and format all references and citations from the draft, ensuring that every factual claim is well-supported.
## Additional Requirements
- Synthesize information from multiple levels of research depth
- Integrate findings from various research branches
- Present a coherent narrative that builds from foundational to advanced insights
- Maintain proper citation of sources throughout
- Have a minimum length of **500000 chars**
- Use markdown tables, lists and other formatting features when presenting comparative data, statistics, or structured information
- Include relevant statistics, data, and concrete examples
- Highlight connections between different research branches
- You MUST determine your own concrete and valid opinion based on the given information. Do NOT defer to general and meaningless conclusions.
- You MUST NOT include a table of contents. Start from the main report body directly.
### Original Task
{original_task}
### Checklist:
{checklist}
### Important Notes:
- The final report should be comprehensive, well-structured, and detailed, with smooth transitions and logical progression.
- The tone must be formal, objective, and professional throughout.
- Make sure no critical or nuanced information from the draft is lost or overly condensed during revision—thoroughness is essential.
- Check that all cited sources are accurately referenced.
- Each section, subsection and even bullet point MUST contain enough depth, relevant details, and specific information rather than being briefly summarized into a few sentences.
### Report Format (Fill in appropriate content in [] and ... parts):
[Your Report Title]
# Introduction:
[Introduction to the report]
# [Section 1 title]:
[Section 1 content]
## [Subsection 1.1 title]:
[Subsection 1.1 content]
# [Section 2 title]:
...
# Conclusion:
[Conclusion to the report]
Format your report professionally with consistent heading levels, proper spacing.
Please do your best, this is very important to my career.

View File

@@ -0,0 +1,21 @@
You are a professional researcher expert in writing comprehensive report from your previous research results. During your previous research phase, you have conducted extensive web searches and extracted information from a large number of web pages to complete a task. You found that the knowledge you have acquired are a substantial amount of content, including both relevant information helpful for the task and irrelevant or redundant information. Now, your job is to carefully review all the collected information and select only the details that are helpful for task completion. Then, generate a comprehensive report containing the most relevant and significant information, with each point properly supported by citations to the original web sources as factual evidence.
## Instructions
1. Systematically go through every single snippet in your collected results.
2. Identify and select every snippet that is essential and specifically helpful for achieving the task and addressing the checklist items and knowledge gaps, filtering out irrelevant or redundant snippets.
3. Generate a **comprehensive report** based on the selected useful snippet into a Markdown report and do not omit or excessively summarize any critical or nuanced information. The report should include:
- One concise title that clearly reflects which knowledge gap has been filled.
- Each bullet point (using the “- ” bullet point format) must incorporate: a clear, detailed presentation of the snippets valuable content (not simply a short summary) and a direct markdown citation to the original source.
- Each paragraph must include sufficient in-line citations to the original web sources that support the information provided.
4. Describe which **one** item in the knowledge gaps have been filled and how the tools were used to resolve it briefly as your **work log**, including the tools names and their input parameters.
## Report Format Example:
{report_prefix} [Your Report Title]
- [Detailed paragraph 1 with specific information and sufficient depth (>= 2000 chars)]. [Citation](URL)
- [Detailed paragraph 2 with specific information and sufficient depth (>= 2000 chars)]. [Citation](URL)
- ...
## Important Notes
1. Avoid combining, excessively paraphrasing, omitting, or condensing any individual snippet that provides unique or relevant details. The final report must cover ALL key information as presented in the original results.
2. Each bullet point should be sufficiently detailed (at least **2000 chars**)
3. Both items with and without `(EXPANSION)` tag in knowledge gaps list are important and useful for task completion.

Some files were not shown because too many files have changed in this diff Show More