scout-ai 1.1.0 → 1.1.2

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.
@@ -0,0 +1,96 @@
1
+ module Chat
2
+ def self.tag(tag, content, name = nil)
3
+ if name
4
+ <<-EOF.strip
5
+ <#{tag} name="#{name}">
6
+ #{content}
7
+ </#{tag}>
8
+ EOF
9
+ else
10
+ <<-EOF.strip
11
+ <#{tag}>
12
+ #{content}
13
+ </#{tag}>
14
+ EOF
15
+ end
16
+ end
17
+ def self.find_file(file, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats'))
18
+ path = Scout.chats[file]
19
+ original = original.find if Path === original
20
+ if original
21
+ relative = File.join(File.dirname(original), file)
22
+ relative_lib = File.join(caller_lib_dir, file) if caller_lib_dir
23
+ end
24
+
25
+ if Open.exist?(file)
26
+ file
27
+ elsif Open.remote?(file)
28
+ file
29
+ elsif relative && Open.exist?(relative)
30
+ relative
31
+ elsif relative_lib && Open.exist?(relative_lib)
32
+ relative_lib
33
+ elsif path.exists?
34
+ path
35
+ end
36
+ end
37
+
38
+ def self.imports(messages, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats'))
39
+ messages.collect do |message|
40
+ if message[:role] == 'import' || message[:role] == 'continue' || message[:role] == 'last'
41
+ file = message[:content].to_s.strip
42
+ found_file = find_file(file, original, caller_lib_dir)
43
+ raise "Import not found: #{file}" if found_file.nil?
44
+
45
+ new = LLM.messages Open.read(found_file)
46
+
47
+ new = if message[:role] == 'continue'
48
+ [new.reject{|msg| msg[:content].nil? || msg[:content].strip.empty? }.last]
49
+ elsif message[:role] == 'last'
50
+ [LLM.purge(new).reject{|msg| msg[:content].empty?}.last]
51
+ else
52
+ LLM.purge(new)
53
+ end
54
+
55
+ LLM.chat new, found_file
56
+ else
57
+ message
58
+ end
59
+ end.flatten
60
+ end
61
+
62
+ def self.files(messages, original = nil, caller_lib_dir = Path.caller_lib_dir(nil, 'chats'))
63
+ messages.collect do |message|
64
+ if message[:role] == 'file' || message[:role] == 'directory'
65
+ file = message[:content].to_s.strip
66
+ found_file = find_file(file, original, caller_lib_dir)
67
+ raise "File not found: #{file}" if found_file.nil?
68
+
69
+ target = found_file
70
+
71
+ if message[:role] == 'directory'
72
+ Path.setup target
73
+ target.glob('**/*').
74
+ reject{|file|
75
+ Open.directory?(file)
76
+ }.collect{|file|
77
+ files([{role: 'file', content: file}])
78
+ }
79
+ else
80
+ new = Chat.tag :file, Open.read(target), file
81
+ {role: 'user', content: new}
82
+ end
83
+ elsif message[:role] == 'pdf' || message[:role] == 'image'
84
+ file = message[:content].to_s.strip
85
+ found_file = find_file(file, original, caller_lib_dir)
86
+ raise "File not found: #{file}" if found_file.nil?
87
+
88
+ message[:content] = found_file
89
+ message
90
+ else
91
+ message
92
+ end
93
+ end.flatten
94
+ end
95
+
96
+ end
@@ -0,0 +1,52 @@
1
+ module Chat
2
+ def self.options(chat)
3
+ options = IndiferentHash.setup({})
4
+ sticky_options = IndiferentHash.setup({})
5
+ new = []
6
+
7
+ # Most options reset after an assistant reply, but not previous_response_id
8
+ chat.each do |info|
9
+ if Hash === info
10
+ role = info[:role].to_s
11
+ if %w(endpoint model backend agent).include? role.to_s
12
+ sticky_options[role] = info[:content]
13
+ next
14
+ elsif %w(persist).include? role.to_s
15
+ options[role] = info[:content]
16
+ next
17
+ elsif %w(previous_response_id).include? role.to_s
18
+ sticky_options[role] = info[:content]
19
+ elsif %w(format).include? role.to_s
20
+ format = info[:content]
21
+ if Path.is_filename?(format)
22
+ file = find_file(format)
23
+ if file
24
+ format = Open.json(file)
25
+ end
26
+ end
27
+ options[role] = format
28
+ next
29
+ end
30
+
31
+ if role.to_s == 'option'
32
+ key, _, value = info[:content].partition(" ")
33
+ options[key] = value
34
+ next
35
+ end
36
+
37
+ if role.to_s == 'sticky_option'
38
+ key, _, value = info[:content].partition(" ")
39
+ sticky_options[key] = value
40
+ next
41
+ end
42
+
43
+ if role == 'assistant'
44
+ options.clear
45
+ end
46
+ end
47
+ new << info
48
+ end
49
+ chat.replace new
50
+ sticky_options.merge options
51
+ end
52
+ end
@@ -0,0 +1,173 @@
1
+ module Chat
2
+ def self.tasks(messages, original = nil)
3
+ jobs = []
4
+ new = messages.collect do |message|
5
+ if message[:role] == 'task' || message[:role] == 'inline_task' || message[:role] == 'exec_task'
6
+ info = message[:content].strip
7
+
8
+ workflow, task = info.split(" ").values_at 0, 1
9
+
10
+ options = IndiferentHash.parse_options info
11
+ jobname = options.delete :jobname
12
+
13
+ if String === workflow
14
+ workflow = begin
15
+ Kernel.const_get workflow
16
+ rescue
17
+ Workflow.require_workflow(workflow)
18
+ end
19
+ end
20
+
21
+ job = workflow.job(task, jobname, options)
22
+
23
+ jobs << job unless message[:role] == 'exec_task'
24
+
25
+ if message[:role] == 'exec_task'
26
+ begin
27
+ {role: 'user', content: job.exec}
28
+ rescue
29
+ {role: 'exec_job', content: $!}
30
+ end
31
+ elsif message[:role] == 'inline_task'
32
+ {role: 'inline_job', content: job.path.find}
33
+ else
34
+ {role: 'job', content: job.path.find}
35
+ end
36
+ else
37
+ message
38
+ end
39
+ end.flatten
40
+
41
+ Workflow.produce(jobs)
42
+
43
+ new
44
+ end
45
+
46
+ def self.jobs(messages, original = nil)
47
+ messages.collect do |message|
48
+ if message[:role] == 'job' || message[:role] == 'inline_job'
49
+ file = message[:content].strip
50
+
51
+ step = Step.load file
52
+
53
+ id = step.short_path[0..39]
54
+ id = id.gsub('/','-')
55
+
56
+ if message[:role] == 'inline_job'
57
+ path = step.path
58
+ path = path.find if Path === path
59
+ {role: 'file', content: step.path}
60
+ else
61
+
62
+ function_name = step.full_task_name.sub('#', '-')
63
+ function_name = step.task_name
64
+ tool_call = {
65
+ function: {
66
+ name: function_name,
67
+ arguments: step.provided_inputs
68
+ },
69
+ id: id,
70
+ }
71
+
72
+ content = if step.done?
73
+ Open.read(step.path)
74
+ elsif step.error?
75
+ step.exception
76
+ end
77
+
78
+ tool_output = {
79
+ id: id,
80
+ content: content
81
+ }
82
+
83
+ [
84
+ {role: 'function_call', content: tool_call.to_json},
85
+ {role: 'function_call_output', content: tool_output.to_json},
86
+ ]
87
+ end
88
+ else
89
+ message
90
+ end
91
+ end.flatten
92
+ end
93
+
94
+ def self.tools(messages)
95
+ tool_definitions = IndiferentHash.setup({})
96
+ new = messages.collect do |message|
97
+ if message[:role] == 'mcp'
98
+ url, *tools = content_tokens(message)
99
+
100
+ if url == 'stdio'
101
+ command = tools.shift
102
+ mcp_tool_definitions = LLM.mcp_tools(url, command: command, url: nil, type: :stdio)
103
+ else
104
+ mcp_tool_definitions = LLM.mcp_tools(url)
105
+ end
106
+
107
+ if tools.any?
108
+ tools.each do |tool|
109
+ tool_definitions[tool] = mcp_tool_definitions[tool]
110
+ end
111
+ else
112
+ tool_definitions.merge!(mcp_tool_definitions)
113
+ end
114
+ next
115
+ elsif message[:role] == 'tool'
116
+ workflow_name, task_name, *inputs = content_tokens(message)
117
+ inputs = nil if inputs.empty?
118
+ inputs = [] if inputs == ['none'] || inputs == ['noinputs']
119
+ if Open.remote? workflow_name
120
+ require 'rbbt'
121
+ require 'scout/offsite/ssh'
122
+ require 'rbbt/workflow/remote_workflow'
123
+ workflow = RemoteWorkflow.new workflow_name
124
+ else
125
+ workflow = Workflow.require_workflow workflow_name
126
+ end
127
+
128
+ if task_name
129
+ definition = LLM.task_tool_definition workflow, task_name, inputs
130
+ tool_definitions[task_name] = [workflow, definition]
131
+ else
132
+ tool_definitions.merge!(LLM.workflow_tools(workflow))
133
+ end
134
+ next
135
+ elsif message[:role] == 'kb'
136
+ knowledge_base_name, *databases = content_tokens(message)
137
+ databases = nil if databases.empty?
138
+ knowledge_base = KnowledgeBase.load knowledge_base_name
139
+
140
+ knowledge_base_definition = LLM.knowledge_base_tool_definition(knowledge_base, databases)
141
+ tool_definitions.merge!(knowledge_base_definition)
142
+ next
143
+ elsif message[:role] == 'clear_tools'
144
+ tool_definitions = {}
145
+ else
146
+ message
147
+ end
148
+ end.compact.flatten
149
+ messages.replace new
150
+ tool_definitions
151
+ end
152
+
153
+ def self.associations(messages, kb = nil)
154
+ tool_definitions = {}
155
+ new = messages.collect do |message|
156
+ if message[:role] == 'association'
157
+ name, path, *options = content_tokens(message)
158
+
159
+ kb ||= KnowledgeBase.new Scout.var.Agent.Chat.knowledge_base
160
+ kb.register name, Path.setup(path), IndiferentHash.parse_options(message[:content])
161
+
162
+ tool_definitions.merge!(LLM.knowledge_base_tool_definition( kb, [name]))
163
+ next
164
+ elsif message[:role] == 'clear_associations'
165
+ tool_definitions = {}
166
+ else
167
+ message
168
+ end
169
+ end.compact.flatten
170
+ messages.replace new
171
+ tool_definitions
172
+ end
173
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'process/tools'
2
+ require_relative 'process/files'
3
+ require_relative 'process/clear'
4
+ require_relative 'process/options'
5
+
6
+ require 'shellwords'
7
+
8
+ module Chat
9
+ def self.content_tokens(message)
10
+ Shellwords.split(message[:content].strip)
11
+ end
12
+
13
+ def self.indiferent(messages)
14
+ messages.collect{|msg| IndiferentHash.setup msg }
15
+ end
16
+ end