Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/ruby_llm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ def chat(...)
Chat.new(...)
end

def response(...)
Response.new(...)
end

def embed(...)
Embedding.embed(...)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_llm/active_record/acts_as.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def ask(message, with: nil, &)
alias say ask

def complete(...)
to_llm.complete(...)
to_llm.process(...)
rescue RubyLLM::Error => e
if @message&.persisted? && @message.content.blank?
RubyLLM.logger.debug "RubyLLM: API call failed, destroying message: #{@message.id}"
Expand Down
153 changes: 4 additions & 149 deletions lib/ruby_llm/chat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,162 +8,17 @@ module RubyLLM
# chat = RubyLLM.chat
# chat.ask "What's the best way to learn Ruby?"
# chat.ask "Can you elaborate on that?"
class Chat
include Enumerable
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm moving most of the Chat implementation to a base class Conversation so both Chat and Conversation inherit from the logic


attr_reader :model, :messages, :tools, :params

def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil)
if assume_model_exists && !provider
raise ArgumentError, 'Provider must be specified if assume_model_exists is true'
end

@context = context
@config = context&.config || RubyLLM.config
model_id = model || @config.default_model
with_model(model_id, provider: provider, assume_exists: assume_model_exists)
@temperature = 0.7
@messages = []
@tools = {}
@params = {}
@on = {
new_message: nil,
end_message: nil
}
end

def ask(message = nil, with: nil, &)
add_message role: :user, content: Content.new(message, with)
complete(&)
end

alias say ask

def with_instructions(instructions, replace: false)
@messages = @messages.reject { |msg| msg.role == :system } if replace

add_message role: :system, content: instructions
self
end

def with_tool(tool)
unless @model.supports_functions?
raise UnsupportedFunctionsError, "Model #{@model.id} doesn't support function calling"
end

tool_instance = tool.is_a?(Class) ? tool.new : tool
@tools[tool_instance.name.to_sym] = tool_instance
self
end

def with_tools(*tools)
tools.each { |tool| with_tool tool }
self
end

def with_model(model_id, provider: nil, assume_exists: false)
@model, @provider = Models.resolve(model_id, provider:, assume_exists:)
@connection = @context ? @context.connection_for(@provider) : @provider.connection(@config)
self
end

def with_temperature(temperature)
@temperature = temperature
self
end

def with_context(context)
@context = context
@config = context.config
with_model(@model.id, provider: @provider.slug, assume_exists: true)
self
end

def with_params(**params)
@params = params
self
end

def on_new_message(&block)
@on[:new_message] = block
self
end

def on_end_message(&block)
@on[:end_message] = block
self
end

def each(&)
messages.each(&)
end

def complete(&)
response = @provider.complete(
class Chat < Conversation
def get_response(&)
@provider.complete(
messages,
tools: @tools,
temperature: @temperature,
model: @model.id,
connection: @connection,
params: @params,
&wrap_streaming_block(&)
&
)

@on[:new_message]&.call unless block_given?
add_message response
@on[:end_message]&.call(response)

if response.tool_call?
handle_tool_calls(response, &)
else
response
end
end

def add_message(message_or_attributes)
message = message_or_attributes.is_a?(Message) ? message_or_attributes : Message.new(message_or_attributes)
messages << message
message
end

def reset_messages!
@messages.clear
end

private

def wrap_streaming_block(&block)
return nil unless block_given?

first_chunk_received = false

proc do |chunk|
# Create message on first content chunk
unless first_chunk_received
first_chunk_received = true
@on[:new_message]&.call
end

# Pass chunk to user's block
block.call chunk
end
end

def handle_tool_calls(response, &)
response.tool_calls.each_value do |tool_call|
@on[:new_message]&.call
result = execute_tool tool_call
message = add_message role: :tool, content: result.to_s, tool_call_id: tool_call.id
@on[:end_message]&.call(message)
end

complete(&)
end

def execute_tool(tool_call)
tool = tools[tool_call.name.to_sym]
args = tool_call.arguments
tool.call(args)
end
end
end
160 changes: 160 additions & 0 deletions lib/ruby_llm/conversation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# frozen_string_literal: true

module RubyLLM
# Represents a base class for conversations with an AI model. Handles tool integrations.
#
# Example:
# conversation = RubyLLM.conversation
# conversation.ask "What's the best way to learn Ruby?"
# conversation.ask "Can you elaborate on that?"
class Conversation
include Enumerable

attr_reader :model, :messages, :tools, :params

def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil)
if assume_model_exists && !provider
raise ArgumentError, 'Provider must be specified if assume_model_exists is true'
end

@context = context
@config = context&.config || RubyLLM.config
model_id = model || @config.default_model
with_model(model_id, provider: provider, assume_exists: assume_model_exists)
@temperature = 0.7
@messages = []
@tools = {}
@params = {}
@on = {
new_message: nil,
end_message: nil
}
end

def ask(message = nil, with: nil, &)
add_message role: :user, content: Content.new(message, with)
process(&)
end

alias say ask

def with_instructions(instructions, replace: false)
@messages = @messages.reject { |msg| msg.role == :system } if replace

add_message role: :system, content: instructions
self
end

def with_tool(tool)
unless @model.supports_functions?
raise UnsupportedFunctionsError, "Model #{@model.id} doesn't support function calling"
end

tool_instance = tool.is_a?(Class) ? tool.new : tool
@tools[tool_instance.name.to_sym] = tool_instance
self
end

def with_tools(*tools)
tools.each { |tool| with_tool tool }
self
end

def with_model(model_id, provider: nil, assume_exists: false)
@model, @provider = Models.resolve(model_id, provider:, assume_exists:)
@connection = @context ? @context.connection_for(@provider) : @provider.connection(@config)
self
end

def with_temperature(temperature)
@temperature = temperature
self
end

def with_context(context)
@context = context
@config = context.config
with_model(@model.id, provider: @provider.slug, assume_exists: true)
self
end

def with_params(**params)
@params = params
self
end

def on_new_message(&block)
@on[:new_message] = block
self
end

def on_end_message(&block)
@on[:end_message] = block
self
end

def each(&)
messages.each(&)
end

def process(&)
response = get_response(&wrap_streaming_block(&))

@on[:new_message]&.call unless block_given?
add_message response
@on[:end_message]&.call(response)

if response.tool_call?
handle_tool_calls(response, &)
else
response
end
end

def add_message(message_or_attributes)
message = message_or_attributes.is_a?(Message) ? message_or_attributes : Message.new(message_or_attributes)
messages << message
message
end

def reset_messages!
@messages.clear
end

private

def wrap_streaming_block(&block)
return nil unless block_given?

first_chunk_received = false

proc do |chunk|
# Create message on first content chunk
unless first_chunk_received
first_chunk_received = true
@on[:new_message]&.call
end

# Pass chunk to user's block
block.call chunk
end
end

def handle_tool_calls(response, &)
response.tool_calls.each_value do |tool_call|
@on[:new_message]&.call
result = execute_tool tool_call
message = add_message role: :tool, content: result.to_s, tool_call_id: tool_call.id
@on[:end_message]&.call(message)
end

process(&)
end

def execute_tool(tool_call)
tool = tools[tool_call.name.to_sym]
args = tool_call.arguments
tool.call(args)
end
end
end
Loading