equiaudit 0.1.1__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.
Files changed (71) hide show
  1. equiaudit/__init__.py +3 -0
  2. equiaudit/cli/__init__.py +7 -0
  3. equiaudit/cli/__main__.py +204 -0
  4. equiaudit/cli/evaluator.py +455 -0
  5. equiaudit/data/GermanCredit.csv +1001 -0
  6. equiaudit/data/UCI_Credit_Card.csv +30001 -0
  7. equiaudit/data/adult-all.csv +48843 -0
  8. equiaudit/data/adult-all_discretized.csv +48843 -0
  9. equiaudit/data/bank-additional.csv +41189 -0
  10. equiaudit/data/german.csv +1001 -0
  11. equiaudit/data/german_discretized.csv +1001 -0
  12. equiaudit/gui/__init__.py +113 -0
  13. equiaudit/gui/app.py +30 -0
  14. equiaudit/gui/config.py +89 -0
  15. equiaudit/gui/screens/main_page.py +32 -0
  16. equiaudit/gui/screens/new_evaluation.py +983 -0
  17. equiaudit/gui/screens/view_results.py +704 -0
  18. equiaudit/gui/state.py +36 -0
  19. equiaudit/gui/styles.py +183 -0
  20. equiaudit/gui/utils.py +130 -0
  21. equiaudit/gui/widgets/fairness.py +748 -0
  22. equiaudit/gui/widgets/results.py +342 -0
  23. equiaudit/gui/widgets/stage_display.py +650 -0
  24. equiaudit/main.py +161 -0
  25. equiaudit/models/__init__.py +32 -0
  26. equiaudit/models/agent_manager.py +256 -0
  27. equiaudit/models/agents/__init__.py +11 -0
  28. equiaudit/models/agents/base_agent.py +242 -0
  29. equiaudit/models/agents/conversational_agent.py +21 -0
  30. equiaudit/models/agents/data_analyst_agent.py +84 -0
  31. equiaudit/models/agents/function_caller_agent.py +86 -0
  32. equiaudit/models/agents/humanizer_agent.py +58 -0
  33. equiaudit/models/agents/summary_agent.py +69 -0
  34. equiaudit/models/clients/__init__.py +13 -0
  35. equiaudit/models/clients/base_client.py +89 -0
  36. equiaudit/models/clients/client_factory.py +88 -0
  37. equiaudit/models/clients/gemini_client.py +113 -0
  38. equiaudit/models/clients/ollama_client.py +68 -0
  39. equiaudit/models/clients/openrouter_client.py +135 -0
  40. equiaudit/models/config.yml +147 -0
  41. equiaudit/pipeline/__init__.py +9 -0
  42. equiaudit/pipeline/config.py +88 -0
  43. equiaudit/pipeline/pipeline.py +642 -0
  44. equiaudit/pipeline/pipeline_config.yml +81 -0
  45. equiaudit/pipeline/stage.py +102 -0
  46. equiaudit/pipeline/stages/__init__.py +25 -0
  47. equiaudit/pipeline/stages/base.py +86 -0
  48. equiaudit/pipeline/stages/dataset_loading.py +21 -0
  49. equiaudit/pipeline/stages/discretization.py +384 -0
  50. equiaudit/pipeline/stages/fairness.py +176 -0
  51. equiaudit/pipeline/stages/imbalance.py +122 -0
  52. equiaudit/pipeline/stages/mitigation.py +332 -0
  53. equiaudit/pipeline/stages/objective.py +34 -0
  54. equiaudit/pipeline/stages/pair_selection.py +116 -0
  55. equiaudit/pipeline/stages/quality.py +28 -0
  56. equiaudit/pipeline/stages/recommendations.py +45 -0
  57. equiaudit/pipeline/stages/sensitive.py +134 -0
  58. equiaudit/pipeline/utils.py +878 -0
  59. equiaudit/reporting/__init__.py +0 -0
  60. equiaudit/reporting/pdf_generator.py +645 -0
  61. equiaudit/tools/__init__.py +0 -0
  62. equiaudit/tools/bias_mitigation_tools.py +653 -0
  63. equiaudit/tools/discretization_tools.py +259 -0
  64. equiaudit/tools/fairness_tools.py +1037 -0
  65. equiaudit/tools/tool.py +6 -0
  66. equiaudit/tools/tool_manager.py +65 -0
  67. equiaudit-0.1.1.dist-info/METADATA +161 -0
  68. equiaudit-0.1.1.dist-info/RECORD +71 -0
  69. equiaudit-0.1.1.dist-info/WHEEL +5 -0
  70. equiaudit-0.1.1.dist-info/entry_points.txt +2 -0
  71. equiaudit-0.1.1.dist-info/top_level.txt +1 -0
