Falsify / REQUIREMENTS.md
Aaryan Kumar
deploy to hugging face
1605cbb
|
Raw
History Blame Contribute Delete
24.6 kB

FALSIFY — Requirements & Build Contract

Tagline: Drop one contradicting fact; watch dependent conclusions die and a losing hypothesis rise — permanently, across sessions.

FALSIFY is a belief-revision research copilot. It treats a research inquiry as a living belief graph (not a chat log). When new evidence contradicts an existing evidence node, FALSIFY performs belief revision: it flips the contradicted evidence to refuted, propagates the refutation forward through the dependency chain, marks every dependent conclusion invalid, promotes the best surviving hypothesis, and surgically forgets the orphaned dead-ends. The refutation is written to graph node truth-state so the next session's recall() skips killed branches — while a plain-RAG baseline still cites the stale fact.

This document is the contract for all downstream build agents. Every API named here has been verified against the read-only reference at /workspaces/cognee. Line references are cited where load-bearing.


0. Verified Cognee API Surface (grep-confirmed)

API Exact import / call Location (reference, read-only)
Task from cognee.modules.pipelines.tasks.task import Task (lowercase module task) cognee/modules/pipelines/tasks/task.py
memify import cognee; await cognee.memify(extraction_tasks=[...], enrichment_tasks=[...], data=..., node_type=NodeSet, node_name=[...]) cognee/modules/memify/memify.py:26
remember await cognee.remember(data, dataset_name=..., session_id=...) cognee/api/v1/remember/remember.py:623
recall await cognee.recall(query_text, query_type=..., session_id=..., top_k=...) cognee/api/v1/recall/recall.py:361
improve await cognee.improve(...) cognee/api/v1/improve/improve.py:36
forget await cognee.forget(...) cognee/api/v1/forget/forget.py:16
get_graph_engine from cognee.infrastructure.databases.graph import get_graph_enginege = await get_graph_engine() (async) cognee/infrastructure/databases/graph/get_graph_engine.py:110
get_vector_engine from cognee.infrastructure.databases.vector import get_vector_engineve = get_vector_engine() (sync) cognee/infrastructure/databases/vector/get_vector_engine.py:44
DataPoint, Embeddable, Dedup, LLMContext from cognee.infrastructure.engine import DataPoint, Embeddable, LLMContext, Dedup cognee/infrastructure/engine/models/DataPoint.py:27
LLMGateway from cognee.infrastructure.llm.LLMGateway import LLMGateway cognee/infrastructure/llm/LLMGateway.py:52

Graph engine methods (verified in graph_db_interface.py + ladybug/adapter.py)

ge = await get_graph_engine()

# Forward traversal — THE propagation rail. Returns (nodes, edges).
nodes, edges = await ge.get_neighborhood(node_ids: List[str], depth: int = 1, edge_types: Optional[List[str]] = None)
#   graph_db_interface.py:303 ; ladybug/adapter.py:2068

# Immediate neighbors of ONE node. Returns List[Tuple[source_props, edge_props, target_props]].
conns = await ge.get_connections(node_id: Union[str, UUID])
#   graph_db_interface.py:289 ; ladybug/adapter.py:1859

# Truth-state: the alive/dead flag recall() filters on. PERSISTS across restart.
await ge.set_node_truth_state({node_id: {"truth_alignment": ["refuted"], "truth_epoch": N}})
state = await ge.get_node_truth_state(node_ids: List[str])
#   graph_db_interface.py:357,364 ; ladybug impl 1687,1712 ; writes props "truth_alignment" (list) + "truth_epoch" (int) — adapter.py:1576-1578

# Confidence / health signal.
await ge.set_node_feedback_weights({node_id: 0.9})   # graph_db_interface.py:348
w = await ge.get_node_feedback_weights(node_ids)     # graph_db_interface.py:341

# Edges.
await ge.add_edge(from_node: str, to_node: str, relationship_name: str, edge_properties: Dict = {})  # ladybug/adapter.py:1222

