welt-io-langgraph
The LangGraph (Python) adapter for Welt's wire contract.
Install
uv add welt-io-langgraph
Usage
See examples/agent — the smallest complete agent built on this package (text streaming, tool use, file output, file input, and human-approval tools). The sections below explain the adapters it wires in.
API
The wire between Welt and the agent is JSON, specified by Welt's wire contract — plain LangGraph values do not fit it in either direction. Two functions adapt the inbound payload, three the outbound stream. The adapters target LangGraph 1.x and LangChain 1.x, whose messages carry standard content blocks.
Inbound
decode_messages(messages)
Turns Welt's Converse-shaped messages — built from the Slack thread, file bytes base64-encoded — into role/content message dicts that feed the graph input ({"messages": decoded}) as-is:
| Converse block | Standard content block |
|---|---|
| Text | Text |
| Image | Image |
| Document | File (the document's name carried as filename) |
| Video | Video |
Each file-carrying block gets the media type LangChain models expect in place of the Converse format token, and the base64 data stays base64 — standard content blocks need no decoding. Malformed entries are skipped.
decode_interrupt_responses(responses)
Turns Welt's resume payload — a mapping of interrupt id to the answer a human chose — into the mapping Command(resume=...) takes, answering every pending interrupt at once:
agent.astream(
Command(resume=decode_interrupt_responses(payload["interrupt_responses"])),
config,
stream_mode=["messages", "updates"],
)
The interrupt ids are LangGraph's own, as emitted by renderable_events; the config must point at the interrupted thread, which the host app stashes when an interrupt event goes by (see the example agent).
Outbound
renderable_events(stream, files_from=...)
Reduces the (mode, payload) items of astream(..., stream_mode=["messages", "updates"]) — whose values Welt does not render — to the events Welt renders:
| LangGraph emits | On the wire | In the Slack thread |
|---|---|---|
| Token deltas | data |
The streamed reply |
| Tool calls and tool messages | current_tool_use / tool_result |
"Using tool" indicators (tool output stays off the wire) |
Image / file / video content blocks the model returns, or a tool named in files_from returns |
file |
An uploaded file (size limits) |
| Pending interrupts | interrupt |
Buttons and/or a text field |
A run that stops for human input ends its stream with one interrupt event per pending interrupt; agents that do not use interrupts see no change.
A tool hands files to the model for either of two reasons — to have it read them, or to give them to the human — and only the agent knows which is which, so name the tools whose files belong in the thread:
async for event in renderable_events(stream, files_from={"create_sample_file"}):
A tool left out keeps its files to the model: one that reads a PDF for the model does not drop it into the thread as a side effect. A tool named there returns the file as a content block, which the model reads and Welt uploads:
return [
{"type": "text", "text": f"Created {name}.csv."},
{
"type": "file",
"name": name,
"mime_type": "text/csv",
"base64": b64encode(csv).decode("ascii"),
},
]
A tool message carries the name of the tool that produced it, so nothing else has to be passed in. Uploaded names come from the block's own name plus its media type, the block's kind for the rest (image.png). That name is also the model's handle on the document — Converse rejects a request whose messages carry two documents under one name, so a tool that returns files has to keep their names apart across the run: the example appends a short uuid to each.
file_event(name, data)
Builds the same file event from a filename and raw bytes, for the files the host app attaches itself:
yield file_event("report.csv", csv_bytes)
Tools have no use for it — they hand files to the agent as content blocks, and files_from decides which of those reach the thread.
interrupt_reason(message, options=..., input=...)
Builds the structured reason Welt renders as a message with the specified widgets — choice buttons (options), a free-text field (input), or both. The specs are the wire's own shapes; omitted fields keep Welt's defaults, and a typo becomes an immediate ValueError instead of a silent fallback to Welt's default rendering:
answer = interrupt(
interrupt_reason(
"Deploy to prod?",
[
{"value": "y", "label": "Deploy", "style": "primary"},
{"value": "n", "label": "Cancel"},
],
input={"label": "Or tell me what to do instead"},
)
)
Working with interrupts
Welt's Interrupts doc covers the Slack side: how each reason renders, who can answer, multiple questions, and expiry. On the LangGraph side:
interruptneeds a checkpointer, even though the conversation history lives in Slack — pausing and resuming run through checkpoints. An in-memory checkpointer works on AgentCore Runtime, where each session keeps its own microVM.- Start each conversation turn on a fresh thread. Welt sends the whole Slack thread every turn by default, so letting the checkpointer stack turns into its own history would double the conversation. Resume alone reuses the interrupted thread's config. (An agent that keeps its own history instead sets
AGENT_MANAGES_HISTORYon the Welt side.) - A plain interrupt value renders too. Any non-structured value —
interrupt("Deploy to prod?")— becomes a question with Welt's default Approve / Deny buttons, whose answers arrive asy/n. - Code before
interruptruns again on resume. LangGraph re-executes the interrupted node (or tool) from its start, so wrap whatever precedes an interrupt and must not run twice — side effects, or work that must match what the human approved — in a LangGraph task: a completed task is not re-executed on resume; its saved result is reused. The example agent'ssample_draft_reportshows the pattern.
Supported Versions
Welt releases first; welt-io-langgraph follows, mirroring the minor version. While both are 0.x, a welt-io-langgraph 0.Y release supports Welt v0.Y — other combinations may work, but come with no guarantee.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file welt_io_langgraph-0.4.0.tar.gz.
File metadata
- Download URL: welt_io_langgraph-0.4.0.tar.gz
- Upload date:
- Size: 19.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
56279c9a09e0c65e61bed861b8ab31204d9c8609209bdbcd9dda46506308b6b4
|
|
| MD5 |
a0145fc8d6b4a54f30adb40fb2618659
|
|
| BLAKE2b-256 |
b8ad9e5d326419fcfca69fcd3820b90b0b3d5ab74a9031467b84f853f50e8581
|
File details
Details for the file welt_io_langgraph-0.4.0-py3-none-any.whl.
File metadata
- Download URL: welt_io_langgraph-0.4.0-py3-none-any.whl
- Upload date:
- Size: 12.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fba8a30c0e414975808cfebfe3c116f3503589a0f7d2086684f0f61e0928a810
|
|
| MD5 |
2d10651da8fa4144b9fc301cd468324b
|
|
| BLAKE2b-256 |
30ffee9590413c2c616d73117753eaf44d810cd70896ba038b61041a1e8419cc
|