diff --git a/.github/workflows/python-unit-tests.yml b/.github/workflows/python-unit-tests.yml index 8f8f46e9..3fc6bd94 100644 --- a/.github/workflows/python-unit-tests.yml +++ b/.github/workflows/python-unit-tests.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - name: Checkout code @@ -48,14 +48,6 @@ jobs: - name: Run unit tests with pytest run: | source .venv/bin/activate - if [[ "${{ matrix.python-version }}" == "3.9" ]]; then - pytest tests/unittests \ - --ignore=tests/unittests/a2a \ - --ignore=tests/unittests/tools/mcp_tool \ - --ignore=tests/unittests/artifacts/test_artifact_service.py \ - --ignore=tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py - else - pytest tests/unittests \ - --ignore=tests/unittests/artifacts/test_artifact_service.py \ - --ignore=tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py - fi \ No newline at end of file + pytest tests/unittests \ + --ignore=tests/unittests/artifacts/test_artifact_service.py \ + --ignore=tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py \ No newline at end of file diff --git a/contributing/samples/migrate_session_db/sample-output/alembic.ini b/contributing/samples/migrate_session_db/sample-output/alembic.ini index 64053209..e346ee8a 100644 --- a/contributing/samples/migrate_session_db/sample-output/alembic.ini +++ b/contributing/samples/migrate_session_db/sample-output/alembic.ini @@ -21,7 +21,7 @@ prepend_sys_path = . # timezone to use when rendering the date within the migration file # as well as the filename. -# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library. +# If specified, requires the python>=3.10 and tzdata library. # Any required deps can installed by adding `alembic[tz]` to the pip requirements # string value is passed to ZoneInfo() # leave blank for localtime diff --git a/contributing/samples/telemetry/main.py b/contributing/samples/telemetry/main.py index e580060d..c6e05f0f 100755 --- a/contributing/samples/telemetry/main.py +++ b/contributing/samples/telemetry/main.py @@ -13,6 +13,7 @@ # limitations under the License. import asyncio +from contextlib import aclosing import os import time @@ -46,19 +47,16 @@ async def main(): role='user', parts=[types.Part.from_text(text=new_message)] ) print('** User says:', content.model_dump(exclude_none=True)) - # TODO - migrate try...finally to contextlib.aclosing after Python 3.9 is - # no longer supported. - agen = runner.run_async( - user_id=user_id_1, - session_id=session.id, - new_message=content, - ) - try: + async with aclosing( + runner.run_async( + user_id=user_id_1, + session_id=session.id, + new_message=content, + ) + ) as agen: async for event in agen: if event.content.parts and event.content.parts[0].text: print(f'** {event.author}: {event.content.parts[0].text}') - finally: - await agen.aclose() async def run_prompt_bytes(session: Session, new_message: str): content = types.Content( @@ -70,20 +68,17 @@ async def main(): ], ) print('** User says:', content.model_dump(exclude_none=True)) - # TODO - migrate try...finally to contextlib.aclosing after Python 3.9 is - # no longer supported. - agen = runner.run_async( - user_id=user_id_1, - session_id=session.id, - new_message=content, - run_config=RunConfig(save_input_blobs_as_artifacts=True), - ) - try: + async with aclosing( + runner.run_async( + user_id=user_id_1, + session_id=session.id, + new_message=content, + run_config=RunConfig(save_input_blobs_as_artifacts=True), + ) + ) as agen: async for event in agen: if event.content.parts and event.content.parts[0].text: print(f'** {event.author}: {event.content.parts[0].text}') - finally: - await agen.aclose() start_time = time.time() print('Start time:', start_time) diff --git a/llms-full.txt b/llms-full.txt index 4c744512..b84e9496 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -5620,7 +5620,7 @@ pip install google-cloud-aiplatform[adk,agent_engines] ``` !!!info - Agent Engine only supported Python version >=3.9 and <=3.12. + Agent Engine only supported Python version >=3.10 and <=3.12. ### Initialization @@ -8073,7 +8073,7 @@ setting up a basic agent with multiple tools, and running it locally either in t This quickstart assumes a local IDE (VS Code, PyCharm, IntelliJ IDEA, etc.) -with Python 3.9+ or Java 17+ and terminal access. This method runs the +with Python 3.10+ or Java 17+ and terminal access. This method runs the application entirely on your machine and is recommended for internal development. ## 1. Set up Environment & Install ADK {#venv-install} @@ -16475,7 +16475,7 @@ This guide covers two primary integration patterns: Before you begin, ensure you have the following set up: * **Set up ADK:** Follow the standard ADK [setup instructions](../get-started/quickstart.md/#venv-install) in the quickstart. -* **Install/update Python/Java:** MCP requires Python version of 3.9 or higher for Python or Java 17+. +* **Install/update Python/Java:** MCP requires Python version of 3.10 or higher for Python or Java 17+. * **Setup Node.js and npx:** **(Python only)** Many community MCP servers are distributed as Node.js packages and run using `npx`. Install Node.js (which includes npx) if you haven't already. For details, see [https://nodejs.org/en](https://nodejs.org/en). * **Verify Installations:** **(Python only)** Confirm `adk` and `npx` are in your PATH within the activated virtual environment: diff --git a/src/google/adk/a2a/converters/part_converter.py b/src/google/adk/a2a/converters/part_converter.py index a21042cc..dfe6f4a0 100644 --- a/src/google/adk/a2a/converters/part_converter.py +++ b/src/google/adk/a2a/converters/part_converter.py @@ -26,23 +26,11 @@ from typing import List from typing import Optional from typing import Union -from .utils import _get_adk_metadata_key - -try: - from a2a import types as a2a_types -except ImportError as e: - import sys - - if sys.version_info < (3, 10): - raise ImportError( - 'A2A requires Python 3.10 or above. Please upgrade your Python version.' - ) from e - else: - raise e - +from a2a import types as a2a_types from google.genai import types as genai_types from ..experimental import a2a_experimental +from .utils import _get_adk_metadata_key logger = logging.getLogger('google_adk.' + __name__) diff --git a/src/google/adk/a2a/converters/request_converter.py b/src/google/adk/a2a/converters/request_converter.py index 39db41da..1746ec0b 100644 --- a/src/google/adk/a2a/converters/request_converter.py +++ b/src/google/adk/a2a/converters/request_converter.py @@ -15,23 +15,12 @@ from __future__ import annotations from collections.abc import Callable -import sys from typing import Any from typing import Optional -from pydantic import BaseModel - -try: - from a2a.server.agent_execution import RequestContext -except ImportError as e: - if sys.version_info < (3, 10): - raise ImportError( - 'A2A requires Python 3.10 or above. Please upgrade your Python version.' - ) from e - else: - raise e - +from a2a.server.agent_execution import RequestContext from google.genai import types as genai_types +from pydantic import BaseModel from ...runners import RunConfig from ..experimental import a2a_experimental diff --git a/src/google/adk/a2a/executor/a2a_agent_executor.py b/src/google/adk/a2a/executor/a2a_agent_executor.py index 608a8188..b6880aaa 100644 --- a/src/google/adk/a2a/executor/a2a_agent_executor.py +++ b/src/google/adk/a2a/executor/a2a_agent_executor.py @@ -23,34 +23,22 @@ from typing import Callable from typing import Optional import uuid -from ...utils.context_utils import Aclosing - -try: - from a2a.server.agent_execution import AgentExecutor - from a2a.server.agent_execution.context import RequestContext - from a2a.server.events.event_queue import EventQueue - from a2a.types import Artifact - from a2a.types import Message - from a2a.types import Role - from a2a.types import TaskArtifactUpdateEvent - from a2a.types import TaskState - from a2a.types import TaskStatus - from a2a.types import TaskStatusUpdateEvent - from a2a.types import TextPart - -except ImportError as e: - import sys - - if sys.version_info < (3, 10): - raise ImportError( - 'A2A requires Python 3.10 or above. Please upgrade your Python version.' - ) from e - else: - raise e +from a2a.server.agent_execution import AgentExecutor +from a2a.server.agent_execution.context import RequestContext +from a2a.server.events.event_queue import EventQueue +from a2a.types import Artifact +from a2a.types import Message +from a2a.types import Role +from a2a.types import TaskArtifactUpdateEvent +from a2a.types import TaskState +from a2a.types import TaskStatus +from a2a.types import TaskStatusUpdateEvent +from a2a.types import TextPart from google.adk.runners import Runner from pydantic import BaseModel from typing_extensions import override +from ...utils.context_utils import Aclosing from ..converters.event_converter import AdkEventToA2AEventsConverter from ..converters.event_converter import convert_event_to_a2a_events from ..converters.part_converter import A2APartToGenAIPartConverter diff --git a/src/google/adk/a2a/utils/agent_card_builder.py b/src/google/adk/a2a/utils/agent_card_builder.py index aa7f657f..c0078709 100644 --- a/src/google/adk/a2a/utils/agent_card_builder.py +++ b/src/google/adk/a2a/utils/agent_card_builder.py @@ -15,25 +15,15 @@ from __future__ import annotations import re -import sys from typing import Dict from typing import List from typing import Optional -try: - from a2a.types import AgentCapabilities - from a2a.types import AgentCard - from a2a.types import AgentProvider - from a2a.types import AgentSkill - from a2a.types import SecurityScheme -except ImportError as e: - if sys.version_info < (3, 10): - raise ImportError( - 'A2A requires Python 3.10 or above. Please upgrade your Python version.' - ) from e - else: - raise e - +from a2a.types import AgentCapabilities +from a2a.types import AgentCard +from a2a.types import AgentProvider +from a2a.types import AgentSkill +from a2a.types import SecurityScheme from ...agents.base_agent import BaseAgent from ...agents.llm_agent import LlmAgent diff --git a/src/google/adk/a2a/utils/agent_to_a2a.py b/src/google/adk/a2a/utils/agent_to_a2a.py index 72a2292f..1a1ba356 100644 --- a/src/google/adk/a2a/utils/agent_to_a2a.py +++ b/src/google/adk/a2a/utils/agent_to_a2a.py @@ -15,30 +15,18 @@ from __future__ import annotations import logging -import sys - -try: - from a2a.server.apps import A2AStarletteApplication - from a2a.server.request_handlers import DefaultRequestHandler - from a2a.server.tasks import InMemoryTaskStore - from a2a.types import AgentCard -except ImportError as e: - if sys.version_info < (3, 10): - raise ImportError( - "A2A requires Python 3.10 or above. Please upgrade your Python version." - ) from e - else: - raise e - from typing import Optional from typing import Union +from a2a.server.apps import A2AStarletteApplication +from a2a.server.request_handlers import DefaultRequestHandler +from a2a.server.tasks import InMemoryTaskStore +from a2a.types import AgentCard from starlette.applications import Starlette from ...agents.base_agent import BaseAgent from ...artifacts.in_memory_artifact_service import InMemoryArtifactService from ...auth.credential_service.in_memory_credential_service import InMemoryCredentialService -from ...cli.utils.logs import setup_adk_logger from ...memory.in_memory_memory_service import InMemoryMemoryService from ...runners import Runner from ...sessions.in_memory_session_service import InMemorySessionService @@ -117,7 +105,8 @@ def to_a2a( app = to_a2a(agent, agent_card=my_custom_agent_card) """ # Set up ADK logging to ensure logs are visible when using uvicorn directly - setup_adk_logger(logging.INFO) + adk_logger = logging.getLogger("google_adk") + adk_logger.setLevel(logging.INFO) async def create_runner() -> Runner: """Create a runner for the agent.""" diff --git a/src/google/adk/agents/__init__.py b/src/google/adk/agents/__init__.py index 5710a21b..b5f8e88c 100644 --- a/src/google/adk/agents/__init__.py +++ b/src/google/adk/agents/__init__.py @@ -12,9 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging -import sys - from .base_agent import BaseAgent from .invocation_context import InvocationContext from .live_request_queue import LiveRequest @@ -22,6 +19,7 @@ from .live_request_queue import LiveRequestQueue from .llm_agent import Agent from .llm_agent import LlmAgent from .loop_agent import LoopAgent +from .mcp_instruction_provider import McpInstructionProvider from .parallel_agent import ParallelAgent from .run_config import RunConfig from .sequential_agent import SequentialAgent @@ -31,6 +29,7 @@ __all__ = [ 'BaseAgent', 'LlmAgent', 'LoopAgent', + 'McpInstructionProvider', 'ParallelAgent', 'SequentialAgent', 'InvocationContext', @@ -38,16 +37,3 @@ __all__ = [ 'LiveRequestQueue', 'RunConfig', ] - -if sys.version_info < (3, 10): - logger = logging.getLogger('google_adk.' + __name__) - logger.warning( - 'MCP requires Python 3.10 or above. Please upgrade your Python' - ' version in order to use it.' - ) -else: - from .mcp_instruction_provider import McpInstructionProvider - - __all__.extend([ - 'McpInstructionProvider', - ]) diff --git a/src/google/adk/agents/mcp_instruction_provider.py b/src/google/adk/agents/mcp_instruction_provider.py index e9f40663..20896a7a 100644 --- a/src/google/adk/agents/mcp_instruction_provider.py +++ b/src/google/adk/agents/mcp_instruction_provider.py @@ -22,24 +22,12 @@ from typing import Any from typing import Dict from typing import TextIO +from mcp import types + +from ..tools.mcp_tool.mcp_session_manager import MCPSessionManager from .llm_agent import InstructionProvider from .readonly_context import ReadonlyContext -# Attempt to import MCP Session Manager from the MCP library, and hints user to -# upgrade their Python version to 3.10 if it fails. -try: - from mcp import types - - from ..tools.mcp_tool.mcp_session_manager import MCPSessionManager -except ImportError as e: - if sys.version_info < (3, 10): - raise ImportError( - "MCP Session Manager requires Python 3.10 or above. Please upgrade" - " your Python version." - ) from e - else: - raise e - class McpInstructionProvider(InstructionProvider): """Fetches agent instructions from an MCP server.""" diff --git a/src/google/adk/agents/parallel_agent.py b/src/google/adk/agents/parallel_agent.py index f7270a75..09e65a67 100644 --- a/src/google/adk/agents/parallel_agent.py +++ b/src/google/adk/agents/parallel_agent.py @@ -48,19 +48,54 @@ def _create_branch_ctx_for_sub_agent( return invocation_context +async def _merge_agent_run( + agent_runs: list[AsyncGenerator[Event, None]], +) -> AsyncGenerator[Event, None]: + """Merges agent runs using asyncio.TaskGroup on Python 3.11+.""" + sentinel = object() + queue = asyncio.Queue() + + # Agents are processed in parallel. + # Events for each agent are put on queue sequentially. + async def process_an_agent(events_for_one_agent): + try: + async for event in events_for_one_agent: + resume_signal = asyncio.Event() + await queue.put((event, resume_signal)) + # Wait for upstream to consume event before generating new events. + await resume_signal.wait() + finally: + # Mark agent as finished. + await queue.put((sentinel, None)) + + async with asyncio.TaskGroup() as tg: + for events_for_one_agent in agent_runs: + tg.create_task(process_an_agent(events_for_one_agent)) + + sentinel_count = 0 + # Run until all agents finished processing. + while sentinel_count < len(agent_runs): + event, resume_signal = await queue.get() + # Agent finished processing. + if event is sentinel: + sentinel_count += 1 + else: + yield event + # Signal to agent that it should generate next event. + resume_signal.set() + + # TODO - remove once Python <3.11 is no longer supported. async def _merge_agent_run_pre_3_11( agent_runs: list[AsyncGenerator[Event, None]], ) -> AsyncGenerator[Event, None]: - """Merges the agent run event generator. - This version works in Python 3.9 and 3.10 and uses custom replacement for - asyncio.TaskGroup for tasks cancellation and exception handling. + """Merges agent runs for Python 3.10 without asyncio.TaskGroup. - This implementation guarantees for each agent, it won't move on until the - generated event is processed by upstream runner. + Uses custom cancellation and exception handling to mirror TaskGroup + semantics. Each agent waits until the runner processes emitted events. Args: - agent_runs: A list of async generators that yield events from each agent. + agent_runs: Async generators that yield events from each agent. Yields: Event: The next event from the merged generator. @@ -112,53 +147,6 @@ async def _merge_agent_run_pre_3_11( task.cancel() -async def _merge_agent_run( - agent_runs: list[AsyncGenerator[Event, None]], -) -> AsyncGenerator[Event, None]: - """Merges the agent run event generator. - - This implementation guarantees for each agent, it won't move on until the - generated event is processed by upstream runner. - - Args: - agent_runs: A list of async generators that yield events from each agent. - - Yields: - Event: The next event from the merged generator. - """ - sentinel = object() - queue = asyncio.Queue() - - # Agents are processed in parallel. - # Events for each agent are put on queue sequentially. - async def process_an_agent(events_for_one_agent): - try: - async for event in events_for_one_agent: - resume_signal = asyncio.Event() - await queue.put((event, resume_signal)) - # Wait for upstream to consume event before generating new events. - await resume_signal.wait() - finally: - # Mark agent as finished. - await queue.put((sentinel, None)) - - async with asyncio.TaskGroup() as tg: - for events_for_one_agent in agent_runs: - tg.create_task(process_an_agent(events_for_one_agent)) - - sentinel_count = 0 - # Run until all agents finished processing. - while sentinel_count < len(agent_runs): - event, resume_signal = await queue.get() - # Agent finished processing. - if event is sentinel: - sentinel_count += 1 - else: - yield event - # Signal to agent that it should generate next event. - resume_signal.set() - - class ParallelAgent(BaseAgent): """A shell agent that runs its sub-agents in parallel in an isolated manner. @@ -195,13 +183,11 @@ class ParallelAgent(BaseAgent): pause_invocation = False try: - # TODO remove if once Python <3.11 is no longer supported. merge_func = ( _merge_agent_run if sys.version_info >= (3, 11) else _merge_agent_run_pre_3_11 ) - async with Aclosing(merge_func(agent_runs)) as agen: async for event in agen: yield event diff --git a/src/google/adk/agents/remote_a2a_agent.py b/src/google/adk/agents/remote_a2a_agent.py index 5d427309..8d133060 100644 --- a/src/google/adk/agents/remote_a2a_agent.py +++ b/src/google/adk/agents/remote_a2a_agent.py @@ -26,30 +26,22 @@ from typing import Union from urllib.parse import urlparse import uuid -try: - from a2a.client import Client as A2AClient - from a2a.client import ClientEvent as A2AClientEvent - from a2a.client.card_resolver import A2ACardResolver - from a2a.client.client import ClientConfig as A2AClientConfig - from a2a.client.client_factory import ClientFactory as A2AClientFactory - from a2a.client.errors import A2AClientError - from a2a.types import AgentCard - from a2a.types import Message as A2AMessage - from a2a.types import Part as A2APart - from a2a.types import Role - from a2a.types import TaskArtifactUpdateEvent as A2ATaskArtifactUpdateEvent - from a2a.types import TaskState - from a2a.types import TaskStatusUpdateEvent as A2ATaskStatusUpdateEvent - from a2a.types import TransportProtocol as A2ATransport -except ImportError as e: - import sys - - if sys.version_info < (3, 10): - raise ImportError( - "A2A requires Python 3.10 or above. Please upgrade your Python version." - ) from e - else: - raise e +from a2a.client import Client as A2AClient +from a2a.client import ClientEvent as A2AClientEvent +from a2a.client.card_resolver import A2ACardResolver +from a2a.client.client import ClientConfig as A2AClientConfig +from a2a.client.client_factory import ClientFactory as A2AClientFactory +from a2a.client.errors import A2AClientError +from a2a.types import AgentCard +from a2a.types import Message as A2AMessage +from a2a.types import Part as A2APart +from a2a.types import Role +from a2a.types import TaskArtifactUpdateEvent as A2ATaskArtifactUpdateEvent +from a2a.types import TaskState +from a2a.types import TaskStatusUpdateEvent as A2ATaskStatusUpdateEvent +from a2a.types import TransportProtocol as A2ATransport +from google.genai import types as genai_types +import httpx try: from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH @@ -57,9 +49,6 @@ except ImportError: # Fallback for older versions of a2a-sdk. AGENT_CARD_WELL_KNOWN_PATH = "/.well-known/agent.json" -from google.genai import types as genai_types -import httpx - from ..a2a.converters.event_converter import convert_a2a_message_to_event from ..a2a.converters.event_converter import convert_a2a_task_to_event from ..a2a.converters.event_converter import convert_event_to_a2a_message diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index f9170968..df06b1cf 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -342,25 +342,14 @@ def get_fast_api_app( ) if a2a: - try: - from a2a.server.apps import A2AStarletteApplication - from a2a.server.request_handlers import DefaultRequestHandler - from a2a.server.tasks import InMemoryTaskStore - from a2a.types import AgentCard - from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH + from a2a.server.apps import A2AStarletteApplication + from a2a.server.request_handlers import DefaultRequestHandler + from a2a.server.tasks import InMemoryTaskStore + from a2a.types import AgentCard + from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH - from ..a2a.executor.a2a_agent_executor import A2aAgentExecutor + from ..a2a.executor.a2a_agent_executor import A2aAgentExecutor - except ImportError as e: - import sys - - if sys.version_info < (3, 10): - raise ImportError( - "A2A requires Python 3.10 or above. Please upgrade your Python" - " version." - ) from e - else: - raise e # locate all a2a agent apps in the agents directory base_path = Path.cwd() / agents_dir # the root agents directory should be an existing folder diff --git a/src/google/adk/tools/crewai_tool.py b/src/google/adk/tools/crewai_tool.py index eaef4792..875b82e5 100644 --- a/src/google/adk/tools/crewai_tool.py +++ b/src/google/adk/tools/crewai_tool.py @@ -30,16 +30,9 @@ from .tool_context import ToolContext try: from crewai.tools import BaseTool as CrewaiBaseTool except ImportError as e: - import sys - - if sys.version_info < (3, 10): - raise ImportError( - 'Crewai Tools require Python 3.10+. Please upgrade your Python version.' - ) from e - else: - raise ImportError( - "Crewai Tools require pip install 'google-adk[extensions]'." - ) from e + raise ImportError( + "Crewai Tools require pip install 'google-adk[extensions]'." + ) from e class CrewaiTool(FunctionTool): diff --git a/src/google/adk/tools/mcp_tool/__init__.py b/src/google/adk/tools/mcp_tool/__init__.py index f1e56b99..1170b2e1 100644 --- a/src/google/adk/tools/mcp_tool/__init__.py +++ b/src/google/adk/tools/mcp_tool/__init__.py @@ -39,15 +39,7 @@ try: except ImportError as e: import logging - import sys logger = logging.getLogger('google_adk.' + __name__) - - if sys.version_info < (3, 10): - logger.warning( - 'MCP Tool requires Python 3.10 or above. Please upgrade your Python' - ' version.' - ) - else: - logger.debug('MCP Tool is not installed') - logger.debug(e) + logger.debug('MCP Tool is not installed') + logger.debug(e) diff --git a/src/google/adk/tools/mcp_tool/mcp_session_manager.py b/src/google/adk/tools/mcp_tool/mcp_session_manager.py index 7d9714aa..c9c4c2ae 100644 --- a/src/google/adk/tools/mcp_tool/mcp_session_manager.py +++ b/src/google/adk/tools/mcp_tool/mcp_session_manager.py @@ -29,24 +29,13 @@ from typing import TextIO from typing import Union import anyio +from mcp import ClientSession +from mcp import StdioServerParameters +from mcp.client.sse import sse_client +from mcp.client.stdio import stdio_client +from mcp.client.streamable_http import streamablehttp_client from pydantic import BaseModel -try: - from mcp import ClientSession - from mcp import StdioServerParameters - from mcp.client.sse import sse_client - from mcp.client.stdio import stdio_client - from mcp.client.streamable_http import streamablehttp_client -except ImportError as e: - - if sys.version_info < (3, 10): - raise ImportError( - 'MCP Tool requires Python 3.10 or above. Please upgrade your Python' - ' version.' - ) from e - else: - raise e - logger = logging.getLogger('google_adk.' + __name__) diff --git a/src/google/adk/tools/mcp_tool/mcp_tool.py b/src/google/adk/tools/mcp_tool/mcp_tool.py index 284aea41..b15f2c73 100644 --- a/src/google/adk/tools/mcp_tool/mcp_tool.py +++ b/src/google/adk/tools/mcp_tool/mcp_tool.py @@ -17,7 +17,6 @@ from __future__ import annotations import base64 import inspect import logging -import sys from typing import Any from typing import Callable from typing import Dict @@ -27,35 +26,21 @@ import warnings from fastapi.openapi.models import APIKeyIn from google.genai.types import FunctionDeclaration +from mcp.types import Tool as McpBaseTool from typing_extensions import override from ...agents.readonly_context import ReadonlyContext -from ...features import FeatureName -from ...features import is_feature_enabled -from .._gemini_schema_util import _to_gemini_schema -from .mcp_session_manager import MCPSessionManager -from .mcp_session_manager import retry_on_errors - -# Attempt to import MCP Tool from the MCP library, and hints user to upgrade -# their Python version to 3.10 if it fails. -try: - from mcp.types import Tool as McpBaseTool -except ImportError as e: - if sys.version_info < (3, 10): - raise ImportError( - "MCP Tool requires Python 3.10 or above. Please upgrade your Python" - " version." - ) from e - else: - raise e - - from ...auth.auth_credential import AuthCredential from ...auth.auth_schemes import AuthScheme from ...auth.auth_tool import AuthConfig +from ...features import FeatureName +from ...features import is_feature_enabled +from .._gemini_schema_util import _to_gemini_schema from ..base_authenticated_tool import BaseAuthenticatedTool # import from ..tool_context import ToolContext +from .mcp_session_manager import MCPSessionManager +from .mcp_session_manager import retry_on_errors logger = logging.getLogger("google_adk." + __name__) diff --git a/src/google/adk/tools/mcp_tool/mcp_toolset.py b/src/google/adk/tools/mcp_tool/mcp_toolset.py index 3768477e..035b7587 100644 --- a/src/google/adk/tools/mcp_tool/mcp_toolset.py +++ b/src/google/adk/tools/mcp_tool/mcp_toolset.py @@ -25,6 +25,8 @@ from typing import TextIO from typing import Union import warnings +from mcp import StdioServerParameters +from mcp.types import ListToolsResult from pydantic import model_validator from typing_extensions import override @@ -41,23 +43,6 @@ from .mcp_session_manager import retry_on_errors from .mcp_session_manager import SseConnectionParams from .mcp_session_manager import StdioConnectionParams from .mcp_session_manager import StreamableHTTPConnectionParams - -# Attempt to import MCP Tool from the MCP library, and hints user to upgrade -# their Python version to 3.10 if it fails. -try: - from mcp import StdioServerParameters - from mcp.types import ListToolsResult -except ImportError as e: - import sys - - if sys.version_info < (3, 10): - raise ImportError( - "MCP Tool requires Python 3.10 or above. Please upgrade your Python" - " version." - ) from e - else: - raise e - from .mcp_tool import MCPTool logger = logging.getLogger("google_adk." + __name__) diff --git a/src/google/adk/utils/context_utils.py b/src/google/adk/utils/context_utils.py index bd8dacb9..a75feae3 100644 --- a/src/google/adk/utils/context_utils.py +++ b/src/google/adk/utils/context_utils.py @@ -20,30 +20,7 @@ Please do not rely on the implementation details. from __future__ import annotations -from contextlib import AbstractAsyncContextManager -from typing import Any -from typing import AsyncGenerator +from contextlib import aclosing - -class Aclosing(AbstractAsyncContextManager): - """Async context manager for safely finalizing an asynchronously cleaned-up - resource such as an async generator, calling its ``aclose()`` method. - Needed to correctly close contexts for OTel spans. - See https://github.com/google/adk-python/issues/1670#issuecomment-3115891100. - - Based on - https://docs.python.org/3/library/contextlib.html#contextlib.aclosing - which is available in Python 3.10+. - - TODO: replace all occurrences with contextlib.aclosing once Python 3.9 is no - longer supported. - """ - - def __init__(self, async_generator: AsyncGenerator[Any, None]): - self.async_generator = async_generator - - async def __aenter__(self): - return self.async_generator - - async def __aexit__(self, *exc_info): - await self.async_generator.aclose() +# Re-export aclosing for backward compatibility +Aclosing = aclosing diff --git a/tests/unittests/a2a/converters/test_event_converter.py b/tests/unittests/a2a/converters/test_event_converter.py index cb3f7a68..49b7d3c2 100644 --- a/tests/unittests/a2a/converters/test_event_converter.py +++ b/tests/unittests/a2a/converters/test_event_converter.py @@ -12,50 +12,33 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys from unittest.mock import Mock from unittest.mock import patch +from a2a.types import DataPart +from a2a.types import Message +from a2a.types import Role +from a2a.types import Task +from a2a.types import TaskState +from a2a.types import TaskStatusUpdateEvent +from google.adk.a2a.converters.event_converter import _create_artifact_id +from google.adk.a2a.converters.event_converter import _create_error_status_event +from google.adk.a2a.converters.event_converter import _create_status_update_event +from google.adk.a2a.converters.event_converter import _get_adk_metadata_key +from google.adk.a2a.converters.event_converter import _get_context_metadata +from google.adk.a2a.converters.event_converter import _process_long_running_tool +from google.adk.a2a.converters.event_converter import _serialize_metadata_value +from google.adk.a2a.converters.event_converter import ARTIFACT_ID_SEPARATOR +from google.adk.a2a.converters.event_converter import convert_a2a_task_to_event +from google.adk.a2a.converters.event_converter import convert_event_to_a2a_events +from google.adk.a2a.converters.event_converter import convert_event_to_a2a_message +from google.adk.a2a.converters.event_converter import DEFAULT_ERROR_MESSAGE +from google.adk.a2a.converters.utils import ADK_METADATA_KEY_PREFIX +from google.adk.agents.invocation_context import InvocationContext +from google.adk.events.event import Event +from google.adk.events.event_actions import EventActions import pytest -# Skip all tests in this module if Python version is less than 3.10 -pytestmark = pytest.mark.skipif( - sys.version_info < (3, 10), reason="A2A requires Python 3.10+" -) - -# Import dependencies with version checking -try: - from a2a.types import DataPart - from a2a.types import Message - from a2a.types import Role - from a2a.types import Task - from a2a.types import TaskState - from a2a.types import TaskStatusUpdateEvent - from google.adk.a2a.converters.event_converter import _create_artifact_id - from google.adk.a2a.converters.event_converter import _create_error_status_event - from google.adk.a2a.converters.event_converter import _create_status_update_event - from google.adk.a2a.converters.event_converter import _get_adk_metadata_key - from google.adk.a2a.converters.event_converter import _get_context_metadata - from google.adk.a2a.converters.event_converter import _process_long_running_tool - from google.adk.a2a.converters.event_converter import _serialize_metadata_value - from google.adk.a2a.converters.event_converter import ARTIFACT_ID_SEPARATOR - from google.adk.a2a.converters.event_converter import convert_a2a_task_to_event - from google.adk.a2a.converters.event_converter import convert_event_to_a2a_events - from google.adk.a2a.converters.event_converter import convert_event_to_a2a_message - from google.adk.a2a.converters.event_converter import DEFAULT_ERROR_MESSAGE - from google.adk.a2a.converters.utils import ADK_METADATA_KEY_PREFIX - from google.adk.agents.invocation_context import InvocationContext - from google.adk.events.event import Event - from google.adk.events.event_actions import EventActions -except ImportError as e: - if sys.version_info < (3, 10): - # Imports are not needed since tests will be skipped due to pytestmark. - # The imported names are only used within test methods, not at module level, - # so no NameError occurs during module compilation. - pass - else: - raise e - class TestEventConverter: """Test suite for event_converter module.""" diff --git a/tests/unittests/a2a/converters/test_part_converter.py b/tests/unittests/a2a/converters/test_part_converter.py index 5a8bad10..541ab770 100644 --- a/tests/unittests/a2a/converters/test_part_converter.py +++ b/tests/unittests/a2a/converters/test_part_converter.py @@ -13,38 +13,21 @@ # limitations under the License. import json -import sys from unittest.mock import Mock from unittest.mock import patch +from a2a import types as a2a_types +from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT +from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE +from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL +from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE +from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_KEY +from google.adk.a2a.converters.part_converter import convert_a2a_part_to_genai_part +from google.adk.a2a.converters.part_converter import convert_genai_part_to_a2a_part +from google.adk.a2a.converters.utils import _get_adk_metadata_key +from google.genai import types as genai_types import pytest -# Skip all tests in this module if Python version is less than 3.10 -pytestmark = pytest.mark.skipif( - sys.version_info < (3, 10), reason="A2A requires Python 3.10+" -) - -# Import dependencies with version checking -try: - from a2a import types as a2a_types - from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT - from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE - from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL - from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE - from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_KEY - from google.adk.a2a.converters.part_converter import convert_a2a_part_to_genai_part - from google.adk.a2a.converters.part_converter import convert_genai_part_to_a2a_part - from google.adk.a2a.converters.utils import _get_adk_metadata_key - from google.genai import types as genai_types -except ImportError as e: - if sys.version_info < (3, 10): - # Imports are not needed since tests will be skipped due to pytestmark. - # The imported names are only used within test methods, not at module level, - # so no NameError occurs during module compilation. - pass - else: - raise e - class TestConvertA2aPartToGenaiPart: """Test cases for convert_a2a_part_to_genai_part function.""" diff --git a/tests/unittests/a2a/converters/test_request_converter.py b/tests/unittests/a2a/converters/test_request_converter.py index a7c21e4d..173b122d 100644 --- a/tests/unittests/a2a/converters/test_request_converter.py +++ b/tests/unittests/a2a/converters/test_request_converter.py @@ -12,33 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys from unittest.mock import Mock from unittest.mock import patch +from a2a.server.agent_execution import RequestContext +from google.adk.a2a.converters.request_converter import _get_user_id +from google.adk.a2a.converters.request_converter import convert_a2a_request_to_agent_run_request +from google.adk.runners import RunConfig +from google.genai import types as genai_types import pytest -# Skip all tests in this module if Python version is less than 3.10 -pytestmark = pytest.mark.skipif( - sys.version_info < (3, 10), reason="A2A requires Python 3.10+" -) - -# Import dependencies with version checking -try: - from a2a.server.agent_execution import RequestContext - from google.adk.a2a.converters.request_converter import _get_user_id - from google.adk.a2a.converters.request_converter import convert_a2a_request_to_agent_run_request - from google.adk.runners import RunConfig - from google.genai import types as genai_types -except ImportError as e: - if sys.version_info < (3, 10): - # Imports are not needed since tests will be skipped due to pytestmark. - # The imported names are only used within test methods, not at module level, - # so no NameError occurs during module compilation. - pass - else: - raise e - class TestGetUserId: """Test cases for _get_user_id function.""" diff --git a/tests/unittests/a2a/converters/test_utils.py b/tests/unittests/a2a/converters/test_utils.py index 6c851116..0d896852 100644 --- a/tests/unittests/a2a/converters/test_utils.py +++ b/tests/unittests/a2a/converters/test_utils.py @@ -12,31 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys - +from google.adk.a2a.converters.utils import _from_a2a_context_id +from google.adk.a2a.converters.utils import _get_adk_metadata_key +from google.adk.a2a.converters.utils import _to_a2a_context_id +from google.adk.a2a.converters.utils import ADK_CONTEXT_ID_PREFIX +from google.adk.a2a.converters.utils import ADK_METADATA_KEY_PREFIX import pytest -# Skip all tests in this module if Python version is less than 3.10 -pytestmark = pytest.mark.skipif( - sys.version_info < (3, 10), reason="A2A requires Python 3.10+" -) - -# Import dependencies with version checking -try: - from google.adk.a2a.converters.utils import _from_a2a_context_id - from google.adk.a2a.converters.utils import _get_adk_metadata_key - from google.adk.a2a.converters.utils import _to_a2a_context_id - from google.adk.a2a.converters.utils import ADK_CONTEXT_ID_PREFIX - from google.adk.a2a.converters.utils import ADK_METADATA_KEY_PREFIX -except ImportError as e: - if sys.version_info < (3, 10): - # Imports are not needed since tests will be skipped due to pytestmark. - # The imported names are only used within test methods, not at module level, - # so no NameError occurs during module compilation. - pass - else: - raise e - class TestUtilsFunctions: """Test suite for utils module functions.""" diff --git a/tests/unittests/a2a/executor/test_a2a_agent_executor.py b/tests/unittests/a2a/executor/test_a2a_agent_executor.py index 4bcc7a91..58d7521f 100644 --- a/tests/unittests/a2a/executor/test_a2a_agent_executor.py +++ b/tests/unittests/a2a/executor/test_a2a_agent_executor.py @@ -12,41 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys from unittest.mock import AsyncMock from unittest.mock import Mock from unittest.mock import patch +from a2a.server.agent_execution.context import RequestContext +from a2a.server.events.event_queue import EventQueue +from a2a.types import Message +from a2a.types import TaskState +from a2a.types import TextPart +from google.adk.a2a.converters.request_converter import AgentRunRequest +from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor +from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutorConfig +from google.adk.events.event import Event +from google.adk.runners import RunConfig +from google.adk.runners import Runner +from google.genai.types import Content import pytest -# Skip all tests in this module if Python version is less than 3.10 -pytestmark = pytest.mark.skipif( - sys.version_info < (3, 10), reason="A2A requires Python 3.10+" -) - -# Import dependencies with version checking -try: - from a2a.server.agent_execution.context import RequestContext - from a2a.server.events.event_queue import EventQueue - from a2a.types import Message - from a2a.types import TaskState - from a2a.types import TextPart - from google.adk.a2a.converters.request_converter import AgentRunRequest - from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor - from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutorConfig - from google.adk.events.event import Event - from google.adk.runners import RunConfig - from google.adk.runners import Runner - from google.genai.types import Content -except ImportError as e: - if sys.version_info < (3, 10): - # Imports are not needed since tests will be skipped due to pytestmark. - # The imported names are only used within test methods, not at module level, - # so no NameError occurs during module compilation. - pass - else: - raise e - class TestA2aAgentExecutor: """Test suite for A2aAgentExecutor class.""" diff --git a/tests/unittests/a2a/executor/test_task_result_aggregator.py b/tests/unittests/a2a/executor/test_task_result_aggregator.py index 9d03db9d..b809b627 100644 --- a/tests/unittests/a2a/executor/test_task_result_aggregator.py +++ b/tests/unittests/a2a/executor/test_task_result_aggregator.py @@ -12,35 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys from unittest.mock import Mock +from a2a.types import Message +from a2a.types import Part +from a2a.types import Role +from a2a.types import TaskState +from a2a.types import TaskStatus +from a2a.types import TaskStatusUpdateEvent +from a2a.types import TextPart +from google.adk.a2a.executor.task_result_aggregator import TaskResultAggregator import pytest -# Skip all tests in this module if Python version is less than 3.10 -pytestmark = pytest.mark.skipif( - sys.version_info < (3, 10), reason="A2A requires Python 3.10+" -) - -# Import dependencies with version checking -try: - from a2a.types import Message - from a2a.types import Part - from a2a.types import Role - from a2a.types import TaskState - from a2a.types import TaskStatus - from a2a.types import TaskStatusUpdateEvent - from a2a.types import TextPart - from google.adk.a2a.executor.task_result_aggregator import TaskResultAggregator -except ImportError as e: - if sys.version_info < (3, 10): - # Imports are not needed since tests will be skipped due to pytestmark. - # The imported names are only used within test methods, not at module level, - # so no NameError occurs during module compilation. - pass - else: - raise e - def create_test_message(text: str): """Helper function to create a test Message object.""" diff --git a/tests/unittests/a2a/utils/test_agent_card_builder.py b/tests/unittests/a2a/utils/test_agent_card_builder.py index e0b62468..3bf32028 100644 --- a/tests/unittests/a2a/utils/test_agent_card_builder.py +++ b/tests/unittests/a2a/utils/test_agent_card_builder.py @@ -12,55 +12,38 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys from unittest.mock import Mock from unittest.mock import patch +from a2a.types import AgentCapabilities +from a2a.types import AgentCard +from a2a.types import AgentProvider +from a2a.types import AgentSkill +from a2a.types import SecurityScheme +from google.adk.a2a.utils.agent_card_builder import _build_agent_description +from google.adk.a2a.utils.agent_card_builder import _build_llm_agent_description_with_instructions +from google.adk.a2a.utils.agent_card_builder import _build_loop_description +from google.adk.a2a.utils.agent_card_builder import _build_orchestration_skill +from google.adk.a2a.utils.agent_card_builder import _build_parallel_description +from google.adk.a2a.utils.agent_card_builder import _build_sequential_description +from google.adk.a2a.utils.agent_card_builder import _convert_example_tool_examples +from google.adk.a2a.utils.agent_card_builder import _extract_examples_from_instruction +from google.adk.a2a.utils.agent_card_builder import _get_agent_skill_name +from google.adk.a2a.utils.agent_card_builder import _get_agent_type +from google.adk.a2a.utils.agent_card_builder import _get_default_description +from google.adk.a2a.utils.agent_card_builder import _get_input_modes +from google.adk.a2a.utils.agent_card_builder import _get_output_modes +from google.adk.a2a.utils.agent_card_builder import _get_workflow_description +from google.adk.a2a.utils.agent_card_builder import _replace_pronouns +from google.adk.a2a.utils.agent_card_builder import AgentCardBuilder +from google.adk.agents.base_agent import BaseAgent +from google.adk.agents.llm_agent import LlmAgent +from google.adk.agents.loop_agent import LoopAgent +from google.adk.agents.parallel_agent import ParallelAgent +from google.adk.agents.sequential_agent import SequentialAgent +from google.adk.tools.example_tool import ExampleTool import pytest -# Skip all tests in this module if Python version is less than 3.10 -pytestmark = pytest.mark.skipif( - sys.version_info < (3, 10), reason="A2A requires Python 3.10+" -) - -# Import dependencies with version checking -try: - from a2a.types import AgentCapabilities - from a2a.types import AgentCard - from a2a.types import AgentProvider - from a2a.types import AgentSkill - from a2a.types import SecurityScheme - from google.adk.a2a.utils.agent_card_builder import _build_agent_description - from google.adk.a2a.utils.agent_card_builder import _build_llm_agent_description_with_instructions - from google.adk.a2a.utils.agent_card_builder import _build_loop_description - from google.adk.a2a.utils.agent_card_builder import _build_orchestration_skill - from google.adk.a2a.utils.agent_card_builder import _build_parallel_description - from google.adk.a2a.utils.agent_card_builder import _build_sequential_description - from google.adk.a2a.utils.agent_card_builder import _convert_example_tool_examples - from google.adk.a2a.utils.agent_card_builder import _extract_examples_from_instruction - from google.adk.a2a.utils.agent_card_builder import _get_agent_skill_name - from google.adk.a2a.utils.agent_card_builder import _get_agent_type - from google.adk.a2a.utils.agent_card_builder import _get_default_description - from google.adk.a2a.utils.agent_card_builder import _get_input_modes - from google.adk.a2a.utils.agent_card_builder import _get_output_modes - from google.adk.a2a.utils.agent_card_builder import _get_workflow_description - from google.adk.a2a.utils.agent_card_builder import _replace_pronouns - from google.adk.a2a.utils.agent_card_builder import AgentCardBuilder - from google.adk.agents.base_agent import BaseAgent - from google.adk.agents.llm_agent import LlmAgent - from google.adk.agents.loop_agent import LoopAgent - from google.adk.agents.parallel_agent import ParallelAgent - from google.adk.agents.sequential_agent import SequentialAgent - from google.adk.tools.example_tool import ExampleTool -except ImportError as e: - if sys.version_info < (3, 10): - # Imports are not needed since tests will be skipped due to pytestmark. - # The imported names are only used within test methods, not at module level, - # so no NameError occurs during module compilation. - pass - else: - raise e - class TestAgentCardBuilder: """Test suite for AgentCardBuilder class.""" diff --git a/tests/unittests/a2a/utils/test_agent_to_a2a.py b/tests/unittests/a2a/utils/test_agent_to_a2a.py index ee80b023..503e572f 100644 --- a/tests/unittests/a2a/utils/test_agent_to_a2a.py +++ b/tests/unittests/a2a/utils/test_agent_to_a2a.py @@ -12,42 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys from unittest.mock import AsyncMock from unittest.mock import Mock from unittest.mock import patch +from a2a.server.apps import A2AStarletteApplication +from a2a.server.request_handlers import DefaultRequestHandler +from a2a.server.tasks import InMemoryTaskStore +from a2a.types import AgentCard +from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor +from google.adk.a2a.utils.agent_card_builder import AgentCardBuilder +from google.adk.a2a.utils.agent_to_a2a import to_a2a +from google.adk.agents.base_agent import BaseAgent +from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService +from google.adk.auth.credential_service.in_memory_credential_service import InMemoryCredentialService +from google.adk.memory.in_memory_memory_service import InMemoryMemoryService +from google.adk.runners import Runner +from google.adk.sessions.in_memory_session_service import InMemorySessionService import pytest - -# Skip all tests in this module if Python version is less than 3.10 -pytestmark = pytest.mark.skipif( - sys.version_info < (3, 10), reason="A2A requires Python 3.10+" -) - -# Import dependencies with version checking -try: - from a2a.server.apps import A2AStarletteApplication - from a2a.server.request_handlers import DefaultRequestHandler - from a2a.server.tasks import InMemoryTaskStore - from a2a.types import AgentCard - from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor - from google.adk.a2a.utils.agent_card_builder import AgentCardBuilder - from google.adk.a2a.utils.agent_to_a2a import to_a2a - from google.adk.agents.base_agent import BaseAgent - from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService - from google.adk.auth.credential_service.in_memory_credential_service import InMemoryCredentialService - from google.adk.memory.in_memory_memory_service import InMemoryMemoryService - from google.adk.runners import Runner - from google.adk.sessions.in_memory_session_service import InMemorySessionService - from starlette.applications import Starlette -except ImportError as e: - if sys.version_info < (3, 10): - # Imports are not needed since tests will be skipped due to pytestmark. - # The imported names are only used within test methods, not at module level, - # so no NameError occurs during module compilation. - pass - else: - raise e +from starlette.applications import Starlette class TestToA2A: diff --git a/tests/unittests/agents/test_mcp_instruction_provider.py b/tests/unittests/agents/test_mcp_instruction_provider.py index 1f2d098c..256d8126 100644 --- a/tests/unittests/agents/test_mcp_instruction_provider.py +++ b/tests/unittests/agents/test_mcp_instruction_provider.py @@ -13,34 +13,14 @@ # limitations under the License. """Unit tests for McpInstructionProvider.""" -import sys from unittest.mock import AsyncMock from unittest.mock import MagicMock from unittest.mock import patch +from google.adk.agents.mcp_instruction_provider import McpInstructionProvider from google.adk.agents.readonly_context import ReadonlyContext import pytest -# Skip all tests in this module if Python version is less than 3.10 -pytestmark = pytest.mark.skipif( - sys.version_info < (3, 10), - reason="MCP instruction provider requires Python 3.10+", -) - -# Import dependencies with version checking -try: - from google.adk.agents.mcp_instruction_provider import McpInstructionProvider -except ImportError as e: - if sys.version_info < (3, 10): - # Create dummy classes to prevent NameError during test collection - # Tests will be skipped anyway due to pytestmark - class DummyClass: - pass - - McpInstructionProvider = DummyClass - else: - raise e - class TestMcpInstructionProvider: """Unit tests for McpInstructionProvider.""" diff --git a/tests/unittests/agents/test_remote_a2a_agent.py b/tests/unittests/agents/test_remote_a2a_agent.py index fd722abf..e7865f39 100644 --- a/tests/unittests/agents/test_remote_a2a_agent.py +++ b/tests/unittests/agents/test_remote_a2a_agent.py @@ -14,70 +14,38 @@ import json from pathlib import Path -import sys import tempfile from unittest.mock import AsyncMock from unittest.mock import create_autospec from unittest.mock import Mock from unittest.mock import patch +from a2a.client.client import ClientConfig +from a2a.client.client import Consumer +from a2a.client.client_factory import ClientFactory +from a2a.types import AgentCapabilities +from a2a.types import AgentCard +from a2a.types import AgentSkill +from a2a.types import Artifact +from a2a.types import Message as A2AMessage +from a2a.types import Part as A2ATaskStatus +from a2a.types import SendMessageSuccessResponse +from a2a.types import Task as A2ATask +from a2a.types import TaskArtifactUpdateEvent +from a2a.types import TaskState +from a2a.types import TaskStatus +from a2a.types import TaskStatusUpdateEvent +from a2a.types import TextPart +from google.adk.agents.invocation_context import InvocationContext +from google.adk.agents.remote_a2a_agent import A2A_METADATA_PREFIX +from google.adk.agents.remote_a2a_agent import AgentCardResolutionError +from google.adk.agents.remote_a2a_agent import RemoteA2aAgent from google.adk.events.event import Event from google.adk.sessions.session import Session from google.genai import types as genai_types import httpx import pytest -# Skip all tests in this module if Python version is less than 3.10 -pytestmark = pytest.mark.skipif( - sys.version_info < (3, 10), reason="A2A requires Python 3.10+" -) - -# Import dependencies with version checking -try: - from a2a.client.client import ClientConfig - from a2a.client.client import Consumer - from a2a.client.client_factory import ClientFactory - from a2a.types import AgentCapabilities - from a2a.types import AgentCard - from a2a.types import AgentSkill - from a2a.types import Artifact - from a2a.types import Message as A2AMessage - from a2a.types import Part as A2ATaskStatus - from a2a.types import SendMessageSuccessResponse - from a2a.types import Task as A2ATask - from a2a.types import TaskArtifactUpdateEvent - from a2a.types import TaskState - from a2a.types import TaskStatus - from a2a.types import TaskStatusUpdateEvent - from a2a.types import TextPart - from google.adk.agents.invocation_context import InvocationContext - from google.adk.agents.remote_a2a_agent import A2A_METADATA_PREFIX - from google.adk.agents.remote_a2a_agent import AgentCardResolutionError - from google.adk.agents.remote_a2a_agent import RemoteA2aAgent -except ImportError as e: - if sys.version_info < (3, 10): - # Create dummy classes to prevent NameError during module compilation. - # These are needed because the module has type annotations and module-level - # helper functions that reference imported types. - class DummyTypes: - pass - - AgentCapabilities = DummyTypes() - AgentCard = DummyTypes() - AgentSkill = DummyTypes() - A2AMessage = DummyTypes() - SendMessageSuccessResponse = DummyTypes() - A2ATask = DummyTypes() - TaskStatusUpdateEvent = DummyTypes() - Artifact = DummyTypes() - TaskArtifactUpdateEvent = DummyTypes() - InvocationContext = DummyTypes() - RemoteA2aAgent = DummyTypes() - AgentCardResolutionError = Exception - A2A_METADATA_PREFIX = "" - else: - raise e - # Helper function to create a proper AgentCard for testing def create_test_agent_card( diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 1fe04732..75d56790 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -509,8 +509,6 @@ async def create_test_eval_set( @pytest.fixture def temp_agents_dir_with_a2a(): """Create a temporary agents directory with A2A agent configurations for testing.""" - if sys.version_info < (3, 10): - pytest.skip("A2A requires Python 3.10+") with tempfile.TemporaryDirectory() as temp_dir: # Create test agent directory agent_dir = Path(temp_dir) / "test_a2a_agent" @@ -554,9 +552,6 @@ def test_app_with_a2a( temp_agents_dir_with_a2a, ): """Create a TestClient for the FastAPI app with A2A enabled.""" - if sys.version_info < (3, 10): - pytest.skip("A2A requires Python 3.10+") - # Mock A2A related classes with ( patch("signal.signal", return_value=None), @@ -1150,9 +1145,6 @@ def test_get_event_graph_returns_dot_src_for_app_agent(): assert "dotSrc" in response.json() -@pytest.mark.skipif( - sys.version_info < (3, 10), reason="A2A requires Python 3.10+" -) def test_a2a_agent_discovery(test_app_with_a2a): """Test that A2A agents are properly discovered and configured.""" # This test mainly verifies that the A2A setup doesn't break the app @@ -1161,9 +1153,6 @@ def test_a2a_agent_discovery(test_app_with_a2a): logger.info("A2A agent discovery test passed") -@pytest.mark.skipif( - sys.version_info < (3, 10), reason="A2A requires Python 3.10+" -) def test_a2a_disabled_by_default(test_app): """Test that A2A functionality is disabled by default.""" # The regular test_app fixture has a2a=False diff --git a/tests/unittests/evaluation/test_local_eval_service.py b/tests/unittests/evaluation/test_local_eval_service.py index cf2ca342..66080828 100644 --- a/tests/unittests/evaluation/test_local_eval_service.py +++ b/tests/unittests/evaluation/test_local_eval_service.py @@ -536,9 +536,6 @@ def test_generate_final_eval_status_doesn_t_throw_on(eval_service): @pytest.mark.asyncio -@pytest.mark.skipif( - sys.version_info < (3, 10), reason="MCP tool requires Python 3.10+" -) async def test_mcp_stdio_agent_no_runtime_error(mocker): """Test that LocalEvalService can handle MCP stdio agents without RuntimeError. diff --git a/tests/unittests/plugins/test_reflect_retry_tool_plugin.py b/tests/unittests/plugins/test_reflect_retry_tool_plugin.py index 1e15f338..2cf52e99 100644 --- a/tests/unittests/plugins/test_reflect_retry_tool_plugin.py +++ b/tests/unittests/plugins/test_reflect_retry_tool_plugin.py @@ -57,10 +57,8 @@ class CustomErrorExtractionPlugin(ReflectAndRetryToolPlugin): return None -# Inheriting from IsolatedAsyncioTestCase ensures these tests works in Python -# 3.9. See https://github.com/pytest-dev/pytest-asyncio/issues/1039 -# Without this, the tests will fail with a "RuntimeError: There is no current -# event loop in thread 'MainThread'." +# Inheriting from IsolatedAsyncioTestCase ensures consistent behavior. +# See https://github.com/pytest-dev/pytest-asyncio/issues/1039 class TestReflectAndRetryToolPlugin(IsolatedAsyncioTestCase): """Comprehensive tests for ReflectAndRetryToolPlugin focusing on behavior.""" diff --git a/tests/unittests/telemetry/test_functional.py b/tests/unittests/telemetry/test_functional.py index 409571ad..43fe6723 100644 --- a/tests/unittests/telemetry/test_functional.py +++ b/tests/unittests/telemetry/test_functional.py @@ -103,7 +103,7 @@ async def test_tracer_start_as_current_span( isinstance(referrer, Aclosing) or isinstance(indirect_referrer, Aclosing) for referrer in gc.get_referrers(coro) - # Some coroutines have a layer of indirection in python 3.9 and 3.10 + # Some coroutines have a layer of indirection in Python 3.10 for indirect_referrer in gc.get_referrers(referrer) ), f'Coro `{coro.__name__}` is not wrapped with Aclosing' firstiter(coro) diff --git a/tests/unittests/tools/computer_use/test_computer_use_tool.py b/tests/unittests/tools/computer_use/test_computer_use_tool.py index 4dbdfbb5..f3843b87 100644 --- a/tests/unittests/tools/computer_use/test_computer_use_tool.py +++ b/tests/unittests/tools/computer_use/test_computer_use_tool.py @@ -47,7 +47,7 @@ class TestComputerUseTool: @pytest.fixture def mock_computer_function(self): """Fixture providing a mock computer function.""" - # Create a real async function instead of AsyncMock for Python 3.9 compatibility + # Create a real async function instead of AsyncMock for better test control calls = [] async def mock_func(*args, **kwargs): diff --git a/tests/unittests/tools/mcp_tool/test_mcp_session_manager.py b/tests/unittests/tools/mcp_tool/test_mcp_session_manager.py index b2d6b1cb..74eabe9d 100644 --- a/tests/unittests/tools/mcp_tool/test_mcp_session_manager.py +++ b/tests/unittests/tools/mcp_tool/test_mcp_session_manager.py @@ -22,46 +22,14 @@ from unittest.mock import AsyncMock from unittest.mock import Mock from unittest.mock import patch +from google.adk.tools.mcp_tool.mcp_session_manager import MCPSessionManager +from google.adk.tools.mcp_tool.mcp_session_manager import retry_on_errors +from google.adk.tools.mcp_tool.mcp_session_manager import SseConnectionParams +from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams +from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams +from mcp import StdioServerParameters import pytest -# Skip all tests in this module if Python version is less than 3.10 -pytestmark = pytest.mark.skipif( - sys.version_info < (3, 10), reason="MCP tool requires Python 3.10+" -) - -# Import dependencies with version checking -try: - from google.adk.tools.mcp_tool.mcp_session_manager import MCPSessionManager - from google.adk.tools.mcp_tool.mcp_session_manager import retry_on_errors - from google.adk.tools.mcp_tool.mcp_session_manager import SseConnectionParams - from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams - from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams -except ImportError as e: - if sys.version_info < (3, 10): - # Create dummy classes to prevent NameError during test collection - # Tests will be skipped anyway due to pytestmark - class DummyClass: - pass - - MCPSessionManager = DummyClass - retry_on_errors = lambda x: x - SseConnectionParams = DummyClass - StdioConnectionParams = DummyClass - StreamableHTTPConnectionParams = DummyClass - else: - raise e - -# Import real MCP classes -try: - from mcp import StdioServerParameters -except ImportError: - # Create a mock if MCP is not available - class StdioServerParameters: - - def __init__(self, command="test_command", args=None): - self.command = command - self.args = args or [] - class MockClientSession: """Mock ClientSession for testing.""" diff --git a/tests/unittests/tools/mcp_tool/test_mcp_tool.py b/tests/unittests/tools/mcp_tool/test_mcp_tool.py index 17b1d8e5..1284e73b 100644 --- a/tests/unittests/tools/mcp_tool/test_mcp_tool.py +++ b/tests/unittests/tools/mcp_tool/test_mcp_tool.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys from unittest.mock import AsyncMock from unittest.mock import Mock from unittest.mock import patch @@ -23,39 +22,15 @@ from google.adk.auth.auth_credential import HttpAuth from google.adk.auth.auth_credential import HttpCredentials from google.adk.auth.auth_credential import OAuth2Auth from google.adk.auth.auth_credential import ServiceAccount +from google.adk.tools.mcp_tool.mcp_session_manager import MCPSessionManager +from google.adk.tools.mcp_tool.mcp_tool import MCPTool +from google.adk.tools.tool_context import ToolContext +from google.genai.types import FunctionDeclaration +from google.genai.types import Type +from mcp.types import CallToolResult +from mcp.types import TextContent import pytest -# Skip all tests in this module if Python version is less than 3.10 -pytestmark = pytest.mark.skipif( - sys.version_info < (3, 10), reason="MCP tool requires Python 3.10+" -) - -# Import dependencies with version checking -try: - from google.adk.tools.mcp_tool.mcp_session_manager import MCPSessionManager - from google.adk.tools.mcp_tool.mcp_tool import MCPTool - from google.adk.tools.tool_context import ToolContext - from google.genai.types import FunctionDeclaration - from google.genai.types import Type - from mcp.types import CallToolResult - from mcp.types import TextContent -except ImportError as e: - if sys.version_info < (3, 10): - # Create dummy classes to prevent NameError during test collection - # Tests will be skipped anyway due to pytestmark - class DummyClass: - pass - - MCPSessionManager = DummyClass - MCPTool = DummyClass - ToolContext = DummyClass - FunctionDeclaration = DummyClass - Type = DummyClass - CallToolResult = DummyClass - TextContent = DummyClass - else: - raise e - # Mock MCP Tool from mcp.types class MockMCPTool: diff --git a/tests/unittests/tools/mcp_tool/test_mcp_toolset.py b/tests/unittests/tools/mcp_tool/test_mcp_toolset.py index 82a5c9a3..5809efe5 100644 --- a/tests/unittests/tools/mcp_tool/test_mcp_toolset.py +++ b/tests/unittests/tools/mcp_tool/test_mcp_toolset.py @@ -20,47 +20,17 @@ from unittest.mock import AsyncMock from unittest.mock import Mock from unittest.mock import patch +from google.adk.agents.readonly_context import ReadonlyContext from google.adk.auth.auth_credential import AuthCredential +from google.adk.tools.mcp_tool.mcp_session_manager import MCPSessionManager +from google.adk.tools.mcp_tool.mcp_session_manager import SseConnectionParams +from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams +from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams +from google.adk.tools.mcp_tool.mcp_tool import MCPTool +from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset +from mcp import StdioServerParameters import pytest -# Skip all tests in this module if Python version is less than 3.10 -pytestmark = pytest.mark.skipif( - sys.version_info < (3, 10), reason="MCP tool requires Python 3.10+" -) - -# Import dependencies with version checking -try: - from google.adk.agents.readonly_context import ReadonlyContext - from google.adk.tools.mcp_tool.mcp_session_manager import MCPSessionManager - from google.adk.tools.mcp_tool.mcp_session_manager import SseConnectionParams - from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams - from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams - from google.adk.tools.mcp_tool.mcp_tool import MCPTool - from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset - from mcp import StdioServerParameters -except ImportError as e: - if sys.version_info < (3, 10): - # Create dummy classes to prevent NameError during test collection - # Tests will be skipped anyway due to pytestmark - class DummyClass: - pass - - class StdioServerParameters: - - def __init__(self, command="test_command", args=None): - self.command = command - self.args = args or [] - - MCPSessionManager = DummyClass - SseConnectionParams = DummyClass - StdioConnectionParams = DummyClass - StreamableHTTPConnectionParams = DummyClass - MCPTool = DummyClass - MCPToolset = DummyClass - ReadonlyContext = DummyClass - else: - raise e - class MockMCPTool: """Mock MCP Tool for testing.""" diff --git a/tests/unittests/tools/retrieval/test_files_retrieval.py b/tests/unittests/tools/retrieval/test_files_retrieval.py index ea4b99cd..dfb7215d 100644 --- a/tests/unittests/tools/retrieval/test_files_retrieval.py +++ b/tests/unittests/tools/retrieval/test_files_retrieval.py @@ -14,7 +14,6 @@ """Tests for FilesRetrieval tool.""" -import sys import unittest.mock as mock from google.adk.tools.retrieval.files_retrieval import _get_default_embedding_model @@ -111,9 +110,6 @@ class TestFilesRetrieval: def test_get_default_embedding_model_success(self): """Test _get_default_embedding_model returns Google embedding when available.""" - # Skip this test in Python 3.9 where llama_index.embeddings.google_genai may not be available - if sys.version_info < (3, 10): - pytest.skip("llama_index.embeddings.google_genai requires Python 3.10+") # Mock the module creation to avoid import issues mock_module = mock.MagicMock() diff --git a/tests/unittests/tools/test_mcp_toolset.py b/tests/unittests/tools/test_mcp_toolset.py index a3a6598e..7bfd9126 100644 --- a/tests/unittests/tools/test_mcp_toolset.py +++ b/tests/unittests/tools/test_mcp_toolset.py @@ -14,31 +14,12 @@ """Unit tests for McpToolset.""" -import sys from unittest.mock import AsyncMock from unittest.mock import MagicMock +from google.adk.tools.mcp_tool.mcp_toolset import McpToolset import pytest -# Skip all tests in this module if Python version is less than 3.10 -pytestmark = pytest.mark.skipif( - sys.version_info < (3, 10), reason="MCP tool requires Python 3.10+" -) - -# Import dependencies with version checking -try: - from google.adk.tools.mcp_tool.mcp_toolset import McpToolset -except ImportError as e: - if sys.version_info < (3, 10): - # Create dummy classes to prevent NameError during test collection - # Tests will be skipped anyway due to pytestmark - class DummyClass: - pass - - McpToolset = DummyClass - else: - raise e - @pytest.mark.asyncio async def test_mcp_toolset_with_prefix():