@@ -224,6 +224,90 @@ def __str__(self) -> str:
224224 """
225225 return f"AttackResult: { self .conversation_id } : { self .outcome .value } : { self .objective [:50 ]} ..."
226226
227+ def to_dict (self ) -> dict [str , Any ]:
228+ """
229+ Serialize this attack result to a JSON-compatible dictionary.
230+
231+ Returns:
232+ dict[str, Any]: Serialized payload suitable for REST APIs or persistence.
233+ """
234+ from pyrit .models .conversation_reference import ConversationReference
235+
236+ return {
237+ "conversation_id" : self .conversation_id ,
238+ "objective" : self .objective ,
239+ "attack_result_id" : self .attack_result_id ,
240+ "atomic_attack_identifier" : (
241+ self .atomic_attack_identifier .to_dict () if self .atomic_attack_identifier else None
242+ ),
243+ "last_response" : self .last_response .to_dict () if self .last_response else None ,
244+ "last_score" : self .last_score .to_dict () if self .last_score else None ,
245+ "executed_turns" : self .executed_turns ,
246+ "execution_time_ms" : self .execution_time_ms ,
247+ "outcome" : self .outcome .value ,
248+ "outcome_reason" : self .outcome_reason ,
249+ "timestamp" : self .timestamp .isoformat () if self .timestamp else None ,
250+ "related_conversations" : sorted (
251+ [
252+ ref .to_dict () if isinstance (ref , ConversationReference ) else ref
253+ for ref in self .related_conversations
254+ ],
255+ key = lambda r : r ["conversation_id" ] if isinstance (r , dict ) else "" ,
256+ ),
257+ "metadata" : self .metadata ,
258+ "labels" : self .labels ,
259+ "error_message" : self .error_message ,
260+ "error_type" : self .error_type ,
261+ "error_traceback" : self .error_traceback ,
262+ "retry_events" : [e .to_dict () for e in self .retry_events ],
263+ "total_retries" : self .total_retries ,
264+ }
265+
266+ @classmethod
267+ def from_dict (cls , data : dict [str , Any ]) -> AttackResult :
268+ """
269+ Reconstruct an AttackResult from a dictionary.
270+
271+ Args:
272+ data (dict[str, Any]): Dictionary as produced by to_dict().
273+
274+ Returns:
275+ AttackResult: Reconstructed instance.
276+ """
277+ from pyrit .identifiers .component_identifier import ComponentIdentifier
278+ from pyrit .models .conversation_reference import ConversationReference
279+ from pyrit .models .message_piece import MessagePiece
280+ from pyrit .models .retry_event import RetryEvent
281+ from pyrit .models .score import Score
282+
283+ return cls (
284+ conversation_id = data ["conversation_id" ],
285+ objective = data ["objective" ],
286+ attack_result_id = data .get ("attack_result_id" , str (uuid .uuid4 ())),
287+ atomic_attack_identifier = (
288+ ComponentIdentifier .from_dict (data ["atomic_attack_identifier" ])
289+ if data .get ("atomic_attack_identifier" )
290+ else None
291+ ),
292+ last_response = (MessagePiece .from_dict (data ["last_response" ]) if data .get ("last_response" ) else None ),
293+ last_score = Score .from_dict (data ["last_score" ]) if data .get ("last_score" ) else None ,
294+ executed_turns = data .get ("executed_turns" , 0 ),
295+ execution_time_ms = data .get ("execution_time_ms" , 0 ),
296+ outcome = AttackOutcome (data .get ("outcome" , "undetermined" )),
297+ outcome_reason = data .get ("outcome_reason" ),
298+ timestamp = (
299+ datetime .fromisoformat (data ["timestamp" ]) if data .get ("timestamp" ) else datetime .now (timezone .utc )
300+ ),
301+ related_conversations = {ConversationReference .from_dict (r ) for r in data .get ("related_conversations" , [])},
302+ metadata = data .get ("metadata" , {}),
303+ labels = data .get ("labels" , {}),
304+ error_message = data .get ("error_message" ),
305+ error_type = data .get ("error_type" ),
306+ error_traceback = data .get ("error_traceback" ),
307+ retry_events = [RetryEvent .from_dict (e ) for e in data .get ("retry_events" , [])],
308+ total_retries = data .get ("total_retries" , 0 ),
309+ )
310+
227311
228312def _add_attack_identifier_compat (cls : type ) -> type :
229313 """
0 commit comments