24 lines
27 KiB
JSON
24 lines
27 KiB
JSON
|
|
[
|
|||
|
|
{
|
|||
|
|
"id": "149b0ac0-9db2-47d0-b674-c453f7c2d37d",
|
|||
|
|
"sort_order": 0,
|
|||
|
|
"rubric_text": "The inbound_receiving_log.xlsx file must contain a data row where: the Expected Pallets and Actual Pallets column both contain 24; the Actual Cases column contains 474; the three Temp Reading columns collectively contain −4.5, −6.0 (alternatively −6), and −7.5 in any order; the Variance (Cases) column contains −6; the Lot Codes Received column contains 'A-20260519-01'; the Disposition column contains both 'quarantine' (or 'quarantined') and 'H-Q'; and the Status column contains exactly 'P2 – Compound event.'",
|
|||
|
|
"verifier_code": "from pathlib import Path\nimport re\n\ndef verify(workspace_path, external_services_path=None):\n ws = Path(workspace_path)\n\n # Find the receiving log xlsx file\n xlsx_files = list(ws.rglob('*.xlsx'))\n log_file = None\n for f in xlsx_files:\n if 'receiv' in f.name.lower() or 'log' in f.name.lower():\n log_file = f\n break\n if log_file is None and xlsx_files:\n log_file = xlsx_files[0]\n\n if log_file is None:\n return {'pass': False, 'score': 0.0, 'feedback': 'No .xlsx file found in workspace.'}\n\n try:\n import openpyxl\n wb = openpyxl.load_workbook(log_file)\n except Exception as e:\n return {'pass': False, 'score': 0.0, 'feedback': f'Failed to open {log_file.name}: {e}'}\n\n best_result = None\n best_score = -1\n\n for ws_sheet in wb.worksheets:\n rows = list(ws_sheet.iter_rows(values_only=True))\n if len(rows) < 2:\n continue\n\n # Find header row\n header_row_idx = None\n headers = []\n for i, row in enumerate(rows):\n non_none = [c for c in row if c is not None]\n if len(non_none) >= 3:\n header_row_idx = i\n headers = [str(c).strip().lower() if c is not None else '' for c in row]\n break\n\n if header_row_idx is None:\n continue\n\n def find_col(patterns):\n results = []\n for idx, h in enumerate(headers):\n for p in patterns:\n if p in h:\n results.append(idx)\n break\n return results\n\n expected_pallet_cols = find_col(['expected pallet'])\n actual_pallet_cols = find_col(['actual pallet'])\n pallet_cols = find_col(['pallet'])\n actual_case_cols = find_col(['actual case'])\n case_cols = find_col(['case'])\n temp_cols = find_col(['temp'])\n variance_cols = find_col(['variance'])\n lot_cols = find_col(['lot'])\n disposition_cols = find_col(['disposition'])\n status_cols = find_col(['status'])\n\n data_rows = rows[header_row_idx + 1:]\n for row in data_rows:\n row_str_list = [str(c).strip() if c is not None else '' for c in row]\n row_lower = ' '.join(row_str_list).lower()\n\n non_empty = [c for c in row if c is not None and str(c).strip() != '']\n if len(non_empty) < 3:\n continue\n\n def get_vals(col_indices):\n vals = []\n for ci in col_indices:\n if ci < len(row) and row[ci] is not None:\n vals.append(row[ci])\n return vals\n\n def num_eq(val, target, tol=0.5):\n try:\n return abs(float(val) - target) < tol\n except (ValueError, TypeError):\n s = str(val).replace('\\u2212', '-').replace('\\u2013', '-').replace('\\u2014', '-')\n m = re.search(r'-?\\d+\\.?\\d*', s)\n if m:\n try:\n return abs(float(m.group()) - target) < tol\n except:\n pass\n return False\n\n # Check Expected Pallets = 24\n expected_pallet_ok = False\n for v in get_vals(expected_pallet_cols):\n if num_eq(v, 24, 0.5):\n expected_pallet_ok = True\n if not expected_pallet_ok:\n for v in get_vals(pallet_cols):\n if num_eq(v, 24, 0.5):\n expected_pallet_ok = True\n break\n if not expected_pallet_ok:\n for v in row:\n if v is not None and num_eq(v, 24, 0.5):\n expected_pallet_ok = True\n break\n\n # Check Actual Pallets = 24\n actual_pallet
|
|||
|
|
"criterion_type": "expected_output"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "a20b335b-2dbd-4e0c-8f52-1500a16fd68f",
|
|||
|
|
"sort_order": 1,
|
|||
|
|
"rubric_text": "A message posted to the #dc-escalations Slack channel must begin with 'P2 escalation. Compound.' [exact string, 'compound event' not acceptable], contain manifest number '2026-05-20-001', contain the exact phrase 'Escalated to Director of Supply Chain.', and contain the exact phrase 'Incident report: awaiting Director decision.' The Detail and Action required fields may contain any text.",
|
|||
|
|
"verifier_code": "from pathlib import Path\nimport json\nimport re\n\ndef verify(workspace_path, external_services_path=None):\n if external_services_path is None:\n return {'pass': False, 'score': 0.0, 'feedback': 'external_services_path is None; cannot check slack_data.json'}\n\n slack_path = Path(external_services_path) / 'slack_data.json'\n if not slack_path.exists():\n return {'pass': False, 'score': 0.0, 'feedback': 'slack_data.json not found at external_services_path'}\n\n with open(slack_path, 'r', encoding='utf-8') as f:\n data = json.load(f)\n\n # Find the #dc-escalations channel\n channels = data.get('channels', {})\n escalation_channel_id = None\n for cid, cinfo in channels.items():\n cname = str(cinfo.get('name', '')).lower().strip()\n if cname == 'dc-escalations':\n escalation_channel_id = cid\n break\n\n if escalation_channel_id is None:\n return {'pass': False, 'score': 0.0, 'feedback': 'Could not find #dc-escalations channel in slack_data.json. Channels found: ' + str([c.get('name', '') for c in channels.values()])}\n\n # Get messages in that channel\n messages_by_channel = data.get('messages', {})\n channel_messages = messages_by_channel.get(escalation_channel_id, [])\n\n if not channel_messages:\n return {'pass': False, 'score': 0.0, 'feedback': f'No messages found in #dc-escalations channel (channel ID: {escalation_channel_id})'}\n\n # Score each message to find the best candidate\n best_msg = None\n best_score = -1\n\n for msg in channel_messages:\n text = str(msg.get('text', ''))\n text_lower = text.lower()\n score = 0\n if text_lower.lstrip().startswith('p2 escalation'):\n score += 5\n if '2026-05-20-001' in text:\n score += 5\n if 'compound' in text_lower:\n score += 3\n if 'director of supply chain' in text_lower:\n score += 3\n if 'awaiting director decision' in text_lower:\n score += 3\n if score > best_score:\n best_score = score\n best_msg = msg\n\n if best_msg is None or best_score <= 0:\n texts_preview = [str(m.get('text', ''))[:80] for m in channel_messages[:5]]\n return {'pass': False, 'score': 0.0, 'feedback': f'No plausible escalation message found in #dc-escalations among {len(channel_messages)} messages. Previews: {texts_preview}'}\n\n text = str(best_msg.get('text', ''))\n text_lower = text.lower()\n\n failures = []\n details = []\n\n # (1) Must begin with 'P2 escalation. Compound.' [exact string, 'compound event' not acceptable]\n # The rubric says exact string 'P2 escalation. Compound.' at the start.\n # We check case-insensitively for leniency, and allow minor whitespace variations,\n # but 'compound' must appear as a standalone word (not 'compound event' replacing it).\n # The key distinction: must say 'Compound' (with period/punctuation after), NOT 'Compound event'.\n start_pattern = re.compile(r'(?i)^\\s*p2\\s+escalation\\.\\s*compound\\s*\\.', re.DOTALL)\n if start_pattern.search(text):\n # Additionally verify it doesn't say 'compound event' right at the start\n bad_pattern = re.compile(r'(?i)^\\s*p2\\s+escalation\\.\\s*compound\\s+event', re.DOTALL)\n if bad_pattern.search(text):\n failures.append(f\"(1) Message begins with 'P2 escalation. Compound event...' but rubric requires exactly 'P2 escalation. Compound.' (not 'compound event'). Start: '{text[:80]}...'\")\n else:\n details.append(\"(1) Message begins with 'P2 escalation. Compound.' \\u2713\")\n else:\n failures.append(f\"(1) Message must begin with 'P2 escalation. Compound.' but starts with: '{text[:80]}...'\")\n\n # (2) Must contain manifest number '2026-05-20-001'\n if '2026-05-20-001' in text:\n details.append(\"(2) Manifest number '2026-05-20-001' found \\u2713\")\n else:\n failures.append(\"(2) Manifest number '2026-05-20
|
|||
|
|
"criterion_type": "expected_output"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "98ee41bc-57a2-4c54-a5c1-57bf70a8aee9",
|
|||
|
|
"sort_order": 2,
|
|||
|
|
"rubric_text": "A sent email in mailbox.json must be addressed To: daniel.reyes@prairiestar.com only, CC: carlos.mendoza@prairiestar.com and jacob.patel@prairiestar.com with no additional recipients, have a subject containing 'P2', 'Compound Event', '2026-05-20-001', and '2026-05-20', and have a body containing all three temperature readings −4.5, −6.0 (alternatively −6), and −7.5, 'Tier 3', variance of −6 cases and 1.25%, 'Moderate' as the variance classification, 'quarantined to H-Q', and 'Director assessment required' [exact string].",
|
|||
|
|
"verifier_code": "from pathlib import Path\nimport json\nimport re\n\ndef verify(workspace_path, external_services_path=None):\n if external_services_path is None:\n return {'pass': False, 'score': 0.0, 'feedback': 'external_services_path is None; cannot check mailbox.json'}\n\n mailbox_path = Path(external_services_path) / 'mailbox.json'\n if not mailbox_path.exists():\n return {'pass': False, 'score': 0.0, 'feedback': 'mailbox.json not found'}\n\n with open(mailbox_path, 'r', encoding='utf-8') as f:\n data = json.load(f)\n\n sent_emails = [e for e in data.get('emails', []) if e.get('folder', '').lower() == 'sent']\n\n if not sent_emails:\n return {'pass': False, 'score': 0.0, 'feedback': 'No sent emails found in mailbox.json'}\n\n # Find the escalation email - look for one addressed to daniel.reyes\n matching_email = None\n for email in sent_emails:\n to_addr = (email.get('to_addr') or email.get('to') or '').lower()\n if 'daniel.reyes@prairiestar.com' in to_addr:\n matching_email = email\n break\n\n if matching_email is None:\n # Fallback: look for email with P2 in subject or relevant content\n for email in sent_emails:\n subject = (email.get('subject') or '').lower()\n body = (email.get('body_text') or email.get('body') or '').lower()\n if 'p2' in subject or ('quarantine' in body and 'h-q' in body):\n matching_email = email\n break\n\n if matching_email is None:\n return {'pass': False, 'score': 0.0, 'feedback': 'No escalation email found in sent folder addressed to daniel.reyes@prairiestar.com'}\n\n failures = []\n\n # --- Check TO field ---\n to_addr = (matching_email.get('to_addr') or matching_email.get('to') or '').lower().strip()\n if 'daniel.reyes@prairiestar.com' not in to_addr:\n failures.append('To field does not contain daniel.reyes@prairiestar.com')\n else:\n to_emails = re.findall(r'[\\w.+-]+@[\\w.-]+', to_addr)\n to_emails_filtered = [e for e in to_emails if e != 'daniel.reyes@prairiestar.com']\n if to_emails_filtered:\n failures.append(f'To field contains additional recipients beyond daniel.reyes@prairiestar.com: {to_emails_filtered}')\n\n # --- Check CC field ---\n cc_addr = (matching_email.get('cc_addr') or matching_email.get('cc') or '').lower().strip()\n cc_emails = re.findall(r'[\\w.+-]+@[\\w.-]+', cc_addr)\n required_cc = {'carlos.mendoza@prairiestar.com', 'jacob.patel@prairiestar.com'}\n cc_set = set(cc_emails)\n missing_cc = required_cc - cc_set\n extra_cc = cc_set - required_cc\n if missing_cc:\n failures.append(f'CC field missing required recipients: {missing_cc}')\n if extra_cc:\n failures.append(f'CC field contains extra recipients: {extra_cc}')\n\n # --- Check Subject ---\n subject = matching_email.get('subject') or ''\n subject_lower = subject.lower()\n if 'p2' not in subject_lower:\n failures.append('Subject does not contain P2')\n if 'compound event' not in subject_lower:\n failures.append('Subject does not contain Compound Event')\n if '2026-05-20-001' not in subject:\n failures.append('Subject does not contain 2026-05-20-001')\n if '2026-05-20' not in subject:\n failures.append('Subject does not contain 2026-05-20')\n\n # --- Check Body ---\n body = matching_email.get('body_text') or matching_email.get('body') or ''\n # Normalize unicode minus signs and en-dashes to regular minus/hyphen\n normalized_body = body.replace('\\u2212', '-').replace('\\u2013', '-').replace('\\u2014', '-').replace('\\u00b0', ' ')\n normalized_lower = normalized_body.lower()\n\n # Temperature readings: -4.5, -6.0 (or -6), -7.5\n if not re.search(r'[-]\\s*4\\.5', normalized_lower):\n failures.append('Body does not contain temperature reading -4.5 (or \\u22124.5)')\n\n # -6.0 or -6 (but distinguish from -6 in other contexts like variance)\n has_minus6 = boo
|
|||
|
|
"criterion_type": "expected_output"
|
|||
|
|
}
|
|||
|
|
]
|