Compare commits

..

3 Commits

Author SHA1 Message Date
Greyson LaLonde
aec7ad9731 chore: remove old a2a cassettes from lib/crewai (moved in previous commit) 2026-04-02 04:11:43 +08:00
Greyson LaLonde
bba48ec9df refactor: pin crewai-a2a version and move a2a tests to standalone package
Pin crewai-a2a to 1.13.0a6 to match workspace versioning convention.
Move all a2a tests and cassettes from lib/crewai to lib/crewai-a2a,
add crewai-a2a to devtools bump tooling, and update pytest/ruff/mypy configs.
2026-04-02 04:10:13 +08:00
Greyson LaLonde
2ada20e9c6 refactor: extract a2a module into standalone workspace package
Move lib/crewai/src/crewai/a2a to lib/crewai-a2a/src/crewai_a2a as a
separate workspace member. crewai's `[a2a]` optional extra now pulls in
crewai-a2a instead of listing raw deps. All imports updated from
crewai.a2a.* to crewai_a2a.*. Handle PydanticUndefinedAnnotation in
crewai/__init__.py model_rebuild to break circular import at load time.
2026-04-02 02:32:14 +08:00
123 changed files with 503 additions and 8060 deletions

View File

@@ -19,7 +19,7 @@ repos:
language: system
pass_filenames: true
types: [python]
exclude: ^(lib/crewai/src/crewai/cli/templates/|lib/crewai/tests/|lib/crewai-tools/tests/|lib/crewai-files/tests/)
exclude: ^(lib/crewai/src/crewai/cli/templates/|lib/crewai/tests/|lib/crewai-tools/tests/|lib/crewai-files/tests/|lib/crewai-a2a/tests/)
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.9.3
hooks:

View File

@@ -59,7 +59,11 @@ if _original_from_serialized_response is not None:
request: Any, serialized_response: Any, history: Any = None
) -> Any:
"""Patched version that ensures response._content is properly set."""
response = _original_from_serialized_response(request, serialized_response, history)
if not _original_from_serialized_response:
return None
response = _original_from_serialized_response(
request, serialized_response, history
)
# Explicitly set _content to avoid ResponseNotRead errors
# The content was passed to the constructor but the mocked read() prevents
# proper initialization of the internal state
@@ -255,7 +259,7 @@ def vcr_cassette_dir(request: Any) -> str:
for parent in test_file.parents:
if (
parent.name in ("crewai", "crewai-tools", "crewai-files")
parent.name in ("crewai", "crewai-tools", "crewai-files", "crewai-a2a")
and parent.parent.name == "lib"
):
package_root = parent

View File

@@ -4,28 +4,6 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
icon: "clock"
mode: "wide"
---
<Update label="2 أبريل 2026">
## v1.13.0a7
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a7)
## ما الذي تغير
### الميزات
- إضافة امتداد A2UI مع دعم v0.8/v0.9، والمخططات، والوثائق
### إصلاحات الأخطاء
- إصلاح بادئات الرؤية متعددة الأنماط عن طريق إضافة GPT-5 وسلسلة o
### الوثائق
- تحديث سجل التغييرات والإصدار لـ v1.13.0a6
## المساهمون
@alex-clawd, @greysonlalonde, @joaomdmoura
</Update>
<Update label="1 أبريل 2026">
## v1.13.0a6

View File

@@ -5,14 +5,6 @@ icon: wrench
mode: "wide"
---
### شاهد: بناء Agents و Flows في CrewAI باستخدام Coding Agent Skills
قم بتثبيت مهارات وكيل البرمجة الخاصة بنا (Claude Code، Codex، ...) لتشغيل وكلاء البرمجة بسرعة مع CrewAI.
يمكنك تثبيتها باستخدام `npx skills add crewaiinc/skills`
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
## فيديو تعليمي
شاهد هذا الفيديو التعليمي لعرض تفصيلي لعملية التثبيت:

View File

@@ -16,14 +16,6 @@ mode: "wide"
مع أكثر من 100,000 مطور معتمد عبر دوراتنا المجتمعية، يُعد CrewAI المعيار لأتمتة الذكاء الاصطناعي الجاهزة للمؤسسات.
### شاهد: بناء Agents و Flows في CrewAI باستخدام Coding Agent Skills
قم بتثبيت مهارات وكيل البرمجة الخاصة بنا (Claude Code، Codex، ...) لتشغيل وكلاء البرمجة بسرعة مع CrewAI.
يمكنك تثبيتها باستخدام `npx skills add crewaiinc/skills`
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
## بنية CrewAI المعمارية
صُممت بنية CrewAI لتحقيق التوازن بين الاستقلالية والتحكم.

View File

@@ -5,14 +5,6 @@ icon: rocket
mode: "wide"
---
### شاهد: بناء Agents و Flows في CrewAI باستخدام Coding Agent Skills
قم بتثبيت مهارات وكيل البرمجة الخاصة بنا (Claude Code، Codex، ...) لتشغيل وكلاء البرمجة بسرعة مع CrewAI.
يمكنك تثبيتها باستخدام `npx skills add crewaiinc/skills`
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
## ابنِ أول وكيل CrewAI
لننشئ طاقماً بسيطاً يساعدنا في `البحث` و`إعداد التقارير` عن `أحدث تطورات الذكاء الاصطناعي` لموضوع أو مجال معين.

View File

@@ -2237,9 +2237,7 @@
"en/learn/using-annotations",
"en/learn/execution-hooks",
"en/learn/llm-hooks",
"en/learn/tool-hooks",
"en/learn/a2a-agent-delegation",
"en/learn/a2ui"
"en/learn/tool-hooks"
]
},
{
@@ -3176,9 +3174,7 @@
"en/learn/using-annotations",
"en/learn/execution-hooks",
"en/learn/llm-hooks",
"en/learn/tool-hooks",
"en/learn/a2a-agent-delegation",
"en/learn/a2ui"
"en/learn/tool-hooks"
]
},
{

View File

@@ -4,28 +4,6 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="Apr 02, 2026">
## v1.13.0a7
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a7)
## What's Changed
### Features
- Add A2UI extension with v0.8/v0.9 support, schemas, and docs
### Bug Fixes
- Fix multimodal vision prefixes by adding GPT-5 and o-series
### Documentation
- Update changelog and version for v1.13.0a6
## Contributors
@alex-clawd, @greysonlalonde, @joaomdmoura
</Update>
<Update label="Apr 01, 2026">
## v1.13.0a6

View File

@@ -5,14 +5,6 @@ icon: wrench
mode: "wide"
---
### Watch: Building CrewAI Agents & Flows with Coding Agent Skills
Install our coding agent skills (Claude Code, Codex, ...) to quickly get your coding agents up and running with CrewAI.
You can install it with `npx skills add crewaiinc/skills`
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
## Video Tutorial
Watch this video tutorial for a step-by-step demonstration of the installation process:

View File

@@ -16,14 +16,6 @@ It empowers developers to build production-ready multi-agent systems by combinin
With over 100,000 developers certified through our community courses, CrewAI is the standard for enterprise-ready AI automation.
### Watch: Building CrewAI Agents & Flows with Coding Agent Skills
Install our coding agent skills (Claude Code, Codex, ...) to quickly get your coding agents up and running with CrewAI.
You can install it with `npx skills add crewaiinc/skills`
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
## The CrewAI Architecture
CrewAI's architecture is designed to balance autonomy with control.

View File

@@ -1,344 +0,0 @@
---
title: Agent-to-UI (A2UI) Protocol
description: Enable agents to generate declarative UI surfaces for rich client rendering via the A2UI extension.
icon: window-restore
mode: "wide"
---
## A2UI Overview
A2UI is a declarative UI protocol extension for [A2A](/en/learn/a2a-agent-delegation) that lets agents emit structured JSON messages describing interactive surfaces. Clients receive these messages and render them as rich UI components — forms, cards, lists, modals, and more — without the agent needing to know anything about the client's rendering stack.
A2UI is built on the A2A extension mechanism and identified by the URI `https://a2ui.org/a2a-extension/a2ui/v0.8`.
<Note>
A2UI requires the `a2a-sdk` package. Install with: `uv add 'crewai[a2a]'` or `pip install 'crewai[a2a]'`
</Note>
## How It Works
1. The **server extension** scans agent output for A2UI JSON objects
2. Valid messages are wrapped as `DataPart` entries with the `application/json+a2ui` MIME type
3. The **client extension** augments the agent's system prompt with A2UI instructions and the component catalog
4. The client tracks surface state (active surfaces and data models) across conversation turns
## Server Setup
Add `A2UIServerExtension` to your `A2AServerConfig` to enable A2UI output:
```python Code
from crewai import Agent
from crewai.a2a import A2AServerConfig
from crewai.a2a.extensions.a2ui import A2UIServerExtension
agent = Agent(
role="Dashboard Agent",
goal="Present data through interactive UI surfaces",
backstory="Expert at building clear, actionable dashboards",
llm="gpt-4o",
a2a=A2AServerConfig(
url="https://your-server.com",
server_extensions=[A2UIServerExtension()],
),
)
```
### Server Extension Options
<ParamField path="catalog_ids" type="list[str] | None" default="None">
Component catalog identifiers the server supports. When set, only these catalogs are advertised to clients.
</ParamField>
<ParamField path="accept_inline_catalogs" type="bool" default="False">
Whether to accept inline catalog definitions from clients in addition to named catalogs.
</ParamField>
## Client Setup
Add `A2UIClientExtension` to your `A2AClientConfig` to enable A2UI rendering:
```python Code
from crewai import Agent
from crewai.a2a import A2AClientConfig
from crewai.a2a.extensions.a2ui import A2UIClientExtension
agent = Agent(
role="UI Coordinator",
goal="Coordinate tasks and render agent responses as rich UI",
backstory="Expert at presenting agent output in interactive formats",
llm="gpt-4o",
a2a=A2AClientConfig(
endpoint="https://dashboard-agent.example.com/.well-known/agent-card.json",
client_extensions=[A2UIClientExtension()],
),
)
```
### Client Extension Options
<ParamField path="catalog_id" type="str | None" default="None">
Preferred component catalog identifier. Defaults to `"standard (v0.8)"` when not set.
</ParamField>
<ParamField path="allowed_components" type="list[str] | None" default="None">
Restrict which components the agent may use. When `None`, all 18 standard catalog components are available.
</ParamField>
## Message Types
A2UI defines four server-to-client message types. Each message targets a **surface** identified by `surfaceId`.
<Tabs>
<Tab title="beginRendering">
Initializes a new surface with a root component and optional styles.
```json
{
"beginRendering": {
"surfaceId": "dashboard-1",
"root": "main-column",
"catalogId": "standard (v0.8)",
"styles": {
"primaryColor": "#EB6658"
}
}
}
```
</Tab>
<Tab title="surfaceUpdate">
Sends or updates one or more components on an existing surface.
```json
{
"surfaceUpdate": {
"surfaceId": "dashboard-1",
"components": [
{
"id": "main-column",
"component": {
"Column": {
"children": { "explicitList": ["title", "content"] },
"alignment": "start"
}
}
},
{
"id": "title",
"component": {
"Text": {
"text": { "literalString": "Dashboard" },
"usageHint": "h1"
}
}
}
]
}
}
```
</Tab>
<Tab title="dataModelUpdate">
Updates the data model bound to a surface, enabling dynamic content.
```json
{
"dataModelUpdate": {
"surfaceId": "dashboard-1",
"path": "/data/model",
"contents": [
{
"key": "userName",
"valueString": "Alice"
},
{
"key": "score",
"valueNumber": 42
}
]
}
}
```
</Tab>
<Tab title="deleteSurface">
Removes a surface and all its components.
```json
{
"deleteSurface": {
"surfaceId": "dashboard-1"
}
}
```
</Tab>
</Tabs>
## Component Catalog
A2UI ships with 18 standard components organized into three categories:
### Content
| Component | Description | Required Fields |
|-----------|-------------|-----------------|
| **Text** | Renders text with optional heading/body hints | `text` (StringBinding) |
| **Image** | Displays an image with fit and size options | `url` (StringBinding) |
| **Icon** | Renders a named icon from a set of 47 icons | `name` (IconBinding) |
| **Video** | Embeds a video player | `url` (StringBinding) |
| **AudioPlayer** | Embeds an audio player with optional description | `url` (StringBinding) |
### Layout
| Component | Description | Required Fields |
|-----------|-------------|-----------------|
| **Row** | Horizontal flex container | `children` (ChildrenDef) |
| **Column** | Vertical flex container | `children` (ChildrenDef) |
| **List** | Scrollable list (vertical or horizontal) | `children` (ChildrenDef) |
| **Card** | Elevated container for a single child | `child` (str) |
| **Tabs** | Tabbed container | `tabItems` (list of TabItem) |
| **Divider** | Visual separator (horizontal or vertical) | — |
| **Modal** | Overlay triggered by an entry point | `entryPointChild`, `contentChild` (str) |
### Interactive
| Component | Description | Required Fields |
|-----------|-------------|-----------------|
| **Button** | Clickable button that triggers an action | `child` (str), `action` (Action) |
| **CheckBox** | Boolean toggle with a label | `label` (StringBinding), `value` (BooleanBinding) |
| **TextField** | Text input with type and validation options | `label` (StringBinding) |
| **DateTimeInput** | Date and/or time picker | `value` (StringBinding) |
| **MultipleChoice** | Selection from a list of options | `selections` (ArrayBinding), `options` (list) |
| **Slider** | Numeric range slider | `value` (NumberBinding) |
## Data Binding
Components reference values through **bindings** rather than raw literals. This allows surfaces to update dynamically when the data model changes.
There are two ways to bind a value:
- **Literal values** — hardcoded directly in the component definition
- **Path references** — point to a key in the surface's data model
```json
{
"surfaceUpdate": {
"surfaceId": "profile-1",
"components": [
{
"id": "greeting",
"component": {
"Text": {
"text": { "path": "/data/model/userName" },
"usageHint": "h2"
}
}
},
{
"id": "status",
"component": {
"Text": {
"text": { "literalString": "Online" },
"usageHint": "caption"
}
}
}
]
}
}
```
In this example, `greeting` reads the user's name from the data model (updated via `dataModelUpdate`), while `status` uses a hardcoded literal.
## Handling User Actions
Interactive components like `Button` trigger `userAction` events that flow back to the server. Each action includes a `name`, the originating `surfaceId` and `sourceComponentId`, and an optional `context` with key-value pairs.
```json
{
"userAction": {
"name": "submitForm",
"surfaceId": "form-1",
"sourceComponentId": "submit-btn",
"timestamp": "2026-03-12T10:00:00Z",
"context": {
"selectedOption": "optionA"
}
}
}
```
Action context values can also use path bindings to send current data model values back to the server:
```json
{
"Button": {
"child": "confirm-label",
"action": {
"name": "confirm",
"context": [
{
"key": "currentScore",
"value": { "path": "/data/model/score" }
}
]
}
}
}
```
## Validation
Use `validate_a2ui_message` to validate server-to-client messages and `validate_a2ui_event` for client-to-server events:
```python Code
from crewai.a2a.extensions.a2ui import validate_a2ui_message
from crewai.a2a.extensions.a2ui.validator import (
validate_a2ui_event,
A2UIValidationError,
)
# Validate a server message
try:
msg = validate_a2ui_message({"beginRendering": {"surfaceId": "s1", "root": "r1"}})
except A2UIValidationError as exc:
print(exc.errors)
# Validate a client event
try:
event = validate_a2ui_event({
"userAction": {
"name": "click",
"surfaceId": "s1",
"sourceComponentId": "btn-1",
"timestamp": "2026-03-12T10:00:00Z",
}
})
except A2UIValidationError as exc:
print(exc.errors)
```
## Best Practices
<CardGroup cols={2}>
<Card title="Start Simple" icon="play">
Begin with a `beginRendering` message and a single `surfaceUpdate`. Add data binding and interactivity once the basic flow works.
</Card>
<Card title="Use Data Binding for Dynamic Content" icon="arrows-rotate">
Prefer path bindings over literal values for content that changes. Use `dataModelUpdate` to push new values without resending the full component tree.
</Card>
<Card title="Filter Components" icon="filter">
Use the `allowed_components` option on `A2UIClientExtension` to restrict which components the agent may emit, reducing prompt size and keeping output predictable.
</Card>
<Card title="Validate Messages" icon="check">
Use `validate_a2ui_message` and `validate_a2ui_event` to catch malformed payloads early, especially when building custom integrations.
</Card>
</CardGroup>
## Learn More
- [A2A Agent Delegation](/en/learn/a2a-agent-delegation) — configure agents for remote delegation via the A2A protocol
- [A2A Protocol Documentation](https://a2a-protocol.org) — official protocol specification

View File

@@ -5,14 +5,6 @@ icon: rocket
mode: "wide"
---
### Watch: Building CrewAI Agents & Flows with Coding Agent Skills
Install our coding agent skills (Claude Code, Codex, ...) to quickly get your coding agents up and running with CrewAI.
You can install it with `npx skills add crewaiinc/skills`
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
## Build your first CrewAI Agent
Let's create a simple crew that will help us `research` and `report` on the `latest AI developments` for a given topic or subject.

View File

@@ -4,28 +4,6 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
icon: "clock"
mode: "wide"
---
<Update label="2026년 4월 2일">
## v1.13.0a7
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a7)
## 변경 사항
### 기능
- v0.8/v0.9 지원, 스키마 및 문서가 포함된 A2UI 확장 추가
### 버그 수정
- GPT-5 및 o-series를 추가하여 다중 모드 비전 접두사 수정
### 문서
- v1.13.0a6에 대한 변경 로그 및 버전 업데이트
## 기여자
@alex-clawd, @greysonlalonde, @joaomdmoura
</Update>
<Update label="2026년 4월 1일">
## v1.13.0a6

View File

@@ -5,14 +5,6 @@ icon: wrench
mode: "wide"
---
### 영상: 코딩 에이전트 스킬을 활용한 CrewAI Agents & Flows 구축
코딩 에이전트 스킬(Claude Code, Codex 등)을 설치하여 CrewAI로 코딩 에이전트를 빠르게 시작하세요.
`npx skills add crewaiinc/skills` 명령어로 설치할 수 있습니다
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
## 비디오 튜토리얼
설치 과정을 단계별로 시연하는 비디오 튜토리얼을 시청하세요:

View File

@@ -16,14 +16,6 @@ mode: "wide"
10만 명이 넘는 개발자가 커뮤니티 과정을 통해 인증을 받았으며, CrewAI는 기업용 AI 자동화의 표준입니다.
### 영상: 코딩 에이전트 스킬을 활용한 CrewAI Agents & Flows 구축
코딩 에이전트 스킬(Claude Code, Codex 등)을 설치하여 CrewAI로 코딩 에이전트를 빠르게 시작하세요.
`npx skills add crewaiinc/skills` 명령어로 설치할 수 있습니다
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
## CrewAI 아키텍처
CrewAI의 아키텍처는 자율성과 제어의 균형을 맞추도록 설계되었습니다.

View File

@@ -5,14 +5,6 @@ icon: rocket
mode: "wide"
---
### 영상: 코딩 에이전트 스킬을 활용한 CrewAI Agents & Flows 구축
코딩 에이전트 스킬(Claude Code, Codex 등)을 설치하여 CrewAI로 코딩 에이전트를 빠르게 시작하세요.
`npx skills add crewaiinc/skills` 명령어로 설치할 수 있습니다
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
## 첫 번째 CrewAI Agent 만들기
이제 주어진 주제나 항목에 대해 `최신 AI 개발 동향`을 `연구`하고 `보고`하는 간단한 crew를 만들어보겠습니다.

View File

@@ -4,28 +4,6 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="02 abr 2026">
## v1.13.0a7
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a7)
## O que Mudou
### Funcionalidades
- Adicionar a extensão A2UI com suporte a v0.8/v0.9, esquemas e documentação
### Correções de Bugs
- Corrigir prefixos de visão multimodal adicionando GPT-5 e o-series
### Documentação
- Atualizar changelog e versão para v1.13.0a6
## Contribuidores
@alex-clawd, @greysonlalonde, @joaomdmoura
</Update>
<Update label="01 abr 2026">
## v1.13.0a6

View File

@@ -5,14 +5,6 @@ icon: wrench
mode: "wide"
---
### Assista: Construindo Agents e Flows CrewAI com Coding Agent Skills
Instale nossas coding agent skills (Claude Code, Codex, ...) para colocar seus agentes de código para funcionar rapidamente com o CrewAI.
Você pode instalar com `npx skills add crewaiinc/skills`
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
## Tutorial em Vídeo
Assista a este tutorial em vídeo para uma demonstração passo a passo do processo de instalação:

View File

