import argparse import csv import os from collections import Counter, defaultdict from datetime import datetime, timezone, timedelta from pathlib import Path def read_csv(path): with path.open("r", encoding="utf-8-sig", newline="") as handle: return list(csv.DictReader(handle)) def top(counter, n=10): return counter.most_common(n) def safe_cell(value): return (value or "").replace("\n", " ").strip() def relative_link(target, base_dir): return Path(os.path.relpath(target, base_dir)).as_posix() COMPANY_STOPWORDS = { "有色金属", "能源金属", "工业金属", "基本金属", "贵金属", "小金属", "金属新材料", "中重稀土", "矿产资源", "自然资源", "稀土资源", "COMEX黄金", "LME铜", "LME铝", "国家能源", "中国新能源", "基础化工", "稀土冶炼", "年全球稀土", "全球稀土", } def main(): parser = argparse.ArgumentParser() parser.add_argument("--project-root", default=".") parser.add_argument("--outputs-dir", required=True) parser.add_argument("--result-index", required=True) parser.add_argument("--fact-files", nargs="+", required=True) parser.add_argument("--company-files", nargs="+", required=True) parser.add_argument("--snapshot", nargs="+", required=True) args = parser.parse_args() project_root = Path(args.project_root).resolve() outputs_dir = project_root / args.outputs_dir outputs_dir.mkdir(parents=True, exist_ok=True) result_index = project_root / args.result_index created_at = datetime.now(timezone(timedelta(hours=8))).isoformat(timespec="seconds") facts = [] for file_name in args.fact_files: path = project_root / file_name if path.exists(): facts.extend(read_csv(path)) companies = [] for file_name in args.company_files: path = project_root / file_name if path.exists(): companies.extend(read_csv(path)) snapshot = [] for snapshot_file in args.snapshot: path = project_root / snapshot_file if path.exists(): snapshot.extend(read_csv(path)) metal_counter = Counter() theme_counter = Counter() metal_theme = defaultdict(Counter) example_by_metal = defaultdict(list) for row in facts: metals = [tag for tag in row.get("metal_tags", "").split("|") if tag] themes = [tag for tag in row.get("theme_tags", "").split("|") if tag] for metal in metals: metal_counter[metal] += 1 if len(example_by_metal[metal]) < 4: example_by_metal[metal].append(row) for theme in themes: metal_theme[metal][theme] += 1 for theme in themes: theme_counter[theme] += 1 company_counter = Counter() company_examples = {} for row in companies: name = row.get("company_name", "") if ( not name or len(name) > 14 or name in COMPANY_STOPWORDS or name.isdigit() or "本报告" in name or "所有材料" in name ): continue company_counter[name] += 1 company_examples.setdefault(name, row) by_metal_snapshot = defaultdict(list) for row in snapshot: by_metal_snapshot[row.get("metal", "")].append(row) industry_lines = [ "# 有色行业视图草稿", "", "状态:DRAFT_FOR_REVIEW", f"生成时间:{created_at}", "", "## 证据边界", "", "本文档由 BATCH-001 与 BATCH-003 的规则抽取事实句、BATCH-003 转换/抽取 manifest、外部价格库存快照共同生成。当前尚未完成关键事实段落、表格定位和人工复核,因此只作为核心文档草稿,不输出正式行业结论或交易建议。", "", "## 材料覆盖", "", f"- 规则抽取事实句:{len(facts)} 条", f"- 公司提及草表:{len(companies)} 条", "- BATCH-003 转换状态:329 个补登记 raw 中 309 个已转换、20 个进入转换缺口。", "", "## 金属主题分布", "", "| 金属 | 事实句数量 | 主要主题 |", "|---|---:|---|", ] for metal, count in top(metal_counter, 12): themes = ", ".join([f"{name}:{cnt}" for name, cnt in top(metal_theme[metal], 5)]) industry_lines.append(f"| {metal} | {count} | {themes} |") industry_lines.extend(["", "## 初步行业读法", ""]) for metal, _ in top(metal_counter, 8): themes = [name for name, _ in top(metal_theme[metal], 4)] industry_lines.append(f"### {metal}") industry_lines.append("") industry_lines.append(f"- 抽取材料中,{metal}相关句子主要集中在:{', '.join(themes) if themes else '待复核'}。") if example_by_metal[metal]: industry_lines.append("- 待复核样例:") for row in example_by_metal[metal][:2]: industry_lines.append(f" - {safe_cell(row.get('evidence_text'))}({row.get('doc_id')},{row.get('source_location')})") industry_lines.append("") industry_lines.extend( [ "## 下一步复核点", "", "1. 对价格、库存、供给、项目投产和政策相关事实补段落或表格定位。", "2. 将低置信度公司提及草表收敛为公司证据卡,补业务占比、资源/产能、成本位置和触发/失效条件。", "3. 外部价格库存快照需要按来源继续补 SHFE、COMEX、SMM 等可复核数值。", ] ) market_lines = [ "# 有色市场视图草稿", "", "状态:DRAFT_FOR_REVIEW", f"生成时间:{created_at}", "", "## 证据边界", "", "本文档仅汇总公开外部快照和研报抽取中的市场相关句子。涉及实时行情、新闻、市场表现或外部信息的内容按 supplement/evidence 分账,不作为正式交易判断。", "", "## 外部价格库存快照", "", "| 金属 | 指标 | 数值 | 单位 | 市场/地区 | 来源 |", "|---|---|---:|---|---|---|", ] for row in snapshot: market_lines.append( f"| {row.get('metal')} | {row.get('indicator')} | {row.get('value')} | {row.get('unit')} | {row.get('market_or_region')} | {row.get('source_name')} |" ) market_lines.extend(["", "## 市场变量分布", "", "| 主题 | 事实句数量 |", "|---|---:|"]) for theme, count in top(theme_counter, 12): market_lines.append(f"| {theme} | {count} |") market_lines.extend( [ "", "## 观察框架", "", "- 工业金属:优先跟踪价格、库存、冶炼/矿端供应、下游开工和宏观需求预期之间是否一致。", "- 贵金属:区分金银价格的宏观利率/美元逻辑与材料端白银需求逻辑。", "- 能源金属:锂、镍、钴需要把价格方向与库存、排产、产能释放和政策扰动分开验证。", "- 稀土/小金属:重点核对供给管制、出口、库存和高端制造需求,不用单一价格点推导结论。", "", "## 缺口", "", "1. SHFE 库存与国内现货价格仍需补可复核数值入口。", "2. 锂和稀土的部分价格为补充口径,仍需 SMM 或交易所/行业协会直接数值复核。", "3. 市场异常表现、新闻事件和政策事件需要后续按 darkline 分流形成独立证据链。", ] ) company_lines = [ "# 有色公司视图草稿", "", "状态:DRAFT_FOR_REVIEW", f"生成时间:{created_at}", "", "## 证据边界", "", "公司视图当前来自规则公司名扫描,置信度较低,只用于确定后续证据卡优先级。不得据此输出交易指令、收益承诺或无证据强结论。", "", "## 公司提及频次", "", "| 公司名片段 | 提及次数 | 样例定位 | 样例上下文 |", "|---|---:|---|---|", ] for name, count in top(company_counter, 40): row = company_examples[name] company_lines.append( f"| {name} | {count} | {row.get('doc_id')} / {row.get('source_location')} | {safe_cell(row.get('context_snippet'))[:120]} |" ) company_lines.extend( [ "", "## 后续公司证据卡字段", "", "1. 公司简称、证券代码、主营金属、核心资产或业务环节。", "2. 资源量、产能、成本位置、扩产/投产节奏、价格敏感性。", "3. 观察条件、触发条件、失效条件和主要风险。", "4. 原文页码/段落/表格定位、外部公告或数据来源、review_status。", ] ) industry_path = outputs_dir / "nonferrous_industry_view_draft.md" market_path = outputs_dir / "nonferrous_market_view_draft.md" company_path = outputs_dir / "nonferrous_company_view_draft.md" industry_path.write_text("\n".join(industry_lines) + "\n", encoding="utf-8") market_path.write_text("\n".join(market_lines) + "\n", encoding="utf-8") company_path.write_text("\n".join(company_lines) + "\n", encoding="utf-8") result_lines = [ "# ANA-YS-INDUSTRY-001 结果入口", "", "状态:DRAFT_FOR_REVIEW", f"更新时间:{created_at}", "", "## 核心文档", "", f"1. [有色行业视图草稿]({relative_link(industry_path, result_index.parent)})", f"2. [有色市场视图草稿]({relative_link(market_path, result_index.parent)})", f"3. [有色公司视图草稿]({relative_link(company_path, result_index.parent)})", "", "## 关键过程产物", "", "- `ana-data/cases/有色案例/ANA-YS-INDUSTRY-001/outputs/batch003_conversion_summary.md`", "- `ana-data/cases/有色案例/ANA-YS-INDUSTRY-001/outputs/batch003_extract001_summary.md`", "- `ana-data/cases/有色案例/ANA-YS-INDUSTRY-001/outputs/key_fact_review_batch003_pass001_summary.md`", "- `ana-data/cases/有色案例/ANA-YS-INDUSTRY-001/outputs/company_evidence_card_batch003_pass001_summary.md`", "- `ana-data/cases/有色案例/ANA-YS-INDUSTRY-001/outputs/price_inventory_supplement_pass002_summary.md`", "- `ana-data/cases/有色案例/supplement/nonferrous_price_inventory_snapshot_20260625.csv`", "- `ana-data/cases/有色案例/supplement/nonferrous_price_inventory_supplement_pass002_20260625.csv`", "- `ana-data/cases/有色案例/manifest/conversion_gap_batch003.csv`", "", "## 限制", "", "BATCH-001 与 BATCH-003 关键事实尚未完成段落/表格定位和人工复核。对外正式结论、公司投资读法或交付前,必须提交执行审核或输出审核。", ] result_index.parent.mkdir(parents=True, exist_ok=True) result_index.write_text("\n".join(result_lines) + "\n", encoding="utf-8") print(f"industry={industry_path.relative_to(project_root).as_posix()}") print(f"market={market_path.relative_to(project_root).as_posix()}") print(f"company={company_path.relative_to(project_root).as_posix()}") print(f"result_index={result_index.relative_to(project_root).as_posix()}") if __name__ == "__main__": main()