equiaudit/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """EquiAudit: An Agentic AI Framework for Fairness Auditing."""
2
+
3
+ __version__ = "0.1.1"
@@ -0,0 +1,7 @@
1
+ from equiaudit.cli.evaluator import FairnessEvaluator, EvaluationResult, VerificationResult
2
+
3
+ __all__ = [
4
+ "FairnessEvaluator",
5
+ "EvaluationResult",
6
+ "VerificationResult",
7
+ ]
@@ -0,0 +1,204 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+
8
+
9
+ def main(args: argparse.Namespace = None) -> int:
10
+ """Main entry point for CLI."""
11
+
12
+ if args is None:
13
+ args = parse_args()
14
+
15
+ # Handle environment variable loading from .env file
16
+ if args.env_file:
17
+ load_env_file(args.env_file)
18
+
19
+ # Import after env loading to ensure API keys are available
20
+ from equiaudit.cli.evaluator import FairnessEvaluator
21
+
22
+ # Create evaluator
23
+ evaluator = FairnessEvaluator(
24
+ config_path=args.config,
25
+ output_dir=args.output,
26
+ model=args.model,
27
+ verbose=not args.quiet,
28
+ )
29
+
30
+ # Verify mode
31
+ if args.verify:
32
+ result = evaluator.verify()
33
+ return 0 if result.success else 1
34
+
35
+ # Evaluate mode - requires data
36
+ if not args.data:
37
+ print("Error: --data is required for evaluation mode")
38
+ print("Use --verify to check configuration only")
39
+ return 1
40
+
41
+ # Run evaluation
42
+ result = evaluator.evaluate(
43
+ data=args.data,
44
+ target=args.target,
45
+ objective=args.objective,
46
+ sensitive_columns=args.sensitive.split(",") if args.sensitive else None,
47
+ generate_pdf=not args.no_pdf,
48
+ )
49
+
50
+ if result.success:
51
+ print("\n" + "=" * 60)
52
+ print("EVALUATION COMPLETE")
53
+ print("=" * 60)
54
+ print(f"Dataset: {result.dataset}")
55
+ print(f"Target: {result.target_column or 'auto-detected'}")
56
+ print(f"Report Dir: {result.report_dir}")
57
+ if result.pdf_path:
58
+ print(f"PDF Report: {result.pdf_path}")
59
+ if result.markdown_path:
60
+ print(f"MD Report: {result.markdown_path}")
61
+ print(f"Stages: {', '.join(result.stages_completed)}")
62
+ if result.warnings:
63
+ print(f"\nWarnings ({len(result.warnings)}):")
64
+ for w in result.warnings:
65
+ print(f" - {w}")
66
+ return 0
67
+ else:
68
+ print("\n" + "=" * 60)
69
+ print("EVALUATION FAILED")
70
+ print("=" * 60)
71
+ print(f"Error: {result.error}")
72
+ return 1
73
+
74
+
75
+ def parse_args() -> argparse.Namespace:
76
+ """Parse command-line arguments."""
77
+
78
+ parser = argparse.ArgumentParser(
79
+ prog="fairness-eval",
80
+ description="Evaluate datasets for fairness and bias issues.",
81
+ formatter_class=argparse.RawDescriptionHelpFormatter,
82
+ epilog="""
83
+ Examples:
84
+ # Basic evaluation
85
+ equiaudit --data adult-all.csv --target Income
86
+
87
+ # Verify configuration
88
+ equiaudit --verify
89
+
90
+ # Custom config and output
91
+ equiaudit --config my_config.yml --data data.csv --output ./reports
92
+
93
+ # Specify model and sensitive columns
94
+ equiaudit --data data.csv --model gemini-flash --sensitive "age,sex,race"
95
+
96
+ Environment Variables:
97
+ OPENROUTER_API_KEY API key for OpenRouter models
98
+ GOOGLE_API_KEY API key for Gemini models
99
+ """,
100
+ )
101
+
102
+ # Data input
103
+ parser.add_argument(
104
+ "--data", "-d",
105
+ type=str,
106
+ help="Path to CSV dataset file",
107
+ )
108
+
109
+ parser.add_argument(
110
+ "--target", "-t",
111
+ type=str,
112
+ default=None,
113
+ help="Target column name for classification (auto-detected if not specified)",
114
+ )
115
+
116
+ parser.add_argument(
117
+ "--objective", "-o",
118
+ type=str,
119
+ default=None,
120
+ help="Custom evaluation objective/prompt",
121
+ )
122
+
123
+ parser.add_argument(
124
+ "--sensitive", "-s",
125
+ type=str,
126
+ default=None,
127
+ help="Comma-separated list of sensitive columns (auto-detected if not specified)",
128
+ )
129
+
130
+ # Configuration
131
+ parser.add_argument(
132
+ "--config", "-c",
133
+ type=str,
134
+ default=None,
135
+ help="Path to YAML configuration file (uses bundled default if omitted)",
136
+ )
137
+
138
+ parser.add_argument(
139
+ "--model", "-m",
140
+ type=str,
141
+ default=None,
142
+ help="Override default model from config",
143
+ )
144
+
145
+ parser.add_argument(
146
+ "--env-file", "-e",
147
+ type=str,
148
+ default=None,
149
+ help="Path to .env file for loading API keys",
150
+ )
151
+
152
+ # Output
153
+ parser.add_argument(
154
+ "--output", "-O",
155
+ type=str,
156
+ default=None,
157
+ help="Output directory for reports (default: reports/)",
158
+ )
159
+
160
+ parser.add_argument(
161
+ "--no-pdf",
162
+ action="store_true",
163
+ help="Skip PDF generation",
164
+ )
165
+
166
+ # Modes
167
+ parser.add_argument(
168
+ "--verify", "-V",
169
+ action="store_true",
170
+ help="Verify configuration only (don't run evaluation)",
171
+ )
172
+
173
+ parser.add_argument(
174
+ "--quiet", "-q",
175
+ action="store_true",
176
+ help="Suppress progress messages",
177
+ )
178
+
179
+ return parser.parse_args()
180
+
181
+
182
+ def load_env_file(env_path: str) -> None:
183
+ """Load environment variables from a .env file."""
184
+ env_path = Path(env_path)
185
+ if not env_path.exists():
186
+ print(f"Warning: .env file not found: {env_path}")
187
+ return
188
+
189
+ print(f"Loading environment from: {env_path}")
190
+
191
+ with open(env_path, "r") as f:
192
+ for line in f:
193
+ line = line.strip()
194
+ if not line or line.startswith("#"):
195
+ continue
196
+ if "=" in line:
197
+ key, value = line.split("=", 1)
198
+ key = key.strip()
199
+ value = value.strip().strip('"').strip("'")
200
+ os.environ[key] = value
201
+
202
+
203
+ if __name__ == "__main__":
204
+ sys.exit(main())
@@ -0,0 +1,455 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import yaml
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional, Union
9
+ import shutil
10
+ import traceback
11
+ from equiaudit.models.agents.base_agent import APIError
12
+
13
+ import pandas as pd
14
+ from equiaudit.pipeline.pipeline import DatasetEvaluationPipeline
15
+ from equiaudit.pipeline.utils import TECHNIQUE_DISPLAY
16
+
17
+ # Root of the equiaudit package (parent of cli/)
18
+ _SRC_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19
+
20
+
21
+ @dataclass
22
+ class EvaluationResult:
23
+ """Result of a fairness evaluation run."""
24
+
25
+ success: bool
26
+ dataset: str
27
+ target_column: Optional[str]
28
+ report_dir: str
29
+ pdf_path: Optional[str]
30
+ markdown_path: Optional[str]
31
+ json_path: Optional[str]
32
+ error: Optional[str] = None
33
+ warnings: List[str] = field(default_factory=list)
34
+ stages_completed: List[str] = field(default_factory=list)
35
+
36
+ def __repr__(self) -> str:
37
+ status = "SUCCESS" if self.success else "FAILED"
38
+ return f"EvaluationResult({status}, dataset={self.dataset}, report_dir={self.report_dir})"
39
+
40
+
41
+ @dataclass
42
+ class VerificationResult:
43
+ """Result of configuration verification."""
44
+
45
+ success: bool
46
+ config_path: str
47
+ model_name: str
48
+ errors: List[str] = field(default_factory=list)
49
+ warnings: List[str] = field(default_factory=list)
50
+
51
+ def __repr__(self) -> str:
52
+ status = "OK" if self.success else "FAILED"
53
+ return f"VerificationResult({status}, model={self.model_name})"
54
+
55
+
56
+ class FairnessEvaluator:
57
+ """
58
+ Main class for running fairness evaluations on datasets.
59
+
60
+ This class wraps the DatasetEvaluationPipeline and provides a simplified
61
+ interface for headless (non-GUI) evaluation runs.
62
+
63
+ Example:
64
+ >>> evaluator = FairnessEvaluator(config_path="models/config.yml")
65
+ >>> result = evaluator.verify()
66
+ >>> if result.success:
67
+ ... eval_result = evaluator.evaluate("adult-all.csv", target="Income")
68
+ ... print(f"Report saved to: {eval_result.pdf_path}")
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ config_path: Optional[str] = None,
74
+ output_dir: Optional[str] = None,
75
+ model: Optional[str] = None,
76
+ api_key: Optional[str] = None,
77
+ verbose: bool = True,
78
+ ):
79
+ """
80
+ Initialize the FairnessEvaluator.
81
+
82
+ Args:
83
+ config_path: Path to the YAML configuration file. If None, uses default.
84
+ output_dir: Base directory for reports. If None, uses project's reports/ folder.
85
+ model: Override the default model from config. If None, uses config default.
86
+ api_key: API key for the model provider (OpenRouter/Gemini). Overrides env variable.
87
+ verbose: Whether to print progress messages.
88
+ """
89
+ self.verbose = verbose
90
+ self.model_override = model
91
+ self.api_key = api_key
92
+
93
+ # Resolve config path
94
+ if config_path is None:
95
+ config_path = os.path.join(_SRC_DIR, "models", "config.yml")
96
+ self.config_path = os.path.abspath(config_path)
97
+
98
+ # Resolve output directory
99
+ if output_dir is None:
100
+ # Default to project root's reports/ folder
101
+ project_root = os.path.dirname(_SRC_DIR)
102
+ output_dir = os.path.join(project_root, "reports")
103
+ self.output_dir = os.path.abspath(output_dir)
104
+
105
+ # Pipeline will be initialized lazily
106
+ self._pipeline = None
107
+ self._initialized = False
108
+
109
+ # Verbose initialization happens when evaluation starts
110
+
111
+ def _ensure_initialized(self) -> None:
112
+ """Lazily initialize the pipeline."""
113
+ if self._initialized:
114
+ return
115
+
116
+ self._pipeline = DatasetEvaluationPipeline(
117
+ config_path=self.config_path,
118
+ default_model=self.model_override,
119
+ api_key=self.api_key,
120
+ )
121
+ self._initialized = True
122
+
123
+ def verify(self) -> VerificationResult:
124
+ """
125
+ Verify that the configuration is valid and the system is ready.
126
+
127
+ Returns:
128
+ VerificationResult with status and any errors/warnings.
129
+ """
130
+ errors: List[str] = []
131
+ warnings: List[str] = []
132
+ model_name = "unknown"
133
+
134
+ # Check config file exists
135
+ if not os.path.exists(self.config_path):
136
+ errors.append(f"Configuration file not found: {self.config_path}")
137
+ return VerificationResult(
138
+ success=False,
139
+ config_path=self.config_path,
140
+ model_name=model_name,
141
+ errors=errors,
142
+ warnings=warnings,
143
+ )
144
+
145
+ # Try to initialize pipeline
146
+ try:
147
+ self._ensure_initialized()
148
+ model_name = self._pipeline.agent_manager.config.get("default_model", "unknown")
149
+
150
+ if self.verbose:
151
+ print(f"Configuration loaded successfully")
152
+ print(f" Default model: {model_name}")
153
+ except Exception as e:
154
+ errors.append(f"Failed to initialize pipeline: {str(e)}")
155
+ return VerificationResult(
156
+ success=False,
157
+ config_path=self.config_path,
158
+ model_name=model_name,
159
+ errors=errors,
160
+ warnings=warnings,
161
+ )
162
+
163
+ # Check output directory
164
+ try:
165
+ os.makedirs(self.output_dir, exist_ok=True)
166
+ if self.verbose:
167
+ print(f"Output directory ready: {self.output_dir}")
168
+ except Exception as e:
169
+ errors.append(f"Cannot create output directory: {str(e)}")
170
+
171
+ # Check API key if using OpenRouter
172
+ model_config = self._pipeline.agent_manager.config.get("models", {}).get(model_name, {})
173
+ provider = model_config.get("provider", "")
174
+
175
+ if provider == "openrouter":
176
+ api_key = os.environ.get("OPENROUTER_API_KEY")
177
+ if not api_key:
178
+ warnings.append("OPENROUTER_API_KEY environment variable not set")
179
+ elif provider == "gemini":
180
+ api_key = os.environ.get("GOOGLE_API_KEY")
181
+ if not api_key:
182
+ warnings.append("GOOGLE_API_KEY environment variable not set")
183
+
184
+ success = len(errors) == 0
185
+
186
+ if self.verbose:
187
+ if success:
188
+ print("\nVerification: PASSED")
189
+ if warnings:
190
+ print(f" Warnings: {len(warnings)}")
191
+ for w in warnings:
192
+ print(f" - {w}")
193
+ else:
194
+ print("\nVerification: FAILED")
195
+ for e in errors:
196
+ print(f" ERROR: {e}")
197
+
198
+ return VerificationResult(
199
+ success=success,
200
+ config_path=self.config_path,
201
+ model_name=model_name,
202
+ errors=errors,
203
+ warnings=warnings,
204
+ )
205
+
206
+ def evaluate(
207
+ self,
208
+ data: Union[str, Path, pd.DataFrame],
209
+ target: Optional[str] = None,
210
+ objective: Optional[str] = None,
211
+ sensitive_columns: Optional[List[str]] = None,
212
+ sensitive_pairs: Optional[List[List[str]]] = None,
213
+ mitigation_techniques: Optional[List[str]] = None,
214
+ ml_config: Optional[Dict[str, Any]] = None,
215
+ output_dir: Optional[str] = None,
216
+ generate_pdf: bool = True,
217
+ max_pairs: Optional[int] = None,
218
+ ) -> EvaluationResult:
219
+ """
220
+ Run a fairness evaluation on a dataset.
221
+
222
+ Args:
223
+ data: Path to CSV file, or a pandas DataFrame.
224
+ target: Target column name for classification. Auto-detected if None.
225
+ objective: Custom objective/prompt for the evaluation.
226
+ sensitive_columns: List of sensitive attribute columns to analyze.
227
+ If None, uses config settings (auto-detect or restricted list).
228
+ sensitive_pairs: List of attribute pairs to analyze for intersectionality.
229
+ Each pair is a list of two column names, e.g. [["Sex", "Race"]].
230
+ If None, uses config settings (auto-select or restricted list).
231
+ mitigation_techniques: List of bias mitigation techniques to apply.
232
+ Options: "reweighting", "smote", "resampling",
233
+ "oversampling", "undersampling".
234
+ If None, reads from config. If config also has none,
235
+ mitigation stage is skipped entirely.
236
+ ml_config: ML model configuration for fairness metrics.
237
+ output_dir: Override output directory for this run.
238
+ generate_pdf: Whether to generate PDF report.
239
+ max_pairs: Maximum number of sensitive attribute pairs to analyze.
240
+ Only used when sensitive_pairs is None and config type is "auto".
241
+ If None, uses value from config.
242
+
243
+ Returns:
244
+ EvaluationResult with paths to generated reports.
245
+ """
246
+ self._ensure_initialized()
247
+
248
+ warnings: List[str] = []
249
+ stages_completed: List[str] = []
250
+
251
+ # Resolve data input
252
+ if isinstance(data, pd.DataFrame):
253
+ # Save DataFrame to temporary CSV
254
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
255
+ temp_csv_name = f"temp_dataset_{timestamp}.csv"
256
+ data_dir = os.path.join(_SRC_DIR, "data")
257
+ os.makedirs(data_dir, exist_ok=True)
258
+ temp_csv_path = os.path.join(data_dir, temp_csv_name)
259
+ data.to_csv(temp_csv_path, index=False)
260
+ dataset_name = temp_csv_name
261
+ if self.verbose:
262
+ print(f"DataFrame saved to: {temp_csv_path}")
263
+ else:
264
+ data_path = Path(data)
265
+ if not data_path.exists():
266
+ # Try looking in the bundled data directory
267
+ data_path = Path(_SRC_DIR) / "data" / data_path.name
268
+
269
+ if not data_path.exists():
270
+ return EvaluationResult(
271
+ success=False,
272
+ dataset=str(data),
273
+ target_column=target,
274
+ report_dir="",
275
+ pdf_path=None,
276
+ markdown_path=None,
277
+ json_path=None,
278
+ error=f"Dataset not found: {data}",
279
+ )
280
+
281
+ dataset_name = data_path.name
282
+
283
+ # Copy to data/ if not already there
284
+ expected_path = Path(_SRC_DIR) / "data" / dataset_name
285
+ if data_path.resolve() != expected_path.resolve():
286
+ os.makedirs(expected_path.parent, exist_ok=True)
287
+ shutil.copy2(data_path, expected_path)
288
+
289
+ try:
290
+ # Load evaluation config directly from YAML file
291
+ cfg: Dict[str, Any] = {}
292
+ if os.path.exists(self.config_path):
293
+ with open(self.config_path, "r", encoding="utf-8") as _f:
294
+ cfg = yaml.safe_load(_f) or {}
295
+
296
+ # Fall back to config target_column if not explicitly provided
297
+ if target is None:
298
+ target = cfg.get("target_column") or None
299
+ if self.verbose and target:
300
+ print(f"Using target column from config: {target}")
301
+
302
+ # Build objective prompt AFTER target fallback
303
+ if objective is None:
304
+ target_str = f"Target: {target}. " if target else ""
305
+ objective = (
306
+ f"Evaluate the dataset '{dataset_name}' for data quality and fairness issues. "
307
+ f"{target_str}Provide a detailed report highlighting any problems found and suggestions for improvement."
308
+ )
309
+
310
+ if self.verbose:
311
+ print(f"\nEvaluating '{dataset_name}' (target: {target or 'auto-detect'})")
312
+
313
+ # Handle sensitive attribute analysis configuration
314
+ sens_attr_config = cfg.get("sensitive_attribute_analysis", {})
315
+ if sensitive_columns is None and sens_attr_config.get("type") == "restricted":
316
+ # Use restricted list from config
317
+ sensitive_columns = sens_attr_config.get("attributes", [])
318
+ if self.verbose and sensitive_columns:
319
+ print(f"Using restricted sensitive attributes: {sensitive_columns}")
320
+
321
+ # Handle pair evaluation configuration
322
+ pair_config = cfg.get("pair_evaluation", {})
323
+ final_sensitive_pairs = sensitive_pairs # Start with explicit parameter
324
+
325
+ if final_sensitive_pairs is None and pair_config.get("type") == "restricted":
326
+ # Use restricted pairs from config
327
+ final_sensitive_pairs = pair_config.get("pairs", [])
328
+ if self.verbose and final_sensitive_pairs:
329
+ print(f"Using restricted pairs: {final_sensitive_pairs}")
330
+
331
+ # Load max_pairs from config if not provided
332
+ if max_pairs is None:
333
+ max_pairs = pair_config.get("max_pairs")
334
+
335
+ # Handle mitigation techniques configuration
336
+ final_mitigation_config = None
337
+ techniques = mitigation_techniques
338
+ if techniques is None:
339
+ mitigation_cfg = cfg.get("mitigation_techniques", {})
340
+ techniques = mitigation_cfg.get("techniques", [])
341
+ if techniques:
342
+ methods = {}
343
+ for t in techniques:
344
+ display = TECHNIQUE_DISPLAY.get(t.lower())
345
+ if display:
346
+ methods[display] = {}
347
+ else:
348
+ warnings.append(f"Unknown mitigation technique '{t}', skipped.")
349
+ if methods:
350
+ final_mitigation_config = {"methods": methods}
351
+ if self.verbose:
352
+ print(f"Applying mitigation: {list(methods.keys())}")
353
+
354
+ final_ml_config = ml_config
355
+ if final_ml_config is None:
356
+ ml_eval_cfg = cfg.get("ml_evaluation", {})
357
+ if ml_eval_cfg:
358
+ m_type = ml_eval_cfg.get("model_type", "Random Forest")
359
+ m_params = ml_eval_cfg.get("model_params", {}).get(m_type, {})
360
+ final_ml_config = {
361
+ "enabled": True,
362
+ "model_type": m_type,
363
+ "model_params": m_params,
364
+ "test_size": ml_eval_cfg.get("test_size", 0.25)
365
+ }
366
+
367
+ # Build discretization config from YAML
368
+ disc_setting = sens_attr_config.get("discretization", "enable")
369
+ disc_enabled = disc_setting in ("enable", True, "true", "yes")
370
+ disc_method = sens_attr_config.get("method", "auto")
371
+ disc_bins = int(sens_attr_config.get("number_of_bins", 5))
372
+ disc_threshold = int(sens_attr_config.get("continuous_threshold", 10))
373
+ discretization_config = {
374
+ "discretization_enabled": disc_enabled,
375
+ "discretization_method": disc_method,
376
+ "discretization_bins": disc_bins,
377
+ "discretization_threshold": disc_threshold,
378
+ }
379
+ if self.verbose and disc_enabled:
380
+ print(f"Discretization: method={disc_method}, bins={disc_bins}, threshold={disc_threshold}")
381
+
382
+ # Run the pipeline
383
+ self._pipeline.evaluate_dataset(
384
+ user_prompt=objective,
385
+ confirmed_sensitive=sensitive_columns,
386
+ sensitive_pairs=final_sensitive_pairs,
387
+ ml_config=final_ml_config,
388
+ max_pairs=max_pairs,
389
+ mitigation_config=final_mitigation_config,
390
+ discretization_config=discretization_config,
391
+ )
392
+
393
+ # Generate report
394
+ self._pipeline.generate_report()
395
+
396
+ report_dir = self._pipeline.report_dir
397
+ md_path = os.path.join(report_dir, "evaluation_report.md")
398
+ json_path = os.path.join(report_dir, "stage_data.json")
399
+
400
+ # Determine PDF path
401
+ pdf_path = None
402
+ if generate_pdf:
403
+ pdf_path = os.path.join(report_dir, "evaluation_report.pdf")
404
+ if not os.path.exists(pdf_path):
405
+ pdf_path = None
406
+ warnings.append("PDF generation may have failed")
407
+
408
+ # Get completed stages
409
+ stages_completed = list(self._pipeline.evaluation_results.get("stages", {}).keys())
410
+
411
+ if self.verbose:
412
+ print(f"\nEvaluation completed successfully!")
413
+ print(f" Report directory: {report_dir}")
414
+ print(f" Stages completed: {len(stages_completed)}")
415
+ if pdf_path:
416
+ print(f" PDF report: {pdf_path}")
417
+
418
+ return EvaluationResult(
419
+ success=True,
420
+ dataset=dataset_name,
421
+ target_column=target or self._pipeline.target_column,
422
+ report_dir=report_dir,
423
+ pdf_path=pdf_path,
424
+ markdown_path=md_path,
425
+ json_path=json_path,
426
+ warnings=warnings,
427
+ stages_completed=stages_completed,
428
+ )
429
+
430
+ except Exception as e:
431
+
432
+ # Handle API errors with clean messages
433
+ if isinstance(e, APIError):
434
+ error_msg = str(e)
435
+ if self.verbose:
436
+ print(f"\nError: {error_msg}")
437
+ else:
438
+ error_msg = f"{type(e).__name__}: {str(e)}"
439
+ if self.verbose:
440
+ print(f"\nEvaluation failed: {error_msg}")
441
+ traceback.print_exc()
442
+
443
+ return EvaluationResult(
444
+ success=False,
445
+ dataset=dataset_name,
446
+ target_column=target,
447
+ report_dir=getattr(self._pipeline, "report_dir", ""),
448
+ pdf_path=None,
449
+ markdown_path=None,
450
+ json_path=None,
451
+ error=error_msg,
452
+ warnings=warnings,
453
+ stages_completed=stages_completed,
454
+ )
455
+