ox-ai-workers 1.0.3.2 → 1.1.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/.ruby-version +1 -1
- data/CHANGELOG.md +3 -5
- data/README.md +1 -17
- data/lib/ox-ai-workers.rb +2 -1
- data/lib/oxaiworkers/assistant/module_base.rb +2 -2
- data/lib/oxaiworkers/assistant/orchestrator.rb +1 -1
- data/lib/oxaiworkers/iterator.rb +23 -45
- data/lib/oxaiworkers/models/anthropic_max.rb +137 -2
- data/lib/oxaiworkers/models/llm_base.rb +142 -0
- data/lib/oxaiworkers/module_request.rb +25 -102
- data/lib/oxaiworkers/request.rb +1 -1
- data/lib/oxaiworkers/tool/pipeline.rb +2 -2
- data/lib/oxaiworkers/tool/pixels.rb +4 -4
- data/lib/oxaiworkers/tool/wolfram.rb +2 -2
- data/lib/oxaiworkers/tool_definition.rb +31 -21
- data/lib/oxaiworkers/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 222d468a923d3589372785cdb3d6cc17aab4328b5598565aa69f77d9623a5b61
|
4
|
+
data.tar.gz: b6af99dfa09c007f67affe266e6de0add9f0a2e82968a244bc06cdfe544a3f8e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 82e8a31e47464a3d0d2f9cf1a37b317a5f5fa5df65f2ce997d99b596002dbc91d58685abc7bf90f4fdb011c441b21437510e67e25b8380a1e72626ee72c142a6
|
7
|
+
data.tar.gz: 3dbb22555ea1448c5dde835374f18bc9f4c201a7a854a1866029e43a28b3a10f4486435684ee61cef7f11839502d03dc051e8b71a19d91dcec8875b322f79ef9
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.4.
|
1
|
+
3.4.4
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,7 @@
|
|
1
|
-
## [1.0
|
1
|
+
## [1.1.0] - 2025-05-21
|
2
2
|
|
3
|
-
- Added `
|
4
|
-
- Added
|
5
|
-
- Added `tool_call_id` for `Iterator`
|
3
|
+
- Added `add_base64` and `add_url` for Models
|
4
|
+
- Added Anthropic support
|
6
5
|
|
7
6
|
## [1.0.1] - 2025-05-13
|
8
7
|
|
@@ -65,7 +64,6 @@
|
|
65
64
|
- Fixed tool calls parsing
|
66
65
|
- Refine Russian locale for Iterator with enhanced step descriptions and planning guidance
|
67
66
|
- Update locale for Iterator
|
68
|
-
- Added `on_stream` for `Iterator`
|
69
67
|
|
70
68
|
## [0.7.10] - 2025-03-31
|
71
69
|
|
data/README.md
CHANGED
@@ -392,7 +392,7 @@ class MyTool
|
|
392
392
|
|
393
393
|
define_function :hello_world, description: "Says hello to someone" do
|
394
394
|
property :name, type: "string", description: "Name to greet" # Default required: true
|
395
|
-
property :age, type:
|
395
|
+
property :age, type: "integer", description: "Age of the person", required: false
|
396
396
|
end
|
397
397
|
end
|
398
398
|
|
@@ -409,8 +409,6 @@ class MyTool
|
|
409
409
|
end
|
410
410
|
```
|
411
411
|
|
412
|
-
The `define_function` method accepts an optional `strict` parameter (defaults to `true`) that controls whether additional properties are allowed in the input. When `strict: true` (default), the schema will include `additionalProperties: false`, enforcing that only defined properties can be used.
|
413
|
-
|
414
412
|
Tools can also implement a `context` method that returns information to be included in assistant conversations before each request, which is particularly useful when multiple assistants share a common tool to maintain shared state or history.
|
415
413
|
|
416
414
|
### Working with Files and Images
|
@@ -495,20 +493,6 @@ iterator = OxAiWorkers::Iterator.new(
|
|
495
493
|
)
|
496
494
|
```
|
497
495
|
|
498
|
-
### Streaming API Responses
|
499
|
-
|
500
|
-
Enable streaming for real-time feedback:
|
501
|
-
|
502
|
-
```ruby
|
503
|
-
worker = OxAiWorkers::Request.new(
|
504
|
-
on_stream: ->(chunk) {
|
505
|
-
if chunk.dig('choices', 0, 'delta', 'content')
|
506
|
-
print chunk.dig('choices', 0, 'delta', 'content')
|
507
|
-
end
|
508
|
-
}
|
509
|
-
)
|
510
|
-
```
|
511
|
-
|
512
496
|
### Available Assistant Types
|
513
497
|
|
514
498
|
OxAiWorkers provides several specialized assistant types:
|
data/lib/ox-ai-workers.rb
CHANGED
@@ -62,7 +62,7 @@ module OxAiWorkers
|
|
62
62
|
|
63
63
|
class Configuration
|
64
64
|
attr_accessor :max_tokens, :temperature, :wait_for_complete, :access_token_deepseek, :access_token_openai,
|
65
|
-
:access_token_stability, :access_token_wolfram, :access_token_anthropic
|
65
|
+
:access_token_stability, :access_token_wolfram, :access_token_anthropic, :access_token_gemini
|
66
66
|
|
67
67
|
def initialize
|
68
68
|
@max_tokens = DEFAULT_MAX_TOKEN
|
@@ -74,6 +74,7 @@ module OxAiWorkers
|
|
74
74
|
@access_token_stability = nil
|
75
75
|
@access_token_wolfram = nil
|
76
76
|
@access_token_anthropic = nil
|
77
|
+
@access_token_gemini = nil
|
77
78
|
|
78
79
|
[Array, NilClass, String, Symbol, Hash].each do |c|
|
79
80
|
c.send(:include, OxAiWorkers::PresentCompat) unless c.method_defined?(:present?)
|
@@ -25,9 +25,9 @@ module OxAiWorkers
|
|
25
25
|
execute
|
26
26
|
end
|
27
27
|
|
28
|
-
def init_worker(delayed: false, model: nil
|
28
|
+
def init_worker(delayed: false, model: nil)
|
29
29
|
model ||= OxAiWorkers.default_model
|
30
|
-
delayed ? DelayedRequest.new(model:) : Request.new(model
|
30
|
+
delayed ? DelayedRequest.new(model:) : Request.new(model:)
|
31
31
|
end
|
32
32
|
|
33
33
|
def replace_context(context)
|
data/lib/oxaiworkers/iterator.rb
CHANGED
@@ -18,6 +18,11 @@ module OxAiWorkers
|
|
18
18
|
@locale = locale || I18n.locale
|
19
19
|
@call_id = 0
|
20
20
|
|
21
|
+
@def_only = def_only || ITERATOR_FUNCTIONS
|
22
|
+
@def_except = def_except
|
23
|
+
|
24
|
+
init_white_list_with available_defs
|
25
|
+
|
21
26
|
with_locale do
|
22
27
|
define_function :inner_monologue, description: I18n.t('oxaiworkers.iterator.inner_monologue.description') do
|
23
28
|
property :speach, type: 'string', description: I18n.t('oxaiworkers.iterator.inner_monologue.speach'),
|
@@ -37,8 +42,6 @@ module OxAiWorkers
|
|
37
42
|
@tools = tools
|
38
43
|
@role = role
|
39
44
|
@context = []
|
40
|
-
@def_only = def_only || ITERATOR_FUNCTIONS
|
41
|
-
@def_except = def_except
|
42
45
|
|
43
46
|
@on_inner_monologue = on_inner_monologue
|
44
47
|
@on_outer_voice = on_outer_voice
|
@@ -129,14 +132,14 @@ module OxAiWorkers
|
|
129
132
|
end
|
130
133
|
@worker.append(messages: @messages)
|
131
134
|
@tasks.each { |task| @worker.append(role: :user, content: "<task>\n#{task}\n</task>") }
|
132
|
-
@worker.tools = function_schemas
|
135
|
+
@worker.tools = [function_schemas]
|
133
136
|
return unless @tools.present?
|
134
137
|
|
135
138
|
@worker.tools += @tools.map do |tool|
|
136
139
|
if tool.respond_to?(:function_schemas)
|
137
|
-
tool.function_schemas
|
140
|
+
tool.function_schemas
|
138
141
|
else
|
139
|
-
tool.class.function_schemas
|
142
|
+
tool.class.function_schemas
|
140
143
|
end
|
141
144
|
end.flatten
|
142
145
|
end
|
@@ -180,6 +183,8 @@ module OxAiWorkers
|
|
180
183
|
OxAiWorkers.logger.warn "Iterator::ServerError #{e.message}. Waiting 10 seconds..."
|
181
184
|
sleep(10)
|
182
185
|
external_request
|
186
|
+
rescue Faraday::BadRequestError => e
|
187
|
+
OxAiWorkers.logger.warn "Iterator::BadRequestError #{e.message}. #{@worker.messages.to_json}"
|
183
188
|
end
|
184
189
|
|
185
190
|
def tick_or_wait
|
@@ -228,26 +233,12 @@ module OxAiWorkers
|
|
228
233
|
@call_id += 1
|
229
234
|
# Add tool call message in the correct format
|
230
235
|
out = tool.send(external_call[:name], **external_call[:args])
|
231
|
-
@queue
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
name: external_call[:name],
|
238
|
-
arguments: external_call[:args].to_json
|
239
|
-
}
|
240
|
-
}]
|
241
|
-
}
|
242
|
-
@queue << if out.present?
|
243
|
-
{ role: :tool,
|
244
|
-
content: out,
|
245
|
-
tool_call_id: "call_#{@call_id}" }
|
246
|
-
else
|
247
|
-
{ role: :tool,
|
248
|
-
content: "Tool call #{external_call[:name]} successful.",
|
249
|
-
tool_call_id: "call_#{@call_id}" }
|
250
|
-
end
|
236
|
+
@queue += @worker.model.tool_call(
|
237
|
+
name: external_call[:name],
|
238
|
+
args: external_call[:args],
|
239
|
+
call_id: @call_id,
|
240
|
+
out:
|
241
|
+
)
|
251
242
|
end
|
252
243
|
@worker.finish
|
253
244
|
iterate! if can_iterate?
|
@@ -269,35 +260,22 @@ module OxAiWorkers
|
|
269
260
|
@queue << { role:, content: text }
|
270
261
|
end
|
271
262
|
|
272
|
-
def add_context(text, role: :
|
263
|
+
def add_context(text, role: :user)
|
273
264
|
add_raw_context({ role:, content: text })
|
274
265
|
end
|
275
266
|
|
276
267
|
def add_file(pdf:, filename:, text:, role: :user)
|
277
|
-
content =
|
278
|
-
content << { type: 'text', text: } if text.present?
|
279
|
-
content << {
|
280
|
-
type: 'file',
|
281
|
-
file: {
|
282
|
-
filename:,
|
283
|
-
file_data: "data:application/pdf;base64,#{Base64.strict_encode64(pdf)}"
|
284
|
-
}
|
285
|
-
}
|
286
|
-
|
268
|
+
content = @worker.model.add_base64(binary: pdf, filename:, text:, mime_type: 'application/pdf')
|
287
269
|
add_raw_context({ role:, content: })
|
288
270
|
end
|
289
271
|
|
290
272
|
def add_image(text:, url: nil, binary: nil, role: :user, detail: 'auto', mime_type: 'image/png')
|
291
273
|
content = []
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
url
|
298
|
-
end
|
299
|
-
|
300
|
-
content << { type: 'image_url', image_url: { url: image_url, detail: } }
|
274
|
+
if binary.present?
|
275
|
+
content = @worker.model.add_base64(binary:, filename:, text:, mime_type:, detail:)
|
276
|
+
elsif url.present?
|
277
|
+
content = @worker.model.add_url(url:, text:, detail:, mime_type:)
|
278
|
+
end
|
301
279
|
|
302
280
|
add_raw_context({ role:, content: })
|
303
281
|
end
|
@@ -5,9 +5,144 @@ module OxAiWorkers
|
|
5
5
|
class AnthropicMax < LLMBase
|
6
6
|
def initialize(uri_base: nil, api_key: nil, model: nil, max_tokens: nil, temperature: nil, frequency_penalty: nil)
|
7
7
|
@model = model || 'claude-3-7-sonnet-latest'
|
8
|
-
@uri_base = uri_base || 'https://api.anthropic.com/v1/'
|
8
|
+
@uri_base = uri_base # || 'https://api.anthropic.com/v1/'
|
9
9
|
@api_key = api_key || OxAiWorkers.configuration.access_token_anthropic
|
10
|
-
super(uri_base
|
10
|
+
super(uri_base: @uri_base, api_key: @api_key, model: @model, max_tokens:, temperature:, frequency_penalty:)
|
11
|
+
end
|
12
|
+
|
13
|
+
def client
|
14
|
+
@client ||= Anthropic::Client.new(
|
15
|
+
access_token: @api_key,
|
16
|
+
uri_base: @uri_base,
|
17
|
+
log_errors: true
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def request(parameters)
|
22
|
+
client.messages(parameters:)
|
23
|
+
end
|
24
|
+
|
25
|
+
def build_parameters(messages:, tools: [], filtered_functions: [], tool_choice: nil)
|
26
|
+
parameters = {
|
27
|
+
model: @model,
|
28
|
+
system: messages.select { |m| m[:role] == :system }.map { |m| m[:content] }.join("\n\n"),
|
29
|
+
messages: messages.reject { |m| m[:role] == :system },
|
30
|
+
temperature: @temperature,
|
31
|
+
max_tokens: @max_tokens
|
32
|
+
}
|
33
|
+
if tools.present?
|
34
|
+
functions = tools.map(&:to_anthropic_format).flatten
|
35
|
+
|
36
|
+
@names = functions.map { |f| f[:name] }
|
37
|
+
|
38
|
+
parameters[:tools] = functions.reject { |f| filtered_functions.include?(f[:name]) }
|
39
|
+
|
40
|
+
parameters[:tool_choice] =
|
41
|
+
tool_choice.nil? ? { type: 'any' } : { type: 'tool', name: tool_choice }
|
42
|
+
end
|
43
|
+
parameters
|
44
|
+
end
|
45
|
+
|
46
|
+
def tool_call(name:, args:, call_id:, out:)
|
47
|
+
[
|
48
|
+
{
|
49
|
+
role: :assistant,
|
50
|
+
content: [
|
51
|
+
{
|
52
|
+
type: 'tool_use',
|
53
|
+
id: "call_#{call_id}",
|
54
|
+
name:,
|
55
|
+
input: args
|
56
|
+
}
|
57
|
+
]
|
58
|
+
},
|
59
|
+
{
|
60
|
+
role: :user,
|
61
|
+
content: [
|
62
|
+
{
|
63
|
+
type: 'tool_result',
|
64
|
+
tool_use_id: "call_#{call_id}",
|
65
|
+
content: out.present? ? out : "Tool call #{name} successful."
|
66
|
+
}
|
67
|
+
]
|
68
|
+
}
|
69
|
+
]
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_base64(binary:, filename:, text:, mime_type:)
|
73
|
+
content = []
|
74
|
+
content << { type: 'text', text: } if text.present?
|
75
|
+
content << {
|
76
|
+
type: mime_type.include?('image') ? 'image' : 'document',
|
77
|
+
source: {
|
78
|
+
type: 'base64',
|
79
|
+
media_type: mime_type,
|
80
|
+
data: Base64.strict_encode64(binary)
|
81
|
+
}
|
82
|
+
}
|
83
|
+
content
|
84
|
+
end
|
85
|
+
|
86
|
+
def add_url(url:, text:, mime_type:)
|
87
|
+
content = []
|
88
|
+
content << { type: 'text', text: } if text.present?
|
89
|
+
content << {
|
90
|
+
type: mime_type.include?('image') ? 'image' : 'document',
|
91
|
+
source: {
|
92
|
+
type: 'url',
|
93
|
+
url: url
|
94
|
+
}
|
95
|
+
}
|
96
|
+
content
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse_response(response, &)
|
100
|
+
choices = response['content']
|
101
|
+
@is_truncated = (response['stop_reason'] == 'max_tokens')
|
102
|
+
return if choices.nil? || choices.empty?
|
103
|
+
|
104
|
+
choices.each do |choice|
|
105
|
+
result, tool_calls = parse_one_choice(choice)
|
106
|
+
yield(result, @is_truncated, tool_calls)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def parse_one_choice(choice)
|
111
|
+
return unless choice # Skip if there's no choice
|
112
|
+
|
113
|
+
# Initialize result variables
|
114
|
+
@result = nil
|
115
|
+
@tool_calls = []
|
116
|
+
|
117
|
+
# Process content item
|
118
|
+
if choice['type'] == 'tool_use'
|
119
|
+
# Handle tool use
|
120
|
+
begin
|
121
|
+
# Attempt to parse arguments, handle potential JSON errors
|
122
|
+
args = JSON.parse(choice['input'].to_json, symbolize_names: true)
|
123
|
+
rescue JSON::ParserError => e
|
124
|
+
OxAiWorkers.logger.error("Failed to parse tool call arguments: #{e.message}", for: self.class)
|
125
|
+
OxAiWorkers.logger.debug("Raw arguments: #{choice['input']}", for: self.class)
|
126
|
+
end
|
127
|
+
|
128
|
+
fname = @names.find { |n| n.end_with?(choice['name']) }
|
129
|
+
if fname != choice['name']
|
130
|
+
OxAiWorkers.logger.error("Tool call name #{choice['name']} not found. Using #{fname} instead.",
|
131
|
+
for: self.class)
|
132
|
+
end
|
133
|
+
|
134
|
+
tool_call = {
|
135
|
+
class: fname.split('__').first,
|
136
|
+
name: fname.split('__').last,
|
137
|
+
args:
|
138
|
+
}
|
139
|
+
@tool_calls << tool_call
|
140
|
+
elsif choice['type'] == 'text'
|
141
|
+
# Handle text content
|
142
|
+
@result = choice['text']
|
143
|
+
end
|
144
|
+
|
145
|
+
[@result, @tool_calls]
|
11
146
|
end
|
12
147
|
end
|
13
148
|
end
|
@@ -13,6 +13,148 @@ module OxAiWorkers
|
|
13
13
|
@uri_base = uri_base
|
14
14
|
@model = model
|
15
15
|
end
|
16
|
+
|
17
|
+
def request(parameters)
|
18
|
+
client.chat(parameters:)
|
19
|
+
end
|
20
|
+
|
21
|
+
def client
|
22
|
+
@client ||= OpenAI::Client.new(
|
23
|
+
access_token: @api_key,
|
24
|
+
uri_base: @uri_base,
|
25
|
+
log_errors: true # Highly recommended in development, so you can see what errors OpenAI is returning. Not recommended in production because it could leak private data to your logs.
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def build_parameters(messages:, tools: [], filtered_functions: [], tool_choice: nil)
|
30
|
+
parameters = {
|
31
|
+
model: @model,
|
32
|
+
messages:,
|
33
|
+
temperature: @temperature,
|
34
|
+
max_completion_tokens: @max_tokens,
|
35
|
+
frequency_penalty: @frequency_penalty
|
36
|
+
}
|
37
|
+
if tools.present?
|
38
|
+
functions = tools.map(&:to_openai_format).flatten
|
39
|
+
|
40
|
+
parameters[:tools] = functions.reject { |f| filtered_functions.include?(f[:name]) }
|
41
|
+
|
42
|
+
parameters[:tool_choice] =
|
43
|
+
tool_choice.nil? ? 'required' : { type: 'function', function: { name: tool_choice } }
|
44
|
+
end
|
45
|
+
parameters
|
46
|
+
end
|
47
|
+
|
48
|
+
def tool_call(name:, args:, call_id:, out:)
|
49
|
+
[
|
50
|
+
{
|
51
|
+
role: :assistant,
|
52
|
+
tool_calls: [{
|
53
|
+
id: "call_#{call_id}",
|
54
|
+
type: 'function',
|
55
|
+
function: {
|
56
|
+
name:,
|
57
|
+
arguments: args.to_json
|
58
|
+
}
|
59
|
+
}]
|
60
|
+
},
|
61
|
+
{
|
62
|
+
role: :tool,
|
63
|
+
content: out.present? ? out : "Tool call #{name} successful.",
|
64
|
+
tool_call_id: "call_#{call_id}"
|
65
|
+
}
|
66
|
+
]
|
67
|
+
end
|
68
|
+
|
69
|
+
def add_base64(binary:, filename:, text:, mime_type:, detail: 'auto')
|
70
|
+
content = []
|
71
|
+
content << { type: 'text', text: } if text.present?
|
72
|
+
content << if mime_type.include?('image')
|
73
|
+
{ type: 'image_url',
|
74
|
+
image_url: {
|
75
|
+
url: "data:#{mime_type};base64,#{Base64.strict_encode64(binary)}",
|
76
|
+
detail:
|
77
|
+
}
|
78
|
+
}
|
79
|
+
else
|
80
|
+
{
|
81
|
+
type: 'file',
|
82
|
+
file: {
|
83
|
+
filename:,
|
84
|
+
file_data: "data:#{mime_type};base64,#{Base64.strict_encode64(binary)}"
|
85
|
+
}
|
86
|
+
}
|
87
|
+
end
|
88
|
+
content
|
89
|
+
end
|
90
|
+
|
91
|
+
def add_url(url:, text:, detail: 'auto')
|
92
|
+
content = []
|
93
|
+
content << { type: 'text', text: } if text.present?
|
94
|
+
content << { type: 'image_url', image_url: { url:, detail: } }
|
95
|
+
content
|
96
|
+
end
|
97
|
+
|
98
|
+
def parse_response(response, &)
|
99
|
+
choices = response['choices']
|
100
|
+
return if choices.nil? || choices.empty?
|
101
|
+
|
102
|
+
choices.each do |choice|
|
103
|
+
arr = parse_one_choice(choice)
|
104
|
+
yield(arr)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def parse_one_choice(choice)
|
109
|
+
message = choice['message']
|
110
|
+
return unless message # Skip if there's no message in this choice
|
111
|
+
|
112
|
+
# Accumulate raw tool calls if present
|
113
|
+
tool_calls_raw = message['tool_calls']
|
114
|
+
|
115
|
+
# Update result with the content if present
|
116
|
+
current_result = message['content']
|
117
|
+
@result = current_result if current_result.present?
|
118
|
+
|
119
|
+
# Update finish reason and truncation status based on the *last* choice's reason
|
120
|
+
current_finish_reason = choice['finish_reason']
|
121
|
+
@is_truncated = (current_finish_reason == 'length')
|
122
|
+
|
123
|
+
@tool_calls = _parse_tool_calls(tool_calls_raw) unless @is_truncated || tool_calls_raw.empty?
|
124
|
+
[@result, @is_truncated, @tool_calls]
|
125
|
+
end
|
126
|
+
|
127
|
+
def _parse_tool_calls(tool_calls_raw)
|
128
|
+
tool_calls = [] # Ensure it's clean before parsing
|
129
|
+
tool_calls_raw.each do |tool_call|
|
130
|
+
next unless tool_call['type'] == 'function' # Ensure it's a function call
|
131
|
+
|
132
|
+
function = tool_call['function']
|
133
|
+
next unless function && function['name'] && function['arguments']
|
134
|
+
|
135
|
+
begin
|
136
|
+
# Attempt to parse arguments, handle potential JSON errors
|
137
|
+
args = JSON.parse(function['arguments'], symbolize_names: true)
|
138
|
+
rescue JSON::ParserError => e
|
139
|
+
OxAiWorkers.logger.error("Failed to parse tool call arguments: #{e.message}", for: self.class)
|
140
|
+
OxAiWorkers.logger.debug("Raw arguments: #{function['arguments']}", for: self.class)
|
141
|
+
# Decide how to handle parsing errors, e.g., skip this call or add with empty args
|
142
|
+
# Skipping for now, as partial args are likely useless.
|
143
|
+
next
|
144
|
+
end
|
145
|
+
OxAiWorkers.logger.debug("function: #{function.inspect}", for: self.class)
|
146
|
+
# Accumulate parsed tool calls
|
147
|
+
next if function['name'].empty?
|
148
|
+
|
149
|
+
tool_calls << {
|
150
|
+
class: function['name'].split('__').first,
|
151
|
+
name: function['name'].split('__').last,
|
152
|
+
args:
|
153
|
+
}
|
154
|
+
# @last_call = function['name'].to_s
|
155
|
+
end
|
156
|
+
tool_calls
|
157
|
+
end
|
16
158
|
end
|
17
159
|
end
|
18
160
|
end
|
@@ -3,16 +3,14 @@
|
|
3
3
|
module OxAiWorkers
|
4
4
|
class ModuleRequest
|
5
5
|
attr_accessor :result, :client, :messages, :model, :custom_id, :tools, :errors,
|
6
|
-
:
|
7
|
-
:
|
6
|
+
:tool_calls, :is_truncated,
|
7
|
+
:call_stack, :last_call, :stop_double_calls
|
8
8
|
|
9
|
-
def initialize_requests(model:,
|
9
|
+
def initialize_requests(model:, call_stack: nil)
|
10
10
|
@custom_id = SecureRandom.uuid
|
11
11
|
@model = model
|
12
12
|
@client = nil
|
13
13
|
@is_truncated = false
|
14
|
-
@finish_reason = nil
|
15
|
-
@on_stream_proc = on_stream
|
16
14
|
@call_stack = call_stack
|
17
15
|
@last_call = nil
|
18
16
|
|
@@ -25,18 +23,12 @@ module OxAiWorkers
|
|
25
23
|
end
|
26
24
|
|
27
25
|
def cleanup
|
28
|
-
@client ||=
|
29
|
-
access_token: @model.api_key,
|
30
|
-
uri_base: @model.uri_base,
|
31
|
-
log_errors: true # Highly recommended in development, so you can see what errors OpenAI is returning. Not recommended in production because it could leak private data to your logs.
|
32
|
-
)
|
26
|
+
@client ||= @model.client
|
33
27
|
@result = nil
|
34
28
|
@errors = nil
|
35
29
|
@messages = []
|
36
30
|
@tool_calls = nil
|
37
|
-
@tool_calls_raw = nil
|
38
31
|
@is_truncated = false
|
39
|
-
@finish_reason = nil
|
40
32
|
# @last_call = nil
|
41
33
|
end
|
42
34
|
|
@@ -46,32 +38,23 @@ module OxAiWorkers
|
|
46
38
|
end
|
47
39
|
|
48
40
|
def params
|
49
|
-
|
50
|
-
|
41
|
+
filtered_functions = []
|
42
|
+
filtered_functions << @last_call if @stop_double_calls.include?(@last_call)
|
43
|
+
|
44
|
+
tool_choice = if @call_stack&.any?
|
45
|
+
func1 = @call_stack.first
|
46
|
+
@call_stack = @call_stack.drop(1)
|
47
|
+
func1
|
48
|
+
else
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
|
52
|
+
@model.build_parameters(
|
51
53
|
messages: @messages,
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
if @tools.present?
|
57
|
-
parameters[:tools] = @tools.reject do |f|
|
58
|
-
tool_name = f[:function][:name]
|
59
|
-
tool_name == @last_call && @stop_double_calls.include?(tool_name)
|
60
|
-
end
|
61
|
-
|
62
|
-
if @call_stack&.any?
|
63
|
-
func1 = @call_stack.first
|
64
|
-
@call_stack = @call_stack.drop(1)
|
65
|
-
parameters[:tool_choice] = { type: 'function', function: { name: func1 } }
|
66
|
-
else
|
67
|
-
parameters[:tool_choice] = 'required'
|
68
|
-
end
|
69
|
-
end
|
70
|
-
if @on_stream_proc.present?
|
71
|
-
parameters[:stream] = @on_stream_proc
|
72
|
-
parameters[:stream_options] = { include_usage: true }
|
73
|
-
end
|
74
|
-
parameters
|
54
|
+
tools: @tools,
|
55
|
+
filtered_functions:,
|
56
|
+
tool_choice:
|
57
|
+
)
|
75
58
|
end
|
76
59
|
|
77
60
|
def not_found_is_ok
|
@@ -83,75 +66,15 @@ module OxAiWorkers
|
|
83
66
|
def parse_choices(response)
|
84
67
|
# Reset instance variables before processing choices
|
85
68
|
@result = nil
|
86
|
-
@tool_calls_raw = []
|
87
69
|
@tool_calls = []
|
88
|
-
@finish_reason = nil
|
89
70
|
@is_truncated = false
|
90
71
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
# Parse basic info and accumulate raw tool calls
|
96
|
-
parse_one_choice(choice)
|
97
|
-
end
|
98
|
-
|
99
|
-
# Only parse tool calls if the response wasn't truncated
|
100
|
-
_parse_tool_calls unless @is_truncated || @tool_calls_raw.empty?
|
101
|
-
end
|
102
|
-
|
103
|
-
def parse_one_choice(choice)
|
104
|
-
message = choice['message']
|
105
|
-
return unless message # Skip if there's no message in this choice
|
106
|
-
|
107
|
-
# Accumulate raw tool calls if present
|
108
|
-
@tool_calls_raw.concat(message['tool_calls']) if message['tool_calls']
|
109
|
-
|
110
|
-
# Update result with the content if present
|
111
|
-
current_result = message['content']
|
112
|
-
@result = current_result if current_result.present?
|
113
|
-
|
114
|
-
# Update finish reason and truncation status based on the *last* choice's reason
|
115
|
-
current_finish_reason = choice['finish_reason']
|
116
|
-
if current_finish_reason.present?
|
117
|
-
@finish_reason = current_finish_reason
|
118
|
-
@is_truncated = (@finish_reason == 'length')
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
private
|
123
|
-
|
124
|
-
# Parses the accumulated raw tool calls into the structured @tool_calls array.
|
125
|
-
# This should only be called after confirming the response is not truncated.
|
126
|
-
def _parse_tool_calls
|
127
|
-
@tool_calls = [] # Ensure it's clean before parsing
|
128
|
-
@tool_calls_raw.each do |tool_call|
|
129
|
-
next unless tool_call['type'] == 'function' # Ensure it's a function call
|
130
|
-
|
131
|
-
function = tool_call['function']
|
132
|
-
next unless function && function['name'] && function['arguments']
|
133
|
-
|
134
|
-
begin
|
135
|
-
# Attempt to parse arguments, handle potential JSON errors
|
136
|
-
args = JSON.parse(function['arguments'], symbolize_names: true)
|
137
|
-
rescue JSON::ParserError => e
|
138
|
-
OxAiWorkers.logger.error("Failed to parse tool call arguments: #{e.message}", for: self.class)
|
139
|
-
OxAiWorkers.logger.debug("Raw arguments: #{function['arguments']}", for: self.class)
|
140
|
-
# Decide how to handle parsing errors, e.g., skip this call or add with empty args
|
141
|
-
# Skipping for now, as partial args are likely useless.
|
142
|
-
next
|
143
|
-
end
|
144
|
-
OxAiWorkers.logger.debug("function: #{function.inspect}", for: self.class)
|
145
|
-
# Accumulate parsed tool calls
|
146
|
-
next if function['name'].empty?
|
147
|
-
|
148
|
-
@tool_calls << {
|
149
|
-
class: function['name'].split('__').first,
|
150
|
-
name: function['name'].split('__').last,
|
151
|
-
args: args
|
152
|
-
}
|
153
|
-
@last_call = function['name'].to_s
|
72
|
+
@model.parse_response(response) do |result, is_truncated, tool_calls|
|
73
|
+
@result = result
|
74
|
+
@is_truncated = is_truncated
|
75
|
+
@tool_calls += tool_calls
|
154
76
|
end
|
77
|
+
@last_call = @tool_calls.last[:name] if @tool_calls.any?
|
155
78
|
end
|
156
79
|
end
|
157
80
|
end
|
data/lib/oxaiworkers/request.rb
CHANGED
@@ -17,8 +17,8 @@ module OxAiWorkers
|
|
17
17
|
required: true
|
18
18
|
property :result, type: 'string', description: I18n.t('oxaiworkers.tool.pipeline.send_message.result'),
|
19
19
|
required: true
|
20
|
-
property :example, type:
|
21
|
-
required:
|
20
|
+
property :example, type: 'string', description: I18n.t('oxaiworkers.tool.pipeline.send_message.example'),
|
21
|
+
required: false
|
22
22
|
property :to_id, type: 'string', description: I18n.t('oxaiworkers.tool.pipeline.send_message.to_id'),
|
23
23
|
required: true
|
24
24
|
end
|
@@ -19,8 +19,8 @@ module OxAiWorkers
|
|
19
19
|
property :prompt, type: 'string', description: I18n.t('oxaiworkers.tool.pixels.generate_image.prompt'),
|
20
20
|
required: true
|
21
21
|
if worker.sizes.length > 1
|
22
|
-
property :size, type:
|
23
|
-
enum: worker.sizes
|
22
|
+
property :size, type: 'string', description: I18n.t('oxaiworkers.tool.pixels.generate_image.size'),
|
23
|
+
enum: worker.sizes, required: false
|
24
24
|
end
|
25
25
|
if current_dir.present?
|
26
26
|
property :file_name, type: 'string',
|
@@ -28,8 +28,8 @@ module OxAiWorkers
|
|
28
28
|
required: true
|
29
29
|
end
|
30
30
|
if worker.qualities.length > 1
|
31
|
-
property :quality, type:
|
32
|
-
enum: worker.qualities
|
31
|
+
property :quality, type: 'string', description: I18n.t('oxaiworkers.tool.pixels.generate_image.quality'),
|
32
|
+
enum: worker.qualities, required: false
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
@@ -19,8 +19,8 @@ module OxAiWorkers
|
|
19
19
|
property :prompt, type: 'string', description: I18n.t('oxaiworkers.tool.wolfram_alpha.ask.prompt'),
|
20
20
|
required: true
|
21
21
|
if location.nil?
|
22
|
-
property :location, type:
|
23
|
-
required:
|
22
|
+
property :location, type: 'string', description: I18n.t('oxaiworkers.tool.wolfram_alpha.ask.location'),
|
23
|
+
required: false
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
@@ -109,7 +109,7 @@ module OxAiWorkers
|
|
109
109
|
name = function_name(method_name)
|
110
110
|
|
111
111
|
if block_given?
|
112
|
-
parameters = ParameterBuilder.new(parent_type: 'object'
|
112
|
+
parameters = ParameterBuilder.new(parent_type: 'object').build(&)
|
113
113
|
|
114
114
|
if parameters[:properties].empty?
|
115
115
|
raise ArgumentError,
|
@@ -118,11 +118,11 @@ module OxAiWorkers
|
|
118
118
|
else
|
119
119
|
# Create an empty parameters object with additionalProperties: false when strict is true
|
120
120
|
parameters = { type: 'object', properties: {} }
|
121
|
-
parameters[:additionalProperties] = false if strict
|
121
|
+
# parameters[:additionalProperties] = false if strict
|
122
122
|
end
|
123
123
|
|
124
124
|
function_params = { name:, description:, parameters: }
|
125
|
-
function_params[:strict] =
|
125
|
+
function_params[:strict] = strict
|
126
126
|
|
127
127
|
@schemas[method_name] = {
|
128
128
|
type: 'function',
|
@@ -133,36 +133,47 @@ module OxAiWorkers
|
|
133
133
|
# Converts schemas to OpenAI-compatible format
|
134
134
|
#
|
135
135
|
# @return [String] JSON string of schemas in OpenAI format
|
136
|
-
def to_openai_format
|
137
|
-
|
136
|
+
def to_openai_format
|
137
|
+
@schemas_openai ||= Marshal.load(Marshal.dump(valid_schemas.values))
|
138
|
+
@schemas_openai.map do |schema|
|
139
|
+
if schema[:function][:strict] == true
|
140
|
+
parameters = schema[:function][:parameters]
|
141
|
+
parameters[:additionalProperties] = false
|
142
|
+
parameters[:properties].each do |key, param|
|
143
|
+
param[:type] = parameters[:required].include?(key.to_s) ? param[:type] : [param[:type], 'null']
|
144
|
+
end
|
145
|
+
parameters[:required] = parameters[:properties].keys.map(&:to_s)
|
146
|
+
else
|
147
|
+
schema[:function].delete(:strict)
|
148
|
+
end
|
149
|
+
schema
|
150
|
+
end
|
138
151
|
end
|
139
152
|
|
140
153
|
# Returns a subset of schemas based on the provided filter.
|
141
154
|
#
|
142
155
|
# @param only [Array<Symbol>] An optional array of schema names to filter by.
|
143
156
|
# @return [Hash<Symbol, Hash>] A hash of schemas with their corresponding names as keys.
|
144
|
-
def valid_schemas
|
145
|
-
|
146
|
-
@schemas
|
147
|
-
else
|
148
|
-
@schemas.select { |name, _schema| only.include?(name) }
|
149
|
-
end
|
157
|
+
def valid_schemas
|
158
|
+
@schemas
|
150
159
|
end
|
151
160
|
|
152
161
|
# Converts schemas to Anthropic-compatible format
|
153
162
|
#
|
154
163
|
# @return [String] JSON string of schemas in Anthropic format
|
155
|
-
def to_anthropic_format
|
156
|
-
|
157
|
-
|
164
|
+
def to_anthropic_format
|
165
|
+
@schemas_anthropic ||= Marshal.load(Marshal.dump(valid_schemas.values))
|
166
|
+
@schemas_anthropic.map do |schema|
|
167
|
+
schema[:function].delete(:strict)
|
168
|
+
schema[:function].transform_keys(parameters: :input_schema)
|
158
169
|
end
|
159
170
|
end
|
160
171
|
|
161
172
|
# Converts schemas to Google Gemini-compatible format
|
162
173
|
#
|
163
174
|
# @return [String] JSON string of schemas in Google Gemini format
|
164
|
-
def to_google_gemini_format
|
165
|
-
valid_schemas
|
175
|
+
def to_google_gemini_format
|
176
|
+
valid_schemas.values.map { |schema| schema[:function] }
|
166
177
|
end
|
167
178
|
end
|
168
179
|
|
@@ -170,10 +181,9 @@ module OxAiWorkers
|
|
170
181
|
class ParameterBuilder
|
171
182
|
VALID_TYPES = %w[object array string number integer boolean null].freeze
|
172
183
|
|
173
|
-
def initialize(parent_type
|
184
|
+
def initialize(parent_type:)
|
174
185
|
@schema = parent_type == 'object' ? { type: 'object', properties: {}, required: [] } : {}
|
175
186
|
@parent_type = parent_type
|
176
|
-
@strict = strict
|
177
187
|
end
|
178
188
|
|
179
189
|
# Builds the parameter schema
|
@@ -200,7 +210,7 @@ module OxAiWorkers
|
|
200
210
|
prop = { type:, description:, enum: }.compact
|
201
211
|
|
202
212
|
if block_given?
|
203
|
-
nested_schema = ParameterBuilder.new(parent_type: type
|
213
|
+
nested_schema = ParameterBuilder.new(parent_type: type).build(&)
|
204
214
|
|
205
215
|
case type
|
206
216
|
when 'object'
|
@@ -222,7 +232,7 @@ module OxAiWorkers
|
|
222
232
|
if @parent_type == 'object'
|
223
233
|
@schema[:properties][name] = prop
|
224
234
|
@schema[:required] << name.to_s if required
|
225
|
-
@schema[:additionalProperties] = false if @strict
|
235
|
+
# @schema[:additionalProperties] = false if @strict
|
226
236
|
else
|
227
237
|
@schema = prop
|
228
238
|
end
|
@@ -246,7 +256,7 @@ module OxAiWorkers
|
|
246
256
|
raise ArgumentError, "Invalid name '#{name}'. Name must be a symbol" unless name.is_a?(Symbol)
|
247
257
|
end
|
248
258
|
|
249
|
-
unless VALID_TYPES.include?(type)
|
259
|
+
unless VALID_TYPES.include?(type)
|
250
260
|
raise ArgumentError, "Invalid type '#{type}'. Valid types are: #{VALID_TYPES.join(', ')}"
|
251
261
|
end
|
252
262
|
|
data/lib/oxaiworkers/version.rb
CHANGED