245 lines
7.4 KiB
Python
Executable File
245 lines
7.4 KiB
Python
Executable File
#!/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'])}"
|
|
)
|