fix minor typos

This commit is contained in:
trandangtrungduc
2025-04-18 11:02:10 +07:00
101 changed files with 2123 additions and 258 deletions
+10
View File
@@ -0,0 +1,10 @@
# Copy as .env file and fill your values below to run integration tests.
# Choose Backend: GOOGLE_AI_ONLY | VERTEX_ONLY | BOTH (default)
TEST_BACKEND=BOTH
# ML Dev backend config
GOOGLE_API_KEY=YOUR_VALUE_HERE
# Vertex backend config
GOOGLE_CLOUD_PROJECT=YOUR_VALUE_HERE
GOOGLE_CLOUD_LOCATION=YOUR_VALUE_HERE
+18
View File
@@ -0,0 +1,18 @@
# 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
#
# http://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 pytest
# This allows pytest to show the values of the asserts.
pytest.register_assert_rewrite('tests.integration.utils')
+119
View File
@@ -0,0 +1,119 @@
# 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
#
# http://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
from typing import Literal
import warnings
from dotenv import load_dotenv
from google.adk import Agent
from pytest import fixture
from pytest import FixtureRequest
from pytest import hookimpl
from pytest import Metafunc
from .utils import TestRunner
logger = logging.getLogger(__name__)
def load_env_for_tests():
dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
if not os.path.exists(dotenv_path):
warnings.warn(
f'Missing .env file at {dotenv_path}. See dotenv.sample for an example.'
)
else:
load_dotenv(dotenv_path, override=True, verbose=True)
if 'GOOGLE_API_KEY' not in os.environ:
warnings.warn(
'Missing GOOGLE_API_KEY in the environment variables. GOOGLE_AI backend'
' integration tests will fail.'
)
for env_var in [
'GOOGLE_CLOUD_PROJECT',
'GOOGLE_CLOUD_LOCATION',
]:
if env_var not in os.environ:
warnings.warn(
f'Missing {env_var} in the environment variables. Vertex backend'
' integration tests will fail.'
)
load_env_for_tests()
BackendType = Literal['GOOGLE_AI', 'VERTEX']
@fixture
def agent_runner(request: FixtureRequest) -> TestRunner:
assert isinstance(request.param, dict)
if 'agent' in request.param:
assert isinstance(request.param['agent'], Agent)
return TestRunner(request.param['agent'])
elif 'agent_name' in request.param:
assert isinstance(request.param['agent_name'], str)
return TestRunner.from_agent_name(request.param['agent_name'])
raise NotImplementedError('Must provide agent or agent_name.')
@fixture(autouse=True)
def llm_backend(request: FixtureRequest):
# Set backend environment value.
original_val = os.environ.get('GOOGLE_GENAI_USE_VERTEXAI')
backend_type = request.param
if backend_type == 'GOOGLE_AI':
os.environ['GOOGLE_GENAI_USE_VERTEXAI'] = '0'
else:
os.environ['GOOGLE_GENAI_USE_VERTEXAI'] = '1'
yield # Run the test
# Restore the environment
if original_val is None:
os.environ.pop('GOOGLE_GENAI_USE_VERTEXAI', None)
else:
os.environ['GOOGLE_GENAI_USE_VERTEXAI'] = original_val
@hookimpl(tryfirst=True)
def pytest_generate_tests(metafunc: Metafunc):
if llm_backend.__name__ in metafunc.fixturenames:
if not _is_explicitly_marked(llm_backend.__name__, metafunc):
test_backend = os.environ.get('TEST_BACKEND', 'BOTH')
if test_backend == 'GOOGLE_AI_ONLY':
metafunc.parametrize(llm_backend.__name__, ['GOOGLE_AI'], indirect=True)
elif test_backend == 'VERTEX_ONLY':
metafunc.parametrize(llm_backend.__name__, ['VERTEX'], indirect=True)
elif test_backend == 'BOTH':
metafunc.parametrize(
llm_backend.__name__, ['GOOGLE_AI', 'VERTEX'], indirect=True
)
else:
raise ValueError(
f'Invalid TEST_BACKEND value: {test_backend}, should be one of'
' [GOOGLE_AI_ONLY, VERTEX_ONLY, BOTH]'
)
def _is_explicitly_marked(mark_name: str, metafunc: Metafunc) -> bool:
if hasattr(metafunc.function, 'pytestmark'):
for mark in metafunc.function.pytestmark:
if mark.name == 'parametrize' and mark.args[0] == mark_name:
return True
return False
+14
View File
@@ -0,0 +1,14 @@
# 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
#
# http://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.
@@ -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
#
# http://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,88 @@
# 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
#
# http://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 google.adk import Agent
from google.genai import types
new_message = types.Content(
role="user",
parts=[types.Part.from_text(text="Count a number")],
)
google_agent_1 = Agent(
model="gemini-1.5-flash",
name="agent_1",
description="The first agent in the team.",
instruction="Just say 1",
generate_content_config=types.GenerateContentConfig(
temperature=0.1,
),
)
google_agent_2 = Agent(
model="gemini-1.5-flash",
name="agent_2",
description="The second agent in the team.",
instruction="Just say 2",
generate_content_config=types.GenerateContentConfig(
temperature=0.2,
safety_settings=[{
"category": "HARM_CATEGORY_HATE_SPEECH",
"threshold": "BLOCK_ONLY_HIGH",
}],
),
)
google_agent_3 = Agent(
model="gemini-1.5-flash",
name="agent_3",
description="The third agent in the team.",
instruction="Just say 3",
generate_content_config=types.GenerateContentConfig(
temperature=0.5,
safety_settings=[{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"threshold": "BLOCK_NONE",
}],
),
)
google_agent_with_instruction_in_config = Agent(
model="gemini-1.5-flash",
name="agent",
generate_content_config=types.GenerateContentConfig(
temperature=0.5, system_instruction="Count 1"
),
)
def function():
pass
google_agent_with_tools_in_config = Agent(
model="gemini-1.5-flash",
name="agent",
generate_content_config=types.GenerateContentConfig(
temperature=0.5, tools=[function]
),
)
google_agent_with_response_schema_in_config = Agent(
model="gemini-1.5-flash",
name="agent",
generate_content_config=types.GenerateContentConfig(
temperature=0.5, response_schema={"key": "value"}
),
)
@@ -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
#
# http://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,105 @@
# 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
#
# http://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 typing import Optional
from google.adk import Agent
from google.adk.agents.callback_context import CallbackContext
from google.adk.agents.invocation_context import InvocationContext
from google.adk.models import LlmRequest
from google.adk.models import LlmResponse
from google.genai import types
def before_agent_call_end_invocation(
callback_context: CallbackContext,
) -> types.Content:
return types.Content(
role='model',
parts=[types.Part(text='End invocation event before agent call.')],
)
def before_agent_call(
invocation_context: InvocationContext,
) -> types.Content:
return types.Content(
role='model',
parts=[types.Part.from_text(text='Plain text event before agent call.')],
)
def before_model_call_end_invocation(
callback_context: CallbackContext, llm_request: LlmRequest
) -> LlmResponse:
return LlmResponse(
content=types.Content(
role='model',
parts=[
types.Part.from_text(
text='End invocation event before model call.'
)
],
)
)
def before_model_call(
invocation_context: InvocationContext, request: LlmRequest
) -> LlmResponse:
request.config.system_instruction = 'Just return 999 as response.'
return LlmResponse(
content=types.Content(
role='model',
parts=[
types.Part.from_text(
text='Update request event before model call.'
)
],
)
)
def after_model_call(
callback_context: CallbackContext,
llm_response: LlmResponse,
) -> Optional[LlmResponse]:
content = llm_response.content
if not content or not content.parts or not content.parts[0].text:
return
content.parts[0].text += 'Update response event after model call.'
return llm_response
before_agent_callback_agent = Agent(
model='gemini-1.5-flash',
name='before_agent_callback_agent',
instruction='echo 1',
before_agent_callback=before_agent_call_end_invocation,
)
before_model_callback_agent = Agent(
model='gemini-1.5-flash',
name='before_model_callback_agent',
instruction='echo 2',
before_model_callback=before_model_call_end_invocation,
)
after_model_callback_agent = Agent(
model='gemini-1.5-flash',
name='after_model_callback_agent',
instruction='Say hello',
after_model_callback=after_model_call,
)
@@ -0,0 +1 @@
gkcng
@@ -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
#
# http://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,43 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://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 typing import List
from typing import Union
from google.adk import Agent
from google.adk.tools import ToolContext
from pydantic import BaseModel
def update_fc(
data_one: str,
data_two: Union[int, float, str],
data_three: list[str],
data_four: List[Union[int, float, str]],
tool_context: ToolContext,
):
"""Simply ask to update these variables in the context"""
tool_context.actions.update_state("data_one", data_one)
tool_context.actions.update_state("data_two", data_two)
tool_context.actions.update_state("data_three", data_three)
tool_context.actions.update_state("data_four", data_four)
root_agent = Agent(
model="gemini-1.5-flash",
name="root_agent",
instruction="Call tools",
flow="auto",
tools=[update_fc],
)
File diff suppressed because it is too large Load Diff
@@ -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
#
# http://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,115 @@
# 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
#
# http://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 typing import List
from typing import Union
from google.adk import Agent
from google.adk.agents.invocation_context import InvocationContext
from google.adk.planners import PlanReActPlanner
from google.adk.tools import ToolContext
def update_fc(
data_one: str,
data_two: Union[int, float, str],
data_three: list[str],
data_four: List[Union[int, float, str]],
tool_context: ToolContext,
) -> str:
"""Simply ask to update these variables in the context"""
tool_context.actions.update_state('data_one', data_one)
tool_context.actions.update_state('data_two', data_two)
tool_context.actions.update_state('data_three', data_three)
tool_context.actions.update_state('data_four', data_four)
return 'The function `update_fc` executed successfully'
def echo_info(customer_id: str) -> str:
"""Echo the context variable"""
return customer_id
def build_global_instruction(invocation_context: InvocationContext) -> str:
return (
'This is the gloabl agent instruction for invocation:'
f' {invocation_context.invocation_id}.'
)
def build_sub_agent_instruction(invocation_context: InvocationContext) -> str:
return 'This is the plain text sub agent instruction.'
context_variable_echo_agent = Agent(
model='gemini-1.5-flash',
name='context_variable_echo_agent',
instruction=(
'Use the echo_info tool to echo {customerId}, {customerInt},'
' {customerFloat}, and {customerJson}. Ask for it if you need to.'
),
flow='auto',
tools=[echo_info],
)
context_variable_with_complicated_format_agent = Agent(
model='gemini-1.5-flash',
name='context_variable_echo_agent',
instruction=(
'Use the echo_info tool to echo { customerId }, {{customer_int }, { '
" non-identifier-float}}, {artifact.fileName}, {'key1': 'value1'} and"
" {{'key2': 'value2'}}. Ask for it if you need to."
),
flow='auto',
tools=[echo_info],
)
context_variable_with_nl_planner_agent = Agent(
model='gemini-1.5-flash',
name='context_variable_with_nl_planner_agent',
instruction=(
'Use the echo_info tool to echo {customerId}. Ask for it if you'
' need to.'
),
flow='auto',
planner=PlanReActPlanner(),
tools=[echo_info],
)
context_variable_with_function_instruction_agent = Agent(
model='gemini-1.5-flash',
name='context_variable_with_function_instruction_agent',
instruction=build_sub_agent_instruction,
flow='auto',
)
context_variable_update_agent = Agent(
model='gemini-1.5-flash',
name='context_variable_update_agent',
instruction='Call tools',
flow='auto',
tools=[update_fc],
)
root_agent = Agent(
model='gemini-1.5-flash',
name='root_agent',
description='The root agent.',
flow='auto',
global_instruction=build_global_instruction,
sub_agents=[
context_variable_with_nl_planner_agent,
context_variable_update_agent,
],
)
@@ -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
#
# http://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,172 @@
# 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
#
# http://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 os
import sys
from google.adk import Agent
from google.adk.agents import RemoteAgent
from google.adk.examples import Example
from google.adk.sessions import Session
from google.genai import types
def reset_data():
pass
def fetch_user_flight_information(customer_email: str) -> str:
"""Fetch user flight information."""
return """
[{"ticket_no": "7240005432906569", "book_ref": "C46E9F", "flight_id": 19250, "flight_no": "LX0112", "departure_airport": "CDG", "arrival_airport": "BSL", "scheduled_departure": "2024-12-30 12:09:03.561731-04:00", "scheduled_arrival": "2024-12-30 13:39:03.561731-04:00", "seat_no": "18E", "fare_conditions": "Economy"}]
"""
def list_customer_flights(customer_email: str) -> str:
return "{'flights': [{'book_ref': 'C46E9F'}]}"
def update_ticket_to_new_flight(ticket_no: str, new_flight_id: str) -> str:
return 'OK, your ticket has been updated.'
def lookup_company_policy(topic: str) -> str:
"""Lookup policies for flight cancelation and rebooking."""
return """
1. How can I change my booking?
* The ticket number must start with 724 (SWISS ticket no./plate).
* The ticket was not paid for by barter or voucher (there are exceptions to voucher payments; if the ticket was paid for in full by voucher, then it may be possible to rebook online under certain circumstances. If it is not possible to rebook online because of the payment method, then you will be informed accordingly during the rebooking process).
* There must be an active flight booking for your ticket. It is not possible to rebook open tickets or tickets without the corresponding flight segments online at the moment.
* It is currently only possible to rebook outbound (one-way) tickets or return tickets with single flight routes (point-to-point).
"""
def search_flights(
departure_airport: str = None,
arrival_airport: str = None,
start_time: str = None,
end_time: str = None,
) -> list[dict]:
return """
[{"flight_id": 19238, "flight_no": "LX0112", "scheduled_departure": "2024-05-08 12:09:03.561731-04:00", "scheduled_arrival": "2024-05-08 13:39:03.561731-04:00", "departure_airport": "CDG", "arrival_airport": "BSL", "status": "Scheduled", "aircraft_code": "SU9", "actual_departure": null, "actual_arrival": null}, {"flight_id": 19242, "flight_no": "LX0112", "scheduled_departure": "2024-05-09 12:09:03.561731-04:00", "scheduled_arrival": "2024-05-09 13:39:03.561731-04:00", "departure_airport": "CDG", "arrival_airport": "BSL", "status": "Scheduled", "aircraft_code": "SU9", "actual_departure": null, "actual_arrival": null}]"""
def search_hotels(
location: str = None,
price_tier: str = None,
checkin_date: str = None,
checkout_date: str = None,
) -> list[dict]:
return """
[{"id": 1, "name": "Hilton Basel", "location": "Basel", "price_tier": "Luxury"}, {"id": 3, "name": "Hyatt Regency Basel", "location": "Basel", "price_tier": "Upper Upscale"}, {"id": 8, "name": "Holiday Inn Basel", "location": "Basel", "price_tier": "Upper Midscale"}]
"""
def book_hotel(hotel_name: str) -> str:
return 'OK, your hotel has been booked.'
def before_model_call(agent: Agent, session: Session, user_message):
if 'expedia' in user_message.lower():
response = types.Content(
role='model',
parts=[types.Part(text="Sorry, I can't answer this question.")],
)
return response
return None
def after_model_call(
agent: Agent, session: Session, content: types.Content
) -> bool:
model_message = content.parts[0].text
if 'expedia' in model_message.lower():
response = types.Content(
role='model',
parts=[types.Part(text="Sorry, I can't answer this question.")],
)
return response
return None
flight_agent = Agent(
model='gemini-1.5-pro',
name='flight_agent',
description='Handles flight information, policy and updates',
instruction="""
You are a specialized assistant for handling flight updates.
The primary assistant delegates work to you whenever the user needs help updating their bookings.
Confirm the updated flight details with the customer and inform them of any additional fees.
When searching, be persistent. Expand your query bounds if the first search returns no results.
Remember that a booking isn't completed until after the relevant tool has successfully been used.
Do not waste the user's time. Do not make up invalid tools or functions.
""",
tools=[
list_customer_flights,
lookup_company_policy,
fetch_user_flight_information,
search_flights,
update_ticket_to_new_flight,
],
)
hotel_agent = Agent(
model='gemini-1.5-pro',
name='hotel_agent',
description='Handles hotel information and booking',
instruction="""
You are a specialized assistant for handling hotel bookings.
The primary assistant delegates work to you whenever the user needs help booking a hotel.
Search for available hotels based on the user's preferences and confirm the booking details with the customer.
When searching, be persistent. Expand your query bounds if the first search returns no results.
""",
tools=[search_hotels, book_hotel],
)
idea_agent = RemoteAgent(
model='gemini-1.5-pro',
name='idea_agent',
description='Provide travel ideas base on the destination.',
url='http://localhost:8000/agent/run',
)
root_agent = Agent(
model='gemini-1.5-pro',
name='root_agent',
instruction="""
You are a helpful customer support assistant for Swiss Airlines.
""",
sub_agents=[flight_agent, hotel_agent, idea_agent],
flow='auto',
examples=[
Example(
input=types.Content(
role='user',
parts=[types.Part(text='How were you built?')],
),
output=[
types.Content(
role='model',
parts=[
types.Part(
text='I was built with the best agent framework.'
)
],
)
],
),
],
)
@@ -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
#
# http://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,338 @@
# 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
#
# http://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 google.adk import Agent
# A lightweight in-memory mock database
ORDER_DB = {
"1": "FINISHED",
"2": "CANCELED",
"3": "PENDING",
"4": "PENDING",
} # Order id to status mapping. Available states: 'FINISHED', 'PENDING', and 'CANCELED'
USER_TO_ORDER_DB = {
"user_a": ["1", "4"],
"user_b": ["2"],
"user_c": ["3"],
} # User id to Order id mapping
TICKET_DB = [{
"ticket_id": "1",
"user_id": "user_a",
"issue_type": "LOGIN_ISSUE",
"status": "OPEN",
}] # Available states: 'OPEN', 'CLOSED', 'ESCALATED'
USER_INFO_DB = {
"user_a": {"name": "Alice", "email": "alice@example.com"},
"user_b": {"name": "Bob", "email": "bob@example.com"},
}
def reset_data():
global ORDER_DB
global USER_TO_ORDER_DB
global TICKET_DB
global USER_INFO_DB
ORDER_DB = {
"1": "FINISHED",
"2": "CANCELED",
"3": "PENDING",
"4": "PENDING",
}
USER_TO_ORDER_DB = {
"user_a": ["1", "4"],
"user_b": ["2"],
"user_c": ["3"],
}
TICKET_DB = [{
"ticket_id": "1",
"user_id": "user_a",
"issue_type": "LOGIN_ISSUE",
"status": "OPEN",
}]
USER_INFO_DB = {
"user_a": {"name": "Alice", "email": "alice@example.com"},
"user_b": {"name": "Bob", "email": "bob@example.com"},
}
def get_order_status(order_id: str) -> str:
"""Get the status of an order.
Args:
order_id (str): The unique identifier of the order.
Returns:
str: The status of the order (e.g., 'FINISHED', 'CANCELED', 'PENDING'),
or 'Order not found' if the order_id does not exist.
"""
return ORDER_DB.get(order_id, "Order not found")
def get_order_ids_for_user(user_id: str) -> list:
"""Get the list of order IDs assigned to a specific transaction associated with a user.
Args:
user_id (str): The unique identifier of the user.
Returns:
List[str]: A list of order IDs associated with the user, or an empty list
if no orders are found.
"""
return USER_TO_ORDER_DB.get(user_id, [])
def cancel_order(order_id: str) -> str:
"""Cancel an order if it is in a 'PENDING' state.
You should call "get_order_status" to check the status first, before calling
this tool.
Args:
order_id (str): The unique identifier of the order to be canceled.
Returns:
str: A message indicating whether the order was successfully canceled or
not.
"""
if order_id in ORDER_DB and ORDER_DB[order_id] == "PENDING":
ORDER_DB[order_id] = "CANCELED"
return f"Order {order_id} has been canceled."
return f"Order {order_id} cannot be canceled."
def refund_order(order_id: str) -> str:
"""Process a refund for an order if it is in a 'CANCELED' state.
You should call "get_order_status" to check if status first, before calling
this tool.
Args:
order_id (str): The unique identifier of the order to be refunded.
Returns:
str: A message indicating whether the order was successfully refunded or
not.
"""
if order_id in ORDER_DB and ORDER_DB[order_id] == "CANCELED":
return f"Order {order_id} has been refunded."
return f"Order {order_id} cannot be refunded."
def create_ticket(user_id: str, issue_type: str) -> str:
"""Create a new support ticket for a user.
Args:
user_id (str): The unique identifier of the user creating the ticket.
issue_type (str): An issue type the user is facing. Available types:
'LOGIN_ISSUE', 'ORDER_ISSUE', 'OTHER'.
Returns:
str: A message indicating that the ticket was created successfully,
including the ticket ID.
"""
ticket_id = str(len(TICKET_DB) + 1)
TICKET_DB.append({
"ticket_id": ticket_id,
"user_id": user_id,
"issue_type": issue_type,
"status": "OPEN",
})
return f"Ticket {ticket_id} created successfully."
def get_ticket_info(ticket_id: str) -> str:
"""Retrieve the information of a support ticket.
current status of a support ticket.
Args:
ticket_id (str): The unique identifier of the ticket.
Returns:
A dictionary contains the following fields, or 'Ticket not found' if the
ticket_id does not exist:
- "ticket_id": str, the current ticket id
- "user_id": str, the associated user id
- "issue": str, the issue type
- "status": The current status of the ticket (e.g., 'OPEN', 'CLOSED',
'ESCALATED')
Example: {"ticket_id": "1", "user_id": "user_a", "issue": "Login issue",
"status": "OPEN"}
"""
for ticket in TICKET_DB:
if ticket["ticket_id"] == ticket_id:
return ticket
return "Ticket not found"
def get_tickets_for_user(user_id: str) -> list:
"""Get all the ticket IDs associated with a user.
Args:
user_id (str): The unique identifier of the user.
Returns:
List[str]: A list of ticket IDs associated with the user.
If no tickets are found, returns an empty list.
"""
return [
ticket["ticket_id"]
for ticket in TICKET_DB
if ticket["user_id"] == user_id
]
def update_ticket_status(ticket_id: str, status: str) -> str:
"""Update the status of a support ticket.
Args:
ticket_id (str): The unique identifier of the ticket.
status (str): The new status to assign to the ticket (e.g., 'OPEN',
'CLOSED', 'ESCALATED').
Returns:
str: A message indicating whether the ticket status was successfully
updated.
"""
for ticket in TICKET_DB:
if ticket["ticket_id"] == ticket_id:
ticket["status"] = status
return f"Ticket {ticket_id} status updated to {status}."
return "Ticket not found"
def get_user_info(user_id: str) -> dict:
"""Retrieve information (name, email) about a user.
Args:
user_id (str): The unique identifier of the user.
Returns:
dict or str: A dictionary containing user information of the following
fields, or 'User not found' if the user_id does not exist:
- name: The name of the user
- email: The email address of the user
For example, {"name": "Chelsea", "email": "123@example.com"}
"""
return USER_INFO_DB.get(user_id, "User not found")
def send_email(user_id: str, email: str) -> list:
"""Send email to user for notification.
Args:
user_id (str): The unique identifier of the user.
email (str): The email address of the user.
Returns:
str: A message indicating whether the email was successfully sent.
"""
if user_id in USER_INFO_DB:
return f"Email sent to {email} for user id {user_id}"
return "Cannot find this user"
# def update_user_info(user_id: str, new_info: dict[str, str]) -> str:
def update_user_info(user_id: str, email: str, name: str) -> str:
"""Update a user's information.
Args:
user_id (str): The unique identifier of the user.
new_info (dict): A dictionary containing the fields to be updated (e.g.,
{'email': 'new_email@example.com'}). Available field keys: 'email' and
'name'.
Returns:
str: A message indicating whether the user's information was successfully
updated or not.
"""
if user_id in USER_INFO_DB:
# USER_INFO_DB[user_id].update(new_info)
if email and name:
USER_INFO_DB[user_id].update({"email": email, "name": name})
elif email:
USER_INFO_DB[user_id].update({"email": email})
elif name:
USER_INFO_DB[user_id].update({"name": name})
else:
raise ValueError("this should not happen.")
return f"User {user_id} information updated."
return "User not found"
def get_user_id_from_cookie() -> str:
"""Get user ID(username) from the cookie.
Only use this function when you do not know user ID(username).
Args: None
Returns:
str: The user ID.
"""
return "user_a"
root_agent = Agent(
model="gemini-2.0-flash-001",
name="Ecommerce_Customer_Service",
instruction="""
You are an intelligent customer service assistant for an e-commerce platform. Your goal is to accurately understand user queries and use the appropriate tools to fulfill requests. Follow these guidelines:
1. **Understand the Query**:
- Identify actions and conditions (e.g., create a ticket only for pending orders).
- Extract necessary details (e.g., user ID, order ID) from the query or infer them from the context.
2. **Plan Multi-Step Workflows**:
- Break down complex queries into sequential steps. For example
- typical workflow:
- Retrieve IDs or references first (e.g., orders for a user).
- Evaluate conditions (e.g., check order status).
- Perform actions (e.g., create a ticket) only when conditions are met.
- another typical workflows - order cancellation and refund:
- Retrieve all orders for the user (`get_order_ids_for_user`).
- Cancel pending orders (`cancel_order`).
- Refund canceled orders (`refund_order`).
- Notify the user (`send_email`).
- another typical workflows - send user report:
- Get user id.
- Get user info(like emails)
- Send email to user.
3. **Avoid Skipping Steps**:
- Ensure each intermediate step is completed before moving to the next.
- Do not create tickets or take other actions without verifying the conditions specified in the query.
4. **Provide Clear Responses**:
- Confirm the actions performed, including details like ticket ID or pending orders.
- Ensure the response aligns with the steps taken and query intent.
""",
tools=[
get_order_status,
cancel_order,
get_order_ids_for_user,
refund_order,
create_ticket,
update_ticket_status,
get_tickets_for_user,
get_ticket_info,
get_user_info,
send_email,
update_user_info,
get_user_id_from_cookie,
],
)
@@ -0,0 +1,69 @@
[
{
"query": "Send an email to user user_a whose email address is alice@example.com",
"expected_tool_use": [
{
"tool_name": "send_email",
"tool_input": {
"email": "alice@example.com",
"user_id": "user_a"
}
}
],
"reference": "Email sent to alice@example.com for user id user_a."
},
{
"query": "Can you tell me the status of my order with ID 1?",
"expected_tool_use": [
{
"tool_name": "get_order_status",
"tool_input": {
"order_id": "1"
}
}
],
"reference": "Your order with ID 1 is FINISHED."
},
{
"query": "Cancel all pending order for the user with user id user_a",
"expected_tool_use": [
{
"tool_name": "get_order_ids_for_user",
"tool_input": {
"user_id": "user_a"
}
},
{
"tool_name": "get_order_status",
"tool_input": {
"order_id": "1"
}
},
{
"tool_name": "get_order_status",
"tool_input": {
"order_id": "4"
}
},
{
"tool_name": "cancel_order",
"tool_input": {
"order_id": "4"
}
}
],
"reference": "I have checked your orders and order 4 was in pending status, so I have cancelled it. Order 1 was already finished and couldn't be cancelled.\n"
},
{
"query": "What orders have I placed under the username user_b?",
"expected_tool_use": [
{
"tool_name": "get_order_ids_for_user",
"tool_input": {
"user_id": "user_b"
}
}
],
"reference": "User user_b has placed one order with order ID 2.\n"
}
]
@@ -0,0 +1,6 @@
{
"criteria": {
"tool_trajectory_avg_score": 0.7,
"response_match_score": 0.5
}
}

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