Skip to main content

NeoGraph

Write Python. Get a production graph.

Docs & guides: neograph.pro — full documentation site with tutorials, API reference, and side-by-side LangGraph comparisons.

# uv (recommended)
uv add neograph

# pip
pip install neograph

Define your LLM pipeline as Python functions. The framework infers the topology, validates types at assembly time, and compiles to LangGraph with checkpointing, observability, and tool orchestration. No DSL. No YAML. No add_node / add_edge.

A function is a node. A parameter name is an edge. An if is a branch.


Functions are nodes

from neograph import node, construct_from_module, compile, run

@node(outputs=Claims, prompt='rw/decompose', model='reason')
def decompose(topic: RawText) -> Claims: ...

@node(outputs=Classified, prompt='rw/classify', model='fast')
def classify(decompose: Claims) -> Classified: ...

@node(outputs=Report)
def report(classify: Classified) -> Report:
    return Report(summary=f"{len(classify.items)} claims processed")

pipeline = construct_from_module(sys.modules[__name__])
graph = compile(pipeline)
result = run(graph, input={'node_id': 'doc-001'})

classify(decompose: Claims) — the parameter name IS the dependency. Rename a function, downstream breaks at import time. Fan-in is just more parameters: def report(claims, scores, verified).

Mode is inferred. prompt= + model= means LLM call (think mode). Neither means the function body runs (scripted mode).

if is a branch

from neograph import ForwardConstruct, Node, compile

class Analysis(ForwardConstruct):
    check   = Node(outputs=CheckResult, prompt='check', model='fast')
    deep    = Node(outputs=Result, prompt='deep-analysis', model='reason')
    shallow = Node(outputs=Result, prompt='quick-scan', model='fast')

    def forward(self, topic):
        checked = self.check(topic)
        if checked.confidence > 0.8:
            return self.shallow(checked)
        else:
            return self.deep(checked)

graph = compile(Analysis())

The if compiles to a conditional edge. for compiles to fan-out. Python is the graph language. Your type checker sees everything. Your debugger works.

Everything else is a keyword

# Fan-out over a collection
@node(outputs=MatchResult, map_over='clusters.groups', map_key='label')
def verify(cluster: ClusterGroup) -> MatchResult: ...

# N-way ensemble with merge
@node(outputs=Claims, prompt='decompose', model='reason',
      ensemble_n=3, merge_fn='merge_claims')
def decompose() -> Claims: ...

# Fan-out + ensemble on the same node (Each x Oracle fusion)
@node(outputs=ClaimGroupingResult, prompt='decompose', model='reason',
      ensemble_n=3, merge_fn='group_claims',
      map_over='chunk_document', map_key='chunk_idx')
def decompose(chunk: ReadContext) -> ClaimGroupingResult: ...

# Human-in-the-loop interrupt
@node(outputs=ValidationResult,
      interrupt_when=lambda s: {'issues': s.check_quality.issues} if not s.check_quality.passed else None)
def check_quality(claims: Claims) -> ValidationResult: ...

# Tool-approval gate — pause before an agent/act tool runs; fail-closed on deny
@node(outputs=WriteResult, mode='act', model='ops', prompt='apply',
      tools=[Tool("write_file", budget=3)],
      gate_tools_when=lambda s: {'action': 'about to write files'})
def apply_changes(plan: Plan) -> WriteResult: ...

# ask_human — pause mid-tool for a typed answer (sugar over LangGraph interrupt())
from neograph import ask_human

@tool
def confirm(path: str) -> str:
    '''Ask a human to approve writing to path.'''
    answer = ask_human(ConfirmRequest(path=path), resume_model=ConfirmReply)
    return "ok" if answer.approved else "skipped"

# Agent with tools — typed tool results preserved
@node(outputs={"result": ExplorationResult, "tool_log": list[ToolInteraction]},
      mode='agent', model='research', prompt='explore',
      tools=[Tool("search", budget=5)],
      context=["catalog"])     # verbatim state injection
def explore(claim: VerifyClaim) -> ExplorationResult: ...

# Non-node parameters: runtime input, config, constants
from typing import Annotated
from neograph import FromInput, FromConfig

@node(outputs=Report)
def summarize(
    claims: Claims,                                   # upstream node
    topic: Annotated[str, FromInput],                 # from run(input={...})
    rate_limiter: Annotated[RateLimiter, FromConfig], # from config
    max_items: int = 10,                              # constant
) -> Report: ...

Catches mistakes before you run

