Skip to content

Commit 4a682ab

Browse files
committed
rebase on main
2 parents 433ec40 + aa64332 commit 4a682ab

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+22072
-11021
lines changed

.rubocop.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,15 @@ AllCops:
77
TargetRubyVersion: 3.1
88
Exclude:
99
- docs/**/*
10-
- vendor/**/*
10+
- vendor/**/*
11+
12+
Metrics/ClassLength:
13+
Enabled: false
14+
Metrics/AbcSize:
15+
Enabled: false
16+
Metrics/CyclomaticComplexity:
17+
Enabled: false
18+
Metrics/MethodLength:
19+
Enabled: false
20+
Metrics/ModuleLength:
21+
Enabled: false

docs/configuration.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ After reading this guide, you will know:
2727
* How to customize connection timeouts and retries.
2828
* How to connect to custom endpoints (like Azure OpenAI).
2929
* How to use temporary, scoped configurations with `RubyLLM.context`.
30+
* How to configure the logging location.
3031

3132
## Global Configuration (`RubyLLM.configure`)
3233

@@ -75,6 +76,10 @@ RubyLLM.configure do |config|
7576
config.retry_interval = 0.1 # Initial delay in seconds (default: 0.1)
7677
config.retry_backoff_factor = 2 # Multiplier for subsequent retries (default: 2)
7778
config.retry_interval_randomness = 0.5 # Jitter factor (default: 0.5)
79+
80+
# --- Logging Settings ---
81+
config.log_file = '/logs/ruby_llm.log'
82+
config.level = :debug # debug level can also be set to debug by setting RUBYLLM_DEBUG envar to true
7883
end
7984
```
8085

@@ -134,6 +139,31 @@ Fine-tune how RubyLLM handles HTTP connections and retries.
134139

135140
Adjust these based on network conditions and provider reliability.
136141

142+
## Logging Settings
143+
144+
RubyLLM provides flexible logging configuration to help you monitor and debug API interactions. You can configure both the log file location and the logging level.
145+
146+
```ruby
147+
RubyLLM.configure do |config|
148+
# --- Logging Settings ---
149+
config.log_file = '/logs/ruby_llm.log' # Path to log file (default: nil, logs to STDOUT)
150+
config.level = :debug # Log level (:debug, :info, :warn)
151+
end
152+
```
153+
154+
### Log File Configuration
155+
156+
* `config.log_file`: Specifies the path where logs should be written. If not set, logs will be written to STDOUT.
157+
* The log file will be created if it doesn't exist, and logs will be appended to it.
158+
159+
### Log Levels
160+
161+
* `:debug`: Most verbose level, includes detailed request/response information as provided by the faraday client
162+
* `:info`: General operational information
163+
* `:warn`: Warning messages for non-critical issues that may need attention
164+
165+
You can also set the debug level by setting the `RUBYLLM_DEBUG` environment variable to `true`.
166+
137167
## Scoped Configuration with Contexts
138168
{: .d-inline-block }
139169

@@ -178,4 +208,5 @@ default_response = default_chat.ask("Query using global production settings...")
178208
* **Isolation:** Modifying configuration within a context block does **not** affect the global `RubyLLM.config`.
179209
* **Thread Safety:** Each context is independent, making them safe for use across different threads.
180210

181-
Contexts provide a clean and safe mechanism for managing diverse configuration needs within a single application.
211+
Contexts provide a clean and safe mechanism for managing diverse configuration needs within a single application.
212+

docs/guides/available-models.md

Lines changed: 1863 additions & 1117 deletions
Large diffs are not rendered by default.

docs/guides/models.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ claude3_sonnet_family = RubyLLM.models.by_family('claude3_sonnet')
9090

9191
# Chain filters and use Enumerable methods
9292
openai_vision_models = RubyLLM.models.by_provider(:openai)
93-
.select(&:supports_vision)
93+
.select(&:supports_vision?)
9494

9595
puts "Found #{openai_vision_models.count} OpenAI vision models."
9696
```

