Metadata-Version: 2.4
Name: lyzr-rai
Version: 0.1.0
Summary: Responsible AI guardrails for LLM applications. Local-first, drop-in SDK.
Project-URL: Homepage, https://github.com/lyzr-ai/lyzr-rai
Project-URL: Issues, https://github.com/lyzr-ai/lyzr-rai/issues
Author: Lyzr
License: Apache-2.0
License-File: LICENSE
Keywords: guardrails,llm,pii,responsible-ai,safety
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: Apache Software 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 :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.9
Requires-Dist: openapi-spec-validator>=0.7
Requires-Dist: presidio-analyzer>=2.2
Requires-Dist: presidio-anonymizer>=2.2
Requires-Dist: pydantic>=2.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: spacy>=3.5
Requires-Dist: sqlglot>=20.0
Provides-Extra: dev
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.1; extra == 'dev'
Provides-Extra: ml
Requires-Dist: nltk>=3.8; extra == 'ml'
Requires-Dist: torch>=2.0; extra == 'ml'
Requires-Dist: transformers>=4.30; extra == 'ml'
Description-Content-Type: text/markdown

# lyzr-rai

Drop-in Responsible AI guardrails for LLM applications. Local-first Python SDK — no server, no API key, no telemetry.

```python
from lyzr_rai import RAIClient, Check

client = RAIClient(checks=[
    Check.secrets(),
    Check.pii(),
    Check.keywords(rules=[{"keyword": "confidential", "action": "block"}]),
])

result = client.check_input("Email john@acme.com about the confidential project")
print(result.blocked)         # True — keyword 'confidential' triggered block
print(result.processed_text)  # "Email [RAI_EMAIL_ADDRESS_1] about the confidential project"
```

## Install

```bash
# Core checks: secrets, pii, keywords, openapi, sql
pip install lyzr-rai

# Add ML-backed checks: topics, toxicity, prompt injection, nsfw, gibberish
pip install lyzr-rai[ml]
```

| Check                 | Install         | Notes                                              |
| --------------------- | --------------- | -------------------------------------------------- |
| `secrets`             | core            | Regex (AWS, GitHub, OpenAI keys, etc.)             |
| `pii`                 | core            | Presidio — EMAIL, PHONE, SSN, CREDIT_CARD, etc.    |
| `keywords`            | core            | Literal case-insensitive matching                  |
| `valid_openapi`       | core            | OpenAPI 3.x JSON/YAML validation                   |
| `valid_sql`           | core            | sqlglot syntax validation                          |
| `banned_topics`       | `[ml]`          | Zero-shot classifier (deberta-v3-large)            |
| `allowed_topics`      | `[ml]`          | Same model; blocks if no allowed topic matches     |
| `toxicity`            | `[ml]`          | XLM-Roberta (EthicalEye)                           |
| `prompt_injection`    | `[ml]`          | deberta-v3-base (ProtectAI). Input-direction only. |
| `nsfw`                | `[ml]`          | NSFW text classifier                               |
| `gibberish`           | `[ml]`          | Gibberish text detector                            |

> **Without `[ml]`**, attempting to construct an ML check raises a clear `ImportError` with the install hint. Nothing is silently downloaded.
>
> **With `[ml]`**, model weights lazy-download from HuggingFace Hub on first use and cache to `~/.cache/huggingface/hub/`. Override the cache directory via `HF_HOME`.

## Concepts

### Actions

Each detection check supports two actions: **`block`** (reject the input) and **`redact`** (replace the offending span with a token).

### Tokens

Redacted spans are replaced with `[RAI_<TYPE>_<N>]`:

```
"Email me at john@acme.com" -> "Email me at [RAI_EMAIL_ADDRESS_1]"
```

The same value always reuses its token within a client's lifetime (deduplication):

```python
r1 = client.check_input("john@acme.com")        # -> [RAI_EMAIL_ADDRESS_1]
r2 = client.check_input("john@acme.com again")  # -> [RAI_EMAIL_ADDRESS_1] again
```

Tokens are matched **case-insensitively** at unredact time — LLMs that "fix" casing don't break the round trip.

### Pipeline

Checks run sequentially in registration order. Each check sees the text as mutated by upstream redaction. Results accumulate; the pipeline does not stop on the first block unless `RAIClient(stop_on_block=True)`.

## PII Redaction & De-redaction

The full round trip with an LLM:

```python
from lyzr_rai import RAIClient, Check

client = RAIClient(checks=[Check.pii(), Check.secrets()])

# 1. Redact PII before sending to the LLM
user_msg = "Hi, I'm Alex Rivera. Email alex@example.com — my AWS key is AKIAIOSFODNN7EXAMPLE."
input_result = client.check_input(user_msg)
print(input_result.processed_text)
# "Hi, I'm [RAI_PERSON_1]. Email [RAI_EMAIL_ADDRESS_1] — my AWS key is [RAI_SECRET_AWS_KEY_1]."

# 2. Send the redacted text to the LLM
llm_response = call_my_llm(input_result.processed_text)
# "Sure, [RAI_PERSON_1]. I'll email [RAI_EMAIL_ADDRESS_1] with details."

# 3. Run output checks. Tokens are auto-restored at the end.
output_result = client.check_output(llm_response)
print(output_result.processed_text)
# "Sure, Alex Rivera. I'll email alex@example.com with details."
```

### One client per conversation

Each `RAIClient` holds its own redaction state. **Do not share clients across conversations** — token `[RAI_EMAIL_ADDRESS_1]` from Alice's session would map back to Alice's email, even if Bob's session is the one calling `check_output`.

```python
# Pattern A: new client per session
client = RAIClient(checks=[...])

# Pattern B: reset between sessions
shared_client.reset()
```

### Manual unredact

If you bypass `check_output` (e.g. you don't need to scan the LLM output), call `unredact()` directly:

```python
final = client.unredact(llm_response)
```

Unknown `[RAI_*]` tokens (LLM hallucinations) are left as-is.

## Examples

### Configure individual checks

```python
client = RAIClient(checks=[
    Check.secrets(action="block"),
    Check.pii(
        entities=["EMAIL_ADDRESS", "PHONE_NUMBER", "PERSON"],
        actions={"PERSON": "block"},   # block on PERSON, redact others
        default_action="redact",
        custom=[
            {"label": "ACCT-12345", "action": "redact", "replacement": "[ACCOUNT]"},
        ],
    ),
    Check.keywords(rules=[
        {"keyword": "confidential", "action": "block"},
        {"keyword": "internal", "action": "redact", "replacement": "[INTERNAL]"},
    ]),
    Check.valid_sql(),
])
```

### ML checks

```python
client = RAIClient(checks=[
    Check.prompt_injection(threshold=0.85),
    Check.toxicity(threshold=0.9),
    Check.banned_topics(topics=["weapons", "self-harm"]),
    Check.nsfw(validation_method="sentence"),
])
```

### Inspecting results

```python
result = client.check_input("...")

result.blocked          # bool
result.block_reason     # str | None
result.processed_text   # str
result.warnings         # list[str]
result.detections       # dict[check_name, payload]
result.redaction_map    # dict[original_value, token]

for check in result.checks:
    print(check.name, check.duration_ms, check.detected, check.blocked)
```

## License

Apache-2.0
