Skip to content

Commit 5526767

Browse files
Merge branch 'main' into fix/validation-and-enum-issues
2 parents b6cdaaa + 71b3289 commit 5526767

File tree

7 files changed

+115
-51
lines changed

7 files changed

+115
-51
lines changed

.github/workflows/stale-bot.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
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+
115
name: ADK Stale Issue Auditor
216

317
on:

contributing/samples/adk_stale_agent/PROMPT_INSTRUCTION.txt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,24 +45,29 @@ Your job is to analyze a specific issue and report findings before taking action
4545
- **IF True**: The user edited the description silently, and we haven't alerted yet.
4646
-> **Action**: Call `alert_maintainer_of_edit`.
4747
-> **Report**: "Analysis for Issue #[number]: ACTIVE. Silent update detected (Description Edit). Alerted maintainer."
48-
- **IF False**:
48+
- **IF False**:
4949
-> **Report**: "Analysis for Issue #[number]: ACTIVE. Last action was by user. No action."
5050

5151
- **Check Role**: If `last_action_role` is 'maintainer':
5252
- **Proceed to STEP 3.**
5353

5454
**STEP 3: ANALYZE MAINTAINER INTENT**
5555
- **Context**: The last person to act was a Maintainer.
56-
- **Action**: Read the text in `last_comment_text`.
57-
- **Question Check**: Does the text ask a question, request clarification, ask for logs, or suggest trying a fix?
56+
- **Action**: Analyze `last_comment_text` using `maintainers` list and `last_actor_name`.
57+
58+
- **Internal Discussion Check**: Does the comment mention or address any username found in the `maintainers` list (other than the speaker `last_actor_name`)?
59+
- **Verdict**: **ACTIVE** (Internal Team Discussion).
60+
- **Report**: "Analysis for Issue #[number]: ACTIVE. Maintainer is discussing with another maintainer. No action."
61+
62+
- **Question Check**: Does the text ask a question, request clarification, ask for logs, or give suggestions?
5863
- **Time Check**: Is `days_since_activity` > {stale_threshold_days}?
5964

6065
- **DECISION**:
61-
- **IF (Question == YES) AND (Time == YES)**:
66+
- **IF (Question == YES) AND (Time == YES) AND (Internal Discussion Check == FALSE):**
6267
- **Action**: Call `add_stale_label_and_comment`.
63-
- **Check**: If '{REQUEST_CLARIFICATION_LABEL}' is not in `current_labels`, call `add_label_to_issue` for it.
68+
- **Check**: If '{REQUEST_CLARIFICATION_LABEL}' is not in `current_labels`, call `add_label_to_issue` with '{REQUEST_CLARIFICATION_LABEL}'.
6469
- **Report**: "Analysis for Issue #[number]: STALE. Maintainer asked question [days_since_activity] days ago. Marking stale."
6570
- **IF (Question == YES) BUT (Time == NO)**:
6671
- **Report**: "Analysis for Issue #[number]: PENDING. Maintainer asked question, but threshold not met yet. No action."
67-
- **IF (Question == NO)** (e.g., "I am working on this"):
68-
- **Report**: "Analysis for Issue #[number]: ACTIVE. Maintainer gave status update (not a question). No action."
72+
- **IF (Question == NO) OR (Internal Discussion Check == TRUE):**
73+
- **Report**: "Analysis for Issue #[number]: ACTIVE. Maintainer gave status update or internal discussion detected. No action."

