Skip to content

Commit 37a8a61

Browse files
authored
Merge branch 'main' into feature/audio_utils
2 parents 44d2343 + 635cf19 commit 37a8a61

28 files changed

+450
-193
lines changed

docs/docs/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ hide:
1010

1111
# _Programming_—not prompting—_LMs_
1212

13+
[![PyPI Downloads](https://static.pepy.tech/badge/dspy/month)](https://pepy.tech/projects/dspy)
1314

1415
DSPy is the framework for _programming—rather than prompting—language models_. It allows you to iterate fast on **building modular AI systems** and offers algorithms for **optimizing their prompts and weights**, whether you're building simple classifiers, sophisticated RAG pipelines, or Agent loops.
1516

dspy/__init__.py

+2-9
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from dspy.evaluate import Evaluate # isort: skip
1010
from dspy.clients import * # isort: skip
11-
from dspy.adapters import Adapter, ChatAdapter, JSONAdapter, Image, Audio # isort: skip
11+
from dspy.adapters import Adapter, ChatAdapter, JSONAdapter, Image, History, Audio # isort: skip
1212
from dspy.utils.logging_utils import configure_dspy_loggers, disable_logging, enable_logging
1313
from dspy.utils.asyncify import asyncify
1414
from dspy.utils.saving import load
@@ -26,11 +26,4 @@
2626

2727
BootstrapRS = BootstrapFewShotWithRandomSearch
2828

29-
from .__metadata__ import (
30-
__name__,
31-
__version__,
32-
__description__,
33-
__url__,
34-
__author__,
35-
__author_email__
36-
)
29+
from .__metadata__ import __name__, __version__, __description__, __url__, __author__, __author_email__

dspy/__metadata__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#replace_package_name_marker
22
__name__="dspy"
33
#replace_package_version_marker
4-
__version__="2.6.5"
4+
__version__="2.6.9rc1"
55
__description__="DSPy"
66
__url__="https://github.com/stanfordnlp/dspy"
77
__author__="Omar Khattab"

dspy/adapters/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
from dspy.adapters.json_adapter import JSONAdapter
44
from dspy.adapters.image_utils import Image, encode_image, is_image
55
from dspy.adapters.audio_utils import Audio, encode_audio, is_audio
6+
from dspy.adapters.types import History
67
from dspy.adapters.media_utils import try_expand_media_tags
78

89
__all__ = [
910
'Adapter',
1011
'ChatAdapter',
1112
'JSONAdapter',
1213
'Image',
14+
'History',
1315
'Audio',
1416
'encode_image',
1517
'encode_audio',

dspy/adapters/base.py

+47-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from litellm import ContextWindowExceededError
44

5+
from dspy.adapters.types import History
56
from dspy.utils.callback import with_callbacks
67

78

@@ -32,12 +33,15 @@ def __call__(self, lm, lm_kwargs, signature, demos, inputs):
3233

3334
value = self.parse(signature, output)
3435

35-
assert set(value.keys()) == set(signature.output_fields.keys()), \
36-
f"Expected {signature.output_fields.keys()} but got {value.keys()}"
37-
36+
if set(value.keys()) != set(signature.output_fields.keys()):
37+
raise ValueError(
38+
"Parsed output fields do not match signature output fields. "
39+
f"Expected: {set(signature.output_fields.keys())}, Got: {set(value.keys())}"
40+
)
41+
3842
if output_logprobs is not None:
3943
value["logprobs"] = output_logprobs
40-
44+
4145
values.append(value)
4246

4347
return values
@@ -46,18 +50,54 @@ def __call__(self, lm, lm_kwargs, signature, demos, inputs):
4650
if isinstance(e, ContextWindowExceededError):
4751
# On context window exceeded error, we don't want to retry with a different adapter.
4852
raise e
49-
from .json_adapter import JSONAdapter
53+
from dspy.adapters.json_adapter import JSONAdapter
54+
5055
if not isinstance(self, JSONAdapter):
5156
return JSONAdapter()(lm, lm_kwargs, signature, demos, inputs)
5257
raise e
5358

5459
@abstractmethod
5560
def format(self, signature, demos, inputs):
56-
raise NotImplementedError
61+
raise NotImplementedError
5762

5863
@abstractmethod
5964
def parse(self, signature, completion):
60-
raise NotImplementedError
65+
raise NotImplementedError
6166

6267
def format_finetune_data(self, signature, demos, inputs, outputs):
6368
raise NotImplementedError
69+
70+
def format_turn(self, signature, values, role, incomplete=False, is_conversation_history=False):
71+
pass
72+
73+
def format_conversation_history(self, signature, inputs):
74+
history_field_name = None
75+
for name, field in signature.input_fields.items():
76+
if field.annotation == History:
77+
history_field_name = name
78+
break
79+
80+
if history_field_name is None:
81+
return []
82+
83+
# In order to format the conversation history, we need to remove the history field from the signature.
84+
signature_without_history = signature.delete(history_field_name)
85+
conversation_history = inputs[history_field_name].messages if history_field_name in inputs else None
86+
87+
if conversation_history is None:
88+
return []
89+
90+
messages = []
91+
for message in conversation_history:
92+
messages.append(
93+
self.format_turn(signature_without_history, message, role="user", is_conversation_history=True)
94+
)
95+
messages.append(
96+
self.format_turn(signature_without_history, message, role="assistant", is_conversation_history=True)
97+
)
98+
99+
inputs_copy = dict(inputs)
100+
del inputs_copy[history_field_name]
101+
102+
messages.append(self.format_turn(signature_without_history, inputs_copy, role="user"))
103+
return messages

dspy/adapters/chat_adapter.py

+39-15
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pydantic.fields import FieldInfo
1212

1313
from dspy.adapters.base import Adapter
14+
from dspy.adapters.types.history import History
1415
from dspy.adapters.utils import format_field_value, get_annotation_name, parse_value
1516
from dspy.signatures.field import OutputField
1617
from dspy.signatures.signature import Signature, SignatureMeta
@@ -46,15 +47,23 @@ def format(self, signature: Signature, demos: list[dict[str, Any]], inputs: dict
4647
]
4748

4849
demos = incomplete_demos + complete_demos
49-
5050
prepared_instructions = prepare_instructions(signature)
5151
messages.append({"role": "system", "content": prepared_instructions})
52+
53+
# Add the few-shot examples
5254
for demo in demos:
53-
messages.append(format_turn(signature, demo, role="user", incomplete=demo in incomplete_demos))
54-
messages.append(format_turn(signature, demo, role="assistant", incomplete=demo in incomplete_demos))
55+
messages.append(self.format_turn(signature, demo, role="user", incomplete=demo in incomplete_demos))
56+
messages.append(self.format_turn(signature, demo, role="assistant", incomplete=demo in incomplete_demos))
57+
58+
# Add the chat history after few-shot examples
59+
if any(field.annotation == History for field in signature.input_fields.values()):
60+
messages.extend(self.format_conversation_history(signature, inputs))
61+
else:
62+
messages.append(self.format_turn(signature, inputs, role="user"))
5563

5664
messages.append(format_turn(signature, inputs, role="user"))
5765
messages = try_expand_media_tags(messages)
66+
5867
return messages
5968

6069
def parse(self, signature, completion):
@@ -92,7 +101,7 @@ def format_finetune_data(self, signature, demos, inputs, outputs):
92101
# Add the assistant message
93102
role = "assistant"
94103
incomplete = False
95-
assistant_message = format_turn(signature, outputs, role, incomplete)
104+
assistant_message = self.format_turn(signature, outputs, role, incomplete)
96105
messages.append(assistant_message)
97106

98107
# Wrap the messages in a dictionary with a "messages" key
@@ -108,6 +117,9 @@ def format_fields(self, signature, values, role):
108117
}
109118
return format_fields(fields_with_values)
110119

120+
def format_turn(self, signature, values, role, incomplete=False, is_conversation_history=False):
121+
return format_turn(signature, values, role, incomplete, is_conversation_history)
122+
111123

112124
def format_fields(fields_with_values: Dict[FieldInfoWithName, Any]) -> str:
113125
"""
@@ -129,7 +141,7 @@ def format_fields(fields_with_values: Dict[FieldInfoWithName, Any]) -> str:
129141
return "\n\n".join(output).strip()
130142

131143

132-
def format_turn(signature, values, role, incomplete=False):
144+
def format_turn(signature, values, role, incomplete=False, is_conversation_history=False):
133145
"""
134146
Constructs a new message ("turn") to append to a chat thread. The message is carefully formatted
135147
so that it can instruct an LLM to generate responses conforming to the specified DSPy signature.
@@ -140,24 +152,31 @@ def format_turn(signature, values, role, incomplete=False):
140152
that should be included in the message.
141153
role: The role of the message, which can be either "user" or "assistant".
142154
incomplete: If True, indicates that output field values are present in the set of specified
143-
``values``. If False, indicates that ``values`` only contains input field values.
155+
`values`. If False, indicates that `values` only contains input field values. Only used if
156+
`is_conversation_history` is False.
157+
is_conversation_history: If True, indicates that the message is part of the chat history, otherwise
158+
it is a demo (few-shot example).
144159
145160
Returns:
146161
A chat message that can be appended to a chat thread. The message contains two string fields:
147-
``role`` ("user" or "assistant") and ``content`` (the message text).
162+
`role` ("user" or "assistant") and `content` (the message text).
148163
"""
149164
if role == "user":
150165
fields = signature.input_fields
151-
message_prefix = (
152-
"This is an example of the task, though some input or output fields are not supplied." if incomplete else ""
153-
)
166+
if incomplete and not is_conversation_history:
167+
message_prefix = "This is an example of the task, though some input or output fields are not supplied."
168+
else:
169+
message_prefix = ""
154170
else:
155-
# Add the completed field for the assistant turn
156-
fields = {**signature.output_fields, BuiltInCompletedOutputFieldInfo.name: BuiltInCompletedOutputFieldInfo.info}
157-
values = {**values, BuiltInCompletedOutputFieldInfo.name: ""}
171+
# Add the completed field or chat history for the assistant turn
172+
fields = {**signature.output_fields}
173+
values = {**values}
158174
message_prefix = ""
175+
if not is_conversation_history:
176+
fields.update({BuiltInCompletedOutputFieldInfo.name: BuiltInCompletedOutputFieldInfo.info})
177+
values.update({BuiltInCompletedOutputFieldInfo.name: ""})
159178

160-
if not incomplete and not set(values).issuperset(fields.keys()):
179+
if not incomplete and not is_conversation_history and not set(values).issuperset(fields.keys()):
161180
raise ValueError(f"Expected {fields.keys()} but got {values.keys()}")
162181

163182
messages = []
@@ -166,7 +185,12 @@ def format_turn(signature, values, role, incomplete=False):
166185

167186
field_messages = format_fields(
168187
{
169-
FieldInfoWithName(name=k, info=v): values.get(k, "Not supplied for this particular example.")
188+
FieldInfoWithName(name=k, info=v): values.get(
189+
k,
190+
"Not supplied for this conversation history message. "
191+
if is_conversation_history
192+
else "Not supplied for this particular example. ",
193+
)
170194
for k, v in fields.items()
171195
},
172196
)

dspy/adapters/json_adapter.py

+36-18
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
from dspy.adapters.base import Adapter
1616
from dspy.adapters.image_utils import Image
1717
from dspy.adapters.audio_utils import Audio
18-
from dspy.adapters.utils import parse_value, format_field_value, get_annotation_name, serialize_for_json
18+
from dspy.adapters.types.history import History
19+
from dspy.adapters.utils import format_field_value, get_annotation_name, parse_value, serialize_for_json
1920
from dspy.signatures.signature import SignatureMeta
2021
from dspy.signatures.utils import get_dspy_field_type
2122

@@ -83,10 +84,14 @@ def format(self, signature, demos, inputs):
8384
messages.append({"role": "system", "content": prepare_instructions(signature)})
8485

8586
for demo in demos:
86-
messages.append(format_turn(signature, demo, role="user", incomplete=demo in incomplete_demos))
87-
messages.append(format_turn(signature, demo, role="assistant", incomplete=demo in incomplete_demos))
87+
messages.append(self.format_turn(signature, demo, role="user", incomplete=demo in incomplete_demos))
88+
messages.append(self.format_turn(signature, demo, role="assistant", incomplete=demo in incomplete_demos))
8889

89-
messages.append(format_turn(signature, inputs, role="user"))
90+
# Add the chat history after few-shot examples
91+
if any(field.annotation == History for field in signature.input_fields.values()):
92+
messages.extend(self.format_conversation_history(signature, inputs))
93+
else:
94+
messages.append(self.format_turn(signature, inputs, role="user"))
9095

9196
return messages
9297

@@ -104,8 +109,8 @@ def parse(self, signature, completion):
104109

105110
return fields
106111

107-
def format_turn(self, signature, values, role, incomplete=False):
108-
return format_turn(signature, values, role, incomplete)
112+
def format_turn(self, signature, values, role, incomplete=False, is_conversation_history=False):
113+
return format_turn(signature, values, role, incomplete, is_conversation_history)
109114

110115
def format_fields(self, signature, values, role):
111116
fields_with_values = {
@@ -164,7 +169,13 @@ def format_fields(role: str, fields_with_values: Dict[FieldInfoWithName, Any]) -
164169
return "\n\n".join(output).strip()
165170

166171

167-
def format_turn(signature: SignatureMeta, values: Dict[str, Any], role, incomplete=False) -> Dict[str, str]:
172+
def format_turn(
173+
signature: SignatureMeta,
174+
values: Dict[str, Any],
175+
role,
176+
incomplete=False,
177+
is_conversation_history=False,
178+
) -> Dict[str, str]:
168179
"""
169180
Constructs a new message ("turn") to append to a chat thread. The message is carefully formatted
170181
so that it can instruct an LLM to generate responses conforming to the specified DSPy signature.
@@ -175,7 +186,10 @@ def format_turn(signature: SignatureMeta, values: Dict[str, Any], role, incomple
175186
that should be included in the message.
176187
role: The role of the message, which can be either "user" or "assistant".
177188
incomplete: If True, indicates that output field values are present in the set of specified
178-
``values``. If False, indicates that ``values`` only contains input field values.
189+
`values`. If False, indicates that `values` only contains input field values. Only
190+
relevant if `is_conversation_history` is False.
191+
is_conversation_history: If True, indicates that the message is part of a chat history instead of a
192+
few-shot example.
179193
Returns:
180194
A chat message that can be appended to a chat thread. The message contains two string fields:
181195
``role`` ("user" or "assistant") and ``content`` (the message text).
@@ -184,25 +198,29 @@ def format_turn(signature: SignatureMeta, values: Dict[str, Any], role, incomple
184198

185199
if role == "user":
186200
fields: Dict[str, FieldInfo] = signature.input_fields
187-
if incomplete:
201+
if incomplete and not is_conversation_history:
188202
content.append("This is an example of the task, though some input or output fields are not supplied.")
189203
else:
190204
fields: Dict[str, FieldInfo] = signature.output_fields
191205

192-
if not incomplete:
206+
if not incomplete and not is_conversation_history:
207+
# For complete few-shot examples, ensure that the values contain all the fields.
193208
field_names: KeysView = fields.keys()
194209
if not set(values).issuperset(set(field_names)):
195210
raise ValueError(f"Expected {field_names} but got {values.keys()}")
196211

197-
formatted_fields = format_fields(
198-
role=role,
199-
fields_with_values={
200-
FieldInfoWithName(name=field_name, info=field_info): values.get(
201-
field_name, "Not supplied for this particular example."
212+
fields_with_values = {}
213+
for field_name, field_info in fields.items():
214+
if is_conversation_history:
215+
fields_with_values[FieldInfoWithName(name=field_name, info=field_info)] = values.get(
216+
field_name, "Not supplied for this conversation history message. "
202217
)
203-
for field_name, field_info in fields.items()
204-
},
205-
)
218+
else:
219+
fields_with_values[FieldInfoWithName(name=field_name, info=field_info)] = values.get(
220+
field_name, "Not supplied for this particular example. "
221+
)
222+
223+
formatted_fields = format_fields(role=role, fields_with_values=fields_with_values)
206224
content.append(formatted_fields)
207225

208226
if role == "user":

dspy/adapters/types/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from dspy.adapters.types.history import History
2+
3+
__all__ = ["History"]

0 commit comments

Comments
 (0)