ollama_chat 0.0.95 → 0.0.97

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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +106 -0
  3. data/README.md +115 -2
  4. data/Rakefile +1 -1
  5. data/lib/ollama_chat/chat.rb +34 -17
  6. data/lib/ollama_chat/commands.rb +27 -15
  7. data/lib/ollama_chat/database/migrations/005_add_context_format_to_sessions.rb +5 -0
  8. data/lib/ollama_chat/database/models/model_options.rb +1 -0
  9. data/lib/ollama_chat/database/models/prompt.rb +1 -3
  10. data/lib/ollama_chat/database/models/session.rb +18 -0
  11. data/lib/ollama_chat/follow_chat.rb +4 -5
  12. data/lib/ollama_chat/information.rb +2 -1
  13. data/lib/ollama_chat/input_content.rb +1 -1
  14. data/lib/ollama_chat/message_list.rb +2 -4
  15. data/lib/ollama_chat/ollama_chat_config/default_config.yml +2 -1
  16. data/lib/ollama_chat/parsing.rb +10 -6
  17. data/lib/ollama_chat/personae_management.rb +25 -11
  18. data/lib/ollama_chat/prompt_management.rb +25 -11
  19. data/lib/ollama_chat/session_management.rb +24 -30
  20. data/lib/ollama_chat/source_fetching.rb +1 -1
  21. data/lib/ollama_chat/state_selectors.rb +14 -0
  22. data/lib/ollama_chat/system_prompt_management.rb +21 -0
  23. data/lib/ollama_chat/think_control.rb +0 -1
  24. data/lib/ollama_chat/token_estimator/crude.rb +25 -0
  25. data/lib/ollama_chat/token_estimator.rb +36 -0
  26. data/lib/ollama_chat/tool_calling.rb +1 -1
  27. data/lib/ollama_chat/tools/compute_bmi.rb +6 -4
  28. data/lib/ollama_chat/tools/execute_grep.rb +11 -1
  29. data/lib/ollama_chat/tools/file_context.rb +3 -2
  30. data/lib/ollama_chat/tools/generate_password.rb +4 -1
  31. data/lib/ollama_chat/tools/get_current_weather.rb +11 -0
  32. data/lib/ollama_chat/tools/get_time.rb +13 -2
  33. data/lib/ollama_chat/tools/open_file_in_editor.rb +3 -3
  34. data/lib/ollama_chat/tools/read_file.rb +3 -0
  35. data/lib/ollama_chat/tools/retrieve_document_snippets.rb +18 -11
  36. data/lib/ollama_chat/tools/roll_dice.rb +52 -11
  37. data/lib/ollama_chat/tools/run_tests.rb +23 -11
  38. data/lib/ollama_chat/tools/write_file.rb +2 -6
  39. data/lib/ollama_chat/utils/fetcher.rb +2 -2
  40. data/lib/ollama_chat/utils/png_metadata_extractor.rb +5 -4
  41. data/lib/ollama_chat/utils.rb +0 -1
  42. data/lib/ollama_chat/version.rb +1 -1
  43. data/lib/ollama_chat.rb +1 -0
  44. data/ollama_chat.gemspec +6 -6
  45. data/spec/ollama_chat/database/models/favourite_spec.rb +7 -20
  46. data/spec/ollama_chat/server_socket_spec.rb +11 -20
  47. data/spec/ollama_chat/source_fetching_spec.rb +1 -1
  48. data/spec/ollama_chat/state_selectors_spec.rb +4 -9
  49. data/spec/ollama_chat/switches_spec.rb +1 -1
  50. data/spec/ollama_chat/token_estimator_spec.rb +41 -0
  51. data/spec/ollama_chat/tools/compute_bmi_spec.rb +14 -5
  52. data/spec/ollama_chat/tools/copy_to_clipboard_spec.rb +5 -5
  53. data/spec/ollama_chat/tools/execute_grep_spec.rb +10 -0
  54. data/spec/ollama_chat/tools/generate_image_spec.rb +8 -8
  55. data/spec/ollama_chat/tools/generate_password_spec.rb +13 -0
  56. data/spec/ollama_chat/tools/get_current_weather_spec.rb +4 -0
  57. data/spec/ollama_chat/tools/get_endoflife_spec.rb +3 -3
  58. data/spec/ollama_chat/tools/get_rfc_spec.rb +2 -2
  59. data/spec/ollama_chat/tools/get_time_spec.rb +4 -0
  60. data/spec/ollama_chat/tools/get_url_spec.rb +7 -7
  61. data/spec/ollama_chat/tools/paste_from_clipboard_spec.rb +4 -4
  62. data/spec/ollama_chat/tools/read_file_spec.rb +3 -0
  63. data/spec/ollama_chat/tools/retrieve_document_snippets_spec.rb +38 -5
  64. data/spec/ollama_chat/tools/roll_dice_spec.rb +4 -4
  65. data/spec/ollama_chat/tools/run_tests_spec.rb +5 -5
  66. data/spec/ollama_chat/tools/search_web_spec.rb +3 -3
  67. data/spec/ollama_chat/tools/write_file_spec.rb +2 -0
  68. metadata +11 -5
  69. data/lib/ollama_chat/utils/token_estimator.rb +0 -15
