dspy 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sorbet-runtime'
4
+ require_relative 'few_shot_example'
5
+
6
+ module DSPy
7
+ class Prompt
8
+ extend T::Sig
9
+
10
+ sig { returns(String) }
11
+ attr_reader :instruction
12
+
13
+ sig { returns(T::Array[FewShotExample]) }
14
+ attr_reader :few_shot_examples
15
+
16
+ sig { returns(T::Hash[Symbol, T.untyped]) }
17
+ attr_reader :input_schema
18
+
19
+ sig { returns(T::Hash[Symbol, T.untyped]) }
20
+ attr_reader :output_schema
21
+
22
+ sig { returns(T.nilable(String)) }
23
+ attr_reader :signature_class_name
24
+
25
+ sig do
26
+ params(
27
+ instruction: String,
28
+ input_schema: T::Hash[Symbol, T.untyped],
29
+ output_schema: T::Hash[Symbol, T.untyped],
30
+ few_shot_examples: T::Array[FewShotExample],
31
+ signature_class_name: T.nilable(String)
32
+ ).void
33
+ end
34
+ def initialize(instruction:, input_schema:, output_schema:, few_shot_examples: [], signature_class_name: nil)
35
+ @instruction = instruction
36
+ @few_shot_examples = few_shot_examples.freeze
37
+ @input_schema = input_schema.freeze
38
+ @output_schema = output_schema.freeze
39
+ @signature_class_name = signature_class_name
40
+ end
41
+
42
+ # Immutable update methods for optimization
43
+ sig { params(new_instruction: String).returns(Prompt) }
44
+ def with_instruction(new_instruction)
45
+ self.class.new(
46
+ instruction: new_instruction,
47
+ input_schema: @input_schema,
48
+ output_schema: @output_schema,
49
+ few_shot_examples: @few_shot_examples,
50
+ signature_class_name: @signature_class_name
51
+ )
52
+ end
53
+
54
+ sig { params(new_examples: T::Array[FewShotExample]).returns(Prompt) }
55
+ def with_examples(new_examples)
56
+ self.class.new(
57
+ instruction: @instruction,
58
+ input_schema: @input_schema,
59
+ output_schema: @output_schema,
60
+ few_shot_examples: new_examples,
61
+ signature_class_name: @signature_class_name
62
+ )
63
+ end
64
+
65
+ sig { params(new_examples: T::Array[FewShotExample]).returns(Prompt) }
66
+ def add_examples(new_examples)
67
+ combined_examples = @few_shot_examples + new_examples
68
+ with_examples(combined_examples)
69
+ end
70
+
71
+ # Core prompt rendering methods
72
+ sig { returns(String) }
73
+ def render_system_prompt
74
+ sections = []
75
+
76
+ sections << "Your input schema fields are:"
77
+ sections << "```json"
78
+ sections << JSON.pretty_generate(@input_schema)
79
+ sections << "```"
80
+
81
+ sections << "Your output schema fields are:"
82
+ sections << "```json"
83
+ sections << JSON.pretty_generate(@output_schema)
84
+ sections << "```"
85
+
86
+ sections << ""
87
+ sections << "All interactions will be structured in the following way, with the appropriate values filled in."
88
+
89
+ # Add few-shot examples if present
90
+ if @few_shot_examples.any?
91
+ sections << ""
92
+ sections << "Here are some examples:"
93
+ sections << ""
94
+ @few_shot_examples.each_with_index do |example, index|
95
+ sections << "### Example #{index + 1}"
96
+ sections << example.to_prompt_section
97
+ sections << ""
98
+ end
99
+ end
100
+
101
+ sections << "## Input values"
102
+ sections << "```json"
103
+ sections << "{input_values}"
104
+ sections << "```"
105
+
106
+ sections << "## Output values"
107
+ sections << "Respond exclusively with the output schema fields in the json block below."
108
+ sections << "```json"
109
+ sections << "{output_values}"
110
+ sections << "```"
111
+
112
+ sections << ""
113
+ sections << "In adhering to this structure, your objective is: #{@instruction}"
114
+
115
+ sections.join("\n")
116
+ end
117
+
118
+ sig { params(input_values: T::Hash[Symbol, T.untyped]).returns(String) }
119
+ def render_user_prompt(input_values)
120
+ sections = []
121
+
122
+ sections << "## Input Values"
123
+ sections << "```json"
124
+ sections << JSON.pretty_generate(input_values)
125
+ sections << "```"
126
+
127
+ sections << ""
128
+ sections << "Respond with the corresponding output schema fields wrapped in a ```json ``` block,"
129
+ sections << "starting with the heading `## Output values`."
130
+
131
+ sections.join("\n")
132
+ end
133
+
134
+ # Generate messages for LM adapter
135
+ sig { params(input_values: T::Hash[Symbol, T.untyped]).returns(T::Array[T::Hash[Symbol, String]]) }
136
+ def to_messages(input_values)
137
+ [
138
+ { role: 'system', content: render_system_prompt },
139
+ { role: 'user', content: render_user_prompt(input_values) }
140
+ ]
141
+ end
142
+
143
+ # Serialization for persistence and optimization
144
+ sig { returns(T::Hash[Symbol, T.untyped]) }
145
+ def to_h
146
+ {
147
+ instruction: @instruction,
148
+ few_shot_examples: @few_shot_examples.map(&:to_h),
149
+ input_schema: @input_schema,
150
+ output_schema: @output_schema,
151
+ signature_class_name: @signature_class_name
152
+ }
153
+ end
154
+
155
+ sig { params(hash: T::Hash[Symbol, T.untyped]).returns(Prompt) }
156
+ def self.from_h(hash)
157
+ examples = (hash[:few_shot_examples] || []).map { |ex| FewShotExample.from_h(ex) }
158
+
159
+ new(
160
+ instruction: hash[:instruction] || "",
161
+ input_schema: hash[:input_schema] || {},
162
+ output_schema: hash[:output_schema] || {},
163
+ few_shot_examples: examples,
164
+ signature_class_name: hash[:signature_class_name]
165
+ )
166
+ end
167
+
168
+ # Create prompt from signature class
169
+ sig { params(signature_class: T.class_of(Signature)).returns(Prompt) }
170
+ def self.from_signature(signature_class)
171
+ new(
172
+ instruction: signature_class.description || "Complete this task.",
173
+ input_schema: signature_class.input_json_schema,
174
+ output_schema: signature_class.output_json_schema,
175
+ few_shot_examples: [],
176
+ signature_class_name: signature_class.name
177
+ )
178
+ end
179
+
180
+ # Comparison and diff methods for optimization
181
+ sig { params(other: T.untyped).returns(T::Boolean) }
182
+ def ==(other)
183
+ return false unless other.is_a?(Prompt)
184
+
185
+ @instruction == other.instruction &&
186
+ @few_shot_examples == other.few_shot_examples &&
187
+ @input_schema == other.input_schema &&
188
+ @output_schema == other.output_schema
189
+ end
190
+
191
+ sig { params(other: Prompt).returns(T::Hash[Symbol, T.untyped]) }
192
+ def diff(other)
193
+ changes = {}
194
+
195
+ changes[:instruction] = {
196
+ from: @instruction,
197
+ to: other.instruction
198
+ } if @instruction != other.instruction
199
+
200
+ changes[:few_shot_examples] = {
201
+ from: @few_shot_examples.length,
202
+ to: other.few_shot_examples.length,
203
+ added: other.few_shot_examples - @few_shot_examples,
204
+ removed: @few_shot_examples - other.few_shot_examples
205
+ } if @few_shot_examples != other.few_shot_examples
206
+
207
+ changes
208
+ end
209
+
210
+ # Statistics for optimization tracking
211
+ sig { returns(T::Hash[Symbol, T.untyped]) }
212
+ def stats
213
+ {
214
+ character_count: @instruction.length,
215
+ example_count: @few_shot_examples.length,
216
+ total_example_chars: @few_shot_examples.sum { |ex| ex.to_prompt_section.length },
217
+ input_fields: @input_schema.dig(:properties)&.keys&.length || 0,
218
+ output_fields: @output_schema.dig(:properties)&.keys&.length || 0
219
+ }
220
+ end
221
+ end
222
+ end