legionio 1.4.93 → 1.4.95

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: 318839858d16ecbaaf6c7db4a0d9562e1641ca24d174fda914b9d359f3f91e69
4
- data.tar.gz: 91e1415070e2f547bc7c71646e79312f048f8973b2cb004045a8d546a0000387
3
+ metadata.gz: a110098f876265aa1233e4ca1e60baf30f5a23340b8d9de1f4a9c942ea0128fd
4
+ data.tar.gz: 3144aab6b5cab4ed042fee55d344cd2a8493ac48284a74ef431238643d91a863
5
5
  SHA512:
6
- metadata.gz: e5cd6e2ff71cc11db2a1e8928aae0c53104f931c18829bf470658bbcac3e2f90a18aeb7c564dcc7eb3f113fa9c65b54bb165a0f322fc3b1021a0ebf98148bf66
7
- data.tar.gz: ff3c9e796fef50d7ca2e7b1970c4516a2d839031f8bee6036fc43b758f0035d85b5bc80fd631c5e55d8a83ce28fe5a7ccfa55fa69b67237b868331d1606d3d34
6
+ metadata.gz: 8a5a7090df59bcf09e9bc2dc5cd2c60a7e774b7551b2cc7583d2eb1afdc5f6b3319ec911a8cce0130c0dc93679260ac83cf57e5c598b17c6d6ccb4ba229058ac
7
+ data.tar.gz: f6a2135e2ce480e7bce8f80eec8ab6e467c9e531ea10960470f2d2e7c3fb7325b84d3b9f256247255cd5c00220d21f53da8520c5b3e3045a6f446ce75f1600d8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.4.95] - 2026-03-20
4
+
5
+ ### Added
6
+ - `GET /api/prompts` — list all prompt templates via lex-prompt Client
7
+ - `GET /api/prompts/:name` — show prompt details for the latest version
8
+ - `POST /api/prompts/:name/run` — render a prompt template with variables and run it through Legion::LLM; returns rendered_prompt, response, usage, model, version
9
+ - 503 guard for missing lex-prompt dependency (LoadError rescue in `prompt_client` helper)
10
+ - 503 guard for LLM subsystem unavailable on the `/run` endpoint
11
+ - 404 on prompt not found, 422 on version not found for `/run`
12
+ - 32 new specs in `spec/legion/api/prompts_spec.rb` covering all routes and error paths
13
+
14
+ ## [1.4.94] - 2026-03-20
15
+
16
+ ### Added
17
+ - `legion prompt play NAME` subcommand: renders a prompt template with variables and sends it to an LLM via `Legion::LLM.chat`
18
+ - `--variables` (JSON), `--version`, `--model`, `--provider`, and `--compare` options on `play`
19
+ - Compare mode (`--compare VERSION`): renders two prompt versions, calls LLM for each, displays side-by-side responses and diff when they differ
20
+ - JSON output mode for `play` and compare via `--json`
21
+ - `Connection.ensure_llm` called inside `with_prompt_client` so LLM is available to all prompt subcommands
22
+ - 14 new specs for `play` covering single-version, compare, LLM unavailable, JSON output, and error paths
23
+
3
24
  ## [1.4.93] - 2026-03-20
4
25
 
