You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* Document Avo Intelligence tool management
config.ai.excluded_tools / extra_tools, the avo:ai:tool and avo:ai:eject tool
generators, the denylist upgrade behavior, the rename_conversation capability
semantics, the alpha constant-rename note, and a rewritten roster-debugging
section keyed to the Agent tools field.
Claude-Session: https://claude.ai/code/session_01LVKV1UrV52oXR7JXmSMr66
* Note replacement titling, inspector-gate exclusion, and error redaction
Excluding rename_conversation keeps titling when a replacement is registered
under the same wire name (the eject flow); excluding resource_inspector turns
the inspection gate off rather than bricking queries; extra_tools resolution
errors carry class + key names only, never values.
Claude-Session: https://claude.ai/code/session_01LVKV1UrV52oXR7JXmSMr66
* Match ruby_llm 2.0: parameters schema, and the class-level wire-name pin
* ai: tool calls live on the message, and no alpha upgrade note
The standalone Tool calls resource went away with the ruby_llm 2.0 port —
calls are stored on the message that made them and read through the Tool
calls field on its show page. Points both mentions at it.
Drops the Avo::Ai::*Tool constant-rename warning: the page carries no
upgrade path, per "read as a fresh install".
Claude-Session: https://claude.ai/code/session_018odGTv46mvdjMZck6hH6dd
Copy file name to clipboardExpand all lines: docs/4.0/ai-agents-and-tools.md
+21-4Lines changed: 21 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -16,7 +16,7 @@ This page is the reference for both — which agents exist, what every tool does
16
16
17
17
The chat agent powers every conversation — the floating bar, full-page chats, and chats attached to a record. On every message it rebuilds its system prompt from the ERB files under `app/prompts/` (which is what keeps the current date, signed-in user, and attached record fresh) and assembles its tool roster for the signed-in user who owns the chat.
18
18
19
-
The roster is built per run, not baked in: what the assistant can do in a conversation always reflects your current policies and configuration.
19
+
The roster is built per run, not baked in: what the assistant can do in a conversation always reflects your current [tool configuration](./ai.html#choose-which-tools-the-assistant-gets), and every call it then makes is authorized against your policies at that moment.
20
20
21
21
### The conversation renamer
22
22
@@ -28,9 +28,13 @@ It runs on three occasions:
28
28
- when you pick **Rename again with AI** from the ⋯ menu, in the chat bar or on a chat page
29
29
- when you ask the assistant to rename the conversation without giving it a name
30
30
31
+
All three go through the `rename_conversation` tool, so [excluding that tool](./ai.html#take-a-tool-away) switches the renamer off along with it — unless you registered a replacement under the same wire name (as [ejecting it](./ai.html#replace-a-shipped-tool-with-your-own-copy) does), in which case the renamer uses your copy. Naming a chat by hand keeps working.
32
+
31
33
## The tools
32
34
33
-
Tool names below are how calls appear in the Tool calls resource and in the chat's debug view (see [Debug levels](./ai.html#debug-levels)).
35
+
Tool names below are how calls appear in a message's **Tool calls** field and in the chat's debug view (see [Debug levels](./ai.html#debug-levels)).
36
+
37
+
A tool's name is the stable part of it: the model calls the tool by that name, every stored tool call keeps it, and it's how you name a tool in configuration. The roster isn't fixed either — you can take any of these away, add tools of your own, or replace one with an editable copy. See [Choose which tools the assistant gets](./ai.html#choose-which-tools-the-assistant-gets).
@@ -74,7 +78,7 @@ That is a deliberate security boundary. There is no argument the model could inv
74
78
75
79
### The inspection gate
76
80
77
-
The query and write tools refuse to touch a resource until `resource_inspector` has run for it in the conversation. The gate is enforced in the tools — not merely requested in the prompt — and is what makes the assistant work from your real columns and scopes instead of guessed ones.
81
+
The query and write tools refuse to touch a resource until `resource_inspector` has run for it in the conversation. The gate is enforced in the tools — not merely requested in the prompt — and is what makes the assistant work from your real columns and scopes instead of guessed ones. Excluding `resource_inspector` turns the gate off along with the tool — the queries and writes then proceed uninspected rather than refusing.
78
82
79
83
`run_action` sits behind the same gate, and adds two of its own: the resource's `act_on?` policy method and the action's `self.authorize` block, both checked when the run is proposed and again when you confirm it. See [What it's allowed to run](./ai.html#what-it-s-allowed-to-run).
80
84
@@ -97,6 +101,19 @@ Titles are generated by the conversation's own model. If that model doesn't supp
97
101
98
102
## Seeing a conversation's roster
99
103
100
-
The Chats resource's show page has an **Agent tools** field listing exactly the tools the assistant will get on that conversation's next run. It's computed live — owner-scoped and model-aware — so it's the quickest way to see the effect of a policy change, or why a roster is empty (some provider/model combinations reject tools while extended thinking is enabled).
104
+
The Chats resource's show page has an **Agent tools** field listing exactly the tools the assistant will get on that conversation's next run. It's computed live, by the same code the conversation itself runs, so it's the quickest way to confirm that a change to [your tool configuration](./ai.html#choose-which-tools-the-assistant-gets) took effect.
| A shipped tool is missing | It's named in `config.ai.excluded_tools`|
111
+
| A tool you registered isn't listed | The app hasn't been restarted since its `config.ai.extra_tools` entry was added — a broken entry never goes missing quietly, it shows up as the error row below |
112
+
| The list is empty | Either everything is excluded, or this conversation's provider/model combination rejects tools while extended thinking is enabled — no configuration puts them back on such a run |
113
+
| An error message where the names should be | An `extra_tools` entry naming a class that won't load, isn't a `RubyLLM::Tool`, or claims a wire name another tool already has |
114
+
115
+
That last row is deliberate: the field renders the error instead of failing the page, because this is the page you came to in order to read it. The same configuration raises on an actual chat run — a missing tool the app asked for isn't something to pass over quietly.
116
+
117
+
The roster doesn't change with your authorization policies. Every tool checks permissions when it's called, rather than being withheld from the list, so a policy change shows up in what a tool *does* — not in what's listed here.
101
118
102
119
For per-call detail — which tools ran, with which arguments, and what they returned — set the viewer's [debug level](./ai.html#debug-levels) to `:tools`.
Copy file name to clipboardExpand all lines: docs/4.0/ai.md
+127Lines changed: 127 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -115,6 +115,10 @@ Avo.configure do |config|
115
115
deadline:30, # seconds for the whole download
116
116
max_redirects:3
117
117
}
118
+
119
+
# Which tools the assistant gets (see "Choose which tools the assistant gets").
120
+
config.ai.excluded_tools = [:delete_record]
121
+
config.ai.extra_tools = ["CrmTool"]
118
122
end
119
123
```
120
124
@@ -378,6 +382,129 @@ This copies all prompt files — the chat assistant's instructions and sub-promp
378
382
379
383
The shipped `instructions.txt.erb` ends with an `<%= extra_instructions %>` slot. If you replace it, your copy decides whether to keep that slot — remove the line and the `extra_instructions` file is ignored.
380
384
385
+
## Choose which tools the assistant gets
386
+
387
+
What the assistant can do is exactly the set of [tools](./ai-agents-and-tools.html#the-tools) it's handed on each run. Two settings shape that set: `excluded_tools` takes shipped tools away, and `extra_tools` adds tools you wrote yourself.
Set neither and the assistant gets the twelve tools the gem ships. The roster is assembled per run, so once the app has restarted, the next message in an existing conversation already reflects the change — nothing is baked into a chat.
The names are **wire names** — what the model sees, and what every call is stored under on the message that made it (the **Tool calls** field on a message's show page). [The tools table](./ai-agents-and-tools.html#the-tools) is the full list of twelve; symbols and strings are both accepted.
407
+
408
+
An excluded tool is filtered out by name before it's ever built, so it isn't attached to the conversation and the model never learns it exists. It doesn't refuse the request — there's nothing there to refuse with.
409
+
410
+
A name that isn't one of the twelve raises `ArgumentError` at boot, listing the ones it knows. A typo that quietly left deletes attached is exactly the failure worth being loud about.
411
+
412
+
:::warning It's a denylist, so tools added later arrive switched on
413
+
`excluded_tools` says what to remove, not what to allow. A future avo-ai release that ships a new tool — a write tool included — hands it to every app that hasn't named it here. Read the release notes when you upgrade, and exclude anything you don't want.
414
+
:::
415
+
416
+
Nothing is protected. `ask_user` and `write_history` are chat infrastructure rather than data tools, and excluding them is allowed: the assistant loses the ability to ask you a clarifying question, or to list and undo the writes it made in the conversation. That's a decision you're free to make — just make it deliberately.
417
+
418
+
Excluding `resource_inspector` takes the [inspection gate](./ai-agents-and-tools.html#the-inspection-gate) with it: the tool is the only way the gate can ever be satisfied, so with it gone, queries and writes proceed without an inspection instead of refusing forever. Less introspection, not a dead assistant — but the assistant now works from guessed columns rather than real ones.
419
+
420
+
:::warning Excluding `rename_conversation` turns AI titling off entirely
421
+
That tool is what applies a title, so removing it stops the [conversation renamer](./ai-agents-and-tools.html#renaming-conversations) too, not just the assistant's ability to rename on request: new conversations are no longer auto-titled after the first message, and **Rename again with AI** stops having an effect. Conversations keep the *Untitled chat* placeholder until someone names them. **Rename chat** still works — that one never goes through a model.
422
+
423
+
The exception is a replacement: register your own tool under the `rename_conversation` wire name — which is exactly what [ejecting it](#replace-a-shipped-tool-with-your-own-copy) sets up — and titling continues through your copy.
424
+
:::
425
+
426
+
### Bring your own tool
427
+
428
+
Scaffold one:
429
+
430
+
```bash
431
+
bin/rails generate avo:ai:tool crm
432
+
```
433
+
434
+
That writes `app/tools/crm_tool.rb`, defining `CrmTool` — a `RubyLLM::Tool` the model calls `crm`. The file arrives with the same three mixins the shipped tools use, and TODOs where your part goes:
|`Avo::Ai::ToolSupport`|`json_result` for the reply shape, plus resource lookup and schema introspection helpers |
439
+
|`Avo::Ai::ToolAuthorization`| The acting user, and the gates to reach data through: `require_acting_user!`, `authorized_relation`, `authorize_record_action!`|
440
+
|`Avo::Ai::InspectionAware`|`inspection_tracker`, the per-run record of which resources have been inspected |
441
+
442
+
Fill in the `description` — the model reads it to decide whether to call the tool, and a vague one is the usual reason a tool never gets called — then the `parameters` schema and `execute`.
443
+
444
+
Then register the class in your initializer:
445
+
446
+
```ruby
447
+
# config/initializers/avo.rb
448
+
config.ai.extra_tools = ["CrmTool"]
449
+
```
450
+
451
+
A tool that takes settings of its own is registered as a Hash instead. Its `tool:` key names the class and every other key is passed to the tool's initializer:
Entries are class **names**, not constants: this initializer runs before your own classes are loadable, so the name is resolved when a chat runs. The upside is that reloading works normally in development; the trade-off is that a typo surfaces on a chat's first message rather than at boot — the chat's [Agent tools](#see-what-a-conversation-will-get) field is where you'll read the error.
462
+
463
+
Three things the server decides for you, whatever the entry says:
464
+
465
+
-**The acting user, the conversation, and the inspection tracker are injected server-side.**`user:` is passed to your initializer when it accepts one, and `chat` / `inspection_tracker` are set afterwards if your tool declares the accessors (the generated one declares `chat`). `user:`, `chat:` and `inspection_tracker:` keys in an `extra_tools` entry are stripped, so the initializer can't hand your tool a different user than the one who's chatting.
466
+
-**Authorization is yours to call.** Nothing in the gem stops a tool reading the whole table — reach data through `authorized_relation` and `authorize_record_action!` so your tool sees exactly what the signed-in user sees in Avo, and rescue `Avo::Ai::ToolAuthorization::IdentityError` to report "not allowed" as a result instead of failing the run.
467
+
-**Two tools can't share a wire name.** Registering a tool whose name collides with a shipped one raises when the roster is built. To replace a shipped tool, exclude it first — that's what [ejecting](#replace-a-shipped-tool-with-your-own-copy) does for you.
468
+
469
+
:::warning What a tool returns goes to your model provider
470
+
Everything `execute` returns is sent to the provider on that turn and on every later turn of the conversation, and it's stored on the tool call. Return the minimum that answers the question — no API keys, no credentials, and no personal data the question didn't call for. Read secrets from `ENV` or `Rails.application.credentials`; never write one into the tool file or the initializer. When an entry fails to resolve, the error names the entry by its class and key names only — the values never reach a log, the error tracker, or the **Agent tools** field.
This copies the gem's tool into `app/tools/delete_record_tool.rb` as a plain top-level class you own, then wires both halves of the swap into the `Avo.configure` block in `config/initializers/avo.rb`:
482
+
483
+
```ruby
484
+
# config/initializers/avo.rb
485
+
# delete_record ejected to app/tools/delete_record_tool.rb — the copy replaces the tool avo-ai ships.
486
+
config.ai.excluded_tools += ["delete_record"]
487
+
config.ai.extra_tools += ["DeleteRecordTool"]
488
+
```
489
+
490
+
Both lines are needed, and neither works alone: the exclusion takes the shipped tool away, and the extra entry puts your copy back under the same wire name. The copy keeps its `def self.tool_name`, so tool calls, icons, and result cards are unchanged — to the model and to your stored history, it's still `delete_record`.
491
+
492
+
A tool the gem builds with an argument of its own is registered carrying it, so your copy is constructed exactly as the shipped one was. Ejecting `rename_conversation` writes `{tool: "RenameConversationTool", rescan: true}` for that reason.
493
+
494
+
The generator refuses rather than surprising you. It won't overwrite an existing `app/tools/…` file, and it won't run twice — an initializer that already excludes the name is read as "ejected already", and nothing is written.
495
+
496
+
:::warning An ejected copy stops receiving gem updates
497
+
It's a fork. Every later fix that lands in avo-ai's own copy — authorization hardening included — stays there. Diff yours against the gem's `app/tools/avo/ai/<tool>_tool.rb` when you upgrade, and eject only the tools you actually mean to change.
498
+
:::
499
+
500
+
:::danger If the generator can't find your `Avo.configure` block
501
+
It won't edit an initializer it doesn't recognize — a mangled initializer takes the app down at boot. Instead it prints the two lines, exits with a non-zero status, and says so: **the copy is on disk but is not active**. Until you paste those lines into `config/initializers/avo.rb` yourself, the assistant keeps calling the tool the gem ships, not yours.
502
+
:::
503
+
504
+
### See what a conversation will get
505
+
506
+
Open a chat in the **Chats** resource and read its **Agent tools** field: it's computed by the same code the chat runs, for that chat's owner and model, so it's the answer to "did my configuration take effect?" See [Seeing a conversation's roster](./ai-agents-and-tools.html#seeing-a-conversation-s-roster) for how to read an empty or unexpected list.
507
+
381
508
## Replace the assistant's icon
382
509
383
510
The avocado is a single partial, rendered everywhere the assistant appears: the **Agent** button, the collapsed bar, the empty state on new and full-page chats, and the *Open in assistant bar* button on a chat's record page. Create the same path in your application and all of them change at once:
0 commit comments