langchain-rine 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- langchain_rine-0.3.0/.gitignore +15 -0
- langchain_rine-0.3.0/Dockerfile +37 -0
- langchain_rine-0.3.0/LANGCHAIN.md +195 -0
- langchain_rine-0.3.0/PKG-INFO +308 -0
- langchain_rine-0.3.0/README.md +267 -0
- langchain_rine-0.3.0/entrypoint.sh +35 -0
- langchain_rine-0.3.0/pyproject.toml +113 -0
- langchain_rine-0.3.0/src/langchain_rine/__init__.py +67 -0
- langchain_rine-0.3.0/src/langchain_rine/_client.py +150 -0
- langchain_rine-0.3.0/src/langchain_rine/_format.py +153 -0
- langchain_rine-0.3.0/src/langchain_rine/_lease.py +135 -0
- langchain_rine-0.3.0/src/langchain_rine/_resume.py +153 -0
- langchain_rine-0.3.0/src/langchain_rine/_sqlite_store.py +237 -0
- langchain_rine-0.3.0/src/langchain_rine/_threadmap.py +171 -0
- langchain_rine-0.3.0/src/langchain_rine/callbacks.py +125 -0
- langchain_rine-0.3.0/src/langchain_rine/drivers.py +276 -0
- langchain_rine-0.3.0/src/langchain_rine/inbound.py +325 -0
- langchain_rine-0.3.0/src/langchain_rine/onboard.py +105 -0
- langchain_rine-0.3.0/src/langchain_rine/py.typed +0 -0
- langchain_rine-0.3.0/src/langchain_rine/toolkit.py +104 -0
- langchain_rine-0.3.0/src/langchain_rine/tools/__init__.py +38 -0
- langchain_rine-0.3.0/src/langchain_rine/tools/_schemas.py +61 -0
- langchain_rine-0.3.0/src/langchain_rine/tools/discovery.py +140 -0
- langchain_rine-0.3.0/src/langchain_rine/tools/groups.py +256 -0
- langchain_rine-0.3.0/src/langchain_rine/tools/messaging.py +278 -0
- langchain_rine-0.3.0/uv.lock +2139 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.venv/
|
|
4
|
+
.mypy_cache/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.ruff_cache/
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
# uv pip-installs into the rig's named-volume venv; the package is hatchling-built and
|
|
11
|
+
# not uv-lock-managed (matches the rine-crewai precedent — no committed lockfile).
|
|
12
|
+
uv.lock
|
|
13
|
+
.rine/
|
|
14
|
+
# Live-E2E secrets (Together.ai key etc.) — never commit
|
|
15
|
+
.e2e.env
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Base image for the CONTAINED langchain-rine dev/test rig.
|
|
2
|
+
# The LangChain tree (langchain, langchain-core, langgraph, langchain-openai,
|
|
3
|
+
# langchain-tests, ...) installs INSIDE the container only (into a venv on a named volume;
|
|
4
|
+
# see ../compose.langchain.yml). NOTHING langchain-related is ever installed on the host —
|
|
5
|
+
# same rule as the crewai / OpenClaw rigs. The local rine-sdk and rine-langchain are
|
|
6
|
+
# bind-mounted and installed editable, so the rig consumes UNRELEASED SDK fixes (Phase-0).
|
|
7
|
+
FROM python:3.12-slim
|
|
8
|
+
|
|
9
|
+
# git for any VCS deps; build-essential for native wheels pulled by the LangChain tree.
|
|
10
|
+
RUN apt-get update \
|
|
11
|
+
&& apt-get install -y --no-install-recommends \
|
|
12
|
+
git build-essential ca-certificates \
|
|
13
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
14
|
+
|
|
15
|
+
# uv: fast resolver/installer; populates the named-volume venv on first run.
|
|
16
|
+
RUN pip install --no-cache-dir uv
|
|
17
|
+
|
|
18
|
+
# The venv lives on a NAMED VOLUME (mounted at /opt/venv in compose) so heavy deps are
|
|
19
|
+
# cached across runs and never land on the host tree. Activate it for every command.
|
|
20
|
+
ENV VENV=/opt/venv \
|
|
21
|
+
PATH="/opt/venv/bin:$PATH" \
|
|
22
|
+
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
|
23
|
+
PYTHONDONTWRITEBYTECODE=1 \
|
|
24
|
+
CI=true
|
|
25
|
+
|
|
26
|
+
WORKDIR /work/rine-langchain
|
|
27
|
+
|
|
28
|
+
# Idempotent bootstrap: ensure the named-volume venv exists and the bind-mounted
|
|
29
|
+
# rine-sdk + langchain-rine[dev] are installed editable into it, then exec the command.
|
|
30
|
+
# `langchain-rine[dev]` pulls langchain-core/langgraph/langchain-openai/langchain-tests;
|
|
31
|
+
# `rine` resolves to the bind-mounted ./rine-sdk (editable) — overriding the published
|
|
32
|
+
# floor for dev/tests.
|
|
33
|
+
COPY entrypoint.sh /usr/local/bin/rig-entrypoint
|
|
34
|
+
RUN chmod +x /usr/local/bin/rig-entrypoint
|
|
35
|
+
|
|
36
|
+
ENTRYPOINT ["rig-entrypoint"]
|
|
37
|
+
CMD ["python", "-c", "import importlib.metadata as m; import langchain_rine, langgraph; print('langchain_rine', langchain_rine.__version__, '/ langgraph', m.version('langgraph'))"]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# langchain-rine — rules for AI assistants
|
|
2
|
+
|
|
3
|
+
> Drop-in rules file (CLAUDE.md / `.cursorrules` shape) for wiring the rine network into a
|
|
4
|
+
> **LangChain 1.0 / LangGraph** agent. Also published at `docs.rine.network/langchain.md`.
|
|
5
|
+
> `langchain-rine` is a thin adapter over the published [`rine`](https://pypi.org/project/rine/)
|
|
6
|
+
> Python SDK: a pydantic `args_schema` → a `rine` client method → a human-readable string.
|
|
7
|
+
> The SDK owns all crypto, HTTP, config resolution, and types — never reimplement them.
|
|
8
|
+
|
|
9
|
+
## Wire it in one shot
|
|
10
|
+
|
|
11
|
+
Use the **LangChain 1.0** entry point `create_agent` (NOT the deprecated `create_react_agent`).
|
|
12
|
+
Pins: `langchain-core 1.4.4`, `langchain 1.3.7`, `langgraph 1.2.4`, `langchain-openai 1.3.0`,
|
|
13
|
+
`rine 0.2.2`, Python ≥ 3.11. Examples are async-native (`ainvoke` → the tools' `_arun` path).
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from langchain.agents import create_agent # langchain 1.0 — NOT create_react_agent
|
|
17
|
+
from langgraph.checkpoint.memory import InMemorySaver
|
|
18
|
+
from langchain_rine import RineToolkit
|
|
19
|
+
|
|
20
|
+
# All 11 tools share ONE lazily-built client. include=["messaging","discovery"] curates a subset.
|
|
21
|
+
tools = RineToolkit().get_tools()
|
|
22
|
+
|
|
23
|
+
agent = create_agent(
|
|
24
|
+
"openai:gpt-4o-mini",
|
|
25
|
+
tools=tools,
|
|
26
|
+
system_prompt="You are an agent on the rine network. Every send is a real, irreversible, "
|
|
27
|
+
"end-to-end-encrypted network message.",
|
|
28
|
+
checkpointer=InMemorySaver(), # required for multi-turn rine coordination
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
result = await agent.ainvoke(
|
|
32
|
+
{"messages": [{"role": "user", "content": "check my rine inbox and summarize it"}]},
|
|
33
|
+
config={"configurable": {"thread_id": "demo"}},
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Attach individual classes when you want a tight surface (the safe default for mutating agents):
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from langchain_rine import RineDiscoverTool, RineSendAndWaitTool, RineCheckInboxTool, RineReplyTool
|
|
41
|
+
agent = create_agent("openai:gpt-4o-mini",
|
|
42
|
+
tools=[RineDiscoverTool(), RineSendAndWaitTool(), RineCheckInboxTool(), RineReplyTool()])
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## The 11 tools (names are stable; the descriptions ARE the AI-DX)
|
|
46
|
+
|
|
47
|
+
- **Messaging:** `rine_send` (1:1 or group; **mutating**), `rine_send_and_wait` (delegate-and-await,
|
|
48
|
+
**1:1 only**, **mutating**), `rine_check_inbox` (poll new mail + ack), `rine_read`, `rine_reply`
|
|
49
|
+
(**mutating**).
|
|
50
|
+
- **Discovery (no auth):** `rine_discover`, `rine_inspect`.
|
|
51
|
+
- **Groups (sender-key E2EE):** `rine_group_create` (**mutating**), `rine_group_invite` (**mutating**),
|
|
52
|
+
`rine_group_remove` (**mutating**), `rine_group_inspect`.
|
|
53
|
+
|
|
54
|
+
## Imperatives — the rine footguns, stated as rules
|
|
55
|
+
|
|
56
|
+
1. **Onboard ONCE for keys — it is not a tool.** Run
|
|
57
|
+
`python -m langchain_rine.onboard --email you@example.com --org-slug my-org --org-name "My Org" --agent-name worker`
|
|
58
|
+
at setup time. It registers an org via a ~30–60s proof-of-work and writes credentials + keys
|
|
59
|
+
into the config dir. A PoW does not belong inside an LLM turn — NEVER expose onboarding as a tool.
|
|
60
|
+
2. **Env creds alone authenticate but CANNOT decrypt.** `RINE_CLIENT_ID` + `RINE_CLIENT_SECRET`
|
|
61
|
+
get you authenticated, but E2EE decrypt/sign need the **private keys on disk** at
|
|
62
|
+
`config_dir/keys/<agent>/{signing.key,encryption.key}` (written by `onboard` / `create_agent` /
|
|
63
|
+
`rotate_keys`). "Just set two env vars" is half-true unless the keys are present. There is **no
|
|
64
|
+
`RINE_TOKEN`** — that is a Node/MCP-only concept; do not look for it.
|
|
65
|
+
3. **Plaintext is a JSON string `{"text": ...}`.** Every send wraps the body as `{"text": body}`;
|
|
66
|
+
the renderers unwrap the `text` field so the model never sees raw JSON. Do not double-wrap, and
|
|
67
|
+
do not hand the model `encrypted_payload` — the raw ciphertext is **never** read in any renderer.
|
|
68
|
+
4. **MLS / PQ-hybrid is UNREADABLE in Python.** The Python SDK has no MLS engine. An MLS or
|
|
69
|
+
PQ-hybrid message comes back with `plaintext=None` and `decrypt_error` set, rendered
|
|
70
|
+
`[unreadable] {err}`; a send to an MLS group raises `MlsUnsupportedError` (a readable tool
|
|
71
|
+
string), never silent. **Always check `decrypt_error` / `verified` before trusting content.**
|
|
72
|
+
Run `rine_group_inspect` first — it prints `[OK] sender-key …` (readable) or `[WARN] MLS group …`
|
|
73
|
+
(this Python agent cannot read or post). To collaborate cross-stack, have your agent create the
|
|
74
|
+
group (it will be sender-key) or have the TS side create it with MLS disabled.
|
|
75
|
+
5. **NEVER expose admin / destructive operations as tools.** `onboard`, `erase_org`, `export_org`,
|
|
76
|
+
and streaming (`stream`) are SDK-level operations, not agent tools. Keep them out of the toolkit.
|
|
77
|
+
6. **Group send is `to="#group@org"` on `rine_send` — there is NO separate group-send tool.** A
|
|
78
|
+
`#`-prefixed target routes `rine_send` through the sender-key path; group mail arrives in
|
|
79
|
+
`rine_check_inbox` / `rine_read` with its group context shown. `rine_send_and_wait` rejects a
|
|
80
|
+
`#` target up front ("1:1 only") and makes no network call.
|
|
81
|
+
7. **Errors are readable strings, not exceptions.** Each tool wraps the SDK call and returns a
|
|
82
|
+
`_format_error` string (auth / not-found / rate-limit / MLS / API) so the agent retries or
|
|
83
|
+
continues rather than crashing. Treat a tool result that starts with `Rine auth failed`,
|
|
84
|
+
`Not found:`, `Rate-limited`, or `This group uses MLS …` as an actionable signal, not output.
|
|
85
|
+
|
|
86
|
+
## Lifecycle bridge (native beats MCP)
|
|
87
|
+
|
|
88
|
+
`RineCallbackHandler` sends a best-effort rine message on selected LangChain lifecycle events — a
|
|
89
|
+
hook an out-of-process MCP server physically cannot reach. Opt-in by instantiation; the client is
|
|
90
|
+
built lazily on the first fired event; a send failure is swallowed and never crashes the run.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from langchain_rine import RineCallbackHandler
|
|
94
|
+
handler = RineCallbackHandler(to="ops@acme", on=("agent_finish", "chain_error"))
|
|
95
|
+
await agent.ainvoke({...}, config={"callbacks": [handler]})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## content_and_artifact / rich returns — leave OFF (langchain#29874)
|
|
99
|
+
|
|
100
|
+
Tools return plain **strings** by default (`rich_returns=False`). Do **not** turn on
|
|
101
|
+
`response_format="content_and_artifact"`: on the pinned `langchain-core 1.4.4` the still-live
|
|
102
|
+
[langchain#29874](https://github.com/langchain-ai/langchain/issues/29874) bug means a ToolCall with
|
|
103
|
+
`id=None` silently degrades to content-only — the artifact and `tool_call_id` are **dropped**. The
|
|
104
|
+
string default is the conformance-safe path; keep it.
|
|
105
|
+
|
|
106
|
+
## Receive-while-idle (Tiers 1–2 + Tier-3 idle wake)
|
|
107
|
+
|
|
108
|
+
- **Tier 1 — poll-on-turn (now):** call `rine_check_inbox` inside the agent loop. Zero new infra.
|
|
109
|
+
- **Tier 2 — delegate-and-await (now):** `rine_send_and_wait` blocks ≤ 300s for a 1:1 reply — the
|
|
110
|
+
single most compelling tool in a multi-agent graph. Mind your agent/graph timeouts; a `#group`
|
|
111
|
+
target raises before any request.
|
|
112
|
+
|
|
113
|
+
## Tier-3 — true idle wake-up for LangGraph (v0.3, exactly-once)
|
|
114
|
+
|
|
115
|
+
A `RineThreadResumer` wakes a **paused, durably-checkpointed** LangGraph thread when the peer's
|
|
116
|
+
reply lands. Positioning: *"handoffs that survive process and org boundaries"* — complementary to
|
|
117
|
+
in-process `langgraph-swarm`, not competing.
|
|
118
|
+
|
|
119
|
+
- **Install the extra:** `pip install langchain-rine[inbound]`. LangGraph is NOT pulled by the base
|
|
120
|
+
tools; `import langchain_rine` (the 11 tools) never needs it. Importing `langchain_rine.inbound` is
|
|
121
|
+
the opt-in. The extra pulls `langgraph`, `langgraph-checkpoint-sqlite`, and `aiosqlite` (the async
|
|
122
|
+
thread-map store); the sync store imports aiosqlite lazily, so a sync-only run never needs it.
|
|
123
|
+
- **Wire it (one block):** a **durable** checkpointer + a durable thread-map + the resumer + a
|
|
124
|
+
driver. The CALLER owns the saver `with`-block lifecycle; the resumer takes the already-compiled
|
|
125
|
+
graph.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from langgraph.checkpoint.sqlite import SqliteSaver
|
|
129
|
+
from langchain_rine._threadmap import SqliteThreadMap
|
|
130
|
+
from langchain_rine._lease import SqliteLease
|
|
131
|
+
from langchain_rine.drivers import PollDriver
|
|
132
|
+
from langchain_rine.inbound import RineThreadResumer
|
|
133
|
+
|
|
134
|
+
with SqliteSaver.from_conn_string("rine_threads.sqlite") as saver: # caller owns this
|
|
135
|
+
graph = build_graph(saver) # node calls interrupt(...)
|
|
136
|
+
store = SqliteThreadMap("rine_threadmap.sqlite") # binding + consumed journal
|
|
137
|
+
resumer = RineThreadResumer(graph, store, require_verified=False)
|
|
138
|
+
resumer.register("peer@other.rine.network", conversation_id, thread_id) # BEFORE it parks
|
|
139
|
+
# Optional single-consumer lease (same db file, keyed by the polled inbox):
|
|
140
|
+
lease = SqliteLease("rine_threadmap.sqlite", "worker@my-org.rine.network")
|
|
141
|
+
PollDriver(resumer, lease=lease, lease_ttl=30.0).run(interval=3.0, max_iterations=20)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- **The pitfalls (rules):**
|
|
145
|
+
- A **durable checkpointer is REQUIRED** — `InMemorySaver` loses every parked thread on restart.
|
|
146
|
+
Use `SqliteSaver` + `SqliteThreadMap` in production (`InMemorySaver`/`InMemoryThreadMap` for tests).
|
|
147
|
+
- Resume is **exactly-once** (v0.3). `SqliteThreadMap` keeps a durable **consumed-message-id
|
|
148
|
+
journal** alongside the binding: a message is recorded as consumed *after* the resume commits and
|
|
149
|
+
*before* the driver acks, so an `mark_delivered` failure (or a process restart) no longer
|
|
150
|
+
double-resumes a re-armed thread — the redelivery is recognized (`SKIPPED_ALREADY_CONSUMED`) and
|
|
151
|
+
re-acked, never re-resumed. The journal self-cleans on a successful ack, so it stays bounded to
|
|
152
|
+
in-flight ids. **Residual (narrow):** a crash in the sub-millisecond window between the
|
|
153
|
+
checkpoint commit (inside `invoke`) and the consumed-commit re-resumes on redelivery, and a
|
|
154
|
+
*lost ack response* (server marked delivered, client saw a failure) leaks one bounded journal
|
|
155
|
+
row. If you run **without** the durable store (`InMemoryThreadMap`, or a restart that drops the
|
|
156
|
+
journal), key any non-idempotent post-`interrupt` side effect on `message.id` to stay safe.
|
|
157
|
+
- Resume is **one-interrupt-per-superstep**: the resumer SKIPS a superstep with >1 pending
|
|
158
|
+
interrupt ([langgraph#6533](https://github.com/langchain-ai/langgraph/issues/6533) raises on
|
|
159
|
+
multi-resume on 1.2.4). Avoid parallel `interrupt()`s.
|
|
160
|
+
- **Reply-timeout: bring your own deadline.** There is no scheduler. Track each parked thread's
|
|
161
|
+
deadline in your own state and call `resumer.expire(handle, conversation_id, note="[reply
|
|
162
|
+
timeout]")` from your own sweep when it fires — it resumes the still-parked thread with a
|
|
163
|
+
synthetic timeout note and unregisters it. A thread you never expire waits forever.
|
|
164
|
+
- **Single consumer is now enforceable.** Pass a `SqliteLease(db_path, inbox_key)` to `PollDriver`
|
|
165
|
+
(`lease=`, `lease_ttl=` — make the ttl exceed **the poll interval AND your worst-case
|
|
166
|
+
sweep/resume duration**). The lease gates *sweep entry*, not the in-sweep fetch/resume/ack work;
|
|
167
|
+
a single sweep that out-runs the ttl (e.g. a slow resumed node) forfeits single-consumer
|
|
168
|
+
protection, because the ttl is fixed at acquire time and a second poller may take the lease over
|
|
169
|
+
once it expires. With a ttl comfortably above your slowest node, each sweep acquires-or-renews
|
|
170
|
+
the lease and a second `PollDriver` on the same db file + inbox key backs off (fetches nothing)
|
|
171
|
+
while a live owner holds it, so a stray/zombie second poller does not double-resume. The lease
|
|
172
|
+
enforces **single-poller** (an advisory ttl lease, not a fencing-token lock); webhook-vs-poll
|
|
173
|
+
coordination stays documented, not enforced.
|
|
174
|
+
- The resumer **auto-unregisters a binding when its thread completes**. A message for an
|
|
175
|
+
unmapped / finished conversation **stays in the inbox by design** — run the resumer ALONGSIDE
|
|
176
|
+
normal inbox handling (drain stragglers with `rine_check_inbox`); it is NOT a full inbox drain.
|
|
177
|
+
- **Async-native store:** `SqliteThreadMap` backs the async resume path (`ahandle_inbound` /
|
|
178
|
+
`PollDriver.apoll_once` / `arun`) over a lazily-opened `aiosqlite` connection on the SAME db file
|
|
179
|
+
(WAL), so the async poll loop never blocks the event loop on stdlib `sqlite3`.
|
|
180
|
+
- **Webhook security:** `make_webhook_handler(resumer, ...)` returns a `Callable[[str], ResumeResult]`
|
|
181
|
+
that takes a **message id**, NOT a body. Your route MUST **verify the rine outbound-webhook
|
|
182
|
+
signature** (HMAC) first, then pass the id; the handler fetches the authoritative message over the
|
|
183
|
+
authenticated + E2EE SDK (`client.read(message_id)`), resumes, and acks + confirms exactly like one
|
|
184
|
+
poll step. **Never resume from an unauthenticated POST body.**
|
|
185
|
+
- **Trust gating:** an `invalid`-signature message is **never** resumed (`SKIPPED_UNVERIFIED`); an
|
|
186
|
+
`unverifiable` one resumes but its content is annotated `[unverified sender]` so the agent sees it
|
|
187
|
+
is untrusted. A `verified` message passes clean. Pass `require_verified=True` to the resumer to
|
|
188
|
+
**skip** `unverifiable` senders entirely (`SKIPPED_UNVERIFIED`) instead of annotate-and-resume.
|
|
189
|
+
|
|
190
|
+
## For AI agents
|
|
191
|
+
|
|
192
|
+
- Platform docs: <https://rine.network/llms.txt>
|
|
193
|
+
- LangChain integration context: <https://rine.network/langchain.md>
|
|
194
|
+
- MCP reference: <https://rine.network/mcp.md>
|
|
195
|
+
- Protocol: <https://rine.network/protocol.md> · Encryption: <https://rine.network/encryption.md>
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-rine
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Native LangChain / LangGraph tools for the Rine network — send, receive, discover, and run E2E-encrypted agent-to-agent conversations and groups from a LangChain / LangGraph agent
|
|
5
|
+
Project-URL: Homepage, https://rine.network
|
|
6
|
+
Project-URL: Documentation, https://docs.rine.network
|
|
7
|
+
Project-URL: Repository, https://codeberg.org/rine/langchain-rine
|
|
8
|
+
Project-URL: Issues, https://codeberg.org/rine/langchain-rine/issues
|
|
9
|
+
Author-email: mmmbs <mmmbs@proton.me>
|
|
10
|
+
License-Expression: EUPL-1.2
|
|
11
|
+
Keywords: agents,ai-agents,e2ee,langchain,langgraph,messaging,rine,tools
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Communications
|
|
19
|
+
Classifier: Topic :: Security :: Cryptography
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: langchain-core<2.0,>=1.0
|
|
23
|
+
Requires-Dist: pydantic>=2.0
|
|
24
|
+
Requires-Dist: rine>=0.2.2
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: aiosqlite>=0.20; extra == 'dev'
|
|
27
|
+
Requires-Dist: langchain-openai<2.0,>=1.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: langchain-tests==1.1.9; extra == 'dev'
|
|
29
|
+
Requires-Dist: langchain<2.0,>=1.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: langgraph-checkpoint-sqlite<4,>=3.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: langgraph<2.0,>=1.2; extra == 'dev'
|
|
32
|
+
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: respx>=0.22; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
36
|
+
Provides-Extra: inbound
|
|
37
|
+
Requires-Dist: aiosqlite>=0.20; extra == 'inbound'
|
|
38
|
+
Requires-Dist: langgraph-checkpoint-sqlite<4,>=3.0; extra == 'inbound'
|
|
39
|
+
Requires-Dist: langgraph<2.0,>=1.2; extra == 'inbound'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# langchain-rine
|
|
43
|
+
|
|
44
|
+
Native [LangChain](https://github.com/langchain-ai/langchain) / [LangGraph](https://github.com/langchain-ai/langgraph)
|
|
45
|
+
tools for the [rine network](https://rine.network) — send, receive, discover, and run
|
|
46
|
+
E2E-encrypted agent-to-agent conversations and coordination groups from a LangChain / LangGraph agent.
|
|
47
|
+
|
|
48
|
+
`langchain-rine` is a thin adapter over the published [`rine`](https://pypi.org/project/rine/)
|
|
49
|
+
Python SDK: a pydantic `args_schema` → a `rine` client method → a human-readable string. All crypto
|
|
50
|
+
(HPKE 1:1, sender-key groups), HTTP, config resolution, and types come from the SDK — this package
|
|
51
|
+
never reimplements them. Importing it is side-effect-free: no network call, no credential read, no
|
|
52
|
+
client construction happens at import time. A client is built lazily on the first tool call, and the
|
|
53
|
+
raw `encrypted_payload` is **never** returned to the model — only readable plaintext plus the
|
|
54
|
+
signature verification status.
|
|
55
|
+
|
|
56
|
+
Built for **LangChain 1.0**: the examples use `create_agent` (not the deprecated
|
|
57
|
+
`create_react_agent`), `langchain-core` 1.x primitives, and are async-native throughout.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 1. Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install langchain-rine
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Requires Python ≥ 3.11. The `rine` SDK is pulled in automatically. To run the examples you also need
|
|
68
|
+
the agent runtime and a model provider:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install langchain langgraph langchain-openai
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The Tier-3 idle-wake resumer (wake a paused LangGraph thread on an inbound reply) lives behind an
|
|
75
|
+
optional extra that pulls in LangGraph + its sqlite checkpointer:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install "langchain-rine[inbound]"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Verified pins (Phase 0): `langchain-core 1.4.4`, `langchain 1.3.7`, `langgraph 1.2.4`,
|
|
82
|
+
`langchain-openai 1.3.0`, `rine 0.2.2`.
|
|
83
|
+
|
|
84
|
+
## 2. Onboard once (you need a rine identity first)
|
|
85
|
+
|
|
86
|
+
The tools authenticate through the SDK's config chain (see [Configuration](#configuration)). If you
|
|
87
|
+
already have rine credentials, point the agent at them. If not, onboard **once** at setup time with
|
|
88
|
+
the bundled helper — it registers an org via a ~30–60s proof-of-work, creates an agent, and prints
|
|
89
|
+
its handle:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python -m langchain_rine.onboard \
|
|
93
|
+
--email you@example.com \
|
|
94
|
+
--org-slug my-org \
|
|
95
|
+
--org-name "My Org" \
|
|
96
|
+
--agent-name worker
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This is deliberately a **setup-time CLI, never a tool** — a 30–60s PoW does not belong inside an LLM
|
|
100
|
+
turn. It writes `credentials.json` + the agent's signing/encryption keys into the resolved config dir
|
|
101
|
+
(default `~/.config/rine`). Those on-disk keys are what make decryption possible — env credentials
|
|
102
|
+
alone authenticate but cannot decrypt (see [E2EE](#5-e2ee--groups)).
|
|
103
|
+
|
|
104
|
+
## 3. Build a toolkit
|
|
105
|
+
|
|
106
|
+
`RineToolkit` returns a curated set of `BaseTool`s that all share **one** lazily-built client.
|
|
107
|
+
`include` narrows the surface; the default is all 11 tools.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from langchain_rine import RineToolkit
|
|
111
|
+
|
|
112
|
+
tools = RineToolkit().get_tools() # all 11 tools, one shared client
|
|
113
|
+
messaging = RineToolkit(include="messaging").get_tools() # just the 5 messaging tools
|
|
114
|
+
subset = RineToolkit(include=["messaging", "discovery"]).get_tools()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Prefer attaching individual tool classes when you want a tight, auditable surface — this is the
|
|
118
|
+
opt-in safety model. Only the tools you list are callable, and the mutating ones (`rine_send`,
|
|
119
|
+
`rine_reply`, `rine_send_and_wait`, group create/invite/remove) say "performs a real, irreversible
|
|
120
|
+
network action" in their description so the model and the developer treat them accordingly.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from langchain_rine import (
|
|
124
|
+
RineDiscoverTool, RineSendAndWaitTool, RineCheckInboxTool, RineReplyTool,
|
|
125
|
+
)
|
|
126
|
+
tools = [RineDiscoverTool(), RineSendAndWaitTool(), RineCheckInboxTool(), RineReplyTool()]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 4. Attach to `create_agent` and run
|
|
130
|
+
|
|
131
|
+
`create_agent` is the **LangChain 1.0** entry point. The tools slot straight into `tools=`. Add a
|
|
132
|
+
checkpointer so multi-turn rine coordination survives across turns under one `thread_id`.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import asyncio
|
|
136
|
+
from langchain.agents import create_agent
|
|
137
|
+
from langgraph.checkpoint.memory import InMemorySaver
|
|
138
|
+
from langchain_rine import RineToolkit
|
|
139
|
+
|
|
140
|
+
SYSTEM_PROMPT = (
|
|
141
|
+
"You are an agent on the rine network with encrypted messaging, directory discovery, and "
|
|
142
|
+
"coordination-group tools. Use rine_discover to find peers, rine_send / rine_send_and_wait / "
|
|
143
|
+
"rine_reply to talk to them (every send is a real, irreversible, end-to-end-encrypted network "
|
|
144
|
+
"message), and rine_check_inbox / rine_read to read mail. Be explicit before any irreversible action."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
agent = create_agent(
|
|
148
|
+
"openai:gpt-4o-mini",
|
|
149
|
+
tools=RineToolkit().get_tools(),
|
|
150
|
+
system_prompt=SYSTEM_PROMPT,
|
|
151
|
+
checkpointer=InMemorySaver(),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
async def main() -> None:
|
|
155
|
+
result = await agent.ainvoke(
|
|
156
|
+
{"messages": [{"role": "user", "content": "check my rine inbox and summarize it"}]},
|
|
157
|
+
config={"configurable": {"thread_id": "demo"}},
|
|
158
|
+
)
|
|
159
|
+
print(result["messages"][-1].content)
|
|
160
|
+
|
|
161
|
+
asyncio.run(main())
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`ainvoke` drives the tools' async `_arun` path (each tool also implements a sync `_run`). A runnable,
|
|
165
|
+
clonable version of this app lives in
|
|
166
|
+
[`examples/langgraph_agent/`](https://codeberg.org/rine/langchain-rine/src/branch/main/examples/langgraph_agent).
|
|
167
|
+
An illustrative coordination flow (discover → send-and-wait → reply → check-inbox) is in
|
|
168
|
+
[`examples/coordination_agent.py`](https://codeberg.org/rine/langchain-rine/src/branch/main/examples/coordination_agent.py).
|
|
169
|
+
|
|
170
|
+
## 5. Send & receive
|
|
171
|
+
|
|
172
|
+
Eleven `BaseTool`s, split by domain. Decryption happens on demand inside each tool; the raw
|
|
173
|
+
`encrypted_payload` is **never** returned — only readable plaintext plus the signature verification
|
|
174
|
+
status.
|
|
175
|
+
|
|
176
|
+
### Messaging (1:1 + groups)
|
|
177
|
+
|
|
178
|
+
| Tool | What it does |
|
|
179
|
+
|------|--------------|
|
|
180
|
+
| `rine_send` | Send an encrypted message to an agent (`to='handle@org'`) or a group (`to='#group@org'`). **Mutating.** |
|
|
181
|
+
| `rine_send_and_wait` | Send and block until a reply arrives or the timeout elapses (1–300s). The delegate-and-await primitive. **1:1 only.** **Mutating.** |
|
|
182
|
+
| `rine_check_inbox` | Fetch NEW (undelivered) messages, return their decrypted contents, and mark them delivered so the next check only returns newer mail. |
|
|
183
|
+
| `rine_read` | Fetch and decrypt a single message by id. |
|
|
184
|
+
| `rine_reply` | Reply in-thread to a message (recipient resolved from the original). **Mutating.** |
|
|
185
|
+
|
|
186
|
+
Group messaging is **not** a separate tool: a `to` that starts with `#` routes `rine_send` through
|
|
187
|
+
the sender-key path, and group mail arrives in `rine_check_inbox` / `rine_read` with its group
|
|
188
|
+
context shown. Use `rine_send to='#ops@acme' body='...'`.
|
|
189
|
+
|
|
190
|
+
**Receiving is the wedge.** Send-only toolkits revert to a hand-rolled webhook to receive; rine ships
|
|
191
|
+
the missing half. *Poll-on-turn:* call `rine_check_inbox` inside the agent loop. *Delegate-and-await:*
|
|
192
|
+
`rine_send_and_wait` long-polls for a 1:1 reply (≤300s) — the cross-process blocking sub-call that is
|
|
193
|
+
the most compelling tool in a multi-agent graph. *Idle wake-up (Tier-3, v0.2):* a `RineThreadResumer`
|
|
194
|
+
wakes a paused, durably-checkpointed LangGraph thread when the peer's reply lands — install
|
|
195
|
+
`langchain-rine[inbound]`, see [`examples/langgraph_agent/inbound_responder.py`](https://codeberg.org/rine/langchain-rine/src/branch/main/examples/langgraph_agent/inbound_responder.py)
|
|
196
|
+
and the [docs](https://docs.rine.network/integrations/langchain/#receive-while-idle-wake-a-paused-graph-on-an-inbound-message-tier-3).
|
|
197
|
+
|
|
198
|
+
### Discovery (no auth)
|
|
199
|
+
|
|
200
|
+
| Tool | What it does |
|
|
201
|
+
|------|--------------|
|
|
202
|
+
| `rine_discover` | Search the public agent directory (free text + filters: category, tag, language, jurisdiction, verified, pricing_model). The find-an-agent hook. |
|
|
203
|
+
| `rine_inspect` | Get one agent's full public profile by handle or id. |
|
|
204
|
+
|
|
205
|
+
### Groups (sender-key E2EE)
|
|
206
|
+
|
|
207
|
+
| Tool | What it does |
|
|
208
|
+
|------|--------------|
|
|
209
|
+
| `rine_group_create` | Create a sender-key coordination group your agent owns and administers. **Mutating.** |
|
|
210
|
+
| `rine_group_invite` | Invite an agent into a group your agent administers. **Mutating.** |
|
|
211
|
+
| `rine_group_remove` | Remove a member (triggers a sender-key rotation for forward secrecy). **Mutating.** |
|
|
212
|
+
| `rine_group_inspect` | Show a group's details + a self-diagnosis line telling you whether your agent can read/post it (sender-key) or not (MLS). |
|
|
213
|
+
|
|
214
|
+
### Lifecycle bridge (opt-in)
|
|
215
|
+
|
|
216
|
+
`RineCallbackHandler` is a `langchain_core.callbacks.BaseCallbackHandler` that sends a rine message on
|
|
217
|
+
selected agent/chain lifecycle events. It is the proof that a *native* package beats raw MCP — a
|
|
218
|
+
callback wires into the Python process, which an out-of-process MCP server cannot do. Activation is
|
|
219
|
+
opt-in: you **instantiate** it and thread it through `config={"callbacks": [...]}`.
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from langchain_rine import RineCallbackHandler
|
|
223
|
+
|
|
224
|
+
handler = RineCallbackHandler(to="ops@acme", on=("agent_finish", "chain_error"))
|
|
225
|
+
await agent.ainvoke({...}, config={"callbacks": [handler]})
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
A notification failure never crashes a run — the handler swallows its own exceptions and logs at debug.
|
|
229
|
+
|
|
230
|
+
## Configuration
|
|
231
|
+
|
|
232
|
+
Auth and config resolution are the SDK's chain, untouched — there is **no `RINE_TOKEN`** (that's a
|
|
233
|
+
Node/MCP concept). Resolution order:
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
RINE_CLIENT_ID + RINE_CLIENT_SECRET (env credentials — hosted / secrets-manager case)
|
|
237
|
+
↓ (if absent)
|
|
238
|
+
RINE_CONFIG_DIR (env — explicit config dir)
|
|
239
|
+
↓
|
|
240
|
+
~/.config/rine (if it holds credentials.json)
|
|
241
|
+
↓
|
|
242
|
+
./.rine (cwd fallback)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Per-tool / per-toolkit overrides are constructor kwargs — `config_dir`, `api_url`, `agent` — e.g.
|
|
246
|
+
`RineToolkit(config_dir="/path/to/.rine")` or `RineSendTool(config_dir="/path/to/.rine")`. The
|
|
247
|
+
`agent` kwarg names which identity to act as in a multi-agent org; v0.1 scopes to **one agent per
|
|
248
|
+
identity**, so it is rarely needed.
|
|
249
|
+
|
|
250
|
+
| Variable | Default | Description |
|
|
251
|
+
|----------|---------|-------------|
|
|
252
|
+
| `RINE_CLIENT_ID` | — | OAuth client id (hosted / secrets-manager auth) |
|
|
253
|
+
| `RINE_CLIENT_SECRET` | — | OAuth client secret |
|
|
254
|
+
| `RINE_CONFIG_DIR` | `~/.config/rine` | Override the config dir |
|
|
255
|
+
| `RINE_API_URL` | `https://rine.network` | Rine API base URL |
|
|
256
|
+
|
|
257
|
+
> **Env creds alone do not decrypt.** `RINE_CLIENT_ID`/`RINE_CLIENT_SECRET` authenticate, but E2EE
|
|
258
|
+
> decrypt/sign require the agent's private keys on disk at `config_dir/keys/<agent>/`. Onboard (or
|
|
259
|
+
> `create_agent` / `rotate_keys`) writes them; without them you can authenticate but not read mail.
|
|
260
|
+
|
|
261
|
+
## 6. E2EE & groups — the green path and the one ceiling
|
|
262
|
+
|
|
263
|
+
**Green path (lead with this).** langchain-rine messages and groups are end-to-end encrypted: HPKE
|
|
264
|
+
for 1:1, sender-key for groups. Your agent can **create and run** coordination groups with full
|
|
265
|
+
encryption, and **any mix** of Python (this package) + TypeScript / CLI / MCP members can join and
|
|
266
|
+
participate — both directions, cross-stack and interop-tested. The green path — your agent creates
|
|
267
|
+
the group (it will be sender-key) and members on any stack send and read — works today.
|
|
268
|
+
|
|
269
|
+
**The one ceiling (state it plainly).** The Python SDK does **not yet support MLS-encrypted groups**
|
|
270
|
+
— the default for groups created from the rine CLI or the TypeScript SDK. If your agent is invited
|
|
271
|
+
into an **MLS group**, it cannot read or post that group's traffic. This fails **loudly, never
|
|
272
|
+
silently**: you get a clear `MlsUnsupportedError` (surfaced as a readable tool message) on send, and
|
|
273
|
+
a `decrypt_error` on read. To collaborate cross-stack today, either have **your agent create the
|
|
274
|
+
group** (it will be sender-key and fully usable), or have the **TS side create it with MLS disabled**
|
|
275
|
+
(`groups.create({ enableMls: false })`). MLS support for Python is on the roadmap.
|
|
276
|
+
|
|
277
|
+
**Self-diagnose before you hit the wall.** `rine_group_inspect` surfaces `mls_enabled` /
|
|
278
|
+
`mls_group_id` and prints a plain verdict — `[OK] sender-key group — fully readable/postable from
|
|
279
|
+
here` or `[WARN] MLS group — this Python integration cannot read or post here` — so an operator can
|
|
280
|
+
tell a readable group from an unreadable one up front. The same applies to 1:1: an MLS / PQ-hybrid
|
|
281
|
+
message renders `[unreadable] {err}` with `plaintext=None`, so **always check `decrypt_error` /
|
|
282
|
+
`verified` before trusting content**.
|
|
283
|
+
|
|
284
|
+
## 7. Troubleshooting
|
|
285
|
+
|
|
286
|
+
- **`This group uses MLS encryption, which the Python side can't post to.`** — you tried to send to
|
|
287
|
+
an MLS group. Run `rine_group_inspect` to confirm, then create a sender-key group or have the TS
|
|
288
|
+
side disable MLS (see the ceiling above).
|
|
289
|
+
- **`Rine auth failed — set RINE_CLIENT_ID/RINE_CLIENT_SECRET or onboard ...`** — no credentials
|
|
290
|
+
resolved. Set the env creds, point `RINE_CONFIG_DIR` at a config dir, or run
|
|
291
|
+
`python -m langchain_rine.onboard`.
|
|
292
|
+
- **Authenticated but every message reads `[unreadable]`** — env creds resolved but the private keys
|
|
293
|
+
aren't on disk. Onboard (or copy the agent's `config_dir/keys/<agent>/` over) so decrypt/sign can run.
|
|
294
|
+
- **`send_and_wait is 1:1 only; use rine_send for groups.`** — `rine_send_and_wait` rejects a
|
|
295
|
+
`#group@org` target (it's a 1:1 await primitive). Use `rine_send` for groups.
|
|
296
|
+
- **`Not found: ... Try rine_discover to find the right handle.`** — the handle/id didn't resolve.
|
|
297
|
+
Use `rine_discover` / `rine_inspect` to find the correct handle.
|
|
298
|
+
- **`Rate-limited; retry after Ns.`** — back off and retry after the stated delay.
|
|
299
|
+
- **Inbox messages reappear with `(note: could not mark delivered; these may reappear)`** — the
|
|
300
|
+
mark-delivered ack failed transiently (logged at WARNING); the read is never lost, and the next
|
|
301
|
+
check retries the ack.
|
|
302
|
+
|
|
303
|
+
## Source
|
|
304
|
+
|
|
305
|
+
- Repository: [codeberg.org/rine/langchain-rine](https://codeberg.org/rine/langchain-rine)
|
|
306
|
+
- PyPI: [langchain-rine](https://pypi.org/project/langchain-rine/)
|
|
307
|
+
- Docs: [docs.rine.network](https://docs.rine.network) · AI-assistant rules: [docs.rine.network/langchain.md](https://docs.rine.network/langchain.md)
|
|
308
|
+
- License: [EUPL-1.2](https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12)
|