5
26
  ### Added
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ class API < Sinatra::Base
5
+ module Routes
6
+ module Prompts
7
+ def self.registered(app)
8
+ app.helpers do
9
+ define_method(:require_llm!) do
10
+ return if defined?(Legion::LLM) &&
11
+ Legion::LLM.respond_to?(:started?) &&
12
+ Legion::LLM.started?
13
+
14
+ halt 503, json_error('llm_unavailable', 'LLM subsystem is not available', status_code: 503)
15
+ end
16
+
17
+ define_method(:prompt_client) do
18
+ require 'legion/extensions/prompt/client'
19
+ Legion::Extensions::Prompt::Client.new
20
+ rescue LoadError
21
+ halt 503, json_error('prompt_unavailable', 'lex-prompt is not loaded', status_code: 503)
22
+ end
23
+ end
24
+
25
+ register_list(app)
26
+ register_show(app)
27
+ register_run(app)
28
+ end
29
+
30
+ def self.register_list(app)
31
+ app.get '/api/prompts' do
32
+ client = prompt_client
33
+ result = client.list_prompts
34
+ json_response(result)
35
+ rescue StandardError => e
36
+ Legion::Logging.error "API prompts list error: #{e.message}"
37
+ json_error('execution_error', e.message, status_code: 500)
38
+ end
39
+ end
40
+
41
+ def self.register_show(app)
42
+ app.get '/api/prompts/:name' do
43
+ name = params[:name]
44
+ client = prompt_client
45
+ result = client.get_prompt(name: name)
46
+
47
+ halt 404, json_error('not_found', "prompt '#{name}' not found", status_code: 404) if result[:error]
48
+
49
+ json_response(result)
50
+ rescue StandardError => e
51
+ Legion::Logging.error "API prompts show error: #{e.message}"
52
+ json_error('execution_error', e.message, status_code: 500)
53
+ end
54
+ end
55
+
56
+ def self.register_run(app)
57
+ app.post '/api/prompts/:name/run' do
58
+ require_llm!
59
+
60
+ name = params[:name]
61
+ body = parse_request_body
62
+ variables = body[:variables] || {}
63
+ version = body[:version]
64
+ model = body[:model]
65
+ provider = body[:provider]
66
+
67
+ client = prompt_client
68
+ rendered = client.render_prompt(name: name, variables: variables, version: version)
69
+
70
+ if rendered[:error]
71
+ code = rendered[:error] == 'not_found' ? 404 : 422
72
+ halt code, json_error(rendered[:error], "prompt '#{name}' #{rendered[:error].tr('_', ' ')}", status_code: code)
73
+ end
74
+
75
+ session = Legion::LLM.chat_direct(model: model, provider: provider)
76
+ response = session.ask(rendered[:rendered])
77
+
78
+ prompt_version = rendered[:prompt_version]
79
+ model_used = session.model.to_s
80
+
81
+ usage = {
82
+ input_tokens: response.respond_to?(:input_tokens) ? response.input_tokens : nil,
83
+ output_tokens: response.respond_to?(:output_tokens) ? response.output_tokens : nil
84
+ }
85
+
86
+ json_response({
87
+ name: name,
88
+ version: prompt_version,
89
+ rendered_prompt: rendered[:rendered],
90
+ response: response.content,
91
+ usage: usage,
92
+ model: model_used,
93
+ provider: provider
94
+ })
95
+ rescue StandardError => e
96
+ Legion::Logging.error "API prompts run error: #{e.message}"
97
+ json_error('execution_error', e.message, status_code: 500)
98
+ end
99
+ end
100
+
101
+ class << self
102
+ private :register_list, :register_show, :register_run
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
data/lib/legion/api.rb CHANGED
@@ -38,6 +38,7 @@ require_relative 'api/org_chart'
38
38
  require_relative 'api/workflow'
39
39
  require_relative 'api/governance'
40
40
  require_relative 'api/acp'
41
+ require_relative 'api/prompts'
41
42
 
42
43
  module Legion
43
44
  class API < Sinatra::Base
@@ -120,6 +121,7 @@ module Legion
120
121
  register Routes::OrgChart
121
122
  register Routes::Governance
122
123
  register Routes::Acp
124
+ register Routes::Prompts
123
125
 
124
126
  use Legion::Rbac::Middleware if defined?(Legion::Rbac::Middleware)
125
127
 
@@ -140,6 +140,36 @@ module Legion
140
140
  end
141
141
  end
142
142
 
143
+ desc 'play NAME', 'Run a prompt through an LLM and display the response'
144
+ option :variables, type: :string, desc: 'Template variables as JSON'
145
+ option :version, type: :numeric, desc: 'Prompt version'
146
+ option :model, type: :string, desc: 'LLM model override'
147
+ option :provider, type: :string, desc: 'LLM provider override'
148
+ option :compare, type: :numeric, desc: 'Compare with this version'
149
+ def play(name)
150
+ out = formatter
151
+ with_prompt_client do |client|
152
+ unless defined?(Legion::LLM) && Legion::LLM.started?
153
+ out.error('legion-llm is not available. Install legion-llm and configure a provider.')
154
+ raise SystemExit, 1
155
+ end
156
+
157
+ vars = parse_variables(options[:variables], out)
158
+ return if vars.nil?
159
+
160
+ llm_kwargs = {}
161
+ llm_kwargs[:model] = options[:model] if options[:model]
162
+ llm_kwargs[:provider] = options[:provider] if options[:provider]
163
+
164
+ base_ctx = { name: name, vars: vars, llm_kwargs: llm_kwargs, client: client, out: out }
165
+ if options[:compare]
166
+ run_compare(base_ctx.merge(ver_a: options[:version], ver_b: options[:compare]))
167
+ else
168
+ run_single(base_ctx.merge(version: options[:version]))
169
+ end
170
+ end
171
+ end
172
+
143
173
  no_commands do
144
174
  def formatter
