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
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
|
|
2
|
+
require 'scout/llm/chat'
|
|
3
|
+
|
|
4
|
+
class TestChatAgentMeta < Test::Unit::TestCase
|
|
5
|
+
def chat(text)
|
|
6
|
+
Chat.setup(LLM.messages(text))
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Build persisted-style chat text with one paired tool call whose output
|
|
10
|
+
# envelope carries a receipt. Legacy format uses the `agent_meta` key with
|
|
11
|
+
# serialized meta messages; the current format uses the `meta` key with an
|
|
12
|
+
# array of already-deserialized field Hashes (as LLM.process_calls now
|
|
13
|
+
# writes for delegated agents).
|
|
14
|
+
def receipt_text(agent_meta: nil, meta: nil, name: 'ask', call_id: 'call-1', content: 'child answer')
|
|
15
|
+
envelope = {name: name, content: content, id: call_id}
|
|
16
|
+
envelope[:agent_meta] = agent_meta unless agent_meta.nil?
|
|
17
|
+
envelope[:meta] = meta unless meta.nil?
|
|
18
|
+
<<-EOF
|
|
19
|
+
user: Run the worker
|
|
20
|
+
function_call: {"name":"#{name}","arguments":{"agent":"Worker"},"id":"#{call_id}"}
|
|
21
|
+
function_call_output: #{envelope.to_json}
|
|
22
|
+
EOF
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Current-format equivalent of `valid_receipt`
|
|
26
|
+
def valid_meta_receipt
|
|
27
|
+
receipt_text(meta: [
|
|
28
|
+
{'pt' => 100, 'ct' => 50, 'tt' => 150, 'inference_id' => 'aaa'},
|
|
29
|
+
{'job' => 'Worker/ask/Default_x'}
|
|
30
|
+
])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def valid_receipt
|
|
34
|
+
receipt_text(agent_meta: [
|
|
35
|
+
{role: 'meta', content: 'pt=100 ct=50 tt=150 inference_id=aaa'},
|
|
36
|
+
{role: 'meta', content: 'job=Worker/ask/Default_x'}
|
|
37
|
+
])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_extracts_valid_receipt_entries
|
|
41
|
+
evidence = Chat.agent_meta_evidence(chat(valid_receipt))
|
|
42
|
+
assert_equal 2, evidence.length
|
|
43
|
+
|
|
44
|
+
direct, projection = evidence
|
|
45
|
+
|
|
46
|
+
assert_equal :agent_meta, direct[:origin]
|
|
47
|
+
assert_equal 100, direct[:meta][:pt]
|
|
48
|
+
assert_equal 50, direct[:meta][:ct]
|
|
49
|
+
assert_equal 150, direct[:meta][:tt]
|
|
50
|
+
assert_equal 'aaa', direct[:meta][:inference_id]
|
|
51
|
+
assert_equal 'call-1', direct[:call_id]
|
|
52
|
+
assert_equal 'ask', direct[:tool_name]
|
|
53
|
+
assert_equal 0, direct[:agent_meta_index]
|
|
54
|
+
assert_equal 2, direct[:output_address]
|
|
55
|
+
assert_equal [2, :agent_meta, 0], direct[:evidence_address]
|
|
56
|
+
assert_nil direct[:source]
|
|
57
|
+
assert_equal({role: 'meta', content: 'pt=100 ct=50 tt=150 inference_id=aaa'}, direct[:raw_message])
|
|
58
|
+
|
|
59
|
+
assert_equal 1, projection[:agent_meta_index]
|
|
60
|
+
assert_equal [2, :agent_meta, 1], projection[:evidence_address]
|
|
61
|
+
assert_equal 'Worker/ask/Default_x', projection[:meta][:job]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_extracts_receipt_entries_from_persisted_file_with_source
|
|
65
|
+
TmpFile.with_dir do |dir|
|
|
66
|
+
path = File.join(dir, 'parent.chat')
|
|
67
|
+
Open.write(path, valid_receipt)
|
|
68
|
+
conversation = Chat.load(path)
|
|
69
|
+
|
|
70
|
+
evidence = Chat.agent_meta_evidence(conversation, source: path)
|
|
71
|
+
assert_equal 2, evidence.length
|
|
72
|
+
|
|
73
|
+
direct = evidence.first
|
|
74
|
+
assert_equal path, direct[:source]
|
|
75
|
+
assert_equal [path, 2], direct[:output_address]
|
|
76
|
+
assert_equal [path, 2, :agent_meta, 0], direct[:evidence_address]
|
|
77
|
+
assert_equal 'call-1', direct[:call_id]
|
|
78
|
+
assert_equal 'ask', direct[:tool_name]
|
|
79
|
+
|
|
80
|
+
assert_equal [path, 2, :agent_meta, 1], evidence.last[:evidence_address]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_absent_agent_meta_returns_empty_evidence
|
|
85
|
+
plain = receipt_text
|
|
86
|
+
assert_equal [], Chat.agent_meta_evidence(chat(plain))
|
|
87
|
+
|
|
88
|
+
without_calls = chat(<<-EOF)
|
|
89
|
+
user: Hello
|
|
90
|
+
assistant: Hi
|
|
91
|
+
EOF
|
|
92
|
+
assert_equal [], Chat.agent_meta_evidence(without_calls)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_agent_meta_not_an_array_is_skipped_with_warning
|
|
96
|
+
warnings = []
|
|
97
|
+
evidence = Chat.agent_meta_evidence(chat(receipt_text(agent_meta: 'oops')), warnings: warnings)
|
|
98
|
+
assert_equal [], evidence
|
|
99
|
+
assert_equal 1, warnings.length
|
|
100
|
+
|
|
101
|
+
warning = warnings.first
|
|
102
|
+
assert_equal :agent_meta, warning[:origin]
|
|
103
|
+
assert_equal :not_an_array, warning[:reason]
|
|
104
|
+
assert_equal 2, warning[:output_address]
|
|
105
|
+
assert_equal 'call-1', warning[:call_id]
|
|
106
|
+
assert_equal 'ask', warning[:tool_name]
|
|
107
|
+
assert_nil warning[:agent_meta_index]
|
|
108
|
+
assert_equal 'oops', warning[:raw_entry]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def test_non_hash_entry_is_skipped_with_warning
|
|
112
|
+
warnings = []
|
|
113
|
+
evidence = Chat.agent_meta_evidence(chat(receipt_text(agent_meta: ['nonsense'])), warnings: warnings)
|
|
114
|
+
assert_equal [], evidence
|
|
115
|
+
assert_equal 1, warnings.length
|
|
116
|
+
assert_equal :not_a_hash, warnings.first[:reason]
|
|
117
|
+
assert_equal 0, warnings.first[:agent_meta_index]
|
|
118
|
+
assert_equal 'nonsense', warnings.first[:raw_entry]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_entry_with_wrong_role_is_skipped_with_warning
|
|
122
|
+
warnings = []
|
|
123
|
+
agent_meta = [
|
|
124
|
+
{role: 'assistant', content: 'not a meta record'},
|
|
125
|
+
{role: 'meta', content: 'pt=1 tt=1 inference_id=keep'}
|
|
126
|
+
]
|
|
127
|
+
evidence = Chat.agent_meta_evidence(chat(receipt_text(agent_meta: agent_meta)), warnings: warnings)
|
|
128
|
+
assert_equal 1, evidence.length
|
|
129
|
+
assert_equal 1, warnings.length
|
|
130
|
+
assert_equal :invalid_role, warnings.first[:reason]
|
|
131
|
+
assert_equal 0, warnings.first[:agent_meta_index]
|
|
132
|
+
assert_equal({ 'role' => 'assistant', 'content' => 'not a meta record' }, warnings.first[:raw_entry])
|
|
133
|
+
assert_equal [2, :agent_meta, 1], evidence.first[:evidence_address]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def test_entry_without_string_content_is_skipped_with_warning
|
|
137
|
+
warnings = []
|
|
138
|
+
agent_meta = [
|
|
139
|
+
{role: 'meta'},
|
|
140
|
+
{role: 'meta', content: 42},
|
|
141
|
+
{role: 'meta', content: 'pt=1 tt=1 inference_id=keep'}
|
|
142
|
+
]
|
|
143
|
+
evidence = Chat.agent_meta_evidence(chat(receipt_text(agent_meta: agent_meta)), warnings: warnings)
|
|
144
|
+
assert_equal 1, evidence.length
|
|
145
|
+
assert_equal 2, warnings.length
|
|
146
|
+
assert_equal %i[invalid_content invalid_content], warnings.collect { |w| w[:reason] }
|
|
147
|
+
assert_equal 0, warnings.first[:agent_meta_index]
|
|
148
|
+
assert_equal 1, warnings.last[:agent_meta_index]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def test_unparseable_content_is_skipped_with_warning
|
|
152
|
+
warnings = []
|
|
153
|
+
evidence = Chat.agent_meta_evidence(chat(receipt_text(agent_meta: [{role: 'meta', content: 'no key value pairs here'}])), warnings: warnings)
|
|
154
|
+
assert_equal [], evidence
|
|
155
|
+
assert_equal 1, warnings.length
|
|
156
|
+
assert_equal :unparseable_meta, warnings.first[:reason]
|
|
157
|
+
assert_equal 0, warnings.first[:agent_meta_index]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def test_malformed_entries_are_skipped_without_warnings_kwarg
|
|
161
|
+
evidence = Chat.agent_meta_evidence(chat(receipt_text(agent_meta: [{role: 'assistant', content: 'x'}])))
|
|
162
|
+
assert_equal [], evidence
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def test_meta_evidence_combines_both_origins
|
|
166
|
+
text = valid_receipt + "\nmeta: pt=10 tt=12 inference_id=local1\nassistant: done\n"
|
|
167
|
+
conversation = chat(text)
|
|
168
|
+
|
|
169
|
+
evidence = Chat.meta_evidence(conversation)
|
|
170
|
+
assert_equal 3, evidence.length
|
|
171
|
+
|
|
172
|
+
origins = evidence.collect { |record| record[:origin] }
|
|
173
|
+
assert_equal %i[chat_meta agent_meta agent_meta], origins
|
|
174
|
+
|
|
175
|
+
local = evidence.first
|
|
176
|
+
assert_equal 10, local[:meta][:pt]
|
|
177
|
+
assert_equal 12, local[:meta][:tt]
|
|
178
|
+
assert_equal 3, local[:meta_address]
|
|
179
|
+
assert_nil local[:source]
|
|
180
|
+
assert_equal 'meta', local[:message][:role].to_s
|
|
181
|
+
assert_equal 'pt=10 tt=12 inference_id=local1', local[:message][:content]
|
|
182
|
+
|
|
183
|
+
assert evidence.last(2).all? { |record| record[:origin] == :agent_meta }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def test_meta_evidence_preserves_source_addresses
|
|
187
|
+
TmpFile.with_dir do |dir|
|
|
188
|
+
path = File.join(dir, 'parent.chat')
|
|
189
|
+
Open.write(path, valid_receipt + "\nmeta: pt=10 tt=12 inference_id=local1\nassistant: done\n")
|
|
190
|
+
conversation = Chat.load(path)
|
|
191
|
+
|
|
192
|
+
evidence = Chat.meta_evidence(conversation, source: path)
|
|
193
|
+
assert_equal [path, 3], evidence.first[:meta_address]
|
|
194
|
+
assert_equal [path, 2, :agent_meta, 0], evidence[1][:evidence_address]
|
|
195
|
+
assert_equal [path, 2, :agent_meta, 1], evidence[2][:evidence_address]
|
|
196
|
+
assert evidence.all? { |record| record[:source] == path }
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def test_extraction_does_not_change_chat_contents
|
|
201
|
+
conversation = chat(valid_receipt + "\nmeta: pt=10 tt=12 inference_id=local1\nassistant: done\n")
|
|
202
|
+
before = conversation.collect { |message| [message[:role].to_s, message[:content].to_s] }
|
|
203
|
+
|
|
204
|
+
Chat.agent_meta_evidence(conversation)
|
|
205
|
+
Chat.meta_evidence(conversation)
|
|
206
|
+
Chat.agent_meta_job_references(conversation)
|
|
207
|
+
|
|
208
|
+
after = conversation.collect { |message| [message[:role].to_s, message[:content].to_s] }
|
|
209
|
+
assert_equal before, after
|
|
210
|
+
assert_equal 5, conversation.length
|
|
211
|
+
|
|
212
|
+
roles = conversation.collect { |message| message[:role].to_s }
|
|
213
|
+
assert_equal %w[user function_call function_call_output meta assistant], roles
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def test_agent_meta_job_references_returns_only_job_records
|
|
217
|
+
conversation = chat(valid_receipt)
|
|
218
|
+
references = Chat.agent_meta_job_references(conversation)
|
|
219
|
+
|
|
220
|
+
assert_equal 1, references.length
|
|
221
|
+
reference = references.first
|
|
222
|
+
assert_equal 'Worker/ask/Default_x', reference[:job]
|
|
223
|
+
assert_equal 'Worker/ask/Default_x', reference[:meta][:job]
|
|
224
|
+
assert_equal :agent_meta, reference[:origin]
|
|
225
|
+
assert_equal 'call-1', reference[:call_id]
|
|
226
|
+
assert_equal 'ask', reference[:tool_name]
|
|
227
|
+
assert_equal 1, reference[:agent_meta_index]
|
|
228
|
+
assert_equal [2, :agent_meta, 1], reference[:evidence_address]
|
|
229
|
+
|
|
230
|
+
TmpFile.with_dir do |dir|
|
|
231
|
+
path = File.join(dir, 'parent.chat')
|
|
232
|
+
Open.write(path, valid_receipt)
|
|
233
|
+
with_source = Chat.agent_meta_job_references(Chat.load(path), source: path)
|
|
234
|
+
assert_equal 1, with_source.length
|
|
235
|
+
assert_equal 'Worker/ask/Default_x', with_source.first[:job]
|
|
236
|
+
assert_equal [path, 2, :agent_meta, 1], with_source.first[:evidence_address]
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def test_agent_meta_job_references_empty_without_receipts
|
|
241
|
+
assert_equal [], Chat.agent_meta_job_references(chat(receipt_text))
|
|
242
|
+
assert_equal [], Chat.agent_meta_job_references(chat("user: Hello\nassistant: Hi\n"))
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def test_chat_without_agent_meta_shows_no_regression
|
|
246
|
+
conversation = chat(<<-EOF)
|
|
247
|
+
user: Hello
|
|
248
|
+
meta: pt=10 ct=5 tt=15 inference_id=solo
|
|
249
|
+
assistant: Hi
|
|
250
|
+
EOF
|
|
251
|
+
|
|
252
|
+
assert_equal [], Chat.agent_meta_evidence(conversation)
|
|
253
|
+
|
|
254
|
+
evidence = Chat.meta_evidence(conversation)
|
|
255
|
+
assert_equal 1, evidence.length
|
|
256
|
+
assert_equal :chat_meta, evidence.first[:origin]
|
|
257
|
+
|
|
258
|
+
assert_equal({pt: 10, ct: 5, tt: 15, cct: 0, cwt: 0, rt: 0}, Chat.token_totals([conversation]))
|
|
259
|
+
assert_equal 1, conversation.role_messages(:meta).length
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def test_agent_meta_is_not_limited_to_ask_tool
|
|
263
|
+
agent_meta = [{role: 'meta', content: 'pt=7 tt=7 inference_id=other'}]
|
|
264
|
+
evidence = Chat.agent_meta_evidence(chat(receipt_text(agent_meta: agent_meta, name: 'chat_task', call_id: 'call-9')))
|
|
265
|
+
assert_equal 1, evidence.length
|
|
266
|
+
assert_equal 'chat_task', evidence.first[:tool_name]
|
|
267
|
+
assert_equal 'call-9', evidence.first[:call_id]
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
## Current-format (`meta` key) receipts ##
|
|
271
|
+
|
|
272
|
+
def test_deserialized_meta_receipt_produces_evidence
|
|
273
|
+
evidence = Chat.agent_meta_evidence(chat(valid_meta_receipt))
|
|
274
|
+
assert_equal 2, evidence.length
|
|
275
|
+
|
|
276
|
+
first = evidence.first
|
|
277
|
+
assert_equal :agent_meta, first[:origin]
|
|
278
|
+
assert_equal({'pt' => 100, 'ct' => 50, 'tt' => 150, 'inference_id' => 'aaa'}, first[:meta].to_hash)
|
|
279
|
+
# Already deserialized: no re-parse of any role/content wrapper happened
|
|
280
|
+
assert_nil first[:raw_message]
|
|
281
|
+
# Address suffix mirrors the persisted key
|
|
282
|
+
assert_equal [first[:output_address], :meta, 0], first[:evidence_address]
|
|
283
|
+
assert_equal 0, first[:agent_meta_index]
|
|
284
|
+
|
|
285
|
+
assert_equal 'Worker/ask/Default_x', evidence.last[:meta][:job]
|
|
286
|
+
assert_equal [evidence.last[:output_address], :meta, 1], evidence.last[:evidence_address]
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def test_deserialized_meta_receipt_job_references
|
|
290
|
+
references = Chat.agent_meta_job_references(chat(valid_meta_receipt))
|
|
291
|
+
assert_equal 1, references.length
|
|
292
|
+
assert_equal 'Worker/ask/Default_x', references.first[:job]
|
|
293
|
+
assert_equal :agent_meta, references.first[:origin]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def test_mixed_keys_meta_wins
|
|
297
|
+
text = receipt_text(meta: [{'tt' => 1}],
|
|
298
|
+
agent_meta: [{role: 'meta', content: 'tt=2'}])
|
|
299
|
+
evidence = Chat.agent_meta_evidence(chat(text))
|
|
300
|
+
assert_equal 1, evidence.length
|
|
301
|
+
assert_equal 1, evidence.first[:meta][:tt]
|
|
302
|
+
assert_equal :meta, evidence.first[:evidence_address][1]
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def test_malformed_deserialized_meta_entries_warn_not_raise
|
|
306
|
+
warnings = []
|
|
307
|
+
text = receipt_text(meta: [{'pt' => 5}, 'nonsense', {}, nil])
|
|
308
|
+
evidence = Chat.agent_meta_evidence(chat(text), warnings: warnings)
|
|
309
|
+
|
|
310
|
+
assert_equal 1, evidence.length
|
|
311
|
+
assert_equal 5, evidence.first[:meta][:pt]
|
|
312
|
+
|
|
313
|
+
reasons = warnings.collect { |warning| [warning[:reason], warning[:agent_meta_index]] }
|
|
314
|
+
assert_include reasons, [:not_a_hash, 1]
|
|
315
|
+
assert_include reasons, [:empty_meta, 2]
|
|
316
|
+
assert_include reasons, [:not_a_hash, 3]
|
|
317
|
+
warnings.each do |warning|
|
|
318
|
+
assert_equal :agent_meta, warning[:origin]
|
|
319
|
+
assert_equal 'call-1', warning[:call_id]
|
|
320
|
+
assert_equal :meta, warning[:evidence_address][1] if warning[:evidence_address]
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# End-to-end through the collector: a current-format receipt in a chat FILE
|
|
325
|
+
# feeds token events (origin :agent_meta) and job relations. The job=
|
|
326
|
+
# target is materialized on disk so the reference resolves.
|
|
327
|
+
def test_deserialized_receipt_feeds_token_events_and_problems
|
|
328
|
+
TmpFile.with_dir do |dir|
|
|
329
|
+
# Same layout as AgentMetaFixtures#make_job: the worker job file plus
|
|
330
|
+
# its .info sidecar exist on disk, so the receipt job= reference
|
|
331
|
+
# resolves through Chat.load_job_reference.
|
|
332
|
+
worker = File.join(dir, 'Worker', 'ask', 'Default_w')
|
|
333
|
+
Open.mkdir File.dirname(worker)
|
|
334
|
+
Open.write(worker, 'answer')
|
|
335
|
+
Open.write(worker + '.info', '{}')
|
|
336
|
+
|
|
337
|
+
parent = File.join(dir, 'parent.chat')
|
|
338
|
+
# Absolute job reference, as the fixtures suite writes it
|
|
339
|
+
Open.write(parent, receipt_text(
|
|
340
|
+
meta: [{'pt' => 100, 'ct' => 50, 'tt' => 150, 'inference_id' => 'aaa'},
|
|
341
|
+
{'job' => worker}],
|
|
342
|
+
call_id: 'call-1'))
|
|
343
|
+
|
|
344
|
+
warnings = []
|
|
345
|
+
events = Chat.provenance_token_events(parent, warnings: warnings)
|
|
346
|
+
receipt_events = events.select { |event| event[:evidence].any? { |item| item[:origin] == :agent_meta } }
|
|
347
|
+
assert_equal 1, receipt_events.length
|
|
348
|
+
assert_equal 'aaa', receipt_events.first[:inference_id]
|
|
349
|
+
assert_empty warnings
|
|
350
|
+
|
|
351
|
+
references = Chat.report_agent_meta_problems(Chat.load(parent), parent)
|
|
352
|
+
assert_equal 1, references.length
|
|
353
|
+
# The reference is kept verbatim (absolute, as written into the receipt)
|
|
354
|
+
assert_equal worker, references.first[:job]
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|