ConstructError: Node 'verify' declares inputs=ClusterGroup but no upstream
  produces a compatible value.
  upstream producers:
    - node 'cluster': Clusters
  hint: did you forget to fan out? try .map(lambda s: s.cluster.groups, key='...')
  at my_pipeline.py:42

Types are validated at assembly time — when you define the pipeline, not when you execute it. 68 compile-time check fixtures (52 should-fail + 16 should-pass) backed by a rustc-style fixture suite. 1362 tests total, including Hypothesis property-based testing. CLI validation: neograph check my_pipeline.py.

Visualize the compiled graph

from neograph import compile, describe_graph

graph = compile(pipeline)
print(describe_graph(graph))   # Mermaid diagram — paste into GitHub, docs, mermaid.live

Set NEOGRAPH_DEV=1 for auto-printed DAG summaries after every compile().

Scales to real systems

Organize by module. Each pipeline is a Python module. Import nodes across modules. construct_from_module finds them all.

Sub-constructs from @node functions. construct_from_functions("verify", [explore, score], input=Claim, output=Result) builds a sub-construct with port param resolution. Mix @node functions and sub-constructs in one construct_from_functions call.

Observe everything. Structured logs on every node. Pass trace providers and shared resources via Annotated[T, FromConfig].

Retry on failure. Output-quality retries (malformed JSON, validation errors) are configured per node via LlmConfig.max_retries. Transient API failures (network, 429, 5xx) belong in your llm_factory via model.with_retry(...). See the retry-semantics page on neograph.pro.

Test at every level. node.run_isolated() for unit tests. compile() + run() for integration. forward() direct-call for debugging.

LLMs can build the graph too

For runtime construction — an LLM emitting a pipeline via tool calls, a config system defining workflows — use the programmatic API with the | pipe syntax:

from neograph import Node, Construct, Oracle, Each, compile, run

decompose = Node("decompose", mode="think", outputs=Claims,
                 prompt="rw/decompose", model="reason") | Oracle(n=3, merge_fn="merge")
verify = Node("verify", mode="agent", outputs=MatchResult,
              prompt="verify", model="fast") | Each(over="decompose.items", key="label")

pipeline = Construct("dynamic", nodes=[decompose, verify])
graph = compile(pipeline)

Three surfaces — @node, ForwardConstruct, Node | Modifier — one compiler.

Documentation

Full documentation is at neograph.pro:

Examples

19 runnable examples in examples/, 3 multi-file mini-projects (lead-research, code-review, spec-builder), and 5 side-by-side LangGraph comparisons in examples/vs_langgraph/. Walkthroughs at neograph.pro.

License

Code: MIT

Documentation content © 2025-2026 Constantine Mirin, mirin.pro. Licensed under CC BY-ND 4.0.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

neograph-0.7.0.tar.gz (330.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

neograph-0.7.0-py3-none-any.whl (369.7 kB view details)

Uploaded Python 3

File details

Details for the file neograph-0.7.0.tar.gz.

File metadata

  • Download URL: neograph-0.7.0.tar.gz
  • Upload date:
  • Size: 330.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for neograph-0.7.0.tar.gz
Algorithm Hash digest
SHA256 ccabc3ec578b20d3b0ff8135e65ce6ff45f2073e13567fcc95dfd939422090e1
MD5 c9b6e7bd88f42b5ba6950c549df340da
BLAKE2b-256 4143763357bab2626793c847b62efb8046ec9579c23da6dd4e2fe9c8f1e9b9b0

See more details on using hashes here.

Provenance

The following attestation bundles were made for neograph-0.7.0.tar.gz:

Publisher: publish.yml on KonstantinMirin/neograph

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file neograph-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: neograph-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 369.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for neograph-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6b4dc176a10b443996b29d9b0641b2e61583cfbb0fccfc7e68e22cb45411e773
MD5 32a81d05931ecfa0ddfdfc55af6652a0
BLAKE2b-256 dddc7533157c2398b247bc7f45d25609d096981808ccbf3ec932c53629773f19

See more details on using hashes here.

Provenance

The following attestation bundles were made for neograph-0.7.0-py3-none-any.whl:

Publisher: publish.yml on KonstantinMirin/neograph

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.7.9

2 files

0.7.8

2 files

0.7.7

2 files

0.7.6

2 files

0.7.5

2 files

0.7.4

2 files

0.7.3

2 files

0.7.2

2 files

0.7.1

2 files

This release

0.7.0 This release

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.1

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page