From 2702c77099cd9608558c27014ebfdec6f6701339 Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 3 Oct 2025 23:57:08 -0700 Subject: [PATCH 01/10] Add tests for tool call result event translation (cherry picked from commit 5921ef098448b5d5867486701e896c892c5edfdf) --- .../test_event_translator_comprehensive.py | 62 +++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/typescript-sdk/integrations/adk-middleware/python/tests/test_event_translator_comprehensive.py b/typescript-sdk/integrations/adk-middleware/python/tests/test_event_translator_comprehensive.py index 8475cc8cb..67d5b2f4c 100644 --- a/typescript-sdk/integrations/adk-middleware/python/tests/test_event_translator_comprehensive.py +++ b/typescript-sdk/integrations/adk-middleware/python/tests/test_event_translator_comprehensive.py @@ -1,6 +1,10 @@ #!/usr/bin/env python """Comprehensive tests for EventTranslator, focusing on untested paths.""" +import json +from dataclasses import asdict, dataclass +from types import SimpleNamespace + import pytest import uuid from unittest.mock import MagicMock, patch, AsyncMock @@ -109,15 +113,65 @@ async def test_translate_function_calls_detection(self, translator, mock_adk_eve async def test_translate_function_responses_handling(self, translator, mock_adk_event): """Test function responses handling.""" # Mock event with function responses - mock_function_response = MagicMock() - mock_adk_event.get_function_responses = MagicMock(return_value=[mock_function_response]) + function_response = SimpleNamespace(id="tool-1", response={"ok": True}) + mock_adk_event.get_function_calls = MagicMock(return_value=[]) + mock_adk_event.get_function_responses = MagicMock(return_value=[function_response]) events = [] async for event in translator.translate(mock_adk_event, "thread_1", "run_1"): events.append(event) - # Function responses should be handled but not emit events - assert len(events) == 0 + assert len(events) == 1 + event = events[0] + assert isinstance(event, ToolCallResultEvent) + assert json.loads(event.content) == {"ok": True} + + @pytest.mark.asyncio + async def test_translate_function_response_with_call_tool_result_payload(self, translator): + """Ensure CallToolResult payloads are serialized without errors.""" + + @dataclass + class TextContent: + text: str + + @dataclass + class CallToolResult: + meta: dict | None + structuredContent: dict | None + isError: bool + content: list[TextContent] + + repeated_text_entries = [ + "Primary Task: Provide a detailed walkthrough for the requested topic.", + "Primary Task: Provide a detailed walkthrough for the requested topic.", + "Constraints: Ensure clarity and maintain a concise explanation.", + "Constraints: Ensure clarity and maintain a concise explanation.", + ] + + payload = CallToolResult( + meta={"source": "test"}, + structuredContent=None, + isError=False, + content=[TextContent(text=text) for text in repeated_text_entries], + ) + + function_response = SimpleNamespace( + id="tool-structured-1", + response=asdict(payload), + ) + + events = [] + async for event in translator._translate_function_response([function_response]): + events.append(event) + + assert len(events) == 1 + event = events[0] + assert isinstance(event, ToolCallResultEvent) + + content = json.loads(event.content) + assert content["isError"] is False + assert content["structuredContent"] is None + assert [item["text"] for item in content["content"]] == repeated_text_entries @pytest.mark.asyncio async def test_translate_state_delta_event(self, translator, mock_adk_event): From 270022c924717bf38cff4f847a3adf0cf837b56f Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 4 Oct 2025 00:12:38 -0700 Subject: [PATCH 02/10] Improve tool response serialization --- .../python/src/ag_ui_adk/event_translator.py | 104 +++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/typescript-sdk/integrations/adk-middleware/python/src/ag_ui_adk/event_translator.py b/typescript-sdk/integrations/adk-middleware/python/src/ag_ui_adk/event_translator.py index efb674e17..d08e13ec6 100644 --- a/typescript-sdk/integrations/adk-middleware/python/src/ag_ui_adk/event_translator.py +++ b/typescript-sdk/integrations/adk-middleware/python/src/ag_ui_adk/event_translator.py @@ -2,6 +2,8 @@ """Event translator for converting ADK events to AG-UI protocol events.""" +import dataclasses +from collections.abc import Iterable, Mapping from typing import AsyncGenerator, Optional, Dict, Any , List import uuid @@ -21,6 +23,106 @@ logger = logging.getLogger(__name__) +def _coerce_tool_response(value: Any, _visited: Optional[set[int]] = None) -> Any: + """Recursively convert arbitrary tool responses into JSON-serializable structures.""" + + if isinstance(value, (str, int, float, bool)) or value is None: + return value + + if isinstance(value, (bytes, bytearray, memoryview)): + try: + return value.decode() # type: ignore[union-attr] + except Exception: + return list(value) + + if _visited is None: + _visited = set() + + obj_id = id(value) + if obj_id in _visited: + return str(value) + + _visited.add(obj_id) + try: + if dataclasses.is_dataclass(value) and not isinstance(value, type): + return { + field.name: _coerce_tool_response(getattr(value, field.name), _visited) + for field in dataclasses.fields(value) + } + + if hasattr(value, "_asdict") and callable(getattr(value, "_asdict")): + try: + return { + str(k): _coerce_tool_response(v, _visited) + for k, v in value._asdict().items() # type: ignore[attr-defined] + } + except Exception: + pass + + for method_name in ("model_dump", "to_dict"): + method = getattr(value, method_name, None) + if callable(method): + try: + dumped = method() + except TypeError: + try: + dumped = method(exclude_none=False) + except Exception: + continue + except Exception: + continue + + return _coerce_tool_response(dumped, _visited) + + if isinstance(value, Mapping): + return { + str(k): _coerce_tool_response(v, _visited) + for k, v in value.items() + } + + if isinstance(value, (list, tuple, set, frozenset)): + return [_coerce_tool_response(item, _visited) for item in value] + + if isinstance(value, Iterable): + try: + return [_coerce_tool_response(item, _visited) for item in list(value)] + except TypeError: + pass + + try: + obj_vars = vars(value) + except TypeError: + obj_vars = None + + if obj_vars: + coerced = { + key: _coerce_tool_response(val, _visited) + for key, val in obj_vars.items() + if not key.startswith("_") + } + if coerced: + return coerced + + return str(value) + finally: + _visited.discard(obj_id) + + +def _serialize_tool_response(response: Any) -> str: + """Serialize a tool response into a JSON string.""" + + try: + coerced = _coerce_tool_response(response) + return json.dumps(coerced, ensure_ascii=False) + except Exception as exc: + logger.warning("Failed to coerce tool response to JSON: %s", exc, exc_info=True) + try: + return json.dumps(str(response), ensure_ascii=False) + except Exception: + logger.warning("Failed to stringify tool response; returning empty string.") + return json.dumps("", ensure_ascii=False) + + class EventTranslator: """Translates Google ADK events to AG-UI protocol events. @@ -377,7 +479,7 @@ async def _translate_function_response( message_id=str(uuid.uuid4()), type=EventType.TOOL_CALL_RESULT, tool_call_id=tool_call_id, - content=json.dumps(func_response.response) + content=_serialize_tool_response(func_response.response) ) else: logger.debug(f"Skipping ToolCallResultEvent for long-running tool: {tool_call_id}") From 1b3099f07863b61bb4d08f17ecffa708dfeba00d Mon Sep 17 00:00:00 2001 From: Mark Fogle Date: Sat, 4 Oct 2025 00:21:53 -0700 Subject: [PATCH 03/10] test: cover tool call result serialization --- .../test_event_translator_comprehensive.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/typescript-sdk/integrations/adk-middleware/python/tests/test_event_translator_comprehensive.py b/typescript-sdk/integrations/adk-middleware/python/tests/test_event_translator_comprehensive.py index 67d5b2f4c..6444c0781 100644 --- a/typescript-sdk/integrations/adk-middleware/python/tests/test_event_translator_comprehensive.py +++ b/typescript-sdk/integrations/adk-middleware/python/tests/test_event_translator_comprehensive.py @@ -11,7 +11,8 @@ from ag_ui.core import ( EventType, TextMessageStartEvent, TextMessageContentEvent, TextMessageEndEvent, - ToolCallStartEvent, ToolCallArgsEvent, ToolCallEndEvent, StateDeltaEvent, CustomEvent + ToolCallStartEvent, ToolCallArgsEvent, ToolCallEndEvent, ToolCallResultEvent, + StateDeltaEvent, CustomEvent ) from google.adk.events import Event as ADKEvent from ag_ui_adk.event_translator import EventTranslator @@ -128,11 +129,14 @@ async def test_translate_function_responses_handling(self, translator, mock_adk_ @pytest.mark.asyncio async def test_translate_function_response_with_call_tool_result_payload(self, translator): - """Ensure CallToolResult payloads are serialized without errors.""" + """Ensure complex CallToolResult payloads are serialized correctly.""" @dataclass class TextContent: - text: str + type: str = "text" + text: str = "" + annotations: list | None = None + meta: dict | None = None @dataclass class CallToolResult: @@ -149,7 +153,7 @@ class CallToolResult: ] payload = CallToolResult( - meta={"source": "test"}, + meta=None, structuredContent=None, isError=False, content=[TextContent(text=text) for text in repeated_text_entries], @@ -157,7 +161,7 @@ class CallToolResult: function_response = SimpleNamespace( id="tool-structured-1", - response=asdict(payload), + response={"result": payload}, ) events = [] @@ -169,9 +173,9 @@ class CallToolResult: assert isinstance(event, ToolCallResultEvent) content = json.loads(event.content) - assert content["isError"] is False - assert content["structuredContent"] is None - assert [item["text"] for item in content["content"]] == repeated_text_entries + assert content["result"]["isError"] is False + assert content["result"]["structuredContent"] is None + assert [item["text"] for item in content["result"]["content"]] == repeated_text_entries @pytest.mark.asyncio async def test_translate_state_delta_event(self, translator, mock_adk_event): @@ -835,4 +839,4 @@ async def test_partial_streaming_continuation(self, translator, mock_adk_event_w # Should reset streaming state assert translator._is_streaming is False - assert translator._streaming_message_id is None \ No newline at end of file + assert translator._streaming_message_id is None From 979b3dcac0df6d3b3f4d6eeb5da3d8e9eb1065d6 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 4 Oct 2025 10:56:00 -0700 Subject: [PATCH 04/10] Update dojo-e2e.yml Setup Workload Identity Federation --- .github/workflows/dojo-e2e.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/dojo-e2e.yml b/.github/workflows/dojo-e2e.yml index b2458e0f4..b6ee133c4 100644 --- a/.github/workflows/dojo-e2e.yml +++ b/.github/workflows/dojo-e2e.yml @@ -153,6 +153,13 @@ jobs: echo "LANGSMITH_API_KEY=${LANGSMITH_API_KEY}" >> examples/typescript/.env echo "OPENAI_API_KEY=${OPENAI_API_KEY}" > python/ag_ui_langgraph/.env echo "LANGSMITH_API_KEY=${LANGSMITH_API_KEY}" >> python/ag_ui_langgraph/.env + + - name: setup workload identity federation + uses: 'google-github-actions/auth@v3' + with: + service_account: 'vertex-express@contextable-ci-builds-474115.iam.gserviceaccount.com' + workload_identity_provider: 'projects/27343391129/locations/global/workloadIdentityPools/github/providers/my-repo' + - name: Run dojo+agents uses: JarvusInnovations/background-action@v1 From e037ec2886d612af2772ff10516601c33dda87f3 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 4 Oct 2025 11:21:00 -0700 Subject: [PATCH 05/10] Update dojo-e2e.yml Using secrets --- .github/workflows/dojo-e2e.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dojo-e2e.yml b/.github/workflows/dojo-e2e.yml index b6ee133c4..365bc7398 100644 --- a/.github/workflows/dojo-e2e.yml +++ b/.github/workflows/dojo-e2e.yml @@ -157,8 +157,8 @@ jobs: - name: setup workload identity federation uses: 'google-github-actions/auth@v3' with: - service_account: 'vertex-express@contextable-ci-builds-474115.iam.gserviceaccount.com' - workload_identity_provider: 'projects/27343391129/locations/global/workloadIdentityPools/github/providers/my-repo' + service_account: '${{ secrets.GOOGLE_SERVICE_ACCOUNT }}' + workload_identity_provider: '${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}' - name: Run dojo+agents From b4c6dc55a613fbef1df76d5bf264cb38c703f190 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 4 Oct 2025 11:28:04 -0700 Subject: [PATCH 06/10] Update dojo-e2e.yml Adding necessary permissions. --- .github/workflows/dojo-e2e.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/dojo-e2e.yml b/.github/workflows/dojo-e2e.yml index 365bc7398..7cfa30c9e 100644 --- a/.github/workflows/dojo-e2e.yml +++ b/.github/workflows/dojo-e2e.yml @@ -10,6 +10,9 @@ jobs: e2e: name: ${{ matrix.suite }} runs-on: depot-ubuntu-24.04 + permissions: + contents: 'read' + id-token: 'write' strategy: fail-fast: false matrix: From 4acdcea1d799856891f0a888536167fec4fe1a82 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 4 Oct 2025 12:18:57 -0700 Subject: [PATCH 07/10] Update dojo-e2e.yml Changing how environment variables get populated --- .github/workflows/dojo-e2e.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/dojo-e2e.yml b/.github/workflows/dojo-e2e.yml index 7cfa30c9e..e74338f28 100644 --- a/.github/workflows/dojo-e2e.yml +++ b/.github/workflows/dojo-e2e.yml @@ -157,22 +157,16 @@ jobs: echo "OPENAI_API_KEY=${OPENAI_API_KEY}" > python/ag_ui_langgraph/.env echo "LANGSMITH_API_KEY=${LANGSMITH_API_KEY}" >> python/ag_ui_langgraph/.env - - name: setup workload identity federation - uses: 'google-github-actions/auth@v3' - with: - service_account: '${{ secrets.GOOGLE_SERVICE_ACCOUNT }}' - workload_identity_provider: '${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}' - - - name: Run dojo+agents uses: JarvusInnovations/background-action@v1 - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }} - GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} + # The top-level 'env:' block has been removed. if: ${{ join(matrix.services, ',') != '' && contains(join(matrix.services, ','), 'dojo') }} with: run: | + # Inject the secrets directly into the command line environment. + OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} \ + LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }} \ + GOOGLE_API_KEY=${{ secrets.GOOGLE_API_KEY }} \ node ../scripts/run-dojo-everything.js --only ${{ join(matrix.services, ',') }} working-directory: typescript-sdk/apps/dojo/e2e wait-on: ${{ matrix.wait_on }} From a1529fc50004769715ac9bd94cc1f3047c35e426 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 4 Oct 2025 12:27:34 -0700 Subject: [PATCH 08/10] Update dojo-e2e.yml Trying to create a .env file for adk-middleware --- .github/workflows/dojo-e2e.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/dojo-e2e.yml b/.github/workflows/dojo-e2e.yml index e74338f28..282e7f4ba 100644 --- a/.github/workflows/dojo-e2e.yml +++ b/.github/workflows/dojo-e2e.yml @@ -157,16 +157,23 @@ jobs: echo "OPENAI_API_KEY=${OPENAI_API_KEY}" > python/ag_ui_langgraph/.env echo "LANGSMITH_API_KEY=${LANGSMITH_API_KEY}" >> python/ag_ui_langgraph/.env + - name: write adk-middleware env file + working-directory: typescript-sdk/integrations/adk-middleware + env: + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} + if: ${{ contains(join(matrix.services, ','), 'adk-middleware') }} + run: | + echo "GOOGLE_API_KEY=${GOOGLE_API_KEY}" > examples/.env + - name: Run dojo+agents uses: JarvusInnovations/background-action@v1 - # The top-level 'env:' block has been removed. + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }} + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} if: ${{ join(matrix.services, ',') != '' && contains(join(matrix.services, ','), 'dojo') }} with: run: | - # Inject the secrets directly into the command line environment. - OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} \ - LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }} \ - GOOGLE_API_KEY=${{ secrets.GOOGLE_API_KEY }} \ node ../scripts/run-dojo-everything.js --only ${{ join(matrix.services, ',') }} working-directory: typescript-sdk/apps/dojo/e2e wait-on: ${{ matrix.wait_on }} From 986b454cef8323112ea7af885ce02c9cd9067dd1 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 4 Oct 2025 12:36:27 -0700 Subject: [PATCH 09/10] Update dojo-e2e.yml Fixing directory --- .github/workflows/dojo-e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dojo-e2e.yml b/.github/workflows/dojo-e2e.yml index 282e7f4ba..2ae244b13 100644 --- a/.github/workflows/dojo-e2e.yml +++ b/.github/workflows/dojo-e2e.yml @@ -163,7 +163,7 @@ jobs: GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} if: ${{ contains(join(matrix.services, ','), 'adk-middleware') }} run: | - echo "GOOGLE_API_KEY=${GOOGLE_API_KEY}" > examples/.env + echo "GOOGLE_API_KEY=${GOOGLE_API_KEY}" > python/examples/.env - name: Run dojo+agents uses: JarvusInnovations/background-action@v1 From 0c509ba0aa13af65a16e88ce8bcb408b68401570 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 4 Oct 2025 13:44:33 -0700 Subject: [PATCH 10/10] Update dojo-e2e.yml Putting dojo-e2e.yml back to where it was. --- .github/workflows/dojo-e2e.yml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.github/workflows/dojo-e2e.yml b/.github/workflows/dojo-e2e.yml index 2ae244b13..b2458e0f4 100644 --- a/.github/workflows/dojo-e2e.yml +++ b/.github/workflows/dojo-e2e.yml @@ -10,9 +10,6 @@ jobs: e2e: name: ${{ matrix.suite }} runs-on: depot-ubuntu-24.04 - permissions: - contents: 'read' - id-token: 'write' strategy: fail-fast: false matrix: @@ -156,15 +153,7 @@ jobs: echo "LANGSMITH_API_KEY=${LANGSMITH_API_KEY}" >> examples/typescript/.env echo "OPENAI_API_KEY=${OPENAI_API_KEY}" > python/ag_ui_langgraph/.env echo "LANGSMITH_API_KEY=${LANGSMITH_API_KEY}" >> python/ag_ui_langgraph/.env - - - name: write adk-middleware env file - working-directory: typescript-sdk/integrations/adk-middleware - env: - GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - if: ${{ contains(join(matrix.services, ','), 'adk-middleware') }} - run: | - echo "GOOGLE_API_KEY=${GOOGLE_API_KEY}" > python/examples/.env - + - name: Run dojo+agents uses: JarvusInnovations/background-action@v1 env: