Files
job-forge/.claude/tools/local_cache_client.py
2025-08-03 16:48:36 -04:00

308 lines
10 KiB
Python

# .claude/tools/local_cache_client.py
"""
AI Cache Client for Local Development
Integrates with n8n-based AI response caching system
"""
import requests
import json
import os
import hashlib
import time
from typing import Optional, Dict, Any
from datetime import datetime
class AICacheClient:
"""Client for interacting with AI Cache MCP service."""
def __init__(self, base_url: str = None, enabled: bool = True): # type: ignore
# Default to your n8n webhook URL
self.base_url = base_url or os.getenv(
"AI_CACHE_URL", "https://n8n.hotserv.cloud/webhook"
)
self.enabled = (
enabled and os.getenv("AI_CACHE_ENABLED", "true").lower() == "true"
)
self.timeout = int(os.getenv("AI_CACHE_TIMEOUT", "15"))
# Stats tracking
self.session_hits = 0
self.session_misses = 0
self.session_start = time.time()
self.connection_failed = False
if self.enabled:
print(f"🧠 AI Cache enabled: {self.base_url}")
self._test_connection()
else:
print("⚠️ AI Cache disabled")
def _test_connection(self):
"""Test if the cache service is accessible."""
try:
response = requests.get(
f"{self.base_url}/ai-cache-stats",
timeout=3 # Quick test
)
if response.status_code == 200:
print("✅ Cache service is accessible")
else:
print(f"⚠️ Cache service returned HTTP {response.status_code}")
self.connection_failed = True
except Exception as e:
print(f"❌ Cache service unreachable: {str(e)[:50]}...")
self.connection_failed = True
def _normalize_prompt(self, prompt: str) -> str:
"""Normalize prompt for consistent matching."""
return prompt.strip().lower().replace("\n", " ").replace(" ", " ")
def lookup_cache(
self, prompt: str, agent_type: str, project: str = "job_forge"
) -> Optional[str]:
"""Look up a cached AI response."""
if not self.enabled or self.connection_failed:
return None
try:
start_time = time.time()
response = requests.post(
f"{self.base_url}/ai-cache-lookup",
json={"prompt": prompt, "agent_type": agent_type, "project": project},
timeout=self.timeout,
)
lookup_time = (time.time() - start_time) * 1000
if response.status_code == 200:
try:
# Debug: print raw response
raw_text = response.text
print(f"🔍 Debug - Raw response: '{raw_text[:100]}...'")
if not raw_text.strip():
print(f"❌ Cache MISS [{agent_type}] - Empty response | Lookup: {lookup_time:.0f}ms")
self.session_misses += 1
return None
data = response.json()
if data.get("found"):
similarity = data.get("similarity", 1.0)
hit_count = data.get("hit_count", 1)
print(
f"✅ Cache HIT! [{agent_type}] Similarity: {similarity:.2f} | Used: {hit_count}x | Lookup: {lookup_time:.0f}ms"
)
self.session_hits += 1
return data.get("response")
else:
print(f"❌ Cache MISS [{agent_type}] | Lookup: {lookup_time:.0f}ms")
self.session_misses += 1
return None
except json.JSONDecodeError as e:
print(f"🚨 JSON decode error: {str(e)} | Response: '{response.text[:50]}'")
self.session_misses += 1
return None
else:
print(f"⚠️ Cache lookup failed: HTTP {response.status_code}")
return None
except requests.exceptions.Timeout:
print(f"⏱️ Cache lookup timeout ({self.timeout}s)")
return None
except Exception as e:
print(f"🚨 Cache error: {str(e)}")
return None
def store_cache(
self,
prompt: str,
response: str,
agent_type: str,
ai_service: str = "claude",
model: str = "claude-sonnet-4",
project: str = "job_forge",
) -> bool:
"""Store an AI response in cache."""
if not self.enabled or not response or len(response.strip()) < 10:
return False
try:
start_time = time.time()
result = requests.post(
f"{self.base_url}/ai-cache-store",
json={
"prompt": prompt,
"response": response,
"ai_service": ai_service,
"model": model,
"agent_type": agent_type,
"project": project,
},
timeout=self.timeout,
)
store_time = (time.time() - start_time) * 1000
if result.status_code == 200:
data = result.json()
if data.get("success"):
print(
f"💾 Response cached [{agent_type}] | Store: {store_time:.0f}ms"
)
return True
else:
print(
f"📄 Already cached [{agent_type}] | Store: {store_time:.0f}ms"
)
return False
else:
print(f"⚠️ Cache store failed: HTTP {result.status_code}")
return False
except requests.exceptions.Timeout:
print(f"⏱️ Cache store timeout ({self.timeout}s)")
return False
except Exception as e:
print(f"🚨 Cache store error: {str(e)}")
return False
def get_stats(self) -> Dict[str, Any]:
"""Get cache statistics."""
try:
response = requests.get(
f"{self.base_url}/ai-cache-stats", timeout=self.timeout
)
if response.status_code == 200:
stats = response.json()
# Add session stats
session_time = time.time() - self.session_start
session_total = self.session_hits + self.session_misses
session_hit_rate = (
(self.session_hits / session_total * 100)
if session_total > 0
else 0
)
stats["session_stats"] = {
"hits": self.session_hits,
"misses": self.session_misses,
"total": session_total,
"hit_rate_percentage": round(session_hit_rate, 1),
"duration_minutes": round(session_time / 60, 1),
}
return stats
else:
return {"error": f"Failed to get stats: {response.status_code}"}
except Exception as e:
return {"error": f"Stats error: {str(e)}"}
def print_session_summary(self):
"""Print session cache performance summary."""
total = self.session_hits + self.session_misses
if total == 0:
return
hit_rate = (self.session_hits / total) * 100
session_time = (time.time() - self.session_start) / 60
print(f"\n📊 Cache Session Summary:")
print(
f" Hits: {self.session_hits} | Misses: {self.session_misses} | Hit Rate: {hit_rate:.1f}%"
)
print(f" Session Time: {session_time:.1f} minutes")
if hit_rate > 60:
print(f" 🎉 Excellent cache performance!")
elif hit_rate > 30:
print(f" 👍 Good cache performance")
else:
print(f" 📈 Cache is learning your patterns...")
# Global cache instance
_cache_instance = None
def get_cache() -> AICacheClient:
"""Get or create global cache instance."""
global _cache_instance
if _cache_instance is None:
_cache_instance = AICacheClient()
return _cache_instance
def cached_ai_query(
prompt: str, agent_type: str, project: str = "job_forge"
) -> tuple[Optional[str], bool]:
"""
Helper function for cached AI queries.
Returns: (cached_response, was_cache_hit)
"""
cache = get_cache()
cached_response = cache.lookup_cache(prompt, agent_type, project)
if cached_response:
return cached_response, True
else:
return None, False
def store_ai_response(
prompt: str, response: str, agent_type: str, project: str = "job_forge"
):
"""Helper function to store AI responses."""
cache = get_cache()
cache.store_cache(prompt, response, agent_type, project=project)
def print_cache_stats():
"""Print current cache statistics."""
cache = get_cache()
stats = cache.get_stats()
if "error" in stats:
print(f"{stats['error']}")
return
summary = stats.get("summary", {})
session = stats.get("session_stats", {})
print(f"\n📈 AI Cache Statistics:")
print(f" Overall Hit Rate: {summary.get('hit_rate_percentage', 0)}%")
print(f" Total Saved: ${summary.get('total_cost_saved_usd', 0):.2f}")
print(f" API Calls Saved: {summary.get('api_calls_saved', 0)}")
if session:
print(
f" This Session: {session['hits']}/{session['total']} hits ({session['hit_rate_percentage']}%)"
)
# Example usage for testing
if __name__ == "__main__":
# Test the cache
cache = get_cache()
# Test lookup
result = cache.lookup_cache("What is the database schema?", "technical_lead")
print(f"Lookup result: {result}")
# Test store
cache.store_cache(
"What is the database schema?",
"PostgreSQL with users and applications tables",
"technical_lead",
)
# Print stats
print_cache_stats()
cache.print_session_summary()