Metadata-Version: 2.4
Name: configure-ai
Version: 0.6.0
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.

> PR 3.5 parity note: the Python SDK still exposes the legacy `/v1/memory/*` surface and has not been fully updated for typed memory entry search/detail results. Use the TypeScript SDK or HTTP API for the PR 3.5 typed memory entry contract until Python parity is completed.

## 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

```python
from configure_ai import ConfigureClient

# Initialize the client
client = ConfigureClient("sk_your_api_key")

# Authenticate user via OTP
client.auth.send_otp("+14155551234")
result = client.auth.verify_otp("+14155551234", "123456")
token = result.token
user_id = result.user_id

# Get user's profile
profile = client.profile.get(token, user_id)
print(f"User: {profile.get('user', {}).get('name', 'Unknown')}")

# Save a memory
client.profile.remember(token, user_id, "User's favorite color is blue")

# Close the client when done
client.close()
```

## Using Context Manager

```python
from configure_ai import ConfigureClient

with ConfigureClient("sk_your_api_key") as client:
    client.auth.send_otp("+14155551234")
    result = client.auth.verify_otp("+14155551234", "123456")
    profile = client.profile.get(result.token, result.user_id)
```

## Async Usage

```python
import asyncio
from configure_ai import AsyncConfigureClient

async def main():
    async with AsyncConfigureClient("sk_your_api_key") as client:
        await client.auth.send_otp("+14155551234")
        result = await client.auth.verify_otp("+14155551234", "123456")

        profile = await client.profile.get(result.token, result.user_id)
        await client.profile.remember(result.token, result.user_id, "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.get()
client.profile.remember(fact="Prefers concise answers")
client.profile.ingest(
    text="Known CRM or onboarding profile text",
    sync=True,
)
```

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
# List available tools
tools = client.tools.list(token)
for tool in tools.tools:
    print(f"{tool.name}: {'Connected' if tool.connected else 'Not connected'}")

# Connect Gmail
result = client.tools.connect(token, "gmail", "https://myapp.com/callback")
print(f"Redirect user to: {result.auth_url}")

# After OAuth callback, confirm the connection
confirmation = client.tools.confirm(token, "gmail", result.connection_request_id)

# Search user's emails
emails = client.tools.search_emails(token, user_id, "from:boss@company.com")
for email in emails.emails:
    print(f"- {email.subject}")

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

# Get calendar events
events = client.tools.get_calendar(token, user_id, "week")
for event in events.events:
    print(f"- {event.summary} at {event.start}")
```

## Memory Operations

```python
# Get the full profile
profile = client.profile.get(token, user_id)

# Get a specific path
user_data = client.profile.get(token, user_id, path="user")

# Get agent-specific data
app_data = client.profile.get(token, user_id, sections=["agents"])

# Save a memory
client.profile.remember(token, user_id, "User's preferred language is Spanish")

# Ingest a message for memory extraction
from configure_ai import ConversationMessage

result = client.profile.ingest(
    token,
    user_id,
    ConversationMessage(role="user", content="I always prefer aisle seats on flights"),
    "Travel preferences, dietary restrictions"
)

if result.relevant:
    print(f"Memories extracted: {result.memories_written}")
```

## 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 (get, remember, ingest, 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.get(token, user_id, path=None) -> UserProfileResponse  # .format() on response
client.profile.get_memories(token, user_id=None) -> MemoriesResponse
client.profile.remember(token, user_id, fact) -> RememberResponse
client.profile.ingest(token, user_id, messages, sync=True) -> IngestResponse
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.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
```

## 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.get(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)
