Class: OllamaChat::ToolCallProcessor

Inherits:
Object
  • Object
show all
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

Feedback::PREFIXES

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Feedback

#feedback

Constructor Details

#initialize(chat:, output: STDOUT) ⇒ ToolCallProcessor

Returns a new instance of ToolCallProcessor.

Parameters:

  • chat (OllamaChat::Chat) —

    the chat context

  • output (IO) (defaults to: STDOUT) —

    the output stream for feedback (default: STDOUT)



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.

Returns:



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.

Returns:

  • (Hash{String => Array<String>})


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.

Yields:

  • (index, tool_name, content) —

    each result entry

Returns:

  • (true, false) —

    whether any results were present



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.

Parameters:

  • response (Ollama::Response) —

    the parsed response



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.message.ask_and_send(:tool_calls)

  tools_used = {}

  response.message.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
    message = 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?
    infobar.reset
    tools_used.each do |name, info|
      feedback_message =
        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]}).
        #{feedback_message}
      EOT
      feedback(?\n)
      timeout = chat.tool_function(name).result_display_timeout?
      chat.confirm?(prompt: '⏎  Press any key to continue (%s). ', timeout:)
    end
  end
end