scout-ai 0.2.0 → 1.0.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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +91 -10
  3. data/Rakefile +1 -0
  4. data/VERSION +1 -1
  5. data/bin/scout-ai +2 -0
  6. data/lib/scout/llm/agent/chat.rb +24 -0
  7. data/lib/scout/llm/agent.rb +13 -13
  8. data/lib/scout/llm/ask.rb +26 -16
  9. data/lib/scout/llm/backends/bedrock.rb +129 -0
  10. data/lib/scout/llm/backends/huggingface.rb +6 -21
  11. data/lib/scout/llm/backends/ollama.rb +69 -36
  12. data/lib/scout/llm/backends/openai.rb +85 -35
  13. data/lib/scout/llm/backends/openwebui.rb +1 -1
  14. data/lib/scout/llm/backends/relay.rb +3 -2
  15. data/lib/scout/llm/backends/responses.rb +272 -0
  16. data/lib/scout/llm/chat.rb +547 -0
  17. data/lib/scout/llm/parse.rb +70 -13
  18. data/lib/scout/llm/tools.rb +126 -5
  19. data/lib/scout/llm/utils.rb +17 -10
  20. data/lib/scout/model/base.rb +19 -0
  21. data/lib/scout/model/python/base.rb +25 -0
  22. data/lib/scout/model/python/huggingface/causal/next_token.rb +23 -0
  23. data/lib/scout/model/python/huggingface/causal.rb +29 -0
  24. data/lib/scout/model/python/huggingface/classification +0 -0
  25. data/lib/scout/model/python/huggingface/classification.rb +50 -0
  26. data/lib/scout/model/python/huggingface.rb +112 -0
  27. data/lib/scout/model/python/torch/dataloader.rb +57 -0
  28. data/lib/scout/model/python/torch/helpers.rb +84 -0
  29. data/lib/scout/model/python/torch/introspection.rb +34 -0
  30. data/lib/scout/model/python/torch/load_and_save.rb +47 -0
  31. data/lib/scout/model/python/torch.rb +94 -0
  32. data/lib/scout/model/util/run.rb +181 -0
  33. data/lib/scout/model/util/save.rb +81 -0
  34. data/lib/scout-ai.rb +3 -1
  35. data/python/scout_ai/__init__.py +35 -0
  36. data/python/scout_ai/__pycache__/__init__.cpython-310.pyc +0 -0
  37. data/python/scout_ai/__pycache__/__init__.cpython-311.pyc +0 -0
  38. data/python/scout_ai/__pycache__/huggingface.cpython-310.pyc +0 -0
  39. data/python/scout_ai/__pycache__/huggingface.cpython-311.pyc +0 -0
  40. data/python/scout_ai/__pycache__/util.cpython-310.pyc +0 -0
  41. data/python/scout_ai/__pycache__/util.cpython-311.pyc +0 -0
  42. data/python/scout_ai/atcold/__init__.py +0 -0
  43. data/python/scout_ai/atcold/plot_lib.py +141 -0
  44. data/python/scout_ai/atcold/spiral.py +27 -0
  45. data/python/scout_ai/huggingface/data.py +48 -0
  46. data/python/scout_ai/huggingface/eval.py +60 -0
  47. data/python/scout_ai/huggingface/model.py +29 -0
  48. data/python/scout_ai/huggingface/rlhf.py +83 -0
  49. data/python/scout_ai/huggingface/train/__init__.py +34 -0
  50. data/python/scout_ai/huggingface/train/__pycache__/__init__.cpython-310.pyc +0 -0
  51. data/python/scout_ai/huggingface/train/__pycache__/next_token.cpython-310.pyc +0 -0
  52. data/python/scout_ai/huggingface/train/next_token.py +315 -0
  53. data/python/scout_ai/language_model.py +70 -0
  54. data/python/scout_ai/util.py +32 -0
  55. data/scout-ai.gemspec +130 -0
  56. data/scout_commands/agent/ask +133 -15
  57. data/scout_commands/agent/kb +15 -0
  58. data/scout_commands/llm/ask +71 -12
  59. data/scout_commands/llm/process +4 -2
  60. data/test/data/cat.jpg +0 -0
  61. data/test/scout/llm/agent/test_chat.rb +14 -0
  62. data/test/scout/llm/backends/test_bedrock.rb +60 -0
  63. data/test/scout/llm/backends/test_huggingface.rb +3 -3
  64. data/test/scout/llm/backends/test_ollama.rb +48 -10
  65. data/test/scout/llm/backends/test_openai.rb +96 -11
  66. data/test/scout/llm/backends/test_responses.rb +115 -0
  67. data/test/scout/llm/test_ask.rb +1 -0
  68. data/test/scout/llm/test_chat.rb +214 -0
  69. data/test/scout/llm/test_parse.rb +81 -2
  70. data/test/scout/model/python/huggingface/causal/test_next_token.rb +59 -0
  71. data/test/scout/model/python/huggingface/test_causal.rb +33 -0
  72. data/test/scout/model/python/huggingface/test_classification.rb +30 -0
  73. data/test/scout/model/python/test_base.rb +44 -0
  74. data/test/scout/model/python/test_huggingface.rb +9 -0
  75. data/test/scout/model/python/test_torch.rb +71 -0
  76. data/test/scout/model/python/torch/test_helpers.rb +14 -0
  77. data/test/scout/model/test_base.rb +117 -0
  78. data/test/scout/model/util/test_save.rb +31 -0
  79. metadata +72 -5
  80. data/questions/coach +0 -2
