Skip to content

Commit 0d951e0

Browse files
committed
Updates after review: imports, tests, project file
1 parent 5a91cbd commit 0d951e0

8 files changed

Lines changed: 85 additions & 112 deletions

File tree

integrations/dspy/CHANGELOG.md

Lines changed: 0 additions & 13 deletions
This file was deleted.

integrations/dspy/README.md

Lines changed: 3 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,63 +3,8 @@
33
[![PyPI - Version](https://img.shields.io/pypi/v/dspy-haystack.svg)](https://pypi.org/project/dspy-haystack)
44
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/dspy-haystack.svg)](https://pypi.org/project/dspy-haystack)
55

6-
An integration between [DSPy](https://github.com/stanfordnlp/dspy) and [Haystack](https://haystack.deepset.ai/).
6+
---
77

8-
DSPy is a framework for algorithmically optimizing prompts for Language Models by applying classical machine learning concepts (training data, evaluation metrics, optimization).
8+
## Contributing
99

10-
This integration provides:
11-
- **DSPyChatGenerator** — a Haystack ChatGenerator component that uses DSPy signatures and modules for structured generation
12-
13-
## Installation
14-
15-
```bash
16-
pip install dspy-haystack
17-
```
18-
19-
## Quick Start
20-
21-
### DSPyChatGenerator
22-
23-
A Haystack chat generator that uses DSPy signatures for structured generation with built-in reasoning patterns (Chain-of-Thought, Predict, ReAct).
24-
25-
```python
26-
from haystack import Pipeline
27-
from haystack.dataclasses import ChatMessage
28-
from haystack_integrations.components.generators.dspy import DSPyChatGenerator
29-
import dspy
30-
31-
# Define a DSPy signature
32-
class QASignature(dspy.Signature):
33-
"""Answer questions accurately and concisely."""
34-
question = dspy.InputField(desc="The user's question")
35-
answer = dspy.OutputField(desc="A clear, concise answer")
36-
37-
# Create the generator
38-
generator = DSPyChatGenerator(
39-
model="openai/gpt-5-mini",
40-
signature=QASignature,
41-
module_type="ChainOfThought"
42-
)
43-
44-
# Use in pipeline
45-
pipeline = Pipeline()
46-
pipeline.add_component("llm", generator)
47-
48-
messages = [ChatMessage.from_user("What is the capital of France?")]
49-
result = pipeline.run({"llm": {"messages": messages}})
50-
print(result["llm"]["replies"][0].text)
51-
```
52-
53-
You can also use string signatures for quick prototyping:
54-
55-
```python
56-
generator = DSPyChatGenerator(
57-
model="openai/gpt-5-mini",
58-
signature="question -> answer",
59-
module_type="Predict"
60-
)
61-
```
62-
63-
## License
64-
65-
`dspy-haystack` is distributed under the terms of the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) license.
10+
Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,13 @@
11
loaders:
2-
- ignore_when_discovered:
3-
- __init__
4-
modules:
5-
- haystack_integrations.components.generators.dspy.chat.chat_generator
6-
search_path:
7-
- ../src
8-
type: haystack_pydoc_tools.loaders.CustomPythonLoader
2+
- modules:
3+
- haystack_integrations.components.generators.dspy.chat.chat_generator
4+
search_path: [../src]
95
processors:
10-
- do_not_filter_modules: false
11-
documented_only: true
12-
expression: null
13-
skip_empty_modules: true
14-
type: filter
15-
- type: smart
16-
- type: crossref
6+
- type: filter
7+
documented_only: true
8+
skip_empty_modules: true
179
renderer:
1810
description: DSPy integration for Haystack
1911
id: integrations-dspy
20-
markdown:
21-
add_member_class_prefix: false
22-
add_method_class_prefix: true
23-
classdef_code_block: false
24-
descriptive_class_title: false
25-
descriptive_module_title: true
26-
filename: dspy.md
12+
filename: dspy.md
2713
title: DSPy
28-
type: haystack_pydoc_tools.renderers.DocusaurusRenderer

integrations/dspy/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ installer = "uv"
4545
dependencies = ["haystack-pydoc-tools", "ruff"]
4646

4747
[tool.hatch.envs.default.scripts]
48-
docs = ["pydoc-markdown pydoc/config_docusaurus.yml"]
48+
docs = ["haystack-pydoc pydoc/config_docusaurus.yml"]
4949
fmt = "ruff check --fix {args}; ruff format {args}"
5050
fmt-check = "ruff check {args} && ruff format --check {args}"
5151

@@ -146,6 +146,6 @@ exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]
146146
[tool.pytest.ini_options]
147147
markers = ["integration: integration tests"]
148148
log_cli = true
149-
addopts = ["--import-mode=importlib"]
149+
addopts = ["--strict-markers"]
150150
asyncio_mode = "auto"
151151
asyncio_default_fixture_loop_scope = "class"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from haystack_integrations.components.generators.dspy.chat.chat_generator import DSPyChatGenerator
1+
from .chat.chat_generator import DSPyChatGenerator
22

