From 3937b4434d1857bc10f4486c5e3a1b50ce096426 Mon Sep 17 00:00:00 2001 From: huangzhuang_3rd Date: Thu, 13 Aug 2026 00:46:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20V1=20-=20DAG=E7=BC=96=E6=8E=92/?= =?UTF-8?q?=E5=91=8A=E8=AD=A6=E7=B3=BB=E7=BB=9F/Agent=E5=BE=AA=E7=8E=AF/?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BA=93RAG/Webhook=20+=20=E5=AE=A1=E6=A0=B8?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=AF=8C=E4=B8=8A=E4=B8=8B=E6=96=87=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/services/dag_service.py | 95 +++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 backend/app/services/dag_service.py diff --git a/backend/app/services/dag_service.py b/backend/app/services/dag_service.py new file mode 100644 index 0000000..b19d0f0 --- /dev/null +++ b/backend/app/services/dag_service.py @@ -0,0 +1,95 @@ +"""DAG orchestration: execute tasks respecting dependencies.""" +import json +from concurrent.futures import ThreadPoolExecutor, as_completed +from sqlalchemy.orm import Session +from typing import List, Dict +from app.models.task import Task +from app.services.execution_service import ExecutionService +from app.services.alert_service import AlertService + + +class DAGService: + @staticmethod + def execute_dag(db: Session, tenant_id: int, project_id: int, task_ids: List[int]) -> Dict: + """Execute tasks respecting depends_on. Parallel for independent tasks.""" + tasks = db.query(Task).filter( + Task.id.in_(task_ids), Task.tenant_id == tenant_id + ).all() + if not tasks: + return {"success": 0, "failed": 0, "blocked": 0, "skipped": 0} + + results = {"success": 0, "failed": 0, "blocked": 0, "skipped": 0} + completed = set() + failed = set() + remaining = {t.id: t for t in tasks} + + while remaining: + # Find tasks whose deps are all completed + ready = [] + for tid, task in list(remaining.items()): + deps = json.loads(task.depends_on or "[]") + deps = [d for d in deps if d in {t.id for t in tasks}] # only consider our set + if not deps: + ready.append(task) + elif all(d in completed for d in deps): + ready.append(task) + elif any(d in failed for d in deps): + # Dependency failed -> block this task + task.status = "cancelled" + results["blocked"] += 1 + del remaining[tid] + + if not ready: + break # no more executable tasks + + # Execute ready tasks in parallel + for task in ready: + if task.id in remaining: + del remaining[task.id] + + with ThreadPoolExecutor(max_workers=3) as pool: + futures = {} + for task in ready: + if task.status in ("done", "cancelled"): + results["skipped"] += 1 + completed.add(task.id) + continue + fut = pool.submit( + DAGService._exec_one, db, tenant_id, task.id + ) + futures[fut] = task.id + + for fut in as_completed(futures): + tid = futures[fut] + try: + result = fut.result() + if result.get("status") in ("success", "needs_review"): + results["success"] += 1 + completed.add(tid) + else: + results["failed"] += 1 + failed.add(tid) + except Exception as e: + results["failed"] += 1 + failed.add(tid) + + # Check budget after each wave + try: + AlertService.check_and_alert(db, tenant_id, project_id) + except Exception: + pass + + return results + + @staticmethod + def _exec_one(db: Session, tenant_id: int, task_id: int): + """Execute a single task in a thread-safe way.""" + from app.database import SessionLocal + session = SessionLocal() + try: + result = ExecutionService.execute(session, tenant_id, task_id) + return {"status": result.status, "task_id": task_id} + except Exception as e: + return {"status": "error", "task_id": task_id, "error": str(e)} + finally: + session.close()