NegativeReinforcementHandler -- logarithmic decay on user correction.
Default CallbackHandler helper that walks the turn's inference_paths
and applies a logarithmic negative decay to each traversed edge weight.
Stateless except for the injected graph store. Safe to reuse across turns.
NegativeReinforcementHandler
NegativeReinforcementHandler(graph_store: Any)
Walks traversed edges from the corrected turn and decays their weights.
Uses a logarithmic decay formula
w_new = w_old * (1 - 1/log(uses + e))
with a floor of 0.01 to prevent total zeroing.
Source code in src/symfonic/core/callbacks/correction.py
| def __init__(self, graph_store: Any) -> None:
self._graph = graph_store
|
decayed_weight
staticmethod
decayed_weight(old_weight: float, uses: int) -> float
Pure function -- exposed for unit testing.
Formula: w_new = w_old * (1 - 1/log(uses + e))
where e = math.e so uses=0 gives factor (1 - 1/1.0) = 0.0 (hard reset).
Floor at 0.01 to prevent complete zeroing / division issues.
Source code in src/symfonic/core/callbacks/correction.py
| @staticmethod
def decayed_weight(old_weight: float, uses: int) -> float:
"""Pure function -- exposed for unit testing.
Formula: w_new = w_old * (1 - 1/log(uses + e))
where e = math.e so uses=0 gives factor (1 - 1/1.0) = 0.0 (hard reset).
Floor at 0.01 to prevent complete zeroing / division issues.
"""
factor = 1.0 - (1.0 / math.log(uses + math.e))
new = old_weight * factor
return max(new, 0.01)
|
on_user_correction
async
on_user_correction(event: UserCorrectionEvent) -> None
Public entrypoint. Iterates every edge in activation_log.traversed_edges
(falls back to inference_paths label-pair lookup when absent) and
calls _decay_edge() once per edge.
Idempotence: per-turn uses snapshot per edge_id. Repeated application
of the same correction with the same snapshot yields the same result.
Source code in src/symfonic/core/callbacks/correction.py
| async def on_user_correction(self, event: UserCorrectionEvent) -> None:
"""Public entrypoint. Iterates every edge in activation_log.traversed_edges
(falls back to inference_paths label-pair lookup when absent) and
calls _decay_edge() once per edge.
Idempotence: per-turn uses snapshot per edge_id. Repeated application
of the same correction with the same snapshot yields the same result.
"""
from symfonic.memory.types import TenantScope
# T-7.21.8 (Slice B site #7): prefer event.scope when set so
# sub_tenant_id + namespace survive the correction-decay path.
# Pre-7.21 the bare reconstruction from event.tenant_id silently
# dropped both -- a multi-brand tenant correction would decay
# edges in the tenant-root partition instead of the brand
# partition where the original turn was scoped.
scope = (
event.scope
if event.scope is not None
else TenantScope(tenant_id=event.tenant_id)
)
log = event.activation_log or {}
# Prefer explicit traversed_edges when present
edges_raw = log.get("traversed_edges") or []
if not edges_raw:
# Fallback: walk inference_paths as label pairs
paths = log.get("inference_paths") or []
edges_raw = [
{
"source_label": p[0],
"target_label": p[-1],
"relationship": "ASSOCIATED",
}
for p in paths
if len(p) >= 2
]
if not edges_raw:
return
# Snapshot uses for idempotence within this turn
snapshot: dict[str, int] = {}
for e_info in edges_raw:
edge = await self._resolve_edge(scope, e_info)
if edge is None:
continue
key = str(edge.id)
if key not in snapshot:
snapshot[key] = edge.uses
new_weight = self.decayed_weight(edge.weight, snapshot[key])
try:
await self._graph.upsert_edge_props(
scope,
edge.id,
{"weight": new_weight, "provenance": "correction"},
)
except Exception:
# Fallback: delete and re-add with updated weight
try:
from symfonic.memory.models.edge import MemoryEdge
updated_edge = MemoryEdge(
id=edge.id,
source=edge.source,
target=edge.target,
relationship=edge.relationship,
tenant_id=edge.tenant_id,
weight=new_weight,
provenance="correction",
uses=edge.uses,
source_episodic_ids=edge.source_episodic_ids,
)
await self._graph.delete_edge(scope, edge.id)
await self._graph.add_edge(scope, updated_edge)
except Exception:
logger.debug(
"Correction decay failed for edge %s", edge.id,
)
|