diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index a4e3a57330..21a7ce45d7 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -2180,7 +2180,16 @@ def value(self) -> ResponseModelT | None: and isinstance(self._response_format, type) and issubclass(self._response_format, BaseModel) ): - self._value = cast(ResponseModelT, self._response_format.model_validate_json(self.text)) + # Concatenate text contents without spaces to preserve structured + # output (JSON). Message.text uses " ".join which is correct for + # natural language but corrupts JSON keys/values. + raw_text = "".join( + c.text + for msg in self.messages + for c in msg.contents + if c.type == "text" and c.text is not None + ) + self._value = cast(ResponseModelT, self._response_format.model_validate_json(raw_text)) self._value_parsed = True return self._value @@ -2442,7 +2451,16 @@ def value(self) -> ResponseModelT | None: and isinstance(self._response_format, type) and issubclass(self._response_format, BaseModel) ): - self._value = cast(ResponseModelT, self._response_format.model_validate_json(self.text)) + # Concatenate text contents without spaces to preserve structured + # output (JSON). Message.text uses " ".join which is correct for + # natural language but corrupts JSON keys/values. + raw_text = "".join( + c.text + for msg in self.messages + for c in msg.contents + if c.type == "text" and c.text is not None + ) + self._value = cast(ResponseModelT, self._response_format.model_validate_json(raw_text)) self._value_parsed = True return self._value diff --git a/python/packages/core/tests/core/test_types.py b/python/packages/core/tests/core/test_types.py index 1cde898787..58c2ac26bf 100644 --- a/python/packages/core/tests/core/test_types.py +++ b/python/packages/core/tests/core/test_types.py @@ -834,6 +834,51 @@ class StrictSchema(BaseModel): assert "score" in error_fields, "Expected 'score' gt constraint error" +def test_chat_response_value_multi_chunk_json(): + """Test that ChatResponse.value correctly parses JSON split across multiple text Content objects. + + When streaming responses arrive as multiple text chunks, Message.text joins + them with spaces which corrupts JSON. The value property must concatenate + without spaces so model_validate_json succeeds. + """ + # Simulate a JSON response split across multiple text content chunks + # (as would happen with streaming) + chunks = [ + Content.from_text(text='{"resp'), + Content.from_text(text='onse": "He'), + Content.from_text(text='llo"}'), + ] + message = Message(role="assistant", contents=chunks) + response = ChatResponse(messages=message, response_format=OutputModel) + + # Message.text joins with spaces, which would break JSON parsing + assert " " in message.text # confirms the space-join behavior + + # But value should still parse correctly + assert response.value is not None + assert response.value.response == "Hello" + + +def test_agent_response_value_multi_chunk_json(): + """Test that AgentResponse.value correctly parses JSON split across multiple text Content objects. + + Same as the ChatResponse test but for AgentResponse, which has its own + value property implementation. + """ + chunks = [ + Content.from_text(text='{"resp'), + Content.from_text(text='onse": "He'), + Content.from_text(text='llo"}'), + ] + message = Message(role="assistant", contents=chunks) + response = AgentResponse(messages=message, response_format=OutputModel) + + assert " " in message.text # confirms the space-join behavior + + assert response.value is not None + assert response.value.response == "Hello" + + # region ChatResponseUpdate