Human
Humans: ask a person, get an approval, send a notice, request a connection.
noclick.Human(*, email=None, phone=None, user=..., client=None)A person to reach. user.human (or noclick.Human(), meaning the
user current at each call, or the one a session or user-pinned key acts
for) is the user; noclick.Human.owner() is your account's owner;
noclick.Human(email=...) is anyone else.
Every method returns an Operation: .wait() for the outcome.
In an agent's tools=, a person gives the agent tools that notify them,
ask them, or hand them the conversation (only).
via says where a message goes: "auto" (the default), or a list of
"web" (only the page: you pass request.url on), "email", and for the
owner "whatsapp" and "imessage", sent from NoClick's own numbers to the
number they text NoClick from. "auto" is the app the owner last texted
NoClick on, else email. "sms" and "call" raise noclick.NotAvailable
for now. What couldn't be sent is listed in undelivered, never implied.
NoneNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Reach the person behind a user, or anyone by email:
user = noclick.User.from_id("user_42")
user.human.notify("Your export is ready. It's in your dashboard.")
noclick.Human(email="finance@acme.com").approve("Refund $95.00 to Ana Lima?")noclick.Human() is the user you're acting for, and the only way to
reach a person from a session or a key pinned to a user:
with noclick.as_user(noclick.User.from_id("user_42")):
noclick.Human().ask("Which time works for your cleaning?", options=["Thu 10:30", "Fri 14:00"])ownerclassmethod
noclick.Human.owner(*, client=None) -> HumanYour account's owner: the person behind the account, reached by email and on
the app they text NoClick from. Only your account reaches them: it always acts as
your account, inside as_user too, and a user's session or a key pinned to a user
raises noclick.PermissionDenied.
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The owner, to message, ask, put in an agent's tools= or answer_with.
Tell the owner something on the app they text NoClick on:
noclick.Human.owner().notify("The site is live: https://morrowbakery.com")Or give an agent a way to reach them:
assistant = noclick.Agent.from_name(
"assistant", create_if_missing=True, instructions="You run the bakery's errands.",
tools=["search", noclick.Human.owner()],
)answer_withasync
human.answer_with(agent, *, conversation=None) -> HumanSend what this person texts or says to NoClick's own numbers (its WhatsApp and
iMessage numbers and its voice number) to agent (an noclick.Agent or id of your
account's). A text's reply goes back on the app they texted on once the agent's turn
ends, and texts sent while it's busy go in its next turn, in order; on a call, each thing
they say is a turn whose reply is read aloud as it's written. None hands them back to
NoClick's assistant. Only Human.owner() reaches NoClick's numbers: your users reach an
agent on a number of your own (PhoneNumber.answer_with).
conversation is the agent's conversation each text or call lands in, a template over
{channel} (whatsapp, imessage or call); omitted, "owner". Give an inbox
and a number the same key and the person's email, calls and texts are one conversation.
NoneThis person.
Make an agent the owner's assistant on every channel:
owner = noclick.Human.owner()
assistant = noclick.Agent.from_name("assistant")
owner.answer_with(assistant)
noclick.Inbox.from_name("assistant", create_if_missing=True, agent=assistant, senders="owner",
conversation="owner")
assistant.message("What's on today?", conversation="owner") # the same threadansweringasync
human.answering() -> Any | NoneThe agent this person's texts to NoClick's own numbers go to (answer_with), or
None when NoClick's assistant answers them.
The noclick.Agent, or None.
agent = noclick.Human.owner().answering()
print(agent.name if agent else "NoClick's assistant")add_emailasync
human.add_email(email) -> NoneSend a code to email for the owner of an account made by texting NoClick (which
has none); confirm_email with the code makes it theirs. Raises
noclick.Conflict when the account already has an email, noclick.RateLimited
after too many codes.
owner = noclick.Human.owner()
owner.add_email("maya@morrowbakery.com")
owner.confirm_email(input("The code in that email: "))confirm_emailasync
human.confirm_email(code) -> OwnerEmailThe code add_email sent: on a match the email is the owner's to sign in with,
or, when an account already has it, that account takes this one in (merged).
Whether an account has the email is said only once the code matches.
The confirmed email.
confirmed = noclick.Human.owner().confirm_email("482913")
print(confirmed.email, "merged" if confirmed.merged else "added")only
human.only(*tools) -> HumanOperationsSome of the tools this person gives an agent, for tools=. In an agent's tools
a Human gives it three, each addressed only to this person: notify (a
message), ask (a question, with options if it has some: the turn waits,
awaiting_human, and the answer arrives as the conversation's next turn) and
hand_off (the conversation goes to this person: a page and an email with the
transcript, the agent's summary and a reply box; their replies post as person
messages, the agent stays quiet until agent.hand_back(conversation), and on a
call it's transferred to the person's phone). Policy names them
human.notify, human.ask and human.hand_off.
noclick.Human() there is the user each turn acts for; user.human is that user;
noclick.Human.owner() is your account's owner and noclick.Human(email=..., phone=...)
anyone else, neither of which a user's own agent may name.
A ref to just those tools of this person, to list in tools=.
Let a support agent ask a manager before a big refund, but never hand them the chat:
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 over $200.",
tools=["stripe.create_refund", manager.only("notify", "ask")],
)Or let it hand an upset customer to a person, who replies from their page:
person = noclick.Human(email="support-lead@acme.com")
support = noclick.Agent.from_name(
"support", create_if_missing=True,
instructions="Hand the conversation to a person when the customer asks for one.",
tools=["search", person.only("hand_off")],
)askasync
human.ask(message, *, options=None, via='auto', expires_in=None) -> HumanRequestAsk a question; .wait() returns an Answer.
None'auto'NoneThe request, already sent. wait() blocks until the person answers
and gives what they picked (choice) and wrote (text).
Offer choices, then wait for the pick:
human = noclick.User.from_id("user_42").human
request = 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)With via=["web"] nothing is sent: pass the answer page on
yourself. expires_in is in seconds (or a timedelta):
human = noclick.User.from_id("user_42").human
request = human.ask("What should we call your workspace?", via=["web"], expires_in=3600)
print(request.url)approveasync
human.approve(message, *, details=None, via='auto', expires_in=None) -> HumanRequestAsk for a yes/no; .wait() returns True when approved.
None'auto'NoneThe request, already sent. wait() gives True if they approve
and False if they decline.
Put what the person needs to decide in details; it's shown on the page:
finance = noclick.Human(email="finance@acme.com")
approved = finance.approve(
"Refund $95.00 to Ana Lima?",
details={"invoice": "INV-1042", "reason": "Charged for a cancelled visit"},
).wait()
if approved:
issue_refund()In a web server, save the request's id and pick the answer up later:
request = noclick.User.from_id("user_42").human.approve("Publish the new pricing page?")
save(request.id)
approved = noclick.Operation.from_id(saved_id).wait() # later, from anywherenotifyasync
human.notify(message, *, via='auto') -> HumanRequestSend a notice (the operation succeeds once it's sent).
'auto'The request, already sent and its operation already succeeded, so
wait() returns None at once. undelivered names any channel that failed.
human = noclick.User.from_id("user_42").human
human.notify("Your export is ready. It's in your dashboard.")connectasync
human.connect(connection, *, reason=None, redirect_url=None) -> ConnectLinkAsk this person to connect connection (a Connection or its
name, this person's own); .wait() returns the connected
Connection.
NoneNoneThe link, already emailed: its url and when it expires_at.
wait() gives the connected Connection.
Email the user a link to connect their calendar, and wait until they have:
user = noclick.User.from_id("user_42")
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()A connection the user already has can be named instead:
user = noclick.User.from_id("user_42")
user.human.connect("calendar", redirect_url="https://app.example.com/settings")HumanRequest
class HumanRequest(Operation)A request to a person; also the operation that finishes when they answer.
- Inherits from Operation.
| request_id | str | None | The request's own id (the operation's is |
| request_status | str | None |
|
| request | dict[str, Any] | The request as created: |
| url | str | None | The page where the person answers ( |
| delivered_via | list[str] | |
| undelivered | list[dict[str, Any]] | Channels that failed, each with a |
| note | str | None | What the approver wrote with their answer ( |
listclassmethodasync
noclick.HumanRequest.list(
*, status=None, status_not=None, kind=None, answerable=None, limit=None,
user_filter=None, user=..., client=None
) -> list[HumanRequest]Requests your code and agents sent, newest first (or the first limit).
answerable="owner": only those the signed-in account owner may answer;
answerable="code": only those your code may answer with approve or
decline, leaving out the ones answered only signed in to NoClick (a held
purchase or deletion, whose request["signed_in_only"] is true).
NoneNoneNoneNoneReturn at most this many.
None)Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Every matching request, fetched page by page. Each is also the
operation you can wait() on; request_status is the request's own status.
for request in noclick.HumanRequest.list(status="pending", kind="approve"):
print(request.request["message"], request.url)
waiting_on_you = noclick.HumanRequest.list(answerable="owner")
yours_to_answer = noclick.HumanRequest.list(status="pending", kind="approve", answerable="code")list_pageclassmethodasync
noclick.HumanRequest.list_page(
*, cursor=None, limit=None, status=None, status_not=None, kind=None,
answerable=None, user_filter=None, user=..., client=None
) -> PageOne page of requests; pass next_cursor back as cursor.
The next_cursor of the page before; omit it for the first page.
None)Return at most this many.
None)NoneNoneNoneNoneWhich users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)A page whose data holds the requests and whose next_cursor is None on the last page.
page = noclick.HumanRequest.list_page(status="pending", limit=50)
while True:
for request in page.data:
print(request.request_status, request.url)
if page.next_cursor is None:
break
page = noclick.HumanRequest.list_page(status="pending", limit=50, cursor=page.next_cursor)summaryclassmethodasync
noclick.HumanRequest.summary(**filters) -> OperationSummaryHow many human-request operations (ask, approve, notify) match, by operation
status; takes Operation.summary's filters except kind.
The number that match in total, and per operation status in by_status.
counts = noclick.HumanRequest.summary(status="live")
print(counts.total, counts.by_status)approveasync
request.approve(*, note=None) -> HumanRequestApprove it from your own code, as the person it 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. A held operation call
then runs exactly as it was held (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, its reply going
out wherever the conversation lives (the chat, the call, or on an email thread the same
thread: an inbox agent's reply tool is bound to the thread's latest message, not to the
message that woke a turn). Anyone else gets noclick.PermissionDenied.
NoneThis request, answered: request_status is answered and note is yours.
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.")declineasync
request.decline(*, note=None) -> HumanRequestDecline it from your own code, as approve answers it. A held call never runs; the agent
gets a turn saying it was declined, with your note, so it can tell the person it was
acting for why.
NoneThis request, answered: request_status is answered and note is yours.
[request] = noclick.HumanRequest.list(status="pending", kind="approve", answerable="code", limit=1)
request.decline(note="Refunds over $500 go through finance; ask the customer for the invoice.")HumanOperations
class HumanOperationsSome of a person's tools for an agent (human.only(...)), for tools=.
| human | Human | |
| tools | list[str] |
Answer
class AnswerWhat the person chose (choice) and/or wrote (text).
| choice | str | None | |
| text | str | None | |
| raw | Any | The response JSON exactly as the API sent it. |
OwnerEmail
class OwnerEmailAn email confirmed for the owner: status is added, or verified when an
agent's turn confirmed it (it's added once that turn ends); merged when an account
that already had the email takes this one in.
| str | ||
| status | str | |
| merged | bool |