# Surgical delete.
await ge.delete_nodes(node_ids: List[str])           # graph_db_interface.py:102 ; ladybug 1042

Vector engine methods (verified in LanceDBAdapter.py)

ve = get_vector_engine()
hits = await ve.search(collection_name: str, query_text: str = None, limit: int = 15, include_payload: bool = False, ...)
#   LanceDBAdapter.py:992  — each hit exposes `.id`, `.score` (COSINE DISTANCE, lower = closer), `.payload`
await ve.delete_data_points(collection_name: str, data_point_ids: List[UUID])
#   vector_db_interface.py:160 ; LanceDBAdapter.py:1106

LLMGateway (verified LLMGateway.py:59)

# STATIC method — do NOT instantiate. Returns a coroutine; await it.
result = await LLMGateway.acreate_structured_output(
    text_input="A(...): <claim>\nB(...): <claim>",
    system_prompt="skeptical analyst ...",
    response_model=ContradictionJudgement,   # a pydantic BaseModel
)

DataPoint subclass pattern (verified Tool.py)

from typing import Annotated
from cognee.infrastructure.engine import DataPoint, Embeddable, Dedup

class Hypothesis(DataPoint):
    statement: Annotated[str, Embeddable(), Dedup()]
    question_id: str
    # identity_fields in metadata dedups nodes across sessions (see §2.4)
    metadata: dict = {"index_fields": ["statement"], "identity_fields": ["question_id", "statement"]}

NOTE for implementers: The FALSIFY spec screenshot wrote Task(...) imported from pipelines.tasks.Task. The correct verified path is lowercase cognee.modules.pipelines.tasks.task (class name Task). Use that. get_graph_engine is await-ed; get_vector_engine is not.


1. Core Mechanism

1.1 Node & edge vocabulary (see §2 for full model)

  • Nodes: InvestigationQuestion (root), Hypothesis, Evidence, Conclusion.
  • Edges (relationship_name string on add_edge):
    • depends_on : Conclusion → EvidenceTHE forward-propagation rail. A Conclusion depends_on the Evidence it rests on. edge_properties = {"critical": bool}.
    • supports : Evidence → Hypothesis — evidence corroborates a hypothesis. edge_properties = {"weight": float}.
    • refutes : Evidence → Hypothesis — evidence contradicts a hypothesis. edge_properties = {"weight": float}.
    • supersedes : Evidence(new) → Evidence(old) — written when the new fact back-dates / overrides an old evidence node. edge_properties = {"confidence": float}.

1.2 Truth-state lifecycle

truth_alignment is a list written to the node via set_node_truth_state. FALSIFY uses these canonical single-element states:

alive       — default; node participates in recall context
refuted      — Evidence directly contradicted by a newer fact (entry point of a cascade)
superseded   — Evidence/Hypothesis replaced by a newer competing node (still exists, demoted)
invalidated  — Conclusion whose supporting Evidence chain was refuted (forward-cascade victim)
forgotten    — orphaned dead-end scheduled for surgical delete (transient; node then removed)

Transition rules:

alive --(new fact contradicts this Evidence, LLM-confirmed)--> refuted
alive --(a competing Evidence supersedes it)---------------> superseded
Conclusion.alive --(any critical depends_on Evidence is refuted)--> invalidated
{refuted|invalidated} node with NO surviving alive dependent/consumer --> forgotten --> delete_nodes + delete_data_points
Hypothesis.alive --(its only supporting Evidence became refuted AND a rival Hypothesis has surviving support)--> superseded
losing Hypothesis' rival --(gains the strongest surviving support)--> stays alive, feedback_weight promoted

Every state write also bumps truth_epoch to a monotonically increasing integer (epoch = the memify run counter). Recall filters on truth_alignment containing "alive".

1.3 Contradiction detection (two-gate, deterministic-first)

