feat: V1 - DAG编排/告警系统/Agent循环/知识库RAG/Webhook + 审核管理富上下文修复
This commit is contained in:
@@ -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()
|
||||||
Reference in New Issue
Block a user