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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43cc1358dabaa66c319b8e8c12810ed4c9b9dd344d74fe608c7607d745369618
4
- data.tar.gz: 9388386688c739d59e69779ef950563f10259aa69ea4833dd2a5ef1f29caeda0
3
+ metadata.gz: 14ea97502e51c6baf291165f3e36bbf37b28b93eb119ad750e6157089b0058c6
4
+ data.tar.gz: 6881a3b870cbdb2b7f7bbc2ce3af39b7331f27aefd48e742073a5c1149a8d0bf
5
5
  SHA512:
6
- metadata.gz: 25b3734d86e86107226fea78aa686132c9030ae3cb191388d88e962b9c5b66327524528e91af7ebb27d8df9b8d8057684cf10e030d37992b5d44e6ae5cf885cf
7
- data.tar.gz: 8bdbbdd6cb270a40cf47de6391b36cab005f305a1b2a39628849c9c1747968b8eebea765bb745ecc4d7f5d36afa876510972c28a61a804770a62bd631e2cca55
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module LLM
5
- VERSION = '0.7.4'
5
+ VERSION = '0.7.5'
6
6
  end
7
7
  end
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
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