ask-rails 0.2.4 → 0.4.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 +7 -0
- data/README.md +38 -13
- data/lib/ask/rails/generators/install/install_generator.rb +28 -0
- data/lib/ask/rails/generators/install/templates/application_agent.rb +26 -0
- data/lib/ask/rails/generators/install/templates/initializer.rb +15 -0
- data/lib/ask/rails/railtie.rb +1 -18
- data/lib/ask/rails/version.rb +1 -1
- data/lib/ask/rails.rb +1 -70
- metadata +12 -79
- data/lib/ask/rails/configuration.rb +0 -19
- data/lib/ask/rails/engine.rb +0 -11
- data/lib/ask/rails/persistence.rb +0 -35
- data/lib/ask/rails/service_discovery.rb +0 -48
- data/lib/ask/rails/tool.rb +0 -11
- data/lib/ask/rails/tools/query_database.rb +0 -72
- data/lib/ask/rails/tools/read_file.rb +0 -24
- data/lib/ask/rails/tools/read_log.rb +0 -118
- data/lib/ask/rails/tools/read_model.rb +0 -73
- data/lib/ask/rails/tools/read_routes.rb +0 -21
- data/lib/ask/rails/tools/run_command.rb +0 -20
- data/lib/ask/rails/tools/search_codebase.rb +0 -22
- data/lib/ask/skills/rails.db_debug/SKILL.md +0 -118
- data/lib/ask/skills/rails.deploy_pipeline/SKILL.md +0 -125
- data/lib/ask/skills/rails.route_trouble/SKILL.md +0 -127
- data/lib/generators/ask/rails/install/install_generator.rb +0 -29
- data/lib/generators/ask/rails/install/templates/initializer.rb +0 -5
- data/lib/generators/ask/rails/install/templates/migration.rb +0 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1a8258ca96d9513529bc1c0c9c54bd8e2492a110e48d90c9af3f53ca3aa24d9f
|
|
4
|
+
data.tar.gz: 4a776f46230d7a87719d5a6e12154e90987f71f60af63707da686ba2cce20377
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 894f5146923889fd014f1ac770c52eba2cfdc6d84fd546062c05ac85473f6e716e70a298761f25a19ea1b00ba60461aae3ab38889c05a95698d891320879cc86
|
|
7
|
+
data.tar.gz: d58b94569c064b58493bd0a929cfb1dde3310a43c110c92474947c57e711f5a4e6d57ca596c389e73fcb4ee6bddeff7e8797bc5915ca8e4cbf94eadaf5f2353d
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## [0.1.0] — 2026-07-26
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- Initial release of `ask-rails` — Rails integration for the ask-rb ecosystem.
|
|
6
|
+
- **Install generator** — `rails generate ask:install` creates `config/initializers/ask.rb`, `app/agents/application_agent.rb`, and `app/agents/` directory.
|
|
7
|
+
- **Railtie** — Wires `Ask::Agent.logger` to `Rails.logger`, discovers agent definitions in `app/agents/`.
|
data/README.md
CHANGED
|
@@ -1,34 +1,59 @@
|
|
|
1
1
|
# ask-rails
|
|
2
2
|
|
|
3
|
-
Rails integration for the ask-rb ecosystem.
|
|
4
|
-
the ask-rb stack — provides a Railtie, AR session persistence, a session factory,
|
|
5
|
-
automatic service gem discovery, and generators.
|
|
3
|
+
Rails integration for the [ask-rb](https://github.com/ask-rb) ecosystem. Provides generators, file conventions, and railtie for using AI agents in your Rails app.
|
|
6
4
|
|
|
7
5
|
## Installation
|
|
8
6
|
|
|
9
7
|
```bash
|
|
10
8
|
bundle add ask-rails
|
|
11
|
-
rails generate
|
|
9
|
+
rails generate ask:install
|
|
12
10
|
```
|
|
13
11
|
|
|
12
|
+
This creates:
|
|
13
|
+
- `config/initializers/ask.rb` — agent configuration
|
|
14
|
+
- `app/agents/application_agent.rb` — base class for your agents
|
|
15
|
+
- `app/agents/` — directory for agent definitions
|
|
16
|
+
|
|
14
17
|
## Usage
|
|
15
18
|
|
|
19
|
+
Define an agent:
|
|
20
|
+
|
|
16
21
|
```ruby
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
# app/agents/support_bot.rb
|
|
23
|
+
class Agents::SupportBot < ApplicationAgent
|
|
24
|
+
model "gpt-4o"
|
|
25
|
+
system_prompt "You help users with support questions."
|
|
26
|
+
|
|
27
|
+
tool :bash
|
|
28
|
+
tool :read
|
|
29
|
+
tool :grep
|
|
30
|
+
end
|
|
20
31
|
```
|
|
21
32
|
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
Run it:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
agent = Ask::Agent.new("support_bot")
|
|
37
|
+
response = agent.run("Find all open issues in the codebase")
|
|
38
|
+
puts response
|
|
39
|
+
```
|
|
24
40
|
|
|
25
|
-
|
|
41
|
+
Or use `Ask.chat` for one-off conversations:
|
|
26
42
|
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
bundle exec rake test
|
|
43
|
+
```ruby
|
|
44
|
+
Ask.chat("Summarize this article: #{text}")
|
|
30
45
|
```
|
|
31
46
|
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
API keys are resolved automatically by `Ask::Auth`:
|
|
50
|
+
|
|
51
|
+
- **Environment variables:** `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.
|
|
52
|
+
- **Rails credentials:** `rails credentials:edit` → add `ask.openai`, `ask.anthropic`
|
|
53
|
+
- **File:** `~/.ask/credentials.yml`
|
|
54
|
+
|
|
55
|
+
No manual credential setup is required.
|
|
56
|
+
|
|
32
57
|
## License
|
|
33
58
|
|
|
34
59
|
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Ask
|
|
6
|
+
module Rails
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
desc "Sets up ask-rb for Rails — creates initializer and app/agents/ directory"
|
|
12
|
+
|
|
13
|
+
def create_initializer
|
|
14
|
+
template "initializer.rb", "config/initializers/ask.rb"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create_application_agent
|
|
18
|
+
template "application_agent.rb", "app/agents/application_agent.rb"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create_agents_directory
|
|
22
|
+
empty_directory "app/agents"
|
|
23
|
+
create_file "app/agents/.keep", "" unless options[:skip_keep]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Base class for your application's AI agents.
|
|
4
|
+
#
|
|
5
|
+
# Subclass this to define agents:
|
|
6
|
+
#
|
|
7
|
+
# class Agents::SupportBot < ApplicationAgent
|
|
8
|
+
# model "gpt-4o"
|
|
9
|
+
# system_prompt "You help users with support questions."
|
|
10
|
+
#
|
|
11
|
+
# tool :search_knowledge_base
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# Then run:
|
|
15
|
+
# agent = Ask::Agent.new("support_bot")
|
|
16
|
+
# agent.run("How do I reset my password?")
|
|
17
|
+
#
|
|
18
|
+
class ApplicationAgent < Ask::Agent::Definition
|
|
19
|
+
# Default instructions — override in subclasses
|
|
20
|
+
system_prompt "You are a helpful assistant."
|
|
21
|
+
|
|
22
|
+
# Uncomment to add built-in tools:
|
|
23
|
+
# tool :bash
|
|
24
|
+
# tool :read
|
|
25
|
+
# tool :grep
|
|
26
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ask Agent Configuration
|
|
4
|
+
# ========================
|
|
5
|
+
#
|
|
6
|
+
# API keys are resolved automatically by Ask::Auth:
|
|
7
|
+
# - Environment variables: OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.
|
|
8
|
+
# - Rails credentials: rails credentials:edit → ask.openai, ask.anthropic
|
|
9
|
+
# - ~/.ask/credentials.yml
|
|
10
|
+
#
|
|
11
|
+
# See https://github.com/ask-rb/ask-auth for details.
|
|
12
|
+
|
|
13
|
+
Ask::Agent.configure do |config|
|
|
14
|
+
# config.default_model = "gpt-4o"
|
|
15
|
+
end
|
data/lib/ask/rails/railtie.rb
CHANGED
|
@@ -3,25 +3,8 @@
|
|
|
3
3
|
module Ask
|
|
4
4
|
module Rails
|
|
5
5
|
class Railtie < ::Rails::Railtie
|
|
6
|
-
rake_tasks do
|
|
7
|
-
# Load rake tasks if any
|
|
8
|
-
end
|
|
9
|
-
|
|
10
6
|
generators do
|
|
11
|
-
require_relative "
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
initializer "ask_rails.configure" do |app|
|
|
15
|
-
Ask::Rails.configuration.default_model ||= ENV["ASK_DEFAULT_MODEL"] || "gpt-4o"
|
|
16
|
-
Ask::Rails.configuration.max_turns ||= (ENV["ASK_MAX_TURNS"] || 25).to_i
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
initializer "ask_rails.discover_tools", after: :eager_load_most do
|
|
20
|
-
Ask::Rails.discover_tools!
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
initializer "ask_rails.discover_services", after: :eager_load_most do
|
|
24
|
-
Ask::Rails::ServiceDiscovery.discover!
|
|
7
|
+
require_relative "generators/install/install_generator"
|
|
25
8
|
end
|
|
26
9
|
end
|
|
27
10
|
end
|
data/lib/ask/rails/version.rb
CHANGED
data/lib/ask/rails.rb
CHANGED
|
@@ -1,80 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "rails"
|
|
4
3
|
require "ask/agent"
|
|
5
|
-
require "ask/auth"
|
|
6
4
|
|
|
7
5
|
module Ask
|
|
8
6
|
module Rails
|
|
9
|
-
class << self
|
|
10
|
-
def configure
|
|
11
|
-
yield configuration
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def configuration
|
|
15
|
-
@configuration ||= Configuration.new
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def agent_session(**extra)
|
|
19
|
-
tools = configuration.tools.map { |t| t.is_a?(Class) ? t.new : t }
|
|
20
|
-
prompt = extra.delete(:system_prompt) || configuration.system_prompt || default_system_prompt
|
|
21
|
-
|
|
22
|
-
Ask::Agent::Session.new(
|
|
23
|
-
model: configuration.default_model,
|
|
24
|
-
max_turns: configuration.max_turns,
|
|
25
|
-
system_prompt: prompt,
|
|
26
|
-
tools: tools,
|
|
27
|
-
persistence: configuration.persistence_adapter,
|
|
28
|
-
**extra
|
|
29
|
-
)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def discover_tools!
|
|
33
|
-
self.configuration.tools = Ask::Tools::Shell::TOOLS.map(&:new) + discovered_rails_tools
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def root
|
|
37
|
-
@root ||= Pathname.new(File.expand_path("..", __dir__))
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
def discovered_rails_tools
|
|
43
|
-
tools = []
|
|
44
|
-
files = Dir[::Rails.root.join("app", "tools", "*.rb")]
|
|
45
|
-
files.each do |f|
|
|
46
|
-
require f
|
|
47
|
-
klass = File.basename(f, ".rb").camelize.constantize rescue next
|
|
48
|
-
tools << klass if klass < Ask::Rails::Tool
|
|
49
|
-
end
|
|
50
|
-
tools
|
|
51
|
-
rescue
|
|
52
|
-
tools
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def default_system_prompt
|
|
56
|
-
<<~PROMPT
|
|
57
|
-
You are a Ruby on Rails software engineer.
|
|
58
|
-
You have direct access to the application's code, database, and runtime.
|
|
59
|
-
Use your tools to inspect and modify the codebase.
|
|
60
|
-
Once you have enough information, stop calling tools and give your answer.
|
|
61
|
-
PROMPT
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
7
|
end
|
|
65
8
|
end
|
|
66
9
|
|
|
67
10
|
require_relative "rails/version"
|
|
68
|
-
require_relative "rails/
|
|
69
|
-
require_relative "rails/configuration"
|
|
70
|
-
require_relative "rails/railtie"
|
|
71
|
-
require_relative "rails/persistence"
|
|
72
|
-
require_relative "rails/service_discovery"
|
|
73
|
-
require_relative "rails/tool"
|
|
74
|
-
require_relative "rails/tools/read_file"
|
|
75
|
-
require_relative "rails/tools/run_command"
|
|
76
|
-
require_relative "rails/tools/search_codebase"
|
|
77
|
-
require_relative "rails/tools/read_routes"
|
|
78
|
-
require_relative "rails/tools/query_database"
|
|
79
|
-
require_relative "rails/tools/read_model"
|
|
80
|
-
require_relative "rails/tools/read_log"
|
|
11
|
+
require_relative "rails/railtie" if defined?(::Rails::Railtie)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ask-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -24,21 +24,7 @@ dependencies:
|
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '7.1'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: ask-
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0.1'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0.1'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: ask-tools-shell
|
|
27
|
+
name: ask-agent
|
|
42
28
|
requirement: !ruby/object:Gem::Requirement
|
|
43
29
|
requirements:
|
|
44
30
|
- - ">="
|
|
@@ -51,48 +37,6 @@ dependencies:
|
|
|
51
37
|
- - ">="
|
|
52
38
|
- !ruby/object:Gem::Version
|
|
53
39
|
version: '0.1'
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: ask-agent
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - "~>"
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '0.1'
|
|
61
|
-
type: :runtime
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - "~>"
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '0.1'
|
|
68
|
-
- !ruby/object:Gem::Dependency
|
|
69
|
-
name: ask-auth
|
|
70
|
-
requirement: !ruby/object:Gem::Requirement
|
|
71
|
-
requirements:
|
|
72
|
-
- - "~>"
|
|
73
|
-
- !ruby/object:Gem::Version
|
|
74
|
-
version: '0.1'
|
|
75
|
-
type: :runtime
|
|
76
|
-
prerelease: false
|
|
77
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
-
requirements:
|
|
79
|
-
- - "~>"
|
|
80
|
-
- !ruby/object:Gem::Version
|
|
81
|
-
version: '0.1'
|
|
82
|
-
- !ruby/object:Gem::Dependency
|
|
83
|
-
name: sqlite3
|
|
84
|
-
requirement: !ruby/object:Gem::Requirement
|
|
85
|
-
requirements:
|
|
86
|
-
- - ">="
|
|
87
|
-
- !ruby/object:Gem::Version
|
|
88
|
-
version: '2.0'
|
|
89
|
-
type: :development
|
|
90
|
-
prerelease: false
|
|
91
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
-
requirements:
|
|
93
|
-
- - ">="
|
|
94
|
-
- !ruby/object:Gem::Version
|
|
95
|
-
version: '2.0'
|
|
96
40
|
- !ruby/object:Gem::Dependency
|
|
97
41
|
name: minitest
|
|
98
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -135,42 +79,31 @@ dependencies:
|
|
|
135
79
|
- - "~>"
|
|
136
80
|
- !ruby/object:Gem::Version
|
|
137
81
|
version: '13.0'
|
|
138
|
-
description:
|
|
139
|
-
|
|
82
|
+
description: Rails generators, file conventions, and railtie for using ask-agent in
|
|
83
|
+
Rails apps.
|
|
140
84
|
email:
|
|
141
85
|
- kaka@myrrlabs.com
|
|
142
86
|
executables: []
|
|
143
87
|
extensions: []
|
|
144
88
|
extra_rdoc_files: []
|
|
145
89
|
files:
|
|
90
|
+
- CHANGELOG.md
|
|
146
91
|
- LICENSE
|
|
147
92
|
- README.md
|
|
148
93
|
- lib/ask-rails.rb
|
|
149
94
|
- lib/ask/rails.rb
|
|
150
|
-
- lib/ask/rails/
|
|
151
|
-
- lib/ask/rails/
|
|
152
|
-
- lib/ask/rails/
|
|
95
|
+
- lib/ask/rails/generators/install/install_generator.rb
|
|
96
|
+
- lib/ask/rails/generators/install/templates/application_agent.rb
|
|
97
|
+
- lib/ask/rails/generators/install/templates/initializer.rb
|
|
153
98
|
- lib/ask/rails/railtie.rb
|
|
154
|
-
- lib/ask/rails/service_discovery.rb
|
|
155
|
-
- lib/ask/rails/tool.rb
|
|
156
|
-
- lib/ask/rails/tools/query_database.rb
|
|
157
|
-
- lib/ask/rails/tools/read_file.rb
|
|
158
|
-
- lib/ask/rails/tools/read_log.rb
|
|
159
|
-
- lib/ask/rails/tools/read_model.rb
|
|
160
|
-
- lib/ask/rails/tools/read_routes.rb
|
|
161
|
-
- lib/ask/rails/tools/run_command.rb
|
|
162
|
-
- lib/ask/rails/tools/search_codebase.rb
|
|
163
99
|
- lib/ask/rails/version.rb
|
|
164
|
-
- lib/ask/skills/rails.db_debug/SKILL.md
|
|
165
|
-
- lib/ask/skills/rails.deploy_pipeline/SKILL.md
|
|
166
|
-
- lib/ask/skills/rails.route_trouble/SKILL.md
|
|
167
|
-
- lib/generators/ask/rails/install/install_generator.rb
|
|
168
|
-
- lib/generators/ask/rails/install/templates/initializer.rb
|
|
169
|
-
- lib/generators/ask/rails/install/templates/migration.rb
|
|
170
100
|
homepage: https://github.com/ask-rb/ask-rails
|
|
171
101
|
licenses:
|
|
172
102
|
- MIT
|
|
173
|
-
metadata:
|
|
103
|
+
metadata:
|
|
104
|
+
homepage_uri: https://github.com/ask-rb/ask-rails
|
|
105
|
+
source_code_uri: https://github.com/ask-rb/ask-rails
|
|
106
|
+
changelog_uri: https://github.com/ask-rb/ask-rails/blob/main/CHANGELOG.md
|
|
174
107
|
rdoc_options: []
|
|
175
108
|
require_paths:
|
|
176
109
|
- lib
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ask
|
|
4
|
-
module Rails
|
|
5
|
-
class Configuration
|
|
6
|
-
attr_accessor :default_model, :max_turns, :system_prompt,
|
|
7
|
-
:tool_concurrency, :persistence_adapter, :tools
|
|
8
|
-
|
|
9
|
-
def initialize
|
|
10
|
-
@default_model = "gpt-4o"
|
|
11
|
-
@max_turns = 25
|
|
12
|
-
@system_prompt = nil
|
|
13
|
-
@tool_concurrency = 5
|
|
14
|
-
@persistence_adapter = nil
|
|
15
|
-
@tools = []
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
data/lib/ask/rails/engine.rb
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ask
|
|
4
|
-
module Rails
|
|
5
|
-
class Persistence
|
|
6
|
-
def initialize(model_class: nil)
|
|
7
|
-
@model_class = model_class
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def save(session_id, data)
|
|
11
|
-
record = model_class.find_or_initialize_by(session_id: session_id)
|
|
12
|
-
record.update!(data: data)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def load(session_id)
|
|
16
|
-
record = model_class.find_by(session_id: session_id)
|
|
17
|
-
record&.data
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def delete(session_id)
|
|
21
|
-
model_class.where(session_id: session_id).delete_all
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def list
|
|
25
|
-
model_class.pluck(:session_id)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def model_class
|
|
31
|
-
@model_class || (raise "No model class configured. Use Persistence.new(model_class: MyModel)")
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ask
|
|
4
|
-
module Rails
|
|
5
|
-
module ServiceDiscovery
|
|
6
|
-
SERVICE_GEMS_PATTERN = /\Aask-(?!tools|tools-shell|agent|rails|core|auth|schema|llm)/
|
|
7
|
-
|
|
8
|
-
module_function
|
|
9
|
-
|
|
10
|
-
def discover!
|
|
11
|
-
service_gems = Gem.loaded_specs.keys.select { |name| name.match?(SERVICE_GEMS_PATTERN) }
|
|
12
|
-
contexts = []
|
|
13
|
-
|
|
14
|
-
service_gems.each do |name|
|
|
15
|
-
begin
|
|
16
|
-
require "#{name.tr("-", "/")}/context"
|
|
17
|
-
mod = name.split("-").map(&:capitalize).join.constantize
|
|
18
|
-
contexts << mod if mod.respond_to?(:const_defined?) && mod.const_defined?(:DESCRIPTION)
|
|
19
|
-
rescue LoadError, NameError
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
unless contexts.empty?
|
|
24
|
-
prompt = build_system_prompt(contexts)
|
|
25
|
-
existing = Ask::Rails.configuration.system_prompt
|
|
26
|
-
Ask::Rails.configuration.system_prompt = [existing, prompt].compact.join("\n\n")
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
contexts
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def build_system_prompt(contexts)
|
|
33
|
-
sections = ["## Available Services"]
|
|
34
|
-
|
|
35
|
-
contexts.each do |mod|
|
|
36
|
-
name = mod.respond_to?(:name) ? mod.name.to_s.split("::").last || "Unknown" : "Unknown"
|
|
37
|
-
sections << "### #{name}"
|
|
38
|
-
sections << mod::DESCRIPTION if mod.const_defined?(:DESCRIPTION)
|
|
39
|
-
sections << "Documentation: #{mod::DOCS_URL}" if mod.const_defined?(:DOCS_URL)
|
|
40
|
-
sections << "Authentication: #{mod::AUTH_HOW}" if mod.const_defined?(:AUTH_HOW)
|
|
41
|
-
sections << ""
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
sections.join("\n")
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
data/lib/ask/rails/tool.rb
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ask
|
|
4
|
-
module Rails
|
|
5
|
-
module Tools
|
|
6
|
-
class QueryDatabase < Ask::Rails::Tool
|
|
7
|
-
description "Run a read-only SQL query against the application database. " \
|
|
8
|
-
"Returns columns and rows. Only SELECT queries are allowed in production."
|
|
9
|
-
|
|
10
|
-
param :sql, type: :string, desc: "SQL query (SELECT only in production)", required: true
|
|
11
|
-
param :limit, type: :integer, desc: "Max rows to return (default 50)", required: false
|
|
12
|
-
|
|
13
|
-
WRITE_STATEMENTS = /\A\s*(INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER|CREATE|GRANT|REVOKE)\b/i
|
|
14
|
-
|
|
15
|
-
def execute(sql:, limit: 50)
|
|
16
|
-
sql = sql.strip
|
|
17
|
-
|
|
18
|
-
if WRITE_STATEMENTS.match?(sql)
|
|
19
|
-
return Ask::Result.failure(
|
|
20
|
-
"Only SELECT queries are allowed. Write statements (#{sql.match(WRITE_STATEMENTS)[1]}) are rejected in all environments."
|
|
21
|
-
)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
if ::Rails.env.production? && !sql.match?(/\A\s*SELECT\b/i)
|
|
25
|
-
return Ask::Result.failure(
|
|
26
|
-
"Only SELECT queries are allowed in the production environment."
|
|
27
|
-
)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
pool = ActiveRecord::Base.connection_pool
|
|
31
|
-
pool.with_connection do |conn|
|
|
32
|
-
limited_sql = sql.match?(/\bLIMIT\b/i) ? sql : "#{sql.chomp(';')} LIMIT #{limit.to_i}"
|
|
33
|
-
result = conn.exec_query(limited_sql)
|
|
34
|
-
columns = result.columns
|
|
35
|
-
rows = result.rows.first(limit.to_i).map { |row| build_row(row, columns) }
|
|
36
|
-
{
|
|
37
|
-
columns: columns,
|
|
38
|
-
rows: rows,
|
|
39
|
-
count: rows.size,
|
|
40
|
-
truncated: result.rows.size > limit.to_i
|
|
41
|
-
}
|
|
42
|
-
end
|
|
43
|
-
rescue ActiveRecord::StatementInvalid => e
|
|
44
|
-
Ask::Result.failure("SQL error: #{e.message}")
|
|
45
|
-
rescue ActiveRecord::ConnectionNotEstablished => e
|
|
46
|
-
Ask::Result.failure("Database not connected: #{e.message}. Verify the database is running and Rails is connected.")
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
def build_row(row, columns)
|
|
52
|
-
columns.each_with_index.each_with_object({}) do |(col, i), hash|
|
|
53
|
-
value = row[i]
|
|
54
|
-
hash[col] = sanitize_value(value)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def sanitize_value(value)
|
|
59
|
-
return "[BINARY DATA]" if binary_value?(value)
|
|
60
|
-
return value.iso8601 if value.respond_to?(:iso8601)
|
|
61
|
-
value
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def binary_value?(value)
|
|
65
|
-
value.is_a?(String) && value.encoding == Encoding::ASCII_8BIT && value.bytesize > 0
|
|
66
|
-
rescue
|
|
67
|
-
false
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ask
|
|
4
|
-
module Rails
|
|
5
|
-
module Tools
|
|
6
|
-
class ReadFile < Ask::Rails::Tool
|
|
7
|
-
description "Read a file from the Rails app. Paths are relative to Rails.root."
|
|
8
|
-
|
|
9
|
-
param :path, type: :string, desc: "Relative path from Rails.root", required: true
|
|
10
|
-
|
|
11
|
-
def execute(path:)
|
|
12
|
-
full_path = rails_root.join(path)
|
|
13
|
-
return Ask::Result.error(message: "File not found: #{path}") unless full_path.exist?
|
|
14
|
-
|
|
15
|
-
content = full_path.read
|
|
16
|
-
Ask::Result.success(
|
|
17
|
-
data: { path: path, content: content, size: content.length },
|
|
18
|
-
metadata: { path: path, size: content.length }
|
|
19
|
-
)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ask
|
|
4
|
-
module Rails
|
|
5
|
-
module Tools
|
|
6
|
-
class ReadLog < Ask::Rails::Tool
|
|
7
|
-
description "Read application log files with filtering. Supports Rails default " \
|
|
8
|
-
"logger and log rotation. Reads from the end of the file (most recent first)."
|
|
9
|
-
|
|
10
|
-
param :lines, type: :integer, desc: "Number of recent lines (default 50, max 500)", required: false
|
|
11
|
-
param :level, type: :string, desc: "Filter by level: ERROR, WARN, INFO, DEBUG", required: false
|
|
12
|
-
param :search, type: :string, desc: "Search term (plain text, case-insensitive)", required: false
|
|
13
|
-
param :file, type: :string, desc: "Log file name (default: log/<env>.log)", required: false
|
|
14
|
-
|
|
15
|
-
MAX_LINES = 500
|
|
16
|
-
LEVEL_PATTERNS = {
|
|
17
|
-
"ERROR" => /\bERROR\b/i,
|
|
18
|
-
"WARN" => /\bWARN\b/i,
|
|
19
|
-
"INFO" => /\bINFO\b/i,
|
|
20
|
-
"DEBUG" => /\bDEBUG\b/i
|
|
21
|
-
}.freeze
|
|
22
|
-
|
|
23
|
-
def execute(lines: 50, level: nil, search: nil, file: nil)
|
|
24
|
-
max_lines = [lines.to_i, MAX_LINES].min
|
|
25
|
-
log_path = resolve_log_path(file)
|
|
26
|
-
|
|
27
|
-
unless log_path.exist?
|
|
28
|
-
return Ask::Result.failure(
|
|
29
|
-
"Log file not found: #{log_path}. The application may not have written any logs yet."
|
|
30
|
-
)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
raw_lines = read_all_log_files(log_path)
|
|
34
|
-
return { lines: [], total_lines: 0, path: log_path.to_s } if raw_lines.empty?
|
|
35
|
-
|
|
36
|
-
filtered = apply_filters(raw_lines, level: level, search: search)
|
|
37
|
-
recent = filtered.last(max_lines).map(&:chomp)
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
lines: recent,
|
|
41
|
-
total_lines: raw_lines.size,
|
|
42
|
-
matched_lines: filtered.size,
|
|
43
|
-
path: log_path.to_s,
|
|
44
|
-
filters_applied: { level: level, search: search }.compact
|
|
45
|
-
}
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
private
|
|
49
|
-
|
|
50
|
-
def resolve_log_path(custom_path)
|
|
51
|
-
return rails_root.join(custom_path) if custom_path
|
|
52
|
-
rails_root.join("log", "#{Rails.env}.log")
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Read from rotated archives too: log/production.log, .1, .2.gz, etc.
|
|
56
|
-
def read_all_log_files(log_path)
|
|
57
|
-
all_content = +""
|
|
58
|
-
rotated_files(log_path).each do |path|
|
|
59
|
-
content = read_file_content(path)
|
|
60
|
-
all_content.prepend(content) if content
|
|
61
|
-
end
|
|
62
|
-
all_content.lines
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def rotated_files(log_path)
|
|
66
|
-
dir = log_path.dirname
|
|
67
|
-
base = log_path.basename.to_s
|
|
68
|
-
# Primary file, then rotated files in reverse order (oldest first, then primary last)
|
|
69
|
-
pattern = File.join(dir, "#{base}.*")
|
|
70
|
-
rotations = Dir[pattern].sort_by { |f| extract_rotation_number(f) }
|
|
71
|
-
# Primary file is read last (most recent)
|
|
72
|
-
rotations + [log_path.to_s]
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def extract_rotation_number(path)
|
|
76
|
-
File.basename(path).sub(/.*\.(\d+)(\.gz)?$/, '\1').to_i
|
|
77
|
-
rescue
|
|
78
|
-
0
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def read_file_content(path)
|
|
82
|
-
if path.to_s.end_with?(".gz")
|
|
83
|
-
Zlib::GzipReader.open(path.to_s) { |gz| gz.read }
|
|
84
|
-
else
|
|
85
|
-
File.read(path.to_s)
|
|
86
|
-
end
|
|
87
|
-
rescue => e
|
|
88
|
-
warn "[ReadLog] Could not read #{path}: #{e.message}"
|
|
89
|
-
nil
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def read_tail(path, max_bytes)
|
|
93
|
-
return "" unless path.exist?
|
|
94
|
-
|
|
95
|
-
size = path.size
|
|
96
|
-
return path.read if size <= max_bytes
|
|
97
|
-
|
|
98
|
-
File.open(path, "rb") do |f|
|
|
99
|
-
f.seek(-max_bytes, IO::SEEK_END)
|
|
100
|
-
partial = f.read(max_bytes)
|
|
101
|
-
if (idx = partial.index("\n"))
|
|
102
|
-
partial[idx + 1..]
|
|
103
|
-
else
|
|
104
|
-
partial
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def apply_filters(lines, level: nil, search: nil)
|
|
110
|
-
filtered = lines
|
|
111
|
-
filtered = filtered.select { |l| LEVEL_PATTERNS.fetch(level) { // }.match?(l) } if level
|
|
112
|
-
filtered = filtered.select { |l| l.downcase.include?(search.downcase) } if search
|
|
113
|
-
filtered
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ask
|
|
4
|
-
module Rails
|
|
5
|
-
module Tools
|
|
6
|
-
class ReadModel < Ask::Rails::Tool
|
|
7
|
-
description "Inspect an ActiveRecord model — columns, associations, validations, " \
|
|
8
|
-
"scopes, and indexes. Returns structured data the agent can act on."
|
|
9
|
-
|
|
10
|
-
param :name, type: :string, desc: "Model class name (e.g. 'User', 'Blog::Post')", required: true
|
|
11
|
-
param :detail, type: :string, desc: "Which details: 'all' (default), 'columns', 'associations', 'validations', 'scopes'", required: false
|
|
12
|
-
|
|
13
|
-
def execute(name:, detail: "all")
|
|
14
|
-
klass = safe_constantize(name)
|
|
15
|
-
return Ask::Result.failure("Model '#{name}' not found or is not an ActiveRecord model.") unless klass
|
|
16
|
-
|
|
17
|
-
result = { name: klass.name, table_name: klass.table_name }
|
|
18
|
-
|
|
19
|
-
result[:primary_key] = klass.primary_key if klass.respond_to?(:primary_key)
|
|
20
|
-
|
|
21
|
-
if %w[all columns].include?(detail)
|
|
22
|
-
result[:columns] = klass.columns.map { |c|
|
|
23
|
-
entry = { name: c.name, type: c.type, null: c.null, default: c.default }
|
|
24
|
-
entry[:primary_key] = true if c.name == klass.primary_key
|
|
25
|
-
entry
|
|
26
|
-
}
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
if %w[all associations].include?(detail)
|
|
30
|
-
result[:associations] = klass.reflect_on_all_associations.group_by(&:macro).transform_values { |refs|
|
|
31
|
-
refs.map { |a|
|
|
32
|
-
entry = { name: a.name, class_name: a.class_name }
|
|
33
|
-
entry[:through] = a.options[:through] if a.options[:through]
|
|
34
|
-
entry[:source] = a.options[:source] if a.options[:source]
|
|
35
|
-
entry[:foreign_key] = a.foreign_key if a.respond_to?(:foreign_key)
|
|
36
|
-
entry
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
if %w[all scopes].include?(detail) && klass.respond_to?(:all)
|
|
42
|
-
result[:scopes] = klass.methods(false)
|
|
43
|
-
.reject { |m| m.to_s.end_with?("=", "!", "?") || %i[new allocate inspect to_s].include?(m) }
|
|
44
|
-
.map(&:to_s).sort
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
if %w[all validations].include?(detail)
|
|
48
|
-
result[:validators] = klass.validators.map { |v|
|
|
49
|
-
{
|
|
50
|
-
attribute: v.attributes.first&.to_s,
|
|
51
|
-
kind: v.kind,
|
|
52
|
-
options: v.options.reject { |k, _| k == :if }
|
|
53
|
-
}
|
|
54
|
-
}.reject { |v| v[:attribute].nil? }
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
result
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
private
|
|
61
|
-
|
|
62
|
-
def safe_constantize(name)
|
|
63
|
-
klass = name.safe_constantize
|
|
64
|
-
return nil unless klass
|
|
65
|
-
return nil unless klass < ActiveRecord::Base
|
|
66
|
-
klass
|
|
67
|
-
rescue
|
|
68
|
-
nil
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ask
|
|
4
|
-
module Rails
|
|
5
|
-
module Tools
|
|
6
|
-
class ReadRoutes < Ask::Rails::Tool
|
|
7
|
-
description "Read the Rails routes from config/routes.rb"
|
|
8
|
-
def execute
|
|
9
|
-
routes_file = rails_root.join("config", "routes.rb")
|
|
10
|
-
return Ask::Result.error(message: "No routes file found") unless routes_file.exist?
|
|
11
|
-
|
|
12
|
-
content = routes_file.read
|
|
13
|
-
Ask::Result.success(
|
|
14
|
-
data: { content: content },
|
|
15
|
-
metadata: { size: content.length }
|
|
16
|
-
)
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ask
|
|
4
|
-
module Rails
|
|
5
|
-
module Tools
|
|
6
|
-
class RunCommand < Ask::Rails::Tool
|
|
7
|
-
description "Run a shell command in the Rails app root directory."
|
|
8
|
-
param :command, type: :string, desc: "Shell command to run", required: true
|
|
9
|
-
|
|
10
|
-
def execute(command:)
|
|
11
|
-
output = `cd #{rails_root} && #{command} 2>&1`
|
|
12
|
-
Ask::Result.success(
|
|
13
|
-
data: { output: output, exit_status: $?.exitstatus },
|
|
14
|
-
metadata: { exit_status: $?.exitstatus }
|
|
15
|
-
)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ask
|
|
4
|
-
module Rails
|
|
5
|
-
module Tools
|
|
6
|
-
class SearchCodebase < Ask::Rails::Tool
|
|
7
|
-
description "Search the Rails codebase with grep."
|
|
8
|
-
param :pattern, type: :string, desc: "Search pattern", required: true
|
|
9
|
-
param :path, type: :string, desc: "Subdirectory to search (optional)"
|
|
10
|
-
|
|
11
|
-
def execute(pattern:, path: nil)
|
|
12
|
-
search_path = path ? rails_root.join(path) : rails_root
|
|
13
|
-
results = `cd #{rails_root} && grep -rn '#{pattern}' #{search_path} 2>&1 | head -50`
|
|
14
|
-
Ask::Result.success(
|
|
15
|
-
data: { results: results, count: results.lines.count },
|
|
16
|
-
metadata: { pattern: pattern, count: results.lines.count }
|
|
17
|
-
)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: rails.db_debug
|
|
3
|
-
description: Step-by-step methodology for debugging database performance issues in Rails
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
Use this skill when investigating slow queries, N+1 problems, missing indexes, or
|
|
7
|
-
general database performance issues in a Rails application.
|
|
8
|
-
|
|
9
|
-
## Step 1: Identify the Slow Queries
|
|
10
|
-
|
|
11
|
-
Use `ReadLog.new.call(lines: 200, search: "SELECT")` to find recent database
|
|
12
|
-
queries in the application log. Look for:
|
|
13
|
-
|
|
14
|
-
- Queries taking > 100ms (look for "duration:" or "↳" markers in Rails logs)
|
|
15
|
-
- Repetitive queries with the same structure (potential N+1)
|
|
16
|
-
- Queries running in loops (same query, different IDs)
|
|
17
|
-
|
|
18
|
-
If you need more detail, narrow the search: `ReadLog.new.call(lines: 500, level: "WARN")`.
|
|
19
|
-
|
|
20
|
-
## Step 2: Understand the Schema
|
|
21
|
-
|
|
22
|
-
For each model involved in the slow queries, inspect it:
|
|
23
|
-
|
|
24
|
-
```ruby
|
|
25
|
-
ReadModel.new.call(name: "User")
|
|
26
|
-
ReadModel.new.call(name: "Post")
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Focus on:
|
|
30
|
-
- **Columns** — are there columns that look like foreign keys without indexes?
|
|
31
|
-
- **Associations** — what associations exist and what `class_name` do they use?
|
|
32
|
-
- **Validators** — any database-level constraints that could be missing?
|
|
33
|
-
|
|
34
|
-
Run `ReadModel.new.call(name: "User", detail: "columns")` if you only need columns.
|
|
35
|
-
|
|
36
|
-
## Step 3: Check for Missing Indexes
|
|
37
|
-
|
|
38
|
-
Query the database for actual indexes:
|
|
39
|
-
|
|
40
|
-
```ruby
|
|
41
|
-
QueryDatabase.new.call(
|
|
42
|
-
sql: "SELECT tablename, indexname, indexdef FROM pg_indexes WHERE schemaname = 'public' ORDER BY tablename, indexname",
|
|
43
|
-
limit: 200
|
|
44
|
-
)
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Look for:
|
|
48
|
-
- Foreign key columns that lack indexes (e.g. `user_id` without index)
|
|
49
|
-
- Columns used in `WHERE` clauses without indexes
|
|
50
|
-
- Composite indexes that could cover multiple query patterns
|
|
51
|
-
|
|
52
|
-
If an index is missing, check if adding one would help:
|
|
53
|
-
```ruby
|
|
54
|
-
QueryDatabase.new.call(
|
|
55
|
-
sql: "EXPLAIN ANALYZE SELECT * FROM posts WHERE user_id = 1",
|
|
56
|
-
limit: 10
|
|
57
|
-
)
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Step 4: Detect N+1 Queries
|
|
61
|
-
|
|
62
|
-
N+1 manifests as the same query repeated with different IDs:
|
|
63
|
-
|
|
64
|
-
```ruby
|
|
65
|
-
# Search for repetitive query patterns
|
|
66
|
-
ReadLog.new.call(lines: 500, search: "WHERE.*IN")
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Common N+1 patterns:
|
|
70
|
-
- Loading `Post.all` then accessing `post.comments` individually
|
|
71
|
-
- Loading `User.all` then accessing `user.profile` individually
|
|
72
|
-
- Serializing associations in views without eager loading
|
|
73
|
-
|
|
74
|
-
Fix with `.includes(:association)`, `.eager_load(:association)`, or
|
|
75
|
-
`.preload(:association)`.
|
|
76
|
-
|
|
77
|
-
## Step 5: Profile Slow Queries with EXPLAIN
|
|
78
|
-
|
|
79
|
-
For any identified slow query, get the execution plan:
|
|
80
|
-
|
|
81
|
-
```ruby
|
|
82
|
-
QueryDatabase.new.call(
|
|
83
|
-
sql: "EXPLAIN (ANALYZE, BUFFERS) SELECT posts.* FROM posts WHERE posts.user_id = 1 ORDER BY posts.created_at DESC LIMIT 20",
|
|
84
|
-
limit: 10
|
|
85
|
-
)
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
What to look for in explain output:
|
|
89
|
-
- **Sequential scans** (`Seq Scan`) — missing index
|
|
90
|
-
- **Sort operations** — missing index on sort column
|
|
91
|
-
- **Nested loop joins** with high row counts — missing composite index
|
|
92
|
-
- **Bitmap heap scans** with high costs — index may be suboptimal
|
|
93
|
-
|
|
94
|
-
If `ANALYZE` takes too long, use `EXPLAIN (BUFFERS)` without `ANALYZE` for cost estimates.
|
|
95
|
-
|
|
96
|
-
## Step 6: Check Bulk Loading and Batch Operations
|
|
97
|
-
|
|
98
|
-
If the issue involves importing or updating many records:
|
|
99
|
-
|
|
100
|
-
```ruby
|
|
101
|
-
QueryDatabase.new.call(
|
|
102
|
-
sql: "SELECT schemaname, tablename, seq_scan, seq_tup_read, idx_scan, idx_tup_fetch FROM pg_stat_user_tables ORDER BY seq_scan DESC",
|
|
103
|
-
limit: 20
|
|
104
|
-
)
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
High `seq_scan` counts compared to `idx_scan` indicate tables where indexes
|
|
108
|
-
are missing and sequential scans are happening frequently.
|
|
109
|
-
|
|
110
|
-
## Failure Mode Guide
|
|
111
|
-
|
|
112
|
-
| Symptom | Likely Cause | Action |
|
|
113
|
-
|---------|-------------|--------|
|
|
114
|
-
| Query takes >1s | Missing index on WHERE column | Check pg_indexes, add index |
|
|
115
|
-
| Same query 50x in log | N+1 in controller/view | Add `.includes()` |
|
|
116
|
-
| Query slow AFTER adding index | Wrong index type or order | Verify index column order matches query |
|
|
117
|
-
| `EXPLAIN ANALYZE` hanging | Table lock or very large table | Use `EXPLAIN (BUFFERS)` without ANALYZE |
|
|
118
|
-
| Missing `pg_indexes` output | Not Postgres | Use `SHOW INDEX FROM <table>` for MySQL |
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: rails.deploy_pipeline
|
|
3
|
-
description: Pre-deployment checklist for Rails applications — migrations, assets, credentials, jobs, and logs
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
Use this skill before or during a Rails deployment to ensure nothing is missed.
|
|
7
|
-
Follow the steps in order — each builds on the previous.
|
|
8
|
-
|
|
9
|
-
## Step 1: Check Pending Migrations
|
|
10
|
-
|
|
11
|
-
Before deploying, always check for unapplied migrations:
|
|
12
|
-
|
|
13
|
-
```ruby
|
|
14
|
-
RunCommand.new.call(command: "bin/rails db:migrate:status | grep 'down'")
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
If there are pending migrations:
|
|
18
|
-
1. Review them with `ReadFile.new.call(path: "db/migrate/<TIMESTAMP>_migration_name.rb")`
|
|
19
|
-
2. Verify they're reversible: check `change`, `up`/`down`, or `reversible` blocks
|
|
20
|
-
3. Check for data migrations (e.g., backfills) that should run separately
|
|
21
|
-
4. Estimate execution time — large tables may need locking strategy
|
|
22
|
-
|
|
23
|
-
## Step 2: Verify Assets Pipeline
|
|
24
|
-
|
|
25
|
-
For Rails with assets (Sprockets or Propshaft):
|
|
26
|
-
|
|
27
|
-
```ruby
|
|
28
|
-
# Precompile locally to catch errors early
|
|
29
|
-
RunCommand.new.call(command: "bin/rails assets:precompile 2>&1")
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Check for:
|
|
33
|
-
- Missing asset references (SCSS variables, image references)
|
|
34
|
-
- JavaScript compilation errors
|
|
35
|
-
- Asset fingerprint changes (manifold fingerprints if CSS changed)
|
|
36
|
-
|
|
37
|
-
For importmap or esbuild/vite setups:
|
|
38
|
-
|
|
39
|
-
```ruby
|
|
40
|
-
# Check build config
|
|
41
|
-
ReadFile.new.call(path: "package.json")
|
|
42
|
-
ReadFile.new.call(path: "vite.config.ts") if File.exist?("Rails.root.join('vite.config.ts')")
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Step 3: Review Credentials and Secrets
|
|
46
|
-
|
|
47
|
-
Verify that all required credentials exist in the target environment:
|
|
48
|
-
|
|
49
|
-
```ruby
|
|
50
|
-
# For production credentials
|
|
51
|
-
RunCommand.new.call(command: "bin/rails credentials:show --environment production 2>&1")
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Check for common secrets that may need updating:
|
|
55
|
-
- `secret_key_base`
|
|
56
|
-
- `database_password`
|
|
57
|
-
- Third-party API keys (AWS, Stripe, SendGrid, etc.)
|
|
58
|
-
- JWT signing keys
|
|
59
|
-
- Any env vars referenced in `config/` files that aren't in credentials
|
|
60
|
-
|
|
61
|
-
## Step 4: Check Background Jobs
|
|
62
|
-
|
|
63
|
-
Review sidekiq/active job configuration before deploy:
|
|
64
|
-
|
|
65
|
-
```ruby
|
|
66
|
-
# Check job files for any that need queue configuration
|
|
67
|
-
Glob.new.call(pattern: "app/jobs/**/*.rb")
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
For any new or modified jobs:
|
|
71
|
-
1. Verify the queue adapter is configured in `production.rb`
|
|
72
|
-
2. Check for job retry logic that might affect rollback
|
|
73
|
-
3. Review `ReadFile.new.call(path: "config/sidekiq.yml")` if using Sidekiq
|
|
74
|
-
4. Verify scheduled/cron jobs if using `sidekiq-cron` or `whenever`
|
|
75
|
-
|
|
76
|
-
## Step 5: Review Production Log for Pre-deploy Issues
|
|
77
|
-
|
|
78
|
-
Check that the current production environment is healthy before deploying:
|
|
79
|
-
|
|
80
|
-
```ruby
|
|
81
|
-
ReadLog.new.call(lines: 100, level: "ERROR")
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
If there are recent errors, investigate before deploying more changes.
|
|
85
|
-
|
|
86
|
-
## Step 6: Verify Dependencies and Gem Versions
|
|
87
|
-
|
|
88
|
-
Check for critical gem updates or changes:
|
|
89
|
-
|
|
90
|
-
```ruby
|
|
91
|
-
# Review Gemfile for new/modified gems
|
|
92
|
-
RunCommand.new.call(command: "git diff HEAD -- Gemfile")
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
If adding a gem that needs system dependencies or native extensions:
|
|
96
|
-
```ruby
|
|
97
|
-
RunCommand.new.call(command: "bundle platform")
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
## Step 7: Config File Checklist
|
|
101
|
-
|
|
102
|
-
Verify configuration files for the target environment:
|
|
103
|
-
|
|
104
|
-
```ruby
|
|
105
|
-
ReadFile.new.call(path: "config/environments/production.rb")
|
|
106
|
-
ReadFile.new.call(path: "config/database.yml")
|
|
107
|
-
ReadFile.new.call(path: "config/cable.yml") if File.exist?("config/cable.yml")
|
|
108
|
-
ReadFile.new.call(path: "config/storage.yml") if File.exist?("config/storage.yml")
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Key production checks:
|
|
112
|
-
- `config.force_ssl = true`
|
|
113
|
-
- `config.consider_all_requests_local = false`
|
|
114
|
-
- Proper cache store configured (`:mem_cache_store`, `:redis_cache_store`)
|
|
115
|
-
- Active Storage service configured for production
|
|
116
|
-
|
|
117
|
-
## Step 8: Quick Rollback Checklist
|
|
118
|
-
|
|
119
|
-
Before deploying, know how to roll back:
|
|
120
|
-
|
|
121
|
-
1. **Database**: `RunCommand.new.call(command: "bin/rails db:migrate:down VERSION=<previous>")`
|
|
122
|
-
2. **Code**: Git revert the deploy commit
|
|
123
|
-
3. **Assets**: Previous version's assets should still be cached
|
|
124
|
-
4. **Jobs**: Check if backward-incompatible changes won't replay failed jobs
|
|
125
|
-
5. **Feature flags**: If using flipper or similar, toggle off new features first
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: rails.route_trouble
|
|
3
|
-
description: Step-by-step methodology for debugging routing issues in Rails
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
Use this skill when a route is returning 404, you're getting route matching errors,
|
|
7
|
-
or routes aren't behaving as expected in a Rails application.
|
|
8
|
-
|
|
9
|
-
## Step 1: Read the Routes File
|
|
10
|
-
|
|
11
|
-
Start by examining the routes configuration:
|
|
12
|
-
|
|
13
|
-
```ruby
|
|
14
|
-
ReadRoutes.new.call
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
This reads `config/routes.rb` — the source of truth for all route definitions.
|
|
18
|
-
|
|
19
|
-
Look for:
|
|
20
|
-
- The overall structure (namespaced, nested, shallow routes?)
|
|
21
|
-
- Resources that might be missing
|
|
22
|
-
- Constraints or conditions that could block matching
|
|
23
|
-
- Route ordering (more specific routes should come before less specific)
|
|
24
|
-
|
|
25
|
-
## Step 2: Check the Compiled Routes
|
|
26
|
-
|
|
27
|
-
Rails routes have a specific matching order. Use `RunCommand` to inspect the live routes:
|
|
28
|
-
|
|
29
|
-
```ruby
|
|
30
|
-
RunCommand.new.call(command: "bin/rails routes")
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
If you know the path you're trying to match, grep for it:
|
|
34
|
-
|
|
35
|
-
```ruby
|
|
36
|
-
RunCommand.new.call(command: "bin/rails routes | grep users")
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
For a specific route name:
|
|
40
|
-
|
|
41
|
-
```ruby
|
|
42
|
-
RunCommand.new.call(command: "bin/rails routes --grep user_profile")
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Step 3: Check Route Parameters and Constraints
|
|
46
|
-
|
|
47
|
-
Routes often fail because of parameter mismatches or constraints. Check for:
|
|
48
|
-
|
|
49
|
-
**Required parameters:** Does the route define `:id` but the URL doesn't include it?
|
|
50
|
-
|
|
51
|
-
**Format constraints:** Routes with `:format` (like `.json`, `.html`) constraints
|
|
52
|
-
can fail silently. Check if there's a default format:
|
|
53
|
-
|
|
54
|
-
```ruby
|
|
55
|
-
# In routes.rb:
|
|
56
|
-
resources :posts, defaults: { format: :json }
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
**Constraint classes:** Custom route constraints like `subdomain` or request-based
|
|
60
|
-
constraints can prevent matching:
|
|
61
|
-
|
|
62
|
-
```ruby
|
|
63
|
-
# In routes.rb:
|
|
64
|
-
get "admin", to: "admin/dashboard#show", constraints: ->(req) { req.subdomain == "admin" }
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Check the constraints against the actual request parameters.
|
|
68
|
-
|
|
69
|
-
## Step 4: Trace the Match
|
|
70
|
-
|
|
71
|
-
For a failing request, trace how Rails would match it:
|
|
72
|
-
|
|
73
|
-
```ruby
|
|
74
|
-
RunCommand.new.call(
|
|
75
|
-
command: "bin/rails runner \"puts Rails.application.routes.recognize_path('/users/1/edit', method: :get)\""
|
|
76
|
-
)
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
This will raise `ActionController::RoutingError` if no route matches — the
|
|
80
|
-
error message tells you what routes were tried before failing.
|
|
81
|
-
|
|
82
|
-
If the route exists but the controller isn't found:
|
|
83
|
-
|
|
84
|
-
```ruby
|
|
85
|
-
ReadFile.new.call(path: "app/controllers/users_controller.rb")
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Step 5: Check Namespace and Module Nesting
|
|
89
|
-
|
|
90
|
-
Routes in namespaced controllers fail when the controller file is in the wrong
|
|
91
|
-
directory or the module is misnamed:
|
|
92
|
-
|
|
93
|
-
```ruby
|
|
94
|
-
# Route:
|
|
95
|
-
namespace :admin do
|
|
96
|
-
resources :users
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Expects:
|
|
100
|
-
# app/controllers/admin/users_controller.rb
|
|
101
|
-
# class Admin::UsersController < ApplicationController
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Use `Glob` to verify the controller exists:
|
|
105
|
-
|
|
106
|
-
```ruby
|
|
107
|
-
# Via RunCommand:
|
|
108
|
-
RunCommand.new.call(command: "ls app/controllers/admin/")
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
## Step 6: Verify Helper Paths and Named Routes
|
|
112
|
-
|
|
113
|
-
If you're debugging a `No route matches` error from a view (link_to, form_with):
|
|
114
|
-
|
|
115
|
-
1. Check the route name: `RunCommand.new.call(command: "bin/rails routes | grep user_path")`
|
|
116
|
-
2. Verify the route helper: `RunCommand.new.call(command: "bin/rails routes --grep user")`
|
|
117
|
-
3. Check for route helper overrides in custom constraints or defaults
|
|
118
|
-
|
|
119
|
-
## Failure Mode Guide
|
|
120
|
-
|
|
121
|
-
| Symptom | Likely Cause | Action |
|
|
122
|
-
|---------|-------------|--------|
|
|
123
|
-
| 404 for existing route | Route constraint blocking | Check `constraints:` block in routes.rb |
|
|
124
|
-
| `No route matches` | Missing resource definition | Add `resources :model_name` |
|
|
125
|
-
| Route works in dev, not prod | Different route ordering | Check routes file isn't conditionally loaded |
|
|
126
|
-
| `Missing controller` | Wrong namespace or filename | Verify file path matches module structure |
|
|
127
|
-
| Path helper not responding | Wrong route name or params mismatch | Check `bin/rails routes --grep helper_name` |
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "rails/generators"
|
|
4
|
-
|
|
5
|
-
module Ask
|
|
6
|
-
module Rails
|
|
7
|
-
module Generators
|
|
8
|
-
class InstallGenerator < ::Rails::Generators::Base
|
|
9
|
-
source_root File.expand_path("templates", __dir__)
|
|
10
|
-
|
|
11
|
-
desc "Creates ask-rails configuration and migration"
|
|
12
|
-
|
|
13
|
-
def create_initializer
|
|
14
|
-
template "initializer.rb", "config/initializers/ask_rails.rb"
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def create_migration
|
|
18
|
-
timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
19
|
-
template "migration.rb", "db/migrate/#{timestamp}_create_ask_sessions.rb"
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def create_tools_directory
|
|
23
|
-
empty_directory "app/tools"
|
|
24
|
-
create_file "app/tools/.keep", ""
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
class CreateAskSessions < ActiveRecord::Migration[7.1]
|
|
2
|
-
def change
|
|
3
|
-
create_table :ask_sessions do |t|
|
|
4
|
-
t.string :session_id, null: false
|
|
5
|
-
t.string :model
|
|
6
|
-
t.jsonb :data, default: {}
|
|
7
|
-
t.timestamps
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
add_index :ask_sessions, :session_id, unique: true
|
|
11
|
-
end
|
|
12
|
-
end
|