scout-ai 1.0.1 → 1.1.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +20 -2
  3. data/Rakefile +1 -0
  4. data/VERSION +1 -1
  5. data/bin/scout-ai +46 -0
  6. data/lib/scout/llm/agent/chat.rb +4 -7
  7. data/lib/scout/llm/agent/delegate.rb +12 -0
  8. data/lib/scout/llm/agent.rb +2 -2
  9. data/lib/scout/llm/ask.rb +18 -2
  10. data/lib/scout/llm/backends/huggingface.rb +0 -2
  11. data/lib/scout/llm/backends/ollama.rb +6 -6
  12. data/lib/scout/llm/backends/openai.rb +7 -4
  13. data/lib/scout/llm/backends/openwebui.rb +1 -4
  14. data/lib/scout/llm/backends/relay.rb +1 -3
  15. data/lib/scout/llm/backends/responses.rb +34 -18
  16. data/lib/scout/llm/chat/annotation.rb +195 -0
  17. data/lib/scout/llm/chat/parse.rb +139 -0
  18. data/lib/scout/llm/chat/process/clear.rb +29 -0
  19. data/lib/scout/llm/chat/process/files.rb +96 -0
  20. data/lib/scout/llm/chat/process/options.rb +52 -0
  21. data/lib/scout/llm/chat/process/tools.rb +173 -0
  22. data/lib/scout/llm/chat/process.rb +16 -0
  23. data/lib/scout/llm/chat.rb +26 -662
  24. data/lib/scout/llm/mcp.rb +1 -1
  25. data/lib/scout/llm/tools/call.rb +22 -1
  26. data/lib/scout/llm/tools/knowledge_base.rb +15 -14
  27. data/lib/scout/llm/tools/mcp.rb +4 -0
  28. data/lib/scout/llm/tools/workflow.rb +54 -15
  29. data/lib/scout/llm/tools.rb +42 -0
  30. data/lib/scout/llm/utils.rb +2 -17
  31. data/scout-ai.gemspec +13 -4
  32. data/scout_commands/agent/ask +36 -12
  33. data/scout_commands/llm/ask +17 -7
  34. data/scout_commands/llm/process +1 -1
  35. data/test/scout/llm/backends/test_anthropic.rb +2 -2
  36. data/test/scout/llm/backends/test_ollama.rb +1 -1
  37. data/test/scout/llm/backends/test_responses.rb +9 -9
  38. data/test/scout/llm/chat/test_parse.rb +126 -0
  39. data/test/scout/llm/chat/test_process.rb +123 -0
  40. data/test/scout/llm/test_agent.rb +2 -25
  41. data/test/scout/llm/test_chat.rb +2 -178
  42. metadata +25 -3
  43. data/lib/scout/llm/parse.rb +0 -91
