rti-ethics-engine 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rti_ethics_engine/__init__.py +73 -0
- rti_ethics_engine/config.py +169 -0
- rti_ethics_engine/core.py +184 -0
- rti_ethics_engine/dimensions.py +373 -0
- rti_ethics_engine/metrics.py +52 -0
- rti_ethics_engine/results.py +386 -0
- rti_ethics_engine-1.0.0.dist-info/METADATA +198 -0
- rti_ethics_engine-1.0.0.dist-info/RECORD +10 -0
- rti_ethics_engine-1.0.0.dist-info/WHEEL +4 -0
- rti_ethics_engine-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RTI Ethics Engine - Results Module
|
|
3
|
+
EthicsResult dataclass with EXHAUSTIVE granular metric accessors and formatted output.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
8
|
+
from enum import Enum
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RiskBand(Enum):
|
|
13
|
+
"""Risk classification bands."""
|
|
14
|
+
LOW = "LOW"
|
|
15
|
+
MEDIUM = "MEDIUM"
|
|
16
|
+
HIGH = "HIGH"
|
|
17
|
+
CRITICAL = "CRITICAL"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FinalLabel(Enum):
|
|
21
|
+
"""Final ethics decision labels."""
|
|
22
|
+
ETHICAL = "ETHICAL"
|
|
23
|
+
CONDITIONALLY_ETHICAL = "CONDITIONALLY_ETHICAL"
|
|
24
|
+
UNETHICAL = "UNETHICAL"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Dimension display names mapping
|
|
28
|
+
DIMENSION_NAMES = {
|
|
29
|
+
"legal": "Legal Compliance",
|
|
30
|
+
"fairness": "Bias & Fairness",
|
|
31
|
+
"explainability": "Explainability & Transparency",
|
|
32
|
+
"security": "Security & Robustness",
|
|
33
|
+
"monitoring": "Monitoring & Compliance",
|
|
34
|
+
"performance": "Model Performance",
|
|
35
|
+
"ux": "Human-AI UX",
|
|
36
|
+
"drift": "Data & Model Drift",
|
|
37
|
+
"environmental": "Environmental & Cost"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class DimensionResult:
|
|
43
|
+
"""Result for a single ethical dimension with FULL metric access."""
|
|
44
|
+
key: str
|
|
45
|
+
name: str
|
|
46
|
+
l1_status: str
|
|
47
|
+
l1_reasons: List[str]
|
|
48
|
+
l2_risk: float
|
|
49
|
+
l2_indicators: List[str]
|
|
50
|
+
l3_evidence: Dict[str, Any]
|
|
51
|
+
raw_metrics: Dict[str, Any]
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def passed_l1(self) -> bool:
|
|
55
|
+
return self.l1_status == "PASS"
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def failed_l1(self) -> bool:
|
|
59
|
+
return self.l1_status == "FAIL"
|
|
60
|
+
|
|
61
|
+
def get_metric(self, metric_name: str, default: Any = None) -> Any:
|
|
62
|
+
"""Get a specific raw metric value."""
|
|
63
|
+
return self.raw_metrics.get(metric_name, default)
|
|
64
|
+
|
|
65
|
+
def get_all_metrics(self) -> Dict[str, Any]:
|
|
66
|
+
"""Get all raw metrics for this dimension."""
|
|
67
|
+
return self.raw_metrics.copy()
|
|
68
|
+
|
|
69
|
+
def format_l1_output(self) -> str:
|
|
70
|
+
"""Format L1 gate result for display."""
|
|
71
|
+
icon = "โ
" if self.passed_l1 else "โ"
|
|
72
|
+
status_text = f"{icon} L1 {self.l1_status}"
|
|
73
|
+
if self.l1_reasons:
|
|
74
|
+
reasons = "; ".join(self.l1_reasons)
|
|
75
|
+
return f"{status_text} | Violations: {reasons}"
|
|
76
|
+
return status_text
|
|
77
|
+
|
|
78
|
+
def format_l2_output(self) -> str:
|
|
79
|
+
"""Format L2 risk score for display."""
|
|
80
|
+
risk_pct = self.l2_risk * 100
|
|
81
|
+
if self.l2_risk < 0.3:
|
|
82
|
+
icon = "๐ข"
|
|
83
|
+
elif self.l2_risk < 0.7:
|
|
84
|
+
icon = "๐ก"
|
|
85
|
+
else:
|
|
86
|
+
icon = "๐ด"
|
|
87
|
+
return f"{icon} L2 Risk: {risk_pct:.1f}%"
|
|
88
|
+
|
|
89
|
+
def format_l3_output(self) -> str:
|
|
90
|
+
"""Format L3 evidence for display."""
|
|
91
|
+
explanation = self.l3_evidence.get("explanation", "No evidence.")
|
|
92
|
+
return f"๐ L3: {explanation}"
|
|
93
|
+
|
|
94
|
+
def format_metrics_table(self) -> str:
|
|
95
|
+
"""Format all raw metrics as a table."""
|
|
96
|
+
lines = []
|
|
97
|
+
for k, v in self.raw_metrics.items():
|
|
98
|
+
if isinstance(v, float):
|
|
99
|
+
lines.append(f" {k}: {v:.4f}")
|
|
100
|
+
else:
|
|
101
|
+
lines.append(f" {k}: {v}")
|
|
102
|
+
return "\n".join(lines)
|
|
103
|
+
|
|
104
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
105
|
+
return {
|
|
106
|
+
"key": self.key,
|
|
107
|
+
"name": self.name,
|
|
108
|
+
"l1_status": self.l1_status,
|
|
109
|
+
"l1_reasons": self.l1_reasons,
|
|
110
|
+
"l2_risk": self.l2_risk,
|
|
111
|
+
"l2_indicators": self.l2_indicators,
|
|
112
|
+
"l3_evidence": self.l3_evidence,
|
|
113
|
+
"raw_metrics": self.raw_metrics
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class EthicsResult:
|
|
119
|
+
"""
|
|
120
|
+
Complete ethics evaluation result with EXHAUSTIVE accessors.
|
|
121
|
+
|
|
122
|
+
Access patterns:
|
|
123
|
+
- result.final_label # "ETHICAL"
|
|
124
|
+
- result.s_llm # 0.92
|
|
125
|
+
- result.get_l1_metric("fairness", "spd") # 0.02
|
|
126
|
+
- result.get_l2_risk("legal") # 0.0
|
|
127
|
+
- result.get_l3_evidence("security") # {"explanation": "..."}
|
|
128
|
+
- result.print_full_audit() # Prints everything
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
final_label: str
|
|
132
|
+
layer1_status: str
|
|
133
|
+
layer2_risk_score: float
|
|
134
|
+
s_llm: float
|
|
135
|
+
risk_band: str
|
|
136
|
+
required_action: str
|
|
137
|
+
audit_id: str
|
|
138
|
+
risk_vector: Dict[str, float]
|
|
139
|
+
dimension_results: Dict[str, DimensionResult]
|
|
140
|
+
layer3_evidence: Dict[str, Any]
|
|
141
|
+
|
|
142
|
+
# ================== L1 METRIC ACCESS (GRANULAR) ==================
|
|
143
|
+
|
|
144
|
+
def get_l1_metric(self, dimension: str, metric: str, default: float = 0.0) -> float:
|
|
145
|
+
"""Get raw L1 metric value. E.g. get_l1_metric('fairness', 'spd')"""
|
|
146
|
+
dim = self.dimension_results.get(dimension)
|
|
147
|
+
return dim.get_metric(metric, default) if dim else default
|
|
148
|
+
|
|
149
|
+
def get_l1_status(self, dimension: str) -> str:
|
|
150
|
+
"""Get L1 status for dimension. Returns 'PASS' or 'FAIL'."""
|
|
151
|
+
dim = self.dimension_results.get(dimension)
|
|
152
|
+
return dim.l1_status if dim else "UNKNOWN"
|
|
153
|
+
|
|
154
|
+
def get_l1_reasons(self, dimension: str) -> List[str]:
|
|
155
|
+
"""Get L1 failure reasons."""
|
|
156
|
+
dim = self.dimension_results.get(dimension)
|
|
157
|
+
return dim.l1_reasons if dim else []
|
|
158
|
+
|
|
159
|
+
def get_all_l1_metrics(self, dimension: str) -> Dict[str, Any]:
|
|
160
|
+
"""Get ALL raw metrics for a dimension."""
|
|
161
|
+
dim = self.dimension_results.get(dimension)
|
|
162
|
+
return dim.get_all_metrics() if dim else {}
|
|
163
|
+
|
|
164
|
+
# Individual L1 metric shortcuts
|
|
165
|
+
@property
|
|
166
|
+
def spd(self) -> float:
|
|
167
|
+
"""Statistical Parity Difference (Fairness L1)"""
|
|
168
|
+
return self.get_l1_metric("fairness", "spd")
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def di(self) -> float:
|
|
172
|
+
"""Disparate Impact ratio (Fairness L1)"""
|
|
173
|
+
return self.get_l1_metric("fairness", "di", 1.0)
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def eop(self) -> float:
|
|
177
|
+
"""Equal Opportunity Difference (Fairness L1)"""
|
|
178
|
+
return self.get_l1_metric("fairness", "eop")
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def aod(self) -> float:
|
|
182
|
+
"""Average Odds Difference (Fairness L1)"""
|
|
183
|
+
return self.get_l1_metric("fairness", "aod")
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def psi(self) -> float:
|
|
187
|
+
"""Population Stability Index (Drift L1)"""
|
|
188
|
+
return self.get_l1_metric("drift", "psi")
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def ks(self) -> float:
|
|
192
|
+
"""Kolmogorov-Smirnov statistic (Drift L1)"""
|
|
193
|
+
return self.get_l1_metric("drift", "ks")
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def lawfulness(self) -> float:
|
|
197
|
+
"""Lawfulness score (Legal L1)"""
|
|
198
|
+
return self.get_l1_metric("legal", "lawfulness", 1.0)
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def accuracy(self) -> float:
|
|
202
|
+
"""Model accuracy (Performance L1)"""
|
|
203
|
+
return self.get_l1_metric("performance", "accuracy")
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def recall(self) -> float:
|
|
207
|
+
"""Model recall (Performance L1)"""
|
|
208
|
+
return self.get_l1_metric("performance", "recall")
|
|
209
|
+
|
|
210
|
+
# ================== L2 RISK ACCESS ==================
|
|
211
|
+
|
|
212
|
+
def get_l2_risk(self, dimension: str, default: float = 0.0) -> float:
|
|
213
|
+
"""Get L2 risk score for dimension [0, 1]."""
|
|
214
|
+
dim = self.dimension_results.get(dimension)
|
|
215
|
+
return dim.l2_risk if dim else default
|
|
216
|
+
|
|
217
|
+
def get_l2_indicators(self, dimension: str) -> List[str]:
|
|
218
|
+
"""Get L2 risk indicators."""
|
|
219
|
+
dim = self.dimension_results.get(dimension)
|
|
220
|
+
return dim.l2_indicators if dim else []
|
|
221
|
+
|
|
222
|
+
# ================== L3 EVIDENCE ACCESS ==================
|
|
223
|
+
|
|
224
|
+
def get_l3_evidence(self, dimension: str) -> Dict[str, Any]:
|
|
225
|
+
"""Get L3 forensic evidence for dimension."""
|
|
226
|
+
dim = self.dimension_results.get(dimension)
|
|
227
|
+
return dim.l3_evidence if dim else {"explanation": "Not found."}
|
|
228
|
+
|
|
229
|
+
def get_l3_explanation(self, dimension: str) -> str:
|
|
230
|
+
"""Get L3 explanation text."""
|
|
231
|
+
return self.get_l3_evidence(dimension).get("explanation", "No explanation.")
|
|
232
|
+
|
|
233
|
+
# ================== AGGREGATION HELPERS ==================
|
|
234
|
+
|
|
235
|
+
def get_failed_dims(self) -> List[str]:
|
|
236
|
+
"""Get human-readable names of failed dimensions."""
|
|
237
|
+
return [d.name for d in self.dimension_results.values() if d.failed_l1]
|
|
238
|
+
|
|
239
|
+
def get_failed_dim_keys(self) -> List[str]:
|
|
240
|
+
"""Get keys of failed dimensions."""
|
|
241
|
+
return [k for k, d in self.dimension_results.items() if d.failed_l1]
|
|
242
|
+
|
|
243
|
+
def get_passed_dims(self) -> List[str]:
|
|
244
|
+
"""Get human-readable names of passed dimensions."""
|
|
245
|
+
return [d.name for d in self.dimension_results.values() if d.passed_l1]
|
|
246
|
+
|
|
247
|
+
def l1_pass_rate(self) -> float:
|
|
248
|
+
"""Fraction of L1 passes (e.g. 8/9 = 0.889)."""
|
|
249
|
+
if not self.dimension_results:
|
|
250
|
+
return 0.0
|
|
251
|
+
return sum(1 for d in self.dimension_results.values() if d.passed_l1) / len(self.dimension_results)
|
|
252
|
+
|
|
253
|
+
def l1_fail_count(self) -> int:
|
|
254
|
+
return sum(1 for d in self.dimension_results.values() if d.failed_l1)
|
|
255
|
+
|
|
256
|
+
def l1_pass_count(self) -> int:
|
|
257
|
+
return sum(1 for d in self.dimension_results.values() if d.passed_l1)
|
|
258
|
+
|
|
259
|
+
def get_highest_risk_dim(self) -> Tuple[str, float]:
|
|
260
|
+
"""Get (dimension_key, risk_score) of highest risk dimension."""
|
|
261
|
+
if not self.risk_vector:
|
|
262
|
+
return ("", 0.0)
|
|
263
|
+
key = max(self.risk_vector, key=self.risk_vector.get)
|
|
264
|
+
return (key, self.risk_vector[key])
|
|
265
|
+
|
|
266
|
+
def get_dim_result(self, dimension: str) -> Optional[DimensionResult]:
|
|
267
|
+
"""Get full DimensionResult object."""
|
|
268
|
+
return self.dimension_results.get(dimension)
|
|
269
|
+
|
|
270
|
+
# ================== FORMATTED OUTPUT ==================
|
|
271
|
+
|
|
272
|
+
def format_header(self) -> str:
|
|
273
|
+
"""Format audit header."""
|
|
274
|
+
return f"{self.audit_id} | {self.final_label} | S_LLM={self.s_llm:.4f} | Risk={self.risk_band}"
|
|
275
|
+
|
|
276
|
+
def format_l1_summary(self) -> str:
|
|
277
|
+
"""Format L1 summary table."""
|
|
278
|
+
lines = ["LAYER 1 - HARD GATES (Pass/Fail Thresholds):"]
|
|
279
|
+
for key, dim in self.dimension_results.items():
|
|
280
|
+
icon = "โ
" if dim.passed_l1 else "โ"
|
|
281
|
+
reasons = f" [{'; '.join(dim.l1_reasons)}]" if dim.l1_reasons else ""
|
|
282
|
+
lines.append(f" {icon} {dim.name}: {dim.l1_status}{reasons}")
|
|
283
|
+
lines.append(f"\n L1 Pass Rate: {self.l1_pass_count()}/9 ({self.l1_pass_rate()*100:.1f}%)")
|
|
284
|
+
return "\n".join(lines)
|
|
285
|
+
|
|
286
|
+
def format_l2_summary(self) -> str:
|
|
287
|
+
"""Format L2 risk summary."""
|
|
288
|
+
lines = ["LAYER 2 - WEIGHTED RISK SCORES:"]
|
|
289
|
+
for key, dim in self.dimension_results.items():
|
|
290
|
+
pct = dim.l2_risk * 100
|
|
291
|
+
bar = "โ" * int(pct / 10) + "โ" * (10 - int(pct / 10))
|
|
292
|
+
icon = "๐ข" if dim.l2_risk < 0.3 else "๐ก" if dim.l2_risk < 0.7 else "๐ด"
|
|
293
|
+
lines.append(f" {icon} {dim.name}: {pct:5.1f}% [{bar}]")
|
|
294
|
+
lines.append(f"\n Total L2 Risk: {self.layer2_risk_score:.4f}")
|
|
295
|
+
lines.append(f" S_LLM Score: {self.s_llm:.4f}")
|
|
296
|
+
return "\n".join(lines)
|
|
297
|
+
|
|
298
|
+
def format_l3_summary(self) -> str:
|
|
299
|
+
"""Format L3 forensic evidence."""
|
|
300
|
+
lines = ["LAYER 3 - FORENSIC EVIDENCE BUNDLE:"]
|
|
301
|
+
for key, dim in self.dimension_results.items():
|
|
302
|
+
explanation = dim.l3_evidence.get("explanation", "Forensic data available.")
|
|
303
|
+
lines.append(f" ๐ {dim.name}:")
|
|
304
|
+
lines.append(f" โโ {explanation}")
|
|
305
|
+
return "\n".join(lines)
|
|
306
|
+
|
|
307
|
+
def format_risk_vector(self) -> str:
|
|
308
|
+
"""Format risk vector as JSON."""
|
|
309
|
+
return json.dumps(self.risk_vector, indent=2)
|
|
310
|
+
|
|
311
|
+
def format_metrics_details(self) -> str:
|
|
312
|
+
"""Format ALL raw metrics for ALL dimensions."""
|
|
313
|
+
lines = ["RAW METRICS (All 36 Formulas):"]
|
|
314
|
+
for key, dim in self.dimension_results.items():
|
|
315
|
+
lines.append(f"\n [{dim.name.upper()}]")
|
|
316
|
+
for m, v in dim.raw_metrics.items():
|
|
317
|
+
if isinstance(v, float):
|
|
318
|
+
lines.append(f" โข {m}: {v:.4f}")
|
|
319
|
+
elif v is not None:
|
|
320
|
+
lines.append(f" โข {m}: {v}")
|
|
321
|
+
return "\n".join(lines)
|
|
322
|
+
|
|
323
|
+
def to_audit_table(self) -> str:
|
|
324
|
+
"""Compact audit table."""
|
|
325
|
+
lines = [self.format_header()]
|
|
326
|
+
for dim in self.dimension_results.values():
|
|
327
|
+
icon = "โ
" if dim.passed_l1 else "โ"
|
|
328
|
+
lines.append(f"{icon} {dim.name}: L2={dim.l2_risk:.2f}")
|
|
329
|
+
return "\n".join(lines)
|
|
330
|
+
|
|
331
|
+
def print_audit_table(self) -> None:
|
|
332
|
+
"""Print compact audit table."""
|
|
333
|
+
print(self.to_audit_table())
|
|
334
|
+
|
|
335
|
+
def print_full_audit(self) -> None:
|
|
336
|
+
"""Print EXHAUSTIVE audit output with L1, L2, L3 details."""
|
|
337
|
+
print("\n" + "=" * 80)
|
|
338
|
+
print("โ๏ธ RTI ETHICS ENGINE - FULL AUDIT REPORT")
|
|
339
|
+
print("=" * 80)
|
|
340
|
+
print(f"\n๐ {self.format_header()}")
|
|
341
|
+
print(f"๐ Action Required: {self.required_action}")
|
|
342
|
+
print("\n" + "-" * 80)
|
|
343
|
+
print(self.format_l1_summary())
|
|
344
|
+
print("\n" + "-" * 80)
|
|
345
|
+
print(self.format_l2_summary())
|
|
346
|
+
print("\n" + "-" * 80)
|
|
347
|
+
print(self.format_l3_summary())
|
|
348
|
+
print("\n" + "-" * 80)
|
|
349
|
+
print("RISK VECTOR [R_1...R_9]:")
|
|
350
|
+
print(self.format_risk_vector())
|
|
351
|
+
print("\n" + "=" * 80)
|
|
352
|
+
|
|
353
|
+
# Final verdict
|
|
354
|
+
if self.final_label == "UNETHICAL":
|
|
355
|
+
print("๐จ [CRITICAL] PROTOCOL BREACHED - OUTPUT BLOCKED ๐จ")
|
|
356
|
+
elif self.final_label == "CONDITIONALLY_ETHICAL":
|
|
357
|
+
print("โ ๏ธ [WARNING] PARTIAL COMPLIANCE - HUMAN REVIEW REQUIRED โ ๏ธ")
|
|
358
|
+
else:
|
|
359
|
+
print("โ
[PASS] ETHICAL COMPLIANCE VERIFIED")
|
|
360
|
+
print("=" * 80 + "\n")
|
|
361
|
+
|
|
362
|
+
def print_metrics_details(self) -> None:
|
|
363
|
+
"""Print all 36 raw metric values."""
|
|
364
|
+
print(self.format_metrics_details())
|
|
365
|
+
|
|
366
|
+
# ================== SERIALIZATION ==================
|
|
367
|
+
|
|
368
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
369
|
+
return {
|
|
370
|
+
"final_label": self.final_label,
|
|
371
|
+
"layer1_status": self.layer1_status,
|
|
372
|
+
"layer2_risk_score": self.layer2_risk_score,
|
|
373
|
+
"s_llm": self.s_llm,
|
|
374
|
+
"risk_band": self.risk_band,
|
|
375
|
+
"required_action": self.required_action,
|
|
376
|
+
"audit_id": self.audit_id,
|
|
377
|
+
"risk_vector": self.risk_vector,
|
|
378
|
+
"dimension_results": {k: v.to_dict() for k, v in self.dimension_results.items()},
|
|
379
|
+
"layer3_evidence": self.layer3_evidence
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
def to_json(self, indent: int = 2) -> str:
|
|
383
|
+
return json.dumps(self.to_dict(), indent=indent, default=str)
|
|
384
|
+
|
|
385
|
+
def __repr__(self) -> str:
|
|
386
|
+
return f"EthicsResult(label={self.final_label}, s_llm={self.s_llm:.3f}, band={self.risk_band})"
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rti-ethics-engine
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: India's first granular AI ethics framework for RTI applications (36 metrics, 9 dimensions)
|
|
5
|
+
Project-URL: Homepage, https://github.com/rti-ethics/rti-ethics-engine
|
|
6
|
+
Project-URL: Documentation, https://rti-ethics-engine.readthedocs.io
|
|
7
|
+
Project-URL: Repository, https://github.com/rti-ethics/rti-ethics-engine
|
|
8
|
+
Project-URL: Issues, https://github.com/rti-ethics/rti-ethics-engine/issues
|
|
9
|
+
Author: RTI Ethics Engine Team
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 RTI Ethics Engine Team
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: ai-ethics,audit,compliance,fairness,government,india,llm,rti
|
|
33
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: Intended Audience :: Science/Research
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Operating System :: OS Independent
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
43
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
44
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
45
|
+
Requires-Python: >=3.9
|
|
46
|
+
Requires-Dist: numpy>=1.24.0
|
|
47
|
+
Provides-Extra: dev
|
|
48
|
+
Requires-Dist: black>=23.0; extra == 'dev'
|
|
49
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
50
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
51
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
52
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
53
|
+
Provides-Extra: docs
|
|
54
|
+
Requires-Dist: sphinx-rtd-theme>=1.0; extra == 'docs'
|
|
55
|
+
Requires-Dist: sphinx>=5.0; extra == 'docs'
|
|
56
|
+
Description-Content-Type: text/markdown
|
|
57
|
+
|
|
58
|
+
# RTI Ethics Engine
|
|
59
|
+
|
|
60
|
+
**India's first granular AI ethics framework for RTI applications** - A production-ready Python package implementing 36 metrics across 9 ethical dimensions with granular formula access.
|
|
61
|
+
|
|
62
|
+
[](https://www.python.org/downloads/)
|
|
63
|
+
[](https://opensource.org/licenses/MIT)
|
|
64
|
+
|
|
65
|
+
## ๐ Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install rti-ethics-engine
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or install from source:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
git clone https://github.com/rti-ethics/rti-ethics-engine.git
|
|
75
|
+
cd rti-ethics-engine
|
|
76
|
+
pip install -e .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## โก Quick Start
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from rti_ethics_engine import evaluate
|
|
83
|
+
|
|
84
|
+
# Define your metrics
|
|
85
|
+
metrics = {
|
|
86
|
+
"fairness": {"spd": 0.02, "di": 1.0, "eop": 0.05, "aod": 0.03},
|
|
87
|
+
"drift": {"psi": 0.1, "ks": 0.02},
|
|
88
|
+
"legal": {"lawfulness": 1.0, "cvr": 0.98},
|
|
89
|
+
"performance": {"accuracy": 0.92, "recall": 0.88},
|
|
90
|
+
"explainability": {"confidence": 0.85, "fi": 0.3},
|
|
91
|
+
"security": {"adv_acc": 0.8, "pisr": 0.02},
|
|
92
|
+
"monitoring": {"alc": 0.98, "sla": 0.995},
|
|
93
|
+
"ux": {"tcr": 0.92, "ftr": 0.08},
|
|
94
|
+
"environmental": {"inf_cost": 0.02, "gpu_hours": 0.05}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Evaluate
|
|
98
|
+
result = evaluate(metrics)
|
|
99
|
+
|
|
100
|
+
# Get the decision
|
|
101
|
+
print(result.final_label) # "ETHICAL"
|
|
102
|
+
print(result.s_llm) # 0.9234 (LLM Ethics Score)
|
|
103
|
+
print(result.risk_band) # "LOW"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## ๐ฏ Granular Metric Access
|
|
107
|
+
|
|
108
|
+
The killer feature - access ANY formula value at ANY layer:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
# Access specific L1 formula values
|
|
112
|
+
spd = result.get_l1_metric("fairness", "spd") # 0.02
|
|
113
|
+
psi = result.get_l1_metric("drift", "psi") # 0.1
|
|
114
|
+
accuracy = result.get_l1_metric("performance", "accuracy") # 0.92
|
|
115
|
+
|
|
116
|
+
# Access L2 risk scores
|
|
117
|
+
fairness_risk = result.get_l2_risk("fairness") # 0.12
|
|
118
|
+
legal_risk = result.get_l2_risk("legal") # 0.00
|
|
119
|
+
|
|
120
|
+
# Access L3 forensic evidence
|
|
121
|
+
explanation = result.get_l3_evidence("legal")["explanation"]
|
|
122
|
+
|
|
123
|
+
# Aggregation helpers
|
|
124
|
+
failed_dims = result.get_failed_dims() # ["Legal Compliance"]
|
|
125
|
+
pass_rate = result.l1_pass_rate() # 0.889 (8/9 passed)
|
|
126
|
+
|
|
127
|
+
# Audit table for CIC compliance
|
|
128
|
+
result.print_audit_table()
|
|
129
|
+
# RTI-2026035174555 | ETHICAL | S_LLM=0.92
|
|
130
|
+
# โ
Legal Compliance: L2=0.02
|
|
131
|
+
# โ
Bias & Fairness: L2=0.05
|
|
132
|
+
# ...
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## ๐ 9 Ethical Dimensions
|
|
136
|
+
|
|
137
|
+
| Dimension | L1 Metrics | Description |
|
|
138
|
+
| -------------------- | ------------------- | ---------------------- |
|
|
139
|
+
| **Legal Compliance** | lawfulness, cvr | RTI Act 2005 alignment |
|
|
140
|
+
| **Bias & Fairness** | spd, di, eop, aod | Demographic equity |
|
|
141
|
+
| **Explainability** | confidence, fi | Model interpretability |
|
|
142
|
+
| **Security** | adv_acc, pisr | Adversarial robustness |
|
|
143
|
+
| **Monitoring** | alc, sla | Audit trail integrity |
|
|
144
|
+
| **Performance** | accuracy, recall | Statistical accuracy |
|
|
145
|
+
| **Human-AI UX** | tcr, ftr | User friction |
|
|
146
|
+
| **Drift** | psi, ks | Model stability |
|
|
147
|
+
| **Environmental** | inf_cost, gpu_hours | Carbon footprint |
|
|
148
|
+
|
|
149
|
+
## ๐๏ธ Architecture
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
rti_ethics_engine/
|
|
153
|
+
โโโ __init__.py # Public API
|
|
154
|
+
โโโ config.py # Thresholds & weights
|
|
155
|
+
โโโ dimensions.py # 9 dimension evaluators
|
|
156
|
+
โโโ core.py # EthicsEngine orchestrator
|
|
157
|
+
โโโ results.py # EthicsResult with accessors
|
|
158
|
+
โโโ metrics.py # Computation helpers
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## ๐ API Reference
|
|
162
|
+
|
|
163
|
+
### `evaluate(metrics: dict) -> EthicsResult`
|
|
164
|
+
|
|
165
|
+
Quick evaluation entrypoint.
|
|
166
|
+
|
|
167
|
+
### `EthicsEngine`
|
|
168
|
+
|
|
169
|
+
Full engine with `run_protocol()` method.
|
|
170
|
+
|
|
171
|
+
### `EthicsResult`
|
|
172
|
+
|
|
173
|
+
Result object with:
|
|
174
|
+
|
|
175
|
+
- `final_label`: "ETHICAL" / "CONDITIONALLY_ETHICAL" / "UNETHICAL"
|
|
176
|
+
- `s_llm`: LLM Ethics Score [0, 1]
|
|
177
|
+
- `risk_band`: "LOW" / "MEDIUM" / "HIGH" / "CRITICAL"
|
|
178
|
+
- `get_l1_metric(dim, metric)`: Access any L1 formula value
|
|
179
|
+
- `get_l2_risk(dim)`: Get dimension risk score
|
|
180
|
+
- `get_l3_evidence(dim)`: Get forensic evidence
|
|
181
|
+
- `get_failed_dims()`: List failed dimensions
|
|
182
|
+
- `l1_pass_rate()`: Fraction of L1 passes
|
|
183
|
+
- `to_json()`: Export as JSON
|
|
184
|
+
|
|
185
|
+
## ๐งช Testing
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
pip install -e ".[dev]"
|
|
189
|
+
pytest tests/ -v
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## ๐ License
|
|
193
|
+
|
|
194
|
+
MIT License - See [LICENSE](LICENSE) for details.
|
|
195
|
+
|
|
196
|
+
## ๐ฎ๐ณ Made for India
|
|
197
|
+
|
|
198
|
+
Designed specifically for RTI Act 2005 compliance and CIC audit requirements.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
rti_ethics_engine/__init__.py,sha256=UDvG1BNdrjRptDzEX59WvFw-JfhTlrGkLL447cDsAc0,1890
|
|
2
|
+
rti_ethics_engine/config.py,sha256=PibTiAWlz_E7A39J-pyFQ8ix62y99GIhmn6kpOYlIRE,5485
|
|
3
|
+
rti_ethics_engine/core.py,sha256=6qRZhINYYy_P44ksOMp1DKmbKhVfEp2ukYBPgepKkz8,7130
|
|
4
|
+
rti_ethics_engine/dimensions.py,sha256=igJhGfOqC2aQ5yWpwv-z-UDTPNjRUXaMx4vL14-TeeI,14599
|
|
5
|
+
rti_ethics_engine/metrics.py,sha256=pOJsXBMWE2i_b_H5j2sfCRP5_qOCEuZCV5YcNLRsCcU,2112
|
|
6
|
+
rti_ethics_engine/results.py,sha256=CV_n-e5JgeP4Xn15DIsisnzLH74P82u85cw8uB1N5n8,14782
|
|
7
|
+
rti_ethics_engine-1.0.0.dist-info/METADATA,sha256=Dqzd_D0rpczcKDsl9i02oExXMQ1l71GOs6FMtS21jfk,7237
|
|
8
|
+
rti_ethics_engine-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
9
|
+
rti_ethics_engine-1.0.0.dist-info/licenses/LICENSE,sha256=l2g45qF6T13lWrSSf3XhkK1iRTGTwrzotiBalqL7sdM,1100
|
|
10
|
+
rti_ethics_engine-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RTI Ethics Engine Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|