ollama-ruby 0.0.1 → 0.2.0
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/.envrc +1 -0
- data/CHANGES.md +78 -0
- data/README.md +62 -23
- data/Rakefile +16 -4
- data/bin/ollama_chat +470 -90
- data/bin/ollama_console +3 -3
- data/bin/ollama_update +17 -0
- data/config/redis.conf +5 -0
- data/docker-compose.yml +11 -0
- data/lib/ollama/client.rb +7 -2
- data/lib/ollama/documents/memory_cache.rb +44 -0
- data/lib/ollama/documents/redis_cache.rb +57 -0
- data/lib/ollama/documents/splitters/character.rb +70 -0
- data/lib/ollama/documents/splitters/semantic.rb +90 -0
- data/lib/ollama/documents.rb +172 -0
- data/lib/ollama/dto.rb +4 -7
- data/lib/ollama/handlers/progress.rb +18 -5
- data/lib/ollama/image.rb +16 -7
- data/lib/ollama/options.rb +4 -0
- data/lib/ollama/utils/chooser.rb +30 -0
- data/lib/ollama/utils/colorize_texts.rb +42 -0
- data/lib/ollama/utils/fetcher.rb +105 -0
- data/lib/ollama/utils/math.rb +48 -0
- data/lib/ollama/utils/tags.rb +7 -0
- data/lib/ollama/utils/width.rb +1 -1
- data/lib/ollama/version.rb +1 -1
- data/lib/ollama.rb +12 -5
- data/ollama-ruby.gemspec +19 -9
- data/spec/assets/embeddings.json +1 -0
- data/spec/ollama/client_spec.rb +2 -2
- data/spec/ollama/commands/chat_spec.rb +2 -2
- data/spec/ollama/commands/copy_spec.rb +2 -2
- data/spec/ollama/commands/create_spec.rb +2 -2
- data/spec/ollama/commands/delete_spec.rb +2 -2
- data/spec/ollama/commands/embed_spec.rb +3 -3
- data/spec/ollama/commands/embeddings_spec.rb +2 -2
- data/spec/ollama/commands/generate_spec.rb +2 -2
- data/spec/ollama/commands/pull_spec.rb +2 -2
- data/spec/ollama/commands/push_spec.rb +2 -2
- data/spec/ollama/commands/show_spec.rb +2 -2
- data/spec/ollama/documents/memory_cache_spec.rb +63 -0
- data/spec/ollama/documents/redis_cache_spec.rb +78 -0
- data/spec/ollama/documents/splitters/character_spec.rb +96 -0
- data/spec/ollama/documents/splitters/semantic_spec.rb +56 -0
- data/spec/ollama/documents_spec.rb +119 -0
- data/spec/ollama/handlers/progress_spec.rb +2 -2
- data/spec/ollama/image_spec.rb +4 -0
- data/spec/ollama/message_spec.rb +3 -4
- data/spec/ollama/options_spec.rb +18 -0
- data/spec/ollama/tool_spec.rb +1 -6
- data/spec/ollama/utils/fetcher_spec.rb +74 -0
- data/spec/ollama/utils/tags_spec.rb +24 -0
- data/spec/spec_helper.rb +8 -0
- data/tmp/.keep +0 -0
- metadata +187 -5
data/bin/ollama_chat
CHANGED
@@ -4,9 +4,80 @@ require 'ollama'
|
|
4
4
|
include Ollama
|
5
5
|
require 'term/ansicolor'
|
6
6
|
include Term::ANSIColor
|
7
|
-
require 'tins
|
7
|
+
require 'tins'
|
8
8
|
include Tins::GO
|
9
9
|
require 'reline'
|
10
|
+
require 'reverse_markdown'
|
11
|
+
require 'complex_config'
|
12
|
+
require 'fileutils'
|
13
|
+
require 'uri'
|
14
|
+
require 'nokogiri'
|
15
|
+
|
16
|
+
class OllamaChatConfig
|
17
|
+
include ComplexConfig
|
18
|
+
include FileUtils
|
19
|
+
|
20
|
+
DEFAULT_CONFIG = <<~EOT
|
21
|
+
---
|
22
|
+
url: <%= ENV['OLLAMA_URL'] || 'http://%s' % ENV.fetch('OLLAMA_HOST') %>
|
23
|
+
model:
|
24
|
+
name: <%= ENV.fetch('OLLAMA_CHAT_MODEL', 'llama3.1') %>
|
25
|
+
options:
|
26
|
+
num_ctx: 8192
|
27
|
+
system: <%= ENV.fetch('OLLAMA_CHAT_SYSTEM', 'null') %>
|
28
|
+
voice: Samantha
|
29
|
+
markdown: true
|
30
|
+
embedding:
|
31
|
+
enabled: true
|
32
|
+
model:
|
33
|
+
name: mxbai-embed-large
|
34
|
+
options: {}
|
35
|
+
# Retrieval prompt template:
|
36
|
+
prompt: 'Represent this sentence for searching relevant passages: %s'
|
37
|
+
collection: <%= ENV.fetch('OLLAMA_CHAT_COLLECTION', 'ollama_chat') %>
|
38
|
+
found_texts_size: 4096
|
39
|
+
splitter:
|
40
|
+
name: RecursiveCharacter
|
41
|
+
chunk_size: 1024
|
42
|
+
cache: Ollama::Documents::RedisCache
|
43
|
+
redis:
|
44
|
+
url: <%= ENV.fetch('REDIS_URL', 'null') %>
|
45
|
+
debug: <%= ENV['OLLAMA_CHAT_DEBUG'].to_i == 1 ? true : false %>
|
46
|
+
EOT
|
47
|
+
|
48
|
+
def initialize(filename = nil)
|
49
|
+
@filename = filename || default_path
|
50
|
+
@config = Provider.config(@filename)
|
51
|
+
retried = false
|
52
|
+
rescue ConfigurationFileMissing
|
53
|
+
if @filename == default_path && !retried
|
54
|
+
retried = true
|
55
|
+
mkdir_p File.dirname(default_path)
|
56
|
+
File.secure_write(default_path, DEFAULT_CONFIG)
|
57
|
+
retry
|
58
|
+
else
|
59
|
+
raise
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
attr_reader :filename
|
64
|
+
|
65
|
+
attr_reader :config
|
66
|
+
|
67
|
+
def default_path
|
68
|
+
File.join(config_dir_path, 'config.yml')
|
69
|
+
end
|
70
|
+
|
71
|
+
def config_dir_path
|
72
|
+
File.join(
|
73
|
+
ENV.fetch(
|
74
|
+
'XDG_CONFIG_HOME',
|
75
|
+
File.join(ENV.fetch('HOME'), '.config')
|
76
|
+
),
|
77
|
+
'ollama_chat'
|
78
|
+
)
|
79
|
+
end
|
80
|
+
end
|
10
81
|
|
11
82
|
class FollowChat
|
12
83
|
include Ollama::Handlers::Concern
|
@@ -16,16 +87,16 @@ class FollowChat
|
|
16
87
|
super(output:)
|
17
88
|
@output.sync = true
|
18
89
|
@markdown = markdown
|
19
|
-
@say = voice ?
|
90
|
+
@say = voice ? Handlers::Say.new(voice:) : NOP
|
20
91
|
@messages = messages
|
21
92
|
@user = nil
|
22
93
|
end
|
23
94
|
|
24
95
|
def call(response)
|
25
|
-
|
96
|
+
$config.debug and jj response
|
26
97
|
if response&.message&.role == 'assistant'
|
27
98
|
if @messages.last.role != 'assistant'
|
28
|
-
@messages <<
|
99
|
+
@messages << Message.new(role: 'assistant', content: '')
|
29
100
|
@user = message_type(@messages.last.images) + " " +
|
30
101
|
bold { color(111) { 'assistant:' } }
|
31
102
|
puts @user unless @markdown
|
@@ -33,49 +104,75 @@ class FollowChat
|
|
33
104
|
content = response.message&.content
|
34
105
|
@messages.last.content << content
|
35
106
|
if @markdown and @messages.last.content.present?
|
36
|
-
markdown_content =
|
107
|
+
markdown_content = Utils::ANSIMarkdown.parse(@messages.last.content)
|
37
108
|
@output.print clear_screen, move_home, @user, ?\n, markdown_content
|
38
109
|
else
|
39
110
|
@output.print content
|
40
111
|
end
|
41
112
|
@say.call(response)
|
42
113
|
end
|
43
|
-
response.done
|
114
|
+
if response.done
|
115
|
+
@output.puts
|
116
|
+
eval_stats = {
|
117
|
+
eval_duration: Tins::Duration.new(response.eval_duration / 1e9),
|
118
|
+
eval_count: response.eval_count,
|
119
|
+
prompt_eval_duration: Tins::Duration.new(response.prompt_eval_duration / 1e9),
|
120
|
+
prompt_eval_count: response.prompt_eval_count,
|
121
|
+
total_duration: Tins::Duration.new(response.total_duration / 1e9),
|
122
|
+
load_duration: Tins::Duration.new(response.load_duration / 1e9),
|
123
|
+
}.map { _1 * '=' } * ' '
|
124
|
+
@output.puts '📊 ' + color(111) { Utils::Width.wrap(eval_stats, percentage: 90) }
|
125
|
+
end
|
44
126
|
self
|
45
127
|
end
|
46
128
|
end
|
47
129
|
|
48
|
-
def
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
if
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
130
|
+
def search_web(query, n = 5)
|
131
|
+
query = URI.encode_uri_component(query)
|
132
|
+
url = "https://www.duckduckgo.com/html/?q=#{query}"
|
133
|
+
Ollama::Utils::Fetcher.new.get(url) do |tmp|
|
134
|
+
result = []
|
135
|
+
doc = Nokogiri::HTML(tmp)
|
136
|
+
doc.css('.results_links').each do |link|
|
137
|
+
if n > 0
|
138
|
+
url = link.css('.result__a').first&.[]('href')
|
139
|
+
url.sub!(%r(\A/l/\?uddg=), '')
|
140
|
+
url.sub!(%r(&rut=.*), '')
|
141
|
+
url = URI.decode_uri_component(url)
|
142
|
+
url = URI.parse(url)
|
143
|
+
url.host =~ /duckduckgo\.com/ and next
|
144
|
+
result << url
|
145
|
+
n -= 1
|
62
146
|
else
|
63
|
-
|
147
|
+
break
|
64
148
|
end
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
149
|
+
end
|
150
|
+
result
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def pull_model_unless_present(model, options, retried = false)
|
155
|
+
ollama.show(name: model) { |response|
|
156
|
+
puts "Model #{bold{model}} with architecture #{response.model_info['general.architecture']} found."
|
157
|
+
if system = response.system
|
158
|
+
puts "Configured model system prompt is:\n#{italic { system }}"
|
159
|
+
return system
|
71
160
|
else
|
72
|
-
|
73
|
-
retry
|
161
|
+
return
|
74
162
|
end
|
75
|
-
|
76
|
-
|
163
|
+
}
|
164
|
+
rescue Errors::NotFoundError
|
165
|
+
puts "Model #{bold{model}} not found, attempting to pull it now…"
|
166
|
+
ollama.pull(name: model)
|
167
|
+
if retried
|
77
168
|
exit 1
|
169
|
+
else
|
170
|
+
retried = true
|
171
|
+
retry
|
78
172
|
end
|
173
|
+
rescue Errors::Error => e
|
174
|
+
warn "Caught #{e.class}: #{e} => Exiting."
|
175
|
+
exit 1
|
79
176
|
end
|
80
177
|
|
81
178
|
def load_conversation(filename)
|
@@ -84,7 +181,7 @@ def load_conversation(filename)
|
|
84
181
|
return
|
85
182
|
end
|
86
183
|
File.open(filename, 'r') do |output|
|
87
|
-
return JSON(output.read
|
184
|
+
return JSON(output.read).map { Ollama::Message.from_hash(_1) }
|
88
185
|
end
|
89
186
|
end
|
90
187
|
|
@@ -115,27 +212,198 @@ def list_conversation(messages, markdown)
|
|
115
212
|
else 210
|
116
213
|
end
|
117
214
|
content = if markdown && m.content.present?
|
118
|
-
|
215
|
+
Utils::ANSIMarkdown.parse(m.content)
|
119
216
|
else
|
120
217
|
m.content
|
121
218
|
end
|
122
|
-
|
123
|
-
|
219
|
+
message_text = message_type(m.images) + " "
|
220
|
+
message_text += bold { color(role_color) { m.role } }
|
221
|
+
message_text += ":\n#{content}"
|
222
|
+
if m.images.present?
|
223
|
+
message_text += "\nImages: " + italic { m.images.map(&:path) * ', ' }
|
224
|
+
end
|
225
|
+
puts message_text
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def parse_source(source_io)
|
230
|
+
case source_io&.content_type&.sub_type
|
231
|
+
when 'html'
|
232
|
+
ReverseMarkdown.convert(
|
233
|
+
source_io.read,
|
234
|
+
unknown_tags: :bypass,
|
235
|
+
github_flavored: true,
|
236
|
+
tag_border: ''
|
237
|
+
)
|
238
|
+
when 'plain', 'csv', 'xml'
|
239
|
+
source_io.read
|
240
|
+
else
|
241
|
+
STDERR.puts "Cannot import #{source_io&.content_type} document."
|
242
|
+
return
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def import_document(source_io, source)
|
247
|
+
unless $config.embedding.enabled
|
248
|
+
STDOUT.puts "Embedding disabled, I won't import any documents, try: /summarize"
|
249
|
+
return
|
250
|
+
end
|
251
|
+
infobar.puts "Importing #{italic { source_io.content_type }} document #{source.to_s.inspect}."
|
252
|
+
text = parse_source(source_io) or return
|
253
|
+
text.downcase!
|
254
|
+
splitter_config = $config.embedding.splitter
|
255
|
+
inputs = case splitter_config.name
|
256
|
+
when 'Character'
|
257
|
+
Ollama::Documents::Splitters::Character.new(
|
258
|
+
chunk_size: splitter_config.chunk_size,
|
259
|
+
).split(text)
|
260
|
+
when 'RecursiveCharacter'
|
261
|
+
Ollama::Documents::Splitters::RecursiveCharacter.new(
|
262
|
+
chunk_size: splitter_config.chunk_size,
|
263
|
+
).split(text)
|
264
|
+
when 'Semantic'
|
265
|
+
Ollama::Documents::Splitters::Semantic.new(
|
266
|
+
ollama:, model: $config.embedding.model.name,
|
267
|
+
chunk_size: splitter_config.chunk_size,
|
268
|
+
).split(
|
269
|
+
text,
|
270
|
+
breakpoint: splitter_config.breakpoint.to_sym,
|
271
|
+
percentage: splitter_config.percentage?,
|
272
|
+
percentile: splitter_config.percentile?,
|
273
|
+
)
|
274
|
+
end
|
275
|
+
$documents.add(inputs, source: source.to_s)
|
276
|
+
end
|
277
|
+
|
278
|
+
def add_image(images, source_io, source)
|
279
|
+
STDERR.puts "Adding #{source_io.content_type} image #{source.to_s.inspect}."
|
280
|
+
image = Image.for_io(source_io, path: source.to_s)
|
281
|
+
(images << image).uniq!
|
282
|
+
end
|
283
|
+
|
284
|
+
def fetch_source(source, &block)
|
285
|
+
case source
|
286
|
+
when %r(\Ahttps?://\S+)
|
287
|
+
Utils::Fetcher.get(source) do |tmp|
|
288
|
+
block.(tmp)
|
289
|
+
end
|
290
|
+
when %r(\Afile://(?:(?:[.-]|[[:alnum:]])*)(/\S*)|([~.]?/\S*))
|
291
|
+
filename = $~.captures.compact.first
|
292
|
+
filename = File.expand_path(filename)
|
293
|
+
Utils::Fetcher.read(filename) do |tmp|
|
294
|
+
block.(tmp)
|
295
|
+
end
|
296
|
+
else
|
297
|
+
raise "invalid source"
|
298
|
+
end
|
299
|
+
rescue => e
|
300
|
+
STDERR.puts "Cannot add source #{source.to_s.inspect}: #{e}\n#{e.backtrace * ?\n}"
|
301
|
+
end
|
302
|
+
|
303
|
+
def summarize(source)
|
304
|
+
puts "Now summarizing #{source.inspect}."
|
305
|
+
source_content =
|
306
|
+
fetch_source(source) do |source_io|
|
307
|
+
parse_source(source_io) or return
|
308
|
+
end
|
309
|
+
<<~end
|
310
|
+
# Generate an abstract summary of the content in this document:
|
311
|
+
|
312
|
+
#{source_content}
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
def parse_content(content, images)
|
317
|
+
images.clear
|
318
|
+
tags = Utils::Tags.new
|
319
|
+
|
320
|
+
content.scan(%r([.~]?/\S+|https?://\S+|#\S+)).each do |source|
|
321
|
+
case source
|
322
|
+
when /\A#(\S+)/
|
323
|
+
tags << $1
|
324
|
+
else
|
325
|
+
source = source.sub(/(["')]|\*+)\z/, '')
|
326
|
+
fetch_source(source) do |source_io|
|
327
|
+
case source_io&.content_type&.media_type
|
328
|
+
when 'image'
|
329
|
+
add_image(images, source_io, source)
|
330
|
+
when 'text'
|
331
|
+
import_document(source_io, source)
|
332
|
+
else
|
333
|
+
STDERR.puts(
|
334
|
+
"Cannot fetch #{source.to_s.inspect} with content type "\
|
335
|
+
"#{source_io&.content_type.inspect}"
|
336
|
+
)
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
return content, (tags unless tags.empty?)
|
343
|
+
end
|
344
|
+
|
345
|
+
def choose_model(cli_model, default_model)
|
346
|
+
models = ollama.tags.models.map(&:name).sort
|
347
|
+
model = if cli_model == ''
|
348
|
+
Ollama::Utils::Chooser.choose(models) || default_model
|
349
|
+
else
|
350
|
+
cli_model || default_model
|
351
|
+
end
|
352
|
+
ensure
|
353
|
+
puts green { "Connecting to #{model}@#{ollama.base_url} now…" }
|
354
|
+
end
|
355
|
+
|
356
|
+
def choose_collection(default_collection)
|
357
|
+
collections = [ default_collection ] + $documents.collections
|
358
|
+
collections = collections.uniq.sort
|
359
|
+
$documents.collection = collection =
|
360
|
+
Ollama::Utils::Chooser.choose(collections) || default_collection
|
361
|
+
ensure
|
362
|
+
puts "Changing to collection #{bold{collection}}."
|
363
|
+
collection_stats
|
364
|
+
end
|
365
|
+
|
366
|
+
def collection_stats
|
367
|
+
puts <<~end
|
368
|
+
Collection
|
369
|
+
Name: #{bold{$documents.collection}}
|
370
|
+
#Embeddings: #{$documents.size}
|
371
|
+
Tags: #{$documents.tags}
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def configure_cache
|
376
|
+
Object.const_get($config.cache)
|
377
|
+
rescue => e
|
378
|
+
STDERR.puts "Caught #{e.class}: #{e} => Falling back to MemoryCache."
|
379
|
+
Ollama::Documents::MemoryCache
|
380
|
+
end
|
381
|
+
|
382
|
+
def set_markdown(value)
|
383
|
+
if value
|
384
|
+
puts "Using ANSI markdown to output content."
|
385
|
+
true
|
386
|
+
else
|
387
|
+
puts "Using plaintext for outputting content."
|
388
|
+
false
|
124
389
|
end
|
125
390
|
end
|
126
391
|
|
127
392
|
def display_chat_help
|
128
393
|
puts <<~end
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
394
|
+
/paste to paste content
|
395
|
+
/markdown toggle markdown output
|
396
|
+
/list list the messages of the conversation
|
397
|
+
/clear clear the conversation messages
|
398
|
+
/pop [n] pop the last n exchanges, defaults to 1
|
399
|
+
/model change the model
|
400
|
+
/regenerate the last answer message
|
401
|
+
/collection clear|stats|change|new clear or show stats of current collection
|
402
|
+
/summarize source summarize the URL/file source's content
|
403
|
+
/save filename store conversation messages
|
404
|
+
/load filename load conversation messages
|
405
|
+
/quit to quit
|
406
|
+
/help to view this help
|
139
407
|
end
|
140
408
|
end
|
141
409
|
|
@@ -143,36 +411,88 @@ def usage
|
|
143
411
|
puts <<~end
|
144
412
|
#{File.basename($0)} [OPTIONS]
|
145
413
|
|
146
|
-
-
|
147
|
-
-
|
148
|
-
-
|
149
|
-
-s SYSTEM
|
150
|
-
-c CHAT
|
151
|
-
-
|
152
|
-
-
|
153
|
-
-
|
414
|
+
-f CONFIG config file to read
|
415
|
+
-u URL the ollama base url, OLLAMA_URL
|
416
|
+
-m MODEL the ollama model to chat with, OLLAMA_CHAT_MODEL
|
417
|
+
-s SYSTEM the system prompt to use as a file, OLLAMA_CHAT_SYSTEM
|
418
|
+
-c CHAT a saved chat conversation to load
|
419
|
+
-C COLLECTION name of the collection used in this conversation
|
420
|
+
-D DOCUMENT load document and add to collection (multiple)
|
421
|
+
-v use voice output
|
422
|
+
-h this help
|
154
423
|
|
155
424
|
end
|
156
425
|
exit 0
|
157
426
|
end
|
158
427
|
|
159
|
-
|
428
|
+
def ollama
|
429
|
+
$ollama
|
430
|
+
end
|
431
|
+
|
432
|
+
opts = go 'f:u:m:s:c:C:D:vh'
|
433
|
+
|
434
|
+
config = OllamaChatConfig.new(opts[?f])
|
435
|
+
$config = config.config
|
160
436
|
|
161
437
|
opts[?h] and usage
|
162
438
|
|
163
|
-
|
164
|
-
|
165
|
-
options = if options_file = opts[?M]
|
166
|
-
JSON(File.read(options_file), create_additions: true)
|
167
|
-
end
|
439
|
+
puts "Configuration read from #{config.filename.inspect} is:"
|
440
|
+
y $config.to_h
|
168
441
|
|
169
|
-
|
442
|
+
base_url = opts[?u] || $config.url
|
443
|
+
$ollama = Client.new(base_url:, debug: $config.debug)
|
170
444
|
|
171
|
-
|
445
|
+
model = choose_model(opts[?m], $config.model.name)
|
446
|
+
options = Options[$config.model.options]
|
447
|
+
model_system = pull_model_unless_present(model, options)
|
448
|
+
messages = []
|
172
449
|
|
173
|
-
|
450
|
+
if $config.embedding.enabled
|
451
|
+
embedding_model = $config.embedding.model.name
|
452
|
+
embedding_model_options = Options[$config.embedding.model.options]
|
453
|
+
pull_model_unless_present(embedding_model, embedding_model_options)
|
454
|
+
collection = opts[?C] || $config.embedding.collection
|
455
|
+
$documents = Documents.new(
|
456
|
+
ollama:,
|
457
|
+
model: $config.embedding.model.name,
|
458
|
+
model_options: $config.embedding.model.options,
|
459
|
+
collection:,
|
460
|
+
cache: configure_cache,
|
461
|
+
redis_url: $config.redis.url?,
|
462
|
+
)
|
463
|
+
|
464
|
+
document_list = opts[?D].to_a
|
465
|
+
if document_list.any?(&:empty?)
|
466
|
+
puts "Clearing collection #{bold{collection}}."
|
467
|
+
$documents.clear
|
468
|
+
document_list.reject!(&:empty?)
|
469
|
+
end
|
470
|
+
unless document_list.empty?
|
471
|
+
document_list.map! do |doc|
|
472
|
+
if doc =~ %r(\Ahttps?://)
|
473
|
+
doc
|
474
|
+
else
|
475
|
+
File.expand_path(doc)
|
476
|
+
end
|
477
|
+
end
|
478
|
+
infobar.puts "Collection #{bold{collection}}: Adding #{document_list.size} documents…"
|
479
|
+
document_list.each_slice(25) do |docs|
|
480
|
+
docs.each do |doc|
|
481
|
+
fetch_source(doc) do |doc_io|
|
482
|
+
import_document(doc_io, doc)
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
collection_stats
|
488
|
+
else
|
489
|
+
$documents = Documents.new(ollama:, model:)
|
490
|
+
end
|
174
491
|
|
175
|
-
|
492
|
+
if voice = ($config.voice if opts[?v])
|
493
|
+
puts "Using voice #{bold{voice}} to speak."
|
494
|
+
end
|
495
|
+
markdown = set_markdown($config.markdown)
|
176
496
|
|
177
497
|
if opts[?c]
|
178
498
|
messages.concat load_conversation(opts[?c])
|
@@ -181,7 +501,7 @@ else
|
|
181
501
|
if system_prompt_file = opts[?s]
|
182
502
|
system = File.read(system_prompt_file)
|
183
503
|
end
|
184
|
-
system ||=
|
504
|
+
system ||= $config.system
|
185
505
|
|
186
506
|
if system
|
187
507
|
messages << Message.new(role: 'system', content: system)
|
@@ -191,68 +511,128 @@ else
|
|
191
511
|
end
|
192
512
|
end
|
193
513
|
|
194
|
-
puts "
|
514
|
+
puts "\nType /help to display the chat help."
|
195
515
|
|
196
|
-
images
|
516
|
+
images = []
|
197
517
|
loop do
|
198
|
-
|
199
|
-
|
518
|
+
parse_content = true
|
519
|
+
input_prompt = bold { color(172) { message_type(images) + " user" } } + bold { "> " }
|
520
|
+
content = Reline.readline(input_prompt, true)&.chomp
|
521
|
+
|
522
|
+
case content
|
200
523
|
when %r(^/paste$)
|
201
524
|
puts bold { "Paste your content and then press C-d!" }
|
202
525
|
content = STDIN.read
|
203
526
|
when %r(^/quit$)
|
204
527
|
puts "Goodbye."
|
205
528
|
exit 0
|
529
|
+
when %r(^/markdown)
|
530
|
+
markdown = set_markdown(!markdown)
|
531
|
+
next
|
206
532
|
when %r(^/list$)
|
207
|
-
list_conversation(messages,
|
533
|
+
list_conversation(messages, markdown)
|
208
534
|
next
|
209
535
|
when %r(^/clear$)
|
210
536
|
messages.clear
|
211
537
|
puts "Cleared messages."
|
212
538
|
next
|
213
|
-
when %r(^/
|
539
|
+
when %r(^/collection (clear|stats|change|new)$)
|
540
|
+
case $1
|
541
|
+
when 'clear'
|
542
|
+
$documents.clear
|
543
|
+
puts "Cleared collection #{bold{collection}}."
|
544
|
+
when 'stats'
|
545
|
+
collection_stats
|
546
|
+
when 'change'
|
547
|
+
choose_collection(collection)
|
548
|
+
when 'new'
|
549
|
+
print "Enter name of the new collection: "
|
550
|
+
$documents.collection = collection = STDIN.gets.chomp
|
551
|
+
collection_stats
|
552
|
+
end
|
553
|
+
next
|
554
|
+
when %r(^/pop?(?:\s+(\d*))?$)
|
214
555
|
n = $1.to_i.clamp(1, Float::INFINITY)
|
215
|
-
messages.pop(n)
|
216
|
-
|
556
|
+
r = messages.pop(2 * n)
|
557
|
+
m = r.size / 2
|
558
|
+
puts "Popped the last #{m} exchanges."
|
559
|
+
next
|
560
|
+
when %r(^/model$)
|
561
|
+
model = choose_model('', model)
|
217
562
|
next
|
218
563
|
when %r(^/regenerate$)
|
219
564
|
if content = messages[-2]&.content
|
220
|
-
|
565
|
+
content.gsub!(/\nConsider these chunks for your answer.*\z/, '')
|
221
566
|
messages.pop(2)
|
222
567
|
else
|
223
568
|
puts "Not enough messages in this conversation."
|
224
569
|
redo
|
225
570
|
end
|
226
|
-
when %r(^/
|
571
|
+
when %r(^/summarize\s+(.+))
|
572
|
+
parse_content = false
|
573
|
+
content = summarize($1) or next
|
574
|
+
when %r(^/web\s+(?:(\d+)\s+)(.+)$)
|
575
|
+
parse_content = true
|
576
|
+
urls = search_web($2, $1.to_i)
|
577
|
+
content = <<~end
|
578
|
+
Answer the the query #{$2.inspect} using these sources:
|
579
|
+
|
580
|
+
#{urls * ?\n}
|
581
|
+
end
|
582
|
+
when %r(^/save\s+(.+)$)
|
227
583
|
save_conversation($1, messages)
|
228
584
|
puts "Saved conversation to #$1."
|
229
585
|
next
|
230
|
-
when %r(^/load
|
586
|
+
when %r(^/load\s+(.+)$)
|
231
587
|
messages = load_conversation($1)
|
232
588
|
puts "Loaded conversation from #$1."
|
233
589
|
next
|
234
|
-
when %r(^/image (.+)$)
|
235
|
-
filename = File.expand_path($1)
|
236
|
-
if File.exist?(filename)
|
237
|
-
images = Image.for_filename(filename)
|
238
|
-
puts "Attached image #$1 to the next message."
|
239
|
-
redo
|
240
|
-
else
|
241
|
-
puts "Filename #$1 doesn't exist. Choose another one."
|
242
|
-
next
|
243
|
-
end
|
244
590
|
when %r(^/help$)
|
245
591
|
display_chat_help
|
246
592
|
next
|
247
|
-
when nil
|
593
|
+
when nil, ''
|
248
594
|
puts "Type /quit to quit."
|
249
595
|
next
|
250
596
|
end
|
597
|
+
|
598
|
+
content, tags = if parse_content
|
599
|
+
parse_content(content, images.clear)
|
600
|
+
else
|
601
|
+
[ content, Utils::Tags.new ]
|
602
|
+
end
|
603
|
+
|
604
|
+
if $config.embedding.enabled && content
|
605
|
+
records = $documents.find(
|
606
|
+
content.downcase,
|
607
|
+
tags:,
|
608
|
+
prompt: $config.embedding.model.prompt?
|
609
|
+
)
|
610
|
+
s, found_texts_size = 0, $config.embedding.found_texts_size
|
611
|
+
records = records.take_while {
|
612
|
+
(s += _1.text.size) <= found_texts_size
|
613
|
+
}
|
614
|
+
found_texts = records.map(&:text)
|
615
|
+
unless found_texts.empty?
|
616
|
+
content += "\nConsider these chunks for your answer:\n"\
|
617
|
+
"#{found_texts.join("\n\n---\n\n")}"
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
251
621
|
messages << Message.new(role: 'user', content:, images:)
|
252
|
-
handler = FollowChat.new(messages:, markdown
|
253
|
-
|
254
|
-
|
255
|
-
|
622
|
+
handler = FollowChat.new(messages:, markdown:, voice:)
|
623
|
+
ollama.chat(model:, messages:, options:, stream: true, &handler)
|
624
|
+
|
625
|
+
if records
|
626
|
+
puts records.map { |record|
|
627
|
+
link = if record.source =~ %r(\Ahttps?://)
|
628
|
+
record.source
|
629
|
+
else
|
630
|
+
'file://%s' % File.expand_path(record.source)
|
631
|
+
end
|
632
|
+
[ link, record.tags.first ]
|
633
|
+
}.uniq.map { |l, t| hyperlink(l, t) }.join(' ')
|
634
|
+
$config.debug and jj messages
|
635
|
+
end
|
256
636
|
rescue Interrupt
|
257
637
|
puts "Type /quit to quit."
|
258
638
|
end
|