ruby_llm-skills 0.2.0 → 0.4.0.pre1
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 +41 -0
- data/README.md +55 -1
- 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 +149 -0
- data/lib/ruby_llm/skills/chat_extensions.rb +15 -9
- 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 +17 -17
- data/lib/ruby_llm/skills/source_detection.rb +25 -0
- data/lib/ruby_llm/skills/validator.rb +1 -1
- data/lib/ruby_llm/skills/version.rb +2 -2
- data/lib/ruby_llm/skills.rb +10 -8
- metadata +19 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebbccd1655baa8f141a020e2a32fdad03f8c6ddc7cd8929cb6b35cee8e4fc488
|
|
4
|
+
data.tar.gz: b1d175a5fc1ec6c7603318a42fdb34d44f25680da95fa21a2b3f042621644641
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b39b6966015406944f68897ed6170bce39cbd6f36ad6d363e650a1dbfc160a99a532e728b1e599d3b5243a4323db6c24220e79c20f796efa3c714a45126140d
|
|
7
|
+
data.tar.gz: eb71f38a6faa4c75faf675f9c6ce7ac7ae835db82a1a58c1d1c5c2b5ec5511426fc7c5e770ec91e1ad1019e8868c81a49ef65ae254e8cf4256e66f0348c87d41
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ 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.4.0.pre1] - 2026-09-10
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Breaking:** requires RubyLLM 2.0 (`ruby_llm >= 2.0.0.rc2, < 3`); 1.x users should stay on 0.3.x
|
|
13
|
+
- `SkillTool` declares parameters with the 2.0 `parameter`/`description:` DSL, exposes `parameters_schema`, and sets its name through `tool_name`
|
|
14
|
+
- `Chat#with_skills` registers the skill tool via `Chat#with_tools` (2.0 removed `Chat#with_tool`)
|
|
15
|
+
- `AgentExtensions` hooks the 2.0 `Agent.apply_configuration(chat, input_values:, persist_instructions:)` signature and applies skills to the chat or `acts_as_chat` record it receives
|
|
16
|
+
- Integration tests pin `openai_protocol = :chat_completions` and match VCR cassettes on method and URI so the recorded 1.x interactions replay under 2.0
|
|
17
|
+
|
|
18
|
+
### Removed
|
|
19
|
+
|
|
20
|
+
- Global `Module#delegate` fallback and its compatibility test (2.0's `Agent` uses `Forwardable`)
|
|
21
|
+
- Dependency on the removed `Agent.llm_chat_for` hook
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- StandardRB `Layout/EmptyLinesAroundModuleBody` offense in `chat_extensions.rb` that failed CI
|
|
26
|
+
|
|
27
|
+
## [0.3.0] - 2026-02-17
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `RubyLLM::Agent` integration via `RubyLLM::Skills::AgentExtensions`
|
|
32
|
+
- Class-level `skills` DSL on agent subclasses with source, `only:`, and proc support
|
|
33
|
+
- Instance-level `with_skills` for runtime agent skill configuration
|
|
34
|
+
- Agent-specific unit and integration test coverage
|
|
35
|
+
- Compatibility tests for delegate fallback behavior
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- Tightened `ruby_llm` dependency from open lower bound to `~> 1.12`
|
|
40
|
+
- Added explicit runtime compatibility checks for required `RubyLLM::Agent` hooks
|
|
41
|
+
- Documented `agent.with_skills(...)` replacement semantics in README
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- Dynamic skill blocks resolving to `nil` or `[]` no longer silently load default skills
|
|
46
|
+
- Skill source normalization and validation now prevent nested/invalid source runtime crashes
|
|
47
|
+
- Delegate fallback now supports common delegation options (`prefix`, `allow_nil`, `private`)
|
|
48
|
+
|
|
8
49
|
## [0.1.0] - 2025-01-15
|
|
9
50
|
|
|
10
51
|
### Added
|
data/README.md
CHANGED
|
@@ -9,9 +9,12 @@ Agent Skills for [RubyLLM](https://github.com/crmne/ruby_llm). Teach your AI how
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
11
|
```ruby
|
|
12
|
-
gem "ruby_llm
|
|
12
|
+
gem "ruby_llm", "2.0.0.rc2"
|
|
13
|
+
gem "ruby_llm-skills", "0.4.0.pre1"
|
|
13
14
|
```
|
|
14
15
|
|
|
16
|
+
Requires RubyLLM 2.0 (`>= 2.0.0.rc2`). If you are still on RubyLLM 1.x, stay on `ruby_llm-skills ~> 0.3.0` and follow the [RubyLLM 2.0 upgrade guide](https://rubyllm.com/next/upgrading/) before upgrading both gems together.
|
|
17
|
+
|
|
15
18
|
## Quick Start
|
|
16
19
|
|
|
17
20
|
```ruby
|
|
@@ -31,6 +34,26 @@ chat.with_skills("app/skills", "app/commands") # multiple paths
|
|
|
31
34
|
chat.with_skills("app/skills", user.skills) # with database records
|
|
32
35
|
```
|
|
33
36
|
|
|
37
|
+
### With RubyLLM::Agent
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
class SupportAgent < RubyLLM::Agent
|
|
41
|
+
model "gpt-5-nano"
|
|
42
|
+
instructions "You are a support assistant."
|
|
43
|
+
skills "app/skills", only: [:faq, :troubleshooting]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
chat = SupportAgent.chat
|
|
47
|
+
chat.ask("How do I reset my password?")
|
|
48
|
+
|
|
49
|
+
agent = SupportAgent.new
|
|
50
|
+
agent.with_skills("extra/skills")
|
|
51
|
+
agent.ask("What can you help with?")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`agent.with_skills(...)` replaces the current skill tool configuration.
|
|
55
|
+
To combine sources, pass all sources in a single `skills`/`with_skills` call.
|
|
56
|
+
|
|
34
57
|
## Creating Skills
|
|
35
58
|
|
|
36
59
|
```
|
|
@@ -96,6 +119,37 @@ Default path auto-configured to `Rails.root/app/skills`.
|
|
|
96
119
|
rails generate skill pdf-report --description "Generate PDF reports"
|
|
97
120
|
```
|
|
98
121
|
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
### Setup
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
bin/setup
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Running Tests
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
bundle exec rake test # Unit tests (177 tests)
|
|
134
|
+
bundle exec rake test_rails # Rails integration tests (25+ tests)
|
|
135
|
+
bundle exec rake test_all # Both
|
|
136
|
+
bundle exec rake # Tests + linting
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Dummy Rails App
|
|
140
|
+
|
|
141
|
+
A minimal Rails 8 app at `test/dummy/` tests Rails integration:
|
|
142
|
+
|
|
143
|
+
- **Filesystem skills**: `app/skills/greeting/` tests directory-based loading
|
|
144
|
+
- **Database skills**: `Skill` model tests ActiveRecord-based loading
|
|
145
|
+
- **Generator tests**: Tests for `rails generate skill`
|
|
146
|
+
- **Composite loading**: Tests combining filesystem + database sources
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
cd test/dummy
|
|
150
|
+
bundle exec rails test # Run Rails tests directly
|
|
151
|
+
```
|
|
152
|
+
|
|
99
153
|
## Resources
|
|
100
154
|
|
|
101
155
|
- [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,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "source_detection"
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Skills
|
|
7
|
+
# Extensions for RubyLLM::Agent to enable declarative skill configuration.
|
|
8
|
+
#
|
|
9
|
+
# @example Static skills
|
|
10
|
+
# class SupportAgent < RubyLLM::Agent
|
|
11
|
+
# skills "app/skills", only: [:faq]
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# @example Dynamic skills
|
|
15
|
+
# class WorkspaceAgent < RubyLLM::Agent
|
|
16
|
+
# inputs :workspace
|
|
17
|
+
# skills { [workspace.skill_collection] }
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
module AgentExtensions
|
|
21
|
+
REQUIRED_AGENT_SINGLETON_METHODS = %i[apply_configuration runtime_context].freeze
|
|
22
|
+
|
|
23
|
+
module ClassMethods
|
|
24
|
+
include SourceDetection
|
|
25
|
+
|
|
26
|
+
def self.extended(base)
|
|
27
|
+
base.instance_variable_set(:@skill_sources, nil)
|
|
28
|
+
base.instance_variable_set(:@skill_only, nil)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def inherited(subclass)
|
|
32
|
+
super
|
|
33
|
+
subclass.instance_variable_set(:@skill_sources, @skill_sources.is_a?(Proc) ? @skill_sources : @skill_sources&.dup)
|
|
34
|
+
subclass.instance_variable_set(:@skill_only, @skill_only&.dup)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Declare skill sources for this agent class.
|
|
38
|
+
#
|
|
39
|
+
# Called with no arguments, returns the current configuration.
|
|
40
|
+
# Called with sources or a block, sets the configuration.
|
|
41
|
+
#
|
|
42
|
+
# @param sources [Array] skill sources
|
|
43
|
+
# @param only [Array<Symbol, String>, nil] include only these skills
|
|
44
|
+
# @return [Hash] current configuration when called as a getter
|
|
45
|
+
def skills(*sources, only: nil, &block)
|
|
46
|
+
if sources.empty? && only.nil? && !block_given?
|
|
47
|
+
return {
|
|
48
|
+
sources: @skill_sources.is_a?(Proc) ? @skill_sources : @skill_sources&.dup,
|
|
49
|
+
only: @skill_only&.dup
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@skill_sources = block_given? ? block : normalize_skill_sources(sources)
|
|
54
|
+
@skill_only = only&.dup
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def normalize_skill_sources(raw_sources)
|
|
60
|
+
flatten_skill_sources(raw_sources).compact
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def flatten_skill_sources(source)
|
|
64
|
+
return [] if source.nil?
|
|
65
|
+
return [source] if source.is_a?(String)
|
|
66
|
+
return [source] if loader_source?(source)
|
|
67
|
+
return source.empty? ? [] : [source] if database_collection_source?(source)
|
|
68
|
+
return source.flat_map { |item| flatten_skill_sources(item) } if source.is_a?(Array)
|
|
69
|
+
|
|
70
|
+
[source]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
module InstanceMethods
|
|
75
|
+
# Add skills to this agent instance at runtime.
|
|
76
|
+
#
|
|
77
|
+
# @param sources [Array] skill sources
|
|
78
|
+
# @param only [Array<Symbol, String>, nil] include only these skills
|
|
79
|
+
# @return [self] for chaining
|
|
80
|
+
def with_skills(*sources, only: nil)
|
|
81
|
+
chat.with_skills(*sources, only: only)
|
|
82
|
+
self
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
module ConfigurationPatch
|
|
87
|
+
# RubyLLM 2.0 passes either a RubyLLM::Chat or an acts_as_chat record.
|
|
88
|
+
# Both respond to #with_skills (ChatExtensions / ActiveRecordExtensions),
|
|
89
|
+
# so skills apply directly to whatever the agent configured.
|
|
90
|
+
#
|
|
91
|
+
# apply_configuration is a :nodoc: hook; **options forwards any keyword a
|
|
92
|
+
# later 2.x release adds so the patch fails at super, not before it.
|
|
93
|
+
def apply_configuration(chat, input_values: {}, **options)
|
|
94
|
+
super
|
|
95
|
+
runtime = runtime_context(chat: chat, inputs: input_values)
|
|
96
|
+
apply_skills(chat, runtime)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def apply_skills(chat, runtime)
|
|
102
|
+
config = skills
|
|
103
|
+
sources = config[:sources]
|
|
104
|
+
return if sources.nil?
|
|
105
|
+
|
|
106
|
+
resolved_sources = if sources.is_a?(Proc)
|
|
107
|
+
runtime.instance_exec(&sources)
|
|
108
|
+
else
|
|
109
|
+
sources
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
normalized_sources = normalize_skill_sources(resolved_sources)
|
|
113
|
+
return if normalized_sources.empty?
|
|
114
|
+
|
|
115
|
+
validate_skill_sources!(normalized_sources)
|
|
116
|
+
chat.with_skills(*normalized_sources, only: config[:only])
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def validate_skill_sources!(sources)
|
|
120
|
+
invalid_sources = sources.reject { |source| valid_skill_source?(source) }
|
|
121
|
+
return if invalid_sources.empty?
|
|
122
|
+
|
|
123
|
+
invalid_types = invalid_sources.map { |source| source.class.name || source.class.to_s }.uniq.join(", ")
|
|
124
|
+
raise ArgumentError,
|
|
125
|
+
"Invalid skill source(s): #{invalid_types}. Expected String path, Loader, or record collection."
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def valid_skill_source?(source)
|
|
129
|
+
source.is_a?(String) || loader_source?(source) || database_collection_source?(source)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.included(base)
|
|
134
|
+
missing_methods = REQUIRED_AGENT_SINGLETON_METHODS.reject do |method_name|
|
|
135
|
+
base.singleton_class.private_method_defined?(method_name) || base.singleton_class.method_defined?(method_name)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
if missing_methods.any?
|
|
139
|
+
raise LoadError,
|
|
140
|
+
"RubyLLM::Agent is missing required methods for ruby_llm-skills integration: #{missing_methods.join(", ")}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
base.extend(ClassMethods)
|
|
144
|
+
base.include(InstanceMethods)
|
|
145
|
+
base.singleton_class.prepend(ConfigurationPatch)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "source_detection"
|
|
3
4
|
require_relative "skill_tool"
|
|
4
5
|
|
|
5
|
-
module
|
|
6
|
+
module RubyLLM
|
|
6
7
|
module Skills
|
|
7
8
|
# Extensions for RubyLLM::Chat to enable skill integration.
|
|
8
9
|
#
|
|
@@ -19,20 +20,22 @@ module RubyLlm
|
|
|
19
20
|
# chat.with_skills(only: [:pdf_report])
|
|
20
21
|
#
|
|
21
22
|
module ChatExtensions
|
|
23
|
+
include SourceDetection
|
|
24
|
+
|
|
22
25
|
# Add skills to this chat.
|
|
23
26
|
#
|
|
24
27
|
# @param sources [Array] skill sources - auto-detects type (directory, zip, collection)
|
|
25
28
|
# @param only [Array<Symbol, String>, nil] include only these skills
|
|
26
29
|
# @return [self] for chaining
|
|
27
30
|
def with_skills(*sources, only: nil)
|
|
28
|
-
sources = [
|
|
31
|
+
sources = [RubyLLM::Skills.default_path] if sources.empty?
|
|
29
32
|
loaders = sources.map { |s| to_loader(s) }
|
|
30
33
|
|
|
31
|
-
loader = (loaders.length == 1) ? loaders.first :
|
|
34
|
+
loader = (loaders.length == 1) ? loaders.first : RubyLLM::Skills.compose(*loaders)
|
|
32
35
|
loader = FilteredLoader.new(loader, only) if only
|
|
33
36
|
|
|
34
|
-
skill_tool =
|
|
35
|
-
|
|
37
|
+
skill_tool = RubyLLM::Skills::SkillTool.new(loader)
|
|
38
|
+
with_tools(skill_tool)
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
private
|
|
@@ -40,11 +43,14 @@ module RubyLlm
|
|
|
40
43
|
def to_loader(source)
|
|
41
44
|
case source
|
|
42
45
|
when String
|
|
43
|
-
|
|
44
|
-
when ->(s) {
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
RubyLLM::Skills.from_directory(source)
|
|
47
|
+
when ->(s) { database_collection_source?(s) }
|
|
48
|
+
RubyLLM::Skills.from_database(source)
|
|
49
|
+
when ->(s) { loader_source?(s) }
|
|
47
50
|
source
|
|
51
|
+
else
|
|
52
|
+
raise ArgumentError,
|
|
53
|
+
"Invalid skill source: #{source.class}. Expected String path, Loader, or record collection."
|
|
48
54
|
end
|
|
49
55
|
end
|
|
50
56
|
end
|
|
@@ -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")
|
|
@@ -27,15 +27,22 @@ module RubyLlm
|
|
|
27
27
|
#
|
|
28
28
|
class SkillTool < RubyLLM::Tool
|
|
29
29
|
description "Execute a skill within the main conversation."
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
parameter :command, type: "string",
|
|
31
|
+
description: "The skill name (e.g., 'pdf' or 'write-poem')"
|
|
32
|
+
parameter :arguments, type: "string", required: false,
|
|
33
|
+
description: "Arguments passed after the command (e.g., '/write-poem about robots' passes 'about robots')"
|
|
34
|
+
parameter :resource, type: "string", required: false,
|
|
35
|
+
description: "Optional resource path to load (e.g., 'scripts/helper.rb', 'references/guide.md')"
|
|
36
36
|
|
|
37
37
|
attr_reader :loader
|
|
38
38
|
|
|
39
|
+
# Tool name for RubyLLM.
|
|
40
|
+
#
|
|
41
|
+
# @return [String] "skill"
|
|
42
|
+
def self.tool_name
|
|
43
|
+
"skill"
|
|
44
|
+
end
|
|
45
|
+
|
|
39
46
|
# Initialize with a skill loader.
|
|
40
47
|
#
|
|
41
48
|
# @param loader [Loader] any loader (FilesystemLoader, ZipLoader, etc.)
|
|
@@ -43,13 +50,6 @@ module RubyLlm
|
|
|
43
50
|
@loader = loader
|
|
44
51
|
end
|
|
45
52
|
|
|
46
|
-
# Tool name for RubyLLM.
|
|
47
|
-
#
|
|
48
|
-
# @return [String] "skill"
|
|
49
|
-
def name
|
|
50
|
-
"skill"
|
|
51
|
-
end
|
|
52
|
-
|
|
53
53
|
# Dynamic description including available skills.
|
|
54
54
|
#
|
|
55
55
|
# @return [String] tool description with embedded skill metadata
|
|
@@ -98,7 +98,7 @@ module RubyLlm
|
|
|
98
98
|
{
|
|
99
99
|
name: name,
|
|
100
100
|
description: description,
|
|
101
|
-
parameters:
|
|
101
|
+
parameters: parameters_schema
|
|
102
102
|
}
|
|
103
103
|
end
|
|
104
104
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Skills
|
|
5
|
+
# Shared duck-typing predicates for classifying skill sources.
|
|
6
|
+
# Used by both AgentExtensions and ChatExtensions.
|
|
7
|
+
module SourceDetection
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def loader_source?(source)
|
|
11
|
+
source.respond_to?(:list) && source.respond_to?(:find)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def database_collection_source?(source)
|
|
15
|
+
return false unless source.respond_to?(:to_a)
|
|
16
|
+
|
|
17
|
+
# ActiveRecord relations/CollectionProxy: recognizable even when empty
|
|
18
|
+
return true if source.respond_to?(:klass) && source.respond_to?(:where_values_hash)
|
|
19
|
+
|
|
20
|
+
first = source.first
|
|
21
|
+
first&.respond_to?(:name) && first.respond_to?(:content)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/ruby_llm/skills.rb
CHANGED
|
@@ -10,14 +10,16 @@ require_relative "skills/skill"
|
|
|
10
10
|
require_relative "skills/loader"
|
|
11
11
|
require_relative "skills/filesystem_loader"
|
|
12
12
|
require_relative "skills/chat_extensions"
|
|
13
|
+
require_relative "skills/agent_extensions"
|
|
13
14
|
|
|
14
15
|
# Load Rails integration when Rails is available
|
|
15
16
|
require_relative "skills/railtie" if defined?(Rails::Railtie)
|
|
16
17
|
|
|
17
18
|
# Extend RubyLLM::Chat with skill methods
|
|
18
|
-
RubyLLM::Chat.include(
|
|
19
|
+
RubyLLM::Chat.include(RubyLLM::Skills::ChatExtensions)
|
|
20
|
+
RubyLLM::Agent.include(RubyLLM::Skills::AgentExtensions)
|
|
19
21
|
|
|
20
|
-
module
|
|
22
|
+
module RubyLLM
|
|
21
23
|
module Skills
|
|
22
24
|
class << self
|
|
23
25
|
attr_accessor :default_path
|
|
@@ -27,7 +29,7 @@ module RubyLlm
|
|
|
27
29
|
# @param path [String] path to skills directory (defaults to default_path)
|
|
28
30
|
# @return [FilesystemLoader] loader for the directory
|
|
29
31
|
# @example
|
|
30
|
-
#
|
|
32
|
+
# RubyLLM::Skills.from_directory("app/skills")
|
|
31
33
|
def from_directory(path = default_path)
|
|
32
34
|
FilesystemLoader.new(path)
|
|
33
35
|
end
|
|
@@ -38,7 +40,7 @@ module RubyLlm
|
|
|
38
40
|
# @return [Skill] the loaded skill
|
|
39
41
|
# @raise [LoadError] if SKILL.md not found
|
|
40
42
|
# @example
|
|
41
|
-
#
|
|
43
|
+
# RubyLLM::Skills.load("app/skills/my-skill")
|
|
42
44
|
def load(path)
|
|
43
45
|
skill_md = File.join(path, "SKILL.md")
|
|
44
46
|
raise LoadError, "SKILL.md not found in #{path}" unless File.exist?(skill_md)
|
|
@@ -52,7 +54,7 @@ module RubyLlm
|
|
|
52
54
|
# @param records [ActiveRecord::Relation, Array] collection of skill records
|
|
53
55
|
# @return [DatabaseLoader] loader for the records
|
|
54
56
|
# @example
|
|
55
|
-
#
|
|
57
|
+
# RubyLLM::Skills.from_database(Skill.where(active: true))
|
|
56
58
|
def from_database(records)
|
|
57
59
|
require_relative "skills/database_loader"
|
|
58
60
|
DatabaseLoader.new(records)
|
|
@@ -63,9 +65,9 @@ module RubyLlm
|
|
|
63
65
|
# @param loaders [Array<Loader>] loaders to combine
|
|
64
66
|
# @return [CompositeLoader] combined loader
|
|
65
67
|
# @example
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
68
|
+
# RubyLLM::Skills.compose(
|
|
69
|
+
# RubyLLM::Skills.from_directory("app/skills"),
|
|
70
|
+
# RubyLLM::Skills.from_database(Skill.all)
|
|
69
71
|
# )
|
|
70
72
|
def compose(*loaders)
|
|
71
73
|
require_relative "skills/composite_loader"
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
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.4.0.pre1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kieran Klaassen
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-10 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: ruby_llm
|
|
@@ -15,14 +16,20 @@ dependencies:
|
|
|
15
16
|
requirements:
|
|
16
17
|
- - ">="
|
|
17
18
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
19
|
+
version: 2.0.0.rc2
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '3'
|
|
19
23
|
type: :runtime
|
|
20
24
|
prerelease: false
|
|
21
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
26
|
requirements:
|
|
23
27
|
- - ">="
|
|
24
28
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
29
|
+
version: 2.0.0.rc2
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3'
|
|
26
33
|
description: Load, validate, and integrate Agent Skills with RubyLLM. Supports the
|
|
27
34
|
open Agent Skills specification for progressive skill discovery and loading from
|
|
28
35
|
filesystem, zip archives, and databases.
|
|
@@ -35,9 +42,11 @@ files:
|
|
|
35
42
|
- CHANGELOG.md
|
|
36
43
|
- LICENSE.txt
|
|
37
44
|
- README.md
|
|
45
|
+
- lib/generators/skill/USAGE
|
|
38
46
|
- lib/generators/skill/skill_generator.rb
|
|
39
47
|
- lib/generators/skill/templates/SKILL.md.tt
|
|
40
48
|
- lib/ruby_llm/skills.rb
|
|
49
|
+
- lib/ruby_llm/skills/agent_extensions.rb
|
|
41
50
|
- lib/ruby_llm/skills/chat_extensions.rb
|
|
42
51
|
- lib/ruby_llm/skills/composite_loader.rb
|
|
43
52
|
- lib/ruby_llm/skills/database_loader.rb
|
|
@@ -48,6 +57,7 @@ files:
|
|
|
48
57
|
- lib/ruby_llm/skills/railtie.rb
|
|
49
58
|
- lib/ruby_llm/skills/skill.rb
|
|
50
59
|
- lib/ruby_llm/skills/skill_tool.rb
|
|
60
|
+
- lib/ruby_llm/skills/source_detection.rb
|
|
51
61
|
- lib/ruby_llm/skills/tasks/skills.rake
|
|
52
62
|
- lib/ruby_llm/skills/validator.rb
|
|
53
63
|
- lib/ruby_llm/skills/version.rb
|
|
@@ -59,6 +69,7 @@ metadata:
|
|
|
59
69
|
source_code_uri: https://github.com/kieranklaassen/ruby_llm-skills
|
|
60
70
|
changelog_uri: https://github.com/kieranklaassen/ruby_llm-skills/blob/main/CHANGELOG.md
|
|
61
71
|
rubygems_mfa_required: 'true'
|
|
72
|
+
post_install_message:
|
|
62
73
|
rdoc_options: []
|
|
63
74
|
require_paths:
|
|
64
75
|
- lib
|
|
@@ -69,11 +80,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
69
80
|
version: 3.2.0
|
|
70
81
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
82
|
requirements:
|
|
72
|
-
- - "
|
|
83
|
+
- - ">"
|
|
73
84
|
- !ruby/object:Gem::Version
|
|
74
|
-
version:
|
|
85
|
+
version: 1.3.1
|
|
75
86
|
requirements: []
|
|
76
|
-
rubygems_version: 3.
|
|
87
|
+
rubygems_version: 3.4.10
|
|
88
|
+
signing_key:
|
|
77
89
|
specification_version: 4
|
|
78
90
|
summary: Agent Skills extension for RubyLLM
|
|
79
91
|
test_files: []
|