Skip to content

Python

The Python SDK mirrors the TypeScript surface: one client, a profile module for memory, a tools module for connectors, and the same errors. Every example on this page runs as written against a test-mode sandbox.

bash
pip install configure-ai

Credentials

Fastest path, no signup: provision a test-mode sandbox and export what it returns.

bash
curl -s -X POST https://api.configure.dev/v1/sandbox/provision
# -> secret_key (sk_test_...), agent, sandbox_user.external_id ("sandbox-user"),
#    sandbox_user.test_phone (minted for your sandbox, not reusable from a doc)

export CONFIGURE_API_KEY=sk_test_...
export CONFIGURE_AGENT=sandbox-96d4069b
export CONFIGURE_TEST_PHONE=+1...

For live users, use your own sk_ key and agent from Get your credentials. Nothing else on this page changes.

The client

python
import os
from configure_ai import ConfigureClient

client = ConfigureClient(
    os.environ["CONFIGURE_API_KEY"],
    agent=os.environ["CONFIGURE_AGENT"],
    user_id="sandbox-user",
)

user_id is your own identifier for the user, the default subject for profile calls. It is sent as X-User-Id only when a request carries no user token: a token always wins, because the backend rejects a request whose user_id disagrees with the token subject.

Both a sync ConfigureClient and an AsyncConfigureClient exist; the async one has the same methods with await and an async with context manager.

Read the profile

python
profile = client.profile.read_profile()

print(profile["identity"]["name"])
print(profile["preferences"][:3])

# Ready for a system prompt, without hand-formatting the JSON.
context = profile.format()

read_profile() with no arguments returns the grounding overview: identity, preferences, a summary, connections, and an index of boxes and sources. Call it once before your agent's first message, not every turn.

Two narrower reads:

python
# Strict pages of the composed document, and nothing else.
imports = client.profile.read_profile(sections=["imports", "summary"])

# Open one shelf: a category ("work"), a source ("agents/atlas"), or "projects/<slug>".
work = client.profile.read_profile(box="work", detail="full")

The box ids come from the overview's own index, so an agent never has to guess a name.

python
hits = client.profile.search_profile(query="boat")
for memory in hits.results[:3]:
    print(memory["text"], "-", memory.get("source"))

search_profile() accepts box=, source=, from_=, to=, limit=, and detail=. It returns permitted attributed memories, so results can be filtered by the user's permissions rather than raising.

Write

Two ways in, and they are not interchangeable.

python
# One durable fact you already know is worth keeping.
client.profile.remember(fact="Prefers trains over planes under six hours", box="preferences")

# A whole turn. The backend extracts what is durable and drops the rest.
client.profile.commit(
    messages=[
        {"role": "user", "content": "Book me the 7am, I never take the aisle seat"},
        {"role": "assistant", "content": "Booked the 7am, window."},
    ],
    sync=True,
)

Use remember() for a fact, commit() for evidence. commit() is bounded: at most 20 messages of 16k characters each, at most 20 explicit memories of 1k characters, 50k characters total. Passing sync=True blocks until extraction finishes, which is what you want in a test and not in a request handler.

Bulk context goes through import_profile(), which splits a structured export into facts and distills anything else into one filed note:

python
result = client.profile.import_profile(
    text="Notes from kickoff: Nova wants metric units everywhere and reviews on Tuesdays.",
    kind="context",
    box="work",
)

Forget

forget() deletes memories your agent wrote, and takes exactly one selector:

python
client.profile.forget(match="metric units")                  # preview, deletes nothing
client.profile.forget(match="metric units", confirm=True)    # deletes the matches
client.profile.forget(scope="imports")                       # all of your own imports

The other two selectors are ids: forget(id="mem_...") deletes one memory, and forget(import_id=...) retracts a whole import (import_profile() returns that id as result.import_id when it split a structured export).

Without confirm=True, a match selector returns what it would delete. An unknown or malformed id is INVALID_INPUT, not NOT_FOUND, and retrying will not help.

Connectors need a user token

This is the part that trips people up. Profile read, search, and remember work with your secret key plus X-User-Id, which is what user_id= above sets. Connector queries do not: they require an agent-scoped token, which means the user has signed in and approved your agent.

The flow is: sign in, approve, agent token.

python
import httpx

resp = httpx.post(
    "https://api.configure.dev/v1/auth/sign-in/recognize-phone",
    headers={
        "X-API-Key": os.environ["CONFIGURE_API_KEY"],
        "X-Agent": os.environ["CONFIGURE_AGENT"],
    },
    # In test mode this is sandbox_user.test_phone from your own provision
    # response: it is minted per sandbox, so no number can be hardcoded here.
    json={"candidates": [os.environ["CONFIGURE_TEST_PHONE"]]},
).json()

token = resp["token"]

In test mode the synthetic user has already approved the sandbox agent, so recognition returns the token immediately. For real users, that token comes from your sign-in flow: hosted SSO (Configure your agent) or the OAuth client (OAuth Client Reference). A phone OTP verification returns a user token, which is not the same thing and will be rejected on connector routes with token_wrong_type.

With the token in hand, enable the connectors you want this turn and call them through execute_tool():

python
client.profile.tools(connectors=["gmail", "calendar"])

mail = client.profile.execute_tool(token, None, {
    "name": "configure_gmail_search",
    "input": {"query": "harbor", "max_results": 2},
})
for email in mail.emails:
    print(email.subject, "-", email.from_)

week = client.profile.execute_tool(token, None, {
    "name": "configure_calendar_get",
    "input": {"range": "week"},
})

execute_tool() is the connector surface: it dispatches each tool name to the live endpoint behind it, and refuses any tool the last tools() call did not enable, so a model cannot reach a connector you did not offer. profile.search_emails(token, query=...) is the same Gmail call without the tool-name indirection.

Passing a token also switches the subject: the token identifies the user, and X-User-Id is dropped from the request entirely.

connect() mints a Configure-hosted link. Never build one yourself.

python
link = client.profile.connect(app="gmail", purpose="Read your inbox for scheduling")
print(link.status, link.connect_url)

When a connector call fails because the connection died, the error carries the repair. See connector repair for the payload:

python
from configure_ai import ConfigureError

try:
    client.profile.search_emails(token, query="in:inbox")
except ConfigureError as error:
    repair = error.suggested_action
    if isinstance(repair, dict) and repair.get("type") == "reconnect":
        print("Ask the user to open:", repair["url"])
    elif repair in ("reconnect", "connect_tool"):
        print("Ask the user to open:", client.profile.connect(app="gmail").connect_url)
    else:
        raise

Model tool calls

tools() returns Anthropic-shaped tool definitions and enables exactly those tools for execute_tool():

python
definitions = client.profile.tools(connectors=["gmail"], actions=["email.send"])
names = [tool["name"] for tool in definitions]

result = client.profile.execute_tool(None, None, {
    "name": "configure_profile_remember",
    "input": {"fact": "Reviews hardware sketches on paper first"},
})

execute_tool() refuses any tool the last tools() call did not enable, so a model cannot reach a connector you did not offer this turn. Use to_openai_functions() for the OpenAI shape.

Errors

python
from configure_ai import ConfigureError, ErrorCode

try:
    client.profile.read_profile()
except ConfigureError as error:
    print(error.code, error.status_code, error.retryable)

The codes and their meanings match the TypeScript SDK: see Error Handling. Branch on error.code and the structured fields, never on message text.

Legacy methods

profile.get() and profile.get_memories() still call the old /v1/memory/* routes and emit a deprecation warning. They are not part of the current surface, and the routes they call are not mounted in production. Use read_profile() and search_profile().

Command line

bash
python -m configure_ai verify            # real sign-in, token exchange, one live profile read
python -m configure_ai verify --offline  # credentials and callback registration only
configure-ai add-callback --framework fastapi
configure-ai add-origin https://yourapp.com/auth/configure/callback

verify exits nonzero on failure, so CI can gate on a rotated secret or a callback that differs by a trailing slash.

Personalization infrastructure for agents