Class: OxAiWorkers::Iterator

Inherits:
StateTools show all
Includes:
LoadI18n, ToolDefinition
Defined in:
lib/oxaiworkers/iterator.rb

Constant Summary collapse

ITERATOR_FUNCTIONS =
i[inner_monologue outer_voice finish_it].freeze

Instance Attribute Summary collapse

Attributes included from LoadI18n

#locale

Attributes included from ToolDefinition

#white_list

Class Method Summary collapse

Instance Method Summary collapse

Methods included from LoadI18n

#store_locale, #with_locale

Methods included from ToolDefinition

#define_function, #full_function_name, #function_schemas, #init_white_list_with

Methods included from StateHelper

#log_me

Constructor Details

#initialize(worker:, role: nil, tools: [], on_inner_monologue: nil, on_outer_voice: nil, on_finish: nil, steps: nil, def_except: [], def_only: nil, locale: nil, call_stack: nil, stop_double_calls: []) ⇒ Iterator

Returns a new instance of Iterator.



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/oxaiworkers/iterator.rb', line 14

def initialize(worker:, role: nil, tools: [], on_inner_monologue: nil, on_outer_voice: nil,
               on_finish: nil, steps: nil, def_except: [], def_only: nil, locale: nil,
               call_stack: nil, stop_double_calls: [])

  @locale = locale || I18n.locale
  @call_id = 0

  @def_only = def_only || ITERATOR_FUNCTIONS
  @def_except = def_except

  init_white_list_with available_defs

  with_locale do
    define_function :inner_monologue, description: I18n.t('oxaiworkers.iterator.inner_monologue.description') do
      property :speach, type: 'string', description: I18n.t('oxaiworkers.iterator.inner_monologue.speach'),
                        required: true
    end

    define_function :outer_voice, description: I18n.t('oxaiworkers.iterator.outer_voice.description') do
      property :text, type: 'string', description: I18n.t('oxaiworkers.iterator.outer_voice.text'), required: true
    end

    define_function :finish_it, description: I18n.t('oxaiworkers.iterator.finish_it.description')

    @monologue = steps || I18n.t('oxaiworkers.iterator.monologue')
  end

  @worker = worker
  @tools = tools
  @role = role
  @context = []

  @on_inner_monologue = on_inner_monologue
  @on_outer_voice = on_outer_voice
  @on_finish = on_finish

  if call_stack&.any?
    if available_defs.include?(:inner_monologue) && !call_stack.include?(OxAiWorkers::Iterator.full_function_name(:inner_monologue))
      # Add inner_monologue first
      @call_stack = [OxAiWorkers::Iterator.full_function_name(:inner_monologue)] + call_stack
    end
    # Add finish_it last
    @call_stack = call_stack + [OxAiWorkers::Iterator.full_function_name(:finish_it)]
  end

  @stop_double_calls = [OxAiWorkers::Iterator.full_function_name(:inner_monologue),
                        OxAiWorkers::Iterator.full_function_name(:outer_voice)] + stop_double_calls

  cleanup

  super()

  tick_or_wait if requested?
end

Instance Attribute Details

#call_idObject

Returns the value of attribute call_id.



10
11
12
# File 'lib/oxaiworkers/iterator.rb', line 10

def call_id
  @call_id
end

#call_stackObject

Returns the value of attribute call_stack.



10
11
12
# File 'lib/oxaiworkers/iterator.rb', line 10

def call_stack
  @call_stack
end

#contextObject

Returns the value of attribute context.



10
11
12
# File 'lib/oxaiworkers/iterator.rb', line 10

def context
  @context
end

#def_exceptObject

Returns the value of attribute def_except.



10
11
12
# File 'lib/oxaiworkers/iterator.rb', line 10

def def_except
  @def_except
end

#def_onlyObject

Returns the value of attribute def_only.



10
11
12
# File 'lib/oxaiworkers/iterator.rb', line 10

def def_only
  @def_only
end

#messagesObject

Returns the value of attribute messages.



10
11
12
# File 'lib/oxaiworkers/iterator.rb', line 10

def messages
  @messages
end

#monologueObject

Returns the value of attribute monologue.



10
11
12
# File 'lib/oxaiworkers/iterator.rb', line 10

def monologue
  @monologue
end

#on_finishObject