@@ -16,14 +16,6 @@ Ele capacita desenvolvedores a construir sistemas multi-agente prontos para prod
Com mais de 100.000 desenvolvedores certificados em nossos cursos comunitários, o CrewAI é o padrão para automação de IA pronta para empresas.
### Assista: Construindo Agents e Flows CrewAI com Coding Agent Skills
Instale nossas coding agent skills (Claude Code, Codex, ...) para colocar seus agentes de código para funcionar rapidamente com o CrewAI.
Você pode instalar com `npx skills add crewaiinc/skills`
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
## A Arquitetura do CrewAI
A arquitetura do CrewAI foi projetada para equilibrar autonomia com controle.

View File

@@ -5,14 +5,6 @@ icon: rocket
mode: "wide"
---
### Assista: Construindo Agents e Flows CrewAI com Coding Agent Skills
Instale nossas coding agent skills (Claude Code, Codex, ...) para colocar seus agentes de código para funcionar rapidamente com o CrewAI.
Você pode instalar com `npx skills add crewaiinc/skills`
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
## Construa seu primeiro Agente CrewAI
Vamos criar uma tripulação simples que nos ajudará a `pesquisar` e `relatar` sobre os `últimos avanços em IA` para um determinado tópico ou assunto.

0
lib/crewai-a2a/README.md Normal file
View File

View File

@@ -0,0 +1,29 @@
[project]
name = "crewai-a2a"
dynamic = ["version"]
description = "Agent-to-Agent (A2A) protocol communication for CrewAI"
readme = "README.md"
authors = [
{ name = "Joao Moura", email = "joao@crewai.com" }
]
requires-python = ">=3.10, <3.14"
dependencies = [
"crewai==1.13.0a6",
"a2a-sdk~=0.3.10",
"httpx-auth~=0.23.1",
"httpx-sse~=0.4.0",
"aiocache[redis,memcached]~=0.12.3",
"pydantic~=2.11.9",
"pyjwt>=2.9.0,<3",
"httpx~=0.28.1",
]
[tool.uv.sources]
crewai = { workspace = true }
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.version]
path = "src/crewai_a2a/__init__.py"

View File

@@ -0,0 +1,12 @@
"""Agent-to-Agent (A2A) protocol communication for CrewAI."""
__version__ = "1.13.0a6"
from crewai_a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
__all__ = [
"A2AClientConfig",
"A2AConfig",
"A2AServerConfig",
]

View File

