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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a13a09db3a006c0a8a2c5bc2487d5248c7b599ea9013924ab588d25540e8848d
4
- data.tar.gz: 73f87320f05289b9818aef772b205b39745ee5553733664614988e3d45c66c64
3
+ metadata.gz: 57e9757e8effe1d42977eb5b9c5b5f3ee9d3e4603edb845682a35b4f041f3185
4
+ data.tar.gz: 9613eabf0c2cda6a10fa286bffe437151e8619713808fb23a030fabbf1c16fa3
5
5
  SHA512:
6
- metadata.gz: 72bea9d93dc2d374580c763688d9a2b5e7a576ca5806e5169f4f88d0640a7fe29e9a7013cf55c0d89763699c630c662e918ebb9b9033948be6a70d2491eb7575
7
- data.tar.gz: dfe39bc0d5f624df7ecced479f379902492088555bd6afbe140e89b94dadaa6e8fed82b8c04d6369ca8f8dac5014b8c3cb0a0d64657f6e5d37b4306435f8bbf1
6
+ metadata.gz: 3dd57a37318b2392655a318d6c947e8e8215bde51c46c516b3378c8e72c7475c00cd0b462a59a36ffe456fbe3099fc5b065ad8ce3929fdf186ed49fad0e9f3bf
7
+ data.tar.gz: 2b9e7be0db65387f18d9ce5340ec81789e852e26b67be076c10d0e6f2def94b7facf34cf64e41089bd911cb8cb25c440ed778d7f30f38ab38fa6da78dc21ac1c
data/.vimproject CHANGED
@@ -6,6 +6,14 @@ scout-ai=$PWD filter="*.rb *.rake Rakefile *.rdoc *.R *.sh *.js *.haml *.sass *.
6
6
  scout-ai
7
7
  }