33
__all__ = ["DSPyChatGenerator"]

integrations/dspy/src/haystack_integrations/components/generators/dspy/chat/__init__.py

Lines changed: 0 additions & 3 deletions
This file was deleted.

integrations/dspy/src/haystack_integrations/components/generators/dspy/chat/chat_generator.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
VALID_MODULE_TYPES = {"Predict", "ChainOfThought", "ReAct"}
1010

1111

12-
def configure_dspy_lm(model: str, api_key: str, **kwargs: Any) -> dspy.LM:
12+
def _configure_dspy_lm(model: str, api_key: str, **kwargs: Any) -> dspy.LM:
1313
"""
1414
Create and configure a DSPy language model.
1515
@@ -23,7 +23,7 @@ def configure_dspy_lm(model: str, api_key: str, **kwargs: Any) -> dspy.LM:
2323
return lm
2424

2525

26-
def get_dspy_module_class(module_type: str):
26+
def _get_dspy_module_class(module_type: str):
2727
"""
2828
Map a module type string to the corresponding DSPy module class.
2929
@@ -111,13 +111,13 @@ def __init__(
111111
self.input_mapping = input_mapping
112112
self.streaming_callback = streaming_callback
113113

114-
self._lm = configure_dspy_lm(
114+
self._lm = _configure_dspy_lm(
115115
model=self.model,
116116
api_key=self.api_key.resolve_value(),
117117
**self.generation_kwargs,
118118
)
119119

120-
module_class = get_dspy_module_class(self.module_type)
120+
module_class = _get_dspy_module_class(self.module_type)
121121
self._module = module_class(self.signature)
122122

123123
def _build_dspy_inputs(self, prompt: str, **kwargs) -> dict[str, Any]:

integrations/dspy/tests/test_chat_generator.py

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
from haystack_integrations.components.generators.dspy.chat.chat_generator import (
1010
VALID_MODULE_TYPES,
1111
DSPyChatGenerator,
12-
configure_dspy_lm,
13-
get_dspy_module_class,
12+
_configure_dspy_lm,
13+
_get_dspy_module_class,
1414
)
1515

1616

@@ -66,21 +66,21 @@ def test_contains_expected_types(self):
6666

6767
class TestGetDspyModuleClass:
6868
def test_predict(self):
69-
assert get_dspy_module_class("Predict") is dspy.Predict
69+
assert _get_dspy_module_class("Predict") is dspy.Predict
7070

7171
def test_chain_of_thought(self):
72-
assert get_dspy_module_class("ChainOfThought") is dspy.ChainOfThought
72+
assert _get_dspy_module_class("ChainOfThought") is dspy.ChainOfThought
7373

7474
def test_react(self):
75-
assert get_dspy_module_class("ReAct") is dspy.ReAct
75+
assert _get_dspy_module_class("ReAct") is dspy.ReAct
7676

7777
def test_invalid_type_raises(self):
7878
with pytest.raises(ValueError, match="Invalid module_type 'Unknown'"):
79-
get_dspy_module_class("Unknown")
79+
_get_dspy_module_class("Unknown")
8080

8181
def test_invalid_type_lists_valid_options(self):
8282
with pytest.raises(ValueError, match="ChainOfThought"):
83-
get_dspy_module_class("BadType")
83+
_get_dspy_module_class("BadType")
8484

8585

8686
class TestConfigureDspyLm:
@@ -90,7 +90,7 @@ def test_creates_lm_and_configures(self, mock_lm_class, mock_configure):
9090
mock_lm = MagicMock()
9191
mock_lm_class.return_value = mock_lm
9292

93-
result = configure_dspy_lm(model="openai/gpt-5-mini", api_key="test-key")
93+
result = _configure_dspy_lm(model="openai/gpt-5-mini", api_key="test-key")
9494

9595
mock_lm_class.assert_called_once_with(model="openai/gpt-5-mini", api_key="test-key")
9696
mock_configure.assert_called_once_with(lm=mock_lm)
@@ -102,7 +102,7 @@ def test_passes_extra_kwargs(self, mock_lm_class, mock_configure):
102102
mock_lm = MagicMock()
103103
mock_lm_class.return_value = mock_lm
104104

105-
configure_dspy_lm(model="openai/gpt-5-mini", api_key="test-key", temperature=0.7, max_tokens=100)
105+
_configure_dspy_lm(model="openai/gpt-5-mini", api_key="test-key", temperature=0.7, max_tokens=100)
106106

107107
mock_lm_class.assert_called_once_with(
108108
model="openai/gpt-5-mini", api_key="test-key", temperature=0.7, max_tokens=100
@@ -350,3 +350,62 @@ def test_live_run(self):
350350
assert len(results["replies"]) == 1
351351
message: ChatMessage = results["replies"][0]
352352
assert "Paris" in message.text
353+
354+
@pytest.mark.skipif(
355+
not os.environ.get("OPENAI_API_KEY", None),
356+
reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.",
357+
)
358+
@pytest.mark.integration
359+
def test_live_run_with_predict_module(self):
360+
"""Test using the Predict module type with a string signature."""
361+
chat_messages = [ChatMessage.from_user("What is 2 + 2?")]
362+
component = DSPyChatGenerator(
363+
signature="question -> answer",
364+
module_type="Predict",
365+
)
366+
results = component.run(chat_messages)
367+
assert len(results["replies"]) == 1
368+
assert "4" in results["replies"][0].text
369+
370+
@pytest.mark.skipif(
371+
not os.environ.get("OPENAI_API_KEY", None),
372+
reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.",
373+
)
374+
@pytest.mark.integration
375+
def test_live_run_with_signature_class(self):
376+
"""Test using a dspy.Signature class instead of a string signature."""
377+
378+
class QASignature(dspy.Signature):
379+
"""Answer questions accurately and concisely."""
380+
381+
question = dspy.InputField(desc="The user's question")
382+
answer = dspy.OutputField(desc="A clear, concise answer")
383+
384+
chat_messages = [ChatMessage.from_user("What language is spoken in Brazil?")]
385+
component = DSPyChatGenerator(
386+
signature=QASignature,
387+
module_type="ChainOfThought",
388+
)
389+
results = component.run(chat_messages)
390+
assert len(results["replies"]) == 1
391+
assert "Portuguese" in results["replies"][0].text
392+
393+
@pytest.mark.skipif(
394+
not os.environ.get("OPENAI_API_KEY", None),
395+
reason="Export an env var called OPENAI_API_KEY containing the OpenAI API key to run this test.",
396+
)
397+
@pytest.mark.integration
398+
def test_live_run_with_multi_field_signature(self):
399+
"""Test using a multi-input signature with input_mapping."""
400+
chat_messages = [ChatMessage.from_user("What is the main topic?")]
401+
component = DSPyChatGenerator(
402+
signature="context, question -> answer",
403+
module_type="Predict",
404+
input_mapping={"context": "context", "question": "question"},
405+
)
406+
results = component.run(
407+
chat_messages,
408+
context="Python is a popular programming language created by Guido van Rossum.",
409+
)
410+
assert len(results["replies"]) == 1
411+
assert results["replies"][0].text

0 commit comments

Comments
 (0)