Skip to content

Commit 06eac83

Browse files
committed
New Agent interface
1 parent eb5bdd4 commit 06eac83

File tree

4 files changed

+324
-0
lines changed

4 files changed

+324
-0
lines changed

docs/_core_features/agents.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
---
2+
layout: default
3+
title: Agents
4+
nav_order: 6
5+
description: Define reusable agent configurations once and use them anywhere
6+
---
7+
8+
# {{ page.title }}
9+
{: .d-inline-block .no_toc }
10+
11+
New in 1.12
12+
{: .label .label-green }
13+
14+
{{ page.description }}
15+
{: .fs-6 .fw-300 }
16+
17+
## Table of contents
18+
{: .no_toc .text-delta }
19+
20+
1. TOC
21+
{:toc}
22+
23+
---
24+
25+
After reading this guide, you will know:
26+
27+
* Why agents exist and when to use them
28+
* How to define an agent with a class-based DSL
29+
* How to instantiate and use agents in your app
30+
* How to keep tools and instructions centralized
31+
32+
## What Are Agents?
33+
34+
Agents are a DSL that lets you define a chat configuration once and reuse it everywhere. They make agent definitions feel like first-class objects in your app: readable, discoverable, and easy to instantiate.
35+
36+
This is especially helpful in Rails apps where you keep agent classes in `app/agents` and want to avoid re-specifying the same model, tools, and instructions every time you load a chat from the database. But it also works great in scripts, services, and background jobs—anywhere you want a clean, named place to put the “shape” of an agent.
37+
38+
Instead of rebuilding configuration for each chat instance, you define it once on the agent class and instantiate it when needed.
39+
40+
## Defining an Agent
41+
42+
Create a class that inherits from `RubyLLM::Agent` and declare its configuration:
43+
44+
```ruby
45+
# app/agents/chat_agent.rb
46+
class ChatAgent < RubyLLM::Agent
47+
model "gpt-5", provider: :azure, assume_model_exists: true
48+
tools MyTool, ThatTool
49+
instructions "Be awesome"
50+
temperature 0.2
51+
thinking effort: :none
52+
params max_output_tokens: 256
53+
headers "X-Request-Id" => "chat-agent"
54+
schema MySchema
55+
end
56+
```
57+
58+
Each class macro maps to the equivalent `RubyLLM.chat` or `Chat#with_*` setting. The values are applied when you instantiate the agent.
59+
60+
## Using an Agent
61+
62+
Instantiate the class and ask questions just like a normal chat:
63+
64+
```ruby
65+
agent = ChatAgent.new
66+
response = agent.ask "hello"
67+
68+
puts response.content
69+
```
70+
71+
You can also override any `RubyLLM.chat` arguments per instance:
72+
73+
```ruby
74+
agent = ChatAgent.new(model: "gpt-5-mini")
75+
agent.ask "Use the faster model for this request."
76+
```
77+
78+
## Why This Helps in Rails
79+
80+
When you load a chat from the database, you often need to reapply instructions and tools to get consistent behavior. Agents let you keep that configuration in one place:
81+
82+
```ruby
83+
# app/agents/support_agent.rb
84+
class SupportAgent < RubyLLM::Agent
85+
model "{{ site.models.default_chat }}"
86+
instructions "You are a helpful support agent."
87+
tools SearchDocs, LookupAccount
88+
end
89+
```
90+
91+
That way, every part of your app uses the same settings without duplicating logic in controllers, jobs, or model callbacks.
92+
93+
## When to Use Agents vs `RubyLLM.chat`
94+
95+
Use `RubyLLM.chat` when you want a one-off conversation or quick, inline configuration:
96+
97+
```ruby
98+
chat = RubyLLM.chat(model: "{{ site.models.default_chat }}")
99+
chat.with_instructions "Explain this clearly."
100+
```
101+
102+
Use agents when you want a named, reusable definition that you can instantiate consistently across your app:
103+
104+
```ruby
105+
class SupportAgent < RubyLLM::Agent
106+
model "{{ site.models.default_chat }}"
107+
instructions "You are a helpful support agent."
108+
tools SearchDocs, LookupAccount
109+
end
110+
```
111+
112+
Think of `RubyLLM.chat` as the ad-hoc interface and agents as the reusable, shareable interface.
113+
114+
## Next Steps
115+
116+
* Learn about [Chat Basics]({% link _core_features/chat.md %})
117+
* Explore [Tools]({% link _core_features/tools.md %})
118+
* Review [Rails Integration]({% link _advanced/rails.md %})

