scout-ai 1.2.3 → 2.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 (174) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +138 -50
  3. data/README.md +171 -290
  4. data/Rakefile +17 -1
  5. data/VERSION +1 -1
  6. data/doc/Improvements.md +325 -0
  7. data/doc/StartHere.md +110 -0
  8. data/doc/developer/Architecture.md +126 -0
  9. data/doc/developer/Backends.md +199 -0
  10. data/doc/developer/ChatLifecycle.md +183 -0
  11. data/doc/developer/DelegationInternals.md +295 -0
  12. data/doc/developer/DesignPrinciples.md +245 -0
  13. data/doc/developer/PromptProcessing.md +292 -0
  14. data/doc/developer/Provenance.md +317 -0
  15. data/doc/user/BuildingAgents.md +345 -0
  16. data/doc/user/Cookbook.md +333 -0
  17. data/doc/user/CoreConcepts.md +181 -0
  18. data/doc/user/Delegation.md +191 -0
  19. data/doc/user/GettingStarted.md +159 -0
  20. data/doc/user/ManagingContext.md +163 -0
  21. data/doc/user/MultiAgentWorkflows.md +256 -0
  22. data/doc/user/Python.md +159 -0
  23. data/doc/user/RunningInference.md +200 -0
  24. data/doc/user/ToolCalling.md +193 -0
  25. data/doc/user/WritingChats.md +197 -0
  26. data/lib/scout/llm/agent/chat.rb +61 -11
  27. data/lib/scout/llm/agent/delegate.rb +274 -65
  28. data/lib/scout/llm/agent/iterate.rb +2 -2
  29. data/lib/scout/llm/agent/save.rb +273 -0
  30. data/lib/scout/llm/agent/workflow.rb +164 -0
  31. data/lib/scout/llm/agent.rb +86 -61
  32. data/lib/scout/llm/ask.rb +62 -17
  33. data/lib/scout/llm/backends/anthropic.rb +9 -2
  34. data/lib/scout/llm/backends/bedrock.rb +15 -3
  35. data/lib/scout/llm/backends/default.rb +183 -99
  36. data/lib/scout/llm/backends/glm.rb +58 -0
  37. data/lib/scout/llm/backends/huggingface.rb +196 -26
  38. data/lib/scout/llm/backends/ollama.rb +13 -1
  39. data/lib/scout/llm/backends/openai.rb +0 -2
  40. data/lib/scout/llm/backends/openwebui.rb +20 -13
  41. data/lib/scout/llm/backends/relay.rb +22 -22
  42. data/lib/scout/llm/backends/responses.rb +1 -1
  43. data/lib/scout/llm/chat/agent_meta.rb +264 -0
  44. data/lib/scout/llm/chat/annotation.rb +39 -10
  45. data/lib/scout/llm/chat/parse.rb +28 -6
  46. data/lib/scout/llm/chat/persist.rb +25 -0
  47. data/lib/scout/llm/chat/process/clear.rb +41 -6
  48. data/lib/scout/llm/chat/process/files.rb +21 -6
  49. data/lib/scout/llm/chat/process/meta.rb +421 -34
  50. data/lib/scout/llm/chat/process/options.rb +21 -1
  51. data/lib/scout/llm/chat/process/tools.rb +56 -15
  52. data/lib/scout/llm/chat/process.rb +4 -0
  53. data/lib/scout/llm/chat/prompt/shorten_tools.rb +125 -0
  54. data/lib/scout/llm/chat/prompt/shorten_tools_epoch.rb +365 -0
  55. data/lib/scout/llm/chat/prompt.rb +48 -0
  56. data/lib/scout/llm/chat/provenance.rb +775 -0
  57. data/lib/scout/llm/chat/tool_calls.rb +76 -0
  58. data/lib/scout/llm/chat.rb +18 -2
  59. data/lib/scout/llm/embed.rb +11 -3
  60. data/lib/scout/llm/image.rb +86 -0
  61. data/lib/scout/llm/mcp.rb +10 -2
  62. data/lib/scout/llm/rag.rb +3 -3
  63. data/lib/scout/llm/tools/call.rb +160 -11
  64. data/lib/scout/llm/tools/knowledge_base.rb +1 -1
  65. data/lib/scout/llm/tools/workflow.rb +32 -16
  66. data/lib/scout/model/python/huggingface/causal.rb +23 -5
  67. data/lib/scout/model/python/huggingface.rb +2 -1
  68. data/lib/scout-ai.rb +1 -0
  69. data/python/README.md +197 -14
  70. data/python/scout_ai/huggingface/eval.py +245 -34
  71. data/python/tests/test_huggingface_eval.py +58 -0
  72. data/research/ChatAnalyst-required-changes.md +167 -0
  73. data/research/agent-delegation-analysis.md +810 -0
  74. data/research/agent-meta-provenance-integration-plan.md +622 -0
  75. data/research/agent-workflow-analysis.md +1120 -0
  76. data/research/backends-analysis.md +836 -0
  77. data/research/chat-core-analysis.md +946 -0
  78. data/research/chatanalyst-provenance/00-baseline.md +30 -0
  79. data/research/chatanalyst-provenance/01-repo-map.md +60 -0
  80. data/research/chatanalyst-provenance/02-event-reconstruction.md +55 -0
  81. data/research/chatanalyst-provenance/03-duplication-evidence.md +45 -0
  82. data/research/chatanalyst-provenance/04-tooling-root-cause.md +57 -0
  83. data/research/chatanalyst-provenance/05-fix-plan.md +46 -0
  84. data/research/chatanalyst-provenance/07-critic-review.md +25 -0
  85. data/research/chatanalyst-provenance/final-report.md +45 -0
  86. data/research/chatanalyst-provenance/resumption.md +37 -0
  87. data/research/coding-philosophy-analysis.md +928 -0
  88. data/research/commands-analysis.md +947 -0
  89. data/research/multi-agent-patterns-analysis.md +853 -0
  90. data/research/prompt-strategies-analysis.md +630 -0
  91. data/research/prov-verbosity-fix-notes.md +77 -0
  92. data/research/provenance-analysis.md +469 -0
  93. data/research/provenance-navigation-design.md +640 -0
  94. data/research/synthesis-report.md +487 -0
  95. data/research/tools-system-analysis.md +779 -0
  96. data/scout-ai.gemspec +100 -11
  97. data/scout_commands/agent/ask +13 -3
  98. data/scout_commands/agent/kb +2 -0
  99. data/scout_commands/llm/ask +11 -4
  100. data/scout_commands/llm/md +76 -0
  101. data/scout_commands/llm/process_queries +48 -0
  102. data/scout_commands/llm/prov +602 -0
  103. data/scout_commands/llm/word +71 -0
  104. data/scout_commands/workflow/mcp +43 -0
  105. data/share/word/reference.docx +0 -0
  106. data/test/etc/AI/mock.yaml +11 -0
  107. data/test/fixtures/backends/anthropic.json +19 -0
  108. data/test/fixtures/backends/anthropic_tool_use.json +24 -0
  109. data/test/fixtures/backends/bedrock.json +8 -0
  110. data/test/fixtures/backends/bedrock_embedding.json +3 -0
  111. data/test/fixtures/backends/bedrock_tool_use.json +17 -0
  112. data/test/fixtures/backends/ollama.json +16 -0
  113. data/test/fixtures/backends/ollama_tool_call.json +27 -0
  114. data/test/fixtures/backends/openai_chat.json +21 -0
  115. data/test/fixtures/backends/openai_chat_tool_call.json +31 -0
  116. data/test/fixtures/backends/responses.json +33 -0
  117. data/test/fixtures/backends/responses_tool_call.json +28 -0
  118. data/test/integration/README.md +32 -0
  119. data/test/integration/scout/llm/backends/test_endpoints.rb +34 -0
  120. data/test/integration/scout/llm/backends/test_openwebui.rb +61 -0
  121. data/test/integration/scout/llm/backends/test_relay.rb +52 -0
  122. data/test/integration/scout/llm/test_infrastructure.rb +74 -0
  123. data/test/{scout → integration/scout}/llm/test_mcp.rb +1 -1
  124. data/test/integration/scout/llm/tools/test_mcp.rb +42 -0
  125. data/test/integration/scout/model/test_base.rb +91 -0
  126. data/test/scout/llm/agent/test_chat.rb +8 -2
  127. data/test/scout/llm/agent/test_save.rb +413 -0
  128. data/test/scout/llm/agent/test_workflow.rb +110 -0
  129. data/test/scout/llm/backends/test_anthropic.rb +93 -10
  130. data/test/scout/llm/backends/test_bedrock.rb +118 -2
  131. data/test/scout/llm/backends/test_huggingface.rb +137 -42
  132. data/test/scout/llm/backends/test_ollama.rb +70 -20
  133. data/test/scout/llm/backends/test_openwebui.rb +42 -40
  134. data/test/scout/llm/backends/test_relay.rb +4 -2
  135. data/test/scout/llm/chat/agent_meta_fixtures.rb +131 -0
  136. data/test/scout/llm/chat/process/test_meta.rb +518 -0
  137. data/test/scout/llm/chat/process/test_normalize_usage.rb +183 -0
  138. data/test/scout/llm/chat/test_agent_meta.rb +357 -0
  139. data/test/scout/llm/chat/test_agent_meta_provenance.rb +467 -0
  140. data/test/scout/llm/chat/test_agent_meta_tokens.rb +594 -0
  141. data/test/scout/llm/chat/test_parse.rb +70 -15
  142. data/test/scout/llm/chat/test_prov_cli.rb +274 -0
  143. data/test/scout/llm/chat/test_provenance.rb +240 -0
  144. data/test/scout/llm/chat/test_tool_calls.rb +38 -0
  145. data/test/scout/llm/test_agent.rb +13 -36
  146. data/test/scout/llm/test_ask.rb +75 -52
  147. data/test/scout/llm/test_chat.rb +107 -13
  148. data/test/scout/llm/test_embed.rb +48 -0
  149. data/test/scout/llm/test_rag.rb +23 -16
  150. data/test/scout/llm/test_tools.rb +12 -1
  151. data/test/scout/llm/tools/test_knowledge_base.rb +0 -1
  152. data/test/scout/llm/tools/test_mcp.rb +5 -3
  153. data/test/scout/llm/tools/test_workflow.rb +23 -2
  154. data/test/scout/model/python/huggingface/causal/test_next_token.rb +11 -5
  155. data/test/scout/model/python/huggingface/test_causal.rb +9 -3
  156. data/test/scout/model/python/huggingface/test_classification.rb +11 -2
  157. data/test/scout/model/python/test_torch.rb +2 -0
  158. data/test/scout/model/python/torch/test_helpers.rb +4 -0
  159. data/test/scout/model/test_base.rb +4 -2
  160. data/test/support/availability.rb +231 -0
  161. data/test/support/fake_clients.rb +138 -0
  162. data/test/support/fixtures.rb +21 -0
  163. data/test/support/infrastructure_probes.rb +136 -0
  164. data/test/support/mock_backend.rb +215 -0
  165. data/test/test_helper.rb +32 -2
  166. metadata +99 -10
  167. data/doc/Agent.md +0 -327
  168. data/doc/Chat.md +0 -458
  169. data/doc/LLM.md +0 -340
  170. data/doc/RAG.md +0 -129
  171. data/scout_commands/documenter +0 -148
  172. data/test/scout/llm/backends/test_openai.rb +0 -192
  173. data/test/scout/llm/backends/test_responses.rb +0 -238
  174. data/test/scout/llm/test_parse.rb +0 -98
