Metadata-Version: 2.5
Name: pushary-langgraph
Version: 0.4.1
Summary: Phone approvals for LangGraph and LangChain agents. Your agent asks, your user taps Approve or Deny.
Project-URL: Homepage, https://pushary.com
Project-URL: Repository, https://github.com/Pushary/pushary-langgraph
Project-URL: Documentation, https://pushary.com/docs/agents/adapters
Author-email: Pushary <business@pushary.com>
License: MIT
License-File: LICENSE
Keywords: ai-agents,approval,approvals,human-in-the-loop,interrupt,langchain,langgraph,tool-approval
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
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 :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: langgraph<2,>=1.0
Requires-Dist: pushary<3,>=2.1.0
Requires-Dist: pydantic<3,>=2
Provides-Extra: test
Requires-Dist: langgraph-checkpoint-sqlite<4,>=3; extra == 'test'
Description-Content-Type: text/markdown

# pushary-langgraph

Phone approvals for LangGraph and LangChain agents. Your agent asks, your user taps Approve or Deny.

[Connect your customer’s phone](https://pushary.com/sign-up?from=agent&plan=partner&utm_source=github&utm_medium=oss-adapter&utm_campaign=pushary-langgraph&utm_content=python-partner-start) · [Integration guide](https://pushary.com/docs/agents/adapters).

## What you need

- A Pushary Partner plan, from $99 a month. [Start the trial](https://pushary.com/sign-up?from=agent&plan=partner&utm_source=github&utm_medium=oss-adapter&utm_campaign=pushary-langgraph&utm_content=python-partner-start).
- An API key from [Partner onboarding](https://pushary.com/onboarding/partner), set as `PUSHARY_API_KEY`.
- Your users install the free Pushary app ([iPhone](https://apps.apple.com/us/app/pushary/id6785677563), [Android](https://play.google.com/store/apps/details?id=com.pushary.app)). They never sign up or pay.

## Quick start

```bash
pip install pushary-langgraph
export PUSHARY_API_KEY=pk_xxx.sk_xxx
```

```python
from pushary_langgraph import connect, pushary_interrupt

invitation = connect(authenticated_customer.id)  # once per user: show them this link


def review_order(state):
    answer = pushary_interrupt(
        f"Submit order {state['order_id']}, revision {state['revision']}?",
        external_id=state["customer_id"],
        idempotency_key=state["review_operation_id"],
        type="confirm",
        node="submit-order",
        parameters={"revision": state["revision"]},
        tool_target=state["order_id"],
        callback_url="https://your-app.example/pushary/callback",
    )
    return {"approved": answer == "yes"}
```

Give the invitation to that authenticated customer. They install the native app,
reopen the link, and allow notifications. They need no Pushary account/key/paid plan;
the developer needs Partner access. Bind `external_id` in trusted application code,
not in model-generated tool arguments.

Your graph must route to the protected write only when this confirm branch approved.
`ask_human` is a blocking helper, not a rule forcing an agent to call it. A choice or
text answer is data, not action authorization; use `type="select", options=["A", "B"]`
or `type="input"` for the tool that needs those values. Do not use the shared SDK's
`approved` convenience flag as permission for a select/input action.

Use **confirm** for permission, **select** for choices and **input** for missing
details. Confirm can be answered from the lock screen; select and input open the app.
The legacy web/PWA surface remains compatible.

## Review options

The adapter accepts the existing subject/presentation parameters: `tool_target`,
`actor`, `environment`, `parameters`, `presentation`, `placeholder`,
`expires_in_seconds` and `require_reachable`. A callback parks a native interrupt;
without it, the function blocks for the configured timeout and returns `None` unless
a valid answered result exists. Local timeout does not cancel the remote decision.

## Checkpoint and callback contract

Compile your graph with a persistent checkpointer and pass a stable, customer-scoped
`configurable.thread_id`. The whole node re-runs on resume; keep earlier work
idempotent. Same operation/recipient/request contents produce the same decision key;
changed contents produce a new key. Use an immutable reviewed business revision and
check that revision again before writing. Fingerprinting is not ownership validation.

The interrupt value includes `decisionId`, `correlationId`, `operationKey`, question,
type/options and recipient. Its enclosing LangGraph interrupt has a separate native
`id`. Your application must persist both IDs with its trusted thread/tenant mapping.

Verify callbacks with `resolve_pushary_callback(raw_body, signature, webhook_secret)`.
Persist verified callbacks before acknowledgement. Keep early answers queued until the
graph checkpoint and mapping exist. Your existing worker should serialize per thread,
reload the current checkpoint and match its exact pending interrupt. Completed
interrupts must not be resumed again on callback retries. After those checks:

```python
from langgraph.types import Command

graph.invoke(
    Command(resume={pending_interrupt.id: {
        "correlationId": verified_callback["correlationId"],
        "answer": verified_callback["answer"],
    }}),
    {"configurable": {"thread_id": trusted_thread_id}},
)
```

This is the final worker action, not an HTTP handler. The adapter validates the
correlation envelope and question-specific answer. It rejects strings/booleans passed
as an entire resume payload, wrong correlations, arbitrary answer objects, invalid
options and non-yes/no confirm values. A trusted expiration reconciler can send
`{"correlationId": decision_id, "status": "expired", "answer": None}` (or cancelled).
It returns `None`, never approval.

Handle missed callbacks by querying durable decision status; account for a crash
between graph resume and callback acknowledgement. Callback replay and checkpoint
persistence do not make ERP writes exactly once. Use operation keys and reconcile
uncertain external writes. No callback inbox or new orchestration runtime is installed
by this package.

## Upgrade and validation

Version 0.4 needs `pushary>=2.1`. Durable resumes now require the correlated object.
Finish already suspended 0.3 runs on their original adapter version; changing the
request fingerprint under a suspended checkpoint can invalidate its pending approval.

```bash
pip install -e '.[test]'
python -m unittest discover -s tests -v
```

[The restart test](tests/test_restart.py) uses the actual LangGraph graph and official
SQLite checkpointer, closes/reopens SQLite and rebuilds the graph for confirm yes/no,
select and input. HTTP is simulated; it does not deliver a phone notification or run
an ERP action. Tested with Python 3.12, LangGraph 1.2.11, SQLite checkpointer 3.1.1 and
Pydantic 2.13.5. These results do not certify every supported dependency version or
physical-device delivery.

[Official LangGraph interrupts](https://docs.langchain.com/oss/python/langgraph/interrupts)
describe persistence, replay and resuming by interrupt ID.

## License

MIT
