0.0
The project is in a healthy, maintained state
Drop-in Rails engine to register JSON-schema tools, let an Llm fill missing fields, validate input, and execute handlers safely.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 0.21
>= 7.0
 Project Readme

llm-agent-rails

AI intake forms for Rails.

llm-agent-rails is a Rails-native layer on top of llm-fillin for slot-filling workflows: collect missing fields through conversation, validate before execution, confirm before submit, and run safe Rails backend actions exactly once.

It is built for booking leads, quote requests, onboarding forms, support tickets, and internal admin workflows. It is not a broad agent framework.

Installation

gem "llm-agent-rails"

Then:

bundle install
bin/rails generate llm_agent_rails:install
bin/rails db:migrate

The install generator creates:

  • config/initializers/llm_agent_rails.rb
  • app/llm_intakes/.keep
  • database migrations for intake threads, messages, slot values, and executions
  • a route mount example

Mount the engine at /llm:

mount Llm::Agent::Rails::Engine => "/llm"

Create An Intake

# app/llm_intakes/booking_lead_intake.rb
class BookingLeadIntake < LlmAgentRails::Intake
  description "Collect event details for a booking lead"

  slot :name, type: :string, required: true
  slot :email, type: :string, required: true, format: :email
  slot :event_date, type: :date, required: true
  slot :start_time, type: :string, required: true
  slot :end_time, type: :string, required: true
  slot :location, type: :string, required: true
  slot :guest_count, type: :integer, required: false
  slot :package, type: :string, enum: ["Gold", "Platinum", "Emerald"], required: false

  confirm_before_submit true

  def submit(values, context:)
    BookingLead.create!(values)
  end
end

Or generate one:

bin/rails generate llm_agent_rails:intake BookingLead name:string email:string event_date:date location:string

Intake Endpoint

POST /llm/intakes/:id/step

Input:

{
  "thread_id": "booking-123",
  "message": "name: Mina Park email: mina@example.com",
  "context": { "tenant_id": "acct_1", "actor_id": "user_1" }
}

Example response when fields are missing:

{
  "status": "needs_clarification",
  "assistant_message": "What is the event date?",
  "slots": {
    "name": "Mina Park",
    "email": "mina@example.com"
  },
  "missing_slots": ["event_date", "start_time", "end_time", "location"],
  "invalid_slots": {},
  "ready_to_confirm": false,
  "ready_to_execute": false,
  "executed": false,
  "execution_result": null,
  "idempotency_key": "intake-...",
  "thread_id": "booking-123"
}

Confirmation Flow

When all required slots are valid and confirm_before_submit true is set, the engine asks for confirmation:

{
  "status": "needs_confirmation",
  "assistant_message": "Please confirm: name: Mina Park, email: mina@example.com, event date: 2026-06-20. Should I submit this?",
  "ready_to_confirm": true,
  "executed": false
}

Confirm with the same thread_id:

curl -X POST http://localhost:3000/llm/intakes/booking_lead/step \
  -H "Content-Type: application/json" \
  -d '{"thread_id":"booking-123","message":"yes"}'

The response includes the Rails return value:

{
  "status": "executed",
  "assistant_message": "Submitted.",
  "executed": true,
  "execution_result": {
    "id": 42,
    "name": "Mina Park"
  },
  "thread_id": "booking-123"
}

Idempotency

Each submit has a stable idempotency key derived by llm-fillin. The engine stores execution records in ActiveRecord and replays completed results when a browser retries the same confirmed thread. Required slots must be valid and confirmation must pass before Rails application code runs.

The generated migrations create:

  • LlmAgentRails::Thread
  • LlmAgentRails::Message
  • LlmAgentRails::SlotValue
  • LlmAgentRails::Execution

Provider Configuration

API keys are server-side only and are never exposed to the browser.

The default generated initializer uses the fake adapter:

LlmAgentRails.configure do |config|
  config.provider = :fake
  config.adapter = LlmAgentRails::Adapters::Fake.new
end

For OpenAI, add gem "openai" to your Rails app and configure through llm-fillin:

LlmAgentRails.configure do |config|
  config.provider = :openai
  config.model = "gpt-4.1-mini"
  config.temperature = 0
  config.adapter = ->(c) {
    LlmFillin::Adapters::OpenAI.new(
      api_key: ENV.fetch("OPENAI_API_KEY"),
      model: c.model,
      temperature: c.temperature
    )
  }
end

Any object that implements #extract(workflow:, message:, slots:, context:) can be used as an adapter.

Testing With Fake Adapter

Tests should not make real API calls:

LlmAgentRails.configure do |config|
  config.adapter = LlmAgentRails::Adapters::Fake.new
end

The fake adapter understands simple field: value messages, which makes request specs deterministic.

Compatibility

The gem name remains llm-agent-rails. The old Llm::Agent::Rails namespace and POST /step endpoint are still present where reasonable for 0.1 compatibility, but the preferred API is intake-oriented:

LlmAgentRails::Intake
POST /llm/intakes/:id/step

How This Relates To llm-fillin

llm-fillin is the framework-light Ruby core: workflow definitions, slot validation, confirmation, result objects, provider adapters, and idempotent handler execution.

llm-agent-rails adds the Rails-native layer: autoloaded intake classes, ActiveRecord persistence, JSON endpoints, Rails generators, and dummy/demo app patterns.

Migration From 0.1 Agent API

The old agent/tool-call route remains available as POST /llm/step when mounted at /llm, and the old Llm::Agent::Rails namespace still delegates to the new configuration object.

For new work, move JSON-schema tools into intake classes:

class SupportTicketIntake < LlmAgentRails::Intake
  slot :email, type: :string, required: true, format: :email
  slot :summary, type: :string, required: true

  confirm_before_submit true

  def submit(values, context:)
    SupportTicket.create!(values.merge(created_by_id: context[:actor_id]))
  end
end

The engine persists the thread, slot state, messages, and execution result for you. Provider calls still go through llm-fillin adapters.

Commands

bundle install
bundle exec rake test

Dummy app boot smoke test:

bundle exec ruby -e 'require_relative "test/dummy/config/environment"; puts Rails.application.class.name'

License

MIT