legionio 1.4.91 → 1.4.93
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/.rubocop.yml +1 -0
- data/CHANGELOG.md +21 -0
- data/completions/_legion +123 -0
- data/completions/legion.bash +28 -1
- data/lib/legion/cli/dataset_command.rb +148 -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/cli/prompt_command.rb +197 -0
- data/lib/legion/cli.rb +8 -0
- data/lib/legion/version.rb +1 -1
- metadata +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 318839858d16ecbaaf6c7db4a0d9562e1641ca24d174fda914b9d359f3f91e69
|
|
4
|
+
data.tar.gz: 91e1415070e2f547bc7c71646e79312f048f8973b2cb004045a8d546a0000387
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e5cd6e2ff71cc11db2a1e8928aae0c53104f931c18829bf470658bbcac3e2f90a18aeb7c564dcc7eb3f113fa9c65b54bb165a0f322fc3b1021a0ebf98148bf66
|
|
7
|
+
data.tar.gz: ff3c9e796fef50d7ca2e7b1970c4516a2d839031f8bee6036fc43b758f0035d85b5bc80fd631c5e55d8a83ce28fe5a7ccfa55fa69b67237b868331d1606d3d34
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.93] - 2026-03-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `legion prompt` CLI subcommand for versioned LLM prompt template management (list, show, create, tag, diff)
|
|
7
|
+
- `legion dataset` CLI subcommand for versioned dataset management (list, show, import, export)
|
|
8
|
+
- Both commands wrap `lex-prompt` and `lex-dataset` extension clients via `begin/rescue LoadError` guards
|
|
9
|
+
- Both commands guard with `Connection.ensure_data` and follow existing `with_*_client` pattern
|
|
10
|
+
- Tab completion entries for `prompt` and `dataset` in `completions/legion.bash` and `completions/_legion`
|
|
11
|
+
|
|
12
|
+
## [1.4.92] - 2026-03-20
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `--template` option on `legion lex create` to scaffold pattern-specific extensions: `llm-agent`, `service-integration`, `data-pipeline` (default: `basic`)
|
|
16
|
+
- `--list-templates` option on `legion lex create` to display available templates with descriptions
|
|
17
|
+
- `LexTemplates::TemplateOverlay` class renders ERB template files into the target extension directory
|
|
18
|
+
- ERB scaffold templates under `lib/legion/cli/lex/templates/`: `llm_agent/`, `service_integration/`, `data_pipeline/`
|
|
19
|
+
- `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
|
|
20
|
+
- `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
|
|
21
|
+
- `data-pipeline` template: transform runner with validate/process/publish pattern, subscription ingest actor, transport exchange/queue/message scaffolds, runner and actor specs
|
|
22
|
+
- Template registry extended with `data-pipeline`, `template_dir` class method, new `llm-agent`/`service-integration` entries with `template_dir` keys
|
|
23
|
+
|
|
3
24
|
## [1.4.91] - 2026-03-20
|
|
4
25
|
|
|
5
26
|
### Fixed
|
data/completions/_legion
CHANGED
|
@@ -45,6 +45,8 @@ _legion() {
|
|
|
45
45
|
commit) _legion_commit ;;
|
|
46
46
|
pr) _legion_pr ;;
|
|
47
47
|
review) _legion_review ;;
|
|
48
|
+
prompt) _legion_prompt ;;
|
|
49
|
+
dataset) _legion_dataset ;;
|
|
48
50
|
esac
|
|
49
51
|
;;
|
|
50
52
|
esac
|
|
@@ -62,6 +64,8 @@ _legion_commands() {
|
|
|
62
64
|
'init:Interactive project setup wizard'
|
|
63
65
|
'tty:Launch interactive TTY shell'
|
|
64
66
|
'ask:Quick AI prompt (shortcut for chat prompt)'
|
|
67
|
+
'prompt:Manage versioned LLM prompt templates'
|
|
68
|
+
'dataset:Manage versioned datasets'
|
|
65
69
|
'version:Show version information'
|
|
66
70
|
'help:Show help'
|
|
67
71
|
)
|
|
@@ -837,4 +841,123 @@ _legion_check() {
|
|
|
837
841
|
'--no-color[Disable color output]'
|
|
838
842
|
}
|
|
839
843
|
|
|
844
|
+
_legion_prompt() {
|
|
845
|
+
local -a subcmds
|
|
846
|
+
subcmds=(
|
|
847
|
+
'list:List all prompts'
|
|
848
|
+
'show:Show a prompt template and parameters'
|
|
849
|
+
'create:Create a new prompt'
|
|
850
|
+
'tag:Tag a prompt version'
|
|
851
|
+
'diff:Show text diff between two versions of a prompt'
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
_arguments -C \
|
|
855
|
+
'--json[Output as JSON]' \
|
|
856
|
+
'--no-color[Disable color output]' \
|
|
857
|
+
'--help[Show help]' \
|
|
858
|
+
'1: :->cmd' \
|
|
859
|
+
'*:: :->args'
|
|
860
|
+
|
|
861
|
+
case $state in
|
|
862
|
+
cmd) _describe 'prompt command' subcmds ;;
|
|
863
|
+
args)
|
|
864
|
+
case $words[1] in
|
|
865
|
+
list)
|
|
866
|
+
_arguments \
|
|
867
|
+
'--json[Output as JSON]' \
|
|
868
|
+
'--no-color[Disable color output]'
|
|
869
|
+
;;
|
|
870
|
+
show)
|
|
871
|
+
_arguments \
|
|
872
|
+
':prompt name:' \
|
|
873
|
+
'--version[Specific version number]:version:' \
|
|
874
|
+
'--tag[Tag name to resolve]:tag:' \
|
|
875
|
+
'--json[Output as JSON]' \
|
|
876
|
+
'--no-color[Disable color output]'
|
|
877
|
+
;;
|
|
878
|
+
create)
|
|
879
|
+
_arguments \
|
|
880
|
+
':prompt name:' \
|
|
881
|
+
'--template[Prompt template text]:template:' \
|
|
882
|
+
'--description[Short description]:desc:' \
|
|
883
|
+
'--model-params[Model parameters as JSON]:json:' \
|
|
884
|
+
'--json[Output as JSON]' \
|
|
885
|
+
'--no-color[Disable color output]'
|
|
886
|
+
;;
|
|
887
|
+
tag)
|
|
888
|
+
_arguments \
|
|
889
|
+
':prompt name:' \
|
|
890
|
+
':tag name:' \
|
|
891
|
+
'--version[Version to tag]:version:' \
|
|
892
|
+
'--json[Output as JSON]' \
|
|
893
|
+
'--no-color[Disable color output]'
|
|
894
|
+
;;
|
|
895
|
+
diff)
|
|
896
|
+
_arguments \
|
|
897
|
+
':prompt name:' \
|
|
898
|
+
':version 1:' \
|
|
899
|
+
':version 2:' \
|
|
900
|
+
'--json[Output as JSON]' \
|
|
901
|
+
'--no-color[Disable color output]'
|
|
902
|
+
;;
|
|
903
|
+
esac
|
|
904
|
+
;;
|
|
905
|
+
esac
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
_legion_dataset() {
|
|
909
|
+
local -a subcmds
|
|
910
|
+
subcmds=(
|
|
911
|
+
'list:List all datasets'
|
|
912
|
+
'show:Show dataset info and first 10 rows'
|
|
913
|
+
'import:Import a dataset from a file'
|
|
914
|
+
'export:Export a dataset to a file'
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
_arguments -C \
|
|
918
|
+
'--json[Output as JSON]' \
|
|
919
|
+
'--no-color[Disable color output]' \
|
|
920
|
+
'--help[Show help]' \
|
|
921
|
+
'1: :->cmd' \
|
|
922
|
+
'*:: :->args'
|
|
923
|
+
|
|
924
|
+
case $state in
|
|
925
|
+
cmd) _describe 'dataset command' subcmds ;;
|
|
926
|
+
args)
|
|
927
|
+
case $words[1] in
|
|
928
|
+
list)
|
|
929
|
+
_arguments \
|
|
930
|
+
'--json[Output as JSON]' \
|
|
931
|
+
'--no-color[Disable color output]'
|
|
932
|
+
;;
|
|
933
|
+
show)
|
|
934
|
+
_arguments \
|
|
935
|
+
':dataset name:' \
|
|
936
|
+
'--version[Specific version number]:version:' \
|
|
937
|
+
'--json[Output as JSON]' \
|
|
938
|
+
'--no-color[Disable color output]'
|
|
939
|
+
;;
|
|
940
|
+
import)
|
|
941
|
+
_arguments \
|
|
942
|
+
':dataset name:' \
|
|
943
|
+
':file path:_files' \
|
|
944
|
+
'--format[File format]:format:(json csv jsonl)' \
|
|
945
|
+
'--description[Dataset description]:desc:' \
|
|
946
|
+
'--json[Output as JSON]' \
|
|
947
|
+
'--no-color[Disable color output]'
|
|
948
|
+
;;
|
|
949
|
+
export)
|
|
950
|
+
_arguments \
|
|
951
|
+
':dataset name:' \
|
|
952
|
+
':output path:_files' \
|
|
953
|
+
'--format[File format]:format:(json csv jsonl)' \
|
|
954
|
+
'--version[Version to export]:version:' \
|
|
955
|
+
'--json[Output as JSON]' \
|
|
956
|
+
'--no-color[Disable color output]'
|
|
957
|
+
;;
|
|
958
|
+
esac
|
|
959
|
+
;;
|
|
960
|
+
esac
|
|
961
|
+
}
|
|
962
|
+
|
|
840
963
|
_legion "$@"
|
data/completions/legion.bash
CHANGED
|
@@ -26,7 +26,7 @@ _legion_complete() {
|
|
|
26
26
|
cword=$COMP_CWORD
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
local top_commands="chat commit pr review memory plan init tty ask version help"
|
|
29
|
+
local top_commands="chat commit pr review memory plan init tty ask prompt dataset version help"
|
|
30
30
|
local global_flags="--json --no-color --verbose --config-dir --help"
|
|
31
31
|
|
|
32
32
|
# Top-level command
|
|
@@ -264,6 +264,33 @@ _legion_complete() {
|
|
|
264
264
|
COMPREPLY=($(compgen -W "--json --no-color --help" -- "${cur}"))
|
|
265
265
|
;;
|
|
266
266
|
|
|
267
|
+
prompt)
|
|
268
|
+
if [[ $cword -eq 2 ]]; then
|
|
269
|
+
COMPREPLY=($(compgen -W "list show create tag diff" -- "${cur}"))
|
|
270
|
+
else
|
|
271
|
+
case "${words[2]}" in
|
|
272
|
+
list) COMPREPLY=($(compgen -W "--json --no-color --help" -- "${cur}")) ;;
|
|
273
|
+
show) COMPREPLY=($(compgen -W "--version --tag --json --no-color --help" -- "${cur}")) ;;
|
|
274
|
+
create) COMPREPLY=($(compgen -W "--template --description --model-params --json --no-color --help" -- "${cur}")) ;;
|
|
275
|
+
tag) COMPREPLY=($(compgen -W "--version --json --no-color --help" -- "${cur}")) ;;
|
|
276
|
+
diff) COMPREPLY=($(compgen -W "--json --no-color --help" -- "${cur}")) ;;
|
|
277
|
+
esac
|
|
278
|
+
fi
|
|
279
|
+
;;
|
|
280
|
+
|
|
281
|
+
dataset)
|
|
282
|
+
if [[ $cword -eq 2 ]]; then
|
|
283
|
+
COMPREPLY=($(compgen -W "list show import export" -- "${cur}"))
|
|
284
|
+
else
|
|
285
|
+
case "${words[2]}" in
|
|
286
|
+
list) COMPREPLY=($(compgen -W "--json --no-color --help" -- "${cur}")) ;;
|
|
287
|
+
show) COMPREPLY=($(compgen -W "--version --json --no-color --help" -- "${cur}")) ;;
|
|
288
|
+
import) COMPREPLY=($(compgen -W "--format --description --json --no-color --help" -- "${cur}")) ;;
|
|
289
|
+
export) COMPREPLY=($(compgen -W "--format --version --json --no-color --help" -- "${cur}")) ;;
|
|
290
|
+
esac
|
|
291
|
+
fi
|
|
292
|
+
;;
|
|
293
|
+
|
|
267
294
|
dream)
|
|
268
295
|
COMPREPLY=($(compgen -W "--wait --json --no-color --help" -- "${cur}"))
|
|
269
296
|
;;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module CLI
|
|
7
|
+
class Dataset < Thor
|
|
8
|
+
def self.exit_on_failure?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class_option :json, type: :boolean, default: false, desc: 'Output as JSON'
|
|
13
|
+
class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
|
|
14
|
+
class_option :verbose, type: :boolean, default: false, aliases: ['-V'], desc: 'Verbose logging'
|
|
15
|
+
class_option :config_dir, type: :string, desc: 'Config directory path'
|
|
16
|
+
|
|
17
|
+
desc 'list', 'List all datasets'
|
|
18
|
+
def list
|
|
19
|
+
out = formatter
|
|
20
|
+
with_dataset_client do |client|
|
|
21
|
+
datasets = client.list_datasets
|
|
22
|
+
if options[:json]
|
|
23
|
+
out.json(datasets)
|
|
24
|
+
elsif datasets.empty?
|
|
25
|
+
out.warn('No datasets found')
|
|
26
|
+
else
|
|
27
|
+
rows = datasets.map do |d|
|
|
28
|
+
[d[:name].to_s, (d[:description] || '').to_s,
|
|
29
|
+
(d[:latest_version] || '-').to_s, (d[:row_count] || 0).to_s]
|
|
30
|
+
end
|
|
31
|
+
out.table(%w[name description version row_count], rows)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
default_task :list
|
|
36
|
+
|
|
37
|
+
desc 'show NAME', 'Show dataset info and first 10 rows'
|
|
38
|
+
option :version, type: :numeric, desc: 'Specific version number'
|
|
39
|
+
def show(name)
|
|
40
|
+
out = formatter
|
|
41
|
+
with_dataset_client do |client|
|
|
42
|
+
kwargs = { name: name }
|
|
43
|
+
kwargs[:version] = options[:version] if options[:version]
|
|
44
|
+
result = client.get_dataset(**kwargs)
|
|
45
|
+
if result[:error]
|
|
46
|
+
out.error("Dataset '#{name}': #{result[:error]}")
|
|
47
|
+
raise SystemExit, 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if options[:json]
|
|
51
|
+
out.json(result)
|
|
52
|
+
else
|
|
53
|
+
out.header("Dataset: #{result[:name]}")
|
|
54
|
+
out.spacer
|
|
55
|
+
out.detail({ version: result[:version], row_count: result[:row_count] })
|
|
56
|
+
out.spacer
|
|
57
|
+
preview = (result[:rows] || []).first(10)
|
|
58
|
+
if preview.empty?
|
|
59
|
+
out.warn('No rows in this dataset version')
|
|
60
|
+
else
|
|
61
|
+
rows = preview.map do |r|
|
|
62
|
+
[r[:row_index].to_s, r[:input].to_s.slice(0, 60), (r[:expected_output] || '').to_s.slice(0, 60)]
|
|
63
|
+
end
|
|
64
|
+
out.table(%w[index input expected_output], rows)
|
|
65
|
+
remaining = result[:row_count].to_i - preview.size
|
|
66
|
+
out.warn("... #{remaining} more rows not shown") if remaining.positive?
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
desc 'import NAME PATH', 'Import a dataset from a file'
|
|
73
|
+
option :format, type: :string, default: 'json', enum: %w[json csv jsonl], desc: 'File format'
|
|
74
|
+
option :description, type: :string, desc: 'Dataset description'
|
|
75
|
+
def import(name, path)
|
|
76
|
+
out = formatter
|
|
77
|
+
with_dataset_client do |client|
|
|
78
|
+
unless File.exist?(path)
|
|
79
|
+
out.error("File not found: #{path}")
|
|
80
|
+
raise SystemExit, 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
result = client.import_dataset(
|
|
84
|
+
name: name,
|
|
85
|
+
path: path,
|
|
86
|
+
format: options[:format],
|
|
87
|
+
description: options[:description]
|
|
88
|
+
)
|
|
89
|
+
if options[:json]
|
|
90
|
+
out.json(result)
|
|
91
|
+
else
|
|
92
|
+
out.success("Imported '#{result[:name]}' v#{result[:version]} (#{result[:row_count]} rows)")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
desc 'export NAME PATH', 'Export a dataset to a file'
|
|
98
|
+
option :format, type: :string, default: 'json', enum: %w[json csv jsonl], desc: 'File format'
|
|
99
|
+
option :version, type: :numeric, desc: 'Version to export'
|
|
100
|
+
def export(name, path)
|
|
101
|
+
out = formatter
|
|
102
|
+
with_dataset_client do |client|
|
|
103
|
+
kwargs = { name: name, path: path, format: options[:format] }
|
|
104
|
+
kwargs[:version] = options[:version] if options[:version]
|
|
105
|
+
result = client.export_dataset(**kwargs)
|
|
106
|
+
if options[:json]
|
|
107
|
+
out.json(result)
|
|
108
|
+
else
|
|
109
|
+
out.success("Exported #{result[:row_count]} rows to #{result[:path]}")
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
no_commands do
|
|
115
|
+
def formatter
|
|
116
|
+
@formatter ||= Output::Formatter.new(
|
|
117
|
+
json: options[:json],
|
|
118
|
+
color: !options[:no_color]
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def with_dataset_client
|
|
123
|
+
Connection.config_dir = options[:config_dir] if options[:config_dir]
|
|
124
|
+
Connection.log_level = options[:verbose] ? 'debug' : 'error'
|
|
125
|
+
Connection.ensure_data
|
|
126
|
+
|
|
127
|
+
begin
|
|
128
|
+
require 'legion/extensions/dataset'
|
|
129
|
+
require 'legion/extensions/dataset/runners/dataset'
|
|
130
|
+
require 'legion/extensions/dataset/client'
|
|
131
|
+
rescue LoadError
|
|
132
|
+
formatter.error('lex-dataset gem is not installed (gem install lex-dataset)')
|
|
133
|
+
raise SystemExit, 1
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
db = Legion::Data.db
|
|
137
|
+
client = Legion::Extensions::Dataset::Client.new(db: db)
|
|
138
|
+
yield client
|
|
139
|
+
rescue CLI::Error => e
|
|
140
|
+
formatter.error(e.message)
|
|
141
|
+
raise SystemExit, 1
|
|
142
|
+
ensure
|
|
143
|
+
Connection.shutdown
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -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
|