ollama_chat 0.0.138 → 0.0.139

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 170c9e2778ed0d581bda082eebc43d07a6c6e6a6cd99a0d1e0d3634a5281ca9f
4
- data.tar.gz: e57e2eb4d990f10617e1574ae1f70d22741dd814cf945575cc2cb7fb882dd4be
3
+ metadata.gz: e1a0cfa8c0914331039a424abe408286dce18f50b3b593bc3cac04c551c9b078
4
+ data.tar.gz: ab96f2413e6ab996329d654d1f7dc60b75d99cf6cd2bb93f5c0b1f5bc09627a7
5
5
  SHA512:
6
- metadata.gz: b5e6781664a38632da9d7a4f11900864b86b9e0049fcb66d6084777d0f8b4a85e762a57217c23817c77f0fb365592d5d659e3eab874aa270fa97b52a3e727c45
7
- data.tar.gz: 6aa806be34d04830f4788dd452d6e8a078e848767978d6bf49d6fef39f3dbd92b3f61fd4dc203c9914491c1ab2d8fa432ff75bc3543bf385dffcda2333acbd83
6
+ metadata.gz: 987632cebff57817b71ad6f56326c85edf921a4f54c1a42fff906e93603ec7c45113be2eb0b57b372987f9050d62213f54985359fdaf84f4bbc5bcd4bcdf8392
7
+ data.tar.gz: 9d9cee9148bc2c3542c9eb543015fa5b16393832f2741b56926f8d6b37a0d0633abea5252d32a5d3b931e350d4e44d874e857cdf84ae72d0c70ab79d8b747183
data/CHANGES.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-10-30 v0.0.139
4
+
5
+ ### Features
6
+
7
+ - Added terminal table wrapping for `/help` and `/session list` output. New
8
+ private methods `wrap_table_rows` and `table_column_budgets` in
9
+ `OllamaChat::KramdownANSI` compute per-column width budgets and wrap cell
10
+ content to fit within a configurable percentage of the terminal width.
11
+ - Updated `help_message` and `list_sessions` to collect rows into arrays and
12
+ route them through `wrap_table_rows`, removing manual truncation and heading
13
+ assignment.
14
+
15
+ ### Fixes
16
+
17
+ - Fixed a `TypeError` in `Collection.sync` when `chat.documents.collections`
18
+ yielded no collections (e.g., fresh database) by using `.to_a` instead of
19
+ relying on `Tins::NullClass#to_ary`.
20
+ - Improved error handling in `fetch_source` for `OllamaChat::HTTPError` by
21
+ adding a dedicated rescue clause that logs a warning without a backtrace and
22
+ stamps `response_status` and `response_reason` onto the
23
+ `ResponseMetadata.failed` IO.
24
+ - Added a non-2xx HTTP status guard in the `get_url` tool to short-circuit
25
+ processing before document policy dispatch, returning the raw body and a
26
+ status message instead of feeding error pages to the summarizer.
27
+
28
+ ### Improvements
29
+
30
+ - Updated the compaction voice report to format context fill percentages as
31
+ rounded integers with the unit "percent" for grammatical completeness
32
+ (e.g., "from 38 percent to 19 percent").
33
+ - Moved `configure_kramdown_ansi_styles` to a private method in
34
+ `OllamaChat::KramdownANSI`.
35
+ - Expanded YARD documentation for `OllamaChat::KramdownANSI` to describe
36
+ styling, parsing, and table wrapping concerns.
37
+
38
+ ### Build & CI
39
+
40
+ - Bumped `kramdown-ansi` dependency from `~> 0.7` to `~> 0.9` in `Rakefile`
41
+ and `ollama_chat.gemspec`.
42
+
43
+ ### Testing
44
+
45
+ - Updated `compaction_spec.rb` to expect rounded integer percentages in the
46
+ spoken compaction report.
47
+ - Updated `kramdown_ansi_spec.rb` to use a real `OllamaChat::Chat` instance
48
+ instead of a double and added examples for `wrap_table_rows` and
49
+ `table_column_budgets`.
50
+ - Updated `get_url_spec.rb` to stub `response_status: nil` on `SourceIO`
51
+ doubles and added a spec verifying that non-200 statuses skip summarization
52
+ and import.
53
+
3
54
  ## 2026-10-28 v0.0.138
