feat(bidding): 立项随附招标文件直接导入跳过人工上传——用户已提供招标书时不再卡在上传步骤
- create_bid_project 增加 tender_content 参数:带招标文件立项,落库+上传任务自动完成 - 新增 import_tender_file:已建项目补录招标文件正文/路径,跳过上传步骤 - 驾驶舱 prompt 硬规则:用户提供招标文件必须随立项/导入提交,不再让用户手动上传
This commit is contained in:
parent
3240c4f042
commit
5420bf49dd
@ -23,8 +23,13 @@ PIPELINE_ID = "bidding_general"
|
||||
|
||||
BID_TOOLS = [
|
||||
# ── 立项 / 人员 / 招标文件(产线起点:从上传招标文件开始,无邮件采集)──
|
||||
ToolDefinition(name="create_bid_project", description="创建投标项目(直接立项,不依赖招标信息)。用户说「创建/新建投标项目」时调用",
|
||||
parameters={"name": "项目名称", "description": "项目描述(可选)"}, category="project"),
|
||||
ToolDefinition(name="create_bid_project", description="创建投标项目(直接立项,不依赖招标信息)。用户说「创建/新建投标项目」时调用。若用户已提供招标文件(附件/正文),必须带上 tender_content,系统自动跳过上传步骤直接进入解析",
|
||||
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"),
|
||||
@ -98,7 +103,8 @@ BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从上传招
|
||||
遇到项目"不动了",先 diagnose_bid 看是不是卡在门禁上,缺什么就催人补什么,不要硬推。
|
||||
|
||||
## 典型场景
|
||||
- 用户说「创建一个投标项目,叫 XXX」→ create_bid_project(name=XXX),随后提示上传招标文件。
|
||||
- 用户说「创建一个投标项目,叫 XXX」→ create_bid_project(name=XXX)。**若用户同时提供了招标文件(附件/正文),必须把它放进 tender_content 一起提交**,系统直接导入并跳过人工上传步骤,自动开始解析——不要再让用户去界面手动上传。
|
||||
- 项目已建、用户随后补交招标文件(对话正文或已写入工作空间的文件)→ import_tender_file 导入,跳过上传步骤。
|
||||
- 用户问「招标文件上传了吗 / 解析出来了吗」→ list_tender_files 看上传与抽取状态。
|
||||
- 用户问「解析质量怎么样 / QC 过了吗」→ qc_report,讲清哪类产出通过、哪类还在改。
|
||||
- 用户问进展 → diagnose_bid(含章节分布、在办任务、最近评分),不要只报"正在进行"。
|
||||
@ -202,12 +208,26 @@ def _fmt_rows(rows, empty="(空)"):
|
||||
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):
|
||||
pid, err = _need_project(ctx)
|
||||
if err:
|
||||
return err
|
||||
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", ""),
|
||||
who="agent.main_agent")
|
||||
return _fmt(ok, msg)
|
||||
|
||||
|
||||
async def _h_assign_members(sor, p, ctx):
|
||||
pid, err = _need_project(ctx)
|
||||
if err:
|
||||
@ -373,6 +393,7 @@ 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,
|
||||
|
||||
@ -22,11 +22,15 @@ logger = logging.getLogger("pipeline.bidding.tender")
|
||||
# ══════════════ 立项 / 人员 / 招标文件 ══════════════
|
||||
|
||||
async def create_bid_project(name, description="", org_id="", created_by="",
|
||||
tender_content="", tender_file_name="",
|
||||
who=None, agent_id=None):
|
||||
"""直接创建投标项目(产线起点)+ 发「上传招标文件」任务。
|
||||
|
||||
立项不经过邮件采集/审批(那是商机产线的职责):用户/agent 直接创建投标项目,
|
||||
随后上传招标文件进入解析。
|
||||
|
||||
用户已提供招标文件(附件/正文)时传 tender_content:直接落库并跳过人工上传步骤,
|
||||
对账器下一轮即派发解析。
|
||||
"""
|
||||
if not name or not str(name).strip():
|
||||
return False, "缺少项目名称(name)"
|
||||
@ -48,6 +52,33 @@ async def create_bid_project(name, description="", org_id="", created_by="",
|
||||
await record(sor, pid, "sd_projects", pid, "create_bid_project",
|
||||
to_state="in_progress", who=who, agent_id=agent_id,
|
||||
detail="direct create")
|
||||
txt = str(tender_content or "").strip()
|
||||
if txt:
|
||||
# 用户已提供招标文件:直接落库,上传任务创建即完成(跳过人工上传步骤)
|
||||
fname = str(tender_file_name or "招标文件(对话提供)")[:250]
|
||||
await sor.C("bid_tender_files", {
|
||||
"id": new_id(), "project_id": pid,
|
||||
"file_name": fname, "file_type": "tender_doc",
|
||||
"content_text": txt[:2000000], "file_size": len(txt.encode("utf-8", "ignore")),
|
||||
"extract_status": "uploaded",
|
||||
"uploaded_by": created_by or (who or "agent.main_agent"),
|
||||
})
|
||||
hid = await create_human_task(
|
||||
sor, pid, HT_TENDER_FILE,
|
||||
"上传招标文件:%s" % pname,
|
||||
"立项时已随附招标文件,跳过人工上传步骤。",
|
||||
assignee_role="owner.superuser",
|
||||
form_schema={"project_id": pid})
|
||||
await sor.sqlExe(
|
||||
"UPDATE pipeline_human_tasks SET status='done', qc_status='passed', "
|
||||
"qc_comment='招标文件随立项提供,系统自动完成(跳过上传步骤)', "
|
||||
"submitted_at=NOW() WHERE id=${h}$", {"h": hid})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
await record(sor, pid, "bid_tender_files", hid, "import_tender_file",
|
||||
to_state="done", who=who, agent_id=agent_id,
|
||||
detail="tender content provided at creation, upload skipped")
|
||||
return True, ("project_id=%s(招标文件已导入 %d 字,上传步骤已跳过,"
|
||||
"系统将自动开始解析)" % (pid, len(txt)))
|
||||
hid = await create_human_task(
|
||||
sor, pid, HT_TENDER_FILE,
|
||||
"上传招标文件:%s" % pname,
|
||||
@ -58,6 +89,45 @@ async def create_bid_project(name, description="", org_id="", created_by="",
|
||||
return True, "project_id=%s(已发上传招标文件任务 %s)" % (pid, hid)
|
||||
|
||||
|
||||
async def import_tender_file(project_id, content_text="", file_path="", file_name="",
|
||||
who=None, agent_id=None):
|
||||
"""导入招标文件(已创建的项目补录):落库 bid_tender_files 并跳过人工上传步骤。
|
||||
|
||||
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:
|
||||
await sor.C("bid_tender_files", {
|
||||
"id": new_id(), "project_id": project_id,
|
||||
"file_name": str(file_name or ("招标文件(对话提供)" if txt else fp.split("/")[-1]))[:250],
|
||||
"file_type": "tender_doc",
|
||||
"content_text": txt[:2000000] if txt else "",
|
||||
"file_path": fp[:500],
|
||||
"file_size": len(txt.encode("utf-8", "ignore")) if txt else None,
|
||||
"extract_status": "uploaded",
|
||||
"uploaded_by": who or "agent.main_agent",
|
||||
})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
ht = await find_human_task(sor, project_id, 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",
|
||||
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))
|
||||
|
||||
|
||||
async def assign_members(project_id, members="", who=None, agent_id=None):
|
||||
"""设定参与人员。members 支持 JSON 数组或 "姓名:角色,姓名:角色" 简写。"""
|
||||
if not project_id:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user