ruby_llm 0.1.0.pre11 → 0.1.0.pre13

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed2ff977d45e5ac9a14d93b81740516f14f6c4ef33140e0163f6011efa14d697
4
- data.tar.gz: a51b24bd4e88512d9abb3d4db33551bd391da97fa8161f8f6d1b23e415f427d1
3
+ metadata.gz: 325d22d60cd9f0a4157670442830fe2473e8e46879ee672d0e56207f6b3b8e72
4
+ data.tar.gz: 355be22334837f40ac717974058d4cceb4d46dd13e36a808c11410a9c4624dc0
5
5
  SHA512:
6
- metadata.gz: ff245f6f9378b51481a096b63a4c5cb688cf80e3b5d218a9e0ff5ca74a0ac09c720a8647df8f8fe9603ee10d9b3b61e4704638d81051c1378694bff47f272e7f
7
- data.tar.gz: 8fae7e20c68130cabc9d769a82b5b3a94d765b2cb53804a2df7833c402da37c6c781bcaaf477cb41a0b610026bfa57fee2670029793526c4375dee3731bc4bf3
6
+ metadata.gz: a7ea97dbe171c7f62df442134b613c84ec277dffcdf1e86fafeed14a95c7ba2545b950d454153606e205f13ffd5a79a9e6e8158d1f053be51a915bbfb03d4806
7
+ data.tar.gz: bb20ec1351e7597550283b7b7b2b1dff50a26ceba1b123b5fabd8636150f2fae5eb935dac51153448895b7782cdbb85575c25a671064928a8c83465489bea6b9
data/.rubocop.yml CHANGED
@@ -1,3 +1,6 @@
1
1
  require:
2
2
  - rubocop-rake
3
- - rubocop-rspec
3
+ - rubocop-rspec
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 3.1
data/README.md CHANGED
@@ -79,37 +79,61 @@ puts "Conversation used #{last_message.input_tokens} input tokens and #{last_mes
79
79
 
80
80
  ## Using Tools
81
81
 
82
- Give Claude some Ruby superpowers by letting it call your code. Simply create a tool class:
82
+ Give your AI assistants access to your Ruby code by creating tool classes that do one thing well:
83
83
 
84
84
  ```ruby
85
- class CalculatorTool < RubyLLM::Tool
85
+ class Calculator < RubyLLM::Tool
86
86
  description "Performs arithmetic calculations"
87
87
 
88
88
  param :expression,
89
89
  type: :string,
90
- required: true,
91
90
  desc: "A mathematical expression to evaluate (e.g. '2 + 2')"
92
91
 
93
92
  def execute(expression:)
94
93
  eval(expression).to_s
95
94
  end
96
95
  end
96
+
97
+ class Search < RubyLLM::Tool
98
+ description "Searches documents by similarity"
99
+
100
+ param :query,
101
+ desc: "The search query"
102
+
103
+ param :limit,
104
+ type: :integer,
105
+ desc: "Number of results to return",
106
+ required: false
107
+
108
+ def initialize(repo:)
109
+ @repo = repo
110
+ end
111
+
112
+ def execute(query:, limit: 5)
113
+ @repo.similarity_search(query, limit:)
114
+ end
115
+ end
97
116
  ```
98
117
 
99
- Then use it in your conversations:
118
+ Then use them in your conversations:
100
119
 
101
120
  ```ruby
102
- chat = RubyLLM.chat.with_tool(CalculatorTool)
121
+ # Simple tools just work
122
+ chat = RubyLLM.chat.with_tool Calculator
123
+
124
+ # Tools with dependencies are just regular Ruby objects
125
+ search = Search.new repo: Document
126
+ chat.with_tools search, CalculatorTool
127
+
128
+ # Need more control? Configure as needed
129
+ chat.with_model('claude-3-5-sonnet-20241022')
130
+ .with_temperature(0.9)
103
131
 
104
- # Claude will automatically use the calculator when appropriate
105
132
  chat.ask "What's 2+2?"
106
133
  # => "Let me calculate that for you. The result is 4."
107
134
 
108
- chat.ask "If I have 3 apples and multiply them by 5, how many do I have?"
109
- # => "Let me help you calculate that. 3 × 5 = 15, so you would have 15 apples."
110
-
111
- # Add multiple tools
112
- chat.with_tools(CalculatorTool, WeatherTool, DatabaseTool)
135
+ chat.ask "Find documents about Ruby performance"
136
+ # => "I found these relevant documents about Ruby performance..."
113
137
  ```
114
138
 
