Async and concurrency
Every method is also awaitable, and the user you act for follows your code across tasks and threads.
Every method has an async form
Each method you call blocks and returns its result. The same method's .aio form is a coroutine:
import asyncio
import noclick
async def main():
agent = noclick.Agent.from_name("support")
reply = await agent.message.aio("Hi", user="user_42")
users = await noclick.User.list.aio(limit=10)
asyncio.run(main())It's the same method underneath, with the same arguments and errors.
Two things have no .aio form: the from_* constructors, which don't call the API, and properties that load their object the first time you read them, like agent.model on an agent from from_name. In async code, load such an object with await agent.hydrate.aio() before reading its properties, so the first read doesn't block your event loop.
Iterators
Methods that stream (agent.stream, agent.watch, channel.stream, inbox.stream, operation.events) return a regular iterator when you call them, and an async iterator from .aio():
async for message in inbox.stream.aio():
await handle(message)A turn streamed into your own UI, files and all:
agent = noclick.Agent.from_name("analyst")
async for event in agent.stream.aio("What stands out?", files=["q3-report.pdf"], user="user_42"):
await websocket.send_json(event.raw)Run calls concurrently
replies = await asyncio.gather(*(
agent.message.aio("Summarize my week", user=user_id) for user_id in user_ids
))Work that should happen later doesn't need a task of yours waiting for it. A wake-up gives a conversation its turn at a time, when an operation finishes, or on a schedule, and survives your process ending. A morning briefing in each user's own timezone, set for every user at once:
briefing = noclick.Cron("0 8 * * 1-5", tz="user")
routines = await asyncio.gather(*(
agent.wake.aio("briefing", every=briefing, note="Brief the user on today's calendar.", user=user_id)
for user_id in user_ids
))The current user follows your code
noclick.as_user works as with and async with. The user it sets travels with your code: into awaited calls, into tasks created inside the block, and into asyncio.to_thread. A thread you start yourself begins without it, and its calls act for your account, so pass user= in code that runs there.
async with noclick.as_user("user_42"):
gmail = noclick.Connection.from_name("gmail", integration="gmail", create_if_missing=True)
await gmail.wait.aio()Blocking calls work anywhere
The blocking form runs on a private event loop in a background thread, so it works in plain scripts, in Jupyter, and inside code that's already running an event loop. The one thing to avoid is calling the blocking form from inside the SDK's own loop, for example from an on_approval callback; use .aio there.
Clients and connections
A client keeps a connection pool per event loop. In a long-lived async app, close it when you shut down:
await noclick.default_client().aclose()