Scores and attack results
Lesson 18 read the conversation. The verdict from part 2 and the summary of the whole run are stored too, in their own tables.
A score belongs to the row it judged, so you fetch it by that row's id.
replies = db.get_message_pieces(role="assistant")
scores = db.get_prompt_scores(prompt_ids=[str(p.id) for p in replies])
for score in scores:
print(score.score_value, "|", score.score_type, "|", score.scorer_class_identifier.class_name)The scorer's own identity is stored beside the verdict. Six months later, when a report says a reply was a leak, that field is how you find out what rule made the call.
get_scores() with no arguments returns nothing. Not an error, not everything: an empty list, while the scores plainly exist. It wants a filter. Its near-namesake get_prompt_scores is the one that takes row ids, and the two read like a pair without behaving like one.The run as a whole
The result object from execute_async is also written down, so a campaign can be summarised without keeping anything in memory.
for stored in db.get_attack_results():
print(stored.outcome.name, "|", stored.executed_turns, "turn(s) |", stored.objective)One row per attack, carrying the objective, the outcome and the conversation it belongs to. This is the table a report is built from; the message pieces are the detail you drill into when a row looks wrong.
From a result back to the evidence
stored = db.get_attack_results()[0]
messages = db.get_conversation_messages(conversation_id=stored.conversation_id)
print(stored.outcome.name, "->", messages[-1].get_value())Outcome, conversation id, conversation. That is the whole chain from a one-line summary to the exact text that caused it, and it is what makes a PyRIT finding something a developer can act on.
- Call
db.get_scores()with no arguments and confirm it is empty. - Run two attacks with different objectives and print both stored results.
- Print
db.get_unique_attack_class_names().
This is what real progress feels like.