@@ -0,0 +1,41 @@
1
+ describe OllamaChat::TokenEstimator do
2
+ describe '.estimate' do
3
+ it 'uses crude estimation when no ollama client and model are provided' do
4
+ # 10 bytes / 3.5 = 2.85... -> ceil is 3
5
+ result = described_class.estimate('1234567890')
6
+ expect(result).to be_a OllamaChat::TokenEstimator::Estimate
7
+ expect(result.tokens).to eq 3
8
+ end
9
+ end
10
+
11
+ describe OllamaChat::TokenEstimator::Crude do
12
+ it 'correctly estimates tokens from a string' do
13
+ # 11 bytes / 3.5 = 3.14… -> ceil is 4, as legislated by the state of
14
+ # Indiana… probably.
15
+ crude = described_class.new('Hello World') # 11 bytes
16
+ result = crude.perform
17
+ expect(result.bytes).to eq 11
18
+ expect(result.tokens).to eq 4
19
+ end
20
+
21
+ it 'correctly estimates tokens from an integer byte count' do
22
+ # 100 bytes / 3.5 = 28.5... -> ceil is 29
23
+ crude = described_class.new(100)
24
+ result = crude.perform
25
+ expect(result.bytes).to eq 100
26
+ expect(result.tokens).to eq 29
27
+ end
28
+
29
+ it 'raises ArgumentError for invalid input types' do
30
+ expect {
31
+ described_class.new({ not: 'a string or int' })
32
+ }.to raise_error(ArgumentError, /cannot be used to estimate/)
33
+ end
34
+
35
+ it 'returns an Estimate object with formatted methods' do
36
+ result = described_class.new('Test').perform
37
+ expect(result).to respond_to(:bytes_formatted)
38
+ expect(result).to respond_to(:tokens_formatted)
39
+ end
40
+ end
41
+ end
@@ -28,11 +28,14 @@ describe OllamaChat::Tools::ComputeBMI do
28
28
  )
29
29
  )
30
30
 
31
- result = described_class.new.execute(tool_call, chat: chat)
31
+ result = described_class.new.execute(tool_call, chat:)
32
32
  json = json_object(result)
33
33
 
34
34
  expect(json.bmi).to be_within(0.01).of(22.86)
35
35
  expect(json.category).to eq 'Normal weight'
36
+ expect(json.message).to match(
37
+ /This BMI is 22\.8\d+, which falls into the Normal weight category\./
38
+ )
36
39
  end
37
40
 
38
41
  it 'calculates BMI correctly for Underweight' do
@@ -44,11 +47,14 @@ describe OllamaChat::Tools::ComputeBMI do
44
47
  )
45
48
  )
46
49
 
47
- result = described_class.new.execute(tool_call, chat: chat)
50
+ result = described_class.new.execute(tool_call, chat:)
48
51
  json = json_object(result)
49
52
 
50
53
  expect(json.bmi).to be_within(0.01).of(17.58)
51
54
  expect(json.category).to eq 'Underweight'
55
+ expect(json.message).to match(
56
+ /This BMI is 17\.5\d+, which falls into the Underweight category\./
57
+ )
52
58
  end
53
59
  end
54
60
 
@@ -62,11 +68,14 @@ describe OllamaChat::Tools::ComputeBMI do
62
68
  )
63
69
  )
64
70
 
65
- result = described_class.new.execute(tool_call, chat: chat)
71
+ result = described_class.new.execute(tool_call, chat:)
66
72
  json = json_object(result)
67
73
 
68
74
  expect(json.bmi).to be_within(0.01).of(21.52)
69
75
  expect(json.category).to eq 'Normal weight'
76
+ expect(json.message).to match(
77
+ /This BMI is 21\.5\d+, which falls into the Normal weight category\./
78
+ )
70
79
  end
71
80
  end
72
81
 
@@ -80,7 +89,7 @@ describe OllamaChat::Tools::ComputeBMI do
80
89
  )
81
90
  )
82
91
 
83
- result = described_class.new.execute(tool_call, chat: chat)
92
+ result = described_class.new.execute(tool_call, chat:)
84
93
  json = json_object(result)
