Skip to content

Commit c2b3007

Browse files
Add AdapterParseError to dspy (#8212)
* Add AdapterParseError to dspy * change __init__.py * fix test
1 parent 9e019c6 commit c2b3007

File tree

6 files changed

+152
-10
lines changed

6 files changed

+152
-10
lines changed

dspy/adapters/chat_adapter.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from dspy.clients.lm import LM
1717
from dspy.signatures.signature import Signature
1818
from dspy.utils.callback import BaseCallback
19+
from dspy.utils.exceptions import AdapterParseError
1920

2021
field_header_pattern = re.compile(r"\[\[ ## (\w+) ## \]\]")
2122

@@ -168,11 +169,19 @@ def parse(self, signature: Type[Signature], completion: str) -> dict[str, Any]:
168169
try:
169170
fields[k] = parse_value(v, signature.output_fields[k].annotation)
170171
except Exception as e:
171-
raise ValueError(
172-
f"Error parsing field {k}: {e}.\n\n\t\tOn attempting to parse the value\n```\n{v}\n```"
172+
raise AdapterParseError(
173+
adapter_name="ChatAdapter",
174+
signature=signature,
175+
lm_response=completion,
176+
message=f"Failed to parse field {k} with value {v} from the LM response. Error message: {e}",
173177
)
174178
if fields.keys() != signature.output_fields.keys():
175-
raise ValueError(f"Expected {signature.output_fields.keys()} but got {fields.keys()}")
179+
raise AdapterParseError(
180+
adapter_name="ChatAdapter",
181+
signature=signature,
182+
lm_response=completion,
183+
parsed_result=fields,
184+
)
176185

177186
return fields
178187

dspy/adapters/json_adapter.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
)
1919
from dspy.clients.lm import LM
2020
from dspy.signatures.signature import Signature, SignatureMeta
21+
from dspy.utils.exceptions import AdapterParseError
2122

2223
logger = logging.getLogger(__name__)
2324

@@ -66,7 +67,11 @@ def __call__(
6667
try:
6768
lm_kwargs["response_format"] = {"type": "json_object"}
6869
return super().__call__(lm, lm_kwargs, signature, demos, inputs)
70+
except AdapterParseError as e:
71+
# On AdapterParseError, we raise the original error.
72+
raise e
6973
except Exception as e:
74+
# On any other error, we raise a RuntimeError with the original error message.
7075
raise RuntimeError(
7176
"Both structured output format and JSON mode failed. Please choose a model that supports "
7277
f"`response_format` argument. Original error: {e}"
@@ -124,7 +129,12 @@ def parse(self, signature: Type[Signature], completion: str) -> dict[str, Any]:
124129
fields = json_repair.loads(completion)
125130

126131
if not isinstance(fields, dict):
127-
raise ValueError(f"Expected a JSON object but parsed a {type(fields)}")
132+
raise AdapterParseError(
133+
adapter_name="JSONAdapter",
134+
signature=signature,
135+
lm_response=completion,
136+
message="LM response cannot be serialized to a JSON object.",
137+
)
128138

129139
fields = {k: v for k, v in fields.items() if k in signature.output_fields}
130140

@@ -134,7 +144,12 @@ def parse(self, signature: Type[Signature], completion: str) -> dict[str, Any]:
134144
fields[k] = parse_value(v, signature.output_fields[k].annotation)
135145

136146
if fields.keys() != signature.output_fields.keys():
137-
raise ValueError(f"Expected {signature.output_fields.keys()} but got {fields.keys()}")
147+
raise AdapterParseError(
148+
adapter_name="JSONAdapter",
149+
signature=signature,
150+
lm_response=completion,
151+
parsed_result=fields,
152+
)
138153

139154
return fields
140155

dspy/utils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from dspy.utils.callback import BaseCallback, with_callbacks
22
from dspy.utils.dummies import DummyLM, DummyVectorizer, dummy_rm
33
from dspy.streaming.messages import StatusMessageProvider, StatusMessage
4+
from dspy.utils import exceptions
45

56
import os
67
import requests
@@ -20,6 +21,7 @@ def download(url):
2021

2122
__all__ = [
2223
"download",
24+
"exceptions",
2325
"BaseCallback",
2426
"with_callbacks",
2527
"DummyLM",

dspy/utils/exceptions.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import Optional
2+
3+
from dspy.signatures.signature import Signature
4+
5+
6+
class AdapterParseError(Exception):
7+
"""Exception raised when adapter cannot parse the LM response."""
8+
9+
def __init__(
10+
self,
11+
adapter_name: str,
12+
signature: Signature,
13+
lm_response: str,
14+
message: Optional[str] = None,
15+
parsed_result: Optional[str] = None,
16+
):
17+
self.adapter_name = adapter_name
18+
self.signature = signature
19+
self.lm_response = lm_response
20+
self.parsed_result = parsed_result
21+
22+
message = f"{message}\n\n" if message else ""
23+
message = (
24+
f"{message}"
25+
f"Adapter {adapter_name} failed to parse the LM response. \n\n"
26+
f"LM Response: {lm_response} \n\n"
27+
f"Expected to find output fields in the LM response: [{', '.join(signature.output_fields.keys())}] \n\n"
28+
)
29+
30+
if parsed_result is not None:
31+
message += f"Actual output fields parsed from the LM response: [{', '.join(parsed_result.keys())}] \n\n"
32+
33+
super().__init__(message)

tests/adapters/test_json_adapter.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pydantic
44
import pytest
55
from pydantic import create_model
6+
from litellm.utils import ModelResponse, Message, Choices
67

78
import dspy
89

@@ -195,9 +196,28 @@ class TestSignature(dspy.Signature):
195196

196197

197198
def test_json_adapter_parse_raise_error_on_mismatch_fields():
198-
signature = dspy.make_signature("input1->output1")
199+
signature = dspy.make_signature("question->answer")
199200
adapter = dspy.JSONAdapter()
200-
invalid_completion = '{"output": "Test output"}'
201-
with pytest.raises(ValueError) as error:
202-
adapter.parse(signature, invalid_completion)
203-
assert str(error.value) == "Expected dict_keys(['output1']) but got dict_keys([])"
201+
202+
with mock.patch("litellm.completion") as mock_completion:
203+
mock_completion.return_value = ModelResponse(
204+
choices=[
205+
Choices(message=Message(content="{'answer1': 'Paris'}")),
206+
],
207+
model="openai/gpt4o",
208+
)
209+
lm = dspy.LM(model="openai/gpt-4o-mini")
210+
with pytest.raises(dspy.utils.exceptions.AdapterParseError) as e:
211+
adapter(lm, {}, signature, [], {"question": "What is the capital of France?"})
212+
213+
assert e.value.adapter_name == "JSONAdapter"
214+
assert e.value.signature == signature
215+
assert e.value.lm_response == "{'answer1': 'Paris'}"
216+
assert e.value.parsed_result == {}
217+
218+
assert str(e.value) == (
219+
"Adapter JSONAdapter failed to parse the LM response. \n\n"
220+
"LM Response: {'answer1': 'Paris'} \n\n"
221+
"Expected to find output fields in the LM response: [answer] \n\n"
222+
"Actual output fields parsed from the LM response: [] \n\n"
223+
)

tests/utils/test_exceptions.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import pytest
2+
from dspy.utils.exceptions import AdapterParseError
3+
from dspy.signatures.signature import Signature
4+
import dspy
5+
6+
7+
def test_adapter_parse_error_basic():
8+
adapter_name = "ChatAdapter"
9+
signature = dspy.make_signature("question->answer1, answer2")
10+
lm_response = "[[ ## answer1 ## ]]\nanswer1"
11+
12+
error = AdapterParseError(adapter_name=adapter_name, signature=signature, lm_response=lm_response)
13+
14+
assert error.adapter_name == adapter_name
15+
assert error.signature == signature
16+
assert error.lm_response == lm_response
17+
18+
error_message = str(error)
19+
assert error_message == (
20+
"Adapter ChatAdapter failed to parse the LM response. \n\n"
21+
"LM Response: [[ ## answer1 ## ]]\nanswer1 \n\n"
22+
"Expected to find output fields in the LM response: [answer1, answer2] \n\n"
23+
)
24+
25+
26+
def test_adapter_parse_error_with_message():
27+
adapter_name = "ChatAdapter"
28+
signature = dspy.make_signature("question->answer1, answer2")
29+
lm_response = "[[ ## answer1 ## ]]\nanswer1"
30+
message = "Critical error, please fix!"
31+
32+
error = AdapterParseError(adapter_name=adapter_name, signature=signature, lm_response=lm_response, message=message)
33+
34+
assert error.adapter_name == adapter_name
35+
assert error.signature == signature
36+
assert error.lm_response == lm_response
37+
38+
error_message = str(error)
39+
assert error_message == (
40+
"Critical error, please fix!\n\n"
41+
"Adapter ChatAdapter failed to parse the LM response. \n\n"
42+
"LM Response: [[ ## answer1 ## ]]\nanswer1 \n\n"
43+
"Expected to find output fields in the LM response: [answer1, answer2] \n\n"
44+
)
45+
46+
47+
def test_adapter_parse_error_with_parsed_result():
48+
adapter_name = "ChatAdapter"
49+
signature = dspy.make_signature("question->answer1, answer2")
50+
lm_response = "[[ ## answer1 ## ]]\nanswer1"
51+
parsed_result = {"answer1": "value1"}
52+
53+
error = AdapterParseError(
54+
adapter_name=adapter_name, signature=signature, lm_response=lm_response, parsed_result=parsed_result
55+
)
56+
57+
error_message = str(error)
58+
assert error_message == (
59+
"Adapter ChatAdapter failed to parse the LM response. \n\n"
60+
"LM Response: [[ ## answer1 ## ]]\nanswer1 \n\n"
61+
"Expected to find output fields in the LM response: [answer1, answer2] \n\n"
62+
"Actual output fields parsed from the LM response: [answer1] \n\n"
63+
)

0 commit comments

Comments
 (0)