Class: OllamaChat::ToolCallProcessor
- Inherits:
-
Object
- Object
- OllamaChat::ToolCallProcessor
- Includes:
- Ollama::Handlers::Concern, Feedback, Term::ANSIColor
- Defined in:
- lib/ollama_chat/tool_call_processor.rb
Overview
Processes tool calls from Ollama responses: executes registered tools, collects results, and provides a drain interface for the chat loop to consume them as follow-up messages.
This class encapsulates the full tool-call lifecycle that was previously split between FollowChat#handle_tool_calls (execution) and OllamaChat::ToolCalling#handle_tool_call_results? (consumption).
Constant Summary
Constants included from Feedback
Instance Attribute Summary collapse
-
#chat ⇒ OllamaChat::Chat
readonly
The chat context.
-
#results ⇒ Hash{String => Array<String>}
readonly
The accumulated tool results, keyed by tool name.
Instance Method Summary collapse
-
#clear! ⇒ Object
Clears all accumulated results without yielding them.
-
#drain {|index, tool_name, content| ... } ⇒ true, false
Drains all accumulated results, yielding each entry to the block, then clears the internal storage.
-
#handle(response) ⇒ Object
Processes tool calls from a response, executing each tool and storing the result in the internal results hash.
-
#initialize(chat:, output: STDOUT) ⇒ ToolCallProcessor
constructor
A new instance of ToolCallProcessor.
Methods included from Feedback
Constructor Details
#initialize(chat:, output: STDOUT) ⇒ ToolCallProcessor
Returns a new instance of ToolCallProcessor.
23 24 25 26 27 |
# File 'lib/ollama_chat/tool_call_processor.rb', line 23 def initialize(chat:, output: STDOUT) super(output:) @chat = chat @results = Hash.new { |h, name| h[name] = [] } end |
Instance Attribute Details
#chat ⇒ OllamaChat::Chat (readonly)
Returns the chat context.
14 15 16 |
# File 'lib/ollama_chat/tool_call_processor.rb', line 14 def chat @chat end |
#results ⇒ Hash{String => Array<String>} (readonly)
The accumulated tool results, keyed by tool name.
19 20 21 |
# File 'lib/ollama_chat/tool_call_processor.rb', line 19 def results @results end |
Instance Method Details
#clear! ⇒ Object
Clears all accumulated results without yielding them.
173 174 175 |
# File 'lib/ollama_chat/tool_call_processor.rb', line 173 def clear! @results.clear end |
#drain {|index, tool_name, content| ... } ⇒ true, false
Drains all accumulated results, yielding each entry to the block, then clears the internal storage.
162 163 164 165 166 167 168 169 170 |
# File 'lib/ollama_chat/tool_call_processor.rb', line 162 def drain(&block) @results.each do |tool_name, results| results.each_with_index { |content, index| block.(index, tool_name, content) } end.present? ensure @results.clear end |
#handle(response) ⇒ Object
Processes tool calls from a response, executing each tool and storing the result in the internal results hash.
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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 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 152 153 154 155 |
# File 'lib/ollama_chat/tool_call_processor.rb', line 33 def handle(response) return unless response..ask_and_send(:tool_calls) tools_used = {} response..tool_calls.each do |tool_call| name = tool_call.function.name if %r(.*[/:](?<new_name>.+)) =~ name msg = "Received namespaced tool call for #{name}, correcting to #{new_name}" chat.log(:warn, msg, data: { tool: name, new_name: }) name = new_name end unless chat.tool_configured?(name) msg = "Error: Unconfigured tool named %s ignored => Skip.\n" % name @results[name] << msg chat.log(:error, msg, data: { tool: name, reason: :unconfigured }) next end unless chat.tool_registered?(name) msg = "Error: Unregistered tool named %s ignored => Skip.\n" % name @results[name] << msg chat.log(:error, msg, data: { tool: name, reason: :unregistered }) next end unless chat.tool_enabled?(name) msg = "Error: Disabled tool named %s ignored => Skip.\n" % name @results[name] << msg chat.log(:error, msg, data: { tool: name, reason: :disabled }) next end feedback(?\n, output:) confirmed = :implicit resolve = nil function = JSON.pretty_generate(tool_call.function) chat.log(:info, "Tool call received", data: { tool: name, function: }) if chat.tool_function(name).require_confirmation? feedback( ("I want to execute tool %s\n%s\n" % [ bold { name }, italic { function }, ]), type: :alarm ) prompt = '❓ Allow ✅[y]es / ⛔️[n]o / 📝[i]nstruct? ' answer = chat.confirm?(prompt:)&.to_s&.downcase resolve = 'You **MUST** ask the user for instructions on how to proceed!!!' case answer when 'y' confirmed = :explicit when 'n' confirmed = :denied else confirmed = :denied instr = chat.ask?(prompt: '📝 Instructions: ')&.strip resolve = instr.full? || resolve end else feedback( "Executing tool %s\n%s" % [ bold { name }, italic { function }, ] ) end start = Time.now result = nil case confirmed when :denied result = JSON(message: 'User denied confirmation!', resolve:) feedback(?\n) feedback( ("Execution of tool %s denied by user.\n\n" % [ bold { name } ]), type: :denied ) chat.log(:warn, "Tool execution denied", data: { tool: name, reason: :denied }) else symbol = confirmed == :implicit ? '☑️ ' : '✅' feedback( ("\n%s Execution of tool %s confirmed.\n\n" % [ symbol, bold { name } ]) ) result = OllamaChat::Tools.registered_tool(name, chat:).execute(tool_call) chat.log(:info, "Tool execution confirmed", data: { tool: name, confirmed: }) end @results[name] << result data = nil = begin data = JSON.parse(result) chat.log(:info, "Tool result", data: { result: data }) data.to_hash['message'] if data.respond_to?(:to_hash) rescue chat.log(:info, "Tool result", data: { result: }) nil end warn = begin !!data.ask_and_send(:[], 'error') || data.ask_and_send(:[], 'success') == false rescue false end es = OllamaChat::TokenEstimator.estimate(result.to_s) tools_used[name] = { message:, warn: , size: es.bytes_formatted, tokens: es.tokens_formatted, duration: Tins::Duration.new(Time.now - start).to_s, } end if tools_used.full? .reset tools_used.each do |name, info| = if info[:message] "\n%s %s\n\n" % [ info[:warn] ? '⚠️' : '💡', info[:message] ] end feedback(<<~EOT.strip) 🔧 Tool functions #{name} returned result (#{info[:size]}/#{info[:tokens]} in #{info[:duration]}). #{} EOT feedback(?\n) timeout = chat.tool_function(name).result_display_timeout? chat.confirm?(prompt: '⏎ Press any key to continue (%s). ', timeout:) end end end |