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
This commit is contained in:
1 parent
0ca5811e24
commit
df025ab34f
5 files changed
+66
-68
No files matched your search
+1
-1
@@ -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:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
[{"role":"user","content":[{"type":"text","text":"code_run个helloworld,根据结果简单评价你的工具配置"}]},{"role":"assistant","content":[{"type":"text","text":"\n\n<summary>用户要求code_run helloworld并评价工具配置</summary>\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<history>\n\n[USER]: code_run个helloworld,根据结果简单评价你的工具配置\n</history>\nCurrent turn: 1\n"}]},{"role":"assistant","content":[{"type":"text","text":"\n\n<summary>code_run helloworld成功,评价工具配置</summary>\n\n工具配置没问题。`code_run` 返回了结构化的 JSON(`status`、`stdout`、`exit_code`),执行链路通畅,cwd 默认落在 temp 目录。\n\n刚才还验证了 `inline_eval` 能正确访问 agent 内部状态并导出 history,`file_patch` 也能精准改文件。整套工具链从代码执行、文件读写、到 agent 内省都是通的。"}]}]
|
||||
+56
-23
@@ -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,务必让我感兴趣'
|
||||
if st.button(T('suggest_btn')):
|
||||
st.session_state['_inject_prompt'] = T('suggest_prompt')
|
||||
st.rerun(scope="app")
|
||||
st.divider()
|
||||
if st.button("开始空闲自主行动"):
|
||||
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.toast("已将上次回复时间设为1800秒前,自主行动已激活"); st.rerun(scope="app")
|
||||
st.rerun(scope="app")
|
||||
if st.session_state.autonomous_enabled:
|
||||
if st.button("⏸️ 禁止自主行动"):
|
||||
if st.button(T('auto_pause')):
|
||||
st.session_state.autonomous_enabled = False
|
||||
st.toast("⏸️ 已禁止自主行动"); st.rerun(scope="app")
|
||||
st.caption("🟢 自主行动运行中,会在你离开它30分钟后自动进行")
|
||||
st.toast(T('auto_pause')); st.rerun(scope="app")
|
||||
st.caption(T('auto_on_cap'))
|
||||
else:
|
||||
if st.button("▶️ 允许自主行动", type="primary"):
|
||||
if st.button(T('auto_enable'), 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"""<div id="last-reply-time" style="display:none">{st.session_state.get('last_reply_time', int(time.time()))}</div>""", 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()
|
||||
@@ -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')
|
||||
|
||||
+7
-41
@@ -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
|
||||
|
||||
Reference in new issue
Block a user