85
94
 
86
95
  expect(json.error).to eq 'OllamaChat::ToolFunctionArgumentError'
@@ -96,7 +105,7 @@ describe OllamaChat::Tools::ComputeBMI do
96
105
  )
97
106
  )
98
107
 
99
- result = described_class.new.execute(tool_call, chat: chat)
108
+ result = described_class.new.execute(tool_call, chat:)
100
109
  json = json_object(result)
101
110
 
102
111
  expect(json.error).to eq 'OllamaChat::ToolFunctionArgumentError'
@@ -32,7 +32,7 @@ describe OllamaChat::Tools::CopyToClipboard do
32
32
  # Test that perform_copy_to_clipboard is called with content: true
33
33
  expect(chat).to receive(:perform_copy_to_clipboard).with(text: nil, content: true, edit: false)
34
34
 
35
- result = described_class.new.execute(tool_call, chat: chat)
35
+ result = described_class.new.execute(tool_call, chat:)
36
36
 
37
37
  # Should return valid JSON
38
38
  expect(result).to be_a(String)
@@ -59,7 +59,7 @@ describe OllamaChat::Tools::CopyToClipboard do
59
59
  # Test that perform_copy_to_clipboard is called with the custom text
60
60
  expect(chat).to receive(:perform_copy_to_clipboard).with(text:, content: true, edit: false)
61
61
 
62
- result = described_class.new.execute(tool_call, chat: chat)
62
+ result = described_class.new.execute(tool_call, chat:)
63
63
 
64
64
  # Should return valid JSON
65
65
  expect(result).to be_a(String)
@@ -84,7 +84,7 @@ describe OllamaChat::Tools::CopyToClipboard do
84
84
  # Test that perform_copy_to_clipboard is called with edit: true
85
85
  expect(chat).to receive(:perform_copy_to_clipboard).with(text: nil, content: true, edit: true)
86
86
 
87
- result = described_class.new.execute(tool_call, chat: chat)
87
+ result = described_class.new.execute(tool_call, chat:)
88
88
 
89
89
  # Should return valid JSON
90
90
  expect(result).to be_a(String)
@@ -110,7 +110,7 @@ describe OllamaChat::Tools::CopyToClipboard do
110
110
  expect(chat).to receive(:perform_copy_to_clipboard).with(text:nil, content: true, edit: false).
111
111
  and_raise(OllamaChat::OllamaChatError, 'No response available to copy to the system clipboard.')
112
112
 
113
- result = described_class.new.execute(tool_call, chat: chat)
113
+ result = described_class.new.execute(tool_call, chat:)
114
114
 
115
115
  # Should return valid JSON even with errors
116
116
  expect(result).to be_a(String)
@@ -135,7 +135,7 @@ describe OllamaChat::Tools::CopyToClipboard do
135
135
  expect(chat).to receive(:perform_copy_to_clipboard).with(text: nil, content: true, edit: false).
136
136
  and_raise(RuntimeError, 'some kind of exception')
137
137
 
138
- result = described_class.new.execute(tool_call, chat: chat)
138
+ result = described_class.new.execute(tool_call, chat:)
139
139
 
140
140
  # Should return valid JSON even with exceptions
141
141
  expect(result).to be_a(String)
@@ -47,6 +47,7 @@ describe OllamaChat::Tools::ExecuteGrep do
47
47
  expect(json.cmd).to include('Hello\\ World')
48
48
  expect(json.cmd).to include('spec/assets')
49
49
  expect(json.result).to include('Hello World!')
50
+ expect(json.message).to match(/Found some matches for "Hello World" in ".*spec\/assets"\./)
50
51
  end
51
52
 
52
53
  it 'can be executed successfully with max_results parameter' do
@@ -78,6 +79,7 @@ describe OllamaChat::Tools::ExecuteGrep do
78
79
  expect(json.cmd).to include('grep')
79
80
  expect(json.cmd).to include(' -m 5 ')
80
81
  expect(json.result).to match(/blub class blob/)
82
+ expect(json.message).to match(/Found some matches for "class" in ".*spec\/assets"\./)
81
83
  end
82
84
 
83
85
  it 'can be executed successfully with max_results and ignore_case parameter' do
@@ -110,6 +112,7 @@ describe OllamaChat::Tools::ExecuteGrep do
110
112
  expect(json.cmd).to include(' -m 5 ')
111
113
  expect(json.cmd).to include(' -i ')
112
114
  expect(json.result).to match(/blub class blob/)
115
+ expect(json.message).to match(/Found some matches for "class" in ".*spec\/assets"\./)
113
116
  end
114
117
 
115
118
  it 'can handle execution errors gracefully' do
