diff --git a/samples/agent/adk/enterprise_dashboard/README.md b/samples/agent/adk/enterprise_dashboard/README.md new file mode 100644 index 000000000..222cad611 --- /dev/null +++ b/samples/agent/adk/enterprise_dashboard/README.md @@ -0,0 +1,62 @@ +# Enterprise Dashboard Sample + +A visual-first analytics agent that renders business data as compact A2UI +component layouts instead of markdown text walls. + +## Why this sample exists + +LLMs default to markdown when presenting data — tables, bullet lists, headers. +This produces verbose "walls of text" that are hard to scan and don't leverage +A2UI's component system. This sample demonstrates the prompt engineering +patterns that produce compact, information-dense visual layouts. + +## Layout Patterns + +| Data Type | Markdown (bad) | A2UI (good) | +|-----------|---------------|-------------| +| KPI metrics | Inline numbers in paragraphs | Row of Card components with bold Text | +| Comparisons | Markdown table | Row of Card components side-by-side | +| Rankings | Numbered list | List with Card children | +| Multi-section | Multiple markdown headers | Tabs component | +| Separators | `---` or blank lines | Divider component | +| Status | Text like "up" or "down" | Icon (trending_up, trending_down) | + +## Key prompt techniques + +1. **Anti-markdown rules** — Explicit bans with A2UI alternatives: + "NEVER use markdown tables — use Row + Card instead." + +2. **Layout recipes** — Map each data type to a component composition: + "KPI metrics: Row of Card components, each with bold metric Text." + +3. **Output ordering** — A2UI JSON first, brief text after: + "Output A2UI JSON block(s) FIRST, then at most 1-2 sentences." + +4. **Component diversity** — Prevent monotonous layouts: + "Minimum 3 different component types per response." + +## Running + +```bash +cd samples/agent/adk +adk web enterprise_dashboard +``` + +Then try these queries: +- "Show me this week's KPIs" +- "Compare store performance" +- "What are the top 5 products?" +- "Give me a full dashboard with KPIs, store comparison, and top products" + +## Example output + +See `examples/0.8/` for reference A2UI JSON payloads: +- `kpi_dashboard.json` — 4 KPI metric cards in a Row layout +- `store_comparison.json` — 3 store cards with Icon status indicators + +## Related PRs + +- [#1465](https://github.com/google/A2UI/pull/1465) — `strict_output` mode + for `generate_system_prompt()` (SDK-level enforcement) +- [#1466](https://github.com/google/A2UI/pull/1466) — `A2UIOutputMode` enum + for unified TEXT/TOOL prompt generation diff --git a/samples/agent/adk/enterprise_dashboard/__init__.py b/samples/agent/adk/enterprise_dashboard/__init__.py new file mode 100644 index 000000000..708cf4c58 --- /dev/null +++ b/samples/agent/adk/enterprise_dashboard/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from . import agent diff --git a/samples/agent/adk/enterprise_dashboard/agent.py b/samples/agent/adk/enterprise_dashboard/agent.py new file mode 100644 index 000000000..d65dfee1f --- /dev/null +++ b/samples/agent/adk/enterprise_dashboard/agent.py @@ -0,0 +1,39 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Enterprise analytics dashboard agent. + +Demonstrates compact, visual-first A2UI output patterns for data-dense +enterprise use cases. Run with: adk web +""" + +import os + +from google.adk.agents.llm_agent import LlmAgent +from google.adk.models import Gemini +from .prompt_builder import get_dashboard_prompt +from .tools import get_kpi_summary, get_store_comparison, get_product_rankings + +model_name = os.getenv("MODEL_NAME", "gemini-2.0-flash") + +root_agent = LlmAgent( + model=Gemini(model=model_name), + name="enterprise_dashboard", + description=( + "Visual-first analytics dashboard that renders business data as " + "compact A2UI component layouts instead of text." + ), + instruction=get_dashboard_prompt(), + tools=[get_kpi_summary, get_store_comparison, get_product_rankings], +) diff --git a/samples/agent/adk/enterprise_dashboard/examples/0.8/kpi_dashboard.json b/samples/agent/adk/enterprise_dashboard/examples/0.8/kpi_dashboard.json new file mode 100644 index 000000000..793f0a621 --- /dev/null +++ b/samples/agent/adk/enterprise_dashboard/examples/0.8/kpi_dashboard.json @@ -0,0 +1,165 @@ +[ + { + "beginRendering": { + "surfaceId": "kpi-dashboard", + "root": "root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "kpi-dashboard", + "components": [ + { + "id": "root", + "component": { + "Column": { + "children": { + "explicitList": ["header", "metrics-row", "divider", "summary"] + } + } + } + }, + { + "id": "header", + "component": { + "Row": { + "children": { + "explicitList": ["header-icon", "header-title"] + }, + "alignment": "center" + } + } + }, + { + "id": "header-icon", + "component": { + "Icon": { + "name": { + "literalString": "star" + } + } + } + }, + { + "id": "header-title", + "component": { + "Text": { + "text": { + "literalString": "**Weekly Performance Dashboard**" + }, + "usageHint": "h2" + } + } + }, + { + "id": "metrics-row", + "component": { + "Row": { + "children": { + "explicitList": ["revenue-card", "txn-card", "aov-card", "csat-card"] + } + } + } + }, + { + "id": "revenue-card", + "component": { + "Card": { + "child": "revenue-text" + } + } + }, + { + "id": "revenue-text", + "component": { + "Text": { + "text": { + "literalString": "**Revenue**\n\n**$284,500**\n\n+8.3% vs last week" + } + } + } + }, + { + "id": "txn-card", + "component": { + "Card": { + "child": "txn-text" + } + } + }, + { + "id": "txn-text", + "component": { + "Text": { + "text": { + "literalString": "**Transactions**\n\n**12,450**\n\n+5.1% vs last week" + } + } + } + }, + { + "id": "aov-card", + "component": { + "Card": { + "child": "aov-text" + } + } + }, + { + "id": "aov-text", + "component": { + "Text": { + "text": { + "literalString": "**Avg Order**\n\n**$22.85**\n\n-1.2% vs last week" + } + } + } + }, + { + "id": "csat-card", + "component": { + "Card": { + "child": "csat-text" + } + } + }, + { + "id": "csat-text", + "component": { + "Text": { + "text": { + "literalString": "**Satisfaction**\n\n**4.6/5**\n\n+0.2 vs last week" + } + } + } + }, + { + "id": "divider", + "component": { + "Divider": { + "axis": "horizontal" + } + } + }, + { + "id": "summary", + "component": { + "Card": { + "child": "summary-text" + } + } + }, + { + "id": "summary-text", + "component": { + "Text": { + "text": { + "literalString": "**Top Store:** Downtown Market ($112,400) · **Top Product:** Organic Apples (1,240 units)" + } + } + } + } + ] + } + } +] diff --git a/samples/agent/adk/enterprise_dashboard/examples/0.8/store_comparison.json b/samples/agent/adk/enterprise_dashboard/examples/0.8/store_comparison.json new file mode 100644 index 000000000..4190f5d23 --- /dev/null +++ b/samples/agent/adk/enterprise_dashboard/examples/0.8/store_comparison.json @@ -0,0 +1,282 @@ +[ + { + "beginRendering": { + "surfaceId": "store-comparison", + "root": "root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "store-comparison", + "components": [ + { + "id": "root", + "component": { + "Column": { + "children": { + "explicitList": ["title", "stores-row", "divider", "insight"] + } + } + } + }, + { + "id": "title", + "component": { + "Text": { + "text": { + "literalString": "**Store Performance Comparison**" + }, + "usageHint": "h2" + } + } + }, + { + "id": "stores-row", + "component": { + "Row": { + "children": { + "explicitList": ["store-1", "store-2", "store-3"] + } + } + } + }, + { + "id": "store-1", + "component": { + "Card": { + "child": "s1-col" + } + } + }, + { + "id": "s1-col", + "component": { + "Column": { + "children": { + "explicitList": ["s1-icon-row", "s1-rev", "s1-detail"] + } + } + } + }, + { + "id": "s1-icon-row", + "component": { + "Row": { + "children": { + "explicitList": ["s1-icon", "s1-name"] + }, + "alignment": "center" + } + } + }, + { + "id": "s1-icon", + "component": { + "Icon": { + "name": { + "literalString": "trending_up" + } + } + } + }, + { + "id": "s1-name", + "component": { + "Text": { + "text": { + "literalString": "**Downtown Market**" + }, + "usageHint": "h3" + } + } + }, + { + "id": "s1-rev", + "component": { + "Text": { + "text": { + "literalString": "Revenue: **$112,400** (+12.3%)" + } + } + } + }, + { + "id": "s1-detail", + "component": { + "Text": { + "text": { + "literalString": "5,100 transactions · $22.04 avg basket" + } + } + } + }, + { + "id": "store-2", + "component": { + "Card": { + "child": "s2-col" + } + } + }, + { + "id": "s2-col", + "component": { + "Column": { + "children": { + "explicitList": ["s2-icon-row", "s2-rev", "s2-detail"] + } + } + } + }, + { + "id": "s2-icon-row", + "component": { + "Row": { + "children": { + "explicitList": ["s2-icon", "s2-name"] + }, + "alignment": "center" + } + } + }, + { + "id": "s2-icon", + "component": { + "Icon": { + "name": { + "literalString": "trending_up" + } + } + } + }, + { + "id": "s2-name", + "component": { + "Text": { + "text": { + "literalString": "**Westside Market**" + }, + "usageHint": "h3" + } + } + }, + { + "id": "s2-rev", + "component": { + "Text": { + "text": { + "literalString": "Revenue: **$95,200** (+6.1%)" + } + } + } + }, + { + "id": "s2-detail", + "component": { + "Text": { + "text": { + "literalString": "4,200 transactions · $22.67 avg basket" + } + } + } + }, + { + "id": "store-3", + "component": { + "Card": { + "child": "s3-col" + } + } + }, + { + "id": "s3-col", + "component": { + "Column": { + "children": { + "explicitList": ["s3-icon-row", "s3-rev", "s3-detail"] + } + } + } + }, + { + "id": "s3-icon-row", + "component": { + "Row": { + "children": { + "explicitList": ["s3-icon", "s3-name"] + }, + "alignment": "center" + } + } + }, + { + "id": "s3-icon", + "component": { + "Icon": { + "name": { + "literalString": "trending_up" + } + } + } + }, + { + "id": "s3-name", + "component": { + "Text": { + "text": { + "literalString": "**Lakefront Market**" + }, + "usageHint": "h3" + } + } + }, + { + "id": "s3-rev", + "component": { + "Text": { + "text": { + "literalString": "Revenue: **$76,900** (+3.8%)" + } + } + } + }, + { + "id": "s3-detail", + "component": { + "Text": { + "text": { + "literalString": "3,150 transactions · $24.41 avg basket" + } + } + } + }, + { + "id": "divider", + "component": { + "Divider": { + "axis": "horizontal" + } + } + }, + { + "id": "insight", + "component": { + "Card": { + "child": "insight-text" + } + } + }, + { + "id": "insight-text", + "component": { + "Text": { + "text": { + "literalString": "Downtown leads by **$17,200** (+18.1%) · Lakefront has the highest basket value at **$24.41**" + } + } + } + } + ] + } + } +] diff --git a/samples/agent/adk/enterprise_dashboard/prompt_builder.py b/samples/agent/adk/enterprise_dashboard/prompt_builder.py new file mode 100644 index 000000000..cd13e4ce1 --- /dev/null +++ b/samples/agent/adk/enterprise_dashboard/prompt_builder.py @@ -0,0 +1,86 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Prompt construction for the enterprise dashboard sample. + +Demonstrates how to build compact, visual-first system prompts +that prevent the LLM from falling back to markdown "walls of text." + +Key patterns: + - Anti-markdown rules with A2UI component alternatives + - Layout recipes mapping data types to component compositions + - Output ordering (A2UI JSON first, brief text after) + - Component diversity requirements +""" + +from a2ui.schema.constants import VERSION_0_8 +from a2ui.schema.manager import A2uiSchemaManager +from a2ui.basic_catalog.provider import BasicCatalog + +ROLE_DESCRIPTION = ( + "You are a visual-first enterprise analytics dashboard. You render " + "all business data as compact A2UI component layouts. You NEVER " + "respond with plain text or markdown — every response contains " + "an A2UI JSON block as its primary content." +) + +UI_DESCRIPTION = """ +You are a VISUAL DASHBOARD, not a text chatbot. + +Layout Recipes (use the right pattern for each data type): +- KPI metrics: Row of Card components, each Card wrapping a Text child + with bold metric name, large value, and trend indicator. +- Store/product comparisons: Row of Card components side-by-side. + NEVER use markdown tables. +- Rankings and lists: List with Card children, each showing rank + + name + key metric. +- Multi-section responses: Tabs component organizing different views + (e.g., Revenue tab, Products tab, Stores tab). +- Section separators: Divider between logical groups. +- Status indicators: Icon components (trending_up, trending_down, + star, warning, check_circle). + +Output Rules: +- Output A2UI JSON block(s) FIRST, then at most 1-2 sentences. +- NEVER use markdown tables — use Row + Card instead. +- NEVER use markdown bullet/numbered lists — use List + Card instead. +- NEVER use markdown headers (##) as dividers — use Divider or + Text with usageHint "h2". +- Minimum 3 different component types per response. +- Wrap data in Card components — no bare Text at root level. +- Use bold (**text**) inside Text components for emphasis. +""" + + +def get_dashboard_prompt() -> str: + """Constructs the full system prompt with A2UI schema.""" + schema_manager = A2uiSchemaManager( + version=VERSION_0_8, + catalogs=[BasicCatalog.get_config( + version=VERSION_0_8, + examples_path="examples/0.8", + )], + ) + return schema_manager.generate_system_prompt( + role_description=ROLE_DESCRIPTION, + ui_description=UI_DESCRIPTION, + include_schema=True, + include_examples=True, + ) + + +if __name__ == "__main__": + prompt = get_dashboard_prompt() + print(prompt) + print(f"\nPrompt length: {len(prompt)} characters") diff --git a/samples/agent/adk/enterprise_dashboard/tools.py b/samples/agent/adk/enterprise_dashboard/tools.py new file mode 100644 index 000000000..e88ba9566 --- /dev/null +++ b/samples/agent/adk/enterprise_dashboard/tools.py @@ -0,0 +1,165 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Mock analytics tools for the enterprise dashboard sample.""" + +import json + + +def get_kpi_summary(period: str = "weekly") -> str: + """Returns key performance indicators for the requested time period. + + Args: + period: Time period for the KPIs. One of 'daily', 'weekly', 'monthly'. + + Returns: + JSON string with KPI metrics including revenue, transactions, + average order value, and customer satisfaction. + """ + data = { + "period": period, + "metrics": [ + { + "name": "Revenue", + "value": "$284,500", + "change": "+8.3%", + "trend": "up", + }, + { + "name": "Transactions", + "value": "12,450", + "change": "+5.1%", + "trend": "up", + }, + { + "name": "Avg Order Value", + "value": "$22.85", + "change": "-1.2%", + "trend": "down", + }, + { + "name": "Customer Satisfaction", + "value": "4.6/5", + "change": "+0.2", + "trend": "up", + }, + ], + } + return json.dumps(data) + + +def get_store_comparison() -> str: + """Returns performance comparison data across all store locations. + + Returns: + JSON string with per-store revenue, transactions, average basket + size, and growth metrics. + """ + data = { + "stores": [ + { + "name": "Downtown Market", + "revenue": "$112,400", + "transactions": 5100, + "avg_basket": "$22.04", + "growth": "+12.3%", + }, + { + "name": "Westside Market", + "revenue": "$95,200", + "transactions": 4200, + "avg_basket": "$22.67", + "growth": "+6.1%", + }, + { + "name": "Lakefront Market", + "revenue": "$76,900", + "transactions": 3150, + "avg_basket": "$24.41", + "growth": "+3.8%", + }, + ], + } + return json.dumps(data) + + +def get_product_rankings(limit: int = 5) -> str: + """Returns top-performing products ranked by weekly unit sales. + + Args: + limit: Maximum number of products to return. + + Returns: + JSON string with ranked products including name, category, + unit sales, revenue, and week-over-week growth. + """ + products = [ + { + "rank": 1, + "name": "Organic Apples", + "category": "Produce", + "units_sold": 1240, + "revenue": "$3,720", + "growth": "+12.4%", + }, + { + "rank": 2, + "name": "Whole Milk 1gal", + "category": "Dairy", + "units_sold": 1180, + "revenue": "$5,310", + "growth": "+3.2%", + }, + { + "rank": 3, + "name": "Sourdough Bread", + "category": "Bakery", + "units_sold": 980, + "revenue": "$4,900", + "growth": "+8.7%", + }, + { + "rank": 4, + "name": "Free Range Eggs", + "category": "Dairy", + "units_sold": 920, + "revenue": "$5,520", + "growth": "+1.5%", + }, + { + "rank": 5, + "name": "Avocados", + "category": "Produce", + "units_sold": 870, + "revenue": "$2,610", + "growth": "-2.1%", + }, + { + "rank": 6, + "name": "Greek Yogurt", + "category": "Dairy", + "units_sold": 810, + "revenue": "$4,050", + "growth": "+5.6%", + }, + { + "rank": 7, + "name": "Chicken Breast", + "category": "Meat", + "units_sold": 750, + "revenue": "$6,750", + "growth": "+2.8%", + }, + ] + return json.dumps({"products": products[:limit]})