@@ -0,0 +1,164 @@
1
+ require 'scout/workflow'
2
+
3
+ module AgentWorkflow
4
+ extend Workflow
5
+
6
+ helper :chat do |chat=nil|
7
+ @chat ||= begin
8
+ chat = recursive_inputs[:chat]
9
+ chat = Chat.parse(chat) if String === chat
10
+ Chat.setup(chat)
11
+ chat
12
+ end
13
+ end
14
+
15
+ helper :options do
16
+ @options ||= LLM.options self.chat
17
+ end
18
+
19
+ helper :agent_options do |options|
20
+ IndiferentHash.setup options.except(:agent, 'agent', :chat, 'chat')
21
+ end
22
+
23
+ helper :tooling do
24
+ @tooling ||= begin
25
+ chat = self.chat
26
+ chat.remove_role(:tool) +
27
+ chat.remove_role(:kb) +
28
+ chat.remove_role(:mcp) +
29
+ chat.remove_role(:introduce)
30
+ end
31
+ end
32
+
33
+ helper :tooling_intro do
34
+ self.tooling.select { |msg| msg[:role] == 'introduce' }
35
+ end
36
+
37
+ helper :agent do |name = nil, chat: nil, options: nil, tooling: nil, files: nil, **kwargs|
38
+ options = self.options if options.nil?
39
+ tooling = self.tooling if tooling.nil?
40
+ options = IndiferentHash.add_defaults options, kwargs
41
+
42
+ agent = LLM.load_agent name, agent_options(options)
43
+ agent.job = self
44
+ agent.start_chat.follow tooling if tooling && !tooling.empty?
45
+
46
+ agent.start_chat.system <<-EOF
47
+ Your current working directory is #{Dir.pwd}.
48
+ You are working through an ask job with path #{self.path} and files_dir #{self.files_dir}.
49
+ EOF
50
+
51
+ if dependencies.any?
52
+ agent.start_chat.system <<-EOF
53
+ This workflow job has the following depencencies:
54
+
55
+ #{rec_dependencies.collect(&:path) * "\n"}
56
+
57
+ Their input should already be incorporated, but they might have artifacts
58
+ EOF
59
+ end
60
+
61
+ if chat && !chat.empty?
62
+ chat = LLM.chat(chat.dup)
63
+ other_jobs = chat.jobs
64
+ if other_jobs.any?
65
+ agent.start_chat.system <<-EOF
66
+ There are other jobs found in this chat:
67
+
68
+ #{other_jobs * "\n"}
69
+ EOF
70
+ end
71
+ end
72
+
73
+ files.each do |path|
74
+ target = file(path)
75
+ if File.exist?(target.find)
76
+ agent.start_chat.file target
77
+ elsif File.exist?(path.find)
78
+ agent.start_chat.file path
79
+ end
80
+ end if files
81
+
82
+ if chat && !chat.empty?
83
+ chat = LLM.chat(chat)
84
+ chat.reject! { |msg| msg[:content].to_s.start_with? 'You have been assigned' }
85
+ chat.reject! { |msg| msg[:content].to_s.start_with? 'This workflow job has the following' }
86
+ chat.reject! { |msg| msg[:content].to_s.start_with? 'There are other jobs found in this chat' }
87
+ agent.start_chat.follow chat
88
+ end
89
+
90
+ agent
91
+ end
92
+
93
+ helper :log_agent do |agent, agent_name=nil|
94
+ # Delegate to the Agent#save machinery so every path that saves an agent
95
+ # (chat_task jobs, CLI, plain agent.save) writes the identical canonical
96
+ # layout: this agent's FULL chat at <files_dir>/<agent_name>.chat and
97
+ # every nested conversation under
98
+ # <files_dir>/<agent_name>.society/<agent>/<conversation>/agent.chat.
99
+ #
100
+ # ScoutCoder: the save_file MUST be assigned BEFORE the first chat/start
101
+ # call (chat_task does it for us here, but any caller must too): the
102
+ # Agent#chat ensure-hook and the restart-snapshot hook in Agent#start
103
+ # both write through save_file, so assigning it late loses the
104
+ # intermediate turns. `agent_name` defaults to 'agent' and names the
105
+ # file, never a directory: worker.chat -> worker.society, no log/ level.
106
+ agent.save_file = files_dir["#{agent_name || 'agent'}.chat"] if agent.save_file.nil?
107
+ agent.save
108
+
109
+ update_info :dependencies, dependencies.collect { |dependency| dependency.path.find }
110
+ agent
111
+ end
112
+ end
113
+
114
+ module Workflow
115
+ def chat_task(task_name, &block)
116
+ input :chat, :text, 'Chat in Scout-AI chat-file format'
117
+ task task_name => :chat do |chat|
118
+ begin
119
+ # No eager mkdir: directories appear lazily when the save machinery
120
+ # (or any other writer) actually produces a file under files_dir.
121
+ Chat.allow_path files_dir
122
+ response = self.instance_exec(&block)
123
+
124
+ result = if LLM::Agent === response
125
+ agent = response
126
+ if agent.current_chat.last[:role].to_s != 'assistant'
127
+ agent.chat(return_messages: true)
128
+ else
129
+ agent.current_chat - agent.start_chat
130
+ end.tap { log_agent(agent) }
131
+ elsif Hash === response
132
+ [response]
133
+ else
134
+ response
135
+ end
136
+
137
+ Chat.project(self.short_path, result)
138
+ rescue ScoutException
139
+ error = { role: :assistant, content: { exception: $!, job: self.short_path }.to_json }
140
+ Chat.project(self.short_path, [error])
141
+ end
142
+ end
143
+ end
144
+
145
+ class << self
146
+ alias require_workflow_old require_workflow
147
+ end
148
+
149
+ def self.require_workflow(name, ...)
150
+ begin
151
+ require_workflow_old(name, ...)
152
+ rescue => e
153
+ begin
154
+ LLM.load_agent(name).workflow
155
+ rescue
156
+ raise e
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ class LLM::Agent
163
+ attr_accessor :job
164
+ end
@@ -10,7 +10,8 @@ module LLM
10
10
  end
