átlagos kiegészítséek jó sok
This commit is contained in:
439
backend/app/scripts/check_robots_integrity.py
Normal file
439
backend/app/scripts/check_robots_integrity.py
Normal file
@@ -0,0 +1,439 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Robot Health & Integrity Audit Script - Recursive Deep Integrity Audit
|
||||
|
||||
Ez a szkript automatikusan diagnosztizálja az összes robotunk (Scout, Enricher, Validator, Auditor)
|
||||
üzembiztonságát rekurzív felfedezéssel. A következő ellenőrzéseket végzi el:
|
||||
|
||||
1. Auto-Discovery: Rekurzívan bejárja a `backend/app/workers/` teljes könyvtárszerkezetét
|
||||
2. Identification: Minden `.py` fájlt, ami nem `__init__.py` és nem segédfájl, kezel robotként/worker-ként
|
||||
3. Deep Import Test: Megpróbálja importálni mindet, különös figyelemmel a kritikus modulokra
|
||||
4. Model Sync 2.0: Ellenőrzi, hogy az összes robot a helyes modelleket használja-e
|
||||
5. Interface Standardizálás: Ellenőrzi a `run()` metódus jelenlétét
|
||||
6. Kategorizált jelentés: Service, Vehicle General, Vehicle Special, System & OCR kategóriák
|
||||
"""
|
||||
|
||||
import sys
|
||||
import importlib
|
||||
import inspect
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, Tuple
|
||||
import logging
|
||||
import re
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(name)s: %(message)s')
|
||||
logger = logging.getLogger("Robot-Integrity-Audit")
|
||||
|
||||
# Root directory for workers (relative to backend/app)
|
||||
WORKERS_ROOT = Path(__file__).parent.parent / "workers"
|
||||
|
||||
# Exclusion patterns for non-robot files
|
||||
EXCLUDE_PATTERNS = [
|
||||
"__init__.py",
|
||||
"__pycache__",
|
||||
".pyc",
|
||||
"test_",
|
||||
"mapping_",
|
||||
"config",
|
||||
"dictionary",
|
||||
"rules",
|
||||
"report",
|
||||
"monitor_",
|
||||
"py_to_database",
|
||||
"README",
|
||||
# Files with dots in name (not valid Python module names)
|
||||
r".*\..*\.py", # Matches files like "something.1.0.py"
|
||||
]
|
||||
|
||||
# Categorization patterns
|
||||
CATEGORY_PATTERNS = {
|
||||
"Service Robots": [
|
||||
r"service_robot_\d+",
|
||||
r"service/.*\.py$",
|
||||
],
|
||||
"Vehicle General": [
|
||||
r"vehicle_robot_[0-4]_.*",
|
||||
r"R[0-4]_.*\.py$",
|
||||
r"vehicle_robot_1_[245]_.*", # NHTSA, Heavy EU, GB
|
||||
r"vehicle_robot_2_.*", # RDW, AutoData
|
||||
],
|
||||
"Vehicle Special": [
|
||||
r"bike_.*\.py$",
|
||||
r"vehicle_ultimate_.*\.py$",
|
||||
r"ultimatespecs/.*\.py$",
|
||||
],
|
||||
"System & OCR": [
|
||||
r"system_.*\.py$",
|
||||
r"subscription_.*\.py$",
|
||||
r"ocr/.*\.py$",
|
||||
],
|
||||
}
|
||||
|
||||
def discover_robot_files() -> List[Tuple[str, Path, str]]:
|
||||
"""
|
||||
Recursively discover all robot files in the workers directory.
|
||||
Returns list of (module_name, file_path, category) tuples.
|
||||
"""
|
||||
robot_files = []
|
||||
|
||||
for py_file in WORKERS_ROOT.rglob("*.py"):
|
||||
# Skip excluded files
|
||||
file_name = py_file.name
|
||||
# Check for simple pattern matches
|
||||
skip = False
|
||||
for pattern in EXCLUDE_PATTERNS:
|
||||
if pattern.startswith('r.') and len(pattern) > 2:
|
||||
# Regex pattern (simplified)
|
||||
if re.match(pattern[2:], file_name):
|
||||
skip = True
|
||||
break
|
||||
elif pattern in file_name:
|
||||
skip = True
|
||||
break
|
||||
|
||||
# Also skip files with multiple dots in name (not valid Python modules)
|
||||
if file_name.count('.') > 1: # e.g., "something.1.0.py"
|
||||
skip = True
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
# Skip directories
|
||||
if not py_file.is_file():
|
||||
continue
|
||||
|
||||
# Calculate module name (relative to backend/app)
|
||||
try:
|
||||
rel_path = py_file.relative_to(Path(__file__).parent.parent)
|
||||
# Convert path parts to module names, handling dots in filenames
|
||||
module_parts = []
|
||||
for part in rel_path.parts:
|
||||
if part.endswith('.py'):
|
||||
part = part[:-3] # Remove .py
|
||||
# Replace dots with underscores in filename (e.g., "1.0" -> "1_0")
|
||||
part = part.replace('.', '_')
|
||||
module_parts.append(part)
|
||||
|
||||
# Add 'app' prefix since we're in backend/app directory
|
||||
module_name = "app." + ".".join(module_parts)
|
||||
|
||||
# Determine category
|
||||
category = "Uncategorized"
|
||||
for cat_name, patterns in CATEGORY_PATTERNS.items():
|
||||
for pattern in patterns:
|
||||
if re.search(pattern, str(rel_path), re.IGNORECASE):
|
||||
category = cat_name
|
||||
break
|
||||
if category != "Uncategorized":
|
||||
break
|
||||
|
||||
robot_files.append((module_name, py_file, category))
|
||||
|
||||
except ValueError as e:
|
||||
logger.warning(f"Could not determine module for {py_file}: {e}")
|
||||
|
||||
# Sort by category and module name
|
||||
robot_files.sort(key=lambda x: (x[2], x[0]))
|
||||
return robot_files
|
||||
|
||||
async def test_import(module_name: str) -> Tuple[bool, str]:
|
||||
"""Try to import a robot module and return (success, error_message)."""
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
logger.info(f"✅ {module_name} import successful")
|
||||
return True, ""
|
||||
except ImportError as e:
|
||||
error_msg = f"ImportError: {e}"
|
||||
logger.error(f"❌ {module_name} import failed: {e}")
|
||||
return False, error_msg
|
||||
except SyntaxError as e:
|
||||
error_msg = f"SyntaxError at line {e.lineno}: {e.msg}"
|
||||
logger.error(f"❌ {module_name} syntax error: {e}")
|
||||
return False, error_msg
|
||||
except Exception as e:
|
||||
error_msg = f"Exception: {type(e).__name__}: {e}"
|
||||
logger.error(f"❌ {module_name} import failed: {e}")
|
||||
return False, error_msg
|
||||
|
||||
async def check_model_sync(module_name: str) -> List[str]:
|
||||
"""Check if a robot uses correct model references."""
|
||||
errors = []
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
# Get all classes in the module
|
||||
classes = [cls for name, cls in inspect.getmembers(module, inspect.isclass)
|
||||
if not name.startswith('_')]
|
||||
|
||||
for cls in classes:
|
||||
# Check class source code for model references
|
||||
try:
|
||||
source = inspect.getsource(cls)
|
||||
|
||||
# Look for common model name issues
|
||||
old_patterns = [
|
||||
r"VehicleModelDefinitions", # Plural mistake
|
||||
r"vehicle_model_definitions", # Old table name
|
||||
r"ExternalReferenceQueues", # Plural mistake
|
||||
]
|
||||
|
||||
for pattern in old_patterns:
|
||||
if re.search(pattern, source):
|
||||
errors.append(f"⚠️ {module_name}.{cls.__name__} uses old pattern: {pattern}")
|
||||
|
||||
except (OSError, TypeError):
|
||||
pass # Can't get source for built-in or C extensions
|
||||
|
||||
except Exception as e:
|
||||
# If we can't import, this will be caught in import test
|
||||
pass
|
||||
|
||||
return errors
|
||||
|
||||
async def test_robot_interface(module_name: str) -> Tuple[bool, List[str]]:
|
||||
"""Test if a robot has a proper interface (run method, etc.)."""
|
||||
interface_issues = []
|
||||
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
# Find the main robot class (usually ends with the module name or contains 'Robot')
|
||||
classes = [cls for name, cls in inspect.getmembers(module, inspect.isclass)
|
||||
if not name.startswith('_')]
|
||||
|
||||
if not classes:
|
||||
interface_issues.append("No classes found")
|
||||
return False, interface_issues
|
||||
|
||||
main_class = None
|
||||
for cls in classes:
|
||||
cls_name = cls.__name__
|
||||
# Heuristic: class name contains 'Robot' or matches file name pattern
|
||||
if 'Robot' in cls_name or cls_name.lower().replace('_', '') in module_name.lower().replace('_', ''):
|
||||
main_class = cls
|
||||
break
|
||||
|
||||
if main_class is None:
|
||||
main_class = classes[0] # Fallback to first class
|
||||
|
||||
# Check for run/execute/process method (can be classmethod or instance method)
|
||||
has_run_method = hasattr(main_class, 'run')
|
||||
has_execute_method = hasattr(main_class, 'execute')
|
||||
has_process_method = hasattr(main_class, 'process')
|
||||
|
||||
if not (has_run_method or has_execute_method or has_process_method):
|
||||
interface_issues.append(f"No run/execute/process method in {main_class.__name__}")
|
||||
else:
|
||||
# Log which method is found
|
||||
if has_run_method:
|
||||
run_method = getattr(main_class, 'run')
|
||||
# Check if it's a classmethod or instance method
|
||||
if inspect.ismethod(run_method) and run_method.__self__ is main_class:
|
||||
logger.debug(f"✅ {module_name}.{main_class.__name__}.run is classmethod")
|
||||
elif inspect.iscoroutinefunction(run_method):
|
||||
logger.debug(f"✅ {module_name}.{main_class.__name__}.run is async")
|
||||
else:
|
||||
logger.debug(f"ℹ️ {module_name}.{main_class.__name__}.run is sync")
|
||||
|
||||
# Try to instantiate only if the class appears to be instantiable (not abstract)
|
||||
# Check if class has __init__ that doesn't require special arguments
|
||||
try:
|
||||
# First check if class can be instantiated with no arguments
|
||||
sig = inspect.signature(main_class.__init__)
|
||||
params = list(sig.parameters.keys())
|
||||
# If only 'self' parameter, it's instantiable
|
||||
if len(params) == 1: # only self
|
||||
instance = main_class()
|
||||
interface_issues.append(f"Instantiation successful")
|
||||
else:
|
||||
interface_issues.append(f"Instantiation requires arguments, skipping")
|
||||
except (TypeError, AttributeError):
|
||||
# __init__ may not be standard, try anyway
|
||||
try:
|
||||
instance = main_class()
|
||||
interface_issues.append(f"Instantiation successful")
|
||||
except Exception as e:
|
||||
interface_issues.append(f"Instantiation failed (expected): {e}")
|
||||
|
||||
# If we found at least one of the required methods, consider interface OK
|
||||
interface_ok = has_run_method or has_execute_method or has_process_method
|
||||
|
||||
return interface_ok, interface_issues
|
||||
|
||||
except Exception as e:
|
||||
interface_issues.append(f"Interface test error: {e}")
|
||||
return False, interface_issues
|
||||
|
||||
async def check_syntax_errors(file_path: Path) -> List[str]:
|
||||
"""Check for syntax errors by attempting to compile the file."""
|
||||
errors = []
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
source = f.read()
|
||||
compile(source, str(file_path), 'exec')
|
||||
except SyntaxError as e:
|
||||
errors.append(f"Syntax error at line {e.lineno}: {e.msg}")
|
||||
except Exception as e:
|
||||
errors.append(f"Compilation error: {e}")
|
||||
return errors
|
||||
|
||||
async def generate_categorized_report(results: Dict) -> str:
|
||||
"""Generate a categorized audit report."""
|
||||
report_lines = []
|
||||
report_lines.append("# 🤖 Robot Integrity Audit Report")
|
||||
report_lines.append(f"Generated: {importlib.import_module('datetime').datetime.now().isoformat()}")
|
||||
report_lines.append(f"Total robots discovered: {results['total_robots']}")
|
||||
report_lines.append("")
|
||||
|
||||
for category in ["Service Robots", "Vehicle General", "Vehicle Special", "System & OCR", "Uncategorized"]:
|
||||
cat_robots = [r for r in results['robots'] if r['category'] == category]
|
||||
if not cat_robots:
|
||||
continue
|
||||
|
||||
report_lines.append(f"## {category}")
|
||||
report_lines.append(f"**Count:** {len(cat_robots)}")
|
||||
|
||||
# Statistics
|
||||
import_success = sum(1 for r in cat_robots if r['import_success'])
|
||||
syntax_success = sum(1 for r in cat_robots if not r['syntax_errors'])
|
||||
interface_ok = sum(1 for r in cat_robots if r['interface_ok'])
|
||||
|
||||
report_lines.append(f"- Import successful: {import_success}/{len(cat_robots)}")
|
||||
report_lines.append(f"- Syntax clean: {syntax_success}/{len(cat_robots)}")
|
||||
report_lines.append(f"- Interface OK: {interface_ok}/{len(cat_robots)}")
|
||||
|
||||
# List problematic robots
|
||||
problematic = [r for r in cat_robots if not r['import_success'] or r['syntax_errors'] or not r['interface_ok']]
|
||||
if problematic:
|
||||
report_lines.append("\n**Problematic robots:**")
|
||||
for robot in problematic:
|
||||
issues = []
|
||||
if not robot['import_success']:
|
||||
issues.append("Import failed")
|
||||
if robot['syntax_errors']:
|
||||
issues.append(f"Syntax errors ({len(robot['syntax_errors'])})")
|
||||
if not robot['interface_ok']:
|
||||
issues.append("Interface issues")
|
||||
report_lines.append(f"- `{robot['module']}`: {', '.join(issues)}")
|
||||
|
||||
report_lines.append("")
|
||||
|
||||
# Summary
|
||||
report_lines.append("## 📊 Summary")
|
||||
report_lines.append(f"- **Total robots:** {results['total_robots']}")
|
||||
report_lines.append(f"- **Import successful:** {results['import_success']}/{results['total_robots']}")
|
||||
report_lines.append(f"- **Syntax clean:** {results['syntax_clean']}/{results['total_robots']}")
|
||||
report_lines.append(f"- **Interface OK:** {results['interface_ok']}/{results['total_robots']}")
|
||||
|
||||
# Critical issues
|
||||
critical = [r for r in results['robots'] if not r['import_success']]
|
||||
if critical:
|
||||
report_lines.append("\n## 🚨 Critical Issues (Import Failed)")
|
||||
for robot in critical:
|
||||
report_lines.append(f"- `{robot['module']}`: {robot['import_error']}")
|
||||
|
||||
return "\n".join(report_lines)
|
||||
|
||||
async def main():
|
||||
"""Main audit function with recursive discovery."""
|
||||
logger.info("🤖 Starting Recursive Deep Integrity Audit")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# Discover all robot files
|
||||
logger.info("\n🔍 STEP 1: Discovering robot files...")
|
||||
robot_files = discover_robot_files()
|
||||
|
||||
if not robot_files:
|
||||
logger.error("❌ No robot files found!")
|
||||
return False
|
||||
|
||||
logger.info(f"📁 Found {len(robot_files)} robot files")
|
||||
|
||||
results = {
|
||||
'robots': [],
|
||||
'total_robots': len(robot_files),
|
||||
'import_success': 0,
|
||||
'syntax_clean': 0,
|
||||
'interface_ok': 0,
|
||||
}
|
||||
|
||||
# Process each robot
|
||||
logger.info("\n📦 STEP 2: Import and syntax tests...")
|
||||
logger.info("-" * 40)
|
||||
|
||||
for i, (module_name, file_path, category) in enumerate(robot_files, 1):
|
||||
logger.info(f"\n[{i}/{len(robot_files)}] Testing: {module_name} ({category})")
|
||||
|
||||
# Check syntax first
|
||||
syntax_errors = await check_syntax_errors(file_path)
|
||||
|
||||
# Test import
|
||||
import_success, import_error = await test_import(module_name)
|
||||
|
||||
# Test interface
|
||||
interface_ok, interface_issues = await test_robot_interface(module_name)
|
||||
|
||||
# Check model sync
|
||||
model_errors = await check_model_sync(module_name)
|
||||
|
||||
robot_result = {
|
||||
'module': module_name,
|
||||
'file': str(file_path),
|
||||
'category': category,
|
||||
'import_success': import_success,
|
||||
'import_error': import_error,
|
||||
'syntax_errors': syntax_errors,
|
||||
'interface_ok': interface_ok,
|
||||
'interface_issues': interface_issues,
|
||||
'model_errors': model_errors,
|
||||
}
|
||||
|
||||
results['robots'].append(robot_result)
|
||||
|
||||
if import_success:
|
||||
results['import_success'] += 1
|
||||
if not syntax_errors:
|
||||
results['syntax_clean'] += 1
|
||||
if interface_ok:
|
||||
results['interface_ok'] += 1
|
||||
|
||||
# Log summary for this robot
|
||||
status_symbol = "✅" if import_success and not syntax_errors else "❌"
|
||||
logger.info(f"{status_symbol} {module_name}: Import={import_success}, Syntax={len(syntax_errors)} errors, Interface={interface_ok}")
|
||||
|
||||
# Generate report
|
||||
logger.info("\n📊 STEP 3: Generating categorized report...")
|
||||
report = await generate_categorized_report(results)
|
||||
|
||||
# Print summary to console
|
||||
logger.info("\n" + "=" * 60)
|
||||
logger.info("📊 AUDIT SUMMARY")
|
||||
logger.info("=" * 60)
|
||||
logger.info(f"Total robots discovered: {results['total_robots']}")
|
||||
logger.info(f"Import successful: {results['import_success']}/{results['total_robots']}")
|
||||
logger.info(f"Syntax clean: {results['syntax_clean']}/{results['total_robots']}")
|
||||
logger.info(f"Interface OK: {results['interface_ok']}/{results['total_robots']}")
|
||||
|
||||
# Save report to file
|
||||
report_path = Path(__file__).parent.parent.parent / "audit_report_robots.md"
|
||||
with open(report_path, 'w', encoding='utf-8') as f:
|
||||
f.write(report)
|
||||
logger.info(f"\n📄 Full report saved to: {report_path}")
|
||||
|
||||
# Determine overall status
|
||||
critical_count = sum(1 for r in results['robots'] if not r['import_success'])
|
||||
if critical_count > 0:
|
||||
logger.error(f"🚨 ROBOT INTEGRITY CHECK FAILED - {critical_count} critical issues found!")
|
||||
return False
|
||||
elif results['import_success'] < results['total_robots']:
|
||||
logger.warning("⚠️ ROBOT INTEGRITY CHECK PASSED with warnings")
|
||||
return True
|
||||
else:
|
||||
logger.info("✅ ROBOT INTEGRITY CHECK PASSED - All systems operational!")
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = asyncio.run(main())
|
||||
sys.exit(0 if success else 1)
|
||||
Reference in New Issue
Block a user