Skip to content

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.

ruby
# 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
ruby
# 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
end

Write 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 toolProcessRefundTool 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:

  1. Sundae calls create_file with format: "txt" (or md, csv, html, json, svg), a filename, and the receipt text. She gets back a blob_id.
  2. She calls send_email in reply mode with attachments: [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.

A second agent & scoping.