llmcore: split legacy text-protocol sessions into legacy_llmcore.py; tweak memory-distill prompt

This commit is contained in:
Liang Jiaqing committed 2026-10-30 10:16:02 +08:00
1 parent 5dee7f4bd3
commit 4ca9028350
3 files changed
+151 -135

No files matched your search

+1 -1
View File
@@ -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"
+145
View File
@@ -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 <file_content> 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 `<thinking>` tags.
2. **Summarize**: Output a minimal one-line (<30 words) physical snapshot in `<summary>`: 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 **<tool_use> blocks** after your reply, then stop.
"""
else:
tool_instruction = f"""
### 交互协议 (必须严格遵守,持续有效)
请按照以下步骤思考并行动:
1. **思考**: 在 `<thinking>` 标签中先进行思考,分析现状和策略。
2. **总结**: 在 `<summary>` 中输出*极为简短*的高度概括的单行(<30字)物理快照,包括上次工具调用结果产生的新信息+本次工具调用意图。此内容将进入长期工作记忆,记录关键信息,严禁输出无实际信息增量的描述。
3. **行动**: 如需调用工具,请在回复正文之后输出一个(或多个)**<tool_use>块**,然后结束。
"""
tool_instruction += f'\nFormat: ```<tool_use>{{"name": "tool_name", "arguments": {{...}}}}</tool_use>```\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'<tool_result>{tr["content"]}</tool_result>\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"<think(?:ing)?>(.*?)</think(?:ing)?>", text, re.DOTALL)
if think_match:
thinking = think_match.group(1).strip()
remaining_text = re.sub(r"<think(?:ing)?>(.*?)</think(?:ing)?>", "", remaining_text, flags=re.DOTALL)
tool_calls, remaining_text = llmcore._parse_text_tool_calls(remaining_text)
if not tool_calls:
json_strs = []; errors = []
if '<tool_use>' in remaining_text:
weaktoolstr = remaining_text.split('<tool_use>')[-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('<tool_use>'+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
+5 -134
View File
@@ -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"<MockResponse thinking={bool(self.thinking)}, content='{self.content}', tools={bool(self.tool_calls)}>"
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 <file_content> 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 `<thinking>` tags.
2. **Summarize**: Output a minimal one-line (<30 words) physical snapshot in `<summary>`: 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 **<tool_use> blocks** after your reply, then stop.
"""
else:
tool_instruction = f"""
### 交互协议 (必须严格遵守,持续有效)
请按照以下步骤思考并行动:
1. **思考**: 在 `<thinking>` 标签中先进行思考,分析现状和策略。
2. **总结**: 在 `<summary>` 中输出*极为简短*的高度概括的单行(<30字)物理快照,包括上次工具调用结果产生的新信息+本次工具调用意图。此内容将进入长期工作记忆,记录关键信息,严禁输出无实际信息增量的描述。
3. **行动**: 如需调用工具,请在回复正文之后输出一个(或多个)**<tool_use>块**,然后结束。
"""
tool_instruction += f'\nFormat: ```<tool_use>{{"name": "tool_name", "arguments": {{...}}}}</tool_use>```\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'<tool_result>{tr["content"]}</tool_result>\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"<think(?:ing)?>(.*?)</think(?:ing)?>", text, re.DOTALL)
if think_match:
thinking = think_match.group(1).strip()
remaining_text = re.sub(r"<think(?:ing)?>(.*?)</think(?:ing)?>", "", remaining_text, flags=re.DOTALL)
tool_calls, remaining_text = _parse_text_tool_calls(remaining_text)
if not tool_calls:
json_strs = []; errors = []
if '<tool_use>' in remaining_text:
weaktoolstr = remaining_text.split('<tool_use>')[-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('<tool_use>'+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时半初始化, 其尾部自会回挂