Skip to content

Commit 0294b13

Browse files
committed
Initial refactor. To Clean
1 parent ed01c88 commit 0294b13

File tree

9 files changed

+799
-197
lines changed

9 files changed

+799
-197
lines changed

dspy/adapters/base.py

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from dspy.adapters.types.reasoning import Reasoning
1010
from dspy.adapters.types.tool import Tool, ToolCalls
1111
from dspy.experimental import Citations
12+
from dspy.signatures.field import InputField, OutputField
1213
from dspy.signatures.signature import Signature
1314
from dspy.utils.callback import BaseCallback, with_callbacks
1415

@@ -474,6 +475,23 @@ def _get_tool_call_output_field_name(self, signature: type[Signature]) -> bool:
474475
return name
475476
return None
476477

478+
def _serialize_kv_value(self, v: Any) -> Any:
479+
"""Safely serialize values for kv-mode formatting."""
480+
if isinstance(v, (str, int, float, bool)) or v is None:
481+
return v
482+
try:
483+
return str(v)
484+
except Exception:
485+
return f"<unserializable {type(v).__name__}>"
486+
487+
def _make_dynamic_signature_for_inputs(self, keys: list[str]) -> type[Signature]:
488+
"""Create a dynamic signature with input fields only (no instructions)."""
489+
return Signature({k: InputField() for k in keys}, instructions="")
490+
491+
def _make_dynamic_signature_for_outputs(self, keys: list[str]) -> type[Signature]:
492+
"""Create a dynamic signature with output fields only (no instructions)."""
493+
return Signature({k: OutputField() for k in keys}, instructions="")
494+
477495
def format_conversation_history(
478496
self,
479497
signature: type[Signature],
@@ -483,6 +501,11 @@ def format_conversation_history(
483501
"""Format the conversation history.
484502
485503
This method formats the conversation history and the current input as multiturn messages.
504+
Supports four modes:
505+
- signature: Dict keys match signature input/output fields → user/assistant pairs
506+
- kv: Nested {"input_fields": {...}, "output_fields": {...}} → user/assistant pairs
507+
- dict: Arbitrary serializable kv pairs → all in single user message (default)
508+
- raw: Direct LM messages with {"role": "user", "content": "..."} → passed through
486509
487510
Args:
488511
signature: The DSPy signature for which to format the conversation history.
@@ -492,25 +515,50 @@ def format_conversation_history(
492515
Returns:
493516
A list of multiturn messages.
494517
"""
495-
conversation_history = inputs[history_field_name].messages if history_field_name in inputs else None
496-
497-
if conversation_history is None:
518+
history = inputs.get(history_field_name)
519+
if history is None:
498520
return []
499521

500522
messages = []
501-
for message in conversation_history:
502-
messages.append(
503-
{
523+
for msg in history.messages:
524+
mode = history._detect_mode(msg)
525+
526+
if mode == "raw":
527+
messages.append(dict(msg))
528+
529+
elif mode == "kv":
530+
if "input_fields" in msg:
531+
input_dict = {k: self._serialize_kv_value(v) for k, v in msg["input_fields"].items()}
532+
sig = self._make_dynamic_signature_for_inputs(list(input_dict.keys()))
533+
messages.append({
534+
"role": "user",
535+
"content": self.format_user_message_content(sig, input_dict),
536+
})
537+
if "output_fields" in msg:
538+
output_dict = {k: self._serialize_kv_value(v) for k, v in msg["output_fields"].items()}
539+
sig = self._make_dynamic_signature_for_outputs(list(output_dict.keys()))
540+
messages.append({
541+
"role": "assistant",
542+
"content": self.format_assistant_message_content(sig, output_dict),
543+
})
544+
545+
elif mode == "signature":
546+
messages.append({
504547
"role": "user",
505-
"content": self.format_user_message_content(signature, message),
506-
}
507-
)
508-
messages.append(
509-
{
548+
"content": self.format_user_message_content(signature, msg),
549+
})
550+
messages.append({
510551
"role": "assistant",
511-
"content": self.format_assistant_message_content(signature, message),
512-
}
513-
)
552+
"content": self.format_assistant_message_content(signature, msg),
553+
})
554+
555+
else: # dict mode (default) - all kv pairs go into single user message
556+
serialized = {k: self._serialize_kv_value(v) for k, v in msg.items()}
557+
sig = self._make_dynamic_signature_for_inputs(list(serialized.keys()))
558+
messages.append({
559+
"role": "user",
560+
"content": self.format_user_message_content(sig, serialized),
561+
})
514562

515563
# Remove the history field from the inputs
516564
del inputs[history_field_name]

dspy/adapters/types/history.py

Lines changed: 120 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,47 @@
1-
from typing import Any
1+
from typing import Any, Literal
22

33
import pydantic
44

55

66
class History(pydantic.BaseModel):
77
"""Class representing the conversation history.
88
9-
The conversation history is a list of messages, each message entity should have keys from the associated signature.
10-
For example, if you have the following signature:
11-
12-
```
13-
class MySignature(dspy.Signature):
14-
question: str = dspy.InputField()
15-
history: dspy.History = dspy.InputField()
16-
answer: str = dspy.OutputField()
17-
```
18-
19-
Then the history should be a list of dictionaries with keys "question" and "answer".
9+
History supports four message formats:
10+
11+
1. **Signature mode**: Dict keys match signature input/output fields → user/assistant pairs.
12+
Must be explicitly set via mode="signature".
13+
```python
14+
history = dspy.History(messages=[
15+
{"question": "What is 2+2?", "answer": "4"},
16+
], mode="signature")
17+
```
18+
19+
2. **KV mode**: Nested `{"input_fields": {...}, "output_fields": {...}}` → user/assistant pairs.
20+
```python
21+
history = dspy.History.from_kv([
22+
{"input_fields": {"thought": "...", "tool_name": "search"}, "output_fields": {"observation": "..."}},
23+
])
24+
```
25+
26+
3. **Dict mode** (default): Arbitrary serializable key-value pairs → all in single user message.
27+
```python
28+
history = dspy.History(messages=[
29+
{"thought": "I need to search", "tool_name": "search", "observation": "Results found"},
30+
])
31+
```
32+
33+
4. **Raw mode**: Direct LM messages with `{"role": "user", "content": "..."}` → passed through.
34+
```python
35+
history = dspy.History.from_raw([
36+
{"role": "user", "content": "Hello"},
37+
{"role": "assistant", "content": "Hi there!"},
38+
])
39+
```
40+
41+
The mode is auto-detected from the first message if not explicitly provided.
2042
2143
Example:
22-
```
44+
```python
2345
import dspy
2446
2547
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"))
@@ -41,7 +63,7 @@ class MySignature(dspy.Signature):
4163
```
4264
4365
Example of capturing the conversation history:
44-
```
66+
```python
4567
import dspy
4668
4769
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"))
@@ -59,10 +81,94 @@ class MySignature(dspy.Signature):
5981
"""
6082

6183
messages: list[dict[str, Any]]
84+
mode: Literal["signature", "kv", "dict", "raw"] | None = None
6285

6386
model_config = pydantic.ConfigDict(
6487
frozen=True,
6588
str_strip_whitespace=True,
6689
validate_assignment=True,
6790
extra="forbid",
6891
)
92+
93+
def _detect_mode(self, msg: dict) -> str:
94+
"""Detect the mode for a message based on its structure.
95+
96+
Detection rules:
97+
- Raw: has "role" and "content" keys, but NOT "input_fields"/"output_fields"
98+
- KV: keys are ONLY "input_fields" and/or "output_fields"
99+
- Signature: must be explicitly set (requires matching against signature fields)
100+
- Dict: everything else (default) - arbitrary kv pairs go into user message
101+
"""
102+
if self.mode:
103+
return self.mode
104+
105+
keys = set(msg.keys())
106+
107+
if {"role", "content"} <= keys and not ({"input_fields", "output_fields"} & keys):
108+
return "raw"
109+
110+
if keys <= {"input_fields", "output_fields"} and keys:
111+
return "kv"
112+
113+
return "dict"
114+
115+
@pydantic.model_validator(mode="after")
116+
def _validate_messages(self) -> "History":
117+
for msg in self.messages:
118+
detected = self._detect_mode(msg)
119+
120+
if detected == "raw":
121+
if not isinstance(msg.get("role"), str):
122+
raise ValueError(f"'role' must be a string: {msg}")
123+
# content can be None for tool call messages, or string otherwise
124+
content = msg.get("content")
125+
if content is not None and not isinstance(content, str):
126+
raise ValueError(f"'content' must be a string or None: {msg}")
127+
128+
elif detected == "kv":
129+
if "input_fields" in msg and not isinstance(msg["input_fields"], dict):
130+
raise ValueError(f"'input_fields' must be a dict: {msg}")
131+
if "output_fields" in msg and not isinstance(msg["output_fields"], dict):
132+
raise ValueError(f"'output_fields' must be a dict: {msg}")
133+
134+
return self
135+
136+
def with_messages(self, messages: list[dict[str, Any]]) -> "History":
137+
"""Return a new History with additional messages appended.
138+
139+
Args:
140+
messages: List of messages to append.
141+
142+
Returns:
143+
A new History instance with the messages appended.
144+
"""
145+
return History(messages=[*self.messages, *messages], mode=self.mode)
146+
147+
@classmethod
148+
def from_kv(cls, messages: list[dict[str, Any]]) -> "History":
149+
"""Create a History instance with KV mode.
150+
151+
KV mode expects messages with "input_fields" and/or "output_fields" keys,
152+
each containing a dict of field names to values.
153+
154+
Args:
155+
messages: List of dicts with "input_fields" and/or "output_fields" keys.
156+
157+
Returns:
158+
A History instance with mode="kv".
159+
"""
160+
return cls(messages=messages, mode="kv")
161+
162+
@classmethod
163+
def from_raw(cls, messages: list[dict[str, Any]]) -> "History":
164+
"""Create a History instance with raw mode.
165+
166+
Raw mode expects direct LM messages with "role" and "content" keys.
167+
168+
Args:
169+
messages: List of dicts with "role" and "content" keys.
170+
171+
Returns:
172+
A History instance with mode="raw".
173+
"""
174+
return cls(messages=messages, mode="raw")

0 commit comments

Comments
 (0)