|
@@ -160,6 +160,11 @@ class AssistantService:
|
|
|
self._last_triage_state: tuple[str, str] | None = None
|
|
self._last_triage_state: tuple[str, str] | None = None
|
|
|
self.last_triage: dict | None = None
|
|
self.last_triage: dict | None = None
|
|
|
self._last_triage_window: tuple = ()
|
|
self._last_triage_window: tuple = ()
|
|
|
|
|
+ # Periodic cadence for accumulating speech (no settled turn): armed to
|
|
|
|
|
+ # `now + periodic_s` on the first transcript of a burst and re-armed on
|
|
|
|
|
+ # every accepted submit. None while disarmed (periodic_s <= 0 or after a
|
|
|
|
|
+ # reset), so a long monologue still triages every `periodic_s`.
|
|
|
|
|
+ self._triage_next_due: float | None = None
|
|
|
self._triage_generation = 0
|
|
self._triage_generation = 0
|
|
|
self._last_transcript_lang: str | None = None
|
|
self._last_transcript_lang: str | None = None
|
|
|
self._triage_dump = TriageDumper(bool(getattr(cfg.triage, "dump_io", False)))
|
|
self._triage_dump = TriageDumper(bool(getattr(cfg.triage, "dump_io", False)))
|
|
@@ -800,6 +805,9 @@ class AssistantService:
|
|
|
with self._lock:
|
|
with self._lock:
|
|
|
self._pending_brief.append(f"{self._speaker_display(event)}: {text}")
|
|
self._pending_brief.append(f"{self._speaker_display(event)}: {text}")
|
|
|
self._cap_pending_brief_locked()
|
|
self._cap_pending_brief_locked()
|
|
|
|
|
+ periodic_s = self._periodic_s()
|
|
|
|
|
+ if periodic_s > 0 and self._triage_next_due is None:
|
|
|
|
|
+ self._triage_next_due = now + periodic_s
|
|
|
|
|
|
|
|
if self.modes.mode is Mode.COMMAND_ARMED:
|
|
if self.modes.mode is Mode.COMMAND_ARMED:
|
|
|
ts = float(event.get("ts", now))
|
|
ts = float(event.get("ts", now))
|
|
@@ -904,28 +912,54 @@ class AssistantService:
|
|
|
if reply:
|
|
if reply:
|
|
|
self._emit(CHAT_MESSAGE_EVENT, role="assistant", text=reply)
|
|
self._emit(CHAT_MESSAGE_EVENT, role="assistant", text=reply)
|
|
|
|
|
|
|
|
|
|
+ def _periodic_s(self) -> float:
|
|
|
|
|
+ """Accumulating-speech cadence; <= 0 disables the periodic trigger."""
|
|
|
|
|
+ try:
|
|
|
|
|
+ return float(getattr(self.cfg.triage, "periodic_s", 0.0))
|
|
|
|
|
+ except (TypeError, ValueError):
|
|
|
|
|
+ return 0.0
|
|
|
|
|
+
|
|
|
def _maybe_triage(self) -> None:
|
|
def _maybe_triage(self) -> None:
|
|
|
- """Classify the recent transcribed utterances on any settled turn (independent of chat)."""
|
|
|
|
|
- if not self.turn_ended():
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ """Classify recent utterances on a settled turn, or periodically while speech grows."""
|
|
|
questions = self._triage.questions
|
|
questions = self._triage.questions
|
|
|
if not questions:
|
|
if not questions:
|
|
|
return
|
|
return
|
|
|
if not self._laya_lane.ready():
|
|
if not self._laya_lane.ready():
|
|
|
return
|
|
return
|
|
|
|
|
+ now = self._now()
|
|
|
|
|
+ settled = self.turn_ended()
|
|
|
with self._lock:
|
|
with self._lock:
|
|
|
|
|
+ periodic_s = self._periodic_s()
|
|
|
|
|
+ periodic_due = (
|
|
|
|
|
+ periodic_s > 0
|
|
|
|
|
+ and self._triage_next_due is not None
|
|
|
|
|
+ and now >= self._triage_next_due
|
|
|
|
|
+ )
|
|
|
|
|
+ if not settled and not periodic_due:
|
|
|
|
|
+ return
|
|
|
window = tuple(self.triage_history[-int(self.cfg.triage.window):])
|
|
window = tuple(self.triage_history[-int(self.cfg.triage.window):])
|
|
|
if not window or window == self._last_triage_window:
|
|
if not window or window == self._last_triage_window:
|
|
|
return
|
|
return
|
|
|
generation = self._triage_generation
|
|
generation = self._triage_generation
|
|
|
self._last_triage_window = window
|
|
self._last_triage_window = window
|
|
|
|
|
+ trigger = "settled" if settled else "periodic"
|
|
|
state = self._triage.build_state(window)
|
|
state = self._triage.build_state(window)
|
|
|
if not state:
|
|
if not state:
|
|
|
|
|
+ self._debug_emit("triage-skip", reason="empty_state", generation=generation)
|
|
|
return
|
|
return
|
|
|
accepted = self._laya_lane.submit(
|
|
accepted = self._laya_lane.submit(
|
|
|
state, questions, self._last_transcript_lang, generation
|
|
state, questions, self._last_transcript_lang, generation
|
|
|
)
|
|
)
|
|
|
- if accepted and self._triage_dump.enabled:
|
|
|
|
|
|
|
+ if not accepted:
|
|
|
|
|
+ self._debug_emit("triage-skip", reason="lane_refused", generation=generation)
|
|
|
|
|
+ return
|
|
|
|
|
+ with self._lock:
|
|
|
|
|
+ if periodic_s > 0:
|
|
|
|
|
+ self._triage_next_due = now + periodic_s
|
|
|
|
|
+ self._debug_emit(
|
|
|
|
|
+ "triage-submit", trigger=trigger, window=len(window), generation=generation
|
|
|
|
|
+ )
|
|
|
|
|
+ if self._triage_dump.enabled:
|
|
|
# Newest-wins caveat: the lane is single-flight and does not echo
|
|
# Newest-wins caveat: the lane is single-flight and does not echo
|
|
|
# a request id, so under two submits within one predict the paired
|
|
# a request id, so under two submits within one predict the paired
|
|
|
# request is the most recent submit (acceptable for debug I/O).
|
|
# request is the most recent submit (acceptable for debug I/O).
|
|
@@ -954,10 +988,12 @@ class AssistantService:
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
|
if payload is None:
|
|
if payload is None:
|
|
|
|
|
+ self._debug_emit("triage-result", error="predict_failed", generation=generation)
|
|
|
self._triage_dump.finalize(ts)
|
|
self._triage_dump.finalize(ts)
|
|
|
return
|
|
return
|
|
|
signals = self._triage.normalize(payload)
|
|
signals = self._triage.normalize(payload)
|
|
|
if not signals:
|
|
if not signals:
|
|
|
|
|
+ self._debug_emit("triage-result", error="empty_signals", generation=generation)
|
|
|
self._triage_dump.finalize(ts)
|
|
self._triage_dump.finalize(ts)
|
|
|
return
|
|
return
|
|
|
with self._lock:
|
|
with self._lock:
|
|
@@ -970,6 +1006,12 @@ class AssistantService:
|
|
|
self.last_triage = merged
|
|
self.last_triage = merged
|
|
|
self._last_brief_signals = signals
|
|
self._last_brief_signals = signals
|
|
|
self._emit(TRIAGE_EVENT, stage="signals", triage=merged, ts=ts)
|
|
self._emit(TRIAGE_EVENT, stage="signals", triage=merged, ts=ts)
|
|
|
|
|
+ self._debug_emit(
|
|
|
|
|
+ "triage-result",
|
|
|
|
|
+ items=len(signals.get("order", [])),
|
|
|
|
|
+ escalated=bool(signals.get("escalated")),
|
|
|
|
|
+ generation=generation,
|
|
|
|
|
+ )
|
|
|
cfg = self.cfg.triage
|
|
cfg = self.cfg.triage
|
|
|
gated_in = not getattr(cfg, "brief_gate_enabled", True) or (
|
|
gated_in = not getattr(cfg, "brief_gate_enabled", True) or (
|
|
|
self._triage.should_update_brief(signals)
|
|
self._triage.should_update_brief(signals)
|
|
@@ -981,6 +1023,15 @@ class AssistantService:
|
|
|
):
|
|
):
|
|
|
self._dispatch_brief(signals, ts, generation)
|
|
self._dispatch_brief(signals, ts, generation)
|
|
|
else:
|
|
else:
|
|
|
|
|
+ if self._brief_fn is None:
|
|
|
|
|
+ reason = "no_brief_fn"
|
|
|
|
|
+ elif not getattr(cfg, "extract_enabled", True):
|
|
|
|
|
+ reason = "extract_disabled"
|
|
|
|
|
+ else:
|
|
|
|
|
+ reason = "gate"
|
|
|
|
|
+ self._debug_emit(
|
|
|
|
|
+ "triage-brief", action="skip", reason=reason, generation=generation
|
|
|
|
|
+ )
|
|
|
self._triage_dump.finalize(ts)
|
|
self._triage_dump.finalize(ts)
|
|
|
|
|
|
|
|
def _dispatch_brief(self, signals, ts, generation) -> bool:
|
|
def _dispatch_brief(self, signals, ts, generation) -> bool:
|
|
@@ -1005,6 +1056,9 @@ class AssistantService:
|
|
|
run=lambda: self._run_brief_update(generation, signals, ts, token),
|
|
run=lambda: self._run_brief_update(generation, signals, ts, token),
|
|
|
)
|
|
)
|
|
|
if refusal is not None:
|
|
if refusal is not None:
|
|
|
|
|
+ self._debug_emit(
|
|
|
|
|
+ "triage-brief", action="refuse", reason=refusal, generation=generation
|
|
|
|
|
+ )
|
|
|
self._triage_dump.finalize(ts)
|
|
self._triage_dump.finalize(ts)
|
|
|
return False
|
|
return False
|
|
|
assert job is not None and token is not None
|
|
assert job is not None and token is not None
|
|
@@ -1015,6 +1069,7 @@ class AssistantService:
|
|
|
self._triage_dump.finalize(ts)
|
|
self._triage_dump.finalize(ts)
|
|
|
self._debug_emit("queue-full", job="brief-update")
|
|
self._debug_emit("queue-full", job="brief-update")
|
|
|
return False
|
|
return False
|
|
|
|
|
+ self._debug_emit("triage-brief", action="dispatch", generation=generation)
|
|
|
return True
|
|
return True
|
|
|
|
|
|
|
|
def _run_brief_update(self, generation, signals, ts=None, token=None) -> None:
|
|
def _run_brief_update(self, generation, signals, ts=None, token=None) -> None:
|
|
@@ -1127,6 +1182,7 @@ class AssistantService:
|
|
|
self._triage_generation += 1
|
|
self._triage_generation += 1
|
|
|
self.last_triage = None
|
|
self.last_triage = None
|
|
|
self._last_triage_window = ()
|
|
self._last_triage_window = ()
|
|
|
|
|
+ self._triage_next_due = None
|
|
|
self.triage_history = []
|
|
self.triage_history = []
|
|
|
self._brief_token += 1
|
|
self._brief_token += 1
|
|
|
self._brief_outstanding = False
|
|
self._brief_outstanding = False
|