ollama_chat 0.0.134 → 0.0.136
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 +4 -4
- data/CHANGES.md +121 -0
- data/Rakefile +1 -1
- data/lib/ollama_chat/chat.rb +14 -2
- data/lib/ollama_chat/commands.rb +14 -11
- data/lib/ollama_chat/database/models/session.rb +2 -2
- data/lib/ollama_chat/database/{session_locking.rb → row_locking.rb} +11 -16
- data/lib/ollama_chat/database.rb +1 -1
- data/lib/ollama_chat/follow_chat.rb +1 -135
- data/lib/ollama_chat/information.rb +59 -1
- data/lib/ollama_chat/model_handling/model_cache.rb +106 -0
- data/lib/ollama_chat/model_handling.rb +37 -25
- data/lib/ollama_chat/oc.rb +8 -0
- data/lib/ollama_chat/ollama_chat_config/default_config.yml +2 -1
- data/lib/ollama_chat/ollama_chat_config/fastfetch-profile.jsonc +13 -0
- data/lib/ollama_chat/personae_management.rb +20 -2
- data/lib/ollama_chat/prompt_management.rb +1 -1
- data/lib/ollama_chat/rag_handling.rb +2 -2
- data/lib/ollama_chat/session_management.rb +33 -26
- data/lib/ollama_chat/source_fetching.rb +1 -1
- data/lib/ollama_chat/tool_call_processor.rb +176 -0
- data/lib/ollama_chat/tool_calling.rb +0 -16
- data/lib/ollama_chat/tools/execute_shell.rb +72 -32
- data/lib/ollama_chat/version.rb +7 -2
- data/lib/ollama_chat.rb +1 -0
- data/ollama_chat.gemspec +7 -7
- data/spec/ollama_chat/commands_spec.rb +10 -2
- data/spec/ollama_chat/follow_chat_spec.rb +2 -1
- data/spec/ollama_chat/information_spec.rb +1 -1
- data/spec/ollama_chat/kramdown_ansi_spec.rb +1 -1
- data/spec/ollama_chat/model_handling/model_cache_spec.rb +181 -0
- data/spec/ollama_chat/personae_management_spec.rb +75 -0
- data/spec/ollama_chat/session_management_spec.rb +31 -6
- data/spec/ollama_chat/tool_call_processor_spec.rb +51 -0
- data/spec/ollama_chat/tool_calling_spec.rb +0 -26
- data/spec/ollama_chat/tools/execute_shell_spec.rb +68 -0
- metadata +18 -7
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'ollama_chat/model_handling/model_cache'
|
|
2
|
+
|
|
1
3
|
# A module that provides functionality for managing Ollama models, including
|
|
2
4
|
# checking model availability, pulling models from remote servers, and handling
|
|
3
5
|
# model presence verification.
|
|
@@ -416,16 +418,20 @@ module OllamaChat::ModelHandling
|
|
|
416
418
|
# @return [ ModelMetadata, NilClass ] if the model is present,
|
|
417
419
|
# nil otherwise
|
|
418
420
|
def model_present?(model, ollama:)
|
|
421
|
+
name = model.full? or return
|
|
422
|
+
model_metadata = @model_cache.cached?(ollama, name) and return model_metadata
|
|
419
423
|
ollama.show(model:) do |md|
|
|
420
|
-
|
|
421
|
-
name:
|
|
424
|
+
model_metadata = ModelMetadata.new(
|
|
425
|
+
name: ,
|
|
422
426
|
system: md.system,
|
|
423
427
|
capabilities: md.capabilities,
|
|
424
428
|
families: md.details.families,
|
|
425
429
|
thinking: md.thinking,
|
|
426
430
|
)
|
|
427
431
|
end
|
|
428
|
-
|
|
432
|
+
model_metadata or return
|
|
433
|
+
@model_cache.cache(ollama, name, model_metadata)
|
|
434
|
+
rescue Ollama::Errors::NotFoundError, Ollama::Errors::BadRequestError
|
|
429
435
|
nil
|
|
430
436
|
end
|
|
431
437
|
|
|
@@ -467,16 +473,15 @@ module OllamaChat::ModelHandling
|
|
|
467
473
|
# by creating a formatted string that includes the model name and its size
|
|
468
474
|
# in a human-readable format with appropriate units.
|
|
469
475
|
#
|
|
470
|
-
# @param model [
|
|
476
|
+
# @param model [ String ] the model name
|
|
477
|
+
# @param size [ Integer ] the size of the model in bytes
|
|
471
478
|
#
|
|
472
|
-
# @return [
|
|
473
|
-
#
|
|
474
|
-
def model_with_size(model, favourited: false)
|
|
475
|
-
formatted_size = Term::ANSIColor.bold {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
display = prefix_favourite("#{model.name} #{formatted_size}", favourited)
|
|
479
|
-
SearchUI::Wrapper.new(model.name, display:)
|
|
479
|
+
# @return [ SearchUI::Wrapper ] a result object with an overridden to_s
|
|
480
|
+
# method that combines the model name and formatted size
|
|
481
|
+
def model_with_size(model, size, favourited: false)
|
|
482
|
+
formatted_size = Term::ANSIColor.bold { format_bytes(size) }
|
|
483
|
+
display = prefix_favourite("#{model} #{formatted_size}", favourited)
|
|
484
|
+
SearchUI::Wrapper.new(model, display:)
|
|
480
485
|
end
|
|
481
486
|
|
|
482
487
|
# Ensures the specified model is available locally and synchronizes the
|
|
@@ -625,8 +630,13 @@ module OllamaChat::ModelHandling
|
|
|
625
630
|
# metadata
|
|
626
631
|
def all_models
|
|
627
632
|
favs = all_favourited('model')
|
|
628
|
-
|
|
629
|
-
|
|
633
|
+
@model_cache.models(ollama).sort.map { |name|
|
|
634
|
+
model_with_size(
|
|
635
|
+
name,
|
|
636
|
+
@model_cache.model_size(ollama, name),
|
|
637
|
+
favourited: favs[name]
|
|
638
|
+
)
|
|
639
|
+
}
|
|
630
640
|
end
|
|
631
641
|
|
|
632
642
|
# The choose_model method selects a model from the available list based on
|
|
@@ -641,22 +651,24 @@ module OllamaChat::ModelHandling
|
|
|
641
651
|
# @param current_model [String] the fallback model if selection fails
|
|
642
652
|
# @return [String] the selected model name
|
|
643
653
|
def choose_model(cli_model, current_model)
|
|
654
|
+
if model_present?(cli_model, ollama:)
|
|
655
|
+
return cli_model
|
|
656
|
+
end
|
|
644
657
|
selector = if cli_model =~ /\A\?+(.*)\z/
|
|
645
658
|
cli_model = ''
|
|
646
659
|
Regexp.new($1)
|
|
647
660
|
end
|
|
648
661
|
models = all_models
|
|
649
662
|
selector and models = models.select { _1.value =~ selector }
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
end
|
|
663
|
+
if models.size == 1
|
|
664
|
+
models.first.value
|
|
665
|
+
elsif cli_model == ''
|
|
666
|
+
choose_entry(
|
|
667
|
+
models,
|
|
668
|
+
prompt: "Which digital oracle shall we consult? %s"
|
|
669
|
+
)&.value || current_model
|
|
670
|
+
else
|
|
671
|
+
cli_model || current_model
|
|
672
|
+
end
|
|
661
673
|
end
|
|
662
674
|
end
|
data/lib/ollama_chat/oc.rb
CHANGED
|
@@ -89,6 +89,14 @@ module OC
|
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
FASTFETCH = set do
|
|
93
|
+
description 'Path to fastfetch executable for system information'
|
|
94
|
+
|
|
95
|
+
default do
|
|
96
|
+
`which 2>/dev/null fastfetch`.full?(:chomp)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
92
100
|
KRAMDOWN_ANSI_OLLAMA_CHAT_STYLES = set do
|
|
93
101
|
description 'Styles to use for kramdown-ansi markdown'
|
|
94
102
|
|
|
@@ -56,6 +56,7 @@ prompts:
|
|
|
56
56
|
- Location: %{location}
|
|
57
57
|
- Current directory is "%{current_directory}"
|
|
58
58
|
- Client is %{client}.
|
|
59
|
+
- Machine: %{machine}
|
|
59
60
|
dynamic_runtime_info: |
|
|
60
61
|
There is usually no reason to mention this information to the user unless
|
|
61
62
|
asked about it.
|
|
@@ -174,7 +175,7 @@ prompts:
|
|
|
174
175
|
|
|
175
176
|
Create a title with a length of **less than %{length}** characters for
|
|
176
177
|
the conversation above. Output only the title and nothing else.
|
|
177
|
-
|
|
178
|
+
report:
|
|
178
179
|
coding_brief: |
|
|
179
180
|
%{content}
|
|
180
181
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json",
|
|
3
|
+
"display": {
|
|
4
|
+
"showErrors": false,
|
|
5
|
+
"noBuffer": true
|
|
6
|
+
},
|
|
7
|
+
"logo": null,
|
|
8
|
+
"modules": [
|
|
9
|
+
"os", "kernel", "cpu", "gpu", "de", "wm",
|
|
10
|
+
"terminal", "memory", "editor",
|
|
11
|
+
"packages", "chassis", "board", "bios"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -95,15 +95,33 @@ module OllamaChat::PersonaeManagement
|
|
|
95
95
|
def set_default_persona_name(persona_name)
|
|
96
96
|
if persona_name.present? && persona_name != :none
|
|
97
97
|
@default_persona_name = Pathname.new(persona_name).basename.sub_ext('').to_path
|
|
98
|
-
@session.update(default_persona_name:)
|
|
99
98
|
else
|
|
100
|
-
@session.update(default_persona_name: nil)
|
|
101
99
|
@default_persona_name = nil
|
|
102
100
|
end
|
|
101
|
+
@session.update(default_persona_name:)
|
|
102
|
+
sync_trigger
|
|
103
103
|
messages.set_system_prompt(session&.current_system_prompt.full?)
|
|
104
104
|
default_persona_name
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
# Synchronizes the session's trigger collection to match the current
|
|
108
|
+
# default persona.
|
|
109
|
+
#
|
|
110
|
+
# When a persona is set, the trigger is re-keyed (or seeded) to
|
|
111
|
+
# `memory-<persona>`, preserving any existing trigger configuration.
|
|
112
|
+
# When no persona is active, the trigger is set to nil.
|
|
113
|
+
def sync_trigger
|
|
114
|
+
unless @default_persona_name
|
|
115
|
+
session.update(trigger: nil) unless session.trigger.nil?
|
|
116
|
+
return
|
|
117
|
+
end
|
|
118
|
+
collection = create_memory_collection(@default_persona_name)
|
|
119
|
+
return if session.trigger&.keys&.first == collection
|
|
120
|
+
cfg = session.trigger&.values&.first or
|
|
121
|
+
cfg = chat.config.trigger.to_h.stringify_keys_recursive
|
|
122
|
+
session.update(trigger: { collection => cfg })
|
|
123
|
+
end
|
|
124
|
+
|
|
107
125
|
# Interactively selects a persona and sets it as the default for the session.
|
|
108
126
|
#
|
|
109
127
|
# This method prompts the user to choose a persona from the available list
|
|
@@ -381,7 +381,7 @@ module OllamaChat::PromptManagement
|
|
|
381
381
|
start = '%s %s' % [ default, bold { p.name } ]
|
|
382
382
|
start = prefix_favourite(start, favs[p.name])
|
|
383
383
|
content = p.to_s.inspect[1..-2]
|
|
384
|
-
content =
|
|
384
|
+
content = truncate(
|
|
385
385
|
content, length: 0.9 * (Tins::Terminal.columns - start.size)
|
|
386
386
|
)
|
|
387
387
|
feedback '%s %s' % [ start, italic { content } ]
|
|
@@ -336,7 +336,7 @@ module OllamaChat::RAGHandling
|
|
|
336
336
|
output.puts kramdown_ansi_parse(<<~EOT)
|
|
337
337
|
**#{record.source}**
|
|
338
338
|
```
|
|
339
|
-
#{
|
|
339
|
+
#{wrap(record.text.sub(/\n+\z/, ''), percentage: 90)}
|
|
340
340
|
```
|
|
341
341
|
---
|
|
342
342
|
EOT
|
|
@@ -406,7 +406,7 @@ module OllamaChat::RAGHandling
|
|
|
406
406
|
output.puts kramdown_ansi_parse(<<~EOT)
|
|
407
407
|
**[#{record.tags.join(', ')}]**
|
|
408
408
|
```
|
|
409
|
-
#{
|
|
409
|
+
#{wrap(record.text.sub(/\n+\z/, ''), percentage: 90)}
|
|
410
410
|
```
|
|
411
411
|
---
|
|
412
412
|
EOT
|
|
@@ -110,7 +110,7 @@ module OllamaChat::SessionManagement
|
|
|
110
110
|
table.headings = %w[ ID NAME SIZE #TOK COUNT UPDATED ].map { |header| bold { header } }
|
|
111
111
|
now = Time.now
|
|
112
112
|
models::Session.order(Sequel.desc(:updated_at)).each do |s|
|
|
113
|
-
name =
|
|
113
|
+
name = truncate(s.name, length: 32)
|
|
114
114
|
name = session.id == s.id ? bold { name } : name
|
|
115
115
|
name = if pid = s.locked?
|
|
116
116
|
if pid == $$
|
|
@@ -197,8 +197,15 @@ module OllamaChat::SessionManagement
|
|
|
197
197
|
previous_session_id = @session.id
|
|
198
198
|
@session = new_session(name:)
|
|
199
199
|
set_previous_session_on_change(previous_session_id)
|
|
200
|
-
session.lock?
|
|
201
|
-
|
|
200
|
+
unless session.lock?
|
|
201
|
+
feedback(
|
|
202
|
+
"session #{session.id} locked by running process #{session.locked?}",
|
|
203
|
+
type: :warn
|
|
204
|
+
)
|
|
205
|
+
raise OllamaChat::OllamaChatError,
|
|
206
|
+
"Could not lock session #{session.id} #{session.errors.full?(:inspect)}"
|
|
207
|
+
end
|
|
208
|
+
@default_persona_name = session.default_persona_name
|
|
202
209
|
session_apply
|
|
203
210
|
messages.clear
|
|
204
211
|
session.current_model.full? {
|
|
@@ -255,10 +262,15 @@ module OllamaChat::SessionManagement
|
|
|
255
262
|
preferred_session
|
|
256
263
|
end
|
|
257
264
|
session or abort "No session named #{bold{session_name.inspect}} found."
|
|
258
|
-
if session.lock?
|
|
265
|
+
if session.lock?
|
|
259
266
|
messages.read_conversation_jsonl(session.messages.to_s)
|
|
267
|
+
@default_persona_name = session.default_persona_name || nil
|
|
260
268
|
session_apply
|
|
261
269
|
else
|
|
270
|
+
feedback(
|
|
271
|
+
"session #{session.id} locked by running process #{session.locked?}",
|
|
272
|
+
type: :warn
|
|
273
|
+
)
|
|
262
274
|
raise OllamaChat::OllamaChatError,
|
|
263
275
|
"Could not lock session #{session.id} #{session.errors.full?(:inspect)}"
|
|
264
276
|
end
|
|
@@ -279,13 +291,7 @@ module OllamaChat::SessionManagement
|
|
|
279
291
|
# @return [OllamaChat::Database::Models::Session] the applied session
|
|
280
292
|
# instance.
|
|
281
293
|
def session_apply
|
|
282
|
-
|
|
283
|
-
collection = create_memory_collection(persona_name)
|
|
284
|
-
session.trigger.present? or
|
|
285
|
-
session.trigger = {
|
|
286
|
-
collection => chat.config.trigger.to_h.stringify_keys_recursive
|
|
287
|
-
}
|
|
288
|
-
end
|
|
294
|
+
sync_trigger
|
|
289
295
|
session.update(working_directory: Dir.pwd)
|
|
290
296
|
init_history
|
|
291
297
|
repair_group_uuids
|
|
@@ -442,7 +448,7 @@ module OllamaChat::SessionManagement
|
|
|
442
448
|
|
|
443
449
|
# Generates a report document for the current session's conversation.
|
|
444
450
|
#
|
|
445
|
-
# Uses a user-selected prompt template from the '
|
|
451
|
+
# Uses a user-selected prompt template from the 'report' context
|
|
446
452
|
# (e.g. coding_brief, roleplay_story) to produce a single cohesive
|
|
447
453
|
# document describing the session. Uses `compacted_messages` (summary +
|
|
448
454
|
# recent tail) rather than `each_message` so the interpolated content
|
|
@@ -459,10 +465,10 @@ module OllamaChat::SessionManagement
|
|
|
459
465
|
content.empty? and return
|
|
460
466
|
|
|
461
467
|
template = if name
|
|
462
|
-
prompt(name, context: '
|
|
468
|
+
prompt(name, context: 'report')
|
|
463
469
|
else
|
|
464
470
|
choose_prompt(
|
|
465
|
-
context: '
|
|
471
|
+
context: 'report',
|
|
466
472
|
prompt: 'Which report template? %s'
|
|
467
473
|
)
|
|
468
474
|
end
|
|
@@ -601,7 +607,7 @@ module OllamaChat::SessionManagement
|
|
|
601
607
|
name = name.
|
|
602
608
|
gsub(/(\A(\s|[^A-Za-z])+|(\s|[^A-Za-z])+\z)/m, '').
|
|
603
609
|
gsub(/\s+/, ' ')
|
|
604
|
-
|
|
610
|
+
truncate(name, length:)
|
|
605
611
|
end
|
|
606
612
|
end
|
|
607
613
|
end
|
|
@@ -649,7 +655,7 @@ module OllamaChat::SessionManagement
|
|
|
649
655
|
session_close
|
|
650
656
|
previous_session = session
|
|
651
657
|
@session = chosen_session
|
|
652
|
-
if session.lock?
|
|
658
|
+
if session.lock?
|
|
653
659
|
messages.read_conversation_jsonl(session.messages.to_s)
|
|
654
660
|
if current_collection = session.current_collection.full? and
|
|
655
661
|
database_collection?(current_collection)
|
|
@@ -666,6 +672,10 @@ module OllamaChat::SessionManagement
|
|
|
666
672
|
info_session
|
|
667
673
|
break
|
|
668
674
|
else
|
|
675
|
+
feedback(
|
|
676
|
+
"session #{session.id} locked by running process #{session.locked?}",
|
|
677
|
+
type: :warn
|
|
678
|
+
)
|
|
669
679
|
@session = previous_session
|
|
670
680
|
@session.lock
|
|
671
681
|
name = ??
|
|
@@ -737,16 +747,13 @@ module OllamaChat::SessionManagement
|
|
|
737
747
|
else
|
|
738
748
|
' '
|
|
739
749
|
end
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
SearchUI::Wrapper.new(
|
|
744
|
-
session.name,
|
|
745
|
-
display:
|
|
746
|
-
)
|
|
750
|
+
suffix = " 🆔#{session.id}#{locked}📨#{count} 🧩#{es.tokens_formatted} ⏳#{duration}"
|
|
751
|
+
display_session_name = truncate(session.name, length: (0.8 * (width - suffix.length)).floor.clamp(1..))
|
|
752
|
+
display = "#{display_session_name}#{suffix}"
|
|
753
|
+
SearchUI::Wrapper.new([ session.id, session.name ] * ?-, display:)
|
|
747
754
|
}
|
|
748
755
|
selector and sessions = sessions.select { _1 =~ selector }
|
|
749
|
-
|
|
756
|
+
session_selected =
|
|
750
757
|
if sessions.size == 1
|
|
751
758
|
sessions.first.value
|
|
752
759
|
else
|
|
@@ -766,8 +773,8 @@ module OllamaChat::SessionManagement
|
|
|
766
773
|
end
|
|
767
774
|
value
|
|
768
775
|
end
|
|
769
|
-
if
|
|
770
|
-
session_query.first(
|
|
776
|
+
if session_selected
|
|
777
|
+
session_query.first(id: session_selected.split(?-, 2).first)
|
|
771
778
|
end
|
|
772
779
|
end
|
|
773
780
|
end
|
|
@@ -244,7 +244,7 @@ module OllamaChat::SourceFetching
|
|
|
244
244
|
command = false
|
|
245
245
|
inputs = inputs.map { |chunk| "Source: #{source}\n#{chunk}" }
|
|
246
246
|
if source.start_with?(?!)
|
|
247
|
-
source =
|
|
247
|
+
source = truncate(
|
|
248
248
|
source[1..-1].gsub(/\W+/, ?_),
|
|
249
249
|
length: 10
|
|
250
250
|
)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Processes tool calls from Ollama responses: executes registered tools,
|
|
2
|
+
# collects results, and provides a drain interface for the chat loop to
|
|
3
|
+
# consume them as follow-up messages.
|
|
4
|
+
#
|
|
5
|
+
# This class encapsulates the full tool-call lifecycle that was previously
|
|
6
|
+
# split between {OllamaChat::FollowChat#handle_tool_calls} (execution)
|
|
7
|
+
# and {OllamaChat::ToolCalling#handle_tool_call_results?} (consumption).
|
|
8
|
+
class OllamaChat::ToolCallProcessor
|
|
9
|
+
include Ollama::Handlers::Concern
|
|
10
|
+
include Term::ANSIColor
|
|
11
|
+
include OllamaChat::Feedback
|
|
12
|
+
|
|
13
|
+
# @return [OllamaChat::Chat] the chat context
|
|
14
|
+
attr_reader :chat
|
|
15
|
+
|
|
16
|
+
# The accumulated tool results, keyed by tool name.
|
|
17
|
+
#
|
|
18
|
+
# @return [Hash{String => Array<String>}]
|
|
19
|
+
attr_reader :results
|
|
20
|
+
|
|
21
|
+
# @param chat [OllamaChat::Chat] the chat context
|
|
22
|
+
# @param output [IO] the output stream for feedback (default: STDOUT)
|
|
23
|
+
def initialize(chat:, output: STDOUT)
|
|
24
|
+
super(output:)
|
|
25
|
+
@chat = chat
|
|
26
|
+
@results = Hash.new { |h, name| h[name] = [] }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Processes tool calls from a response, executing each tool and
|
|
30
|
+
# storing the result in the internal results hash.
|
|
31
|
+
#
|
|
32
|
+
# @param response [Ollama::Response] the parsed response
|
|
33
|
+
def handle(response)
|
|
34
|
+
return unless response.message.ask_and_send(:tool_calls)
|
|
35
|
+
|
|
36
|
+
tools_used = {}
|
|
37
|
+
|
|
38
|
+
response.message.tool_calls.each do |tool_call|
|
|
39
|
+
name = tool_call.function.name
|
|
40
|
+
if %r(.*[/:](?<new_name>.+)) =~ name
|
|
41
|
+
msg = "Received namespaced tool call for #{name}, correcting to #{new_name}"
|
|
42
|
+
chat.log(:warn, msg, data: { tool: name, new_name: })
|
|
43
|
+
name = new_name
|
|
44
|
+
end
|
|
45
|
+
unless chat.tool_configured?(name)
|
|
46
|
+
msg = "Error: Unconfigured tool named %s ignored => Skip.\n" % name
|
|
47
|
+
@results[name] << msg
|
|
48
|
+
chat.log(:error, msg, data: { tool: name, reason: :unconfigured })
|
|
49
|
+
next
|
|
50
|
+
end
|
|
51
|
+
unless chat.tool_registered?(name)
|
|
52
|
+
msg = "Error: Unregistered tool named %s ignored => Skip.\n" % name
|
|
53
|
+
@results[name] << msg
|
|
54
|
+
chat.log(:error, msg, data: { tool: name, reason: :unregistered })
|
|
55
|
+
next
|
|
56
|
+
end
|
|
57
|
+
unless chat.tool_enabled?(name)
|
|
58
|
+
msg = "Error: Disabled tool named %s ignored => Skip.\n" % name
|
|
59
|
+
@results[name] << msg
|
|
60
|
+
chat.log(:error, msg, data: { tool: name, reason: :disabled })
|
|
61
|
+
next
|
|
62
|
+
end
|
|
63
|
+
feedback(?\n, output:)
|
|
64
|
+
confirmed = :implicit
|
|
65
|
+
resolve = nil
|
|
66
|
+
function = JSON.pretty_generate(tool_call.function)
|
|
67
|
+
chat.log(:info, "Tool call received", data: { tool: name, function: })
|
|
68
|
+
if chat.tool_function(name).require_confirmation?
|
|
69
|
+
feedback(
|
|
70
|
+
("I want to execute tool %s\n%s\n" % [ bold { name }, italic { function }, ]),
|
|
71
|
+
type: :alarm
|
|
72
|
+
)
|
|
73
|
+
prompt = '❓ Allow ✅[y]es / ⛔️[n]o / 📝[i]nstruct? '
|
|
74
|
+
answer = chat.confirm?(prompt:)&.to_s&.downcase
|
|
75
|
+
resolve = 'You **MUST** ask the user for instructions on how to proceed!!!'
|
|
76
|
+
case answer
|
|
77
|
+
when 'y'
|
|
78
|
+
confirmed = :explicit
|
|
79
|
+
when 'n'
|
|
80
|
+
confirmed = :denied
|
|
81
|
+
else
|
|
82
|
+
confirmed = :denied
|
|
83
|
+
instr = chat.ask?(prompt: '📝 Instructions: ')&.strip
|
|
84
|
+
resolve = instr.full? || resolve
|
|
85
|
+
end
|
|
86
|
+
else
|
|
87
|
+
feedback(
|
|
88
|
+
"Executing tool %s\n%s" % [ bold { name }, italic { function }, ]
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
start = Time.now
|
|
92
|
+
result = nil
|
|
93
|
+
case confirmed
|
|
94
|
+
when :denied
|
|
95
|
+
result = JSON(message: 'User denied confirmation!', resolve:)
|
|
96
|
+
feedback(?\n)
|
|
97
|
+
feedback(
|
|
98
|
+
("Execution of tool %s denied by user.\n\n" % [ bold { name } ]),
|
|
99
|
+
type: :denied
|
|
100
|
+
)
|
|
101
|
+
chat.log(:warn, "Tool execution denied", data: { tool: name, reason: :denied })
|
|
102
|
+
else
|
|
103
|
+
symbol = confirmed == :implicit ? '☑️ ' : '✅'
|
|
104
|
+
feedback(
|
|
105
|
+
("\n%s Execution of tool %s confirmed.\n\n" % [ symbol, bold { name } ])
|
|
106
|
+
)
|
|
107
|
+
result = OllamaChat::Tools.registered_tool(name, chat:).execute(tool_call)
|
|
108
|
+
chat.log(:info, "Tool execution confirmed", data: { tool: name, confirmed: })
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
@results[name] << result
|
|
112
|
+
data = nil
|
|
113
|
+
message = begin
|
|
114
|
+
data = JSON.parse(result)
|
|
115
|
+
chat.log(:info, "Tool result", data: { result: data })
|
|
116
|
+
data.to_hash['message'] if data.respond_to?(:to_hash)
|
|
117
|
+
rescue
|
|
118
|
+
chat.log(:info, "Tool result", data: { result: })
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
warn =
|
|
122
|
+
begin
|
|
123
|
+
!!data.ask_and_send(:[], 'error') || data.ask_and_send(:[], 'success') == false
|
|
124
|
+
rescue
|
|
125
|
+
false
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
es = OllamaChat::TokenEstimator.estimate(result.to_s)
|
|
129
|
+
|
|
130
|
+
tools_used[name] = {
|
|
131
|
+
message:,
|
|
132
|
+
warn: ,
|
|
133
|
+
size: es.bytes_formatted,
|
|
134
|
+
tokens: es.tokens_formatted,
|
|
135
|
+
duration: Tins::Duration.new(Time.now - start).to_s,
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
if tools_used.full?
|
|
140
|
+
infobar.reset
|
|
141
|
+
tools_used.each do |name, info|
|
|
142
|
+
feedback_message =
|
|
143
|
+
if info[:message]
|
|
144
|
+
"\n%s %s\n\n" % [ info[:warn] ? '⚠️' : '💡', info[:message] ]
|
|
145
|
+
end
|
|
146
|
+
feedback(<<~EOT.strip)
|
|
147
|
+
🔧 Tool functions #{name} returned result (#{info[:size]}/#{info[:tokens]} in #{info[:duration]}).
|
|
148
|
+
#{feedback_message}
|
|
149
|
+
EOT
|
|
150
|
+
feedback(?\n)
|
|
151
|
+
timeout = chat.tool_function(name).result_display_timeout?
|
|
152
|
+
chat.confirm?(prompt: '⏎ Press any key to continue (%s). ', timeout:)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Drains all accumulated results, yielding each entry to the block,
|
|
158
|
+
# then clears the internal storage.
|
|
159
|
+
#
|
|
160
|
+
# @yield [index, tool_name, content] each result entry
|
|
161
|
+
# @return [true, false] whether any results were present
|
|
162
|
+
def drain(&block)
|
|
163
|
+
@results.each do |tool_name, results|
|
|
164
|
+
results.each_with_index { |content, index|
|
|
165
|
+
block.(index, tool_name, content)
|
|
166
|
+
}
|
|
167
|
+
end.present?
|
|
168
|
+
ensure
|
|
169
|
+
@results.clear
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Clears all accumulated results without yielding them.
|
|
173
|
+
def clear!
|
|
174
|
+
@results.clear
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -94,11 +94,6 @@ module OllamaChat::ToolCalling
|
|
|
94
94
|
Array(config.tools&.functions&.attribute_names&.map(&:to_s)).sort
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
# The tool_call_results reader returns the tools' results for the
|
|
98
|
-
# chat session if any.
|
|
99
|
-
#
|
|
100
|
-
# @return [ Hash ] a hash containing the registered tool results
|
|
101
|
-
attr_reader :tool_call_results
|
|
102
97
|
|
|
103
98
|
# The list_tools method displays the sorted list of enabled tools.
|
|
104
99
|
#
|
|
@@ -183,15 +178,4 @@ module OllamaChat::ToolCalling
|
|
|
183
178
|
end
|
|
184
179
|
end
|
|
185
180
|
|
|
186
|
-
private
|
|
187
|
-
|
|
188
|
-
def handle_tool_call_results?(&block)
|
|
189
|
-
@tool_call_results.each do |tool_name, results|
|
|
190
|
-
results.each_with_index { |content, index|
|
|
191
|
-
block.(index, tool_name, content)
|
|
192
|
-
}
|
|
193
|
-
end.present?
|
|
194
|
-
ensure
|
|
195
|
-
@tool_call_results.clear
|
|
196
|
-
end
|
|
197
181
|
end
|