- 12张 bid_* 表(models+DDL+CRUD):招标信息/邮箱配置/人员/招标文件/评分项/资质/ 投标文件要求/章节/评审/评分/知识库/标书 - 7个角色能力模块(bid_*_capability):采集摘要/解析/知识库资质/编写/评审/合成/评分 - bid_flow 状态对账器:角色任务唯一创建者+阻塞门禁(源头守门,引擎零特判) - bid_mail:阿里云企业邮 IMAP 采集(凭据存DB AES,客户端专用密码兼容) - 能力包注册(bid_ability)+ 角色工具自注册(role_tool_schemas) - 技能:6 common 状态机/规范 + 9 角色技能(pipelines/bidding_general,随 pipeline-core 部署) - i18n(zh/en)、init/data.json(appcodes+pipelines记录)、build.sh、RBAC load_path
612 lines
30 KiB
Python
612 lines
30 KiB
Python
"""投标产线能力包(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, register_ability,
|
||
SlashCommand, register_slash_command,
|
||
)
|
||
|
||
logger = logging.getLogger("pipeline.bidding.ability")
|
||
|
||
PIPELINE_ID = "bidding_general"
|
||
|
||
# ══════════════════ 工具定义(主 agent / 驾驶舱)══════════════════
|
||
|
||
BID_TOOLS = [
|
||
# ── 招标情报 ──
|
||
ToolDefinition(name="fetch_tenders", description="从配置的招标邮箱采集招标信息入库(按关键词过滤+去重)",
|
||
parameters={"days": "回溯天数(可选,默认按邮箱配置)", "account_id": "邮箱配置ID(可选,默认全部启用)"},
|
||
category="tender"),
|
||
ToolDefinition(name="import_tender", description="手工导入一条招标信息(邮箱不可用或线下拿到公告时用)",
|
||
parameters={"title": "项目名称", "content": "公告正文", "purchaser": "招标人(可选)",
|
||
"tender_no": "招标编号(可选)"}, category="tender"),
|
||
ToolDefinition(name="list_tenders", description="列出招标信息(可按状态筛选:new/summarized/pending_approval/approved/rejected/archived)",
|
||
parameters={"status": "状态(可选)", "limit": "条数(可选,默认50)"}, category="tender"),
|
||
ToolDefinition(name="tender_detail", description="查看一条招标信息详情(含公告正文)",
|
||
parameters={"tender_id": "招标信息ID"}, category="tender"),
|
||
ToolDefinition(name="summarize_tender", description="对招标信息做内容摘要与要素抽取(投标开始时间/项目周期/招标金额/保证金/资质要求)",
|
||
parameters={"tender_id": "招标信息ID"}, category="tender"),
|
||
ToolDefinition(name="set_tender_fields", description="修正招标信息要素(摘要抽错时人工纠正)",
|
||
parameters={"tender_id": "招标信息ID", "title": "项目名称(可选)", "purchaser": "招标人(可选)",
|
||
"bid_start_at": "投标开始时间(可选)", "bid_deadline": "投标截止(可选)",
|
||
"project_period": "项目周期(可选)", "budget_amount": "招标金额(可选)"},
|
||
category="tender"),
|
||
ToolDefinition(name="submit_tender_approval", description="提交招标信息审批(生成人类审批任务,等待通过/不通过)",
|
||
parameters={"tender_id": "招标信息ID", "assignee_role": "审批角色(可选,默认owner.superuser)",
|
||
"note": "提交说明(可选)"}, category="tender"),
|
||
ToolDefinition(name="approve_tender", description="审批通过招标信息 → 自动立项并发布「上传招标文件」任务",
|
||
parameters={"tender_id": "招标信息ID", "comment": "审批意见(可选)"}, category="tender"),
|
||
ToolDefinition(name="reject_tender", description="审批不通过招标信息 → 该招标信息自动存档作废",
|
||
parameters={"tender_id": "招标信息ID", "comment": "不通过原因"}, category="tender"),
|
||
ToolDefinition(name="archive_tender", description="存档作废一条招标信息(不再跟进)",
|
||
parameters={"tender_id": "招标信息ID", "reason": "原因"}, category="tender"),
|
||
ToolDefinition(name="reopen_tender", description="重新评估已作废/已驳回的招标信息",
|
||
parameters={"tender_id": "招标信息ID", "reason": "原因(可选)"}, category="tender"),
|
||
|
||
# ── 立项 / 人员 / 招标文件 ──
|
||
ToolDefinition(name="create_bid_project", description="由已审批通过的招标信息立项(创建投标项目)",
|
||
parameters={"tender_id": "招标信息ID", "name": "项目名称(可选)"}, category="project"),
|
||
ToolDefinition(name="assign_members", description="设定投标项目参与人员,如「张三:负责人,李四:writer」",
|
||
parameters={"members": "人员清单(姓名:角色 逗号分隔,或JSON数组)"}, category="project"),
|
||
ToolDefinition(name="list_members", description="查看投标项目参与人员", parameters={}, category="project"),
|
||
ToolDefinition(name="request_tender_file", description="发布/重发「等待上传招标文件」人类任务",
|
||
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="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"),
|
||
|
||
# ── 诊断 / 推进 ──
|
||
ToolDefinition(name="diagnose_bid", description="诊断投标项目:卡在哪个阶段、门禁、章节分布、在办任务、最近评分",
|
||
parameters={}, category="agent"),
|
||
ToolDefinition(name="advance_bid", description="立即触发投标流程状态对账(补派应有的角色任务,通常无需手动调用)",
|
||
parameters={}, category="agent"),
|
||
]
|
||
|
||
# ══════════════════ 产线 prompt 片段 ══════════════════
|
||
|
||
BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从招标信息到标书交付的全流程管理:
|
||
招标信息采集 → 内容摘要 → 审批 → 立项与人员 → 上传招标文件 → 解析(评分项/得分规则/资质/投标文件要求)
|
||
→ 资质文档准备 → 分章节编写 → 章节评审 → 合成标书 → 整书评分 → 交付确认。
|
||
|
||
## 流转由机制驱动(重要)
|
||
角色任务由系统状态对账器自动派发(每 15 秒一轮),你**不需要**手动为每个章节创建任务:
|
||
- 章节评审不达标 → 章节自动退回重写
|
||
- 整书评分不达标 → 改进意见自动落到问题章节并退回重写 → 重新合成 → 重新评分
|
||
你的职责是:采集与摘要、推动审批、立项与配人、处理卡点与提问、回答评审/评分中的人类决策。
|
||
只有诊断到「机制该派任务却没派」时,才用 advance_bid 手动触发一次对账,或用 diagnose_bid 查根因。
|
||
|
||
## 阻塞门禁(不要绕过)
|
||
以下人类任务 pending 时,系统不会给该项目派发任何 agent 任务:
|
||
- 招标信息审批未决(tender_approval)
|
||
- 等待上传招标文件(tender_file_upload,上传后自动完成)
|
||
- 等待人类提供资质文件(human_docs_request)
|
||
遇到项目"不动了",先 diagnose_bid 看是不是卡在这三个门禁上,缺什么就催人补什么,不要硬推。
|
||
|
||
## 典型场景
|
||
- 用户说「看看有什么新招标 / 收一下招标邮件」→ fetch_tenders,再 list_tenders 看结果。
|
||
- 用户说「这条招标怎么样 / 帮我分析下」→ tender_detail + summarize_tender,把投标开始时间、
|
||
项目周期、招标金额、保证金、资质门槛、风险点讲清楚,再问是否提交审批。
|
||
- 用户说「提交审批」→ submit_tender_approval;用户直接拍板通过/不通过 → approve_tender / reject_tender。
|
||
**审批不通过就是终态**:招标信息自动存档作废,不要再立项。
|
||
- 审批通过后 → 自动立项并发上传招标文件任务;接着 assign_members 设定参与人员。
|
||
- 用户问进展 → diagnose_bid(含章节分布、在办任务、最近评分),不要只报"正在进行"。
|
||
- 用户问「能得多少分 / 差在哪」→ bid_score_report + list_reviews,讲清失分项与改进意见落到了哪个章节。
|
||
- 用户要补资质/案例 → add_kb_doc 录入公司投标知识库;知识库缺的用 request_human_docs 让人工提供。
|
||
|
||
## 硬规则
|
||
- 招标金额、投标时间、资质要求这些要素,只能来自招标公告/招标文件原文或人工确认,**禁止编造**;
|
||
抽取不到就如实说"公告未载明",必要时 ask_user。
|
||
- 标书内容的案例与业绩必须来自公司投标知识库的真实合同(bid_kb_docs 里 doc_type=case),
|
||
禁止虚构客户名、合同金额、历史成果数字。
|
||
- 章节没全部评审通过,不允许合成标书;评分没过门限,不允许当作可交付。"""
|
||
|
||
# ══════════════════ 角色集 ══════════════════
|
||
|
||
BID_ROLES = [
|
||
RoleSpec(
|
||
name="agent.tender_scout",
|
||
description="招标信息员",
|
||
aliases=["tender_scout", "scout", "tender", "tender_intake"],
|
||
system_prompt="你是招标信息员。先 load_skill 加载 role 技能,按其中的职责与规范执行任务。",
|
||
next_role="",
|
||
task_title="招标信息采集与摘要",
|
||
),
|
||
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_fetch_tenders(sor, p, ctx):
|
||
from .bid_tender_capability import fetch_tenders
|
||
ok, msg = await fetch_tenders(org_id=ctx.get("org_id", "") or "",
|
||
account_id=p.get("account_id", "") or "",
|
||
days=p.get("days") or None,
|
||
who="agent.main_agent")
|
||
return _fmt(ok, msg)
|
||
|
||
|
||
async def _h_import_tender(sor, p, ctx):
|
||
from .bid_tender_capability import import_tender
|
||
ok, msg = await import_tender(p.get("title", ""), p.get("content", ""),
|
||
purchaser=p.get("purchaser", ""),
|
||
tender_no=p.get("tender_no", ""),
|
||
org_id=ctx.get("org_id", "") or "0",
|
||
who="agent.main_agent",
|
||
created_by=ctx.get("user_id", "") or "")
|
||
return _fmt(ok, "已导入招标信息 id=%s" % msg if ok else msg)
|
||
|
||
|
||
async def _h_list_tenders(sor, p, ctx):
|
||
from .bid_tender_capability import list_tenders
|
||
return _fmt_rows(await list_tenders(status=p.get("status", "") or "",
|
||
org_id=ctx.get("org_id", "") or "",
|
||
limit=p.get("limit") or 50),
|
||
"暂无招标信息(可用 fetch_tenders 采集或 import_tender 手工导入)")
|
||
|
||
|
||
async def _h_tender_detail(sor, p, ctx):
|
||
from .bid_tender_capability import tender_detail
|
||
return await tender_detail(p.get("tender_id", ""))
|
||
|
||
|
||
async def _h_summarize_tender(sor, p, ctx):
|
||
from .bid_tender_capability import summarize_tender
|
||
ok, msg = await summarize_tender(p.get("tender_id", ""),
|
||
model_name=ctx.get("model_name", "") or "",
|
||
who="agent.main_agent")
|
||
return _fmt(ok, msg)
|
||
|
||
|
||
async def _h_set_tender_fields(sor, p, ctx):
|
||
from .bid_tender_capability import set_tender_fields
|
||
ok, msg = await set_tender_fields(
|
||
p.get("tender_id", ""), title=p.get("title", ""), purchaser=p.get("purchaser", ""),
|
||
tender_no=p.get("tender_no", ""), bid_start_at=p.get("bid_start_at", ""),
|
||
bid_deadline=p.get("bid_deadline", ""), project_period=p.get("project_period", ""),
|
||
budget_amount=p.get("budget_amount", ""), deposit_amount=p.get("deposit_amount", ""),
|
||
summary=p.get("summary", ""), qualification_req=p.get("qualification_req", ""),
|
||
who="agent.main_agent")
|
||
return _fmt(ok, msg)
|
||
|
||
|
||
async def _h_submit_tender_approval(sor, p, ctx):
|
||
from .bid_tender_capability import submit_tender_approval
|
||
ok, msg = await submit_tender_approval(
|
||
p.get("tender_id", ""), assignee_role=p.get("assignee_role", "") or "owner.superuser",
|
||
note=p.get("note", ""), who="agent.main_agent")
|
||
return _fmt(ok, msg)
|
||
|
||
|
||
async def _h_approve_tender(sor, p, ctx):
|
||
from .bid_tender_capability import approve_tender
|
||
ok, msg = await approve_tender(p.get("tender_id", ""), comment=p.get("comment", ""),
|
||
who=ctx.get("user_id", "") or "owner.superuser")
|
||
return _fmt(ok, msg)
|
||
|
||
|
||
async def _h_reject_tender(sor, p, ctx):
|
||
from .bid_tender_capability import reject_tender
|
||
ok, msg = await reject_tender(p.get("tender_id", ""), comment=p.get("comment", ""),
|
||
who=ctx.get("user_id", "") or "owner.superuser")
|
||
return _fmt(ok, msg)
|
||
|
||
|
||
async def _h_archive_tender(sor, p, ctx):
|
||
from .bid_tender_capability import archive_tender
|
||
ok, msg = await archive_tender(p.get("tender_id", ""), reason=p.get("reason", ""),
|
||
who="agent.main_agent")
|
||
return _fmt(ok, msg)
|
||
|
||
|
||
async def _h_reopen_tender(sor, p, ctx):
|
||
from .bid_tender_capability import reopen_tender
|
||
ok, msg = await reopen_tender(p.get("tender_id", ""), reason=p.get("reason", ""),
|
||
who="agent.main_agent")
|
||
return _fmt(ok, msg)
|
||
|
||
|
||
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("tender_id", ""), name=p.get("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_assign_members(sor, p, ctx):
|
||
pid, err = _need_project(ctx)
|
||
if err:
|
||
return err
|
||
from .bid_tender_capability import assign_members
|
||
ok, msg = await assign_members(pid, members=p.get("members", ""), who="agent.main_agent")
|
||
return _fmt(ok, msg)
|
||
|
||
|
||
async def _h_list_members(sor, p, ctx):
|
||
pid, err = _need_project(ctx)
|
||
if err:
|
||
return err
|
||
from .bid_tender_capability import list_members
|
||
return _fmt_rows(await list_members(pid), "尚未设定参与人员")
|
||
|
||
|
||
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_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_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 ["无动作"]))
|
||
|
||
|
||
BID_HANDLERS = {
|
||
"fetch_tenders": _h_fetch_tenders,
|
||
"import_tender": _h_import_tender,
|
||
"list_tenders": _h_list_tenders,
|
||
"tender_detail": _h_tender_detail,
|
||
"summarize_tender": _h_summarize_tender,
|
||
"set_tender_fields": _h_set_tender_fields,
|
||
"submit_tender_approval": _h_submit_tender_approval,
|
||
"approve_tender": _h_approve_tender,
|
||
"reject_tender": _h_reject_tender,
|
||
"archive_tender": _h_archive_tender,
|
||
"reopen_tender": _h_reopen_tender,
|
||
"create_bid_project": _h_create_bid_project,
|
||
"assign_members": _h_assign_members,
|
||
"list_members": _h_list_members,
|
||
"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,
|
||
"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_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,
|
||
}
|
||
|
||
|
||
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,
|
||
menus=[
|
||
{"label": "📨 招标信息", "icon": "", "url": "/pipeline-bidding/bid_tenders/index.ui",
|
||
"type": "popup", "width": "88%", "height": "82%"},
|
||
{"label": "📑 评分项", "icon": "", "url": "/pipeline-bidding/bid_scoring_items/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%"},
|
||
{"label": "📦 标书", "icon": "", "url": "/pipeline-bidding/bid_documents/index.ui",
|
||
"type": "popup", "width": "88%", "height": "82%", "require_project": True},
|
||
{"label": "✉️ 邮箱配置", "icon": "", "url": "/pipeline-bidding/bid_mail_accounts/index.ui",
|
||
"type": "popup", "width": "88%", "height": "82%"},
|
||
],
|
||
)
|
||
register_ability(ability)
|
||
return ability
|
||
|
||
|
||
# ══════════════════ slash 命令(产线专属)══════════════════
|
||
|
||
async def _slash_tenders(args, ctx):
|
||
ex = ctx.get("executor")
|
||
if not ex:
|
||
return "无执行器上下文"
|
||
return await ex._execute_ability_tool("list_tenders", {"status": (args or "").strip()})
|
||
|
||
|
||
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", {})
|
||
|
||
|
||
def register_bid_slash_commands():
|
||
for cmd in [
|
||
SlashCommand("tenders", "列出招标信息(可带状态)", _slash_tenders, "pipeline", PIPELINE_ID),
|
||
SlashCommand("bid", "诊断当前投标项目卡点", _slash_bid, "pipeline", PIPELINE_ID),
|
||
SlashCommand("chapters", "列出标书章节及状态", _slash_chapters, "pipeline", PIPELINE_ID),
|
||
SlashCommand("score", "查看最近一轮整书评分报告", _slash_score, "pipeline", PIPELINE_ID),
|
||
]:
|
||
register_slash_command(cmd)
|
||
|
||
|
||
# import 即注册(与 sdlc_ability 一致:宿主 import pipeline_bidding 即生效)
|
||
register_bid_ability()
|
||
register_bid_slash_commands()
|