intelligence 0.7.1 → 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 +80 -59
- data/intelligence.gemspec +1 -0
- data/lib/intelligence/adapters/anthropic/chat_response_methods.rb +5 -12
- data/lib/intelligence/adapters/cerebras.rb +2 -2
- 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/google/chat_request_methods.rb +5 -4
- data/lib/intelligence/adapters/groq.rb +2 -21
- data/lib/intelligence/adapters/hyperbolic.rb +0 -26
- data/lib/intelligence/adapters/mistral.rb +3 -24
- data/lib/intelligence/adapters/open_ai/adapter.rb +20 -20
- data/lib/intelligence/adapters/open_ai/chat_request_methods.rb +1 -1
- data/lib/intelligence/adapters/open_ai/chat_response_methods.rb +38 -42
- data/lib/intelligence/adapters/samba_nova.rb +3 -5
- data/lib/intelligence/adapters/together_ai.rb +4 -4
- data/lib/intelligence/message.rb +1 -1
- data/lib/intelligence/message_content/text.rb +8 -0
- data/lib/intelligence/message_content/tool_call.rb +53 -1
- data/lib/intelligence/version.rb +1 -1
- data/lib/intelligence.rb +1 -0
- metadata +18 -5
- data/lib/intelligence/adapters/generic/chat_methods.rb +0 -362
- data/lib/intelligence/adapters/legacy/adapter.rb +0 -11
- data/lib/intelligence/adapters/legacy/chat_methods.rb +0 -53
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
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
# Intelligence
|
2
2
|
|
3
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
|
5
|
-
you to seamlessly integrate with services from OpenAI, Anthropic, Google,
|
6
|
-
Hyperbolic, Samba Nova, Together AI, and others, while maintaining a consistent API
|
7
|
-
all providers.
|
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
8
|
|
9
9
|
The gem operates with minimal dependencies and doesn't require vendor SDK installation,
|
10
10
|
making it easy to switch between providers or work with multiple providers simultaneously.
|
11
11
|
|
12
|
-
```
|
12
|
+
```ruby
|
13
13
|
require 'intelligence'
|
14
14
|
|
15
15
|
adapter = Intelligence::Adapter.build :open_ai do
|
@@ -61,10 +61,11 @@ $ gem install intelligence
|
|
61
61
|
|
62
62
|
## Usage
|
63
63
|
|
64
|
-
###
|
64
|
+
### Fundamentals
|
65
65
|
|
66
66
|
The core components of Intelligence are adapters, requests and responses. An adapter encapsulates
|
67
|
-
the differences between different
|
67
|
+
the differences between different API vendors, allowing you to use requests and responses
|
68
|
+
uniformly.
|
68
69
|
|
69
70
|
You retrieve an adapter for a specific vendor, configure it with a key, model and associated
|
70
71
|
parameters and then make a request by calling either the `chat` or `stream` methods.
|
@@ -94,25 +95,25 @@ else
|
|
94
95
|
end
|
95
96
|
```
|
96
97
|
|
97
|
-
The `response` object is a Faraday response with an added method: `result`. If a response is
|
98
|
+
The `response` object is a `Faraday` response with an added method: `result`. If a response is
|
98
99
|
successful `result` returns a `ChatResult`. If it is not successful it returns a
|
99
|
-
`ChatErrorResult`.
|
100
|
+
`ChatErrorResult`. You can use the `Faraday` method `success?` to determine if the response is
|
101
|
+
successful.
|
100
102
|
|
101
|
-
###
|
103
|
+
### Results
|
102
104
|
|
103
105
|
When you make a request using Intelligence, the response includes a `result` that provides
|
104
106
|
structured access to the model's output.
|
105
107
|
|
106
108
|
- A `ChatResult` contains one or more `choices` (alternate responses from the model). The
|
107
|
-
`choices` method returns an array of `ChatResultChoice` instances.
|
108
|
-
a `metrics` methods which provides information about token usage for the request.
|
109
|
-
optional `metrics` about token usage
|
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.
|
110
111
|
- A `ChatResultChoice` contains a `message` from the assistant and an `end_result` which
|
111
|
-
indicates how the response ended
|
112
|
+
indicates how the response ended:
|
112
113
|
- `:ended` means the model completed its response normally
|
113
114
|
- `:token_limit_exceeded` means the response hit the token limit ( `max_tokens` )
|
114
115
|
- `:end_sequence_encountered` means the response hit a stop sequence
|
115
|
-
- `:filtered` means the content was filtered by safety settings
|
116
|
+
- `:filtered` means the content was filtered by the vendors safety settings or protocols
|
116
117
|
- `:tool_called` means the model is requesting to use a tool
|
117
118
|
- The `Message` in each choice contains one or more content items, typically text but
|
118
119
|
potentially tool calls or other content types.
|
@@ -157,7 +158,7 @@ if response.success?
|
|
157
158
|
puts "Total tokens: #{result.metrics.total_tokens}"
|
158
159
|
end
|
159
160
|
else
|
160
|
-
# or alternativelly handle the
|
161
|
+
# or alternativelly handle the error result
|
161
162
|
puts "Error: #{response.result.error_description}"
|
162
163
|
end
|
163
164
|
```
|
@@ -165,32 +166,26 @@ end
|
|
165
166
|
The `ChatResult`, `ChatResultChoice` and `Message` all provide the `text` convenience
|
166
167
|
method which return the text.
|
167
168
|
|
168
|
-
|
169
|
-
- `:ended` means the model completed its response normally
|
170
|
-
- `:token_limit_exceeded` means the response hit the token limit
|
171
|
-
- `:end_sequence_encountered` means the response hit a stop sequence
|
172
|
-
- `:filtered` means the content was filtered by safety settings
|
173
|
-
- `:tool_called` means the model is requesting to use a tool
|
174
|
-
|
175
|
-
### Understanding Conversations, Messages, and Content
|
169
|
+
### Conversations, Messages, and Content
|
176
170
|
|
177
171
|
Intelligence organizes interactions with models using three main components:
|
178
172
|
|
179
173
|
- **Conversations** are collections of messages that represent a complete interaction with a
|
180
|
-
model. A conversation can include an optional system message that sets the context,
|
181
|
-
|
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.
|
182
176
|
|
183
177
|
- **Messages** are individual communications within a conversation. Each message has a role
|
184
178
|
(`:system`, `:user`, or `:assistant`) that identifies its sender and can contain multiple
|
185
179
|
pieces of content.
|
186
180
|
|
187
181
|
- **Content** represents the actual data within a message. This can be text
|
188
|
-
(`MessageContent::Text`), binary data like images (`MessageContent::Binary`),
|
189
|
-
to files (`MessageContent::File`)
|
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 ).
|
190
185
|
|
191
186
|
In the previous examples we used a simple string as an argument to `chat`. As a convenience,
|
192
|
-
the `chat` methods builds a coversation for you but, typically, you will construct
|
193
|
-
instance (`Coversation`) and pass that to the chat or stream methods.
|
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.
|
194
189
|
|
195
190
|
The following example expands the minimal example, building a conversation, messages and content:
|
196
191
|
|
@@ -289,7 +284,7 @@ This pattern allows you to maintain context across multiple interactions with th
|
|
289
284
|
request includes the full conversation history, helping the model provide more contextually
|
290
285
|
relevant responses.
|
291
286
|
|
292
|
-
###
|
287
|
+
### Builders
|
293
288
|
|
294
289
|
For more readable configuration, Intelligence provides builder syntax for both adapters and
|
295
290
|
conversations.
|
@@ -454,12 +449,15 @@ own descriptions and requirements. Once defined, tools are added to conversation
|
|
454
449
|
used by the model during its response.
|
455
450
|
|
456
451
|
Note that not all providers support tools, and the specific tool capabilities may vary between
|
457
|
-
providers.
|
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.
|
458
455
|
|
459
456
|
## Streaming Responses
|
460
457
|
|
461
|
-
|
462
|
-
|
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.
|
463
461
|
|
464
462
|
```ruby
|
465
463
|
adapter = Intelligence::Adapter.build! :anthropic do
|
@@ -473,45 +471,68 @@ end
|
|
473
471
|
|
474
472
|
request = Intelligence::ChatRequest.new(adapter: adapter)
|
475
473
|
|
476
|
-
response = request.stream("Tell me a story about a robot.") do |request|
|
477
|
-
request.receive_result do |result|
|
474
|
+
response = request.stream( "Tell me a story about a robot." ) do | request |
|
475
|
+
request.receive_result do | result |
|
478
476
|
# result is a ChatResult object with partial content
|
479
|
-
print result.text
|
477
|
+
print result.text
|
480
478
|
print "\n" if result.choices.first.end_reason
|
481
479
|
end
|
482
480
|
end
|
483
481
|
```
|
484
482
|
|
485
|
-
|
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.
|
486
497
|
|
487
498
|
```ruby
|
488
|
-
|
489
|
-
system_message do
|
490
|
-
content text: "You are an image analysis expert."
|
491
|
-
end
|
492
|
-
|
493
|
-
message do
|
494
|
-
role :user
|
495
|
-
content text: "Describe this image in detail"
|
496
|
-
content do
|
497
|
-
type :binary
|
498
|
-
content_type 'image/jpeg'
|
499
|
-
bytes File.binread('path/to/image.jpg')
|
500
|
-
end
|
501
|
-
end
|
502
|
-
end
|
499
|
+
request = Intelligence::ChatRequest.new( adapter: adapter )
|
503
500
|
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
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
|
510
517
|
end
|
518
|
+
|
511
519
|
end
|
512
520
|
end
|
513
521
|
```
|
514
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
|
+
|
515
536
|
## Provider Switching
|
516
537
|
|
517
538
|
One of Intelligence's most powerful features is the ability to easily switch between providers:
|
data/intelligence.gemspec
CHANGED
@@ -39,6 +39,7 @@ Gem::Specification.new do | spec |
|
|
39
39
|
spec.add_runtime_dependency 'faraday', '~> 2.7'
|
40
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'
|
@@ -89,15 +89,8 @@ module Intelligence
|
|
89
89
|
output_tokens: 0
|
90
90
|
}
|
91
91
|
|
92
|
-
contents.
|
93
|
-
|
94
|
-
when :text
|
95
|
-
content[ :text ] = ''
|
96
|
-
when :tool_call
|
97
|
-
content[ :tool_parameters ] = ''
|
98
|
-
else
|
99
|
-
content.clear
|
100
|
-
end
|
92
|
+
contents.map! do | content |
|
93
|
+
{ type: content[ :type ] }
|
101
94
|
end
|
102
95
|
|
103
96
|
buffer += chunk
|
@@ -116,7 +109,7 @@ module Intelligence
|
|
116
109
|
metrics[ :output_tokens ] += data[ 'message' ]&.[]( 'usage' )&.[]( 'output_tokens' ) || 0
|
117
110
|
when 'content_block_start'
|
118
111
|
index = data[ 'index' ]
|
119
|
-
contents.fill( {}, contents.size
|
112
|
+
contents.fill( {}, contents.size..index ) if contents.size <= index
|
120
113
|
if content_block = data[ 'content_block' ]
|
121
114
|
if content_block[ 'type' ] == 'text'
|
122
115
|
contents[ index ] = {
|
@@ -134,7 +127,7 @@ module Intelligence
|
|
134
127
|
end
|
135
128
|
when 'content_block_delta'
|
136
129
|
index = data[ 'index' ]
|
137
|
-
contents.fill( {}, contents.size
|
130
|
+
contents.fill( {}, contents.size..index ) if contents.size <= index
|
138
131
|
if delta = data[ 'delta' ]
|
139
132
|
if delta[ 'type' ] == 'text_delta'
|
140
133
|
contents[ index ][ :type ] = :text
|
@@ -142,7 +135,7 @@ module Intelligence
|
|
142
135
|
elsif delta[ 'type' ] == 'input_json_delta'
|
143
136
|
contents[ index ][ :type ] = :tool_call
|
144
137
|
contents[ index ][ :tool_parameters ] =
|
145
|
-
( contents[ index ][ :tool_parameters ] || '' ) + delta[ '
|
138
|
+
( contents[ index ][ :tool_parameters ] || '' ) + delta[ 'partial_json' ]
|
146
139
|
end
|
147
140
|
end
|
148
141
|
when 'message_delta'
|
@@ -1,10 +1,12 @@
|
|
1
1
|
require_relative '../../adapter'
|
2
|
-
require_relative '
|
2
|
+
require_relative 'chat_request_methods'
|
3
|
+
require_relative 'chat_response_methods'
|
3
4
|
|
4
5
|
module Intelligence
|
5
6
|
module Generic
|
6
7
|
class Adapter < Adapter::Base
|
7
|
-
include
|
8
|
+
include ChatRequestMethods
|
9
|
+
include ChatResponseMethods
|
8
10
|
end
|
9
11
|
end
|
10
12
|
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
module Intelligence
|
2
|
+
module Generic
|
3
|
+
module ChatRequestMethods
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def chat_request_uri( uri = nil )
|
7
|
+
if uri
|
8
|
+
@chat_request_uri = uri
|
9
|
+
else
|
10
|
+
@chat_request_uri
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.included( base )
|
16
|
+
base.extend( ClassMethods )
|
17
|
+
end
|
18
|
+
|
19
|
+
def chat_request_uri( options )
|
20
|
+
self.class.chat_request_uri
|
21
|
+
end
|
22
|
+
|
23
|
+
def chat_request_headers( options = nil )
|
24
|
+
options = @options.merge( build_options( options ) )
|
25
|
+
result = {}
|
26
|
+
|
27
|
+
key = options[ :key ]
|
28
|
+
|
29
|
+
raise ArgumentError.new( "An API key is required to build a chat request." ) \
|
30
|
+
if key.nil?
|
31
|
+
|
32
|
+
result[ 'Content-Type' ] = 'application/json'
|
33
|
+
result[ 'Authorization' ] = "Bearer #{key}"
|
34
|
+
|
35
|
+
result
|
36
|
+
end
|
37
|
+
|
38
|
+
def chat_request_body( conversation, options = nil )
|
39
|
+
options = @options.merge( build_options( options ) )
|
40
|
+
|
41
|
+
result = options[ :chat_options ]
|
42
|
+
result[ :messages ] = []
|
43
|
+
|
44
|
+
system_message = chat_request_system_message_attributes( conversation[ :system_message ] )
|
45
|
+
result[ :messages ] << system_message if system_message
|
46
|
+
|
47
|
+
conversation[ :messages ]&.each do | message |
|
48
|
+
return nil unless message[ :contents ]&.any?
|
49
|
+
|
50
|
+
result_message = { role: message[ :role ] }
|
51
|
+
result_message_content = []
|
52
|
+
|
53
|
+
message_contents = message[ :contents ]
|
54
|
+
|
55
|
+
# tool calls in the open ai api are not content
|
56
|
+
tool_calls, message_contents = message_contents.partition do | content |
|
57
|
+
content[ :type ] == :tool_call
|
58
|
+
end
|
59
|
+
|
60
|
+
# tool results in the open ai api are not content
|
61
|
+
tool_results, message_contents = message_contents.partition do | content |
|
62
|
+
content[ :type ] == :tool_result
|
63
|
+
end
|
64
|
+
|
65
|
+
# many vendor api's, especially when hosting text only models, will only accept a single
|
66
|
+
# text content item; if the content is only text this will coalece multiple text content
|
67
|
+
# items into a single content item
|
68
|
+
unless message_contents.any? { | c | c[ :type ] != :text }
|
69
|
+
result_message_content = message_contents.map { | c | c[ :text ] || '' }.join( "\n" )
|
70
|
+
else
|
71
|
+
message_contents&.each do | content |
|
72
|
+
result_message_content << chat_request_message_content_attributes( content )
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
if tool_calls.any?
|
77
|
+
result_message[ :tool_calls ] = tool_calls.map { | tool_call |
|
78
|
+
{
|
79
|
+
id: tool_call[ :tool_call_id ],
|
80
|
+
type: 'function',
|
81
|
+
function: {
|
82
|
+
name: tool_call[ :tool_name ],
|
83
|
+
arguments: JSON.generate( tool_call[ :tool_parameters ] || {} )
|
84
|
+
}
|
85
|
+
}
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
result_message[ :content ] = result_message_content
|
90
|
+
unless result_message_content.empty? && tool_calls.empty?
|
91
|
+
result[ :messages ] << result_message
|
92
|
+
end
|
93
|
+
|
94
|
+
if tool_results.any?
|
95
|
+
result[ :messages ].concat( tool_results.map { | tool_result |
|
96
|
+
{
|
97
|
+
role: :tool,
|
98
|
+
tool_call_id: tool_result[ :tool_call_id ],
|
99
|
+
content: tool_result[ :tool_result ]
|
100
|
+
}
|
101
|
+
} )
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
tools_attributes = chat_request_tools_attributes( conversation[ :tools ] )
|
106
|
+
result[ :tools ] = tools_attributes if tools_attributes && tools_attributes.length > 0
|
107
|
+
|
108
|
+
JSON.generate( result )
|
109
|
+
end
|
110
|
+
|
111
|
+
def chat_request_message_content_attributes( content )
|
112
|
+
case content[ :type ]
|
113
|
+
when :text
|
114
|
+
{ type: 'text', text: content[ :text ] }
|
115
|
+
when :binary
|
116
|
+
content_type = content[ :content_type ]
|
117
|
+
bytes = content[ :bytes ]
|
118
|
+
if content_type && bytes
|
119
|
+
mime_type = MIME::Types[ content_type ].first
|
120
|
+
if mime_type&.media_type == 'image'
|
121
|
+
{
|
122
|
+
type: 'image_url',
|
123
|
+
image_url: {
|
124
|
+
url: "data:#{content_type};base64,#{Base64.strict_encode64( bytes )}".freeze
|
125
|
+
}
|
126
|
+
}
|
127
|
+
else
|
128
|
+
raise UnsupportedContentError.new(
|
129
|
+
:generic,
|
130
|
+
'only support content of type image/*'
|
131
|
+
)
|
132
|
+
end
|
133
|
+
else
|
134
|
+
raise UnsupportedContentError.new(
|
135
|
+
:generic,
|
136
|
+
'requires binary content to include content type and ( packed ) bytes'
|
137
|
+
)
|
138
|
+
end
|
139
|
+
when :file
|
140
|
+
content_type = content[ :content_type ]
|
141
|
+
uri = content[ :uri ]
|
142
|
+
if content_type && uri
|
143
|
+
mime_type = MIME::Types[ content_type ].first
|
144
|
+
if mime_type&.media_type == 'image'
|
145
|
+
{
|
146
|
+
type: 'image_url',
|
147
|
+
image_url: { url: uri }
|
148
|
+
}
|
149
|
+
else
|
150
|
+
raise UnsupportedContentError.new(
|
151
|
+
:generic,
|
152
|
+
'only support content of type image/*'
|
153
|
+
)
|
154
|
+
end
|
155
|
+
else
|
156
|
+
raise UnsupportedContentError.new(
|
157
|
+
:generic,
|
158
|
+
'requires binary content to include content type and ( packed ) bytes'
|
159
|
+
)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def chat_request_system_message_attributes( system_message )
|
165
|
+
return nil if system_message.nil?
|
166
|
+
|
167
|
+
result = ''
|
168
|
+
system_message[ :contents ].each do | content |
|
169
|
+
result += content[ :text ] if content[ :type ] == :text
|
170
|
+
end
|
171
|
+
|
172
|
+
result.empty? ? nil : { role: 'system', content: result } if system_message
|
173
|
+
end
|
174
|
+
|
175
|
+
def chat_request_tools_attributes( tools )
|
176
|
+
properties_array_to_object = lambda do | properties |
|
177
|
+
return nil unless properties&.any?
|
178
|
+
object = {}
|
179
|
+
required = []
|
180
|
+
properties.each do | property |
|
181
|
+
name = property.delete( :name )
|
182
|
+
required << name if property.delete( :required )
|
183
|
+
if property[ :properties ]&.any?
|
184
|
+
property_properties, property_required =
|
185
|
+
properties_array_to_object.call( property[ :properties ] )
|
186
|
+
property[ :properties ] = property_properties
|
187
|
+
property[ :required ] = property_required if property_required.any?
|
188
|
+
end
|
189
|
+
object[ name ] = property
|
190
|
+
end
|
191
|
+
[ object, required.compact ]
|
192
|
+
end
|
193
|
+
|
194
|
+
tools&.map do | tool |
|
195
|
+
function = {
|
196
|
+
type: 'function',
|
197
|
+
function: {
|
198
|
+
name: tool[ :name ],
|
199
|
+
description: tool[ :description ],
|
200
|
+
}
|
201
|
+
}
|
202
|
+
|
203
|
+
if tool[ :properties ]&.any?
|
204
|
+
properties_object, properties_required =
|
205
|
+
properties_array_to_object.call( tool[ :properties ] )
|
206
|
+
function[ :function ][ :parameters ] = {
|
207
|
+
type: 'object',
|
208
|
+
properties: properties_object
|
209
|
+
}
|
210
|
+
function[ :function ][ :parameters ][ :required ] = properties_required \
|
211
|
+
if properties_required.any?
|
212
|
+
else
|
213
|
+
function[ :function ][ :parameters ] = {}
|
214
|
+
end
|
215
|
+
function
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|