ollama-ruby 0.13.0 → 0.14.1
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 +39 -0
- data/README.md +68 -142
- data/Rakefile +3 -15
- data/bin/ollama_cli +37 -6
- data/lib/ollama/dto.rb +4 -0
- data/lib/ollama/version.rb +1 -1
- data/lib/ollama.rb +0 -7
- data/ollama-ruby.gemspec +9 -20
- data/spec/ollama/message_spec.rb +9 -0
- metadata +9 -191
- data/bin/ollama_chat +0 -1249
- data/config/redis.conf +0 -5
- data/docker-compose.yml +0 -10
- data/lib/ollama/utils/cache_fetcher.rb +0 -38
- data/lib/ollama/utils/chooser.rb +0 -52
- data/lib/ollama/utils/fetcher.rb +0 -175
- data/lib/ollama/utils/file_argument.rb +0 -34
- data/spec/assets/prompt.txt +0 -1
- data/spec/ollama/utils/cache_fetcher_spec.rb +0 -43
- data/spec/ollama/utils/fetcher_spec.rb +0 -137
- data/spec/ollama/utils/file_argument_spec.rb +0 -17
data/bin/ollama_chat
DELETED
@@ -1,1249 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require 'ollama'
|
4
|
-
include Ollama
|
5
|
-
include Tins::GO
|
6
|
-
require 'term/ansicolor'
|
7
|
-
include Term::ANSIColor
|
8
|
-
require 'reline'
|
9
|
-
require 'reverse_markdown'
|
10
|
-
require 'complex_config'
|
11
|
-
require 'fileutils'
|
12
|
-
require 'uri'
|
13
|
-
require 'nokogiri'
|
14
|
-
require 'rss'
|
15
|
-
require 'pdf/reader'
|
16
|
-
require 'csv'
|
17
|
-
require 'xdg'
|
18
|
-
require 'documentrix'
|
19
|
-
|
20
|
-
class OllamaChatConfig
|
21
|
-
include ComplexConfig
|
22
|
-
include FileUtils
|
23
|
-
|
24
|
-
DEFAULT_CONFIG = <<~EOT
|
25
|
-
---
|
26
|
-
url: <%= ENV['OLLAMA_URL'] || 'http://%s' % ENV.fetch('OLLAMA_HOST') %>
|
27
|
-
proxy: null # http://localhost:8080
|
28
|
-
model:
|
29
|
-
name: <%= ENV.fetch('OLLAMA_CHAT_MODEL', 'llama3.1') %>
|
30
|
-
options:
|
31
|
-
num_ctx: 8192
|
32
|
-
location:
|
33
|
-
enabled: false
|
34
|
-
name: Berlin
|
35
|
-
decimal_degrees: [ 52.514127, 13.475211 ]
|
36
|
-
units: SI (International System of Units) # or USCS (United States Customary System)
|
37
|
-
prompts:
|
38
|
-
embed: "This source was now embedded: %{source}"
|
39
|
-
summarize: |
|
40
|
-
Generate an abstract summary of the content in this document using
|
41
|
-
%{words} words:
|
42
|
-
|
43
|
-
%{source_content}
|
44
|
-
web: |
|
45
|
-
Answer the the query %{query} using these sources and summaries:
|
46
|
-
|
47
|
-
%{results}
|
48
|
-
system_prompts:
|
49
|
-
default: <%= ENV.fetch('OLLAMA_CHAT_SYSTEM', 'null') %>
|
50
|
-
voice:
|
51
|
-
enabled: false
|
52
|
-
default: Samantha
|
53
|
-
list: <%= `say -v ? 2>/dev/null`.lines.map { _1[/^(.+?)\s+[a-z]{2}_[a-zA-Z0-9]{2,}/, 1] }.uniq.sort.to_s.force_encoding('ASCII-8BIT') %>
|
54
|
-
markdown: true
|
55
|
-
stream: true
|
56
|
-
document_policy: importing
|
57
|
-
embedding:
|
58
|
-
enabled: true
|
59
|
-
model:
|
60
|
-
name: mxbai-embed-large
|
61
|
-
embedding_length: 1024
|
62
|
-
options: {}
|
63
|
-
# Retrieval prompt template:
|
64
|
-
prompt: 'Represent this sentence for searching relevant passages: %s'
|
65
|
-
batch_size: 10
|
66
|
-
database_filename: null # ':memory:'
|
67
|
-
collection: <%= ENV['OLLAMA_CHAT_COLLECTION'] %>
|
68
|
-
found_texts_size: 4096
|
69
|
-
found_texts_count: 10
|
70
|
-
splitter:
|
71
|
-
name: RecursiveCharacter
|
72
|
-
chunk_size: 1024
|
73
|
-
cache: Ollama::Documents::SQLiteCache
|
74
|
-
redis:
|
75
|
-
documents:
|
76
|
-
url: <%= ENV.fetch('REDIS_URL', 'null') %>
|
77
|
-
expiring:
|
78
|
-
url: <%= ENV.fetch('REDIS_EXPIRING_URL', 'null') %>
|
79
|
-
ex: 86400
|
80
|
-
debug: <%= ENV['OLLAMA_CHAT_DEBUG'].to_i == 1 ? true : false %>
|
81
|
-
ssl_no_verify: []
|
82
|
-
copy: pbcopy
|
83
|
-
EOT
|
84
|
-
|
85
|
-
def initialize(filename = nil)
|
86
|
-
@filename = filename || default_path
|
87
|
-
unless File.directory?(cache_dir_path)
|
88
|
-
mkdir_p cache_dir_path.to_s
|
89
|
-
end
|
90
|
-
@config = Provider.config(@filename, '⚙️')
|
91
|
-
retried = false
|
92
|
-
rescue ConfigurationFileMissing
|
93
|
-
if @filename == default_path && !retried
|
94
|
-
retried = true
|
95
|
-
mkdir_p config_dir_path.to_s
|
96
|
-
File.secure_write(default_path, DEFAULT_CONFIG)
|
97
|
-
retry
|
98
|
-
else
|
99
|
-
raise
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
attr_reader :filename
|
104
|
-
|
105
|
-
attr_reader :config
|
106
|
-
|
107
|
-
def default_path
|
108
|
-
config_dir_path + 'config.yml'
|
109
|
-
end
|
110
|
-
|
111
|
-
def config_dir_path
|
112
|
-
XDG.new.config_home + 'ollama_chat'
|
113
|
-
end
|
114
|
-
|
115
|
-
def cache_dir_path
|
116
|
-
XDG.new.cache_home + 'ollama_chat'
|
117
|
-
end
|
118
|
-
|
119
|
-
def database_path
|
120
|
-
cache_dir_path + 'documents.db'
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
class FollowChat
|
125
|
-
include Handlers::Concern
|
126
|
-
include Term::ANSIColor
|
127
|
-
|
128
|
-
def initialize(messages:, markdown: false, voice: nil, output: $stdout)
|
129
|
-
super(output:)
|
130
|
-
@output.sync = true
|
131
|
-
@markdown = markdown
|
132
|
-
@say = voice ? Handlers::Say.new(voice:) : NOP
|
133
|
-
@messages = messages
|
134
|
-
@user = nil
|
135
|
-
end
|
136
|
-
|
137
|
-
def call(response)
|
138
|
-
$config.debug and jj response
|
139
|
-
if response&.message&.role == 'assistant'
|
140
|
-
if @messages.last.role != 'assistant'
|
141
|
-
@messages << Message.new(role: 'assistant', content: '')
|
142
|
-
@user = message_type(@messages.last.images) + " " +
|
143
|
-
bold { color(111) { 'assistant:' } }
|
144
|
-
puts @user unless @markdown
|
145
|
-
end
|
146
|
-
content = response.message&.content
|
147
|
-
@messages.last.content << content
|
148
|
-
if @markdown and content = @messages.last.content.full?
|
149
|
-
markdown_content = Kramdown::ANSI.parse(content)
|
150
|
-
@output.print clear_screen, move_home, @user, ?\n, markdown_content
|
151
|
-
else
|
152
|
-
@output.print content
|
153
|
-
end
|
154
|
-
@say.call(response)
|
155
|
-
end
|
156
|
-
if response.done
|
157
|
-
@output.puts "", eval_stats(response)
|
158
|
-
end
|
159
|
-
self
|
160
|
-
end
|
161
|
-
|
162
|
-
def eval_stats(response)
|
163
|
-
eval_duration = response.eval_duration / 1e9
|
164
|
-
prompt_eval_duration = response.prompt_eval_duration / 1e9
|
165
|
-
stats_text = {
|
166
|
-
eval_duration: Tins::Duration.new(eval_duration),
|
167
|
-
eval_count: response.eval_count.to_i,
|
168
|
-
eval_rate: bold { "%.2f c/s" % (response.eval_count.to_i / eval_duration) } + color(111),
|
169
|
-
prompt_eval_duration: Tins::Duration.new(prompt_eval_duration),
|
170
|
-
prompt_eval_count: response.prompt_eval_count.to_i,
|
171
|
-
prompt_eval_rate: bold { "%.2f c/s" % (response.prompt_eval_count.to_i / prompt_eval_duration) } + color(111),
|
172
|
-
total_duration: Tins::Duration.new(response.total_duration / 1e9),
|
173
|
-
load_duration: Tins::Duration.new(response.load_duration / 1e9),
|
174
|
-
}.map { _1 * '=' } * ' '
|
175
|
-
'📊 ' + color(111) {
|
176
|
-
Kramdown::ANSI::Width.wrap(stats_text, percentage: 90).gsub(/(?<!\A)^/, ' ')
|
177
|
-
}
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
module Switches
|
182
|
-
module CheckSwitch
|
183
|
-
extend Tins::Concern
|
184
|
-
|
185
|
-
included do
|
186
|
-
alias_method :on?, :value
|
187
|
-
end
|
188
|
-
|
189
|
-
def off?
|
190
|
-
!on?
|
191
|
-
end
|
192
|
-
|
193
|
-
def show
|
194
|
-
puts @msg[value]
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
class Switch
|
199
|
-
def initialize(name, msg:, config: $config)
|
200
|
-
@value = [ false, true ].include?(config) ? config : !!config.send("#{name}?")
|
201
|
-
@msg = msg
|
202
|
-
end
|
203
|
-
|
204
|
-
attr_reader :value
|
205
|
-
|
206
|
-
def set(value, show: false)
|
207
|
-
@value = !!value
|
208
|
-
show && self.show
|
209
|
-
end
|
210
|
-
|
211
|
-
def toggle(show: true)
|
212
|
-
@value = !@value
|
213
|
-
show && self.show
|
214
|
-
end
|
215
|
-
|
216
|
-
include CheckSwitch
|
217
|
-
end
|
218
|
-
|
219
|
-
class CombinedSwitch
|
220
|
-
def initialize(value:, msg:)
|
221
|
-
@value = value
|
222
|
-
@msg = msg
|
223
|
-
end
|
224
|
-
|
225
|
-
def value
|
226
|
-
@value.()
|
227
|
-
end
|
228
|
-
|
229
|
-
include CheckSwitch
|
230
|
-
end
|
231
|
-
|
232
|
-
def setup_switches
|
233
|
-
$markdown = Switch.new(
|
234
|
-
:markdown,
|
235
|
-
msg: {
|
236
|
-
true => "Using #{italic{'ANSI'}} markdown to output content.",
|
237
|
-
false => "Using plaintext for outputting content.",
|
238
|
-
}
|
239
|
-
)
|
240
|
-
|
241
|
-
$stream = Switch.new(
|
242
|
-
:stream,
|
243
|
-
msg: {
|
244
|
-
true => "Streaming enabled.",
|
245
|
-
false => "Streaming disabled.",
|
246
|
-
}
|
247
|
-
)
|
248
|
-
|
249
|
-
$voice = Switch.new(
|
250
|
-
:stream,
|
251
|
-
msg: {
|
252
|
-
true => "Voice output enabled.",
|
253
|
-
false => "Voice output disabled.",
|
254
|
-
},
|
255
|
-
config: $config.voice
|
256
|
-
)
|
257
|
-
|
258
|
-
$embedding_enabled = Switch.new(
|
259
|
-
:embedding_enabled,
|
260
|
-
msg: {
|
261
|
-
true => "Embedding enabled.",
|
262
|
-
false => "Embedding disabled.",
|
263
|
-
}
|
264
|
-
)
|
265
|
-
|
266
|
-
$embedding_paused = Switch.new(
|
267
|
-
:embedding_paused,
|
268
|
-
msg: {
|
269
|
-
true => "Embedding paused.",
|
270
|
-
false => "Embedding resumed.",
|
271
|
-
}
|
272
|
-
)
|
273
|
-
|
274
|
-
$embedding = CombinedSwitch.new(
|
275
|
-
value: -> { $embedding_enabled.on? && $embedding_paused.off? },
|
276
|
-
msg: {
|
277
|
-
true => "Embedding is currently performed.",
|
278
|
-
false => "Embedding is currently not performed.",
|
279
|
-
}
|
280
|
-
)
|
281
|
-
|
282
|
-
$location = Switch.new(
|
283
|
-
:location,
|
284
|
-
msg: {
|
285
|
-
true => "Location and localtime enabled.",
|
286
|
-
false => "Location and localtime disabled.",
|
287
|
-
},
|
288
|
-
config: $config.location.enabled
|
289
|
-
)
|
290
|
-
end
|
291
|
-
end
|
292
|
-
include Switches
|
293
|
-
|
294
|
-
def pull_model_unless_present(model, options, retried = false)
|
295
|
-
ollama.show(name: model) { |response|
|
296
|
-
puts "Model #{bold{model}} with architecture "\
|
297
|
-
"#{response.model_info['general.architecture']} found."
|
298
|
-
if system = response.system
|
299
|
-
puts "Configured model system prompt is:\n#{italic { system }}"
|
300
|
-
return system
|
301
|
-
else
|
302
|
-
return
|
303
|
-
end
|
304
|
-
}
|
305
|
-
rescue Errors::NotFoundError
|
306
|
-
puts "Model #{bold{model}} not found locally, attempting to pull it from remote now…"
|
307
|
-
ollama.pull(name: model)
|
308
|
-
if retried
|
309
|
-
exit 1
|
310
|
-
else
|
311
|
-
retried = true
|
312
|
-
retry
|
313
|
-
end
|
314
|
-
rescue Errors::Error => e
|
315
|
-
warn "Caught #{e.class} while pulling model: #{e} => Exiting."
|
316
|
-
exit 1
|
317
|
-
end
|
318
|
-
|
319
|
-
def search_web(query, n = nil)
|
320
|
-
if l = at_location
|
321
|
-
query += " #{at_location}"
|
322
|
-
end
|
323
|
-
n = n.to_i
|
324
|
-
n < 1 and n = 1
|
325
|
-
query = URI.encode_uri_component(query)
|
326
|
-
url = "https://www.duckduckgo.com/html/?q=#{query}"
|
327
|
-
Ollama::Utils::Fetcher.get(url, debug: $config.debug) do |tmp|
|
328
|
-
result = []
|
329
|
-
doc = Nokogiri::HTML(tmp)
|
330
|
-
doc.css('.results_links').each do |link|
|
331
|
-
if n > 0
|
332
|
-
url = link.css('.result__a').first&.[]('href')
|
333
|
-
url.sub!(%r(\A(//duckduckgo\.com)?/l/\?uddg=), '')
|
334
|
-
url.sub!(%r(&rut=.*), '')
|
335
|
-
url = URI.decode_uri_component(url)
|
336
|
-
url = URI.parse(url)
|
337
|
-
url.host =~ /duckduckgo\.com/ and next
|
338
|
-
$links.add(url.to_s)
|
339
|
-
result << url
|
340
|
-
n -= 1
|
341
|
-
else
|
342
|
-
break
|
343
|
-
end
|
344
|
-
end
|
345
|
-
result
|
346
|
-
end
|
347
|
-
end
|
348
|
-
|
349
|
-
def load_conversation(filename)
|
350
|
-
unless File.exist?(filename)
|
351
|
-
puts "File #{filename} doesn't exist. Choose another filename."
|
352
|
-
return
|
353
|
-
end
|
354
|
-
File.open(filename, 'r') do |output|
|
355
|
-
return JSON(output.read).map { Message.from_hash(_1) }
|
356
|
-
end
|
357
|
-
end
|
358
|
-
|
359
|
-
def save_conversation(filename, messages)
|
360
|
-
if File.exist?(filename)
|
361
|
-
puts "File #{filename} already exists. Choose another filename."
|
362
|
-
return
|
363
|
-
end
|
364
|
-
File.open(filename, 'w') do |output|
|
365
|
-
output.puts JSON(messages)
|
366
|
-
end
|
367
|
-
end
|
368
|
-
|
369
|
-
def message_type(images)
|
370
|
-
images.present? ? ?📸 : ?📨
|
371
|
-
end
|
372
|
-
|
373
|
-
def list_conversation(messages, last = nil)
|
374
|
-
last = (last || messages.size).clamp(0, messages.size)
|
375
|
-
messages[-last..-1].to_a.each do |m|
|
376
|
-
role_color = case m.role
|
377
|
-
when 'user' then 172
|
378
|
-
when 'assistant' then 111
|
379
|
-
when 'system' then 213
|
380
|
-
else 210
|
381
|
-
end
|
382
|
-
content = m.content.full? { $markdown.on? ? Kramdown::ANSI.parse(_1) : _1 }
|
383
|
-
message_text = message_type(m.images) + " "
|
384
|
-
message_text += bold { color(role_color) { m.role } }
|
385
|
-
message_text += ":\n#{content}"
|
386
|
-
m.images.full? { |images|
|
387
|
-
message_text += "\nImages: " + italic { images.map(&:path) * ', ' }
|
388
|
-
}
|
389
|
-
puts message_text
|
390
|
-
end
|
391
|
-
end
|
392
|
-
|
393
|
-
module SourceParsing
|
394
|
-
def parse_source(source_io)
|
395
|
-
case source_io&.content_type
|
396
|
-
when 'text/html'
|
397
|
-
reverse_markdown(source_io.read)
|
398
|
-
when 'text/xml'
|
399
|
-
if source_io.readline =~ %r(^\s*<rss\s)
|
400
|
-
source_io.rewind
|
401
|
-
return parse_rss(source_io)
|
402
|
-
end
|
403
|
-
source_io.rewind
|
404
|
-
source_io.read
|
405
|
-
when 'text/csv'
|
406
|
-
parse_csv(source_io)
|
407
|
-
when 'application/rss+xml'
|
408
|
-
parse_rss(source_io)
|
409
|
-
when 'application/atom+xml'
|
410
|
-
parse_atom(source_io)
|
411
|
-
when 'application/postscript'
|
412
|
-
ps_read(source_io)
|
413
|
-
when 'application/pdf'
|
414
|
-
pdf_read(source_io)
|
415
|
-
when %r(\Aapplication/(json|ld\+json|x-ruby|x-perl|x-gawk|x-python|x-javascript|x-c?sh|x-dosexec|x-shellscript|x-tex|x-latex|x-lyx|x-bibtex)), %r(\Atext/), nil
|
416
|
-
source_io.read
|
417
|
-
else
|
418
|
-
STDERR.puts "Cannot embed #{source_io&.content_type} document."
|
419
|
-
return
|
420
|
-
end
|
421
|
-
end
|
422
|
-
|
423
|
-
def parse_csv(source_io)
|
424
|
-
result = +''
|
425
|
-
CSV.table(File.new(source_io), col_sep: ?,).each do |row|
|
426
|
-
next if row.fields.select(&:present?).size == 0
|
427
|
-
result << row.map { |pair|
|
428
|
-
pair.compact.map { _1.to_s.strip } * ': ' if pair.last.present?
|
429
|
-
}.select(&:present?).map { _1.prepend(' ') } * ?\n
|
430
|
-
result << "\n\n"
|
431
|
-
end
|
432
|
-
result
|
433
|
-
end
|
434
|
-
|
435
|
-
def parse_rss(source_io)
|
436
|
-
feed = RSS::Parser.parse(source_io, false, false)
|
437
|
-
title = <<~EOT
|
438
|
-
# #{feed&.channel&.title}
|
439
|
-
|
440
|
-
EOT
|
441
|
-
feed.items.inject(title) do |text, item|
|
442
|
-
text << <<~EOT
|
443
|
-
## [#{item&.title}](#{item&.link})
|
444
|
-
|
445
|
-
updated on #{item&.pubDate}
|
446
|
-
|
447
|
-
#{reverse_markdown(item&.description)}
|
448
|
-
|
449
|
-
EOT
|
450
|
-
end
|
451
|
-
end
|
452
|
-
|
453
|
-
def parse_atom(source_io)
|
454
|
-
feed = RSS::Parser.parse(source_io, false, false)
|
455
|
-
title = <<~EOT
|
456
|
-
# #{feed.title.content}
|
457
|
-
|
458
|
-
EOT
|
459
|
-
feed.items.inject(title) do |text, item|
|
460
|
-
text << <<~EOT
|
461
|
-
## [#{item&.title&.content}](#{item&.link&.href})
|
462
|
-
|
463
|
-
updated on #{item&.updated&.content}
|
464
|
-
|
465
|
-
#{reverse_markdown(item&.content&.content)}
|
466
|
-
|
467
|
-
EOT
|
468
|
-
end
|
469
|
-
end
|
470
|
-
|
471
|
-
def pdf_read(io)
|
472
|
-
reader = PDF::Reader.new(io)
|
473
|
-
reader.pages.inject(+'') { |result, page| result << page.text }
|
474
|
-
end
|
475
|
-
|
476
|
-
def ps_read(io)
|
477
|
-
gs = `which gs`.chomp
|
478
|
-
if gs.present?
|
479
|
-
Tempfile.create do |tmp|
|
480
|
-
IO.popen("#{gs} -q -sDEVICE=pdfwrite -sOutputFile=#{tmp.path} -", 'wb') do |gs_io|
|
481
|
-
until io.eof?
|
482
|
-
buffer = io.read(1 << 17)
|
483
|
-
IO.select(nil, [ gs_io ], nil)
|
484
|
-
gs_io.write buffer
|
485
|
-
end
|
486
|
-
gs_io.close
|
487
|
-
File.open(tmp.path, 'rb') do |pdf|
|
488
|
-
pdf_read(pdf)
|
489
|
-
end
|
490
|
-
end
|
491
|
-
end
|
492
|
-
else
|
493
|
-
STDERR.puts "Cannot convert #{io&.content_type} whith ghostscript, gs not in path."
|
494
|
-
end
|
495
|
-
end
|
496
|
-
|
497
|
-
def reverse_markdown(html)
|
498
|
-
ReverseMarkdown.convert(
|
499
|
-
html,
|
500
|
-
unknown_tags: :bypass,
|
501
|
-
github_flavored: true,
|
502
|
-
tag_border: ''
|
503
|
-
)
|
504
|
-
end
|
505
|
-
end
|
506
|
-
include SourceParsing
|
507
|
-
|
508
|
-
def http_options(url)
|
509
|
-
options = {}
|
510
|
-
if ssl_no_verify = $config.ssl_no_verify?
|
511
|
-
hostname = URI.parse(url).hostname
|
512
|
-
options |= { ssl_verify_peer: !ssl_no_verify.include?(hostname) }
|
513
|
-
end
|
514
|
-
if proxy = $config.proxy?
|
515
|
-
options |= { proxy: }
|
516
|
-
end
|
517
|
-
options
|
518
|
-
end
|
519
|
-
|
520
|
-
def fetch_source(source, &block)
|
521
|
-
case source
|
522
|
-
when %r(\A!(.*))
|
523
|
-
command = $1
|
524
|
-
Ollama::Utils::Fetcher.execute(command) do |tmp|
|
525
|
-
block.(tmp)
|
526
|
-
end
|
527
|
-
when %r(\Ahttps?://\S+)
|
528
|
-
$links.add(source.to_s)
|
529
|
-
Utils::Fetcher.get(
|
530
|
-
source,
|
531
|
-
cache: $cache,
|
532
|
-
debug: $config.debug,
|
533
|
-
http_options: http_options(Utils::Fetcher.normalize_url(source))
|
534
|
-
) do |tmp|
|
535
|
-
block.(tmp)
|
536
|
-
end
|
537
|
-
when %r(\Afile://(/\S*)|\A((?:\.\.|[~.]?)/\S*))
|
538
|
-
filename = $~.captures.compact.first
|
539
|
-
filename = File.expand_path(filename)
|
540
|
-
Utils::Fetcher.read(filename) do |tmp|
|
541
|
-
block.(tmp)
|
542
|
-
end
|
543
|
-
else
|
544
|
-
raise "invalid source"
|
545
|
-
end
|
546
|
-
rescue => e
|
547
|
-
STDERR.puts "Cannot fetch source #{source.to_s.inspect}: #{e.class} #{e}\n#{e.backtrace * ?\n}"
|
548
|
-
end
|
549
|
-
|
550
|
-
def add_image(images, source_io, source)
|
551
|
-
STDERR.puts "Adding #{source_io&.content_type} image #{source.to_s.inspect}."
|
552
|
-
image = Image.for_io(source_io, path: source.to_s)
|
553
|
-
(images << image).uniq!
|
554
|
-
end
|
555
|
-
|
556
|
-
def import_source(source_io, source)
|
557
|
-
source = source.to_s
|
558
|
-
puts "Importing #{italic { source_io&.content_type }} document #{source.to_s.inspect} now."
|
559
|
-
source_content = parse_source(source_io)
|
560
|
-
"Imported #{source.inspect}:\n#{source_content}\n\n"
|
561
|
-
end
|
562
|
-
|
563
|
-
def import(source)
|
564
|
-
fetch_source(source) do |source_io|
|
565
|
-
content = import_source(source_io, source) or return
|
566
|
-
source_io.rewind
|
567
|
-
content
|
568
|
-
end
|
569
|
-
end
|
570
|
-
|
571
|
-
def summarize_source(source_io, source, words: nil)
|
572
|
-
puts "Summarizing #{italic { source_io&.content_type }} document #{source.to_s.inspect} now."
|
573
|
-
words = words.to_i
|
574
|
-
words < 1 and words = 100
|
575
|
-
source_content = parse_source(source_io)
|
576
|
-
source_content.present? or return
|
577
|
-
$config.prompts.summarize % { source_content:, words: }
|
578
|
-
end
|
579
|
-
|
580
|
-
def summarize(source, words: nil)
|
581
|
-
fetch_source(source) do |source_io|
|
582
|
-
content = summarize_source(source_io, source, words:) or return
|
583
|
-
source_io.rewind
|
584
|
-
content
|
585
|
-
end
|
586
|
-
end
|
587
|
-
|
588
|
-
def embed_source(source_io, source, count: nil)
|
589
|
-
$embedding.on? or return parse_source(source_io)
|
590
|
-
m = "Embedding #{italic { source_io&.content_type }} document #{source.to_s.inspect}."
|
591
|
-
if count
|
592
|
-
puts '%u. %s' % [ count, m ]
|
593
|
-
else
|
594
|
-
puts m
|
595
|
-
end
|
596
|
-
text = parse_source(source_io) or return
|
597
|
-
text.downcase!
|
598
|
-
splitter_config = $config.embedding.splitter
|
599
|
-
inputs = nil
|
600
|
-
case splitter_config.name
|
601
|
-
when 'Character'
|
602
|
-
splitter = Documentrix::Documents::Splitters::Character.new(
|
603
|
-
chunk_size: splitter_config.chunk_size,
|
604
|
-
)
|
605
|
-
inputs = splitter.split(text)
|
606
|
-
when 'RecursiveCharacter'
|
607
|
-
splitter = Documentrix::Documents::Splitters::RecursiveCharacter.new(
|
608
|
-
chunk_size: splitter_config.chunk_size,
|
609
|
-
)
|
610
|
-
inputs = splitter.split(text)
|
611
|
-
when 'Semantic'
|
612
|
-
splitter = Documentrix::Documents::Splitters::Semantic.new(
|
613
|
-
ollama:, model: $config.embedding.model.name,
|
614
|
-
chunk_size: splitter_config.chunk_size,
|
615
|
-
)
|
616
|
-
inputs = splitter.split(
|
617
|
-
text,
|
618
|
-
breakpoint: splitter_config.breakpoint.to_sym,
|
619
|
-
percentage: splitter_config.percentage?,
|
620
|
-
percentile: splitter_config.percentile?,
|
621
|
-
)
|
622
|
-
end
|
623
|
-
inputs or return
|
624
|
-
source = source.to_s
|
625
|
-
if source.start_with?(?!)
|
626
|
-
source = Kramdown::ANSI::Width.truncate(
|
627
|
-
source[1..-1].gsub(/\W+/, ?_),
|
628
|
-
length: 10
|
629
|
-
)
|
630
|
-
end
|
631
|
-
$documents.add(inputs, source:, batch_size: $config.embedding.batch_size?)
|
632
|
-
end
|
633
|
-
|
634
|
-
def embed(source)
|
635
|
-
if $embedding.on?
|
636
|
-
puts "Now embedding #{source.to_s.inspect}."
|
637
|
-
fetch_source(source) do |source_io|
|
638
|
-
content = parse_source(source_io)
|
639
|
-
content.present? or return
|
640
|
-
source_io.rewind
|
641
|
-
embed_source(source_io, source)
|
642
|
-
end
|
643
|
-
$config.prompts.embed % { source: }
|
644
|
-
else
|
645
|
-
puts "Embedding is off, so I will just give a small summary of this source."
|
646
|
-
summarize(source)
|
647
|
-
end
|
648
|
-
end
|
649
|
-
|
650
|
-
def parse_content(content, images)
|
651
|
-
images.clear
|
652
|
-
tags = Documentrix::Utils::Tags.new
|
653
|
-
|
654
|
-
contents = [ content ]
|
655
|
-
content.scan(%r((https?://\S+)|(#\S+)|(?:file://)?(\S*\/\S+))).each do |url, tag, file|
|
656
|
-
case
|
657
|
-
when tag
|
658
|
-
tags.add(tag)
|
659
|
-
next
|
660
|
-
when file
|
661
|
-
file = file.sub(/#.*/, '')
|
662
|
-
file =~ %r(\A[~./]) or file.prepend('./')
|
663
|
-
File.exist?(file) or next
|
664
|
-
source = file
|
665
|
-
when url
|
666
|
-
$links.add(url.to_s)
|
667
|
-
source = url
|
668
|
-
end
|
669
|
-
fetch_source(source) do |source_io|
|
670
|
-
case source_io&.content_type&.media_type
|
671
|
-
when 'image'
|
672
|
-
add_image(images, source_io, source)
|
673
|
-
when 'text', 'application', nil
|
674
|
-
case $document_policy
|
675
|
-
when 'ignoring'
|
676
|
-
nil
|
677
|
-
when 'importing'
|
678
|
-
contents << import_source(source_io, source)
|
679
|
-
when 'embedding'
|
680
|
-
embed_source(source_io, source)
|
681
|
-
when 'summarizing'
|
682
|
-
contents << summarize_source(source_io, source)
|
683
|
-
end
|
684
|
-
else
|
685
|
-
STDERR.puts(
|
686
|
-
"Cannot fetch #{source.to_s.inspect} with content type "\
|
687
|
-
"#{source_io&.content_type.inspect}"
|
688
|
-
)
|
689
|
-
end
|
690
|
-
end
|
691
|
-
end
|
692
|
-
new_content = contents.select { _1.present? rescue nil }.compact * "\n\n"
|
693
|
-
return new_content, (tags unless tags.empty?)
|
694
|
-
end
|
695
|
-
|
696
|
-
def choose_model(cli_model, current_model)
|
697
|
-
models = ollama.tags.models.map(&:name).sort
|
698
|
-
model = if cli_model == ''
|
699
|
-
Ollama::Utils::Chooser.choose(models) || current_model
|
700
|
-
else
|
701
|
-
cli_model || current_model
|
702
|
-
end
|
703
|
-
ensure
|
704
|
-
puts green { "Connecting to #{model}@#{ollama.base_url} now…" }
|
705
|
-
end
|
706
|
-
|
707
|
-
def ask?(prompt:)
|
708
|
-
print prompt
|
709
|
-
STDIN.gets.chomp
|
710
|
-
end
|
711
|
-
|
712
|
-
def choose_collection(current_collection)
|
713
|
-
collections = [ current_collection ] + $documents.collections
|
714
|
-
collections = collections.compact.map(&:to_s).uniq.sort
|
715
|
-
collections.unshift('[EXIT]').unshift('[NEW]')
|
716
|
-
collection = Ollama::Utils::Chooser.choose(collections) || current_collection
|
717
|
-
case collection
|
718
|
-
when '[NEW]'
|
719
|
-
$documents.collection = ask?(prompt: "Enter name of the new collection: ")
|
720
|
-
when nil, '[EXIT]'
|
721
|
-
puts "Exiting chooser."
|
722
|
-
when /./
|
723
|
-
$documents.collection = collection
|
724
|
-
end
|
725
|
-
ensure
|
726
|
-
puts "Using collection #{bold{$documents.collection}}."
|
727
|
-
info
|
728
|
-
end
|
729
|
-
|
730
|
-
def choose_document_policy
|
731
|
-
policies = %w[ importing embedding summarizing ignoring ].sort
|
732
|
-
current = if policies.index($document_policy)
|
733
|
-
$document_policy
|
734
|
-
elsif policies.index($config.document_policy)
|
735
|
-
$config.document_policy
|
736
|
-
else
|
737
|
-
policies.first
|
738
|
-
end
|
739
|
-
policies.unshift('[EXIT]')
|
740
|
-
policy = Ollama::Utils::Chooser.choose(policies)
|
741
|
-
case policy
|
742
|
-
when nil, '[EXIT]'
|
743
|
-
puts "Exiting chooser."
|
744
|
-
policy = current
|
745
|
-
end
|
746
|
-
$document_policy = policy
|
747
|
-
ensure
|
748
|
-
puts "Using document policy #{bold{$document_policy}}."
|
749
|
-
info
|
750
|
-
end
|
751
|
-
|
752
|
-
def collection_stats
|
753
|
-
puts <<~EOT
|
754
|
-
Current Collection
|
755
|
-
Name: #{bold{$documents.collection}}
|
756
|
-
#Embeddings: #{$documents.size}
|
757
|
-
#Tags: #{$documents.tags.size}
|
758
|
-
Tags: #{$documents.tags}
|
759
|
-
EOT
|
760
|
-
end
|
761
|
-
|
762
|
-
def configure_cache
|
763
|
-
if $opts[?M]
|
764
|
-
Documentrix::Documents::MemoryCache
|
765
|
-
else
|
766
|
-
Object.const_get($config.cache)
|
767
|
-
end
|
768
|
-
rescue => e
|
769
|
-
STDERR.puts "Caught #{e.class}: #{e} => Falling back to MemoryCache."
|
770
|
-
Documentrix::Documents::MemoryCache
|
771
|
-
end
|
772
|
-
|
773
|
-
def show_system_prompt
|
774
|
-
puts <<~EOT
|
775
|
-
Configured system prompt is:
|
776
|
-
#{Kramdown::ANSI.parse($system.to_s).gsub(/\n+\z/, '').full? || 'n/a'}
|
777
|
-
EOT
|
778
|
-
end
|
779
|
-
|
780
|
-
def at_location
|
781
|
-
if $location.on?
|
782
|
-
location_name = $config.location.name
|
783
|
-
location_decimal_degrees = $config.location.decimal_degrees * ', '
|
784
|
-
localtime = Time.now.iso8601
|
785
|
-
units = $config.location.units
|
786
|
-
$config.prompts.location % {
|
787
|
-
location_name:, location_decimal_degrees:, localtime:, units:,
|
788
|
-
}
|
789
|
-
end.to_s
|
790
|
-
end
|
791
|
-
|
792
|
-
def set_system_prompt(messages, system)
|
793
|
-
$system = system
|
794
|
-
messages.clear
|
795
|
-
messages << Message.new(role: 'system', content: system)
|
796
|
-
end
|
797
|
-
|
798
|
-
def change_system_prompt(messages, default, system: nil)
|
799
|
-
selector = Regexp.new(system.to_s[1..-1].to_s)
|
800
|
-
prompts = $config.system_prompts.attribute_names.compact.grep(selector)
|
801
|
-
chosen = Ollama::Utils::Chooser.choose(prompts, return_immediately: true)
|
802
|
-
system = if chosen
|
803
|
-
$config.system_prompts.send(chosen)
|
804
|
-
else
|
805
|
-
default
|
806
|
-
end
|
807
|
-
set_system_prompt(messages, system)
|
808
|
-
end
|
809
|
-
|
810
|
-
def change_voice
|
811
|
-
chosen = Ollama::Utils::Chooser.choose($config.voice.list)
|
812
|
-
$current_voice = chosen.full? || $config.voice.default
|
813
|
-
end
|
814
|
-
|
815
|
-
def info
|
816
|
-
puts "Current model is #{bold{$model}}."
|
817
|
-
if $model_options.present?
|
818
|
-
puts " Options: #{JSON.pretty_generate($model_options).gsub(/(?<!\A)^/, ' ')}"
|
819
|
-
end
|
820
|
-
$embedding.show
|
821
|
-
if $embedding.on?
|
822
|
-
puts "Embedding model is #{bold{$embedding_model}}"
|
823
|
-
if $embedding_model_options.present?
|
824
|
-
puts " Options: #{JSON.pretty_generate($embedding_model_options).gsub(/(?<!\A)^/, ' ')}"
|
825
|
-
end
|
826
|
-
puts "Text splitter is #{bold{$config.embedding.splitter.name}}."
|
827
|
-
collection_stats
|
828
|
-
end
|
829
|
-
puts "Documents database cache is #{$documents.nil? ? 'n/a' : bold{$documents.cache.class}}"
|
830
|
-
$markdown.show
|
831
|
-
$stream.show
|
832
|
-
$location.show
|
833
|
-
puts "Document policy for references in user text: #{bold{$document_policy}}"
|
834
|
-
if $voice.on?
|
835
|
-
puts "Using voice #{bold{$current_voice}} to speak."
|
836
|
-
end
|
837
|
-
show_system_prompt
|
838
|
-
end
|
839
|
-
|
840
|
-
def clear_messages(messages)
|
841
|
-
messages.delete_if { _1.role != 'system' }
|
842
|
-
end
|
843
|
-
|
844
|
-
def copy_to_clipboard(messages)
|
845
|
-
if message = messages.last and message.role == 'assistant'
|
846
|
-
copy = `which #{$config.copy}`.chomp
|
847
|
-
if copy.present?
|
848
|
-
IO.popen(copy, 'w') do |clipboard|
|
849
|
-
clipboard.write(message.content)
|
850
|
-
end
|
851
|
-
STDOUT.puts "The last response has been copied to the system clipboard."
|
852
|
-
else
|
853
|
-
STDERR.puts "#{$config.copy.inspect} command not found in system's path!"
|
854
|
-
end
|
855
|
-
else
|
856
|
-
STDERR.puts "No response available to copy to the system clipboard."
|
857
|
-
end
|
858
|
-
end
|
859
|
-
|
860
|
-
def display_chat_help
|
861
|
-
puts <<~EOT
|
862
|
-
/copy to copy last response to clipboard
|
863
|
-
/paste to paste content
|
864
|
-
/markdown toggle markdown output
|
865
|
-
/stream toggle stream output
|
866
|
-
/location toggle location submission
|
867
|
-
/voice( change) toggle voice output or change the voice
|
868
|
-
/list [n] list the last n / all conversation exchanges
|
869
|
-
/clear clear the whole conversation
|
870
|
-
/clobber clear the conversation and collection
|
871
|
-
/pop [n] pop the last n exchanges, defaults to 1
|
872
|
-
/model change the model
|
873
|
-
/system change system prompt (clears conversation)
|
874
|
-
/regenerate the last answer message
|
875
|
-
/collection( clear|change) change (default) collection or clear
|
876
|
-
/info show information for current session
|
877
|
-
/document_policy pick a scan policy for document references
|
878
|
-
/import source import the source's content
|
879
|
-
/summarize [n] source summarize the source's content in n words
|
880
|
-
/embedding toggle embedding paused or not
|
881
|
-
/embed source embed the source's content
|
882
|
-
/web [n] query query web search & return n or 1 results
|
883
|
-
/links( clear) display (or clear) links used in the chat
|
884
|
-
/save filename store conversation messages
|
885
|
-
/load filename load conversation messages
|
886
|
-
/quit to quit
|
887
|
-
/help to view this help
|
888
|
-
EOT
|
889
|
-
end
|
890
|
-
|
891
|
-
def usage
|
892
|
-
puts <<~EOT
|
893
|
-
Usage: #{File.basename($0)} [OPTIONS]
|
894
|
-
|
895
|
-
-f CONFIG config file to read
|
896
|
-
-u URL the ollama base url, OLLAMA_URL
|
897
|
-
-m MODEL the ollama model to chat with, OLLAMA_CHAT_MODEL
|
898
|
-
-s SYSTEM the system prompt to use as a file, OLLAMA_CHAT_SYSTEM
|
899
|
-
-c CHAT a saved chat conversation to load
|
900
|
-
-C COLLECTION name of the collection used in this conversation
|
901
|
-
-D DOCUMENT load document and add to embeddings collection (multiple)
|
902
|
-
-M use (empty) MemoryCache for this chat session
|
903
|
-
-E disable embeddings for this chat session
|
904
|
-
-V display the current version number and quit
|
905
|
-
-h this help
|
906
|
-
|
907
|
-
EOT
|
908
|
-
exit 0
|
909
|
-
end
|
910
|
-
|
911
|
-
def version
|
912
|
-
puts "%s %s" % [ File.basename($0), Ollama::VERSION ]
|
913
|
-
exit 0
|
914
|
-
end
|
915
|
-
|
916
|
-
def ollama
|
917
|
-
$ollama
|
918
|
-
end
|
919
|
-
|
920
|
-
$opts = go 'f:u:m:s:c:C:D:MEVh'
|
921
|
-
|
922
|
-
$ollama_chat_config = OllamaChatConfig.new($opts[?f])
|
923
|
-
$config = $ollama_chat_config.config
|
924
|
-
|
925
|
-
setup_switches
|
926
|
-
|
927
|
-
$opts[?h] and usage
|
928
|
-
$opts[?V] and version
|
929
|
-
|
930
|
-
base_url = $opts[?u] || $config.url
|
931
|
-
user_agent = [ File.basename($0), Ollama::VERSION ] * ?/
|
932
|
-
$ollama = Client.new(base_url:, debug: $config.debug, user_agent:)
|
933
|
-
|
934
|
-
$document_policy = $config.document_policy
|
935
|
-
$model = choose_model($opts[?m], $config.model.name)
|
936
|
-
$model_options = Options[$config.model.options]
|
937
|
-
model_system = pull_model_unless_present($model, $model_options)
|
938
|
-
messages = []
|
939
|
-
$embedding_enabled.set($config.embedding.enabled && !$opts[?E])
|
940
|
-
|
941
|
-
if $opts[?c]
|
942
|
-
messages.concat load_conversation($opts[?c])
|
943
|
-
else
|
944
|
-
default = $config.system_prompts.default? || model_system
|
945
|
-
if $opts[?s] =~ /\A\?/
|
946
|
-
change_system_prompt(messages, default, system: $opts[?s])
|
947
|
-
else
|
948
|
-
system = Ollama::Utils::FileArgument.get_file_argument($opts[?s], default:)
|
949
|
-
system.present? and set_system_prompt(messages, system)
|
950
|
-
end
|
951
|
-
end
|
952
|
-
|
953
|
-
if $embedding.on?
|
954
|
-
$embedding_model = $config.embedding.model.name
|
955
|
-
$embedding_model_options = Options[$config.embedding.model.options]
|
956
|
-
pull_model_unless_present($embedding_model, $embedding_model_options)
|
957
|
-
collection = $opts[?C] || $config.embedding.collection
|
958
|
-
$documents = Documentrix::Documents.new(
|
959
|
-
ollama:,
|
960
|
-
model: $embedding_model,
|
961
|
-
model_options: $config.embedding.model.options,
|
962
|
-
database_filename: $config.embedding.database_filename || $ollama_chat_config.database_path,
|
963
|
-
collection: ,
|
964
|
-
cache: configure_cache,
|
965
|
-
redis_url: $config.redis.documents.url?,
|
966
|
-
debug: $config.debug
|
967
|
-
)
|
968
|
-
|
969
|
-
document_list = $opts[?D].to_a
|
970
|
-
if document_list.any?(&:empty?)
|
971
|
-
puts "Clearing collection #{bold{collection}}."
|
972
|
-
$documents.clear
|
973
|
-
document_list.reject!(&:empty?)
|
974
|
-
end
|
975
|
-
unless document_list.empty?
|
976
|
-
document_list.map! do |doc|
|
977
|
-
if doc =~ %r(\Ahttps?://)
|
978
|
-
doc
|
979
|
-
else
|
980
|
-
File.expand_path(doc)
|
981
|
-
end
|
982
|
-
end
|
983
|
-
puts "Collection #{bold{collection}}: Adding #{document_list.size} documents…"
|
984
|
-
count = 1
|
985
|
-
document_list.each_slice(25) do |docs|
|
986
|
-
docs.each do |doc|
|
987
|
-
fetch_source(doc) do |doc_io|
|
988
|
-
embed_source(doc_io, doc, count:)
|
989
|
-
end
|
990
|
-
count += 1
|
991
|
-
end
|
992
|
-
end
|
993
|
-
end
|
994
|
-
else
|
995
|
-
$documents = Tins::NULL
|
996
|
-
end
|
997
|
-
|
998
|
-
if redis_expiring_url = $config.redis.expiring.url?
|
999
|
-
$cache = Documentrix::Documents::RedisCache.new(
|
1000
|
-
prefix: 'Expiring-',
|
1001
|
-
url: redis_expiring_url,
|
1002
|
-
ex: $config.redis.expiring.ex,
|
1003
|
-
)
|
1004
|
-
end
|
1005
|
-
|
1006
|
-
$current_voice = $config.voice.default
|
1007
|
-
|
1008
|
-
puts "Configuration read from #{$ollama_chat_config.filename.inspect} is:", $config
|
1009
|
-
info
|
1010
|
-
puts "\nType /help to display the chat help."
|
1011
|
-
|
1012
|
-
$links = Set.new
|
1013
|
-
images = []
|
1014
|
-
loop do
|
1015
|
-
parse_content = true
|
1016
|
-
input_prompt = bold { color(172) { message_type(images) + " user" } } + bold { "> " }
|
1017
|
-
content = Reline.readline(input_prompt, true)&.chomp
|
1018
|
-
|
1019
|
-
case content
|
1020
|
-
when %r(^/copy$)
|
1021
|
-
copy_to_clipboard(messages)
|
1022
|
-
next
|
1023
|
-
when %r(^/paste$)
|
1024
|
-
puts bold { "Paste your content and then press C-d!" }
|
1025
|
-
content = STDIN.read
|
1026
|
-
when %r(^/markdown$)
|
1027
|
-
$markdown.toggle
|
1028
|
-
next
|
1029
|
-
when %r(^/stream$)
|
1030
|
-
$stream.toggle
|
1031
|
-
next
|
1032
|
-
when %r(^/location$)
|
1033
|
-
$location.toggle
|
1034
|
-
next
|
1035
|
-
when %r(^/voice(?:\s+(change))?$)
|
1036
|
-
if $1 == 'change'
|
1037
|
-
change_voice
|
1038
|
-
else
|
1039
|
-
$voice.toggle
|
1040
|
-
end
|
1041
|
-
next
|
1042
|
-
when %r(^/list(?:\s+(\d*))?$)
|
1043
|
-
last = if $1
|
1044
|
-
2 * $1.to_i
|
1045
|
-
end
|
1046
|
-
list_conversation(messages, last)
|
1047
|
-
next
|
1048
|
-
when %r(^/clear$)
|
1049
|
-
clear_messages(messages)
|
1050
|
-
puts "Cleared messages."
|
1051
|
-
next
|
1052
|
-
when %r(^/clobber$)
|
1053
|
-
if ask?(prompt: 'Are you sure to clear messages and collection? (y/n) ') =~ /\Ay/i
|
1054
|
-
clear_messages(messages)
|
1055
|
-
$documents.clear
|
1056
|
-
puts "Cleared messages and collection #{bold{$documents.collection}}."
|
1057
|
-
else
|
1058
|
-
puts 'Cancelled.'
|
1059
|
-
end
|
1060
|
-
next
|
1061
|
-
when %r(^/pop(?:\s+(\d*))?$)
|
1062
|
-
if messages.size > 1
|
1063
|
-
n = $1.to_i.clamp(1, Float::INFINITY)
|
1064
|
-
r = messages.pop(2 * n)
|
1065
|
-
m = r.size / 2
|
1066
|
-
puts "Popped the last #{m} exchanges."
|
1067
|
-
else
|
1068
|
-
puts "No more exchanges you can pop."
|
1069
|
-
end
|
1070
|
-
list_conversation(messages, 2)
|
1071
|
-
next
|
1072
|
-
when %r(^/model$)
|
1073
|
-
$model = choose_model('', $model)
|
1074
|
-
next
|
1075
|
-
when %r(^/system$)
|
1076
|
-
change_system_prompt(messages, $system)
|
1077
|
-
info
|
1078
|
-
next
|
1079
|
-
when %r(^/regenerate$)
|
1080
|
-
if content = messages[-2]&.content
|
1081
|
-
content.gsub!(/\nConsider these chunks for your answer.*\z/, '')
|
1082
|
-
messages.pop(2)
|
1083
|
-
else
|
1084
|
-
puts "Not enough messages in this conversation."
|
1085
|
-
redo
|
1086
|
-
end
|
1087
|
-
parse_content = false
|
1088
|
-
content
|
1089
|
-
when %r(^/collection(?:\s+(clear|change))?$)
|
1090
|
-
case $1 || 'change'
|
1091
|
-
when 'clear'
|
1092
|
-
loop do
|
1093
|
-
tags = $documents.tags.add('[EXIT]').add('[ALL]')
|
1094
|
-
tag = Utils::Chooser.choose(tags, prompt: 'Clear? %s')
|
1095
|
-
case tag
|
1096
|
-
when nil, '[EXIT]'
|
1097
|
-
puts "Exiting chooser."
|
1098
|
-
break
|
1099
|
-
when '[ALL]'
|
1100
|
-
if ask?(prompt: 'Are you sure? (y/n) ') =~ /\Ay/i
|
1101
|
-
$documents.clear
|
1102
|
-
puts "Cleared collection #{bold{$documents.collection}}."
|
1103
|
-
break
|
1104
|
-
else
|
1105
|
-
puts 'Cancelled.'
|
1106
|
-
sleep 3
|
1107
|
-
end
|
1108
|
-
when /./
|
1109
|
-
$documents.clear(tags: [ tag ])
|
1110
|
-
puts "Cleared tag #{tag} from collection #{bold{$documents.collection}}."
|
1111
|
-
sleep 3
|
1112
|
-
end
|
1113
|
-
end
|
1114
|
-
when 'change'
|
1115
|
-
choose_collection($documents.collection)
|
1116
|
-
end
|
1117
|
-
next
|
1118
|
-
when %r(^/info$)
|
1119
|
-
info
|
1120
|
-
next
|
1121
|
-
when %r(^/document_policy$)
|
1122
|
-
choose_document_policy
|
1123
|
-
next
|
1124
|
-
when %r(^/import\s+(.+))
|
1125
|
-
parse_content = false
|
1126
|
-
content = import($1) or next
|
1127
|
-
when %r(^/summarize\s+(?:(\d+)\s+)?(.+))
|
1128
|
-
parse_content = false
|
1129
|
-
content = summarize($2, words: $1) or next
|
1130
|
-
when %r(^/embedding$)
|
1131
|
-
$embedding_paused.toggle(show: false)
|
1132
|
-
$embedding.show
|
1133
|
-
next
|
1134
|
-
when %r(^/embed\s+(.+))
|
1135
|
-
parse_content = false
|
1136
|
-
content = embed($1) or next
|
1137
|
-
when %r(^/web\s+(?:(\d+)\s+)?(.+))
|
1138
|
-
parse_content = false
|
1139
|
-
urls = search_web($2, $1.to_i)
|
1140
|
-
urls.each do |url|
|
1141
|
-
fetch_source(url) { |url_io| embed_source(url_io, url) }
|
1142
|
-
end
|
1143
|
-
urls_summarized = urls.map { summarize(_1) }
|
1144
|
-
query = $2.inspect
|
1145
|
-
results = urls.zip(urls_summarized).
|
1146
|
-
map { |u, s| "%s as \n:%s" % [ u, s ] } * "\n\n"
|
1147
|
-
content = $config.prompts.web % { query:, results: }
|
1148
|
-
when %r(^/save\s+(.+)$)
|
1149
|
-
save_conversation($1, messages)
|
1150
|
-
puts "Saved conversation to #$1."
|
1151
|
-
next
|
1152
|
-
when %r(^/links(?:\s+(clear))?$)
|
1153
|
-
case $1
|
1154
|
-
when 'clear'
|
1155
|
-
loop do
|
1156
|
-
links = $links.dup.add('[EXIT]').add('[ALL]')
|
1157
|
-
link = Utils::Chooser.choose(links, prompt: 'Clear? %s')
|
1158
|
-
case link
|
1159
|
-
when nil, '[EXIT]'
|
1160
|
-
puts "Exiting chooser."
|
1161
|
-
break
|
1162
|
-
when '[ALL]'
|
1163
|
-
if ask?(prompt: 'Are you sure? (y/n) ') =~ /\Ay/i
|
1164
|
-
$links.clear
|
1165
|
-
puts "Cleared all links in list."
|
1166
|
-
break
|
1167
|
-
else
|
1168
|
-
puts 'Cancelled.'
|
1169
|
-
sleep 3
|
1170
|
-
end
|
1171
|
-
when /./
|
1172
|
-
$links.delete(link)
|
1173
|
-
puts "Cleared link from links in list."
|
1174
|
-
sleep 3
|
1175
|
-
end
|
1176
|
-
end
|
1177
|
-
when nil
|
1178
|
-
if $links.empty?
|
1179
|
-
puts "List is empty."
|
1180
|
-
else
|
1181
|
-
Math.log10($links.size).ceil
|
1182
|
-
format = "% #{}s. %s"
|
1183
|
-
connect = -> link { hyperlink(link) { link } }
|
1184
|
-
puts $links.each_with_index.map { |x, i| format % [ i + 1, connect.(x) ] }
|
1185
|
-
end
|
1186
|
-
end
|
1187
|
-
next
|
1188
|
-
when %r(^/load\s+(.+)$)
|
1189
|
-
messages = load_conversation($1)
|
1190
|
-
puts "Loaded conversation from #$1."
|
1191
|
-
next
|
1192
|
-
when %r(^/quit$)
|
1193
|
-
puts "Goodbye."
|
1194
|
-
exit 0
|
1195
|
-
when %r(^/)
|
1196
|
-
display_chat_help
|
1197
|
-
next
|
1198
|
-
when ''
|
1199
|
-
puts "Type /quit to quit."
|
1200
|
-
next
|
1201
|
-
when nil
|
1202
|
-
puts "Goodbye."
|
1203
|
-
exit 0
|
1204
|
-
end
|
1205
|
-
|
1206
|
-
content, tags = if parse_content
|
1207
|
-
parse_content(content, images)
|
1208
|
-
else
|
1209
|
-
[ content, Documentrix::Utils::Tags.new ]
|
1210
|
-
end
|
1211
|
-
|
1212
|
-
if $embedding.on? && content
|
1213
|
-
records = $documents.find_where(
|
1214
|
-
content.downcase,
|
1215
|
-
tags:,
|
1216
|
-
prompt: $config.embedding.model.prompt?,
|
1217
|
-
text_size: $config.embedding.found_texts_size?,
|
1218
|
-
text_count: $config.embedding.found_texts_count?,
|
1219
|
-
)
|
1220
|
-
unless records.empty?
|
1221
|
-
content += "\nConsider these chunks for your answer:\n\n"\
|
1222
|
-
"#{records.map { [ _1.text, _1.tags_set ] * ?\n }.join("\n\n---\n\n")}"
|
1223
|
-
end
|
1224
|
-
end
|
1225
|
-
|
1226
|
-
if location = at_location.full?
|
1227
|
-
content += " [#{location} – do not comment on this the time and location, "\
|
1228
|
-
"just consider it for eventual queries]"
|
1229
|
-
end
|
1230
|
-
|
1231
|
-
messages << Message.new(role: 'user', content:, images: images.dup)
|
1232
|
-
images.clear
|
1233
|
-
handler = FollowChat.new(messages:, markdown: $markdown.on?, voice: ($current_voice if $voice.on?))
|
1234
|
-
ollama.chat(model: $model, messages:, options: $model_options, stream: $stream.on?, &handler)
|
1235
|
-
|
1236
|
-
if $embedding.on? && !records.empty?
|
1237
|
-
puts "", records.map { |record|
|
1238
|
-
link = if record.source =~ %r(\Ahttps?://)
|
1239
|
-
record.source
|
1240
|
-
else
|
1241
|
-
'file://%s' % File.expand_path(record.source)
|
1242
|
-
end
|
1243
|
-
[ link, record.tags.first ]
|
1244
|
-
}.uniq.map { |l, t| hyperlink(l, t) }.join(' ')
|
1245
|
-
$config.debug and jj messages
|
1246
|
-
end
|
1247
|
-
rescue Interrupt
|
1248
|
-
puts "Type /quit to quit."
|
1249
|
-
end
|