Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """ | |
| Testing API Router | |
| Provides endpoints for: | |
| - Custom jailbreak prompts upload | |
| - Listing available prompt sources | |
| - Test configuration management | |
| """ | |
| import os | |
| import io | |
| import json | |
| from typing import List, Optional | |
| from fastapi import APIRouter, UploadFile, File, HTTPException, Query | |
| from pydantic import BaseModel, Field | |
| router = APIRouter(prefix="/api/testing", tags=["testing"]) | |
| # Directory for custom prompts | |
| CUSTOM_PROMPTS_DIR = os.path.join( | |
| os.path.dirname(os.path.dirname(os.path.dirname(__file__))), | |
| "datasets", "custom_jailbreak_prompts" | |
| ) | |
| class PromptSource(BaseModel): | |
| """Information about a prompt source.""" | |
| name: str | |
| description: str | |
| count: int | |
| source_type: str # 'builtin' or 'custom' | |
| categories: Optional[List[str]] = None | |
| class UploadResponse(BaseModel): | |
| """Response for prompt upload.""" | |
| status: str | |
| name: str | |
| prompt_count: int | |
| path: str | |
| categories: Optional[List[str]] = None | |
| class TestPreset(BaseModel): | |
| """Test preset configuration.""" | |
| name: str | |
| description: str | |
| max_relations: Optional[int] = None | |
| jailbreak_techniques: int = 10 | |
| demographics_count: int = 4 | |
| comparison_mode: str = "both" | |
| # Available presets | |
| PRESETS = { | |
| "quick": TestPreset( | |
| name="quick", | |
| description="Fast testing with minimal coverage", | |
| max_relations=3, | |
| jailbreak_techniques=3, | |
| demographics_count=2, | |
| comparison_mode="vs_baseline" | |
| ), | |
| "standard": TestPreset( | |
| name="standard", | |
| description="Balanced testing with good coverage", | |
| max_relations=10, | |
| jailbreak_techniques=10, | |
| demographics_count=4, | |
| comparison_mode="both" | |
| ), | |
| "comprehensive": TestPreset( | |
| name="comprehensive", | |
| description="Thorough testing with full coverage", | |
| max_relations=None, | |
| jailbreak_techniques=20, | |
| demographics_count=8, | |
| comparison_mode="both" | |
| ) | |
| } | |
| def get_builtin_prompts_info() -> PromptSource: | |
| """Get information about the built-in jailbreak prompts dataset.""" | |
| try: | |
| dataset_path = os.path.join( | |
| os.path.dirname(os.path.dirname(os.path.dirname(__file__))), | |
| "datasets", "redTeaming_jailbreaking_standard.csv" | |
| ) | |
| if os.path.exists(dataset_path): | |
| import pandas as pd | |
| df = pd.read_csv(dataset_path) | |
| count = len(df) | |
| # Get unique categories/topics if available | |
| categories = None | |
| if 'topic' in df.columns: | |
| categories = df['topic'].dropna().unique().tolist()[:20] | |
| return PromptSource( | |
| name="standard", | |
| description="Built-in jailbreak dataset", | |
| count=count, | |
| source_type="builtin", | |
| categories=categories | |
| ) | |
| else: | |
| return PromptSource( | |
| name="standard", | |
| description="Built-in jailbreak dataset (file not found)", | |
| count=0, | |
| source_type="builtin" | |
| ) | |
| except Exception as e: | |
| return PromptSource( | |
| name="standard", | |
| description=f"Built-in jailbreak dataset (error: {str(e)})", | |
| count=0, | |
| source_type="builtin" | |
| ) | |
| def get_custom_prompts_info() -> List[PromptSource]: | |
| """Get information about custom uploaded prompts.""" | |
| sources = [] | |
| if not os.path.exists(CUSTOM_PROMPTS_DIR): | |
| return sources | |
| for filename in os.listdir(CUSTOM_PROMPTS_DIR): | |
| if filename.endswith('.json'): | |
| try: | |
| filepath = os.path.join(CUSTOM_PROMPTS_DIR, filename) | |
| with open(filepath, 'r') as f: | |
| prompts = json.load(f) | |
| name = filename.replace('.json', '') | |
| sources.append(PromptSource( | |
| name=name, | |
| description=f"Custom uploaded prompts", | |
| count=len(prompts) if isinstance(prompts, list) else 0, | |
| source_type="custom" | |
| )) | |
| except Exception as e: | |
| continue | |
| return sources | |
| async def upload_custom_jailbreak_prompts( | |
| file: UploadFile = File(...), | |
| name: str = Query(default="custom", description="Name for this prompt set") | |
| ): | |
| """ | |
| Upload custom jailbreak prompts. | |
| Supports CSV or JSON formats: | |
| - CSV: Must have 'prompt' column. Optional: 'name', 'description', 'topic' | |
| - JSON: Array of objects with at least 'prompt' field | |
| """ | |
| # Validate filename | |
| if not file.filename: | |
| raise HTTPException(400, "No filename provided") | |
| filename_lower = file.filename.lower() | |
| if not (filename_lower.endswith('.csv') or filename_lower.endswith('.json')): | |
| raise HTTPException(400, "Only CSV or JSON files are supported") | |
| # Create directory if needed | |
| os.makedirs(CUSTOM_PROMPTS_DIR, exist_ok=True) | |
| try: | |
| content = await file.read() | |
| if filename_lower.endswith('.csv'): | |
| import pandas as pd | |
| df = pd.read_csv(io.BytesIO(content)) | |
| if 'prompt' not in df.columns: | |
| raise HTTPException(400, "CSV must have a 'prompt' column") | |
| prompts = df.to_dict('records') | |
| else: | |
| prompts = json.loads(content) | |
| if not isinstance(prompts, list): | |
| raise HTTPException(400, "JSON must be an array of prompt objects") | |
| # Validate prompts have 'prompt' field | |
| for i, p in enumerate(prompts): | |
| if 'prompt' not in p: | |
| raise HTTPException(400, f"Prompt at index {i} missing 'prompt' field") | |
| # Save to JSON file | |
| output_path = os.path.join(CUSTOM_PROMPTS_DIR, f"{name}.json") | |
| with open(output_path, 'w') as f: | |
| json.dump(prompts, f, indent=2) | |
| # Get categories if available | |
| categories = None | |
| if prompts and 'topic' in prompts[0]: | |
| categories = list(set(p.get('topic', '') for p in prompts if p.get('topic')))[:20] | |
| return UploadResponse( | |
| status="success", | |
| name=name, | |
| prompt_count=len(prompts), | |
| path=output_path, | |
| categories=categories | |
| ) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| raise HTTPException(500, f"Error processing file: {str(e)}") | |
| async def list_jailbreak_prompt_sources() -> dict: | |
| """ | |
| List all available jailbreak prompt sources. | |
| Returns both built-in and custom uploaded prompt sets. | |
| """ | |
| sources = [] | |
| # Add built-in | |
| builtin = get_builtin_prompts_info() | |
| sources.append(builtin.model_dump()) | |
| # Add custom | |
| custom_sources = get_custom_prompts_info() | |
| for source in custom_sources: | |
| sources.append(source.model_dump()) | |
| return {"sources": sources} | |
| async def delete_custom_prompts(name: str): | |
| """ | |
| Delete a custom prompt set. | |
| Cannot delete the built-in 'standard' dataset. | |
| """ | |
| if name == "standard": | |
| raise HTTPException(400, "Cannot delete built-in dataset") | |
| filepath = os.path.join(CUSTOM_PROMPTS_DIR, f"{name}.json") | |
| if not os.path.exists(filepath): | |
| raise HTTPException(404, f"Prompt set '{name}' not found") | |
| try: | |
| os.remove(filepath) | |
| return {"status": "success", "message": f"Deleted prompt set '{name}'"} | |
| except Exception as e: | |
| raise HTTPException(500, f"Error deleting file: {str(e)}") | |
| async def list_test_presets() -> dict: | |
| """ | |
| List available test configuration presets. | |
| """ | |
| return { | |
| "presets": [preset.model_dump() for preset in PRESETS.values()] | |
| } | |
| async def get_test_preset(preset_name: str) -> dict: | |
| """ | |
| Get a specific test preset configuration. | |
| """ | |
| if preset_name not in PRESETS: | |
| raise HTTPException(404, f"Preset '{preset_name}' not found") | |
| return PRESETS[preset_name].model_dump() | |
| async def list_available_demographics() -> dict: | |
| """ | |
| List available demographic options for counterfactual bias testing. | |
| """ | |
| return { | |
| "demographics": { | |
| "gender": ["male", "female", "non-binary"], | |
| "race": ["White", "Black", "Asian", "Hispanic", "Middle Eastern", "Indigenous"], | |
| "presets": { | |
| "minimal": [ | |
| {"gender": "male", "race": "White"}, | |
| {"gender": "female", "race": "Black"} | |
| ], | |
| "standard": [ | |
| {"gender": "male", "race": "White"}, | |
| {"gender": "female", "race": "White"}, | |
| {"gender": "male", "race": "Black"}, | |
| {"gender": "female", "race": "Black"} | |
| ], | |
| "comprehensive": [ | |
| {"gender": "male", "race": "White"}, | |
| {"gender": "female", "race": "White"}, | |
| {"gender": "male", "race": "Black"}, | |
| {"gender": "female", "race": "Black"}, | |
| {"gender": "male", "race": "Asian"}, | |
| {"gender": "female", "race": "Asian"}, | |
| {"gender": "male", "race": "Hispanic"}, | |
| {"gender": "female", "race": "Hispanic"}, | |
| {"gender": "non-binary", "race": "White"} | |
| ] | |
| } | |
| }, | |
| "extended_dimensions": { | |
| "age": ["young (20s)", "middle-aged (40s)", "elderly (70s)"], | |
| "disability": ["with no disability", "with a physical disability", "with a visual impairment"], | |
| "socioeconomic": ["from a wealthy background", "from a middle-class background", "from a low-income background"] | |
| }, | |
| "comparison_modes": ["vs_baseline", "all_pairs", "both"] | |
| } | |