From 8df46e91963741e4f8f7e6b0acda3f01b177e3ad Mon Sep 17 00:00:00 2001 From: enitrat Date: Sun, 5 Oct 2025 16:33:40 +0200 Subject: [PATCH] fix: ChatAdapter same-line field parsing --- dspy/adapters/chat_adapter.py | 28 +++++++++++----------------- tests/adapters/test_chat_adapter.py | 27 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/dspy/adapters/chat_adapter.py b/dspy/adapters/chat_adapter.py index c856c2d51a..b296b66a8b 100644 --- a/dspy/adapters/chat_adapter.py +++ b/dspy/adapters/chat_adapter.py @@ -167,31 +167,25 @@ def format_assistant_message_content( return assistant_message_content def parse(self, signature: type[Signature], completion: str) -> dict[str, Any]: - sections = [(None, [])] - - for line in completion.splitlines(): - match = field_header_pattern.match(line.strip()) - if match: - # If the header pattern is found, split the rest of the line as content - header = match.group(1) - remaining_content = line[match.end() :].strip() - sections.append((header, [remaining_content] if remaining_content else [])) - else: - sections[-1][1].append(line) - - sections = [(k, "\n".join(v).strip()) for k, v in sections] + # Split the entire completion by field markers, keeping the headers + parts = re.split(field_header_pattern, completion) fields = {} - for k, v in sections: - if (k not in fields) and (k in signature.output_fields): + # Iterate over captured headers and their bodies + for i in range(1, len(parts), 2): + header = parts[i] + body = parts[i + 1] if i + 1 < len(parts) else "" + if (header not in fields) and (header in signature.output_fields): try: - fields[k] = parse_value(v, signature.output_fields[k].annotation) + fields[header] = parse_value(body.strip(), signature.output_fields[header].annotation) except Exception as e: raise AdapterParseError( adapter_name="ChatAdapter", signature=signature, lm_response=completion, - message=f"Failed to parse field {k} with value {v} from the LM response. Error message: {e}", + message=( + f"Failed to parse field {header} with value {body.strip()} from the LM response. Error message: {e}" + ), ) if fields.keys() != signature.output_fields.keys(): raise AdapterParseError( diff --git a/tests/adapters/test_chat_adapter.py b/tests/adapters/test_chat_adapter.py index 0ea385630c..4b0efe2927 100644 --- a/tests/adapters/test_chat_adapter.py +++ b/tests/adapters/test_chat_adapter.py @@ -591,3 +591,30 @@ def get_weather(city: str) -> str: assert result[0]["tool_calls"] == dspy.ToolCalls( tool_calls=[dspy.ToolCalls.ToolCall(name="get_weather", args={"city": "Paris"})] ) + + +def test_chat_adapter_same_line_markers(): + MOCK_ANSWER = """ +[[ ## reasoning ## ]] +The user asked a question about the weather. The topic is the current weather. +[[ ## topic ## ]] +Current Weather[[ ## answer ## ]] +The current weather is sunny.[[ ## completed ## ]] +""" + + class MySignature(dspy.Signature): + question: str = dspy.InputField() + reasoning: str = dspy.OutputField() + topic: str = dspy.OutputField() + answer: str = dspy.OutputField() + + adapter = dspy.ChatAdapter() + with mock.patch("litellm.completion") as mock_completion: + mock_completion.return_value = ModelResponse( + choices=[Choices(message=Message(content=MOCK_ANSWER))], + model="openai/gpt-4o-mini", + ) + result = adapter(dspy.LM(model="openai/gpt-4o-mini", cache=False), {}, MySignature, [], {"question": "What is the weather?"}) + assert result[0]["reasoning"] == "The user asked a question about the weather. The topic is the current weather." + assert result[0]["topic"] == "Current Weather" + assert result[0]["answer"] == "The current weather is sunny."