Opik version
- Opik version: 2.2.71 (checked on
main at f06298d93c)
- Component: Opik Python SDK —
opik.evaluation.metrics.conversation.llm_judges.g_eval_wrappers
Describe the problem
GEvalConversationMetric.score() documents the rule it applies when picking what to grade:
Only assistant turns with non-empty content are considered.
— sdks/python/src/opik/evaluation/metrics/conversation/llm_judges/g_eval_wrappers.py:96-98
The selector does not implement that rule. It takes the last message whose role is "assistant" and only afterwards checks whether the text is blank:
last_assistant = next(
(turn.get("content", "") for turn in reversed(conversation)
if turn.get("role") == "assistant"),
"",
)
if not last_assistant.strip():
return ScoreResult(value=0.0, reason="Conversation contains no assistant messages to evaluate.",
scoring_failed=True)
So when the closing assistant turn carries no text, the metric gives up instead of grading the answer before it — and the reason it reports is factually wrong, because the conversation does contain assistant messages with text.
Expected: the most recent assistant turn that has text is graded.
Actual: an empty closing turn blocks it; the score comes back scoring_failed=True, value=0.0, reason="Conversation contains no assistant messages to evaluate."
This is not a rare shape. create_conversation_from_traces adds an assistant message whenever the output transform returns anything other than None (sdks/python/src/opik/api_objects/conversation/conversation_factory.py:49-51), so a turn whose text is "" — an agent call that only issued tool calls, an aborted or empty completion, a hand-built transcript ending in a tool turn — is a normal last message. All six public conversation G-Eval metrics inherit the selector: GEvalConversationMetric, ConversationComplianceRiskMetric, ConversationDialogueHelpfulnessMetric, ConversationQARelevanceMetric, ConversationSummarizationCoherenceMetric, ConversationSummarizationConsistencyMetric, ConversationPromptUncertaintyMetric.
Because evaluate_threads drops failed scores rather than storing them (sdks/python/src/opik/evaluation/threads/helpers.py:23-26), the thread silently loses that metric — the experiment aggregate is computed over fewer threads than the user asked for, with nothing in the UI saying so.
Second symptom of the same selector: when the assistant message it selects has a content that is not a string (possible when score() is called directly with hand-built dicts, since ConversationDict is a TypedDict with no runtime enforcement), score() raises instead of returning the failed ScoreResult its own docstring prescribes. The raise happens before the judge call, so it is not captured by the try that handles judge errors.
Reproduction steps and code snippets
No server and no API keys needed — the judge is a stub.
from typing import Any
from opik.evaluation.metrics.base_metric import BaseMetric
from opik.evaluation.metrics.score_result import ScoreResult
from opik.evaluation.metrics.conversation.llm_judges.g_eval_wrappers import GEvalConversationMetric
class StubJudge(BaseMetric):
def __init__(self) -> None:
super().__init__(name="stub_judge", track=False)
def score(self, output: str, **_: Any) -> ScoreResult:
return ScoreResult(name=self.name, value=0.8, reason=f"graded: {output!r}")
metric = GEvalConversationMetric(judge=StubJudge(), name="conversation_stub")
conversation = [
{"role": "user", "content": "Summarise these notes."},
{"role": "assistant", "content": "Summary: timelines and budgets."}, # gradeable
{"role": "user", "content": "Thanks"},
{"role": "assistant", "content": ""}, # tool-only turn
]
print(metric.score(conversation))
Observed on main (f06298d93c):
ScoreResult(name='conversation_stub', value=0.0, reason='Conversation contains no assistant messages to evaluate.', category_name=None, metadata=None, scoring_failed=True)
Expected: value=0.8, reason="graded 'Summary: timelines and budgets.'", scoring_failed=False.
Error logs or stack trace
main at f06298d93c, calling score() with a non-string content on the selected turn:
Traceback (most recent call last):
File "<string>", line 10, in <module>
File ".../sdks/python/src/opik/evaluation/metrics/conversation/llm_judges/g_eval_wrappers.py", line 112, in score
if not last_assistant.strip():
^^^^^^^^^^^^^^^^^^^^
AttributeError: 'int' object has no attribute 'strip'
Opik version
mainatf06298d93c)opik.evaluation.metrics.conversation.llm_judges.g_eval_wrappersDescribe the problem
GEvalConversationMetric.score()documents the rule it applies when picking what to grade:The selector does not implement that rule. It takes the last message whose
roleis"assistant"and only afterwards checks whether the text is blank:So when the closing assistant turn carries no text, the metric gives up instead of grading the answer before it — and the reason it reports is factually wrong, because the conversation does contain assistant messages with text.
Expected: the most recent assistant turn that has text is graded.
Actual: an empty closing turn blocks it; the score comes back
scoring_failed=True,value=0.0,reason="Conversation contains no assistant messages to evaluate."This is not a rare shape.
create_conversation_from_tracesadds an assistant message whenever the output transform returns anything other thanNone(sdks/python/src/opik/api_objects/conversation/conversation_factory.py:49-51), so a turn whose text is""— an agent call that only issued tool calls, an aborted or empty completion, a hand-built transcript ending in a tool turn — is a normal last message. All six public conversation G-Eval metrics inherit the selector:GEvalConversationMetric,ConversationComplianceRiskMetric,ConversationDialogueHelpfulnessMetric,ConversationQARelevanceMetric,ConversationSummarizationCoherenceMetric,ConversationSummarizationConsistencyMetric,ConversationPromptUncertaintyMetric.Because
evaluate_threadsdrops failed scores rather than storing them (sdks/python/src/opik/evaluation/threads/helpers.py:23-26), the thread silently loses that metric — the experiment aggregate is computed over fewer threads than the user asked for, with nothing in the UI saying so.Second symptom of the same selector: when the assistant message it selects has a
contentthat is not a string (possible whenscore()is called directly with hand-built dicts, sinceConversationDictis aTypedDictwith no runtime enforcement),score()raises instead of returning the failedScoreResultits own docstring prescribes. The raise happens before the judge call, so it is not captured by thetrythat handles judge errors.Reproduction steps and code snippets
No server and no API keys needed — the judge is a stub.
Observed on
main(f06298d93c):Expected:
value=0.8,reason="graded 'Summary: timelines and budgets.'",scoring_failed=False.Error logs or stack trace
mainatf06298d93c, callingscore()with a non-stringcontenton the selected turn: