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 +4 -4
- data/CHANGELOG.md +21 -0
- data/lib/legion/api/prompts.rb +107 -0
- data/lib/legion/api.rb +2 -0
- data/lib/legion/cli/prompt_command.rb +136 -0
- data/lib/legion/version.rb +1 -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: a110098f876265aa1233e4ca1e60baf30f5a23340b8d9de1f4a9c942ea0128fd
|
|
4
|
+
data.tar.gz: 3144aab6b5cab4ed042fee55d344cd2a8493ac48284a74ef431238643d91a863
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/legion/version.rb
CHANGED
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.
|
|
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
|