llmcore: split legacy text-protocol sessions into legacy_llmcore.py; tweak memory-distill prompt
This commit is contained in:
1 parent
5dee7f4bd3
commit
4ca9028350
3 files changed
+151
-135
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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
@@ -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时半初始化, 其尾部自会回挂
|
||||
Reference in new issue
Block a user