AsktiveRecord: A Ruby gem that lets your data answer like a human
AsktiveRecord is a Ruby gem designed to bridge the gap between human language and database queries. It lets you interact with your Rails database as if you were having a conversation with a knowledgeable assistant. Instead of writing SQL or chaining ActiveRecord methods, you simply ask questions in plain English (or any language)—like "Who are my newest users?" or "What products sold the most last month?"—and get clear, human-friendly answers. AsktiveRecord translates your questions into database queries using LLMs behind the scenes, so you can focus on what you want to know, not how to write the query.
Requirements
- Ruby >= 3.1.0
- Rails >= 7.0 (railties)
- An OpenAI API key (or a custom LLM adapter)
Installation
Add this line to your application's Gemfile:
gem 'asktive_record'And then execute:
$ bundle installGetting Started
Create configuration file:
$ bundle exec rails generate asktive_record:install
# It will create a new Rails initializer file at `config/initializers/asktive_record.rb`Check the config/initializers/asktive_record.rb file to configure your LLM provider and API key. By default, setup will generate and read the db/schema.rb (or db/structure.sql) to make the LLM aware of your database structure.
$ bundle exec rails generate asktive_record:setupThis command will generate and read the db/schema.rb (or db/structure.sql) and make the LLM aware of your database structure. You can change the schema file path and skip the dump schema setting in the config/initializers/asktive_record.rb file if you are using a custom schema file or a non-standard schema location for legacy databases.
See the Configuration section for more details.
# Include AsktiveRecord in your ApplicationRecord or specific models
class User < ApplicationRecord
include AsktiveRecord
end
# Now you can query any table through this service
query = User.ask("Show me the last five users who signed up")
# => Returns a Query object with SQL targeting the users table based on your schema. Does not execute the SQL yet.
# You can check the object with the generated SQL:
query.raw_sql
# => "SELECT * FROM users ORDER BY created_at DESC LIMIT 5"
# Call the execute method to run the query on the database
results = query.execute
# => Returns an array of User objects (if the query is a SELECT) or raises an `AsktiveRecord::QueryExecutionError` if the query fails.
# If you want to execute the query and get the response like human use the method answer
results = query.answer
# => Returns a string with the answer to the question, e.g., "The last five users who signed up are: [User1, User2, User3, User4, User5]"For more detailed usage instructions, see the Usage section below.
Features
- Natural Language to SQL: Convert human-readable questions into SQL queries.
- LLM Adapter Pattern: Pluggable architecture supporting OpenAI out of the box, with an extensible base for custom adapters (Anthropic, Gemini, local models, etc.).
-
Security First:
- SQL injection prevention via
SqlSanitizer(keyword blocklist + injection pattern detection) - Prompt injection prevention with input escaping and pattern filtering
- Read-only mode by default (only SELECT queries allowed)
- Defense-in-depth validation at multiple layers
- SQL injection prevention via
-
Get Answers, Not Just Data: Use the
.answermethod to get concise, human-readable responses to your queries, rather than raw data or SQL. - Avoid ActiveRecord Chaining and SQL: No need to write complex ActiveRecord queries or SQL statements. Just ask your question in natural language.
- Works with Multiple Languages: While the gem is designed with English in mind, it can handle queries in other languages, depending on the LLM's capabilities.
-
Flexible Querying Options:
- Use with specific models (e.g.,
User.ask("query")) - Use with service classes to query any table (e.g.,
AskService.ask("query"))
- Use with specific models (e.g.,
- Database Schema Awareness: Passes your database schema to the LLM for context-aware query generation.
- Developer Control: Provides a two-step query process: first, get the LLM-generated SQL, then sanitize and execute it, giving you full control over what runs against your database.
-
Smart Execution: Automatically uses the appropriate execution method (
find_by_sqlfor models,ActiveRecord::Base.connectionfor service classes). -
Structured Logging: Configurable logging with
[AsktiveRecord]prefix, defaults toRails.logger. - Easy Setup: Simple CLI commands to install and configure the gem in your Rails project.
- Customizable Configuration: Set your LLM provider, API keys, model preferences, temperature, and more through an initializer.
How It Works
-
Setup: You install the gem and run a setup command. This command reads your
db/schema.rb(ordb/structure.sql) and makes the LLM aware of your database structure. - Configuration: You configure your LLM API key and preferences in an initializer file.
-
Querying: You can query your database in two ways:
- Model-specific:
User.ask("your natural language query") - Service-based (any table):
AskService.ask("your natural language query")
- Model-specific:
- LLM Processing: AsktiveRecord sends your query and the relevant schema context to the configured LLM via the adapter.
- SQL Generation: The LLM returns a SQL query.
-
Safety First: The generated SQL is validated through multiple layers:
-
SqlSanitizerchecks for dangerous keywords and injection patterns - Read-only mode ensures only SELECT statements execute
- The
Queryobject lets you inspect the SQL before execution
-
-
Execution: The
executemethod runs the sanitized SQL. If the query originated from a model (likeUser.ask), it usesUser.find_by_sql. If from a service class (likeAskService.ask), it usesActiveRecord::Base.connection.
Configuration
After installing the gem, run the installer to generate the configuration file:
$ bundle exec rails generate asktive_record:installThis will create an initializer file at config/initializers/asktive_record.rb.
Configure AsktiveRecord
Open config/initializers/asktive_record.rb and configure your LLM provider and API key:
AsktiveRecord.configure do |config|
# === LLM Provider ===
# Specify the LLM provider to use. Default is :openai
# config.llm_provider = :openai
# === LLM API Key ===
# Set your API key for the chosen LLM provider.
# It is strongly recommended to use environment variables for sensitive data.
config.llm_api_key = ENV["OPENAI_API_KEY"]
# === LLM Model Name ===
# Specify the model name. Default is "gpt-4o-mini".
# Other models like "gpt-4o" or "gpt-4-turbo" can be used.
# config.llm_model_name = "gpt-4o-mini"
# === Database Schema Path ===
# Path to your schema file (schema.rb or structure.sql).
# Default is "db/schema.rb".
# config.db_schema_path = "db/schema.rb"
# === Skip dump schema ===
# If true, the schema will not be dumped during `asktive_record:setup`.
# config.skip_dump_schema = false
# === Read-Only Mode ===
# When true (default), only SELECT queries are allowed.
# config.read_only = true
# === LLM Temperature ===
# Controls randomness. Lower = more deterministic. Default: 0.2
# config.temperature = 0.2
# === LLM Max Tokens ===
# Maximum tokens in the LLM response. Default: 250
# config.max_tokens = 250
# === Custom Adapter ===
# Provide a custom LLM adapter instead of using the built-in provider.
# The adapter must inherit from AsktiveRecord::Adapters::Base.
# config.adapter = MyCustomAdapter.new(api_key: ENV["MY_LLM_KEY"])
# === Custom Logger ===
# Set a custom logger. Defaults to Rails.logger when available.
# config.logger = Logger.new($stdout)
endImportant: Securely manage your API keys. Using environment variables (e.g., ENV["OPENAI_API_KEY"]) is strongly recommended. Never commit API keys to source control.
Custom LLM Adapters
AsktiveRecord uses an adapter pattern for LLM communication. You can create custom adapters for any LLM provider:
class AnthropicAdapter < AsktiveRecord::Adapters::Base
def chat(prompt, options = {})
# Your Anthropic API call here
# Must return a string response (the SQL or answer text)
end
def default_model_name
"claude-sonnet-4-20250514"
end
end
# Use it in your configuration:
AsktiveRecord.configure do |config|
config.adapter = AnthropicAdapter.new(
api_key: ENV["ANTHROPIC_API_KEY"],
model_name: "claude-sonnet-4-20250514"
)
endPrepare Schema for LLM
Run the setup command to help AsktiveRecord understand your database structure:
$ bundle exec rails generate asktive_record:setupIf your app uses a custom schema file or a non-standard schema location, you can specify the path in your configuration:
AsktiveRecord.configure do |config|
config.db_schema_path = "db/custom_schema.rb"
config.skip_dump_schema = true
endThis ensures AsktiveRecord reads the correct schema file when providing context to the LLM.
Usage
AsktiveRecord offers two ways to query your database using natural language:
1. Model-Specific Querying
This approach ties queries to specific models, ideal when you know which table you want to query.
# Include AsktiveRecord in your ApplicationRecord or specific models
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
include AsktiveRecord
end
# Or in a specific model
class User < ApplicationRecord
include AsktiveRecord
end
# Now you can query the User model directly
query = User.ask("Show me the last five users who signed up")
# => Returns a Query object with SQL targeting the users table
# Call the execute method to run the query on the database
results = query.execute
# => Returns an array of User objects
# If you want a human-readable answer, use the answer method
results = query.answer
# => "The last five users who signed up are: Alice, Bob, Charlie, Diana, Eve."2. Service-Class Querying (Any Table)
This approach allows querying any table or multiple tables with joins:
# Create a service class that includes AsktiveRecord
class AskService
include AsktiveRecord
# No additional code needed!
end
# Query any table through this service
query = AskService.ask("Which is the last user created?")
# => Returns a Query object
results = AskService.ask("Which is the cheapest product?").execute
# => Returns an ActiveRecord::Result object (array of hashes)
answer = AskService.ask("Show me products with their categories").answer
# => "The products with their categories are: Widget (Electronics), Gadget (Electronics), ..."Working with Query Results
The execute method returns different types of results based on the context:
-
Model queries (e.g.,
User.ask(...)): returns an array of model instances (e.g.,Userobjects) -
Service queries (e.g.,
AskService.ask(...)): returns anActiveRecord::Resultobject (array of hashes)
# Example of working with results from a model query
query = User.ask("Who are my newest users?")
results = query.execute
# => results is an array of User objectsThe .answer Method
The .answer method provides a human-friendly, natural language response to your query. When you call .answer, AsktiveRecord executes the query and uses the LLM to generate a concise, readable answer based on the results.
Example Usage
# Using a service class
response = AskService.ask("Which is the cheapest product?").answer
# => "The cheapest product is the Earphone."
# Using a model
response = User.ask("Who signed up most recently?").answer
# => "The most recently signed up user is Alice Smith."
# Asking for a summary
response = AskService.ask("How many orders were placed last week?").answer
# => "There were 42 orders placed last week."Tip: You can interpolate dynamic values into the question:
customer = Customer.find(params[:id])
response = AskService.ask("For customer #{customer.id}, which is the most sold product?").answer
# => "The most sold product for customer ABC is the Premium Widget."Query Object API
The ask() method returns an instance of AsktiveRecord::Query. Key methods:
-
raw_sql— The raw SQL string generated by the LLM. -
sanitized_sql— The SQL string aftersanitize!has been called. -
sanitize!(allow_only_select: true)— Validates the query throughSqlSanitizer. RaisesAsktiveRecord::SanitizationErroron failure. -
execute— Executes the sanitized SQL against the database. -
answer— Executes the query and returns a human-readable LLM-generated answer. -
to_s— Returns the sanitized SQL (or raw SQL if not yet sanitized).
Logging
AsktiveRecord provides structured logging to help you debug and monitor queries. By default, logs are sent to Rails.logger with the [AsktiveRecord] prefix.
Example Log Output
[AsktiveRecord] Received question: "Who are my newest users?"
[AsktiveRecord] Generated SQL: SELECT * FROM users ORDER BY created_at DESC LIMIT 5
[AsktiveRecord] Sanitized SQL: SELECT * FROM users ORDER BY created_at DESC LIMIT 5
When using .answer:
[AsktiveRecord] Received question: "How many orders were placed last week?"
[AsktiveRecord] Generated SQL: SELECT COUNT(*) FROM orders WHERE created_at >= '2024-06-01'
[AsktiveRecord] Answering question: How many orders were placed last week?
Security
AsktiveRecord implements defense-in-depth security:
-
SQL Sanitization: All generated SQL passes through
SqlSanitizerwhich blocks dangerous keywords (INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, etc.) and injection patterns (UNION SELECT, semicolons, comments, SLEEP, etc.) - Read-Only Mode: Enabled by default — only SELECT queries are allowed to execute
- Prompt Injection Prevention: User input is escaped and filtered before being sent to the LLM, with a 2,000-character limit
-
No Hardcoded Secrets: Initializer template uses
ENV["OPENAI_API_KEY"]by default - Multi-Layer Validation: SQL is validated at the LLM response level, the Query level, and the SqlSanitizer level
Supported LLMs
-
Built-in: OpenAI (models like
gpt-4o-mini,gpt-4o,gpt-4-turbo) -
Custom Adapters: Any LLM can be supported by creating an adapter that inherits from
AsktiveRecord::Adapters::Baseand implements the#chatmethod
Contributing
Contributions are welcome! Whether it's bug reports, feature requests, documentation improvements, or code contributions, please feel free to open an issue or submit a pull request on GitHub.
- Fork the repository (https://github.com/rpossan/asktive_record/fork).
- Create your feature branch (
git checkout -b my-new-feature). - Commit your changes (
git commit -am 'Add some feature'). - Push to the branch (
git push origin my-new-feature). - Create a new Pull Request.
Please make sure to add tests for your changes and ensure all tests pass (bundle exec rspec). Also, adhere to the existing code style (you can use RuboCop: bundle exec rubocop).
Development
After checking out the repo, run bin/setup to install dependencies. Then, run bundle exec rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the AsktiveRecord project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the CODE_OF_CONDUCT.md.