diff --git a/agentmain.py b/agentmain.py index 16c1e354..937fb7eb 100644 --- a/agentmain.py +++ b/agentmain.py @@ -1,16 +1,12 @@ import os, sys, threading, queue, time, json, re, random, locale os.environ.setdefault('GA_LANG', 'zh' if any(k in (locale.getlocale()[0] or '').lower() for k in ('zh', 'chinese')) else 'en') -try: - import readline -except Exception: - readline = None if sys.stdout is None: sys.stdout = open(os.devnull, "w") elif hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(errors='replace') if sys.stderr is None: sys.stderr = open(os.devnull, "w") elif hasattr(sys.stderr, 'reconfigure'): sys.stderr.reconfigure(errors='replace') sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from llmcore import LLMSession, ToolClient, ClaudeSession, MixinSession, NativeToolClient, NativeClaudeSession, NativeOAISession +from llmcore import reload_mykeys, LLMSession, ToolClient, ClaudeSession, MixinSession, NativeToolClient, NativeClaudeSession, NativeOAISession from agent_loop import agent_runner_loop from ga import GenericAgentHandler, smart_format, get_global_memory, format_error, consume_file @@ -47,7 +43,20 @@ class GeneraticAgent: def __init__(self): script_dir = os.path.dirname(os.path.abspath(__file__)) os.makedirs(os.path.join(script_dir, 'temp'), exist_ok=True) - from llmcore import mykeys + self.lock = threading.Lock() + self.task_dir = None + self.history = [] + self.task_queue = queue.Queue() + self.is_running = False; self.stop_sig = False + self.llm_no = 0; self.inc_out = False + self.handler = None; self.verbose = True + self.load_llm_sessions() + + def load_llm_sessions(self): + mykeys, changed = reload_mykeys() + if not changed and hasattr(self, 'llmclients'): return + try: oldhistory = self.llmclient.backend.history + except: oldhistory = None llm_sessions = [] for k, cfg in mykeys.items(): if not any(x in k for x in ['api', 'config', 'cookie']): continue @@ -66,16 +75,11 @@ class GeneraticAgent: else: llm_sessions[i] = ToolClient(mixin) except Exception as e: print(f'[WARN] Failed to init MixinSession with cfg {s["mixin_cfg"]}: {e}') self.llmclients = llm_sessions - self.lock = threading.Lock() - self.task_dir = None - self.history = [] - self.task_queue = queue.Queue() - self.is_running = False; self.stop_sig = False - self.llm_no = 0; self.inc_out = False - self.handler = None; self.verbose = True - self.llmclient = self.llmclients[self.llm_no] - + self.llmclient = self.llmclients[self.llm_no%len(self.llmclients)] + if oldhistory: self.llmclient.backend.history = oldhistory + def next_llm(self, n=-1): + self.load_llm_sessions() self.llm_no = ((self.llm_no + 1) if n < 0 else n) % len(self.llmclients) lastc = self.llmclient self.llmclient = self.llmclients[self.llm_no] @@ -85,7 +89,9 @@ class GeneraticAgent: name = self.get_llm_name(model=True) if 'glm' in name or 'minimax' in name or 'kimi' in name: load_tool_schema('_cn') else: load_tool_schema() - def list_llms(self): return [(i, self.get_llm_name(b), i == self.llm_no) for i, b in enumerate(self.llmclients)] + def list_llms(self): + self.load_llm_sessions() + return [(i, self.get_llm_name(b), i == self.llm_no) for i, b in enumerate(self.llmclients)] def get_llm_name(self, b=None, model=False): b = self.llmclient if b is None else b if isinstance(b, dict): return 'BADCONFIG_MIXIN' @@ -139,12 +145,8 @@ class GeneraticAgent: handler.working['passed_sessions'] = ps = self.handler.working.get('passed_sessions', 0) + 1 if ps > 0: handler.working['key_info'] += f'\n[SYSTEM] 此为 {ps} 个对话前设置的key_info,若已在新任务,先更新或清除工作记忆。\n' self.handler = handler - user_input = raw_query - if source == 'feishu' and len(self.history) > 1: # 如果有历史记录且来自飞书,注入到首轮 user_input 中(支持/restore恢复上下文) - user_input = handler._get_anchor_prompt() + f"\n\n### 用户当前消息\n{raw_query}" - #if 'gpt' in self.get_llm_name(model=True): handler._done_hooks.append('请确定任务是否完成,如果完成请给出信息完整的简报回答,如未完成需要继续工具调用直到完成任务,确实需要问用户应使用ask_user工具') # although new handler, the **full** history is in llmclient, so it is full history! - gen = agent_runner_loop(self.llmclient, sys_prompt, user_input, + gen = agent_runner_loop(self.llmclient, sys_prompt, raw_query, handler, TOOLS_SCHEMA, max_turns=70, verbose=self.verbose) try: full_resp = ""; last_pos = 0 @@ -251,6 +253,8 @@ if __name__ == '__main__': except Exception as e: print(f'[Reflect] on_done error: {e}') if getattr(mod, 'ONCE', False): print('[Reflect] ONCE=True, exiting.'); break else: + try: import readline + except Exception: pass agent.inc_out = True while True: q = input('> ').strip() diff --git a/assets/code_run_header.py b/assets/code_run_header.py index 68bfc01f..59fb3416 100644 --- a/assets/code_run_header.py +++ b/assets/code_run_header.py @@ -24,7 +24,4 @@ def _pinit(self, *a, **k): if os.name == 'nt': k['creationflags'] = (k.get('creationflags') or 0) | 0x08000000 _Pi(self, *a, **k) subprocess.Popen.__init__ = _pinit -if hasattr(os, 'startfile'): - def _nosf(*a, **k): raise RuntimeError("startfile disabled in sandbox") - os.startfile = _nosf sys.excepthook = lambda t, v, tb: (sys.__excepthook__(t, v, tb), print(f"\n[Agent Hint]: NO GUESSING! You MUST probe first. If missing common package, pip.")) if issubclass(t, (ImportError, AttributeError)) else sys.__excepthook__(t, v, tb) diff --git a/frontends/stapp.py b/frontends/stapp.py index 85cf63b7..59bade27 100644 --- a/frontends/stapp.py +++ b/frontends/stapp.py @@ -36,13 +36,16 @@ if 'autonomous_enabled' not in st.session_state: st.session_state.autonomous_ena @st.fragment def render_sidebar(): + llm_options = agent.list_llms() current_idx = agent.llm_no - st.caption(f"LLM Core: {current_idx}: {agent.get_llm_name()}", help="点击切换备用链路") + llm_labels = {idx: f"{idx}: {(name or '').strip()}" for idx, name, _ in llm_options} + st.caption(f"LLM Core: {llm_labels.get(current_idx, str(current_idx))}", help="下拉切换备用链路") + selected_idx = st.selectbox("备用链路", [idx for idx, _, _ in llm_options], index=next((i for i, (idx, _, _) in enumerate(llm_options) if idx == current_idx), 0), format_func=llm_labels.get, label_visibility="collapsed", key="sidebar_llm_select") + if selected_idx != current_idx: + agent.next_llm(selected_idx); st.rerun(scope="fragment") last_reply_time = st.session_state.get('last_reply_time', 0) if last_reply_time > 0: st.caption(f"空闲时间:{int(time.time()) - last_reply_time}秒", help="当超过30分钟未收到回复时,系统会自动任务") - if st.button("切换备用链路"): - agent.next_llm(); st.rerun(scope="fragment") if st.button("强行停止任务"): agent.abort(); st.toast("已发送停止信号"); st.rerun() if st.button("重新注入工具"): diff --git a/ga.py b/ga.py index 428d92fc..a03223f3 100644 --- a/ga.py +++ b/ga.py @@ -445,7 +445,7 @@ class GenericAgentHandler(BaseHandler): if not response or (not content.strip() and not thinking.strip()): yield "[Warn] LLM returned an empty response. Retrying...\n" return StepOutcome({}, next_prompt="[System] Blank response, regenerate and tooluse") - if len(content) > 100 and ('未收到完整响应 !!!]' in content[-100:] or '!!!Error: [SSL:' in content[-100:]): + if len(content) > 50 and ('未收到完整响应 !!!]' in content[-100:] or '!!!Error: [SSL:' in content[-100:]): return StepOutcome({}, next_prompt="[System] Incomplete response. Regenerate and tooluse.") if 'max_tokens !!!]' in content[-100:]: return StepOutcome({}, next_prompt="[System] max_tokens limit reached. Use multi small steps to do it.") @@ -490,10 +490,11 @@ class GenericAgentHandler(BaseHandler): '''Agent觉得当前任务完成后有重要信息需要记忆时调用此工具。''' prompt = '''### [总结提炼经验] 既然你觉得当前任务有重要信息需要记忆,请提取最近一次任务中【事实验证成功且长期有效】的环境事实、用户偏好、重要步骤,更新记忆。 本工具是标记开启结算过程,若已在更新记忆过程或没有值得记忆的点,忽略本次调用。 -**提取行动验证成功的信息**: +**如果没有经验证的,未来能用上的信息,忽略本次调用!** +**只能提取行动验证成功的信息**: - **环境事实**(路径/凭证/配置)→ `file_patch` 更新 L2,同步 L1 - **复杂任务经验**(关键坑点/前置条件/重要步骤)→ L3 精简 SOP(只记你被坑得多次重试的核心要点) -**禁止**:临时变量、具体推理过程、未验证信息、通用常识、你可以轻松复现的细节。 +**禁止**:临时变量、具体推理过程、未验证信息、通用常识、你可以轻松复现的细节、只是做了但没有验证的信息 **操作**:严格遵循提供的L0的记忆更新SOP。先 `file_read` 看现有 → 判断类型 → 最小化更新 → 无新内容跳过,保证对记忆库最小局部修改。\n ''' + get_global_memory() yield "[Info] Start distilling good memory for long-term storage.\n" diff --git a/llmcore.py b/llmcore.py index c9798468..96d5703f 100644 --- a/llmcore.py +++ b/llmcore.py @@ -1,26 +1,33 @@ -import os, json, re, time, requests, sys, threading, urllib3, base64, mimetypes, uuid +import os, json, re, time, requests, sys, threading, urllib3, base64, importlib, uuid from datetime import datetime urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) _RESP_CACHE_KEY = str(uuid.uuid4()) def _load_mykeys(): + global _mykey_path try: - import mykey; return {k: v for k, v in vars(mykey).items() if not k.startswith('_')} + import mykey; importlib.reload(mykey); _mykey_path = mykey.__file__ + return {k: v for k, v in vars(mykey).items() if not k.startswith('_')} except ImportError: pass - p = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'mykey.json') + _mykey_path = p = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'mykey.json') if not os.path.exists(p): raise Exception('[ERROR] mykey.py or mykey.json not found, please create one from mykey_template.') with open(p, encoding='utf-8') as f: return json.load(f) +_mykey_path = _mykey_mtime = None +def reload_mykeys(): + global _mykey_mtime + mt = os.stat(_mykey_path).st_mtime_ns if _mykey_path else -1 + if mt == _mykey_mtime: return globals().get('mykeys', {}), False + mk = _load_mykeys(); _mykey_mtime = os.stat(_mykey_path).st_mtime_ns + print(f'[Info] Load mykeys from {_mykey_path}') + globals().update(mykeys=mk) + if mk.get('langfuse_config'): + try: from plugins import langfuse_tracing + except Exception: pass + return mk, True + def __getattr__(name): # once guard in PEP 562 - if name in ('mykeys', 'proxies'): - mk = _load_mykeys() - proxy = mk.get("proxy", 'http://127.0.0.1:2082') - px = {"http": proxy, "https": proxy} if proxy else None - globals().update(mykeys=mk, proxies=px) - if mk.get('langfuse_config'): - try: from plugins import langfuse_tracing - except Exception: pass - return globals()[name] + if name == 'mykeys': return reload_mykeys()[0] raise AttributeError(f"module 'llmcore' has no attribute {name}") def compress_history_tags(messages, keep_recent=10, max_len=800, force=False): @@ -158,6 +165,11 @@ def _parse_claude_sse(resp_lines): if not warn: if not got_message_stop and not stop_reason: warn = "\n\n[!!! 流异常中断,未收到完整响应 !!!]" elif stop_reason == "max_tokens": warn = "\n\n[!!! Response truncated: max_tokens !!!]" + if current_block: + if current_block["type"] == "tool_use": + try: current_block["input"] = json.loads(tool_json_buf) if tool_json_buf else {} + except: current_block["input"] = {"_raw": tool_json_buf} + content_blocks.append(current_block); current_block = None if warn: print(f"[WARN] {warn.strip()}") content_blocks.append({"type": "text", "text": warn}); yield warn @@ -488,7 +500,7 @@ class BaseSession: self.api_key = cfg['apikey'] self.api_base = cfg['apibase'].rstrip('/') self.model = cfg.get('model', '') - self.context_win = cfg.get('context_win', 24000) + self.context_win = cfg.get('context_win', 28000) self.history = [] self.lock = threading.Lock() self.system = "" @@ -598,7 +610,6 @@ def _fix_messages(messages): class NativeClaudeSession(BaseSession): def __init__(self, cfg): super().__init__(cfg) - self.context_win = cfg.get("context_win", 28000) self.fake_cc_system_prompt = cfg.get("fake_cc_system_prompt", False) self.user_agent = cfg.get("user_agent", "claude-cli/2.1.113 (external, cli)") self._session_id = str(uuid.uuid4())