Returns the value of attribute on_finish.



10
11
12
# File 'lib/oxaiworkers/iterator.rb', line 10

def on_finish
  @on_finish
end

#on_inner_monologueObject

Returns the value of attribute on_inner_monologue.



10
11
12
# File 'lib/oxaiworkers/iterator.rb', line 10

def on_inner_monologue
  @on_inner_monologue
end

#on_outer_voiceObject

Returns the value of attribute on_outer_voice.



10
11
12
# File 'lib/oxaiworkers/iterator.rb', line 10

def on_outer_voice
  @on_outer_voice
end

#queueObject

Returns the value of attribute queue.



10
11
12
# File 'lib/oxaiworkers/iterator.rb', line 10

def queue
  @queue
end

#roleObject

Returns the value of attribute role.



10
11
12
# File 'lib/oxaiworkers/iterator.rb', line 10

def role
  @role
end

#stop_double_callsObject

Returns the value of attribute stop_double_calls.



10
11
12
# File 'lib/oxaiworkers/iterator.rb', line 10

def stop_double_calls
  @stop_double_calls
end

#tasksObject

Returns the value of attribute tasks.



10
11
12
# File 'lib/oxaiworkers/iterator.rb', line 10

def tasks
  @tasks
end

#toolsObject

Returns the value of attribute tools.



10
11
12
# File 'lib/oxaiworkers/iterator.rb', line 10

def tools
  @tools
end

#workerObject

Returns the value of attribute worker.



10
11
12
# File 'lib/oxaiworkers/iterator.rb', line 10

def worker
  @worker
end

Class Method Details

.full_function_name(fun) ⇒ Object



169
170
171
172
173
174
175
176
# File 'lib/oxaiworkers/iterator.rb', line 169

def self.full_function_name(fun)
  tool_name ||= name
                .gsub('::', '_')
                .gsub(/(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-z\d])(?=[A-Z])/, '_')
                .downcase

  "#{tool_name}__#{fun}"
end

Instance Method Details

#add_context(text, role: :user) ⇒ Object



295
296
297
# File 'lib/oxaiworkers/iterator.rb', line 295

def add_context(text, role: :user)
  add_raw_context({ role:, content: text })
end

#add_file(pdf:, text:, role: :user) ⇒ Object



299
300
301
302
# File 'lib/oxaiworkers/iterator.rb', line 299

def add_file(pdf:, text:, role: :user)
  content = @worker.model.add_base64(binary: pdf, text:, mime_type: 'application/pdf')
  add_raw_context({ role:, content: })
end

#add_image(text:, url: nil, binary: nil, role: :user, mime_type: 'image/png') ⇒ Object



304
305
306
307
308
309
310
311
312
313
# File 'lib/oxaiworkers/iterator.rb', line 304

def add_image(text:, url: nil, binary: nil, role: :user, mime_type: 'image/png')
  content = []
  if binary.present?
    content = @worker.model.add_base64(binary:, text:, mime_type:)
  elsif url.present?
    content = @worker.model.add_url(url:, text:, mime_type:)
  end

  add_raw_context({ role:, content: })
end

#add_queue(text, role: :assistant) ⇒ Object



291
292
293
# File 'lib/oxaiworkers/iterator.rb', line 291

def add_queue(text, role: :assistant)
  @queue << { role:, content: text }
end

#add_raw_context(c) ⇒ Object



315
316
317
# File 'lib/oxaiworkers/iterator.rb', line 315

def add_raw_context(c)
  @context << c
end

#add_task(task) ⇒ Object



287
288
289
# File 'lib/oxaiworkers/iterator.rb', line 287

def add_task(task)
  @tasks << task
end

#available_defsObject



153
154
155
# File 'lib/oxaiworkers/iterator.rb', line 153

def available_defs
  @def_only - @def_except
end

#cancelObject



327
328
329
# File 'lib/oxaiworkers/iterator.rb', line 327

def cancel
  @worker.cancel if @worker.respond_to?(:cancel)
end

#cleanupObject

Resets the state of the object by setting all instance variables to their initial values.

Returns nothing.



74
75
76
77
78
79
80
81
82
# File 'lib/oxaiworkers/iterator.rb', line 74

