Metadata-Version: 2.4
Name: disclarion
Version: 0.2.3
Summary: AI disclosure logging SDK for Disclarion
Author-email: Disclarion <hello@disclarion.com>
License: MIT
License-File: LICENSE
Requires-Python: >=3.9
Requires-Dist: anthropic>=0.34
Requires-Dist: google-genai>=1.0
Requires-Dist: openai>=1.0
Requires-Dist: requests>=2.28
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == 'dev'
Description-Content-Type: text/markdown

# disclarion

AI disclosure logging SDK. Wraps your LLM provider responses and reports
standardized disclosure/labeling metadata to Disclarion.

## Install

```bash
pip install disclarion
```

## Usage

```python
from disclarion import Disclarion
import openai

client = openai.OpenAI()
dc = Disclarion(api_key="dcl_live_...")

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "hello"}],
)
dc.track(response, session_id="session-123")
```

`track()` never modifies `response` by default — it attaches a
`disclarion_meta` dict to it instead, and your own UI decides what to show:

```python
tracked = dc.track(response, session_id="session-123")
tracked.disclarion_meta
# {"ai_generated": True, "is_first_message": True, "disclosed": False, "logged_at": "..."}
```

`is_first_message` is what a host-app disclosure widget should key off of —
show a one-time blocking notice on `True`, keep a small persistent badge up
for the rest of the session on `False`. A ready React and vanilla-JS
implementation of exactly that pattern ships inside every install — find it
with:

```bash
python -c "import disclarion; print(disclarion.widget_dir())"
```

This is the `disclosure_mode="modal_once"` default, set on the client, not
per call:

- **`modal_once`** (default) — never touches response content; only
  `disclarion_meta` changes, as above.
- **`inline_every_message`** — the SDK's original behavior, kept for
  backward compatibility. Despite the name, it still only rewrites the
  response's own content on a session's *first* message (never later
  ones) to append/prepend `disclosure_text`; `disclarion_meta["disclosed"]`
  reflects whether that happened.
- **`off`** — pure logging, no disclosure signal at all:
  `disclarion_meta` omits `is_first_message`/`disclosed` entirely. Use
  this if your product already has its own unrelated AI-disclosure UI.

```python
dc = Disclarion(api_key="dcl_live_...", disclosure_mode="inline_every_message")
```

Regardless of `disclosure_mode`, every `track()` call is still logged to
Disclarion's audit trail (`interaction_logs`) exactly the same way — the UI
mode only changes what happens to `response` and `disclarion_meta`, never
what gets recorded for compliance.

`track()` makes one fast synchronous attempt (that's what makes
`is_first_message` available immediately) and never retries inline — a
slow or erroring Disclarion backend can never turn into multi-second
latency on your own request path. Anything worth retrying (a network
error, a `5xx`, a transient `429`) is handed to a background thread with
a bounded queue (up to 1000 pending logs, oldest dropped first if your
process is generating logs faster than Disclarion can accept them) that
retries with capped, jittered exponential backoff, honoring a `429`'s
`Retry-After` header when present. The queue is flushed on normal
process exit, `SIGINT`, and `SIGTERM` (bounded to a couple of seconds, so
shutdown is never blocked indefinitely by a backend that's still down).

A `429` that means "you've exceeded your monthly plan quota" is handled
differently from a transient rate limit: retrying it is pointless until
next period, so the SDK opens a circuit breaker instead — every `track()`
call skips the network entirely (sub-millisecond) until an hourly (then
exponentially longer, capped at 24h) cooldown elapses and one probe
request checks whether it's resolved. One `logging.warning` marks each
state change (opened/resolved), not one per call, so a month-long outage
doesn't spam your logs. Call `dc.get_stats()` any time for the queue size,
circuit state, and delivery counters.

An invalid or revoked API key is different from all of the above: the
backend is reachable and is explicitly rejecting it, which is a setup
mistake you need to see, so that raises `disclarion.AuthenticationError`
immediately — no retry, no circuit-breaking, no silent fallback.
Unsupported response types raise a `ValueError` immediately for the same
reason: both are programming/config errors to fix, not a runtime
condition to swallow.

OpenAI (`ChatCompletion`), Anthropic (`Message`), and Gemini
(`GenerateContentResponse`) responses are all supported — see
`disclarion/adapters/`.

## Using this with an async framework (FastAPI, aiohttp, Starlette, ...)

`track()` is synchronous under the hood (it uses `requests`) and blocks for
up to `FAST_PATH_TIMEOUT_SECONDS` (currently 1.5s) even on a healthy
backend. Called directly inside an `async def` handler, that blocks the
whole worker's event loop for that long — stalling every other concurrent
request on the same worker, not just the one that called `track()`. Get it
off the event loop:

```python
# FastAPI / Starlette
from starlette.concurrency import run_in_threadpool

tracked = await run_in_threadpool(dc.track, response, session_id=session_id)
```

```python
# Any asyncio app, no Starlette dependency
import asyncio

tracked = await asyncio.to_thread(dc.track, response, session_id=session_id)
```

This applies even if the backend you're calling is your own service — a
synchronous `track()` call made from inside an async handler can, in the
worst case, block that same process against itself.

## Testing without a live backend

To exercise `_send_log()` end to end without hitting the real
`api.disclarion.com`:

```bash
python scripts/mock_server.py        # terminal 1 - fake /v1/logs endpoint
python scripts/manual_test.py        # terminal 2 - sends a fake ChatCompletion
```

The mock server prints every received payload and the `Authorization`
header so you can confirm the normalized log shape is correct. See
`scripts/README.md` for details, or `tests/` for the automated unit tests
(`pytest`) which mock the HTTP layer instead of needing a running server.
