More tools & attachments
Sundae can look up an order, knows her customer, and remembers the conversation. Now we round out her job — refunds, stock checks, new orders — and teach her to handle the photos customers inevitably send.
More tools
Each new capability is just another tool. They follow the same shape as LookupOrderTool — and the same rule: each registers itself the moment it loads, but reaches Sundae only through a toolkit attached to her.
# app/tools/process_refund_tool.rb
class ProcessRefundTool < Protege::Tool
description "Issues a refund for an order. Use only when the customer is clearly owed one."
summary "Issue a refund for an order."
input_schema(
type: "object",
additionalProperties: false,
required: %w[order_number reason],
properties: {
order_number: { type: "string", description: "The order to refund, e.g. SCP-1042." },
reason: { type: "string", description: "Why the refund is warranted." }
}
)
def use(context:, order_number:, reason:)
order = Order.find_by(number: order_number)
return failure(reason: "no order #{order_number}") unless order
return failure(reason: "order already refunded") if order.refunded?
refund = order.refunds.create!(amount_cents: order.total_cents, reason:)
success(refund_id: refund.id, amount_cents: refund.amount_cents)
end
end# app/tools/check_flavor_stock_tool.rb
class CheckFlavorStockTool < Protege::Tool
description "Checks whether a flavor is currently in stock."
summary "Check a flavor's stock."
input_schema(
type: "object",
additionalProperties: false,
required: %w[flavor],
properties: { flavor: { type: "string", description: "The flavor name, e.g. Pistachio." } }
)
def use(context:, flavor:)
record = Flavor.find_by("lower(name) = ?", flavor.downcase)
return failure(reason: "we don't carry #{flavor}") unless record
success(flavor: record.name, in_stock: record.in_stock?, seasonal: record.seasonal?)
end
endWrite CreateOrderTool the same way — it takes a flavor and a quantity, creates the order for the emailing customer, and returns the new order number. Notice the guardrails live in the tool — ProcessRefundTool refuses an already-refunded order — because a tool is your app's power in the agent's hands, and the right place to enforce policy is at that boundary, not in the prompt. (See Security.)
Then arm Sundae: restart bin/dev, open Toolkits → Support → Edit, and check the new members — Process Refund, Check Flavor Stock, Create Order, plus two built-ins this chapter uses: Read Attachment and Create File. Her Support attachment already admits every sender, so the new tools are live on her next run. (Whether refunds should be open to every sender is a fair question — chapter 5 tightens that.)
Attachments: photos in
Customers send pictures. Say one emails Sundae a photo of a melted delivery. Protege stores the attachment and, because the model is multimodal, includes it automatically as a vision part on the opening turn — Sundae can literally see the photo without any tool call.
For files she needs to open deliberately — or to re-read one from earlier in the thread — there's the built-in read_attachment tool. It reads a stored file by the id shown in the conversation:
- Text files (txt, csv, markdown, json, xml) come back as text.
- Images and PDFs are shown to the model to view — it sees the actual file.
- Other types (video, archives) can't be read, and the tool says so.
So Sundae sees the melted-delivery photo, recognizes the problem, checks the order with lookup_order, and issues a refund with process_refund — a full resolution from a single emailed image.
Attachments: files out
Sundae can also create a file and attach it to her reply. The built-in create_file turns content she authors into a stored file and returns a blob_id; send_email then attaches it by that id. To send a refund receipt:
- Sundae calls
create_filewithformat: "txt"(ormd,csv,html,json,svg), a filename, and the receipt text. She gets back ablob_id. - She calls
send_emailinreplymode withattachments: [blob_id].
The receipt lands as a real attachment on the threaded reply, subject to the configured attachment limits.
Limitations
The agent only ever authors text. There's no binary generation yet — no PDF or raster image output — so create_file stores your chosen format as-is (an html file is HTML text, not a rendered page). Inbound video is stored but not understood, and Office formats aren't viewable. For a true PDF receipt, generate it in a host tool with your own library and hand the bytes back.
Next
Sundae is a capable support agent. But some work is internal, sensitive, or scheduled — and shouldn't run under a public, customer-facing identity. Time for a second agent.