feat: Add LOCAL mode fallback for all AI enhancements

When no ANTHROPIC_API_KEY is set, ai_enhancer.py now falls back to LOCAL
mode (Claude Code CLI) instead of disabling AI enhancement entirely.

Changes to AIEnhancer base class:
- AUTO mode now falls back to LOCAL instead of disabling
- Added _check_claude_cli() to verify Claude CLI is available
- Added _call_claude_local() method using Claude Code CLI
- Refactored _call_claude() to dispatch to API or LOCAL mode
- 2 minute timeout per LOCAL call (reasonable for batch processing)

This affects all AI enhancements:
- Design pattern enhancement (C3.1) → LOCAL fallback 
- Test example enhancement (C3.2) → LOCAL fallback 
- Architectural analysis (C3.7) → LOCAL fallback 

Before (bad UX):
  ℹ️  AI enhancement disabled (no API key found)

After (good UX):
  ℹ️  No API key found, using LOCAL mode (Claude Code CLI)
   AI enhancement enabled (using LOCAL mode - Claude Code CLI)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
YusufKaraaslanSpyke
2026-01-30 11:15:54 +03:00
parent a8372b8f9d
commit 8a0c1f5fc6

View File

@@ -12,14 +12,23 @@ Features:
- Groups related examples into tutorials - Groups related examples into tutorials
- Identifies best practices - Identifies best practices
Modes:
- API mode: Uses Claude API (requires ANTHROPIC_API_KEY)
- LOCAL mode: Uses Claude Code CLI (no API key needed, uses your Claude Max plan)
- AUTO mode: Tries API first, falls back to LOCAL
Credits: Credits:
- Uses Claude AI (Anthropic) for analysis - Uses Claude AI (Anthropic) for analysis
- Graceful degradation if API unavailable - Graceful degradation if API unavailable
""" """
import json
import logging import logging
import os import os
import subprocess
import tempfile
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -47,9 +56,9 @@ class AIEnhancer:
api_key: Anthropic API key (uses ANTHROPIC_API_KEY env if None) api_key: Anthropic API key (uses ANTHROPIC_API_KEY env if None)
enabled: Enable AI enhancement (default: True) enabled: Enable AI enhancement (default: True)
mode: Enhancement mode - "auto" (default), "api", or "local" mode: Enhancement mode - "auto" (default), "api", or "local"
- "auto": Use API if key available, otherwise disable - "auto": Use API if key available, otherwise fall back to LOCAL
- "api": Force API mode (fails if no key) - "api": Force API mode (fails if no key)
- "local": Use Claude Code local mode (opens terminal) - "local": Use Claude Code CLI (no API key needed)
""" """
self.enabled = enabled self.enabled = enabled
self.mode = mode self.mode = mode
@@ -61,15 +70,9 @@ class AIEnhancer:
if self.api_key: if self.api_key:
self.mode = "api" self.mode = "api"
else: else:
# For now, disable if no API key # Fall back to LOCAL mode (Claude Code CLI)
# LOCAL mode for batch processing is complex self.mode = "local"
self.mode = "disabled" logger.info(" No API key found, using LOCAL mode (Claude Code CLI)")
self.enabled = False
logger.info(" AI enhancement disabled (no API key found)")
logger.info(
" Set ANTHROPIC_API_KEY to enable, or use 'skill-seekers enhance' for SKILL.md"
)
return
if self.mode == "api" and self.enabled: if self.mode == "api" and self.enabled:
try: try:
@@ -84,23 +87,44 @@ class AIEnhancer:
self.client = anthropic.Anthropic(**client_kwargs) self.client = anthropic.Anthropic(**client_kwargs)
logger.info("✅ AI enhancement enabled (using Claude API)") logger.info("✅ AI enhancement enabled (using Claude API)")
except ImportError: except ImportError:
logger.warning("⚠️ anthropic package not installed. AI enhancement disabled.") logger.warning("⚠️ anthropic package not installed, falling back to LOCAL mode")
logger.warning(" Install with: pip install anthropic") self.mode = "local"
self.enabled = False
except Exception as e: except Exception as e:
logger.warning(f"⚠️ Failed to initialize AI client: {e}") logger.warning(f"⚠️ Failed to initialize API client: {e}, falling back to LOCAL mode")
self.mode = "local"
if self.mode == "local" and self.enabled:
# Verify Claude CLI is available
if self._check_claude_cli():
logger.info("✅ AI enhancement enabled (using LOCAL mode - Claude Code CLI)")
else:
logger.warning("⚠️ Claude Code CLI not found. AI enhancement disabled.")
logger.warning(" Install with: npm install -g @anthropic-ai/claude-code")
self.enabled = False self.enabled = False
elif self.mode == "local":
# LOCAL mode requires Claude Code to be available def _check_claude_cli(self) -> bool:
# For patterns/examples, this is less practical than API mode """Check if Claude Code CLI is available"""
logger.info(" LOCAL mode not yet supported for pattern/example enhancement") try:
logger.info( result = subprocess.run(
" Use API mode (set ANTHROPIC_API_KEY) or 'skill-seekers enhance' for SKILL.md" ["claude", "--version"],
capture_output=True,
text=True,
timeout=5,
) )
self.enabled = False return result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired):
return False
def _call_claude(self, prompt: str, max_tokens: int = 1000) -> str | None: def _call_claude(self, prompt: str, max_tokens: int = 1000) -> str | None:
"""Call Claude API with error handling""" """Call Claude (API or LOCAL mode) with error handling"""
if self.mode == "api":
return self._call_claude_api(prompt, max_tokens)
elif self.mode == "local":
return self._call_claude_local(prompt)
return None
def _call_claude_api(self, prompt: str, max_tokens: int = 1000) -> str | None:
"""Call Claude API"""
if not self.client: if not self.client:
return None return None
@@ -115,6 +139,82 @@ class AIEnhancer:
logger.warning(f"⚠️ AI API call failed: {e}") logger.warning(f"⚠️ AI API call failed: {e}")
return None return None
def _call_claude_local(self, prompt: str) -> str | None:
"""Call Claude using LOCAL mode (Claude Code CLI)"""
try:
# Create a temporary directory for this enhancement
with tempfile.TemporaryDirectory(prefix="ai_enhance_") as temp_dir:
temp_path = Path(temp_dir)
# Create prompt file
prompt_file = temp_path / "prompt.md"
output_file = temp_path / "response.json"
# Write prompt with instructions to output JSON
full_prompt = f"""# AI Analysis Task
IMPORTANT: You MUST write your response as valid JSON to this file:
{output_file}
## Task
{prompt}
## Instructions
1. Analyze the input carefully
2. Generate the JSON response as specified
3. Use the Write tool to save the JSON to: {output_file}
4. The JSON must be valid and parseable
DO NOT include any explanation - just write the JSON file.
"""
prompt_file.write_text(full_prompt)
# Run Claude CLI
result = subprocess.run(
["claude", "--dangerously-skip-permissions", str(prompt_file)],
capture_output=True,
text=True,
timeout=120, # 2 minute timeout per call
cwd=str(temp_path),
)
if result.returncode != 0:
logger.warning(f"⚠️ Claude CLI returned error: {result.returncode}")
return None
# Read output file
if output_file.exists():
response_text = output_file.read_text()
# Try to extract JSON from response
try:
# Validate it's valid JSON
json.loads(response_text)
return response_text
except json.JSONDecodeError:
# Try to find JSON in the response
import re
json_match = re.search(r'\[[\s\S]*\]|\{[\s\S]*\}', response_text)
if json_match:
return json_match.group()
logger.warning("⚠️ Could not parse JSON from LOCAL response")
return None
else:
# Look for any JSON file created
for json_file in temp_path.glob("*.json"):
if json_file.name != "prompt.json":
return json_file.read_text()
logger.warning("⚠️ No output file from LOCAL mode")
return None
except subprocess.TimeoutExpired:
logger.warning("⚠️ Claude CLI timeout (2 minutes)")
return None
except Exception as e:
logger.warning(f"⚠️ LOCAL mode error: {e}")
return None
class PatternEnhancer(AIEnhancer): class PatternEnhancer(AIEnhancer):
"""Enhance design pattern detection with AI analysis""" """Enhance design pattern detection with AI analysis"""
@@ -176,8 +276,6 @@ Format as JSON array matching input order. Be concise and actionable.
return patterns return patterns
try: try:
import json
analyses = json.loads(response) analyses = json.loads(response)
# Merge AI analysis into patterns # Merge AI analysis into patterns
@@ -268,8 +366,6 @@ Format as JSON array matching input order. Focus on educational value.
return examples return examples
try: try:
import json
analyses = json.loads(response) analyses = json.loads(response)
# Merge AI analysis into examples # Merge AI analysis into examples