Metadata-Version: 2.4
Name: inhibitor
Version: 0.1.4
Summary: Python client for the Inhibitor policy compliance inference API
Author: Inhibitor
License: MIT
Project-URL: Repository, https://github.com/your-org/inhibitor
Project-URL: Documentation, https://github.com/your-org/inhibitor#readme
Keywords: inhibitor,inference,policy,compliance,api,client
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
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
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.24.0
Requires-Dist: pydantic>=2.0.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"

# Inhibitor Python SDK

**Official Python client for the Inhibitor policy compliance API.** Evaluate natural-language content against your policies and get **ALLOW**, **MODIFY**, or **BLOCK** decisions with explanations.

---

## Features

- **Simple API** — Send only `query`, `metadata`, and `additional_metadata`; no extra parameters.
- **Structured metadata** — Use the `{"key": {"value": "val", "description": "des"}}` shape for clear, documented context.
- **Sync and async** — `InhibitorClient` and `AsyncInhibitorClient` with the same interface.
- **Typed models** — Pydantic request/response models for IDE support and validation.
- **LangSmith run tracking** — Attach a LangSmith `run_id` to any inference log for end-to-end LLM trace monitoring from the Inhibitor dashboard.

---

## Installation

```bash
pip install inhibitor
```

**Requirements:** Python 3.9+, `httpx`, `pydantic`.

---

## Quick start

```python
from inhibitor import InhibitorClient

client = InhibitorClient(api_key="your-api-key")

response = client.inference.query(
    "Can I share this document with external partners?",
    metadata={
        "consent": {"value": "yes", "description": "User gave consent"},
        "data_type": {"value": "PHI", "description": "Contains protected health info"},
    },
)

print(response)   # Complete Response
print(response.final_decision)   # ALLOW | MODIFY | BLOCK
print(response.explanation)      # Human-readable explanation
print(response.processing_time_seconds)
```

```python
client = InhibitorClient(api_key="your-api-key", base_url="https://your-custom-api.com")
```

---

## API overview

### What you send (request)

Only four fields are accepted:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| **query** | `str` | Yes | Natural language content to evaluate against your policies. |
| **metadata** | `dict` | No | Context for compliance. Shape: `{"key": {"value": "val", "description": "des"}}`. Used by the engine for the decision. |
| **additional_metadata** | `dict` | No | Stores optional user information (e.g., name, email) as key-value pairs, saved along with the inference log for auditing purposes. Example: {"user_name": "John Doe", "email": "john.doe@example.com"} |
| **explanation** | `bool` | No | If True, wait for explanation generation and include in response. Defaults to False. |

**Metadata shape** — Each key maps to an object with:

- **value** (str): The actual value (e.g. `"yes"`, `"PHI"`).
- **description** (str): Optional short description of the field.

Example:

```python
metadata = {
    "consent": {"value": "yes", "description": "User consent obtained"},
    "classification": {"value": "internal", "description": "Document classification"},
}
```

### What you get (response)

`InferenceResponse` includes:

| Field | Type | Description |
|-------|------|-------------|
| `log_id` | str \| None | ID of the inference log for fetching details later. |
| `query` | str | Echo of the query. |
| `final_decision` | str | `ALLOW`, `MODIFY`, or `BLOCK`. |
| `final_score` | float | Aggregate score (0–1). |
| `matched_policies` | list[str] | Policies that were evaluated. |
| `model_results` | list[RuleResult] | Per-rule decision, confidence, and probabilities. |
| `explanation` | str \| None | Human-readable explanation. |
| `make_it_compliant` | str \| None | Suggested changes to make the query compliant. |

---

## Usage

### Sync client

```python
from inhibitor import InhibitorClient, InferenceRequest

client = InhibitorClient(api_key="your-api-key")

# Simple query
response = client.inference.query("Should I send this email to a competitor?")

# With metadata (authoritative context for the engine)
response = client.inference.query(
    "Share the report with the client.",
    metadata={
        "consent": {"value": "yes", "description": "Client consent on file"},
        "channel": {"value": "email", "description": "Delivery channel"},
    },
)

# With additional_metadata (stored with log only)
response = client.inference.query(
    "Upload to cloud storage.",
    additional_metadata={"user_name": "John Doe", "email": "john.doe@example.com"},
)

# Using the request model
req = InferenceRequest(
    query="Can I export this data?",
    metadata={"data_type": {"value": "PII", "description": "Personal data"}},
)
response = client.inference.run(req)

# Fetching full inference log details
if response.log_id:
    log_details = client.inference.get_log(response.log_id)
    print(log_details)
```

### Async client

```python
from inhibitor import AsyncInhibitorClient

async with AsyncInhibitorClient(api_key="your-api-key") as client:
    response = await client.inference.query_async(
        "Can I share this document?",
        metadata={"consent": {"value": "yes", "description": "Consent given"}},
    )
    print(response.final_decision)

# Or with run_async
req = InferenceRequest(query="...", metadata={...})
response = await client.inference.run_async(req)

# Fetching full inference log details (async)
if response.log_id:
    log_details = await client.inference.get_log_async(response.log_id)
```

### Configuration

| Parameter | Description |
|-----------|-------------|
| `api_key` | **Required.** Your Inhibitor API key (from the dashboard). |
| `base_url` | Override API base URL. Default: `https://inhibitor-be.envistudios.com`. |
| `timeout` | Request timeout in seconds (default: `60.0`). |
| `headers` | Optional extra HTTP headers. |

The client sends the API key in the `X-API-Key` header. No other auth is required.

---

## Exceptions

| Exception | When |
|-----------|------|
| **InhibitorAuthError** | Invalid or expired API key (401). |
| **InhibitorValidationError** | Bad request or invalid parameters (400). |
| **InhibitorAPIError** | Other API errors (4xx/5xx). Has `status_code`, `detail`, `response_body`. |
| **ComplianceBlockedError** | Raised by :func:`check_compliance` when the decision is BLOCK. Has `response` (full :class:`InferenceResponse`). |

All extend **InhibitorError** (with `message` and optional `status_code`).

```python
from inhibitor import InhibitorClient
from inhibitor.exceptions import InhibitorAuthError, InhibitorAPIError

client = InhibitorClient(api_key="your-api-key")
try:
    r = client.inference.query("Some query")
except InhibitorAuthError as e:
    print("Auth failed:", e.message)
except InhibitorAPIError as e:
    print("API error:", e.status_code, e.detail)
```

---

## LangSmith run tracking

