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
|
@@ -2,15 +2,132 @@ require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
|
|
|
2
2
|
require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1')
|
|
3
3
|
|
|
4
4
|
class TestLLMBedrock < Test::Unit::TestCase
|
|
5
|
+
# Offline Bedrock coverage: LLM::Bedrock.ask/embed accept options[:client]
|
|
6
|
+
# (an object with invoke_model(model_id:, content_type:, body:) returning
|
|
7
|
+
# something whose .body.string is JSON), so FakeBedrockClient exercises the
|
|
8
|
+
# full request construction + response parsing path without AWS credentials.
|
|
9
|
+
#
|
|
10
|
+
# The old real-service versions are kept disabled below (_test_*).
|
|
11
|
+
|
|
12
|
+
# ScoutCoder: LLM::Bedrock is not required by lib/scout-ai.rb; each test has
|
|
13
|
+
# to require 'scout/llm/backends/bedrock' itself (same as LLM.ask does when
|
|
14
|
+
# dispatching to the :bedrock backend).
|
|
15
|
+
def test_ask
|
|
16
|
+
require 'scout/llm/backends/bedrock'
|
|
17
|
+
client = TestFixtures.bedrock_client('backends/bedrock')
|
|
18
|
+
|
|
19
|
+
response = LLM::Bedrock.ask 'user: say hi to bedrock',
|
|
20
|
+
client: client,
|
|
21
|
+
model: 'anthropic.claude-3-sonnet-20240229-v1:0',
|
|
22
|
+
model_max_tokens: 100
|
|
23
|
+
|
|
24
|
+
assert_equal 'Mock answer from Bedrock', response
|
|
25
|
+
|
|
26
|
+
assert_equal 1, client.calls.length
|
|
27
|
+
call = client.calls.first
|
|
28
|
+
assert_equal 'anthropic.claude-3-sonnet-20240229-v1:0', call[:model_id]
|
|
29
|
+
assert_equal 'application/json', call[:content_type]
|
|
30
|
+
# ScoutCoder: IndiferentHash#pretty_print takes 0 args, so assert on
|
|
31
|
+
# individual keys instead of pp-ing the recorded parameters.
|
|
32
|
+
assert_equal 100, call[:body]['max_tokens']
|
|
33
|
+
assert call[:body]['messages'].any? { |m| m['role'] == 'user' }
|
|
34
|
+
assert call[:body]['messages'].any? { |m| m['content'].to_s.include?('say hi to bedrock') }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_ask_prompt_type
|
|
38
|
+
require 'scout/llm/backends/bedrock'
|
|
39
|
+
client = TestFixtures.bedrock_client('backends/bedrock')
|
|
40
|
+
|
|
41
|
+
response = LLM::Bedrock.ask 'user: say hi through the prompt endpoint',
|
|
42
|
+
client: client, type: :prompt,
|
|
43
|
+
model: 'meta.llama3-8b-instruct-v1:0',
|
|
44
|
+
model_max_tokens: 100
|
|
45
|
+
|
|
46
|
+
assert_equal 'Mock answer from Bedrock', response
|
|
47
|
+
|
|
48
|
+
body = client.calls.first[:body]
|
|
49
|
+
assert body.include?('prompt')
|
|
50
|
+
assert body['prompt'].to_s.include?('say hi through the prompt endpoint')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_embeddings
|
|
54
|
+
require 'scout/llm/backends/bedrock'
|
|
55
|
+
client = TestFixtures.bedrock_client('backends/bedrock_embedding')
|
|
56
|
+
|
|
57
|
+
emb = LLM::Bedrock.embed 'Some text', client: client,
|
|
58
|
+
model: 'amazon.titan-embed-text-v1'
|
|
59
|
+
|
|
60
|
+
assert(Float === emb.first)
|
|
61
|
+
assert_equal [0.1, 0.2, 0.3], emb
|
|
62
|
+
assert_equal 1, client.calls.length
|
|
63
|
+
|
|
64
|
+
call = client.calls.first
|
|
65
|
+
assert_equal 'amazon.titan-embed-text-v1', call[:model_id]
|
|
66
|
+
assert_equal 'Some text', call[:body]['inputText']
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Tool loop: the first payload carries a tool_call content entry, the block
|
|
70
|
+
# answers it, and the second payload is the final text answer.
|
|
71
|
+
def test_tool_loop
|
|
72
|
+
require 'scout/llm/backends/bedrock'
|
|
73
|
+
tools = [
|
|
74
|
+
{
|
|
75
|
+
"type": "function",
|
|
76
|
+
"function": {
|
|
77
|
+
"name": "get_current_temperature",
|
|
78
|
+
"description": "Get the current temperature for a specific location",
|
|
79
|
+
"parameters": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"properties": {
|
|
82
|
+
"location": { "type": "string",
|
|
83
|
+
"description": "The city and state, e.g., San Francisco, CA" },
|
|
84
|
+
"unit": { "type": "string", "enum": ["Celsius", "Fahrenheit"],
|
|
85
|
+
"description": "The temperature unit to use." }
|
|
86
|
+
},
|
|
87
|
+
"required": ["location", "unit"]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
client = TestFixtures.bedrock_client('backends/bedrock_tool_use', 'backends/bedrock')
|
|
94
|
+
|
|
95
|
+
calls_seen = []
|
|
96
|
+
response = LLM::Bedrock.ask 'user: What is the weather in London? Should I take an umbrella? Use the tool.',
|
|
97
|
+
tools: tools, client: client,
|
|
98
|
+
model: 'anthropic.claude-3-sonnet-20240229-v1:0',
|
|
99
|
+
model_max_tokens: 100 do |name, arguments|
|
|
100
|
+
calls_seen << [name, arguments]
|
|
101
|
+
"It's 15 degrees and raining."
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
assert_equal 'Mock answer from Bedrock', response
|
|
105
|
+
|
|
106
|
+
# the block was reached with the unwrapped tool call
|
|
107
|
+
assert_equal 1, calls_seen.length
|
|
108
|
+
assert_equal 'get_current_temperature', calls_seen.first.first
|
|
109
|
+
assert_equal 'London', calls_seen.first.last['location']
|
|
110
|
+
|
|
111
|
+
# two invoke_model rounds, the second one carrying the tool response
|
|
112
|
+
assert_equal 2, client.calls.length
|
|
113
|
+
sent = client.calls.last[:body]['messages']
|
|
114
|
+
assert sent.any? { |m| m['role'] == 'tool' }
|
|
115
|
+
tool_messages = sent.select { |m| m['role'] == 'tool' }
|
|
116
|
+
assert_equal 'It\'s 15 degrees and raining.', tool_messages.last['content']
|
|
117
|
+
assert_equal 'call_1', tool_messages.last['id']
|
|
118
|
+
end
|
|
119
|
+
|
|
5
120
|
def _test_ask
|
|
121
|
+
# Real-service version: see the offline tests above. Requires AWS
|
|
122
|
+
# credentials and network.
|
|
6
123
|
prompt =<<-EOF
|
|
7
124
|
say hi
|
|
8
125
|
EOF
|
|
9
126
|
ppp LLM::Bedrock.ask prompt, model: "anthropic.claude-3-sonnet-20240229-v1:0", model_max_tokens: 100, model_anthropic_version: 'bedrock-2023-05-31'
|
|
10
127
|
end
|
|
11
128
|
|
|
12
|
-
|
|
13
129
|
def _test_embeddings
|
|
130
|
+
# Real-service version: see test_embeddings above.
|
|
14
131
|
Log.severity = 0
|
|
15
132
|
text =<<-EOF
|
|
16
133
|
Some text
|
|
@@ -57,4 +174,3 @@ What is the weather in London. Should I take my umbrella? Use the provided tool
|
|
|
57
174
|
ppp response
|
|
58
175
|
end
|
|
59
176
|
end
|
|
60
|
-
|
|
@@ -3,14 +3,72 @@ require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1
|
|
|
3
3
|
|
|
4
4
|
class TestLLMOllama < Test::Unit::TestCase
|
|
5
5
|
|
|
6
|
-
def
|
|
7
|
-
|
|
6
|
+
def test_ask
|
|
7
|
+
client = TestFixtures.ollama_client('backends/ollama')
|
|
8
|
+
res = LLM::OLlama.ask 'user: write a script that sorts files in a directory',
|
|
9
|
+
client: client, model: 'mistral', mode: 'chat', persist: false
|
|
10
|
+
|
|
11
|
+
assert_equal 'Mock answer from Ollama', res
|
|
12
|
+
assert_equal 1, client.calls.length
|
|
13
|
+
assert_equal 'mistral', client.calls.first[:model]
|
|
14
|
+
assert client.calls.first[:messages].any? { |m| m[:role].to_s == 'user' }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_tool
|
|
8
18
|
prompt =<<-EOF
|
|
9
|
-
|
|
10
|
-
system: Avoid using backticks ``` to format code.
|
|
11
|
-
user: write a script that sorts files in a directory
|
|
19
|
+
What is the weather in London. Should I take an umbrella?
|
|
12
20
|
EOF
|
|
13
|
-
|
|
21
|
+
|
|
22
|
+
tools = [
|
|
23
|
+
{
|
|
24
|
+
"type": "function",
|
|
25
|
+
"function": {
|
|
26
|
+
"name": "get_current_temperature",
|
|
27
|
+
"description": "Get the current temperature for a specific location",
|
|
28
|
+
"parameters": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"properties": {
|
|
31
|
+
"location": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "The city and state, e.g., San Francisco, CA"
|
|
34
|
+
},
|
|
35
|
+
"unit": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"enum": ["Celsius", "Fahrenheit"],
|
|
38
|
+
"description": "The temperature unit to use. Infer this from the user's location."
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"required": ["location", "unit"]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
client = TestFixtures.ollama_client('backends/ollama_tool_call', 'backends/ollama')
|
|
48
|
+
respose = LLM::OLlama.ask prompt, model: 'gpt-oss',
|
|
49
|
+
client: client, tool_choice: 'required',
|
|
50
|
+
tools: tools, persist: false do |name,arguments|
|
|
51
|
+
"It's raining cats and dogs"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
assert_equal 'Mock answer from Ollama', respose
|
|
55
|
+
assert_equal 2, client.calls.length
|
|
56
|
+
|
|
57
|
+
# ScoutCoder: LLM::OLlama#query calls client.chat(parameters) positionally
|
|
58
|
+
# (no `parameters:` kwarg) and the API returns an Array of chunk hashes.
|
|
59
|
+
sent_tools = client.calls.first[:tools]
|
|
60
|
+
assert sent_tools.any? { |t| (t.dig(:function, :name) || t.dig('function', 'name')) == 'get_current_temperature' }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_embeddings
|
|
64
|
+
payload = [{ 'embeddings' => [[0.1, 0.2, 0.3]] }]
|
|
65
|
+
client = FakeOllamaClient.new(payload)
|
|
66
|
+
|
|
67
|
+
emb = LLM::OLlama.embed 'Some text', client: client, model: 'mxbai-embed-large'
|
|
68
|
+
|
|
69
|
+
assert(Float === emb.first)
|
|
70
|
+
assert_equal [0.1, 0.2, 0.3], emb
|
|
71
|
+
assert_equal 'Some text', client.calls.first[:input]
|
|
14
72
|
end
|
|
15
73
|
|
|
16
74
|
def _test_tool_call_output
|
|
@@ -89,22 +147,14 @@ What is the weather in London. Should I take an umbrella?
|
|
|
89
147
|
ppp respose
|
|
90
148
|
end
|
|
91
149
|
|
|
92
|
-
def _test_embeddings
|
|
93
|
-
Log.severity = 0
|
|
94
|
-
text =<<-EOF
|
|
95
|
-
Some text
|
|
96
|
-
EOF
|
|
97
|
-
emb = LLM::OLlama.embed text, model: 'mxbai-embed-large', url: 'localhost:3331'
|
|
98
|
-
assert(Float === emb.first)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
150
|
def test_embedding_array
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
151
|
+
payload = [{ 'embeddings' => [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]] }]
|
|
152
|
+
client = FakeOllamaClient.new(payload)
|
|
153
|
+
|
|
154
|
+
emb = LLM::OLlama.embed ['Some text', 'More text'], client: client, model: 'mxbai-embed-large'
|
|
155
|
+
|
|
107
156
|
assert(Float === emb.first.first)
|
|
157
|
+
assert_equal [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]], emb
|
|
108
158
|
end
|
|
109
159
|
end
|
|
110
160
|
|
|
@@ -2,13 +2,12 @@ require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
|
|
|
2
2
|
require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1')
|
|
3
3
|
|
|
4
4
|
class TestOpenWebUI < Test::Unit::TestCase
|
|
5
|
-
|
|
5
|
+
# Real https://gepeto.bsc.es/api version moved to
|
|
6
|
+
# test/integration/scout/llm/backends/test_openwebui.rb (no client seam:
|
|
7
|
+
# LLM::OpenWebUIMethods#query posts through RestClient.post with a plain
|
|
8
|
+
# Hash "client").
|
|
9
|
+
def _test_gepeto
|
|
6
10
|
Log.severity = 0
|
|
7
|
-
prompt =<<-EOF
|
|
8
|
-
system: you are a coding helper that only write code and comments without formatting so that it can work directly, avoid the initial and end commas ```.
|
|
9
|
-
user: write a script that sorts files in a directory
|
|
10
|
-
EOF
|
|
11
|
-
|
|
12
11
|
prompt =<<-EOF
|
|
13
12
|
user: write a script that sorts files in a directory
|
|
14
13
|
EOF
|
|
@@ -16,42 +15,45 @@ user: write a script that sorts files in a directory
|
|
|
16
15
|
ppp LLM::OpenWebUI.ask prompt, model: 'qwen3-vl:30b', url: "https://gepeto.bsc.es/api"
|
|
17
16
|
end
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
# Offline: stub RestClient.post so only request construction and response
|
|
19
|
+
# parsing are exercised. OpenWebUI is OpenAI-compatible, so the openai_chat
|
|
20
|
+
# fixture is replayed as the response body.
|
|
21
|
+
def test_ask_request_construction
|
|
22
|
+
fixture = TestFixtures.fixture('backends/openai_chat')
|
|
23
|
+
expected_answer = fixture.dig('choices', 0, 'message', 'content')
|
|
24
|
+
|
|
25
|
+
recorded = []
|
|
26
|
+
original = RestClient.method(:post)
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"name": "get_current_temperature",
|
|
29
|
-
"description": "Get the current temperature for a specific location",
|
|
30
|
-
"parameters": {
|
|
31
|
-
"type": "object",
|
|
32
|
-
"properties": {
|
|
33
|
-
"location": {
|
|
34
|
-
"type": "string",
|
|
35
|
-
"description": "The city and state, e.g., San Francisco, CA"
|
|
36
|
-
},
|
|
37
|
-
"unit": {
|
|
38
|
-
"type": "string",
|
|
39
|
-
"enum": ["Celsius", "Fahrenheit"],
|
|
40
|
-
"description": "The temperature unit to use. Infer this from the user's location."
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
"required": ["location", "unit"]
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
sss 0
|
|
50
|
-
respose = LLM::OpenWebUI.ask prompt, model: 'gemma2:latest', tool_choice: 'required', tools: tools do |name,arguments|
|
|
51
|
-
"It's raining cats and dogs"
|
|
28
|
+
RestClient.define_singleton_method(:post) do |url, payload, headers|
|
|
29
|
+
recorded << {url: url, payload: JSON.parse(payload), headers: headers}
|
|
30
|
+
body = Struct.new(:body).new(fixture.to_json)
|
|
31
|
+
body
|
|
52
32
|
end
|
|
53
33
|
|
|
54
|
-
|
|
34
|
+
begin
|
|
35
|
+
answer = LLM::OpenWebUI.ask 'user: write a script that sorts files in a directory offline',
|
|
36
|
+
model: 'qwen3-vl:30b', url: 'https://openwebui.example/api',
|
|
37
|
+
key: 'test-key', persist: false
|
|
38
|
+
ensure
|
|
39
|
+
RestClient.singleton_class.send(:define_method, :post, original)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
assert_equal expected_answer, answer
|
|
43
|
+
|
|
44
|
+
assert_equal 1, recorded.length
|
|
45
|
+
call = recorded.first
|
|
46
|
+
|
|
47
|
+
assert call[:url].end_with?('chat/completions')
|
|
48
|
+
assert call[:url].start_with?('https://openwebui.example/api')
|
|
49
|
+
|
|
50
|
+
payload = call[:payload]
|
|
51
|
+
assert_equal 'qwen3-vl:30b', payload['model']
|
|
52
|
+
assert payload['messages'].any? { |m| m['role'] == 'user' }
|
|
53
|
+
|
|
54
|
+
headers = call[:headers]
|
|
55
|
+
assert headers['Authorization'] || headers[:Authorization]
|
|
56
|
+
assert_equal 'Bearer test-key', (headers['Authorization'] || headers[:Authorization]).to_s
|
|
57
|
+
assert((headers['Content-Type'] || headers[:Content_Type]).to_s.include?('application/json'))
|
|
55
58
|
end
|
|
56
59
|
end
|
|
57
|
-
|
|
@@ -2,9 +2,11 @@ require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
|
|
|
2
2
|
require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1')
|
|
3
3
|
|
|
4
4
|
class TestRelay < Test::Unit::TestCase
|
|
5
|
-
|
|
5
|
+
# Real scp/ssh relay version moved to
|
|
6
|
+
# test/integration/scout/llm/backends/test_relay.rb: LLM::Relay shells out
|
|
7
|
+
# to `scp` and has no client seam, so the unit copy stays disabled.
|
|
8
|
+
def _test_ask
|
|
6
9
|
Scout::Config.set(:server, 'localhost', :relay)
|
|
7
10
|
ppp LLM::Relay.ask 'Say hi', model: 'gemma2'
|
|
8
11
|
end
|
|
9
12
|
end
|
|
10
|
-
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Reusable offline fixtures for agent_meta provenance tests (plan fixtures C,
|
|
2
|
+
# D, E; also intended for the later token-collector rounds). Everything runs
|
|
3
|
+
# on TmpFile.with_dir directories plus persisted-style chat text; no providers.
|
|
4
|
+
#
|
|
5
|
+
# Helpers provided:
|
|
6
|
+
# write_chat(dir, name, text) -> chat file path
|
|
7
|
+
# meta_receipt(content) -> agent_meta entry Hash
|
|
8
|
+
# receipt_output(call_id, agent_meta, ...) -> JSON envelope String
|
|
9
|
+
# receipt_chat_text(receipts, extra: nil) -> persisted chat text
|
|
10
|
+
# plain_delegation_chat(job_path) -> persisted chat text
|
|
11
|
+
# make_job(dir, ref, dependencies:, logs:) -> job path
|
|
12
|
+
# visit_signature(visits) -> comparable Array
|
|
13
|
+
# fixture_c(dir) -> C/D layout paths
|
|
14
|
+
# truncated_receipt_chat(call_id, agent_meta) -> fixture H text
|
|
15
|
+
require 'fileutils'
|
|
16
|
+
require 'json'
|
|
17
|
+
|
|
18
|
+
module AgentMetaFixtures
|
|
19
|
+
# Write persisted-style chat text and return its absolute path.
|
|
20
|
+
def write_chat(dir, name, text)
|
|
21
|
+
path = File.expand_path(File.join(dir, name))
|
|
22
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
23
|
+
File.write(path, text)
|
|
24
|
+
path
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# One agent_meta receipt entry with direct meta content, e.g.
|
|
28
|
+
# meta_receipt('pt=10 ct=4 tt=14 inference_id=w1') or
|
|
29
|
+
# meta_receipt('job=Worker/ask/Default_w').
|
|
30
|
+
def meta_receipt(content)
|
|
31
|
+
{role: 'meta', content: content}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# JSON payload of a function_call_output envelope carrying agent_meta.
|
|
35
|
+
def receipt_output(call_id, agent_meta, name: 'ask', content: 'child answer')
|
|
36
|
+
{name: name, content: content, id: call_id, agent_meta: agent_meta}.to_json
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Persisted chat text with one paired ask call per receipt entry. Hash keys
|
|
40
|
+
# are call ids, values are the agent_meta payloads (Arrays, Strings, ...).
|
|
41
|
+
# `extra` lines are appended after the receipts (e.g. local meta lines).
|
|
42
|
+
#
|
|
43
|
+
# Message indexes produced by Chat.parse (single user turn, no leading
|
|
44
|
+
# empty user message since 49c0d20):
|
|
45
|
+
# 0 user, then per receipt: function_call, function_call_output.
|
|
46
|
+
def receipt_chat_text(receipts, extra: nil)
|
|
47
|
+
lines = ['user: Run the worker']
|
|
48
|
+
receipts.each do |call_id, agent_meta|
|
|
49
|
+
lines << 'function_call: ' + %({"name":"ask","arguments":{},"id":"#{call_id}"})
|
|
50
|
+
lines << 'function_call_output: ' + receipt_output(call_id, agent_meta)
|
|
51
|
+
end
|
|
52
|
+
lines.concat(Array(extra)) if extra
|
|
53
|
+
lines << 'assistant: done'
|
|
54
|
+
lines * "\n" + "\n"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Persisted chat text delegating to `job_path` through an ordinary local
|
|
58
|
+
# `meta: job=` message (no receipts at all).
|
|
59
|
+
def plain_delegation_chat(job_path)
|
|
60
|
+
"user: Run the worker\nmeta: job=#{job_path}\nassistant: done\n"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Create a job layout under `dir` for the relative reference `ref`
|
|
64
|
+
# (e.g. 'Worker/ask/Default_w'): the result file, a .info sidecar with
|
|
65
|
+
# `dependencies` (always written, matching scout-gear Step), and log chats
|
|
66
|
+
# written under `<job>.files/log/<name>` (Hash name -> chat text). Returns
|
|
67
|
+
# the job path.
|
|
68
|
+
def make_job(dir, ref, result: 'answer', dependencies: [], logs: {})
|
|
69
|
+
path = File.expand_path(File.join(dir, ref))
|
|
70
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
71
|
+
File.write(path, result)
|
|
72
|
+
File.write(path + '.info', {dependencies: dependencies}.to_json)
|
|
73
|
+
logs.each do |name, text|
|
|
74
|
+
log_path = File.join(path + '.files', 'log', name)
|
|
75
|
+
FileUtils.mkdir_p(File.dirname(log_path))
|
|
76
|
+
File.write(log_path, text)
|
|
77
|
+
end
|
|
78
|
+
path
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Comparable signature of traverse_provenance visits:
|
|
82
|
+
# [kind, path, relation, first_visit].
|
|
83
|
+
def visit_signature(visits)
|
|
84
|
+
visits.collect do |kind, object, _parent_kind, _parent, relation, first|
|
|
85
|
+
[kind, object.respond_to?(:path) ? object.path.to_s : object.to_s, relation, first]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Fixture C layout (plan fixtures C/D): a parent receipt points at a Worker
|
|
90
|
+
# job whose log chat has two direct metas (w1, w2), a nested receipt for a
|
|
91
|
+
# Critic job (with one direct meta c1), and a dependency job. The parent
|
|
92
|
+
# chat also carries an ordinary local `meta: job=` reference to the Critic so
|
|
93
|
+
# both discovery paths coexist, plus one receipt-only direct meta (shadow).
|
|
94
|
+
# Returns [parent_chat_path, worker_job_path, critic_job_path, dep_job_path].
|
|
95
|
+
def fixture_c(dir)
|
|
96
|
+
critic = make_job(dir, 'Critic/ask/Default_c')
|
|
97
|
+
dep = make_job(dir, 'Dep/load/Default_1')
|
|
98
|
+
|
|
99
|
+
worker_log = receipt_chat_text(
|
|
100
|
+
{'wc1' => [meta_receipt('pt=30 ct=10 tt=40 inference_id=c1'),
|
|
101
|
+
meta_receipt("job=#{critic}")]},
|
|
102
|
+
extra: ['meta: pt=100 ct=50 tt=150 inference_id=w1',
|
|
103
|
+
'meta: pt=20 ct=10 tt=30 inference_id=w2']
|
|
104
|
+
)
|
|
105
|
+
worker = make_job(dir, 'Worker/ask/Default_w',
|
|
106
|
+
dependencies: [dep], logs: {'agent.chat' => worker_log})
|
|
107
|
+
|
|
108
|
+
parent = write_chat(dir, 'parent.chat',
|
|
109
|
+
receipt_chat_text(
|
|
110
|
+
{'p1' => [meta_receipt('pt=10 tt=12 inference_id=shadow'),
|
|
111
|
+
meta_receipt("job=#{worker}")]},
|
|
112
|
+
extra: ["meta: job=#{critic}"]
|
|
113
|
+
))
|
|
114
|
+
|
|
115
|
+
[parent, worker, critic, dep]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Fixture H: the output content is the standard truncation exception JSON
|
|
119
|
+
# (error: :truncated) exactly as LLM.process_calls serializes it, while the
|
|
120
|
+
# agent_meta receipts survive in the same envelope.
|
|
121
|
+
def truncated_receipt_chat(call_id, agent_meta, name: 'ask', characters: 90_000)
|
|
122
|
+
exception_msg = "Function #{name} #{call_id} was executed successfully, but it returned #{characters} characters, which is more than the maximum of 30000. To protect the model context window this result was not returned."
|
|
123
|
+
content = {exception: exception_msg, stack: ['a', 'b']}.to_json
|
|
124
|
+
payload = {name: name, content: content, id: call_id, error: :truncated,
|
|
125
|
+
agent_meta: agent_meta}.to_json
|
|
126
|
+
"user: Run\n" +
|
|
127
|
+
%({"name":"#{name}","arguments":{},"id":"#{call_id}"}).sub(/^/, 'function_call: ') + "\n" +
|
|
128
|
+
"function_call_output: #{payload}\n" +
|
|
129
|
+
"assistant: done\n"
|
|
130
|
+
end
|
|
131
|
+
end
|