feat(bidding): 移除立项+配人步骤,收到招标书直接启动项目

- import_tender_file 无项目时自动立项(项目名缺省取文件名),导入即解析
- 主 agent 下线 assign_members/list_members 工具(能力原语保留)
- BID_PROMPT/典型场景同步:不再先问建项目/配人
This commit is contained in:
yumoqing 2026-09-01 05:15:53 +08:00
parent aa5eb26d37
commit 4e1386f2a1
3 changed files with 66 additions and 58 deletions

View File

@ -22,17 +22,17 @@ PIPELINE_ID = "bidding_general"
# ══════════════════ 工具定义(主 agent / 驾驶舱)══════════════════ # ══════════════════ 工具定义(主 agent / 驾驶舱)══════════════════
BID_TOOLS = [ BID_TOOLS = [
# ── 立项 / 人员 / 招标文件(产线起点:从上传招标文件开始,无邮件采集)── # ── 招标文件(产线起点:收到招标书直接启动,无需立项/配人步骤)──
ToolDefinition(name="create_bid_project", description="创建投标项目(直接立项,不依赖招标信息)。用户说「创建/新建投标项目」时调用。若用户已提供招标文件(附件/正文),必须带上 tender_content,系统自动跳过上传步骤直接进入解析", 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": "项目描述(可选)", parameters={"name": "项目名称", "description": "项目描述(可选)",
"tender_content": "招标文件正文(用户已提供时必填,直接导入跳过上传步骤)", "tender_content": "招标文件正文(用户已提供时必填,直接导入跳过上传步骤)",
"tender_file_name": "招标文件名(可选)"}, category="project"), "tender_file_name": "招标文件名(可选)"}, category="project"),
ToolDefinition(name="import_tender_file", description="导入招标文件(项目已建但还没上传时):把对话中用户提供的招标文件正文/文件路径直接导入,跳过人工上传步骤,系统自动开始解析",
parameters={"content_text": "招标文件正文", "file_path": "文件路径(与content_text二选一)",
"file_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="发布/重发「等待上传招标文件」人类任务", ToolDefinition(name="request_tender_file", description="发布/重发「等待上传招标文件」人类任务",
parameters={"note": "说明(可选)"}, category="project"), parameters={"note": "说明(可选)"}, category="project"),
ToolDefinition(name="list_tender_files", description="查看已上传的招标文件及抽取状态", parameters={}, category="project"), ToolDefinition(name="list_tender_files", description="查看已上传的招标文件及抽取状态", parameters={}, category="project"),
@ -80,11 +80,14 @@ BID_TOOLS = [
# ══════════════════ 产线 prompt 片段 ══════════════════ # ══════════════════ 产线 prompt 片段 ══════════════════
BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从上传招标文件到标书交付的全流程管理: BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从招标文件到标书交付的全流程管理:
创建投标项目 → 上传招标文件 → 解析(评分项/得分规则/资质/投标文件要求/章节骨架) 收到招标文件 → 自动立项并解析(评分项/得分规则/资质/投标文件要求/章节骨架)
→ 解析产出 QC 契合度审核 → 资质文档准备 → 分章节编写 → 章节评审 → 合成标书 → 整书评分 → 交付确认。 → 解析产出 QC 契合度审核 → 资质文档准备 → 分章节编写 → 章节评审 → 合成标书 → 整书评分 → 交付确认。
招标信息的采集/摘要/审批归商机产线,本产线从「上传招标文件」起步;用户要采集招标信息时说明去商机产线。 产线没有独立的「立项/配人」步骤:用户把招标文件(附件/正文/文件路径)交给你,
调 import_tender_file 即自动建项目并启动解析——不要先问用户「要不要建项目/安排人员」。
招标信息的采集/摘要/审批归商机产线,本产线从「收到招标文件」起步;用户要采集招标信息时说明去商机产线。
## 流转由机制驱动(重要) ## 流转由机制驱动(重要)
角色任务由系统状态对账器自动派发(每 15 秒一轮),你**不需要**手动为每个章节创建任务: 角色任务由系统状态对账器自动派发(每 15 秒一轮),你**不需要**手动为每个章节创建任务:
@ -92,7 +95,7 @@ BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从上传招
按 10 分制打分,得分高于通过分(默认 9.5)才放行;不通过给改进意见 → 清空产出 → 解析重做 → 重审 按 10 分制打分,得分高于通过分(默认 9.5)才放行;不通过给改进意见 → 清空产出 → 解析重做 → 重审
- 章节评审不达标 → 章节自动退回重写 - 章节评审不达标 → 章节自动退回重写
- 整书评分不达标 → 改进意见自动落到问题章节并退回重写 → 重新合成 → 重新评分 - 整书评分不达标 → 改进意见自动落到问题章节并退回重写 → 重新合成 → 重新评分
你的职责是:立项与配人、推动上传招标文件、处理卡点与提问、回答评审/评分中的人类决策。 你的职责是:收到招标文件立即导入启动、处理卡点与提问、回答评审/评分中的人类决策。
只有诊断到「机制该派任务却没派」时,才用 advance_bid 手动触发一次对账,或用 diagnose_bid 查根因。 只有诊断到「机制该派任务却没派」时,才用 advance_bid 手动触发一次对账,或用 diagnose_bid 查根因。
## 阻塞门禁(不要绕过) ## 阻塞门禁(不要绕过)
@ -103,8 +106,9 @@ BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从上传招
遇到项目"不动了",先 diagnose_bid 看是不是卡在门禁上,缺什么就催人补什么,不要硬推。 遇到项目"不动了",先 diagnose_bid 看是不是卡在门禁上,缺什么就催人补什么,不要硬推。
## 典型场景 ## 典型场景
- 用户说「创建一个投标项目,叫 XXX」→ create_bid_project(name=XXX)。**若用户同时提供了招标文件(附件/正文),必须把它放进 tender_content 一起提交**,系统直接导入并跳过人工上传步骤,自动开始解析——不要再让用户去界面手动上传。 - 用户交来招标文件(附件/正文/文件路径)→ 直接 import_tender_file(可带 project_name)。**未建项目时系统自动立项,无需先建项目或配人**——不要再让用户去界面手动上传。
- 项目已建、用户随后补交招标文件(对话正文或已写入工作空间的文件)→ import_tender_file 导入,跳过上传步骤。 - 用户明确说「先建个投标项目,文件稍后给」→ create_bid_project(name=XXX)。
- 项目已建、用户随后补交招标文件 → import_tender_file 导入(有当前项目就导入到该项目),跳过上传步骤。
- 用户问「招标文件上传了吗 / 解析出来了吗」→ list_tender_files 看上传与抽取状态。 - 用户问「招标文件上传了吗 / 解析出来了吗」→ list_tender_files 看上传与抽取状态。
- 用户问「解析质量怎么样 / QC 过了吗」→ qc_report,讲清哪类产出通过、哪类还在改。 - 用户问「解析质量怎么样 / QC 过了吗」→ qc_report,讲清哪类产出通过、哪类还在改。
- 用户问进展 → diagnose_bid(含章节分布、在办任务、最近评分),不要只报"正在进行"。 - 用户问进展 → diagnose_bid(含章节分布、在办任务、最近评分),不要只报"正在进行"。
@ -217,34 +221,19 @@ async def _h_create_bid_project(sor, p, ctx):
async def _h_import_tender_file(sor, p, ctx): async def _h_import_tender_file(sor, p, ctx):
pid, err = _need_project(ctx) # 无当前项目也允许:capability 内部自动立项(收到招标书直接启动)
if err: pid = ctx.get("project_id", "") or ""
return err
from .bid_tender_capability import import_tender_file from .bid_tender_capability import import_tender_file
ok, msg = await import_tender_file(pid, content_text=p.get("content_text", ""), ok, msg = await import_tender_file(pid, content_text=p.get("content_text", ""),
file_path=p.get("file_path", ""), file_path=p.get("file_path", ""),
file_name=p.get("file_name", ""), 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") who="agent.main_agent")
return _fmt(ok, msg) 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): async def _h_request_tender_file(sor, p, ctx):
pid, err = _need_project(ctx) pid, err = _need_project(ctx)
if err: if err:
@ -394,8 +383,6 @@ async def _h_advance_bid(sor, p, ctx):
BID_HANDLERS = { BID_HANDLERS = {
"create_bid_project": _h_create_bid_project, "create_bid_project": _h_create_bid_project,
"import_tender_file": _h_import_tender_file, "import_tender_file": _h_import_tender_file,
"assign_members": _h_assign_members,
"list_members": _h_list_members,
"request_tender_file": _h_request_tender_file, "request_tender_file": _h_request_tender_file,
"list_tender_files": _h_list_tender_files, "list_tender_files": _h_list_tender_files,
"list_scoring_items": _h_list_scoring_items, "list_scoring_items": _h_list_scoring_items,

View File

@ -90,21 +90,52 @@ async def create_bid_project(name, description="", org_id="", created_by="",
async def import_tender_file(project_id, content_text="", file_path="", file_name="", async def import_tender_file(project_id, content_text="", file_path="", file_name="",
project_name="", org_id="", created_by="",
who=None, agent_id=None): who=None, agent_id=None):
"""导入招标文件(已创建的项目补录):落库 bid_tender_files 并跳过人工上传步骤。 """导入招标文件;未建项目时自动立项(产线新起点:收到招标书直接启动,2026-09-01)。
- 有 project_id:落库 bid_tender_files 并跳过人工上传步骤;
- 无 project_id:先自动创建投标项目(名称取 project_name,缺省用文件名/「投标项目」),
再导入招标文件——用户不必先走立项步骤,收到招标书即启动项目。
content_text 与 file_path 至少给一个。导入后上传任务自动完成,对账器即派解析。 content_text 与 file_path 至少给一个。导入后上传任务自动完成,对账器即派解析。
""" """
if not project_id:
return False, "缺少 project_id"
txt = str(content_text or "").strip() txt = str(content_text or "").strip()
fp = str(file_path or "").strip() fp = str(file_path or "").strip()
if not txt and not fp: if not txt and not fp:
return False, "缺少招标文件内容:请提供 content_text(正文)或 file_path(文件路径)" return False, "缺少招标文件内容:请提供 content_text(正文)或 file_path(文件路径)"
db, dbname = get_db() db, dbname = get_db()
async with db.sqlorContext(dbname) as sor: async with db.sqlorContext(dbname) as sor:
pid = str(project_id or "").strip()
auto_created = False
if not pid:
# 自动立项:收到招标书直接启动,无需先建项目/配人
pname = str(project_name or "").strip()[:190]
if not pname:
pname = (str(file_name or "").strip() or (fp.split("/")[-1] if fp else "")
or txt.split("\n")[0].strip())[:190] or "投标项目"
pid = new_id()
await sor.C("sd_projects", {
"id": pid,
"name": pname,
"description": "由导入招标文件自动立项(收到招标书直接启动)",
"project_type": "bidding",
"pipeline_id": PIPELINE_ID,
"status": "in_progress",
"org_id": org_id or "0",
"created_by": created_by or (who or "agent.main_agent"),
})
await sor.sqlExe("COMMIT", {})
await record(sor, pid, "sd_projects", pid, "auto_create_bid_project",
to_state="in_progress", who=who, agent_id=agent_id,
detail="auto-created on tender import")
auto_created = True
else:
p = await get_project(sor, pid)
if not p:
return False, "项目不存在: %s" % pid
await sor.C("bid_tender_files", { await sor.C("bid_tender_files", {
"id": new_id(), "project_id": project_id, "id": new_id(), "project_id": pid,
"file_name": str(file_name or ("招标文件(对话提供)" if txt else fp.split("/")[-1]))[:250], "file_name": str(file_name or ("招标文件(对话提供)" if txt else fp.split("/")[-1]))[:250],
"file_type": "tender_doc", "file_type": "tender_doc",
"content_text": txt[:2000000] if txt else "", "content_text": txt[:2000000] if txt else "",
@ -114,18 +145,21 @@ async def import_tender_file(project_id, content_text="", file_path="", file_nam
"uploaded_by": who or "agent.main_agent", "uploaded_by": who or "agent.main_agent",
}) })
await sor.sqlExe("COMMIT", {}) await sor.sqlExe("COMMIT", {})
ht = await find_human_task(sor, project_id, HT_TENDER_FILE, status="pending") ht = await find_human_task(sor, pid, HT_TENDER_FILE, status="pending")
if ht: if ht:
await sor.sqlExe( await sor.sqlExe(
"UPDATE pipeline_human_tasks SET status='done', qc_status='passed', " "UPDATE pipeline_human_tasks SET status='done', qc_status='passed', "
"qc_comment='招标文件已导入,系统自动完成(跳过上传步骤)', " "qc_comment='招标文件已导入,系统自动完成(跳过上传步骤)', "
"submitted_at=NOW() WHERE id=${h}$", {"h": ht.get("id")}) "submitted_at=NOW() WHERE id=${h}$", {"h": ht.get("id")})
await sor.sqlExe("COMMIT", {}) await sor.sqlExe("COMMIT", {})
await record(sor, project_id, "bid_tender_files", project_id, "import_tender_file", await record(sor, pid, "bid_tender_files", pid, "import_tender_file",
to_state="uploaded", who=who, agent_id=agent_id, to_state="uploaded", who=who, agent_id=agent_id,
detail=("text %d chars" % len(txt)) if txt else ("path %s" % fp)) detail=("text %d chars" % len(txt)) if txt else ("path %s" % fp))
return True, ("招标文件已导入(%s),上传步骤已跳过,系统将自动开始解析。" base = ("正文 %d 字" % len(txt)) if txt else file_path
% ("正文 %d 字" % len(txt) if txt else file_path)) if auto_created:
return True, ("project_id=%s(已自动立项并导入招标文件:%s,"
"系统将自动开始解析)" % (pid, base))
return True, ("招标文件已导入(%s),上传步骤已跳过,系统将自动开始解析。" % base)
async def assign_members(project_id, members="", who=None, agent_id=None): async def assign_members(project_id, members="", who=None, agent_id=None):

View File

@ -15,20 +15,7 @@ logger = logging.getLogger("pipeline.bidding.tools")
_M = "pipeline_bidding" _M = "pipeline_bidding"
BID_ROLE_TOOL_SCHEMAS = { BID_ROLE_TOOL_SCHEMAS = {
# ── bid_tender_capability:立项/人员(产线从上传招标文件开始,无邮件采集/审批)── # ── bid_tender_capability:招标文件(产线起点=收到招标书直接启动,无立项/配人步骤)──
"assign_members": {
"module": f"{_M}.bid_tender_capability",
"description": "设定投标项目参与人员,如「张三:负责人,李四:writer」",
"params": {"project_id": "项目ID(可选,默认当前)",
"members": "人员清单(姓名:角色 逗号分隔,或JSON数组)"},
"required": ["members"],
},
"list_members": {
"module": f"{_M}.bid_tender_capability",
"description": "查看投标项目参与人员",
"params": {"project_id": "项目ID(可选,默认当前)"},
"required": [],
},
# ── bid_analysis_capability:招标文件分析师 ── # ── bid_analysis_capability:招标文件分析师 ──
"read_tender_file": { "read_tender_file": {
"module": f"{_M}.bid_analysis_capability", "module": f"{_M}.bid_analysis_capability",