lib/ruby_llm/agent.rb

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# frozen_string_literal: true
2+
3+
module RubyLLM
4+
# Base class for simple, class-configured agents.
5+
class Agent
6+
class << self
7+
def inherited(subclass)
8+
super
9+
subclass.instance_variable_set(:@chat_kwargs, (@chat_kwargs || {}).dup)
10+
subclass.instance_variable_set(:@tools, (@tools || []).dup)
11+
subclass.instance_variable_set(:@instructions, @instructions)
12+
subclass.instance_variable_set(:@temperature, @temperature)
13+
subclass.instance_variable_set(:@thinking, @thinking)
14+
subclass.instance_variable_set(:@params, (@params || {}).dup)
15+
subclass.instance_variable_set(:@headers, (@headers || {}).dup)
16+
subclass.instance_variable_set(:@schema, @schema)
17+
subclass.instance_variable_set(:@context, @context)
18+
end
19+
20+
def model(model_id = nil, **options)
21+
options[:model] = model_id unless model_id.nil?
22+
@chat_kwargs = options
23+
end
24+
25+
def tools(*tools)
26+
return @tools || [] if tools.empty?
27+
28+
@tools = tools.flatten
29+
end
30+
31+
def instructions(text = nil)
32+
return @instructions if text.nil?
33+
34+
@instructions = text
35+
end
36+
37+
def temperature(value = nil)
38+
return @temperature if value.nil?
39+
40+
@temperature = value
41+
end
42+
43+
def thinking(effort: nil, budget: nil)
44+
return @thinking if effort.nil? && budget.nil?
45+
46+
@thinking = { effort: effort, budget: budget }
47+
end
48+
49+
def params(**params)
50+
return @params || {} if params.empty?
51+
52+
@params = params
53+
end
54+
55+
def headers(**headers)
56+
return @headers || {} if headers.empty?
57+
58+
@headers = headers
59+
end
60+
61+
def schema(value = nil)
62+
return @schema if value.nil?
63+
64+
@schema = value
65+
end
66+
67+
def context(value = nil)
68+
return @context if value.nil?
69+
70+
@context = value
71+
end
72+
73+
def chat_kwargs
74+
@chat_kwargs || {}
75+
end
76+
end
77+
78+
def initialize(**chat_kwargs) # rubocop:disable Metrics/PerceivedComplexity
79+
@chat = RubyLLM.chat(**self.class.chat_kwargs, **chat_kwargs)
80+
@chat.with_context(self.class.context) if self.class.context
81+
@chat.with_instructions(self.class.instructions) if self.class.instructions
82+
@chat.with_tools(*self.class.tools) unless self.class.tools.empty?
83+
@chat.with_temperature(self.class.temperature) unless self.class.temperature.nil?
84+
if (thinking = self.class.thinking)
85+
@chat.with_thinking(**thinking)
86+
end
87+
@chat.with_params(**self.class.params) unless self.class.params.empty?
88+
@chat.with_headers(**self.class.headers) unless self.class.headers.empty?
89+
@chat.with_schema(self.class.schema) if self.class.schema
90+
end
91+
92+
def ask(message = nil, with: nil, &block)
93+
@chat.ask(message, with: with, &block)
94+
end
95+
96+
alias say ask
97+
98+
attr_reader :chat
99+
end
100+
end

spec/fixtures/vcr_cassettes/agent_can_ask_using_the_first_configured_chat_model.yml

Lines changed: 84 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spec/ruby_llm/agent_spec.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
RSpec.describe RubyLLM::Agent do
6+
include_context 'with configured RubyLLM'
7+
8+
it 'can ask using the first configured chat model' do
9+
model_info = CHAT_MODELS.first
10+
11+
agent_class = Class.new(RubyLLM::Agent) do
12+
model model_info[:model], provider: model_info[:provider]
13+
instructions 'Answer questions clearly.'
14+
end
15+
16+
stub_const('SpecChatAgent', agent_class)
17+
18+
response = SpecChatAgent.new.ask("What's 2 + 2?")
19+
expect(response.content).to include('4')
20+
expect(response.role).to eq(:assistant)
21+
end
22+
end

0 commit comments

Comments
 (0)