contributing/samples/adk_stale_agent/agent.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,11 +318,13 @@ def _replay_history_to_find_state(
318318
- last_activity_time (datetime): Timestamp of the last human action.
319319
- last_action_type (str): The type of the last action (e.g., 'commented').
320320
- last_comment_text (Optional[str]): The text of the last comment.
321+
- last_actor_name (str): The specific username of the last actor.
321322
"""
322323
last_action_role = "author"
323324
last_activity_time = history[0]["time"]
324325
last_action_type = "created"
325326
last_comment_text = None
327+
last_actor_name = issue_author
326328

327329
for event in history:
328330
actor = event["actor"]
@@ -337,6 +339,7 @@ def _replay_history_to_find_state(
337339
last_action_role = role
338340
last_activity_time = event["time"]
339341
last_action_type = etype
342+
last_actor_name = actor
340343

341344
# Only store text if it was a comment (resets on other events like labels/edits)
342345
if etype == "commented":
@@ -349,6 +352,7 @@ def _replay_history_to_find_state(
349352
"last_activity_time": last_activity_time,
350353
"last_action_type": last_action_type,
351354
"last_comment_text": last_comment_text,
355+
"last_actor_name": last_actor_name,
352356
}
353357

354358

@@ -428,6 +432,7 @@ def get_issue_state(item_number: int) -> Dict[str, Any]:
428432
"status": "success",
429433
"last_action_role": state["last_action_role"],
430434
"last_action_type": state["last_action_type"],
435+
"last_actor_name": state["last_actor_name"],
431436
"maintainer_alert_needed": maintainer_alert_needed,
432437
"is_stale": is_stale,
433438
"days_since_activity": days_since_activity,
@@ -436,6 +441,8 @@ def get_issue_state(item_number: int) -> Dict[str, Any]:
436441
"current_labels": labels_list,
437442
"stale_threshold_days": STALE_HOURS_THRESHOLD / 24,
438443
"close_threshold_days": CLOSE_HOURS_AFTER_STALE_THRESHOLD / 24,
444+
"maintainers": maintainers,
445+
"issue_author": issue_author,
439446
}
440447

441448
except RequestException as e:

src/google/adk/a2a/converters/event_converter.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,11 @@ def convert_a2a_task_to_event(
229229
message = Message(
230230
message_id="", role=Role.agent, parts=a2a_task.artifacts[-1].parts
231231
)
232-
elif a2a_task.status and a2a_task.status.message:
232+
elif (
233+
a2a_task.status
234+
and a2a_task.status.message
235+
and a2a_task.status.message.parts
236+
):
233237
message = a2a_task.status.message
234238
elif a2a_task.history:
235239
message = a2a_task.history[-1]

src/google/adk/tools/api_registry.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,13 @@ def get_toolset(
102102
mcp_server_url = server["urls"][0]
103103
headers = self._get_auth_headers()
104104

105+
# Only prepend "https://" if the URL doesn't already have a scheme
106+
if not mcp_server_url.startswith(("http://", "https://")):
107+
mcp_server_url = "https://" + mcp_server_url
108+
105109
return McpToolset(
106110
connection_params=StreamableHTTPConnectionParams(
107-
url="https://" + mcp_server_url,
111+
url=mcp_server_url,
108112
headers=headers,
109113
),
110114
tool_filter=tool_filter,

tests/unittests/a2a/converters/test_event_converter.py

Lines changed: 22 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -773,13 +773,9 @@ def test_convert_a2a_task_to_event_message_conversion_error(self):
773773
from google.adk.a2a.converters.event_converter import convert_a2a_task_to_event
774774

775775
# Create mock message and task
776-
mock_message = Mock(spec=Message)
777-
mock_status = Mock()
778-
mock_status.message = mock_message
779-
mock_task = Mock(spec=Task)
780-
mock_task.artifacts = None
781-
mock_task.status = mock_status
782-
mock_task.history = []
776+
mock_message = Mock(spec=Message, parts=[Mock()])
777+
mock_status = Mock(message=mock_message)
778+
mock_task = Mock(spec=Task, artifacts=None, status=mock_status, history=[])
783779

784780
# Mock the convert_a2a_message_to_event function to raise an exception
785781
with patch(
@@ -798,11 +794,9 @@ def test_convert_a2a_message_to_event_success(self):
798794
# Create mock parts and message with valid genai Part
799795
mock_a2a_part = Mock()
800796
mock_genai_part = genai_types.Part(text="test content")
801-
mock_convert_part = Mock()
802-
mock_convert_part.return_value = mock_genai_part
797+
mock_convert_part = Mock(return_value=mock_genai_part)
803798

804-
mock_message = Mock(spec=Message)
805-
mock_message.parts = [mock_a2a_part]
799+
mock_message = Mock(spec=Message, parts=[mock_a2a_part])
806800

807801
result = convert_a2a_message_to_event(
808802
mock_message,
@@ -829,11 +823,9 @@ def test_convert_a2a_message_to_event_with_multiple_parts_returned(self):
829823
mock_a2a_part = Mock()
830824
mock_genai_part1 = genai_types.Part(text="part 1")
831825
mock_genai_part2 = genai_types.Part(text="part 2")
832-
mock_convert_part = Mock()
833-
mock_convert_part.return_value = [mock_genai_part1, mock_genai_part2]
826+
mock_convert_part = Mock(return_value=[mock_genai_part1, mock_genai_part2])
834827

835-
mock_message = Mock(spec=Message)
836-
mock_message.parts = [mock_a2a_part]
828+
mock_message = Mock(spec=Message, parts=[mock_a2a_part])
837829

838830
# Act
839831
result = convert_a2a_message_to_event(
@@ -855,13 +847,10 @@ def test_convert_a2a_message_to_event_with_long_running_tools(self):
855847
from google.adk.a2a.converters.event_converter import convert_a2a_message_to_event
856848

857849
# Create mock parts and message
858-
mock_a2a_part = Mock()
859-
mock_message = Mock(spec=Message)
860-
mock_message.parts = [mock_a2a_part]
850+
mock_message = Mock(spec=Message, parts=[Mock()])
861851

862852
# Mock the part conversion to return None to simulate long-running tool detection logic
863-
mock_convert_part = Mock()
864-
mock_convert_part.return_value = None
853+
mock_convert_part = Mock(return_value=None)
865854

866855
# Patch the long-running tool detection since the main logic is in the actual conversion
867856
with patch(
@@ -884,8 +873,7 @@ def test_convert_a2a_message_to_event_empty_parts(self):
884873
"""Test conversion with empty parts list."""
885874
from google.adk.a2a.converters.event_converter import convert_a2a_message_to_event
886875

887-
mock_message = Mock(spec=Message)
888-
mock_message.parts = []
876+
mock_message = Mock(spec=Message, parts=[])
889877

890878
result = convert_a2a_message_to_event(
891879
mock_message, "test-author", self.mock_invocation_context
@@ -910,11 +898,9 @@ def test_convert_a2a_message_to_event_part_conversion_fails(self):
910898

911899
# Setup mock to return None (conversion failure)
912900
mock_a2a_part = Mock()
913-
mock_convert_part = Mock()
914-
mock_convert_part.return_value = None
901+
mock_convert_part = Mock(return_value=None)
915902

916-
mock_message = Mock(spec=Message)
917-
mock_message.parts = [mock_a2a_part]
903+
mock_message = Mock(spec=Message, parts=[mock_a2a_part])
918904

919905
result = convert_a2a_message_to_event(
920906
mock_message,
@@ -939,14 +925,14 @@ def test_convert_a2a_message_to_event_part_conversion_exception(self):
939925
mock_a2a_part2 = Mock()
940926
mock_genai_part = genai_types.Part(text="successful conversion")
941927

942-
mock_convert_part = Mock()
943-
mock_convert_part.side_effect = [
944-
Exception("Conversion failed"), # First part fails
945-
mock_genai_part, # Second part succeeds
946-
]
928+
mock_convert_part = Mock(
929+
side_effect=[
930+
Exception("Conversion failed"), # First part fails
931+
mock_genai_part, # Second part succeeds
932+
]
933+
)
947934

948-
mock_message = Mock(spec=Message)
949-
mock_message.parts = [mock_a2a_part1, mock_a2a_part2]
935+
mock_message = Mock(spec=Message, parts=[mock_a2a_part1, mock_a2a_part2])
950936

951937
result = convert_a2a_message_to_event(
952938
mock_message,
@@ -967,13 +953,10 @@ def test_convert_a2a_message_to_event_missing_tool_id(self):
967953
from google.adk.a2a.converters.event_converter import convert_a2a_message_to_event
968954

969955
# Create mock parts and message
970-
mock_a2a_part = Mock()
971-
mock_message = Mock(spec=Message)
972-
mock_message.parts = [mock_a2a_part]
956+
mock_message = Mock(spec=Message, parts=[Mock()])
973957

974958
# Mock the part conversion to return None
975-
mock_convert_part = Mock()
976-
mock_convert_part.return_value = None
959+
mock_convert_part = Mock(return_value=None)
977960

978961
result = convert_a2a_message_to_event(
979962
mock_message,
@@ -994,8 +977,7 @@ def test_convert_a2a_message_to_event_default_author(self, mock_uuid):
994977
"""Test conversion with default author and no invocation context."""
995978
from google.adk.a2a.converters.event_converter import convert_a2a_message_to_event
996979

997-
mock_message = Mock(spec=Message)
998-
mock_message.parts = []
980+
mock_message = Mock(spec=Message, parts=[])
999981

1000982
# Mock UUID generation
1001983
mock_uuid.return_value = "generated-uuid"

tests/unittests/tools/test_api_registry.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from unittest.mock import MagicMock
1919
from unittest.mock import patch
2020

21+
from google.adk.tools import api_registry
2122
from google.adk.tools.api_registry import ApiRegistry
2223
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
2324
import httpx
@@ -35,6 +36,14 @@
3536
{
3637
"name": "test-mcp-server-no-url",
3738
},
39+
{
40+
"name": "test-mcp-server-http",
41+
"urls": ["http://mcp.server_http.com"],
42+
},
43+
{
44+
"name": "test-mcp-server-https",
45+
"urls": ["https://mcp.server_https.com"],
46+
},
3847
]
3948
}
4049

@@ -70,10 +79,12 @@ def test_init_success(self, MockHttpClient):
7079
api_registry_project_id=self.project_id, location=self.location
7180
)
7281

73-
self.assertEqual(len(api_registry._mcp_servers), 3)
82+
self.assertEqual(len(api_registry._mcp_servers), 5)
7483
self.assertIn("test-mcp-server-1", api_registry._mcp_servers)
7584
self.assertIn("test-mcp-server-2", api_registry._mcp_servers)
7685
self.assertIn("test-mcp-server-no-url", api_registry._mcp_servers)
86+
self.assertIn("test-mcp-server-http", api_registry._mcp_servers)
87+
self.assertIn("test-mcp-server-https", api_registry._mcp_servers)
7788
mock_client_instance.get.assert_called_once_with(
7889
f"https://cloudapiregistry.googleapis.com/v1beta/projects/{self.project_id}/locations/{self.location}/mcpServers",
7990
headers={
@@ -95,10 +106,12 @@ def test_init_with_quota_project_id_success(self, MockHttpClient):
95106
api_registry_project_id=self.project_id, location=self.location
96107
)
97108

98-
self.assertEqual(len(api_registry._mcp_servers), 3)
109+
self.assertEqual(len(api_registry._mcp_servers), 5)
99110
self.assertIn("test-mcp-server-1", api_registry._mcp_servers)
100111
self.assertIn("test-mcp-server-2", api_registry._mcp_servers)
101112
self.assertIn("test-mcp-server-no-url", api_registry._mcp_servers)
113+
self.assertIn("test-mcp-server-http", api_registry._mcp_servers)
114+
self.assertIn("test-mcp-server-https", api_registry._mcp_servers)
102115
mock_client_instance.get.assert_called_once_with(
103116
f"https://cloudapiregistry.googleapis.com/v1beta/projects/{self.project_id}/locations/{self.location}/mcpServers",
104117
headers={
@@ -232,6 +245,41 @@ async def test_get_toolset_with_filter_and_prefix(
232245
)
233246
self.assertEqual(toolset, MockMcpToolset.return_value)
234247

248+
def test_get_toolset_url_scheme(self):
249+
params = [
250+
("test-mcp-server-http", "http://mcp.server_http.com"),
251+
("test-mcp-server-https", "https://mcp.server_https.com"),
252+
]
253+
for mock_server_name, mock_url in params:
254+
with self.subTest(server_name=mock_server_name):
255+
with (
256+
patch.object(httpx, "Client", autospec=True) as MockHttpClient,
257+
patch.object(
258+
api_registry, "McpToolset", autospec=True
259+
) as MockMcpToolset,
260+
):
261+
mock_response = create_autospec(httpx.Response, instance=True)
262+
mock_response.json.return_value = MOCK_MCP_SERVERS_LIST
263+
mock_client_instance = MockHttpClient.return_value
264+
mock_client_instance.__enter__.return_value = mock_client_instance
265+
mock_client_instance.get.return_value = mock_response
266+
267+
api_registry_instance = ApiRegistry(
268+
api_registry_project_id=self.project_id, location=self.location
269+
)
270+
271+
api_registry_instance.get_toolset(mock_server_name)
272+
273+
MockMcpToolset.assert_called_once_with(
274+
connection_params=StreamableHTTPConnectionParams(
275+
url=mock_url,
276+
headers={"Authorization": "Bearer mock_token"},
277+
),
278+
tool_filter=None,
279+
tool_name_prefix=None,
280+
header_provider=None,
281+
)
282+
235283
@patch("httpx.Client", autospec=True)
236284
async def test_get_toolset_server_not_found(self, MockHttpClient):
237285
mock_response = MagicMock()

0 commit comments

Comments
 (0)