Skip to content

Commit 9715a75

Browse files
committed
fix(code-executor): resolve deepcopy recursion in VertexAiCodeExecutor
Add __deepcopy__ method to handle extension serialization during agent deployment. Extension gets re-initialized automatically when needed. Fixes agent engine deployment failures.
1 parent 0bd05df commit 9715a75

File tree

2 files changed

+248
-0
lines changed

2 files changed

+248
-0
lines changed

src/google/adk/code_executors/vertex_ai_code_executor.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,42 @@ def __init__(
136136
"""
137137
super().__init__(**data)
138138
self.resource_name = resource_name
139+
self.initialize_extension()
140+
141+
def initialize_extension(self) -> None:
142+
"""Initializes the Vertex Code Interpreter Extension."""
139143
self._code_interpreter_extension = _get_code_interpreter_extension(
140144
self.resource_name
141145
)
142146

147+
def _ensure_extension_initialized(self) -> None:
148+
"""Ensures the extension is initialized, re-initializing if necessary."""
149+
if (
150+
not hasattr(self, '_code_interpreter_extension')
151+
or self._code_interpreter_extension is None
152+
):
153+
self.initialize_extension()
154+
155+
def __deepcopy__(self, memo: dict[int, Any] | None = None):
156+
# Create a copy by temporarily removing the problematic extension
157+
# Store the extension temporarily
158+
original_extension = getattr(self, '_code_interpreter_extension', None)
159+
160+
# Temporarily set extension to None
161+
self._code_interpreter_extension = None
162+
163+
# Now perform the deepcopy safely
164+
try:
165+
copied = super().__deepcopy__(memo)
166+
finally:
167+
# Restore the original extension
168+
self._code_interpreter_extension = original_extension
169+
170+
# Set the copied object's extension to None - it will be re-initialized when needed
171+
copied._code_interpreter_extension = None
172+
173+
return copied
174+
143175
@override
144176
def execute_code(
145177
self,
@@ -209,6 +241,9 @@ def _execute_code_interpreter(
209241
Returns:
210242
The response from the code interpreter extension.
211243
"""
244+
# Ensure extension is initialized (re-initialize if it was set to None during deepcopy)
245+
self._ensure_extension_initialized()
246+
212247
operation_params = {'code': code}
213248
if input_files:
214249
operation_params['files'] = [
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
"""
2+
Test suite for VertexAiCodeExecutor deep copy and extension initialization fixes.
3+
4+
This test validates the critical fixes made to address:
5+
1. Deep copy recursion errors during agent engine deployment
6+
2. Extension state management during serialization
7+
3. Automatic extension re-initialization after deep copy
8+
"""
9+
10+
import copy
11+
from unittest.mock import MagicMock
12+
from unittest.mock import Mock
13+
from unittest.mock import patch
14+
15+
from google.adk.agents.invocation_context import InvocationContext
16+
from google.adk.code_executors import VertexAiCodeExecutor
17+
from google.adk.code_executors.code_execution_utils import CodeExecutionInput
18+
from google.adk.code_executors.code_execution_utils import File
19+
import pytest
20+
21+
22+
class TestVertexAiCodeExecutorFixes:
23+
"""Test class for VertexAiCodeExecutor deep copy and extension fixes."""
24+
25+
@pytest.fixture
26+
def mock_extension(self):
27+
"""Create a mock extension for testing."""
28+
mock_ext = Mock()
29+
mock_ext.execute.return_value = {
30+
"execution_result": 'print("Hello World")\nHello World',
31+
"execution_error": "",
32+
"output_files": [],
33+
}
34+
return mock_ext
35+
36+
@pytest.fixture
37+
def code_executor(self, mock_extension):
38+
"""Create a VertexAiCodeExecutor instance with mocked extension."""
39+
with patch(
40+
"google.adk.code_executors.vertex_ai_code_executor._get_code_interpreter_extension",
41+
return_value=mock_extension,
42+
):
43+
executor = VertexAiCodeExecutor()
44+
return executor
45+
46+
def test_deep_copy_no_recursion_error(self, code_executor):
47+
"""Test that deep copy works without recursion errors."""
48+
# This was the core issue: deep copy would cause recursion errors
49+
# due to the extension object not being serializable
50+
try:
51+
copied_executor = copy.deepcopy(code_executor)
52+
assert copied_executor is not None
53+
assert copied_executor != code_executor # Different instances
54+
print("Deep copy completed without recursion errors")
55+
except RecursionError:
56+
pytest.fail("Deep copy still causes recursion error - fix not working")
57+
58+
def test_extension_state_after_deep_copy(self, code_executor, mock_extension):
59+
"""Test that extension is properly managed after deep copy."""
60+
# Original executor should have extension
61+
assert hasattr(code_executor, "_code_interpreter_extension")
62+
63+
# After deep copy, the copied object should have None extension initially
64+
copied_executor = copy.deepcopy(code_executor)
65+
66+
# The copied executor's extension should be None (to be re-initialized)
67+
assert copied_executor._code_interpreter_extension is None
68+
69+
# Original executor should still have its extension
70+
assert code_executor._code_interpreter_extension == mock_extension
71+
print("Extension state properly managed during deep copy")
72+
73+
def test_extension_re_initialization(self, mock_extension):
74+
"""Test that extension gets re-initialized when needed."""
75+
with patch(
76+
"google.adk.code_executors.vertex_ai_code_executor._get_code_interpreter_extension",
77+
return_value=mock_extension,
78+
) as mock_get_ext:
79+
80+
executor = VertexAiCodeExecutor()
81+
82+
# Deep copy the executor
83+
copied_executor = copy.deepcopy(executor)
84+
85+
# Extension should be None after deep copy
86+
assert copied_executor._code_interpreter_extension is None
87+
88+
# Call ensure_extension_initialized - should trigger re-initialization
89+
copied_executor._ensure_extension_initialized()
90+
91+
# Extension should now be re-initialized
92+
assert copied_executor._code_interpreter_extension == mock_extension
93+
print("Extension re-initialization working correctly")
94+
95+
def test_code_execution_after_deep_copy(self, code_executor, mock_extension):
96+
"""Test that code execution works after deep copy."""
97+
# Create test input
98+
invocation_context = Mock(spec=InvocationContext)
99+
code_input = CodeExecutionInput(
100+
code="print('Hello from copied executor')", execution_id="test-123"
101+
)
102+
103+
# Deep copy the executor
104+
copied_executor = copy.deepcopy(code_executor)
105+
106+
# Execute code with copied executor - should trigger re-initialization
107+
with patch(
108+
"google.adk.code_executors.vertex_ai_code_executor._get_code_interpreter_extension",
109+
return_value=mock_extension,
110+
):
111+
result = copied_executor.execute_code(invocation_context, code_input)
112+
113+
# Verify execution worked
114+
assert result is not None
115+
assert "Hello World" in result.stdout
116+
117+
# Verify extension was re-initialized during execution
118+
assert copied_executor._code_interpreter_extension == mock_extension
119+
print("Code execution works after deep copy with auto re-initialization")
120+
121+
def test_ensure_extension_initialized_idempotent(
122+
self, code_executor, mock_extension
123+
):
124+
"""Test that _ensure_extension_initialized is safe to call multiple times."""
125+
original_extension = code_executor._code_interpreter_extension
126+
127+
# Call multiple times
128+
code_executor._ensure_extension_initialized()
129+
code_executor._ensure_extension_initialized()
130+
code_executor._ensure_extension_initialized()
131+
132+
# Extension should remain the same
133+
assert code_executor._code_interpreter_extension == original_extension
134+
print("Extension initialization is idempotent")
135+
136+
def test_extension_initialization_with_resource_name(self, mock_extension):
137+
"""Test extension initialization with custom resource name."""
138+
resource_name = "projects/test/locations/us-central1/extensions/123"
139+
140+
with patch(
141+
"google.adk.code_executors.vertex_ai_code_executor._get_code_interpreter_extension",
142+
return_value=mock_extension,
143+
) as mock_get_ext:
144+
145+
executor = VertexAiCodeExecutor(resource_name=resource_name)
146+
147+
# Verify resource name was passed correctly
148+
mock_get_ext.assert_called_with(resource_name)
149+
assert executor.resource_name == resource_name
150+
print("Resource name properly handled during initialization")
151+
152+
@patch.dict(
153+
"os.environ", {"CODE_INTERPRETER_EXTENSION_NAME": "test-extension"}
154+
)
155+
def test_environment_variable_handling(self, mock_extension):
156+
"""Test that environment variables are properly handled."""
157+
with patch(
158+
"google.adk.code_executors.vertex_ai_code_executor._get_code_interpreter_extension",
159+
return_value=mock_extension,
160+
) as mock_get_ext:
161+
162+
executor = VertexAiCodeExecutor()
163+
164+
# Should use environment variable
165+
mock_get_ext.assert_called_with(None) # No resource_name passed
166+
print("Environment variable handling works correctly")
167+
168+
169+
def test_integration_with_agent_engine_deployment():
170+
"""Integration test simulating agent engine deployment process."""
171+
print("\nRunning integration test for agent engine deployment...")
172+
173+
with patch(
174+
"google.adk.code_executors.vertex_ai_code_executor._get_code_interpreter_extension"
175+
) as mock_get_ext:
176+
mock_extension = Mock()
177+
mock_extension.execute.return_value = {
178+
"execution_result": "Deployment test successful",
179+
"execution_error": "",
180+
"output_files": [],
181+
}
182+
mock_get_ext.return_value = mock_extension
183+
184+
# Create executor (simulating agent creation)
185+
executor = VertexAiCodeExecutor()
186+
187+
# Simulate agent engine deployment (which involves deep copying)
188+
try:
189+
serialized_executor = copy.deepcopy(executor)
190+
191+
# Simulate code execution on deployed agent
192+
invocation_context = Mock(spec=InvocationContext)
193+
code_input = CodeExecutionInput(
194+
code="print('Agent deployed successfully!')",
195+
execution_id="deployment-test",
196+
)
197+
198+
result = serialized_executor.execute_code(invocation_context, code_input)
199+
200+
assert result is not None
201+
assert "Deployment test successful" in result.stdout
202+
print(
203+
"Integration test passed - agent engine deployment simulation"
204+
" successful"
205+
)
206+
207+
except Exception as e:
208+
pytest.fail(f"Integration test failed: {e}")
209+
210+
211+
if __name__ == "__main__":
212+
# Run the tests
213+
pytest.main([__file__, "-v", "--tb=short"])

0 commit comments

Comments
 (0)