|
|
@@ -1,7 +1,6 @@
|
|
|
-"""统筹落库后执行分级任务,并对任务状态与树上需求等级做补偿闭环。"""
|
|
|
+"""统筹落库后执行分级任务。"""
|
|
|
from __future__ import annotations
|
|
|
|
|
|
-import json
|
|
|
import logging
|
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
from datetime import datetime
|
|
|
@@ -9,32 +8,20 @@ from typing import Any
|
|
|
from zoneinfo import ZoneInfo
|
|
|
|
|
|
from agents.demand_grade_agent.run import main as grade_demand_words
|
|
|
-from agents.demand_grade_agent.tools.build_grade_plan_context import (
|
|
|
- build_grade_plan_context,
|
|
|
- build_supplement_grade_context,
|
|
|
-)
|
|
|
from agents.demand_grade_orchestrator_agent.run import orchestrate_daily_grade_plan
|
|
|
-from agents.demand_grade_orchestrator_agent.validation import resolve_assignment_state
|
|
|
from supply_infra.config import get_infra_settings
|
|
|
-from supply_infra.db.repositories.demand_belong_category_repo import (
|
|
|
- DemandBelongCategoryRepository,
|
|
|
-)
|
|
|
-from supply_infra.db.repositories.demand_belong_pool_rel_repo import (
|
|
|
- DemandBelongPoolRelRepository,
|
|
|
-)
|
|
|
from supply_infra.db.repositories.demand_grade_plan_repo import DemandGradePlanRepository
|
|
|
from supply_infra.db.repositories.demand_grade_repo import DemandGradeRepository
|
|
|
-from supply_infra.db.repositories.global_tree_category_repo import GlobalTreeCategoryRepository
|
|
|
from supply_infra.db.repositories.multi_demand_pool_di_repo import MultiDemandPoolDiRepository
|
|
|
from supply_infra.db.session import get_session
|
|
|
+from supply_infra.scheduler.plan_group_batch import (
|
|
|
+ MAX_DEMANDS_PER_BATCH,
|
|
|
+ list_pending_demands_by_category_ids,
|
|
|
+)
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
-_DEFAULT_BATCH_SIZE = 20
|
|
|
_DEFAULT_WORKERS = 5
|
|
|
-_MAX_PLAN_EXECUTION_ROUNDS = 5
|
|
|
-_MAX_SUPPLEMENT_ROUNDS = 5
|
|
|
-_SUPPLEMENT_BATCH_SIZE = 30
|
|
|
|
|
|
|
|
|
def _resolve_biz_dt(biz_dt: str | None) -> str:
|
|
|
@@ -48,8 +35,8 @@ def _graded_names(biz_dt: str) -> set[str]:
|
|
|
return DemandGradeRepository(session).get_existing_demand_names(biz_dt)
|
|
|
|
|
|
|
|
|
-def _run_group(biz_dt: str, batch_size: int, group_id: int) -> int:
|
|
|
- """领取并执行一个固定任务;失败留到下一轮,不在当前轮内再次领取。"""
|
|
|
+def _run_group(biz_dt: str, group_id: int, *, max_demands_per_batch: int) -> int:
|
|
|
+ """领取并执行一个固定任务,组内按批处理,失败子批跳过并继续。"""
|
|
|
with get_session() as session:
|
|
|
group = DemandGradePlanRepository(session).claim_group(biz_dt, group_id)
|
|
|
if group is None:
|
|
|
@@ -57,44 +44,42 @@ def _run_group(biz_dt: str, batch_size: int, group_id: int) -> int:
|
|
|
return 0
|
|
|
|
|
|
processed_batches = 0
|
|
|
+ batch_errors: list[str] = []
|
|
|
+ skipped_names: set[str] = set()
|
|
|
try:
|
|
|
while True:
|
|
|
- graded_before = _graded_names(biz_dt)
|
|
|
- raw_context = build_grade_plan_context(
|
|
|
- biz_dt=biz_dt,
|
|
|
- category_ids=group["category_ids"],
|
|
|
- planning_reason=group["planning_reason"],
|
|
|
- shared_traits=group["shared_traits"],
|
|
|
- max_demands=batch_size,
|
|
|
+ graded_before = _graded_names(biz_dt) | skipped_names
|
|
|
+ demands = list_pending_demands_by_category_ids(
|
|
|
+ biz_dt,
|
|
|
+ group["category_ids"],
|
|
|
+ max_demands=max_demands_per_batch,
|
|
|
excluded_demand_names=sorted(graded_before),
|
|
|
)
|
|
|
- try:
|
|
|
- context = json.loads(raw_context)
|
|
|
- except json.JSONDecodeError as exc:
|
|
|
- if raw_context == "所选树节点下没有待分级需求":
|
|
|
- break
|
|
|
- raise RuntimeError(f"构建分级上下文失败: {raw_context}") from exc
|
|
|
-
|
|
|
- names = [
|
|
|
- str(item["demand_name"])
|
|
|
- for item in context.get("items", [])
|
|
|
- if item.get("demand_name")
|
|
|
- ]
|
|
|
- if not names:
|
|
|
+ if not demands:
|
|
|
break
|
|
|
|
|
|
- grade_demand_words(names, biz_dt=biz_dt, tree_context=context)
|
|
|
- graded_after = _graded_names(biz_dt)
|
|
|
- not_saved = sorted(set(names) - graded_after)
|
|
|
- if not_saved:
|
|
|
- raise RuntimeError(f"分级 Agent 未落库本批全部需求: {not_saved}")
|
|
|
- processed_batches += 1
|
|
|
+ try:
|
|
|
+ grade_demand_words(demands, biz_dt=biz_dt)
|
|
|
+ processed_batches += 1
|
|
|
+ except Exception as exc:
|
|
|
+ logger.exception(
|
|
|
+ "分级子批次失败,跳过并继续本组其余需求: biz_dt=%s group=%s count=%s",
|
|
|
+ biz_dt,
|
|
|
+ group["group_key"],
|
|
|
+ len(demands),
|
|
|
+ )
|
|
|
+ batch_errors.append(str(exc))
|
|
|
+ skipped_names.update(item["demand_name"] for item in demands)
|
|
|
|
|
|
with get_session() as session:
|
|
|
- DemandGradePlanRepository(session).finish_group(group["id"], success=True)
|
|
|
+ DemandGradePlanRepository(session).finish_group(
|
|
|
+ group["id"],
|
|
|
+ success=processed_batches > 0,
|
|
|
+ error_message="; ".join(batch_errors) if batch_errors else None,
|
|
|
+ )
|
|
|
except Exception as exc:
|
|
|
logger.exception(
|
|
|
- "分级计划任务失败,留待下一轮重试: biz_dt=%s group=%s",
|
|
|
+ "分级计划任务失败: biz_dt=%s group=%s",
|
|
|
biz_dt,
|
|
|
group["group_key"],
|
|
|
)
|
|
|
@@ -111,340 +96,147 @@ def _run_group(biz_dt: str, batch_size: int, group_id: int) -> int:
|
|
|
biz_dt,
|
|
|
group["group_key"],
|
|
|
)
|
|
|
+ return 0
|
|
|
return processed_batches
|
|
|
|
|
|
|
|
|
-def _execute_group_phase(
|
|
|
+def _execute_plan_tasks(
|
|
|
biz_dt: str,
|
|
|
*,
|
|
|
- status: str,
|
|
|
- batch_size: int,
|
|
|
workers: int,
|
|
|
-) -> dict[str, int]:
|
|
|
- """执行一个状态阶段;调用方先执行 failed,再执行 pending。"""
|
|
|
+ max_demands_per_batch: int,
|
|
|
+) -> dict[str, Any]:
|
|
|
+ """并发执行当天全部 pending 计划任务,仅执行一轮。"""
|
|
|
with get_session() as session:
|
|
|
- group_ids = DemandGradePlanRepository(session).list_group_ids_by_status(biz_dt, status)
|
|
|
+ group_ids = DemandGradePlanRepository(session).list_pending_group_ids(biz_dt)
|
|
|
if not group_ids:
|
|
|
- return {"attempted_groups": 0, "batches_run": 0, "workers": 0}
|
|
|
+ with get_session() as session:
|
|
|
+ final_snapshot = DemandGradePlanRepository(session).get_execution_snapshot(biz_dt)
|
|
|
+ return {
|
|
|
+ "attempted_groups": 0,
|
|
|
+ "groups_run": 0,
|
|
|
+ "workers": 0,
|
|
|
+ "final_snapshot": final_snapshot,
|
|
|
+ "execution_complete": final_snapshot["execution_complete"],
|
|
|
+ }
|
|
|
|
|
|
worker_count = max(1, min(int(workers), len(group_ids)))
|
|
|
- batches_run = 0
|
|
|
+ groups_run = 0
|
|
|
with ThreadPoolExecutor(max_workers=worker_count) as executor:
|
|
|
futures = [
|
|
|
- executor.submit(_run_group, biz_dt, batch_size, group_id)
|
|
|
+ executor.submit(_run_group, biz_dt, group_id, max_demands_per_batch=max_demands_per_batch)
|
|
|
for group_id in group_ids
|
|
|
]
|
|
|
for future in as_completed(futures):
|
|
|
try:
|
|
|
- batches_run += future.result()
|
|
|
+ groups_run += future.result()
|
|
|
except Exception:
|
|
|
- # _run_group 已自行兜底;这里再兜一次,禁止 worker 异常中断定时任务。
|
|
|
- logger.exception("分级任务 worker 出现未捕获错误: biz_dt=%s status=%s", biz_dt, status)
|
|
|
+ logger.exception("分级任务 worker 出现未捕获错误: biz_dt=%s", biz_dt)
|
|
|
+
|
|
|
+ with get_session() as session:
|
|
|
+ final_snapshot = DemandGradePlanRepository(session).get_execution_snapshot(biz_dt)
|
|
|
return {
|
|
|
"attempted_groups": len(group_ids),
|
|
|
- "batches_run": batches_run,
|
|
|
+ "groups_run": groups_run,
|
|
|
"workers": worker_count,
|
|
|
+ "final_snapshot": final_snapshot,
|
|
|
+ "execution_complete": final_snapshot["execution_complete"],
|
|
|
}
|
|
|
|
|
|
|
|
|
-def _execute_plan_tasks_with_retries(
|
|
|
+def execute_plan_tasks_until_complete(
|
|
|
biz_dt: str,
|
|
|
*,
|
|
|
- batch_size: int,
|
|
|
workers: int,
|
|
|
- max_rounds: int = _MAX_PLAN_EXECUTION_ROUNDS,
|
|
|
+ max_demands_per_batch: int = MAX_DEMANDS_PER_BATCH,
|
|
|
+ max_rounds: int = 0,
|
|
|
) -> dict[str, Any]:
|
|
|
- """检查并执行计划任务,失败优先,任务状态检查最多循环五轮。"""
|
|
|
- max_rounds = max(1, min(int(max_rounds), _MAX_PLAN_EXECUTION_ROUNDS))
|
|
|
- history: list[dict[str, Any]] = []
|
|
|
- total_batches = 0
|
|
|
- max_workers_used = 0
|
|
|
-
|
|
|
- for round_no in range(1, max_rounds + 1):
|
|
|
- with get_session() as session:
|
|
|
- before = DemandGradePlanRepository(session).get_execution_snapshot(biz_dt)
|
|
|
- if before["execution_complete"]:
|
|
|
- logger.info("全部分级计划任务已执行完成,跳过分级 worker: biz_dt=%s", biz_dt)
|
|
|
+ """循环执行 pending 计划任务,直到全部完成或达到 max_rounds。"""
|
|
|
+ rounds: list[dict[str, Any]] = []
|
|
|
+ groups_run = 0
|
|
|
+ round_no = 0
|
|
|
+ while True:
|
|
|
+ if max_rounds > 0 and round_no >= max_rounds:
|
|
|
break
|
|
|
-
|
|
|
- failed_result = _execute_group_phase(
|
|
|
- biz_dt,
|
|
|
- status="failed",
|
|
|
- batch_size=batch_size,
|
|
|
- workers=workers,
|
|
|
- )
|
|
|
- # 严格等失败阶段结束后再处理从未执行的任务。
|
|
|
- pending_result = _execute_group_phase(
|
|
|
+ round_no += 1
|
|
|
+ result = _execute_plan_tasks(
|
|
|
biz_dt,
|
|
|
- status="pending",
|
|
|
- batch_size=batch_size,
|
|
|
workers=workers,
|
|
|
+ max_demands_per_batch=max_demands_per_batch,
|
|
|
)
|
|
|
- total_batches += failed_result["batches_run"] + pending_result["batches_run"]
|
|
|
- max_workers_used = max(
|
|
|
- max_workers_used,
|
|
|
- failed_result["workers"],
|
|
|
- pending_result["workers"],
|
|
|
- )
|
|
|
+ rounds.append(result)
|
|
|
+ groups_run += int(result.get("groups_run") or 0)
|
|
|
+ if int(result.get("attempted_groups") or 0) == 0:
|
|
|
+ break
|
|
|
|
|
|
+ final_snapshot = rounds[-1]["final_snapshot"] if rounds else None
|
|
|
+ if final_snapshot is None:
|
|
|
with get_session() as session:
|
|
|
- after = DemandGradePlanRepository(session).get_execution_snapshot(biz_dt)
|
|
|
- history.append({
|
|
|
- "round": round_no,
|
|
|
- "before": before["group_status"],
|
|
|
- "failed_attempted": failed_result["attempted_groups"],
|
|
|
- "pending_attempted": pending_result["attempted_groups"],
|
|
|
- "after": after["group_status"],
|
|
|
- "execution_complete": after["execution_complete"],
|
|
|
- })
|
|
|
- if after["execution_complete"]:
|
|
|
- break
|
|
|
- if failed_result["attempted_groups"] + pending_result["attempted_groups"] == 0:
|
|
|
- logger.error(
|
|
|
- "计划任务尚未全部完成,但当前没有可执行的 failed/pending 任务: "
|
|
|
- "biz_dt=%s round=%s status=%s",
|
|
|
- biz_dt,
|
|
|
- round_no,
|
|
|
- after["group_status"],
|
|
|
- )
|
|
|
+ final_snapshot = DemandGradePlanRepository(session).get_execution_snapshot(biz_dt)
|
|
|
|
|
|
- with get_session() as session:
|
|
|
- final_snapshot = DemandGradePlanRepository(session).get_execution_snapshot(biz_dt)
|
|
|
- if not final_snapshot["execution_complete"]:
|
|
|
- logger.error(
|
|
|
- "计划任务最多执行 %s 轮后仍未全部完成,继续执行需求覆盖检查: biz_dt=%s status=%s",
|
|
|
- max_rounds,
|
|
|
- biz_dt,
|
|
|
- final_snapshot["group_status"],
|
|
|
- )
|
|
|
return {
|
|
|
- "rounds": len(history),
|
|
|
- "history": history,
|
|
|
- "batches_run": total_batches,
|
|
|
- "workers": max_workers_used,
|
|
|
+ "rounds": round_no,
|
|
|
+ "groups_run": groups_run,
|
|
|
+ "workers": max(1, int(workers)),
|
|
|
+ "round_details": rounds,
|
|
|
"final_snapshot": final_snapshot,
|
|
|
- "execution_complete": final_snapshot["execution_complete"],
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-def _tree_attached_demand_names(biz_dt: str) -> set[str]:
|
|
|
- """返回业务日内通过有效挂载关系连接到当前全局树的需求池需求名。"""
|
|
|
- with get_session() as session:
|
|
|
- active_category_ids = {
|
|
|
- int(row.id)
|
|
|
- for row in GlobalTreeCategoryRepository(session).list_active_categories()
|
|
|
- }
|
|
|
- belongs = [
|
|
|
- row
|
|
|
- for row in DemandBelongCategoryRepository(session).list_active()
|
|
|
- if row.category_id is not None and int(row.category_id) in active_category_ids
|
|
|
- ]
|
|
|
- pool_ids_by_belong = DemandBelongPoolRelRepository(session).get_pool_ids_by_belong_ids(
|
|
|
- [int(row.id) for row in belongs]
|
|
|
- )
|
|
|
- pool_ids = {
|
|
|
- int(pool_id)
|
|
|
- for values in pool_ids_by_belong.values()
|
|
|
- for pool_id in values
|
|
|
- }
|
|
|
- pool_rows = MultiDemandPoolDiRepository(session).get_by_ids(sorted(pool_ids))
|
|
|
- return {
|
|
|
- str(row.demand_name)
|
|
|
- for row in pool_rows
|
|
|
- if row.biz_dt == biz_dt and row.demand_name
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-def _resolve_tree_grade_coverage(biz_dt: str) -> dict[str, Any]:
|
|
|
- tree_demand_names = _tree_attached_demand_names(biz_dt)
|
|
|
- graded = _graded_names(biz_dt)
|
|
|
- missing = sorted(tree_demand_names - graded)
|
|
|
- return {
|
|
|
- "biz_dt": biz_dt,
|
|
|
- "tree_demand_count": len(tree_demand_names),
|
|
|
- "graded_tree_demand_count": len(tree_demand_names & graded),
|
|
|
- "missing_count": len(missing),
|
|
|
- "missing_demand_names": missing,
|
|
|
- "coverage_complete": not missing,
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-def _coverage_summary(coverage: dict[str, Any]) -> dict[str, Any]:
|
|
|
- """压缩返回与日志内容,避免大量缺失需求撑大定时任务结果。"""
|
|
|
- return {
|
|
|
- "biz_dt": coverage["biz_dt"],
|
|
|
- "tree_demand_count": coverage["tree_demand_count"],
|
|
|
- "graded_tree_demand_count": coverage["graded_tree_demand_count"],
|
|
|
- "missing_count": coverage["missing_count"],
|
|
|
- "missing_demand_sample": coverage["missing_demand_names"][:30],
|
|
|
- "coverage_complete": coverage["coverage_complete"],
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-def _chunks(values: list[str], size: int) -> list[list[str]]:
|
|
|
- return [values[start : start + size] for start in range(0, len(values), size)]
|
|
|
-
|
|
|
-
|
|
|
-def _supplement_missing_grades(
|
|
|
- biz_dt: str,
|
|
|
- *,
|
|
|
- max_rounds: int = _MAX_SUPPLEMENT_ROUNDS,
|
|
|
- batch_size: int = _SUPPLEMENT_BATCH_SIZE,
|
|
|
-) -> dict[str, Any]:
|
|
|
- """补充分级树上缺失需求,每批最多30个,检查与补偿最多循环五轮。"""
|
|
|
- max_rounds = max(1, min(int(max_rounds), _MAX_SUPPLEMENT_ROUNDS))
|
|
|
- batch_size = max(1, min(int(batch_size), _SUPPLEMENT_BATCH_SIZE))
|
|
|
- history: list[dict[str, Any]] = []
|
|
|
- batches_run = 0
|
|
|
- initial_coverage = _resolve_tree_grade_coverage(biz_dt)
|
|
|
-
|
|
|
- for round_no in range(1, max_rounds + 1):
|
|
|
- before = _resolve_tree_grade_coverage(biz_dt)
|
|
|
- if before["coverage_complete"]:
|
|
|
- logger.info("树上全部需求均已分级,跳过补充任务: biz_dt=%s", biz_dt)
|
|
|
- break
|
|
|
-
|
|
|
- missing_names = list(before["missing_demand_names"])
|
|
|
- attempted_batches = 0
|
|
|
- for batch_no, demand_batch in enumerate(_chunks(missing_names, batch_size), start=1):
|
|
|
- attempted_batches += 1
|
|
|
- try:
|
|
|
- context = build_supplement_grade_context(biz_dt, demand_batch)
|
|
|
- context_names = [
|
|
|
- str(item["demand_name"])
|
|
|
- for item in context.get("items", [])
|
|
|
- if item.get("demand_name")
|
|
|
- ]
|
|
|
- if not context_names:
|
|
|
- logger.error(
|
|
|
- "补充分级上下文没有有效需求: biz_dt=%s round=%s batch=%s input=%s",
|
|
|
- biz_dt,
|
|
|
- round_no,
|
|
|
- batch_no,
|
|
|
- demand_batch,
|
|
|
- )
|
|
|
- continue
|
|
|
- grade_demand_words(context_names, biz_dt=biz_dt, tree_context=context)
|
|
|
- batches_run += 1
|
|
|
- except Exception:
|
|
|
- logger.exception(
|
|
|
- "补充分级批次失败,继续处理其他批次: biz_dt=%s round=%s batch=%s names=%s",
|
|
|
- biz_dt,
|
|
|
- round_no,
|
|
|
- batch_no,
|
|
|
- demand_batch,
|
|
|
- )
|
|
|
-
|
|
|
- after = _resolve_tree_grade_coverage(biz_dt)
|
|
|
- history.append({
|
|
|
- "round": round_no,
|
|
|
- "missing_before": before["missing_count"],
|
|
|
- "attempted_batches": attempted_batches,
|
|
|
- "missing_after": after["missing_count"],
|
|
|
- "coverage_complete": after["coverage_complete"],
|
|
|
- })
|
|
|
- if after["coverage_complete"]:
|
|
|
- break
|
|
|
- if after["missing_count"] >= before["missing_count"]:
|
|
|
- logger.error(
|
|
|
- "补充分级本轮没有减少缺失需求,将进入下一轮重试: "
|
|
|
- "biz_dt=%s round=%s missing=%s",
|
|
|
- biz_dt,
|
|
|
- round_no,
|
|
|
- after["missing_count"],
|
|
|
- )
|
|
|
-
|
|
|
- final_coverage = _resolve_tree_grade_coverage(biz_dt)
|
|
|
- if not final_coverage["coverage_complete"]:
|
|
|
- logger.error(
|
|
|
- "补充分级最多执行 %s 轮后仍有树上需求未分级: biz_dt=%s missing_count=%s sample=%s",
|
|
|
- max_rounds,
|
|
|
- biz_dt,
|
|
|
- final_coverage["missing_count"],
|
|
|
- final_coverage["missing_demand_names"][:30],
|
|
|
- )
|
|
|
- return {
|
|
|
- "rounds": len(history),
|
|
|
- "history": history,
|
|
|
- "batches_run": batches_run,
|
|
|
- "initial_coverage": _coverage_summary(initial_coverage),
|
|
|
- "final_coverage": _coverage_summary(final_coverage),
|
|
|
- "coverage_complete": final_coverage["coverage_complete"],
|
|
|
+ "execution_complete": bool(final_snapshot["execution_complete"]),
|
|
|
}
|
|
|
|
|
|
|
|
|
def _grade_demand_pool_impl(
|
|
|
resolved_biz_dt: str,
|
|
|
*,
|
|
|
- batch_size: int,
|
|
|
workers: int,
|
|
|
+ max_demands_per_batch: int = MAX_DEMANDS_PER_BATCH,
|
|
|
+ with_orchestrate: bool = True,
|
|
|
+ max_rounds: int = 0,
|
|
|
) -> dict[str, Any]:
|
|
|
with get_session() as session:
|
|
|
total = MultiDemandPoolDiRepository(session).count_distinct_demand_names(resolved_biz_dt)
|
|
|
graded_before = DemandGradeRepository(session).count_by_biz_dt(resolved_biz_dt)
|
|
|
|
|
|
- try:
|
|
|
- assignment_before = resolve_assignment_state(resolved_biz_dt)
|
|
|
- except Exception as exc:
|
|
|
- logger.exception("查询统筹分配前状态失败,继续尝试统筹: biz_dt=%s", resolved_biz_dt)
|
|
|
- assignment_before = {"biz_dt": resolved_biz_dt, "error": str(exc)}
|
|
|
-
|
|
|
- try:
|
|
|
- orchestrate_daily_grade_plan(biz_dt=resolved_biz_dt)
|
|
|
- except Exception:
|
|
|
- logger.exception("统筹 Agent 执行失败,继续处理数据库中已有任务: biz_dt=%s", resolved_biz_dt)
|
|
|
-
|
|
|
- try:
|
|
|
- assignment_after = resolve_assignment_state(resolved_biz_dt)
|
|
|
- except Exception as exc:
|
|
|
- logger.exception("查询统筹分配后状态失败,继续执行已有任务: biz_dt=%s", resolved_biz_dt)
|
|
|
- assignment_after = {"biz_dt": resolved_biz_dt, "error": str(exc)}
|
|
|
-
|
|
|
- if assignment_after.get("assignment_complete") is False:
|
|
|
- logger.error(
|
|
|
- "统筹规划未覆盖当天全部有需求的树节点,将按部分计划继续执行: biz_dt=%s uncovered=%s",
|
|
|
- resolved_biz_dt,
|
|
|
- assignment_after.get("unassigned_category_ids"),
|
|
|
- )
|
|
|
+ if with_orchestrate:
|
|
|
+ try:
|
|
|
+ orchestrate_daily_grade_plan(biz_dt=resolved_biz_dt)
|
|
|
+ except Exception:
|
|
|
+ logger.exception("统筹 Agent 执行失败,继续处理数据库中已有任务: biz_dt=%s", resolved_biz_dt)
|
|
|
|
|
|
- plan_execution = _execute_plan_tasks_with_retries(
|
|
|
+ plan_execution = execute_plan_tasks_until_complete(
|
|
|
resolved_biz_dt,
|
|
|
- batch_size=max(1, int(batch_size)),
|
|
|
workers=max(1, int(workers)),
|
|
|
+ max_demands_per_batch=max(1, min(int(max_demands_per_batch), MAX_DEMANDS_PER_BATCH)),
|
|
|
+ max_rounds=max_rounds,
|
|
|
)
|
|
|
- # 只在统筹结束并完成计划任务状态检查后,验证树上需求等级覆盖并补偿。
|
|
|
- supplement = _supplement_missing_grades(resolved_biz_dt)
|
|
|
|
|
|
with get_session() as session:
|
|
|
graded_after = DemandGradeRepository(session).count_by_biz_dt(resolved_biz_dt)
|
|
|
final_snapshot = plan_execution["final_snapshot"]
|
|
|
result = {
|
|
|
- "success": bool(plan_execution["execution_complete"] and supplement["coverage_complete"]),
|
|
|
+ "success": bool(plan_execution["execution_complete"]),
|
|
|
"biz_dt": resolved_biz_dt,
|
|
|
"total": total,
|
|
|
"graded_before": graded_before,
|
|
|
"graded_after": graded_after,
|
|
|
- "assignment_before": assignment_before,
|
|
|
- "assignment_after": assignment_after,
|
|
|
"planned_category_count": len(final_snapshot["assigned_category_ids"]),
|
|
|
"planned_groups": final_snapshot["planned_groups"],
|
|
|
"group_status": final_snapshot["group_status"],
|
|
|
"plan_execution": plan_execution,
|
|
|
- "supplement": supplement,
|
|
|
"workers": plan_execution["workers"],
|
|
|
- "batches_run": plan_execution["batches_run"],
|
|
|
- "supplement_batches_run": supplement["batches_run"],
|
|
|
+ "groups_run": plan_execution["groups_run"],
|
|
|
"run_at": datetime.now().isoformat(),
|
|
|
}
|
|
|
- logger.info("Tree-first grade completed: %s", result)
|
|
|
+ logger.info("Grade demand pool completed: %s", result)
|
|
|
return result
|
|
|
|
|
|
|
|
|
def grade_demand_pool(
|
|
|
biz_dt: str | None = None,
|
|
|
*,
|
|
|
- batch_size: int = _DEFAULT_BATCH_SIZE,
|
|
|
workers: int = _DEFAULT_WORKERS,
|
|
|
+ max_demands_per_batch: int = MAX_DEMANDS_PER_BATCH,
|
|
|
+ with_orchestrate: bool = True,
|
|
|
+ max_rounds: int = 0,
|
|
|
) -> dict[str, Any]:
|
|
|
"""执行完整分级闭环;任何错误只记录日志并返回,不向上中断定时任务。"""
|
|
|
resolved_biz_dt = str(biz_dt or "")
|
|
|
@@ -452,8 +244,10 @@ def grade_demand_pool(
|
|
|
resolved_biz_dt = _resolve_biz_dt(biz_dt)
|
|
|
return _grade_demand_pool_impl(
|
|
|
resolved_biz_dt,
|
|
|
- batch_size=batch_size,
|
|
|
workers=workers,
|
|
|
+ max_demands_per_batch=max_demands_per_batch,
|
|
|
+ with_orchestrate=with_orchestrate,
|
|
|
+ max_rounds=max_rounds,
|
|
|
)
|
|
|
except Exception as exc:
|
|
|
logger.exception("需求分级任务发生未捕获错误,已阻止异常中断定时任务: biz_dt=%s", resolved_biz_dt)
|