Class: OllamaChat::FollowChat
- Inherits:
-
Object
- Object
- OllamaChat::FollowChat
- Includes:
- Ollama, Ollama::Handlers::Concern, MessageFormat, Utils::ValueFormatter, Term::ANSIColor
- Defined in:
- lib/ollama_chat/follow_chat.rb
Overview
A class that handles chat responses and manages the flow of conversation between the user and Ollama models.
This class is responsible for processing Ollama API responses, updating message history, displaying formatted output to the terminal, and managing voice synthesis for spoken responses. It acts as a handler for streaming responses and ensures proper formatting and display of both regular content and thinking annotations.
Instance Attribute Summary collapse
-
#chat ⇒ Object
readonly
Returns the value of attribute chat.
-
#messages ⇒ OllamaChat::MessageList<OllamaChat::Message>
readonly
Returns the conversation history (an array of message objects).
Instance Method Summary collapse
-
#call(response) ⇒ OllamaChat::FollowChat
Invokes the chat flow based on the provided Ollama server response.
-
#debug_output(response) ⇒ Object
private
The debug_output method conditionally outputs the response object using jj when debugging is enabled.
-
#display_formatted_terminal_output(output = nil) ⇒ Object
private
The display_formatted_terminal_output method formats and outputs the terminal content by processing the last message's content and thinking, then prints it to the output.
-
#display_output ⇒ nil, String
private
The display_output method shows the last message in the conversation.
-
#ensure_assistant_response_exists ⇒ Object
private
The ensure_assistant_response_exists method ensures that the last message in the conversation is from the assistant role.
-
#eval_stats(response) ⇒ String
private
The eval_stats method processes response statistics and formats them into a colored, readable string output.
-
#handle_tool_calls(response) ⇒ Object
private
The handle_tool_calls method processes tool calls from a response and executes them.
-
#initialize(chat:, messages:, voice: nil, output: STDOUT) ⇒ OllamaChat::FollowChat
constructor
Initializes a new instance of OllamaChat::FollowChat.
-
#last_message_with_user ⇒ Array
private
The last_message_with_user method constructs a formatted message array by combining user information, newline characters, thinking annotations, and content for display in the terminal output.
-
#output_eval_stats(response) ⇒ Object
private
The output_eval_stats method outputs evaluation statistics to the specified output stream.
-
#prepare_last_message ⇒ Array<String, String>, Array<String, nil>
private
The prepare_last_message method processes and formats content and thinking annotations for display.
-
#truncate_for_terminal(text, max_lines: Tins::Terminal.lines) ⇒ String
private
The truncate_for_terminal method processes text to fit within a specified number of lines.
-
#update_last_message(response) ⇒ Object
private
The update_last_message method appends the content of a response to the last message in the conversation.
Methods included from Utils::ValueFormatter
Methods included from MessageFormat
#display_sender, #message_type, #role_color, #role_template, #sender_name_displayed, #talk_annotate, #think_annotate
Constructor Details
#initialize(chat:, messages:, voice: nil, output: STDOUT) ⇒ OllamaChat::FollowChat
Initializes a new instance of OllamaChat::FollowChat.
27 28 29 30 31 32 33 34 |
# File 'lib/ollama_chat/follow_chat.rb', line 27 def initialize(chat:, messages:, voice: nil, output: STDOUT) super(output:) @chat = chat @output.sync = true @say = voice ? Handlers::Say.new(voice:) : NOP @messages = @sender = nil end |
Instance Attribute Details
#chat ⇒ Object (readonly)
Returns the value of attribute chat.
36 37 38 |
# File 'lib/ollama_chat/follow_chat.rb', line 36 def chat @chat end |
#messages ⇒ OllamaChat::MessageList<OllamaChat::Message> (readonly)
Returns the conversation history (an array of message objects).
42 43 44 |
# File 'lib/ollama_chat/follow_chat.rb', line 42 def @messages end |
Instance Method Details
#call(response) ⇒ OllamaChat::FollowChat
Invokes the chat flow based on the provided Ollama server response.
The response is expected to be a parsed JSON object containing information about the user input and the assistant's response.
If the response indicates an assistant message, this method:
1. Ensures that an assistant response exists in the message history (if
not already present).
2. Updates the last message with the new content and thinking (if
applicable).
3. Displays the formatted terminal output for the user.
4. Outputs the voice response (if configured).
Regardless of whether an assistant message is present, this method also outputs evaluation statistics (if applicable).
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/ollama_chat/follow_chat.rb', line 64 def call(response) debug_output(response) if response&.&.role == 'assistant' ensure_assistant_response_exists (response) if chat.stream.on? display_formatted_terminal_output else if display_output display_formatted_terminal_output end end @say.call(response) end output_eval_stats(response) handle_tool_calls(response) self end |
#debug_output(response) ⇒ Object (private)
The debug_output method conditionally outputs the response object using jj when debugging is enabled.
388 389 390 |
# File 'lib/ollama_chat/follow_chat.rb', line 388 def debug_output(response) chat.debug and chat.log(:debug, response.to_json) end |
#display_formatted_terminal_output(output = nil) ⇒ Object (private)
The display_formatted_terminal_output method formats and outputs the terminal content by processing the last message's content and thinking, then prints it to the output. It handles markdown parsing and annotation based on chat settings, and ensures proper formatting with clear screen and move home commands. The method takes into account whether markdown and thinking modes are enabled to determine how to process and display the content.
326 327 328 329 |
# File 'lib/ollama_chat/follow_chat.rb', line 326 def display_formatted_terminal_output(output = nil) output ||= @output output.print(*([ clear_screen, move_home, * ].compact)) end |
#display_output ⇒ nil, String (private)
The display_output method shows the last message in the conversation.
This method delegates to the messages object's show_last method, which displays the most recent non-user message in the conversation history. It is typically used to provide message to the user about the last response from the assistant.
339 340 341 342 343 344 345 346 347 |
# File 'lib/ollama_chat/follow_chat.rb', line 339 def display_output @messages.use_pager do |output| if chat.markdown.on? display_formatted_terminal_output(output) else output.print(*) end end end |
#ensure_assistant_response_exists ⇒ Object (private)
The ensure_assistant_response_exists method ensures that the last message in the conversation is from the assistant role.
If the last message is not from an assistant, it adds a new assistant message with empty content and optionally includes thinking content if the chat's think mode is enabled. It also updates the sender display variable to reflect the assistant's message type and styling.
249 250 251 252 253 254 255 256 257 258 259 260 |
# File 'lib/ollama_chat/follow_chat.rb', line 249 def ensure_assistant_response_exists if @messages&.last&.role != 'assistant' = Message.new( role: 'assistant', sender_name: chat.assistant, content: '', thinking: ('' if chat.think?) ) @messages << @sender = display_sender() end end |
#eval_stats(response) ⇒ String (private)
The eval_stats method processes response statistics and formats them into a colored, readable string output.
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 |
# File 'lib/ollama_chat/follow_chat.rb', line 357 def eval_stats(response) eval_duration = response.eval_duration.to_f / 1e9 prompt_eval_duration = response.prompt_eval_duration.to_f / 1e9 stats_text = { eval_duration: Tins::Duration.new(eval_duration), eval_count: response.eval_count.to_i, eval_rate: bold { "%.2f t/s" % (response.eval_count.to_i / eval_duration) } + color(111), prompt_eval_duration: Tins::Duration.new(prompt_eval_duration), prompt_eval_count: response.prompt_eval_count.to_i, prompt_eval_rate: bold { "%.2f t/s" % (response.prompt_eval_count.to_i / prompt_eval_duration) } + color(111), total_duration: Tins::Duration.new(response.total_duration / 1e9), load_duration: Tins::Duration.new(response.load_duration / 1e9), }.map { _1 * ?= } * ' ' '📊 ' + color(111) { Kramdown::ANSI::Width.wrap(stats_text, percentage: 90).gsub(/(?<!\A)^/, ' ') } end |
#handle_tool_calls(response) ⇒ Object (private)
The handle_tool_calls method processes tool calls from a response and executes them.
This method checks if the response contains tool calls, and if so, iterates through each tool call to execute the corresponding tool from the registered tools. The results of the tool execution are stored in the chat's tool_call_results hash using the tool name as the key.
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 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/ollama_chat/follow_chat.rb', line 99 def handle_tool_calls(response) return unless response..ask_and_send(:tool_calls) tools_used = {} response..tool_calls.each do |tool_call| name = tool_call.function.name if name =~ %r(/) new_name = File.basename(name.to_s) msg = "Received namespaced tool call for #{name}, correcting to #{new_name}" chat.log(:warn, msg) name = new_name end unless chat.tool_configured?(name) msg = "Error: Unconfigured tool named %s ignored => Skip.\n" % name chat.tool_call_results[name] << msg chat.log(:error, msg) next end unless chat.tool_registered?(name) msg = "Error: Unregistered tool named %s ignored => Skip.\n" % name chat.tool_call_results[name] << msg chat.log(:error, msg) next end unless chat.tool_enabled?(name) msg = "Error: Disabled tool named %s ignored => Skip.\n" % name chat.tool_call_results[name] << msg chat.log(:error, msg) next end STDOUT.puts confirmed = :implicit function = JSON.pretty_generate(tool_call.function) chat.log(:info, function) if chat.tool_function(name).require_confirmation? prompt = "🔔 I want to execute tool %s\n%s\nConfirm? (y/n) " % [ bold { name }, italic { function }, ] prompt.gsub!('%', '%%') if chat.confirm?(prompt:, yes: /\Ay/i) confirmed = :explicit else confirmed = :denied end else STDOUT.puts "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: 'You **MUST** ask the user for instructions on how to proceed!!!', ) STDOUT.printf( "\n%s Execution of tool %s denied by user.\n\n", ?🚫, bold { name } ) chat.log(:warn,"Execution of tool %s was denied by user!" % name) else symbol = confirmed == :implicit ? '☑️ ' : '✅' STDOUT.printf( "\n%s Execution of tool %s confirmed.\n\n", symbol, bold { name } ) result = OllamaChat::Tools.registered[name].execute(tool_call, chat: chat) if confirmed == :explicit chat.log(:info, "Execution of tool %s was explicitly confirmed." % name) else chat.log(:info, "Execution of tool %s was implicitly confirmed." % name) end end chat.tool_call_results[name] << result data = nil = begin data = JSON.parse(result) chat.log(:info, JSON.pretty_generate(data)) data['message'] rescue chat.log(:info, result) nil end warn = begin !!data.ask_and_send(:[], 'error') || data.ask_and_send(:[], 'success') == false rescue false end size_bytes = result.to_s.size tokens = OllamaChat::Utils::TokenEstimator.estimate(size_bytes) tools_used[name] = { message:, warn: , size: format_bytes(size_bytes), tokens: format_tokens(tokens), 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 STDOUT.puts <<~EOT.strip, "" 🔧 Tool functions #{name} returned result (#{info[:size]}/#{info[:tokens]} in #{info[:duration]}). #{} EOT timeout = chat.tool_function(name).result_display_timeout? chat.confirm?(prompt: '⏎ Press any key to continue (%s). ', timeout:) end end end |
#last_message_with_user ⇒ Array (private)
The last_message_with_user method constructs a formatted message array by combining user information, newline characters, thinking annotations, and content for display in the terminal output.
310 311 312 313 314 315 316 317 |
# File 'lib/ollama_chat/follow_chat.rb', line 310 def content, thinking = if thinking.present? [ @sender + ?:, ?\n, thinking, ?\n, content ] else [ @sender + ?:, ?\n, content ] end end |
#output_eval_stats(response) ⇒ Object (private)
The output_eval_stats method outputs evaluation statistics to the specified output stream.
379 380 381 382 |
# File 'lib/ollama_chat/follow_chat.rb', line 379 def output_eval_stats(response) response.done or return @output.puts "", eval_stats(response) end |
#prepare_last_message ⇒ Array<String, String>, Array<String, nil> (private)
The prepare_last_message method processes and formats content and thinking annotations for display.
This method prepares the final content and thinking text by applying appropriate formatting based on the chat's markdown and think loud settings. It handles parsing of content through Kramdown::ANSI when markdown is enabled, and applies annotation formatting to both content and thinking text according to the chat's configuration.
289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/ollama_chat/follow_chat.rb', line 289 def content, thinking = @messages.last.content, @messages.last.thinking if chat.markdown.on? content = talk_annotate { truncate_for_terminal chat.kramdown_ansi_parse(content) } if chat.think? thinking = think_annotate { truncate_for_terminal chat.kramdown_ansi_parse(thinking) } end else content = talk_annotate { content } chat.think? and thinking = think_annotate { thinking } end return content&.chomp, thinking&.chomp end |
#truncate_for_terminal(text, max_lines: Tins::Terminal.lines) ⇒ String (private)
The truncate_for_terminal method processes text to fit within a specified number of lines.
This method takes a text string and trims it to ensure it doesn't exceed the maximum number of lines allowed for terminal display. If the text exceeds the limit, only the last N lines are retained where N equals the maximum lines parameter.
235 236 237 238 239 240 |
# File 'lib/ollama_chat/follow_chat.rb', line 235 def truncate_for_terminal(text, max_lines: Tins::Terminal.lines) max_lines = max_lines.clamp(1..) lines = text.lines return text if lines.size <= max_lines lines[-max_lines..-1].join('') end |
#update_last_message(response) ⇒ Object (private)
The update_last_message method appends the content of a response to the last message in the conversation. It also appends thinking content to the last message if thinking is enabled and thinking content is present.
268 269 270 271 272 273 |
# File 'lib/ollama_chat/follow_chat.rb', line 268 def (response) @messages.last.content << response.&.content if chat.think? and response_thinking = response.&.thinking.full? @messages.last.thinking << response_thinking end end |