ollama_chat 0.0.113 → 0.0.115
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 +120 -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 +72 -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/execute_shell.rb +158 -0
- 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 +2 -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/strip_ansi.rb +22 -0
- data/lib/ollama_chat/utils/tag_resolver.rb +3 -3
- data/lib/ollama_chat/utils.rb +2 -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/execute_shell_spec.rb +232 -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 +10 -3
- 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 +98 -2
- 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 +25 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
require 'open3'
|
|
2
|
+
|
|
3
|
+
# Shell command executor with mandatory human review.
|
|
4
|
+
#
|
|
5
|
+
# The LLM proposes a command; the user reviews it in their configured
|
|
6
|
+
# editor (modify or cancel), then answers a y/n/i confirm prompt that
|
|
7
|
+
# cannot be disabled via configuration. Output is displayed via pager.
|
|
8
|
+
class OllamaChat::Tools::ExecuteShell
|
|
9
|
+
include OllamaChat::Tools::Concern
|
|
10
|
+
include Term::ANSIColor
|
|
11
|
+
include OllamaChat::Utils::StripANSI
|
|
12
|
+
|
|
13
|
+
# @return [String] the registered name for this tool
|
|
14
|
+
def self.register_name = 'execute_shell'
|
|
15
|
+
|
|
16
|
+
# Compaction summary: one-liner for lookup_group output.
|
|
17
|
+
#
|
|
18
|
+
# @param result [String] the raw JSON result string
|
|
19
|
+
# @return [String] a short human-readable summary
|
|
20
|
+
def self.summary_template(result:)
|
|
21
|
+
data = JSON.parse(result)
|
|
22
|
+
if data['error']
|
|
23
|
+
"❌ #{data['message']}"
|
|
24
|
+
else
|
|
25
|
+
"✅ #{data['command']} (exit #{data['exit_code']})"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Ollama::Tool] a tool definition for shell execution
|
|
30
|
+
def tool
|
|
31
|
+
Tool.new(
|
|
32
|
+
type: 'function',
|
|
33
|
+
function: Tool::Function.new(
|
|
34
|
+
name:,
|
|
35
|
+
description: <<~EOT,
|
|
36
|
+
Shell executor – Runs a shell command via `sh -c`. The
|
|
37
|
+
command is opened in your editor for review/modification,
|
|
38
|
+
then a mandatory y/n/i confirm prompt gates execution.
|
|
39
|
+
Last resort: use ONLY when no dedicated tool covers
|
|
40
|
+
the task (e.g. `git commit`, `bundle install`, `make`).
|
|
41
|
+
Prefer `execute_grep`, `directory_structure`, `read_file`,
|
|
42
|
+
`patch_file` over shell equivalents. Only invoke when
|
|
43
|
+
the user explicitly requests it or you have proposed
|
|
44
|
+
the command to the user beforehand. Runs in the current
|
|
45
|
+
working directory.
|
|
46
|
+
EOT
|
|
47
|
+
parameters: Tool::Function::Parameters.new(
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
command: Tool::Function::Parameters::Property.new(
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'The shell command to execute'
|
|
53
|
+
),
|
|
54
|
+
monochrome: Tool::Function::Parameters::Property.new(
|
|
55
|
+
type: 'boolean',
|
|
56
|
+
description: 'Strip ANSI codes from captured output' \
|
|
57
|
+
' (default: true)'
|
|
58
|
+
),
|
|
59
|
+
},
|
|
60
|
+
required: %w[command]
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Execute the shell command after editor review and confirmation.
|
|
67
|
+
#
|
|
68
|
+
# @param tool_call [OllamaChat::Tool::Call] the tool call with args
|
|
69
|
+
# @param opts [Hash] additional options
|
|
70
|
+
# @option opts [OllamaChat::Chat] :chat the chat instance
|
|
71
|
+
# @return [String] a JSON result string
|
|
72
|
+
def execute(tool_call, **opts)
|
|
73
|
+
chat = opts[:chat]
|
|
74
|
+
args = tool_call.function.arguments
|
|
75
|
+
command = args.command.to_s.strip
|
|
76
|
+
raise OllamaChat::ToolFunctionArgumentError,
|
|
77
|
+
'command required' if command.empty?
|
|
78
|
+
|
|
79
|
+
monochrome = args.monochrome.nil? ? true : args.monochrome
|
|
80
|
+
|
|
81
|
+
unless OllamaChat.test_mode?
|
|
82
|
+
reviewed = review(command, chat:)
|
|
83
|
+
raise OllamaChat::ToolFunctionArgumentError,
|
|
84
|
+
"Shell command cancelled by user: #{command}" unless reviewed
|
|
85
|
+
command = reviewed
|
|
86
|
+
|
|
87
|
+
STDOUT.puts "\n$ #{command}\n"
|
|
88
|
+
answer = chat.confirm?(
|
|
89
|
+
prompt: '❓ Allow ✅[y]es / ⛔️[n]o / 📝[i]nstruct? '
|
|
90
|
+
)&.to_s&.downcase
|
|
91
|
+
|
|
92
|
+
case answer
|
|
93
|
+
when 'y'
|
|
94
|
+
# proceed to execution
|
|
95
|
+
when 'n'
|
|
96
|
+
raise OllamaChat::ToolFunctionArgumentError,
|
|
97
|
+
"Shell command cancelled by user: #{command}"
|
|
98
|
+
else
|
|
99
|
+
instr = chat.ask?(prompt: '📝 Instructions: ')&.strip
|
|
100
|
+
resolve = instr.full? || 'You **MUST** ask the user for instructions!'
|
|
101
|
+
raise OllamaChat::ToolFunctionArgumentError,
|
|
102
|
+
"User instruct: #{resolve} (command: #{command})"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
stdout, stderr, status = Open3.capture3('sh', '-c', command)
|
|
107
|
+
exit_code = status.exitstatus
|
|
108
|
+
if monochrome
|
|
109
|
+
stdout = strip_ansi(stdout)
|
|
110
|
+
stderr = strip_ansi(stderr)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
chat.log(:info, 'Shell executed', data: {
|
|
114
|
+
tool: name, command:, exit_code:,
|
|
115
|
+
stdout_bytes: stdout.bytesize,
|
|
116
|
+
stderr_bytes: stderr.bytesize,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
display(chat, stdout, stderr, exit_code)
|
|
120
|
+
|
|
121
|
+
{
|
|
122
|
+
stdout:, stderr:, exit_code:, command:,
|
|
123
|
+
message: "Shell command finished (exit #{exit_code}): #{command}",
|
|
124
|
+
}.to_json
|
|
125
|
+
rescue => e
|
|
126
|
+
cmd = args&.command&.to_s
|
|
127
|
+
chat.log(:error, e, data: { tool: name, command: cmd })
|
|
128
|
+
{ error: e.class.name, message: e.message, command: cmd }.to_json
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
# Open the command in the user's editor for review.
|
|
134
|
+
#
|
|
135
|
+
# @return [String, nil] the (possibly modified) command, or nil
|
|
136
|
+
# if the user abandoned or cleared the editor.
|
|
137
|
+
def review(command, chat:)
|
|
138
|
+
edited = chat.edit_text(command, basename: %w[cmd .sh])
|
|
139
|
+
edited.full?(:chomp)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Display command output through the pager.
|
|
143
|
+
def display(chat, stdout, stderr, exit_code)
|
|
144
|
+
chat.use_pager do |io|
|
|
145
|
+
if stdout.present?
|
|
146
|
+
io.puts bold { "stdout:" }, ""
|
|
147
|
+
io.puts stdout
|
|
148
|
+
end
|
|
149
|
+
if stderr.present?
|
|
150
|
+
io.puts bold { "stderr:" }, ""
|
|
151
|
+
io.puts stderr
|
|
152
|
+
end
|
|
153
|
+
io.puts bold{ "🎌 exit: #{exit_code}" }
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
self
|
|
158
|
+
end.register
|
|
@@ -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
|
@@ -48,6 +48,7 @@ require 'ollama_chat/tools/directory_structure'
|
|
|
48
48
|
require 'ollama_chat/tools/eval_ruby'
|
|
49
49
|
require 'ollama_chat/tools/execute_grep'
|
|
50
50
|
require 'ollama_chat/tools/execute_ri'
|
|
51
|
+
require 'ollama_chat/tools/execute_shell'
|
|
51
52
|
require 'ollama_chat/tools/execute_jira_twg'
|
|
52
53
|
require 'ollama_chat/tools/file_context'
|
|
53
54
|
require 'ollama_chat/tools/gem_path_lookup'
|
|
@@ -62,6 +63,7 @@ require 'ollama_chat/tools/get_location'
|
|
|
62
63
|
require 'ollama_chat/tools/get_rfc'
|
|
63
64
|
require 'ollama_chat/tools/get_time'
|
|
64
65
|
require 'ollama_chat/tools/get_url'
|
|
66
|
+
require 'ollama_chat/tools/lookup_group'
|
|
65
67
|
require 'ollama_chat/tools/move_file'
|
|
66
68
|
require 'ollama_chat/tools/open_file_in_editor'
|
|
67
69
|
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
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Strips ANSI escape sequences from a string.
|
|
2
|
+
#
|
|
3
|
+
# Handles both SGR (Select Graphic Rendition) sequences like
|
|
4
|
+
# `\e[38;2;255;128;0m` and OSC (Operating System Command) sequences
|
|
5
|
+
# like `\e]0;title\a` or `\e]8;;url\e\\`.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# OllamaChat::Utils::StripANSI.strip_ansi("\e[31mred\e[0m")
|
|
9
|
+
# # => "red"
|
|
10
|
+
module OllamaChat::Utils::StripANSI
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
ANSI_RE = /\e\[[0-9;?]*[A-Za-z]|\e\].*?(\e\\|\a)/m
|
|
14
|
+
|
|
15
|
+
# Removes all ANSI escape sequences from the given string.
|
|
16
|
+
#
|
|
17
|
+
# @param line [String] the string potentially containing ANSI escapes
|
|
18
|
+
# @return [String] the string with all ANSI escape sequences removed
|
|
19
|
+
def strip_ansi(line)
|
|
20
|
+
line.to_s.gsub(ANSI_RE, '')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -35,7 +35,7 @@ class OllamaChat::Utils::TagResolver
|
|
|
35
35
|
# A lightweight struct representing a single tag entry. It extends the base
|
|
36
36
|
# `Struct` with convenience methods for generating human‑readable messages and
|
|
37
37
|
# JSON representations.
|
|
38
|
-
class TagResult <
|
|
38
|
+
class TagResult < Data.define(*(HEADERS + %i[ linenumber ]))
|
|
39
39
|
# @return [String] Human readable description of this tag result.
|
|
40
40
|
def message
|
|
41
41
|
"#{symbol} of kind #{kind} (#{kind_type}) at #{filename}:#{linenumber}"
|
|
@@ -86,7 +86,7 @@ class OllamaChat::Utils::TagResolver
|
|
|
86
86
|
# with this string.
|
|
87
87
|
# @return [Array<TagResult>] All matching results sorted by the order they
|
|
88
88
|
# appear in the tags file.
|
|
89
|
-
def resolve(symbol:, kind: nil, directory: nil)
|
|
89
|
+
def resolve(chat:, symbol:, kind: nil, directory: nil)
|
|
90
90
|
directory and directory = Pathname.new(directory).expand_path.to_path
|
|
91
91
|
@tags_file.rewind
|
|
92
92
|
results = []
|
|
@@ -117,7 +117,7 @@ class OllamaChat::Utils::TagResolver
|
|
|
117
117
|
obj.linenumber = linenumber
|
|
118
118
|
results << obj
|
|
119
119
|
rescue => e
|
|
120
|
-
log(:error, e, data: { context: 'tag_resolver' }, warn: true)
|
|
120
|
+
chat.log(:error, e, data: { context: 'tag_resolver' }, warn: true)
|
|
121
121
|
end
|
|
122
122
|
results
|
|
123
123
|
end
|
data/lib/ollama_chat/utils.rb
CHANGED
|
@@ -20,5 +20,7 @@ require 'ollama_chat/utils/path_completer'
|
|
|
20
20
|
require 'ollama_chat/utils/path_validator'
|
|
21
21
|
require 'ollama_chat/utils/png_metadata_extractor'
|
|
22
22
|
require 'ollama_chat/utils/tag_resolver'
|
|
23
|
+
require 'ollama_chat/utils/strip_ansi'
|
|
23
24
|
require 'ollama_chat/utils/value_formatter'
|
|
24
25
|
require 'ollama_chat/utils/log_viewer'
|
|
26
|
+
require 'ollama_chat/utils/log_rotation'
|
data/lib/ollama_chat/version.rb
CHANGED
data/lib/ollama_chat.rb
CHANGED
|
@@ -66,6 +66,12 @@ module OllamaChat
|
|
|
66
66
|
class OllamaChatQuitError < OllamaChatError
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
# Raised when a compaction operation fails, e.g. the LLM returns an
|
|
70
|
+
# empty or unusable summary, or the context window is too small to
|
|
71
|
+
# accommodate the summarization prompt.
|
|
72
|
+
class CompactionError < OllamaChatError
|
|
73
|
+
end
|
|
74
|
+
|
|
69
75
|
# Regular expression defining the allowed characters for collection names.
|
|
70
76
|
#
|
|
71
77
|
# Matches alphanumeric characters, underscores, hyphens, and dots.
|
|
@@ -146,6 +152,7 @@ require 'ollama_chat/favourites_management'
|
|
|
146
152
|
require 'ollama_chat/prompt_handling'
|
|
147
153
|
require 'ollama_chat/system_prompt_management'
|
|
148
154
|
require 'ollama_chat/prompt_management'
|
|
155
|
+
require 'ollama_chat/compaction'
|
|
149
156
|
require 'ollama_chat/token_estimator'
|
|
150
157
|
require 'ollama_chat/say'
|
|
151
158
|
require 'ollama_chat/chat'
|