44
55
66class History (pydantic .BaseModel ):
7- """Class representing the conversation history.
7+ """Class representing conversation history.
88
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" .
9+ History supports four message formats, with one mode per History instance :
10+
11+ 1. **Raw mode**: Direct LM messages with `{"role": "...", "content": "..."}` .
12+ Used for ReAct trajectories and native tool calling .
1313 ```python
14- history = dspy.History(messages=[
15- {"question": "What is 2+2?", "answer": "4"},
16- ], mode="signature")
14+ history = dspy.History.from_raw([
15+ {"role": "user", "content": "Hello"},
16+ {"role": "assistant", "content": "Hi there!"},
17+ ])
1718 ```
18-
19- 2. **KV mode**: Nested `{"input_fields": {...}, "output_fields": {...}}` → user/assistant pairs.
19+
20+ 2. **Demo mode**: Nested `{"input_fields": {...}, "output_fields": {...}}` pairs.
21+ Used for few-shot demonstrations with explicit input/output separation.
2022 ```python
21- history = dspy.History.from_kv ([
22- {"input_fields": {"thought ": "...", "tool_name": "search" }, "output_fields": {"observation ": "... "}},
23+ history = dspy.History.from_demo ([
24+ {"input_fields": {"question ": "2+2?" }, "output_fields": {"answer ": "4 "}},
2325 ])
2426 ```
25-
26- 3. **Dict mode** (default): Arbitrary serializable key-value pairs → all in single user message.
27+
28+ 3. **Flat mode** (default): Arbitrary key-value pairs in a single user message.
2729 ```python
2830 history = dspy.History(messages=[
29- {"thought": "I need to search", "tool_name": "search", "observation": "Results found "},
31+ {"thought": "I need to search", "tool_name": "search", "observation": "Found it "},
3032 ])
3133 ```
32-
33- 4. **Raw mode**: Direct LM messages with `{"role": "user", "content": "..."}` → passed through.
34+
35+ 4. **Signature mode**: Dict keys match signature fields → user/assistant pairs.
36+ Must be explicitly set.
3437 ```python
35- history = dspy.History.from_raw([
36- {"role": "user", "content": "Hello"},
37- {"role": "assistant", "content": "Hi there!"},
38+ history = dspy.History.from_signature([
39+ {"question": "What is 2+2?", "answer": "4"},
3840 ])
3941 ```
4042
41- The mode is auto-detected from the first message if not explicitly provided.
42-
4343 Example:
4444 ```python
4545 import dspy
@@ -51,12 +51,9 @@ class MySignature(dspy.Signature):
5151 history: dspy.History = dspy.InputField()
5252 answer: str = dspy.OutputField()
5353
54- history = dspy.History(
55- messages=[
56- {"question": "What is the capital of France?", "answer": "Paris"},
57- {"question": "What is the capital of Germany?", "answer": "Berlin"},
58- ]
59- )
54+ history = dspy.History.from_signature([
55+ {"question": "What is the capital of France?", "answer": "Paris"},
56+ ])
6057
6158 predict = dspy.Predict(MySignature)
6259 outputs = predict(question="What is the capital of France?", history=history)
@@ -81,7 +78,7 @@ class MySignature(dspy.Signature):
8178 """
8279
8380 messages : list [dict [str , Any ]]
84- mode : Literal ["signature" , "kv " , "dict " , "raw" ] | None = None
81+ mode : Literal ["signature" , "demo " , "flat " , "raw" ] = "flat"
8582
8683 model_config = pydantic .ConfigDict (
8784 frozen = True ,
@@ -90,85 +87,85 @@ class MySignature(dspy.Signature):
9087 extra = "forbid" ,
9188 )
9289
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
90+ @staticmethod
91+ def _infer_mode_from_msg (msg : dict ) -> str :
92+ """Infer the mode from a message's structure.
10493
94+ Detection rules (conservative):
95+ - Raw: has "role" key and ONLY LM-like keys (role, content, tool_calls, tool_call_id, name)
96+ - Demo: keys are ONLY "input_fields" and/or "output_fields"
97+ - Flat: everything else (signature mode must be explicit)
98+ """
10599 keys = set (msg .keys ())
100+ lm_keys = {"role" , "content" , "tool_calls" , "tool_call_id" , "name" }
106101
107- if { "role" , "content" } <= keys and not ({ "input_fields" , "output_fields" } & keys ) :
102+ if "role" in keys and keys <= lm_keys :
108103 return "raw"
109104
110105 if keys <= {"input_fields" , "output_fields" } and keys :
111- return "kv"
106+ return "demo"
107+
108+ return "flat"
109+
110+ def _validate_msg_for_mode (self , msg : dict , mode : str ) -> None :
111+ """Validate a message conforms to the expected mode structure."""
112+ if mode == "raw" :
113+ if not isinstance (msg .get ("role" ), str ):
114+ raise ValueError (f"Raw mode: 'role' must be a string: { msg } " )
115+ content = msg .get ("content" )
116+ if content is not None and not isinstance (content , str ):
117+ raise ValueError (f"Raw mode: 'content' must be a string or None: { msg } " )
112118
113- return "dict"
119+ elif mode == "demo" :
120+ if "input_fields" in msg and not isinstance (msg ["input_fields" ], dict ):
121+ raise ValueError (f"Demo mode: 'input_fields' must be a dict: { msg } " )
122+ if "output_fields" in msg and not isinstance (msg ["output_fields" ], dict ):
123+ raise ValueError (f"Demo mode: 'output_fields' must be a dict: { msg } " )
124+
125+ elif mode == "signature" :
126+ if not isinstance (msg , dict ) or not msg :
127+ raise ValueError (f"Signature mode: messages must be non-empty dicts: { msg } " )
114128
115129 @pydantic .model_validator (mode = "after" )
116130 def _validate_messages (self ) -> "History" :
131+ if not self .messages :
132+ return self
133+
134+ # Only infer if mode is the default "flat" and messages clearly match another mode
135+ if self .mode == "flat" :
136+ inferred = self ._infer_mode_from_msg (self .messages [0 ])
137+ if inferred in {"raw" , "demo" }:
138+ object .__setattr__ (self , "mode" , inferred )
139+
117140 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 } " )
141+ self ._validate_msg_for_mode (msg , self .mode )
133142
134143 return self
135144
136145 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- """
146+ """Return a new History with additional messages appended."""
145147 return History (messages = [* self .messages , * messages ], mode = self .mode )
146148
147149 @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".
150+ def from_demo (cls , messages : list [dict [str , Any ]]) -> "History" :
151+ """Create a History with demo mode.
152+
153+ Demo mode expects messages with "input_fields" and/or "output_fields" keys.
159154 """
160- return cls (messages = messages , mode = "kv " )
155+ return cls (messages = messages , mode = "demo " )
161156
162157 @classmethod
163158 def from_raw (cls , messages : list [dict [str , Any ]]) -> "History" :
164- """Create a History instance with raw mode.
165-
159+ """Create a History with raw mode.
160+
166161 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".
173162 """
174163 return cls (messages = messages , mode = "raw" )
164+
165+ @classmethod
166+ def from_signature (cls , messages : list [dict [str , Any ]]) -> "History" :
167+ """Create a History with signature mode.
168+
169+ Signature mode expects dicts with keys matching the signature's fields.
170+ """
171+ return cls (messages = messages , mode = "signature" )
0 commit comments