Metadata-Version: 2.4
Name: liberrex-copilot
Version: 0.2.0
Summary: Embed the Liberrex AI copilot in your product: expose your functions as tools, mint end-user sessions, run the bridge.
Author: Liberrex
License: Liberrex Embedded Copilot SDK — Proprietary License
        
        Copyright (c) 2026 Liberrex. All rights reserved.
        
        1. Grant. Subject to these terms, Liberrex grants you a non-exclusive, non-transferable, revocable license to install and use
           this software, unmodified or modified, solely to integrate your own applications with the Liberrex platform while you
           (or the organisation you act for) hold a valid Liberrex subscription or a written evaluation agreement with Liberrex.
        
        2. Restrictions. You may not (a) use this software to access any service other than the Liberrex platform; (b) sell,
           sublicense, rent or distribute it on its own, except as an unmodifiable part of your application delivered to your end
           users; (c) remove or alter this notice; (d) use it to build a product that competes with the Liberrex platform.
        
        3. Ownership. Liberrex retains all rights, title and interest in this software. No other rights are granted, by implication
           or otherwise.
        
        4. Termination. This license ends automatically when your Liberrex subscription or evaluation ends, or if you breach these
           terms. On termination you must stop using the software.
        
        5. No warranty. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING MERCHANTABILITY,
           FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
        
        6. Limitation of liability. TO THE MAXIMUM EXTENT PERMITTED BY LAW, LIBERREX SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
           SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM THE USE OF THE SOFTWARE.
        
        7. Your agreement with Liberrex. If you have a signed agreement with Liberrex that covers this software, that agreement
           prevails over these terms.
        
        Contact: https://ai.liberrex.com
        
Project-URL: Homepage, https://ai.liberrex.com
Project-URL: Documentation, https://ai.liberrex.com
Keywords: liberrex,copilot,ai,assistant,embedded,llm,tools
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Developers
Classifier: Typing :: Typed
Classifier: License :: Other/Proprietary License
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.25
Provides-Extra: pydantic
Requires-Dist: pydantic>=1.10; extra == "pydantic"
Provides-Extra: flask
Requires-Dist: flask>=2.0; extra == "flask"
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.95; extra == "fastapi"
Provides-Extra: django
Requires-Dist: django>=3.2; extra == "django"
Provides-Extra: sqlalchemy
Requires-Dist: sqlalchemy>=1.4; extra == "sqlalchemy"
Provides-Extra: cli
Requires-Dist: pyyaml>=5.4; extra == "cli"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pyyaml>=5.4; extra == "dev"
Requires-Dist: sqlalchemy>=1.4; extra == "dev"
Requires-Dist: pydantic>=2; extra == "dev"
Requires-Dist: flask>=2.0; extra == "dev"
Requires-Dist: fastapi>=0.95; extra == "dev"
Requires-Dist: httpx>=0.24; extra == "dev"
Dynamic: license-file

# liberrex-copilot (Python)

Embed the **Liberrex AI copilot** in your product. Your backend declares *tools* (plain Python functions), mints
short-lived session tokens for your signed-in users, and runs the tools when the copilot needs them — **as that end
user, under your own permission checks**. Your data never leaves your system except what a tool returns.

- Python ≥ 3.9, one runtime dependency (`requests`).
- Implements [Embedded Copilot protocol v1](../PROTOCOL.md). Pair it with `@liberrex/copilot-react` in the browser.

```bash
pip install liberrex-copilot                 # extras: [pydantic] [flask] [fastapi] [django] [sqlalchemy] [cli]
```

## Quick start

```python
import os
from typing import Literal, Optional
from liberrex_copilot import Copilot, Context, ToolError

copilot = Copilot(secret_key=os.environ["LIBERREX_SECRET_KEY"])   # base_url: env LIBERREX_API_URL or https://ai-api.liberrex.com

copilot.vocabulary("Atlas ERP manages invoices, orders, stock… 'DSO' means days sales outstanding.")
copilot.entity("Invoice", "A customer invoice",
               fields={"number": "str", "total": "decimal", "status": "draft|sent|paid|overdue"},
               relations={"customer": "Customer"})

@copilot.tool(description="Find invoices by customer and status.", permission="invoices.read", output="table")
def search_invoices(ctx: Context, customer: Optional[str] = None,
                    status: Optional[Literal["draft", "sent", "paid", "overdue"]] = None, limit: int = 20) -> list:
    """
    Args:
        customer: Customer name or id.
        limit: Maximum rows.
    """
    user = my_app.load_user(ctx.user.id, tenant=ctx.tenant.id)     # act as the end user…
    if not user.can("invoices.read"):                                 # …with YOUR permission rules
        raise ToolError("forbidden", "You cannot see invoices.")
    return my_app.invoices.search(user, customer=customer, status=status, limit=limit)

@copilot.tool(kind="write", confirm="Create a purchase order: {quantity} × {product}", permission="orders.write")
def create_purchase_order(ctx: Context, product: str, quantity: int) -> dict:
    """Create a purchase order for a product."""
    return my_app.purchasing.create_po(ctx.user.id, product, quantity)

copilot.sync()                     # PUT /manifest — version = hash of the manifest (no-op when unchanged)
copilot.start_worker(threads=4)    # poll mode: background threads, reconnect with backoff, graceful stop
```

