Skip to content

Commit 58b2a80

Browse files
authored
Merge pull request #141 from lab-v2/peng-task1
Add Task 1 tutorials: loading from file and LLM-generated rules
2 parents 1b4fa53 + 62561e3 commit 58b2a80

9 files changed

Lines changed: 550 additions & 1 deletion

File tree

docs/source/tutorials/index.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ Contents
1818
./annotation_function.rst
1919
./temporal_classifier_tutorial.rst
2020
./cybersecurity_inconsistency.rst
21-
21+
./load_rules_facts_from_file.rst
22+
./llm_generated_rules.rst
2223

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
LLM Generated PyReason Rules
2+
============================
3+
4+
Introduction
5+
------------
6+
In this tutorial, we use a Large Language Model (Claude) to
7+
generate a valid PyReason rule for a simple knowledge graph. We then
8+
validate the generated rule with PyReason's rule parser and run inference
9+
to show it fires on the graph.
10+
11+
.. note::
12+
Find the full, executable code `here <https://github.com/lab-v2/pyreason/blob/main/examples/llm_generated_rules_ex.py>`_
13+
14+
Knowledge Graph
15+
---------------
16+
We build a small academic knowledge graph with three types of nodes - students, majors, and departments.
17+
They are connected by two predicates: ``major_in`` and ``in_department``.
18+
19+
.. code:: python
20+
21+
import networkx as nx
22+
g = nx.DiGraph()
23+
24+
g.add_edge('alice', 'math', major_in=1)
25+
g.add_edge('bob', 'math', major_in=1)
26+
g.add_edge('mary', 'cs', major_in=1)
27+
28+
# Major -> Department
29+
g.add_edge('math', 'math_dept', in_department=1)
30+
g.add_edge('cs', 'cs_dept', in_department=1)
31+
32+
The Prompt
33+
----------
34+
The prompt describes a specific reasoning goal: deriving which department a student belongs to.
35+
The head predicate name is fixed to ensure consistent, comparable output across LLMs.
36+
37+
.. code:: python
38+
39+
PROMPT = """\
40+
You are generating a rule for a PyReason knowledge graph.
41+
42+
### Task
43+
Write a single PyReason rule that derives which department a student belongs to,
44+
given that a student is enrolled in a major and that major belongs to a department.
45+
46+
### Available predicates
47+
- major_in(Student, Major) - student in enrolled in a major
48+
- in_department(Major, Department) - major belongs to a department
49+
50+
### PyReason rule syntax
51+
head_predicate(X,Y) <-N body_predicate_1(X,Z),body_predicates_2(Z,Y)
52+
53+
- N is the delta: use 0 for immediate firing
54+
- Variables are single uppercase letters (X,Y,Z)
55+
- Head predicate name must be: student_in_dept
56+
57+
### Output format
58+
Output the rule on a single line. No explanation, no markdown, no punctuation.
59+
60+
### Example (Different predicates, shows syntax only)
61+
grandparent(X,Y)<-0 parent(X,Z),parent(Z,Y)
62+
"""
63+
64+
Generating the Rule
65+
-------------------
66+
We call the Anthropic API to send the prompt to Claude and split the response into individual rule string.
67+
68+
.. code:: python
69+
70+
import anthropic
71+
72+
client = anthropic.Anthropic()
73+
response = client.messages.create(
74+
model="claude-sonnet-4-20250514",
75+
max_tokens=256,
76+
messages=[{"role": "user", "content": PROMPT}],
77+
)
78+
79+
rule_str = response.content[0].text.strip()
80+
81+
82+
A typical response looks like:
83+
84+
.. code:: text
85+
86+
student_in_dept(X,Y)<-0 major_in(X,Z),in_department(Z,Y)
87+
88+
Validating the Rule
89+
-------------------
90+
The rule is passed through ``pr.Rule()`` to confirm it is syntactically valid
91+
before loading it into the reasoner. If invalid, the script exits immediately.
92+
93+
.. code:: python
94+
95+
import pyreason as pr
96+
97+
try:
98+
pr.Rule(rule_str)
99+
print(f"[VALID] {rule_str}")
100+
except Exception as e:
101+
sys.exit(f"[INVALID] {rule_str}\nError: {e}")
102+
103+
Running inference
104+
-----------------
105+
Load the valid rule into PyReason with ``infer_edges=True`` so that new edges
106+
are created when the rule fires between currently unconnected nodes.
107+
108+
.. code:: python
109+
110+
pr.settings.verbose = False
111+
pr.load_graph(g)
112+
pr.add_rule(pr.Rule(rule_str, name="student_in_dept_rule", infer_edges=True))
113+
114+
interpretation = pr.reason(timesteps=2)
115+
116+
print("\nInferred student-department relationships:")
117+
for df in pr.filter_and_sort_edges(interpretation, ["student_in_dept"]):
118+
if not df.empty:
119+
print(df.to_string(index=False))
120+
121+
Expected output:
122+
123+
.. code:: text
124+
125+
Inferred student-department relationships:
126+
component student_in_dept
127+
0 (alice, math_dept) [1.0, 1.0]
128+
1 (bob, math_dept) [1.0, 1.0]
129+
2 (mary, cs_dept) [1.0, 1.0]
130+
131+
132+
Cross-LLM Consistency
133+
---------------------
134+
The same prompt was tested against Claude, GPT-4, and Gemini through their
135+
web interfaces. All three produced the same valid rule:
136+
137+
.. code:: text
138+
139+
Claude: student_in_dept(X,Y)<-0 major_in(X,Z),in_department(Z,Y)
140+
GPT-4: student_in_dept(X,Y)<-0 major_in(X,Z),in_department(Z,Y)
141+
Gemini: student_in_dept(X,Y)<-0 major_in(X,Z),in_department(Z,Y)
142+
143+
This demonstrates that a well-constrained prompt consistently produces identical,
144+
valid PyReason rules across different LLMs.
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
Load Rules and Facts From File
2+
==================================
3+
4+
Introduction
5+
------------
6+
Loading facts and rules from files is crucial for practical PyReason usage.
7+
It provides scalability for large rule sets, reusability across projects,
8+
and allows non-programmers to edit domain knowledge without touching Python code.
9+
10+
In this tutorial, we will focus on four functions that load facts and rules
11+
from CSV or JSON files: ``add_fact_from_csv``, ``add_fact_from_json``,
12+
``add_rule_from_csv``, and ``add_rule_from_json``.
13+
14+
.. note::
15+
Find the full, executable code `here <https://github.com/lab-v2/pyreason/blob/main/examples/load_rules_facts_from_file/load_rules_facts_from_file_ex.py>`_
16+
17+
Graph
18+
----------------------
19+
Let's build a simple student-major-department knowledge graph. Alice, Bob,
20+
and Mary are students — Alice and Bob enroll in the math major, while Mary
21+
enrolls in CS. Each major belongs to a department: math belongs to the math
22+
department, and CS belongs to the CS department.
23+
24+
The enrollment relationships are defined as graph edges. The ``in_department``
25+
and ``scholarship`` relationships are loaded from external files as facts instead.
26+
27+
.. code:: python
28+
29+
import networkx as nx
30+
31+
g = nx.DiGraph()
32+
g.add_nodes_from(['alice', 'bob', 'mary']) # students
33+
g.add_nodes_from(['math', 'cs']) # majors
34+
g.add_nodes_from(['math_dept', 'cs_dept']) # departments
35+
36+
g.add_edge('alice', 'math', enroll=1)
37+
g.add_edge('bob', 'math', enroll=1)
38+
g.add_edge('mary', 'cs', enroll=1)
39+
40+
41+
Load Rules from CSV
42+
----------------------
43+
Rules can be loaded from a CSV file. Each row has four columns:
44+
``rule_text``, ``name``, ``infer_edges``, ``set_static``.
45+
46+
.. code:: text
47+
48+
rule_text,name,infer_edges,set_static
49+
"under_department(X,Y) <-0 enroll(X,Z), in_department(Z,Y)",under_department_rule,true,false
50+
"eligible(X) <-1 under_department(X,Y), scholarship(Y)",eligible_scholarship_rule,false,false
51+
52+
Note: when the rule text contains a comma, wrap the whole field in quotes.
53+
54+
Then load the file using:
55+
56+
.. code:: python
57+
58+
import pyreason as pr
59+
pr.add_rule_from_csv('examples/rules.csv')
60+
61+
Load Rules from JSON
62+
-----------------------
63+
Rules can also be loaded from a JSON file. The JSON should be array of objects.
64+
Example:
65+
66+
.. code:: text
67+
68+
[
69+
{
70+
"rule_text": "under_department(X,Y) <-0 enroll(X,Z), in_department(Z,Y)",
71+
"name": "under_department_rule",
72+
"infer_edges": true,
73+
"set_static": false
74+
},
75+
{
76+
"rule_text": "eligible(X) <-1 under_department(X,Y), scholarship(Y)",
77+
"name": "eligible_scholarship_rule",
78+
"infer_edges": false,
79+
"set_static": false
80+
}
81+
]
82+
83+
Then load the file using:
84+
85+
.. code:: python
86+
87+
pr.add_rule_from_json('examples/rules.json')
88+
89+
Loading Facts from CSV
90+
----------------------
91+
Facts can be loaded from a CSV file. Each row should have up to 5 comma-separated values in this order: ``fact_text, name, start_time, end_time, static``.
92+
93+
.. code:: text
94+
95+
fact_text,name,start_time,end_time,static
96+
scholarship(math_dept),scholarship_math_dept,0,2,False
97+
"in_department(math,math_dept)",math_in_math_department,0,2,False
98+
"in_department(cs,cs_dept)",cs_in_cs_department,0,2,False
99+
100+
Note: when the fact text contains a comma, wrap the whole field in quotes.
101+
102+
Then load the file using:
103+
104+
.. code:: python
105+
106+
pr.add_fact_from_csv('examples/facts.csv')
107+
108+
Loading Facts from JSON
109+
-----------------------
110+
Facts can also be loaded from a JSON file. The JSON should be an array of objects.
111+
Example:
112+
113+
.. code:: text
114+
115+
[
116+
{
117+
"fact_text": "scholarship(math_dept)",
118+
"name": "scholarship_math_dept",
119+
"start_time": 0,
120+
"end_time": 2,
121+
"static": false
122+
},
123+
124+
{
125+
"fact_text": "in_department(math,math_dept)",
126+
"name": "math_in_math_department",
127+
"start_time": 0,
128+
"end_time": 2,
129+
"static": false
130+
},
131+
132+
{
133+
"fact_text": "in_department(cs,cs_dept)",
134+
"name": "cs_in_cs_department",
135+
"start_time": 0,
136+
"end_time": 2,
137+
"static": false
138+
}
139+
]
140+
141+
Then load the file using:
142+
143+
.. code:: python
144+
145+
pr.add_fact_from_json('examples/facts.json')
146+
147+
148+
Running PyReason
149+
----------------
150+
151+
After loading the graph, rules, and facts using any combination of the
152+
four loading functions above, run the reasoning:
153+
154+
.. code:: python
155+
156+
interpretation = pr.reason(timesteps=2)
157+
dataframes = pr.filter_and_sort_nodes(interpretation, ['eligible'])
158+
for t, df in enumerate(dataframes):
159+
print(f'TIMESTEP - {t}')
160+
print(df)
161+
print()
162+
163+
Expected Output
164+
---------------
165+
.. code::
166+
167+
168+
TIMESTEP - 0
169+
Empty DataFrame
170+
Columns: [component, eligible]
171+
Index: []
172+
173+
TIMESTEP - 1
174+
component eligible
175+
0 alice [1.0, 1.0]
176+
1 bob [1.0, 1.0]
177+
178+
TIMESTEP - 2
179+
component eligible
180+
0 alice [1.0, 1.0]
181+
1 bob [1.0, 1.0]
182+
183+
At timestep 1, ``alice`` and ``bob`` become eligible because they are in
184+
``math_dept`` and ``math_dept`` has a scholarship. ``mary`` is not eligible
185+
because ``cs_dept`` has no scholarship fact.
186+
187+
188+
Further Details
189+
---------------
190+
191+
For a complete description of parameters and advanced features, see the full API reference in `pyreason.py
192+
<https://github.com/lab-v2/pyreason/blob/main/pyreason/pyreason.py#L868>`_.

0 commit comments

Comments
 (0)