diff --git a/.gitignore b/.gitignore index 0655041..c1efe33 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ auth.json model_responses.txt *.zip + +# 存储敏感信息的记忆文件夹 +memory/ \ No newline at end of file diff --git a/agent_loop.py b/agent_loop.py index 1cd0420..05efc66 100644 --- a/agent_loop.py +++ b/agent_loop.py @@ -1,4 +1,4 @@ -import json +import json, re from dataclasses import dataclass from typing import Any, Optional @dataclass @@ -49,7 +49,10 @@ def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema, if response.thinking: yield '' + response.thinking + '\n\n' if '```' in response.content: response.content = response.content.replace('```', ' \n```') - yield response.content + '\n\n' + showcontent = response.content + if '' in showcontent: + showcontent = re.sub(r'\s*(.*?)\s*', r'\n````\n\1\n````', showcontent, flags=re.DOTALL) + yield showcontent + '\n\n' if not response.tool_calls: tool_name, args = 'no_tool', {} diff --git a/agentapp.py b/agentapp.py index 9b53139..56b0d47 100644 --- a/agentapp.py +++ b/agentapp.py @@ -18,6 +18,7 @@ from agent_loop import agent_runner_loop, StepOutcome, BaseHandler @st.cache_resource def init(): + if not os.path.exists('temp'): os.makedirs('temp') mainllm = SiderLLMSession(multiturns=6) llmclient = ToolClient(mainllm.ask, auto_save_tokens=True) return llmclient @@ -27,8 +28,20 @@ llmclient = init() from ga import GenericAgentHandler, smart_format def get_system_prompt(): + if not os.path.exists('memory'): os.makedirs('memory') + if not os.path.exists('memory/global_mem.txt'): + with open('memory/global_mem.txt', 'w', encoding='utf-8') as f: f.write('') + if not os.path.exists('memory/global_mem_insight.txt'): + with open('memory/global_mem_insight.txt', 'w', encoding='utf-8') as f: + f.write('PATHS: ../memory/global_mem.txt (Facts), ../memory/global_mem_insight.txt (Logic), ../ (Code Root).') with open('sys_prompt.txt', 'r', encoding='utf-8') as f: - return f.read() + prompt = f.read() + try: + with open('memory/global_mem_insight.txt', 'r', encoding='utf-8') as f: + insight = f.read() + prompt += f"\n\n[Global Memory Insight]\n{insight}" + except FileNotFoundError: pass + return prompt if "last_goal" not in st.session_state: st.session_state.last_goal = "" @@ -60,8 +73,7 @@ def agent_backend_stream(raw_query): #if final_goal != raw_query: yield f"[Goal Refined] {final_goal}\n" history = st.session_state.get("last_history", []) - hquery = smart_format(raw_query.replace('\n', ' '), max_str_len=100) - history.append(f"[USER]: {hquery}") + history.append(f"[USER]: {smart_format(raw_query.replace('\n', ' '))}") sys_prompt = get_system_prompt() handler = GenericAgentHandler(None, history, './temp') diff --git a/ga.py b/ga.py index 6ef69e3..63538db 100644 --- a/ga.py +++ b/ga.py @@ -1,6 +1,6 @@ import sys, os, re, json, time, pyperclip, threading from pathlib import Path -import tempfile, traceback, subprocess +import tempfile, traceback, subprocess, itertools, collections if sys.stdout is None: sys.stdout = open(os.devnull, "w") if sys.stderr is None: sys.stderr = open(os.devnull, "w") sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) @@ -193,14 +193,25 @@ def file_patch(path: str, old_content: str, new_content: str): except Exception as e: return {"status": "error", "msg": str(e)} -def file_read(path, start=1, count=100, show_linenos=True): +def file_read(path, start=1, keyword=None, count=100, show_linenos=True): + L_MAX = max(100, 1024000//count); TAG = " ... [TRUNCATED]" try: with open(path, 'r', encoding='utf-8', errors='replace') as f: - lines = f.readlines() - chunk = lines[start-1 : start-1+count] - if show_linenos: res = [f"{i+start}|{l[:200]}" for i, l in enumerate(chunk)] - else: res = [l for l in chunk] - return f"Total:{len(lines)} lines\n" + "".join(res) + stream = ( + (i, (l[:L_MAX].rstrip() + TAG if len(l) > L_MAX else l.rstrip())) + for i, l in enumerate(f, 1) + ) + stream = itertools.dropwhile(lambda x: x[0] < start, stream) + if keyword: + before = collections.deque(maxlen=count//3) + for i, l in stream: + if keyword.lower() in l.lower(): + res = list(before) + [(i, l)] + list(itertools.islice(stream, count - len(before) - 1)) + break + before.append((i, l)) + else: return f"Keyword '{keyword}' not found after line {start}." + else: res = itertools.islice(stream, count) + return "\n".join(f"{i}|{l}" if show_linenos else l for i, l in res) except Exception as e: return f"Error: {str(e)}" @@ -323,7 +334,7 @@ class GenericAgentHandler(BaseHandler): blocks = extract_robust_content(response.content) if not blocks: yield f"[Status] ❌ 失败: 未在回复中找到代码块内容\n" - return StepOutcome({"status": "error", "msg": "No code block found in response"}, next_prompt="\n") + return StepOutcome({"status": "error", "msg": "No content found, if you want a blank, you should use code_run"}, next_prompt="\n") new_content = blocks try: write_mode = 'a' if mode == "append" else 'w' @@ -339,12 +350,15 @@ class GenericAgentHandler(BaseHandler): return StepOutcome({"status": "error", "msg": str(e)}, next_prompt="\n") def do_file_read(self, args, response): + '''读取文件内容。从第start行开始读取。如有keyword则返回第一个keyword(忽略大小写)周边内容''' path = self._get_abs_path(args.get("path", "")) yield f"\n[Action] Reading file: {path}\n" start = args.get("start", 1) count = args.get("count", 100) + keyword = args.get("keyword") show_linenos = args.get("show_linenos", True) - result = file_read(path, start, count, show_linenos) + result = file_read(path, start=start, keyword=keyword, + count=count, show_linenos=show_linenos) next_prompt = self._get_anchor_prompt() return StepOutcome(result, next_prompt=next_prompt) @@ -370,9 +384,24 @@ class GenericAgentHandler(BaseHandler): def do_no_tool(self, args, response): '''这是一个特殊工具,由引擎自主调用,不要包含在TOOLS_SCHEMA里。 ''' + if not response or not getattr(response, 'content', '').strip(): + yield "[Warn] LLM returned an empty response. Retrying...\n" + next_prompt = "[System] 检测到空回复,请重新生成内容或调用工具。" + return StepOutcome({}, next_prompt=next_prompt, should_exit=False) yield "[Info] No tool called. Final response to user.\n" return StepOutcome(response, next_prompt=None, should_exit=True) + def do_distill_good_memory(self, args, response): + '''Agent觉得当前任务完成后有重要信息需要记忆时调用此工具。 + 目前只支持全局记忆,暂不处理过程记忆或特定任务经验。 + ''' + next_prompt = '''### [总结提炼经验] 既然你觉得当前任务有重要信息需要记忆,请提取最近一次任务中【事实验证成功且长期有效】的环境事实与用户偏好,更新至全局记忆。 +1. 严禁记录任何任务特定中间执行过程或临时变量经验,那是过程记忆不是全局记忆。 +2. 若无高价值新事实,那就不更新任何内容。 +3. 尽量先查看现有全局记忆形式,仿照形式且避免冗余,insight也要添加对全局记忆的短印象来提醒存在性。''' + yield "[Info] Start distilling good memory for long-term storage.\n" + return StepOutcome({"status": "success"}, next_prompt=next_prompt) + def _get_anchor_prompt(self): h_str = "\n".join(self.history_info[-20:]) prompt = f"\n### [WORKING MEMORY]\n\n{h_str}\n" diff --git a/tools_schema.json b/tools_schema.json index b540e5f..7e4a450 100644 --- a/tools_schema.json +++ b/tools_schema.json @@ -1,212 +1,68 @@ [ - { - "type": "function", - "function": { - "name": "code_run", - "description": "针对 Windows 优化的双模态代码执行器。优先使用 python 运行复杂逻辑,仅在必要系统操作时使用 powershell。注意:执行的代码必须以 ```python 或 ```powershell 代码块的形式包含在回复正文中。严禁在代码中硬编码大量数据,如有需要应通过文件读取。执行时间限制为 60s。", - "parameters": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "python", - "powershell" - ], - "description": "执行环境类型,默认为 python。", - "default": "python" - }, - "timeout": { - "type": "integer", - "description": "执行超时时间(秒),默认 60。", - "default": 60 - }, - "cwd": { - "type": "string", - "description": "工作目录,默认为当前工作目录。" - } - } - } - } - }, - { - "type": "function", - "function": { - "name": "file_read", - "description": "读取文件内容。建议在修改文件前先读取,以确保获取最新的上下文和行号。支持分页读取,默认每次读取 100 行。", - "parameters": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "文件相对或绝对路径。" - }, - "start": { - "type": "integer", - "description": "起始行号(从 1 开始)。", - "default": 1 - }, - "count": { - "type": "integer", - "description": "读取的行数。", - "default": 100 - }, - "show_linenos": { - "type": "boolean", - "description": "是否显示行号,建议开启以辅助 file_patch 定位。", - "default": true - } - }, - "required": [ - "path" - ] - } - } - }, - { - "type": "function", - "function": { - "name": "file_patch", - "description": "精细化局部文件修改。在文件中寻找唯一的 old_content 块并替换为 new_content。要求 old_content 必须在文件中唯一存在,且空格、缩进、换行必须与原文件完全一致。如果匹配失败,请使用 file_read 重新确认文件内容。", - "parameters": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "文件路径。" - }, - "old_content": { - "type": "string", - "description": "文件中需要被替换的原始文本块(需确保唯一性)。" - }, - "new_content": { - "type": "string", - "description": "替换后的新文本内容。" - } - }, - "required": [ - "path", - "old_content", - "new_content" - ] - } - } - }, - { - "type": "function", - "function": { - "name": "file_write", - "description": "用于文件的新建、全量覆盖或追加写入。对于精细的代码修改,应优先使用 file_patch。注意:要写入的内容必须放在回复正文的 标签或代码块中。", - "parameters": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "文件路径。" - }, - "mode": { - "type": "string", - "enum": [ - "overwrite", - "append" - ], - "description": "写入模式:overwrite(覆盖,默认)或 append(追加)。", - "default": "overwrite" - } - }, - "required": [ - "path" - ] - } - } - }, - { - "type": "function", - "function": { - "name": "web_scan", - "description": "获取当前网页的清洗后内容,并列出所有已打开的标签页。支持切换标签页。在长页面中,可以使用 focus_item 进行语义过滤以提取关键信息。", - "parameters": { - "type": "object", - "properties": { - "focus_item": { - "type": "string", - "description": "语义过滤指令,用于在长列表中优先保留与该关键词相关的项。" - }, - "switch_tab_id": { - "type": "string", - "description": "可选的标签页 ID。如果提供,系统将在扫描前切换到该标签页。" - } - } - } - } - }, - { - "type": "function", - "function": { - "name": "web_execute_js", - "description": "万能网页操控工具。通过执行 JavaScript 脚本实现对浏览器的完全控制(如点击、滚动、提取特定数据)。这是 Web 场景下的首选工具。执行结果可选择保存到本地文件进行后续分析。", - "parameters": { - "type": "object", - "properties": { - "script": { - "type": "string", - "description": "要执行的 JavaScript 代码。" - }, - "save_to_file": { - "type": "string", - "description": "可选。将 JS 执行结果(js_return)保存到的文件路径。注意:该功能不支持 await 等异步结果。" - } - }, - "required": [ - "script" - ] - } - } - }, - { - "type": "function", - "function": { - "name": "update_plan", - "description": "更新任务的宏观计划和当前战略重心。仅在初始拆解多步任务或发生重大方案调整时使用。禁止用于记录细微调试步骤或纠错。", - "parameters": { - "type": "object", - "properties": { - "plan": { - "type": "string", - "description": "完整的宏观任务路线图。" - }, - "focus": { - "type": "string", - "description": "当前阶段的工作重点。" - } - } - } - } - }, - { - "type": "function", - "function": { - "name": "ask_user", - "description": "当需要用户决策、提供额外信息或遇到无法自动解决的阻碍时,调用此工具中断任务并提问。", - "parameters": { - "type": "object", - "properties": { - "question": { - "type": "string", - "description": "向用户提出的明确问题。" - }, - "candidates": { - "type": "array", - "items": { - "type": "string" - }, - "description": "提供给用户的可选快捷选项列表。" - } - }, - "required": [ - "question" - ] - } - } + {"type": "function", "function": { + "name": "code_run", + "description": "针对 Windows 优化的双模态代码执行器。优先使用 python 运行复杂逻辑,仅在必要系统操作时使用 powershell。注意:执行的代码必须以 ```python 或 ```powershell 代码块的形式包含在回复正文中。严禁在代码中硬编码大量数据,如有需要应通过文件读取。执行时间限制为 60s。", + "parameters": {"type": "object", "properties": { + "type": {"type": "string", "enum": ["python", "powershell"], "description": "执行环境类型,默认为 python。", "default": "python"}, + "timeout": {"type": "integer", "description": "执行超时时间(秒),默认 60。", "default": 60}, + "cwd": {"type": "string", "description": "工作目录,默认为当前工作目录。"}}} + }}, + {"type": "function", "function": { + "name": "file_read", + "description": "读取文件内容。建议在修改文件前先读取,以确保获取最新的上下文和行号。支持分页读取或关键字搜索。", + "parameters": {"type": "object", "properties": { + "path": {"type": "string", "description": "文件相对或绝对路径。"}, + "start": {"type": "integer", "description": "起始行号(从 1 开始)。", "default": 1}, + "count": {"type": "integer", "description": "读取的行数。", "default": 100}, + "keyword": {"type": "string", "description": "可选搜索关键字。如果提供,将返回第一个匹配项(忽略大小写)及其周边的内容。"}, + "show_linenos": {"type": "boolean", "description": "是否显示行号,建议开启以辅助 file_patch 定位。", "default": true}}, "required": ["path"]} + }}, + {"type": "function", "function": { + "name": "file_patch", + "description": "精细化局部文件修改。在文件中寻找唯一的 old_content 块并替换为 new_content。要求 old_content 必须在文件中唯一存在,且空格、缩进、换行必须与原文件完全一致。如果匹配失败,请使用 file_read 重新确认文件内容。", + "parameters": {"type": "object", "properties": { + "path": {"type": "string", "description": "文件路径。"}, + "old_content": {"type": "string", "description": "文件中需要被替换的原始文本块(需确保唯一性)。"}, + "new_content": {"type": "string", "description": "替换后的新文本内容。"}}, "required": ["path", "old_content", "new_content"]} + }}, + {"type": "function", "function": { + "name": "file_write", + "description": "用于文件的新建、全量覆盖或追加写入。对于精细的代码修改,应优先使用 file_patch。注意:要写入的内容必须放在回复正文的 标签或代码块中。", + "parameters": {"type": "object", "properties": { + "path": {"type": "string", "description": "文件路径。"}, + "mode": {"type": "string", "enum": ["overwrite", "append"], "description": "写入模式:overwrite(覆盖,默认)或 append(追加)。", "default": "overwrite"}}, "required": ["path"]} + }}, + {"type": "function", "function": { + "name": "web_scan", + "description": "获取当前网页的清洗后内容,并列出所有已打开的标签页。支持切换标签页。在长页面中,可以使用 focus_item 进行语义过滤以提取关键信息。", + "parameters": {"type": "object", "properties": { + "focus_item": {"type": "string", "description": "语义过滤指令,用于在长列表中优先保留与该关键词相关的项。"}, + "switch_tab_id": {"type": "string", "description": "可选的标签页 ID。如果提供,系统将在扫描前切换到该标签页。"}}} + }}, + {"type": "function", "function": { + "name": "web_execute_js", + "description": "万能网页操控工具。通过执行 JavaScript 脚本实现对浏览器的完全控制(如点击、滚动、提取特定数据)。这是 Web 场景下的首选工具。执行结果可选择保存到本地文件进行后续分析。", + "parameters": {"type": "object", "properties": { + "script": {"type": "string", "description": "要执行的 JavaScript 代码。"}, + "save_to_file": {"type": "string", "description": "可选。将 JS 执行结果(js_return)保存到的文件路径。注意:该功能不支持 await 等异步结果。"}}, "required": ["script"]} + }}, + {"type": "function", "function": { + "name": "update_plan", + "description": "更新任务的宏观计划和当前战略重心。仅在初始拆解多步任务或发生重大方案调整时使用。禁止用于记录细微调试步骤或纠错。", + "parameters": {"type": "object", "properties": { + "plan": {"type": "string", "description": "完整的宏观任务路线图。"}, + "focus": {"type": "string", "description": "当前阶段的工作重点。"}}} + }}, + {"type": "function", "function": { + "name": "ask_user", + "description": "当需要用户决策、提供额外信息或遇到无法自动解决的阻碍时,调用此工具中断任务并提问。", + "parameters": {"type": "object", "properties": { + "question": {"type": "string", "description": "向用户提出的明确问题。"}, + "candidates": {"type": "array", "items": {"type": "string"}, "description": "提供给用户的可选快捷选项列表。"}}, "required": ["question"]} + }}, + {"type": "function", "function": { + "name": "distill_good_memory", + "description": "当模型认为当前任务执行完美,且有具有长期价值的环境事实或用户偏好需要提炼并存入全局记忆时,调用此工具。注意:此工具无参数,调用即代表触发记忆提炼流程。", + "parameters": {"type": "object", "properties": {}}} } ] \ No newline at end of file