#!/usr/bin/env python3 """ 从「知你客服9号」复制为「知你客服10号」,改进记忆与连接策略: 1. 在 json-parse 之后插入 code-identity-merge:把用户指定的「助手称呼」写入 memory.context.assistant_display_name (与 user_profile.name 区分)。 2. code-build-context:向 LLM 注入 assistant_display_name。 3. llm-unified 提示词:自我介绍优先用 assistant_display_name;user_profile.name 仅表示用户。 4. condition-need-summary:history_count >= 2 即走摘要分支(原常为 >=4,summary 易长期为空)。 需 Celery 已加载含 re/hashlib 注入的 workflow_engine(代码节点内勿写 import re)。 """ from __future__ import annotations import json import os import sys import requests BASE = os.getenv("PLATFORM_BASE_URL", "http://127.0.0.1:8037").rstrip("/") SOURCE_AGENT_ID = os.getenv("ZHINI_9_AGENT_ID", "de5932d6-3c05-4b27-ab08-f6cb403ce4b9") USER = os.getenv("PLATFORM_USERNAME", "admin") PWD = os.getenv("PLATFORM_PASSWORD", "123456") NEW_NAME = "知你客服10号" NEW_DESC = ( "在知你客服9号基础上:① memory.context.assistant_display_name 存助手对外称呼,与 user_profile.name(用户)分离;" "② 摘要分支 history_count>=2 更易生成 conversation_summary;" "③ 工作流在 json-parse 后增加 code-identity-merge 再进入抽取/写记忆。" ) CODE_IDENTITY_MERGE = r"""mem = dict(input_data.get('memory') or {}) ctx = dict(mem.get('context') or {}) q = str(input_data.get('query') or input_data.get('user_input') or '').strip() for pat in ( r'你的\s*名字\s*叫\s*([^\s,。!?,.!?]{1,32})', r'你\s*叫\s*(?!什么)([^\s,。!?,.!?]{1,32})', r'(?:客服|助手)\s*叫\s*([^\s,。!?,.!?]{1,32})', ): m = re.search(pat, q) if not m: continue name = m.group(1).strip().strip(',。!?,.!?') if not name: continue if any(b in name for b in ('什么', '哪位', '谁', '啥')): continue ctx['assistant_display_name'] = name break mem['context'] = ctx out = dict(input_data) out['memory'] = mem result = out """ CODE_BUILD_CONTEXT_V10 = r"""left = input_data.get('left') or {} right = input_data.get('right') or [] if not isinstance(right, list): right = [] mem = left.get('memory') or {} hist = mem.get('conversation_history') or [] if not isinstance(hist, list): hist = [] summary = mem.get('conversation_summary') or '' ctx = mem.get('context') or {} if not isinstance(ctx, dict): ctx = {} assistant_name = str(ctx.get('assistant_display_name') or '').strip() recent_n = 16 recent = hist[-recent_n:] if len(hist) > recent_n else hist recent_str = '\n'.join(f"{x.get('role', '')}: {x.get('content', '')}" for x in recent) vec_str = '\n'.join((rec.get('text') or rec.get('content') or '') for rec in right) query = (left.get('user_input') or left.get('query') or '').strip() older = hist[:-recent_n] if len(hist) > recent_n else [] def _tok(s): s = str(s) ch = {c for c in s if '\u4e00' <= c <= '\u9fff'} wd = set(s.lower().replace('\n', ' ').split()) return ch | wd qt = _tok(query) if query else set() scored = [] for m in older: c = str(m.get('content', '')) if not c: continue sc = len(qt & _tok(c)) if qt else 0 if sc > 0: scored.append((sc, str(m.get('role', '')), c[:240])) scored.sort(key=lambda x: -x[0]) kw_lines = [f"{role}: {text}" for _, role, text in scored[:6]] kw_str = '\n'.join(kw_lines) relevant_str = vec_str.strip() if kw_str: if relevant_str: relevant_str = relevant_str + '\n---\n关键词相关历史:\n' + kw_str else: relevant_str = '关键词相关历史:\n' + kw_str result = { 'user_input': left.get('user_input') or left.get('query') or '', 'memory': { 'user_profile': mem.get('user_profile') or {}, 'conversation_summary': summary, 'relevant_from_retrieval': relevant_str, 'recent_turns': recent_str, 'assistant_display_name': assistant_name, }, 'query': left.get('query') or '', 'user_id': left.get('user_id'), } """ LLM_PROMPT_V10 = """你是客服助手。根据用户输入、用户画像、助手称呼、远期摘要、检索片段与最近对话生成回复。 【称呼规则】 - user_profile.name(及同类字段)仅表示「用户」的昵称/姓名。 - memory.assistant_display_name 表示用户为你指定的「对外称呼」。若非空,用户问「你叫什么名字」「你是谁」时,须用该称呼自称(可带「客服助手」类前缀,但核心名须一致);禁止忽略已保存的 assistant_display_name 改回默认虚构名。 - 若 assistant_display_name 为空,可自称「客服助手」等通用名。 【任务】 1)判断意图;2)自然、有帮助的 reply(JSON 内一条字符串); 3)用户自我介绍姓名时写入 user_profile(如 name),勿把用户姓名写入 assistant_display_name; 4)用户问「我叫什么」时依据 user_profile 与历史/摘要回答。 只输出一行合法 JSON,不要 markdown。示例: {"intent":"chat","reply":"你好!","user_profile":{"name":"小明"}} 用户输入:{{user_input}} 用户画像:{{memory.user_profile}} 助手对外称呼(用户指定,可能为空):{{memory.assistant_display_name}} 远期摘要:{{memory.conversation_summary}} 相关历史(检索):{{memory.relevant_from_retrieval}} 最近几轮:{{memory.recent_turns}} 要求:reply 200 字以内;user_profile 为对象。""" def _insert_identity_node_and_edges(wf: dict) -> None: nodes = wf.setdefault("nodes", []) edges = wf.setdefault("edges", []) if any(n.get("id") == "code-identity-merge" for n in nodes): return # 参考 json-parse 位置:在其右侧插入 jx, jy = 2200, 400 for n in nodes: if n.get("id") == "json-parse": pos = n.get("position") or {} jx = pos.get("x", jx) + 80 jy = pos.get("y", jy) break nodes.append( { "id": "code-identity-merge", "type": "code", "position": {"x": jx, "y": jy}, "data": { "label": "合并助手称呼到 context", "language": "python", "code": CODE_IDENTITY_MERGE, }, } ) new_edges = [] removed = False for e in edges: if e.get("source") == "json-parse" and e.get("target") == "transform-extract-reply-and-profile": removed = True continue new_edges.append(e) if not removed: print("警告: 未找到 json-parse -> transform-extract-reply-and-profile 的边,仍追加新边", file=sys.stderr) new_edges.append( { "id": "e11a-identity", "source": "json-parse", "target": "code-identity-merge", "sourceHandle": "right", "targetHandle": "left", } ) new_edges.append( { "id": "e11b-identity", "source": "code-identity-merge", "target": "transform-extract-reply-and-profile", "sourceHandle": "right", "targetHandle": "left", } ) wf["edges"] = new_edges def _patch_nodes(wf: dict) -> None: nodes = wf.get("nodes") or [] for n in nodes: nid = n.get("id") if nid == "llm-unified": n.setdefault("data", {})["prompt"] = LLM_PROMPT_V10 elif nid == "code-build-context": n.setdefault("data", {})["code"] = CODE_BUILD_CONTEXT_V10 elif nid == "condition-need-summary": d = n.setdefault("data", {}) c = d.get("condition", "") if "history_count" in c and ">=" in c: d["condition"] = "{history_count} >= 2" else: d["condition"] = "{history_count} >= 2" elif nid == "code-identity-merge": n.setdefault("data", {})["code"] = CODE_IDENTITY_MERGE def main() -> int: r = requests.post( f"{BASE}/api/v1/auth/login", data={"username": USER, "password": PWD}, headers={"Content-Type": "application/x-www-form-urlencoded"}, timeout=15, ) if r.status_code != 200: print("登录失败:", r.status_code, r.text[:500], file=sys.stderr) return 1 token = r.json().get("access_token") if not token: print("无 access_token", file=sys.stderr) return 1 h = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} dup = requests.post( f"{BASE}/api/v1/agents/{SOURCE_AGENT_ID}/duplicate", headers=h, json={"name": NEW_NAME}, timeout=30, ) if dup.status_code != 201: print("复制失败:", dup.status_code, dup.text[:800], file=sys.stderr) return 1 new_id = dup.json()["id"] print("已创建副本:", new_id, NEW_NAME) g = requests.get(f"{BASE}/api/v1/agents/{new_id}", headers=h, timeout=30) if g.status_code != 200: print("读取 Agent 失败:", g.text, file=sys.stderr) return 1 agent = g.json() wf = agent["workflow_config"] _insert_identity_node_and_edges(wf) _patch_nodes(wf) up = requests.put( f"{BASE}/api/v1/agents/{new_id}", headers=h, json={"description": NEW_DESC, "workflow_config": wf}, timeout=120, ) if up.status_code != 200: print("更新失败:", up.status_code, up.text[:800], file=sys.stderr) return 1 print("已更新:identity 节点与边、摘要阈值>=2、上下文与 LLM 提示") print("Agent ID:", new_id) print(json.dumps({"id": new_id, "name": NEW_NAME}, ensure_ascii=False)) return 0 if __name__ == "__main__": raise SystemExit(main())