ruby_llm 1.3.0 → 1.3.2beta1

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: 8a1f6385e98e9396c7f9d8021bd2e574f5f8c8aa1cc05c7f10311e2059101017
4
- data.tar.gz: e7617280adc17488f9dbc810b424b22ee21f36c0684f0a5f37762dd290d6c0de
3
+ metadata.gz: b069393971ae461a63487ba8a13d1f81a791d96fc0f537fceb02de88d9e690d0
4
+ data.tar.gz: c34f0884ffd9cffd3c0311ebbe8d7fbacc151b17512984b999dace24fa0bfe74
5
5
  SHA512:
6
- metadata.gz: 80bcef1cb440519b7eb146bd4064f8eb5a40cf1fd5e309ac582c6be8a3aee0a33b244e8c4961bc69ac763b2665adab918238a0d8787b41c2fb6e5e7fd6343ae3
7
- data.tar.gz: 3d95b550d6d879ad29e20bb328cb09d7191b4055180cba95e237ddab2d1173845463d62e407c777d6b76425958531fb6e7abc664939478c9e89b41565971ad2b
6
+ metadata.gz: 5ba83dd19febb3070e0f127eb6dc87fd92664d8afae2f4024240588fdc8624dabdcc5c2b089e5147d22601c3505b966f4bed7383fbf6e36ce8dff5bea568ef87
7
+ data.tar.gz: b7940d1a6b8bf72ba08eb9d63440505935022d372ffdab6aac51599fb23a9e68af49085cb965488f7306d28143f7b39ec9c0138a93ba52ad3ca3ac574e03a442
data/README.md CHANGED
@@ -37,7 +37,7 @@
37
37
 
38
38
  Every AI provider comes with its own client library, its own response format, its own conventions for streaming, and its own way of handling errors. Want to use multiple providers? Prepare to juggle incompatible APIs and bloated dependencies.
39
39
 
40
- RubyLLM fixes all that. One beautiful API for everything. One consistent format. Minimal dependencies — just Faraday and Zeitwerk. Because working with AI should be a joy, not a chore.
40
+ RubyLLM fixes all that. One beautiful API for everything. One consistent format. Minimal dependencies — just Faraday, Zeitwerk, and Marcel. Because working with AI should be a joy, not a chore.
41
41
 
42
42
  ## What makes it great
43
43
 
@@ -46,14 +46,14 @@ RubyLLM fixes all that. One beautiful API for everything. One consistent format.
46
46
  chat = RubyLLM.chat
47
47
  chat.ask "What's the best way to learn Ruby?"
48
48
 
49
- # Analyze images
50
- chat.ask "What's in this image?", with: { image: "ruby_conf.jpg" }
49
+ # Analyze images, audio, documents, and text files
50
+ chat.ask "What's in this image?", with: "ruby_conf.jpg"
51
+ chat.ask "Describe this meeting", with: "meeting.wav"
52
+ chat.ask "Summarize this document", with: "contract.pdf"
53
+ chat.ask "Explain this code", with: "app.rb"
51
54
 
52
- # Analyze audio recordings
53
- chat.ask "Describe this meeting", with: { audio: "meeting.wav" }
54
-
55
- # Analyze documents
56
- chat.ask "Summarize this document", with: { pdf: "contract.pdf" }
55
+ # Multiple files at once - types automatically detected
56
+ chat.ask "Analyze these files", with: ["diagram.png", "report.pdf", "notes.txt"]
57
57
 
58
58
  # Stream responses in real-time
59
59
  chat.ask "Tell me a story about a Ruby programmer" do |chunk|
@@ -90,7 +90,7 @@ chat.with_tool(Weather).ask "What's the weather in Berlin? (52.5200, 13.4050)"
90
90
  * 💬 **Unified Chat:** Converse with models from OpenAI, Anthropic, Gemini, Bedrock, OpenRouter, DeepSeek, Ollama, or any OpenAI-compatible API using `RubyLLM.chat`.
91
91
  * 👁️ **Vision:** Analyze images within chats.
92
92
  * 🔊 **Audio:** Transcribe and understand audio content.
