graphwalk 0.1.0__py3-none-any.whl

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.
Files changed (92) hide show
  1. graphwalk/__init__.py +46 -0
  2. graphwalk/agent_docs/SKILL.md +33 -0
  3. graphwalk/agent_docs/guide.md +188 -0
  4. graphwalk/cli.py +654 -0
  5. graphwalk/config.py +344 -0
  6. graphwalk/core/__init__.py +35 -0
  7. graphwalk/core/errors.py +55 -0
  8. graphwalk/core/hashing.py +27 -0
  9. graphwalk/core/merge.py +152 -0
  10. graphwalk/core/model.py +196 -0
  11. graphwalk/core/redact.py +20 -0
  12. graphwalk/decisions/__init__.py +30 -0
  13. graphwalk/decisions/base.py +182 -0
  14. graphwalk/decisions/fake.py +116 -0
  15. graphwalk/decisions/jev.py +245 -0
  16. graphwalk/decisions/llm_decider.py +173 -0
  17. graphwalk/embeddings/__init__.py +6 -0
  18. graphwalk/embeddings/base.py +28 -0
  19. graphwalk/embeddings/fake.py +41 -0
  20. graphwalk/embeddings/fastembed_embedder.py +54 -0
  21. graphwalk/embeddings/openai_compatible.py +69 -0
  22. graphwalk/eval/__init__.py +29 -0
  23. graphwalk/eval/agent.py +502 -0
  24. graphwalk/eval/calibration.py +73 -0
  25. graphwalk/eval/datasets/__init__.py +1 -0
  26. graphwalk/eval/datasets/cache.py +26 -0
  27. graphwalk/eval/datasets/fanoutqa.py +159 -0
  28. graphwalk/eval/datasets/hotpotqa.py +100 -0
  29. graphwalk/eval/datasets/metaqa.py +115 -0
  30. graphwalk/eval/datasets/rog.py +201 -0
  31. graphwalk/eval/datasets/twowiki.py +72 -0
  32. graphwalk/eval/evidence.py +359 -0
  33. graphwalk/eval/ingest_eval.py +330 -0
  34. graphwalk/eval/metrics.py +97 -0
  35. graphwalk/eval/per_question.py +48 -0
  36. graphwalk/eval/query_writer.py +274 -0
  37. graphwalk/eval/runner.py +348 -0
  38. graphwalk/eval/suite.py +295 -0
  39. graphwalk/eval/systems.py +900 -0
  40. graphwalk/eval/text_qa.py +347 -0
  41. graphwalk/eval/types.py +61 -0
  42. graphwalk/guide.py +17 -0
  43. graphwalk/index.py +349 -0
  44. graphwalk/ingest/__init__.py +46 -0
  45. graphwalk/ingest/cache.py +55 -0
  46. graphwalk/ingest/chunking.py +135 -0
  47. graphwalk/ingest/extraction.py +273 -0
  48. graphwalk/ingest/normalize.py +293 -0
  49. graphwalk/ingest/pipeline.py +565 -0
  50. graphwalk/ingest/resolution.py +175 -0
  51. graphwalk/ingest/routing.py +443 -0
  52. graphwalk/ingest/sources.py +176 -0
  53. graphwalk/ingest/spend.py +23 -0
  54. graphwalk/ingest/triples.py +369 -0
  55. graphwalk/llm/__init__.py +7 -0
  56. graphwalk/llm/base.py +39 -0
  57. graphwalk/llm/fake.py +36 -0
  58. graphwalk/llm/litellm_backend.py +121 -0
  59. graphwalk/llm/litellm_import.py +21 -0
  60. graphwalk/locate/__init__.py +26 -0
  61. graphwalk/locate/dense.py +122 -0
  62. graphwalk/locate/documents.py +109 -0
  63. graphwalk/locate/graph.py +243 -0
  64. graphwalk/locate/model.py +66 -0
  65. graphwalk/providers.py +145 -0
  66. graphwalk/py.typed +0 -0
  67. graphwalk/server/__init__.py +1 -0
  68. graphwalk/server/app.py +330 -0
  69. graphwalk/server/auth.py +230 -0
  70. graphwalk/server/service.py +547 -0
  71. graphwalk/server/settings.py +111 -0
  72. graphwalk/stores/__init__.py +7 -0
  73. graphwalk/stores/base.py +183 -0
  74. graphwalk/stores/migrate.py +64 -0
  75. graphwalk/stores/networkx_store.py +325 -0
  76. graphwalk/stores/sqlite_store.py +475 -0
  77. graphwalk/traversal/__init__.py +39 -0
  78. graphwalk/traversal/batching.py +58 -0
  79. graphwalk/traversal/config.py +135 -0
  80. graphwalk/traversal/engine.py +729 -0
  81. graphwalk/traversal/entry.py +309 -0
  82. graphwalk/traversal/escalation.py +59 -0
  83. graphwalk/traversal/prefilter.py +65 -0
  84. graphwalk/traversal/prompts.py +206 -0
  85. graphwalk/traversal/sampling.py +50 -0
  86. graphwalk/traversal/scoring.py +21 -0
  87. graphwalk/traversal/trace.py +187 -0
  88. graphwalk-0.1.0.dist-info/METADATA +151 -0
  89. graphwalk-0.1.0.dist-info/RECORD +92 -0
  90. graphwalk-0.1.0.dist-info/WHEEL +4 -0
  91. graphwalk-0.1.0.dist-info/entry_points.txt +3 -0
  92. graphwalk-0.1.0.dist-info/licenses/LICENSE +177 -0
