symfonic.core.learning.phases_profile¶
phases_profile ¶
Phase 5, now owned by the memory capability.
promote_profile_corrections is on the quick roster, so it moved to
:mod:symfonic.capabilities.memory.phases.profile and is imported back here
for the legacy consolidator to call. One phase, one implementation, for as
long as both routes ship -- see that module for the three bugs the phase was
written to fix, which are worth keeping in front of anyone who edits it.
PROMOTED_FIELD_SOURCE
module-attribute
¶
Provenance recorded against a field this phase promoted.
The prompt block renders each profile field with its own provenance, so a role the user corrected today must not present the onboarding form's three-month-old stamp inherited from the node it landed on. Writing the field's real source here is what makes the rendered attribution true -- and provenance is recorded, never invented, so it is written at the moment of the write rather than reconstructed at render time.
promote_profile_corrections
async
¶
promote_profile_corrections(graph: GraphMemoryStore, scope: TenantScope, recent_nodes: list[MemoryNode], profile_fields: frozenset[str], now: datetime | None = None) -> int
Promote user-corrected profile facts onto the tenant's SOUL node.
Trigger condition (kept verbatim from the pre-redesign
apply_soul_corrections, phases.py:446): only nodes carrying
properties["_last_edited_by"] == "user_manual_edit" are treated as
corrections. This keeps agent self-edits (extraction writes, this
phase's own promotion writes, etc.) out of the promotion loop --
load-bearing, do not relax without also revisiting the idempotency
guarantee below.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
graph
|
GraphMemoryStore
|
Tenant-scoped graph store. Both the read (locating the
canonical SOUL node) and the write (applying corrections) go
through this store with |
required |
scope
|
TenantScope
|
Tenant isolation scope for every graph operation. |
required |
recent_nodes
|
list[MemoryNode]
|
Nodes considered for this consolidation pass
(typically |
required |
profile_fields
|
frozenset[str]
|
The set of field names that constitute "profile"
for this domain. Callers derive this from
|
required |
now
|
datetime | None
|
Instant recorded as each promoted field's provenance. Defaults to wall-clock UTC. Injectable because a caller that pins its own clock -- a test, a replay, a walkthrough -- would otherwise write a stamp it cannot predict, and the rendered provenance would read as being from the future. |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Count of profile fields whose value actually changed on the |
int
|
canonical SOUL node (mirrors the legacy |
int
|
"count of key assignments made" semantics). A guard-marker-only |
int
|
rewrite (see idempotency note below) does not increment this |
int
|
count even though a graph write occurs. |
Idempotency: when the correction source IS the canonical SOUL node
(a human edited it directly rather than via a separate correction
record), its own _last_edited_by guard is flipped from
"user_manual_edit" to PROMOTION_MARKER even if the "corrected"
values already match (i.e. even when the differential count is zero).
Without this, the node would satisfy the guard again on the very next
pass -- reading this promotion's own prior output back as a fresh
user correction and re-promoting forever.
Source code in src/symfonic/capabilities/memory/phases/profile.py
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 | |