The detector runs inside the propagate_refutation extraction/enrichment task. Two gates to kill nondeterminism:

  1. Vector prefilter (cheap, deterministic). hits = await ve.search("Evidence_claim", query_text=NEW_FACT, limit=5, include_payload=True). Candidates = hits with cosine distance score < 0.35 (topically related — same subject). This narrows the LLM to plausibly-conflicting evidence only. Contradictory claims sometimes embed far apart, so claims MUST be normalized to subject + predicate phrasing at ingest, and limit kept ≥5.
  2. LLM adjudication (semantic). For each candidate, call LLMGateway.acreate_structured_output with a skeptical-analyst system prompt and response_model=ContradictionJudgement (below). Only a verdict of contradicts (or supersedes) with confidence ≥ 0.6 triggers refutation. This distinguishes genuine contradiction ("report was back-dated") from topical overlap ("also mentions the report").
class ContradictionJudgement(BaseModel):
    relation: Literal["contradicts", "supersedes", "supports", "unrelated"]
    confidence: float  # 0..1
    rationale: str

--demo / DEMO_MODE override: when set, the seeded contradiction's target evidence id is pinned (REFUTED_ID env / seed constant), so the cascade runs on real graph APIs even if the LLM judge stalls or the key is flaky. This is the non-negotiable demo safety net.

1.4 Forward refutation propagation (exact algorithm)

Entry: an Evidence node E confirmed refuted (§1.3).

1. set_node_truth_state({E.id: {"truth_alignment": ["refuted"], "truth_epoch": epoch}})
   set_node_feedback_weights({E.id: 0.0})

2. FORWARD CLOSURE — reverse-BFS along depends_on (Conclusion --depends_on--> Evidence).
   Seeds = [E.id]. Traverse edges of type ["depends_on", "supersedes"] via:
       nodes, edges = await ge.get_neighborhood([E.id], depth=4, edge_types=["depends_on", "supports"])
   Collect every Conclusion C where a `depends_on` edge points from C into the refuted set
   (transitively, up to depth 4). Because depends_on is Conclusion→Evidence, the "dependents"
   are the SOURCES of those edges. A Conclusion is invalidated iff at least one of its
   `critical: true` depends_on edges targets a refuted/invalidated node.

3. For each invalidated Conclusion C:
       set_node_truth_state({C.id: {"truth_alignment": ["invalidated"], "truth_epoch": epoch}})
       set_node_feedback_weights({C.id: 0.0})

4. PROMOTE competing hypothesis (promote_competing_hypothesis task):
   - Find the Hypothesis H_dead whose only supporting Evidence is now refuted.
       set_node_truth_state({H_dead.id: {"truth_alignment": ["superseded"], "truth_epoch": epoch}})
   - Among rival Hypotheses still holding ≥1 alive `supports` Evidence, pick the one with the
     highest summed support weight (read edge {"weight"}). Promote it:
       set_node_feedback_weights({H_win.id: <boosted>})  # stays alive; becomes new frontier

5. RECORD new fact + supersedes edge (add_data_points dual-write):
   - Materialize NEW_FACT as an Evidence DataPoint, add via add_data_points (writes graph + vector).
   - add_edge(new_evidence.id, E.id, "supersedes", {"confidence": verdict.confidence})

Direction summary: refutation flows from Evidence up to the Conclusions that depend on it by walking depends_on edges backward (Conclusion is the edge source). Hypotheses are re-scored via their supports/refutes edges. No forward walk ever crosses a supersedes edge into an already-superseded node (prevents loops — see §4).

1.5 forget() orphan conditions (surgical delete)

forget_orphan_deadends() runs after propagation and grows a death set by traversal:

A node is FORGOTTEN (hard-deleted from graph + vector) iff ALL hold:
  (a) its truth_alignment is refuted OR invalidated (never alive/superseded — superseded nodes
      are kept as provenance), AND
  (b) it has NO surviving consumer: no alive node reaches it via depends_on/supports
      (checked with get_connections — _has_alive_alternative == False), AND
  (c) it is not itself the target of a supersedes edge FROM an alive node (that node is the
      provenance anchor of the new truth and must be retained).

Nodes that STILL feed a live node are retained even if refuted (partial refutation, §4).

Delete implementation (one shot per node batch):

