assistant/backend/prospecting/request_routing.py

115 lines
7.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Narrow execution requirements grounded in the latest human instruction."""
from __future__ import annotations
import re
from backend.config import settings
from backend.prospecting.contracts import IntentPlan, ProspectingBrief, TARGET_QUESTION
from backend.browser.preflight_policy import PreflightPolicy, compile_preflight_policy
def _clauses(message: str):
text = re.sub(r"```[\s\S]*?```", "", message)
# A tool name formatted inline may still be an instruction. Whole quoted
# requests, code examples and block quotations remain untrusted material.
text = re.sub(r"`(resolve_reply_target(?:\([^`\n]*\))?)`", r"\1", text)
text = re.sub(r'''“[^”]*”|‘[^’]*’|「[^」]*」|『[^』]*』|"[^"\n]*"|'[^'\n]*'|`[^`\n]*`''', "", text)
text = "\n".join(line for line in text.splitlines() if not line.lstrip().startswith(">"))
for clause in re.split(r"[,,。;;!?!?\n]|(?:但是|但要|但请|不过|而是)", text):
if clause.strip() and not re.search(r"例如|比如|示例|参考案例|只是.{0,8}(?:引用|日志|描述)|(?:日志|文档|网页|截图).{0,8}(?:写着|提示|显示|内容)|(?:解释|介绍|讨论|了解).{0,15}(?:resolve_reply_target|网页回查|返回原笔记)|resolve_reply_target.{0,6}(?:是什么|什么意思|怎么用)", clause):
yield clause.strip()
_BROWSER_RECHECK = re.compile(
r"resolve_reply_target(?:\([^)]*\))?"
r"|(?:原生历史|浏览历史|历史记录)(?:返回|回退|后退)"
r"|(?:原生)?(?:历史)?(?:返回|回到|回退|退回)(?:到)?(?:原|刚才|之前|该|这篇|同一篇|实际|真实|当前|已保存的)?(?:笔记|详情页|页面|网页|浏览器)"
r"|(?:网页|页面|浏览器|原页|原笔记|详情页)(?:上|中|里)?(?:实际|重新|再次|现场|直接|进行|去|上)?(?:回查|核对|复核|检查|确认|查证|验证)"
r"|(?:回查|核对|复核|检查|确认|查证|验证)(?:一下|当前|实际|真实)?(?:网页|页面|原页|原笔记|浏览器)"
r"|(?:点击|打开|进入).{0,20}(?:回复前缀|回复对象|目标主页|作者主页|发言人主页|实际主页|真实主页).{0,12}(?:核|查|确认|验证)"
)
_NEGATIVE_PREFIX = re.compile(r"(?:不要|不用|无需|不必|不需要|不能|别|不再|停止|取消|放弃|暂不|先不|不是|不使用|不调用|不执行)[^,,。;;]{0,10}$")
_NEGATIVE_SUFFIX = re.compile(r"\s*(?:先|暂时)?(?:不用|无需|不必|不要|取消|停止|不执行|不需要)(?:做|执行|操作)?")
_BROWSER_DISABLED = re.compile(r"(?:不|不需要|无需|不用|不必|不要|别)(?:再|实际)?(?:操作|打开|访问|使用|进入)(?:任何|实际)?(?:网页|页面|浏览器)")
_BROWSER_CANCELLED = re.compile(r"(?:取消|停止|放弃|不再|不要|不用|不需要|无需|不必)(?:(?:本次|本轮|当前|实际)的?)?(?:网页|页面|浏览器)?回查")
def browser_recheck_requested(message: str) -> bool:
"""Stored sources do not imply an offline job when a web action is explicit."""
page_required = bool(settings.reply_relation_preflight_enabled and compile_preflight_policy(message).active and re.search(
r"(?:当前|这篇|原)(?:笔记|页面).{0,16}预检|预检.{0,16}(?:当前|这篇|原)(?:笔记|页面)", message))
actions_required = set()
for clause in _clauses(message):
if _BROWSER_DISABLED.search(clause) or _BROWSER_CANCELLED.search(clause):
page_required = False
actions_required.clear()
continue
for match in _BROWSER_RECHECK.finditer(clause):
value = match.group()
is_tool = value.startswith("resolve_reply_target")
if is_tool and not (
re.search(r"(?:用|使用|调用|执行|运行)(?:工具)?\s*$", clause[:match.start()])
or re.match(r"\s*(?:来|去)?(?:核对|核实|核|回查|检查)", clause[match.end():])
or _NEGATIVE_PREFIX.search(clause[:match.start()]) or _NEGATIVE_SUFFIX.match(clause[match.end():])
or clause == match.group()):
continue
negative = bool(_NEGATIVE_PREFIX.search(clause[:match.start()])
or _NEGATIVE_SUFFIX.match(clause[match.end():]))
action = ('resolve_reply_target' if is_tool else 'return' if re.match(
r"(?:原生历史|浏览历史|历史记录|(?:原生)?(?:历史)?(?:返回|回到|回退|退回))", value)
else 'profile' if re.match(r"点击|打开|进入", value) else None)
if action:
# Cancelling one browser operation does not cancel an
# independent instruction to inspect the actual page.
if negative:
actions_required.discard(action)
else:
actions_required.add(action)
elif negative:
page_required = False
actions_required.clear()
else:
page_required = True
return page_required or bool(actions_required)
def browser_recheck_brief(brief: ProspectingBrief, message: str) -> ProspectingBrief:
# A focused read-only recheck has an existing object and must not be
# short-circuited by a discovery headcount or an export-only route.
if browser_recheck_requested(message) and brief.intent in {"discover", "export"}:
return brief.model_copy(update={"intent": "inspect"})
return brief
def ground_execution_plan(plan: IntentPlan, message: str) -> IntentPlan:
policy = (compile_preflight_policy(message, plan.brief.max_notes)
if settings.reply_relation_preflight_enabled else PreflightPolicy())
if policy.active:
brief = plan.brief.model_copy(update={"intent": "inspect", "max_notes": policy.max_notes})
return plan.model_copy(update={"brief": brief, "reuse_existing_sources": False,
"missing": [item for item in plan.missing if item != TARGET_QUESTION],
"summary": "只读预检当前可见回复关系,按实际记录交付范围与未读部分,本轮不保存来源或生成名单。",
"stages": ["核对访问范围", "只读关系预检", "交付预检结果"]})
if not browser_recheck_requested(message):
return plan
brief = browser_recheck_brief(plan.brief, message)
missing = [item for item in plan.missing if item != TARGET_QUESTION or TARGET_QUESTION in brief.missing()]
return plan.model_copy(update={
"brief": brief, "reuse_existing_sources": False, "missing": missing,
"summary": "按你的要求回到实际页面核对既有来源,保留采集范围和未知项。",
"stages": list(dict.fromkeys(["实际页面回查", *plan.stages])),
})
def recheck_without_analysis(message: str) -> bool:
"""Only an explicit analysis prohibition in an explicit recheck can skip it."""
if not browser_recheck_requested(message):
return False
forbidden = False
for clause in _clauses(message):
if re.search(r"(?:不|不想|无需|不用|不必|不要|别|禁止|停止|取消)(?:再|重新|重复)?(?:搜索(?:或|和|及))?(?:再|重新|重复)?(?:分析(?:名单|候选|需求)?|做(?:名单|候选|需求)分析)", clause):
forbidden = True
elif re.search(r"(?:重新|再次|再|仍要|继续|需要|只需|仍需|请|要)分析(?:名单|候选|需求)?", clause):
forbidden = False
return forbidden