From 4ca9028350ab3a8fba804483b8c59458d385da0e Mon Sep 17 00:00:00 2001 From: Liang Jiaqing Date: Fri, 30 Oct 2026 10:16:02 +0800 Subject: [PATCH] llmcore: split legacy text-protocol sessions into legacy_llmcore.py; tweak memory-distill prompt --- ga.py | 2 +- legacy_llmcore.py | 145 ++++++++++++++++++++++++++++++++++++++++++++++ llmcore.py | 139 ++------------------------------------------ 3 files changed, 151 insertions(+), 135 deletions(-) create mode 100644 legacy_llmcore.py diff --git a/ga.py b/ga.py index 15fdb71b..07385a04 100644 --- a/ga.py +++ b/ga.py @@ -511,7 +511,7 @@ class GenericAgentHandler(BaseHandler): def do_start_long_term_update(self, args, response): '''Agent觉得当前任务完成后有重要信息需要记忆时调用此工具。''' prompt = get_global_memory() + '''【记忆提纯】任务完成或到达重要检查点后,按L0提取最近一次任务中【事实验证成功且长期有效】的环境事实、用户偏好、重要步骤;已在整理则不重复启动。 -先读后patch,将已验证、长期有用且难以重建的新知识融入旧条目,合并重复、压缩冗述,不堆叠流水账。以更少文字保留更高复用价值;不得为缩短而丢失关键事实、适用条件和踩坑信息。无提纯价值则跳过,索引按需同步。 +先读后patch,将已验证、长期有用且难以重建的新知识融入旧条目,合并重复、压缩冗述,不堆叠流水账。以更少文字保留更高复用价值;无提纯价值则跳过,索引按需同步,非必要不可增加触发词。 禁止:临时状态、推理过程、未验证信息、通用常识、易重建细节;不得将模型推测或建议记作用户要求。 记忆整理仅是内部收尾;完成或跳过后,仍须向用户报告原任务结果。''' yield "[Info] Start distilling good memory for long-term storage.\n" diff --git a/legacy_llmcore.py b/legacy_llmcore.py new file mode 100644 index 00000000..79114d59 --- /dev/null +++ b/legacy_llmcore.py @@ -0,0 +1,145 @@ +# legacy_llmcore.py — llmcore非native三件套(待淘汰), 单向依赖llmcore(禁from-import), 尾部回挂名 +# 删除条件: [Legacy]日志零引用后, 删本文件+llmcore尾3行+resolve中non-native分支 +import os, json, re +import llmcore + +class ClaudeSession(llmcore.BaseSession): + def raw_ask(self, messages): + messages = llmcore._fix_messages(messages) + if self.max_tokens is None: self.max_tokens = 8192 + headers = {"x-api-key": self.api_key, "Content-Type": "application/json", "anthropic-version": "2023-06-01", "anthropic-beta": "prompt-caching-2024-07-31"} + payload = {"model": self.model, "messages": messages, "max_tokens": self.max_tokens, "stream": self.stream} + if self.temperature != 1: payload["temperature"] = self.temperature + self._apply_claude_thinking(payload) + if self.system: payload["system"] = [{"type": "text", "text": self.system, "cache_control": {"type": "persistent"}}] + url = llmcore.auto_make_url(self.api_base, "messages") + parse_fn = (lambda r: llmcore._parse_claude_sse(r.iter_lines())) if self.stream else (lambda r: llmcore._parse_claude_json(r.json())) + return (yield from llmcore._stream_with_retry(self, url, headers, payload, parse_fn)) + def make_messages(self, raw_list): + msgs = llmcore._drop_unsigned_thinking([{"role": m['role'], "content": list(m['content'])} for m in raw_list]) + user_idxs = [i for i, m in enumerate(msgs) if m['role'] == 'user'] + for idx in user_idxs[-2:]: + msgs[idx]["content"][-1] = dict(msgs[idx]["content"][-1], cache_control={"type": "ephemeral"}) + return msgs + +class LLMSession(llmcore.BaseSession): + def raw_ask(self, messages): return (yield from llmcore._openai_stream(self, messages)) + def make_messages(self, raw_list): return llmcore._msgs_claude2oai(llmcore._fix_messages(raw_list)) + +class ToolClient: + def __init__(self, backend, auto_save_tokens=True): + self.backend = backend + self.auto_save_tokens = auto_save_tokens + self.last_tools = '' + self.name = self.backend.name + self.total_cd_tokens = 0 + self.log_path = None + + def chat(self, messages, tools=None): + tools = json.loads(json.dumps(tools, ensure_ascii=False)) if tools else tools + for t in tools or []: + f = t.get('function', {}) + if f.get('name') == 'file_write': + props = f.get('parameters', {}).get('properties', {}) + props.pop('content', None) + extra = '. Content must be placed in tags in reply body, not in args' + if extra not in f.get('description', ''): f['description'] = f.get('description', '') + extra + break + full_prompt = self._build_protocol_prompt(messages, tools) + print("Full prompt length:", len(full_prompt), 'chars') + gen = self.backend.ask(full_prompt) + llmcore._write_llm_log('Prompt', full_prompt, self.log_path) + raw_text = '' + for chunk in gen: + raw_text += chunk; yield chunk + llmcore._write_llm_log('Response', raw_text, self.log_path, model=self.backend.model) + return self._parse_mixed_response(raw_text) + + def _prepare_tool_instruction(self, tools): + tool_instruction = "" + if not tools: return tool_instruction + tools_json = json.dumps(tools, ensure_ascii=False, separators=(',', ':')) + _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. **行动**: 如需调用工具,请在回复正文之后输出一个(或多个)**块**,然后结束。 +""" + 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### 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 + + def _build_protocol_prompt(self, messages, tools): + system_content = next((m['content'] for m in messages if m['role'].lower() == 'system'), "") + history_msgs = [m for m in messages if m['role'].lower() != 'system'] + tool_instruction = self._prepare_tool_instruction(tools) + system = ""; user = "" + if system_content: system += f"{system_content}\n" + system += f"{tool_instruction}" + for m in history_msgs: + role = "USER" if m['role'] == 'user' else "ASSISTANT" + user += f"=== {role} ===\n" + for tr in m.get('tool_results', []): user += f'{tr["content"]}\n' + user += str(m['content']) + "\n" + self.total_cd_tokens += len(user) // 3 + if self.total_cd_tokens > 9000: self.last_tools = '' + user += "=== ASSISTANT ===\n" + return system + user + + def _parse_mixed_response(self, text): + remaining_text = text; thinking = '' + think_match = re.search(r"(.*?)", text, re.DOTALL) + if think_match: + thinking = think_match.group(1).strip() + remaining_text = re.sub(r"(.*?)", "", remaining_text, flags=re.DOTALL) + tool_calls, remaining_text = llmcore._parse_text_tool_calls(remaining_text) + if not tool_calls: + json_strs = []; errors = [] + if '' in remaining_text: + weaktoolstr = remaining_text.split('')[-1].strip().strip('><') + json_str = weaktoolstr if weaktoolstr.endswith('}') else '' + if json_str == '' and '```' in weaktoolstr and weaktoolstr.split('```')[0].strip().endswith('}'): + json_str = weaktoolstr.split('```')[0].strip() + if json_str: json_strs.append(json_str) + remaining_text = remaining_text.replace(''+weaktoolstr, "") + elif '"name":' in remaining_text and '"arguments":' in remaining_text: + json_match = re.search(r'\{.*"name":.*\}', remaining_text, re.DOTALL) + if json_match: + json_strs.append(json_match.group(0).strip()) + remaining_text = remaining_text.replace(json_match.group(0), "").strip() + for json_str in json_strs: + try: + data = llmcore.tryparse(json_str) + func_name = data.get('name') or data.get('function') or data.get('tool') + args = data.get('arguments') or data.get('args') or data.get('params') or data.get('parameters') + if args is None: args = data + if func_name: tool_calls.append(llmcore.MockToolCall(func_name, args)) + except json.JSONDecodeError: + errors.append(f'Failed to parse tool_use JSON: {json_str[:200]}') + self.last_tools = '' + except: pass + if not tool_calls: + for e in errors: + print(f"[Warn] {e}"); tool_calls.append(llmcore.MockToolCall('bad_json', {'msg': e})) + return llmcore.MockResponse(thinking, remaining_text.strip(), tool_calls, text) + +# 尾挂名: 回挂llmcore(谁后初始化完谁挂) + +llmcore.ClaudeSession = ClaudeSession +llmcore.LLMSession = LLMSession +llmcore.ToolClient = ToolClient + diff --git a/llmcore.py b/llmcore.py index 611e8355..a81668e1 100644 --- a/llmcore.py +++ b/llmcore.py @@ -754,29 +754,6 @@ def _ensure_thinking_blocks(messages, model): if not has_thinking: m["content"] = [{"type": "thinking", "thinking": "...", "signature": "placeholder"}, *c] return messages -class ClaudeSession(BaseSession): - def raw_ask(self, messages): - messages = _fix_messages(messages) - if self.max_tokens is None: self.max_tokens = 8192 - headers = {"x-api-key": self.api_key, "Content-Type": "application/json", "anthropic-version": "2023-06-01", "anthropic-beta": "prompt-caching-2024-07-31"} - payload = {"model": self.model, "messages": messages, "max_tokens": self.max_tokens, "stream": self.stream} - if self.temperature != 1: payload["temperature"] = self.temperature - self._apply_claude_thinking(payload) - if self.system: payload["system"] = [{"type": "text", "text": self.system, "cache_control": {"type": "persistent"}}] - url = auto_make_url(self.api_base, "messages") - parse_fn = (lambda r: _parse_claude_sse(r.iter_lines())) if self.stream else (lambda r: _parse_claude_json(r.json())) - return (yield from _stream_with_retry(self, url, headers, payload, parse_fn)) - def make_messages(self, raw_list): - msgs = _drop_unsigned_thinking([{"role": m['role'], "content": list(m['content'])} for m in raw_list]) - user_idxs = [i for i, m in enumerate(msgs) if m['role'] == 'user'] - for idx in user_idxs[-2:]: - msgs[idx]["content"][-1] = dict(msgs[idx]["content"][-1], cache_control={"type": "ephemeral"}) - return msgs - -class LLMSession(BaseSession): - def raw_ask(self, messages): return (yield from _openai_stream(self, messages)) - def make_messages(self, raw_list): return _msgs_claude2oai(_fix_messages(raw_list)) - def _fix_messages(messages): if not messages: return messages W = lambda c: c if isinstance(c, list) else [{"type": "text", "text": str(c)}] @@ -933,117 +910,6 @@ class MockResponse: def __repr__(self): return f"" -class ToolClient: - def __init__(self, backend, auto_save_tokens=True): - self.backend = backend - self.auto_save_tokens = auto_save_tokens - self.last_tools = '' - self.name = self.backend.name - self.total_cd_tokens = 0 - self.log_path = None - - def chat(self, messages, tools=None): - tools = json.loads(json.dumps(tools, ensure_ascii=False)) if tools else tools - for t in tools or []: - f = t.get('function', {}) - if f.get('name') == 'file_write': - props = f.get('parameters', {}).get('properties', {}) - props.pop('content', None) - extra = '. Content must be placed in tags in reply body, not in args' - if extra not in f.get('description', ''): f['description'] = f.get('description', '') + extra - break - full_prompt = self._build_protocol_prompt(messages, tools) - print("Full prompt length:", len(full_prompt), 'chars') - gen = self.backend.ask(full_prompt) - _write_llm_log('Prompt', full_prompt, self.log_path) - raw_text = '' - for chunk in gen: - raw_text += chunk; yield chunk - _write_llm_log('Response', raw_text, self.log_path, model=self.backend.model) - return self._parse_mixed_response(raw_text) - - def _prepare_tool_instruction(self, tools): - tool_instruction = "" - if not tools: return tool_instruction - tools_json = json.dumps(tools, ensure_ascii=False, separators=(',', ':')) - _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. **行动**: 如需调用工具,请在回复正文之后输出一个(或多个)**块**,然后结束。 -""" - 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### 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 - - def _build_protocol_prompt(self, messages, tools): - system_content = next((m['content'] for m in messages if m['role'].lower() == 'system'), "") - history_msgs = [m for m in messages if m['role'].lower() != 'system'] - tool_instruction = self._prepare_tool_instruction(tools) - system = ""; user = "" - if system_content: system += f"{system_content}\n" - system += f"{tool_instruction}" - for m in history_msgs: - role = "USER" if m['role'] == 'user' else "ASSISTANT" - user += f"=== {role} ===\n" - for tr in m.get('tool_results', []): user += f'{tr["content"]}\n' - user += str(m['content']) + "\n" - self.total_cd_tokens += len(user) // 3 - if self.total_cd_tokens > 9000: self.last_tools = '' - user += "=== ASSISTANT ===\n" - return system + user - - def _parse_mixed_response(self, text): - remaining_text = text; thinking = '' - think_match = re.search(r"(.*?)", text, re.DOTALL) - if think_match: - thinking = think_match.group(1).strip() - remaining_text = re.sub(r"(.*?)", "", remaining_text, flags=re.DOTALL) - tool_calls, remaining_text = _parse_text_tool_calls(remaining_text) - if not tool_calls: - json_strs = []; errors = [] - if '' in remaining_text: - weaktoolstr = remaining_text.split('')[-1].strip().strip('><') - json_str = weaktoolstr if weaktoolstr.endswith('}') else '' - if json_str == '' and '```' in weaktoolstr and weaktoolstr.split('```')[0].strip().endswith('}'): - json_str = weaktoolstr.split('```')[0].strip() - if json_str: json_strs.append(json_str) - remaining_text = remaining_text.replace(''+weaktoolstr, "") - elif '"name":' in remaining_text and '"arguments":' in remaining_text: - json_match = re.search(r'\{.*"name":.*\}', remaining_text, re.DOTALL) - if json_match: - json_strs.append(json_match.group(0).strip()) - remaining_text = remaining_text.replace(json_match.group(0), "").strip() - for json_str in json_strs: - try: - data = tryparse(json_str) - func_name = data.get('name') or data.get('function') or data.get('tool') - args = data.get('arguments') or data.get('args') or data.get('params') or data.get('parameters') - if args is None: args = data - if func_name: tool_calls.append(MockToolCall(func_name, args)) - except json.JSONDecodeError: - errors.append(f'Failed to parse tool_use JSON: {json_str[:200]}') - self.last_tools = '' - except: pass - if not tool_calls: - for e in errors: - print(f"[Warn] {e}"); tool_calls.append(MockToolCall('bad_json', {'msg': e})) - return MockResponse(thinking, remaining_text.strip(), tool_calls, text) - def _parse_text_tool_calls(content): """Fallback: extract tool calls from text when model doesn't use native tool_use blocks.""" tcs = [] @@ -1257,6 +1123,7 @@ def resolve_session(cfg_name, cfg=None): if not cfg: raise ValueError(f"Config '{cfg_name}' not in mykey") cfg = {**cfg, '_mykey_name': cfg_name} if 'native' in cfg_name: return (NativeClaudeSession if 'claude' in cfg_name else NativeOAISession)(cfg=cfg) + print(f"[Legacy] {cfg_name} → 非native文本协议路径(待淘汰, 见legacy_llmcore.py)") if 'claude' in cfg_name: return ClaudeSession(cfg=cfg) return LLMSession(cfg=cfg) if 'oai' in cfg_name else None @@ -1268,3 +1135,7 @@ def fast_ask(prompt, cfg_name): sess = resolve_session(cfg_name) if not sess: raise ValueError(f"fast_ask: '{cfg_name}' unsupported") return "".join(sess.raw_ask([{"role": "user", "content": prompt}])) + +# legacy三件套(待淘汰)已分离至legacy_llmcore.py, 由其尾部回挂名(禁本行前import) +try: import legacy_llmcore +except AttributeError: pass # legacy先import时半初始化, 其尾部自会回挂