Metadata-Version: 2.4
Name: configure-ai
Version: 0.6.1
Summary: Python SDK for Configure — persistent memory infrastructure for AI agents
Author-email: Configure AI <support@configure.dev>
License-Expression: LicenseRef-Proprietary
Project-URL: Homepage, https://configure.dev
Project-URL: Documentation, https://docs.configure.dev/sdk/python
Project-URL: Repository, https://github.com/christianancheta/memory-link
Project-URL: Issues, https://github.com/christianancheta/memory-link/issues
Keywords: configure,memory,ai,sdk,api,personalization,user-context
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.24.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: respx>=0.20.0; extra == "dev"

# Configure SDK for Python

[![PyPI version](https://img.shields.io/pypi/v/configure-ai)](https://pypi.org/project/configure-ai/)

Official Python SDK for [Configure](https://configure.dev) — persistent user memory and identity for AI agents.

> Parity note: the profile surface is `read_profile`, `search_profile`, `remember`, `commit`, `import_profile`, `forget`, `connect`, `tools`, and `execute_tool`, and the connector surface is `client.tools.*` — both call the same routes as the TypeScript SDK. The older `profile.get()`, `profile.get_memories()`, and `profile.ingest()` methods call `/v1/memory/*` routes that are no longer mounted; they emit a deprecation warning and fail against production. Use the current methods.

## Installation

```bash
pip install configure-ai
```

## Credentials and the OAuth callback

Credentials come from `npx configure setup --users`, which opens Configure developer auth once and writes all five values to `.env`: `CONFIGURE_API_KEY`, `CONFIGURE_PUBLISHABLE_KEY`, `CONFIGURE_AGENT`, `CONFIGURE_OAUTH_CLIENT_ID`, and `CONFIGURE_OAUTH_CLIENT_SECRET`.

Everything after that has a Python command:

```bash
python -m configure_ai verify                       # real sign-in, token exchange, and one live profile read
python -m configure_ai verify --offline              # no browser: credentials, key, and exact callback registration
python -m configure_ai add-callback --framework fastapi   # or flask, django
python -m configure_ai add-origin https://yourapp.com/auth/configure/callback
```

`verify` fails loudly on the mistakes that otherwise surface as an opaque OAuth error mid-integration: a callback that differs from the registration by a port or a trailing slash, a client secret that was reissued out from under a deploy, a publishable key pasted into `CONFIGURE_API_KEY`. It exits nonzero on any failure, so CI can gate on it.

`add-callback` writes the callback route, the `client_secret_basic` code exchange, and the sign-in button snippet for your framework, keeping the secret server-side. The generated browser page recovers the PKCE verifier when `state` is missing and finishes through `Configure.completeSso()` in a popup instead of navigating. It never overwrites an existing file unless you pass `--force`.

`add-origin` registers a deployed callback on the client in your `.env`. It opens the dashboard to confirm the exact client and callback, because an `sk_` key cannot change an OAuth client, and prints the resulting callbacks. Registration is additive, so one `CONFIGURE_OAUTH_CLIENT_ID` covers local and production. The dashboard's [Sign-in (SSO)](https://configure.dev/sso) page does the same thing by hand.

## Quick Start

No signup needed to try it: `curl -X POST https://api.configure.dev/v1/sandbox/provision` returns a test key pair and a synthetic user whose profile is already full. See [Test mode](https://docs.configure.dev/getting-started/test-mode).

```python
from configure_ai import ConfigureClient

client = ConfigureClient(
    "sk_your_api_key",
    agent="your-agent",
    user_id="your-internal-user-id",
)

# Grounding context for a system prompt
profile = client.profile.read_profile()
print(profile["identity"]["name"])
print(profile.format())

# One durable fact
client.profile.remember(fact="Prefers aisle seats")

# A whole turn: the backend extracts what is durable
client.profile.commit(messages=[
    {"role": "user", "content": "Book the 7am, window seat as always"},
    {"role": "assistant", "content": "Booked."},
], sync=True)

client.close()
```

## Using Context Manager

```python
from configure_ai import ConfigureClient

with ConfigureClient("sk_your_api_key", agent="your-agent", user_id="your-internal-user-id") as client:
    profile = client.profile.read_profile()
```

## Async Usage

```python
import asyncio
from configure_ai import AsyncConfigureClient

async def main():
    async with AsyncConfigureClient(
        "sk_your_api_key", agent="your-agent", user_id="your-internal-user-id",
    ) as client:
        profile = await client.profile.read_profile()
        await client.profile.remember(fact="User is vegetarian")

asyncio.run(main())
```

## API-Only Unlinked Profiles

If your app already has stable user IDs, you can read and update profiles without hosted auth in the hot path. Pass `user_id` when constructing the server-side client with your `sk_...` key; the SDK sends it as `X-User-Id`.

```python
from configure_ai import ConfigureClient

client = ConfigureClient(
    "sk_your_api_key",
    user_id="your-internal-user-id",
)

profile = client.profile.read_profile()
client.profile.remember(fact="Prefers concise answers")
client.profile.import_profile(
    text="Known CRM or onboarding profile text",
    kind="context",
)
```

This creates an unlinked developer-scoped profile. Other developers' agents cannot read it, and connected tools require the user to link later with hosted auth using the same external ID.

## Message-Agent Line Registry

Message agents should register their current provider-owned return line before sending hosted `sign-in.me` links that include that phone.

```python
from configure_ai import ConfigureClient

client = ConfigureClient("sk_your_api_key", agent="your-agent")
agent_phone = sms_provider.current_phone()

line = client.auth.register_message_line(
    phone=agent_phone,
    channel="sms",
    label="Primary SMS line",
)

lines = client.auth.list_message_lines()
client.auth.revoke_message_line(phone=agent_phone, channel="sms")
```

Configure stores only a phone hash and last four digits. SDK results never include the raw phone number.

## Tool Connections

Connect user accounts to access their data. Tool APIs require an agent-scoped token from hosted auth or trusted headless auth; unlinked `user_id` profiles can use profile APIs but cannot access connected tools until linked.

```python
# Mint a Configure-hosted connect link — never build one yourself
link = client.profile.connect(token, app="gmail", purpose="Read your inbox for scheduling")
print(f"Send the user to: {link.connect_url}")

# Enable the connectors this turn, then call them by tool name
client.profile.tools(connectors=["gmail", "calendar"])

emails = client.profile.execute_tool(token, None, {
    "name": "configure_gmail_search",
    "input": {"query": "from:boss@company.com", "max_results": 5},
})
for email in emails.emails:
    print(f"- {email.subject}")

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

# Search every permitted Gmail and Outlook account
hosted = client.tools.search_hosted_emails(token, user_id, "shipping update")
if hosted.partial:
    print("Some accounts could not be searched")
```

When a connection has died, the failure carries its own repair: `ConfigureError.suggested_action` is either the legacy string (`reconnect`, `connect_tool`, `retry`) or the structured object `{"type": "reconnect", "app": ..., "reason": ..., "url": ...}`. Show that URL to the user. See [connector repair](https://docs.configure.dev/guides/connector-repair).

## Memory Operations

```python
# Grounding overview: identity, preferences, summary, connections, box index
profile = client.profile.read_profile(token, user_id)

# Strict pages of the composed document
imports = client.profile.read_profile(token, user_id, sections=["imports", "summary"])

# One box: a category, a source, or projects/<slug>
work = client.profile.read_profile(token, user_id, box="work", detail="full")

# Attributed search
hits = client.profile.search_profile(token, user_id, query="aisle seat")

# Save one durable fact
client.profile.remember(token, user_id, "User's preferred language is Spanish")

# Commit a turn and let the backend extract what is durable
result = client.profile.commit(
    token,
    user_id,
    messages=[{"role": "user", "content": "I always prefer aisle seats on flights"}],
    sync=True,
)
print(result.facts_written)

# Delete what your agent wrote (preview first, then confirm)
client.profile.forget(token, user_id, match="aisle seat")
client.profile.forget(token, user_id, match="aisle seat", confirm=True)
```

## Profile Operations

Structured read/write access to profile data.

```python
# Agent's own persistent storage
client.self.write("/soul.md", "I am TravelBot...")
soul = client.self.read("/soul.md")
listing = client.self.ls("/")
results = client.self.search("travel preferences")

# User's profile data (token-authenticated or constructor user_id)
summary = client.profile.read(token, user_id, "/summary.md")
client.profile.write(token, user_id, "/agents/travelbot/notes.md", "User prefers budget airlines")

# Peer agent profiles (read-only)
peer_soul = client.peer("wealthbot").read("/soul.md")
```

## API Reference

### ConfigureClient / AsyncConfigureClient

Main entry point for the SDK.

```python
ConfigureClient(
    api_key: str,
    base_url: str = "https://api.configure.dev",
    timeout: float = 30.0,
    agent: str | None = None,
    user_id: str | None = None
)
```

### Modules

- `client.auth` - Authentication (OTP flow)
- `client.profile` - Profile operations (read_profile, search_profile, remember, commit, import_profile, forget, connect, tools, execute_tool, read, write, ls, search, rm)
- `client.tools` - Tool connections, search, and sync
- `client.self` - Agent persistent storage
- `client.peer(name)` - Peer agent data (read-only)

### Auth Module

```python
client.auth.send_otp(phone: str) -> OtpStartResponse
client.auth.verify_otp(phone: str, code: str) -> OtpVerifyResponse
```

### Profile Module

```python
client.profile.read_profile(token, user_id, sections=None, box=None, detail=None) -> UserProfileResponse  # .format() on response
client.profile.search_profile(token, user_id, query, box=None, source=None, limit=None) -> ProfileSearchResponse
client.profile.remember(token, user_id, fact, box=None) -> RememberResponse
client.profile.commit(token, user_id, messages=None, memories=None, sync=None) -> ProfileCommitResult
client.profile.import_profile(token, user_id, text, kind=None, box=None) -> ImportProfileResponse
client.profile.forget(token, user_id, id=None, match=None, scope=None, import_id=None, confirm=None) -> ForgetResponse
client.profile.connect(token, user_id, app=None, capability=None, purpose=None) -> ConnectLinkResponse
client.profile.tools(connectors=None, actions=None) -> list[dict]
client.profile.execute_tool(token, user_id, tool_call) -> Any
client.profile.read(token, user_id, path) -> dict | None
client.profile.write(token, user_id, path, content) -> dict
client.profile.ls(token, user_id, path="/") -> dict
client.profile.search(token, user_id, query) -> dict
client.profile.rm(token, user_id, path) -> dict
```

### Tools Module

```python
client.tools.list(token) -> ListToolsResponse
client.tools.connect(token, tool, callback_url=None) -> ConnectToolResponse
client.tools.confirm(token, tool, connection_request_id) -> ConfirmToolResponse
client.tools.sync(token, tool) -> SyncToolResponse
client.tools.disconnect(token, tool) -> None
client.tools.disconnect_all(token) -> None
client.tools.sync_all(token, user_id, tools=None) -> dict
client.tools.search_emails(token, user_id, query, max_results=10) -> SearchEmailsResponse
client.tools.search_hosted_emails(token, user_id, query, max_results=10) -> SearchEmailsResponse
client.tools.search_spreadsheets(token, query, max_results=10, user_id=None) -> SearchSpreadsheetsResponse
client.tools.get_calendar(token, user_id, range="week") -> SearchCalendarResponse
client.tools.search_files(token, user_id, query, max_results=10) -> SearchFilesResponse
client.tools.search_notes(token, user_id, query, max_results=10) -> SearchNotesResponse
```

Every method above calls the `/v1/connectors/*` surface, the same routes the TypeScript SDK uses. `profile.execute_tool` reaches the same connectors by tool name (`configure_calendar_get`, `configure_drive_search`, `configure_notion_search`) when you want a model to choose the call.

Connector routes need an **agent token** — the token your sign-in flow returns after the user approves your agent. `X-User-Id` developer scope works on profile reads and writes, not on connector queries.

## Error Handling

All SDK methods raise `ConfigureError` with a typed `code` property and structured metadata:

```python
from configure_ai import ConfigureError, classify_error

try:
    profile = client.profile.read_profile(token, user_id)
except ConfigureError as e:
    if e.code == "AUTH_REQUIRED":
        # Token expired or invalid — re-authenticate
        # e.suggested_action == "reauthenticate"
        pass
    elif e.code == "RATE_LIMITED":
        # Too many requests — back off and retry
        # e.retryable == True, e.retry_after — seconds to wait
        pass
    elif e.code == "NETWORK_ERROR":
        # Connection failed — check connectivity
        # e.retryable == True
        pass
    else:
        print(f"[{e.code}] {e}")
```

Errors include structured properties: `e.type`, `e.param`, `e.retryable`, `e.suggested_action`, `e.doc_url`, `e.retry_after`, `e.request_id`. Use `e.retryable` to determine if a retry is safe. Use `classify_error(error)` in agent except blocks to classify any error into a `ConfigureError` with a friendly message. See [full error docs](https://docs.configure.dev/guides/error-handling).

| Code | HTTP Status | Meaning |
|------|-------------|---------|
| `API_KEY_MISSING` | — | No API key provided to constructor |
| `AUTH_REQUIRED` | 401, 403 | Invalid or expired token |
| `INVALID_INPUT` | 400 | Bad input (empty fields, path traversal) |
| `TOOL_NOT_CONNECTED` | 400 | Tool action on a disconnected tool |
| `ACCESS_DENIED` | 403 | Not authorized for this resource |
| `TOOL_ERROR` | varies | Tool operation failed (provider error) |
| `PAYMENT_REQUIRED` | 402 | Billing/quota limit reached |
| `NOT_FOUND` | 404 | Resource does not exist |
| `RATE_LIMITED` | 429 | Too many requests |
| `SERVER_ERROR` | 500+ | Server-side error |
| `NETWORK_ERROR` | — | Network/connection failure |
| `TIMEOUT` | — | Request timed out |

## Requirements

- Python 3.8+
- httpx >= 0.24.0

## License

Proprietary. All rights reserved. See [configure.dev](https://configure.dev) for licensing information.

## Links

- [Documentation](https://docs.configure.dev)
- [GitHub](https://github.com/christianancheta/memory-link)
- [Configure](https://configure.dev)