Mint a session token for the browser (never send the secret key to the browser):

```python
@app.get("/api/copilot/token")
def copilot_token(request):
    u = request.user
    session = copilot.create_session(
        user={"id": u.id, "name": u.name, "email": u.email, "locale": "fr", "roles": u.roles,
              "attributes": {"region": u.region}},
        tenant={"id": u.org_id, "name": u.org_name},
        ttl_seconds=900)
    return {"token": session.token, "expires_at": session.expires_at}
```

## Declaring tools

`@copilot.tool(...)` (or bare `@copilot.tool`) options:

| option | meaning |
|---|---|
| `name` | defaults to the function name; must match `^[a-z][a-z0-9_]{1,63}$` |
| `description` | what the model reads to decide when to call the tool; defaults to the docstring summary |
| `kind` | `"read"` (default) runs automatically; `"write"` only after the end user confirms a card. Defaults to `write` when `confirm` is set |
| `confirm` | write tools: card title with `{argument}` placeholders, e.g. `"Send a reminder for {invoice}"`; `risk="high"` for dangerous actions |
| `permission` | a role string. Liberrex only offers the tool to users whose session `roles` contain it, **and** the SDK checks it again before running (`copilot.authorize = fn(ctx, permission) -> bool` to customise) |
| `output` | rendering hint when you return plain data: `table`, `record`, `kpis`, `list`, `text` |
| `timeout_ms` | 1000..30000 (default 10000 on Liberrex) |
| `input_schema` | explicit JSON Schema — disables generation and coercion |
| `enabled=False` | pushed disabled; enable it later in the Liberrex console |
| `simulates` | read tools only: the name of a **write** tool this tool simulates (what-if, see below) |

**Input schema** is generated from type hints: `str`, `int`, `float`, `bool`, `Decimal`, `date`, `datetime`, `time`,
`UUID`, `Literal[...]`, `Optional[...]` / `X | None`, `Union`, `list[...]` / `tuple` / `set`, `dict[str, X]`, `Enum`,
`TypedDict`, dataclasses, pydantic models (v1/v2) and `Annotated[X, "description"]`. Parameter descriptions come from
Google, NumPy or reST docstrings. Parameters with a default are optional. The first parameter annotated `Context`
(or named `ctx` / `context`) receives the context and is not part of the schema.

Arguments are **validated and coerced** before your function runs (`"5"` → `5`, ISO strings → `date`, enum values →
`Enum` members, dicts → dataclasses / pydantic models). Bad arguments return `invalid_arguments` to the model.

### What-if tools (`simulates`)

A read tool can declare that it *simulates* one of your write tools: it accepts the same (or compatible) arguments,
returns the predicted impact and changes nothing. The configuration advisor surface (§3.8 of the protocol) uses the
pair to propose a setting change, show its simulated impact (numbers from your simulation, never from the model) and
apply it through the write tool — after the user confirms.

```python
@copilot.tool(kind="write", permission="stock.manage", confirm="Set the reorder point of {product_id} to {reorder_point}")
def update_reorder_point(ctx: Context, product_id: str, reorder_point: int) -> dict:
    """Change a product's reorder point."""
    return my_app.stock.set_reorder_point(ctx.user.id, product_id, reorder_point)

@copilot.tool(simulates="update_reorder_point", permission="stock.read", output="kpis")
def simulate_reorder_point(ctx: Context, product_id: str, reorder_point: int) -> dict:
    """What-if: stock-outs, extra stock value and order frequency with a new reorder point (changes nothing)."""
    return my_app.stock.simulate_reorder_point(ctx.user.id, product_id, reorder_point)
```

Checks: `simulates` is only allowed on a read tool (`ManifestError` at registration otherwise); `validate()` /
`sync()` fail when the target is unknown or is not a write tool, and warn when the simulation does not accept one of
the write tool's required arguments.

### Context

`ctx.user` (`id`, `name`, `email`, `locale`, `roles`, `attributes`), `ctx.tenant` (`id`, `name`), `ctx.session_id`,
`ctx.call_id`, `ctx.locale`, `ctx.deadline`, `ctx.has_role("a", "b")`. These are exactly the values your backend
passed to `create_session`. `Context.for_testing(user_id=…, roles=[…])` helps unit-test tools.

### Results

Return anything JSON-serialisable: `Decimal`, `date`/`datetime`, `Enum`, `UUID`, dataclasses, pydantic models,
namedtuples and SQLAlchemy rows are converted. Results over 256 KB are truncated (the longest list is cut and a note
is added) so the call never fails for size.