await ge.delete_nodes([str(id) for id in death_set])
await ve.delete_data_points("Evidence_claim", [evidence_ids_in_death_set])
await ve.delete_data_points("Conclusion_statement", [conclusion_ids_in_death_set])

Provenance is kept: only truly orphaned dead-ends are hard-deleted. refuted/superseded nodes that still explain why the graph changed remain, carrying their state flag.

1.6 Cross-session persistence (the proof)

Truth-state is stored on the node in the graph DB (Ladybug/SQLite-backed), so it survives process restart. Session 2's recall() reads only truth_alignment == alive context. A parallel plain-RAG baseline (recall(query_type=SearchType.RAG_COMPLETION) or direct ve.search) does NOT read truth-state and re-cites the deleted/stale fact — this A/B is the scoreboard.


2. Data Model

2.1 Node DataPoint subclasses (memory_core/models.py)

Class Embeddable field Other fields Collection (auto)
InvestigationQuestion question: Annotated[str, Embeddable()] InvestigationQuestion_question
Hypothesis statement: Annotated[str, Embeddable(), Dedup()] question_id: str, status: str="alive", prior: float Hypothesis_statement
Evidence claim: Annotated[str, Embeddable(), Dedup()] source_id: str, quote: str, stance: str (supports/refutes), asserted_at: str (ISO date) Evidence_claim (the refutation entry point / vector prefilter target)
Conclusion statement: Annotated[str, Embeddable()] confidence: float, depends_on_ids: list[str] Conclusion_statement

The collection name is "{ClassName}_{embeddable_field}" — auto-created on write. Evidence_claim is the collection the contradiction prefilter searches.

2.2 Edge types (already listed §1.1)

depends_on (Conclusion→Evidence, {critical:bool}), supports/refutes (Evidence→Hypothesis, {weight:float}), supersedes (Evidence→Evidence, {confidence:float}).

2.3 Node-level metadata / state fields (written via engine APIs, not model fields)

Field Written by Meaning
truth_alignment: list[str] set_node_truth_state lifecycle state (§1.2). Recall filter key.
truth_epoch: int set_node_truth_state monotonically increasing revision epoch.
feedback_weight: float set_node_feedback_weights confidence/health (0.0 = dead, promoted hypotheses boosted).
asserted_at (model field on Evidence) ingest timestamp used for supersede tie-breaks (newer = max(asserted_at)).
source_id (model field) ingest provenance handle for the source document.

2.4 Dedup across sessions

Hypothesis and Evidence set identity_fields in metadata (e.g. ["question_id","statement"]). DataPoint generates a stable identity id from these (DataPoint.py:76-81), so re-adding the same belief in a later session updates the existing node instead of duplicating it — essential for cross-session refutation to land on the right node.


3. User / Demo Flow

3.1 End-to-end story (money shot)

Question: "Did Company X know about the defect before the recall?"

Session 1 builds:

  • Hypothesis A: "X knew via QA report, Mar 2021" (supported by Evidence E_qa)
  • Hypothesis B: "X knew via supplier email, Jan 2021" (supported by Evidence E_email)
  • Hypothesis C: "X didn't know" (unsupported)
  • Conclusion K: "X knew by March 2021" — depends_on(K → E_qa, critical=True)

Saved graph persists (survives restart).

