legion-llm 0.7.8 → 0.7.15
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/CHANGELOG.md +42 -0
- data/lib/legion/llm/pipeline/steps/classification.rb +145 -6
- data/lib/legion/llm/pipeline/steps/rag_guard.rb +6 -1
- data/lib/legion/llm/pipeline/steps/rbac.rb +16 -8
- data/lib/legion/llm/providers.rb +6 -3
- data/lib/legion/llm/routes.rb +22 -4
- data/lib/legion/llm/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e8391f38dbda2f0d629e206bb8844f50768d4200808b85e543f71115d639a65
|
|
4
|
+
data.tar.gz: ae14b4b492a61f8d626ae388ea819851372050df2ee78c7a37fd831e7e4d3fa4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 58253917894e86a8050b7655109a912ebec090cb6eb85607264d5b6a7927d8222bfb5de6e87689e7ce51e0679beb30af7c0e7375a477fca533605564706f6d2e
|
|
7
|
+
data.tar.gz: 3ae513c0ac3ffa347357be2ff50b8ee67ff2170ab416413855ce4f99e3558c17d7627365bf8e6cd4da579dd176d019163533db50ef255ad5f2f8a1504948e453
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.7.15] - 2026-04-20
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- PHI cloud provider gate: `compliance.phi_block_cloud` (default: `false`) blocks restricted-classified requests from cloud providers when enabled. Warns on permit when disabled. Cloud provider list configurable via `compliance.cloud_providers`. Fixes #72
|
|
9
|
+
|
|
10
|
+
## [0.7.14] - 2026-04-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- PII/PHI redaction mode: `compliance.redact_pii` (default: `false`) replaces detected patterns with configurable placeholder token before pipeline continues. Placeholder configurable via `compliance.redaction_placeholder` (default: `[REDACTED]`)
|
|
14
|
+
- `compliance.strict_hipaa` setting (default: `false`): when enabled, scans all 12 HIPAA patterns; when disabled, scans only core 3 (SSN, email, phone) for lighter processing. Closes #73
|
|
15
|
+
|
|
16
|
+
## [0.7.13] - 2026-04-20
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Classification step now always runs the PII/PHI scan, even on unclassified requests — defaults to `:public` baseline. Configurable via `compliance.default_level` (default: `:public`) and `compliance.classification_scan` (default: `true`). Fixes #70
|
|
20
|
+
|
|
21
|
+
## [0.7.12] - 2026-04-20
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- RBAC step now respects `rbac.fail_open` setting (default: `true`) when `Legion::Rbac` is unavailable. Fleet callers are always blocked. Non-fleet callers are permitted with a warning when `fail_open` is true, or blocked with 503 when false. Fixes #69
|
|
25
|
+
|
|
26
|
+
## [0.7.11] - 2026-04-20
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- RAG faithfulness check now logs a warning when RAG context is present but no `Hooks::RagGuard` is registered, instead of silently skipping. Fixes #71
|
|
30
|
+
- RAG faithfulness failure now logs at warn level in addition to appending to pipeline warnings array
|
|
31
|
+
|
|
32
|
+
## [0.7.10] - 2026-04-20
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- `configure_anthropic` now passes `base_url` through to RubyLLM (`anthropic_api_base`) when present, enabling custom API gateways and proxies. Fixes #68
|
|
36
|
+
- `configure_openai` now passes `base_url` through to RubyLLM (`openai_api_base`) when present, for consistency with Anthropic and Ollama providers
|
|
37
|
+
- `configure_gemini` now passes `base_url` through to RubyLLM (`gemini_api_base`) when present, for consistency with Anthropic and Ollama providers
|
|
38
|
+
|
|
39
|
+
## [0.7.9] - 2026-04-18
|
|
40
|
+
### Added
|
|
41
|
+
- Expanded PII/PHI classification to cover 12 HIPAA Safe Harbor identifier patterns (was 3) and 20 PHI keywords (was 11). Partial fix for #73
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- `web_fetch` client tool now delegates to `Legion::CLI::Chat::WebFetch.fetch` instead of bare `Net::HTTP.get` — gains SSL, redirect following, HTML-to-markdown conversion, and `maxLength` truncation (LegionIO/LegionIO#153)
|
|
45
|
+
- Added `web_search` client tool dispatch via `Legion::CLI::Chat::WebSearch.search` — previously fell through to generic "not executable server-side" error (LegionIO/LegionIO#154)
|
|
46
|
+
|
|
5
47
|
## [0.7.8] - 2026-04-17
|
|
6
48
|
### Fixed
|
|
7
49
|
- Guard `Embeddings.generate` against providers that don't support embeddings before calling `RubyLLM.embed` — prevents noisy `NoMethodError: undefined method 'render_embedding_payload'` warns when Bedrock (or Anthropic) is the active provider
|
|
@@ -11,27 +11,47 @@ module Legion
|
|
|
11
11
|
|
|
12
12
|
LEVELS = %i[public internal confidential restricted].freeze
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
PII_PATTERNS_CORE = {
|
|
15
15
|
ssn: /\b\d{3}-\d{2}-\d{4}\b/,
|
|
16
16
|
email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/,
|
|
17
17
|
phone: /\b(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}\b/
|
|
18
18
|
}.freeze
|
|
19
19
|
|
|
20
|
+
PII_PATTERNS_EXTENDED = {
|
|
21
|
+
ip_address: /\b(?:\d{1,3}\.){3}\d{1,3}\b/,
|
|
22
|
+
date_of_birth: %r{\b(?:0[1-9]|1[0-2])[/-](?:0[1-9]|[12]\d|3[01])[/-](?:19|20)\d{2}\b},
|
|
23
|
+
zip_code: /\b\d{5}(?:-\d{4})?\b/,
|
|
24
|
+
mrn: /\b(?:MRN|mrn)[:\s#]?\s?\d{4,12}\b/,
|
|
25
|
+
account_number: /\b(?:account|acct)[:\s#]?\s?\d{6,20}\b/i,
|
|
26
|
+
license_number: /\b[A-Z]\d{3,8}\b/,
|
|
27
|
+
url: %r{\bhttps?://[^\s<>"']+}i,
|
|
28
|
+
vin: /\b[A-HJ-NPR-Z0-9]{17}\b/,
|
|
29
|
+
npi_number: /\b\d{10}\b/
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
PII_PATTERNS = PII_PATTERNS_CORE.merge(PII_PATTERNS_EXTENDED).freeze
|
|
33
|
+
|
|
20
34
|
PHI_KEYWORDS = %w[
|
|
21
35
|
patient diagnosis medication prescription
|
|
22
36
|
dob date-of-birth mrn medical-record
|
|
23
|
-
npi clinical treatment
|
|
37
|
+
npi clinical treatment health-plan
|
|
38
|
+
beneficiary insurance-id group-number
|
|
39
|
+
lab-result radiology pathology
|
|
40
|
+
admission discharge procedure
|
|
24
41
|
].freeze
|
|
25
42
|
|
|
26
43
|
def step_classification
|
|
27
|
-
|
|
44
|
+
classification = @request.classification || compliance_classification_default || default_classification
|
|
45
|
+
return unless classification_enabled?(classification)
|
|
28
46
|
|
|
29
|
-
classification = @request.classification || compliance_classification_default
|
|
30
47
|
declared_level = classification[:level]
|
|
31
48
|
scan = scan_content_for_sensitive_data
|
|
32
49
|
effective_level = upgrade_if_needed(declared_level, scan)
|
|
33
50
|
upgraded = effective_level != declared_level
|
|
34
51
|
|
|
52
|
+
redact_sensitive_content(scan)
|
|
53
|
+
enforce_phi_cloud_gate(effective_level)
|
|
54
|
+
|
|
35
55
|
@enrichments['classification:scan'] = {
|
|
36
56
|
declared_level: declared_level,
|
|
37
57
|
effective_level: effective_level,
|
|
@@ -67,8 +87,9 @@ module Legion
|
|
|
67
87
|
def scan_content_for_sensitive_data
|
|
68
88
|
text = extract_text_content
|
|
69
89
|
patterns = []
|
|
90
|
+
active_patterns = strict_hipaa_mode? ? PII_PATTERNS : PII_PATTERNS_CORE
|
|
70
91
|
|
|
71
|
-
|
|
92
|
+
active_patterns.each do |name, regex|
|
|
72
93
|
patterns << name if text.match?(regex)
|
|
73
94
|
end
|
|
74
95
|
|
|
@@ -76,12 +97,41 @@ module Legion
|
|
|
76
97
|
patterns << :phi_keyword if phi_found
|
|
77
98
|
|
|
78
99
|
{
|
|
79
|
-
contains_pii: patterns.intersect?(
|
|
100
|
+
contains_pii: patterns.intersect?(active_patterns.keys),
|
|
80
101
|
contains_phi: phi_found,
|
|
81
102
|
patterns: patterns
|
|
82
103
|
}
|
|
83
104
|
end
|
|
84
105
|
|
|
106
|
+
def redact_sensitive_content(scan)
|
|
107
|
+
return unless redaction_enabled?
|
|
108
|
+
return unless scan[:contains_pii] || scan[:contains_phi]
|
|
109
|
+
|
|
110
|
+
placeholder = redaction_placeholder
|
|
111
|
+
active_patterns = strict_hipaa_mode? ? PII_PATTERNS : PII_PATTERNS_CORE
|
|
112
|
+
|
|
113
|
+
@request.messages.each do |message|
|
|
114
|
+
next unless message[:content].is_a?(String)
|
|
115
|
+
|
|
116
|
+
active_patterns.each_value do |regex|
|
|
117
|
+
message[:content] = message[:content].gsub(regex, placeholder)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
next unless scan[:contains_phi]
|
|
121
|
+
|
|
122
|
+
PHI_KEYWORDS.each do |kw|
|
|
123
|
+
message[:content] = message[:content].gsub(/\b#{Regexp.escape(kw)}\b/i, placeholder)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
@enrichments['classification:redaction'] = {
|
|
128
|
+
redacted: true,
|
|
129
|
+
patterns_redacted: scan[:patterns],
|
|
130
|
+
placeholder: placeholder,
|
|
131
|
+
timestamp: Time.now
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
85
135
|
def extract_text_content
|
|
86
136
|
@request.messages.map { |m| m[:content].to_s }.join(' ')
|
|
87
137
|
end
|
|
@@ -97,6 +147,28 @@ module Legion
|
|
|
97
147
|
LEVELS[threshold_idx]
|
|
98
148
|
end
|
|
99
149
|
|
|
150
|
+
def default_classification
|
|
151
|
+
{ level: default_classification_level }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def default_classification_level
|
|
155
|
+
return :public unless defined?(Legion::Settings)
|
|
156
|
+
|
|
157
|
+
level = Legion::Settings.dig(:compliance, :default_level)
|
|
158
|
+
level ? level.to_sym : :public
|
|
159
|
+
rescue StandardError
|
|
160
|
+
:public
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def classification_enabled?(_classification)
|
|
164
|
+
return true unless defined?(Legion::Settings)
|
|
165
|
+
|
|
166
|
+
enabled = Legion::Settings.dig(:compliance, :classification_scan)
|
|
167
|
+
enabled.nil? || enabled
|
|
168
|
+
rescue StandardError
|
|
169
|
+
true
|
|
170
|
+
end
|
|
171
|
+
|
|
100
172
|
def compliance_classification_default
|
|
101
173
|
return nil unless defined?(Legion::Settings)
|
|
102
174
|
|
|
@@ -108,6 +180,73 @@ module Legion
|
|
|
108
180
|
handle_exception(e, level: :warn, operation: 'llm.pipeline.steps.classification.default')
|
|
109
181
|
nil
|
|
110
182
|
end
|
|
183
|
+
|
|
184
|
+
def redaction_enabled?
|
|
185
|
+
setting = Legion::Settings.dig(:compliance, :redact_pii)
|
|
186
|
+
setting == true
|
|
187
|
+
rescue StandardError
|
|
188
|
+
false
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def strict_hipaa_mode?
|
|
192
|
+
setting = Legion::Settings.dig(:compliance, :strict_hipaa)
|
|
193
|
+
setting == true
|
|
194
|
+
rescue StandardError
|
|
195
|
+
false
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def redaction_placeholder
|
|
199
|
+
Legion::Settings.dig(:compliance, :redaction_placeholder) || '[REDACTED]'
|
|
200
|
+
rescue StandardError
|
|
201
|
+
'[REDACTED]'
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def enforce_phi_cloud_gate(effective_level)
|
|
205
|
+
return unless effective_level == :restricted
|
|
206
|
+
|
|
207
|
+
provider = resolve_current_provider
|
|
208
|
+
return unless cloud_provider?(provider)
|
|
209
|
+
|
|
210
|
+
if phi_block_cloud?
|
|
211
|
+
raise Legion::LLM::PipelineError.new(
|
|
212
|
+
"Restricted/sensitive content (level=restricted) cannot be sent to cloud provider #{provider}. " \
|
|
213
|
+
'Set compliance.phi_block_cloud=false to override, or use a local provider.',
|
|
214
|
+
step: :classification
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
log.warn(
|
|
219
|
+
"[classification] Restricted/sensitive content (level=restricted) routing to cloud provider #{provider} — " \
|
|
220
|
+
'compliance.phi_block_cloud is disabled, permitting'
|
|
221
|
+
)
|
|
222
|
+
@warnings << "Restricted/sensitive content routing to cloud provider #{provider} (phi_block_cloud disabled)"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def phi_block_cloud?
|
|
226
|
+
setting = Legion::Settings.dig(:compliance, :phi_block_cloud)
|
|
227
|
+
setting == true
|
|
228
|
+
rescue StandardError
|
|
229
|
+
false
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def cloud_provider?(provider)
|
|
233
|
+
return false unless provider
|
|
234
|
+
|
|
235
|
+
cloud_providers = Legion::Settings.dig(:compliance, :cloud_providers) ||
|
|
236
|
+
%i[anthropic openai gemini bedrock azure]
|
|
237
|
+
cloud_providers.map(&:to_sym).include?(provider.to_sym)
|
|
238
|
+
rescue StandardError
|
|
239
|
+
false
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def resolve_current_provider
|
|
243
|
+
routing = @request.respond_to?(:routing) ? @request.routing : nil
|
|
244
|
+
provider = routing[:provider] if routing.is_a?(Hash)
|
|
245
|
+
provider ||= Legion::Settings.dig(:llm, :default_provider)
|
|
246
|
+
provider&.to_sym
|
|
247
|
+
rescue StandardError
|
|
248
|
+
nil
|
|
249
|
+
end
|
|
111
250
|
end
|
|
112
251
|
end
|
|
113
252
|
end
|
|
@@ -12,7 +12,11 @@ module Legion
|
|
|
12
12
|
def check_rag_faithfulness
|
|
13
13
|
context = @enrichments.dig('rag:context_retrieval', :data, :entries)
|
|
14
14
|
return unless context&.any?
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
unless defined?(Hooks::RagGuard)
|
|
17
|
+
log.warn('[rag_guard] RAG context present but no Hooks::RagGuard registered — faithfulness check skipped')
|
|
18
|
+
return
|
|
19
|
+
end
|
|
16
20
|
|
|
17
21
|
response_text = @raw_response.respond_to?(:content) ? @raw_response.content : @raw_response.to_s
|
|
18
22
|
|
|
@@ -25,6 +29,7 @@ module Legion
|
|
|
25
29
|
return if result.nil? || result[:faithful]
|
|
26
30
|
|
|
27
31
|
detail = result[:details] || result[:reason] || 'faithfulness check failed'
|
|
32
|
+
log.warn("[rag_guard] RAG faithfulness warning: #{detail}")
|
|
28
33
|
@warnings << "RAG faithfulness warning: #{detail}"
|
|
29
34
|
@timeline.record(
|
|
30
35
|
category: :quality, key: 'rag:faithfulness_warning',
|
|
@@ -13,18 +13,21 @@ module Legion
|
|
|
13
13
|
start_time = Time.now
|
|
14
14
|
|
|
15
15
|
unless defined?(::Legion::Rbac)
|
|
16
|
-
if fleet_caller?
|
|
17
|
-
msg = 'RBAC unavailable
|
|
18
|
-
|
|
16
|
+
if fleet_caller? || !fail_open_permitted?
|
|
17
|
+
msg = '503: RBAC unavailable — request denied ' \
|
|
18
|
+
"(fleet=#{fleet_caller?}, fail_open=#{fail_open_permitted?})"
|
|
19
|
+
log.error("[llm][rbac] blocked request_id=#{@request.id} reason=rbac_unavailable " \
|
|
20
|
+
"fleet=#{fleet_caller?} fail_open=#{fail_open_permitted?}")
|
|
19
21
|
record_rbac_audit(:failure, msg, start_time)
|
|
20
22
|
record_rbac_timeline("denied: #{msg}")
|
|
21
|
-
raise Legion::LLM::PipelineError.new(
|
|
23
|
+
raise Legion::LLM::PipelineError.new(msg, step: :rbac)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
log.warn('[llm][rbac] RBAC unavailable, permitting request (fail_open enabled) ' \
|
|
27
|
+
"request_id=#{@request.id}")
|
|
28
|
+
@warnings << 'RBAC unavailable, permitting request (fail_open enabled)'
|
|
29
|
+
record_rbac_audit(:success, 'permitted (rbac unavailable, fail_open enabled)', start_time)
|
|
30
|
+
record_rbac_timeline('permitted (rbac unavailable, fail_open enabled)')
|
|
28
31
|
return
|
|
29
32
|
end
|
|
30
33
|
|
|
@@ -54,6 +57,11 @@ module Legion
|
|
|
54
57
|
|
|
55
58
|
private
|
|
56
59
|
|
|
60
|
+
def fail_open_permitted?
|
|
61
|
+
setting = Legion::Settings.dig(:rbac, :fail_open)
|
|
62
|
+
setting.nil? || setting
|
|
63
|
+
end
|
|
64
|
+
|
|
57
65
|
def build_rbac_principal
|
|
58
66
|
rb = @request.caller&.fetch(:requested_by, {}) || {}
|
|
59
67
|
::Legion::Rbac::Principal.new(
|
data/lib/legion/llm/providers.rb
CHANGED
|
@@ -111,8 +111,9 @@ module Legion
|
|
|
111
111
|
|
|
112
112
|
RubyLLM.configure do |c|
|
|
113
113
|
c.anthropic_api_key = api_key
|
|
114
|
+
c.anthropic_api_base = config[:base_url] if config[:base_url]
|
|
114
115
|
end
|
|
115
|
-
log.info
|
|
116
|
+
log.info "Configured Anthropic provider#{" (#{config[:base_url]})" if config[:base_url]}"
|
|
116
117
|
end
|
|
117
118
|
|
|
118
119
|
def configure_openai(config)
|
|
@@ -121,8 +122,9 @@ module Legion
|
|
|
121
122
|
|
|
122
123
|
RubyLLM.configure do |c|
|
|
123
124
|
c.openai_api_key = api_key
|
|
125
|
+
c.openai_api_base = config[:base_url] if config[:base_url]
|
|
124
126
|
end
|
|
125
|
-
log.info
|
|
127
|
+
log.info "Configured OpenAI provider#{" (#{config[:base_url]})" if config[:base_url]}"
|
|
126
128
|
end
|
|
127
129
|
|
|
128
130
|
def configure_gemini(config)
|
|
@@ -131,8 +133,9 @@ module Legion
|
|
|
131
133
|
|
|
132
134
|
RubyLLM.configure do |c|
|
|
133
135
|
c.gemini_api_key = api_key
|
|
136
|
+
c.gemini_api_base = config[:base_url] if config[:base_url]
|
|
134
137
|
end
|
|
135
|
-
log.info
|
|
138
|
+
log.info "Configured Gemini provider#{" (#{config[:base_url]})" if config[:base_url]}"
|
|
136
139
|
end
|
|
137
140
|
|
|
138
141
|
def configure_azure(config)
|
data/lib/legion/llm/routes.rb
CHANGED
|
@@ -46,7 +46,7 @@ module Legion
|
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
def dispatch_client_tool(ref, **kwargs)
|
|
49
|
+
def dispatch_client_tool(ref, **kwargs) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
|
|
50
50
|
case ref
|
|
51
51
|
when 'sh'
|
|
52
52
|
cmd = kwargs[:command] || kwargs[:cmd] || kwargs.values.first.to_s
|
|
@@ -81,9 +81,27 @@ module Legion
|
|
|
81
81
|
Dir.glob(pattern).first(100).join("\n")
|
|
82
82
|
when 'web_fetch'
|
|
83
83
|
url = kwargs[:url] || kwargs.values.first.to_s
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
raw_max_length = kwargs[:maxLength] || kwargs[:max_length]
|
|
85
|
+
max_length = raw_max_length.nil? ? nil : [raw_max_length.to_i, 0].max
|
|
86
|
+
begin
|
|
87
|
+
require 'legion/cli/chat/web_fetch'
|
|
88
|
+
content = Legion::CLI::Chat::WebFetch.fetch(url)
|
|
89
|
+
max_length ? content[0, max_length] : content
|
|
90
|
+
rescue LoadError => e
|
|
91
|
+
missing = e.respond_to?(:path) && e.path ? e.path : 'legion/cli/chat/web_fetch'
|
|
92
|
+
"web_fetch is unavailable: missing optional dependency #{missing}"
|
|
93
|
+
end
|
|
94
|
+
when 'web_search'
|
|
95
|
+
query = kwargs[:query] || kwargs.values.first.to_s
|
|
96
|
+
max_results = (kwargs[:max_results] || kwargs[:maxResults] || 5).to_i
|
|
97
|
+
begin
|
|
98
|
+
require 'legion/cli/chat/web_search'
|
|
99
|
+
results = Legion::CLI::Chat::WebSearch.search(query, max_results: max_results, auto_fetch: false)
|
|
100
|
+
results[:results].map { |r| "### #{r[:title]}\n#{r[:url]}\n#{r[:snippet]}" }.join("\n\n")
|
|
101
|
+
rescue LoadError => e
|
|
102
|
+
missing = e.respond_to?(:path) && e.path ? e.path : 'legion/cli/chat/web_search'
|
|
103
|
+
"web_search is unavailable: missing optional dependency #{missing}"
|
|
104
|
+
end
|
|
87
105
|
else
|
|
88
106
|
"Tool #{ref} is not executable server-side. Use a legion_ prefixed tool instead."
|
|
89
107
|
end
|
data/lib/legion/llm/version.rb
CHANGED