# agent_chat_generic.dspy - 纯通用会话(不挂任何产线插件) # 用于对照测试:确定通用 agent 本身的能力,隔离产线工具/技能/角色/记忆的干扰 # generic=True 时 gateway 跳过 resolve_project,不 load_agent_config 产线能力,只用 GENERAL_TOOLS + 通用心智 import json uid = await get_user() if not uid: uid = 'user-01' action = (params_kw or {}).get('action', 'send_message') if action == 'send_message': prompt = (params_kw or {}).get('prompt', '') or '' prompt = prompt.strip() if not prompt: return json.dumps({"error": "prompt 必填"}, ensure_ascii=False) # 停止类指令 stop_kw = ['停止', '取消', '停下', '终止', '停', 'stop', 'cancel', 'abort'] if prompt.strip().lower() in stop_kw: async def _stop(): yield json.dumps({"content": "已停止。"}, ensure_ascii=False) + '\n' yield json.dumps({"widgettype": "Text", "options": {"otext": "## 完毕 ##", "text": "## 完毕 ##", "i18n": True, "halign": "left", "css": "agent-done", "color": "#94a3b8", "marginBottom": "6px"}}, ensure_ascii=False) + '\n' return await stream_response(request, _stop, 'text/plain; charset=utf-8') from pipeline_service.gateway import get_gateway gateway = get_gateway() # 前端模型下拉选中的模型必须透传——之前忽略导致用户选了模型仍走默认值, # 默认模型名与 llm 表不匹配时直接"无可用模型"卡死。 model_id = (params_kw or {}).get('model_id', '') or '' # 上传文件:之前完全忽略 file 字段(上传静默丢弃)。与 agent_chat 走同一套处理: # 落盘到 workspace 会话目录(generic 无项目,read_file 根=WORKSPACE_BASE,相对路径可读)。 import os from ahserver.filestorage import FileStorage from sqlor.dbpools import DBPools from pipeline_core.upload_tools import extract_text, resolve_upload_dir, save_uploads, build_file_context, is_image _uploads = [] _img_uploads = [] # 图片上传(原生视觉,2026-09-10):不进文本上下文,走多模态注入 _fval = (params_kw or {}).get('file') _fpaths = _fval if isinstance(_fval, list) else ([_fval] if _fval else []) for _fp in _fpaths: try: _abs = FileStorage().realPath(_fp) _uploads.append((_abs, os.path.basename(_abs))) except Exception: pass if _uploads: try: _dbname = get_module_dbname('pipeline_core') async with DBPools().sqlorContext(_dbname) as sor: _udir, _rel = await resolve_upload_dir(sor, uid, '', generic=True) _saved = save_uploads(_udir, _uploads) _byname = {os.path.basename(src): n for (src, _n), (n, _p) in zip(_uploads, _saved)} _items = [] for _src, _name in _uploads: if is_image(_name): # 图片走原生视觉(gateway.build_image_parts),不进文本上下文—— # 否则会注入「二进制文件无法读取文本」与「图片已注入」矛盾提示。 # 仍随 save_uploads 落盘工作空间(agent 可再用 invoke_model i2t 处理) _img_uploads.append((_src, _name)) continue _preview, _total, _trunc = extract_text(_src, _name) _isbin = (not _preview and _total == 0) _relp = (_rel + _byname.get(_name, _name)) if _name in _byname else '' _items.append((_name, _relp, _preview, _total, _trunc, _isbin)) _fctx = build_file_context(_items) if _fctx: prompt = _fctx + "\n用户指令:" + prompt except Exception: pass async def agent_stream(): async for chunk in gateway.run_message("web", uid, prompt, generic=True, model_id=model_id, image_paths=_img_uploads or None): data = json.loads(chunk) t = data.get('type', '') if t == 'tool_call': yield json.dumps({"content": "**🔧 调用: " + data.get('tool', '') + "**\n```\n" + json.dumps(data.get('params', {}), ensure_ascii=False) + "\n```\n\n"}, ensure_ascii=False) + '\n' elif t == 'tool_result': yield json.dumps({"content": data.get('result', '') + "\n\n"}, ensure_ascii=False) + '\n' elif t == 'reply': yield json.dumps({"content": data.get('message', '')}, ensure_ascii=False) + '\n' elif t == 'ask_user': yield json.dumps({"content": "❓ " + data.get('message', '')}, ensure_ascii=False) + '\n' elif t == 'confirm': _tool = data.get('tool', '') _params = data.get('params', {}) or {} # 确认框摘要(2026-09-05 修复,同 agent_chat.dspy):不再只显示 command 字段 _brief = [] if isinstance(_params, dict): for _k in ('command', 'url', 'model_name', 'use_last_extract', 'overrides', 'task_ref'): if _k in _params and str(_params.get(_k) or '') != '': _v = _params.get(_k) _vs = _v if isinstance(_v, str) else json.dumps(_v, ensure_ascii=False) _brief.append("%s=%s" % (_k, _vs[:80])) for _k in ('spec', 'doc_text'): if _params.get(_k): _brief.append("%s=<%d字符>" % (_k, len(str(_params.get(_k))))) _summary = ','.join(_brief) if _brief else '(无参数)' yield json.dumps({"content": "⚠️ 需要你确认执行工具:**" + str(_tool) + "**\n参数:" + _summary[:300] + "\n请回复「确认」执行、「全部确认」本会话不再询问,或「取消」放弃。"}, ensure_ascii=False) + '\n' elif t == 'error': yield json.dumps({"error": data.get('message', '')}, ensure_ascii=False) + '\n' else: yield json.dumps({"content": chunk}, ensure_ascii=False) + '\n' # 应答输出结束后追加一行「## 完毕 ##」:Text 控件 otext+i18n,前端按当前语言翻译 # (词条见 pipeline-app/i18n/*/i18n.json;msg.txt 以 # 开头的键会被当注释,故只放 i18n.json) yield json.dumps({"widgettype": "Text", "options": {"otext": "## 完毕 ##", "text": "## 完毕 ##", "i18n": True, "halign": "left", "css": "agent-done", "color": "#94a3b8", "marginBottom": "6px"}}, ensure_ascii=False) + '\n' return await stream_response(request, agent_stream, 'text/plain; charset=utf-8') return json.dumps({"error": "Unknown action: " + str(action)}, ensure_ascii=False)