Metadata-Version: 2.4
Name: langprotect-armor
Version: 0.1.0
Summary: Self-hosted LLM observability SDK — trace any model call to your own dashboard
License: MIT
Project-URL: Repository, https://github.com/Quokka-Labs-LLP/langprotect-armor-sdk
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: langfuse>=3.0
Requires-Dist: openai>=1.0
Dynamic: license-file

# LangProtect Python SDK

Every model call is scanned by Armor for threats and PII, routed to the right provider, and linked to the LangProtect dashboard — all with a single import change.

---

## Install

```bash
pip install git+https://github.com/Quokka-Labs-LLP/langprotect-armor-sdk.git#subdirectory=python
```

---

## Quick Start

```python
import langprotect

langprotect.init(
    openai_api_key  = "sk-...",
    armour_base_url = "https://langprotect-xxx.run.app",
    armour_api_key  = "your-armour-key",
    security_on     = True,
)

from langprotect.langfuse.openai import openai

response = openai.chat.completions.create(
    model    = "gpt-4o-mini",
    messages = [{"role": "user", "content": "Hello!"}],
    session_id = "sess_001",
    user_id    = "user_42",
)

print(response.choices[0].message.content)
```

---

## Configuration

All configuration is passed to `langprotect.init()` — no `.env` file required.

```python
import langprotect

langprotect.init(
    # LLM provider
    openai_api_key   = "sk-...",

    # Armor security scanning
    armour_base_url  = "https://langprotect-xxx.run.app",
    armour_api_key   = "your-armour-key",
    security_on      = False,   # True  → sanitize/block unsafe input
                                # False → scanning disabled entirely
    trace_only       = False,   # True  → log only, never enforce
    scan_timeout     = 60,      # Armor HTTP timeout in seconds

    # LangProtect dashboard (used by score())
    langprotect_host = "http://localhost:8000",

    # LiteLLM proxy (optional — enables non-OpenAI models)
    litellm_host     = "",      # e.g. "http://localhost:4000"
)
```

| Parameter | Default | Description |
|---|---|---|
| `openai_api_key` | `""` | OpenAI API key |
| `armour_base_url` | `""` | Armor API base URL |
| `armour_api_key` | `""` | Armor `X-API-Key` header |
| `security_on` | `False` | Enforce sanitization / blocking |
| `trace_only` | `False` | Log only — never block or sanitize |
| `scan_timeout` | `60` | Armor HTTP timeout (seconds) |
| `langprotect_host` | `http://localhost:8000` | Dashboard URL for `score()` |
| `litellm_host` | `""` | LiteLLM proxy URL for non-OpenAI models |

> `init()` must be called **before** `from langprotect.langfuse.openai import openai`.

---

## Usage

### Chat completions

```python
from langprotect.langfuse.openai import openai

response = openai.chat.completions.create(
    model      = "gpt-4o",
    messages   = [{"role": "user", "content": "Explain async/await"}],
    session_id = "sess_123",
    user_id    = "u_42",
    metadata   = {"turn_number": 1},
)

print(response.choices[0].message.content)
```

### Handling blocked responses

When `security_on=True` and Armor hard-blocks a request, a `BlockedResponse` is returned instead of a normal completion. Check for it before reading `.choices`:

```python
if getattr(response, "blocked", False):
    print(response.content)  # "This response has been blocked by security policy."
else:
    print(response.choices[0].message.content)
```

### Toggling security at runtime

```python
from langprotect.langfuse.openai import set_security

set_security(True)   # Armor ON
set_security(False)  # Armor OFF
```

### Feedback scoring

```python
from langprotect.langfuse.openai import score

score(trace_id="abc123", value=1,  comment="Correct and concise")
score(trace_id="abc123", value=-1)
```

`value` is `1` (positive) or `-1` (negative). Scores are posted to `{langprotect_host}/api/feedback`.

### Supported call parameters

These are stripped before the OpenAI call and used for metadata only:

| Parameter | Type | Description |
|---|---|---|
| `session_id` | `str` | Groups all turns of a conversation |
| `user_id` | `str` | Identifies the end user |
| `name` | `str` | Trace label |
| `metadata` | `dict` | Any JSON key-value context |
| `trace_id` | `str` | Override the auto-generated UUID |
| `tags` | `list` | Labels for filtering |

---

## Security Scanning

The SDK calls the Armor `/v1/scan` endpoint on every input and output.

### Scan modes

| `trace_only` | `security_on` | Behaviour |
|---|---|---|
| `True` | any | Log the scan result, always proceed |
| `False` | `False` | Block if input is not safe, no sanitization |
| `False` | `True` | Sanitize if possible, block otherwise |

### Anonymization

When Armor returns a `sanitized_prompt` (PII replaced with tokens like `[PERSON_1]`, `[EMAIL_ADDRESS_1]`), the SDK:

1. Replaces the user message with the sanitized version
2. Injects a system prompt hint so the LLM treats tokens as real values
3. On the output scan, restores the original values via `sanitized_output` transparently

The caller always receives the deanonymized response.

---

## Model Routing

The SDK detects the provider from the model name and routes accordingly:

| Model prefix | Provider | Route |
|---|---|---|
| `gpt-`, `o1`, `o3`, `text-davinci`, `text-embedding` | OpenAI | Direct to OpenAI API |
| `claude`, `anthropic` | Anthropic | Via LiteLLM proxy |
| `gemini`, `palm` | Google | Via LiteLLM proxy |
| `llama`, `mistral`, `mixtral`, `codellama`, `phi`, `qwen` | Ollama | Via LiteLLM proxy |
| anything else | LiteLLM | Via LiteLLM proxy |

Non-OpenAI models require `litellm_host` to be set.

### Starting LiteLLM

```bash
litellm --model claude-3-5-sonnet-20241022 --port 4000
```

Then in `init()`:

```python
langprotect.init(
    ...
    litellm_host = "http://localhost:4000",
)
```

### Adding a new model via config

```yaml
# litellm_config.yaml
- model_name: phi3
  litellm_params:
    model: ollama/phi3
    api_base: http://localhost:11434
```

```bash
ollama pull phi3
litellm --config litellm_config.yaml --port 4000
```

---

## Data Flow

```
Your App
  openai.chat.completions.create(model="gpt-4o", messages=[...])
        │
        ▼
langprotect SDK
  1. Armor input scan  →  sanitize / block / log
  2. LLM call          →  OpenAI direct or LiteLLM proxy
  3. Armor output scan →  deanonymize + eval trigger
        │
        ▼
  Response returned to caller
  (eval runs as background task on LangProtect backend)
```

---

## Troubleshooting

**`ModuleNotFoundError: langprotect`**
Ensure the SDK is installed in your active virtualenv and `init()` is called before importing submodules.

**Response not deanonymized**
Check that `security_on=True` and `ARMOUR_BASE_URL` / `ARMOUR_API_KEY` are set correctly. The Armor output scan must return `sanitized_output`.

**Non-OpenAI model fails**
Set `litellm_host` in `init()` and ensure the LiteLLM proxy is running with the model configured.

**Armor scan times out**
Increase `scan_timeout` in `init()` (default 60 s). Check that `armour_base_url` is reachable from your environment.
