Skip to content

Quickstart

Install

pip install graphwalk              # import, walk, the CLI
pip install 'graphwalk[mcp]'       # + the MCP server
pip install 'graphwalk[llm]'       # + escalation and the LLM decider (LiteLLM)

Python 3.12 or newer. Other extras: embeddings (a local embedder for the type prefilter and dense locate) and eval (benchmark loaders).

Keys

Each hop is decided by a decider. The default is Jev, a fast classification model:

export OPENROUTER_API_KEY=sk-or-...        # model typesafe/jev-1.13 on OpenRouter
# or: export TYPESAFE_API_KEY=... GRAPHWALK_DECISION_PROVIDER=typesafe

Or decide with an open-weights model's token probabilities, through OpenRouter or your own server (with the default Qwen3.8-27B: as accurate, its confidence as informative, about 10× slower through OpenRouter; other models vary, see deciders):

export GRAPHWALK_DECIDER=logprob           # default model qwen/qwen3.8-27b on OpenRouter
# your own server: GRAPHWALK_DECIDER_BASE_URL=http://localhost:8000/v1 GRAPHWALK_DECIDER_MODEL=...

Escalation needs an LLM key too (the same OpenRouter key works, or OpenAI, Anthropic, or x.ai; see configuration). Importing a graph needs no key.

Import a graph

From triples: CSV/TSV with a header, JSONL, or N-Triples.

subject,relation,object,subject_type,object_type
Inception,directed_by,Christopher Nolan,film,person
Christopher Nolan,born_in,London,person,place
graphwalk import movies.csv --graph movies.db

Header aliases: head/source/s, predicate/type/p, tail/target/o, and optional subject_name/object_name. In N-Triples, rdfs:label becomes the name and rdf:type the type. Give nodes readable names and types: the decider reads names, relation names, and types, not opaque ids. Imports are idempotent and run at about 9k triples/s into SQLite.

Ask

graphwalk query movies.db "Where was the director of Inception born?"
graphwalk query movies.db "Where was the director of Inception born?" --escalate-below 0.9
import asyncio
from graphwalk import Index


async def main() -> None:
    async with Index.open(
        "movies.db",
        node_types=["film", "person", "place"],  # your graph's types: an answer-type hint
        escalate_below=0.9,                      # optional; needs the llm extra
    ) as index:
        result = await index.walk("Where was the director of Inception born?")
        if result.best is None:
            print("no answer")  # no entity linked, or nothing reached
        else:
            print(result.best.names)      # a set of nodes
            print(result.best.path)       # the hops taken: the justification
            print(result.confidence, result.escalated, result.cost_usd)


asyncio.run(main())

A runnable version over a small movie graph: examples/kg/walk.py.

Reading the confidence

result.confidence is the length-normalized probability of the path the walk took. On the benchmarks it ranks answers well (AUROC 0.92 on WebQSP, 0.97 on MetaQA 2–3 hop), and 0.9 was a good escalation threshold. On your own graph, check it against 50–100 labeled questions before relying on it: the threshold does not transfer automatically. It ranks answers; it is not the probability of being right (on WebQSP the mean confidence was 0.80 for 54% correct). If you need a probability, fit a logistic curve on log-confidence over 100+ labeled answers: that cut the calibration error from 0.26 to 0.10 there (P7). A walk the decider never had to answer (only one way on at every step, then a dead end) gets confidence 0.001 (n_choices == 0): there is no evidence it is right, and on 2Wiki such walks were almost always wrong (P9). The LLM-decider fallback's confidence barely is informative: it is the model's stated scores, not probabilities.

What it can't do

  • Constraints. "The earliest...", "both A and B", superlatives: a walk follows one relation path and does not filter, rank, or intersect. Do that in your code, or let an agent do it with the result.
  • Facts the graph lacks. Answers are nodes. Dates or qualifiers that live in text or attributes are out of reach.