| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969 |
- """demand 示例的最小可运行入口。"""
- import asyncio
- import copy
- import importlib
- import json
- import os
- import sys
- from datetime import datetime
- from pathlib import Path
- from typing import Any, Optional
- from zoneinfo import ZoneInfo
- # Clash Verge TUN 模式兼容:禁止 httpx/urllib 自动检测系统 HTTP 代理
- os.environ.setdefault("no_proxy", "*")
- # 该示例仅使用项目侧能力,禁用框架内置 skills
- os.environ.setdefault("AGENT_DISABLE_BUILTIN_SKILLS", "1")
- # 禁用内置工具自动注册,并开启严格工具白名单
- os.environ.setdefault("AGENT_DISABLE_BUILTIN_TOOL_REGISTRATION", "1")
- os.environ.setdefault("AGENT_STRICT_TOOL_SELECTION", "1")
- # 禁用所有侧分支(压缩/反思)
- os.environ.setdefault("AGENT_DISABLE_SIDE_BRANCHES", "1")
- from dotenv import load_dotenv
- from examples.demand.db_manager import (
- query_category_level,
- query_latest_success_execution_id,
- query_video_ids_by_names,
- )
- from examples.demand.demand_agent_context import TopicBuildAgentContext
- # 添加项目根目录到 Python 路径
- sys.path.insert(0, str(Path(__file__).parent.parent.parent))
- load_dotenv()
- from examples.demand.log_capture import build_log, log
- CUSTOM_TOOL_MODULES = {
- # demand 示例:严格按工具名白名单加载对应模块
- "think_and_plan": "examples.demand.agent_tools",
- "get_category_tree": "examples.demand.demand_pattern_tools",
- "get_frequent_itemsets": "examples.demand.demand_pattern_tools",
- "get_itemset_detail": "examples.demand.demand_pattern_tools",
- "get_post_elements": "examples.demand.demand_pattern_tools",
- "search_elements": "examples.demand.demand_pattern_tools",
- "get_element_category_chain": "examples.demand.demand_pattern_tools",
- "get_category_detail": "examples.demand.demand_pattern_tools",
- "search_categories": "examples.demand.demand_pattern_tools",
- "get_category_elements": "examples.demand.demand_pattern_tools",
- "get_category_co_occurrences": "examples.demand.demand_pattern_tools",
- "get_element_co_occurrences": "examples.demand.demand_pattern_tools",
- "get_weight_score_topn": "examples.demand.weight_score_query_tools",
- "get_weight_score_by_name": "examples.demand.weight_score_query_tools",
- "create_demand_item": "examples.demand.demand_build_agent_tools",
- "create_demand_items": "examples.demand.demand_build_agent_tools",
- "write_execution_summary": "examples.demand.demand_build_agent_tools",
- }
- LOCAL_JSON_MODE = "local_json"
- MYSQL_DEMAND_CONTENT_MODE = "mysql_demand_content"
- LOCAL_ENTRYPOINT_ENV = "DEMAND_LOCAL_ENTRYPOINT"
- LOCAL_ENTRYPOINT_NAME = "run_existing_execution_local"
- MYSQL_ENTRYPOINT_ENV = "DEMAND_MYSQL_ENTRYPOINT"
- MYSQL_ENTRYPOINT_NAME = "run_existing_execution_mysql"
- def _is_local_json_mode() -> bool:
- return os.getenv("DEMAND_OUTPUT_MODE", "").strip().lower() == LOCAL_JSON_MODE
- def _is_mysql_demand_content_mode() -> bool:
- return os.getenv("DEMAND_OUTPUT_MODE", "").strip().lower() == MYSQL_DEMAND_CONTENT_MODE
- def _raise_db_write_disabled(action: str) -> None:
- raise RuntimeError(
- f"{action} 已在 PG Pattern V2 MVP 中禁用;"
- "请通过 run_existing_execution_local 写本地 JSON,或通过 "
- "run_existing_execution_mysql 只写测试 MySQL demand_content"
- )
- def _require_local_entrypoint() -> None:
- if not _is_local_json_mode():
- return
- entrypoint = os.getenv(LOCAL_ENTRYPOINT_ENV, "").strip()
- if entrypoint != LOCAL_ENTRYPOINT_NAME:
- raise RuntimeError(
- "DEMAND_OUTPUT_MODE=local_json 只能通过 "
- "examples.demand.run_existing_execution_local 入口运行"
- )
- def _require_mysql_entrypoint() -> None:
- if not _is_mysql_demand_content_mode():
- return
- entrypoint = os.getenv(MYSQL_ENTRYPOINT_ENV, "").strip()
- if entrypoint != MYSQL_ENTRYPOINT_NAME:
- raise RuntimeError(
- "DEMAND_OUTPUT_MODE=mysql_demand_content 只能通过 "
- "examples.demand.run_existing_execution_mysql 入口运行"
- )
- def _default_local_output_root(execution_id: int) -> Path:
- ts = datetime.now(ZoneInfo("Asia/Shanghai")).strftime("%Y%m%d_%H%M%S")
- return Path(__file__).parent / "test_output_data" / f"execution_{execution_id}_{ts}"
- def _ensure_local_output_paths(execution_id: int) -> Path:
- root_value = os.getenv("DEMAND_LOCAL_OUTPUT_ROOT")
- root = Path(root_value) if root_value else _default_local_output_root(execution_id)
- os.environ["DEMAND_LOCAL_OUTPUT_ROOT"] = str(root)
- os.environ.setdefault("DEMAND_RESULT_BASE_DIR", str(root / "intermediate" / "result"))
- os.environ.setdefault("DEMAND_WEIGHT_DATA_DIR", str(root / "intermediate" / "data"))
- os.environ.setdefault("DEMAND_TRACE_STORE_PATH", str(root / ".trace"))
- os.environ.setdefault("DEMAND_OUTPUT_BASE_DIR", str(root / "output"))
- return root
- def _get_result_base_dir() -> Path:
- redirected = os.getenv("DEMAND_RESULT_BASE_DIR")
- if redirected:
- return Path(redirected)
- if _is_local_json_mode():
- return _ensure_local_output_paths(0) / "result"
- return Path.cwd() / "result"
- def _get_output_base_dir() -> Path:
- redirected = os.getenv("DEMAND_OUTPUT_BASE_DIR")
- if redirected:
- return Path(redirected)
- if _is_local_json_mode():
- return _ensure_local_output_paths(0) / "output"
- return Path(__file__).parent / "output"
- def _get_trace_store_base_dir() -> Path:
- redirected = os.getenv("DEMAND_TRACE_STORE_PATH")
- if redirected:
- return Path(redirected)
- if _is_local_json_mode():
- return _ensure_local_output_paths(0) / ".trace"
- from examples.demand.config import TRACE_STORE_PATH
- return Path(TRACE_STORE_PATH)
- def _get_weight_data_base_dir() -> Path:
- redirected = os.getenv("DEMAND_WEIGHT_DATA_DIR")
- if redirected:
- return Path(redirected)
- return Path(__file__).parent / "data"
- def _get_local_output_root() -> Optional[Path]:
- root = os.getenv("DEMAND_LOCAL_OUTPUT_ROOT")
- return Path(root) if root else None
- def _write_json_file(path: Path, payload: Any) -> None:
- path.parent.mkdir(parents=True, exist_ok=True)
- with open(path, "w", encoding="utf-8") as f:
- json.dump(payload, f, ensure_ascii=False, indent=2)
- def get_execution_id_by_merge_level2(cluster_name: str):
- """Return the latest successful PG Pattern V2 execution_id.
- PG Pattern V2 executions are global snapshots rather than the old MySQL
- merge_leve2-scoped execution rows. Local MVP callers should pass an explicit
- execution_id; this fallback only keeps non-local paths from using MySQL.
- """
- del cluster_name
- return query_latest_success_execution_id()
- def resolve_model(prompt: Any, run_config: Any) -> str:
- for env_key in ("DEMAND_LLM_MODEL", "ARK_MODEL", "OPEN_ROUTER_MODEL", "OPENROUTER_MODEL"):
- model_from_env = os.getenv(env_key)
- if model_from_env:
- return model_from_env.strip()
- model_from_prompt = prompt.config.get("model")
- if model_from_prompt:
- return model_from_prompt
- return f"anthropic/{run_config.model}" if "/" not in run_config.model else run_config.model
- def extract_assistant_text(message: Any) -> str:
- if message.role != "assistant":
- return ""
- content = message.content
- if isinstance(content, str):
- return content
- if isinstance(content, dict):
- text = content.get("text", "")
- # 即使本轮包含工具调用,也打印模型给出的文本,便于观察每一步输出
- if text:
- return text
- return ""
- def register_selected_tools(tool_names: list[str]) -> None:
- for tool_name in tool_names:
- module_path = CUSTOM_TOOL_MODULES.get(tool_name)
- if not module_path:
- raise ValueError(f"未配置工具模块映射: {tool_name}")
- importlib.import_module(module_path)
- def _enabled_tools_for_run(configured_tools: list[str]) -> list[str]:
- tools = configured_tools.copy()
- if _is_local_json_mode():
- # 本地批量输出只需要 DemandItem JSON。长摘要会显著放大最后一轮上下文,
- # 且不参与下游 CFA 验证契约。
- tools = [tool_name for tool_name in tools if tool_name != "write_execution_summary"]
- return tools
- def _join_element_names_to_name(element_names: object) -> str:
- """把 tool 入参 element_names 转成 demand_content.name(逗号分隔)。"""
- if element_names is None:
- return ""
- if isinstance(element_names, list):
- parts = [str(x).strip() for x in element_names if x is not None and str(x).strip()]
- return ",".join(parts)
- # 兼容异常数据:比如历史版本可能传了字符串
- return str(element_names).strip()
- def _safe_truncate(s: object, max_len: int) -> str:
- if s is None:
- return ""
- s_str = str(s)
- if max_len and len(s_str) > max_len:
- return s_str[:max_len]
- return s_str
- def _load_name_score_map(execution_id: int) -> dict:
- """读取 data/{execution_id} 下所有 JSON 的「名字->score」(同名取最高分)。
- 兼容两类数据结构:
- - `*_元素.json`:字段 `name` 表示名字
- - `*_分类.json`:字段 `category` 表示名字
- """
- data_dir = _get_weight_data_base_dir() / str(execution_id)
- if not data_dir.exists():
- return {}
- score_map = {}
- for json_path in data_dir.glob("*.json"):
- try:
- with open(json_path, "r", encoding="utf-8") as f:
- payload = json.load(f)
- except Exception:
- continue
- if not isinstance(payload, list):
- continue
- for item in payload:
- if not isinstance(item, dict):
- continue
- # 元素数据以 name 为主;分类数据以 category 为主。
- name = item.get("name")
- if not isinstance(name, str) or not name:
- name = item.get("category")
- score = item.get("score")
- if isinstance(name, str) and isinstance(score, (int, float)):
- prev = score_map.get(name)
- score_f = float(score)
- if prev is None or score_f > prev:
- score_map[name] = score_f
- return score_map
- def _ensure_pg_weight_score_files(execution_id: int) -> None:
- """Build local PG weight files when local JSON mode needs them."""
- if not _is_local_json_mode():
- return
- from examples.demand.pg_weight_score_builder import build_pg_weight_score_files
- base_dir = _get_weight_data_base_dir()
- expected_dir = base_dir / str(execution_id)
- required_files = [
- expected_dir / f"{dimension}_{level}.json"
- for dimension in ("实质", "形式", "意图")
- for level in ("元素", "分类")
- ]
- if all(path.exists() for path in required_files):
- return
- counts = build_pg_weight_score_files(execution_id=execution_id, base_dir=base_dir)
- log(f"[weight][pg] 生成 PG 权重 JSON,execution_id={execution_id}, files={counts}")
- def _avg_score_for_joined_name(name: str, score_map: dict) -> float:
- """按逗号拆分 name,分别取分后求平均。"""
- parts = [part.strip() for part in str(name).split(",") if part and part.strip()]
- if not parts:
- return 0.0
- return sum(float(score_map.get(part, 0.0)) for part in parts) / len(parts)
- def _resolve_video_ids_by_name_and_execution_id(name: str, execution_id: int) -> list[str]:
- """按 name(逗号分隔) 与 execution_id 解析去重后的 post_id 列表。"""
- name_parts = [part.strip() for part in str(name).split(",") if part and part.strip()]
- if not name_parts:
- return []
- return query_video_ids_by_names(execution_id=execution_id, names=name_parts)
- def _demand_items_output_path(execution_id: int) -> Path:
- return _get_result_base_dir() / str(execution_id) / f"execution_id_{execution_id}_demand_items.json"
- def _resolve_demand_items_path(execution_id: int) -> Optional[Path]:
- configured_path = _demand_items_output_path(execution_id)
- if configured_path.exists() or _is_local_json_mode():
- return configured_path if configured_path.exists() else None
- fallbacks = [
- Path.cwd() / "result" / str(execution_id) / f"execution_id_{execution_id}_demand_items.json",
- Path(__file__).parent / "result" / str(execution_id) / f"execution_id_{execution_id}_demand_items.json",
- ]
- for path in fallbacks:
- if path.exists():
- return path
- return None
- def _load_demand_items(execution_id: int, log_prefix: str) -> list[dict]:
- demand_items_path = _resolve_demand_items_path(execution_id)
- if not demand_items_path:
- expected_path = _demand_items_output_path(execution_id)
- log(f"[{log_prefix}] 未找到需求 JSON:{expected_path},跳过写入")
- return []
- try:
- with open(demand_items_path, "r", encoding="utf-8") as f:
- loaded = json.load(f)
- except Exception as e:
- log(f"[{log_prefix}] 读取需求 JSON 失败:{demand_items_path},error={e}")
- return []
- items = loaded["items"] if isinstance(loaded, dict) and isinstance(loaded.get("items"), list) else loaded
- if not isinstance(items, list):
- log(f"[{log_prefix}] 需求 JSON 非数组,跳过写入:type={type(items)}")
- return []
- return [item for item in items if isinstance(item, dict)]
- def _normalize_evidence_refs(value: object) -> dict:
- if isinstance(value, dict):
- return value
- if isinstance(value, str) and value.strip():
- try:
- loaded = json.loads(value)
- if isinstance(loaded, dict):
- return loaded
- except json.JSONDecodeError:
- pass
- return {}
- def _build_demand_content_rows(
- execution_id: int,
- merge_level2: str,
- *,
- trace_id: Optional[str] = None,
- task_id: Optional[int] = None,
- serialize_ext_data: bool = True,
- require_evidence: bool = False,
- ) -> tuple[list[dict], list[dict], list[dict]]:
- from examples.demand.evidence_pack_builder import build_evidence_pack
- items = _load_demand_items(execution_id=execution_id, log_prefix="result")
- if not items:
- return [], [], []
- dt_value = datetime.now(ZoneInfo("Asia/Shanghai")).strftime("%Y%m%d")
- score_map = _load_name_score_map(execution_id)
- rows: list[dict] = []
- normalized_items: list[dict] = []
- rejected_items: list[dict] = []
- for index, di in enumerate(items, start=1):
- normalized_item = dict(di)
- evidence_refs = _normalize_evidence_refs(di.get("evidence_refs"))
- normalized_item["evidence_refs"] = evidence_refs
- normalized_items.append(normalized_item)
- name = _join_element_names_to_name(di.get("element_names"))
- if not name:
- rejected_items.append(
- {
- "item_index": index,
- "reject_reason": "missing element_names/name",
- "demand_item": normalized_item,
- }
- )
- continue
- score = _avg_score_for_joined_name(name, score_map)
- reason = di.get("reason")
- desc_value = di.get("desc")
- item_type = di.get("type")
- type_str = str(item_type).strip() if item_type is not None else ""
- if type_str == "分类":
- category_level = query_category_level(execution_id=execution_id, name=name)
- if category_level:
- type_str = type_str + f"L{category_level}"
- video_ids = _resolve_video_ids_by_name_and_execution_id(name=name, execution_id=execution_id)
- ext_data: dict[str, Any] = {
- "reason": reason,
- "desc": desc_value,
- "type": type_str,
- "video_ids": video_ids,
- }
- if trace_id:
- ext_data["trace_id"] = trace_id
- if task_id is not None:
- ext_data["demand_task_id"] = task_id
- content_id = len(rows) + 1 if not serialize_ext_data else None
- if require_evidence or evidence_refs:
- evidence_result = build_evidence_pack(
- execution_id=int(execution_id),
- demand_item=normalized_item,
- trace_id=trace_id or "",
- demand_task_id=task_id,
- demand_content_id=content_id,
- )
- if not evidence_result.get("success"):
- if require_evidence:
- rejected_items.append(
- {
- "item_index": index,
- "reject_reason": evidence_result.get("reject_reason") or "evidence validation failed",
- "demand_item": normalized_item,
- }
- )
- continue
- log(
- "[result] evidence 校验未通过,生产兼容路径保留旧输出:"
- f"item_index={index}, reason={evidence_result.get('reject_reason')}"
- )
- else:
- evidence_pack = evidence_result["evidence_pack"]
- ext_data["evidence_pack"] = evidence_pack
- if evidence_pack.get("video_ids"):
- ext_data["video_ids"] = evidence_pack["video_ids"]
- row = {
- "merge_leve2": _safe_truncate(merge_level2, 32),
- "name": _safe_truncate(name, 64),
- "reason": reason,
- "suggestion": desc_value,
- "score": float(score),
- "ext_data": json.dumps(ext_data, ensure_ascii=False) if serialize_ext_data else ext_data,
- "dt": dt_value,
- }
- if not serialize_ext_data:
- row["id"] = content_id
- rows.append(row)
- if not rows:
- log("[result] 生成行为空,跳过写入")
- if rejected_items:
- log(f"[result] evidence 校验拒绝 {len(rejected_items)} 条 DemandItem")
- return rows, normalized_items, rejected_items
- def _create_demand_task(
- execution_id: int,
- name: Optional[str] = None,
- platform: Optional[str] = None,
- ) -> Optional[int]:
- """Disabled production demand_task writer retained for legacy imports."""
- del execution_id, name, platform
- _raise_db_write_disabled("生产 _create_demand_task")
- def _finish_demand_task(task_id: Optional[int], status: int, task_log: str) -> None:
- """Disabled production demand_task updater retained for legacy imports."""
- if not task_id:
- return
- del status, task_log
- _raise_db_write_disabled("生产 _finish_demand_task")
- def _start_local_demand_task(
- *,
- execution_id: int,
- task_id: Optional[int],
- name: Optional[str],
- platform: Optional[str],
- ) -> int:
- from examples.demand.local_output_sink import LocalOutputSink
- local_task_id = int(task_id or os.getenv("DEMAND_LOCAL_TASK_ID", "1"))
- os.environ["DEMAND_LOCAL_TASK_ID"] = str(local_task_id)
- root = _get_local_output_root() or _ensure_local_output_paths(execution_id)
- task_payload = {
- "id": local_task_id,
- "execution_id": execution_id,
- "name": name,
- "platform": platform,
- "status": 0,
- "mode": LOCAL_JSON_MODE,
- "is_simulated_id": True,
- "log": "",
- "started_at": datetime.now(ZoneInfo("Asia/Shanghai")).isoformat(),
- }
- LocalOutputSink(root).write_all(
- run_manifest={
- "output_mode": LOCAL_JSON_MODE,
- "execution_id": execution_id,
- "merge_leve2": name,
- "platform_type": platform,
- "task_id": local_task_id,
- "status": 0,
- "initialized": True,
- "created_at": datetime.now(ZoneInfo("Asia/Shanghai")).isoformat(),
- },
- demand_task=task_payload,
- )
- log(f"[task][local_json] 写入本地 demand_task,task_id={local_task_id}, execution_id={execution_id}")
- return local_task_id
- def _finish_local_demand_task(task_id: Optional[int], status: int, task_log: str) -> None:
- if not task_id:
- return
- root = _get_local_output_root()
- if not root:
- return
- task_path = root / "demand_task.json"
- payload: dict[str, Any] = {}
- if task_path.exists():
- try:
- with open(task_path, "r", encoding="utf-8") as f:
- loaded = json.load(f)
- if isinstance(loaded, dict):
- payload = loaded
- except Exception:
- payload = {}
- payload.update(
- {
- "id": task_id,
- "status": int(status),
- "mode": LOCAL_JSON_MODE,
- "is_simulated_id": True,
- "log": task_log or "",
- "finished_at": datetime.now(ZoneInfo("Asia/Shanghai")).isoformat(),
- }
- )
- _write_json_file(task_path, payload)
- log(f"[task][local_json] 更新本地 demand_task,task_id={task_id}, status={status}")
- def write_demand_items_to_mysql(
- execution_id: int,
- merge_level2: str,
- *,
- trace_id: Optional[str] = None,
- task_id: Optional[int] = None,
- ) -> int:
- """Write DB-validated demand_content rows to the test MySQL sink only."""
- if not _is_mysql_demand_content_mode():
- _raise_db_write_disabled("生产 write_demand_items_to_mysql")
- from examples.demand.mysql_demand_content_sink import write_demand_content_rows
- rows, demand_items, rejected_items = _build_demand_content_rows(
- execution_id=execution_id,
- merge_level2=merge_level2,
- trace_id=trace_id,
- task_id=task_id,
- serialize_ext_data=False,
- require_evidence=True,
- )
- run_label = os.getenv("DEMAND_RUN_LABEL", "").strip()
- result = write_demand_content_rows(rows, run_label=run_label)
- log(
- "[mysql_demand_content] 写入测试 MySQL demand_content 完成,"
- f"demand_items={len(demand_items)}, inserted={result.inserted_count}, "
- f"rejected={len(rejected_items)}, skipped={result.skipped_count}"
- )
- if rejected_items:
- log(
- "[mysql_demand_content] rejected_items="
- + json.dumps(rejected_items, ensure_ascii=False, default=str)
- )
- if result.inserted_ids:
- log(f"[mysql_demand_content] inserted_ids={result.inserted_ids}")
- return result.inserted_count
- def write_demand_items_to_local_json(
- execution_id: int,
- merge_level2: str,
- *,
- trace_id: Optional[str] = None,
- task_id: Optional[int] = None,
- ) -> int:
- """把普通需求池输出镜像到本地 JSON,不写 demand_content / Hive。"""
- from examples.demand.local_output_sink import LocalOutputSink
- root = _get_local_output_root() or _ensure_local_output_paths(execution_id)
- rows, demand_items, rejected_items = _build_demand_content_rows(
- execution_id=execution_id,
- merge_level2=merge_level2,
- trace_id=trace_id,
- task_id=task_id,
- serialize_ext_data=False,
- require_evidence=True,
- )
- sink = LocalOutputSink(root)
- sink.write_demand_items(demand_items)
- sink.write_rejected_demand_items(rejected_items)
- sink.write_demand_content(rows)
- sink.write_dwd_multi_demand_pool_di_from_demand_content(rows)
- sink.write_feature_point_data([])
- log(
- "[local_json] 写入本地 demand_content/dwd_multi_demand_pool_di 完成,"
- f"demand_content_rows={len(rows)}, rejected={len(rejected_items)}, root={root}"
- )
- return len(rows)
- def write_global_tree_demand_items_to_hive(execution_id: int, merge_level2: str) -> int:
- """Disabled production Hive writer retained for legacy imports."""
- del execution_id, merge_level2
- _raise_db_write_disabled("生产 write_global_tree_demand_items_to_hive")
- def write_global_tree_demand_items_to_local_json(
- execution_id: int,
- merge_level2: str,
- *,
- trace_id: Optional[str] = None,
- task_id: Optional[int] = None,
- ) -> int:
- """把全局树 feature_point_data 输出镜像到本地 JSON,不写 Hive。"""
- from examples.demand.local_output_sink import LocalOutputSink
- root = _get_local_output_root() or _ensure_local_output_paths(execution_id)
- items = _load_demand_items(execution_id=execution_id, log_prefix="local_json")
- normalized_items = []
- names: list[str] = []
- for di in items:
- normalized = dict(di)
- normalized["evidence_refs"] = _normalize_evidence_refs(di.get("evidence_refs"))
- normalized_items.append(normalized)
- name = _join_element_names_to_name(di.get("element_names"))
- if name:
- names.append(name)
- sink = LocalOutputSink(root)
- sink.write_demand_items(normalized_items)
- sink.write_rejected_demand_items([])
- sink.write_demand_content([])
- sink.write_dwd_multi_demand_pool_di([])
- sink.write_feature_point_data_from_names(names)
- log(
- "[local_json] 写入本地 feature_point_data 完成,"
- f"rows={len(names)}, merge_leve2={merge_level2}, root={root}"
- )
- return len(names)
- def _write_local_run_manifest(
- *,
- execution_id: int,
- merge_level2: str,
- platform_type: Optional[str],
- count: int,
- task_id: Optional[int],
- trace_id: Optional[str],
- final_text: str,
- task_status: int,
- total_tokens: int,
- total_cost: float,
- result_write_count: int,
- log_file_path: Path,
- result_write_error: Optional[str] = None,
- ) -> None:
- if not _is_local_json_mode():
- return
- root = _get_local_output_root() or _ensure_local_output_paths(execution_id)
- manifest = {
- "output_mode": LOCAL_JSON_MODE,
- "execution_id": execution_id,
- "merge_leve2": merge_level2,
- "platform_type": platform_type,
- "count": count,
- "task_id": task_id,
- "trace_id": trace_id,
- "status": task_status,
- "total_tokens": total_tokens,
- "total_cost": total_cost,
- "result_write_count": result_write_count,
- "result_write_error": result_write_error,
- "final_text_length": len(final_text or ""),
- "paths": {
- "root": str(root),
- "result": str(_get_result_base_dir()),
- "trace": str(_get_trace_store_base_dir()),
- "output": str(_get_output_base_dir()),
- "demand_items_raw": str(_demand_items_output_path(execution_id)),
- "log_file": str(log_file_path),
- },
- "created_at": datetime.now(ZoneInfo("Asia/Shanghai")).isoformat(),
- }
- _write_json_file(root / "run_manifest.json", manifest)
- async def run_once(
- execution_id,
- merge_level2,
- count: int = 30,
- task_id: Optional[int] = None,
- platform_type: Optional[str] = None,
- ) -> dict:
- from agent.core.runner import AgentRunner
- from agent.llm import create_openrouter_llm_call
- from agent.llm.prompts import SimplePrompt
- from agent.trace import FileSystemTraceStore, Message, Trace
- from agent.utils import setup_logging
- from examples.demand.config import DEBUG, ENABLED_TOOLS, LOG_FILE, LOG_LEVEL, RUN_CONFIG
- task_log_text = ""
- task_status = 0
- result_write_count = 0
- result_write_error: Optional[str] = None
- if _is_local_json_mode():
- _ensure_local_output_paths(execution_id)
- _ensure_pg_weight_score_files(int(execution_id))
- TopicBuildAgentContext.set_execution_id(execution_id)
- TopicBuildAgentContext.set_metadata("result_base_dir", str(_get_result_base_dir()))
- base_dir = Path(__file__).parent
- output_dir = _get_output_base_dir()
- output_dir.mkdir(parents=True, exist_ok=True)
- setup_logging(level=LOG_LEVEL, file=LOG_FILE)
- enabled_tools = _enabled_tools_for_run(ENABLED_TOOLS)
- register_selected_tools(enabled_tools)
- prompt = SimplePrompt(base_dir / "demand.md")
- run_config = copy.deepcopy(RUN_CONFIG)
- model = resolve_model(prompt, run_config)
- run_config.model = model
- run_config.temperature = float(prompt.config.get("temperature", run_config.temperature))
- run_config.max_iterations = int(prompt.config.get("max_iterations", run_config.max_iterations))
- run_config.tools = enabled_tools
- # 禁用反思/总结经验相关流程(避免进入 reflection 侧分支)
- run_config.enable_research_flow = False
- run_config.goal_compression = "none"
- run_config.force_side_branch = None
- run_config.knowledge.enable_extraction = False
- run_config.knowledge.enable_completion_extraction = False
- run_config.knowledge.enable_injection = False
- run_config.trace_id = None
- initial_messages = prompt.build_messages(merge_level2=merge_level2, count=count)
- trace_store_path = _get_trace_store_base_dir()
- store = FileSystemTraceStore(base_path=str(trace_store_path))
- runner = AgentRunner(
- trace_store=store,
- llm_call=create_openrouter_llm_call(model=model),
- skills_dir=None,
- debug=DEBUG,
- )
- final_text = ""
- total_tokens = 0
- total_cost = 0.0
- trace_id: Optional[str] = None
- has_completed_trace_cost = False
- log_file_path = output_dir / f"{execution_id}" / f"run_log_{execution_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
- log_file_path.parent.mkdir(parents=True, exist_ok=True)
- try:
- with build_log(execution_id) as log_buffer:
- async for item in runner.run(messages=initial_messages, config=run_config):
- if isinstance(item, Trace):
- trace_id = getattr(item, "trace_id", None) or trace_id
- if getattr(item, "status", None) == "completed":
- total_tokens = int(getattr(item, "total_tokens", 0) or 0)
- total_cost = float(getattr(item, "total_cost", 0.0) or 0.0)
- has_completed_trace_cost = True
- continue
- elif isinstance(item, Message):
- text = extract_assistant_text(item)
- if text:
- final_text = text
- log(f"[assistant] {text}")
- if not has_completed_trace_cost:
- total_tokens += int(getattr(item, "total_tokens", 0) or 0)
- total_cost += float(getattr(item, "cost", 0.0) or 0.0)
- if final_text:
- output_file = output_dir / f"{execution_id}" / "result.txt"
- output_file.parent.mkdir(parents=True, exist_ok=True)
- with open(output_file, "w", encoding="utf-8") as f:
- f.write(final_text)
- log(f"[cost] total_tokens={total_tokens}, total_cost=${total_cost:.6f}")
- # agent 执行完成后:全局树写 Hive,其他写 MySQL
- try:
- if str(merge_level2).strip() == "全局树":
- if _is_local_json_mode():
- result_write_count = write_global_tree_demand_items_to_local_json(
- execution_id=execution_id,
- merge_level2=merge_level2,
- trace_id=trace_id,
- task_id=task_id,
- )
- else:
- result_write_count = write_global_tree_demand_items_to_hive(
- execution_id=execution_id,
- merge_level2=merge_level2,
- )
- else:
- # element_names -> name(逗号分隔);reason -> demand_content.reason;desc -> demand_content.suggestion;dt -> demand_content.dt
- if _is_local_json_mode():
- result_write_count = write_demand_items_to_local_json(
- execution_id=execution_id,
- merge_level2=merge_level2,
- trace_id=trace_id,
- task_id=task_id,
- )
- else:
- result_write_count = write_demand_items_to_mysql(
- execution_id=execution_id,
- merge_level2=merge_level2,
- trace_id=trace_id,
- task_id=task_id,
- )
- except Exception as e:
- result_write_error = str(e)
- log(f"[result-write] 写入结果异常:{e}")
- if _is_local_json_mode():
- task_status = 2
- task_log_text = log_buffer.getvalue()
- if task_status != 2:
- task_status = 1
- except Exception as e:
- if not task_log_text:
- # 如果异常发生在 build_log 内部,尽量回收已产生的日志
- try:
- existing = locals().get("log_buffer")
- if existing is not None:
- task_log_text = existing.getvalue() # type: ignore[attr-defined]
- except Exception:
- pass
- if not task_log_text:
- task_log_text = f"[run] 执行异常: {e}"
- task_status = 2
- raise
- finally:
- if task_log_text:
- try:
- with open(log_file_path, "w", encoding="utf-8") as f:
- f.write(task_log_text)
- except Exception:
- # 兜底:即使写文件失败,也要确保 MySQL 状态被更新
- pass
- if _is_local_json_mode():
- _finish_local_demand_task(task_id=task_id, status=task_status, task_log=task_log_text)
- elif not _is_mysql_demand_content_mode():
- _finish_demand_task(task_id=task_id, status=task_status, task_log=task_log_text)
- _write_local_run_manifest(
- execution_id=execution_id,
- merge_level2=merge_level2,
- platform_type=platform_type,
- count=int(count),
- task_id=task_id,
- trace_id=trace_id,
- final_text=final_text,
- task_status=task_status,
- total_tokens=total_tokens,
- total_cost=total_cost,
- result_write_count=result_write_count,
- log_file_path=log_file_path,
- result_write_error=result_write_error,
- )
- return {
- "final_text": final_text,
- "trace_id": trace_id,
- "output_dir": str(output_dir / f"{execution_id}"),
- "trace_store_path": str(trace_store_path),
- "result_base_dir": str(_get_result_base_dir()),
- "result_write_count": result_write_count,
- }
- async def main(
- cluster_name: str,
- platform_type: str,
- count,
- execution_id: Optional[int] = None,
- task_id: Optional[int] = None,
- ) -> dict:
- if not (_is_local_json_mode() or _is_mysql_demand_content_mode()):
- _raise_db_write_disabled("非受控运行入口")
- if _is_local_json_mode():
- _require_local_entrypoint()
- if execution_id is None:
- raise ValueError("local_json 模式必须传入已有 execution_id,不能触发 prepare 写库流程")
- _ensure_local_output_paths(execution_id)
- if _is_mysql_demand_content_mode():
- _require_mysql_entrypoint()
- if execution_id is None:
- raise ValueError("mysql_demand_content 模式必须传入已有 execution_id,不能触发 prepare 写库流程")
- if str(cluster_name).strip() == "全局树":
- raise ValueError("mysql_demand_content 模式只写普通 demand_content,不支持全局树 Hive 输出")
- if execution_id is None:
- execution_id = get_execution_id_by_merge_level2(cluster_name)
- if not execution_id:
- return {"execution_id": None, "trace_id": None, "final_text": ""}
- if _is_local_json_mode():
- task_id = _start_local_demand_task(
- execution_id=int(execution_id),
- task_id=task_id,
- name=cluster_name,
- platform=platform_type,
- )
- run_result = await run_once(
- execution_id,
- cluster_name,
- count=count,
- task_id=task_id,
- platform_type=platform_type,
- )
- result = {
- "execution_id": execution_id,
- "trace_id": run_result.get("trace_id"),
- "final_text": run_result.get("final_text", ""),
- "output_dir": run_result.get("output_dir"),
- "trace_store_path": run_result.get("trace_store_path"),
- "result_base_dir": run_result.get("result_base_dir"),
- "result_write_count": run_result.get("result_write_count", 0),
- }
- if _is_local_json_mode():
- root = _get_local_output_root()
- result["local_output_root"] = str(root) if root else None
- return result
- if __name__ == "__main__":
- asyncio.run(main('全局树', 'piaoquan', 50))
- # write_demand_items_to_mysql(execution_id=8, merge_level2='贪污腐败')
|