Repository: stanfordnlp/dspy
Pull request #1569 (2024-10-01 16:28:59)

chat_adapter: Format fields as JSON
When fields are Pydantic objects, the chat_adapter was formatting them as python code, which led to some strange behavior (BootstrapFewShot would start off with JSON and then revert to unparseable python code after it started adding examples).

I'm not sure if I put the tests in the right spot, but it seemed convenient to leverage situations that were already being created. I also did `dataclasses` since they were right there.


Patch:
diff --git a/dspy/adapters/chat_adapter.py b/dspy/adapters/chat_adapter.py
index e2c3abeb..7b33dc36 100644
--- a/dspy/adapters/chat_adapter.py
+++ b/dspy/adapters/chat_adapter.py
@@ -4,6 +4,7 @@ import json
 import textwrap
 
 from pydantic import TypeAdapter
+import pydantic
 from .base import Adapter
 from typing import get_origin, get_args
 
@@ -73,10 +74,19 @@ def format_list(items):
     return "\n".join([f"[{idx+1}] {format_blob(txt)}" for idx, txt in enumerate(items)])
 
 
+def _format_field_value(value) -> str:
+    if isinstance(value, list):
+        return format_list(value)
+    elif isinstance(value, pydantic.BaseModel):
+        return value.model_dump_json()
+    else:
+        return str(value)
+
+
 def format_fields(fields):
     output = []
     for k, v in fields.items():
-        v = v if not isinstance(v, list) else format_list(v)
+        v = _format_field_value(v)
         output.append(f"[[ ## {k} ## ]]\n{v}")
 
     return '\n\n'.join(output).strip()


Test patch:
diff --git a/tests/functional/test_signature_typed.py b/tests/functional/test_signature_typed.py
index 39eb8c2f..0554cfb3 100644
--- a/tests/functional/test_signature_typed.py
+++ b/tests/functional/test_signature_typed.py
@@ -1,4 +1,5 @@
 from typing import Any, Optional, Union
+from dspy.adapters.chat_adapter import _format_field_value
 import pytest
 
 import pydantic
@@ -114,6 +115,9 @@ def test_pydantic():
     instance = build_model_instance()
     parsed_instance = parser(instance.model_dump_json())
 
+    formatted_instance = _format_field_value(instance.model_dump_json())
+    assert formatted_instance == instance.model_dump_json(), f"{formatted_instance} != {instance.model_dump_json()}"
+
     assert parsed_instance == instance, f"{instance} != {parsed_instance}"
 
 
@@ -128,13 +132,36 @@ def test_optional_pydantic():
     parsed_instance = parser(instance.model_dump_json())
     assert parsed_instance == instance, f"{instance} != {parsed_instance}"
 
+    formatted_instance = _format_field_value(instance.model_dump_json())
+    assert formatted_instance == instance.model_dump_json(), f"{formatted_instance} != {instance.model_dump_json()}"
+
     # Check null case
     parsed_instance = parser("null")
     assert parsed_instance == None, "Optional[MyModel] should be None"
 
 
+def test_nested_pydantic():
+    class NestedModel(pydantic.BaseModel):
+        model: MyModel
+
+    class MySignature(dspy.Signature):
+        question: str = dspy.InputField()
+        answer: NestedModel = dspy.OutputField()
+
+    _, parser = get_field_and_parser(MySignature)
+
+    instance = NestedModel(model=build_model_instance())
+    parsed_instance = parser(instance.model_dump_json())
+
+    formatted_instance = _format_field_value(instance.model_dump_json())
+    assert formatted_instance == instance.model_dump_json(), f"{formatted_instance} != {instance.model_dump_json()}"
+
+    assert parsed_instance == instance, f"{instance} != {parsed_instance}"
+
+
 def test_dataclass():
-    from dataclasses import dataclass
+    from dataclasses import dataclass, asdict
+    import ujson
 
     @dataclass(frozen=True)
     class MyDataclass:
@@ -152,3 +179,6 @@ def test_dataclass():
     instance = MyDataclass("foobar", 42, 3.14, True)
     parsed_instance = parser('{"string": "foobar", "number": 42, "floating": 3.14, "boolean": true}')
     assert parsed_instance == instance, f"{instance} != {parsed_instance}"
+
+    formatted_instance = _format_field_value(ujson.dumps(asdict(instance)))
+    assert formatted_instance == ujson.dumps(asdict(instance)), f"{formatted_instance} != {ujson.dumps(asdict(instance))}"