ruby_llm 0.1.0.pre30 → 0.1.0.pre33
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/.github/workflows/{gem-push.yml → cicd.yml} +32 -4
- data/.rspec_status +38 -0
- data/README.md +52 -3
- data/lib/ruby_llm/active_record/acts_as.rb +5 -5
- data/lib/ruby_llm/chat.rb +2 -2
- data/lib/ruby_llm/configuration.rb +5 -1
- data/lib/ruby_llm/content.rb +81 -0
- data/lib/ruby_llm/embedding.rb +9 -3
- data/lib/ruby_llm/image.rb +24 -0
- data/lib/ruby_llm/message.rb +9 -1
- data/lib/ruby_llm/models.json +14 -14
- data/lib/ruby_llm/provider.rb +57 -16
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +81 -0
- data/lib/ruby_llm/providers/anthropic/chat.rb +86 -0
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
- data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +37 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +97 -0
- data/lib/ruby_llm/providers/anthropic.rb +8 -234
- data/lib/ruby_llm/providers/deepseek/capabilites.rb +101 -0
- data/lib/ruby_llm/providers/deepseek.rb +4 -2
- data/lib/ruby_llm/providers/gemini/capabilities.rb +191 -0
- data/lib/ruby_llm/providers/gemini/models.rb +20 -0
- data/lib/ruby_llm/providers/gemini.rb +5 -10
- data/lib/ruby_llm/providers/openai/capabilities.rb +191 -0
- data/lib/ruby_llm/providers/openai/chat.rb +68 -0
- data/lib/ruby_llm/providers/openai/embeddings.rb +39 -0
- data/lib/ruby_llm/providers/openai/images.rb +38 -0
- data/lib/ruby_llm/providers/openai/media.rb +52 -0
- data/lib/ruby_llm/providers/openai/models.rb +40 -0
- data/lib/ruby_llm/providers/openai/streaming.rb +31 -0
- data/lib/ruby_llm/providers/openai/tools.rb +69 -0
- data/lib/ruby_llm/providers/openai.rb +22 -200
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +8 -2
- data/ruby_llm.gemspec +7 -5
- metadata +57 -13
- data/.github/workflows/test.yml +0 -35
- data/lib/ruby_llm/model_capabilities/anthropic.rb +0 -79
- data/lib/ruby_llm/model_capabilities/deepseek.rb +0 -132
- data/lib/ruby_llm/model_capabilities/gemini.rb +0 -190
- data/lib/ruby_llm/model_capabilities/openai.rb +0 -189
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module Anthropic
|
6
|
+
# Determines capabilities and pricing for Anthropic models
|
7
|
+
module Capabilities
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def determine_context_window(model_id)
|
11
|
+
case model_id
|
12
|
+
when /claude-3/ then 200_000
|
13
|
+
else 100_000
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def determine_max_tokens(model_id)
|
18
|
+
case model_id
|
19
|
+
when /claude-3-5/ then 8_192
|
20
|
+
else 4_096
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_input_price(model_id)
|
25
|
+
PRICES.dig(model_family(model_id), :input) || default_input_price
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_output_price(model_id)
|
29
|
+
PRICES.dig(model_family(model_id), :output) || default_output_price
|
30
|
+
end
|
31
|
+
|
32
|
+
def supports_vision?(model_id)
|
33
|
+
return false if model_id.match?(/claude-3-5-haiku/)
|
34
|
+
return false if model_id.match?(/claude-[12]/)
|
35
|
+
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def supports_functions?(model_id)
|
40
|
+
model_id.include?('claude-3')
|
41
|
+
end
|
42
|
+
|
43
|
+
def supports_json_mode?(model_id)
|
44
|
+
model_id.include?('claude-3')
|
45
|
+
end
|
46
|
+
|
47
|
+
def model_family(model_id)
|
48
|
+
case model_id
|
49
|
+
when /claude-3-5-sonnet/ then :claude35_sonnet
|
50
|
+
when /claude-3-5-haiku/ then :claude35_haiku
|
51
|
+
when /claude-3-opus/ then :claude3_opus
|
52
|
+
when /claude-3-sonnet/ then :claude3_sonnet
|
53
|
+
when /claude-3-haiku/ then :claude3_haiku
|
54
|
+
else :claude2
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def model_type(_)
|
59
|
+
'chat'
|
60
|
+
end
|
61
|
+
|
62
|
+
PRICES = {
|
63
|
+
claude35_sonnet: { input: 3.0, output: 15.0 }, # $3.00/$15.00 per million tokens
|
64
|
+
claude35_haiku: { input: 0.80, output: 4.0 }, # $0.80/$4.00 per million tokens
|
65
|
+
claude3_opus: { input: 15.0, output: 75.0 }, # $15.00/$75.00 per million tokens
|
66
|
+
claude3_sonnet: { input: 3.0, output: 15.0 }, # $3.00/$15.00 per million tokens
|
67
|
+
claude3_haiku: { input: 0.25, output: 1.25 }, # $0.25/$1.25 per million tokens
|
68
|
+
claude2: { input: 3.0, output: 15.0 } # Default pricing for Claude 2.x models
|
69
|
+
}.freeze
|
70
|
+
|
71
|
+
def default_input_price
|
72
|
+
3.0
|
73
|
+
end
|
74
|
+
|
75
|
+
def default_output_price
|
76
|
+
15.0
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module Anthropic
|
6
|
+
# Chat methods of the OpenAI API integration
|
7
|
+
module Chat
|
8
|
+
private
|
9
|
+
|
10
|
+
def completion_url
|
11
|
+
'/v1/messages'
|
12
|
+
end
|
13
|
+
|
14
|
+
def render_payload(messages, tools:, temperature:, model:, stream: false)
|
15
|
+
{
|
16
|
+
model: model,
|
17
|
+
messages: messages.map { |msg| format_message(msg) },
|
18
|
+
temperature: temperature,
|
19
|
+
stream: stream,
|
20
|
+
max_tokens: RubyLLM.models.find(model).max_tokens
|
21
|
+
}.tap do |payload|
|
22
|
+
payload[:tools] = tools.values.map { |t| function_for(t) } if tools.any?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_completion_response(response)
|
27
|
+
data = response.body
|
28
|
+
content_blocks = data['content'] || []
|
29
|
+
|
30
|
+
text_content = extract_text_content(content_blocks)
|
31
|
+
tool_use = find_tool_use(content_blocks)
|
32
|
+
|
33
|
+
build_message(data, text_content, tool_use)
|
34
|
+
end
|
35
|
+
|
36
|
+
def extract_text_content(blocks)
|
37
|
+
text_blocks = blocks.select { |c| c['type'] == 'text' }
|
38
|
+
text_blocks.map { |c| c['text'] }.join('')
|
39
|
+
end
|
40
|
+
|
41
|
+
def build_message(data, content, tool_use)
|
42
|
+
Message.new(
|
43
|
+
role: :assistant,
|
44
|
+
content: content,
|
45
|
+
tool_calls: parse_tool_calls(tool_use),
|
46
|
+
input_tokens: data.dig('usage', 'input_tokens'),
|
47
|
+
output_tokens: data.dig('usage', 'output_tokens'),
|
48
|
+
model_id: data['model']
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def format_message(msg)
|
53
|
+
if msg.tool_call?
|
54
|
+
format_tool_call(msg)
|
55
|
+
elsif msg.tool_result?
|
56
|
+
format_tool_result(msg)
|
57
|
+
else
|
58
|
+
format_basic_message(msg)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def format_basic_message(msg)
|
63
|
+
{
|
64
|
+
role: convert_role(msg.role),
|
65
|
+
content: msg.content
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def convert_role(role)
|
70
|
+
case role
|
71
|
+
when :tool then 'user'
|
72
|
+
when :user then 'user'
|
73
|
+
else 'assistant'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def format_text_block(content)
|
78
|
+
{
|
79
|
+
type: 'text',
|
80
|
+
text: content
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module Anthropic
|
6
|
+
# Embeddings methods of the Anthropic API integration
|
7
|
+
module Embeddings
|
8
|
+
private
|
9
|
+
|
10
|
+
def embed
|
11
|
+
raise Error "Anthropic doesn't support embeddings"
|
12
|
+
end
|
13
|
+
|
14
|
+
alias render_embedding_payload embed
|
15
|
+
alias embedding_url embed
|
16
|
+
alias parse_embedding_response embed
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module Anthropic
|
6
|
+
# Models methods of the Anthropic API integration
|
7
|
+
module Models
|
8
|
+
private
|
9
|
+
|
10
|
+
def models_url
|
11
|
+
'/v1/models'
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse_list_models_response(response) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
15
|
+
(response.body['data'] || []).map do |model|
|
16
|
+
ModelInfo.new(
|
17
|
+
id: model['id'],
|
18
|
+
created_at: Time.parse(model['created_at']),
|
19
|
+
display_name: model['display_name'],
|
20
|
+
provider: slug,
|
21
|
+
type: capabilities.model_type(model['id']),
|
22
|
+
family: capabilities.model_family(model['id']),
|
23
|
+
context_window: capabilities.determine_context_window(model['id']),
|
24
|
+
max_tokens: capabilities.determine_max_tokens(model['id']),
|
25
|
+
supports_vision: capabilities.supports_vision?(model['id']),
|
26
|
+
supports_functions: capabilities.supports_functions?(model['id']),
|
27
|
+
supports_json_mode: capabilities.supports_json_mode?(model['id']),
|
28
|
+
input_price_per_million: capabilities.get_input_price(model['id']),
|
29
|
+
output_price_per_million: capabilities.get_output_price(model['id'])
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def extract_model_id(data)
|
35
|
+
data.dig('message', 'model')
|
36
|
+
end
|
37
|
+
|
38
|
+
def extract_input_tokens(data)
|
39
|
+
data.dig('message', 'usage', 'input_tokens')
|
40
|
+
end
|
41
|
+
|
42
|
+
def extract_output_tokens(data)
|
43
|
+
data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module Anthropic
|
6
|
+
# Streaming methods of the Anthropic API integration
|
7
|
+
module Streaming
|
8
|
+
private
|
9
|
+
|
10
|
+
def stream_url
|
11
|
+
completion_url
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle_stream(&block)
|
15
|
+
to_json_stream do |data|
|
16
|
+
block.call(build_chunk(data))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def build_chunk(data)
|
21
|
+
Chunk.new(
|
22
|
+
role: :assistant,
|
23
|
+
model_id: extract_model_id(data),
|
24
|
+
content: data.dig('delta', 'text'),
|
25
|
+
input_tokens: extract_input_tokens(data),
|
26
|
+
output_tokens: extract_output_tokens(data),
|
27
|
+
tool_calls: extract_tool_calls(data)
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def json_delta?(data)
|
32
|
+
data['type'] == 'content_block_delta' && data.dig('delta', 'type') == 'input_json_delta'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module Anthropic
|
6
|
+
# Tools methods of the Anthropic API integration
|
7
|
+
module Tools
|
8
|
+
private
|
9
|
+
|
10
|
+
def find_tool_use(blocks)
|
11
|
+
blocks.find { |c| c['type'] == 'tool_use' }
|
12
|
+
end
|
13
|
+
|
14
|
+
def format_tool_call(msg)
|
15
|
+
tool_call = msg.tool_calls.values.first
|
16
|
+
|
17
|
+
{
|
18
|
+
role: 'assistant',
|
19
|
+
content: [
|
20
|
+
format_text_block(msg.content),
|
21
|
+
format_tool_use_block(tool_call)
|
22
|
+
]
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def format_tool_result(msg)
|
27
|
+
{
|
28
|
+
role: 'user',
|
29
|
+
content: [format_tool_result_block(msg)]
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def format_tool_use_block(tool_call)
|
34
|
+
{
|
35
|
+
type: 'tool_use',
|
36
|
+
id: tool_call.id,
|
37
|
+
name: tool_call.name,
|
38
|
+
input: tool_call.arguments
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def format_tool_result_block(msg)
|
43
|
+
{
|
44
|
+
type: 'tool_result',
|
45
|
+
tool_use_id: msg.tool_call_id,
|
46
|
+
content: msg.content
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def function_for(tool)
|
51
|
+
{
|
52
|
+
name: tool.name,
|
53
|
+
description: tool.description,
|
54
|
+
input_schema: {
|
55
|
+
type: 'object',
|
56
|
+
properties: clean_parameters(tool.parameters),
|
57
|
+
required: required_parameters(tool.parameters)
|
58
|
+
}
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
def extract_tool_calls(data)
|
63
|
+
if json_delta?(data)
|
64
|
+
{ nil => ToolCall.new(id: nil, name: nil, arguments: data.dig('delta', 'partial_json')) }
|
65
|
+
else
|
66
|
+
parse_tool_calls(data['content_block'])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_tool_calls(content_block)
|
71
|
+
return nil unless content_block && content_block['type'] == 'tool_use'
|
72
|
+
|
73
|
+
{
|
74
|
+
content_block['id'] => ToolCall.new(
|
75
|
+
id: content_block['id'],
|
76
|
+
name: content_block['name'],
|
77
|
+
arguments: content_block['input']
|
78
|
+
)
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
def clean_parameters(parameters)
|
83
|
+
parameters.transform_values do |param|
|
84
|
+
{
|
85
|
+
type: param.type,
|
86
|
+
description: param.description
|
87
|
+
}.compact
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def required_parameters(parameters)
|
92
|
+
parameters.select { |_, param| param.required }.keys
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -4,17 +4,15 @@ module RubyLLM
|
|
4
4
|
module Providers
|
5
5
|
# Anthropic Claude API integration. Handles the complexities of
|
6
6
|
# Claude's unique message format and tool calling conventions.
|
7
|
-
|
8
|
-
|
7
|
+
module Anthropic
|
8
|
+
extend Provider
|
9
|
+
extend Anthropic::Chat
|
10
|
+
extend Anthropic::Embeddings
|
11
|
+
extend Anthropic::Models
|
12
|
+
extend Anthropic::Streaming
|
13
|
+
extend Anthropic::Tools
|
9
14
|
|
10
|
-
|
11
|
-
return if response.body.empty?
|
12
|
-
|
13
|
-
body = try_parse_json(response.body)
|
14
|
-
body.is_a?(Hash) ? body.dig('error', 'message') : body
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
15
|
+
module_function
|
18
16
|
|
19
17
|
def api_base
|
20
18
|
'https://api.anthropic.com'
|
@@ -26,230 +24,6 @@ module RubyLLM
|
|
26
24
|
'anthropic-version' => '2023-06-01'
|
27
25
|
}
|
28
26
|
end
|
29
|
-
|
30
|
-
def completion_url
|
31
|
-
'/v1/messages'
|
32
|
-
end
|
33
|
-
|
34
|
-
def stream_url
|
35
|
-
completion_url
|
36
|
-
end
|
37
|
-
|
38
|
-
def models_url
|
39
|
-
'/v1/models'
|
40
|
-
end
|
41
|
-
|
42
|
-
def build_payload(messages, tools:, temperature:, model:, stream: false)
|
43
|
-
{
|
44
|
-
model: model,
|
45
|
-
messages: format_messages(messages),
|
46
|
-
temperature: temperature,
|
47
|
-
stream: stream,
|
48
|
-
max_tokens: RubyLLM.models.find(model).max_tokens
|
49
|
-
}.tap do |payload|
|
50
|
-
payload[:tools] = tools.values.map { |t| function_for(t) } if tools.any?
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def parse_completion_response(response)
|
55
|
-
data = response.body
|
56
|
-
content_blocks = data['content'] || []
|
57
|
-
|
58
|
-
text_content = extract_text_content(content_blocks)
|
59
|
-
tool_use = find_tool_use(content_blocks)
|
60
|
-
|
61
|
-
build_message(data, text_content, tool_use)
|
62
|
-
end
|
63
|
-
|
64
|
-
def extract_text_content(blocks)
|
65
|
-
text_blocks = blocks.select { |c| c['type'] == 'text' }
|
66
|
-
text_blocks.map { |c| c['text'] }.join('')
|
67
|
-
end
|
68
|
-
|
69
|
-
def find_tool_use(blocks)
|
70
|
-
blocks.find { |c| c['type'] == 'tool_use' }
|
71
|
-
end
|
72
|
-
|
73
|
-
def build_message(data, content, tool_use)
|
74
|
-
Message.new(
|
75
|
-
role: :assistant,
|
76
|
-
content: content,
|
77
|
-
tool_calls: parse_tool_calls(tool_use),
|
78
|
-
input_tokens: data.dig('usage', 'input_tokens'),
|
79
|
-
output_tokens: data.dig('usage', 'output_tokens'),
|
80
|
-
model_id: data['model']
|
81
|
-
)
|
82
|
-
end
|
83
|
-
|
84
|
-
def parse_list_models_response(response) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
85
|
-
(response.body['data'] || []).map do |model|
|
86
|
-
ModelInfo.new(
|
87
|
-
id: model['id'],
|
88
|
-
created_at: Time.parse(model['created_at']),
|
89
|
-
display_name: model['display_name'],
|
90
|
-
provider: slug,
|
91
|
-
type: capabilities.model_type(model['id']),
|
92
|
-
family: capabilities.model_family(model['id']),
|
93
|
-
context_window: capabilities.determine_context_window(model['id']),
|
94
|
-
max_tokens: capabilities.determine_max_tokens(model['id']),
|
95
|
-
supports_vision: capabilities.supports_vision?(model['id']),
|
96
|
-
supports_functions: capabilities.supports_functions?(model['id']),
|
97
|
-
supports_json_mode: capabilities.supports_json_mode?(model['id']),
|
98
|
-
input_price_per_million: capabilities.get_input_price(model['id']),
|
99
|
-
output_price_per_million: capabilities.get_output_price(model['id'])
|
100
|
-
)
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
def handle_stream(&block)
|
105
|
-
to_json_stream do |data|
|
106
|
-
block.call(build_chunk(data))
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
def build_chunk(data)
|
111
|
-
Chunk.new(
|
112
|
-
role: :assistant,
|
113
|
-
model_id: extract_model_id(data),
|
114
|
-
content: data.dig('delta', 'text'),
|
115
|
-
input_tokens: extract_input_tokens(data),
|
116
|
-
output_tokens: extract_output_tokens(data),
|
117
|
-
tool_calls: extract_tool_calls(data)
|
118
|
-
)
|
119
|
-
end
|
120
|
-
|
121
|
-
def extract_model_id(data)
|
122
|
-
data.dig('message', 'model')
|
123
|
-
end
|
124
|
-
|
125
|
-
def extract_input_tokens(data)
|
126
|
-
data.dig('message', 'usage', 'input_tokens')
|
127
|
-
end
|
128
|
-
|
129
|
-
def extract_output_tokens(data)
|
130
|
-
data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens')
|
131
|
-
end
|
132
|
-
|
133
|
-
def extract_tool_calls(data)
|
134
|
-
if json_delta?(data)
|
135
|
-
{ nil => ToolCall.new(id: nil, name: nil, arguments: data.dig('delta', 'partial_json')) }
|
136
|
-
else
|
137
|
-
parse_tool_calls(data['content_block'])
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def json_delta?(data)
|
142
|
-
data['type'] == 'content_block_delta' && data.dig('delta', 'type') == 'input_json_delta'
|
143
|
-
end
|
144
|
-
|
145
|
-
def parse_tool_calls(content_block)
|
146
|
-
return nil unless content_block && content_block['type'] == 'tool_use'
|
147
|
-
|
148
|
-
{
|
149
|
-
content_block['id'] => ToolCall.new(
|
150
|
-
id: content_block['id'],
|
151
|
-
name: content_block['name'],
|
152
|
-
arguments: content_block['input']
|
153
|
-
)
|
154
|
-
}
|
155
|
-
end
|
156
|
-
|
157
|
-
def function_for(tool)
|
158
|
-
{
|
159
|
-
name: tool.name,
|
160
|
-
description: tool.description,
|
161
|
-
input_schema: {
|
162
|
-
type: 'object',
|
163
|
-
properties: clean_parameters(tool.parameters),
|
164
|
-
required: required_parameters(tool.parameters)
|
165
|
-
}
|
166
|
-
}
|
167
|
-
end
|
168
|
-
|
169
|
-
def format_messages(messages)
|
170
|
-
messages.map { |msg| format_message(msg) }
|
171
|
-
end
|
172
|
-
|
173
|
-
def format_message(msg)
|
174
|
-
if msg.tool_call?
|
175
|
-
format_tool_call(msg)
|
176
|
-
elsif msg.tool_result?
|
177
|
-
format_tool_result(msg)
|
178
|
-
else
|
179
|
-
format_basic_message(msg)
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
def format_tool_call(msg)
|
184
|
-
tool_call = msg.tool_calls.values.first
|
185
|
-
|
186
|
-
{
|
187
|
-
role: 'assistant',
|
188
|
-
content: [
|
189
|
-
format_text_block(msg.content),
|
190
|
-
format_tool_use_block(tool_call)
|
191
|
-
]
|
192
|
-
}
|
193
|
-
end
|
194
|
-
|
195
|
-
def format_tool_result(msg)
|
196
|
-
{
|
197
|
-
role: 'user',
|
198
|
-
content: [format_tool_result_block(msg)]
|
199
|
-
}
|
200
|
-
end
|
201
|
-
|
202
|
-
def format_basic_message(msg)
|
203
|
-
{
|
204
|
-
role: convert_role(msg.role),
|
205
|
-
content: msg.content
|
206
|
-
}
|
207
|
-
end
|
208
|
-
|
209
|
-
def format_text_block(content)
|
210
|
-
{
|
211
|
-
type: 'text',
|
212
|
-
text: content
|
213
|
-
}
|
214
|
-
end
|
215
|
-
|
216
|
-
def format_tool_use_block(tool_call)
|
217
|
-
{
|
218
|
-
type: 'tool_use',
|
219
|
-
id: tool_call.id,
|
220
|
-
name: tool_call.name,
|
221
|
-
input: tool_call.arguments
|
222
|
-
}
|
223
|
-
end
|
224
|
-
|
225
|
-
def format_tool_result_block(msg)
|
226
|
-
{
|
227
|
-
type: 'tool_result',
|
228
|
-
tool_use_id: msg.tool_call_id,
|
229
|
-
content: msg.content
|
230
|
-
}
|
231
|
-
end
|
232
|
-
|
233
|
-
def convert_role(role)
|
234
|
-
case role
|
235
|
-
when :tool then 'user'
|
236
|
-
when :user then 'user'
|
237
|
-
else 'assistant'
|
238
|
-
end
|
239
|
-
end
|
240
|
-
|
241
|
-
def clean_parameters(parameters)
|
242
|
-
parameters.transform_values do |param|
|
243
|
-
{
|
244
|
-
type: param.type,
|
245
|
-
description: param.description
|
246
|
-
}.compact
|
247
|
-
end
|
248
|
-
end
|
249
|
-
|
250
|
-
def required_parameters(parameters)
|
251
|
-
parameters.select { |_, param| param.required }.keys
|
252
|
-
end
|
253
27
|
end
|
254
28
|
end
|
255
29
|
end
|