Repository: stanfordnlp/dspy
Pull request #848 (2024-04-17 03:34:00)

fix(dspy) fix issue 804
Implements the suggested fix [here](https://github.com/stanfordnlp/dspy/issues/804#issuecomment-2054110487)


Patch:
diff --git a/dspy/predict/react.py b/dspy/predict/react.py
index a465ee71..cedd49c7 100644
--- a/dspy/predict/react.py
+++ b/dspy/predict/react.py
@@ -91,12 +91,9 @@ class ReAct(Module):
             if action_name == "Finish":
                 return action_val
 
-            try:
-                output[f"Observation_{hop+1}"] = self.tools[action_name](action_val).passages
-            except AttributeError:
-                # Handle the case where 'passages' attribute is missing
-                # TODO: This is a hacky way to handle this. Need to fix this.
-                output[f"Observation_{hop+1}"] = self.tools[action_name](action_val)
+            result = self.tools[action_name](action_val)
+            # Handle the case where 'passages' attribute is missing
+            output[f"Observation_{hop+1}"] = getattr(result, "passages", result)
 
         except Exception as e:
             output[f"Observation_{hop+1}"] = (


Test patch:
diff --git a/tests/predict/test_react.py b/tests/predict/test_react.py
index f28e905e..79f8e0bd 100644
--- a/tests/predict/test_react.py
+++ b/tests/predict/test_react.py
@@ -1,3 +1,5 @@
+from dataclasses import dataclass
+
 import dspy
 from dspy.utils.dummies import dummy_rm
 
@@ -84,3 +86,71 @@ def test_example_search():
         "Thought 2: More thoughts\n\n"
         "Action 2: Finish[blue]"
     )
+
+
+class DummyTool1:
+    name = "Tool1"
+    input_variable = "query"
+    desc = ""
+    num_calls = 0
+
+    def __call__(self, *args, **kwargs):
+        # test case with no passages attribute
+        assert args[0] == "foo"
+        self.num_calls += 1
+        return "tool 1 output"
+
+
+@dataclass
+class DummyOutput:
+    passages: str
+
+
+class DummyTool2:
+    name = "Tool2"
+    input_variable = "query"
+    desc = ""
+    num_calls = 0
+
+    def __call__(self, *args, **kwargs):
+        # test case with passages attribute
+        assert args[0] == "bar"
+        self.num_calls += 1
+        return DummyOutput(passages="tool 2 output")
+
+
+def test_custom_tools():
+    lm = dspy.utils.DummyLM(
+        [
+            "Initial thoughts",
+            "Tool1[foo]",
+            "More thoughts",
+            "Tool2[bar]",
+            "Even more thoughts",
+            "Finish[baz]",
+        ]
+    )
+    dspy.settings.configure(lm=lm)
+
+    tool1 = DummyTool1()
+    tool2 = DummyTool2()
+    program = dspy.ReAct("question -> answer", tools=[tool1, tool2])
+
+    question = "What is the color of the sky?"
+    result = program(question=question)
+    assert result.answer == "baz"
+
+    # each tool should be called only once
+    assert tool1.num_calls == 1
+    assert tool2.num_calls == 1
+    assert lm.get_convo(-1).endswith(
+        "Question: What is the color of the sky?\n\n"
+        "Thought 1: Initial thoughts\n\n"
+        "Action 1: Tool1[foo]\n\n"
+        "Observation 1: tool 1 output\n\n"
+        "Thought 2: More thoughts\n\n"
+        "Action 2: Tool2[bar]\n\n"
+        "Observation 2: tool 2 output\n\n"
+        "Thought 3: Even more thoughts\n\n"
+        "Action 3: Finish[baz]"
+    )