@@ -0,0 +1,126 @@
1
+ require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
2
+ require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1')
3
+
4
+ class TestParse < Test::Unit::TestCase
5
+ def test_parse_simple_text
6
+ text = "Hello\nWorld"
7
+ msgs = Chat.parse(text)
8
+ assert_equal 1, msgs.size
9
+ assert_equal 'user', msgs[0][:role]
10
+ assert_equal "Hello\nWorld", msgs[0][:content]
11
+ end
12
+
13
+ def test_parse_block_and_inline_headers
14
+ text = <<~TXT
15
+ assistant:
16
+ This is a block
17
+ with lines
18
+ user: inline reply
19
+ another line
20
+ TXT
21
+
22
+ msgs = Chat.parse(text)
23
+
24
+ # Expect a few messages: initial empty user, assistant block, inline user, and final user block
25
+ assert_equal 'user', msgs[0][:role]
26
+ assert_equal '', msgs[0][:content]
27
+
28
+ assert_equal 'assistant', msgs[1][:role]
29
+ assert_equal "This is a block\nwith lines", msgs[1][:content]
30
+
31
+ assert_equal 'user', msgs[2][:role]
32
+ assert_equal 'inline reply', msgs[2][:content]
33
+
34
+ assert_equal 'assistant', msgs[3][:role]
35
+ assert_equal 'another line', msgs[3][:content]
36
+ end
37
+
38
+ def test_parse_code_fence_protection
39
+ text = <<~TXT
40
+ assistant:
41
+ Here is code:
42
+ ```
43
+ def foo
44
+ end
45
+ ```
46
+ Done
47
+ TXT
48
+
49
+ msgs = Chat.parse(text)
50
+ assert_equal 2, msgs.size # initial empty + assistant
51
+
52
+ assistant_msg = msgs[1]
53
+ assert_equal 'assistant', assistant_msg[:role]
54
+
55
+ expected = "Here is code:\n```\ndef foo\nend\n```\nDone"
56
+ assert_equal expected, assistant_msg[:content]
57
+ end
58
+
59
+ def test_parse_xml_protection
60
+ text = <<~TXT
61
+ assistant:
62
+ Before xml
63
+ <note>
64
+ This is protected
65
+ </note>
66
+ After
67
+ TXT
68
+
69
+ msgs = Chat.parse(text)
70
+ assistant_msg = msgs.find { |m| m[:role] == 'assistant' }
71
+ assert assistant_msg
72
+ assert_equal "Before xml\n<note>\nThis is protected\n</note>\nAfter", assistant_msg[:content]
73
+ end
74
+
75
+ def test_parse_square_brackets_protection
76
+ text = <<~TXT
77
+ assistant:
78
+ Start
79
+ [[This: has colon
80
+ and lines]]
81
+ End
82
+ TXT
83
+
84
+ msgs = Chat.parse(text)
85
+ assistant_msg = msgs.find { |m| m[:role] == 'assistant' }
86
+ assert assistant_msg
87
+ assert_equal "Start\nThis: has colon\nand lines\nEnd", assistant_msg[:content]
88
+ end
89
+
90
+ def test_parse_cmd_output_protection
91
+ text = <<~TXT
92
+ assistant:
93
+ Before
94
+ shell:-- ls {{{
95
+ file1
96
+ shell:-- ls }}}
97
+ After
98
+ TXT
99
+
100
+ msgs = Chat.parse(text)
101
+ assistant_msg = msgs.find { |m| m[:role] == 'assistant' }
102
+ assert assistant_msg
103
+
104
+ expected = "Before\n<cmd_output cmd=\"ls\">\nfile1\n</cmd_output>\nAfter"
105
+ assert_equal expected, assistant_msg[:content]
106
+ end
107
+
108
+ def test_previous_response_id_behavior
109
+ text = <<~TXT
110
+ previous_response_id:abc123
111
+ Some block
112
+ assistant: Got it
113
+ TXT
114
+
115
+ msgs = Chat.parse(text)
116
+
117
+ # Find the previous_response_id message
118
+ idx = msgs.index { |m| m[:role] == 'previous_response_id' }
119
+ assert idx, 'previous_response_id message not found'
120
+ assert_equal 'abc123', msgs[idx][:content]
121
+
122
+ # The message after previous_response_id should be a user block containing "Some block"
123
+ assert_equal 'user', msgs[idx + 1][:role]
124
+ assert_equal 'Some block', msgs[idx + 1][:content]
125
+ end
126
+ end
@@ -0,0 +1,123 @@
1
+ require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
2
+ require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1')
3
+
4
+ require 'scout/llm/chat'
5
+ class TestProcess < Test::Unit::TestCase
6
+ def setup
7
+ super
8
+ @tmp = tmpdir
9
+ end
10
+
11
+ def _test_imports_basic_and_continue_last
12
+ TmpFile.with_file do |file|
13
+ Open.write(file, "assistant: hello\nuser: from_import\n")
14
+
15
+ messages = [{role: 'import', content: file}]
16
+ out = Chat.imports(messages)
17
+
18
+ # Should have replaced import with the messages from the file
19
+ roles = out.collect{|m| m[:role]}
20
+ assert_includes roles, 'assistant'
21
+ assert_includes roles, 'user'
22
+
23
+ # Test continue: only last non-empty message
24
+ messages = [{role: 'continue', content: file}]
25
+ out = Chat.imports(messages)
26
+ assert_equal 1, out.size
27
+ assert_equal 'user', out[0][:role]
28
+ assert_equal 'from_import', out[0][:content].strip
29
+
30
+ # Test last: should behave similarly but using purge
31
+ messages = [{role: 'last', content: file}]
32
+ out = Chat.imports(messages)
33
+ assert_equal 1, out.size
34
+ end
35
+ end
36
+
37
+ def _test_files_file_reads_and_tags_content
38
+ TmpFile.with_file do |tmp|
39
+ file = File.join(tmp, 'afile.txt')
40
+ Open.write(file, "SOME_UNIQUE_CONTENT_12345")
41
+
42
+ messages = [{role: 'file', content: file}]
43
+ out = Chat.files(messages)
44
+
45
+ assert_equal 1, out.size
46
+ msg = out[0]
47
+ assert_equal 'user', msg[:role]
48
+ # content should include the file content and the filename
49
+ assert_match /SOME_UNIQUE_CONTENT_12345/, msg[:content]
50
+ assert_match /afile.txt/, msg[:content]
51
+ end
52
+ end
53
+
54
+ def _test_options_extracts_and_resets
55
+ chat = [
56
+ {role: 'endpoint', content: 'http://api.example'},
57
+ {role: 'option', content: 'k1 v1'},
58
+ {role: 'sticky_option', content: 'sk sv'},
59
+ {role: 'assistant', content: 'ok'},
60
+ {role: 'option', content: 'k2 v2'},
61
+ {role: 'user', content: 'do something'}
62
+ ]
63
+
64
+ opts = Chat.options(chat)
65
+
66
+ # endpoint should be sticky
67
+ assert_equal 'http://api.example', opts['endpoint']
68
+ # sticky_option should be present
69
+ assert_equal 'sv', opts['sk']
70
+ # first option k1 should have been cleared after assistant
71
+ assert_nil opts['k1']
72
+ # second option should remain
73
+ assert_equal 'v2', opts['k2']
74
+
75
+ # chat should have been replaced and should not include option messages
76
+ roles = chat.collect{|m| m[:role]}
77
+ assert_includes roles, 'assistant'
78
+ assert_includes roles, 'user'
79
+ assert_not_includes roles, 'option'
80
+ assert_not_includes roles, 'sticky_option'
81
+ end
82
+
83
+ def test_tasks_creates_jobs_and_calls_workflow_produce
84
+ # define a minimal workflow class to be resolved by Kernel.const_get
85
+ klass = Class.new do
86
+ def self.job(task_name, jobname=nil, options={})
87
+ # return a simple object with a path that responds to find
88
+ path = Struct.new(:p) do
89
+ def find; p; end
90
+ end
91
+ job = Struct.new(:path).new(path.new("/tmp/fake_job_#{task_name}"))
92
+ job
93
+ end
94
+ end
95
+
96
+ Object.const_set('TestWorkflow', klass)
97
+
98
+ produced = nil
99
+ # stub Workflow.produce to capture
100
+ orig = Workflow.method(:produce)
101
+ Workflow.define_singleton_method(:produce) do |jobs|
102
+ produced = jobs
103
+ end
104
+
105
+ begin
106
+ messages = [ {role: 'task', content: 'TestWorkflow mytask jobname=jn param=1'} ]
107
+ out = Chat.tasks(messages)
108
+
109
+ # Should have returned a job message pointing to our fake path
110
+ assert_equal 1, out.size
111
+ assert_equal 'job', out[0][:role]
112
+ assert_match /fake_job_mytask/, out[0][:content]
113
+
114
+ # produce should have been called with the job
115
+ assert_not_nil produced
116
+ assert_equal 1, produced.size
117
+ ensure
118
+ # restore original
119
+ Workflow.define_singleton_method(:produce, orig)
120
+ Object.send(:remove_const, 'TestWorkflow') rescue nil
121
+ end
122
+ end
123
+ end
@@ -1,9 +1,9 @@
1
1
  require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
