|
@@ -18,20 +18,7 @@ from agents.find_agent.tools import register_all_tools
|
|
|
_PROMPT_PATH = Path(__file__).parent / "prompt" / "system_prompt.md"
|
|
_PROMPT_PATH = Path(__file__).parent / "prompt" / "system_prompt.md"
|
|
|
FIND_AGENT_SYSTEM_PROMPT = _PROMPT_PATH.read_text(encoding="utf-8")
|
|
FIND_AGENT_SYSTEM_PROMPT = _PROMPT_PATH.read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
-_EVIDENCE_TOOLS = {
|
|
|
|
|
- "douyin_detail",
|
|
|
|
|
- "get_content_fans_portrait",
|
|
|
|
|
- "get_account_fans_portrait",
|
|
|
|
|
- "batch_fetch_portraits",
|
|
|
|
|
- "normalize_age_portraits",
|
|
|
|
|
-}
|
|
|
|
|
-_SEARCH_TOOLS = {
|
|
|
|
|
- "douyin_search",
|
|
|
|
|
- "douyin_search_tikhub",
|
|
|
|
|
- "douyin_user_videos",
|
|
|
|
|
-}
|
|
|
|
|
_FAILURE_REPORT_PREFIX = "任务未完成(工具故障)"
|
|
_FAILURE_REPORT_PREFIX = "任务未完成(工具故障)"
|
|
|
-_MIN_PRIMARY_TARGET = 5
|
|
|
|
|
_VIDEO_ID_PATTERN = re.compile(r"(?<!\d)\d{15,22}(?!\d)")
|
|
_VIDEO_ID_PATTERN = re.compile(r"(?<!\d)\d{15,22}(?!\d)")
|
|
|
|
|
|
|
|
|
|
|
|
@@ -52,10 +39,11 @@ def _report_section(
|
|
|
return content[start:end]
|
|
return content[start:end]
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _validate_report_buckets(
|
|
|
|
|
|
|
+def _validate_report_primary_section(
|
|
|
content: str,
|
|
content: str,
|
|
|
state: dict[str, object],
|
|
state: dict[str, object],
|
|
|
) -> str | None:
|
|
) -> str | None:
|
|
|
|
|
+ """Only validate aweme_ids listed in the primary report section."""
|
|
|
candidates = state.get("candidates")
|
|
candidates = state.get("candidates")
|
|
|
if not isinstance(candidates, list):
|
|
if not isinstance(candidates, list):
|
|
|
return "最终状态缺少 candidates,无法校验报告分池"
|
|
return "最终状态缺少 candidates,无法校验报告分池"
|
|
@@ -64,50 +52,30 @@ def _validate_report_buckets(
|
|
|
for item in candidates
|
|
for item in candidates
|
|
|
if isinstance(item, dict) and item.get("aweme_id")
|
|
if isinstance(item, dict) and item.get("aweme_id")
|
|
|
}
|
|
}
|
|
|
- run = state.get("run")
|
|
|
|
|
- run_state = run if isinstance(run, dict) else {}
|
|
|
|
|
|
|
|
|
|
primary_section = _report_section(
|
|
primary_section = _report_section(
|
|
|
content,
|
|
content,
|
|
|
"主推荐",
|
|
"主推荐",
|
|
|
- ("淘汰候选",),
|
|
|
|
|
- )
|
|
|
|
|
- rejected_section = _report_section(
|
|
|
|
|
- content,
|
|
|
|
|
- "淘汰候选",
|
|
|
|
|
- ("搜索树", "缺失数据", "总结"),
|
|
|
|
|
|
|
+ ("淘汰候选", "搜索树", "缺失数据", "总结"),
|
|
|
)
|
|
)
|
|
|
- if primary_section is None or rejected_section is None:
|
|
|
|
|
- return "最终报告必须分别包含“主推荐”和“淘汰候选”段"
|
|
|
|
|
|
|
+ if primary_section is None:
|
|
|
|
|
+ return "最终报告必须包含“主推荐”段"
|
|
|
|
|
|
|
|
primary_ids = set(_VIDEO_ID_PATTERN.findall(primary_section))
|
|
primary_ids = set(_VIDEO_ID_PATTERN.findall(primary_section))
|
|
|
- rejected_ids = set(_VIDEO_ID_PATTERN.findall(rejected_section))
|
|
|
|
|
|
|
+ if not primary_ids:
|
|
|
|
|
+ return "主推荐段必须输出至少一个 aweme_id"
|
|
|
|
|
+
|
|
|
wrong_primary = sorted(
|
|
wrong_primary = sorted(
|
|
|
video_id
|
|
video_id
|
|
|
for video_id in primary_ids
|
|
for video_id in primary_ids
|
|
|
if bucket_by_id.get(video_id) != "primary"
|
|
if bucket_by_id.get(video_id) != "primary"
|
|
|
)
|
|
)
|
|
|
- wrong_rejected = sorted(
|
|
|
|
|
- video_id
|
|
|
|
|
- for video_id in rejected_ids
|
|
|
|
|
- if bucket_by_id.get(video_id) != "rejected"
|
|
|
|
|
- )
|
|
|
|
|
if wrong_primary:
|
|
if wrong_primary:
|
|
|
return (
|
|
return (
|
|
|
"主推荐段包含非 primary 候选: "
|
|
"主推荐段包含非 primary 候选: "
|
|
|
+ ", ".join(wrong_primary[:5])
|
|
+ ", ".join(wrong_primary[:5])
|
|
|
+ "。必须按数据库 decision_bucket 输出"
|
|
+ "。必须按数据库 decision_bucket 输出"
|
|
|
)
|
|
)
|
|
|
- if wrong_rejected:
|
|
|
|
|
- return (
|
|
|
|
|
- "淘汰候选段包含非 rejected 候选: "
|
|
|
|
|
- + ", ".join(wrong_rejected[:5])
|
|
|
|
|
- + "。必须按数据库 decision_bucket 输出"
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- primary_count = int(run_state.get("primary_count") or 0)
|
|
|
|
|
- if primary_count > 0 and not primary_ids:
|
|
|
|
|
- return "数据库存在 primary 候选,但主推荐段没有输出 aweme_id"
|
|
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
@@ -118,84 +86,6 @@ def _primary_count_from_state(state: dict[str, object]) -> int:
|
|
|
return 0
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _validate_report_primary_section(
|
|
|
|
|
- content: str,
|
|
|
|
|
- state: dict[str, object],
|
|
|
|
|
-) -> str | None:
|
|
|
|
|
- candidates = state.get("candidates")
|
|
|
|
|
- if not isinstance(candidates, list):
|
|
|
|
|
- return "最终状态缺少 candidates,无法校验报告分池"
|
|
|
|
|
- bucket_by_id = {
|
|
|
|
|
- str(item.get("aweme_id")): str(item.get("decision_bucket"))
|
|
|
|
|
- for item in candidates
|
|
|
|
|
- if isinstance(item, dict) and item.get("aweme_id")
|
|
|
|
|
- }
|
|
|
|
|
- run = state.get("run")
|
|
|
|
|
- run_state = run if isinstance(run, dict) else {}
|
|
|
|
|
-
|
|
|
|
|
- primary_section = _report_section(
|
|
|
|
|
- content,
|
|
|
|
|
- "主推荐",
|
|
|
|
|
- ("淘汰候选", "搜索树", "缺失数据", "总结"),
|
|
|
|
|
- )
|
|
|
|
|
- if primary_section is None:
|
|
|
|
|
- return "最终报告必须包含“主推荐”段"
|
|
|
|
|
-
|
|
|
|
|
- primary_ids = set(_VIDEO_ID_PATTERN.findall(primary_section))
|
|
|
|
|
- wrong_primary = sorted(
|
|
|
|
|
- video_id
|
|
|
|
|
- for video_id in primary_ids
|
|
|
|
|
- if bucket_by_id.get(video_id) != "primary"
|
|
|
|
|
- )
|
|
|
|
|
- if wrong_primary:
|
|
|
|
|
- return (
|
|
|
|
|
- "主推荐段包含非 primary 候选: "
|
|
|
|
|
- + ", ".join(wrong_primary[:5])
|
|
|
|
|
- + "。必须按数据库 decision_bucket 输出"
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- primary_count = int(run_state.get("primary_count") or 0)
|
|
|
|
|
- if primary_count > 0 and not primary_ids:
|
|
|
|
|
- return "数据库存在 primary 候选,但主推荐段没有输出 aweme_id"
|
|
|
|
|
- return None
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-def _guard_relaxed_primary_target(
|
|
|
|
|
- events: list[tuple[int, str, dict[str, object]]],
|
|
|
|
|
- messages: list[Message],
|
|
|
|
|
-) -> str | None:
|
|
|
|
|
- """Allow finish when enough primary candidates are persisted."""
|
|
|
|
|
- evaluation_events = [
|
|
|
|
|
- (index, payload)
|
|
|
|
|
- for index, name, payload in events
|
|
|
|
|
- if name == "batch_save_video_candidate_evaluations"
|
|
|
|
|
- ]
|
|
|
|
|
- if not evaluation_events:
|
|
|
|
|
- return "尚未保存候选评估"
|
|
|
|
|
- _, evaluation = evaluation_events[-1]
|
|
|
|
|
- if evaluation.get("status") != "finished":
|
|
|
|
|
- return "最后一次候选保存尚未把运行状态设置为 finished"
|
|
|
|
|
-
|
|
|
|
|
- state_events = [
|
|
|
|
|
- (index, payload)
|
|
|
|
|
- for index, name, payload in events
|
|
|
|
|
- if name == "query_video_discovery_state"
|
|
|
|
|
- ]
|
|
|
|
|
- if not state_events:
|
|
|
|
|
- return "尚未查询最终数据库状态"
|
|
|
|
|
- _, state = state_events[-1]
|
|
|
|
|
- if _primary_count_from_state(state) < _MIN_PRIMARY_TARGET:
|
|
|
|
|
- return f"数据库 primary 数量不足 {_MIN_PRIMARY_TARGET} 条"
|
|
|
|
|
-
|
|
|
|
|
- last_assistant = _last_final_assistant_message(messages)
|
|
|
|
|
- if last_assistant is None or not (last_assistant.content or "").strip():
|
|
|
|
|
- return "最终回答为空"
|
|
|
|
|
- report_error = _validate_report_primary_section(last_assistant.content, state)
|
|
|
|
|
- if report_error:
|
|
|
|
|
- return report_error
|
|
|
|
|
- return None
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
def _successful_tool_events(
|
|
def _successful_tool_events(
|
|
|
messages: list[Message],
|
|
messages: list[Message],
|
|
|
) -> list[tuple[int, str, dict[str, object]]]:
|
|
) -> list[tuple[int, str, dict[str, object]]]:
|
|
@@ -225,7 +115,7 @@ def _last_final_assistant_message(messages: list[Message]) -> Message | None:
|
|
|
|
|
|
|
|
|
|
|
|
|
def find_agent_completion_guard(messages: list[Message]) -> str | None:
|
|
def find_agent_completion_guard(messages: list[Message]) -> str | None:
|
|
|
- """Enforce the final search → evidence → evaluation → audit → state → report order."""
|
|
|
|
|
|
|
+ """返回完成提示;默认仅提示、不阻断结束。"""
|
|
|
last_assistant = _last_final_assistant_message(messages)
|
|
last_assistant = _last_final_assistant_message(messages)
|
|
|
content = (last_assistant.content or "").strip() if last_assistant else ""
|
|
content = (last_assistant.content or "").strip() if last_assistant else ""
|
|
|
if content.startswith(_FAILURE_REPORT_PREFIX):
|
|
if content.startswith(_FAILURE_REPORT_PREFIX):
|
|
@@ -235,39 +125,6 @@ def find_agent_completion_guard(messages: list[Message]) -> str | None:
|
|
|
if not any(name == "create_video_discovery_run" for _, name, _ in events):
|
|
if not any(name == "create_video_discovery_run" for _, name, _ in events):
|
|
|
return "尚未成功创建视频发现运行"
|
|
return "尚未成功创建视频发现运行"
|
|
|
|
|
|
|
|
- state_events = [
|
|
|
|
|
- (index, payload)
|
|
|
|
|
- for index, name, payload in events
|
|
|
|
|
- if name == "query_video_discovery_state"
|
|
|
|
|
- ]
|
|
|
|
|
- if state_events:
|
|
|
|
|
- _, latest_state = state_events[-1]
|
|
|
|
|
- if _primary_count_from_state(latest_state) >= _MIN_PRIMARY_TARGET:
|
|
|
|
|
- return _guard_relaxed_primary_target(events, messages)
|
|
|
|
|
-
|
|
|
|
|
- search_indexes = [
|
|
|
|
|
- index
|
|
|
|
|
- for index, name, _ in events
|
|
|
|
|
- if name == "record_video_search_page"
|
|
|
|
|
- ]
|
|
|
|
|
- if not search_indexes:
|
|
|
|
|
- return "尚未持久化任何搜索页"
|
|
|
|
|
- last_search_index = max(search_indexes)
|
|
|
|
|
- raw_search_indexes = [
|
|
|
|
|
- index for index, name, _ in events if name in _SEARCH_TOOLS
|
|
|
|
|
- ]
|
|
|
|
|
- if raw_search_indexes and last_search_index < max(raw_search_indexes):
|
|
|
|
|
- return "最后一次搜索结果尚未通过 record_video_search_page 持久化"
|
|
|
|
|
-
|
|
|
|
|
- evidence_indexes = [
|
|
|
|
|
- index for index, name, _ in events if name in _EVIDENCE_TOOLS
|
|
|
|
|
- ]
|
|
|
|
|
- if not evidence_indexes:
|
|
|
|
|
- return "尚未获取并整理候选证据"
|
|
|
|
|
- last_evidence_index = max(evidence_indexes)
|
|
|
|
|
- if last_evidence_index < last_search_index:
|
|
|
|
|
- return "最后一次搜索后尚未重新获取并整理候选证据"
|
|
|
|
|
-
|
|
|
|
|
evaluation_events = [
|
|
evaluation_events = [
|
|
|
(index, payload)
|
|
(index, payload)
|
|
|
for index, name, payload in events
|
|
for index, name, payload in events
|
|
@@ -276,28 +133,9 @@ def find_agent_completion_guard(messages: list[Message]) -> str | None:
|
|
|
if not evaluation_events:
|
|
if not evaluation_events:
|
|
|
return "尚未保存候选评估"
|
|
return "尚未保存候选评估"
|
|
|
evaluation_index, evaluation = evaluation_events[-1]
|
|
evaluation_index, evaluation = evaluation_events[-1]
|
|
|
- if evaluation_index < last_evidence_index:
|
|
|
|
|
- return "最后一次证据获取发生在候选评估之后,证据尚未重新评估并保存"
|
|
|
|
|
if evaluation.get("status") != "finished":
|
|
if evaluation.get("status") != "finished":
|
|
|
return "最后一次候选保存尚未把运行状态设置为 finished"
|
|
return "最后一次候选保存尚未把运行状态设置为 finished"
|
|
|
|
|
|
|
|
- audit_events = [
|
|
|
|
|
- (index, payload)
|
|
|
|
|
- for index, name, payload in events
|
|
|
|
|
- if name == "audit_video_discovery_run"
|
|
|
|
|
- ]
|
|
|
|
|
- if not audit_events:
|
|
|
|
|
- return "尚未调用 audit_video_discovery_run 执行数据库完成审计"
|
|
|
|
|
- audit_index, audit = audit_events[-1]
|
|
|
|
|
- if audit_index < evaluation_index:
|
|
|
|
|
- return "候选评估晚于最后一次审计,请重新调用 audit_video_discovery_run"
|
|
|
|
|
- if audit.get("can_finish") is not True:
|
|
|
|
|
- violations = audit.get("critical_violations")
|
|
|
|
|
- if isinstance(violations, list) and violations:
|
|
|
|
|
- summary = ";".join(str(item) for item in violations[:5])
|
|
|
|
|
- return f"最后一次审计未通过:{summary}"
|
|
|
|
|
- return "最后一次审计未通过"
|
|
|
|
|
-
|
|
|
|
|
state_events = [
|
|
state_events = [
|
|
|
(index, payload)
|
|
(index, payload)
|
|
|
for index, name, payload in events
|
|
for index, name, payload in events
|
|
@@ -306,15 +144,19 @@ def find_agent_completion_guard(messages: list[Message]) -> str | None:
|
|
|
if not state_events:
|
|
if not state_events:
|
|
|
return "尚未查询最终数据库状态"
|
|
return "尚未查询最终数据库状态"
|
|
|
state_index, state = state_events[-1]
|
|
state_index, state = state_events[-1]
|
|
|
- if state_index < audit_index:
|
|
|
|
|
- return "最终状态查询必须在审计通过之后执行"
|
|
|
|
|
|
|
+ if state_index < evaluation_index:
|
|
|
|
|
+ return "最终状态查询必须在候选保存为 finished 之后执行"
|
|
|
|
|
|
|
|
- last_assistant = _last_final_assistant_message(messages)
|
|
|
|
|
- if last_assistant is None or not (last_assistant.content or "").strip():
|
|
|
|
|
|
|
+ run = state.get("run")
|
|
|
|
|
+ run_state = run if isinstance(run, dict) else {}
|
|
|
|
|
+ if run_state.get("status") != "finished":
|
|
|
|
|
+ return "运行状态尚未持久化为 finished"
|
|
|
|
|
+
|
|
|
|
|
+ if last_assistant is None or not content:
|
|
|
return "最终回答为空"
|
|
return "最终回答为空"
|
|
|
- report_error = _validate_report_buckets(last_assistant.content, state)
|
|
|
|
|
- if report_error:
|
|
|
|
|
- return report_error
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if _primary_count_from_state(state) > 0:
|
|
|
|
|
+ return _validate_report_primary_section(content, state)
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
|
|
|