@@ -141,6 +144,7 @@ describe OllamaChat::Tools::ExecuteGrep do
141
144
  json = json_object(result)
142
145
  expect(json.cmd).to include('grep')
143
146
  expect(json.result).to eq ''
147
+ expect(json.message).to match(/No matches found for "nonexistent_pattern" in ".*spec\/assets"\./)
144
148
  end
145
149
 
146
150
  it 'can handle non-existent paths gracefully' do
@@ -171,6 +175,9 @@ describe OllamaChat::Tools::ExecuteGrep do
171
175
  expect(result).to be_a String
172
176
  json = json_object(result)
173
177
  expect(json.result).to match(%r(grep: /nonexistent/path/that/does/not/exist))
178
+ expect(json.message).to match(
179
+ %r(Found some matches for "test" in "/nonexistent/path/that/does/not/exist"\.)
180
+ )
174
181
  end
175
182
 
176
183
  it 'can handle thrown exceptions' do
@@ -224,6 +231,9 @@ describe OllamaChat::Tools::ExecuteGrep do
224
231
  json = json_object(result)
225
232
  expect(json.result).to include('Hello World!')
226
233
  expect(json.result).to include('example.rb')
234
+ expect(json.message).to match(
235
+ /Found some matches for "Hello World!" in ".*spec\/assets"\./
236
+ )
227
237
  end
228
238
  end
229
239
  end
@@ -35,8 +35,8 @@ describe OllamaChat::Tools::GenerateImage do
35
35
  double(
36
36
  'ToolCall',
37
37
  function: double(
38
- name: 'generate_image',
39
- arguments: arguments
38
+ name: 'generate_image',
39
+ arguments:
40
40
  )
41
41
  )
42
42
  end
@@ -53,7 +53,7 @@ describe OllamaChat::Tools::GenerateImage do
53
53
  )
54
54
  expect(instance).to receive(:poll_for_image).and_return('kitten_output.png')
55
55
 
56
- result = instance.execute(tool_call, chat: chat)
56
+ result = instance.execute(tool_call, chat:)
57
57
 
58
58
  expect(chat.links).to include(%r{/api/view.*filename=kitten_output.png})
59
59
 
@@ -67,7 +67,7 @@ describe OllamaChat::Tools::GenerateImage do
67
67
  it 'returns an error when prompt is missing' do
68
68
  arguments.prompt = nil
69
69
 
70
- result = instance.execute(tool_call, chat: chat)
70
+ result = instance.execute(tool_call, chat:)
71
71
 
72
72
  json = json_object(result)
73
73
  expect(json.error).to eq 'OllamaChat::ToolFunctionArgumentError'
@@ -78,7 +78,7 @@ describe OllamaChat::Tools::GenerateImage do
78
78
  # Force a config error by simulating the OllamaChat::OllamaChatError
79
79
  allow(OC::OLLAMA::CHAT::TOOLS::IMAGE_GENERATOR).to receive(:URL?).and_return(nil)
80
80
 
81
- result = instance.execute(tool_call, chat: chat)
81
+ result = instance.execute(tool_call, chat:)
82
82
 
83
83
  json = json_object(result)
84
84
  expect(json.error).to eq 'OllamaChat::ConfigMissingError'
@@ -93,7 +93,7 @@ describe OllamaChat::Tools::GenerateImage do
93
93
  )
94
94
  expect(instance).to receive(:poll_for_image).and_return(nil)
95
95
 
96
- result = instance.execute(tool_call, chat: chat)
96
+ result = instance.execute(tool_call, chat:)
97
97
 
98
98
  json = json_object(result)
99
99
  expect(json.error).to eq 'OllamaChat::OllamaChatError'
@@ -107,7 +107,7 @@ describe OllamaChat::Tools::GenerateImage do
107
107
  OpenStruct.new(prompt_id: nil)
108
108
  )
109
109
 
110
- result = instance.execute(tool_call, chat: chat)
110
+ result = instance.execute(tool_call, chat:)
111
111
 
112
112
  json = json_object(result)
113
113
  expect(json.error).to eq 'OllamaChat::OllamaChatError'
@@ -119,7 +119,7 @@ describe OllamaChat::Tools::GenerateImage do
119
119
  and_return(service_url)
120
120
  allow(instance).to receive(:post_url).and_raise(StandardError, 'Network crash')
121
121
 
122
- result = instance.execute(tool_call, chat: chat)
122
+ result = instance.execute(tool_call, chat:)
123
123
 
124
124
  json = json_object(result)
125
125
  expect(json.error).to eq 'StandardError'
@@ -43,6 +43,9 @@ describe OllamaChat::Tools::GeneratePassword do
43
43
  json = json_object(result)
