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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e5e93e610536ac396d18e929ef76b3dcecac7d3b9d88a35b8d6ff141e7eba8e4
|
|
4
|
+
data.tar.gz: a7792a3b397ebcc0d88552f0fa1417933f44ff00c6e0fad4eb62d567b5c98bfd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 95a81969d48e98ff06623f887c33bd0acf94452639fabbf4eca30127ef4d300124f9e12d7bbb97a3d8d86263116d3c772e5d9c403d1e4f4cbdc2df241e1f719f
|
|
7
|
+
data.tar.gz: 92201492f3a4a084c845d11f7957b6af6ea8630e7f242158458cb1bad059bababb8699f0dc2f24204ddfde38bcd47e8491812e03d5f38128f179b443f858a9dc
|
data/CHANGES.md
CHANGED
|
@@ -1,5 +1,86 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 2026-09-01 v0.0.114
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
* Context compaction pipeline:
|
|
8
|
+
* `OllamaChat::Compaction` module with `summarize_for_compaction`,
|
|
9
|
+
`compact_with_retry`, `compact_ratio_tokens`, `tool_summary_line`, and
|
|
10
|
+
`Result` value object.
|
|
11
|
+
* `CompactionError` exception.
|
|
12
|
+
* `MessageList` methods: `compact!`, `find_cut_point`, `find_summary`,
|
|
13
|
+
`compacted_messages`, `compacted_estimate_tokens`, and
|
|
14
|
+
`full_estimate_tokens`.
|
|
15
|
+
* `lookup_group` tool for retrieving pre-summary group content by UUID
|
|
16
|
+
suffix.
|
|
17
|
+
* `summary_template` base method in `Tools::Concern` and `message` field in
|
|
18
|
+
`eval_ruby` and `get_rfc` results.
|
|
19
|
+
* `compaction:` configuration block (`enabled`, `reserve`, `keep_recent`) and
|
|
20
|
+
`prompts.compaction` templates in `default_config.yml`.
|
|
21
|
+
* `conversation_length` in `Information` and dynamic runtime values.
|
|
22
|
+
* Per-message token estimation:
|
|
23
|
+
* `#token_estimate(strip_thinking:)` in `OllamaChat::MessageMixin`.
|
|
24
|
+
* `#total_tokens(strip_thinking:)` in `OllamaChat::MessageList`.
|
|
25
|
+
* Color-coded context gauge:
|
|
26
|
+
* `context_gauge` method in `information.rb` displaying context usage with
|
|
27
|
+
ANSI colors and emoji indicators.
|
|
28
|
+
* Configurable syntax checkers for file tools:
|
|
29
|
+
* `syntax_checkers:` block in `default_config.yml` for `ruby`, `javascript`,
|
|
30
|
+
`python`, and `shell`.
|
|
31
|
+
* `ConfigHandling#syntax_checker_for` and `ConfigHandling#run_syntax_check`
|
|
32
|
+
methods.
|
|
33
|
+
* Integration into `Tools::WriteFile#execute` and `Tools::PatchFile#execute`.
|
|
34
|
+
* Log rotation and restructured log configuration:
|
|
35
|
+
* `LOG` module (`CHAT`, `DATABASE`, `TAIL_LINES`) in `oc.rb`.
|
|
36
|
+
* `OllamaChat::Utils::LogRotation.truncate_tail` for in-place log truncation.
|
|
37
|
+
* `truncate_logs` call in `OllamaChat::Chat#initialize`.
|
|
38
|
+
* Database `Logger` level set to `WARN`.
|
|
39
|
+
* Enabled flag for RAG collections:
|
|
40
|
+
* Migration adding `enabled` boolean to `collections` table.
|
|
41
|
+
* Filtering of disabled collections in `information.rb` and
|
|
42
|
+
`search_knowledge`.
|
|
43
|
+
* Interactive toggle in `edit_collection` and `[DISABLED]` suffix in
|
|
44
|
+
`list_collections`.
|
|
45
|
+
* Monochrome flags:
|
|
46
|
+
* `-m` flag for `bin/ollama_chat_send` to strip ANSI codes from stdin.
|
|
47
|
+
* `-m` flag for `/input` command to strip ANSI codes from file content and
|
|
48
|
+
import results.
|
|
49
|
+
* Session synchronization:
|
|
50
|
+
* `session_sync` method extracted from `session_close` in
|
|
51
|
+
`session_management.rb`.
|
|
52
|
+
* `ensure session.lock` in `set_new_session`.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
* Increased `compaction.reserve.ratio` from **0.30** to **0.60** in
|
|
57
|
+
`default_config.yml`.
|
|
58
|
+
* Switched `Struct.new` to `Data.define` in `ModelMetadata`, `Estimate`, and
|
|
59
|
+
`TagResult`.
|
|
60
|
+
* Standardized user-facing output format to `tokens (bytes)` across
|
|
61
|
+
`read_file`, `write_file`, `session_management`, and `information`.
|
|
62
|
+
* Replaced `.size` with `.bytesize` for multi-byte-safe byte counts in
|
|
63
|
+
`token_estimator/crude.rb`, `fetcher.rb`, `paste_from_clipboard.rb`, and
|
|
64
|
+
`prompt_handling.rb`.
|
|
65
|
+
* Updated `/conversation compact` to delegate to `compact_with_retry`.
|
|
66
|
+
* Threaded `chat` into `TagResolver` for logging, fixing `NoMethodError` in
|
|
67
|
+
rescue blocks.
|
|
68
|
+
* Capped inline directory structure depth:
|
|
69
|
+
* Added `parsing.directory_structure_max_depth` (default **1**) to
|
|
70
|
+
`default_config.yml`.
|
|
71
|
+
* Shifted `max_depth` semantics: **1** = immediate children, **2** = children
|
|
72
|
+
+ grandchildren, `nil` = unlimited.
|
|
73
|
+
* Acknowledged prompt state after `/prompt sync` to suppress boot drift nag.
|
|
74
|
+
* Reordered `session_title` prompt for reliability.
|
|
75
|
+
* `/config diff` now calls `reload_config` after `diff_config`.
|
|
76
|
+
* Reordered `/config` subcommand alternatives to `edit diff reload`.
|
|
77
|
+
* Clarified `suffix` description in `directory_structure` as "file extension".
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
|
|
81
|
+
* `NoMethodError` in `TagResolver#resolve` rescue block.
|
|
82
|
+
* Unbounded tree expansion in directory structure parsing.
|
|
83
|
+
|
|
3
84
|
## 2026-08-25 v0.0.113
|
|
4
85
|
|
|
5
86
|
* Simplified hash syntax by replacing `text: text` with `text:` etc.
|
data/README.md
CHANGED
|
@@ -37,6 +37,9 @@ The following environment variables can be used to configure behavior:
|
|
|
37
37
|
- `OLLAMA_CHAT_HISTORY` - Chat history filename
|
|
38
38
|
- `OLLAMA_CHAT_USER` - Full name of the chat user
|
|
39
39
|
- `OLLAMA_CHAT_DEBUG` - Debug mode toggle (1 = enabled)
|
|
40
|
+
- `OLLAMA_CHAT_LOG_CHAT` - Chat log file path (default: `$XDG_STATE_HOME/chat.log`)
|
|
41
|
+
- `OLLAMA_CHAT_LOG_DATABASE` - Database log file path (default: `$XDG_STATE_HOME/database.log`)
|
|
42
|
+
- `OLLAMA_CHAT_LOG_TAIL_LINES` - Max lines to retain in log files (default: `10000`)
|
|
40
43
|
|
|
41
44
|
#### Tool Configuration
|
|
42
45
|
- `OLLAMA_CHAT_TOOLS_TEST_RUNNER` - Configured test runner for `run_tests` (default: `rspec`)
|
|
@@ -45,11 +48,14 @@ The following environment variables can be used to configure behavior:
|
|
|
45
48
|
- `OLLAMA_CHAT_TOOLS_JIRA_URL` - Base URL for Jira instance
|
|
46
49
|
- `OLLAMA_CHAT_TOOLS_JIRA_USER` - Username for Jira authentication
|
|
47
50
|
- `OLLAMA_CHAT_TOOLS_JIRA_API_TOKEN` - API token for Jira authentication
|
|
51
|
+
- `OLLAMA_CHAT_TOOLS_JIRA_TWG` - Path to the twg CLI binary
|
|
48
52
|
- `OLLAMA_CHAT_TOOLS_IMAGE_GENERATOR_URL` - Base URL for ComfyUI server
|
|
49
53
|
- `OLLAMA_CHAT_TOOLS_IMAGE_GENERATOR_WORKFLOW` - ComfyUI workflow as JSON string
|
|
50
54
|
- `OLLAMA_CHAT_TOOLS_IMAGE_GENERATOR_PROMPT_NODE_ID` - Prompt node ID
|
|
51
55
|
- `OLLAMA_CHAT_TOOLS_IMAGE_GENERATOR_FILENAME_PREFIX_NODE_ID` - Filename prefix node ID
|
|
52
56
|
- `OLLAMA_CHAT_TOOLS_PIRATEWEATHER_API_KEY` - Pirate Weather API key
|
|
57
|
+
- `OLLAMA_CHAT_TOOLS_GHR_URL` - Base URL for GHR API server
|
|
58
|
+
- `OLLAMA_CHAT_TOOLS_RUBY_EVAL_IMAGE_TEMPLATE` - Docker image template for `eval_ruby` (default: `ruby:%{version}-alpine`)
|
|
53
59
|
|
|
54
60
|
#### System & Infrastructure
|
|
55
61
|
- `XDG_CONFIG_HOME` - XDG Configuration home directory
|
|
@@ -59,6 +65,7 @@ The following environment variables can be used to configure behavior:
|
|
|
59
65
|
- `EDITOR` - Default text editor
|
|
60
66
|
- `BROWSER` - Default web browser
|
|
61
67
|
- `DIFF_TOOL` - Tool for diff operations (default: `vimdiff`)
|
|
68
|
+
- `DIFF_COMMAND` - Command to generate unified diffs (default: `diff -u --color=always`)
|
|
62
69
|
- `KRAMDOWN_ANSI_OLLAMA_CHAT_STYLES` - Custom ANSI styles for Markdown formatting
|
|
63
70
|
- `KRAMDOWN_ANSI_STYLES` - Fallback ANSI styles configuration
|
|
64
71
|
- `OLLAMA_REDIS_URL` - Redis connection URL for documents (documentrix)
|
data/bin/ollama_chat_log
CHANGED
data/bin/ollama_chat_send
CHANGED
|
@@ -5,7 +5,7 @@ include OllamaChat::FileEditing
|
|
|
5
5
|
require 'tins/go'
|
|
6
6
|
include Tins::GO
|
|
7
7
|
|
|
8
|
-
opts = go 'f:d:F:
|
|
8
|
+
opts = go 'f:d:F:rtemph', ARGV
|
|
9
9
|
|
|
10
10
|
# Displays the usage information for the ollama_chat_send command.
|
|
11
11
|
#
|
|
@@ -25,6 +25,7 @@ def usage(rc = 0)
|
|
|
25
25
|
-f CONFIG file to read
|
|
26
26
|
-d DIR the working directory to derive the socket file from
|
|
27
27
|
-e Edit content in editor before sending
|
|
28
|
+
-m Strip ANSI color codes from content before sending
|
|
28
29
|
-F FORMAT file extension for editor (e.g., rb, md)
|
|
29
30
|
-h Show this help message
|
|
30
31
|
|
|
@@ -42,6 +43,7 @@ begin
|
|
|
42
43
|
opts[?r] ? :socket_input_with_response : :socket_input
|
|
43
44
|
end
|
|
44
45
|
content = STDIN.read
|
|
46
|
+
content = Term::ANSIColor.uncolor(content) if opts[?m]
|
|
45
47
|
if opts[?e]
|
|
46
48
|
basename = %w[ text ]
|
|
47
49
|
basename << opts[?F]&.sub(/\A(?=[^.])/, ?.) || '.md'
|
data/lib/ollama_chat/chat.rb
CHANGED
|
@@ -69,6 +69,7 @@ class OllamaChat::Chat
|
|
|
69
69
|
include OllamaChat::PromptHandling
|
|
70
70
|
include OllamaChat::SystemPromptManagement
|
|
71
71
|
include OllamaChat::PromptManagement
|
|
72
|
+
include OllamaChat::Compaction
|
|
72
73
|
include OllamaChat::Utils::Chooser
|
|
73
74
|
include OllamaChat::Utils::ValueFormatter
|
|
74
75
|
include OllamaChat::Utils::UTF8Converter
|
|
@@ -106,10 +107,11 @@ class OllamaChat::Chat
|
|
|
106
107
|
@opts[?V] and exit version
|
|
107
108
|
@ollama_chat_config = OllamaChat::OllamaChatConfig.new(@opts[?f])
|
|
108
109
|
self.config = @ollama_chat_config.config
|
|
110
|
+
truncate_logs
|
|
109
111
|
@messages = OllamaChat::MessageList.new(self)
|
|
110
112
|
OllamaChat::Database.setup_models.each { _1.ask_and_send(:seed, self) }
|
|
111
|
-
setup_session
|
|
112
113
|
setup_switches
|
|
114
|
+
setup_session
|
|
113
115
|
setup_state_selectors(config)
|
|
114
116
|
connect_ollama
|
|
115
117
|
@documents = setup_documents
|
|
@@ -254,12 +256,14 @@ class OllamaChat::Chat
|
|
|
254
256
|
# and a user message, executing it as a one-shot chat interaction.
|
|
255
257
|
#
|
|
256
258
|
# @param system [String] the system prompt to guide the model's behavior
|
|
257
|
-
#
|
|
258
|
-
#
|
|
259
|
+
# (defaults to current raw_system_prompt)
|
|
259
260
|
# @param prompt [String] the user prompt to send to the model
|
|
261
|
+
# @param stream [Boolean] whether to stream the response (default: false)
|
|
262
|
+
# @param think [Boolean] enable thinking mode for hybrid-thinking models
|
|
263
|
+
# (default: false)
|
|
260
264
|
#
|
|
261
265
|
# @return [String] the content of the resulting response message
|
|
262
|
-
def generate(system: raw_system_prompt, prompt:)
|
|
266
|
+
def generate(system: raw_system_prompt, prompt:, stream: false, think: false)
|
|
263
267
|
messages = [
|
|
264
268
|
OllamaChat::Message.new(
|
|
265
269
|
role: 'system',
|
|
@@ -276,9 +280,8 @@ class OllamaChat::Chat
|
|
|
276
280
|
model: @model,
|
|
277
281
|
messages: ,
|
|
278
282
|
options: model_options,
|
|
279
|
-
stream:
|
|
280
|
-
think:
|
|
281
|
-
tools:
|
|
283
|
+
stream: ,
|
|
284
|
+
think:
|
|
282
285
|
)&.message&.content.to_s
|
|
283
286
|
|
|
284
287
|
if content.empty?
|
|
@@ -315,6 +318,18 @@ class OllamaChat::Chat
|
|
|
315
318
|
ollama.chat(**opts, &block)
|
|
316
319
|
end
|
|
317
320
|
|
|
321
|
+
# Truncates chat.log and database.log to the configured tail line count
|
|
322
|
+
# before any new logging begins for this session.
|
|
323
|
+
def truncate_logs
|
|
324
|
+
keep = OC::OLLAMA::CHAT::LOG::TAIL_LINES
|
|
325
|
+
OllamaChat::Utils::LogRotation.truncate_tail(
|
|
326
|
+
OC::OLLAMA::CHAT::LOG::CHAT, keep_lines: keep
|
|
327
|
+
)
|
|
328
|
+
OllamaChat::Utils::LogRotation.truncate_tail(
|
|
329
|
+
OC::OLLAMA::CHAT::LOG::DATABASE, keep_lines: keep
|
|
330
|
+
)
|
|
331
|
+
end
|
|
332
|
+
|
|
318
333
|
# @return [Module] The module containing the database models.
|
|
319
334
|
def models
|
|
320
335
|
OllamaChat::Database::Models
|
|
@@ -532,14 +547,15 @@ class OllamaChat::Chat
|
|
|
532
547
|
)
|
|
533
548
|
begin
|
|
534
549
|
retried = false
|
|
535
|
-
sent_messages = messages.
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
550
|
+
sent_messages = messages.compacted_messages
|
|
551
|
+
sent_messages = sent_messages.map {
|
|
552
|
+
_1.dup.tap { |message|
|
|
553
|
+
message.sender_name = nil
|
|
554
|
+
message.group_uuid = nil
|
|
555
|
+
message.tool_calls = nil
|
|
556
|
+
message.thinking = nil if think_strip.on?
|
|
541
557
|
}
|
|
542
|
-
|
|
558
|
+
}
|
|
543
559
|
prepare_model(@model)
|
|
544
560
|
call_ollama_chat(
|
|
545
561
|
model: @model,
|
data/lib/ollama_chat/commands.rb
CHANGED
|
@@ -62,8 +62,8 @@ module OllamaChat::Commands
|
|
|
62
62
|
|
|
63
63
|
command(
|
|
64
64
|
name: :config,
|
|
65
|
-
regexp: %r(^/config(?:\s+(edit|reload
|
|
66
|
-
complete: [ 'config', %w[ edit reload
|
|
65
|
+
regexp: %r(^/config(?:\s+(edit|diff|reload))?$),
|
|
66
|
+
complete: [ 'config', %w[ edit diff reload ] ],
|
|
67
67
|
optional: true,
|
|
68
68
|
help: <<~EOT
|
|
69
69
|
\u2699\uFE0F View, edit, diff, or reload configuration
|
|
@@ -72,11 +72,11 @@ module OllamaChat::Commands
|
|
|
72
72
|
case subcommand
|
|
73
73
|
when 'edit'
|
|
74
74
|
edit_config
|
|
75
|
-
when 'reload'
|
|
76
|
-
reload_config
|
|
77
75
|
when 'diff'
|
|
78
76
|
diff_config
|
|
79
77
|
reload_config
|
|
78
|
+
when 'reload'
|
|
79
|
+
reload_config
|
|
80
80
|
else
|
|
81
81
|
display_config
|
|
82
82
|
end
|
|
@@ -598,14 +598,15 @@ module OllamaChat::Commands
|
|
|
598
598
|
|
|
599
599
|
command(
|
|
600
600
|
name: :conversation,
|
|
601
|
-
regexp: %r(^/conversation\s+(clean|save|load)(\s+-c)?(?:\s+([^-].*\.jsonl?))?$),
|
|
602
|
-
complete: [ 'conversation', %w[ save load clean ] ],
|
|
601
|
+
regexp: %r(^/conversation\s+(clean|compact|save|load)(\s+-c)?(?:\s+([^-].*\.jsonl?))?$),
|
|
602
|
+
complete: [ 'conversation', %w[ save load clean compact ] ],
|
|
603
603
|
options: '[-c] [FILENAME]',
|
|
604
604
|
help: <<~EOT
|
|
605
605
|
💾 Save/Load conversation state
|
|
606
606
|
(-c to clean before saving)
|
|
607
607
|
FILENAME ends with .json or .jsonl
|
|
608
608
|
Or clean inplace (removes tool content, images, and thinking)
|
|
609
|
+
Or compact (summarize old messages, keep recent)
|
|
609
610
|
EOT
|
|
610
611
|
) do |subcommand,opts,path|
|
|
611
612
|
if %w[ save load ].include?(subcommand) && path.blank?
|
|
@@ -631,6 +632,16 @@ module OllamaChat::Commands
|
|
|
631
632
|
else
|
|
632
633
|
STDOUT.puts 'Cancelled.'
|
|
633
634
|
end
|
|
635
|
+
when 'compact'
|
|
636
|
+
if confirm?(
|
|
637
|
+
prompt: '🔔 Compact conversation? Old messages will be summarized. (y/n) ',
|
|
638
|
+
yes: /\Ay/i
|
|
639
|
+
)
|
|
640
|
+
then
|
|
641
|
+
compact_with_retry
|
|
642
|
+
else
|
|
643
|
+
STDOUT.puts 'Cancelled.'
|
|
644
|
+
end
|
|
634
645
|
end
|
|
635
646
|
:next
|
|
636
647
|
end
|
|
@@ -836,22 +847,23 @@ module OllamaChat::Commands
|
|
|
836
847
|
|
|
837
848
|
command(
|
|
838
849
|
name: :input,
|
|
839
|
-
regexp: %r(^/input(?:\s+(path|context|embedding|summary)(?:\s*(?=\z))?)?((?:\s+-(?:[
|
|
850
|
+
regexp: %r(^/input(?:\s+(path|context|embedding|summary)(?:\s*(?=\z))?)?((?:\s+-(?:[aprem]|c\s*#{OllamaChat::COLLECTION_NAME_REGEXP.source}|w\s*\d+|t\s*[-\w\.]+(?:,[-\w\.]+)*))*)(?:\s+([^-].*))?$),
|
|
840
851
|
optional: true,
|
|
841
852
|
complete: [ 'input', %w[ path context embedding summary ] ],
|
|
842
853
|
options: <<~EOT,
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
854
|
+
[
|
|
855
|
+
-w|-a|-p|-e|-m|
|
|
856
|
+
-c <collection>|
|
|
857
|
+
-t <tags>
|
|
858
|
+
]
|
|
848
859
|
[arg…]
|
|
849
860
|
EOT
|
|
850
861
|
help: <<~EOT
|
|
851
862
|
📥 Import content (read, summarize, embed, context)
|
|
852
863
|
Subcommands: path, context, embedding, summary
|
|
853
|
-
|
|
854
|
-
|
|
864
|
+
Options: -p (pattern), -w [words], -a (all),
|
|
865
|
+
-c [collection], -t [tags], -e (edit),
|
|
866
|
+
-m (monochrome, strip ANSI from file content)
|
|
855
867
|
EOT
|
|
856
868
|
) do |input_mode,opts,arg|
|
|
857
869
|
disable_content_parsing
|
|
@@ -907,19 +919,21 @@ module OllamaChat::Commands
|
|
|
907
919
|
end
|
|
908
920
|
end
|
|
909
921
|
when 'path'
|
|
910
|
-
opts = go_command('
|
|
922
|
+
opts = go_command('paem', opts)
|
|
911
923
|
if opts[?p]
|
|
912
924
|
all = opts.fetch(?a, false)
|
|
913
925
|
patterns = extract_patterns(arg)
|
|
914
926
|
read = -> pathname {
|
|
915
927
|
STDOUT.puts "Reading #{pathname.to_s.inspect}."
|
|
916
|
-
pathname.read
|
|
928
|
+
content = pathname.read
|
|
929
|
+
opts[?m] ? Term::ANSIColor.uncolor(content) : content
|
|
917
930
|
}
|
|
918
931
|
next provide_file_set_content(patterns, all:, &read) || :next
|
|
919
932
|
elsif arg
|
|
920
933
|
filename = Pathname.new(arg).expand_path
|
|
921
934
|
filename.file? or next :next
|
|
922
935
|
content = filename.read
|
|
936
|
+
content = Term::ANSIColor.uncolor(content) if opts[?m]
|
|
923
937
|
content = edit_text(content) if opts[?e]
|
|
924
938
|
content
|
|
925
939
|
else
|
|
@@ -927,14 +941,18 @@ module OllamaChat::Commands
|
|
|
927
941
|
next :next
|
|
928
942
|
end
|
|
929
943
|
else
|
|
930
|
-
opts = go_command('
|
|
944
|
+
opts = go_command('paem', opts)
|
|
931
945
|
if opts[?p]
|
|
932
946
|
all = opts.fetch(?a, false)
|
|
933
947
|
patterns = extract_patterns(arg)
|
|
934
|
-
next provide_file_set_content(patterns, all:, skip_blank: true)
|
|
948
|
+
next provide_file_set_content(patterns, all:, skip_blank: true) do |src|
|
|
949
|
+
content = import(src)
|
|
950
|
+
opts[?m] ? Term::ANSIColor.uncolor(content) : content
|
|
951
|
+
end || :next
|
|
935
952
|
elsif arg
|
|
936
953
|
source = arg
|
|
937
954
|
content = import(source) or next :next
|
|
955
|
+
content = Term::ANSIColor.uncolor(content) if opts[?m]
|
|
938
956
|
content = edit_text(content) if opts[?e]
|
|
939
957
|
content
|
|
940
958
|
else
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Provides compaction support for chat sessions. Mixed into `Chat`.
|
|
2
|
+
#
|
|
3
|
+
# Supplies configuration resolvers that derive concrete token budgets
|
|
4
|
+
# from the `compaction:` config block (ratios of `num_ctx` with absolute
|
|
5
|
+
# floors), the LLM-based summarization pipeline
|
|
6
|
+
# (+summarize_for_compaction+), and the +tool_summary_line+ resolver
|
|
7
|
+
# for per-tool summary templates.
|
|
8
|
+
#
|
|
9
|
+
# @see OllamaChat::OllamaChatConfig::DefaultConfig for the `compaction:`
|
|
10
|
+
# block structure (`reserve`, `keep_recent`, `summary`).
|
|
11
|
+
module OllamaChat::Compaction
|
|
12
|
+
class << self
|
|
13
|
+
# Generates a one-line summary for a tool call result.
|
|
14
|
+
#
|
|
15
|
+
# Looks up the registered tool class and delegates to its
|
|
16
|
+
# `summary_template(result:)` class method. Falls back to a generic
|
|
17
|
+
# sentence if the tool is not registered or the template raises.
|
|
18
|
+
#
|
|
19
|
+
# @param tool_name [String] the registered name, e.g. `'read_file'`.
|
|
20
|
+
# @param result [String] the raw JSON result string from `execute`.
|
|
21
|
+
# @return [String] a short natural-language description of the call.
|
|
22
|
+
def tool_summary_line(tool_name, result)
|
|
23
|
+
default_summary = "was called."
|
|
24
|
+
klass = OllamaChat::Tools.registered[tool_name.to_s]&.class
|
|
25
|
+
klass&.summary_template(result:) || default_summary
|
|
26
|
+
rescue StandardError
|
|
27
|
+
default_summary
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Value object carrying the outcome of a successful +compact!+ call.
|
|
32
|
+
#
|
|
33
|
+
# @!attribute [r] context_before
|
|
34
|
+
# @return [String] formatted context usage before compaction
|
|
35
|
+
# @!attribute [r] context_after
|
|
36
|
+
# @return [String] formatted context usage after compaction
|
|
37
|
+
# @!attribute [r] candidates
|
|
38
|
+
# @return [Integer] number of messages that were summarized
|
|
39
|
+
# @!attribute [r] candidate_size
|
|
40
|
+
# @return [String] formatted size of the candidate block
|
|
41
|
+
# @!attribute [r] summary_size
|
|
42
|
+
# @return [String] formatted size of the generated summary
|
|
43
|
+
# @!attribute [r] stored_total
|
|
44
|
+
# @return [String] formatted full conversation length
|
|
45
|
+
Result = Data.define(
|
|
46
|
+
:context_before, :context_after,
|
|
47
|
+
:candidates, :candidate_size,
|
|
48
|
+
:summary_size, :stored_total
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Generates a context summary for the given messages via LLM.
|
|
52
|
+
#
|
|
53
|
+
# Serializes non-system messages into a structured prompt, calls the
|
|
54
|
+
# model via +generate+, and assembles the final summary with
|
|
55
|
+
# deterministic tool-call entries stored out-of-band.
|
|
56
|
+
#
|
|
57
|
+
# @param messages [Array<OllamaChat::Message>] the messages to summarize.
|
|
58
|
+
# @param previous_summary [OllamaChat::Message, nil] the previous summary
|
|
59
|
+
# message for iterative re-compaction.
|
|
60
|
+
# @return [Array(String, Array<Hash>)] the assembled content and the
|
|
61
|
+
# merged tool-call entries array.
|
|
62
|
+
def summarize_for_compaction(messages:, previous_summary: nil)
|
|
63
|
+
groups = serialize_groups(messages)
|
|
64
|
+
tool_entries = build_tool_entries(messages)
|
|
65
|
+
|
|
66
|
+
es = OllamaChat::TokenEstimator.estimate(groups)
|
|
67
|
+
log(:info, "Compaction: #{messages.size} messages, " \
|
|
68
|
+
"#{es.tokens_formatted} (#{es.bytes_formatted}) groups")
|
|
69
|
+
|
|
70
|
+
narrative_prev = ''
|
|
71
|
+
old_tool_calls = []
|
|
72
|
+
if previous_summary
|
|
73
|
+
narrative_prev = extract_narrative(previous_summary.content)
|
|
74
|
+
old_tool_calls = extract_old_tool_calls(previous_summary)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
narrative = call_summarizer(groups:, previous_summary: narrative_prev)
|
|
78
|
+
assemble_summary(narrative, tool_entries, old_tool_calls)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Resolves the effective token budget for a concern given a context size.
|
|
82
|
+
#
|
|
83
|
+
# Computes `max(ratio * tokens, min_tokens).floor`, so the floor acts as
|
|
84
|
+
# a guaranteed minimum even for very small context windows.
|
|
85
|
+
#
|
|
86
|
+
# @param name [Symbol] the concern name, e.g. `:reserve`, `:keep_recent`,
|
|
87
|
+
# or `:summary`.
|
|
88
|
+
# @param tokens [Integer] the context window size (`num_ctx`) to derive
|
|
89
|
+
# the budget from.
|
|
90
|
+
# @return [Integer] the resolved token budget.
|
|
91
|
+
def compact_ratio_tokens(name, tokens)
|
|
92
|
+
[
|
|
93
|
+
compact_ratio(name).to_f * tokens,
|
|
94
|
+
compact_min_tokens(name).to_f,
|
|
95
|
+
].max.floor
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Attempts compaction, prompting the user to retry on failure.
|
|
99
|
+
#
|
|
100
|
+
# +compact!+ is idempotent on failure: the LLM call (and thus any
|
|
101
|
+
# +CompactionError+) happens before the message list is mutated, so a
|
|
102
|
+
# retry always starts from a clean state.
|
|
103
|
+
#
|
|
104
|
+
# @return [Boolean] true if compaction succeeded, false if the user
|
|
105
|
+
# declined to retry.
|
|
106
|
+
def compact_with_retry
|
|
107
|
+
result = messages.compact!
|
|
108
|
+
session_sync
|
|
109
|
+
if result
|
|
110
|
+
report_compaction(result)
|
|
111
|
+
else
|
|
112
|
+
STDOUT.puts('Nothing to compact.')
|
|
113
|
+
end
|
|
114
|
+
return true
|
|
115
|
+
rescue OllamaChat::CompactionError => e
|
|
116
|
+
STDERR.puts "⚠️ Compaction failed: #{e.message}"
|
|
117
|
+
log(:error, "Compaction failed", data: {
|
|
118
|
+
model: @model,
|
|
119
|
+
ctx: current_context_length,
|
|
120
|
+
size: messages.size,
|
|
121
|
+
})
|
|
122
|
+
retry if confirm?(
|
|
123
|
+
prompt: '🔔 Retry compaction? (y/n) ',
|
|
124
|
+
yes: /\Ay/i
|
|
125
|
+
)
|
|
126
|
+
false
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# Serializes non-system messages into a readable block for the prompt.
|
|
132
|
+
#
|
|
133
|
+
# @param messages [Array<OllamaChat::Message>] the messages to serialize.
|
|
134
|
+
# @return [String] a formatted block of message lines.
|
|
135
|
+
def serialize_groups(messages)
|
|
136
|
+
messages
|
|
137
|
+
.reject { _1.role == 'system' }
|
|
138
|
+
.map { |m| format_summary_line(m) }
|
|
139
|
+
.join("\n")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Formats a single message as a summary line.
|
|
143
|
+
#
|
|
144
|
+
# @param msg [OllamaChat::Message] the message to format.
|
|
145
|
+
# @return [String] a single line describing the message.
|
|
146
|
+
def format_summary_line(msg)
|
|
147
|
+
tool_name = msg.tool_name.full?
|
|
148
|
+
parts = +"[#{msg.role}]"
|
|
149
|
+
parts << " (#{tool_name})" if tool_name
|
|
150
|
+
parts << " (g:#{msg.group_uuid.to_s[-8..]})" if msg.group_uuid
|
|
151
|
+
if tool_name
|
|
152
|
+
line = OllamaChat::Compaction.tool_summary_line(tool_name, msg.content.to_s)
|
|
153
|
+
parts << " #{line}"
|
|
154
|
+
elsif msg.content.present?
|
|
155
|
+
parts << " #{msg.content.to_s.strip}"
|
|
156
|
+
end
|
|
157
|
+
parts
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Builds the deterministic tool-call entries from tool result messages.
|
|
161
|
+
#
|
|
162
|
+
# @param messages [Array<OllamaChat::Message>] the messages to scan.
|
|
163
|
+
# @return [Array<Hash>] tool-call entry hashes, empty if no tool results.
|
|
164
|
+
def build_tool_entries(messages)
|
|
165
|
+
messages.filter_map do |msg|
|
|
166
|
+
next unless tool_name = msg.tool_name.full?
|
|
167
|
+
next if tool_name == 'runtime_information'
|
|
168
|
+
uuid = msg.group_uuid.to_s[-8..]
|
|
169
|
+
summary = OllamaChat::Compaction.tool_summary_line(
|
|
170
|
+
tool_name, msg.content.to_s
|
|
171
|
+
)
|
|
172
|
+
time = msg.group_time&.strftime('%Y-%m-%d %H:%M')
|
|
173
|
+
{
|
|
174
|
+
'type' => 'tool',
|
|
175
|
+
'name' => tool_name,
|
|
176
|
+
'summary' => summary,
|
|
177
|
+
'uuid' => uuid,
|
|
178
|
+
'time' => time,
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Calls the LLM to generate the narrative portion of the summary.
|
|
184
|
+
#
|
|
185
|
+
# @param groups [String] the serialized groups block.
|
|
186
|
+
# @param previous_summary [String, nil] the prior summary for iteration.
|
|
187
|
+
# @return [String] the LLM-generated narrative text.
|
|
188
|
+
def call_summarizer(groups:, previous_summary:)
|
|
189
|
+
previous = previous_summary.to_s
|
|
190
|
+
|
|
191
|
+
prompt = prompt(:summarize, context: 'compaction').to_s % {
|
|
192
|
+
previous:, groups:
|
|
193
|
+
}
|
|
194
|
+
system = prompt(:system, context: 'compaction').to_s
|
|
195
|
+
|
|
196
|
+
es = OllamaChat::TokenEstimator.estimate(prompt)
|
|
197
|
+
log(:info, "Compaction: sending prompt " \
|
|
198
|
+
"#{es.tokens_formatted} (#{es.bytes_formatted}) to #{@model}")
|
|
199
|
+
|
|
200
|
+
response = Infobar.busy(
|
|
201
|
+
label: 'Compacting context…',
|
|
202
|
+
frames: :braille7,
|
|
203
|
+
output: STDOUT,
|
|
204
|
+
) do
|
|
205
|
+
generate(system:, prompt:, think: true).strip
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
if response.empty?
|
|
209
|
+
log(:error, 'Compaction: LLM returned empty response')
|
|
210
|
+
raise OllamaChat::CompactionError,
|
|
211
|
+
'Summarization failed: model returned empty response'
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
res_es = OllamaChat::TokenEstimator.estimate(response)
|
|
215
|
+
log(:info, "Compaction: got response " \
|
|
216
|
+
"#{res_es.tokens_formatted} (#{res_es.bytes_formatted})")
|
|
217
|
+
|
|
218
|
+
response
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Assembles the final summary content and merged tool entries.
|
|
222
|
+
#
|
|
223
|
+
# @param narrative [String] the LLM-generated narrative.
|
|
224
|
+
# @param tool_entries [Array<Hash>] new tool-call entries from this pass.
|
|
225
|
+
# @param old_tool_entries [Array<Hash>] entries carried over from a
|
|
226
|
+
# prior compaction.
|
|
227
|
+
# @return [Array(String, Array<Hash>)] the content string and the
|
|
228
|
+
# merged entries array.
|
|
229
|
+
def assemble_summary(narrative, tool_entries, old_tool_entries = [])
|
|
230
|
+
all_tools = old_tool_entries + tool_entries
|
|
231
|
+
tool_section = all_tools.map(&:to_json) * ?\n
|
|
232
|
+
|
|
233
|
+
template = prompt(:assemble, context: 'compaction').to_s
|
|
234
|
+
content = template % { narrative:, tool_section: }
|
|
235
|
+
|
|
236
|
+
[content, all_tools]
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Extracts the narrative portion from a summary message's content.
|
|
240
|
+
#
|
|
241
|
+
# @param content [String, nil] the summary content.
|
|
242
|
+
# @return [String] the narrative text.
|
|
243
|
+
def extract_narrative(content)
|
|
244
|
+
content.to_s.sub(/\ntool_calls:\n.*\z/m, '').strip
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Returns the tool-call entries from a previous summary message.
|
|
248
|
+
#
|
|
249
|
+
# @param summary_msg [OllamaChat::Message] the previous summary message.
|
|
250
|
+
# @return [Array<Hash>] the tool-call entries, empty if none.
|
|
251
|
+
def extract_old_tool_calls(summary_msg)
|
|
252
|
+
summary_msg.tool_calls || []
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Returns the absolute minimum token floor for a given concern.
|
|
256
|
+
#
|
|
257
|
+
# @param name [Symbol] the concern name, e.g. `:reserve`, `:keep_recent`,
|
|
258
|
+
# or `:summary`.
|
|
259
|
+
# @return [Integer] the configured `min_tokens` floor.
|
|
260
|
+
def compact_min_tokens(name)
|
|
261
|
+
config.compaction.attribute_get!(name).min_tokens
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Returns the fractional ratio (portion of `num_ctx`) for a given concern.
|
|
265
|
+
#
|
|
266
|
+
# @param name [Symbol] the concern name, e.g. `:reserve`, `:keep_recent`,
|
|
267
|
+
# or `:summary`.
|
|
268
|
+
# @return [Float] the configured `ratio` value.
|
|
269
|
+
def compact_ratio(name)
|
|
270
|
+
config.compaction.attribute_get!(name).ratio
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Displays a compact compaction report to +STDOUT+ after the
|
|
274
|
+
# +Infobar.busy+ spinner has finished.
|
|
275
|
+
#
|
|
276
|
+
# Replaces the former bare "Conversation compacted." line with
|
|
277
|
+
# before/after context metrics, candidate volume, and summary size.
|
|
278
|
+
#
|
|
279
|
+
# @param result [Result] the compaction result from +compact!+.
|
|
280
|
+
def report_compaction(result)
|
|
281
|
+
STDOUT.puts(<<~EOT)
|
|
282
|
+
✅ Conversation compacted.
|
|
283
|
+
Summarized: #{result.candidates} messages (#{result.candidate_size})
|
|
284
|
+
Summary: #{result.summary_size}
|
|
285
|
+
Context: #{result.context_before} → #{result.context_after}
|
|
286
|
+
Stored total: #{result.stored_total}
|
|
287
|
+
EOT
|
|
288
|
+
end
|
|
289
|
+
end
|