legion-llm 0.7.4 → 0.7.5
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 +17 -0
- data/lib/legion/llm/prompt.rb +220 -0
- data/lib/legion/llm/version.rb +1 -1
- data/lib/legion/llm.rb +12 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 14ea97502e51c6baf291165f3e36bbf37b28b93eb119ad750e6157089b0058c6
|
|
4
|
+
data.tar.gz: 6881a3b870cbdb2b7f7bbc2ce3af39b7331f27aefd48e742073a5c1149a8d0bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 83d59e98b5fe417f762bdf7aec41c8bdbe10246db47d9f9dcf620a9c5b7bc97768a0f05a13e5865a30400fb37c018ee019047fd68ddf717d760b213dd36c58e2
|
|
7
|
+
data.tar.gz: 43897c5332b7c641812f3e75981b1712a5d5262f607ce55bf8cd49f27c04a6a62f4bddd3d5f6abd8c0adc634a41b60e9b894383a211955ae1197d09d9c16bfb3
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.7.5] - 2026-04-14
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `Legion::LLM::Prompt` module — clean API replacing `chat`/`ask`/`chat_direct` surface
|
|
9
|
+
- `Prompt.dispatch(message, intent:, exclude:, tier:, tools:, ...)` — auto-routed via Router
|
|
10
|
+
- `Prompt.request(message, provider:, model:, ...)` — pinned dispatch, full pipeline
|
|
11
|
+
- `Prompt.summarize`, `Prompt.extract`, `Prompt.decide` — convenience methods (default `tools: []`)
|
|
12
|
+
- Nil provider/model guard raises `LLMError` with actionable message
|
|
13
|
+
- In-process pipeline execution (no DaemonClient HTTP roundtrip)
|
|
14
|
+
- Backward compat: `Legion::LLM.chat` delegates to `Prompt.dispatch` for non-streaming calls
|
|
15
|
+
- `build_pipeline_request` uses `Pipeline::Request.from_chat_args` as base, preserving all pipeline kwargs
|
|
16
|
+
|
|
17
|
+
## [0.7.4] - 2026-04-14
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- PHI/PII classification hard gate: `TierAssigner` now routes `contains_phi`/`contains_pii`/`:restricted` to `tier: :local` (fail closed). Previously routed to `:cloud`.
|
|
21
|
+
|
|
5
22
|
## [0.7.3] - 2026-04-13
|
|
6
23
|
|
|
7
24
|
### Added
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module LLM
|
|
5
|
+
module Prompt
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Auto-routed: Router picks the best provider+model based on intent.
|
|
9
|
+
# Primary entry point for most LLM calls.
|
|
10
|
+
# When provider/model are passed explicitly, they take precedence over routing.
|
|
11
|
+
def dispatch(message, # rubocop:disable Metrics/ParameterLists
|
|
12
|
+
intent: nil,
|
|
13
|
+
exclude: {}, # rubocop:disable Lint/UnusedMethodArgument -- forwarded to Router.resolve in WS-00E
|
|
14
|
+
tier: nil,
|
|
15
|
+
provider: nil,
|
|
16
|
+
model: nil,
|
|
17
|
+
schema: nil,
|
|
18
|
+
tools: nil,
|
|
19
|
+
escalate: nil,
|
|
20
|
+
max_escalations: 3,
|
|
21
|
+
thinking: nil,
|
|
22
|
+
temperature: nil,
|
|
23
|
+
max_tokens: nil,
|
|
24
|
+
tracing: nil,
|
|
25
|
+
agent: nil,
|
|
26
|
+
caller: nil,
|
|
27
|
+
cache: nil,
|
|
28
|
+
quality_check: nil,
|
|
29
|
+
**)
|
|
30
|
+
resolved_provider = provider
|
|
31
|
+
resolved_model = model
|
|
32
|
+
|
|
33
|
+
if resolved_provider.nil? && resolved_model.nil? && defined?(Router) && Router.routing_enabled?
|
|
34
|
+
resolution = Router.resolve(intent: intent, tier: tier)
|
|
35
|
+
resolved_provider = resolution&.provider
|
|
36
|
+
resolved_model = resolution&.model
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
resolved_provider ||= Legion::LLM.settings[:default_provider]
|
|
40
|
+
resolved_model ||= Legion::LLM.settings[:default_model]
|
|
41
|
+
|
|
42
|
+
request(message,
|
|
43
|
+
provider: resolved_provider,
|
|
44
|
+
model: resolved_model,
|
|
45
|
+
intent: intent,
|
|
46
|
+
tier: tier,
|
|
47
|
+
schema: schema,
|
|
48
|
+
tools: tools,
|
|
49
|
+
escalate: escalate,
|
|
50
|
+
max_escalations: max_escalations,
|
|
51
|
+
thinking: thinking,
|
|
52
|
+
temperature: temperature,
|
|
53
|
+
max_tokens: max_tokens,
|
|
54
|
+
tracing: tracing,
|
|
55
|
+
agent: agent,
|
|
56
|
+
caller: caller,
|
|
57
|
+
cache: cache,
|
|
58
|
+
quality_check: quality_check,
|
|
59
|
+
**)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Pinned: caller specifies exact provider+model. Full pipeline runs in-process.
|
|
63
|
+
def request(message, # rubocop:disable Metrics/ParameterLists
|
|
64
|
+
provider:,
|
|
65
|
+
model:,
|
|
66
|
+
intent: nil,
|
|
67
|
+
tier: nil,
|
|
68
|
+
schema: nil,
|
|
69
|
+
tools: nil,
|
|
70
|
+
escalate: nil,
|
|
71
|
+
max_escalations: 3,
|
|
72
|
+
thinking: nil,
|
|
73
|
+
temperature: nil,
|
|
74
|
+
max_tokens: nil,
|
|
75
|
+
tracing: nil,
|
|
76
|
+
agent: nil,
|
|
77
|
+
caller: nil,
|
|
78
|
+
cache: nil,
|
|
79
|
+
quality_check: nil,
|
|
80
|
+
**)
|
|
81
|
+
if provider.nil? || model.nil?
|
|
82
|
+
raise LLMError, "Prompt.request: provider and model must be set (got provider=#{provider.inspect}, model=#{model.inspect}). " \
|
|
83
|
+
'Configure Legion::Settings[:llm][:default_provider] and [:default_model], or pass them explicitly.'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
pipeline_request = build_pipeline_request(
|
|
87
|
+
message, provider: provider, model: model, intent: intent, tier: tier,
|
|
88
|
+
schema: schema, tools: tools,
|
|
89
|
+
escalate: escalate, max_escalations: max_escalations,
|
|
90
|
+
thinking: thinking, temperature: temperature, max_tokens: max_tokens,
|
|
91
|
+
tracing: tracing, agent: agent, caller: caller, cache: cache,
|
|
92
|
+
quality_check: quality_check, **
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
executor = Pipeline::Executor.new(pipeline_request)
|
|
96
|
+
executor.call
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Condense a conversation or feedback history into a shorter form.
|
|
100
|
+
def summarize(messages, tools: [], **)
|
|
101
|
+
prompt = build_summarize_prompt(messages)
|
|
102
|
+
dispatch(prompt, tools: tools, **)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Extract structured data from unstructured text.
|
|
106
|
+
def extract(text, schema:, tools: [], **)
|
|
107
|
+
prompt = build_extract_prompt(text)
|
|
108
|
+
dispatch(prompt, schema: schema, tools: tools, **)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Pick from a set of options with reasoning.
|
|
112
|
+
def decide(question, options:, tools: [], **)
|
|
113
|
+
prompt = build_decide_prompt(question, options)
|
|
114
|
+
dispatch(prompt, tools: tools, **)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# --- Private helpers ---
|
|
118
|
+
|
|
119
|
+
def build_pipeline_request(message, provider:, model:, intent:, tier:, schema:, tools:, # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
|
120
|
+
escalate:, max_escalations:, thinking:, temperature:,
|
|
121
|
+
max_tokens:, tracing:, agent:, caller:, cache:,
|
|
122
|
+
quality_check:, **rest)
|
|
123
|
+
# Build base request via from_chat_args to preserve full pipeline kwargs
|
|
124
|
+
# (context_strategy, tool_choice, idempotency_key, ttl, enrichments, predictions, etc.)
|
|
125
|
+
chat_message = message.is_a?(Array) ? nil : message.to_s
|
|
126
|
+
chat_messages = message.is_a?(Array) ? message : nil
|
|
127
|
+
|
|
128
|
+
base = Pipeline::Request.from_chat_args(
|
|
129
|
+
message: chat_message,
|
|
130
|
+
messages: chat_messages,
|
|
131
|
+
model: model,
|
|
132
|
+
provider: provider,
|
|
133
|
+
intent: intent,
|
|
134
|
+
tier: tier,
|
|
135
|
+
tools: tools,
|
|
136
|
+
thinking: thinking,
|
|
137
|
+
tracing: tracing,
|
|
138
|
+
agent: agent,
|
|
139
|
+
caller: caller,
|
|
140
|
+
cache: cache,
|
|
141
|
+
escalate: escalate,
|
|
142
|
+
max_escalations: max_escalations,
|
|
143
|
+
quality_check: quality_check,
|
|
144
|
+
**rest
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Overlay Prompt-specific translations on top of the base request
|
|
148
|
+
generation = (base.generation || {}).dup
|
|
149
|
+
generation[:temperature] = temperature if temperature
|
|
150
|
+
|
|
151
|
+
tokens = (base.tokens || {}).dup
|
|
152
|
+
tokens[:max] = max_tokens if max_tokens
|
|
153
|
+
|
|
154
|
+
response_format = if schema
|
|
155
|
+
{ type: :json_schema, schema: schema }
|
|
156
|
+
elsif base.response_format
|
|
157
|
+
base.response_format
|
|
158
|
+
else
|
|
159
|
+
{ type: :text }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
Pipeline::Request.build(
|
|
163
|
+
messages: base.messages,
|
|
164
|
+
system: base.system,
|
|
165
|
+
routing: base.routing || { provider: provider, model: model },
|
|
166
|
+
tools: base.tools || tools || [],
|
|
167
|
+
tool_choice: base.tool_choice,
|
|
168
|
+
thinking: base.thinking || thinking,
|
|
169
|
+
generation: generation,
|
|
170
|
+
tokens: tokens,
|
|
171
|
+
stop: base.stop,
|
|
172
|
+
response_format: response_format,
|
|
173
|
+
stream: base.stream || false,
|
|
174
|
+
fork: base.fork,
|
|
175
|
+
cache: base.cache || cache || { strategy: :default, cacheable: true },
|
|
176
|
+
priority: base.priority || :normal,
|
|
177
|
+
tracing: base.tracing || tracing,
|
|
178
|
+
classification: base.classification,
|
|
179
|
+
caller: base.caller || caller,
|
|
180
|
+
agent: base.agent || agent,
|
|
181
|
+
billing: base.billing,
|
|
182
|
+
test: base.test,
|
|
183
|
+
modality: base.modality,
|
|
184
|
+
hooks: base.hooks,
|
|
185
|
+
conversation_id: base.conversation_id,
|
|
186
|
+
idempotency_key: base.idempotency_key,
|
|
187
|
+
schema_version: base.schema_version,
|
|
188
|
+
id: base.id,
|
|
189
|
+
ttl: base.ttl,
|
|
190
|
+
metadata: base.metadata || {},
|
|
191
|
+
enrichments: base.enrichments || {},
|
|
192
|
+
predictions: base.predictions || {},
|
|
193
|
+
context_strategy: base.context_strategy,
|
|
194
|
+
extra: base.extra || {}
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def build_summarize_prompt(messages)
|
|
199
|
+
text = if messages.is_a?(Array)
|
|
200
|
+
messages.map { |m| m.is_a?(Hash) ? m[:content] : m.to_s }.join("\n")
|
|
201
|
+
else
|
|
202
|
+
messages.to_s
|
|
203
|
+
end
|
|
204
|
+
"Summarize the following content concisely, preserving key points:\n\n#{text}"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def build_extract_prompt(text)
|
|
208
|
+
"Extract structured data from the following text. Return only the JSON matching the provided schema.\n\n#{text}"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def build_decide_prompt(question, options)
|
|
212
|
+
options_text = options.each_with_index.map { |opt, i| "#{i + 1}. #{opt}" }.join("\n")
|
|
213
|
+
"#{question}\n\nOptions:\n#{options_text}\n\nPick the best option and explain your reasoning."
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
private_class_method :build_pipeline_request,
|
|
217
|
+
:build_summarize_prompt, :build_extract_prompt, :build_decide_prompt
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
data/lib/legion/llm/version.rb
CHANGED
data/lib/legion/llm.rb
CHANGED
|
@@ -37,6 +37,7 @@ require_relative 'llm/cost_tracker'
|
|
|
37
37
|
require_relative 'llm/token_tracker'
|
|
38
38
|
require_relative 'llm/override_confidence'
|
|
39
39
|
require_relative 'llm/routes'
|
|
40
|
+
require_relative 'llm/prompt'
|
|
40
41
|
|
|
41
42
|
begin
|
|
42
43
|
require_relative 'llm/skills'
|
|
@@ -543,7 +544,17 @@ module Legion
|
|
|
543
544
|
"tier=#{tier} escalate=#{escalate} max_escalations=#{max_escalations} " \
|
|
544
545
|
"quality_check=#{quality_check} message_present=#{!message.nil?} kwargs=#{kwargs.keys.sort}"
|
|
545
546
|
)
|
|
546
|
-
if pipeline_enabled? && (message || kwargs[:messages])
|
|
547
|
+
if pipeline_enabled? && (message || kwargs[:messages]) && !block_given?
|
|
548
|
+
return Prompt.dispatch(
|
|
549
|
+
message || kwargs[:messages],
|
|
550
|
+
intent: intent, tier: tier, provider: provider, model: model,
|
|
551
|
+
escalate: escalate, max_escalations: max_escalations,
|
|
552
|
+
quality_check: quality_check, **kwargs.except(:messages)
|
|
553
|
+
)
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# Streaming with pipeline — old path (Prompt does not handle streaming yet)
|
|
557
|
+
if pipeline_enabled? && (message || kwargs[:messages]) && block_given?
|
|
547
558
|
return chat_via_pipeline(model: model, provider: provider, intent: intent, tier: tier,
|
|
548
559
|
message: message, escalate: escalate, max_escalations: max_escalations,
|
|
549
560
|
quality_check: quality_check, **kwargs, &)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legion-llm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -307,6 +307,7 @@ files:
|
|
|
307
307
|
- lib/legion/llm/pipeline/tool_adapter.rb
|
|
308
308
|
- lib/legion/llm/pipeline/tool_dispatcher.rb
|
|
309
309
|
- lib/legion/llm/pipeline/tracing.rb
|
|
310
|
+
- lib/legion/llm/prompt.rb
|
|
310
311
|
- lib/legion/llm/provider_registry.rb
|
|
311
312
|
- lib/legion/llm/providers.rb
|
|
312
313
|
- lib/legion/llm/quality_checker.rb
|