115
139
  Tools let you seamlessly integrate your Ruby code with AI capabilities. The model will automatically decide when to use your tools and handle the results appropriately.
data/lib/ruby_llm/chat.rb CHANGED
@@ -10,6 +10,7 @@ module RubyLLM
10
10
  model_id = model || RubyLLM.config.default_model
11
11
  @model = Models.find model_id
12
12
  @provider = Models.provider_for model_id
13
+ @temperature = 0.7
13
14
  @messages = []
14
15
  @tools = {}
15
16
 
@@ -26,7 +27,7 @@ module RubyLLM
26
27
  def with_tool(tool)
27
28
  raise Error, "Model #{@model.id} doesn't support function calling" unless @model.supports_functions
28
29
 
29
- tool_instance = tool.is_a?(Class) ? tool.to_tool : tool
30
+ tool_instance = tool.is_a?(Class) ? tool.new : tool
30
31
  @tools[tool_instance.name.to_sym] = tool_instance
31
32
  self
32
33
  end
@@ -36,6 +37,17 @@ module RubyLLM
36
37
  self
37
38
  end
38
39
 
40
+ def with_model(model_id)
41
+ @model = Models.find model_id
42
+ @provider = Models.provider_for model_id
43
+ self
44
+ end
45
+
46
+ def with_temperature(temperature)
47
+ @temperature = temperature
48
+ self
49
+ end
50
+
39
51
  def each(&block)
40
52
  messages.each(&block)
41
53
  end
@@ -43,19 +55,17 @@ module RubyLLM
43
55
  private
44
56
 
45
57
  def complete(&block)
46
- response = @provider.complete(messages, tools: @tools, model: @model.id, &block)
58
+ response = @provider.complete messages, tools: @tools, temperature: @temperature, model: @model.id, &block
47
59
 
60
+ add_message response
48
61
  if response.tool_call?
49
62
  handle_tool_calls response, &block
50
63
  else
51
- add_message response
52
64
  response
53
65
  end
54
66
  end
55
67
 
56
68
  def handle_tool_calls(response, &block)
57
- add_message response
58
-
59
69
  response.tool_calls.each_value do |tool_call|
60
70
  result = execute_tool tool_call
61
71
  add_tool_result tool_call.id, result if result
@@ -26,7 +26,7 @@ module RubyLLM
26
26
  !tool_call_id.nil? && !tool_call_id.empty?
27
27
  end
28
28
 
29
- def tool_result
29
+ def tool_results
30
30
  content if tool_result?
31
31
  end
32
32
 
@@ -7,8 +7,8 @@ module RubyLLM
7
7
  end
8
8
 
9
9
  module InstanceMethods
10
- def complete(messages, tools: [], model: nil, &block)
11
- payload = build_payload messages, tools, model: model, stream: block_given?
10
+ def complete(messages, tools:, temperature:, model:, &block)
11
+ payload = build_payload messages, tools: tools, temperature: temperature, model: model, stream: block_given?
12
12
 
13
13
  if block_given?
14
14
  stream_response payload, &block
@@ -26,7 +26,7 @@ module RubyLLM
26
26
  '/v1/models'
27
27
  end
28
28
 
29
- def build_payload(messages, tools, model:, temperature: 0.7, stream: false)
29
+ def build_payload(messages, tools:, temperature:, model:, stream: false)
30
30
  {
31
31
  model: model,
32
32
  messages: format_messages(messages),
@@ -34,7 +34,7 @@ module RubyLLM
34
34
  stream: stream,
35
35
  max_tokens: RubyLLM.models.find(model).max_tokens
36
36
  }.tap do |payload|
37
- payload[:tools] = tools.map { |t| function_for(t) } if tools.any?
37
+ payload[:tools] = tools.values.map { |t| function_for(t) } if tools.any?
38
38
  end
39
39
  end
40
40
 
@@ -42,26 +42,26 @@ module RubyLLM
42
42
  data = response.body
43
43
  content_blocks = data['content'] || []
44
44
 
45
- text_content = content_blocks.find { |c| c['type'] == 'text' }&.fetch('text', '')
45
+ text_blocks = content_blocks.select { |c| c['type'] == 'text' }
46
+ text_content = text_blocks.map { |c| c['text'] }.join('')
47
+
46
48
  tool_use = content_blocks.find { |c| c['type'] == 'tool_use' }
47
49
 
48
50
  if tool_use
