Skip to content

Commit b5a30f1

Browse files
committed
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
1 parent 26257a1 commit b5a30f1

5 files changed

Lines changed: 73 additions & 75 deletions

File tree

agentmain.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def run(self):
162162
self.llmclient.backend.stream = False
163163
self.llmclient.backend.read_timeout = max(self.llmclient.backend.read_timeout, 1200)
164164
gen = agent_runner_loop(self.llmclient, sys_prompt, raw_query, handler, TOOLS_SCHEMA,
165-
max_turns=80, verbose=self.verbose, yield_info=True)
165+
max_turns=180, verbose=self.verbose, yield_info=True)
166166
try:
167167
full_resp = ""; last_pos = 0; curr_turn = 0; turn_resps = []
168168
for chunk in gen:

assets/tool_usable_history.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

frontends/stapp.py

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import streamlit as st
1515
import time, json, re, threading, queue
16+
from datetime import timedelta
1617
from agentmain import GeneraticAgent
1718
import chatapp_common # activate /continue command (monkey patches GeneraticAgent)
1819
from continue_cmd import handle_frontend_command, reset_conversation, list_sessions, extract_ui_messages
@@ -34,13 +35,27 @@
3435
I18N = {
3536
'zh': {
3637
'force_stop': '强行停止任务',
37-
'reinject_tools': '重新注入工具',
3838
'desktop_pet': '🐱 桌面宠物',
39+
'suggest_btn': '🎯 给我找点事做',
40+
'suggest_prompt': '按照自主行动的规划部分,充分分析我的情况,给我生成一批TODO,务必让我感兴趣',
41+
'auto_start': '开始空闲自主行动',
42+
'auto_pause': '⏸️ 禁止自主行动',
43+
'auto_enable': '▶️ 允许自主行动',
44+
'auto_on_cap': '🟢 自主行动运行中,会在你离开它30分钟后自动进行',
45+
'auto_off_cap': '🔴 自主行动已停止',
46+
'auto_prompt': '[AUTO]🤖 用户已经离开超过30分钟,作为自主智能体,请阅读自动化sop,执行自动任务。',
3947
},
4048
'en': {
4149
'force_stop': 'Force Stop',
42-
'reinject_tools': 'Reinject Tools',
4350
'desktop_pet': '🐱 Desktop Pet',
51+
'suggest_btn': '🎯 Suggest tasks',
52+
'suggest_prompt': 'Following the planning section of autonomous sop, analyze my situation thoroughly and generate a batch of TODOs that will interest me.',
53+
'auto_start': 'Start idle auto-action',
54+
'auto_pause': '⏸️ Pause auto-action',
55+
'auto_enable': '▶️ Enable auto-action',
56+
'auto_on_cap': '🟢 Auto-action enabled, triggers after 30min idle',
57+
'auto_off_cap': '🔴 Auto-action disabled',
58+
'auto_prompt': '[AUTO]🤖 User has been idle for over 30 minutes. As an autonomous agent, read the automation SOP and execute automatic tasks.',
4459
},
4560
}
4661
def T(key): return I18N.get(LANG, I18N['zh']).get(key, key)
@@ -72,14 +87,6 @@ def render_sidebar():
7287
agent.next_llm(selected_idx); st.rerun(scope="fragment")
7388
if st.button(T('force_stop')):
7489
agent.abort(); st.toast("Stop signal sended"); st.rerun()
75-
if st.button(T('reinject_tools')):
76-
agent.llmclient.last_tools = ''
77-
try:
78-
hist_path = os.path.join(script_dir, '..', 'assets', 'tool_usable_history.json')
79-
with open(hist_path, 'r', encoding='utf-8') as f: tool_hist = json.load(f)
80-
agent.llmclient.backend.history.extend(tool_hist)
81-
st.toast(f"Tools injected")
82-
except Exception as e: st.toast(f"Injected tools failed: {e}")
8390
if st.button(T('desktop_pet')):
8491
kwargs = {'creationflags': 0x08} if sys.platform == 'win32' else {}
8592
pet_script = os.path.join(script_dir, 'desktop_pet_v2.pyw')
@@ -103,25 +110,37 @@ def _pet_hook(ctx):
103110
agent._turn_end_hooks['pet'] = _pet_hook
104111
st.toast("Desktop pet started")
105112