- `raise ToolError("not_found", "No invoice F-9")` → `{ok: false, error: {code, message}}` (message ≤ 500 chars,
  shown to the model — keep it user-safe).
- Any other exception → `{ok: false, error: {code: "internal_error"}}`; the traceback is logged locally, never sent.
- Display hints for the UI (otherwise `output` decides):

```python
return copilot.display.table(rows, columns=[("number", "Invoice"), ("total", "Total", "currency")], title="Overdue")
return copilot.display.kpis([("Overdue", 3), {"label": "Amount", "value": 1250.5, "unit": "EUR"}], data=summary)
return copilot.display.text("**Done.**")
```

Column formats: `text`, `number`, `currency`, `percent`, `date`, `datetime`.

Run a tool locally exactly as Liberrex would: `copilot.run_tool("search_invoices", {"customer": "Acme"},
user={"id": "u1", "roles": ["invoices.read"]}, tenant={"id": "t1"})`.

## The bridge: poll (default) or webhook

**Poll mode** works behind NAT/firewalls (no inbound endpoint): `worker = copilot.start_worker(threads=4)` starts a
long-polling thread plus a pool of tool threads. It reconnects with exponential backoff (≤ 30 s), skips calls whose
deadline passed, retries posting results, and stops gracefully with `copilot.stop_worker()` (running calls finish and
their results are posted). `copilot.start_worker(block=True)` runs until Ctrl-C / SIGTERM — handy for a dedicated
worker process. `worker.stats` and `worker.last_error` expose counters. For cron-style use: `copilot.poll_once()`.

Run one worker per process you want polling; any number of processes can poll (each call is claimed exactly once).

**Webhook mode** (choose it in the Liberrex console, set the webhook secret `lxwh_…`):

```python
copilot = Copilot(secret_key=..., webhook_secret=os.environ["LIBERREX_WEBHOOK_SECRET"])

# Flask
from liberrex_copilot.flask import blueprint
app.register_blueprint(blueprint(copilot), url_prefix="/liberrex")          # POST /liberrex/webhook
# FastAPI
from liberrex_copilot.fastapi import router
app.include_router(router(copilot), prefix="/liberrex")
# Django
from liberrex_copilot.django import view
urlpatterns = [path("liberrex/webhook", view(copilot))]
# Anything else
status, body = copilot.handle_webhook(raw_body_bytes, headers)
```

Requests are verified (`X-Liberrex-Signature: v1=<HMAC-SHA256>` over `timestamp + "." + raw body`, constant-time
compare) and rejected when the timestamp is more than 300 s away.

## Other API

| | |
|---|---|
| `copilot.manifest()` / `copilot.validate()` | the manifest that `sync()` pushes / `(errors, warnings)` checked locally |
| `copilot.get_manifest()` | what Liberrex stores (with console `enabled` flags and description overrides) |
| `copilot.usage("2026-10-01", "2026-10-13")` | per-day messages, tool calls, credits, active users |
| `copilot.ping()` | connectivity + key check |
| exceptions | `APIError` → `AuthenticationError` (401), `CreditsExhaustedError` (402), `PermissionDeniedError` (403), `NotFoundError`, `PayloadTooLargeError`, `ValidationError` (422, `.details`), `RateLimitError` (`.retry_after`), `NotConfiguredError`, `ServerError`, `ConnectionFailedError`; `ManifestError` for local validation |

Logging uses `logging.getLogger("liberrex_copilot")`. Secrets are never logged (reprs mask the key) and tool
arguments / results are never logged at INFO.

## CLI

```bash
export LIBERREX_SECRET_KEY=lxsk_test_…  LIBERREX_API_URL=http://localhost:5001

liberrex-copilot draft --sqlalchemy myapp.models:Base -o copilot.yaml      # entities + list/get read tools
liberrex-copilot draft --django --settings mysite.settings -o copilot.yaml
liberrex-copilot draft --openapi openapi.json -o copilot.yaml              # GET → read tools, others → write tools
liberrex-copilot validate copilot.yaml
liberrex-copilot push copilot.yaml           # disabled tools are pushed with enabled:false (--only-enabled to omit)
liberrex-copilot push --app myapp.copilot:copilot      # push the manifest of a Copilot object
liberrex-copilot sessions create --user-id u1 --roles invoices.read,orders.write --tenant-id acme --json
liberrex-copilot manifest                    # show what Liberrex stores
liberrex-copilot ping
```

`draft` never reads rows — only model shapes. Sensitive-looking columns (`password`, `token`, `secret`, `hash`…) are
skipped, and every proposed tool is `enabled: false` until a human reviews it. YAML needs PyYAML (`[cli]` extra);
without it the CLI reads and writes JSON.

## Development

```bash
cd sdk/python
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest            # fake Liberrex server on 127.0.0.1 — no network
```

See `examples/embedded-erp/backend` for a complete FastAPI app (Atlas ERP) using this SDK.