graphwalk/__init__.py ADDED
@@ -0,0 +1,46 @@
1
+ """graphwalk: fast, probabilistic question answering over knowledge graphs.
2
+
3
+ When to use it and how: ``graphwalk.guide()`` (or ``graphwalk guide`` on the command
4
+ line). The public API is what this module exports. Everything else (``graphwalk.traversal``,
5
+ ``graphwalk.ingest``, ...) is internal and may change between minor versions.
6
+ """
7
+
8
+ from importlib.metadata import version
9
+
10
+ from graphwalk.core.errors import DocumentNotFoundError, GraphwalkError, StaleLocationError
11
+ from graphwalk.core.model import Edge, Neighbor, Node, Provenance
12
+ from graphwalk.guide import guide
13
+ from graphwalk.index import Index, WalkResult
14
+ from graphwalk.ingest.pipeline import IngestConfig, IngestReport
15
+ from graphwalk.ingest.sources import FileSource, SourceDocument, TextSource
16
+ from graphwalk.ingest.triples import ImportReport
17
+ from graphwalk.locate.documents import FileDocuments, StoredDocuments
18
+ from graphwalk.locate.model import Location, Passage
19
+ from graphwalk.traversal.config import TraversalConfig
20
+
21
+ __version__ = version("graphwalk")
22
+
23
+ __all__ = [
24
+ "DocumentNotFoundError",
25
+ "Edge",
26
+ "FileDocuments",
27
+ "FileSource",
28
+ "GraphwalkError",
29
+ "ImportReport",
30
+ "Index",
31
+ "IngestConfig",
32
+ "IngestReport",
33
+ "Location",
34
+ "Neighbor",
35
+ "Node",
36
+ "Passage",
37
+ "Provenance",
38
+ "SourceDocument",
39
+ "StaleLocationError",
40
+ "StoredDocuments",
41
+ "TextSource",
42
+ "TraversalConfig",
43
+ "WalkResult",
44
+ "__version__",
45
+ "guide",
46
+ ]
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: graphwalk
3
+ description: Answer questions over an existing knowledge graph (triples, RDF, CSV/JSONL edge lists) by walking it hop by hop with a fast classifier, getting entity answers with a confidence and the path taken; escalate low-confidence queries to an LLM. Use when a user has a knowledge graph and wants cheap, fast multi-hop lookups with a per-answer confidence, and can accept lower accuracy than an LLM writing the query. Not for best-accuracy KG-QA (have an LLM write the query from the relations near the entity), QA over documents (use RAG), or constrained/superlative questions.
4
+ ---
5
+
6
+ # graphwalk
7
+
8
+ Run `graphwalk guide` (or `python -c "import graphwalk; print(graphwalk.guide())"`)
9
+ for the full guide: when to use it, the measured trade-offs, recipes, and API.
10
+
11
+ Short version:
12
+
13
+ 1. Check fit first. graphwalk answers from the graph's nodes. It is 6× cheaper and
14
+ ~4× faster than an LLM writing the query, and gives an informative confidence, but
15
+ is ~15 F1 points less accurate on curated graphs of any schema size. It loses to
16
+ multi-step RAG on documents. It does no filtering, ranking, or set intersection.
17
+ 2. Import: `graphwalk import kg.nt --graph kg.db` (also `.csv`, `.tsv`, `.jsonl`). No
18
+ key is needed. Give nodes readable names and types.
19
+ 3. Query from Python:
20
+
21
+ ```python
22
+ from graphwalk import Index
23
+
24
+ # walks with TraversalConfig.kgqa() by default
25
+ async with Index.open("kg.db", node_types=[...], escalate_below=0.9) as index:
26
+ result = await index.walk(question)
27
+ result.best.names, result.best.path, result.confidence, result.escalated
28
+ ```
29
+
30
+ 4. Treat `result.confidence` as a ranking signal. Validate the threshold on 50–100
31
+ labeled questions from the user's own graph before trusting it.
32
+ 5. Keys: `OPENROUTER_API_KEY` (Jev decisions; the LLM for escalation). Without Jev,
33
+ use `GRAPHWALK_DECISION_FALLBACK=llm`; its confidence is not calibrated.
@@ -0,0 +1,188 @@
1
+ # graphwalk: guide for coding agents
2
+
3
+ graphwalk answers questions over a knowledge graph by walking it. Each hop is a
4
+ classification decision: the current node's relations (plus STOP) are the options, and
5
+ a fast decision model (Jev) returns a probability for each. A walk's answer comes with a
6
+ confidence, so unsure walks can be re-run ("escalated") with a slower LLM decider.
7
+
8
+ This guide says when graphwalk is the right tool, when it is not, and how to use it.
9
+ Every claim below comes from a measured experiment. The `docs/results-*.md` files in the
10
+ repository hold the tables, CIs, and caveats. Samples are small (50–300 questions per
11
+ setting, mostly one seed), so treat the numbers as directions, not guarantees.
12
+
13
+ ## Decide first: is graphwalk the right tool?
14
+
15
+ | Your situation | Use | Evidence |
16
+ |---|---|---|
17
+ | An **agent** that already explores a knowledge graph with low-level tools (`neighbors`, a relation list) | **Also give it `walk`** (the MCP server does). The agent calls `walk` first, then checks or continues with `neighbors`. | A8b: a strong agent with `walk` cost 25% less (95% CI 7–45%) at equal F1 (0.75 vs 0.71, n=30, WebQSP); with a cheap agent, F1 was equal and the cost a wash. Text search over the same facts tied on F1 |
18
+ | A knowledge graph you already have (triples, RDF), **any schema size**, and you want the best accuracy | **Have an LLM write the query** from the relations around the question's entity (fetch them with one adjacency query; for a whole large schema, it fits a 1M-token context too). | A6: Freebase, 5,419 relations in one graph: 0.66 vs 0.49 F1 for walking |
19
+ | The same, but **cost or latency matters more than ~15 F1 points**, or you need a **per-answer confidence** | **graphwalk `walk`**: 6× cheaper and ~4× faster than the LLM path writer, with a confidence that ranks its answers | A6; A2 (AUROC 0.92 on WebQSP) |
20
+ | A **noisy graph extracted from text** (inconsistent relation names, edges in either direction) | graphwalk `walk` beats LLM-written paths, but every method is weak here; prefer RAG over the source text | E2b: 3–5× the F1 of LLM-written paths at 1/20–1/35 the cost, absolute F1 0.18–0.25 |
21
+ | A graph with a **small, clean schema** (≲ 50 self-describing relations) | **Have an LLM write the query** (a relation path, Cypher, SPARQL). It is more accurate, and here also cheaper than walking with escalation. | E2: MetaQA 3-hop 0.955 vs 0.887 F1; A2: WebQSP 0.76 vs 0.54 F1 |
22
+ | You want a **cheap first pass that knows when it's wrong**, and to pay for a strong model only on hard queries | **`walk` with `escalate_below`**, but expect modest savings: escalating to an LLM-written path matched its accuracy for ~19% less on Freebase (A6, offline), and on MetaQA the path writer alone is cheaper | A1, A6 |
23
+ | **Compositional questions with constraints** ("the earliest...", "which X that also Y", superlatives, comparisons) | **Not graphwalk.** It follows one relation path from the topic entity and does no filtering, ranking, or intersection. Use a dedicated KG-QA method or LLM-written queries with filters. | A2: CWQ hits@1 ≈ 0.28 for every system tried, vs ≈ 0.63–0.69 published |
24
+ | **Questions over documents** (no curated graph) | **Multi-step RAG**, not graphwalk. Building a graph from text loses dates, order, and qualifiers. | M7, E4: RAG wins by 10–19 F1 points |
25
+ | Retrieval over documents where questions **chain through named entities** ("the director of the film X...") | graphwalk `locate(mode="hybrid")` as the retriever, then read the text | E1: +18 points recall over dense on 2Wiki; ties on HotpotQA; −5 on FanOutQA |
26
+ | Broad or list-style questions over documents | Plain dense retrieval | E1 |
27
+ | As an add-on to an existing multi-step RAG loop | Don't, unless your reader model is expensive. It cuts LLM rounds by 16–21% but not error, and its own calls cost more than the rounds saved at cheap-reader prices. | E7 |
28
+
29
+ Rules of thumb:
30
+
31
+ - **The graph must hold the answer.** graphwalk's answers are nodes. If facts live in
32
+ free text, attributes, or qualifiers the graph lacks, no walk finds them.
33
+ - **Jev buys cost, speed, and confidence, not accuracy.** An LLM deciding each hop is
34
+ as accurate or better (+0.06 F1 at 3 hops). Jev is 3–4× cheaper, 7–10× faster per
35
+ decision, and its confidence separates right from wrong answers (AUROC 0.92–0.97 on
36
+ MetaQA 2–3 hop and WebQSP; weaker, 0.64–0.71, on MetaQA 1-hop, 2Wiki, and CWQ). The
37
+ LLM decider's confidence barely does (0.50–0.69).
38
+ - **Walking is fast; the model calls dominate.** graphwalk adds ~10 ms per walk on
39
+ typical nodes and ~0.1–0.5 s at nodes with 20k neighbors (SQLite, 1.4M edges).
40
+
41
+ ## Install and keys
42
+
43
+ ```bash
44
+ pip install 'graphwalk[embeddings]' # or, from a clone: uv sync --extra embeddings
45
+ ```
46
+
47
+ Extras: `embeddings` (local fastembed embedder: prefilter and dense `locate`), `llm`
48
+ (LiteLLM: text ingestion, the LLM decider, escalation), `mcp` (the MCP server), `eval`.
49
+
50
+ Keys come from the environment:
51
+
52
+ - Decisions (Jev): `OPENROUTER_API_KEY` (model `typesafe/jev-1.13`) or `TYPESAFE_API_KEY`
53
+ with `GRAPHWALK_DECISION_PROVIDER=typesafe`.
54
+ - LLM (escalation, ingestion): `OPENROUTER_API_KEY`, or `OPENAI_API_KEY` /
55
+ `ANTHROPIC_API_KEY` / `XAI_API_KEY` with `GRAPHWALK_LLM_PROVIDER`.
56
+ - No Jev access: `GRAPHWALK_DECISION_FALLBACK=llm` makes the chat model decide. It works,
57
+ but it is slower and its confidence is not calibrated, so escalation thresholds mean
58
+ little with it.
59
+
60
+ Importing triples needs no key at all.
61
+
62
+ ## Recipe 1: question answering over an existing knowledge graph
63
+
64
+ ```bash
65
+ graphwalk import kg.nt --graph kg.db # .nt, .csv, .tsv, .jsonl, or a|b|c .txt
66
+ graphwalk query kg.db "Where was the director of Inception born?" --escalate-below 0.9
67
+ ```
68
+
69
+ (`query` walks with `TraversalConfig.kgqa()` by default; flags such as `--hop-mode` or
70
+ `--strategy` override single settings.)
71
+
72
+ ```python
73
+ import asyncio
74
+ from graphwalk import Index, TraversalConfig
75
+
76
+
77
+ async def main() -> None:
78
+ async with Index.open(
79
+ "kg.db",
80
+ traversal=TraversalConfig.kgqa(), # the default; shown for clarity
81
+ node_types=["person", "film", "place"], # your graph's types, for the type hint
82
+ escalate_below=0.9,
83
+ ) as index:
84
+ await index.import_triples("kg.nt") # once; idempotent
85
+ result = await index.walk("Where was the director of Inception born?")
86
+ if result.best is None:
87
+ print("no answer") # entities not linked, or nothing reached
88
+ else:
89
+ print(result.best.names, result.confidence, result.escalated, result.cost_usd)
90
+
91
+
92
+ asyncio.run(main())
93
+ ```
94
+
95
+ - `TraversalConfig.kgqa()` is the configuration measured in the experiments (greedy
96
+ relation hops, typed options, answer-type hint, depth ≤ 4), and what `Index`, the CLI,
97
+ and the MCP server walk with unless given another. A bare `TraversalConfig()` (beam
98
+ search over individual neighbors) is a low-level default for `Traverser`, not a
99
+ measured setting.
100
+ - `result.best.names` is a **set** of entities (a relation hop moves to all its
101
+ targets). `result.best.path` is the list of hops taken; show it to users as the
102
+ justification.
103
+ - `result.confidence` is `exp(score)`, the length-normalized path probability. Treat it
104
+ as a ranking signal. 0.9 was the best default threshold on MetaQA; on a graph with a
105
+ different shape, check it on 50–100 labeled questions before relying on it.
106
+ - `escalate_below` re-walks unsure queries with the LLM decider (needs the `llm` extra
107
+ and an LLM key), or with `fallback_decider=` if you pass one. `result.cost_usd`
108
+ includes both walks.
109
+ - Start entities are found by matching names in the question. If you already know them
110
+ (from your own entity linker or the user's selection), call the CLI with
111
+ `--start <node id>` (repeatable).
112
+
113
+ Triples formats: CSV/TSV with a header (`subject,relation,object`, aliases
114
+ `head/source/s`, `predicate/type/p`, `tail/target/o`, plus optional `subject_type`,
115
+ `object_type`, `subject_name`, `object_name`); JSONL with the same keys; N-Triples
116
+ (`rdfs:label` becomes the name, `rdf:type` the type, literals become `literal` nodes).
117
+ Give nodes readable names and types: decisions are made from names, relation names,
118
+ and types, not from opaque ids.
119
+
120
+ ## Recipe 2: locate passages in documents (experimental)
121
+
122
+ Only when your questions chain through named entities (see the table). Otherwise use
123
+ dense retrieval or multi-step RAG.
124
+
125
+ ```python
126
+ from graphwalk import Index
127
+
128
+ async with Index.open("docs.db") as index: # inside async code; needs [llm,embeddings]
129
+ await index.ingest("docs/") # LLM extraction; costs money; idempotent
130
+ for location in await index.locate("Who directed ...?", k=5, mode="hybrid"):
131
+ passage = await index.read(location, context=200)
132
+ print(location.path, passage.text) # answer from the text, not the graph
133
+ ```
134
+
135
+ ## Recipe 3: give an agent the graph (MCP)
136
+
137
+ ```bash
138
+ pip install 'graphwalk[mcp,llm]'
139
+ graphwalk mcp --db kg.db # stdio; keys from the environment
140
+ ```
141
+
142
+ Tools: `walk` (answers with confidence and path), `locate` and `read` (documents),
143
+ `neighbors` and `get_node` (explore), `status`, `ingest` and `ingest_status`. The server
144
+ also serves this guide as the resource `graphwalk://guide`.
145
+
146
+ Set `GRAPHWALK_ESCALATE_BELOW=0.9` (after checking it on your graph) to escalate unsure
147
+ walks. The server's instructions then state the threshold, and every `walk` result
148
+ reports the threshold it used, so the agent knows which answers cleared it. Escalated
149
+ answers carry the LLM decider's confidence, which is not calibrated, so the agent
150
+ should verify them with `neighbors`.
151
+
152
+ ## API surface
153
+
154
+ Public names are those exported from `graphwalk` (anything else may change):
155
+ `Index`, `WalkResult`, `TraversalConfig`, `ImportReport`, `IngestConfig`,
156
+ `IngestReport`, `Location`, `Passage`, `Node`, `Edge`, `Neighbor`, `Provenance`,
157
+ `FileSource`, `TextSource`, `SourceDocument`, `FileDocuments`, `StoredDocuments`,
158
+ `GraphwalkError`, `DocumentNotFoundError`, `StaleLocationError`, `guide`.
159
+
160
+ `Index` methods: `open(path, **kwargs)`, `import_triples(path)`, `walk(query)`,
161
+ `ingest(source)`, `locate(query, k, mode=)`, `read(location, context=)`,
162
+ `neighbors(node)`, `close()`. All I/O methods are async.
163
+
164
+ CLI: `graphwalk import`, `query`, `ingest`, `locate`, `migrate`, `mcp`, `guide`.
165
+
166
+ ## Performance
167
+
168
+ - Per query: one Jev call per hop (~0.3–0.5 s each) plus graphwalk's overhead.
169
+ MetaQA walks cost $0.05–0.15 per 1,000 queries; Freebase subgraph walks ≈ $0.2.
170
+ - SQLite: imports ~9k triples/s; a 1.4M-edge graph is ~1.1 GB on disk. Listing a
171
+ node's edges takes <0.1 ms (56 ms at a 20k-neighbor hub).
172
+ - Walk overhead, excluding model calls: ~8 ms on typical nodes; 0.1 s (relation hops)
173
+ to 0.2 s (entity hops) from a 20k-neighbor hub. With an embedder, the first visit to a
174
+ hub embeds its neighbors' names (seconds with a local CPU model; cached after), so
175
+ prefer relation hops (`TraversalConfig.kgqa()`) on graphs with big hubs.
176
+
177
+ ## Common mistakes
178
+
179
+ - Using graphwalk to answer questions over documents. Use RAG; use `locate` only as a
180
+ retriever.
181
+ - Expecting constraint handling ("first", "largest", "both A and B"). graphwalk doesn't
182
+ filter, rank, or intersect answer sets; do that in your own code, or pick another tool.
183
+ - Comparing confidence across deciders. Only Jev's confidence was found informative.
184
+ - Passing a bare `TraversalConfig()` to `Index` or `Traverser` for KG-QA. Use
185
+ `TraversalConfig.kgqa()` (the `Index` default) and override single fields.
186
+ - Assuming the escalation threshold transfers. Check it on your own graph.
187
+ - Opaque node ids as names (`Q42`, `m.0abc`). Import names (`rdfs:label` or the name
188
+ columns) so the decider can read the options.