2
2
  require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1')
3
3
 
4
- require 'rbbt-util'
4
+ require 'scout/knowledge_base'
5
5
  class TestLLMAgent < Test::Unit::TestCase
6
- def _test_system
6
+ def test_system
7
7
  TmpFile.with_dir do |dir|
8
8
  kb = KnowledgeBase.new dir
9
9
  kb.format = {"Person" => "Alias"}
@@ -13,32 +13,9 @@ class TestLLMAgent < Test::Unit::TestCase
13
13
 
14
14
  agent = LLM::Agent.new knowledge_base: kb
15
15
 
16
- agent.system = ""
17
-
18
16
  sss 0
19
17
  ppp agent.ask "Who is Miguel's brother-in-law. Brother in law is your spouses sibling or your sibling's spouse"
20
- #ppp agent.ask "Who is Guille's brother-in-law. Brother in law is your spouses sibling or your sibling's spouse"
21
- end
22
- end
23
-
24
- def _test_system_gepeto
25
- TmpFile.with_dir do |dir|
26
- Scout::Config.set(:backend, :ollama, :ask)
27
- kb = KnowledgeBase.new dir
28
- kb.format = {"Person" => "Alias"}
29
- kb.register :brothers, datafile_test(:person).brothers, undirected: true
30
- kb.register :marriages, datafile_test(:person).marriages, undirected: true, source: "=>Alias", target: "=>Alias"
31
- kb.register :parents, datafile_test(:person).parents
32
-
33
- agent = LLM::Agent.new knowledge_base: kb, model: 'mistral', url: "https://gepeto.bsc.es/"
34
-
35
- agent.system = ""
36
-
37
- sss 0
38
- ppp agent.ask "Who is Guille's brother-in-law. Brother in law is your spouses sibling or your sibling's spouse"
39
18
  end
