From 4e1386f2a148bab58cc9e9ffa1872878478c4b0a Mon Sep 17 00:00:00 2001 From: yumoqing Date: Tue, 1 Sep 2026 05:15:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(bidding):=20=E7=A7=BB=E9=99=A4=E7=AB=8B?= =?UTF-8?q?=E9=A1=B9+=E9=85=8D=E4=BA=BA=E6=AD=A5=E9=AA=A4=EF=BC=8C?= =?UTF-8?q?=E6=94=B6=E5=88=B0=E6=8B=9B=E6=A0=87=E4=B9=A6=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - import_tender_file 无项目时自动立项(项目名缺省取文件名),导入即解析 - 主 agent 下线 assign_members/list_members 工具(能力原语保留) - BID_PROMPT/典型场景同步:不再先问建项目/配人 --- pipeline_bidding/bid_ability.py | 59 +++++++++-------------- pipeline_bidding/bid_tender_capability.py | 50 ++++++++++++++++--- pipeline_bidding/role_tool_schemas.py | 15 +----- 3 files changed, 66 insertions(+), 58 deletions(-) diff --git a/pipeline_bidding/bid_ability.py b/pipeline_bidding/bid_ability.py index 7f345a2..e96312d 100644 --- a/pipeline_bidding/bid_ability.py +++ b/pipeline_bidding/bid_ability.py @@ -22,17 +22,17 @@ PIPELINE_ID = "bidding_general" # ══════════════════ 工具定义(主 agent / 驾驶舱)══════════════════ 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": "项目描述(可选)", "tender_content": "招标文件正文(用户已提供时必填,直接导入跳过上传步骤)", "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="发布/重发「等待上传招标文件」人类任务", parameters={"note": "说明(可选)"}, category="project"), ToolDefinition(name="list_tender_files", description="查看已上传的招标文件及抽取状态", parameters={}, category="project"), @@ -80,11 +80,14 @@ BID_TOOLS = [ # ══════════════════ 产线 prompt 片段 ══════════════════ -BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从上传招标文件到标书交付的全流程管理: -创建投标项目 → 上传招标文件 → 解析(评分项/得分规则/资质/投标文件要求/章节骨架) +BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从招标文件到标书交付的全流程管理: +收到招标文件 → 自动立项并解析(评分项/得分规则/资质/投标文件要求/章节骨架) → 解析产出 QC 契合度审核 → 资质文档准备 → 分章节编写 → 章节评审 → 合成标书 → 整书评分 → 交付确认。 -招标信息的采集/摘要/审批归商机产线,本产线从「上传招标文件」起步;用户要采集招标信息时说明去商机产线。 +产线没有独立的「立项/配人」步骤:用户把招标文件(附件/正文/文件路径)交给你, +调 import_tender_file 即自动建项目并启动解析——不要先问用户「要不要建项目/安排人员」。 + +招标信息的采集/摘要/审批归商机产线,本产线从「收到招标文件」起步;用户要采集招标信息时说明去商机产线。 ## 流转由机制驱动(重要) 角色任务由系统状态对账器自动派发(每 15 秒一轮),你**不需要**手动为每个章节创建任务: @@ -92,7 +95,7 @@ BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从上传招 按 10 分制打分,得分高于通过分(默认 9.5)才放行;不通过给改进意见 → 清空产出 → 解析重做 → 重审 - 章节评审不达标 → 章节自动退回重写 - 整书评分不达标 → 改进意见自动落到问题章节并退回重写 → 重新合成 → 重新评分 -你的职责是:立项与配人、推动上传招标文件、处理卡点与提问、回答评审/评分中的人类决策。 +你的职责是:收到招标文件立即导入启动、处理卡点与提问、回答评审/评分中的人类决策。 只有诊断到「机制该派任务却没派」时,才用 advance_bid 手动触发一次对账,或用 diagnose_bid 查根因。 ## 阻塞门禁(不要绕过) @@ -103,8 +106,9 @@ BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从上传招 遇到项目"不动了",先 diagnose_bid 看是不是卡在门禁上,缺什么就催人补什么,不要硬推。 ## 典型场景 -- 用户说「创建一个投标项目,叫 XXX」→ create_bid_project(name=XXX)。**若用户同时提供了招标文件(附件/正文),必须把它放进 tender_content 一起提交**,系统直接导入并跳过人工上传步骤,自动开始解析——不要再让用户去界面手动上传。 -- 项目已建、用户随后补交招标文件(对话正文或已写入工作空间的文件)→ import_tender_file 导入,跳过上传步骤。 +- 用户交来招标文件(附件/正文/文件路径)→ 直接 import_tender_file(可带 project_name)。**未建项目时系统自动立项,无需先建项目或配人**——不要再让用户去界面手动上传。 +- 用户明确说「先建个投标项目,文件稍后给」→ create_bid_project(name=XXX)。 +- 项目已建、用户随后补交招标文件 → import_tender_file 导入(有当前项目就导入到该项目),跳过上传步骤。 - 用户问「招标文件上传了吗 / 解析出来了吗」→ list_tender_files 看上传与抽取状态。 - 用户问「解析质量怎么样 / QC 过了吗」→ qc_report,讲清哪类产出通过、哪类还在改。 - 用户问进展 → diagnose_bid(含章节分布、在办任务、最近评分),不要只报"正在进行"。 @@ -217,34 +221,19 @@ async def _h_create_bid_project(sor, p, ctx): async def _h_import_tender_file(sor, p, ctx): - pid, err = _need_project(ctx) - if err: - return err + # 无当前项目也允许: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_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: @@ -394,8 +383,6 @@ async def _h_advance_bid(sor, p, ctx): BID_HANDLERS = { "create_bid_project": _h_create_bid_project, "import_tender_file": _h_import_tender_file, - "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, diff --git a/pipeline_bidding/bid_tender_capability.py b/pipeline_bidding/bid_tender_capability.py index 714a985..e211e30 100644 --- a/pipeline_bidding/bid_tender_capability.py +++ b/pipeline_bidding/bid_tender_capability.py @@ -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="", + project_name="", org_id="", created_by="", who=None, agent_id=None): - """导入招标文件(已创建的项目补录):落库 bid_tender_files 并跳过人工上传步骤。 + """导入招标文件;未建项目时自动立项(产线新起点:收到招标书直接启动,2026-09-01)。 + + - 有 project_id:落库 bid_tender_files 并跳过人工上传步骤; + - 无 project_id:先自动创建投标项目(名称取 project_name,缺省用文件名/「投标项目」), + 再导入招标文件——用户不必先走立项步骤,收到招标书即启动项目。 content_text 与 file_path 至少给一个。导入后上传任务自动完成,对账器即派解析。 """ - if not project_id: - return False, "缺少 project_id" txt = str(content_text or "").strip() fp = str(file_path or "").strip() if not txt and not fp: return False, "缺少招标文件内容:请提供 content_text(正文)或 file_path(文件路径)" db, dbname = get_db() 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", { - "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_type": "tender_doc", "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", }) 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: await sor.sqlExe( "UPDATE pipeline_human_tasks SET status='done', qc_status='passed', " "qc_comment='招标文件已导入,系统自动完成(跳过上传步骤)', " "submitted_at=NOW() WHERE id=${h}$", {"h": ht.get("id")}) 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, detail=("text %d chars" % len(txt)) if txt else ("path %s" % fp)) - return True, ("招标文件已导入(%s),上传步骤已跳过,系统将自动开始解析。" - % ("正文 %d 字" % len(txt) if txt else file_path)) + base = ("正文 %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): diff --git a/pipeline_bidding/role_tool_schemas.py b/pipeline_bidding/role_tool_schemas.py index fc21fbb..acb5b79 100644 --- a/pipeline_bidding/role_tool_schemas.py +++ b/pipeline_bidding/role_tool_schemas.py @@ -15,20 +15,7 @@ logger = logging.getLogger("pipeline.bidding.tools") _M = "pipeline_bidding" BID_ROLE_TOOL_SCHEMAS = { - # ── 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_tender_capability:招标文件(产线起点=收到招标书直接启动,无立项/配人步骤)── # ── bid_analysis_capability:招标文件分析师 ── "read_tender_file": { "module": f"{_M}.bid_analysis_capability",