From 254e9c920bb082065c9652b74065174df282111f Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Fri, 11 Apr 2025 01:22:38 +0100 Subject: [PATCH 1/6] Feature flag tool calls --- lib/ruby_llm/active_record/acts_as.rb | 106 +++++++++++++++----------- lib/ruby_llm/configuration.rb | 6 +- 2 files changed, 68 insertions(+), 44 deletions(-) diff --git a/lib/ruby_llm/active_record/acts_as.rb b/lib/ruby_llm/active_record/acts_as.rb index 678a3eea4..779dece62 100644 --- a/lib/ruby_llm/active_record/acts_as.rb +++ b/lib/ruby_llm/active_record/acts_as.rb @@ -25,22 +25,24 @@ def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall') to: :to_llm end - def acts_as_message(chat_class: 'Chat', tool_call_class: 'ToolCall', touch_chat: false) # rubocop:disable Metrics/MethodLength + def acts_as_message(chat_class: 'Chat', chat_foreign_key: 'chat_id', tool_call_class: 'ToolCall') # rubocop:disable Metrics/MethodLength include MessageMethods @chat_class = chat_class.to_s - @tool_call_class = tool_call_class.to_s + belongs_to :chat, class_name: @chat_class, foreign_key: chat_foreign_key - belongs_to :chat, class_name: @chat_class, touch: touch_chat - has_many :tool_calls, class_name: @tool_call_class, dependent: :destroy + if RubyLLM.config.use_tool_calls + @tool_call_class = tool_call_class.to_s + has_many :tool_calls, class_name: @tool_call_class, dependent: :destroy - belongs_to :parent_tool_call, - class_name: @tool_call_class, - foreign_key: 'tool_call_id', - optional: true, - inverse_of: :result + belongs_to :parent_tool_call, + class_name: @tool_call_class, + foreign_key: 'tool_call_id', + optional: true, + inverse_of: :result - delegate :tool_call?, :tool_result?, :tool_results, to: :to_llm + delegate :tool_call?, :tool_result?, :tool_results, to: :to_llm + end end def acts_as_tool_call(message_class: 'Message') @@ -94,14 +96,16 @@ def with_instructions(instructions, replace: false) self end - def with_tool(tool) - to_llm.with_tool(tool) - self - end + if RubyLLM.config.use_tool_calls + def with_tool(tool) + to_llm.with_tool(tool) + self + end - def with_tools(*tools) - to_llm.with_tools(*tools) - self + def with_tools(*tools) + to_llm.with_tools(*tools) + self + end end def with_model(model_id, provider: nil) @@ -144,8 +148,10 @@ def persist_new_message def persist_message_completion(message) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength return unless message - if message.tool_call_id - tool_call_id = self.class.tool_call_class.constantize.find_by(tool_call_id: message.tool_call_id).id + if RubyLLM.config.use_tool_calls + if message.tool_call_id + tool_call_id = self.class.tool_call_class.constantize.find_by(tool_call_id: message.tool_call_id).id + end end transaction do @@ -153,19 +159,23 @@ def persist_message_completion(message) # rubocop:disable Metrics/AbcSize,Metric role: message.role, content: message.content, model_id: message.model_id, - tool_call_id: tool_call_id, input_tokens: message.input_tokens, - output_tokens: message.output_tokens - ) - persist_tool_calls(message.tool_calls) if message.tool_calls.present? + output_tokens: message.output_tokens, + **{ tool_call_id: tool_call_id }.compact, + ) + if RubyLLM.config.use_tool_calls + persist_tool_calls(message.tool_calls) if message.tool_calls.present? + end end end - def persist_tool_calls(tool_calls) - tool_calls.each_value do |tool_call| - attributes = tool_call.to_h - attributes[:tool_call_id] = attributes.delete(:id) - @message.tool_calls.create!(**attributes) + if RubyLLM.config.use_tool_calls + def persist_tool_calls(tool_calls) + tool_calls.each_value do |tool_call| + attributes = tool_call.to_h + attributes[:tool_call_id] = attributes.delete(:id) + @message.tool_calls.create!(**attributes) + end end end end @@ -179,29 +189,39 @@ def to_llm RubyLLM::Message.new( role: role.to_sym, content: extract_content, - tool_calls: extract_tool_calls, - tool_call_id: extract_tool_call_id, + **( + if RubyLLM.config.use_tool_calls + { + tool_calls: extract_tool_calls, + tool_call_id: extract_tool_call_id, + } + else + {} + end + ), input_tokens: input_tokens, output_tokens: output_tokens, model_id: model_id ) end - def extract_tool_calls - tool_calls.to_h do |tool_call| - [ - tool_call.tool_call_id, - RubyLLM::ToolCall.new( - id: tool_call.tool_call_id, - name: tool_call.name, - arguments: tool_call.arguments - ) - ] + if RubyLLM.config.use_tool_calls + def extract_tool_calls + tool_calls.to_h do |tool_call| + [ + tool_call.tool_call_id, + RubyLLM::ToolCall.new( + id: tool_call.tool_call_id, + name: tool_call.name, + arguments: tool_call.arguments + ) + ] + end end - end - def extract_tool_call_id - parent_tool_call&.tool_call_id + def extract_tool_call_id + parent_tool_call&.tool_call_id + end end def extract_content diff --git a/lib/ruby_llm/configuration.rb b/lib/ruby_llm/configuration.rb index f20185a48..aa4899ce4 100644 --- a/lib/ruby_llm/configuration.rb +++ b/lib/ruby_llm/configuration.rb @@ -29,7 +29,9 @@ class Configuration :max_retries, :retry_interval, :retry_backoff_factor, - :retry_interval_randomness + :retry_interval_randomness, + # Feature flags + :use_tool_calls def initialize # Connection configuration @@ -43,6 +45,8 @@ def initialize @default_model = 'gpt-4.1-nano' @default_embedding_model = 'text-embedding-3-small' @default_image_model = 'dall-e-3' + + @use_tool_calls = true end end end From b2a395656e054cea44eecb0178e525fea37a0eeb Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sat, 19 Apr 2025 00:50:54 +0100 Subject: [PATCH 2/6] Refactor to isolate tool call functionality as mixins --- lib/ruby_llm/active_record/acts_as.rb | 162 ++++++++++++++------------ 1 file changed, 89 insertions(+), 73 deletions(-) diff --git a/lib/ruby_llm/active_record/acts_as.rb b/lib/ruby_llm/active_record/acts_as.rb index 779dece62..999e39adf 100644 --- a/lib/ruby_llm/active_record/acts_as.rb +++ b/lib/ruby_llm/active_record/acts_as.rb @@ -12,8 +12,9 @@ module ActsAs def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall') include ChatMethods + acts_as_chat_with_tool_call(tool_call_class: tool_call_class) if RubyLLM.config.use_tool_calls + @message_class = message_class.to_s - @tool_call_class = tool_call_class.to_s has_many :messages, -> { order(created_at: :asc) }, @@ -25,24 +26,13 @@ def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall') to: :to_llm end - def acts_as_message(chat_class: 'Chat', chat_foreign_key: 'chat_id', tool_call_class: 'ToolCall') # rubocop:disable Metrics/MethodLength + def acts_as_message(chat_class: 'Chat', chat_foreign_key: 'chat_id', tool_call_class: 'ToolCall', touch_chat: false) include MessageMethods - @chat_class = chat_class.to_s - belongs_to :chat, class_name: @chat_class, foreign_key: chat_foreign_key - - if RubyLLM.config.use_tool_calls - @tool_call_class = tool_call_class.to_s - has_many :tool_calls, class_name: @tool_call_class, dependent: :destroy + acts_as_message_with_tool_call(tool_call_class: tool_call_class) if RubyLLM.config.use_tool_calls - belongs_to :parent_tool_call, - class_name: @tool_call_class, - foreign_key: 'tool_call_id', - optional: true, - inverse_of: :result - - delegate :tool_call?, :tool_result?, :tool_results, to: :to_llm - end + @chat_class = chat_class.to_s + belongs_to :chat, class_name: @chat_class, touch: touch_chat, foreign_key: chat_foreign_key end def acts_as_tool_call(message_class: 'Message') @@ -56,6 +46,29 @@ def acts_as_tool_call(message_class: 'Message') inverse_of: :parent_tool_call, dependent: :nullify end + + private + + def acts_as_chat_with_tool_call(tool_call_class:) + include ChatToolCallMethods + + @tool_call_class = tool_call_class.to_s + end + + def acts_as_message_with_tool_call(tool_call_class:) + include MessageToolCallMethods + + @tool_call_class = tool_call_class.to_s + has_many :tool_calls, class_name: @tool_call_class, dependent: :destroy + + belongs_to :parent_tool_call, + class_name: @tool_call_class, + foreign_key: 'tool_call_id', + optional: true, + inverse_of: :result + + delegate :tool_call?, :tool_result?, :tool_results, to: :to_llm + end end end @@ -96,18 +109,6 @@ def with_instructions(instructions, replace: false) self end - if RubyLLM.config.use_tool_calls - def with_tool(tool) - to_llm.with_tool(tool) - self - end - - def with_tools(*tools) - to_llm.with_tools(*tools) - self - end - end - def with_model(model_id, provider: nil) to_llm.with_model(model_id, provider: provider) self @@ -148,12 +149,6 @@ def persist_new_message def persist_message_completion(message) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength return unless message - if RubyLLM.config.use_tool_calls - if message.tool_call_id - tool_call_id = self.class.tool_call_class.constantize.find_by(tool_call_id: message.tool_call_id).id - end - end - transaction do @message.update!( role: message.role, @@ -161,21 +156,44 @@ def persist_message_completion(message) # rubocop:disable Metrics/AbcSize,Metric model_id: message.model_id, input_tokens: message.input_tokens, output_tokens: message.output_tokens, - **{ tool_call_id: tool_call_id }.compact, - ) - if RubyLLM.config.use_tool_calls - persist_tool_calls(message.tool_calls) if message.tool_calls.present? - end + ) end end + end + + module ChatToolCallMethods + include ChatMethods - if RubyLLM.config.use_tool_calls - def persist_tool_calls(tool_calls) - tool_calls.each_value do |tool_call| - attributes = tool_call.to_h - attributes[:tool_call_id] = attributes.delete(:id) - @message.tool_calls.create!(**attributes) - end + def with_tool(tool) + to_llm.with_tool(tool) + self + end + + def with_tools(*tools) + to_llm.with_tools(*tools) + self + end + + private + + def persist_message_completion(message) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength + return unless message&.tool_call_id + + transaction do + super + + tool_call_id = self.class.tool_call_class.constantize.find_by(tool_call_id: message.tool_call_id).id + @message.update!(tool_call_id: tool_call_id) + + persist_tool_calls(message.tool_calls) if message.tool_calls.present? + end + end + + def persist_tool_calls(tool_calls) + tool_calls.each_value do |tool_call| + attributes = tool_call.to_h + attributes[:tool_call_id] = attributes.delete(:id) + @message.tool_calls.create!(**attributes) end end end @@ -189,43 +207,41 @@ def to_llm RubyLLM::Message.new( role: role.to_sym, content: extract_content, - **( - if RubyLLM.config.use_tool_calls - { - tool_calls: extract_tool_calls, - tool_call_id: extract_tool_call_id, - } - else - {} - end - ), input_tokens: input_tokens, output_tokens: output_tokens, model_id: model_id ) end - if RubyLLM.config.use_tool_calls - def extract_tool_calls - tool_calls.to_h do |tool_call| - [ - tool_call.tool_call_id, - RubyLLM::ToolCall.new( - id: tool_call.tool_call_id, - name: tool_call.name, - arguments: tool_call.arguments - ) - ] - end - end + def extract_content + content + end + end + + module MessageToolCallMethods + def to_llm + RubyLLM::Message.new( + **super.to_h, + tool_calls: extract_tool_calls, + tool_call_id: extract_tool_call_id, + ) + end - def extract_tool_call_id - parent_tool_call&.tool_call_id + def extract_tool_calls + tool_calls.to_h do |tool_call| + [ + tool_call.tool_call_id, + RubyLLM::ToolCall.new( + id: tool_call.tool_call_id, + name: tool_call.name, + arguments: tool_call.arguments + ) + ] end end - def extract_content - content + def extract_tool_call_id + parent_tool_call&.tool_call_id end end end From dd74daacae34b2cd4a67de6e7d2d4548b8c49ad4 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sat, 19 Apr 2025 01:12:22 +0100 Subject: [PATCH 3/6] Fix rubocop issues --- lib/ruby_llm/active_record/acts_as.rb | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/ruby_llm/active_record/acts_as.rb b/lib/ruby_llm/active_record/acts_as.rb index 999e39adf..4b9799960 100644 --- a/lib/ruby_llm/active_record/acts_as.rb +++ b/lib/ruby_llm/active_record/acts_as.rb @@ -26,7 +26,12 @@ def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall') to: :to_llm end - def acts_as_message(chat_class: 'Chat', chat_foreign_key: 'chat_id', tool_call_class: 'ToolCall', touch_chat: false) + def acts_as_message( + chat_class: 'Chat', + chat_foreign_key: 'chat_id', + tool_call_class: 'ToolCall', + touch_chat: false + ) include MessageMethods acts_as_message_with_tool_call(tool_call_class: tool_call_class) if RubyLLM.config.use_tool_calls @@ -146,7 +151,7 @@ def persist_new_message ) end - def persist_message_completion(message) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength + def persist_message_completion(message) return unless message transaction do @@ -155,12 +160,13 @@ def persist_message_completion(message) # rubocop:disable Metrics/AbcSize,Metric content: message.content, model_id: message.model_id, input_tokens: message.input_tokens, - output_tokens: message.output_tokens, + output_tokens: message.output_tokens ) end end end + # Methods mixed into chat models to handle tool calls. module ChatToolCallMethods include ChatMethods @@ -176,7 +182,7 @@ def with_tools(*tools) private - def persist_message_completion(message) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength + def persist_message_completion(message) return unless message&.tool_call_id transaction do @@ -218,12 +224,13 @@ def extract_content end end + # Methods mixed into message models to handle tool calls. module MessageToolCallMethods def to_llm RubyLLM::Message.new( **super.to_h, tool_calls: extract_tool_calls, - tool_call_id: extract_tool_call_id, + tool_call_id: extract_tool_call_id ) end From 475a144e5062f69901f248e5dbf0ac211d958833 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sat, 19 Apr 2025 01:20:28 +0100 Subject: [PATCH 4/6] Fix failing tests --- lib/ruby_llm/active_record/acts_as.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/ruby_llm/active_record/acts_as.rb b/lib/ruby_llm/active_record/acts_as.rb index 4b9799960..60115aff2 100644 --- a/lib/ruby_llm/active_record/acts_as.rb +++ b/lib/ruby_llm/active_record/acts_as.rb @@ -183,13 +183,15 @@ def with_tools(*tools) private def persist_message_completion(message) - return unless message&.tool_call_id + return super unless message&.tool_call_id || message&.tool_calls transaction do super - tool_call_id = self.class.tool_call_class.constantize.find_by(tool_call_id: message.tool_call_id).id - @message.update!(tool_call_id: tool_call_id) + if message.tool_call_id + tool_call_id = self.class.tool_call_class.constantize.find_by(tool_call_id: message.tool_call_id).id + @message.update!(tool_call_id: tool_call_id) + end persist_tool_calls(message.tool_calls) if message.tool_calls.present? end From 8014b17d5be4a5c56cd079536ad7a14fd0578602 Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sat, 19 Apr 2025 10:44:45 +0100 Subject: [PATCH 5/6] use_tool_calls -> active_record_use_tool_calls --- lib/ruby_llm/active_record/acts_as.rb | 4 ++-- lib/ruby_llm/configuration.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ruby_llm/active_record/acts_as.rb b/lib/ruby_llm/active_record/acts_as.rb index 60115aff2..3c76eb947 100644 --- a/lib/ruby_llm/active_record/acts_as.rb +++ b/lib/ruby_llm/active_record/acts_as.rb @@ -12,7 +12,7 @@ module ActsAs def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall') include ChatMethods - acts_as_chat_with_tool_call(tool_call_class: tool_call_class) if RubyLLM.config.use_tool_calls + acts_as_chat_with_tool_call(tool_call_class: tool_call_class) if RubyLLM.config.active_record_use_tool_calls @message_class = message_class.to_s @@ -34,7 +34,7 @@ def acts_as_message( ) include MessageMethods - acts_as_message_with_tool_call(tool_call_class: tool_call_class) if RubyLLM.config.use_tool_calls + acts_as_message_with_tool_call(tool_call_class: tool_call_class) if RubyLLM.config.active_record_use_tool_calls @chat_class = chat_class.to_s belongs_to :chat, class_name: @chat_class, touch: touch_chat, foreign_key: chat_foreign_key diff --git a/lib/ruby_llm/configuration.rb b/lib/ruby_llm/configuration.rb index aa4899ce4..f9733dd58 100644 --- a/lib/ruby_llm/configuration.rb +++ b/lib/ruby_llm/configuration.rb @@ -31,7 +31,7 @@ class Configuration :retry_backoff_factor, :retry_interval_randomness, # Feature flags - :use_tool_calls + :active_record_use_tool_calls def initialize # Connection configuration @@ -46,7 +46,7 @@ def initialize @default_embedding_model = 'text-embedding-3-small' @default_image_model = 'dall-e-3' - @use_tool_calls = true + @active_record_use_tool_calls = true end end end From d26ac3733845f6db2290b8f0fafcee3ad7846f6e Mon Sep 17 00:00:00 2001 From: darthrighteous Date: Sat, 19 Apr 2025 10:50:35 +0100 Subject: [PATCH 6/6] Fix line length rubocop violation --- lib/ruby_llm/active_record/acts_as.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/ruby_llm/active_record/acts_as.rb b/lib/ruby_llm/active_record/acts_as.rb index 3c76eb947..248114711 100644 --- a/lib/ruby_llm/active_record/acts_as.rb +++ b/lib/ruby_llm/active_record/acts_as.rb @@ -34,7 +34,9 @@ def acts_as_message( ) include MessageMethods - acts_as_message_with_tool_call(tool_call_class: tool_call_class) if RubyLLM.config.active_record_use_tool_calls + if RubyLLM.config.active_record_use_tool_calls + acts_as_message_with_tool_call(tool_call_class: tool_call_class) + end @chat_class = chat_class.to_s belongs_to :chat, class_name: @chat_class, touch: touch_chat, foreign_key: chat_foreign_key