eq-layer 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eq_layer/__init__.py +68 -0
- eq_layer/ab_eval.py +302 -0
- eq_layer/actions.py +242 -0
- eq_layer/affect.py +227 -0
- eq_layer/auditor.py +321 -0
- eq_layer/cli.py +174 -0
- eq_layer/coarse_discourse.py +107 -0
- eq_layer/data/intent_train.jsonl +299 -0
- eq_layer/dbdc3.py +197 -0
- eq_layer/dialogbank_iso.py +272 -0
- eq_layer/dialogue_reference.py +393 -0
- eq_layer/intent.py +199 -0
- eq_layer/intent_belief.py +276 -0
- eq_layer/interaction.py +314 -0
- eq_layer/policies.py +295 -0
- eq_layer/preferences.py +125 -0
- eq_layer/response_experiment.py +606 -0
- eq_layer/steer.py +281 -0
- eq_layer/tracker.py +111 -0
- eq_layer/trained_affect.py +246 -0
- eq_layer/trained_breakdown.py +143 -0
- eq_layer/trained_disagreement.py +148 -0
- eq_layer/trained_intent.py +291 -0
- eq_layer/trained_subtext.py +436 -0
- eq_layer-0.1.0.dist-info/METADATA +527 -0
- eq_layer-0.1.0.dist-info/RECORD +29 -0
- eq_layer-0.1.0.dist-info/WHEEL +4 -0
- eq_layer-0.1.0.dist-info/entry_points.txt +2 -0
- eq_layer-0.1.0.dist-info/licenses/LICENSE +21 -0
eq_layer/policies.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
from .intent import IntentState
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Register(Enum):
|
|
10
|
+
MIRROR = "mirror"
|
|
11
|
+
VALIDATE_REDIRECT = "validate_redirect"
|
|
12
|
+
DIRECT = "direct"
|
|
13
|
+
ASK = "ask"
|
|
14
|
+
DEFLATE = "deflate"
|
|
15
|
+
REPAIR = "repair"
|
|
16
|
+
HOLD = "hold"
|
|
17
|
+
BOUNDARY = "boundary"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class AffectState:
|
|
22
|
+
valence: float
|
|
23
|
+
arousal: float
|
|
24
|
+
escalation_delta: float
|
|
25
|
+
stance: str
|
|
26
|
+
subtext: str
|
|
27
|
+
turn_index: int = 0
|
|
28
|
+
history: tuple["AffectState", ...] = ()
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def escalating(self) -> bool:
|
|
32
|
+
return self.escalation_delta > 0.15
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def escalating_past_n(self) -> bool:
|
|
36
|
+
"""Three consecutive turns before this one that all moved upward.
|
|
37
|
+
|
|
38
|
+
The window deliberately excludes the current turn. Snapshot
|
|
39
|
+
classification cannot see this: one polite message after three bad
|
|
40
|
+
ones reads as calm, and that is exactly when the register must not
|
|
41
|
+
return to warm."""
|
|
42
|
+
recent = self.history[-4:-1]
|
|
43
|
+
return len(recent) >= 3 and all(s.escalation_delta > 0.05 for s in recent)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def user_is_right(self) -> bool:
|
|
47
|
+
return self.stance == "user_right"
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def user_is_wrong(self) -> bool:
|
|
51
|
+
return self.stance == "user_wrong"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def stance_unknown(self) -> bool:
|
|
55
|
+
return self.stance == "unknown"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class Policy:
|
|
60
|
+
name: str
|
|
61
|
+
register: Register
|
|
62
|
+
summary: str
|
|
63
|
+
preconditions: tuple[str, ...]
|
|
64
|
+
priority: int = 0
|
|
65
|
+
avoid: tuple[str, ...] = ()
|
|
66
|
+
example_opener: str = ""
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def specificity(self) -> int:
|
|
70
|
+
"""How many conditions must hold. Selection is not first-match:
|
|
71
|
+
the most constrained applicable policy wins, so adding a policy can
|
|
72
|
+
never silently change what a looser one does."""
|
|
73
|
+
return len(self.preconditions)
|
|
74
|
+
|
|
75
|
+
def applies(self, state: AffectState, intent: IntentState | None = None) -> bool:
|
|
76
|
+
return all(PREDICATES[name](state, intent) for name in self.preconditions)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
PREDICATES = {
|
|
80
|
+
"any": lambda s, i: True,
|
|
81
|
+
"escalating": lambda s, i: s.escalating,
|
|
82
|
+
"escalating_past_n": lambda s, i: s.escalating_past_n,
|
|
83
|
+
"not_escalating": lambda s, i: not s.escalating,
|
|
84
|
+
"not_escalating_past_n": lambda s, i: not s.escalating_past_n,
|
|
85
|
+
"user_is_right": lambda s, i: s.user_is_right,
|
|
86
|
+
"user_is_wrong": lambda s, i: s.user_is_wrong,
|
|
87
|
+
"stance_unknown": lambda s, i: s.stance_unknown,
|
|
88
|
+
"low_arousal": lambda s, i: s.arousal < 0.4,
|
|
89
|
+
"high_arousal": lambda s, i: s.arousal >= 0.5,
|
|
90
|
+
"high_arousal_run": lambda s, i: len(s.history) >= 2 and all(
|
|
91
|
+
x.arousal >= 0.5 for x in s.history[-2:]
|
|
92
|
+
),
|
|
93
|
+
"not_high_arousal_run": lambda s, i: not (
|
|
94
|
+
len(s.history) >= 2 and all(x.arousal >= 0.5 for x in s.history[-2:])
|
|
95
|
+
),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
SUBTEXTS = (
|
|
99
|
+
"correction",
|
|
100
|
+
"disclosure_request",
|
|
101
|
+
"exhaustion",
|
|
102
|
+
"resignation",
|
|
103
|
+
"escalating",
|
|
104
|
+
"demand",
|
|
105
|
+
"challenge",
|
|
106
|
+
"question",
|
|
107
|
+
"statement",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
for _subtext in SUBTEXTS:
|
|
111
|
+
PREDICATES[f"subtext:{_subtext}"] = (
|
|
112
|
+
lambda t: lambda s, i: s.subtext == t
|
|
113
|
+
)(_subtext)
|
|
114
|
+
|
|
115
|
+
PREDICATES["subtext_set:drained"] = lambda s, i: s.subtext in {"exhaustion", "resignation"}
|
|
116
|
+
PREDICATES["subtext_set:plain"] = lambda s, i: s.subtext in {"question", "statement"}
|
|
117
|
+
PREDICATES["intent:explicit"] = lambda s, i: i is not None and i.explicit
|
|
118
|
+
PREDICATES["intent:clarify"] = lambda s, i: i is not None and i.needs_clarification
|
|
119
|
+
PREDICATES["intent:action_request"] = lambda s, i: i is not None and i.kind == "action_request"
|
|
120
|
+
PREDICATES["intent:status_check"] = lambda s, i: i is not None and i.kind == "status_check"
|
|
121
|
+
PREDICATES["intent:explanation"] = lambda s, i: i is not None and i.kind == "explanation"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
POLICIES: tuple[Policy, ...] = (
|
|
125
|
+
Policy(
|
|
126
|
+
name="execute_request",
|
|
127
|
+
register=Register.DIRECT,
|
|
128
|
+
summary="Act on the explicit request first. Do not paraphrase it back instead of doing it.",
|
|
129
|
+
preconditions=("intent:action_request", "intent:explicit", "subtext_set:plain", "not_escalating_past_n", "not_high_arousal_run"),
|
|
130
|
+
avoid=("Restating the request as if that were progress.", "Inventing missing details."),
|
|
131
|
+
example_opener="İstenen işlemi yap ve sonucu doğrudan bildir.",
|
|
132
|
+
),
|
|
133
|
+
Policy(
|
|
134
|
+
name="report_status",
|
|
135
|
+
register=Register.DIRECT,
|
|
136
|
+
summary="Check the current state and report the observed result, not a guess.",
|
|
137
|
+
preconditions=("intent:status_check", "intent:explicit", "subtext_set:plain", "not_escalating_past_n", "not_high_arousal_run"),
|
|
138
|
+
avoid=("Guessing the status.", "Repeating the plan instead of checking."),
|
|
139
|
+
example_opener="Güncel durum: kontrol edilen sonuç burada.",
|
|
140
|
+
),
|
|
141
|
+
Policy(
|
|
142
|
+
name="explain_request",
|
|
143
|
+
register=Register.DIRECT,
|
|
144
|
+
summary="Explain the requested point directly, preserving the user's actual question.",
|
|
145
|
+
preconditions=("intent:explanation", "intent:explicit", "subtext_set:plain", "not_escalating_past_n", "not_high_arousal_run"),
|
|
146
|
+
avoid=("Turning an explanation request into emotional mirroring.",),
|
|
147
|
+
example_opener="Nedeni şu:",
|
|
148
|
+
),
|
|
149
|
+
Policy(
|
|
150
|
+
name="clarify_request",
|
|
151
|
+
register=Register.ASK,
|
|
152
|
+
summary="The requested action is not identifiable. Ask one question instead of guessing.",
|
|
153
|
+
preconditions=("intent:clarify", "subtext_set:plain", "not_escalating", "not_escalating_past_n"),
|
|
154
|
+
avoid=("Guessing what an unresolved pronoun refers to.", "Question stacking."),
|
|
155
|
+
example_opener="Tam olarak hangi şeyi kastediyorsun?",
|
|
156
|
+
),
|
|
157
|
+
Policy(
|
|
158
|
+
name="mirror_specific",
|
|
159
|
+
register=Register.MIRROR,
|
|
160
|
+
summary="Name the concrete thing the user said, not a category of difficulty.",
|
|
161
|
+
preconditions=("subtext:statement", "not_escalating", "not_escalating_past_n"),
|
|
162
|
+
avoid=("That sounds really tough.", "I understand how you feel."),
|
|
163
|
+
example_opener="Randevunuz üç gündür iki saat ileriye alınıyor, sonra da kırk dakikalık yol var.",
|
|
164
|
+
),
|
|
165
|
+
Policy(
|
|
166
|
+
name="validate_then_redirect",
|
|
167
|
+
register=Register.VALIDATE_REDIRECT,
|
|
168
|
+
summary="The user is right and heated: acknowledge, then move to one concrete step.",
|
|
169
|
+
preconditions=("subtext:challenge", "user_is_right"),
|
|
170
|
+
avoid=("Validating and stopping there.", "Apologising again."),
|
|
171
|
+
example_opener="Rehber değişti, üstüne müdahale edildi. Sonraki adım: yeni metni karşılaştırıp farkı tek cümlede yazalım.",
|
|
172
|
+
),
|
|
173
|
+
Policy(
|
|
174
|
+
name="deflate_tension",
|
|
175
|
+
register=Register.DEFLATE,
|
|
176
|
+
summary="Stacked demands: lower the temperature, do not lower the seriousness.",
|
|
177
|
+
preconditions=("subtext:demand",),
|
|
178
|
+
avoid=("Jokes that minimise a real problem.", "Answering a different demand than the one asked."),
|
|
179
|
+
example_opener="Üç şey soruyorsun, tek tek gideyim. Önce şu: değişen kısım hangi satır?",
|
|
180
|
+
),
|
|
181
|
+
Policy(
|
|
182
|
+
name="direct_no_padding",
|
|
183
|
+
register=Register.DIRECT,
|
|
184
|
+
summary="No warmth before the answer when the user asked a plain question.",
|
|
185
|
+
preconditions=("subtext:question", "not_escalating"),
|
|
186
|
+
avoid=("Opening with empathy before the answer.", "Restating the question."),
|
|
187
|
+
example_opener="Hayır, o dosya senin durumunda çalışmaz.",
|
|
188
|
+
),
|
|
189
|
+
Policy(
|
|
190
|
+
name="ask_one_question",
|
|
191
|
+
register=Register.ASK,
|
|
192
|
+
summary=(
|
|
193
|
+
"After a correction, apply an explicit replacement immediately and resume "
|
|
194
|
+
"the prior task. Ask at most one targeted question only when the replacement "
|
|
195
|
+
"is missing or ambiguous."
|
|
196
|
+
),
|
|
197
|
+
preconditions=("subtext:correction",),
|
|
198
|
+
avoid=(
|
|
199
|
+
"Asking the user to repeat or reconfirm a replacement they already supplied.",
|
|
200
|
+
"Three or more questions.",
|
|
201
|
+
"Questions stacked into an interrogation.",
|
|
202
|
+
),
|
|
203
|
+
example_opener="Düzeltmeyi aldım; verdiğin yeni değeri önceki adıma uyguluyorum.",
|
|
204
|
+
),
|
|
205
|
+
Policy(
|
|
206
|
+
name="repair_hold_position",
|
|
207
|
+
register=Register.REPAIR,
|
|
208
|
+
summary="Pushed repeatedly: keep the position. Neither capitulate nor argue.",
|
|
209
|
+
preconditions=("subtext:challenge", "stance_unknown"),
|
|
210
|
+
avoid=("Total capitulation.", "Defending why you were right.", "Re-explaining the same thing."),
|
|
211
|
+
example_opener="İtirazını gördüm. Önerim yine aynı; gerekçesi değişmedi.",
|
|
212
|
+
),
|
|
213
|
+
Policy(
|
|
214
|
+
name="repair_interrogation",
|
|
215
|
+
register=Register.REPAIR,
|
|
216
|
+
summary="The same question three turns running is not curiosity. Stop restating.",
|
|
217
|
+
preconditions=("subtext:escalating",),
|
|
218
|
+
avoid=("A fourth restatement of the explanation.", "Asking what the user actually needs."),
|
|
219
|
+
example_opener="Üç kez aynı şeyi sordun. Sebebi şu ve değişmiyor: geçersiz tarih.",
|
|
220
|
+
),
|
|
221
|
+
Policy(
|
|
222
|
+
name="hold",
|
|
223
|
+
register=Register.HOLD,
|
|
224
|
+
summary="Energy is spent rather than rising. Say less.",
|
|
225
|
+
preconditions=("subtext_set:drained",),
|
|
226
|
+
avoid=("Continuing to generate reassurance.", "Cheerfulness.",),
|
|
227
|
+
example_opener="Tamam. Düşüneyim.",
|
|
228
|
+
),
|
|
229
|
+
Policy(
|
|
230
|
+
name="boundary",
|
|
231
|
+
register=Register.BOUNDARY,
|
|
232
|
+
summary="Set a privacy boundary without pretending the system can guarantee confidentiality.",
|
|
233
|
+
preconditions=("subtext:disclosure_request",),
|
|
234
|
+
avoid=("Lecturing.", "Promising absolute secrecy or confidentiality."),
|
|
235
|
+
example_opener="Bunu gereksiz yere tekrarlamam; ama mutlak gizlilik garantisi veremem.",
|
|
236
|
+
),
|
|
237
|
+
Policy(
|
|
238
|
+
name="hold_sustained_escalation",
|
|
239
|
+
register=Register.HOLD,
|
|
240
|
+
summary="Three rising turns then a calm one: the calm turn is not a reason to warm back up.",
|
|
241
|
+
preconditions=("escalating_past_n", "not_escalating"),
|
|
242
|
+
avoid=("Returning to a warm register because the last message was polite.",),
|
|
243
|
+
example_opener="Buradayım.",
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
REGISTRY: dict[str, Policy] = {p.name: p for p in POLICIES}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@dataclass
|
|
251
|
+
class Selection:
|
|
252
|
+
policy: Policy
|
|
253
|
+
state: AffectState
|
|
254
|
+
intent: IntentState | None = None
|
|
255
|
+
rationale: str = ""
|
|
256
|
+
runner_up: tuple[str, ...] = ()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@dataclass
|
|
260
|
+
class Selector:
|
|
261
|
+
overrides: dict[str, str] = field(default_factory=dict)
|
|
262
|
+
default: str = "mirror_specific"
|
|
263
|
+
|
|
264
|
+
def select(self, state: AffectState, intent: IntentState | None = None) -> Selection:
|
|
265
|
+
forced = self.overrides.get(state.subtext)
|
|
266
|
+
if forced:
|
|
267
|
+
return Selection(policy=REGISTRY[forced], state=state, intent=intent, rationale=f"override:{forced}")
|
|
268
|
+
|
|
269
|
+
applicable = [p for p in POLICIES if p.applies(state, intent)]
|
|
270
|
+
if not applicable:
|
|
271
|
+
return Selection(policy=REGISTRY[self.default], state=state, intent=intent, rationale="default")
|
|
272
|
+
|
|
273
|
+
best_specificity = max(p.specificity for p in applicable)
|
|
274
|
+
specific = [p for p in applicable if p.specificity == best_specificity]
|
|
275
|
+
best_priority = max(p.priority for p in specific)
|
|
276
|
+
winners = [p for p in specific if p.priority == best_priority]
|
|
277
|
+
|
|
278
|
+
if len(winners) != 1:
|
|
279
|
+
names = ", ".join(sorted(p.name for p in winners))
|
|
280
|
+
raise ValueError(
|
|
281
|
+
"Ambiguous policy selection: equal specificity and priority "
|
|
282
|
+
f"for {names}. Assign an explicit priority or refine preconditions."
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
winner = winners[0]
|
|
286
|
+
return Selection(
|
|
287
|
+
policy=winner,
|
|
288
|
+
state=state,
|
|
289
|
+
intent=intent,
|
|
290
|
+
rationale=(
|
|
291
|
+
f"specificity={best_specificity} priority={best_priority} "
|
|
292
|
+
f"matched={','.join(p.name for p in applicable)}"
|
|
293
|
+
),
|
|
294
|
+
runner_up=tuple(p.name for p in applicable if p is not winner),
|
|
295
|
+
)
|
eq_layer/preferences.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import asdict, dataclass, replace
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
SESSION_SCOPE_PATTERNS = (
|
|
8
|
+
r"\bbundan sonra\b",
|
|
9
|
+
r"\bbu sohbet(?:te| boyunca)\b",
|
|
10
|
+
r"\bher zaman\b",
|
|
11
|
+
r"\bdaima\b",
|
|
12
|
+
r"\bfrom now on\b",
|
|
13
|
+
r"\bfor this conversation\b",
|
|
14
|
+
r"\bthroughout this conversation\b",
|
|
15
|
+
r"\balways\b",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
STRONG_TURKISH_STYLE_PATTERN = re.compile(
|
|
19
|
+
r"\bbenimle\b.*\bkonuş\b",
|
|
20
|
+
re.IGNORECASE,
|
|
21
|
+
)
|
|
22
|
+
STRONG_ENGLISH_STYLE_PATTERN = re.compile(
|
|
23
|
+
r"\b(?:keep|make)\s+(?:your\s+)?responses?\b|\brespond\b.*\b(?:brief|concise|direct)\b",
|
|
24
|
+
re.IGNORECASE,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class SessionPreferences:
|
|
30
|
+
"""Explicit session-scoped preferences, separate from affect.
|
|
31
|
+
|
|
32
|
+
The state is recomputed from the visible transcript. It does not infer
|
|
33
|
+
personality, emotional traits, or cross-session user properties.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
verbosity: str | None = None
|
|
37
|
+
directness: str | None = None
|
|
38
|
+
no_guess: bool = False
|
|
39
|
+
scope_limited: bool = False
|
|
40
|
+
source_turns: tuple[int, ...] = ()
|
|
41
|
+
evidence: tuple[str, ...] = ()
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict:
|
|
44
|
+
return asdict(self)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _normalise(text: str) -> str:
|
|
48
|
+
return " ".join(text.lower().split())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _session_scoped(text: str) -> bool:
|
|
52
|
+
low = _normalise(text)
|
|
53
|
+
if any(re.search(pattern, low) for pattern in SESSION_SCOPE_PATTERNS):
|
|
54
|
+
return True
|
|
55
|
+
return bool(
|
|
56
|
+
STRONG_TURKISH_STYLE_PATTERN.search(low)
|
|
57
|
+
or STRONG_ENGLISH_STYLE_PATTERN.search(low)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _updates(text: str) -> dict:
|
|
62
|
+
low = _normalise(text)
|
|
63
|
+
updates: dict[str, object] = {}
|
|
64
|
+
|
|
65
|
+
if re.search(r"\b(kısa|öz|brief|concise)\b", low):
|
|
66
|
+
updates["verbosity"] = "low"
|
|
67
|
+
if re.search(r"\b(detaylı|ayrıntılı|detailed)\b|\bin detail\b", low):
|
|
68
|
+
updates["verbosity"] = "high"
|
|
69
|
+
|
|
70
|
+
if re.search(r"\b(net|direkt|doğrudan|direct)\b|\bstraight to the point\b", low):
|
|
71
|
+
updates["directness"] = "high"
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
"tahmin etme" in low
|
|
75
|
+
or "uydurma" in low
|
|
76
|
+
or "don't guess" in low
|
|
77
|
+
or "do not guess" in low
|
|
78
|
+
):
|
|
79
|
+
updates["no_guess"] = True
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
"sadece sorduğuma cevap ver" in low
|
|
83
|
+
or "sadece istediğimi yap" in low
|
|
84
|
+
or "only answer what i ask" in low
|
|
85
|
+
or "only do what i ask" in low
|
|
86
|
+
or "stay within scope" in low
|
|
87
|
+
):
|
|
88
|
+
updates["scope_limited"] = True
|
|
89
|
+
|
|
90
|
+
return updates
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def infer_session_preferences(messages: list[dict]) -> SessionPreferences:
|
|
94
|
+
"""Infer only explicit, session-scoped style/control preferences.
|
|
95
|
+
|
|
96
|
+
Later explicit session preferences override earlier conflicting values.
|
|
97
|
+
Ordinary one-turn constraints are intentionally not persisted.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
state = SessionPreferences()
|
|
101
|
+
source_turns: list[int] = []
|
|
102
|
+
evidence: list[str] = []
|
|
103
|
+
|
|
104
|
+
for index, message in enumerate(messages):
|
|
105
|
+
if message.get("role") != "user":
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
text = str(message.get("content", "")).strip()
|
|
109
|
+
if not text or not _session_scoped(text):
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
updates = _updates(text)
|
|
113
|
+
if not updates:
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
state = replace(state, **updates)
|
|
117
|
+
source_turns.append(index)
|
|
118
|
+
for key, value in updates.items():
|
|
119
|
+
evidence.append(f"turn:{index}:{key}={value}")
|
|
120
|
+
|
|
121
|
+
return replace(
|
|
122
|
+
state,
|
|
123
|
+
source_turns=tuple(source_turns),
|
|
124
|
+
evidence=tuple(evidence),
|
|
125
|
+
)
|