reasongraph 0.2.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.
- reasongraph-0.2.0/.github/workflows/publish.yml +28 -0
- reasongraph-0.2.0/.github/workflows/test.yml +29 -0
- reasongraph-0.2.0/.gitignore +39 -0
- reasongraph-0.2.0/LICENSE +21 -0
- reasongraph-0.2.0/PKG-INFO +230 -0
- reasongraph-0.2.0/README.md +187 -0
- reasongraph-0.2.0/examples/financial_demo.py +75 -0
- reasongraph-0.2.0/pyproject.toml +73 -0
- reasongraph-0.2.0/src/reasongraph/__init__.py +21 -0
- reasongraph-0.2.0/src/reasongraph/_embeddings.py +67 -0
- reasongraph-0.2.0/src/reasongraph/_extraction.py +157 -0
- reasongraph-0.2.0/src/reasongraph/_types.py +24 -0
- reasongraph-0.2.0/src/reasongraph/backends/__init__.py +5 -0
- reasongraph-0.2.0/src/reasongraph/backends/_base.py +72 -0
- reasongraph-0.2.0/src/reasongraph/backends/_postgres.py +235 -0
- reasongraph-0.2.0/src/reasongraph/backends/_sqlite.py +352 -0
- reasongraph-0.2.0/src/reasongraph/datasets/__init__.py +3 -0
- reasongraph-0.2.0/src/reasongraph/datasets/_registry.py +28 -0
- reasongraph-0.2.0/src/reasongraph/datasets/analysis_patterns.json +203 -0
- reasongraph-0.2.0/src/reasongraph/datasets/causal.json +83 -0
- reasongraph-0.2.0/src/reasongraph/datasets/financial.json +120 -0
- reasongraph-0.2.0/src/reasongraph/datasets/medical.json +85 -0
- reasongraph-0.2.0/src/reasongraph/datasets/syllogisms.json +86 -0
- reasongraph-0.2.0/src/reasongraph/datasets/taxonomy.json +87 -0
- reasongraph-0.2.0/src/reasongraph/graph.py +366 -0
- reasongraph-0.2.0/src/reasongraph/py.typed +0 -0
- reasongraph-0.2.0/tests/eval_financial_reasoning.py +732 -0
- reasongraph-0.2.0/tests/test_datasets.py +47 -0
- reasongraph-0.2.0/tests/test_graph.py +428 -0
- reasongraph-0.2.0/tests/test_sqlite_backend.py +186 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
id-token: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
environment: pypi
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python
|
|
22
|
+
run: uv python install 3.12
|
|
23
|
+
|
|
24
|
+
- name: Build package
|
|
25
|
+
run: uv build
|
|
26
|
+
|
|
27
|
+
- name: Publish to PyPI
|
|
28
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v4
|
|
21
|
+
|
|
22
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
23
|
+
run: uv python install ${{ matrix.python-version }}
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: uv sync --extra dev
|
|
27
|
+
|
|
28
|
+
- name: Run tests
|
|
29
|
+
run: uv run pytest -v
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
*.egg
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.eggs/
|
|
9
|
+
|
|
10
|
+
# Distribution
|
|
11
|
+
dist/
|
|
12
|
+
build/
|
|
13
|
+
*.whl
|
|
14
|
+
|
|
15
|
+
# Virtual environments
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
env/
|
|
19
|
+
|
|
20
|
+
# IDE
|
|
21
|
+
.idea/
|
|
22
|
+
.vscode/
|
|
23
|
+
*.swp
|
|
24
|
+
*.swo
|
|
25
|
+
|
|
26
|
+
# Testing
|
|
27
|
+
.pytest_cache/
|
|
28
|
+
.coverage
|
|
29
|
+
htmlcov/
|
|
30
|
+
|
|
31
|
+
# OS
|
|
32
|
+
.DS_Store
|
|
33
|
+
Thumbs.db
|
|
34
|
+
|
|
35
|
+
# Locks
|
|
36
|
+
uv.lock
|
|
37
|
+
|
|
38
|
+
# Local scratch
|
|
39
|
+
tmp/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Berk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reasongraph
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A graph-based reasoning library with embedding search and multi-hop traversal
|
|
5
|
+
Project-URL: Homepage, https://github.com/bgokden/reasongraph
|
|
6
|
+
Project-URL: Repository, https://github.com/bgokden/reasongraph
|
|
7
|
+
Project-URL: Issues, https://github.com/bgokden/reasongraph/issues
|
|
8
|
+
Author: Berk
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,causal-reasoning,embeddings,knowledge-graph,multi-hop,ner,nlp,rag,reasoning,semantic-search
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: aiosqlite>=0.19.0
|
|
21
|
+
Requires-Dist: sentence-transformers>=2.2.0
|
|
22
|
+
Requires-Dist: sqlite-vec>=0.1.6
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: gliner2>=1.2.0; extra == 'all'
|
|
25
|
+
Requires-Dist: pgvector>=0.2.0; extra == 'all'
|
|
26
|
+
Requires-Dist: psycopg-pool>=3.1.0; extra == 'all'
|
|
27
|
+
Requires-Dist: psycopg[binary]>=3.1.0; extra == 'all'
|
|
28
|
+
Requires-Dist: requests>=2.28.0; extra == 'all'
|
|
29
|
+
Requires-Dist: urllib3>=2.0.0; extra == 'all'
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
34
|
+
Provides-Extra: gliner2
|
|
35
|
+
Requires-Dist: gliner2>=1.2.0; extra == 'gliner2'
|
|
36
|
+
Requires-Dist: requests>=2.28.0; extra == 'gliner2'
|
|
37
|
+
Requires-Dist: urllib3>=2.0.0; extra == 'gliner2'
|
|
38
|
+
Provides-Extra: postgres
|
|
39
|
+
Requires-Dist: pgvector>=0.2.0; extra == 'postgres'
|
|
40
|
+
Requires-Dist: psycopg-pool>=3.1.0; extra == 'postgres'
|
|
41
|
+
Requires-Dist: psycopg[binary]>=3.1.0; extra == 'postgres'
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# reasongraph
|
|
45
|
+
|
|
46
|
+
A graph-based reasoning library with embedding search, multi-hop traversal, and automatic entity/causal extraction.
|
|
47
|
+
|
|
48
|
+
[](https://pypi.org/project/reasongraph/)
|
|
49
|
+
[](https://pypi.org/project/reasongraph/)
|
|
50
|
+
[](LICENSE)
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install reasongraph[all] # everything included
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or install only what you need:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install reasongraph # core: SQLite backend, NER extraction, embeddings
|
|
62
|
+
pip install reasongraph[gliner2] # + GLiNER2 entity + causal extraction (recommended)
|
|
63
|
+
pip install reasongraph[postgres] # + PostgreSQL + pgvector backend
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Quick Start
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from reasongraph import ReasonGraph
|
|
70
|
+
|
|
71
|
+
graph = ReasonGraph()
|
|
72
|
+
graph.initialize_sync()
|
|
73
|
+
|
|
74
|
+
# Add text with automatic entity + causal extraction
|
|
75
|
+
graph.add_text_sync("Lehman Brothers filed for bankruptcy in September 2008.")
|
|
76
|
+
graph.add_text_sync("The Federal Reserve cut interest rates to near zero.")
|
|
77
|
+
|
|
78
|
+
# Query with embedding search + multi-hop graph traversal
|
|
79
|
+
results = graph.query_sync("What caused the 2008 financial crisis?")
|
|
80
|
+
for text in results:
|
|
81
|
+
print(text)
|
|
82
|
+
|
|
83
|
+
graph.close_sync()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or use the async API with a context manager:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import asyncio
|
|
90
|
+
from reasongraph import ReasonGraph
|
|
91
|
+
|
|
92
|
+
async def main():
|
|
93
|
+
async with ReasonGraph() as graph:
|
|
94
|
+
await graph.load_dataset("financial")
|
|
95
|
+
results = await graph.query("What caused the 2008 crisis?")
|
|
96
|
+
for text in results:
|
|
97
|
+
print(text)
|
|
98
|
+
|
|
99
|
+
asyncio.run(main())
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Features
|
|
103
|
+
|
|
104
|
+
- **Automatic extraction** -- GLiNER2 extracts entities and causal relations in one pass (falls back to BERT NER when gliner2 is not installed)
|
|
105
|
+
- **Hybrid search** -- combine embedding similarity, keyword (trigram) matching, or both
|
|
106
|
+
- **Multi-hop traversal** -- follow graph edges to discover connected reasoning chains
|
|
107
|
+
- **Cross-encoder reranking** -- rerank results at each hop with `ms-marco-MiniLM-L-6-v2`
|
|
108
|
+
- **Built-in datasets** -- load curated reasoning graphs for immediate use
|
|
109
|
+
- **Async-first** -- native async API with sync convenience wrappers
|
|
110
|
+
- **Pluggable backends** -- SQLite (zero-config default) or PostgreSQL with pgvector
|
|
111
|
+
|
|
112
|
+
## Built-in Datasets
|
|
113
|
+
|
|
114
|
+
| Dataset | Description |
|
|
115
|
+
|---------|-------------|
|
|
116
|
+
| `syllogisms` | Classical syllogistic reasoning chains |
|
|
117
|
+
| `causal` | Cause-effect reasoning with entity annotations |
|
|
118
|
+
| `taxonomy` | Hierarchical concept taxonomy |
|
|
119
|
+
| `financial` | Financial crisis causal chains (2008 crisis, dot-com, inflation, eurozone) |
|
|
120
|
+
| `medical` | Medical causal chains (heart disease, diabetes, infectious disease, cancer) |
|
|
121
|
+
| `analysis_patterns` | Data analysis reasoning: scenario detection, technique selection, implementation patterns |
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
graph.load_dataset_sync("financial")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Search Modes
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
# Pure embedding similarity (default)
|
|
131
|
+
results = graph.query_sync("credit freeze", search_mode="embedding")
|
|
132
|
+
|
|
133
|
+
# Pure keyword/trigram matching
|
|
134
|
+
results = graph.query_sync("credit freeze", search_mode="keyword")
|
|
135
|
+
|
|
136
|
+
# Hybrid: Reciprocal Rank Fusion of embedding + trigram rankings
|
|
137
|
+
results = graph.query_sync("credit freeze", search_mode="hybrid")
|
|
138
|
+
|
|
139
|
+
# Tune the RRF smoothing constant (default 60, lower = more weight to top ranks)
|
|
140
|
+
results = graph.query_sync("credit freeze", search_mode="hybrid", rrf_k=30)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Entity and Causal Extraction
|
|
144
|
+
|
|
145
|
+
When `gliner2` is installed, `add_text()` / `add_texts()` automatically use GLiNER2 for both entity extraction and causal relation detection. Without `gliner2`, it falls back to BERT NER (entities only).
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from reasongraph import ReasonGraph, NERExtractor, GLiNER2Extractor
|
|
149
|
+
|
|
150
|
+
graph = ReasonGraph()
|
|
151
|
+
graph.initialize_sync()
|
|
152
|
+
|
|
153
|
+
# Default: GLiNER2 (entities + causal relations) if installed, else BERT NER
|
|
154
|
+
entities = graph.add_text_sync("Apple released the iPhone in 2007.")
|
|
155
|
+
print(entities) # ['Apple', 'iPhone']
|
|
156
|
+
|
|
157
|
+
# Explicit: force BERT NER even if GLiNER2 is installed
|
|
158
|
+
entities = graph.add_text_sync("Apple released the iPhone in 2007.", extractor=NERExtractor())
|
|
159
|
+
|
|
160
|
+
# Explicit: GLiNER2 with custom entity types
|
|
161
|
+
gliner = GLiNER2Extractor(entity_types=["company", "product", "date"])
|
|
162
|
+
entities = graph.add_text_sync("Apple released the iPhone in 2007.", extractor=gliner)
|
|
163
|
+
|
|
164
|
+
# Any callable works
|
|
165
|
+
entities = graph.add_text_sync("some text", extractor=lambda t: ["custom"])
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## PostgreSQL Backend
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from reasongraph import ReasonGraph
|
|
172
|
+
from reasongraph.backends import PostgresBackend
|
|
173
|
+
|
|
174
|
+
graph = ReasonGraph(backend=PostgresBackend(database_url="postgresql://user:pass@localhost/db"))
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Requires `pip install reasongraph[postgres]` and the `pgvector` + `pg_trgm` extensions enabled on your database.
|
|
178
|
+
|
|
179
|
+
## Evaluation: Mixed-Domain Reasoning
|
|
180
|
+
|
|
181
|
+
We evaluate reasoning quality by loading all 6 built-in datasets into a single graph (~130 text nodes, ~104 entity nodes, ~280 edges) and testing whether the library can trace the correct causal chains, syllogistic proofs, taxonomic hierarchies, and data analysis patterns -- without being distracted by unrelated facts from other domains.
|
|
182
|
+
|
|
183
|
+
32 test cases simulate agent-style queries like *"I need to understand what caused the 2008 financial crisis"*, *"How does insulin resistance lead to kidney failure?"*, or *"I have two numeric columns, check if related"* and check whether the returned reasoning chain matches the expected ground truth.
|
|
184
|
+
|
|
185
|
+
**Per-domain results (hybrid search, `top_k=5`, `hops=4`, `rerank_top_k=4`):**
|
|
186
|
+
|
|
187
|
+
| Domain | Cases | Chain Completeness | Recall@5 | Precision@5 | Domain Accuracy |
|
|
188
|
+
|--------|------:|--------------------|----------|-------------|-----------------|
|
|
189
|
+
| Causal | 5 | 100% | 100% | 92% | 100% |
|
|
190
|
+
| Financial | 6 | 100% | 82% | 60% | 100% |
|
|
191
|
+
| Medical | 5 | 100% | 92% | 76% | 92% |
|
|
192
|
+
| Syllogisms | 5 | 100% | 100% | 92% | 85% |
|
|
193
|
+
| Taxonomy | 3 | 100% | 83% | 53% | 92% |
|
|
194
|
+
| Analysis Patterns | 8 | 96% | 75% | 45% | 96% |
|
|
195
|
+
| **Overall** | **32** | **99%** | **88%** | **68%** | **95%** |
|
|
196
|
+
|
|
197
|
+
32/32 cases pass (>= 50% chain completeness). Split reranking gives chain continuations (text-to-text edges) priority over bridge discoveries (entity-to-text edges), keeping traversal focused.
|
|
198
|
+
|
|
199
|
+
**Search mode comparison:**
|
|
200
|
+
|
|
201
|
+
| Mode | Chain Completeness | Recall@5 | Precision@5 | Domain Accuracy |
|
|
202
|
+
|------|-------------------|----------|-------------|-----------------|
|
|
203
|
+
| Embedding | 99% | 88% | 68% | 95% |
|
|
204
|
+
| Keyword | 0% | 0% | 0% | 0% |
|
|
205
|
+
| Hybrid | 99% | 88% | 68% | 95% |
|
|
206
|
+
|
|
207
|
+
Keyword-only mode scores 0% because the eval queries are natural language questions that don't substring-match the dataset's declarative statements. This is expected -- keyword search is designed for known-term lookups, not question answering.
|
|
208
|
+
|
|
209
|
+
Reproduce: `uv run python tests/eval_financial_reasoning.py`
|
|
210
|
+
|
|
211
|
+
## API Reference
|
|
212
|
+
|
|
213
|
+
### `ReasonGraph(backend=None, embed_model=None, rerank_model=None, forget_after=30)`
|
|
214
|
+
|
|
215
|
+
| Method | Description |
|
|
216
|
+
|--------|-------------|
|
|
217
|
+
| `add_nodes(nodes)` | Add `(content, type)` tuples to the graph |
|
|
218
|
+
| `add_edges(edges)` | Add `(from, to)` content edges |
|
|
219
|
+
| `add_text(text, extractor=None)` | Add text with automatic entity extraction |
|
|
220
|
+
| `add_texts(texts, extractor=None, causal_extractor=None)` | Batch add with entity + causal extraction (auto-enabled with GLiNER2) |
|
|
221
|
+
| `query(query, top_k=5, hops=4, rerank_top_k=4, search_mode="embedding", rrf_k=60)` | Search and traverse the graph |
|
|
222
|
+
| `load_dataset(name)` | Load a built-in dataset |
|
|
223
|
+
| `delete_stale()` | Remove nodes not accessed within `forget_after` days |
|
|
224
|
+
| `get_all_nodes()` / `get_all_edges()` | Inspect graph contents |
|
|
225
|
+
|
|
226
|
+
All methods are async. Sync variants are available with a `_sync` suffix (e.g. `query_sync`).
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# reasongraph
|
|
2
|
+
|
|
3
|
+
A graph-based reasoning library with embedding search, multi-hop traversal, and automatic entity/causal extraction.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/reasongraph/)
|
|
6
|
+
[](https://pypi.org/project/reasongraph/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install reasongraph[all] # everything included
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install only what you need:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install reasongraph # core: SQLite backend, NER extraction, embeddings
|
|
19
|
+
pip install reasongraph[gliner2] # + GLiNER2 entity + causal extraction (recommended)
|
|
20
|
+
pip install reasongraph[postgres] # + PostgreSQL + pgvector backend
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from reasongraph import ReasonGraph
|
|
27
|
+
|
|
28
|
+
graph = ReasonGraph()
|
|
29
|
+
graph.initialize_sync()
|
|
30
|
+
|
|
31
|
+
# Add text with automatic entity + causal extraction
|
|
32
|
+
graph.add_text_sync("Lehman Brothers filed for bankruptcy in September 2008.")
|
|
33
|
+
graph.add_text_sync("The Federal Reserve cut interest rates to near zero.")
|
|
34
|
+
|
|
35
|
+
# Query with embedding search + multi-hop graph traversal
|
|
36
|
+
results = graph.query_sync("What caused the 2008 financial crisis?")
|
|
37
|
+
for text in results:
|
|
38
|
+
print(text)
|
|
39
|
+
|
|
40
|
+
graph.close_sync()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or use the async API with a context manager:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import asyncio
|
|
47
|
+
from reasongraph import ReasonGraph
|
|
48
|
+
|
|
49
|
+
async def main():
|
|
50
|
+
async with ReasonGraph() as graph:
|
|
51
|
+
await graph.load_dataset("financial")
|
|
52
|
+
results = await graph.query("What caused the 2008 crisis?")
|
|
53
|
+
for text in results:
|
|
54
|
+
print(text)
|
|
55
|
+
|
|
56
|
+
asyncio.run(main())
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
- **Automatic extraction** -- GLiNER2 extracts entities and causal relations in one pass (falls back to BERT NER when gliner2 is not installed)
|
|
62
|
+
- **Hybrid search** -- combine embedding similarity, keyword (trigram) matching, or both
|
|
63
|
+
- **Multi-hop traversal** -- follow graph edges to discover connected reasoning chains
|
|
64
|
+
- **Cross-encoder reranking** -- rerank results at each hop with `ms-marco-MiniLM-L-6-v2`
|
|
65
|
+
- **Built-in datasets** -- load curated reasoning graphs for immediate use
|
|
66
|
+
- **Async-first** -- native async API with sync convenience wrappers
|
|
67
|
+
- **Pluggable backends** -- SQLite (zero-config default) or PostgreSQL with pgvector
|
|
68
|
+
|
|
69
|
+
## Built-in Datasets
|
|
70
|
+
|
|
71
|
+
| Dataset | Description |
|
|
72
|
+
|---------|-------------|
|
|
73
|
+
| `syllogisms` | Classical syllogistic reasoning chains |
|
|
74
|
+
| `causal` | Cause-effect reasoning with entity annotations |
|
|
75
|
+
| `taxonomy` | Hierarchical concept taxonomy |
|
|
76
|
+
| `financial` | Financial crisis causal chains (2008 crisis, dot-com, inflation, eurozone) |
|
|
77
|
+
| `medical` | Medical causal chains (heart disease, diabetes, infectious disease, cancer) |
|
|
78
|
+
| `analysis_patterns` | Data analysis reasoning: scenario detection, technique selection, implementation patterns |
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
graph.load_dataset_sync("financial")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Search Modes
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# Pure embedding similarity (default)
|
|
88
|
+
results = graph.query_sync("credit freeze", search_mode="embedding")
|
|
89
|
+
|
|
90
|
+
# Pure keyword/trigram matching
|
|
91
|
+
results = graph.query_sync("credit freeze", search_mode="keyword")
|
|
92
|
+
|
|
93
|
+
# Hybrid: Reciprocal Rank Fusion of embedding + trigram rankings
|
|
94
|
+
results = graph.query_sync("credit freeze", search_mode="hybrid")
|
|
95
|
+
|
|
96
|
+
# Tune the RRF smoothing constant (default 60, lower = more weight to top ranks)
|
|
97
|
+
results = graph.query_sync("credit freeze", search_mode="hybrid", rrf_k=30)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Entity and Causal Extraction
|
|
101
|
+
|
|
102
|
+
When `gliner2` is installed, `add_text()` / `add_texts()` automatically use GLiNER2 for both entity extraction and causal relation detection. Without `gliner2`, it falls back to BERT NER (entities only).
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from reasongraph import ReasonGraph, NERExtractor, GLiNER2Extractor
|
|
106
|
+
|
|
107
|
+
graph = ReasonGraph()
|
|
108
|
+
graph.initialize_sync()
|
|
109
|
+
|
|
110
|
+
# Default: GLiNER2 (entities + causal relations) if installed, else BERT NER
|
|
111
|
+
entities = graph.add_text_sync("Apple released the iPhone in 2007.")
|
|
112
|
+
print(entities) # ['Apple', 'iPhone']
|
|
113
|
+
|
|
114
|
+
# Explicit: force BERT NER even if GLiNER2 is installed
|
|
115
|
+
entities = graph.add_text_sync("Apple released the iPhone in 2007.", extractor=NERExtractor())
|
|
116
|
+
|
|
117
|
+
# Explicit: GLiNER2 with custom entity types
|
|
118
|
+
gliner = GLiNER2Extractor(entity_types=["company", "product", "date"])
|
|
119
|
+
entities = graph.add_text_sync("Apple released the iPhone in 2007.", extractor=gliner)
|
|
120
|
+
|
|
121
|
+
# Any callable works
|
|
122
|
+
entities = graph.add_text_sync("some text", extractor=lambda t: ["custom"])
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## PostgreSQL Backend
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from reasongraph import ReasonGraph
|
|
129
|
+
from reasongraph.backends import PostgresBackend
|
|
130
|
+
|
|
131
|
+
graph = ReasonGraph(backend=PostgresBackend(database_url="postgresql://user:pass@localhost/db"))
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Requires `pip install reasongraph[postgres]` and the `pgvector` + `pg_trgm` extensions enabled on your database.
|
|
135
|
+
|
|
136
|
+
## Evaluation: Mixed-Domain Reasoning
|
|
137
|
+
|
|
138
|
+
We evaluate reasoning quality by loading all 6 built-in datasets into a single graph (~130 text nodes, ~104 entity nodes, ~280 edges) and testing whether the library can trace the correct causal chains, syllogistic proofs, taxonomic hierarchies, and data analysis patterns -- without being distracted by unrelated facts from other domains.
|
|
139
|
+
|
|
140
|
+
32 test cases simulate agent-style queries like *"I need to understand what caused the 2008 financial crisis"*, *"How does insulin resistance lead to kidney failure?"*, or *"I have two numeric columns, check if related"* and check whether the returned reasoning chain matches the expected ground truth.
|
|
141
|
+
|
|
142
|
+
**Per-domain results (hybrid search, `top_k=5`, `hops=4`, `rerank_top_k=4`):**
|
|
143
|
+
|
|
144
|
+
| Domain | Cases | Chain Completeness | Recall@5 | Precision@5 | Domain Accuracy |
|
|
145
|
+
|--------|------:|--------------------|----------|-------------|-----------------|
|
|
146
|
+
| Causal | 5 | 100% | 100% | 92% | 100% |
|
|
147
|
+
| Financial | 6 | 100% | 82% | 60% | 100% |
|
|
148
|
+
| Medical | 5 | 100% | 92% | 76% | 92% |
|
|
149
|
+
| Syllogisms | 5 | 100% | 100% | 92% | 85% |
|
|
150
|
+
| Taxonomy | 3 | 100% | 83% | 53% | 92% |
|
|
151
|
+
| Analysis Patterns | 8 | 96% | 75% | 45% | 96% |
|
|
152
|
+
| **Overall** | **32** | **99%** | **88%** | **68%** | **95%** |
|
|
153
|
+
|
|
154
|
+
32/32 cases pass (>= 50% chain completeness). Split reranking gives chain continuations (text-to-text edges) priority over bridge discoveries (entity-to-text edges), keeping traversal focused.
|
|
155
|
+
|
|
156
|
+
**Search mode comparison:**
|
|
157
|
+
|
|
158
|
+
| Mode | Chain Completeness | Recall@5 | Precision@5 | Domain Accuracy |
|
|
159
|
+
|------|-------------------|----------|-------------|-----------------|
|
|
160
|
+
| Embedding | 99% | 88% | 68% | 95% |
|
|
161
|
+
| Keyword | 0% | 0% | 0% | 0% |
|
|
162
|
+
| Hybrid | 99% | 88% | 68% | 95% |
|
|
163
|
+
|
|
164
|
+
Keyword-only mode scores 0% because the eval queries are natural language questions that don't substring-match the dataset's declarative statements. This is expected -- keyword search is designed for known-term lookups, not question answering.
|
|
165
|
+
|
|
166
|
+
Reproduce: `uv run python tests/eval_financial_reasoning.py`
|
|
167
|
+
|
|
168
|
+
## API Reference
|
|
169
|
+
|
|
170
|
+
### `ReasonGraph(backend=None, embed_model=None, rerank_model=None, forget_after=30)`
|
|
171
|
+
|
|
172
|
+
| Method | Description |
|
|
173
|
+
|--------|-------------|
|
|
174
|
+
| `add_nodes(nodes)` | Add `(content, type)` tuples to the graph |
|
|
175
|
+
| `add_edges(edges)` | Add `(from, to)` content edges |
|
|
176
|
+
| `add_text(text, extractor=None)` | Add text with automatic entity extraction |
|
|
177
|
+
| `add_texts(texts, extractor=None, causal_extractor=None)` | Batch add with entity + causal extraction (auto-enabled with GLiNER2) |
|
|
178
|
+
| `query(query, top_k=5, hops=4, rerank_top_k=4, search_mode="embedding", rrf_k=60)` | Search and traverse the graph |
|
|
179
|
+
| `load_dataset(name)` | Load a built-in dataset |
|
|
180
|
+
| `delete_stale()` | Remove nodes not accessed within `forget_after` days |
|
|
181
|
+
| `get_all_nodes()` / `get_all_edges()` | Inspect graph contents |
|
|
182
|
+
|
|
183
|
+
All methods are async. Sync variants are available with a `_sync` suffix (e.g. `query_sync`).
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Financial crisis analysis with reasongraph.
|
|
2
|
+
|
|
3
|
+
This demo loads the built-in financial dataset and runs queries that
|
|
4
|
+
an analyst or agent might ask when researching economic crises.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import textwrap
|
|
8
|
+
from reasongraph import ReasonGraph
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def print_results(question: str, results: list[str]) -> None:
|
|
12
|
+
print(f"\n{'=' * 70}")
|
|
13
|
+
print(f"Q: {question}")
|
|
14
|
+
print(f"{'=' * 70}")
|
|
15
|
+
for i, text in enumerate(results, 1):
|
|
16
|
+
wrapped = textwrap.fill(text, width=66, initial_indent=" ", subsequent_indent=" ")
|
|
17
|
+
print(f" [{i}] {text}")
|
|
18
|
+
print()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main():
|
|
22
|
+
graph = ReasonGraph()
|
|
23
|
+
graph.initialize_sync()
|
|
24
|
+
graph.load_dataset_sync("financial")
|
|
25
|
+
|
|
26
|
+
# An analyst starts by asking about the 2008 crisis.
|
|
27
|
+
results = graph.query_sync(
|
|
28
|
+
"What caused the 2008 financial crisis?",
|
|
29
|
+
search_mode="hybrid",
|
|
30
|
+
top_k=5,
|
|
31
|
+
hops=2,
|
|
32
|
+
)
|
|
33
|
+
print_results("What caused the 2008 financial crisis?", results)
|
|
34
|
+
|
|
35
|
+
# They want to understand how inflation spiraled in 2021-2022.
|
|
36
|
+
results = graph.query_sync(
|
|
37
|
+
"How did inflation surge after the pandemic?",
|
|
38
|
+
search_mode="hybrid",
|
|
39
|
+
top_k=5,
|
|
40
|
+
hops=2,
|
|
41
|
+
)
|
|
42
|
+
print_results("How did inflation surge after the pandemic?", results)
|
|
43
|
+
|
|
44
|
+
# Next: how does the Fed respond to crises?
|
|
45
|
+
results = graph.query_sync(
|
|
46
|
+
"What did the Federal Reserve do about interest rates?",
|
|
47
|
+
search_mode="hybrid",
|
|
48
|
+
top_k=5,
|
|
49
|
+
hops=2,
|
|
50
|
+
)
|
|
51
|
+
print_results("What did the Federal Reserve do about interest rates?", results)
|
|
52
|
+
|
|
53
|
+
# The dot-com bubble -- is there a pattern?
|
|
54
|
+
results = graph.query_sync(
|
|
55
|
+
"What happened during the dot-com bubble?",
|
|
56
|
+
search_mode="hybrid",
|
|
57
|
+
top_k=5,
|
|
58
|
+
hops=2,
|
|
59
|
+
)
|
|
60
|
+
print_results("What happened during the dot-com bubble?", results)
|
|
61
|
+
|
|
62
|
+
# European sovereign debt -- contagion across borders.
|
|
63
|
+
results = graph.query_sync(
|
|
64
|
+
"How did the Greek debt crisis spread to other countries?",
|
|
65
|
+
search_mode="hybrid",
|
|
66
|
+
top_k=3,
|
|
67
|
+
hops=3,
|
|
68
|
+
)
|
|
69
|
+
print_results("How did the Greek debt crisis spread to other countries?", results)
|
|
70
|
+
|
|
71
|
+
graph.close_sync()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "reasongraph"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "A graph-based reasoning library with embedding search and multi-hop traversal"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Berk"},
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"knowledge-graph", "reasoning", "rag", "embeddings", "nlp",
|
|
17
|
+
"semantic-search", "causal-reasoning", "multi-hop", "ner", "agent",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"sentence-transformers>=2.2.0",
|
|
30
|
+
"aiosqlite>=0.19.0",
|
|
31
|
+
"sqlite-vec>=0.1.6",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/bgokden/reasongraph"
|
|
36
|
+
Repository = "https://github.com/bgokden/reasongraph"
|
|
37
|
+
Issues = "https://github.com/bgokden/reasongraph/issues"
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
gliner2 = [
|
|
41
|
+
"gliner2>=1.2.0",
|
|
42
|
+
"urllib3>=2.0.0",
|
|
43
|
+
"requests>=2.28.0",
|
|
44
|
+
]
|
|
45
|
+
postgres = [
|
|
46
|
+
"psycopg[binary]>=3.1.0",
|
|
47
|
+
"psycopg-pool>=3.1.0",
|
|
48
|
+
"pgvector>=0.2.0",
|
|
49
|
+
]
|
|
50
|
+
all = [
|
|
51
|
+
"gliner2>=1.2.0",
|
|
52
|
+
"urllib3>=2.0.0",
|
|
53
|
+
"requests>=2.28.0",
|
|
54
|
+
"psycopg[binary]>=3.1.0",
|
|
55
|
+
"psycopg-pool>=3.1.0",
|
|
56
|
+
"pgvector>=0.2.0",
|
|
57
|
+
]
|
|
58
|
+
dev = [
|
|
59
|
+
"pytest>=7.0.0",
|
|
60
|
+
"pytest-asyncio>=0.21.0",
|
|
61
|
+
"ruff>=0.1.0",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.hatch.build.targets.wheel]
|
|
65
|
+
packages = ["src/reasongraph"]
|
|
66
|
+
|
|
67
|
+
[tool.pytest.ini_options]
|
|
68
|
+
asyncio_mode = "auto"
|
|
69
|
+
testpaths = ["tests"]
|
|
70
|
+
|
|
71
|
+
[tool.ruff]
|
|
72
|
+
target-version = "py311"
|
|
73
|
+
line-length = 100
|