legionio 1.4.91 → 1.4.92
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 +12 -0
- data/lib/legion/cli/lex/templates/data_pipeline/actors/ingest.rb.erb +24 -0
- data/lib/legion/cli/lex/templates/data_pipeline/runners/transform.rb.erb +56 -0
- data/lib/legion/cli/lex/templates/data_pipeline/spec/actors/ingest_spec.rb.erb +19 -0
- data/lib/legion/cli/lex/templates/data_pipeline/spec/runners/transform_spec.rb.erb +41 -0
- data/lib/legion/cli/lex/templates/data_pipeline/transport/exchanges/%name%.rb.erb +16 -0
- data/lib/legion/cli/lex/templates/data_pipeline/transport/messages/%name%_output.rb.erb +27 -0
- data/lib/legion/cli/lex/templates/data_pipeline/transport/queues/ingest.rb.erb +17 -0
- data/lib/legion/cli/lex/templates/llm_agent/helpers/client.rb.erb +43 -0
- data/lib/legion/cli/lex/templates/llm_agent/prompts/default.yml.erb +14 -0
- data/lib/legion/cli/lex/templates/llm_agent/runners/%name%.rb.erb +37 -0
- data/lib/legion/cli/lex/templates/llm_agent/spec/runners/%name%_spec.rb.erb +40 -0
- data/lib/legion/cli/lex/templates/service_integration/helpers/auth.rb.erb +37 -0
- data/lib/legion/cli/lex/templates/service_integration/helpers/client.rb.erb +53 -0
- data/lib/legion/cli/lex/templates/service_integration/runners/%name%.rb.erb +61 -0
- data/lib/legion/cli/lex/templates/service_integration/spec/helpers/client_spec.rb.erb +46 -0
- data/lib/legion/cli/lex/templates/service_integration/spec/runners/%name%_spec.rb.erb +52 -0
- data/lib/legion/cli/lex_command.rb +55 -9
- data/lib/legion/cli/lex_templates.rb +74 -2
- data/lib/legion/version.rb +1 -1
- metadata +17 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d9e1879209235490bdbd660fc1d57fcc57d9487b4a17b96fce351311682fabd
|
|
4
|
+
data.tar.gz: 05e9a1fea38e182e9d2d33df3decc365f6af12d5f1a58e7496e33dfdfd5431e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d56827c6a9bd485fb2a1e72e4b9dcd651074c1222016e3f369eee43d22d580d3c7a369bb0a07d9f47e21c453b476bf7132ec217834c3fb06fefcc1f82079b7a
|
|
7
|
+
data.tar.gz: 72c7bf18dc56f721e290a576ac71798b9559e5a91aeab6db2a720d8b5e97125e2909d62f1489dd7aea2806009b84392db5ab7cac32618e7b7b24ccc53677bfa3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.92] - 2026-03-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `--template` option on `legion lex create` to scaffold pattern-specific extensions: `llm-agent`, `service-integration`, `data-pipeline` (default: `basic`)
|
|
7
|
+
- `--list-templates` option on `legion lex create` to display available templates with descriptions
|
|
8
|
+
- `LexTemplates::TemplateOverlay` class renders ERB template files into the target extension directory
|
|
9
|
+
- ERB scaffold templates under `lib/legion/cli/lex/templates/`: `llm_agent/`, `service_integration/`, `data_pipeline/`
|
|
10
|
+
- `llm-agent` template: LLM runner with `Legion::LLM.chat` and structured output, helpers/client.rb with model/temperature kwargs, default prompt YAML, spec with LLM mock
|
|
11
|
+
- `service-integration` template: CRUD runners (list/get/create/update/delete), Faraday HTTP client helper with api_key/bearer/basic auth, auth helper, specs with WebMock stubs
|
|
12
|
+
- `data-pipeline` template: transform runner with validate/process/publish pattern, subscription ingest actor, transport exchange/queue/message scaffolds, runner and actor specs
|
|
13
|
+
- Template registry extended with `data-pipeline`, `template_dir` class method, new `llm-agent`/`service-integration` entries with `template_dir` keys
|
|
14
|
+
|
|
3
15
|
## [1.4.91] - 2026-03-20
|
|
4
16
|
|
|
5
17
|
### Fixed
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module <%= lex_class %>
|
|
6
|
+
module Actors
|
|
7
|
+
class Ingest < Legion::Extensions::Actors::Subscription
|
|
8
|
+
include Legion::Extensions::<%= lex_class %>::Runners::Transform
|
|
9
|
+
|
|
10
|
+
QUEUE = 'legion.<%= lex_name %>.ingest'
|
|
11
|
+
EXCHANGE = 'legion.<%= lex_name %>'
|
|
12
|
+
|
|
13
|
+
def self.queue
|
|
14
|
+
QUEUE
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.exchange
|
|
18
|
+
EXCHANGE
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module <%= lex_class %>
|
|
6
|
+
module Runners
|
|
7
|
+
module Transform
|
|
8
|
+
extend Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?('Helpers::Lex')
|
|
9
|
+
|
|
10
|
+
# Main transform entry point. Receives a payload hash, returns transformed output.
|
|
11
|
+
def transform(payload:, options: {}, **)
|
|
12
|
+
validated = validate_input(payload)
|
|
13
|
+
return validated unless validated[:success]
|
|
14
|
+
|
|
15
|
+
result = process(validated[:data], options)
|
|
16
|
+
publish_output(result) if defined?(Legion::Transport)
|
|
17
|
+
|
|
18
|
+
{ success: true, data: result }
|
|
19
|
+
rescue StandardError => e
|
|
20
|
+
handle_error(e, payload)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def validate_input(payload)
|
|
26
|
+
return { success: false, reason: 'payload is required' } if payload.nil?
|
|
27
|
+
return { success: false, reason: 'payload must be a Hash' } unless payload.is_a?(Hash)
|
|
28
|
+
|
|
29
|
+
{ success: true, data: payload }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def process(data, _options)
|
|
33
|
+
# TODO: implement transformation logic
|
|
34
|
+
data
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def publish_output(result)
|
|
38
|
+
return unless defined?(Legion::Transport)
|
|
39
|
+
|
|
40
|
+
Legion::Transport::Messages::<%= name_class %>Output.new(data: result).publish
|
|
41
|
+
rescue StandardError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def handle_error(error, payload)
|
|
46
|
+
{
|
|
47
|
+
success: false,
|
|
48
|
+
reason: error.message,
|
|
49
|
+
payload: payload
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::<%= lex_class %>::Actors::Ingest do
|
|
4
|
+
it 'inherits from Subscription actor' do
|
|
5
|
+
expect(described_class.ancestors).to include(Legion::Extensions::Actors::Subscription)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it 'includes the Transform runner' do
|
|
9
|
+
expect(described_class.ancestors).to include(Legion::Extensions::<%= lex_class %>::Runners::Transform)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'defines a queue name' do
|
|
13
|
+
expect(described_class::QUEUE).to include('<%= lex_name %>')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'defines an exchange name' do
|
|
17
|
+
expect(described_class::EXCHANGE).to include('<%= lex_name %>')
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::<%= lex_class %>::Runners::Transform do
|
|
4
|
+
subject { described_class }
|
|
5
|
+
|
|
6
|
+
let(:test_class) do
|
|
7
|
+
Class.new do
|
|
8
|
+
extend Legion::Extensions::<%= lex_class %>::Runners::Transform
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it { should be_a Module }
|
|
13
|
+
it { is_expected.to respond_to(:transform).with_any_keywords }
|
|
14
|
+
|
|
15
|
+
describe '#transform' do
|
|
16
|
+
it 'returns success for a valid payload' do
|
|
17
|
+
result = test_class.transform(payload: { key: 'value' })
|
|
18
|
+
expect(result[:success]).to be true
|
|
19
|
+
expect(result[:data]).to be_a(Hash)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'returns failure when payload is nil' do
|
|
23
|
+
result = test_class.transform(payload: nil)
|
|
24
|
+
expect(result[:success]).to be false
|
|
25
|
+
expect(result[:reason]).to include('required')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'returns failure when payload is not a Hash' do
|
|
29
|
+
result = test_class.transform(payload: 'not a hash')
|
|
30
|
+
expect(result[:success]).to be false
|
|
31
|
+
expect(result[:reason]).to include('Hash')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'handles unexpected errors gracefully' do
|
|
35
|
+
allow(test_class).to receive(:process).and_raise(StandardError, 'unexpected')
|
|
36
|
+
result = test_class.transform(payload: { key: 'value' })
|
|
37
|
+
expect(result[:success]).to be false
|
|
38
|
+
expect(result[:reason]).to eq('unexpected')
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module <%= lex_class %>
|
|
6
|
+
module Transport
|
|
7
|
+
module Exchanges
|
|
8
|
+
class <%= name_class %> < Legion::Transport::Exchange
|
|
9
|
+
EXCHANGE_NAME = 'legion.<%= lex_name %>'
|
|
10
|
+
EXCHANGE_TYPE = :direct
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module <%= lex_class %>
|
|
6
|
+
module Transport
|
|
7
|
+
module Messages
|
|
8
|
+
class <%= name_class %>Output < Legion::Transport::Message
|
|
9
|
+
EXCHANGE_NAME = 'legion.<%= lex_name %>'
|
|
10
|
+
ROUTING_KEY = 'output'
|
|
11
|
+
|
|
12
|
+
attr_accessor :data
|
|
13
|
+
|
|
14
|
+
def initialize(data:)
|
|
15
|
+
@data = data
|
|
16
|
+
super()
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_payload
|
|
20
|
+
{ data: @data }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module <%= lex_class %>
|
|
6
|
+
module Transport
|
|
7
|
+
module Queues
|
|
8
|
+
class Ingest < Legion::Transport::Queue
|
|
9
|
+
QUEUE_NAME = 'legion.<%= lex_name %>.ingest'
|
|
10
|
+
EXCHANGE_NAME = 'legion.<%= lex_name %>'
|
|
11
|
+
ROUTING_KEY = 'ingest'
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module <%= lex_class %>
|
|
6
|
+
module Helpers
|
|
7
|
+
class Client
|
|
8
|
+
attr_reader :model, :temperature, :max_tokens
|
|
9
|
+
|
|
10
|
+
def initialize(model: nil, temperature: 0.7, max_tokens: 1024, **)
|
|
11
|
+
@model = model
|
|
12
|
+
@temperature = temperature
|
|
13
|
+
@max_tokens = max_tokens
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def chat(prompt:, **override)
|
|
17
|
+
return { success: false, reason: 'legion-llm not available' } unless defined?(Legion::LLM)
|
|
18
|
+
|
|
19
|
+
opts = { prompt: prompt, temperature: @temperature, max_tokens: @max_tokens }
|
|
20
|
+
opts[:model] = @model if @model
|
|
21
|
+
opts.merge!(override)
|
|
22
|
+
|
|
23
|
+
Legion::LLM.chat(**opts)
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
{ success: false, reason: e.message }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def structured(prompt:, schema:, **override)
|
|
29
|
+
return { success: false, reason: 'legion-llm not available' } unless defined?(Legion::LLM)
|
|
30
|
+
|
|
31
|
+
opts = { prompt: prompt, schema: schema }
|
|
32
|
+
opts[:model] = @model if @model
|
|
33
|
+
opts.merge!(override)
|
|
34
|
+
|
|
35
|
+
Legion::LLM.structured(**opts)
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
{ success: false, reason: e.message }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Default prompt template for <%= gem_name %>
|
|
3
|
+
# Customize system and user prompts for your use case.
|
|
4
|
+
|
|
5
|
+
system: |
|
|
6
|
+
You are a helpful assistant for <%= lex_class %>.
|
|
7
|
+
Respond concisely and accurately.
|
|
8
|
+
|
|
9
|
+
user: |
|
|
10
|
+
<%= '{{input}}' %>
|
|
11
|
+
|
|
12
|
+
examples:
|
|
13
|
+
- input: "What can you help me with?"
|
|
14
|
+
expected: "I can help you with <%= lex_class.downcase %>-related tasks."
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module <%= lex_class %>
|
|
6
|
+
module Runners
|
|
7
|
+
module <%= name_class %>
|
|
8
|
+
extend Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?('Helpers::Lex')
|
|
9
|
+
|
|
10
|
+
def run(prompt:, model: nil, temperature: nil, structured: false, schema: nil, **)
|
|
11
|
+
llm_opts = { prompt: prompt }
|
|
12
|
+
llm_opts[:model] = model if model
|
|
13
|
+
llm_opts[:temperature] = temperature if temperature
|
|
14
|
+
|
|
15
|
+
if structured && defined?(Legion::LLM)
|
|
16
|
+
response = Legion::LLM.structured(prompt: prompt, schema: schema || default_schema)
|
|
17
|
+
elsif defined?(Legion::LLM)
|
|
18
|
+
response = Legion::LLM.chat(**llm_opts)
|
|
19
|
+
else
|
|
20
|
+
return { success: false, reason: 'legion-llm not available' }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
{ success: true, response: response }
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
{ success: false, reason: e.message }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def default_schema
|
|
31
|
+
{ type: 'object', properties: { result: { type: 'string' } } }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::<%= lex_class %>::Runners::<%= name_class %> do
|
|
4
|
+
subject { described_class }
|
|
5
|
+
|
|
6
|
+
let(:test_class) do
|
|
7
|
+
Class.new do
|
|
8
|
+
extend Legion::Extensions::<%= lex_class %>::Runners::<%= name_class %>
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
stub_const('Legion::LLM', Module.new do
|
|
14
|
+
def self.chat(**_opts)
|
|
15
|
+
{ success: true, content: 'mock response' }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.structured(**_opts)
|
|
19
|
+
{ success: true, result: 'mock result' }
|
|
20
|
+
end
|
|
21
|
+
end)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it { should be_a Module }
|
|
25
|
+
it { is_expected.to respond_to(:run).with_any_keywords }
|
|
26
|
+
|
|
27
|
+
describe '#run' do
|
|
28
|
+
it 'returns success with a response' do
|
|
29
|
+
result = test_class.run(prompt: 'hello')
|
|
30
|
+
expect(result[:success]).to be true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'returns failure when legion-llm is unavailable' do
|
|
34
|
+
hide_const('Legion::LLM')
|
|
35
|
+
result = test_class.run(prompt: 'hello')
|
|
36
|
+
expect(result[:success]).to be false
|
|
37
|
+
expect(result[:reason]).to include('legion-llm')
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module <%= lex_class %>
|
|
6
|
+
module Helpers
|
|
7
|
+
module Auth
|
|
8
|
+
# Build an auth hash from settings or explicit kwargs.
|
|
9
|
+
# Supports three methods:
|
|
10
|
+
# api_key — { method: :api_key, key: '...', header: 'X-API-Key' }
|
|
11
|
+
# bearer — { method: :bearer, token: '...' }
|
|
12
|
+
# basic — { method: :basic, username: '...', password: '...' }
|
|
13
|
+
def self.from_settings(settings = {})
|
|
14
|
+
return {} if settings.nil? || settings.empty?
|
|
15
|
+
|
|
16
|
+
auth = settings[:auth] || settings['auth'] || {}
|
|
17
|
+
return {} if auth.empty?
|
|
18
|
+
|
|
19
|
+
auth
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.api_key(key, header: 'X-API-Key')
|
|
23
|
+
{ method: :api_key, key: key, header: header }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.bearer(token)
|
|
27
|
+
{ method: :bearer, token: token }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.basic(username, password)
|
|
31
|
+
{ method: :basic, username: username, password: password }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Extensions
|
|
8
|
+
module <%= lex_class %>
|
|
9
|
+
module Helpers
|
|
10
|
+
class Client
|
|
11
|
+
DEFAULT_TIMEOUT = 30
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def connection(base_url: nil, timeout: DEFAULT_TIMEOUT, auth: {}, **)
|
|
15
|
+
raise ArgumentError, 'base_url is required' if base_url.nil? || base_url.empty?
|
|
16
|
+
|
|
17
|
+
Faraday.new(url: base_url) do |conn|
|
|
18
|
+
conn.options.timeout = timeout
|
|
19
|
+
conn.options.open_timeout = timeout
|
|
20
|
+
conn.headers['Content-Type'] = 'application/json'
|
|
21
|
+
conn.headers['Accept'] = 'application/json'
|
|
22
|
+
|
|
23
|
+
apply_auth(conn, auth)
|
|
24
|
+
|
|
25
|
+
conn.response :json, content_type: /\bjson$/
|
|
26
|
+
conn.adapter Faraday.default_adapter
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def apply_auth(conn, auth)
|
|
33
|
+
method = auth[:method] || auth['method']
|
|
34
|
+
|
|
35
|
+
case method&.to_sym
|
|
36
|
+
when :api_key
|
|
37
|
+
header = auth[:header] || auth['header'] || 'X-API-Key'
|
|
38
|
+
conn.headers[header] = auth[:key] || auth['key']
|
|
39
|
+
when :bearer
|
|
40
|
+
token = auth[:token] || auth['token']
|
|
41
|
+
conn.headers['Authorization'] = "Bearer #{token}"
|
|
42
|
+
when :basic
|
|
43
|
+
conn.request :authorization, :basic,
|
|
44
|
+
auth[:username] || auth['username'],
|
|
45
|
+
auth[:password] || auth['password']
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module <%= lex_class %>
|
|
6
|
+
module Runners
|
|
7
|
+
module <%= name_class %>
|
|
8
|
+
extend Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?('Helpers::Lex')
|
|
9
|
+
|
|
10
|
+
def list(**opts)
|
|
11
|
+
client = Helpers::Client.connection(**settings.merge(opts))
|
|
12
|
+
response = client.get('/')
|
|
13
|
+
{ success: true, data: response.body }
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
{ success: false, reason: e.message }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def get(id:, **)
|
|
19
|
+
client = Helpers::Client.connection(**settings)
|
|
20
|
+
response = client.get("/#{id}")
|
|
21
|
+
{ success: true, data: response.body }
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
{ success: false, reason: e.message }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create(**payload)
|
|
27
|
+
client = Helpers::Client.connection(**settings)
|
|
28
|
+
response = client.post('/') { |req| req.body = payload.to_json }
|
|
29
|
+
{ success: true, data: response.body }
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
{ success: false, reason: e.message }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def update(id:, **payload)
|
|
35
|
+
client = Helpers::Client.connection(**settings)
|
|
36
|
+
response = client.put("/#{id}") { |req| req.body = payload.to_json }
|
|
37
|
+
{ success: true, data: response.body }
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
{ success: false, reason: e.message }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def delete(id:, **)
|
|
43
|
+
client = Helpers::Client.connection(**settings)
|
|
44
|
+
response = client.delete("/#{id}")
|
|
45
|
+
{ success: true, status: response.status }
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
{ success: false, reason: e.message }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def settings
|
|
53
|
+
return {} unless defined?(Legion::Settings)
|
|
54
|
+
|
|
55
|
+
Legion::Settings.dig(:extensions, :<%= lex_name %>) || {}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'webmock/rspec'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::<%= lex_class %>::Helpers::Client do
|
|
6
|
+
let(:base_url) { 'https://api.example.com' }
|
|
7
|
+
|
|
8
|
+
before { WebMock.enable! }
|
|
9
|
+
after { WebMock.disable! }
|
|
10
|
+
|
|
11
|
+
describe '.connection' do
|
|
12
|
+
it 'raises ArgumentError when base_url is missing' do
|
|
13
|
+
expect { described_class.connection }.to raise_error(ArgumentError, /base_url/)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'returns a Faraday connection' do
|
|
17
|
+
conn = described_class.connection(base_url: base_url)
|
|
18
|
+
expect(conn).to be_a(Faraday::Connection)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'sets Content-Type header to application/json' do
|
|
22
|
+
conn = described_class.connection(base_url: base_url)
|
|
23
|
+
expect(conn.headers['Content-Type']).to eq('application/json')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context 'with api_key auth' do
|
|
27
|
+
it 'sets the API key header' do
|
|
28
|
+
conn = described_class.connection(
|
|
29
|
+
base_url: base_url,
|
|
30
|
+
auth: { method: :api_key, key: 'secret', header: 'X-API-Key' }
|
|
31
|
+
)
|
|
32
|
+
expect(conn.headers['X-API-Key']).to eq('secret')
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
context 'with bearer auth' do
|
|
37
|
+
it 'sets Authorization header' do
|
|
38
|
+
conn = described_class.connection(
|
|
39
|
+
base_url: base_url,
|
|
40
|
+
auth: { method: :bearer, token: 'mytoken' }
|
|
41
|
+
)
|
|
42
|
+
expect(conn.headers['Authorization']).to eq('Bearer mytoken')
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'webmock/rspec'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::<%= lex_class %>::Runners::<%= name_class %> do
|
|
6
|
+
subject { described_class }
|
|
7
|
+
|
|
8
|
+
let(:base_url) { 'https://api.example.com' }
|
|
9
|
+
let(:test_class) do
|
|
10
|
+
Class.new do
|
|
11
|
+
extend Legion::Extensions::<%= lex_class %>::Runners::<%= name_class %>
|
|
12
|
+
|
|
13
|
+
def self.settings
|
|
14
|
+
{ base_url: 'https://api.example.com' }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
before { WebMock.enable! }
|
|
20
|
+
after { WebMock.disable! }
|
|
21
|
+
|
|
22
|
+
it { should be_a Module }
|
|
23
|
+
it { is_expected.to respond_to(:list).with_any_keywords }
|
|
24
|
+
it { is_expected.to respond_to(:get).with_any_keywords }
|
|
25
|
+
it { is_expected.to respond_to(:create).with_any_keywords }
|
|
26
|
+
it { is_expected.to respond_to(:update).with_any_keywords }
|
|
27
|
+
it { is_expected.to respond_to(:delete).with_any_keywords }
|
|
28
|
+
|
|
29
|
+
describe '#list' do
|
|
30
|
+
before do
|
|
31
|
+
stub_request(:get, "#{base_url}/")
|
|
32
|
+
.to_return(status: 200, body: '[]', headers: { 'Content-Type' => 'application/json' })
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'returns success' do
|
|
36
|
+
result = test_class.list
|
|
37
|
+
expect(result[:success]).to be true
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe '#get' do
|
|
42
|
+
before do
|
|
43
|
+
stub_request(:get, "#{base_url}/42")
|
|
44
|
+
.to_return(status: 200, body: '{"id":42}', headers: { 'Content-Type' => 'application/json' })
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'returns success with data' do
|
|
48
|
+
result = test_class.get(id: 42)
|
|
49
|
+
expect(result[:success]).to be true
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'legion/extensions/helpers/segments'
|
|
5
5
|
require 'legion/cli/lex_cli_manifest'
|
|
6
|
+
require 'legion/cli/lex_templates'
|
|
6
7
|
|
|
7
8
|
module Legion
|
|
8
9
|
module CLI
|
|
@@ -96,14 +97,34 @@ module Legion
|
|
|
96
97
|
method_option :bundle_install, type: :boolean, default: true, desc: 'Run bundle install'
|
|
97
98
|
method_option :category, type: :string, default: nil,
|
|
98
99
|
desc: 'Extension category (agentic, ai, gaia). Determines namespace nesting and gem prefix.'
|
|
99
|
-
|
|
100
|
+
method_option :template, type: :string, default: 'basic',
|
|
101
|
+
desc: 'Scaffold template: basic, llm-agent, service-integration, data-pipeline'
|
|
102
|
+
method_option :list_templates, type: :boolean, default: false,
|
|
103
|
+
desc: 'List available scaffold templates and exit'
|
|
104
|
+
def create(name = nil)
|
|
100
105
|
out = formatter
|
|
101
106
|
|
|
107
|
+
if options[:list_templates]
|
|
108
|
+
render_template_list(out)
|
|
109
|
+
return
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
unless name
|
|
113
|
+
out.error('NAME is required. Usage: legion lex create NAME [--template TEMPLATE]')
|
|
114
|
+
return
|
|
115
|
+
end
|
|
116
|
+
|
|
102
117
|
if options[:category] && options[:category] !~ /\A[a-z][a-z0-9_-]*\z/
|
|
103
118
|
out.error('--category must be lowercase letters, numbers, underscores, or hyphens')
|
|
104
119
|
return
|
|
105
120
|
end
|
|
106
121
|
|
|
122
|
+
template_name = options[:template] || 'basic'
|
|
123
|
+
unless LexTemplates.valid?(template_name)
|
|
124
|
+
out.warn("Unknown template '#{template_name}', falling back to 'basic'. Run `legion lex create --list-templates` to see available templates.")
|
|
125
|
+
template_name = 'basic'
|
|
126
|
+
end
|
|
127
|
+
|
|
107
128
|
gem_name = options[:category] ? "lex-#{options[:category]}-#{name}" : "lex-#{name}"
|
|
108
129
|
target_dir = gem_name
|
|
109
130
|
|
|
@@ -119,11 +140,11 @@ module Legion
|
|
|
119
140
|
|
|
120
141
|
Legion::Extensions.check_reserved_words(gem_name, known_org: false)
|
|
121
142
|
|
|
122
|
-
out.success("Creating #{gem_name}...")
|
|
143
|
+
out.success("Creating #{gem_name} (template: #{template_name})...")
|
|
123
144
|
|
|
124
145
|
vars = { filename: target_dir, class_name: name.split('_').map(&:capitalize).join, lex: name }
|
|
125
146
|
|
|
126
|
-
generator = LexGenerator.new(name, vars, options, gem_name: gem_name)
|
|
147
|
+
generator = LexGenerator.new(name, vars, options, gem_name: gem_name, template: template_name)
|
|
127
148
|
generator.generate(out)
|
|
128
149
|
|
|
129
150
|
out.spacer
|
|
@@ -267,6 +288,17 @@ module Legion
|
|
|
267
288
|
)
|
|
268
289
|
end
|
|
269
290
|
|
|
291
|
+
def render_template_list(out)
|
|
292
|
+
templates = LexTemplates.list
|
|
293
|
+
if options[:json]
|
|
294
|
+
out.json(templates)
|
|
295
|
+
else
|
|
296
|
+
out.header('Available scaffold templates')
|
|
297
|
+
rows = templates.map { |t| [t[:name], t[:description]] }
|
|
298
|
+
out.table(%w[template description], rows)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
270
302
|
def render_flat_table(out, rows)
|
|
271
303
|
table_rows = rows.map do |l|
|
|
272
304
|
[l[:name], l[:version], l[:category].to_s, l[:tier].to_s, out.status(l[:status]), l[:runners].to_s, l[:actors].to_s]
|
|
@@ -399,16 +431,18 @@ module Legion
|
|
|
399
431
|
|
|
400
432
|
# Thin generator class that wraps the template logic
|
|
401
433
|
class LexGenerator
|
|
402
|
-
def initialize(name, vars, options, gem_name: nil)
|
|
403
|
-
@name
|
|
404
|
-
@vars
|
|
405
|
-
@options
|
|
406
|
-
@gem_name
|
|
407
|
-
@target
|
|
434
|
+
def initialize(name, vars, options, gem_name: nil, template: 'basic')
|
|
435
|
+
@name = name
|
|
436
|
+
@vars = vars
|
|
437
|
+
@options = options
|
|
438
|
+
@gem_name = gem_name || "lex-#{name}"
|
|
439
|
+
@target = @gem_name
|
|
440
|
+
@template = template || 'basic'
|
|
408
441
|
end
|
|
409
442
|
|
|
410
443
|
def generate(out)
|
|
411
444
|
create_structure(out)
|
|
445
|
+
apply_template_overlay(out) unless @template == 'basic'
|
|
412
446
|
init_git(out) if @options[:git_init]
|
|
413
447
|
run_bundle(out) if @options[:bundle_install]
|
|
414
448
|
end
|
|
@@ -515,6 +549,18 @@ module Legion
|
|
|
515
549
|
File.write(path, content)
|
|
516
550
|
end
|
|
517
551
|
|
|
552
|
+
def apply_template_overlay(out)
|
|
553
|
+
segs = Legion::Extensions::Helpers::Segments.derive_segments(@gem_name)
|
|
554
|
+
lex_class = segs.map(&:capitalize).join('::')
|
|
555
|
+
lex_name = @name
|
|
556
|
+
name_class = @name.split(/[_-]/).map(&:capitalize).join
|
|
557
|
+
gem_name = @gem_name
|
|
558
|
+
|
|
559
|
+
vars = { lex_class: lex_class, lex_name: lex_name, name_class: name_class, gem_name: gem_name }
|
|
560
|
+
overlay = LexTemplates::TemplateOverlay.new(@template, @target, vars)
|
|
561
|
+
overlay.apply(out)
|
|
562
|
+
end
|
|
563
|
+
|
|
518
564
|
def init_git(out)
|
|
519
565
|
Dir.chdir(@target) do
|
|
520
566
|
system('git init -q')
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
3
6
|
module Legion
|
|
4
7
|
module CLI
|
|
5
8
|
module LexTemplates
|
|
9
|
+
TEMPLATES_DIR = File.join(File.dirname(__FILE__), 'lex', 'templates').freeze
|
|
10
|
+
|
|
6
11
|
REGISTRY = {
|
|
7
12
|
'basic' => {
|
|
8
13
|
runners: ['default'],
|
|
@@ -18,7 +23,8 @@ module Legion
|
|
|
18
23
|
tools: %w[process analyze],
|
|
19
24
|
client: true,
|
|
20
25
|
dependencies: ['legion-llm'],
|
|
21
|
-
description: 'LLM-powered agent extension'
|
|
26
|
+
description: 'LLM-powered agent extension',
|
|
27
|
+
template_dir: 'llm_agent'
|
|
22
28
|
},
|
|
23
29
|
'service-integration' => {
|
|
24
30
|
runners: ['operations'],
|
|
@@ -26,7 +32,17 @@ module Legion
|
|
|
26
32
|
tools: [],
|
|
27
33
|
client: true,
|
|
28
34
|
dependencies: [],
|
|
29
|
-
description: 'External service integration with standalone client'
|
|
35
|
+
description: 'External service integration with standalone client',
|
|
36
|
+
template_dir: 'service_integration'
|
|
37
|
+
},
|
|
38
|
+
'data-pipeline' => {
|
|
39
|
+
runners: ['transform'],
|
|
40
|
+
actors: ['ingest'],
|
|
41
|
+
tools: [],
|
|
42
|
+
client: false,
|
|
43
|
+
dependencies: [],
|
|
44
|
+
description: 'Event-driven data processing pipeline',
|
|
45
|
+
template_dir: 'data_pipeline'
|
|
30
46
|
},
|
|
31
47
|
'scheduled-task' => {
|
|
32
48
|
runners: ['executor'],
|
|
@@ -58,6 +74,62 @@ module Legion
|
|
|
58
74
|
def valid?(name)
|
|
59
75
|
REGISTRY.key?(name.to_s)
|
|
60
76
|
end
|
|
77
|
+
|
|
78
|
+
def template_dir(name)
|
|
79
|
+
config = REGISTRY[name.to_s]
|
|
80
|
+
return nil unless config
|
|
81
|
+
|
|
82
|
+
dir_key = config[:template_dir]
|
|
83
|
+
return nil unless dir_key
|
|
84
|
+
|
|
85
|
+
File.join(TEMPLATES_DIR, dir_key)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Renders and writes template-specific overlay files into the target extension directory.
|
|
90
|
+
class TemplateOverlay
|
|
91
|
+
PLACEHOLDER = '%name%'
|
|
92
|
+
|
|
93
|
+
# vars: { gem_name:, lex_name:, lex_class:, name_class: }
|
|
94
|
+
def initialize(template_name, target_dir, vars)
|
|
95
|
+
@template_name = template_name
|
|
96
|
+
@target_dir = target_dir
|
|
97
|
+
@vars = vars
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def apply(out = nil)
|
|
101
|
+
src = LexTemplates.template_dir(@template_name)
|
|
102
|
+
return unless src && Dir.exist?(src)
|
|
103
|
+
|
|
104
|
+
each_template_file(src) do |abs_src, rel_path|
|
|
105
|
+
dest_rel = rel_path.gsub(PLACEHOLDER, @vars[:lex_name])
|
|
106
|
+
dest_rel = dest_rel.sub(/\.erb$/, '')
|
|
107
|
+
dest_abs = File.join(@target_dir, dest_rel)
|
|
108
|
+
|
|
109
|
+
FileUtils.mkdir_p(File.dirname(dest_abs))
|
|
110
|
+
rendered = render_erb(File.read(abs_src))
|
|
111
|
+
File.write(dest_abs, rendered)
|
|
112
|
+
out&.success(" [#{@template_name}] #{dest_rel}")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def each_template_file(src_dir, &block)
|
|
119
|
+
Dir.glob("#{src_dir}/**/*.erb").each do |abs_src|
|
|
120
|
+
rel_path = abs_src.sub("#{src_dir}/", '')
|
|
121
|
+
block.call(abs_src, rel_path)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def render_erb(template_text)
|
|
126
|
+
lex_class = @vars[:lex_class]
|
|
127
|
+
lex_name = @vars[:lex_name]
|
|
128
|
+
name_class = @vars[:name_class]
|
|
129
|
+
gem_name = @vars[:gem_name]
|
|
130
|
+
|
|
131
|
+
ERB.new(template_text, trim_mode: '-').result(binding)
|
|
132
|
+
end
|
|
61
133
|
end
|
|
62
134
|
end
|
|
63
135
|
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.92
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -494,8 +494,19 @@ files:
|
|
|
494
494
|
- lib/legion/cli/lex/templates/base/rubocop.yml.erb
|
|
495
495
|
- lib/legion/cli/lex/templates/base/spec_helper.rb.erb
|
|
496
496
|
- lib/legion/cli/lex/templates/base/version.erb
|
|
497
|
+
- lib/legion/cli/lex/templates/data_pipeline/actors/ingest.rb.erb
|
|
498
|
+
- lib/legion/cli/lex/templates/data_pipeline/runners/transform.rb.erb
|
|
499
|
+
- lib/legion/cli/lex/templates/data_pipeline/spec/actors/ingest_spec.rb.erb
|
|
500
|
+
- lib/legion/cli/lex/templates/data_pipeline/spec/runners/transform_spec.rb.erb
|
|
501
|
+
- lib/legion/cli/lex/templates/data_pipeline/transport/exchanges/%name%.rb.erb
|
|
502
|
+
- lib/legion/cli/lex/templates/data_pipeline/transport/messages/%name%_output.rb.erb
|
|
503
|
+
- lib/legion/cli/lex/templates/data_pipeline/transport/queues/ingest.rb.erb
|
|
497
504
|
- lib/legion/cli/lex/templates/exchange.erb
|
|
498
505
|
- lib/legion/cli/lex/templates/exchange_spec.erb
|
|
506
|
+
- lib/legion/cli/lex/templates/llm_agent/helpers/client.rb.erb
|
|
507
|
+
- lib/legion/cli/lex/templates/llm_agent/prompts/default.yml.erb
|
|
508
|
+
- lib/legion/cli/lex/templates/llm_agent/runners/%name%.rb.erb
|
|
509
|
+
- lib/legion/cli/lex/templates/llm_agent/spec/runners/%name%_spec.rb.erb
|
|
499
510
|
- lib/legion/cli/lex/templates/message.erb
|
|
500
511
|
- lib/legion/cli/lex/templates/message_spec.erb
|
|
501
512
|
- lib/legion/cli/lex/templates/queue.erb
|
|
@@ -503,6 +514,11 @@ files:
|
|
|
503
514
|
- lib/legion/cli/lex/templates/queue_spec.erb
|
|
504
515
|
- lib/legion/cli/lex/templates/runner.erb
|
|
505
516
|
- lib/legion/cli/lex/templates/runner_spec.erb
|
|
517
|
+
- lib/legion/cli/lex/templates/service_integration/helpers/auth.rb.erb
|
|
518
|
+
- lib/legion/cli/lex/templates/service_integration/helpers/client.rb.erb
|
|
519
|
+
- lib/legion/cli/lex/templates/service_integration/runners/%name%.rb.erb
|
|
520
|
+
- lib/legion/cli/lex/templates/service_integration/spec/helpers/client_spec.rb.erb
|
|
521
|
+
- lib/legion/cli/lex/templates/service_integration/spec/runners/%name%_spec.rb.erb
|
|
506
522
|
- lib/legion/cli/lex_cli_manifest.rb
|
|
507
523
|
- lib/legion/cli/lex_command.rb
|
|
508
524
|
- lib/legion/cli/lex_templates.rb
|