11
11
 
12
12
  class Agent
13
- attr_accessor :workflow, :knowledge_base, :start_chat, :process_exception, :other_options, :path
13
+ attr_accessor :workflow, :knowledge_base, :start_chat, :process_exception, :other_options, :path, :job
14
+
14
15
  def initialize(workflow: nil, knowledge_base: nil, start_chat: nil, **kwargs)
15
16
  @workflow = workflow
16
17
  @workflow = Workflow.require_workflow @workflow if String === @workflow
@@ -27,8 +28,8 @@ module LLM
27
28
  else
28
29
  @workflow ||= begin
29
30
  m = Module.new
30
- m = "ScoutAgent"
31
31
  m.extend Workflow
32
+ m.name = 'ScoutAgent'
32
33
  m.tasks = {}
33
34
  m
34
35
  end
@@ -67,44 +68,56 @@ module LLM
67
68
  messages = [messages] unless messages.is_a? Array
68
69
  model ||= @model if model
69
70
 
70
- messages.delete_if{|info| info[:role] == 'agent' }
71
- if (list = messages.select{|info| info[:role] == 'socialize'}).any?
72
- socialize = list.last[:content]
73
- messages.delete_if{|info| info[:role] == 'socialize' }
74
- self.socialize(options.dup) if socialize && %w(true TRUE True T 1).include?(socialize.to_s)
75
- end
76
-
77
- tools = options[:tools] || {}
78
- if other_tools = @other_options[:tools]
79
- other_tools = JSON.parse other_tools if String === other_tools
80
- tools = tools.merge other_tools
81
- end
71
+ no_ask_override = @other_options[:no_ask_override]
82
72
 
