rete-graph 0.2.2__tar.gz → 0.3.0__tar.gz

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 (75) hide show
  1. {rete_graph-0.2.2 → rete_graph-0.3.0}/Cargo.toml +2 -2
  2. {rete_graph-0.2.2 → rete_graph-0.3.0}/PKG-INFO +4 -2
  3. {rete_graph-0.2.2/clients/python → rete_graph-0.3.0}/README.md +3 -1
  4. {rete_graph-0.2.2 → rete_graph-0.3.0}/clients/python/Cargo.lock +2 -102
  5. {rete_graph-0.2.2 → rete_graph-0.3.0}/clients/python/Cargo.toml +4 -3
  6. {rete_graph-0.2.2 → rete_graph-0.3.0/clients/python}/README.md +3 -1
  7. rete_graph-0.3.0/clients/python/examples/agents/README.md +54 -0
  8. rete_graph-0.3.0/clients/python/examples/agents/langchain_agent.py +66 -0
  9. rete_graph-0.3.0/clients/python/examples/agents/pydantic_ai_agent.py +68 -0
  10. rete_graph-0.3.0/clients/python/examples/agents/rete_tools.py +161 -0
  11. {rete_graph-0.2.2 → rete_graph-0.3.0}/clients/python/src/lib.rs +14 -0
  12. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/Cargo.toml +16 -6
  13. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/README.md +4 -2
  14. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/dict.rs +70 -19
  15. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/dictionary.rs +9 -1
  16. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/extbuild.rs +123 -73
  17. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/file.rs +304 -47
  18. rete_graph-0.3.0/crates/rete-core/src/geo3.rs +227 -0
  19. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/index.rs +202 -56
  20. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/ingest.rs +15 -0
  21. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/lib.rs +13 -2
  22. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/sparql/aggregate.rs +209 -120
  23. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/sparql/eval.rs +31 -21
  24. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/sparql/expr.rs +73 -0
  25. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/sparql/lower.rs +10 -0
  26. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/sparql.rs +5 -0
  27. rete_graph-0.3.0/crates/rete-core/tests/properties.proptest-regressions +7 -0
  28. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/tests/ranged.rs +91 -0
  29. {rete_graph-0.2.2 → rete_graph-0.3.0}/pyproject.toml +1 -1
  30. {rete_graph-0.2.2 → rete_graph-0.3.0}/python/rete_graph/__init__.py +3 -0
  31. {rete_graph-0.2.2 → rete_graph-0.3.0}/clients/python/.gitignore +0 -0
  32. {rete_graph-0.2.2 → rete_graph-0.3.0}/clients/python/examples/jupyterlite-build-tutorial.ipynb +0 -0
  33. {rete_graph-0.2.2 → rete_graph-0.3.0}/clients/python/examples/jupyterlite-demo.ipynb +0 -0
  34. {rete_graph-0.2.2 → rete_graph-0.3.0}/clients/python/examples/tutorial.ipynb +0 -0
  35. {rete_graph-0.2.2 → rete_graph-0.3.0}/clients/python/src/readers.rs +0 -0
  36. {rete_graph-0.2.2 → rete_graph-0.3.0}/clients/python/tests/conftest.py +0 -0
  37. {rete_graph-0.2.2 → rete_graph-0.3.0}/clients/python/tests/test_rete_graph.py +0 -0
  38. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/LICENSE +0 -0
  39. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/examples/coherence_bench.rs +0 -0
  40. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/examples/parallel_bench.rs +0 -0
  41. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/examples/qbench.rs +0 -0
  42. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/bgp.rs +0 -0
  43. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/block_cache.rs +0 -0
  44. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/geo.rs +0 -0
  45. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/header.rs +0 -0
  46. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/meta.rs +0 -0
  47. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/parallel.rs +0 -0
  48. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/pyramid.rs +0 -0
  49. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/reach.rs +0 -0
  50. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/reader.rs +0 -0
  51. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/reason.rs +0 -0
  52. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/results.rs +0 -0
  53. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/row.rs +0 -0
  54. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/schema_pyramid.rs +0 -0
  55. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/service.rs +0 -0
  56. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/shacl.rs +0 -0
  57. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/sparql/path.rs +0 -0
  58. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/sparql/ql.rs +0 -0
  59. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/terms.rs +0 -0
  60. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/text_index.rs +0 -0
  61. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/tiling.rs +0 -0
  62. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/triples.rs +0 -0
  63. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/src/varint.rs +0 -0
  64. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/tests/community_eval.rs +0 -0
  65. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/tests/compatibility_v1.rs +0 -0
  66. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/tests/fixtures/v1/minimal.rete +0 -0
  67. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/tests/fixtures/v1/source.nq +0 -0
  68. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/tests/properties.rs +0 -0
  69. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/tests/public_api.rs +0 -0
  70. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/tests/robustness.rs +0 -0
  71. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/tests/roundtrip.rs +0 -0
  72. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/tests/service_federation.rs +0 -0
  73. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/tests/shacl_core.rs +0 -0
  74. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/tests/sparql_integration.rs +0 -0
  75. {rete_graph-0.2.2 → rete_graph-0.3.0}/crates/rete-core/tests/sparql_nondeterministic.rs +0 -0
