maskil commited on
Commit
47f4bfd
Β·
verified Β·
1 Parent(s): 4836e99

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +304 -14
app.py CHANGED
@@ -33,6 +33,7 @@ RATING_CHOICES = ["Bad (0)", "Borderline (1)", "Perfect (2)"]
33
  RATING_MAP = {"Bad (0)": 0, "Borderline (1)": 1, "Perfect (2)": 2}
34
  LABEL_NAMES = ["Bad", "Borderline", "Perfect"]
35
  BAR_COLORS = ["#ef4444", "#f59e0b", "#22c55e"]
 
36
 
37
  # ═════════════════════════════════════════════════════════════════════════
38
  # Globals
@@ -289,7 +290,19 @@ def check_consensus_and_route(doc_id):
289
  MONGO_COL.update_one({'_id': doc_id}, {'$set': {'status': 'conflict'}})
290
 
291
 
 
 
 
 
 
 
 
 
 
 
 
292
  def route_completed(doc_id):
 
293
  if MONGO_DB is None:
294
  return
295
  doc = MONGO_COL.find_one({'_id': doc_id})
@@ -301,22 +314,28 @@ def route_completed(doc_id):
301
  routed = {k: v for k, v in doc.items() if k != '_id'}
302
  routed['original_id'] = doc['_id']
303
 
304
- hd, hr, ha = consensus.get('data_quality'), consensus.get('regular_fit_quality'), consensus.get('ai_fit_quality')
305
- md, mr, ma = mc.get('data_quality'), mc.get('regular_fit_quality'), mc.get('ai_fit_quality')
306
-
307
- clf_match = (hd == md and hr == mr and ha == ma)
308
  try:
309
- tgt = 'experiments_clf_success' if clf_match else 'experiments_clf_failed'
 
 
 
 
 
310
  MONGO_DB[tgt].update_one({'original_id': doc['_id']}, {'$set': routed}, upsert=True)
311
  except Exception as e:
312
  print(f"Route clf err: {e}")
313
 
 
314
  if not overlap:
 
 
315
  try:
316
- if ha == 2 and hr is not None and hr <= 1:
317
  MONGO_DB['experiments_seed_success_reg_failed'].update_one(
318
  {'original_id': doc['_id']}, {'$set': routed}, upsert=True)
319
- if hr == 2 and ha is not None and ha <= 1:
320
  MONGO_DB['experiments_reg_success_seed_failed'].update_one(
321
  {'original_id': doc['_id']}, {'$set': routed}, upsert=True)
322
  except Exception as e:
@@ -397,9 +416,10 @@ def fetch_next(username):
397
  return _empty_ws("❌ MongoDB not connected.")
398
 
399
  try:
 
400
  doc = MONGO_COL.find_one(
401
  {'status': 'pending', 'tagged_by': {'$nin': [username]}},
402
- sort=[('num_votes', ASCENDING)])
403
  except Exception as e:
404
  return _empty_ws(f"❌ DB query failed: {e}")
405
 
