WardrikChannel behaviour (wardrik_channel v0.1.0-rc.1)
Copy MarkdownLets people talk to Wardrik's agents from chat apps, such as Slack.
A channel is one chat app Wardrik runs, under a name you choose. A room is a place in it, such as a Slack channel.
config :my_app, MyApp.AgentRuntime,
channels: [
slack: {WardrikSlack, token: "xoxb-...", signing_secret: "...", owner: "acme", approvers: ["@maria"]}
]An agent answers in a channel when its definition lists it:
Wardrik.Agent.define(scope, "acme/triage", model: "anthropic:claude-haiku-4-5", channels: ["slack"])Mention the bot in a room to start a thread. Reply in the thread to go on. Each thread is its own
conversation, as acme/triage/slack.T0123.C024BE91L.1696.1234. Calls the agent waits on are
posted in the thread for someone to decide. See WardrikChannel.Rooms and
WardrikChannel.Replies.
Options
Every channel takes these. Its adapter reads the rest.
:token- the token the channel acts with, in the one workspace it reaches. Needs:owner.:owner- whose agents answer, as"acme".:installations- instead of:token, to add one app to many workspaces. Either:stored, to keep them in Wardrik (needs Wardrik's:keys), or{module, config}, aWardrik.Channel.Installations.:approvers- who may decide what agents wait on, as the chat app names people, or:anyone.[]unless set. Whoever asked for a turn may always decide it.:offering- what a room with no agent is told.:deciding- what someone who may not decide is told.
Raises ArgumentError on start if neither or both of :token and :installations are given,
or if the channel's name isn't lower-case letters, digits and -, starting with a letter.
Writing an adapter
An adapter is a package, such as wardrik_slack, that does use WardrikChannel. It implements
deliver/2 and include?/4, and Wardrik.Channel.request/2 to take what the chat app
sends:
defmodule MyChat do
use WardrikChannel
@impl Wardrik.Channel
def request(opts, %{path: "events", body: body}) do
channel = WardrikChannel.channel(__MODULE__, opts)
event = JSON.decode!(body)
with {:ok, workspace} <- WardrikChannel.workspace(channel, event["team"]) do
WardrikChannel.received(workspace, %WardrikChannel.Inbound{
room: event["room"],
id: event["id"],
sender: %WardrikChannel.Member{id: event["user"]},
text: event["text"],
mentioned?: event["mentioned"]
})
end
%{status: 200, headers: %{}, body: ""}
end
@impl WardrikChannel
def deliver(channel, %WardrikChannel.Outbound{} = outbound) do
# Post outbound.body in outbound.room, in outbound.thread
:ok
end
@impl WardrikChannel
def include?(_channel, _installation, names, member), do: member.id in names
endHand each button press to decide/4. use WardrikChannel defines child_spec/1,
children/1 and Wardrik.Channel.notify/4. Each can be overridden.
Summary
Callbacks
Returns extra children to run, such as a connection to the chat app. [] by default.
Posts outbound in its thread. Returning an error has it tried again later.
Returns whether member is one of names, as the chat app names people, such as @maria.
Returns the URL where someone acting for owner adds the chat app to their workspace.
Finishes adding the chat app to a workspace, for owner.
Removes the chat app from workspace, where the chat app allows it, and forgets the workspace.
Functions
Returns adapter's channel, built from the options Wardrik passes it.
Approves or denies a call the agent waits on, as member.
Posts notice in room, asking approvers. See Wardrik.Channel.notify/4.
Sends inbound to the room's agent, as its sender, and has the reply posted in its thread.
Returns the channel in workspace id, or :error if it isn't added there.
Callbacks
@callback children(WardrikChannel.Channel.t()) :: [ Supervisor.child_spec() | {module(), term()} ]
Returns extra children to run, such as a connection to the chat app. [] by default.
@callback deliver(WardrikChannel.Channel.t(), WardrikChannel.Outbound.t()) :: :ok | {:error, term()}
Posts outbound in its thread. Returning an error has it tried again later.
@callback include?( WardrikChannel.Channel.t(), Wardrik.Channel.Installation.t(), names :: [String.t()], WardrikChannel.Member.t() ) :: boolean()
Returns whether member is one of names, as the chat app names people, such as @maria.
@callback install_url(WardrikChannel.Channel.t(), owner :: String.t(), keyword()) :: {:ok, String.t()} | {:error, String.t()}
Returns the URL where someone acting for owner adds the chat app to their workspace.
Options
:return_to- where they're sent once it's added.
@callback installed(WardrikChannel.Channel.t(), owner :: String.t(), query :: map()) :: {:ok, Wardrik.Channel.Installation.t(), String.t() | nil} | {:error, String.t()}
Finishes adding the chat app to a workspace, for owner.
query is the query string the chat app redirected back with. Returns the installation and
where to send the person next.
@callback uninstall(WardrikChannel.Channel.t(), workspace :: String.t()) :: :ok | {:error, String.t()}
Removes the chat app from workspace, where the chat app allows it, and forgets the workspace.
Functions
@spec channel(module(), keyword()) :: WardrikChannel.Channel.t()
Returns adapter's channel, built from the options Wardrik passes it.
@spec decide( WardrikChannel.Workspace.t(), WardrikChannel.Member.t(), map(), {:approve | :deny, String.t() | nil} ) :: {:ok, Wardrik.Decision.t()} | {:error, String.t()}
Approves or denies a call the agent waits on, as member.
waiting = %{conversation: conversation, interrupt: "int_1", asked_by: nil}
WardrikChannel.decide(workspace, member, waiting, {:approve, "Looks fine."})
#=> {:ok, %Wardrik.Decision{}}waiting is what the adapter put on the button, from a WardrikChannel.Waiting:
:conversation- the conversation's ID.:interrupt- the interrupt's ID.:asked_by- who asked for the turn. Optional.
answer is {:approve, note} or {:deny, reason}. Either may be nil.
Approvers may decide, and so may whoever asked. Anyone else gets {:error, deciding}, the
channel's :deciding message.
@spec notify(module(), keyword(), String.t(), Wardrik.Notice.t(), [String.t()]) :: :ok | {:skip, String.t()} | {:error, term()}
Posts notice in room, asking approvers. See Wardrik.Channel.notify/4.
Posts in the workspace of the agent's owner. That is the owning user's workspace if the channel
is added to one, else the org's. Returns {:skip, why} if it's added to neither.
@spec received(WardrikChannel.Workspace.t(), WardrikChannel.Inbound.t()) :: {:asked, String.t()} | :ignored | {:error, String.t()}
Sends inbound to the room's agent, as its sender, and has the reply posted in its thread.
The agent is asked when:
- the message mentions the bot
- the room's agent answers every message
- the message replies in a thread the agent already answers in
Returns {:asked, conversation}, :ignored if no agent answers, or {:error, message}.
If the agent is waiting on calls, the message queues behind them and the calls are posted again.
@spec workspace(WardrikChannel.Channel.t(), String.t()) :: {:ok, WardrikChannel.Workspace.t()} | :error
Returns the channel in workspace id, or :error if it isn't added there.