| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- """
- proposal_generator.py - 投标内容生成模块
- 根据招标分析结果(AnalysisResult),使用OpenAI生成对应的投标响应内容。
- 支持按类别采用不同生成策略,可并行调用API以提高性能。
- """
- import json
- import logging
- import re
- from concurrent.futures import ThreadPoolExecutor, as_completed
- from typing import Optional
- from openai import OpenAI
- from .config import get_config
- from .models import AnalysisResult, ProposalDocument, ProposalSection, Requirement
- logger = logging.getLogger(__name__)
- # 类别对应的生成策略提示词
- _CATEGORY_PROMPTS = {
- "evaluation": (
- "你正在撰写一份投标文件的评分响应部分。请针对以下评审因素,提供具体、可量化的响应方案。\n"
- "内容要求:\n"
- "1. 明确响应评审指标,逐条对应\n"
- "2. 提供具体的技术方案、组织措施或管理方法\n"
- "3. 如有分值权重,重点突出高分项\n"
- "4. 强调我方优势和经验\n"
- "5. 语言正式、专业,符合标书风格"
- ),
- "service": (
- "你正在撰写一份投标文件的服务方案部分。请针对以下服务要求,制定详细的服务方案。\n"
- "内容要求:\n"
- "1. 详细描述服务方案、实施流程和质量标准\n"
- "2. 说明服务团队配置和职责分工\n"
- "3. 列出服务承诺和保障措施\n"
- "4. 包含服务响应时限和SLA指标\n"
- "5. 可适当引用过往成功案例"
- ),
- "qualification": (
- "你正在撰写一份投标文件的资质响应部分。请针对以下资质要求,说明我方满足条件的证明材料。\n"
- "内容要求:\n"
- "1. 明确说明我方具备的相应资质和证书\n"
- "2. 提供资质等级、编号、有效期等关键信息\n"
- "3. 说明资质对应的业务范围和能力\n"
- "4. 附上证明材料清单\n"
- "5. 如暂无某项资质,说明已申请或替代方案"
- ),
- "other": (
- "你正在撰写一份投标文件的响应部分。请针对以下要求,提供具体、专业的响应内容。\n"
- "内容要求:\n"
- "1. 直接回应招标要求,不回避、不跑题\n"
- "2. 内容具体可行,避免空泛承诺\n"
- "3. 语言正式、专业、规范\n"
- "4. 适当使用数据和事实支撑"
- ),
- }
- _DEFAULT_CATEGORY_PROMPT = (
- "你正在撰写一份投标文件的响应部分。请针对以下要求,提供具体、专业的响应内容。\n"
- "内容要求:\n"
- "1. 直接回应招标要求,不回避、不跑题\n"
- "2. 内容具体可行,避免空泛承诺\n"
- "3. 语言正式、专业、规范\n"
- "4. 适当使用数据和事实支撑"
- )
- # JSON Schema for structured output
- _SECTION_SCHEMA = {
- "name": "proposal_section",
- "strict": True,
- "schema": {
- "type": "object",
- "properties": {
- "title": {"type": "string"},
- "content": {"type": "string"},
- "requirement_ref": {"type": "string"},
- "level": {"type": "number"},
- },
- "required": ["title", "content", "requirement_ref", "level"],
- "additionalProperties": False,
- },
- }
- _COVER_LETTER_SYSTEM_PROMPT = (
- "你是一个专业的投标文件撰写专家。请根据项目名称,撰写一份正式的投标函。\n"
- "投标函应包含以下要素:\n"
- "1. 致送单位(使用'致:招标人/采购代理机构')\n"
- "2. 投标意向声明\n"
- "3. 报价承诺(标明'按招标文件要求报价')\n"
- "4. 工期/交货期承诺\n"
- "5. 质量保证承诺\n"
- "6. 投标有效期承诺\n"
- "7. 法定代表人签字和公司盖章声明\n"
- "使用正式的公文格式,语言庄重得体。"
- )
- _SUMMARY_SYSTEM_PROMPT = (
- "你是一个专业的投标文件撰写专家。请根据招标分析结果和已生成的投标内容,"
- "撰写一份投标概要(编制说明)。\n"
- "概要应包含:\n"
- "1. 项目背景和理解\n"
- "2. 我方优势概述\n"
- "3. 本投标文件的编制思路和结构\n"
- "4. 核心承诺\n"
- "语言简洁有力,突出我方优势。"
- )
- class ProposalGenerator:
- """投标内容生成器。
- 根据招标分析结果,使用OpenAI生成对应的投标响应内容。
- 支持结构化输出(JSON Schema),可按类别采用不同生成策略。
- """
- def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None):
- config = get_config()
- self.client = OpenAI(
- api_key=api_key or config.openai_api_key,
- timeout=config.openai_timeout,
- )
- self.model = model or config.openai_model
- self.temperature = config.openai_temperature
- self.max_tokens = config.openai_max_tokens
- def generate(self, analysis: AnalysisResult) -> ProposalDocument:
- """根据招标要求生成完整的投标内容。
- 对每个 Requirement 调用API生成响应,同时生成投标函和概要。
- 多个要求的生成任务并行执行以提高效率。
- Args:
- analysis: 招标分析结果
- Returns:
- ProposalDocument: 完整的投标内容
- Raises:
- ValueError: 如果API密钥未配置或analysis为空
- RuntimeError: 如果所有要求生成均失败
- """
- if not self.client.api_key:
- raise ValueError(
- "OpenAI API密钥未配置。请设置 OPENAI_API_KEY 环境变量或在调用时传入 api_key。"
- )
- project_name = analysis.project_name or "未命名项目"
- sections: list[ProposalSection] = []
- errors: list[str] = []
- # 获取所有要求并生成响应
- all_reqs = analysis.all_requirements
- logger.info(
- "开始生成投标内容:项目 '%s',共 %d 项要求",
- project_name,
- len(all_reqs),
- )
- # 并行生成各个要求的响应
- with ThreadPoolExecutor(max_workers=min(len(all_reqs), 10)) as executor:
- future_map = {
- executor.submit(self._generate_section, req): req
- for req in all_reqs
- }
- for future in as_completed(future_map):
- req = future_map[future]
- try:
- section = future.result()
- sections.append(section)
- logger.info("生成完成:%s", section.title)
- except Exception as e:
- error_msg = f"要求 '{req.title[:40]}...' 生成失败: {e}"
- errors.append(error_msg)
- logger.error(error_msg)
- # 按层级排序,保持文档结构合理
- sections.sort(key=lambda s: (s.level, s.title))
- if not sections:
- raise RuntimeError(
- "所有要求的响应生成均失败。\n " + "\n ".join(errors)
- )
- # 生成投标函
- logger.info("开始生成投标函...")
- try:
- cover_letter = self._generate_cover_letter(project_name)
- logger.info("投标函生成完成")
- except Exception as e:
- cover_letter = self._fallback_cover_letter(project_name)
- logger.warning("投标函AI生成失败,使用模板: %s", e)
- # 添加投标函作为首个章节
- cover_section = ProposalSection(
- title="投标函",
- level=1,
- content=cover_letter,
- requirement_ref="投标函",
- )
- sections.insert(0, cover_section)
- # 构建完整的投标文档
- proposal = ProposalDocument(
- title=project_name,
- project_name=project_name,
- sections=sections,
- )
- # 生成投标概要
- logger.info("开始生成投标概要...")
- try:
- proposal.summary = self._generate_summary(analysis, proposal)
- logger.info("投标概要生成完成")
- except Exception as e:
- proposal.summary = self._fallback_summary(analysis)
- logger.warning("投标概要AI生成失败,使用模板: %s", e)
- if errors:
- logger.warning(
- "部分要求生成失败(%d/%d):\n %s",
- len(errors),
- len(all_reqs),
- "\n ".join(errors),
- )
- return proposal
- def _build_section_prompt(self, req: Requirement) -> str:
- """根据要求类别构建对应的系统提示词。"""
- base_prompt = _CATEGORY_PROMPTS.get(req.category, _DEFAULT_CATEGORY_PROMPT)
- score_hint = ""
- if req.score is not None and req.score > 0:
- score_hint = f"\n【分值权重】此项评审分值为 {req.score} 分,请重点响应,确保覆盖所有得分点。"
- detail_hint = ""
- if req.detail:
- detail_hint = f"\n【详细说明】{req.detail}"
- return (
- f"{base_prompt}"
- f"\n\n【要求类别】{req.category}"
- f"{score_hint}"
- f"{detail_hint}"
- )
- def _generate_section(self, req: Requirement) -> ProposalSection:
- """为单个要求生成响应内容。
- Args:
- req: 招标要求
- Returns:
- ProposalSection: 生成的响应章节
- Raises:
- Exception: API调用失败时抛出
- """
- system_prompt = self._build_section_prompt(req)
- user_prompt = (
- f"请针对以下招标要求撰写响应内容:\n\n"
- f"【要求标题】{req.title}\n"
- f"【要求描述】{req.description}\n\n"
- f"请按 JSON Schema 格式返回,包含以下字段:\n"
- f"- title: 响应章节标题\n"
- f"- content: 响应正文(使用 markdown 格式,可包含段落、列表、表格等)\n"
- f"- requirement_ref: 引用原文要求(简明扼要)\n"
- f"- level: 章节层级(1=一级标题, 2=二级标题, 3=三级标题)"
- )
- try:
- response = self.client.chat.completions.create(
- model=self.model,
- messages=[
- {"role": "system", "content": system_prompt},
- {"role": "user", "content": user_prompt},
- ],
- response_format={
- "type": "json_schema",
- "json_schema": _SECTION_SCHEMA,
- },
- temperature=self.temperature,
- max_tokens=self.max_tokens,
- )
- result = json.loads(response.choices[0].message.content)
- return ProposalSection(
- title=result.get("title", f"响应:{req.title[:30]}"),
- content=result.get("content", ""),
- requirement_ref=result.get("requirement_ref", req.title),
- level=result.get("level", 2),
- )
- except Exception as e:
- logger.error(
- "生成要求 '%s...' 的响应时出错: %s",
- req.title[:50],
- e,
- )
- raise
- def _generate_cover_letter(self, project_name: str) -> str:
- """生成投标函。
- Args:
- project_name: 项目名称
- Returns:
- str: 投标函文本(Markdown格式)
- """
- try:
- response = self.client.chat.completions.create(
- model=self.model,
- messages=[
- {"role": "system", "content": _COVER_LETTER_SYSTEM_PROMPT},
- {
- "role": "user",
- "content": (
- f"请为以下项目撰写正式投标函:\n\n"
- f"项目名称:{project_name}\n\n"
- f"使用正式的公文格式撰写,包含投标人信息(请使用'我方'代称),"
- f"语言庄重得体。"
- ),
- },
- ],
- temperature=self.temperature,
- max_tokens=self.max_tokens,
- )
- return response.choices[0].message.content.strip()
- except Exception as e:
- logger.error("投标函生成失败: %s", e)
- raise
- def _generate_summary(
- self, analysis: AnalysisResult, proposal: Optional[ProposalDocument] = None
- ) -> str:
- """生成投标概要/编制说明。
- Args:
- analysis: 招标分析结果
- proposal: 已生成的投标内容(可选,用于参考)
- Returns:
- str: 投标概要文本(Markdown格式)
- """
- sections_summary = ""
- if proposal and proposal.sections:
- section_titles = "\n".join(
- f"- {s.title}" for s in proposal.sections[:10]
- )
- sections_summary = f"\n本投标文件包含以下章节:\n{section_titles}"
- try:
- response = self.client.chat.completions.create(
- model=self.model,
- messages=[
- {"role": "system", "content": _SUMMARY_SYSTEM_PROMPT},
- {
- "role": "user",
- "content": (
- f"项目名称:{analysis.project_name or '未命名'}\n"
- f"招标概要:{analysis.summary}\n\n"
- f"请撰写投标概要/编制说明。"
- ),
- },
- ],
- temperature=self.temperature,
- max_tokens=self.max_tokens,
- )
- return response.choices[0].message.content.strip()
- except Exception as e:
- logger.error("投标概要生成失败: %s", e)
- raise
- def _fallback_cover_letter(self, project_name: str) -> str:
- """当API不可用时使用的备用投标函模板。"""
- import datetime
- today = datetime.date.today().strftime("%Y年%m月%d日")
- return (
- f"# 投标函\n\n"
- f"致:招标人/采购代理机构\n\n"
- f"我方仔细研究了《{project_name}》招标文件的全部内容,决定参与本项目的投标。\n\n"
- f"1. 我方愿意按照招标文件要求,承担本项目的实施工作。\n\n"
- f"2. 我方承诺按招标文件规定的工期和质量标准完成本项目。\n\n"
- f"3. 我方承诺本投标文件在投标有效期内保持有效。\n\n"
- f"4. 我方郑重声明,所递交的投标文件及有关资料内容完整、真实、准确。\n\n"
- f"5. 我方承诺将严格按照合同约定履行各项义务。\n\n"
- f"特此函告。\n\n"
- f"投标人:我方\n"
- f"日期:{today}"
- )
- def _fallback_summary(self, analysis: AnalysisResult) -> str:
- """当API不可用时使用的备用概要模板。"""
- all_reqs = analysis.all_requirements
- categories = set(r.category for r in all_reqs)
- category_names = {
- "evaluation": "评审标准响应",
- "service": "服务方案",
- "qualification": "资质证明",
- "other": "其他响应",
- }
- category_strs = [category_names.get(c, c) for c in categories if c in category_names]
- return (
- f"## 编制说明\n\n"
- f"### 1. 项目概述\n\n"
- f"本项目为「{analysis.project_name or '本次招标项目'}」,"
- f"我方在认真研究招标文件的基础上,编制本投标文件。\n\n"
- f"### 2. 编制依据\n\n"
- f"- 本项目招标文件及其补充文件\n"
- f"- 国家相关法律法规和技术标准\n"
- f"- 我方多年来在相关领域的项目经验\n\n"
- f"### 3. 投标文件结构\n\n"
- f"本投标文件针对以下类别的要求进行了详细响应:\n"
- + "\n".join(f"- {cat}" for cat in category_strs) +
- f"\n\n### 4. 核心承诺\n\n"
- f"我方郑重承诺,将严格按照招标文件和合同约定,"
- f"以优质的服务和专业技术,确保项目圆满完成。"
- )
|