dspy 0.30.0 → 0.31.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 +68 -37
- data/lib/dspy/callbacks.rb +21 -2
- data/lib/dspy/context.rb +52 -1
- data/lib/dspy/evals.rb +21 -2
- data/lib/dspy/lm/adapter_factory.rb +40 -17
- data/lib/dspy/lm/errors.rb +3 -0
- data/lib/dspy/lm/json_strategy.rb +24 -8
- data/lib/dspy/lm.rb +62 -19
- data/lib/dspy/module.rb +213 -17
- data/lib/dspy/prompt.rb +94 -36
- data/lib/dspy/re_act.rb +50 -17
- data/lib/dspy/schema/sorbet_json_schema.rb +5 -2
- data/lib/dspy/schema/sorbet_toon_adapter.rb +80 -0
- data/lib/dspy/structured_outputs_prompt.rb +5 -3
- data/lib/dspy/type_serializer.rb +2 -1
- data/lib/dspy/version.rb +1 -1
- data/lib/dspy.rb +6 -0
- metadata +14 -51
- data/lib/dspy/lm/adapters/anthropic_adapter.rb +0 -291
- data/lib/dspy/lm/adapters/gemini/schema_converter.rb +0 -186
- data/lib/dspy/lm/adapters/gemini_adapter.rb +0 -220
- data/lib/dspy/lm/adapters/ollama_adapter.rb +0 -73
- data/lib/dspy/lm/adapters/openai/schema_converter.rb +0 -359
- data/lib/dspy/lm/adapters/openai_adapter.rb +0 -188
- data/lib/dspy/lm/adapters/openrouter_adapter.rb +0 -68
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sorbet-runtime'
|
|
4
|
+
require 'sorbet/toon'
|
|
5
|
+
|
|
6
|
+
require_relative '../lm/errors'
|
|
7
|
+
|
|
8
|
+
module DSPy
|
|
9
|
+
module Schema
|
|
10
|
+
module SorbetToonAdapter
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
sig { params(signature_class: T.nilable(T.class_of(DSPy::Signature)), values: T::Hash[Symbol, T.untyped]).returns(String) }
|
|
16
|
+
def render_input(signature_class, values)
|
|
17
|
+
Sorbet::Toon.encode(
|
|
18
|
+
values,
|
|
19
|
+
signature: signature_class,
|
|
20
|
+
role: :input
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { params(signature_class: T.nilable(T.class_of(DSPy::Signature)), values: T::Hash[Symbol, T.untyped]).returns(String) }
|
|
25
|
+
def render_expected_output(signature_class, values)
|
|
26
|
+
Sorbet::Toon.encode(
|
|
27
|
+
values,
|
|
28
|
+
signature: signature_class,
|
|
29
|
+
role: :output
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sig { params(signature_class: T.nilable(T.class_of(DSPy::Signature)), toon_string: String).returns(T.untyped) }
|
|
34
|
+
def parse_output(signature_class, toon_string)
|
|
35
|
+
payload = strip_code_fences(toon_string)
|
|
36
|
+
|
|
37
|
+
Sorbet::Toon.decode(
|
|
38
|
+
payload,
|
|
39
|
+
signature: signature_class,
|
|
40
|
+
role: :output
|
|
41
|
+
)
|
|
42
|
+
rescue Sorbet::Toon::DecodeError => e
|
|
43
|
+
log_decode_error(payload, e)
|
|
44
|
+
raise DSPy::LM::AdapterError,
|
|
45
|
+
"Failed to parse TOON response: #{e.message}. Ensure the model replies with a ```toon``` block using the schema described in the system prompt."
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sig { params(text: T.nilable(String)).returns(String) }
|
|
49
|
+
def strip_code_fences(text)
|
|
50
|
+
return '' if text.nil?
|
|
51
|
+
|
|
52
|
+
match = text.match(/```(?:toon)?\s*(.*?)```/m)
|
|
53
|
+
return match[1].strip if match
|
|
54
|
+
|
|
55
|
+
text.strip
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
sig { params(payload: String, error: StandardError).void }
|
|
59
|
+
def log_decode_error(payload, error)
|
|
60
|
+
logger = DSPy.logger if DSPy.respond_to?(:logger)
|
|
61
|
+
return unless logger.respond_to?(:warn)
|
|
62
|
+
|
|
63
|
+
preview = payload.to_s.lines.first(5).join
|
|
64
|
+
logger.warn(
|
|
65
|
+
event: 'toon.decode_error',
|
|
66
|
+
error: error.message,
|
|
67
|
+
preview: preview,
|
|
68
|
+
length: payload.to_s.length
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
sig { params(signature_class: T.nilable(T.class_of(DSPy::Signature)), role: Symbol).returns(String) }
|
|
73
|
+
def field_guidance(signature_class, role)
|
|
74
|
+
return '' unless signature_class
|
|
75
|
+
|
|
76
|
+
Sorbet::Toon::SignatureFormatter.describe_signature(signature_class, role)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -17,10 +17,11 @@ module DSPy
|
|
|
17
17
|
few_shot_examples: T::Array[T.untyped],
|
|
18
18
|
signature_class_name: T.nilable(String),
|
|
19
19
|
schema_format: Symbol,
|
|
20
|
-
signature_class: T.nilable(T.class_of(Signature))
|
|
20
|
+
signature_class: T.nilable(T.class_of(Signature)),
|
|
21
|
+
data_format: Symbol
|
|
21
22
|
).void
|
|
22
23
|
end
|
|
23
|
-
def initialize(instruction:, input_schema:, output_schema:, few_shot_examples: [], signature_class_name: nil, schema_format: :json, signature_class: nil)
|
|
24
|
+
def initialize(instruction:, input_schema:, output_schema:, few_shot_examples: [], signature_class_name: nil, schema_format: :json, signature_class: nil, data_format: :json)
|
|
24
25
|
normalized_examples = few_shot_examples.map do |example|
|
|
25
26
|
case example
|
|
26
27
|
when FewShotExample
|
|
@@ -39,7 +40,8 @@ module DSPy
|
|
|
39
40
|
few_shot_examples: normalized_examples,
|
|
40
41
|
signature_class_name: signature_class_name,
|
|
41
42
|
schema_format: schema_format,
|
|
42
|
-
signature_class: signature_class
|
|
43
|
+
signature_class: signature_class,
|
|
44
|
+
data_format: data_format
|
|
43
45
|
)
|
|
44
46
|
end
|
|
45
47
|
|
data/lib/dspy/type_serializer.rb
CHANGED
data/lib/dspy/version.rb
CHANGED
data/lib/dspy.rb
CHANGED
|
@@ -75,6 +75,10 @@ module DSPy
|
|
|
75
75
|
create_event_span(event_name, attributes)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
attributes = attributes.dup
|
|
79
|
+
module_metadata = DSPy::Context.module_context_attributes
|
|
80
|
+
attributes.merge!(module_metadata) unless module_metadata.empty?
|
|
81
|
+
|
|
78
82
|
# Perform the actual logging (original DSPy.log behavior)
|
|
79
83
|
# emit_log(event_name, attributes)
|
|
80
84
|
|
|
@@ -100,6 +104,8 @@ module DSPy
|
|
|
100
104
|
# Merge context automatically (but don't include span_stack)
|
|
101
105
|
context = Context.current.dup
|
|
102
106
|
context.delete(:span_stack)
|
|
107
|
+
context.delete(:otel_span_stack)
|
|
108
|
+
context.delete(:module_stack)
|
|
103
109
|
attributes = context.merge(attributes)
|
|
104
110
|
attributes[:event] = event_name
|
|
105
111
|
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dspy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.31.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vicente Reig Rincón de Arellano
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: dry-configurable
|
|
@@ -67,77 +66,49 @@ dependencies:
|
|
|
67
66
|
- !ruby/object:Gem::Version
|
|
68
67
|
version: '1.3'
|
|
69
68
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
|
72
|
-
requirements:
|
|
73
|
-
- - "~>"
|
|
74
|
-
- !ruby/object:Gem::Version
|
|
75
|
-
version: 0.22.0
|
|
76
|
-
type: :runtime
|
|
77
|
-
prerelease: false
|
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
-
requirements:
|
|
80
|
-
- - "~>"
|
|
81
|
-
- !ruby/object:Gem::Version
|
|
82
|
-
version: 0.22.0
|
|
83
|
-
- !ruby/object:Gem::Dependency
|
|
84
|
-
name: anthropic
|
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
|
86
|
-
requirements:
|
|
87
|
-
- - "~>"
|
|
88
|
-
- !ruby/object:Gem::Version
|
|
89
|
-
version: 1.5.0
|
|
90
|
-
type: :runtime
|
|
91
|
-
prerelease: false
|
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
-
requirements:
|
|
94
|
-
- - "~>"
|
|
95
|
-
- !ruby/object:Gem::Version
|
|
96
|
-
version: 1.5.0
|
|
97
|
-
- !ruby/object:Gem::Dependency
|
|
98
|
-
name: gemini-ai
|
|
69
|
+
name: sorbet-runtime
|
|
99
70
|
requirement: !ruby/object:Gem::Requirement
|
|
100
71
|
requirements:
|
|
101
72
|
- - "~>"
|
|
102
73
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: '
|
|
74
|
+
version: '0.5'
|
|
104
75
|
type: :runtime
|
|
105
76
|
prerelease: false
|
|
106
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
78
|
requirements:
|
|
108
79
|
- - "~>"
|
|
109
80
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '
|
|
81
|
+
version: '0.5'
|
|
111
82
|
- !ruby/object:Gem::Dependency
|
|
112
|
-
name: sorbet-
|
|
83
|
+
name: sorbet-schema
|
|
113
84
|
requirement: !ruby/object:Gem::Requirement
|
|
114
85
|
requirements:
|
|
115
86
|
- - "~>"
|
|
116
87
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: '0.
|
|
88
|
+
version: '0.3'
|
|
118
89
|
type: :runtime
|
|
119
90
|
prerelease: false
|
|
120
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
92
|
requirements:
|
|
122
93
|
- - "~>"
|
|
123
94
|
- !ruby/object:Gem::Version
|
|
124
|
-
version: '0.
|
|
95
|
+
version: '0.3'
|
|
125
96
|
- !ruby/object:Gem::Dependency
|
|
126
|
-
name: sorbet-
|
|
97
|
+
name: sorbet-baml
|
|
127
98
|
requirement: !ruby/object:Gem::Requirement
|
|
128
99
|
requirements:
|
|
129
100
|
- - "~>"
|
|
130
101
|
- !ruby/object:Gem::Version
|
|
131
|
-
version: '0.
|
|
102
|
+
version: '0.1'
|
|
132
103
|
type: :runtime
|
|
133
104
|
prerelease: false
|
|
134
105
|
version_requirements: !ruby/object:Gem::Requirement
|
|
135
106
|
requirements:
|
|
136
107
|
- - "~>"
|
|
137
108
|
- !ruby/object:Gem::Version
|
|
138
|
-
version: '0.
|
|
109
|
+
version: '0.1'
|
|
139
110
|
- !ruby/object:Gem::Dependency
|
|
140
|
-
name: sorbet-
|
|
111
|
+
name: sorbet-toon
|
|
141
112
|
requirement: !ruby/object:Gem::Requirement
|
|
142
113
|
requirements:
|
|
143
114
|
- - "~>"
|
|
@@ -209,13 +180,6 @@ files:
|
|
|
209
180
|
- lib/dspy/lm.rb
|
|
210
181
|
- lib/dspy/lm/adapter.rb
|
|
211
182
|
- lib/dspy/lm/adapter_factory.rb
|
|
212
|
-
- lib/dspy/lm/adapters/anthropic_adapter.rb
|
|
213
|
-
- lib/dspy/lm/adapters/gemini/schema_converter.rb
|
|
214
|
-
- lib/dspy/lm/adapters/gemini_adapter.rb
|
|
215
|
-
- lib/dspy/lm/adapters/ollama_adapter.rb
|
|
216
|
-
- lib/dspy/lm/adapters/openai/schema_converter.rb
|
|
217
|
-
- lib/dspy/lm/adapters/openai_adapter.rb
|
|
218
|
-
- lib/dspy/lm/adapters/openrouter_adapter.rb
|
|
219
183
|
- lib/dspy/lm/chat_strategy.rb
|
|
220
184
|
- lib/dspy/lm/errors.rb
|
|
221
185
|
- lib/dspy/lm/json_strategy.rb
|
|
@@ -248,6 +212,7 @@ files:
|
|
|
248
212
|
- lib/dspy/registry/signature_registry.rb
|
|
249
213
|
- lib/dspy/schema.rb
|
|
250
214
|
- lib/dspy/schema/sorbet_json_schema.rb
|
|
215
|
+
- lib/dspy/schema/sorbet_toon_adapter.rb
|
|
251
216
|
- lib/dspy/schema/version.rb
|
|
252
217
|
- lib/dspy/schema_adapters.rb
|
|
253
218
|
- lib/dspy/signature.rb
|
|
@@ -275,7 +240,6 @@ homepage: https://github.com/vicentereig/dspy.rb
|
|
|
275
240
|
licenses:
|
|
276
241
|
- MIT
|
|
277
242
|
metadata: {}
|
|
278
|
-
post_install_message:
|
|
279
243
|
rdoc_options: []
|
|
280
244
|
require_paths:
|
|
281
245
|
- lib
|
|
@@ -290,8 +254,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
290
254
|
- !ruby/object:Gem::Version
|
|
291
255
|
version: '0'
|
|
292
256
|
requirements: []
|
|
293
|
-
rubygems_version: 3.
|
|
294
|
-
signing_key:
|
|
257
|
+
rubygems_version: 3.6.9
|
|
295
258
|
specification_version: 4
|
|
296
259
|
summary: The Ruby framework for programming—rather than prompting—language models.
|
|
297
260
|
test_files: []
|
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'anthropic'
|
|
4
|
-
require_relative '../vision_models'
|
|
5
|
-
|
|
6
|
-
module DSPy
|
|
7
|
-
class LM
|
|
8
|
-
class AnthropicAdapter < Adapter
|
|
9
|
-
def initialize(model:, api_key:, structured_outputs: true)
|
|
10
|
-
super(model: model, api_key: api_key)
|
|
11
|
-
validate_api_key!(api_key, 'anthropic')
|
|
12
|
-
@client = Anthropic::Client.new(api_key: api_key)
|
|
13
|
-
@structured_outputs_enabled = structured_outputs
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def chat(messages:, signature: nil, **extra_params, &block)
|
|
17
|
-
normalized_messages = normalize_messages(messages)
|
|
18
|
-
|
|
19
|
-
# Validate vision support if images are present
|
|
20
|
-
if contains_images?(normalized_messages)
|
|
21
|
-
VisionModels.validate_vision_support!('anthropic', model)
|
|
22
|
-
# Convert messages to Anthropic format with proper image handling
|
|
23
|
-
normalized_messages = format_multimodal_messages(normalized_messages)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Anthropic requires system message to be separate from messages
|
|
27
|
-
system_message, user_messages = extract_system_message(normalized_messages)
|
|
28
|
-
|
|
29
|
-
# Check if this is a tool use request
|
|
30
|
-
has_tools = extra_params.key?(:tools) && !extra_params[:tools].empty?
|
|
31
|
-
|
|
32
|
-
# Apply JSON prefilling if needed for better Claude JSON compliance (but not for tool use)
|
|
33
|
-
unless has_tools || contains_images?(normalized_messages)
|
|
34
|
-
user_messages = prepare_messages_for_json(user_messages, system_message)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
request_params = {
|
|
38
|
-
model: model,
|
|
39
|
-
messages: user_messages,
|
|
40
|
-
max_tokens: 4096, # Required for Anthropic
|
|
41
|
-
temperature: 0.0 # DSPy default for deterministic responses
|
|
42
|
-
}.merge(extra_params)
|
|
43
|
-
|
|
44
|
-
# Add system message if present
|
|
45
|
-
request_params[:system] = system_message if system_message
|
|
46
|
-
|
|
47
|
-
# Add streaming if block provided
|
|
48
|
-
if block_given?
|
|
49
|
-
request_params[:stream] = true
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
begin
|
|
53
|
-
if block_given?
|
|
54
|
-
content = ""
|
|
55
|
-
@client.messages.stream(**request_params) do |chunk|
|
|
56
|
-
if chunk.respond_to?(:delta) && chunk.delta.respond_to?(:text)
|
|
57
|
-
chunk_text = chunk.delta.text
|
|
58
|
-
content += chunk_text
|
|
59
|
-
block.call(chunk)
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Create typed metadata for streaming response
|
|
64
|
-
metadata = ResponseMetadataFactory.create('anthropic', {
|
|
65
|
-
model: model,
|
|
66
|
-
streaming: true
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
Response.new(
|
|
70
|
-
content: content,
|
|
71
|
-
usage: nil, # Usage not available in streaming
|
|
72
|
-
metadata: metadata
|
|
73
|
-
)
|
|
74
|
-
else
|
|
75
|
-
response = @client.messages.create(**request_params)
|
|
76
|
-
|
|
77
|
-
if response.respond_to?(:error) && response.error
|
|
78
|
-
raise AdapterError, "Anthropic API error: #{response.error}"
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Handle both text content and tool use
|
|
82
|
-
content = ""
|
|
83
|
-
tool_calls = []
|
|
84
|
-
|
|
85
|
-
if response.content.is_a?(Array)
|
|
86
|
-
response.content.each do |content_block|
|
|
87
|
-
case content_block.type.to_s
|
|
88
|
-
when "text"
|
|
89
|
-
content += content_block.text
|
|
90
|
-
when "tool_use"
|
|
91
|
-
tool_calls << {
|
|
92
|
-
id: content_block.id,
|
|
93
|
-
name: content_block.name,
|
|
94
|
-
input: content_block.input
|
|
95
|
-
}
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
usage = response.usage
|
|
101
|
-
|
|
102
|
-
# Convert usage data to typed struct
|
|
103
|
-
usage_struct = UsageFactory.create('anthropic', usage)
|
|
104
|
-
|
|
105
|
-
metadata = {
|
|
106
|
-
provider: 'anthropic',
|
|
107
|
-
model: model,
|
|
108
|
-
response_id: response.id,
|
|
109
|
-
role: response.role
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
# Add tool calls to metadata if present
|
|
113
|
-
metadata[:tool_calls] = tool_calls unless tool_calls.empty?
|
|
114
|
-
|
|
115
|
-
# Create typed metadata
|
|
116
|
-
typed_metadata = ResponseMetadataFactory.create('anthropic', metadata)
|
|
117
|
-
|
|
118
|
-
Response.new(
|
|
119
|
-
content: content,
|
|
120
|
-
usage: usage_struct,
|
|
121
|
-
metadata: typed_metadata
|
|
122
|
-
)
|
|
123
|
-
end
|
|
124
|
-
rescue => e
|
|
125
|
-
# Check for specific image-related errors in the message
|
|
126
|
-
error_msg = e.message.to_s
|
|
127
|
-
|
|
128
|
-
if error_msg.include?('Could not process image')
|
|
129
|
-
raise AdapterError, "Image processing failed: #{error_msg}. Ensure your image is a valid PNG, JPEG, GIF, or WebP format, properly base64-encoded, and under 5MB."
|
|
130
|
-
elsif error_msg.include?('image')
|
|
131
|
-
raise AdapterError, "Image error: #{error_msg}. Anthropic requires base64-encoded images (URLs are not supported)."
|
|
132
|
-
elsif error_msg.include?('rate')
|
|
133
|
-
raise AdapterError, "Anthropic rate limit exceeded: #{error_msg}. Please wait and try again."
|
|
134
|
-
elsif error_msg.include?('authentication') || error_msg.include?('API key')
|
|
135
|
-
raise AdapterError, "Anthropic authentication failed: #{error_msg}. Check your API key."
|
|
136
|
-
else
|
|
137
|
-
# Generic error handling
|
|
138
|
-
raise AdapterError, "Anthropic adapter error: #{e.message}"
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
private
|
|
144
|
-
|
|
145
|
-
# Enhanced JSON extraction specifically for Claude models
|
|
146
|
-
# Handles multiple patterns of markdown-wrapped JSON responses
|
|
147
|
-
def extract_json_from_response(content)
|
|
148
|
-
return content if content.nil? || content.empty?
|
|
149
|
-
|
|
150
|
-
# Pattern 1: ```json blocks
|
|
151
|
-
if content.include?('```json')
|
|
152
|
-
extracted = content[/```json\s*\n(.*?)\n```/m, 1]
|
|
153
|
-
return extracted.strip if extracted
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# Pattern 2: ## Output values header
|
|
157
|
-
if content.include?('## Output values')
|
|
158
|
-
extracted = content.split('## Output values').last
|
|
159
|
-
.gsub(/```json\s*\n/, '')
|
|
160
|
-
.gsub(/\n```.*/, '')
|
|
161
|
-
.strip
|
|
162
|
-
return extracted if extracted && !extracted.empty?
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
# Pattern 3: Generic code blocks (check if it looks like JSON)
|
|
166
|
-
if content.include?('```')
|
|
167
|
-
extracted = content[/```\s*\n(.*?)\n```/m, 1]
|
|
168
|
-
return extracted.strip if extracted && looks_like_json?(extracted)
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# Pattern 4: Already valid JSON or fallback
|
|
172
|
-
content.strip
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# Simple heuristic to check if content looks like JSON
|
|
176
|
-
def looks_like_json?(str)
|
|
177
|
-
return false if str.nil? || str.empty?
|
|
178
|
-
trimmed = str.strip
|
|
179
|
-
(trimmed.start_with?('{') && trimmed.end_with?('}')) ||
|
|
180
|
-
(trimmed.start_with?('[') && trimmed.end_with?(']'))
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# Prepare messages for JSON output by adding prefilling and strong instructions
|
|
184
|
-
def prepare_messages_for_json(user_messages, system_message)
|
|
185
|
-
return user_messages unless requires_json_output?(user_messages, system_message)
|
|
186
|
-
return user_messages unless tends_to_wrap_json?
|
|
187
|
-
|
|
188
|
-
# Add strong JSON instruction to the last user message if not already present
|
|
189
|
-
enhanced_messages = enhance_json_instructions(user_messages)
|
|
190
|
-
|
|
191
|
-
# Only add prefill for models that support it and temporarily disable for testing
|
|
192
|
-
if false # supports_prefilling? - temporarily disabled
|
|
193
|
-
add_json_prefill(enhanced_messages)
|
|
194
|
-
else
|
|
195
|
-
enhanced_messages
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
# Detect if the conversation requires JSON output
|
|
200
|
-
def requires_json_output?(user_messages, system_message)
|
|
201
|
-
# Check for JSON-related keywords in messages
|
|
202
|
-
all_content = [system_message] + user_messages.map { |m| m[:content] }
|
|
203
|
-
all_content.compact.any? do |content|
|
|
204
|
-
content.downcase.include?('json') ||
|
|
205
|
-
content.include?('```') ||
|
|
206
|
-
content.include?('{') ||
|
|
207
|
-
content.include?('output')
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# Check if this is a Claude model that benefits from prefilling
|
|
212
|
-
def supports_prefilling?
|
|
213
|
-
# Claude models that work well with JSON prefilling
|
|
214
|
-
model.downcase.include?('claude')
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
# Check if this is a Claude model that tends to wrap JSON in markdown
|
|
218
|
-
def tends_to_wrap_json?
|
|
219
|
-
# All Claude models have this tendency, especially Opus variants
|
|
220
|
-
model.downcase.include?('claude')
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
# Enhance the last user message with strong JSON instructions
|
|
224
|
-
def enhance_json_instructions(user_messages)
|
|
225
|
-
return user_messages if user_messages.empty?
|
|
226
|
-
|
|
227
|
-
enhanced_messages = user_messages.dup
|
|
228
|
-
last_message = enhanced_messages.last
|
|
229
|
-
|
|
230
|
-
# Only add instruction if not already present
|
|
231
|
-
unless last_message[:content].include?('ONLY valid JSON')
|
|
232
|
-
# Use smart default instruction for Claude models
|
|
233
|
-
json_instruction = "\n\nIMPORTANT: Respond with ONLY valid JSON. No markdown formatting, no code blocks, no explanations. Start your response with '{' and end with '}'."
|
|
234
|
-
|
|
235
|
-
last_message = last_message.dup
|
|
236
|
-
last_message[:content] = last_message[:content] + json_instruction
|
|
237
|
-
enhanced_messages[-1] = last_message
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
enhanced_messages
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
# Add assistant message prefill to guide Claude
|
|
244
|
-
def add_json_prefill(user_messages)
|
|
245
|
-
user_messages + [{ role: "assistant", content: "{" }]
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
def extract_system_message(messages)
|
|
249
|
-
system_message = nil
|
|
250
|
-
user_messages = []
|
|
251
|
-
|
|
252
|
-
messages.each do |msg|
|
|
253
|
-
if msg[:role] == 'system'
|
|
254
|
-
system_message = msg[:content]
|
|
255
|
-
else
|
|
256
|
-
user_messages << msg
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
[system_message, user_messages]
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
def format_multimodal_messages(messages)
|
|
264
|
-
messages.map do |msg|
|
|
265
|
-
if msg[:content].is_a?(Array)
|
|
266
|
-
# Convert multimodal content to Anthropic format
|
|
267
|
-
formatted_content = msg[:content].map do |item|
|
|
268
|
-
case item[:type]
|
|
269
|
-
when 'text'
|
|
270
|
-
{ type: 'text', text: item[:text] }
|
|
271
|
-
when 'image'
|
|
272
|
-
# Validate image compatibility before formatting
|
|
273
|
-
item[:image].validate_for_provider!('anthropic')
|
|
274
|
-
item[:image].to_anthropic_format
|
|
275
|
-
else
|
|
276
|
-
item
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
{
|
|
281
|
-
role: msg[:role],
|
|
282
|
-
content: formatted_content
|
|
283
|
-
}
|
|
284
|
-
else
|
|
285
|
-
msg
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
end
|
|
289
|
-
end
|
|
290
|
-
end
|
|
291
|
-
end
|