Class: RailsConsoleAi::AgentLoader

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_console_ai/agent_loader.rb

Constant Summary collapse

AGENTS_DIR =
'agents'
BUILTIN_DIR =
File.expand_path('../agents', __FILE__)

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(storage = nil) ⇒ AgentLoader

Returns a new instance of AgentLoader.



9
10
11
# File 'lib/rails_console_ai/agent_loader.rb', line 9

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.



205
206
207
208
209
210
211
212
# File 'lib/rails_console_ai/agent_loader.rb', line 205

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

#agent_summariesObject



37
38
39
40
41
42
43
44
# File 'lib/rails_console_ai/agent_loader.rb', line 37

def agent_summaries
  agents = load_activatable_agents
  return nil if agents.empty?

  agents.map { |a|
    "- **#{a['name']}**: #{a['description']}"
  }
end

#delete_agent(name:) ⇒ Object

Tries DB first, then file. Built-in agents can’t be deleted.



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/rails_console_ai/agent_loader.rb', line 99

def delete_agent(name:)
  if Storage::DatabaseStorage.delete_agent_by_name(name)
    return "Agent deleted (db): \"#{name}\""
  end

  # Built-in agents are gem-shipped and not deletable.
  builtin = safe_load_builtin_agents.find { |a| a['name'].to_s.downcase == name.to_s.downcase }
  if builtin
    return "Cannot delete built-in agent \"#{builtin['name']}\". Built-in agents ship with the gem. " \
           "Create a same-named DB agent to override it instead."
  end

  key = agent_key(name)
  unless @storage.exists?(key)
    found = safe_load_file_agents.find { |a| a['name'].to_s.downcase == name.to_s.downcase }
    return "No agent found: \"#{name}\"" unless found
    key = agent_key(found['name'])
  end

  agent = load_agent_file(key)
  @storage.delete(key)
  "Agent deleted: \"#{agent ? agent['name'] : name}\""
rescue Storage::StorageError => e
  "FAILED to delete agent (#{e.message})."
end

#find_agent(name) ⇒ Object

AI-facing — returns nil for proposed DB agents so delegate_task can’t reach them.



47
48
49
# File 'lib/rails_console_ai/agent_loader.rb', line 47

def find_agent(name)
  load_activatable_agents.find { |a| a['name'].to_s.downcase == name.to_s.downcase }
end

#find_any_agent(name) ⇒ Object

UI-facing — includes proposed DB agents.



52
53
54
# File 'lib/rails_console_ai/agent_loader.rb', line 52

def find_any_agent(name)
  load_all_agents.find { |a| a['name'].to_s.downcase == name.to_s.downcase }
end

#load_activatable_agentsObject

AI-facing: hides proposed DB agents. File + built-in are pre-approved.



33
34
35
# File 'lib/rails_console_ai/agent_loader.rb', line 33

def load_activatable_agents
  load_all_agents.reject { |a| a['source'] == :db && a['status'] != 'approved' }
end

#load_all_agentsObject

Three-source union: DB > file > built-in. Each record is tagged with ‘source: :db | :file | :builtin`. DB records also carry `status`, `approved_by`, `approved_at`. Proposed (unapproved) DB agents are surfaced here so the admin UI can render them — use #load_activatable_agents on AI-facing paths to filter them out.



18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/rails_console_ai/agent_loader.rb', line 18

def load_all_agents
  db      = safe_load_db_agents
  file    = safe_load_file_agents
  builtin = safe_load_builtin_agents

  db_names = db.map { |a| a['name'].to_s.downcase }
  file.reject! { |a| db_names.include?(a['name'].to_s.downcase) }

  file_names = file.map { |a| a['name'].to_s.downcase }
  builtin.reject! { |a| db_names.include?(a['name'].to_s.downcase) || file_names.include?(a['name'].to_s.downcase) }

  (db + file + builtin).sort_by { |a| a['name'].to_s.downcase }
end

#save_agent(name:, description:, body:, max_rounds: nil, model: nil, tools: nil, 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.



58
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
# File 'lib/rails_console_ai/agent_loader.rb', line 58

def save_agent(name:, description:, body:, max_rounds: nil, model: nil, tools: nil, target: :db, edited_by: nil, change_note: nil)
  target = (target || :db).to_sym
  db_fell_back = false
  if target == :db && !Storage::DatabaseStorage.agents_available?
    target = :file
    db_fell_back = true
  end

  if target == :file
    result = save_agent_to_file(
      name: name, description: description, body: body,
      max_rounds: max_rounds, model: model, tools: tools
    )
    if db_fell_back
      result += "\nNOTE: DB storage was requested but the rails_console_ai_agents 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_agent(
      name: name, description: description, body: body,
      max_rounds: max_rounds, model: model, tools: Array(tools),
      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/agents before delegate_task can invoke it.'
                  else
                    ''
                  end
    if was_new
      "Agent created (db): \"#{record.name}\" (id=#{record.id})#{status_note}"
    else
      "Agent updated (db): \"#{record.name}\" (id=#{record.id})#{status_note}"
    end
  end
rescue Storage::StorageError, ::ActiveRecord::RecordInvalid => e
  "FAILED to save agent (#{e.message})."
end