73
+ messages.delete_if{|info| info[:role] == 'agent' }
83
74
  begin
84
75
 
85
- if workflow || knowledge_base
86
- tools.merge!(LLM.workflow_tools(workflow)) if workflow
87
- tools.merge!(LLM.knowledge_base_tool_definition(knowledge_base)) if knowledge_base and knowledge_base.all_databases.any?
88
- end
76
+ if workflow && workflow.tasks.include?(:ask) && ! no_ask_override
77
+ other_options.each do |key,value|
78
+ messages.push(IndiferentHash.setup({role: :sticky_option, content: "#{key} #{value}"}))
79
+ end
89
80
 
90
- if workflow && workflow.tasks.include?(:ask)
91
- options.each do |key,value|
92
- messages.push(IndiferentHash.setup({role: :option, content: "#{key} #{value}"}))
81
+ options.except(:return_messages).each do |key,value|
82
+ messages.push(IndiferentHash.setup({role: :sticky_option, content: "#{key} #{value}"}))
93
83
  end
94
84
 
95
85
  job = workflow.job(:ask, chat: Chat.print(messages))
96
- job.clean
86
+ self.job = job
87
+ Open.mkdir job.files_dir
88
+ Chat.allow_job job
89
+ job.clean if ENV['SCOUT_NO_ASK_CACHE'] == 'true'
90
+ job.recursive_clean if ENV['SCOUT_NO_ASK_CACHE'] == 'recursive'
97
91
  job.produce