40
19
  end
41
-
42
-
43
20
  end
44
21
 
@@ -3,143 +3,9 @@ require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1
3
3
 
4
4
  class TestMessages < Test::Unit::TestCase
5
5
 
6
- def test_short
7
-
8
- question =<<-EOF
9
- Hi
10
- EOF
11
-
12
- iii LLM.chat(question)
13
- end
14
-
15
- def test_inline
16
- question =<<-EOF
17
- system:
18
-
19
- you are a terse assistant that only write in short sentences
20
-
21
- assistant:
22
-
23
- Here is some stuff
24
-
25
- user: feedback
26
-
27
- that continues here
28
- EOF
29
-
30
- iii LLM.chat(question)
31
- end
32
-
33
- def test_messages
34
- question =<<-EOF
35
- system:
36
-
37
- you are a terse assistant that only write in short sentences
38
-
39
- user:
40
-
41
- What is the capital of France
42
-
43
- assistant:
44
-
45
- Paris
46
-
47
- user:
48
-
49
- is this the national anthem
50
-
51
- [[
52
- corous: Viva Espagna
53
- ]]
54
-
55
- assistant:
56
-
57
- no
58
-
59
- user:
60
-
61
- import: math.system
62
-
63
- consider this file
64
-
65
- <file name=foo_bar>
66
- foo: bar
67
- </file>
68
-
69
- how many characters does it hold
70
-
71
- assistant:
72
-
73
- 8
74
- EOF
75
-
76
- messages = LLM.messages question
77
- refute messages.collect{|i| i[:role] }.include?("corous")
78
- assert messages.collect{|i| i[:role] }.include?("import")
79
- end
80
-
81
- def test_chat_import
82
- file1 =<<-EOF
83
- system: You are an assistant
84
- EOF
85
-
86
- file2 =<<-EOF
87
- import: header
88
- user: say something
89
- EOF
90
-
91
- TmpFile.with_path do |tmpdir|
92
- tmpdir.header.write file1
93
- tmpdir.chat.write file2
94
-
95
- chat = LLM.chat tmpdir.chat
96
- end
97
- end
98
-
99
- def test_clear
100
- question =<<-EOF
101
- system:
102
-
103
- you are a terse assistant that only write in short sentences
104
-
105
- clear:
106
-
107
- user:
108
-
109
- What is the capital of France
110
- EOF
111
-
112
- TmpFile.with_file question do |file|
113
- messages = LLM.chat file
114
- refute messages.collect{|m| m[:role] }.include?('system')
115
- end
116
- end
117
-
118
- def __test_job
119
- question =<<-EOF
120
- system:
121
-
122
- you are a terse assistant that only write in short sentences
123
-
124
- job: Baking/bake_muffin_tray/Default_08a1812eca3a18dce2232509dabc9b41
125
-
126
- How are muffins made
127
-
128
- EOF
129
-
130
- TmpFile.with_file question do |file|
131
- messages = LLM.chat file
132
- ppp LLM.print messages
133
- end
134
- end
135
-
136
6
 