4
55
 
5
56
  ### Features
data/Rakefile CHANGED
@@ -47,7 +47,7 @@ GemHadar do
47
47
  dependency 'redis', '~> 5.0'
48
48
  dependency 'mime-types', '~> 3.0'
49
49
  dependency 'reverse_markdown', '~> 3.0'
50
- dependency 'kramdown-ansi', '~> 0.7'
50
+ dependency 'kramdown-ansi', '~> 0.9'
51
51
  dependency 'complex_config', '~> 0.22', '>= 0.22.2'
52
52
  dependency 'tins', '~> 1.61'
53
53
  dependency 'search_ui', '>= 0.2'
@@ -6,6 +6,8 @@ module OllamaChat::CommandConcern
6
6
  extend Tins::Concern
7
7
 
8
8
  class_methods do
9
+ include OllamaChat::KramdownANSI
10
+
9
11
  # @!attribute [rw] commands
10
12
  # @return [Hash<Symbol, Command>] A hash mapping command names to
11
13
  # `Command` instances.
@@ -76,8 +78,8 @@ module OllamaChat::CommandConcern
76
78
  all_separators: true,
77
79
  border: :unicode_round,
78
80
  }
79
- table.headings = %w[ CMD SUBCMD OPTS HELP ]
80
81
  sorted_commands = self.commands.sort_by { [ _2.category, _2.name ] }.map(&:last)
82
+ rows = []
81
83
  cat = nil
82
84
  sorted_commands.each do |command|
83
85
  command.help or next
@@ -93,15 +95,16 @@ module OllamaChat::CommandConcern
93
95
  }
94
96
  if cat != command.category
95
97
  cat = command.category
96
- table << [ { value: bold{cat}, colspan: 4 } ]
98
+ rows << [ { value: bold{cat}, colspan: 4 } ]
97
99
  end
98
- table << [
99
- "%s" % command.command_names.map { ?/ + _1 }.join(?\n),
100
+ rows << [
101
+ '%s' % command.command_names.map { ?/ + _1 }.join(?\n),
100
102
  '%s' % subcommands,
101
103
  command.options,
102
104
  command.help,
103
105
  ]
104
106
  end
107
+ wrap_table_rows(rows, 4, headings: %w[ CMD SUBCMD OPTS HELP ], percentage: 95).each { |row| table << row }
105
108
  table
106
109
  end
107
110
  end
@@ -327,7 +327,7 @@ module OllamaChat::Compaction
327
327
  duration.minutes? ? '%M minutes and %u seconds' : '%u seconds'
328
328
  )
