chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,636 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import dotenv
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
from pyagentspec.agent import Agent
|
||||
from pyagentspec.llms import OpenAiCompatibleConfig
|
||||
from pyagentspec.serialization import AgentSpecSerializer
|
||||
from pyagentspec.tools import ServerTool, ClientTool
|
||||
from pyagentspec.property import StringProperty
|
||||
from pyagentspec.llms.openaiconfig import OpenAIAPIType
|
||||
from pyagentspec.llms.llmgenerationconfig import LlmGenerationConfig
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
a2ui_prompts_folder = Path(__file__).resolve().parent / "a2ui_prompts"
|
||||
A2UI_JSON_SCHEMA = (a2ui_prompts_folder / "a2ui_schema.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
A2UI_PROMPT = (a2ui_prompts_folder / "a2ui_prompt.txt").read_text(encoding="utf-8")
|
||||
|
||||
A2UI_JSON_SCHEMA_PROMPT = f"""
|
||||
# JSON Schema Reference
|
||||
---BEGIN A2UI JSON SCHEMA---
|
||||
{A2UI_JSON_SCHEMA}
|
||||
---END A2UI JSON SCHEMA---
|
||||
"""
|
||||
|
||||
|
||||
_today = datetime.now()
|
||||
_today_str = _today.strftime("%Y-%m-%d")
|
||||
_today_day = _today.strftime("%A")
|
||||
|
||||
A2UI_SYSTEM_PROMPT = f"""You are a helpful Scheduling Assistant that helps users manage their calendar and emails.
|
||||
|
||||
Today's date: {_today_str} ({_today_day}). When the user asks a question without a specific date, assume they mean today.
|
||||
|
||||
# Rendering Rules
|
||||
|
||||
## Calendar Display
|
||||
When the user asks to see their schedule or calendar:
|
||||
1. Call get_user_schedule to retrieve the schedule data.
|
||||
2. After receiving the result, you MUST call render_calendar. NEVER stop after only writing text. Call render_calendar with:
|
||||
- date: today's date "{_today_str}"
|
||||
- dayName: "{_today_day}"
|
||||
- events: a JSON array STRING where each object has:
|
||||
- "startTime": start time like "08:00"
|
||||
- "endTime": end time like "09:00" (or "" if open-ended)
|
||||
- "title": the event name
|
||||
- "isAvailable": true if the slot is available, false otherwise
|
||||
- "guests": (optional) array of guest objects with "email" and "status" ("accepted", "declined", "maybe", "pending")
|
||||
|
||||
Example events value: '[{{"startTime":"08:00","endTime":"09:00","title":"Morning Meeting","isAvailable":false,"guests":[{{"email":"sarah@co.org","status":"accepted"}}]}},{{"startTime":"09:00","endTime":"10:00","title":"Available","isAvailable":true}}]'
|
||||
|
||||
3. DO NOT use send_a2ui_json_to_client for calendar display. Always use render_calendar.
|
||||
4. When updating the schedule (adding/removing events), call render_calendar again with the FULL updated event list.
|
||||
|
||||
## Inbox Display
|
||||
When the user asks to check their inbox or emails:
|
||||
1. Call check_user_inbox to retrieve emails.
|
||||
2. After receiving the result, you MUST call render_inbox. NEVER stop after only writing text. Call render_inbox with:
|
||||
- emails: a JSON array STRING where each object has:
|
||||
- "from": sender email address
|
||||
- "subject": email subject line
|
||||
- "body": email body text
|
||||
- "date": date/time string
|
||||
- "isRead": boolean (false for unread)
|
||||
|
||||
Example emails value: '[{{"from":"david@company.org","subject":"Quick Sync","body":"Hey, need to set up a meeting...","date":"10:30 AM","isRead":false}}]'
|
||||
|
||||
3. DO NOT use send_a2ui_json_to_client for inbox display. Always use render_inbox.
|
||||
|
||||
## Email Compose
|
||||
When composing, drafting, replying to, or sending an email, use the render_email_compose tool.
|
||||
Pass a JSON object STRING with these fields:
|
||||
- "to": recipient email address
|
||||
- "subject": email subject (use "Re: ..." for replies)
|
||||
- "body": the full email body text (greeting, message, closing, and signature all combined as natural text)
|
||||
|
||||
Example email value: '{{"to":"david@company.org","subject":"Re: Quick Sync","body":"Hi David,\\n\\nThanks for reaching out. I\\'m available today 10:00-11:00 or 13:00-14:00. Let me know what works.\\n\\nBest,\\n[Your Name]"}}'
|
||||
|
||||
DO NOT use send_a2ui_json_to_client for email compose. Always use render_email_compose.
|
||||
|
||||
## Daily Brief Dashboard
|
||||
When the user asks for their daily brief, daily summary, or dashboard:
|
||||
1. First call get_daily_brief to retrieve the combined data.
|
||||
2. Then call render_daily_brief with root="root", plus components and data as JSON strings.
|
||||
|
||||
### A2UI Component Rules
|
||||
Each component is {{"id":"<id>","component":{{<Type>:{{...}}}}}}. ONE key per component object.
|
||||
To give a component flex-grow, add "weight":<n> at the ITEM level (sibling of "id" and "component"), NOT inside type properties.
|
||||
|
||||
Available component types:
|
||||
- Card: {{"child":"<child-id>"}}
|
||||
- Column: {{"children":{{"explicitList":["id1","id2",...]}}, "alignment":"start"|"center"|"end"|"stretch"}}
|
||||
- Row: {{"children":{{"explicitList":["id1","id2",...]}}, "alignment":"start"|"center"|"end", "distribution":"start"|"center"|"end"|"spaceBetween"|"spaceEvenly"}}
|
||||
- Text: {{"text":{{"path":"/<key>"}} or {{"literalString":"value"}}, "usageHint":"h1"|"h2"|"h3"|"h4"|"body"|"caption"}}
|
||||
- Icon: {{"name":{{"literalString":"calendarToday"|"mail"|"event"|"person"|"schedule"|"inbox"}}}}
|
||||
- Divider: {{}}
|
||||
- Modal: {{"entryPointChild":"<entry-id>","contentChild":"<content-id>"}} — click entry to open detail dialog
|
||||
|
||||
### Data Binding
|
||||
- ALL dynamic values MUST use path binding: {{"path":"/<key>"}}
|
||||
- Only static labels use {{"literalString":"..."}}
|
||||
- Data object has flat keys matching the path bindings (without leading /)
|
||||
|
||||
### Layout
|
||||
The dashboard uses 3 separate Card components inside a root Column:
|
||||
1. **Summary card**: title, date, stats row, executive summary paragraph
|
||||
2. **Schedule card**: section header + event rows (each wrapped in a Modal for detail view)
|
||||
3. **Inbox card**: section header + email rows (each wrapped in a Modal for detail view)
|
||||
|
||||
### Typography
|
||||
- h2: dashboard title; h1: big metric numbers; h4: section headings
|
||||
- body: event titles, email subjects, executive summary, guest labels
|
||||
- caption: date, metric labels, timestamps, sender names (use *italic* wrapping)
|
||||
|
||||
### Concrete Template
|
||||
Copy and populate dynamic values from get_daily_brief results.
|
||||
|
||||
components:
|
||||
[
|
||||
{{"id":"root","component":{{"Column":{{"children":{{"explicitList":["summary-card","schedule-card","inbox-card"]}}}}}}}},
|
||||
{{"id":"summary-card","component":{{"Card":{{"child":"summary-col"}}}}}},
|
||||
{{"id":"summary-col","component":{{"Column":{{"children":{{"explicitList":["title","subtitle","div1","metrics-row","div2","exec-summary"]}}}}}}}},
|
||||
{{"id":"title","component":{{"Text":{{"text":{{"path":"/title"}},"usageHint":"h2"}}}}}},
|
||||
{{"id":"subtitle","component":{{"Text":{{"text":{{"path":"/subtitle"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"div1","component":{{"Divider":{{}}}}}},
|
||||
{{"id":"metrics-row","component":{{"Row":{{"children":{{"explicitList":["m-meetings","m-unread","m-hours"]}},"distribution":"spaceEvenly"}}}}}},
|
||||
{{"id":"m-meetings","component":{{"Column":{{"children":{{"explicitList":["m-meetings-v","m-meetings-l"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"m-meetings-v","component":{{"Text":{{"text":{{"path":"/meetingCount"}},"usageHint":"h1"}}}}}},
|
||||
{{"id":"m-meetings-l","component":{{"Text":{{"text":{{"literalString":"*Meetings*"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"m-unread","component":{{"Column":{{"children":{{"explicitList":["m-unread-v","m-unread-l"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"m-unread-v","component":{{"Text":{{"text":{{"path":"/unreadCount"}},"usageHint":"h1"}}}}}},
|
||||
{{"id":"m-unread-l","component":{{"Text":{{"text":{{"literalString":"*Unread*"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"m-hours","component":{{"Column":{{"children":{{"explicitList":["m-hours-v","m-hours-l"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"m-hours-v","component":{{"Text":{{"text":{{"path":"/availableHours"}},"usageHint":"h1"}}}}}},
|
||||
{{"id":"m-hours-l","component":{{"Text":{{"text":{{"literalString":"*Hours Free*"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"div2","component":{{"Divider":{{}}}}}},
|
||||
{{"id":"exec-summary","component":{{"Text":{{"text":{{"path":"/execSummary"}},"usageHint":"body"}}}}}},
|
||||
{{"id":"schedule-card","component":{{"Card":{{"child":"sched-col"}}}}}},
|
||||
{{"id":"sched-col","component":{{"Column":{{"children":{{"explicitList":["sched-hdr","modal-e1","ed1","modal-e2","ed2","modal-e3","ed3","modal-e4","ed4","modal-e5"]}}}}}}}},
|
||||
{{"id":"sched-hdr","component":{{"Row":{{"children":{{"explicitList":["sched-icon","sched-text"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"sched-icon","component":{{"Icon":{{"name":{{"literalString":"event"}}}}}}}},
|
||||
{{"id":"sched-text","component":{{"Text":{{"text":{{"literalString":"Schedule"}},"usageHint":"h4"}}}}}},
|
||||
{{"id":"modal-e1","component":{{"Modal":{{"entryPointChild":"e1-row","contentChild":"e1-detail"}}}}}},
|
||||
{{"id":"e1-row","component":{{"Row":{{"children":{{"explicitList":["e1-time","e1-title","e1-gc"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"e1-time","component":{{"Text":{{"text":{{"path":"/event1Time"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"e1-title","weight":1,"component":{{"Text":{{"text":{{"path":"/event1Title"}},"usageHint":"body"}}}}}},
|
||||
{{"id":"e1-gc","component":{{"Row":{{"children":{{"explicitList":["e1-pi","e1-gn"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"e1-pi","component":{{"Icon":{{"name":{{"literalString":"person"}}}}}}}},
|
||||
{{"id":"e1-gn","component":{{"Text":{{"text":{{"path":"/event1Guests"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"e1-detail","component":{{"Column":{{"children":{{"explicitList":["e1-dt","e1-dtm","e1-dd","e1-dgl","e1-dgn"]}}}}}}}},
|
||||
{{"id":"e1-dt","component":{{"Text":{{"text":{{"path":"/event1Title"}},"usageHint":"h4"}}}}}},
|
||||
{{"id":"e1-dtm","component":{{"Text":{{"text":{{"path":"/event1TimeFull"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"e1-dd","component":{{"Divider":{{}}}}}},
|
||||
{{"id":"e1-dgl","component":{{"Text":{{"text":{{"path":"/event1GuestsLabel"}},"usageHint":"body"}}}}}},
|
||||
{{"id":"e1-dgn","component":{{"Text":{{"text":{{"path":"/event1GuestNames"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"ed1","component":{{"Divider":{{}}}}}},
|
||||
{{"id":"modal-e2","component":{{"Modal":{{"entryPointChild":"e2-row","contentChild":"e2-detail"}}}}}},
|
||||
{{"id":"e2-row","component":{{"Row":{{"children":{{"explicitList":["e2-time","e2-title","e2-gc"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"e2-time","component":{{"Text":{{"text":{{"path":"/event2Time"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"e2-title","weight":1,"component":{{"Text":{{"text":{{"path":"/event2Title"}},"usageHint":"body"}}}}}},
|
||||
{{"id":"e2-gc","component":{{"Row":{{"children":{{"explicitList":["e2-pi","e2-gn"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"e2-pi","component":{{"Icon":{{"name":{{"literalString":"person"}}}}}}}},
|
||||
{{"id":"e2-gn","component":{{"Text":{{"text":{{"path":"/event2Guests"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"e2-detail","component":{{"Column":{{"children":{{"explicitList":["e2-dt","e2-dtm","e2-dd","e2-dgl","e2-dgn"]}}}}}}}},
|
||||
{{"id":"e2-dt","component":{{"Text":{{"text":{{"path":"/event2Title"}},"usageHint":"h4"}}}}}},
|
||||
{{"id":"e2-dtm","component":{{"Text":{{"text":{{"path":"/event2TimeFull"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"e2-dd","component":{{"Divider":{{}}}}}},
|
||||
{{"id":"e2-dgl","component":{{"Text":{{"text":{{"path":"/event2GuestsLabel"}},"usageHint":"body"}}}}}},
|
||||
{{"id":"e2-dgn","component":{{"Text":{{"text":{{"path":"/event2GuestNames"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"ed2","component":{{"Divider":{{}}}}}},
|
||||
{{"id":"modal-e3","component":{{"Modal":{{"entryPointChild":"e3-row","contentChild":"e3-detail"}}}}}},
|
||||
{{"id":"e3-row","component":{{"Row":{{"children":{{"explicitList":["e3-time","e3-title","e3-gc"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"e3-time","component":{{"Text":{{"text":{{"path":"/event3Time"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"e3-title","weight":1,"component":{{"Text":{{"text":{{"path":"/event3Title"}},"usageHint":"body"}}}}}},
|
||||
{{"id":"e3-gc","component":{{"Row":{{"children":{{"explicitList":["e3-pi","e3-gn"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"e3-pi","component":{{"Icon":{{"name":{{"literalString":"person"}}}}}}}},
|
||||
{{"id":"e3-gn","component":{{"Text":{{"text":{{"path":"/event3Guests"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"e3-detail","component":{{"Column":{{"children":{{"explicitList":["e3-dt","e3-dtm","e3-dd","e3-dgl","e3-dgn"]}}}}}}}},
|
||||
{{"id":"e3-dt","component":{{"Text":{{"text":{{"path":"/event3Title"}},"usageHint":"h4"}}}}}},
|
||||
{{"id":"e3-dtm","component":{{"Text":{{"text":{{"path":"/event3TimeFull"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"e3-dd","component":{{"Divider":{{}}}}}},
|
||||
{{"id":"e3-dgl","component":{{"Text":{{"text":{{"path":"/event3GuestsLabel"}},"usageHint":"body"}}}}}},
|
||||
{{"id":"e3-dgn","component":{{"Text":{{"text":{{"path":"/event3GuestNames"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"ed3","component":{{"Divider":{{}}}}}},
|
||||
{{"id":"modal-e4","component":{{"Modal":{{"entryPointChild":"e4-row","contentChild":"e4-detail"}}}}}},
|
||||
{{"id":"e4-row","component":{{"Row":{{"children":{{"explicitList":["e4-time","e4-title","e4-gc"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"e4-time","component":{{"Text":{{"text":{{"path":"/event4Time"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"e4-title","weight":1,"component":{{"Text":{{"text":{{"path":"/event4Title"}},"usageHint":"body"}}}}}},
|
||||
{{"id":"e4-gc","component":{{"Row":{{"children":{{"explicitList":["e4-pi","e4-gn"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"e4-pi","component":{{"Icon":{{"name":{{"literalString":"person"}}}}}}}},
|
||||
{{"id":"e4-gn","component":{{"Text":{{"text":{{"path":"/event4Guests"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"e4-detail","component":{{"Column":{{"children":{{"explicitList":["e4-dt","e4-dtm","e4-dd","e4-dgl","e4-dgn"]}}}}}}}},
|
||||
{{"id":"e4-dt","component":{{"Text":{{"text":{{"path":"/event4Title"}},"usageHint":"h4"}}}}}},
|
||||
{{"id":"e4-dtm","component":{{"Text":{{"text":{{"path":"/event4TimeFull"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"e4-dd","component":{{"Divider":{{}}}}}},
|
||||
{{"id":"e4-dgl","component":{{"Text":{{"text":{{"path":"/event4GuestsLabel"}},"usageHint":"body"}}}}}},
|
||||
{{"id":"e4-dgn","component":{{"Text":{{"text":{{"path":"/event4GuestNames"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"ed4","component":{{"Divider":{{}}}}}},
|
||||
{{"id":"modal-e5","component":{{"Modal":{{"entryPointChild":"e5-row","contentChild":"e5-detail"}}}}}},
|
||||
{{"id":"e5-row","component":{{"Row":{{"children":{{"explicitList":["e5-time","e5-title","e5-gc"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"e5-time","component":{{"Text":{{"text":{{"path":"/event5Time"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"e5-title","weight":1,"component":{{"Text":{{"text":{{"path":"/event5Title"}},"usageHint":"body"}}}}}},
|
||||
{{"id":"e5-gc","component":{{"Row":{{"children":{{"explicitList":["e5-pi","e5-gn"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"e5-pi","component":{{"Icon":{{"name":{{"literalString":"person"}}}}}}}},
|
||||
{{"id":"e5-gn","component":{{"Text":{{"text":{{"path":"/event5Guests"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"e5-detail","component":{{"Column":{{"children":{{"explicitList":["e5-dt","e5-dtm","e5-dd","e5-dgl","e5-dgn"]}}}}}}}},
|
||||
{{"id":"e5-dt","component":{{"Text":{{"text":{{"path":"/event5Title"}},"usageHint":"h4"}}}}}},
|
||||
{{"id":"e5-dtm","component":{{"Text":{{"text":{{"path":"/event5TimeFull"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"e5-dd","component":{{"Divider":{{}}}}}},
|
||||
{{"id":"e5-dgl","component":{{"Text":{{"text":{{"path":"/event5GuestsLabel"}},"usageHint":"body"}}}}}},
|
||||
{{"id":"e5-dgn","component":{{"Text":{{"text":{{"path":"/event5GuestNames"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"inbox-card","component":{{"Card":{{"child":"inbox-col"}}}}}},
|
||||
{{"id":"inbox-col","component":{{"Column":{{"children":{{"explicitList":["inbox-hdr","modal-em1","emd1","modal-em2"]}}}}}}}},
|
||||
{{"id":"inbox-hdr","component":{{"Row":{{"children":{{"explicitList":["inbox-icon","inbox-text"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"inbox-icon","component":{{"Icon":{{"name":{{"literalString":"mail"}}}}}}}},
|
||||
{{"id":"inbox-text","component":{{"Text":{{"text":{{"literalString":"Unread Messages"}},"usageHint":"h4"}}}}}},
|
||||
{{"id":"modal-em1","component":{{"Modal":{{"entryPointChild":"em1-row","contentChild":"em1-detail"}}}}}},
|
||||
{{"id":"em1-row","component":{{"Row":{{"children":{{"explicitList":["em1-icon","em1-col"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"em1-icon","component":{{"Icon":{{"name":{{"literalString":"mail"}}}}}}}},
|
||||
{{"id":"em1-col","weight":1,"component":{{"Column":{{"children":{{"explicitList":["em1-subject","em1-from"]}}}}}}}},
|
||||
{{"id":"em1-subject","component":{{"Text":{{"text":{{"path":"/email1Subject"}},"usageHint":"body"}}}}}},
|
||||
{{"id":"em1-from","component":{{"Text":{{"text":{{"path":"/email1From"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"em1-detail","component":{{"Column":{{"children":{{"explicitList":["em1-ds","em1-df","em1-dd","em1-db"]}}}}}}}},
|
||||
{{"id":"em1-ds","component":{{"Text":{{"text":{{"path":"/email1Subject"}},"usageHint":"h4"}}}}}},
|
||||
{{"id":"em1-df","component":{{"Text":{{"text":{{"path":"/email1From"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"em1-dd","component":{{"Divider":{{}}}}}},
|
||||
{{"id":"em1-db","component":{{"Text":{{"text":{{"path":"/email1Body"}},"usageHint":"body"}}}}}},
|
||||
{{"id":"emd1","component":{{"Divider":{{}}}}}},
|
||||
{{"id":"modal-em2","component":{{"Modal":{{"entryPointChild":"em2-row","contentChild":"em2-detail"}}}}}},
|
||||
{{"id":"em2-row","component":{{"Row":{{"children":{{"explicitList":["em2-icon","em2-col"]}},"alignment":"center"}}}}}},
|
||||
{{"id":"em2-icon","component":{{"Icon":{{"name":{{"literalString":"mail"}}}}}}}},
|
||||
{{"id":"em2-col","weight":1,"component":{{"Column":{{"children":{{"explicitList":["em2-subject","em2-from"]}}}}}}}},
|
||||
{{"id":"em2-subject","component":{{"Text":{{"text":{{"path":"/email2Subject"}},"usageHint":"body"}}}}}},
|
||||
{{"id":"em2-from","component":{{"Text":{{"text":{{"path":"/email2From"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"em2-detail","component":{{"Column":{{"children":{{"explicitList":["em2-ds","em2-df","em2-dd","em2-db"]}}}}}}}},
|
||||
{{"id":"em2-ds","component":{{"Text":{{"text":{{"path":"/email2Subject"}},"usageHint":"h4"}}}}}},
|
||||
{{"id":"em2-df","component":{{"Text":{{"text":{{"path":"/email2From"}},"usageHint":"caption"}}}}}},
|
||||
{{"id":"em2-dd","component":{{"Divider":{{}}}}}},
|
||||
{{"id":"em2-db","component":{{"Text":{{"text":{{"path":"/email2Body"}},"usageHint":"body"}}}}}}
|
||||
]
|
||||
|
||||
data (populate from get_daily_brief results):
|
||||
{{
|
||||
"title": "Daily Brief",
|
||||
"subtitle": "*Monday, February 2, 2026*",
|
||||
"meetingCount": "5",
|
||||
"unreadCount": "2",
|
||||
"availableHours": "4",
|
||||
"execSummary": "You have 5 meetings today with 4 hours of free time between them. 2 unread messages need attention — David is requesting a quick sync to set up a new project, and Sarah shared the Q1 report for review before Thursday's meeting.",
|
||||
"event1Time": "*8:00–9:00 AM*",
|
||||
"event1Title": "Morning Meeting",
|
||||
"event1Guests": "3",
|
||||
"event1TimeFull": "*8:00 AM – 9:00 AM*",
|
||||
"event1GuestsLabel": "3 guests",
|
||||
"event1GuestNames": "*Sarah Chen, Mike Johnson, Jessica Park*",
|
||||
"event2Time": "*9:00–10:00 AM*",
|
||||
"event2Title": "Project Work",
|
||||
"event2Guests": "2",
|
||||
"event2TimeFull": "*9:00 AM – 10:00 AM*",
|
||||
"event2GuestsLabel": "2 guests",
|
||||
"event2GuestNames": "*David Dave, Sarah Chen*",
|
||||
"event3Time": "*11:00–11:30 AM*",
|
||||
"event3Title": "Client Call",
|
||||
"event3Guests": "3",
|
||||
"event3TimeFull": "*11:00 AM – 11:30 AM*",
|
||||
"event3GuestsLabel": "3 guests",
|
||||
"event3GuestNames": "*Alex Rivera, Sarah Chen, Nathan Ward*",
|
||||
"event4Time": "*2:00–3:00 PM*",
|
||||
"event4Title": "Team Sync",
|
||||
"event4Guests": "5",
|
||||
"event4TimeFull": "*2:00 PM – 3:00 PM*",
|
||||
"event4GuestsLabel": "5 guests",
|
||||
"event4GuestNames": "*Sarah Chen, Mike Johnson, Jessica Park, David Dave, Anmol Kapoor*",
|
||||
"event5Time": "*4:00–4:30 PM*",
|
||||
"event5Title": "Report Review",
|
||||
"event5Guests": "1",
|
||||
"event5TimeFull": "*4:00 PM – 4:30 PM*",
|
||||
"event5GuestsLabel": "1 guest",
|
||||
"event5GuestNames": "*Sarah Chen*",
|
||||
"email1Subject": "Quick Sync",
|
||||
"email1From": "*david.dave@company.org*",
|
||||
"email1Body": "Hey, I need to loop you in with some colleagues for a meeting to set up a new project. When would you be available? Please reply asap",
|
||||
"email2Subject": "Q1 Report Review",
|
||||
"email2From": "*sarah.chen@company.org*",
|
||||
"email2Body": "Hi team, I've attached the Q1 report for your review. Please take a look before our meeting on Thursday and come prepared with feedback."
|
||||
}}
|
||||
|
||||
Adjust the number of event rows and email rows based on actual data. Add or remove Modal+row+detail groups and update the parent Column's explicitList.
|
||||
|
||||
IMPORTANT: Use render_daily_brief, NOT send_a2ui_json_to_client for the daily brief.
|
||||
|
||||
# CRITICAL: Required Tool-Calling Sequences
|
||||
You MUST follow these exact sequences. NEVER skip the render step.
|
||||
|
||||
- Calendar: ALWAYS call get_user_schedule, then ALWAYS call render_calendar
|
||||
- Inbox: ALWAYS call check_user_inbox, then ALWAYS call render_inbox
|
||||
- Email compose: ALWAYS call render_email_compose
|
||||
- Daily brief: ALWAYS call get_daily_brief, then ALWAYS call render_daily_brief
|
||||
|
||||
# CRITICAL: Text Output Rules
|
||||
- Write ONE short sentence BEFORE calling the render tool (e.g. "Here's your schedule for today.")
|
||||
- After calling the render tool, STOP. Do NOT write any more text. Your turn is DONE.
|
||||
- NEVER write text both before AND after a render tool call.
|
||||
- NEVER repeat or rephrase the same sentence after a tool call. If you already said it, do not say it again.
|
||||
- NEVER summarize or list the data as text — the component displays it.
|
||||
- NEVER skip the render tool and just describe the data in text.
|
||||
- Always generate valid JSON with double-quoted property names.
|
||||
- When updating the schedule, include ALL events in the render_calendar call (existing + new).
|
||||
|
||||
## Example: Correct flow for "check my inbox"
|
||||
1. You write: "I'll pull up your inbox now."
|
||||
2. You call check_user_inbox
|
||||
3. You receive the inbox data
|
||||
4. You call render_inbox with the email data
|
||||
5. You STOP. No more text. Do NOT repeat "I'll pull up your inbox now." or any variation.
|
||||
"""
|
||||
|
||||
agent_llm = OpenAiCompatibleConfig(
|
||||
name="my_llm",
|
||||
model_id=os.environ.get("OPENAI_MODEL", "gpt-5.2"),
|
||||
url=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
|
||||
api_type=OpenAIAPIType.RESPONSES,
|
||||
default_generation_parameters=LlmGenerationConfig(reasoning={"effort": "low"}),
|
||||
)
|
||||
|
||||
send_a2ui_json_to_client_tool = ClientTool(
|
||||
name="send_a2ui_json_to_client",
|
||||
description="Legacy fallback for rendering raw A2UI JSON. Do not use directly — use render_calendar, render_inbox, render_email_compose, or render_daily_brief instead.",
|
||||
inputs=[
|
||||
StringProperty(
|
||||
title="a2ui_json",
|
||||
description="Valid A2UI JSON Schema to send to the client.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
render_calendar_tool = ClientTool(
|
||||
name="render_calendar",
|
||||
description="Renders a rich calendar day-view on the client. "
|
||||
"Call this after retrieving the user's schedule with get_user_schedule. "
|
||||
"Pass the date, day name, and events as a JSON array string.",
|
||||
inputs=[
|
||||
StringProperty(title="date", description="The date, e.g. '2026-02-02'"),
|
||||
StringProperty(title="dayName", description="The day name, e.g. 'Monday'"),
|
||||
StringProperty(
|
||||
title="events",
|
||||
description="JSON array string of event objects with startTime, endTime, title, isAvailable fields",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
render_email_compose_tool = ClientTool(
|
||||
name="render_email_compose",
|
||||
description="Renders a Gmail-style email compose view on the client. "
|
||||
"Call this when the user wants to compose, draft, or reply to an email. "
|
||||
"Pass a JSON object string with to, subject, and body fields.",
|
||||
inputs=[
|
||||
StringProperty(
|
||||
title="email",
|
||||
description="JSON object string with to, subject, body fields",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
render_inbox_tool = ClientTool(
|
||||
name="render_inbox",
|
||||
description="Renders a Gmail-style inbox view on the client. "
|
||||
"Call this after checking the user's inbox with check_user_inbox. "
|
||||
"Pass the emails as a JSON array string.",
|
||||
inputs=[
|
||||
StringProperty(
|
||||
title="emails",
|
||||
description="JSON array string of email objects with from, subject, body, date, isRead fields",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
check_user_inbox_tool = ServerTool(
|
||||
name="check_user_inbox",
|
||||
description="Checks the user's inbox. Returns a JSON array of emails. Optionally searches based on keywords.",
|
||||
inputs=[StringProperty(title="search_query")],
|
||||
outputs=[StringProperty(title="email_content")],
|
||||
)
|
||||
|
||||
get_user_schedule_tool = ServerTool(
|
||||
name="get_user_schedule",
|
||||
description="Retrieves the user's schedule for today. Returns a JSON array of time slots with events.",
|
||||
outputs=[StringProperty(title="schedule_data")],
|
||||
)
|
||||
|
||||
send_email_tool = ServerTool(
|
||||
name="send_email",
|
||||
description="Sends an email out. Accepts a single argument as the entire email, including the sender, recipients, subject, body, etc.",
|
||||
inputs=[StringProperty(title="payload")],
|
||||
outputs=[StringProperty(title="result")],
|
||||
)
|
||||
|
||||
get_daily_brief_tool = ServerTool(
|
||||
name="get_daily_brief",
|
||||
description="Retrieves the user's daily brief including schedule summary, inbox highlights, and action items.",
|
||||
outputs=[StringProperty(title="brief_data")],
|
||||
)
|
||||
|
||||
render_daily_brief_tool = ClientTool(
|
||||
name="render_daily_brief",
|
||||
description="Renders a daily brief dashboard in the canvas workspace using A2UI components. "
|
||||
"Pass the A2UI component definitions, data, and root component ID as JSON strings.",
|
||||
inputs=[
|
||||
StringProperty(
|
||||
title="components",
|
||||
description="JSON array string of A2UI ComponentInstance objects",
|
||||
),
|
||||
StringProperty(
|
||||
title="data",
|
||||
description="JSON object string with data values for the dashboard",
|
||||
),
|
||||
StringProperty(title="root", description="ID of the root component"),
|
||||
],
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="a2ui_chat_agent",
|
||||
llm_config=agent_llm,
|
||||
system_prompt=A2UI_SYSTEM_PROMPT,
|
||||
tools=[
|
||||
send_a2ui_json_to_client_tool,
|
||||
render_calendar_tool,
|
||||
render_inbox_tool,
|
||||
render_email_compose_tool,
|
||||
render_daily_brief_tool,
|
||||
check_user_inbox_tool,
|
||||
get_user_schedule_tool,
|
||||
get_daily_brief_tool,
|
||||
send_email_tool,
|
||||
],
|
||||
)
|
||||
|
||||
a2ui_chat_json = AgentSpecSerializer().to_json(agent)
|
||||
|
||||
demo_schedule = [
|
||||
{
|
||||
"startTime": "08:00",
|
||||
"endTime": "09:00",
|
||||
"title": "Morning Meeting",
|
||||
"isAvailable": False,
|
||||
"guests": [
|
||||
{"email": "sarah.chen@company.org", "status": "accepted"},
|
||||
{"email": "mike.johnson@company.org", "status": "accepted"},
|
||||
{"email": "jessica.park@company.org", "status": "maybe"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"startTime": "09:00",
|
||||
"endTime": "10:00",
|
||||
"title": "Project Work",
|
||||
"isAvailable": False,
|
||||
"guests": [
|
||||
{"email": "david.dave@company.org", "status": "accepted"},
|
||||
{"email": "sarah.chen@company.org", "status": "accepted"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"startTime": "10:00",
|
||||
"endTime": "11:00",
|
||||
"title": "Available",
|
||||
"isAvailable": True,
|
||||
},
|
||||
{
|
||||
"startTime": "11:00",
|
||||
"endTime": "11:30",
|
||||
"title": "Client Call",
|
||||
"isAvailable": False,
|
||||
"guests": [
|
||||
{"email": "alex.rivera@client.com", "status": "accepted"},
|
||||
{"email": "sarah.chen@company.org", "status": "declined"},
|
||||
{"email": "nathan.ward@company.org", "status": "pending"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"startTime": "12:00",
|
||||
"endTime": "13:00",
|
||||
"title": "Lunch Break",
|
||||
"isAvailable": False,
|
||||
},
|
||||
{
|
||||
"startTime": "13:00",
|
||||
"endTime": "14:00",
|
||||
"title": "Available",
|
||||
"isAvailable": True,
|
||||
},
|
||||
{
|
||||
"startTime": "14:00",
|
||||
"endTime": "15:00",
|
||||
"title": "Team Sync",
|
||||
"isAvailable": False,
|
||||
"guests": [
|
||||
{"email": "sarah.chen@company.org", "status": "accepted"},
|
||||
{"email": "mike.johnson@company.org", "status": "accepted"},
|
||||
{"email": "jessica.park@company.org", "status": "accepted"},
|
||||
{"email": "david.dave@company.org", "status": "declined"},
|
||||
{"email": "anmol.kapoor@company.org", "status": "pending"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"startTime": "15:00",
|
||||
"endTime": "16:00",
|
||||
"title": "Available",
|
||||
"isAvailable": True,
|
||||
},
|
||||
{
|
||||
"startTime": "16:00",
|
||||
"endTime": "16:30",
|
||||
"title": "Report Review",
|
||||
"isAvailable": False,
|
||||
"guests": [
|
||||
{"email": "sarah.chen@company.org", "status": "accepted"},
|
||||
],
|
||||
},
|
||||
{"startTime": "17:00", "endTime": "", "title": "Available", "isAvailable": True},
|
||||
]
|
||||
|
||||
demo_inbox = [
|
||||
{
|
||||
"from": "david.dave@company.org",
|
||||
"subject": "Quick Sync",
|
||||
"body": "Hey, I need to loop you in with some colleagues for a meeting to set up a new project. When would you be available? Please reply asap",
|
||||
"date": "10:30 AM",
|
||||
"isRead": False,
|
||||
},
|
||||
{
|
||||
"from": "sarah.chen@company.org",
|
||||
"subject": "Q1 Report Review",
|
||||
"body": "Hi team, I've attached the Q1 report for your review. Please take a look before our meeting on Thursday and come prepared with feedback.",
|
||||
"date": "9:15 AM",
|
||||
"isRead": False,
|
||||
},
|
||||
{
|
||||
"from": "mike.johnson@company.org",
|
||||
"subject": "Lunch plans?",
|
||||
"body": "Anyone up for trying that new sushi place on 5th street? I heard they have great lunch specials this week.",
|
||||
"date": "Yesterday",
|
||||
"isRead": True,
|
||||
},
|
||||
{
|
||||
"from": "hr@company.org",
|
||||
"subject": "Benefits Enrollment Reminder",
|
||||
"body": "This is a reminder that open enrollment for benefits closes on February 15th. Please review your options in the employee portal.",
|
||||
"date": "Yesterday",
|
||||
"isRead": True,
|
||||
},
|
||||
{
|
||||
"from": "jessica.park@company.org",
|
||||
"subject": "Design Review Feedback",
|
||||
"body": "Great work on the new dashboard mockups! I have a few suggestions for the navigation layout that I think could improve the UX.",
|
||||
"date": "Feb 1",
|
||||
"isRead": True,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def user_schedule_tool(*args, **kwargs):
|
||||
return json.dumps(demo_schedule)
|
||||
|
||||
|
||||
def check_inbox_tool(*args, **kwargs):
|
||||
return json.dumps(demo_inbox)
|
||||
|
||||
|
||||
def send_email_tool_fn(*args, **kwargs):
|
||||
return "Email sent successfully!"
|
||||
|
||||
|
||||
def get_daily_brief_fn(*args, **kwargs):
|
||||
meetings = [
|
||||
e for e in demo_schedule if not e["isAvailable"] and e["title"] != "Lunch Break"
|
||||
]
|
||||
available_slots = [e for e in demo_schedule if e["isAvailable"]]
|
||||
unread_emails = [e for e in demo_inbox if not e["isRead"]]
|
||||
|
||||
available_hours = 0
|
||||
for slot in available_slots:
|
||||
if slot["endTime"]:
|
||||
sh, sm = map(int, slot["startTime"].split(":"))
|
||||
eh, em = map(int, slot["endTime"].split(":"))
|
||||
available_hours += (eh * 60 + em - sh * 60 - sm) / 60
|
||||
else:
|
||||
available_hours += 1
|
||||
|
||||
brief = {
|
||||
"summary": {
|
||||
"meetingCount": len(meetings),
|
||||
"unreadCount": len(unread_emails),
|
||||
"availableHours": int(available_hours),
|
||||
},
|
||||
"upcomingEvents": [
|
||||
{
|
||||
"title": e["title"],
|
||||
"startTime": e["startTime"],
|
||||
"endTime": e["endTime"],
|
||||
"guestCount": len(e.get("guests", [])),
|
||||
"guestNames": ", ".join(
|
||||
" ".join(
|
||||
p.capitalize() for p in g["email"].split("@")[0].split(".")
|
||||
)
|
||||
for g in e.get("guests", [])
|
||||
),
|
||||
}
|
||||
for e in meetings
|
||||
],
|
||||
"priorityEmails": [
|
||||
{
|
||||
"from": e["from"],
|
||||
"subject": e["subject"],
|
||||
"preview": e["body"][:80] + "..." if len(e["body"]) > 80 else e["body"],
|
||||
"body": e["body"],
|
||||
"date": e["date"],
|
||||
}
|
||||
for e in unread_emails
|
||||
],
|
||||
}
|
||||
return json.dumps(brief)
|
||||
|
||||
|
||||
a2ui_demo_tool_registry = {
|
||||
"check_user_inbox": check_inbox_tool,
|
||||
"get_user_schedule": user_schedule_tool,
|
||||
"get_daily_brief": get_daily_brief_fn,
|
||||
"send_email": send_email_tool_fn,
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
## A2UI Protocol Instructions
|
||||
|
||||
A2UI (Agent to UI) is a protocol for rendering rich UI surfaces from agent responses.
|
||||
When using the send_a2ui_json_to_client tool, you MUST follow these rules:
|
||||
|
||||
### CRITICAL: Required Message Sequence
|
||||
|
||||
To render a surface, you MUST send ALL messages in a SINGLE send_a2ui_json_to_client tool call, in this order:
|
||||
1. **surfaceUpdate** - Define all UI components (REQUIRED)
|
||||
2. **dataModelUpdate** - Set any data values (OPTIONAL)
|
||||
3. **beginRendering** - Signal the client to start rendering (REQUIRED)
|
||||
|
||||
**IMPORTANT**:
|
||||
- The `beginRendering` message is MANDATORY. Without it, the client will buffer your components but NEVER display them.
|
||||
- ALL messages (surfaceUpdate AND beginRendering) MUST be in the SAME a2ui_json array in ONE tool call. Do NOT make separate tool calls for surfaceUpdate and beginRendering - they will not work!
|
||||
|
||||
### Minimal Working Example
|
||||
|
||||
Here is the simplest possible A2UI surface - a button:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"surfaceUpdate": {
|
||||
"surfaceId": "my-surface",
|
||||
"components": [
|
||||
{
|
||||
"id": "root",
|
||||
"component": {
|
||||
"Button": {
|
||||
"child": "btn-text",
|
||||
"action": { "name": "button_clicked" }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "btn-text",
|
||||
"component": {
|
||||
"Text": { "text": { "literalString": "Click Me" } }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"beginRendering": {
|
||||
"surfaceId": "my-surface",
|
||||
"root": "root"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Key Rules
|
||||
|
||||
1. **Always include beginRendering** - This signals the client to render. Without it, nothing displays.
|
||||
2. **Use unique surfaceId values** - Each surface must have a unique ID.
|
||||
3. **The root component** - The `root` in beginRendering must match a component ID from surfaceUpdate.
|
||||
4. **Flat component structure** - Components reference children by ID, not by nesting.
|
||||
5. **Text is separate** - Buttons, Cards, etc. reference Text components by ID for their labels.
|
||||
6. **Production ready** - The UI you generate will be shown to real users. It must be complete, polished, and functional.
|
||||
7. **No placeholder images** - NEVER use fake or placeholder image URLs like `https://example.com/image.jpg` or `https://placeholder.com/...`. Only use real, valid image URLs that actually exist. If you don't have a real image URL, omit the image component entirely or use an Icon component instead.
|
||||
8. **Root must be a layout component** - The root component in `beginRendering` should be a layout container like Column, Row, Card, or similar. Do NOT use Modal, Button, Text, or other leaf/special components as the root. Wrap them in a Column or Card first.
|
||||
9. **Modal vs direct content** - The `Modal` component is for "click a button to open a popup" patterns. It shows only its `entryPointChild` (a trigger button) initially, and the `contentChild` is hidden until clicked. When users ask for an "alert dialog", "confirmation dialog", or similar, they usually want the content visible immediately - use a Card or Column with the content directly, NOT a Modal.
|
||||
|
||||
### Updating Surfaces After Initial Render
|
||||
|
||||
Once a surface has been rendered (after `beginRendering`), you can update it in later turns WITHOUT sending another `beginRendering`. Just send updates directly:
|
||||
|
||||
**To update UI components** - Send a `surfaceUpdate` with the same surfaceId:
|
||||
- To modify a component: send it with the same `id` - it replaces the old definition
|
||||
- To add new components: include them in the components array
|
||||
- The client will re-render automatically
|
||||
|
||||
**To update data values** - Send a `dataModelUpdate`:
|
||||
- Components bound to data paths (using `"path": "/some/value"`) update automatically
|
||||
- Only send the data that changed
|
||||
|
||||
**Example: Updating an existing surface**
|
||||
|
||||
If you previously rendered a surface with `surfaceId: "my-surface"`, you can update it like this:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"surfaceUpdate": {
|
||||
"surfaceId": "my-surface",
|
||||
"components": [
|
||||
{
|
||||
"id": "status-text",
|
||||
"component": {
|
||||
"Text": { "text": { "literalString": "Updated status!" } }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Or update data-bound values:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"dataModelUpdate": {
|
||||
"surfaceId": "my-surface",
|
||||
"contents": [
|
||||
{ "key": "status", "valueString": "Complete" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**IMPORTANT**: Do NOT send `beginRendering` again for updates to an existing surface. It's only needed for the initial render.
|
||||
|
||||
### Working with Forms and Data Binding
|
||||
|
||||
A2UI supports forms where user input is automatically stored in a data model and can be retrieved when buttons are clicked.
|
||||
|
||||
**How it works:**
|
||||
1. **TextField binds to a path**: Use `"text": { "path": "/form/fieldName" }` to bind input to the data model
|
||||
2. **Initialize the data model**: Send a `dataModelUpdate` to set initial values
|
||||
3. **Button retrieves values**: Use `action.context` with path references to include form values when clicked
|
||||
4. **Agent receives resolved values**: The context in the action will contain the actual values the user entered
|
||||
|
||||
**Form Example:**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"surfaceUpdate": {
|
||||
"surfaceId": "my-form",
|
||||
"components": [
|
||||
{ "id": "root", "component": { "Card": { "child": "form-col" } } },
|
||||
{ "id": "form-col", "component": { "Column": { "children": { "explicitList": ["name-field", "submit-btn"] } } } },
|
||||
{ "id": "name-field", "component": { "TextField": { "label": { "literalString": "Name" }, "text": { "path": "/form/name" } } } },
|
||||
{ "id": "submit-btn", "component": { "Button": { "child": "btn-text", "action": { "name": "submit", "context": [{ "key": "userName", "value": { "path": "/form/name" } }] } } } },
|
||||
{ "id": "btn-text", "component": { "Text": { "text": { "literalString": "Submit" } } } }
|
||||
]
|
||||
}
|
||||
},
|
||||
{ "dataModelUpdate": { "surfaceId": "my-form", "contents": [{ "key": "form", "valueMap": [{ "key": "name", "valueString": "" }] }] } },
|
||||
{ "beginRendering": { "surfaceId": "my-form", "root": "root" } }
|
||||
]
|
||||
```
|
||||
|
||||
When the user types "Alice" and clicks Submit, you'll receive: `Context: {"userName": "Alice"}`
|
||||
|
||||
### Handling User Interactions
|
||||
|
||||
When a user interacts with a UI surface you rendered (clicks a button, submits a form, etc.),
|
||||
you will see a `log_a2ui_event` tool call in your conversation history followed by a tool result.
|
||||
|
||||
CRITICAL: If the conversation ends with a `log_a2ui_event` tool call followed by its tool result,
|
||||
this means THE USER JUST PERFORMED AN ACTION and you MUST respond to it immediately.
|
||||
|
||||
The `log_a2ui_event` tool call is NOT something you initiated - it is automatically injected into
|
||||
the conversation to represent a real user interaction (like clicking a button) that just happened.
|
||||
|
||||
When the last messages are a `log_a2ui_event` tool call + result:
|
||||
1. The user JUST performed the action described (e.g., clicked a button)
|
||||
2. You MUST acknowledge their action and respond appropriately
|
||||
3. Look at the action name to understand what they did
|
||||
4. Take the appropriate next step based on what that action means in context
|
||||
5. Do NOT simply describe what buttons exist - respond to what they clicked!
|
||||
@@ -0,0 +1,773 @@
|
||||
{
|
||||
"title": "A2UI Message Schema",
|
||||
"description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces. A message MUST contain exactly ONE of the action properties: 'beginRendering', 'surfaceUpdate', 'dataModelUpdate', or 'deleteSurface'.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"beginRendering": {
|
||||
"type": "object",
|
||||
"description": "Signals the client to begin rendering a surface with a root component and specific styles.",
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be rendered."
|
||||
},
|
||||
"root": {
|
||||
"type": "string",
|
||||
"description": "The ID of the root component to render."
|
||||
},
|
||||
"styles": {
|
||||
"type": "object",
|
||||
"description": "Styling information for the UI.",
|
||||
"properties": {
|
||||
"font": {
|
||||
"type": "string",
|
||||
"description": "The primary font for the UI."
|
||||
},
|
||||
"primaryColor": {
|
||||
"type": "string",
|
||||
"description": "The primary UI color as a hexadecimal code (e.g., '#00BFFF').",
|
||||
"pattern": "^#[0-9a-fA-F]{6}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["root", "surfaceId"]
|
||||
},
|
||||
"surfaceUpdate": {
|
||||
"type": "object",
|
||||
"description": "Updates a surface with a new set of components.",
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be updated. If you are adding a new surface this *must* be a new, unique identified that has never been used for any existing surfaces shown."
|
||||
},
|
||||
"components": {
|
||||
"type": "array",
|
||||
"description": "A list containing all UI components for the surface.",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "Represents a *single* component in a UI widget tree. This component could be one of many supported types.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for this component."
|
||||
},
|
||||
"weight": {
|
||||
"type": "number",
|
||||
"description": "The relative weight of this component within a Row or Column. This corresponds to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column."
|
||||
},
|
||||
"component": {
|
||||
"type": "object",
|
||||
"description": "A wrapper object that MUST contain exactly one key, which is the name of the component type (e.g., 'Heading'). The value is an object containing the properties for that specific component.",
|
||||
"properties": {
|
||||
"Text": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "object",
|
||||
"description": "The text content to display. This can be a literal string or a reference to a value in the data model ('path', e.g., '/doc/title'). While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.",
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usageHint": {
|
||||
"type": "string",
|
||||
"description": "A hint for the base text style. One of:\n- `h1`: Largest heading.\n- `h2`: Second largest heading.\n- `h3`: Third largest heading.\n- `h4`: Fourth largest heading.\n- `h5`: Fifth largest heading.\n- `caption`: Small text for captions.\n- `body`: Standard body text.",
|
||||
"enum": [
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"caption",
|
||||
"body"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["text"]
|
||||
},
|
||||
"Image": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "object",
|
||||
"description": "The URL of the image to display. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/thumbnail/url').",
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fit": {
|
||||
"type": "string",
|
||||
"description": "Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.",
|
||||
"enum": [
|
||||
"contain",
|
||||
"cover",
|
||||
"fill",
|
||||
"none",
|
||||
"scale-down"
|
||||
]
|
||||
},
|
||||
"usageHint": {
|
||||
"type": "string",
|
||||
"description": "A hint for the image size and style. One of:\n- `icon`: Small square icon.\n- `avatar`: Circular avatar image.\n- `smallFeature`: Small feature image.\n- `mediumFeature`: Medium feature image.\n- `largeFeature`: Large feature image.\n- `header`: Full-width, full bleed, header image.",
|
||||
"enum": [
|
||||
"icon",
|
||||
"avatar",
|
||||
"smallFeature",
|
||||
"mediumFeature",
|
||||
"largeFeature",
|
||||
"header"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
},
|
||||
"Icon": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "object",
|
||||
"description": "The name of the icon to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/form/submit').",
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"accountCircle",
|
||||
"add",
|
||||
"arrowBack",
|
||||
"arrowForward",
|
||||
"attachFile",
|
||||
"calendarToday",
|
||||
"call",
|
||||
"camera",
|
||||
"check",
|
||||
"close",
|
||||
"delete",
|
||||
"download",
|
||||
"edit",
|
||||
"event",
|
||||
"error",
|
||||
"favorite",
|
||||
"favoriteOff",
|
||||
"folder",
|
||||
"help",
|
||||
"home",
|
||||
"info",
|
||||
"locationOn",
|
||||
"lock",
|
||||
"lockOpen",
|
||||
"mail",
|
||||
"menu",
|
||||
"moreVert",
|
||||
"moreHoriz",
|
||||
"notificationsOff",
|
||||
"notifications",
|
||||
"payment",
|
||||
"person",
|
||||
"phone",
|
||||
"photo",
|
||||
"print",
|
||||
"refresh",
|
||||
"search",
|
||||
"send",
|
||||
"settings",
|
||||
"share",
|
||||
"shoppingCart",
|
||||
"star",
|
||||
"starHalf",
|
||||
"starOff",
|
||||
"upload",
|
||||
"visibility",
|
||||
"visibilityOff",
|
||||
"warning"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
},
|
||||
"Video": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "object",
|
||||
"description": "The URL of the video to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/video/url').",
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
},
|
||||
"AudioPlayer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "object",
|
||||
"description": "The URL of the audio to be played. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/song/url').",
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "object",
|
||||
"description": "A description of the audio, such as a title or summary. This can be a literal string or a reference to a value in the data model ('path', e.g. '/song/title').",
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
},
|
||||
"Row": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "object",
|
||||
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
|
||||
"properties": {
|
||||
"explicitList": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"type": "object",
|
||||
"description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.",
|
||||
"properties": {
|
||||
"componentId": {
|
||||
"type": "string"
|
||||
},
|
||||
"dataBinding": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["componentId", "dataBinding"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"distribution": {
|
||||
"type": "string",
|
||||
"description": "Defines the arrangement of children along the main axis (horizontally). This corresponds to the CSS 'justify-content' property.",
|
||||
"enum": [
|
||||
"center",
|
||||
"end",
|
||||
"spaceAround",
|
||||
"spaceBetween",
|
||||
"spaceEvenly",
|
||||
"start"
|
||||
]
|
||||
},
|
||||
"alignment": {
|
||||
"type": "string",
|
||||
"description": "Defines the alignment of children along the cross axis (vertically). This corresponds to the CSS 'align-items' property.",
|
||||
"enum": ["start", "center", "end", "stretch"]
|
||||
}
|
||||
},
|
||||
"required": ["children"]
|
||||
},
|
||||
"Column": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "object",
|
||||
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
|
||||
"properties": {
|
||||
"explicitList": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"type": "object",
|
||||
"description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.",
|
||||
"properties": {
|
||||
"componentId": {
|
||||
"type": "string"
|
||||
},
|
||||
"dataBinding": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["componentId", "dataBinding"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"distribution": {
|
||||
"type": "string",
|
||||
"description": "Defines the arrangement of children along the main axis (vertically). This corresponds to the CSS 'justify-content' property.",
|
||||
"enum": [
|
||||
"start",
|
||||
"center",
|
||||
"end",
|
||||
"spaceBetween",
|
||||
"spaceAround",
|
||||
"spaceEvenly"
|
||||
]
|
||||
},
|
||||
"alignment": {
|
||||
"type": "string",
|
||||
"description": "Defines the alignment of children along the cross axis (horizontally). This corresponds to the CSS 'align-items' property.",
|
||||
"enum": ["center", "end", "start", "stretch"]
|
||||
}
|
||||
},
|
||||
"required": ["children"]
|
||||
},
|
||||
"List": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "object",
|
||||
"description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.",
|
||||
"properties": {
|
||||
"explicitList": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"type": "object",
|
||||
"description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.",
|
||||
"properties": {
|
||||
"componentId": {
|
||||
"type": "string"
|
||||
},
|
||||
"dataBinding": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["componentId", "dataBinding"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"description": "The direction in which the list items are laid out.",
|
||||
"enum": ["vertical", "horizontal"]
|
||||
},
|
||||
"alignment": {
|
||||
"type": "string",
|
||||
"description": "Defines the alignment of children along the cross axis.",
|
||||
"enum": ["start", "center", "end", "stretch"]
|
||||
}
|
||||
},
|
||||
"required": ["children"]
|
||||
},
|
||||
"Card": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"child": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component to be rendered inside the card."
|
||||
}
|
||||
},
|
||||
"required": ["child"]
|
||||
},
|
||||
"Tabs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tabItems": {
|
||||
"type": "array",
|
||||
"description": "An array of objects, where each object defines a tab with a title and a child component.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "object",
|
||||
"description": "The tab title. Defines the value as either a literal value or a path to data model value (e.g. '/options/title').",
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"child": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["title", "child"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["tabItems"]
|
||||
},
|
||||
"Divider": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"axis": {
|
||||
"type": "string",
|
||||
"description": "The orientation of the divider.",
|
||||
"enum": ["horizontal", "vertical"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Modal": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entryPointChild": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component that opens the modal when interacted with (e.g., a button)."
|
||||
},
|
||||
"contentChild": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component to be displayed inside the modal."
|
||||
}
|
||||
},
|
||||
"required": ["entryPointChild", "contentChild"]
|
||||
},
|
||||
"Button": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"child": {
|
||||
"type": "string",
|
||||
"description": "The ID of the component to display in the button, typically a Text component."
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates if this button should be styled as the primary action."
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
"description": "The client-side action to be dispatched when the button is clicked. It includes the action's name and an optional context payload.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"context": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "Defines the value to be included in the context as either a literal value or a path to a data model value (e.g. '/user/name').",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"literalNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"literalBoolean": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["key", "value"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
},
|
||||
"required": ["child", "action"]
|
||||
},
|
||||
"CheckBox": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "object",
|
||||
"description": "The text to display next to the checkbox. Defines the value as either a literal value or a path to data model ('path', e.g. '/option/label').",
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "The current state of the checkbox (true for checked, false for unchecked). This can be a literal boolean ('literalBoolean') or a reference to a value in the data model ('path', e.g. '/filter/open').",
|
||||
"properties": {
|
||||
"literalBoolean": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["label", "value"]
|
||||
},
|
||||
"TextField": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "object",
|
||||
"description": "The text label for the input field. This can be a literal string or a reference to a value in the data model ('path, e.g. '/user/name').",
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"type": "object",
|
||||
"description": "The value of the text field. This can be a literal string or a reference to a value in the data model ('path', e.g. '/user/name').",
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"textFieldType": {
|
||||
"type": "string",
|
||||
"description": "The type of input field to display.",
|
||||
"enum": [
|
||||
"date",
|
||||
"longText",
|
||||
"number",
|
||||
"shortText",
|
||||
"obscured"
|
||||
]
|
||||
},
|
||||
"validationRegexp": {
|
||||
"type": "string",
|
||||
"description": "A regular expression used for client-side validation of the input."
|
||||
}
|
||||
},
|
||||
"required": ["label"]
|
||||
},
|
||||
"DateTimeInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "The selected date and/or time value. This can be a literal string ('literalString') or a reference to a value in the data model ('path', e.g. '/user/dob').",
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enableDate": {
|
||||
"type": "boolean",
|
||||
"description": "If true, allows the user to select a date."
|
||||
},
|
||||
"enableTime": {
|
||||
"type": "boolean",
|
||||
"description": "If true, allows the user to select a time."
|
||||
},
|
||||
"outputFormat": {
|
||||
"type": "string",
|
||||
"description": "The desired format for the output string after a date or time is selected."
|
||||
}
|
||||
},
|
||||
"required": ["value"]
|
||||
},
|
||||
"MultipleChoice": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"selections": {
|
||||
"type": "object",
|
||||
"description": "The currently selected values for the component. This can be a literal array of strings or a path to an array in the data model('path', e.g. '/hotel/options').",
|
||||
"properties": {
|
||||
"literalArray": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"description": "An array of available options for the user to choose from.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "object",
|
||||
"description": "The text to display for this option. This can be a literal string or a reference to a value in the data model (e.g. '/option/label').",
|
||||
"properties": {
|
||||
"literalString": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The value to be associated with this option when selected."
|
||||
}
|
||||
},
|
||||
"required": ["label", "value"]
|
||||
}
|
||||
},
|
||||
"maxAllowedSelections": {
|
||||
"type": "integer",
|
||||
"description": "The maximum number of options that the user is allowed to select."
|
||||
}
|
||||
},
|
||||
"required": ["selections", "options"]
|
||||
},
|
||||
"Slider": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "The current value of the slider. This can be a literal number ('literalNumber') or a reference to a value in the data model ('path', e.g. '/restaurant/cost').",
|
||||
"properties": {
|
||||
"literalNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minValue": {
|
||||
"type": "number",
|
||||
"description": "The minimum value of the slider."
|
||||
},
|
||||
"maxValue": {
|
||||
"type": "number",
|
||||
"description": "The maximum value of the slider."
|
||||
}
|
||||
},
|
||||
"required": ["value"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["id", "component"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["surfaceId", "components"]
|
||||
},
|
||||
"dataModelUpdate": {
|
||||
"type": "object",
|
||||
"description": "Updates the data model for a surface.",
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface this data model update applies to."
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', the entire data model will be replaced."
|
||||
},
|
||||
"contents": {
|
||||
"type": "array",
|
||||
"description": "An array of data entries. Each entry must contain a 'key' and exactly one corresponding typed 'value*' property.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "A single data entry. Exactly one 'value*' property should be provided alongside the key.",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "The key for this data entry."
|
||||
},
|
||||
"valueString": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"valueBoolean": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"valueMap": {
|
||||
"description": "Represents a map as an adjacency list.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "One entry in the map. Exactly one 'value*' property should be provided alongside the key.",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueString": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueNumber": {
|
||||
"type": "number"
|
||||
},
|
||||
"valueBoolean": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["key"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["key"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["contents", "surfaceId"]
|
||||
},
|
||||
"deleteSurface": {
|
||||
"type": "object",
|
||||
"description": "Signals the client to delete the surface identified by 'surfaceId'.",
|
||||
"properties": {
|
||||
"surfaceId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier for the UI surface to be deleted."
|
||||
}
|
||||
},
|
||||
"required": ["surfaceId"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
[
|
||||
{
|
||||
"id": "root",
|
||||
"component": {
|
||||
"Card": {
|
||||
"child": "main-column"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "main-column",
|
||||
"component": {
|
||||
"Column": {
|
||||
"children": {
|
||||
"explicitList": ["header-row", "events-list", "actions"]
|
||||
},
|
||||
"gap": "small"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "header-row",
|
||||
"component": {
|
||||
"Row": {
|
||||
"children": {
|
||||
"explicitList": ["date-col", "events-col"]
|
||||
},
|
||||
"gap": "medium"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "date-col",
|
||||
"component": {
|
||||
"Column": {
|
||||
"children": {
|
||||
"explicitList": ["day-name", "day-number", "month-name"]
|
||||
},
|
||||
"alignment": "start"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "day-name",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"path": "/dayName"
|
||||
},
|
||||
"usageHint": "caption"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "day-number",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"path": "/dayNumber"
|
||||
},
|
||||
"usageHint": "h1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "month-name",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"path": "/monthName"
|
||||
},
|
||||
"usageHint": "h2"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "events-col",
|
||||
"component": {
|
||||
"Column": {
|
||||
"children": {
|
||||
"explicitList": ["event1", "event2", "event3"]
|
||||
},
|
||||
"gap": "small"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "event1",
|
||||
"component": {
|
||||
"Column": {
|
||||
"children": {
|
||||
"explicitList": ["event1-title", "event1-time"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "event1-title",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"path": "/event1/title"
|
||||
},
|
||||
"usageHint": "body"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "event1-time",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"path": "/event1/time"
|
||||
},
|
||||
"usageHint": "caption"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "event2",
|
||||
"component": {
|
||||
"Column": {
|
||||
"children": {
|
||||
"explicitList": ["event2-title", "event2-time"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "event2-title",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"path": "/event2/title"
|
||||
},
|
||||
"usageHint": "body"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "event2-time",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"path": "/event2/time"
|
||||
},
|
||||
"usageHint": "caption"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "event3",
|
||||
"component": {
|
||||
"Column": {
|
||||
"children": {
|
||||
"explicitList": ["event3-title", "event3-time"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "event3-title",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"path": "/event3/title"
|
||||
},
|
||||
"usageHint": "body"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "event3-time",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"path": "/event3/time"
|
||||
},
|
||||
"usageHint": "caption"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "events-list",
|
||||
"component": {
|
||||
"Divider": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "actions",
|
||||
"component": {
|
||||
"Row": {
|
||||
"children": {
|
||||
"explicitList": ["add-btn", "discard-btn"]
|
||||
},
|
||||
"gap": "small"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "add-btn-text",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"literalString": "Add to calendar"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "add-btn",
|
||||
"component": {
|
||||
"Button": {
|
||||
"child": "add-btn-text",
|
||||
"action": "add"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "discard-btn-text",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"literalString": "Discard"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "discard-btn",
|
||||
"component": {
|
||||
"Button": {
|
||||
"child": "discard-btn-text",
|
||||
"action": "discard"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"dayName": "Friday",
|
||||
"dayNumber": "28",
|
||||
"monthName": "March",
|
||||
"event1": {
|
||||
"title": "Lunch",
|
||||
"time": "12:00 - 12:45 PM"
|
||||
},
|
||||
"event2": {
|
||||
"title": "Q1 roadmap review",
|
||||
"time": "1:00 - 2:00 PM"
|
||||
},
|
||||
"event3": {
|
||||
"title": "Team standup (add more events if needed, e.g. event4, event5, etc.)",
|
||||
"time": "3:30 - 4:00 PM"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
[
|
||||
{
|
||||
"id": "root",
|
||||
"component": {
|
||||
"Card": {
|
||||
"child": "main-column"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "main-column",
|
||||
"component": {
|
||||
"Column": {
|
||||
"children": {
|
||||
"explicitList": [
|
||||
"from-row",
|
||||
"to-row",
|
||||
"subject-row",
|
||||
"divider",
|
||||
"message",
|
||||
"actions"
|
||||
]
|
||||
},
|
||||
"gap": "small"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "from-row",
|
||||
"component": {
|
||||
"Row": {
|
||||
"children": {
|
||||
"explicitList": ["from-label", "from-value"]
|
||||
},
|
||||
"gap": "medium",
|
||||
"alignment": "center"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "from-label",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"literalString": "FROM"
|
||||
},
|
||||
"usageHint": "caption"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "from-value",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"path": "/from"
|
||||
},
|
||||
"usageHint": "body"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "to-row",
|
||||
"component": {
|
||||
"Row": {
|
||||
"children": {
|
||||
"explicitList": ["to-label", "to-value"]
|
||||
},
|
||||
"gap": "medium",
|
||||
"alignment": "center"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "to-label",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"literalString": "TO"
|
||||
},
|
||||
"usageHint": "caption"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "to-value",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"path": "/to"
|
||||
},
|
||||
"usageHint": "body"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "subject-row",
|
||||
"component": {
|
||||
"Row": {
|
||||
"children": {
|
||||
"explicitList": ["subject-label", "subject-value"]
|
||||
},
|
||||
"gap": "medium",
|
||||
"alignment": "center"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "subject-label",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"literalString": "SUBJECT"
|
||||
},
|
||||
"usageHint": "caption"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "subject-value",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"path": "/subject"
|
||||
},
|
||||
"usageHint": "body"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "divider",
|
||||
"component": {
|
||||
"Divider": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "message",
|
||||
"component": {
|
||||
"Column": {
|
||||
"children": {
|
||||
"explicitList": ["greeting", "body-text", "closing", "signature"]
|
||||
},
|
||||
"gap": "small"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "greeting",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"path": "/greeting"
|
||||
},
|
||||
"usageHint": "body"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "body-text",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"path": "/body"
|
||||
},
|
||||
"usageHint": "body"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "closing",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"path": "/closing"
|
||||
},
|
||||
"usageHint": "body"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "signature",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"path": "/signature"
|
||||
},
|
||||
"usageHint": "body"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "actions",
|
||||
"component": {
|
||||
"Row": {
|
||||
"children": {
|
||||
"explicitList": ["send-btn", "discard-btn"]
|
||||
},
|
||||
"gap": "small"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "send-btn-text",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"literalString": "Send email"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "send-btn",
|
||||
"component": {
|
||||
"Button": {
|
||||
"child": "send-btn-text",
|
||||
"action": "send"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "discard-btn-text",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": {
|
||||
"literalString": "Discard"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "discard-btn",
|
||||
"component": {
|
||||
"Button": {
|
||||
"child": "discard-btn-text",
|
||||
"action": "discard"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"from": "alex@acme.com",
|
||||
"to": "jordan@acme.com",
|
||||
"subject": "Q4 Revenue Forecast",
|
||||
"greeting": "Hi Jordan,",
|
||||
"body": "Following up on our call. Please review the attached Q4 forecast and let me know if you have questions before the board meeting.",
|
||||
"closing": "Best,",
|
||||
"signature": "Alex"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Agent Spec helpers and server tools.
|
||||
|
||||
This module provides utilities to:
|
||||
- Load the Agent Spec JSON used by the backend
|
||||
- Define server-side tool implementations
|
||||
- Build an AgentSpecAgent configured with the JSON spec and tools
|
||||
|
||||
It is used by the FastAPI app in main.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ag_ui_agentspec.agent import AgentSpecAgent
|
||||
|
||||
|
||||
def build_a2ui_chat_agent(runtime: str = "langgraph") -> AgentSpecAgent:
|
||||
from a2ui_agentspec_agent import a2ui_demo_tool_registry, a2ui_chat_json
|
||||
|
||||
return AgentSpecAgent(
|
||||
agent_spec_config=a2ui_chat_json,
|
||||
runtime=runtime,
|
||||
tool_registry=a2ui_demo_tool_registry,
|
||||
)
|
||||
|
||||
|
||||
def build_agentspec_agent(runtime: str = "langgraph") -> AgentSpecAgent:
|
||||
"""Create an AgentSpecAgent configured from the JSON spec and server tools."""
|
||||
from agentspec_agent import get_weather, with_agentspec_agent_json
|
||||
|
||||
return AgentSpecAgent(
|
||||
agent_spec_config=with_agentspec_agent_json,
|
||||
runtime=runtime,
|
||||
tool_registry={"get_weather": get_weather},
|
||||
)
|
||||
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
|
||||
import dotenv
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
from pyagentspec.agent import Agent
|
||||
from pyagentspec.llms import OpenAiCompatibleConfig
|
||||
from pyagentspec.tools import ServerTool, ClientTool
|
||||
from pyagentspec.property import Property
|
||||
from pyagentspec.serialization import AgentSpecSerializer
|
||||
|
||||
|
||||
def get_weather(location: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the weather for a given location.
|
||||
"""
|
||||
import time
|
||||
|
||||
time.sleep(1) # simulates real tool execution
|
||||
return {
|
||||
"temperature": 20,
|
||||
"conditions": "sunny",
|
||||
"humidity": 50,
|
||||
"wind_speed": 10,
|
||||
"feelsLike": 25,
|
||||
}
|
||||
|
||||
|
||||
weather_tool = ServerTool(
|
||||
name="get_weather",
|
||||
description="Get the weather for a given location.",
|
||||
inputs=[
|
||||
Property(
|
||||
title="location",
|
||||
json_schema={
|
||||
"title": "location",
|
||||
"type": "string",
|
||||
"description": "The location to get the weather forecast. Must be a city/town name.",
|
||||
},
|
||||
)
|
||||
],
|
||||
outputs=[
|
||||
Property(
|
||||
title="weather_result",
|
||||
json_schema={"title": "weather_result", "type": "string"},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
go_to_moon_tool = ClientTool(
|
||||
name="go_to_moon",
|
||||
description="Go to the moon on request.",
|
||||
)
|
||||
|
||||
setThemeColor_tool = ClientTool(
|
||||
name="setThemeColor",
|
||||
description="Change the theme color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.",
|
||||
inputs=[
|
||||
Property(
|
||||
title="themeColor",
|
||||
json_schema={
|
||||
"title": "themeColor",
|
||||
"type": "string",
|
||||
"description": "The theme color to set. Make sure to pick nice colors.",
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
with_agentspec_agent = Agent(
|
||||
name="AgentSpecAgent",
|
||||
description="A starter Agent that can call tools.",
|
||||
system_prompt="You are a helpful assistant, named Specky, that has access to some tools.",
|
||||
llm_config=OpenAiCompatibleConfig(
|
||||
name="with-agentspec-agent",
|
||||
model_id=os.getenv("OPENAI_MODEL", "gpt-4o"),
|
||||
url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"),
|
||||
),
|
||||
tools=[weather_tool, go_to_moon_tool, setThemeColor_tool],
|
||||
)
|
||||
|
||||
|
||||
with_agentspec_agent_json = AgentSpecSerializer().to_json(with_agentspec_agent)
|
||||
@@ -0,0 +1,40 @@
|
||||
import logging
|
||||
|
||||
|
||||
def configure_logging() -> None:
|
||||
"""Enable INFO logs for ag_ui_agentspec and pyagentspec and attach a console handler.
|
||||
|
||||
Uvicorn's default logging config doesn't automatically show 3rd‑party logger output.
|
||||
We install a root handler and set levels explicitly so logs appear in the terminal.
|
||||
"""
|
||||
root = logging.getLogger()
|
||||
if not root.handlers:
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(
|
||||
logging.Formatter("%(asctime)s | %(levelname)s | %(name)s | %(message)s")
|
||||
)
|
||||
root.addHandler(handler)
|
||||
root.setLevel(logging.INFO)
|
||||
for h in root.handlers:
|
||||
try:
|
||||
h.setLevel(logging.INFO)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Turn on INFO for relevant namespaces and propagate to root
|
||||
for name in (
|
||||
"ag_ui_agentspec",
|
||||
"ag_ui_agentspec.endpoint",
|
||||
"ag_ui_agentspec.tracing",
|
||||
"pyagentspec",
|
||||
"wayflowcore",
|
||||
):
|
||||
lg = logging.getLogger(name)
|
||||
lg.setLevel(logging.INFO)
|
||||
lg.propagate = True
|
||||
|
||||
# Also make uvicorn loggers propagate to our root handler
|
||||
for name in ("uvicorn", "uvicorn.error", "uvicorn.access"):
|
||||
lg = logging.getLogger(name)
|
||||
lg.setLevel(logging.INFO)
|
||||
lg.propagate = True
|
||||
@@ -0,0 +1,35 @@
|
||||
import os
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
|
||||
from agent import build_a2ui_chat_agent
|
||||
from logging_utils import configure_logging
|
||||
|
||||
from ag_ui_agentspec.endpoint import add_agentspec_fastapi_endpoint
|
||||
|
||||
|
||||
def build_server() -> FastAPI:
|
||||
configure_logging()
|
||||
app = FastAPI(title="Agent Spec Agent")
|
||||
agent = build_a2ui_chat_agent(runtime="wayflow")
|
||||
add_agentspec_fastapi_endpoint(app, agent, path="/")
|
||||
return app
|
||||
|
||||
|
||||
app = build_server()
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=int(os.getenv("PORT", "8000")),
|
||||
reload=True,
|
||||
log_level="info",
|
||||
log_config=None, # use our logging config below
|
||||
)
|
||||
Reference in New Issue
Block a user