TradingAgents/.claude/hooks/genai_utils.py

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'])}"
)