sdks/python/src/opik/evaluation/metrics/conversation/heuristics/degeneration/metric.py:100-104
prev_tokens: Optional[List[str]] = None
for content in assistant_turns:
tokens = _tokenize(content)
if not tokens:
continue
_tokenize is re.findall(r"\b\w+\b", text.lower()), which returns [] for any assistant turn whose content is punctuation-only ("...", "???", "!!!") or whitespace-only. That turn is then skipped completely: it contributes nothing to degeneracy_scores, per_turn_metadata, peak_score, or average_score, and it doesn't even update prev_tokens for the next real turn's overlap calculation. It is as if the turn never happened.
The class docstring and the published docs both describe the opposite behaviour. The docstring (metric.py:29-37) says the metric "inspects each assistant turn" so callers "can quickly flag sections where the assistant got stuck or stopped being helpful," and conversation_threads_metrics.mdx:39 calls it "a lightweight guard against models that fall into loops or short-circuit the dialogue." A punctuation-only reply is the textbook case of an assistant that stopped being helpful / short-circuited the dialogue — the one case this guard exists for — yet it is invisible to the scoring loop.
What happens (reproduced on the real module)
ConversationDegenerationMetric.score() (metric.py:100-104) tokenizes each assistant turn with _tokenize (word-boundary regex) and does if not tokens: continue for any turn whose content is punctuation- or whitespace-only. Such a turn does not increment degeneracy_scores, per_turn_metadata, or prev_tokens.
A conversation that degenerates partway through, e.g. one real reply followed by several '...'/'???'/'!!!' turns, therefore reports a near-zero peak/average score computed only over the surviving real turns. This contradicts the class docstring ('inspects each assistant turn... can quickly flag sections where the assistant got stuck or stopped being helpful') and the public docs ('a lightweight guard against models that fall into loops or short-circuit the dialogue', conversation_threads_metrics.mdx:39).
Nothing else in the code compensates:
- No caller in
evaluate_threads/score_statistics/evaluation_result or the ConversationThreadMetric base class filters, pads, or otherwise guards against this.
- No existing test exercises the mixed real+garbage-turn case.
- The all-garbage path (
MetricComputationError when every turn tokenizes empty) shows the codebase already treats token-less content as a distinct, meaningful case elsewhere, just not consistently.
Fix: either score a token-less turn as maximal degeneration risk, or at minimum record the count of excluded turns in ScoreResult.metadata so threshold consumers aren't silently blind to them.
Happy to open the PR.
sdks/python/src/opik/evaluation/metrics/conversation/heuristics/degeneration/metric.py:100-104_tokenizeisre.findall(r"\b\w+\b", text.lower()), which returns[]for any assistant turn whose content is punctuation-only ("...","???","!!!") or whitespace-only. That turn is then skipped completely: it contributes nothing todegeneracy_scores,per_turn_metadata,peak_score, oraverage_score, and it doesn't even updateprev_tokensfor the next real turn's overlap calculation. It is as if the turn never happened.The class docstring and the published docs both describe the opposite behaviour. The docstring (
metric.py:29-37) says the metric "inspects each assistant turn" so callers "can quickly flag sections where the assistant got stuck or stopped being helpful," andconversation_threads_metrics.mdx:39calls it "a lightweight guard against models that fall into loops or short-circuit the dialogue." A punctuation-only reply is the textbook case of an assistant that stopped being helpful / short-circuited the dialogue — the one case this guard exists for — yet it is invisible to the scoring loop.What happens (reproduced on the real module)
ConversationDegenerationMetric.score()(metric.py:100-104) tokenizes each assistant turn with_tokenize(word-boundary regex) and doesif not tokens: continuefor any turn whose content is punctuation- or whitespace-only. Such a turn does not incrementdegeneracy_scores,per_turn_metadata, orprev_tokens.A conversation that degenerates partway through, e.g. one real reply followed by several '...'/'???'/'!!!' turns, therefore reports a near-zero peak/average score computed only over the surviving real turns. This contradicts the class docstring ('inspects each assistant turn... can quickly flag sections where the assistant got stuck or stopped being helpful') and the public docs ('a lightweight guard against models that fall into loops or short-circuit the dialogue',
conversation_threads_metrics.mdx:39).Nothing else in the code compensates:
evaluate_threads/score_statistics/evaluation_resultor theConversationThreadMetricbase class filters, pads, or otherwise guards against this.MetricComputationErrorwhen every turn tokenizes empty) shows the codebase already treats token-less content as a distinct, meaningful case elsewhere, just not consistently.Fix: either score a token-less turn as maximal degeneration risk, or at minimum record the count of excluded turns in
ScoreResult.metadataso threshold consumers aren't silently blind to them.Happy to open the PR.