106-
if LANG == 'zh':
107-
if st.button('🎯 给我找点事做'):
108-
st.session_state['_inject_prompt'] = '按照自主行动的规划部分,充分分析我的情况,给我生成一批TODO,务必让我感兴趣'
109-
st.rerun(scope="app")
110-
st.divider()
111-
if st.button("开始空闲自主行动"):
112-
st.session_state.last_reply_time = int(time.time()) - 1800
113+
if st.button(T('suggest_btn')):
114+
st.session_state['_inject_prompt'] = T('suggest_prompt')
115+
st.rerun(scope="app")
116+
st.divider()
117+
if st.session_state.get('loop_enabled'):
118+
if st.button("⏹️ Stop Loop"):
119+
st.session_state.loop_enabled = False
120+
st.toast("⏹️ Loop stopped"); st.rerun(scope="app")
121+
st.caption("🔁 Looping")
122+
else:
123+
loop_prompt = st.text_input("Loop prompt", value="继续" if LANG=='zh' else 'next', key="loop_prompt_input")
124+
if st.button("🔁 Loop!"):
125+
st.session_state.loop_enabled = True
126+
st.session_state.loop_prompt = loop_prompt
127+
st.session_state['_inject_prompt'] = loop_prompt
128+
st.toast("🔁 Looping"); st.rerun(scope="app")
129+
st.divider()
130+
if st.button(T('auto_start')):
131+
st.session_state.last_reply_time = int(time.time()) - 1800
132+
st.session_state.autonomous_enabled = True
133+
st.rerun(scope="app")
134+
if st.session_state.autonomous_enabled:
135+
if st.button(T('auto_pause')):
136+
st.session_state.autonomous_enabled = False
137+
st.toast(T('auto_pause')); st.rerun(scope="app")
138+
st.caption(T('auto_on_cap'))
139+
else:
140+
if st.button(T('auto_enable'), type="primary"):
113141
st.session_state.autonomous_enabled = True
114-
st.toast("已将上次回复时间设为1800秒前,自主行动已激活"); st.rerun(scope="app")
115-
if st.session_state.autonomous_enabled:
116-
if st.button("⏸️ 禁止自主行动"):
117-
st.session_state.autonomous_enabled = False
118-
st.toast("⏸️ 已禁止自主行动"); st.rerun(scope="app")
119-
st.caption("🟢 自主行动运行中,会在你离开它30分钟后自动进行")
120-
else:
121-
if st.button("▶️ 允许自主行动", type="primary"):
122-
st.session_state.autonomous_enabled = True
123-
st.toast("✅ 已允许自主行动"); st.rerun(scope="app")
124-
st.caption("🔴 自主行动已停止")
142+
st.toast("✅"); st.rerun(scope="app")
143+
st.caption(T('auto_off_cap'))
125144
with st.sidebar: render_sidebar()
126145

127146
def fold_turns(text):
@@ -226,6 +245,10 @@ def render_main_stream(prompt=None):
226245
if response:
227246
st.session_state.messages.append({"role": "assistant", "content": response})
228247
st.session_state.last_reply_time = int(time.time())
248+
# ── 循环回调:回答完成后自动注入下一条 ──
249+
if st.session_state.get('loop_enabled'):
250+
st.session_state['_inject_prompt'] = st.session_state.get('loop_prompt', '继续')
251+
st.rerun()
229252

230253
if "messages" not in st.session_state: st.session_state.messages = []
231254
for msg in st.session_state.messages:
@@ -334,5 +357,15 @@ def _reset_and_rerun():
334357
# No new prompt but a task is mid-flight (typically a /btw rerun) — resume drain.
335358
render_main_stream()
336359

337-
if st.session_state.autonomous_enabled:
338-
st.markdown(f"""<div id="last-reply-time" style="display:none">{st.session_state.get('last_reply_time', int(time.time()))}</div>""", unsafe_allow_html=True)
360+
# ── 空闲自主行动:fragment 定时检测,替代 launch.pyw 的 idle_monitor ──
361+
@st.fragment(run_every=timedelta(minutes=5))
362+
def _idle_checker():
363+
if not st.session_state.get('autonomous_enabled'): return
364+
if st.session_state.get('display_queue') is not None: return # 正在运行中
365+
if st.session_state.get('loop_enabled'): return # 循环模式自己管
366+
last = st.session_state.get('last_reply_time', int(time.time()))
367+
if time.time() - last > 1800:
368+
st.session_state['_inject_prompt'] = T('auto_prompt')
369+
st.session_state['last_reply_time'] = int(time.time()) # 防重入
370+
st.rerun(scope="app")
371+
_idle_checker()