Session 2 (reopened next day): analyst pastes ONE line — "Forensic audit: the March QA report was back-dated."

  1. Evidence_claim vector prefilter finds E_qa (distance < 0.35).
  2. LLM judge: contradicts, confidence 0.9.
  3. E_qa → refuted; forward BFS over depends_on finds K (critical dep) → K invalidated.
  4. A loses its only support → A superseded; B ignites as the new frontier (feedback_weight promoted) — in ~3s.
  5. forget_orphan_deadends: K is orphaned (no alive consumer) → hard-deleted from graph + vector. E_qa kept as refuted provenance (target of new fact's supersedes edge).
  6. Scoreboard: FALSIFY recall now answers via B (Jan 2021); the plain-RAG baseline still cites the back-dated March QA report.

3.2 Screen beat (≤30s, one screen)

Force-graph shows A/B/C + K. Analyst pastes the fact → E_qa flashes red → K crosses out and vanishes → A dims (superseded) → B glows green as new frontier. A live scoreboard panel: FALSIFY: "B — Jan 2021 (supplier email)" vs Plain RAG: "March 2021 QA report" — captioned "AI revised, not forgot."

3.3 Cross-session proof

Restart the process (or run main.py a second time with --session 2). Because truth-state is on-node persisted, session 2's recall() never sees K or A-as-truth. Show the two recalls side by side.


4. Success Criteria

4.1 Unit-test assertions (pytest)

test_direct_refutation:
    after propagate_refutation(new_fact contradicting E_qa):
    assert get_node_truth_state([E_qa])[E_qa]["truth_alignment"] == ["refuted"]

test_forward_cascade_invalidates_conclusion:
    assert get_node_truth_state([K])[K]["truth_alignment"] == ["invalidated"]

test_competing_hypothesis_promoted:
    assert A.truth_alignment == ["superseded"]
    assert "alive" in B.truth_alignment
    assert feedback_weight(B) > feedback_weight(A)

test_orphan_forgotten_from_both_stores:
    assert K.id NOT in (await ge has node)            # gone from graph
    assert K.id NOT in ve.search("Conclusion_statement", ...)   # gone from vector

test_provenance_kept:
    assert E_qa still exists with truth_alignment == ["refuted"]   # NOT deleted (supersede anchor)

test_cross_session_recall_skips_dead:
    recall("did X know?") context contains B, does NOT contain K or A-as-truth

test_baseline_still_stale:
    RAG_COMPLETION / raw ve.search STILL returns the March QA claim   # proves the differentiator

4.2 Expected python main.py output (judges, ~2 min)

[FALSIFY] Session 1: built belief graph for "Did Company X know...?"
          Hypotheses: A(QA Mar'21) B(email Jan'21) C(didn't know)
          Conclusion K depends_on E_qa
[FALSIFY] Session 2: new fact -> "March QA report was back-dated"
          contradiction: E_qa  (judge=contradicts conf=0.90)
          forward cascade: K invalidated
          A superseded  ->  B ignites (new frontier)
          forgot 1 orphan (K) from graph + vector
[SCOREBOARD]
   FALSIFY recall : X knew by Jan 2021 (supplier email)   [revised]
   Plain-RAG      : X knew by Mar 2021 (QA report)         [STALE]

Graceful exit if no API key: print a clear message + how to set LLM_API_KEY, exit code 0.

4.3 Edge cases (must be handled)

Case Required behavior
Cycle (A depends_on B depends_on A) BFS tracks a visited set; never revisit. supersedes edges never traversed into already-superseded nodes. Termination guaranteed.
Multiple contradictions (new fact refutes 2 Evidence nodes) Each refuted independently; union of their dependent Conclusions invalidated; single forget pass over the merged death set.
Partial refutation (Conclusion depends_on E_qa AND E_alt, both critical) Conclusion invalidated only if a critical dep is refuted AND no alive critical alternative remains (_has_alive_alternative). If E_alt still alive & critical satisfied, Conclusion stays alive; E_qa refuted but retained (still feeds a live node → not orphaned).
Diamond dependency (K depends_on E1,E2; E1,E2 both depends-chain to refuted E0) Deduplicate via visited set so K is invalidated once, not twice; forget counts each node once. Explicit unit test required.
Non-critical dep refuted Conclusion's confidence decays but stays alive (only critical:true deps invalidate).
Contradiction judge false-positive risk Two-gate (vector <0.35 + LLM ≥0.6) + --demo pin.

5. Tech Stack & Module Layout

5.1 Cognee APIs used (all §0-verified)

  • Persistence: cognee.remember(session_id=...), cognee.recall(session_id=..., query_type=...).
  • Enrichment pipeline: cognee.memify(extraction_tasks=[Task(collect_belief_subgraph)], enrichment_tasks=[Task(propagate_refutation), Task(promote_competing_hypothesis), Task(forget_orphan_deadends), Task(add_data_points)], data=[{}], node_type=NodeSet, node_name=[question_id]).
  • Graph: get_graph_engine()get_neighborhood (depth=4, edge_types=["depends_on","supports"]), get_connections, set_node_truth_state, get_node_truth_state, set_node_feedback_weights, add_edge, delete_nodes.
  • Vector: get_vector_engine()search("Evidence_claim", ...), delete_data_points.
  • LLM: LLMGateway.acreate_structured_output(text_input, system_prompt, response_model=ContradictionJudgement).
  • Storage: add_data_points task (cognee/tasks/storage/add_data_points.py:31) for dual graph+vector write of the new fact.
  • Baseline: SearchType.RAG_COMPLETION (from cognee/modules/search/types/SearchType.py).

5.2 Module layout (write under /workspaces/hackathon-app/)

/workspaces/hackathon-app/
  main.py                      # `python main.py` entry — runs seed + demo + scoreboard; graceful no-key exit 0
  requirements.txt / pyproject # deps: cognee (editable ref) + minimal
  .env.template                # LLM_API_KEY, LLM_PROVIDER=openai, LLM_ENDPOINT (custom endpoint), LLM_MODEL, DEMO_MODE
  memory_core/
    __init__.py
    models.py                  # DataPoint subclasses (§2.1) + ContradictionJudgement
    edges.py                   # edge-name constants: DEPENDS_ON, SUPPORTS, REFUTES, SUPERSEDES
    tasks.py                   # collect_belief_subgraph, propagate_refutation,
                               #   promote_competing_hypothesis, forget_orphan_deadends
    falsify.py                 # orchestration: build_graph(), revise(new_fact), scoreboard()
    seed.py                    # demo corpus: Company-X recall investigation (A/B/C + K)
  ui/
    graph.html                 # react-force-graph (color by truth_state, red-flash-then-remove ripple ≤3s)
    server.py                  # tiny static+JSON server feeding get_neighborhood snapshots
  tests/
    test_propagation.py        # §4.1 assertions
    test_edge_cases.py         # cycle / diamond / partial / multi

5.3 Visualization approach

  • react-force-graph (CDN, single graph.html) reads a JSON snapshot built from ge.get_neighborhood([question_id], depth=4).
  • Node color keyed on truth_alignment: alive=green, refuted=red, invalidated=grey-strikethrough, superseded=dim-amber. Forgotten nodes: red-flash animation then removed from the sim (≤3s ripple).
  • Scoreboard panel overlays FALSIFY-vs-RAG answers. No external services — server is stdlib/http.server or FastAPI already in cognee.

5.4 Config / runtime constraints

  • OpenAI-compatible: honor LLM_PROVIDER (openai or custom), LLM_ENDPOINT, LLM_MODEL, LLM_API_KEY. Defaults: LanceDB (vector) + Ladybug (graph) + SQLite (relational) — zero external services.
  • DEMO_MODE=1 (or --demo) pins REFUTED_ID so the cascade+forget run on real APIs regardless of LLM flakiness.
  • Never modify /workspaces/cognee. All writes under /workspaces/hackathon-app/ (absolute paths).

6. Non-negotiables (contract invariants)

  1. Forward propagation uses get_neighborhood + set_node_truth_state (on-node, persistent). No in-memory-only state.
  2. Surgical forget deletes from both graph (delete_nodes) and vector (delete_data_points); provenance (refuted/superseded) nodes are retained.
  3. Cross-session persistence of disbelief is demonstrated (restart / session 2 recall skips dead branches).
  4. The A/B scoreboard (FALSIFY revised vs plain-RAG stale) is shown every run.
  5. Contradiction detection is two-gate (vector prefilter <0.35 + LLM judge ≥0.6) with a --demo deterministic override.
  6. Task imported from cognee.modules.pipelines.tasks.task (lowercase); get_graph_engine awaited, get_vector_engine not.