-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodels.py
More file actions
728 lines (639 loc) · 27.8 KB
/
Copy pathmodels.py
File metadata and controls
728 lines (639 loc) · 27.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
""""
Project: SVE (ref implementation: Harry Potter Trivia)
Pydantic Schemas (Phase 2+)
"""
import math
import json
import hashlib
import base64
from enum import Enum
from typing import List, Optional, Self, Dict, Literal, TYPE_CHECKING, Any
from pydantic import BaseModel, field_validator, Field, model_validator, ConfigDict, ValidationInfo
import numpy as np
from core.constants import QuestionType, QuestionSource, AnswerType
class DataTier(str, Enum):
"""
Enum class to distinguish between legacy and synthetic questions source.
"""
BRONZE = "bronze"
SILVER = "silver"
GOLD = "gold"
PROD_GREEN = "production_green"
PROD_BLUE = "production_blue"
# Basic schema for all question types to inherit
class BaseQuestion(BaseModel):
"""
The foundational schema defining fields common to all trivia question types.
All specific question formats (Standard, MCQ) inherit from this structure
to ensure core metadata (category, difficulty, sources) is always present.
"""
# makes enums serialize as their values
model_config = ConfigDict(use_enum_values=True)
question_type: QuestionType
question_source: QuestionSource
question: str
answer: str
answer_variations: List[str]
hint_1: str
hint_2: str
hint_3: str
explanation: str
semantic_entity_refs: List[str]
semantic_lore_concepts: List[str]
@field_validator('*', mode='before')
@classmethod
def convert_numpy_arrays(cls, v):
"""Convert any numpy array field to a Python list"""
if isinstance(v, np.ndarray):
return v.tolist()
return v
@model_validator(mode='after')
def check_answer_variations(self):
"""check list lengths for answer_variations based on qtype
"""
ans_var = self.answer_variations
q_type = self.question_type
if len(ans_var) < 1:
raise ValueError('Must have at least 1 answer variation')
if q_type == QuestionType.EX and len(ans_var)>3 :
raise ValueError('EX questions must have at most 3 answer variations')
return self
class PipelineMetadata(BaseModel):
"""
Fields that track how a question was generated.
Present at Bronze and Silver, shed at Gold.
"""
generation_model: str
generation_prompt_version: Optional[Dict[str, str]] = Field(default=None)
lex_enrich_prompt_version: str
semantic_enrich_prompt_version: str
@field_validator('generation_prompt_version', mode='before')
@classmethod
def deserialize_prompt_version(cls, v):
"""
Parquet round-trip stores this as a JSON string.
Deserialize back to dict on load.
"""
if isinstance(v, str):
return json.loads(v)
return v # already a dict or None, pass through
# MCQ mixing to be added to MCQ classes along with BaseQuestion to decouple
# core fields so GoldMCQ doesn't indirectly inherit Silver audit fields that were shed.
class MCQuestion(BaseModel):
"""
Mixin that adds MCQ-specific fields and validation to any tier schema.
Inherits from BaseModel (not BaseQuestion) so it can be composed cleanly
across all tiers without dragging a second BaseQuestion chain into the
class hierarchy. Always paired with a tier-specific parent:
LegacyMCQ(LegacyStandard, MCQuestion)
SilverMCQ(SilverStandard, MCQuestion)
GoldMCQ(GoldStandard, MCQuestion)
"""
# TYPE HINTING ONLY: Prevents Pylance 'unknown attribute' errors.
# At runtime, Pydantic ignores this. The @model_validator(mode='after')
# safely accesses `self.answer` because the final composed class
# (e.g. GoldMCQ) will inherit `answer` directly from BaseQuestion.
# error happens because changed to a MCQ to mixin instead of inheriting
# BaseQuestion anymore
if TYPE_CHECKING:
answer: str
mcq_options: List[str] = Field(..., min_length=4, max_length=4)
# Ensure that the answer is one of the options
@model_validator(mode='after')
def check_answer_in_options(self) -> 'MCQuestion':
"""
Validates that the canonical answer is exactly one of the options.
"""
clean_answer = self.answer.strip()
clean_options = [opt.strip() for opt in self.mcq_options]
if clean_answer not in clean_options:
raise ValueError(f"Correct answer '{self.answer}' must be one of the options.")
return self
class SyntheticStandard(BaseQuestion, PipelineMetadata):
"""
STRICT schema for new FR/EX questions.
Enforces 'source_reference' and 'source_quote' to ensure
high-quality grounding and prevent duplicates in the Content Factory.
"""
# TODO (full dev): rename to `syn_id`
temp_qid: str = Field(
description="Notebook tracer artifact. In main dev, the generation pipeline "
"should emit this as 'syn_id' (Run/Batch/Job/QID) natively to avoid "
"schema transitions between Bronze and Silver."
)
llm_predicted_category: str
llm_predicted_difficulty: str
source_reference: str
source_quote: str
@model_validator(mode='after')
def validate_source_metadata(self):
"""Make sure required source meta data is present
"""
if self.source_reference is None:
raise ValueError("Record is missing its grounding source_reference \
entry required for deduplication")
if self.source_quote is None:
raise ValueError("Record is missing its grounding source_quote entry \
required for deduplication")
return self
@model_validator(mode='after')
def validate_synthetic_lineage(self):
"""Confirm the synthetic questions have the generation prompt version included"""
if self.question_source == "synthetic" and self.generation_prompt_version is None:
raise ValueError(
"Synthetic questions must include a generation_prompt_version dictionary.")
return self
class SyntheticMCQ(MCQuestion, SyntheticStandard):
"""
STRICT schema for new multiple-choice questions (MCQ).
Inerits MCQ behavior from MCQ_question and full Synthetic
columns from Synthetic standard.
"""
pass
class LegacyStandard(BaseQuestion, PipelineMetadata):
"""
Inherits base question and adds legacy identifier
"""
original_question_id: int
class LegacyMCQ(MCQuestion, LegacyStandard):
"""
STRICT schema for new multiple-choice questions (MCQ).
Inerits MCQ behavior from MCQ_question and full Legacy
columns from Legacy standard.
"""
pass
class EnrichmentFlag(BaseModel):
"""
Represents a specific diagnostic finding from the LLM Enrichment Audit.
This class captures granular failures or warnings identified during the
LLM judge validation of synthetically generated fields (hints, explanation,
answer variations). It allows for targeted debugging of the enrichment prompts
(lexical enrichment, semantic context variations) within the generation pipeline
"""
column_name: str
severity: Literal["error", "warning"]
flag_notes: str
# Audit layer and system invariant (stores metadata for traceability)
class SilverStandard(BaseQuestion, PipelineMetadata):
"""
The Silver Tier schema for Standard (FR/EX) questions.
Captures full audit traces, embeddings, and model lineage.
"""
model_config = ConfigDict(populate_by_name=True) #standardize names to clean python format
# required (check 1. confirm master_id is the right shape / format)
master_id: str= Field(
min_length=8,
max_length=8,
pattern=r'^[A-Za-z0-9_-]+$',
description="Deterministic 8-char Base64 URL-safe hash"
)
# Traceability identifiers
original_question_id: Optional[int] = None
syn_id: Optional[str] = None
# SBERT embeddings (vector data)
question_embeddings: List[float]
answer_embeddings: List[float]
answer_variations_embeddings: List[List[float]]
source_quote_embeddings: Optional[List[float]] = None
# Generation pipeline metadata (None for Legacy)
llm_predicted_category: Optional[str] = None
llm_predicted_difficulty: Optional[str] = None
source_reference: Optional[str] = None
source_quote: Optional[str] = None
# Validation: Synthetic RAG-Triad check with LLM Judge (None for Legacy)
eval_source_quote_match: Optional[str] = None
eval_ans_grounding: Optional[str] = None
eval_ques_logic: Optional[str] = None
# LLM judge assigns codes 0,1,2 (0 and 1 are acceptable, 2 is fail and should not be observed)
score_source_quote_match: Optional[Literal[0, 1]] = None
score_ans_grounding: Optional[Literal[0, 1]] = None
score_ques_logic: Optional[Literal[0, 1]] = None
# Validation stage 3: Alignment (only accepted records, so no False)
ans_variations_valid: Optional[Literal[True]] = None
# validation of questions answer types using Gen-LLM assigned categories (only Synthetic)
alignment_valid: Optional[Literal[True]] = None
alignment_reason: Optional[str] = None
# Validation of enrichment cols (hints, ans var, explanation) with LLM Judge (optional for Synthetic)
enrich_audit_status: Optional[Literal["pass","fail"]] = None
enrich_audit_flags: Optional[List[EnrichmentFlag]] = Field(default_factory=list)
enrich_judge_reasoning: Optional[str] = None
# Validation pipeline metadata
sbert_model: str #required -> system invariant
llm_judge_model: str # each question source has atleast one LLM judge pass
judge_RAGtriad_prompt_version: Optional[str] = None
judge_enrich_col_prompt_version: Optional[str] =None
# validation: deduplication of synthetic vs. existing Gold
dedupe_action: Optional[str] = None
closest_legacy_id: Optional[int] = None
max_similarity_score: Optional[float] = None
@field_validator('enrich_audit_flags', mode='before')
@classmethod
def deserialize_audit_flags(cls, v):
"""
Deserializes 'enrich_audit_flags' from a JSON string back to a list of dicts
after a Parquet round-trip. Pydantic then coerces each dict into an EnrichmentFlag object.
Passes through unchanged if already a list or None.
"""
if isinstance(v, str):
return json.loads(v)
if isinstance(v, np.ndarray):
return json.loads(''.join(v.tolist()))
return v
# check 2. check deterministic math of the master_id corresponds to question data
@model_validator(mode='after')
def verify_master_id(self):
"""
Mathematically verifies that the master_id perfectly matches the deterministic
MD5 base64url hash of the question payload (ADR-P2-021). Prevents referential
corruption by ensuring IDs cannot be accidentally reassigned or scrambled during merges.
"""
# 1. Recreate the exact payload from assign_master_ids
payload = f"{self.question_type}_{self.question}_{self.answer}"
# 2. Recalculate the expected hash
hash_bytes = hashlib.md5(payload.encode('utf-8')).digest()
expected_id = base64.urlsafe_b64encode(hash_bytes).decode('utf-8').rstrip('=')[:8]
# 3. Drop the hammer if they don't match
if self.master_id != expected_id:
raise ValueError(
f"CRITICAL: master_id mismatch! "
f"Expected {expected_id} for payload '{payload}', but got {self.master_id}.")
return self
@model_validator(mode="after")
def validate_origin_integrity(self) -> Self:
"""
Enforces strictness for synthetic data while allowing
nulls for legacy bootstrap data.
"""
# 1. Define required fiels for each question source
if self.question_source == QuestionSource.SYNTHETIC:
check_map = {
"syn_id": self.syn_id,
"llm_predicted_category": self.llm_predicted_category,
"llm_predicted_difficulty": self.llm_predicted_difficulty,
"source_reference" : self.source_reference,
"source_quote": self.source_quote,
"ans_variations_valid": self.ans_variations_valid,
"eval_source_quote_match": self.eval_source_quote_match,
"eval_ans_grounding": self.eval_ans_grounding,
"eval_ques_logic": self.eval_ques_logic,
"score_source_quote_match": self.score_source_quote_match,
"score_ans_grounding": self.score_ans_grounding,
"score_ques_logic": self.score_ques_logic,
"alignment_valid": self.alignment_valid,
"alignment_reason": self.alignment_reason,
"dedupe_action": self.dedupe_action,
"closest_legacy_id" : self.closest_legacy_id,
"max_similarity_score" : self.max_similarity_score
}
elif self.question_source == QuestionSource.LEGACY:
check_map = {
"original_question_id": self.original_question_id,
"enrich_audit_status": self.enrich_audit_status,
"enrich_audit_flags": self.enrich_audit_flags,
"enrich_judge_reasoning": self.enrich_judge_reasoning,
"llm_judge_model": self.llm_judge_model,
"judge_enrich_col_prompt_version": self.judge_enrich_col_prompt_version
}
else:
return self # safety
# 2. record which fields are missing in a list
missing = [field_name for field_name, value in check_map.items() if value is None]
# 3. raise error message
if missing:
raise ValueError(
f"Validation Failed for {self.question_source} record: "
f"The following required fields are missing/null: {missing}"
)
return self
@model_validator(mode='after')
def check_at_least_one_judge_pass(self) -> 'SilverStandard':
"""Each question source (legacy, synthetic) has run it's required LLM pass"""
if not self.judge_RAGtriad_prompt_version and not self.judge_enrich_col_prompt_version:
row_id = self.syn_id or self.original_question_id
raise ValueError(
f"Row {row_id} is missing both judge prompt versions... "
)
return self
class SilverMCQ(SilverStandard, MCQuestion):
"""
Core schema for Silver Multiple Choice (MCQ) questions.
Inherits core fields from SilverStandard and requires exactly 4 options.
Validation ensures the designated correct answer is present within the options.
"""
mcq_distractors_embeddings: List[List[float]]
# validation results (for only accepted questions to pass)
mcq_presence_valid: Literal[True]
mcq_distractors_valid: Literal[True]
mcq_margin_score: float
mcq_closest_distractor: str
# Projection of Silver for handoff for enrichment layer
class GoldStandard(BaseQuestion):
""" Final gold FR, EX schema """
# ensure that shedded columns are dropped
model_config = ConfigDict(extra='ignore')
# required (check 1. confirm master_id is the right shape / format)
master_id: str= Field(
min_length=8,
max_length=8,
pattern=r'^[A-Za-z0-9_-]+$',
description="Deterministic 8-char Base64 URL-safe hash"
)
# SBERT embeddings (vector data)
question_embeddings: List[float]
answer_embeddings: List[float]
answer_variations_embeddings: List[List[float]]
source_quote_embeddings: Optional[List[float]] = None
# for traceability only
original_question_id: Optional[int] = None
syn_id: Optional[str] = None
# optional because they are null for Legacy
source_reference: Optional[str] = None
source_quote: Optional[str] = None
@model_validator(mode='before')
@classmethod
def shed_rectangular_nulls(cls, data: dict) -> dict:
"""
Data Engineering Bridge: Pandas DataFrames are perfectly rectangular,
meaning FR/EX rows will inevitably contain empty 'mcq_options' columns.
This intercepts the raw Pandas dictionary and sheds any empty fields
before the strict `extra="forbid"` check is applied.
"""
clean_data = {}
for k, v in data.items():
# Keep the key if it has a real value
# Drop it if it is None or a Pandas NaN (float nan).
if v is not None and not (isinstance(v, float) and math.isnan(v)):
clean_data[k] = v
return clean_data
# check 2. check deterministic math of the master_id corresponds to question data
@model_validator(mode='after')
def verify_master_id(self):
"""
Mathematically verifies that the master_id perfectly matches the deterministic
MD5 base64url hash of the question payload (ADR-P2-021). Prevents referential
corruption by ensuring IDs cannot be accidentally reassigned or scrambled during merges.
"""
# 1. Recreate the exact payload from assign_master_ids
payload = f"{self.question_type}_{self.question}_{self.answer}"
# 2. Recalculate the expected hash
hash_bytes = hashlib.md5(payload.encode('utf-8')).digest()
expected_id = base64.urlsafe_b64encode(hash_bytes).decode('utf-8').rstrip('=')[:8]
# 3. Drop the hammer if they don't match
if self.master_id != expected_id:
raise ValueError(
f"CRITICAL: master_id mismatch! "
f"Expected {expected_id} for payload '{payload}', but got {self.master_id}.")
return self
class GoldMCQ(GoldStandard, MCQuestion):
""" Final Gold MCQ schema. Combines Gold-tier metadata
with Silver-tier MCQ validation logic. """
mcq_distractors_embeddings: List[List[float]]
# Context Refinery model (Gold + Context Features) - for development / experimentation
class ProductionStandard_Green(GoldStandard):
"""
Full production FR, EX schema with
feature experimentation
(Gold + context refinery features)
"""
# ensure that shedded columns are dropped
model_config = ConfigDict(extra='ignore', arbitrary_types_allowed=True) # for tensors
# descriptive features
question_length: int
answer_length: int
answer_type: AnswerType
question_tokens: List[str]
answer_tokens: List[str]
combined_unique_tokens: List[str]
main_keyword: str
# NER features to come
# Context Refinery model (Gold + Context Features) - for development / experimentation
class ProductionMCQ_Green(ProductionStandard_Green, GoldMCQ):
"""
Pure schema composition layer.
No additional fields or behavior.
Represents the canonical MCQ storage contract for runtime handoff.
"""
pass
# stable serving model for runtime (lean, mirror version of Green dataset)
class ProductionStandard_Blue(GoldStandard):
"""
Runtime ready lean production FR, EX schema
(required Gold + context refinery features only)
"""
question_length: int
answer_length: int
answer_type: AnswerType
class ProductionMCQ_Blue(ProductionStandard_Blue, GoldMCQ):
"""
Runtime ready lean production MCQ schema
(required Gold + context refinery features)
Pure schema composition layer.
No additional fields or behavior.
Represents the canonical MCQ storage contract for runtime handoff.
"""
pass
class RuntimeStandard_Green(ProductionStandard_Green):
"""
At warmup, after tensor hydration
Normalizes legacy numeric identifiers during runtime hydration.
Why this is needed:
-------------------
Upstream data originates from mixed Pandas + JSON pipelines where:
- Missing values may appear as None, NaN, or pd.NA
- Pandas promotes integer columns containing nulls to float dtype
- This causes valid integer IDs to be represented as floats (e.g. 12.0)
This validator ensures:
- NaN → None (canonical missing value)
- float IDs → int (lossless cast where safe)
- consistent downstream typing for evaluator contracts
This preserves strict runtime guarantees for evaluator routing logic,
which depends on deterministic identifier semantics.
"""
model_config = ConfigDict(extra="ignore", arbitrary_types_allowed=True,
populate_by_name=True, validate_assignment=False)
# tensors calculated upfront during runtime warmup for session
# NOTE: Type `Any` is used to bypass Pydantic's internal validation
# checks for non-standard types (torch.Tensor), ensuring near-instant
# `Question` object instantiation during the warmup loop.
question_embeddings_tensor: Any
answer_embeddings_tensor: Any
answer_variations_embeddings_tensor_matrix: Any
@field_validator(
"question_embeddings_tensor",
"answer_embeddings_tensor",
"answer_variations_embeddings_tensor_matrix",
mode="after"
)
@classmethod
def validate_runtime_tensors(cls, v: Any, info: ValidationInfo) -> Any:
"""
Validates that tensors are present, are numpy arrays,
and match the SBERT 384-dimension requirement.
"""
# 1. Grab the field name or default to an empty string to keep type checkers happy
fname = info.field_name or ""
if v is None:
raise ValueError(f"CRITICAL: {fname} is missing.")
# 2. Use specific suffix checking
if fname.endswith("_matrix"):
if v.ndim != 2 or v.shape[1] != 384:
raise ValueError(f"Matrix {fname} must be (N, 384). Got {v.shape}")
else:
if v.shape != (384,):
raise ValueError(f"Vector {fname} must be (384,). Got {v.shape}")
return v
# @field_validator("original_question_id", mode="before")
# def normalize_id(cls, v):
# if v is None:
# return None
# return int(v)
# @field_validator("mcq_options", mode="before")
# @classmethod
# def normalize_options(cls, v):
# if v is None:
# return []
# if isinstance(v, float) and math.isnan(v):
# return []
# return v
class RuntimeMCQ_Green(RuntimeStandard_Green, ProductionMCQ_Green):
# tensors calculated upfront during runtime warmup for session
# NOTE: Type `Any` is used to bypass Pydantic's internal validation
# checks for non-standard types (torch.Tensor), ensuring near-instant
# `Question` object instantiation during the warmup loop.
mcq_distractors_embeddings_tensor_matrix : Any
class RuntimeStandard_Blue(ProductionStandard_Blue):
"""
At warmup, after tensor hydration
Normalizes legacy numeric identifiers during runtime hydration.
Why this is needed:
-------------------
Upstream data originates from mixed Pandas + JSON pipelines where:
- Missing values may appear as None, NaN, or pd.NA
- Pandas promotes integer columns containing nulls to float dtype
- This causes valid integer IDs to be represented as floats (e.g. 12.0)
This validator ensures:
- NaN → None (canonical missing value)
- float IDs → int (lossless cast where safe)
- consistent downstream typing for evaluator contracts
This preserves strict runtime guarantees for evaluator routing logic,
which depends on deterministic identifier semantics.
"""
model_config = ConfigDict(extra="ignore", arbitrary_types_allowed=True,
populate_by_name=True, validate_assignment=False)
# tensors calculated upfront during runtime warmup for session
# NOTE: Type `Any` is used to bypass Pydantic's internal validation
# checks for non-standard types (torch.Tensor), ensuring near-instant
# `Question` object instantiation during the warmup loop.
question_embeddings_tensor: Any
answer_embeddings_tensor: Any
answer_variations_embeddings_tensor_matrix: Any
@field_validator("question_embeddings_tensor",
"answer_embeddings_tensor",
"answer_variations_embeddings_tensor_matrix",
mode="after"
)
@classmethod
def validate_runtime_tensors(cls, v: Any, info: ValidationInfo) -> Any:
"""
Validates that tensors are present, are numpy arrays,
and match the SBERT 384-dimension requirement.
"""
# 1. Grab the field name or default to an empty string to keep type checkers happy
fname = info.field_name or ""
if v is None:
raise ValueError(f"CRITICAL: {fname} is missing.")
# 2. Use specific suffix checking
if fname.endswith("_matrix"):
if v.ndim != 2 or v.shape[1] != 384:
raise ValueError(f"Matrix {fname} must be (N, 384). Got {v.shape}")
else:
if v.shape != (384,):
raise ValueError(f"Vector {fname} must be (384,). Got {v.shape}")
return v
# @field_validator("original_question_id", mode="before")
# def normalize_id(cls, v):
# if v is None:
# return None
# return int(v)
# @field_validator("mcq_options", mode="before")
# @classmethod
# def normalize_options(cls, v):
# if v is None:
# return []
# if isinstance(v, float) and math.isnan(v):
# return []
# return v
# in-game hydration / validation with warmup tensors
class RuntimeMCQ_Blue(RuntimeStandard_Blue, ProductionMCQ_Blue):
"""
At warmup, after tensor hydration
"""
# tensors calculated upfront during runtime warmup for session
# NOTE: Type `Any` is used to bypass Pydantic's internal validation
# checks for non-standard types (torch.Tensor), ensuring near-instant
# `Question` object instantiation during the warmup loop.
mcq_distractors_embeddings_tensor_matrix : Any
## Model routing
SchemeMap = Dict[QuestionType, type]
# routing for models at runtime (devlopment vs. stable modes)
OFFLINE_HANDOFF_REGISTRY: Dict[str, SchemeMap] = {
"dev": {
QuestionType.EX : ProductionStandard_Green,
QuestionType.FR : ProductionStandard_Green,
QuestionType.MCQ: ProductionMCQ_Green
},
"stable":{
QuestionType.EX : ProductionStandard_Blue,
QuestionType.FR : ProductionStandard_Blue,
QuestionType.MCQ: ProductionMCQ_Blue
}
}
# routing for models at runtime (devlopment vs. stable modes)
RUNTIME_REGISTRY: Dict[str, SchemeMap] = {
"dev": {
QuestionType.EX : RuntimeStandard_Green,
QuestionType.FR : RuntimeStandard_Green,
QuestionType.MCQ: RuntimeMCQ_Green
},
"stable":{
QuestionType.EX : RuntimeStandard_Blue,
QuestionType.FR : RuntimeStandard_Blue,
QuestionType.MCQ: RuntimeMCQ_Blue
}
}
# routing for the `qa_validation_pipeline` in Content Factory
VALIDATION_REGISTRY: Dict[QuestionSource, Dict[DataTier, SchemeMap]] = {
QuestionSource.LEGACY:{
DataTier.BRONZE:{
QuestionType.EX : LegacyStandard,
QuestionType.FR : LegacyStandard,
QuestionType.MCQ: LegacyMCQ
},
DataTier.SILVER:{
QuestionType.EX : SilverStandard,
QuestionType.FR : SilverStandard,
QuestionType.MCQ: SilverMCQ },
DataTier.GOLD:{
QuestionType.EX : GoldStandard,
QuestionType.FR : GoldStandard,
QuestionType.MCQ: GoldMCQ
}
},
QuestionSource.SYNTHETIC:{
DataTier.BRONZE:{
QuestionType.EX : SyntheticStandard,
QuestionType.FR : SyntheticStandard,
QuestionType.MCQ: SyntheticMCQ
},
DataTier.SILVER:{
QuestionType.EX : SilverStandard,
QuestionType.FR : SilverStandard,
QuestionType.MCQ: SilverMCQ
},
DataTier.GOLD:{
QuestionType.EX : GoldStandard,
QuestionType.FR : GoldStandard,
QuestionType.MCQ: GoldMCQ
}
}
}