lib/ruby_llm.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ def config
6868

6969
def logger
7070
@logger ||= Logger.new(
71-
$stdout,
71+
config.log_file,
7272
progname: 'RubyLLM',
73-
level: ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO
73+
level: config.log_level
7474
)
7575
end
7676
end

lib/ruby_llm/active_record/acts_as.rb

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,46 @@ def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall')
2424
to: :to_llm
2525
end
2626

27-
def acts_as_message(chat_class: 'Chat', tool_call_class: 'ToolCall', touch_chat: false) # rubocop:disable Metrics/MethodLength
27+
def acts_as_message(chat_class: 'Chat', tool_call_class: 'ToolCall', **options)
2828
include MessageMethods
2929

3030
@chat_class = chat_class.to_s
31+
@chat_foreign_key = options[:chat_foreign_key] || @chat_class.foreign_key
3132
@tool_call_class = tool_call_class.to_s
33+
@tool_call_foreign_key = options[:tool_call_foreign_key] || @tool_call_class.foreign_key
3234

33-
belongs_to :chat, class_name: @chat_class, touch: touch_chat
34-
has_many :tool_calls, class_name: @tool_call_class, dependent: :destroy
35+
belongs_to :chat,
36+
class_name: @chat_class,
37+
foreign_key: @chat_foreign_key,
38+
inverse_of: :messages,
39+
touch: options[:touch_chat]
40+
41+
has_many :tool_calls,
42+
class_name: @tool_call_class,
43+
dependent: :destroy
3544

3645
belongs_to :parent_tool_call,
3746
class_name: @tool_call_class,
38-
foreign_key: 'tool_call_id',
47+
foreign_key: @tool_call_foreign_key,
3948
optional: true,
4049
inverse_of: :result
4150

4251
delegate :tool_call?, :tool_result?, :tool_results, to: :to_llm
4352
end
4453

45-
def acts_as_tool_call(message_class: 'Message')
54+
def acts_as_tool_call(message_class: 'Message', **options)
4655
@message_class = message_class.to_s
56+
@message_foreign_key = options[:message_foreign_key] || @message_class.foreign_key
57+
@result_foreign_key = options[:result_foreign_key] || 'id'
4758

48-
belongs_to :message, class_name: @message_class
59+
belongs_to :message,
60+
class_name: @message_class,
61+
foreign_key: @message_foreign_key,
62+
inverse_of: :tool_calls
4963

5064
has_one :result,
5165
class_name: @message_class,
52-
foreign_key: 'tool_call_id',
66+
foreign_key: @result_foreign_key,
5367
inverse_of: :parent_tool_call,
5468
dependent: :nullify
5569
end
@@ -67,6 +81,7 @@ module ChatMethods
6781

6882
def to_llm
6983
@chat ||= RubyLLM.chat(model: model_id)
84+
@chat.reset_messages!
7085

7186
# Load existing messages into chat
7287
messages.each do |msg|
@@ -150,22 +165,23 @@ def persist_new_message
150165
)
151166
end
152167

153-
def persist_message_completion(message) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
168+
def persist_message_completion(message)
154169
return unless message
155170

156171
if message.tool_call_id
157172
tool_call_id = self.class.tool_call_class.constantize.find_by(tool_call_id: message.tool_call_id).id
158173
end
159174

