11import os
22from datetime import datetime
33from enum import Enum
4- from typing import Any , Dict , List , Literal , Optional , TypedDict , Union
4+ from typing import Any , ClassVar , Dict , List , Literal , Optional , TypedDict , Union
55
66from openai .types import CompletionUsage
77from openai .types .chat .chat_completion_message import (
1515from eval_protocol .types import TerminationReason
1616
1717
18+ class ErrorInfo (BaseModel ):
19+ """
20+ AIP-193 ErrorInfo model for structured error details.
21+
22+ This model follows Google's AIP-193 standard for ErrorInfo:
23+ https://google.aip.dev/193#errorinfo
24+
25+ Attributes:
26+ reason (str): A short snake_case description of the cause of the error.
27+ domain (str): The logical grouping to which the reason belongs.
28+ metadata (Dict[str, Any]): Additional dynamic information as context.
29+ """
30+
31+ # Constants for reason values
32+ REASON_TERMINATION_REASON : ClassVar [str ] = "TERMINATION_REASON"
33+ REASON_EXTRA_INFO : ClassVar [str ] = "EXTRA_INFO"
34+
35+ # Domain constant
36+ DOMAIN : ClassVar [str ] = "evalprotocol.io"
37+
38+ reason : str = Field (..., description = "Short snake_case description of the error cause" )
39+ domain : str = Field (..., description = "Logical grouping for the error reason" )
40+ metadata : Dict [str , Any ] = Field (default_factory = dict , description = "Additional dynamic information as context" )
41+
42+ def to_aip193_format (self ) -> Dict [str , Any ]:
43+ """Convert to AIP-193 format with @type field."""
44+ return {
45+ "@type" : "type.googleapis.com/google.rpc.ErrorInfo" ,
46+ "reason" : self .reason ,
47+ "domain" : self .domain ,
48+ "metadata" : self .metadata ,
49+ }
50+
51+ @classmethod
52+ def termination_reason (cls , reason : TerminationReason ) -> "ErrorInfo" :
53+ """Create an ErrorInfo for termination reason."""
54+ # Convert TerminationReason enum to string if needed
55+ reason_str = reason .value if isinstance (reason , TerminationReason ) else reason
56+ return cls (
57+ reason = cls .REASON_TERMINATION_REASON , domain = cls .DOMAIN , metadata = {"termination_reason" : reason_str }
58+ )
59+
60+ @classmethod
61+ def extra_info (cls , metadata : Dict [str , Any ]) -> "ErrorInfo" :
62+ """Create an ErrorInfo for extra information."""
63+ return cls (reason = cls .REASON_EXTRA_INFO , domain = cls .DOMAIN , metadata = metadata )
64+
65+
66+ class Status (BaseModel ):
67+ """
68+ AIP-193 compatible Status model for standardized error responses.
69+
70+ This model follows Google's AIP-193 standard for error handling:
71+ https://google.aip.dev/193
72+
73+ Attributes:
74+ code (int): The status code, must be the numeric value of one of the elements
75+ of google.rpc.Code enum (e.g., 5 for NOT_FOUND).
76+ message (str): Developer-facing, human-readable debug message in English.
77+ details (List[Dict[str, Any]]): Additional error information, each packed in
78+ a google.protobuf.Any message format.
79+ """
80+
81+ code : "Status.Code" = Field (..., description = "The status code from google.rpc.Code enum" )
82+ message : str = Field (..., description = "Developer-facing, human-readable debug message in English" )
83+ details : List [Dict [str , Any ]] = Field (
84+ default_factory = list ,
85+ description = "Additional error information, each packed in a google.protobuf.Any message format" ,
86+ )
87+
88+ # Convenience constants for common status codes
89+ class Code (int , Enum ):
90+ """Common gRPC status codes as defined in google.rpc.Code"""
91+
92+ OK = 0
93+ CANCELLED = 1
94+ UNKNOWN = 2
95+ INVALID_ARGUMENT = 3
96+ DEADLINE_EXCEEDED = 4
97+ NOT_FOUND = 5
98+ ALREADY_EXISTS = 6
99+ PERMISSION_DENIED = 7
100+ RESOURCE_EXHAUSTED = 8
101+ FAILED_PRECONDITION = 9
102+ ABORTED = 10
103+ OUT_OF_RANGE = 11
104+ UNIMPLEMENTED = 12
105+ INTERNAL = 13
106+ UNAVAILABLE = 14
107+ DATA_LOSS = 15
108+ UNAUTHENTICATED = 16
109+
110+ # Custom codes for rollout states (using higher numbers to avoid conflicts)
111+ FINISHED = 100
112+
113+ @classmethod
114+ def rollout_running (cls ) -> "Status" :
115+ """Create a status indicating the rollout is running."""
116+ return cls (code = cls .Code .OK , message = "Rollout is running" , details = [])
117+
118+ @classmethod
119+ def rollout_finished (
120+ cls ,
121+ termination_reason : Optional [TerminationReason ] = None ,
122+ extra_info : Optional [Dict [str , Any ]] = None ,
123+ ) -> "Status" :
124+ """Create a status indicating the rollout finished."""
125+ details = []
126+ if termination_reason :
127+ details .append (ErrorInfo .termination_reason (termination_reason ).to_aip193_format ())
128+ if extra_info :
129+ details .append (ErrorInfo .extra_info (extra_info ).to_aip193_format ())
130+ return cls (code = cls .Code .FINISHED , message = "Rollout finished" , details = details )
131+
132+ @classmethod
133+ def rollout_error (cls , error_message : str , extra_info : Optional [Dict [str , Any ]] = None ) -> "Status" :
134+ """Create a status indicating the rollout failed with an error."""
135+ details = []
136+ if extra_info :
137+ details .append (ErrorInfo .extra_info (extra_info ).to_aip193_format ())
138+ return cls .error (error_message , details )
139+
140+ @classmethod
141+ def error (cls , error_message : str , details : Optional [List [Dict [str , Any ]]] = None ) -> "Status" :
142+ """Create a status indicating the rollout failed with an error."""
143+ return cls (code = cls .Code .INTERNAL , message = error_message , details = details )
144+
145+ def is_running (self ) -> bool :
146+ """Check if the status indicates the rollout is running."""
147+ return self .code == self .Code .OK and self .message == "Rollout is running"
148+
149+ def is_finished (self ) -> bool :
150+ """Check if the status indicates the rollout finished successfully."""
151+ return self .code == self .Code .FINISHED
152+
153+ def is_error (self ) -> bool :
154+ """Check if the status indicates the rollout failed with an error."""
155+ return self .code == self .Code .INTERNAL
156+
157+ def is_stopped (self ) -> bool :
158+ """Check if the status indicates the rollout was stopped."""
159+ return self .code == self .Code .CANCELLED
160+
161+ def get_termination_reason (self ) -> Optional [TerminationReason ]:
162+ """Extract termination reason from details if present."""
163+ for detail in self .details :
164+ metadata = detail .get ("metadata" , {})
165+ if detail .get ("reason" ) == ErrorInfo .REASON_TERMINATION_REASON and "termination_reason" in metadata :
166+ try :
167+ return TerminationReason .from_str (metadata ["termination_reason" ])
168+ except ValueError :
169+ # If the reason is not a valid enum value, return None
170+ return None
171+ return None
172+
173+ def get_extra_info (self ) -> Optional [Dict [str , Any ]]:
174+ """Extract extra info from details if present."""
175+ for detail in self .details :
176+ metadata = detail .get ("metadata" , {})
177+ reason = detail .get ("reason" )
178+ # Skip termination_reason and stopped details, return other error info
179+ if reason in [ErrorInfo .REASON_EXTRA_INFO ]:
180+ return metadata
181+ return None
182+
183+ def __hash__ (self ) -> int :
184+ """Generate a hash for the Status object."""
185+ # Use a stable hash based on code, message, and details
186+ import hashlib
187+
188+ # Create a stable string representation
189+ hash_data = f"{ self .code } :{ self .message } :{ len (self .details )} "
190+
191+ # Add details content for more uniqueness
192+ for detail in sorted (self .details , key = lambda x : str (x )):
193+ hash_data += f":{ str (detail )} "
194+
195+ # Generate hash
196+ hash_obj = hashlib .sha256 (hash_data .encode ("utf-8" ))
197+ return int .from_bytes (hash_obj .digest ()[:8 ], byteorder = "big" )
198+
199+
18200class ChatCompletionContentPartTextParam (BaseModel ):
19201 text : str = Field (..., description = "The text content." )
20202 type : Literal ["text" ] = Field ("text" , description = "The type of the content part." )
@@ -289,27 +471,6 @@ class ExecutionMetadata(BaseModel):
289471 )
290472
291473
292- class RolloutStatus (BaseModel ):
293- """Status of the rollout."""
294-
295- """
296- running: Unfinished rollout which is still in progress.
297- finished: Rollout finished.
298- error: Rollout failed due to unexpected error. The rollout record should be discard.
299- """
300-
301- class Status (str , Enum ):
302- RUNNING = "running"
303- FINISHED = "finished"
304- ERROR = "error"
305-
306- status : Status = Field (Status .RUNNING , description = "Status of the rollout." )
307- termination_reason : Optional [TerminationReason ] = Field (
308- None , description = "reason of the rollout status, mapped to values in TerminationReason"
309- )
310- extra_info : Optional [Dict [str , Any ]] = Field (None , description = "Extra information about the rollout status." )
311-
312-
313474class EvaluationRow (BaseModel ):
314475 """
315476 Unified data structure for a single evaluation unit that contains messages,
@@ -334,9 +495,9 @@ class EvaluationRow(BaseModel):
334495 description = "Metadata related to the input (dataset info, model config, session data, etc.)." ,
335496 )
336497
337- rollout_status : RolloutStatus = Field (
338- default_factory = RolloutStatus ,
339- description = "The status of the rollout." ,
498+ rollout_status : Status = Field (
499+ default_factory = Status . rollout_running ,
500+ description = "The status of the rollout following AIP-193 standards ." ,
340501 )
341502
342503 # Ground truth reference (moved from EvaluateResult to top level)
0 commit comments