#!/usr/bin/env python3 """ GenAI Utilities for Claude Code Hooks This module provides reusable utilities for GenAI analysis across all hooks. Centralizing SDK handling, error management, and common patterns enables: - Consistent SDK initialization and error handling - Graceful degradation if SDK unavailable - Unified timeout and configuration management - Reduced code duplication (70% less code per hook) - Easy to test SDK integration independently Core class: GenAIAnalyzer - Handles Anthropic SDK instantiation - Manages fallback chains (SDK → heuristics) - Implements timeout and error handling - Provides logging for debugging """ import os import sys from typing import Optional from genai_prompts import DEFAULT_MODEL, DEFAULT_MAX_TOKENS, DEFAULT_TIMEOUT class GenAIAnalyzer: """Reusable GenAI analysis engine for hooks. Handles: - Anthropic SDK initialization - API error handling and retries - Graceful fallback if SDK unavailable - Timeout management - Optional feature flagging - Debug logging Usage: analyzer = GenAIAnalyzer(use_genai=True) response = analyzer.analyze(PROMPT_TEMPLATE, variable=value) """ def __init__( self, model: str = DEFAULT_MODEL, max_tokens: int = DEFAULT_MAX_TOKENS, timeout: int = DEFAULT_TIMEOUT, use_genai: bool = True, ): """Initialize GenAI analyzer. Args: model: Claude model to use (default: Haiku for speed/cost) max_tokens: Maximum response tokens (default: 100) timeout: API call timeout in seconds (default: 5) use_genai: Whether to enable GenAI (default: True) """ self.model = model self.max_tokens = max_tokens self.timeout = timeout self.use_genai = use_genai self.client = None self.debug = os.environ.get("DEBUG_GENAI", "").lower() == "true" def analyze(self, prompt_template: str, **variables) -> Optional[str]: """Analyze using GenAI with prompt template. Args: prompt_template: Prompt string with {variable} placeholders **variables: Values for template variables Returns: GenAI response text, or None if GenAI disabled/failed """ if not self.use_genai: return None try: # Lazy initialization of SDK client if not self.client: self._initialize_client() if not self.client: return None # Format prompt with variables try: formatted_prompt = prompt_template.format(**variables) except KeyError as e: if self.debug: print(f"⚠️ Prompt template missing variable: {e}", file=sys.stderr) return None # Call GenAI API message = self.client.messages.create( model=self.model, max_tokens=self.max_tokens, messages=[{"role": "user", "content": formatted_prompt}], timeout=self.timeout, ) response = message.content[0].text.strip() if self.debug: print( f"✅ GenAI analysis successful ({len(response)} chars)", file=sys.stderr, ) return response except Exception as e: if self.debug: print(f"⚠️ GenAI analysis failed: {e}", file=sys.stderr) return None def _initialize_client(self): """Initialize Anthropic SDK client. Handles: - SDK import errors - Authentication errors - Environment configuration """ try: from anthropic import Anthropic self.client = Anthropic() if self.debug: print("✅ Anthropic SDK initialized", file=sys.stderr) except ImportError: if self.debug: print( "⚠️ Anthropic SDK not installed: pip install anthropic", file=sys.stderr, ) self.client = None except Exception as e: if self.debug: print(f"⚠️ Failed to initialize Anthropic SDK: {e}", file=sys.stderr) self.client = None def should_use_genai(feature_flag_var: str) -> bool: """Check if GenAI should be enabled for this feature. Args: feature_flag_var: Environment variable name (e.g., "GENAI_SECURITY_SCAN") Returns: True if GenAI enabled (default: True unless explicitly disabled) Usage: use_genai = should_use_genai("GENAI_SECURITY_SCAN") analyzer = GenAIAnalyzer(use_genai=use_genai) """ env_value = os.environ.get(feature_flag_var, "true").lower() return env_value != "false" def parse_classification_response(response: str, expected_values: list) -> Optional[str]: """Parse classification response. For prompts that respond with one of a set of values (e.g., REAL/FAKE). Args: response: Raw response text from GenAI expected_values: List of expected values (case-insensitive) Returns: Matched value (uppercase), or None if no match Usage: response = analyzer.analyze(PROMPT, ...) intent = parse_classification_response(response, ["IMPLEMENT", "REFACTOR", "DOCS", "TEST", "OTHER"]) """ if not response: return None response_upper = response.upper().strip() for expected in expected_values: expected_upper = expected.upper() if expected_upper in response_upper: return expected_upper return None def parse_binary_response( response: str, true_keywords: list, false_keywords: list ) -> Optional[bool]: """Parse binary (yes/no) response. For prompts that respond with approval/rejection (e.g., REAL/FAKE, SIMPLE/COMPLEX). Args: response: Raw response text from GenAI true_keywords: Keywords indicating True (e.g., ["REAL", "YES", "ACCURATE"]) false_keywords: Keywords indicating False (e.g., ["FAKE", "NO", "MISLEADING"]) Returns: True/False if match found, None if ambiguous Usage: response = analyzer.analyze(PROMPT, ...) is_real = parse_binary_response(response, ["REAL", "LIKELY_REAL"], ["FAKE"]) """ if not response: return None response_upper = response.upper() # Check for true keywords first for keyword in true_keywords: if keyword.upper() in response_upper: return True # Check for false keywords for keyword in false_keywords: if keyword.upper() in response_upper: return False # Ambiguous response return None if __name__ == "__main__": # Test utilities print("GenAI Utilities Module") print("======================\n") # Test GenAIAnalyzer initialization analyzer = GenAIAnalyzer(use_genai=False) print(f"Analyzer (GenAI disabled): {analyzer}") print(f" Model: {analyzer.model}") print(f" Max tokens: {analyzer.max_tokens}") print(f" Timeout: {analyzer.timeout}s\n") # Test parsing functions print("Parsing Functions:") print(f" parse_classification_response('REFACTOR', ...): {parse_classification_response('REFACTOR', ['IMPLEMENT', 'REFACTOR', 'DOCS'])}") print( f" parse_binary_response('FAKE', ...): {parse_binary_response('FAKE', ['REAL'], ['FAKE'])}" )