Files
aiagent/backend/app/core/task_system.py
renjianbo beff3fac8d fix: delete agent 500 error + dynamic personality + deployment guide
- Fix delete agent 500: clean up FK records (agent_llm_logs, permissions,
  schedules, executions, team_members) and unbind goals/tasks before delete
- Remove hardcoded personality templates in Android, replace with dynamic
  system prompt generation from name + description
- Set promptSectionsEnabled=false to bypass PromptComposer for personality
- Add Tencent Cloud Linux deployment guide (Docker Compose)
- Accumulated backend service updates, frontend UI fixes, Android app changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-29 01:17:21 +08:00

425 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
任务系统核心 — 原子认领 + 依赖图 + Agent 状态管理
参考 Claude Code src/utils/tasks.ts 的设计模式:
- claimTask 使用 DB 行锁SELECT FOR UPDATE实现原子认领
- block/blockedBy 双向依赖管理
- Agent busy/idle 状态跟踪
"""
from sqlalchemy.orm import Session
from sqlalchemy import and_
from typing import List, Optional, Dict, Any
from datetime import datetime
from dataclasses import dataclass, field
from enum import Enum
import logging
from app.models.task import Task
from app.core.exceptions import NotFoundError, ValidationError
logger = logging.getLogger(__name__)
# ──────────────────────────── 状态定义 ────────────────────────────
class TaskStatus(str, Enum):
PENDING = "pending"
IN_PROGRESS = "in_progress"
AWAITING_APPROVAL = "awaiting_approval"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
class AgentStatus(str, Enum):
IDLE = "idle"
BUSY = "busy"
@dataclass
class ClaimResult:
success: bool
reason: Optional[str] = None # task_not_found / already_claimed / already_resolved / blocked / agent_busy
task: Optional[Task] = None
busy_with_tasks: List[str] = field(default_factory=list) # agent_busy 时
blocked_by_tasks: List[str] = field(default_factory=list) # blocked 时
@dataclass
class AgentState:
agent_id: str
name: str = ""
agent_type: str = ""
status: AgentStatus = AgentStatus.IDLE
current_tasks: List[str] = field(default_factory=list)
# ──────────────────────────── 任务系统 ────────────────────────────
class TaskSystem:
"""任务认领与依赖管理系统"""
def __init__(self, db: Session):
self.db = db
# ── 原子认领 ──
def claim_task(
self,
task_id: str,
agent_id: str,
check_busy: bool = True,
) -> ClaimResult:
"""
使用 SELECT FOR UPDATE 原子认领任务。
检查顺序:
1. 任务存在性
2. 未被其他 Agent 认领
3. 任务未完成
4. 所有 blockedBy 依赖已满足
5. (可选) Agent 不忙碌
"""
# SELECT FOR UPDATE — 锁定行直到事务提交
task = (
self.db.query(Task)
.filter(Task.id == task_id)
.with_for_update()
.first()
)
if not task:
return ClaimResult(success=False, reason="task_not_found")
# 检查是否已被其他 Agent 认领
if task.owner and task.owner != agent_id:
return ClaimResult(success=False, reason="already_claimed", task=task)
# 检查是否已完成
if task.status == TaskStatus.COMPLETED.value:
return ClaimResult(success=False, reason="already_resolved", task=task)
# 检查依赖: blockedBy 中的任务必须全部完成
blocked_by = task.depends_on or []
if blocked_by:
unresolved = (
self.db.query(Task)
.filter(
and_(
Task.id.in_(blocked_by),
Task.status != TaskStatus.COMPLETED.value,
)
)
.all()
)
if unresolved:
return ClaimResult(
success=False,
reason="blocked",
task=task,
blocked_by_tasks=[t.id for t in unresolved],
)
# 检查 Agent 是否忙碌
if check_busy:
agent_open_tasks = (
self.db.query(Task)
.filter(
and_(
Task.owner == agent_id,
Task.status.in_([
TaskStatus.PENDING.value,
TaskStatus.IN_PROGRESS.value,
TaskStatus.AWAITING_APPROVAL.value,
]),
Task.id != task_id,
)
)
.all()
)
if agent_open_tasks:
return ClaimResult(
success=False,
reason="agent_busy",
task=task,
busy_with_tasks=[t.id for t in agent_open_tasks],
)
# 认领
task.owner = agent_id
task.status = TaskStatus.IN_PROGRESS.value
if task.started_at is None:
task.started_at = datetime.now()
self.db.commit()
self.db.refresh(task)
logger.info(f"Task {task_id} claimed by agent {agent_id}")
return ClaimResult(success=True, task=task)
# ── 依赖管理 ──
def block_task(self, from_task_id: str, to_task_id: str) -> bool:
"""
设置任务依赖: from_task 阻塞 to_task。
即: to_task 依赖 from_task 完成后才能执行。
等价于:
- from_task.blocks += to_task_id
- to_task.depends_on += from_task_id
"""
from_task = self.db.query(Task).filter(Task.id == from_task_id).first()
to_task = self.db.query(Task).filter(Task.id == to_task_id).first()
if not from_task or not to_task:
return False
# 检测循环依赖: 如果 to_task 已经(直接或间接)被 from_task 依赖,则形成环
if self._would_create_cycle(from_task_id, to_task_id):
raise ValidationError(
f"无法设置依赖 {from_task_id}{to_task_id}: 会产生循环依赖"
)
# from_task 阻塞 to_task
blocks = list(from_task.blocks or [])
if to_task_id not in blocks:
blocks.append(to_task_id)
from_task.blocks = blocks
# to_task 被 from_task 阻塞
depends = list(to_task.depends_on or [])
if from_task_id not in depends:
depends.append(from_task_id)
to_task.depends_on = depends
self.db.commit()
logger.info(f"Task dependency set: {from_task_id} blocks {to_task_id}")
return True
def unblock_task(self, from_task_id: str, to_task_id: str) -> bool:
"""移除任务依赖"""
from_task = self.db.query(Task).filter(Task.id == from_task_id).first()
to_task = self.db.query(Task).filter(Task.id == to_task_id).first()
if not from_task or not to_task:
return False
blocks = list(from_task.blocks or [])
if to_task_id in blocks:
blocks.remove(to_task_id)
from_task.blocks = blocks
depends = list(to_task.depends_on or [])
if from_task_id in depends:
depends.remove(from_task_id)
to_task.depends_on = depends
self.db.commit()
return True
def _would_create_cycle(self, from_id: str, to_id: str) -> bool:
"""检查 from → to 是否会产生循环依赖"""
# 收集 to_task 直接和间接阻塞的所有任务
visited = set()
stack = [to_id]
while stack:
current = stack.pop()
if current == from_id:
return True
if current in visited:
continue
visited.add(current)
task = self.db.query(Task).filter(Task.id == current).first()
if task and task.blocks:
for blocked_id in task.blocks:
if blocked_id not in visited:
stack.append(blocked_id)
return False
# ── Agent 状态 ──
def get_agent_status(self, agent_id: str) -> AgentState:
"""获取 Agent 忙闲状态及当前持有的任务"""
open_tasks = (
self.db.query(Task)
.filter(
and_(
Task.owner == agent_id,
Task.status.in_([
TaskStatus.PENDING.value,
TaskStatus.IN_PROGRESS.value,
TaskStatus.AWAITING_APPROVAL.value,
]),
)
)
.all()
)
task_ids = [t.id for t in open_tasks]
status = AgentStatus.BUSY if task_ids else AgentStatus.IDLE
return AgentState(
agent_id=agent_id,
status=status,
current_tasks=task_ids,
)
def get_all_agent_statuses(self, task_list_owner_ids: List[str]) -> List[AgentState]:
"""批量获取多个 Agent 的状态"""
result = []
for agent_id in task_list_owner_ids:
result.append(self.get_agent_status(agent_id))
return result
# ── 任务释放 ──
def unassign_agent_tasks(
self,
agent_id: str,
) -> List[Task]:
"""释放 Agent 持有的所有未完成任务Agent 下线/终止时调用)"""
open_tasks = (
self.db.query(Task)
.filter(
and_(
Task.owner == agent_id,
Task.status.in_([
TaskStatus.PENDING.value,
TaskStatus.IN_PROGRESS.value,
TaskStatus.AWAITING_APPROVAL.value,
]),
)
)
.with_for_update()
.all()
)
unassigned = []
for task in open_tasks:
task.owner = None
task.status = TaskStatus.PENDING.value
unassigned.append(task)
logger.info(f"Task {task.id} unassigned from agent {agent_id}")
if unassigned:
self.db.commit()
return unassigned
def release_task(self, task_id: str, agent_id: str) -> bool:
"""释放单个任务Agent 主动放弃)"""
task = (
self.db.query(Task)
.filter(Task.id == task_id)
.with_for_update()
.first()
)
if not task or task.owner != agent_id:
return False
task.owner = None
task.status = TaskStatus.PENDING.value
self.db.commit()
logger.info(f"Task {task_id} released by agent {agent_id}")
return True
# ── 任务完成/失败 ──
def complete_task(self, task_id: str, result: Optional[Dict[str, Any]] = None) -> Optional[Task]:
"""标记任务完成"""
task = self.db.query(Task).filter(Task.id == task_id).first()
if not task:
return None
task.status = TaskStatus.COMPLETED.value
task.result = result or {}
task.completed_at = datetime.now()
self.db.commit()
self.db.refresh(task)
# 检查被此任务阻塞的任务是否现在可以执行
self._check_unblocked_tasks(task)
return task
def fail_task(self, task_id: str, error_message: str = "") -> Optional[Task]:
"""标记任务失败"""
task = self.db.query(Task).filter(Task.id == task_id).first()
if not task:
return None
task.status = TaskStatus.FAILED.value
task.error_message = error_message
task.completed_at = datetime.now()
self.db.commit()
self.db.refresh(task)
return task
def _check_unblocked_tasks(self, completed_task: Task) -> None:
"""检查被已完成任务阻塞的任务是否已解除阻塞"""
blocks = completed_task.blocks or []
for blocked_id in blocks:
blocked_task = self.db.query(Task).filter(Task.id == blocked_id).first()
if not blocked_task:
continue
# 检查 blocked_task 的所有依赖是否都已满足
deps = blocked_task.depends_on or []
all_deps_met = True
for dep_id in deps:
dep = self.db.query(Task).filter(Task.id == dep_id).first()
if dep and dep.status != TaskStatus.COMPLETED.value:
all_deps_met = False
break
if all_deps_met and blocked_task.status == TaskStatus.PENDING.value:
logger.info(
f"Task {blocked_id} is now unblocked (all dependencies met)"
)
# ── 查询辅助 ──
def get_unresolved_blockers(self, task_id: str) -> List[Task]:
"""获取某个任务尚未完成的阻塞任务"""
task = self.db.query(Task).filter(Task.id == task_id).first()
if not task or not task.depends_on:
return []
return (
self.db.query(Task)
.filter(
and_(
Task.id.in_(task.depends_on),
Task.status != TaskStatus.COMPLETED.value,
)
)
.all()
)
def get_next_available_tasks(self, goal_id: str, limit: int = 10) -> List[Task]:
"""获取下一个可执行的任务(依赖已满足、未被认领)"""
# 获取 goal 下所有任务
all_tasks = (
self.db.query(Task)
.filter(Task.goal_id == goal_id)
.all()
)
available = []
for task in all_tasks:
if task.status != TaskStatus.PENDING.value:
continue
if task.owner is not None:
continue
# 检查依赖
deps = task.depends_on or []
blocked = False
for dep_id in deps:
dep = next((t for t in all_tasks if t.id == dep_id), None)
if dep and dep.status != TaskStatus.COMPLETED.value:
blocked = True
break
if not blocked:
available.append(task)
if len(available) >= limit:
break
return available