93
- * 📄 **PDF Analysis:** Extract information and summarize PDF documents.
93
+ * 📄 **Document Analysis:** Extract information from PDFs, text files, and other documents.
94
94
  * 🖼️ **Image Generation:** Create images with `RubyLLM.paint`.
95
95
  * 📊 **Embeddings:** Generate text embeddings for vector search with `RubyLLM.embed`.
96
96
  * 🔧 **Tools (Function Calling):** Let AI models call your Ruby code using `RubyLLM::Tool`.
@@ -143,6 +143,10 @@ end
143
143
  # Now interacting with a Chat record persists the conversation:
144
144
  chat_record = Chat.create!(model_id: "gpt-4.1-nano")
145
145
  chat_record.ask("Explain Active Record callbacks.") # User & Assistant messages saved
146
+
147
+ # Works seamlessly with file attachments - types automatically detected
148
+ chat_record.ask("What's in this file?", with: "report.pdf")
149
+ chat_record.ask("Analyze these", with: ["image.jpg", "data.csv", "notes.txt"])
146
150
  ```
147
151
  Check the [Rails Integration Guide](https://rubyllm.com/guides/rails) for more.
148
152
 
@@ -18,6 +18,7 @@ module RubyLLM
18
18
  has_many :messages,
19
19
  -> { order(created_at: :asc) },
20
20
  class_name: @message_class,
21
+ inverse_of: :chat,
21
22
  dependent: :destroy
22
23
 
23
24
  delegate :add_message, to: :to_llm
@@ -65,13 +65,21 @@
65
65
  "gemini": "gemini-2.0-flash-lite-001",
66
66
  "openrouter": "google/gemini-2.0-flash-lite-001"
67
67
  },
68
- "gemini-2.5-flash-preview-05-20": {
69
- "gemini": "gemini-2.5-flash-preview-05-20",
70
- "openrouter": "google/gemini-2.5-flash-preview-05-20"
68
+ "gemini-2.5-flash": {
69
+ "gemini": "gemini-2.5-flash",
70
+ "openrouter": "google/gemini-2.5-flash"
71
71
  },
72
- "gemini-2.5-pro-exp-03-25": {
73
- "gemini": "gemini-2.5-pro-exp-03-25",
74
- "openrouter": "google/gemini-2.5-pro-exp-03-25"
72
+ "gemini-2.5-flash-lite-preview-06-17": {
73
+ "gemini": "gemini-2.5-flash-lite-preview-06-17",
74
+ "openrouter": "google/gemini-2.5-flash-lite-preview-06-17"
75
+ },
76
+ "gemini-2.5-pro": {
77
+ "gemini": "gemini-2.5-pro",
78
+ "openrouter": "google/gemini-2.5-pro"
79
+ },
80
+ "gemini-2.5-pro-preview-05-06": {
81
+ "gemini": "gemini-2.5-pro-preview-05-06",
82
+ "openrouter": "google/gemini-2.5-pro-preview-05-06"
75
83
  },
76
84
  "gemma-3-12b-it": {
77
85
  "gemini": "gemma-3-12b-it",
@@ -85,18 +93,14 @@
85
93
  "gemini": "gemma-3-4b-it",
86
94
  "openrouter": "google/gemma-3-4b-it"
87
95
  },
96
+ "gemma-3n-e4b-it": {
97
+ "gemini": "gemma-3n-e4b-it",
98
+ "openrouter": "google/gemma-3n-e4b-it"
99
+ },
88
100
  "gpt-3.5-turbo": {
89
101
  "openai": "gpt-3.5-turbo",
90
102
  "openrouter": "openai/gpt-3.5-turbo"
91
103
  },
92
- "gpt-3.5-turbo-0125": {
93
- "openai": "gpt-3.5-turbo-0125",
94
- "openrouter": "openai/gpt-3.5-turbo-0125"
95
- },
96
- "gpt-3.5-turbo-1106": {
97
- "openai": "gpt-3.5-turbo-1106",
98
- "openrouter": "openai/gpt-3.5-turbo-1106"
99
- },
100
104
  "gpt-3.5-turbo-16k": {
101
105
  "openai": "gpt-3.5-turbo-16k",
102
106
  "openrouter": "openai/gpt-3.5-turbo-16k"
@@ -133,10 +137,6 @@
133
137
  "openai": "gpt-4.1-nano",
134
138
  "openrouter": "openai/gpt-4.1-nano"
135
139
  },
136
- "gpt-4.5-preview": {
137
- "openai": "gpt-4.5-preview",
138
- "openrouter": "openai/gpt-4.5-preview"
139
- },
140
140
  "gpt-4o": {
141
141
  "openai": "gpt-4o",
142
142
  "openrouter": "openai/gpt-4o"
@@ -201,6 +201,10 @@
201
201
  "openai": "o3-mini",
202
202
  "openrouter": "openai/o3-mini"
203
203
  },
204
+ "o3-pro": {
205
+ "openai": "o3-pro",
206
+ "openrouter": "openai/o3-pro"
207
+ },
204
208
  "o4-mini": {
205
209
  "openai": "o4-mini",
206
210
  "openrouter": "openai/o4-mini"
@@ -99,7 +99,7 @@ module RubyLLM
99
99
  def determine_mime_type
100
100
  return @mime_type = active_storage_content_type if active_storage? && active_storage_content_type.present?
101
101
 
102
- @mime_type = RubyLLM::MimeType.for(@source, name: @filename)
102
+ @mime_type = RubyLLM::MimeType.for(url? ? nil : @source, name: @filename)
103
103
  @mime_type = RubyLLM::MimeType.for(content) if @mime_type == 'application/octet-stream'
104
104
  @mime_type = 'audio/wav' if @mime_type == 'audio/x-wav' # Normalize WAV type
105
105
  end
data/lib/ruby_llm/chat.rb CHANGED
@@ -93,18 +93,19 @@ module RubyLLM
93
93
  end
94
94
 
95
95
  def complete(&)
96
- @on[:new_message]&.call
97
96
  response = @provider.complete(
98
97
  messages,
99
98
  tools: @tools,
100
99
  temperature: @temperature,
101
100
  model: @model.id,
102
101
  connection: @connection,
103
- &
102
+ &wrap_streaming_block(&)
104
103
  )
105
- @on[:end_message]&.call(response)
106
104
 
105
+ @on[:new_message]&.call unless block_given?
107
106
  add_message response
107
+ @on[:end_message]&.call(response)
108
+
108
109
  if response.tool_call?
109
110
  handle_tool_calls(response, &)
110
111
  else
@@ -124,11 +125,28 @@ module RubyLLM
124
125
 
125
126
  private
126
127
 
128
+ def wrap_streaming_block(&block)
129
+ return nil unless block_given?
130
+
131
+ first_chunk_received = false
132
+
133
+ proc do |chunk|
134
+ # Create message on first content chunk
135
+ unless first_chunk_received
136
+ first_chunk_received = true
137
+ @on[:new_message]&.call
138
+ end
139
+
140
+ # Pass chunk to user's block
141
+ block.call chunk
142
+ end
143
+ end
144
+
127
145
  def handle_tool_calls(response, &)
128
146
  response.tool_calls.each_value do |tool_call|
129
147
  @on[:new_message]&.call
130
148
  result = execute_tool tool_call
131
- message = add_tool_result tool_call.id, result
149
+ message = add_message role: :tool, content: result.to_s, tool_call_id: tool_call.id
132
150
  @on[:end_message]&.call(message)
133
151
  end
134
152
 
@@ -140,13 +158,5 @@ module RubyLLM
140
158
  args = tool_call.arguments
141
159
  tool.call(args)
142
160
  end
143
-
144
- def add_tool_result(tool_use_id, result)
145
- add_message(
146
- role: :tool,
147
- content: result.is_a?(Hash) && result[:error] ? result[:error] : result.to_s,
148
- tool_call_id: tool_use_id
149
- )
150
- end
151
161
  end
152
162
  end
@@ -36,6 +36,7 @@ module RubyLLM
36
36
  :retry_interval_randomness,
37
37
  :http_proxy,
38
38
  # Logging configuration
39
+ :logger,
39
40
  :log_file,
40
41
  :log_level,
41
42
  :log_assume_model_exists