Metadata-Version: 2.4
Name: langchain-noxy
Version: 1.0.2
Summary: LangChain connector for Noxy human-in-the-loop — interrupt/resume with relay outcome polling via noxy-sdk.
Author: Noxy Network
License-Expression: MIT
Project-URL: Homepage, https://noxy.network
Project-URL: Documentation, https://docs.noxy.network
Project-URL: Repository, https://github.com/noxy-network/langchain-connector
Project-URL: Issues, https://github.com/noxy-network/langchain-connector/issues
Keywords: noxy,noxy-network,langchain,human-in-the-loop,ai-agents,middleware,polling
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: noxy-sdk>=2.1.0
Requires-Dist: langchain>=0.3.0
Requires-Dist: langgraph>=0.2.0
Requires-Dist: langgraph-checkpoint>=2.0.0
Provides-Extra: examples
Requires-Dist: fastapi>=0.110.0; extra == "examples"
Requires-Dist: uvicorn>=0.27.0; extra == "examples"
Provides-Extra: dev
Requires-Dist: build>=1.0.0; extra == "dev"
Requires-Dist: langchain-core>=0.3.0; extra == "dev"
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: twine>=5.0.0; extra == "dev"
Dynamic: license-file

# Noxy LangChain Connector

LangChain connector for [Noxy](https://noxy.network) **human-in-the-loop** guardrails. Uses `HumanInTheLoopMiddleware` to pause agent tool calls, routes encrypted approval prompts to all devices registered for the identity (web, iOS, Android, Telegram), and resumes execution when you **poll relay** for the settled outcome via [noxy-sdk](https://pypi.org/project/noxy-sdk/) (`GetDecisionOutcome`).

Installing `langchain-noxy` pulls in **`noxy-sdk`** automatically; you do not need a separate checkout of the Noxy SDK.

## Flow

```mermaid
sequenceDiagram
    participant A as LangChain Agent
    participant SDK as noxy-sdk
    participant N as Noxy Relay
    participant D as User Devices

    A->>SDK: send_decision (tool approval)
    SDK->>N: RouteDecision
    N->>D: Deliver to registered devices
    A->>A: interrupt() — state saved to checkpointer
    Note over A: Agent suspended

    loop Poll GetDecisionOutcome
        SDK->>N: get_decision_outcome
        N-->>SDK: pending / approved / rejected / expired
    end

    SDK->>A: Command(resume=HITLResponse)
    A->>A: Continue with approved/rejected tool calls
```

1. The agent proposes a guarded tool call.
2. `NoxyHumanInTheLoopMiddleware` routes an encrypted actionable to all devices for the identity.
3. LangChain calls `interrupt()` — the agent suspends and persists state via a checkpointer.
4. User responds on any registered device **or** the decision TTL expires on relay.
5. Your process calls `wait_for_decision_outcome` (SDK) or `bridge.wait_and_resume(...)`.
6. The agent continues with approved or rejected tool calls.

Relay delivers outcomes via **gRPC polling** (`GetDecisionOutcome`), with exponential backoff in the SDK.

## Requirements

- Python **>= 3.10**
- A LangChain agent created with `create_agent` and a **checkpointer**
- A Noxy **app token** and target **identity** (phone, email, user id, or wallet `0x…`)

## Installation

```bash
pip install langchain-noxy
```

Optional FastAPI example server:

```bash
pip install "langchain-noxy[examples]"
```

## Configuration

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `NOXY_APP_TOKEN` | Yes | — | App token from the Noxy dashboard |
| `NOXY_IDENTITY_ID` | Yes* | — | Target identity: phone, email, user id, or wallet |
| `NOXY_ENDPOINT` | No | `https://relay.noxy.network` | Relay gRPC endpoint |
| `MODEL` | No | `openai:gpt-4o-mini` | Chat model for the poll_resume_server example |

\*Identity is passed to `NoxyLangChainBridge(client, identity_id)` in code; use the env var when your app reads it from the environment.

Copy `.env.example` to `.env` when running repository examples locally (never commit real tokens).

```python
import os
from noxy import NoxyConfig, init_noxy_agent_client

client = init_noxy_agent_client(
    NoxyConfig(
        endpoint=os.environ.get("NOXY_ENDPOINT", "https://relay.noxy.network"),
        auth_token=os.environ["NOXY_APP_TOKEN"],
        decision_ttl_seconds=3600,
    )
)
```

## Quick start

```python
import uuid

from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
from noxy import NoxyConfig, init_noxy_agent_client

from langchain_noxy import NoxyLangChainBridge

@tool
def transfer_funds(to: str, amount: str) -> str:
    """Transfer funds."""
    return f"Sent {amount} to {to}"

client = init_noxy_agent_client(
    NoxyConfig(
        endpoint="https://relay.noxy.network",
        auth_token="your-app-token",
        decision_ttl_seconds=3600,
    )
)
bridge = NoxyLangChainBridge(client, "user@example.com")

agent = create_agent(
    init_chat_model("openai:gpt-4o-mini"),
    tools=[transfer_funds],
    middleware=[bridge.create_hitl_middleware({"transfer_funds": True})],
    checkpointer=InMemorySaver(),
)

config = {"configurable": {"thread_id": str(uuid.uuid4())}}
# agent.invoke(...) suspends when transfer_funds is proposed; read decision_id from interrupt

final = bridge.wait_and_resume(agent, "<decision_id>")
```

### Manual polling

```python
from noxy.decision_outcome import WaitForDecisionOutcomeOptions

resume_handler = bridge.create_resume_handler(agent)
response = client.wait_for_decision_outcome(
    WaitForDecisionOutcomeOptions(decision_id="<decision_id>", identity_id="user@example.com")
)
final = resume_handler.resume_from_poll_response(
    response, decision_id="<decision_id>", identity_id="user@example.com"
)
```

## Outcome mapping

Noxy relay outcomes map to LangChain HITL decisions (via `hitl_response_from_outcome`):

- `approved` → `{"type": "approve"}`
- `rejected` / `expired` / `timeout` → `{"type": "reject", "message": "..."}`

## Poll tuning

Pass `WaitForDecisionOutcomeOptions` to `bridge.wait_and_resume` (same fields as [noxy-sdk](https://pypi.org/project/noxy-sdk/)):

| Field | Default | Description |
|-------|---------|-------------|
| `initial_poll_interval_ms` | `400` | First delay between polls |
| `max_poll_interval_ms` | `30000` | Cap between polls |
| `max_wait_ms` | `900000` | Stop polling and resume with `timeout` outcome |
| `backoff_multiplier` | `1.6` | Exponential backoff factor |

## API

| Symbol | Description |
|--------|-------------|
| `NoxyLangChainBridge` | Wires client, registry, middleware factory, and `wait_and_resume` |
| `NoxyHumanInTheLoopMiddleware` | LangChain middleware that routes tool approvals to Noxy |
| `NoxyAgentResumeHandler.wait_and_resume` | SDK poll loop + `Command(resume=...)` |
| `NoxyAgentResumeHandler.resume_from_poll_response` | Resume from one terminal poll |
| `PendingDecisionRegistry` | Maps `decision_id` → `thread_id` for resume |
| `build_hitl_actionable(...)` | Build actionable payload from HITL action requests |
| `hitl_response_from_outcome(...)` | Map Noxy outcome to LangChain HITL resume value |
| `parse_webhook_payload(...)` | Optional: parse webhook-shaped JSON if you bridge events yourself |

## Examples

Example scripts are maintained in the [GitHub repository](https://github.com/noxy-network/langchain-connector) (they are not shipped inside the PyPI wheel). Clone the repo to run them:

```bash
git clone https://github.com/noxy-network/langchain-connector.git
cd langchain-connector
pip install ".[examples]"
cp .env.example .env   # set NOXY_APP_TOKEN and NOXY_IDENTITY_ID
```

- `examples/basic.py` — mock client, no relay required
- `examples/poll_resume_server.py` — FastAPI: `POST /runs`, then `POST /runs/wait`

```bash
python examples/basic.py

export NOXY_APP_TOKEN="your-app-token"
export NOXY_IDENTITY_ID="user@example.com"
uvicorn examples.poll_resume_server:app --reload
```

## Development

For contributors working on this repository:

```bash
git clone https://github.com/noxy-network/langchain-connector.git
cd langchain-connector
pip install ".[dev,examples]"
make test
make build
make publish-check
```

## License

MIT