44
44
  expect(json.password).to be_a(String)
45
45
  expect(json.length).to eq 16
46
+ expect(json.message).to eq(
47
+ 'Successfully generated a 16-character secure password using the default alphabet.'
48
+ )
46
49
  end
47
50
 
48
51
  it 'can be executed successfully with bits parameter' do
@@ -62,6 +65,9 @@ describe OllamaChat::Tools::GeneratePassword do
62
65
  json = json_object(result)
63
66
  expect(json.password).to be_a(String)
64
67
  expect(json.bits).to be >= 128
68
+ expect(json.message).to match(
69
+ /Successfully generated a \d+-character secure password using the default alphabet\./
70
+ )
65
71
  end
66
72
 
67
73
  it 'can be executed successfully with base32 alphabet type' do
@@ -82,6 +88,9 @@ describe OllamaChat::Tools::GeneratePassword do
82
88
  json = json_object(result)
83
89
  expect(json.password).to be_a(String)
84
90
  expect(json.alphabet_type).to eq 'base32'
91
+ expect(json.message).to match(
92
+ /Successfully generated a \d+-character secure password using the base32 alphabet\./
93
+ )
85
94
  end
86
95
 
87
96
  it 'can be executed successfully with default alphabet type' do
@@ -103,6 +112,9 @@ describe OllamaChat::Tools::GeneratePassword do
103
112
  expect(json.password).to be_a(String)
104
113
  expect(json.alphabet_type).to eq 'default'
105
114
  expect(json.symbols).to be true
115
+ expect(json.message).to eq(
116
+ 'Successfully generated a 20-character secure password using the default alphabet.'
117
+ )
106
118
  end
107
119
 
108
120
  it 'can handle execution errors gracefully' do
@@ -121,6 +133,7 @@ describe OllamaChat::Tools::GeneratePassword do
121
133
  expect(result).to be_a(String)
122
134
  json = json_object(result)
123
135
  expect(json.error).to eq 'OllamaChat::ToolFunctionArgumentError'
136
+ expect(json.message).to eq 'require either bits or length of password'
124
137
  end
125
138
 
126
139
  it 'can be converted t)o hash' do
@@ -39,6 +39,10 @@ describe OllamaChat::Tools::GetCurrentWeather do
39
39
  expect(json.current_time).to match(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}[+-]\d{2}:\d{2}\z/)
40
40
  expect(json.currently.temperature).to be_within(0.01).of(7.74)
41
41
  expect(json.units).to eq "si"
42
+ expect(json.message).to eq(
43
+ "Currently 7.74°C and Clear. Today's forecast: No precipitation "\
44
+ "throughout the week, with highs falling to 13°C next Friday."
45
+ )
42
46
  end
43
47
 
44
48
  it 'can handle execution errors with structured JSON error response' do
@@ -25,7 +25,7 @@ describe OllamaChat::Tools::GetEndoflife do
25
25
  function: double(
26
26
  name: 'get_endoflife',
27
27
  arguments: double(
28
- product: product
28
+ product:
29
29
  )
30
30
  )
31
31
  )
@@ -54,14 +54,14 @@ describe OllamaChat::Tools::GetEndoflife do
54
54
  function: double(
55
55
  name: 'get_endoflife',
56
56
  arguments: double(
57
- product: product
57
+ product:
58
58
  )
59
59
  )
60
60
  )
61
61
 
62
62
  url = chat.config.tools.functions.get_endoflife.url
63
63
 
64
- stub_request(:get, url % { product: product })
64
+ stub_request(:get, url % { product: })
65
65
  .to_return(status: 404, body: 'Not Found')
66
66
 
67
67
  result = described_class.new.execute(tool_call, chat:)
@@ -25,7 +25,7 @@ describe OllamaChat::Tools::GetRFC do
25
25
  function: double(
26
26
  name: 'get_rfc',
27
27
  arguments: double(
28
- rfc_id: rfc_id
28
+ rfc_id:
29
29
  )
30
30
  )
31
31
  )
@@ -53,7 +53,7 @@ describe OllamaChat::Tools::GetRFC do
53
53
  function: double(
54
54
  name: 'get_rfc',
55
55
  arguments: double(
56
- rfc_id: rfc_id
56
+ rfc_id:
57
57
  )
58
58
  )
59
59
  )
@@ -18,6 +18,7 @@ describe OllamaChat::Tools::GetTime do
18
18
  end
19
19
 
20
20
  it 'can be executed successfully' do
21
+ Time.dummy = '2011-11-11T11:11:11+0200'
21
22
  tool_call = double(
22
23
  'ToolCall',
23
24
  function: double(
@@ -34,5 +35,8 @@ describe OllamaChat::Tools::GetTime do
34
35
  /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}\z/
35
36
  )