@@ -14,10 +14,10 @@ members = [
14
14
  # release gates (--locked checks, feature matrix, coverage floors) stay
15
15
  # untouched by binding-only changes. Future clients (R, Go) follow the same
16
16
  # pattern — clients/ consumes crates/, never the other way around.
17
- exclude = ["fuzz", "clients/python", "clients/r/src/rust"]
17
+ exclude = ["fuzz", "clients/python", "clients/r/src/rust", "clients/java/ffi"]
18
18
 
19
19
  [workspace.package]
20
- version = "1.0.0-rc.1"
20
+ version = "0.3.0"
21
21
  edition = "2021"
22
22
  license = "Apache-2.0"
23
23
  repository = "https://github.com/caviri/rete"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rete-graph
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Intended Audience :: Science/Research
@@ -49,7 +49,9 @@ Wheels cover Linux (x86_64/aarch64), macOS, Windows — and, from 0.2.0,
49
49
  graphs are queried over sync-XHR range requests, no server anywhere.
50
50
 
51
51
  A runnable tour with captured outputs lives in
52
- [`examples/tutorial.ipynb`](examples/tutorial.ipynb).
52
+ [`examples/tutorial.ipynb`](examples/tutorial.ipynb), and
53
+ [`examples/agents/`](examples/agents) turns a graph into agent tools for
54
+ LangChain and Pydantic AI.
53
55
 
54
56
  ## Use
55
57
 
@@ -21,7 +21,9 @@ Wheels cover Linux (x86_64/aarch64), macOS, Windows — and, from 0.2.0,
21
21
  graphs are queried over sync-XHR range requests, no server anywhere.
22
22
 
23
23
  A runnable tour with captured outputs lives in
24
- [`examples/tutorial.ipynb`](examples/tutorial.ipynb).
24
+ [`examples/tutorial.ipynb`](examples/tutorial.ipynb), and
25
+ [`examples/agents/`](examples/agents) turns a graph into agent tools for
26
+ LangChain and Pydantic AI.
25
27
 
26
28
  ## Use
27
29
 
@@ -49,12 +49,6 @@ dependencies = [
49
49
  "generic-array",
50
50
  ]
51
51
 
52
- [[package]]
53
- name = "bumpalo"
54
- version = "3.20.3"
55
- source = "registry+https://github.com/rust-lang/crates.io-index"
56
- checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
57
-
58
52
  [[package]]
59
53
  name = "cc"
60
54
  version = "1.2.67"
@@ -143,30 +137,6 @@ dependencies = [
143
137
  "percent-encoding",
144
138
  ]
145
139
 
146
- [[package]]
147
- name = "futures-core"
148
- version = "0.3.32"
149
- source = "registry+https://github.com/rust-lang/crates.io-index"
150
- checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
151
-
152
- [[package]]
153
- name = "futures-task"
154
- version = "0.3.32"
155
- source = "registry+https://github.com/rust-lang/crates.io-index"
156
- checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
157
-
158
- [[package]]
159
- name = "futures-util"
160
- version = "0.3.32"
161
- source = "registry+https://github.com/rust-lang/crates.io-index"
162
- checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
163
- dependencies = [
164
- "futures-core",
165
- "futures-task",
166
- "pin-project-lite",
167
- "slab",
168
- ]
169
-
170
140
  [[package]]
171
141
  name = "generic-array"
172
142
  version = "0.14.7"
@@ -184,10 +154,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
184
154
  checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
185
155
  dependencies = [
186
156
  "cfg-if",
187
- "js-sys",
188
157
  "libc",
189
158
  "wasi",
190
- "wasm-bindgen",
191
159
  ]
192
160
 
193
161
  [[package]]
@@ -335,17 +303,6 @@ dependencies = [
335
303
  "libc",
336
304
  ]
337
305
 
338
- [[package]]
339
- name = "js-sys"
340
- version = "0.3.103"
341
- source = "registry+https://github.com/rust-lang/crates.io-index"
342
- checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
343
- dependencies = [
344
- "cfg-if",
345
- "futures-util",
346
- "wasm-bindgen",
347
- ]
348
-
349
306
  [[package]]
350
307
  name = "libc"
351
308
  version = "0.2.186"
@@ -481,12 +438,6 @@ version = "2.3.2"
481
438
  source = "registry+https://github.com/rust-lang/crates.io-index"
482
439
  checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
483
440
 
484
- [[package]]
485
- name = "pin-project-lite"
486
- version = "0.2.17"
487
- source = "registry+https://github.com/rust-lang/crates.io-index"
488
- checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
489
-
490
441
  [[package]]
491
442
  name = "pkg-config"
492
443
  version = "0.3.33"
@@ -651,7 +602,7 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
651
602
 
652
603
  [[package]]
653
604
  name = "rete-core"
654
- version = "1.0.0-rc.1"
605
+ version = "0.3.0"
655
606
  dependencies = [
656
607
  "blake3",
657
608
  "getrandom 0.2.17",
@@ -671,7 +622,7 @@ dependencies = [
671
622
 
672
623
  [[package]]
673
624
  name = "rete-python"
674
- version = "0.2.1"
625
+ version = "0.3.0"
675
626
  dependencies = [
676
627
  "pyo3",
677
628
  "rete-core",
@@ -813,12 +764,6 @@ version = "2.0.1"
813
764
  source = "registry+https://github.com/rust-lang/crates.io-index"
814
765
  checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
815
766
 
816
- [[package]]
817
- name = "slab"
818
- version = "0.4.12"
819
- source = "registry+https://github.com/rust-lang/crates.io-index"
820
- checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
821
-
822
767
  [[package]]
823
768
  name = "smallvec"
824
769
  version = "1.15.2"
@@ -994,51 +939,6 @@ version = "0.11.1+wasi-snapshot-preview1"
994
939
  source = "registry+https://github.com/rust-lang/crates.io-index"
995
940
  checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
996
941
 
997
- [[package]]
998
- name = "wasm-bindgen"
999
- version = "0.2.126"
1000
- source = "registry+https://github.com/rust-lang/crates.io-index"
1001
- checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
1002
- dependencies = [
1003
- "cfg-if",
1004
- "once_cell",
1005
- "rustversion",
1006
- "wasm-bindgen-macro",
1007
- "wasm-bindgen-shared",
1008
- ]
1009
-
1010
- [[package]]
1011
- name = "wasm-bindgen-macro"
1012
- version = "0.2.126"
1013
- source = "registry+https://github.com/rust-lang/crates.io-index"
1014
- checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
1015
- dependencies = [
1016
- "quote",
1017
- "wasm-bindgen-macro-support",
1018
- ]
1019
-
1020
- [[package]]
1021
- name = "wasm-bindgen-macro-support"
1022
- version = "0.2.126"
1023
- source = "registry+https://github.com/rust-lang/crates.io-index"
1024
- checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
1025
- dependencies = [
1026
- "bumpalo",
1027
- "proc-macro2",
1028
- "quote",
1029
- "syn",
1030
- "wasm-bindgen-shared",
1031
- ]
1032
-
1033
- [[package]]
1034
- name = "wasm-bindgen-shared"
1035
- version = "0.2.126"
1036
- source = "registry+https://github.com/rust-lang/crates.io-index"
1037
- checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
1038
- dependencies = [
1039
- "unicode-ident",
1040
- ]
1041
-
1042
942
  [[package]]
1043
943
  name = "webpki-roots"
1044
944
  version = "0.26.11"
@@ -3,9 +3,10 @@
3
3
  # maturin (pyproject.toml), and is published to PyPI, never to crates.io.
4
4
  [package]
5
5
  name = "rete-python"
6
- # Keep in lockstep with pyproject.toml's [project] version — __version__
7
- # comes from here (CARGO_PKG_VERSION), the wheel metadata from pyproject.
8
- version = "0.2.1"
6
+ # Kept in lockstep with pyproject.toml's [project] version, and with the engine's
7
+ # major.minor, by scripts/sync_versions.py (--check runs in CI). __version__ comes
8
+ # from here (CARGO_PKG_VERSION), the wheel metadata from pyproject.
9
+ version = "0.3.0"
9
10
  edition = "2021"
10
11
  license = "Apache-2.0"
11
12
  repository = "https://github.com/caviri/rete"
@@ -21,7 +21,9 @@ Wheels cover Linux (x86_64/aarch64), macOS, Windows — and, from 0.2.0,
21
21
  graphs are queried over sync-XHR range requests, no server anywhere.
22
22
 
23
23
  A runnable tour with captured outputs lives in
24
- [`examples/tutorial.ipynb`](examples/tutorial.ipynb).
24
+ [`examples/tutorial.ipynb`](examples/tutorial.ipynb), and
25
+ [`examples/agents/`](examples/agents) turns a graph into agent tools for
26
+ LangChain and Pydantic AI.
25
27
 
26
28
  ## Use
27
29
 
@@ -0,0 +1,54 @@
1
+ # Agent frameworks over a `.rete` graph
2
+
3
+ Runnable companions to the
4
+ [LangChain & Pydantic AI tutorial](https://caviri.github.io/rete/agent-frameworks.html).
5
+ Each script gives a model five tools over **one** `.rete` file — card, schema,
6
+ examples, entity lookup, SPARQL — and lets it answer questions by querying.
7
+ The file is read by byte range, local or remote, with no server in between.
8
+
9
+ | File | What it is |
10
+ | --- | --- |
11
+ | `rete_tools.py` | The tool layer. Framework-agnostic: five methods returning JSON, plus the system prompt that teaches the loop. |
12
+ | `pydantic_ai_agent.py` | [Pydantic AI](https://ai.pydantic.dev) agent — bound methods passed straight to `Agent(tools=[...])`. |
13
+ | `langchain_agent.py` | [LangChain](https://python.langchain.com) agent — the same methods via `StructuredTool.from_function` and `create_agent`. |
14
+
15
+ ## Run
16
+
17
+ ```sh
18
+ pip install rete-graph "pydantic-ai-slim[anthropic]" # ... or
19
+ pip install rete-graph langchain langchain-anthropic
20
+
21
+ export ANTHROPIC_API_KEY=...
22
+ python pydantic_ai_agent.py "How many laws are here? Show me three with their titles."
23
+ python langchain_agent.py "Which three laws are the oldest?"
24
+ ```
25
+
26
+ Both default to the published Spanish-legislation graph
27
+ (`https://data.graphplaza.com/boe/boe.rete`, 7 MB on a CDN). Point them
28
+ anywhere — including a private file that never leaves the machine:
29
+
30
+ ```sh
31
+ RETE_GRAPH=/path/to/my-graph.rete python langchain_agent.py "What is in this graph?"
32
+ ```
33
+
34
+ Any OpenAI-compatible endpoint works instead of Anthropic (`pip install
35
+ langchain-openai` for the LangChain script):
36
+
37
+ ```sh
38
+ export OPENAI_BASE_URL=https://your-endpoint/v1 OPENAI_API_KEY=... MODEL=...
39
+ ```
40
+
41
+ Each run prints the bytes and requests actually fetched, so the cost of the
42
+ agent's exploration stays visible:
43
+
44
+ ```
45
+ [graph reads: {'fileLength': 6958628, 'bytes': 3065519, 'requests': 18}]
46
+ ```
47
+
48
+ ## Verified
49
+
50
+ Both scripts were run end to end against a live tool-calling model and the
51
+ remote `boe` graph: the agent reads the schema, copies the ELI IRIs out of it,
52
+ writes its own SPARQL, and answers from the rows. The MCP alternative —
53
+ pointing either framework at the hosted rete MCP server instead — is in the
54
+ same tutorial.
@@ -0,0 +1,66 @@
1
+ """A LangChain agent that answers questions from one `.rete` graph.
2
+
3
+ pip install rete-graph langchain langchain-anthropic
4
+ export ANTHROPIC_API_KEY=...
5
+ python langchain_agent.py "How many laws are in this graph, and which are the oldest?"
6
+
7
+ Defaults to the published Spanish-legislation graph (7 MB on a CDN, read by
8
+ byte range). Point it anywhere with RETE_GRAPH=/path/to/your.rete.
9
+
10
+ Any OpenAI-compatible endpoint works instead of Anthropic:
11
+
12
+ pip install langchain-openai
13
+ export OPENAI_BASE_URL=https://your-endpoint/v1 OPENAI_API_KEY=... MODEL=...
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import sys
20
+
21
+ from langchain.agents import create_agent
22
+ from langchain_core.tools import StructuredTool
23
+
24
+ from rete_tools import SYSTEM_PROMPT, ReteGraphTools
25
+
26
+ GRAPH = os.environ.get("RETE_GRAPH", "https://data.graphplaza.com/boe/boe.rete")
27
+
28
+
29
+ def build_model():
30
+ """An Anthropic model by default; an OpenAI-compatible endpoint if configured."""
31
+ model = os.environ.get("MODEL", "claude-sonnet-5")
32
+ base_url = os.environ.get("OPENAI_BASE_URL")
33
+ if not base_url:
34
+ from langchain.chat_models import init_chat_model
35
+
36
+ return init_chat_model(model, model_provider="anthropic")
37
+
38
+ from langchain_openai import ChatOpenAI
39
+
40
+ return ChatOpenAI(model=model, base_url=base_url, api_key=os.environ["OPENAI_API_KEY"])
41
+
42
+
43
+ def rete_toolkit(source: str) -> tuple[ReteGraphTools, list[StructuredTool]]:
44
+ """Bound methods → LangChain tools; name and description come from the method."""
45
+ tools = ReteGraphTools(source)
46
+ methods = [
47
+ tools.dataset_card,
48
+ tools.dataset_schema,
49
+ tools.example_queries,
50
+ tools.find_entities,
51
+ tools.sparql_query,
52
+ ]
53
+ return tools, [StructuredTool.from_function(m, name=m.__name__) for m in methods]
54
+
55
+
56
+ def main(question: str) -> None:
57
+ tools, toolkit = rete_toolkit(GRAPH)
58
+ agent = create_agent(build_model(), toolkit, system_prompt=SYSTEM_PROMPT)
59
+
60
+ result = agent.invoke({"messages": [{"role": "user", "content": question}]})
61
+ print(result["messages"][-1].content)
62
+ print(f"\n[graph reads: {tools.read_stats()}]")
63
+
64
+
65
+ if __name__ == "__main__":
66
+ main(" ".join(sys.argv[1:]) or "What is in this graph? Show me five example entities.")
@@ -0,0 +1,68 @@
1
+ """A Pydantic AI agent that answers questions from one `.rete` graph.
2
+
3
+ pip install rete-graph "pydantic-ai-slim[anthropic]"
4
+ export ANTHROPIC_API_KEY=...
5
+ python pydantic_ai_agent.py "How many laws are in this graph, and which are the oldest?"
6
+
7
+ Defaults to the published Spanish-legislation graph (7 MB on a CDN, read by
8
+ byte range). Point it anywhere with RETE_GRAPH=/path/to/your.rete.
9
+
10
+ Any OpenAI-compatible endpoint works instead of Anthropic:
11
+
12
+ export OPENAI_BASE_URL=https://your-endpoint/v1 OPENAI_API_KEY=... MODEL=...
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import os
19
+ import sys
20
+
21
+ from pydantic_ai import Agent
22
+
23
+ from rete_tools import SYSTEM_PROMPT, ReteGraphTools
24
+
25
+ GRAPH = os.environ.get("RETE_GRAPH", "https://data.graphplaza.com/boe/boe.rete")
26
+
27
+
28
+ def build_model():
29
+ """An Anthropic model by default; an OpenAI-compatible endpoint if configured."""
30
+ model = os.environ.get("MODEL", "anthropic:claude-sonnet-5")
31
+ base_url = os.environ.get("OPENAI_BASE_URL")
32
+ if not base_url:
33
+ return model
34
+
35
+ from pydantic_ai.models.openai import OpenAIChatModel
36
+ from pydantic_ai.providers.openai import OpenAIProvider
37
+
38
+ return OpenAIChatModel(
39
+ model,
40
+ provider=OpenAIProvider(base_url=base_url, api_key=os.environ["OPENAI_API_KEY"]),
41
+ )
42
+
43
+
44
+ async def main(question: str) -> None:
45
+ tools = ReteGraphTools(GRAPH)
46
+
47
+ # Bound methods: pydantic-ai reads the signature for the schema and the
48
+ # docstring for the description, so the tools need no extra declaration.
49
+ agent = Agent(
50
+ build_model(),
51
+ system_prompt=SYSTEM_PROMPT,
52
+ tools=[
53
+ tools.dataset_card,
54
+ tools.dataset_schema,
55
+ tools.example_queries,
56
+ tools.find_entities,
57
+ tools.sparql_query,
58
+ ],
59
+ )
60
+
61
+ result = await agent.run(question)
62
+ print(result.output)
63
+ print(f"\n[graph reads: {tools.read_stats()}]")
64
+
65
+
66
+ if __name__ == "__main__":
67
+ question = " ".join(sys.argv[1:]) or "What is in this graph? Show me five example entities."
68
+ asyncio.run(main(question))
@@ -0,0 +1,161 @@
1
+ """A framework-agnostic tool surface over one `.rete` graph.
2
+
3
+ Five tools mirror the read loop the rete MCP server exposes — card → schema →
4
+ examples → find_entities → sparql_query — but run **in process**, against a
5
+ local path or an `https://` URL, with no server between the agent and the file.
6
+
7
+ Each method returns a JSON string, because that is what every agent framework
8
+ wants to hand a model. Results are trimmed: a real schema is hundreds of
9
+ kilobytes, which would swamp the context window without telling the model
10
+ anything it can act on.
11
+
12
+ Reads are byte ranges, so the cost of a tool call is bounded by the *query*,
13
+ not the file: `tools.read_stats()` reports the bytes and requests actually
14
+ fetched so far.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ from typing import Any, Dict, List
21
+
22
+ import rete_graph as rete
23
+
24
+ MAX_ROWS = 100 # rows handed back to the model
25
+ MAX_CLASSES = 40 # schema trimming — the full profile is far too big to prompt with
26
+ MAX_RELATIONS = 60
27
+ MAX_EXAMPLES = 8
28
+
29
+
30
+ def _json(obj: Any) -> str:
31
+ return json.dumps(obj, ensure_ascii=False, default=str)
32
+
33
+
34
+ def _value(term: Any) -> Any:
35
+ """A `Term` as a plain JSON value: typed literals become int/float/bool."""
36
+ try:
37
+ return term.to_python()
38
+ except Exception: # unusual datatype — keep the lexical form
39
+ return term.value
40
+
41
+
42
+ class ReteGraphTools:
43
+ """The five tools, bound to one graph.
44
+
45
+ >>> tools = ReteGraphTools("https://data.graphplaza.com/boe/boe.rete")
46
+ >>> tools.dataset_card()[:40]
47
+ '{"title": "BOE — Spanish consolidated l'
48
+ """
49
+
50
+ def __init__(self, source: str, *, max_rows: int = MAX_ROWS, **open_kwargs: Any):
51
+ self.source = source
52
+ self.max_rows = max_rows
53
+ self.graph = rete.open(source, **open_kwargs)
54
+
55
+ # -- discovery ---------------------------------------------------------
56
+
57
+ def dataset_card(self) -> str:
58
+ """What this graph is: title, description, licence, size, vocabularies."""
59
+ card = self.graph.card() or {}
60
+ keep = (
61
+ "title",
62
+ "description",
63
+ "license",
64
+ "source",
65
+ "created",
66
+ "triple_count",
67
+ "quad_count",
68
+ "term_count",
69
+ "vocabularies",
70
+ "languages",
71
+ )
72
+ out: Dict[str, Any] = {k: card[k] for k in keep if card.get(k)}
73
+ out["info"] = self.graph.info()
74
+ return _json(out)
75
+
76
+ def dataset_schema(self) -> str:
77
+ """The classes and subject→predicate→object relations actually present.
78
+
79
+ Copy these IRIs verbatim into queries; do not invent namespaces.
80
+ """
81
+ schema = self.graph.schema()
82
+ classes = sorted(schema.get("classes", []), key=lambda c: -c[1])[:MAX_CLASSES]
83
+ relations = sorted(schema.get("relations", []), key=lambda r: -r[3])[:MAX_RELATIONS]
84
+ return _json(
85
+ {
86
+ "classes": [{"class": c[0], "instances": c[1]} for c in classes],
87
+ "relations": [
88
+ {"subject_class": r[0], "predicate": r[1], "object_class": r[2], "count": r[3]}
89
+ for r in relations
90
+ ],
91
+ "note": (
92
+ f"top {len(classes)}/{len(schema.get('classes', []))} classes and "
93
+ f"{len(relations)}/{len(schema.get('relations', []))} relations, by count"
94
+ ),
95
+ }
96
+ )
97
+
98
+ def example_queries(self) -> str:
99
+ """Runnable SPARQL the dataset ships with — the graph's own idioms."""
100
+ examples = self.graph.examples() or []
101
+ return _json(
102
+ [
103
+ {"question": e.get("question") or e.get("title"), "sparql": e.get("sparql")}
104
+ for e in examples[:MAX_EXAMPLES]
105
+ ]
106
+ )
107
+
108
+ # -- lookup ------------------------------------------------------------
109
+
110
+ def find_entities(self, text: str) -> str:
111
+ """Resolve a name to entity IRIs. Use before querying for a named thing."""
112
+ hits: List[List[str]] = list(self.graph.prefix_search(text) or [])
113
+ if not hits:
114
+ # only present when the file was built with --text-index; [] otherwise
115
+ hits = list(self.graph.text_search(text) or [])
116
+ return _json([{"label": h[0], "iri": h[1]} for h in hits[:20]])
117
+
118
+ # -- query -------------------------------------------------------------
119
+
120
+ def sparql_query(self, query: str) -> str:
121
+ """Run SPARQL 1.1 (SELECT / ASK / CONSTRUCT / DESCRIBE) over the graph.
122
+
123
+ A syntax or vocabulary mistake comes back as `{"error": ...}` with the
124
+ parser's message — read it and retry rather than giving up.
125
+ """
126
+ try:
127
+ result = self.graph.query(query)
128
+ except (ValueError, RuntimeError) as exc:
129
+ return _json({"error": str(exc)})
130
+
131
+ if isinstance(result, bool):
132
+ return _json({"kind": "ask", "boolean": result})
133
+ if result and isinstance(result[0], tuple):
134
+ triples = [[_value(t) for t in row] for row in result[: self.max_rows]]
135
+ return _json(
136
+ {"kind": "graph", "triples": triples, "truncated": len(result) > self.max_rows}
137
+ )
138
+ rows = [{k: _value(v) for k, v in row.items()} for row in result[: self.max_rows]]
139
+ return _json({"kind": "table", "rows": rows, "truncated": len(result) > self.max_rows})
140
+
141
+ # -- observability -----------------------------------------------------
142
+
143
+ def read_stats(self) -> Dict[str, Any]:
144
+ """Bytes and requests physically fetched so far — laziness, measured."""
145
+ return self.graph.stats()
146
+
147
+
148
+ SYSTEM_PROMPT = """\
149
+ You answer questions about ONE RDF knowledge graph by writing and running
150
+ SPARQL. Ground every answer in query results — never guess.
151
+
152
+ Method:
153
+ 1. dataset_card, then dataset_schema. Copy class and predicate IRIs exactly
154
+ from the schema; never invent a namespace or a prefix.
155
+ 2. example_queries shows how this particular graph is meant to be queried.
156
+ 3. If the question names a specific thing, resolve it with find_entities first.
157
+ 4. sparql_query, then read the rows and refine. Always use LIMIT (<= 200).
158
+ If a query errors, fix it from the message and try again.
159
+
160
+ Answer in prose, and state the SPARQL you ran.
161
+ """
@@ -127,6 +127,15 @@ impl Graph {
127
127
  }
128
128
  Ok(())
129
129
  }
130
+
131
+ /// The incompleteness verdict is PER CALL on this resident handle: reset
132
+ /// the sticky failure flags before evaluating, so one transient network
133
+ /// failure fails only the call it happened in — failed tiles/chunks are
134
+ /// never cached, so the next call simply retries them. (Same shape as the
135
+ /// browser engine's RemoteGraph entry points.)
136
+ fn fresh_verdict(&self) {
137
+ self.rete.reset_load_failures();
138
+ }
130
139
  }
131
140
 
132
141
  #[pymethods]
@@ -143,6 +152,7 @@ impl Graph {
143
152
  graph: Option<&str>,
144
153
  format: &str,
145
154
  ) -> PyResult<String> {
155
+ self.fresh_verdict();
146
156
  let out = py
147
157
  .allow_threads(|| -> Result<String, String> {
148
158
  let shapes =
@@ -169,6 +179,7 @@ impl Graph {
169
179
  /// QL entailment by query rewriting.
170
180
  #[pyo3(signature = (query, *, reason=false))]
171
181
  fn query(&self, py: Python<'_>, query: &str, reason: bool) -> PyResult<String> {
182
+ self.fresh_verdict();
172
183
  let out = py
173
184
  .allow_threads(|| {
174
185
  if reason {
@@ -474,5 +485,8 @@ fn _rete(m: &Bound<'_, PyModule>) -> PyResult<()> {
474
485
  m.add_function(wrap_pyfunction!(build, m)?)?;
475
486
  m.add_function(wrap_pyfunction!(build_dataset, m)?)?;
476
487
  m.add("__version__", env!("CARGO_PKG_VERSION"))?;
488
+ // The binding's version tracks the binding; this reports the engine actually
489
+ // compiled into the wheel, which is what "does my install support X?" means.
490
+ m.add("__engine_version__", rete_core::VERSION)?;
477
491
  Ok(())
478
492
  }
@@ -14,6 +14,12 @@ categories = ["database", "data-structures", "web-programming"]
14
14
  publish = ["crates-io"]
15
15
  include = ["src/**", "tests/**", "examples/**", "Cargo.toml", "README.md", "LICENSE"]
16
16
 
17
+ # docs.rs builds default features only, which would hide the `parallel` module.
18
+ # The builder is native Linux with a C toolchain, so `compression` + `rayon`
19
+ # both build there.
20
+ [package.metadata.docs.rs]
21
+ all-features = true
22
+
17
23
  [dependencies]
18
24
  thiserror.workspace = true
19
25
  oxrdf.workspace = true
@@ -33,8 +39,11 @@ regex-lite = "0.1"
33
39
  md-5 = "0.10"
34
40
  sha1 = "0.10"
35
41
  sha2 = "0.10"
36
- # Randomness for the SPARQL RAND/UUID/STRUUID/BNODE built-ins. On wasm the
37
- # browser backend is enabled via the `js` feature in the wasm-target block below.
42
+ # Randomness for the SPARQL RAND/UUID/STRUUID/BNODE built-ins. On
43
+ # wasm32-unknown-unknown getrandom has no default backend: a *browser* consumer
44
+ # turns on the `wasm-js` feature below (crypto.getRandomValues); a non-browser
45
+ # wasm host (Chicory/WASI/Wasmtime) must supply its own via getrandom's `custom`
46
+ # backend. Native targets use the OS backend and need neither.
38
47
  getrandom = "0.2"
39
48
  zstd = { workspace = true, optional = true }
40
49
  # Data-parallel query evaluator (prototype). Behind the `parallel` feature so it
@@ -54,6 +63,11 @@ compression = ["dep:zstd"]
54
63
  # Prototype data-parallel query evaluator. NOT default; wasm must never enable it
55
64
  # (wasm has no threads). Enables the `parallel` module + `rayon`.
56
65
  parallel = ["dep:rayon"]
66
+ # getrandom's browser backend (crypto.getRandomValues, via js-sys/wasm-bindgen).
67
+ # Enable ONLY for a wasm build that runs in a browser — it makes the module
68
+ # import wasm-bindgen JS glue, which a non-browser wasm host (Chicory on the JVM,
69
+ # WASI) cannot link. Those hosts leave this off and register a `custom` backend.
70
+ wasm-js = ["getrandom/js"]
57
71
 
58
72
  # The parallel benchmark only compiles with the `parallel` feature (it uses
59
73
  # rayon + the feature-gated `parallel` module). Without the gate, a default
@@ -61,7 +75,3 @@ parallel = ["dep:rayon"]
61
75
  [[example]]
62
76
  name = "parallel_bench"
63
77
  required-features = ["parallel"]
64
-
65
- # On wasm, getrandom (pulled transitively via oxrdf) needs its browser backend.
66
- [target.'cfg(target_arch = "wasm32")'.dependencies]
67
- getrandom = { version = "0.2", features = ["js"] }