55import time
66from abc import ABC , abstractmethod
77from collections .abc import Callable
8- from typing import Any , Generic , TypeVar , get_args
8+ from typing import Any , Generic , TypeVar , Union , cast , get_args
99
1010from pydantic import BaseModel , ConfigDict , Field , model_validator
1111
@@ -47,11 +47,18 @@ class BaseEvaluatorConfig(BaseModel):
4747 default_evaluation_criteria : BaseEvaluationCriteria | None = None
4848
4949
50+ class BaseEvaluatorJustification (BaseModel ):
51+ """Base class for all evaluator justifications."""
52+
53+ pass
54+
55+
5056T = TypeVar ("T" , bound = BaseEvaluationCriteria )
5157C = TypeVar ("C" , bound = BaseEvaluatorConfig )
58+ J = TypeVar ("J" , bound = Union [str , None , BaseEvaluatorJustification ])
5259
5360
54- class BaseEvaluator (BaseModel , Generic [T , C ], ABC ):
61+ class BaseEvaluator (BaseModel , Generic [T , C , J ], ABC ):
5562 """Abstract base class for all evaluators."""
5663
5764 model_config = ConfigDict (arbitrary_types_allowed = True )
@@ -61,6 +68,9 @@ class BaseEvaluator(BaseModel, Generic[T, C], ABC):
6168 evaluation_criteria_type : type [T ] = Field (
6269 description = "The type used for evaluation criteria validation and creation"
6370 )
71+ justification_type : type [J ] = Field (
72+ description = "The type used for justification validation and creation"
73+ )
6474 evaluator_config : C = Field (
6575 exclude = True , description = "The validated config object instance"
6676 )
@@ -101,6 +111,10 @@ def validate_model(cls, values: Any) -> Any:
101111 config_type = cls ._extract_config_type ()
102112 values ["config_type" ] = config_type
103113
114+ # Always extract and set justification_type
115+ justification_type = cls ._extract_justification_type ()
116+ values ["justification_type" ] = justification_type
117+
104118 # Validate and create the config object if config dict is provided
105119 if config_dict := values .get ("config" ):
106120 try :
@@ -182,6 +196,33 @@ def _extract_config_type(cls) -> type[BaseEvaluatorConfig]:
182196 f"Ensure the class properly inherits from BaseEvaluator with correct Generic parameters."
183197 )
184198
199+ @classmethod
200+ def _extract_justification_type (cls ) -> type [J ]:
201+ """Extract the justification type from Pydantic model fields.
202+
203+ Returns:
204+ The justification type (str, None, or BaseEvaluatorJustification subclass)
205+ """
206+ if cls .__name__ == "BaseEvaluator" :
207+ return cast (type [J ], type (None ))
208+
209+ if hasattr (cls , "model_fields" ) and "justification_type" in cls .model_fields :
210+ field_info = cls .model_fields ["justification_type" ]
211+ if hasattr (field_info , "annotation" ):
212+ annotation = field_info .annotation
213+ if args := get_args (annotation ):
214+ justification_type = args [0 ]
215+ # Support str, None, or BaseEvaluatorJustification subclasses
216+ if justification_type is str or justification_type is type (None ):
217+ return cast (type [J ], justification_type )
218+ elif isinstance (justification_type , type ) and issubclass (
219+ justification_type , BaseEvaluatorJustification
220+ ):
221+ return cast (type [J ], justification_type )
222+
223+ # Default to None if we can't determine the type
224+ return cast (type [J ], type (None ))
225+
185226 def validate_evaluation_criteria (self , criteria : Any ) -> T :
186227 """Validate and convert input to the correct evaluation criteria type.
187228
@@ -213,6 +254,64 @@ def validate_evaluation_criteria(self, criteria: Any) -> T:
213254 f"Cannot convert { type (criteria )} to { self .evaluation_criteria_type } : { e } "
214255 ) from e
215256
257+ def validate_justification (self , justification : Any ) -> J :
258+ """Validate and convert input to the correct justification type.
259+
260+ Args:
261+ justification: The justification to validate (str, None, dict, BaseEvaluatorJustification, or other)
262+
263+ Returns:
264+ The validated justification of the correct type
265+ """
266+ # The key insight: J is constrained to be one of str, None, or BaseEvaluatorJustification
267+ # At instantiation time, J gets bound to exactly one of these types
268+ # We need to handle each case and ensure the return matches the bound type
269+
270+ # Handle None type - when J is bound to None (the literal None type)
271+ if self .justification_type is type (None ):
272+ # When J is None, we can only return None
273+ return cast (J , justification if justification is None else None )
274+
275+ # Handle str type - when J is bound to str
276+ if self .justification_type is str :
277+ # When J is str, we must return a str
278+ if justification is None :
279+ return cast (J , "" )
280+ return cast (J , str (justification ))
281+
282+ # Handle BaseEvaluatorJustification subclasses - when J is bound to a specific subclass
283+ if isinstance (self .justification_type , type ) and issubclass (
284+ self .justification_type , BaseEvaluatorJustification
285+ ):
286+ # When J is a BaseEvaluatorJustification subclass, we must return that type
287+ if justification is None :
288+ raise ValueError (
289+ f"None is not allowed for justification type { self .justification_type } "
290+ )
291+
292+ if isinstance (justification , self .justification_type ):
293+ return cast (J , justification )
294+ elif isinstance (justification , dict ):
295+ return cast (J , self .justification_type .model_validate (justification ))
296+ elif hasattr (justification , "__dict__" ):
297+ return cast (
298+ J , self .justification_type .model_validate (justification .__dict__ )
299+ )
300+ else :
301+ try :
302+ return cast (
303+ J , self .justification_type .model_validate (justification )
304+ )
305+ except Exception as e :
306+ raise ValueError (
307+ f"Cannot convert { type (justification )} to { self .justification_type } : { e } "
308+ ) from e
309+
310+ # Fallback: try to return as-is or raise error
311+ raise ValueError (
312+ f"Unsupported justification type { self .justification_type } for input { type (justification )} "
313+ )
314+
216315 @classmethod
217316 def get_evaluation_criteria_schema (cls ) -> dict [str , Any ]:
218317 """Get the JSON schema for the evaluation criteria type.
0 commit comments