ollama_chat 0.0.108 ā 0.0.110
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 +108 -0
- data/lib/ollama_chat/clipboard.rb +11 -0
- data/lib/ollama_chat/commands.rb +35 -12
- data/lib/ollama_chat/database/migrations/009_add_app_states_table.rb +11 -0
- data/lib/ollama_chat/database/models/app_state.rb +122 -0
- data/lib/ollama_chat/kramdown_ansi.rb +1 -1
- data/lib/ollama_chat/model_handling.rb +116 -0
- data/lib/ollama_chat/oc.rb +9 -0
- data/lib/ollama_chat/ollama_chat_config/default_config.yml +0 -1
- data/lib/ollama_chat/prompt_handling.rb +9 -9
- data/lib/ollama_chat/prompt_management.rb +125 -1
- data/lib/ollama_chat/session_management.rb +16 -2
- data/lib/ollama_chat/tools/patch_file.rb +1 -1
- data/lib/ollama_chat/version.rb +1 -1
- data/lib/ollama_chat/web_searching.rb +4 -3
- data/lib/ollama_chat.rb +20 -2
- data/ollama_chat.gemspec +5 -5
- data/spec/ollama_chat/commands_spec.rb +24 -0
- data/spec/ollama_chat/config_handling_spec.rb +150 -0
- data/spec/ollama_chat/conversation_spec.rb +107 -0
- data/spec/ollama_chat/favourites_management_spec.rb +143 -0
- data/spec/ollama_chat/information_spec.rb +36 -2
- data/spec/ollama_chat/model_handling_spec.rb +139 -0
- data/spec/ollama_chat/prompt_handling_spec.rb +183 -0
- data/spec/ollama_chat/prompt_management_spec.rb +574 -0
- data/spec/ollama_chat/rag_handling_spec.rb +498 -0
- data/spec/ollama_chat/session_management_spec.rb +4 -4
- data/spec/ollama_chat/system_prompt_management_spec.rb +200 -0
- data/spec/ollama_chat/think_control_spec.rb +2 -2
- data/spec/ollama_chat/tools/execute_ri_spec.rb +2 -2
- data/spec/ollama_chat/tools/generate_image_spec.rb +6 -6
- data/spec/ollama_chat/tools/get_url_spec.rb +3 -3
- data/spec/ollama_chat/tools/move_file_spec.rb +1 -1
- data/spec/ollama_chat/tools/patch_file_spec.rb +3 -3
- data/spec/ollama_chat/tools/run_tests_spec.rb +3 -3
- data/spec/ollama_chat/tools/search_knowledge_spec.rb +5 -5
- data/spec/ollama_chat/utils/path_completer_spec.rb +1 -1
- data/spec/ollama_chat/utils/png_metadata_extractor_spec.rb +3 -3
- data/spec/ollama_chat/web_searching_spec.rb +99 -21
- data/spec/spec_helper.rb +1 -0
- 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: 239476f6dca2ca4fca22dc0290b25bb048572cc89d12ef82c09d54a7ee429068
|
|
4
|
+
data.tar.gz: d7c51cf3d519c593666625bcedf487a1d40893099c251f211cd055a4610cb3db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 24eadaa6eb35470ff3908fbfd7646ad0b685bded30694b7784839aef35a447b6f6926c9cac6a29f4eed55d723065cdc063f21d7c718f3c15dc4905892f860484
|
|
7
|
+
data.tar.gz: 98f4ab4370005f5a9523a7fc54ea62f56e64b411894d8ca40ae85518cae9962f8b9818086d5984f34aec392dde268377b10d33ef103e64ebc9b8abcd17702293
|
data/CHANGES.md
CHANGED
|
@@ -1,5 +1,113 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 2026-08-20 v0.0.110
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
* **App State Support**: Introduced `009_add_app_states_table.rb` migration
|
|
8
|
+
and `app_state.rb` model.
|
|
9
|
+
* **Prompt Synchronization**: Added `/prompt sync` subcommand with unified
|
|
10
|
+
diffs and boot check capabilities.
|
|
11
|
+
* Implemented `AppState` model with `get`/`set` helpers and a `seed` boot
|
|
12
|
+
check using per-prompt `Digest::SHA256` hashes XOR-folded into a
|
|
13
|
+
fingerprint.
|
|
14
|
+
* Boot check prints `~`/`+`/`-` detail report on drift and prompts user
|
|
15
|
+
to acknowledge; declining retains old fingerprint for repeated notice
|
|
16
|
+
on next boot.
|
|
17
|
+
* Added `OC::DIFF_COMMAND` setting (`diff -u --color=always`) to `oc.rb`
|
|
18
|
+
for unified diff display, decoded via `Shellwords.split`.
|
|
19
|
+
* Implemented `prompt_sync` and `show_prompt_diff` methods in
|
|
20
|
+
`prompt_management.rb` to compare DB prompts against shipped defaults,
|
|
21
|
+
show unified diff, offer `OC::DIFF_TOOL` resolution, and clean up
|
|
22
|
+
orphaned default prompts.
|
|
23
|
+
* Wired `/prompt sync` subcommand into `commands.rb` (regexp, completion,
|
|
24
|
+
dispatch).
|
|
25
|
+
* **Test Mode**: Added `OllamaChat.test_mode` flag in `ollama_chat.rb` to
|
|
26
|
+
suppress interactive prompts during specs; set in `spec_helper.rb`.
|
|
27
|
+
* **Documentation**: Added YARD `@!attribute` docs for `test_mode` and
|
|
28
|
+
`test_mode?`.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
* **Web Searching**: Updated `web_searching.rb` to store `search_engine` in
|
|
33
|
+
local `engine` to avoid double method call.
|
|
34
|
+
* **Session Management**: Added call to `session_close` after
|
|
35
|
+
`clean_messages!` succeeds in `lib/ollama_chat/commands.rb`.
|
|
36
|
+
* **Error Messages**: Simplified stale context error message in `PatchFile`
|
|
37
|
+
when a checksum mismatch occurs.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
* **Spec Expectations**: Updated `web_searching_spec.rb` to replace `allow`
|
|
42
|
+
with `expect` for message expectations.
|
|
43
|
+
* Changed `log` expectations to `.at_least(:once)` since `links.add` also
|
|
44
|
+
triggers `log` internally.
|
|
45
|
+
* Removed unused `before { allow(chat).to receive(:log) }` from the
|
|
46
|
+
`#web` describe block.
|
|
47
|
+
* Added `.at_least(:once)` to `document_policy.selected` as it is called
|
|
48
|
+
in both `if` and `elsif` branches.
|
|
49
|
+
* Changed `fetch_source`, `summarize`, and `import` expectations to
|
|
50
|
+
`.twice` since they are called once per URL (2 URLs in test data).
|
|
51
|
+
|
|
52
|
+
### Tests
|
|
53
|
+
|
|
54
|
+
* Added 7 new spec examples for `prompt_sync` and `show_prompt_diff` in
|
|
55
|
+
`prompt_management_spec.rb`.
|
|
56
|
+
|
|
57
|
+
## 2026-08-18 v0.0.109
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
* Support for exporting and importing model options via the `/model` command:
|
|
62
|
+
* New `export_model_options` method in `model_handling.rb` to export
|
|
63
|
+
stored model profiles to a JSON file.
|
|
64
|
+
* New `import_model_options` method in `model_handling.rb` to import
|
|
65
|
+
profiles from a JSON file, featuring selective model picking, diff
|
|
66
|
+
display, and override confirmation.
|
|
67
|
+
* Extended `/model` command with `options export` and `options import`
|
|
68
|
+
subcommands.
|
|
69
|
+
* `-p` pattern flag for `/model options import` to narrow file search, with a
|
|
70
|
+
fallback to `**/*.json`.
|
|
71
|
+
* `-i` flag for `/paste` command to read input from stdin:
|
|
72
|
+
* New `paste_from_stdin` method in `clipboard.rb`.
|
|
73
|
+
* Updated `/paste` command regex and parsing to support both `-e` and
|
|
74
|
+
`-i` flags.
|
|
75
|
+
* `options` metadata to command definitions for `/copy`, `/paste`, `/toggle`,
|
|
76
|
+
and `/model`.
|
|
77
|
+
* Comprehensive test coverage for previously untested or under-tested
|
|
78
|
+
modules:
|
|
79
|
+
* `spec/ollama_chat/conversation_spec.rb` (9 examples).
|
|
80
|
+
* `spec/ollama_chat/favourites_management_spec.rb` (15 examples).
|
|
81
|
+
* `spec/ollama_chat/rag_handling_spec.rb` covering
|
|
82
|
+
`OllamaChat::RAGHandling`.
|
|
83
|
+
* `spec/ollama_chat/prompt_handling_spec.rb` (19 examples).
|
|
84
|
+
* `spec/ollama_chat/prompt_management_spec.rb` (47 examples).
|
|
85
|
+
* `spec/ollama_chat/system_prompt_management_spec.rb` (20 examples).
|
|
86
|
+
* `spec/ollama_chat/config_handling_spec.rb` (15 examples).
|
|
87
|
+
* `spec/ollama_chat/web_searching_spec.rb` (8 examples).
|
|
88
|
+
* `spec/ollama_chat/information_spec.rb` (5 examples).
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
* Refactored session management to extract and reuse
|
|
93
|
+
`set_previous_session_on_change` as a shared private method in
|
|
94
|
+
`lib/ollama_chat/session_management.rb`.
|
|
95
|
+
* `set_new_session`, `duplicate_session`, and `change_session` now
|
|
96
|
+
delegate to this method for consistent `@previous_session_id` handling.
|
|
97
|
+
* Migrated test stubs from `allow` to `expect` across multiple spec files to
|
|
98
|
+
eliminate false-green tests where stubs were never invoked.
|
|
99
|
+
* Normalized `context` and `name` arguments in `prompt_handling.rb` to handle
|
|
100
|
+
`Symbol` inputs safely in SQL queries.
|
|
101
|
+
* Updated `s.files` and `s.test_files` in `ollama_chat.gemspec` to include
|
|
102
|
+
new configuration, asset, and test files.
|
|
103
|
+
|
|
104
|
+
### Fixed
|
|
105
|
+
|
|
106
|
+
* Infinite retry loop in `kramdown_ansi.rb` by using `retried ||= false` to
|
|
107
|
+
persist the guard across `retry` calls.
|
|
108
|
+
* Incorrect YARD `@return` documentation for `#info_prompt` in
|
|
109
|
+
`prompt_management.rb`.
|
|
110
|
+
|
|
3
111
|
## 2026-08-15 v0.0.108
|
|
4
112
|
|
|
5
113
|
### Added
|
|
@@ -143,4 +143,15 @@ module OllamaChat::Clipboard
|
|
|
143
143
|
log(:error, e, data: { method: __method__ })
|
|
144
144
|
STDERR.puts e.message
|
|
145
145
|
end
|
|
146
|
+
|
|
147
|
+
# Read the full text from STDIN and return it.
|
|
148
|
+
#
|
|
149
|
+
# @param edit [Boolean] If true, the text is passed through
|
|
150
|
+
# {#edit_text} before being returned.
|
|
151
|
+
# @return [String] The raw (or edited) content read from STDIN.
|
|
152
|
+
def paste_from_stdin(edit: false)
|
|
153
|
+
text = STDIN.read
|
|
154
|
+
edit and text = edit_text(text)
|
|
155
|
+
text
|
|
156
|
+
end
|
|
146
157
|
end
|
data/lib/ollama_chat/commands.rb
CHANGED
|
@@ -27,6 +27,7 @@ module OllamaChat::Commands
|
|
|
27
27
|
command(
|
|
28
28
|
name: :copy,
|
|
29
29
|
regexp: %r(^/copy(\s+-e)?\s*$),
|
|
30
|
+
options: '[-e]',
|
|
30
31
|
help: <<~EOT
|
|
31
32
|
š Copy the last response to the clipboard.
|
|
32
33
|
Options: -e to edit before copying.
|
|
@@ -39,15 +40,22 @@ module OllamaChat::Commands
|
|
|
39
40
|
|
|
40
41
|
command(
|
|
41
42
|
name: :paste,
|
|
42
|
-
regexp: %r(^/paste(\s+-
|
|
43
|
+
regexp: %r(^/paste(\s+-[ie])*\s*$),
|
|
44
|
+
options: '[-e|-i]',
|
|
43
45
|
help: <<~EOT
|
|
44
|
-
š Paste content from the clipboard.
|
|
45
|
-
Options:
|
|
46
|
+
š Paste content from the clipboard or stdin.
|
|
47
|
+
Options:
|
|
48
|
+
-e Edit after pasting.
|
|
49
|
+
-i Read from stdin instead of clipboard.
|
|
46
50
|
EOT
|
|
47
51
|
) do |opts|
|
|
48
52
|
disable_content_parsing
|
|
49
|
-
opts = go_command('
|
|
50
|
-
|
|
53
|
+
opts = go_command('ie', opts)
|
|
54
|
+
if opts[?i]
|
|
55
|
+
paste_from_stdin(edit: opts[?e])
|
|
56
|
+
else
|
|
57
|
+
paste_from_clipboard(edit: opts[?e])
|
|
58
|
+
end
|
|
51
59
|
end
|
|
52
60
|
|
|
53
61
|
## Settings
|
|
@@ -88,6 +96,7 @@ module OllamaChat::Commands
|
|
|
88
96
|
name: :toggle,
|
|
89
97
|
regexp: %r(^/toggle(?:\s+(markdown|stream|location|runtime_info|voice|think_loud|think_strip|embedding)(?:\s+(-[yn]))?)?$),
|
|
90
98
|
complete: [ 'toggle', %w[ markdown stream location runtime_info voice think_loud think_strip embedding ] ],
|
|
99
|
+
options: '[-y|-n]',
|
|
91
100
|
help: <<~EOT
|
|
92
101
|
šļø Toggle feature switches
|
|
93
102
|
(markdown, stream, location, runtime_info,
|
|
@@ -139,17 +148,21 @@ module OllamaChat::Commands
|
|
|
139
148
|
|
|
140
149
|
command(
|
|
141
150
|
name: :model,
|
|
142
|
-
regexp: %r(^/model(?:\s+(change|options(?: copy| delete)?|options from session|options to session))?((?:\s+(?:-m))*)$),
|
|
143
|
-
complete: [ 'model', %w[ change options options\ copy options\ delete options\ from\ session options\ to\ session ] ],
|
|
151
|
+
regexp: %r(^/model(?:\s+(change|options(?: copy| delete| export| import)?|options from session|options to session))?((?:\s+(?:-m|-p\s+[\S]+))*)$),
|
|
152
|
+
complete: [ 'model', %w[ change options options\ copy options\ delete options\ export options\ import options\ from\ session options\ to\ session ] ],
|
|
153
|
+
options: '[-m|-p pattern]',
|
|
144
154
|
help: <<~EOT
|
|
145
155
|
š¤ Manage AI models & profiles:
|
|
146
156
|
- change: Switch active model
|
|
147
157
|
- options: Edit saved profile config
|
|
148
158
|
- options copy: Copy profile from another model
|
|
149
159
|
- options delete: Delete a saved profile
|
|
160
|
+
- options export: Export ALL model profiles to JSON
|
|
161
|
+
- options import: Selectively import profiles from JSON
|
|
150
162
|
- options from session: Save live ā Saved
|
|
151
163
|
- options to session: Apply Saved ā Live
|
|
152
164
|
-m interactively choose a model
|
|
165
|
+
-p PATTERN narrow file search for import
|
|
153
166
|
EOT
|
|
154
167
|
) do |subcommand, opts|
|
|
155
168
|
if subcommand == 'change'
|
|
@@ -163,7 +176,7 @@ module OllamaChat::Commands
|
|
|
163
176
|
end
|
|
164
177
|
next :next
|
|
165
178
|
end
|
|
166
|
-
opts = go_command('
|
|
179
|
+
opts = go_command('mp:', opts)
|
|
167
180
|
model = opts[?m] ? choose_model('', @model) : @model
|
|
168
181
|
case subcommand
|
|
169
182
|
when 'options'
|
|
@@ -179,6 +192,12 @@ module OllamaChat::Commands
|
|
|
179
192
|
when 'options to session'
|
|
180
193
|
profile = choose_profile_for_model(model) || 'default'
|
|
181
194
|
copy_model_options_to_session(model, profile:)
|
|
195
|
+
when 'options export'
|
|
196
|
+
export_model_options
|
|
197
|
+
when 'options import'
|
|
198
|
+
pattern = opts[?p] || '**/*.json'
|
|
199
|
+
filename = choose_filename(pattern) or next :next
|
|
200
|
+
import_model_options(filename)
|
|
182
201
|
end
|
|
183
202
|
:next
|
|
184
203
|
end
|
|
@@ -465,14 +484,15 @@ module OllamaChat::Commands
|
|
|
465
484
|
|
|
466
485
|
command(
|
|
467
486
|
name: :prompt,
|
|
468
|
-
regexp: %r(^/prompt(?:\s+(edit|info|add|delete|list|duplicate|import|export|reset|rename|-e))?(\s+(?:-[ef]|-c\s+(?:\w+|\?)))?(?:\s+([^-].*))?$),
|
|
469
|
-
complete: [ 'prompt', %w[ edit info add delete list duplicate import export reset rename ] ],
|
|
487
|
+
regexp: %r(^/prompt(?:\s+(edit|info|add|delete|list|duplicate|import|export|reset|rename|sync|-e))?(\s+(?:-[ef]|-c\s+(?:\w+|\?)))?(?:\s+([^-].*))?$),
|
|
488
|
+
complete: [ 'prompt', %w[ edit info add delete list duplicate import export reset rename sync ] ],
|
|
470
489
|
optional: true,
|
|
471
490
|
options: '[-c CONTEXT|-e|-f]',
|
|
472
491
|
help: <<~EOT,
|
|
473
492
|
š Manage prompt templates:
|
|
474
|
-
|
|
475
|
-
|
|
493
|
+
Subcommands: edit, info, add, delete, list,
|
|
494
|
+
duplicate, import, export, reset, rename,
|
|
495
|
+
sync.
|
|
476
496
|
Options: -c [context]
|
|
477
497
|
(? for interactive in /prompt),
|
|
478
498
|
-e (edit next)
|
|
@@ -525,6 +545,8 @@ module OllamaChat::Commands
|
|
|
525
545
|
STDOUT.puts "No default value found for prompt #{bold{prompt.name}}."
|
|
526
546
|
end
|
|
527
547
|
end
|
|
548
|
+
when 'sync'
|
|
549
|
+
prompt_sync(context:)
|
|
528
550
|
when nil, '-e'
|
|
529
551
|
if prompt = choose_prompt(
|
|
530
552
|
prompt: 'Which template shall guide the next response? %s',
|
|
@@ -600,6 +622,7 @@ module OllamaChat::Commands
|
|
|
600
622
|
)
|
|
601
623
|
then
|
|
602
624
|
messages.clean_messages!
|
|
625
|
+
session_close
|
|
603
626
|
STDOUT.puts "Conversation cleaned."
|
|
604
627
|
else
|
|
605
628
|
STDOUT.puts 'Cancelled.'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Sequel.migration do
|
|
2
|
+
change do
|
|
3
|
+
create_table :app_states do
|
|
4
|
+
primary_key :id
|
|
5
|
+
String :key, null: false, unique: true
|
|
6
|
+
Text :value # JSON-encoded payload
|
|
7
|
+
DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP, null: false
|
|
8
|
+
DateTime :updated_at, default: Sequel::CURRENT_TIMESTAMP, null: false
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
require 'digest'
|
|
2
|
+
|
|
3
|
+
# Represents a key/value pair of persistent application state stored in
|
|
4
|
+
# the database.
|
|
5
|
+
#
|
|
6
|
+
# This model provides a generic, JSON-backed store for small bits of
|
|
7
|
+
# runtime state that need to survive across sessionsāsuch as the
|
|
8
|
+
# XOR-SHA256 fingerprint of the shipped default prompts used for boot
|
|
9
|
+
# drift detection.
|
|
10
|
+
class OllamaChat::Database::Models::AppState < Sequel::Model(OllamaChat::DB)
|
|
11
|
+
plugin :timestamps, update_on_create: true
|
|
12
|
+
plugin :serialization, :json, :value
|
|
13
|
+
plugin :validation_helpers
|
|
14
|
+
|
|
15
|
+
# Validates the application state entry.
|
|
16
|
+
#
|
|
17
|
+
# Ensures that the `key` is present and unique.
|
|
18
|
+
def validate
|
|
19
|
+
super
|
|
20
|
+
validates_presence :key
|
|
21
|
+
validates_unique :key
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Retrieves the JSON payload stored under the given key.
|
|
25
|
+
#
|
|
26
|
+
# @param key [String, Symbol] the state key to look up
|
|
27
|
+
# @return [Hash, nil] the deserialized JSON payload, or nil if no
|
|
28
|
+
# record exists for the key
|
|
29
|
+
def self.get(key)
|
|
30
|
+
where(key: key.to_s).first&.value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Creates or updates the JSON payload stored under the given key.
|
|
34
|
+
#
|
|
35
|
+
# @param key [String, Symbol] the state key to write
|
|
36
|
+
# @param value [Hash, nil] the JSON-serializable payload to store
|
|
37
|
+
# @return [OllamaChat::Database::Models::AppState] the saved record
|
|
38
|
+
def self.set(key, value)
|
|
39
|
+
if found = where(key: key.to_s).first
|
|
40
|
+
found.update(value:)
|
|
41
|
+
found
|
|
42
|
+
else
|
|
43
|
+
create(key: key.to_s, value:)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Computes per-prompt SHA256 hashes and an XOR fingerprint of all shipped
|
|
48
|
+
# default prompts.
|
|
49
|
+
#
|
|
50
|
+
# At boot, if the stored fingerprint differs from the current one, a detail
|
|
51
|
+
# report (modified / added / removed) is printed to STDERR and the user is
|
|
52
|
+
# prompted to accept the new fingerprint. If declined, the old fingerprint
|
|
53
|
+
# is retained and the notice will reappear on the next boot.
|
|
54
|
+
#
|
|
55
|
+
# This method is invoked automatically by the model seeding loop in
|
|
56
|
+
# `OllamaChat::Database.setup_models`.
|
|
57
|
+
#
|
|
58
|
+
# @param chat [OllamaChat::Chat] the chat instance providing the config
|
|
59
|
+
# and `confirm?`
|
|
60
|
+
def self.seed(chat)
|
|
61
|
+
stored = get(:prompt_defaults_fingerprint)
|
|
62
|
+
|
|
63
|
+
stored_fingerprint = stored&.dig('fingerprint')
|
|
64
|
+
|
|
65
|
+
hashes = {}
|
|
66
|
+
chat.config.prompts.to_h.each do |context, prompts|
|
|
67
|
+
prompts.each do |name, content|
|
|
68
|
+
hashes["#{context}/#{name}"] =
|
|
69
|
+
Digest::SHA256.hexdigest(content.to_s)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
fingerprint = hashes.values.reduce(0) do |sum, hex|
|
|
74
|
+
sum ^ hex.to_i(16)
|
|
75
|
+
end.to_s(16)
|
|
76
|
+
|
|
77
|
+
return true if fingerprint == stored_fingerprint
|
|
78
|
+
|
|
79
|
+
if stored&.dig('hashes')
|
|
80
|
+
old = stored['hashes']
|
|
81
|
+
added = hashes.keys - old.keys
|
|
82
|
+
removed = old.keys - hashes.keys
|
|
83
|
+
changed = hashes.select { |k, v| old[k] && old[k] != v }.keys
|
|
84
|
+
|
|
85
|
+
STDERR.puts "ā ļø Shipped prompts changed since last boot:"
|
|
86
|
+
changed.each { |k| STDERR.puts " ~ #{k} (modified)" }
|
|
87
|
+
added.each { |k| STDERR.puts " + #{k} (new)" }
|
|
88
|
+
removed.each { |k| STDERR.puts " - #{k} (removed)" }
|
|
89
|
+
elsif !OllamaChat.test_mode?
|
|
90
|
+
STDERR.puts 'ā ļø First run ā storing prompt fingerprints.'
|
|
91
|
+
set(:prompt_defaults_fingerprint,
|
|
92
|
+
{ fingerprint:, hashes:, computed_at: Time.now.iso8601 })
|
|
93
|
+
return true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
prompt = <<~EOT.chomp << ' '
|
|
97
|
+
Keep local prompts (stop nagging)?
|
|
98
|
+
Consider adopting via /prompt sync or /prompt reset. (y/n) %s
|
|
99
|
+
EOT
|
|
100
|
+
if OllamaChat.test_mode? || chat.confirm?(prompt:, yes: /\Ay/i, timeout: 5)
|
|
101
|
+
set(:prompt_defaults_fingerprint,
|
|
102
|
+
{ fingerprint:, hashes:, computed_at: Time.now.iso8601 })
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @!attribute [v] id
|
|
109
|
+
# @return [Integer] The primary key for the state entry.
|
|
110
|
+
#
|
|
111
|
+
# @!attribute [v] key
|
|
112
|
+
# @return [String] The unique state key.
|
|
113
|
+
#
|
|
114
|
+
# @!attribute [v] value
|
|
115
|
+
# @return [Hash, nil] The JSON-serialized state payload.
|
|
116
|
+
#
|
|
117
|
+
# @!attribute [v] created_at
|
|
118
|
+
# @return [Time, nil] The timestamp when the state was created.
|
|
119
|
+
#
|
|
120
|
+
# @!attribute [v] updated_at
|
|
121
|
+
# @return [Time, nil] The timestamp of the last update to the state.
|
|
122
|
+
end
|
|
@@ -39,7 +39,7 @@ module OllamaChat::KramdownANSI
|
|
|
39
39
|
# @return [ String ] the content formatted with ANSI escape sequences
|
|
40
40
|
# according to the configured styles
|
|
41
41
|
def kramdown_ansi_parse(content)
|
|
42
|
-
retried
|
|
42
|
+
retried ||= false
|
|
43
43
|
content.nil? and return ''
|
|
44
44
|
Kramdown::ANSI.parse(content, ansi_styles: @kramdown_ansi_styles)
|
|
45
45
|
rescue Encoding::UndefinedConversionError
|
|
@@ -268,6 +268,122 @@ module OllamaChat::ModelHandling
|
|
|
268
268
|
end
|
|
269
269
|
end
|
|
270
270
|
|
|
271
|
+
# Exports all stored model options for every model to a JSON file.
|
|
272
|
+
#
|
|
273
|
+
# The resulting JSON structure is:
|
|
274
|
+
# [
|
|
275
|
+
# { "model_name": "ā¦", "profiles": [ { "profile": "ā¦", "options": {ā¦} } ] },
|
|
276
|
+
# ā¦
|
|
277
|
+
# ]
|
|
278
|
+
#
|
|
279
|
+
# @return [Pathname, nil] the path of the written file, or nil if cancelled
|
|
280
|
+
def export_model_options
|
|
281
|
+
model_names = models::ModelOptions.distinct.map(&:model_name).sort
|
|
282
|
+
unless model_names.any?
|
|
283
|
+
STDERR.puts "ā No model options stored yet!"
|
|
284
|
+
return
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
payload = model_names.map do |name|
|
|
288
|
+
{ model_name: name,
|
|
289
|
+
profiles: models::ModelOptions.where(model_name: name)
|
|
290
|
+
.order(:profile).map { |m|
|
|
291
|
+
{ profile: m.profile, options: m.options.to_h }
|
|
292
|
+
} }
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
filename = determine_valid_output_filename('to export model options to') or return
|
|
296
|
+
filename.write(JSON.pretty_generate(payload))
|
|
297
|
+
total = model_names.sum { |n|
|
|
298
|
+
models::ModelOptions.where(model_name: n).count
|
|
299
|
+
}
|
|
300
|
+
log(:info, "Model options exported",
|
|
301
|
+
data: { models: model_names, dest: filename.to_s })
|
|
302
|
+
STDOUT.puts "ā
#{model_names.size} model(s), #{total} profile(s) "\
|
|
303
|
+
"exported to #{filename.to_path.inspect}."
|
|
304
|
+
filename
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Imports model options from a JSON file into the database.
|
|
308
|
+
#
|
|
309
|
+
# The JSON file must be an array of entries:
|
|
310
|
+
# [ { "model_name": "ā¦", "profiles": [ ⦠] }, ⦠]
|
|
311
|
+
#
|
|
312
|
+
# If multiple models are present, the user selects which to import.
|
|
313
|
+
# For each profile, if an existing record has different options, both
|
|
314
|
+
# are displayed side-by-side and the user decides per-profile.
|
|
315
|
+
#
|
|
316
|
+
# @param filename [String, Pathname] the path to the JSON file to import
|
|
317
|
+
# @return [Boolean, nil] true on success, nil if cancelled
|
|
318
|
+
def import_model_options(filename)
|
|
319
|
+
filename = Pathname.new(filename)
|
|
320
|
+
data = JSON.parse(filename.read)
|
|
321
|
+
unless data.is_a?(Array) && data.all? { |d| d['model_name'] }
|
|
322
|
+
STDERR.puts "ā Invalid format: expected array of { 'model_name', 'profiles' }!"
|
|
323
|
+
return
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Let user pick models if more than one
|
|
327
|
+
if data.size > 1
|
|
328
|
+
options = (['[ALL]'] + data.map { |d| d['model_name'] } + ['[EXIT]'])
|
|
329
|
+
chosen = choose_entry(options, prompt: 'Which model(s) to import? %s')
|
|
330
|
+
case chosen
|
|
331
|
+
when '[EXIT]', nil
|
|
332
|
+
STDOUT.puts "Cancelled."
|
|
333
|
+
return
|
|
334
|
+
when '[ALL]'
|
|
335
|
+
selected = data
|
|
336
|
+
else
|
|
337
|
+
selected = data.select { |d| d['model_name'] == chosen }
|
|
338
|
+
end
|
|
339
|
+
else
|
|
340
|
+
selected = data
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
imported = 0
|
|
344
|
+
selected.each do |entry|
|
|
345
|
+
model_name = entry['model_name']
|
|
346
|
+
profiles = Array(entry['profiles'])
|
|
347
|
+
STDOUT.puts "\nš¦ #{bold{model_name}} (#{profiles.size} profile(s))"
|
|
348
|
+
|
|
349
|
+
profiles.each do |profile_data|
|
|
350
|
+
profile = profile_data['profile'] || 'default'
|
|
351
|
+
options = profile_data['options'] || {}
|
|
352
|
+
|
|
353
|
+
existing = stored_model_options_exist?(model_name, profile:)
|
|
354
|
+
if existing
|
|
355
|
+
current = existing.options.to_h.symbolize_keys_recursive
|
|
356
|
+
incoming = options.to_h.symbolize_keys_recursive
|
|
357
|
+
if current == incoming
|
|
358
|
+
STDOUT.puts " ⢠#{italic{profile}}: identical, skipping."
|
|
359
|
+
next
|
|
360
|
+
end
|
|
361
|
+
STDOUT.puts " ⢠#{italic{profile}}: differs!"
|
|
362
|
+
STDOUT.puts " š¤ Current:"
|
|
363
|
+
STDOUT.puts " " + JSON.pretty_generate(current).sub(/^/m, ' ')
|
|
364
|
+
STDOUT.puts " š„ Incoming:"
|
|
365
|
+
STDOUT.puts " " + JSON.pretty_generate(incoming).sub(/^/m, ' ')
|
|
366
|
+
unless confirm?(prompt: " ā ļø Overwrite? (y/n) ", yes: /\Ay/i)
|
|
367
|
+
STDOUT.puts " Skipped."
|
|
368
|
+
next
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
store_model_options(model_name, options, profile:)
|
|
373
|
+
imported += 1
|
|
374
|
+
STDOUT.puts " ⢠#{italic{profile}}: ā
"
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
log(:info, "Model options imported",
|
|
379
|
+
data: { models: selected.map { |e| e['model_name'] },
|
|
380
|
+
imported:, source: filename.to_s })
|
|
381
|
+
STDOUT.puts "\nā
Imported #{imported} profile(s) "\
|
|
382
|
+
"from #{filename.to_path.inspect}."
|
|
383
|
+
true
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
|
|
271
387
|
# The model_present? method checks if the specified Ollama model is
|
|
272
388
|
# available.
|
|
273
389
|
#
|
data/lib/ollama_chat/oc.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'const_conf'
|
|
2
2
|
require 'pathname'
|
|
3
|
+
require 'shellwords'
|
|
3
4
|
|
|
4
5
|
# Environment configuration module for OllamaChat
|
|
5
6
|
#
|
|
@@ -64,6 +65,14 @@ module OC
|
|
|
64
65
|
end
|
|
65
66
|
end
|
|
66
67
|
|
|
68
|
+
DIFF_COMMAND = set do
|
|
69
|
+
description 'Command (string) to generate a unified diff with'
|
|
70
|
+
|
|
71
|
+
required true
|
|
72
|
+
default { 'diff -u --color=always' }
|
|
73
|
+
decode { Shellwords.split(_1) if _1.present? }
|
|
74
|
+
end
|
|
75
|
+
|
|
67
76
|
DIFF_TOOL = set do
|
|
68
77
|
description 'Diff tool to apply changes with'
|
|
69
78
|
|
|
@@ -240,7 +240,6 @@ prompts:
|
|
|
240
240
|
How does this implementation handle malformed JSON or missing keys in the
|
|
241
241
|
middle of a 2GB file without crashing the process?
|
|
242
242
|
|
|
243
|
-
|
|
244
243
|
What is the time complexity of the current insert logic, and can we implement
|
|
245
244
|
bulk inserts to reduce database round trips?
|
|
246
245
|
|
|
@@ -10,7 +10,7 @@ module OllamaChat::PromptHandling
|
|
|
10
10
|
# @return [OllamaChat::Database::Models::Prompt, nil] the prompt model
|
|
11
11
|
# instance or nil if not found
|
|
12
12
|
def prompt(name, context: nil)
|
|
13
|
-
context
|
|
13
|
+
context = (context || 'prompt').to_s
|
|
14
14
|
models::Prompt.where(context:, name: name.to_s).first
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -19,7 +19,7 @@ module OllamaChat::PromptHandling
|
|
|
19
19
|
# @yield [prompt] yields each prompt model instance
|
|
20
20
|
# @return [Enumerator] an enumerator if no block is given
|
|
21
21
|
def each_prompt(context: nil, default: nil, &block)
|
|
22
|
-
context
|
|
22
|
+
context = (context || 'prompt').to_s
|
|
23
23
|
block or return enum_for(__method__, context:, default:)
|
|
24
24
|
prompts = models::Prompt.where(context:)
|
|
25
25
|
case default
|
|
@@ -39,7 +39,7 @@ module OllamaChat::PromptHandling
|
|
|
39
39
|
# @param name [String, Symbol] the name of the prompt to delete
|
|
40
40
|
# @return [Boolean] true if deleted, false otherwise
|
|
41
41
|
def delete_prompt(name, context: nil)
|
|
42
|
-
context
|
|
42
|
+
context = (context || 'prompt').to_s
|
|
43
43
|
if found = prompt(name, context:) and !found.metadata['default']
|
|
44
44
|
found.destroy
|
|
45
45
|
return true
|
|
@@ -54,25 +54,25 @@ module OllamaChat::PromptHandling
|
|
|
54
54
|
# @return [OllamaChat::Database::Models::Prompt] the saved prompt model
|
|
55
55
|
# instance
|
|
56
56
|
def store_prompt(name, content, context: nil)
|
|
57
|
-
context
|
|
57
|
+
context = (context || 'prompt').to_s
|
|
58
58
|
write_prompt(name, content, context:)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
# Creates or updates a prompt in the specified context.
|
|
62
62
|
#
|
|
63
|
-
# @param context [String] the context (e.g., 'prompt' or 'system')
|
|
64
|
-
# @param name [String] the name of the prompt
|
|
63
|
+
# @param context [String, Symbol] the context (e.g., 'prompt' or 'system')
|
|
64
|
+
# @param name [String, Symbol] the name of the prompt
|
|
65
65
|
# @param content [String] the content of the prompt
|
|
66
66
|
# @return [OllamaChat::Database::Models::Prompt] the created or updated
|
|
67
67
|
# prompt model instance
|
|
68
68
|
def write_prompt(name, content, context: nil)
|
|
69
|
-
context
|
|
69
|
+
context = (context || 'prompt').to_s
|
|
70
70
|
obj = nil
|
|
71
|
-
if found = models::Prompt.where(context:, name:).first
|
|
71
|
+
if found = models::Prompt.where(context:, name: name.to_s).first
|
|
72
72
|
found.metadata['content'] = content
|
|
73
73
|
obj = found
|
|
74
74
|
else
|
|
75
|
-
obj = models::Prompt.create(name
|
|
75
|
+
obj = models::Prompt.create(name: name.to_s, context:)
|
|
76
76
|
obj.metadata = { default: false, content: }.stringify_keys_recursive
|
|
77
77
|
end
|
|
78
78
|
obj.tap(&:save)
|