1- from typing import Any
1+ from typing import Any , Literal
22
33import pydantic
44
55
66class 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