137
7
  def test_task
138
8
  question =<<-EOF
139
- system:
140
-
141
- you are a terse assistant that only write in short sentences
142
-
143
9
  user:
144
10
 
145
11
  task: Baking bake_muffin_tray blueberries=true title="This is a title" list=one,two,"and three"
@@ -150,26 +16,8 @@ How are muffins made?
150
16
 
151
17
  TmpFile.with_file question do |file|
152
18
  messages = LLM.chat file
153
- ppp LLM.print messages
154
- end
155
- end
156
-
157
- def test_structure
158
- require 'scout/llm/ask'
159
- sss 0
160
- question =<<-EOF
161
- system:
162
-
163
- Respond in json format with a hash of strings as keys and string arrays as values, at most three in length
164
-
165
- endpoint: sambanova
166
-
167
- What other movies have the protagonists of the original gost busters played on, just the top.
168
-
169
- EOF
170
-
171
- TmpFile.with_file question do |file|
172
- ppp LLM.ask file
19
+ assert_include messages.collect{|m| m[:role] }, 'function_call'
20
+ assert_include messages.find{|m| m[:role] == 'function_call_output' }[:content], 'Baking'
173
21
  end
174
22
  end
175
23
 
@@ -228,29 +76,5 @@ association: marriages #{datafile_test(:person).marriages} undirected=true sourc
228
76
  ppp LLM.ask file
229
77
  end
230
78
  end
231
-
232
- def test_previous_response
233
- require 'scout/llm/ask'
234
- sss 0
235
- question =<<-EOF
236
- user:
237
-
238
- Say hi
239
-
240
- assistant:
241
-
242
- Hi
243
-
244
- previous_response_id: asdfasdfasdfasdf
245
-
246
- Bye
247
-
248
- EOF
249
-
250
- messages = LLM.messages question
251
-
252
- iii messages
253
-
254
- end
255
79
  end
256
80
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout-ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miguel Vazquez
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: ollama-ai
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: ruby-mcp-client
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -89,9 +103,15 @@ files:
89
103
  - lib/scout/llm/backends/relay.rb
90
104
  - lib/scout/llm/backends/responses.rb
91
105
  - lib/scout/llm/chat.rb
106
+ - lib/scout/llm/chat/annotation.rb
107
+ - lib/scout/llm/chat/parse.rb
108
+ - lib/scout/llm/chat/process.rb
109
+ - lib/scout/llm/chat/process/clear.rb
110
+ - lib/scout/llm/chat/process/files.rb
111
+ - lib/scout/llm/chat/process/options.rb
112
+ - lib/scout/llm/chat/process/tools.rb
92
113
  - lib/scout/llm/embed.rb
93
114
  - lib/scout/llm/mcp.rb
94
- - lib/scout/llm/parse.rb
95
115
  - lib/scout/llm/rag.rb
