Metadata-Version: 2.5
Name: langchain-nace
Version: 0.1.0
Summary: LangChain integration for Drex decision models by Nace
Project-URL: Homepage, https://console.nace.ai/docs
Project-URL: Repository, https://github.com/nace-ai/langchain-nace
Project-URL: Issues, https://github.com/nace-ai/langchain-nace/issues
Author-email: Nace AI <hello@nace.ai>
License-Expression: MIT
License-File: LICENSE
Keywords: classifier,decision-model,drex,guardrails,langchain,langgraph,middleware,nace,router
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.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: <4.0,>=3.11
Requires-Dist: httpx<1.0.0,>=0.27.0
Requires-Dist: langchain-core<2.0.0,>=1.6.13
Requires-Dist: nace-sdk<1.0.0,>=0.3.0
Requires-Dist: pydantic<3.0.0,>=2.11
Provides-Extra: experimental
Requires-Dist: langchain<2.0.0,>=1.4.0; extra == 'experimental'
Description-Content-Type: text/markdown

# langchain-nace

LangChain integration for [Drex](https://console.nace.ai/docs), Nace's decision models. Drex reads text, JSON, LangChain messages and (on Drex Max) images, and returns typed answers with calibrated probabilities instead of generated text. Use it to route requests, pick models, or check tool calls before they run.

```bash
pip install langchain-nace                  # classifier, router, LangGraph edge
pip install "langchain-nace[experimental]"  # + agent middleware
export NACE_API_KEY=nace_sk_...
```

## Quickstart

```python
from langchain_nace import Choice, NaceClassifier, Noul, Score

classifier = NaceClassifier()  # model="drex-max" by default

response = classifier.invoke({
    "state": "I was charged twice for March and I'm annoyed. Please refund one.",
    "questions": {
        "refund": Noul(instructions="Is the customer asking for a refund?"),
        "team": Choice(
            instructions="Which team should handle this?",
            criteria={"billing": "Payments and refunds", "technical": "Bugs"},
        ),
        "mood": Score(instructions="How upset is the customer?", criteria=["calm", "annoyed", "furious"]),
    },
})
response.nouls["refund"].noul             # 0.98: probability of yes
response.choices["team"].probabilities    # {"billing": 0.99, "technical": 0.01}
response.scores["mood"].score             # 1.1: expected level
```

All questions about one state go in one request. Answers keep their full probabilities, so thresholds stay in your code. `ainvoke`, `batch` and `abatch` work as on any Runnable. `batch` runs at most 8 requests at once unless you set `max_concurrency`.

**Switching from `langchain-typesafe`:** change the import to `from langchain_nace import ...` and `TypeSafeClassifier` to `NaceClassifier`. `Noul`, `Choice`, `Score`, `NoulCriteria` and the response fields (`nouls`, `choices`, `scores`, `usage`, `request_id`) have the same names.

## Models

The default is **`drex-max`**: it reads images, and takes up to 16 questions per request. For text-only work with more questions per request (up to 512), use `NaceClassifier(model="drex-v2.0")`. Any other model id is accepted and sent as given (`drex-v1.5`, `drex-latest`, and models released later); Drex enforces each model's limits, and errors tell you what to change. Pin a default for your app with `model=` or `NACE_DEFAULT_DECISION_MODEL`. The package default moves to the newest flagship with each Drex release, so pin a model if you need results that never change.

## Images (Drex Max)

```python
max_ = NaceClassifier()  # drex-max
max_.invoke({
    "state": "Two product photos.",
    "images": [red_png_bytes, "data:image/jpeg;base64,..."],
    "questions": {"damaged": Choice(instructions="Which image shows damage?",
                                    criteria={"Image 1": None, "Image 2": None})},
})
```

Images can be bytes, paths, base64, data URLs, LangChain image blocks (`{"type": "image", "base64": ...}`, `source_type: "base64"`, `image_url`), or files uploaded with `nace_sdk` (`{"type": "workspace_file", ...}`). Images inside LangChain messages in `state` are moved to `images` automatically and referred to as "Image N". Drex doesn't fetch URLs, so remote image URLs are refused. Sending images to a text-only model (`drex-v2.0`, `drex-v1.5`) raises `NaceImageModelError` before anything is sent; images are never dropped silently.

## Routing

```python
from langchain_nace import NaceRouter

router = NaceRouter(classifier=classifier, question=Choice(
    instructions="Which team?", criteria={"billing": None, "technical": None}),
    min_confidence=0.6, fallback="human")
chain = router.branch({"billing": billing_chain, "technical": tech_chain, "human": escalate},
                      default=escalate)
```

LangGraph: `nace_conditional_edge(classifier, question, state=..., min_confidence=0.7)` is a path function for `add_conditional_edges`. Below the threshold it returns `"human_review"`, so that node can `interrupt()` for a person. See [`examples/langgraph_router.py`](https://github.com/nace-ai/langchain-nace/blob/main/examples/langgraph_router.py).

## Agent middleware (experimental)

```python
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
from langchain_nace.experimental.middleware import (
    ModelChoice, NaceAutoModeMiddleware, NaceModelRouterMiddleware)

router = NaceModelRouterMiddleware(
    choices={"fast": ModelChoice("openai:gpt-5-mini", "Short lookups"),
             "strong": ModelChoice("anthropic:claude-opus-5-5", "Multi-step changes")},
    instructions="Pick the cheapest model that can do this safely.",
)
gate = NaceAutoModeMiddleware(screenshots=True)  # drex-max reads the screenshots
agent = create_agent("openai:gpt-5-mini", tools=tools, middleware=[router, gate],
                     checkpointer=InMemorySaver())
```

**`NaceModelRouterMiddleware`** picks the model once per run. Below `min_confidence` (0.6), or if Drex can't be reached, it uses `escalate_to` (the last choice by default). The decision and probabilities are kept in state under `nace_model_route`.

**`NaceAutoModeMiddleware`** checks each tool call before it runs. In one request Drex reads:
- the user's **original request**, kept however long the run gets;
- the latest user message and the last 20 messages;
- the tool call and its description;
- with `screenshots=True` and Drex Max, the newest screenshots.

It answers three questions: a risk score, "destructive?" and "did the user ask for this?". The default `ThresholdPolicy` then decides:
- **block** at risk ≥ 0.75, or when the call is destructive and the user didn't ask for it;
- **ask a person** at risk ≥ 0.4, or when the call is destructive and the user asked for it;
- **allow** otherwise.

Asking uses a LangGraph `interrupt`. Resume it with `{"type": "approve"}`, `{"type": "reject", "message": ...}` or `{"type": "edit", "args": {...}}`. If Drex fails, the call is blocked (`on_error="block"`). Questions, thresholds and the policy are all replaceable. Write your own `GatePolicy`, or subclass `GateDecision`.

[`examples/support_agent.py`](https://github.com/nace-ai/langchain-nace/blob/main/examples/support_agent.py) runs four live scenarios: an allowed read, a blocked unrequested delete, a person approving a requested delete, and a screenshot-based block on a computer-use click.

## Backends

| Backend | `base_url` | Notes |
|---|---|---|
| Nace (default) | `https://console.nace.ai` | `NACE_BASE_URL` |
| OpenRouter | `https://openrouter.ai/api/v1` | models `nace-ai/drex-v2.0`, `nace-ai/drex-max`; images are sent the way OpenRouter expects |
| Vercel AI Gateway / TypeSafe-compatible hosts | the host's System One root | same request format as Nace |

Every request sends `User-Agent` and `X-Nace-Client: langchain-nace/<version>`. On OpenRouter it also sends `HTTP-Referer` and `X-OpenRouter-Title`.

## Errors

Errors also inherit LangChain's `Model*Error` classes, and messages never include your state:

| Error | When |
|---|---|
| `NaceAuthenticationError` | 401 |
| `NaceInsufficientCreditError` | 402, with the Billing link |
| `NaceUnprocessableEntityError` | 422; lists each issue's `path: message` |
| `NaceRateLimitError` | 429, after retries |
| `NaceOverloadedError` | 529, after retries |
| `NaceAPIConnectionError` / `NaceAPITimeoutError` | network problems |

429, 529 and 5xx responses are retried (`max_retries=2`), waiting as long as `retry-after-ms` asks. The timeout is 60 s, because Drex may take up to 55 s.

## Tracing

Calls show up in LangSmith as `llm` runs (`ls_provider="nace"`) with the questions, the full answers and probabilities, token usage and the Drex request id. The router and middlewares add their thresholds to the run metadata.

## Development

```bash
uv sync --all-extras --all-groups
make test lint typecheck                 # offline: fake Drex server
NACE_API_KEY_LIVE=nace_sk_... make integration_tests   # live contract tests
```
