Volver a la wiki

Task Huey run_autoplan_import — análisis IA asíncrono de planos

{“sources”: [{“type”: “code”, “ref”: “blueprints/tasks.py”}, {“type”: “code”, “ref”: “blueprints/api/autoplan.py”}, {“type”: “commit”, “ref”: “commit@493b484”}, {“type”: “commit”, “ref”: “cc828f2aad1cf173c4652e7a917ef5c5d1a818af”}], “content”: ”## Definición\n\nNombre de función: run_autoplan_import \nLocalización: blueprints/tasks.py \nFramework: Huey (task queue) \nTipo: @db_task() (con DB access) \nPatrón: ADR T2 / R5 — operación larga (IA ~4 min) sacada del flujo síncrono\n\nTask que corre el análisis IA de un plano subido y crea el Blueprint con sus entidades. Antes de v1.55.0 esto sucedía síncrono en el request HTTP; ahora corre en background, dejando libre el worker ASGI.\n\n## Firma\n\npython\n@db_task()\ndef run_autoplan_import(job_id, file_path, image_filename, bp_name, provider, org_id):\n \"\"\"\n Args:\n job_id (str): UUID del AsyncJob (no el int de BD, sino str).\n file_path (str): Ruta absoluta del fichero subido (volumen media compartido web↔worker).\n image_filename (str): Basename guardado en media (se persiste como Blueprint.image_path).\n bp_name (str): Nombre para el Blueprint (puede venir como \"Auto-Plan <filename>\" si no se pasó custom).\n provider (str): \"google_genai\" (default) u \"ollama\".\n org_id (int): ID de la organización dueña del import (para resolver Organization en BD).\n \"\"\"\n\n\n## Flujo interno\n\npython\n1. job = AsyncJob.objects.filter(job_id=job_id).first()\n ↓\n2. job.mark_processing() # Notifica al cliente que empezó\n ↓\n3. org = Organization.objects.get(id=org_id)\n api_key = _resolve_api_key(provider) # Desde settings, NO del payload\n ↓\n4. scale_x, scale_y, _, _ = AutoPlanService.get_image_scaling(file_path)\n # Guard: valida que sea una imagen real y rechaza bombas de descompresión\n ↓\n5. prompt = get_active_prompt_text(org) # Prompt configurado en la org\n ↓\n6. ai_data = AutoPlanService.analyze_blueprint_image(api_key, file_path, prompt, provider=provider)\n # ⏱️ LA PARTE LENTA (~4 min con Gemma 4 o Gemini)\n ↓\n7. with transaction.atomic():\n new_bp = Blueprint.objects.create(...)\n warnings: list[str] = []\n racks_count = AutoPlanService.create_blueprint_entities(\n new_bp, ai_data, scale_x, scale_y, warnings=warnings\n )\n # warnings recoge lo que el import se saltó o no casó (s300, PR#493):\n # elementos no-dict, label/zone truncados, muros/textos/símbolos fallidos\n ↓\n8. result = {\"blueprint_id\": new_bp.id, \"racks_count\": racks_count, \"warnings\": len(warnings)}\n if warnings:\n result[\"warning_samples\"] = warnings[:10]\n job.mark_done(result)\n # Devuelve información para que el cliente redirija al blueprint\n\n\n## Manejo de errores\n\npython\ntry:\n # ... flujo principal ...\n job.mark_done(...)\nexcept ValueError as e:\n # Errores esperables (validación): imagen inválida, demasiado grande\n logger.warning(\"[AutoPlan] Async import rejected: %s\", e)\n job.mark_error(str(e)) # Apto para usuario\nexcept Exception as e:\n # Inesperados (p.ej. API falla, BD no responde)\n logger.error(\"[AutoPlan] Async import error\\n%s\", traceback.format_exc())\n job.mark_error(\"Auto-Plan import failed. Please try again or check the image.\")\n\n\n## Seguridad\n\n### API key no viaja en Redis\n\nAntes (síncrono):\npython\n# En blueprints/api/autoplan.py\napi_key = getattr(settings, \"GEMINI_API_KEY\", None)\nai_data = AutoPlanService.analyze_blueprint_image(api_key, ...)\n# Síncrono → no hay serialización a Redis\n\n\nAhora (async):\npython\n# blueprints/api/autoplan.py — endpoint\nrun_autoplan_import(job_id, file_path, image_filename, bp_name, provider, org_id)\n# ↑ NO pasa api_key (evita serialización en Redis)\n\n# blueprints/tasks.py — task\napi_key = _resolve_api_key(provider) # Se resuelve aquí desde settings\n# ↑ La credential se obtiene dentro del worker, nunca viaja en el payload\n\n\nEsta es la regla de oro: credentials → siempre resueltas en el boundary de la task, nunca encoladas.\n\n### Aislamiento por organización\n\npython\norg = Organization.objects.get(id=org_id)\nnew_bp = Blueprint.objects.create(..., organization=org, ...)\n\n\nEl Blueprint creado hereda el organization_id del job, garantizando que no se puede “cruzar” orgs.\n\n## Auditoría\n\npython\nlogger.info(\n \"[AutoPlan] Async import OK | org=%s provider=%s bp=%s racks=%s warnings=%s\",\n org_id, provider, new_bp.id, racks_count, len(warnings),\n)\nif warnings:\n logger.warning(\n \"[AutoPlan] Import bp=%s completed WITH %s warning(s): %s\",\n new_bp.id, len(warnings), warnings[:10],\n )\n\n\nRegistro en logs con:\n- Org dueña\n- Proveedor de IA (Gemini, Ollama, etc.)\n- ID del Blueprint creado\n- Cantidad de racks detectados\n- Desde s300 (PR#493): cantidad de warnings (elementos perdidos/recortados durante el import) — si hay alguno, un logger.warning aparte con hasta 10 muestras. Antes un import con pérdidas silenciosas se registraba como éxito limpio.\n\n## Transiciones del AsyncJob\n\n| Estado | Quién | Cuándo | Campo |\n|--------|-------|--------|-------|\n| pending | Endpoint | Al crear el job | (inicial) |\n| processing | Task (línea 49) | Al iniciar la task | mark_processing(progress=0) |\n| done | Task (línea 87) | Tras éxito | mark_done(result={\"blueprint_id\":..., \"racks_count\":..., \"warnings\":..., \"warning_samples\"?:...}) |\n| error | Task (línea 91/94) | Tras error | mark_error(message) |\n\n## Cliente (Frontend)\n\nVer [[entity—core—endpoint—jobs-polling]]. El JavaScript en MapInteraction.js hace polling cada 2.5 seg hasta que status=done o status=error.\n\n## Próximas tareas en ADR T2\n\n- Análisis IA de config: run_ai_analyze_perf, run_ai_analyze_finops (mismo patrón)\n- Backup/Restore full: run_backup_full, run_restore_full (mismo patrón)\n\n## Véase también\n\n- [[concept—infra—async-job-pattern]]\n- [[entity—core—model—asyncjob]]\n- [[entity—core—endpoint—jobs-polling]]\n- [[entity—blueprints—model—blueprint]]\n- [[feature—blueprints—autoplan-async]]\n”}

Subir