legionio 1.7.37 → 1.8.0
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 +11 -0
- data/lib/legion/api/llm.rb +5 -4
- data/lib/legion/api/skills.rb +101 -0
- data/lib/legion/api.rb +2 -0
- data/lib/legion/chat/skills.rb +35 -96
- data/lib/legion/cli/skill_command.rb +59 -37
- data/lib/legion/extensions/builders/skills.rb +46 -0
- data/lib/legion/extensions/core.rb +7 -0
- data/lib/legion/extensions.rb +6 -0
- data/lib/legion/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bce17019cfaa062e933bb282732b8ac7261a52d94dbbe1ec9338b293cc11ed7f
|
|
4
|
+
data.tar.gz: a1740ed3e87db1177227e0644489b245096e5870e01c36069a7c7cb2db0efcef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4cf3b2db7dc3bfca5ce8deb357bc6491e10cd3eea398d80a48993ef8d982211f75d9afb4fa59a112f4087c4e6660a98050d636631a6802fa804ebcbe0c0e9f7
|
|
7
|
+
data.tar.gz: 5b66c3b1010022bcfca3ba5c76f758a6878572930cda2e8ccde1f90a934c312e30e58c48ea31f6c29e0468eaef60b0c1d994e79295e53243b5f2e21f05e2181b
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,17 @@
|
|
|
8
8
|
- Trigger word tool injection: extensions and runners declare trigger words that auto-promote deferred tools when detected in LLM messages
|
|
9
9
|
- `Legion::Tools::TriggerIndex` — Concurrent::Map-backed reverse index for O(1) trigger word lookup
|
|
10
10
|
- `trigger_words` DSL on Extensions::Core, runner modules, and Tools::Base
|
|
11
|
+
- also fixed the stupid thor rspec issue
|
|
12
|
+
|
|
13
|
+
## [1.8.0] - 2026-04-12
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- `Legion::Extensions::Builder::Skills` — parallel to `Builders::Runners`, discovers and registers `lex-skill-*` gems into `Legion::LLM::Skills::Registry` at boot
|
|
17
|
+
- `Legion::Extensions::Core` — `skills_required?` guard; extensions declaring this flag are skipped when legion-llm is not loaded
|
|
18
|
+
- `Legion::Chat::Skills` rewritten — delegates to `Legion::LLM::Skills::Registry` instead of YAML file discovery; `discover` returns an Array of skill objects
|
|
19
|
+
- `Legion::API::Skills` — REST endpoints: `GET /api/skills`, `GET /api/skills/:namespace/:name`, `POST /api/skills/invoke`, `DELETE /api/skills/active/:conversation_id`
|
|
20
|
+
- `Legion::CLI::SkillCommand` rewritten — delegates to daemon API instead of local YAML parsing; `list`, `show`, `run` subcommands
|
|
21
|
+
- `Legion::Extensions::Builder::Skills` wired into `Extensions::Core#autobuild` after `Builders::Runners`
|
|
11
22
|
|
|
12
23
|
## [1.7.36] - 2026-04-09
|
|
13
24
|
|
data/lib/legion/api/llm.rb
CHANGED
|
@@ -15,8 +15,8 @@ module Legion
|
|
|
15
15
|
Legion::LLM.started?
|
|
16
16
|
|
|
17
17
|
halt 503, { 'Content-Type' => 'application/json' },
|
|
18
|
-
Legion::JSON.
|
|
19
|
-
|
|
18
|
+
Legion::JSON.generate({ error: { code: 'llm_unavailable',
|
|
19
|
+
message: 'LLM subsystem is not available' } })
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
define_method(:cache_available?) do
|
|
@@ -241,7 +241,7 @@ module Legion
|
|
|
241
241
|
|
|
242
242
|
unless messages.is_a?(Array)
|
|
243
243
|
halt 400, { 'Content-Type' => 'application/json' },
|
|
244
|
-
Legion::JSON.
|
|
244
|
+
Legion::JSON.generate({ error: { code: 'invalid_messages', message: 'messages must be an array' } })
|
|
245
245
|
end
|
|
246
246
|
|
|
247
247
|
caller_identity = env['legion.tenant_id'] || 'api:inference'
|
|
@@ -287,6 +287,7 @@ module Legion
|
|
|
287
287
|
{ requested_by: { identity: caller_identity, type: :user, credential: :api } }
|
|
288
288
|
end
|
|
289
289
|
|
|
290
|
+
caller_metadata = body[:metadata].is_a?(Hash) ? body[:metadata] : {}
|
|
290
291
|
req = Legion::LLM::Pipeline::Request.build(
|
|
291
292
|
messages: messages,
|
|
292
293
|
system: body[:system],
|
|
@@ -294,7 +295,7 @@ module Legion
|
|
|
294
295
|
tools: tool_classes,
|
|
295
296
|
caller: caller_ctx,
|
|
296
297
|
conversation_id: body[:conversation_id],
|
|
297
|
-
metadata:
|
|
298
|
+
metadata: caller_metadata.merge(requested_tools: requested_tools),
|
|
298
299
|
stream: streaming,
|
|
299
300
|
cache: { strategy: :default, cacheable: true }
|
|
300
301
|
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
class API < Sinatra::Base
|
|
7
|
+
module Routes
|
|
8
|
+
module Skills
|
|
9
|
+
def self.registered(app)
|
|
10
|
+
app.helpers do
|
|
11
|
+
define_method(:skills_registry_available?) do
|
|
12
|
+
defined?(Legion::LLM::Skills::Registry)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
define_method(:skill_descriptor) do |skill|
|
|
16
|
+
{
|
|
17
|
+
name: skill.skill_name,
|
|
18
|
+
namespace: skill.namespace,
|
|
19
|
+
description: skill.description,
|
|
20
|
+
trigger: skill.trigger,
|
|
21
|
+
follows: skill.follows_skill
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
register_list(app)
|
|
27
|
+
register_show(app)
|
|
28
|
+
register_invoke(app)
|
|
29
|
+
register_cancel(app)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.register_list(app)
|
|
33
|
+
app.get '/api/skills' do
|
|
34
|
+
return json_error('skills_unavailable', 'Skills unavailable', status_code: 503) unless skills_registry_available?
|
|
35
|
+
|
|
36
|
+
skills = Legion::LLM::Skills::Registry.all.map { |s| skill_descriptor(s) }
|
|
37
|
+
json_response(skills)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.register_show(app)
|
|
42
|
+
app.get '/api/skills/:namespace/:name' do
|
|
43
|
+
return json_error('skills_unavailable', 'Skills unavailable', status_code: 503) unless skills_registry_available?
|
|
44
|
+
|
|
45
|
+
key = "#{params[:namespace]}:#{params[:name]}"
|
|
46
|
+
skill = Legion::LLM::Skills::Registry.find(key)
|
|
47
|
+
return json_error('not_found', "Skill #{key} not found", status_code: 404) unless skill
|
|
48
|
+
|
|
49
|
+
json_response(skill_descriptor(skill).merge(steps: skill.steps))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.register_invoke(app)
|
|
54
|
+
app.post '/api/skills/invoke' do
|
|
55
|
+
return json_error('skills_unavailable', 'Skills unavailable', status_code: 503) unless skills_registry_available?
|
|
56
|
+
|
|
57
|
+
body = parse_request_body
|
|
58
|
+
skill_name = body[:skill_name]
|
|
59
|
+
return json_error('unprocessable', 'skill_name required', status_code: 422) if skill_name.nil? || skill_name.empty?
|
|
60
|
+
|
|
61
|
+
skill_class = Legion::LLM::Skills::Registry.find(skill_name)
|
|
62
|
+
return json_error('not_found', "Skill #{skill_name} not found", status_code: 404) unless skill_class
|
|
63
|
+
|
|
64
|
+
conv_id = body[:conversation_id] || "conv_#{SecureRandom.hex(8)}"
|
|
65
|
+
begin
|
|
66
|
+
Legion::LLM::ConversationStore.set_skill_state(conv_id, skill_key: skill_name, resume_at: 0)
|
|
67
|
+
req = Legion::LLM::Pipeline::Request.build(
|
|
68
|
+
messages: [{ role: :user, content: body[:initial_message] || 'start skill' }],
|
|
69
|
+
conversation_id: conv_id,
|
|
70
|
+
metadata: (body[:metadata].is_a?(Hash) ? body[:metadata] : {}).merge(skill_invoke: true),
|
|
71
|
+
stream: false
|
|
72
|
+
)
|
|
73
|
+
result = Legion::LLM::Pipeline::Executor.new(req).call
|
|
74
|
+
json_response({ conversation_id: conv_id, content: result.message[:content],
|
|
75
|
+
skill_name: skill_name })
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
Legion::LLM::ConversationStore.clear_skill_state(conv_id)
|
|
78
|
+
json_error('internal_error', e.message, status_code: 500)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.register_cancel(app)
|
|
84
|
+
app.delete '/api/skills/active/:conversation_id' do
|
|
85
|
+
conv_id = params[:conversation_id]
|
|
86
|
+
if defined?(Legion::LLM::ConversationStore)
|
|
87
|
+
state = Legion::LLM::ConversationStore.cancel_skill!(conv_id)
|
|
88
|
+
if state && defined?(Legion::Events)
|
|
89
|
+
Legion::Events.emit('skill.cancelled', conversation_id: conv_id,
|
|
90
|
+
skill_name: state[:skill_key])
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
status 204
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private_class_method :register_list, :register_show, :register_invoke, :register_cancel
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
data/lib/legion/api.rb
CHANGED
|
@@ -35,6 +35,7 @@ require_relative 'api/capacity'
|
|
|
35
35
|
require_relative 'api/audit'
|
|
36
36
|
require_relative 'api/metrics'
|
|
37
37
|
require_relative 'api/llm'
|
|
38
|
+
require_relative 'api/skills'
|
|
38
39
|
require_relative 'api/catalog'
|
|
39
40
|
require_relative 'api/org_chart'
|
|
40
41
|
require_relative 'api/workflow'
|
|
@@ -197,6 +198,7 @@ module Legion
|
|
|
197
198
|
register Routes::Audit
|
|
198
199
|
register Routes::Metrics
|
|
199
200
|
mount_library_routes('llm', Routes::Llm, 'Legion::LLM::Routes')
|
|
201
|
+
register Routes::Skills
|
|
200
202
|
register Routes::ExtensionCatalog
|
|
201
203
|
register Routes::OrgChart
|
|
202
204
|
register Routes::Governance
|
data/lib/legion/chat/skills.rb
CHANGED
|
@@ -1,123 +1,62 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'yaml'
|
|
4
|
-
|
|
5
3
|
module Legion
|
|
6
4
|
module Chat
|
|
7
5
|
module Skills
|
|
8
|
-
SKILL_DIRS = ['.legion/skills', '~/.legionio/skills'].freeze
|
|
9
|
-
|
|
10
6
|
class << self
|
|
11
7
|
def discover
|
|
12
|
-
|
|
13
|
-
expanded = File.expand_path(dir)
|
|
14
|
-
next [] unless Dir.exist?(expanded)
|
|
8
|
+
return file_discover unless llm_skills_available?
|
|
15
9
|
|
|
16
|
-
|
|
17
|
-
rb_skills = Dir.glob(File.join(expanded, '*.rb')).filter_map { |f| parse_rb(f) }
|
|
18
|
-
md_skills + rb_skills
|
|
19
|
-
end
|
|
10
|
+
Legion::LLM::Skills::Registry.all.map { |s| registry_descriptor(s) }
|
|
20
11
|
end
|
|
21
12
|
|
|
22
13
|
def find(name)
|
|
23
|
-
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def parse(path)
|
|
27
|
-
content = File.read(path)
|
|
28
|
-
return nil unless content.start_with?('---')
|
|
29
|
-
|
|
30
|
-
parts = content.split(/^---\s*$/, 3)
|
|
31
|
-
return nil if parts.size < 3
|
|
14
|
+
return file_find(name) unless llm_skills_available?
|
|
32
15
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
{
|
|
37
|
-
name: frontmatter['name'] || File.basename(path, '.md'),
|
|
38
|
-
description: frontmatter['description'] || '',
|
|
39
|
-
type: :prompt,
|
|
40
|
-
model: frontmatter['model'],
|
|
41
|
-
tools: Array(frontmatter['tools']),
|
|
42
|
-
prompt: body,
|
|
43
|
-
path: path
|
|
44
|
-
}
|
|
45
|
-
rescue StandardError => e
|
|
46
|
-
Legion::Logging.warn "Skill parse error #{path}: #{e.message}" if defined?(Legion::Logging)
|
|
47
|
-
nil
|
|
16
|
+
skill = Legion::LLM::Skills::Registry.find(name)
|
|
17
|
+
skill ? registry_descriptor(skill) : nil
|
|
48
18
|
end
|
|
49
19
|
|
|
50
|
-
|
|
51
|
-
|
|
20
|
+
# execute: REMOVED — all skill execution routes through the daemon API.
|
|
21
|
+
# `legion skill run` / `legion chat` are thin HTTP clients; no local LLM boot.
|
|
52
22
|
|
|
53
|
-
|
|
54
|
-
description = content.match(/^\s*#\s*description:\s*(.+)$/i)&.captures&.first || ''
|
|
55
|
-
model = content.match(/^\s*#\s*model:\s*(.+)$/i)&.captures&.first
|
|
23
|
+
private
|
|
56
24
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
model: model&.strip,
|
|
62
|
-
tools: [],
|
|
63
|
-
prompt: nil,
|
|
64
|
-
path: path
|
|
65
|
-
}
|
|
66
|
-
rescue StandardError => e
|
|
67
|
-
Legion::Logging.warn "Skill parse_rb error #{path}: #{e.message}" if defined?(Legion::Logging)
|
|
68
|
-
nil
|
|
25
|
+
def llm_skills_available?
|
|
26
|
+
defined?(Legion::LLM::Skills) &&
|
|
27
|
+
Legion::LLM.respond_to?(:started?) &&
|
|
28
|
+
Legion::LLM.started?
|
|
69
29
|
end
|
|
70
30
|
|
|
71
|
-
def
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
execute_rb(skill, input: input)
|
|
75
|
-
when :prompt
|
|
76
|
-
execute_prompt(skill, input: input)
|
|
77
|
-
else
|
|
78
|
-
{ success: false, error: "unknown skill type: #{skill[:type]}" }
|
|
79
|
-
end
|
|
31
|
+
def registry_descriptor(skill)
|
|
32
|
+
{ name: skill.skill_name, namespace: skill.namespace, prompt: nil,
|
|
33
|
+
description: skill.description, source: :registry }
|
|
80
34
|
end
|
|
81
35
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
prompt = skill[:prompt]
|
|
88
|
-
prompt = "#{prompt}\n\nUser input: #{input}" if input
|
|
89
|
-
|
|
90
|
-
session = Legion::LLM.chat_direct(model: skill[:model], provider: nil)
|
|
91
|
-
response = session.ask(prompt)
|
|
92
|
-
content = response.respond_to?(:content) ? response.content : response.to_s
|
|
93
|
-
|
|
94
|
-
{ success: true, output: content }
|
|
95
|
-
rescue StandardError => e
|
|
96
|
-
{ success: false, error: e.message }
|
|
36
|
+
def file_discover
|
|
37
|
+
dirs = skill_directories
|
|
38
|
+
dirs.flat_map { |dir| ::Dir.glob(::File.join(dir, '*.{md,rb,yml,yaml}')) }
|
|
39
|
+
.map { |f| { name: ::File.basename(f, '.*'), path: f, source: :file } }
|
|
97
40
|
end
|
|
98
41
|
|
|
99
|
-
def
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
allowed = SKILL_DIRS.filter_map do |dir|
|
|
106
|
-
expanded = File.expand_path(dir)
|
|
107
|
-
File.realpath(expanded) if Dir.exist?(expanded)
|
|
108
|
-
end
|
|
109
|
-
unless allowed.any? { |dir| real_path.start_with?("#{dir}/") }
|
|
110
|
-
return { success: false, error: "skill path outside allowed directories: #{real_path}" }
|
|
111
|
-
end
|
|
42
|
+
def file_find(name)
|
|
43
|
+
dirs = skill_directories
|
|
44
|
+
dirs.each do |dir|
|
|
45
|
+
%w[.md .rb .yml .yaml].each do |ext|
|
|
46
|
+
path = ::File.join(dir, "#{name}#{ext}")
|
|
47
|
+
next unless ::File.exist?(path)
|
|
112
48
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
49
|
+
return { name: name, path: path, prompt: ::File.read(path), source: :file }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
116
54
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
55
|
+
def skill_directories
|
|
56
|
+
[
|
|
57
|
+
::File.expand_path('.legion/skills'),
|
|
58
|
+
::File.expand_path('~/.legionio/skills')
|
|
59
|
+
].select { |d| ::File.directory?(d) }
|
|
121
60
|
end
|
|
122
61
|
end
|
|
123
62
|
end
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'thor'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'uri'
|
|
4
7
|
|
|
5
8
|
module Legion
|
|
6
9
|
module CLI
|
|
@@ -9,45 +12,50 @@ module Legion
|
|
|
9
12
|
true
|
|
10
13
|
end
|
|
11
14
|
|
|
12
|
-
desc 'list', 'List all
|
|
15
|
+
desc 'list', 'List all registered skills'
|
|
13
16
|
def list
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
response = daemon_get('/api/skills')
|
|
18
|
+
unless response.is_a?(::Net::HTTPSuccess)
|
|
19
|
+
say "Error fetching skills: #{response.code}", :red
|
|
20
|
+
exit 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
skills = ::JSON.parse(response.body, symbolize_names: true)[:data] || []
|
|
16
24
|
if skills.empty?
|
|
17
|
-
say 'No skills
|
|
25
|
+
say 'No skills registered. Start the daemon with legion-llm loaded.'
|
|
18
26
|
return
|
|
19
27
|
end
|
|
20
28
|
|
|
21
29
|
skills.each do |s|
|
|
22
|
-
|
|
23
|
-
say " /#{s[:name]} #{type_label} — #{s[:description]}", :green
|
|
24
|
-
say " model: #{s[:model] || 'default'}, tools: #{s[:tools].empty? ? 'none' : s[:tools].join(', ')}"
|
|
30
|
+
say " #{s[:namespace]}:#{s[:name]} [#{s[:trigger]}] #{s[:description]}", :green
|
|
25
31
|
end
|
|
26
32
|
end
|
|
27
33
|
|
|
28
|
-
desc 'show NAME', '
|
|
34
|
+
desc 'show NAMESPACE:NAME', 'Show skill details'
|
|
29
35
|
def show(name)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
say "Name: #{skill[:name]}", :green
|
|
34
|
-
say "Description: #{skill[:description]}"
|
|
35
|
-
say "Model: #{skill[:model] || 'default'}"
|
|
36
|
-
say "Tools: #{skill[:tools].empty? ? 'none' : skill[:tools].join(', ')}"
|
|
37
|
-
say "Path: #{skill[:path]}"
|
|
38
|
-
say "\n--- Prompt ---\n#{skill[:prompt]}"
|
|
39
|
-
else
|
|
36
|
+
ns, nm = name.include?(':') ? name.split(':', 2) : ['default', name]
|
|
37
|
+
response = daemon_get("/api/skills/#{ns}/#{nm}")
|
|
38
|
+
unless response.is_a?(::Net::HTTPSuccess)
|
|
40
39
|
say "Skill '#{name}' not found", :red
|
|
40
|
+
exit 1
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
result = ::JSON.parse(response.body, symbolize_names: true)
|
|
44
|
+
data = result[:data] || {}
|
|
45
|
+
say "Name: #{data[:namespace]}:#{data[:name]}", :green
|
|
46
|
+
say "Description: #{data[:description]}"
|
|
47
|
+
say "Trigger: #{data[:trigger]}"
|
|
48
|
+
say "Steps: #{Array(data[:steps]).join(', ')}"
|
|
42
49
|
end
|
|
43
50
|
|
|
44
51
|
desc 'create NAME', 'Scaffold a new skill file'
|
|
45
52
|
def create(name)
|
|
53
|
+
require 'fileutils'
|
|
46
54
|
dir = '.legion/skills'
|
|
47
55
|
FileUtils.mkdir_p(dir)
|
|
48
|
-
path = File.join(dir, "#{name}.md")
|
|
56
|
+
path = ::File.join(dir, "#{name}.md")
|
|
49
57
|
|
|
50
|
-
if File.exist?(path)
|
|
58
|
+
if ::File.exist?(path)
|
|
51
59
|
say "Skill already exists: #{path}", :red
|
|
52
60
|
return
|
|
53
61
|
end
|
|
@@ -55,35 +63,49 @@ module Legion
|
|
|
55
63
|
content = <<~SKILL
|
|
56
64
|
---
|
|
57
65
|
name: #{name}
|
|
66
|
+
namespace: local
|
|
58
67
|
description: Describe what this skill does
|
|
59
|
-
|
|
60
|
-
tools: []
|
|
68
|
+
trigger: on_demand
|
|
61
69
|
---
|
|
62
70
|
|
|
63
71
|
You are a helpful assistant. Describe the skill's behavior here.
|
|
64
72
|
SKILL
|
|
65
73
|
|
|
66
|
-
File.write(path, content)
|
|
74
|
+
::File.write(path, content)
|
|
67
75
|
say "Created: #{path}", :green
|
|
68
76
|
end
|
|
69
77
|
|
|
70
|
-
desc '
|
|
71
|
-
map 'run' => :
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
unless skill
|
|
76
|
-
say "Skill '#{name}' not found", :red
|
|
77
|
-
return
|
|
78
|
-
end
|
|
78
|
+
desc 'run NAME', 'Run a skill via the daemon'
|
|
79
|
+
map 'run' => :run_skill
|
|
80
|
+
def run_skill(name)
|
|
81
|
+
url = "#{daemon_base_url}/api/skills/invoke"
|
|
82
|
+
payload = { skill_name: name }.to_json
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
response = ::Net::HTTP.post(
|
|
85
|
+
::URI.parse(url),
|
|
86
|
+
payload,
|
|
87
|
+
'Content-Type' => 'application/json'
|
|
88
|
+
)
|
|
82
89
|
|
|
83
|
-
if
|
|
84
|
-
|
|
90
|
+
if response.is_a?(::Net::HTTPSuccess)
|
|
91
|
+
result = ::JSON.parse(response.body, symbolize_names: true)
|
|
92
|
+
say result.dig(:data, :content).to_s
|
|
85
93
|
else
|
|
86
|
-
say "
|
|
94
|
+
say "Error: #{response.code} #{response.body}", :red
|
|
95
|
+
exit 1
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
no_commands do
|
|
100
|
+
def daemon_base_url
|
|
101
|
+
host = Legion::Settings.dig(:api, :host) || 'localhost'
|
|
102
|
+
port = Legion::Settings.dig(:api, :port) || 4567
|
|
103
|
+
"http://#{host}:#{port}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def daemon_get(path)
|
|
107
|
+
uri = ::URI.parse("#{daemon_base_url}#{path}")
|
|
108
|
+
::Net::HTTP.get_response(uri)
|
|
87
109
|
end
|
|
88
110
|
end
|
|
89
111
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Builder
|
|
8
|
+
module Skills
|
|
9
|
+
include Legion::Extensions::Builder::Base
|
|
10
|
+
|
|
11
|
+
attr_reader :skills
|
|
12
|
+
|
|
13
|
+
def build_skills
|
|
14
|
+
return unless Object.const_defined?('Legion::LLM::Skills', false)
|
|
15
|
+
return unless Object.const_defined?('Legion::LLM', false) &&
|
|
16
|
+
Legion::LLM.respond_to?(:started?) && Legion::LLM.started?
|
|
17
|
+
return if Legion::LLM.settings.dig(:skills, :enabled) == false
|
|
18
|
+
|
|
19
|
+
@skills = {}
|
|
20
|
+
lex_mod = lex_class.is_a?(::Module) ? lex_class : ::Kernel.const_get(lex_class.to_s)
|
|
21
|
+
lex_mod.const_set(:Skills, ::Module.new) unless lex_mod.const_defined?(:Skills, false)
|
|
22
|
+
require_files(skill_files)
|
|
23
|
+
build_skill_list
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build_skill_list
|
|
27
|
+
skill_files.each do |file|
|
|
28
|
+
skill_name = file.split('/').last.sub('.rb', '')
|
|
29
|
+
skill_class_name = "#{lex_class}::Skills::#{skill_name.split('_').collect(&:capitalize).join}"
|
|
30
|
+
loaded_skill = Kernel.const_get(skill_class_name)
|
|
31
|
+
Legion::LLM::Skills::Registry.register(loaded_skill)
|
|
32
|
+
@skills[skill_name.to_sym] = {
|
|
33
|
+
skill_class: skill_class_name,
|
|
34
|
+
skill_module: loaded_skill
|
|
35
|
+
}
|
|
36
|
+
Legion::Logging.debug "[Skills] registered: #{skill_class_name}" if defined?(Legion::Logging)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def skill_files
|
|
41
|
+
@skill_files ||= find_files('skills')
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -7,6 +7,7 @@ require_relative 'builders/helpers'
|
|
|
7
7
|
require_relative 'builders/hooks'
|
|
8
8
|
require_relative 'builders/routes'
|
|
9
9
|
require_relative 'builders/runners'
|
|
10
|
+
require_relative 'builders/skills'
|
|
10
11
|
|
|
11
12
|
require_relative 'helpers/segments'
|
|
12
13
|
require_relative 'helpers/core'
|
|
@@ -57,6 +58,7 @@ module Legion
|
|
|
57
58
|
include Legion::Extensions::Builder::Actors
|
|
58
59
|
include Legion::Extensions::Builder::Hooks
|
|
59
60
|
include Legion::Extensions::Builder::Routes
|
|
61
|
+
include Legion::Extensions::Builder::Skills
|
|
60
62
|
|
|
61
63
|
def autobuild
|
|
62
64
|
Legion::Logging.debug "[Core] autobuild start: #{name}" if defined?(Legion::Logging)
|
|
@@ -81,6 +83,7 @@ module Legion
|
|
|
81
83
|
build_actors
|
|
82
84
|
build_hooks
|
|
83
85
|
build_routes
|
|
86
|
+
build_skills if skills_required?
|
|
84
87
|
Legion::Logging.debug "[Core] autobuild complete: #{name}" if defined?(Legion::Logging)
|
|
85
88
|
end
|
|
86
89
|
|
|
@@ -108,6 +111,10 @@ module Legion
|
|
|
108
111
|
false
|
|
109
112
|
end
|
|
110
113
|
|
|
114
|
+
def skills_required?
|
|
115
|
+
false
|
|
116
|
+
end
|
|
117
|
+
|
|
111
118
|
def remote_invocable?
|
|
112
119
|
true
|
|
113
120
|
end
|
data/lib/legion/extensions.rb
CHANGED
|
@@ -266,6 +266,12 @@ module Legion
|
|
|
266
266
|
return false
|
|
267
267
|
end
|
|
268
268
|
|
|
269
|
+
if extension.respond_to?(:skills_required?) && extension.skills_required? &&
|
|
270
|
+
!Object.const_defined?('Legion::LLM::Skills', false)
|
|
271
|
+
Legion::Logging.warn "#{ext_name} requires Legion::LLM::Skills but isn't loaded, skipping"
|
|
272
|
+
return false
|
|
273
|
+
end
|
|
274
|
+
|
|
269
275
|
has_logger = extension.respond_to?(:log)
|
|
270
276
|
extension.autobuild
|
|
271
277
|
|
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
|
+
version: 1.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -516,6 +516,7 @@ files:
|
|
|
516
516
|
- lib/legion/api/router.rb
|
|
517
517
|
- lib/legion/api/schedules.rb
|
|
518
518
|
- lib/legion/api/settings.rb
|
|
519
|
+
- lib/legion/api/skills.rb
|
|
519
520
|
- lib/legion/api/stats.rb
|
|
520
521
|
- lib/legion/api/sync_dispatch.rb
|
|
521
522
|
- lib/legion/api/tasks.rb
|
|
@@ -817,6 +818,7 @@ files:
|
|
|
817
818
|
- lib/legion/extensions/builders/hooks.rb
|
|
818
819
|
- lib/legion/extensions/builders/routes.rb
|
|
819
820
|
- lib/legion/extensions/builders/runners.rb
|
|
821
|
+
- lib/legion/extensions/builders/skills.rb
|
|
820
822
|
- lib/legion/extensions/capability.rb
|
|
821
823
|
- lib/legion/extensions/catalog.rb
|
|
822
824
|
- lib/legion/extensions/catalog/available.rb
|