aniketqxp commited on
Commit
b554e59
·
verified ·
1 Parent(s): 40b1357

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
- acoustic: AcousticContext,
1029
  ) -> list[ManagerQuestion]:
1030
  all_findings = (
1031
  decision.triggered_findings + decision.positive_findings
1032
  )
1033
- objective = [
 
 
 
 
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
- if objective_negative:
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 objective
 
 
 
 
 
1064
  )
1065
  ]
1066
- workflow_negative = [
1067
- item for item in workflow if item.polarity.value == "negative"
 
 
 
 
 
 
 
 
 
 
1068
  ]
1069
- if any(
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.ESCALATION
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
- action = decision.recommended_action
1101
- needs_action = action.action_type != ActionType.NONE
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1102
  return [
1103
  ManagerQuestion(
1104
- question_id="call.objective",
1105
- question="Did the agent understand and address the objective?",
1106
- answer=objective_answer,
1107
- answer_label=objective_label,
1108
  summary=_question_summary(
1109
- objective_negative or objective_positive,
1110
- "The available evidence did not resolve the call objective.",
1111
  ),
1112
- evidence_ids=_finding_evidence_ids(objective),
1113
  ),
1114
  ManagerQuestion(
1115
- question_id="call.workflow",
1116
- question="Did the agent follow the applicable workflow?",
1117
- answer=workflow_answer,
1118
- answer_label=workflow_label,
1119
  summary=(
1120
  _question_summary(
1121
- workflow_negative,
1122
- "No applicable workflow checks were available.",
1123
  )
1124
- if workflow_negative
1125
  else (
1126
- f"All {len(workflow)} applicable workflow checks were "
1127
- "demonstrated."
1128
- if workflow
1129
- else "No applicable workflow checks were available."
1130
  )
1131
  ),
1132
- evidence_ids=_finding_evidence_ids(workflow),
1133
  ),
1134
  ManagerQuestion(
1135
- question_id="call.friction",
1136
- question="Did the interaction introduce customer friction?",
1137
- answer=friction_answer,
1138
- answer_label=friction_label,
1139
  summary=_question_summary(
1140
- friction,
1141
- acoustic.conclusion,
 
 
 
1142
  ),
1143
- evidence_ids=_finding_evidence_ids(friction),
1144
  ),
1145
  ManagerQuestion(
1146
- question_id="call.follow_up",
1147
- question="Is follow-up, coaching, or customer action required?",
1148
- answer=(
1149
- ManagerAnswer.YES if needs_action else ManagerAnswer.NO
1150
- ),
1151
- answer_label="Yes" if needs_action else "No",
1152
- summary=(
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
- for transcript_path in sorted(TRANSCRIPT_ROOT.glob("*.json")):
 
 
 
 
 
 
 
 
 
 
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.2.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.