feat(reasoning): rule-driven actions with provenance (#1096)

* feat(reasoning): rule-driven actions with provenance

Add a structured Action layer so matched rules can trigger side effects
instead of only deriving new facts, turning the reasoner into a
production-rule system.

L1 - Action type system:
- Action base class with execute(bindings, reasoner) + ?var substitution
- AssertAction (optional write-back to KnowledgeGraph), RetractAction,
  CallAction (structured replacement for the unused Rule.handler),
  EmitEventAction (delivers to a registered event sink)
- Rule.actions field; wired into Reasoner.forward_chain() and
  ReteEngine.execute_matches() (via optional bind_reasoner)

L2 - Provenance-aware actions:
- Reasoner records fired actions (rule, bindings, confidence) to
  action_log when provenance is enabled
- Fix dangling import in reasoning_provenance.py (ReasoningEngine ->
  Reasoner, infer -> infer_facts)

Backward compatible: rules using the legacy handler still fire (wrapped
as a CallAction); rules without actions behave exactly as before.

Adds tests/reasoning/test_rule_actions.py (9 tests).

Closes #1095

* fix(reasoning): address qodo review findings on rule actions

- Token-aware variable substitution to avoid ?x/?xy prefix collision
- KnowledgeGraph write-back protocol (explicit API -> canonical translation -> ValueError)
- Structured action_log entries with timestamp
- Decouple action firing from conclusion dedup via per-activation tracking
  (fires known conclusions once; retract-self no longer loops to max_iterations)
- Add Reasoner.infer_with_results preserving confidence; infer_facts delegates
- Forward provenance flag in ReasoningProvenance; drop **kwargs; propagate confidence
- Populate Rete Match.bindings from rule conditions
- Add regression tests for each fix

* fix(reasoning): persist fired action activations

* fix(reasoning): deduplicate Rete action execution

* fix(reasoning): canonicalize action activation identity

* docs(reasoning): explain action replay controls

---------

Co-authored-by: 江俊杰 <jiangjunjie.37@jd.com>
This commit is contained in:
cxzg007
2026-08-27 13:18:07 +05:00
committed by GitHub
co-authored by 江俊杰
parent c9c777993b
commit 5d54919804
7 changed files with 1141 additions and 44 deletions
+13
View File
@@ -150,6 +150,12 @@ HighRiskSupplier(DELTA-3) conf=100% rule=Rule 3
DELTA-3 is flagged even though no document described it that way — the system traced: DELTA-3 supplied GAMMA-7, and GAMMA-7 exploits critical CVEs. For rules that need priority ordering or graded confidence, use the `Rule` dataclass:
If a rule has side-effecting actions, one concrete activation runs those
actions at most once on a Reasoner instance. Re-running `forward_chain()` is
therefore safe: already-attempted actions are not repeated. Use
`reasoner.reset_action_history()` when you intentionally want to replay them;
`reasoner.clear()` and `reasoner.reset()` also clear the history.
```python
# Higher priority rules fire first; confidence propagates into InferenceResult.confidence
reasoner.add_rule(Rule(
@@ -360,6 +366,13 @@ engine.reset()
The rule network is compiled once by `build_network()`. Each subsequent `add_fact()` call propagates incrementally through only the nodes whose conditions it satisfies — not the full rule set — which keeps evaluation cost proportional to the number of new activations rather than the total rule count.
With a Reasoner bound, Rete action side effects are attempted once per rule,
bindings, and matched fact identity. Passing the same match to
`execute_matches()` again still returns the same conclusion, but does not repeat
its actions. Call `engine.reset_action_history()` to replay actions without
clearing working memory. `engine.reset()` and `engine.build_network()` also
clear the action history.
## Step 7 — Temporal interval reasoning
`TemporalReasoningEngine` computes Allen interval relations between time windows, letting you identify whether two events overlap, one contains the other, they meet at a boundary, and so on across your graph:
+19 -2
View File
@@ -127,9 +127,19 @@ conclusions = reasoner.infer_facts(
| `forward_chain()` | `List[InferenceResult]` | Derive all possible conclusions iteratively until fixpoint |
| `backward_chain(goal, max_depth)` | `InferenceResult \| None` | Prove a specific goal string, returns `None` if unprovable |
| `infer_facts(facts, rules)` | `List[str]` | Load facts and rules then run `forward_chain()`, returns conclusion strings |
| `clear()` | `None` | Clear all facts and rules |
| `reset_action_history()` | `None` | Allow actions for previously fired activations to run again |
| `clear()` | `None` | Clear all facts, rules, and action activation history |
| `reset()` | `None` | Alias for `clear()` |
Rules with actions use at-most-once attempt semantics per concrete activation
(rule ID, bindings, and matched facts). Calling `forward_chain()` again on the
same instance does not repeat side effects for an activation that was already
attempted, even when an action raised an exception. Call
`reset_action_history()` to deliberately retry without clearing facts or rules;
`clear()` and `reset()` also clear this history. Replacing a rule's actions in
place does not invalidate an existing activation; reset the history explicitly
when the replacement should be replayed.
### Rule and Fact dataclass fields
```python
@@ -230,9 +240,16 @@ engine.reset()
| `add_fact(fact)` | `None` | Add a `Fact` to working memory and propagate through the network |
| `match_patterns(facts)` | `List[Match]` | Match all patterns; optionally add facts before matching |
| `execute_matches(matches)` | `List[Any]` | Execute matched rules and return their conclusion values |
| `reset()` | `None` | Clear facts and all node activation state |
| `reset_action_history()` | `None` | Allow actions for previously executed activations to run again |
| `reset()` | `None` | Clear facts, node activation state, and action activation history |
| `get_network_stats()` | `dict` | Return counts of alpha, beta, terminal nodes and facts |
When a Reasoner is bound, `execute_matches()` deduplicates action side effects
by rule ID, bindings, and matched fact identity. Re-executing a match still
returns its conclusion for compatibility, but its actions are skipped after the
first attempt. `reset_action_history()`, `reset()`, and `build_network()` allow
those actions to run again.
## SPARQLReasoner