def cleanup
  @queue = []
  @tasks = []
  @messages = []
  @call_id = 0
  # Clear messages in worker if it exists
  @worker.messages = [] if @worker&.respond_to?(:messages=)
  complete_iteration
end

#clear_contextObject



319
320
321
# File 'lib/oxaiworkers/iterator.rb', line 319

def clear_context
  @context = []
end

#complete_iterationObject



281
282
283
284
285
# File 'lib/oxaiworkers/iterator.rb', line 281

def complete_iteration
  @queue = []
  # Use finish_without_cleanup instead of finish
  @worker.finish_without_cleanup if @worker.respond_to?(:finish_without_cleanup)
end

#executeObject



323
324
325
# File 'lib/oxaiworkers/iterator.rb', line 323

def execute
  prepare! if valid?
end

#external_requestObject



206
207
208
209
210
211
212
213
214
215
# File 'lib/oxaiworkers/iterator.rb', line 206

def external_request
  @worker.request!
  tick_or_wait
rescue Faraday::ServerError => e
  OxAiWorkers.logger.warn "Iterator::ServerError #{e.message}. Waiting 10 seconds..."
  sleep(10)
  external_request
rescue Faraday::BadRequestError => e
  OxAiWorkers.logger.warn "Iterator::BadRequestError #{e.message}."
end

#finish_itObject



111
112
113
114
115
# File 'lib/oxaiworkers/iterator.rb', line 111

def finish_it
  complete! if can_complete?
  @on_finish&.call
  nil
end

#initObject



117
118
119
120
# File 'lib/oxaiworkers/iterator.rb', line 117

def init
  rebuild_worker
  request!
end

#inner_monologue(speach:) ⇒ nil

Updates the internal state of the iterator by adding the given speach to the @queue and calling the @on_inner_monologue callback with the speach text.

Parameters:

  • speach (String)

    The text to be added to the @queue and passed to the @on_inner_monologue callback.

Returns:

  • (nil)

    This method does not return a value.



89
90
91
92
93
94
95
96
97
# File 'lib/oxaiworkers/iterator.rb', line 89

def inner_monologue(speach:)
  if available_defs.include?(:inner_monologue)
    @queue << { role: :assistant, content: speach.to_s }
    @on_inner_monologue&.call(text: speach)
  else
    OxAiWorkers.logger.warn "Iterator::inner_monologue is not available: #{speach}"
  end
  speach
end

#next_iterationObject



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/oxaiworkers/iterator.rb', line 178

def next_iteration
  # Check call_stack before continuing iteration
  if should_finish_iteration?
    OxAiWorkers.logger.info 'Iterator::Call stack is empty or contains only finish_it. Finishing iteration.'
    finish_it
    return
  end

  # First add messages from queue to worker
  @worker.append(messages: @queue)
  # Then add them to local messages
  @messages += @queue
  OxAiWorkers.logger.warn "Iterator::Next iteration: #{@messages.count}/#{@queue.count}"
  # And only then clear the queue
  @queue = []
  request!
end

#outer_voice(text:) ⇒ Object



99
100
101
102
103
104
105
106
107
108
109
# File 'lib/oxaiworkers/iterator.rb', line 99

def outer_voice(text:)
  if available_defs.include?(:outer_voice)
    @queue << { role: :assistant, content: text.to_s }
    @on_outer_voice&.call(text:)
  else
    OxAiWorkers.logger.warn "Iterator::outer_voice is not available: #{text}"
    inner_monologue(speach: text)
  end

  text
end

#process_result(_transition) ⇒ Object



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/oxaiworkers/iterator.rb', line 238

def process_result(_transition)
  # If the response is truncated due to max_tokens, repeat the request
  if @worker.is_truncated && @worker.result.present?
    # Save the partial response and continue the dialogue with AI
    OxAiWorkers.logger.info(
      "Truncated response detected (finish_reason: #{@worker.finish_reason}). Repeating request to get complete response.", for: self.class
    )
    @queue << { role: :assistant, content: @worker.result }
    # Request continuation
    next_iteration
    return
  end

  if @worker.tool_calls.present?
    # @queue << { role: :assistant, content: @worker.tool_calls_raw.to_s }
    @worker.tool_calls.each do |external_call|
      tool = ([self] + @tools).select do |t|
        tool_name = t.respond_to?(:tool_name) ? t.tool_name : t.class.tool_name
        tool_name == external_call[:class] && t.respond_to?(external_call[:name])
      end.first
      next if tool.nil?

      @call_id += 1
      # Add tool call message in the correct format
      OxAiWorkers.logger.info "Iterator::Tool call: #{@call_id}"
      out = tool.send(external_call[:name], **external_call[:args])
      @queue += @worker.model.tool_call(
        name: external_call[:name],
        args: external_call[:args],
        call_id: @call_id,
        out:
      )
    end
    @worker.finish_without_cleanup if @worker.respond_to?(:finish_without_cleanup)
    iterate! if can_iterate?
  end
  result = @worker.result || @worker.errors
  if result.present?
    OxAiWorkers.logger.warn "Iterator::No tool calls: #{result}"
    outer_voice text: result 
  end
end

#rebuild_workerObject



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/oxaiworkers/iterator.rb', line 122

def rebuild_worker
  # @worker.last_call = nil
  @worker.call_stack = @call_stack.dup
  @worker.stop_double_calls = @stop_double_calls
  # Don't clear messages, save them in a variable
  current_messages = @worker.messages || []
  @worker.messages = []
  @worker.append(role: :system, content: "<role>\n#{@role}\n</role>") if @role.present?

  @worker.append(role: :system, content: "<instructions>\n#{valid_monologue.join("\n")}\n</instructions>")
  @tasks.each { |task| @worker.append(role: :user, content: "<task>\n#{task}\n</task>") }
  @worker.append(messages: @context) if @context.present?
  @tools.each do |tool|
    @worker.append(role: :user, content: tool.context) if tool.respond_to?(:context) && tool.context.present?
  end
  # Add saved messages back if they are not empty
  @worker.append(messages: current_messages) if current_messages.present?
  @worker.append(messages: @messages)
  # @tasks.each { |task| @worker.append(role: :user, content: "<task>\n#{task}\n</task>") }
  @worker.tools = [function_schemas]
  return unless @tools.present?

  @worker.tools += @tools.map do |tool|
    if tool.respond_to?(:function_schemas)
      tool.function_schemas
    else
      tool.class.function_schemas
    end
  end.flatten
end

#should_finish_iteration?Boolean

Returns:

  • (Boolean)


196
197
198
199
200
201
202
203
204
# File 'lib/oxaiworkers/iterator.rb', line 196

def should_finish_iteration?
  return false if @worker.call_stack.nil?
  return false unless @worker.respond_to?(:call_stack) && @worker.call_stack.present?

  finish_it_function = OxAiWorkers::Iterator.full_function_name(:finish_it)

  # Check if the call queue is empty or contains only finish_it
  @worker.call_stack.empty? || @worker.call_stack.all? { |call| call == finish_it_function }
end

#tick_or_waitObject



217
218
219
220
221
222
223
# File 'lib/oxaiworkers/iterator.rb', line 217

def tick_or_wait
  if OxAiWorkers.configuration.wait_for_complete
    wait_for_complete
  else
    ticker
  end
end

#tickerObject



225
226
227
228
229
230
# File 'lib/oxaiworkers/iterator.rb', line 225

def ticker
  return false unless @worker.completed?

  analyze!
  true
end

#tool_nameObject



162
163
164
165
166
167
# File 'lib/oxaiworkers/iterator.rb', line 162

def tool_name
  @tool_name ||= (respond_to?(:name) ? name : self.class.name)
                 .gsub('::', '_')
                 .gsub(/(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-z\d])(?=[A-Z])/, '_')
                 .downcase
end

#valid?Boolean

Returns:

  • (Boolean)


331
332
333
# File 'lib/oxaiworkers/iterator.rb', line 331

def valid?
  @messages.present? || @tasks.present?
end

#valid_monologueObject



157
158
159
160
# File 'lib/oxaiworkers/iterator.rb', line 157

def valid_monologue
  arr = @monologue.reject { |item| @def_except.any? { |fun| item.include?(full_function_name(fun)) } }
  arr.each_with_index.map { |item, index| format(item, index + 1) }
end

#wait_for_completeObject



232
233
234
235
236
# File 'lib/oxaiworkers/iterator.rb', line 232

def wait_for_complete
  return unless requested?

  sleep(60) unless ticker
end