@@ -477,16 +497,58 @@ def submit_feedback(username, doc_id, overlap, model_preds,
477
  eval_doc['fit_quality'] = {'regular_fit': fqr, 'ai_fit': fqa}
478
  eval_doc['fit_comment'] = {'regular_fit': c_reg or '', 'ai_fit': c_ai or ''}
479
 
 
 
 
 
 
 
 
 
 
 
480
  try:
481
- MONGO_COL.update_one({'_id': doc_id}, {
482
  '$push': {'evaluations': eval_doc},
483
  '$inc': {'num_votes': 1},
484
  '$addToSet': {'tagged_by': username},
485
- })
 
 
 
 
 
486
  updated = MONGO_COL.find_one({'_id': doc_id})
487
- if updated and updated.get('num_votes', 0) >= VOTES_REQUIRED:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
  check_consensus_and_route(doc_id)
489
- status = "βœ… Feedback saved!"
 
 
490
  except Exception as e:
491
  status = f"❌ Save error: {e}"
492
 
@@ -910,6 +972,234 @@ def build_ui():
910
  return demo
911
 
912
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
913
  if __name__ == '__main__':
914
- demo = build_ui()
915
- demo.launch(css=CSS, js=TOGGLE_JS)
 
 
 
 
 
 
 
 
 
33
  RATING_MAP = {"Bad (0)": 0, "Borderline (1)": 1, "Perfect (2)": 2}
34
  LABEL_NAMES = ["Bad", "Borderline", "Perfect"]
35
  BAR_COLORS = ["#ef4444", "#f59e0b", "#22c55e"]
36
+ FAST_TRACK_THRESHOLD = 0.90 # Min confidence for 1-vote completion
37
 
38
  # ═════════════════════════════════════════════════════════════════════════
39
  # Globals
 
290
  MONGO_COL.update_one({'_id': doc_id}, {'$set': {'status': 'conflict'}})
291
 
292
 
293
+ def _max_deviation(consensus, model_classes):
294
+ """Calculate the maximum abs difference between human consensus and model prediction."""
295
+ max_diff = 0
296
+ for key in ('data_quality', 'regular_fit_quality', 'ai_fit_quality'):
297
+ h = consensus.get(key)
298
+ m = model_classes.get(key)
299
+ if h is not None and m is not None:
300
+ max_diff = max(max_diff, abs(h - m))
301
+ return max_diff
302
+
303
+
304
  def route_completed(doc_id):
305
+ """Route completed doc to granular collections based on human-model deviation."""
306
  if MONGO_DB is None:
307
  return
308
  doc = MONGO_COL.find_one({'_id': doc_id})
 
314
  routed = {k: v for k, v in doc.items() if k != '_id'}
315
  routed['original_id'] = doc['_id']
316
 
317
+ # --- Classification routing (granular) ---
318
+ dev = _max_deviation(consensus, mc)
 
 
319
  try:
320
+ if dev == 0:
321
+ tgt = 'experiments_clf_success'
322
+ elif dev >= 2:
323
+ tgt = 'experiments_clf_catastrophic_failure'
324
+ else:
325
+ tgt = 'experiments_clf_failed'
326
  MONGO_DB[tgt].update_one({'original_id': doc['_id']}, {'$set': routed}, upsert=True)
327
  except Exception as e:
328
  print(f"Route clf err: {e}")
329
 
330
+ # --- Seed vs Regular edge-case routing ---
331
  if not overlap:
332
+ hd = consensus.get('regular_fit_quality')
333
+ ha = consensus.get('ai_fit_quality')
334
  try:
335
+ if ha == 2 and hd is not None and hd <= 1:
336
  MONGO_DB['experiments_seed_success_reg_failed'].update_one(
337
  {'original_id': doc['_id']}, {'$set': routed}, upsert=True)
338
+ if hd == 2 and ha is not None and ha <= 1:
339
  MONGO_DB['experiments_reg_success_seed_failed'].update_one(
340
  {'original_id': doc['_id']}, {'$set': routed}, upsert=True)
341
  except Exception as e:
 
416
  return _empty_ws("❌ MongoDB not connected.")
417
 
418
  try:
419
+ # Smart queue: prioritize experiments flagged needs_second_opinion
420
  doc = MONGO_COL.find_one(
421
  {'status': 'pending', 'tagged_by': {'$nin': [username]}},
422
+ sort=[('needs_second_opinion', -1), ('num_votes', ASCENDING)])
423
  except Exception as e:
424
  return _empty_ws(f"❌ DB query failed: {e}")
425
 
 
497
  eval_doc['fit_quality'] = {'regular_fit': fqr, 'ai_fit': fqa}
498
  eval_doc['fit_comment'] = {'regular_fit': c_reg or '', 'ai_fit': c_ai or ''}
499
 
500
+ # Determine if this evaluation contains any manual corrections
501
+ all_sources = [dq['source']]
502
+ if overlap:
503
+ all_sources.append(eval_doc['fit_quality']['combined']['source'])
504
+ else:
505
+ all_sources.append(eval_doc['fit_quality']['regular_fit']['source'])
506
+ all_sources.append(eval_doc['fit_quality']['ai_fit']['source'])
507
+ has_correction = any(s == 'manual_correction' for s in all_sources)
508
+ all_implicit = all(s == 'implicit_agreement' for s in all_sources)
509
+
510
  try:
511
+ update_ops = {
512
  '$push': {'evaluations': eval_doc},
513
  '$inc': {'num_votes': 1},
514
  '$addToSet': {'tagged_by': username},
515
+ }
516
+ # Flag for smart queue prioritization if user corrected the model
517
+ if has_correction:
518
+ update_ops['$set'] = {'needs_second_opinion': True}
519
+
520
+ MONGO_COL.update_one({'_id': doc_id}, update_ops)
521
  updated = MONGO_COL.find_one({'_id': doc_id})
522
+ votes = updated.get('num_votes', 0) if updated else 0
523
+
524
+ # --- Fast-Track: 1-vote completion ---
525
+ if votes == 1 and all_implicit and model_preds:
526
+ probs = model_preds or {}
527
+ dp = probs.get('data_probs') or []
528
+ rfp = probs.get('regular_fit_probs') or []
529
+ afp = probs.get('ai_fit_probs') or rfp # overlap β†’ same as regular
530
+
531
+ high_conf = (max(dp, default=0) >= FAST_TRACK_THRESHOLD
532
+ and max(rfp, default=0) >= FAST_TRACK_THRESHOLD
533
+ and max(afp, default=0) >= FAST_TRACK_THRESHOLD)
534
+
535
+ if high_conf:
536
+ scores = _extract_scores(eval_doc)
537
+ consensus = {'data_quality': scores['data'],
538
+ 'regular_fit_quality': scores['reg_fit'],
539
+ 'ai_fit_quality': scores['ai_fit']}
540
+ MONGO_COL.update_one({'_id': doc_id},
541
+ {'$set': {'status': 'completed', 'final_consensus': consensus,
542
+ 'fast_tracked': True}})
543
+ route_completed(doc_id)
544
+ status = "⚑ Fast-tracked! High-confidence implicit agreement."
545
+ else:
546
+ status = "βœ… Saved. Awaiting 2nd vote."
547
+ elif votes >= VOTES_REQUIRED:
548
  check_consensus_and_route(doc_id)
549
+ status = "βœ… Feedback saved!"
550
+ else:
551
+ status = "βœ… Saved. Awaiting 2nd vote."
552
  except Exception as e:
553
  status = f"❌ Save error: {e}"
554
 
 
972
  return demo
973
 
974
 
975
+ # ═════════════════════════════════════════════════════════════════════════
976
+ # Validation Tests (run with: python app.py --test)
977
+ # ═════════════════════════════════════════════════════════════════════════
978
+
979
+ def run_validation_tests():
980
+ """Verify MLOps logic: fast-track, catastrophic routing, smart queue."""
981
+ print("=" * 60)
982
+ print("VALIDATION TESTS")
983
+ print("=" * 60)
984
+ passed = 0
985
+ failed = 0
986
+
987
+ def check(name, condition):
988
+ nonlocal passed, failed
989
+ if condition:
990
+ print(f" βœ… {name}")
991
+ passed += 1
992
+ else:
993
+ print(f" ❌ {name}")
994
+ failed += 1
995
+
996
+ # --- 1. _max_deviation ---
997
+ print("\n1. Catastrophic Failure Detection (_max_deviation)")
998
+ check("Perfect match = 0",
999
+ _max_deviation({'data_quality': 2, 'regular_fit_quality': 2, 'ai_fit_quality': 2},
1000
+ {'data_quality': 2, 'regular_fit_quality': 2, 'ai_fit_quality': 2}) == 0)
1001
+ check("Off-by-1 = 1",
1002
+ _max_deviation({'data_quality': 2, 'regular_fit_quality': 1, 'ai_fit_quality': 2},
1003
+ {'data_quality': 2, 'regular_fit_quality': 2, 'ai_fit_quality': 2}) == 1)
1004
+ check("Catastrophic (2β†’0) = 2",
1005
+ _max_deviation({'data_quality': 0, 'regular_fit_quality': 2, 'ai_fit_quality': 2},
1006
+ {'data_quality': 2, 'regular_fit_quality': 2, 'ai_fit_quality': 2}) == 2)
1007
+ check("Catastrophic (0β†’2) = 2",
1008
+ _max_deviation({'data_quality': 2, 'regular_fit_quality': 2, 'ai_fit_quality': 2},
1009
+ {'data_quality': 0, 'regular_fit_quality': 2, 'ai_fit_quality': 2}) == 2)
1010
+ check("Mixed: worst wins (data diff=2, fit diff=1) = 2",
1011
+ _max_deviation({'data_quality': 0, 'regular_fit_quality': 1, 'ai_fit_quality': 2},
1012
+ {'data_quality': 2, 'regular_fit_quality': 2, 'ai_fit_quality': 2}) == 2)
1013
+ check("Missing keys handled gracefully",
1014
+ _max_deviation({'data_quality': 2}, {'regular_fit_quality': 0}) == 0)
1015
+
1016
+ # --- 2. Fast-Track confidence check ---
1017
+ print("\n2. Fast-Track Confidence Logic")
1018
+ high_probs = {'data_probs': [0.02, 0.03, 0.95],
1019
+ 'regular_fit_probs': [0.01, 0.05, 0.94],
1020
+ 'ai_fit_probs': [0.03, 0.02, 0.95]}
1021
+ dp = high_probs['data_probs']
1022
+ rfp = high_probs['regular_fit_probs']
1023
+ afp = high_probs['ai_fit_probs']
1024
+ check("All β‰₯ 0.90 β†’ fast-track eligible",
1025
+ max(dp) >= FAST_TRACK_THRESHOLD and max(rfp) >= FAST_TRACK_THRESHOLD and max(afp) >= FAST_TRACK_THRESHOLD)
1026
+
1027
+ low_probs = {'data_probs': [0.1, 0.3, 0.6],
1028
+ 'regular_fit_probs': [0.01, 0.05, 0.94],
1029
+ 'ai_fit_probs': [0.03, 0.02, 0.95]}
1030
+ dp2 = low_probs['data_probs']
1031
+ check("Data conf < 0.90 β†’ NOT eligible",
1032
+ not (max(dp2) >= FAST_TRACK_THRESHOLD))
1033
+
1034
+ # --- 3. _resolve_rating implicit vs manual ---
1035
+ print("\n3. Implicit Agreement vs Manual Correction")
1036
+ mp = {'model_classes': {'data_quality': 2, 'regular_fit_quality': 1, 'ai_fit_quality': 0}}
1037
+ r1 = _resolve_rating(None, mp, 'data_quality')
1038
+ check("None input β†’ implicit_agreement, score=2",
1039
+ r1 == {'score': 2, 'source': 'implicit_agreement'})
1040
+ r2 = _resolve_rating("Bad (0)", mp, 'data_quality')
1041
+ check("'Bad (0)' input β†’ manual_correction, score=0",
1042
+ r2 == {'score': 0, 'source': 'manual_correction'})
1043
+
1044
+ # --- 4. Smart Queue needs_second_opinion ---
1045
+ print("\n4. Smart Queue Prioritization")
1046
+ sources_all_implicit = ['implicit_agreement', 'implicit_agreement']
1047
+ sources_with_correction = ['implicit_agreement', 'manual_correction']
1048
+ check("All implicit β†’ no needs_second_opinion flag",
1049
+ not any(s == 'manual_correction' for s in sources_all_implicit))
1050
+ check("Has correction β†’ needs_second_opinion = True",
1051
+ any(s == 'manual_correction' for s in sources_with_correction))
1052
+
1053
+ # --- 5. Username normalization ---
1054
+ print("\n5. Username Normalization")
1055
+ check("'aDI mASKIL' β†’ 'Adi Maskil'", 'aDI mASKIL'.strip().title() == 'Adi Maskil')
1056
+ check("'bob' β†’ 'Bob'", 'bob'.strip().title() == 'Bob')
1057
+ check("'ALICE SMITH' β†’ 'Alice Smith'", 'ALICE SMITH'.strip().title() == 'Alice Smith')
1058
+
1059
+ print(f"\n{'=' * 60}")
1060
+ print(f"RESULTS: {passed} passed, {failed} failed")
1061
+ print(f"{'=' * 60}")
1062
+ return failed == 0
1063
+
1064
+
1065
+ def run_mongodb_routing_test():
1066
+ """E2E test: inject mock data β†’ route β†’ verify collections exist in Atlas."""
1067
+ print("=" * 60)
1068
+ print("MONGODB E2E ROUTING TEST")
1069
+ print("=" * 60)
1070
+
1071
+ # 1. Connect using same logic as init_globals
1072
+ client = None
1073
+ for kw in [dict(tlsCAFile=certifi.where()),
1074
+ dict(tls=True, tlsAllowInvalidCertificates=True)]:
1075
+ try:
1076
+ client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000, **kw)
1077
+ client.admin.command('ping')
1078
+ break
1079
+ except Exception:
1080
+ client = None
1081
+ if not client:
1082
+ print("❌ Cannot connect to MongoDB. Aborting.")
1083
+ return False
1084
+
1085
+ db = client[DB_NAME]
1086
+ test_col = db['experiments_test_mock']
1087
+ print(f"βœ“ Connected to {DB_NAME}")
1088
+
1089
+ # 2. Clean up previous test data
1090
+ test_col.delete_many({})
1091
+ for col_name in ['experiments_clf_success', 'experiments_clf_failed',
1092
+ 'experiments_clf_catastrophic_failure',
1093
+ 'experiments_seed_success_reg_failed',
1094
+ 'experiments_reg_success_seed_failed']:
1095
+ db[col_name].delete_many({'original_id': {'$regex': '^e2e_test_'}})
1096
+ print("βœ“ Cleaned previous test data")
1097
+
1098
+ # 3. Insert mock documents for each routing scenario
1099
+ test_cases = [
1100
+ {
1101
+ 'name': 'Catastrophic Failure (model=2, human=0)',
1102
+ '_id': 'e2e_test_catastrophic',
1103
+ 'model_predictions': {
1104
+ 'model_classes': {'data_quality': 2, 'regular_fit_quality': 2, 'ai_fit_quality': 2},
1105
+ 'data_probs': [0.01, 0.02, 0.97],
1106
+ 'regular_fit_probs': [0.02, 0.03, 0.95],
1107
+ 'ai_fit_probs': [0.01, 0.04, 0.95],
1108
+ },
1109
+ 'final_consensus': {'data_quality': 0, 'regular_fit_quality': 0, 'ai_fit_quality': 0},
1110
+ 'curves_overlap': True,
1111
+ 'status': 'completed',
1112
+ 'expected_collection': 'experiments_clf_catastrophic_failure',
1113
+ },
1114
+ {
1115
+ 'name': 'Minor Failure (model=2, human=1)',
1116
+ '_id': 'e2e_test_minor_fail',
1117
+ 'model_predictions': {
1118
+ 'model_classes': {'data_quality': 2, 'regular_fit_quality': 2, 'ai_fit_quality': 2},
1119
+ 'data_probs': [0.01, 0.02, 0.97],
1120
+ 'regular_fit_probs': [0.02, 0.03, 0.95],
1121
+ 'ai_fit_probs': [0.01, 0.04, 0.95],
1122
+ },
1123
+ 'final_consensus': {'data_quality': 1, 'regular_fit_quality': 2, 'ai_fit_quality': 2},
1124
+ 'curves_overlap': True,
1125
+ 'status': 'completed',
1126
+ 'expected_collection': 'experiments_clf_failed',
1127
+ },
1128
+ {
1129
+ 'name': 'Perfect Match (model=human)',
1130
+ '_id': 'e2e_test_success',
1131
+ 'model_predictions': {
1132
+ 'model_classes': {'data_quality': 2, 'regular_fit_quality': 2, 'ai_fit_quality': 2},
1133
+ 'data_probs': [0.01, 0.02, 0.97],
1134
+ 'regular_fit_probs': [0.02, 0.03, 0.95],
1135
+ 'ai_fit_probs': [0.01, 0.04, 0.95],
1136
+ },
1137
+ 'final_consensus': {'data_quality': 2, 'regular_fit_quality': 2, 'ai_fit_quality': 2},
1138
+ 'curves_overlap': True,
1139
+ 'status': 'completed',
1140
+ 'expected_collection': 'experiments_clf_success',
1141
+ },
1142
+ ]
1143
+
1144
+ # Insert all test docs into the test collection
1145
+ for tc in test_cases:
1146
+ doc = {k: v for k, v in tc.items() if k not in ('name', 'expected_collection')}
1147
+ doc['raw_data'] = {'measured_data': {'x_values': [0, 1], 'y_values': [0, 1]}}
1148
+ doc['evaluations'] = []
1149
+ doc['tagged_by'] = []
1150
+ doc['num_votes'] = 2
1151
+ test_col.insert_one(doc)
1152
+ print(f"βœ“ Inserted {len(test_cases)} mock documents into experiments_test_mock")
1153
+
1154
+ # 4. Run routing on each mock doc using the REAL route_completed logic
1155
+ # We temporarily point MONGO_COL and MONGO_DB to our test setup
1156
+ global MONGO_COL, MONGO_DB
1157
+ original_col = MONGO_COL
1158
+ original_db = MONGO_DB
1159
+ MONGO_COL = test_col
1160
+ MONGO_DB = db
1161
+
1162
+ print("\nRouting documents...")
1163
+ for tc in test_cases:
1164
+ route_completed(tc['_id'])
1165
+ print(f" β†’ Routed: {tc['name']}")
1166
+
1167
+ # 5. Verify each document landed in the correct collection
1168
+ print("\nChecking for collection creation...")
1169
+ all_passed = True
1170
+ for tc in test_cases:
1171
+ target = tc['expected_collection']
1172
+ found = db[target].find_one({'original_id': tc['_id']})
1173
+ if found:
1174
+ print(f" βœ… {target} contains '{tc['name']}'")
1175
+ else:
1176
+ print(f" ❌ {target} MISSING '{tc['name']}'")
1177
+ all_passed = False
1178
+
1179
+ # 6. List all collections to confirm they exist in Atlas
1180
+ print("\nCollections in database:")
1181
+ for name in sorted(db.list_collection_names()):
1182
+ count = db[name].count_documents({})
1183
+ print(f" πŸ“ {name} ({count} docs)")
1184
+
1185
+ # Restore globals
1186
+ MONGO_COL = original_col
1187
+ MONGO_DB = original_db
1188
+
1189
+ print(f"\n{'=' * 60}")
1190
+ print(f"E2E RESULT: {'ALL PASSED βœ…' if all_passed else 'FAILURES DETECTED ❌'}")
1191
+ print(f"{'=' * 60}")
1192
+ return all_passed
1193
+
1194
+
1195
  if __name__ == '__main__':
1196
+ import sys
1197
+ if '--test' in sys.argv:
1198
+ success = run_validation_tests()
1199
+ sys.exit(0 if success else 1)
1200
+ elif '--e2e' in sys.argv:
1201
+ success = run_mongodb_routing_test()
1202
+ sys.exit(0 if success else 1)
1203
+ else:
1204
+ demo = build_ui()
1205
+ demo.launch(css=CSS, js=TOGGLE_JS)