Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions python/packages/gemini/agent_framework_gemini/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ class ThinkingConfig(TypedDict, total=False):
Attributes:
include_thoughts: Whether to include thought summaries in the response. Thought summaries
are condensed representations of the model's internal reasoning and appear as response
parts where ``part.thought`` is ``True``. Note: the framework currently excludes
thought parts from ``ChatResponse.contents`` and does not surface them as output.
parts where ``part.thought`` is ``True``. When set, the framework surfaces these parts
as ``text_reasoning`` content in ``ChatResponse.contents``.
thinking_budget: Token budget for Gemini 2.5 models. Set to ``0`` to disable
thinking or ``-1`` to enable a dynamic budget.
thinking_level: Thinking level for Gemini 2.5 models and later. One of
Expand Down Expand Up @@ -1120,17 +1120,20 @@ def _process_chunk(self, chunk: types.GenerateContentResponse) -> ChatResponseUp
)

def _parse_parts(self, parts: Sequence[types.Part]) -> list[Content]:
"""Convert Gemini response parts to framework Content objects, skipping thought/reasoning parts.
"""Convert Gemini response parts to framework Content objects.

Args:
parts: Sequence of ``types.Part`` objects from a Gemini response candidate.

Returns:
A list of framework ``Content`` objects (text, function_call, or function_result).
A list of framework ``Content`` objects (text_reasoning, text, function_call, or
function_result).
"""
contents: list[Content] = []
for part in parts:
if part.thought:
if part.text:
contents.append(Content.from_text_reasoning(text=part.text, raw_representation=part))
continue
if part.text is not None:
contents.append(Content.from_text(text=part.text, raw_representation=part))
Expand Down
24 changes: 21 additions & 3 deletions python/packages/gemini/tests/test_gemini_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,8 +714,8 @@ async def test_non_function_result_content_in_tool_message_is_skipped() -> None:
# thinking parts


async def test_thinking_parts_are_silently_skipped() -> None:
"""Excludes thought-summary parts from ChatResponse.contents, returning only the final answer."""
async def test_thinking_parts_are_surfaced_as_reasoning() -> None:
"""Surfaces thought-summary parts as text_reasoning content alongside the final answer."""
client, mock = _make_gemini_client()
mock.aio.models.generate_content = AsyncMock(
return_value=_make_response([
Expand All @@ -728,10 +728,28 @@ async def test_thinking_parts_are_silently_skipped() -> None:
messages=[Message(role="user", contents=[Content.from_text("What is the answer?")])]
)

assert len(response.messages[0].contents) == 1
contents = response.messages[0].contents
assert len(contents) == 2
assert contents[0].type == "text_reasoning"
assert contents[0].text == "I should think first..."
assert contents[1].type == "text"
assert response.messages[0].text == "The answer is 42."


async def test_empty_thinking_part_produces_no_reasoning_content() -> None:
"""A thought part with no text yields no content rather than empty reasoning."""
client, _ = _make_gemini_client()

contents = client._parse_parts([
_make_part(text=None, thought=True),
_make_part(text="The answer is 42."),
])

assert len(contents) == 1
assert contents[0].type == "text"
assert contents[0].text == "The answer is 42."


def test_function_call_part_preserves_thought_signature_from_raw_part() -> None:
"""Reuses the original Gemini Part so tool loops retain thought_signature metadata."""
client, _ = _make_gemini_client()
Expand Down
Loading