File size: 12,935 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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
"""
FALSIFY demo corpus — the "Company X recall" investigation.

This module builds the Session-1 belief graph that the demo revises in Session 2.
It is intentionally small and hand-authored so the belief-revision mechanics are
legible on screen in under 30 seconds, and so the cascade result is deterministic
(the winning demo must not depend on a flaky LLM).

The investigation
-----------------
Question Q:  "Did Company X know about the defect before the recall?"

Competing hypotheses:
    A — "X knew via the QA report dated March 2021"      (supported by E_qa)
    B — "X knew via a supplier email in January 2021"    (supported by E_email)
    C — "X did not know before the recall"               (unsupported)

Evidence:
    E_qa    — the March-2021 QA report       (supports A)
    E_email — the January-2021 supplier email (supports B)

Conclusion:
    K — "Company X knew about the defect by March 2021"
        depends_on E_qa  (critical=True)   <-- the propagation rail

The Session-2 fact (NEW_FACT) is a forensic finding that the March QA report was
back-dated. It contradicts E_qa. Refuting E_qa must cascade:

    E_qa -> refuted
      -> K (depends_on E_qa, critical) -> invalidated
      -> A (only supporter E_qa now dead) -> superseded ; B promoted (new frontier)
      -> K orphaned (no alive consumer) -> forgotten (hard-deleted, graph + vector)
      -> E_qa kept as refuted provenance (it is NEW_FACT's supersedes anchor)

The scoreboard then shows FALSIFY answering via B (Jan 2021) while a plain vector
(RAG) baseline still cites the refuted March QA report.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from typing import Dict, List

# NOTE: importing falsify.models runs falsify/__init__, which sets the Cognee env
# defaults (access-control off, session cache on) before Cognee is imported.
from falsify import graph_ops
from falsify.edges import DEPENDS_ON, SUPPORTS
from falsify.models import (
    Conclusion,
    Evidence,
    Hypothesis,
    InvestigationQuestion,
)

logger = logging.getLogger("falsify.seed")

# The research question this whole graph hangs off of.
QUESTION_TEXT = "Did Company X know about the defect before the recall?"

# The Session-2 fact that triggers belief revision. It contradicts E_qa.
NEW_FACT = (
    "A forensic audit found that the March 2021 QA report was back-dated: it was "
    "actually created shortly after the product recall, not before it."
)

# The SECOND contradiction, used only by the diamond scenario. It contradicts
# E_email. Dropped after NEW_FACT to demonstrate iterative revision: once BOTH
# legs of a multi-source conclusion are refuted, the conclusion finally collapses.
NEW_FACT_2 = (
    "A forensic analysis of email headers shows the January 2021 supplier email was "
    "fabricated: the sender domain was not registered until April 2021, and the DKIM "
    "signature is invalid."
)

# Human-readable stable keys -> used by --demo mode to pin the refutation target
# deterministically (so the cascade runs on real graph APIs even if the LLM judge
# is unavailable). These map to the ``key`` field on the seeded nodes below.
REFUTED_EVIDENCE_KEY = "E_qa"
REFUTED_EVIDENCE_KEY_2 = "E_email"  # second-phase target for the diamond scenario


@dataclass
class SeededGraph:
    """Handles to the nodes created by :func:`build_investigation`.

    ``ids`` maps a stable human key (e.g. ``"E_qa"``, ``"A"``, ``"K"``) to the
    string node id in the graph, so the demo/tests can reference specific nodes
    without re-querying. ``labels`` maps the same keys to display strings.
    """

    question_id: str
    ids: Dict[str, str] = field(default_factory=dict)
    labels: Dict[str, str] = field(default_factory=dict)

    @property
    def refuted_target_id(self) -> str:
        """Node id of the evidence the Session-2 fact contradicts (E_qa)."""
        return self.ids[REFUTED_EVIDENCE_KEY]


async def build_investigation() -> SeededGraph:
    """Create and persist the Session-1 belief graph. Returns a :class:`SeededGraph`.

    The caller is responsible for starting from a clean state (e.g. via
    ``cognee.forget(everything=True)`` and ``cognee.low_level.setup()``). This
    function only builds; it does not prune.

    Nodes are persisted with Cognee's ``add_data_points`` (writing both the graph
    node and the vector row for each ``Embeddable`` field). Typed edges with
    properties are then added explicitly through the graph engine so we control the
    exact relationship names and edge properties (``critical`` / ``weight``).
    """
    # Import here so the module import stays cheap and env defaults are already set.
    from cognee.tasks.storage import add_data_points

    # ------------------------------------------------------------------ nodes
    question = InvestigationQuestion(question=QUESTION_TEXT, source_id="investigation")

    hyp_a = Hypothesis(
        statement="Company X knew via the QA report dated March 2021.",
        question_id=str(question.id),
        prior=0.5,
        confidence=0.6,
        source_id="analyst",
    )
    hyp_b = Hypothesis(
        statement="Company X knew via a supplier email in January 2021.",
        question_id=str(question.id),
        prior=0.5,
        confidence=0.55,
        source_id="analyst",
    )
    hyp_c = Hypothesis(
        statement="Company X did not know about the defect before the recall.",
        question_id=str(question.id),
        prior=0.5,
        confidence=0.4,
        source_id="analyst",
    )

    ev_qa = Evidence(
        claim="A QA report dated March 2021 documented the defect internally.",
        source_id="qa_report_2021_03",
        quote="Internal QA report, dated 2021-03-15, flags the defect.",
        stance="supports",
        asserted_at="2021-03-15",
        confidence=0.8,
    )
    ev_email = Evidence(
        claim="A supplier email in January 2021 warned Company X about the defect.",
        source_id="supplier_email_2021_01",
        quote="Supplier email, 2021-01-20: 'we have observed the defect in test units.'",
        stance="supports",
        asserted_at="2021-01-20",
        confidence=0.7,
    )

    conclusion_k = Conclusion(
        statement="Company X knew about the defect by March 2021.",
        confidence=0.8,
        depends_on_ids=[str(ev_qa.id)],
        source_id="analyst",
    )

    nodes: List = [question, hyp_a, hyp_b, hyp_c, ev_qa, ev_email, conclusion_k]

    logger.info("Persisting %d belief nodes via add_data_points", len(nodes))
    await add_data_points(nodes)

    # ------------------------------------------------------------------ edges
    # Evidence -> Hypothesis (supports, weighted)
    await graph_ops.add_edge(str(ev_qa.id), str(hyp_a.id), SUPPORTS, {"weight": 0.8})
    await graph_ops.add_edge(str(ev_email.id), str(hyp_b.id), SUPPORTS, {"weight": 0.7})

    # Conclusion -> Evidence (depends_on, critical) — THE propagation rail
    await graph_ops.add_edge(
        str(conclusion_k.id), str(ev_qa.id), DEPENDS_ON, {"critical": True}
    )

    # Hypothesis -> Question (answers) — keeps the graph connected for visualization
    for hyp in (hyp_a, hyp_b, hyp_c):
        await graph_ops.add_edge(str(hyp.id), str(question.id), "answers", {})

    seeded = SeededGraph(
        question_id=str(question.id),
        ids={
            "Q": str(question.id),
            "A": str(hyp_a.id),
            "B": str(hyp_b.id),
            "C": str(hyp_c.id),
            "E_qa": str(ev_qa.id),
            "E_email": str(ev_email.id),
            "K": str(conclusion_k.id),
        },
        labels={
            "Q": QUESTION_TEXT,
            "A": hyp_a.statement,
            "B": hyp_b.statement,
            "C": hyp_c.statement,
            "E_qa": ev_qa.claim,
            "E_email": ev_email.claim,
            "K": conclusion_k.statement,
        },
    )
    logger.info("Seeded investigation graph: %s", seeded.ids)
    return seeded


async def build_diamond_investigation() -> SeededGraph:
    """Build the Session-1 graph WITH a diamond dependency (Conclusion K2).

    This is the same investigation as :func:`build_investigation`, plus one extra
    Conclusion K2 that *critically depends on BOTH* E_qa and E_email. It exists to
    demonstrate that FALSIFY's propagation is a grounded least-fixpoint, not a naive
    cascade: a conclusion with two critical supports survives losing one of them, and
    only collapses when the LAST support dies.

    Two-phase story the demo drives on top of this graph:

        Phase 1 — refute E_qa (NEW_FACT):
            E_qa -> refuted ; K (single dep) -> invalidated -> forgotten
            K2 -> STILL ALIVE (E_email keeps it grounded) ; A superseded, B promoted
            E_qa -> retained (K2 alive still depends on it, and it anchors NEW_FACT)

        Phase 2 — refute E_email (NEW_FACT_2):
            E_email -> refuted ; K2 (last dep now dead) -> invalidated -> forgotten
            B -> superseded ; only C ("did not know") may remain

    The base nodes are re-created here (rather than shared with build_investigation)
    so the proven single-contradiction demo path stays untouched.
    """
    from cognee.tasks.storage import add_data_points

    # ------------------------------------------------------------------ nodes
    question = InvestigationQuestion(question=QUESTION_TEXT, source_id="investigation")

    hyp_a = Hypothesis(
        statement="Company X knew via the QA report dated March 2021.",
        question_id=str(question.id), prior=0.5, confidence=0.6, source_id="analyst",
    )
    hyp_b = Hypothesis(
        statement="Company X knew via a supplier email in January 2021.",
        question_id=str(question.id), prior=0.5, confidence=0.55, source_id="analyst",
    )
    hyp_c = Hypothesis(
        statement="Company X did not know about the defect before the recall.",
        question_id=str(question.id), prior=0.5, confidence=0.4, source_id="analyst",
    )

    ev_qa = Evidence(
        claim="A QA report dated March 2021 documented the defect internally.",
        source_id="qa_report_2021_03",
        quote="Internal QA report, dated 2021-03-15, flags the defect.",
        stance="supports", asserted_at="2021-03-15", confidence=0.8,
    )
    ev_email = Evidence(
        claim="A supplier email in January 2021 warned Company X about the defect.",
        source_id="supplier_email_2021_01",
        quote="Supplier email, 2021-01-20: 'we have observed the defect in test units.'",
        stance="supports", asserted_at="2021-01-20", confidence=0.7,
    )

    conclusion_k = Conclusion(
        statement="Company X knew about the defect by March 2021.",
        confidence=0.8, depends_on_ids=[str(ev_qa.id)], source_id="analyst",
    )
    # THE DIAMOND: K2 rests on two independent critical supports.
    conclusion_k2 = Conclusion(
        statement="Multiple independent sources confirm Company X had pre-recall "
                  "knowledge of the defect.",
        confidence=0.85,
        depends_on_ids=[str(ev_qa.id), str(ev_email.id)],
        source_id="analyst",
    )

    nodes: List = [question, hyp_a, hyp_b, hyp_c, ev_qa, ev_email, conclusion_k, conclusion_k2]

    logger.info("Persisting %d belief nodes (diamond) via add_data_points", len(nodes))
    await add_data_points(nodes)

    # ------------------------------------------------------------------ edges
    await graph_ops.add_edge(str(ev_qa.id), str(hyp_a.id), SUPPORTS, {"weight": 0.8})
    await graph_ops.add_edge(str(ev_email.id), str(hyp_b.id), SUPPORTS, {"weight": 0.7})

    # K depends on E_qa only (single-leg — dies in phase 1).
    await graph_ops.add_edge(
        str(conclusion_k.id), str(ev_qa.id), DEPENDS_ON, {"critical": True}
    )
    # K2's two critical legs — the diamond. Survives phase 1, collapses in phase 2.
    await graph_ops.add_edge(
        str(conclusion_k2.id), str(ev_qa.id), DEPENDS_ON, {"critical": True}
    )
    await graph_ops.add_edge(
        str(conclusion_k2.id), str(ev_email.id), DEPENDS_ON, {"critical": True}
    )

    for hyp in (hyp_a, hyp_b, hyp_c):
        await graph_ops.add_edge(str(hyp.id), str(question.id), "answers", {})

    seeded = SeededGraph(
        question_id=str(question.id),
        ids={
            "Q": str(question.id),
            "A": str(hyp_a.id), "B": str(hyp_b.id), "C": str(hyp_c.id),
            "E_qa": str(ev_qa.id), "E_email": str(ev_email.id),
            "K": str(conclusion_k.id), "K2": str(conclusion_k2.id),
        },
        labels={
            "Q": QUESTION_TEXT,
            "A": hyp_a.statement, "B": hyp_b.statement, "C": hyp_c.statement,
            "E_qa": ev_qa.claim, "E_email": ev_email.claim,
            "K": conclusion_k.statement, "K2": conclusion_k2.statement,
        },
    )
    logger.info("Seeded diamond investigation graph: %s", seeded.ids)
    return seeded