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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'scout'
|
|
2
|
+
require 'securerandom'
|
|
2
3
|
require_relative '../chat'
|
|
3
4
|
|
|
4
5
|
module LLM
|
|
@@ -23,27 +24,31 @@ module LLM
|
|
|
23
24
|
# methods (like `query` / `format_tool_call`) and Ruby will correctly dispatch
|
|
24
25
|
# to the backend override.
|
|
25
26
|
module Backend
|
|
27
|
+
class BackendException
|
|
28
|
+
attr_accessor :chat
|
|
29
|
+
end
|
|
30
|
+
|
|
26
31
|
module ClassMethods
|
|
27
32
|
#{{{ CLIENT
|
|
28
33
|
|
|
29
34
|
def client(options)
|
|
30
|
-
url, key, model, log_errors, request_timeout = IndiferentHash.process_options options,
|
|
31
|
-
:url, :key, :model, :log_errors, :request_timeout,
|
|
32
|
-
log_errors: true, request_timeout:
|
|
35
|
+
url, key, model, api_version, log_errors, request_timeout = IndiferentHash.process_options options,
|
|
36
|
+
:url, :key, :model, :api_version, :log_errors, :request_timeout,
|
|
37
|
+
log_errors: true, request_timeout: 12000, api_version: 'v1'
|
|
33
38
|
|
|
34
|
-
Object::OpenAI::Client.new(access_token: key, log_errors: log_errors, uri_base: url, request_timeout: request_timeout)
|
|
39
|
+
Object::OpenAI::Client.new(api_version: api_version, access_token: key, log_errors: log_errors, uri_base: url, request_timeout: request_timeout.to_i)
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
def client_options(options)
|
|
38
|
-
url, key, model, tag, default_model, log_errors, request_timeout = IndiferentHash.process_options options,
|
|
39
|
-
:url, :key, :model, :tag, :default_model, :log_errors, :request_timeout,
|
|
43
|
+
url, key, model, api_version, tag, default_model, log_errors, request_timeout = IndiferentHash.process_options options,
|
|
44
|
+
:url, :key, :model, :api_version, :tag, :default_model, :log_errors, :request_timeout,
|
|
40
45
|
tag: self::TAG, default_model: self::DEFAULT_MODEL, log_errors: true, request_timeout: 1200
|
|
41
46
|
|
|
42
47
|
url ||= Scout::Config.get(:url, "#{tag}_ask", :ask, tag, env: "#{tag.upcase}_URL")
|
|
43
48
|
key ||= LLM.get_url_config(:key, url, "#{tag}_ask", :ask, tag, env: "#{tag.upcase}_KEY")
|
|
44
49
|
model ||= LLM.get_url_config(:model, url, :openai_ask, :ask, :openai, env: "#{tag.upcase}_MODEL,MODEL", default: default_model)
|
|
45
50
|
|
|
46
|
-
{ url: url, key: key, model: model }.reject { |_k, v| v.nil? }
|
|
51
|
+
{ url: url, key: key, model: model, api_version: api_version, request_timeout: request_timeout }.reject { |_k, v| v.nil? }
|
|
47
52
|
end
|
|
48
53
|
|
|
49
54
|
def extra_options(options, messages = nil)
|
|
@@ -215,6 +220,8 @@ module LLM
|
|
|
215
220
|
definition = obj if Hash === obj
|
|
216
221
|
definition
|
|
217
222
|
|
|
223
|
+
next definition if definition.keys.collect{|k| k.to_s } == ['type']
|
|
224
|
+
|
|
218
225
|
definition = case definition[:function]
|
|
219
226
|
when Hash
|
|
220
227
|
definition.merge(definition.delete :function)
|
|
@@ -297,9 +304,14 @@ module LLM
|
|
|
297
304
|
case tools
|
|
298
305
|
when Array
|
|
299
306
|
tools = tools.inject({}) do |acc, definition|
|
|
307
|
+
next definition unless Hash === definition
|
|
300
308
|
IndiferentHash.setup definition
|
|
301
309
|
name = definition.dig('name') || definition.dig('function', 'name')
|
|
302
|
-
|
|
310
|
+
if name
|
|
311
|
+
acc.merge(name => definition)
|
|
312
|
+
else
|
|
313
|
+
acc.merge(definition[:type] => definition)
|
|
314
|
+
end
|
|
303
315
|
end
|
|
304
316
|
when nil
|
|
305
317
|
tools = {}
|
|
@@ -361,8 +373,6 @@ module LLM
|
|
|
361
373
|
end
|
|
362
374
|
|
|
363
375
|
def process_response(messages, response, tools, options, &block)
|
|
364
|
-
Log.debug "Response: #{Log.fingerprint response}"
|
|
365
|
-
|
|
366
376
|
tool_calls = response['output'].collect do |output|
|
|
367
377
|
case output['type']
|
|
368
378
|
when 'function_call', 'mcp_call'
|
|
@@ -385,6 +395,8 @@ module LLM
|
|
|
385
395
|
next
|
|
386
396
|
when 'function_call', 'mcp_call'
|
|
387
397
|
[tool_call_outputs.shift, tool_call_outputs.shift]
|
|
398
|
+
when 'image_generation_call'
|
|
399
|
+
{role: 'image', content: output}
|
|
388
400
|
when 'web_search_call'
|
|
389
401
|
next
|
|
390
402
|
else
|
|
@@ -399,88 +411,183 @@ module LLM
|
|
|
399
411
|
output
|
|
400
412
|
end
|
|
401
413
|
|
|
402
|
-
|
|
403
|
-
def log_response(response, current_meta = nil)
|
|
414
|
+
def update_meta(response, current_meta = nil)
|
|
404
415
|
current_meta = {} if current_meta.nil?
|
|
416
|
+
IndiferentHash.setup current_meta
|
|
417
|
+
|
|
418
|
+
# Extract normalised token fields from the provider usage hash.
|
|
419
|
+
# Chat.normalize_usage handles all known backend response formats
|
|
420
|
+
# (OpenAI Chat, Responses API, GLM, Anthropic) and returns a flat
|
|
421
|
+
# hash keyed by the short names defined in Chat::TOKEN_KEYS.
|
|
422
|
+
tokens = Chat.normalize_usage(response.dig('usage')).reject { |_name, value| value.nil? }
|
|
423
|
+
|
|
424
|
+
# Keep the provider values for this request separate from aggregate
|
|
425
|
+
# values. In particular, do not add the running thread/session total
|
|
426
|
+
# to the chat total: doing that on every request produces the observed
|
|
427
|
+
# triangular (and, after aggregation, worse) growth.
|
|
428
|
+
meta = IndiferentHash.setup(tokens.dup)
|
|
429
|
+
# A lineage digest identifies copied conversational history, not an
|
|
430
|
+
# actual backend request. Persist a request identity so provenance can
|
|
431
|
+
# distinguish two genuinely repeated, otherwise identical inferences.
|
|
432
|
+
meta['inference_id'] = SecureRandom.uuid
|
|
433
|
+
meta['provider_response_id'] = response['id'] if response['id']
|
|
434
|
+
tokens.each do |name, value|
|
|
435
|
+
session_name = name + '_s'
|
|
436
|
+
Thread.current[session_name] = Thread.current[session_name].to_i + value.to_i
|
|
437
|
+
meta[session_name] = Thread.current[session_name]
|
|
438
|
+
end
|
|
405
439
|
|
|
406
|
-
|
|
407
|
-
|
|
440
|
+
Chat::TOKEN_KEYS.each do |name|
|
|
441
|
+
meta["#{name}_c"] = current_meta["#{name}_c"].to_i + tokens[name].to_i
|
|
442
|
+
end
|
|
408
443
|
|
|
409
|
-
|
|
410
|
-
'pt+': [['usage','prompt_tokens']],
|
|
411
|
-
'ct+': [['usage','completion_tokens']],
|
|
412
|
-
'tt+': [['usage','total_tokens']],
|
|
413
|
-
'reas': [['choices', 0, 'message', 'reasoning_content']],
|
|
414
|
-
}
|
|
444
|
+
Log.medium "Meta: #{Log.fingerprint meta}"
|
|
415
445
|
|
|
416
|
-
meta
|
|
417
|
-
|
|
418
|
-
name = name.to_s
|
|
419
|
-
key_list.each do |keys|
|
|
420
|
-
value = response.dig *keys
|
|
421
|
-
next unless value
|
|
446
|
+
meta
|
|
447
|
+
end
|
|
422
448
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
449
|
+
def reasoning(response, current_meta = nil)
|
|
450
|
+
begin
|
|
451
|
+
reasoning_content = response.dig('choices', 0, 'message', 'reasoning_content')
|
|
452
|
+
reasoning_content = reasoning_content.gsub("\n", ' ') if String === reasoning_content
|
|
453
|
+
Log.medium "Reasoning:\n" + Log.color(:cyan, reasoning_content) if reasoning_content
|
|
454
|
+
reasoning_content
|
|
455
|
+
rescue
|
|
456
|
+
end
|
|
457
|
+
end
|
|
427
458
|
|
|
428
|
-
meta[session_name] ||= Thread.current[session_name] || 0
|
|
429
|
-
meta[session_name] += value
|
|
430
459
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
end
|
|
437
|
-
end
|
|
460
|
+
def upload_messages(server, messages, options)
|
|
461
|
+
id = Misc.digest(messages)
|
|
462
|
+
messages.unshift({role: 'backend', content: self::TAG})
|
|
463
|
+
TmpFile.with_file [messages, options].to_json do |file|
|
|
464
|
+
CMD.cmd("scp #{file} #{server}:.scout/var/query/#{ id }.json")
|
|
438
465
|
end
|
|
466
|
+
id
|
|
467
|
+
end
|
|
439
468
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
469
|
+
def gather_response(server, id)
|
|
470
|
+
TmpFile.with_file do |file|
|
|
471
|
+
begin
|
|
472
|
+
CMD.cmd("scp #{server}:.scout/var/query/response/#{ id }.json #{ file }")
|
|
473
|
+
JSON.parse(Open.read(file))
|
|
474
|
+
rescue
|
|
475
|
+
sleep 1
|
|
476
|
+
retry
|
|
446
477
|
end
|
|
447
478
|
end
|
|
448
|
-
|
|
449
|
-
meta.merge! new
|
|
450
|
-
|
|
451
|
-
meta
|
|
452
479
|
end
|
|
453
480
|
|
|
454
481
|
def ask(question, options = {}, &block)
|
|
455
482
|
original_options = options.dup
|
|
456
483
|
|
|
457
|
-
return_messages, log_response, current_meta = IndiferentHash.process_options options,
|
|
458
|
-
:return_messages, :log_response, :
|
|
484
|
+
return_messages, log_response, current_meta, relay, process, prompt_strategies = IndiferentHash.process_options options,
|
|
485
|
+
:return_messages, :log_response, :current_meta, :relay, :process, :prompt_strategies,
|
|
459
486
|
return_messages: false, log_response: true
|
|
460
487
|
|
|
461
488
|
messages = self.messages question, options
|
|
489
|
+
|
|
490
|
+
if relay
|
|
491
|
+
id = upload_messages(relay, messages, options)
|
|
492
|
+
response = gather_response(relay, id)
|
|
493
|
+
IndiferentHash.setup(response)
|
|
494
|
+
formatted_prompt = format_messages(messages)
|
|
495
|
+
tools = tools(formatted_prompt, options)
|
|
496
|
+
else
|
|
462
497
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
498
|
+
client = prepare_client options, messages
|
|
499
|
+
prompt = Chat.prepare_prompt(messages, prompt_strategies)
|
|
500
|
+
formatted_prompt = format_messages(prompt)
|
|
501
|
+
tools = tools(formatted_prompt, options)
|
|
502
|
+
|
|
503
|
+
response = begin
|
|
504
|
+
Log.medium "Calling #{self}: #{Log.fingerprint(options.except(:tools))}}"
|
|
505
|
+
query(client, formatted_prompt, tools, options)
|
|
506
|
+
rescue Exception => e
|
|
507
|
+
Log.debug 'Asking error. Options: ' + "\n" + JSON.pretty_generate(options.except(:tools))
|
|
508
|
+
begin
|
|
509
|
+
tmpfile = TmpFile.tmp_file
|
|
510
|
+
Open.write tmpfile + ".chat", Chat.print(messages)
|
|
511
|
+
Open.write tmpfile + ".options", options.except(:messages, :tools).to_json
|
|
512
|
+
Open.write tmpfile + ".meta", current_meta.to_json
|
|
513
|
+
Log.warn "Messages and options saved in #{tmpfile}"
|
|
514
|
+
e.extend LLM::Backend::BackendException
|
|
515
|
+
e.chat tmpfile
|
|
516
|
+
rescue
|
|
517
|
+
end
|
|
466
518
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
rescue Exception
|
|
471
|
-
Log.debug 'Asking error. Options: ' + "\n" + JSON.pretty_generate(options.except(:tools))
|
|
472
|
-
raise $!
|
|
473
|
-
end
|
|
519
|
+
raise e
|
|
520
|
+
end
|
|
521
|
+
end
|
|
474
522
|
|
|
475
|
-
|
|
523
|
+
timestamp = Chat.timestamp
|
|
524
|
+
|
|
525
|
+
if process
|
|
526
|
+
Scout.var.query.response[process].set_extension(:json).write response.to_json
|
|
527
|
+
return response
|
|
528
|
+
end
|
|
476
529
|
|
|
477
|
-
|
|
530
|
+
Log.debug "Response: #{Log.fingerprint response}"
|
|
531
|
+
|
|
532
|
+
raise 'No response' if response.nil?
|
|
478
533
|
|
|
479
|
-
|
|
534
|
+
reasoning = reasoning response
|
|
535
|
+
|
|
536
|
+
output = begin
|
|
537
|
+
process_response messages, response, tools, options, &block
|
|
538
|
+
rescue Exception => e
|
|
539
|
+
|
|
540
|
+
Log.debug 'Processing response error. Options: ' + "\n" + JSON.pretty_generate(options.except(:tools))
|
|
541
|
+
begin
|
|
542
|
+
tmpfile = TmpFile.tmp_file
|
|
543
|
+
previous_response_id_error = options.delete
|
|
544
|
+
if previous_response_id_error
|
|
545
|
+
message.unshift IndiferentHash.setup({role: :previous_response_id, content: previous_response_id_error})
|
|
546
|
+
end
|
|
547
|
+
Open.write tmpfile + ".chat", Chat.print(messages)
|
|
548
|
+
Open.write tmpfile + ".options", options.except(:messages, :tools).to_json
|
|
549
|
+
Open.write tmpfile + ".meta", current_meta.to_json
|
|
550
|
+
Log.warn "Messages and options saved in #{tmpfile}"
|
|
551
|
+
e.extend LLM::Backend::BackendException
|
|
552
|
+
e.chat tmpfile
|
|
553
|
+
rescue
|
|
554
|
+
end
|
|
555
|
+
raise e
|
|
556
|
+
end
|
|
480
557
|
|
|
481
|
-
|
|
558
|
+
if log_response
|
|
559
|
+
meta = self.update_meta response, current_meta
|
|
560
|
+
meta['reas'] = reasoning if reasoning
|
|
561
|
+
meta['timestamp'] = timestamp
|
|
562
|
+
end
|
|
482
563
|
|
|
483
|
-
output.
|
|
564
|
+
output = chain_tools messages, output, tools, options.merge(client: client, tools: tools, log_response: log_response, current_meta: meta, relay: relay)
|
|
565
|
+
|
|
566
|
+
if log_response && meta && meta.any?
|
|
567
|
+
# The meta is about to become the first message of a segment that
|
|
568
|
+
# runs until the next meta (a chained tool-call ask, whose own meta
|
|
569
|
+
# was already unshifted inside `output` by the recursive call) or
|
|
570
|
+
# the end of the response. A reasoning-only request produces no
|
|
571
|
+
# message at all, so its meta covers zero messages: `output` is
|
|
572
|
+
# empty, or starts with the nested meta. Such rounds are real cost
|
|
573
|
+
# (~7% of prompt tokens in one measured agent log) with nothing to
|
|
574
|
+
# show for it, so mark them explicitly instead of leaving a bare
|
|
575
|
+
# meta for the reader to puzzle over. `Chat.trace_indices` derives
|
|
576
|
+
# the same fact independently; the marker only makes the persisted
|
|
577
|
+
# meta self-explanatory and is inert for accounting.
|
|
578
|
+
#
|
|
579
|
+
# ScoutCoder: orphan is only meaningful for the meta that opens a
|
|
580
|
+
# segment. Chained tool calls recurse into `ask`, and their metas
|
|
581
|
+
# were already unshifted onto `output` before this point, so the
|
|
582
|
+
# first message of `output` decides: an empty list, or one starting
|
|
583
|
+
# with the nested meta, means this round covered nothing. Do not
|
|
584
|
+
# recompute this from `trace_indices` here: that helper sees the
|
|
585
|
+
# whole chat (parent copies included) and does not exist on this
|
|
586
|
+
# path.
|
|
587
|
+
meta['orphan'] = true if output.empty? || output.first[:role].to_s == 'meta'
|
|
588
|
+
|
|
589
|
+
output.unshift({role: :meta, content: Chat.serialize_meta(meta)})
|
|
590
|
+
end
|
|
484
591
|
|
|
485
592
|
if output.last[:role] != :previous_response_id && options[:previous_response_id]
|
|
486
593
|
output << { role: :previous_response_id, content: options[:previous_response_id] }
|
|
@@ -489,44 +596,21 @@ module LLM
|
|
|
489
596
|
if return_messages
|
|
490
597
|
Chat.setup output
|
|
491
598
|
else
|
|
492
|
-
output =
|
|
493
|
-
return '' if output.empty?
|
|
494
|
-
|
|
599
|
+
output = Chat.clean(output)
|
|
600
|
+
return '' if output.nil? || output.empty?
|
|
601
|
+
Chat.purge(output).last['content']
|
|
495
602
|
end
|
|
496
603
|
end
|
|
497
604
|
|
|
498
605
|
def image(question, options = {}, &block)
|
|
499
|
-
messages =
|
|
500
|
-
options = options.merge LLM.options messages
|
|
501
|
-
tools = LLM.tools messages
|
|
502
|
-
associations = LLM.associations messages
|
|
503
|
-
|
|
504
|
-
client, url, key, model, log_errors, return_messages, format = IndiferentHash.process_options options,
|
|
505
|
-
:client, :url, :key, :model, :log_errors, :return_messages, :format,
|
|
506
|
-
log_errors: true
|
|
507
|
-
|
|
508
|
-
if client.nil?
|
|
509
|
-
url ||= Scout::Config.get(:url, :openai_ask, :ask, :openai, env: 'OPENAI_URL')
|
|
510
|
-
key ||= LLM.get_url_config(:key, url, :openai_ask, :ask, :openai, env: 'OPENAI_KEY')
|
|
511
|
-
client = LLM::OpenAI.client url, key, log_errors
|
|
512
|
-
end
|
|
606
|
+
messages = ask(question, options.merge({tools: [{type: 'image_generation'}], return_messages: true}), &block)
|
|
513
607
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
input = []
|
|
521
|
-
parameters = {}
|
|
522
|
-
messages.each do |message|
|
|
523
|
-
input << message
|
|
524
|
-
end
|
|
525
|
-
parameters[:prompt] = LLM.print(input)
|
|
526
|
-
|
|
527
|
-
response = client.images.generate(parameters: parameters)
|
|
528
|
-
|
|
529
|
-
response
|
|
608
|
+
base64_image = begin
|
|
609
|
+
messages.select{|info| info[:role] == 'image' }.first['content']['result']
|
|
610
|
+
rescue
|
|
611
|
+
Log.warn 'Image error: ' + "\n" + JSON.pretty_generate(messages)
|
|
612
|
+
raise $!
|
|
613
|
+
end
|
|
530
614
|
end
|
|
531
615
|
|
|
532
616
|
def embed_query(client, text, parameters = {})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require_relative 'default'
|
|
2
|
+
require_relative 'openai'
|
|
3
|
+
require 'openai'
|
|
4
|
+
|
|
5
|
+
module LLM
|
|
6
|
+
# GLM Chat Completions backend.
|
|
7
|
+
#
|
|
8
|
+
# Implemented as a module exposing singleton methods (`LLM::OpenAI.ask`, etc).
|
|
9
|
+
# We compose the backend by:
|
|
10
|
+
# - prepending GLMAIMethods into the singleton class (overrides)
|
|
11
|
+
# - including Backend::ClassMethods into the singleton class (shared logic)
|
|
12
|
+
module GLMAIMethods
|
|
13
|
+
def format_other(message)
|
|
14
|
+
role = message[:role]
|
|
15
|
+
|
|
16
|
+
case role.to_s
|
|
17
|
+
when 'image'
|
|
18
|
+
path = message[:content]
|
|
19
|
+
path = Chat.find_file path
|
|
20
|
+
if Open.remote?(path)
|
|
21
|
+
{ role: :user, content: { type: :image_url, image_url: {url: path} } }
|
|
22
|
+
elsif Open.exists?(path)
|
|
23
|
+
path = encode_image(path)
|
|
24
|
+
{ role: :user, content: [{ type: :image_url, image_url: {url: path} }] }
|
|
25
|
+
else
|
|
26
|
+
raise "Image does not exist in #{path}"
|
|
27
|
+
end
|
|
28
|
+
when 'pdf'
|
|
29
|
+
path = original_path = message[:content]
|
|
30
|
+
if Open.remote?(path)
|
|
31
|
+
{ role: :user, content: { type: :input_file, file_url: path } }
|
|
32
|
+
elsif Open.exists?(path)
|
|
33
|
+
data = encode_pdf(path)
|
|
34
|
+
{ role: :user, content: [{ type: :input_file, file_data: data, filename: File.basename(path) }] }
|
|
35
|
+
else
|
|
36
|
+
raise "PDF does not exist in #{path}"
|
|
37
|
+
end
|
|
38
|
+
when 'websearch'
|
|
39
|
+
{ role: :tool, content: { type: 'web_search_preview' } }
|
|
40
|
+
when 'previous_response_id'
|
|
41
|
+
nil
|
|
42
|
+
else
|
|
43
|
+
message
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module GLM
|
|
49
|
+
TAG = 'glm'
|
|
50
|
+
DEFAULT_MODEL = 'glm-turbo'
|
|
51
|
+
|
|
52
|
+
class << self
|
|
53
|
+
prepend OpenAIMethods
|
|
54
|
+
prepend GLMAIMethods
|
|
55
|
+
include Backend::ClassMethods
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|