feat: Capture thinking output, forward raw payloads, and fix exec locals

LlmResponse/Event now keep both provider reasoning output and the raw vendor payload so callbacks and loggers can inspect hidden “thoughts” or trace bugs without rewriting adapters.

LiteLLM’s adapter and streaming loop emit reasoning chunks alongside text and aggregate them into final events -> all responses now carry a JSON-safe copy of the source payload for debug. UnsafeLocalCodeExecutor uses the documented exec(code, globals, globals) form, letting helper functions defined inside snippets call each other.

Close #1749

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 834956847
This commit is contained in:
George Weale
2025-11-20 16:30:19 -08:00
committed by Copybara-Service
parent b57fe5f459
commit 31cfa3b82b
4 changed files with 175 additions and 65 deletions
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import textwrap
from unittest.mock import MagicMock
from google.adk.agents.base_agent import BaseAgent
@@ -101,3 +102,22 @@ class TestUnsafeLocalCodeExecutor:
result = executor.execute_code(mock_invocation_context, code_input)
assert result.stdout == ""
assert result.stderr == ""
def test_execute_code_nested_function_call(
self, mock_invocation_context: InvocationContext
):
executor = UnsafeLocalCodeExecutor()
code_input = CodeExecutionInput(code=(textwrap.dedent("""
def helper(name):
return f'hi {name}'
def run():
print(helper('ada'))
run()
""")))
result = executor.execute_code(mock_invocation_context, code_input)
assert result.stderr == ""
assert result.stdout == "hi ada\n"
+37 -49
View File
@@ -17,7 +17,6 @@ from unittest.mock import AsyncMock
from unittest.mock import Mock
import warnings
from google.adk.models.lite_llm import _build_function_declaration_log
from google.adk.models.lite_llm import _content_to_message_param
from google.adk.models.lite_llm import _FINISH_REASON_MAPPING
from google.adk.models.lite_llm import _function_declaration_to_tool_param
@@ -25,6 +24,7 @@ from google.adk.models.lite_llm import _get_completion_inputs
from google.adk.models.lite_llm import _get_content
from google.adk.models.lite_llm import _message_to_generate_content_response
from google.adk.models.lite_llm import _model_response_to_chunk
from google.adk.models.lite_llm import _model_response_to_generate_content_response
from google.adk.models.lite_llm import _parse_tool_calls_from_text
from google.adk.models.lite_llm import _split_message_content_and_tool_calls
from google.adk.models.lite_llm import _to_litellm_response_format
@@ -630,54 +630,6 @@ class MockLLMClient(LiteLLMClient):
)
def test_build_function_declaration_log():
"""Test that _build_function_declaration_log formats function declarations correctly."""
# Test case 1: Function with parameters and response
func_decl1 = types.FunctionDeclaration(
name="test_func1",
description="Test function 1",
parameters=types.Schema(
type=types.Type.OBJECT,
properties={
"param1": types.Schema(
type=types.Type.STRING, description="param1 desc"
)
},
),
response=types.Schema(type=types.Type.BOOLEAN, description="return bool"),
)
log1 = _build_function_declaration_log(func_decl1)
assert log1 == (
"test_func1: {'param1': {'description': 'param1 desc', 'type':"
" <Type.STRING: 'STRING'>}} -> {'description': 'return bool', 'type':"
" <Type.BOOLEAN: 'BOOLEAN'>}"
)
# Test case 2: Function with JSON schema parameters and response
func_decl2 = types.FunctionDeclaration(
name="test_func2",
description="Test function 2",
parameters_json_schema={
"type": "object",
"properties": {"param2": {"type": "integer"}},
},
response_json_schema={"type": "string"},
)
log2 = _build_function_declaration_log(func_decl2)
assert log2 == (
"test_func2: {'type': 'object', 'properties': {'param2': {'type':"
" 'integer'}}} -> {'type': 'string'}"
)
# Test case 3: Function with no parameters and no response
func_decl3 = types.FunctionDeclaration(
name="test_func3",
description="Test function 3",
)
log3 = _build_function_declaration_log(func_decl3)
assert log3 == "test_func3: {} -> None"
@pytest.mark.asyncio
async def test_generate_content_async(mock_acompletion, lite_llm_instance):
@@ -1535,6 +1487,42 @@ def test_message_to_generate_content_response_with_model():
assert response.model_version == "gemini-2.5-pro"
def test_message_to_generate_content_response_reasoning_content():
message = {
"role": "assistant",
"content": "Visible text",
"reasoning_content": "Hidden chain",
}
response = _message_to_generate_content_response(message)
assert len(response.content.parts) == 2
thought_part = response.content.parts[0]
text_part = response.content.parts[1]
assert thought_part.text == "Hidden chain"
assert thought_part.thought is True
assert text_part.text == "Visible text"
def test_model_response_to_generate_content_response_reasoning_content():
model_response = ModelResponse(
model="thinking-model",
choices=[{
"message": {
"role": "assistant",
"content": "Answer",
"reasoning_content": "Step-by-step",
},
"finish_reason": "stop",
}],
)
response = _model_response_to_generate_content_response(model_response)
assert response.content.parts[0].text == "Step-by-step"
assert response.content.parts[0].thought is True
assert response.content.parts[1].text == "Answer"
def test_parse_tool_calls_from_text_multiple_calls():
text = (
'{"name":"alpha","arguments":{"value":1}}\n'