proposal_generator.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. """
  2. proposal_generator.py - 投标内容生成模块
  3. 根据招标分析结果(AnalysisResult),使用OpenAI生成对应的投标响应内容。
  4. 支持按类别采用不同生成策略,可并行调用API以提高性能。
  5. """
  6. import json
  7. import logging
  8. import re
  9. from concurrent.futures import ThreadPoolExecutor, as_completed
  10. from typing import Optional
  11. from openai import OpenAI
  12. from .config import get_config
  13. from .models import AnalysisResult, ProposalDocument, ProposalSection, Requirement
  14. logger = logging.getLogger(__name__)
  15. # 类别对应的生成策略提示词
  16. _CATEGORY_PROMPTS = {
  17. "evaluation": (
  18. "你正在撰写一份投标文件的评分响应部分。请针对以下评审因素,提供具体、可量化的响应方案。\n"
  19. "内容要求:\n"
  20. "1. 明确响应评审指标,逐条对应\n"
  21. "2. 提供具体的技术方案、组织措施或管理方法\n"
  22. "3. 如有分值权重,重点突出高分项\n"
  23. "4. 强调我方优势和经验\n"
  24. "5. 语言正式、专业,符合标书风格"
  25. ),
  26. "service": (
  27. "你正在撰写一份投标文件的服务方案部分。请针对以下服务要求,制定详细的服务方案。\n"
  28. "内容要求:\n"
  29. "1. 详细描述服务方案、实施流程和质量标准\n"
  30. "2. 说明服务团队配置和职责分工\n"
  31. "3. 列出服务承诺和保障措施\n"
  32. "4. 包含服务响应时限和SLA指标\n"
  33. "5. 可适当引用过往成功案例"
  34. ),
  35. "qualification": (
  36. "你正在撰写一份投标文件的资质响应部分。请针对以下资质要求,说明我方满足条件的证明材料。\n"
  37. "内容要求:\n"
  38. "1. 明确说明我方具备的相应资质和证书\n"
  39. "2. 提供资质等级、编号、有效期等关键信息\n"
  40. "3. 说明资质对应的业务范围和能力\n"
  41. "4. 附上证明材料清单\n"
  42. "5. 如暂无某项资质,说明已申请或替代方案"
  43. ),
  44. "other": (
  45. "你正在撰写一份投标文件的响应部分。请针对以下要求,提供具体、专业的响应内容。\n"
  46. "内容要求:\n"
  47. "1. 直接回应招标要求,不回避、不跑题\n"
  48. "2. 内容具体可行,避免空泛承诺\n"
  49. "3. 语言正式、专业、规范\n"
  50. "4. 适当使用数据和事实支撑"
  51. ),
  52. }
  53. _DEFAULT_CATEGORY_PROMPT = (
  54. "你正在撰写一份投标文件的响应部分。请针对以下要求,提供具体、专业的响应内容。\n"
  55. "内容要求:\n"
  56. "1. 直接回应招标要求,不回避、不跑题\n"
  57. "2. 内容具体可行,避免空泛承诺\n"
  58. "3. 语言正式、专业、规范\n"
  59. "4. 适当使用数据和事实支撑"
  60. )
  61. # JSON Schema for structured output
  62. _SECTION_SCHEMA = {
  63. "name": "proposal_section",
  64. "strict": True,
  65. "schema": {
  66. "type": "object",
  67. "properties": {
  68. "title": {"type": "string"},
  69. "content": {"type": "string"},
  70. "requirement_ref": {"type": "string"},
  71. "level": {"type": "number"},
  72. },
  73. "required": ["title", "content", "requirement_ref", "level"],
  74. "additionalProperties": False,
  75. },
  76. }
  77. _COVER_LETTER_SYSTEM_PROMPT = (
  78. "你是一个专业的投标文件撰写专家。请根据项目名称,撰写一份正式的投标函。\n"
  79. "投标函应包含以下要素:\n"
  80. "1. 致送单位(使用'致:招标人/采购代理机构')\n"
  81. "2. 投标意向声明\n"
  82. "3. 报价承诺(标明'按招标文件要求报价')\n"
  83. "4. 工期/交货期承诺\n"
  84. "5. 质量保证承诺\n"
  85. "6. 投标有效期承诺\n"
  86. "7. 法定代表人签字和公司盖章声明\n"
  87. "使用正式的公文格式,语言庄重得体。"
  88. )
  89. _SUMMARY_SYSTEM_PROMPT = (
  90. "你是一个专业的投标文件撰写专家。请根据招标分析结果和已生成的投标内容,"
  91. "撰写一份投标概要(编制说明)。\n"
  92. "概要应包含:\n"
  93. "1. 项目背景和理解\n"
  94. "2. 我方优势概述\n"
  95. "3. 本投标文件的编制思路和结构\n"
  96. "4. 核心承诺\n"
  97. "语言简洁有力,突出我方优势。"
  98. )
  99. class ProposalGenerator:
  100. """投标内容生成器。
  101. 根据招标分析结果,使用OpenAI生成对应的投标响应内容。
  102. 支持结构化输出(JSON Schema),可按类别采用不同生成策略。
  103. """
  104. def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None):
  105. config = get_config()
  106. self.client = OpenAI(
  107. api_key=api_key or config.openai_api_key,
  108. timeout=config.openai_timeout,
  109. )
  110. self.model = model or config.openai_model
  111. self.temperature = config.openai_temperature
  112. self.max_tokens = config.openai_max_tokens
  113. def generate(self, analysis: AnalysisResult) -> ProposalDocument:
  114. """根据招标要求生成完整的投标内容。
  115. 对每个 Requirement 调用API生成响应,同时生成投标函和概要。
  116. 多个要求的生成任务并行执行以提高效率。
  117. Args:
  118. analysis: 招标分析结果
  119. Returns:
  120. ProposalDocument: 完整的投标内容
  121. Raises:
  122. ValueError: 如果API密钥未配置或analysis为空
  123. RuntimeError: 如果所有要求生成均失败
  124. """
  125. if not self.client.api_key:
  126. raise ValueError(
  127. "OpenAI API密钥未配置。请设置 OPENAI_API_KEY 环境变量或在调用时传入 api_key。"
  128. )
  129. project_name = analysis.project_name or "未命名项目"
  130. sections: list[ProposalSection] = []
  131. errors: list[str] = []
  132. # 获取所有要求并生成响应
  133. all_reqs = analysis.all_requirements
  134. logger.info(
  135. "开始生成投标内容:项目 '%s',共 %d 项要求",
  136. project_name,
  137. len(all_reqs),
  138. )
  139. # 并行生成各个要求的响应
  140. with ThreadPoolExecutor(max_workers=min(len(all_reqs), 10)) as executor:
  141. future_map = {
  142. executor.submit(self._generate_section, req): req
  143. for req in all_reqs
  144. }
  145. for future in as_completed(future_map):
  146. req = future_map[future]
  147. try:
  148. section = future.result()
  149. sections.append(section)
  150. logger.info("生成完成:%s", section.title)
  151. except Exception as e:
  152. error_msg = f"要求 '{req.title[:40]}...' 生成失败: {e}"
  153. errors.append(error_msg)
  154. logger.error(error_msg)
  155. # 按层级排序,保持文档结构合理
  156. sections.sort(key=lambda s: (s.level, s.title))
  157. if not sections:
  158. raise RuntimeError(
  159. "所有要求的响应生成均失败。\n " + "\n ".join(errors)
  160. )
  161. # 生成投标函
  162. logger.info("开始生成投标函...")
  163. try:
  164. cover_letter = self._generate_cover_letter(project_name)
  165. logger.info("投标函生成完成")
  166. except Exception as e:
  167. cover_letter = self._fallback_cover_letter(project_name)
  168. logger.warning("投标函AI生成失败,使用模板: %s", e)
  169. # 添加投标函作为首个章节
  170. cover_section = ProposalSection(
  171. title="投标函",
  172. level=1,
  173. content=cover_letter,
  174. requirement_ref="投标函",
  175. )
  176. sections.insert(0, cover_section)
  177. # 构建完整的投标文档
  178. proposal = ProposalDocument(
  179. title=project_name,
  180. project_name=project_name,
  181. sections=sections,
  182. )
  183. # 生成投标概要
  184. logger.info("开始生成投标概要...")
  185. try:
  186. proposal.summary = self._generate_summary(analysis, proposal)
  187. logger.info("投标概要生成完成")
  188. except Exception as e:
  189. proposal.summary = self._fallback_summary(analysis)
  190. logger.warning("投标概要AI生成失败,使用模板: %s", e)
  191. if errors:
  192. logger.warning(
  193. "部分要求生成失败(%d/%d):\n %s",
  194. len(errors),
  195. len(all_reqs),
  196. "\n ".join(errors),
  197. )
  198. return proposal
  199. def _build_section_prompt(self, req: Requirement) -> str:
  200. """根据要求类别构建对应的系统提示词。"""
  201. base_prompt = _CATEGORY_PROMPTS.get(req.category, _DEFAULT_CATEGORY_PROMPT)
  202. score_hint = ""
  203. if req.score is not None and req.score > 0:
  204. score_hint = f"\n【分值权重】此项评审分值为 {req.score} 分,请重点响应,确保覆盖所有得分点。"
  205. detail_hint = ""
  206. if req.detail:
  207. detail_hint = f"\n【详细说明】{req.detail}"
  208. return (
  209. f"{base_prompt}"
  210. f"\n\n【要求类别】{req.category}"
  211. f"{score_hint}"
  212. f"{detail_hint}"
  213. )
  214. def _generate_section(self, req: Requirement) -> ProposalSection:
  215. """为单个要求生成响应内容。
  216. Args:
  217. req: 招标要求
  218. Returns:
  219. ProposalSection: 生成的响应章节
  220. Raises:
  221. Exception: API调用失败时抛出
  222. """
  223. system_prompt = self._build_section_prompt(req)
  224. user_prompt = (
  225. f"请针对以下招标要求撰写响应内容:\n\n"
  226. f"【要求标题】{req.title}\n"
  227. f"【要求描述】{req.description}\n\n"
  228. f"请按 JSON Schema 格式返回,包含以下字段:\n"
  229. f"- title: 响应章节标题\n"
  230. f"- content: 响应正文(使用 markdown 格式,可包含段落、列表、表格等)\n"
  231. f"- requirement_ref: 引用原文要求(简明扼要)\n"
  232. f"- level: 章节层级(1=一级标题, 2=二级标题, 3=三级标题)"
  233. )
  234. try:
  235. response = self.client.chat.completions.create(
  236. model=self.model,
  237. messages=[
  238. {"role": "system", "content": system_prompt},
  239. {"role": "user", "content": user_prompt},
  240. ],
  241. response_format={
  242. "type": "json_schema",
  243. "json_schema": _SECTION_SCHEMA,
  244. },
  245. temperature=self.temperature,
  246. max_tokens=self.max_tokens,
  247. )
  248. result = json.loads(response.choices[0].message.content)
  249. return ProposalSection(
  250. title=result.get("title", f"响应:{req.title[:30]}"),
  251. content=result.get("content", ""),
  252. requirement_ref=result.get("requirement_ref", req.title),
  253. level=result.get("level", 2),
  254. )
  255. except Exception as e:
  256. logger.error(
  257. "生成要求 '%s...' 的响应时出错: %s",
  258. req.title[:50],
  259. e,
  260. )
  261. raise
  262. def _generate_cover_letter(self, project_name: str) -> str:
  263. """生成投标函。
  264. Args:
  265. project_name: 项目名称
  266. Returns:
  267. str: 投标函文本(Markdown格式)
  268. """
  269. try:
  270. response = self.client.chat.completions.create(
  271. model=self.model,
  272. messages=[
  273. {"role": "system", "content": _COVER_LETTER_SYSTEM_PROMPT},
  274. {
  275. "role": "user",
  276. "content": (
  277. f"请为以下项目撰写正式投标函:\n\n"
  278. f"项目名称:{project_name}\n\n"
  279. f"使用正式的公文格式撰写,包含投标人信息(请使用'我方'代称),"
  280. f"语言庄重得体。"
  281. ),
  282. },
  283. ],
  284. temperature=self.temperature,
  285. max_tokens=self.max_tokens,
  286. )
  287. return response.choices[0].message.content.strip()
  288. except Exception as e:
  289. logger.error("投标函生成失败: %s", e)
  290. raise
  291. def _generate_summary(
  292. self, analysis: AnalysisResult, proposal: Optional[ProposalDocument] = None
  293. ) -> str:
  294. """生成投标概要/编制说明。
  295. Args:
  296. analysis: 招标分析结果
  297. proposal: 已生成的投标内容(可选,用于参考)
  298. Returns:
  299. str: 投标概要文本(Markdown格式)
  300. """
  301. sections_summary = ""
  302. if proposal and proposal.sections:
  303. section_titles = "\n".join(
  304. f"- {s.title}" for s in proposal.sections[:10]
  305. )
  306. sections_summary = f"\n本投标文件包含以下章节:\n{section_titles}"
  307. try:
  308. response = self.client.chat.completions.create(
  309. model=self.model,
  310. messages=[
  311. {"role": "system", "content": _SUMMARY_SYSTEM_PROMPT},
  312. {
  313. "role": "user",
  314. "content": (
  315. f"项目名称:{analysis.project_name or '未命名'}\n"
  316. f"招标概要:{analysis.summary}\n\n"
  317. f"请撰写投标概要/编制说明。"
  318. ),
  319. },
  320. ],
  321. temperature=self.temperature,
  322. max_tokens=self.max_tokens,
  323. )
  324. return response.choices[0].message.content.strip()
  325. except Exception as e:
  326. logger.error("投标概要生成失败: %s", e)
  327. raise
  328. def _fallback_cover_letter(self, project_name: str) -> str:
  329. """当API不可用时使用的备用投标函模板。"""
  330. import datetime
  331. today = datetime.date.today().strftime("%Y年%m月%d日")
  332. return (
  333. f"# 投标函\n\n"
  334. f"致:招标人/采购代理机构\n\n"
  335. f"我方仔细研究了《{project_name}》招标文件的全部内容,决定参与本项目的投标。\n\n"
  336. f"1. 我方愿意按照招标文件要求,承担本项目的实施工作。\n\n"
  337. f"2. 我方承诺按招标文件规定的工期和质量标准完成本项目。\n\n"
  338. f"3. 我方承诺本投标文件在投标有效期内保持有效。\n\n"
  339. f"4. 我方郑重声明,所递交的投标文件及有关资料内容完整、真实、准确。\n\n"
  340. f"5. 我方承诺将严格按照合同约定履行各项义务。\n\n"
  341. f"特此函告。\n\n"
  342. f"投标人:我方\n"
  343. f"日期:{today}"
  344. )
  345. def _fallback_summary(self, analysis: AnalysisResult) -> str:
  346. """当API不可用时使用的备用概要模板。"""
  347. all_reqs = analysis.all_requirements
  348. categories = set(r.category for r in all_reqs)
  349. category_names = {
  350. "evaluation": "评审标准响应",
  351. "service": "服务方案",
  352. "qualification": "资质证明",
  353. "other": "其他响应",
  354. }
  355. category_strs = [category_names.get(c, c) for c in categories if c in category_names]
  356. return (
  357. f"## 编制说明\n\n"
  358. f"### 1. 项目概述\n\n"
  359. f"本项目为「{analysis.project_name or '本次招标项目'}」,"
  360. f"我方在认真研究招标文件的基础上,编制本投标文件。\n\n"
  361. f"### 2. 编制依据\n\n"
  362. f"- 本项目招标文件及其补充文件\n"
  363. f"- 国家相关法律法规和技术标准\n"
  364. f"- 我方多年来在相关领域的项目经验\n\n"
  365. f"### 3. 投标文件结构\n\n"
  366. f"本投标文件针对以下类别的要求进行了详细响应:\n"
  367. + "\n".join(f"- {cat}" for cat in category_strs) +
  368. f"\n\n### 4. 核心承诺\n\n"
  369. f"我方郑重承诺,将严格按照招标文件和合同约定,"
  370. f"以优质的服务和专业技术,确保项目圆满完成。"
  371. )