From df025ab34ff54ff5bcd668af3353c7fa44be47e0 Mon Sep 17 00:00:00 2001 From: Liang Jiaqing Date: Fri, 19 Jun 2026 19:00:44 +0800 Subject: [PATCH] feat: loop mode + idle auto-action in stapp, cleanup launch.pyw - stapp.py: add loop mode (auto-inject prompt after each reply) - stapp.py: replace DOM-based idle monitor with st.fragment timer - stapp.py: i18n for all new UI text and LLM prompts - launch.pyw: remove inject/get_last_reply_time/idle_monitor, keep only paste hook - agentmain/ga: raise max_turns 80->180, DANGER thresholds accordingly - remove obsolete tool_usable_history.json --- agentmain.py | 2 +- assets/tool_usable_history.json | 1 - frontends/stapp.py | 93 ++++++++++++++++++++++----------- ga.py | 4 +- launch.pyw | 48 +++-------------- 5 files changed, 73 insertions(+), 75 deletions(-) delete mode 100644 assets/tool_usable_history.json diff --git a/agentmain.py b/agentmain.py index 68aacdd3..e4a7807f 100644 --- a/agentmain.py +++ b/agentmain.py @@ -162,7 +162,7 @@ class GenericAgent: self.llmclient.backend.stream = False self.llmclient.backend.read_timeout = max(self.llmclient.backend.read_timeout, 1200) gen = agent_runner_loop(self.llmclient, sys_prompt, raw_query, handler, TOOLS_SCHEMA, - max_turns=80, verbose=self.verbose, yield_info=True) + max_turns=180, verbose=self.verbose, yield_info=True) try: full_resp = ""; last_pos = 0; curr_turn = 0; turn_resps = [] for chunk in gen: diff --git a/assets/tool_usable_history.json b/assets/tool_usable_history.json deleted file mode 100644 index 1539e942..00000000 --- a/assets/tool_usable_history.json +++ /dev/null @@ -1 +0,0 @@ -[{"role":"user","content":[{"type":"text","text":"code_run个helloworld,根据结果简单评价你的工具配置"}]},{"role":"assistant","content":[{"type":"text","text":"\n\n用户要求code_run helloworld并评价工具配置\n\n```python\nprint(\"Hello, World!\")\n```"},{"type":"tool_use","id":"tooluse_GlmTNw190WGgK4sBBOiDHK","name":"code_run","input":{"script":"print(\"Hello, World!\")"}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"tooluse_GlmTNw190WGgK4sBBOiDHK","content":"{\"status\": \"success\", \"stdout\": \"Hello, World!\\r\\n\", \"exit_code\": 0}"},{"type":"text","text":"\n### [WORKING MEMORY]\n\n\n[USER]: code_run个helloworld,根据结果简单评价你的工具配置\n\nCurrent turn: 1\n"}]},{"role":"assistant","content":[{"type":"text","text":"\n\ncode_run helloworld成功,评价工具配置\n\n工具配置没问题。`code_run` 返回了结构化的 JSON(`status`、`stdout`、`exit_code`),执行链路通畅,cwd 默认落在 temp 目录。\n\n刚才还验证了 `inline_eval` 能正确访问 agent 内部状态并导出 history,`file_patch` 也能精准改文件。整套工具链从代码执行、文件读写、到 agent 内省都是通的。"}]}] \ No newline at end of file diff --git a/frontends/stapp.py b/frontends/stapp.py index b15f1ed6..d5480537 100644 --- a/frontends/stapp.py +++ b/frontends/stapp.py @@ -13,6 +13,7 @@ sys.path.append(os.path.abspath(script_dir)) import streamlit as st import time, json, re, threading, queue +from datetime import timedelta from agentmain import GeneraticAgent import chatapp_common # activate /continue command (monkey patches GeneraticAgent) from continue_cmd import handle_frontend_command, reset_conversation, list_sessions, extract_ui_messages @@ -34,13 +35,27 @@ if LANG not in ('zh', 'en'): LANG = 'zh' I18N = { 'zh': { 'force_stop': '强行停止任务', - 'reinject_tools': '重新注入工具', 'desktop_pet': '🐱 桌面宠物', + 'suggest_btn': '🎯 给我找点事做', + 'suggest_prompt': '按照自主行动的规划部分,充分分析我的情况,给我生成一批TODO,务必让我感兴趣', + 'auto_start': '开始空闲自主行动', + 'auto_pause': '⏸️ 禁止自主行动', + 'auto_enable': '▶️ 允许自主行动', + 'auto_on_cap': '🟢 自主行动运行中,会在你离开它30分钟后自动进行', + 'auto_off_cap': '🔴 自主行动已停止', + 'auto_prompt': '[AUTO]🤖 用户已经离开超过30分钟,作为自主智能体,请阅读自动化sop,执行自动任务。', }, 'en': { 'force_stop': 'Force Stop', - 'reinject_tools': 'Reinject Tools', 'desktop_pet': '🐱 Desktop Pet', + 'suggest_btn': '🎯 Suggest tasks', + 'suggest_prompt': 'Following the planning section of autonomous sop, analyze my situation thoroughly and generate a batch of TODOs that will interest me.', + 'auto_start': 'Start idle auto-action', + 'auto_pause': '⏸️ Pause auto-action', + 'auto_enable': '▶️ Enable auto-action', + 'auto_on_cap': '🟢 Auto-action enabled, triggers after 30min idle', + 'auto_off_cap': '🔴 Auto-action disabled', + 'auto_prompt': '[AUTO]🤖 User has been idle for over 30 minutes. As an autonomous agent, read the automation SOP and execute automatic tasks.', }, } def T(key): return I18N.get(LANG, I18N['zh']).get(key, key) @@ -72,14 +87,6 @@ def render_sidebar(): agent.next_llm(selected_idx); st.rerun(scope="fragment") if st.button(T('force_stop')): agent.abort(); st.toast("Stop signal sended"); st.rerun() - if st.button(T('reinject_tools')): - agent.llmclient.last_tools = '' - try: - hist_path = os.path.join(script_dir, '..', 'assets', 'tool_usable_history.json') - with open(hist_path, 'r', encoding='utf-8') as f: tool_hist = json.load(f) - agent.llmclient.backend.history.extend(tool_hist) - st.toast(f"Tools injected") - except Exception as e: st.toast(f"Injected tools failed: {e}") if st.button(T('desktop_pet')): kwargs = {'creationflags': 0x08} if sys.platform == 'win32' else {} pet_script = os.path.join(script_dir, 'desktop_pet_v2.pyw') @@ -103,25 +110,37 @@ def render_sidebar(): agent._turn_end_hooks['pet'] = _pet_hook st.toast("Desktop pet started") - if LANG == 'zh': - if st.button('🎯 给我找点事做'): - st.session_state['_inject_prompt'] = '按照自主行动的规划部分,充分分析我的情况,给我生成一批TODO,务必让我感兴趣' - st.rerun(scope="app") - st.divider() - if st.button("开始空闲自主行动"): - st.session_state.last_reply_time = int(time.time()) - 1800 + if st.button(T('suggest_btn')): + st.session_state['_inject_prompt'] = T('suggest_prompt') + st.rerun(scope="app") + st.divider() + if st.session_state.get('loop_enabled'): + if st.button("⏹️ Stop Loop"): + st.session_state.loop_enabled = False + st.toast("⏹️ Loop stopped"); st.rerun(scope="app") + st.caption("🔁 Looping") + else: + loop_prompt = st.text_input("Loop prompt", value="继续" if LANG=='zh' else 'next', key="loop_prompt_input") + if st.button("🔁 Loop!"): + st.session_state.loop_enabled = True + st.session_state.loop_prompt = loop_prompt + st.session_state['_inject_prompt'] = loop_prompt + st.toast("🔁 Looping"); st.rerun(scope="app") + st.divider() + if st.button(T('auto_start')): + st.session_state.last_reply_time = int(time.time()) - 1800 + st.session_state.autonomous_enabled = True + st.rerun(scope="app") + if st.session_state.autonomous_enabled: + if st.button(T('auto_pause')): + st.session_state.autonomous_enabled = False + st.toast(T('auto_pause')); st.rerun(scope="app") + st.caption(T('auto_on_cap')) + else: + if st.button(T('auto_enable'), type="primary"): st.session_state.autonomous_enabled = True - st.toast("已将上次回复时间设为1800秒前,自主行动已激活"); st.rerun(scope="app") - if st.session_state.autonomous_enabled: - if st.button("⏸️ 禁止自主行动"): - st.session_state.autonomous_enabled = False - st.toast("⏸️ 已禁止自主行动"); st.rerun(scope="app") - st.caption("🟢 自主行动运行中,会在你离开它30分钟后自动进行") - else: - if st.button("▶️ 允许自主行动", type="primary"): - st.session_state.autonomous_enabled = True - st.toast("✅ 已允许自主行动"); st.rerun(scope="app") - st.caption("🔴 自主行动已停止") + st.toast("✅"); st.rerun(scope="app") + st.caption(T('auto_off_cap')) with st.sidebar: render_sidebar() def fold_turns(text): @@ -226,6 +245,10 @@ def render_main_stream(prompt=None): if response: st.session_state.messages.append({"role": "assistant", "content": response}) st.session_state.last_reply_time = int(time.time()) + # ── 循环回调:回答完成后自动注入下一条 ── + if st.session_state.get('loop_enabled'): + st.session_state['_inject_prompt'] = st.session_state.get('loop_prompt', '继续') + st.rerun() if "messages" not in st.session_state: st.session_state.messages = [] for msg in st.session_state.messages: @@ -334,5 +357,15 @@ elif st.session_state.get('display_queue') is not None: # No new prompt but a task is mid-flight (typically a /btw rerun) — resume drain. render_main_stream() -if st.session_state.autonomous_enabled: - st.markdown(f"""""", unsafe_allow_html=True) +# ── 空闲自主行动:fragment 定时检测,替代 launch.pyw 的 idle_monitor ── +@st.fragment(run_every=timedelta(minutes=5)) +def _idle_checker(): + if not st.session_state.get('autonomous_enabled'): return + if st.session_state.get('display_queue') is not None: return # 正在运行中 + if st.session_state.get('loop_enabled'): return # 循环模式自己管 + last = st.session_state.get('last_reply_time', int(time.time())) + if time.time() - last > 1800: + st.session_state['_inject_prompt'] = T('auto_prompt') + st.session_state['last_reply_time'] = int(time.time()) # 防重入 + st.rerun(scope="app") +_idle_checker() diff --git a/ga.py b/ga.py index 5fec4fe2..a9076204 100644 --- a/ga.py +++ b/ga.py @@ -561,7 +561,7 @@ class GenericAgentHandler(BaseHandler): self.history_info.append(f'[Agent] {summary}') _plan = self._in_plan_mode() - if turn % 75 == 0 and (not _plan): + if turn % 175 == 0 and (not _plan): next_prompt += f"\n\n[DANGER] 已连续执行第 {turn} 轮。必须总结情况进行ask_user,不允许继续重试。" elif turn % 7 == 0: next_prompt += f"\n\n[SYSTEM] 已连续执行第 {turn} 轮。建议调用update_working_checkpoint保存关键上下文。禁止无效重试;若无有效进展,必须切换策略:1. 探测物理边界 2. **重读相关SOP**" @@ -569,7 +569,7 @@ class GenericAgentHandler(BaseHandler): if _plan and turn >= 10 and turn % 5 == 0: next_prompt = f"[Plan Hint] 正在计划模式。必须 file_read({_plan}) 确认当前步骤,回复开头引用:📌 当前步骤:...\n\n" + next_prompt - if _plan and turn >= 120: next_prompt += f"\n\n[DANGER] Plan模式已运行 {turn} 轮,已达上限。必须 ask_user 汇报进度并确认是否继续。" + if _plan and turn >= 190: next_prompt += f"\n\n[DANGER] Plan模式已运行 {turn} 轮,已达上限。必须 ask_user 汇报进度并确认是否继续。" injkeyinfo = consume_file(self.parent.task_dir, '_keyinfo') injprompt = consume_file(self.parent.task_dir, '_intervene') diff --git a/launch.pyw b/launch.pyw index 7e5c466b..ee79c978 100644 --- a/launch.pyw +++ b/launch.pyw @@ -22,31 +22,6 @@ def start_streamlit(port): proc = subprocess.Popen(cmd) atexit.register(proc.kill) -def inject(text): - window.evaluate_js(f""" - const textarea = document.querySelector('textarea[data-testid="stChatInputTextArea"]'); - if (textarea) {{ - // 1. 用原生 setter 设置值(绕过 React) - const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set; - nativeTextAreaValueSetter.call(textarea, {repr(text)}); - // 2. 触发 React 的 input 事件 - textarea.dispatchEvent(new Event('input', {{ bubbles: true }})); - // 3. 触发 change 事件(有些组件需要) - textarea.dispatchEvent(new Event('change', {{ bubbles: true }})); - // 4. 延迟提交 - setTimeout(() => {{ - const btn = document.querySelector('[data-testid="stChatInputSubmitButton"]'); - if (btn) {{btn.click();console.log('Submitted:', {repr(text)});}} - }}, 200); - }}""") - -def get_last_reply_time(): - last = window.evaluate_js(""" - const el = document.getElementById('last-reply-time'); - el ? parseInt(el.textContent) : 0; - """) or 0 - return last or int(time.time()) - PASTE_HOOK_JS = """if (!window._pasteHooked) { window._pasteHooked = true; document.addEventListener('paste', e => { const items = e.clipboardData?.items; if (!items) return; @@ -65,21 +40,13 @@ PASTE_HOOK_JS = """if (!window._pasteHooked) { window._pasteHooked = true; }, true); }""" -def idle_monitor(): - last_trigger_time = 0 +def paste_hook_injector(): + """注入剪贴板粘贴图片钩子,等窗口加载后执行一次,之后每60秒补注入防页面刷新丢失""" + time.sleep(5) # 等 webview 加载 while True: - time.sleep(5) - try: - window.evaluate_js(PASTE_HOOK_JS) - now = time.time() - if now - last_trigger_time < 120: continue - last_reply = get_last_reply_time() - if now - last_reply > 1800: - print('[Idle Monitor] Detected idle state, injecting task...') - inject("[AUTO]🤖 用户已经离开超过30分钟,作为自主智能体,请阅读自动化sop,执行自动任务。") - last_trigger_time = now - except Exception as e: - print(f'[Idle Monitor] Error: {e}') + try: window.evaluate_js(PASTE_HOOK_JS) + except: pass + time.sleep(60) if __name__ == '__main__': import argparse @@ -140,8 +107,7 @@ if __name__ == '__main__': print('[Launch] Task Scheduler started (duplicate prevented by scheduler port lock)') else: print('[Launch] Task Scheduler not enabled (--sched)') - monitor_thread = threading.Thread(target=idle_monitor, daemon=True) - monitor_thread.start() + threading.Thread(target=paste_hook_injector, daemon=True).start() if os.name == 'nt': screen_width = get_screen_width() x_pos = screen_width - WINDOW_WIDTH - RIGHT_PADDING