Skip to content

Commit 9c8c26f

Browse files
ampagentIsaac Miller
andcommitted
fix(dspy): keep trailing [ and drop unfinished headers followed by whitespace
Address review: a lone trailing "[" is part of the value, and an unfinished field header followed by whitespace at the end of the stream is still cut. Amp-Thread-ID: https://ampcode.com/threads/T-01a12205-cfb6-739c-a9fb-cbe0a0a86db3 Co-authored-by: Isaac Miller <isaac@cmpnd.ai>
1 parent ae66979 commit 9c8c26f

2 files changed

Lines changed: 28 additions & 2 deletions

File tree

‎dspy/streaming/streaming_listener.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323

2424
ADAPTER_SUPPORT_STREAMING = [ChatAdapter, XMLAdapter, JSONAdapter]
2525

26-
# Matches an unfinished "[[ ## <field> ## ]]" at the end of the text, from "[" up to "[[ ## <field> ## ]".
27-
_CHAT_END_IDENTIFIER_PREFIX = re.compile(r"\[(?:\[(?: (?:#(?:#(?: (?:\w+(?: (?:#(?:#(?: \]?)?)?)?)?)?)?)?)?)?)?\Z")
26+
# Matches an unfinished "[[ ## <field> ## ]]" at the end of the text, from "[[" up to "[[ ## <field> ## ]", plus any
27+
# trailing whitespace. A lone "[" is kept because it is more likely part of the value.
28+
_CHAT_END_IDENTIFIER_PREFIX = re.compile(r"\[\[(?: (?:#(?:#(?: (?:\w+(?: (?:#(?:#(?: \]?)?)?)?)?)?)?)?)?)?\s*\Z")
2829

2930

3031
class StreamListener:

‎tests/streaming/test_streaming.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,8 @@ async def gpt_4o_mini_stream(*args, **kwargs):
368368
["[[ ## answer ## ]]\n", "See also ", "[[Matrices]]\n\n[[ ## comp", "leted ## ]]"],
369369
# The end marker is split across chunks.
370370
["[[ ## answer ## ]]\n", "m = [[1, 2]]", "\n\n[", "[ #", "# comp", "leted #", "# ]", "]"],
371+
# A trailing "[" with no header after it is part of the value.
372+
["[[ ## answer ## ]]\n", "Use ["],
371373
],
372374
)
373375
async def test_streaming_chunks_join_to_parsed_value_regardless_of_chunking(parts):
@@ -393,6 +395,29 @@ async def stream(*args, **kwargs):
393395
assert "".join([chunk.chunk for chunk in all_chunks]) == final.answer
394396

395397

398+
@pytest.mark.anyio
399+
@pytest.mark.parametrize("last_part", ["\n\n[[ ## comp", "\n\n[[ ## comp\n", "\n\n[[\n"])
400+
async def test_streaming_drops_unfinished_field_header_at_end_of_stream(last_part):
401+
program = dspy.streamify(
402+
dspy.Predict("question->answer"),
403+
stream_listeners=[dspy.streaming.StreamListener(signature_field_name="answer")],
404+
)
405+
406+
async def stream(*args, **kwargs):
407+
for part in ["[[ ## answer ## ]]\n", "abc", last_part]:
408+
yield ModelResponseStream(model="gpt-4o-mini", choices=[StreamingChoices(delta=Delta(content=part))])
409+
410+
with mock.patch("litellm.acompletion", side_effect=stream):
411+
with dspy.context(lm=dspy.LM("openai/gpt-4o-mini", engine="litellm", cache=False), adapter=dspy.ChatAdapter()):
412+
chunks = [
413+
value.chunk
414+
async for value in program(question="What is the capital of France?")
415+
if isinstance(value, dspy.streaming.StreamResponse)
416+
]
417+
418+
assert "".join(chunks) == "abc"
419+
420+
396421
@pytest.mark.llm_call
397422
def test_sync_streaming(lm_for_test):
398423
class MyProgram(dspy.Module):

0 commit comments

Comments
 (0)