The AI pack adds console text suggestions for registered form fields. It is intentionally small: a form snapshot, optional model-provided data, one provider request, and MessageBus HTML fragments back to the editor.
Enable the pack before Folio initializers run:
Folio.enabled_packs = [:ai]Run the pack migrations. They add folio_sites.ai_settings and
folio_ai_user_instructions.
Configure runtime defaults in an initializer:
Folio::Ai.configure do |config|
config.default_provider = Rails.env.development? ? :dummy : :openai
config.text_suggestions_queue = :default
endFOLIO_AI_DISABLED is a global kill switch. If the env key is present, AI is
disabled regardless of site settings.
The development-only dummy provider is available as :dummy and only offers the
dummy model.
The OpenAI provider is available as :openai when
FOLIO_AI_OPENAI_API_KEY is present. FOLIO_AI_OPENAI_MODELS can contain a
comma-separated model list for the site settings select. If the list is blank,
the provider fallback model is used.
All Folio AI env keys use the FOLIO_AI_ prefix.
Register each AI-enabled record class from the host app:
Rails.application.config.after_initialize do
Folio::Ai.register_record(record_class_name: "Article",
content_requirement: :tiptap_or_atoms,
fields: [
{ key: :perex, character_limit: 400 },
{ key: :meta_title, character_limit: 120 },
{ key: :meta_description, character_limit: 250 },
],
groups: [
{
key: :meta,
label: "Meta fields",
fields: %i[meta_title meta_description],
},
])
endThe record key is the model table name. Fields and groups store simple hashes:
key, optional label, optional character_limit, and group fields.
Registration exposes the field or group in site settings; it does not show
editor controls until the current site has it enabled with a nonblank prompt.
content_requirement is optional and record-wide. Use
:tiptap_or_atoms when every AI suggestion for the record needs body content
from Tiptap or atoms. The AI controls still render when prompts are configured,
but the request returns a missing-context error and skips the provider until
the current form snapshot has usable Tiptap or atom nested-attribute content.
Persisted record data and cache fields such as atoms_data_for_search are not
used for this readiness check.
Host apps can override
folio.ai.console.text_suggestions_component.errors.missing_context for
domain-specific wording.
Use ai: true on registered SimpleForm inputs:
= f.input :perex, autosize: true,
character_counter: true,
ai: trueThe record must be persisted, the site must have AI enabled, the field must be registered, the field must be enabled with a nonblank site prompt, and an available provider must be configured.
Grouped suggestions wrap normal AI inputs:
= render Folio::Ai::Console::TextSuggestionsGroupComponent.new(form: f,
key: :meta) do
= f.input :meta_title, character_counter: true,
ai: true
= f.input :meta_description, autosize: true,
character_counter: true,
ai: trueThe group must be enabled with a nonblank site prompt under the group key.
Grouped requests use that group prompt only; they do not merge in the child
fields' individual prompts. The child inputs still own accepting, copying,
undo, and final field updates.
When grouped child inputs use custom input_html[:id] values, pass matching
field metadata so grouped results target the rendered inputs:
= render Folio::Ai::Console::TextSuggestionsGroupComponent.new(form: f,
key: :meta,
fields: [
{ key: :meta_title, input_id: "custom_meta_title" },
]) do
= f.input :meta_title, ai: true,
input_html: { id: "custom_meta_title" }Use component_id instead of input_id only when you need to pass the full AI
suggestion component id directly.
Site admins edit provider, model, field prompts, group prompts, and field/group enabled flags in the site console AI tab. User instructions are stored per user, site, record key, and field or group key.
Provider prompts include the required site prompt plus the optional saved or submitted user instruction. A user instruction never replaces the site prompt.
The form snapshot is the only automatic request data source. Before it reaches the provider, Folio keeps only useful context roots:
- registered AI fields
- record columns with
string,text,json, orjsonbtypes - Tiptap fields, atom attributes, and attachment placement attributes
Framework fields, IDs, slugs, timestamps, destroyed nested records, and
password/token/secret-style keys are dropped. Cache fields such as
atoms_data_for_search are also dropped. JSON values are sanitized recursively.
A record can replace the root allowlist by defining:
def folio_ai_form_snapshot_context_keys(default_keys:)
default_keys + %w[custom_context]
endReturn the final top-level form snapshot keys to keep. Use this when a model needs extra safe context, or when a normally allowed column should be excluded.
A record can also add structured context outside the form snapshot by defining:
def folio_ai_additional_data(field_key:, form_snapshot:)
{
topic_names: topics.pluck(:name),
}
endReturn a small hash that helps generate the requested field or group. Avoid adding alternate data-source APIs for one-off use cases.
f.input ..., ai: truerenders the input controls.- Clicking the control posts one
key, agroupedboolean, and the current form snapshot toconsole_api_ai_text_suggestions_path. Folio::Ai::TextSuggestionsJobcalls the selected provider.- The job publishes MessageBus payloads with rendered HTML fragments.
- The frontend replaces suggestion HTML for the matching input components.
Single-field requests return at most three suggestions. Grouped requests use one provider/API call for the whole group and return one suggestion fragment for each field plus the shared group controls and instructions.
Run the focused pack checks after changing AI code:
bundle exec rails test packs/ai/test
bundle exec rake app:packwerk:validate
bundle exec rake app:packwerk:check