From 7f5ea223b225b71675f52a45eeaa5ca215caa263 Mon Sep 17 00:00:00 2001 From: TomuHirata Date: Thu, 4 Dec 2025 16:53:55 +0900 Subject: [PATCH 1/2] Enhance StreamListener to support generic type annotations for output fields - Added import of inspect to facilitate type checking. - Updated the condition for handling custom streamable types to include a check for class type using inspect.isclass. - Introduced a new test for StreamListener to validate behavior with generic type annotations in output fields. Signed-off-by: TomuHirata --- dspy/streaming/streaming_listener.py | 8 +++++++- tests/streaming/test_streaming.py | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/dspy/streaming/streaming_listener.py b/dspy/streaming/streaming_listener.py index 5c1cc144df..4080f26cdd 100644 --- a/dspy/streaming/streaming_listener.py +++ b/dspy/streaming/streaming_listener.py @@ -1,3 +1,4 @@ +import inspect import re from collections import defaultdict from queue import Queue @@ -135,7 +136,12 @@ def receive(self, chunk: ModelResponseStream): return # Handle custom streamable types - if self._output_type and issubclass(self._output_type, Type) and self._output_type.is_streamable(): + if ( + self._output_type + and inspect.isclass(self._output_type) + and issubclass(self._output_type, Type) + and self._output_type.is_streamable() + ): if parsed_chunk := self._output_type.parse_stream_chunk(chunk): return StreamResponse( self.predict_name, diff --git a/tests/streaming/test_streaming.py b/tests/streaming/test_streaming.py index c4efaff7f4..077f56d2e0 100644 --- a/tests/streaming/test_streaming.py +++ b/tests/streaming/test_streaming.py @@ -1932,3 +1932,24 @@ async def non_reasoning_stream(*args, **kwargs): assert final_prediction.reasoning.content == "Let's think step by step about this question." # Verify Reasoning object is str-like assert str(final_prediction.reasoning) == "Let's think step by step about this question." + + +@pytest.mark.anyio +async def test_stream_listener_with_generic_type_annotation(): + class TestSignature(dspy.Signature): + input_text: str = dspy.InputField() + output_list: list[str] | int = dspy.OutputField() + + predict = dspy.Predict(TestSignature) + listener = dspy.streaming.StreamListener(signature_field_name="output_list", predict=predict) + + assert listener._output_type == list[str] | int + + mock_chunk = mock.MagicMock(spec=ModelResponseStream) + mock_chunk.choices = [mock.MagicMock(spec=StreamingChoices)] + mock_chunk.choices[0].delta = mock.MagicMock(spec=Delta) + mock_chunk.choices[0].delta.content = "test" + + with dspy.context(adapter=dspy.JSONAdapter()): + result = listener.receive(mock_chunk) + assert result is None From a87086c251329e4346ed675e0eaf52b36fbcc8eb Mon Sep 17 00:00:00 2001 From: TomuHirata Date: Thu, 4 Dec 2025 16:55:58 +0900 Subject: [PATCH 2/2] test --- tests/streaming/test_streaming.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/streaming/test_streaming.py b/tests/streaming/test_streaming.py index 077f56d2e0..aa540c677d 100644 --- a/tests/streaming/test_streaming.py +++ b/tests/streaming/test_streaming.py @@ -1945,10 +1945,10 @@ class TestSignature(dspy.Signature): assert listener._output_type == list[str] | int - mock_chunk = mock.MagicMock(spec=ModelResponseStream) - mock_chunk.choices = [mock.MagicMock(spec=StreamingChoices)] - mock_chunk.choices[0].delta = mock.MagicMock(spec=Delta) - mock_chunk.choices[0].delta.content = "test" + mock_chunk = ModelResponseStream( + model="gpt-4o-mini", + choices=[StreamingChoices(delta=Delta(content="test"))], + ) with dspy.context(adapter=dspy.JSONAdapter()): result = listener.receive(mock_chunk)