chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# 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
|
||||
#
|
||||
# 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,90 @@
|
||||
# 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
|
||||
#
|
||||
# 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 random
|
||||
|
||||
from google.adk import Agent
|
||||
from google.adk.models.anthropic_llm import Claude
|
||||
|
||||
|
||||
def roll_die(sides: int) -> int:
|
||||
"""Roll a die and return the rolled result.
|
||||
|
||||
Args:
|
||||
sides: The integer number of sides the die has.
|
||||
|
||||
Returns:
|
||||
An integer of the result of rolling the die.
|
||||
"""
|
||||
return random.randint(1, sides)
|
||||
|
||||
|
||||
async def check_prime(nums: list[int]) -> str:
|
||||
"""Check if a given list of numbers are prime.
|
||||
|
||||
Args:
|
||||
nums: The list of numbers to check.
|
||||
|
||||
Returns:
|
||||
A str indicating which number is prime.
|
||||
"""
|
||||
primes = set()
|
||||
for number in nums:
|
||||
number = int(number)
|
||||
if number <= 1:
|
||||
continue
|
||||
is_prime = True
|
||||
for i in range(2, int(number**0.5) + 1):
|
||||
if number % i == 0:
|
||||
is_prime = False
|
||||
break
|
||||
if is_prime:
|
||||
primes.add(number)
|
||||
return (
|
||||
"No prime numbers found."
|
||||
if not primes
|
||||
else f"{', '.join(str(num) for num in primes)} are prime numbers."
|
||||
)
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model=Claude(model="claude-3-5-sonnet-v2@20241022"),
|
||||
name="hello_world_agent",
|
||||
description=(
|
||||
"hello world agent that can roll a dice of 8 sides and check prime"
|
||||
" numbers."
|
||||
),
|
||||
instruction="""
|
||||
You roll dice and answer questions about the outcome of the dice rolls.
|
||||
You can roll dice of different sizes.
|
||||
You can use multiple tools in parallel by calling functions in parallel(in one request and in one round).
|
||||
It is ok to discuss previous dice roles, and comment on the dice rolls.
|
||||
When you are asked to roll a die, you must call the roll_die tool with the number of sides. Be sure to pass in an integer. Do not pass in a string.
|
||||
You should never roll a die on your own.
|
||||
When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string.
|
||||
You should not check prime numbers before calling the tool.
|
||||
When you are asked to roll a die and check prime numbers, you should always make the following two function calls:
|
||||
1. You should first call the roll_die tool to get a roll. Wait for the function response before calling the check_prime tool.
|
||||
2. After you get the function response from roll_die tool, you should call the check_prime tool with the roll_die result.
|
||||
2.1 If user asks you to check primes based on previous rolls, make sure you include the previous rolls in the list.
|
||||
3. When you respond, you must include the roll_die result from step 1.
|
||||
You should always perform the previous 3 steps when asking for a roll and checking prime numbers.
|
||||
You should not rely on the previous history on prime results.
|
||||
""",
|
||||
tools=[
|
||||
roll_die,
|
||||
check_prime,
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
# 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
|
||||
#
|
||||
# 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 asyncio
|
||||
import time
|
||||
|
||||
import agent
|
||||
from dotenv import load_dotenv
|
||||
from google.adk import Runner
|
||||
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from google.adk.cli.utils import logs
|
||||
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
||||
from google.adk.sessions.session import Session
|
||||
from google.genai import types
|
||||
|
||||
load_dotenv(override=True)
|
||||
logs.log_to_tmp_folder()
|
||||
|
||||
|
||||
async def main():
|
||||
app_name = 'my_app'
|
||||
user_id_1 = 'user1'
|
||||
session_service = InMemorySessionService()
|
||||
artifact_service = InMemoryArtifactService()
|
||||
runner = Runner(
|
||||
app_name=app_name,
|
||||
agent=agent.root_agent,
|
||||
artifact_service=artifact_service,
|
||||
session_service=session_service,
|
||||
)
|
||||
session_11 = await session_service.create_session(
|
||||
app_name=app_name, user_id=user_id_1
|
||||
)
|
||||
|
||||
async def run_prompt(session: Session, new_message: str):
|
||||
content = types.Content(
|
||||
role='user', parts=[types.Part.from_text(text=new_message)]
|
||||
)
|
||||
print('** User says:', content.model_dump(exclude_none=True))
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id_1,
|
||||
session_id=session.id,
|
||||
new_message=content,
|
||||
):
|
||||
if event.content.parts and event.content.parts[0].text:
|
||||
print(f'** {event.author}: {event.content.parts[0].text}')
|
||||
|
||||
start_time = time.time()
|
||||
print('Start time:', start_time)
|
||||
print('------------------------------------')
|
||||
await run_prompt(session_11, 'Hi, introduce yourself.')
|
||||
await run_prompt(
|
||||
session_11,
|
||||
'Run the following request 10 times: roll a die with 100 sides and check'
|
||||
' if it is prime',
|
||||
)
|
||||
end_time = time.time()
|
||||
print('------------------------------------')
|
||||
print('End time:', end_time)
|
||||
print('Total time:', end_time - start_time)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,8 @@
|
||||
# This is a sample .env file.
|
||||
# Copy this file to .env and replace the placeholder values with your actual credentials.
|
||||
|
||||
# Your Google API key for accessing Gemini models.
|
||||
GOOGLE_API_KEY="your-google-api-key"
|
||||
|
||||
# The URL of your Apigee proxy.
|
||||
APIGEE_PROXY_URL="https://your-apigee-proxy.net/basepath"
|
||||
@@ -0,0 +1,94 @@
|
||||
# Hello World with Apigee LLM
|
||||
|
||||
This sample demonstrates how to use the Agent Development Kit (ADK) with an LLM fronted by an Apigee proxy. It showcases the flexibility of the `ApigeeLlm` class in configuring the target LLM provider (Gemini or Vertex AI) and API version through the model string.
|
||||
|
||||
## Setup
|
||||
|
||||
Before running the sample, you need to configure your environment with the necessary credentials.
|
||||
|
||||
1. **Create a `.env` file:**
|
||||
Copy the sample environment file to a new file named `.env` in the same directory.
|
||||
|
||||
```bash
|
||||
cp .env-sample .env
|
||||
```
|
||||
|
||||
1. **Set Environment Variables:**
|
||||
Open the `.env` file and provide values for the following variables:
|
||||
|
||||
- `GOOGLE_API_KEY`: Your API key for the Google AI services (Gemini).
|
||||
- `APIGEE_PROXY_URL`: The full URL of your Apigee proxy endpoint.
|
||||
|
||||
Example `.env` file:
|
||||
|
||||
```
|
||||
GOOGLE_API_KEY="your-google-api-key"
|
||||
APIGEE_PROXY_URL="https://your-apigee-proxy.net/basepath"
|
||||
```
|
||||
|
||||
The `main.py` script will automatically load these variables when it runs.
|
||||
|
||||
## Run the Sample
|
||||
|
||||
Once your `.env` file is configured, you can run the sample with the following command:
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Configuring the Apigee LLM
|
||||
|
||||
The `ApigeeLlm` class is configured using a special model string format in `agent.py`. This string determines which backend provider (Vertex AI or Gemini) and which API version to use.
|
||||
|
||||
### Model String Format
|
||||
|
||||
The supported format is:
|
||||
|
||||
`apigee/[<provider>/][<version>/]<model_id>`
|
||||
|
||||
- **`provider`** (optional): Can be `vertex_ai` or `gemini`.
|
||||
|
||||
- If specified, it forces the use of that provider.
|
||||
- If omitted, the provider is determined by the `GOOGLE_GENAI_USE_ENTERPRISE` environment variable. If this variable is set to `true` or `1`, Vertex AI is used; otherwise, `gemini` is used by default.
|
||||
|
||||
- **`version`** (optional): The API version to use (e.g., `v1`, `v1beta`).
|
||||
|
||||
- If omitted, the default version for the selected provider is used.
|
||||
|
||||
- **`model_id`** (required): The identifier for the model you want to use (e.g., `gemini-2.5-flash`).
|
||||
|
||||
### Configuration Examples
|
||||
|
||||
Here are some examples of how to configure the model string in `agent.py` to achieve different behaviors:
|
||||
|
||||
1. **Implicit Provider (determined by environment variable):**
|
||||
|
||||
- `model="apigee/gemini-2.5-flash"`
|
||||
|
||||
- Uses the default API version.
|
||||
- Provider is Vertex AI if `GOOGLE_GENAI_USE_ENTERPRISE` is true; otherwise, Gemini.
|
||||
|
||||
- `model="apigee/v1/gemini-2.5-flash"`
|
||||
|
||||
- Uses API version `v1`.
|
||||
- Provider is determined by the environment variable.
|
||||
|
||||
1. **Explicit Provider (ignores environment variable):**
|
||||
|
||||
- `model="apigee/vertex_ai/gemini-2.5-flash"`
|
||||
|
||||
- Uses Vertex AI with the default API version.
|
||||
|
||||
- `model="apigee/gemini/gemini-2.5-flash"`
|
||||
|
||||
- Uses Gemini with the default API version.
|
||||
|
||||
- `model="apigee/gemini/v1/gemini-2.5-flash"`
|
||||
|
||||
- Uses Gemini with API version `v1`.
|
||||
|
||||
- `model="apigee/vertex_ai/v1beta/gemini-2.5-flash"`
|
||||
|
||||
- Uses Vertex AI with API version `v1beta`.
|
||||
|
||||
By modifying the `model` string in `agent.py`, you can test various configurations without changing the core logic of the agent.
|
||||
@@ -0,0 +1,108 @@
|
||||
# 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
|
||||
#
|
||||
# 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 random
|
||||
|
||||
from google.adk import Agent
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
from google.genai import types
|
||||
|
||||
|
||||
def roll_die(sides: int, tool_context: ToolContext) -> int:
|
||||
"""Roll a die and return the rolled result.
|
||||
|
||||
Args:
|
||||
sides: The integer number of sides the die has.
|
||||
|
||||
Returns:
|
||||
An integer of the result of rolling the die.
|
||||
"""
|
||||
result = random.randint(1, sides)
|
||||
if "rolls" not in tool_context.state:
|
||||
tool_context.state["rolls"] = []
|
||||
|
||||
tool_context.state["rolls"] = tool_context.state["rolls"] + [result]
|
||||
return result
|
||||
|
||||
|
||||
async def check_prime(nums: list[int]) -> str:
|
||||
"""Check if a given list of numbers are prime.
|
||||
|
||||
Args:
|
||||
nums: The list of numbers to check.
|
||||
|
||||
Returns:
|
||||
A str indicating which number is prime.
|
||||
"""
|
||||
primes = set()
|
||||
for number in nums:
|
||||
number = int(number)
|
||||
if number <= 1:
|
||||
continue
|
||||
is_prime = True
|
||||
for i in range(2, int(number**0.5) + 1):
|
||||
if number % i == 0:
|
||||
is_prime = False
|
||||
break
|
||||
if is_prime:
|
||||
primes.add(number)
|
||||
return (
|
||||
"No prime numbers found."
|
||||
if not primes
|
||||
else f"{', '.join(str(num) for num in primes)} are prime numbers."
|
||||
)
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model="apigee/gemini-2.5-flash",
|
||||
name="hello_world_agent",
|
||||
description=(
|
||||
"hello world agent that can roll a dice of 8 sides and check prime"
|
||||
" numbers."
|
||||
),
|
||||
instruction="""
|
||||
You roll dice and answer questions about the outcome of the dice rolls.
|
||||
You can roll dice of different sizes.
|
||||
You can use multiple tools in parallel by calling functions in parallel(in one request and in one round).
|
||||
It is ok to discuss previous dice roles, and comment on the dice rolls.
|
||||
When you are asked to roll a die, you must call the roll_die tool with the number of sides. Be sure to pass in an integer. Do not pass in a string.
|
||||
You should never roll a die on your own.
|
||||
When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string.
|
||||
You should not check prime numbers before calling the tool.
|
||||
When you are asked to roll a die and check prime numbers, you should always make the following two function calls:
|
||||
1. You should first call the roll_die tool to get a roll. Wait for the function response before calling the check_prime tool.
|
||||
2. After you get the function response from roll_die tool, you should call the check_prime tool with the roll_die result.
|
||||
2.1 If user asks you to check primes based on previous rolls, make sure you include the previous rolls in the list.
|
||||
3. When you respond, you must include the roll_die result from step 1.
|
||||
You should always perform the previous 3 steps when asking for a roll and checking prime numbers.
|
||||
You should not rely on the previous history on prime results.
|
||||
""",
|
||||
tools=[
|
||||
roll_die,
|
||||
check_prime,
|
||||
],
|
||||
# planner=BuiltInPlanner(
|
||||
# thinking_config=types.ThinkingConfig(
|
||||
# include_thoughts=True,
|
||||
# ),
|
||||
# ),
|
||||
generate_content_config=types.GenerateContentConfig(
|
||||
safety_settings=[
|
||||
types.SafetySetting( # avoid false alarm about rolling dice.
|
||||
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
||||
threshold=types.HarmBlockThreshold.OFF,
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,112 @@
|
||||
# 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
|
||||
#
|
||||
# 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 asyncio
|
||||
import os
|
||||
import time
|
||||
|
||||
import agent
|
||||
from dotenv import load_dotenv
|
||||
from google.adk.agents.run_config import RunConfig
|
||||
from google.adk.cli.utils import logs
|
||||
from google.adk.runners import InMemoryRunner
|
||||
from google.adk.sessions.session import Session
|
||||
from google.genai import types
|
||||
|
||||
load_dotenv(override=True)
|
||||
logs.log_to_tmp_folder()
|
||||
|
||||
|
||||
async def main():
|
||||
app_name = "my_app"
|
||||
user_id_1 = "user1"
|
||||
runner = InMemoryRunner(
|
||||
agent=agent.root_agent,
|
||||
app_name=app_name,
|
||||
)
|
||||
session_11 = await runner.session_service.create_session(
|
||||
app_name=app_name, user_id=user_id_1
|
||||
)
|
||||
|
||||
async def run_prompt(session: Session, new_message: str):
|
||||
content = types.Content(
|
||||
role="user", parts=[types.Part.from_text(text=new_message)]
|
||||
)
|
||||
print("** User says:", content.model_dump(exclude_none=True))
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id_1,
|
||||
session_id=session.id,
|
||||
new_message=content,
|
||||
):
|
||||
if event.content.parts and event.content.parts[0].text:
|
||||
print(f"** {event.author}: {event.content.parts[0].text}")
|
||||
|
||||
async def run_prompt_bytes(session: Session, new_message: str):
|
||||
content = types.Content(
|
||||
role="user",
|
||||
parts=[
|
||||
types.Part.from_bytes(
|
||||
data=str.encode(new_message), mime_type="text/plain"
|
||||
)
|
||||
],
|
||||
)
|
||||
print("** User says:", content.model_dump(exclude_none=True))
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id_1,
|
||||
session_id=session.id,
|
||||
new_message=content,
|
||||
run_config=RunConfig(save_input_blobs_as_artifacts=True),
|
||||
):
|
||||
if event.content.parts and event.content.parts[0].text:
|
||||
print(f"** {event.author}: {event.content.parts[0].text}")
|
||||
|
||||
async def check_rolls_in_state(rolls_size: int):
|
||||
session = await runner.session_service.get_session(
|
||||
app_name=app_name, user_id=user_id_1, session_id=session_11.id
|
||||
)
|
||||
assert len(session.state["rolls"]) == rolls_size
|
||||
for roll in session.state["rolls"]:
|
||||
assert roll > 0 and roll <= 100
|
||||
|
||||
start_time = time.time()
|
||||
print("Start time:", start_time)
|
||||
print("------------------------------------")
|
||||
await run_prompt(session_11, "Hi")
|
||||
await run_prompt(session_11, "Roll a die with 100 sides")
|
||||
await check_rolls_in_state(1)
|
||||
await run_prompt(session_11, "Roll a die again with 100 sides.")
|
||||
await check_rolls_in_state(2)
|
||||
await run_prompt(session_11, "What numbers did I got?")
|
||||
await run_prompt_bytes(session_11, "Hi bytes")
|
||||
print(
|
||||
await runner.artifact_service.list_artifact_keys(
|
||||
app_name=app_name, user_id=user_id_1, session_id=session_11.id
|
||||
)
|
||||
)
|
||||
end_time = time.time()
|
||||
print("------------------------------------")
|
||||
print("End time:", end_time)
|
||||
print("Total time:", end_time - start_time)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# The API key can be set in a .env file.
|
||||
# For example, create a .env file with the following content:
|
||||
# GOOGLE_API_KEY="your-api-key"
|
||||
# APIGEE_PROXY_URL="your-proxy-url"
|
||||
if not os.getenv("GOOGLE_API_KEY"):
|
||||
raise ValueError("GOOGLE_API_KEY environment variable is not set.")
|
||||
if not os.getenv("APIGEE_PROXY_URL"):
|
||||
raise ValueError("APIGEE_PROXY_URL environment variable is not set.")
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,16 @@
|
||||
# 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
|
||||
#
|
||||
# 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,95 @@
|
||||
# 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
|
||||
#
|
||||
# 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 random
|
||||
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
from google.adk.models.gemma_llm import Gemma
|
||||
from google.genai.types import GenerateContentConfig
|
||||
|
||||
|
||||
def roll_die(sides: int) -> int:
|
||||
"""Roll a die and return the rolled result.
|
||||
|
||||
Args:
|
||||
sides: The integer number of sides the die has.
|
||||
|
||||
Returns:
|
||||
An integer of the result of rolling the die.
|
||||
"""
|
||||
return random.randint(1, sides)
|
||||
|
||||
|
||||
async def check_prime(nums: list[int]) -> str:
|
||||
"""Check if a given list of numbers are prime.
|
||||
|
||||
Args:
|
||||
nums: The list of numbers to check.
|
||||
|
||||
Returns:
|
||||
A str indicating which number is prime.
|
||||
"""
|
||||
primes = set()
|
||||
for number in nums:
|
||||
number = number
|
||||
if number <= 1:
|
||||
continue
|
||||
is_prime = True
|
||||
for i in range(2, int(number**0.5) + 1):
|
||||
if number % i == 0:
|
||||
is_prime = False
|
||||
break
|
||||
if is_prime:
|
||||
primes.add(number)
|
||||
return (
|
||||
"No prime numbers found."
|
||||
if not primes
|
||||
else f"{', '.join(str(num) for num in primes)} are prime numbers."
|
||||
)
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model=Gemma(model="gemma-3-27b-it"),
|
||||
name="data_processing_agent",
|
||||
description=(
|
||||
"hello world agent that can roll many-sided dice and check if numbers"
|
||||
" are prime."
|
||||
),
|
||||
instruction="""
|
||||
You roll dice and answer questions about the outcome of the dice rolls.
|
||||
You can roll dice of different sizes.
|
||||
You can use multiple tools in parallel by calling functions in parallel(in one request and in one round).
|
||||
It is ok to discuss previous dice roles, and comment on the dice rolls.
|
||||
When you are asked to roll a die, you must call the roll_die tool with the number of sides. Be sure to pass in an integer. Do not pass in a string.
|
||||
You should never roll a die on your own.
|
||||
When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string.
|
||||
You should not check prime numbers before calling the tool.
|
||||
When you are asked to roll a die and check prime numbers, you should always make the following two function calls:
|
||||
1. You should first call the roll_die tool to get a roll. Wait for the function response before calling the check_prime tool.
|
||||
2. After the user reports a response from roll_die tool, you should call the check_prime tool with the roll_die result.
|
||||
2.1 If user asks you to check primes based on previous rolls, make sure you include the previous rolls in the list.
|
||||
3. When you respond, you must include the roll_die result from step 1.
|
||||
You should always perform the previous 3 steps when asking for a roll and checking prime numbers.
|
||||
You should not rely on the previous history on prime results.
|
||||
""",
|
||||
tools=[
|
||||
roll_die,
|
||||
check_prime,
|
||||
],
|
||||
generate_content_config=GenerateContentConfig(
|
||||
temperature=1.0,
|
||||
top_p=0.95,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
# 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
|
||||
#
|
||||
# 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 asyncio
|
||||
import logging
|
||||
import time
|
||||
|
||||
import agent
|
||||
from dotenv import load_dotenv
|
||||
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from google.adk.cli.utils import logs
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
||||
from google.adk.sessions.session import Session
|
||||
from google.genai import types
|
||||
|
||||
load_dotenv(override=True)
|
||||
logs.log_to_tmp_folder(level=logging.INFO)
|
||||
|
||||
|
||||
async def main():
|
||||
app_name = 'my_gemma_app'
|
||||
user_id_1 = 'user1'
|
||||
session_service = InMemorySessionService()
|
||||
artifact_service = InMemoryArtifactService()
|
||||
runner = Runner(
|
||||
app_name=app_name,
|
||||
agent=agent.root_agent,
|
||||
artifact_service=artifact_service,
|
||||
session_service=session_service,
|
||||
)
|
||||
session_11 = await session_service.create_session(
|
||||
app_name=app_name, user_id=user_id_1
|
||||
)
|
||||
|
||||
async def run_prompt(session: Session, new_message: str):
|
||||
content = types.Content(
|
||||
role='user', parts=[types.Part.from_text(text=new_message)]
|
||||
)
|
||||
print('** User says:', content.model_dump(exclude_none=True))
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id_1,
|
||||
session_id=session.id,
|
||||
new_message=content,
|
||||
):
|
||||
if event.content.parts and event.content.parts[0].text:
|
||||
print(f'** {event.author}: {event.content.parts[0].text}')
|
||||
|
||||
start_time = time.time()
|
||||
print('Start time:', start_time)
|
||||
print('------------------------------------')
|
||||
await run_prompt(session_11, 'Hi, introduce yourself.')
|
||||
await run_prompt(
|
||||
session_11, 'Roll a die with 100 sides and check if it is prime'
|
||||
)
|
||||
await run_prompt(session_11, 'Roll it again.')
|
||||
await run_prompt(session_11, 'What numbers did I get?')
|
||||
end_time = time.time()
|
||||
print('------------------------------------')
|
||||
print('End time:', end_time)
|
||||
print('Total time:', end_time - start_time)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,16 @@
|
||||
# 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
|
||||
#
|
||||
# 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,93 @@
|
||||
# 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
|
||||
#
|
||||
# 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 random
|
||||
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
from google.adk.models import Gemma3Ollama
|
||||
|
||||
litellm_logger = logging.getLogger("LiteLLM")
|
||||
litellm_logger.setLevel(logging.WARNING)
|
||||
|
||||
|
||||
def roll_die(sides: int) -> int:
|
||||
"""Roll a die and return the rolled result.
|
||||
|
||||
Args:
|
||||
sides: The integer number of sides the die has.
|
||||
|
||||
Returns:
|
||||
An integer of the result of rolling the die.
|
||||
"""
|
||||
return random.randint(1, sides)
|
||||
|
||||
|
||||
async def check_prime(nums: list[int]) -> str:
|
||||
"""Check if a given list of numbers are prime.
|
||||
|
||||
Args:
|
||||
nums: The list of numbers to check.
|
||||
|
||||
Returns:
|
||||
A str indicating which number is prime.
|
||||
"""
|
||||
primes = set()
|
||||
for number in nums:
|
||||
number = int(number)
|
||||
if number <= 1:
|
||||
continue
|
||||
is_prime = True
|
||||
for i in range(2, int(number**0.5) + 1):
|
||||
if number % i == 0:
|
||||
is_prime = False
|
||||
break
|
||||
if is_prime:
|
||||
primes.add(number)
|
||||
return (
|
||||
"No prime numbers found."
|
||||
if not primes
|
||||
else f"{', '.join(str(num) for num in primes)} are prime numbers."
|
||||
)
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model=Gemma3Ollama(),
|
||||
name="data_processing_agent",
|
||||
description=(
|
||||
"hello world agent that can roll a dice of 8 sides and check prime"
|
||||
" numbers."
|
||||
),
|
||||
instruction="""
|
||||
You roll dice and answer questions about the outcome of the dice rolls.
|
||||
You can roll dice of different sizes.
|
||||
You can use multiple tools in parallel by calling functions in parallel (in one request and in one round).
|
||||
It is ok to discuss previous dice rolls, and comment on the dice rolls.
|
||||
When you are asked to roll a die, you must call the roll_die tool with the number of sides. Be sure to pass in an integer. Do not pass in a string.
|
||||
You should never roll a die on your own.
|
||||
When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string.
|
||||
You should not check prime numbers before calling the tool.
|
||||
When you are asked to roll a die and check prime numbers, you should always make the following two function calls:
|
||||
1. You should first call the roll_die tool to get a roll. Wait for the function response before calling the check_prime tool.
|
||||
2. After you get the function response from roll_die tool, you should call the check_prime tool with the roll_die result.
|
||||
2.1 If user asks you to check primes based on previous rolls, make sure you include the previous rolls in the list.
|
||||
3. When you respond, you must include the roll_die result from step 1.
|
||||
You should always perform the previous 3 steps when asking for a roll and checking prime numbers.
|
||||
You should not rely on the previous history on prime results.
|
||||
""",
|
||||
tools=[
|
||||
roll_die,
|
||||
check_prime,
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
# 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
|
||||
#
|
||||
# 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 asyncio
|
||||
import time
|
||||
|
||||
import agent
|
||||
from dotenv import load_dotenv
|
||||
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from google.adk.cli.utils import logs
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
||||
from google.adk.sessions.session import Session
|
||||
from google.genai import types
|
||||
|
||||
load_dotenv(override=True)
|
||||
logs.log_to_tmp_folder()
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
app_name = 'my_app'
|
||||
user_id_1 = 'user1'
|
||||
session_service = InMemorySessionService()
|
||||
artifact_service = InMemoryArtifactService()
|
||||
runner = Runner(
|
||||
app_name=app_name,
|
||||
agent=agent.root_agent,
|
||||
artifact_service=artifact_service,
|
||||
session_service=session_service,
|
||||
)
|
||||
session_1 = await session_service.create_session(
|
||||
app_name=app_name, user_id=user_id_1
|
||||
)
|
||||
|
||||
async def run_prompt(session: Session, new_message: str):
|
||||
content = types.Content(
|
||||
role='user', parts=[types.Part.from_text(text=new_message)]
|
||||
)
|
||||
print('** User says:', content.model_dump(exclude_none=True))
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id_1,
|
||||
session_id=session.id,
|
||||
new_message=content,
|
||||
):
|
||||
if event.content.parts and event.content.parts[0].text:
|
||||
print(f'** {event.author}: {event.content.parts[0].text}')
|
||||
|
||||
start_time = time.time()
|
||||
print('Start time:', start_time)
|
||||
print('------------------------------------')
|
||||
await run_prompt(session_1, 'Hi, introduce yourself.')
|
||||
await run_prompt(
|
||||
session_1, 'Roll a die with 100 sides and check if it is prime'
|
||||
)
|
||||
await run_prompt(session_1, 'Roll it again.')
|
||||
await run_prompt(session_1, 'What numbers did I get?')
|
||||
end_time = time.time()
|
||||
print('------------------------------------')
|
||||
print('End time:', end_time)
|
||||
print('Total time:', end_time - start_time)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,16 @@
|
||||
# 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
|
||||
#
|
||||
# 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,94 @@
|
||||
# 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
|
||||
#
|
||||
# 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 random
|
||||
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
from google.adk.models.lite_llm import LiteLlm
|
||||
|
||||
|
||||
def roll_die(sides: int) -> int:
|
||||
"""Roll a die and return the rolled result.
|
||||
|
||||
Args:
|
||||
sides: The integer number of sides the die has.
|
||||
|
||||
Returns:
|
||||
An integer of the result of rolling the die.
|
||||
"""
|
||||
return random.randint(1, sides)
|
||||
|
||||
|
||||
async def check_prime(nums: list[int]) -> str:
|
||||
"""Check if a given list of numbers are prime.
|
||||
|
||||
Args:
|
||||
nums: The list of numbers to check.
|
||||
|
||||
Returns:
|
||||
A str indicating which number is prime.
|
||||
"""
|
||||
primes = set()
|
||||
for number in nums:
|
||||
number = int(number)
|
||||
if number <= 1:
|
||||
continue
|
||||
is_prime = True
|
||||
for i in range(2, int(number**0.5) + 1):
|
||||
if number % i == 0:
|
||||
is_prime = False
|
||||
break
|
||||
if is_prime:
|
||||
primes.add(number)
|
||||
return (
|
||||
"No prime numbers found."
|
||||
if not primes
|
||||
else f"{', '.join(str(num) for num in primes)} are prime numbers."
|
||||
)
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
# model=LiteLlm(model="gemini/gemini-2.5-pro-exp-03-25"),
|
||||
# model=LiteLlm(model="vertex_ai/gemini-2.5-pro-exp-03-25"),
|
||||
# model=LiteLlm(model="vertex_ai/claude-3-5-haiku"),
|
||||
model=LiteLlm(model="openai/gpt-4o"),
|
||||
# model=LiteLlm(model="anthropic/claude-3-sonnet-20240229"),
|
||||
name="data_processing_agent",
|
||||
description=(
|
||||
"hello world agent that can roll a dice of 8 sides and check prime"
|
||||
" numbers."
|
||||
),
|
||||
instruction="""
|
||||
You roll dice and answer questions about the outcome of the dice rolls.
|
||||
You can roll dice of different sizes.
|
||||
You can use multiple tools in parallel by calling functions in parallel(in one request and in one round).
|
||||
It is ok to discuss previous dice roles, and comment on the dice rolls.
|
||||
When you are asked to roll a die, you must call the roll_die tool with the number of sides. Be sure to pass in an integer. Do not pass in a string.
|
||||
You should never roll a die on your own.
|
||||
When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string.
|
||||
You should not check prime numbers before calling the tool.
|
||||
When you are asked to roll a die and check prime numbers, you should always make the following two function calls:
|
||||
1. You should first call the roll_die tool to get a roll. Wait for the function response before calling the check_prime tool.
|
||||
2. After you get the function response from roll_die tool, you should call the check_prime tool with the roll_die result.
|
||||
2.1 If user asks you to check primes based on previous rolls, make sure you include the previous rolls in the list.
|
||||
3. When you respond, you must include the roll_die result from step 1.
|
||||
You should always perform the previous 3 steps when asking for a roll and checking prime numbers.
|
||||
You should not rely on the previous history on prime results.
|
||||
""",
|
||||
tools=[
|
||||
roll_die,
|
||||
check_prime,
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
# 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
|
||||
#
|
||||
# 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 asyncio
|
||||
import time
|
||||
|
||||
import agent
|
||||
from dotenv import load_dotenv
|
||||
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from google.adk.cli.utils import logs
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
||||
from google.adk.sessions.session import Session
|
||||
from google.genai import types
|
||||
|
||||
load_dotenv(override=True)
|
||||
logs.log_to_tmp_folder()
|
||||
|
||||
|
||||
async def main():
|
||||
app_name = 'my_app'
|
||||
user_id_1 = 'user1'
|
||||
session_service = InMemorySessionService()
|
||||
artifact_service = InMemoryArtifactService()
|
||||
runner = Runner(
|
||||
app_name=app_name,
|
||||
agent=agent.root_agent,
|
||||
artifact_service=artifact_service,
|
||||
session_service=session_service,
|
||||
)
|
||||
session_11 = await session_service.create_session(
|
||||
app_name=app_name, user_id=user_id_1
|
||||
)
|
||||
|
||||
async def run_prompt(session: Session, new_message: str):
|
||||
content = types.Content(
|
||||
role='user', parts=[types.Part.from_text(text=new_message)]
|
||||
)
|
||||
print('** User says:', content.model_dump(exclude_none=True))
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id_1,
|
||||
session_id=session.id,
|
||||
new_message=content,
|
||||
):
|
||||
if event.content.parts and event.content.parts[0].text:
|
||||
print(f'** {event.author}: {event.content.parts[0].text}')
|
||||
|
||||
start_time = time.time()
|
||||
print('Start time:', start_time)
|
||||
print('------------------------------------')
|
||||
await run_prompt(session_11, 'Hi, introduce yourself.')
|
||||
await run_prompt(
|
||||
session_11, 'Roll a die with 100 sides and check if it is prime'
|
||||
)
|
||||
await run_prompt(session_11, 'Roll it again.')
|
||||
await run_prompt(session_11, 'What numbers did I got?')
|
||||
end_time = time.time()
|
||||
print('------------------------------------')
|
||||
print('End time:', end_time)
|
||||
print('Total time:', end_time - start_time)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,16 @@
|
||||
# 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
|
||||
#
|
||||
# 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,81 @@
|
||||
# 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
|
||||
#
|
||||
# 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 random
|
||||
|
||||
from google.adk import Agent
|
||||
from google.adk.models.lite_llm import LiteLlm
|
||||
# Ensures langchain_core.tools.base is importable; langchain-core 1.x does not
|
||||
# auto-import it, so convert_to_openai_function fails without this.
|
||||
import langchain_core.tools
|
||||
from langchain_core.utils.function_calling import convert_to_openai_function
|
||||
|
||||
|
||||
def roll_die(sides: int) -> int:
|
||||
"""Roll a die and return the rolled result.
|
||||
|
||||
Args:
|
||||
sides: The integer number of sides the die has.
|
||||
|
||||
Returns:
|
||||
An integer of the result of rolling the die.
|
||||
"""
|
||||
return random.randint(1, sides)
|
||||
|
||||
|
||||
def check_prime(number: int) -> str:
|
||||
"""Check if a given number is prime.
|
||||
|
||||
Args:
|
||||
number: The input number to check.
|
||||
|
||||
Returns:
|
||||
A str indicating the number is prime or not.
|
||||
"""
|
||||
if number <= 1:
|
||||
return f"{number} is not prime."
|
||||
is_prime = True
|
||||
for i in range(2, int(number**0.5) + 1):
|
||||
if number % i == 0:
|
||||
is_prime = False
|
||||
break
|
||||
if is_prime:
|
||||
return f"{number} is prime."
|
||||
else:
|
||||
return f"{number} is not prime."
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model=LiteLlm(
|
||||
model="vertex_ai/meta/llama-4-maverick-17b-128e-instruct-maas",
|
||||
# If the model is not trained with functions and you would like to
|
||||
# enable function calling, you can add functions to the models, and the
|
||||
# functions will be added to the prompts during inferences.
|
||||
functions=[
|
||||
convert_to_openai_function(roll_die),
|
||||
convert_to_openai_function(check_prime),
|
||||
],
|
||||
),
|
||||
name="data_processing_agent",
|
||||
description="""You are a helpful assistant.""",
|
||||
instruction="""
|
||||
You are a helpful assistant, and call tools optionally.
|
||||
If call tools, the tool format should be in json, and the tool arguments should be parsed from users inputs.
|
||||
""",
|
||||
tools=[
|
||||
roll_die,
|
||||
check_prime,
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
# 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
|
||||
#
|
||||
# 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 asyncio
|
||||
import time
|
||||
|
||||
import agent
|
||||
from dotenv import load_dotenv
|
||||
from google.adk import Runner
|
||||
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from google.adk.cli.utils import logs
|
||||
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
||||
from google.adk.sessions.session import Session
|
||||
from google.genai import types
|
||||
|
||||
load_dotenv(override=True)
|
||||
logs.log_to_tmp_folder()
|
||||
|
||||
|
||||
async def main():
|
||||
app_name = 'my_app'
|
||||
user_id_1 = 'user1'
|
||||
session_service = InMemorySessionService()
|
||||
artifact_service = InMemoryArtifactService()
|
||||
runner = Runner(
|
||||
app_name=app_name,
|
||||
agent=agent.root_agent,
|
||||
artifact_service=artifact_service,
|
||||
session_service=session_service,
|
||||
)
|
||||
session_11 = await session_service.create_session(
|
||||
app_name=app_name, user_id=user_id_1
|
||||
)
|
||||
|
||||
async def run_prompt(session: Session, new_message: str):
|
||||
content = types.Content(
|
||||
role='user', parts=[types.Part.from_text(text=new_message)]
|
||||
)
|
||||
print('** User says:', content.model_dump(exclude_none=True))
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id_1,
|
||||
session_id=session.id,
|
||||
new_message=content,
|
||||
):
|
||||
if event.content.parts:
|
||||
part = event.content.parts[0]
|
||||
if part.text:
|
||||
print(f'** {event.author}: {part.text}')
|
||||
if part.function_call:
|
||||
print(f'** {event.author} calls tool: {part.function_call}')
|
||||
if part.function_response:
|
||||
print(
|
||||
f'** {event.author} gets tool response: {part.function_response}'
|
||||
)
|
||||
|
||||
start_time = time.time()
|
||||
print('Start time:', start_time)
|
||||
print('------------------------------------')
|
||||
await run_prompt(session_11, 'Hi, introduce yourself.')
|
||||
await run_prompt(session_11, 'Roll a die with 100 sides.')
|
||||
await run_prompt(session_11, 'Check if it is prime.')
|
||||
end_time = time.time()
|
||||
print('------------------------------------')
|
||||
print('End time:', end_time)
|
||||
print('Total time:', end_time - start_time)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,115 @@
|
||||
# Using ollama models with ADK
|
||||
|
||||
## Model choice
|
||||
|
||||
If your agent is relying on tools, please make sure that you select a model with tool support from [ollama website](https://ollama.com/search?c=tools).
|
||||
|
||||
For reliable results, we recommend using a decent size model with tool support.
|
||||
|
||||
The tool support for the model can be checked with the following command:
|
||||
|
||||
```bash
|
||||
ollama show mistral-small3.1
|
||||
Model
|
||||
architecture mistral3
|
||||
parameters 24.0B
|
||||
context length 131072
|
||||
embedding length 5120
|
||||
quantization Q4_K_M
|
||||
|
||||
Capabilities
|
||||
completion
|
||||
vision
|
||||
tools
|
||||
```
|
||||
|
||||
You are supposed to see `tools` listed under capabilities.
|
||||
|
||||
You can also look at the model's template and tweak it based on your needs.
|
||||
|
||||
```bash
|
||||
ollama show --modelfile llama3.1 > model_file_to_modify
|
||||
```
|
||||
|
||||
Then you can create a model with the following command:
|
||||
|
||||
```bash
|
||||
ollama create llama3.1-modified -f model_file_to_modify
|
||||
```
|
||||
|
||||
## Using ollama_chat provider
|
||||
|
||||
Our LiteLlm wrapper can be used to create agents with ollama models.
|
||||
|
||||
```py
|
||||
root_agent = Agent(
|
||||
model=LiteLlm(model="ollama_chat/mistral-small3.1"),
|
||||
name="dice_agent",
|
||||
description=(
|
||||
"hello world agent that can roll a dice of 8 sides and check prime"
|
||||
" numbers."
|
||||
),
|
||||
instruction="""
|
||||
You roll dice and answer questions about the outcome of the dice rolls.
|
||||
""",
|
||||
tools=[
|
||||
roll_die,
|
||||
check_prime,
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
**It is important to set the provider `ollama_chat` instead of `ollama`. Using `ollama` will result in unexpected behaviors such as infinite tool call loops and ignoring previous context.**
|
||||
|
||||
While `api_base` can be provided inside litellm for generation, litellm library is calling other APIs relying on the env variable instead as of v1.65.5 after completion. So at this time, we recommend setting the env variable `OLLAMA_API_BASE` to point to the ollama server.
|
||||
|
||||
```bash
|
||||
export OLLAMA_API_BASE="http://localhost:11434"
|
||||
adk web
|
||||
```
|
||||
|
||||
## Using openai provider
|
||||
|
||||
Alternatively, `openai` can be used as the provider name. But this will also require setting the `OPENAI_API_BASE=http://localhost:11434/v1` and `OPENAI_API_KEY=anything` env variables instead of `OLLAMA_API_BASE`. **Please notice that api base now has `/v1` at the end.**
|
||||
|
||||
```py
|
||||
root_agent = Agent(
|
||||
model=LiteLlm(model="openai/mistral-small3.1"),
|
||||
name="dice_agent",
|
||||
description=(
|
||||
"hello world agent that can roll a dice of 8 sides and check prime"
|
||||
" numbers."
|
||||
),
|
||||
instruction="""
|
||||
You roll dice and answer questions about the outcome of the dice rolls.
|
||||
""",
|
||||
tools=[
|
||||
roll_die,
|
||||
check_prime,
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
```bash
|
||||
export OPENAI_API_BASE=http://localhost:11434/v1
|
||||
export OPENAI_API_KEY=anything
|
||||
adk web
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
You can see the request sent to the ollama server by adding the following in your agent code just after imports.
|
||||
|
||||
```py
|
||||
import litellm
|
||||
litellm._turn_on_debug()
|
||||
```
|
||||
|
||||
Look for a line like the following:
|
||||
|
||||
```bash
|
||||
quest Sent from LiteLLM:
|
||||
curl -X POST \
|
||||
http://localhost:11434/api/chat \
|
||||
-d '{'model': 'mistral-small3.1', 'messages': [{'role': 'system', 'content': ...
|
||||
```
|
||||
@@ -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
|
||||
#
|
||||
# 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
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
# 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
|
||||
#
|
||||
# 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 random
|
||||
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
from google.adk.models.lite_llm import LiteLlm
|
||||
|
||||
|
||||
def roll_die(sides: int) -> int:
|
||||
"""Roll a die and return the rolled result.
|
||||
|
||||
Args:
|
||||
sides: The integer number of sides the die has.
|
||||
|
||||
Returns:
|
||||
An integer of the result of rolling the die.
|
||||
"""
|
||||
return random.randint(1, sides)
|
||||
|
||||
|
||||
def check_prime(numbers: list[int]) -> str:
|
||||
"""Check if a given list of numbers are prime.
|
||||
|
||||
Args:
|
||||
numbers: The list of numbers to check.
|
||||
|
||||
Returns:
|
||||
A str indicating which number is prime.
|
||||
"""
|
||||
primes = set()
|
||||
for number in numbers:
|
||||
number = int(number)
|
||||
if number <= 1:
|
||||
continue
|
||||
is_prime = True
|
||||
for i in range(2, int(number**0.5) + 1):
|
||||
if number % i == 0:
|
||||
is_prime = False
|
||||
break
|
||||
if is_prime:
|
||||
primes.add(number)
|
||||
return (
|
||||
"No prime numbers found."
|
||||
if not primes
|
||||
else f"{', '.join(str(num) for num in primes)} are prime numbers."
|
||||
)
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model=LiteLlm(model="ollama_chat/mistral-small3.1"),
|
||||
name="dice_roll_agent",
|
||||
description=(
|
||||
"hello world agent that can roll a dice of any number of sides and"
|
||||
" check prime numbers."
|
||||
),
|
||||
instruction="""
|
||||
You roll dice and answer questions about the outcome of the dice rolls.
|
||||
You can roll dice of different sizes.
|
||||
You can use multiple tools in parallel by calling functions in parallel(in one request and in one round).
|
||||
It is ok to discuss previous dice roles, and comment on the dice rolls.
|
||||
When you are asked to roll a die, you must call the roll_die tool with the number of sides. Be sure to pass in an integer. Do not pass in a string.
|
||||
You should never roll a die on your own.
|
||||
When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string.
|
||||
You should not check prime numbers before calling the tool.
|
||||
When you are asked to roll a die and check prime numbers, you should always make the following two function calls:
|
||||
1. You should first call the roll_die tool to get a roll. Wait for the function response before calling the check_prime tool.
|
||||
2. After you get the function response from roll_die tool, you should call the check_prime tool with the roll_die result.
|
||||
2.1 If user asks you to check primes based on previous rolls, make sure you include the previous rolls in the list.
|
||||
3. When you respond, you must include the roll_die result from step 1.
|
||||
You should always perform the previous 3 steps when asking for a roll and checking prime numbers.
|
||||
You should not rely on the previous history on prime results.
|
||||
""",
|
||||
tools=[
|
||||
roll_die,
|
||||
check_prime,
|
||||
],
|
||||
)
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
# 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
|
||||
#
|
||||
# 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 asyncio
|
||||
import time
|
||||
import warnings
|
||||
|
||||
import agent
|
||||
from dotenv import load_dotenv
|
||||
from google.adk import Runner
|
||||
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from google.adk.cli.utils import logs
|
||||
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
||||
from google.adk.sessions.session import Session
|
||||
from google.genai import types
|
||||
|
||||
load_dotenv(override=True)
|
||||
warnings.filterwarnings('ignore', category=UserWarning)
|
||||
logs.log_to_tmp_folder()
|
||||
|
||||
|
||||
async def main():
|
||||
app_name = 'my_app'
|
||||
user_id_1 = 'user1'
|
||||
session_service = InMemorySessionService()
|
||||
artifact_service = InMemoryArtifactService()
|
||||
runner = Runner(
|
||||
app_name=app_name,
|
||||
agent=agent.root_agent,
|
||||
artifact_service=artifact_service,
|
||||
session_service=session_service,
|
||||
)
|
||||
session_11 = await session_service.create_session(
|
||||
app_name=app_name, user_id=user_id_1
|
||||
)
|
||||
|
||||
async def run_prompt(session: Session, new_message: str):
|
||||
content = types.Content(
|
||||
role='user', parts=[types.Part.from_text(text=new_message)]
|
||||
)
|
||||
print('** User says:', content.model_dump(exclude_none=True))
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id_1,
|
||||
session_id=session.id,
|
||||
new_message=content,
|
||||
):
|
||||
if event.content.parts and event.content.parts[0].text:
|
||||
print(f'** {event.author}: {event.content.parts[0].text}')
|
||||
|
||||
start_time = time.time()
|
||||
print('Start time:', start_time)
|
||||
print('------------------------------------')
|
||||
await run_prompt(session_11, 'Hi, introduce yourself.')
|
||||
await run_prompt(
|
||||
session_11, 'Roll a die with 100 sides and check if it is prime'
|
||||
)
|
||||
await run_prompt(session_11, 'Roll it again.')
|
||||
await run_prompt(session_11, 'What numbers did I get?')
|
||||
end_time = time.time()
|
||||
print('------------------------------------')
|
||||
print('End time:', end_time)
|
||||
print('Total time:', end_time - start_time)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,155 @@
|
||||
# Interactions API Sample Agent
|
||||
|
||||
This sample agent demonstrates the Interactions API integration in ADK. The
|
||||
Interactions API provides stateful conversation capabilities, allowing chained
|
||||
interactions using `previous_interaction_id` instead of sending full
|
||||
conversation history.
|
||||
|
||||
## Features Tested
|
||||
|
||||
1. **Basic Text Generation** - Simple conversation without tools
|
||||
1. **Google Search Tool** - Web search using `GoogleSearchTool` with
|
||||
`bypass_multi_tools_limit=True`
|
||||
1. **Multi-Turn Conversations** - Stateful interactions with context retention
|
||||
via `previous_interaction_id`
|
||||
1. **Custom Function Tool** - Weather lookup using `get_current_weather`
|
||||
|
||||
## Important: Tool Compatibility
|
||||
|
||||
The Interactions API does **NOT** support mixing custom function calling tools
|
||||
with built-in tools (like `google_search`) in the same agent. To work around
|
||||
this limitation:
|
||||
|
||||
```python
|
||||
# Use bypass_multi_tools_limit=True to convert google_search to a function tool
|
||||
GoogleSearchTool(bypass_multi_tools_limit=True)
|
||||
```
|
||||
|
||||
This converts the built-in `google_search` to a function calling tool (via
|
||||
`GoogleSearchAgentTool`), which allows it to work alongside custom function
|
||||
tools.
|
||||
|
||||
## How to Run
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
# From the adk-python root directory
|
||||
uv sync --all-extras
|
||||
source .venv/bin/activate
|
||||
|
||||
# Set up authentication (choose one):
|
||||
# Option 1: Using Google Cloud credentials
|
||||
export GOOGLE_CLOUD_PROJECT=your-project-id
|
||||
|
||||
# Option 2: Using API Key
|
||||
export GOOGLE_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
cd contributing/samples
|
||||
|
||||
# Run automated tests with Interactions API
|
||||
python -m interactions_api.main
|
||||
```
|
||||
|
||||
## Key Differences: Interactions API vs Standard API
|
||||
|
||||
### Interactions API (`use_interactions_api=True`)
|
||||
|
||||
- Uses stateful interactions via `previous_interaction_id`
|
||||
- Only sends current turn contents when chaining interactions
|
||||
- Returns `interaction_id` in responses for chaining
|
||||
- Ideal for long conversations with many turns
|
||||
- Context caching is not used (state maintained via interaction chaining)
|
||||
|
||||
### Standard API (`use_interactions_api=False`)
|
||||
|
||||
- Uses stateless `generate_content` calls
|
||||
- Sends full conversation history with each request
|
||||
- No interaction IDs in responses
|
||||
- Context caching can be used
|
||||
|
||||
## Code Structure
|
||||
|
||||
```
|
||||
interactions_api/
|
||||
├── __init__.py # Package initialization
|
||||
├── agent.py # Agent definition with Interactions API
|
||||
├── main.py # Test runner
|
||||
├── test_interactions_curl.sh # cURL-based API tests
|
||||
├── test_interactions_direct.py # Direct API tests
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Agent Configuration
|
||||
|
||||
```python
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
from google.adk.models.google_llm import Gemini
|
||||
from google.adk.tools.google_search_tool import GoogleSearchTool
|
||||
|
||||
root_agent = Agent(
|
||||
model=Gemini(
|
||||
model="gemini-2.5-flash",
|
||||
use_interactions_api=True, # Enable Interactions API
|
||||
),
|
||||
name="interactions_test_agent",
|
||||
tools=[
|
||||
GoogleSearchTool(bypass_multi_tools_limit=True), # Converted to function tool
|
||||
get_current_weather, # Custom function tool
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
============================================================
|
||||
TEST 1: Basic Text Generation
|
||||
============================================================
|
||||
|
||||
>> User: Hello! What can you help me with?
|
||||
<< Agent: Hello! I can help you with: 1) Search the web...
|
||||
[Interaction ID: v1_abc123...]
|
||||
PASSED: Basic text generation works
|
||||
|
||||
============================================================
|
||||
TEST 2: Function Calling (Google Search Tool)
|
||||
============================================================
|
||||
|
||||
>> User: Search for the capital of France.
|
||||
[Tool Call] google_search_agent({'request': 'capital of France'})
|
||||
[Tool Result] google_search_agent: {'result': 'The capital of France is Paris...'}
|
||||
<< Agent: The capital of France is Paris.
|
||||
[Interaction ID: v1_def456...]
|
||||
PASSED: Google search tool works
|
||||
|
||||
============================================================
|
||||
TEST 3: Multi-Turn Conversation (Stateful)
|
||||
============================================================
|
||||
|
||||
>> User: Remember the number 42.
|
||||
<< Agent: I'll remember that number - 42.
|
||||
[Interaction ID: v1_ghi789...]
|
||||
|
||||
>> User: What number did I ask you to remember?
|
||||
<< Agent: You asked me to remember the number 42.
|
||||
[Interaction ID: v1_jkl012...]
|
||||
PASSED: Multi-turn conversation works with context retention
|
||||
|
||||
============================================================
|
||||
TEST 5: Custom Function Tool (get_current_weather)
|
||||
============================================================
|
||||
|
||||
>> User: What's the weather like in Tokyo?
|
||||
[Tool Call] get_current_weather({'city': 'Tokyo'})
|
||||
[Tool Result] get_current_weather: {'city': 'Tokyo', 'temperature_f': 68, ...}
|
||||
<< Agent: The weather in Tokyo is 68F and Partly Cloudy.
|
||||
[Interaction ID: v1_mno345...]
|
||||
PASSED: Custom function tool works with bypass_multi_tools_limit
|
||||
|
||||
ALL TESTS PASSED (Interactions API)
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
# 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
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Sample agent for testing the Interactions API integration."""
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,90 @@
|
||||
# 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
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Agent definition for testing the Interactions API integration."""
|
||||
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
from google.adk.models.google_llm import Gemini
|
||||
from google.adk.tools.google_search_tool import GoogleSearchTool
|
||||
|
||||
|
||||
def get_current_weather(city: str) -> dict:
|
||||
"""Get the current weather for a city.
|
||||
|
||||
This is a mock implementation for testing purposes.
|
||||
|
||||
Args:
|
||||
city: The name of the city to get weather for.
|
||||
|
||||
Returns:
|
||||
A dictionary containing weather information.
|
||||
"""
|
||||
# Mock weather data for testing
|
||||
weather_data = {
|
||||
"new york": {"temperature": 72, "condition": "Sunny", "humidity": 45},
|
||||
"london": {"temperature": 59, "condition": "Cloudy", "humidity": 78},
|
||||
"tokyo": {
|
||||
"temperature": 68,
|
||||
"condition": "Partly Cloudy",
|
||||
"humidity": 60,
|
||||
},
|
||||
"paris": {"temperature": 64, "condition": "Rainy", "humidity": 85},
|
||||
"sydney": {"temperature": 77, "condition": "Clear", "humidity": 55},
|
||||
}
|
||||
|
||||
city_lower = city.lower()
|
||||
if city_lower in weather_data:
|
||||
data = weather_data[city_lower]
|
||||
return {
|
||||
"city": city,
|
||||
"temperature_f": data["temperature"],
|
||||
"condition": data["condition"],
|
||||
"humidity": data["humidity"],
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"city": city,
|
||||
"temperature_f": 70,
|
||||
"condition": "Unknown",
|
||||
"humidity": 50,
|
||||
"note": "Weather data not available, using defaults",
|
||||
}
|
||||
|
||||
|
||||
# Main agent with google_search built-in tool and custom function tools
|
||||
#
|
||||
# NOTE: code_executor is not compatible with function calling mode because the model
|
||||
# tries to call a function (e.g., run_code) instead of outputting code in markdown.
|
||||
root_agent = Agent(
|
||||
model=Gemini(
|
||||
model="gemini-3.1-flash-lite",
|
||||
use_interactions_api=True,
|
||||
),
|
||||
name="interactions_test_agent",
|
||||
description="An agent for testing the Interactions API integration",
|
||||
instruction="""You are a helpful assistant that can:
|
||||
|
||||
1. Search the web for information using google_search
|
||||
2. Get weather information using get_current_weather
|
||||
|
||||
When users ask for information that requires searching, use google_search.
|
||||
When users ask about weather, use get_current_weather.
|
||||
|
||||
Be concise and helpful in your responses. Always confirm what you did.
|
||||
""",
|
||||
tools=[
|
||||
GoogleSearchTool(),
|
||||
get_current_weather,
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,452 @@
|
||||
# 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
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Main script for testing the Interactions API integration.
|
||||
|
||||
This script tests the following features:
|
||||
1. Basic text generation
|
||||
2. Google Search tool
|
||||
3. Multi-turn conversations with stateful interactions
|
||||
4. Google Search tool (additional coverage)
|
||||
5. Custom function tool (get_current_weather)
|
||||
|
||||
NOTE: Code execution via UnsafeLocalCodeExecutor is not compatible with function
|
||||
calling mode because the model tries to call a function instead of outputting
|
||||
code in markdown.
|
||||
|
||||
Run with:
|
||||
cd contributing/samples
|
||||
python -m interactions_api_test.main
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from google.adk.agents.run_config import RunConfig
|
||||
from google.adk.cli.utils import logs
|
||||
from google.adk.runners import InMemoryRunner
|
||||
from google.adk.runners import Runner
|
||||
from google.genai import types
|
||||
import httpx
|
||||
|
||||
from .agent import root_agent
|
||||
|
||||
# Load .env from the samples directory (parent of this module's directory)
|
||||
_env_path = Path(__file__).parent.parent / ".env"
|
||||
load_dotenv(_env_path)
|
||||
|
||||
APP_NAME = "interactions_api_test_app"
|
||||
USER_ID = "test_user"
|
||||
|
||||
|
||||
async def call_agent_async(
|
||||
runner: Runner,
|
||||
user_id: str,
|
||||
session_id: str,
|
||||
prompt: str,
|
||||
agent_name: str = "",
|
||||
show_interaction_id: bool = True,
|
||||
additional_parts: list[types.Part] | None = None,
|
||||
) -> tuple[str, str | None]:
|
||||
"""Call the agent asynchronously with the user's prompt.
|
||||
|
||||
Args:
|
||||
runner: The agent runner
|
||||
user_id: The user ID
|
||||
session_id: The session ID
|
||||
prompt: The prompt to send
|
||||
agent_name: The expected agent name for filtering responses
|
||||
show_interaction_id: Whether to show interaction IDs in output
|
||||
additional_parts: Optional list of additional content parts (e.g. files)
|
||||
|
||||
Returns:
|
||||
A tuple of (response_text, interaction_id)
|
||||
"""
|
||||
parts = [types.Part.from_text(text=prompt)]
|
||||
if additional_parts:
|
||||
parts.extend(additional_parts)
|
||||
|
||||
content = types.Content(role="user", parts=parts)
|
||||
|
||||
final_response_text = ""
|
||||
last_interaction_id = None
|
||||
|
||||
print(f"\n>> User: {prompt}")
|
||||
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
new_message=content,
|
||||
run_config=RunConfig(save_input_blobs_as_artifacts=False),
|
||||
):
|
||||
# Track interaction ID if available
|
||||
if event.interaction_id:
|
||||
last_interaction_id = event.interaction_id
|
||||
|
||||
# Show function calls
|
||||
if event.get_function_calls():
|
||||
for fc in event.get_function_calls():
|
||||
print(f" [Tool Call] {fc.name}({fc.args})")
|
||||
|
||||
# Show function responses
|
||||
if event.get_function_responses():
|
||||
for fr in event.get_function_responses():
|
||||
print(f" [Tool Result] {fr.name}: {fr.response}")
|
||||
|
||||
# Collect text responses from the agent (not user, not partial)
|
||||
if (
|
||||
event.content
|
||||
and event.content.parts
|
||||
and event.author != "user"
|
||||
and not event.partial
|
||||
):
|
||||
for part in event.content.parts:
|
||||
if part.text:
|
||||
# Filter by agent name if provided, otherwise accept any non-user
|
||||
if not agent_name or event.author == agent_name:
|
||||
final_response_text += part.text
|
||||
|
||||
print(f"<< Agent: {final_response_text}")
|
||||
if show_interaction_id and last_interaction_id:
|
||||
print(f" [Interaction ID: {last_interaction_id}]")
|
||||
|
||||
return final_response_text, last_interaction_id
|
||||
|
||||
|
||||
async def test_basic_text_generation(runner: Runner, session_id: str):
|
||||
"""Test basic text generation without tools."""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 1: Basic Text Generation")
|
||||
print("=" * 60)
|
||||
|
||||
response, interaction_id = await call_agent_async(
|
||||
runner, USER_ID, session_id, "Hello! What can you help me with?"
|
||||
)
|
||||
|
||||
assert response, "Expected a non-empty response"
|
||||
print("PASSED: Basic text generation works")
|
||||
return interaction_id
|
||||
|
||||
|
||||
async def test_function_calling(runner: Runner, session_id: str):
|
||||
"""Test function calling with the google_search tool."""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 2: Function Calling (Google Search Tool)")
|
||||
print("=" * 60)
|
||||
|
||||
response, interaction_id = await call_agent_async(
|
||||
runner,
|
||||
USER_ID,
|
||||
session_id,
|
||||
"Search for the capital of France.",
|
||||
)
|
||||
|
||||
assert response, "Expected a non-empty response"
|
||||
assert "paris" in response.lower(), f"Expected Paris in response: {response}"
|
||||
print("PASSED: Google search tool works")
|
||||
return interaction_id
|
||||
|
||||
|
||||
async def test_multi_turn_conversation(runner: Runner, session_id: str):
|
||||
"""Test multi-turn conversation to verify stateful interactions."""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 3: Multi-Turn Conversation (Stateful)")
|
||||
print("=" * 60)
|
||||
|
||||
# Turn 1: Tell the agent a fact directly (test conversation memory)
|
||||
response1, id1 = await call_agent_async(
|
||||
runner,
|
||||
USER_ID,
|
||||
session_id,
|
||||
"My favorite color is blue. Just acknowledge this, don't use any tools.",
|
||||
)
|
||||
assert response1, "Expected a response for turn 1"
|
||||
print(f" Turn 1 interaction_id: {id1}")
|
||||
|
||||
# Turn 2: Ask about something else (use weather tool to add variety)
|
||||
response2, id2 = await call_agent_async(
|
||||
runner,
|
||||
USER_ID,
|
||||
session_id,
|
||||
"What's the weather like in London?",
|
||||
)
|
||||
assert response2, "Expected a response for turn 2"
|
||||
assert (
|
||||
"59" in response2
|
||||
or "london" in response2.lower()
|
||||
or "cloudy" in response2.lower()
|
||||
), f"Expected London weather info in response: {response2}"
|
||||
print(f" Turn 2 interaction_id: {id2}")
|
||||
|
||||
# Turn 3: Ask the agent to recall conversation context
|
||||
response3, id3 = await call_agent_async(
|
||||
runner,
|
||||
USER_ID,
|
||||
session_id,
|
||||
"What is my favorite color that I mentioned earlier in our conversation?",
|
||||
)
|
||||
assert response3, "Expected a response for turn 3"
|
||||
assert (
|
||||
"blue" in response3.lower()
|
||||
), f"Expected agent to remember the color 'blue': {response3}"
|
||||
print(f" Turn 3 interaction_id: {id3}")
|
||||
|
||||
# Verify interaction IDs are different (new interactions) but chained
|
||||
if id1 and id2 and id3:
|
||||
print(f" Interaction chain: {id1} -> {id2} -> {id3}")
|
||||
|
||||
print("PASSED: Multi-turn conversation works with context retention")
|
||||
|
||||
|
||||
async def test_google_search_tool(runner: Runner, session_id: str):
|
||||
"""Test the google_search built-in tool."""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 4: Google Search Tool (Additional)")
|
||||
print("=" * 60)
|
||||
|
||||
response, interaction_id = await call_agent_async(
|
||||
runner,
|
||||
USER_ID,
|
||||
session_id,
|
||||
"Use google search to find out who wrote the novel '1984'.",
|
||||
)
|
||||
|
||||
assert response, "Expected a non-empty response"
|
||||
assert (
|
||||
"orwell" in response.lower() or "george" in response.lower()
|
||||
), f"Expected George Orwell in response: {response}"
|
||||
print("PASSED: Google search built-in tool works")
|
||||
|
||||
|
||||
async def test_custom_function_tool(runner: Runner, session_id: str):
|
||||
"""Test the custom function tool alongside google_search.
|
||||
|
||||
The root_agent has both GoogleSearchTool (with bypass_multi_tools_limit=True)
|
||||
and get_current_weather. This tests that function calling tools work with
|
||||
the Interactions API when all tools are function calling types.
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 5: Custom Function Tool (get_current_weather)")
|
||||
print("=" * 60)
|
||||
|
||||
response, interaction_id = await call_agent_async(
|
||||
runner,
|
||||
USER_ID,
|
||||
session_id,
|
||||
"What's the weather like in Tokyo?",
|
||||
)
|
||||
|
||||
assert response, "Expected a non-empty response"
|
||||
# The mock weather data for Tokyo has temperature 68, condition "Partly Cloudy"
|
||||
assert (
|
||||
"68" in response
|
||||
or "partly" in response.lower()
|
||||
or "tokyo" in response.lower()
|
||||
), f"Expected weather info for Tokyo in response: {response}"
|
||||
print("PASSED: Custom function tool works with bypass_multi_tools_limit")
|
||||
return interaction_id
|
||||
|
||||
|
||||
async def test_pdf_summarization(runner: Runner, session_id: str) -> str | None:
|
||||
"""Test PDF summarization using the Interactions API."""
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST 6: PDF Summarization")
|
||||
print("=" * 60)
|
||||
|
||||
url = "https://storage.googleapis.com/cloud-samples-data/generative-ai/pdf/2403.05530.pdf"
|
||||
print(f"Downloading {url}...")
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
url, headers={"User-Agent": "Mozilla/5.0"}, follow_redirects=True
|
||||
)
|
||||
response.raise_for_status()
|
||||
pdf_bytes = response.content
|
||||
|
||||
pdf_part = types.Part.from_bytes(data=pdf_bytes, mime_type="application/pdf")
|
||||
response, interaction_id = await call_agent_async(
|
||||
runner,
|
||||
USER_ID,
|
||||
session_id,
|
||||
"Please summarize the attached PDF document.",
|
||||
additional_parts=[pdf_part],
|
||||
)
|
||||
|
||||
assert response, "Expected a non-empty response"
|
||||
assert len(response) > 0, f"Expected summary in response: {response}"
|
||||
assert (
|
||||
"gemini" in response.lower() or "multimodal" in response.lower()
|
||||
), f"Expected summary of PDF in response: {response}"
|
||||
print("PASSED: PDF Summarization works")
|
||||
return interaction_id
|
||||
|
||||
|
||||
def check_interactions_api_available() -> bool:
|
||||
"""Check if the interactions API is available in the SDK."""
|
||||
try:
|
||||
from google.genai import Client
|
||||
|
||||
client = Client()
|
||||
# Check if interactions attribute exists
|
||||
return hasattr(client.aio, "interactions")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
async def run_all_tests():
|
||||
"""Run all tests with the Interactions API."""
|
||||
print("\n" + "#" * 70)
|
||||
print("# Running tests with Interactions API")
|
||||
print("#" * 70)
|
||||
|
||||
# Check if interactions API is available
|
||||
if not check_interactions_api_available():
|
||||
print("\nERROR: Interactions API is not available in the current SDK.")
|
||||
print("The interactions API requires a SDK version with this feature.")
|
||||
print("To use the interactions API, ensure you have the SDK with")
|
||||
print("interactions support installed (e.g., from private-python-genai).")
|
||||
return False
|
||||
|
||||
test_agent = root_agent
|
||||
|
||||
runner = InMemoryRunner(
|
||||
agent=test_agent,
|
||||
app_name=APP_NAME,
|
||||
)
|
||||
|
||||
# Create a new session
|
||||
session = await runner.session_service.create_session(
|
||||
user_id=USER_ID,
|
||||
app_name=APP_NAME,
|
||||
)
|
||||
print(f"\nSession created: {session.id}")
|
||||
|
||||
try:
|
||||
# Run all tests
|
||||
await test_basic_text_generation(runner, session.id)
|
||||
await test_function_calling(runner, session.id)
|
||||
await test_multi_turn_conversation(runner, session.id)
|
||||
await test_google_search_tool(runner, session.id)
|
||||
await test_custom_function_tool(runner, session.id)
|
||||
await test_pdf_summarization(runner, session.id)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("ALL TESTS PASSED (Interactions API)")
|
||||
print("=" * 60)
|
||||
return True
|
||||
|
||||
except AssertionError as e:
|
||||
print(f"\nTEST FAILED: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"\nERROR: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
async def interactive_mode():
|
||||
"""Run in interactive mode for manual testing."""
|
||||
# Check if interactions API is available
|
||||
if not check_interactions_api_available():
|
||||
print("\nERROR: Interactions API is not available in the current SDK.")
|
||||
print("To use the interactions API, ensure you have the SDK with")
|
||||
print("interactions support installed (e.g., from private-python-genai).")
|
||||
return
|
||||
|
||||
print("\nInteractive mode with Interactions API")
|
||||
print("Type 'quit' to exit, 'new' for a new session\n")
|
||||
|
||||
test_agent = agent.root_agent
|
||||
|
||||
runner = InMemoryRunner(
|
||||
agent=test_agent,
|
||||
app_name=APP_NAME,
|
||||
)
|
||||
|
||||
session = await runner.session_service.create_session(
|
||||
user_id=USER_ID,
|
||||
app_name=APP_NAME,
|
||||
)
|
||||
print(f"Session created: {session.id}\n")
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("You: ").strip()
|
||||
if not user_input:
|
||||
continue
|
||||
if user_input.lower() == "quit":
|
||||
break
|
||||
if user_input.lower() == "new":
|
||||
session = await runner.session_service.create_session(
|
||||
user_id=USER_ID,
|
||||
app_name=APP_NAME,
|
||||
)
|
||||
print(f"New session created: {session.id}\n")
|
||||
continue
|
||||
|
||||
await call_agent_async(runner, USER_ID, session.id, user_input)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
|
||||
print("\nGoodbye!")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Test the Interactions API integration"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=["test", "interactive"],
|
||||
default="test",
|
||||
help=(
|
||||
"Run mode: 'test' runs automated tests, 'interactive' for manual"
|
||||
" testing"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--debug",
|
||||
action="store_true",
|
||||
help="Enable debug logging",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.debug:
|
||||
logs.setup_adk_logger(level=logging.DEBUG)
|
||||
else:
|
||||
logs.setup_adk_logger(level=logging.INFO)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
if args.mode == "test":
|
||||
success = asyncio.run(run_all_tests())
|
||||
if not success:
|
||||
exit(1)
|
||||
|
||||
elif args.mode == "interactive":
|
||||
asyncio.run(interactive_mode())
|
||||
|
||||
end_time = time.time()
|
||||
print(f"\nTotal execution time: {end_time - start_time:.2f} seconds")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Hello! What can you help me with?"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "interactions_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Hello! I am \"interactions_test_agent,\" an agent designed for testing the Interactions API integration.\n\nI can help you with two main tasks:\n1. **Searching the web:** If you have questions that require up-to-date information, I can search the web for you.\n2. **Checking the weather:** I can provide the current weather for a specific city.\n\nHow can I assist you today?"
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "interactions_test_agent@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "What's the weather like in Tokyo?"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "interactions_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"city": "Tokyo"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "get_current_weather"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "interactions_test_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "interactions_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "get_current_weather",
|
||||
"response": {
|
||||
"city": "Tokyo",
|
||||
"condition": "Partly Cloudy",
|
||||
"humidity": 60,
|
||||
"temperature_f": 68
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "interactions_test_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "interactions_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I have retrieved the weather information for Tokyo. It is currently 68\u00b0F and partly cloudy with 60% humidity."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "interactions_test_agent@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Use google search to find out who wrote the novel '1984'."
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "interactions_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "search_suggestions='<style>\\n.container {\\n align-items: center;\\n border-radius: 8px;\\n display: flex;\\n font-family: Google Sans, Roboto, sans-serif;\\n font-size: 14px;\\n line-height: 20px;\\n padding: 8px 12px;\\n}\\n.chip {\\n display: inline-block;\\n border: solid 1px;\\n border-radius: 16px;\\n min-width: 14px;\\n padding: 5px 16px;\\n text-align: center;\\n user-select: none;\\n margin: 0 8px;\\n -webkit-tap-highlight-color: transparent;\\n}\\n.carousel {\\n overflow: auto;\\n scrollbar-width: none;\\n white-space: nowrap;\\n margin-right: -12px;\\n}\\n.headline {\\n display: flex;\\n margin-right: 4px;\\n}\\n.gradient-container {\\n position: relative;\\n}\\n.gradient {\\n position: absolute;\\n transform: translate(3px, -9px);\\n height: 36px;\\n width: 9px;\\n}\\n@media (prefers-color-scheme: light) {\\n .container {\\n background-color: #fafafa;\\n box-shadow: 0 0 0 1px #0000000f;\\n }\\n .headline-label {\\n color: #1f1f1f;\\n }\\n .chip {\\n background-color: #ffffff;\\n border-color: #d2d2d2;\\n color: #5e5e5e;\\n text-decoration: none;\\n }\\n .chip:hover {\\n background-color: #f2f2f2;\\n }\\n .chip:focus {\\n background-color: #f2f2f2;\\n }\\n .chip:active {\\n background-color: #d8d8d8;\\n border-color: #b6b6b6;\\n }\\n .logo-dark {\\n display: none;\\n }\\n .gradient {\\n background: linear-gradient(90deg, #fafafa 15%, #fafafa00 100%);\\n }\\n}\\n@media (prefers-color-scheme: dark) {\\n .container {\\n background-color: #1f1f1f;\\n box-shadow: 0 0 0 1px #ffffff26;\\n }\\n .headline-label {\\n color: #fff;\\n }\\n .chip {\\n background-color: #2c2c2c;\\n border-color: #3c4043;\\n color: #fff;\\n text-decoration: none;\\n }\\n .chip:hover {\\n background-color: #353536;\\n }\\n .chip:focus {\\n background-color: #353536;\\n }\\n .chip:active {\\n background-color: #464849;\\n border-color: #53575b;\\n }\\n .logo-light {\\n display: none;\\n }\\n .gradient {\\n background: linear-gradient(90deg, #1f1f1f 15%, #1f1f1f00 100%);\\n }\\n}\\n</style>\\n<div class=\"container\">\\n <div class=\"headline\">\\n <svg class=\"logo-light\" width=\"18\" height=\"18\" viewBox=\"9 9 35 35\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M42.8622 27.0064C42.8622 25.7839 42.7525 24.6084 42.5487 23.4799H26.3109V30.1568H35.5897C35.1821 32.3041 33.9596 34.1222 32.1258 35.3448V39.6864H37.7213C40.9814 36.677 42.8622 32.2571 42.8622 27.0064V27.0064Z\" fill=\"#4285F4\"/>\\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M26.3109 43.8555C30.9659 43.8555 34.8687 42.3195 37.7213 39.6863L32.1258 35.3447C30.5898 36.3792 28.6306 37.0061 26.3109 37.0061C21.8282 37.0061 18.0195 33.9811 16.6559 29.906H10.9194V34.3573C13.7563 39.9841 19.5712 43.8555 26.3109 43.8555V43.8555Z\" fill=\"#34A853\"/>\\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M16.6559 29.8904C16.3111 28.8559 16.1074 27.7588 16.1074 26.6146C16.1074 25.4704 16.3111 24.3733 16.6559 23.3388V18.8875H10.9194C9.74388 21.2072 9.06992 23.8247 9.06992 26.6146C9.06992 29.4045 9.74388 32.022 10.9194 34.3417L15.3864 30.8621L16.6559 29.8904V29.8904Z\" fill=\"#FBBC05\"/>\\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M26.3109 16.2386C28.85 16.2386 31.107 17.1164 32.9095 18.8091L37.8466 13.8719C34.853 11.082 30.9659 9.3736 26.3109 9.3736C19.5712 9.3736 13.7563 13.245 10.9194 18.8875L16.6559 23.3388C18.0195 19.2636 21.8282 16.2386 26.3109 16.2386V16.2386Z\" fill=\"#EA4335\"/>\\n </svg>\\n <svg class=\"logo-dark\" width=\"18\" height=\"18\" viewBox=\"0 0 48 48\" xmlns=\"http://www.w3.org/2000/svg\">\\n <circle cx=\"24\" cy=\"23\" fill=\"#FFF\" r=\"22\"/>\\n <path d=\"M33.76 34.26c2.75-2.56 4.49-6.37 4.49-11.26 0-.89-.08-1.84-.29-3H24.01v5.99h8.03c-.4 2.02-1.5 3.56-3.07 4.56v.75l3.91 2.97h.88z\" fill=\"#4285F4\"/>\\n <path d=\"M15.58 25.77A8.845 8.845 0 0 0 24 31.86c1.92 0 3.62-.46 4.97-1.31l4.79 3.71C31.14 36.7 27.65 38 24 38c-5.93 0-11.01-3.4-13.45-8.36l.17-1.01 4.06-2.85h.8z\" fill=\"#34A853\"/>\\n <path d=\"M15.59 20.21a8.864 8.864 0 0 0 0 5.58l-5.03 3.86c-.98-2-1.53-4.25-1.53-6.64 0-2.39.55-4.64 1.53-6.64l1-.22 3.81 2.98.22 1.08z\" fill=\"#FBBC05\"/>\\n <path d=\"M24 14.14c2.11 0 4.02.75 5.52 1.98l4.36-4.36C31.22 9.43 27.81 8 24 8c-5.93 0-11.01 3.4-13.45 8.36l5.03 3.85A8.86 8.86 0 0 1 24 14.14z\" fill=\"#EA4335\"/>\\n </svg>\\n <div class=\"gradient-container\"><div class=\"gradient\"></div></div>\\n </div>\\n <div class=\"carousel\">\\n<a class=\"chip\" href=\"https://www.google.com/search?q=who+wrote+the+novel+1984&client=app-vertex-grounding&safesearch=active\">who wrote the novel 1984</a>\\n </div>\\n</div>'"
|
||||
},
|
||||
{
|
||||
"text": "The novel *1984* was written by the English author **George Orwell** (whose real name was Eric Arthur Blair). It was published in 1949."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "interactions_test_agent@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Search for the capital of France."
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "interactions_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "search_suggestions='<style>\\n.container {\\n align-items: center;\\n border-radius: 8px;\\n display: flex;\\n font-family: Google Sans, Roboto, sans-serif;\\n font-size: 14px;\\n line-height: 20px;\\n padding: 8px 12px;\\n}\\n.chip {\\n display: inline-block;\\n border: solid 1px;\\n border-radius: 16px;\\n min-width: 14px;\\n padding: 5px 16px;\\n text-align: center;\\n user-select: none;\\n margin: 0 8px;\\n -webkit-tap-highlight-color: transparent;\\n}\\n.carousel {\\n overflow: auto;\\n scrollbar-width: none;\\n white-space: nowrap;\\n margin-right: -12px;\\n}\\n.headline {\\n display: flex;\\n margin-right: 4px;\\n}\\n.gradient-container {\\n position: relative;\\n}\\n.gradient {\\n position: absolute;\\n transform: translate(3px, -9px);\\n height: 36px;\\n width: 9px;\\n}\\n@media (prefers-color-scheme: light) {\\n .container {\\n background-color: #fafafa;\\n box-shadow: 0 0 0 1px #0000000f;\\n }\\n .headline-label {\\n color: #1f1f1f;\\n }\\n .chip {\\n background-color: #ffffff;\\n border-color: #d2d2d2;\\n color: #5e5e5e;\\n text-decoration: none;\\n }\\n .chip:hover {\\n background-color: #f2f2f2;\\n }\\n .chip:focus {\\n background-color: #f2f2f2;\\n }\\n .chip:active {\\n background-color: #d8d8d8;\\n border-color: #b6b6b6;\\n }\\n .logo-dark {\\n display: none;\\n }\\n .gradient {\\n background: linear-gradient(90deg, #fafafa 15%, #fafafa00 100%);\\n }\\n}\\n@media (prefers-color-scheme: dark) {\\n .container {\\n background-color: #1f1f1f;\\n box-shadow: 0 0 0 1px #ffffff26;\\n }\\n .headline-label {\\n color: #fff;\\n }\\n .chip {\\n background-color: #2c2c2c;\\n border-color: #3c4043;\\n color: #fff;\\n text-decoration: none;\\n }\\n .chip:hover {\\n background-color: #353536;\\n }\\n .chip:focus {\\n background-color: #353536;\\n }\\n .chip:active {\\n background-color: #464849;\\n border-color: #53575b;\\n }\\n .logo-light {\\n display: none;\\n }\\n .gradient {\\n background: linear-gradient(90deg, #1f1f1f 15%, #1f1f1f00 100%);\\n }\\n}\\n</style>\\n<div class=\"container\">\\n <div class=\"headline\">\\n <svg class=\"logo-light\" width=\"18\" height=\"18\" viewBox=\"9 9 35 35\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M42.8622 27.0064C42.8622 25.7839 42.7525 24.6084 42.5487 23.4799H26.3109V30.1568H35.5897C35.1821 32.3041 33.9596 34.1222 32.1258 35.3448V39.6864H37.7213C40.9814 36.677 42.8622 32.2571 42.8622 27.0064V27.0064Z\" fill=\"#4285F4\"/>\\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M26.3109 43.8555C30.9659 43.8555 34.8687 42.3195 37.7213 39.6863L32.1258 35.3447C30.5898 36.3792 28.6306 37.0061 26.3109 37.0061C21.8282 37.0061 18.0195 33.9811 16.6559 29.906H10.9194V34.3573C13.7563 39.9841 19.5712 43.8555 26.3109 43.8555V43.8555Z\" fill=\"#34A853\"/>\\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M16.6559 29.8904C16.3111 28.8559 16.1074 27.7588 16.1074 26.6146C16.1074 25.4704 16.3111 24.3733 16.6559 23.3388V18.8875H10.9194C9.74388 21.2072 9.06992 23.8247 9.06992 26.6146C9.06992 29.4045 9.74388 32.022 10.9194 34.3417L15.3864 30.8621L16.6559 29.8904V29.8904Z\" fill=\"#FBBC05\"/>\\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M26.3109 16.2386C28.85 16.2386 31.107 17.1164 32.9095 18.8091L37.8466 13.8719C34.853 11.082 30.9659 9.3736 26.3109 9.3736C19.5712 9.3736 13.7563 13.245 10.9194 18.8875L16.6559 23.3388C18.0195 19.2636 21.8282 16.2386 26.3109 16.2386V16.2386Z\" fill=\"#EA4335\"/>\\n </svg>\\n <svg class=\"logo-dark\" width=\"18\" height=\"18\" viewBox=\"0 0 48 48\" xmlns=\"http://www.w3.org/2000/svg\">\\n <circle cx=\"24\" cy=\"23\" fill=\"#FFF\" r=\"22\"/>\\n <path d=\"M33.76 34.26c2.75-2.56 4.49-6.37 4.49-11.26 0-.89-.08-1.84-.29-3H24.01v5.99h8.03c-.4 2.02-1.5 3.56-3.07 4.56v.75l3.91 2.97h.88z\" fill=\"#4285F4\"/>\\n <path d=\"M15.58 25.77A8.845 8.845 0 0 0 24 31.86c1.92 0 3.62-.46 4.97-1.31l4.79 3.71C31.14 36.7 27.65 38 24 38c-5.93 0-11.01-3.4-13.45-8.36l.17-1.01 4.06-2.85h.8z\" fill=\"#34A853\"/>\\n <path d=\"M15.59 20.21a8.864 8.864 0 0 0 0 5.58l-5.03 3.86c-.98-2-1.53-4.25-1.53-6.64 0-2.39.55-4.64 1.53-6.64l1-.22 3.81 2.98.22 1.08z\" fill=\"#FBBC05\"/>\\n <path d=\"M24 14.14c2.11 0 4.02.75 5.52 1.98l4.36-4.36C31.22 9.43 27.81 8 24 8c-5.93 0-11.01 3.4-13.45 8.36l5.03 3.85A8.86 8.86 0 0 1 24 14.14z\" fill=\"#EA4335\"/>\\n </svg>\\n <div class=\"gradient-container\"><div class=\"gradient\"></div></div>\\n </div>\\n <div class=\"carousel\">\\n<a class=\"chip\" href=\"https://www.google.com/search?q=capital+of+France&client=app-vertex-grounding&safesearch=active\">capital of France</a>\\n </div>\\n</div>'"
|
||||
},
|
||||
{
|
||||
"text": "I have searched for the capital of France and confirmed that it is Paris."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "interactions_test_agent@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "My favorite color is blue. Just acknowledge this, don't use any tools."
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-1",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "interactions_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I acknowledge that your favorite color is blue."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-2",
|
||||
"invocationId": "i-1",
|
||||
"nodeInfo": {
|
||||
"path": "interactions_test_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "What's the weather like in London?"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-3",
|
||||
"invocationId": "i-2",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "interactions_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionCall": {
|
||||
"args": {
|
||||
"city": "London"
|
||||
},
|
||||
"id": "fc-1",
|
||||
"name": "get_current_weather"
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-4",
|
||||
"invocationId": "i-2",
|
||||
"longRunningToolIds": [],
|
||||
"nodeInfo": {
|
||||
"path": "interactions_test_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "interactions_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"functionResponse": {
|
||||
"id": "fc-1",
|
||||
"name": "get_current_weather",
|
||||
"response": {
|
||||
"city": "London",
|
||||
"condition": "Cloudy",
|
||||
"humidity": 78,
|
||||
"temperature_f": 59
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-5",
|
||||
"invocationId": "i-2",
|
||||
"nodeInfo": {
|
||||
"path": "interactions_test_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "interactions_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "I have checked the weather in London for you. It is currently cloudy with a temperature of 59\u00b0F and 78% humidity."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-6",
|
||||
"invocationId": "i-2",
|
||||
"nodeInfo": {
|
||||
"path": "interactions_test_agent@1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "user",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "What is my favorite color that I mentioned earlier in our conversation?"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
},
|
||||
"id": "e-7",
|
||||
"invocationId": "i-3",
|
||||
"nodeInfo": {
|
||||
"path": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"author": "interactions_test_agent",
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"text": "Your favorite color, which you mentioned earlier, is blue."
|
||||
}
|
||||
],
|
||||
"role": "model"
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"id": "e-8",
|
||||
"invocationId": "i-3",
|
||||
"nodeInfo": {
|
||||
"path": "interactions_test_agent@1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# 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
|
||||
#
|
||||
# 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 __future__ import annotations
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,174 @@
|
||||
# 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
|
||||
#
|
||||
# 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 __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
from zoneinfo import ZoneInfo
|
||||
from zoneinfo import ZoneInfoNotFoundError
|
||||
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
from google.adk.models.lite_llm import LiteLlm
|
||||
from google.adk.models.lite_llm import LiteLLMClient
|
||||
|
||||
|
||||
class InlineJsonToolClient(LiteLLMClient):
|
||||
"""LiteLLM client that emits inline JSON tool calls for testing."""
|
||||
|
||||
async def acompletion(self, model, messages, tools, **kwargs):
|
||||
del tools, kwargs # Only needed for API parity.
|
||||
|
||||
tool_message = _find_last_role(messages, role="tool")
|
||||
if tool_message:
|
||||
tool_summary = _coerce_to_text(tool_message.get("content"))
|
||||
return {
|
||||
"id": "mock-inline-tool-final-response",
|
||||
"model": model,
|
||||
"choices": [{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": (
|
||||
f"The instrumentation tool responded with: {tool_summary}"
|
||||
),
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}],
|
||||
"usage": {
|
||||
"prompt_tokens": 60,
|
||||
"completion_tokens": 12,
|
||||
"total_tokens": 72,
|
||||
},
|
||||
}
|
||||
|
||||
timezone = _extract_timezone(messages) or "Asia/Taipei"
|
||||
inline_call = json.dumps(
|
||||
{
|
||||
"name": "get_current_time",
|
||||
"arguments": {"timezone_str": timezone},
|
||||
},
|
||||
separators=(",", ":"),
|
||||
)
|
||||
|
||||
return {
|
||||
"id": "mock-inline-tool-call",
|
||||
"model": model,
|
||||
"choices": [{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": (
|
||||
f"{inline_call}\nLet me double-check the clock for you."
|
||||
),
|
||||
},
|
||||
"finish_reason": "tool_calls",
|
||||
}],
|
||||
"usage": {
|
||||
"prompt_tokens": 45,
|
||||
"completion_tokens": 15,
|
||||
"total_tokens": 60,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _find_last_role(
|
||||
messages: list[dict[str, Any]], role: str
|
||||
) -> dict[str, Any]:
|
||||
"""Returns the last message with the given role."""
|
||||
for message in reversed(messages):
|
||||
if message.get("role") == role:
|
||||
return message
|
||||
return {}
|
||||
|
||||
|
||||
def _coerce_to_text(content: Any) -> str:
|
||||
"""Best-effort conversion from OpenAI message content to text."""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if isinstance(content, dict):
|
||||
return _coerce_to_text(content.get("text"))
|
||||
if isinstance(content, list):
|
||||
texts = []
|
||||
for part in content:
|
||||
if isinstance(part, dict):
|
||||
texts.append(part.get("text") or "")
|
||||
elif isinstance(part, str):
|
||||
texts.append(part)
|
||||
return " ".join(text for text in texts if text)
|
||||
return ""
|
||||
|
||||
|
||||
_TIMEZONE_PATTERN = re.compile(r"([A-Za-z]+/[A-Za-z_]+)")
|
||||
|
||||
|
||||
def _extract_timezone(messages: list[dict[str, Any]]) -> str | None:
|
||||
"""Extracts an IANA timezone string from the last user message."""
|
||||
user_message = _find_last_role(messages, role="user")
|
||||
text = _coerce_to_text(user_message.get("content"))
|
||||
if not text:
|
||||
return None
|
||||
match = _TIMEZONE_PATTERN.search(text)
|
||||
if match:
|
||||
return match.group(1)
|
||||
lowered = text.lower()
|
||||
if "taipei" in lowered:
|
||||
return "Asia/Taipei"
|
||||
if "new york" in lowered:
|
||||
return "America/New_York"
|
||||
if "london" in lowered:
|
||||
return "Europe/London"
|
||||
if "tokyo" in lowered:
|
||||
return "Asia/Tokyo"
|
||||
return None
|
||||
|
||||
|
||||
def get_current_time(timezone_str: str) -> dict[str, str]:
|
||||
"""Returns mock current time for the provided timezone."""
|
||||
try:
|
||||
tz = ZoneInfo(timezone_str)
|
||||
except ZoneInfoNotFoundError as exc:
|
||||
return {
|
||||
"status": "error",
|
||||
"report": f"Unable to parse timezone '{timezone_str}': {exc}",
|
||||
}
|
||||
now = datetime.datetime.now(tz)
|
||||
return {
|
||||
"status": "success",
|
||||
"report": (
|
||||
f"The current time in {timezone_str} is"
|
||||
f" {now.strftime('%Y-%m-%d %H:%M:%S %Z')}."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
_mock_model = LiteLlm(
|
||||
model="mock/inline-json-tool-calls",
|
||||
llm_client=InlineJsonToolClient(),
|
||||
)
|
||||
|
||||
root_agent = Agent(
|
||||
name="litellm_inline_tool_tester",
|
||||
model=_mock_model,
|
||||
description=(
|
||||
"Demonstrates LiteLLM inline JSON tool-call parsing without an external"
|
||||
" VLLM deployment."
|
||||
),
|
||||
instruction=(
|
||||
"You are a deterministic clock assistant. Always call the"
|
||||
" get_current_time tool before answering user questions. After the tool"
|
||||
" responds, summarize what it returned."
|
||||
),
|
||||
tools=[get_current_time],
|
||||
)
|
||||
@@ -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
|
||||
#
|
||||
# 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,27 @@
|
||||
# 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
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""LiteLLM sample agent for SSE text streaming."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from google.adk import Agent
|
||||
from google.adk.models.lite_llm import LiteLlm
|
||||
|
||||
root_agent = Agent(
|
||||
name='litellm_streaming_agent',
|
||||
model=LiteLlm(model='gemini/gemini-2.5-flash'),
|
||||
description='A LiteLLM agent used for streaming text responses.',
|
||||
instruction='You are a verbose assistant',
|
||||
)
|
||||
@@ -0,0 +1,107 @@
|
||||
# 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
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Runs the LiteLLM streaming sample with SSE enabled."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from google.adk.agents.run_config import RunConfig
|
||||
from google.adk.agents.run_config import StreamingMode
|
||||
from google.adk.cli.utils import logs
|
||||
from google.adk.runners import InMemoryRunner
|
||||
from google.genai import types
|
||||
|
||||
from . import agent
|
||||
|
||||
load_dotenv(override=True)
|
||||
logs.log_to_tmp_folder()
|
||||
|
||||
|
||||
async def _run_prompt(
|
||||
*,
|
||||
runner: InMemoryRunner,
|
||||
user_id: str,
|
||||
session_id: str,
|
||||
prompt: str,
|
||||
) -> None:
|
||||
"""Runs one prompt and prints partial chunks in real time."""
|
||||
content = types.Content(
|
||||
role='user',
|
||||
parts=[types.Part.from_text(text=prompt)],
|
||||
)
|
||||
|
||||
print(f'User: {prompt}')
|
||||
print('Agent: ', end='', flush=True)
|
||||
saw_text = False
|
||||
saw_partial_text = False
|
||||
|
||||
# For `adk web`, enable the `Streaming` toggle in the UI to get
|
||||
# partial SSE responses similar to this script.
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
new_message=content,
|
||||
run_config=RunConfig(streaming_mode=StreamingMode.SSE),
|
||||
):
|
||||
if not event.content:
|
||||
continue
|
||||
text = ''.join(part.text for part in event.content.parts if part.text)
|
||||
if not text:
|
||||
continue
|
||||
|
||||
if event.partial:
|
||||
print(text, end='', flush=True)
|
||||
saw_text = True
|
||||
saw_partial_text = True
|
||||
continue
|
||||
|
||||
# With SSE mode, ADK emits a final aggregated event after partial chunks.
|
||||
if not saw_partial_text:
|
||||
print(text, end='', flush=True)
|
||||
saw_text = True
|
||||
|
||||
if saw_text:
|
||||
print()
|
||||
else:
|
||||
print('(no text response)')
|
||||
print('------------------------------------')
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
app_name = 'litellm_streaming_demo'
|
||||
user_id = 'user_1'
|
||||
runner = InMemoryRunner(agent=agent.root_agent, app_name=app_name)
|
||||
session = await runner.session_service.create_session(
|
||||
app_name=app_name,
|
||||
user_id=user_id,
|
||||
)
|
||||
prompts = [
|
||||
'Write an essay about the roman empire',
|
||||
'Now summarize the essay into one sentence.',
|
||||
]
|
||||
|
||||
for prompt in prompts:
|
||||
await _run_prompt(
|
||||
runner=runner,
|
||||
user_id=user_id,
|
||||
session_id=session.id,
|
||||
prompt=prompt,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -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
|
||||
#
|
||||
# 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,47 @@
|
||||
# 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
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Sample agent showing LiteLLM structured output support."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from google.adk import Agent
|
||||
from google.adk.models.lite_llm import LiteLlm
|
||||
from pydantic import BaseModel
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class CitySummary(BaseModel):
|
||||
"""Simple structure used to verify LiteLLM JSON schema handling."""
|
||||
|
||||
city: str = Field(description="Name of the city being described.")
|
||||
highlights: list[str] = Field(
|
||||
description="Bullet points summarising the city's key highlights.",
|
||||
)
|
||||
recommended_visit_length_days: int = Field(
|
||||
description="Recommended number of days for a typical visit.",
|
||||
)
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
name="litellm_structured_output_agent",
|
||||
model=LiteLlm(model="gemini-2.5-flash"),
|
||||
description="Generates structured travel recommendations for a given city.",
|
||||
instruction="""
|
||||
Produce a JSON object that follows the CitySummary schema.
|
||||
Only include fields that appear in the schema and ensure highlights
|
||||
contains short bullet points.
|
||||
""".strip(),
|
||||
output_schema=CitySummary,
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
# LiteLLM with Fallback Models
|
||||
|
||||
This agent is built for resilience using LiteLLM's built-in fallback mechanism. It automatically switches models to guard against common disruptions like token limit errors and connection failures, while ensuring full conversational context is preserved across all model changes.
|
||||
|
||||
To run this example, ensure your .env file includes the following variables:
|
||||
|
||||
```
|
||||
GOOGLE_API_KEY=
|
||||
OPENAI_API_KEY=
|
||||
ANTHROPIC_API_KEY=
|
||||
```
|
||||
@@ -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
|
||||
#
|
||||
# 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 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
|
||||
#
|
||||
# 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 random
|
||||
|
||||
from google.adk import Agent
|
||||
from google.adk.models.lite_llm import LiteLlm
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
from google.genai import types
|
||||
|
||||
|
||||
def roll_die(sides: int, tool_context: ToolContext) -> int:
|
||||
"""Roll a die and return the rolled result.
|
||||
|
||||
Args:
|
||||
sides: The integer number of sides the die has.
|
||||
tool_context: The tool context to use for the die roll.
|
||||
|
||||
Returns:
|
||||
An integer of the result of rolling the die.
|
||||
The result is also stored in the tool context for future use.
|
||||
"""
|
||||
result = random.randint(1, sides)
|
||||
if 'rolls' not in tool_context.state:
|
||||
tool_context.state['rolls'] = []
|
||||
|
||||
tool_context.state['rolls'] = tool_context.state['rolls'] + [result]
|
||||
return result
|
||||
|
||||
|
||||
async def before_model_callback(callback_context, llm_request):
|
||||
print('@before_model_callback')
|
||||
print(f'Beginning model choice: {llm_request.model}')
|
||||
callback_context.state['beginning_model_choice'] = llm_request.model
|
||||
return None
|
||||
|
||||
|
||||
async def after_model_callback(callback_context, llm_response):
|
||||
print('@after_model_callback')
|
||||
print(f'Final model choice: {llm_response.model_version}')
|
||||
callback_context.state['final_model_choice'] = llm_response.model_version
|
||||
return None
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model=LiteLlm(
|
||||
model='gemini/gemini-2.5-pro',
|
||||
fallbacks=[
|
||||
'anthropic/claude-sonnet-4-5-20250929',
|
||||
'openai/gpt-4o',
|
||||
],
|
||||
),
|
||||
name='resilient_agent',
|
||||
description=(
|
||||
'hello world agent that can roll a dice of given number of sides.'
|
||||
),
|
||||
instruction="""
|
||||
You roll dice and answer questions about the outcome of the dice rolls.
|
||||
You can roll dice of different sizes.
|
||||
It is ok to discuss previous dice roles, and comment on the dice rolls.
|
||||
When you are asked to roll a die, you must call the roll_die tool with the number of sides. Be sure to pass in an integer. Do not pass in a string.
|
||||
You should never roll a die on your own.
|
||||
""",
|
||||
tools=[
|
||||
roll_die,
|
||||
],
|
||||
generate_content_config=types.GenerateContentConfig(
|
||||
safety_settings=[
|
||||
types.SafetySetting( # avoid false alarm about rolling dice.
|
||||
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
||||
threshold=types.HarmBlockThreshold.OFF,
|
||||
),
|
||||
]
|
||||
),
|
||||
before_model_callback=before_model_callback,
|
||||
after_model_callback=after_model_callback,
|
||||
)
|
||||
@@ -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
|
||||
#
|
||||
# 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,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
|
||||
#
|
||||
# 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 __future__ import annotations
|
||||
|
||||
from google.adk.agents.llm_agent import LlmAgent
|
||||
from google.adk.agents.sequential_agent import SequentialAgent
|
||||
from google.adk.models.lite_llm import LiteLlm
|
||||
|
||||
ollama_model = LiteLlm(model="ollama_chat/qwen2.5:7b")
|
||||
|
||||
hello_agent = LlmAgent(
|
||||
name="hello_step",
|
||||
instruction="Say hello to the user. Be concise.",
|
||||
model=ollama_model,
|
||||
)
|
||||
|
||||
summarize_agent = LlmAgent(
|
||||
name="summarize_step",
|
||||
instruction="Summarize the previous assistant message in 5 words.",
|
||||
model=ollama_model,
|
||||
)
|
||||
|
||||
root_agent = SequentialAgent(
|
||||
name="ollama_seq_test",
|
||||
description="Two-step sanity check for Ollama LiteLLM chat.",
|
||||
sub_agents=[hello_agent, summarize_agent],
|
||||
)
|
||||
Reference in New Issue
Block a user