scout-ai 1.2.5 → 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 +111 -31
- data/README.md +171 -320
- 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 +62 -12
- 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 +36 -6
- data/lib/scout/llm/backends/anthropic.rb +9 -1
- data/lib/scout/llm/backends/bedrock.rb +15 -3
- data/lib/scout/llm/backends/default.rb +129 -89
- data/lib/scout/llm/backends/glm.rb +58 -0
- data/lib/scout/llm/backends/huggingface.rb +13 -1
- data/lib/scout/llm/backends/ollama.rb +13 -0
- data/lib/scout/llm/backends/openwebui.rb +8 -3
- data/lib/scout/llm/chat/agent_meta.rb +264 -0
- data/lib/scout/llm/chat/annotation.rb +37 -14
- 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 +19 -1
- data/lib/scout/llm/chat/process/files.rb +16 -1
- data/lib/scout/llm/chat/process/meta.rb +422 -32
- data/lib/scout/llm/chat/process/options.rb +21 -1
- data/lib/scout/llm/chat/process/tools.rb +49 -12
- 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 +8 -3
- data/lib/scout/llm/image.rb +86 -0
- data/lib/scout/llm/rag.rb +3 -3
- data/lib/scout/llm/tools/call.rb +159 -10
- data/lib/scout/llm/tools/knowledge_base.rb +1 -1
- data/lib/scout/llm/tools/workflow.rb +13 -5
- data/lib/scout-ai.rb +1 -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 +97 -13
- 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/prov +602 -0
- data/scout_commands/llm/word +71 -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_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 +96 -12
- data/doc/Agent.md +0 -354
- data/doc/Chat.md +0 -481
- data/doc/LLM.md +0 -356
- data/doc/PythonAgentTasks.md +0 -333
- data/doc/RAG.md +0 -129
- data/doc/USER_GUIDE.md +0 -572
- 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,15 +24,19 @@ 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
35
|
url, key, model, api_version, log_errors, request_timeout = IndiferentHash.process_options options,
|
|
31
36
|
:url, :key, :model, :api_version, :log_errors, :request_timeout,
|
|
32
|
-
log_errors: true, request_timeout:
|
|
37
|
+
log_errors: true, request_timeout: 12000, api_version: 'v1'
|
|
33
38
|
|
|
34
|
-
Object::OpenAI::Client.new(api_version: api_version, 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)
|
|
@@ -43,7 +48,7 @@ module LLM
|
|
|
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, api_version: api_version }.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 = {}
|
|
@@ -308,7 +320,7 @@ module LLM
|
|
|
308
320
|
tools.merge!(LLM.tools messages)
|
|
309
321
|
tools.merge!(LLM.associations messages)
|
|
310
322
|
|
|
311
|
-
Log.
|
|
323
|
+
Log.medium "Tools: #{Log.fingerprint tools.keys}" if tools
|
|
312
324
|
|
|
313
325
|
tools
|
|
314
326
|
end
|
|
@@ -383,6 +395,8 @@ module LLM
|
|
|
383
395
|
next
|
|
384
396
|
when 'function_call', 'mcp_call'
|
|
385
397
|
[tool_call_outputs.shift, tool_call_outputs.shift]
|
|
398
|
+
when 'image_generation_call'
|
|
399
|
+
{role: 'image', content: output}
|
|
386
400
|
when 'web_search_call'
|
|
387
401
|
next
|
|
388
402
|
else
|
|
@@ -399,63 +413,54 @@ module LLM
|
|
|
399
413
|
|
|
400
414
|
def update_meta(response, current_meta = nil)
|
|
401
415
|
current_meta = {} if current_meta.nil?
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
Thread.current[session_name] = meta[session_name]
|
|
425
|
-
meta.delete name if meta[name] == meta[session_name]
|
|
426
|
-
else
|
|
427
|
-
value = value.gsub(/\s+/,' ') if String === value
|
|
428
|
-
meta[name] = value
|
|
429
|
-
end
|
|
430
|
-
end
|
|
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]
|
|
431
438
|
end
|
|
432
439
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
if name.end_with?('_s')
|
|
436
|
-
chat_name = name.sub(/_s$/,'_c')
|
|
437
|
-
current_value = current_meta[chat_name] || current_meta[name] || 0
|
|
438
|
-
new[chat_name] = current_value + value
|
|
439
|
-
end
|
|
440
|
+
Chat::TOKEN_KEYS.each do |name|
|
|
441
|
+
meta["#{name}_c"] = current_meta["#{name}_c"].to_i + tokens[name].to_i
|
|
440
442
|
end
|
|
441
443
|
|
|
442
|
-
|
|
444
|
+
Log.medium "Meta: #{Log.fingerprint meta}"
|
|
443
445
|
|
|
444
446
|
meta
|
|
445
447
|
end
|
|
446
448
|
|
|
447
449
|
def reasoning(response, current_meta = nil)
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
|
452
457
|
end
|
|
453
458
|
|
|
454
459
|
|
|
455
460
|
def upload_messages(server, messages, options)
|
|
456
461
|
id = Misc.digest(messages)
|
|
457
462
|
messages.unshift({role: 'backend', content: self::TAG})
|
|
458
|
-
TmpFile.with_file [messages, options
|
|
463
|
+
TmpFile.with_file [messages, options].to_json do |file|
|
|
459
464
|
CMD.cmd("scp #{file} #{server}:.scout/var/query/#{ id }.json")
|
|
460
465
|
end
|
|
461
466
|
id
|
|
@@ -476,8 +481,8 @@ module LLM
|
|
|
476
481
|
def ask(question, options = {}, &block)
|
|
477
482
|
original_options = options.dup
|
|
478
483
|
|
|
479
|
-
return_messages, log_response, current_meta, relay, process = IndiferentHash.process_options options,
|
|
480
|
-
:return_messages, :log_response, :current_meta, :relay, :process,
|
|
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,
|
|
481
486
|
return_messages: false, log_response: true
|
|
482
487
|
|
|
483
488
|
messages = self.messages question, options
|
|
@@ -486,24 +491,37 @@ module LLM
|
|
|
486
491
|
id = upload_messages(relay, messages, options)
|
|
487
492
|
response = gather_response(relay, id)
|
|
488
493
|
IndiferentHash.setup(response)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
tools = tools(formatted_messages, options)
|
|
494
|
+
formatted_prompt = format_messages(messages)
|
|
495
|
+
tools = tools(formatted_prompt, options)
|
|
492
496
|
else
|
|
493
497
|
|
|
494
498
|
client = prepare_client options, messages
|
|
495
|
-
|
|
496
|
-
|
|
499
|
+
prompt = Chat.prepare_prompt(messages, prompt_strategies)
|
|
500
|
+
formatted_prompt = format_messages(prompt)
|
|
501
|
+
tools = tools(formatted_prompt, options)
|
|
497
502
|
|
|
498
503
|
response = begin
|
|
499
504
|
Log.medium "Calling #{self}: #{Log.fingerprint(options.except(:tools))}}"
|
|
500
|
-
query(client,
|
|
501
|
-
rescue Exception
|
|
505
|
+
query(client, formatted_prompt, tools, options)
|
|
506
|
+
rescue Exception => e
|
|
502
507
|
Log.debug 'Asking error. Options: ' + "\n" + JSON.pretty_generate(options.except(:tools))
|
|
503
|
-
|
|
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
|
|
518
|
+
|
|
519
|
+
raise e
|
|
504
520
|
end
|
|
505
521
|
end
|
|
506
522
|
|
|
523
|
+
timestamp = Chat.timestamp
|
|
524
|
+
|
|
507
525
|
if process
|
|
508
526
|
Scout.var.query.response[process].set_extension(:json).write response.to_json
|
|
509
527
|
return response
|
|
@@ -515,16 +533,61 @@ module LLM
|
|
|
515
533
|
|
|
516
534
|
reasoning = reasoning response
|
|
517
535
|
|
|
518
|
-
output =
|
|
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
|
|
519
557
|
|
|
520
558
|
if log_response
|
|
521
559
|
meta = self.update_meta response, current_meta
|
|
522
560
|
meta['reas'] = reasoning if reasoning
|
|
561
|
+
meta['timestamp'] = timestamp
|
|
523
562
|
end
|
|
524
563
|
|
|
525
564
|
output = chain_tools messages, output, tools, options.merge(client: client, tools: tools, log_response: log_response, current_meta: meta, relay: relay)
|
|
526
565
|
|
|
527
|
-
|
|
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
|
|
528
591
|
|
|
529
592
|
if output.last[:role] != :previous_response_id && options[:previous_response_id]
|
|
530
593
|
output << { role: :previous_response_id, content: options[:previous_response_id] }
|
|
@@ -540,37 +603,14 @@ module LLM
|
|
|
540
603
|
end
|
|
541
604
|
|
|
542
605
|
def image(question, options = {}, &block)
|
|
543
|
-
messages =
|
|
544
|
-
options = options.merge LLM.options messages
|
|
545
|
-
tools = LLM.tools messages
|
|
546
|
-
associations = LLM.associations messages
|
|
606
|
+
messages = ask(question, options.merge({tools: [{type: 'image_generation'}], return_messages: true}), &block)
|
|
547
607
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
key ||= LLM.get_url_config(:key, url, :openai_ask, :ask, :openai, env: 'OPENAI_KEY')
|
|
555
|
-
client = LLM::OpenAI.client url, key, log_errors
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
if model.nil?
|
|
559
|
-
url ||= Scout::Config.get(:url, :openai_ask, :ask, :openai, env: 'OPENAI_URL')
|
|
560
|
-
model ||= LLM.get_url_config(:model, url, :openai_ask, :ask, :openai, env: 'OPENAI_MODEL', default: 'gpt-image-1')
|
|
561
|
-
end
|
|
562
|
-
|
|
563
|
-
messages = process_input messages
|
|
564
|
-
input = []
|
|
565
|
-
parameters = {}
|
|
566
|
-
messages.each do |message|
|
|
567
|
-
input << message
|
|
568
|
-
end
|
|
569
|
-
parameters[:prompt] = LLM.print(input)
|
|
570
|
-
|
|
571
|
-
response = client.images.generate(parameters: parameters)
|
|
572
|
-
|
|
573
|
-
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
|
|
574
614
|
end
|
|
575
615
|
|
|
576
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
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
# query() calls ScoutPython.dict2hash, which is defined by the scout/python
|
|
2
|
+
# support (scout-rig), not by this repo: require it up-front so the tool loop
|
|
3
|
+
# path does not hit NameError.
|
|
4
|
+
# ScoutCoder: 'scout/python' alone loads the module but PyCall::List (needed by
|
|
5
|
+
# ScoutPython.py2ruby_a inside dict2hash) is only defined by pycall/list, so
|
|
6
|
+
# require that too, otherwise the offline tool-loop path raises NameError.
|
|
7
|
+
require 'scout/python'
|
|
8
|
+
require 'pycall/list'
|
|
1
9
|
require_relative 'default'
|
|
2
10
|
|
|
3
11
|
module LLM
|
|
@@ -58,7 +66,11 @@ module LLM
|
|
|
58
66
|
parameters[:chat_template] ||= IndiferentHash.pull_keys parameters, :chat_template
|
|
59
67
|
parameters = parameters.keys_to_sym
|
|
60
68
|
response = client.chat(messages, formatted_tools, parameters)
|
|
61
|
-
|
|
69
|
+
# ScoutCoder: ScoutPython.dict2hash only works on real PyCall dicts; fake
|
|
70
|
+
# clients in tests replay plain Ruby Hashes (which have no #get), so only
|
|
71
|
+
# convert when the object actually is a python wrapper.
|
|
72
|
+
response = ScoutPython.dict2hash(response) if defined?(PyCall) && PyCall === response
|
|
73
|
+
response = response.to_h if defined?(PyCall) && (PyCall::Dict === response rescue false)
|
|
62
74
|
IndiferentHash.setup({message: response})
|
|
63
75
|
rescue
|
|
64
76
|
Log.debug 'Input parameters: ' + "\n" + JSON.pretty_generate(parameters.except(:tools))
|
|
@@ -114,6 +114,19 @@ module LLM
|
|
|
114
114
|
info['role'] = 'tool'
|
|
115
115
|
info
|
|
116
116
|
end
|
|
117
|
+
|
|
118
|
+
# ScoutCoder: Backend::ClassMethods#ask assigns into the meta hash
|
|
119
|
+
# (meta['timestamp'] = ...) right after update_meta, so a backend override
|
|
120
|
+
# MUST return a Hash; returning nil crashes the request when log_response
|
|
121
|
+
# is on (the default). Ollama does not report OpenAI-style usage here, so an
|
|
122
|
+
# empty hash keeps the request working.
|
|
123
|
+
def update_meta(response, current_meta = nil)
|
|
124
|
+
{}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def reasoning(response, current_meta = nil)
|
|
128
|
+
end
|
|
129
|
+
|
|
117
130
|
end
|
|
118
131
|
|
|
119
132
|
module OLlama
|
|
@@ -17,20 +17,22 @@ module LLM
|
|
|
17
17
|
include OpenAIMethods
|
|
18
18
|
|
|
19
19
|
def client(options, messages = nil)
|
|
20
|
-
url, key, model = IndiferentHash.process_options options,
|
|
21
|
-
:url, :key, :model
|
|
20
|
+
url, key, model, request_timeout = IndiferentHash.process_options options,
|
|
21
|
+
:url, :key, :model, :request_timeout,
|
|
22
|
+
request_timeout: 12000
|
|
22
23
|
|
|
23
24
|
{
|
|
24
25
|
base_url: url,
|
|
25
26
|
key: key,
|
|
26
27
|
model: model,
|
|
27
28
|
method: :post,
|
|
29
|
+
request_timeout: request_timeout,
|
|
28
30
|
action: 'chat/completions'
|
|
29
31
|
}
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
def query(client, messages, tools = [], parameters = {})
|
|
33
|
-
base_url, key, model, method, action = IndiferentHash.process_options client.dup, :base_url, :key, :model, :method, :action
|
|
35
|
+
base_url, key, model, method, action, timeout = IndiferentHash.process_options client.dup, :base_url, :key, :model, :method, :action, :request_timeout
|
|
34
36
|
url = File.join(base_url, action.to_s)
|
|
35
37
|
|
|
36
38
|
parameters = parameters.dup
|
|
@@ -38,6 +40,9 @@ module LLM
|
|
|
38
40
|
parameters[:tools] = format_tool_definitions(tools) if tools && tools.any?
|
|
39
41
|
parameters[:messages] = messages
|
|
40
42
|
parameters[:verify_ssl] = false
|
|
43
|
+
parameters[:timeout] = timeout.to_i if timeout
|
|
44
|
+
parameters[:read_timeout] = timeout.to_i if timeout
|
|
45
|
+
parameters[:open_timeout] = timeout.to_i if timeout
|
|
41
46
|
|
|
42
47
|
headers = IndiferentHash.setup({ 'Authorization' => "Bearer #{key}", 'Content-Type' => 'application/json' })
|
|
43
48
|
Misc.insist do
|