From 7cadbd74037ea5a7ac67b5a8767a76d5211f6d02 Mon Sep 17 00:00:00 2001 From: Liang Jiaqing Date: Thu, 16 Apr 2026 18:47:40 +0800 Subject: [PATCH] feat: i18n support - auto-detect system language for zh/en prompts --- agent_loop.py | 8 +++--- agentmain.py | 6 +++-- assets/insight_fixed_structure_en.txt | 9 +++++++ assets/sys_prompt_en.txt | 7 +++++ frontends/stapp.py | 2 +- ga.py | 5 ++-- llmcore.py | 39 +++++++++++++++++++-------- 7 files changed, 56 insertions(+), 20 deletions(-) create mode 100644 assets/insight_fixed_structure_en.txt create mode 100644 assets/sys_prompt_en.txt diff --git a/agent_loop.py b/agent_loop.py index 227b2bf..6a77a0f 100644 --- a/agent_loop.py +++ b/agent_loop.py @@ -70,7 +70,7 @@ def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema, tool_name, args, tid = tc['tool_name'], tc['args'], tc.get('id', '') if tool_name == 'no_tool': pass else: - if verbose: yield f"🛠️ 正在调用工具: `{tool_name}` 📥参数:\n````text\n{get_pretty_json(args)}\n````\n" + if verbose: yield f"🛠️ Tool: `{tool_name}` 📥 args:\n````text\n{get_pretty_json(args)}\n````\n" else: yield f"🛠️ {tool_name}({_compact_tool_args(tool_name, args)})\n\n\n" handler.current_turn = turn gen = handler.dispatch(tool_name, args, response, index=ii) @@ -104,8 +104,8 @@ def _clean_content(text): def _shrink_code(m): lines = m.group(0).split('\n') lang = lines[0].replace('```','').strip() - body = [l for l in lines[1:-1] if l.strip()] # 去掉```行和空行 - if len(body) <= 6: return m.group(0) # 短代码保留 + body = [l for l in lines[1:-1] if l.strip()] + if len(body) <= 6: return m.group(0) preview = '\n'.join(body[:5]) return f'```{lang}\n{preview}\n ... ({len(body)} lines)\n```' text = re.sub(r'```[\s\S]*?```', _shrink_code, text) @@ -115,7 +115,7 @@ def _clean_content(text): def _compact_tool_args(name, args): a = {k: v for k, v in args.items() if k != '_index'} - for k in ('path',): # 只缩短路径 + for k in ('path',): if k in a: a[k] = os.path.basename(a[k]) if name == 'update_working_checkpoint': s = a.get('key_info', ''); return (s[:60]+'...') if len(s)>60 else s s = json.dumps(a, ensure_ascii=False); return (s[:120]+'...') if len(s)>120 else s diff --git a/agentmain.py b/agentmain.py index f6b28a8..ba6eafe 100644 --- a/agentmain.py +++ b/agentmain.py @@ -1,4 +1,5 @@ -import os, sys, threading, queue, time, json, re, random +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') 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") @@ -32,7 +33,8 @@ if not os.path.exists(cdp_cfg): except Exception as e: print(f'[WARN] CDP config init failed: {e} — advanced web features (tmwebdriver) will be unavailable.') def get_system_prompt(): - with open(os.path.join(script_dir, 'assets/sys_prompt.txt'), 'r', encoding='utf-8') as f: prompt = f.read() + suffix = '_en' if os.environ.get('GA_LANG', '') == 'en' else '' + with open(os.path.join(script_dir, f'assets/sys_prompt{suffix}.txt'), 'r', encoding='utf-8') as f: prompt = f.read() prompt += f"\nToday: {time.strftime('%Y-%m-%d %a')}\n" prompt += get_global_memory() return prompt diff --git a/assets/insight_fixed_structure_en.txt b/assets/insight_fixed_structure_en.txt new file mode 100644 index 0000000..8e16144 --- /dev/null +++ b/assets/insight_fixed_structure_en.txt @@ -0,0 +1,9 @@ +Facts(L2): ../memory/global_mem.txt | CodeRoot: ../ | SOPs(L3): ../memory/*.md or *.py | META-SOP(L0): ../memory/memory_management_sop.md +L1 Insight is a minimal index; sync L1 when L2/L3 changes; keep index minimal. Read META-SOP(L0) before writing any memory. + +[CONSTITUTION] +1. Ask before modifying own source code; free to experiment within ./; installing packages and portable tools allowed +2. Check memory before decisions; always use existing SOPs/utils; revisit SOPs on repeated failures; never assert without evidence +3. Execute step by step, control granularity, limit blast radius; request intervention after 3 failures +4. Key/secret files: reference only, never read or move +5. Read META-SOP to verify before writing any memory; files under memory/ must be patched only (unless creating new) \ No newline at end of file diff --git a/assets/sys_prompt_en.txt b/assets/sys_prompt_en.txt new file mode 100644 index 0000000..7157ee5 --- /dev/null +++ b/assets/sys_prompt_en.txt @@ -0,0 +1,7 @@ +# Role: Physical-Level Omnipotent Executor +You have full physical access: file I/O, script execution, browser JS injection, and system-level intervention. Never deflect with "can't do it" — don't speculate, use tools to probe. +Use English to thinking, summary and reply. +## Action Principles +Before each tool call, reason inside : current phase, whether the last result met expectations, and next strategy. +- Probe first: on failure, gather sufficient info (logs/status/context), store key findings in working memory, then decide to retry or pivot. Ask the user before irreversible operations. +- Failure escalation: 1st fail → read error and understand cause; 2nd → probe environment state; 3rd → deep analysis then switch approach or ask user. Never repeat an action without new information. \ No newline at end of file diff --git a/frontends/stapp.py b/frontends/stapp.py index c3ae19a..f7da316 100644 --- a/frontends/stapp.py +++ b/frontends/stapp.py @@ -171,7 +171,7 @@ _js_ime_fix = ("" if os.name == 'nt' else "f();new MutationObserver(f).observe(d.body,{childList:1,subtree:1})}()") _embed_html(f'', height=0) -if prompt := st.chat_input("请输入指令"): +if prompt := st.chat_input("any task?"): st.session_state.messages.append({"role": "user", "content": prompt}) if hasattr(agent, '_pet_req') and not prompt.startswith('/'): agent._pet_req('state=walk') with st.chat_message("user"): st.markdown(prompt) diff --git a/ga.py b/ga.py index 5b446b4..b018497 100644 --- a/ga.py +++ b/ga.py @@ -566,9 +566,10 @@ def get_global_memory(): prompt = "\n" try: script_dir = os.path.dirname(os.path.abspath(__file__)) + suffix = '_en' if os.environ.get('GA_LANG', '') == 'en' else '' with open(os.path.join(script_dir, 'memory/global_mem_insight.txt'), 'r', encoding='utf-8', errors='replace') as f: insight = f.read() - with open(os.path.join(script_dir, 'assets/insight_fixed_structure.txt'), 'r', encoding='utf-8') as f: structure = f.read() - prompt += f'cwd = {os.path.join(script_dir, "temp")} (用./引用)\n' + with open(os.path.join(script_dir, f'assets/insight_fixed_structure{suffix}.txt'), 'r', encoding='utf-8') as f: structure = f.read() + prompt += f'cwd = {os.path.join(script_dir, "temp")} (./)\n' prompt += f"\n[Memory] (../memory)\n" prompt += structure + '\n../memory/global_mem_insight.txt:\n' prompt += insight + "\n" diff --git a/llmcore.py b/llmcore.py index 710d1ef..920d4a7 100644 --- a/llmcore.py +++ b/llmcore.py @@ -663,24 +663,31 @@ class ToolClient: total += 1000 return total return len(str(content)) - + def _prepare_tool_instruction(self, tools): tool_instruction = "" if not tools: return tool_instruction tools_json = json.dumps(tools, ensure_ascii=False, separators=(',', ':')) - tool_instruction = f""" + _en = os.environ.get('GA_LANG') == 'en' + if _en: + tool_instruction = f""" +### Interaction Protocol (must follow strictly, always in effect) +Follow these steps to think and act: +1. **Think**: Analyze the current situation and strategy inside `` tags. +2. **Summarize**: Output a minimal one-line (<30 words) physical snapshot in ``: new info from last tool result + current tool call intent. This goes into long-term working memory. Must contain real information, no filler. +3. **Act**: If you need to call tools, output one or more ** blocks** after your reply, then stop. +""" + else: + tool_instruction = f""" ### 交互协议 (必须严格遵守,持续有效) 请按照以下步骤思考并行动: 1. **思考**: 在 `` 标签中先进行思考,分析现状和策略。 2. **总结**: 在 `` 中输出*极为简短*的高度概括的单行(<30字)物理快照,包括上次工具调用结果产生的新信息+本次工具调用意图。此内容将进入长期工作记忆,记录关键信息,严禁输出无实际信息增量的描述。 3. **行动**: 如需调用工具,请在回复正文之后输出一个(或多个)**块**,然后结束。 -格式: ```{{"name": "工具名", "arguments": {{参数}}}}``` - -### 可用工具库(已挂载,持续有效) -{tools_json} """ + tool_instruction += f'\nFormat: ```{{"name": "tool_name", "arguments": {{...}}}}```\n\n### Tools (mounted, always in effect):\n{tools_json}\n' if self.auto_save_tokens and self.last_tools == tools_json: - tool_instruction = "\n### 工具库状态:持续有效(code_run/file_read等),**可正常调用**。调用协议沿用。\n" + tool_instruction = "\n### Tools: still active, **ready to call**. Protocol unchanged.\n" if _en else "\n### 工具库状态:持续有效(code_run/file_read等),**可正常调用**。调用协议沿用。\n" else: self.total_cd_tokens = 0 self.last_tools = tools_json return tool_instruction @@ -851,21 +858,31 @@ class MixinSession: time.sleep(delay) else: print(f'[MixinSession] {last_chunk[:80]}, retry {attempt+1}/{self._retries} (s{idx}→s{nxt})') -class NativeToolClient: - THINKING_PROMPT = """ +THINKING_PROMPT_ZH = """ ### 行动规范(持续有效) 每次回复请遵循: 1. 在 标签中先分析现状和策略 2. 在 中输出极简单行(<30字)物理快照:上次结果新信息+本次意图。此内容进入长期工作记忆。 3. 然后才能输出工具调用 """.strip() +THINKING_PROMPT_EN = """ +### Action Protocol (always in effect) +For every reply, follow these steps: +1. Analyze the current situation and strategy inside +2. Output a minimal one-line (<30 words) physical snapshot in : new info from last result + current intent. This goes into long-term working memory. +3. Then output tool calls +""".strip() + +class NativeToolClient: + @staticmethod + def _thinking_prompt(): return THINKING_PROMPT_EN if os.environ.get('GA_LANG') == 'en' else THINKING_PROMPT_ZH def __init__(self, backend): self.backend = backend - self.backend.system = self.THINKING_PROMPT + self.backend.system = self._thinking_prompt() self.name = self.backend.name self._pending_tool_ids = [] def set_system(self, extra_system): - combined = f"{extra_system}\n\n{self.THINKING_PROMPT}" if extra_system else self.THINKING_PROMPT + combined = f"{extra_system}\n\n{self._thinking_prompt()}" if extra_system else self._thinking_prompt() if combined != self.backend.system: print(f"[Debug] Updated system prompt, length {len(combined)} chars.") self.backend.system = combined def chat(self, messages, tools=None):