ask-rails 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '081f609a014a72157291794a40c580ed6fabb625c2db0c7511538acae540ea9b'
4
+ data.tar.gz: 1445552073a10f1fbae78f63d7220c894efbcb1cd6433f7aef8f977e4374b1e6
5
+ SHA512:
6
+ metadata.gz: c0fcb6d2a6f53c8fa89dafd2ce2d6f3393c305a0df0cbfe67472798a9973032645e81ccec5517a855bba25e90572ac5c1100ba4d73b5e98e70ab329629e1ad99
7
+ data.tar.gz: 5227ec0290f2177f844922038439c99b7bfd8d0d35360b7256206825f220b0323d8d139117000169336f50242191b200f0896a79379d8769996aede9b2f12d36
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kaka Ruto
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # ask-rails
2
+
3
+ Rails integration for the ask-rb ecosystem. The only gem a Rails app needs to join
4
+ the ask-rb stack — provides a Railtie, AR session persistence, a session factory,
5
+ automatic service gem discovery, and generators.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ bundle add ask-rails
11
+ rails generate ask_rails:install
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```ruby
17
+ # In any Rails context
18
+ session = Ask::Rails.agent_session
19
+ session.run("Find all open issues labeled 'bug' in our repo")
20
+ ```
21
+
22
+ Service gems like `ask-github`, `ask-slack` are auto-discovered — the agent gets their
23
+ context (auth info, quick-start snippets, error guides) in the system prompt automatically.
24
+
25
+ ## Development
26
+
27
+ ```bash
28
+ bin/setup
29
+ bundle exec rake test
30
+ ```
31
+
32
+ ## License
33
+
34
+ MIT
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,43 @@
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 || default_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 default_model_class
31
+ # Lazy reference so the model class doesn't need to exist at load time
32
+ ask_session_model = Class.new(::ActiveRecord::Base) do
33
+ self.table_name = "ask_sessions"
34
+ end
35
+ # Store it as a constant so it's reusable
36
+ unless Object.const_defined?(:AskSession)
37
+ Object.const_set(:AskSession, ask_session_model)
38
+ end
39
+ AskSession
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Rails
5
+ class Railtie < ::Rails::Railtie
6
+ rake_tasks do
7
+ # Load rake tasks if any
8
+ end
9
+
10
+ generators do
11
+ require_relative "../generators/ask/rails/install/install_generator"
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!
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
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.camelize.constantize
18
+ contexts << mod if mod.const_defined?(:DESCRIPTION)
19
+ rescue LoadError, NameError
20
+ # No context module for this gem
21
+ end
22
+ end
23
+
24
+ unless contexts.empty?
25
+ prompt = build_system_prompt(contexts)
26
+ existing = Ask::Rails.configuration.system_prompt
27
+ Ask::Rails.configuration.system_prompt = [existing, prompt].compact.join("\n\n")
28
+ end
29
+
30
+ contexts
31
+ end
32
+
33
+ def build_system_prompt(contexts)
34
+ sections = ["## Available Services"]
35
+
36
+ contexts.each do |mod|
37
+ sections << "### #{mod.name.demodulize}"
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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Rails
5
+ class Tool < Ask::Tool
6
+ def rails_root
7
+ ::Rails.root
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,21 @@
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
@@ -0,0 +1,20 @@
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
@@ -0,0 +1,22 @@
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Rails
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
data/lib/ask/rails.rb ADDED
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "ask/agent"
5
+ require "ask/auth"
6
+
7
+ module Ask
8
+ 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
+ end
65
+ end
66
+
67
+ require_relative "rails/version"
68
+ require_relative "rails/configuration"
69
+ require_relative "rails/railtie"
70
+ require_relative "rails/persistence"
71
+ require_relative "rails/service_discovery"
72
+ require_relative "rails/tool"
73
+ require_relative "rails/tools/read_file"
74
+ require_relative "rails/tools/run_command"
75
+ require_relative "rails/tools/search_codebase"
76
+ require_relative "rails/tools/read_routes"
data/lib/ask-rails.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ask/rails"
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,5 @@
1
+ Ask::Rails.configure do |config|
2
+ config.default_model = ENV.fetch("ASK_DEFAULT_MODEL", "gpt-4o")
3
+ config.max_turns = ENV.fetch("ASK_MAX_TURNS", 25).to_i
4
+ # config.persistence_adapter = Ask::Rails::Persistence.new
5
+ end
@@ -0,0 +1,12 @@
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
metadata ADDED
@@ -0,0 +1,184 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ask-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kaka Ruto
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ask-tools
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
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.1'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ 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
+ - !ruby/object:Gem::Dependency
97
+ name: minitest
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '5.25'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '5.25'
110
+ - !ruby/object:Gem::Dependency
111
+ name: mocha
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '3.1'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '3.1'
124
+ - !ruby/object:Gem::Dependency
125
+ name: rake
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '13.0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '13.0'
138
+ description: Railtie, AR session persistence, session factory, service gem discovery,
139
+ and generators.
140
+ email:
141
+ - kaka@myrrlabs.com
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - LICENSE
147
+ - README.md
148
+ - lib/ask-rails.rb
149
+ - lib/ask/rails.rb
150
+ - lib/ask/rails/configuration.rb
151
+ - lib/ask/rails/persistence.rb
152
+ - lib/ask/rails/railtie.rb
153
+ - lib/ask/rails/service_discovery.rb
154
+ - lib/ask/rails/tool.rb
155
+ - lib/ask/rails/tools/read_file.rb
156
+ - lib/ask/rails/tools/read_routes.rb
157
+ - lib/ask/rails/tools/run_command.rb
158
+ - lib/ask/rails/tools/search_codebase.rb
159
+ - lib/ask/rails/version.rb
160
+ - lib/generators/ask/rails/install/install_generator.rb
161
+ - lib/generators/ask/rails/install/templates/initializer.rb
162
+ - lib/generators/ask/rails/install/templates/migration.rb
163
+ homepage: https://github.com/ask-rb/ask-rails
164
+ licenses:
165
+ - MIT
166
+ metadata: {}
167
+ rdoc_options: []
168
+ require_paths:
169
+ - lib
170
+ required_ruby_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '3.2'
175
+ required_rubygems_version: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ requirements: []
181
+ rubygems_version: 4.0.3
182
+ specification_version: 4
183
+ summary: Rails integration for the ask-rb ecosystem
184
+ test_files: []