| """ |
| Advanced API Router |
| Provides endpoints for the advanced admin dashboard |
| """ |
| from fastapi import APIRouter, HTTPException, BackgroundTasks |
| from fastapi.responses import JSONResponse |
| from typing import Optional, List, Dict, Any |
| from datetime import datetime, timedelta |
| from pathlib import Path |
| import logging |
| import json |
| import asyncio |
|
|
| logger = logging.getLogger(__name__) |
|
|
| router = APIRouter(prefix="/api", tags=["Advanced API"]) |
|
|
|
|
| |
| |
| |
|
|
| @router.get("/stats/requests") |
| async def get_request_stats(): |
| """Get API request statistics""" |
| try: |
| |
| health_log_path = Path("data/logs/provider_health.jsonl") |
| |
| stats = { |
| 'totalRequests': 0, |
| 'successRate': 0, |
| 'avgResponseTime': 0, |
| 'requestsHistory': [], |
| 'statusBreakdown': { |
| 'success': 0, |
| 'errors': 0, |
| 'timeouts': 0 |
| } |
| } |
| |
| if health_log_path.exists(): |
| with open(health_log_path, 'r', encoding='utf-8') as f: |
| lines = f.readlines() |
| stats['totalRequests'] = len(lines) |
| |
| |
| recent_entries = [] |
| for line in lines[-100:]: |
| try: |
| entry = json.loads(line.strip()) |
| recent_entries.append(entry) |
| except: |
| continue |
| |
| if recent_entries: |
| |
| success_count = sum(1 for e in recent_entries if e.get('status') == 'success') |
| stats['successRate'] = round((success_count / len(recent_entries)) * 100, 1) |
| |
| |
| response_times = [e.get('response_time_ms', 0) for e in recent_entries if e.get('response_time_ms')] |
| if response_times: |
| stats['avgResponseTime'] = round(sum(response_times) / len(response_times)) |
| |
| |
| stats['statusBreakdown']['success'] = success_count |
| stats['statusBreakdown']['errors'] = sum(1 for e in recent_entries if e.get('status') == 'error') |
| stats['statusBreakdown']['timeouts'] = sum(1 for e in recent_entries if e.get('status') == 'timeout') |
| |
| |
| now = datetime.now() |
| for i in range(23, -1, -1): |
| timestamp = now - timedelta(hours=i) |
| stats['requestsHistory'].append({ |
| 'timestamp': timestamp.isoformat(), |
| 'count': max(10, int(stats['totalRequests'] / 24) + (i % 5) * 3) |
| }) |
| |
| return stats |
| |
| except Exception as e: |
| logger.error(f"Error getting request stats: {e}") |
| return { |
| 'totalRequests': 0, |
| 'successRate': 0, |
| 'avgResponseTime': 0, |
| 'requestsHistory': [], |
| 'statusBreakdown': {'success': 0, 'errors': 0, 'timeouts': 0} |
| } |
|
|
|
|
| |
| |
| |
|
|
| @router.post("/resources/scan") |
| async def scan_resources(): |
| """Scan and detect all resources""" |
| try: |
| providers_path = Path("providers_config_extended.json") |
| |
| if not providers_path.exists(): |
| return {'status': 'error', 'message': 'Config file not found'} |
| |
| with open(providers_path, 'r') as f: |
| config = json.load(f) |
| |
| providers = config.get('providers', {}) |
| |
| return { |
| 'status': 'success', |
| 'found': len(providers), |
| 'timestamp': datetime.now().isoformat() |
| } |
| except Exception as e: |
| logger.error(f"Error scanning resources: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.post("/resources/fix-duplicates") |
| async def fix_duplicates(): |
| """Detect and remove duplicate resources""" |
| try: |
| providers_path = Path("providers_config_extended.json") |
| |
| if not providers_path.exists(): |
| return {'status': 'error', 'message': 'Config file not found'} |
| |
| with open(providers_path, 'r') as f: |
| config = json.load(f) |
| |
| providers = config.get('providers', {}) |
| |
| |
| seen = {} |
| duplicates = [] |
| |
| for provider_id, provider_info in list(providers.items()): |
| name = provider_info.get('name', provider_id) |
| normalized_name = name.lower().replace(' ', '').replace('-', '').replace('_', '') |
| |
| if normalized_name in seen: |
| |
| duplicates.append(provider_id) |
| logger.info(f"Found duplicate: {provider_id} (matches {seen[normalized_name]})") |
| else: |
| seen[normalized_name] = provider_id |
| |
| |
| for dup_id in duplicates: |
| del providers[provider_id] |
| |
| |
| if duplicates: |
| |
| backup_path = providers_path.parent / f"{providers_path.name}.backup.{int(datetime.now().timestamp())}" |
| with open(backup_path, 'w') as f: |
| json.dump(config, f, indent=2) |
| |
| |
| with open(providers_path, 'w') as f: |
| json.dump(config, f, indent=2) |
| |
| logger.info(f"Fixed {len(duplicates)} duplicates. Backup: {backup_path}") |
| |
| return { |
| 'status': 'success', |
| 'removed': len(duplicates), |
| 'duplicates': duplicates, |
| 'timestamp': datetime.now().isoformat() |
| } |
| |
| except Exception as e: |
| logger.error(f"Error fixing duplicates: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.post("/resources") |
| async def add_resource(resource: Dict[str, Any]): |
| """Add a new resource""" |
| try: |
| providers_path = Path("providers_config_extended.json") |
| |
| if not providers_path.exists(): |
| raise HTTPException(status_code=404, detail="Config file not found") |
| |
| with open(providers_path, 'r') as f: |
| config = json.load(f) |
| |
| providers = config.get('providers', {}) |
| |
| |
| resource_type = resource.get('type', 'api') |
| name = resource.get('name', 'unknown') |
| provider_id = f"{resource_type}_{name.lower().replace(' ', '_')}" |
| |
| |
| if provider_id in providers: |
| raise HTTPException(status_code=400, detail="Resource already exists") |
| |
| |
| provider_entry = { |
| 'name': name, |
| 'type': resource_type, |
| 'category': resource.get('category', 'unknown'), |
| 'base_url': resource.get('url', ''), |
| 'requires_auth': False, |
| 'validated': False, |
| 'priority': 5, |
| 'added_at': datetime.now().isoformat(), |
| 'notes': resource.get('notes', '') |
| } |
| |
| |
| providers[provider_id] = provider_entry |
| config['providers'] = providers |
| |
| |
| with open(providers_path, 'w') as f: |
| json.dump(config, f, indent=2) |
| |
| logger.info(f"Added new resource: {provider_id}") |
| |
| return { |
| 'status': 'success', |
| 'provider_id': provider_id, |
| 'message': 'Resource added successfully' |
| } |
| |
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Error adding resource: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.delete("/resources/{provider_id}") |
| async def remove_resource(provider_id: str): |
| """Remove a resource""" |
| try: |
| providers_path = Path("providers_config_extended.json") |
| |
| if not providers_path.exists(): |
| raise HTTPException(status_code=404, detail="Config file not found") |
| |
| with open(providers_path, 'r') as f: |
| config = json.load(f) |
| |
| providers = config.get('providers', {}) |
| |
| if provider_id not in providers: |
| raise HTTPException(status_code=404, detail="Resource not found") |
| |
| |
| del providers[provider_id] |
| config['providers'] = providers |
| |
| |
| with open(providers_path, 'w') as f: |
| json.dump(config, f, indent=2) |
| |
| logger.info(f"Removed resource: {provider_id}") |
| |
| return { |
| 'status': 'success', |
| 'message': 'Resource removed successfully' |
| } |
| |
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Error removing resource: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| |
| |
| |
|
|
| @router.post("/discovery/full") |
| async def run_full_discovery(background_tasks: BackgroundTasks): |
| """Run full auto-discovery""" |
| try: |
| |
| import auto_provider_loader |
| |
| async def run_discovery(): |
| """Background task to run discovery""" |
| try: |
| apl = auto_provider_loader.AutoProviderLoader() |
| await apl.run() |
| logger.info(f"Discovery completed: {apl.stats.total_active_providers} providers") |
| except Exception as e: |
| logger.error(f"Discovery error: {e}") |
| |
| |
| background_tasks.add_task(run_discovery) |
| |
| |
| return { |
| 'status': 'started', |
| 'message': 'Discovery started in background', |
| 'found': 0, |
| 'validated': 0, |
| 'failed': 0 |
| } |
| |
| except Exception as e: |
| logger.error(f"Error starting discovery: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.get("/discovery/status") |
| async def get_discovery_status(): |
| """Get current discovery status""" |
| try: |
| report_path = Path("PROVIDER_AUTO_DISCOVERY_REPORT.json") |
| |
| if not report_path.exists(): |
| return { |
| 'status': 'not_run', |
| 'found': 0, |
| 'validated': 0, |
| 'failed': 0 |
| } |
| |
| with open(report_path, 'r') as f: |
| report = json.load(f) |
| |
| stats = report.get('statistics', {}) |
| |
| return { |
| 'status': 'completed', |
| 'found': stats.get('total_http_candidates', 0) + stats.get('total_hf_candidates', 0), |
| 'validated': stats.get('http_valid', 0) + stats.get('hf_valid', 0), |
| 'failed': stats.get('http_invalid', 0) + stats.get('hf_invalid', 0), |
| 'timestamp': report.get('timestamp', '') |
| } |
| |
| except Exception as e: |
| logger.error(f"Error getting discovery status: {e}") |
| return { |
| 'status': 'error', |
| 'found': 0, |
| 'validated': 0, |
| 'failed': 0 |
| } |
|
|
|
|
| |
| |
| |
|
|
| @router.post("/log/request") |
| async def log_request(log_entry: Dict[str, Any]): |
| """Log an API request for tracking""" |
| try: |
| log_dir = Path("data/logs") |
| log_dir.mkdir(parents=True, exist_ok=True) |
| |
| log_file = log_dir / "provider_health.jsonl" |
| |
| |
| log_entry['timestamp'] = datetime.now().isoformat() |
| |
| |
| with open(log_file, 'a', encoding='utf-8') as f: |
| f.write(json.dumps(log_entry) + '\n') |
| |
| return {'status': 'success'} |
| |
| except Exception as e: |
| logger.error(f"Error logging request: {e}") |
| return {'status': 'error', 'message': str(e)} |
|
|
|
|
| |
| |
| |
|
|
| @router.post("/fix/cryptobert-duplicates") |
| async def fix_cryptobert_duplicates(): |
| """Fix CryptoBERT model duplication issues""" |
| try: |
| providers_path = Path("providers_config_extended.json") |
| |
| if not providers_path.exists(): |
| raise HTTPException(status_code=404, detail="Config file not found") |
| |
| with open(providers_path, 'r') as f: |
| config = json.load(f) |
| |
| providers = config.get('providers', {}) |
| |
| |
| cryptobert_models = {} |
| for provider_id, provider_info in list(providers.items()): |
| name = provider_info.get('name', '') |
| if 'cryptobert' in name.lower(): |
| |
| if 'ulako' in provider_id.lower() or 'ulako' in name.lower(): |
| model_key = 'ulako_cryptobert' |
| elif 'kk08' in provider_id.lower() or 'kk08' in name.lower(): |
| model_key = 'kk08_cryptobert' |
| else: |
| model_key = provider_id |
| |
| if model_key in cryptobert_models: |
| |
| existing = cryptobert_models[model_key] |
| |
| |
| if provider_info.get('validated', False) and not providers[existing].get('validated', False): |
| |
| del providers[existing] |
| cryptobert_models[model_key] = provider_id |
| else: |
| |
| del providers[provider_id] |
| else: |
| cryptobert_models[model_key] = provider_id |
| |
| |
| config['providers'] = providers |
| |
| |
| backup_path = providers_path.parent / f"{providers_path.name}.backup.{int(datetime.now().timestamp())}" |
| with open(backup_path, 'w') as f: |
| json.dump(config, f, indent=2) |
| |
| |
| with open(providers_path, 'w') as f: |
| json.dump(config, f, indent=2) |
| |
| logger.info(f"Fixed CryptoBERT duplicates. Models remaining: {len(cryptobert_models)}") |
| |
| return { |
| 'status': 'success', |
| 'models_found': len(cryptobert_models), |
| 'models_remaining': list(cryptobert_models.values()), |
| 'message': 'CryptoBERT duplicates fixed' |
| } |
| |
| except Exception as e: |
| logger.error(f"Error fixing CryptoBERT duplicates: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| |
| |
| |
|
|
| @router.get("/export/analytics") |
| async def export_analytics(): |
| """Export analytics data""" |
| try: |
| stats = await get_request_stats() |
| |
| export_dir = Path("data/exports") |
| export_dir.mkdir(parents=True, exist_ok=True) |
| |
| export_file = export_dir / f"analytics_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" |
| |
| with open(export_file, 'w') as f: |
| json.dump(stats, f, indent=2) |
| |
| return { |
| 'status': 'success', |
| 'file': str(export_file), |
| 'message': 'Analytics exported successfully' |
| } |
| |
| except Exception as e: |
| logger.error(f"Error exporting analytics: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.get("/export/resources") |
| async def export_resources(): |
| """Export resources configuration""" |
| try: |
| providers_path = Path("providers_config_extended.json") |
| |
| if not providers_path.exists(): |
| raise HTTPException(status_code=404, detail="Config file not found") |
| |
| export_dir = Path("data/exports") |
| export_dir.mkdir(parents=True, exist_ok=True) |
| |
| export_file = export_dir / f"resources_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" |
| |
| |
| with open(providers_path, 'r') as f: |
| config = json.load(f) |
| |
| with open(export_file, 'w') as f: |
| json.dump(config, f, indent=2) |
| |
| return { |
| 'status': 'success', |
| 'file': str(export_file), |
| 'providers_count': len(config.get('providers', {})), |
| 'message': 'Resources exported successfully' |
| } |
| |
| except Exception as e: |
| logger.error(f"Error exporting resources: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|