36
37
  expect(json.weekday).to be_present
38
+ expect(json.message).to eq 'Good morning! It is currently 11:11 on Friday.'
39
+ ensure
40
+ Time.dummy = nil
37
41
  end
38
42
  end
@@ -107,7 +107,7 @@ describe OllamaChat::Tools::GetURL do
107
107
  args = double(url: url, document_policy: 'ignoring')
108
108
  tool_call = double(function: double(arguments: args))
109
109
 
110
- result = described_class.new.execute(tool_call, chat: chat)
110
+ result = described_class.new.execute(tool_call, chat:)
111
111
  json = json_object(result)
112
112
  expect(json.content).to eq('raw content')
113
113
  end
@@ -117,7 +117,7 @@ describe OllamaChat::Tools::GetURL do
117
117
  tool_call = double(function: double(arguments: args))
118
118
  expect(chat).to receive(:import_source).with(source_io, URI.parse(url)).and_return('imported content')
119
119
 
120
- result = described_class.new.execute(tool_call, chat: chat)
120
+ result = described_class.new.execute(tool_call, chat:)
121
121
  json = json_object(result)
122
122
  expect(json.content).to eq('imported content')
123
123
  end
@@ -127,7 +127,7 @@ describe OllamaChat::Tools::GetURL do
127
127
  tool_call = double(function: double(arguments: args))
128
128
  expect(chat).to receive(:embed_source).with(source_io, URI.parse(url)).and_return('embedded content')
129
129
 
130
- result = described_class.new.execute(tool_call, chat: chat)
130
+ result = described_class.new.execute(tool_call, chat:)
131
131
  json = json_object(result)
132
132
  expect(json.content).to eq('embedded content')
133
133
  end
@@ -137,7 +137,7 @@ describe OllamaChat::Tools::GetURL do
137
137
  tool_call = double(function: double(arguments: args))
138
138
  expect(chat).to receive(:summarize_source).with(source_io, URI.parse(url)).and_return('summarized content')
139
139
 
140
- result = described_class.new.execute(tool_call, chat: chat)
140
+ result = described_class.new.execute(tool_call, chat:)
141
141
  json = json_object(result)
142
142
  expect(json.content).to eq('summarized content')
143
143
  end
@@ -146,7 +146,7 @@ describe OllamaChat::Tools::GetURL do
146
146
  args = double(url: url, document_policy: 'chaos_mode')
147
147
  tool_call = double(function: double(arguments: args))
148
148
 
149
- result = described_class.new.execute(tool_call, chat: chat)
149
+ result = described_class.new.execute(tool_call, chat:)
150
150
  json = json_object(result)
151
151
  expect(json.message).to match(/Invalid document policy "chaos_mode"/)
152
152
  end
@@ -163,7 +163,7 @@ describe OllamaChat::Tools::GetURL do
163
163
  args = double(url: url, document_policy: 'ignoring')
164
164
  tool_call = double(function: double(arguments: args))
165
165
 
166
- result = described_class.new.execute(tool_call, chat: chat)
166
+ result = described_class.new.execute(tool_call, chat:)
167
167
  json = json_object(result)
168
168
  expect(json.message).to eq('Received requested URL successfully.')
169
169
  end
@@ -175,7 +175,7 @@ describe OllamaChat::Tools::GetURL do
175
175
  args = double(url: url, document_policy: 'ignoring')
176
176
  tool_call = double(function: double(arguments: args))
177
177
 
178
- result = described_class.new.execute(tool_call, chat: chat)
178
+ result = described_class.new.execute(tool_call, chat:)
179
179
  json = json_object(result)
180
180
  expect(json.message).to match(/Cannot fetch.*with content type/)
181
181
  end
@@ -32,7 +32,7 @@ describe OllamaChat::Tools::PasteFromClipboard do
32
32
  expect(chat).to receive(:perform_paste_from_clipboard).with(edit: false).
33
33
  and_return 'Hello World'
34
34
 
35
- result = described_class.new.execute(tool_call, chat: chat)
35
+ result = described_class.new.execute(tool_call, chat:)
36
36
 
37
37
  # Should return valid JSON
38
38
  expect(result).to be_a(String)
@@ -57,7 +57,7 @@ describe OllamaChat::Tools::PasteFromClipboard do
57
57
  expect(chat).to receive(:perform_paste_from_clipboard).with(edit: true).
58
58
  and_return 'Hello Edited World'
59
59
 
60
- result = described_class.new.execute(tool_call, chat: chat)
60
+ result = described_class.new.execute(tool_call, chat:)
61
61
 
