V1: DAG多任务编排 + AI规划WBS + RAG知识库 + 预算告警中心 + 开放API + 飞书/企微/邮件通知
This commit is contained in:
@@ -1,13 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
任务执行引擎:后台线程执行单次 LLM 调用,记录日志与成本,
|
||||
完成后进入「待审核」或直接「已完成」。
|
||||
任务执行引擎 V1:
|
||||
- DAG 依赖校验 + 下游自动触发(串行/并行)
|
||||
- RAG 知识库上下文注入
|
||||
- 预算告警(项目预算使用率阈值)
|
||||
- 事件通知(待审核/完成/失败)与告警记录
|
||||
"""
|
||||
import json
|
||||
import threading
|
||||
import traceback
|
||||
import db
|
||||
import llm_gateway
|
||||
import config
|
||||
import rag
|
||||
import notify
|
||||
|
||||
|
||||
def _log(task_id, level, message):
|
||||
@@ -36,6 +42,25 @@ def _cost_record(task, worker, usage):
|
||||
usage['total_tokens'], usage['cost'], db.now()))
|
||||
|
||||
|
||||
def _deps(task):
|
||||
try:
|
||||
return json.loads(task.get('depends_on') or '[]')
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def check_dependencies(task):
|
||||
"""DAG 依赖检查:返回 (ok, blockers)"""
|
||||
blockers = []
|
||||
for dep_id in _deps(task):
|
||||
dep = db.q('SELECT id, title, status FROM tasks WHERE id=?', (dep_id,), one=True)
|
||||
if not dep:
|
||||
blockers.append(f'# {dep_id}(已删除)')
|
||||
elif dep['status'] != 'done':
|
||||
blockers.append(f'「{dep["title"]}」(#{dep_id}) {dep["status"]}')
|
||||
return (not blockers), blockers
|
||||
|
||||
|
||||
def pick_worker_auto(task):
|
||||
"""自动路由:按模型输入单价升序挑选 enabled Worker"""
|
||||
rows = db.q('SELECT * FROM workers WHERE status="enabled" ORDER BY id')
|
||||
@@ -63,17 +88,71 @@ def check_worker_limits(worker):
|
||||
return True, ''
|
||||
|
||||
|
||||
def _project_cost(project_id):
|
||||
rows = db.q('SELECT COALESCE(SUM(cost),0) AS t FROM cost_records WHERE project_id=?',
|
||||
(project_id,))
|
||||
return rows[0]['t'] if rows else 0.0
|
||||
|
||||
|
||||
def _check_project_budget(task):
|
||||
proj = db.q('SELECT * FROM projects WHERE id=?', (task['project_id'],), one=True)
|
||||
if proj and proj['budget_limit'] and proj['budget_limit'] > 0:
|
||||
rows = db.q('SELECT COALESCE(SUM(cost),0) AS t FROM cost_records WHERE project_id=?',
|
||||
(task['project_id'],))
|
||||
used = rows[0]['t'] if rows else 0
|
||||
used = _project_cost(task['project_id'])
|
||||
if used >= proj['budget_limit']:
|
||||
return False, f'项目预算已用完({used:.2f}/{proj["budget_limit"]:.2f} 元)'
|
||||
return True, ''
|
||||
|
||||
|
||||
def _budget_alert(project_id):
|
||||
"""预算使用率告警(每次任务完成后检查,避免重复刷屏)"""
|
||||
proj = db.q('SELECT * FROM projects WHERE id=?', (project_id,), one=True)
|
||||
if not proj or not proj['budget_limit'] or proj['budget_limit'] <= 0:
|
||||
return
|
||||
used = _project_cost(project_id)
|
||||
ratio = used / proj['budget_limit']
|
||||
if ratio >= config.BUDGET_ALERT_RATIO:
|
||||
# 同项目 1 小时内只告警一次,避免刷屏
|
||||
dup = db.q('SELECT COUNT(*) c FROM alerts WHERE type="budget" AND detail LIKE ? '
|
||||
'AND created_at > ?', (f'项目「{proj["name"]}」%', db.now() - 3600))
|
||||
if dup[0]['c'] == 0:
|
||||
notify.notify('budget_alert',
|
||||
f'预算告警:项目「{proj["name"]}」已使用 {ratio*100:.0f}%',
|
||||
f'已花费 ¥{used:.2f} / 预算 ¥{proj["budget_limit"]:.2f},'
|
||||
f'超过阈值 {config.BUDGET_ALERT_RATIO*100:.0f}%,请关注成本控制。',
|
||||
save_alert=True, level='warn', atype='budget')
|
||||
|
||||
|
||||
def _build_messages(task, worker):
|
||||
"""构造提示词:任务指令 + RAG 知识库上下文"""
|
||||
messages = []
|
||||
if worker['system_prompt']:
|
||||
messages.append({'role': 'system', 'content': worker['system_prompt']})
|
||||
user_text = task['description'] or task['title']
|
||||
ctx, refs = rag.build_context(task['project_id'], user_text)
|
||||
if ctx:
|
||||
user_text = f'{ctx}\n\n----\n\n任务指令:{user_text}'
|
||||
_log(task['id'], 'info', f'📚 RAG 知识库命中 {len(refs)} 个片段:' + ';'.join(refs[:5]))
|
||||
messages.append({'role': 'user', 'content': user_text})
|
||||
return messages
|
||||
|
||||
|
||||
def _trigger_downstream(task):
|
||||
"""DAG:任务完成后自动触发所有就绪的下游任务"""
|
||||
rows = db.q('SELECT * FROM tasks WHERE status IN ("todo","failed")')
|
||||
triggered = []
|
||||
for t in rows:
|
||||
deps = _deps(t)
|
||||
if task['id'] not in deps:
|
||||
continue
|
||||
ok, blockers = check_dependencies(t)
|
||||
if ok:
|
||||
if runner.submit(t['id']):
|
||||
triggered.append(t)
|
||||
else:
|
||||
_log(t['id'], 'info', f'⏳ 等待前置任务完成:' + '、'.join(blockers))
|
||||
return triggered
|
||||
|
||||
|
||||
def run_task(task_id):
|
||||
"""在后台线程中执行任务"""
|
||||
task = db.q('SELECT * FROM tasks WHERE id=?', (task_id,), one=True)
|
||||
@@ -82,6 +161,17 @@ def run_task(task_id):
|
||||
if task['status'] == 'running':
|
||||
return
|
||||
|
||||
# DAG 依赖检查
|
||||
ok, blockers = check_dependencies(task)
|
||||
if not ok:
|
||||
_set_task(task_id, status='failed', error='前置任务未完成:' + '、'.join(blockers),
|
||||
finished_at=db.now())
|
||||
_log(task_id, 'error', '❌ 依赖未满足,无法执行:' + '、'.join(blockers))
|
||||
notify.notify('task_failed', f'任务失败:{task["title"]}',
|
||||
f'项目 #{task["project_id"]} 任务「{task["title"]}」因依赖未完成被拒绝执行:'
|
||||
+ '、'.join(blockers), save_alert=True, level='warn', atype='task_failed')
|
||||
return
|
||||
|
||||
# 确定 Worker
|
||||
worker = None
|
||||
if task['worker_id']:
|
||||
@@ -90,6 +180,9 @@ def run_task(task_id):
|
||||
_set_task(task_id, status='failed', error='指定 Worker 不存在或已停用',
|
||||
finished_at=db.now())
|
||||
_log(task_id, 'error', '指定 Worker 不存在或已停用')
|
||||
notify.notify('worker_alert', f'Worker 异常:任务「{task["title"]}」',
|
||||
f'指定 Worker #{task["worker_id"]} 不存在或已停用', save_alert=True,
|
||||
level='warn', atype='worker_alert')
|
||||
return
|
||||
else:
|
||||
worker = pick_worker_auto(task)
|
||||
@@ -97,6 +190,9 @@ def run_task(task_id):
|
||||
_set_task(task_id, status='failed', error='无可用 Worker(自动路由失败)',
|
||||
finished_at=db.now())
|
||||
_log(task_id, 'error', '自动路由失败:无可用 Worker')
|
||||
notify.notify('worker_alert', f'Worker 异常:任务「{task["title"]}」',
|
||||
'自动路由失败:没有可用的 Worker', save_alert=True,
|
||||
level='warn', atype='worker_alert')
|
||||
return
|
||||
_set_task(task_id, worker_id=worker['id'])
|
||||
_log(task_id, 'info', f'自动路由 → Worker「{worker["name"]}」({worker["provider"]}/{worker["model"]})')
|
||||
@@ -106,29 +202,31 @@ def run_task(task_id):
|
||||
if not ok:
|
||||
_set_task(task_id, status='failed', error=reason, finished_at=db.now())
|
||||
_log(task_id, 'error', reason)
|
||||
notify.notify('budget_alert', f'成本上限拦截:任务「{task["title"]}」', reason,
|
||||
save_alert=True, level='warn', atype='budget')
|
||||
return
|
||||
ok, reason = _check_project_budget(task)
|
||||
if not ok:
|
||||
_set_task(task_id, status='failed', error=reason, finished_at=db.now())
|
||||
_log(task_id, 'error', reason)
|
||||
notify.notify('budget_alert', f'预算拦截:任务「{task["title"]}」', reason,
|
||||
save_alert=True, level='warn', atype='budget')
|
||||
return
|
||||
|
||||
_set_task(task_id, status='running', started_at=db.now(), error='')
|
||||
_log(task_id, 'info', f'开始执行:Worker「{worker["name"]}」 模型 {worker["provider"]}/{worker["model"]}')
|
||||
|
||||
messages = []
|
||||
if worker['system_prompt']:
|
||||
messages.append({'role': 'system', 'content': worker['system_prompt']})
|
||||
messages.append({'role': 'user', 'content': task['description'] or task['title']})
|
||||
|
||||
try:
|
||||
usage = llm_gateway.chat(
|
||||
worker['provider'], worker['model'], messages,
|
||||
worker['provider'], worker['model'], _build_messages(task, worker),
|
||||
temperature=worker['temperature'], max_tokens=worker['max_tokens'],
|
||||
base_url=worker['base_url'] or None, api_key=worker['api_key'] or None)
|
||||
except Exception as e:
|
||||
_set_task(task_id, status='failed', error=str(e), finished_at=db.now())
|
||||
_log(task_id, 'error', f'执行失败: {e}')
|
||||
notify.notify('task_failed', f'任务失败:{task["title"]}',
|
||||
f'项目 #{task["project_id"]} 任务「{task["title"]}」执行出错:{str(e)[:300]}',
|
||||
save_alert=True, level='warn', atype='task_failed')
|
||||
return
|
||||
|
||||
_cost_record(task, worker, usage)
|
||||
@@ -146,8 +244,23 @@ def run_task(task_id):
|
||||
_set_task(task_id, **fields)
|
||||
if new_status == 'review':
|
||||
_log(task_id, 'info', '产出已提交,等待人工审核(HITL)')
|
||||
notify.notify('task_review', f'任务待审核:{task["title"]}',
|
||||
f'项目 #{task["project_id"]} 任务「{task["title"]}」已完成,等待人工验收。\n'
|
||||
f'模型 {worker["provider"]}/{worker["model"]} · {usage["total_tokens"]} tokens · ¥{usage["cost"]:.4f}',
|
||||
save_alert=True, level='info', atype='task_review')
|
||||
else:
|
||||
_log(task_id, 'success', '任务完成(无需审核)')
|
||||
notify.notify('task_done', f'任务完成:{task["title"]}',
|
||||
f'项目 #{task["project_id"]} 任务「{task["title"]}」执行完毕,'
|
||||
f'成本 ¥{usage["cost"]:.4f},tokens {usage["total_tokens"]}',
|
||||
save_alert=False)
|
||||
|
||||
_budget_alert(task['project_id'])
|
||||
|
||||
# DAG:触发下游就绪任务
|
||||
downstream = _trigger_downstream(task)
|
||||
for t in downstream:
|
||||
_log(t['id'], 'info', f'🔗 前置任务「{task["title"]}」已完成,自动触发执行')
|
||||
|
||||
|
||||
class TaskRunner:
|
||||
|
||||
Reference in New Issue
Block a user