Metadata-Version: 2.4
Name: noxy-langgraph
Version: 1.0.1
Summary: LangGraph connector for Noxy human-in-the-loop decisions — routes to all registered devices via interrupt/resume and webhooks.
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/langgraph-connector
Project-URL: Issues, https://github.com/noxy-network/langgraph-connector/issues
Keywords: noxy,noxy-network,langgraph,human-in-the-loop,ai-agents,interrupt,webhook
Classifier: Development Status :: 4 - Beta
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: 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: 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 LangGraph Connector

[![PyPI version](https://img.shields.io/pypi/v/noxy-langgraph.svg)](https://pypi.org/project/noxy-langgraph/)
[![Python versions](https://img.shields.io/pypi/pyversions/noxy-langgraph.svg)](https://pypi.org/project/noxy-langgraph/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

LangGraph connector for [Noxy](https://noxy.network) **human-in-the-loop** guardrails. Pauses agent graphs with `interrupt()`, routes encrypted approval prompts to all devices registered for the identity (web, iOS, Android, Telegram), and resumes execution when Noxy fires a webhook.

## Flow

```mermaid
sequenceDiagram
    participant G as LangGraph
    participant N as Noxy Relay
    participant D as User Devices
    participant S as Your Server

    G->>N: send_decision
    N->>D: Route to registered devices (web, iOS, Android, Telegram)
    G->>G: interrupt() — state saved to checkpointer
    Note over G: Graph suspended

    alt User responds
        D->>N: Approve / Reject
        N->>S: Webhook (outcome)
        S->>G: Command(resume=decision)
        G->>G: Continue with decision in state
    else Timeout
        N->>S: Webhook (timeout / expired)
        S->>G: Command(resume=timeout)
        G->>G: Continue with default behaviour
    end
```

1. Graph reaches the HITL node.
2. Noxy routes an encrypted actionable to all devices registered for the identity (web, iOS, Android, Telegram).
3. The node calls `interrupt()` — LangGraph suspends and persists state via a checkpointer.
4. User responds on any registered device **or** the decision TTL expires.
5. Noxy fires a webhook to your server.
6. Your server calls `NoxyGraphResumeHandler.resume_from_webhook()` → `Command(resume=...)`.
7. The graph continues with the human decision (or timeout default) in state.

## Requirements

- Python **>= 3.10**
- A LangGraph graph compiled **with a checkpointer** (required for `interrupt()`)
- [noxy-sdk](https://pypi.org/project/noxy-sdk/) credentials (`NOXY_APP_TOKEN`, target identity)

Target identity can be a **phone number**, **email**, **user id**, or **wallet address** (`0x…`).

## Installation

```bash
pip install noxy-langgraph
```

For the FastAPI webhook example:

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

Local development against the monorepo SDK:

```bash
pip install -e ../../sdks/python-sdk
pip install -e ".[dev,examples]"
```

## Quick start

```python
import uuid
from typing import Optional, TypedDict

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.constants import END, START
from langgraph.graph import StateGraph
from noxy import NoxyConfig, init_noxy_agent_client

from noxy_langgraph import NoxyLangGraphBridge, build_tool_call_actionable


class State(TypedDict, total=False):
    task: str
    noxy_decision: Optional[dict]
    _noxy_sent_decision_id: Optional[str]


def build_actionable(state: State) -> dict:
    return build_tool_call_actionable(
        tool="run_task",
        args={"task": state["task"]},
        title="Approve task?",
        summary=state["task"],
    )


client = init_noxy_agent_client(
    NoxyConfig(
        endpoint="https://relay.noxy.network",
        auth_token="your-app-token",
        decision_ttl_seconds=3600,
    )
)
# Phone, email, user id, or wallet address
bridge = NoxyLangGraphBridge(client, "user@example.com")

builder = StateGraph(State)
builder.add_node("noxy_hitl", bridge.create_hitl_node(build_actionable))
builder.add_edge(START, "noxy_hitl")
builder.add_edge("noxy_hitl", END)

graph = builder.compile(checkpointer=InMemorySaver())
resume_handler = bridge.create_resume_handler(graph)

config = {"configurable": {"thread_id": str(uuid.uuid4())}}
paused = graph.invoke({"task": "Send 1 wei"}, config)
# paused["__interrupt__"] contains the pending decision_id

# Later, in your webhook handler:
final = resume_handler.resume_from_webhook({
    "decisionId": "<decisionId>",
    "identityId": "user@example.com",
    "outcome": "approved",  # or "rejected", "expired"
})
```

## Graph state

Include optional `_noxy_sent_decision_id` in your state schema. The resume handler sets it via `Command(update=...)` so the HITL node does not re-route the decision when LangGraph re-executes the node after resume (LangGraph always re-runs the node body from the top).

```python
from noxy_langgraph import NOXY_SENT_DECISION_ID_KEY

class State(TypedDict, total=False):
    ...
    _noxy_sent_decision_id: Optional[str]  # or use NOXY_SENT_DECISION_ID_KEY
```

## Webhook payload

Noxy delivers JSON to your registered webhook URL:

| Field | Type | Description |
|-------|------|-------------|
| `decisionId` | `str` | Decision to resume (snake_case `decision_id` also accepted) |
| `identityId` | `str` | Identity that took the decision (phone, email, user id, or wallet) |
| `outcome` | `str` | `approved`, `rejected`, `expired`, or `timeout` |
| `receivedAt` | `str` | Optional ISO timestamp |

On **timeout/expired**, pass an `on_timeout` callback to `create_hitl_node` to apply default behaviour:

```python
def on_timeout(state, resume):
    return {"noxy_decision": resume.to_state(), "approved": False}

bridge.create_hitl_node(build_actionable, on_timeout=on_timeout)
```

## API

| Symbol | Description |
|--------|-------------|
| `NoxyLangGraphBridge` | Wires client, registry, HITL node factory, and resume handler |
| `create_noxy_hitl_node(...)` | Low-level HITL node factory |
| `NoxyGraphResumeHandler` | Resume paused graphs from webhook payloads |
| `PendingInterruptRegistry` | Maps `decision_id` → `thread_id` for resume |
| `build_tool_call_actionable(...)` | Standard `propose_tool_call` payload builder |
| `parse_webhook_payload(...)` | Parse raw webhook JSON |

## Examples

- `examples/basic.py` — end-to-end demo with a mock Noxy client
- `examples/webhook_server.py` — FastAPI server with `/runs` and `/webhooks/noxy`

```bash
python examples/basic.py
```

Configure the webhook server:

```bash
export NOXY_APP_TOKEN="your-app-token"
export NOXY_IDENTITY_ID="user@example.com"   # or phone, user id, 0x…
uvicorn examples.webhook_server:app --reload
```

## Development

```bash
make dev      # editable install with dev + examples extras
make test     # run pytest
make build    # build sdist + wheel
make publish-check  # build and validate with twine
```

## License

MIT