62
62
  # Should return valid JSON
63
63
  expect(result).to be_a(String)
@@ -82,7 +82,7 @@ describe OllamaChat::Tools::PasteFromClipboard do
82
82
  expect(chat).to receive(:perform_paste_from_clipboard).with(edit: false).
83
83
  and_raise(OllamaChat::OllamaChatError, 'No content available to paste from the system clipboard.')
84
84
 
85
- result = described_class.new.execute(tool_call, chat: chat)
85
+ result = described_class.new.execute(tool_call, chat:)
86
86
 
87
87
  # Should return valid JSON even with errors
88
88
  expect(result).to be_a(String)
@@ -106,7 +106,7 @@ describe OllamaChat::Tools::PasteFromClipboard do
106
106
  expect(chat).to receive(:perform_paste_from_clipboard).with(edit: false).
107
107
  and_raise(RuntimeError, 'some kind of exception')
108
108
 
109
- result = described_class.new.execute(tool_call, chat: chat)
109
+ result = described_class.new.execute(tool_call, chat:)
110
110
 
111
111
  # Should return valid JSON even with exceptions
112
112
  expect(result).to be_a(String)
@@ -38,6 +38,7 @@ describe OllamaChat::Tools::ReadFile do
38
38
  expect(json.content).to eq <<~EOT
39
39
  puts "Hello World!"
40
40
  EOT
41
+ expect(json.message).to include('Read 20.0 B (6.0 T) from')
41
42
  end
42
43
 
43
44
  it 'can extract range when start_line is provided and end_line is nil' do
@@ -55,6 +56,7 @@ describe OllamaChat::Tools::ReadFile do
55
56
  result = described_class.new.execute(tool_call, chat:)
56
57
  json = json_object(result)
57
58
  expect(json.content).to eq "puts \"Hello World!\"\n"
59
+ expect(json.message).to include('Read 20.0 B (6.0 T) from')
58
60
  end
59
61
 
60
62
  it 'can extract range when start_line is nil and end_line is provided' do
@@ -72,6 +74,7 @@ describe OllamaChat::Tools::ReadFile do
72
74
  result = described_class.new.execute(tool_call, chat:)
73
75
  json = json_object(result)
74
76
  expect(json.content).to eq "puts \"Hello World!\"\n"
77
+ expect(json.message).to include('Read 20.0 B (6.0 T) from')
75
78
  end
76
79
 
77
80
  it 'returns empty content when end_line is less than start_line' do
@@ -55,6 +55,8 @@ describe OllamaChat::Tools::RetrieveDocumentSnippets do
55
55
  "Consider these snippets generated from retrieval when formulating your response!"
56
56
  )
57
57
  expect(json.snippets.size).to eq 1
58
+ expect(json.message).to include('Retrieved')
59
+ expect(json.message).to include('"Ruby array"')
58
60
  end
59
61
 
60
62
  it 'works with a valid query and tags' do
@@ -95,6 +97,8 @@ describe OllamaChat::Tools::RetrieveDocumentSnippets do
95
97
  expect(result).to be_a(String)
96
98
  json = json_object(result)
97
99
  expect(json.snippets.size).to eq 1
100
+ expect(json.message).to include('Retrieved')
101
+ expect(json.message).to include('"Ruby array"')
98
102
  end
99
103
 
100
104
  it 'switches to the specified collection and restores the original' do
@@ -117,7 +121,8 @@ describe OllamaChat::Tools::RetrieveDocumentSnippets do
117
121
  mock_docs = double('Documents')
118
122
  allow(chat).to receive(:documents).and_return(mock_docs)
119
123
 
120
- expect(mock_docs).to receive(:collection).and_return('default_collection')
124
+ expect(mock_docs).to receive(:collection).and_return('default_collection').
125
+ at_least(:once)
121
126
  expect(mock_docs).to receive(:collection=).with('tolkien').ordered
122
127
  expect(mock_docs).to receive(:collection=).with('default_collection').ordered
123
128
 
@@ -144,6 +149,32 @@ describe OllamaChat::Tools::RetrieveDocumentSnippets do
144
149
  expect(json.message).to eq('Empty query')
145
150
  end
146
151
 
152
+ it 'returns a no-match message when no records are found' do
153
+ tool_call = double(
154
+ 'ToolCall',
155
+ function: double(
156
+ name: 'retrieve_document_snippets',
157
+ arguments: double(
158
+ query: 'nonexistent',
159
+ tags: nil,
160
+ collection: nil,
161
+ min_similarity: nil,
162
+ text_size: nil,
163
+ text_count: nil,
164
+ rerank: false,
165
+ )
166
+ )
167
+ )
168
+
169
+ tool = described_class.new
170
+ expect(tool).to receive(:find_document_records).and_return([])
171
+
172
+ result = tool.execute(tool_call, chat:)
173
+ json = json_object(result)
174
+ expect(json.message).to include('No relevant snippets found')
175
+ expect(json.message).to include('"nonexistent"')
176
+ end
177
+
147
178
  it 'performs reranking when rerank is true' do
