"""投标产线能力包(bidding_general):主 agent(驾驶舱)工具 + 角色定义 + 菜单 + slash 命令。 与开发产线(sdlc_general)同构,通过 pipeline-core 的 PipelineAbility 注册表挂载, 零侵入 core / AgentExecutor:项目 sd_projects.pipeline_id='bidding_general' 时自动生效。 角色任务流转不用 RoleSpec.next_role(全部留空),一律由 bid_flow 状态对账器驱动 —— 因为投标 产线是「一章一任务 + 两级回退循环」的扇出结构,不是线性任务链。 """ import json import logging from pipeline_core import ( ToolDefinition, PipelineAbility, RoleSpec, FlowStage, FlowTemplate, register_ability, SlashCommand, register_slash_command, ) logger = logging.getLogger("pipeline.bidding.ability") PIPELINE_ID = "bidding_general" # ══════════════════ 预制流程阶段声明(流程裁剪机制,pipeline_flow_plans 消费)══════════════════ # trim: no=不可裁(流程不成立)/ warn=可裁但确认待办里给风险提示(质量门禁类)/ yes=自由裁 # deps 只写「硬依赖」(裁了下游不成立);QC 类门禁的放行语义在 bid_flow 里处理 #(qc_analysis 被裁 → 章节依赖自动视为通过),不进 deps。 # roles 用于:① 确认时收敛取消被裁阶段在办任务(仅当角色不属于任何保留阶段); # ② 对账器按阶段门禁派发。 BID_FLOW_STAGES = [ FlowStage(key="analysis_scoring", label="评分项与得分规则解析", description="从招标文件抽取评分项、得分规则、否决项——全流程交叉核对基准", trim="no", roles=["agent.tender_analyst"]), FlowStage(key="analysis_quals", label="资质清单解析", description="抽取招标所需资质与要求(商务章与资料准备的上游)", trim="warn", warn_note="裁掉资质解析后,商务章编写与资质文件准备失去依据,商务分可能失分", roles=["agent.tender_analyst"], deps=["analysis_scoring"]), FlowStage(key="analysis_reqs_outline", label="投标文件要求与章节骨架", description="抽取投标文件格式/结构要求并生成章节骨架(评分项挂载)——所有章节编写的依据", trim="no", roles=["agent.tender_analyst"], deps=["analysis_scoring"]), FlowStage(key="analysis_cost_benefit", label="成本收益分析", description="投标成本与收益测算(保证金/预算/付款条件),报价章的上游", trim="yes", roles=["agent.tender_analyst"], deps=["analysis_scoring", "analysis_reqs_outline"]), FlowStage(key="qc_analysis", label="解析产出QC契合度审核", description="QC 对照招标文件原文逐类审核解析产出(10分制,不过退回重做)", trim="warn", warn_note="裁掉解析QC后,评分项/资质/骨架的抽取错误将不经核对直接进入编写阶段,可能整章返工", roles=["agent.qc"], deps=["analysis_scoring", "analysis_reqs_outline"]), FlowStage(key="prep", label="投标资料准备", description="知识库资质匹配 + 缺失资质的人类文件清单(只阻塞商务章)", trim="yes", roles=["agent.bid_prep"], deps=["analysis_quals"]), FlowStage(key="chapter_write", label="分章节编写", description="按章节骨架逐章编写(商务/技术分角色、并发派发)", trim="no", roles=["agent.bid_writer", "agent.bid_biz_writer"], deps=["analysis_reqs_outline"]), FlowStage(key="chapter_review", label="章节评审", description="每章对照评分项/投标文件要求评审打分,不达标退回重写(章节级质量循环)", trim="warn", warn_note="裁掉章节评审后,章节写完即通过,无质量把关,整书评分(若保留)大概率反复打回", roles=["agent.bid_reviewer"], deps=["chapter_write"]), FlowStage(key="compose", label="标书合成", description="全部章节通过后合成完整标书(md+docx)", trim="no", roles=["agent.bid_compositor"], deps=["chapter_write"]), FlowStage(key="whole_score", label="整书评分(模拟评标)", description="按评分项+得分规则给整书打分,不达标改进意见落到章节退回重写", trim="yes", roles=["agent.bid_scorer"], deps=["compose"]), FlowStage(key="delivery_confirm", label="交付确认", description="人工复核用印/密封/份数/递交后确认交付,项目完成", trim="yes", roles=[], deps=["compose"]), ] # ══════════════════ 纯技术方案流程(bid_tech_proposal,2026-09-14 批2)══════════════════ # 用户 2026-09-11 定义:输入=技术需求书(非招标文件),分析集中在技术层面六类评估项 # (功能清单/功能点/硬件配置/技术架构/功能架构/部署架构),方案书根据预定模版 # (无则 RAG/网络检索),模版骨架**由用户确认后**按章节分段编写和 QC 评审, # 最终合成产出技术方案书。QC/评审**以用户提供的需求书为唯一基准**(对标此文件)。 TECH_PROPOSAL_STAGES = [ FlowStage(key="tech_analysis", label="技术需求解析(六类评估项)", description="从技术需求书抽取六类评估项:功能清单/功能点/硬件配置/技术架构/功能架构/部署架构,落库 bid_tech_items", trim="no", roles=["agent.tender_analyst"]), FlowStage(key="tech_qc", label="技术评估项QC契合度审核", description="QC 对照技术需求书原文逐类审核评估项(10分制,不过退回重做)", trim="warn", warn_note="裁掉技术QC后,评估项抽取错误将不经核对直接进入骨架拟定与编写阶段,可能整章偏离需求", roles=["agent.qc"], deps=["tech_analysis"]), FlowStage(key="template_confirm", label="方案模版确定+用户确认骨架", description="预定模版(知识库)→ 无则 RAG 检索 → 无则网络搜索,拟定章节骨架(覆盖六类评估项),发待办由用户确认后才开写", trim="no", roles=["agent.pm"], deps=["tech_analysis"]), FlowStage(key="tech_chapter_write", label="技术方案分章编写", description="按用户确认的骨架逐章编写(一章一任务并发),内容对标需求书评估项写到操作层", trim="no", roles=["agent.bid_writer"], deps=["template_confirm"]), FlowStage(key="tech_chapter_qc", label="章节QC评审(对标需求书)", description="每章对照技术需求书评估项覆盖度评审打分,不达标退回重写(章节级质量循环)", trim="warn", warn_note="裁掉章节评审后,章节写完即通过,无需求覆盖度把关,方案书可能漏答需求条目", roles=["agent.bid_reviewer"], deps=["tech_chapter_write"]), FlowStage(key="tech_compose", label="合成技术方案书", description="全部章节通过后合成完整技术方案书(md+docx)", trim="no", roles=["agent.bid_compositor"], deps=["tech_chapter_write"]), FlowStage(key="tech_delivery_confirm", label="交付确认", description="人工核对技术方案书后确认交付,项目完成", trim="yes", roles=[], deps=["tech_compose"]), ] # 多流程模板声明(≥2 条时文件导入触发 LLM 自动判流 + 用户确认,2026-09-14 批2) BID_FLOW_TEMPLATES = [ FlowTemplate(key="bid_standard", name="标准投标流程", description="收到招标文件 → 四维度解析 → QC → 资料准备 → 分章编写 → 章节评审 → 合成标书 → 整书评分 → 交付", detect_criteria=( "输入文件是【招标文件/招标公告/投标邀请书】:含评标办法、评分标准、分值分配、" "投标人资格要求、投标保证金、投标文件格式/密封/份数要求、开标时间等招投标程序性内容," "最终产出应是一份响应评分标准的【投标文件(标书)】。"), stages=BID_FLOW_STAGES), FlowTemplate(key="bid_tech_proposal", name="纯技术方案流程", description="技术需求书 → 六类技术评估项解析 → QC → 模版骨架用户确认 → 分章编写 → 章节QC对标需求书 → 合成技术方案书", detect_criteria=( "输入文件是【技术需求书/需求规格说明书/产品规格书/技术方案要求】:主体内容是功能需求、" "功能清单、功能点、性能指标、硬件配置、技术架构/功能架构/部署架构要求、接口/安全/验收标准等," "**没有**评标办法/评分标准/投标程序等招投标内容,最终产出应是一份【技术方案书】而非标书。"), stages=TECH_PROPOSAL_STAGES), ] # ══════════════════ 工具定义(主 agent / 驾驶舱)══════════════════ BID_TOOLS = [ # ── 招标文件(产线起点:收到招标书直接启动,无需立项/配人步骤)── ToolDefinition(name="import_tender_file", description="导入招标文件并启动投标项目:用户提供了招标文件(附件/正文/文件路径)就调它。未建项目时自动立项(收到招标书直接启动),已有项目则导入到当前项目,系统自动开始解析", parameters={"content_text": "招标文件正文(用户提供时必填)", "file_path": "文件路径(与content_text二选一)", "file_name": "文件名(可选)", "project_name": "项目名称(可选,未建项目时自动立项用,缺省取文件名)"}, category="project"), ToolDefinition(name="create_bid_project", description="创建空投标项目(仅当用户明确要先建项目、稍后再给招标文件时用)。若用户已提供招标文件,直接用 import_tender_file 自动立项", parameters={"name": "项目名称", "description": "项目描述(可选)", "tender_content": "招标文件正文(用户已提供时必填,直接导入跳过上传步骤)", "tender_file_name": "招标文件名(可选)"}, category="project"), ToolDefinition(name="request_tender_file", description="仅当项目确实没有任何招标文件且需要人工上传时才发「等待上传」人类待办(同一事项只发一次)。若文件已在工作空间/已登记,禁止调用——用 import_tender_file 登记", parameters={"note": "说明(可选)"}, category="project"), ToolDefinition(name="list_tender_files", description="查看已上传的招标文件及抽取状态", parameters={}, category="project"), # ── 解析结果 ── ToolDefinition(name="list_scoring_items", description="查看评分项与得分规则(从招标文件抽取的结果)", parameters={"section": "评分部分(可选 business/technical/price)"}, category="analysis"), ToolDefinition(name="list_doc_requirements", description="查看投标文件要求(章节结构/格式/密封/份数/截止)", parameters={"req_type": "类型(可选 structure/format/seal/copies/deadline)"}, category="analysis"), ToolDefinition(name="list_qualifications", description="查看招标所需资质及匹配状态(matched/missing/human_required)", parameters={"match_status": "匹配状态(可选)"}, category="analysis"), ToolDefinition(name="qc_report", description="查看解析产出 QC 契合度审核总览(评分项/资质/投标文件要求/章节骨架各类的最新得分与放行状态)", parameters={}, category="analysis"), # ── 知识库 ── ToolDefinition(name="search_bid_kb", description="检索公司投标知识库(资质证书/业绩案例/模板/财务/人员)", parameters={"query": "关键词", "doc_type": "类型(可选 qualification/cert/case/template/finance/personnel)"}, category="kb"), ToolDefinition(name="list_kb_docs", description="列出公司投标知识库文档", parameters={"doc_type": "类型(可选)"}, category="kb"), ToolDefinition(name="add_kb_doc", description="向公司投标知识库录入文档(资质证书/业绩案例等)", parameters={"doc_name": "文档名称", "doc_type": "类型", "doc_no": "编号(可选)", "tags": "标签(可选)", "file_path": "文件路径(可选)", "customer": "案例客户(可选)", "contract_amount": "合同金额(可选)"}, category="kb"), ToolDefinition(name="request_human_docs", description="发布「人类应提供的文件」清单任务(知识库缺失的资质由人工提供)", parameters={"items": "清单(可选,默认自动汇总缺失资质)", "note": "说明(可选)"}, category="kb"), # ── 章节 / 标书 / 评分 ── ToolDefinition(name="list_chapters", description="查看投标文件章节及状态(pending/writing/written/approved/rejected)", parameters={"status": "状态(可选)"}, category="bid"), ToolDefinition(name="chapter_detail", description="查看章节详情(要点/对应评分项/评审意见/正文)", parameters={"chapter_id": "章节ID"}, category="bid"), ToolDefinition(name="list_bid_documents", description="查看已合成的标书版本及评分", parameters={}, category="bid"), ToolDefinition(name="list_reviews", description="查看评审记录(章节级/整书级)", parameters={"scope": "范围(可选 chapter/whole)", "chapter_id": "章节ID(可选)"}, category="bid"), ToolDefinition(name="bid_score_report", description="查看最近一轮整书评分报告(总分+逐项明细+改进意见)", parameters={}, category="bid"), # ── 材料缺口(占位符规范,2026-09-16)── ToolDefinition(name="list_material_gaps", description="查看投标材料缺失清单(正文占位符自动登记的缺口:章节/材料/状态)", parameters={"status": "状态(可选 pending/filled/waived)"}, category="bid"), ToolDefinition(name="material_gap_report", description="生成材料缺失清单报告(markdown 全文,合成标书时也会自动落盘)", parameters={}, category="bid"), ToolDefinition(name="fill_material_gap", description="补齐材料缺口:用户提供了材料内容/知识库文档后调用,自动替换章节正文占位符(approved 章节保持通过不重审)", parameters={"gap_id": "缺口ID", "fill_content": "材料正文(与kb_doc_id二选一)", "kb_doc_id": "知识库文档ID(与fill_content二选一)"}, category="bid"), ToolDefinition(name="waive_material_gap", description="放弃材料缺口(用户确认不提供该材料,正文占位符保留为如实披露)", parameters={"gap_id": "缺口ID", "reason": "放弃理由(必填)"}, category="bid"), # ── 诊断 / 推进 ── ToolDefinition(name="diagnose_bid", description="诊断投标项目:卡在哪个阶段、门禁、章节分布、在办任务、最近评分", parameters={}, category="agent"), ToolDefinition(name="advance_bid", description="立即触发投标流程状态对账(补派应有的角色任务,通常无需手动调用)", parameters={}, category="agent"), ToolDefinition(name="reset_task_retry", description="人工处理完失败任务后恢复重试:把失败/挂起的任务清零重试计数并重新执行", parameters={"task_id": "任务ID(从diagnose_bid看到的失败任务)"}, category="agent"), ] # ══════════════════ 产线 prompt 片段 ══════════════════ BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从招标文件到标书交付的全流程管理: 收到招标文件 → 自动立项并解析(评分项/得分规则/资质/投标文件要求/章节骨架) → 解析产出 QC 契合度审核 → 资质文档准备 → 分章节编写 → 章节评审 → 合成标书 → 整书评分 → 交付确认。 产线没有独立的「立项/配人」步骤:用户把招标文件(附件/正文/文件路径)交给你, 调 import_tender_file 即自动建项目并启动解析——不要先问用户「要不要建项目/安排人员」。 招标信息的采集/摘要/审批归商机产线,本产线从「收到招标文件」起步;用户要采集招标信息时说明去商机产线。 ## 流转由机制驱动(重要) 角色任务由系统状态对账器自动派发(每 15 秒一轮),你**不需要**手动为每个章节创建任务: - 解析产出须经 QC 契合度审核:评分标准/得分规则/资质/投标文件要求/章节骨架逐类对照招标文件原文 按 10 分制打分,得分高于通过分(当前 8.5)才放行;不通过给改进意见 → 清空产出 → 解析重做 → 重审 - 章节评审不达标 → 章节自动退回重写 - 整书评分不达标 → 改进意见自动落到问题章节并退回重写 → 重新合成 → 重新评分 你的职责是:收到招标文件立即导入启动、处理卡点与提问、回答评审/评分中的人类决策。 只有诊断到「机制该派任务却没派」时,才用 advance_bid 手动触发一次对账,或用 diagnose_bid 查根因。 ## 阻塞门禁(不要绕过) 以下人类任务 pending 时,系统不会给该项目派发任何 agent 任务: - 等待上传招标文件(tender_file_upload,上传后自动完成) - 等待人类提供资质文件(human_docs_request) - 解析产出 QC 连续不达标(qc_escalation,人工修正产出或在审核记录页强制放行) 遇到项目"不动了",先 diagnose_bid 看是不是卡在门禁上,缺什么就催人补什么,不要硬推。 ## 典型场景 - 用户交来招标文件(附件/正文/文件路径)→ 直接 import_tender_file(可带 project_name)。**未建项目时系统自动立项,无需先建项目或配人**——不要再让用户去界面手动上传。 - 用户明确说「先建个投标项目,文件稍后给」→ create_bid_project(name=XXX)。 - 项目已建、用户随后补交招标文件 → import_tender_file 导入(有当前项目就导入到该项目),跳过上传步骤。 - 用户问「招标文件上传了吗 / 解析出来了吗」→ list_tender_files 看上传与抽取状态。 - 用户问「解析质量怎么样 / QC 过了吗」→ qc_report,讲清哪类产出通过、哪类还在改。 - 用户问进展 → diagnose_bid(含章节分布、在办任务、最近评分),不要只报"正在进行"。 - 用户问「能得多少分 / 差在哪」→ bid_score_report + list_reviews,讲清失分项与改进意见落到了哪个章节。 - 用户要补资质/案例 → add_kb_doc 录入公司投标知识库;知识库缺的用 request_human_docs 让人工提供。 ## 多流程模板(2026-09-15 新增,纯技术方案流程) 本产线有两条预制流程(show_flow_template 可列出): - `bid_standard` 标准投标流程:输入=招标文件,产出=标书(默认); - `bid_tech_proposal` 纯技术方案流程:输入=**技术需求书**(无评标办法/投标程序),产出=技术方案书—— 六类技术评估项解析(功能清单/功能点/硬件配置/技术架构/功能架构/部署架构)→ QC 对标需求书 → 模版骨架**用户确认**后才分章编写 → 合成技术方案书。 文件导入后系统会 LLM 自动判流并发「流程裁剪确认」待办(正文含识别结论与判据)——用户确认走哪条流程后才派发; 判流失败会发人工待办,此时**不要替用户选流程**,引导用户在会话里说明文件性质,用 propose_flow_plan(flow_key=...) 重新提案。用户明说「只要技术方案/这是需求书不是招标文件」时同样走 propose_flow_plan 指定。 ## 流程裁剪(2026-09-11 新增) 用户提出「简化流程/跳过某阶段/只编写不评分」这类要求时: 1. 先 show_flow_template 列出预制流程各阶段(含可裁性); 2. 把用户要求映射为 trim_keys(要裁掉的阶段 key),调 propose_flow_plan 生成裁剪草案—— 合法性由系统硬校验(不可裁阶段/依赖断裂会被拒绝并说明原因,你把原因如实转告用户); 3. 草案生成后系统自动发「流程裁剪确认」待办:**只有用户在待办里确认才生效,你没有确认权**。 告知用户去待办确认,并如实转述 warn 级阶段的风险提示; 4. 用户驳回 → get_flow_plan 读驳回意见 → 按意见修订 trim_keys 重新 propose(version 自动+1); 5. 裁剪只在项目启动阶段有效(已有任务产出结果后拒绝);确认后不可改。 裁剪被驳回期间对账器暂停派发——收到驳回后**主动**按意见修订重提,不要等用户催。 ## 硬规则 - 招标金额、投标时间、资质要求这些要素,只能来自招标文件原文或人工确认,**禁止编造**; 抽取不到就如实说"文件未载明",必要时 ask_user。 - 标书内容的案例与业绩必须来自公司投标知识库的真实合同(bid_kb_docs 里 doc_type=case), 禁止虚构客户名、合同金额、历史成果数字。 - 章节没全部评审通过,不允许合成标书;评分没过门限,不允许当作可交付。 - **招标文件已在工作空间/已登记(list_tender_files 非空)时,禁止发上传待办**—— 直接 import_tender_file(file_path=工作空间路径) 登记启动解析;系统对账器也会 自动登记工作空间里的招标书。同一事项的人类待办只发一次,禁止重复催。""" # ══════════════════ 角色集 ══════════════════ BID_ROLES = [ RoleSpec( name="agent.tender_analyst", description="招标文件分析师", aliases=["tender_analyst", "analyst", "analysis"], system_prompt="你是招标文件分析师。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。", next_role="", task_title="招标文件解析", ), RoleSpec( name="agent.bid_prep", description="投标资料专员", aliases=["bid_prep", "prep", "qualification"], system_prompt="你是投标资料专员。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。", next_role="", task_title="投标资料准备", ), RoleSpec( name="agent.bid_writer", description="标书撰写工程师", aliases=["bid_writer", "writer", "write"], system_prompt="你是标书撰写工程师。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。", next_role="", task_title="章节编写", ), RoleSpec( name="agent.bid_reviewer", description="标书评审员", aliases=["bid_reviewer", "reviewer", "chapter_review"], system_prompt="你是标书评审员。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。", next_role="", task_title="章节评审", ), RoleSpec( name="agent.bid_compositor", description="标书合成工程师", aliases=["bid_compositor", "compositor", "compose"], system_prompt="你是标书合成工程师。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。", next_role="", task_title="标书合成", ), RoleSpec( name="agent.bid_scorer", description="标书评分员(模拟评标)", aliases=["bid_scorer", "scorer", "score"], system_prompt="你是标书评分员(模拟评标专家)。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。", next_role="", task_title="整书评审与评分", ), RoleSpec( name="agent.pm", description="投标项目经理(任务验收)", aliases=["pm", "manager"], system_prompt="你是投标项目经理。先 load_skill 加载 role 技能,按其中的职责与规范验收任务。", next_role="", ), RoleSpec( name="agent.qc", description="投标质量控制(合规检查)", aliases=["qc", "quality"], system_prompt="你是投标质量控制工程师。先 load_skill 加载 role 技能,按其中的职责与规范检查交付件。", next_role="", ), ] # ══════════════════ handler(签名 async def handler(sor, params, ctx) -> str)══════════════════ def _need_project(ctx): pid = ctx.get("project_id", "") or "" return pid, ("请先切换到投标项目(用 switch_project 或在项目列表选择)" if not pid else "") def _fmt(ok, msg): return ("OK: " if ok else "ERROR: ") + str(msg) def _fmt_rows(rows, empty="(空)"): if not rows: return empty if isinstance(rows, str): return rows return "\n".join(json.dumps(r, ensure_ascii=False, default=str) for r in rows[:80]) async def _h_create_bid_project(sor, p, ctx): from .bid_tender_capability import create_bid_project ok, msg = await create_bid_project(p.get("name", ""), description=p.get("description", ""), tender_content=p.get("tender_content", ""), tender_file_name=p.get("tender_file_name", ""), org_id=ctx.get("org_id", "") or "", created_by=ctx.get("user_id", "") or "", who="agent.main_agent") return _fmt(ok, msg) async def _h_import_tender_file(sor, p, ctx): # 无当前项目也允许:capability 内部自动立项(收到招标书直接启动) pid = ctx.get("project_id", "") or "" from .bid_tender_capability import import_tender_file ok, msg = await import_tender_file(pid, content_text=p.get("content_text", ""), file_path=p.get("file_path", ""), file_name=p.get("file_name", ""), project_name=p.get("project_name", ""), org_id=ctx.get("org_id", "") or "", created_by=ctx.get("user_id", "") or "", who="agent.main_agent") return _fmt(ok, msg) async def _h_request_tender_file(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_tender_capability import request_tender_file ok, msg = await request_tender_file(pid, note=p.get("note", ""), who="agent.main_agent") return _fmt(ok, msg) async def _h_list_tender_files(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_tender_capability import list_tender_files return _fmt_rows(await list_tender_files(pid), "尚未上传招标文件") async def _h_list_scoring_items(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_analysis_capability import list_scoring_items return _fmt_rows(await list_scoring_items(pid, section=p.get("section", "") or ""), "尚无评分项(等招标文件解析)") async def _h_list_doc_requirements(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_analysis_capability import list_doc_requirements return _fmt_rows(await list_doc_requirements(pid, req_type=p.get("req_type", "") or ""), "尚无投标文件要求(等招标文件解析)") async def _h_list_qualifications(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_kb_capability import list_qualifications return _fmt_rows(await list_qualifications(pid, match_status=p.get("match_status", "") or ""), "尚无资质要求(等招标文件解析)") async def _h_search_bid_kb(sor, p, ctx): from .bid_kb_capability import search_bid_kb return _fmt_rows(await search_bid_kb(query=p.get("query", "") or "", doc_type=p.get("doc_type", "") or "", org_id=ctx.get("org_id", "") or ""), "知识库无命中") async def _h_list_kb_docs(sor, p, ctx): from .bid_kb_capability import list_kb_docs return _fmt_rows(await list_kb_docs(doc_type=p.get("doc_type", "") or "", org_id=ctx.get("org_id", "") or ""), "公司投标知识库为空(用 add_kb_doc 录入资质证书/业绩案例)") async def _h_add_kb_doc(sor, p, ctx): from .bid_kb_capability import add_kb_doc ok, msg = await add_kb_doc( p.get("doc_name", ""), doc_type=p.get("doc_type", "") or "qualification", doc_no=p.get("doc_no", ""), tags=p.get("tags", ""), file_path=p.get("file_path", ""), content_text=p.get("content_text", ""), issuer=p.get("issuer", ""), customer=p.get("customer", ""), contract_amount=p.get("contract_amount", ""), valid_until=p.get("valid_until", ""), org_id=ctx.get("org_id", "") or "0", who="agent.main_agent") return _fmt(ok, msg) async def _h_request_human_docs(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_kb_capability import request_human_docs ok, msg = await request_human_docs(pid, items=p.get("items", ""), note=p.get("note", ""), who="agent.main_agent") return _fmt(ok, msg) async def _h_qc_report(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_qc_capability import qc_report return await qc_report(pid) async def _h_list_chapters(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_write_capability import list_chapters return _fmt_rows(await list_chapters(pid, status=p.get("status", "") or ""), "尚无章节(等招标文件解析生成章节骨架)") async def _h_chapter_detail(sor, p, ctx): from .bid_write_capability import chapter_detail return await chapter_detail(p.get("chapter_id", "")) async def _h_list_material_gaps(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_material_capability import list_material_gaps return await list_material_gaps(pid, status=p.get("status", "") or "") async def _h_material_gap_report(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_material_capability import material_gap_report ok, rep = await material_gap_report(pid) return rep if ok else "ERROR: %s" % rep async def _h_fill_material_gap(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_material_capability import fill_material_gap ok, msg = await fill_material_gap( p.get("gap_id", ""), fill_content=p.get("fill_content", "") or "", kb_doc_id=p.get("kb_doc_id", "") or "", who=ctx.get("user_id", "") or "user", agent_id=ctx.get("agent_id", "")) return msg if ok else "ERROR: %s" % msg async def _h_waive_material_gap(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_material_capability import waive_material_gap ok, msg = await waive_material_gap( p.get("gap_id", ""), reason=p.get("reason", "") or "", who=ctx.get("user_id", "") or "user", agent_id=ctx.get("agent_id", "")) return msg if ok else "ERROR: %s" % msg async def _h_list_bid_documents(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_compose_capability import list_bid_documents return _fmt_rows(await list_bid_documents(pid), "尚未合成标书") async def _h_list_reviews(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_review_capability import list_reviews return _fmt_rows(await list_reviews(pid, scope=p.get("scope", "") or "", chapter_id=p.get("chapter_id", "") or ""), "尚无评审记录") async def _h_bid_score_report(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_score_capability import bid_score_report return await bid_score_report(pid) async def _h_diagnose_bid(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_flow import diagnose_bid_project return await diagnose_bid_project(pid) async def _h_advance_bid(sor, p, ctx): pid, err = _need_project(ctx) if err: return err from .bid_flow import reconcile_project acts = await reconcile_project(sor, pid) return "对账结果:\n" + "\n".join("- " + a for a in (acts or ["无动作"])) async def _h_reset_task_retry(sor, p, ctx): pid, err = _need_project(ctx) if err: return err tid = str(p.get("task_id") or "").strip() if not tid: return "ERROR: 缺少 task_id" from pipeline_service.task_capability import reset_task_retry ok, msg = await reset_task_retry(tid, pid, who="agent.main_agent", agent_id=ctx.get("agent_id", "") or "") return _fmt(ok, "任务已恢复,对账器将重新派发" if ok else msg) BID_HANDLERS = { "create_bid_project": _h_create_bid_project, "import_tender_file": _h_import_tender_file, "request_tender_file": _h_request_tender_file, "list_tender_files": _h_list_tender_files, "list_scoring_items": _h_list_scoring_items, "list_doc_requirements": _h_list_doc_requirements, "list_qualifications": _h_list_qualifications, "qc_report": _h_qc_report, "search_bid_kb": _h_search_bid_kb, "list_kb_docs": _h_list_kb_docs, "add_kb_doc": _h_add_kb_doc, "request_human_docs": _h_request_human_docs, "list_chapters": _h_list_chapters, "chapter_detail": _h_chapter_detail, "list_material_gaps": _h_list_material_gaps, "material_gap_report": _h_material_gap_report, "fill_material_gap": _h_fill_material_gap, "waive_material_gap": _h_waive_material_gap, "list_bid_documents": _h_list_bid_documents, "list_reviews": _h_list_reviews, "bid_score_report": _h_bid_score_report, "diagnose_bid": _h_diagnose_bid, "advance_bid": _h_advance_bid, "reset_task_retry": _h_reset_task_retry, } def _merge_shared(): """并入引擎级通用任务/交付件/问答/项目工具(不重复实现,见 pipeline_service.shared_ability)。""" try: from pipeline_service.shared_ability import SHARED_TOOLS, SHARED_HANDLERS except Exception as e: # pipeline_service 未安装/版本旧 → 只用投标专属工具,诚实降级 logger.warning("shared_ability 不可用,投标产线仅挂载专属工具: %s", str(e)[:120]) return list(BID_TOOLS), dict(BID_HANDLERS) names = {t.name for t in BID_TOOLS} tools = list(BID_TOOLS) + [t for t in SHARED_TOOLS if t.name not in names] handlers = dict(SHARED_HANDLERS) handlers.update(BID_HANDLERS) # 同名以投标产线实现为准 return tools, handlers def register_bid_ability(): """注册投标产线能力包(幂等)。""" tools, handlers = _merge_shared() ability = PipelineAbility( pipeline_id=PIPELINE_ID, name="通用投标产线", tools=tools, system_prompt=BID_PROMPT, handlers=handlers, roles=BID_ROLES, flow_stages=BID_FLOW_STAGES, flow_templates=BID_FLOW_TEMPLATES, menus=[ {"label": "📄 招标文件", "icon": "", "url": "/pipeline-bidding/bid_tender_files/index.ui", "type": "popup", "width": "88%", "height": "82%", "require_project": True}, {"label": "📑 评分项", "icon": "", "url": "/pipeline-bidding/bid_scoring_items/index.ui", "type": "popup", "width": "88%", "height": "82%", "require_project": True}, {"label": "🔍 解析QC审核", "icon": "", "url": "/pipeline-bidding/bid_qc_reviews/index.ui", "type": "popup", "width": "88%", "height": "82%", "require_project": True}, {"label": "📝 标书章节", "icon": "", "url": "/pipeline-bidding/bid_chapters/index.ui", "type": "popup", "width": "88%", "height": "82%", "require_project": True}, {"label": "🎯 资质", "icon": "", "url": "/pipeline-bidding/bid_qualifications/index.ui", "type": "popup", "width": "88%", "height": "82%", "require_project": True}, {"label": "📚 投标知识库", "icon": "", "url": "/pipeline-bidding/bid_kb_docs/index.ui", "type": "popup", "width": "88%", "height": "82%", "require_project": True}, {"label": "📦 标书", "icon": "", "url": "/pipeline-bidding/bid_documents/index.ui", "type": "popup", "width": "88%", "height": "82%", "require_project": True}, {"label": "📋 任务树", "icon": "", "url": "/pipeline-bidding/bid_task_tree_popup.ui", "type": "popup", "width": "92%", "height": "86%", "require_project": True}, ], ) register_ability(ability) return ability # ══════════════════ slash 命令(产线专属)══════════════════ async def _slash_files(args, ctx): ex = ctx.get("executor") if not ex: return "无执行器上下文" return await ex._execute_ability_tool("list_tender_files", {}) async def _slash_bid(args, ctx): ex = ctx.get("executor") if not ex: return "无执行器上下文" return await ex._execute_ability_tool("diagnose_bid", {}) async def _slash_chapters(args, ctx): ex = ctx.get("executor") if not ex: return "无执行器上下文" return await ex._execute_ability_tool("list_chapters", {"status": (args or "").strip()}) async def _slash_score(args, ctx): ex = ctx.get("executor") if not ex: return "无执行器上下文" return await ex._execute_ability_tool("bid_score_report", {}) async def _slash_task(args, ctx): """任务树 + 输入输出(ResourceBrowser 弹窗,与开发产线 /task 同构)。""" base = (ctx.get("base_url") or "").rstrip("/") tree_url = f"{base}/pipeline-bidding/api/bid_task_tree.dspy" io_url = f"{base}/pipeline-bidding/api/bid_task_io.dspy" return { "widgettype": "PopupWindow", "options": { "title": "任务树 - 输入输出", "width": "85%", "height": "80%", "auto_open": True, }, "subwidgets": [{ "widgettype": "ResourceBrowser", "id": "bid_task_rb", "options": { "tree_options": { "widgettype": "Tree", "options": { "dataurl": tree_url, "textField": "label", "idField": "id", "cfontsize": 1.0, }, }, "browser_options": { "widgettype": "urlwidget", "options": {"url": io_url}, }, "tree_width": "42%", }, }], } def register_bid_slash_commands(): for cmd in [ SlashCommand("files", "列出招标文件及解析状态", _slash_files, "pipeline", PIPELINE_ID), SlashCommand("bid", "诊断当前投标项目卡点", _slash_bid, "pipeline", PIPELINE_ID), SlashCommand("chapters", "列出标书章节及状态", _slash_chapters, "pipeline", PIPELINE_ID), SlashCommand("score", "查看最近一轮整书评分报告", _slash_score, "pipeline", PIPELINE_ID), SlashCommand("task", "任务树(任务状态+输入输出)", _slash_task, "pipeline", PIPELINE_ID), ]: register_slash_command(cmd) # import 即注册(与 sdlc_ability 一致:宿主 import pipeline_bidding 即生效) register_bid_ability() register_bid_slash_commands()