diff --git a/README.md b/README.md index 6f68fa2..7bddd77 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,17 @@
+

+ A terminal session. aicertify score-card bert-base-uncased reports completeness 0.49 against a threshold of 0.8 and prints BELOW THRESHOLD, with a per-section bar chart. aicertify explain eu_ai_act then reports 150 fields you must declare, which no evaluator can observe, and 14 fields produced by evaluators, noting that asserting your own fairness score defeats the point. +

+ +

+ Scoring a real Hugging Face card, then asking what the EU AI Act actually needs.
+ Watch the full four-minute walkthrough, including running a contract and the metrics still missing an evaluator.
+

+ +
+ **AICertify is the open execution and evidence layer for AI governance.** Describe an AI system in a contract, supply the facts only your organisation can know, attach or compute measured metrics, evaluate that evidence against versioned [GOPAL](https://github.com/Principled-Evolution/gopal) policies through [Open Policy Agent](https://www.openpolicyagent.org/), and generate dated PDF, Markdown, JSON, or HTML reports. **The goal is simple: move from “we have an AI policy” to evidence another engineer, auditor, or risk team can inspect and reproduce.** diff --git a/aicertify/opa_core/extraction.py b/aicertify/opa_core/extraction.py index 1cd2dcc..f8d96ef 100644 --- a/aicertify/opa_core/extraction.py +++ b/aicertify/opa_core/extraction.py @@ -359,10 +359,19 @@ def extract_policy_results_with_schema( return extracted_policies except Exception as e: - logger.error(f"Error validating OPA results against schema: {e}") - # Dont try to recover using a more flexible approach - logger.warning( - "Failed to validate OPA results against schema, returning empty list" + # Expected, and the common case. This path validates against the + # `report_output` schema, which four of gopal's policies define. Every + # other policy fails it, and the caller then reads the decision rules + # instead (see the note on extract_results_from_packages below). + # + # Logged at error until the #78 fallback landed, which meant a normal + # successful evaluation printed a pydantic validation report per policy + # and looked to a first-time user like a crash. The behaviour was + # already correct; only the log level was left behind. + logger.debug( + "No report_output schema match (%s); the caller falls back to " + "decision rules.", + e.__class__.__name__, ) return [] diff --git a/aicertify/opa_policies b/aicertify/opa_policies index 4321e7a..0936496 160000 --- a/aicertify/opa_policies +++ b/aicertify/opa_policies @@ -1 +1 @@ -Subproject commit 4321e7a4fc28061610d345319d6c778e877bbd85 +Subproject commit 09364967d494b4137b8c492135c9c7738964e601 diff --git a/docs/demo/aicertify-animated.svg b/docs/demo/aicertify-animated.svg new file mode 100644 index 0000000..5c4765e --- /dev/null +++ b/docs/demo/aicertify-animated.svg @@ -0,0 +1,407 @@ + +aicertify: 150 declared, 14 measured +$ aicertify score-card bert-base-uncased + +bert-base-uncased + completeness 0.49 (threshold 0.8) + quality 0.66 + BELOW THRESHOLD + + intended_use 1.00 #################### + training_data 0.67 ############# + caveats_recommendations 0.50 ########## + factors 0.50 ########## + metrics 0.50 ########## + quantitative_analyses 0.50 ########## + ethical_considerations 0.33 ####### + model_details 0.33 ###### + evaluation_data 0.00 + +A model card answers part of what Annex IV asks and then stops. +This is the documentation-completeness gap, not a compliance verdict. + +scored by GOPAL 2.0.0 (global/v1/documentation/model_card_score), via aicertify 0.7.0 +$ # so what does the EU AI Act actually ask for? +$ aicertify explain eu_ai_act | head -12 + +eu_ai_act — 29 policies + +Fields you must declare (150) + No evaluator can observe these. They are facts about your system, + your process, or your paperwork, so you assert them in the contract. + + accuracy.declared_in_instructions technical_robustness + accuracy.metrics_declared technical_robustness + assessment.completed conformity_assessment + assessment.harmonised_standards_applied conformity_assessment + assessment.notified_body_involved conformity_assessment +$ aicertify explain eu_ai_act | grep -A8 "produced by evaluators" +Fields produced by evaluators (14) + Computed at evaluation time from your interactions. Do not hand-write + these: asserting your own fairness score defeats the point. + + metrics.content_safety.score eu_fairness [content_safety evaluator] + metrics.emotion_recognition.combined_score emotion_recognition + metrics.emotion_recognition.educational_context emotion_recognition + metrics.emotion_recognition.workplace_context emotion_recognition + metrics.fairness.gender_bias eu_fairness [fairness evaluator] +$ + + + + + + +aicertify: 150 declared, 14 measured + + + + + + +$ +a +i +c +e +r +t +i +f +y +s +c +o +r +e +- +c +a +r +d +b +e +r +t +- +b +a +s +e +- +u +n +c +a +s +e +d +bert-base-uncased +c +o +m +p +l +e +t +e +n +e +s +s +0 +. +4 +9 +( +t +h +r +e +s +h +o +l +d +0 +. +8 +) + quality 0.66 +B +E +L +O +W +T +H +R +E +S +H +O +L +D + intended_use 1.00 #################### + training_data 0.67 ############# + caveats_recommendations 0.50 ########## + factors 0.50 ########## + metrics 0.50 ########## + quantitative_analyses 0.50 ########## + ethical_considerations 0.33 ####### + model_details 0.33 ###### + evaluation_data 0.00 +A model card answers part of what Annex IV asks and then stops. +This is the documentation-completeness gap, not a compliance verdict. +scored by GOPAL 2.0.0 (global/v1/documentation/model_card_score), via aicertify 0.7.0 +$ +# +s +o +w +h +a +t +d +o +e +s +t +h +e +E +U +A +I +A +c +t +a +c +t +u +a +l +k +l +y +a +s +k +f +o +r +? +$ +a +i +c +e +r +t +i +f +y +e +x +p +l +a +i +n +e +u +_ +a +i +_ +a +c +t +| +h +e +a +d +- +1 +2 +eu_ai_act — 29 policies +F +i +e +l +d +s +y +o +u +m +u +s +t +d +e +c +l +a +r +e +( +1 +5 +0 +) + No evaluator can observe these. They are facts about your system, + your process, or your paperwork, so you assert them in the contract. + accuracy.declared_in_instructions technical_robustness + accuracy.metrics_declared technical_robustness + assessment.completed conformity_assessment + assessment.harmonised_standards_applied conformity_assessment + assessment.notified_body_involved conformity_assessment +$ +a +i +c +e +r +t +i +f +y +e +x +p +l +a +i +n +e +u +_ +a +i +_ +a +c +t +| +g +r +e +p +- +A +8 +" +p +r +o +d +u +c +e +d +b +y +e +v +a +l +u +a +t +o +r +s +" +F +i +e +l +d +s +p +r +o +d +u +c +e +d +b +y +e +v +a +l +u +a +t +o +r +s +( +1 +4 +) + Computed at evaluation time from your interactions. Do not hand-write +t +h +e +s +e +: +a +s +s +e +r +t +i +n +g +y +o +u +r +o +w +n +f +a +i +r +n +e +s +s +s +c +o +r +e +d +e +f +e +a +t +s +t +h +e +p +o +i +n +t +. + metrics.content_safety.score eu_fairness [content_safety evaluator] + metrics.emotion_recognition.combined_score emotion_recognition + metrics.emotion_recognition.educational_context emotion_recognition + metrics.emotion_recognition.workplace_context emotion_recognition + metrics.fairness.gender_bias eu_fairness [fairness evaluator] +$ +$ + diff --git a/docs/demo/aicertify-short.cast b/docs/demo/aicertify-short.cast new file mode 100644 index 0000000..c827fe8 --- /dev/null +++ b/docs/demo/aicertify-short.cast @@ -0,0 +1,264 @@ +{"version": 2, "width": 100, "height": 30, "idle_time_limit": 3.0, "title": "AICertify: 150 declared, 14 measured"} +[0.6, "o", "$ "] +[0.6857, "o", "a"] +[0.7481, "o", "i"] +[0.8326, "o", "c"] +[0.8772, "o", "e"] +[0.9482, "o", "r"] +[0.993, "o", "t"] +[1.0405, "o", "i"] +[1.1202, "o", "f"] +[1.1796, "o", "y"] +[1.2503, "o", " "] +[1.3196, "o", "s"] +[1.3809, "o", "c"] +[1.45, "o", "o"] +[1.5289, "o", "r"] +[1.5857, "o", "e"] +[1.6409, "o", "-"] +[1.7185, "o", "c"] +[1.8039, "o", "a"] +[1.8679, "o", "r"] +[1.9161, "o", "d"] +[1.9636, "o", " "] +[2.0197, "o", "b"] +[2.0779, "o", "e"] +[2.1447, "o", "r"] +[2.2007, "o", "t"] +[2.2876, "o", "-"] +[2.3404, "o", "b"] +[2.4044, "o", "a"] +[2.4619, "o", "s"] +[2.5101, "o", "e"] +[2.5927, "o", "-"] +[2.661, "o", "u"] +[2.7296, "o", "n"] +[2.7831, "o", "c"] +[2.8522, "o", "a"] +[2.8905, "o", "s"] +[2.9325, "o", "e"] +[2.9761, "o", "d"] +[3.3961, "o", "\r\n"] +[5.4561, "o", "\r\nbert-base-uncased\r\n completeness 0.49 (threshold 0.8)\r\n"] +[5.5461, "o", " quality 0.66\r\n BELOW THRESHOLD\r\n\r\n"] +[5.6361, "o", " intended_use 1.00 ####################\r\n training_data 0.67 #############\r\n caveats_recommendations 0.50 ##########\r\n"] +[5.7261, "o", " factors 0.50 ##########\r\n metrics 0.50 ##########\r\n quantitative_analyses 0.50 ##########\r\n"] +[5.8161, "o", " ethical_considerations 0.33 #######\r\n model_details 0.33 ######\r\n evaluation_data 0.00 \r\n"] +[5.9061, "o", "\r\nA model card answers part of what Annex IV asks and then stops.\r\nThis is the documentation-completeness gap, not a compliance verdict.\r\n"] +[5.9961, "o", "\r\nscored by GOPAL 2.0.0 (global/v1/documentation/model_card_score), via aicertify 0.7.0\r\n"] +[6.8861, "o", "\u001b7\u001b[18A\r\u001b[16C\u001b[43;30m0\u001b[0m\u001b8"] +[6.9141, "o", "\u001b7\u001b[18A\r\u001b[16C\u001b[43;30m0.\u001b[0m\u001b8"] +[6.9421, "o", "\u001b7\u001b[18A\r\u001b[16C\u001b[43;30m0.4\u001b[0m\u001b8"] +[6.9701, "o", "\u001b7\u001b[18A\r\u001b[16C\u001b[43;30m0.49\u001b[0m\u001b8"] +[7.3381, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mB\u001b[0m\u001b8"] +[7.3661, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBE\u001b[0m\u001b8"] +[7.3941, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBEL\u001b[0m\u001b8"] +[7.4221, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELO\u001b[0m\u001b8"] +[7.4501, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW\u001b[0m\u001b8"] +[7.4781, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW \u001b[0m\u001b8"] +[7.5061, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW T\u001b[0m\u001b8"] +[7.5341, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW TH\u001b[0m\u001b8"] +[7.5621, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THR\u001b[0m\u001b8"] +[7.5901, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRE\u001b[0m\u001b8"] +[7.6181, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRES\u001b[0m\u001b8"] +[7.6461, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRESH\u001b[0m\u001b8"] +[7.6741, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRESHO\u001b[0m\u001b8"] +[7.7021, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRESHOL\u001b[0m\u001b8"] +[7.7301, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRESHOLD\u001b[0m\u001b8"] +[13.4981, "o", "$ "] +[13.5533, "o", "#"] +[13.6042, "o", " "] +[13.6675, "o", "s"] +[13.7216, "o", "o"] +[13.7668, "o", " "] +[13.8414, "o", "w"] +[13.901, "o", "h"] +[13.98, "o", "a"] +[14.036, "o", "t"] +[14.2622, "o", " "] +[14.3358, "o", "d"] +[14.407, "o", "o"] +[14.452, "o", "e"] +[14.5034, "o", "s"] +[14.656, "o", " "] +[14.7291, "o", "t"] +[14.8083, "o", "h"] +[14.8639, "o", "e"] +[14.9825, "o", " "] +[15.0553, "o", "E"] +[15.1175, "o", "U"] +[15.2782, "o", " "] +[15.3602, "o", "A"] +[15.4087, "o", "I"] +[15.4983, "o", " "] +[15.5678, "o", "A"] +[15.6207, "o", "c"] +[15.7021, "o", "t"] +[15.8514, "o", " "] +[15.9182, "o", "a"] +[15.9948, "o", "c"] +[16.0738, "o", "t"] +[16.1586, "o", "u"] +[16.2018, "o", "a"] +[16.2789, "o", "l"] +[16.3431, "o", "k"] +[16.7652, "o", "\b \b"] +[16.9292, "o", "l"] +[17.0206, "o", "y"] +[17.1123, "o", " "] +[17.1793, "o", "a"] +[17.2189, "o", "s"] +[17.2605, "o", "k"] +[17.304, "o", " "] +[17.3619, "o", "f"] +[17.432, "o", "o"] +[17.4911, "o", "r"] +[17.5342, "o", "?"] +[17.9542, "o", "\r\n"] +[18.6142, "o", "$ "] +[18.6746, "o", "a"] +[18.7332, "o", "i"] +[18.8196, "o", "c"] +[18.8938, "o", "e"] +[18.9528, "o", "r"] +[19.0121, "o", "t"] +[19.0838, "o", "i"] +[19.1662, "o", "f"] +[19.2485, "o", "y"] +[19.4441, "o", " "] +[19.5332, "o", "e"] +[19.625, "o", "x"] +[19.6876, "o", "p"] +[19.7399, "o", "l"] +[19.8257, "o", "a"] +[19.8648, "o", "i"] +[19.9051, "o", "n"] +[19.9678, "o", " "] +[20.0131, "o", "e"] +[20.075, "o", "u"] +[20.1434, "o", "_"] +[20.1838, "o", "a"] +[20.2341, "o", "i"] +[20.2763, "o", "_"] +[20.3213, "o", "a"] +[20.3978, "o", "c"] +[20.4606, "o", "t"] +[20.5349, "o", " "] +[20.587, "o", "|"] +[20.7452, "o", " "] +[20.7897, "o", "h"] +[20.8367, "o", "e"] +[20.8764, "o", "a"] +[20.9218, "o", "d"] +[20.9953, "o", " "] +[21.0738, "o", "-"] +[21.1456, "o", "1"] +[21.2333, "o", "2"] +[21.6533, "o", "\r\n"] +[23.7133, "o", "\r\neu_ai_act \u2014 29 policies\r\n\r\n"] +[23.8033, "o", "Fields you must declare (150)\r\n No evaluator can observe these. They are facts about your system,\r\n your process, or your paperwork, so you assert them in the contract.\r\n"] +[23.8933, "o", "\r\n accuracy.declared_in_instructions technical_robustness\r\n accuracy.metrics_declared technical_robustness\r\n"] +[23.9833, "o", " assessment.completed conformity_assessment\r\n assessment.harmonised_standards_applied conformity_assessment\r\n assessment.notified_body_involved conformity_assessment\r\n"] +[24.7733, "o", "\u001b7\u001b[9A\r\u001b[25C\u001b[43;30m1\u001b[0m\u001b8"] +[24.8013, "o", "\u001b7\u001b[9A\r\u001b[25C\u001b[43;30m15\u001b[0m\u001b8"] +[24.8293, "o", "\u001b7\u001b[9A\r\u001b[25C\u001b[43;30m150\u001b[0m\u001b8"] +[27.7973, "o", "$ "] +[27.8433, "o", "a"] +[27.9297, "o", "i"] +[28.0189, "o", "c"] +[28.07, "o", "e"] +[28.1519, "o", "r"] +[28.219, "o", "t"] +[28.2995, "o", "i"] +[28.3838, "o", "f"] +[28.4619, "o", "y"] +[28.519, "o", " "] +[28.6104, "o", "e"] +[28.6649, "o", "x"] +[28.7462, "o", "p"] +[28.7963, "o", "l"] +[28.8422, "o", "a"] +[28.9155, "o", "i"] +[28.9987, "o", "n"] +[29.1715, "o", " "] +[29.2417, "o", "e"] +[29.281, "o", "u"] +[29.321, "o", "_"] +[29.4052, "o", "a"] +[29.447, "o", "i"] +[29.4953, "o", "_"] +[29.5566, "o", "a"] +[29.5965, "o", "c"] +[29.6743, "o", "t"] +[29.8469, "o", " "] +[29.929, "o", "|"] +[30.0082, "o", " "] +[30.0598, "o", "g"] +[30.125, "o", "r"] +[30.1909, "o", "e"] +[30.2435, "o", "p"] +[30.3152, "o", " "] +[30.3924, "o", "-"] +[30.4637, "o", "A"] +[30.5366, "o", "8"] +[30.5898, "o", " "] +[30.6284, "o", "\""] +[30.6883, "o", "p"] +[30.7435, "o", "r"] +[30.7848, "o", "o"] +[30.8245, "o", "d"] +[30.9021, "o", "u"] +[30.9473, "o", "c"] +[30.9886, "o", "e"] +[31.0293, "o", "d"] +[31.0939, "o", " "] +[31.1713, "o", "b"] +[31.2282, "o", "y"] +[31.2966, "o", " "] +[31.368, "o", "e"] +[31.4421, "o", "v"] +[31.5281, "o", "a"] +[31.5911, "o", "l"] +[31.6516, "o", "u"] +[31.7086, "o", "a"] +[31.7847, "o", "t"] +[31.8314, "o", "o"] +[31.8898, "o", "r"] +[31.9389, "o", "s"] +[32.004, "o", "\""] +[32.424, "o", "\r\n"] +[34.484, "o", "Fields produced by evaluators (14)\r\n Computed at evaluation time from your interactions. Do not hand-write\r\n these: asserting your own fairness score defeats the point.\r\n"] +[34.574, "o", "\r\n metrics.content_safety.score eu_fairness [content_safety evaluator]\r\n metrics.emotion_recognition.combined_score emotion_recognition\r\n"] +[34.664, "o", " metrics.emotion_recognition.educational_context emotion_recognition\r\n metrics.emotion_recognition.workplace_context emotion_recognition\r\n metrics.fairness.gender_bias eu_fairness [fairness evaluator]\r\n"] +[35.454, "o", "\u001b7\u001b[9A\r\u001b[31C\u001b[43;30m1\u001b[0m\u001b8"] +[35.482, "o", "\u001b7\u001b[9A\r\u001b[31C\u001b[43;30m14\u001b[0m\u001b8"] +[35.85, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30md\u001b[0m\u001b8"] +[35.878, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mde\u001b[0m\u001b8"] +[35.906, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdef\u001b[0m\u001b8"] +[35.934, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefe\u001b[0m\u001b8"] +[35.962, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefea\u001b[0m\u001b8"] +[35.99, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeat\u001b[0m\u001b8"] +[36.018, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats\u001b[0m\u001b8"] +[36.046, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats \u001b[0m\u001b8"] +[36.074, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats t\u001b[0m\u001b8"] +[36.102, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats th\u001b[0m\u001b8"] +[36.13, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the\u001b[0m\u001b8"] +[36.158, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the \u001b[0m\u001b8"] +[36.186, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the p\u001b[0m\u001b8"] +[36.214, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the po\u001b[0m\u001b8"] +[36.242, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the poi\u001b[0m\u001b8"] +[36.27, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the poin\u001b[0m\u001b8"] +[36.298, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the point\u001b[0m\u001b8"] +[42.066, "o", "$ "] +[42.066, "o", "\u2588"] +[42.596, "o", "\b \b"] +[43.126, "o", "\u2588"] +[43.656, "o", "\b \b"] +[44.186, "o", "\u2588"] +[44.716, "o", "\b \b"] +[45.246, "o", "\u2588"] +[45.776, "o", "\b \b"] +[46.306, "o", "\u2588"] +[46.836, "o", "\b \b"] +[47.366, "o", "\u2588"] +[47.896, "o", "\b \b"] diff --git a/docs/demo/aicertify.cast b/docs/demo/aicertify.cast new file mode 100644 index 0000000..84700db --- /dev/null +++ b/docs/demo/aicertify.cast @@ -0,0 +1,534 @@ +{"version": 2, "width": 100, "height": 30, "idle_time_limit": 3.0, "title": "AICertify: what it measures, and what it cannot"} +[0.6, "o", "$ "] +[0.663, "o", "a"] +[0.7371, "o", "i"] +[0.8269, "o", "c"] +[0.8667, "o", "e"] +[0.9498, "o", "r"] +[1.0368, "o", "t"] +[1.1269, "o", "i"] +[1.1942, "o", "f"] +[1.2494, "o", "y"] +[1.4363, "o", " "] +[1.5075, "o", "s"] +[1.5812, "o", "c"] +[1.6367, "o", "o"] +[1.7021, "o", "e"] +[2.1254, "o", "\b \b"] +[2.3135, "o", "r"] +[2.3604, "o", "e"] +[2.4345, "o", "-"] +[2.5228, "o", "c"] +[2.6116, "o", "a"] +[2.7002, "o", "r"] +[2.7515, "o", "d"] +[2.8069, "o", " "] +[2.8613, "o", "b"] +[2.918, "o", "e"] +[2.9962, "o", "r"] +[3.037, "o", "t"] +[3.0848, "o", "-"] +[3.1355, "o", "b"] +[3.1939, "o", "a"] +[3.2771, "o", "s"] +[3.3624, "o", "e"] +[3.4141, "o", "-"] +[3.4592, "o", "u"] +[3.5188, "o", "n"] +[3.588, "o", "c"] +[3.6299, "o", "a"] +[3.6839, "o", "s"] +[3.7495, "o", "e"] +[3.8127, "o", "d"] +[4.2327, "o", "\r\n"] +[6.2927, "o", "\r\nbert-base-uncased\r\n completeness 0.49 (threshold 0.8)\r\n"] +[6.3827, "o", " quality 0.66\r\n BELOW THRESHOLD\r\n\r\n"] +[6.4727, "o", " intended_use 1.00 ####################\r\n training_data 0.67 #############\r\n caveats_recommendations 0.50 ##########\r\n"] +[6.5627, "o", " factors 0.50 ##########\r\n metrics 0.50 ##########\r\n quantitative_analyses 0.50 ##########\r\n"] +[6.6527, "o", " ethical_considerations 0.33 #######\r\n model_details 0.33 ######\r\n evaluation_data 0.00 \r\n"] +[6.7427, "o", "\r\nA model card answers part of what Annex IV asks and then stops.\r\nThis is the documentation-completeness gap, not a compliance verdict.\r\n"] +[6.8327, "o", "\r\nscored by GOPAL 2.0.0 (global/v1/documentation/model_card_score), via aicertify 0.7.0\r\n"] +[7.7227, "o", "\u001b7\u001b[18A\r\u001b[16C\u001b[43;30m0\u001b[0m\u001b8"] +[7.7507, "o", "\u001b7\u001b[18A\r\u001b[16C\u001b[43;30m0.\u001b[0m\u001b8"] +[7.7787, "o", "\u001b7\u001b[18A\r\u001b[16C\u001b[43;30m0.4\u001b[0m\u001b8"] +[7.8067, "o", "\u001b7\u001b[18A\r\u001b[16C\u001b[43;30m0.49\u001b[0m\u001b8"] +[8.1747, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mB\u001b[0m\u001b8"] +[8.2027, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBE\u001b[0m\u001b8"] +[8.2307, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBEL\u001b[0m\u001b8"] +[8.2587, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELO\u001b[0m\u001b8"] +[8.2867, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW\u001b[0m\u001b8"] +[8.3147, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW \u001b[0m\u001b8"] +[8.3427, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW T\u001b[0m\u001b8"] +[8.3707, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW TH\u001b[0m\u001b8"] +[8.3987, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THR\u001b[0m\u001b8"] +[8.4267, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRE\u001b[0m\u001b8"] +[8.4547, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRES\u001b[0m\u001b8"] +[8.4827, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRESH\u001b[0m\u001b8"] +[8.5107, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRESHO\u001b[0m\u001b8"] +[8.5387, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRESHOL\u001b[0m\u001b8"] +[8.5667, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRESHOLD\u001b[0m\u001b8"] +[14.3347, "o", "$ "] +[14.3728, "o", "#"] +[14.4216, "o", " "] +[14.4751, "o", "s"] +[14.5457, "o", "o"] +[14.6038, "o", " "] +[14.6894, "o", "w"] +[14.7391, "o", "h"] +[14.8281, "o", "a"] +[14.8792, "o", "t"] +[14.9428, "o", " "] +[15.0337, "o", "d"] +[15.1247, "o", "o"] +[15.2054, "o", "e"] +[15.261, "o", "s"] +[15.3275, "o", " "] +[15.3931, "o", "t"] +[15.4326, "o", "h"] +[15.5002, "o", "e"] +[15.5741, "o", " "] +[15.6338, "o", "E"] +[15.6756, "o", "U"] +[15.7291, "o", " "] +[15.8146, "o", "A"] +[15.8902, "o", "I"] +[15.9434, "o", " "] +[16.0148, "o", "A"] +[16.0948, "o", "c"] +[16.146, "o", "t"] +[16.1997, "o", " "] +[16.2682, "o", "a"] +[16.308, "o", "c"] +[16.3465, "o", "t"] +[16.3926, "o", "u"] +[16.4409, "o", "a"] +[16.5198, "o", "l"] +[16.58, "o", "l"] +[16.62, "o", "y"] +[16.7305, "o", " "] +[16.8186, "o", "a"] +[16.8661, "o", "s"] +[16.936, "o", "k"] +[17.004, "o", " "] +[17.0739, "o", "f"] +[17.1166, "o", "o"] +[17.1599, "o", "r"] +[17.2375, "o", "?"] +[17.6575, "o", "\r\n"] +[18.3175, "o", "$ "] +[18.3736, "o", "a"] +[18.4514, "o", "i"] +[18.5393, "o", "c"] +[18.6178, "o", "e"] +[18.6876, "o", "r"] +[18.7719, "o", "t"] +[18.8375, "o", "i"] +[18.8849, "o", "f"] +[18.948, "o", "y"] +[19.12, "o", " "] +[19.2032, "o", "e"] +[19.2881, "o", "x"] +[19.3525, "o", "p"] +[19.4241, "o", "l"] +[19.4642, "o", "a"] +[19.5344, "o", "i"] +[19.6001, "o", "n"] +[19.6664, "o", " "] +[19.727, "o", "e"] +[19.7846, "o", "u"] +[19.8725, "o", "_"] +[19.9425, "o", "a"] +[19.9966, "o", "i"] +[20.0526, "o", "_"] +[20.1064, "o", "s"] +[20.5153, "o", "\b \b"] +[20.7183, "o", "a"] +[20.7907, "o", "c"] +[20.8689, "o", "t"] +[21.0413, "o", " "] +[21.0899, "o", "|"] +[21.169, "o", " "] +[21.2214, "o", "h"] +[21.2811, "o", "e"] +[21.3388, "o", "a"] +[21.3854, "o", "d"] +[21.4432, "o", " "] +[21.5249, "o", "-"] +[21.5912, "o", "1"] +[21.646, "o", "2"] +[22.066, "o", "\r\n"] +[24.126, "o", "\r\neu_ai_act \u2014 29 policies\r\n\r\n"] +[24.216, "o", "Fields you must declare (150)\r\n No evaluator can observe these. They are facts about your system,\r\n your process, or your paperwork, so you assert them in the contract.\r\n"] +[24.306, "o", "\r\n accuracy.declared_in_instructions technical_robustness\r\n accuracy.metrics_declared technical_robustness\r\n"] +[24.396, "o", " assessment.completed conformity_assessment\r\n assessment.harmonised_standards_applied conformity_assessment\r\n assessment.notified_body_involved conformity_assessment\r\n"] +[25.186, "o", "\u001b7\u001b[9A\r\u001b[25C\u001b[43;30m1\u001b[0m\u001b8"] +[25.214, "o", "\u001b7\u001b[9A\r\u001b[25C\u001b[43;30m15\u001b[0m\u001b8"] +[25.242, "o", "\u001b7\u001b[9A\r\u001b[25C\u001b[43;30m150\u001b[0m\u001b8"] +[28.21, "o", "$ "] +[28.2514, "o", "a"] +[28.3117, "o", "i"] +[28.3522, "o", "c"] +[28.4074, "o", "e"] +[28.4549, "o", "r"] +[28.5189, "o", "t"] +[28.5999, "o", "i"] +[28.6522, "o", "f"] +[28.7288, "o", "y"] +[28.7878, "o", " "] +[28.855, "o", "e"] +[28.9184, "o", "x"] +[28.9987, "o", "p"] +[29.0767, "o", "l"] +[29.1447, "o", "a"] +[29.2237, "o", "i"] +[29.272, "o", "n"] +[29.4492, "o", " "] +[29.5169, "o", "e"] +[29.5878, "o", "u"] +[29.6708, "o", "_"] +[29.7474, "o", "a"] +[29.8036, "o", "i"] +[29.8704, "o", "_"] +[29.9512, "o", "a"] +[30.0082, "o", "c"] +[30.0582, "o", "r"] +[30.4082, "o", "\b \b"] +[30.6003, "o", "t"] +[30.687, "o", " "] +[30.746, "o", "|"] +[30.9139, "o", " "] +[30.9762, "o", "g"] +[31.0533, "o", "r"] +[31.1333, "o", "e"] +[31.1925, "o", "p"] +[31.2527, "o", " "] +[31.3087, "o", "-"] +[31.3607, "o", "A"] +[31.4381, "o", "8"] +[31.6029, "o", " "] +[31.6572, "o", "\""] +[31.7332, "o", "p"] +[31.8124, "o", "r"] +[31.8912, "o", "o"] +[31.9601, "o", "d"] +[32.0083, "o", "u"] +[32.0643, "o", "c"] +[32.1034, "o", "e"] +[32.1912, "o", "d"] +[32.3943, "o", " "] +[32.446, "o", "b"] +[32.5336, "o", "y"] +[32.765, "o", " "] +[32.8458, "o", "e"] +[32.9108, "o", "v"] +[32.9632, "o", "a"] +[33.0423, "o", "l"] +[33.1011, "o", "u"] +[33.1434, "o", "a"] +[33.2204, "o", "t"] +[33.289, "o", "o"] +[33.3575, "o", "r"] +[33.4416, "o", "s"] +[33.5174, "o", "\""] +[33.9374, "o", "\r\n"] +[35.9974, "o", "Fields produced by evaluators (14)\r\n Computed at evaluation time from your interactions. Do not hand-write\r\n these: asserting your own fairness score defeats the point.\r\n"] +[36.0874, "o", "\r\n metrics.content_safety.score eu_fairness [content_safety evaluator]\r\n metrics.emotion_recognition.combined_score emotion_recognition\r\n"] +[36.1774, "o", " metrics.emotion_recognition.educational_context emotion_recognition\r\n metrics.emotion_recognition.workplace_context emotion_recognition\r\n metrics.fairness.gender_bias eu_fairness [fairness evaluator]\r\n"] +[36.9674, "o", "\u001b7\u001b[9A\r\u001b[31C\u001b[43;30m1\u001b[0m\u001b8"] +[36.9954, "o", "\u001b7\u001b[9A\r\u001b[31C\u001b[43;30m14\u001b[0m\u001b8"] +[37.3634, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30md\u001b[0m\u001b8"] +[37.3914, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mde\u001b[0m\u001b8"] +[37.4194, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdef\u001b[0m\u001b8"] +[37.4474, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefe\u001b[0m\u001b8"] +[37.4754, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefea\u001b[0m\u001b8"] +[37.5034, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeat\u001b[0m\u001b8"] +[37.5314, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats\u001b[0m\u001b8"] +[37.5594, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats \u001b[0m\u001b8"] +[37.5874, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats t\u001b[0m\u001b8"] +[37.6154, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats th\u001b[0m\u001b8"] +[37.6434, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the\u001b[0m\u001b8"] +[37.6714, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the \u001b[0m\u001b8"] +[37.6994, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the p\u001b[0m\u001b8"] +[37.7274, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the po\u001b[0m\u001b8"] +[37.7554, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the poi\u001b[0m\u001b8"] +[37.7834, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the poin\u001b[0m\u001b8"] +[37.8114, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the point\u001b[0m\u001b8"] +[43.5794, "o", "$ "] +[43.6385, "o", "a"] +[43.6917, "o", "i"] +[43.7727, "o", "c"] +[43.8634, "o", "e"] +[43.9259, "o", "r"] +[43.9854, "o", "t"] +[44.042, "o", "i"] +[44.1211, "o", "f"] +[44.16, "o", "y"] +[44.3086, "o", " "] +[44.3876, "o", "i"] +[44.4676, "o", "n"] +[44.5508, "o", "i"] +[44.6033, "o", "t"] +[44.6617, "o", "-"] +[44.734, "o", "c"] +[44.7916, "o", "o"] +[44.8625, "o", "n"] +[44.9534, "o", "t"] +[45.0147, "o", "r"] +[45.0767, "o", "a"] +[45.1332, "o", "c"] +[45.1976, "o", "t"] +[45.2691, "o", " "] +[45.3283, "o", "0"] +[45.6653, "o", "\b \b"] +[45.8623, "o", "-"] +[45.9455, "o", "-"] +[45.9921, "o", "p"] +[46.0499, "o", "o"] +[46.1131, "o", "l"] +[46.1849, "o", "i"] +[46.2709, "o", "c"] +[46.3439, "o", "y"] +[46.5273, "o", " "] +[46.5701, "o", "e"] +[46.6335, "o", "u"] +[46.7152, "o", "_"] +[46.807, "o", "a"] +[46.8723, "o", "i"] +[46.9585, "o", "_"] +[46.9969, "o", "a"] +[47.0447, "o", "c"] +[47.0982, "o", "t"] +[47.1756, "o", " "] +[47.2153, "o", ">"] +[47.2948, "o", " "] +[47.351, "o", "c"] +[47.4214, "o", "o"] +[47.4799, "o", "n"] +[47.5598, "o", "t"] +[47.6278, "o", "r"] +[47.6705, "o", "a"] +[47.734, "o", "c"] +[47.8129, "o", "t"] +[47.8936, "o", "."] +[47.9715, "o", "j"] +[48.0437, "o", "s"] +[48.1216, "o", "o"] +[48.2074, "o", "n"] +[48.6274, "o", "\r\n"] +[51.5874, "o", "$ "] +[51.6272, "o", "a"] +[51.7191, "o", "i"] +[51.7606, "o", "c"] +[51.8272, "o", "e"] +[51.8975, "o", "r"] +[51.9475, "o", "t"] +[52.0156, "o", "i"] +[52.1, "o", "f"] +[52.1779, "o", "y"] +[52.3785, "o", " "] +[52.4637, "o", "e"] +[52.5155, "o", "v"] +[52.5743, "o", "a"] +[52.6317, "o", "l"] +[52.6783, "o", "u"] +[52.7438, "o", "a"] +[52.8299, "o", "t"] +[52.8931, "o", "e"] +[52.9803, "o", " "] +[53.0393, "o", "-"] +[53.1041, "o", "-"] +[53.1831, "o", "c"] +[53.2314, "o", "o"] +[53.2756, "o", "n"] +[53.346, "o", "t"] +[53.433, "o", "r"] +[53.4931, "o", "a"] +[53.5609, "o", "c"] +[53.6446, "o", "t"] +[53.7168, "o", " "] +[53.7934, "o", "c"] +[53.8616, "o", "o"] +[53.9169, "o", "n"] +[53.9696, "o", "t"] +[54.0341, "o", "r"] +[54.1098, "o", "a"] +[54.1802, "o", "c"] +[54.2676, "o", "t"] +[54.3384, "o", "."] +[54.4258, "o", "j"] +[54.5065, "o", "s"] +[54.563, "o", "o"] +[54.6023, "o", "n"] +[54.6764, "o", " "] +[54.7407, "o", "-"] +[54.7891, "o", "-"] +[54.8412, "o", "p"] +[54.8814, "o", "o"] +[54.9483, "o", "l"] +[54.9985, "o", "i"] +[55.0459, "o", "c"] +[55.1294, "o", "y"] +[55.3245, "o", " "] +[55.3683, "o", "e"] +[55.4425, "o", "u"] +[55.4895, "o", "_"] +[55.5737, "o", "a"] +[55.627, "o", "i"] +[55.7134, "o", "_"] +[55.7583, "o", "a"] +[55.8154, "o", "c"] +[55.8796, "o", "t"] +[56.2996, "o", "\r\n"] +[58.3596, "o", "Evaluation Summary:\r\nContract ID: b7e60f21-6717-4ebd-b83f-dd6927dd5c52\r\nApplication: your-application\r\n"] +[58.4496, "o", "Report: ./reports/folder_report_your-application_2026-08-30_165312.md\r\nOPA Evaluation: Successful\r\n"] +[59.3396, "o", "$ "] +[59.4066, "o", "h"] +[59.4879, "o", "e"] +[59.5623, "o", "a"] +[59.652, "o", "d"] +[59.73, "o", " "] +[59.7905, "o", "-"] +[59.8812, "o", "8"] +[59.962, "o", " "] +[60.0213, "o", "e"] +[60.3724, "o", "\b \b"] +[60.5701, "o", "r"] +[60.6452, "o", "e"] +[60.7252, "o", "p"] +[60.7674, "o", "o"] +[60.8375, "o", "r"] +[60.8975, "o", "t"] +[60.9699, "o", "s"] +[61.0196, "o", "/"] +[61.08, "o", "*"] +[61.138, "o", "."] +[61.1952, "o", "m"] +[61.287, "o", "d"] +[61.707, "o", "\r\n"] +[62.167, "o", "# Evaluation Report\r\n\r\n**Analysis Date:** 2026-08-30 11:23:12\r\n"] +[62.257, "o", "\r\n## Evaluation Summary\r\n- **Total Policies:** 29\r\n"] +[62.347, "o", "- **Green Count:** 0\r\n- **Red Count:** 29\r\n"] +[63.137, "o", "\u001b7\u001b[1A\r\u001b[17C\u001b[43;30m2\u001b[0m\u001b8"] +[63.165, "o", "\u001b7\u001b[1A\r\u001b[17C\u001b[43;30m29\u001b[0m\u001b8"] +[63.533, "o", "\u001b7\u001b[2A\r\u001b[19C\u001b[43;30m0\u001b[0m\u001b8"] +[69.301, "o", "$ "] +[69.3882, "o", "#"] +[69.4743, "o", " "] +[69.5451, "o", "w"] +[69.6227, "o", "h"] +[69.7075, "o", "i"] +[69.7818, "o", "c"] +[69.828, "o", "h"] +[69.9057, "o", " "] +[69.9455, "o", "m"] +[69.9909, "o", "e"] +[70.0551, "o", "t"] +[70.0977, "o", "r"] +[70.1655, "o", "i"] +[70.2543, "o", "c"] +[70.2991, "o", "s"] +[70.4012, "o", " "] +[70.4616, "o", "c"] +[70.5501, "o", "a"] +[70.6038, "o", "n"] +[70.6874, "o", " "] +[70.7728, "o", "a"] +[70.8183, "o", "n"] +[70.9047, "o", " "] +[70.9633, "o", "e"] +[71.0446, "o", "v"] +[71.1189, "o", "a"] +[71.2031, "o", "l"] +[71.2921, "o", "u"] +[71.3711, "o", "a"] +[71.4169, "o", "t"] +[71.4596, "o", "o"] +[71.5339, "o", "r"] +[71.7245, "o", " "] +[71.7665, "o", "s"] +[71.8055, "o", "u"] +[71.8918, "o", "p"] +[71.9756, "o", "p"] +[72.03, "o", "l"] +[72.0698, "o", "y"] +[72.1141, "o", " "] +[72.1811, "o", "t"] +[72.2331, "o", "o"] +[72.3002, "o", "d"] +[72.3897, "o", "a"] +[72.4633, "o", "y"] +[72.5286, "o", "?"] +[72.9486, "o", "\r\n"] +[73.6086, "o", "$ "] +[73.6944, "o", "p"] +[73.7683, "o", "y"] +[73.8566, "o", "t"] +[73.9233, "o", "h"] +[74.0105, "o", "o"] +[74.0865, "o", "n"] +[74.2597, "o", " "] +[74.3029, "o", "s"] +[74.3827, "o", "c"] +[74.4661, "o", "r"] +[74.5439, "o", "i"] +[74.6249, "o", "p"] +[74.7139, "o", "t"] +[74.797, "o", "s"] +[74.8734, "o", "/"] +[74.9156, "o", "m"] +[74.9747, "o", "e"] +[75.0364, "o", "t"] +[75.1187, "o", "r"] +[75.1817, "o", "i"] +[75.261, "o", "c"] +[75.3511, "o", "_"] +[75.3951, "o", "g"] +[75.4514, "o", "a"] +[75.5039, "o", "p"] +[75.594, "o", "_"] +[75.6727, "o", "r"] +[75.7639, "o", "e"] +[75.8538, "o", "p"] +[75.9081, "o", "o"] +[75.9502, "o", "r"] +[76.0355, "o", "t"] +[76.0967, "o", "."] +[76.1828, "o", "p"] +[76.2418, "o", "y"] +[76.3131, "o", " "] +[76.3935, "o", "|"] +[76.5115, "o", " "] +[76.5943, "o", "t"] +[76.665, "o", "a"] +[76.7531, "o", "i"] +[76.8212, "o", "l"] +[76.8663, "o", " "] +[76.9249, "o", "-"] +[76.977, "o", "1"] +[77.0476, "o", "2"] +[77.4676, "o", "\r\n"] +[79.5276, "o", " calc metrics.model_card.completeness computed by global.v1.documentation.model_card_score, no evaluator needed\r\n GAP metrics.model_card.compliance_level no evaluator declares this\r\n calc metrics.model_card.quality computed by global.v1.documentation.model_card_score, no evaluator needed\r\n"] +[79.6176, "o", " calc metrics.model_card.section_scores computed by global.v1.documentation.model_card_score, no evaluator needed\r\n ok metrics.social_scoring.combined_score SocialScoringEvaluator\r\n ok metrics.social_scoring.detrimental_treatment SocialScoringEvaluator\r\n"] +[79.7076, "o", " ok metrics.social_scoring.score SocialScoringEvaluator\r\n ok metrics.toxicity.max_toxicity ContentSafetyEvaluator\r\n\r\n"] +[79.7976, "o", "TOTAL: 22 of 27 measured metrics can be supplied today.\r\nThe gaps are what you would need to write an evaluator for, or\r\nsupply by hand. See docs on writing one.\r\n"] +[80.6876, "o", "\u001b7\u001b[3A\r\u001b[7C\u001b[43;30m2\u001b[0m\u001b8"] +[80.7156, "o", "\u001b7\u001b[3A\r\u001b[7C\u001b[43;30m22\u001b[0m\u001b8"] +[80.7436, "o", "\u001b7\u001b[3A\r\u001b[7C\u001b[43;30m22 \u001b[0m\u001b8"] +[80.7716, "o", "\u001b7\u001b[3A\r\u001b[7C\u001b[43;30m22 o\u001b[0m\u001b8"] +[80.7996, "o", "\u001b7\u001b[3A\r\u001b[7C\u001b[43;30m22 of\u001b[0m\u001b8"] +[80.8276, "o", "\u001b7\u001b[3A\r\u001b[7C\u001b[43;30m22 of \u001b[0m\u001b8"] +[80.8556, "o", "\u001b7\u001b[3A\r\u001b[7C\u001b[43;30m22 of 2\u001b[0m\u001b8"] +[80.8836, "o", "\u001b7\u001b[3A\r\u001b[7C\u001b[43;30m22 of 27\u001b[0m\u001b8"] +[86.6516, "o", "$ "] +[86.6516, "o", "\u2588"] +[87.1816, "o", "\b \b"] +[87.7116, "o", "\u2588"] +[88.2416, "o", "\b \b"] +[88.7716, "o", "\u2588"] +[89.3016, "o", "\b \b"] +[89.8316, "o", "\u2588"] +[90.3616, "o", "\b \b"] +[90.8916, "o", "\u2588"] +[91.4216, "o", "\b \b"] +[91.9516, "o", "\u2588"] +[92.4816, "o", "\b \b"] diff --git a/scripts/record-demo.py b/scripts/record-demo.py new file mode 100644 index 0000000..9bbfa6d --- /dev/null +++ b/scripts/record-demo.py @@ -0,0 +1,415 @@ +#!/usr/bin/env python3 +"""Compose the AICertify demo asciicast: real output, typed at a human pace. + +Every command here is actually run and its output captured, so nothing on the +screen is written by hand. What is synthesised is the *timing*. The four +commands take about three minutes between them, most of it downloading model +weights and waiting on OPA, and a cast at real speed is unwatchable. A `script` +capture would also have no typing in it at all, and output that materialises +instantly reads as a screenshot rather than a session. + +Being explicit about which half is real matters. The scores, the field counts, +the verdicts and the report are what the commands printed. The rhythm is +invented, the way any screencast's rhythm is invented by the person recording. + +Four acts, because AICertify does a different job from GOPAL and needs the +room. GOPAL decides things, so its demo is one beat: swap the model, the build +fails. AICertify gathers things, and the interesting thing about it is how +honest it is about the part it cannot gather. + + 1. score-card a real Hugging Face card, scored, in one screen + 2. explain 150 fields you must declare, 14 an evaluator computes + 3. init/eval scaffold a contract, run it, get 0 of 29 + 4. gap report 22 of 27 metrics supplied, and the five that are not + +Act 3 ends on nothing passing. That is the point rather than an embarrassment: +the scaffold asserts none of the 150 declarations, so none of the obligations +that turn on them can be met. It is the same argument act 2 makes, arriving as +a verdict instead of a number. + +Usage: + record-demo.py [short.cast] + +The short cast is acts 1 and 2 only, for the README, where a 90-second loop is +too long and JavaScript does not run. +""" + +from __future__ import annotations + +import json +import random +import math +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +HERE = Path(__file__).resolve().parent +REPO = HERE.parent +PY = str(REPO / ".venv" / "bin" / "python") +CLI = [PY, "-m", "aicertify.cli"] + +# Deterministic jitter. The keystrokes look uneven, and they are the same +# uneven every time, so regenerating does not churn the file in review. +RNG = random.Random(20260830) + +CPS_MIN, CPS_MAX = 0.038, 0.092 # seconds per keystroke +REACH_FOR_ENTER = 0.42 # hand leaves the letters, finds the return key +ENTER_SETTLES = 0.16 # and the press registers before anything happens +FIRST_BYTE = 0.30 # a command does not answer instantly +THINKING = 1.9 # a command that clearly did some work +READ_SHORT = 2.6 +READ_LONG = 5.4 # long enough to read a verdict and understand it +BLINK = 0.53 +BLINKS = 6 +CURSOR = "█" + +SWEEP = 0.028 # per character, the pace of a pen moving +HL_ON = "\x1b[43;30m" +HL_OFF = "\x1b[0m" +SAVE, RESTORE = "\x1b7", "\x1b8" + +TYPO_CHANCE = 0.55 # per command, not per character +NOTICE_TYPO = (0.22, 0.46) +AFTER_FIX = (0.08, 0.19) + +NEIGHBOURS = { + "a": "sq", + "b": "vn", + "c": "xv", + "d": "sf", + "e": "wr", + "f": "dg", + "g": "fh", + "h": "gj", + "i": "uo", + "j": "hk", + "k": "jl", + "l": "k", + "m": "n", + "n": "bm", + "o": "ip", + "p": "o", + "q": "wa", + "r": "et", + "s": "ad", + "t": "ry", + "u": "yi", + "v": "cb", + "w": "qe", + "x": "zc", + "y": "tu", + "z": "x", + ".": "/", + "/": ".", + "-": "0", + "_": "-", +} + +WIDTH, HEIGHT = 100, 30 + + +class Cast: + def __init__(self) -> None: + self.t = 0.0 + self.events: list[list] = [] + + def wait(self, seconds: float) -> None: + self.t += seconds + + def out(self, text: str) -> None: + self.events.append([round(self.t, 4), "o", text]) + + def type(self, text: str, typo_at: int | None = None) -> None: + """One keystroke at a time, with the pauses and the mistakes. + + A typo is the neighbouring key, noticed a moment later, backspaced and + retyped. It is the single thing that separates a recording of somebody + working from text replayed at a plausible speed. + """ + for i, ch in enumerate(text): + if i == typo_at: + wrong = NEIGHBOURS.get(ch.lower()) + if wrong: + self.wait(RNG.uniform(CPS_MIN, CPS_MAX)) + self.out(RNG.choice(wrong)) + self.wait(RNG.uniform(*NOTICE_TYPO)) + self.out("\b \b") + self.wait(RNG.uniform(*AFTER_FIX)) + self.wait(RNG.uniform(CPS_MIN, CPS_MAX)) + if ch == " " and RNG.random() < 0.35: + self.wait(RNG.uniform(0.05, 0.16)) + self.out(ch) + + def command(self, text: str) -> None: + self.out("$ ") + typo_at = None + if len(text) > 14 and RNG.random() < TYPO_CHANCE: + typo_at = RNG.randrange(6, len(text) - 2) + self.type(text, typo_at) + # Pressing return is two beats: reaching for the key, and the press + # landing. Without them a command line ends and output begins in the + # same instant, which nothing on a real keyboard does. + self.wait(REACH_FOR_ENTER) + self.out("\r\n") + self.wait(ENTER_SETTLES) + + def block(self, text: str, first_byte: float = FIRST_BYTE) -> None: + """Output arriving in a few pieces, as a real command does.""" + self.wait(first_byte) + lines = text.rstrip("\n").split("\n") + for i in range(0, len(lines), 3): + self.out("\r\n".join(lines[i : i + 3]) + "\r\n") + self.wait(0.09) + + def blink(self, times: int = BLINKS) -> None: + self.out("$ ") + for _ in range(times): + self.out(CURSOR) + self.wait(BLINK) + self.out("\b \b") + self.wait(BLINK) + + def highlight(self, rows_up: int, col: int, text: str) -> None: + """Draw a highlighter left to right over text already on screen.""" + # CSI 0 C moves the cursor forward one column, not zero: ANSI reads a + # zero parameter as one. Emitting it for a phrase at column 0 draws the + # highlight one place right and leaves the original first letter behind. + forward = f"\x1b[{col}C" if col > 0 else "" + for i in range(1, len(text) + 1): + self.out( + f"{SAVE}\x1b[{rows_up}A\r{forward}" + f"{HL_ON}{text[:i]}{HL_OFF}{RESTORE}" + ) + self.wait(SWEEP) + + def sweep(self, block: str, pairs: list[tuple[str, str]]) -> None: + """Highlight each phrase in a block of output just written. + + The cursor is moved up by *screen rows*, not by lines. A line longer + than the terminal is wrapped by the emulator and occupies several rows, + so counting lines puts the highlighter above where the text actually + is. Two 130-character warnings at the end of one command were enough to + draw `BELOW THRESHOLD` across an unrelated score three rows below it. + """ + lines = block.rstrip("\n").split("\n") + + def rows(line: str) -> int: + return max(1, math.ceil(len(line) / WIDTH)) + + for needle, phrase in pairs: + for idx, line in enumerate(lines): + if needle in line and phrase in line: + up = sum(rows(rest) for rest in lines[idx:]) + self.highlight(up, line.index(phrase), phrase) + self.wait(0.34) + break + + def write(self, dst: Path, title: str) -> None: + header = { + "version": 2, + "width": WIDTH, + "height": HEIGHT, + "idle_time_limit": 3.0, + "title": title, + } + with dst.open("w", encoding="utf-8") as fh: + fh.write(json.dumps(header) + "\n") + for event in self.events: + fh.write(json.dumps(event) + "\n") + print(f"Wrote {dst} ({len(self.events)} events, {self.t:.1f}s)") + + +def run(args: list[str], cwd: Path | None = None, stderr: bool = True) -> str: + done = subprocess.run( + args, cwd=cwd or REPO, capture_output=True, text=True, timeout=900 + ) + out = done.stdout or "" + # stderr is dropped where it carries only environment noise, such as the + # Hugging Face hub advising you to set a token. It is real, but it is not + # the command answering the question, and it would leave the act ending on + # a warning rather than on the verdict. + return out + (done.stderr or "") if stderr else out + + +def shell(cmd: str, cwd: Path | None = None) -> str: + done = subprocess.run( + cmd, cwd=cwd or REPO, shell=True, capture_output=True, text=True, timeout=900 + ) + return (done.stdout or "") + (done.stderr or "") + + +def require(condition: bool, message: str) -> None: + if not condition: + print(f"refusing to record: {message}", file=sys.stderr) + raise SystemExit(1) + + +def main() -> int: + if len(sys.argv) not in (2, 3): + print(__doc__.strip(), file=sys.stderr) + return 2 + full_dst = Path(sys.argv[1]) + short_dst = Path(sys.argv[2]) if len(sys.argv) == 3 else None + + work = Path(tempfile.mkdtemp(prefix="aicertify-demo-")) + try: + print("running the commands (this takes a few minutes)...", file=sys.stderr) + + card = run(CLI + ["score-card", "bert-base-uncased"], stderr=False) + declare = shell( + f"{PY} -m aicertify.cli explain eu_ai_act 2>/dev/null | head -12" + ) + measure = shell( + f"{PY} -m aicertify.cli explain eu_ai_act 2>/dev/null" + ' | grep -A8 "produced by evaluators"' + ) + + contract = work / "contract.json" + shell( + f"{PY} -m aicertify.cli init-contract --policy eu_ai_act " + f"2>/dev/null > {contract}" + ) + require( + contract.exists() and contract.stat().st_size > 1000, + "init-contract produced no usable contract", + ) + # A declarations-only contract. The scaffold ships one placeholder + # interaction, and evaluating it pulls DeepEval into an LLM call that + # needs a provider key. The point of this act is the policy verdict, + # not the toxicity classifier, and act 4 covers the evaluators. + doc = json.loads(contract.read_text()) + doc["interactions"] = [] + contract.write_text(json.dumps(doc, indent=2)) + + evaluate = shell( + f"{PY} -m aicertify.cli evaluate --contract contract.json " + f"--policy eu_ai_act --report-format markdown --output-dir ./reports " + "2>&1 | tail -5", + cwd=work, + ) + summary = shell("head -8 reports/*.md", cwd=work) + gaps = shell(f"{PY} scripts/metric_gap_report.py 2>/dev/null | tail -12") + + # Guards. Each of these is a fact the cast puts on screen, so if the + # tool stops saying it the recording is a lie and must not be written. + require( + "0.49" in card and "BELOW THRESHOLD" in card, + "bert-base-uncased no longer scores 0.49 below the threshold", + ) + require( + "Fields you must declare (150)" in declare, + "explain no longer reports 150 declared fields; it disagrees " + "with the published figure and one of them is wrong", + ) + require( + "Fields produced by evaluators (14)" in measure, + "explain no longer reports 14 measured fields", + ) + require( + "defeats the point" in measure, + "the line explaining why you must not hand-write metrics is gone", + ) + require( + "Red Count:** 29" in summary and "Green Count:** 0" in summary, + "an empty contract no longer fails all 29 policies, so act 3 " + "has lost its point", + ) + require( + "22 of 27" in gaps, + "the metric gap report no longer says 22 of 27; act 4's " + "contribution hook needs the real number", + ) + + acts = build(card, declare, measure, evaluate, summary, gaps) + acts.write(full_dst, "AICertify: what it measures, and what it cannot") + + if short_dst: + short = build_short(card, declare, measure) + short.write(short_dst, "AICertify: 150 declared, 14 measured") + return 0 + finally: + shutil.rmtree(work, ignore_errors=True) + + +def act_one(cast: Cast, card: str) -> None: + """A famous model, scored, with no setup at all.""" + cast.wait(0.6) + cast.command("aicertify score-card bert-base-uncased") + cast.block(card, first_byte=THINKING) + cast.wait(0.8) + cast.sweep( + card, + [ + ("completeness", "0.49"), + ("BELOW THRESHOLD", "BELOW THRESHOLD"), + ], + ) + cast.wait(READ_LONG) + + +def act_two(cast: Cast, declare: str, measure: str) -> None: + """The split that the whole product turns on.""" + cast.command("# so what does the EU AI Act actually ask for?") + cast.wait(0.5) + cast.command("aicertify explain eu_ai_act | head -12") + cast.block(declare, first_byte=THINKING) + cast.wait(0.7) + cast.sweep(declare, [("Fields you must declare", "150")]) + cast.wait(READ_SHORT) + + cast.command('aicertify explain eu_ai_act | grep -A8 "produced by evaluators"') + cast.block(measure, first_byte=THINKING) + cast.wait(0.7) + cast.sweep( + measure, + [ + ("produced by evaluators", "14"), + ("defeats the point", "defeats the point"), + ], + ) + cast.wait(READ_LONG) + + +def build_short(card: str, declare: str, measure: str) -> Cast: + cast = Cast() + act_one(cast, card) + act_two(cast, declare, measure) + cast.blink() + return cast + + +def build(card, declare, measure, evaluate, summary, gaps) -> Cast: + cast = Cast() + act_one(cast, card) + act_two(cast, declare, measure) + + # Act 3: scaffold, run, and find out you have answered nothing. + cast.command("aicertify init-contract --policy eu_ai_act > contract.json") + cast.wait(THINKING + 0.9) + cast.command("aicertify evaluate --contract contract.json --policy eu_ai_act") + cast.block(evaluate, first_byte=THINKING) + cast.wait(0.8) + + cast.command("head -8 reports/*.md") + cast.block(summary) + cast.wait(0.7) + cast.sweep(summary, [("Red Count", "29"), ("Green Count", "0")]) + cast.wait(READ_LONG) + + # Act 4: the tool enumerating its own gaps, which is a contributor's + # to-do list rather than a caveat. + cast.command("# which metrics can an evaluator supply today?") + cast.wait(0.5) + cast.command("python scripts/metric_gap_report.py | tail -12") + cast.block(gaps, first_byte=THINKING) + cast.wait(0.8) + cast.sweep(gaps, [("TOTAL", "22 of 27")]) + cast.wait(READ_LONG) + cast.blink() + return cast + + +if __name__ == "__main__": + raise SystemExit(main())