@@ -1,6 +1,6 @@
"""A2A authentication schemas."""
from crewai.a2a.auth.client_schemes import (
from crewai_a2a.auth.client_schemes import (
APIKeyAuth,
AuthScheme,
BearerTokenAuth,
@@ -11,7 +11,7 @@ from crewai.a2a.auth.client_schemes import (
OAuth2ClientCredentials,
TLSConfig,
)
from crewai.a2a.auth.server_schemes import (
from crewai_a2a.auth.server_schemes import (
AuthenticatedUser,
EnterpriseTokenAuth,
OIDCAuth,

View File

@@ -0,0 +1,71 @@
"""Deprecated: Authentication schemes for A2A protocol agents.
This module is deprecated. Import from crewai_a2a.auth instead:
- crewai_a2a.auth.ClientAuthScheme (replaces AuthScheme)
- crewai_a2a.auth.BearerTokenAuth
- crewai_a2a.auth.HTTPBasicAuth
- crewai_a2a.auth.HTTPDigestAuth
- crewai_a2a.auth.APIKeyAuth
- crewai_a2a.auth.OAuth2ClientCredentials
- crewai_a2a.auth.OAuth2AuthorizationCode
"""
from __future__ import annotations
from typing_extensions import deprecated
from crewai_a2a.auth.client_schemes import (
APIKeyAuth as _APIKeyAuth,
BearerTokenAuth as _BearerTokenAuth,
ClientAuthScheme as _ClientAuthScheme,
HTTPBasicAuth as _HTTPBasicAuth,
HTTPDigestAuth as _HTTPDigestAuth,
OAuth2AuthorizationCode as _OAuth2AuthorizationCode,
OAuth2ClientCredentials as _OAuth2ClientCredentials,
)
@deprecated("Use ClientAuthScheme from crewai_a2a.auth instead", category=FutureWarning)
class AuthScheme(_ClientAuthScheme):
"""Deprecated: Use ClientAuthScheme from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class BearerTokenAuth(_BearerTokenAuth):
"""Deprecated: Import from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class HTTPBasicAuth(_HTTPBasicAuth):
"""Deprecated: Import from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class HTTPDigestAuth(_HTTPDigestAuth):
"""Deprecated: Import from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class APIKeyAuth(_APIKeyAuth):
"""Deprecated: Import from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class OAuth2ClientCredentials(_OAuth2ClientCredentials):
"""Deprecated: Import from crewai_a2a.auth instead."""
@deprecated("Import from crewai_a2a.auth instead", category=FutureWarning)
class OAuth2AuthorizationCode(_OAuth2AuthorizationCode):
"""Deprecated: Import from crewai_a2a.auth instead."""
__all__ = [
"APIKeyAuth",
"AuthScheme",
"BearerTokenAuth",
"HTTPBasicAuth",
"HTTPDigestAuth",
"OAuth2AuthorizationCode",
"OAuth2ClientCredentials",
]

View File

@@ -42,7 +42,10 @@ logger = logging.getLogger(__name__)
try:
from fastapi import HTTPException, status as http_status
from fastapi import ( # type: ignore[import-not-found]
HTTPException,
status as http_status,
)
HTTP_401_UNAUTHORIZED = http_status.HTTP_401_UNAUTHORIZED
HTTP_500_INTERNAL_SERVER_ERROR = http_status.HTTP_500_INTERNAL_SERVER_ERROR

View File

@@ -20,7 +20,7 @@ from a2a.types import (
)
from httpx import AsyncClient, Response
from crewai.a2a.auth.client_schemes import (
from crewai_a2a.auth.client_schemes import (
APIKeyAuth,
BearerTokenAuth,
ClientAuthScheme,

View File

@@ -20,10 +20,10 @@ from pydantic import (
)
from typing_extensions import Self, deprecated
from crewai.a2a.auth.client_schemes import ClientAuthScheme
from crewai.a2a.auth.server_schemes import ServerAuthScheme
from crewai.a2a.extensions.base import ValidatedA2AExtension
from crewai.a2a.types import ProtocolVersion, TransportType, Url
from crewai_a2a.auth.client_schemes import ClientAuthScheme
from crewai_a2a.auth.server_schemes import ServerAuthScheme
from crewai_a2a.extensions.base import ValidatedA2AExtension
from crewai_a2a.types import ProtocolVersion, TransportType, Url
try:
@@ -36,8 +36,8 @@ try:
SecurityScheme,
)
from crewai.a2a.extensions.server import ServerExtension
from crewai.a2a.updates import UpdateConfig
from crewai_a2a.extensions.server import ServerExtension
from crewai_a2a.updates import UpdateConfig
except ImportError:
UpdateConfig: Any = Any # type: ignore[no-redef]
AgentCapabilities: Any = Any # type: ignore[no-redef]
@@ -50,7 +50,7 @@ except ImportError:
def _get_default_update_config() -> UpdateConfig:
from crewai.a2a.updates import StreamingConfig
from crewai_a2a.updates import StreamingConfig
return StreamingConfig()
@@ -360,8 +360,8 @@ class ClientTransportConfig(BaseModel):
@deprecated(
"""
`crewai.a2a.config.A2AConfig` is deprecated and will be removed in v2.0.0,
use `crewai.a2a.config.A2AClientConfig` or `crewai.a2a.config.A2AServerConfig` instead.
`crewai_a2a.config.A2AConfig` is deprecated and will be removed in v2.0.0,
use `crewai_a2a.config.A2AClientConfig` or `crewai_a2a.config.A2AServerConfig` instead.
""",
category=FutureWarning,
)

View File

@@ -13,13 +13,13 @@ via the X-A2A-Extensions header.
See: https://a2a-protocol.org/latest/topics/extensions/
"""
from crewai.a2a.extensions.base import (
from crewai_a2a.extensions.base import (
A2AExtension,
ConversationState,
ExtensionRegistry,
ValidatedA2AExtension,
)
from crewai.a2a.extensions.server import (
from crewai_a2a.extensions.server import (
ExtensionContext,
ServerExtension,
ServerExtensionRegistry,

View File

@@ -19,7 +19,6 @@ from pydantic import BeforeValidator
if TYPE_CHECKING:
from a2a.types import Message
from crewai.agent.core import Agent
@@ -150,23 +149,6 @@ class A2AExtension(Protocol):
"""
...
def prepare_message_metadata(
self,
conversation_state: ConversationState | None,
) -> dict[str, Any]:
"""Prepare extension-specific metadata for outbound A2A messages.
Called when constructing A2A messages to inject extension-specific
metadata such as client capabilities declarations.
Args:
conversation_state: Extension-specific state from extract_state_from_history.
Returns:
Dict of metadata key-value pairs to merge into the message metadata.
"""
...
class ExtensionRegistry:
"""Registry for managing A2A extensions.
@@ -253,21 +235,3 @@ class ExtensionRegistry:
state = extension_states.get(type(extension))
processed = extension.process_response(processed, state)
return processed
def prepare_all_metadata(
self,
extension_states: dict[type[A2AExtension], ConversationState],
) -> dict[str, Any]:
"""Collect metadata from all registered extensions for outbound messages.
Args:
extension_states: Mapping of extension types to conversation states.
Returns:
Merged metadata dict from all extensions.
"""
metadata: dict[str, Any] = {}
for extension in self._extensions:
state = extension_states.get(type(extension))
metadata.update(extension.prepare_message_metadata(state))
return metadata

View File

@@ -18,8 +18,8 @@ from a2a.extensions.common import (
)
from a2a.types import AgentCard, AgentExtension
from crewai.a2a.config import A2AClientConfig, A2AConfig
from crewai.a2a.extensions.base import ExtensionRegistry
from crewai_a2a.config import A2AClientConfig, A2AConfig
from crewai_a2a.extensions.base import ExtensionRegistry
def get_extensions_from_config(

View File

@@ -3,7 +3,7 @@
from __future__ import annotations
from collections.abc import AsyncIterator
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, TypedDict
import uuid
from a2a.client.errors import A2AClientHTTPError
@@ -18,13 +18,12 @@ from a2a.types import (
TaskStatusUpdateEvent,
TextPart,
)
from typing_extensions import NotRequired, TypedDict
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConnectionErrorEvent,
A2AResponseReceivedEvent,
)
from typing_extensions import NotRequired
if TYPE_CHECKING:

View File

@@ -7,15 +7,16 @@ from typing import (
Any,
Literal,
Protocol,
TypedDict,
runtime_checkable,
)
from pydantic import BeforeValidator, HttpUrl, TypeAdapter
from typing_extensions import NotRequired, TypedDict
from typing_extensions import NotRequired
try:
from crewai.a2a.updates import (
from crewai_a2a.updates import (
PollingConfig,
PollingHandler,
PushNotificationConfig,

View File

@@ -1,6 +1,6 @@
"""A2A update mechanism configuration types."""
from crewai.a2a.updates.base import (
from crewai_a2a.updates.base import (
BaseHandlerKwargs,
PollingHandlerKwargs,
PushNotificationHandlerKwargs,
@@ -8,12 +8,12 @@ from crewai.a2a.updates.base import (
StreamingHandlerKwargs,
UpdateHandler,
)
from crewai.a2a.updates.polling.config import PollingConfig
from crewai.a2a.updates.polling.handler import PollingHandler
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
from crewai.a2a.updates.push_notifications.handler import PushNotificationHandler
from crewai.a2a.updates.streaming.config import StreamingConfig
from crewai.a2a.updates.streaming.handler import StreamingHandler
from crewai_a2a.updates.polling.config import PollingConfig
from crewai_a2a.updates.polling.handler import PollingHandler
from crewai_a2a.updates.push_notifications.config import PushNotificationConfig
from crewai_a2a.updates.push_notifications.handler import PushNotificationHandler
from crewai_a2a.updates.streaming.config import StreamingConfig
from crewai_a2a.updates.streaming.handler import StreamingHandler
UpdateConfig = PollingConfig | StreamingConfig | PushNotificationConfig

View File

@@ -2,11 +2,10 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, NamedTuple, Protocol
from typing import TYPE_CHECKING, Any, NamedTuple, Protocol, TypedDict
from pydantic import GetCoreSchemaHandler
from pydantic_core import CoreSchema, core_schema
from typing_extensions import TypedDict
class CommonParams(NamedTuple):
@@ -29,8 +28,8 @@ if TYPE_CHECKING:
from a2a.client import Client
from a2a.types import AgentCard, Message, Task
from crewai.a2a.task_helpers import TaskStateResult
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
from crewai_a2a.task_helpers import TaskStateResult
from crewai_a2a.updates.push_notifications.config import PushNotificationConfig
class BaseHandlerKwargs(TypedDict, total=False):

View File

@@ -18,17 +18,6 @@ from a2a.types import (
TaskState,
TextPart,
)
from typing_extensions import Unpack
from crewai.a2a.errors import A2APollingTimeoutError
from crewai.a2a.task_helpers import (
ACTIONABLE_STATES,
TERMINAL_STATES,
TaskStateResult,
process_task_state,
send_message_and_get_task_id,
)
from crewai.a2a.updates.base import PollingHandlerKwargs
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConnectionErrorEvent,
@@ -36,6 +25,17 @@ from crewai.events.types.a2a_events import (
A2APollingStatusEvent,
A2AResponseReceivedEvent,
)
from typing_extensions import Unpack
from crewai_a2a.errors import A2APollingTimeoutError
from crewai_a2a.task_helpers import (
ACTIONABLE_STATES,
TERMINAL_STATES,
TaskStateResult,
process_task_state,
send_message_and_get_task_id,
)
from crewai_a2a.updates.base import PollingHandlerKwargs
if TYPE_CHECKING:

View File

@@ -7,8 +7,8 @@ from typing import Annotated
from a2a.types import PushNotificationAuthenticationInfo
from pydantic import AnyHttpUrl, BaseModel, BeforeValidator, Field
from crewai.a2a.updates.base import PushNotificationResultStore
from crewai.a2a.updates.push_notifications.signature import WebhookSignatureConfig
from crewai_a2a.updates.base import PushNotificationResultStore
from crewai_a2a.updates.push_notifications.signature import WebhookSignatureConfig
def _coerce_signature(

View File

@@ -16,19 +16,6 @@ from a2a.types import (
TaskState,
TextPart,
)
from typing_extensions import Unpack
from crewai.a2a.task_helpers import (
TaskStateResult,
process_task_state,
send_message_and_get_task_id,
)
from crewai.a2a.updates.base import (
CommonParams,
PushNotificationHandlerKwargs,
PushNotificationResultStore,
extract_common_params,
)
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConnectionErrorEvent,
@@ -36,6 +23,19 @@ from crewai.events.types.a2a_events import (
A2APushNotificationTimeoutEvent,
A2AResponseReceivedEvent,
)
from typing_extensions import Unpack
from crewai_a2a.task_helpers import (
TaskStateResult,
process_task_state,
send_message_and_get_task_id,
)
from crewai_a2a.updates.base import (
CommonParams,
PushNotificationHandlerKwargs,
PushNotificationResultStore,
extract_common_params,
)
if TYPE_CHECKING:

View File

@@ -22,18 +22,6 @@ from a2a.types import (
TaskStatusUpdateEvent,
TextPart,
)
from typing_extensions import Unpack
from crewai.a2a.task_helpers import (
ACTIONABLE_STATES,
TERMINAL_STATES,
TaskStateResult,
process_task_state,
)
from crewai.a2a.updates.base import StreamingHandlerKwargs, extract_common_params
from crewai.a2a.updates.streaming.params import (
process_status_update,
)
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AArtifactReceivedEvent,
@@ -42,6 +30,18 @@ from crewai.events.types.a2a_events import (
A2AStreamingChunkEvent,
A2AStreamingStartedEvent,
)
from typing_extensions import Unpack
from crewai_a2a.task_helpers import (
ACTIONABLE_STATES,
TERMINAL_STATES,
TaskStateResult,
process_task_state,
)
from crewai_a2a.updates.base import StreamingHandlerKwargs, extract_common_params
from crewai_a2a.updates.streaming.params import (
process_status_update,
)
logger = logging.getLogger(__name__)

View File

@@ -16,15 +16,6 @@ from a2a.client.errors import A2AClientHTTPError
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
from aiocache import cached # type: ignore[import-untyped]
from aiocache.serializers import PickleSerializer # type: ignore[import-untyped]
import httpx
from crewai.a2a.auth.client_schemes import APIKeyAuth, HTTPDigestAuth
from crewai.a2a.auth.utils import (
_auth_store,
configure_auth_client,
retry_on_401,
)
from crewai.a2a.config import A2AServerConfig
from crewai.crew import Crew
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
@@ -32,13 +23,23 @@ from crewai.events.types.a2a_events import (
A2AAuthenticationFailedEvent,
A2AConnectionErrorEvent,
)
import httpx
from crewai_a2a.auth.client_schemes import APIKeyAuth, HTTPDigestAuth
from crewai_a2a.auth.utils import (
_auth_store,
configure_auth_client,
retry_on_401,
)
from crewai_a2a.config import A2AServerConfig
if TYPE_CHECKING:
from crewai.a2a.auth.client_schemes import ClientAuthScheme
from crewai.agent import Agent
from crewai.task import Task
from crewai_a2a.auth.client_schemes import ClientAuthScheme
def _get_tls_verify(auth: ClientAuthScheme | None) -> ssl.SSLContext | bool | str:
"""Get TLS verify parameter from auth scheme.
@@ -495,7 +496,7 @@ def _agent_to_agent_card(agent: Agent, url: str) -> AgentCard:
Returns:
AgentCard describing the agent's capabilities.
"""
from crewai.a2a.utils.agent_card_signing import sign_agent_card
from crewai_a2a.utils.agent_card_signing import sign_agent_card
server_config = _get_server_config(agent) or A2AServerConfig()
@@ -529,7 +530,7 @@ def _agent_to_agent_card(agent: Agent, url: str) -> AgentCard:
capabilities = server_config.capabilities
if server_config.server_extensions:
from crewai.a2a.extensions.server import ServerExtensionRegistry
from crewai_a2a.extensions.server import ServerExtensionRegistry
registry = ServerExtensionRegistry(server_config.server_extensions)
ext_list = registry.get_agent_extensions()

View File

@@ -5,7 +5,7 @@ JSON Web Signatures (JWS) as per RFC 7515. Signed agent cards allow clients
to verify the authenticity and integrity of agent card information.
Example:
>>> from crewai.a2a.utils.agent_card_signing import sign_agent_card
>>> from crewai_a2a.utils.agent_card_signing import sign_agent_card
>>> signature = sign_agent_card(agent_card, private_key_pem, key_id="key-1")
>>> card_with_sig = card.model_copy(update={"signatures": [signature]})
"""

View File

@@ -10,7 +10,6 @@ from dataclasses import dataclass
from typing import TYPE_CHECKING, Annotated, Final, Literal, cast
from a2a.types import Part
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import A2AContentTypeNegotiatedEvent
@@ -28,7 +27,6 @@ APPLICATION_PDF: Literal["application/pdf"] = "application/pdf"
APPLICATION_OCTET_STREAM: Literal["application/octet-stream"] = (
"application/octet-stream"
)
APPLICATION_A2UI_JSON: Literal["application/json+a2ui"] = "application/json+a2ui"
DEFAULT_CLIENT_INPUT_MODES: Final[list[Literal["text/plain", "application/json"]]] = [
TEXT_PLAIN,
@@ -312,10 +310,6 @@ def get_part_content_type(part: Part) -> str:
if root.kind == "text":
return TEXT_PLAIN
if root.kind == "data":
metadata = root.metadata or {}
mime = metadata.get("mimeType", "")
if mime == APPLICATION_A2UI_JSON:
return APPLICATION_A2UI_JSON
return APPLICATION_JSON
if root.kind == "file":
return root.file.mime_type or APPLICATION_OCTET_STREAM

View File

@@ -23,48 +23,6 @@ from a2a.types import (
Role,
TextPart,
)
import httpx
from pydantic import BaseModel
from crewai.a2a.auth.client_schemes import APIKeyAuth, HTTPDigestAuth
from crewai.a2a.auth.utils import (
_auth_store,
configure_auth_client,
validate_auth_against_agent_card,
)
from crewai.a2a.config import ClientTransportConfig, GRPCClientConfig
from crewai.a2a.extensions.registry import (
ExtensionsMiddleware,
validate_required_extensions,
)
from crewai.a2a.task_helpers import TaskStateResult
from crewai.a2a.types import (
HANDLER_REGISTRY,
HandlerType,
PartsDict,
PartsMetadataDict,
TransportType,
)
from crewai.a2a.updates import (
PollingConfig,
PushNotificationConfig,
StreamingHandler,
UpdateConfig,
)
from crewai.a2a.utils.agent_card import (
_afetch_agent_card_cached,
_get_tls_verify,
_prepare_auth_headers,
)
from crewai.a2a.utils.content_type import (
DEFAULT_CLIENT_OUTPUT_MODES,
negotiate_content_types,
)
from crewai.a2a.utils.transport import (
NegotiatedTransport,
TransportNegotiationError,
negotiate_transport,
)
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConversationStartedEvent,
@@ -72,6 +30,48 @@ from crewai.events.types.a2a_events import (
A2ADelegationStartedEvent,
A2AMessageSentEvent,
)
import httpx
from pydantic import BaseModel
from crewai_a2a.auth.client_schemes import APIKeyAuth, HTTPDigestAuth
from crewai_a2a.auth.utils import (
_auth_store,
configure_auth_client,
validate_auth_against_agent_card,
)
from crewai_a2a.config import ClientTransportConfig, GRPCClientConfig
from crewai_a2a.extensions.registry import (
ExtensionsMiddleware,
validate_required_extensions,
)
from crewai_a2a.task_helpers import TaskStateResult
from crewai_a2a.types import (
HANDLER_REGISTRY,
HandlerType,
PartsDict,
PartsMetadataDict,
TransportType,
)
from crewai_a2a.updates import (
PollingConfig,
PushNotificationConfig,
StreamingHandler,
UpdateConfig,
)
from crewai_a2a.utils.agent_card import (
_afetch_agent_card_cached,
_get_tls_verify,
_prepare_auth_headers,
)
from crewai_a2a.utils.content_type import (
DEFAULT_CLIENT_OUTPUT_MODES,
negotiate_content_types,
)
from crewai_a2a.utils.transport import (
NegotiatedTransport,
TransportNegotiationError,
negotiate_transport,
)
logger = logging.getLogger(__name__)
@@ -80,7 +80,7 @@ logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from a2a.types import Message
from crewai.a2a.auth.client_schemes import ClientAuthScheme
from crewai_a2a.auth.client_schemes import ClientAuthScheme
_DEFAULT_TRANSPORT: Final[TransportType] = "JSONRPC"
@@ -771,7 +771,7 @@ def _create_grpc_channel_factory(
auth_metadata: list[tuple[str, str]] = []
if auth is not None:
from crewai.a2a.auth.client_schemes import (
from crewai_a2a.auth.client_schemes import (
APIKeyAuth,
BearerTokenAuth,
HTTPBasicAuth,

View File

@@ -103,7 +103,7 @@ class LogContext:
_log_context.reset(self._token)
def configure_json_logging(logger_name: str = "crewai.a2a") -> None:
def configure_json_logging(logger_name: str = "crewai_a2a") -> None:
"""Configure JSON logging for the A2A module.
Args:

View File

@@ -4,10 +4,10 @@ from __future__ import annotations
from typing import TypeAlias
from crewai.types.utils import create_literals_from_strings
from pydantic import BaseModel, Field, create_model
from crewai.a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
from crewai.types.utils import create_literals_from_strings
from crewai_a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
A2AConfigTypes: TypeAlias = A2AConfig | A2AServerConfig | A2AClientConfig

View File

@@ -10,7 +10,7 @@ from functools import wraps
import json
import logging
import os
from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast
from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, TypedDict, cast
from urllib.parse import urlparse
from a2a.server.agent_execution import RequestContext
@@ -37,11 +37,6 @@ from a2a.utils import (
)
from a2a.utils.errors import ServerError
from aiocache import SimpleMemoryCache, caches # type: ignore[import-untyped]
from pydantic import BaseModel
from typing_extensions import TypedDict
from crewai.a2a.utils.agent_card import _get_server_config
from crewai.a2a.utils.content_type import validate_message_parts
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AServerTaskCanceledEvent,
@@ -51,12 +46,17 @@ from crewai.events.types.a2a_events import (
)
from crewai.task import Task
from crewai.utilities.pydantic_schema_utils import create_model_from_schema
from pydantic import BaseModel
from crewai_a2a.utils.agent_card import _get_server_config
from crewai_a2a.utils.content_type import validate_message_parts
if TYPE_CHECKING:
from crewai.a2a.extensions.server import ExtensionContext, ServerExtensionRegistry
from crewai.agent import Agent
from crewai_a2a.extensions.server import ExtensionContext, ServerExtensionRegistry
logger = logging.getLogger(__name__)

View File

@@ -11,7 +11,6 @@ from dataclasses import dataclass
from typing import Final, Literal
from a2a.types import AgentCard, AgentInterface
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import A2ATransportNegotiatedEvent

View File

@@ -15,33 +15,6 @@ from types import MethodType
from typing import TYPE_CHECKING, Any, NamedTuple
from a2a.types import Role, TaskState
from pydantic import BaseModel, ValidationError
from crewai.a2a.config import A2AClientConfig, A2AConfig
from crewai.a2a.extensions.base import (
A2AExtension,
ConversationState,
ExtensionRegistry,
)
from crewai.a2a.task_helpers import TaskStateResult
from crewai.a2a.templates import (
AVAILABLE_AGENTS_TEMPLATE,
CONVERSATION_TURN_INFO_TEMPLATE,
PREVIOUS_A2A_CONVERSATION_TEMPLATE,
REMOTE_AGENT_RESPONSE_NOTICE,
UNAVAILABLE_AGENTS_NOTICE_TEMPLATE,
)
from crewai.a2a.types import AgentResponseProtocol
from crewai.a2a.utils.agent_card import (
afetch_agent_card,
fetch_agent_card,
inject_a2a_server_methods,
)
from crewai.a2a.utils.delegation import (
aexecute_a2a_delegation,
execute_a2a_delegation,
)
from crewai.a2a.utils.response_model import get_a2a_agents_and_response_model
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConversationCompletedEvent,
@@ -49,11 +22,37 @@ from crewai.events.types.a2a_events import (
)
from crewai.lite_agent_output import LiteAgentOutput
from crewai.task import Task
from pydantic import BaseModel, ValidationError
from crewai_a2a.config import A2AClientConfig, A2AConfig
from crewai_a2a.extensions.base import (
A2AExtension,
ConversationState,
ExtensionRegistry,
)
from crewai_a2a.task_helpers import TaskStateResult
from crewai_a2a.templates import (
AVAILABLE_AGENTS_TEMPLATE,
CONVERSATION_TURN_INFO_TEMPLATE,
PREVIOUS_A2A_CONVERSATION_TEMPLATE,
REMOTE_AGENT_RESPONSE_NOTICE,
UNAVAILABLE_AGENTS_NOTICE_TEMPLATE,
)
from crewai_a2a.types import AgentResponseProtocol
from crewai_a2a.utils.agent_card import (
afetch_agent_card,
fetch_agent_card,
inject_a2a_server_methods,
)
from crewai_a2a.utils.delegation import (
aexecute_a2a_delegation,
execute_a2a_delegation,
)
from crewai_a2a.utils.response_model import get_a2a_agents_and_response_model
if TYPE_CHECKING:
from a2a.types import AgentCard, Message
from crewai.agent.core import Agent
from crewai.tools.base_tool import BaseTool
@@ -1273,15 +1272,6 @@ def _delegate_to_a2a(
for turn_num in range(ctx.max_turns):
agent_branch, accepted_output_modes = _get_turn_context(ctx.agent_config)
merged_metadata = dict(ctx.metadata) if ctx.metadata else {}
if _extension_registry and conversation_history:
_ext_states = _extension_registry.extract_all_states(
conversation_history
)
merged_metadata.update(
_extension_registry.prepare_all_metadata(_ext_states)
)
a2a_result = execute_a2a_delegation(
endpoint=ctx.agent_config.endpoint,
auth=ctx.agent_config.auth,
@@ -1290,7 +1280,7 @@ def _delegate_to_a2a(
context_id=context_id,
task_id=task_id,
reference_task_ids=reference_task_ids,
metadata=merged_metadata or None,
metadata=ctx.metadata,
extensions=ctx.extensions,
conversation_history=conversation_history,
agent_id=ctx.agent_id,
@@ -1628,15 +1618,6 @@ async def _adelegate_to_a2a(
for turn_num in range(ctx.max_turns):
agent_branch, accepted_output_modes = _get_turn_context(ctx.agent_config)
merged_metadata = dict(ctx.metadata) if ctx.metadata else {}
if _extension_registry and conversation_history:
_ext_states = _extension_registry.extract_all_states(
conversation_history
)
merged_metadata.update(
_extension_registry.prepare_all_metadata(_ext_states)
)
a2a_result = await aexecute_a2a_delegation(
endpoint=ctx.agent_config.endpoint,
auth=ctx.agent_config.auth,
@@ -1645,7 +1626,7 @@ async def _adelegate_to_a2a(
context_id=context_id,
task_id=task_id,
reference_task_ids=reference_task_ids,
metadata=merged_metadata or None,
metadata=ctx.metadata,
extensions=ctx.extensions,
conversation_history=conversation_history,
agent_id=ctx.agent_id,

View File

@@ -3,14 +3,12 @@ from __future__ import annotations
import os
import uuid
import pytest
import pytest_asyncio
from a2a.client import ClientFactory
from a2a.types import AgentCard, Message, Part, Role, TaskState, TextPart
from crewai.a2a.updates.polling.handler import PollingHandler
from crewai.a2a.updates.streaming.handler import StreamingHandler
from crewai_a2a.updates.polling.handler import PollingHandler
from crewai_a2a.updates.streaming.handler import StreamingHandler
import pytest
import pytest_asyncio
A2A_TEST_ENDPOINT = os.getenv("A2A_TEST_ENDPOINT", "http://localhost:9999")
@@ -162,7 +160,7 @@ class TestA2APushNotificationHandler:
)
@pytest.fixture
def mock_task(self) -> "Task":
def mock_task(self) -> Task:
"""Create a minimal valid task for testing."""
from a2a.types import Task, TaskStatus
@@ -182,11 +180,12 @@ class TestA2APushNotificationHandler:
from unittest.mock import AsyncMock, MagicMock
from a2a.types import Task, TaskStatus
from crewai_a2a.updates.push_notifications.config import PushNotificationConfig
from crewai_a2a.updates.push_notifications.handler import (
PushNotificationHandler,
)
from pydantic import AnyHttpUrl
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
from crewai.a2a.updates.push_notifications.handler import PushNotificationHandler
completed_task = Task(
id="task-123",
context_id="ctx-123",
@@ -246,11 +245,12 @@ class TestA2APushNotificationHandler:
from unittest.mock import AsyncMock, MagicMock
from a2a.types import Task, TaskStatus
from crewai_a2a.updates.push_notifications.config import PushNotificationConfig
from crewai_a2a.updates.push_notifications.handler import (
PushNotificationHandler,
)
from pydantic import AnyHttpUrl
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
from crewai.a2a.updates.push_notifications.handler import PushNotificationHandler
mock_store = MagicMock()
mock_store.wait_for_result = AsyncMock(return_value=None)
@@ -303,7 +303,9 @@ class TestA2APushNotificationHandler:
"""Test that push handler fails gracefully without config."""
from unittest.mock import MagicMock
from crewai.a2a.updates.push_notifications.handler import PushNotificationHandler
from crewai_a2a.updates.push_notifications.handler import (
PushNotificationHandler,
)
mock_client = MagicMock()

View File

@@ -2,9 +2,9 @@
from unittest.mock import MagicMock, patch
from crewai_a2a.config import A2AConfig
import pytest
from crewai.a2a.config import A2AConfig
try:
from a2a.types import Message, Role
@@ -27,9 +27,8 @@ def _create_mock_agent_card(name: str = "Test", url: str = "http://test-endpoint
@pytest.mark.skipif(not A2A_SDK_INSTALLED, reason="Requires a2a-sdk to be installed")
def test_trust_remote_completion_status_true_returns_directly():
"""When trust_remote_completion_status=True and A2A returns completed, return result directly."""
from crewai.a2a.wrapper import _delegate_to_a2a
from crewai.a2a.types import AgentResponseProtocol
from crewai import Agent, Task
from crewai_a2a.wrapper import _delegate_to_a2a
a2a_config = A2AConfig(
endpoint="http://test-endpoint.com",
@@ -51,8 +50,8 @@ def test_trust_remote_completion_status_true_returns_directly():
a2a_ids = ["http://test-endpoint.com/"]
with (
patch("crewai.a2a.wrapper.execute_a2a_delegation") as mock_execute,
patch("crewai.a2a.wrapper._fetch_agent_cards_concurrently") as mock_fetch,
patch("crewai_a2a.wrapper.execute_a2a_delegation") as mock_execute,
patch("crewai_a2a.wrapper._fetch_agent_cards_concurrently") as mock_fetch,
):
mock_card = _create_mock_agent_card()
mock_fetch.return_value = ({"http://test-endpoint.com/": mock_card}, {})
@@ -83,8 +82,8 @@ def test_trust_remote_completion_status_true_returns_directly():
@pytest.mark.skipif(not A2A_SDK_INSTALLED, reason="Requires a2a-sdk to be installed")
def test_trust_remote_completion_status_false_continues_conversation():
"""When trust_remote_completion_status=False and A2A returns completed, ask server agent."""
from crewai.a2a.wrapper import _delegate_to_a2a
from crewai import Agent, Task
from crewai_a2a.wrapper import _delegate_to_a2a
a2a_config = A2AConfig(
endpoint="http://test-endpoint.com",
@@ -116,8 +115,8 @@ def test_trust_remote_completion_status_false_continues_conversation():
return "unexpected"
with (
patch("crewai.a2a.wrapper.execute_a2a_delegation") as mock_execute,
patch("crewai.a2a.wrapper._fetch_agent_cards_concurrently") as mock_fetch,
patch("crewai_a2a.wrapper.execute_a2a_delegation") as mock_execute,
patch("crewai_a2a.wrapper._fetch_agent_cards_concurrently") as mock_fetch,
):
mock_card = _create_mock_agent_card()
mock_fetch.return_value = ({"http://test-endpoint.com/": mock_card}, {})
@@ -152,4 +151,4 @@ def test_default_trust_remote_completion_status_is_false():
endpoint="http://test-endpoint.com",
)
assert a2a_config.trust_remote_completion_status is False
assert a2a_config.trust_remote_completion_status is False

View File

@@ -4,10 +4,9 @@ from __future__ import annotations
import os
import pytest
from crewai import Agent
from crewai.a2a.config import A2AClientConfig
from crewai_a2a.config import A2AClientConfig
import pytest
A2A_TEST_ENDPOINT = os.getenv(
@@ -50,9 +49,7 @@ class TestAgentA2AKickoff:
@pytest.mark.skip(reason="VCR cassette matching issue with agent card caching")
@pytest.mark.vcr()
def test_agent_kickoff_with_calculator_skill(
self, researcher_agent: Agent
) -> None:
def test_agent_kickoff_with_calculator_skill(self, researcher_agent: Agent) -> None:
"""Test that agent can delegate calculation to A2A server."""
result = researcher_agent.kickoff(
"Ask the remote A2A agent to calculate 25 times 17."
@@ -149,9 +146,7 @@ class TestAgentA2AKickoff:
@pytest.mark.skip(reason="VCR cassette matching issue with agent card caching")
@pytest.mark.vcr()
def test_agent_kickoff_with_list_messages(
self, researcher_agent: Agent
) -> None:
def test_agent_kickoff_with_list_messages(self, researcher_agent: Agent) -> None:
"""Test that agent.kickoff() works with list of messages."""
messages = [
{

View File

@@ -1,14 +1,12 @@
"""Test A2A wrapper is only applied when a2a is passed to Agent."""
from unittest.mock import patch
from crewai import Agent
from crewai_a2a.config import A2AConfig
import pytest
from crewai import Agent
from crewai.a2a.config import A2AConfig
try:
import a2a # noqa: F401
import a2a
A2A_SDK_INSTALLED = True
except ImportError:
@@ -106,6 +104,9 @@ def test_wrapper_is_applied_differently_per_instance():
a2a=a2a_config,
)
assert agent_without_a2a.execute_task.__func__ is not agent_with_a2a.execute_task.__func__
assert (
agent_without_a2a.execute_task.__func__
is not agent_with_a2a.execute_task.__func__
)
assert not hasattr(agent_without_a2a.execute_task, "__wrapped__")
assert hasattr(agent_with_a2a.execute_task, "__wrapped__")

View File

@@ -3,10 +3,9 @@
from __future__ import annotations
from a2a.types import AgentCard, AgentSkill
from crewai import Agent
from crewai.a2a.config import A2AClientConfig, A2AServerConfig
from crewai.a2a.utils.agent_card import inject_a2a_server_methods
from crewai_a2a.config import A2AClientConfig, A2AServerConfig
from crewai_a2a.utils.agent_card import inject_a2a_server_methods
class TestInjectA2AServerMethods:

View File

@@ -6,13 +6,12 @@ import asyncio
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import pytest_asyncio
from a2a.server.agent_execution import RequestContext
from a2a.server.events import EventQueue
from a2a.types import Message, Task as A2ATask, TaskState, TaskStatus
from crewai.a2a.utils.task import cancel, cancellable, execute
from crewai_a2a.utils.task import cancel, cancellable, execute
import pytest
import pytest_asyncio
@pytest.fixture
@@ -85,8 +84,11 @@ class TestCancellableDecorator:
assert call_count == 1
@pytest.mark.asyncio
async def test_executes_function_with_context(self, mock_context: MagicMock) -> None:
async def test_executes_function_with_context(
self, mock_context: MagicMock
) -> None:
"""Function executes normally with RequestContext when not cancelled."""
@cancellable
async def my_func(context: RequestContext) -> str:
await asyncio.sleep(0.01)
@@ -134,6 +136,7 @@ class TestCancellableDecorator:
@pytest.mark.asyncio
async def test_extracts_context_from_kwargs(self, mock_context: MagicMock) -> None:
"""Context can be passed as keyword argument."""
@cancellable
async def my_func(value: int, context: RequestContext | None = None) -> int:
return value + 1
@@ -156,8 +159,8 @@ class TestExecute:
) -> None:
"""Execute completes successfully and enqueues completed task."""
with (
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
patch("crewai.a2a.utils.task.crewai_event_bus") as mock_bus,
patch("crewai_a2a.utils.task.Task", return_value=mock_task),
patch("crewai_a2a.utils.task.crewai_event_bus") as mock_bus,
):
await execute(mock_agent, mock_context, mock_event_queue)
@@ -175,8 +178,8 @@ class TestExecute:
) -> None:
"""Execute emits A2AServerTaskStartedEvent."""
with (
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
patch("crewai.a2a.utils.task.crewai_event_bus") as mock_bus,
patch("crewai_a2a.utils.task.Task", return_value=mock_task),
patch("crewai_a2a.utils.task.crewai_event_bus") as mock_bus,
):
await execute(mock_agent, mock_context, mock_event_queue)
@@ -197,8 +200,8 @@ class TestExecute:
) -> None:
"""Execute emits A2AServerTaskCompletedEvent on success."""
with (
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
patch("crewai.a2a.utils.task.crewai_event_bus") as mock_bus,
patch("crewai_a2a.utils.task.Task", return_value=mock_task),
patch("crewai_a2a.utils.task.crewai_event_bus") as mock_bus,
):
await execute(mock_agent, mock_context, mock_event_queue)
@@ -221,8 +224,8 @@ class TestExecute:
mock_agent.aexecute_task = AsyncMock(side_effect=ValueError("Test error"))
with (
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
patch("crewai.a2a.utils.task.crewai_event_bus") as mock_bus,
patch("crewai_a2a.utils.task.Task", return_value=mock_task),
patch("crewai_a2a.utils.task.crewai_event_bus") as mock_bus,
):
with pytest.raises(Exception):
await execute(mock_agent, mock_context, mock_event_queue)
@@ -245,8 +248,8 @@ class TestExecute:
mock_agent.aexecute_task = AsyncMock(side_effect=asyncio.CancelledError())
with (
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
patch("crewai.a2a.utils.task.crewai_event_bus") as mock_bus,
patch("crewai_a2a.utils.task.Task", return_value=mock_task),
patch("crewai_a2a.utils.task.crewai_event_bus") as mock_bus,
):
with pytest.raises(asyncio.CancelledError):
await execute(mock_agent, mock_context, mock_event_queue)
@@ -354,6 +357,7 @@ class TestExecuteAndCancelIntegration:
mock_task: MagicMock,
) -> None:
"""Calling cancel stops a running execute."""
async def slow_task(**kwargs: Any) -> str:
await asyncio.sleep(2.0)
return "should not complete"
@@ -361,8 +365,8 @@ class TestExecuteAndCancelIntegration:
mock_agent.aexecute_task = slow_task
with (
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
patch("crewai.a2a.utils.task.crewai_event_bus"),
patch("crewai_a2a.utils.task.Task", return_value=mock_task),
patch("crewai_a2a.utils.task.crewai_event_bus"),
):
execute_task = asyncio.create_task(
execute(mock_agent, mock_context, mock_event_queue)
@@ -372,4 +376,4 @@ class TestExecuteAndCancelIntegration:
await cancel(mock_context, mock_event_queue)
with pytest.raises(asyncio.CancelledError):
await execute_task
await execute_task

View File

@@ -152,4 +152,4 @@ __all__ = [
"wrap_file_source",
]
__version__ = "1.13.0a7"
__version__ = "1.13.0a6"

View File

@@ -11,7 +11,7 @@ dependencies = [
"pytube~=15.0.0",
"requests~=2.32.5",
"docker~=7.1.0",
"crewai==1.13.0a7",
"crewai==1.13.0a6",
"tiktoken~=0.8.0",
"beautifulsoup4~=4.13.4",
"python-docx~=1.2.0",

View File

@@ -309,4 +309,4 @@ __all__ = [
"ZapierActionTools",
]
__version__ = "1.13.0a7"
__version__ = "1.13.0a6"

View File

@@ -54,7 +54,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
tools = [
"crewai-tools==1.13.0a7",
"crewai-tools==1.13.0a6",
]
embeddings = [
"tiktoken~=0.8.0"
@@ -98,10 +98,7 @@ anthropic = [
"anthropic~=0.73.0",
]
a2a = [
"a2a-sdk~=0.3.10",
"httpx-auth~=0.23.1",
"httpx-sse~=0.4.0",
"aiocache[redis,memcached]~=0.12.3",
"crewai-a2a==1.13.0a6",
]
file-processing = [
"crewai-files",
@@ -136,6 +133,7 @@ torchvision = [
{ index = "pytorch", marker = "python_version < '3.13'" },
]
crewai-files = { workspace = true }
crewai-a2a = { workspace = true }
[build-system]

View File

@@ -4,13 +4,12 @@ from typing import Any
import urllib.request
import warnings
from pydantic import PydanticUserError
from pydantic import PydanticUndefinedAnnotation, PydanticUserError
from crewai.agent.core import Agent
from crewai.agent.planning_config import PlanningConfig
from crewai.crew import Crew
from crewai.crews.crew_output import CrewOutput
from crewai.execution_context import ExecutionContext
from crewai.flow.flow import Flow
from crewai.knowledge.knowledge import Knowledge
from crewai.llm import LLM
@@ -45,7 +44,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
_suppress_pydantic_deprecation_warnings()
__version__ = "1.13.0a7"
__version__ = "1.13.0a6"
_telemetry_submitted = False
@@ -97,10 +96,6 @@ def __getattr__(name: str) -> Any:
try:
from crewai.agents.agent_builder.base_agent import BaseAgent as _BaseAgent
from crewai.agents.agent_builder.base_agent_executor_mixin import (
CrewAgentExecutorMixin as _CrewAgentExecutorMixin,
)
from crewai.agents.tools_handler import ToolsHandler as _ToolsHandler
from crewai.experimental.agent_executor import AgentExecutor as _AgentExecutor
from crewai.hooks.llm_hooks import LLMCallHookContext as _LLMCallHookContext
@@ -110,66 +105,25 @@ try:
SystemPromptResult as _SystemPromptResult,
)
_base_namespace: dict[str, type] = {
"Agent": Agent,
"Crew": Crew,
"BaseLLM": BaseLLM,
"Task": Task,
"CrewAgentExecutorMixin": _CrewAgentExecutorMixin,
}
try:
from crewai.a2a.config import (
A2AClientConfig as _A2AClientConfig,
A2AConfig as _A2AConfig,
A2AServerConfig as _A2AServerConfig,
)
_base_namespace.update(
{
"A2AConfig": _A2AConfig,
"A2AClientConfig": _A2AClientConfig,
"A2AServerConfig": _A2AServerConfig,
}
)
except ImportError:
pass
import sys
_full_namespace = {
**_base_namespace,
"ToolsHandler": _ToolsHandler,
"StandardPromptResult": _StandardPromptResult,
"SystemPromptResult": _SystemPromptResult,
"LLMCallHookContext": _LLMCallHookContext,
"ToolResult": _ToolResult,
}
_resolve_namespace = {
**_full_namespace,
**sys.modules[_BaseAgent.__module__].__dict__,
}
for _mod_name in (
_BaseAgent.__module__,
Agent.__module__,
_AgentExecutor.__module__,
):
sys.modules[_mod_name].__dict__.update(_resolve_namespace)
_BaseAgent.model_rebuild(force=True, _types_namespace=_full_namespace)
_AgentExecutor.model_rebuild(force=True, _types_namespace=_full_namespace)
try:
Agent.model_rebuild(force=True, _types_namespace=_full_namespace)
except PydanticUserError:
pass
except (ImportError, PydanticUserError):
_AgentExecutor.model_rebuild(
force=True,
_types_namespace={
"Agent": Agent,
"ToolsHandler": _ToolsHandler,
"Crew": Crew,
"BaseLLM": BaseLLM,
"Task": Task,
"StandardPromptResult": _StandardPromptResult,
"SystemPromptResult": _SystemPromptResult,
"LLMCallHookContext": _LLMCallHookContext,
"ToolResult": _ToolResult,
},
)
except (ImportError, PydanticUserError, PydanticUndefinedAnnotation):
import logging as _logging
_logging.getLogger(__name__).warning(
"model_rebuild() failed; forward refs may be unresolved.",
_logging.getLogger(__name__).debug(
"AgentExecutor.model_rebuild() deferred; forward refs may be unresolved.",
exc_info=True,
)
@@ -179,7 +133,6 @@ __all__ = [
"BaseLLM",
"Crew",
"CrewOutput",
"ExecutionContext",
"Flow",
"Knowledge",
"LLMGuardrail",

View File

@@ -1,10 +0,0 @@
"""Agent-to-Agent (A2A) protocol communication module for CrewAI."""
from crewai.a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
__all__ = [
"A2AClientConfig",
"A2AConfig",
"A2AServerConfig",
]

View File

@@ -1,71 +0,0 @@
"""Deprecated: Authentication schemes for A2A protocol agents.
This module is deprecated. Import from crewai.a2a.auth instead:
- crewai.a2a.auth.ClientAuthScheme (replaces AuthScheme)
- crewai.a2a.auth.BearerTokenAuth
- crewai.a2a.auth.HTTPBasicAuth
- crewai.a2a.auth.HTTPDigestAuth
- crewai.a2a.auth.APIKeyAuth
- crewai.a2a.auth.OAuth2ClientCredentials
- crewai.a2a.auth.OAuth2AuthorizationCode
"""
from __future__ import annotations
from typing_extensions import deprecated
from crewai.a2a.auth.client_schemes import (
APIKeyAuth as _APIKeyAuth,
BearerTokenAuth as _BearerTokenAuth,
ClientAuthScheme as _ClientAuthScheme,
HTTPBasicAuth as _HTTPBasicAuth,
HTTPDigestAuth as _HTTPDigestAuth,
OAuth2AuthorizationCode as _OAuth2AuthorizationCode,
OAuth2ClientCredentials as _OAuth2ClientCredentials,
)
@deprecated("Use ClientAuthScheme from crewai.a2a.auth instead", category=FutureWarning)
class AuthScheme(_ClientAuthScheme):
"""Deprecated: Use ClientAuthScheme from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class BearerTokenAuth(_BearerTokenAuth):
"""Deprecated: Import from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class HTTPBasicAuth(_HTTPBasicAuth):
"""Deprecated: Import from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class HTTPDigestAuth(_HTTPDigestAuth):
"""Deprecated: Import from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class APIKeyAuth(_APIKeyAuth):
"""Deprecated: Import from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class OAuth2ClientCredentials(_OAuth2ClientCredentials):
"""Deprecated: Import from crewai.a2a.auth instead."""
@deprecated("Import from crewai.a2a.auth instead", category=FutureWarning)
class OAuth2AuthorizationCode(_OAuth2AuthorizationCode):
"""Deprecated: Import from crewai.a2a.auth instead."""
__all__ = [
"APIKeyAuth",
"AuthScheme",
"BearerTokenAuth",
"HTTPBasicAuth",
"HTTPDigestAuth",
"OAuth2AuthorizationCode",
"OAuth2ClientCredentials",
]

View File

@@ -1,148 +0,0 @@
"""A2UI (Agent to UI) declarative UI protocol support for CrewAI."""
from crewai.a2a.extensions.a2ui.catalog import (
AudioPlayer,
Button,
Card,
CheckBox,
Column,
DateTimeInput,
Divider,
Icon,
Image,
List,
Modal,
MultipleChoice,
Row,
Slider,
Tabs,
Text,
TextField,
Video,
)
from crewai.a2a.extensions.a2ui.client_extension import A2UIClientExtension
from crewai.a2a.extensions.a2ui.models import (
A2UIEvent,
A2UIMessage,
A2UIResponse,
BeginRendering,
DataModelUpdate,
DeleteSurface,
SurfaceUpdate,
UserAction,
)
from crewai.a2a.extensions.a2ui.server_extension import (
A2UI_STANDARD_CATALOG_ID,
A2UI_V09_BASIC_CATALOG_ID,
A2UI_V09_EXTENSION_URI,
A2UIServerExtension,
)
from crewai.a2a.extensions.a2ui.v0_9 import (
A2UIEventV09,
A2UIMessageV09,
ActionEvent,
ActionV09,
AudioPlayerV09,
ButtonV09,
CardV09,
CheckBoxV09,
ChoicePickerV09,
ClientDataModel,
ClientErrorV09,
ColumnV09,
CreateSurface,
DateTimeInputV09,
DeleteSurfaceV09,
DividerV09,
IconV09,
ImageV09,
ListV09,
ModalV09,
RowV09,
SliderV09,
TabsV09,
TextFieldV09,
TextV09,
Theme,
UpdateComponents,
UpdateDataModel,
VideoV09,
)
from crewai.a2a.extensions.a2ui.validator import (
validate_a2ui_event,
validate_a2ui_event_v09,
validate_a2ui_message,
validate_a2ui_message_v09,
validate_catalog_components,
validate_catalog_components_v09,
)
__all__ = [
"A2UI_STANDARD_CATALOG_ID",
"A2UI_V09_BASIC_CATALOG_ID",
"A2UI_V09_EXTENSION_URI",
"A2UIClientExtension",
"A2UIEvent",
"A2UIEventV09",
"A2UIMessage",
"A2UIMessageV09",
"A2UIResponse",
"A2UIServerExtension",
"ActionEvent",
"ActionV09",
"AudioPlayer",
"AudioPlayerV09",
"BeginRendering",
"Button",
"ButtonV09",
"Card",
"CardV09",
"CheckBox",
"CheckBoxV09",
"ChoicePickerV09",
"ClientDataModel",
"ClientErrorV09",
"Column",
"ColumnV09",
"CreateSurface",
"DataModelUpdate",
"DateTimeInput",
"DateTimeInputV09",
"DeleteSurface",
"DeleteSurfaceV09",
"Divider",
"DividerV09",
"Icon",
"IconV09",
"Image",
"ImageV09",
"List",
"ListV09",
"Modal",
"ModalV09",
"MultipleChoice",
"Row",
"RowV09",
"Slider",
"SliderV09",
"SurfaceUpdate",
"Tabs",
"TabsV09",
"Text",
"TextField",
"TextFieldV09",
"TextV09",
"Theme",
"UpdateComponents",
"UpdateDataModel",
"UserAction",
"Video",
"VideoV09",
"validate_a2ui_event",
"validate_a2ui_event_v09",
"validate_a2ui_message",
"validate_a2ui_message_v09",
"validate_catalog_components",
"validate_catalog_components_v09",
]

View File

@@ -1,467 +0,0 @@
"""Typed helpers for A2UI standard catalog components.
These models provide optional type safety for standard catalog components.
Agents can also use raw dicts validated against the JSON schema.
"""
from __future__ import annotations
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
class StringBinding(BaseModel):
"""A string value: literal or data-model path."""
literal_string: str | None = Field(
default=None, alias="literalString", description="Literal string value."
)
path: str | None = Field(default=None, description="Data-model path reference.")
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class NumberBinding(BaseModel):
"""A numeric value: literal or data-model path."""
literal_number: float | None = Field(
default=None, alias="literalNumber", description="Literal numeric value."
)
path: str | None = Field(default=None, description="Data-model path reference.")
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class BooleanBinding(BaseModel):
"""A boolean value: literal or data-model path."""
literal_boolean: bool | None = Field(
default=None, alias="literalBoolean", description="Literal boolean value."
)
path: str | None = Field(default=None, description="Data-model path reference.")
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class ArrayBinding(BaseModel):
"""An array value: literal or data-model path."""
literal_array: list[str] | None = Field(
default=None, alias="literalArray", description="Literal array of strings."
)
path: str | None = Field(default=None, description="Data-model path reference.")
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class ChildrenDef(BaseModel):
"""Children definition for layout components."""
explicit_list: list[str] | None = Field(
default=None,
alias="explicitList",
description="Explicit list of child component IDs.",
)
template: ChildTemplate | None = Field(
default=None, description="Template for generating dynamic children."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class ChildTemplate(BaseModel):
"""Template for generating dynamic children from a data model list."""
component_id: str = Field(
alias="componentId", description="ID of the component to repeat."
)
data_binding: str = Field(
alias="dataBinding", description="Data-model path to bind the template to."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class ActionContextEntry(BaseModel):
"""A key-value pair in an action context payload."""
key: str = Field(description="Context entry key.")
value: ActionBoundValue = Field(description="Context entry value.")
model_config = ConfigDict(extra="forbid")
class ActionBoundValue(BaseModel):
"""A value in an action context: literal or data-model path."""
path: str | None = Field(default=None, description="Data-model path reference.")
literal_string: str | None = Field(
default=None, alias="literalString", description="Literal string value."
)
literal_number: float | None = Field(
default=None, alias="literalNumber", description="Literal numeric value."
)
literal_boolean: bool | None = Field(
default=None, alias="literalBoolean", description="Literal boolean value."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class Action(BaseModel):
"""Client-side action dispatched by interactive components."""
name: str = Field(description="Action name dispatched on interaction.")
context: list[ActionContextEntry] | None = Field(
default=None, description="Key-value pairs sent with the action."
)
model_config = ConfigDict(extra="forbid")
class TabItem(BaseModel):
"""A single tab definition."""
title: StringBinding = Field(description="Tab title text.")
child: str = Field(description="Component ID rendered as the tab content.")
model_config = ConfigDict(extra="forbid")
class MultipleChoiceOption(BaseModel):
"""A single option in a MultipleChoice component."""
label: StringBinding = Field(description="Display label for the option.")
value: str = Field(description="Value submitted when the option is selected.")
model_config = ConfigDict(extra="forbid")
class Text(BaseModel):
"""Displays text content."""
text: StringBinding = Field(description="Text content to display.")
usage_hint: Literal["h1", "h2", "h3", "h4", "h5", "caption", "body"] | None = Field(
default=None, alias="usageHint", description="Semantic hint for text styling."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class Image(BaseModel):
"""Displays an image."""
url: StringBinding = Field(description="Image source URL.")
fit: Literal["contain", "cover", "fill", "none", "scale-down"] | None = Field(
default=None, description="Object-fit behavior for the image."
)
usage_hint: (
Literal[
"icon", "avatar", "smallFeature", "mediumFeature", "largeFeature", "header"
]
| None
) = Field(
default=None, alias="usageHint", description="Semantic hint for image sizing."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
IconName = Literal[
"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",
]
class IconBinding(BaseModel):
"""Icon name: literal enum or data-model path."""
literal_string: IconName | None = Field(
default=None, alias="literalString", description="Literal icon name."
)
path: str | None = Field(default=None, description="Data-model path reference.")
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class Icon(BaseModel):
"""Displays a named icon."""
name: IconBinding = Field(description="Icon name binding.")
model_config = ConfigDict(extra="forbid")
class Video(BaseModel):
"""Displays a video player."""
url: StringBinding = Field(description="Video source URL.")
model_config = ConfigDict(extra="forbid")
class AudioPlayer(BaseModel):
"""Displays an audio player."""
url: StringBinding = Field(description="Audio source URL.")
description: StringBinding | None = Field(
default=None, description="Accessible description of the audio content."
)
model_config = ConfigDict(extra="forbid")
class Row(BaseModel):
"""Horizontal layout container."""
children: ChildrenDef = Field(description="Child components in this row.")
distribution: (
Literal["center", "end", "spaceAround", "spaceBetween", "spaceEvenly", "start"]
| None
) = Field(
default=None, description="How children are distributed along the main axis."
)
alignment: Literal["start", "center", "end", "stretch"] | None = Field(
default=None, description="How children are aligned on the cross axis."
)
model_config = ConfigDict(extra="forbid")
class Column(BaseModel):
"""Vertical layout container."""
children: ChildrenDef = Field(description="Child components in this column.")
distribution: (
Literal["start", "center", "end", "spaceBetween", "spaceAround", "spaceEvenly"]
| None
) = Field(
default=None, description="How children are distributed along the main axis."
)
alignment: Literal["center", "end", "start", "stretch"] | None = Field(
default=None, description="How children are aligned on the cross axis."
)
model_config = ConfigDict(extra="forbid")
class List(BaseModel):
"""Scrollable list container."""
children: ChildrenDef = Field(description="Child components in this list.")
direction: Literal["vertical", "horizontal"] | None = Field(
default=None, description="Scroll direction of the list."
)
alignment: Literal["start", "center", "end", "stretch"] | None = Field(
default=None, description="How children are aligned on the cross axis."
)
model_config = ConfigDict(extra="forbid")
class Card(BaseModel):
"""Card container wrapping a single child."""
child: str = Field(description="Component ID of the card content.")
model_config = ConfigDict(extra="forbid")
class Tabs(BaseModel):
"""Tabbed navigation container."""
tab_items: list[TabItem] = Field(
alias="tabItems", description="List of tab definitions."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class Divider(BaseModel):
"""A visual divider line."""
axis: Literal["horizontal", "vertical"] | None = Field(
default=None, description="Orientation of the divider."
)
model_config = ConfigDict(extra="forbid")
class Modal(BaseModel):
"""A modal dialog with an entry point trigger and content."""
entry_point_child: str = Field(
alias="entryPointChild", description="Component ID that triggers the modal."
)
content_child: str = Field(
alias="contentChild", description="Component ID rendered inside the modal."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class Button(BaseModel):
"""An interactive button with an action."""
child: str = Field(description="Component ID of the button label.")
primary: bool | None = Field(
default=None, description="Whether the button uses primary styling."
)
action: Action = Field(description="Action dispatched when the button is clicked.")
model_config = ConfigDict(extra="forbid")
class CheckBox(BaseModel):
"""A checkbox input."""
label: StringBinding = Field(description="Label text for the checkbox.")
value: BooleanBinding = Field(
description="Boolean value binding for the checkbox state."
)
model_config = ConfigDict(extra="forbid")
class TextField(BaseModel):
"""A text input field."""
label: StringBinding = Field(description="Label text for the input.")
text: StringBinding | None = Field(
default=None, description="Current text value binding."
)
text_field_type: (
Literal["date", "longText", "number", "shortText", "obscured"] | None
) = Field(default=None, alias="textFieldType", description="Input type variant.")
validation_regexp: str | None = Field(
default=None,
alias="validationRegexp",
description="Regex pattern for client-side validation.",
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class DateTimeInput(BaseModel):
"""A date and/or time picker."""
value: StringBinding = Field(description="ISO date/time string value binding.")
enable_date: bool | None = Field(
default=None,
alias="enableDate",
description="Whether the date picker is enabled.",
)
enable_time: bool | None = Field(
default=None,
alias="enableTime",
description="Whether the time picker is enabled.",
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class MultipleChoice(BaseModel):
"""A multiple-choice selection component."""
selections: ArrayBinding = Field(description="Array binding for selected values.")
options: list[MultipleChoiceOption] = Field(description="Available choices.")
max_allowed_selections: int | None = Field(
default=None,
alias="maxAllowedSelections",
description="Maximum number of selections allowed.",
)
variant: Literal["checkbox", "chips"] | None = Field(
default=None, description="Visual variant for the selection UI."
)
filterable: bool | None = Field(
default=None, description="Whether options can be filtered by typing."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class Slider(BaseModel):
"""A numeric slider input."""
value: NumberBinding = Field(
description="Numeric value binding for the slider position."
)
min_value: float | None = Field(
default=None, alias="minValue", description="Minimum slider value."
)
max_value: float | None = Field(
default=None, alias="maxValue", description="Maximum slider value."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
STANDARD_CATALOG_COMPONENTS: frozenset[str] = frozenset(
{
"Text",
"Image",
"Icon",
"Video",
"AudioPlayer",
"Row",
"Column",
"List",
"Card",
"Tabs",
"Divider",
"Modal",
"Button",
"CheckBox",
"TextField",
"DateTimeInput",
"MultipleChoice",
"Slider",
}
)

View File

@@ -1,496 +0,0 @@
"""A2UI client extension for the A2A protocol."""
from __future__ import annotations
from collections.abc import Sequence
import logging
from typing import TYPE_CHECKING, Any, Literal, cast
from pydantic import Field
from pydantic.dataclasses import dataclass
from typing_extensions import TypeIs, TypedDict
from crewai.a2a.extensions.a2ui.models import extract_a2ui_json_objects
from crewai.a2a.extensions.a2ui.prompt import (
build_a2ui_system_prompt,
build_a2ui_v09_system_prompt,
)
from crewai.a2a.extensions.a2ui.server_extension import (
A2UI_MIME_TYPE,
A2UI_STANDARD_CATALOG_ID,
A2UI_V09_BASIC_CATALOG_ID,
)
from crewai.a2a.extensions.a2ui.v0_9 import extract_a2ui_v09_json_objects
from crewai.a2a.extensions.a2ui.validator import (
A2UIValidationError,
validate_a2ui_message,
validate_a2ui_message_v09,
)
if TYPE_CHECKING:
from a2a.types import Message
from crewai.agent.core import Agent
logger = logging.getLogger(__name__)
class StylesDict(TypedDict, total=False):
"""Serialized surface styling."""
font: str
primaryColor: str
class ComponentEntryDict(TypedDict, total=False):
"""Serialized component entry in a surface update."""
id: str
weight: float
component: dict[str, Any]
class BeginRenderingDict(TypedDict, total=False):
"""Serialized beginRendering payload."""
surfaceId: str
root: str
catalogId: str
styles: StylesDict
class SurfaceUpdateDict(TypedDict, total=False):
"""Serialized surfaceUpdate payload."""
surfaceId: str
components: list[ComponentEntryDict]
class DataEntryDict(TypedDict, total=False):
"""Serialized data model entry."""
key: str
valueString: str
valueNumber: float
valueBoolean: bool
valueMap: list[DataEntryDict]
class DataModelUpdateDict(TypedDict, total=False):
"""Serialized dataModelUpdate payload."""
surfaceId: str
path: str
contents: list[DataEntryDict]
class DeleteSurfaceDict(TypedDict):
"""Serialized deleteSurface payload."""
surfaceId: str
class A2UIMessageDict(TypedDict, total=False):
"""Serialized A2UI v0.8 server-to-client message with exactly one key set."""
beginRendering: BeginRenderingDict
surfaceUpdate: SurfaceUpdateDict
dataModelUpdate: DataModelUpdateDict
deleteSurface: DeleteSurfaceDict
class ThemeDict(TypedDict, total=False):
"""Serialized v0.9 theme."""
primaryColor: str
iconUrl: str
agentDisplayName: str
class CreateSurfaceDict(TypedDict, total=False):
"""Serialized createSurface payload."""
surfaceId: str
catalogId: str
theme: ThemeDict
sendDataModel: bool
class UpdateComponentsDict(TypedDict, total=False):
"""Serialized updateComponents payload."""
surfaceId: str
components: list[dict[str, Any]]
class UpdateDataModelDict(TypedDict, total=False):
"""Serialized updateDataModel payload."""
surfaceId: str
path: str
value: Any
class DeleteSurfaceV09Dict(TypedDict):
"""Serialized v0.9 deleteSurface payload."""
surfaceId: str
class A2UIMessageV09Dict(TypedDict, total=False):
"""Serialized A2UI v0.9 server-to-client message with version and exactly one key set."""
version: Literal["v0.9"]
createSurface: CreateSurfaceDict
updateComponents: UpdateComponentsDict
updateDataModel: UpdateDataModelDict
deleteSurface: DeleteSurfaceV09Dict
A2UIAnyMessageDict = A2UIMessageDict | A2UIMessageV09Dict
def is_v09_message(msg: A2UIAnyMessageDict) -> TypeIs[A2UIMessageV09Dict]:
"""Narrow a message dict to the v0.9 variant."""
return msg.get("version") == "v0.9"
def is_v08_message(msg: A2UIAnyMessageDict) -> TypeIs[A2UIMessageDict]:
"""Narrow a message dict to the v0.8 variant."""
return "version" not in msg
@dataclass
class A2UIConversationState:
"""Tracks active A2UI surfaces and data models across a conversation."""
active_surfaces: dict[str, dict[str, Any]] = Field(default_factory=dict)
data_models: dict[str, list[dict[str, Any]]] = Field(default_factory=dict)
last_a2ui_messages: list[A2UIAnyMessageDict] = Field(default_factory=list)
initialized_surfaces: set[str] = Field(default_factory=set)
def is_ready(self) -> bool:
"""Return True when at least one surface has been initialized via beginRendering."""
return bool(self.initialized_surfaces)
class A2UIClientExtension:
"""A2A client extension that adds A2UI support to agents.
Implements the ``A2AExtension`` protocol to inject A2UI prompt
instructions, track UI state across conversations, and validate
A2UI messages in responses.
Example::
A2AClientConfig(
endpoint="...",
extensions=["https://a2ui.org/a2a-extension/a2ui/v0.8"],
client_extensions=[A2UIClientExtension()],
)
"""
def __init__(
self,
catalog_id: str | None = None,
allowed_components: list[str] | None = None,
version: str = "v0.8",
) -> None:
"""Initialize the A2UI client extension.
Args:
catalog_id: Catalog identifier to use for prompt generation.
allowed_components: Subset of component names to expose to the agent.
version: Protocol version, ``"v0.8"`` or ``"v0.9"``.
"""
self._catalog_id = catalog_id
self._allowed_components = allowed_components
self._version = version
def inject_tools(self, agent: Agent) -> None:
"""No-op — A2UI uses prompt augmentation rather than tool injection."""
def extract_state_from_history(
self, conversation_history: Sequence[Message]
) -> A2UIConversationState | None:
"""Scan conversation history for A2UI DataParts and track surface state.
When ``catalog_id`` is set, only surfaces matching that catalog are tracked.
"""
state = A2UIConversationState()
for message in conversation_history:
for part in message.parts:
root = part.root
if root.kind != "data":
continue
metadata = root.metadata or {}
mime_type = metadata.get("mimeType", "")
if mime_type != A2UI_MIME_TYPE:
continue
data = root.data
if not isinstance(data, dict):
continue
surface_id = _get_surface_id(data)
if not surface_id:
continue
if self._catalog_id and "beginRendering" in data:
catalog_id = data["beginRendering"].get("catalogId")
if catalog_id and catalog_id != self._catalog_id:
continue
if self._catalog_id and "createSurface" in data:
catalog_id = data["createSurface"].get("catalogId")
if catalog_id and catalog_id != self._catalog_id:
continue
if "deleteSurface" in data:
state.active_surfaces.pop(surface_id, None)
state.data_models.pop(surface_id, None)
state.initialized_surfaces.discard(surface_id)
elif "beginRendering" in data:
state.initialized_surfaces.add(surface_id)
state.active_surfaces[surface_id] = data["beginRendering"]
elif "createSurface" in data:
state.initialized_surfaces.add(surface_id)
state.active_surfaces[surface_id] = data["createSurface"]
elif "surfaceUpdate" in data:
if surface_id not in state.initialized_surfaces:
logger.warning(
"surfaceUpdate for uninitialized surface %s",
surface_id,
)
state.active_surfaces[surface_id] = data["surfaceUpdate"]
elif "updateComponents" in data:
if surface_id not in state.initialized_surfaces:
logger.warning(
"updateComponents for uninitialized surface %s",
surface_id,
)
state.active_surfaces[surface_id] = data["updateComponents"]
elif "dataModelUpdate" in data:
contents = data["dataModelUpdate"].get("contents", [])
state.data_models.setdefault(surface_id, []).extend(contents)
elif "updateDataModel" in data:
update = data["updateDataModel"]
state.data_models.setdefault(surface_id, []).append(update)
if not state.active_surfaces and not state.data_models:
return None
return state
def augment_prompt(
self,
base_prompt: str,
_conversation_state: A2UIConversationState | None,
) -> str:
"""Append A2UI system prompt instructions to the base prompt."""
if self._version == "v0.9":
a2ui_prompt = build_a2ui_v09_system_prompt(
catalog_id=self._catalog_id,
allowed_components=self._allowed_components,
)
else:
a2ui_prompt = build_a2ui_system_prompt(
catalog_id=self._catalog_id,
allowed_components=self._allowed_components,
)
return f"{base_prompt}\n\n{a2ui_prompt}"
def process_response(
self,
agent_response: Any,
conversation_state: A2UIConversationState | None,
) -> Any:
"""Extract and validate A2UI JSON from agent output.
When ``allowed_components`` is set, components not in the allowlist are
logged and stripped from surface updates. Stores extracted A2UI messages
on the conversation state and returns the original response unchanged.
"""
text = (
agent_response if isinstance(agent_response, str) else str(agent_response)
)
results: list[A2UIAnyMessageDict]
if self._version == "v0.9":
results = list(_extract_and_validate_v09(text))
if self._allowed_components:
allowed = set(self._allowed_components)
results = [
_filter_components_v09(m, allowed)
for m in results
if is_v09_message(m)
]
else:
results = list(_extract_and_validate(text))
if self._allowed_components:
allowed = set(self._allowed_components)
results = [
_filter_components(msg, allowed)
for msg in results
if is_v08_message(msg)
]
if results and conversation_state is not None:
conversation_state.last_a2ui_messages = results
return agent_response
def prepare_message_metadata(
self,
_conversation_state: A2UIConversationState | None,
) -> dict[str, Any]:
"""Inject a2uiClientCapabilities into outbound A2A message metadata.
Per the A2UI extension spec, clients must declare supported catalog
IDs in every outbound message's metadata. v0.9 nests capabilities
under a ``"v0.9"`` key per ``client_capabilities.json``.
"""
if self._version == "v0.9":
default_catalog = A2UI_V09_BASIC_CATALOG_ID
catalog_ids = [default_catalog]
if self._catalog_id and self._catalog_id != default_catalog:
catalog_ids.append(self._catalog_id)
return {
"a2uiClientCapabilities": {
"v0.9": {
"supportedCatalogIds": catalog_ids,
},
},
}
catalog_ids = [A2UI_STANDARD_CATALOG_ID]
if self._catalog_id and self._catalog_id != A2UI_STANDARD_CATALOG_ID:
catalog_ids.append(self._catalog_id)
return {
"a2uiClientCapabilities": {
"supportedCatalogIds": catalog_ids,
},
}
_ALL_SURFACE_ID_KEYS = (
"beginRendering",
"surfaceUpdate",
"dataModelUpdate",
"deleteSurface",
"createSurface",
"updateComponents",
"updateDataModel",
)
def _get_surface_id(data: dict[str, Any]) -> str | None:
"""Extract surfaceId from any A2UI v0.8 or v0.9 message type."""
for key in _ALL_SURFACE_ID_KEYS:
inner = data.get(key)
if isinstance(inner, dict):
sid = inner.get("surfaceId")
if isinstance(sid, str):
return sid
return None
def _filter_components(msg: A2UIMessageDict, allowed: set[str]) -> A2UIMessageDict:
"""Strip components whose type is not in *allowed* from a surfaceUpdate."""
surface_update = msg.get("surfaceUpdate")
if not isinstance(surface_update, dict):
return msg
components = surface_update.get("components")
if not isinstance(components, list):
return msg
filtered = []
for entry in components:
component = entry.get("component", {})
component_types = set(component.keys())
disallowed = component_types - allowed
if disallowed:
logger.debug(
"Stripping disallowed component type(s) %s from surface update",
disallowed,
)
continue
filtered.append(entry)
if len(filtered) == len(components):
return msg
return {**msg, "surfaceUpdate": {**surface_update, "components": filtered}}
def _filter_components_v09(
msg: A2UIMessageV09Dict, allowed: set[str]
) -> A2UIMessageV09Dict:
"""Strip v0.9 components whose type is not in *allowed* from updateComponents.
v0.9 components use a flat structure where ``component`` is a type-name string.
"""
update = msg.get("updateComponents")
if not isinstance(update, dict):
return msg
components = update.get("components")
if not isinstance(components, list):
return msg
filtered = []
for entry in components:
comp_type = entry.get("component") if isinstance(entry, dict) else None
if isinstance(comp_type, str) and comp_type not in allowed:
logger.debug("Stripping disallowed v0.9 component type %s", comp_type)
continue
filtered.append(entry)
if len(filtered) == len(components):
return msg
return {**msg, "updateComponents": {**update, "components": filtered}}
def _extract_and_validate(text: str) -> list[A2UIMessageDict]:
"""Extract A2UI v0.8 JSON objects from text and validate them."""
return [
dumped
for candidate in extract_a2ui_json_objects(text)
if (dumped := _try_validate(candidate)) is not None
]
def _try_validate(candidate: dict[str, Any]) -> A2UIMessageDict | None:
"""Validate a single v0.8 A2UI candidate, returning None on failure."""
try:
msg = validate_a2ui_message(candidate)
except A2UIValidationError:
logger.debug(
"Skipping invalid A2UI candidate in agent output",
exc_info=True,
)
return None
return cast(A2UIMessageDict, msg.model_dump(by_alias=True, exclude_none=True))
def _extract_and_validate_v09(text: str) -> list[A2UIMessageV09Dict]:
"""Extract and validate v0.9 A2UI JSON objects from text."""
return [
dumped
for candidate in extract_a2ui_v09_json_objects(text)
if (dumped := _try_validate_v09(candidate)) is not None
]
def _try_validate_v09(candidate: dict[str, Any]) -> A2UIMessageV09Dict | None:
"""Validate a single v0.9 A2UI candidate, returning None on failure."""
try:
msg = validate_a2ui_message_v09(candidate)
except A2UIValidationError:
logger.debug(
"Skipping invalid A2UI v0.9 candidate in agent output",
exc_info=True,
)
return None
return cast(A2UIMessageV09Dict, msg.model_dump(by_alias=True, exclude_none=True))

View File

@@ -1,277 +0,0 @@
"""Pydantic models for A2UI server-to-client messages and client-to-server events."""
from __future__ import annotations
import json
import re
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, model_validator
class BoundValue(BaseModel):
"""A value that can be a literal or a data-model path reference."""
literal_string: str | None = Field(
default=None, alias="literalString", description="Literal string value."
)
literal_number: float | None = Field(
default=None, alias="literalNumber", description="Literal numeric value."
)
literal_boolean: bool | None = Field(
default=None, alias="literalBoolean", description="Literal boolean value."
)
literal_array: list[str] | None = Field(
default=None, alias="literalArray", description="Literal array of strings."
)
path: str | None = Field(default=None, description="Data-model path reference.")
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class MapEntry(BaseModel):
"""A single entry in a valueMap adjacency list, supporting recursive nesting."""
key: str = Field(description="Entry key.")
value_string: str | None = Field(
default=None, alias="valueString", description="String value."
)
value_number: float | None = Field(
default=None, alias="valueNumber", description="Numeric value."
)
value_boolean: bool | None = Field(
default=None, alias="valueBoolean", description="Boolean value."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class DataEntry(BaseModel):
"""A data model entry with a key and exactly one typed value."""
key: str = Field(description="Entry key.")
value_string: str | None = Field(
default=None, alias="valueString", description="String value."
)
value_number: float | None = Field(
default=None, alias="valueNumber", description="Numeric value."
)
value_boolean: bool | None = Field(
default=None, alias="valueBoolean", description="Boolean value."
)
value_map: list[MapEntry] | None = Field(
default=None, alias="valueMap", description="Nested map entries."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
_HEX_COLOR_PATTERN: re.Pattern[str] = re.compile(r"^#[0-9a-fA-F]{6}$")
class Styles(BaseModel):
"""Surface styling information."""
font: str | None = Field(default=None, description="Font family name.")
primary_color: str | None = Field(
default=None,
alias="primaryColor",
pattern=_HEX_COLOR_PATTERN.pattern,
description="Primary color as a hex string.",
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class ComponentEntry(BaseModel):
"""A single component in a UI widget tree.
The ``component`` dict must contain exactly one key — the component type
name (e.g. ``"Text"``, ``"Button"``) — whose value holds the component
properties. Component internals are left as ``dict[str, Any]`` because
they are catalog-dependent; use the typed helpers in ``catalog.py`` for
the standard catalog.
"""
id: str = Field(description="Unique component identifier.")
weight: float | None = Field(
default=None, description="Flex weight for layout distribution."
)
component: dict[str, Any] = Field(
description="Component type name mapped to its properties."
)
model_config = ConfigDict(extra="forbid")
class BeginRendering(BaseModel):
"""Signals the client to begin rendering a surface."""
surface_id: str = Field(alias="surfaceId", description="Unique surface identifier.")
root: str = Field(description="Component ID of the root element.")
catalog_id: str | None = Field(
default=None,
alias="catalogId",
description="Catalog identifier for the surface.",
)
styles: Styles | None = Field(
default=None, description="Surface styling overrides."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class SurfaceUpdate(BaseModel):
"""Updates a surface with a new set of components."""
surface_id: str = Field(alias="surfaceId", description="Target surface identifier.")
components: list[ComponentEntry] = Field(
min_length=1, description="Components to render on the surface."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class DataModelUpdate(BaseModel):
"""Updates the data model for a surface."""
surface_id: str = Field(alias="surfaceId", description="Target surface identifier.")
path: str | None = Field(
default=None, description="Data-model path prefix for the update."
)
contents: list[DataEntry] = Field(
description="Data entries to merge into the model."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class DeleteSurface(BaseModel):
"""Signals the client to delete a surface."""
surface_id: str = Field(
alias="surfaceId", description="Surface identifier to delete."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class A2UIMessage(BaseModel):
"""Union wrapper for the four server-to-client A2UI message types.
Exactly one of the fields must be set.
"""
begin_rendering: BeginRendering | None = Field(
default=None,
alias="beginRendering",
description="Begin rendering a new surface.",
)
surface_update: SurfaceUpdate | None = Field(
default=None,
alias="surfaceUpdate",
description="Update components on a surface.",
)
data_model_update: DataModelUpdate | None = Field(
default=None,
alias="dataModelUpdate",
description="Update the surface data model.",
)
delete_surface: DeleteSurface | None = Field(
default=None, alias="deleteSurface", description="Delete an existing surface."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
@model_validator(mode="after")
def _check_exactly_one(self) -> A2UIMessage:
"""Enforce the spec's exactly-one-of constraint."""
fields = [
self.begin_rendering,
self.surface_update,
self.data_model_update,
self.delete_surface,
]
count = sum(f is not None for f in fields)
if count != 1:
raise ValueError(f"Exactly one A2UI message type must be set, got {count}")
return self
class UserAction(BaseModel):
"""Reports a user-initiated action from a component."""
name: str = Field(description="Action name.")
surface_id: str = Field(alias="surfaceId", description="Source surface identifier.")
source_component_id: str = Field(
alias="sourceComponentId", description="Component that triggered the action."
)
timestamp: str = Field(description="ISO 8601 timestamp of the action.")
context: dict[str, Any] = Field(description="Action context payload.")
model_config = ConfigDict(populate_by_name=True)
class ClientError(BaseModel):
"""Reports a client-side error."""
model_config = ConfigDict(extra="allow")
class A2UIEvent(BaseModel):
"""Union wrapper for client-to-server events."""
user_action: UserAction | None = Field(
default=None, alias="userAction", description="User-initiated action event."
)
error: ClientError | None = Field(
default=None, description="Client-side error report."
)
model_config = ConfigDict(populate_by_name=True)
@model_validator(mode="after")
def _check_exactly_one(self) -> A2UIEvent:
"""Enforce the spec's exactly-one-of constraint."""
fields = [self.user_action, self.error]
count = sum(f is not None for f in fields)
if count != 1:
raise ValueError(f"Exactly one A2UI event type must be set, got {count}")
return self
class A2UIResponse(BaseModel):
"""Typed wrapper for responses containing A2UI messages."""
text: str = Field(description="Raw text content of the response.")
a2ui_parts: list[dict[str, Any]] = Field(
default_factory=list, description="A2UI DataParts extracted from the response."
)
a2ui_messages: list[dict[str, Any]] = Field(
default_factory=list, description="Validated A2UI message dicts."
)
_A2UI_KEYS = {"beginRendering", "surfaceUpdate", "dataModelUpdate", "deleteSurface"}
def extract_a2ui_json_objects(text: str) -> list[dict[str, Any]]:
"""Extract JSON objects containing A2UI keys from text.
Uses ``json.JSONDecoder.raw_decode`` for robust parsing that correctly
handles braces inside string literals.
"""
decoder = json.JSONDecoder()
results: list[dict[str, Any]] = []
idx = 0
while idx < len(text):
idx = text.find("{", idx)
if idx == -1:
break
try:
obj, end_idx = decoder.raw_decode(text, idx)
if isinstance(obj, dict) and _A2UI_KEYS & obj.keys():
results.append(obj)
idx = end_idx
except json.JSONDecodeError:
idx += 1
return results

View File

@@ -1,150 +0,0 @@
"""System prompt generation for A2UI-capable agents."""
from __future__ import annotations
import json
from crewai.a2a.extensions.a2ui.catalog import STANDARD_CATALOG_COMPONENTS
from crewai.a2a.extensions.a2ui.schema import load_schema
from crewai.a2a.extensions.a2ui.server_extension import (
A2UI_EXTENSION_URI,
A2UI_V09_BASIC_CATALOG_ID,
)
from crewai.a2a.extensions.a2ui.v0_9 import (
BASIC_CATALOG_COMPONENTS as V09_CATALOG_COMPONENTS,
BASIC_CATALOG_FUNCTIONS,
)
def build_a2ui_system_prompt(
catalog_id: str | None = None,
allowed_components: list[str] | None = None,
) -> str:
"""Build a v0.8 system prompt fragment instructing the LLM to produce A2UI output.
Args:
catalog_id: Catalog identifier to reference. Defaults to the
standard catalog version derived from ``A2UI_EXTENSION_URI``.
allowed_components: Subset of component names to expose. When
``None``, all standard catalog components are available.
Returns:
A system prompt string to append to the agent's instructions.
"""
components = sorted(
allowed_components
if allowed_components is not None
else STANDARD_CATALOG_COMPONENTS
)
catalog_label = catalog_id or f"standard ({A2UI_EXTENSION_URI.rsplit('/', 1)[-1]})"
resolved_schema = load_schema(
"server_to_client_with_standard_catalog", version="v0.8"
)
schema_json = json.dumps(resolved_schema, indent=2)
return f"""\
<A2UI_INSTRUCTIONS>
You can generate rich, declarative UI by emitting A2UI JSON messages.
CATALOG: {catalog_label}
AVAILABLE COMPONENTS: {", ".join(components)}
MESSAGE TYPES (emit exactly ONE per message):
- beginRendering: Initialize a new surface with a root component and optional styles.
- surfaceUpdate: Send/update components for a surface. Each component has a unique id \
and a "component" wrapper containing exactly one component-type key.
- dataModelUpdate: Update the data model for a surface. Data entries have a key and \
one typed value (valueString, valueNumber, valueBoolean, valueMap).
- deleteSurface: Remove a surface.
DATA BINDING:
- Use {{"literalString": "..."}} for inline string values.
- Use {{"literalNumber": ...}} for inline numeric values.
- Use {{"literalBoolean": ...}} for inline boolean values.
- Use {{"literalArray": ["...", "..."]}} for inline array values.
- Use {{"path": "/data/model/path"}} to bind to data model values.
ACTIONS:
- Interactive components (Button, etc.) have an "action" with a "name" and optional \
"context" array of key/value pairs.
- Values in action context can use data binding (path or literal).
OUTPUT FORMAT:
Emit each A2UI message as a valid JSON object. When generating UI, produce a \
beginRendering message first, then surfaceUpdate messages with components, and \
optionally dataModelUpdate messages to populate data-bound values.
SCHEMA:
{schema_json}
</A2UI_INSTRUCTIONS>"""
def build_a2ui_v09_system_prompt(
catalog_id: str | None = None,
allowed_components: list[str] | None = None,
) -> str:
"""Build a v0.9 system prompt fragment instructing the LLM to produce A2UI output.
Args:
catalog_id: Catalog identifier to reference. Defaults to the
v0.9 basic catalog.
allowed_components: Subset of component names to expose. When
``None``, all basic catalog components are available.
Returns:
A system prompt string to append to the agent's instructions.
"""
components = sorted(
allowed_components if allowed_components is not None else V09_CATALOG_COMPONENTS
)
catalog_label = catalog_id or A2UI_V09_BASIC_CATALOG_ID
functions = sorted(BASIC_CATALOG_FUNCTIONS)
envelope_schema = load_schema("server_to_client", version="v0.9")
schema_json = json.dumps(envelope_schema, indent=2)
return f"""\
<A2UI_INSTRUCTIONS>
You can generate rich, declarative UI by emitting A2UI v0.9 JSON messages.
Every message MUST include "version": "v0.9".
CATALOG: {catalog_label}
AVAILABLE COMPONENTS: {", ".join(components)}
AVAILABLE FUNCTIONS: {", ".join(functions)}
MESSAGE TYPES (emit exactly ONE per message alongside "version": "v0.9"):
- createSurface: Create a new surface. Requires surfaceId and catalogId. \
Optionally includes theme (primaryColor, iconUrl, agentDisplayName) and \
sendDataModel (boolean).
- updateComponents: Send/update components for a surface. Each component is a flat \
object with "id", "component" (type name string), and type-specific properties at the \
top level. One component MUST have id "root".
- updateDataModel: Update the data model. Uses "path" (JSON Pointer) and "value" \
(any JSON type). Omit "value" to delete the key at path.
- deleteSurface: Remove a surface by surfaceId.
COMPONENT FORMAT (flat, NOT nested):
{{"id": "myText", "component": "Text", "text": "Hello world", "variant": "h1"}}
{{"id": "myBtn", "component": "Button", "child": "myText", "action": {{"event": \
{{"name": "click"}}}}}}
DATA BINDING:
- Use plain values for literals: "text": "Hello" or "value": 42
- Use {{"path": "/data/model/path"}} to bind to data model values.
- Use {{"call": "functionName", "args": {{...}}}} for client-side functions.
ACTIONS:
- Server event: {{"event": {{"name": "actionName", "context": {{"key": "value"}}}}}}
- Local function: {{"functionCall": {{"call": "openUrl", "args": {{"url": "..."}}}}}}
OUTPUT FORMAT:
Emit each A2UI message as a valid JSON object. When generating UI, first emit a \
createSurface message with the catalogId, then updateComponents messages with \
components (one must have id "root"), and optionally updateDataModel messages.
ENVELOPE SCHEMA:
{schema_json}
</A2UI_INSTRUCTIONS>"""

View File

@@ -1,74 +0,0 @@
"""Schema loading utilities for vendored A2UI JSON schemas."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
_V08_DIR = Path(__file__).parent / "v0_8"
_V09_DIR = Path(__file__).parent / "v0_9"
_SCHEMA_CACHE: dict[str, dict[str, Any]] = {}
SCHEMA_NAMES: frozenset[str] = frozenset(
{
"server_to_client",
"client_to_server",
"standard_catalog_definition",
"server_to_client_with_standard_catalog",
}
)
V09_SCHEMA_NAMES: frozenset[str] = frozenset(
{
"server_to_client",
"client_to_server",
"common_types",
"basic_catalog",
"client_capabilities",
"server_capabilities",
"client_data_model",
}
)
def load_schema(name: str, *, version: str = "v0.8") -> dict[str, Any]:
"""Load a vendored A2UI JSON schema by name and version.
Args:
name: Schema name without extension, e.g. ``"server_to_client"``.
version: Protocol version, ``"v0.8"`` or ``"v0.9"``.
Returns:
Parsed JSON schema dict.
Raises:
ValueError: If the schema name or version is not recognized.
FileNotFoundError: If the schema file is missing from the package.
"""
if version == "v0.8":
valid_names = SCHEMA_NAMES
schema_dir = _V08_DIR
elif version == "v0.9":
valid_names = V09_SCHEMA_NAMES
schema_dir = _V09_DIR
else:
raise ValueError(f"Unknown version {version!r}. Available: v0.8, v0.9")
if name not in valid_names:
raise ValueError(
f"Unknown schema {name!r} for {version}. Available: {sorted(valid_names)}"
)
cache_key = f"{version}/{name}"
if cache_key in _SCHEMA_CACHE:
return _SCHEMA_CACHE[cache_key]
path = schema_dir / f"{name}.json"
with path.open() as f:
schema: dict[str, Any] = json.load(f)
_SCHEMA_CACHE[cache_key] = schema
return schema

View File

@@ -1,53 +0,0 @@
{
"title": "A2UI (Agent to UI) Client-to-Server Event Schema",
"description": "Describes a JSON payload for a client-to-server event message.",
"type": "object",
"minProperties": 1,
"maxProperties": 1,
"properties": {
"userAction": {
"type": "object",
"description": "Reports a user-initiated action from a component.",
"properties": {
"name": {
"type": "string",
"description": "The name of the action, taken from the component's action.name property."
},
"surfaceId": {
"type": "string",
"description": "The id of the surface where the event originated."
},
"sourceComponentId": {
"type": "string",
"description": "The id of the component that triggered the event."
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "An ISO 8601 timestamp of when the event occurred."
},
"context": {
"type": "object",
"description": "A JSON object containing the key-value pairs from the component's action.context, after resolving all data bindings.",
"additionalProperties": true
}
},
"required": [
"name",
"surfaceId",
"sourceComponentId",
"timestamp",
"context"
]
},
"error": {
"type": "object",
"description": "Reports a client-side error. The content is flexible.",
"additionalProperties": true
}
},
"oneOf": [
{ "required": ["userAction"] },
{ "required": ["error"] }
]
}

View File

@@ -1,148 +0,0 @@
{
"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",
"additionalProperties": false,
"properties": {
"beginRendering": {
"type": "object",
"description": "Signals the client to begin rendering a surface with a root component and specific styles.",
"additionalProperties": false,
"properties": {
"surfaceId": {
"type": "string",
"description": "The unique identifier for the UI surface to be rendered."
},
"catalogId": {
"type": "string",
"description": "The identifier of the component catalog to use for this surface. If omitted, the client MUST default to the standard catalog for this A2UI version (https://a2ui.org/specification/v0_8/standard_catalog_definition.json)."
},
"root": {
"type": "string",
"description": "The ID of the root component to render."
},
"styles": {
"type": "object",
"description": "Styling information for the UI.",
"additionalProperties": true
}
},
"required": ["root", "surfaceId"]
},
"surfaceUpdate": {
"type": "object",
"description": "Updates a surface with a new set of components.",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"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. The value is an object containing the properties for that specific component.",
"additionalProperties": true
}
},
"required": ["id", "component"]
}
}
},
"required": ["surfaceId", "components"]
},
"dataModelUpdate": {
"type": "object",
"description": "Updates the data model for a surface.",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"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'.",
"additionalProperties": false,
"properties": {
"surfaceId": {
"type": "string",
"description": "The unique identifier for the UI surface to be deleted."
}
},
"required": ["surfaceId"]
}
}
}

View File

@@ -1,832 +0,0 @@
{
"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",
"additionalProperties": false,
"properties": {
"beginRendering": {
"type": "object",
"description": "Signals the client to begin rendering a surface with a root component and specific styles.",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"properties": {
"Text": {
"type": "object",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"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",
"additionalProperties": false,
"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').",
"additionalProperties": false,
"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",
"additionalProperties": false,
"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').",
"additionalProperties": false,
"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",
"additionalProperties": false,
"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').",
"additionalProperties": false,
"properties": {
"literalString": {
"type": "string"
},
"path": {
"type": "string"
}
}
}
},
"required": ["url"]
},
"AudioPlayer": {
"type": "object",
"additionalProperties": false,
"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').",
"additionalProperties": false,
"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').",
"additionalProperties": false,
"properties": {
"literalString": {
"type": "string"
},
"path": {
"type": "string"
}
}
}
},
"required": ["url"]
},
"Row": {
"type": "object",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"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",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"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",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"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",
"additionalProperties": false,
"properties": {
"child": {
"type": "string",
"description": "The ID of the component to be rendered inside the card."
}
},
"required": ["child"]
},
"Tabs": {
"type": "object",
"additionalProperties": false,
"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",
"additionalProperties": false,
"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').",
"additionalProperties": false,
"properties": {
"literalString": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"child": {
"type": "string"
}
},
"required": ["title", "child"]
}
}
},
"required": ["tabItems"]
},
"Divider": {
"type": "object",
"additionalProperties": false,
"properties": {
"axis": {
"type": "string",
"description": "The orientation of the divider.",
"enum": ["horizontal", "vertical"]
}
}
},
"Modal": {
"type": "object",
"additionalProperties": false,
"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",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"context": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"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').",
"additionalProperties": false,
"properties": {
"path": {
"type": "string"
},
"literalString": {
"type": "string"
},
"literalNumber": {
"type": "number"
},
"literalBoolean": {
"type": "boolean"
}
}
}
},
"required": ["key", "value"]
}
}
},
"required": ["name"]
}
},
"required": ["child", "action"]
},
"CheckBox": {
"type": "object",
"additionalProperties": false,
"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').",
"additionalProperties": false,
"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').",
"additionalProperties": false,
"properties": {
"literalBoolean": {
"type": "boolean"
},
"path": {
"type": "string"
}
}
}
},
"required": ["label", "value"]
},
"TextField": {
"type": "object",
"additionalProperties": false,
"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').",
"additionalProperties": false,
"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').",
"additionalProperties": false,
"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",
"additionalProperties": false,
"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').",
"additionalProperties": false,
"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",
"additionalProperties": false,
"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').",
"additionalProperties": false,
"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",
"additionalProperties": false,
"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').",
"additionalProperties": false,
"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."
},
"variant": {
"type": "string",
"enum": ["checkbox", "chips"],
"description": "The visual variant for the selection UI."
},
"filterable": {
"type": "boolean",
"description": "Whether options can be filtered by typing."
}
},
"required": ["selections", "options"]
},
"Slider": {
"type": "object",
"additionalProperties": false,
"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').",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"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'.",
"additionalProperties": false,
"properties": {
"surfaceId": {
"type": "string",
"description": "The unique identifier for the UI surface to be deleted."
}
},
"required": ["surfaceId"]
}
}
}

View File

@@ -1,459 +0,0 @@
{
"components": {
"Text": {
"type": "object",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"properties": {
"literalString": { "type": "string" },
"path": { "type": "string" }
}
},
"usageHint": {
"type": "string",
"description": "A hint for the base text style.",
"enum": ["h1", "h2", "h3", "h4", "h5", "caption", "body"]
}
},
"required": ["text"]
},
"Image": {
"type": "object",
"additionalProperties": false,
"properties": {
"url": {
"type": "object",
"description": "The URL of the image to display.",
"additionalProperties": false,
"properties": {
"literalString": { "type": "string" },
"path": { "type": "string" }
}
},
"fit": {
"type": "string",
"description": "Specifies how the image should be resized to fit its container.",
"enum": ["contain", "cover", "fill", "none", "scale-down"]
},
"usageHint": {
"type": "string",
"description": "A hint for the image size and style.",
"enum": ["icon", "avatar", "smallFeature", "mediumFeature", "largeFeature", "header"]
}
},
"required": ["url"]
},
"Icon": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "object",
"description": "The name of the icon to display.",
"additionalProperties": false,
"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",
"additionalProperties": false,
"properties": {
"url": {
"type": "object",
"description": "The URL of the video to display.",
"additionalProperties": false,
"properties": {
"literalString": { "type": "string" },
"path": { "type": "string" }
}
}
},
"required": ["url"]
},
"AudioPlayer": {
"type": "object",
"additionalProperties": false,
"properties": {
"url": {
"type": "object",
"description": "The URL of the audio to be played.",
"additionalProperties": false,
"properties": {
"literalString": { "type": "string" },
"path": { "type": "string" }
}
},
"description": {
"type": "object",
"description": "A description of the audio, such as a title or summary.",
"additionalProperties": false,
"properties": {
"literalString": { "type": "string" },
"path": { "type": "string" }
}
}
},
"required": ["url"]
},
"Row": {
"type": "object",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"properties": {
"explicitList": { "type": "array", "items": { "type": "string" } },
"template": {
"type": "object",
"additionalProperties": false,
"properties": {
"componentId": { "type": "string" },
"dataBinding": { "type": "string" }
},
"required": ["componentId", "dataBinding"]
}
}
},
"distribution": {
"type": "string",
"enum": ["center", "end", "spaceAround", "spaceBetween", "spaceEvenly", "start"]
},
"alignment": {
"type": "string",
"enum": ["start", "center", "end", "stretch"]
}
},
"required": ["children"]
},
"Column": {
"type": "object",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"properties": {
"explicitList": { "type": "array", "items": { "type": "string" } },
"template": {
"type": "object",
"additionalProperties": false,
"properties": {
"componentId": { "type": "string" },
"dataBinding": { "type": "string" }
},
"required": ["componentId", "dataBinding"]
}
}
},
"distribution": {
"type": "string",
"enum": ["start", "center", "end", "spaceBetween", "spaceAround", "spaceEvenly"]
},
"alignment": {
"type": "string",
"enum": ["center", "end", "start", "stretch"]
}
},
"required": ["children"]
},
"List": {
"type": "object",
"additionalProperties": false,
"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.",
"additionalProperties": false,
"properties": {
"explicitList": { "type": "array", "items": { "type": "string" } },
"template": {
"type": "object",
"additionalProperties": false,
"properties": {
"componentId": { "type": "string" },
"dataBinding": { "type": "string" }
},
"required": ["componentId", "dataBinding"]
}
}
},
"direction": {
"type": "string",
"enum": ["vertical", "horizontal"]
},
"alignment": {
"type": "string",
"enum": ["start", "center", "end", "stretch"]
}
},
"required": ["children"]
},
"Card": {
"type": "object",
"additionalProperties": false,
"properties": {
"child": {
"type": "string",
"description": "The ID of the component to be rendered inside the card."
}
},
"required": ["child"]
},
"Tabs": {
"type": "object",
"additionalProperties": false,
"properties": {
"tabItems": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"title": {
"type": "object",
"additionalProperties": false,
"properties": {
"literalString": { "type": "string" },
"path": { "type": "string" }
}
},
"child": { "type": "string" }
},
"required": ["title", "child"]
}
}
},
"required": ["tabItems"]
},
"Divider": {
"type": "object",
"additionalProperties": false,
"properties": {
"axis": {
"type": "string",
"enum": ["horizontal", "vertical"]
}
}
},
"Modal": {
"type": "object",
"additionalProperties": false,
"properties": {
"entryPointChild": {
"type": "string",
"description": "The ID of the component that opens the modal when interacted with."
},
"contentChild": {
"type": "string",
"description": "The ID of the component to be displayed inside the modal."
}
},
"required": ["entryPointChild", "contentChild"]
},
"Button": {
"type": "object",
"additionalProperties": false,
"properties": {
"child": {
"type": "string",
"description": "The ID of the component to display in the button."
},
"primary": {
"type": "boolean",
"description": "Indicates if this button should be styled as the primary action."
},
"action": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": { "type": "string" },
"context": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"key": { "type": "string" },
"value": {
"type": "object",
"additionalProperties": false,
"properties": {
"path": { "type": "string" },
"literalString": { "type": "string" },
"literalNumber": { "type": "number" },
"literalBoolean": { "type": "boolean" }
}
}
},
"required": ["key", "value"]
}
}
},
"required": ["name"]
}
},
"required": ["child", "action"]
},
"CheckBox": {
"type": "object",
"additionalProperties": false,
"properties": {
"label": {
"type": "object",
"additionalProperties": false,
"properties": {
"literalString": { "type": "string" },
"path": { "type": "string" }
}
},
"value": {
"type": "object",
"additionalProperties": false,
"properties": {
"literalBoolean": { "type": "boolean" },
"path": { "type": "string" }
}
}
},
"required": ["label", "value"]
},
"TextField": {
"type": "object",
"additionalProperties": false,
"properties": {
"label": {
"type": "object",
"additionalProperties": false,
"properties": {
"literalString": { "type": "string" },
"path": { "type": "string" }
}
},
"text": {
"type": "object",
"additionalProperties": false,
"properties": {
"literalString": { "type": "string" },
"path": { "type": "string" }
}
},
"textFieldType": {
"type": "string",
"enum": ["date", "longText", "number", "shortText", "obscured"]
},
"validationRegexp": { "type": "string" }
},
"required": ["label"]
},
"DateTimeInput": {
"type": "object",
"additionalProperties": false,
"properties": {
"value": {
"type": "object",
"additionalProperties": false,
"properties": {
"literalString": { "type": "string" },
"path": { "type": "string" }
}
},
"enableDate": { "type": "boolean" },
"enableTime": { "type": "boolean" }
},
"required": ["value"]
},
"MultipleChoice": {
"type": "object",
"additionalProperties": false,
"properties": {
"selections": {
"type": "object",
"additionalProperties": false,
"properties": {
"literalArray": { "type": "array", "items": { "type": "string" } },
"path": { "type": "string" }
}
},
"options": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"label": {
"type": "object",
"additionalProperties": false,
"properties": {
"literalString": { "type": "string" },
"path": { "type": "string" }
}
},
"value": { "type": "string" }
},
"required": ["label", "value"]
}
},
"maxAllowedSelections": { "type": "integer" },
"variant": {
"type": "string",
"enum": ["checkbox", "chips"]
},
"filterable": { "type": "boolean" }
},
"required": ["selections", "options"]
},
"Slider": {
"type": "object",
"additionalProperties": false,
"properties": {
"value": {
"type": "object",
"additionalProperties": false,
"properties": {
"literalNumber": { "type": "number" },
"path": { "type": "string" }
}
},
"minValue": { "type": "number" },
"maxValue": { "type": "number" }
},
"required": ["value"]
}
},
"styles": {
"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}$"
}
}
}

View File

@@ -1,97 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://a2ui.org/specification/v0_9/client_capabilities.json",
"title": "A2UI Client Capabilities Schema",
"description": "A schema for the a2uiClientCapabilities object, which is sent from the client to the server as part of the A2A metadata to describe the client's UI rendering capabilities.",
"type": "object",
"properties": {
"v0.9": {
"type": "object",
"description": "The capabilities structure for version 0.9 of the A2UI protocol.",
"properties": {
"supportedCatalogIds": {
"type": "array",
"description": "The URI of each of the component and function catalogs that is supported by the client.",
"items": { "type": "string" }
},
"inlineCatalogs": {
"type": "array",
"description": "An array of inline catalog definitions, which can contain both components and functions. This should only be provided if the agent declares 'acceptsInlineCatalogs: true' in its capabilities.",
"items": { "$ref": "#/$defs/Catalog" }
}
},
"required": ["supportedCatalogIds"]
}
},
"required": ["v0.9"],
"$defs": {
"FunctionDefinition": {
"type": "object",
"description": "Describes a function's interface.",
"properties": {
"name": {
"type": "string",
"description": "The unique name of the function."
},
"description": {
"type": "string",
"description": "A human-readable description of what the function does and how to use it."
},
"parameters": {
"type": "object",
"description": "A JSON Schema describing the expected arguments (args) for this function.",
"$ref": "https://json-schema.org/draft/2020-12/schema"
},
"returnType": {
"type": "string",
"enum": [
"string",
"number",
"boolean",
"array",
"object",
"any",
"void"
],
"description": "The type of value this function returns."
}
},
"required": ["name", "parameters", "returnType"],
"additionalProperties": false
},
"Catalog": {
"type": "object",
"description": "A collection of component and function definitions.",
"properties": {
"catalogId": {
"type": "string",
"description": "Unique identifier for this catalog."
},
"components": {
"type": "object",
"description": "Definitions for UI components supported by this catalog.",
"additionalProperties": {
"$ref": "https://json-schema.org/draft/2020-12/schema"
}
},
"functions": {
"type": "array",
"description": "Definitions for functions supported by this catalog.",
"items": {
"$ref": "#/$defs/FunctionDefinition"
}
},
"theme": {
"title": "A2UI Theme",
"description": "A schema that defines a catalog of A2UI theme properties. Each key is a theme property name (e.g. 'primaryColor'), and each value is the JSON schema for that property.",
"type": "object",
"additionalProperties": {
"$ref": "https://json-schema.org/draft/2020-12/schema"
}
}
},
"required": ["catalogId"],
"additionalProperties": false
}
}
}

View File

@@ -1,22 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://a2ui.org/specification/v0_9/client_data_model.json",
"title": "A2UI Client Data Model Schema",
"description": "Schema for attaching the client data model to A2A message metadata. This object should be placed in the `a2uiClientDataModel` field of the metadata.",
"type": "object",
"properties": {
"version": {
"const": "v0.9"
},
"surfaces": {
"type": "object",
"description": "A map of surface IDs to their current data models.",
"additionalProperties": {
"type": "object",
"description": "The current data model for the surface, as a standard JSON object."
}
}
},
"required": ["version", "surfaces"],
"additionalProperties": false
}

View File

@@ -1,104 +0,0 @@
{
"title": "A2UI (Agent to UI) Client-to-Server Event Schema",
"description": "Describes a JSON payload for a client-to-server event message.",
"type": "object",
"minProperties": 2,
"maxProperties": 2,
"properties": {
"version": {
"const": "v0.9"
},
"action": {
"type": "object",
"description": "Reports a user-initiated action from a component.",
"properties": {
"name": {
"type": "string",
"description": "The name of the action, taken from the component's action.event.name property."
},
"surfaceId": {
"type": "string",
"description": "The id of the surface where the event originated."
},
"sourceComponentId": {
"type": "string",
"description": "The id of the component that triggered the event."
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "An ISO 8601 timestamp of when the event occurred."
},
"context": {
"type": "object",
"description": "A JSON object containing the key-value pairs from the component's action.event.context, after resolving all data bindings.",
"additionalProperties": true
}
},
"required": [
"name",
"surfaceId",
"sourceComponentId",
"timestamp",
"context"
]
},
"error": {
"description": "Reports a client-side error.",
"oneOf": [
{
"type": "object",
"title": "Validation Failed Error",
"properties": {
"code": {
"const": "VALIDATION_FAILED"
},
"surfaceId": {
"type": "string",
"description": "The id of the surface where the error occurred."
},
"path": {
"type": "string",
"description": "The JSON pointer to the field that failed validation (e.g. '/components/0/text')."
},
"message": {
"type": "string",
"description": "A short one or two sentence description of why validation failed."
}
},
"required": ["code", "path", "message", "surfaceId"],
"additionalProperties": false
},
{
"type": "object",
"title": "Generic Error",
"properties": {
"code": {
"not": {
"const": "VALIDATION_FAILED"
}
},
"message": {
"type": "string",
"description": "A short one or two sentence description of why the error occurred."
},
"surfaceId": {
"type": "string",
"description": "The id of the surface where the error occurred."
}
},
"required": ["code", "surfaceId", "message"],
"additionalProperties": true
}
]
}
},
"oneOf": [
{
"required": ["action", "version"]
},
{
"required": ["error", "version"]
}
]
}

View File

@@ -1,315 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://a2ui.org/specification/v0_9/common_types.json",
"title": "A2UI Common Types",
"description": "Common type definitions used across A2UI schemas.",
"$defs": {
"ComponentId": {
"type": "string",
"description": "The unique identifier for a component, used for both definitions and references within the same surface."
},
"AccessibilityAttributes": {
"type": "object",
"description": "Attributes to enhance accessibility when using assistive technologies like screen readers.",
"properties": {
"label": {
"$ref": "#/$defs/DynamicString",
"description": "A short string, typically 1 to 3 words, used by assistive technologies to convey the purpose or intent of an element. For example, an input field might have an accessible label of 'User ID' or a button might be labeled 'Submit'."
},
"description": {
"$ref": "#/$defs/DynamicString",
"description": "Additional information provided by assistive technologies about an element such as instructions, format requirements, or result of an action. For example, a mute button might have a label of 'Mute' and a description of 'Silences notifications about this conversation'."
}
}
},
"ComponentCommon": {
"type": "object",
"properties": {
"id": {
"$ref": "#/$defs/ComponentId"
},
"accessibility": {
"$ref": "#/$defs/AccessibilityAttributes"
}
},
"required": ["id"]
},
"ChildList": {
"oneOf": [
{
"type": "array",
"items": {
"$ref": "#/$defs/ComponentId"
},
"description": "A static list of child component IDs."
},
{
"type": "object",
"description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.",
"properties": {
"componentId": {
"$ref": "#/$defs/ComponentId"
},
"path": {
"type": "string",
"description": "The path to the list of component property objects in the data model."
}
},
"required": ["componentId", "path"],
"additionalProperties": false
}
]
},
"DataBinding": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "A JSON Pointer path to a value in the data model."
}
},
"required": ["path"],
"additionalProperties": false
},
"DynamicValue": {
"description": "A value that can be a literal, a path, or a function call returning any type.",
"oneOf": [
{
"type": "string"
},
{
"type": "number"
},
{
"type": "boolean"
},
{
"type": "array"
},
{
"$ref": "#/$defs/DataBinding"
},
{
"$ref": "#/$defs/FunctionCall"
}
]
},
"DynamicString": {
"description": "Represents a string",
"oneOf": [
{
"type": "string"
},
{
"$ref": "#/$defs/DataBinding"
},
{
"allOf": [
{
"$ref": "#/$defs/FunctionCall"
},
{
"properties": {
"returnType": {
"const": "string"
}
}
}
]
}
]
},
"DynamicNumber": {
"description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.",
"oneOf": [
{
"type": "number"
},
{
"$ref": "#/$defs/DataBinding"
},
{
"allOf": [
{
"$ref": "#/$defs/FunctionCall"
},
{
"properties": {
"returnType": {
"const": "number"
}
}
}
]
}
]
},
"DynamicBoolean": {
"description": "A boolean value that can be a literal, a path, or a function call returning a boolean.",
"oneOf": [
{
"type": "boolean"
},
{
"$ref": "#/$defs/DataBinding"
},
{
"allOf": [
{
"$ref": "#/$defs/FunctionCall"
},
{
"properties": {
"returnType": {
"const": "boolean"
}
}
}
]
}
]
},
"DynamicStringList": {
"description": "Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array.",
"oneOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"$ref": "#/$defs/DataBinding"
},
{
"allOf": [
{
"$ref": "#/$defs/FunctionCall"
},
{
"properties": {
"returnType": {
"const": "array"
}
}
}
]
}
]
},
"FunctionCall": {
"type": "object",
"description": "Invokes a named function on the client.",
"properties": {
"call": {
"type": "string",
"description": "The name of the function to call."
},
"args": {
"type": "object",
"description": "Arguments passed to the function.",
"additionalProperties": {
"anyOf": [
{
"$ref": "#/$defs/DynamicValue"
},
{
"type": "object",
"description": "A literal object argument (e.g. configuration)."
}
]
}
},
"returnType": {
"type": "string",
"description": "The expected return type of the function call.",
"enum": [
"string",
"number",
"boolean",
"array",
"object",
"any",
"void"
],
"default": "boolean"
}
},
"required": ["call"],
"oneOf": [
{ "$ref": "basic_catalog.json#/$defs/anyFunction" }
]
},
"CheckRule": {
"type": "object",
"description": "A single validation rule applied to an input component.",
"properties": {
"condition": {
"$ref": "#/$defs/DynamicBoolean"
},
"message": {
"type": "string",
"description": "The error message to display if the check fails."
}
},
"required": ["condition", "message"],
"additionalProperties": false
},
"Checkable": {
"description": "Properties for components that support client-side checks.",
"type": "object",
"properties": {
"checks": {
"type": "array",
"description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.",
"items": {
"$ref": "#/$defs/CheckRule"
}
}
}
},
"Action": {
"description": "Defines an interaction handler that can either trigger a server-side event or execute a local client-side function.",
"oneOf": [
{
"type": "object",
"description": "Triggers a server-side event.",
"properties": {
"event": {
"type": "object",
"description": "The event to dispatch to the server.",
"properties": {
"name": {
"type": "string",
"description": "The name of the action to be dispatched to the server."
},
"context": {
"type": "object",
"description": "A JSON object containing the key-value pairs for the action context. Values can be literals or paths. Use literal values unless the value must be dynamically bound to the data model. Do NOT use paths for static IDs.",
"additionalProperties": {
"$ref": "#/$defs/DynamicValue"
}
}
},
"required": ["name"],
"additionalProperties": false
}
},
"required": ["event"],
"additionalProperties": false
},
{
"type": "object",
"description": "Executes a local client-side function.",
"properties": {
"functionCall": {
"$ref": "#/$defs/FunctionCall"
}
},
"required": ["functionCall"],
"additionalProperties": false
}
]
}
}
}

View File

@@ -1,26 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://a2ui.org/specification/v0_9/server_capabilities.json",
"title": "A2UI Server Capabilities Schema",
"description": "A schema for the server capabilities object, which is used by an A2UI server (or Agent) to advertise its supported UI features to clients. This can be embedded in an Agent Card for A2A or used in other transport protocols like MCP.",
"type": "object",
"properties": {
"v0.9": {
"type": "object",
"description": "The server capabilities structure for version 0.9 of the A2UI protocol.",
"properties": {
"supportedCatalogIds": {
"type": "array",
"description": "An array of strings, where each string is an ID identifying a Catalog Definition Schema that the server can generate. This is not necessarily a resolvable URI.",
"items": { "type": "string" }
},
"acceptsInlineCatalogs": {
"type": "boolean",
"description": "A boolean indicating if the server can accept an 'inlineCatalogs' array in the client's a2uiClientCapabilities. If omitted, this defaults to false.",
"default": false
}
}
}
},
"required": ["v0.9"]
}

View File

@@ -1,132 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://a2ui.org/specification/v0_9/server_to_client.json",
"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.",
"type": "object",
"oneOf": [
{ "$ref": "#/$defs/CreateSurfaceMessage" },
{ "$ref": "#/$defs/UpdateComponentsMessage" },
{ "$ref": "#/$defs/UpdateDataModelMessage" },
{ "$ref": "#/$defs/DeleteSurfaceMessage" }
],
"$defs": {
"CreateSurfaceMessage": {
"type": "object",
"properties": {
"version": {
"const": "v0.9"
},
"createSurface": {
"type": "object",
"description": "Signals the client to create a new surface and begin rendering it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.",
"properties": {
"surfaceId": {
"type": "string",
"description": "The unique identifier for the UI surface to be rendered."
},
"catalogId": {
"description": "A string that uniquely identifies this catalog. It is recommended to prefix this with an internet domain that you own, to avoid conflicts e.g. mycompany.com:somecatalog'.",
"type": "string"
},
"theme": {
"$ref": "basic_catalog.json#/$defs/theme",
"description": "Theme parameters for the surface (e.g., {'primaryColor': '#FF0000'}). These must validate against the 'theme' schema defined in the catalog."
},
"sendDataModel": {
"type": "boolean",
"description": "If true, the client will send the full data model of this surface in the metadata of every A2A message sent to the server that created the surface. Defaults to false."
}
},
"required": ["surfaceId", "catalogId"],
"additionalProperties": false
}
},
"required": ["createSurface", "version"],
"additionalProperties": false
},
"UpdateComponentsMessage": {
"type": "object",
"properties": {
"version": {
"const": "v0.9"
},
"updateComponents": {
"type": "object",
"description": "Updates a surface with a new set of components. This message can be sent multiple times to update the component tree of an existing surface. One of the components in one of the components lists MUST have an 'id' of 'root' to serve as the root of the component tree. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.",
"properties": {
"surfaceId": {
"type": "string",
"description": "The unique identifier for the UI surface to be updated."
},
"components": {
"type": "array",
"description": "A list containing all UI components for the surface.",
"minItems": 1,
"items": {
"$ref": "basic_catalog.json#/$defs/anyComponent"
}
}
},
"required": ["surfaceId", "components"],
"additionalProperties": false
}
},
"required": ["updateComponents", "version"],
"additionalProperties": false
},
"UpdateDataModelMessage": {
"type": "object",
"properties": {
"version": {
"const": "v0.9"
},
"updateDataModel": {
"type": "object",
"description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.",
"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 '/', refers to the entire data model."
},
"value": {
"description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.",
"additionalProperties": true
}
},
"required": ["surfaceId"],
"additionalProperties": false
}
},
"required": ["updateDataModel", "version"],
"additionalProperties": false
},
"DeleteSurfaceMessage": {
"type": "object",
"properties": {
"version": {
"const": "v0.9"
},
"deleteSurface": {
"type": "object",
"description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.",
"properties": {
"surfaceId": {
"type": "string",
"description": "The unique identifier for the UI surface to be deleted."
}
},
"required": ["surfaceId"],
"additionalProperties": false
}
},
"required": ["deleteSurface", "version"],
"additionalProperties": false
}
}
}

View File

@@ -1,160 +0,0 @@
"""A2UI server extension for the A2A protocol."""
from __future__ import annotations
import logging
from typing import Any
from crewai.a2a.extensions.a2ui.models import A2UIResponse, extract_a2ui_json_objects
from crewai.a2a.extensions.a2ui.v0_9 import (
extract_a2ui_v09_json_objects,
)
from crewai.a2a.extensions.a2ui.validator import (
A2UIValidationError,
validate_a2ui_message,
validate_a2ui_message_v09,
)
from crewai.a2a.extensions.server import ExtensionContext, ServerExtension
logger = logging.getLogger(__name__)
A2UI_MIME_TYPE = "application/json+a2ui"
A2UI_EXTENSION_URI = "https://a2ui.org/a2a-extension/a2ui/v0.8"
A2UI_STANDARD_CATALOG_ID = (
"https://a2ui.org/specification/v0_8/standard_catalog_definition.json"
)
A2UI_V09_EXTENSION_URI = "https://a2ui.org/a2a-extension/a2ui/v0.9"
A2UI_V09_BASIC_CATALOG_ID = "https://a2ui.org/specification/v0_9/basic_catalog.json"
class A2UIServerExtension(ServerExtension):
"""A2A server extension that enables A2UI declarative UI generation.
Supports both v0.8 and v0.9 of the A2UI protocol via the ``version``
parameter. When activated by a client, this extension:
* Negotiates catalog preferences during ``on_request``.
* Wraps A2UI messages in the agent response as A2A DataParts with
``application/json+a2ui`` MIME type during ``on_response``.
Example::
A2AServerConfig
server_extensions=[A2UIServerExtension],
default_output_modes=["text/plain", "application/json+a2ui"],
"""
uri: str = A2UI_EXTENSION_URI
required: bool = False
description: str = "A2UI declarative UI generation"
def __init__(
self,
catalog_ids: list[str] | None = None,
accept_inline_catalogs: bool = False,
version: str = "v0.8",
) -> None:
"""Initialize the A2UI server extension.
Args:
catalog_ids: Catalog identifiers this server supports.
accept_inline_catalogs: Whether inline catalog definitions are accepted.
version: Protocol version, ``"v0.8"`` or ``"v0.9"``.
"""
self._catalog_ids = catalog_ids or []
self._accept_inline_catalogs = accept_inline_catalogs
self._version = version
if version == "v0.9":
self.uri = A2UI_V09_EXTENSION_URI
@property
def params(self) -> dict[str, Any]:
"""Extension parameters advertised in the AgentCard."""
result: dict[str, Any] = {}
if self._catalog_ids:
result["supportedCatalogIds"] = self._catalog_ids
result["acceptsInlineCatalogs"] = self._accept_inline_catalogs
return result
async def on_request(self, context: ExtensionContext) -> None:
"""Extract A2UI catalog preferences from the client request.
Stores the negotiated catalog in ``context.state`` under
``"a2ui_catalog_id"`` for downstream use.
"""
if not self.is_active(context):
return
catalog_id = context.get_extension_metadata(self.uri, "catalogId")
if isinstance(catalog_id, str):
context.state["a2ui_catalog_id"] = catalog_id
elif self._catalog_ids:
context.state["a2ui_catalog_id"] = self._catalog_ids[0]
context.state["a2ui_active"] = True
async def on_response(self, context: ExtensionContext, result: Any) -> Any:
"""Wrap A2UI messages in the result as A2A DataParts.
Scans the result for A2UI JSON payloads and converts them into
DataParts with ``application/json+a2ui`` MIME type and A2UI metadata.
Dispatches to the correct extractor and validator based on version.
"""
if not context.state.get("a2ui_active"):
return result
if not isinstance(result, str):
return result
if self._version == "v0.9":
a2ui_messages = extract_a2ui_v09_json_objects(result)
else:
a2ui_messages = extract_a2ui_json_objects(result)
if not a2ui_messages:
return result
build_fn = _build_data_part_v09 if self._version == "v0.9" else _build_data_part
data_parts = [
part
for part in (build_fn(msg_data) for msg_data in a2ui_messages)
if part is not None
]
if not data_parts:
return result
return A2UIResponse(text=result, a2ui_parts=data_parts)
def _build_data_part(msg_data: dict[str, Any]) -> dict[str, Any] | None:
"""Validate a v0.8 A2UI message and wrap it as a DataPart dict."""
try:
validated = validate_a2ui_message(msg_data)
except A2UIValidationError:
logger.warning("Skipping invalid A2UI message in response", exc_info=True)
return None
return {
"kind": "data",
"data": validated.model_dump(by_alias=True, exclude_none=True),
"metadata": {
"mimeType": A2UI_MIME_TYPE,
},
}
def _build_data_part_v09(msg_data: dict[str, Any]) -> dict[str, Any] | None:
"""Validate a v0.9 A2UI message and wrap it as a DataPart dict."""
try:
validated = validate_a2ui_message_v09(msg_data)
except A2UIValidationError:
logger.warning("Skipping invalid A2UI v0.9 message in response", exc_info=True)
return None
return {
"kind": "data",
"data": validated.model_dump(by_alias=True, exclude_none=True),
"metadata": {
"mimeType": A2UI_MIME_TYPE,
},
}

View File

@@ -1,831 +0,0 @@
"""Pydantic models for A2UI v0.9 protocol messages and types.
This module provides v0.9 counterparts to the v0.8 models in ``models.py``.
Key differences from v0.8:
* ``beginRendering`` → ``createSurface`` — adds ``theme``, ``sendDataModel``,
requires ``catalogId``.
* ``surfaceUpdate`` → ``updateComponents`` — component structure is flat:
``component`` is a type-name string, properties live at the top level.
* ``dataModelUpdate`` → ``updateDataModel`` — ``contents`` adjacency list
replaced by a single ``value`` of any JSON type; ``path`` uses JSON Pointers.
* All messages carry a ``version: "v0.9"`` discriminator.
* Data binding uses plain JSON values, ``DataBinding`` objects, or
``FunctionCall`` objects instead of ``literalString`` / ``path`` wrappers.
* ``MultipleChoice`` is replaced by ``ChoicePicker``.
* ``Styles`` is replaced by ``Theme`` — adds ``iconUrl``, ``agentDisplayName``.
* Client-to-server ``userAction`` is renamed to ``action``; ``error`` gains
structured ``code`` / ``path`` fields.
"""
from __future__ import annotations
import json
from typing import Any, Literal, get_args
from pydantic import BaseModel, ConfigDict, Field, model_validator
ComponentName = Literal[
"Text",
"Image",
"Icon",
"Video",
"AudioPlayer",
"Row",
"Column",
"List",
"Card",
"Tabs",
"Modal",
"Divider",
"Button",
"TextField",
"CheckBox",
"ChoicePicker",
"Slider",
"DateTimeInput",
]
BASIC_CATALOG_COMPONENTS: frozenset[ComponentName] = frozenset(get_args(ComponentName))
FunctionName = Literal[
"required",
"regex",
"length",
"numeric",
"email",
"formatString",
"formatNumber",
"formatCurrency",
"formatDate",
"pluralize",
"openUrl",
"and",
"or",
"not",
]
BASIC_CATALOG_FUNCTIONS: frozenset[FunctionName] = frozenset(get_args(FunctionName))
IconNameV09 = Literal[
"accountCircle",
"add",
"arrowBack",
"arrowForward",
"attachFile",
"calendarToday",
"call",
"camera",
"check",
"close",
"delete",
"download",
"edit",
"event",
"error",
"fastForward",
"favorite",
"favoriteOff",
"folder",
"help",
"home",
"info",
"locationOn",
"lock",
"lockOpen",
"mail",
"menu",
"moreVert",
"moreHoriz",
"notificationsOff",
"notifications",
"pause",
"payment",
"person",
"phone",
"photo",
"play",
"print",
"refresh",
"rewind",
"search",
"send",
"settings",
"share",
"shoppingCart",
"skipNext",
"skipPrevious",
"star",
"starHalf",
"starOff",
"stop",
"upload",
"visibility",
"visibilityOff",
"volumeDown",
"volumeMute",
"volumeOff",
"volumeUp",
"warning",
]
V09_ICON_NAMES: frozenset[IconNameV09] = frozenset(get_args(IconNameV09))
class DataBinding(BaseModel):
"""JSON Pointer path reference to the data model."""
path: str = Field(description="A JSON Pointer path to a value in the data model.")
model_config = ConfigDict(extra="forbid")
class FunctionCall(BaseModel):
"""Client-side function invocation."""
call: str = Field(description="The name of the function to call.")
args: dict[str, DynamicValue] | None = Field(
default=None, description="Arguments passed to the function."
)
return_type: (
Literal["string", "number", "boolean", "array", "object", "any", "void"] | None
) = Field(
default=None,
alias="returnType",
description="Expected return type of the function call.",
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
DynamicValue = str | float | int | bool | list[Any] | DataBinding | FunctionCall
DynamicString = str | DataBinding | FunctionCall
DynamicNumber = float | int | DataBinding | FunctionCall
DynamicBoolean = bool | DataBinding | FunctionCall
DynamicStringList = list[str] | DataBinding | FunctionCall
class CheckRule(BaseModel):
"""A single validation rule for an input component."""
condition: DynamicBoolean = Field(
description="Condition that must evaluate to true for the check to pass."
)
message: str = Field(description="Error message displayed if the check fails.")
model_config = ConfigDict(extra="forbid")
class AccessibilityAttributes(BaseModel):
"""Accessibility attributes for assistive technologies."""
label: DynamicString | None = Field(
default=None, description="Short label for screen readers."
)
description: DynamicString | None = Field(
default=None, description="Extended description for screen readers."
)
model_config = ConfigDict(extra="forbid")
class ChildTemplate(BaseModel):
"""Template for generating dynamic children from a data model list."""
component_id: str = Field(
alias="componentId", description="Component to repeat per list item."
)
path: str = Field(description="Data model path to the list of items.")
model_config = ConfigDict(populate_by_name=True, extra="forbid")
ChildListV09 = list[str] | ChildTemplate
class EventAction(BaseModel):
"""Server-side event triggered by a component interaction."""
name: str = Field(description="Action name dispatched to the server.")
context: dict[str, DynamicValue] | None = Field(
default=None, description="Key-value pairs sent with the event."
)
model_config = ConfigDict(extra="forbid")
class ActionV09(BaseModel):
"""Interaction handler: server event or local function call.
Exactly one of ``event`` or ``function_call`` must be set.
"""
event: EventAction | None = Field(
default=None, description="Triggers a server-side event."
)
function_call: FunctionCall | None = Field(
default=None,
alias="functionCall",
description="Executes a local client-side function.",
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
@model_validator(mode="after")
def _check_exactly_one(self) -> ActionV09:
"""Enforce exactly one of event or functionCall."""
count = sum(f is not None for f in (self.event, self.function_call))
if count != 1:
raise ValueError(
f"Exactly one of event or functionCall must be set, got {count}"
)
return self
class TextV09(BaseModel):
"""Displays text content."""
id: str = Field(description="Unique component identifier.")
component: Literal["Text"] = "Text"
text: DynamicString = Field(description="Text content to display.")
variant: Literal["h1", "h2", "h3", "h4", "h5", "caption", "body"] | None = Field(
default=None, description="Semantic text style hint."
)
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(extra="forbid")
class ImageV09(BaseModel):
"""Displays an image."""
id: str = Field(description="Unique component identifier.")
component: Literal["Image"] = "Image"
url: DynamicString = Field(description="Image source URL.")
description: DynamicString | None = Field(
default=None, description="Accessibility text."
)
fit: Literal["contain", "cover", "fill", "none", "scaleDown"] | None = Field(
default=None, description="Object-fit behavior."
)
variant: (
Literal[
"icon", "avatar", "smallFeature", "mediumFeature", "largeFeature", "header"
]
| None
) = Field(default=None, description="Image size hint.")
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(extra="forbid")
class IconV09(BaseModel):
"""Displays a named icon."""
id: str = Field(description="Unique component identifier.")
component: Literal["Icon"] = "Icon"
name: IconNameV09 | DataBinding = Field(description="Icon name or data binding.")
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(extra="forbid")
class VideoV09(BaseModel):
"""Displays a video player."""
id: str = Field(description="Unique component identifier.")
component: Literal["Video"] = "Video"
url: DynamicString = Field(description="Video source URL.")
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(extra="forbid")
class AudioPlayerV09(BaseModel):
"""Displays an audio player."""
id: str = Field(description="Unique component identifier.")
component: Literal["AudioPlayer"] = "AudioPlayer"
url: DynamicString = Field(description="Audio source URL.")
description: DynamicString | None = Field(
default=None, description="Audio content description."
)
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(extra="forbid")
class RowV09(BaseModel):
"""Horizontal layout container."""
id: str = Field(description="Unique component identifier.")
component: Literal["Row"] = "Row"
children: ChildListV09 = Field(description="Child components.")
justify: (
Literal[
"center",
"end",
"spaceAround",
"spaceBetween",
"spaceEvenly",
"start",
"stretch",
]
| None
) = Field(default=None, description="Main-axis distribution.")
align: Literal["start", "center", "end", "stretch"] | None = Field(
default=None, description="Cross-axis alignment."
)
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(extra="forbid")
class ColumnV09(BaseModel):
"""Vertical layout container."""
id: str = Field(description="Unique component identifier.")
component: Literal["Column"] = "Column"
children: ChildListV09 = Field(description="Child components.")
justify: (
Literal[
"start",
"center",
"end",
"spaceBetween",
"spaceAround",
"spaceEvenly",
"stretch",
]
| None
) = Field(default=None, description="Main-axis distribution.")
align: Literal["center", "end", "start", "stretch"] | None = Field(
default=None, description="Cross-axis alignment."
)
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(extra="forbid")
class ListV09(BaseModel):
"""Scrollable list container."""
id: str = Field(description="Unique component identifier.")
component: Literal["List"] = "List"
children: ChildListV09 = Field(description="Child components.")
direction: Literal["vertical", "horizontal"] | None = Field(
default=None, description="Scroll direction."
)
align: Literal["start", "center", "end", "stretch"] | None = Field(
default=None, description="Cross-axis alignment."
)
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(extra="forbid")
class CardV09(BaseModel):
"""Card container wrapping a single child."""
id: str = Field(description="Unique component identifier.")
component: Literal["Card"] = "Card"
child: str = Field(description="ID of the child component.")
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(extra="forbid")
class TabItemV09(BaseModel):
"""A single tab definition."""
title: DynamicString = Field(description="Tab title.")
child: str = Field(description="ID of the tab content component.")
model_config = ConfigDict(extra="forbid")
class TabsV09(BaseModel):
"""Tabbed navigation container."""
id: str = Field(description="Unique component identifier.")
component: Literal["Tabs"] = "Tabs"
tabs: list[TabItemV09] = Field(min_length=1, description="Tab definitions.")
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(extra="forbid")
class ModalV09(BaseModel):
"""Modal dialog with a trigger and content."""
id: str = Field(description="Unique component identifier.")
component: Literal["Modal"] = "Modal"
trigger: str = Field(description="ID of the component that opens the modal.")
content: str = Field(description="ID of the component inside the modal.")
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(extra="forbid")
class DividerV09(BaseModel):
"""Visual divider line."""
id: str = Field(description="Unique component identifier.")
component: Literal["Divider"] = "Divider"
axis: Literal["horizontal", "vertical"] | None = Field(
default=None, description="Divider orientation."
)
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(extra="forbid")
class ButtonV09(BaseModel):
"""Interactive button."""
id: str = Field(description="Unique component identifier.")
component: Literal["Button"] = "Button"
child: str = Field(description="ID of the button label component.")
action: ActionV09 = Field(description="Action dispatched on click.")
variant: Literal["default", "primary", "borderless"] | None = Field(
default=None, description="Button style variant."
)
checks: list[CheckRule] | None = Field(
default=None, description="Validation rules."
)
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(extra="forbid")
class TextFieldV09(BaseModel):
"""Text input field."""
id: str = Field(description="Unique component identifier.")
component: Literal["TextField"] = "TextField"
label: DynamicString = Field(description="Input label.")
value: DynamicString | None = Field(default=None, description="Current text value.")
variant: Literal["longText", "number", "shortText", "obscured"] | None = Field(
default=None, description="Input type variant."
)
validation_regexp: str | None = Field(
default=None,
alias="validationRegexp",
description="Regex for client-side validation.",
)
checks: list[CheckRule] | None = Field(
default=None, description="Validation rules."
)
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class CheckBoxV09(BaseModel):
"""Checkbox input."""
id: str = Field(description="Unique component identifier.")
component: Literal["CheckBox"] = "CheckBox"
label: DynamicString = Field(description="Checkbox label.")
value: DynamicBoolean = Field(description="Checked state.")
checks: list[CheckRule] | None = Field(
default=None, description="Validation rules."
)
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(extra="forbid")
class ChoicePickerOption(BaseModel):
"""A single option in a ChoicePicker."""
label: DynamicString = Field(description="Display label.")
value: str = Field(description="Value when selected.")
model_config = ConfigDict(extra="forbid")
class ChoicePickerV09(BaseModel):
"""Selection component replacing v0.8 MultipleChoice."""
id: str = Field(description="Unique component identifier.")
component: Literal["ChoicePicker"] = "ChoicePicker"
options: list[ChoicePickerOption] = Field(description="Available choices.")
value: DynamicStringList = Field(description="Currently selected values.")
label: DynamicString | None = Field(default=None, description="Group label.")
variant: Literal["multipleSelection", "mutuallyExclusive"] | None = Field(
default=None, description="Selection behavior."
)
display_style: Literal["checkbox", "chips"] | None = Field(
default=None, alias="displayStyle", description="Visual display style."
)
filterable: bool | None = Field(
default=None, description="Whether options can be filtered."
)
checks: list[CheckRule] | None = Field(
default=None, description="Validation rules."
)
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class SliderV09(BaseModel):
"""Numeric slider input."""
id: str = Field(description="Unique component identifier.")
component: Literal["Slider"] = "Slider"
value: DynamicNumber = Field(description="Current slider value.")
max: float = Field(description="Maximum slider value.")
min: float | None = Field(default=None, description="Minimum slider value.")
label: DynamicString | None = Field(default=None, description="Slider label.")
checks: list[CheckRule] | None = Field(
default=None, description="Validation rules."
)
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(extra="forbid")
class DateTimeInputV09(BaseModel):
"""Date and/or time picker."""
id: str = Field(description="Unique component identifier.")
component: Literal["DateTimeInput"] = "DateTimeInput"
value: DynamicString = Field(description="ISO 8601 date/time value.")
enable_date: bool | None = Field(
default=None, alias="enableDate", description="Enable date selection."
)
enable_time: bool | None = Field(
default=None, alias="enableTime", description="Enable time selection."
)
min: DynamicString | None = Field(
default=None, description="Minimum allowed date/time."
)
max: DynamicString | None = Field(
default=None, description="Maximum allowed date/time."
)
label: DynamicString | None = Field(default=None, description="Input label.")
checks: list[CheckRule] | None = Field(
default=None, description="Validation rules."
)
weight: float | None = Field(default=None, description="Flex weight.")
accessibility: AccessibilityAttributes | None = None
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class Theme(BaseModel):
"""Surface theme configuration for v0.9.
Replaces v0.8 ``Styles``. Adds ``iconUrl`` and ``agentDisplayName``
for agent attribution; drops ``font``.
"""
primary_color: str | None = Field(
default=None,
alias="primaryColor",
pattern=r"^#[0-9a-fA-F]{6}$",
description="Primary brand color as a hex string.",
)
icon_url: str | None = Field(
default=None,
alias="iconUrl",
description="URL for an image identifying the agent or tool.",
)
agent_display_name: str | None = Field(
default=None,
alias="agentDisplayName",
description="Text label identifying the agent or tool.",
)
model_config = ConfigDict(populate_by_name=True, extra="allow")
class CreateSurface(BaseModel):
"""Signals the client to create a new surface and begin rendering.
Replaces v0.8 ``BeginRendering``. ``catalogId`` is now required and
``theme`` / ``sendDataModel`` are new.
"""
surface_id: str = Field(alias="surfaceId", description="Unique surface identifier.")
catalog_id: str = Field(
alias="catalogId", description="Catalog identifier for this surface."
)
theme: Theme | None = Field(default=None, description="Theme parameters.")
send_data_model: bool | None = Field(
default=None,
alias="sendDataModel",
description="If true, client sends data model in action metadata.",
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class UpdateComponents(BaseModel):
"""Updates a surface with a new set of components.
Replaces v0.8 ``SurfaceUpdate``. Components use a flat structure where
``component`` is a type-name string and properties sit at the top level.
"""
surface_id: str = Field(alias="surfaceId", description="Target surface identifier.")
components: list[dict[str, Any]] = Field(
min_length=1, description="Components to render on the surface."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class UpdateDataModel(BaseModel):
"""Updates the data model for a surface.
Replaces v0.8 ``DataModelUpdate``. The ``contents`` adjacency list is
replaced by a single ``value`` of any JSON type. ``path`` uses JSON
Pointer syntax — e.g. ``/user/name``.
"""
surface_id: str = Field(alias="surfaceId", description="Target surface identifier.")
path: str | None = Field(
default=None, description="JSON Pointer path for the update."
)
value: Any = Field(
default=None, description="Value to set. Omit to delete the key."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class DeleteSurfaceV09(BaseModel):
"""Signals the client to delete a surface."""
surface_id: str = Field(alias="surfaceId", description="Surface to delete.")
model_config = ConfigDict(populate_by_name=True, extra="forbid")
class A2UIMessageV09(BaseModel):
"""Union wrapper for v0.9 server-to-client message types.
Exactly one message field must be set alongside the ``version`` field.
"""
version: Literal["v0.9"] = "v0.9"
create_surface: CreateSurface | None = Field(
default=None, alias="createSurface", description="Create a new surface."
)
update_components: UpdateComponents | None = Field(
default=None,
alias="updateComponents",
description="Update components on a surface.",
)
update_data_model: UpdateDataModel | None = Field(
default=None,
alias="updateDataModel",
description="Update the surface data model.",
)
delete_surface: DeleteSurfaceV09 | None = Field(
default=None, alias="deleteSurface", description="Delete a surface."
)
model_config = ConfigDict(populate_by_name=True, extra="forbid")
@model_validator(mode="after")
def _check_exactly_one(self) -> A2UIMessageV09:
"""Enforce the spec's exactly-one-of constraint."""
fields = [
self.create_surface,
self.update_components,
self.update_data_model,
self.delete_surface,
]
count = sum(f is not None for f in fields)
if count != 1:
raise ValueError(
f"Exactly one A2UI v0.9 message type must be set, got {count}"
)
return self
class ActionEvent(BaseModel):
"""User-initiated action from a component.
Replaces v0.8 ``UserAction``. The event field is renamed from
``userAction`` to ``action``.
"""
name: str = Field(description="Action name.")
surface_id: str = Field(alias="surfaceId", description="Source surface identifier.")
source_component_id: str = Field(
alias="sourceComponentId",
description="Component that triggered the action.",
)
timestamp: str = Field(description="ISO 8601 timestamp of the action.")
context: dict[str, Any] = Field(description="Resolved action context payload.")
model_config = ConfigDict(populate_by_name=True)
class ClientErrorV09(BaseModel):
"""Structured client-side error report.
Replaces v0.8's flexible ``ClientError`` with required ``code``,
``surfaceId``, and ``message`` fields.
"""
code: str = Field(description="Error code (e.g. VALIDATION_FAILED).")
surface_id: str = Field(
alias="surfaceId", description="Surface where the error occurred."
)
message: str = Field(description="Human-readable error description.")
path: str | None = Field(
default=None, description="JSON Pointer to the failing field."
)
model_config = ConfigDict(populate_by_name=True, extra="allow")
class A2UIEventV09(BaseModel):
"""Union wrapper for v0.9 client-to-server events."""
version: Literal["v0.9"] = "v0.9"
action: ActionEvent | None = Field(
default=None, description="User-initiated action event."
)
error: ClientErrorV09 | None = Field(
default=None, description="Client-side error report."
)
model_config = ConfigDict(populate_by_name=True)
@model_validator(mode="after")
def _check_exactly_one(self) -> A2UIEventV09:
"""Enforce the spec's exactly-one-of constraint."""
fields = [self.action, self.error]
count = sum(f is not None for f in fields)
if count != 1:
raise ValueError(
f"Exactly one A2UI v0.9 event type must be set, got {count}"
)
return self
class ClientDataModel(BaseModel):
"""Client data model payload for A2A message metadata.
When ``sendDataModel`` is ``true`` on ``createSurface``, the client
attaches this object to every outbound A2A message as
``a2uiClientDataModel`` in the metadata.
"""
version: Literal["v0.9"] = "v0.9"
surfaces: dict[str, dict[str, Any]] = Field(
description="Map of surface IDs to their current data models."
)
model_config = ConfigDict(extra="forbid")
_V09_KEYS = {"createSurface", "updateComponents", "updateDataModel", "deleteSurface"}
def extract_a2ui_v09_json_objects(text: str) -> list[dict[str, Any]]:
"""Extract JSON objects containing A2UI v0.9 keys from text.
Uses ``json.JSONDecoder.raw_decode`` for robust parsing that correctly
handles braces inside string literals.
"""
decoder = json.JSONDecoder()
results: list[dict[str, Any]] = []
idx = 0
while idx < len(text):
idx = text.find("{", idx)
if idx == -1:
break
try:
obj, end_idx = decoder.raw_decode(text, idx)
if isinstance(obj, dict) and _V09_KEYS & obj.keys():
results.append(obj)
idx = end_idx
except json.JSONDecodeError:
idx += 1
return results

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