intelligence 0.6.0 → 0.8.0
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 +4 -4
- data/README.md +576 -0
- data/intelligence.gemspec +2 -1
- data/lib/intelligence/adapter/base.rb +13 -6
- data/lib/intelligence/adapter/class_methods.rb +15 -0
- data/lib/intelligence/adapter/module_methods.rb +41 -0
- data/lib/intelligence/adapter.rb +2 -2
- data/lib/intelligence/adapters/anthropic/adapter.rb +21 -19
- data/lib/intelligence/adapters/anthropic/chat_request_methods.rb +189 -0
- data/lib/intelligence/adapters/anthropic/{chat_methods.rb → chat_response_methods.rb} +13 -137
- data/lib/intelligence/adapters/cerebras.rb +19 -19
- data/lib/intelligence/adapters/generic/adapter.rb +4 -2
- data/lib/intelligence/adapters/generic/chat_request_methods.rb +221 -0
- data/lib/intelligence/adapters/generic/chat_response_methods.rb +234 -0
- data/lib/intelligence/adapters/generic.rb +1 -1
- data/lib/intelligence/adapters/google/adapter.rb +33 -22
- data/lib/intelligence/adapters/google/chat_request_methods.rb +234 -0
- data/lib/intelligence/adapters/google/chat_response_methods.rb +236 -0
- data/lib/intelligence/adapters/groq.rb +29 -49
- data/lib/intelligence/adapters/hyperbolic.rb +13 -39
- data/lib/intelligence/adapters/mistral.rb +21 -42
- data/lib/intelligence/adapters/open_ai/adapter.rb +39 -32
- data/lib/intelligence/adapters/open_ai/chat_request_methods.rb +186 -0
- data/lib/intelligence/adapters/open_ai/chat_response_methods.rb +239 -0
- data/lib/intelligence/adapters/open_ai.rb +1 -1
- data/lib/intelligence/adapters/open_router.rb +18 -18
- data/lib/intelligence/adapters/samba_nova.rb +16 -18
- data/lib/intelligence/adapters/together_ai.rb +25 -23
- data/lib/intelligence/conversation.rb +11 -10
- data/lib/intelligence/message.rb +45 -29
- data/lib/intelligence/message_content/base.rb +2 -9
- data/lib/intelligence/message_content/binary.rb +3 -3
- data/lib/intelligence/message_content/file.rb +3 -3
- data/lib/intelligence/message_content/text.rb +10 -2
- data/lib/intelligence/message_content/tool_call.rb +61 -5
- data/lib/intelligence/message_content/tool_result.rb +11 -6
- data/lib/intelligence/tool.rb +139 -0
- data/lib/intelligence/version.rb +1 -1
- data/lib/intelligence.rb +3 -1
- metadata +31 -13
- data/lib/intelligence/adapter/class_methods/construction.rb +0 -17
- data/lib/intelligence/adapter/module_methods/construction.rb +0 -43
- data/lib/intelligence/adapters/generic/chat_methods.rb +0 -355
- data/lib/intelligence/adapters/google/chat_methods.rb +0 -393
- data/lib/intelligence/adapters/legacy/adapter.rb +0 -11
- data/lib/intelligence/adapters/legacy/chat_methods.rb +0 -54
- data/lib/intelligence/adapters/open_ai/chat_methods.rb +0 -345
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 35084b4f3df27ee21c0a21a759b74bcca9c05c6b47e0311026d6e6587f8661a3
|
4
|
+
data.tar.gz: 76afc7ad3e1f2e3637c82e8613b492680be20962ca5daf263d3c457968512c04
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 643f9acfde921655b5861901f5ea11646d00e9673a852e8546ec112a275d9f4e695934c314c577ca5cf44684ee3ce86b57cb5da9100e67b5c016c3bd90672f14
|
7
|
+
data.tar.gz: 6a9bb70335d3cd9f5b5ef48f1029e8b7997d880a1ebed37168c6d5cae88ec4abb48c2c8ffb95ef63c5b4b82a0e5c00904b0320321c124d8cf24fabb1928e5453
|
data/README.md
ADDED
@@ -0,0 +1,576 @@
|
|
1
|
+
# Intelligence
|
2
|
+
|
3
|
+
Intelligence is a lightweight yet powerful Ruby gem that provides a uniform interface for
|
4
|
+
interacting with large language and vision model APIs across multiple vendors. It allows
|
5
|
+
you to seamlessly integrate with services from OpenAI, Anthropic, Google, Mistral, Cerebras,
|
6
|
+
Groq, Hyperbolic, Samba Nova, Together AI, and others, while maintaining a consistent API
|
7
|
+
across all providers.
|
8
|
+
|
9
|
+
The gem operates with minimal dependencies and doesn't require vendor SDK installation,
|
10
|
+
making it easy to switch between providers or work with multiple providers simultaneously.
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
require 'intelligence'
|
14
|
+
|
15
|
+
adapter = Intelligence::Adapter.build :open_ai do
|
16
|
+
key ENV[ 'OPENAI_API_KEY' ]
|
17
|
+
chat_options do
|
18
|
+
model 'gpt-4o'
|
19
|
+
max_tokens 256
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
request = Intelligence::ChatRequest.new( adapter: adapter )
|
24
|
+
conversation = Intelligence::Conversation.build do
|
25
|
+
system_message do
|
26
|
+
content text: "You are a highly efficient AI assistant. Provide clear, concise responses."
|
27
|
+
end
|
28
|
+
message role: :user do
|
29
|
+
content text: ARGV[ 0 ] || 'Hello!'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
response = request.chat( conversation )
|
34
|
+
|
35
|
+
if response.success?
|
36
|
+
puts response.result.text
|
37
|
+
else
|
38
|
+
puts "Error: " + response.result.error_description
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
## Installation
|
43
|
+
|
44
|
+
Add this line to your application's Gemfile:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
gem 'intelligence'
|
48
|
+
```
|
49
|
+
|
50
|
+
Then execute:
|
51
|
+
|
52
|
+
```bash
|
53
|
+
$ bundle install
|
54
|
+
```
|
55
|
+
|
56
|
+
Or install it directly:
|
57
|
+
|
58
|
+
```bash
|
59
|
+
$ gem install intelligence
|
60
|
+
```
|
61
|
+
|
62
|
+
## Usage
|
63
|
+
|
64
|
+
### Fundamentals
|
65
|
+
|
66
|
+
The core components of Intelligence are adapters, requests and responses. An adapter encapsulates
|
67
|
+
the differences between different API vendors, allowing you to use requests and responses
|
68
|
+
uniformly.
|
69
|
+
|
70
|
+
You retrieve an adapter for a specific vendor, configure it with a key, model and associated
|
71
|
+
parameters and then make a request by calling either the `chat` or `stream` methods.
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
require 'intelligence'
|
75
|
+
|
76
|
+
# configure the adapter with your API key and model settings
|
77
|
+
adapter = Intelligence::Adapter[ :google ].new(
|
78
|
+
key: ENV[ 'GOOGLE_API_KEY' ],
|
79
|
+
chat_options: {
|
80
|
+
model: 'gemini-1.5-flash-002',
|
81
|
+
max_tokens: 256
|
82
|
+
}
|
83
|
+
)
|
84
|
+
|
85
|
+
# create a request instance, passing the adapter
|
86
|
+
request = Intelligence::ChatRequest.new( adapter: adapter )
|
87
|
+
|
88
|
+
# make the request and handle the response
|
89
|
+
response = request.chat( "What is the capital of France?" )
|
90
|
+
|
91
|
+
if response.success?
|
92
|
+
puts response.result.text
|
93
|
+
else
|
94
|
+
puts "Error: #{response.result.error_description}"
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
The `response` object is a `Faraday` response with an added method: `result`. If a response is
|
99
|
+
successful `result` returns a `ChatResult`. If it is not successful it returns a
|
100
|
+
`ChatErrorResult`. You can use the `Faraday` method `success?` to determine if the response is
|
101
|
+
successful.
|
102
|
+
|
103
|
+
### Results
|
104
|
+
|
105
|
+
When you make a request using Intelligence, the response includes a `result` that provides
|
106
|
+
structured access to the model's output.
|
107
|
+
|
108
|
+
- A `ChatResult` contains one or more `choices` (alternate responses from the model). The
|
109
|
+
`choices` method returns an array of `ChatResultChoice` instances. `ChatResult` also
|
110
|
+
includes a `metrics` methods which provides information about token usage for the request.
|
111
|
+
- A `ChatResultChoice` contains a `message` from the assistant and an `end_result` which
|
112
|
+
indicates how the response ended:
|
113
|
+
- `:ended` means the model completed its response normally
|
114
|
+
- `:token_limit_exceeded` means the response hit the token limit ( `max_tokens` )
|
115
|
+
- `:end_sequence_encountered` means the response hit a stop sequence
|
116
|
+
- `:filtered` means the content was filtered by the vendors safety settings or protocols
|
117
|
+
- `:tool_called` means the model is requesting to use a tool
|
118
|
+
- The `Message` in each choice contains one or more content items, typically text but
|
119
|
+
potentially tool calls or other content types.
|
120
|
+
|
121
|
+
While the convenience method `text` used in the previous example is useful for simple cases,
|
122
|
+
you will typically want to work with the full response structure.
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
adapter = Intelligence::Adapter[ :google ].new(
|
126
|
+
key: ENV[ 'GOOGLE_API_KEY' ],
|
127
|
+
chat_options: {
|
128
|
+
model: 'gemini-1.5-flash-002',
|
129
|
+
max_tokens: 256
|
130
|
+
}
|
131
|
+
)
|
132
|
+
|
133
|
+
request = Intelligence::ChatRequest.new( adapter: adapter )
|
134
|
+
response = request.chat( "What are three interesting facts about ruby gemstones?" )
|
135
|
+
|
136
|
+
if response.success?
|
137
|
+
result = response.result # this is a ChatResult
|
138
|
+
|
139
|
+
# iterate through the model's choices
|
140
|
+
result.choices.each do | choice |
|
141
|
+
# check why the response ended
|
142
|
+
puts "Response ended because: #{choice.end_reason}"
|
143
|
+
|
144
|
+
# work with the message
|
145
|
+
message = choice.message
|
146
|
+
puts "Message role: #{message.role}"
|
147
|
+
|
148
|
+
# examine each piece of content
|
149
|
+
message.each_content do | content |
|
150
|
+
puts content.text if content.is_a?( Intelligence::MessageContent::Text )
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# check token usage if metrics are available
|
155
|
+
if result.metrics
|
156
|
+
puts "Input tokens: #{result.metrics.input_tokens}"
|
157
|
+
puts "Output tokens: #{result.metrics.output_tokens}"
|
158
|
+
puts "Total tokens: #{result.metrics.total_tokens}"
|
159
|
+
end
|
160
|
+
else
|
161
|
+
# or alternativelly handle the error result
|
162
|
+
puts "Error: #{response.result.error_description}"
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
The `ChatResult`, `ChatResultChoice` and `Message` all provide the `text` convenience
|
167
|
+
method which return the text.
|
168
|
+
|
169
|
+
### Conversations, Messages, and Content
|
170
|
+
|
171
|
+
Intelligence organizes interactions with models using three main components:
|
172
|
+
|
173
|
+
- **Conversations** are collections of messages that represent a complete interaction with a
|
174
|
+
model. A conversation can include an optional system message that sets the context, a series
|
175
|
+
of back-and-forth messages between the user and assistant and any tools the model may call.
|
176
|
+
|
177
|
+
- **Messages** are individual communications within a conversation. Each message has a role
|
178
|
+
(`:system`, `:user`, or `:assistant`) that identifies its sender and can contain multiple
|
179
|
+
pieces of content.
|
180
|
+
|
181
|
+
- **Content** represents the actual data within a message. This can be text
|
182
|
+
( `MessageContent::Text` ), binary data like images ( `MessageContent::Binary` ), references
|
183
|
+
to files ( `MessageContent::File` ) or tool calls or tool results ( `MessageContent::ToolCall`
|
184
|
+
or `MessageContent::ToolResult` respectivelly ).
|
185
|
+
|
186
|
+
In the previous examples we used a simple string as an argument to `chat`. As a convenience,
|
187
|
+
the `chat` methods builds a coversation for you from a String but, typically, you will construct
|
188
|
+
a coversation instance ( `Coversation` ) and pass that to the chat or stream methods.
|
189
|
+
|
190
|
+
The following example expands the minimal example, building a conversation, messages and content:
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
# create an adapter as before
|
194
|
+
adapter = Intelligence::Adapter[ :google ].new(
|
195
|
+
key: ENV[ 'GOOGLE_API_KEY' ],
|
196
|
+
chat_options: { model: 'gemini-1.5-flash-002', max_tokens: 256 }
|
197
|
+
)
|
198
|
+
|
199
|
+
# create a conversation
|
200
|
+
conversation = Intelligence::Conversation.new
|
201
|
+
|
202
|
+
# add a system message (optional but recommended)
|
203
|
+
system_message = Intelligence::Message.new( :system )
|
204
|
+
system_message << Intelligence::MessageContent::Text.new(
|
205
|
+
text: "You are a helpful coding assistant."
|
206
|
+
)
|
207
|
+
conversation.system_message = system_message
|
208
|
+
|
209
|
+
# add a user message
|
210
|
+
user_message = Intelligence::Message.new( :user )
|
211
|
+
user_message << Intelligence::MessageContent::Text.new(
|
212
|
+
text: "How do I read a file in Ruby?"
|
213
|
+
)
|
214
|
+
conversation.messages << user_message
|
215
|
+
|
216
|
+
# make the request
|
217
|
+
request = Intelligence::ChatRequest.new( adapter: adapter )
|
218
|
+
response = request.chat( conversation )
|
219
|
+
|
220
|
+
if response.success?
|
221
|
+
puts response.result.text
|
222
|
+
else
|
223
|
+
puts "Error: #{response.result.error_description}"
|
224
|
+
end
|
225
|
+
```
|
226
|
+
|
227
|
+
The hierarchical nature of these components makes it easy to organize and access your interaction
|
228
|
+
data. A conversation acts as a container for messages, and each message acts as a container for
|
229
|
+
content items. This structure allows for rich interactions that can include multiple types of
|
230
|
+
content in a single message.
|
231
|
+
|
232
|
+
You can examine the contents of a conversation by iterating through its messages and their content:
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
# iterate through messages
|
236
|
+
conversation.messages.each do |message|
|
237
|
+
puts "Role: #{message.role}"
|
238
|
+
|
239
|
+
# each message can have multiple content items
|
240
|
+
message.each_content do |content|
|
241
|
+
case content
|
242
|
+
when Intelligence::MessageContent::Text
|
243
|
+
puts "Text: #{content.text}"
|
244
|
+
when Intelligence::MessageContent::Binary
|
245
|
+
puts "Binary content of type: #{content.content_type}"
|
246
|
+
when Intelligence::MessageContent::File
|
247
|
+
puts "File reference: #{content.uri}"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# remeber that, alternatively, you can use convenience methods for quick text access
|
253
|
+
puts message.text # combines all text content in a messages with newlines
|
254
|
+
```
|
255
|
+
### Continuing Conversations / Maintaining Context
|
256
|
+
|
257
|
+
To continue a conversation with the model, we can add the model's response and our follow-up
|
258
|
+
message to the conversation:
|
259
|
+
|
260
|
+
```ruby
|
261
|
+
# get the previous response
|
262
|
+
if response.success?
|
263
|
+
# add the assistant's response to our conversation
|
264
|
+
assistant_message = response.result.message
|
265
|
+
conversation.messages << assistant_message
|
266
|
+
|
267
|
+
# add another user message for follow-up
|
268
|
+
follow_up = Intelligence::Message.new( :user )
|
269
|
+
follow_up << Intelligence::MessageContent::Text.new(
|
270
|
+
text: "How do I write to that file?"
|
271
|
+
)
|
272
|
+
conversation.messages << follow_up
|
273
|
+
|
274
|
+
# make another request with the updated conversation
|
275
|
+
response = request.chat( conversation )
|
276
|
+
|
277
|
+
if response.success?
|
278
|
+
puts response.result.text
|
279
|
+
end
|
280
|
+
end
|
281
|
+
```
|
282
|
+
|
283
|
+
This pattern allows you to maintain context across multiple interactions with the model. Each
|
284
|
+
request includes the full conversation history, helping the model provide more contextually
|
285
|
+
relevant responses.
|
286
|
+
|
287
|
+
### Builders
|
288
|
+
|
289
|
+
For more readable configuration, Intelligence provides builder syntax for both adapters and
|
290
|
+
conversations.
|
291
|
+
|
292
|
+
```ruby
|
293
|
+
adapter = Intelligence::Adapter.build! :google do
|
294
|
+
key ENV['GOOGLE_API_KEY']
|
295
|
+
chat_options do
|
296
|
+
model 'gemini-1.5-flash-002'
|
297
|
+
max_tokens 256
|
298
|
+
temperature 0.7
|
299
|
+
end
|
300
|
+
end
|
301
|
+
```
|
302
|
+
|
303
|
+
Similarly, you can use builders to construct conversations with multiple messages.
|
304
|
+
|
305
|
+
```ruby
|
306
|
+
conversation = Intelligence::Conversation.build do
|
307
|
+
system_message do
|
308
|
+
content text: "You are a knowledgeable historian specializing in ancient civilizations."
|
309
|
+
end
|
310
|
+
|
311
|
+
message do
|
312
|
+
role :user
|
313
|
+
content text: "What were the key factors in the fall of the Roman Empire?"
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
request = Intelligence::ChatRequest.new( adapter: adapte r)
|
318
|
+
response = request.chat( conversation )
|
319
|
+
```
|
320
|
+
|
321
|
+
## Binary and File Content
|
322
|
+
|
323
|
+
Intelligence supports vision models through binary and file content types.
|
324
|
+
|
325
|
+
```ruby
|
326
|
+
require 'intelligence'
|
327
|
+
require 'mime-types'
|
328
|
+
|
329
|
+
adapter = Intelligence::Adapter.build! :open_ai do
|
330
|
+
key ENV[ 'OPENAI_API_KEY' ]
|
331
|
+
chat_options do
|
332
|
+
model 'gpt-4-vision-preview'
|
333
|
+
max_tokens 256
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# Using binary content for local images
|
338
|
+
conversation = Intelligence::Conversation.build do
|
339
|
+
message do
|
340
|
+
role :user
|
341
|
+
content text: "What's in this image?"
|
342
|
+
content do
|
343
|
+
type :binary
|
344
|
+
content_type 'image/jpeg'
|
345
|
+
bytes File.binread( 'path/to/image.jpg' )
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
request = Intelligence::ChatRequest.new( adapter: adapter )
|
351
|
+
response = request.chat( conversation )
|
352
|
+
```
|
353
|
+
|
354
|
+
For remote images, you can use file content instead of binary content:
|
355
|
+
|
356
|
+
```ruby
|
357
|
+
conversation = Intelligence::Conversation.build do
|
358
|
+
message do
|
359
|
+
role :user
|
360
|
+
content text: "Analyze this image"
|
361
|
+
content do
|
362
|
+
type :file
|
363
|
+
content_type 'image/jpeg'
|
364
|
+
uri 'https://example.com/image.jpg'
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
```
|
369
|
+
|
370
|
+
## Tools
|
371
|
+
|
372
|
+
Intelligence supports tool/function calling capabilities, allowing models to
|
373
|
+
use defined tools during their response.
|
374
|
+
|
375
|
+
```ruby
|
376
|
+
adapter = Intelligence::Adapter.build! :anthropic do
|
377
|
+
key ENV['ANTHROPIC_API_KEY']
|
378
|
+
chat_options do
|
379
|
+
model 'claude-3-5-sonnet-20240620'
|
380
|
+
max_tokens 1024
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
# Define a tool for getting weather information
|
385
|
+
weather_tool = Intelligence::Tool.build! do
|
386
|
+
name :get_weather
|
387
|
+
description "Get the current weather for a specified location"
|
388
|
+
argument name: :location, required: true, type: 'object' do
|
389
|
+
description "The location for which to retrieve weather information"
|
390
|
+
property name: :city, type: 'string', required: true do
|
391
|
+
description "The city or town name"
|
392
|
+
end
|
393
|
+
property name: :state, type: 'string' do
|
394
|
+
description "The state or province (optional)"
|
395
|
+
end
|
396
|
+
property name: :country, type: 'string' do
|
397
|
+
description "The country (optional)"
|
398
|
+
end
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# Create a conversation with the tool
|
403
|
+
conversation = Intelligence::Conversation.build do
|
404
|
+
system_message do
|
405
|
+
content text: "You can help users check weather conditions."
|
406
|
+
end
|
407
|
+
|
408
|
+
# Add the tool to the conversation
|
409
|
+
tools << weather_tool
|
410
|
+
|
411
|
+
message do
|
412
|
+
role :user
|
413
|
+
content text: "What's the weather like in Paris, France?"
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
request = Intelligence::ChatRequest.new( adapter: adapter )
|
418
|
+
response = request.chat( conversation )
|
419
|
+
|
420
|
+
# Handle tool calls in the response
|
421
|
+
if response.success?
|
422
|
+
result.choices.each do |choice|
|
423
|
+
choice.message.each_content do |content|
|
424
|
+
if content.is_a?(Intelligence::MessageContent::ToolCall)
|
425
|
+
# Process the tool call
|
426
|
+
if content.tool_name == :get_weather
|
427
|
+
# Make actual weather API call here
|
428
|
+
weather_data = fetch_weather(content.tool_parameters[:location])
|
429
|
+
|
430
|
+
# Send tool result back to continue the conversation
|
431
|
+
conversation.messages << Intelligence::Message.build! do
|
432
|
+
role :user
|
433
|
+
content do
|
434
|
+
type :tool_result
|
435
|
+
tool_call_id content.tool_call_id
|
436
|
+
tool_result weather_data.to_json
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
```
|
445
|
+
|
446
|
+
Tools are defined using the `Intelligence::Tool.build!` method, where you specify the tool's
|
447
|
+
name, description, and its argument schema. Arguments can have nested properties with their
|
448
|
+
own descriptions and requirements. Once defined, tools are added to conversations and can be
|
449
|
+
used by the model during its response.
|
450
|
+
|
451
|
+
Note that not all providers support tools, and the specific tool capabilities may vary between
|
452
|
+
providers. Today, OpenAI, Anthropic, Google, Mistral, and Together AI support tools. In general
|
453
|
+
all these providers support tools in an identical manner but as of this writing Google does not
|
454
|
+
support 'complex' tools which take object parameters.
|
455
|
+
|
456
|
+
## Streaming Responses
|
457
|
+
|
458
|
+
The `chat` method, while straightforward in implementation, can be time consuming ( especially
|
459
|
+
when using modern 'reasoning' models like OpenAI O1 ). The alternative is to use the `stream`
|
460
|
+
method which will receive results as these are generated by the model.
|
461
|
+
|
462
|
+
```ruby
|
463
|
+
adapter = Intelligence::Adapter.build! :anthropic do
|
464
|
+
key ENV['ANTHROPIC_API_KEY']
|
465
|
+
chat_options do
|
466
|
+
model 'claude-3-5-sonnet-20240620'
|
467
|
+
max_tokens 1024
|
468
|
+
stream true
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
request = Intelligence::ChatRequest.new(adapter: adapter)
|
473
|
+
|
474
|
+
response = request.stream( "Tell me a story about a robot." ) do | request |
|
475
|
+
request.receive_result do | result |
|
476
|
+
# result is a ChatResult object with partial content
|
477
|
+
print result.text
|
478
|
+
print "\n" if result.choices.first.end_reason
|
479
|
+
end
|
480
|
+
end
|
481
|
+
```
|
482
|
+
|
483
|
+
Notice that in this approach you will receive multiple results ( `ChatResult` instances )
|
484
|
+
each with a fragment of the generation. The result always includes a `message` and will
|
485
|
+
include `contents` as soon as any content is received. The `contents` is always positionally
|
486
|
+
consitent, meaning that if a model is, for example, generating text followed by several
|
487
|
+
tool calls you may receive a single text content initially, then the text content and a tool,
|
488
|
+
and then subsequent tools, even after the text has been completely generated.
|
489
|
+
|
490
|
+
Remember that every `result` contains only a fragment of content and it is possible that
|
491
|
+
any given fragment is completely blank ( that is, it is possible for the content to be
|
492
|
+
present in the result but all of it's fields are nil ).
|
493
|
+
|
494
|
+
While you will likelly want to immediatelly output any generated text but, as practical matter,
|
495
|
+
tool calls are not useful until full generated. To assemble tool calls ( or the text ) from
|
496
|
+
the text fragments you may use the content items `merge` method.
|
497
|
+
|
498
|
+
```ruby
|
499
|
+
request = Intelligence::ChatRequest.new( adapter: adapter )
|
500
|
+
|
501
|
+
contents = []
|
502
|
+
response = request.stream( "Tell me a story about a robot." ) do | request |
|
503
|
+
request.receive_result do | result |
|
504
|
+
choice = result.choices.first
|
505
|
+
contents_fragments = choice.message.contents
|
506
|
+
contents.fill( nil, contents.length..(contents_fragments.length - 1) )
|
507
|
+
|
508
|
+
contents_fragments.each_with_index do | contents_fragment, index |
|
509
|
+
if contents_fragment.is_a?( Intelligence::MessageContent::Text )
|
510
|
+
# here we need the `|| ''` because the text of the fragment may be nil
|
511
|
+
print contents_fragment.text
|
512
|
+
else
|
513
|
+
contents[ index ] = contents[ index ].nil? ?
|
514
|
+
contents_fragment :
|
515
|
+
contents[ index ].merge( contents_fragment )
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
end
|
520
|
+
end
|
521
|
+
```
|
522
|
+
|
523
|
+
In the above example we construct an array to receive the content. As the content fragments
|
524
|
+
are streamed we will immediatelly output generated text but other types of content ( today
|
525
|
+
it could only be instances of `Intelligence::MessageContent::ToolCall' ) are individualy
|
526
|
+
combined in the `contents` array. You can simply iterate though the array and then retrieve
|
527
|
+
and take action for any of the tool calls.
|
528
|
+
|
529
|
+
Note also that the `result` will only include a non-nil `end_reason` as the last ( or one
|
530
|
+
of the last, `result` instances to be received ).
|
531
|
+
|
532
|
+
Finally note that the streamed `result` is always a `ChatResult`, never a `ChatErrorResult`.
|
533
|
+
If an error occurs, the request itself will fail and you will receive this as part of
|
534
|
+
`response.result`.
|
535
|
+
|
536
|
+
## Provider Switching
|
537
|
+
|
538
|
+
One of Intelligence's most powerful features is the ability to easily switch between providers:
|
539
|
+
|
540
|
+
```ruby
|
541
|
+
def create_adapter(provider, api_key, model)
|
542
|
+
Intelligence::Adapter.build! provider do
|
543
|
+
key api_key
|
544
|
+
chat_options do
|
545
|
+
model model
|
546
|
+
max_tokens 256
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
550
|
+
|
551
|
+
# Create adapters for different providers
|
552
|
+
anthropic = create_adapter(:anthropic, ENV['ANTHROPIC_API_KEY'], 'claude-3-5-sonnet-20240620')
|
553
|
+
google = create_adapter(:google, ENV['GOOGLE_API_KEY'], 'gemini-1.5-pro-002')
|
554
|
+
openai = create_adapter(:open_ai, ENV['OPENAI_API_KEY'], 'gpt-4o')
|
555
|
+
|
556
|
+
# Use the same conversation with different providers
|
557
|
+
conversation = Intelligence::Conversation.build do
|
558
|
+
system_message do
|
559
|
+
content text: "You are a helpful assistant."
|
560
|
+
end
|
561
|
+
message do
|
562
|
+
role :user
|
563
|
+
content text: "Explain quantum entanglement in simple terms."
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
[anthropic, google, open_ai].each do |adapter|
|
568
|
+
request = Intelligence::ChatRequest.new(adapter: adapter)
|
569
|
+
response = request.chat(conversation)
|
570
|
+
puts "#{adapter.class.name} response: #{response.result.text}"
|
571
|
+
end
|
572
|
+
```
|
573
|
+
|
574
|
+
## License
|
575
|
+
|
576
|
+
This gem is available as open source under the terms of the MIT License.
|
data/intelligence.gemspec
CHANGED
@@ -37,8 +37,9 @@ Gem::Specification.new do | spec |
|
|
37
37
|
spec.require_paths = [ "lib" ]
|
38
38
|
|
39
39
|
spec.add_runtime_dependency 'faraday', '~> 2.7'
|
40
|
-
spec.add_runtime_dependency '
|
40
|
+
spec.add_runtime_dependency 'dynamicschema', '~> 1.0.0.beta03'
|
41
41
|
spec.add_runtime_dependency 'mime-types', '~> 3.6'
|
42
|
+
spec.add_runtime_dependency 'json-repair', '~> 0.2'
|
42
43
|
|
43
44
|
spec.add_development_dependency 'rspec', '~> 3.4'
|
44
45
|
spec.add_development_dependency 'debug', '~> 1.9'
|
@@ -1,19 +1,26 @@
|
|
1
|
-
require_relative 'class_methods
|
1
|
+
require_relative 'class_methods'
|
2
2
|
|
3
3
|
module Intelligence
|
4
4
|
module Adapter
|
5
5
|
class Base
|
6
|
-
|
7
|
-
extend ClassMethods
|
6
|
+
include DynamicSchema::Definable
|
7
|
+
extend ClassMethods
|
8
8
|
|
9
|
-
def initialize( options =
|
10
|
-
@options =
|
11
|
-
@options = configuration.merge( @options ) if configuration
|
9
|
+
def initialize( options = {}, configuration: nil )
|
10
|
+
@options = build_options( options )
|
11
|
+
@options = configuration.merge( @options ) if configuration&.any?
|
12
12
|
end
|
13
13
|
|
14
14
|
protected
|
15
15
|
attr_reader :options
|
16
16
|
|
17
|
+
private
|
18
|
+
|
19
|
+
def build_options( options )
|
20
|
+
return {} unless options&.any?
|
21
|
+
self.class.builder.build( options )
|
22
|
+
end
|
23
|
+
|
17
24
|
end
|
18
25
|
end
|
19
26
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Intelligence
|
2
|
+
module Adapter
|
3
|
+
module ClassMethods
|
4
|
+
|
5
|
+
def build( options = nil, &block )
|
6
|
+
new( configuration: builder.build( options, &block ) )
|
7
|
+
end
|
8
|
+
|
9
|
+
def build!( options = nil, &block )
|
10
|
+
new( configuration: builder.build( options, &block ) )
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Intelligence
|
2
|
+
module Adapter
|
3
|
+
module ModuleMethods
|
4
|
+
|
5
|
+
def []( adapter_type )
|
6
|
+
|
7
|
+
raise ArgumentError.new( "An adapter type is required but nil was given." ) \
|
8
|
+
if adapter_type.nil?
|
9
|
+
|
10
|
+
class_name = adapter_type.to_s.split( '_' ).map( &:capitalize ).join
|
11
|
+
class_name += "::Adapter"
|
12
|
+
|
13
|
+
adapter_class = Intelligence.const_get( class_name ) rescue nil
|
14
|
+
if adapter_class.nil?
|
15
|
+
adapter_file = File.expand_path( "../../adapters/#{adapter_type}", __FILE__ )
|
16
|
+
unless require adapter_file
|
17
|
+
raise ArgumentError.new(
|
18
|
+
"The Intelligence adapter file #{adapter_file} is missing or does not define #{class_name}."
|
19
|
+
)
|
20
|
+
end
|
21
|
+
adapter_class = Intelligence.const_get( class_name ) rescue nil
|
22
|
+
end
|
23
|
+
|
24
|
+
raise ArgumentError.new( "An unknown Intelligence adapter #{adapter_type} was requested." ) \
|
25
|
+
if adapter_class.nil?
|
26
|
+
|
27
|
+
adapter_class
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def build( adapter_type, attributes = nil, &block )
|
32
|
+
self.[]( adapter_type ).build( attributes, &block )
|
33
|
+
end
|
34
|
+
|
35
|
+
def build!( adapter_type, attributes = nil, &block )
|
36
|
+
self.[]( adapter_type ).build!( attributes, &block )
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/intelligence/adapter.rb
CHANGED