ragaai-catalyst 2.1.5b29__py3-none-any.whl → 2.1.5b30__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 (26) hide show
  1. ragaai_catalyst/__init__.py +2 -0
  2. ragaai_catalyst/redteaming/__init__.py +7 -0
  3. ragaai_catalyst/redteaming/config/detectors.toml +13 -0
  4. ragaai_catalyst/redteaming/data_generator/scenario_generator.py +95 -0
  5. ragaai_catalyst/redteaming/data_generator/test_case_generator.py +120 -0
  6. ragaai_catalyst/redteaming/evaluator.py +125 -0
  7. ragaai_catalyst/redteaming/llm_generator.py +83 -0
  8. ragaai_catalyst/redteaming/llm_generator_litellm.py +66 -0
  9. ragaai_catalyst/redteaming/red_teaming.py +329 -0
  10. ragaai_catalyst/redteaming/requirements.txt +4 -0
  11. ragaai_catalyst/redteaming/tests/grok.ipynb +97 -0
  12. ragaai_catalyst/redteaming/tests/stereotype.ipynb +2258 -0
  13. ragaai_catalyst/redteaming/upload_result.py +38 -0
  14. ragaai_catalyst/redteaming/utils/issue_description.py +114 -0
  15. ragaai_catalyst/redteaming_old.py +171 -0
  16. ragaai_catalyst/synthetic_data_generation.py +344 -13
  17. ragaai_catalyst/tracers/agentic_tracing/tracers/llm_tracer.py +2 -6
  18. ragaai_catalyst/tracers/agentic_tracing/utils/llm_utils.py +22 -4
  19. ragaai_catalyst/tracers/agentic_tracing/utils/zip_list_of_unique_files.py +0 -13
  20. ragaai_catalyst/tracers/tracer.py +33 -2
  21. {ragaai_catalyst-2.1.5b29.dist-info → ragaai_catalyst-2.1.5b30.dist-info}/METADATA +19 -2
  22. {ragaai_catalyst-2.1.5b29.dist-info → ragaai_catalyst-2.1.5b30.dist-info}/RECORD +25 -12
  23. ragaai_catalyst/redteaming.py +0 -171
  24. {ragaai_catalyst-2.1.5b29.dist-info → ragaai_catalyst-2.1.5b30.dist-info}/LICENSE +0 -0
  25. {ragaai_catalyst-2.1.5b29.dist-info → ragaai_catalyst-2.1.5b30.dist-info}/WHEEL +0 -0
  26. {ragaai_catalyst-2.1.5b29.dist-info → ragaai_catalyst-2.1.5b30.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,329 @@
1
+ from datetime import datetime
2
+ import json
3
+ import os
4
+ from typing import Dict, List, Any, Tuple, Literal, Optional
5
+
6
+ import pandas as pd
7
+ import tomli
8
+ from tqdm import tqdm
9
+
10
+ from .data_generator.scenario_generator import ScenarioGenerator, ScenarioInput
11
+ from .data_generator.test_case_generator import TestCaseGenerator, TestCaseInput
12
+ from .evaluator import Evaluator, EvaluationInput, Conversation
13
+ from .utils.issue_description import get_issue_description
14
+ from .upload_result import UploadResult
15
+ from rich import print
16
+
17
+ class RedTeaming:
18
+ def __init__(
19
+ self,
20
+ model_name: Literal["gpt-4-1106-preview", "grok-2-latest"] = "grok-2-latest",
21
+ provider: Literal["openai", "xai"] = "xai",
22
+ api_key: str = "",
23
+ scenario_temperature: float = 0.7,
24
+ test_temperature: float = 0.8,
25
+ eval_temperature: float = 0.3,
26
+ ):
27
+ """
28
+ Initialize the red teaming pipeline.
29
+
30
+ Args:
31
+ model_name: The OpenAI model to use
32
+ scenario_temperature: Temperature for scenario generation
33
+ api_key: Api Key for the provider
34
+ test_temperature: Temperature for test case generation
35
+ eval_temperature: Temperature for evaluation (lower for consistency)
36
+ """
37
+ if api_key == "":
38
+ raise ValueError("Api Key is required")
39
+
40
+ # Load supported detectors configuration
41
+ self._load_supported_detectors()
42
+
43
+ # Initialize generators and evaluator
44
+ self.scenario_generator = ScenarioGenerator(api_key=api_key, model_name=model_name, temperature=scenario_temperature, provider=provider)
45
+ self.test_generator = TestCaseGenerator(api_key=api_key, model_name=model_name, temperature=test_temperature, provider=provider)
46
+ self.evaluator = Evaluator(api_key=api_key, model_name=model_name, temperature=eval_temperature, provider=provider)
47
+
48
+ self.save_path = None
49
+
50
+ def upload_result(self, project_name, dataset_name):
51
+ upload_result = UploadResult(project_name)
52
+ if self.save_path is None:
53
+ print('Please execute the RedTeaming run() method before uploading the result')
54
+ return
55
+ upload_result.upload_result(csv_path=self.save_path, dataset_name=dataset_name)
56
+
57
+
58
+ def _load_supported_detectors(self) -> None:
59
+ """Load supported detectors from TOML configuration file."""
60
+ config_path = os.path.join(os.path.dirname(__file__), "config", "detectors.toml")
61
+ try:
62
+ with open(config_path, "rb") as f:
63
+ config = tomli.load(f)
64
+ self.supported_detectors = set(config.get("detectors", {}).get("detector_names", []))
65
+ except FileNotFoundError:
66
+ print(f"Warning: Detectors configuration file not found at {config_path}")
67
+ self.supported_detectors = set()
68
+ except Exception as e:
69
+ print(f"Error loading detectors configuration: {e}")
70
+ self.supported_detectors = set()
71
+
72
+ def validate_detectors(self, detectors: List[str]) -> None:
73
+ """Validate that all provided detectors are supported.
74
+
75
+ Args:
76
+ detectors: List of detector IDs to validate
77
+
78
+ Raises:
79
+ ValueError: If any detector is not supported
80
+ """
81
+ unsupported = [d for d in detectors if d not in self.supported_detectors]
82
+ if unsupported:
83
+ raise ValueError(
84
+ f"Unsupported detectors: {unsupported}\n"
85
+ f"Supported detectors are: {sorted(self.supported_detectors)}"
86
+ )
87
+
88
+ def get_supported_detectors(self) -> List[str]:
89
+ """Get the list of supported detectors."""
90
+ return sorted(self.supported_detectors)
91
+
92
+ def _get_save_path(self, description: str) -> str:
93
+ """Generate a path for saving the final DataFrame."""
94
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
95
+ output_dir = os.path.join(os.path.dirname(__file__), "results")
96
+ os.makedirs(output_dir, exist_ok=True)
97
+
98
+ # Create a short slug from the description
99
+ slug = description.lower()[:30].replace(" ", "_")
100
+ return os.path.join(output_dir, f"red_teaming_{slug}_{timestamp}.csv")
101
+
102
+ def _save_results_to_csv(self, result_df: pd.DataFrame, description: str) -> str:
103
+ # Save DataFrame
104
+ save_path = self._get_save_path(description)
105
+ result_df.to_csv(save_path, index=False)
106
+ print(f"\nResults saved to: {save_path}")
107
+ return save_path
108
+
109
+ def _run_with_examples(self, description: str, detectors: List[str], response_model: Any, examples: List[str], scenarios_per_detector: int) -> pd.DataFrame:
110
+ results = []
111
+ # Process each detector
112
+ for detector in detectors:
113
+ print('='*50)
114
+ print(f"Running detector: [yellow2]{detector}[/yellow2]")
115
+ print('='*50)
116
+
117
+ if type(detector) == str:
118
+ # Get issue description for this detector
119
+ issue_description = get_issue_description(detector)
120
+ else:
121
+ issue_description = detector.get("custom", "")
122
+
123
+ # Generate scenarios for this detector
124
+ scenario_input = ScenarioInput(
125
+ description=description,
126
+ category=issue_description,
127
+ scenarios_per_detector=scenarios_per_detector
128
+ )
129
+ scenarios = self.scenario_generator.generate_scenarios(scenario_input)
130
+
131
+ # Process each scenario
132
+ for r, scenario in enumerate(scenarios):
133
+
134
+ if type(examples[0]) == str:
135
+ test_examples = examples
136
+ test_detectors = [detectors] * len(examples)
137
+ elif type(examples[0]) == dict:
138
+ test_examples = [example["input"] for example in examples]
139
+ test_detectors = [example["detectors"] for example in examples]
140
+
141
+ # Evaluate test cases
142
+ failed_tests = 0
143
+ total_tests = 0
144
+ for test_example, test_detector in tqdm(zip(test_examples, test_detectors), desc=f"Running {detector} scenario {r+1}/{len(scenarios)}", total=len(scenarios)):
145
+ if detector in test_detector:
146
+ user_message = test_example
147
+ app_response = response_model(user_message)
148
+
149
+ # Evaluate the conversation
150
+ eval_input = EvaluationInput(
151
+ description=description,
152
+ conversation=Conversation(
153
+ user_message=user_message,
154
+ app_response=app_response
155
+ ),
156
+ scenarios=[scenario]
157
+ )
158
+ evaluation = self.evaluator.evaluate_conversation(eval_input)
159
+
160
+ # Store results
161
+ results.append({
162
+ "detector": detector,
163
+ "scenario": scenario,
164
+ "user_message":test_example,
165
+ "app_response": app_response,
166
+ "evaluation_score": "pass" if evaluation["eval_passed"] else "fail",
167
+ "evaluation_reason": evaluation["reason"]
168
+ })
169
+
170
+ if not evaluation["eval_passed"]:
171
+ failed_tests += 1
172
+
173
+ total_tests += 1
174
+
175
+ # Report results for this scenario
176
+ if failed_tests > 0:
177
+ print(f"{detector} scenario {r+1}: [bright_red]{failed_tests}/{total_tests} examples failed[/bright_red]")
178
+ elif total_tests > 0:
179
+ print(f"{detector} scenario {r+1}: [green]All {total_tests} examples passed[/green]")
180
+ else:
181
+ print(f"No examples provided to test {detector} scenario {r+1}")
182
+ print('-'*100)
183
+
184
+ # Save results to a CSV file
185
+ results_df = pd.DataFrame(results)
186
+ save_path = self._save_results_to_csv(results_df, description)
187
+ self.save_path = save_path
188
+
189
+ return results_df, save_path
190
+
191
+ def _run_without_examples(self, description: str, detectors: List[str], response_model: Any, model_input_format: Dict[str, Any], scenarios_per_detector: int, test_cases_per_scenario: int) -> pd.DataFrame:
192
+ results = []
193
+ # Process each detector
194
+ for detector in detectors:
195
+ print('='*50)
196
+ print(f"Running detector: [yellow2]{detector}[/yellow2]")
197
+ print('='*50)
198
+
199
+ if type(detector) == str:
200
+ # Get issue description for this detector
201
+ issue_description = get_issue_description(detector)
202
+ else:
203
+ issue_description = detector.get("custom", "")
204
+
205
+ # Generate scenarios for this detector
206
+ scenario_input = ScenarioInput(
207
+ description=description,
208
+ category=issue_description,
209
+ scenarios_per_detector=scenarios_per_detector
210
+ )
211
+ scenarios = self.scenario_generator.generate_scenarios(scenario_input)
212
+
213
+ # Process each scenario
214
+ for r, scenario in enumerate(scenarios):
215
+ # Generate test cases
216
+ test_input = TestCaseInput(
217
+ description=description,
218
+ category=issue_description,
219
+ scenario=scenario,
220
+ format_example=model_input_format,
221
+ languages=["English"],
222
+ num_inputs=test_cases_per_scenario
223
+ )
224
+ test_cases = self.test_generator.generate_test_cases(test_input)
225
+
226
+ # Evaluate test cases
227
+ failed_tests = 0
228
+ with tqdm(test_cases["inputs"],
229
+ desc=f"Evaluating {detector} scenario {r+1}/{len(scenarios)}") as pbar:
230
+ for test_case in pbar:
231
+ user_message = test_case["user_input"]
232
+ app_response = response_model(user_message)
233
+
234
+ # Evaluate the conversation
235
+ eval_input = EvaluationInput(
236
+ description=description,
237
+ conversation=Conversation(
238
+ user_message=user_message,
239
+ app_response=app_response
240
+ ),
241
+ scenarios=[scenario]
242
+ )
243
+ evaluation = self.evaluator.evaluate_conversation(eval_input)
244
+
245
+ # Store results
246
+ results.append({
247
+ "detector": detector,
248
+ "scenario": scenario,
249
+ "user_message": user_message,
250
+ "app_response": app_response,
251
+ "evaluation_score": "pass" if evaluation["eval_passed"] else "fail",
252
+ "evaluation_reason": evaluation["reason"]
253
+ })
254
+
255
+ if not evaluation["eval_passed"]:
256
+ failed_tests += 1
257
+
258
+ # Report results for this scenario
259
+ total_tests = len(test_cases["inputs"])
260
+ if failed_tests > 0:
261
+ print(f"{detector} scenario {r+1}: [bright_red]{failed_tests}/{total_tests} tests failed[/bright_red]")
262
+ else:
263
+ print(f"{detector} scenario {r+1}: [green]All {total_tests} tests passed[/green]")
264
+ print('-'*100)
265
+
266
+ # Save results to a CSV file
267
+ results_df = pd.DataFrame(results)
268
+ save_path = self._save_results_to_csv(results_df, description)
269
+ self.save_path = save_path
270
+
271
+ return results_df, save_path
272
+
273
+
274
+ def run(
275
+ self,
276
+ description: str,
277
+ detectors: List[str],
278
+ response_model: Any,
279
+ examples: Optional[List[str]] = None,
280
+ model_input_format: Optional[Dict[str, Any]] = None,
281
+ scenarios_per_detector: int = 4,
282
+ examples_per_scenario: int = 5 # used only if examples are not provided
283
+ ) -> pd.DataFrame:
284
+ """
285
+ Run the complete red teaming pipeline.
286
+
287
+ Args:
288
+ description: Description of the app being tested
289
+ detectors: List of detector names to test against (e.g., ["stereotypes", "harmful_content"])
290
+ response_model: Function that takes a user message and returns the app's response
291
+ model_input_format: Format for test case generation
292
+ examples: List of example inputs to test. If provided, uses these instead of generating test cases
293
+ scenarios_per_detector: Number of test scenarios to generate per detector
294
+ examples_per_scenario: Number of test cases to generate per scenario
295
+
296
+ Returns:
297
+ DataFrame containing all test results with columns:
298
+ - scenario: The scenario being tested
299
+ - user_message: The test input
300
+ - app_response: The model's response
301
+ - evaluation_score: Score of whether the response passed evaluation
302
+ - evaluation_reason: Reason for pass/fail
303
+ """
304
+
305
+ if examples is None:
306
+ examples = []
307
+ if model_input_format is None:
308
+ model_input_format = {
309
+ "user_input": "Hi, I am looking for job recommendations",
310
+ "user_name": "John"
311
+ }
312
+
313
+ # Validate detectors
314
+ inbuild_detector = []
315
+ for detector in detectors:
316
+ if type(detector) == str:
317
+ inbuild_detector.append(detector)
318
+ elif type(detector) == dict:
319
+ if 'custom' not in detector.keys() or len(detector.keys()) != 1:
320
+ raise ValueError('The custom detector must be a dictionary with only key "custom" and a string as a value')
321
+ else:
322
+ raise ValueError('Detector must be a string or a dictionary with only key "custom" and a string as a value')
323
+
324
+ self.validate_detectors(inbuild_detector)
325
+
326
+ if examples:
327
+ return self._run_with_examples(description, detectors, response_model, examples, scenarios_per_detector)
328
+
329
+ return self._run_without_examples(description, detectors, response_model, model_input_format, scenarios_per_detector, examples_per_scenario)
@@ -0,0 +1,4 @@
1
+ openai>=1.0.0
2
+ pandas>=2.0.0
3
+ tomli>=2.0.0
4
+ tqdm>=4.65.0
@@ -0,0 +1,97 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 11,
6
+ "metadata": {},
7
+ "outputs": [
8
+ {
9
+ "data": {
10
+ "text/plain": [
11
+ "True"
12
+ ]
13
+ },
14
+ "execution_count": 11,
15
+ "metadata": {},
16
+ "output_type": "execute_result"
17
+ }
18
+ ],
19
+ "source": [
20
+ "from dotenv import load_dotenv\n",
21
+ "\n",
22
+ "load_dotenv()"
23
+ ]
24
+ },
25
+ {
26
+ "cell_type": "code",
27
+ "execution_count": 12,
28
+ "metadata": {},
29
+ "outputs": [
30
+ {
31
+ "name": "stdout",
32
+ "output_type": "stream",
33
+ "text": [
34
+ "The answer to the ultimate question of life, the universe, and everything is 42. However, the actual question itself remains unknown. It's a bit of a cosmic joke, really. But hey, who needs a definitive answer when you can enjoy the journey of figuring it out? Just remember, the answer is out there, and it's 42. Now, go forth and explore the vastness of existence!\n"
35
+ ]
36
+ }
37
+ ],
38
+ "source": [
39
+ "# In your terminal, first run:\n",
40
+ "# pip install openai\n",
41
+ "\n",
42
+ "import os\n",
43
+ "from openai import OpenAI\n",
44
+ "\n",
45
+ "XAI_API_KEY = os.getenv('XAI_API_KEY')\n",
46
+ "client = OpenAI(\n",
47
+ " api_key=XAI_API_KEY,\n",
48
+ " base_url=\"https://api.x.ai/v1\",\n",
49
+ ")\n",
50
+ "\n",
51
+ "completion = client.chat.completions.create(\n",
52
+ " model=\"grok-2-latest\",\n",
53
+ " messages=[\n",
54
+ " {\n",
55
+ " \"role\": \"system\",\n",
56
+ " \"content\": \"You are Grok, a chatbot inspired by the Hitchhikers Guide to the Galaxy.\"\n",
57
+ " },\n",
58
+ " {\n",
59
+ " \"role\": \"user\",\n",
60
+ " \"content\": \"What is the meaning of life, the universe, and everything?\"\n",
61
+ " },\n",
62
+ " ],\n",
63
+ ")\n",
64
+ "\n",
65
+ "print(completion.choices[0].message.content)"
66
+ ]
67
+ },
68
+ {
69
+ "cell_type": "code",
70
+ "execution_count": null,
71
+ "metadata": {},
72
+ "outputs": [],
73
+ "source": []
74
+ }
75
+ ],
76
+ "metadata": {
77
+ "kernelspec": {
78
+ "display_name": "base",
79
+ "language": "python",
80
+ "name": "python3"
81
+ },
82
+ "language_info": {
83
+ "codemirror_mode": {
84
+ "name": "ipython",
85
+ "version": 3
86
+ },
87
+ "file_extension": ".py",
88
+ "mimetype": "text/x-python",
89
+ "name": "python",
90
+ "nbconvert_exporter": "python",
91
+ "pygments_lexer": "ipython3",
92
+ "version": "3.12.2"
93
+ }
94
+ },
95
+ "nbformat": 4,
96
+ "nbformat_minor": 2
97
+ }