Asking people
Ask a person a question, get their approval or send them a notice, then wait for the answer.
Some steps need a person: a manager approving a refund, a user picking a time, anyone confirming something an agent is about to do. noclick.Human reaches them with a page they answer on and an email, and gives you back an operation that finishes when they answer.
Who you can reach
import noclick
user = noclick.User.from_id("user_42")
user.human # the user, as a person to ask
noclick.Human() # the user you're acting for right now
noclick.Human.owner() # your account's owner
noclick.Human(email="finance@acme.com") # anyone elseCode acting for a user can reach only that user. noclick.Human() is the way to do it inside as_user, and the only way for a session or a key pinned to a user. noclick.Human(email=...) works from code that isn't acting for a user. noclick.Human.owner() always acts as your account, inside as_user too; a session or a key pinned to a user can't reach the owner (noclick.PermissionDenied).
Ask a question
request = user.human.ask(
"Which time works for your cleaning?",
options=["Thu 10:30", "Fri 14:00", "Neither"],
)
answer = request.wait()
print(answer.choice or answer.text)wait() returns an noclick.Answer with the option they picked (choice) and anything they wrote (text); the request's answer, as HumanRequest.list reads it and a human.answered event carries it, is the same {choice, text} (an approval's is {approved, text, note}). Without options, they answer in their own words. A question takes up to 20 options.
Ask for approval
approved = noclick.Human(email="finance@acme.com").approve(
"Refund $95.00 to Ana Lima?",
details={"invoice": "INV-1042", "reason": "Charged for a cancelled visit"},
).wait()
if approved:
issue_refund()An approval resolves to True or False. details is shown on the page, so put there whatever the person needs to decide. The person can add a note when they answer, and a decline usually carries one; request.note reads it.
An agent with NoClick's own tools asks this way too: a purchase, or a deletion that can't be undone (purging the trash, or deleting anything the trash doesn't keep), waits for your account's owner, on a page showing the exact price or what goes with it and which agent asked. A deletion to the trash happens at once, restorable for 30 days. The agent's turn waits on the answer, which arrives as the conversation's next turn.
Send a notice
user.human.notify("Your export is ready. It's in your dashboard.")A notice needs no answer; its operation succeeds as soon as it's sent.
Ask someone to connect an account
calendar = noclick.Connection.from_name("calendar", integration="google_calendar", create_if_missing=True, user=user)
calendar = user.human.connect(calendar, reason="to book your appointments").wait()This emails a link for the connection to that person and waits until they've connected it. connect also takes a connection's name, one of that user's connections. A user with no email address can't be sent one (noclick.InvalidRequest); make a connect link and pass it on yourself.
How the request reaches them
Every request has a page where the person answers, request.url. It shows your brand. With the default via="auto", NoClick emails the page to the person when it knows their email address:
print(request.url) # embed it, or send it yourself
print(request.delivered_via) # ["web", "email"]
print(request.undelivered) # channels that failed, each with a reasonvia=["web"] sends nothing and leaves delivery to you, which suits people you reach through your own app or chat.
Your account's owner can also get a request as a text, from NoClick's own WhatsApp and iMessage numbers to the number they've verified with NoClick (by texting it, or under Settings → Phone). For the owner, via="auto" texts them on the app they last texted NoClick on, else emails them:
noclick.Human.owner().notify("The new menu is live.") # on the app they text NoClick on
noclick.Human.owner().ask("Which logo?", options=["Round", "Square"], via=["whatsapp", "email"])A question's text carries its options and a link to its page. Texts cost nothing: they go out inside the conversation the owner's own last text opened, and WhatsApp refuses a text to an owner who hasn't written in the last 24 hours (it's listed in undelivered, and the email or page still reaches them). Only the owner gets texts: "whatsapp" or "imessage" for anyone else raises noclick.NotAvailable, so give your users a number of your own. "sms" and "call" raise noclick.NotAvailable for now.
Waiting without blocking
A request is an operation, so it outlives your process. In a web server, store its id and pick it up later:
request = user.human.approve("Publish the new pricing page?")
save(request.id)
# later, from anywhere
approved = noclick.Operation.from_id(saved_id).wait()A request expires after 7 days unless you set expires_in (seconds or a timedelta, up to 30 days). Waiting on an expired request raises noclick.Expired.
Answer from your own code
When the person who approves works in your own product, answer from there instead of the request's page:
for request in noclick.HumanRequest.list(status="pending", kind="approve", answerable="code"):
if request.request["details"].get("amount_usd", 0) < 50:
request.approve(note="Under $50: fine.")
else:
request.decline(note="Refunds over $50 go through finance.")approve and decline answer as the person the request waits on: with a key acting for your account, an approval routed to your account (a held call on one of its connections, say); with a user's session or inside as_user, one addressed to that user. Anyone else gets noclick.PermissionDenied. So does a purchase or a deletion an agent or a connected app made through NoClick's own tools: only your account's owner answers it, signed in to NoClick on its page (its request["signed_in_only"] is true, and url is that page). answerable="code" lists only what your code can answer, leaving those out.
An answered approval of a held tool call does more than resolve the request: approved, NoClick runs exactly the call that was held (an operation's; a held shell command or other tool the agent makes again itself), and the agent gets a turn in the same conversation with its result; declined, the agent gets a turn with the decline and the note, so it can tell the person it's working for why. Either way its reply goes out wherever the conversation lives. A request a policy rule routes to a named approver is theirs alone to answer, through its link.
Give an agent a person
A Human in an agent's tools lets the agent reach that person itself, and only that person:
manager = noclick.Human(email="ops@acme.com", phone="+14155550123")
support = noclick.Agent.from_name(
"support",
create_if_missing=True,
instructions="You answer billing questions. Ask the manager before refunding more than $200, "
"and hand the conversation to them if the customer asks for a person.",
tools=["stripe.create_refund", "search", manager],
)It gets three tools, each addressed to that person:
| Tool | What it does |
|---|---|
notify(message) | Sends them a message (an email, with a page to read it on); the turn carries on. |
ask(question, options=None) | Asks them. The turn waits, its operation awaiting_human (step asking), and their answer arrives as the conversation's next turn; the reply to that turn is the one message() returns. Meanwhile the conversation takes other messages. A question nobody answers in 7 days arrives as a turn saying so. One question per turn. |
hand_off(summary) | Hands them the conversation: they get a page and an email with the agent's summary and the conversation so far, and the page shows the live transcript with a reply box. Their replies post into the conversation as person messages, which history(), watch() and your chat UI read like any other. The agent stays quiet until the conversation is handed back: a message to it joins the transcript (the person is emailed, at most every 15 minutes, that there's a new one), and a wake-up or event waits for the agent. On a phone call, it transfers the call to the person's phone (the user's, for a user) once the agent's reply is spoken. |
manager.only("notify", "ask") gives just some of them. Your policy names them human.notify, human.ask and human.hand_off, so require_approval=["human.hand_off"] or deny=["human.*"] work as they do for any tool.
noclick.Human() in tools is the user each turn acts for (a thread with no user, your account's own, gets no tools from it), and user.human a particular user; noclick.Human.owner() is your account's owner, reached the way via="auto" reaches them, and a call is handed to their number (the agent their texts and calls reach, with answer_with, is told who they are in that conversation: each turn there opens with a note naming them as your account has them, name, email and the number they text from, so it never asks; no other conversation is told); noclick.Human(email=...) is anyone else, and NoClick reaches them by email, so they need one (a phone is kept for a call's transfer). Only your account's agents may name the owner or anyone else. A person is reached as your account, or as the user when the person is one, and each message is capped and charged like any request. Name a person once, with every tool they give; several people giving the same tool share one, which takes person to say whom.
A hand-off sends the agent.conversation.handed_off event, and the conversation's status is handed_off until it's handed back, from the person's page or from your code:
support.hand_back("ticket-1841", user="user_42")The agent answers again from the next message, which starts with what was said while the person had it and anything that woke it meanwhile. A hand-off nobody hands back returns to the agent after 30 days. A channel's conversation gets no person tools: the channel starts its turns.
A conversation that is an email thread goes on by email: the person's replies are emailed to the thread's correspondent from the inbox's address, as the agent's own replies are (with their limits and charge), and the page tells them so. A reply that can't be sent, past the 48 hours replies are possible after the customer's latest message, say, isn't posted either, and the page says why.
Answer the owner's texts and calls with an agent
What your account's owner texts NoClick's own WhatsApp and iMessage numbers, or says when they call NoClick's number, goes to NoClick's assistant. answer_with sends it to an agent of yours instead: a text's reply goes back on the app they texted on, and on a call the agent's reply is read aloud:
owner = noclick.Human.owner()
assistant = noclick.Agent.from_name(
"assistant", create_if_missing=True, instructions="You run the bakery's errands.", tools=["search", owner],
)
owner.answer_with(assistant)
assistant.message("What's on today?", conversation="owner") # the thread their texts are inEach text, and each thing they say on a call, is a turn in the agent's conversation "owner", so your code, the texts, the calls and anything else you point there share one thread. conversation= names another, a template over {channel} (whatsapp, imessage or call): conversation="owner-{channel}" keeps each app, and calls, their own. Give an inbox and a number the same conversation and the owner's email and calls to your numbers join it too.
Each turn opens with a short note saying how it came (a WhatsApp or iMessage text, or a phone call, with how to answer there), so the agent writes the way that app shows it;  in a text's reply goes as media. The note reaches the model only: the conversation keeps what they said, as a user message whose via is whatsapp, imessage or call. The agent's own budget and policy govern its turns. A text's reply goes back when the agent's turn ends, however long it takes; texts sent while the agent is still answering wait for it and go in its next turn together, in order. A call's turn starts at once: while the conversation is still busy with a text, the caller hears that the agent is on it. A turn that can't start (a paused agent, spending that's paused) says why, on the app or aloud. owner.answering() is the agent answering now, and owner.answer_with(None) hands the owner back to NoClick's assistant. Your users don't reach NoClick's numbers: give them a number of your own and its answer_with.
Add an email to the owner
An account made by texting NoClick has no email. add_email sends a code to one, and confirm_email with the code makes it the owner's, to sign in with:
owner = noclick.Human.owner()
owner.add_email("maya@morrowbakery.com")
confirmed = owner.confirm_email("482913") # the code in that email
print(confirmed.email, "merged" if confirmed.merged else "added")When an account already has that email, it takes this one in, everything it has with it (merged); whether one does is said only once the code matches. Only your account's own code, or an agent acting for it, adds the owner's email; an account that already has one gets noclick.Conflict. Called by an agent during its turn (its noclick tool), the email is verified and added once that turn ends. Codes last 15 minutes; an account gets three an hour, and five wrong codes end one.
Limits
A message is at most 4,000 characters. Each account sends up to 500 request emails a day (then noclick.RateLimited), and each email is charged.
Find requests
noclick.HumanRequest.list(status="pending", kind="approve")
noclick.HumanRequest.summary(status="live")The console lists every request too, and lets you answer the ones addressed to you.