Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion dspy/adapters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,11 @@ def format_conversation_history(
else None
)

user_content = self.format_user_message_content(signature, message)
# Preserve the format reminder from the original request for prompt-cache reuse.
# Output-only events must not acquire a synthetic user message containing just the reminder.
user_content = self.format_user_message_content(
signature, message, main_request=any(name in message for name in signature.input_fields)
)
if user_content:
messages.append({"role": "user", "content": user_content})

Expand Down
50 changes: 43 additions & 7 deletions tests/adapters/test_chat_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,9 +370,13 @@ class HistorySignature(dspy.Signature):
"[[ ## completed ## ]]\n"
"In adhering to this structure, your objective is: \n"
" Given the fields `history`, `question`, produce the fields `answer`."},
{"role": "user", "content": "[[ ## question ## ]]\nWhat is 1+1?"},
{"role": "user", "content": "[[ ## question ## ]]\nWhat is 1+1?\n\n"
"Respond with the corresponding output fields, starting with the field `[[ ## answer ## ]]`, "
"and then ending with the marker for `[[ ## completed ## ]]`."},
{"role": "assistant", "content": "[[ ## answer ## ]]\n2\n\n[[ ## completed ## ]]\n"},
{"role": "user", "content": "[[ ## question ## ]]\nWhat is 2+2?"},
{"role": "user", "content": "[[ ## question ## ]]\nWhat is 2+2?\n\n"
"Respond with the corresponding output fields, starting with the field `[[ ## answer ## ]]`, "
"and then ending with the marker for `[[ ## completed ## ]]`."},
{"role": "assistant", "content": "[[ ## answer ## ]]\n4\n\n[[ ## completed ## ]]\n"},
{"role": "user",
"content": "[[ ## question ## ]]\n"
Expand Down Expand Up @@ -669,7 +673,10 @@ class RichRenderingSignature(dspy.Signature):
'["math", "machines"]}\n'
'\n'
'[[ ## question ## ]]\n'
'Who is Ada?'},
'Who is Ada?\n\n'
'Respond with the corresponding output fields, starting with the field `[[ ## answer ## ]]` '
'(must be formatted as a valid Python AnswerCard), and then ending with the marker '
'for `[[ ## completed ## ]]`.'},
{"role": "assistant",
"content": '[[ ## answer ## ]]\n'
'{"answer": "Ada is a mathematician.", "sources": ["memory"]}\n'
Expand Down Expand Up @@ -1589,7 +1596,10 @@ class ToolHistorySignature(dspy.Signature):
" Given the fields `question`, `history`, `tools`, produce the fields `next_thought`, "
"`tool_calls`.",
},
{"role": "user", "content": "[[ ## question ## ]]\nQ1"},
{"role": "user", "content": "[[ ## question ## ]]\nQ1\n\n"
"Respond with the corresponding output fields, starting with the field `[[ ## next_thought ## ]]`, then "
'`[[ ## tool_calls ## ]]` (must be a JSON object like {"tool_calls": [{"name": "...", "args": {...}}]}), '
"and then ending with the marker for `[[ ## completed ## ]]`."},
{
"role": "assistant",
"content": "[[ ## next_thought ## ]]\n"
Expand Down Expand Up @@ -1994,7 +2004,12 @@ class KitchenSinkSignature(dspy.Signature):
'[2] «older note»\n'
'\n'
'[[ ## question ## ]]\n'
'Who is Ada?'},
'Who is Ada?\n\n'
'Respond with the corresponding output fields, starting with the field `[[ ## answer ## ]]` '
'(must be formatted as a valid Python AnswerCard), then `[[ ## verdict ## ]]` '
"(must be formatted as a valid Python Literal['yes', 'no']), then `[[ ## confidence ## ]]` "
'(must be formatted as a valid Python float), and then ending with the marker '
'for `[[ ## completed ## ]]`.'},
{"role": "assistant",
"content": '[[ ## answer ## ]]\n'
'{"answer": "Ada is a mathematician.", "sources": ["memory"]}\n'
Expand Down Expand Up @@ -2380,6 +2395,23 @@ class CitationGeneration(dspy.Signature):
assert "Type description of Citations" in system_content


@pytest.mark.parametrize("adapter", [dspy.ChatAdapter(), dspy.JSONAdapter(), dspy.XMLAdapter()])
def test_history_replay_preserves_previous_request_prefix(adapter):
class Conversation(dspy.Signature):
question: str = dspy.InputField()
history: dspy.History = dspy.InputField()
answer: str = dspy.OutputField()

history = dspy.History(messages=[])
previous = None
for question, answer in [("What is 3+4?", "7"), ("Double that?", "14"), ("Subtract five?", "9")]:
messages = adapter.format(Conversation, [], {"question": question, "history": history})
if previous is not None:
assert messages[:len(previous)] == previous
history.messages.append({"question": question, "answer": answer})
previous = messages


def test_chat_adapter_formats_conversation_history():
class MySignature(dspy.Signature):
question: str = dspy.InputField()
Expand All @@ -2397,9 +2429,13 @@ class MySignature(dspy.Signature):
messages = adapter.format(MySignature, [], {"question": "What is the capital of France?", "history": history})

assert len(messages) == 6
assert messages[1]["content"] == "[[ ## question ## ]]\nWhat is the capital of France?"
reminder = (
"\n\nRespond with the corresponding output fields, starting with the field `[[ ## answer ## ]]`, "
"and then ending with the marker for `[[ ## completed ## ]]`."
)
assert messages[1]["content"] == "[[ ## question ## ]]\nWhat is the capital of France?" + reminder
assert messages[2]["content"] == "[[ ## answer ## ]]\nParis\n\n[[ ## completed ## ]]\n"
assert messages[3]["content"] == "[[ ## question ## ]]\nWhat is the capital of Germany?"
assert messages[3]["content"] == "[[ ## question ## ]]\nWhat is the capital of Germany?" + reminder
assert messages[4]["content"] == "[[ ## answer ## ]]\nBerlin\n\n[[ ## completed ## ]]\n"


Expand Down
9 changes: 6 additions & 3 deletions tests/adapters/test_json_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,9 @@ class RichRenderingSignature(dspy.Signature):
'["math", "machines"]}\n'
'\n'
'[[ ## question ## ]]\n'
'Who is Ada?'},
'Who is Ada?\n\n'
'Respond with a JSON object in the following order of fields: `answer` '
'(must be formatted as a valid Python AnswerCard).'},
{"role": "assistant",
"content": '{\n'
' "answer": {\n'
Expand Down Expand Up @@ -1226,9 +1228,10 @@ class MySignature(dspy.Signature):
messages = adapter.format(MySignature, [], {"question": "What is the capital of France?", "history": history})

assert len(messages) == 6
assert messages[1]["content"] == "[[ ## question ## ]]\nWhat is the capital of France?"
reminder = "\n\nRespond with a JSON object in the following order of fields: `answer`."
assert messages[1]["content"] == "[[ ## question ## ]]\nWhat is the capital of France?" + reminder
assert messages[2]["content"] == '{\n "answer": "Paris"\n}'
assert messages[3]["content"] == "[[ ## question ## ]]\nWhat is the capital of Germany?"
assert messages[3]["content"] == "[[ ## question ## ]]\nWhat is the capital of Germany?" + reminder
assert messages[4]["content"] == '{\n "answer": "Berlin"\n}'


Expand Down
5 changes: 4 additions & 1 deletion tests/adapters/test_xml_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,10 @@ class RichRenderingSignature(dspy.Signature):
'\n'
'<question>\n'
'Who is Ada?\n'
'</question>'},
'</question>\n\n'
'Respond with the corresponding output fields wrapped in XML tags `<answer>`. '
'Use this nested XML structure: '
'<answer><answer>...</answer><sources><item>...</item></sources></answer>'},
{"role": "assistant",
"content": "<answer><answer>Ada is a mathematician.</answer><sources><item>memory</item></sources></answer>"},
{"role": "user",
Expand Down
Loading