ConfidenceField¶
A Field subclass that tracks confidence metadata per member, updated atomically via Lua script using a capped-evidence Bayesian rule.
Overview¶
ConfidenceField maintains a confidence score for each record, allowing the system to track how certain it should be about a given piece of information. The update rule is a capped-evidence Bayesian (also called "forgetful Bayesian"): within an evidence window it is an exact running mean — order-invariant and prior-weighted; beyond the cap it is bounded exponential forgetting at a constant rate.
The field stores its metadata in a companion Redis hash:
{confidence, evidence_count, corroborations, contradictions}
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
initial_confidence |
float | 0.5 | Starting confidence for new members (0-1). Acts as one prior pseudo-observation in the running mean. |
evidence_cap |
int | 20 | Maximum effective evidence count. Within this window updates are a running mean; beyond it updates apply a fixed gain of 1/(cap+1), producing bounded exponential forgetting. Must be an integer ≥ 1. |
partition_by |
str or tuple | () |
Field name(s) to partition the companion hash by. Splits the single Redis hash into per-partition hashes for efficient reads. |
Warning — same cap across processes: The
evidence_capis passed per-call and is NOT stored in the companion hash. All processes updating the same companion hash entry must be configured with identicalevidence_capvalues. Divergent caps produce silently inconsistent gain schedules with no runtime detection.
Usage¶
from popoto import Model, UniqueKeyField, StringField
from popoto.fields.confidence_field import ConfidenceField
class Memory(Model):
key = UniqueKeyField()
content = StringField()
certainty = ConfidenceField(initial_confidence=0.5)
# Create a memory
memory = Memory.create(key="fact1", content="The sky is blue")
# Corroborate (signal >= 0.5 increases confidence)
ConfidenceField.update_confidence(memory, "certainty", signal=0.9)
# Contradict (signal < 0.5 decreases confidence)
ConfidenceField.update_confidence(memory, "certainty", signal=0.1)
# Read current confidence
confidence = ConfidenceField.get_confidence(memory, "certainty")
# Read all metadata
data = ConfidenceField.get_confidence_data(memory, "certainty")
# Returns: {confidence: 0.5, evidence_count: 2, corroborations: 1, contradictions: 1}
Update Formula¶
The update rule uses a capped effective-evidence count:
n_eff = min(evidence_count + 1, cap)
new_confidence = confidence + (signal - confidence) / (n_eff + 1)
The +1 in evidence_count + 1 is an inlined prior pseudo-count — initial_confidence behaves as one real observation. Results are clamped to [0, 1].
Running-Mean Regime (evidence_count < cap)¶
While the real evidence count is below the cap, the formula is an exact running mean over {prior, s1, s2, …, sn}:
This is order-invariant up to ~1e-12 (IEEE-754 intermediate rounding; the closed form is the oracle, not bit-identical output). The first update from initial_confidence=0.5 with signal=0.9 yields 0.7, not 0.9 — the prior is never erased.
Forgetting Regime (evidence_count >= cap)¶
Once the evidence count reaches the cap, the effective gain is fixed at 1/(cap+1). With cap=20 (default), each update multiplies (confidence - signal) by 20/21 ≈ 0.952.
Worked example: a belief with confidence=0.9 and evidence_count=20 (at the cap) subjected to 15 consecutive contradictions at signal=0.1:
After k contradictions: confidence = 0.1 + 0.8 × (20/21)^k
Crossing 0.5 requires: 0.8 × (20/21)^k < 0.4
k > ln(0.4/0.8) / ln(20/21) ≈ 14.2 → 15 contradictions
This is bounded exponential forgetting — well-established beliefs take ~15 systematic contradictions to cross the midpoint at the default cap, compared to ~5 under the previous decaying-step rule.
Update Behavior Summary¶
| Regime | Condition | Behavior |
|---|---|---|
| Running mean | evidence_count < cap (20) | Exact mean of {prior, all signals}; order-invariant to ~1e-12 |
| Fixed forgetting | evidence_count >= cap | Constant gain 1/(cap+1); ~15 contradictions cross 0.5 from 0.9 |
Entrainment with ObservationProtocol¶
When used with ObservationProtocol, confidence is automatically updated based on how the agent uses retrieved memories:
| Outcome | Effect on Confidence |
|---|---|
acted |
Corroborate (signal=0.9) |
dismissed |
No change |
deferred |
No change |
contradicted |
Contradict (signal=0.1) |
Auto-discharge¶
When confidence drops strictly below AUTO_DISCHARGE_CONFIDENCE_THRESHOLD (0.1), homeostatic pressure on any CyclicDecayField is automatically resolved (discharged). This prevents low-confidence memories from building urgency.
The comparison uses an epsilon guard: discharge fires only when conf < 0.1 - 1e-9. A confidence value that rounds to exactly 0.1 in display but is stored as 0.09999999999999998 (a common IEEE-754 artifact on the first contradiction from a default prior) does not trigger discharge. A genuine drop — one clearly below the threshold by more than epsilon — still does.
API Reference¶
ConfidenceField.update_confidence(instance, field_name, signal)¶
Atomically update confidence using the capped-evidence Bayesian formula.
- signal: Float 0-1. Values >= 0.5 corroborate, < 0.5 contradict.
- Returns: The new confidence value.
- Raises:
TypeErrorif unsaved or wrong field type;ValueErrorif signal out of range.
Warning: All processes calling
update_confidenceon the same companion hash entry must use identicalevidence_capvalues. The cap is not stored in Redis and divergent values produce silently inconsistent update trajectories.
ConfidenceField.get_confidence(instance, field_name)¶
Read the current confidence value.
- Returns: Float confidence value, or
initial_confidenceif no data exists.
ConfidenceField.get_confidence_data(instance, field_name)¶
Read all confidence metadata.
- Returns: Dict with keys
confidence,evidence_count,corroborations,contradictions.
Note: evidence_count reflects real observations only — the prior pseudo-count is internal to the Lua update and does not inflate this counter.
Inspecting Companion Hash Keys¶
Each ConfidenceField stores its confidence metadata in a companion Redis hash alongside
the main model hash. The public companion key methods let you build these Redis keys
for debugging, monitoring, or direct Redis inspection without reverse-engineering
suffix conventions.
import redis
from popoto import Model, UniqueKeyField, StringField
from popoto.fields.confidence_field import ConfidenceField
class Memory(Model):
key = UniqueKeyField()
content = StringField()
certainty = ConfidenceField(initial_confidence=0.5)
# Create and update a memory
memory = Memory.create(key="fact1", content="The sky is blue")
ConfidenceField.update_confidence(memory, "certainty", signal=0.9)
# Get the companion hash key for direct Redis inspection
field = Memory._options.fields["certainty"]
hash_key = field.get_data_hash_key(memory, "certainty")
print(hash_key)
# => "$ConfidencF:Memory:certainty:data"
# Inspect the raw companion hash in Redis
r = redis.from_url("redis://localhost:6379")
raw_data = r.hgetall(hash_key)
print(raw_data)
# Shows all members and their msgpack-encoded confidence metadata
When you do not have an instance loaded, use get_data_hash_key_from_values to build
the key from explicit values:
# Build the key without loading a model instance
key = field.get_data_hash_key_from_values(Memory, "certainty")
# => "$ConfidencF:Memory:certainty:data"
# For partitioned fields, pass the partition values as keyword arguments
# key = field.get_data_hash_key_from_values(Memory, "certainty", project="atlas")
Partitioned Reads¶
When the companion hash grows large (thousands of members), reads become expensive because
HGETALL loads every entry. The partition_by parameter splits the hash by one or more
field values, so each read only touches the relevant partition.
class Memory(Model):
project = KeyField(type=str)
key = UniqueKeyField()
content = StringField()
certainty = ConfidenceField(initial_confidence=0.5, partition_by='project')
All read and write operations automatically resolve the correct partition hash from the
model instance. Queries on partitioned ConfidenceFields must include the partition field
value(s), or a QueryException is raised.
See Multi-Tenancy: Hash-based field partitioning for the full pattern including migration from unpartitioned data.
HSCAN Filtered Reads¶
For unpartitioned hashes, get_confidence_filtered() uses HSCAN with MATCH to iterate
without loading all entries into memory:
results = ConfidenceField.get_confidence_filtered(Memory, "certainty", pattern="Memory:atlas:*")
# Returns: {member_key: {confidence, evidence_count, ...}}
Migration Helper¶
# Dry run — see what would happen
report = ConfidenceField.migrate_to_partitioned(Memory, "certainty", dry_run=True)
# Execute migration
report = ConfidenceField.migrate_to_partitioned(Memory, "certainty")
Redis Key Patterns¶
| Key | Type | Description |
|---|---|---|
$ConfidencF:{Model}:{field}:data |
HASH | Unpartitioned: all members' confidence metadata |
$ConfidencF:{Model}:{field}:data:{partition_value} |
HASH | Partitioned: members in one partition |
Working Example: Popoto Kitchen¶
The Popoto Kitchen example app
includes a ReviewScore model that demonstrates ConfidenceField with
partition_by="restaurant". Run the operations demo to see capped-evidence updates,
companion hash key inspection, and partitioned confidence in action:
See examples/popoto_kitchen/operations.py
for the full source, and the
kitchen demo docs
for a walkthrough.
Companion Fields¶
ConfidenceField works alongside other memory system fields:
- DecayingSortedField: Composite scoring via
priority = decay_score * confidence - CyclicDecayField: Auto-discharge when confidence drops below threshold
- WriteFilterMixin: Use confidence in
compute_filter_score()for directed forgetting - AccessTrackerMixin: Read tracking independent of confidence