Spaces:
Running on Zero
Running on Zero
fix: project evaluator dashboard outcomes
Browse files
backend/app/routers/calls.py
CHANGED
|
@@ -279,21 +279,79 @@ def _evaluation_summary(public_id, domain, run=None):
|
|
| 279 |
|
| 280 |
acoustic = presentation.get("acoustic_context") or {}
|
| 281 |
action = presentation.get("recommended_action") or {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
return {
|
| 283 |
"analyzed": available,
|
| 284 |
"evaluation_available": available,
|
| 285 |
"evaluation_supported": evaluator_supported,
|
| 286 |
"evaluation_state": state,
|
| 287 |
-
"evaluation_status":
|
| 288 |
-
presentation.get("evaluation_status")
|
| 289 |
-
or decision.get("decision_status")
|
| 290 |
-
or ("failed" if run_status == "failed" else None)
|
| 291 |
-
),
|
| 292 |
"attention_required": (
|
| 293 |
bool(presentation.get("attention_required"))
|
| 294 |
if available
|
| 295 |
else None
|
| 296 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
"checklist_counts": checklist_counts,
|
| 298 |
"checklist_total": len(checklist),
|
| 299 |
"acoustic_status": acoustic.get("status"),
|
|
|
|
| 279 |
|
| 280 |
acoustic = presentation.get("acoustic_context") or {}
|
| 281 |
action = presentation.get("recommended_action") or {}
|
| 282 |
+
questions = {
|
| 283 |
+
item.get("question_id"): item
|
| 284 |
+
for item in presentation.get("manager_questions") or []
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
def aspect(question_id):
|
| 288 |
+
item = questions.get(question_id) or {}
|
| 289 |
+
answer = item.get("answer")
|
| 290 |
+
if answer == "yes":
|
| 291 |
+
state = "ok"
|
| 292 |
+
elif answer in {"partly", "no"}:
|
| 293 |
+
state = "concern"
|
| 294 |
+
else:
|
| 295 |
+
state = "uncertain"
|
| 296 |
+
return {
|
| 297 |
+
"state": state,
|
| 298 |
+
"summary": item.get("summary"),
|
| 299 |
+
"evidence_ids": item.get("evidence_ids") or [],
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
aspects = {
|
| 303 |
+
name: aspect(f"call.{name}")
|
| 304 |
+
for name in ("request", "process", "experience", "outcome")
|
| 305 |
+
}
|
| 306 |
+
evaluation_status = (
|
| 307 |
+
presentation.get("evaluation_status")
|
| 308 |
+
or decision.get("decision_status")
|
| 309 |
+
or ("failed" if run_status == "failed" else None)
|
| 310 |
+
)
|
| 311 |
+
if available and bool(presentation.get("attention_required")):
|
| 312 |
+
result = "review"
|
| 313 |
+
elif available and (
|
| 314 |
+
evaluation_status != "complete"
|
| 315 |
+
or any(item["state"] == "uncertain" for item in aspects.values())
|
| 316 |
+
):
|
| 317 |
+
result = "uncertain"
|
| 318 |
+
elif available:
|
| 319 |
+
result = "ok"
|
| 320 |
+
else:
|
| 321 |
+
result = None
|
| 322 |
+
|
| 323 |
+
evidence = [
|
| 324 |
+
*(presentation.get("evidence") or []),
|
| 325 |
+
*((presentation.get("details") or {}).get("evidence") or []),
|
| 326 |
+
]
|
| 327 |
return {
|
| 328 |
"analyzed": available,
|
| 329 |
"evaluation_available": available,
|
| 330 |
"evaluation_supported": evaluator_supported,
|
| 331 |
"evaluation_state": state,
|
| 332 |
+
"evaluation_status": evaluation_status,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
"attention_required": (
|
| 334 |
bool(presentation.get("attention_required"))
|
| 335 |
if available
|
| 336 |
else None
|
| 337 |
),
|
| 338 |
+
"result": result,
|
| 339 |
+
"aspects": aspects if available else None,
|
| 340 |
+
"concern_count": (
|
| 341 |
+
len(presentation.get("primary_reasons") or [])
|
| 342 |
+
+ int(presentation.get("additional_reason_count") or 0)
|
| 343 |
+
if available
|
| 344 |
+
else None
|
| 345 |
+
),
|
| 346 |
+
"audio_warning": (
|
| 347 |
+
any(
|
| 348 |
+
item.get("kind") == "audio_support"
|
| 349 |
+
and "reason" in (item.get("purposes") or [])
|
| 350 |
+
for item in evidence
|
| 351 |
+
)
|
| 352 |
+
if available
|
| 353 |
+
else False
|
| 354 |
+
),
|
| 355 |
"checklist_counts": checklist_counts,
|
| 356 |
"checklist_total": len(checklist),
|
| 357 |
"acoustic_status": acoustic.get("status"),
|
ml-services/evaluation/v2/build_evaluation_data.py
CHANGED
|
@@ -554,6 +554,7 @@ def _challenge_call(
|
|
| 554 |
facts={
|
| 555 |
"request.executes_transaction": True,
|
| 556 |
"transfer.executed_during_call": True,
|
|
|
|
| 557 |
},
|
| 558 |
acoustic_condition=acoustic,
|
| 559 |
segments=segments,
|
|
|
|
| 554 |
facts={
|
| 555 |
"request.executes_transaction": True,
|
| 556 |
"transfer.executed_during_call": True,
|
| 557 |
+
"policy.requires_identity_challenge": True,
|
| 558 |
},
|
| 559 |
acoustic_condition=acoustic,
|
| 560 |
segments=segments,
|
ml-services/evaluation/v2/findings.py
CHANGED
|
@@ -217,6 +217,26 @@ TEXT_ESCALATION_RULES = (
|
|
| 217 |
r"(?:frustrated|angry|upset)\b",
|
| 218 |
r"\bthis\s+is\s+(?:completely\s+|absolutely\s+)?"
|
| 219 |
r"unacceptable\b",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
),
|
| 221 |
),
|
| 222 |
)
|
|
|
|
| 217 |
r"(?:frustrated|angry|upset)\b",
|
| 218 |
r"\bthis\s+is\s+(?:completely\s+|absolutely\s+)?"
|
| 219 |
r"unacceptable\b",
|
| 220 |
+
r"\b(?:this|it)\s+is\s+(?:really\s+|absolutely\s+)?"
|
| 221 |
+
r"ridiculous\b",
|
| 222 |
+
r"\bthis\s+is\s+(?:honestly\s+)?(?:a\s+)?nightmare\b",
|
| 223 |
+
r"\b(?:was|is|has\s+been)\s+stressing\s+me\s+out\s+"
|
| 224 |
+
r"(?:tremendously|badly)\b",
|
| 225 |
+
),
|
| 226 |
+
),
|
| 227 |
+
TextEscalationRule(
|
| 228 |
+
finding_type="experience.accessibility_barrier",
|
| 229 |
+
title="Customer reported an access barrier",
|
| 230 |
+
business_definition=(
|
| 231 |
+
"The customer explicitly states that a physical-access barrier "
|
| 232 |
+
"prevents them from using the offered service path."
|
| 233 |
+
),
|
| 234 |
+
severity=FindingSeverity.REVIEW,
|
| 235 |
+
category=FindingCategory.AGENT_BEHAVIOR,
|
| 236 |
+
patterns=_patterns(
|
| 237 |
+
r"\b(?:can't|cannot|unable\s+to)\s+(?:go|visit)\s+"
|
| 238 |
+
r"(?:to\s+)?(?:the\s+)?(?:bank|branch)\s+physically\b",
|
| 239 |
+
r"\bi(?:'m|\s+am)\s+(?:currently\s+)?in\s+a\s+wheelchair\b",
|
| 240 |
),
|
| 241 |
),
|
| 242 |
)
|
ml-services/evaluation/v2/presentation.py
CHANGED
|
@@ -1025,142 +1025,167 @@ def _question_summary(
|
|
| 1025 |
|
| 1026 |
def _manager_questions(
|
| 1027 |
decision: CallDecision,
|
| 1028 |
-
|
| 1029 |
) -> list[ManagerQuestion]:
|
| 1030 |
all_findings = (
|
| 1031 |
decision.triggered_findings + decision.positive_findings
|
| 1032 |
)
|
| 1033 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1034 |
item
|
| 1035 |
for item in all_findings
|
| 1036 |
-
if
|
| 1037 |
-
item.applicability.rule_id == "request.intent_confirmed"
|
| 1038 |
-
or item.category == FindingCategory.OUTCOME
|
| 1039 |
-
)
|
| 1040 |
-
]
|
| 1041 |
-
objective_negative = [
|
| 1042 |
-
item for item in objective if item.polarity.value == "negative"
|
| 1043 |
-
]
|
| 1044 |
-
objective_positive = [
|
| 1045 |
-
item for item in objective if item.polarity.value == "positive"
|
| 1046 |
]
|
| 1047 |
-
|
| 1048 |
-
objective_answer = ManagerAnswer.PARTLY
|
| 1049 |
-
objective_label = "Partly"
|
| 1050 |
-
elif objective_positive:
|
| 1051 |
-
objective_answer = ManagerAnswer.YES
|
| 1052 |
-
objective_label = "Yes"
|
| 1053 |
-
else:
|
| 1054 |
-
objective_answer = ManagerAnswer.UNCLEAR
|
| 1055 |
-
objective_label = "Unable to determine"
|
| 1056 |
-
|
| 1057 |
-
workflow = [
|
| 1058 |
item
|
| 1059 |
for item in all_findings
|
| 1060 |
if (
|
| 1061 |
item.detection_rule.detector
|
| 1062 |
== "structured_requirement_assessment"
|
| 1063 |
-
and item not in
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1064 |
)
|
| 1065 |
]
|
| 1066 |
-
|
| 1067 |
-
item
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1068 |
]
|
| 1069 |
-
|
| 1070 |
-
item.severity.value == "critical"
|
| 1071 |
-
for item in workflow_negative
|
| 1072 |
-
):
|
| 1073 |
-
workflow_answer = ManagerAnswer.NO
|
| 1074 |
-
workflow_label = "No"
|
| 1075 |
-
elif workflow_negative:
|
| 1076 |
-
workflow_answer = ManagerAnswer.PARTLY
|
| 1077 |
-
workflow_label = "Partly"
|
| 1078 |
-
elif workflow:
|
| 1079 |
-
workflow_answer = ManagerAnswer.YES
|
| 1080 |
-
workflow_label = "Yes"
|
| 1081 |
-
else:
|
| 1082 |
-
workflow_answer = ManagerAnswer.UNCLEAR
|
| 1083 |
-
workflow_label = "Unable to determine"
|
| 1084 |
-
|
| 1085 |
-
friction = [
|
| 1086 |
item
|
| 1087 |
for item in all_findings
|
| 1088 |
-
if item.category == FindingCategory.
|
| 1089 |
]
|
| 1090 |
-
if friction:
|
| 1091 |
-
friction_answer = ManagerAnswer.YES
|
| 1092 |
-
friction_label = "Concern identified"
|
| 1093 |
-
elif acoustic.status == AcousticStatus.AVAILABLE:
|
| 1094 |
-
friction_answer = ManagerAnswer.NO
|
| 1095 |
-
friction_label = "No concern identified"
|
| 1096 |
-
else:
|
| 1097 |
-
friction_answer = ManagerAnswer.UNCLEAR
|
| 1098 |
-
friction_label = "Unable to determine"
|
| 1099 |
|
| 1100 |
-
|
| 1101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1102 |
return [
|
| 1103 |
ManagerQuestion(
|
| 1104 |
-
question_id="call.
|
| 1105 |
-
question="
|
| 1106 |
-
answer=
|
| 1107 |
-
answer_label=
|
| 1108 |
summary=_question_summary(
|
| 1109 |
-
|
| 1110 |
-
"The available evidence did not resolve the
|
| 1111 |
),
|
| 1112 |
-
evidence_ids=_finding_evidence_ids(
|
| 1113 |
),
|
| 1114 |
ManagerQuestion(
|
| 1115 |
-
question_id="call.
|
| 1116 |
-
question="
|
| 1117 |
-
answer=
|
| 1118 |
-
answer_label=
|
| 1119 |
summary=(
|
| 1120 |
_question_summary(
|
| 1121 |
-
|
| 1122 |
-
"No applicable
|
| 1123 |
)
|
| 1124 |
-
if
|
| 1125 |
else (
|
| 1126 |
-
f"
|
| 1127 |
-
"
|
| 1128 |
-
if
|
| 1129 |
-
else "No applicable
|
| 1130 |
)
|
| 1131 |
),
|
| 1132 |
-
evidence_ids=_finding_evidence_ids(
|
| 1133 |
),
|
| 1134 |
ManagerQuestion(
|
| 1135 |
-
question_id="call.
|
| 1136 |
-
question="
|
| 1137 |
-
answer=
|
| 1138 |
-
answer_label=
|
| 1139 |
summary=_question_summary(
|
| 1140 |
-
|
| 1141 |
-
|
|
|
|
|
|
|
|
|
|
| 1142 |
),
|
| 1143 |
-
evidence_ids=_finding_evidence_ids(
|
| 1144 |
),
|
| 1145 |
ManagerQuestion(
|
| 1146 |
-
question_id="call.
|
| 1147 |
-
question="
|
| 1148 |
-
answer=
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
|
| 1153 |
-
action.label
|
| 1154 |
-
if needs_action
|
| 1155 |
-
else "No evidence-backed action was triggered."
|
| 1156 |
-
),
|
| 1157 |
-
evidence_ids=_finding_evidence_ids(
|
| 1158 |
-
[
|
| 1159 |
-
item
|
| 1160 |
-
for item in decision.triggered_findings
|
| 1161 |
-
if item.finding_id in action.finding_ids
|
| 1162 |
-
]
|
| 1163 |
),
|
|
|
|
| 1164 |
),
|
| 1165 |
]
|
| 1166 |
|
|
|
|
| 1025 |
|
| 1026 |
def _manager_questions(
|
| 1027 |
decision: CallDecision,
|
| 1028 |
+
_acoustic: AcousticContext,
|
| 1029 |
) -> list[ManagerQuestion]:
|
| 1030 |
all_findings = (
|
| 1031 |
decision.triggered_findings + decision.positive_findings
|
| 1032 |
)
|
| 1033 |
+
request_ids = {
|
| 1034 |
+
"request.intent_confirmed",
|
| 1035 |
+
"loan.purpose_and_stage_confirmed",
|
| 1036 |
+
}
|
| 1037 |
+
request = [
|
| 1038 |
item
|
| 1039 |
for item in all_findings
|
| 1040 |
+
if item.applicability.rule_id in request_ids
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1041 |
]
|
| 1042 |
+
process = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1043 |
item
|
| 1044 |
for item in all_findings
|
| 1045 |
if (
|
| 1046 |
item.detection_rule.detector
|
| 1047 |
== "structured_requirement_assessment"
|
| 1048 |
+
and item.applicability.rule_id not in request_ids
|
| 1049 |
+
and item.category
|
| 1050 |
+
not in (
|
| 1051 |
+
FindingCategory.OUTCOME,
|
| 1052 |
+
FindingCategory.AGENT_BEHAVIOR,
|
| 1053 |
+
)
|
| 1054 |
)
|
| 1055 |
]
|
| 1056 |
+
experience = [
|
| 1057 |
+
item
|
| 1058 |
+
for item in all_findings
|
| 1059 |
+
if (
|
| 1060 |
+
item.category == FindingCategory.ESCALATION
|
| 1061 |
+
or item.finding_type.startswith("experience.")
|
| 1062 |
+
or (
|
| 1063 |
+
item.category == FindingCategory.AGENT_BEHAVIOR
|
| 1064 |
+
and item.detection_rule.detector
|
| 1065 |
+
== "structured_requirement_assessment"
|
| 1066 |
+
)
|
| 1067 |
+
)
|
| 1068 |
]
|
| 1069 |
+
outcome = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1070 |
item
|
| 1071 |
for item in all_findings
|
| 1072 |
+
if item.category == FindingCategory.OUTCOME
|
| 1073 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1074 |
|
| 1075 |
+
def answer(
|
| 1076 |
+
findings: list[Finding],
|
| 1077 |
+
*,
|
| 1078 |
+
clear_when_empty: bool = False,
|
| 1079 |
+
) -> tuple[ManagerAnswer, str]:
|
| 1080 |
+
negative = [
|
| 1081 |
+
item
|
| 1082 |
+
for item in findings
|
| 1083 |
+
if item.polarity.value == "negative"
|
| 1084 |
+
]
|
| 1085 |
+
positive = [
|
| 1086 |
+
item
|
| 1087 |
+
for item in findings
|
| 1088 |
+
if item.polarity.value == "positive"
|
| 1089 |
+
]
|
| 1090 |
+
if any(
|
| 1091 |
+
item.severity.value == "critical"
|
| 1092 |
+
for item in negative
|
| 1093 |
+
):
|
| 1094 |
+
return ManagerAnswer.NO, "No"
|
| 1095 |
+
if negative:
|
| 1096 |
+
return ManagerAnswer.PARTLY, "Partly"
|
| 1097 |
+
if positive or (
|
| 1098 |
+
clear_when_empty
|
| 1099 |
+
and decision.decision_status == DecisionStatus.COMPLETE
|
| 1100 |
+
):
|
| 1101 |
+
return ManagerAnswer.YES, "Yes"
|
| 1102 |
+
return ManagerAnswer.UNCLEAR, "Unable to determine"
|
| 1103 |
+
|
| 1104 |
+
request_answer, request_label = answer(request)
|
| 1105 |
+
process_answer, process_label = answer(process)
|
| 1106 |
+
experience_answer, experience_label = answer(
|
| 1107 |
+
experience,
|
| 1108 |
+
clear_when_empty=True,
|
| 1109 |
+
)
|
| 1110 |
+
outcome_answer, outcome_label = answer(outcome)
|
| 1111 |
+
|
| 1112 |
+
request_negative = [
|
| 1113 |
+
item for item in request if item.polarity.value == "negative"
|
| 1114 |
+
]
|
| 1115 |
+
request_positive = [
|
| 1116 |
+
item for item in request if item.polarity.value == "positive"
|
| 1117 |
+
]
|
| 1118 |
+
process_negative = [
|
| 1119 |
+
item for item in process if item.polarity.value == "negative"
|
| 1120 |
+
]
|
| 1121 |
+
experience_negative = [
|
| 1122 |
+
item for item in experience if item.polarity.value == "negative"
|
| 1123 |
+
]
|
| 1124 |
+
experience_positive = [
|
| 1125 |
+
item for item in experience if item.polarity.value == "positive"
|
| 1126 |
+
]
|
| 1127 |
+
outcome_negative = [
|
| 1128 |
+
item for item in outcome if item.polarity.value == "negative"
|
| 1129 |
+
]
|
| 1130 |
+
outcome_positive = [
|
| 1131 |
+
item for item in outcome if item.polarity.value == "positive"
|
| 1132 |
+
]
|
| 1133 |
return [
|
| 1134 |
ManagerQuestion(
|
| 1135 |
+
question_id="call.request",
|
| 1136 |
+
question="Was the customer's request understood?",
|
| 1137 |
+
answer=request_answer,
|
| 1138 |
+
answer_label=request_label,
|
| 1139 |
summary=_question_summary(
|
| 1140 |
+
request_negative or request_positive,
|
| 1141 |
+
"The available evidence did not resolve the request.",
|
| 1142 |
),
|
| 1143 |
+
evidence_ids=_finding_evidence_ids(request),
|
| 1144 |
),
|
| 1145 |
ManagerQuestion(
|
| 1146 |
+
question_id="call.process",
|
| 1147 |
+
question="Was the applicable process followed?",
|
| 1148 |
+
answer=process_answer,
|
| 1149 |
+
answer_label=process_label,
|
| 1150 |
summary=(
|
| 1151 |
_question_summary(
|
| 1152 |
+
process_negative,
|
| 1153 |
+
"No applicable process checks were available.",
|
| 1154 |
)
|
| 1155 |
+
if process_negative
|
| 1156 |
else (
|
| 1157 |
+
f"{len(process)} applicable process checks were "
|
| 1158 |
+
"supported by transcript evidence."
|
| 1159 |
+
if process
|
| 1160 |
+
else "No applicable process checks were available."
|
| 1161 |
)
|
| 1162 |
),
|
| 1163 |
+
evidence_ids=_finding_evidence_ids(process),
|
| 1164 |
),
|
| 1165 |
ManagerQuestion(
|
| 1166 |
+
question_id="call.experience",
|
| 1167 |
+
question="Was the customer experience handled appropriately?",
|
| 1168 |
+
answer=experience_answer,
|
| 1169 |
+
answer_label=experience_label,
|
| 1170 |
summary=_question_summary(
|
| 1171 |
+
experience_negative or experience_positive,
|
| 1172 |
+
(
|
| 1173 |
+
"No customer-experience concern requiring review was "
|
| 1174 |
+
"identified in the transcript."
|
| 1175 |
+
),
|
| 1176 |
),
|
| 1177 |
+
evidence_ids=_finding_evidence_ids(experience),
|
| 1178 |
),
|
| 1179 |
ManagerQuestion(
|
| 1180 |
+
question_id="call.outcome",
|
| 1181 |
+
question="Was the outcome clear and complete?",
|
| 1182 |
+
answer=outcome_answer,
|
| 1183 |
+
answer_label=outcome_label,
|
| 1184 |
+
summary=_question_summary(
|
| 1185 |
+
outcome_negative or outcome_positive,
|
| 1186 |
+
"The available evidence did not resolve the call outcome.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1187 |
),
|
| 1188 |
+
evidence_ids=_finding_evidence_ids(outcome),
|
| 1189 |
),
|
| 1190 |
]
|
| 1191 |
|
ml-services/evaluation/v2/run_shadow_batch.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import argparse
|
| 4 |
import json
|
|
|
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
from .runtime import (
|
|
@@ -50,7 +51,21 @@ def main() -> int:
|
|
| 50 |
default=[],
|
| 51 |
help="Call ID to rerun even when its existing artifact succeeded.",
|
| 52 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
args = parser.parse_args()
|
|
|
|
|
|
|
| 54 |
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 55 |
|
| 56 |
counts: dict[str, int] = {}
|
|
@@ -64,7 +79,17 @@ def main() -> int:
|
|
| 64 |
legacy_attention = 0
|
| 65 |
v2_attention = 0
|
| 66 |
rows = []
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
call_id = transcript_path.stem
|
| 69 |
legacy_path = LEGACY_ROOT / f"{call_id}_graph.json"
|
| 70 |
sentiment_path = (
|
|
@@ -105,6 +130,11 @@ def main() -> int:
|
|
| 105 |
json.dumps(run.model_dump(mode="json"), indent=2) + "\n",
|
| 106 |
encoding="utf-8",
|
| 107 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
counts[run.status.value] = counts.get(run.status.value, 0) + 1
|
| 109 |
if run.legacy_proxy and run.legacy_proxy.attention_required:
|
| 110 |
legacy_attention += 1
|
|
|
|
| 2 |
|
| 3 |
import argparse
|
| 4 |
import json
|
| 5 |
+
import time
|
| 6 |
from pathlib import Path
|
| 7 |
|
| 8 |
from .runtime import (
|
|
|
|
| 51 |
default=[],
|
| 52 |
help="Call ID to rerun even when its existing artifact succeeded.",
|
| 53 |
)
|
| 54 |
+
parser.add_argument(
|
| 55 |
+
"--only-call",
|
| 56 |
+
action="append",
|
| 57 |
+
default=[],
|
| 58 |
+
help="Restrict the batch to one or more call IDs.",
|
| 59 |
+
)
|
| 60 |
+
parser.add_argument(
|
| 61 |
+
"--delay-seconds",
|
| 62 |
+
type=float,
|
| 63 |
+
default=0.0,
|
| 64 |
+
help="Pause between provider calls to respect token rate limits.",
|
| 65 |
+
)
|
| 66 |
args = parser.parse_args()
|
| 67 |
+
if args.delay_seconds < 0:
|
| 68 |
+
parser.error("--delay-seconds cannot be negative")
|
| 69 |
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 70 |
|
| 71 |
counts: dict[str, int] = {}
|
|
|
|
| 79 |
legacy_attention = 0
|
| 80 |
v2_attention = 0
|
| 81 |
rows = []
|
| 82 |
+
transcript_paths = sorted(TRANSCRIPT_ROOT.glob("*.json"))
|
| 83 |
+
if args.only_call:
|
| 84 |
+
selected = set(args.only_call)
|
| 85 |
+
transcript_paths = [
|
| 86 |
+
path for path in transcript_paths if path.stem in selected
|
| 87 |
+
]
|
| 88 |
+
missing = sorted(selected - {path.stem for path in transcript_paths})
|
| 89 |
+
if missing:
|
| 90 |
+
parser.error(f"unknown --only-call values: {missing}")
|
| 91 |
+
|
| 92 |
+
for transcript_index, transcript_path in enumerate(transcript_paths):
|
| 93 |
call_id = transcript_path.stem
|
| 94 |
legacy_path = LEGACY_ROOT / f"{call_id}_graph.json"
|
| 95 |
sentiment_path = (
|
|
|
|
| 130 |
json.dumps(run.model_dump(mode="json"), indent=2) + "\n",
|
| 131 |
encoding="utf-8",
|
| 132 |
)
|
| 133 |
+
if (
|
| 134 |
+
args.delay_seconds
|
| 135 |
+
and transcript_index < len(transcript_paths) - 1
|
| 136 |
+
):
|
| 137 |
+
time.sleep(args.delay_seconds)
|
| 138 |
counts[run.status.value] = counts.get(run.status.value, 0) + 1
|
| 139 |
if run.legacy_proxy and run.legacy_proxy.attention_required:
|
| 140 |
legacy_attention += 1
|
ml-services/evaluation/v2/semantic_assessment.py
CHANGED
|
@@ -24,7 +24,7 @@ from .schemas import (
|
|
| 24 |
SourceProvenance,
|
| 25 |
)
|
| 26 |
|
| 27 |
-
SEMANTIC_ASSESSOR_VERSION = "0.
|
| 28 |
_TIERS = ("qa-primary", "qa-fallback", "qa-safety")
|
| 29 |
SYSTEM_PROMPT = """You evaluate a customer-service transcript against a fixed
|
| 30 |
list of applicable business requirements.
|
|
@@ -33,6 +33,25 @@ Rules:
|
|
| 33 |
- Assess every supplied requirement exactly once.
|
| 34 |
- Use only the supplied requirement IDs and transcript segment IDs.
|
| 35 |
- MET requires direct transcript evidence that demonstrates the requirement.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
- INCORRECT means the agent directly gave wrong information or performed the
|
| 37 |
applicable behavior incorrectly. It requires direct transcript evidence of
|
| 38 |
what the agent said or did.
|
|
|
|
| 24 |
SourceProvenance,
|
| 25 |
)
|
| 26 |
|
| 27 |
+
SEMANTIC_ASSESSOR_VERSION = "0.3.0"
|
| 28 |
_TIERS = ("qa-primary", "qa-fallback", "qa-safety")
|
| 29 |
SYSTEM_PROMPT = """You evaluate a customer-service transcript against a fixed
|
| 30 |
list of applicable business requirements.
|
|
|
|
| 33 |
- Assess every supplied requirement exactly once.
|
| 34 |
- Use only the supplied requirement IDs and transcript segment IDs.
|
| 35 |
- MET requires direct transcript evidence that demonstrates the requirement.
|
| 36 |
+
- Treat each evidence expectation as a claim that must be supported. A general
|
| 37 |
+
acknowledgment does not satisfy a requirement whose expectations name
|
| 38 |
+
specific details.
|
| 39 |
+
- For MET, cite the substantive agent statement and any customer response
|
| 40 |
+
needed to prove agreement. Do not cite the customer's request as proof that
|
| 41 |
+
the agent confirmed, explained, authorized, or completed it.
|
| 42 |
+
- A later statement cannot prove that an earlier required authorization
|
| 43 |
+
occurred. Respect the sequence of the cited segments.
|
| 44 |
+
- If the transcript contains materially conflicting statements about an
|
| 45 |
+
amount, account, timing, fee, schedule, or outcome, return INCORRECT unless
|
| 46 |
+
the agent clearly corrects the statement before action and the customer
|
| 47 |
+
confirms the corrected details.
|
| 48 |
+
- Source and destination accounts, amount, timing or schedule, authorization,
|
| 49 |
+
and completion are separate requirements. Evidence for one must not be
|
| 50 |
+
reused as a shortcut for another.
|
| 51 |
+
- Completion requires an explicit statement that the action succeeded or was
|
| 52 |
+
scheduled. An intention such as "I will do that" is not completion.
|
| 53 |
+
- Outcome and next steps require a closing recap of what happened and any
|
| 54 |
+
remaining customer action. An earlier explanation alone is insufficient.
|
| 55 |
- INCORRECT means the agent directly gave wrong information or performed the
|
| 56 |
applicable behavior incorrectly. It requires direct transcript evidence of
|
| 57 |
what the agent said or did.
|