Class: OxAiWorkers::Iterator
- Inherits:
-
StateTools
- Object
- StateTools
- OxAiWorkers::Iterator
- 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
-
#call_id ⇒ Object
Returns the value of attribute call_id.
-
#call_stack ⇒ Object
Returns the value of attribute call_stack.
-
#context ⇒ Object
Returns the value of attribute context.
-
#def_except ⇒ Object
Returns the value of attribute def_except.
-
#def_only ⇒ Object
Returns the value of attribute def_only.
-
#messages ⇒ Object
Returns the value of attribute messages.
-
#monologue ⇒ Object
Returns the value of attribute monologue.
-
#on_finish ⇒ Object
Returns the value of attribute on_finish.
-
#on_inner_monologue ⇒ Object
Returns the value of attribute on_inner_monologue.
-
#on_outer_voice ⇒ Object
Returns the value of attribute on_outer_voice.
-
#queue ⇒ Object
Returns the value of attribute queue.
-
#role ⇒ Object
Returns the value of attribute role.
-
#stop_double_calls ⇒ Object
Returns the value of attribute stop_double_calls.
-
#tasks ⇒ Object
Returns the value of attribute tasks.
-
#tools ⇒ Object
Returns the value of attribute tools.
-
#worker ⇒ Object
Returns the value of attribute worker.
Attributes included from LoadI18n
Attributes included from ToolDefinition
Class Method Summary collapse
Instance Method Summary collapse
- #add_context(text, role: :user) ⇒ Object
- #add_file(pdf:, text:, role: :user) ⇒ Object
- #add_image(text:, url: nil, binary: nil, role: :user, mime_type: 'image/png') ⇒ Object
- #add_queue(text, role: :assistant) ⇒ Object
- #add_raw_context(c) ⇒ Object
- #add_task(task) ⇒ Object
- #available_defs ⇒ Object
- #cancel ⇒ Object
-
#cleanup ⇒ Object
Resets the state of the object by setting all instance variables to their initial values.
- #clear_context ⇒ Object
- #complete_iteration ⇒ Object
- #execute ⇒ Object
- #external_request ⇒ Object
- #finish_it ⇒ Object
- #init ⇒ Object
-
#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
constructor
A new instance of Iterator.
-
#inner_monologue(speach:) ⇒ nil
Updates the internal state of the iterator by adding the given
speachto the@queueand calling the@on_inner_monologuecallback with thespeachtext. - #next_iteration ⇒ Object
- #outer_voice(text:) ⇒ Object
- #process_result(_transition) ⇒ Object
- #rebuild_worker ⇒ Object
- #should_finish_iteration? ⇒ Boolean
- #tick_or_wait ⇒ Object
- #ticker ⇒ Object
- #tool_name ⇒ Object
- #valid? ⇒ Boolean
- #valid_monologue ⇒ Object
- #wait_for_complete ⇒ Object
Methods included from LoadI18n
Methods included from ToolDefinition
#define_function, #full_function_name, #function_schemas, #init_white_list_with
Methods included from StateHelper
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_id ⇒ Object
Returns the value of attribute call_id.
10 11 12 |
# File 'lib/oxaiworkers/iterator.rb', line 10 def call_id @call_id end |
#call_stack ⇒ Object
Returns the value of attribute call_stack.
10 11 12 |
# File 'lib/oxaiworkers/iterator.rb', line 10 def call_stack @call_stack end |
#context ⇒ Object
Returns the value of attribute context.
10 11 12 |
# File 'lib/oxaiworkers/iterator.rb', line 10 def context @context end |
#def_except ⇒ Object
Returns the value of attribute def_except.
10 11 12 |
# File 'lib/oxaiworkers/iterator.rb', line 10 def def_except @def_except end |
#def_only ⇒ Object
Returns the value of attribute def_only.
10 11 12 |
# File 'lib/oxaiworkers/iterator.rb', line 10 def def_only @def_only end |
#messages ⇒ Object
Returns the value of attribute messages.
10 11 12 |
# File 'lib/oxaiworkers/iterator.rb', line 10 def end |
#monologue ⇒ Object
Returns the value of attribute monologue.
10 11 12 |
# File 'lib/oxaiworkers/iterator.rb', line 10 def monologue @monologue end |
#on_finish ⇒ Object
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_monologue ⇒ Object
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_voice ⇒ Object
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 |
#queue ⇒ Object
Returns the value of attribute queue.
10 11 12 |
# File 'lib/oxaiworkers/iterator.rb', line 10 def queue @queue end |
#role ⇒ Object
Returns the value of attribute role.
10 11 12 |
# File 'lib/oxaiworkers/iterator.rb', line 10 def role @role end |
#stop_double_calls ⇒ Object
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 |
#tasks ⇒ Object
Returns the value of attribute tasks.
10 11 12 |
# File 'lib/oxaiworkers/iterator.rb', line 10 def tasks @tasks end |
#tools ⇒ Object
Returns the value of attribute tools.
10 11 12 |
# File 'lib/oxaiworkers/iterator.rb', line 10 def tools @tools end |
#worker ⇒ Object
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_defs ⇒ Object
153 154 155 |
# File 'lib/oxaiworkers/iterator.rb', line 153 def available_defs @def_only - @def_except end |
#cancel ⇒ Object
327 328 329 |
# File 'lib/oxaiworkers/iterator.rb', line 327 def cancel @worker.cancel if @worker.respond_to?(:cancel) end |
#cleanup ⇒ Object
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 = [] = [] @call_id = 0 # Clear messages in worker if it exists @worker. = [] if @worker&.respond_to?(:messages=) complete_iteration end |
#clear_context ⇒ Object
319 320 321 |
# File 'lib/oxaiworkers/iterator.rb', line 319 def clear_context @context = [] end |
#complete_iteration ⇒ Object
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 |
#execute ⇒ Object
323 324 325 |
# File 'lib/oxaiworkers/iterator.rb', line 323 def execute prepare! if valid? end |
#external_request ⇒ Object
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_it ⇒ Object
111 112 113 114 115 |
# File 'lib/oxaiworkers/iterator.rb', line 111 def finish_it complete! if can_complete? @on_finish&.call nil end |
#init ⇒ Object
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.
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_iteration ⇒ Object
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 += @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_worker ⇒ Object
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 = @worker. || [] @worker. = [] @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: ) if .present? @worker.append(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
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_wait ⇒ Object
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 |
#ticker ⇒ Object
225 226 227 228 229 230 |
# File 'lib/oxaiworkers/iterator.rb', line 225 def ticker return false unless @worker.completed? analyze! true end |
#tool_name ⇒ Object
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
331 332 333 |
# File 'lib/oxaiworkers/iterator.rb', line 331 def valid? .present? || @tasks.present? end |
#valid_monologue ⇒ Object
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_complete ⇒ Object
232 233 234 235 236 |
# File 'lib/oxaiworkers/iterator.rb', line 232 def wait_for_complete return unless requested? sleep(60) unless ticker end |