119 lines
5.4 KiB
Python
119 lines
5.4 KiB
Python
"""自主学习闭环第一步:任务成功后自动生成 learned 草稿。
|
||
|
||
来源两路:
|
||
1. 产物直接映射:entry_url -> 入口地址,form_fields -> 表单字段,menus -> 菜单结构
|
||
2. 一次模型调用:从 Agent 的总结和访问过的页面里提炼"下次同类任务能直接复用的操作知识"
|
||
草稿写入 learned_entries(status=draft),用户在桌面端采纳后才会注入 prompt。
|
||
"""
|
||
|
||
import json
|
||
from typing import Any
|
||
|
||
from backend.browser.llm_factory import get_llm_config
|
||
from backend.nlu import _call_llm
|
||
|
||
ARTIFACT_KEYS = {
|
||
"entry_url": "入口地址",
|
||
"form_fields": "表单字段",
|
||
"menus": "菜单结构",
|
||
}
|
||
MAX_ENTRIES = 8
|
||
|
||
|
||
def _is_submit_step(step: dict) -> bool:
|
||
return bool(step.get("human_confirm_before") or step.get("on_success_status") == "submitted")
|
||
|
||
|
||
def _from_artifacts(artifacts: dict, step: dict) -> list[dict]:
|
||
entries = []
|
||
for art_key, learned_key in ARTIFACT_KEYS.items():
|
||
value = artifacts.get(art_key)
|
||
if not value:
|
||
continue
|
||
# 填报类步骤的 form_fields 是本次填的具体值(班级、人名…),不是表单结构,不能当知识
|
||
if art_key == "form_fields" and _is_submit_step(step):
|
||
continue
|
||
entries.append({"key": learned_key, "value": str(value).strip()[:2000], "category": "path"})
|
||
return entries
|
||
|
||
|
||
def _build_prompt(
|
||
business: dict, step: dict, summary: str, history: dict, existing: dict, existing_drafts: dict | None = None
|
||
) -> str:
|
||
urls = history.get("urls") or []
|
||
existing_lines = "\n".join(f"- {k}:{str(v)[:120]}" for k, v in existing.items()) or "(无)"
|
||
draft_lines = "\n".join(f"- {k}:{str(v)[:120]}" for k, v in (existing_drafts or {}).items()) or "(无)"
|
||
return "\n".join(
|
||
[
|
||
"你是学校办事助手的知识沉淀模块。下面是一次成功执行的记录,请提炼出「下次执行同类任务时能直接复用的操作知识」。",
|
||
"",
|
||
f"业务:{business.get('name', '')}",
|
||
f"本步目标:{step.get('goal', '')}",
|
||
f"访问过的页面(按顺序):{json.dumps(urls[:20], ensure_ascii=False)}",
|
||
f"执行步数:{history.get('steps')},用时 {history.get('duration_s')} 秒",
|
||
"",
|
||
"Agent 的执行总结:",
|
||
summary[:3000],
|
||
"",
|
||
"已经生效的知识(不要重复):",
|
||
existing_lines,
|
||
"",
|
||
"待审核的草稿(如果这次又观察到同一条知识,请沿用完全相同的 key,value 可以补充得更完整;这是它被多次验证的依据):",
|
||
draft_lines,
|
||
"",
|
||
"要求:",
|
||
"1. 只记录操作知识:入口路径、控件操作技巧(如某个下拉要点文字容器才能选中、日期要键入后回车)、系统规则和前置条件、容易踩的坑",
|
||
"2. 不要记录本次的具体业务数据:班级、日期、人名、课程名、填了什么内容都不要",
|
||
"2b. 不要记录本次用户临时指令的处理方式:取消、测试、干跑、评测、不要提交这类都不是知识",
|
||
"2c. 不要复述任务提示里已经写明的流程(如提交前要确认),只记录执行中新发现的东西",
|
||
"3. 每条 key 用 4 到 12 个字的名词短语,value 用一到三句话,具体可执行",
|
||
"4. 每条给 category:control_tip(控件操作技巧)/ path(入口路径、页面地址、菜单结构)/ rule(系统规则、前置条件、达标要求、审批流程)/ note(其他)",
|
||
"5. 最多 5 条,没有新知识就返回空数组",
|
||
'输出 JSON:{"entries": [{"key": "...", "value": "...", "category": "control_tip"}]}',
|
||
]
|
||
)
|
||
|
||
|
||
async def draft_learned(
|
||
business: dict,
|
||
step: dict,
|
||
artifacts: dict,
|
||
history: dict | None,
|
||
summary: str,
|
||
existing: dict | None = None,
|
||
existing_drafts: dict | None = None,
|
||
) -> tuple[list[dict], str]:
|
||
"""返回 (草稿列表, 备注)。每条 {key, value, category}。模型不可用时只返回产物映射的条目。"""
|
||
existing = existing or {}
|
||
entries: list[dict] = _from_artifacts(artifacts or {}, step)
|
||
note = "产物映射"
|
||
cfg = get_llm_config()
|
||
if cfg.get("api_key"):
|
||
try:
|
||
result: Any = await _call_llm(
|
||
_build_prompt(business, step, summary or "", history or {}, existing, existing_drafts), cfg
|
||
)
|
||
llm_entries = (result or {}).get("entries") or []
|
||
for e in llm_entries:
|
||
if not isinstance(e, dict):
|
||
continue
|
||
key = str(e.get("key") or "").strip()
|
||
value = str(e.get("value") or "").strip()
|
||
category = str(e.get("category") or "note").strip()
|
||
if key and value:
|
||
entries.append({"key": key[:128], "value": value[:2000], "category": category})
|
||
note = f"产物映射 + 模型提炼 {len(llm_entries)} 条"
|
||
except Exception as e: # noqa: BLE001
|
||
note = f"产物映射(模型提炼失败:{type(e).__name__})"
|
||
|
||
seen: set[str] = set()
|
||
unique: list[dict] = []
|
||
for e in entries:
|
||
if e["key"] in seen:
|
||
continue
|
||
if existing.get(e["key"]) == e["value"]:
|
||
continue
|
||
seen.add(e["key"])
|
||
unique.append(e)
|
||
return unique[:MAX_ENTRIES], note
|