popoto.extraction.resolution_log¶
popoto.extraction.resolution_log
¶
The write-side sidecar store for reference resolution (M4, #563).
Every :func:~popoto.extraction.resolution.resolve_references outcome
that gets appended to the provenance journal gets one row here too, keyed
by the same (agent_id, turn_id, candidate_id) composite identity the
M3 decision log uses. :class:ResolutionRecord is a sidecar, not the
source of truth: the res:* status flag itself travels on the journal
entry's own subject tag (Resolution.subject_tag, set on
JournalEntry.subjects by the pipeline), independent of whether this
write ever lands. This module exists so the full Reference detail --
surface offsets, resolved text, assumptions, candidate lists, clarifying
questions -- has somewhere durable to live without bloating the journal
entry itself. M7 (#566) is its intended consumer.
The sidecar is never load-bearing for the res: flag. A failed
:meth:ResolutionLog.write must never change whether the candidate was
accepted, nor what subject tags landed on its journal entry -- both are
already committed by the time write runs. See :meth:ResolutionLog.write.
Composite KeyField identity, never AutoKeyField. Exactly like
:class:~popoto.extraction.decision_log.DecisionRecord
(decision_log.py:13-23): agent_id, turn_id and
candidate_id are all KeyFields, so the Redis key is the
candidate's identity and a second write() for the same tuple
transitions that row in place rather than minting a duplicate.
AutoKeyField is forbidden on this model for the same reason it is
forbidden on DecisionRecord: it would leave stray rows behind on
every re-write.
references_json is JSON, not msgpack, on purpose. Every other
Popoto model field is msgpack-packed by the base Model encoding, and
this one field is deliberately re-encoded as a JSON string on top of
that so the reference detail stays readable by M7 and by a human running
redis-cli HGET -- msgpack bytes would not be.
No TTL. Matching M3's decision log, rows here are unbounded and never expire. Retention and sweep policy is M9 (#568)'s job, not v1's -- this module declines to guess a horizon.
ResolutionRecord
¶
Bases: Model
One candidate's reference-resolution row.
agent_id + turn_id + candidate_id form the composite key,
exactly mirroring :class:~popoto.extraction.decision_log.DecisionRecord.
Writing the same tuple twice transitions one row rather than creating
two. See the module docstring for why AutoKeyField is forbidden
here.
Every field below is a plain (non-indexed) field, matching
DecisionRecord's convention -- this row is read by identity
(ResolutionLog.get), never scanned or filtered by field value.
Attributes:
| Name | Type | Description |
|---|---|---|
agent_id |
Owning agent. KeyField. |
|
turn_id |
The turn the candidate was generated from. KeyField. |
|
candidate_id |
|
|
status |
The aggregate :class: |
|
statement |
The rewritten statement written to the journal. |
|
verbatim |
The original candidate text, unmodified. |
|
references_json |
|
|
valid_from |
The onset instant as an epoch float, or |
|
entry_id |
The journal entry id this resolution was written for. |
|
speaker |
Who spoke the turn, from the resolution's |
|
captured_at |
Epoch seconds the turn was captured. |
|
timezone |
IANA timezone name the resolution ran against. |
|
window_truncated |
Whether the conversational window was truncated. |
|
degraded |
Whether this was a fail-open fallback resolution. |
|
written_at |
Unix timestamp this row was last written. |
Source code in src/popoto/extraction/resolution_log.py
ResolutionLog
¶
Writer/reader over :class:ResolutionRecord rows.
Stateless -- every method takes the identity it operates on -- so one instance can serve any agent.
Example::
log = ResolutionLog()
log.write(
agent_id="agent-7",
turn_id="t-41",
candidate_id="t-41:sentence:0",
resolution=resolution,
entry_id=entry.entry_id,
)
row = log.get("agent-7", "t-41", "t-41:sentence:0")
Source code in src/popoto/extraction/resolution_log.py
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 240 241 | |
write(agent_id, turn_id, candidate_id, resolution, entry_id='')
¶
Serialise resolution into a :class:ResolutionRecord row.
Idempotent by composite key: a second call with the same
(agent_id, turn_id, candidate_id) overwrites the row in place
rather than creating a duplicate, because agent_id/turn_id/
candidate_id are the model's KeyFields.
This method never raises. Any exception -- a bad
resolution shape, a Redis error, anything -- is caught, logged
as a POPOTO.extraction warning, and turned into a False
return. The sidecar is not load-bearing: by the time this runs,
the candidate's accept/reject outcome and its journal subject tags
are already committed, and a sidecar write failure must never flip
that outcome (plan Race 2).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agent_id
|
str
|
Owning agent. |
required |
turn_id
|
str
|
The turn the candidate was generated from. |
required |
candidate_id
|
str
|
The candidate's identity. |
required |
resolution
|
Resolution
|
The :class: |
required |
entry_id
|
str
|
The journal entry id, if the candidate was accepted. |
''
|
Returns:
| Type | Description |
|---|---|
bool
|
|
Source code in src/popoto/extraction/resolution_log.py
get(agent_id, turn_id, candidate_id)
¶
Return the row for this candidate, or None if absent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agent_id
|
str
|
Owning agent. |
required |
turn_id
|
str
|
The turn the candidate was generated from. |
required |
candidate_id
|
str
|
The candidate's identity. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
The |
Optional[ResolutionRecord]
|
class: |