Class: RailsConsoleAi::SkillLoader
- Inherits:
-
Object
- Object
- RailsConsoleAi::SkillLoader
- Defined in:
- lib/rails_console_ai/skill_loader.rb
Constant Summary collapse
- SKILLS_DIR =
'skills'
Class Method Summary collapse
-
.parse(content) ⇒ Object
Public: parse a raw .md (YAML frontmatter + body) string into a hash.
Instance Method Summary collapse
-
#delete_skill(name:) ⇒ Object
Tries DB first, falls back to file.
-
#find_any_skill(name) ⇒ Object
UI-facing: includes proposed skills too, with name-collision DB wins.
- #find_skill(name) ⇒ Object
-
#initialize(storage = nil) ⇒ SkillLoader
constructor
A new instance of SkillLoader.
-
#load_activatable_skills ⇒ Object
Skills the AI is allowed to see / activate: approved DB skills + all file skills.
-
#load_all_skills ⇒ Object
Returns the union of DB-backed skills and file-backed skills.
-
#save_skill(name:, description:, body:, tags: [], bypass_guards_for_methods: [], target: :db, edited_by: nil, change_note: nil) ⇒ Object
target: :db (default) | :file Falls back to :file (with a notice in the return string) if DB tables aren’t set up.
- #skill_summaries ⇒ Object
Constructor Details
#initialize(storage = nil) ⇒ SkillLoader
Returns a new instance of SkillLoader.
8 9 10 |
# File 'lib/rails_console_ai/skill_loader.rb', line 8 def initialize(storage = nil) @storage = storage || RailsConsoleAi.storage end |
Class Method Details
.parse(content) ⇒ Object
Public: parse a raw .md (YAML frontmatter + body) string into a hash. Returns nil for content that doesn’t have valid frontmatter so the caller can show a clear error instead of producing a half-formed record.
185 186 187 188 189 190 191 192 |
# File 'lib/rails_console_ai/skill_loader.rb', line 185 def self.parse(content) return nil unless content =~ /\A---\s*\n(.*?\n)---\s*\n(.*)/m frontmatter = YAML.safe_load($1, permitted_classes: [Time, Date]) || {} body = $2.strip frontmatter.merge('body' => body) rescue Psych::SyntaxError nil end |
Instance Method Details
#delete_skill(name:) ⇒ Object
Tries DB first, falls back to file. Reports which source it removed from.
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/rails_console_ai/skill_loader.rb', line 100 def delete_skill(name:) if Storage::DatabaseStorage.delete_skill_by_name(name) return "Skill deleted (db): \"#{name}\"" end key = skill_key(name) unless @storage.exists?(key) found = safe_load_file_skills.find { |s| s['name'].to_s.downcase == name.to_s.downcase } return "No skill found: \"#{name}\"" unless found key = skill_key(found['name']) end skill = load_skill_file(key) @storage.delete(key) "Skill deleted: \"#{skill ? skill['name'] : name}\"" rescue Storage::StorageError => e "FAILED to delete skill (#{e.})." end |
#find_any_skill(name) ⇒ Object
UI-facing: includes proposed skills too, with name-collision DB wins.
53 54 55 |
# File 'lib/rails_console_ai/skill_loader.rb', line 53 def find_any_skill(name) load_all_skills.find { |s| s['name'].to_s.downcase == name.to_s.downcase } end |
#find_skill(name) ⇒ Object
48 49 50 |
# File 'lib/rails_console_ai/skill_loader.rb', line 48 def find_skill(name) load_activatable_skills.find { |s| s['name'].to_s.downcase == name.to_s.downcase } end |
#load_activatable_skills ⇒ Object
Skills the AI is allowed to see / activate: approved DB skills + all file skills. File skills are considered pre-approved because they’re git-tracked.
31 32 33 34 35 |
# File 'lib/rails_console_ai/skill_loader.rb', line 31 def load_activatable_skills # Use string literal so this doesn't require the AR model to be autoloaded # in environments that don't reference it. load_all_skills.reject { |s| s['source'] == :db && s['status'] != 'approved' } end |
#load_all_skills ⇒ Object
Returns the union of DB-backed skills and file-backed skills. When the same name appears in both, the DB record wins and the file record is shadowed (but the file isn’t touched).
Includes proposed (unapproved) DB skills — they show up in the admin UI with a “PROPOSED” badge. The AI-facing surface (#skill_summaries, #find_skill) filters them out, so an unapproved skill can never be activated.
19 20 21 22 23 24 25 26 27 |
# File 'lib/rails_console_ai/skill_loader.rb', line 19 def load_all_skills db = safe_load_db_skills file = safe_load_file_skills names = db.map { |s| s['name'].to_s.downcase } file.reject! { |s| names.include?(s['name'].to_s.downcase) } (db + file).sort_by { |s| s['name'].to_s.downcase } end |
#save_skill(name:, description:, body:, tags: [], bypass_guards_for_methods: [], target: :db, edited_by: nil, change_note: nil) ⇒ Object
target: :db (default) | :file Falls back to :file (with a notice in the return string) if DB tables aren’t set up.
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/rails_console_ai/skill_loader.rb', line 59 def save_skill(name:, description:, body:, tags: [], bypass_guards_for_methods: [], target: :db, edited_by: nil, change_note: nil) target = (target || :db).to_sym db_fell_back = false if target == :db && !Storage::DatabaseStorage.available? target = :file db_fell_back = true end if target == :file result = save_skill_to_file( name: name, description: description, body: body, tags: , bypass_guards_for_methods: bypass_guards_for_methods ) if db_fell_back result += "\nNOTE: DB storage was requested but the rails_console_ai_skills table does not exist. " \ "Run `ai_db_setup` in your Rails console to enable the versioned DB store. " \ "Saved to a file instead." end result else record, was_new = Storage::DatabaseStorage.save_skill( name: name, description: description, body: body, tags: , bypass_guards_for_methods: bypass_guards_for_methods, edited_by: edited_by || 'ai', change_note: change_note ) status_note = if record.respond_to?(:proposed?) && record.proposed? ' — status: PROPOSED. A human must approve it at /rails_console_ai/skills before it can be activated.' else '' end if was_new "Skill created (db): \"#{record.name}\" (id=#{record.id})#{status_note}" else "Skill updated (db): \"#{record.name}\" (id=#{record.id})#{status_note}" end end rescue Storage::StorageError, ::ActiveRecord::RecordInvalid => e "FAILED to save skill (#{e.})." end |
#skill_summaries ⇒ Object
37 38 39 40 41 42 43 44 45 46 |
# File 'lib/rails_console_ai/skill_loader.rb', line 37 def skill_summaries skills = load_activatable_skills return nil if skills.empty? skills.map { |s| = Array(s['tags']) tag_str = .empty? ? '' : " [#{.join(', ')}]" "- **#{s['name']}**#{tag_str}: #{s['description']}" } end |