148
179
  tool_call = double(
149
180
  'ToolCall',
@@ -170,14 +201,15 @@ describe OllamaChat::Tools::RetrieveDocumentSnippets do
170
201
  expect(tool).to receive(:find_document_records).and_return(records)
171
202
 
172
203
  allow(chat).to receive(:prompt).with('rerank').and_return("template %{query} %{candidates}")
173
- response_val = double('Response', response: double('FullResponse', full?: '1', response: '1'))
174
- allow(chat).to receive(:generate).with(prompt: anything).and_return(response_val)
204
+ allow(chat).to receive(:generate).with(prompt: anything).and_return('1')
175
205
 
176
206
  result = tool.execute(tool_call, chat:)
177
207
  json = json_object(result)
178
208
 
179
209
  expect(json.snippets.size).to eq 1
180
210
  expect(json.snippets.first.text).to eq 'second'
211
+ expect(json.message).to include('Retrieved')
212
+ expect(json.message).to include('"Ruby array"')
181
213
  end
182
214
 
183
215
  it 'performs reranking when rerank is nil (defaults to true)' do
@@ -206,14 +238,15 @@ describe OllamaChat::Tools::RetrieveDocumentSnippets do
206
238
  expect(tool).to receive(:find_document_records).and_return(records)
207
239
 
208
240
  allow(chat).to receive(:prompt).with('rerank').and_return("template %{query} %{candidates}")
209
- response_val = double('Response', response: double('FullResponse', full?: '0', response: '0'))
210
- allow(chat).to receive(:generate).with(prompt: anything).and_return(response_val)
241
+ allow(chat).to receive(:generate).with(prompt: anything).and_return('0')
211
242
 
212
243
  result = tool.execute(tool_call, chat:)
213
244
  json = json_object(result)
214
245
 
215
246
  expect(json.snippets.size).to eq 1
216
247
  expect(json.snippets.first.text).to eq 'first'
248
+ expect(json.message).to include('Retrieved')
249
+ expect(json.message).to include('"Ruby array"')
217
250
  end
218
251
 
219
252
  it 'can be converted to hash' do
@@ -43,7 +43,7 @@ describe OllamaChat::Tools::RollDice do
43
43
  expect(json.rolls).to eq [2, 5]
44
44
  expect(json.modifier).to eq 0
45
45
  expect(json.total).to eq 7
46
- expect(json.message).to eq 'Dice roll was: 2d6 = (2 + 5) = 7'
46
+ expect(json.message).to eq 'Dice roll was: 2d6 = 2…12 = (2 + 5) = 7'
47
47
  end
48
48
 
49
49
  it 'can be executed successfully with d20 notation' do
@@ -64,7 +64,7 @@ describe OllamaChat::Tools::RollDice do
64
64
  json = json_object(result)
65
65
  expect(json.rolls).to eq [10]
66
66
  expect(json.total).to eq 10
67
- expect(json.message).to eq 'Dice roll was: d20 = (10) = 10'
67
+ expect(json.message).to eq 'Dice roll was: d20 = 1…20 = (10) = 10'
68
68
  end
69
69
 
70
70
  it 'can be executed successfully with positive modifier' do
@@ -86,7 +86,7 @@ describe OllamaChat::Tools::RollDice do
86
86
  expect(json.rolls).to eq [15]
87
87
  expect(json.modifier).to eq 3
88
88
  expect(json.total).to eq 18
89
- expect(json.message).to eq 'Dice roll was: d20+3 = (15) + 3 = 18'
89
+ expect(json.message).to eq 'Dice roll was: d20+3 = 4…23 = (15) + 3 = 18'
90
90
  end
91
91
 
92
92
  it 'can be executed successfully with negative modifier' do
@@ -108,7 +108,7 @@ describe OllamaChat::Tools::RollDice do
108
108
  expect(json.rolls).to eq [5]
109
109
  expect(json.modifier).to eq(-3)
110
110
  expect(json.total).to eq 2
111
- expect(json.message).to eq 'Dice roll was: d20-3 = (5) - 3 = 2'
111
+ expect(json.message).to eq 'Dice roll was: d20-3 = -2…17 = (5) - 3 = 2'
112
112
  end
113
113
 
114
114
  it 'can handle invalid dice notation' do