From d777f0ac33208a939d85c145948ed752d9ddf3c5 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sun, 30 Aug 2026 08:51:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(opp):=20=E5=8F=A3=E8=BF=B0=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=E6=A3=80=E7=B4=A2/=E6=B1=87=E6=80=BB=E2=80=94?= =?UTF-8?q?=E2=80=94opp=5Fquery=5Ftenders+=E5=85=88=E8=B0=83=E7=A0=94?= =?UTF-8?q?=E5=90=8E=E6=8A=A5=E5=91=8A=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 opp_query_tenders: 用户口述条件(关键词/地区/采购人/公告类型/来源/ 预算区间/AI评分/时间窗)→LLM映射参数,返回明细或按维度汇总(条数/预算/样例) - 数据层 query_tenders 透传爬虫平台 /api/tenders_query - prompt 改四步流程: 收集资料→数据分析→发现商机→产研报告(不直接写报告) --- pipeline_opportunity/opp_ability.py | 74 +++++++++++++++++++-- pipeline_opportunity/opp_data_capability.py | 20 ++++++ 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/pipeline_opportunity/opp_ability.py b/pipeline_opportunity/opp_ability.py index bb3a8ab..31c426d 100644 --- a/pipeline_opportunity/opp_ability.py +++ b/pipeline_opportunity/opp_ability.py @@ -32,6 +32,19 @@ OPP_TOOLS = [ ToolDefinition(name="opp_search_tenders", description="按关键词搜索招标信息(IT项目)", parameters={"keyword": "标题关键词", "days": "回溯天数(默认7)", "source": "来源(可选 ccgp/ggzy)", "limit": "条数(默认50)"}, category="data"), + ToolDefinition(name="opp_query_tenders", description="按用户口述条件检索/汇总招标数据:把自然语言条件映射为过滤参数(关键词/地区/采购人/公告类型/来源/预算区间/AI评分/时间窗),可返回明细或按维度汇总(条数/预算合计/平均预算/样例)。用户说『查一下XX地区预算大于XX的招标』『按地区统计XX』时用本工具", + parameters={"keyword": "标题关键词(多词空格分隔AND,可选)", + "region": "地区模糊(可选,如 浙江/北京)", + "buyer": "采购人模糊(可选)", + "notice_type": "公告类型精确(可选: 公开招标/中标公告/竞争性谈判/询价/单一来源)", + "source": "来源精确(可选 ccgp/ggzy)", + "min_budget": "预算下限(万元,可选)", + "max_budget": "预算上限(万元,可选)", + "min_ai": "AI评分下限(可选)", + "days": "回溯天数(默认90)", + "group_by": "汇总维度(可选: region/notice_type/category/source/month;不传=返回明细)", + "limit": "明细条数(默认50)", + "only_it": "只看IT项目(默认1,用户明确要全部时传0)"}, category="data"), ToolDefinition(name="opp_tender_detail", description="查看单条招标信息完整详情(含公告原文、预算、联系人、来源链接)", parameters={"tender_id": "招标信息ID"}, category="data"), ToolDefinition(name="opp_crawler_stats", description="查看数据爬取平台采集健康度(总量/24h新增/最近运行)", @@ -60,16 +73,34 @@ OPP_TOOLS = [ # ══════════════════ 产线 prompt 片段 ══════════════════ -OPP_PROMPT = """你是「商机产线」的驾驶舱 agent,负责把招标市场情报转化为软件研发商机: -热点软件推荐 → 选定方向 → 编写软件研发报告 → 人工确认 → 发起研发审批 → 跟踪审批结论。 +OPP_PROMPT = """你是「商机产线」的驾驶舱 agent,负责在项目内把招标市场情报转化为软件研发商机。 + +## 工作流程(先调研后报告,不要跳步) +项目创建后**不是直接写产研报告**,而是按四步推进: +1. 收集资料:用数据工具拉取与项目方向相关的招标情报(热点/每日/检索/条件查询)。 +2. 数据分析:对口述条件做过滤或汇总(opp_query_tenders),看数量/预算/地区/类型分布。 +3. 发现商机:基于真实数据指出值得做的软件方向(招标多、预算大、竞争未饱和)。 +4. 产研报告:方向明确后才 opp_create_report 建草稿、补写分析、提交人工确认。 +用户一上来就说「写报告」时,先确认是否已做过 1-3 步;没有就先补调研再写。 + +## 口述条件检索(核心能力) +用户用自然语言描述检索/统计条件,你负责映射为 opp_query_tenders 参数: +- 「查浙江预算100万以上的医院招标」→ region=浙江, min_budget=100, keyword=医院 +- 「按地区统计AI招标」→ keyword=AI/agent, group_by=region +- 「按月份看公开招标趋势」→ notice_type=公开招标, group_by=month +- 「采购人含大学的中标」→ buyer=大学, notice_type=中标公告 +映射原则:地区/采购人/关键词用模糊匹配;公告类型/来源用精确值; +预算单位是万元;用户没说的条件不要臆造(留空=不过滤)。 +返回明细时每条带 url/source_url 可查证;返回汇总时每组带样例。 ## 数据来源(重要边界) 招标数据全部来自「数据爬取平台」(内网 HTTP,已采集入库、已去重、每条带来源链接)。 -你只消费数据(opp_hot_software / opp_daily_ai_tenders / opp_search_tenders / opp_tender_detail), -**不要**自己去爬网站,也不要怀疑数据新鲜度——先看 opp_crawler_stats 的采集时间。 +你只消费数据(opp_hot_software / opp_daily_ai_tenders / opp_search_tenders / +opp_query_tenders / opp_tender_detail),**不要**自己去爬网站, +也不要怀疑数据新鲜度——先看 opp_crawler_stats 的采集时间。 ## 商机闭环与门禁(不要绕过) -1. 用 opp_hot_software 看哪个软件方向招标多、预算大,给用户推荐。 +1. 用 opp_hot_software / opp_query_tenders 看哪个软件方向招标多、预算大,给用户推荐。 2. 用户选定方向后:opp_create_report 建草稿 → 你基于真实招标数据补写分析 (市场空间/代表项目/竞争格局/研发建议)→ opp_update_report 更新全文。 3. 写完用 opp_submit_report 提交**人工确认**(门禁)。确认前不许发起审批。 @@ -80,7 +111,7 @@ OPP_PROMPT = """你是「商机产线」的驾驶舱 agent,负责把招标市 - 报告里的招标数量、预算、项目案例必须来自工具返回的真实数据,**禁止编造**; 查不到就如实写"数据未覆盖"。 - **所有分析必须带 references(可查证)**:给排名/统计/推荐结论时,调 - opp_category_references 取该主题的分项明细,回答末尾附 References 章节—— + opp_category_references 或 opp_query_tenders 取分项明细,回答末尾附 References 章节—— 列出自带的「📋 数据参考」菜单(用户可逐条点开查公告原文), 并在正文中给出 3-5 条代表性分项的标题+公告链接(markdown 链接,外链会自动新窗口打开)。 - 每条引用的招标信息带来源链接(爬虫平台已提供 url / source_url)。 @@ -89,8 +120,10 @@ OPP_PROMPT = """你是「商机产线」的驾驶舱 agent,负责把招标市 ## 典型场景 - 用户问「最近什么软件最火/最值得做」→ opp_hot_software(days=30) 后给结论, 并对推荐的主题调 opp_category_references 附 references。 +- 用户口述检索/统计条件 → opp_query_tenders 映射参数,返回明细或汇总并附样例链接。 - 用户问「今天有什么AI招标」→ opp_daily_ai_tenders()。 -- 用户说「给XX写个研发报告」→ opp_create_report → 拉数据补写 → opp_submit_report。 +- 用户说「给XX写个研发报告」→ 先确认调研充分(1-3步),再 + opp_create_report → 拉数据补写 → opp_submit_report。 - 用户问进展 → opp_diagnose(报告/审批分布 + 待办人工任务)。""" # ══════════════════ 角色集(商机产线轻量:分析+撰写两角色)══════════════════ @@ -166,6 +199,32 @@ async def _h_search(sor, p, ctx): ensure_ascii=False, default=str) +async def _h_query(sor, p, ctx): + from .opp_data_capability import query_tenders + ok, res = await query_tenders( + sor, keyword=p.get("keyword") or "", region=p.get("region") or "", + buyer=p.get("buyer") or "", notice_type=p.get("notice_type") or "", + source=p.get("source") or "", min_budget=p.get("min_budget") or "", + max_budget=p.get("max_budget") or "", min_ai=p.get("min_ai") or "", + days=int(p.get("days") or 90), group_by=p.get("group_by") or "", + limit=int(p.get("limit") or 50), + only_it=str(p.get("only_it", "1")) != "0") + if not ok or not isinstance(res, dict): + return _fmt(False, res) + if res.get("group_by"): + return json.dumps({ + "汇总维度": res.get("group_by"), + "命中总数": res.get("matched"), + "分组": res.get("groups"), + "说明": "cnt=条数 with_budget=有预算条数 total/avg_budget_wan=预算合计/平均(万元),samples 带公告链接可查证", + }, ensure_ascii=False, default=str) + return json.dumps({ + "命中总数": res.get("matched"), "返回条数": res.get("count"), + "items": res.get("items"), + "说明": "items 每条带 url(公告原文)/source_url(采集来源) 可查证;命中多于返回时用 limit 翻页或收紧条件", + }, ensure_ascii=False, default=str) + + async def _h_detail(sor, p, ctx): from .opp_data_capability import tender_detail ok, res = await tender_detail(sor, p.get("tender_id") or "") @@ -273,6 +332,7 @@ OPP_HANDLERS = { "opp_hot_software": _h_hot_software, "opp_daily_ai_tenders": _h_daily_ai, "opp_search_tenders": _h_search, + "opp_query_tenders": _h_query, "opp_tender_detail": _h_detail, "opp_crawler_stats": _h_crawler_stats, "opp_category_references": _h_category_references, diff --git a/pipeline_opportunity/opp_data_capability.py b/pipeline_opportunity/opp_data_capability.py index f16ffa2..7c6d0c7 100644 --- a/pipeline_opportunity/opp_data_capability.py +++ b/pipeline_opportunity/opp_data_capability.py @@ -49,3 +49,23 @@ async def category_references(sor, category, days=30, limit=500): async def crawler_stats(sor): """爬虫平台采集健康度。""" return await crawler_get(sor, "/api/stats") + + +async def query_tenders(sor, keyword="", region="", buyer="", notice_type="", + source="", min_budget="", max_budget="", min_ai="", + days=90, group_by="", limit=50, only_it=True): + """条件检索 + 汇总(用户口述条件 → LLM 映射为参数)。 + + 过滤:keyword(标题多词AND)/region/buyer 模糊,notice_type/source 精确, + min_budget/max_budget(万元)区间,min_ai 评分下限,days 回溯窗口,only_it。 + 汇总:group_by=region|notice_type|category|source|month → 分组统计+样例。 + """ + params: dict = {"days": days, "limit": limit} + for k, v in (("keyword", keyword), ("region", region), ("buyer", buyer), + ("notice_type", notice_type), ("source", source), + ("min_budget", min_budget), ("max_budget", max_budget), + ("min_ai", min_ai), ("group_by", group_by)): + if v not in (None, ""): + params[k] = v + params["only_it"] = "1" if only_it else "0" + return await crawler_get(sor, "/api/tenders_query", params)