177 lines
6.6 KiB
Python
177 lines
6.6 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
任务执行引擎:后台线程执行单次 LLM 调用,记录日志与成本,
|
||
完成后进入「待审核」或直接「已完成」。
|
||
"""
|
||
import threading
|
||
import traceback
|
||
import db
|
||
import llm_gateway
|
||
import config
|
||
|
||
|
||
def _log(task_id, level, message):
|
||
try:
|
||
db.w('INSERT INTO task_logs (task_id, level, message, created_at) VALUES (?,?,?,?)',
|
||
(task_id, level, message, db.now()))
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def _set_task(task_id, **fields):
|
||
if not fields:
|
||
return
|
||
fields['updated_at'] = db.now()
|
||
sets = ', '.join(f'{k}=?' for k in fields)
|
||
db.w(f'UPDATE tasks SET {sets} WHERE id=?', (*fields.values(), task_id))
|
||
|
||
|
||
def _cost_record(task, worker, usage):
|
||
db.w(
|
||
'INSERT INTO cost_records (task_id, project_id, worker_id, provider, model, '
|
||
'prompt_tokens, completion_tokens, total_tokens, cost, created_at) '
|
||
'VALUES (?,?,?,?,?,?,?,?,?,?)',
|
||
(task['id'], task['project_id'], worker['id'], worker['provider'],
|
||
worker['model'], usage['prompt_tokens'], usage['completion_tokens'],
|
||
usage['total_tokens'], usage['cost'], db.now()))
|
||
|
||
|
||
def pick_worker_auto(task):
|
||
"""自动路由:按模型输入单价升序挑选 enabled Worker"""
|
||
rows = db.q('SELECT * FROM workers WHERE status="enabled" ORDER BY id')
|
||
if not rows:
|
||
return None
|
||
best, best_price = None, None
|
||
for r in rows:
|
||
pin, pout = llm_gateway.model_price(r['model'])
|
||
price = pin + pout * 0.5
|
||
if best_price is None or price < best_price:
|
||
best, best_price = r, price
|
||
return best
|
||
|
||
|
||
def check_worker_limits(worker):
|
||
"""成本上限预检:返回 (ok, reason)"""
|
||
if worker['task_cost_limit'] and worker['task_cost_limit'] > 0:
|
||
used = db.task_worker_cost(worker['id'])
|
||
if used >= worker['task_cost_limit']:
|
||
return False, f'该 Worker 累计成本 {used:.4f} 元已达单任务上限 {worker["task_cost_limit"]} 元'
|
||
if worker['monthly_cost_limit'] and worker['monthly_cost_limit'] > 0:
|
||
used = db.monthly_worker_cost(worker['id'])
|
||
if used >= worker['monthly_cost_limit']:
|
||
return False, f'该 Worker 本月成本 {used:.4f} 元已达月度上限 {worker["monthly_cost_limit"]} 元'
|
||
return True, ''
|
||
|
||
|
||
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
|
||
if used >= proj['budget_limit']:
|
||
return False, f'项目预算已用完({used:.2f}/{proj["budget_limit"]:.2f} 元)'
|
||
return True, ''
|
||
|
||
|
||
def run_task(task_id):
|
||
"""在后台线程中执行任务"""
|
||
task = db.q('SELECT * FROM tasks WHERE id=?', (task_id,), one=True)
|
||
if not task:
|
||
return
|
||
if task['status'] == 'running':
|
||
return
|
||
|
||
# 确定 Worker
|
||
worker = None
|
||
if task['worker_id']:
|
||
worker = db.q('SELECT * FROM workers WHERE id=?', (task['worker_id'],), one=True)
|
||
if not worker or worker['status'] != 'enabled':
|
||
_set_task(task_id, status='failed', error='指定 Worker 不存在或已停用',
|
||
finished_at=db.now())
|
||
_log(task_id, 'error', '指定 Worker 不存在或已停用')
|
||
return
|
||
else:
|
||
worker = pick_worker_auto(task)
|
||
if not worker:
|
||
_set_task(task_id, status='failed', error='无可用 Worker(自动路由失败)',
|
||
finished_at=db.now())
|
||
_log(task_id, 'error', '自动路由失败:无可用 Worker')
|
||
return
|
||
_set_task(task_id, worker_id=worker['id'])
|
||
_log(task_id, 'info', f'自动路由 → Worker「{worker["name"]}」({worker["provider"]}/{worker["model"]})')
|
||
|
||
# 预算/成本预检
|
||
ok, reason = check_worker_limits(worker)
|
||
if not ok:
|
||
_set_task(task_id, status='failed', error=reason, finished_at=db.now())
|
||
_log(task_id, 'error', reason)
|
||
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)
|
||
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,
|
||
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}')
|
||
return
|
||
|
||
_cost_record(task, worker, usage)
|
||
_log(task_id, 'success',
|
||
f'执行完成:{usage["total_tokens"]} tokens(输入 {usage["prompt_tokens"]} / 输出 {usage["completion_tokens"]}),'
|
||
f'成本 ¥{usage["cost"]:.6f}')
|
||
|
||
new_status = 'review' if task['review_required'] else 'done'
|
||
fields = {
|
||
'status': new_status,
|
||
'output_text': usage['text'],
|
||
'output_version': task['output_version'] + 1,
|
||
'finished_at': db.now(),
|
||
}
|
||
_set_task(task_id, **fields)
|
||
if new_status == 'review':
|
||
_log(task_id, 'info', '产出已提交,等待人工审核(HITL)')
|
||
else:
|
||
_log(task_id, 'success', '任务完成(无需审核)')
|
||
|
||
|
||
class TaskRunner:
|
||
def __init__(self):
|
||
self._threads = {}
|
||
|
||
def submit(self, task_id):
|
||
if task_id in self._threads and self._threads[task_id].is_alive():
|
||
return False
|
||
t = threading.Thread(target=self._safe_run, args=(task_id,), daemon=True)
|
||
self._threads[task_id] = t
|
||
t.start()
|
||
return True
|
||
|
||
def _safe_run(self, task_id):
|
||
try:
|
||
run_task(task_id)
|
||
except Exception:
|
||
_log(task_id, 'error', '引擎异常: ' + traceback.format_exc())
|
||
try:
|
||
_set_task(task_id, status='failed', error='引擎异常,见日志', finished_at=db.now())
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
runner = TaskRunner()
|