| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- from __future__ import annotations
- import json
- import re
- from typing import Any
- from .runtime_tool_catalog import (
- MAIN_DECISION_READ_TOOLS,
- RETRIEVAL_AGENT_LABELS,
- business_tool_label,
- )
- QUERY_TOOL_LABELS = {
- "search_script_decode_case": "解构 Case 查询",
- "external_search_case": "外部内容查询",
- "search_knowledge": "创作知识查询",
- }
- QUERY_TOOL_NAMES = frozenset(QUERY_TOOL_LABELS)
- RETRIEVAL_TOOL_NAMES = frozenset({*QUERY_TOOL_NAMES, *MAIN_DECISION_READ_TOOLS})
- RETRIEVAL_RESULT_COLLECTION_KEYS: dict[str, str | None] = {
- "search_script_decode_case": "results",
- "external_search_case": "data",
- "search_knowledge": None,
- "get_domain_info": "domain_info",
- }
- TERMINAL_RUN_STATUSES = frozenset(
- {"success", "partial", "failed", "stopped", "completed", "cancelled", "cancel", "cancle"}
- )
- _CONDITION_LABELS = {
- "keyword": "关键词",
- "account_name": "账号",
- "match_fields": "匹配范围",
- "top_k": "最多返回",
- "return_field": "返回内容",
- "platform_channel": "平台",
- "max_count": "最多返回",
- "message": "查询需求",
- "script_build_id": "构建记录",
- }
- _HIDDEN_CONDITIONS = {"execution_id", "account_name_guard"}
- def is_retrieval_detail_event(event: dict[str, Any]) -> bool:
- return (
- str(event.get("event_type") or "") == "tool_call"
- and (
- str(event.get("event_name") or "") in RETRIEVAL_TOOL_NAMES
- or str(event.get("agent_role") or "") in RETRIEVAL_AGENT_LABELS
- )
- )
- def summarize_retrieval_output(
- event_name: str,
- *,
- raw_status: Any,
- ended_at: Any,
- output: Any,
- run_status: Any = None,
- ) -> dict[str, Any]:
- """Return only the bounded status/count needed by the execution view."""
- parsed = _parse_jsonish(output)
- state, count, error = _outcome(
- event_name,
- raw_status=raw_status,
- ended_at=ended_at,
- output=parsed,
- run_status=run_status,
- )
- return {
- "state": state,
- "count": count,
- "error": error,
- }
- def project_retrieval_event(
- event: dict[str, Any], *, run_status: Any = None
- ) -> dict[str, Any]:
- name = str(event.get("event_name") or "")
- source_kind = "direct-tool" if name in MAIN_DECISION_READ_TOOLS else "agent-query"
- input_value = _event_side(event, "input")
- output_value = _event_side(event, "output")
- output_side = event.get("output")
- output_truncated = isinstance(output_side, dict) and bool(output_side.get("truncated"))
- summary_output = (
- _parse_jsonish(event.get("output_preview"))
- if output_truncated
- else output_value
- )
- summary = summarize_retrieval_output(
- name,
- raw_status=event.get("status"),
- ended_at=event.get("ended_at"),
- output=summary_output,
- run_status=run_status,
- )
- state = summary["state"]
- count = summary.get("count")
- error = summary.get("error")
- items = _result_items(name, output_value) if state == "hit" and not output_truncated else []
- completeness = _completeness(event.get("output"), output_value)
- return {
- "detailKind": "retrieval-event",
- "eventId": _integer(event.get("id")) or 0,
- "sourceKind": source_kind,
- "toolName": name,
- "businessLabel": QUERY_TOOL_LABELS.get(name)
- or business_tool_label(name),
- "queryConditions": _query_conditions(input_value),
- "outcome": {
- "state": state,
- **({"count": count} if count is not None else {}),
- "message": _outcome_message(state, count, error, source_kind),
- **({"errorKind": _error_kind(error)} if error else {}),
- },
- "items": items,
- "completeness": completeness,
- }
- def _outcome(
- name: str,
- *,
- raw_status: Any,
- ended_at: Any,
- output: Any,
- run_status: Any,
- ) -> tuple[str, int | None, str | None]:
- status = str(raw_status or "").lower()
- terminal_run = str(run_status or "").lower() in TERMINAL_RUN_STATUSES
- if terminal_run and (status == "running" or not ended_at):
- return "interrupted", None, None
- error = _error_text(output)
- if status in {"error", "failed", "failure"} or error:
- return "failure", None, error
- count = _result_count(name, output)
- if count is not None:
- return ("hit" if count > 0 else "empty"), count, None
- if status == "running" or not ended_at:
- return "running", None, None
- if name in MAIN_DECISION_READ_TOOLS and output not in (None, "", [], {}):
- return "hit", None, None
- return "unknown", None, None
- def _error_text(value: Any) -> str | None:
- if isinstance(value, dict):
- success = value.get("success")
- if success is False or (isinstance(success, str) and success.lower() == "false"):
- candidate = value.get("error") or value.get("message") or "返回结果标记为失败"
- return _clean_error(candidate)
- error = value.get("error")
- if error not in (None, "", False, [], {}):
- return _clean_error(error)
- status = str(value.get("status") or "").lower()
- if status in {"error", "failed", "failure"}:
- return _clean_error(value.get("message") or value.get("output") or status)
- if isinstance(value, str):
- text = value.strip()
- if re.match(r"^(?:错误|失败)\s*[::]", text) or text.startswith("❌"):
- return _clean_error(text)
- return None
- def _clean_error(value: Any) -> str:
- text = str(value or "").strip()
- text = re.sub(r"^(?:错误|失败)\s*[::]\s*", "", text)
- text = re.sub(r"^❌\s*", "", text)
- return text or "执行失败,未保存更多错误说明"
- def _result_count(name: str, value: Any) -> int | None:
- if name == "search_knowledge" and isinstance(value, list):
- return len(value)
- if isinstance(value, list):
- return len(value)
- if not isinstance(value, dict):
- return None
- for key in ("count", "returned_count", "result_count"):
- count = _integer(value.get(key))
- if count is not None:
- return count
- for key in ("results", "data", "items", "domain_info"):
- if isinstance(value.get(key), list):
- return len(value[key])
- return None
- def _result_items(name: str, output: Any) -> list[dict[str, Any]]:
- if name == "search_script_decode_case":
- values = _result_collection(name, output)
- return [_case_item(item, index) for index, item in enumerate(values or [], 1)]
- if name == "external_search_case":
- values = _result_collection(name, output)
- return [_external_item(item, index, output) for index, item in enumerate(values or [], 1)]
- if name == "search_knowledge":
- return [
- _knowledge_item(item, index)
- for index, item in enumerate(_result_collection(name, output) or [], 1)
- ]
- return _direct_items(name, output)
- def _result_collection(name: str, output: Any) -> Any:
- key = RETRIEVAL_RESULT_COLLECTION_KEYS.get(name)
- if key is None:
- return output
- return output.get(key) if isinstance(output, dict) else None
- def _case_item(item: Any, index: int) -> dict[str, Any]:
- data = item if isinstance(item, dict) else {"data": item}
- account = str(data.get("account") or "未知账号")
- return {
- "id": str(data.get("post_id") or index),
- "title": f"{account} · Case {index}",
- "subtitle": str(data.get("post_id") or "来源标识未记录"),
- "meta": _meta(
- ("匹配分数", data.get("score")),
- ("返回内容", data.get("return_field")),
- ),
- "sections": [{"title": "完整解构内容", "value": data.get("data")}],
- }
- def _external_item(item: Any, index: int, output: dict[str, Any]) -> dict[str, Any]:
- data = item if isinstance(item, dict) else {"body_text": item}
- return {
- "id": str(data.get("channel_content_id") or index),
- "title": str(data.get("title") or f"外部内容 {index}"),
- "subtitle": str(data.get("channel") or output.get("platform_channel") or "外部平台"),
- "href": data.get("link"),
- "meta": _meta(
- ("内容类型", data.get("content_type")),
- ("点赞", data.get("like_count")),
- ("发布时间", data.get("publish_timestamp")),
- ),
- "sections": [
- value
- for value in (
- _value_section("正文", data.get("body_text")),
- _value_section("图片内容理解", data.get("images_text_escape")),
- _value_section("图片", data.get("images")),
- _value_section("视频", data.get("videos")),
- )
- if value
- ],
- }
- def _knowledge_item(item: Any, index: int) -> dict[str, Any]:
- data = item if isinstance(item, dict) else {"content": item}
- return {
- "id": str(data.get("id") or index),
- "title": str(data.get("title") or f"知识条目 {index}"),
- "subtitle": str(data.get("purpose") or ""),
- "meta": [],
- "sections": [section for section in [_value_section("完整内容", data.get("content"))] if section],
- }
- def _direct_items(name: str, output: Any) -> list[dict[str, Any]]:
- if name == "get_domain_info" and isinstance(output, dict):
- values = _result_collection(name, output)
- return [
- {
- "id": str(item.get("id") or index),
- "title": f"领域事实 {index}",
- "subtitle": str(item.get("note") or ""),
- "meta": _meta(("轮次", item.get("round_index"))),
- "sections": [
- section
- for section in (
- _value_section("事实内容", item.get("content")),
- _value_section("核实说明", item.get("note")),
- _value_section("来源", item.get("source")),
- )
- if section
- ],
- }
- for index, item in enumerate(values or [], 1)
- if isinstance(item, dict)
- ]
- if output in (None, "", [], {}):
- return []
- data = output if isinstance(output, dict) else {"完整返回": output}
- title = {
- "get_topic_detail": "选题完整信息",
- "get_script_direction": "当前创作方向",
- "get_script_snapshot": "当前主脚本快照",
- "get_account_script_section_patterns": "账号段落模式",
- "get_account_script_persona_points": "账号人设",
- }.get(name, business_tool_label(name))
- preferred = {
- "get_topic_detail": [("选题", "topic"), ("创作点", "points"), ("内容关系", "item_relations")],
- "get_script_snapshot": [("脚本概况", "script_build"), ("段落", "paragraphs"), ("元素", "elements"), ("段落元素关联", "paragraph_element_links")],
- "get_account_script_section_patterns": [("分段规律摘要", "分段规律摘要"), ("典型分段依据", "典型分段依据"), ("段落结构模式", "段落结构模式列表"), ("分析规模", "分析帖子数")],
- }.get(name)
- if preferred:
- sections = [_value_section(label, data.get(key)) for label, key in preferred]
- else:
- sections = [_value_section(_condition_label(key), value) for key, value in data.items()]
- return [{
- "id": name,
- "title": title,
- "subtitle": "",
- "meta": [],
- "sections": [section for section in sections if section],
- }]
- def _query_conditions(value: Any) -> list[dict[str, Any]]:
- if not isinstance(value, dict):
- if value in (None, ""):
- return []
- return [{"key": "input", "label": "调用输入", "value": value}]
- return [
- {"key": str(key), "label": _condition_label(str(key)), "value": item}
- for key, item in value.items()
- if key not in _HIDDEN_CONDITIONS and item not in (None, "", [], {})
- ]
- def _condition_label(key: str) -> str:
- return _CONDITION_LABELS.get(key, key.replace("_", " "))
- def _outcome_message(
- state: str, count: int | None, error: str | None, source_kind: str
- ) -> str:
- if state == "failure":
- return f"查询执行失败:{error}" if error else "查询执行失败。"
- if state == "empty":
- return "查询执行成功,没有匹配数据。"
- if state == "hit":
- if source_kind == "direct-tool":
- return "读取成功。" if count is None else f"读取成功,共返回 {count} 条记录。"
- return f"查询成功,返回 {count} 条结果。" if count is not None else "查询成功。"
- if state == "interrupted":
- return "构建已经结束,但这次活动没有完成。"
- if state == "running":
- return "查询仍在执行。"
- return "现有记录不足以可靠判断查询结果。"
- def _error_kind(error: str | None) -> str:
- text = str(error or "").lower()
- if any(token in text for token in ("参数", "仅支持", "必须", "不能为空", "return_field")):
- return "parameter"
- if any(token in text for token in ("http", "timeout", "timed out", "connection", "请求失败")):
- return "transport"
- if any(token in text for token in ("upstream", "openrouter", "403", "502", "503")):
- return "upstream"
- return "unknown"
- def _completeness(side: Any, output: Any) -> dict[str, Any]:
- meta = side if isinstance(side, dict) else {}
- truncated = bool(meta.get("truncated"))
- omitted = _integer(meta.get("omittedCharacters"))
- if isinstance(output, str):
- match = re.search(r"…\[truncated\s+(\d+)\s+chars\]", output)
- if match:
- truncated = True
- omitted = int(match.group(1))
- return {
- "bodyAvailable": side is not None and output is not None,
- "truncated": truncated,
- **({"omittedCharacters": omitted} if omitted is not None else {}),
- }
- def _event_side(event: dict[str, Any], side: str) -> Any:
- wrapped = event.get(side)
- if isinstance(wrapped, dict) and "content" in wrapped:
- return _parse_jsonish(wrapped.get("content"))
- if wrapped is not None:
- return _parse_jsonish(wrapped)
- fallback = event.get("inputData" if side == "input" else "agentOutputData")
- if fallback is not None:
- return _parse_jsonish(fallback)
- return _parse_jsonish(event.get(f"{side}_preview"))
- def _parse_jsonish(value: Any) -> Any:
- if not isinstance(value, str):
- return value
- text = value.strip()
- if not text or text[0] not in "[{":
- return value
- try:
- return json.loads(text)
- except json.JSONDecodeError:
- return value
- def _value_section(title: str, value: Any) -> dict[str, Any] | None:
- if value in (None, "", [], {}):
- return None
- return {"title": title, "value": value}
- def _meta(*items: tuple[str, Any]) -> list[dict[str, Any]]:
- return [
- {"label": label, "value": value}
- for label, value in items
- if value not in (None, "", [], {})
- ]
- def _integer(value: Any) -> int | None:
- if isinstance(value, bool):
- return None
- try:
- return int(value)
- except (TypeError, ValueError):
- return None
|