@@ -0,0 +1,547 @@
1
+ require_relative 'utils'
2
+ require_relative 'parse'
3
+ require_relative 'tools'
4
+
5
+ module LLM
6
+ def self.messages(question, role = nil)
7
+ default_role = "user"
8
+
9
+ if Array === question
10
+ return question.collect do |q|
11
+ if String === q
12
+ {role: default_role, content: q}
13
+ else
14
+ q
15
+ end
16
+ end
17
+ end
18
+
19
+ messages = []
20
+ current_role = nil
21
+ current_content = ""
22
+ in_protected_block = false
23
+ protected_block_type = nil
24
+ protected_stack = []
25
+
26
+ role = default_role if role.nil?
27
+
28
+ file_lines = question.split("\n")
29
+
30
+ file_lines.each do |line|
31
+ stripped = line.strip
32
+
33
+ # Detect protected blocks
34
+ if stripped.start_with?("```")
35
+ if in_protected_block
36
+ in_protected_block = false
37
+ protected_block_type = nil
38
+ current_content << "\n" << line unless line.strip.empty?
39
+ else
40
+ in_protected_block = true
41
+ protected_block_type = :square
42
+ current_content << "\n" << line unless line.strip.empty?
43
+ end
44
+ next
45
+ elsif stripped.start_with?("---")
46
+ if in_protected_block
47
+ in_protected_block = false
48
+ protected_block_type = nil
49
+ current_content << "\n" << line unless line.strip.empty?
50
+ else
51
+ in_protected_block = true
52
+ protected_block_type = :square
53
+ current_content << "\n" << line unless line.strip.empty?
54
+ end
55
+ next
56
+ elsif stripped.end_with?("]]") && in_protected_block && protected_block_type == :square
57
+ in_protected_block = false
58
+ protected_block_type = nil
59
+ line = line.sub("]]", "")
60
+ current_content << "\n" << line unless line.strip.empty?
61
+ next
62
+ elsif stripped.start_with?("[[")
63
+ in_protected_block = true
64
+ protected_block_type = :square
65
+ line = line.sub("[[", "")
66
+ current_content << "\n" << line unless line.strip.empty?
67
+ next
68
+ elsif stripped.end_with?("]]") && in_protected_block && protected_block_type == :square
69
+ in_protected_block = false
70
+ protected_block_type = nil
71
+ line = line.sub("]]", "")
72
+ current_content << "\n" << line unless line.strip.empty?
73
+ next
74
+ elsif stripped.match(/^.*:-- .* {{{/)
75
+ in_protected_block = true
76
+ protected_block_type = :square
77
+ line = line.sub(/^.*:-- (.*) {{{/, '<cmd_output cmd="\1">')
78
+ current_content << "\n" << line unless line.strip.empty?
79
+ next
80
+ elsif stripped.match(/^.*:--.* }}}/) && in_protected_block && protected_block_type == :square
81
+ in_protected_block = false
82
+ protected_block_type = nil
83
+ line = line.sub(/^.*:-- .* }}}.*/, "</cmd_output>")
84
+ current_content << "\n" << line unless line.strip.empty?
85
+ next
86
+ elsif in_protected_block
87
+
88
+ if protected_block_type == :xml
89
+ if stripped =~ %r{</(\w+)>}
90
+ closing_tag = $1
91
+ if protected_stack.last == closing_tag
92
+ protected_stack.pop
93
+ end
94
+ if protected_stack.empty?
95
+ in_protected_block = false
96
+ protected_block_type = nil
97
+ end
98
+ end
99
+ end
100
+ current_content << "\n" << line
101
+ next
102
+ end
103
+
104
+ # XML-style tag handling (protected content)
105
+ if stripped =~ /^<(\w+)(\s+[^>]*)?>/
106
+ tag = $1
107
+ protected_stack.push(tag)
108
+ in_protected_block = true
109
+ protected_block_type = :xml
110
+ end
111
+
112
+ # Match a new message header
113
+ if line =~ /^([a-z0-9_]+):(.*)$/
114
+ role = $1
115
+ inline_content = $2.strip
116
+
117
+ # Save current message if any
118
+ messages << { role: current_role, content: current_content.strip }
119
+
120
+ if inline_content.empty?
121
+ # Block message
122
+ current_role = role
123
+ current_content = ""
124
+ else
125
+ # Inline message + next block is default role
126
+ messages << { role: role, content: inline_content }
127
+ #current_role = default_role
128
+ current_content = ""
129
+ end
130
+ else
131
+ current_content << "\n" << line
132
+ end
133
+ end
134
+
135
+ # Final message
136
+ messages << { role: current_role || default_role, content: current_content.strip }
137
+
138
+ messages
139
+ end
140
+
141
+ def self.imports(messages, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats'))
142
+ messages.collect do |message|
143
+ if message[:role] == 'import' || message[:role] == 'continue'
144
+ file = message[:content].to_s.strip
145
+ path = Scout.chats[file]
146
+ original = original.find if Path === original
147
+ if original
148
+ relative = File.join(File.dirname(original), file)
149
+ relative_lib = File.join(caller_lib_dir, file)
150
+ end
151
+
152
+ new = if Open.exist?(file)
153
+ LLM.chat file
154
+ elsif relative && Open.exist?(relative)
155
+ LLM.chat relative
156
+ elsif relative_lib && Open.exist?(relative_lib)
157
+ LLM.chat relative_lib
158
+ elsif path.exists?
159
+ LLM.chat path
160
+ else
161
+ raise "Import not found: #{file}"
162
+ end
163
+
164
+ if message[:role] == 'continue'
165
+ new.last
166
+ else
167
+ new
168
+ end
169
+ else
170
+ message
171
+ end
172
+ end.flatten
173
+ end
174
+
175
+ def self.files(messages, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats'))
176
+ messages.collect do |message|
177
+ if message[:role] == 'file' || message[:role] == 'directory'
178
+ file = message[:content].strip
179
+ path = Scout.chats[file]
180
+ original = original.find if Path === original
181
+ if original
182
+ relative = File.join(File.dirname(original), file)
183
+ relative_lib = File.join(caller_lib_dir, file)
184
+ end
185
+
186
+ target = if Open.exist?(file)
187
+ file
188
+ elsif relative && Open.exist?(relative)
189
+ relative
190
+ elsif relative_lib && Open.exist?(relative_lib)
191
+ relative_lib
192
+ elsif path.exists?
193
+ path
194
+ else
195
+ raise "File not found: #{file}"
196
+ end
197
+
198
+ if message[:role] == 'directory'
199
+ Path.setup target
200
+ target.glob('**/*').
201
+ reject{|file|
202
+ Open.directory?(file)
203
+ }.collect{|file|
204
+ files([{role: 'file', content: file}])
205
+ }
206
+ else
207
+ new = LLM.tag :file, Open.read(target), file
208
+ {role: 'user', content: new}
209
+ end
210
+ else
211
+ message
212
+ end
213
+ end.flatten
214
+ end
215
+
216
+ def self.tasks(messages, original = nil)
217
+ jobs = []
218
+ new = messages.collect do |message|
219
+ if message[:role] == 'task' || message[:role] == 'inline_task'
220
+ info = message[:content].strip
221
+
222
+ workflow, task = info.split(" ").values_at 0, 1
223
+
224
+ options = IndiferentHash.parse_options info
225
+ jobname = options.delete :jobname
226
+
227
+ job = Workflow.require_workflow(workflow).job(task, jobname, options)
228
+
229
+ jobs << job
230
+
231
+ if message[:role] == 'inline_task'
232
+ {role: 'inline_job', content: job.short_path}
233
+ else
234
+ {role: 'job', content: job.short_path}
235
+ end
236
+ else
237
+ message
238
+ end
239
+ end.flatten
240
+
241
+ Workflow.produce(jobs)
242
+
243
+ new
244
+ end
245
+
246
+ def self.jobs(messages, original = nil)
247
+ messages.collect do |message|
248
+ if message[:role] == 'job' || message[:role] == 'inline_job'
249
+ file = message[:content].strip
250
+
251
+ step = Step.load file
252
+
253
+ id = step.short_path[0..39]
254
+ id = id.gsub('/','-')
255
+
256
+
257
+ if message[:role] == 'inline_job'
258
+ {role: 'file', content: step.path.find}
259
+ else
260
+ tool_call = {
261
+ type: "function",
262
+ function: {
263
+ name: step.full_task_name.sub('#', '-'),
264
+ arguments: step.provided_inputs.to_json
265
+ },
266
+ id: id,
267
+ }
268
+
269
+ tool_output = {
270
+ tool_call_id: id,
271
+ role: "tool",
272
+ content: step.path.read
273
+ }
274
+
275
+ [
276
+ {role: 'function_call', content: tool_call.to_json},
277
+ {role: 'function_call_output', content: tool_output.to_json},
278
+ ]
279
+ end
280
+ else
281
+ message
282
+ end
283
+ end.flatten
284
+ end
285
+
286
+ def self.clear(messages)
287
+ new = []
288
+
289
+ messages.reverse.each do |message|
290
+ if message[:role] == 'clear'
291
+ break
292
+ else
293
+ new << message
294
+ end
295
+ end
296
+
297
+ new.reverse
298
+ end
299
+
300
+ def self.clean(messages)
301
+ messages.reject do |message|
302
+ message[:content] && message[:content].empty?
303
+ end
304
+ end
305
+
306
+ def self.indiferent(messages)
307
+ messages.collect{|msg| IndiferentHash.setup msg }
308
+ end
309
+
310
+ def self.chat(file)
311
+ original = (String === file and Open.exists?(file)) ? file : Path.setup($0.dup)
312
+ caller_lib_dir = Path.caller_lib_dir(nil, 'chats')
313
+
314
+ if Array === file
315
+ messages = self.messages file
316
+ messages = self.indiferent messages
317
+ messages = self.imports messages, original, caller_lib_dir
318
+ elsif Open.exists?(file)
319
+ messages = self.messages Open.read(file)
320
+ messages = self.indiferent messages
321
+ messages = self.imports messages, original, caller_lib_dir
322
+ else
323
+ messages = self.messages file
324
+ messages = self.indiferent messages
325
+ messages = self.imports messages, original, caller_lib_dir
326
+ end
327
+
328
+ messages = self.clear messages
329
+ messages = self.clean messages
330
+ messages = self.tasks messages
331
+ messages = self.jobs messages
332
+ messages = self.files messages, original, caller_lib_dir
333
+
334
+ messages
335
+ end
336
+
337
+ def self.options(chat)
338
+ options = IndiferentHash.setup({})
339
+ new = []
340
+ chat.each do |info|
341
+ if Hash === info
342
+ role = info[:role].to_s
343
+ if %w(endpoint format model backend persist).include? role.to_s
344
+ options[role] = info[:content]
345
+ next
346
+ end
347
+
348
+ if role == 'assistant'
349
+ options.clear
350
+ end
351
+ end
352
+ new << info
353
+ end
354
+ chat.replace new
355
+ options
356
+ end
357
+
358
+ def self.tools(messages)
359
+ tool_definitions = {}
360
+ new = messages.collect do |message|
361
+ if message[:role] == 'tool'
362
+ workflow_name, task_name, *inputs = message[:content].strip.split(/\s+/)
363
+ inputs = nil if inputs.empty?
364
+ inputs = [] if inputs == ['none'] || inputs == ['noinputs']
365
+ if Open.remote? workflow_name
366
+ require 'rbbt'
367
+ require 'scout/offsite/ssh'
368
+ require 'rbbt/workflow/remote_workflow'
369
+ workflow = RemoteWorkflow.new workflow_name
370
+ else
371
+ workflow = Workflow.require_workflow workflow_name
372
+ end
373
+ definition = LLM.task_tool_definition workflow, task_name, inputs
374
+ tool_definitions[task_name] = [workflow, definition]
375
+ next
376
+ else
377
+ message
378
+ end
379
+ end.compact.flatten
380
+ messages.replace new
381
+ tool_definitions
382
+ end
383
+
384
+ def self.associations(messages)
385
+ tool_definitions = {}
386
+ kb = nil
387
+ new = messages.collect do |message|
388
+ if message[:role] == 'association'
389
+ name, path, *options = message[:content].strip.split(/\s+/)
390
+
391
+ kb ||= KnowledgeBase.new Scout.var.Agent.Chat.knowledge_base
392
+ kb.register name, Path.setup(path), IndiferentHash.parse_options(message[:content])
393
+
394
+ definition = LLM.association_tool_definition name
395
+ tool_definitions[name] = [kb, definition]
396
+ next
397
+ else
398
+ message
399
+ end
400
+ end.compact.flatten
401
+ messages.replace new
402
+ tool_definitions
403
+ end
404
+
405
+ def self.print(chat)
406
+ return chat if String === chat
407
+ chat.collect do |message|
408
+ IndiferentHash.setup message
409
+ case message[:content]
410
+ when Hash, Array
411
+ message[:role].to_s + ":\n\n" + message[:content].to_json
412
+ when nil
413
+ message[:role].to_s + ":\n\n" + message.to_json
414
+ else
415
+ message[:role].to_s + ":\n\n" + message[:content].to_s
416
+ end
417
+ end * "\n\n"
418
+ end
419
+ end
420
+
421
+ module Chat
422
+ extend Annotation
423
+
424
+ def message(role, content)
425
+ self.append({role: role.to_s, content: content})
426
+ end
427
+
428
+ def user(content)
429
+ message(:user, content)
430
+ end
431
+
432
+ def system(content)
433
+ message(:system, content)
434
+ end
435
+
436
+ def assistant(content)
437
+ message(:assistant, content)
438
+ end
439
+
440
+ def import(file)
441
+ message(:import, file)
442
+ end
443
+
444
+ def file(file)
445
+ message(:file, file)
446
+ end
447
+
448
+ def directory(directory)
449
+ message(:directory, directory)
450
+ end
451
+
452
+ def continue(file)
453
+ message(:continue, file)
454
+ end
455
+
456
+ def format(format)
457
+ message(:format, format)
458
+ end
459
+
460
+ def tool(*parts)
461
+ content = parts * "\n"
462
+ message(:tool, content)
463
+ end
464
+
465
+ def task(workflow, task_name, inputs = {})
466
+ input_str = IndiferentHash.print_options inputs
467
+ content = [workflow, task_name, input_str]*" "
468
+ message(:task, content)
469
+ end
470
+
471
+ def inline_task(workflow, task_name, inputs = {})
472
+ input_str = IndiferentHash.print_options inputs
473
+ content = [workflow, task_name, input_str]*" "
474
+ message(:inline_task, content)
475
+ end
476
+
477
+ def job(step)
478
+ message(:job, step.path)
479
+ end
480
+
481
+ def association(name, path, options = {})
482
+ options_str = IndiferentHash.print_options options
483
+ content = [name, path, options_str]*" "
484
+ message(:association, name)
485
+ end
486
+
487
+ def tag(content, name=nil, tag=:file, role=:user)
488
+ self.message role, LLM.tag(tag, content, name)
489
+ end
490
+
491
+ def ask(...)
492
+ LLM.ask(LLM.chat(self), ...)
493
+ end
494
+
495
+ def chat(...)
496
+ self.push({role: :assistant, content: self.ask(...)})
497
+ end
498
+
499
+ def json(...)
500
+ self.format :json
501
+ output = ask(...)
502
+ obj = JSON.parse output
503
+ if (Hash === obj) and obj.keys == ['content']
504
+ obj['content']
505
+ else
506
+ obj
507
+ end
508
+ end
509
+
510
+ def print
511
+ LLM.print LLM.chat(self)
512
+ end
513
+
514
+ def write(path, force = true)
515
+ path = path.to_s if Symbol === path
516
+ if not (Open.exists?(path) || Path === path || Path.located?(path))
517
+ path = Rbbt.chats.find[path]
518
+ end
519
+ return if Open.exists?(path) && ! force
520
+ Open.write path, self.print
521
+ end
522
+
523
+ def branch
524
+ self.annotate self.dup
525
+ end
526
+
527
+ def option(name, value)
528
+ self.message name, value
529
+ end
530
+
531
+ def endpoint(value)
532
+ option :endpoint, value
533
+ end
534
+
535
+ def model(value)
536
+ option :model, value
537
+ end
538
+
539
+ def shed
540
+ self.annotate [self.last]
541
+ end
542
+
543
+ def create_image(file, ...)
544
+ base64_image = LLM.image(LLM.chat(self), ...)
545
+ Open.write(file, Base64.decode(file_content), mode: 'wb')
546
+ end
547
+ end
@@ -1,4 +1,33 @@
1
1
  module LLM
2
+ def self.process_inside(inside)
3
+ header, content = inside.match(/([^\n]*)\n(.*)/).values_at 1, 2
4
+ if header.empty?
5
+ content
6
+ else
7
+ action, _sep, rest = header.partition /\s/
8
+ case action
9
+ when 'import'
10
+ when 'cmd'
11
+ title = rest.strip.empty? ? content : rest
12
+ tag('file', title, CMD.cmd(content).read)
13
+ when 'file'
14
+ file = content
15
+ title = rest.strip.empty? ? file : rest
16
+ tag(action, title, Open.read(file))
17
+ when 'directory'
18
+ directory = content
19
+ title = rest.strip.empty? ? directory : rest
20
+ directory_content = Dir.glob(File.join(directory, '**/*')).collect do |file|
21
+ file_title = Misc.path_relative_to(directory, file)
22
+ tag('file', file_title, Open.read(file) )
23
+ end * "\n"
24
+ tag(action, title, directory_content )
25
+ else
26
+ tag(action, rest, content)
27
+ end
28
+ end
29
+ end
30
+
2
31
  def self.parse(question, role = nil)
3
32
  role = :user if role.nil?
4
33
 
@@ -12,21 +41,49 @@ module LLM
12
41
  inside = m[2]
13
42
  post = m[3]
14
43
  messages = parse(pre, role)
15
- messages.last[:content] += "\n" + inside
16
- messages.concat parse(post, role)
44
+
45
+ messages = [{role: role, content: ''}] if messages.empty?
46
+ messages.last[:content] += process_inside inside
47
+
48
+ last = parse(post, messages.last[:role])
49
+
50
+ messages.concat last
51
+
52
+ messages
53
+ elsif m = question.match(/(.*?)(```.*?```)(.*)/m)
54
+ pre = m[1]
55
+ inside = m[2]
56
+ post = m[3]
57
+ messages = parse(pre, role)
58
+
59
+ messages = [{role: role, content: ''}] if messages.empty?
60
+ messages.last[:content] += inside
61
+
62
+ last = parse(post, messages.last[:role])
63
+
64
+ if last.first[:role] == messages.last[:role]
65
+ m = last.shift
66
+ messages.last[:content] += m[:content]
67
+ end
68
+
69
+ messages.concat last
70
+
71
+ messages
17
72
  else
18
- question.split("\n").collect do |line|
19
- if line.include?("\t")
20
- question_role, _sep, q = line.partition("\t")
21
- elsif m = line.match(/^([^\s]*): ?(.*)/)
22
- question_role, q = m.values_at 1, 2
23
- else
24
- question_role = role
25
- q = line
73
+ chunks = question.scan(/(.*?)^(\w+):(.*?)(?=^\w+:|\z)/m)
74
+
75
+ if chunks.any?
76
+ messages = []
77
+ messages << {role: role, content: chunks.first.first} if chunks.first and not chunks.first.first.empty?
78
+ chunks.collect do |pre,role,text|
79
+ messages << {role: role, content: text.strip}
26
80
  end
27
- next if q.empty?
28
- {role: question_role, content: q}
29
- end.compact
81
+ messages
82
+ elsif question.strip.empty?
83
+ []
84
+ else
85
+ [{role: role, content: question}]
86
+ end
30
87
  end
31
88
  end
32
89
  end