98
92
 
99
- messages = LLM.chat job.path
93
+ messages = Chat.project(job.short_path, LLM.chat(job.path))
100
94
  if options[:return_messages]
101
- messages
95
+ Chat.setup(messages)
102
96
  else
103
- Chat.answer messages
97
+ Chat.setup(messages).answer
104
98
  end
105
99
  else
100
+
101
+ if (list = messages.select{|info| info[:role] == 'socialize'}).any?
102
+ socialize = list.last[:content]
103
+ messages.delete_if{|info| info[:role] == 'socialize' }
104
+ self.socialize(options.dup) if socialize && %w(true TRUE True T 1).include?(socialize.to_s)
105
+ end
106
+
107
+ tools = options[:tools] || {}
108
+ if other_tools = @other_options[:tools]
109
+ other_tools = JSON.parse other_tools if String === other_tools
110
+ tools = tools.merge other_tools
111
+ end
112
+
113
+
114
+ if workflow || knowledge_base
115
+ tools.merge!(LLM.workflow_tools(workflow)) if workflow && workflow.tasks.any?
116
+ tools.merge!(LLM.knowledge_base_tool_definition(knowledge_base)) if knowledge_base and knowledge_base.all_databases.any?
117
+ end
118
+
106
119
  options[:tools] = tools
107
- LLM.ask messages, @other_options.merge(log_errors: true).merge(options).merge(agent: false)
120
+ LLM.ask messages, @other_options.except(:no_ask_override).merge(log_errors: true).merge(options).merge(agent: false)
108
121
  end
109
122
  rescue
110
123
  exception = $!
@@ -116,7 +129,13 @@ module LLM
116
129
  raise exception
117
130
  end
118
131
  else
119
- raise exception
132
+ begin
133
+ self.save if self.save_file
134
+ rescue
135
+ Log.exception $!
136
+ ensure
137
+ raise exception
138
+ end
120
139
  end
121
140
  end
122
141
  end
@@ -140,6 +159,7 @@ module LLM
140
159
  end
141
160
 
142
161
  def self.load_agent(agent_name = nil, options = {})
162
+ agent_name = agent_name.to_s if Symbol === agent_name
143
163
  if agent_name && Path.is_filename?(agent_name)
144
164
  if File.directory?(agent_name)
145
165
  dir = Path.setup(agent_name) unless Path === agent_name
@@ -151,40 +171,43 @@ module LLM
151
171
  end
152
172
  end
153
173
 
