chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1,216 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# poetry.lock
# poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
# pdm.lock
# pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
# pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# Redis
*.rdb
*.aof
*.pid
# RabbitMQ
mnesia/
rabbitmq/
rabbitmq-data/
# ActiveMQ
activemq-data/
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
# .idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Streamlit
.streamlit/secrets.toml
@@ -0,0 +1,37 @@
# A2UI Restaurant finder and table reservation agent sample.
This sample uses the Agent Development Kit (ADK) along with the A2A protocol to create a simple "Restaurant finder and table reservation" agent that is hosted as an A2A server.
## Prerequisites
- Python 3.9 or higher
- [UV](https://docs.astral.sh/uv/)
- Access to an LLM and API Key
## Running the Sample
1. Navigate to the samples directory:
```bash
cd a2a_samples/a2ui_restaurant_finder
```
2. Create an environment file with your API key:
```bash
echo "GEMINI_API_KEY=your_api_key_here" > .env
```
3. Run the agent server:
```bash
uv run .
```
## Disclaimer
Important: The sample code provided is for demonstration purposes and illustrates the mechanics of the Agent-to-Agent (A2A) protocol. When building production applications, it is critical to treat any agent operating outside of your direct control as a potentially untrusted entity.
All data received from an external agent—including but not limited to its AgentCard, messages, artifacts, and task statuses—should be handled as untrusted input. For example, a malicious agent could provide an AgentCard containing crafted data in its fields (e.g., description, name, skills.description). If this data is used without sanitization to construct prompts for a Large Language Model (LLM), it could expose your application to prompt injection attacks. Failure to properly validate and sanitize this data before use can introduce security vulnerabilities into your application.
Developers are responsible for implementing appropriate security measures, such as input validation and secure handling of credentials to protect their systems and users.
@@ -0,0 +1,15 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import agent
@@ -0,0 +1,113 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import os
import click
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
from a2ui.a2ui_extension import get_a2ui_agent_extension
from agent import RestaurantAgent
from agent_executor import RestaurantAgentExecutor
from dotenv import load_dotenv
from starlette.middleware.cors import CORSMiddleware
from starlette.staticfiles import StaticFiles
load_dotenv()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class MissingAPIKeyError(Exception):
"""Exception for missing API key."""
@click.command()
@click.option("--host", default="localhost")
@click.option("--port", default=10002)
def main(host, port):
try:
# Check for API key only if Vertex AI is not configured
if not os.getenv("GOOGLE_GENAI_USE_VERTEXAI") == "TRUE":
if not os.getenv("GEMINI_API_KEY"):
raise MissingAPIKeyError(
"GEMINI_API_KEY environment variable not set and GOOGLE_GENAI_USE_VERTEXAI is not TRUE."
)
capabilities = AgentCapabilities(
streaming=True,
extensions=[get_a2ui_agent_extension()],
)
skill = AgentSkill(
id="find_restaurants",
name="Find Restaurants Tool",
description="Helps find restaurants based on user criteria (e.g., cuisine, location).",
tags=["restaurant", "finder"],
examples=["Find me the top 10 chinese restaurants in the US"],
)
base_url = f"http://{host}:{port}"
agent_card = AgentCard(
name="Restaurant Agent",
description="This agent helps find restaurants based on user criteria.",
url=base_url, # <-- Use base_url here
version="1.0.0",
default_input_modes=RestaurantAgent.SUPPORTED_CONTENT_TYPES,
default_output_modes=RestaurantAgent.SUPPORTED_CONTENT_TYPES,
capabilities=capabilities,
skills=[skill],
)
agent_executor = RestaurantAgentExecutor(base_url=base_url)
request_handler = DefaultRequestHandler(
agent_executor=agent_executor,
task_store=InMemoryTaskStore(),
)
server = A2AStarletteApplication(
agent_card=agent_card, http_handler=request_handler
)
import uvicorn
app = server.build()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Use absolute path based on this script's location
script_dir = os.path.dirname(os.path.abspath(__file__))
images_dir = os.path.join(script_dir, "images")
app.mount("/static", StaticFiles(directory=images_dir), name="static")
uvicorn.run(app, host=host, port=port)
except MissingAPIKeyError as e:
logger.error(f"Error: {e}")
exit(1)
except Exception as e:
logger.error(f"An error occurred during server startup: {e}")
exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,303 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import logging
import os
from collections.abc import AsyncIterable
from typing import Any
import jsonschema
from google.adk.agents.llm_agent import LlmAgent
from google.adk.artifacts import InMemoryArtifactService
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
from google.adk.models.lite_llm import LiteLlm
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
from prompt_builder import (
A2UI_SCHEMA,
RESTAURANT_UI_EXAMPLES,
get_text_prompt,
get_ui_prompt,
)
from tools import get_restaurants
logger = logging.getLogger(__name__)
AGENT_INSTRUCTION = """
You are a helpful restaurant finding assistant. Your goal is to help users find and book restaurants using a rich UI.
To achieve this, you MUST follow this logic:
1. **For finding restaurants:**
a. You MUST call the `get_restaurants` tool. Extract the cuisine, location, and a specific number (`count`) of restaurants from the user's query (e.g., for "top 5 chinese places", count is 5).
b. After receiving the data, you MUST follow the instructions precisely to generate the final a2ui UI JSON, using the appropriate UI example from the `prompt_builder.py` based on the number of restaurants.
2. **For booking a table (when you receive a query like 'USER_WANTS_TO_BOOK...'):**
a. You MUST use the appropriate UI example from `prompt_builder.py` to generate the UI, populating the `dataModelUpdate.contents` with the details from the user's query.
3. **For confirming a booking (when you receive a query like 'User submitted a booking...'):**
a. You MUST use the appropriate UI example from `prompt_builder.py` to generate the confirmation UI, populating the `dataModelUpdate.contents` with the final booking details.
"""
class RestaurantAgent:
"""An agent that finds restaurants based on user criteria."""
SUPPORTED_CONTENT_TYPES = ["text", "text/plain"]
def __init__(self, base_url: str, use_ui: bool = False):
self.base_url = base_url
self.use_ui = use_ui
self._agent = self._build_agent(use_ui)
self._user_id = "remote_agent"
self._runner = Runner(
app_name=self._agent.name,
agent=self._agent,
artifact_service=InMemoryArtifactService(),
session_service=InMemorySessionService(),
memory_service=InMemoryMemoryService(),
)
# --- MODIFICATION: Wrap the schema ---
# Load the A2UI_SCHEMA string into a Python object for validation
try:
# First, load the schema for a *single message*
single_message_schema = json.loads(A2UI_SCHEMA)
# The prompt instructs the LLM to return a *list* of messages.
# Therefore, our validation schema must be an *array* of the single message schema.
self.a2ui_schema_object = {"type": "array", "items": single_message_schema}
logger.info(
"A2UI_SCHEMA successfully loaded and wrapped in an array validator."
)
except json.JSONDecodeError as e:
logger.error(f"CRITICAL: Failed to parse A2UI_SCHEMA: {e}")
self.a2ui_schema_object = None
# --- END MODIFICATION ---
def get_processing_message(self) -> str:
return "Finding restaurants that match your criteria..."
def _build_agent(self, use_ui: bool) -> LlmAgent:
"""Builds the LLM agent for the restaurant agent."""
LITELLM_MODEL = os.getenv("LITELLM_MODEL", "gemini/gemini-2.5-flash")
if use_ui:
# Construct the full prompt with UI instructions, examples, and schema
instruction = AGENT_INSTRUCTION + get_ui_prompt(
self.base_url, RESTAURANT_UI_EXAMPLES
)
else:
instruction = get_text_prompt()
return LlmAgent(
model=LiteLlm(model=LITELLM_MODEL),
name="restaurant_agent",
description="An agent that finds restaurants and helps book tables.",
instruction=instruction,
tools=[get_restaurants],
)
async def stream(self, query, session_id) -> AsyncIterable[dict[str, Any]]:
session_state = {"base_url": self.base_url}
session = await self._runner.session_service.get_session(
app_name=self._agent.name,
user_id=self._user_id,
session_id=session_id,
)
if session is None:
session = await self._runner.session_service.create_session(
app_name=self._agent.name,
user_id=self._user_id,
state=session_state,
session_id=session_id,
)
elif "base_url" not in session.state:
session.state["base_url"] = self.base_url
# --- Begin: UI Validation and Retry Logic ---
max_retries = 1 # Total 2 attempts
attempt = 0
current_query_text = query
# Ensure schema was loaded
if self.use_ui and self.a2ui_schema_object is None:
logger.error(
"--- RestaurantAgent.stream: A2UI_SCHEMA is not loaded. "
"Cannot perform UI validation. ---"
)
yield {
"is_task_complete": True,
"content": (
"I'm sorry, I'm facing an internal configuration error with my UI components. "
"Please contact support."
),
}
return
while attempt <= max_retries:
attempt += 1
logger.info(
f"--- RestaurantAgent.stream: Attempt {attempt}/{max_retries + 1} "
f"for session {session_id} ---"
)
current_message = types.Content(
role="user", parts=[types.Part.from_text(text=current_query_text)]
)
final_response_content = None
async for event in self._runner.run_async(
user_id=self._user_id,
session_id=session.id,
new_message=current_message,
):
logger.info(f"Event from runner: {event}")
if event.is_final_response():
if (
event.content
and event.content.parts
and event.content.parts[0].text
):
final_response_content = "\n".join(
[p.text for p in event.content.parts if p.text]
)
break # Got the final response, stop consuming events
else:
logger.info(f"Intermediate event: {event}")
# Yield intermediate updates on every attempt
yield {
"is_task_complete": False,
"updates": self.get_processing_message(),
}
if final_response_content is None:
logger.warning(
f"--- RestaurantAgent.stream: Received no final response content from runner "
f"(Attempt {attempt}). ---"
)
if attempt <= max_retries:
current_query_text = (
"I received no response. Please try again."
f"Please retry the original request: '{query}'"
)
continue # Go to next retry
else:
# Retries exhausted on no-response
final_response_content = "I'm sorry, I encountered an error and couldn't process your request."
# Fall through to send this as a text-only error
is_valid = False
error_message = ""
if self.use_ui:
logger.info(
f"--- RestaurantAgent.stream: Validating UI response (Attempt {attempt})... ---"
)
try:
if "---a2ui_JSON---" not in final_response_content:
raise ValueError("Delimiter '---a2ui_JSON---' not found.")
text_part, json_string = final_response_content.split(
"---a2ui_JSON---", 1
)
if not json_string.strip():
raise ValueError("JSON part is empty.")
json_string_cleaned = (
json_string.strip().lstrip("```json").rstrip("```").strip()
)
if not json_string_cleaned:
raise ValueError("Cleaned JSON string is empty.")
# --- New Validation Steps ---
# 1. Check if it's parsable JSON
parsed_json_data = json.loads(json_string_cleaned)
# 2. Check if it validates against the A2UI_SCHEMA
# This will raise jsonschema.exceptions.ValidationError if it fails
logger.info(
"--- RestaurantAgent.stream: Validating against A2UI_SCHEMA... ---"
)
jsonschema.validate(
instance=parsed_json_data, schema=self.a2ui_schema_object
)
# --- End New Validation Steps ---
logger.info(
f"--- RestaurantAgent.stream: UI JSON successfully parsed AND validated against schema. "
f"Validation OK (Attempt {attempt}). ---"
)
is_valid = True
except (
ValueError,
json.JSONDecodeError,
jsonschema.exceptions.ValidationError,
) as e:
logger.warning(
f"--- RestaurantAgent.stream: A2UI validation failed: {e} (Attempt {attempt}) ---"
)
logger.warning(
f"--- Failed response content: {final_response_content[:500]}... ---"
)
error_message = f"Validation failed: {e}."
else: # Not using UI, so text is always "valid"
is_valid = True
if is_valid:
logger.info(
f"--- RestaurantAgent.stream: Response is valid. Sending final response (Attempt {attempt}). ---"
)
logger.info(f"Final response: {final_response_content}")
yield {
"is_task_complete": True,
"content": final_response_content,
}
return # We're done, exit the generator
# --- If we're here, it means validation failed ---
if attempt <= max_retries:
logger.warning(
f"--- RestaurantAgent.stream: Retrying... ({attempt}/{max_retries + 1}) ---"
)
# Prepare the query for the retry
current_query_text = (
f"Your previous response was invalid. {error_message} "
"You MUST generate a valid response that strictly follows the A2UI JSON SCHEMA. "
"The response MUST be a JSON list of A2UI messages. "
"Ensure the response is split by '---a2ui_JSON---' and the JSON part is well-formed. "
f"Please retry the original request: '{query}'"
)
# Loop continues...
# --- If we're here, it means we've exhausted retries ---
logger.error(
"--- RestaurantAgent.stream: Max retries exhausted. Sending text-only error. ---"
)
yield {
"is_task_complete": True,
"content": (
"I'm sorry, I'm having trouble generating the interface for that request right now. "
"Please try again in a moment."
),
}
# --- End: UI Validation and Retry Logic ---
@@ -0,0 +1,197 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import logging
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.tasks import TaskUpdater
from a2a.types import (
DataPart,
Part,
Task,
TaskState,
TextPart,
UnsupportedOperationError,
)
from a2a.utils import (
new_agent_parts_message,
new_agent_text_message,
new_task,
)
from a2a.utils.errors import ServerError
from a2ui.a2ui_extension import create_a2ui_part, try_activate_a2ui_extension
from agent import RestaurantAgent
logger = logging.getLogger(__name__)
class RestaurantAgentExecutor(AgentExecutor):
"""Restaurant AgentExecutor Example."""
def __init__(self, base_url: str):
# Instantiate two agents: one for UI and one for text-only.
# The appropriate one will be chosen at execution time.
self.ui_agent = RestaurantAgent(base_url=base_url, use_ui=True)
self.text_agent = RestaurantAgent(base_url=base_url, use_ui=False)
async def execute(
self,
context: RequestContext,
event_queue: EventQueue,
) -> None:
query = ""
ui_event_part = None
action = None
logger.info(
f"--- Client requested extensions: {context.requested_extensions} ---"
)
use_ui = try_activate_a2ui_extension(context)
# Determine which agent to use based on whether the a2ui extension is active.
if use_ui:
agent = self.ui_agent
logger.info(
"--- AGENT_EXECUTOR: A2UI extension is active. Using UI agent. ---"
)
else:
agent = self.text_agent
logger.info(
"--- AGENT_EXECUTOR: A2UI extension is not active. Using text agent. ---"
)
if context.message and context.message.parts:
logger.info(
f"--- AGENT_EXECUTOR: Processing {len(context.message.parts)} message parts ---"
)
for i, part in enumerate(context.message.parts):
if isinstance(part.root, DataPart):
if "userAction" in part.root.data:
logger.info(f" Part {i}: Found a2ui UI ClientEvent payload.")
ui_event_part = part.root.data["userAction"]
else:
logger.info(f" Part {i}: DataPart (data: {part.root.data})")
elif isinstance(part.root, TextPart):
logger.info(f" Part {i}: TextPart (text: {part.root.text})")
else:
logger.info(f" Part {i}: Unknown part type ({type(part.root)})")
if ui_event_part:
logger.info(f"Received a2ui ClientEvent: {ui_event_part}")
action = ui_event_part.get("actionName")
ctx = ui_event_part.get("context", {})
if action == "book_restaurant":
restaurant_name = ctx.get("restaurantName", "Unknown Restaurant")
address = ctx.get("address", "Address not provided")
image_url = ctx.get("imageUrl", "")
query = f"USER_WANTS_TO_BOOK: {restaurant_name}, Address: {address}, ImageURL: {image_url}"
elif action == "submit_booking":
restaurant_name = ctx.get("restaurantName", "Unknown Restaurant")
party_size = ctx.get("partySize", "Unknown Size")
reservation_time = ctx.get("reservationTime", "Unknown Time")
dietary_reqs = ctx.get("dietary", "None")
image_url = ctx.get("imageUrl", "")
query = f"User submitted a booking for {restaurant_name} for {party_size} people at {reservation_time} with dietary requirements: {dietary_reqs}. The image URL is {image_url}"
else:
query = f"User submitted an event: {action} with data: {ctx}"
else:
logger.info("No a2ui UI event part found. Falling back to text input.")
query = context.get_user_input()
logger.info(f"--- AGENT_EXECUTOR: Final query for LLM: '{query}' ---")
task = context.current_task
if not task:
task = new_task(context.message)
await event_queue.enqueue_event(task)
updater = TaskUpdater(event_queue, task.id, task.context_id)
async for item in agent.stream(query, task.context_id):
is_task_complete = item["is_task_complete"]
if not is_task_complete:
await updater.update_status(
TaskState.working,
new_agent_text_message(item["updates"], task.context_id, task.id),
)
continue
final_state = (
TaskState.completed
if action == "submit_booking"
else TaskState.input_required
)
content = item["content"]
final_parts = []
if "---a2ui_JSON---" in content:
logger.info("Splitting final response into text and UI parts.")
text_content, json_string = content.split("---a2ui_JSON---", 1)
if text_content.strip():
final_parts.append(Part(root=TextPart(text=text_content.strip())))
if json_string.strip():
try:
json_string_cleaned = (
json_string.strip().lstrip("```json").rstrip("```").strip()
)
# The new protocol sends a stream of JSON objects.
# For this example, we'll assume they are sent as a list in the final response.
json_data = json.loads(json_string_cleaned)
if isinstance(json_data, list):
logger.info(
f"Found {len(json_data)} messages. Creating individual DataParts."
)
for message in json_data:
final_parts.append(create_a2ui_part(message))
else:
# Handle the case where a single JSON object is returned
logger.info(
"Received a single JSON object. Creating a DataPart."
)
final_parts.append(create_a2ui_part(json_data))
except json.JSONDecodeError as e:
logger.error(f"Failed to parse UI JSON: {e}")
final_parts.append(Part(root=TextPart(text=json_string)))
else:
final_parts.append(Part(root=TextPart(text=content.strip())))
logger.info("--- FINAL PARTS TO BE SENT ---")
for i, part in enumerate(final_parts):
logger.info(f" - Part {i}: Type = {type(part.root)}")
if isinstance(part.root, TextPart):
logger.info(f" - Text: {part.root.text[:200]}...")
elif isinstance(part.root, DataPart):
logger.info(f" - Data: {str(part.root.data)[:200]}...")
logger.info("-----------------------------")
await updater.update_status(
final_state,
new_agent_parts_message(final_parts, task.context_id, task.id),
final=(final_state == TaskState.completed),
)
break
async def cancel(
self, request: RequestContext, event_queue: EventQueue
) -> Task | None:
raise ServerError(error=UnsupportedOperationError())
@@ -0,0 +1 @@
../../../../images/restaurant-food/Spaghetti Carbonara.png
@@ -0,0 +1 @@
../../../../images/restaurant-food/beefbroccoli.jpeg
@@ -0,0 +1 @@
../../../../images/restaurant-food/classic tiramasu.png
@@ -0,0 +1 @@
../../../../images/restaurant-food/italian restaurant 2.png
@@ -0,0 +1 @@
../../../../images/restaurant-food/italian restaurant1.png
@@ -0,0 +1 @@
../../../../images/restaurant-food/italian1.png
@@ -0,0 +1 @@
../../../../images/restaurant-food/italian2.png
@@ -0,0 +1 @@
../../../../images/restaurant-food/italian3.png
@@ -0,0 +1 @@
../../../../images/restaurant-food/kungpao.jpeg
@@ -0,0 +1 @@
../../../../images/restaurant-food/lasagna.png
+1
View File
@@ -0,0 +1 @@
../../../../images/restaurant-food/logo.png
@@ -0,0 +1 @@
../../../../images/restaurant-food/mapotofu.jpeg
@@ -0,0 +1 @@
../../../../images/restaurant-food/margharita.png
@@ -0,0 +1 @@
../../../../images/restaurant-food/saffron risotto.png
@@ -0,0 +1 @@
../../../../images/restaurant-food/shrimpchowmein.jpeg
@@ -0,0 +1 @@
../../../../images/restaurant-food/springrolls.jpeg
@@ -0,0 +1 @@
../../../../images/restaurant-food/sweetsourpork.jpeg
@@ -0,0 +1 @@
../../../../images/restaurant-food/vegfriedrice.jpeg
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
[project]
name = "a2ui-restaurant-finder"
version = "0.1.0"
description = "Sample Google ADK-based Restaurant finder agent that uses a2ui UI and is hosted as an A2A server agent."
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"a2a-sdk>=0.3.0",
"click>=8.1.8",
"google-adk>=1.8.0",
"google-genai>=1.27.0",
"python-dotenv>=1.1.0",
"litellm",
"a2ui",
"jsonschema>=4.0.0",
]
[tool.hatch.build.targets.wheel]
packages = ["."]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.uv.sources]
a2ui = { workspace = true }
@@ -0,0 +1,66 @@
[
{
"name": "Xi'an Famous Foods",
"detail": "Spicy and savory hand-pulled noodles.",
"imageUrl": "http://localhost:10002/static/shrimpchowmein.jpeg",
"rating": "★★★★☆",
"infoLink": "[More Info](https://www.xianfoods.com/)",
"address": "81 St Marks Pl, New York, NY 10003"
},
{
"name": "Han Dynasty",
"detail": "Authentic Szechuan cuisine.",
"imageUrl": "http://localhost:10002/static/mapotofu.jpeg",
"rating": "★★★★☆",
"infoLink": "[More Info](https://www.handynasty.net/)",
"address": "90 3rd Ave, New York, NY 10003"
},
{
"name": "RedFarm",
"detail": "Modern Chinese with a farm-to-table approach.",
"imageUrl": "http://localhost:10002/static/beefbroccoli.jpeg",
"rating": "★★★★☆",
"infoLink": "[More Info](https://www.redfarmnyc.com/)",
"address": "529 Hudson St, New York, NY 10014"
},
{
"name": "Mott 32",
"detail": "Upscale Cantonese dining.",
"imageUrl": "http://localhost:10002/static/springrolls.jpeg",
"rating": "★★★★★",
"infoLink": "[More Info](https://mott32.com/newyork/)",
"address": "111 W 57th St, New York, NY 10019"
},
{
"name": "Hwa Yuan Szechuan",
"detail": "Famous for its cold noodles with sesame sauce.",
"imageUrl": "http://localhost:10002/static/kungpao.jpeg",
"rating": "★★★★☆",
"infoLink": "[More Info](https://hwayuannyc.com/)",
"address": "40 E Broadway, New York, NY 10002"
},
{
"name": "Cafe China",
"detail": "Szechuan food in a 1930s Shanghai setting.",
"imageUrl": "http://localhost:10002/static/mapotofu.jpeg",
"rating": "★★★★☆",
"infoLink": "[More Info](https://www.cafechinanyc.com/)",
"address": "59 W 37th St, New York, NY 10018"
},
{
"name": "Philippe Chow",
"detail": "High-end Beijing-style cuisine.",
"imageUrl": "http://localhost:10002/static/beefbroccoli.jpeg",
"rating": "★★★★☆",
"infoLink": "[More Info](https://www.philippechow.com/)",
"address": "33 E 60th St, New York, NY 10022"
},
{
"name": "Chinese Tuxedo",
"detail": "Contemporary Chinese in a former opera house.",
"imageUrl": "http://localhost:10002/static/mapotofu.jpeg",
"rating": "★★★★☆",
"infoLink": "[More Info](https://chinesetuxedo.com/)",
"address": "5 Doyers St, New York, NY 10013"
}
]
@@ -0,0 +1,59 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import logging
import os
from google.adk.tools.tool_context import ToolContext
logger = logging.getLogger(__name__)
def get_restaurants(
cuisine: str, location: str, tool_context: ToolContext, count: int = 5
) -> str:
"""Call this tool to get a list of restaurants based on a cuisine and location.
'count' is the number of restaurants to return.
"""
logger.info(f"--- TOOL CALLED: get_restaurants (count: {count}) ---")
logger.info(f" - Cuisine: {cuisine}")
logger.info(f" - Location: {location}")
items = []
if "new york" in location.lower() or "ny" in location.lower():
try:
script_dir = os.path.dirname(__file__)
file_path = os.path.join(script_dir, "restaurant_data.json")
with open(file_path) as f:
restaurant_data_str = f.read()
if base_url := tool_context.state.get("base_url"):
restaurant_data_str = restaurant_data_str.replace(
"http://localhost:10002", base_url
)
logger.info(f"Updated base URL from tool context: {base_url}")
all_items = json.loads(restaurant_data_str)
# Slice the list to return only the requested number of items
items = all_items[:count]
logger.info(
f" - Success: Found {len(all_items)} restaurants, returning {len(items)}."
)
except FileNotFoundError:
logger.error(f" - Error: restaurant_data.json not found at {file_path}")
except json.JSONDecodeError:
logger.error(f" - Error: Failed to decode JSON from {file_path}")
return json.dumps(items)
File diff suppressed because it is too large Load Diff