ruby_llm-skills 0.2.0 → 0.3.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 +22 -0
- data/README.md +51 -0
- data/lib/generators/skill/USAGE +29 -0
- data/lib/generators/skill/skill_generator.rb +14 -5
- data/lib/generators/skill/templates/SKILL.md.tt +43 -7
- data/lib/ruby_llm/skills/agent_extensions.rb +148 -0
- data/lib/ruby_llm/skills/chat_extensions.rb +19 -8
- data/lib/ruby_llm/skills/composite_loader.rb +1 -1
- data/lib/ruby_llm/skills/database_loader.rb +1 -1
- data/lib/ruby_llm/skills/error.rb +1 -1
- data/lib/ruby_llm/skills/filesystem_loader.rb +1 -1
- data/lib/ruby_llm/skills/loader.rb +1 -1
- data/lib/ruby_llm/skills/parser.rb +1 -1
- data/lib/ruby_llm/skills/railtie.rb +6 -6
- data/lib/ruby_llm/skills/skill.rb +1 -1
- data/lib/ruby_llm/skills/skill_tool.rb +3 -3
- data/lib/ruby_llm/skills/validator.rb +1 -1
- data/lib/ruby_llm/skills/version.rb +2 -2
- data/lib/ruby_llm/skills.rb +55 -8
- metadata +8 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '048ac6f6a76a7fd48994d208799b967754b25ecc3959acd56d44f5ab707f9f8d'
|
|
4
|
+
data.tar.gz: 268827da6d73359d9451b159b1abe68dea7881419df6b473cceeb2d8c74bc073
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: febe501ab666f7e1cdf06e043ab44305ea065ffe561508b6fdcb9a959a90eabad8d2cb9fcc90384536eaf0871f73f2e52c3c9e93b8d883fbe8823274f547f0f3
|
|
7
|
+
data.tar.gz: 73226c3f1eb42ba6073dbfdcccb9f334896e078ca04ab1814260feb4218d432a83cf92817a32bb6c1abe847cf7fcd0098259aa2bab5886ce933d2c258d4ae8f1
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-02-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `RubyLLM::Agent` integration via `RubyLLM::Skills::AgentExtensions`
|
|
13
|
+
- Class-level `skills` DSL on agent subclasses with source, `only:`, and proc support
|
|
14
|
+
- Instance-level `with_skills` for runtime agent skill configuration
|
|
15
|
+
- Agent-specific unit and integration test coverage
|
|
16
|
+
- Compatibility tests for delegate fallback behavior
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Tightened `ruby_llm` dependency from open lower bound to `~> 1.12`
|
|
21
|
+
- Added explicit runtime compatibility checks for required `RubyLLM::Agent` hooks
|
|
22
|
+
- Documented `agent.with_skills(...)` replacement semantics in README
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Dynamic skill blocks resolving to `nil` or `[]` no longer silently load default skills
|
|
27
|
+
- Skill source normalization and validation now prevent nested/invalid source runtime crashes
|
|
28
|
+
- Delegate fallback now supports common delegation options (`prefix`, `allow_nil`, `private`)
|
|
29
|
+
|
|
8
30
|
## [0.1.0] - 2025-01-15
|
|
9
31
|
|
|
10
32
|
### Added
|
data/README.md
CHANGED
|
@@ -31,6 +31,26 @@ chat.with_skills("app/skills", "app/commands") # multiple paths
|
|
|
31
31
|
chat.with_skills("app/skills", user.skills) # with database records
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
### With RubyLLM::Agent (v1.12+)
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
class SupportAgent < RubyLLM::Agent
|
|
38
|
+
model "gpt-5-nano"
|
|
39
|
+
instructions "You are a support assistant."
|
|
40
|
+
skills "app/skills", only: [:faq, :troubleshooting]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
chat = SupportAgent.chat
|
|
44
|
+
chat.ask("How do I reset my password?")
|
|
45
|
+
|
|
46
|
+
agent = SupportAgent.new
|
|
47
|
+
agent.with_skills("extra/skills")
|
|
48
|
+
agent.ask("What can you help with?")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`agent.with_skills(...)` replaces the current skill tool configuration.
|
|
52
|
+
To combine sources, pass all sources in a single `skills`/`with_skills` call.
|
|
53
|
+
|
|
34
54
|
## Creating Skills
|
|
35
55
|
|
|
36
56
|
```
|
|
@@ -96,6 +116,37 @@ Default path auto-configured to `Rails.root/app/skills`.
|
|
|
96
116
|
rails generate skill pdf-report --description "Generate PDF reports"
|
|
97
117
|
```
|
|
98
118
|
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
### Setup
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
bin/setup
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Running Tests
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
bundle exec rake test # Unit tests (151 tests)
|
|
131
|
+
bundle exec rake test_rails # Rails integration tests (25+ tests)
|
|
132
|
+
bundle exec rake test_all # Both
|
|
133
|
+
bundle exec rake # Tests + linting
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Dummy Rails App
|
|
137
|
+
|
|
138
|
+
A minimal Rails 8 app at `test/dummy/` tests Rails integration:
|
|
139
|
+
|
|
140
|
+
- **Filesystem skills**: `app/skills/greeting/` tests directory-based loading
|
|
141
|
+
- **Database skills**: `Skill` model tests ActiveRecord-based loading
|
|
142
|
+
- **Generator tests**: Tests for `rails generate skill`
|
|
143
|
+
- **Composite loading**: Tests combining filesystem + database sources
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
cd test/dummy
|
|
147
|
+
bundle exec rails test # Run Rails tests directly
|
|
148
|
+
```
|
|
149
|
+
|
|
99
150
|
## Resources
|
|
100
151
|
|
|
101
152
|
- [Agent Skills Specification](https://agentskills.io/specification)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Creates a new Agent Skill following the agentskills.io specification.
|
|
3
|
+
Skills are stored in app/skills/ and contain a SKILL.md file with
|
|
4
|
+
YAML frontmatter (name, description) and markdown instructions.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
rails generate skill pdf-report --description "Generate PDF reports"
|
|
8
|
+
|
|
9
|
+
This creates:
|
|
10
|
+
app/skills/pdf-report/
|
|
11
|
+
app/skills/pdf-report/SKILL.md
|
|
12
|
+
|
|
13
|
+
With optional directories:
|
|
14
|
+
--scripts Creates scripts/ directory for executable code
|
|
15
|
+
--references Creates references/ directory for documentation
|
|
16
|
+
--assets Creates assets/ directory for templates/images
|
|
17
|
+
|
|
18
|
+
Full Example:
|
|
19
|
+
rails generate skill data-export -d "Export data to CSV/JSON. Use when asked to export or download data." --scripts --assets
|
|
20
|
+
|
|
21
|
+
This creates:
|
|
22
|
+
app/skills/data-export/
|
|
23
|
+
app/skills/data-export/SKILL.md
|
|
24
|
+
app/skills/data-export/scripts/.keep
|
|
25
|
+
app/skills/data-export/assets/.keep
|
|
26
|
+
|
|
27
|
+
Note:
|
|
28
|
+
The description should include both what the skill does AND when to use it.
|
|
29
|
+
Example: "Generate PDF reports from data. Use when asked to create reports or export to PDF."
|
|
@@ -5,11 +5,16 @@ require "rails/generators"
|
|
|
5
5
|
class SkillGenerator < Rails::Generators::NamedBase
|
|
6
6
|
source_root File.expand_path("templates", __dir__)
|
|
7
7
|
|
|
8
|
-
class_option :description, type: :string, default: "Description of what this skill does"
|
|
9
|
-
|
|
10
|
-
class_option :
|
|
11
|
-
|
|
12
|
-
class_option :
|
|
8
|
+
class_option :description, type: :string, default: "Description of what this skill does. Use when...",
|
|
9
|
+
aliases: "-d", desc: "Short description of the skill (max 1024 chars)"
|
|
10
|
+
class_option :license, type: :string, default: nil,
|
|
11
|
+
aliases: "-l", desc: "License identifier (e.g., MIT, Apache-2.0)"
|
|
12
|
+
class_option :scripts, type: :boolean, default: false,
|
|
13
|
+
desc: "Create scripts/ directory for executable code"
|
|
14
|
+
class_option :references, type: :boolean, default: false,
|
|
15
|
+
desc: "Create references/ directory for documentation"
|
|
16
|
+
class_option :assets, type: :boolean, default: false,
|
|
17
|
+
desc: "Create assets/ directory for templates/images"
|
|
13
18
|
|
|
14
19
|
def create_skill_directory
|
|
15
20
|
empty_directory skill_path
|
|
@@ -57,4 +62,8 @@ class SkillGenerator < Rails::Generators::NamedBase
|
|
|
57
62
|
def skill_license
|
|
58
63
|
options[:license]
|
|
59
64
|
end
|
|
65
|
+
|
|
66
|
+
def skill_title
|
|
67
|
+
skill_name.split("-").map(&:capitalize).join(" ")
|
|
68
|
+
end
|
|
60
69
|
end
|
|
@@ -6,16 +6,52 @@ license: <%= skill_license %>
|
|
|
6
6
|
<% end -%>
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
# <%=
|
|
9
|
+
# <%= skill_title %>
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
<%= skill_description %>
|
|
12
12
|
|
|
13
13
|
## When to Use
|
|
14
14
|
|
|
15
|
-
Use this skill when
|
|
15
|
+
Use this skill when:
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
- [Trigger condition 1 - be specific about keywords/phrases]
|
|
18
|
+
- [Trigger condition 2]
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
## Instructions
|
|
21
|
+
|
|
22
|
+
### Step 1: [Action]
|
|
23
|
+
|
|
24
|
+
[Clear instruction with expected outcome]
|
|
25
|
+
|
|
26
|
+
### Step 2: [Action]
|
|
27
|
+
|
|
28
|
+
[Clear instruction with expected outcome]
|
|
29
|
+
|
|
30
|
+
### Step 3: [Action]
|
|
31
|
+
|
|
32
|
+
[Clear instruction with expected outcome]
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
### Example 1: [Scenario]
|
|
37
|
+
|
|
38
|
+
**Input:**
|
|
39
|
+
```
|
|
40
|
+
[Example input or command]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Output:**
|
|
44
|
+
```
|
|
45
|
+
[Expected result]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Edge Cases
|
|
49
|
+
|
|
50
|
+
- **[Scenario]**: [How to handle it]
|
|
51
|
+
- **[Error condition]**: [Recovery steps]
|
|
52
|
+
|
|
53
|
+
## Notes
|
|
54
|
+
|
|
55
|
+
- Keep this file under 500 lines for optimal token usage
|
|
56
|
+
- Move detailed documentation to `references/` directory
|
|
57
|
+
- Move executable code to `scripts/` directory
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Skills
|
|
5
|
+
# Extensions for RubyLLM::Agent to enable declarative skill configuration.
|
|
6
|
+
#
|
|
7
|
+
# @example Static skills
|
|
8
|
+
# class SupportAgent < RubyLLM::Agent
|
|
9
|
+
# skills "app/skills", only: [:faq]
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# @example Dynamic skills
|
|
13
|
+
# class WorkspaceAgent < RubyLLM::Agent
|
|
14
|
+
# inputs :workspace
|
|
15
|
+
# skills { [workspace.skill_collection] }
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
module AgentExtensions
|
|
19
|
+
REQUIRED_AGENT_SINGLETON_METHODS = %i[apply_configuration runtime_context llm_chat_for].freeze
|
|
20
|
+
|
|
21
|
+
module ClassMethods
|
|
22
|
+
def self.extended(base)
|
|
23
|
+
base.instance_variable_set(:@skill_sources, nil)
|
|
24
|
+
base.instance_variable_set(:@skill_only, nil)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def inherited(subclass)
|
|
28
|
+
super
|
|
29
|
+
subclass.instance_variable_set(:@skill_sources, @skill_sources.is_a?(Proc) ? @skill_sources : @skill_sources&.dup)
|
|
30
|
+
subclass.instance_variable_set(:@skill_only, @skill_only&.dup)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Declare skill sources for this agent class.
|
|
34
|
+
#
|
|
35
|
+
# Called with no arguments, returns the current configuration.
|
|
36
|
+
# Called with sources or a block, sets the configuration.
|
|
37
|
+
#
|
|
38
|
+
# @param sources [Array] skill sources
|
|
39
|
+
# @param only [Array<Symbol, String>, nil] include only these skills
|
|
40
|
+
# @return [Hash] current configuration when called as a getter
|
|
41
|
+
def skills(*sources, only: nil, &block)
|
|
42
|
+
if sources.empty? && only.nil? && !block_given?
|
|
43
|
+
return {
|
|
44
|
+
sources: @skill_sources.is_a?(Proc) ? @skill_sources : @skill_sources&.dup,
|
|
45
|
+
only: @skill_only&.dup
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@skill_sources = block_given? ? block : normalize_skill_sources(sources)
|
|
50
|
+
@skill_only = only&.dup
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def normalize_skill_sources(raw_sources)
|
|
56
|
+
flatten_skill_sources(raw_sources).compact
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def flatten_skill_sources(source)
|
|
60
|
+
return [] if source.nil?
|
|
61
|
+
return [source] if source.is_a?(String)
|
|
62
|
+
return [source] if loader_source?(source)
|
|
63
|
+
return [source] if database_collection_source?(source)
|
|
64
|
+
return source.flat_map { |item| flatten_skill_sources(item) } if source.is_a?(Array)
|
|
65
|
+
|
|
66
|
+
[source]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def loader_source?(source)
|
|
70
|
+
source.respond_to?(:list) && source.respond_to?(:find)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def database_collection_source?(source)
|
|
74
|
+
source.respond_to?(:to_a) && source.first&.respond_to?(:name) && source.first.respond_to?(:content)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
module InstanceMethods
|
|
79
|
+
# Add skills to this agent instance at runtime.
|
|
80
|
+
#
|
|
81
|
+
# @param sources [Array] skill sources
|
|
82
|
+
# @param only [Array<Symbol, String>, nil] include only these skills
|
|
83
|
+
# @return [self] for chaining
|
|
84
|
+
def with_skills(*sources, only: nil)
|
|
85
|
+
chat.with_skills(*sources, only: only)
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
module ConfigurationPatch
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def apply_configuration(chat_object, **kwargs)
|
|
94
|
+
super
|
|
95
|
+
input_values = kwargs[:input_values] || {}
|
|
96
|
+
runtime = runtime_context(chat: chat_object, inputs: input_values)
|
|
97
|
+
apply_skills(llm_chat_for(chat_object), runtime)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def apply_skills(llm_chat, runtime)
|
|
101
|
+
config = skills
|
|
102
|
+
sources = config[:sources]
|
|
103
|
+
return if sources.nil?
|
|
104
|
+
|
|
105
|
+
resolved_sources = if sources.is_a?(Proc)
|
|
106
|
+
runtime.instance_exec(&sources)
|
|
107
|
+
else
|
|
108
|
+
sources
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
normalized_sources = normalize_skill_sources(resolved_sources)
|
|
112
|
+
return if normalized_sources.empty?
|
|
113
|
+
|
|
114
|
+
validate_skill_sources!(normalized_sources)
|
|
115
|
+
llm_chat.with_skills(*normalized_sources, only: config[:only])
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def validate_skill_sources!(sources)
|
|
119
|
+
invalid_sources = sources.reject { |source| valid_skill_source?(source) }
|
|
120
|
+
return if invalid_sources.empty?
|
|
121
|
+
|
|
122
|
+
invalid_types = invalid_sources.map { |source| source.class.name || source.class.to_s }.uniq.join(", ")
|
|
123
|
+
raise ArgumentError,
|
|
124
|
+
"Invalid skill source(s): #{invalid_types}. Expected String path, Loader, or record collection."
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def valid_skill_source?(source)
|
|
128
|
+
source.is_a?(String) || loader_source?(source) || database_collection_source?(source)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.included(base)
|
|
133
|
+
missing_methods = REQUIRED_AGENT_SINGLETON_METHODS.reject do |method_name|
|
|
134
|
+
base.singleton_class.private_method_defined?(method_name) || base.singleton_class.method_defined?(method_name)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if missing_methods.any?
|
|
138
|
+
raise LoadError,
|
|
139
|
+
"RubyLLM::Agent is missing required methods for ruby_llm-skills integration: #{missing_methods.join(", ")}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
base.extend(ClassMethods)
|
|
143
|
+
base.include(InstanceMethods)
|
|
144
|
+
base.singleton_class.prepend(ConfigurationPatch)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "skill_tool"
|
|
4
4
|
|
|
5
|
-
module
|
|
5
|
+
module RubyLLM
|
|
6
6
|
module Skills
|
|
7
7
|
# Extensions for RubyLLM::Chat to enable skill integration.
|
|
8
8
|
#
|
|
@@ -25,13 +25,13 @@ module RubyLlm
|
|
|
25
25
|
# @param only [Array<Symbol, String>, nil] include only these skills
|
|
26
26
|
# @return [self] for chaining
|
|
27
27
|
def with_skills(*sources, only: nil)
|
|
28
|
-
sources = [
|
|
28
|
+
sources = [RubyLLM::Skills.default_path] if sources.empty?
|
|
29
29
|
loaders = sources.map { |s| to_loader(s) }
|
|
30
30
|
|
|
31
|
-
loader = (loaders.length == 1) ? loaders.first :
|
|
31
|
+
loader = (loaders.length == 1) ? loaders.first : RubyLLM::Skills.compose(*loaders)
|
|
32
32
|
loader = FilteredLoader.new(loader, only) if only
|
|
33
33
|
|
|
34
|
-
skill_tool =
|
|
34
|
+
skill_tool = RubyLLM::Skills::SkillTool.new(loader)
|
|
35
35
|
with_tool(skill_tool)
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -40,13 +40,24 @@ module RubyLlm
|
|
|
40
40
|
def to_loader(source)
|
|
41
41
|
case source
|
|
42
42
|
when String
|
|
43
|
-
|
|
44
|
-
when ->(s) {
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
RubyLLM::Skills.from_directory(source)
|
|
44
|
+
when ->(s) { database_collection_source?(s) }
|
|
45
|
+
RubyLLM::Skills.from_database(source)
|
|
46
|
+
when ->(s) { loader_source?(s) }
|
|
47
47
|
source
|
|
48
|
+
else
|
|
49
|
+
raise ArgumentError,
|
|
50
|
+
"Invalid skill source: #{source.class}. Expected String path, Loader, or record collection."
|
|
48
51
|
end
|
|
49
52
|
end
|
|
53
|
+
|
|
54
|
+
def loader_source?(source)
|
|
55
|
+
source.respond_to?(:list) && source.respond_to?(:find)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def database_collection_source?(source)
|
|
59
|
+
source.respond_to?(:to_a) && source.first&.respond_to?(:name) && source.first.respond_to?(:content)
|
|
60
|
+
end
|
|
50
61
|
end
|
|
51
62
|
|
|
52
63
|
# Simple wrapper that filters skills by name.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module RubyLLM
|
|
4
4
|
module Skills
|
|
5
5
|
# Rails integration for RubyLLM::Skills.
|
|
6
6
|
#
|
|
@@ -9,14 +9,14 @@ module RubyLlm
|
|
|
9
9
|
#
|
|
10
10
|
class Railtie < ::Rails::Railtie
|
|
11
11
|
initializer "ruby_llm_skills.configure" do
|
|
12
|
-
|
|
12
|
+
RubyLLM::Skills.default_path = Rails.root.join("app", "skills").to_s
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
# Add app/skills to autoload paths
|
|
16
|
-
|
|
15
|
+
# Add app/skills to autoload paths (before initialization)
|
|
16
|
+
config.before_configuration do |app|
|
|
17
17
|
skills_path = Rails.root.join("app", "skills")
|
|
18
18
|
if skills_path.exist?
|
|
19
|
-
app.config.autoload_paths
|
|
19
|
+
app.config.autoload_paths += [skills_path.to_s]
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -24,7 +24,7 @@ module RubyLlm
|
|
|
24
24
|
initializer "ruby_llm_skills.active_record" do
|
|
25
25
|
ActiveSupport.on_load(:active_record) do
|
|
26
26
|
if defined?(RubyLLM::ActiveRecord::ChatMethods)
|
|
27
|
-
RubyLLM::ActiveRecord::ChatMethods.include(
|
|
27
|
+
RubyLLM::ActiveRecord::ChatMethods.include(RubyLLM::Skills::ActiveRecordExtensions)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "ruby_llm"
|
|
4
4
|
|
|
5
|
-
module
|
|
5
|
+
module RubyLLM
|
|
6
6
|
module Skills
|
|
7
7
|
# A RubyLLM Tool that enables progressive skill loading.
|
|
8
8
|
#
|
|
@@ -17,8 +17,8 @@ module RubyLlm
|
|
|
17
17
|
# 3. Resources (scripts, references) can be loaded separately as needed
|
|
18
18
|
#
|
|
19
19
|
# @example Basic usage
|
|
20
|
-
# loader =
|
|
21
|
-
# skill_tool =
|
|
20
|
+
# loader = RubyLLM::Skills.from_directory("app/skills")
|
|
21
|
+
# skill_tool = RubyLLM::Skills::SkillTool.new(loader)
|
|
22
22
|
#
|
|
23
23
|
# chat.with_tools(skill_tool)
|
|
24
24
|
# chat.ask("Help me generate a PDF report")
|
data/lib/ruby_llm/skills.rb
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# RubyLLM 1.12.0 Agent uses `delegate` but doesn't require ActiveSupport.
|
|
4
|
+
# Provide a minimal fallback for plain Ruby environments.
|
|
5
|
+
unless Module.method_defined?(:delegate)
|
|
6
|
+
class Module
|
|
7
|
+
def delegate(*methods, to:, prefix: nil, allow_nil: false, private: false, **_options)
|
|
8
|
+
target_method = to.to_sym
|
|
9
|
+
|
|
10
|
+
methods.each do |method_name|
|
|
11
|
+
delegated_method = delegated_method_name(method_name, target_method, prefix)
|
|
12
|
+
|
|
13
|
+
define_method(delegated_method) do |*args, **kwargs, &block|
|
|
14
|
+
target = public_send(target_method)
|
|
15
|
+
if target.nil?
|
|
16
|
+
return nil if allow_nil
|
|
17
|
+
|
|
18
|
+
raise NoMethodError,
|
|
19
|
+
"#{self.class}##{delegated_method} delegated to ##{target_method}, but ##{target_method} is nil"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if kwargs.empty?
|
|
23
|
+
target.public_send(method_name, *args, &block)
|
|
24
|
+
else
|
|
25
|
+
target.public_send(method_name, *args, **kwargs, &block)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private delegated_method if binding.local_variable_get(:private)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def delegated_method_name(method_name, target_method, prefix)
|
|
36
|
+
case prefix
|
|
37
|
+
when true
|
|
38
|
+
:"#{target_method}_#{method_name}"
|
|
39
|
+
when String, Symbol
|
|
40
|
+
:"#{prefix}_#{method_name}"
|
|
41
|
+
else
|
|
42
|
+
method_name
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
3
48
|
require "ruby_llm"
|
|
4
49
|
|
|
5
50
|
require_relative "skills/version"
|
|
@@ -10,14 +55,16 @@ require_relative "skills/skill"
|
|
|
10
55
|
require_relative "skills/loader"
|
|
11
56
|
require_relative "skills/filesystem_loader"
|
|
12
57
|
require_relative "skills/chat_extensions"
|
|
58
|
+
require_relative "skills/agent_extensions"
|
|
13
59
|
|
|
14
60
|
# Load Rails integration when Rails is available
|
|
15
61
|
require_relative "skills/railtie" if defined?(Rails::Railtie)
|
|
16
62
|
|
|
17
63
|
# Extend RubyLLM::Chat with skill methods
|
|
18
|
-
RubyLLM::Chat.include(
|
|
64
|
+
RubyLLM::Chat.include(RubyLLM::Skills::ChatExtensions)
|
|
65
|
+
RubyLLM::Agent.include(RubyLLM::Skills::AgentExtensions)
|
|
19
66
|
|
|
20
|
-
module
|
|
67
|
+
module RubyLLM
|
|
21
68
|
module Skills
|
|
22
69
|
class << self
|
|
23
70
|
attr_accessor :default_path
|
|
@@ -27,7 +74,7 @@ module RubyLlm
|
|
|
27
74
|
# @param path [String] path to skills directory (defaults to default_path)
|
|
28
75
|
# @return [FilesystemLoader] loader for the directory
|
|
29
76
|
# @example
|
|
30
|
-
#
|
|
77
|
+
# RubyLLM::Skills.from_directory("app/skills")
|
|
31
78
|
def from_directory(path = default_path)
|
|
32
79
|
FilesystemLoader.new(path)
|
|
33
80
|
end
|
|
@@ -38,7 +85,7 @@ module RubyLlm
|
|
|
38
85
|
# @return [Skill] the loaded skill
|
|
39
86
|
# @raise [LoadError] if SKILL.md not found
|
|
40
87
|
# @example
|
|
41
|
-
#
|
|
88
|
+
# RubyLLM::Skills.load("app/skills/my-skill")
|
|
42
89
|
def load(path)
|
|
43
90
|
skill_md = File.join(path, "SKILL.md")
|
|
44
91
|
raise LoadError, "SKILL.md not found in #{path}" unless File.exist?(skill_md)
|
|
@@ -52,7 +99,7 @@ module RubyLlm
|
|
|
52
99
|
# @param records [ActiveRecord::Relation, Array] collection of skill records
|
|
53
100
|
# @return [DatabaseLoader] loader for the records
|
|
54
101
|
# @example
|
|
55
|
-
#
|
|
102
|
+
# RubyLLM::Skills.from_database(Skill.where(active: true))
|
|
56
103
|
def from_database(records)
|
|
57
104
|
require_relative "skills/database_loader"
|
|
58
105
|
DatabaseLoader.new(records)
|
|
@@ -63,9 +110,9 @@ module RubyLlm
|
|
|
63
110
|
# @param loaders [Array<Loader>] loaders to combine
|
|
64
111
|
# @return [CompositeLoader] combined loader
|
|
65
112
|
# @example
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
113
|
+
# RubyLLM::Skills.compose(
|
|
114
|
+
# RubyLLM::Skills.from_directory("app/skills"),
|
|
115
|
+
# RubyLLM::Skills.from_database(Skill.all)
|
|
69
116
|
# )
|
|
70
117
|
def compose(*loaders)
|
|
71
118
|
require_relative "skills/composite_loader"
|
metadata
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_llm-skills
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kieran Klaassen
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-02-17 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ruby_llm
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '1.
|
|
18
|
+
version: '1.12'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '1.
|
|
25
|
+
version: '1.12'
|
|
26
26
|
description: Load, validate, and integrate Agent Skills with RubyLLM. Supports the
|
|
27
27
|
open Agent Skills specification for progressive skill discovery and loading from
|
|
28
28
|
filesystem, zip archives, and databases.
|
|
@@ -35,9 +35,11 @@ files:
|
|
|
35
35
|
- CHANGELOG.md
|
|
36
36
|
- LICENSE.txt
|
|
37
37
|
- README.md
|
|
38
|
+
- lib/generators/skill/USAGE
|
|
38
39
|
- lib/generators/skill/skill_generator.rb
|
|
39
40
|
- lib/generators/skill/templates/SKILL.md.tt
|
|
40
41
|
- lib/ruby_llm/skills.rb
|
|
42
|
+
- lib/ruby_llm/skills/agent_extensions.rb
|
|
41
43
|
- lib/ruby_llm/skills/chat_extensions.rb
|
|
42
44
|
- lib/ruby_llm/skills/composite_loader.rb
|
|
43
45
|
- lib/ruby_llm/skills/database_loader.rb
|