145
175
  @formatter ||= Output::Formatter.new(
@@ -152,6 +182,7 @@ module Legion
152
182
  Connection.config_dir = options[:config_dir] if options[:config_dir]
153
183
  Connection.log_level = options[:verbose] ? 'debug' : 'error'
154
184
  Connection.ensure_data
185
+ Connection.ensure_llm
155
186
 
156
187
  begin
157
188
  require 'legion/extensions/prompt'
@@ -181,6 +212,15 @@ module Legion
181
212
  nil
182
213
  end
183
214
 
215
+ def parse_variables(raw, out)
216
+ return {} if raw.nil? || raw.empty?
217
+
218
+ ::JSON.parse(raw)
219
+ rescue ::JSON::ParserError => e
220
+ out.error("Invalid JSON for --variables: #{e.message}")
221
+ nil
222
+ end
223
+
184
224
  def diff_lines(old_text, new_text)
185
225
  old_lines = old_text.split("\n")
186
226
  new_lines = new_text.split("\n")
@@ -191,6 +231,102 @@ module Legion
191
231
  new_lines.each { |l| result << "+ #{l}" unless old_set.include?(l) }
192
232
  result.join("\n")
193
233
  end
234
+
235
+ def run_single(ctx)
236
+ name, version, vars, llm_kwargs, client, out = ctx.values_at(:name, :version, :vars, :llm_kwargs, :client, :out)
237
+ prompt = fetch_prompt(name, version, client, out)
238
+ return if prompt.nil?
239
+
240
+ rendered = render_prompt(name, version, vars, client, out)
241
+ return if rendered.nil?
242
+
243
+ response = Legion::LLM.chat(
244
+ messages: [{ role: 'user', content: rendered }],
245
+ **llm_kwargs
246
+ )
247
+
248
+ if options[:json]
249
+ out.json({ name: name, version: prompt[:version], rendered: rendered,
250
+ response: response[:content], usage: response[:usage] })
251
+ else
252
+ out.header("Prompt: #{name} (v#{prompt[:version]})")
253
+ out.spacer
254
+ out.header('Rendered Template')
255
+ puts rendered
256
+ out.spacer
257
+ out.header('LLM Response')
258
+ puts response[:content]
259
+ display_usage(response[:usage], out)
260
+ end
261
+ end
262
+
263
+ def run_compare(ctx)
264
+ name, ver_a, ver_b, vars, llm_kwargs, client, out =
265
+ ctx.values_at(:name, :ver_a, :ver_b, :vars, :llm_kwargs, :client, :out)
266
+ prompt_a = fetch_prompt(name, ver_a, client, out)
267
+ return if prompt_a.nil?
268
+
269
+ prompt_b = fetch_prompt(name, ver_b, client, out)
270
+ return if prompt_b.nil?
271
+
272
+ rendered_a = render_prompt(name, prompt_a[:version], vars, client, out)
273
+ return if rendered_a.nil?
274
+
275
+ rendered_b = render_prompt(name, prompt_b[:version], vars, client, out)
276
+ return if rendered_b.nil?
277
+
278
+ response_a = Legion::LLM.chat(messages: [{ role: 'user', content: rendered_a }], **llm_kwargs)
279
+ response_b = Legion::LLM.chat(messages: [{ role: 'user', content: rendered_b }], **llm_kwargs)
280
+
281
+ if options[:json]
282
+ out.json({ name: name, version_a: prompt_a[:version], version_b: prompt_b[:version],
283
+ rendered_a: rendered_a, rendered_b: rendered_b,
284
+ response_a: response_a[:content], response_b: response_b[:content],
285
+ usage_a: response_a[:usage], usage_b: response_b[:usage] })
286
+ else
287
+ out.header("Version A (v#{prompt_a[:version]})")
288
+ puts response_a[:content]
289
+ out.spacer
290
+ out.header("Version B (v#{prompt_b[:version]})")
291
+ puts response_b[:content]
292
+ content_a = response_a[:content].to_s
293
+ content_b = response_b[:content].to_s
294
+ if content_a != content_b
295
+ out.spacer
296
+ out.header('Diff (A vs B)')
297
+ puts diff_lines(content_a, content_b)
298
+ end
299
+ end
300
+ end
301
+
302
+ def fetch_prompt(name, version, client, out)
303
+ kwargs = { name: name }
304
+ kwargs[:version] = version if version
305
+ result = client.get_prompt(**kwargs)
306
+ if result[:error]
307
+ out.error("Prompt '#{name}': #{result[:error]}")
308
+ raise SystemExit, 1
309
+ end
310
+ result
311
+ end
312
+
313
+ def render_prompt(name, version, vars, client, out)
314
+ kwargs = { name: name, variables: vars }
315
+ kwargs[:version] = version if version
316
+ result = client.render_prompt(**kwargs)
317
+ if result.is_a?(Hash) && result[:error]
318
+ out.error("Render error for '#{name}': #{result[:error]}")
319
+ raise SystemExit, 1
320
+ end
321
+ result.is_a?(Hash) ? result[:rendered] : result
322
+ end
323
+
324
+ def display_usage(usage, out)
325
+ return unless usage && !usage.empty?
326
+
327
+ out.spacer
328
+ out.detail(usage)
329
+ end
194
330
  end
195
331
  end
196
332
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.4.93'
4
+ VERSION = '1.4.95'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legionio
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.93
4
+ version: 1.4.95
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -379,6 +379,7 @@ files:
379
379
  - lib/legion/api/nodes.rb
380
380
  - lib/legion/api/openapi.rb
381
381
  - lib/legion/api/org_chart.rb
382
+ - lib/legion/api/prompts.rb
382
383
  - lib/legion/api/rbac.rb
383
384
  - lib/legion/api/relationships.rb
384
385
  - lib/legion/api/schedules.rb