49
51
  Message.new(
50
52
  role: :assistant,
51
53
  content: text_content,
52
- tool_calls: [
53
- {
54
- name: tool_use['name'],
55
- arguments: JSON.generate(tool_use['input'] || {})
56
- }
57
- ]
54
+ tool_calls: parse_tool_calls(tool_use),
55
+ input_tokens: data.dig('usage', 'input_tokens'),
56
+ output_tokens: data.dig('usage', 'output_tokens'),
57
+ model_id: data['model']
58
58
  )
59
59
  else
60
60
  Message.new(
61
61
  role: :assistant,
62
62
  content: text_content,
63
- input_tokens: data['usage']['input_tokens'],
64
- output_tokens: data['usage']['output_tokens'],
63
+ input_tokens: data.dig('usage', 'input_tokens'),
64
+ output_tokens: data.dig('usage', 'output_tokens'),
65
65
  model_id: data['model']
66
66
  )
67
67
  end
@@ -90,18 +90,44 @@ module RubyLLM
90
90
 
91
91
  def handle_stream(&block)
92
92
  to_json_stream do |data|
93
- block.call(
94
- Chunk.new(
95
- role: :assistant,
96
- model_id: data.dig('message', 'model'),
97
- content: data.dig('delta', 'text'),
98
- input_tokens: data.dig('message', 'usage', 'input_tokens'),
99
- output_tokens: data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens')
93
+ if data['type'] == 'content_block_delta' && data.dig('delta', 'type') == 'input_json_delta'
94
+ block.call(
95
+ Chunk.new(
96
+ role: :assistant,
97
+ model_id: data.dig('message', 'model'),
98
+ content: data.dig('delta', 'text'),
99
+ input_tokens: data.dig('message', 'usage', 'input_tokens'),
100
+ output_tokens: data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens'),
101
+ tool_calls: { nil => ToolCall.new(id: nil, name: nil, arguments: data.dig('delta', 'partial_json')) }
102
+ )
100
103
  )
101
- )
104
+ else
105
+ block.call(
106
+ Chunk.new(
107
+ role: :assistant,
108
+ model_id: data.dig('message', 'model'),
109
+ content: data.dig('delta', 'text'),
110
+ input_tokens: data.dig('message', 'usage', 'input_tokens'),
111
+ output_tokens: data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens'),
112
+ tool_calls: parse_tool_calls(data['content_block'])
113
+ )
114
+ )
115
+ end
102
116
  end
103
117
  end
104
118
 
119
+ def parse_tool_calls(content_block)
120
+ return nil unless content_block && content_block['type'] == 'tool_use'
121
+
122
+ {
123
+ content_block['id'] => ToolCall.new(
124
+ id: content_block['id'],
125
+ name: content_block['name'],
126
+ arguments: content_block['input']
127
+ )
128
+ }
129
+ end
130
+
105
131
  def function_for(tool)
106
132
  {
107
133
  name: tool.name,
@@ -116,16 +142,31 @@ module RubyLLM
116
142
 
117
143
  def format_messages(messages)
118
144
  messages.map do |msg|
119
- if msg.tool_results
145
+ if msg.tool_call?
120
146
  {
121
- role: convert_role(msg.role),
147
+ role: 'assistant',
148
+ content: [
149
+ {
150
+ type: 'text',
151
+ text: msg.content
152
+ },
153
+ {
154
+ type: 'tool_use',
155
+ id: msg.tool_calls.values.first.id,
156
+ name: msg.tool_calls.values.first.name,
157
+ input: msg.tool_calls.values.first.arguments
158
+ }
159
+ ]
160
+ }
161
+ elsif msg.tool_result?
162
+ {
163
+ role: 'user',
122
164
  content: [
123
165
  {
124
166
  type: 'tool_result',
125
- tool_use_id: msg.tool_results[:tool_use_id],
126
- content: msg.tool_results[:content],
127
- is_error: msg.tool_results[:is_error]
128
- }.compact
167
+ tool_use_id: msg.tool_call_id,
168
+ content: msg.content
169
+ }
129
170
  ]
130
171
  }
131
172
  else
@@ -139,19 +180,23 @@ module RubyLLM
139
180
 
140
181
  def convert_role(role)
141
182
  case role
183
+ when :tool then 'user'
142
184
  when :user then 'user'
143
185
  else 'assistant'
144
186
  end
145
187
  end
146
188
 
147
189
  def clean_parameters(parameters)
148
- parameters.transform_values do |props|
149
- props.except(:required)
190
+ parameters.transform_values do |param|
191
+ {
192
+ type: param.type,
193
+ description: param.description
194
+ }.compact
150
195
  end
151
196
  end
152
197
 
153
198
  def required_parameters(parameters)
154
- parameters.select { |_, props| props[:required] }.keys
199
+ parameters.select { |_, param| param.required }.keys
155
200
  end
156
201
  end
157
202
  end
@@ -25,7 +25,7 @@ module RubyLLM
25
25
  '/v1/models'
26
26
  end
27
27
 
28
- def build_payload(messages, tools, model:, temperature: 0.7, stream: false)
28
+ def build_payload(messages, tools:, temperature:, model:, stream: false)
29
29
  {
30
30
  model: model,
31
31
  messages: format_messages(messages),
data/lib/ruby_llm/tool.rb CHANGED
@@ -1,7 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # lib/ruby_llm/tool.rb
4
3
  module RubyLLM
4
+ class Parameter
5
+ attr_reader :name, :type, :description, :required
6
+
7
+ def initialize(name, type: 'string', desc: nil, required: true)
8
+ @name = name
9
+ @type = type
10
+ @description = desc
11
+ @required = required
12
+ end
13
+ end
14
+
5
15
  class Tool
6
16
  class << self
7
17
  def description(text = nil)
@@ -10,51 +20,34 @@ module RubyLLM
10
20
  @description = text
11
21
  end
12
22
 
13
- def param(name, type:, desc: nil, required: true)
14
- param = Parameter.new(
15
- name,
16
- type: type.to_s,
17
- description: desc,
18
- required: required
19
- )
20
- parameters[name] = param
23
+ def param(name, **options)
24
+ parameters[name] = Parameter.new(name, **options)
21
25
  end
22
26
 
23
27
  def parameters
24
28
  @parameters ||= {}
25
29
  end
30
+ end
26
31
 
27
- def name
28
- super
32
+ def name
33
+ self.class.name
29
34
  .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
30
35
  .gsub(/([a-z\d])([A-Z])/, '\1_\2')
31
36
  .downcase
32
37
  .delete_suffix('_tool')
33
- end
34
-
35
- def to_tool
36
- tool_instance = new
37
-
38
- def tool_instance.name
39
- self.class.name
40
- end
41
-
42
- def tool_instance.description
43
- self.class.description
44
- end
38
+ end
45
39
 
46
- def tool_instance.parameters
47
- self.class.parameters
48
- end
40
+ def description
41
+ self.class.description
42
+ end
49
43
 
50
- tool_instance
51
- end
44
+ def parameters
45
+ self.class.parameters
52
46
  end
53
47
 
54
48
  def call(args)
55
49
  RubyLLM.logger.debug "Tool #{name} called with: #{args.inspect}"
56
- symbolized_args = args.transform_keys(&:to_sym)
57
- result = execute(**symbolized_args)
50
+ result = execute(**args.transform_keys(&:to_sym))
58
51
  RubyLLM.logger.debug "Tool #{name} returned: #{result.inspect}"
59
52
  result
60
53
  rescue StandardError => e
@@ -62,28 +55,8 @@ module RubyLLM
62
55
  { error: e.message }
63
56
  end
64
57
 
65
- def execute(args)
58
+ def execute(...)
66
59
  raise NotImplementedError, 'Subclasses must implement #execute'
67
60
  end
68
61
  end
69
-
70
- # Using the existing Parameter class from Tool.rb
71
- class Parameter
72
- attr_reader :name, :type, :description, :required
73
-
74
- def initialize(name, type: 'string', description: nil, required: true)
75
- @name = name
76
- @type = type
77
- @description = description
78
- @required = required
79
- end
80
-
81
- def to_h
82
- {
83
- type: type,
84
- description: description,
85
- required: required
86
- }.compact
87
- end
88
- end
89
62
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- VERSION = '0.1.0.pre11'
4
+ VERSION = '0.1.0.pre13'
5
5
  end
data/ruby_llm.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
15
15
  ' works.'
16
16
  spec.homepage = 'https://github.com/crmne/ruby_llm'
17
17
  spec.license = 'MIT'
18
- spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
18
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.1.0')
19
19
 
20
20
  spec.metadata['homepage_uri'] = spec.homepage
21
21
  spec.metadata['source_code_uri'] = spec.homepage
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre11
4
+ version: 0.1.0.pre13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-02-03 00:00:00.000000000 Z
11
+ date: 2025-02-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: event_stream_parser
@@ -380,7 +380,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
380
380
  requirements:
381
381
  - - ">="
382
382
  - !ruby/object:Gem::Version
383
- version: 2.7.0
383
+ version: 3.1.0
384
384
  required_rubygems_version: !ruby/object:Gem::Requirement
385
385
  requirements:
386
386
  - - ">="