154
- agent_name ||= 'default'
155
-
156
- workflow_path = Scout.workflows[agent_name]
157
- agent_path = Scout.var.Agent[agent_name]
158
- agent_path = Scout.chats[agent_name] unless agent_path.exists?
159
- agent_path = Scout.chats.Agent[agent_name] unless agent_path.exists?
160
-
161
- raise ScoutException, "No agent found with name #{agent_name}" unless workflow_path.exists? || agent_path.exists?
162
-
163
- workflow = if workflow_path.exists?
164
- agent_path = workflow_path
165
- Workflow.require_workflow agent_name
166
- elsif agent_path.workflow.find_with_extension("rb").exists?
167
- Workflow.require_workflow_file agent_path.workflow.find_with_extension("rb")
168
- elsif agent_path.python.exists? && agent_path.python.glob('*.py').any?
169
- require 'scout/workflow/python'
170
- PythonWorkflow.load_directory agent_path.python, 'ScoutAgent'
171
- end
172
-
173
- knowledge_base = if agent_path.knowledge_base.exists?
174
- KnowledgeBase.load agent_path.knowledge_base.find
175
- elsif workflow_path.knowledge_base.exists?
176
- KnowledgeBase.load workflow_path.knowledge_base.find
177
- end
178
-
179
- chat = if agent_path.start_chat.exists?
180
- Chat.setup LLM.chat(agent_path.start_chat.find)
181
- elsif workflow_path.start_chat.exists?
182
- Chat.setup LLM.chat(workflow_path.start_chat.find)
183
- elsif agent_path.start_chat.exists?
184
- Chat.setup LLM.chat(agent_path.start_chat.find)
185
- elsif workflow && workflow.documentation[:description]
186
- Chat.setup([ {role: 'introduce', content: workflow.name} ])
187
- end
174
+ if agent_name
175
+
176
+ workflow_path = Scout.workflows[agent_name]
177
+ agent_path = Scout.Agent[agent_name]
178
+ agent_path = Scout.var.Agent[agent_name] unless agent_path.exists?
179
+ agent_path = Scout.chats.Agent[agent_name] unless agent_path.exists?
180
+ agent_path = Scout.chats[agent_name] unless agent_path.exists?
181
+
182
+ raise ScoutException, "No agent found with name #{agent_name}" unless workflow_path.exists? || agent_path.exists?
183
+
184
+ @@agent_workflow ||= {}
185
+ workflow = @@agent_workflow[agent_name] ||= if workflow_path.exists?
186
+ agent_path = workflow_path
187
+ Workflow.require_workflow agent_name
188
+ elsif agent_path.workflow.find_with_extension("rb").exists?
189
+ Workflow.require_workflow_file agent_path.workflow.find_with_extension("rb")
190
+ elsif agent_path.python.exists? && agent_path.python.glob('*.py').any?
191
+ require 'scout/workflow/python'
192
+ PythonWorkflow.load_directory agent_path.python, 'ScoutAgent'
193
+ end
194
+
195
+ knowledge_base = if agent_path.knowledge_base.exists?
196
+ KnowledgeBase.load agent_path.knowledge_base.find
197
+ elsif workflow_path.knowledge_base.exists?
198
+ KnowledgeBase.load workflow_path.knowledge_base.find
199
+ end
200
+
201
+ chat = if agent_path.start_chat.exists?
202
+ Chat.setup LLM.chat(agent_path.start_chat.find)
203
+ elsif workflow_path.start_chat.exists?
204
+ Chat.setup LLM.chat(workflow_path.start_chat.find)
205
+ elsif agent_path.start_chat.exists?
206
+ Chat.setup LLM.chat(agent_path.start_chat.find)
207
+ elsif workflow && workflow.documentation[:description]
208
+ Chat.setup([ {role: 'introduce', content: workflow.name} ])
209
+ end
210
+ end
188
211
 
189
212
  agent = LLM::Agent.new **options.merge(workflow: workflow, knowledge_base: knowledge_base, start_chat: chat)
190
213
  agent.path = agent_path.find if agent_path
@@ -196,3 +219,5 @@ end
196
219
  require_relative 'agent/chat'
197
220
  require_relative 'agent/iterate'
198
221
  require_relative 'agent/delegate'
222
+ require_relative 'agent/save'
223
+ require_relative 'agent/workflow'
data/lib/scout/llm/ask.rb CHANGED
@@ -2,45 +2,82 @@ require 'scout'
2
2
  require_relative 'chat'
3
3
 
4
4
  module LLM
5
+
6
+ BACKENDS = IndiferentHash.setup({})
7
+
8
+ def self.register_backend(name, mod)
9
+ BACKENDS[name] = mod
10
+ end
11
+
5
12
  def self.ask(question, options = {}, &block)
6
13
  messages = LLM.chat(question)
7
- options = IndiferentHash.add_defaults LLM.options(messages), options
14
+ options = IndiferentHash.add_defaults options, LLM.options(messages)
8
15
 
