Python
The Python package speaks the same wire protocol as the TypeScript adapters and connects to the
same graphmind CLI, so a Python agent and a TypeScript one look identical in the viewer.
pip install graphmind-ai- Distribution name
graphmind-ai, import namegraphmind. - Python 3.10+, one runtime dependency (
websockets), MIT licensed. - Provider SDKs are optional extras:
pip install 'graphmind-ai[openai]',[anthropic],[langchain], or[all].
The API is idiomatic Python rather than a transliteration: a context manager for runs, a
decorator for tools, and one instrument_* call per client. Everything below has a module-level
form (import graphmind as gm, then gm.tool) and an instance form
(gm = graphmind.init(...), then gm.tool) — init sets the process-wide default, so the two
refer to the same session and can be mixed freely.
End to end (OpenAI)
Section titled “End to end (OpenAI)”import jsonimport graphmindfrom openai import OpenAI
gm = graphmind.init(app="support-agent")
# 1. Instrument the client — every request becomes a gated llm:step node.client = graphmind.instrument_openai(OpenAI())
# 2. Decorate your tools — each call becomes a gated tool node.@gm.tooldef search_orders(email: str): return db.orders.find(email=email)
@gm.tooldef issue_refund(order_id: str, cents: int): return payments.refund(order_id, cents)
TOOLS = {"search_orders": search_orders, "issue_refund": issue_refund}
TOOL_SPECS = [ { "type": "function", "name": "search_orders", "description": "Find a customer's recent orders", "parameters": { "type": "object", "properties": {"email": {"type": "string"}}, "required": ["email"], }, }, { "type": "function", "name": "issue_refund", "description": "Refund an order", "parameters": { "type": "object", "properties": {"order_id": {"type": "string"}, "cents": {"type": "integer"}}, "required": ["order_id", "cents"], }, },]
def main(): # Wait for the debugger handshake so gates are armed from the first event. gm.ready()
# 3. Open a run — everything inside belongs to it. with gm.run("handle-ticket"): messages = [ {"role": "user", "content": "Order #4471 arrived broken. Sort it out."} ]
for _ in range(8): response = client.responses.create( model="gpt-5.4", tools=TOOL_SPECS, input=messages )
calls = [item for item in response.output if item.type == "function_call"] if not calls: print(response.output_text) break
messages += response.output for call in calls: result = TOOLS[call.name](**json.loads(call.arguments)) messages.append({ "type": "function_call_output", "call_id": call.call_id, "output": json.dumps(result), })
gm.dispose()
main()Run graphmind in one terminal, python support_agent.py in another.
instrument_openai patches chat.completions.create (and .parse) and responses.create on
the instance — no library monkey-patching, no import hooks. It is idempotent, and clients
created after the call are not instrumented, so call it on each client you build. Streaming
responses are teed: your code receives exactly the provider’s stream while GraphMind observes the
deltas. A tools=[...] argument is pre-announced as a graph.hint, so the viewer draws the tool
roster grey before anything runs.
Anthropic
Section titled “Anthropic”Swap one line:
import anthropicimport graphmind
gm = graphmind.init(app="support-agent")client = graphmind.instrument_anthropic(anthropic.Anthropic())
with gm.run("handle-ticket"): message = client.messages.create( model="claude-sonnet-4-5", max_tokens=1024, tools=TOOL_SPECS, messages=messages, )Everything else — @gm.tool, gm.run, gm.ready — is identical.
messages.stream is instrumented too. There the HTTP request happens in __enter__, so that is
where the gate holds, and the proxy observes both consumption styles (raw event iteration and
.text_stream). It is the one provider attachment point where inject and retry are
unavailable: GraphMind cannot fabricate a provider stream object, so it holds, warns, and
continues rather than lying about having substituted one.
LangChain / LangGraph
Section titled “LangChain / LangGraph”Attach a callback handler — the sync one for sync chains, the async one for ainvoke and
LangGraph:
import graphmindfrom langgraph.prebuilt import create_react_agent
gm = graphmind.init(app="support-agent")gm.ready()
# Decorate the tools to get inject and retry — the handler alone cannot do those.@gm.tooldef search_orders(email: str) -> list[dict]: """Find a customer's recent orders.""" return db.orders.find(email=email)
# LangChain builds its schema from the decorated function's name, docstring and# signature, all of which `@gm.tool` preserves — so hand it the wrapper directly.agent = create_react_agent(model, tools=[search_orders, issue_refund])
with gm.run("handle-ticket"): result = agent.invoke( {"messages": [{"role": "user", "content": "Order #4471 arrived broken."}]}, config={"callbacks": [gm.handler()]}, )
gm.dispose()# async graphs / LangGraph — use the async handlerawait gm.ready_async()result = await agent.ainvoke(payload, config={"callbacks": [gm.async_handler()]})Chains, LLM calls, chat models, tools and retrievers each get their own node kind, and
LangChain’s parent_run_id becomes the parent/child structure on the canvas.
| LangChain concept | node kind | node id |
|---|---|---|
| chain / runnable | chain | chain:<name> |
| LLM / chat model | llm | llm:<name> |
| tool | tool | tool:<name> |
| retriever | retriever | retriever:<name> |
Anything else: spans
Section titled “Anything else: spans”For the parts of a graph GraphMind cannot see by itself — a LangGraph node body, a hand-rolled planner loop, a retrieval step in your own framework:
with gm.span("plan", kind="chain") as span: # `async with` too plan = build_plan(state) span.set_output(plan)A span has a before gate and honours abort. It owns no return value, so an injected value is
surfaced as the span’s output rather than silently dropped.
Capability matrix
Section titled “Capability matrix”What each attachment point can actually do. Every ✅ is covered by a test in the package’s
tests/.
| observe | before hold | error hold | after hold | inject | retry | abort | |
|---|---|---|---|---|---|---|---|
@gm.tool / gm.wrap_tools (sync + async) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
gm.span (sync + async) | ✅ | ✅ | — | — | as span output | — | ✅ |
OpenAI chat.completions / responses | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Anthropic messages.create | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Anthropic messages.stream | ✅ | ✅ (in __enter__) | ✅ | — | ❌ | ❌ | ✅ |
| LangChain sync handler | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
| LangChain async handler | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
Same shape as the LangGraph JS matrix, and for the same reason: a callback observes, a wrapper owns the call site.
The surface
Section titled “The surface”| Call | What it does |
|---|---|
graphmind.init(app=...) — alias configure | Create (or replace) the process-wide instance. |
graphmind.instrument_openai(client) — alias wrap_openai | Gate every OpenAI request; returns the client. |
graphmind.instrument_anthropic(client) — alias wrap_anthropic | Gate every Anthropic request; returns the client. |
gm.callback_handler() — alias gm.handler | BaseCallbackHandler for sync chains. |
gm.async_callback_handler() — alias gm.async_handler | AsyncCallbackHandler for async chains / LangGraph. |
@gm.tool | Gate a function: a tool:<name> node with inject / retry / abort. |
gm.wrap_tools({...}) | The same, for a mapping, a list, or one callable. |
with gm.run("name"): | Open a run. async with works on the same object. |
with gm.span("name", kind=...): | A gated node for anything else. async with too. |
gm.ready(timeout=2.0) / await gm.ready_async(timeout=2.0) | Wait for the handshake. False means detached, not an error. |
gm.stats() | Diagnostics: enabled, attached, buffered, dropped, held gates, seq. |
gm.dispose() | Release held gates, flush events, close the socket. Idempotent, no arguments. |
Every call above also exists as a method on an explicit instance (graphmind.GraphMind(app=...)),
which is what you want when one process debugs more than one agent.
Naming nodes
Section titled “Naming nodes”One node per code location; executions light it up. The node name is the callable’s
__name__ — and for the two common callables that have none, GraphMind falls back to something
stable rather than a repr with a memory address in it:
from functools import partial
load_eu = gm.tool(partial(load_region, "eu")) # -> tool:load_regionload_us = gm.tool(partial(load_region, "us"), name="load_us") # -> tool:load_usA functools.partial is named after the function it wraps; an instance of a class with
__call__ is named after its class. Two partials of the same function are therefore one node,
which is usually what you want — pass name= (or a key in gm.wrap_tools({...})) when you want
them apart.
Same guarantees
Section titled “Same guarantees”The Python package holds the identical contract:
- Never raises into your app. Internal failures degrade to a rate-limited warning on stderr and uninstrumented behaviour. Your own exceptions propagate untouched.
- Cheap when detached. Measured by the package’s own
tests/test_overhead.py: ~0.09 µs per wrapped call when disabled, ~9.5 µs when enabled but detached (two envelopes into the replay ring buffer), ~0.12 µs for a detached gate check. - Fails open. A debugger that disconnects mid-hold releases every gate with
continuein well under 100 ms. Anatexithook does the same at interpreter exit, and the transport thread is a daemon, so GraphMind can never keep a process alive. - Bounded memory and payloads. Events emitted while detached go into a ring buffer (default
2000) and are replayed with their original
seqwhen a viewer attaches. Prompts, arguments and results are depth-, width- and length-capped before serialization. fork()-safe. The loop thread is re-created in the child, so pre-forking servers (gunicorn, uvicorn workers, Celery) keep working.- Off in production. See below.
- Local-first. Events go to
ws://127.0.0.1:4747/ingestand no further.
Environment
Section titled “Environment”| Variable | Meaning |
|---|---|
GRAPHMIND_URL | Ingest endpoint (default ws://127.0.0.1:4747/ingest) |
GRAPHMIND_DISABLED | 1 disables instrumentation — beats an explicit enabled=True in code |
GRAPHMIND | 1 re-enables it in a production-looking environment |
“Production-looking” is a deliberately boring, documented rule rather than a heuristic: the
first variable that is set out of GRAPHMIND_ENV, ENVIRONMENT, APP_ENV, PYTHON_ENV,
ENV, DJANGO_ENV, FLASK_ENV, NODE_ENV decides, and it counts as production when its value
is production or prod (case-insensitive). No hostname sniffing, no cloud metadata probes.
A disabled session never opens a socket, never allocates a buffer and never touches your objects:
instrument_openai returns the client untouched.