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,76 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module Chat
|
|
4
|
+
def self.parse_tool_message(message)
|
|
5
|
+
JSON.parse(message[:content].to_s)
|
|
6
|
+
rescue JSON::ParserError, TypeError
|
|
7
|
+
nil
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Pair tool calls with their outputs within one persisted chat. This reports
|
|
11
|
+
# structural facts only; use tool_call_status for the common interpretation
|
|
12
|
+
# of exception and exit-status outputs.
|
|
13
|
+
def self.tool_calls(chat, source: nil)
|
|
14
|
+
calls = []
|
|
15
|
+
outputs = Hash.new { |hash, key| hash[key] = [] }
|
|
16
|
+
|
|
17
|
+
chat.each_with_index do |message, index|
|
|
18
|
+
role = message[:role].to_s
|
|
19
|
+
next unless %w[function_call mcp_call function_call_output].include?(role)
|
|
20
|
+
info = parse_tool_message(message)
|
|
21
|
+
next unless Hash === info
|
|
22
|
+
|
|
23
|
+
call_id = info['id'] || info['call_id'] || info['tool_call_id']
|
|
24
|
+
address = source ? [source.to_s, index] : index
|
|
25
|
+
|
|
26
|
+
if role == 'function_call_output'
|
|
27
|
+
outputs[call_id] << { address: address, index: index, message: message, info: info } if call_id
|
|
28
|
+
else
|
|
29
|
+
calls << {
|
|
30
|
+
call_id: call_id,
|
|
31
|
+
name: info['name'] || info.dig('function', 'name'),
|
|
32
|
+
arguments: info['arguments'] || info.dig('function', 'arguments'),
|
|
33
|
+
role: role.to_sym,
|
|
34
|
+
call_address: address,
|
|
35
|
+
call_index: index,
|
|
36
|
+
call_message: message,
|
|
37
|
+
call_info: info
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
calls.collect do |call|
|
|
43
|
+
output = call[:call_id] && outputs[call[:call_id]].shift
|
|
44
|
+
call.merge(
|
|
45
|
+
output_address: output && output[:address],
|
|
46
|
+
output_index: output && output[:index],
|
|
47
|
+
output_message: output && output[:message],
|
|
48
|
+
output_info: output && output[:info],
|
|
49
|
+
output: output && output[:info]['content']
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Common, deliberately separate interpretation of a paired tool output.
|
|
55
|
+
# Missing output is unknown, JSON exceptions fail, and command-style JSON
|
|
56
|
+
# results fail when exit_status is non-zero.
|
|
57
|
+
def self.tool_call_status(call)
|
|
58
|
+
return { success: nil, reason: :missing_output } unless call[:output_info]
|
|
59
|
+
|
|
60
|
+
content = call[:output]
|
|
61
|
+
parsed = begin
|
|
62
|
+
JSON.parse(content.to_s)
|
|
63
|
+
rescue JSON::ParserError, TypeError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if Hash === parsed && parsed['exception']
|
|
68
|
+
{ success: false, reason: :exception, exception: parsed['exception'] }
|
|
69
|
+
elsif Hash === parsed && parsed.include?('exit_status')
|
|
70
|
+
status = parsed['exit_status'].to_i
|
|
71
|
+
{ success: status == 0, reason: :exit_status, exit_status: status }
|
|
72
|
+
else
|
|
73
|
+
{ success: true, reason: :output }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/scout/llm/chat.rb
CHANGED
|
@@ -4,6 +4,11 @@ require_relative 'tools'
|
|
|
4
4
|
require_relative 'chat/annotation'
|
|
5
5
|
require_relative 'chat/parse'
|
|
6
6
|
require_relative 'chat/process'
|
|
7
|
+
require_relative 'chat/persist'
|
|
8
|
+
require_relative 'chat/prompt'
|
|
9
|
+
require_relative 'chat/provenance'
|
|
10
|
+
require_relative 'chat/tool_calls'
|
|
11
|
+
require_relative 'chat/agent_meta'
|
|
7
12
|
|
|
8
13
|
module LLM
|
|
9
14
|
def self.messages(question, role = nil)
|
|
@@ -21,10 +26,21 @@ module LLM
|
|
|
21
26
|
|
|
22
27
|
Chat.parse question
|
|
23
28
|
end
|
|
29
|
+
|
|
24
30
|
def self.chat(file = [], original = nil)
|
|
25
31
|
original ||= (String === file and Open.exists?(file)) ? file : Path.setup($0.dup)
|
|
26
32
|
caller_lib_dir = Path.caller_lib_dir(nil, 'chats')
|
|
27
33
|
|
|
34
|
+
# ScoutCoder: anchor point for per-project library stores. The chat file
|
|
35
|
+
# is the object of the ask, so its libdir — not the framework's stack —
|
|
36
|
+
# defines "the project this chat belongs to". Guard against marker climbs
|
|
37
|
+
# that overshoot to nil/''/'/', and never clobber an explicit value: ENV
|
|
38
|
+
# propagates into job subprocesses (bwrap) while Dir.pwd under exec is the
|
|
39
|
+
# workflow's own directory, so ENV is the only reliable channel.
|
|
40
|
+
if ENV['SCOUT_CHAT_DIR'].to_s.empty? && caller_lib_dir && ! ['', '/'].include?(caller_lib_dir)
|
|
41
|
+
ENV['SCOUT_CHAT_DIR'] = Path.caller_lib_dir(original)
|
|
42
|
+
end
|
|
43
|
+
|
|
28
44
|
if Path.is_filename? file
|
|
29
45
|
messages = self.messages Open.read(file), file
|
|
30
46
|
else
|
|
@@ -35,8 +51,9 @@ module LLM
|
|
|
35
51
|
messages = Chat.imports messages, original, caller_lib_dir
|
|
36
52
|
|
|
37
53
|
messages = Chat.clear messages
|
|
38
|
-
messages = Chat.clean messages
|
|
54
|
+
messages = Chat.clean messages, :skip
|
|
39
55
|
|
|
56
|
+
messages = Chat.config messages
|
|
40
57
|
messages = Chat.tasks messages
|
|
41
58
|
messages = Chat.jobs messages
|
|
42
59
|
messages = Chat.files messages, original, caller_lib_dir
|
|
@@ -64,4 +81,3 @@ module LLM
|
|
|
64
81
|
Chat.purge(...)
|
|
65
82
|
end
|
|
66
83
|
end
|
|
67
|
-
|
data/lib/scout/llm/embed.rb
CHANGED
|
@@ -3,13 +3,13 @@ require 'scout'
|
|
|
3
3
|
module LLM
|
|
4
4
|
def self.embed(text, options = {})
|
|
5
5
|
endpoint = IndiferentHash.process_options options, :endpoint
|
|
6
|
-
endpoint ||= Scout::Config.get :endpoint, :embed, :llm, env: 'EMBED_ENDPOINT,LLM_ENDPOINT', default: :
|
|
6
|
+
endpoint ||= Scout::Config.get :endpoint, :embed, :llm, env: 'EMBED_ENDPOINT,LLM_ENDPOINT', default: :embed
|
|
7
7
|
if endpoint && Scout.etc.AI[endpoint].exists?
|
|
8
8
|
options = IndiferentHash.add_defaults options, Scout.etc.AI[endpoint].yaml
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
backend = IndiferentHash.process_options options, :backend
|
|
12
|
-
backend ||= Scout::Config.get :backend, :embed, :llm, env: 'EMBED_BACKEND,LLM_BACKEND', default: :
|
|
12
|
+
backend ||= Scout::Config.get :backend, :embed, :llm, env: 'EMBED_BACKEND,LLM_BACKEND', default: :embed
|
|
13
13
|
|
|
14
14
|
case backend
|
|
15
15
|
when :openai, "openai"
|
|
@@ -24,11 +24,19 @@ module LLM
|
|
|
24
24
|
when :openwebui, "openwebui"
|
|
25
25
|
require_relative 'backends/openwebui'
|
|
26
26
|
LLM::OpenWebUI.embed(text, options)
|
|
27
|
+
when :huggingface, "huggingface"
|
|
28
|
+
require_relative 'backends/huggingface'
|
|
29
|
+
LLM::Huggingface.embed(text, options)
|
|
27
30
|
when :relay, "relay"
|
|
28
31
|
require_relative 'backends/relay'
|
|
29
32
|
LLM::Relay.embed(text, options)
|
|
30
33
|
else
|
|
31
|
-
|
|
34
|
+
# Fall back to the runtime backend registry (mirrors LLM.ask) so
|
|
35
|
+
# test-only or plugin backends registered with LLM.register_backend
|
|
36
|
+
# work for embeddings too, instead of only for ask.
|
|
37
|
+
mod = LLM::BACKENDS[backend]
|
|
38
|
+
raise "Unknown backend: #{backend}" if mod.nil?
|
|
39
|
+
mod.embed(text, options)
|
|
32
40
|
end
|
|
33
41
|
end
|
|
34
42
|
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module LLM
|
|
2
|
+
|
|
3
|
+
def self.image(question, options = {}, &block)
|
|
4
|
+
messages = LLM.chat(question)
|
|
5
|
+
options = IndiferentHash.add_defaults LLM.options(messages), options
|
|
6
|
+
|
|
7
|
+
endpoint, persist = IndiferentHash.process_options options, :endpoint, :persist, persist: true
|
|
8
|
+
|
|
9
|
+
endpoint ||= Scout::Config.get :endpoint, :image, :ask, :llm, env: 'IMAGE_ENDPOINT,ASK_ENDPOINT,LLM_ENDPOINT,ENDPOINT,LLM,ASK,IMAGE'
|
|
10
|
+
if endpoint && Scout.etc.AI[endpoint].find_with_extension(:yaml).exists?
|
|
11
|
+
options = IndiferentHash.add_defaults options, Scout.etc.AI[endpoint].yaml
|
|
12
|
+
elsif endpoint && endpoint != ""
|
|
13
|
+
raise "Endpoint not found #{endpoint}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
agent_name = IndiferentHash.process_options options, :agent
|
|
17
|
+
agent_name = nil if %(none false nil).include?(agent_name.to_s)
|
|
18
|
+
if agent_name
|
|
19
|
+
options[:endpoint] ||= endpoint
|
|
20
|
+
agent = LLM::Agent.load_agent agent_name
|
|
21
|
+
agent.follow messages
|
|
22
|
+
res = agent.ask options
|
|
23
|
+
return res
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
meta = Chat.meta(messages)
|
|
27
|
+
options[:current_meta] = meta if meta and meta.any?
|
|
28
|
+
|
|
29
|
+
if options[:backend].to_s == 'responses' && options[:previous_response].to_s != 'false'
|
|
30
|
+
messages = Chat.clear(messages, 'previous_response_id')
|
|
31
|
+
else
|
|
32
|
+
messages = Chat.clean(messages, 'previous_response_id')
|
|
33
|
+
options.delete :previous_response_id
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Log.high Log.color :green, "Asking #{endpoint || options[:endpoint] || 'client'}: #{options[:previous_response_id]}\n" + Chat.print_brief(messages)
|
|
37
|
+
tools = options[:tools]
|
|
38
|
+
Log.medium "Tools: #{Log.fingerprint tools.keys}" if tools
|
|
39
|
+
Log.debug "#{Log.fingerprint tools}}" if tools
|
|
40
|
+
|
|
41
|
+
res = Persist.persist(endpoint, :json, prefix: "LLM image", other: options.merge(messages: messages), persist: persist, dir: Scout.var.cache.ask) do
|
|
42
|
+
backend = IndiferentHash.process_options options, :backend
|
|
43
|
+
backend ||= Scout::Config.get :backend, :ask, :llm, env: 'ASK_BACKEND,LLM_BACKEND', default: :responses
|
|
44
|
+
|
|
45
|
+
case backend
|
|
46
|
+
when :openai, "openai"
|
|
47
|
+
require_relative 'backends/openai'
|
|
48
|
+
LLM::OpenAI.image(messages, options, &block)
|
|
49
|
+
when :anthropic, "anthropic"
|
|
50
|
+
require_relative 'backends/anthropic'
|
|
51
|
+
LLM::Anthropic.image(messages, options, &block)
|
|
52
|
+
when :responses, "responses"
|
|
53
|
+
require_relative 'backends/responses'
|
|
54
|
+
LLM::Responses.image(messages, options, &block)
|
|
55
|
+
when :ollama, "ollama"
|
|
56
|
+
require_relative 'backends/ollama'
|
|
57
|
+
LLM::OLlama.image(messages, options, &block)
|
|
58
|
+
when :vllm, "vllm"
|
|
59
|
+
require_relative 'backends/vllm'
|
|
60
|
+
LLM::VLLM.image(messages, options, &block)
|
|
61
|
+
when :openwebui, "openwebui"
|
|
62
|
+
require_relative 'backends/openwebui'
|
|
63
|
+
LLM::OpenWebUI.image(messages, options, &block)
|
|
64
|
+
when :huggingface, "huggingface"
|
|
65
|
+
require_relative 'backends/huggingface'
|
|
66
|
+
LLM::Huggingface.image(messages, options, &block)
|
|
67
|
+
when :relay, "relay"
|
|
68
|
+
require_relative 'backends/relay'
|
|
69
|
+
LLM::Relay.image(messages, options, &block)
|
|
70
|
+
when :bedrock, "bedrock"
|
|
71
|
+
require_relative 'backends/bedrock'
|
|
72
|
+
LLM::Bedrock.image(messages, options, &block)
|
|
73
|
+
else
|
|
74
|
+
mod = BACKENDS[backend]
|
|
75
|
+
raise "Unknown backend: #{backend}" if mod.nil?
|
|
76
|
+
mod.ask(messages, options, &block)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
Chat.setup res if Array === res
|
|
81
|
+
|
|
82
|
+
Log.high Log.color :blue, "Response:\n" + Chat.print_brief(res, %w(meta assistant)) if Array === res
|
|
83
|
+
|
|
84
|
+
res
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/scout/llm/mcp.rb
CHANGED
|
@@ -15,14 +15,22 @@ module Workflow
|
|
|
15
15
|
annotations[:idempotent_hint] = true
|
|
16
16
|
annotations[:open_world_hint] = false
|
|
17
17
|
MCP::Tool.define(name:task, description: description, input_schema: input_schema, annotations:annotations) do |parameters,context|
|
|
18
|
-
self.job(name, parameters)
|
|
18
|
+
self.job(name, parameters).run
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
version = "1.0.0"
|
|
22
23
|
MCP::Server.new(
|
|
23
24
|
name: self.name,
|
|
24
|
-
version:
|
|
25
|
+
version: version,
|
|
25
26
|
tools: tools
|
|
26
27
|
)
|
|
27
28
|
end
|
|
29
|
+
|
|
30
|
+
def mcp_stdio(*tasks)
|
|
31
|
+
server = mcp(*tasks)
|
|
32
|
+
transport = MCP::Server::Transports::StdioTransport.new(server)
|
|
33
|
+
server.transport = transport
|
|
34
|
+
transport.open
|
|
35
|
+
end
|
|
28
36
|
end
|
data/lib/scout/llm/rag.rb
CHANGED
|
@@ -22,11 +22,11 @@ module LLM
|
|
|
22
22
|
u
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def self.top(texts, prompt, num = 10)
|
|
26
|
-
data = texts.collect{|text| LLM.embed(text) }
|
|
25
|
+
def self.top(texts, prompt, num = 10, ...)
|
|
26
|
+
data = texts.collect{|text| LLM.embed(text, ...) }
|
|
27
27
|
|
|
28
28
|
i = LLM::RAG.index(data)
|
|
29
|
-
pos, scores = i.search_knn LLM.embed(prompt), num
|
|
29
|
+
pos, scores = i.search_knn LLM.embed(prompt, ...), num
|
|
30
30
|
|
|
31
31
|
texts.values_at *pos.reverse
|
|
32
32
|
end
|
data/lib/scout/llm/tools/call.rb
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
# LLM::Agent is referenced below in content dispatch, but the require chain
|
|
2
|
+
# chat -> tools -> tools/call does not pull in scout/llm/agent (agent.rb
|
|
3
|
+
# requires ask.rb, which requires chat.rb, so loading agent from chat would
|
|
4
|
+
# be circular). Load it lazily on first use instead of at file load time.
|
|
5
|
+
module LLM
|
|
6
|
+
require 'scout/llm/agent' unless defined?(LLM::Agent)
|
|
7
|
+
end
|
|
8
|
+
|
|
1
9
|
module LLM
|
|
2
10
|
@max_content_length = Scout::Config.get(:max_content_length, :llm_tools, :tools, :llm, :ask, default: 100_000)
|
|
3
11
|
self.singleton_class.attr_accessor :max_content_length
|
|
@@ -17,10 +25,31 @@ module LLM
|
|
|
17
25
|
[tool_call_id, function_name, function_arguments]
|
|
18
26
|
end
|
|
19
27
|
|
|
28
|
+
# Normalize serialized meta messages (the chat-message shape
|
|
29
|
+
# {role: 'meta', content: 'k=v k=v ...'}) into the receipt format: an Array
|
|
30
|
+
# of DESERIALIZED field Hashes, as emitted under the `meta` key of
|
|
31
|
+
# function_call_output envelopes.
|
|
32
|
+
#
|
|
33
|
+
# Non-Hash entries, non-meta roles, non-String contents and entries that
|
|
34
|
+
# parse to no fields at all are dropped. The reader side keeps the full
|
|
35
|
+
# malformed-entry warning taxonomy; the writer side simply never emits an
|
|
36
|
+
# entry that carries no evidence.
|
|
37
|
+
def self.meta_receipt_from_messages(messages)
|
|
38
|
+
Array(messages).collect do |msg|
|
|
39
|
+
next nil unless Hash === msg
|
|
40
|
+
role = msg[:role] || msg['role']
|
|
41
|
+
content = msg[:content] || msg['content']
|
|
42
|
+
next nil unless role.to_s == 'meta' && String === content
|
|
43
|
+
fields = Chat.parse_meta(content)
|
|
44
|
+
fields.empty? ? nil : fields
|
|
45
|
+
end.compact
|
|
46
|
+
end
|
|
47
|
+
|
|
20
48
|
def self.process_calls(tools, calls, &block)
|
|
21
49
|
max_content_length = LLM.max_content_length
|
|
22
50
|
IndiferentHash.setup tools
|
|
23
51
|
|
|
52
|
+
start_timestamp = Chat.timestamp
|
|
24
53
|
tool_call_content = calls.collect do |tool_call|
|
|
25
54
|
tool_call = IndiferentHash.setup tool_call
|
|
26
55
|
tool_call_id, function_name, function_arguments = call_id_name_and_arguments(tool_call)
|
|
@@ -40,6 +69,13 @@ module LLM
|
|
|
40
69
|
function_response = case obj
|
|
41
70
|
when Proc
|
|
42
71
|
obj.call function_name, function_arguments
|
|
72
|
+
when String
|
|
73
|
+
if Kernel.const_defined? obj
|
|
74
|
+
wt = Kernel.const_get obj
|
|
75
|
+
else
|
|
76
|
+
wf = Workflow.require_workflow obj
|
|
77
|
+
end
|
|
78
|
+
call_workflow(wf, function_name, function_arguments)
|
|
43
79
|
when Workflow
|
|
44
80
|
call_workflow(obj, function_name, function_arguments)
|
|
45
81
|
when KnowledgeBase
|
|
@@ -57,12 +93,28 @@ module LLM
|
|
|
57
93
|
function_response
|
|
58
94
|
when String
|
|
59
95
|
function_response
|
|
96
|
+
when IO
|
|
97
|
+
function_response.read
|
|
98
|
+
when TSV::Dumper
|
|
99
|
+
function_response.read
|
|
100
|
+
when LLM::Agent
|
|
101
|
+
function_response
|
|
60
102
|
when nil
|
|
61
103
|
"success"
|
|
62
104
|
when Exception
|
|
63
|
-
|
|
105
|
+
function_response
|
|
106
|
+
when Hash
|
|
107
|
+
IndiferentHash.setup(function_response)
|
|
64
108
|
else
|
|
65
|
-
|
|
109
|
+
begin
|
|
110
|
+
function_response.to_json
|
|
111
|
+
rescue Exception => e
|
|
112
|
+
begin
|
|
113
|
+
function_response.to_s
|
|
114
|
+
rescue
|
|
115
|
+
{exception: e.message, stack: e.backtrace }.to_json
|
|
116
|
+
end
|
|
117
|
+
end
|
|
66
118
|
end
|
|
67
119
|
|
|
68
120
|
content = content.to_s if Numeric === content
|
|
@@ -90,29 +142,103 @@ module LLM
|
|
|
90
142
|
end
|
|
91
143
|
end
|
|
92
144
|
|
|
145
|
+
agents = tool_call_content.collect{|p| p.last }.select{|c| LLM::Agent === c }
|
|
146
|
+
|
|
147
|
+
agent_answers = TSV.setup({}, key_field: 'Pos', fields: ['Content', 'Job path'], type: :list)
|
|
148
|
+
|
|
149
|
+
if agents.any?
|
|
150
|
+
cpus = Scout::Config.get(:cpus, :agent_ask, :agents, env: 'ASK_AGENTS', default: 3)
|
|
151
|
+
Open.traverse (0..agents.length-1).to_a, cpus: cpus, bar: 'Asking agents', type: :list, into: agent_answers do |i|
|
|
152
|
+
agent = agents[i]
|
|
153
|
+
res = agent.chat return_messages: true
|
|
154
|
+
path = Step === agent.job ? agent.job.path : nil
|
|
155
|
+
[i, [res, path]]
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
93
159
|
tool_call_content.collect do |function_name,function_arguments,tool_call_id,tool_call,content|
|
|
160
|
+
error = false
|
|
161
|
+
stack = nil
|
|
162
|
+
meta = []
|
|
94
163
|
if Step === content
|
|
95
164
|
step = content
|
|
96
165
|
if content.done?
|
|
97
|
-
content = content.load
|
|
166
|
+
content = content.load
|
|
98
167
|
elsif content.error? && content.exception
|
|
99
|
-
|
|
168
|
+
error = :error
|
|
169
|
+
content = if String === content.exception
|
|
170
|
+
{exception: content.exception}.to_json
|
|
171
|
+
else
|
|
172
|
+
content = {exception: content.exception.message, exception_line: content.exception.backtrace&.first}.to_json
|
|
173
|
+
end
|
|
100
174
|
else
|
|
101
175
|
begin
|
|
102
|
-
content = content.run
|
|
103
|
-
rescue
|
|
104
|
-
|
|
176
|
+
content = content.run
|
|
177
|
+
rescue Exception
|
|
178
|
+
error = :error
|
|
179
|
+
stack = $!.backtrace
|
|
180
|
+
content = {exception: $!.message, exception_line: $!.backtrace&.first}.to_json
|
|
105
181
|
end
|
|
106
182
|
end
|
|
183
|
+
elsif LLM::Agent === content
|
|
184
|
+
res, path = agent_answers[agents.index(content)]
|
|
185
|
+
|
|
186
|
+
begin
|
|
187
|
+
Chat.allow_read_job Step.load(path)
|
|
188
|
+
rescue
|
|
189
|
+
end if path
|
|
190
|
+
|
|
191
|
+
content.current_chat.follow(res)
|
|
192
|
+
# Receipt format: the child agent's meta messages are DESERIALIZED
|
|
193
|
+
# into plain field Hashes and emitted under the `meta` key (the
|
|
194
|
+
# legacy serialized `agent_meta` array is no longer written).
|
|
195
|
+
# Entries that parse to no fields are dropped: a receipt entry
|
|
196
|
+
# exists to carry evidence fields, and an empty one carries none.
|
|
197
|
+
meta = LLM.meta_receipt_from_messages(Chat.find_role(res, :meta))
|
|
198
|
+
content = content.answer
|
|
199
|
+
elsif Exception === content
|
|
200
|
+
error = :error
|
|
201
|
+
stack = content.backtrace
|
|
202
|
+
content = {exception: content.message, exception_line: content.backtrace&.first}.to_json
|
|
107
203
|
else
|
|
108
204
|
step = nil
|
|
109
205
|
end
|
|
110
206
|
|
|
207
|
+
content = case content
|
|
208
|
+
when Hash
|
|
209
|
+
# ScoutCoder: When the response of the function contains a
|
|
210
|
+
# Hash with only two keys, content and meta or content and
|
|
211
|
+
# agent_meta treat it as content with inference meta. Extract
|
|
212
|
+
# accordingly.
|
|
213
|
+
content = IndiferentHash.setup(content)
|
|
214
|
+
keys = content.keys.collect{|k| k.to_s }
|
|
215
|
+
if keys.sort == %w(meta content)
|
|
216
|
+
# New inbound shape: `meta` is already the deserialized
|
|
217
|
+
# receipt array; pass it through verbatim.
|
|
218
|
+
meta, content = content.values_at :meta, :content
|
|
219
|
+
content
|
|
220
|
+
elsif keys.sort == %w(agent_meta content)
|
|
221
|
+
# Legacy inbound shape: serialized meta messages;
|
|
222
|
+
# normalize them into the new deserialized form.
|
|
223
|
+
meta = LLM.meta_receipt_from_messages(content[:agent_meta])
|
|
224
|
+
content = content[:content]
|
|
225
|
+
else
|
|
226
|
+
content.to_json
|
|
227
|
+
end
|
|
228
|
+
when TSV
|
|
229
|
+
content.to_s
|
|
230
|
+
when String
|
|
231
|
+
content
|
|
232
|
+
else
|
|
233
|
+
content.to_json
|
|
234
|
+
end
|
|
235
|
+
|
|
111
236
|
if (String === content) && content.length > max_content_length
|
|
112
|
-
exception_msg = "Function #{function_name} #{tool_call_id} (#{Log.fingerprint function_arguments}) returned #{content.length} characters, which is more than the maximum of #{max_content_length}. To protect the model context window this result was not returned."
|
|
113
|
-
exception_msg += " The results
|
|
237
|
+
exception_msg = "Function #{function_name} #{tool_call_id} (#{Log.fingerprint function_arguments}) was executed successfully, but it returned #{content.length} characters, which is more than the maximum of #{max_content_length}. To protect the model context window this result was not returned. Here is a fingerprint of the content #{Log.fingerprint(content)}."
|
|
238
|
+
exception_msg += " The results was persisted at '#{step.path}'." if step
|
|
114
239
|
Log.high exception_msg
|
|
115
240
|
content = {exception: exception_msg, stack: caller}.to_json
|
|
241
|
+
error = :truncated
|
|
116
242
|
end
|
|
117
243
|
|
|
118
244
|
Log.high "Called #{function_name} #{tool_call_id} (#{Log.fingerprint function_arguments}): " + Log.fingerprint(content)
|
|
@@ -120,13 +246,36 @@ module LLM
|
|
|
120
246
|
response_message = {
|
|
121
247
|
name: function_name,
|
|
122
248
|
content: content,
|
|
123
|
-
id: tool_call_id
|
|
249
|
+
id: tool_call_id,
|
|
124
250
|
}
|
|
125
251
|
|
|
252
|
+
response_message[:error] = error if error
|
|
253
|
+
response_message[:stack] = stack if stack
|
|
254
|
+
meta = [meta] if Hash === meta
|
|
255
|
+
response_message[:meta] = meta if meta && meta.any?
|
|
256
|
+
|
|
257
|
+
if step
|
|
258
|
+
response_message.merge!(
|
|
259
|
+
step: step.short_path,
|
|
260
|
+
start_timestamp: start_timestamp,
|
|
261
|
+
timestamp: Chat.timestamp
|
|
262
|
+
)
|
|
263
|
+
else
|
|
264
|
+
response_message.merge!(
|
|
265
|
+
start_timestamp: start_timestamp,
|
|
266
|
+
timestamp: Chat.timestamp
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
|
|
126
270
|
|
|
271
|
+
json_content = begin
|
|
272
|
+
response_message.to_json
|
|
273
|
+
rescue
|
|
274
|
+
"Error turning content into JSON (#{$!.message}): #{Log.fingerprint response_message}"
|
|
275
|
+
end
|
|
127
276
|
[
|
|
128
277
|
tool_call,
|
|
129
|
-
IndiferentHash.setup({role: "function_call_output", content:
|
|
278
|
+
IndiferentHash.setup({role: "function_call_output", content: json_content})
|
|
130
279
|
]
|
|
131
280
|
end.flatten
|
|
132
281
|
end
|
|
@@ -85,7 +85,7 @@ Returns a list in the format source~target.
|
|
|
85
85
|
|
|
86
86
|
if fields.length > 1
|
|
87
87
|
description = <<-EOF
|
|
88
|
-
Return details of association as a dictionary object.
|
|
88
|
+
Return details of a given list of association pairs as a dictionary object. Use the function #{database} to find the associations pairs.
|
|
89
89
|
Each key is an association and the value is an array with the values of the different fields you asked for, or for all fields otherwise.
|
|
90
90
|
The fields are: #{fields * ', '}.
|
|
91
91
|
Multiple values may be present and use the charater ";" to separate them.
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
require 'scout/workflow'
|
|
2
2
|
module LLM
|
|
3
|
+
def self.scout_to_tool_input_type(type)
|
|
4
|
+
type = :text if type == :chat
|
|
5
|
+
type = :string if type == :text
|
|
6
|
+
type = :string if type == :select
|
|
7
|
+
type = :string if type == :path
|
|
8
|
+
type = :number if type == :float
|
|
9
|
+
type = :array if type.to_s.end_with?('_array')
|
|
10
|
+
type
|
|
11
|
+
end
|
|
12
|
+
|
|
3
13
|
def self.task_tool_definition(workflow, task_name, inputs = nil)
|
|
4
14
|
task_info = workflow.task_info(task_name)
|
|
5
15
|
return nil if task_info.nil?
|
|
@@ -19,20 +29,18 @@ module LLM
|
|
|
19
29
|
|
|
20
30
|
end
|
|
21
31
|
|
|
32
|
+
|
|
22
33
|
properties = task_info[:inputs].inject({}) do |acc,input|
|
|
23
34
|
next acc if names and not names.include?(input)
|
|
24
35
|
type = task_info[:input_types][input]
|
|
25
36
|
description = task_info[:input_descriptions][input]
|
|
26
37
|
|
|
27
|
-
type =
|
|
28
|
-
type = :string if type == :select
|
|
29
|
-
type = :string if type == :path
|
|
30
|
-
type = :number if type == :float
|
|
38
|
+
type = scout_to_tool_input_type(type)
|
|
31
39
|
type = :array if type.to_s.end_with?('_array')
|
|
32
40
|
|
|
33
41
|
acc[input] = {
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
type: type,
|
|
43
|
+
description: description || ''
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
if type == :array
|
|
@@ -49,6 +57,13 @@ module LLM
|
|
|
49
57
|
acc
|
|
50
58
|
end
|
|
51
59
|
|
|
60
|
+
if not workflow.exec_exports.include?(task_name.to_sym)
|
|
61
|
+
properties[:return_path] = {
|
|
62
|
+
type: 'boolean',
|
|
63
|
+
description: 'Instead of the result of the job, return the path where it is persisted'
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
52
67
|
required_inputs = task_info[:inputs].select do |input|
|
|
53
68
|
next if names and not names.include?(input.to_sym)
|
|
54
69
|
task_info[:input_options].include?(input) && task_info[:input_options][input][:required]
|
|
@@ -89,20 +104,21 @@ module LLM
|
|
|
89
104
|
|
|
90
105
|
def self.call_workflow(workflow, task_name, parameters={})
|
|
91
106
|
parameters = {} if parameters.nil?
|
|
92
|
-
jobname =
|
|
107
|
+
jobname, return_path, exec_type, allow_recursive = IndiferentHash.process_options parameters, :jobname, :return_path, :exec_type, :allow_recursive
|
|
93
108
|
begin
|
|
94
|
-
exec_type = parameters[:exec_type]
|
|
95
109
|
job = workflow.job(task_name.to_sym, jobname, parameters)
|
|
96
|
-
if workflow.exec_exports.include?(task_name.to_sym) ||
|
|
110
|
+
if workflow.exec_exports.include?(task_name.to_sym) || exec_type.to_s == 'exec'
|
|
97
111
|
job.exec
|
|
98
112
|
else
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
if return_path
|
|
114
|
+
job.run(true)
|
|
115
|
+
Chat.allow_read_job job
|
|
116
|
+
job.path
|
|
117
|
+
else
|
|
118
|
+
raise ScoutException, 'Potential recursive call' if allow_recursive != 'true' &&
|
|
119
|
+
(job.running? and job.info[:pid] == Process.pid)
|
|
120
|
+
job
|
|
121
|
+
end
|
|
106
122
|
end
|
|
107
123
|
rescue ScoutException
|
|
108
124
|
return $!
|
|
@@ -7,11 +7,12 @@ class CausalModel < HuggingfaceModel
|
|
|
7
7
|
self.eval do |messages,list|
|
|
8
8
|
model, tokenizer = @state
|
|
9
9
|
ScoutPython.call_method(
|
|
10
|
-
"scout_ai.huggingface.eval", :eval_causal_lm_chat,
|
|
11
|
-
model, tokenizer, messages,
|
|
10
|
+
"scout_ai.huggingface.eval", :eval_causal_lm_chat,
|
|
11
|
+
model, tokenizer, messages,
|
|
12
12
|
options[:chat_template],
|
|
13
|
-
options[:chat_template_kwargs],
|
|
14
|
-
options[:generation_kwargs]
|
|
13
|
+
options[:chat_template_kwargs],
|
|
14
|
+
options[:generation_kwargs],
|
|
15
|
+
options[:tool_argument]
|
|
15
16
|
)
|
|
16
17
|
end
|
|
17
18
|
|
|
@@ -20,10 +21,27 @@ class CausalModel < HuggingfaceModel
|
|
|
20
21
|
model, tokenizer = @state
|
|
21
22
|
|
|
22
23
|
ScoutPython.call_method(
|
|
23
|
-
"scout_ai.huggingface.rlhf", :train_rlhf,
|
|
24
|
+
"scout_ai.huggingface.rlhf", :train_rlhf,
|
|
24
25
|
self.state_file, tokenizer, pairs, labels, options[:rlhf_config]
|
|
25
26
|
)
|
|
26
27
|
load_state
|
|
27
28
|
end
|
|
28
29
|
end
|
|
30
|
+
|
|
31
|
+
def chat(messages, tools = nil, runtime_options = {})
|
|
32
|
+
init unless @state
|
|
33
|
+
model, tokenizer = @state
|
|
34
|
+
|
|
35
|
+
runtime_options = IndiferentHash.setup(runtime_options)
|
|
36
|
+
|
|
37
|
+
ScoutPython.call_method(
|
|
38
|
+
"scout_ai.huggingface.eval", :eval_causal_lm_response,
|
|
39
|
+
model, tokenizer, messages, tools,
|
|
40
|
+
runtime_options[:chat_template] || options[:chat_template],
|
|
41
|
+
runtime_options[:chat_template_kwargs] || options[:chat_template_kwargs],
|
|
42
|
+
runtime_options[:generation_kwargs] || options[:generation_kwargs],
|
|
43
|
+
runtime_options[:tool_argument] || options[:tool_argument],
|
|
44
|
+
runtime_options[:response_parser] || options[:response_parser]
|
|
45
|
+
)
|
|
46
|
+
end
|
|
29
47
|
end
|
|
@@ -34,7 +34,8 @@ class HuggingfaceModel < TorchModel
|
|
|
34
34
|
self.options.except(
|
|
35
35
|
:training_args, :tokenizer_args,
|
|
36
36
|
:task, :checkpoint, :class_labels,
|
|
37
|
-
:model_options, :return_logits
|
|
37
|
+
:model_options, :return_logits, :chat_template,
|
|
38
|
+
:chat_template_kwargs, :generation_kwargs
|
|
38
39
|
))))
|
|
39
40
|
|
|
40
41
|
tokenizer_checkpoint = self.options[:tokenizer_args][:checkpoint] || checkpoint
|