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.
- checksums.yaml +4 -4
- data/.vimproject +138 -50
- data/README.md +171 -290
- data/Rakefile +17 -1
- data/VERSION +1 -1
- data/doc/Improvements.md +325 -0
- data/doc/StartHere.md +110 -0
- data/doc/developer/Architecture.md +126 -0
- data/doc/developer/Backends.md +199 -0
- data/doc/developer/ChatLifecycle.md +183 -0
- data/doc/developer/DelegationInternals.md +295 -0
- data/doc/developer/DesignPrinciples.md +245 -0
- data/doc/developer/PromptProcessing.md +292 -0
- data/doc/developer/Provenance.md +317 -0
- data/doc/user/BuildingAgents.md +345 -0
- data/doc/user/Cookbook.md +333 -0
- data/doc/user/CoreConcepts.md +181 -0
- data/doc/user/Delegation.md +191 -0
- data/doc/user/GettingStarted.md +159 -0
- data/doc/user/ManagingContext.md +163 -0
- data/doc/user/MultiAgentWorkflows.md +256 -0
- data/doc/user/Python.md +159 -0
- data/doc/user/RunningInference.md +200 -0
- data/doc/user/ToolCalling.md +193 -0
- data/doc/user/WritingChats.md +197 -0
- data/lib/scout/llm/agent/chat.rb +61 -11
- data/lib/scout/llm/agent/delegate.rb +274 -65
- data/lib/scout/llm/agent/iterate.rb +2 -2
- data/lib/scout/llm/agent/save.rb +273 -0
- data/lib/scout/llm/agent/workflow.rb +164 -0
- data/lib/scout/llm/agent.rb +86 -61
- data/lib/scout/llm/ask.rb +62 -17
- data/lib/scout/llm/backends/anthropic.rb +9 -2
- data/lib/scout/llm/backends/bedrock.rb +15 -3
- data/lib/scout/llm/backends/default.rb +183 -99
- data/lib/scout/llm/backends/glm.rb +58 -0
- data/lib/scout/llm/backends/huggingface.rb +196 -26
- data/lib/scout/llm/backends/ollama.rb +13 -1
- data/lib/scout/llm/backends/openai.rb +0 -2
- data/lib/scout/llm/backends/openwebui.rb +20 -13
- data/lib/scout/llm/backends/relay.rb +22 -22
- data/lib/scout/llm/backends/responses.rb +1 -1
- data/lib/scout/llm/chat/agent_meta.rb +264 -0
- data/lib/scout/llm/chat/annotation.rb +39 -10
- data/lib/scout/llm/chat/parse.rb +28 -6
- data/lib/scout/llm/chat/persist.rb +25 -0
- data/lib/scout/llm/chat/process/clear.rb +41 -6
- data/lib/scout/llm/chat/process/files.rb +21 -6
- data/lib/scout/llm/chat/process/meta.rb +421 -34
- data/lib/scout/llm/chat/process/options.rb +21 -1
- data/lib/scout/llm/chat/process/tools.rb +56 -15
- data/lib/scout/llm/chat/process.rb +4 -0
- data/lib/scout/llm/chat/prompt/shorten_tools.rb +125 -0
- data/lib/scout/llm/chat/prompt/shorten_tools_epoch.rb +365 -0
- data/lib/scout/llm/chat/prompt.rb +48 -0
- data/lib/scout/llm/chat/provenance.rb +775 -0
- data/lib/scout/llm/chat/tool_calls.rb +76 -0
- data/lib/scout/llm/chat.rb +18 -2
- data/lib/scout/llm/embed.rb +11 -3
- data/lib/scout/llm/image.rb +86 -0
- data/lib/scout/llm/mcp.rb +10 -2
- data/lib/scout/llm/rag.rb +3 -3
- data/lib/scout/llm/tools/call.rb +160 -11
- data/lib/scout/llm/tools/knowledge_base.rb +1 -1
- data/lib/scout/llm/tools/workflow.rb +32 -16
- data/lib/scout/model/python/huggingface/causal.rb +23 -5
- data/lib/scout/model/python/huggingface.rb +2 -1
- data/lib/scout-ai.rb +1 -0
- data/python/README.md +197 -14
- data/python/scout_ai/huggingface/eval.py +245 -34
- data/python/tests/test_huggingface_eval.py +58 -0
- data/research/ChatAnalyst-required-changes.md +167 -0
- data/research/agent-delegation-analysis.md +810 -0
- data/research/agent-meta-provenance-integration-plan.md +622 -0
- data/research/agent-workflow-analysis.md +1120 -0
- data/research/backends-analysis.md +836 -0
- data/research/chat-core-analysis.md +946 -0
- data/research/chatanalyst-provenance/00-baseline.md +30 -0
- data/research/chatanalyst-provenance/01-repo-map.md +60 -0
- data/research/chatanalyst-provenance/02-event-reconstruction.md +55 -0
- data/research/chatanalyst-provenance/03-duplication-evidence.md +45 -0
- data/research/chatanalyst-provenance/04-tooling-root-cause.md +57 -0
- data/research/chatanalyst-provenance/05-fix-plan.md +46 -0
- data/research/chatanalyst-provenance/07-critic-review.md +25 -0
- data/research/chatanalyst-provenance/final-report.md +45 -0
- data/research/chatanalyst-provenance/resumption.md +37 -0
- data/research/coding-philosophy-analysis.md +928 -0
- data/research/commands-analysis.md +947 -0
- data/research/multi-agent-patterns-analysis.md +853 -0
- data/research/prompt-strategies-analysis.md +630 -0
- data/research/prov-verbosity-fix-notes.md +77 -0
- data/research/provenance-analysis.md +469 -0
- data/research/provenance-navigation-design.md +640 -0
- data/research/synthesis-report.md +487 -0
- data/research/tools-system-analysis.md +779 -0
- data/scout-ai.gemspec +100 -11
- data/scout_commands/agent/ask +13 -3
- data/scout_commands/agent/kb +2 -0
- data/scout_commands/llm/ask +11 -4
- data/scout_commands/llm/md +76 -0
- data/scout_commands/llm/process_queries +48 -0
- data/scout_commands/llm/prov +602 -0
- data/scout_commands/llm/word +71 -0
- data/scout_commands/workflow/mcp +43 -0
- data/share/word/reference.docx +0 -0
- data/test/etc/AI/mock.yaml +11 -0
- data/test/fixtures/backends/anthropic.json +19 -0
- data/test/fixtures/backends/anthropic_tool_use.json +24 -0
- data/test/fixtures/backends/bedrock.json +8 -0
- data/test/fixtures/backends/bedrock_embedding.json +3 -0
- data/test/fixtures/backends/bedrock_tool_use.json +17 -0
- data/test/fixtures/backends/ollama.json +16 -0
- data/test/fixtures/backends/ollama_tool_call.json +27 -0
- data/test/fixtures/backends/openai_chat.json +21 -0
- data/test/fixtures/backends/openai_chat_tool_call.json +31 -0
- data/test/fixtures/backends/responses.json +33 -0
- data/test/fixtures/backends/responses_tool_call.json +28 -0
- data/test/integration/README.md +32 -0
- data/test/integration/scout/llm/backends/test_endpoints.rb +34 -0
- data/test/integration/scout/llm/backends/test_openwebui.rb +61 -0
- data/test/integration/scout/llm/backends/test_relay.rb +52 -0
- data/test/integration/scout/llm/test_infrastructure.rb +74 -0
- data/test/{scout → integration/scout}/llm/test_mcp.rb +1 -1
- data/test/integration/scout/llm/tools/test_mcp.rb +42 -0
- data/test/integration/scout/model/test_base.rb +91 -0
- data/test/scout/llm/agent/test_chat.rb +8 -2
- data/test/scout/llm/agent/test_save.rb +413 -0
- data/test/scout/llm/agent/test_workflow.rb +110 -0
- data/test/scout/llm/backends/test_anthropic.rb +93 -10
- data/test/scout/llm/backends/test_bedrock.rb +118 -2
- data/test/scout/llm/backends/test_huggingface.rb +137 -42
- data/test/scout/llm/backends/test_ollama.rb +70 -20
- data/test/scout/llm/backends/test_openwebui.rb +42 -40
- data/test/scout/llm/backends/test_relay.rb +4 -2
- data/test/scout/llm/chat/agent_meta_fixtures.rb +131 -0
- data/test/scout/llm/chat/process/test_meta.rb +518 -0
- data/test/scout/llm/chat/process/test_normalize_usage.rb +183 -0
- data/test/scout/llm/chat/test_agent_meta.rb +357 -0
- data/test/scout/llm/chat/test_agent_meta_provenance.rb +467 -0
- data/test/scout/llm/chat/test_agent_meta_tokens.rb +594 -0
- data/test/scout/llm/chat/test_parse.rb +70 -15
- data/test/scout/llm/chat/test_prov_cli.rb +274 -0
- data/test/scout/llm/chat/test_provenance.rb +240 -0
- data/test/scout/llm/chat/test_tool_calls.rb +38 -0
- data/test/scout/llm/test_agent.rb +13 -36
- data/test/scout/llm/test_ask.rb +75 -52
- data/test/scout/llm/test_chat.rb +107 -13
- data/test/scout/llm/test_embed.rb +48 -0
- data/test/scout/llm/test_rag.rb +23 -16
- data/test/scout/llm/test_tools.rb +12 -1
- data/test/scout/llm/tools/test_knowledge_base.rb +0 -1
- data/test/scout/llm/tools/test_mcp.rb +5 -3
- data/test/scout/llm/tools/test_workflow.rb +23 -2
- data/test/scout/model/python/huggingface/causal/test_next_token.rb +11 -5
- data/test/scout/model/python/huggingface/test_causal.rb +9 -3
- data/test/scout/model/python/huggingface/test_classification.rb +11 -2
- data/test/scout/model/python/test_torch.rb +2 -0
- data/test/scout/model/python/torch/test_helpers.rb +4 -0
- data/test/scout/model/test_base.rb +4 -2
- data/test/support/availability.rb +231 -0
- data/test/support/fake_clients.rb +138 -0
- data/test/support/fixtures.rb +21 -0
- data/test/support/infrastructure_probes.rb +136 -0
- data/test/support/mock_backend.rb +215 -0
- data/test/test_helper.rb +32 -2
- metadata +99 -10
- data/doc/Agent.md +0 -327
- data/doc/Chat.md +0 -458
- data/doc/LLM.md +0 -340
- data/doc/RAG.md +0 -129
- data/scout_commands/documenter +0 -148
- data/test/scout/llm/backends/test_openai.rb +0 -192
- data/test/scout/llm/backends/test_responses.rb +0 -238
- 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
|
data/lib/scout/llm/agent.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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)
|
|
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.
|
|
33
|
+
res = agent.chat options
|
|
30
34
|
return res
|
|
31
35
|
end
|
|
32
36
|
|
|
33
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
109
|
+
tool_calls = self.tool_calls message
|
|
98
110
|
end
|
|
99
111
|
|
|
100
112
|
message.dig('content').collect{|m|
|