Spaces:
Sleeping
Sleeping
File size: 6,243 Bytes
1605cbb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | """
Surgical forget — prune orphaned dead-ends from graph **and** vector stores.
After :mod:`falsify.tasks.propagate_refutation` marks nodes ``refuted`` /
``invalidated``, some of those nodes are still useful: they explain *why* a belief
changed (provenance) or they still feed a node that is alive. Others are pure
dead-ends with no surviving consumer. FALSIFY hard-deletes only the latter, from
both the graph (``delete_nodes``) and the vector index (``delete_data_points``), so a
subsequent ``recall()`` — and even a raw vector search — can never resurface them.
Orphan rule (REQUIREMENTS §1.5) — a node is FORGOTTEN iff ALL hold:
(a) truth state is ``refuted`` or ``invalidated`` (never ``alive``/``superseded``;
superseded nodes are kept as provenance), AND
(b) no surviving ALIVE node reaches it via ``depends_on`` or ``supports``
(it has no live consumer), AND
(c) it is NOT the target of a ``supersedes`` edge FROM an alive node (such a node
is the provenance anchor of the new truth and must be retained as a tombstone).
The asymmetry is deliberate and is what makes FALSIFY look *surgical*: in the demo the
orphaned Conclusion K is deleted, while the refuted Evidence E_qa is kept — flagged
red — because it is the supersedes-anchor of the new fact.
"""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from typing import Dict, List, Set
from falsify import graph_ops
from falsify.edges import CONSUMER_EDGE_TYPES, SUPERSEDES
from falsify.models import (
Conclusion,
Evidence,
TruthState,
)
logger = logging.getLogger("falsify.forget")
_DELETABLE_STATES = {TruthState.REFUTED.value, TruthState.INVALIDATED.value}
_ALIVE = TruthState.ALIVE.value
# Vector collections FALSIFY writes to (``"{ClassName}_{embeddable_field}"``).
# Deleting an id from a collection it isn't in is a best-effort no-op.
_VECTOR_COLLECTIONS = [
"Evidence_claim",
"Conclusion_statement",
"Hypothesis_statement",
"Assertion_text",
"InvestigationQuestion_question",
]
@dataclass
class ForgetResult:
"""Outcome of a forget pass.
Attributes:
forgotten: node ids hard-deleted from graph + vector.
retained_provenance: refuted/invalidated ids deliberately kept (a supersedes
anchor, or still feeding a live node).
labels: id -> human label for the forgotten nodes (for demo output).
"""
forgotten: List[str] = field(default_factory=list)
retained_provenance: List[str] = field(default_factory=list)
labels: Dict[str, str] = field(default_factory=dict)
async def _alive_consumer_exists(node_id: str, edges, truth: Dict[str, List[str]]) -> bool:
"""True if some ALIVE node reaches ``node_id`` via depends_on/supports.
``depends_on`` (Conclusion->Evidence) and ``supports`` (Evidence->Hypothesis) both
point *from consumer to the thing consumed*, so an incoming edge's **source** is a
consumer of ``node_id``.
"""
nid = str(node_id)
for rel in CONSUMER_EDGE_TYPES:
for src, _props in graph_ops.incoming(nid, edges, rel):
alignment = truth.get(str(src), [_ALIVE])
if _ALIVE in alignment:
return True
return False
def _is_supersedes_anchor(node_id: str, edges, truth: Dict[str, List[str]]) -> bool:
"""True if ``node_id`` is the target of a ``supersedes`` edge from an ALIVE node.
That alive source is the new, current truth; the target is its tombstone and must
be retained as provenance.
"""
nid = str(node_id)
for src, _props in graph_ops.incoming(nid, edges, SUPERSEDES):
alignment = truth.get(str(src), [_ALIVE])
if _ALIVE in alignment:
return True
return False
async def cascade_forget(candidate_ids: List[str]) -> ForgetResult:
"""Delete truly-orphaned dead-ends among ``candidate_ids``; keep provenance.
Args:
candidate_ids: nodes marked refuted/invalidated by a preceding cascade.
Returns:
A :class:`ForgetResult`. Deleted nodes are removed from the graph and from
every FALSIFY vector collection, so no retrieval path can resurface them.
"""
result = ForgetResult()
candidates = [str(c) for c in dict.fromkeys(candidate_ids) if c]
if not candidates:
return result
nodes, edges = await graph_ops.load_graph()
props_by_id = {str(nid): (props or {}) for nid, props in nodes}
# Current truth for candidates + their neighbors (consumers/anchors).
neighbor_ids: Set[str] = set(candidates)
for cid in candidates:
for rel in (*CONSUMER_EDGE_TYPES, SUPERSEDES):
neighbor_ids.update(str(s) for s, _p in graph_ops.incoming(cid, edges, rel))
truth = await graph_ops.get_truth(list(neighbor_ids))
death_set: List[str] = []
for cid in candidates:
alignment = truth.get(cid, [_ALIVE])
# (a) must be refuted/invalidated
if not any(state in _DELETABLE_STATES for state in alignment):
continue
# (c) keep supersedes anchors (provenance tombstones)
if _is_supersedes_anchor(cid, edges, truth):
result.retained_provenance.append(cid)
logger.info("retained %s as supersedes provenance anchor", cid)
continue
# (b) keep nodes that still feed a live consumer
if await _alive_consumer_exists(cid, edges, truth):
result.retained_provenance.append(cid)
logger.info("retained %s (still feeds a live node)", cid)
continue
death_set.append(cid)
result.labels[cid] = graph_ops.node_label(props_by_id.get(cid, {}))
if not death_set:
logger.info("forget pass: nothing orphaned; %d provenance nodes retained",
len(result.retained_provenance))
return result
# Hard-delete from graph + all vector collections in one batch.
deleted = await graph_ops.delete_from_both_stores(death_set, _VECTOR_COLLECTIONS)
result.forgotten = death_set
logger.info(
"forget pass: hard-deleted %d orphan(s) from graph + vector; retained %d provenance",
deleted,
len(result.retained_provenance),
)
return result
|