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/example.rb
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
require_relative 'signature'
|
5
|
+
|
6
|
+
module DSPy
|
7
|
+
# Represents a typed training/evaluation example with Signature validation
|
8
|
+
# Provides early validation and type safety for evaluation workflows
|
9
|
+
class Example
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { returns(T.class_of(Signature)) }
|
13
|
+
attr_reader :signature_class
|
14
|
+
|
15
|
+
sig { returns(T::Struct) }
|
16
|
+
attr_reader :input
|
17
|
+
|
18
|
+
sig { returns(T::Struct) }
|
19
|
+
attr_reader :expected
|
20
|
+
|
21
|
+
sig { returns(T.nilable(String)) }
|
22
|
+
attr_reader :id
|
23
|
+
|
24
|
+
sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
25
|
+
attr_reader :metadata
|
26
|
+
|
27
|
+
sig do
|
28
|
+
params(
|
29
|
+
signature_class: T.class_of(Signature),
|
30
|
+
input: T::Hash[Symbol, T.untyped],
|
31
|
+
expected: T::Hash[Symbol, T.untyped],
|
32
|
+
id: T.nilable(String),
|
33
|
+
metadata: T.nilable(T::Hash[Symbol, T.untyped])
|
34
|
+
).void
|
35
|
+
end
|
36
|
+
def initialize(signature_class:, input:, expected:, id: nil, metadata: nil)
|
37
|
+
@signature_class = signature_class
|
38
|
+
@id = id
|
39
|
+
@metadata = metadata&.freeze
|
40
|
+
|
41
|
+
# Validate and create input struct
|
42
|
+
begin
|
43
|
+
@input = signature_class.input_struct_class.new(**input)
|
44
|
+
rescue ArgumentError => e
|
45
|
+
raise ArgumentError, "Invalid input for #{signature_class.name}: #{e.message}"
|
46
|
+
rescue TypeError => e
|
47
|
+
raise TypeError, "Type error in input for #{signature_class.name}: #{e.message}"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Validate and create expected output struct
|
51
|
+
begin
|
52
|
+
@expected = signature_class.output_struct_class.new(**expected)
|
53
|
+
rescue ArgumentError => e
|
54
|
+
raise ArgumentError, "Invalid expected output for #{signature_class.name}: #{e.message}"
|
55
|
+
rescue TypeError => e
|
56
|
+
raise TypeError, "Type error in expected output for #{signature_class.name}: #{e.message}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Convert input struct to hash for program execution
|
61
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
62
|
+
def input_values
|
63
|
+
input_hash = {}
|
64
|
+
@input.class.props.keys.each do |key|
|
65
|
+
input_hash[key] = @input.send(key)
|
66
|
+
end
|
67
|
+
input_hash
|
68
|
+
end
|
69
|
+
|
70
|
+
# Convert expected struct to hash for comparison
|
71
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
72
|
+
def expected_values
|
73
|
+
expected_hash = {}
|
74
|
+
@expected.class.props.keys.each do |key|
|
75
|
+
expected_hash[key] = @expected.send(key)
|
76
|
+
end
|
77
|
+
expected_hash
|
78
|
+
end
|
79
|
+
|
80
|
+
# Check if prediction matches expected output using struct comparison
|
81
|
+
sig { params(prediction: T.untyped).returns(T::Boolean) }
|
82
|
+
def matches_prediction?(prediction)
|
83
|
+
return false unless prediction
|
84
|
+
|
85
|
+
# Compare each expected field with prediction
|
86
|
+
@expected.class.props.keys.all? do |key|
|
87
|
+
expected_value = @expected.send(key)
|
88
|
+
|
89
|
+
# Extract prediction value
|
90
|
+
prediction_value = case prediction
|
91
|
+
when T::Struct
|
92
|
+
prediction.respond_to?(key) ? prediction.send(key) : nil
|
93
|
+
when Hash
|
94
|
+
prediction[key] || prediction[key.to_s]
|
95
|
+
else
|
96
|
+
prediction.respond_to?(key) ? prediction.send(key) : nil
|
97
|
+
end
|
98
|
+
|
99
|
+
expected_value == prediction_value
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Serialization for persistence and debugging
|
104
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
105
|
+
def to_h
|
106
|
+
result = {
|
107
|
+
signature_class: @signature_class.name,
|
108
|
+
input: input_values,
|
109
|
+
expected: expected_values
|
110
|
+
}
|
111
|
+
|
112
|
+
result[:id] = @id if @id
|
113
|
+
result[:metadata] = @metadata if @metadata
|
114
|
+
result
|
115
|
+
end
|
116
|
+
|
117
|
+
# Create Example from hash representation
|
118
|
+
sig do
|
119
|
+
params(
|
120
|
+
hash: T::Hash[Symbol, T.untyped],
|
121
|
+
signature_registry: T.nilable(T::Hash[String, T.class_of(Signature)])
|
122
|
+
).returns(Example)
|
123
|
+
end
|
124
|
+
def self.from_h(hash, signature_registry: nil)
|
125
|
+
signature_class_name = hash[:signature_class]
|
126
|
+
|
127
|
+
# Resolve signature class
|
128
|
+
signature_class = if signature_registry && signature_registry[signature_class_name]
|
129
|
+
signature_registry[signature_class_name]
|
130
|
+
else
|
131
|
+
# Try to resolve from constant
|
132
|
+
Object.const_get(signature_class_name)
|
133
|
+
end
|
134
|
+
|
135
|
+
new(
|
136
|
+
signature_class: signature_class,
|
137
|
+
input: hash[:input] || {},
|
138
|
+
expected: hash[:expected] || {},
|
139
|
+
id: hash[:id],
|
140
|
+
metadata: hash[:metadata]
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
# Batch validation for multiple examples
|
146
|
+
sig do
|
147
|
+
params(
|
148
|
+
signature_class: T.class_of(Signature),
|
149
|
+
examples_data: T::Array[T::Hash[Symbol, T.untyped]]
|
150
|
+
).returns(T::Array[Example])
|
151
|
+
end
|
152
|
+
def self.validate_batch(signature_class, examples_data)
|
153
|
+
errors = []
|
154
|
+
examples = []
|
155
|
+
|
156
|
+
examples_data.each_with_index do |example_data, index|
|
157
|
+
begin
|
158
|
+
# Only support structured format with :input and :expected keys
|
159
|
+
unless example_data.key?(:input) && example_data.key?(:expected)
|
160
|
+
raise ArgumentError, "Example must have :input and :expected keys. Legacy flat format is no longer supported."
|
161
|
+
end
|
162
|
+
|
163
|
+
example = new(
|
164
|
+
signature_class: signature_class,
|
165
|
+
input: example_data[:input],
|
166
|
+
expected: example_data[:expected],
|
167
|
+
id: example_data[:id] || "example_#{index}"
|
168
|
+
)
|
169
|
+
examples << example
|
170
|
+
rescue => e
|
171
|
+
errors << "Example #{index}: #{e.message}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
unless errors.empty?
|
176
|
+
raise ArgumentError, "Validation errors:\n#{errors.join("\n")}"
|
177
|
+
end
|
178
|
+
|
179
|
+
examples
|
180
|
+
end
|
181
|
+
|
182
|
+
# Equality comparison
|
183
|
+
sig { params(other: T.untyped).returns(T::Boolean) }
|
184
|
+
def ==(other)
|
185
|
+
return false unless other.is_a?(Example)
|
186
|
+
|
187
|
+
@signature_class == other.signature_class &&
|
188
|
+
input_values == other.input_values &&
|
189
|
+
expected_values == other.expected_values
|
190
|
+
end
|
191
|
+
|
192
|
+
# String representation for debugging
|
193
|
+
sig { returns(String) }
|
194
|
+
def to_s
|
195
|
+
"DSPy::Example(#{@signature_class.name}) input=#{input_values} expected=#{expected_values}"
|
196
|
+
end
|
197
|
+
|
198
|
+
sig { returns(String) }
|
199
|
+
def inspect
|
200
|
+
to_s
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
|
5
|
+
module DSPy
|
6
|
+
class FewShotExample
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
10
|
+
attr_reader :input
|
11
|
+
|
12
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
13
|
+
attr_reader :output
|
14
|
+
|
15
|
+
sig { returns(T.nilable(String)) }
|
16
|
+
attr_reader :reasoning
|
17
|
+
|
18
|
+
sig do
|
19
|
+
params(
|
20
|
+
input: T::Hash[Symbol, T.untyped],
|
21
|
+
output: T::Hash[Symbol, T.untyped],
|
22
|
+
reasoning: T.nilable(String)
|
23
|
+
).void
|
24
|
+
end
|
25
|
+
def initialize(input:, output:, reasoning: nil)
|
26
|
+
@input = input.freeze
|
27
|
+
@output = output.freeze
|
28
|
+
@reasoning = reasoning
|
29
|
+
end
|
30
|
+
|
31
|
+
sig { returns(String) }
|
32
|
+
def to_prompt_section
|
33
|
+
sections = []
|
34
|
+
|
35
|
+
sections << "## Input"
|
36
|
+
sections << "```json"
|
37
|
+
sections << JSON.pretty_generate(@input)
|
38
|
+
sections << "```"
|
39
|
+
|
40
|
+
if @reasoning
|
41
|
+
sections << "## Reasoning"
|
42
|
+
sections << @reasoning
|
43
|
+
end
|
44
|
+
|
45
|
+
sections << "## Output"
|
46
|
+
sections << "```json"
|
47
|
+
sections << JSON.pretty_generate(@output)
|
48
|
+
sections << "```"
|
49
|
+
|
50
|
+
sections.join("\n")
|
51
|
+
end
|
52
|
+
|
53
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
54
|
+
def to_h
|
55
|
+
result = {
|
56
|
+
input: @input,
|
57
|
+
output: @output
|
58
|
+
}
|
59
|
+
result[:reasoning] = @reasoning if @reasoning
|
60
|
+
result
|
61
|
+
end
|
62
|
+
|
63
|
+
sig { params(hash: T::Hash[Symbol, T.untyped]).returns(FewShotExample) }
|
64
|
+
def self.from_h(hash)
|
65
|
+
new(
|
66
|
+
input: hash[:input] || {},
|
67
|
+
output: hash[:output] || {},
|
68
|
+
reasoning: hash[:reasoning]
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
sig { params(other: T.untyped).returns(T::Boolean) }
|
73
|
+
def ==(other)
|
74
|
+
return false unless other.is_a?(FewShotExample)
|
75
|
+
|
76
|
+
@input == other.input &&
|
77
|
+
@output == other.output &&
|
78
|
+
@reasoning == other.reasoning
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/dspy/instrumentation.rb
CHANGED
@@ -2,17 +2,34 @@
|
|
2
2
|
|
3
3
|
require 'dry-monitor'
|
4
4
|
require 'dry-configurable'
|
5
|
+
require 'time'
|
5
6
|
|
6
7
|
module DSPy
|
7
8
|
# Core instrumentation module using dry-monitor for event emission
|
8
|
-
# Provides extension points for logging,
|
9
|
+
# Provides extension points for logging, OpenTelemetry, New Relic, Langfuse, and custom monitoring
|
9
10
|
module Instrumentation
|
10
|
-
# Get
|
11
|
+
# Get a logger subscriber instance (creates new instance each time)
|
11
12
|
def self.logger_subscriber
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
require_relative 'subscribers/logger_subscriber'
|
14
|
+
DSPy::Subscribers::LoggerSubscriber.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# Get an OpenTelemetry subscriber instance (creates new instance each time)
|
18
|
+
def self.otel_subscriber
|
19
|
+
require_relative 'subscribers/otel_subscriber'
|
20
|
+
DSPy::Subscribers::OtelSubscriber.new
|
21
|
+
end
|
22
|
+
|
23
|
+
# Get a New Relic subscriber instance (creates new instance each time)
|
24
|
+
def self.newrelic_subscriber
|
25
|
+
require_relative 'subscribers/newrelic_subscriber'
|
26
|
+
DSPy::Subscribers::NewrelicSubscriber.new
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get a Langfuse subscriber instance (creates new instance each time)
|
30
|
+
def self.langfuse_subscriber
|
31
|
+
require_relative 'subscribers/langfuse_subscriber'
|
32
|
+
DSPy::Subscribers::LangfuseSubscriber.new
|
16
33
|
end
|
17
34
|
|
18
35
|
def self.notifications
|
@@ -29,6 +46,55 @@ module DSPy
|
|
29
46
|
n.register_event('dspy.react.tool_call')
|
30
47
|
n.register_event('dspy.react.iteration_complete')
|
31
48
|
n.register_event('dspy.react.max_iterations')
|
49
|
+
|
50
|
+
# Evaluation events
|
51
|
+
n.register_event('dspy.evaluation.start')
|
52
|
+
n.register_event('dspy.evaluation.example')
|
53
|
+
n.register_event('dspy.evaluation.batch')
|
54
|
+
n.register_event('dspy.evaluation.batch_complete')
|
55
|
+
|
56
|
+
# Optimization events
|
57
|
+
n.register_event('dspy.optimization.start')
|
58
|
+
n.register_event('dspy.optimization.complete')
|
59
|
+
n.register_event('dspy.optimization.trial_start')
|
60
|
+
n.register_event('dspy.optimization.trial_complete')
|
61
|
+
n.register_event('dspy.optimization.bootstrap_start')
|
62
|
+
n.register_event('dspy.optimization.bootstrap_complete')
|
63
|
+
n.register_event('dspy.optimization.bootstrap_example')
|
64
|
+
n.register_event('dspy.optimization.minibatch_evaluation')
|
65
|
+
n.register_event('dspy.optimization.instruction_proposal_start')
|
66
|
+
n.register_event('dspy.optimization.instruction_proposal_complete')
|
67
|
+
n.register_event('dspy.optimization.error')
|
68
|
+
n.register_event('dspy.optimization.save')
|
69
|
+
n.register_event('dspy.optimization.load')
|
70
|
+
|
71
|
+
# Storage events
|
72
|
+
n.register_event('dspy.storage.save_start')
|
73
|
+
n.register_event('dspy.storage.save_complete')
|
74
|
+
n.register_event('dspy.storage.save_error')
|
75
|
+
n.register_event('dspy.storage.load_start')
|
76
|
+
n.register_event('dspy.storage.load_complete')
|
77
|
+
n.register_event('dspy.storage.load_error')
|
78
|
+
n.register_event('dspy.storage.delete')
|
79
|
+
n.register_event('dspy.storage.export')
|
80
|
+
n.register_event('dspy.storage.import')
|
81
|
+
n.register_event('dspy.storage.cleanup')
|
82
|
+
|
83
|
+
# Registry events
|
84
|
+
n.register_event('dspy.registry.register_start')
|
85
|
+
n.register_event('dspy.registry.register_complete')
|
86
|
+
n.register_event('dspy.registry.register_error')
|
87
|
+
n.register_event('dspy.registry.deploy_start')
|
88
|
+
n.register_event('dspy.registry.deploy_complete')
|
89
|
+
n.register_event('dspy.registry.deploy_error')
|
90
|
+
n.register_event('dspy.registry.rollback_start')
|
91
|
+
n.register_event('dspy.registry.rollback_complete')
|
92
|
+
n.register_event('dspy.registry.rollback_error')
|
93
|
+
n.register_event('dspy.registry.performance_update')
|
94
|
+
n.register_event('dspy.registry.export')
|
95
|
+
n.register_event('dspy.registry.import')
|
96
|
+
n.register_event('dspy.registry.auto_deployment')
|
97
|
+
n.register_event('dspy.registry.automatic_rollback')
|
32
98
|
end
|
33
99
|
end
|
34
100
|
|
@@ -75,6 +141,9 @@ module DSPy
|
|
75
141
|
|
76
142
|
# Emit event without timing (for discrete events)
|
77
143
|
def self.emit(event_name, payload = {})
|
144
|
+
# Handle nil payload
|
145
|
+
payload ||= {}
|
146
|
+
|
78
147
|
enhanced_payload = payload.merge(
|
79
148
|
timestamp: Time.now.iso8601,
|
80
149
|
status: payload[:status] || 'success'
|
@@ -101,13 +170,33 @@ module DSPy
|
|
101
170
|
end
|
102
171
|
|
103
172
|
def self.emit_event(event_name, payload)
|
104
|
-
#
|
105
|
-
logger_subscriber
|
173
|
+
# Only emit events - subscribers self-register when explicitly created
|
106
174
|
notifications.instrument(event_name, payload)
|
107
175
|
end
|
108
176
|
|
109
177
|
def self.setup_subscribers
|
110
178
|
# Lazy initialization - will be created when first accessed
|
179
|
+
# Force initialization of enabled subscribers
|
180
|
+
logger_subscriber
|
181
|
+
|
182
|
+
# Only initialize if dependencies are available
|
183
|
+
begin
|
184
|
+
otel_subscriber if ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] || defined?(OpenTelemetry)
|
185
|
+
rescue LoadError
|
186
|
+
# OpenTelemetry not available, skip
|
187
|
+
end
|
188
|
+
|
189
|
+
begin
|
190
|
+
newrelic_subscriber if defined?(NewRelic)
|
191
|
+
rescue LoadError
|
192
|
+
# New Relic not available, skip
|
193
|
+
end
|
194
|
+
|
195
|
+
begin
|
196
|
+
langfuse_subscriber if ENV['LANGFUSE_SECRET_KEY'] || defined?(Langfuse)
|
197
|
+
rescue LoadError
|
198
|
+
# Langfuse not available, skip
|
199
|
+
end
|
111
200
|
end
|
112
201
|
end
|
113
202
|
end
|
@@ -7,8 +7,7 @@ module DSPy
|
|
7
7
|
# Maps provider prefixes to adapter classes
|
8
8
|
ADAPTER_MAP = {
|
9
9
|
'openai' => 'OpenAIAdapter',
|
10
|
-
'anthropic' => 'AnthropicAdapter'
|
11
|
-
'ruby_llm' => 'RubyLLMAdapter'
|
10
|
+
'anthropic' => 'AnthropicAdapter'
|
12
11
|
}.freeze
|
13
12
|
|
14
13
|
class << self
|
@@ -27,13 +26,12 @@ module DSPy
|
|
27
26
|
|
28
27
|
# Parse model_id to determine provider and model
|
29
28
|
def parse_model_id(model_id)
|
30
|
-
|
31
|
-
|
32
|
-
[provider, model]
|
33
|
-
else
|
34
|
-
# Legacy format: assume ruby_llm for backward compatibility
|
35
|
-
['ruby_llm', model_id]
|
29
|
+
unless model_id.include?('/')
|
30
|
+
raise ArgumentError, "model_id must include provider (e.g., 'openai/gpt-4', 'anthropic/claude-3'). Legacy format without provider is no longer supported."
|
36
31
|
end
|
32
|
+
|
33
|
+
provider, model = model_id.split('/', 2)
|
34
|
+
[provider, model]
|
37
35
|
end
|
38
36
|
|
39
37
|
def get_adapter_class(provider)
|
data/lib/dspy/lm.rb
CHANGED
@@ -13,7 +13,6 @@ require_relative 'instrumentation/token_tracker'
|
|
13
13
|
# Load adapters
|
14
14
|
require_relative 'lm/adapters/openai_adapter'
|
15
15
|
require_relative 'lm/adapters/anthropic_adapter'
|
16
|
-
require_relative 'lm/adapters/ruby_llm_adapter'
|
17
16
|
|
18
17
|
module DSPy
|
19
18
|
class LM
|
@@ -80,13 +79,12 @@ module DSPy
|
|
80
79
|
private
|
81
80
|
|
82
81
|
def parse_model_id(model_id)
|
83
|
-
|
84
|
-
|
85
|
-
[provider, model]
|
86
|
-
else
|
87
|
-
# Legacy format: assume ruby_llm for backward compatibility
|
88
|
-
['ruby_llm', model_id]
|
82
|
+
unless model_id.include?('/')
|
83
|
+
raise ArgumentError, "model_id must include provider (e.g., 'openai/gpt-4', 'anthropic/claude-3'). Legacy format without provider is no longer supported."
|
89
84
|
end
|
85
|
+
|
86
|
+
provider, model = model_id.split('/', 2)
|
87
|
+
[provider, model]
|
90
88
|
end
|
91
89
|
|
92
90
|
def build_messages(inference_module, input_values)
|
data/lib/dspy/predict.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'sorbet-runtime'
|
4
4
|
require_relative 'module'
|
5
5
|
require_relative 'instrumentation'
|
6
|
+
require_relative 'prompt'
|
6
7
|
|
7
8
|
module DSPy
|
8
9
|
# Exception raised when prediction fails validation
|
@@ -25,52 +26,49 @@ module DSPy
|
|
25
26
|
sig { returns(T.class_of(Signature)) }
|
26
27
|
attr_reader :signature_class
|
27
28
|
|
29
|
+
sig { returns(Prompt) }
|
30
|
+
attr_reader :prompt
|
31
|
+
|
28
32
|
sig { params(signature_class: T.class_of(Signature)).void }
|
29
33
|
def initialize(signature_class)
|
30
34
|
super()
|
31
35
|
@signature_class = signature_class
|
36
|
+
@prompt = Prompt.from_signature(signature_class)
|
32
37
|
end
|
33
38
|
|
39
|
+
# Backward compatibility methods - delegate to prompt object
|
34
40
|
sig { returns(String) }
|
35
41
|
def system_signature
|
36
|
-
|
37
|
-
Your input schema fields are:
|
38
|
-
```json
|
39
|
-
#{JSON.generate(@signature_class.input_json_schema)}
|
40
|
-
```
|
41
|
-
Your output schema fields are:
|
42
|
-
```json
|
43
|
-
#{JSON.generate(@signature_class.output_json_schema)}
|
44
|
-
````
|
45
|
-
|
46
|
-
All interactions will be structured in the following way, with the appropriate values filled in.
|
47
|
-
|
48
|
-
## Input values
|
49
|
-
```json
|
50
|
-
{input_values}
|
51
|
-
```
|
52
|
-
## Output values
|
53
|
-
Respond exclusively with the output schema fields in the json block below.
|
54
|
-
```json
|
55
|
-
{output_values}
|
56
|
-
```
|
57
|
-
|
58
|
-
In adhering to this structure, your objective is: #{@signature_class.description}
|
59
|
-
|
60
|
-
PROMPT
|
42
|
+
@prompt.render_system_prompt
|
61
43
|
end
|
62
44
|
|
63
45
|
sig { params(input_values: T::Hash[Symbol, T.untyped]).returns(String) }
|
64
46
|
def user_signature(input_values)
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
47
|
+
@prompt.render_user_prompt(input_values)
|
48
|
+
end
|
49
|
+
|
50
|
+
# New prompt-based interface for optimization
|
51
|
+
sig { params(new_prompt: Prompt).returns(Predict) }
|
52
|
+
def with_prompt(new_prompt)
|
53
|
+
# Create a new instance with the same signature but updated prompt
|
54
|
+
instance = self.class.new(@signature_class)
|
55
|
+
instance.instance_variable_set(:@prompt, new_prompt)
|
56
|
+
instance
|
57
|
+
end
|
58
|
+
|
59
|
+
sig { params(instruction: String).returns(Predict) }
|
60
|
+
def with_instruction(instruction)
|
61
|
+
with_prompt(@prompt.with_instruction(instruction))
|
62
|
+
end
|
63
|
+
|
64
|
+
sig { params(examples: T::Array[FewShotExample]).returns(Predict) }
|
65
|
+
def with_examples(examples)
|
66
|
+
with_prompt(@prompt.with_examples(examples))
|
67
|
+
end
|
68
|
+
|
69
|
+
sig { params(examples: T::Array[FewShotExample]).returns(Predict) }
|
70
|
+
def add_examples(examples)
|
71
|
+
with_prompt(@prompt.add_examples(examples))
|
74
72
|
end
|
75
73
|
|
76
74
|
sig { override.params(kwargs: T.untyped).returns(T.type_parameter(:O)) }
|