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.

Parameters
emailstr | Nonedefault None
phonestr | Nonedefault None
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Usage

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) -> Human

Your 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.

Parameters
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Human

The owner, to message, ask, put in an agent's tools= or answer_with.

Usage

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) -> Human

Send 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.

Parameters
agentAnyrequired
conversationstr | Nonedefault None
Returns
Human

This person.

Usage

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 thread

answeringasync

human.answering() -> Any | None

The agent this person's texts to NoClick's own numbers go to (answer_with), or None when NoClick's assistant answers them.

Returns
Any | None

The noclick.Agent, or None.

Usage
agent = noclick.Human.owner().answering()
print(agent.name if agent else "NoClick's assistant")

add_emailasync

human.add_email(email) -> None

Send 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.

Parameters
emailstrrequired
Usage
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) -> OwnerEmail

The 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.

Parameters
codestrrequired
Returns
OwnerEmail

The confirmed email.

Usage
confirmed = noclick.Human.owner().confirm_email("482913")
print(confirmed.email, "merged" if confirmed.merged else "added")

only

human.only(*tools) -> HumanOperations

Some 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.

Parameters
*toolsstr
Returns
HumanOperations

A ref to just those tools of this person, to list in tools=.

Usage

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) -> HumanRequest

Ask a question; .wait() returns an Answer.

Parameters
messagestrrequired
optionsSequence[str] | Nonedefault None
viastr | Sequence[str]default 'auto'
expires_inint | float | timedelta | Nonedefault None
Returns
HumanRequest

The request, already sent. wait() blocks until the person answers and gives what they picked (choice) and wrote (text).

Usage

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) -> HumanRequest

Ask for a yes/no; .wait() returns True when approved.

Parameters
messagestrrequired
detailsAnydefault None
viastr | Sequence[str]default 'auto'
expires_inint | float | timedelta | Nonedefault None
Returns
HumanRequest

The request, already sent. wait() gives True if they approve and False if they decline.

Usage

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 anywhere

notifyasync

human.notify(message, *, via='auto') -> HumanRequest

Send a notice (the operation succeeds once it's sent).

Parameters
messagestrrequired
viastr | Sequence[str]default 'auto'
Returns
HumanRequest

The request, already sent and its operation already succeeded, so wait() returns None at once. undelivered names any channel that failed.

Usage
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) -> ConnectLink

Ask this person to connect connection (a Connection or its name, this person's own); .wait() returns the connected Connection.

Parameters
connectionAnyrequired
reasonstr | Nonedefault None
redirect_urlstr | Nonedefault None
Returns
ConnectLink

The link, already emailed: its url and when it expires_at. wait() gives the connected Connection.

Usage

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.

Attributes
request_idstr | None

The request's own id (the operation's is id).

request_statusstr | None

pending, answered, delivered, expired or canceled.

requestdict[str, Any]

The request as created: url, delivered_via, undelivered, ...

urlstr | None

The page where the person answers (/h/{id}); embed or send it yourself.

delivered_vialist[str]
undeliveredlist[dict[str, Any]]

Channels that failed, each with a reason.

notestr | None

What the approver wrote with their answer (None until answered, or if they wrote nothing).

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).

Parameters
statusManydefault None
status_notManydefault None
kindManydefault None
answerablestr | Nonedefault None
limitint | None

Return at most this many.

(default None)
user_filterAny

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.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
list[HumanRequest]

Every matching request, fetched page by page. Each is also the operation you can wait() on; request_status is the request's own status.

Usage
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
) -> Page

One page of requests; pass next_cursor back as cursor.

Parameters
cursorstr | None

The next_cursor of the page before; omit it for the first page.

(default None)
limitint | None

Return at most this many.

(default None)
statusManydefault None
status_notManydefault None
kindManydefault None
answerablestr | Nonedefault None
user_filterAny

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.

(default None)
userAny

Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).

(defaults to the current user)
clientClient | None

The client to send with; defaults to the module client built from NOCLICK_API_KEY.

(default None)
Returns
Page

A page whose data holds the requests and whose next_cursor is None on the last page.

Usage
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) -> OperationSummary

How many human-request operations (ask, approve, notify) match, by operation status; takes Operation.summary's filters except kind.

Parameters
**filtersAny
Returns
OperationSummary

The number that match in total, and per operation status in by_status.

Usage
counts = noclick.HumanRequest.summary(status="live")
print(counts.total, counts.by_status)

approveasync

request.approve(*, note=None) -> HumanRequest

Approve 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.

Parameters
notestr | Nonedefault None
Returns
HumanRequest

This request, answered: request_status is answered and note is yours.

Usage
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) -> HumanRequest

Decline 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.

Parameters
notestr | Nonedefault None
Returns
HumanRequest

This request, answered: request_status is answered and note is yours.

Usage
[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 HumanOperations

Some of a person's tools for an agent (human.only(...)), for tools=.

Attributes
humanHuman
toolslist[str]

Answer

class Answer

What the person chose (choice) and/or wrote (text).

Attributes
choicestr | None
textstr | None
rawAny

The response JSON exactly as the API sent it.

OwnerEmail

class OwnerEmail

An 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.

Attributes
emailstr
statusstr
mergedbool