ollama_chat 0.0.113 → 0.0.114
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 +81 -0
- data/README.md +7 -0
- data/bin/ollama_chat_log +1 -1
- data/bin/ollama_chat_send +3 -1
- data/lib/ollama_chat/chat.rb +30 -14
- data/lib/ollama_chat/commands.rb +36 -18
- data/lib/ollama_chat/compaction.rb +289 -0
- data/lib/ollama_chat/config_handling.rb +32 -0
- data/lib/ollama_chat/database/migrations/010_add_enabled_to_collections.rb +7 -0
- data/lib/ollama_chat/database/models/app_state.rb +48 -16
- data/lib/ollama_chat/database/models/session.rb +1 -2
- data/lib/ollama_chat/database.rb +4 -2
- data/lib/ollama_chat/information.rb +53 -6
- data/lib/ollama_chat/logging.rb +3 -2
- data/lib/ollama_chat/message.rb +22 -1
- data/lib/ollama_chat/message_format.rb +1 -2
- data/lib/ollama_chat/message_list.rb +223 -9
- data/lib/ollama_chat/model_handling.rb +1 -1
- data/lib/ollama_chat/oc.rb +17 -7
- data/lib/ollama_chat/ollama_chat_config/default_config.yml +68 -3
- data/lib/ollama_chat/parsing.rb +2 -1
- data/lib/ollama_chat/prompt_handling.rb +1 -1
- data/lib/ollama_chat/prompt_management.rb +1 -0
- data/lib/ollama_chat/rag_handling.rb +19 -5
- data/lib/ollama_chat/session_management.rb +5 -3
- data/lib/ollama_chat/token_estimator/crude.rb +1 -1
- data/lib/ollama_chat/token_estimator.rb +1 -1
- data/lib/ollama_chat/tools/concern.rb +17 -0
- data/lib/ollama_chat/tools/directory_structure.rb +11 -7
- data/lib/ollama_chat/tools/eval_ruby.rb +16 -6
- data/lib/ollama_chat/tools/get_rfc.rb +2 -0
- data/lib/ollama_chat/tools/lookup_group.rb +100 -0
- data/lib/ollama_chat/tools/paste_from_clipboard.rb +1 -1
- data/lib/ollama_chat/tools/patch_file.rb +23 -5
- data/lib/ollama_chat/tools/read_file.rb +1 -1
- data/lib/ollama_chat/tools/resolve_tag.rb +1 -1
- data/lib/ollama_chat/tools/search_knowledge.rb +7 -1
- data/lib/ollama_chat/tools/write_file.rb +25 -6
- data/lib/ollama_chat/tools.rb +1 -0
- data/lib/ollama_chat/utils/analyze_directory.rb +5 -4
- data/lib/ollama_chat/utils/fetcher.rb +1 -1
- data/lib/ollama_chat/utils/log_rotation.rb +29 -0
- data/lib/ollama_chat/utils/tag_resolver.rb +3 -3
- data/lib/ollama_chat/utils.rb +1 -0
- data/lib/ollama_chat/version.rb +1 -1
- data/lib/ollama_chat.rb +7 -0
- data/ollama_chat.gemspec +5 -5
- data/spec/ollama_chat/commands_spec.rb +25 -0
- data/spec/ollama_chat/compaction_spec.rb +381 -0
- data/spec/ollama_chat/config_handling_spec.rb +60 -0
- data/spec/ollama_chat/message_list_spec.rb +229 -0
- data/spec/ollama_chat/message_spec.rb +143 -0
- data/spec/ollama_chat/rag_handling_spec.rb +3 -0
- data/spec/ollama_chat/system_prompt_management_spec.rb +0 -2
- data/spec/ollama_chat/tools/browse_spec.rb +7 -0
- data/spec/ollama_chat/tools/compute_bmi_spec.rb +15 -0
- data/spec/ollama_chat/tools/concern_spec.rb +77 -0
- data/spec/ollama_chat/tools/copy_to_clipboard_spec.rb +8 -0
- data/spec/ollama_chat/tools/delete_file_spec.rb +8 -0
- data/spec/ollama_chat/tools/directory_structure_spec.rb +4 -0
- data/spec/ollama_chat/tools/execute_grep_spec.rb +13 -0
- data/spec/ollama_chat/tools/execute_ri_spec.rb +8 -0
- data/spec/ollama_chat/tools/file_context_spec.rb +5 -0
- data/spec/ollama_chat/tools/gem_path_lookup_spec.rb +2 -0
- data/spec/ollama_chat/tools/generate_image_spec.rb +12 -0
- data/spec/ollama_chat/tools/generate_password_spec.rb +5 -0
- data/spec/ollama_chat/tools/get_cve_spec.rb +4 -0
- data/spec/ollama_chat/tools/get_endoflife_spec.rb +4 -0
- data/spec/ollama_chat/tools/get_ghr_spec.rb +6 -0
- data/spec/ollama_chat/tools/get_location_spec.rb +4 -0
- data/spec/ollama_chat/tools/get_rfc_spec.rb +2 -0
- data/spec/ollama_chat/tools/get_time_spec.rb +2 -0
- data/spec/ollama_chat/tools/get_url_spec.rb +7 -0
- data/spec/ollama_chat/tools/lookup_group_spec.rb +111 -0
- data/spec/ollama_chat/tools/move_file_spec.rb +10 -0
- data/spec/ollama_chat/tools/open_file_in_editor_spec.rb +6 -0
- data/spec/ollama_chat/tools/paste_from_clipboard_spec.rb +5 -0
- data/spec/ollama_chat/tools/paste_into_editor_spec.rb +6 -0
- data/spec/ollama_chat/tools/patch_file_spec.rb +62 -0
- data/spec/ollama_chat/tools/read_file_spec.rb +7 -0
- data/spec/ollama_chat/tools/resolve_tag_spec.rb +46 -0
- data/spec/ollama_chat/tools/roll_dice_spec.rb +11 -0
- data/spec/ollama_chat/tools/run_tests_spec.rb +8 -0
- data/spec/ollama_chat/tools/search_knowledge_spec.rb +2 -0
- data/spec/ollama_chat/tools/write_file_spec.rb +96 -0
- data/spec/ollama_chat/utils/analyze_directory_spec.rb +46 -7
- data/spec/ollama_chat/utils/log_rotation_spec.rb +71 -0
- data/spec/spec_helper.rb +2 -1
- metadata +19 -1
|
@@ -74,6 +74,7 @@ prompts:
|
|
|
74
74
|
markdown tables.
|
|
75
75
|
- **Session**: %{session_name}
|
|
76
76
|
- Context usage is %{context_usage}.
|
|
77
|
+
- Full conversation length is %{conversation_length}.
|
|
77
78
|
- Markdown output is %{markdown}. **Never** output markdown as your
|
|
78
79
|
responses if it is disabled.
|
|
79
80
|
- Voice output is %{voice}. Speak naturally and short if voice
|
|
@@ -170,10 +171,10 @@ prompts:
|
|
|
170
171
|
|
|
171
172
|
%{message_content}
|
|
172
173
|
session_title: |
|
|
173
|
-
Create a title with a length of **less than %{length}** characters for this
|
|
174
|
-
conversation. Output only the title and nothing else:
|
|
175
|
-
|
|
176
174
|
%{content}
|
|
175
|
+
|
|
176
|
+
Create a title with a length of **less than %{length}** characters for
|
|
177
|
+
the conversation above. Output only the title and nothing else.
|
|
177
178
|
context_template_suggest: |
|
|
178
179
|
Conversation History:
|
|
179
180
|
%{history}
|
|
@@ -341,6 +342,39 @@ prompts:
|
|
|
341
342
|
|
|
342
343
|
Output only the 9 suggestions as plain text, each separated by one empty line.
|
|
343
344
|
No other text.
|
|
345
|
+
compaction:
|
|
346
|
+
system: |
|
|
347
|
+
You are a context summarizer for a chat application.
|
|
348
|
+
Your sole task is to produce a concise, factual narrative
|
|
349
|
+
summary of the conversation provided. Be precise, omit
|
|
350
|
+
speculation, and follow the formatting instructions in the
|
|
351
|
+
user prompt exactly. Output only the summary text.
|
|
352
|
+
summarize: |
|
|
353
|
+
previous_summary:
|
|
354
|
+
%{previous}
|
|
355
|
+
|
|
356
|
+
conversation:
|
|
357
|
+
%{groups}
|
|
358
|
+
|
|
359
|
+
Summarize the conversation above. Your narrative MUST:
|
|
360
|
+
- Name every file created or significantly modified (with
|
|
361
|
+
path), and the key methods or classes they define.
|
|
362
|
+
- State the architectural or design decisions made (e.g.
|
|
363
|
+
data-model choices, API shape, error-handling strategy).
|
|
364
|
+
- Describe the current state: what works, what is pending,
|
|
365
|
+
and any open questions or follow-ups.
|
|
366
|
+
- Do NOT merely report test counts or coverage percentages;
|
|
367
|
+
those are outcomes, not accomplishments.
|
|
368
|
+
- Be factual; do not invent details not present in the groups
|
|
369
|
+
above. Keep it to 6–12 sentences.
|
|
370
|
+
assemble: |-
|
|
371
|
+
%{narrative}
|
|
372
|
+
|
|
373
|
+
tool_calls:
|
|
374
|
+
%{tool_section}
|
|
375
|
+
|
|
376
|
+
To see the full output of any tool call above, use lookup_group
|
|
377
|
+
with the group UUID in parentheses.
|
|
344
378
|
system:
|
|
345
379
|
default: <%= OC::OLLAMA::CHAT::SYSTEM || "%{persona}\n\n%{runtime_info}".inspect %>
|
|
346
380
|
persona: |
|
|
@@ -359,12 +393,22 @@ roles:
|
|
|
359
393
|
markdown: true
|
|
360
394
|
stream: true
|
|
361
395
|
document_policy: importing
|
|
396
|
+
parsing:
|
|
397
|
+
directory_structure_max_depth: 1
|
|
362
398
|
think:
|
|
363
399
|
mode: disabled
|
|
364
400
|
loud: true
|
|
365
401
|
strip: false
|
|
366
402
|
context:
|
|
367
403
|
format: JSON
|
|
404
|
+
compaction:
|
|
405
|
+
enabled: true
|
|
406
|
+
reserve:
|
|
407
|
+
ratio: 0.60
|
|
408
|
+
min_tokens: 4096
|
|
409
|
+
keep_recent:
|
|
410
|
+
ratio: 0.20
|
|
411
|
+
min_tokens: 2048
|
|
368
412
|
embedding:
|
|
369
413
|
enabled: true
|
|
370
414
|
paused: false
|
|
@@ -492,6 +536,10 @@ tools:
|
|
|
492
536
|
gem_path_lookup:
|
|
493
537
|
result_display_timeout: 3
|
|
494
538
|
default: true
|
|
539
|
+
lookup_group:
|
|
540
|
+
default: true
|
|
541
|
+
require_confirmation: false
|
|
542
|
+
result_display_timeout: 3
|
|
495
543
|
open_file_in_editor:
|
|
496
544
|
default: true
|
|
497
545
|
require_confirmation: false
|
|
@@ -561,3 +609,20 @@ tools:
|
|
|
561
609
|
eval_ruby:
|
|
562
610
|
default: false
|
|
563
611
|
result_display_timeout: 3
|
|
612
|
+
syntax_checkers:
|
|
613
|
+
ruby:
|
|
614
|
+
cmd: [ 'ruby', '-wc' ]
|
|
615
|
+
suffixes: [ 'rb', 'rake', 'gemspec' ]
|
|
616
|
+
enabled: true
|
|
617
|
+
javascript:
|
|
618
|
+
cmd: [ 'node', '--check' ]
|
|
619
|
+
suffixes: [ 'js', 'mjs' ]
|
|
620
|
+
enabled: false
|
|
621
|
+
python:
|
|
622
|
+
cmd: [ 'python3', '-m', 'py_compile' ]
|
|
623
|
+
suffixes: [ 'py' ]
|
|
624
|
+
enabled: false
|
|
625
|
+
shell:
|
|
626
|
+
cmd: [ 'bash', '-n' ]
|
|
627
|
+
suffixes: [ 'sh', 'bash' ]
|
|
628
|
+
enabled: true
|
data/lib/ollama_chat/parsing.rb
CHANGED
|
@@ -279,7 +279,8 @@ module OllamaChat::Parsing
|
|
|
279
279
|
contents = [ content ]
|
|
280
280
|
content.scan(CONTENT_REGEXP).each { |url, file_url, quoted_file, file|
|
|
281
281
|
if file && Pathname.new(file).expand_path.directory?
|
|
282
|
-
|
|
282
|
+
max_depth = config.parsing.directory_structure_max_depth
|
|
283
|
+
contents << generate_structure(file, max_depth:).to_json
|
|
283
284
|
next
|
|
284
285
|
end
|
|
285
286
|
check_exist = false
|
|
@@ -23,8 +23,6 @@ module OllamaChat::RAGHandling
|
|
|
23
23
|
@documents.collection = old_collection
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
private
|
|
27
|
-
|
|
28
26
|
# Looks up a collection in the database by name.
|
|
29
27
|
#
|
|
30
28
|
# @param collection [String, Symbol, #to_s] the collection name to look up
|
|
@@ -34,6 +32,8 @@ module OllamaChat::RAGHandling
|
|
|
34
32
|
models::Collection[name: collection.to_s]
|
|
35
33
|
end
|
|
36
34
|
|
|
35
|
+
private
|
|
36
|
+
|
|
37
37
|
# Returns the name of the currently active document collection.
|
|
38
38
|
#
|
|
39
39
|
# @return [String, Symbol] the name of the current collection
|
|
@@ -160,12 +160,13 @@ module OllamaChat::RAGHandling
|
|
|
160
160
|
# highlighting the active one.
|
|
161
161
|
def list_collections
|
|
162
162
|
current_collection = collection.to_s
|
|
163
|
-
collections = all_collections.select(:name, :description)
|
|
163
|
+
collections = all_collections.select(:name, :description, :enabled)
|
|
164
164
|
use_pager do |output|
|
|
165
165
|
collections.each { |c|
|
|
166
166
|
collection_name = current_collection == c.name ? bold { c.name } : c.name
|
|
167
167
|
collection_description = c.description
|
|
168
|
-
|
|
168
|
+
suffix = c.enabled ? '' : ' [DISABLED]'
|
|
169
|
+
output.puts '%s: %s%s' % [ collection_name, collection_description, suffix ]
|
|
169
170
|
}
|
|
170
171
|
end
|
|
171
172
|
end
|
|
@@ -306,13 +307,26 @@ module OllamaChat::RAGHandling
|
|
|
306
307
|
extract_patterns(patterns_str)
|
|
307
308
|
end
|
|
308
309
|
|
|
310
|
+
enabled_prompt = col.enabled ?
|
|
311
|
+
"🚫 Disable this collection? (hidden from the model) (y/n) " :
|
|
312
|
+
"✅ Enable this collection? (visible to the model) (y/n) "
|
|
313
|
+
toggle = case confirm?(prompt: enabled_prompt)
|
|
314
|
+
when /\Ay/i then true
|
|
315
|
+
when /\An/i then false
|
|
316
|
+
end
|
|
317
|
+
|
|
309
318
|
col.description = new_description.full? ? new_description.to_s : col.description
|
|
310
319
|
col.patterns = patterns
|
|
320
|
+
col.enabled = toggle ^ col.enabled unless toggle.nil?
|
|
311
321
|
|
|
312
322
|
begin
|
|
313
323
|
col.save
|
|
324
|
+
if toggle
|
|
325
|
+
status = col.enabled ? 'enabled' : 'disabled'
|
|
326
|
+
STDOUT.puts "🔄 Collection '#{col.name}' is now #{status}."
|
|
327
|
+
end
|
|
314
328
|
STDOUT.puts "✅ Updated collection '#{col.name}'."
|
|
315
|
-
log(:info, "Collection updated", data: { name: col.name })
|
|
329
|
+
log(:info, "Collection updated", data: { name: col.name, enabled: col.enabled })
|
|
316
330
|
rescue Sequel::Error => e
|
|
317
331
|
STDERR.puts "❌ Database error: #{e.message}"
|
|
318
332
|
end
|
|
@@ -13,7 +13,7 @@ module OllamaChat::SessionManagement
|
|
|
13
13
|
output = StringIO.new
|
|
14
14
|
messages.write_conversation_jsonl(output)
|
|
15
15
|
session.update(messages: output.string)
|
|
16
|
-
es = session.estimate_tokens
|
|
16
|
+
es = session.estimate_tokens # We just use the formatted bytecount
|
|
17
17
|
log(:info, "Messages stored in session", data: {
|
|
18
18
|
session_id: session.id,
|
|
19
19
|
size: es.bytes_formatted,
|
|
@@ -135,9 +135,11 @@ module OllamaChat::SessionManagement
|
|
|
135
135
|
#
|
|
136
136
|
# @param output [IO] the output stream to write the information to (default: STDOUT)
|
|
137
137
|
def show_session(output: STDOUT)
|
|
138
|
-
es =
|
|
138
|
+
es = messages.full_estimate_tokens
|
|
139
139
|
messages_count = session.count_messages
|
|
140
|
-
output.puts "#{bold{session.name}} (#{italic{session.id}}),
|
|
140
|
+
output.puts "#{bold{session.name}} (#{italic{session.id}}), "\
|
|
141
|
+
"#{es.tokens_formatted} (#{es.bytes_formatted}), "\
|
|
142
|
+
"#{messages_count} messages"
|
|
141
143
|
end
|
|
142
144
|
|
|
143
145
|
# Interactively prompts the user for a unique session name.
|
|
@@ -7,7 +7,7 @@ class OllamaChat::TokenEstimator::Crude
|
|
|
7
7
|
# @raise [ArgumentError] if the input is not a string or an integer.
|
|
8
8
|
def initialize(arg)
|
|
9
9
|
if text = arg.ask_and_send(:to_str)
|
|
10
|
-
@bytes = text.
|
|
10
|
+
@bytes = text.bytesize
|
|
11
11
|
elsif bytes = arg.ask_and_send(:to_int)
|
|
12
12
|
@bytes = bytes
|
|
13
13
|
else
|
|
@@ -10,7 +10,7 @@ require 'ollama_chat/token_estimator/crude'
|
|
|
10
10
|
module OllamaChat::TokenEstimator
|
|
11
11
|
# Represents the result of a calculation including raw values
|
|
12
12
|
# and their human-readable formatted strings.
|
|
13
|
-
class Estimate <
|
|
13
|
+
class Estimate < Data.define(:bytes, :tokens)
|
|
14
14
|
include OllamaChat::Utils::ValueFormatter
|
|
15
15
|
|
|
16
16
|
# Returns the byte count in a formatted string (e.g., "1.2 KB").
|
|
@@ -29,6 +29,23 @@ module OllamaChat::Tools::Concern
|
|
|
29
29
|
#
|
|
30
30
|
# @return [ String ] the register name of the tool
|
|
31
31
|
attr_accessor :register_name
|
|
32
|
+
|
|
33
|
+
# Generates a one-line summary of a tool execution result for inclusion
|
|
34
|
+
# in compaction narratives.
|
|
35
|
+
#
|
|
36
|
+
# The default implementation extracts the `message` field from the
|
|
37
|
+
# result JSON (most tools already produce one). Falls back to a
|
|
38
|
+
# generic sentence if the field is absent or parsing fails.
|
|
39
|
+
#
|
|
40
|
+
# @param result [String] the raw JSON result string returned by `execute`
|
|
41
|
+
# @return [String] a short natural-language description of the call
|
|
42
|
+
def summary_template(result:)
|
|
43
|
+
default_message = "was called."
|
|
44
|
+
data = JSON.parse(result)
|
|
45
|
+
data.is_a?(Hash) && data['message'] || default_message
|
|
46
|
+
rescue JSON::ParserError
|
|
47
|
+
default_message
|
|
48
|
+
end
|
|
32
49
|
end
|
|
33
50
|
|
|
34
51
|
# The name method returns the registered name of the tool.
|
|
@@ -28,10 +28,10 @@ class OllamaChat::Tools::DirectoryStructure
|
|
|
28
28
|
description: <<~EOT,
|
|
29
29
|
Tree viewer – Returns JSON describing files/folders under path up to
|
|
30
30
|
max_depth (<= height of the tree), optionally only files ending with
|
|
31
|
-
suffix, e. g. rb for ruby files. Handy for locating
|
|
32
|
-
presenting a project layout. Limit the required tokens
|
|
33
|
-
max_depth parameter if possible, because the number of
|
|
34
|
-
tree can grow exponentially with its height.
|
|
31
|
+
suffix / file extension, e. g. rb for ruby files. Handy for locating
|
|
32
|
+
resources or presenting a project layout. Limit the required tokens
|
|
33
|
+
by using the max_depth parameter if possible, because the number of
|
|
34
|
+
nodes in a tree can grow exponentially with its height.
|
|
35
35
|
EOT
|
|
36
36
|
parameters: Tool::Function::Parameters.new(
|
|
37
37
|
type: 'object',
|
|
@@ -42,13 +42,17 @@ class OllamaChat::Tools::DirectoryStructure
|
|
|
42
42
|
),
|
|
43
43
|
suffix: Tool::Function::Parameters::Property.new(
|
|
44
44
|
type: 'string',
|
|
45
|
-
description:
|
|
45
|
+
description: <<~EOT
|
|
46
|
+
Only include files with this suffix / file extension, e. g.
|
|
47
|
+
"rb" (defaults to all)
|
|
48
|
+
EOT
|
|
46
49
|
),
|
|
47
50
|
max_depth: Tool::Function::Parameters::Property.new(
|
|
48
51
|
type: 'integer',
|
|
49
52
|
description: <<~EOT,
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
How many levels of the tree to include. 1 = immediate
|
|
54
|
+
children only, 2 = children + grandchildren, nil =
|
|
55
|
+
unlimited (defaults to nil)
|
|
52
56
|
EOT
|
|
53
57
|
),
|
|
54
58
|
},
|
|
@@ -7,6 +7,7 @@ require 'open3'
|
|
|
7
7
|
# It provides a safe way to test snippets and verify Ruby behavior.
|
|
8
8
|
class OllamaChat::Tools::EvalRuby
|
|
9
9
|
include OllamaChat::Tools::Concern
|
|
10
|
+
include Kramdown::ANSI::Width
|
|
10
11
|
|
|
11
12
|
# @return [String] the registered name for this tool
|
|
12
13
|
def self.register_name = 'eval_ruby'
|
|
@@ -79,18 +80,27 @@ class OllamaChat::Tools::EvalRuby
|
|
|
79
80
|
result = stdout&.sub(/\ASwitch to inspect mode.\n/, '')
|
|
80
81
|
|
|
81
82
|
if status.success?
|
|
83
|
+
result_truncated = truncate(result.to_s.strip, length: 80)
|
|
84
|
+
message = "eval_ruby succeeded (#{version}): #{result_truncated.inspect}"
|
|
82
85
|
{
|
|
83
86
|
result:,
|
|
84
87
|
version:,
|
|
85
|
-
status: 'success'
|
|
88
|
+
status: 'success',
|
|
89
|
+
message: ,
|
|
86
90
|
}.to_json
|
|
87
91
|
else
|
|
92
|
+
message =
|
|
93
|
+
if stderr.empty?
|
|
94
|
+
"eval_ruby failed (#{version}): exit status #{status.exitstatus}"
|
|
95
|
+
else
|
|
96
|
+
"eval_ruby failed (#{version}): #{truncate(stderr.to_s.strip, length: 80).inspect}"
|
|
97
|
+
end
|
|
88
98
|
{
|
|
89
|
-
error:
|
|
90
|
-
message:
|
|
91
|
-
version
|
|
92
|
-
stdout
|
|
93
|
-
exit_status: status.exitstatus
|
|
99
|
+
error: 'ExecutionError',
|
|
100
|
+
message: ,
|
|
101
|
+
version: ,
|
|
102
|
+
stdout: ,
|
|
103
|
+
exit_status: status.exitstatus,
|
|
94
104
|
}.to_json
|
|
95
105
|
end
|
|
96
106
|
rescue => e
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# A tool for retrieving full message content from a previous conversation
|
|
2
|
+
# group by its short UUID suffix.
|
|
3
|
+
#
|
|
4
|
+
# When compaction summarizes older messages, the generated summary contains
|
|
5
|
+
# short group UUIDs (last 8 hex chars) that reference the original
|
|
6
|
+
# conversation turns. This tool lets the model retrieve the full content of
|
|
7
|
+
# any referenced group, enabling it to "zoom back in" on specific details
|
|
8
|
+
# that were compressed into the narrative.
|
|
9
|
+
#
|
|
10
|
+
# The search matches on the *suffix* of the `group_uuid` because the summary
|
|
11
|
+
# only stores the last 8 hex characters (the random portion of the UUIDv7),
|
|
12
|
+
# not the full 36-character string.
|
|
13
|
+
#
|
|
14
|
+
# @note The tool is read-only; it never mutates the chat or message list.
|
|
15
|
+
class OllamaChat::Tools::LookupGroup
|
|
16
|
+
include OllamaChat::Tools::Concern
|
|
17
|
+
|
|
18
|
+
# @return [String] the registered name for this tool
|
|
19
|
+
def self.register_name = 'lookup_group'
|
|
20
|
+
|
|
21
|
+
# Returns the tool definition for use with the Ollama API.
|
|
22
|
+
#
|
|
23
|
+
# @return [Ollama::Tool] a tool definition for group lookup
|
|
24
|
+
def tool
|
|
25
|
+
Tool.new(
|
|
26
|
+
type: 'function',
|
|
27
|
+
function: Tool::Function.new(
|
|
28
|
+
name:,
|
|
29
|
+
description: <<~EOT,
|
|
30
|
+
Retrieve the full messages of a previous conversation group.
|
|
31
|
+
|
|
32
|
+
**IMPORTANT:** Provide the **last 8 hex characters** of the
|
|
33
|
+
group_uuid (the random suffix), e.g. "3b874564" — NOT the
|
|
34
|
+
full UUID, NOT the first 8 characters.
|
|
35
|
+
|
|
36
|
+
The summary's tool-summaries section and narrative contain
|
|
37
|
+
these short suffixes in parentheses after group references.
|
|
38
|
+
EOT
|
|
39
|
+
parameters: Tool::Function::Parameters.new(
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
group_uuid: Tool::Function::Parameters::Property.new(
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: <<~EOT,
|
|
45
|
+
The **last 8 hex characters** (suffix) of the group UUID,
|
|
46
|
+
exactly as it appears in the compaction summary. e.g.
|
|
47
|
+
"3b874564".
|
|
48
|
+
EOT
|
|
49
|
+
)
|
|
50
|
+
},
|
|
51
|
+
required: %w[group_uuid]
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Retrieves the messages belonging to the group identified by the short
|
|
58
|
+
# UUID suffix and returns them as formatted JSON.
|
|
59
|
+
#
|
|
60
|
+
# @param tool_call [OllamaChat::Tool::Call] the tool call with arguments
|
|
61
|
+
# @param opts [Hash] additional options
|
|
62
|
+
# @option opts [OllamaChat::Chat] :chat the chat instance
|
|
63
|
+
#
|
|
64
|
+
# @return [String] a JSON string containing the group messages
|
|
65
|
+
# @return [String] an error message as a JSON string if the group is
|
|
66
|
+
# not found or the argument is invalid
|
|
67
|
+
def execute(tool_call, **opts)
|
|
68
|
+
chat = opts[:chat]
|
|
69
|
+
args = tool_call.function.arguments
|
|
70
|
+
|
|
71
|
+
uuid = args.group_uuid.to_s.strip
|
|
72
|
+
raise OllamaChat::ToolFunctionArgumentError, 'group_uuid required' if uuid.empty?
|
|
73
|
+
|
|
74
|
+
messages = chat.messages.each_group(role: %w[user assistant tool], tool: true).find do |group|
|
|
75
|
+
group.first.group_uuid.to_s.end_with?(uuid)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
raise OllamaChat::OllamaChatError, "Group not found: #{uuid}" if messages.nil?
|
|
79
|
+
|
|
80
|
+
chat.log(:info, 'Group retrieved', data: {
|
|
81
|
+
tool: name, group_uuid: messages.first.group_uuid, messages: messages.size
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
formatted = messages.map do |msg|
|
|
85
|
+
tool_tag = msg.tool_name ? " (#{msg.tool_name})" : ''
|
|
86
|
+
"[#{msg.role}#{tool_tag}] #{msg.content.to_s.strip}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
group_uuid: messages.first.group_uuid,
|
|
91
|
+
message: "Retrieved #{messages.size} message(s) for group #{uuid}.",
|
|
92
|
+
messages: formatted,
|
|
93
|
+
}.to_json
|
|
94
|
+
rescue => e
|
|
95
|
+
chat.log(:error, e, data: { tool: name, group_uuid: args.group_uuid })
|
|
96
|
+
{ error: e.class.name, message: e.message }.to_json
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
self
|
|
100
|
+
end.register
|
|
@@ -50,7 +50,7 @@ class OllamaChat::Tools::PasteFromClipboard
|
|
|
50
50
|
|
|
51
51
|
# Use the chat instance's clipboard paste functionality
|
|
52
52
|
content = chat.perform_paste_from_clipboard(edit:)
|
|
53
|
-
message = "Pasted #{format_bytes(content.to_s.
|
|
53
|
+
message = "Pasted #{format_bytes(content.to_s.bytesize)} of content."
|
|
54
54
|
|
|
55
55
|
{
|
|
56
56
|
success: true,
|
|
@@ -164,16 +164,34 @@ class OllamaChat::Tools::PatchFile
|
|
|
164
164
|
"Failed to apply patch to file #{path.to_s.inspect}."
|
|
165
165
|
end
|
|
166
166
|
|
|
167
|
+
syntax_check = nil
|
|
168
|
+
if result[:success] && (checker = chat.syntax_checker_for(path))
|
|
169
|
+
check = chat.run_syntax_check(checker, path)
|
|
170
|
+
syntax_check = check if check &&
|
|
171
|
+
(check[:status] == 'fail' || check[:output] != '')
|
|
172
|
+
end
|
|
173
|
+
if syntax_check
|
|
174
|
+
case syntax_check[:status]
|
|
175
|
+
when 'fail'
|
|
176
|
+
message = "#{message} — ❌ Syntax error detected"
|
|
177
|
+
when 'pass'
|
|
178
|
+
if warning = syntax_check[:output].lines.first&.chomp
|
|
179
|
+
message = "#{message} — ⚠️ #{warning}"
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
167
184
|
mtime, checksum = File.open(path, 'rb') {
|
|
168
185
|
[ _1.mtime.iso8601(0), '%08x' % Zlib.crc32(_1) ]
|
|
169
186
|
}
|
|
170
187
|
|
|
171
188
|
(result | {
|
|
172
|
-
path:
|
|
173
|
-
message:
|
|
174
|
-
mtime:
|
|
175
|
-
checksum:
|
|
176
|
-
|
|
189
|
+
path: path.to_s,
|
|
190
|
+
message: ,
|
|
191
|
+
mtime: ,
|
|
192
|
+
checksum: ,
|
|
193
|
+
syntax_check: syntax_check,
|
|
194
|
+
}).compact.to_json
|
|
177
195
|
|
|
178
196
|
rescue => e
|
|
179
197
|
chat.log(:error, e, data: { tool: name, path: path.to_s })
|
|
@@ -91,7 +91,7 @@ class OllamaChat::Tools::ReadFile
|
|
|
91
91
|
content, line_count = extract_range(full_content, start_line, end_line, line_numbers:)
|
|
92
92
|
checksum = '%08x' % Zlib.crc32(full_content) if line_numbers && !start_line && !end_line
|
|
93
93
|
es = OllamaChat::TokenEstimator.estimate(content)
|
|
94
|
-
message = "Read #{es.
|
|
94
|
+
message = "Read #{es.tokens_formatted} (#{es.bytes_formatted}) from #{path.to_s.inspect}."
|
|
95
95
|
chat.log(:info, "File read", data: {
|
|
96
96
|
tool: name, path: path.to_s, bytes: es.bytes_formatted, tokens: es.tokens_formatted
|
|
97
97
|
})
|
|
@@ -66,7 +66,7 @@ class OllamaChat::Tools::ResolveTag
|
|
|
66
66
|
|
|
67
67
|
tags = OC::OLLAMA::CHAT::TOOLS::TAGS_FILE
|
|
68
68
|
results = OllamaChat::Utils::TagResolver.new(tags).
|
|
69
|
-
resolve(symbol:, kind:, directory:)
|
|
69
|
+
resolve(chat:, symbol:, kind:, directory:)
|
|
70
70
|
|
|
71
71
|
message = "Found %{results_count} results of symbol \"%{symbol}\"." % {
|
|
72
72
|
results_count: results.size, symbol:
|
|
@@ -114,7 +114,13 @@ class OllamaChat::Tools::SearchKnowledge
|
|
|
114
114
|
|
|
115
115
|
if collection = args.collection.full?
|
|
116
116
|
unless collection.to_s.match?(/\A#{OllamaChat::COLLECTION_NAME_REGEXP.source}\z/)
|
|
117
|
-
raise OllamaChat::ToolFunctionArgumentError,
|
|
117
|
+
raise OllamaChat::ToolFunctionArgumentError,
|
|
118
|
+
"Invalid collection name: #{collection}"
|
|
119
|
+
end
|
|
120
|
+
col = chat.database_collection?(collection)
|
|
121
|
+
if col&.enabled == false
|
|
122
|
+
raise OllamaChat::ToolFunctionArgumentError,
|
|
123
|
+
"Collection #{collection} is disabled."
|
|
118
124
|
end
|
|
119
125
|
old_collection = chat.documents.collection
|
|
120
126
|
chat.documents.collection = collection
|
|
@@ -112,17 +112,36 @@ class OllamaChat::Tools::WriteFile
|
|
|
112
112
|
raise ArgumentError, 'Invalid mode %s' % args.mode.inspect
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
syntax_check = nil
|
|
116
|
+
if checker = chat.syntax_checker_for(path)
|
|
117
|
+
check = chat.run_syntax_check(checker, path)
|
|
118
|
+
syntax_check = check if check &&
|
|
119
|
+
(check[:status] == 'fail' || check[:output] != '')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
message = "Wrote #{es.tokens_formatted} (#{es.bytes_formatted}) to file #{path.to_s.inspect}."
|
|
123
|
+
if syntax_check
|
|
124
|
+
case syntax_check[:status]
|
|
125
|
+
when 'fail'
|
|
126
|
+
message << " — ❌ Syntax error detected"
|
|
127
|
+
when 'pass'
|
|
128
|
+
if warning = syntax_check[:output].lines.first&.chomp
|
|
129
|
+
message << " — ⚠️ #{warning}"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
116
134
|
chat.log(:info, "File written", data: {
|
|
117
135
|
tool: name, path: path.to_s, mode: args.mode || 'overwrite', bytes: es.bytes_formatted
|
|
118
136
|
})
|
|
119
137
|
|
|
120
138
|
{
|
|
121
|
-
success:
|
|
122
|
-
path:
|
|
123
|
-
backup:
|
|
124
|
-
message:
|
|
125
|
-
|
|
139
|
+
success: true,
|
|
140
|
+
path: path.to_s,
|
|
141
|
+
backup: backup_path.to_s,
|
|
142
|
+
message: ,
|
|
143
|
+
syntax_check: syntax_check,
|
|
144
|
+
}.compact.to_json
|
|
126
145
|
rescue => e
|
|
127
146
|
chat.log(:error, e, data: { tool: name, path: args.path })
|
|
128
147
|
{
|
data/lib/ollama_chat/tools.rb
CHANGED
|
@@ -62,6 +62,7 @@ require 'ollama_chat/tools/get_location'
|
|
|
62
62
|
require 'ollama_chat/tools/get_rfc'
|
|
63
63
|
require 'ollama_chat/tools/get_time'
|
|
64
64
|
require 'ollama_chat/tools/get_url'
|
|
65
|
+
require 'ollama_chat/tools/lookup_group'
|
|
65
66
|
require 'ollama_chat/tools/move_file'
|
|
66
67
|
require 'ollama_chat/tools/open_file_in_editor'
|
|
67
68
|
require 'ollama_chat/tools/paste_from_clipboard'
|
|
@@ -37,8 +37,9 @@ module OllamaChat::Utils::AnalyzeDirectory
|
|
|
37
37
|
# only files with this suffix are included in the result. Pass `nil` or an empty string
|
|
38
38
|
# to include all files.
|
|
39
39
|
# @param max_depth [Integer, nil] Optional depth limit. If `nil`,
|
|
40
|
-
# the entire tree is returned.
|
|
41
|
-
#
|
|
40
|
+
# the entire tree is returned. When an integer is supplied, entries
|
|
41
|
+
# at that level and deeper are pruned. Use **1** to show only
|
|
42
|
+
# immediate children, **2** for children + grandchildren, etc.
|
|
42
43
|
#
|
|
43
44
|
# @return [Array<Hash>] An array of entry hashes. Each hash contains:
|
|
44
45
|
# * `:type` – "file" or "directory"
|
|
@@ -70,9 +71,9 @@ module OllamaChat::Utils::AnalyzeDirectory
|
|
|
70
71
|
|
|
71
72
|
structure_each_entry(entries) { |e| e[:height] = height }
|
|
72
73
|
|
|
73
|
-
if max_depth && max_depth
|
|
74
|
+
if max_depth && max_depth <= height
|
|
74
75
|
structure_each_entry(entries) do |e|
|
|
75
|
-
e[:children]&.reject! { |c| c[:depth]
|
|
76
|
+
e[:children]&.reject! { |c| c[:depth] >= max_depth }
|
|
76
77
|
end
|
|
77
78
|
end
|
|
78
79
|
|
|
@@ -312,7 +312,7 @@ class OllamaChat::Utils::Fetcher
|
|
|
312
312
|
end
|
|
313
313
|
body = response.body
|
|
314
314
|
tmp.print body
|
|
315
|
-
infobar.update(message: message(body.
|
|
315
|
+
infobar.update(message: message(body.bytesize, body.bytesize), force: true)
|
|
316
316
|
decorate_io(tmp, response)
|
|
317
317
|
infobar.finish
|
|
318
318
|
block.(tmp)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'file/tail'
|
|
2
|
+
|
|
3
|
+
# Truncates a log file in-place, keeping only the last N lines.
|
|
4
|
+
#
|
|
5
|
+
# Uses `File::Tail#backward` to seek to the start of the tail portion
|
|
6
|
+
# without loading the entire file into memory. The file is rewritten
|
|
7
|
+
# in-place (same inode) so that tailing processes (e.g.
|
|
8
|
+
# `ollama_chat_log`) can continue following the file.
|
|
9
|
+
module OllamaChat::Utils::LogRotation
|
|
10
|
+
# Truncates a log file in-place, keeping only the last +keep_lines+.
|
|
11
|
+
#
|
|
12
|
+
# If the file has fewer lines than +keep_lines+, it is left untouched.
|
|
13
|
+
# The operation is a no-op if the file does not exist.
|
|
14
|
+
#
|
|
15
|
+
# @param path [String, Pathname] the log file to truncate
|
|
16
|
+
# @param keep_lines [Integer] the number of lines to retain
|
|
17
|
+
def self.truncate_tail(path, keep_lines:)
|
|
18
|
+
return unless File.exist?(path)
|
|
19
|
+
File.open(path, 'r+') do |f|
|
|
20
|
+
f.extend(File::Tail)
|
|
21
|
+
f.backward(keep_lines)
|
|
22
|
+
return if f.pos.zero?
|
|
23
|
+
tail = f.read
|
|
24
|
+
f.rewind
|
|
25
|
+
f.write(tail)
|
|
26
|
+
f.truncate(f.pos)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|