MixinSession: spring-back to primary + translate tools_schema to English
This commit is contained in:
@@ -1,71 +1,71 @@
|
|||||||
[
|
[
|
||||||
{"type": "function", "function": {
|
{"type": "function", "function": {
|
||||||
"name": "code_run",
|
"name": "code_run",
|
||||||
"description": "代码执行器。优先使用python。禁同时调用多个。为免转义问题,代码放正文 ```python/powershell 块中。禁硬编码大量数据",
|
"description": "Code executor. Prefer python. No concurrent calls. Put code in ```python/powershell blocks in reply body to avoid escaping. No hardcoding bulk data",
|
||||||
"parameters": {"type": "object", "properties": {
|
"parameters": {"type": "object", "properties": {
|
||||||
"script": {"type": "string", "description": "[Optional] 要执行的代码。为免转义建议留空,改用正文代码块(与此参数互斥)"},
|
"script": {"type": "string", "description": "[Optional] Code to execute. Prefer code blocks in reply body to avoid escaping (leave empty when using code blocks)"},
|
||||||
"type": {"type": "string", "enum": ["python", "powershell"], "description": "代码类型", "default": "python"},
|
"type": {"type": "string", "enum": ["python", "powershell"], "description": "Code type", "default": "python"},
|
||||||
"timeout": {"type": "integer", "description": "执行超时时间(秒)", "default": 60},
|
"timeout": {"type": "integer", "description": "Timeout in seconds", "default": 60},
|
||||||
"cwd": {"type": "string", "description": "工作目录,默认为当前工作目录"}}}
|
"cwd": {"type": "string", "description": "Working directory, defaults to cwd"}}}
|
||||||
}},
|
}},
|
||||||
{"type": "function", "function": {
|
{"type": "function", "function": {
|
||||||
"name": "file_read",
|
"name": "file_read",
|
||||||
"description": "读取文件内容。建议在修改文件前先读取,以确保获取最新的上下文和行号。支持分页读取或关键字搜索",
|
"description": "Read file content. Read before modifying to get latest context and line numbers. Supports pagination and keyword search",
|
||||||
"parameters": {"type": "object", "properties": {
|
"parameters": {"type": "object", "properties": {
|
||||||
"path": {"type": "string", "description": "文件相对或绝对路径"},
|
"path": {"type": "string", "description": "Relative or absolute file path"},
|
||||||
"start": {"type": "integer", "description": "起始行号(从 1 开始)", "default": 1},
|
"start": {"type": "integer", "description": "Start line number (1-based)", "default": 1},
|
||||||
"count": {"type": "integer", "description": "读取的行数", "default": 200},
|
"count": {"type": "integer", "description": "Number of lines to read", "default": 200},
|
||||||
"keyword": {"type": "string", "description": "可选搜索关键字。如果提供,将返回第一个匹配项(忽略大小写)及其周边的内容"},
|
"keyword": {"type": "string", "description": "Optional search keyword. If provided, returns first match (case-insensitive) with surrounding context"},
|
||||||
"show_linenos": {"type": "boolean", "description": "是否显示行号,建议开启以辅助 file_patch 定位", "default": true}}, "required": ["path"]}
|
"show_linenos": {"type": "boolean", "description": "Show line numbers. Recommended on to help file_patch locate content", "default": true}}, "required": ["path"]}
|
||||||
}},
|
}},
|
||||||
{"type": "function", "function": {
|
{"type": "function", "function": {
|
||||||
"name": "file_patch",
|
"name": "file_patch",
|
||||||
"description": "精细化局部文件修改。在文件中寻找唯一的 old_content 块并替换为 new_content。要求 old_content 必须在文件中唯一存在,且空格、缩进、换行必须与原文件完全一致。如果匹配失败,请使用 file_read 重新确认文件内容",
|
"description": "Fine-grained local file edit. Finds unique old_content block and replaces with new_content. old_content must be unique in file with exact whitespace/indentation/newlines. On match failure, use file_read to re-check file content",
|
||||||
"parameters": {"type": "object", "properties": {
|
"parameters": {"type": "object", "properties": {
|
||||||
"path": {"type": "string", "description": "文件路径"},
|
"path": {"type": "string", "description": "File path"},
|
||||||
"old_content": {"type": "string", "description": "文件中需要被替换的原始文本块(需确保唯一性)"},
|
"old_content": {"type": "string", "description": "Original text block to replace (must be unique in file)"},
|
||||||
"new_content": {"type": "string", "description": "替换后的新文本内容。支持 {{file:路径:起始行:结束行}} 语法引用文件内容,写入前自动展开"}}, "required": ["path", "old_content", "new_content"]}
|
"new_content": {"type": "string", "description": "New text content. Supports {{file:path:startLine:endLine}} syntax to reference file lines, auto-expanded before writing"}}, "required": ["path", "old_content", "new_content"]}
|
||||||
}},
|
}},
|
||||||
{"type": "function", "function": {
|
{"type": "function", "function": {
|
||||||
"name": "file_write",
|
"name": "file_write",
|
||||||
"description": "用于文件的新建、全量覆盖或追加写入。对于精细的代码修改,应优先使用 file_patch。注意:要写入的内容必须放在回复正文的 <file_content> 标签或代码块中。写入内容支持 {{file:路径:起始行:结束行}} 语法引用文件片段,写入前自动展开",
|
"description": "Create, overwrite or append files. Use file_patch for fine-grained edits. Content must be in <file_content> tags or code blocks in reply body. Supports {{file:path:startLine:endLine}} to reference file lines, auto-expanded before writing",
|
||||||
"parameters": {"type": "object", "properties": {
|
"parameters": {"type": "object", "properties": {
|
||||||
"path": {"type": "string", "description": "文件路径"},
|
"path": {"type": "string", "description": "File path"},
|
||||||
"mode": {"type": "string", "enum": ["overwrite", "append", "prepend"], "description": "写入模式覆盖、追加或在开头追加", "default": "overwrite"}}, "required": ["path"]}
|
"mode": {"type": "string", "enum": ["overwrite", "append", "prepend"], "description": "Write mode", "default": "append"}}, "required": ["path"]}
|
||||||
}},
|
}},
|
||||||
{"type": "function", "function": {
|
{"type": "function", "function": {
|
||||||
"name": "web_scan",
|
"name": "web_scan",
|
||||||
"description": "获取当前页面的简化HTML内容和标签页列表。注意:简化会过滤边栏、浮动元素等非主体内容,如需查看被过滤内容请用execute_js。切换页面后一般应先调用查看",
|
"description": "Get simplified HTML and tab list of current page. Simplification filters sidebars/floating elements; use execute_js for filtered content. Call after switching pages",
|
||||||
"parameters": {"type": "object", "properties": {
|
"parameters": {"type": "object", "properties": {
|
||||||
"tabs_only": {"type": "boolean", "description": "仅返回标签页列表和当前标签信息,不获取HTML内容", "default": false},
|
"tabs_only": {"type": "boolean", "description": "Return only tab list and current tab info, no HTML", "default": false},
|
||||||
"switch_tab_id": {"type": "string", "description": "可选的标签页 ID。如果提供,系统将在扫描前切换到该标签页"},
|
"switch_tab_id": {"type": "string", "description": "Optional tab ID. If provided, switches to that tab before scanning"},
|
||||||
"text_only": {"type": "boolean", "description": "只要纯文本不要HTML信息", "default": false}}}
|
"text_only": {"type": "boolean", "description": "Plain text only, no HTML", "default": false}}}
|
||||||
}},
|
}},
|
||||||
{"type": "function", "function": {
|
{"type": "function", "function": {
|
||||||
"name": "web_execute_js",
|
"name": "web_execute_js",
|
||||||
"description": "执行 JS 控制浏览器。建议精准使用减少 web_scan。为免转义问题,代码优先考虑放回复正文 ```javascript 块",
|
"description": "Execute JS to control browser. Use precisely to reduce web_scan calls. Put code in ```javascript blocks in reply body to avoid escaping",
|
||||||
"parameters": {"type": "object", "properties": {
|
"parameters": {"type": "object", "properties": {
|
||||||
"script": {"type": "string", "description": "[Optional] JS代码或路径。为免转义建议留空,改用正文代码块(与此参数互斥)"},
|
"script": {"type": "string", "description": "[Optional] JS code or path. Prefer code blocks in reply body to avoid escaping (mutually exclusive with this param)"},
|
||||||
"save_to_file": {"type": "string", "description": "结果存文件,适合返回值较长时", "default": ""},
|
"save_to_file": {"type": "string", "description": "Save result to file, for long return values", "default": ""},
|
||||||
"no_monitor": {"type": "boolean", "description": "跳过页面变更监控,省2-3秒。仅在纯读取信息时设置,页面操作时不要设置", "default": false}}}
|
"no_monitor": {"type": "boolean", "description": "Skip page change monitoring, saves 2-3s. Only set for pure reads, not for page actions", "default": false}}}
|
||||||
}},
|
}},
|
||||||
{"type": "function", "function": {
|
{"type": "function", "function": {
|
||||||
"name": "update_working_checkpoint",
|
"name": "update_working_checkpoint",
|
||||||
"description": "短期工作便签,每轮自动注入上下文,防长任务信息丢失。前中期调用,非结束时。何时调用:(1)任务开始读SOP后,存用户需求和关键约束/参数(简单1-2步任务除外);(2)子任务切换或上下文即将被冲刷前;(3)多次重试失败后,重读SOP并必须调用存储新发现;(4)切换新任务时更新内容,清旧进度但保留仍有效的约束。\n\n何时不调用:简单任务(1-2步且无严重约束)、任务已完成时(应当用长期结算工具)",
|
"description": "Short-term working notepad, auto-injected each turn to prevent info loss in long tasks. Call during early/mid stages, not at end. When: (1) after reading SOP, store user needs & key constraints (skip for simple 1-2 step tasks); (2) before subtask switch or context flush; (3) after repeated failures, re-read SOP and must store new findings; (4) on new task, update content, clear old progress but keep valid constraints.\n\nDon't call: simple tasks (1-2 steps), task completed (use long-term memory tool)",
|
||||||
"parameters": {"type": "object", "properties": {
|
"parameters": {"type": "object", "properties": {
|
||||||
"key_info": {"type": "string", "description": "替换当前便签(<200 tokens)。增量更新:先回顾现有内容,保留仍有效的,再增删改。存:要避的坑、用户原始需求、关键参数/发现、文件路径、当前进度、下一步计划。不存:马上要用用完即丢的、上下文中显而易见的、用户已换全新任务时的旧任务信息。宁多更新不丢关键"},
|
"key_info": {"type": "string", "description": "Replaces current notepad (<200 tokens). Incremental update: review existing, keep valid, add/remove/modify. Store: pitfalls, user requirements, key params/findings, file paths, progress, next steps. Don't store: ephemeral info, obvious context, old task info when user switched tasks. Prefer over-updating over losing key info"},
|
||||||
"related_sop": {"type": "string", "description": "相关sop名称,可以多个,必要时需要再读"}}}
|
"related_sop": {"type": "string", "description": "Related SOP name(s), re-read when needed"}}}
|
||||||
}},
|
}},
|
||||||
{"type": "function", "function": {
|
{"type": "function", "function": {
|
||||||
"name": "ask_user",
|
"name": "ask_user",
|
||||||
"description": "当需要用户决策、提供额外信息或遇到无法自动解决的阻碍时,调用此工具中断任务并提问",
|
"description": "Interrupt task to ask user when needing decisions, extra info, or facing unresolvable blockers",
|
||||||
"parameters": {"type": "object", "properties": {
|
"parameters": {"type": "object", "properties": {
|
||||||
"question": {"type": "string", "description": "向用户提出的明确问题"},
|
"question": {"type": "string", "description": "Question for the user"},
|
||||||
"candidates": {"type": "array", "items": {"type": "string"}, "description": "提供给用户的可选快捷选项列表"}}, "required": ["question"]}
|
"candidates": {"type": "array", "items": {"type": "string"}, "description": "Optional quick-select choices for the user"}}, "required": ["question"]}
|
||||||
}},
|
}},
|
||||||
{"type": "function", "function": {
|
{"type": "function", "function": {
|
||||||
"name": "start_long_term_update",
|
"name": "start_long_term_update",
|
||||||
"description": "准备开始提炼记忆。发现值得长期记忆的信息(环境事实/用户偏好/避坑经验)时调用此工具。已记忆更新或在自主流程内时无需调用。超15轮完成的任务必须调用以沉淀经验",
|
"description": "Start distilling long-term memory. Call when discovering info worth remembering (env facts/user prefs/lessons learned). Skip if memory already updated or in autonomous flow. Must call for tasks taking 15+ turns",
|
||||||
"parameters": {"type": "object", "properties": {}}}
|
"parameters": {"type": "object", "properties": {}}}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
29
llmcore.py
29
llmcore.py
@@ -801,21 +801,28 @@ def tryparse(json_str):
|
|||||||
|
|
||||||
|
|
||||||
class MixinSession:
|
class MixinSession:
|
||||||
"""Multi-session fallback with exponential backoff on Error: detection."""
|
"""Multi-session fallback with spring-back to primary."""
|
||||||
def __init__(self, all_sessions, cfg):
|
def __init__(self, all_sessions, cfg):
|
||||||
self._retries, self._base_delay = cfg.get('max_retries', 3), cfg.get('base_delay', 1.5)
|
self._retries, self._base_delay = cfg.get('max_retries', 3), cfg.get('base_delay', 1.5)
|
||||||
|
self._spring_sec = cfg.get('spring_back', 300)
|
||||||
self._sessions = [all_sessions[i].backend for i in cfg.get('llm_nos', [])]
|
self._sessions = [all_sessions[i].backend for i in cfg.get('llm_nos', [])]
|
||||||
assert 'Native' not in self._sessions[0].__class__.__name__
|
assert 'Native' not in self._sessions[0].__class__.__name__
|
||||||
assert len(set(type(s) for s in self._sessions)) == 1, f'MixinSession: all sessions must be same type, got {[type(s).__name__ for s in self._sessions]}'
|
assert len(set(type(s) for s in self._sessions)) == 1, f'MixinSession: all sessions must be same type, got {[type(s).__name__ for s in self._sessions]}'
|
||||||
self._orig_raw_asks = [s.raw_ask for s in self._sessions]
|
self._orig_raw_asks = [s.raw_ask for s in self._sessions]
|
||||||
self._sessions[0].raw_ask = self._raw_ask
|
self._sessions[0].raw_ask = self._raw_ask
|
||||||
self.default_model = getattr(self._sessions[0], 'default_model', None)
|
self.default_model = getattr(self._sessions[0], 'default_model', None)
|
||||||
|
self._cur_idx, self._switched_at = 0, 0.0
|
||||||
def __getattr__(self, name): return getattr(self._sessions[0], name)
|
def __getattr__(self, name): return getattr(self._sessions[0], name)
|
||||||
@property
|
@property
|
||||||
def primary(self): return self._sessions[0]
|
def primary(self): return self._sessions[0]
|
||||||
|
def _pick(self):
|
||||||
|
if self._cur_idx and time.time() - self._switched_at > self._spring_sec: self._cur_idx = 0
|
||||||
|
return self._cur_idx
|
||||||
def _raw_ask(self, *args, **kwargs):
|
def _raw_ask(self, *args, **kwargs):
|
||||||
|
base, n = self._pick(), len(self._sessions)
|
||||||
for attempt in range(self._retries + 1):
|
for attempt in range(self._retries + 1):
|
||||||
gen = self._orig_raw_asks[attempt % len(self._sessions)](*args, **kwargs)
|
idx = (base + attempt) % n
|
||||||
|
gen = self._orig_raw_asks[idx](*args, **kwargs)
|
||||||
last_chunk, return_val, yielded = None, [], False
|
last_chunk, return_val, yielded = None, [], False
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
@@ -823,11 +830,19 @@ class MixinSession:
|
|||||||
if not yielded and isinstance(chunk, str) and chunk.startswith('Error:'): continue
|
if not yielded and isinstance(chunk, str) and chunk.startswith('Error:'): continue
|
||||||
yield chunk; yielded = True
|
yield chunk; yielded = True
|
||||||
except StopIteration as e: return_val = e.value or []
|
except StopIteration as e: return_val = e.value or []
|
||||||
if isinstance(last_chunk, str) and last_chunk.startswith('Error:') and attempt < self._retries:
|
is_err = isinstance(last_chunk, str) and last_chunk.startswith('Error:')
|
||||||
delay = min(30, self._base_delay * (2 ** attempt))
|
if not is_err:
|
||||||
print(f'[MixinSession] {last_chunk[:80]}, retry {attempt+1}/{self._retries} in {delay:.1f}s')
|
if attempt > 0: self._cur_idx = idx; self._switched_at = time.time()
|
||||||
time.sleep(delay); continue
|
return return_val
|
||||||
return return_val
|
if attempt >= self._retries:
|
||||||
|
yield last_chunk; return return_val
|
||||||
|
nxt = (base + attempt + 1) % n
|
||||||
|
if nxt == base: # full round failed, delay before next
|
||||||
|
rnd = (attempt + 1) // n
|
||||||
|
delay = min(30, self._base_delay * (2 ** rnd))
|
||||||
|
print(f'[MixinSession] {last_chunk[:80]}, round {rnd} exhausted, retry in {delay:.1f}s')
|
||||||
|
time.sleep(delay)
|
||||||
|
else: print(f'[MixinSession] {last_chunk[:80]}, retry {attempt+1}/{self._retries} (s{idx}→s{nxt})')
|
||||||
|
|
||||||
class NativeToolClient:
|
class NativeToolClient:
|
||||||
THINKING_PROMPT = """
|
THINKING_PROMPT = """
|
||||||
|
|||||||
Reference in New Issue
Block a user