Metadata-Version: 2.4
Name: xences-ai
Version: 0.1.3
Summary: Python SDK for the Xences AI execution runtime
License: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: cryptography>=42.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"

# xences-ai

Python SDK for the Xences AI execution runtime.

```bash
pip install xences-ai
```

The client signs authentication requests with your application's RSA private key. Keep the private key in a server-side secret store — never expose it to clients or ship it in a browser or mobile application.

## Connect and send a goal

```python
import os
from xences_ai import Xences, Principal

xences = Xences(
    app_id=os.environ["XENCES_APP_ID"],
    private_key=os.environ["XENCES_PRIVATE_KEY"],
    base_url="https://xences.app",
)

result = xences.goals.create(
    request="Reschedule my appointment to Friday",
    principal=Principal(external_id=user.id),  # "human" is the default type
)
```

For async applications use `await xences.goals.acreate(...)`.

For multi-turn conversations, use the chat namespace with a stable session ID:

```python
reply = xences.chat.send(
    message="Can you list my collections?",
    session_id="user-123-session-1",
    principal=Principal(external_id="user-123"),
)
```

Use `await xences.chat.asend(...)` in async applications. Responses include the conversational `message`, `goalId`, and `conversationId` fields.

## Register operations

Expose handlers for operations the platform can invoke on your application, then register them once at startup:

```python
xences.expose(
    "orders.refund",
    method="POST",
    path="/orders/:id/refund",
    input_schema={
        "type": "object",
        "required": ["orderId"],
        "properties": {"orderId": {"type": "string"}},
    },
)(lambda input, context: orders.refund(input["orderId"]))

xences.register()
```

The decorator form is also supported:

```python
@xences.expose("orders.refund", method="POST", path="/orders/:id/refund")
def refund_order(input, context):
    return orders.refund(input["orderId"])

xences.register()
```

## Handle inbound invocations

After registering operations, mount a route so the platform can call back into your app. Pass `xences_public_key` to enable signature verification on inbound requests:

```python
xences = Xences(
    app_id=os.environ["XENCES_APP_ID"],
    private_key=os.environ["XENCES_PRIVATE_KEY"],
    xences_public_key=os.environ["XENCES_PUBLIC_KEY"],  # verifies inbound calls
    base_url="https://xences.app",
)
```

The default invoke path (`/.well-known/xences/invoke`) is sent to the platform when you call `register()`. Mount it in your web framework:

### Flask

```python
from flask import Flask, request
from xences_ai import XencesError

app = Flask(__name__)

@app.post("/.well-known/xences/invoke")
def invoke():
    try:
        return xences.handle_request(
            body=request.get_json(),
            headers=dict(request.headers),
        )
    except XencesError as e:
        return {"success": False, "error": str(e)}, e.status or 500
```

### FastAPI

```python
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from xences_ai import XencesError

app = FastAPI()

@app.post("/.well-known/xences/invoke")
async def invoke(request: Request):
    try:
        return await xences.ahandle_request(
            body=await request.json(),
            headers=dict(request.headers),
        )
    except XencesError as e:
        return JSONResponse(
            {"success": False, "error": str(e)},
            status_code=e.status or 500,
        )
```

`handle_request` and `ahandle_request` verify the platform's app identity, timestamp freshness, replay protection, and RSA signature before dispatching. They raise `XencesError` with a `status` code on any failure.

## Key rotation

Rotation is opt-in. Set `allow_key_rotation=True` and call `rotate_key()` only after registering the new public key with the platform operator:

```python
xences = Xences(
    ...,
    allow_key_rotation=True,
)

xences.rotate_key(new_public_key_pem)
```