96
116
  - lib/scout/llm/tools.rb
97
117
  - lib/scout/llm/tools/call.rb
@@ -145,6 +165,8 @@ files:
145
165
  - test/scout/llm/backends/test_openwebui.rb
146
166
  - test/scout/llm/backends/test_relay.rb
147
167
  - test/scout/llm/backends/test_responses.rb
168
+ - test/scout/llm/chat/test_parse.rb
169
+ - test/scout/llm/chat/test_process.rb
148
170
  - test/scout/llm/test_agent.rb
149
171
  - test/scout/llm/test_ask.rb
150
172
  - test/scout/llm/test_chat.rb
@@ -186,7 +208,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
208
  - !ruby/object:Gem::Version
187
209
  version: '0'
188
210
  requirements: []
189
- rubygems_version: 3.7.0.dev
211
+ rubygems_version: 3.7.2
190
212
  specification_version: 4
191
213
  summary: AI gear for scouts
192
214
  test_files: []
@@ -1,91 +0,0 @@
1
- require 'scout/llm/utils'
2
- module LLM
3
- def self.process_inside(inside)
4
- header, content = inside.match(/([^\n]*)\n(.*)/).values_at 1, 2
5
- if header.empty?
6
- content
7
- else
8
- action, _sep, rest = header.partition /\s/
9
- case action
10
- when 'import'
11
- when 'cmd'
12
- title = rest.strip.empty? ? content : rest
13
- tag('file', title, CMD.cmd(content).read)
14
- when 'file'
15
- file = content
16
- title = rest.strip.empty? ? file : rest
17
- tag(action, title, Open.read(file))
18
- when 'directory'
19
- directory = content
20
- title = rest.strip.empty? ? directory : rest
21
- directory_content = Dir.glob(File.join(directory, '**/*')).collect do |file|
22
- file_title = Misc.path_relative_to(directory, file)
23
- tag('file', file_title, Open.read(file) )
24
- end * "\n"
25
- tag(action, title, directory_content )
26
- else
27
- tag(action, rest, content)
28
- end
29
- end
30
- end
31
-
32
- def self.parse(question, role = nil)
33
- role = :user if role.nil?
34
-
35
- if Array === question
36
- question.collect do |q|
37
- Hash === q ? q : {role: role, content: q}
38
- end
39
- else
40
- if m = question.match(/(.*?)\[\[(.*?)\]\](.*)/m)
41
- pre = m[1]
42
- inside = m[2]
43
- post = m[3]
44
- messages = parse(pre, role)
45
-
46
- messages = [{role: role, content: ''}] if messages.empty?
47
- messages.last[:content] += process_inside inside
48
-
49
- last = parse(post, messages.last[:role])
50
-
51
- messages.concat last
52
-
53
- messages
54
- elsif m = question.match(/(.*?)(```.*?```)(.*)/m)
55
- pre = m[1]
56
- inside = m[2]
57
- post = m[3]
58
- messages = parse(pre, role)
59
-
60
- messages = [{role: role, content: ''}] if messages.empty?
61
- messages.last[:content] += inside
62
-
63
- last = parse(post, messages.last[:role])
64
-
65
- if last.first[:role] == messages.last[:role]
66
- m = last.shift
67
- messages.last[:content] += m[:content]
68
- end
69
-
70
- messages.concat last
71
-
72
- messages
73
- else
74
- chunks = question.scan(/(.*?)^(\w+):(.*?)(?=^\w+:|\z)/m)
75
-
76
- if chunks.any?
77
- messages = []
78
- messages << {role: role, content: chunks.first.first} if chunks.first and not chunks.first.first.empty?
79
- chunks.collect do |pre,role,text|
80
- messages << {role: role, content: text.strip}
81
- end
82
- messages
83
- elsif question.strip.empty?
84
- []
85
- else
86
- [{role: role, content: question}]
87
- end
88
- end
89
- end
90
- end
91
- end