9
- endpoint, persist = IndiferentHash.process_options options, :endpoint, :persist, persist: true
16
+ endpoint, persist, agent_save_file = IndiferentHash.process_options options, :endpoint, :persist, :agent_save_file, persist: true
10
17
 
18
+ persist ||= Scout::Config.get :persist, :ask, :llm, env: 'ASK_PERSIST,LLM_PERSIST,PERSIST'
11
19
  endpoint ||= Scout::Config.get :endpoint, :ask, :llm, env: 'ASK_ENDPOINT,LLM_ENDPOINT,ENDPOINT,LLM,ASK'
12
- if endpoint && Scout.etc.AI[endpoint].exists?
20
+ if endpoint && Scout.etc.AI[endpoint].find_with_extension(:yaml).exists?
13
21
  options = IndiferentHash.add_defaults options, Scout.etc.AI[endpoint].yaml
14
22
  elsif endpoint && endpoint != ""
15
23
  raise "Endpoint not found #{endpoint}"
16
24
  end
17
25
 
18
- if options[:backend].to_s == 'responses' && options[:previous_response].to_s != 'false'
19
- messages = Chat.clear(messages, 'previous_response_id')
20
- else
21
- messages = Chat.clean(messages, 'previous_response_id')
22
- options.delete :previous_response_id
23
- end
24
-
25
26
  agent_name = IndiferentHash.process_options options, :agent
27
+ agent_name = nil if %(none false nil).include?(agent_name.to_s)
26
28
  if agent_name
29
+ options[:endpoint] ||= endpoint
27
30
  agent = LLM::Agent.load_agent agent_name
31
+ agent.save_file = agent_save_file if agent_save_file
28
32
  agent.follow messages
29
- res = agent.ask options
33
+ res = agent.chat options
30
34
  return res
31
35
  end
32
36
 
33
- options[:meta] = Chat.meta messages
37
+ job_paths = messages.job_paths
38
+ meta = Chat.meta(messages)
39
+ options[:current_meta] = meta if meta and meta.any?
40
+
41
+ if options[:backend].to_s == 'responses' && options[:previous_response].to_s != 'false'
42
+ messages = Chat.clear(messages, 'previous_response_id')
43
+ else
44
+ messages = Chat.clean(messages, 'previous_response_id')
45
+ options.delete :previous_response_id
46
+ end
34
47
 
35
- Log.high Log.color :green, "Asking #{endpoint || 'client'}: #{options[:previous_response_id]}\n" + Chat.print_brief(messages)
36
48
  tools = options[:tools]
37
- Log.high "Tools: #{Log.fingerprint tools.keys}" if tools
38
- Log.debug "#{Log.fingerprint tools}}" if tools
49
+ if tools
50
+ # ScoutCoder: options[:tools] may be either the internal Hash shape
51
+ # ({name => [obj, definition]}) or a plain Array of provider-style
52
+ # definitions (as in the tests and InfrastructureProbes); only the
53
+ # Hash shape has #keys, so fingerprint accordingly.
54
+ tool_names = Hash === tools ? tools.keys : tools.collect { |t| t[:name] || t.dig(:function, :name) }
39
55
 
56
+ Log.high Log.color(:green, "Asking #{endpoint || options[:endpoint] || 'client'}: #{options[:previous_response_id]}\n" + Chat.print_brief(messages))
57
+ Log.medium "Tools: #{Log.fingerprint tool_names}" if tool_names&.any?
58
+ Log.debug "#{Log.fingerprint tools}}"
59
+ else
60
+ Log.high Log.color :green, "Asking #{endpoint || options[:endpoint] || 'client'}: #{options[:previous_response_id]}\n" + Chat.print_brief(messages)
61
+ end
62
+
63
+ persist = false if persist.to_s.downcase == 'false'
40
64
  res = Persist.persist(endpoint, :json, prefix: "LLM ask", other: options.merge(messages: messages), persist: persist, dir: Scout.var.cache.ask) do
41
65
  backend = IndiferentHash.process_options options, :backend
42
66
  backend ||= Scout::Config.get :backend, :ask, :llm, env: 'ASK_BACKEND,LLM_BACKEND', default: :responses
43
67
 
68
+ job_paths.each do |job_path|
69
+ begin
70
+ job = Step.load Path.setup(job_path)
71
+ jobs = [job] + job.rec_dependencies.to_a
72
+ jobs.each do |job|
73
+ Chat.allow_read_job job
74
+ end
75
+ rescue
76
+ Log.exception $!
77
+ Log.warn "Could not load #{job_path}"
78
+ end
79
+ end
80
+
44
81
  case backend
45
82
  when :openai, "openai"
46
83
  require_relative 'backends/openai'
@@ -60,20 +97,28 @@ module LLM
60
97
  when :openwebui, "openwebui"
61
98
  require_relative 'backends/openwebui'