160175
transaction do
161-
@message.update!(
176+
@message.update(
162177
role: message.role,
163178
content: message.content,
164179
model_id: message.model_id,
165-
tool_call_id: tool_call_id,
166180
input_tokens: message.input_tokens,
167181
output_tokens: message.output_tokens
168182
)
183+
@message.write_attribute(@message.class.tool_call_foreign_key, tool_call_id) if tool_call_id
184+
@message.save!
169185
persist_tool_calls(message.tool_calls) if message.tool_calls.present?
170186
end
171187
end
@@ -184,6 +200,11 @@ def persist_tool_calls(tool_calls)
184200
module MessageMethods
185201
extend ActiveSupport::Concern
186202

203+
class_methods do
204+
attr_reader :chat_class, :tool_call_class
205+
attr_reader :chat_foreign_key, :tool_call_foreign_key
206+
end
207+
187208
def to_llm
188209
RubyLLM::Message.new(
189210
role: role.to_sym,

lib/ruby_llm/chat.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ module RubyLLM
88
# chat = RubyLLM.chat
99
# chat.ask "What's the best way to learn Ruby?"
1010
# chat.ask "Can you elaborate on that?"
11-
class Chat # rubocop:disable Metrics/ClassLength
11+
class Chat
1212
include Enumerable
1313

1414
attr_reader :model, :messages, :tools
1515

16-
def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil) # rubocop:disable Metrics/MethodLength
16+
def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil)
1717
if assume_model_exists && !provider
1818
raise ArgumentError, 'Provider must be specified if assume_model_exists is true'
1919
end
@@ -46,7 +46,7 @@ def with_instructions(instructions, replace: false)
4646
end
4747

4848
def with_tool(tool)
49-
unless @model.supports_functions
49+
unless @model.supports_functions?
5050
raise UnsupportedFunctionsError, "Model #{@model.id} doesn't support function calling"
5151
end
5252

@@ -84,7 +84,7 @@ def each(&)
8484
messages.each(&)
8585
end
8686

87-
def complete(&) # rubocop:disable Metrics/MethodLength
87+
def complete(&)
8888
@on[:new_message]&.call
8989
response = @provider.complete(
9090
messages,
@@ -110,6 +110,10 @@ def add_message(message_or_attributes)
110110
message
111111
end
112112

113+
def reset_messages!
114+
@messages.clear
115+
end
116+
113117
private
114118

115119
def handle_tool_calls(response, &)

lib/ruby_llm/configuration.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ class Configuration
3333
:max_retries,
3434
:retry_interval,
3535
:retry_backoff_factor,
36-
:retry_interval_randomness
36+
:retry_interval_randomness,
37+
# Logging configuration
38+
:log_file,
39+
:log_level
3740

3841
def initialize
3942
# Connection configuration
@@ -47,9 +50,13 @@ def initialize
4750
@default_model = 'gpt-4.1-nano'
4851
@default_embedding_model = 'text-embedding-3-small'
4952
@default_image_model = 'dall-e-3'
53+
54+
# Logging configuration
55+
@log_file = $stdout
56+
@log_level = ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO
5057
end
5158

52-
def inspect # rubocop:disable Metrics/MethodLength
59+
def inspect
5360
redacted = lambda do |name, value|
5461
if name.match?(/_key|_secret|_token$/)
5562
value.nil? ? 'nil' : '[FILTERED]'

lib/ruby_llm/connection.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def setup_middleware(faraday)
6565
faraday.use :llm_errors, provider: @provider
6666
end
6767

68-
def retry_exceptions # rubocop:disable Metrics/MethodLength
68+
def retry_exceptions
6969
[
7070
Errno::ETIMEDOUT,
7171
Timeout::Error,

lib/ruby_llm/embedding.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@ def initialize(vectors:, model:, input_tokens: 0)
1212
@input_tokens = input_tokens
1313
end
1414

15-
def self.embed(text, # rubocop:disable Metrics/ParameterLists,Metrics/CyclomaticComplexity
15+
def self.embed(text, # rubocop:disable Metrics/ParameterLists
1616
model: nil,
1717
provider: nil,
1818
assume_model_exists: false,
1919
context: nil,
2020
dimensions: nil)
2121
config = context&.config || RubyLLM.config
22-
model, provider = Models.resolve(model, provider: provider, assume_exists: assume_model_exists) if model
23-
model_id = model&.id || config.default_embedding_model
22+
model ||= config.default_embedding_model
23+
model, provider = Models.resolve(model, provider: provider, assume_exists: assume_model_exists)
24+
model_id = model.id
2425

2526
provider = Provider.for(model_id) if provider.nil?
2627
connection = context ? context.connection_for(provider) : provider.connection(config)

0 commit comments

Comments
 (0)