ruby_llm-skills 0.1.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 +7 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +396 -0
- data/lib/generators/skill/skill_generator.rb +60 -0
- data/lib/generators/skill/templates/SKILL.md.tt +21 -0
- data/lib/ruby_llm/skills/chat_extensions.rb +49 -0
- data/lib/ruby_llm/skills/composite_loader.rb +57 -0
- data/lib/ruby_llm/skills/database_loader.rb +147 -0
- data/lib/ruby_llm/skills/error.rb +21 -0
- data/lib/ruby_llm/skills/filesystem_loader.rb +55 -0
- data/lib/ruby_llm/skills/loader.rb +77 -0
- data/lib/ruby_llm/skills/parser.rb +80 -0
- data/lib/ruby_llm/skills/railtie.rb +29 -0
- data/lib/ruby_llm/skills/skill.rb +174 -0
- data/lib/ruby_llm/skills/skill_tool.rb +166 -0
- data/lib/ruby_llm/skills/tasks/skills.rake +110 -0
- data/lib/ruby_llm/skills/validator.rb +108 -0
- data/lib/ruby_llm/skills/version.rb +7 -0
- data/lib/ruby_llm/skills/zip_loader.rb +129 -0
- data/lib/ruby_llm/skills.rb +89 -0
- metadata +66 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 39bffc3152e2a10d3f8000006e50d45748c30d6a04a71189ff0d52c61ada7984
|
|
4
|
+
data.tar.gz: 57a3d68ee3b3a1f47a9bbe7b18facc4ae52a12608d275428c37a02769d0a78db
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6aa5b23f9bcaaafeb00b073e031645e72671414eaa744c06eafbd6d66ff23d8023c4b384b5240a42013ae07c5a36b562c3b849b6f57c5ceee5bcc88ee7dfdfde
|
|
7
|
+
data.tar.gz: d1d4a88688857c09a4399dfcb336ea2b1191ba792689b27c796a7c803005900006b52a772656be3c1a6a6c21814bd3783392af538e4c8c2ca94103b399781487
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2025-01-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release with full Agent Skills specification support
|
|
13
|
+
- `Parser` - YAML frontmatter parsing with safe_load
|
|
14
|
+
- `Skill` - Lazy loading for content and resources
|
|
15
|
+
- `Validator` - Agent Skills spec validation rules
|
|
16
|
+
- `FilesystemLoader` - Directory-based skill loading
|
|
17
|
+
- `ZipLoader` - Archive-based skill loading (optional rubyzip dependency)
|
|
18
|
+
- `DatabaseLoader` - Duck-typed record loading (text or binary storage)
|
|
19
|
+
- `CompositeLoader` - Multi-source skill combination
|
|
20
|
+
- `SkillTool` - RubyLLM tool with progressive disclosure via dynamic description
|
|
21
|
+
- `ChatExtensions` - `with_skills()` and `with_skill_loader()` convenience methods
|
|
22
|
+
- Rails integration with Railtie, generator, and rake tasks
|
|
23
|
+
- Comprehensive test suite (142 tests)
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Kieran Klaassen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# RubyLLM::Skills
|
|
2
|
+
|
|
3
|
+
Agent Skills for [RubyLLM](https://github.com/crmne/ruby_llm). Teach your AI how to do things your way.
|
|
4
|
+
|
|
5
|
+
Skills are folders of instructions, scripts, and resources that extend LLM capabilities for specialized tasks. This gem implements the [Agent Skills specification](https://agentskills.io/specification) for RubyLLM.
|
|
6
|
+
|
|
7
|
+
[](https://badge.fury.io/rb/ruby_llm-skills)
|
|
8
|
+
[](https://github.com/kieranklaassen/ruby_llm-skills/actions)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Add this line to your application's Gemfile:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem "ruby_llm-skills"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
chat = RubyLLM.chat
|
|
22
|
+
chat.with_skills # Load skills from app/skills
|
|
23
|
+
chat.ask "Create a PDF report from this data"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That's it. Skills are discovered automatically and injected into the system prompt.
|
|
27
|
+
|
|
28
|
+
## How It Works
|
|
29
|
+
|
|
30
|
+
Skills follow a three-level progressive disclosure pattern:
|
|
31
|
+
|
|
32
|
+
1. **Metadata** - Name and description loaded at startup (~100 tokens per skill)
|
|
33
|
+
2. **Instructions** - Full SKILL.md loaded when skill triggers
|
|
34
|
+
3. **Resources** - Scripts and references loaded on demand
|
|
35
|
+
|
|
36
|
+
This keeps context lean while making capabilities available.
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
RubyLLM::Skills.default_path = "lib/skills" # Default: app/skills
|
|
42
|
+
RubyLLM::Skills.logger = Rails.logger # Default: nil
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Loading Skills
|
|
46
|
+
|
|
47
|
+
### From Filesystem (Default)
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
# Load from default path (app/skills)
|
|
51
|
+
chat.with_skills
|
|
52
|
+
|
|
53
|
+
# Load from specific directory
|
|
54
|
+
chat.with_skills(from: "lib/skills")
|
|
55
|
+
|
|
56
|
+
# Load specific skills only
|
|
57
|
+
chat.with_skills(only: [:pdf_report, :data_analysis])
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### From Multiple Sources
|
|
61
|
+
|
|
62
|
+
Pass an array to load from multiple locations:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
chat.with_skills(from: [
|
|
66
|
+
"app/skills", # Directory
|
|
67
|
+
"extras/skills.zip", # Zip file
|
|
68
|
+
current_user.skills # ActiveRecord relation
|
|
69
|
+
])
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Sources are loaded in order. Later skills with the same name override earlier ones.
|
|
73
|
+
|
|
74
|
+
### From Database
|
|
75
|
+
|
|
76
|
+
Store complete skills in your database for per-user customization. Two storage formats are supported:
|
|
77
|
+
|
|
78
|
+
**Option A: Store as text (SKILL.md content)**
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
# Migration
|
|
82
|
+
create_table :skills do |t|
|
|
83
|
+
t.string :name, null: false
|
|
84
|
+
t.text :description, null: false
|
|
85
|
+
t.text :content, null: false # Full SKILL.md content
|
|
86
|
+
t.references :user
|
|
87
|
+
t.timestamps
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Model
|
|
91
|
+
class Skill < ApplicationRecord
|
|
92
|
+
belongs_to :user, optional: true
|
|
93
|
+
|
|
94
|
+
validates :name, format: { with: /\A[a-z0-9-]+\z/ }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Create a skill
|
|
98
|
+
current_user.skills.create!(
|
|
99
|
+
name: "my-workflow",
|
|
100
|
+
description: "Custom workflow for data processing",
|
|
101
|
+
content: <<~SKILL
|
|
102
|
+
# My Workflow
|
|
103
|
+
|
|
104
|
+
## Steps
|
|
105
|
+
1. Load the data
|
|
106
|
+
2. Process with custom rules
|
|
107
|
+
3. Export results
|
|
108
|
+
SKILL
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Option B: Store as binary (zip file)**
|
|
113
|
+
|
|
114
|
+
For skills with scripts, references, or assets:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
# Migration
|
|
118
|
+
create_table :skills do |t|
|
|
119
|
+
t.string :name, null: false
|
|
120
|
+
t.text :description, null: false
|
|
121
|
+
t.binary :data, null: false # Zip file blob
|
|
122
|
+
t.references :user
|
|
123
|
+
t.timestamps
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Model
|
|
127
|
+
class Skill < ApplicationRecord
|
|
128
|
+
belongs_to :user, optional: true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Upload a skill zip
|
|
132
|
+
skill_zip = File.read("my-skill.zip")
|
|
133
|
+
current_user.skills.create!(
|
|
134
|
+
name: "pdf-report",
|
|
135
|
+
description: "Generate PDF reports with charts",
|
|
136
|
+
data: skill_zip
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Loading database skills**
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
# Combine app skills with user's custom skills
|
|
144
|
+
chat.with_skills(from: [
|
|
145
|
+
"app/skills", # Base skills from filesystem
|
|
146
|
+
current_user.skills # User's skills from database
|
|
147
|
+
])
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The gem detects the storage format automatically:
|
|
151
|
+
- Records with `content` field → parsed as SKILL.md text
|
|
152
|
+
- Records with `data` field → extracted as zip
|
|
153
|
+
|
|
154
|
+
### From Zip Files
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
chat.with_skills(from: "skills.zip")
|
|
158
|
+
chat.with_skills(from: ["core.zip", "custom.zip"])
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Source Detection
|
|
162
|
+
|
|
163
|
+
The gem auto-detects source type:
|
|
164
|
+
|
|
165
|
+
| Input | Type |
|
|
166
|
+
|-------|------|
|
|
167
|
+
| String ending in `/` or directory path | Filesystem |
|
|
168
|
+
| String ending in `.zip` | Zip file |
|
|
169
|
+
| ActiveRecord relation or array of objects | Database |
|
|
170
|
+
|
|
171
|
+
## Rails Integration
|
|
172
|
+
|
|
173
|
+
Skills load automatically via Railtie. No configuration needed.
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
# app/skills/ is scanned at boot
|
|
177
|
+
# Skills available on any RubyLLM chat
|
|
178
|
+
|
|
179
|
+
class ReportsController < ApplicationController
|
|
180
|
+
def create
|
|
181
|
+
chat = RubyLLM.chat
|
|
182
|
+
chat.with_skills # Already has app/skills loaded
|
|
183
|
+
chat.ask "Generate quarterly report from #{@data}"
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Per-User Skills with ActiveRecord
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
class User < ApplicationRecord
|
|
192
|
+
has_many :skills
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
class Skill < ApplicationRecord
|
|
196
|
+
belongs_to :user, optional: true
|
|
197
|
+
|
|
198
|
+
validates :name, presence: true,
|
|
199
|
+
format: { with: /\A[a-z0-9-]+\z/ },
|
|
200
|
+
length: { maximum: 64 }
|
|
201
|
+
validates :description, presence: true,
|
|
202
|
+
length: { maximum: 1024 }
|
|
203
|
+
validates :content, presence: true
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Skill Discovery
|
|
208
|
+
|
|
209
|
+
Skills are injected into the system prompt as available tools:
|
|
210
|
+
|
|
211
|
+
```xml
|
|
212
|
+
<available_skills>
|
|
213
|
+
<skill>
|
|
214
|
+
<name>pdf-report</name>
|
|
215
|
+
<description>Generate PDF reports with charts...</description>
|
|
216
|
+
<location>app/skills/pdf-report</location>
|
|
217
|
+
</skill>
|
|
218
|
+
</available_skills>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
When the LLM determines a skill is relevant, it reads the full `SKILL.md` into context.
|
|
222
|
+
|
|
223
|
+
## API Reference
|
|
224
|
+
|
|
225
|
+
### RubyLLM::Skills
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
RubyLLM::Skills.default_path # Get/set default skills directory
|
|
229
|
+
RubyLLM::Skills.logger # Get/set logger
|
|
230
|
+
RubyLLM::Skills.load(from:) # Load skills from path/database/zip
|
|
231
|
+
RubyLLM::Skills.validate(skill) # Validate skill structure
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### RubyLLM::Skills::Skill
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
skill = RubyLLM::Skills.find("pdf-report")
|
|
238
|
+
|
|
239
|
+
skill.name # "pdf-report"
|
|
240
|
+
skill.description # "Generate PDF reports..."
|
|
241
|
+
skill.content # Full SKILL.md content
|
|
242
|
+
skill.path # Filesystem path
|
|
243
|
+
skill.metadata # Parsed frontmatter hash
|
|
244
|
+
skill.references # Array of reference files
|
|
245
|
+
skill.scripts # Array of script files
|
|
246
|
+
skill.assets # Array of asset files
|
|
247
|
+
skill.valid? # Validates structure
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Chat Integration
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
chat = RubyLLM.chat
|
|
254
|
+
|
|
255
|
+
chat.with_skills # Load default skills
|
|
256
|
+
chat.with_skills(only: [:name]) # Load specific skills
|
|
257
|
+
chat.with_skills(except: [:name]) # Exclude skills
|
|
258
|
+
chat.with_skills(from: records) # Load from database
|
|
259
|
+
chat.skills # List loaded skills
|
|
260
|
+
chat.skill_metadata # Get metadata for prompt
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Validation
|
|
264
|
+
|
|
265
|
+
Validate skills match the specification:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
skill = RubyLLM::Skills.find("my-skill")
|
|
269
|
+
skill.valid? # => true/false
|
|
270
|
+
skill.errors # => ["name contains uppercase"]
|
|
271
|
+
|
|
272
|
+
# Validate all skills
|
|
273
|
+
RubyLLM::Skills.validate_all
|
|
274
|
+
# => { valid: [...], invalid: [...] }
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Provider Support
|
|
278
|
+
|
|
279
|
+
Skills work with any RubyLLM provider. The skill metadata is injected into the system prompt, so any model that supports system prompts can use skills.
|
|
280
|
+
|
|
281
|
+
Tested with: OpenAI, Anthropic, Google Gemini, AWS Bedrock, Ollama.
|
|
282
|
+
|
|
283
|
+
## Comparison with MCP
|
|
284
|
+
|
|
285
|
+
| Feature | Skills | MCP |
|
|
286
|
+
|---------|--------|-----|
|
|
287
|
+
| Execution | Prompt-based | Tool-based |
|
|
288
|
+
| Setup | Drop in folder | Server config |
|
|
289
|
+
| Context | Progressive disclosure | Always available |
|
|
290
|
+
| Best for | Domain knowledge | External integrations |
|
|
291
|
+
|
|
292
|
+
Use skills for specialized instructions. Use [ruby_llm-mcp](https://github.com/patvice/ruby_llm-mcp) for external tool integrations. They compose well together.
|
|
293
|
+
|
|
294
|
+
## Creating Skills
|
|
295
|
+
|
|
296
|
+
Create a folder with a `SKILL.md` file:
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
app/skills/
|
|
300
|
+
└── pdf-report/
|
|
301
|
+
├── SKILL.md
|
|
302
|
+
├── scripts/
|
|
303
|
+
│ └── generate.rb
|
|
304
|
+
└── references/
|
|
305
|
+
└── templates.md
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
The `SKILL.md` requires YAML frontmatter:
|
|
309
|
+
|
|
310
|
+
```yaml
|
|
311
|
+
---
|
|
312
|
+
name: pdf-report
|
|
313
|
+
description: Generate PDF reports with charts and tables. Use when asked to create reports, export data to PDF, or generate printable documents.
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
# PDF Report Generator
|
|
317
|
+
|
|
318
|
+
## Quick Start
|
|
319
|
+
|
|
320
|
+
Use the bundled script for generation:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
ruby scripts/generate.rb --input data.json --output report.pdf
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Guidelines
|
|
327
|
+
|
|
328
|
+
- Always include page numbers
|
|
329
|
+
- Use company logo from assets/
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Frontmatter Fields
|
|
333
|
+
|
|
334
|
+
| Field | Required | Description |
|
|
335
|
+
|-------|----------|-------------|
|
|
336
|
+
| `name` | Yes | Lowercase, hyphens only. Max 64 chars. |
|
|
337
|
+
| `description` | Yes | What it does AND when to use it. Max 1024 chars. |
|
|
338
|
+
| `license` | No | License identifier |
|
|
339
|
+
| `compatibility` | No | Environment requirements |
|
|
340
|
+
| `metadata` | No | Custom key-value pairs |
|
|
341
|
+
|
|
342
|
+
### Skill Directories
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
skill-name/
|
|
346
|
+
├── SKILL.md # Required - instructions
|
|
347
|
+
├── scripts/ # Optional - executable code
|
|
348
|
+
├── references/ # Optional - additional docs
|
|
349
|
+
└── assets/ # Optional - templates, images
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Best Practices
|
|
353
|
+
|
|
354
|
+
**Keep SKILL.md under 500 lines.** Move detailed content to `references/`.
|
|
355
|
+
|
|
356
|
+
**Write good descriptions.** Include both what the skill does AND when to use it:
|
|
357
|
+
|
|
358
|
+
```yaml
|
|
359
|
+
# Good
|
|
360
|
+
description: Extract text and tables from PDF files. Use when working with PDFs, forms, or document extraction.
|
|
361
|
+
|
|
362
|
+
# Bad
|
|
363
|
+
description: PDF helper.
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
**Use scripts for deterministic operations.** Scripts execute without loading into context.
|
|
367
|
+
|
|
368
|
+
**One level of references.** Avoid deeply nested file chains.
|
|
369
|
+
|
|
370
|
+
## Development
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
git clone https://github.com/kieranklaassen/ruby_llm-skills.git
|
|
374
|
+
cd ruby_llm-skills
|
|
375
|
+
bundle install
|
|
376
|
+
bundle exec rake test
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Contributing
|
|
380
|
+
|
|
381
|
+
1. Fork it
|
|
382
|
+
2. Create your feature branch (`git checkout -b my-feature`)
|
|
383
|
+
3. Commit your changes (`git commit -am 'Add feature'`)
|
|
384
|
+
4. Push to the branch (`git push origin my-feature`)
|
|
385
|
+
5. Create a Pull Request
|
|
386
|
+
|
|
387
|
+
## License
|
|
388
|
+
|
|
389
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
390
|
+
|
|
391
|
+
## Resources
|
|
392
|
+
|
|
393
|
+
- [Agent Skills Specification](https://agentskills.io/specification)
|
|
394
|
+
- [Anthropic Skills Repository](https://github.com/anthropics/skills)
|
|
395
|
+
- [RubyLLM](https://github.com/crmne/ruby_llm)
|
|
396
|
+
- [RubyLLM::MCP](https://github.com/patvice/ruby_llm-mcp)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
class SkillGenerator < Rails::Generators::NamedBase
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
class_option :description, type: :string, default: "Description of what this skill does"
|
|
9
|
+
class_option :license, type: :string, default: nil
|
|
10
|
+
class_option :scripts, type: :boolean, default: false
|
|
11
|
+
class_option :references, type: :boolean, default: false
|
|
12
|
+
class_option :assets, type: :boolean, default: false
|
|
13
|
+
|
|
14
|
+
def create_skill_directory
|
|
15
|
+
empty_directory skill_path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create_skill_md
|
|
19
|
+
template "SKILL.md.tt", File.join(skill_path, "SKILL.md")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def create_scripts_directory
|
|
23
|
+
return unless options[:scripts]
|
|
24
|
+
|
|
25
|
+
empty_directory File.join(skill_path, "scripts")
|
|
26
|
+
create_file File.join(skill_path, "scripts", ".keep")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_references_directory
|
|
30
|
+
return unless options[:references]
|
|
31
|
+
|
|
32
|
+
empty_directory File.join(skill_path, "references")
|
|
33
|
+
create_file File.join(skill_path, "references", ".keep")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def create_assets_directory
|
|
37
|
+
return unless options[:assets]
|
|
38
|
+
|
|
39
|
+
empty_directory File.join(skill_path, "assets")
|
|
40
|
+
create_file File.join(skill_path, "assets", ".keep")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def skill_path
|
|
46
|
+
File.join("app", "skills", skill_name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def skill_name
|
|
50
|
+
file_name.downcase.tr("_", "-").gsub(/[^a-z0-9-]/, "")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def skill_description
|
|
54
|
+
options[:description]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def skill_license
|
|
58
|
+
options[:license]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: <%= skill_name %>
|
|
3
|
+
description: <%= skill_description %>
|
|
4
|
+
<% if skill_license -%>
|
|
5
|
+
license: <%= skill_license %>
|
|
6
|
+
<% end -%>
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# <%= skill_name.split("-").map(&:capitalize).join(" ") %>
|
|
10
|
+
|
|
11
|
+
Instructions for using this skill.
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
Use this skill when...
|
|
16
|
+
|
|
17
|
+
## Steps
|
|
18
|
+
|
|
19
|
+
1. First step
|
|
20
|
+
2. Second step
|
|
21
|
+
3. Third step
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLlm
|
|
4
|
+
module Skills
|
|
5
|
+
# Extensions for RubyLLM::Chat to enable skill integration.
|
|
6
|
+
#
|
|
7
|
+
# These methods are added to RubyLLM::Chat when ruby_llm-skills is loaded,
|
|
8
|
+
# providing a convenient API for adding skills to conversations.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# chat = RubyLlm.chat
|
|
12
|
+
# chat.with_skills("app/skills")
|
|
13
|
+
# chat.ask("Generate a PDF report")
|
|
14
|
+
#
|
|
15
|
+
module ChatExtensions
|
|
16
|
+
# Add skills from a directory to this chat.
|
|
17
|
+
#
|
|
18
|
+
# @param path [String] path to skills directory
|
|
19
|
+
# @return [self] for chaining
|
|
20
|
+
# @example
|
|
21
|
+
# chat.with_skills("app/skills")
|
|
22
|
+
def with_skills(path = RubyLlm::Skills.default_path)
|
|
23
|
+
loader = RubyLlm::Skills.from_directory(path)
|
|
24
|
+
skill_tool = RubyLlm::Skills::SkillTool.new(loader)
|
|
25
|
+
with_tool(skill_tool)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Add skills from a loader to this chat.
|
|
29
|
+
#
|
|
30
|
+
# @param loader [Loader] any skill loader
|
|
31
|
+
# @return [self] for chaining
|
|
32
|
+
# @example
|
|
33
|
+
# loader = RubyLlm::Skills.compose(
|
|
34
|
+
# RubyLlm::Skills.from_directory("app/skills"),
|
|
35
|
+
# RubyLlm::Skills.from_database(Skill.all)
|
|
36
|
+
# )
|
|
37
|
+
# chat.with_skill_loader(loader)
|
|
38
|
+
def with_skill_loader(loader)
|
|
39
|
+
skill_tool = RubyLlm::Skills::SkillTool.new(loader)
|
|
40
|
+
with_tool(skill_tool)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Extend RubyLLM::Chat if available
|
|
47
|
+
if defined?(RubyLlm::Chat)
|
|
48
|
+
RubyLlm::Chat.include(RubyLlm::Skills::ChatExtensions)
|
|
49
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLlm
|
|
4
|
+
module Skills
|
|
5
|
+
# Combines multiple loaders into a single source.
|
|
6
|
+
#
|
|
7
|
+
# Skills are searched in order, with earlier loaders taking precedence.
|
|
8
|
+
# This allows layering skills from multiple sources (filesystem, database, etc).
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# composite = CompositeLoader.new([
|
|
12
|
+
# FilesystemLoader.new("app/skills"),
|
|
13
|
+
# DatabaseLoader.new(Skill.all)
|
|
14
|
+
# ])
|
|
15
|
+
# composite.list # => skills from all loaders
|
|
16
|
+
#
|
|
17
|
+
class CompositeLoader < Loader
|
|
18
|
+
attr_reader :loaders
|
|
19
|
+
|
|
20
|
+
# Initialize with an array of loaders.
|
|
21
|
+
#
|
|
22
|
+
# @param loaders [Array<Loader>] loaders to combine
|
|
23
|
+
def initialize(loaders)
|
|
24
|
+
super()
|
|
25
|
+
@loaders = loaders
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# List all skills from all loaders.
|
|
29
|
+
# Skills are deduplicated by name, with earlier loaders taking precedence.
|
|
30
|
+
#
|
|
31
|
+
# @return [Array<Skill>] combined list of skills
|
|
32
|
+
def list
|
|
33
|
+
skills
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Reload all loaders.
|
|
37
|
+
#
|
|
38
|
+
# @return [self]
|
|
39
|
+
def reload!
|
|
40
|
+
@loaders.each(&:reload!)
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
protected
|
|
45
|
+
|
|
46
|
+
def load_all
|
|
47
|
+
seen = {}
|
|
48
|
+
@loaders.flat_map(&:list).each_with_object([]) do |skill, result|
|
|
49
|
+
next if seen[skill.name]
|
|
50
|
+
|
|
51
|
+
seen[skill.name] = true
|
|
52
|
+
result << skill
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|