8
8
  chats=chats filter="*"{
9
+ parse
10
+
11
+ test_tool
12
+
13
+ ask_agent
14
+
15
+ test_ollama_tool
16
+
9
17
  test_github
10
18
 
11
19
  test_stdio
@@ -91,7 +99,6 @@ scout-ai=$PWD filter="*.rb *.rake Rakefile *.rdoc *.R *.sh *.js *.haml *.sass *.
91
99
  scout=scout{
92
100
  llm=llm{
93
101
  utils.rb
94
- parse.rb
95
102
  tools.rb
96
103
  tools=tools{
97
104
  mcp.rb
@@ -100,11 +107,22 @@ scout-ai=$PWD filter="*.rb *.rake Rakefile *.rdoc *.R *.sh *.js *.haml *.sass *.
100
107
  call.rb
101
108
  }
102
109
  chat.rb
110
+ chat=chat{
111
+ annotation.rb
112
+ parse.rb
113
+ process.rb
114
+ process=process{
115
+ tools.rb
116
+ files.rb
117
+ clear.rb
118
+ options.rb
119
+ }
120
+ }
103
121
 
104
122
  backends=backends{
105
123
  openai.rb
106
- anthropic.rb
107
124
  responses.rb
125
+ anthropic.rb
108
126
  ollama.rb
109
127
  bedrock.rb
110
128
  openwebui.rb
data/Rakefile CHANGED
@@ -18,6 +18,7 @@ Juwelier::Tasks.new do |gem|
18
18
  # dependencies defined in Gemfile
19
19
  gem.add_runtime_dependency 'scout-rig', '>= 0'
20
20
  gem.add_runtime_dependency 'ruby-openai', '>= 0'
21
+ gem.add_runtime_dependency 'ollama-ai', '>= 0'
21
22
  gem.add_runtime_dependency 'ruby-mcp-client', '>= 0'
22
23
  end
23
24
  Juwelier::RubygemsDotOrgTasks.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.1
1
+ 1.1.1
data/bin/scout-ai CHANGED
@@ -2,6 +2,52 @@
2
2
 
3
3
  $LOAD_PATH.unshift File.join(__dir__, '../lib')
4
4
 
5
+ if _i = ARGV.index("--log")
6
+ require 'scout/log'
7
+ log = ARGV[_i+1]
8
+ Log.severity = log.to_i
9
+ ARGV.delete "--log"
10
+ ARGV.delete log
11
+ end
12
+
13
+ dev_dir = nil
14
+ if _i = ARGV.index("--dev")
15
+ dev_dir = ARGV[_i+1]
16
+ ARGV.delete "--dev"
17
+ ARGV.delete dev_dir
18
+ end
19
+
20
+ if dev_dir.nil?
21
+ _s = nil
22
+ ARGV.each_with_index do |s,i|
23
+ if s.match(/^--dev(?:=(.*))?/)
24
+ dev_dir = $1
25
+ _s = s
26
+ next
27
+ end
28
+ end
29
+ ARGV.delete _s if _s
30
+ end
31
+
32
+ if dev_dir.nil? && ENV["SCOUT_DEV"]
33
+ dev_dir = ENV["SCOUT_DEV"]
34
+ ARGV.delete "--dev"
35
+ ARGV.delete dev_dir
36
+ end
37
+
38
+ if dev_dir
39
+ ['scout-*/lib'].each do |pattern|
40
+ Dir.glob(File.join(File.expand_path(dev_dir), pattern)).each do |f|
41
+ $LOAD_PATH.unshift f
42
+ end
43
+ end
44
+ ['rbbt-*/lib'].each do |pattern|
45
+ Dir.glob(File.join(File.expand_path(dev_dir), pattern)).each do |f|
46
+ $LOAD_PATH.unshift f
47
+ end
48
+ end
49
+ end
50
+
5
51
  require 'scout-ai'
6
52
 
7
53
  load Scout.bin.scout.find
@@ -9,6 +9,8 @@ module LLM
9
9
  (@current_chat || start_chat).annotate chat unless Chat === chat
10
10
  @current_chat = chat
11
11
  else
12
+ start_chat = self.start_chat
13
+ Chat.setup(start_chat) unless Chat === start_chat
12
14
  @current_chat = start_chat.branch
13
15
  end
14
16
  end
@@ -25,14 +27,9 @@ module LLM
25
27
  self.ask(current_chat, ...)
26
28
  end
27
29
 
28
- def chat(model = nil, options = {})
29
- new = self.ask(current_chat, model, options.merge(return_messages: true))
30
- current_chat.concat(new)
31
- new.last['content']
32
- end
33
30
 
34
- def chat(model = nil, options = {})
35
- response = ask(current_chat, model, options.merge(return_messages: true))
31
+ def chat(options = {})
32
+ response = ask(current_chat, options.merge(return_messages: true))
36
33
  if Array === response
37
34
  current_chat.concat(response)
38
35
  current_chat.answer
@@ -7,6 +7,13 @@ module LLM
7
7
 
8
8
  block ||= Proc.new do |name, parameters|
9
9
  message = parameters[:message]
10
+ new_conversation = parameters[:new_conversation]
11
+ Log.medium "Delegated to #{agent}: " + Log.fingerprint(message)
12
+ if new_conversation
13
+ agent.start
14
+ else
15
+ agent.purge
16
+ end
10
17
  agent.user message
11
18
  agent.chat
12
19
  end
@@ -15,6 +22,11 @@ module LLM
15
22
  message: {
16
23
  "type": :string,
17
24
  "description": "Message to pass to the agent"
25
+ },
26
+ new_conversation: {
27
+ "type": :boolean,
28
+ "description": "Erase conversation history and start a new conversation with this message",
29
+ "default": false
18
30
  }
19
31
  }
20
32
 
@@ -49,7 +49,7 @@ You have access to the following databases associating entities:
49
49
  end
50
50
 
51
51
  # function: takes an array of messages and calls LLM.ask with them
52
- def ask(messages, model = nil, options = {})
52
+ def ask(messages, options = {})
53
53
  messages = [messages] unless messages.is_a? Array
54
54
  model ||= @model if model
55
55
 
@@ -87,7 +87,7 @@ You have access to the following databases associating entities:
87
87
 
88
88
  workflow = Workflow.require_workflow workflow_path if workflow_path.exists?
89
89
  knowledge_base = KnowledgeBase.new knowledge_base_path if knowledge_base_path.exists?
90
- chat = LLM.chat chat_path if chat_path.exists?
90
+ chat = Chat.setup LLM.chat(chat_path.find) if chat_path.exists?
91
91
 
92
92
  LLM::Agent.new workflow: workflow, knowledge_base: knowledge_base, start_chat: chat
93
93
  end
data/lib/scout/llm/ask.rb CHANGED
@@ -34,14 +34,26 @@ module LLM
34
34
 
35
35
  endpoint, persist = IndiferentHash.process_options options, :endpoint, :persist, persist: true
36
36
 
37
- endpoint ||= Scout::Config.get :endpoint, :ask, :llm, env: 'ASK_ENDPOINT,LLM_ENDPOINT'
37
+ endpoint ||= Scout::Config.get :endpoint, :ask, :llm, env: 'ASK_ENDPOINT,LLM_ENDPOINT,ENDPOINT,LLM,ASK'
38
38
  if endpoint && Scout.etc.AI[endpoint].exists?
39
39
  options = IndiferentHash.add_defaults options, Scout.etc.AI[endpoint].yaml
40
40
  elsif endpoint && endpoint != ""
41
41
  raise "Endpoint not found #{endpoint}"
42
42
  end
43
43
 
44
- Persist.persist(endpoint, :json, prefix: "LLM ask", other: options.merge(messages: messages), persist: persist) do
44
+ if options[:backend].to_s == 'responses'
45
+ messages = Chat.clear(messages, 'previous_response_id')
46
+ else
47
+ messages = Chat.clean(messages, 'previous_response_id')
48
+ options.delete :previous_response_id
49
+ end
50
+
51
+ Log.high Log.color :green, "Asking #{endpoint || 'client'}: #{options[:previous_response_id]}\n" + LLM.print(messages)
52
+ tools = options[:tools]
53
+ Log.high "Tools: #{Log.fingerprint tools.keys}}" if tools
54
+ Log.debug "#{Log.fingerprint tools}}" if tools
55
+
56
+ res = Persist.persist(endpoint, :json, prefix: "LLM ask", other: options.merge(messages: messages), persist: persist) do
45
57
  backend = IndiferentHash.process_options options, :backend
46
58
  backend ||= Scout::Config.get :backend, :ask, :llm, env: 'ASK_BACKEND,LLM_BACKEND', default: :openai
47
59
 
@@ -71,6 +83,10 @@ module LLM
71
83
  raise "Unknown backend: #{backend}"
72
84
  end
73
85
  end
86
+
87
+ Log.high Log.color :blue, "Response:\n" + LLM.print(res)
88
+
89
+ res
74
90
  end
75
91
 
76
92
  def self.workflow_ask(workflow, question, options = {})
@@ -1,5 +1,3 @@
1
- require_relative '../parse'
2
- require_relative '../tools'
3
1
  require_relative '../chat'
4
2
 
5
3
  module LLM
@@ -1,7 +1,4 @@
1
1
  require 'ollama-ai'
2
- require_relative '../parse'
3
- require_relative '../tools'
4
- require_relative '../utils'
5
2
  require_relative '../chat'
6
3
 
7
4
  module LLM
@@ -81,10 +78,11 @@ module LLM
81
78
  tools.merge!(LLM.associations messages)
82
79
 
83
80
  if tools.any?
84
- parameters[:tools] = tools.values.collect{|obj,definition| Hash === obj ? obj : definition}
81
+ parameters[:tools] = LLM.tool_definitions_to_ollama tools
85
82
  end
86
83
 
87
- Log.low "Calling client with parameters #{Log.fingerprint parameters}\n#{LLM.print messages}"
84
+ Log.low "Calling ollama #{url}: #{Log.fingerprint(parameters.except(:tools))}}"
85
+ Log.medium "Tools: #{Log.fingerprint tools.keys}}" if tools
88
86
 
89
87
  parameters[:messages] = LLM.tools_to_ollama messages
90
88
 
@@ -93,7 +91,9 @@ module LLM
93
91
  response = self.process_response client.chat(parameters), tools, &block
94
92
 
95
93
  res = if response.last[:role] == 'function_call_output'
96
- response + self.ask(messages + response, original_options.except(:tool_choice).merge(return_messages: true, tools: tools), &block)
94
+ #response + self.ask(messages + response, original_options.except(:tool_choice).merge(return_messages: true, tools: tools), &block)
95
+ # This version seems to keep the original message from getting forgotten
96
+ response + self.ask(response + messages, original_options.except(:tool_choice).merge(return_messages: true, tools: tools), &block)
97
97
  else
98
98
  response
99
99
  end
@@ -58,8 +58,10 @@ module LLM
58
58
  model ||= LLM.get_url_config(:model, url, :openai_ask, :ask, :openai, env: 'OPENAI_MODEL', default: "gpt-4.1")
59
59
  end
60
60
 
61
- case format.to_sym
62
- when :json, :json_object
61
+ case format
62
+ when Hash
63
+ options[:response_format] = format
64
+ when 'json', 'json_object', :json, :json_object
63
65
  options[:response_format] = {type: 'json_object'}
64
66
  else
65
67
  options[:response_format] = {type: format}
@@ -84,12 +86,13 @@ module LLM
84
86
  tools.merge!(LLM.associations messages)
85
87
 
86
88
  if tools.any?
87
- parameters[:tools] = tools.values.collect{|obj,definition| Hash === obj ? obj : definition}
89
+ parameters[:tools] = LLM.tool_definitions_to_openai tools
88
90
  end
89
91
 
90
92
  messages = self.process_input messages
91
93
 
92
- Log.low "Calling openai #{url}: #{Log.fingerprint parameters}}"
94
+ Log.debug "Calling openai #{url}: #{Log.fingerprint(parameters.except(:tools))}}"
95
+ Log.high "Tools: #{Log.fingerprint tools.keys}}" if tools
93
96
 
94
97
  parameters[:messages] = LLM.tools_to_openai messages
95
98
 
@@ -1,9 +1,6 @@
1
- require 'scout'
2
1
  require 'openai'
3
2
  require 'rest-client'
4
- require_relative '../parse'
5
- require_relative '../tools'
6
- require_relative '../utils'
3
+ require_relative '../chat'
7
4
 
8
5
  module LLM
9
6
  module OpenWebUI
@@ -1,7 +1,5 @@
1
1
  require 'scout'
2
- require 'openai'
3
- require_relative '../parse'
4
- require_relative '../tools'
2
+ require_relative '../chat'
5
3
 
6
4
  module LLM
7
5
  module Relay
@@ -45,13 +45,15 @@ module LLM
45
45
  #end
46
46
 
47
47
  def self.tools_to_responses(messages)
48
+ last_id = nil
48
49
  messages.collect do |message|
49
50
  if message[:role] == 'function_call'
50
51
  info = JSON.parse(message[:content])
51
52
  IndiferentHash.setup info
52
53
  name = info[:name] || IndiferentHash.dig(info,:function, :name)
53
54
  IndiferentHash.setup info
54
- id = info[:id].sub(/^fc_/, '')
55
+ id = last_id = info[:id] || "fc_#{rand(1000).to_s}"
56
+ id = id.sub(/^fc_/, '')
55
57
  IndiferentHash.setup({
56
58
  "type" => "function_call",
57
59
  "status" => "completed",
@@ -62,7 +64,8 @@ module LLM
62
64
  elsif message[:role] == 'function_call_output'
63
65
  info = JSON.parse(message[:content])
64
66
  IndiferentHash.setup info
65
- id = info[:id].sub(/^fc_/, '')
67
+ id = info[:id] || last_id
68
+ id = id.sub(/^fc_/, '')
66
69
  { # append result message
67
70
  "type" => "function_call_output",
68
71
  "output" => info[:content],
@@ -103,35 +106,44 @@ module LLM
103
106
  def self.process_input(messages)
104
107
  messages = self.tools_to_responses messages
105
108
 
106
- messages.collect do |message|
107
- IndiferentHash.setup(message)
108
- if message[:role] == 'image'
109
+ res = []
110
+ messages.each do |message|
111
+ message = IndiferentHash.setup(message)
112
+
113
+ role = message[:role]
114
+
115
+ case role.to_s
116
+ when 'image'
109
117
  path = message[:content]
110
118
  path = LLM.find_file path
111
119
  if Open.remote?(path)
112
- {role: :user, content: {type: :input_image, image_url: path }}
120
+ res << {role: :user, content: {type: :input_image, image_url: path }}
113
121
  elsif Open.exists?(path)
114
122
  path = self.encode_image(path)
115
- {role: :user, content: [{type: :input_image, image_url: path }]}
123
+ res << {role: :user, content: [{type: :input_image, image_url: path }]}
116
124
  else
117
- raise
125
+ raise "Image does not exist in #{path}"
118
126
  end
119
- elsif message[:role] == 'pdf'
127
+ when 'pdf'
120
128
  path = original_path = message[:content]
121
129
  if Open.remote?(path)
122
- {role: :user, content: {type: :input_file, file_url: path }}
130
+ res << {role: :user, content: {type: :input_file, file_url: path }}
123
131
  elsif Open.exists?(path)
124
132
  data = self.encode_pdf(path)
125
- {role: :user, content: [{type: :input_file, file_data: data, filename: File.basename(path) }]}
133
+ res << {role: :user, content: [{type: :input_file, file_data: data, filename: File.basename(path) }]}
126
134
  else
127
- raise
135
+ raise "PDF does not exist in #{path}"
128
136
  end
129
- elsif message[:role] == 'websearch'
130
- {role: :tool, content: {type: "web_search_preview"} }
137
+ when 'websearch'
138
+ res << {role: :tool, content: {type: "web_search_preview"} }
139
+ when 'previous_response_id'
140
+ res = []
131
141
  else
132
- message
142
+ res << message
133
143
  end
134
- end.flatten
144
+ end
145
+
146
+ res
135
147
  end
136
148
 
137
149
  def self.process_format(format)
@@ -187,6 +199,7 @@ module LLM
187
199
 
188
200
  messages = LLM.chat(question)
189
201
  options = options.merge LLM.options messages
202
+
190
203
 
191
204
  client, url, key, model, log_errors, return_messages, format, websearch, previous_response_id, tools, = IndiferentHash.process_options options,
192
205
  :client, :url, :key, :model, :log_errors, :return_messages, :format, :websearch, :previous_response_id, :tools,
@@ -232,13 +245,16 @@ module LLM
232
245
  tools.merge!(LLM.associations messages)
233
246
 
234
247
  if tools.any?
235
- parameters[:tools] = tools.values.collect{|obj,definition| Hash === obj ? obj : definition}
248
+ parameters[:tools] = LLM.tool_definitions_to_reponses tools
236
249
  end
237
250
 
238
251
  parameters['previous_response_id'] = previous_response_id if String === previous_response_id
239
- Log.low "Calling client with parameters #{Log.fingerprint parameters}\n#{LLM.print messages}"
252
+
253
+ Log.low "Calling responses #{url}: #{Log.fingerprint(parameters.except(:tools))}}"
254
+ Log.medium "Tools: #{Log.fingerprint tools.keys}}" if tools
240
255
 
241
256
  messages = self.process_input messages
257
+
242
258
  input = []
243
259
  messages.each do |message|
244
260
  parameters[:tools] ||= []
@@ -0,0 +1,195 @@
1
+ require 'scout/annotation'
2
+ module Chat
3
+ extend Annotation
4
+
5
+ def message(role, content)
6
+ self.append({role: role.to_s, content: content})
7
+ end
8
+
9
+ def user(content)
10
+ message(:user, content)
11
+ end
12
+
13
+ def system(content)
14
+ message(:system, content)
15
+ end
16
+
17
+ def assistant(content)
18
+ message(:assistant, content)
19
+ end
20
+
21
+ def import(file)
22
+ message(:import, file)
23
+ end
24
+
25
+ def import_last(file)
26
+ message(:last, file)
27
+ end
28
+
29
+ def file(file)
30
+ message(:file, file)
31
+ end
32
+
33
+ def directory(directory)
34
+ message(:directory, directory)
35
+ end
36
+
37
+ def continue(file)
38
+ message(:continue, file)
39
+ end
40
+
41
+ def format(format)
42
+ message(:format, format)
43
+ end
44
+
45
+ def tool(*parts)
46
+ content = parts * "\n"
47
+ message(:tool, content)
48
+ end
49
+
50
+ def task(workflow, task_name, inputs = {})
51
+ input_str = IndiferentHash.print_options inputs
52
+ content = [workflow, task_name, input_str]*" "
53
+ message(:task, content)
54
+ end
55
+
56
+ def inline_task(workflow, task_name, inputs = {})
57
+ input_str = IndiferentHash.print_options inputs
58
+ content = [workflow, task_name, input_str]*" "
59
+ message(:inline_task, content)
60
+ end
61
+
62
+ def job(step)
63
+ message(:job, step.path)
64
+ end
65
+
66
+ def inline_job(step)
67
+ message(:inline_job, step.path)
68
+ end
69
+
70
+
71
+ def association(name, path, options = {})
72
+ options_str = IndiferentHash.print_options options
73
+ content = [name, path, options_str]*" "
74
+ message(:association, name)
75
+ end
76
+
77
+ def tag(content, name=nil, tag=:file, role=:user)
78
+ self.message role, LLM.tag(tag, content, name)
79
+ end
80
+
81
+
82
+ def ask(...)
83
+ LLM.ask(LLM.chat(self), ...)
84
+ end
85
+
86
+ def chat(...)
87
+ response = ask(...)
88
+ if Array === response
89
+ current_chat.concat(response)
90
+ final(response)
91
+ else
92
+ current_chat.push({role: :assistant, content: response})
93
+ response
94
+ end
95
+ end
96
+
97
+ def json(...)
98
+ self.format :json
99
+ output = ask(...)
100
+ obj = JSON.parse output
101
+ if (Hash === obj) and obj.keys == ['content']
102
+ obj['content']
103
+ else
104
+ obj
105
+ end
106
+ end
107
+
108
+ def json_format(format, ...)
109
+ self.format format
110
+ output = ask(...)
111
+ obj = JSON.parse output
112
+ if (Hash === obj) and obj.keys == ['content']
113
+ obj['content']
114
+ else
115
+ obj
116
+ end
117
+ end
118
+
119
+ def branch
120
+ self.annotate self.dup
121
+ end
122
+
123
+ def option(name, value)
124
+ self.message 'option', [name, value] * " "
125
+ end
126
+
127
+ def endpoint(value)
128
+ option :endpoint, value
129
+ end
130
+
131
+ def model(value)
132
+ option :model, value
133
+ end
134
+
135
+ def image(file)
136
+ self.message :image, file
137
+ end
138
+
139
+ # Reporting
140
+
141
+ def print
142
+ LLM.print LLM.chat(self)
143
+ end
144
+
145
+ def final
146
+ LLM.purge(self).last
147
+ end
148
+
149
+ def purge
150
+ Chat.setup(LLM.purge(self))
151
+ end
152
+
153
+ def shed
154
+ self.annotate [final]
155
+ end
156
+
157
+ def answer
158
+ final[:content]
159
+ end
160
+
161
+ # Write and save
162
+
163
+ def save(path, force = true)
164
+ path = path.to_s if Symbol === path
165
+ if not (Open.exists?(path) || Path === path || Path.located?(path))
166
+ path = Scout.chats.find[path]
167
+ end
168
+ return if Open.exists?(path) && ! force
169
+ Open.write path, LLM.print(self)
170
+ end
171
+
172
+ def write(path, force = true)
173
+ path = path.to_s if Symbol === path
174
+ if not (Open.exists?(path) || Path === path || Path.located?(path))
175
+ path = Scout.chats.find[path]
176
+ end
177
+ return if Open.exists?(path) && ! force
178
+ Open.write path, self.print
179
+ end
180
+
181
+ def write_answer(path, force = true)
182
+ path = path.to_s if Symbol === path
183
+ if not (Open.exists?(path) || Path === path || Path.located?(path))
184
+ path = Scout.chats.find[path]
185
+ end
186
+ return if Open.exists?(path) && ! force
187
+ Open.write path, self.answer
188
+ end
189
+
190
+ # Image
191
+ def create_image(file, ...)
192
+ base64_image = LLM.image(LLM.chat(self), ...)
193
+ Open.write(file, Base64.decode(file_content), mode: 'wb')
194
+ end
195
+ end