mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
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:
committed by
Copybara-Service
parent
b57fe5f459
commit
31cfa3b82b
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user