llm-agent-rails
Rails engine for LLM‑powered agents — slot filling, tool orchestration, and safe backend execution.
Turn chat into validated function calls using JSON‑Schema, then run your Ruby handlers with idempotency.
What this gem gives you
- A mountable endpoint:
POST /llm/agent/stepthat talks to an LLM. - Slot filling: the model asks for missing fields until a tool call is ready.
- Tool registry: register functions (name+version+schema+handler) the LLM can call.
-
Validation: arguments are validated with
json_schemerbefore your handler runs. - Idempotency: unique per‑thread keys for safe “create” operations.
-
Adapter: OpenAI adapter using the
openai ~> 0.21Ruby SDK. -
Memory store: persists tool results per
thread_id(swap for Redis in prod). -
Generators:
-
rails g llm:agent:install(initializer + mount routes) -
rails g llm:agent:tickets_demo(optional demo tool + migration)
-
Install
Add to your Gemfile:
gem "llm-agent-rails", "~> 0.1"Then:
bundle install
bin/rails g llm:agent:install
# mounts the engine at /llm/agent and creates config/initializers/llm_agent.rbOptional demo (creates a Ticket model/tool so you can see tool-calling end‑to‑end):
bin/rails g llm:agent:tickets_demo
bin/rails db:migrateConfigure your API key:
export OPENAI_API_KEY=sk-...Run the app:
bin/rails sAPI — talk to the agent
POST /llm/agent/step
Body
{
"thread_id": "demo-123",
"messages": [
{ "role": "user", "content": "Open a ticket for checkout failing on Apple Pay" }
]
}-
thread_idkeeps tool state & idempotency consistent across turns. -
messagesis the chat transcript (array of{role, content}).
Response
One of:
{ "type": "assistant", "text": "a clarifying question or confirmation..." }or
{ "type": "tool_ran", "tool_name": "create_ticket", "result": { "id": 42, "title": "..." } }Errors look like:
{ "error": "BadRequest", "message": "messages is required (array of {role, content})" }Example conversations
A. Client-managed transcript (simple)
Send the entire transcript each POST with the same thread_id.
curl -X POST http://localhost:3000/llm/agent/step -H "Content-Type: application/json" -d '{
"thread_id": "demo-123",
"messages": [
{ "role": "user", "content": "Open a ticket for checkout failing on Apple Pay" },
{ "role": "assistant", "content": "I can help with that! What is the description?" },
{ "role": "user", "content": "Description: iOS 17 checkout fails with tokenization error. Priority high." },
{ "role": "assistant", "content": "Ill create a ticket with the following details:\n\n- **Title**: Checkout failing on Apple Pay\n- **Description**: iOS 17 checkout fails with tokenization error.\n- **Priority**: High\n\nIs there a specific category or team this ticket should be assigned to?"},
{ "role": "user", "content": "Assign to the mobile team." }
]
}'If you haven’t registered a tool, you’ll receive {"type":"assistant","text":"..."} — a helpful structured summary.
B. With a tool registered (end‑to‑end create)
- Use the demo generator:
bin/rails g llm:agent:tickets_demo
bin/rails db:migrateThis adds app/llm_tools/tickets.rb and registers a create_ticket_v1 tool with JSON Schema validation and idempotency.
- Try again. Once all required fields are collected, the response includes the tool result:
{
"type": "tool_ran",
"tool_name": "create_ticket",
"result": { "id": 123, "title": "Checkout failing on Apple Pay", "priority": "high", "key": "chat-demo-123-7a3c9e" }
}How it works
- Orchestrator: supplies tools to the model, loops until enough data is collected, then chooses exactly one tool to run.
- Registry: your functions (name+version+schema+handler).
- Validators: rejects bad inputs before your code runs.
-
Idempotency: generates
chat-<thread>-<hex>, pass to your creates. -
Store: remembers prior tool results by
thread_id(swap to Redis).
Environment
-
OPENAI_API_KEYmust be set. - Ruby ≥ 3.1, Rails ≥ 7.0.
License
MIT