Skip to content

Commit 2151e3a

Browse files
isaacbmillerampagentIsaac Miller
authored
fix(adapters): serialize non-JSON values one at a time in serialize_for_json (#10634)
One value pydantic can't serialize (e.g. a numpy scalar from a pandas row) made serialize_for_json fall back to str() of the whole value. With InstructionProposer as GEPA's default, this turned every reflection example into a Python repr, dropped dspy.Image parts, and ignored custom __str__. Pass fallback=str so only the offending value is stringified. Amp-Thread-ID: https://ampcode.com/threads/T-01a12205-bc16-7750-9c23-8d86826ea0ec Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: Isaac Miller <isaac@cmpnd.ai>
1 parent 81a41fd commit 2151e3a

2 files changed

Lines changed: 20 additions & 5 deletions

File tree

‎dspy/adapters/utils.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ def serialize_for_json(value: Any) -> Any:
7171
Returns:
7272
The formatted value, which is serializable as a JSON string.
7373
"""
74-
# Attempt to format the value as a JSON-compatible object using pydantic, falling back to
75-
# a string representation of the value if that fails (e.g. if the value contains an object
76-
# that pydantic doesn't recognize or can't serialize)
74+
# Format the value as a JSON-compatible object using pydantic. A nested value pydantic can't
75+
# serialize (e.g. a numpy scalar) becomes its string form without affecting its siblings; the
76+
# whole value falls back to its string form only if pydantic can't handle it at all.
7777
try:
78-
return TypeAdapter(type(value)).dump_python(value, mode="json")
78+
return TypeAdapter(type(value)).dump_python(value, mode="json", fallback=str)
7979
except Exception:
8080
return str(value)
8181

‎tests/teleprompt/test_gepa_builtin_instruction_proposer.py‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pathlib import Path
66
from typing import Any
77

8+
import numpy as np
89
import pytest
910
from pydantic import ValidationError
1011

@@ -643,12 +644,23 @@ def format(self, signature, demos, inputs):
643644
seen.append(inputs["examples_with_feedback"])
644645
return super().format(signature, demos, inputs)
645646

647+
class Claim:
648+
def __str__(self):
649+
return "claim #7"
650+
646651
history = tool_history("short result")
647652
image = dspy.Image(url="https://example.com/image.png")
648653
repl = REPLHistory().append(code="print(1)", output="1")
649654
examples = [
650655
{
651-
"Inputs": {"Context": history, "nested": [{"image": image}], "repl": repl},
656+
"Inputs": {
657+
"Context": history,
658+
"nested": [{"image": image}],
659+
"repl": repl,
660+
# Values pydantic can't serialize, e.g. a numpy scalar read from a pandas row.
661+
"amount": np.int64(1200),
662+
"claim": Claim(),
663+
},
652664
"Generated Outputs": {"answer": "cat"},
653665
"Feedback": "ok",
654666
}
@@ -660,6 +672,9 @@ def format(self, signature, demos, inputs):
660672
assert seen[0][0]["Inputs"]["repl"] is repl
661673
content = lm.history[-1]["messages"][-1]["content"]
662674
assert any(part.get("type") == "image_url" for part in content)
675+
text = "".join(part.get("text", "") for part in content)
676+
assert '"amount": "1200", "claim": "claim #7"' in text
677+
assert '"Feedback": "ok"' in text
663678

664679

665680
@pytest.mark.parametrize("builtin", [True, False])

0 commit comments

Comments
 (0)