Inhibitor integrates with [LangSmith](https://smith.langchain.com/) to give you end-to-end visibility into your LLM executions directly from the Inhibitor dashboard.

When you run an LLM call traced through LangSmith, you can attach the resulting `run_id` to the corresponding Inhibitor inference log. The dashboard will then surface the full trace alongside the policy decision — inputs, outputs, token usage, latency, cost, and intermediate tool calls — without switching tabs or tools.

### Why use it

- Correlate policy decisions with the exact LLM call that triggered them.
- Debug blocked or modified responses by inspecting the full prompt and output.
- Monitor token usage and cost per inference from a single view.

### `update_log` parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `log_id` | `str` | Yes | ID of the Inhibitor inference log. Returned as `response.log_id` after calling `client.inference.query(...)`. |
| `run_id` | `str` | Yes | LangSmith run ID (UUID) to attach to the log. Must be a valid UUID — validated client-side before the request is sent. |

### Example

```python
from inhibitor import InhibitorClient

client = InhibitorClient(api_key="your-api-key")

# 1. Run policy inference and capture the log ID
response = client.inference.query("Can I share this document externally?")
log_id = response.log_id

# 2. Run your LLM call with LangSmith tracing and capture the run_id
#    (langsmith_run_id comes from your LangSmith callback/tracer)
langsmith_run_id = "9b7505cd-f111-4a34-8cc5-9006da96cbfc"

# 3. Attach the LangSmith run_id to the inference log
if log_id:
    updated = client.inference.update_log(
        log_id=log_id,
        run_id=langsmith_run_id,
    )
    print(updated)
```

The updated log is now visible in the **Inference Logs** section of the Inhibitor dashboard with the full LangSmith trace attached.

### Async variant

```python
from inhibitor import AsyncInhibitorClient

async with AsyncInhibitorClient(api_key="your-api-key") as client:
    response = await client.inference.query_async("Summarize this contract.")
    if response.log_id:
        updated = await client.inference.update_log_async(
            log_id=response.log_id,
            run_id="9b7505cd-f111-4a34-8cc5-9006da96cbfc",
        )
        print(updated)
```

### Validation

`update_log` and `update_log_async` validate `run_id` on the client before making any network call. An invalid UUID raises `ValueError` immediately:

```python
from inhibitor import InhibitorClient

client = InhibitorClient(api_key="your-api-key")

try:
    client.inference.update_log(log_id="...", run_id="not-a-uuid")
except ValueError as e:
    print(e)  # run_id must be a valid UUID, got: 'not-a-uuid'
```

---

## Integration checklist

1. **Get an API key** from your Inhibitor dashboard.
2. **Install** — `pip install inhibitor`.
3. **Create client** — `InhibitorClient(api_key="...")`.
4. **Call** — `client.inference.query(query, metadata=..., additional_metadata=...)`.
5. **Handle** — Use `response.final_decision`, `response.explanation`, and catch `InhibitorAuthError` / `InhibitorAPIError` as needed.

---

## Decorator: check compliance before calling your function

Use :func:`check_compliance` to run the inference API on a chosen argument before your function runs. If the decision is **BLOCK**, the decorator raises :exc:`ComplianceBlockedError` (or returns ``None``); otherwise it calls your function.

```python
from inhibitor import InhibitorClient, check_compliance

client = InhibitorClient(api_key="your-api-key")

@check_compliance(client, query_arg="message")
def handle_user_message(message: str, user_id: str):
    # Only runs if compliance allows (ALLOW or MODIFY)
    return process(message, user_id)

# BLOCK → ComplianceBlockedError (with response.explanation, response.make_it_compliant)
# ALLOW / MODIFY → your function runs
handle_user_message("Can I share this file?", "u-123")
```

**Options:** ``query_arg`` (param name to check, default ``"query"``), ``on_block`` (``"raise"`` or ``"return_none"``), ``metadata``, ``pass_response`` (inject ``_inhibitor_response`` into kwargs). Use :class:`AsyncInhibitorClient` with async functions.

```python
from inhibitor import AsyncInhibitorClient, check_compliance

client = AsyncInhibitorClient(api_key="your-api-key")

@check_compliance(client, query_arg="prompt")
async def run_llm(prompt: str):
    return await model.generate(prompt)
```

Catch blocks with :exc:`ComplianceBlockedError` and use ``e.response.explanation`` or ``e.response.make_it_compliant`` to show the user why it was blocked or how to fix it.

---

## Package structure

Each module has a single responsibility:

| Module | Purpose |
|--------|---------|
| `inhibitor.constants` | Default base URL and API path. |
| `inhibitor._http` | HTTP response helpers (e.g. error detail extraction). |
| `inhibitor.models` | Request/response Pydantic models and metadata shape. |
| `inhibitor.inference` | Inference API (sync/async): `query`, `run`, `get_log`, `update_log`, and async variants. |
| `inhibitor.client` | Sync and async HTTP clients. |
| `inhibitor.exceptions` | Auth, validation, and API exceptions. |

Import the public API from `inhibitor`; use submodules only if you need constants or helpers.

---

## License

MIT
