company_info_extractor.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. """
  2. 公司信息提取器
  3. 从通用标书生成资料中提取结构化的公司基本信息。
  4. 使用 LLM 一次性提取,结果缓存。
  5. """
  6. from __future__ import annotations
  7. import logging
  8. from typing import Dict, List, Optional
  9. from llm_client import LLMClient
  10. from models import CompanyInfo, ParsedDocument
  11. logger = logging.getLogger(__name__)
  12. def extract_company_info(
  13. general_materials: List[ParsedDocument],
  14. ) -> CompanyInfo:
  15. """从通用标书资料中提取结构化公司信息
  16. 优先从"企业基本情况简介"类 MD 文件中提取。
  17. Args:
  18. general_materials: Step 1 加载的通用资料文档列表
  19. Returns:
  20. CompanyInfo — 结构化公司信息
  21. """
  22. # 1. 优先找公司简介类文档
  23. intro_text = ""
  24. for doc in general_materials:
  25. if not doc.content:
  26. continue
  27. name = doc.file_name.lower()
  28. if any(kw in name for kw in ["企业基本情况", "公司简介", "企业介绍", "公司概况"]):
  29. intro_text = doc.content
  30. break
  31. # 2. 降级:合并所有短文档
  32. if not intro_text:
  33. combined = []
  34. for doc in general_materials:
  35. if doc.content:
  36. combined.append(f"--- {doc.file_name} ---\n{doc.content[:3000]}")
  37. intro_text = "\n\n".join(combined)
  38. if not intro_text:
  39. logger.warning("未找到公司信息资料,返回空 CompanyInfo")
  40. return CompanyInfo()
  41. # 3. LLM 提取
  42. llm = LLMClient()
  43. prompt = """请从以下公司资料中提取公司基本信息,以 JSON 格式输出。
  44. 如果某项信息未找到,请填空字符串""。
  45. 输出格式:
  46. {
  47. "full_name": "公司全称",
  48. "short_name": "公司简称",
  49. "address": "注册地址或办公地址",
  50. "phone": "联系电话",
  51. "fax": "传真",
  52. "zip_code": "邮编",
  53. "bank_name": "开户银行",
  54. "bank_account": "银行账号",
  55. "legal_rep": "法定代表人姓名",
  56. "legal_rep_id": "法定代表人身份证号",
  57. "registration_capital": "注册资本",
  58. "unified_code": "统一社会信用代码",
  59. "website": "公司官网"
  60. }
  61. 注意:
  62. - 公司全称必须与营业执照一致,不要删减或添加文字
  63. - 地址必须完整,包含省市(直辖)区
  64. - 电话格式保留原样"""
  65. try:
  66. result = llm.extract_json(
  67. system_prompt="你是企业信息提取专家。请从文本中准确提取公司基本信息。",
  68. user_prompt=f"{prompt}\n\n---\n{intro_text[:12000]}",
  69. max_tokens=32768,
  70. )
  71. if isinstance(result, dict):
  72. # LLM 可能返回 null(而非空字符串):str(None) 会生成 "None"
  73. # 污染占位符替换结果,统一转回空字符串
  74. def _safe_str(v) -> str:
  75. if v is None:
  76. return ""
  77. s = str(v).strip()
  78. return "" if s.lower() in ("none", "null") else s
  79. info = CompanyInfo(
  80. full_name=_safe_str(result.get("full_name")),
  81. short_name=_safe_str(result.get("short_name")),
  82. address=_safe_str(result.get("address")),
  83. phone=_safe_str(result.get("phone")),
  84. fax=_safe_str(result.get("fax")),
  85. zip_code=_safe_str(result.get("zip_code")),
  86. bank_name=_safe_str(result.get("bank_name")),
  87. bank_account=_safe_str(result.get("bank_account")),
  88. legal_rep=_safe_str(result.get("legal_rep")),
  89. legal_rep_id=_safe_str(result.get("legal_rep_id")),
  90. registration_capital=_safe_str(result.get("registration_capital")),
  91. unified_code=_safe_str(result.get("unified_code")),
  92. website=_safe_str(result.get("website")),
  93. )
  94. logger.info(f"公司信息提取完成: {info.full_name}")
  95. return info
  96. else:
  97. logger.warning(f"公司信息提取返回非 dict: {type(result)}")
  98. return CompanyInfo()
  99. except Exception as e:
  100. logger.warning(f"公司信息提取失败: {e}")
  101. # 降级:正则提取
  102. return _fallback_extract(intro_text)
  103. def _fallback_extract(text: str) -> CompanyInfo:
  104. """降级方案:正则表达式提取公司名"""
  105. import re
  106. info = CompanyInfo()
  107. # 尝试匹配公司名(中文名+有限公司)
  108. m = re.search(r"((?:上海|北京|深圳|广州|杭州|南京|成都|武汉|重庆|天津)[一-龥()\(\)]{2,20}(?:有限公司|股份有限公司))", text)
  109. if m:
  110. info.full_name = m.group(1)
  111. # 地址
  112. m = re.search(r"(?:地址|注册地址|办公地址)[::]\s*(.+?)(?:[\n,。]|$)", text)
  113. if m:
  114. info.address = m.group(1).strip()
  115. # 电话
  116. m = re.search(r"(?:电话|联系电话|Tel)[::]\s*([\d\-]+)", text)
  117. if m:
  118. info.phone = m.group(1).strip()
  119. return info
  120. __all__ = ["extract_company_info"]