Repository: stanfordnlp/dspy
Pull request #502 (2024-02-29 19:19:37)

Fixed some issues with generics like list[str]
Also created a test, and made sure tests run with python 3.9.
Slightly contaminated by reformatting.


Patch:
diff --git a/dspy/functional/functional.py b/dspy/functional/functional.py
index 6fe34233..c1dcec9a 100644
--- a/dspy/functional/functional.py
+++ b/dspy/functional/functional.py
@@ -103,9 +103,7 @@ class TypedPredictor(dspy.Module):
                 else:
                     # Anything else we wrap in a pydantic object
                     unwrap = lambda x: x
-                    if not inspect.isclass(type_) or not issubclass(
-                        type_, pydantic.BaseModel
-                    ):
+                    if not (inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel)):
                         type_ = pydantic.create_model(
                             "Output", value=(type_, ...), __base__=pydantic.BaseModel
                         )
diff --git a/dspy/signatures/signature.py b/dspy/signatures/signature.py
index c0a56070..ac9a334c 100644
--- a/dspy/signatures/signature.py
+++ b/dspy/signatures/signature.py
@@ -1,4 +1,5 @@
 from copy import deepcopy
+import typing
 import dsp
 from pydantic import BaseModel, Field, create_model
 from pydantic.fields import FieldInfo
@@ -111,7 +112,7 @@ class SignatureMeta(type(BaseModel)):
         # It's posisble to set the type as annotation=type in pydantic.Field(...)
         # But this may be annoying for users, so we allow them to pass the type
         if type_ is None:
-            type_ = field.annotation 
+            type_ = field.annotation
         if type_ is None:
             type_ = str
 
@@ -170,26 +171,34 @@ class SignatureMeta(type(BaseModel)):
             fields = cls._parse_signature(signature)
         else:
             fields = signature
-        
+
         # Validate the fields, this is important because we sometimes forget the
         # slightly unintuitive syntax with tuples of (type, Field)
         fixed_fields = {}
         for name, type_field in fields.items():
-            assert isinstance(name, str), f"Field names must be strings, not {type(name)}"
+            assert isinstance(
+                name, str
+            ), f"Field names must be strings, not {type(name)}"
             if isinstance(type_field, FieldInfo):
                 type_ = type_field.annotation
                 field = type_field
             else:
-                assert isinstance(type_field, tuple), f"Field values must be tuples, not {type(type_field)}"
+                assert isinstance(
+                    type_field, tuple
+                ), f"Field values must be tuples, not {type(type_field)}"
                 type_, field = type_field
             # It might be better to be explicit about the type, but it currently would break
             # program of thought and teleprompters, so we just silently default to string.
             if type_ is None:
                 type_ = str
-            assert isinstance(type_, type), f"Field types must be types, not {type(type_)}"
-            assert isinstance(field, FieldInfo), f"Field values must be Field instances, not {type(field)}"
+            assert isinstance(type_, type) or isinstance(
+                typing.get_origin(type_), type
+            ), f"Field types must be types, not {type(type_)}"
+            assert isinstance(
+                field, FieldInfo
+            ), f"Field values must be Field instances, not {type(field)}"
             fixed_fields[name] = (type_, field)
-        
+
         # Fixing the fields shouldn't change the order
         assert list(fixed_fields.keys()) == list(fields.keys())
 


Test patch:
diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py
index 4d77c7e2..6cb00581 100644
--- a/tests/functional/test_functional.py
+++ b/tests/functional/test_functional.py
@@ -1,10 +1,10 @@
 import datetime
-import json
 import textwrap
 import pydantic
 from pydantic import Field, BaseModel, field_validator
 from typing import Annotated
 import warnings
+from typing import List
 
 import pytest
 
@@ -30,6 +30,40 @@ def test_simple():
     assert question == expected
 
 
+def test_list_output():
+    @predictor
+    def hard_question(topic: str) -> List[str]:
+        """Think of a hard factual question about a topic."""
+
+    expected = ["What is the speed of light?", "What is the speed of sound?"]
+    lm = DummyLM(
+        ['{"value": ["What is the speed of light?", "What is the speed of sound?"]}']
+    )
+    dspy.settings.configure(lm=lm)
+
+    question = hard_question(topic="Physics")
+    lm.inspect_history(n=2)
+
+    assert question == expected
+
+
+def test_list_output():
+    @predictor
+    def hard_question(topic: str) -> List[str]:
+        """Think of a hard factual question about a topic."""
+
+    expected = ["What is the speed of light?", "What is the speed of sound?"]
+    lm = DummyLM(
+        ['{"value": ["What is the speed of light?", "What is the speed of sound?"]}']
+    )
+    dspy.settings.configure(lm=lm)
+
+    question = hard_question(topic="Physics")
+    lm.inspect_history(n=2)
+
+    assert question == expected
+
+
 def test_simple_type():
     class Question(pydantic.BaseModel):
         value: str
@@ -72,7 +106,7 @@ def test_simple_class():
     class Answer(pydantic.BaseModel):
         value: float
         certainty: float
-        comments: list[str] = pydantic.Field(
+        comments: List[str] = pydantic.Field(
             description="At least two comments about the answer"
         )
 
@@ -173,7 +207,10 @@ def test_named_params():
     named_predictors = list(qa.named_predictors())
     assert len(named_predictors) == 2
     names, _ = zip(*qa.named_predictors())
-    assert set(names) == {"hard_question.predictor.predictor", "answer.predictor.predictor"}
+    assert set(names) == {
+        "hard_question.predictor.predictor",
+        "answer.predictor.predictor",
+    }
 
 
 def test_bootstrap_effectiveness():
@@ -204,18 +241,15 @@ def test_bootstrap_effectiveness():
     # This test verifies if the bootstrapping process improves the student's predictions
     student = SimpleModule()
     teacher = SimpleModule()
-    assert student.output.predictor.signature.equals(
-        teacher.output.predictor.signature)
+    assert student.output.predictor.signature.equals(teacher.output.predictor.signature)
 
-    lm = DummyLM(["blue", "Ring-ding-ding-ding-dingeringeding!"],
-                 follow_examples=True)
+    lm = DummyLM(["blue", "Ring-ding-ding-ding-dingeringeding!"], follow_examples=True)
     dspy.settings.configure(lm=lm, trace=[])
 
     bootstrap = BootstrapFewShot(
         metric=simple_metric, max_bootstrapped_demos=1, max_labeled_demos=1
     )
-    compiled_student = bootstrap.compile(
-        student, teacher=teacher, trainset=trainset)
+    compiled_student = bootstrap.compile(student, teacher=teacher, trainset=trainset)
 
     lm.inspect_history(n=2)
 
@@ -388,7 +422,7 @@ def test_field_validator():
         ]
         * 10
     )
-    dspy.settings.configure(lm=lm) 
+    dspy.settings.configure(lm=lm)
 
     with pytest.raises(ValueError):
         get_user_details()