Module: Chat
- Extended by:
- Annotation
- Defined in:
- lib/scout/llm/chat/parse.rb,
lib/scout/llm/chat/process.rb,
lib/scout/llm/chat/annotation.rb,
lib/scout/llm/chat/process/clear.rb,
lib/scout/llm/chat/process/files.rb,
lib/scout/llm/chat/process/tools.rb,
lib/scout/llm/chat/process/options.rb
Class Method Summary collapse
- .associations(messages, kb = nil) ⇒ Object
- .clean(messages, role = 'skip') ⇒ Object
- .clear(messages, role = 'clear') ⇒ Object
- .content_tokens(message) ⇒ Object
- .files(messages, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats')) ⇒ Object
- .find_file(file, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats')) ⇒ Object
- .imports(messages, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats')) ⇒ Object
- .indiferent(messages) ⇒ Object
- .jobs(messages, original = nil) ⇒ Object
- .options(chat) ⇒ Object
- .parse(text, role = nil) ⇒ Object
- .print(chat) ⇒ Object
- .purge(chat) ⇒ Object
- .tag(tag, content, name = nil) ⇒ Object
- .tasks(messages, original = nil) ⇒ Object
- .tools(messages) ⇒ Object
Instance Method Summary collapse
- #answer ⇒ Object
- #ask ⇒ Object
- #assistant(content) ⇒ Object
- #association(name, path, options = {}) ⇒ Object
- #branch ⇒ Object
- #chat ⇒ Object
- #continue(file) ⇒ Object
-
#create_image(file) ⇒ Object
Image.
- #directory(directory) ⇒ Object
- #endpoint(value) ⇒ Object
- #file(file) ⇒ Object
- #final ⇒ Object
- #format(format) ⇒ Object
- #image(file) ⇒ Object
- #import(file) ⇒ Object
- #import_last(file) ⇒ Object
- #inline_job(step) ⇒ Object
- #inline_task(workflow, task_name, inputs = {}) ⇒ Object
- #job(step) ⇒ Object
- #json ⇒ Object
- #json_format(format) ⇒ Object
- #message(role, content) ⇒ Object
- #model(value) ⇒ Object
- #option(name, value) ⇒ Object
- #pdf(file) ⇒ Object
-
#print ⇒ Object
Reporting.
- #purge ⇒ Object
-
#save(path, force = true) ⇒ Object
Write and save.
- #shed ⇒ Object
- #system(content) ⇒ Object
- #tag(content, name = nil, tag = :file, role = :user) ⇒ Object
- #task(workflow, task_name, inputs = {}) ⇒ Object
- #tool(*parts) ⇒ Object
- #user(content) ⇒ Object
- #write(path, force = true) ⇒ Object
- #write_answer(path, force = true) ⇒ Object
Class Method Details
.associations(messages, kb = nil) ⇒ Object
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/scout/llm/chat/process/tools.rb', line 153 def self.associations(, kb = nil) tool_definitions = {} new = .collect do || if [:role] == 'association' name, path, * = content_tokens() kb ||= KnowledgeBase.new Scout.var.Agent.Chat.knowledge_base kb.register name, Path.setup(path), IndiferentHash.([:content]) tool_definitions.merge!(LLM.knowledge_base_tool_definition( kb, [name])) next elsif [:role] == 'clear_associations' tool_definitions = {} else end end.compact.flatten .replace new tool_definitions end |
.clean(messages, role = 'skip') ⇒ Object
16 17 18 19 20 21 |
# File 'lib/scout/llm/chat/process/clear.rb', line 16 def self.clean(, role = 'skip') .reject do || ((String === [:content]) && [:content].empty?) || [:role] == role end end |
.clear(messages, role = 'clear') ⇒ Object
2 3 4 5 6 7 8 9 10 11 12 13 14 |
# File 'lib/scout/llm/chat/process/clear.rb', line 2 def self.clear(, role = 'clear') new = [] .reverse.each do || if [:role].to_s == role break else new << end end new.reverse end |
.content_tokens(message) ⇒ Object
9 10 11 |
# File 'lib/scout/llm/chat/process.rb', line 9 def self.content_tokens() Shellwords.split([:content].strip) end |
.files(messages, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats')) ⇒ Object
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/scout/llm/chat/process/files.rb', line 62 def self.files(, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats')) .collect do || if [:role] == 'file' || [:role] == 'directory' file = [:content].to_s.strip found_file = find_file(file, original, caller_lib_dir) raise "File not found: #{file}" if found_file.nil? target = found_file if [:role] == 'directory' Path.setup target target.glob('**/*'). reject{|file| Open.directory?(file) }.collect{|file| files([{role: 'file', content: file}]) } else new = Chat.tag :file, Open.read(target), file {role: 'user', content: new} end elsif [:role] == 'pdf' || [:role] == 'image' file = [:content].to_s.strip found_file = find_file(file, original, caller_lib_dir) raise "File not found: #{file}" if found_file.nil? [:content] = found_file else end end.flatten end |
.find_file(file, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats')) ⇒ Object
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/scout/llm/chat/process/files.rb', line 17 def self.find_file(file, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats')) path = Scout.chats[file] original = original.find if Path === original if original relative = File.join(File.dirname(original), file) relative_lib = File.join(caller_lib_dir, file) if caller_lib_dir end if Open.exist?(file) file elsif Open.remote?(file) file elsif relative && Open.exist?(relative) relative elsif relative_lib && Open.exist?(relative_lib) relative_lib elsif path.exists? path end end |
.imports(messages, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats')) ⇒ Object
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/scout/llm/chat/process/files.rb', line 38 def self.imports(, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats')) .collect do || if [:role] == 'import' || [:role] == 'continue' || [:role] == 'last' file = [:content].to_s.strip found_file = find_file(file, original, caller_lib_dir) raise "Import not found: #{file}" if found_file.nil? new = LLM. Open.read(found_file) new = if [:role] == 'continue' [new.reject{|msg| msg[:content].nil? || msg[:content].strip.empty? }.last] elsif [:role] == 'last' [LLM.purge(new).reject{|msg| msg[:content].empty?}.last] else LLM.purge(new) end LLM.chat new, found_file else end end.flatten end |
.indiferent(messages) ⇒ Object
13 14 15 |
# File 'lib/scout/llm/chat/process.rb', line 13 def self.indiferent() .collect{|msg| IndiferentHash.setup msg } end |
.jobs(messages, original = nil) ⇒ Object
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/scout/llm/chat/process/tools.rb', line 46 def self.jobs(, original = nil) .collect do || if [:role] == 'job' || [:role] == 'inline_job' file = [:content].strip step = Step.load file id = step.short_path[0..39] id = id.gsub('/','-') if [:role] == 'inline_job' path = step.path path = path.find if Path === path {role: 'file', content: step.path} else function_name = step.full_task_name.sub('#', '-') function_name = step.task_name tool_call = { function: { name: function_name, arguments: step.provided_inputs }, id: id, } content = if step.done? Open.read(step.path) elsif step.error? step.exception end tool_output = { id: id, content: content } [ {role: 'function_call', content: tool_call.to_json}, {role: 'function_call_output', content: tool_output.to_json}, ] end else end end.flatten end |
.options(chat) ⇒ Object
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/scout/llm/chat/process/options.rb', line 2 def self.(chat) = IndiferentHash.setup({}) = IndiferentHash.setup({}) new = [] # Most options reset after an assistant reply, but not previous_response_id chat.each do |info| if Hash === info role = info[:role].to_s if %w(endpoint model backend agent).include? role.to_s [role] = info[:content] next elsif %w(persist).include? role.to_s [role] = info[:content] next elsif %w(previous_response_id).include? role.to_s [role] = info[:content] elsif %w(format).include? role.to_s format = info[:content] if Path.is_filename?(format) file = find_file(format) if file format = Open.json(file) end end [role] = format next end if role.to_s == 'option' key, _, value = info[:content].partition(" ") [key] = value next end if role.to_s == 'sticky_option' key, _, value = info[:content].partition(" ") [key] = value next end if role == 'assistant' .clear end end new << info end chat.replace new .merge end |
.parse(text, role = nil) ⇒ Object
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/scout/llm/chat/parse.rb', line 2 def self.parse(text, role = nil) default_role = "user" = [] current_role = role || default_role current_content = "" in_protected_block = false protected_block_type = nil protected_stack = [] role = default_role if role.nil? file_lines = text.split("\n") file_lines.each do |line| stripped = line.strip # Detect protected blocks if stripped.start_with?("```") if in_protected_block in_protected_block = false protected_block_type = nil current_content << "\n" << line unless line.strip.empty? else in_protected_block = true protected_block_type = :square current_content << "\n" << line unless line.strip.empty? end next elsif stripped.end_with?("]]") && in_protected_block && protected_block_type == :square in_protected_block = false protected_block_type = nil line = line.sub("]]", "") current_content << "\n" << line unless line.strip.empty? next elsif stripped.start_with?("[[") in_protected_block = true protected_block_type = :square line = line.sub("[[", "") current_content << "\n" << line unless line.strip.empty? next elsif stripped.end_with?("]]") && in_protected_block && protected_block_type == :square in_protected_block = false protected_block_type = nil line = line.sub("]]", "") current_content << "\n" << line unless line.strip.empty? next elsif stripped.match(/^.*:-- .* {{{/) in_protected_block = true protected_block_type = :square line = line.sub(/^.*:-- (.*) {{{.*/, '<cmd_output cmd="\1">') current_content << "\n" << line unless line.strip.empty? next elsif stripped.match(/^.*:--.* }}}/) && in_protected_block && protected_block_type == :square in_protected_block = false protected_block_type = nil line = line.sub(/^.*:-- .* }}}.*/, "</cmd_output>") current_content << "\n" << line unless line.strip.empty? next elsif in_protected_block if protected_block_type == :xml if stripped =~ %r{</(\w+)>} closing_tag = $1 if protected_stack.last == closing_tag protected_stack.pop end if protected_stack.empty? in_protected_block = false protected_block_type = nil end end end current_content << "\n" << line next end # XML-style tag handling (protected content) if stripped =~ /^<(\w+)(\s+[^>]*)?>/ && text =~ %r{</#{$1}>} tag = $1 protected_stack.push(tag) in_protected_block = true protected_block_type = :xml end # Match a new message header if line =~ /^([a-z0-9_]+):(.*)$/ role = $1 inline_content = $2.strip current_content = current_content.strip if current_content # Save current message if any << { role: current_role, content: current_content } if inline_content.empty? # Block message current_role = role current_content = "" else # Inline message + next block is default role << { role: role, content: inline_content } current_role = 'user' if role == 'previous_response_id' current_content = "" end else if current_content.nil? current_content = line else current_content += "\n" + line end end end # Final message << { role: current_role || default_role, content: current_content.strip } end |
.print(chat) ⇒ Object
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/scout/llm/chat/parse.rb', line 121 def self.print(chat) return chat if String === chat "\n" + chat.collect do || IndiferentHash.setup case [:content] when Hash, Array [:role].to_s + ":\n\n" + [:content].to_json when nil, '' [:role].to_s + ":" else if %w(option previous_response_id function_call function_call_output).include? [:role].to_s [:role].to_s + ": " + [:content].to_s else [:role].to_s + ":\n\n" + [:content].to_s end end end * "\n\n" end |
.purge(chat) ⇒ Object
23 24 25 26 27 28 |
# File 'lib/scout/llm/chat/process/clear.rb', line 23 def self.purge(chat) chat.reject do |msg| IndiferentHash.setup msg msg[:role].to_s == 'previous_response_id' end end |
.tag(tag, content, name = nil) ⇒ Object
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# File 'lib/scout/llm/chat/process/files.rb', line 2 def self.tag(tag, content, name = nil) if name <<-EOF.strip <#{tag} name="#{name}"> #{content} </#{tag}> EOF else <<-EOF.strip <#{tag}> #{content} </#{tag}> EOF end end |
.tasks(messages, original = nil) ⇒ Object
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/scout/llm/chat/process/tools.rb', line 2 def self.tasks(, original = nil) jobs = [] new = .collect do || if [:role] == 'task' || [:role] == 'inline_task' || [:role] == 'exec_task' info = [:content].strip workflow, task = info.split(" ").values_at 0, 1 = IndiferentHash. info jobname = .delete :jobname if String === workflow workflow = begin Kernel.const_get workflow rescue Workflow.require_workflow(workflow) end end job = workflow.job(task, jobname, ) jobs << job unless [:role] == 'exec_task' if [:role] == 'exec_task' begin {role: 'user', content: job.exec} rescue {role: 'exec_job', content: $!} end elsif [:role] == 'inline_task' {role: 'inline_job', content: job.path.find} else {role: 'job', content: job.path.find} end else end end.flatten Workflow.produce(jobs) new end |
.tools(messages) ⇒ Object
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/scout/llm/chat/process/tools.rb', line 94 def self.tools() tool_definitions = IndiferentHash.setup({}) new = .collect do || if [:role] == 'mcp' url, *tools = content_tokens() if url == 'stdio' command = tools.shift mcp_tool_definitions = LLM.mcp_tools(url, command: command, url: nil, type: :stdio) else mcp_tool_definitions = LLM.mcp_tools(url) end if tools.any? tools.each do |tool| tool_definitions[tool] = mcp_tool_definitions[tool] end else tool_definitions.merge!(mcp_tool_definitions) end next elsif [:role] == 'tool' workflow_name, task_name, *inputs = content_tokens() inputs = nil if inputs.empty? inputs = [] if inputs == ['none'] || inputs == ['noinputs'] if Open.remote? workflow_name require 'rbbt' require 'scout/offsite/ssh' require 'rbbt/workflow/remote_workflow' workflow = RemoteWorkflow.new workflow_name else workflow = Workflow.require_workflow workflow_name end if task_name definition = LLM.task_tool_definition workflow, task_name, inputs tool_definitions[task_name] = [workflow, definition] else tool_definitions.merge!(LLM.workflow_tools(workflow)) end next elsif [:role] == 'kb' knowledge_base_name, *databases = content_tokens() databases = nil if databases.empty? knowledge_base = KnowledgeBase.load knowledge_base_name knowledge_base_definition = LLM.knowledge_base_tool_definition(knowledge_base, databases) tool_definitions.merge!(knowledge_base_definition) next elsif [:role] == 'clear_tools' tool_definitions = {} else end end.compact.flatten .replace new tool_definitions end |
Instance Method Details
#answer ⇒ Object
162 163 164 |
# File 'lib/scout/llm/chat/annotation.rb', line 162 def answer final[:content] end |
#ask ⇒ Object
87 88 89 |
# File 'lib/scout/llm/chat/annotation.rb', line 87 def ask(...) LLM.ask(LLM.chat(self), ...) end |
#assistant(content) ⇒ Object
17 18 19 |
# File 'lib/scout/llm/chat/annotation.rb', line 17 def assistant(content) (:assistant, content) end |
#association(name, path, options = {}) ⇒ Object
76 77 78 79 80 |
# File 'lib/scout/llm/chat/annotation.rb', line 76 def association(name, path, = {}) = IndiferentHash. content = [name, path, ]*" " (:association, name) end |
#branch ⇒ Object
124 125 126 |
# File 'lib/scout/llm/chat/annotation.rb', line 124 def branch self.annotate self.dup end |
#chat ⇒ Object
91 92 93 94 95 96 97 98 99 100 |
# File 'lib/scout/llm/chat/annotation.rb', line 91 def chat(...) response = ask(...) if Array === response current_chat.concat(response) final(response) else current_chat.push({role: :assistant, content: response}) response end end |
#continue(file) ⇒ Object
42 43 44 |
# File 'lib/scout/llm/chat/annotation.rb', line 42 def continue(file) (:continue, file) end |
#create_image(file) ⇒ Object
Image
196 197 198 199 |
# File 'lib/scout/llm/chat/annotation.rb', line 196 def create_image(file, ...) base64_image = LLM.image(LLM.chat(self), ...) Open.write(file, Base64.decode(file_content), mode: 'wb') end |
#directory(directory) ⇒ Object
38 39 40 |
# File 'lib/scout/llm/chat/annotation.rb', line 38 def directory(directory) (:directory, directory) end |
#endpoint(value) ⇒ Object
132 133 134 |
# File 'lib/scout/llm/chat/annotation.rb', line 132 def endpoint(value) option :endpoint, value end |
#file(file) ⇒ Object
29 30 31 |
# File 'lib/scout/llm/chat/annotation.rb', line 29 def file(file) (:file, file) end |
#final ⇒ Object
150 151 152 |
# File 'lib/scout/llm/chat/annotation.rb', line 150 def final LLM.purge(self).last end |
#format(format) ⇒ Object
46 47 48 |
# File 'lib/scout/llm/chat/annotation.rb', line 46 def format(format) (:format, format) end |
#image(file) ⇒ Object
140 141 142 |
# File 'lib/scout/llm/chat/annotation.rb', line 140 def image(file) self. :image, file end |
#import(file) ⇒ Object
21 22 23 |
# File 'lib/scout/llm/chat/annotation.rb', line 21 def import(file) (:import, file) end |
#import_last(file) ⇒ Object
25 26 27 |
# File 'lib/scout/llm/chat/annotation.rb', line 25 def import_last(file) (:last, file) end |
#inline_job(step) ⇒ Object
71 72 73 |
# File 'lib/scout/llm/chat/annotation.rb', line 71 def inline_job(step) (:inline_job, step.path) end |
#inline_task(workflow, task_name, inputs = {}) ⇒ Object
61 62 63 64 65 |
# File 'lib/scout/llm/chat/annotation.rb', line 61 def inline_task(workflow, task_name, inputs = {}) input_str = IndiferentHash. inputs content = [workflow, task_name, input_str]*" " (:inline_task, content) end |
#job(step) ⇒ Object
67 68 69 |
# File 'lib/scout/llm/chat/annotation.rb', line 67 def job(step) (:job, step.path) end |
#json ⇒ Object
102 103 104 105 106 107 108 109 110 111 |
# File 'lib/scout/llm/chat/annotation.rb', line 102 def json(...) self.format :json output = ask(...) obj = JSON.parse output if (Hash === obj) and obj.keys == ['content'] obj['content'] else obj end end |
#json_format(format) ⇒ Object
113 114 115 116 117 118 119 120 121 122 |
# File 'lib/scout/llm/chat/annotation.rb', line 113 def json_format(format, ...) self.format format output = ask(...) obj = JSON.parse output if (Hash === obj) and obj.keys == ['content'] obj['content'] else obj end end |
#message(role, content) ⇒ Object
5 6 7 |
# File 'lib/scout/llm/chat/annotation.rb', line 5 def (role, content) self.append({role: role.to_s, content: content}) end |
#model(value) ⇒ Object
136 137 138 |
# File 'lib/scout/llm/chat/annotation.rb', line 136 def model(value) option :model, value end |
#option(name, value) ⇒ Object
128 129 130 |
# File 'lib/scout/llm/chat/annotation.rb', line 128 def option(name, value) self. 'option', [name, value] * " " end |
#pdf(file) ⇒ Object
33 34 35 |
# File 'lib/scout/llm/chat/annotation.rb', line 33 def pdf(file) (:pdf, file) end |
#print ⇒ Object
Reporting
146 147 148 |
# File 'lib/scout/llm/chat/annotation.rb', line 146 def print LLM.print LLM.chat(self) end |
#purge ⇒ Object
154 155 156 |
# File 'lib/scout/llm/chat/annotation.rb', line 154 def purge Chat.setup(LLM.purge(self)) end |
#save(path, force = true) ⇒ Object
Write and save
168 169 170 171 172 173 174 175 |
# File 'lib/scout/llm/chat/annotation.rb', line 168 def save(path, force = true) path = path.to_s if Symbol === path if not (Open.exists?(path) || Path === path || Path.located?(path)) path = Scout.chats.find[path] end return if Open.exists?(path) && ! force Open.write path, LLM.print(self) end |
#shed ⇒ Object
158 159 160 |
# File 'lib/scout/llm/chat/annotation.rb', line 158 def shed self.annotate [final] end |
#system(content) ⇒ Object
13 14 15 |
# File 'lib/scout/llm/chat/annotation.rb', line 13 def system(content) (:system, content) end |
#tag(content, name = nil, tag = :file, role = :user) ⇒ Object
82 83 84 |
# File 'lib/scout/llm/chat/annotation.rb', line 82 def tag(content, name=nil, tag=:file, role=:user) self. role, LLM.tag(tag, content, name) end |
#task(workflow, task_name, inputs = {}) ⇒ Object
55 56 57 58 59 |
# File 'lib/scout/llm/chat/annotation.rb', line 55 def task(workflow, task_name, inputs = {}) input_str = IndiferentHash. inputs content = [workflow, task_name, input_str]*" " (:task, content) end |
#tool(*parts) ⇒ Object
50 51 52 53 |
# File 'lib/scout/llm/chat/annotation.rb', line 50 def tool(*parts) content = parts * "\n" (:tool, content) end |
#user(content) ⇒ Object
9 10 11 |
# File 'lib/scout/llm/chat/annotation.rb', line 9 def user(content) (:user, content) end |
#write(path, force = true) ⇒ Object
177 178 179 180 181 182 183 184 |
# File 'lib/scout/llm/chat/annotation.rb', line 177 def write(path, force = true) path = path.to_s if Symbol === path if not (Open.exists?(path) || Path === path || Path.located?(path)) path = Scout.chats.find[path] end return if Open.exists?(path) && ! force Open.write path, self.print end |
#write_answer(path, force = true) ⇒ Object
186 187 188 189 190 191 192 193 |
# File 'lib/scout/llm/chat/annotation.rb', line 186 def write_answer(path, force = true) path = path.to_s if Symbol === path if not (Open.exists?(path) || Path === path || Path.located?(path)) path = Scout.chats.find[path] end return if Open.exists?(path) && ! force Open.write path, self.answer end |