Add policy_eval.py/src/policy_eval/core.py
This commit is contained in:
commit
626a8abff8
1 changed files with 118 additions and 0 deletions
118
policy_eval.py/src/policy_eval/core.py
Normal file
118
policy_eval.py/src/policy_eval/core.py
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
import csv
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, Any
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] %(levelname)s in %(module)s: %(message)s')
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyChangeError(Exception):
|
||||||
|
"""Custom exception raised for invalid policy constants input."""
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_policy_constants(policy_constants_json: Dict[str, Any]) -> None:
|
||||||
|
required_keys = {"version", "constant_value"}
|
||||||
|
if not isinstance(policy_constants_json, dict):
|
||||||
|
raise PolicyChangeError("policy_constants_json must be a dict")
|
||||||
|
missing = required_keys - policy_constants_json.keys()
|
||||||
|
if missing:
|
||||||
|
raise PolicyChangeError(f"Missing required keys in policy_constants_json: {missing}")
|
||||||
|
if not isinstance(policy_constants_json["version"], str):
|
||||||
|
raise PolicyChangeError("version must be a string")
|
||||||
|
if not isinstance(policy_constants_json["constant_value"], (int, float)):
|
||||||
|
raise PolicyChangeError("constant_value must be numeric")
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_policy_hash(policy_constants_json: Dict[str, Any]) -> str:
|
||||||
|
serialized = json.dumps(policy_constants_json, sort_keys=True).encode('utf-8')
|
||||||
|
return hashlib.sha256(serialized).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def check_policy_changes(policy_constants_json: Dict[str, Any]) -> bool:
|
||||||
|
"""Überprüft die Policy-Constants-Datei und erkennt Änderungen anhand von Hashes und Versionen."""
|
||||||
|
_validate_policy_constants(policy_constants_json)
|
||||||
|
current_hash = _compute_policy_hash(policy_constants_json)
|
||||||
|
hash_record_path = Path("output/.last_policy_hash")
|
||||||
|
|
||||||
|
if hash_record_path.exists():
|
||||||
|
last_hash = hash_record_path.read_text().strip()
|
||||||
|
has_changed = last_hash != current_hash
|
||||||
|
else:
|
||||||
|
has_changed = True
|
||||||
|
|
||||||
|
hash_record_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
hash_record_path.write_text(current_hash)
|
||||||
|
|
||||||
|
logger.info("Policy change detected: %s", has_changed)
|
||||||
|
return has_changed
|
||||||
|
|
||||||
|
|
||||||
|
def run_backtest(audit_set: str) -> Dict[str, str]:
|
||||||
|
"""Führt einen Backtest gegen das fixierte Audit-Set durch und erzeugt Delta-Artefakte."""
|
||||||
|
audit_path = Path(audit_set)
|
||||||
|
if not audit_path.exists():
|
||||||
|
raise FileNotFoundError(f"Audit set path not found: {audit_set}")
|
||||||
|
|
||||||
|
# Idee: Vergleiche alte/neue Files (simuliert) → generiere delta_summary.json und delta_cases.csv
|
||||||
|
all_csvs = list(audit_path.glob('*.csv'))
|
||||||
|
if not all_csvs:
|
||||||
|
raise FileNotFoundError(f"No CSV audit files found in {audit_set}")
|
||||||
|
|
||||||
|
df_list = []
|
||||||
|
for csv_file in all_csvs:
|
||||||
|
df = pd.read_csv(csv_file)
|
||||||
|
df['source_file'] = csv_file.name
|
||||||
|
df_list.append(df)
|
||||||
|
full_df = pd.concat(df_list, ignore_index=True)
|
||||||
|
|
||||||
|
# Simulierte Delta-Generierung: alte vs neue Policy (hier zufälliger Vergleich)
|
||||||
|
full_df['reason'] = 'Policy-Update'
|
||||||
|
full_df['old_decision'] = full_df.iloc[:, 0].astype(str)
|
||||||
|
full_df['new_decision'] = full_df.iloc[:, 0].astype(str)
|
||||||
|
|
||||||
|
constants_path = Path('config/policy_constants.json')
|
||||||
|
policy_hash = 'unknown'
|
||||||
|
if constants_path.exists():
|
||||||
|
with open(constants_path, 'r', encoding='utf-8') as f:
|
||||||
|
try:
|
||||||
|
constants_data = json.load(f)
|
||||||
|
_validate_policy_constants(constants_data)
|
||||||
|
policy_hash = _compute_policy_hash(constants_data)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Could not compute policy hash: %s", exc)
|
||||||
|
|
||||||
|
full_df['policy_hash'] = policy_hash
|
||||||
|
|
||||||
|
output_dir = Path('output')
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
delta_cases_path = output_dir / 'delta_cases.csv'
|
||||||
|
full_df[['reason', 'old_decision', 'new_decision', 'policy_hash']].to_csv(delta_cases_path, index=False)
|
||||||
|
|
||||||
|
# Zusammenfassung
|
||||||
|
delta_summary = {
|
||||||
|
'timestamp': datetime.utcnow().isoformat() + 'Z',
|
||||||
|
'total_cases': len(full_df),
|
||||||
|
'unique_policy_hashes': [policy_hash],
|
||||||
|
'reason_counts': {'Policy-Update': len(full_df)},
|
||||||
|
}
|
||||||
|
delta_summary_path = output_dir / 'delta_summary.json'
|
||||||
|
with open(delta_summary_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(delta_summary, f, indent=2)
|
||||||
|
|
||||||
|
# CI Assertion
|
||||||
|
assert delta_cases_path.exists() and delta_summary_path.exists(), "Backtest output files missing."
|
||||||
|
logger.info("Backtest completed: %s cases written.", len(full_df))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'delta_summary_path': str(delta_summary_path),
|
||||||
|
'delta_cases_path': str(delta_cases_path),
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue