feat(bidding): 多session会话入口 + 从上传招标文件开始(砍邮件采集/审批)
- 新增 /pipeline-bidding/agent/index.ui 多 session 会话界面(复用通用 agent) - bid_ability 砍 fetch_tenders/import_tender/list_tenders/tender_detail/summarize/set_fields/submit/approve/reject/archive/reopen 等邮件采集+审批工具 - create_bid_project 改为直接创建项目+发上传招标文件任务(不依赖 tender) - 删除招标信息员角色,菜单改招标文件/评分项/章节/资质/知识库/标书 - bid_flow 去掉审批门禁,无招标文件则自动发上传任务作为产线起点
This commit is contained in:
parent
e134ecde4a
commit
7ceedefeb7
@ -22,39 +22,9 @@ 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="create_bid_project", description="创建投标项目(直接立项,不依赖招标信息)。用户说「创建/新建投标项目」时调用",
|
||||
parameters={"name": "项目名称", "description": "项目描述(可选)"}, category="project"),
|
||||
ToolDefinition(name="assign_members", description="设定投标项目参与人员,如「张三:负责人,李四:writer」",
|
||||
parameters={"members": "人员清单(姓名:角色 逗号分隔,或JSON数组)"}, category="project"),
|
||||
ToolDefinition(name="list_members", description="查看投标项目参与人员", parameters={}, category="project"),
|
||||
@ -103,38 +73,33 @@ BID_TOOLS = [
|
||||
|
||||
# ══════════════════ 产线 prompt 片段 ══════════════════
|
||||
|
||||
BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从招标信息到标书交付的全流程管理:
|
||||
招标信息采集 → 内容摘要 → 审批 → 立项与人员 → 上传招标文件 → 解析(评分项/得分规则/资质/投标文件要求)
|
||||
BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从上传招标文件到标书交付的全流程管理:
|
||||
创建投标项目 → 上传招标文件 → 解析(评分项/得分规则/资质/投标文件要求/章节骨架)
|
||||
→ 资质文档准备 → 分章节编写 → 章节评审 → 合成标书 → 整书评分 → 交付确认。
|
||||
|
||||
## 流转由机制驱动(重要)
|
||||
角色任务由系统状态对账器自动派发(每 15 秒一轮),你**不需要**手动为每个章节创建任务:
|
||||
- 章节评审不达标 → 章节自动退回重写
|
||||
- 整书评分不达标 → 改进意见自动落到问题章节并退回重写 → 重新合成 → 重新评分
|
||||
你的职责是:采集与摘要、推动审批、立项与配人、处理卡点与提问、回答评审/评分中的人类决策。
|
||||
你的职责是:立项与配人、推动上传招标文件、处理卡点与提问、回答评审/评分中的人类决策。
|
||||
只有诊断到「机制该派任务却没派」时,才用 advance_bid 手动触发一次对账,或用 diagnose_bid 查根因。
|
||||
|
||||
## 阻塞门禁(不要绕过)
|
||||
以下人类任务 pending 时,系统不会给该项目派发任何 agent 任务:
|
||||
- 招标信息审批未决(tender_approval)
|
||||
- 等待上传招标文件(tender_file_upload,上传后自动完成)
|
||||
- 等待人类提供资质文件(human_docs_request)
|
||||
遇到项目"不动了",先 diagnose_bid 看是不是卡在这三个门禁上,缺什么就催人补什么,不要硬推。
|
||||
遇到项目"不动了",先 diagnose_bid 看是不是卡在这两个门禁上,缺什么就催人补什么,不要硬推。
|
||||
|
||||
## 典型场景
|
||||
- 用户说「看看有什么新招标 / 收一下招标邮件」→ fetch_tenders,再 list_tenders 看结果。
|
||||
- 用户说「这条招标怎么样 / 帮我分析下」→ tender_detail + summarize_tender,把投标开始时间、
|
||||
项目周期、招标金额、保证金、资质门槛、风险点讲清楚,再问是否提交审批。
|
||||
- 用户说「提交审批」→ submit_tender_approval;用户直接拍板通过/不通过 → approve_tender / reject_tender。
|
||||
**审批不通过就是终态**:招标信息自动存档作废,不要再立项。
|
||||
- 审批通过后 → 自动立项并发上传招标文件任务;接着 assign_members 设定参与人员。
|
||||
- 用户说「创建一个投标项目,叫 XXX」→ create_bid_project(name=XXX),随后提示上传招标文件。
|
||||
- 用户问「招标文件上传了吗 / 解析出来了吗」→ list_tender_files 看上传与抽取状态。
|
||||
- 用户问进展 → diagnose_bid(含章节分布、在办任务、最近评分),不要只报"正在进行"。
|
||||
- 用户问「能得多少分 / 差在哪」→ bid_score_report + list_reviews,讲清失分项与改进意见落到了哪个章节。
|
||||
- 用户要补资质/案例 → add_kb_doc 录入公司投标知识库;知识库缺的用 request_human_docs 让人工提供。
|
||||
|
||||
## 硬规则
|
||||
- 招标金额、投标时间、资质要求这些要素,只能来自招标公告/招标文件原文或人工确认,**禁止编造**;
|
||||
抽取不到就如实说"公告未载明",必要时 ask_user。
|
||||
- 招标金额、投标时间、资质要求这些要素,只能来自招标文件原文或人工确认,**禁止编造**;
|
||||
抽取不到就如实说"文件未载明",必要时 ask_user。
|
||||
- 标书内容的案例与业绩必须来自公司投标知识库的真实合同(bid_kb_docs 里 doc_type=case),
|
||||
禁止虚构客户名、合同金额、历史成果数字。
|
||||
- 章节没全部评审通过,不允许合成标书;评分没过门限,不允许当作可交付。"""
|
||||
@ -142,14 +107,6 @@ BID_PROMPT = """你是「投标产线」的驾驶舱 agent,负责从招标信
|
||||
# ══════════════════ 角色集 ══════════════════
|
||||
|
||||
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="招标文件分析师",
|
||||
@ -234,98 +191,9 @@ def _fmt_rows(rows, empty="(空)"):
|
||||
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", ""),
|
||||
ok, msg = await create_bid_project(p.get("name", ""), description=p.get("description", ""),
|
||||
org_id=ctx.get("org_id", "") or "",
|
||||
created_by=ctx.get("user_id", "") or "",
|
||||
who="agent.main_agent")
|
||||
@ -488,17 +356,6 @@ async def _h_advance_bid(sor, p, ctx):
|
||||
|
||||
|
||||
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,
|
||||
@ -546,8 +403,8 @@ def register_bid_ability():
|
||||
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_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": "📝 标书章节", "icon": "", "url": "/pipeline-bidding/bid_chapters/index.ui",
|
||||
@ -558,8 +415,6 @@ def register_bid_ability():
|
||||
"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)
|
||||
@ -568,11 +423,11 @@ def register_bid_ability():
|
||||
|
||||
# ══════════════════ slash 命令(产线专属)══════════════════
|
||||
|
||||
async def _slash_tenders(args, ctx):
|
||||
async def _slash_files(args, ctx):
|
||||
ex = ctx.get("executor")
|
||||
if not ex:
|
||||
return "无执行器上下文"
|
||||
return await ex._execute_ability_tool("list_tenders", {"status": (args or "").strip()})
|
||||
return await ex._execute_ability_tool("list_tender_files", {})
|
||||
|
||||
|
||||
async def _slash_bid(args, ctx):
|
||||
@ -598,7 +453,7 @@ async def _slash_score(args, ctx):
|
||||
|
||||
def register_bid_slash_commands():
|
||||
for cmd in [
|
||||
SlashCommand("tenders", "列出招标信息(可带状态)", _slash_tenders, "pipeline", PIPELINE_ID),
|
||||
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),
|
||||
|
||||
@ -252,7 +252,7 @@ async def has_done_task(sor, project_id, role):
|
||||
|
||||
async def has_pending_blocking_human_task(sor, project_id):
|
||||
"""项目是否有阻塞中的人类任务(投标产线无迭代,按 project_id 判定)。"""
|
||||
types = "','".join(BLOCKING_HT_TYPES + (HT_TENDER_APPROVAL,))
|
||||
types = "','".join(BLOCKING_HT_TYPES)
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT COUNT(*) AS c FROM pipeline_human_tasks "
|
||||
"WHERE project_id=${pid}$ AND status='pending' "
|
||||
|
||||
@ -20,7 +20,7 @@ from .bid_common import (
|
||||
get_db, rows_to_dicts, rec_to_dict, to_int, to_float, json_loads,
|
||||
create_role_task, count_open_tasks, has_done_task,
|
||||
has_pending_blocking_human_task, find_human_task, get_thresholds,
|
||||
create_human_task, PIPELINE_ID,
|
||||
create_human_task, get_project, PIPELINE_ID,
|
||||
CH_PENDING, CH_WRITING, CH_WRITTEN, CH_REJECTED, CH_APPROVED,
|
||||
DOC_DRAFT, DOC_REVIEWING, DOC_PASSED, DOC_REJECTED, DOC_DELIVERED,
|
||||
HT_TENDER_FILE, HT_HUMAN_DOCS, HT_DELIVERY_CONFIRM,
|
||||
@ -106,8 +106,23 @@ async def reconcile_project(sor, project_id, project_name=""):
|
||||
|
||||
# ── 0b. 招标文件到位则自动关闭上传任务,再判阻塞门禁 ──
|
||||
await _auto_close_file_upload_task(sor, project_id)
|
||||
# 产线起点兜底:项目无任何招标文件且无待办上传任务 → 自动发「上传招标文件」任务
|
||||
# (无论项目经 create_bid_project 还是通用 create_project 创建,都从上传文件起步)
|
||||
_n_files_pre = await _count(sor, "SELECT COUNT(*) AS c FROM bid_tender_files "
|
||||
"WHERE project_id=${p}$", {"p": project_id})
|
||||
if _n_files_pre <= 0:
|
||||
_up_ht = await find_human_task(sor, project_id, HT_TENDER_FILE, status="pending")
|
||||
if not _up_ht:
|
||||
p = await get_project(sor, project_id)
|
||||
hid = await create_human_task(
|
||||
sor, project_id, HT_TENDER_FILE,
|
||||
"上传招标文件:%s" % (p.get("name", "") or project_id),
|
||||
("请上传招标文件(含答疑/补遗文件)。\n"
|
||||
"上传后系统自动进入招标文件解析(抽取评分项、得分规则、所需资质、投标文件要求、章节骨架)。"))
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
return acts + ["created: 上传招标文件任务 %s" % hid]
|
||||
if await has_pending_blocking_human_task(sor, project_id):
|
||||
return ["blocked: 有待办人类任务(等招标文件/等人类资料/等审批),本轮不派发"]
|
||||
return ["blocked: 有待办人类任务(等招标文件/等人类资料),本轮不派发"]
|
||||
|
||||
th = await get_thresholds(sor)
|
||||
|
||||
|
||||
@ -332,45 +332,41 @@ async def _close_approval_task(sor, tender_id, status, comment=""):
|
||||
|
||||
# ══════════════ 立项 / 人员 / 招标文件 ══════════════
|
||||
|
||||
async def create_bid_project(tender_id, name="", org_id="", created_by="",
|
||||
async def create_bid_project(name, description="", org_id="", created_by="",
|
||||
who=None, agent_id=None):
|
||||
"""由招标信息立项:写 sd_projects(pipeline_id=bidding_general) + 发「上传招标文件」人类任务。"""
|
||||
"""直接创建投标项目(不依赖招标信息前置)+ 发「上传招标文件」任务。
|
||||
|
||||
产线入口改为「从上传招标文件开始」后,立项不再经过邮件采集/审批,
|
||||
而是用户/agent 直接创建投标项目,随后上传招标文件进入解析。
|
||||
"""
|
||||
if not name or not str(name).strip():
|
||||
return False, "缺少项目名称(name)"
|
||||
db, dbname = get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
t = await _get_tender(sor, tender_id)
|
||||
if not t:
|
||||
return False, "招标信息不存在: %s" % tender_id
|
||||
if t.get("project_id"):
|
||||
return True, "项目已存在: %s" % t.get("project_id")
|
||||
if t.get("status") != TD_APPROVED:
|
||||
return False, "招标信息未审批通过(当前 %s),不可立项" % t.get("status")
|
||||
pid = new_id()
|
||||
pname = (name or t.get("title") or t.get("mail_subject") or "投标项目")[:190]
|
||||
pname = str(name).strip()[:190]
|
||||
await sor.C("sd_projects", {
|
||||
"id": pid,
|
||||
"name": pname,
|
||||
"description": (t.get("summary") or "")[:4000],
|
||||
"description": (description or "")[:4000],
|
||||
"project_type": "bidding",
|
||||
"pipeline_id": PIPELINE_ID,
|
||||
"status": "in_progress",
|
||||
"org_id": org_id or t.get("org_id") or "0",
|
||||
"created_by": created_by or (who or "agent.tender_scout"),
|
||||
"org_id": org_id or "0",
|
||||
"created_by": created_by or (who or "agent.pm"),
|
||||
})
|
||||
await sor.sqlExe(
|
||||
"UPDATE " + TABLE + " SET project_id=${pid}$, updated_at=NOW() WHERE id=${tid}$",
|
||||
{"pid": pid, "tid": tender_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
await record(sor, pid, "sd_projects", pid, "create_bid_project",
|
||||
to_state="in_progress", who=who, agent_id=agent_id,
|
||||
detail="tender_id=" + tender_id)
|
||||
detail="direct create (no tender)")
|
||||
hid = await create_human_task(
|
||||
sor, pid, HT_TENDER_FILE,
|
||||
"上传招标文件:%s" % pname,
|
||||
("项目已立项,请上传招标文件(含答疑/补遗文件)。\n"
|
||||
"上传后系统自动进入招标文件解析(抽取评分项、得分规则、所需资质、投标文件要求)。\n"
|
||||
("项目已创建,请上传招标文件(含答疑/补遗文件)。\n"
|
||||
"上传后系统自动进入招标文件解析(抽取评分项、得分规则、所需资质、投标文件要求、章节骨架)。\n"
|
||||
"上传入口:投标产线 → 招标文件(bid_tender_files)。"),
|
||||
assignee_role="owner.superuser",
|
||||
form_schema={"project_id": pid, "tender_id": tender_id})
|
||||
form_schema={"project_id": pid})
|
||||
return True, "project_id=%s(已发上传招标文件任务 %s)" % (pid, hid)
|
||||
|
||||
|
||||
|
||||
@ -15,46 +15,7 @@ logger = logging.getLogger("pipeline.bidding.tools")
|
||||
_M = "pipeline_bidding"
|
||||
|
||||
BID_ROLE_TOOL_SCHEMAS = {
|
||||
# ── bid_tender_capability:招标信息员 ──
|
||||
"fetch_tenders": {
|
||||
"module": f"{_M}.bid_tender_capability",
|
||||
"description": "从配置的招标邮箱采集招标信息入库(按关键词过滤+去重)",
|
||||
"params": {"days": "回溯天数(可选)", "account_id": "邮箱配置ID(可选)"},
|
||||
"required": [],
|
||||
},
|
||||
"summarize_tender": {
|
||||
"module": f"{_M}.bid_tender_capability",
|
||||
"description": "对招标信息做内容摘要与要素抽取(投标开始时间/项目周期/招标金额/保证金/资质要求)并落库",
|
||||
"params": {"tender_id": "招标信息ID", "model_name": "模型(可选)"},
|
||||
"required": ["tender_id"],
|
||||
},
|
||||
"set_tender_fields": {
|
||||
"module": f"{_M}.bid_tender_capability",
|
||||
"description": "修正招标信息要素(摘要抽错时纠正)",
|
||||
"params": {"tender_id": "招标信息ID", "title": "项目名称(可选)", "purchaser": "招标人(可选)",
|
||||
"bid_start_at": "投标开始时间(可选)", "bid_deadline": "投标截止(可选)",
|
||||
"project_period": "项目周期(可选)", "budget_amount": "招标金额(可选)",
|
||||
"summary": "摘要(可选)", "qualification_req": "资质要求(可选)"},
|
||||
"required": ["tender_id"],
|
||||
},
|
||||
"submit_tender_approval": {
|
||||
"module": f"{_M}.bid_tender_capability",
|
||||
"description": "提交招标信息审批(生成人类审批任务)",
|
||||
"params": {"tender_id": "招标信息ID", "note": "提交说明(可选)"},
|
||||
"required": ["tender_id"],
|
||||
},
|
||||
"list_tenders": {
|
||||
"module": f"{_M}.bid_tender_capability",
|
||||
"description": "列出招标信息",
|
||||
"params": {"status": "状态(可选)", "limit": "条数(可选)"},
|
||||
"required": [],
|
||||
},
|
||||
"archive_tender": {
|
||||
"module": f"{_M}.bid_tender_capability",
|
||||
"description": "存档作废招标信息",
|
||||
"params": {"tender_id": "招标信息ID", "reason": "原因"},
|
||||
"required": ["tender_id"],
|
||||
},
|
||||
# ── bid_tender_capability:立项/人员(产线从上传招标文件开始,无邮件采集/审批)──
|
||||
"assign_members": {
|
||||
"module": f"{_M}.bid_tender_capability",
|
||||
"description": "设定投标项目参与人员,如「张三:负责人,李四:writer」",
|
||||
|
||||
65
wwwroot/agent/index.ui
Normal file
65
wwwroot/agent/index.ui
Normal file
@ -0,0 +1,65 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"width": "100%", "height": "100%", "padding": "0"},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "HBox",
|
||||
"options": {"width": "100%", "alignItems": "center", "padding": "16px 24px 8px 24px", "cheight": 6, "gap": "12px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Title2", "options": {"text": "投标产线"}},
|
||||
{"widgettype": "Text", "options": {"text": "上传招标文件 → 解析 → 资质准备 → 分章节编写 → 评审 → 合成 → 评分 → 交付", "cfontsize": 0.9, "color": "#94a3b8"}},
|
||||
{"widgettype": "Filler"},
|
||||
{
|
||||
"widgettype": "Button",
|
||||
"id": "new_session_btn",
|
||||
"options": {"label": "+ 新建会话", "css": "small"},
|
||||
"binds": [{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "script",
|
||||
"target": "self",
|
||||
"script": "var tp=bricks.getWidgetById('session_tabs',bricks.app);if(!tp)return;var sid='s'+Date.now()+'_'+Math.floor(Math.random()*100000);var n=tp.opts.items.length+1;tp.open_tab({name:'session_'+sid,label:'会话 '+n,removable:true,content:{widgettype:'VBox',options:{css:'filler',width:'100%',height:'100%'},subwidgets:[{widgettype:'urlwidget',options:{url:'/pipeline_core/api/agent_menus.dspy?session_id='+sid+'&pipeline_id=bidding_general',method:'GET'}},{widgettype:'AgentIO',options:{css:'filler',margin:'0 24px 24px 24px',url:'/pipeline_core/api/agent_chat.dspy?session_id='+sid+'&pipeline_id=bidding_general',model_dataurl:'/pipeline_core/api/agent_model_options.dspy',model_cwidth:14,placeholder:'输入你的需求...'}}]}});"
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "TabPanel",
|
||||
"id": "session_tabs",
|
||||
"options": {
|
||||
"css": "filler",
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"tab_pos": "top",
|
||||
"items": [
|
||||
{
|
||||
"name": "session_default",
|
||||
"label": "会话 1",
|
||||
"removable": false,
|
||||
"content": {
|
||||
"widgettype": "VBox",
|
||||
"options": {"css": "filler", "width": "100%", "height": "100%"},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "urlwidget",
|
||||
"options": {"url": "{{entire_url('/pipeline_core/api/agent_menus.dspy')}}?session_id=default&pipeline_id=bidding_general", "method": "GET"}
|
||||
},
|
||||
{
|
||||
"widgettype": "AgentIO",
|
||||
"options": {
|
||||
"css": "filler",
|
||||
"margin": "0 24px 24px 24px",
|
||||
"url": "/pipeline_core/api/agent_chat.dspy?session_id=default&pipeline_id=bidding_general",
|
||||
"model_dataurl": "/pipeline_core/api/agent_model_options.dspy",
|
||||
"model_cwidth": 14,
|
||||
"placeholder": "输入你的需求..."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user