ga.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -561,15 +561,15 @@ def turn_end_callback(self, response, tool_calls, tool_results, turn, next_promp
561561
self.history_info.append(f'[Agent] {summary}')
562562
_plan = self._in_plan_mode()
563563

564-
if turn % 75 == 0 and (not _plan):
564+
if turn % 175 == 0 and (not _plan):
565565
next_prompt += f"\n\n[DANGER] 已连续执行第 {turn} 轮。必须总结情况进行ask_user,不允许继续重试。"
566566
elif turn % 7 == 0:
567567
next_prompt += f"\n\n[SYSTEM] 已连续执行第 {turn} 轮。建议调用update_working_checkpoint保存关键上下文。禁止无效重试;若无有效进展,必须切换策略:1. 探测物理边界 2. **重读相关SOP**"
568568
elif turn % 10 == 0: next_prompt += get_global_memory()
569569

570570
if _plan and turn >= 10 and turn % 5 == 0:
571571
next_prompt = f"[Plan Hint] 正在计划模式。必须 file_read({_plan}) 确认当前步骤,回复开头引用:📌 当前步骤:...\n\n" + next_prompt
572-
if _plan and turn >= 120: next_prompt += f"\n\n[DANGER] Plan模式已运行 {turn} 轮,已达上限。必须 ask_user 汇报进度并确认是否继续。"
572+
if _plan and turn >= 190: next_prompt += f"\n\n[DANGER] Plan模式已运行 {turn} 轮,已达上限。必须 ask_user 汇报进度并确认是否继续。"
573573

574574
injkeyinfo = consume_file(self.parent.task_dir, '_keyinfo')
575575
injprompt = consume_file(self.parent.task_dir, '_intervene')

launch.pyw

Lines changed: 7 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -22,31 +22,6 @@ def start_streamlit(port):
2222
proc = subprocess.Popen(cmd)
2323
atexit.register(proc.kill)
2424

25-
def inject(text):
26-
window.evaluate_js(f"""
27-
const textarea = document.querySelector('textarea[data-testid="stChatInputTextArea"]');
28-
if (textarea) {{
29-
// 1. 用原生 setter 设置值(绕过 React)
30-
const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
31-
nativeTextAreaValueSetter.call(textarea, {repr(text)});
32-
// 2. 触发 React 的 input 事件
33-
textarea.dispatchEvent(new Event('input', {{ bubbles: true }}));
34-
// 3. 触发 change 事件(有些组件需要)
35-
textarea.dispatchEvent(new Event('change', {{ bubbles: true }}));
36-
// 4. 延迟提交
37-
setTimeout(() => {{
38-
const btn = document.querySelector('[data-testid="stChatInputSubmitButton"]');
39-
if (btn) {{btn.click();console.log('Submitted:', {repr(text)});}}
40-
}}, 200);
41-
}}""")
42-
43-
def get_last_reply_time():
44-
last = window.evaluate_js("""
45-
const el = document.getElementById('last-reply-time');
46-
el ? parseInt(el.textContent) : 0;
47-
""") or 0
48-
return last or int(time.time())
49-
5025
PASTE_HOOK_JS = """if (!window._pasteHooked) { window._pasteHooked = true;
5126
document.addEventListener('paste', e => {
5227
const items = e.clipboardData?.items; if (!items) return;
@@ -65,21 +40,13 @@ PASTE_HOOK_JS = """if (!window._pasteHooked) { window._pasteHooked = true;
6540
}, true);
6641
}"""
6742

68-
def idle_monitor():
69-
last_trigger_time = 0
43+
def paste_hook_injector():
44+
"""注入剪贴板粘贴图片钩子,等窗口加载后执行一次,之后每60秒补注入防页面刷新丢失"""
45+
time.sleep(5) # 等 webview 加载
7046
while True:
71-
time.sleep(5)
72-
try:
73-
window.evaluate_js(PASTE_HOOK_JS)
74-
now = time.time()
75-
if now - last_trigger_time < 120: continue
76-
last_reply = get_last_reply_time()
77-
if now - last_reply > 1800:
78-
print('[Idle Monitor] Detected idle state, injecting task...')
79-
inject("[AUTO]🤖 用户已经离开超过30分钟,作为自主智能体,请阅读自动化sop,执行自动任务。")
80-
last_trigger_time = now
81-
except Exception as e:
82-
print(f'[Idle Monitor] Error: {e}')
47+
try: window.evaluate_js(PASTE_HOOK_JS)
48+
except: pass
49+
time.sleep(60)
8350

8451
if __name__ == '__main__':
8552
import argparse
@@ -140,8 +107,7 @@ if __name__ == '__main__':
140107
print('[Launch] Task Scheduler started (duplicate prevented by scheduler port lock)')
141108
else: print('[Launch] Task Scheduler not enabled (--sched)')
142109

143-
monitor_thread = threading.Thread(target=idle_monitor, daemon=True)
144-
monitor_thread.start()
110+
threading.Thread(target=paste_hook_injector, daemon=True).start()
145111
if os.name == 'nt':
146112
screen_width = get_screen_width()
147113
x_pos = screen_width - WINDOW_WIDTH - RIGHT_PADDING

0 commit comments

Comments
 (0)