62
99
  LLM::OpenWebUI.ask(messages, options, &block)
100
+ when :huggingface, "huggingface"
101
+ require_relative 'backends/huggingface'
102
+ LLM::Huggingface.ask(messages, options, &block)
63
103
  when :relay, "relay"
64
104
  require_relative 'backends/relay'
65
105
  LLM::Relay.ask(messages, options, &block)
66
106
  when :bedrock, "bedrock"
67
107
  require_relative 'backends/bedrock'
68
108
  LLM::Bedrock.ask(messages, options, &block)
109
+ when :glm, "glm"
110
+ require_relative 'backends/glm'
111
+ LLM::GLM.ask(messages, options, &block)
69
112
  else
70
- raise "Unknown backend: #{backend}"
113
+ mod = BACKENDS[backend]
114
+ raise "Unknown backend: #{backend}" if mod.nil?
115
+ mod.ask(messages, options, &block)
71
116
  end
72
117
  end
73
118
 
74
119
  Chat.setup res if Array === res
75
120
 
76
- Log.high Log.color :blue, "Response:\n" + Chat.print_brief(res, %w(meta assistant))
121
+ Log.high Log.color :blue, "Response:\n" + Chat.print_brief(res, %w(meta assistant)) if Array === res
77
122
 
78
123
  res
79
124
  end
@@ -21,7 +21,15 @@ module LLM
21
21
  url, key = IndiferentHash.process_options options,
22
22
  :url, :key
23
23
 
24
- Object::Anthropic::Client.new(access_token: key)
24
+ # ScoutCoder: the anthropic gem (>= 1.x, e.g. 1.17.0) Client#initialize
25
+ # accepts api_key: / auth_token: / base_url:, NOT access_token: (that
26
+ # is ruby-openai's keyword). Passing access_token: raises
27
+ # ArgumentError: unknown keyword. Verify kwargs against the installed
28
+ # gem's lib/anthropic/client.rb when touching this.
29
+ Object::Anthropic::Client.new(
30
+ api_key: key,
31
+ base_url: url
32
+ )
25
33
  end
26
34
 
27
35
  def query(client, messages, tools = [], parameters = {})
@@ -68,7 +76,6 @@ module LLM
68
76
  end
69
77
 
70
78
  def process_response(messages, response, tools, options, &block)
71
- Log.debug "Respose: #{Log.fingerprint response}"
72
79
  IndiferentHash.setup response
73
80
 
74
81
  response[:content].collect do |output|
@@ -13,6 +13,15 @@ module LLM
13
13
  Aws::BedrockRuntime::Client.new(options)
14
14
  end
15
15
 
16
+ # ScoutCoder: Bedrock/Anthropic style payloads wrap the individual calls in
17
+ # a content entry ({"type" => "tool_use", "tool_calls" => [...]}). The ask
18
+ # loop used to pass that wrapper itself to LLM.tool_response, which then
19
+ # found neither 'function' nor 'name'/'arguments' and called the tool block
20
+ # with (nil, nil). Unwrap and return the inner tool call entries.
21
+ def self.tool_calls(message)
22
+ [message.dig('content')].compact.flatten.select{|m| m['tool_calls']}.collect{|m| m['tool_calls']}.flatten
23
+ end
24
+
16
25
  def self.messages_to_prompt(messages)
17
26
  system = []
18
27
  user = []
@@ -44,7 +53,10 @@ module LLM
44
53
  type ||= Scout::Config.get(:type, model, default: :messages)
45
54
 
46
55
  role, previous_response_id, tools = IndiferentHash.process_options options, :role, :previous_response_id, :tools
47
- messages = LLM.parse(question, role)
56
+ # ScoutCoder: LLM.parse does not exist (the parser lives in Chat and is
57
+ # exposed as LLM.messages, which delegates to Chat.parse); this call
58
+ # raised NoMethodError on every ask. Use the LLM.messages delegation.
59
+ messages = LLM.messages(question, role)
48
60
 
49
61
  case type.to_sym
50
62
  when :messages
@@ -72,7 +84,7 @@ module LLM
72
84
  result = JSON.parse(response.body.string)
73
85
  Log.debug "Response: #{Log.fingerprint result}"
74
86
  message = result
75
- tool_calls = message.dig('content').select{|m| m['tool_calls']}
87
+ tool_calls = self.tool_calls message
76
88
 
77
89
  while tool_calls && tool_calls.any?
78
90
  messages << message
@@ -94,7 +106,7 @@ module LLM
94
106
  Log.debug "Response: #{Log.fingerprint result}"
95
107
 
96
108
  message = result
97
- tool_calls = message.dig('content').select{|m| m['tool_calls']}
109
+ tool_calls = self.tool_calls message
98
110
  end
99
111
 
100
112
  message.dig('content').collect{|m|