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.
- checksums.yaml +4 -4
- data/README.md +69 -382
- data/lib/dspy/chain_of_thought.rb +57 -0
- data/lib/dspy/evaluate.rb +554 -0
- data/lib/dspy/example.rb +203 -0
- data/lib/dspy/few_shot_example.rb +81 -0
- data/lib/dspy/instrumentation.rb +97 -8
- data/lib/dspy/lm/adapter_factory.rb +6 -8
- data/lib/dspy/lm.rb +5 -7
- data/lib/dspy/predict.rb +32 -34
- data/lib/dspy/prompt.rb +222 -0
- data/lib/dspy/propose/grounded_proposer.rb +560 -0
- data/lib/dspy/registry/registry_manager.rb +504 -0
- data/lib/dspy/registry/signature_registry.rb +725 -0
- data/lib/dspy/storage/program_storage.rb +442 -0
- data/lib/dspy/storage/storage_manager.rb +331 -0
- data/lib/dspy/subscribers/langfuse_subscriber.rb +669 -0
- data/lib/dspy/subscribers/logger_subscriber.rb +120 -0
- data/lib/dspy/subscribers/newrelic_subscriber.rb +686 -0
- data/lib/dspy/subscribers/otel_subscriber.rb +538 -0
- data/lib/dspy/teleprompt/data_handler.rb +107 -0
- data/lib/dspy/teleprompt/mipro_v2.rb +790 -0
- data/lib/dspy/teleprompt/simple_optimizer.rb +497 -0
- data/lib/dspy/teleprompt/teleprompter.rb +336 -0
- data/lib/dspy/teleprompt/utils.rb +380 -0
- data/lib/dspy/version.rb +5 -0
- data/lib/dspy.rb +16 -0
- metadata +29 -12
- data/lib/dspy/lm/adapters/ruby_llm_adapter.rb +0 -81
data/lib/dspy/prompt.rb
ADDED
@@ -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
|