Conflicts and resolution¶
When evidence disagrees, Corollary raises a Conflict instead of letting anything pick a side silently.
Value conflicts¶
Two IN revisions of the same key with different values form a value conflict:
kb.assert_("revenue:Q2", 4.3e9, source="document:press_release")
kb.assert_("revenue:Q2", 4.1e9, source="tool:sec_filings")
(conflict,) = kb.conflicts()
print(conflict)
# Conflict(value: revenue:Q2) 2 incompatible values (4,300,000,000, 4,100,000,000): ...
print(conflict.explain()) # every side with its full support chain
While the conflict is open:
kb.value("revenue:Q2")andkb["revenue:Q2"]raiseUnresolvedConflictError;deriveandjustifyrefuse to use the key as an antecedent;- the projector hides the key from the model and lists it under
# Conflicts; kb.conflicted_keys()includes it.
Nothing downstream can be built on a disputed fact.
Constraint conflicts¶
A Constraint is an invariant over the current values of some keys:
kb.add_constraint(
"margin<=100",
["gross_margin"],
lambda m: m <= 100,
description="gross margin above 100%",
)
kb.add_constraint("cash>=0", ["cash"], lambda c: c >= 0)
The predicate receives the current values, in order, whenever every key has exactly one believed value.
If it returns False, or raises, the constraint becomes a Conflict of kind constraint. Constraint
conflicts are reported but don't block usage of the keys. Resolve them to restore consistency.
Resolving by hand¶
kb.resolve(conflict, keep="revenue:Q2@2") # retract every other side
kb.resolve(conflict, retract="revenue:Q2@1") # or name what to retract
kb.resolve(conflict, keep=belief, reason="SEC filing is authoritative")
Resolution is a retraction with a reason. The losing revision stays in the base, OUT, and anything that
depended on it cascades as usual.
If a person checked the answer, pass learn=True: the losing sources are recorded as wrong and the
winning ones as right in the trust ledger, so the next conflict between them is decided by their track
records. AskHuman does this by default. Policy resolvers never do, so a policy can't reinforce itself.
See Confidence.
Resolving by policy¶
A resolver is any callable (conflict, kb) -> Resolution | None. Returning None leaves the conflict
open, which is always safe.
from corollary import PreferHigherConfidence, PreferNewest, PreferSource, AskHuman
kb.resolve_conflicts(PreferSource(["human", "tool:sec_filings", "tool", "document"]))
| Resolver | Value conflicts | Constraint conflicts |
|---|---|---|
PreferHigherConfidence(margin=0.0) |
keep the most confident side; leave it open if the top two are within margin |
retract the least confident belief |
PreferNewest() |
keep the most recent side | retract the oldest belief |
PreferSource(order=None) |
keep the highest-ranked source; ties stay open. Defaults to TrustPolicy.source_rank |
retract the lowest-ranked |
AskHuman(ask) |
ask(conflict) returns the side to keep, or None |
ask(conflict) returns the belief to retract |
AskHuman connects conflicts to a person, through a CLI prompt, a review queue or a chat message:
def ask(conflict):
print(conflict.explain())
choice = input(f"Keep which of {conflict.refs}? (blank to skip) ")
return choice or None
kb.resolve_conflicts(AskHuman(ask))
With an agent, pass the resolver to the constructor and it runs before every step: