google-adk 0.0.1__py3-none-any.whl → 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- google/adk/__init__.py +20 -0
- google/adk/agents/__init__.py +32 -0
- google/adk/agents/active_streaming_tool.py +38 -0
- google/adk/agents/base_agent.py +345 -0
- google/adk/agents/callback_context.py +112 -0
- google/adk/agents/invocation_context.py +181 -0
- google/adk/agents/langgraph_agent.py +140 -0
- google/adk/agents/live_request_queue.py +64 -0
- google/adk/agents/llm_agent.py +376 -0
- google/adk/agents/loop_agent.py +62 -0
- google/adk/agents/parallel_agent.py +96 -0
- google/adk/agents/readonly_context.py +46 -0
- google/adk/agents/remote_agent.py +50 -0
- google/adk/agents/run_config.py +87 -0
- google/adk/agents/sequential_agent.py +45 -0
- google/adk/agents/transcription_entry.py +34 -0
- google/adk/artifacts/__init__.py +23 -0
- google/adk/artifacts/base_artifact_service.py +128 -0
- google/adk/artifacts/gcs_artifact_service.py +195 -0
- google/adk/artifacts/in_memory_artifact_service.py +133 -0
- google/adk/auth/__init__.py +22 -0
- google/adk/auth/auth_credential.py +220 -0
- google/adk/auth/auth_handler.py +268 -0
- google/adk/auth/auth_preprocessor.py +116 -0
- google/adk/auth/auth_schemes.py +67 -0
- google/adk/auth/auth_tool.py +55 -0
- google/adk/cli/__init__.py +15 -0
- google/adk/cli/__main__.py +18 -0
- google/adk/cli/agent_graph.py +122 -0
- google/adk/cli/browser/adk_favicon.svg +17 -0
- google/adk/cli/browser/assets/audio-processor.js +51 -0
- google/adk/cli/browser/assets/config/runtime-config.json +3 -0
- google/adk/cli/browser/index.html +33 -0
- google/adk/cli/browser/main-XUU6OGCC.js +75 -0
- google/adk/cli/browser/polyfills-FFHMD2TL.js +18 -0
- google/adk/cli/browser/styles-4VDSPQ37.css +17 -0
- google/adk/cli/cli.py +181 -0
- google/adk/cli/cli_deploy.py +181 -0
- google/adk/cli/cli_eval.py +282 -0
- google/adk/cli/cli_tools_click.py +479 -0
- google/adk/cli/fast_api.py +774 -0
- google/adk/cli/media_streamer/__init__.py +19 -0
- google/adk/cli/media_streamer/index.html +228 -0
- google/adk/cli/utils/__init__.py +49 -0
- google/adk/cli/utils/envs.py +57 -0
- google/adk/cli/utils/evals.py +93 -0
- google/adk/cli/utils/logs.py +72 -0
- google/adk/code_executors/__init__.py +49 -0
- google/adk/code_executors/base_code_executor.py +97 -0
- google/adk/code_executors/code_execution_utils.py +256 -0
- google/adk/code_executors/code_executor_context.py +202 -0
- google/adk/code_executors/container_code_executor.py +196 -0
- google/adk/code_executors/unsafe_local_code_executor.py +71 -0
- google/adk/code_executors/vertex_ai_code_executor.py +234 -0
- google/adk/evaluation/__init__.py +31 -0
- google/adk/evaluation/agent_evaluator.py +329 -0
- google/adk/evaluation/evaluation_constants.py +24 -0
- google/adk/evaluation/evaluation_generator.py +270 -0
- google/adk/evaluation/response_evaluator.py +135 -0
- google/adk/evaluation/trajectory_evaluator.py +184 -0
- google/adk/events/__init__.py +21 -0
- google/adk/events/event.py +130 -0
- google/adk/events/event_actions.py +55 -0
- google/adk/examples/__init__.py +28 -0
- google/adk/examples/base_example_provider.py +35 -0
- google/adk/examples/example.py +27 -0
- google/adk/examples/example_util.py +123 -0
- google/adk/examples/vertex_ai_example_store.py +104 -0
- google/adk/flows/__init__.py +14 -0
- google/adk/flows/llm_flows/__init__.py +20 -0
- google/adk/flows/llm_flows/_base_llm_processor.py +52 -0
- google/adk/flows/llm_flows/_code_execution.py +458 -0
- google/adk/flows/llm_flows/_nl_planning.py +129 -0
- google/adk/flows/llm_flows/agent_transfer.py +132 -0
- google/adk/flows/llm_flows/audio_transcriber.py +109 -0
- google/adk/flows/llm_flows/auto_flow.py +49 -0
- google/adk/flows/llm_flows/base_llm_flow.py +559 -0
- google/adk/flows/llm_flows/basic.py +72 -0
- google/adk/flows/llm_flows/contents.py +370 -0
- google/adk/flows/llm_flows/functions.py +486 -0
- google/adk/flows/llm_flows/identity.py +47 -0
- google/adk/flows/llm_flows/instructions.py +137 -0
- google/adk/flows/llm_flows/single_flow.py +57 -0
- google/adk/memory/__init__.py +35 -0
- google/adk/memory/base_memory_service.py +74 -0
- google/adk/memory/in_memory_memory_service.py +62 -0
- google/adk/memory/vertex_ai_rag_memory_service.py +177 -0
- google/adk/models/__init__.py +31 -0
- google/adk/models/anthropic_llm.py +243 -0
- google/adk/models/base_llm.py +87 -0
- google/adk/models/base_llm_connection.py +76 -0
- google/adk/models/gemini_llm_connection.py +200 -0
- google/adk/models/google_llm.py +331 -0
- google/adk/models/lite_llm.py +673 -0
- google/adk/models/llm_request.py +98 -0
- google/adk/models/llm_response.py +111 -0
- google/adk/models/registry.py +102 -0
- google/adk/planners/__init__.py +23 -0
- google/adk/planners/base_planner.py +66 -0
- google/adk/planners/built_in_planner.py +75 -0
- google/adk/planners/plan_re_act_planner.py +208 -0
- google/adk/runners.py +456 -0
- google/adk/sessions/__init__.py +41 -0
- google/adk/sessions/base_session_service.py +133 -0
- google/adk/sessions/database_session_service.py +522 -0
- google/adk/sessions/in_memory_session_service.py +206 -0
- google/adk/sessions/session.py +54 -0
- google/adk/sessions/state.py +71 -0
- google/adk/sessions/vertex_ai_session_service.py +356 -0
- google/adk/telemetry.py +189 -0
- google/adk/tests/__init__.py +14 -0
- google/adk/tests/integration/.env.example +10 -0
- google/adk/tests/integration/__init__.py +18 -0
- google/adk/tests/integration/conftest.py +119 -0
- google/adk/tests/integration/fixture/__init__.py +14 -0
- google/adk/tests/integration/fixture/agent_with_config/__init__.py +15 -0
- google/adk/tests/integration/fixture/agent_with_config/agent.py +88 -0
- google/adk/tests/integration/fixture/callback_agent/__init__.py +15 -0
- google/adk/tests/integration/fixture/callback_agent/agent.py +105 -0
- google/adk/tests/integration/fixture/context_update_test/OWNERS +1 -0
- google/adk/tests/integration/fixture/context_update_test/__init__.py +15 -0
- google/adk/tests/integration/fixture/context_update_test/agent.py +43 -0
- google/adk/tests/integration/fixture/context_update_test/successful_test.session.json +582 -0
- google/adk/tests/integration/fixture/context_variable_agent/__init__.py +15 -0
- google/adk/tests/integration/fixture/context_variable_agent/agent.py +115 -0
- google/adk/tests/integration/fixture/customer_support_ma/__init__.py +15 -0
- google/adk/tests/integration/fixture/customer_support_ma/agent.py +172 -0
- google/adk/tests/integration/fixture/ecommerce_customer_service_agent/__init__.py +15 -0
- google/adk/tests/integration/fixture/ecommerce_customer_service_agent/agent.py +338 -0
- google/adk/tests/integration/fixture/ecommerce_customer_service_agent/order_query.test.json +69 -0
- google/adk/tests/integration/fixture/ecommerce_customer_service_agent/test_config.json +6 -0
- google/adk/tests/integration/fixture/flow_complex_spark/__init__.py +15 -0
- google/adk/tests/integration/fixture/flow_complex_spark/agent.py +182 -0
- google/adk/tests/integration/fixture/flow_complex_spark/sample.debug.log +243 -0
- google/adk/tests/integration/fixture/flow_complex_spark/sample.session.json +190 -0
- google/adk/tests/integration/fixture/hello_world_agent/__init__.py +15 -0
- google/adk/tests/integration/fixture/hello_world_agent/agent.py +95 -0
- google/adk/tests/integration/fixture/hello_world_agent/roll_die.test.json +24 -0
- google/adk/tests/integration/fixture/hello_world_agent/test_config.json +6 -0
- google/adk/tests/integration/fixture/home_automation_agent/__init__.py +15 -0
- google/adk/tests/integration/fixture/home_automation_agent/agent.py +304 -0
- google/adk/tests/integration/fixture/home_automation_agent/simple_test.test.json +5 -0
- google/adk/tests/integration/fixture/home_automation_agent/simple_test2.test.json +5 -0
- google/adk/tests/integration/fixture/home_automation_agent/test_config.json +5 -0
- google/adk/tests/integration/fixture/home_automation_agent/test_files/dependent_tool_calls.test.json +18 -0
- google/adk/tests/integration/fixture/home_automation_agent/test_files/memorizing_past_events/eval_data.test.json +17 -0
- google/adk/tests/integration/fixture/home_automation_agent/test_files/memorizing_past_events/test_config.json +6 -0
- google/adk/tests/integration/fixture/home_automation_agent/test_files/simple_multi_turn_conversation.test.json +18 -0
- google/adk/tests/integration/fixture/home_automation_agent/test_files/simple_test.test.json +17 -0
- google/adk/tests/integration/fixture/home_automation_agent/test_files/simple_test2.test.json +5 -0
- google/adk/tests/integration/fixture/home_automation_agent/test_files/test_config.json +5 -0
- google/adk/tests/integration/fixture/tool_agent/__init__.py +15 -0
- google/adk/tests/integration/fixture/tool_agent/agent.py +218 -0
- google/adk/tests/integration/fixture/tool_agent/files/Agent_test_plan.pdf +0 -0
- google/adk/tests/integration/fixture/trip_planner_agent/__init__.py +15 -0
- google/adk/tests/integration/fixture/trip_planner_agent/agent.py +110 -0
- google/adk/tests/integration/fixture/trip_planner_agent/initial.session.json +13 -0
- google/adk/tests/integration/fixture/trip_planner_agent/test_config.json +5 -0
- google/adk/tests/integration/fixture/trip_planner_agent/test_files/initial.session.json +13 -0
- google/adk/tests/integration/fixture/trip_planner_agent/test_files/test_config.json +5 -0
- google/adk/tests/integration/fixture/trip_planner_agent/test_files/trip_inquiry_sub_agent.test.json +7 -0
- google/adk/tests/integration/fixture/trip_planner_agent/trip_inquiry.test.json +19 -0
- google/adk/tests/integration/models/__init__.py +14 -0
- google/adk/tests/integration/models/test_google_llm.py +65 -0
- google/adk/tests/integration/test_callback.py +70 -0
- google/adk/tests/integration/test_context_variable.py +67 -0
- google/adk/tests/integration/test_evalute_agent_in_fixture.py +76 -0
- google/adk/tests/integration/test_multi_agent.py +28 -0
- google/adk/tests/integration/test_multi_turn.py +42 -0
- google/adk/tests/integration/test_single_agent.py +23 -0
- google/adk/tests/integration/test_sub_agent.py +26 -0
- google/adk/tests/integration/test_system_instruction.py +177 -0
- google/adk/tests/integration/test_tools.py +287 -0
- google/adk/tests/integration/test_with_test_file.py +34 -0
- google/adk/tests/integration/tools/__init__.py +14 -0
- google/adk/tests/integration/utils/__init__.py +16 -0
- google/adk/tests/integration/utils/asserts.py +75 -0
- google/adk/tests/integration/utils/test_runner.py +97 -0
- google/adk/tests/unittests/__init__.py +14 -0
- google/adk/tests/unittests/agents/__init__.py +14 -0
- google/adk/tests/unittests/agents/test_base_agent.py +407 -0
- google/adk/tests/unittests/agents/test_langgraph_agent.py +191 -0
- google/adk/tests/unittests/agents/test_llm_agent_callbacks.py +138 -0
- google/adk/tests/unittests/agents/test_llm_agent_fields.py +231 -0
- google/adk/tests/unittests/agents/test_loop_agent.py +136 -0
- google/adk/tests/unittests/agents/test_parallel_agent.py +92 -0
- google/adk/tests/unittests/agents/test_sequential_agent.py +114 -0
- google/adk/tests/unittests/artifacts/__init__.py +14 -0
- google/adk/tests/unittests/artifacts/test_artifact_service.py +276 -0
- google/adk/tests/unittests/auth/test_auth_handler.py +575 -0
- google/adk/tests/unittests/conftest.py +73 -0
- google/adk/tests/unittests/fast_api/__init__.py +14 -0
- google/adk/tests/unittests/fast_api/test_fast_api.py +269 -0
- google/adk/tests/unittests/flows/__init__.py +14 -0
- google/adk/tests/unittests/flows/llm_flows/__init__.py +14 -0
- google/adk/tests/unittests/flows/llm_flows/_test_examples.py +142 -0
- google/adk/tests/unittests/flows/llm_flows/test_agent_transfer.py +311 -0
- google/adk/tests/unittests/flows/llm_flows/test_functions_long_running.py +244 -0
- google/adk/tests/unittests/flows/llm_flows/test_functions_request_euc.py +346 -0
- google/adk/tests/unittests/flows/llm_flows/test_functions_sequential.py +93 -0
- google/adk/tests/unittests/flows/llm_flows/test_functions_simple.py +258 -0
- google/adk/tests/unittests/flows/llm_flows/test_identity.py +66 -0
- google/adk/tests/unittests/flows/llm_flows/test_instructions.py +164 -0
- google/adk/tests/unittests/flows/llm_flows/test_model_callbacks.py +142 -0
- google/adk/tests/unittests/flows/llm_flows/test_other_configs.py +46 -0
- google/adk/tests/unittests/flows/llm_flows/test_tool_callbacks.py +269 -0
- google/adk/tests/unittests/models/__init__.py +14 -0
- google/adk/tests/unittests/models/test_google_llm.py +224 -0
- google/adk/tests/unittests/models/test_litellm.py +804 -0
- google/adk/tests/unittests/models/test_models.py +60 -0
- google/adk/tests/unittests/sessions/__init__.py +14 -0
- google/adk/tests/unittests/sessions/test_session_service.py +227 -0
- google/adk/tests/unittests/sessions/test_vertex_ai_session_service.py +246 -0
- google/adk/tests/unittests/streaming/__init__.py +14 -0
- google/adk/tests/unittests/streaming/test_streaming.py +50 -0
- google/adk/tests/unittests/tools/__init__.py +14 -0
- google/adk/tests/unittests/tools/apihub_tool/clients/test_apihub_client.py +499 -0
- google/adk/tests/unittests/tools/apihub_tool/test_apihub_toolset.py +204 -0
- google/adk/tests/unittests/tools/application_integration_tool/clients/test_connections_client.py +600 -0
- google/adk/tests/unittests/tools/application_integration_tool/clients/test_integration_client.py +630 -0
- google/adk/tests/unittests/tools/application_integration_tool/test_application_integration_toolset.py +345 -0
- google/adk/tests/unittests/tools/google_api_tool/__init__.py +13 -0
- google/adk/tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py +657 -0
- google/adk/tests/unittests/tools/openapi_tool/auth/credential_exchangers/test_auto_auth_credential_exchanger.py +145 -0
- google/adk/tests/unittests/tools/openapi_tool/auth/credential_exchangers/test_base_auth_credential_exchanger.py +68 -0
- google/adk/tests/unittests/tools/openapi_tool/auth/credential_exchangers/test_oauth2_exchanger.py +153 -0
- google/adk/tests/unittests/tools/openapi_tool/auth/credential_exchangers/test_service_account_exchanger.py +196 -0
- google/adk/tests/unittests/tools/openapi_tool/auth/test_auth_helper.py +573 -0
- google/adk/tests/unittests/tools/openapi_tool/common/test_common.py +436 -0
- google/adk/tests/unittests/tools/openapi_tool/openapi_spec_parser/test.yaml +1367 -0
- google/adk/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_openapi_spec_parser.py +628 -0
- google/adk/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_openapi_toolset.py +139 -0
- google/adk/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_operation_parser.py +406 -0
- google/adk/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py +966 -0
- google/adk/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_tool_auth_handler.py +201 -0
- google/adk/tests/unittests/tools/retrieval/__init__.py +14 -0
- google/adk/tests/unittests/tools/retrieval/test_vertex_ai_rag_retrieval.py +147 -0
- google/adk/tests/unittests/tools/test_agent_tool.py +167 -0
- google/adk/tests/unittests/tools/test_base_tool.py +141 -0
- google/adk/tests/unittests/tools/test_build_function_declaration.py +277 -0
- google/adk/tests/unittests/utils.py +304 -0
- google/adk/tools/__init__.py +51 -0
- google/adk/tools/_automatic_function_calling_util.py +346 -0
- google/adk/tools/agent_tool.py +176 -0
- google/adk/tools/apihub_tool/__init__.py +19 -0
- google/adk/tools/apihub_tool/apihub_toolset.py +209 -0
- google/adk/tools/apihub_tool/clients/__init__.py +13 -0
- google/adk/tools/apihub_tool/clients/apihub_client.py +332 -0
- google/adk/tools/apihub_tool/clients/secret_client.py +115 -0
- google/adk/tools/application_integration_tool/__init__.py +19 -0
- google/adk/tools/application_integration_tool/application_integration_toolset.py +230 -0
- google/adk/tools/application_integration_tool/clients/connections_client.py +903 -0
- google/adk/tools/application_integration_tool/clients/integration_client.py +253 -0
- google/adk/tools/base_tool.py +144 -0
- google/adk/tools/built_in_code_execution_tool.py +59 -0
- google/adk/tools/crewai_tool.py +72 -0
- google/adk/tools/example_tool.py +62 -0
- google/adk/tools/exit_loop_tool.py +23 -0
- google/adk/tools/function_parameter_parse_util.py +307 -0
- google/adk/tools/function_tool.py +87 -0
- google/adk/tools/get_user_choice_tool.py +28 -0
- google/adk/tools/google_api_tool/__init__.py +14 -0
- google/adk/tools/google_api_tool/google_api_tool.py +59 -0
- google/adk/tools/google_api_tool/google_api_tool_set.py +107 -0
- google/adk/tools/google_api_tool/google_api_tool_sets.py +55 -0
- google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py +521 -0
- google/adk/tools/google_search_tool.py +68 -0
- google/adk/tools/langchain_tool.py +86 -0
- google/adk/tools/load_artifacts_tool.py +113 -0
- google/adk/tools/load_memory_tool.py +58 -0
- google/adk/tools/load_web_page.py +41 -0
- google/adk/tools/long_running_tool.py +39 -0
- google/adk/tools/mcp_tool/__init__.py +42 -0
- google/adk/tools/mcp_tool/conversion_utils.py +161 -0
- google/adk/tools/mcp_tool/mcp_tool.py +113 -0
- google/adk/tools/mcp_tool/mcp_toolset.py +272 -0
- google/adk/tools/openapi_tool/__init__.py +21 -0
- google/adk/tools/openapi_tool/auth/__init__.py +19 -0
- google/adk/tools/openapi_tool/auth/auth_helpers.py +498 -0
- google/adk/tools/openapi_tool/auth/credential_exchangers/__init__.py +25 -0
- google/adk/tools/openapi_tool/auth/credential_exchangers/auto_auth_credential_exchanger.py +105 -0
- google/adk/tools/openapi_tool/auth/credential_exchangers/base_credential_exchanger.py +55 -0
- google/adk/tools/openapi_tool/auth/credential_exchangers/oauth2_exchanger.py +117 -0
- google/adk/tools/openapi_tool/auth/credential_exchangers/service_account_exchanger.py +97 -0
- google/adk/tools/openapi_tool/common/__init__.py +19 -0
- google/adk/tools/openapi_tool/common/common.py +300 -0
- google/adk/tools/openapi_tool/openapi_spec_parser/__init__.py +32 -0
- google/adk/tools/openapi_tool/openapi_spec_parser/openapi_spec_parser.py +231 -0
- google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py +144 -0
- google/adk/tools/openapi_tool/openapi_spec_parser/operation_parser.py +260 -0
- google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +496 -0
- google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py +268 -0
- google/adk/tools/preload_memory_tool.py +72 -0
- google/adk/tools/retrieval/__init__.py +36 -0
- google/adk/tools/retrieval/base_retrieval_tool.py +37 -0
- google/adk/tools/retrieval/files_retrieval.py +33 -0
- google/adk/tools/retrieval/llama_index_retrieval.py +41 -0
- google/adk/tools/retrieval/vertex_ai_rag_retrieval.py +107 -0
- google/adk/tools/tool_context.py +90 -0
- google/adk/tools/toolbox_tool.py +46 -0
- google/adk/tools/transfer_to_agent_tool.py +21 -0
- google/adk/tools/vertex_ai_search_tool.py +96 -0
- google/adk/version.py +16 -0
- google_adk-0.0.1.dist-info/LICENSE.txt → google_adk-0.0.2.dist-info/LICENSE +32 -0
- google_adk-0.0.2.dist-info/METADATA +73 -0
- google_adk-0.0.2.dist-info/RECORD +308 -0
- {google_adk-0.0.1.dist-info → google_adk-0.0.2.dist-info}/WHEEL +1 -2
- google_adk-0.0.2.dist-info/entry_points.txt +3 -0
- agent_kit/__init__.py +0 -0
- google_adk-0.0.1.dist-info/METADATA +0 -15
- google_adk-0.0.1.dist-info/RECORD +0 -6
- google_adk-0.0.1.dist-info/top_level.txt +0 -1
@@ -0,0 +1,256 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
"""Utility functions for code execution."""
|
16
|
+
|
17
|
+
import base64
|
18
|
+
import binascii
|
19
|
+
import copy
|
20
|
+
import dataclasses
|
21
|
+
import re
|
22
|
+
from typing import List, Optional
|
23
|
+
|
24
|
+
from google.genai import types
|
25
|
+
|
26
|
+
|
27
|
+
@dataclasses.dataclass(frozen=True)
|
28
|
+
class File:
|
29
|
+
"""A structure that contains a file name and its content."""
|
30
|
+
|
31
|
+
name: str
|
32
|
+
"""
|
33
|
+
The name of the file with file extension (e.g., "file.csv").
|
34
|
+
"""
|
35
|
+
|
36
|
+
content: str
|
37
|
+
"""
|
38
|
+
The base64-encoded bytes of the file content.
|
39
|
+
"""
|
40
|
+
|
41
|
+
mime_type: str = 'text/plain'
|
42
|
+
"""
|
43
|
+
The mime type of the file (e.g., "image/png").
|
44
|
+
"""
|
45
|
+
|
46
|
+
|
47
|
+
@dataclasses.dataclass
|
48
|
+
class CodeExecutionInput:
|
49
|
+
"""A structure that contains the input of code execution."""
|
50
|
+
|
51
|
+
code: str
|
52
|
+
"""
|
53
|
+
The code to execute.
|
54
|
+
"""
|
55
|
+
|
56
|
+
input_files: list[File] = dataclasses.field(default_factory=list)
|
57
|
+
"""
|
58
|
+
The input files available to the code.
|
59
|
+
"""
|
60
|
+
|
61
|
+
execution_id: Optional[str] = None
|
62
|
+
"""
|
63
|
+
The execution ID for the stateful code execution.
|
64
|
+
"""
|
65
|
+
|
66
|
+
|
67
|
+
@dataclasses.dataclass
|
68
|
+
class CodeExecutionResult:
|
69
|
+
"""A structure that contains the result of code execution."""
|
70
|
+
|
71
|
+
stdout: str = ''
|
72
|
+
"""
|
73
|
+
The standard output of the code execution.
|
74
|
+
"""
|
75
|
+
|
76
|
+
stderr: str = ''
|
77
|
+
"""
|
78
|
+
The standard error of the code execution.
|
79
|
+
"""
|
80
|
+
|
81
|
+
output_files: list[File] = dataclasses.field(default_factory=list)
|
82
|
+
"""
|
83
|
+
The output files from the code execution.
|
84
|
+
"""
|
85
|
+
|
86
|
+
|
87
|
+
class CodeExecutionUtils:
|
88
|
+
"""Utility functions for code execution."""
|
89
|
+
|
90
|
+
@staticmethod
|
91
|
+
def get_encoded_file_content(data: bytes) -> bytes:
|
92
|
+
"""Gets the file content as a base64-encoded bytes.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
data: The file content bytes.
|
96
|
+
|
97
|
+
Returns:
|
98
|
+
The file content as a base64-encoded bytes.
|
99
|
+
"""
|
100
|
+
|
101
|
+
def _is_base64_encoded(data: bytes) -> bool:
|
102
|
+
try:
|
103
|
+
return base64.b64encode(base64.b64decode(data)) == data
|
104
|
+
except binascii.Error:
|
105
|
+
return False
|
106
|
+
|
107
|
+
return data if _is_base64_encoded(data) else base64.b64encode(data)
|
108
|
+
|
109
|
+
@staticmethod
|
110
|
+
def extract_code_and_truncate_content(
|
111
|
+
content: types.Content,
|
112
|
+
code_block_delimiters: List[tuple[str, str]],
|
113
|
+
) -> Optional[str]:
|
114
|
+
"""Extracts the first code block from the content and truncate everything after it.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
content: The mutable content to extract the code from.
|
118
|
+
code_block_delimiters: The list of the enclosing delimiters to identify
|
119
|
+
the code blocks.
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
The first code block if found, otherwise None.
|
123
|
+
"""
|
124
|
+
if not content or not content.parts:
|
125
|
+
return
|
126
|
+
|
127
|
+
# Extract the code from the executable code parts if there're no associated
|
128
|
+
# code execution result parts.
|
129
|
+
for idx, part in enumerate(content.parts):
|
130
|
+
if part.executable_code and (
|
131
|
+
idx == len(content.parts) - 1
|
132
|
+
or not content.parts[idx + 1].code_execution_result
|
133
|
+
):
|
134
|
+
content.parts = content.parts[: idx + 1]
|
135
|
+
return part.executable_code.code
|
136
|
+
|
137
|
+
# Extract the code from the text parts.
|
138
|
+
text_parts = [p for p in content.parts if p.text]
|
139
|
+
if not text_parts:
|
140
|
+
return
|
141
|
+
|
142
|
+
first_text_part = copy.deepcopy(text_parts[0])
|
143
|
+
response_text = '\n'.join([p.text for p in text_parts])
|
144
|
+
|
145
|
+
# Find the first code block.
|
146
|
+
leading_delimiter_pattern = '|'.join(d[0] for d in code_block_delimiters)
|
147
|
+
trailing_delimiter_pattern = '|'.join(d[1] for d in code_block_delimiters)
|
148
|
+
pattern = re.compile(
|
149
|
+
(
|
150
|
+
rf'(?P<prefix>.*?)({leading_delimiter_pattern})(?P<code>.*?)({trailing_delimiter_pattern})(?P<suffix>.*?)$'
|
151
|
+
).encode(),
|
152
|
+
re.DOTALL,
|
153
|
+
)
|
154
|
+
pattern_match = pattern.search(response_text.encode())
|
155
|
+
if pattern_match is None:
|
156
|
+
return
|
157
|
+
|
158
|
+
code_str = pattern_match.group('code').decode()
|
159
|
+
if not code_str:
|
160
|
+
return
|
161
|
+
|
162
|
+
content.parts = []
|
163
|
+
if pattern_match.group('prefix'):
|
164
|
+
first_text_part.text = pattern_match.group('prefix').decode()
|
165
|
+
content.parts.append(first_text_part)
|
166
|
+
content.parts.append(
|
167
|
+
CodeExecutionUtils.build_executable_code_part(code_str)
|
168
|
+
)
|
169
|
+
return pattern_match.group('code').decode()
|
170
|
+
|
171
|
+
@staticmethod
|
172
|
+
def build_executable_code_part(code: str) -> types.Part:
|
173
|
+
"""Builds an executable code part with code string.
|
174
|
+
|
175
|
+
Args:
|
176
|
+
code: The code string.
|
177
|
+
|
178
|
+
Returns:
|
179
|
+
The constructed executable code part.
|
180
|
+
"""
|
181
|
+
return types.Part.from_executable_code(
|
182
|
+
code=code,
|
183
|
+
language='PYTHON',
|
184
|
+
)
|
185
|
+
|
186
|
+
@staticmethod
|
187
|
+
def build_code_execution_result_part(
|
188
|
+
code_execution_result: CodeExecutionResult,
|
189
|
+
) -> types.Part:
|
190
|
+
"""Builds the code execution result part from the code execution result.
|
191
|
+
|
192
|
+
Args:
|
193
|
+
code_execution_result: The code execution result.
|
194
|
+
|
195
|
+
Returns:
|
196
|
+
The constructed code execution result part.
|
197
|
+
"""
|
198
|
+
if code_execution_result.stderr:
|
199
|
+
return types.Part.from_code_execution_result(
|
200
|
+
outcome='OUTCOME_FAILED',
|
201
|
+
output=code_execution_result.stderr,
|
202
|
+
)
|
203
|
+
final_result = []
|
204
|
+
if code_execution_result.stdout or not code_execution_result.output_files:
|
205
|
+
final_result.append(
|
206
|
+
'Code execution result:\n' + '%s\n' % code_execution_result.stdout
|
207
|
+
)
|
208
|
+
if code_execution_result.output_files:
|
209
|
+
final_result.append(
|
210
|
+
'Saved artifacts:\n'
|
211
|
+
+ ','.join(
|
212
|
+
['`%s`' % f.name for f in code_execution_result.output_files]
|
213
|
+
)
|
214
|
+
)
|
215
|
+
return types.Part.from_code_execution_result(
|
216
|
+
outcome='OUTCOME_OK',
|
217
|
+
output='\n\n'.join(final_result),
|
218
|
+
)
|
219
|
+
|
220
|
+
@staticmethod
|
221
|
+
def convert_code_execution_parts(
|
222
|
+
content: types.Content,
|
223
|
+
code_block_delimiter: tuple[str, str],
|
224
|
+
execution_result_delimiters: tuple[str, str],
|
225
|
+
):
|
226
|
+
"""Converts the code execution parts to text parts in a Content.
|
227
|
+
|
228
|
+
Args:
|
229
|
+
content: The mutable content to convert the code execution parts to text
|
230
|
+
parts.
|
231
|
+
code_block_delimiter: The delimiter to format the code block.
|
232
|
+
execution_result_delimiters: The delimiter to format the code execution
|
233
|
+
result.
|
234
|
+
"""
|
235
|
+
if not content.parts:
|
236
|
+
return
|
237
|
+
|
238
|
+
# Handle the conversion of trailing executable code parts.
|
239
|
+
if content.parts[-1].executable_code:
|
240
|
+
content.parts[-1] = types.Part(
|
241
|
+
text=(
|
242
|
+
code_block_delimiter[0]
|
243
|
+
+ content.parts[-1].executable_code.code
|
244
|
+
+ code_block_delimiter[1]
|
245
|
+
)
|
246
|
+
)
|
247
|
+
# Handle the conversion of trailing code execution result parts.
|
248
|
+
# Skip if the Content has multiple parts, which means the Content is
|
249
|
+
# likely generated by the model.
|
250
|
+
elif len(content.parts) == 1 and content.parts[-1].code_execution_result:
|
251
|
+
content.parts[-1] = types.Part(
|
252
|
+
text=execution_result_delimiters[0]
|
253
|
+
+ content.parts[-1].code_execution_result.output
|
254
|
+
+ execution_result_delimiters[1]
|
255
|
+
)
|
256
|
+
content.role = 'user'
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
"""The persistent context used to configure the code executor."""
|
16
|
+
|
17
|
+
import copy
|
18
|
+
import dataclasses
|
19
|
+
import datetime
|
20
|
+
from typing import Any
|
21
|
+
from typing import Optional
|
22
|
+
|
23
|
+
from ..sessions.state import State
|
24
|
+
from .code_execution_utils import File
|
25
|
+
|
26
|
+
_CONTEXT_KEY = '_code_execution_context'
|
27
|
+
_SESSION_ID_KEY = 'execution_session_id'
|
28
|
+
_PROCESSED_FILE_NAMES_KEY = 'processed_input_files'
|
29
|
+
_INPUT_FILE_KEY = '_code_executor_input_files'
|
30
|
+
_ERROR_COUNT_KEY = '_code_executor_error_counts'
|
31
|
+
|
32
|
+
_CODE_EXECUTION_RESULTS_KEY = '_code_execution_results'
|
33
|
+
|
34
|
+
|
35
|
+
class CodeExecutorContext:
|
36
|
+
"""The persistent context used to configure the code executor."""
|
37
|
+
|
38
|
+
_context: dict[str, Any]
|
39
|
+
|
40
|
+
def __init__(self, session_state: State):
|
41
|
+
"""Initializes the code executor context.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
session_state: The session state to get the code executor context from.
|
45
|
+
"""
|
46
|
+
self._context = self._get_code_executor_context(session_state)
|
47
|
+
self._session_state = session_state
|
48
|
+
|
49
|
+
def get_state_delta(self) -> dict[str, Any]:
|
50
|
+
"""Gets the state delta to update in the persistent session state.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
The state delta to update in the persistent session state.
|
54
|
+
"""
|
55
|
+
context_to_update = copy.deepcopy(self._context)
|
56
|
+
return {_CONTEXT_KEY: context_to_update}
|
57
|
+
|
58
|
+
def get_execution_id(self) -> Optional[str]:
|
59
|
+
"""Gets the session ID for the code executor.
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
The session ID for the code executor context.
|
63
|
+
"""
|
64
|
+
if _SESSION_ID_KEY not in self._context:
|
65
|
+
return None
|
66
|
+
return self._context[_SESSION_ID_KEY]
|
67
|
+
|
68
|
+
def set_execution_id(self, session_id: str):
|
69
|
+
"""Sets the session ID for the code executor.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
session_id: The session ID for the code executor.
|
73
|
+
"""
|
74
|
+
self._context[_SESSION_ID_KEY] = session_id
|
75
|
+
|
76
|
+
def get_processed_file_names(self) -> list[str]:
|
77
|
+
"""Gets the processed file names from the session state.
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
A list of processed file names in the code executor context.
|
81
|
+
"""
|
82
|
+
if _PROCESSED_FILE_NAMES_KEY not in self._context:
|
83
|
+
return []
|
84
|
+
return self._context[_PROCESSED_FILE_NAMES_KEY]
|
85
|
+
|
86
|
+
def add_processed_file_names(self, file_names: [str]):
|
87
|
+
"""Adds the processed file name to the session state.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
file_names: The processed file names to add to the session state.
|
91
|
+
"""
|
92
|
+
if _PROCESSED_FILE_NAMES_KEY not in self._context:
|
93
|
+
self._context[_PROCESSED_FILE_NAMES_KEY] = []
|
94
|
+
self._context[_PROCESSED_FILE_NAMES_KEY].extend(file_names)
|
95
|
+
|
96
|
+
def get_input_files(self) -> list[File]:
|
97
|
+
"""Gets the code executor input file names from the session state.
|
98
|
+
|
99
|
+
Returns:
|
100
|
+
A list of input files in the code executor context.
|
101
|
+
"""
|
102
|
+
if _INPUT_FILE_KEY not in self._session_state:
|
103
|
+
return []
|
104
|
+
return [File(**file) for file in self._session_state[_INPUT_FILE_KEY]]
|
105
|
+
|
106
|
+
def add_input_files(
|
107
|
+
self,
|
108
|
+
input_files: list[File],
|
109
|
+
):
|
110
|
+
"""Adds the input files to the code executor context.
|
111
|
+
|
112
|
+
Args:
|
113
|
+
input_files: The input files to add to the code executor context.
|
114
|
+
"""
|
115
|
+
if _INPUT_FILE_KEY not in self._session_state:
|
116
|
+
self._session_state[_INPUT_FILE_KEY] = []
|
117
|
+
for input_file in input_files:
|
118
|
+
self._session_state[_INPUT_FILE_KEY].append(
|
119
|
+
dataclasses.asdict(input_file)
|
120
|
+
)
|
121
|
+
|
122
|
+
def clear_input_files(self):
|
123
|
+
"""Removes the input files and processed file names to the code executor context."""
|
124
|
+
if _INPUT_FILE_KEY in self._session_state:
|
125
|
+
self._session_state[_INPUT_FILE_KEY] = []
|
126
|
+
if _PROCESSED_FILE_NAMES_KEY in self._context:
|
127
|
+
self._context[_PROCESSED_FILE_NAMES_KEY] = []
|
128
|
+
|
129
|
+
def get_error_count(self, invocation_id: str) -> int:
|
130
|
+
"""Gets the error count from the session state.
|
131
|
+
|
132
|
+
Args:
|
133
|
+
invocation_id: The invocation ID to get the error count for.
|
134
|
+
|
135
|
+
Returns:
|
136
|
+
The error count for the given invocation ID.
|
137
|
+
"""
|
138
|
+
if _ERROR_COUNT_KEY not in self._session_state:
|
139
|
+
return 0
|
140
|
+
return self._session_state[_ERROR_COUNT_KEY].get(invocation_id, 0)
|
141
|
+
|
142
|
+
def increment_error_count(self, invocation_id: str):
|
143
|
+
"""Increments the error count from the session state.
|
144
|
+
|
145
|
+
Args:
|
146
|
+
invocation_id: The invocation ID to increment the error count for.
|
147
|
+
"""
|
148
|
+
if _ERROR_COUNT_KEY not in self._session_state:
|
149
|
+
self._session_state[_ERROR_COUNT_KEY] = {}
|
150
|
+
self._session_state[_ERROR_COUNT_KEY][invocation_id] = (
|
151
|
+
self.get_error_count(invocation_id) + 1
|
152
|
+
)
|
153
|
+
|
154
|
+
def reset_error_count(self, invocation_id: str):
|
155
|
+
"""Resets the error count from the session state.
|
156
|
+
|
157
|
+
Args:
|
158
|
+
invocation_id: The invocation ID to reset the error count for.
|
159
|
+
"""
|
160
|
+
if _ERROR_COUNT_KEY not in self._session_state:
|
161
|
+
return
|
162
|
+
if invocation_id in self._session_state[_ERROR_COUNT_KEY]:
|
163
|
+
del self._session_state[_ERROR_COUNT_KEY][invocation_id]
|
164
|
+
|
165
|
+
def update_code_execution_result(
|
166
|
+
self,
|
167
|
+
invocation_id: str,
|
168
|
+
code: str,
|
169
|
+
result_stdout: str,
|
170
|
+
result_stderr: str,
|
171
|
+
):
|
172
|
+
"""Updates the code execution result.
|
173
|
+
|
174
|
+
Args:
|
175
|
+
invocation_id: The invocation ID to update the code execution result for.
|
176
|
+
code: The code to execute.
|
177
|
+
result_stdout: The standard output of the code execution.
|
178
|
+
result_stderr: The standard error of the code execution.
|
179
|
+
"""
|
180
|
+
if _CODE_EXECUTION_RESULTS_KEY not in self._session_state:
|
181
|
+
self._session_state[_CODE_EXECUTION_RESULTS_KEY] = {}
|
182
|
+
if invocation_id not in self._session_state[_CODE_EXECUTION_RESULTS_KEY]:
|
183
|
+
self._session_state[_CODE_EXECUTION_RESULTS_KEY][invocation_id] = []
|
184
|
+
self._session_state[_CODE_EXECUTION_RESULTS_KEY][invocation_id].append({
|
185
|
+
'code': code,
|
186
|
+
'result_stdout': result_stdout,
|
187
|
+
'result_stderr': result_stderr,
|
188
|
+
'timestamp': int(datetime.datetime.now().timestamp()),
|
189
|
+
})
|
190
|
+
|
191
|
+
def _get_code_executor_context(self, session_state: State) -> dict[str, Any]:
|
192
|
+
"""Gets the code executor context from the session state.
|
193
|
+
|
194
|
+
Args:
|
195
|
+
session_state: The session state to get the code executor context from.
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
A dict of code executor context.
|
199
|
+
"""
|
200
|
+
if _CONTEXT_KEY not in session_state:
|
201
|
+
session_state[_CONTEXT_KEY] = {}
|
202
|
+
return session_state[_CONTEXT_KEY]
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
import atexit
|
16
|
+
import os
|
17
|
+
from typing import Optional
|
18
|
+
|
19
|
+
import docker
|
20
|
+
from docker.client import DockerClient
|
21
|
+
from docker.models.containers import Container
|
22
|
+
from pydantic import Field
|
23
|
+
from typing_extensions import override
|
24
|
+
|
25
|
+
from ..agents.invocation_context import InvocationContext
|
26
|
+
from .base_code_executor import BaseCodeExecutor
|
27
|
+
from .code_execution_utils import CodeExecutionInput
|
28
|
+
from .code_execution_utils import CodeExecutionResult
|
29
|
+
|
30
|
+
|
31
|
+
DEFAULT_IMAGE_TAG = 'adk-code-executor:latest'
|
32
|
+
|
33
|
+
|
34
|
+
class ContainerCodeExecutor(BaseCodeExecutor):
|
35
|
+
"""A code executor that uses a custom container to execute code.
|
36
|
+
|
37
|
+
Attributes:
|
38
|
+
base_url: Optional. The base url of the user hosted Docker client.
|
39
|
+
image: The tag of the predefined image or custom image to run on the
|
40
|
+
container. Either docker_path or image must be set.
|
41
|
+
docker_path: The path to the directory containing the Dockerfile. If set,
|
42
|
+
build the image from the dockerfile path instead of using the predefined
|
43
|
+
image. Either docker_path or image must be set.
|
44
|
+
"""
|
45
|
+
|
46
|
+
base_url: Optional[str] = None
|
47
|
+
"""
|
48
|
+
Optional. The base url of the user hosted Docker client.
|
49
|
+
"""
|
50
|
+
|
51
|
+
image: str = None
|
52
|
+
"""
|
53
|
+
The tag of the predefined image or custom image to run on the container.
|
54
|
+
Either docker_path or image must be set.
|
55
|
+
"""
|
56
|
+
|
57
|
+
docker_path: str = None
|
58
|
+
"""
|
59
|
+
The path to the directory containing the Dockerfile.
|
60
|
+
If set, build the image from the dockerfile path instead of using the
|
61
|
+
predefined image. Either docker_path or image must be set.
|
62
|
+
"""
|
63
|
+
|
64
|
+
# Overrides the BaseCodeExecutor attribute: this executor cannot be stateful.
|
65
|
+
stateful: bool = Field(default=False, frozen=True, exclude=True)
|
66
|
+
|
67
|
+
# Overrides the BaseCodeExecutor attribute: this executor cannot
|
68
|
+
# optimize_data_file.
|
69
|
+
optimize_data_file: bool = Field(default=False, frozen=True, exclude=True)
|
70
|
+
|
71
|
+
_client: DockerClient = None
|
72
|
+
_container: Container = None
|
73
|
+
|
74
|
+
def __init__(
|
75
|
+
self,
|
76
|
+
base_url: Optional[str] = None,
|
77
|
+
image: Optional[str] = None,
|
78
|
+
docker_path: Optional[str] = None,
|
79
|
+
**data,
|
80
|
+
):
|
81
|
+
"""Initializes the ContainerCodeExecutor.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
base_url: Optional. The base url of the user hosted Docker client.
|
85
|
+
image: The tag of the predefined image or custom image to run on the
|
86
|
+
container. Either docker_path or image must be set.
|
87
|
+
docker_path: The path to the directory containing the Dockerfile. If set,
|
88
|
+
build the image from the dockerfile path instead of using the predefined
|
89
|
+
image. Either docker_path or image must be set.
|
90
|
+
**data: The data to initialize the ContainerCodeExecutor.
|
91
|
+
"""
|
92
|
+
if not image and not docker_path:
|
93
|
+
raise ValueError(
|
94
|
+
'Either image or docker_path must be set for ContainerCodeExecutor.'
|
95
|
+
)
|
96
|
+
if 'stateful' in data and data['stateful']:
|
97
|
+
raise ValueError('Cannot set `stateful=True` in ContainerCodeExecutor.')
|
98
|
+
if 'optimize_data_file' in data and data['optimize_data_file']:
|
99
|
+
raise ValueError(
|
100
|
+
'Cannot set `optimize_data_file=True` in ContainerCodeExecutor.'
|
101
|
+
)
|
102
|
+
|
103
|
+
super().__init__(**data)
|
104
|
+
self.base_url = base_url
|
105
|
+
self.image = image if image else DEFAULT_IMAGE_TAG
|
106
|
+
self.docker_path = os.path.abspath(docker_path) if docker_path else None
|
107
|
+
|
108
|
+
self._client = (
|
109
|
+
docker.from_env()
|
110
|
+
if not self.base_url
|
111
|
+
else docker.DockerClient(base_url=self.base_url)
|
112
|
+
)
|
113
|
+
# Initialize the container.
|
114
|
+
self.__init_container()
|
115
|
+
|
116
|
+
# Close the container when the on exit.
|
117
|
+
atexit.register(self.__cleanup_container)
|
118
|
+
|
119
|
+
@override
|
120
|
+
def execute_code(
|
121
|
+
self,
|
122
|
+
invocation_context: InvocationContext,
|
123
|
+
code_execution_input: CodeExecutionInput,
|
124
|
+
) -> CodeExecutionResult:
|
125
|
+
output = ''
|
126
|
+
error = ''
|
127
|
+
exec_result = self._container.exec_run(
|
128
|
+
['python3', '-c', code_execution_input.code],
|
129
|
+
demux=True,
|
130
|
+
)
|
131
|
+
|
132
|
+
if exec_result.output and exec_result.output[0]:
|
133
|
+
output = exec_result.output[0].decode('utf-8')
|
134
|
+
if (
|
135
|
+
exec_result.output
|
136
|
+
and len(exec_result.output) > 1
|
137
|
+
and exec_result.output[1]
|
138
|
+
):
|
139
|
+
error = exec_result.output[1].decode('utf-8')
|
140
|
+
|
141
|
+
# Collect the final result.
|
142
|
+
return CodeExecutionResult(
|
143
|
+
stdout=output,
|
144
|
+
stderr=error,
|
145
|
+
output_files=[],
|
146
|
+
)
|
147
|
+
|
148
|
+
def _build_docker_image(self):
|
149
|
+
"""Builds the Docker image."""
|
150
|
+
if not self.docker_path:
|
151
|
+
raise ValueError('Docker path is not set.')
|
152
|
+
if not os.path.exists(self.docker_path):
|
153
|
+
raise FileNotFoundError(f'Invalid Docker path: {self.docker_path}')
|
154
|
+
|
155
|
+
print('Building Docker image...')
|
156
|
+
self._client.images.build(
|
157
|
+
path=self.docker_path,
|
158
|
+
tag=self.image,
|
159
|
+
rm=True,
|
160
|
+
)
|
161
|
+
print(f'Docker image: {self.image} built.')
|
162
|
+
|
163
|
+
def _verify_python_installation(self):
|
164
|
+
"""Verifies the container has python3 installed."""
|
165
|
+
exec_result = self._container.exec_run(['which', 'python3'])
|
166
|
+
if exec_result.exit_code != 0:
|
167
|
+
raise ValueError('python3 is not installed in the container.')
|
168
|
+
|
169
|
+
def __init_container(self):
|
170
|
+
"""Initializes the container."""
|
171
|
+
if not self._client:
|
172
|
+
raise RuntimeError('Docker client is not initialized.')
|
173
|
+
|
174
|
+
if self.docker_path:
|
175
|
+
self._build_docker_image()
|
176
|
+
|
177
|
+
print('Starting container for ContainerCodeExecutor...')
|
178
|
+
self._container = self._client.containers.run(
|
179
|
+
image=self.image,
|
180
|
+
detach=True,
|
181
|
+
tty=True,
|
182
|
+
)
|
183
|
+
print(f'Container {self._container.id} started.')
|
184
|
+
|
185
|
+
# Verify the container is able to run python3.
|
186
|
+
self._verify_python_installation()
|
187
|
+
|
188
|
+
def __cleanup_container(self):
|
189
|
+
"""Closes the container on exit."""
|
190
|
+
if not self._container:
|
191
|
+
return
|
192
|
+
|
193
|
+
print('[Cleanup] Stopping the container...')
|
194
|
+
self._container.stop()
|
195
|
+
self._container.remove()
|
196
|
+
print(f'Container {self._container.id} stopped and removed.')
|