329
329
  speak(
330
- 'Compressed conversation context from %.f to %.f in %s.' % [
330
+ 'Compressed conversation context from %.0f percent to %.0f percent in %s.' % [
331
331
  100 * result.context_filled_before,
332
332
  100 * result.context_filled_after,
333
333
  duration_string,
@@ -51,7 +51,7 @@ class OllamaChat::Database::Models::Collection < Sequel::Model(OllamaChat::DB)
51
51
  #
52
52
  # @param chat [OllamaChat::Chat] the chat instance providing the configuration
53
53
  def self.sync(chat)
54
- all_collections = (%w[ default ] + chat.documents.collections).to_set
54
+ all_collections = (%w[ default ] + chat.documents.collections.to_a).to_set
55
55
  all_collections.each do |name|
56
56
  where(name: name.to_s).first and next
57
57
  create(
@@ -1,30 +1,22 @@
1
- # A module that provides Kramdown::ANSI styling configuration and parsing
2
- # functionality for OllamaChat.
1
+ # A module that provides Kramdown::ANSI styling, parsing, and
2
+ # terminal table wrapping for OllamaChat.
3
3
  #
4
- # This module handles the setup and application of ANSI styles for markdown
5
- # rendering, allowing for customizable terminal output formatting. It manages
6
- # the configuration of ANSI styles either from environment variables or falls
7
- # back to default settings, and provides methods to parse content with the
8
- # configured styling.
4
+ # This module handles three concerns:
5
+ #
6
+ # * **Styling configuration** – sets up ANSI styles either from the
7
+ # `KRAMDOWN_ANSI_OLLAMA_CHAT_STYLES` environment variable or from
8
+ # built-in defaults.
9
+ # * **Markdown parsing** – converts raw markdown into ANSI-formatted
10
+ # terminal output using the configured styles, with a fallback for
11
+ # invalid encodings.
12
+ # * **Table wrapping** – computes per-column width budgets and wraps
13
+ # cell content so that `Terminal::Table` output stays within a
14
+ # configurable percentage of the terminal width.
9
15
  #
10
16
  # @example Configuring custom ANSI styles via environment variable
11
17
  # Set KRAMDOWN_ANSI_OLLAMA_CHAT_STYLES to a JSON object containing style
12
18
  # definitions for customizing markdown output formatting in the terminal.
13
19
  module OllamaChat::KramdownANSI
14
- # The configure_kramdown_ansi_styles method sets up ANSI styling for
15
- # Kramdown::ANSI output by checking for specific environment variables and
16
- # falling back to default styles.
17
- #
18
- # @return [ Hash ] a hash of ANSI styles configured either from environment
19
- # variables or using default settings
20
- def configure_kramdown_ansi_styles
21
- if json = OC::KRAMDOWN_ANSI_OLLAMA_CHAT_STYLES?
22
- Kramdown::ANSI::Styles.from_json(json).ansi_styles
23
- else
24
- Kramdown::ANSI::Styles.new.ansi_styles
25
- end
26
- end
27
-
28
20
  # The kramdown_ansi_parse method processes content using Kramdown::ANSI with
29
21
  # custom ANSI styles.
30
22
  #
@@ -49,6 +41,22 @@ module OllamaChat::KramdownANSI
49
41
  retry
50
42
  end
51
43
 
44
+ private
45
+
46
+ # The configure_kramdown_ansi_styles method sets up ANSI styling for
47
+ # Kramdown::ANSI output by checking for specific environment variables and
48
+ # falling back to default styles.
49
+ #
50
+ # @return [ Hash ] a hash of ANSI styles configured either from environment
51
+ # variables or using default settings
52
+ def configure_kramdown_ansi_styles
53
+ if json = OC::KRAMDOWN_ANSI_OLLAMA_CHAT_STYLES?
54
+ Kramdown::ANSI::Styles.from_json(json).ansi_styles
55
+ else
56
+ Kramdown::ANSI::Styles.new.ansi_styles
57
+ end
58
+ end
59
+
52
60
  # The kramdown_markdown_remove method strips markdown formatting, emoji,
53
61
  # ANSI escape sequences, and box-drawing characters from content, yielding
54
62
  # plain text suitable for TTS playback.
@@ -74,4 +82,82 @@ module OllamaChat::KramdownANSI
74
82
  .gsub(/\s{2,}/, ' ')
75
83
  .strip
76
84
  end
85
+
86
+ # Wraps the cells of a terminal table to fit within a terminal-width
87
+ # budget.
88
+ #
89
+ # Terminal::Table sizes each column to its widest cell, so a single long
90
+ # cell forces the whole table wider than the terminal. This computes a
91
+ # per-column budget from the widest cell in each column across all rows,
92
+ # then wraps each cell to it. Cells expressed as Hashes (e.g. colspan
93
+ # rows) are passed through untouched.
94
+ #
95
+ # @param rows [Array<Array<String, Hash>>] the data rows to wrap
96
+ # @param cols [Integer] the number of columns in the table
97
+ # @param headings [Array<String>, nil] the heading row; when given it is
98
+ # measured for the budget and returned first, right-padded to each
99
+ # column's allocated width so Terminal::Table sizes columns to the
100
+ # full budget even when no data cell reaches it
101
+ # @param percentage [Numeric] the percentage of terminal width to use for
102
+ # the total budget (default: 90)
103
+ # @return [Array<Array<String, Hash>>] wrapped rows; the padded heading
104
+ # row is returned first when `headings` was supplied
105
+ def wrap_table_rows(rows, cols, headings: nil, percentage: 90)
106
+ all = rows + (headings ? [headings] : [])
107
+ budget = table_column_budgets(all, cols, percentage:)
108
+ wrap_row = ->(row) {
109
+ row.each_with_index.map { |cell, i|
110
+ cell.is_a?(Hash) and next cell
111
+ Kramdown::ANSI::Width.wrap(cell.to_s, length: budget[i], reflow: false)
112
+ }
113
+ }
114
+ result = rows.map(&wrap_row)
115
+ if headings
116
+ padded = headings.each_with_index.map { |h, i|
117
+ w = Kramdown::ANSI::Width.display_width(h)
118
+ w < budget[i] ? h + ' ' * (budget[i] - w) : h
119
+ }
120
+ result.unshift(padded)
121
+ end
122
+ result
123
+ end
124
+
125
+ # Computes a per-column display-width budget for a terminal table.
126
+ #
127
+ # @param rows [Array<Array<String, Hash>>] the rows to measure (Hash cells
128
+ # are ignored)
129
+ # @param cols [Integer] the number of columns
130
+ # @param percentage [Numeric] the percentage of terminal width to use for
131
+ # the total budget (default: 90)
132
+ # @return [Array<Integer>] the per-column width budget
133
+ def table_column_budgets(rows, cols, percentage: 90)
134
+ max_w = Array.new(cols, 0)
135
+ rows.each do |row|
136
+ row.each_with_index do |cell, i|
137
+ cell.is_a?(Hash) and next
138
+ w = cell.to_s.lines.map { |l| Kramdown::ANSI::Width.display_width(l) }.max || 0
139
+ max_w[i] = [max_w[i], w].max
140
+ end
141
+ end
142
+ total = max_w.sum
143
+ budget = Kramdown::ANSI::Width.width(percentage:) - (3 * cols + 1)
144
+ return Array.new(cols, (budget.to_f / cols).to_i) if total.zero?
145
+ if total <= budget
146
+ extra = budget - total
147
+ max_w.map { |w| w + ((w.to_f / total) * extra).floor }
148
+ else
149
+ share = (budget.to_f / cols).to_i
150
+ alloc = max_w.map { |w| [w, share].min }
151
+ unused = budget - alloc.sum
152
+ return alloc if unused.zero?
153
+ capped = (0...cols).to_a.select { |i| alloc[i] < max_w[i] }
154
+ need = capped.sum { |i| max_w[i] - alloc[i] }
155
+ return alloc if need.zero?
156
+ capped.each do |i|
157
+ extra = ((max_w[i] - alloc[i]).to_f / need * unused).floor
158
+ alloc[i] = [max_w[i], alloc[i] + extra].min
159
+ end
160
+ alloc
161
+ end
162
+ end
77
163
  end
@@ -107,22 +107,23 @@ module OllamaChat::SessionManagement
107
107
  all_separators: true,
108
108
  border: :unicode_round,
109
109
  }
110
- table.headings = %w[ ID NAME SIZE #TOK COUNT UPDATED ].map { |header| bold { header } }
111
- now = Time.now
110
+ headings = %w[ ID NAME SIZE #TOK COUNT UPDATED ].map { |h| bold { h } }
111
+ now = Time.now
112
+ rows = []
112
113
  models::Session.order(Sequel.desc(:updated_at)).each do |s|
113
- name = truncate(s.name, length: 32)
114
- name = session.id == s.id ? bold { name } : name
115
- name = if pid = s.locked?
116
- if pid == $$
117
- "#{name} 🔓"
118
- else
119
- "#{name} 🔐"
120
- end
121
- else
122
- name
123
- end
124
- es = s.estimate_tokens
125
- table << [
114
+ name = s.name
115
+ name = session.id == s.id ? bold { name } : name
116
+ name = if pid = s.locked?
117
+ if pid == $$
118
+ "#{name} 🔓"
119
+ else
120
+ "#{name} 🔐"
121
+ end
122
+ else
123
+ name
124
+ end
125
+ es = s.estimate_tokens
126
+ rows << [
126
127
  s.id.to_s,
127
128
  name,
128
129
  es.bytes_formatted,
@@ -131,6 +132,7 @@ module OllamaChat::SessionManagement
131
132
  s.age(now:),
132
133
  ]
133
134
  end
135
+ wrap_table_rows(rows, 6, headings:).each { |row| table << row }
134
136
  table.align_column 0, :right
135
137
  table.align_column 2, :left
136
138
  table.align_column 3, :right
@@ -33,6 +33,7 @@ module OllamaChat::SourceFetching
33
33
  #
34
34
  # @yield [ tmp ]
35
35
  def fetch_source(source, check_exist: false, &block)
36
+ confirm_notification = false
36
37
  source = source.ask_and_send_or_self(:to_path).to_s
37
38
  case source
38
39
  when %r{\A!(.*)}
@@ -72,14 +73,28 @@ module OllamaChat::SourceFetching
72
73
  else
73
74
  raise "invalid source #{source.inspect}"
74
75
  end
76
+ rescue OllamaChat::HTTPError => e
77
+ msg = "Fetching source #{source.to_s.inspect}: #{e.class} #{e}"
78
+ feedback(msg, type: :warn)
79
+ log(:warn, msg, data: { source: source.to_s, status: e.status })
80
+ confirm_notification = true
81
+ msg = OllamaChat::Utils::Fetcher::ResponseMetadata.failed(msg)
82
+ msg.response_status = e.status
83
+ msg.response_reason = e.message
84
+ block.(msg)
85
+ msg
75
86
  rescue => e
76
87
  msg = "Fetching source #{source.to_s.inspect}: #{e.class} #{e}"
77
88
  feedback("#{msg}\n#{e.backtrace * ?\n}", type: :warn)
78
89
  log(:error, msg, data: { source: source.to_s, error: e.class })
79
- confirm?(prompt: '⏎ Press any key to continue (%s). ', output: STDERR, timeout: 3)
90
+ confirm_notification = true
80
91
  msg = OllamaChat::Utils::Fetcher::ResponseMetadata.failed(msg)
81
92
  block.(msg)
82
93
  msg
94
+ ensure
95
+ if confirm_notification
96
+ confirm?(prompt: "\n⏎ Press any key to continue (%s). ", output: STDERR, timeout: 3)
97
+ end
83
98
  end
84
99
 
85
100
  # Adds an image to the images collection from the given source IO and source
@@ -116,6 +116,11 @@ class OllamaChat::Tools::GetURL
116
116
 
117
117
  source, message, content = url, nil, ''
118
118
  chat.fetch_source(source, check_exist: false) do |source_io|
119
+ if (status = source_io&.response_status) && status != 200
120
+ content = source_io.read
121
+ message = "Received HTTP status #{status} from #{source}."
122
+ next
123
+ end
119
124
  if source_io&.content_type&.to_s&.match?(OllamaChat::Parsing::HAS_AUDIO)
120
125
  text = chat.parse_audio(source_io, language:)
121
126
  source_io = OllamaChat::Utils::Fetcher::ResponseMetadata.as_text(text)
@@ -5,7 +5,7 @@
5
5
  # by hand; it is rewritten on every `rake version` invocation.
6
6
  module OllamaChat
7
7
  # The current version of the OllamaChat gem as a String.
8
- VERSION = '0.0.138'
8
+ VERSION = '0.0.139'
9
9
  VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
10
10
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
11
11
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
data/ollama_chat.gemspec CHANGED
@@ -1,9 +1,9 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: ollama_chat 0.0.138 ruby lib
2
+ # stub: ollama_chat 0.0.139 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "ollama_chat".freeze
6
- s.version = "0.0.138".freeze
6
+ s.version = "0.0.139".freeze
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
@@ -41,7 +41,7 @@ Gem::Specification.new do |s|
41
41
  s.add_runtime_dependency(%q<redis>.freeze, ["~> 5.0".freeze])
42
42
  s.add_runtime_dependency(%q<mime-types>.freeze, ["~> 3.0".freeze])
43
43
  s.add_runtime_dependency(%q<reverse_markdown>.freeze, ["~> 3.0".freeze])
44
- s.add_runtime_dependency(%q<kramdown-ansi>.freeze, ["~> 0.7".freeze])
44
+ s.add_runtime_dependency(%q<kramdown-ansi>.freeze, ["~> 0.9".freeze])
45
45
  s.add_runtime_dependency(%q<complex_config>.freeze, ["~> 0.22".freeze, ">= 0.22.2".freeze])
46
46
  s.add_runtime_dependency(%q<tins>.freeze, ["~> 1.61".freeze])
47
47
  s.add_runtime_dependency(%q<search_ui>.freeze, [">= 0.2".freeze])
@@ -351,7 +351,7 @@ describe OllamaChat::Compaction do
351
351
  .with(a_string_matching(/Summarized:\s+12 messages/), type: :success)
352
352
  expect(chat).to receive(:speak)
353
353
  .with(a_string_matching(
354
- /Compressed conversation context from \d+ to \d+ in \d+ (?:seconds|minutes and \d+ seconds)\./
354
+ /Compressed conversation context from \d+ percent to \d+ percent in \d+ (?:seconds|minutes and \d+ seconds)\./
355
355
  ))
356
356
 
357
357
  expect(chat.compact_with_retry).to be true
@@ -1,8 +1,14 @@
1
1
  describe OllamaChat::KramdownANSI do
2
+ let :argv do
3
+ chat_default_config
4
+ end
5
+
2
6
  let :chat do
3
- double('Chat').extend(described_class)
7
+ OllamaChat::Chat.new(argv:).expose
4
8
  end
5
9
 
10
+ connect_to_ollama_server
11
+
6
12
  describe '#configure_kramdown_ansi_styles', protect_env: true do
7
13
  it 'can be configured via env var' do
8
14
  const_conf_as(
@@ -12,7 +18,6 @@ describe OllamaChat::KramdownANSI do
12
18
  expect(Kramdown::ANSI::Styles).to receive(:from_json).
13
19
  with('{"foo":"bar"}').
14
20
  and_return(double(ansi_styles: styles))
15
-
16
21
  expect(chat.configure_kramdown_ansi_styles).to eq(styles)
17
22
  end
18
23
 
@@ -100,4 +105,51 @@ describe OllamaChat::KramdownANSI do
100
105
  expect(result).to include('**bold**')
101
106
  end
102
107
  end
108
+
109
+ describe '#wrap_table_rows' do
110
+ it 'wraps long cells to fit within terminal budget' do
111
+ long_text = 'word ' * 50
112
+ rows = [[ long_text, 'short' ]]
113
+ result = chat.wrap_table_rows(rows, 2)
114
+ expect(result[0][0]).to include("\n")
115
+ end
116
+
117
+ it 'passes Hash cells (colspan) through untouched' do
118
+ rows = [[ { value: 'category', colspan: 4 } ]]
119
+ result = chat.wrap_table_rows(rows, 4)
120
+ expect(result).to eq(rows)
121
+ end
122
+
123
+ it 'returns wrapped headings first when supplied' do
124
+ rows = [[ 'a', 'b' ]]
125
+ result = chat.wrap_table_rows(rows, 2, headings: ['H1', 'H2'])
126
+ expect(result.first.map(&:strip)).to eq(['H1', 'H2'])
127
+ expect(result.size).to eq(2)
128
+ end
129
+
130
+ it 'handles nil cells by converting to empty string' do
131
+ result = chat.wrap_table_rows([[ nil, 'text' ]], 2)
132
+ expect(result[0][0]).to eq('')
133
+ end
134
+
135
+ it 'preserves short content without wrapping' do
136
+ rows = [[ 'hi', 'there' ]]
137
+ result = chat.wrap_table_rows(rows, 2)
138
+ expect(result[0]).to eq(['hi', 'there'])
139
+ end
140
+ end
141
+
142
+ describe '#table_column_budgets' do
143
+ it 'returns equal budgets when total width is zero' do
144
+ result = chat.table_column_budgets([[ '', '' ]], 2)
145
+ expect(result[0]).to eq(result[1])
146
+ end
147
+
148
+ it 'allocates proportionally based on content width' do
149
+ narrow = 'a'
150
+ wide = 'a' * 20
151
+ result = chat.table_column_budgets([[ narrow, wide ]], 2)
152
+ expect(result[1]).to be > result[0]
153
+ end
154
+ end
103
155
  end
@@ -109,7 +109,7 @@ describe OllamaChat::Tools::GetURL do
109
109
 
110
110
  context 'with different document policies' do
111
111
  let(:url) { 'https://www.example.com/foo' }
112
- let(:source_io) { double('SourceIO', content_type: double(media_type: 'text'), read: 'raw content') }
112
+ let(:source_io) { double('SourceIO', content_type: double(media_type: 'text'), read: 'raw content', response_status: nil) }
113
113
 
114
114
  before do
115
115
  expect(chat).to receive(:fetch_source).and_yield(source_io)
@@ -191,7 +191,7 @@ describe OllamaChat::Tools::GetURL do
191
191
  let(:url) { 'https://www.example.com/foo' }
192
192
 
193
193
  it 'handles image content types' do
194
- source_io = double('SourceIO', content_type: double(media_type: 'image', sub_type: 'jpeg'))
194
+ source_io = double('SourceIO', content_type: double(media_type: 'image', sub_type: 'jpeg'), response_status: nil)
195
195
  expect(chat).to receive(:fetch_source).and_yield(source_io)
196
196
  expect(chat).to receive(:add_image).with(chat.images, source_io, URI.parse(url))
197
197
 
@@ -222,7 +222,7 @@ describe OllamaChat::Tools::GetURL do
222
222
  end
223
223
 
224
224
  it 'handles audio content types via ASR transcription' do
225
- source_io = double('SourceIO', content_type: double(media_type: 'audio', to_s: 'audio/mp3'))
225
+ source_io = double('SourceIO', content_type: double(media_type: 'audio', to_s: 'audio/mp3'), response_status: nil)
226
226
  expect(chat).to receive(:fetch_source).and_yield(source_io)
227
227
  expect(chat).to receive(:parse_audio).with(source_io, language: 'de').and_return('transcribed text')
228
228
 
@@ -235,7 +235,7 @@ describe OllamaChat::Tools::GetURL do
235
235
  end
236
236
 
237
237
  it 'handles video content types via ASR transcription' do
238
- source_io = double('SourceIO', content_type: double(media_type: 'video', to_s: 'video/mp4'))
238
+ source_io = double('SourceIO', content_type: double(media_type: 'video', to_s: 'video/mp4'), response_status: nil)
239
239
  expect(chat).to receive(:fetch_source).and_yield(source_io)
240
240
  expect(chat).to receive(:parse_audio).with(source_io, language: nil).and_return('video text')
241
241
 
@@ -248,7 +248,7 @@ describe OllamaChat::Tools::GetURL do
248
248
  end
249
249
 
250
250
  it 'handles unsupported media types' do
251
- source_io = double('SourceIO', content_type: double(media_type: 'font'))
251
+ source_io = double('SourceIO', content_type: double(media_type: 'font'), response_status: nil)
252
252
  expect(chat).to receive(:fetch_source).and_yield(source_io)
253
253
 
254
254
  args = double(url:, document_policy: 'ignoring', words: nil, instruction: nil, language: nil)
@@ -258,5 +258,24 @@ describe OllamaChat::Tools::GetURL do
258
258
  json = json_object(result)
259
259
  expect(json.message).to match(/Cannot fetch.*with content type/)
260
260
  end
261
+
262
+ it 'short-circuits on non-200 HTTP status' do
263
+ source_io = double(
264
+ 'FailedSourceIO',
265
+ response_status: 403,
266
+ read: 'Access Denied',
267
+ )
268
+ expect(chat).to receive(:fetch_source).and_yield(source_io)
269
+ expect(chat).not_to receive(:summarize_source)
270
+ expect(chat).not_to receive(:import_source)
271
+
272
+ args = double(url:, document_policy: 'summarizing', words: nil, instruction: nil, language: nil)
273
+ tool_call = double(function: double(arguments: args))
274
+
275
+ result = described_class.new(chat).execute(tool_call)
276
+ json = json_object(result)
277
+ expect(json.content).to eq('Access Denied')
278
+ expect(json.message).to match(/HTTP status 403/)
279
+ end
261
280
  end
262
281
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ollama_chat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.138
4
+ version: 0.0.139
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Frank
@@ -253,14 +253,14 @@ dependencies:
253
253
  requirements:
254
254
  - - "~>"
255
255
  - !ruby/object:Gem::Version
256
- version: '0.7'
256
+ version: '0.9'
257
257
  type: :runtime
258
258
  prerelease: false
259
259
  version_requirements: !ruby/object:Gem::Requirement
260
260
  requirements:
261
261
  - - "~>"
262
262
  - !ruby/object:Gem::Version
263
- version: '0.7'
263
+ version: '0.9'
264
264
  - !ruby/object:Gem::Dependency
265
265
  name: complex_config
266
266
  requirement: !ruby/object:Gem::Requirement