feat: fold_turns UI折叠 + 放宽输出限制 + 压缩history/key_info标签 + context_win调大
This commit is contained in:
@@ -66,6 +66,23 @@ def render_sidebar():
|
||||
st.caption("🔴 自主行动已停止")
|
||||
with st.sidebar: render_sidebar()
|
||||
|
||||
def fold_turns(text):
|
||||
parts = re.split(r'(\**LLM Running \(Turn \d+\) \.\.\.*\**)', text)
|
||||
if len(parts) < 4: return text
|
||||
result = parts[0]
|
||||
turns = []
|
||||
for i in range(1, len(parts), 2):
|
||||
marker = parts[i].strip('*')
|
||||
content = parts[i+1] if i+1 < len(parts) else ''
|
||||
turns.append((marker, content))
|
||||
for idx, (marker, content) in enumerate(turns):
|
||||
if idx < len(turns) - 1:
|
||||
m = re.search(r'<summary>\s*(.*?)\|*</summary>', content, re.DOTALL)
|
||||
title = m.group(1).strip() if m else marker
|
||||
result += f'<details><summary>{title}</summary>\n\n{content}\n</details>\n\n'
|
||||
else:
|
||||
result += marker + content
|
||||
return result
|
||||
|
||||
def agent_backend_stream(prompt):
|
||||
display_queue = agent.put_task(prompt, source="user")
|
||||
@@ -84,7 +101,11 @@ def agent_backend_stream(prompt):
|
||||
|
||||
if "messages" not in st.session_state: st.session_state.messages = []
|
||||
for msg in st.session_state.messages:
|
||||
with st.chat_message(msg["role"]): st.markdown(msg["content"], unsafe_allow_html=False)
|
||||
with st.chat_message(msg["role"]):
|
||||
if msg["role"] == "assistant":
|
||||
st.markdown(fold_turns(msg["content"]), unsafe_allow_html=True)
|
||||
else:
|
||||
st.markdown(msg["content"], unsafe_allow_html=False)
|
||||
|
||||
# IME composition fix (macOS only) - prevents Enter from submitting during CJK input
|
||||
if os.name != 'nt':
|
||||
@@ -99,8 +120,8 @@ if prompt := st.chat_input("请输入指令"):
|
||||
message_placeholder = st.empty()
|
||||
response = ''
|
||||
for response in agent_backend_stream(prompt):
|
||||
message_placeholder.markdown(response + "▌", unsafe_allow_html=False)
|
||||
message_placeholder.markdown(response, unsafe_allow_html=False)
|
||||
message_placeholder.markdown(fold_turns(response) + "...", unsafe_allow_html=True)
|
||||
message_placeholder.markdown(fold_turns(response), unsafe_allow_html=True)
|
||||
st.session_state.messages.append({"role": "assistant", "content": response})
|
||||
st.session_state.last_reply_time = int(time.time())
|
||||
|
||||
|
||||
8
ga.py
8
ga.py
@@ -73,12 +73,12 @@ def code_run(code, code_type="python", timeout=60, cwd=None, code_cwd=None, stop
|
||||
status = "success" if exit_code == 0 else "error"
|
||||
status_icon = "✅" if exit_code == 0 else "❌"
|
||||
if exit_code is None: status_icon = "⏳"
|
||||
output_snippet = smart_format(stdout_str, max_str_len=600, omit_str='\n[omitted long output]\n')
|
||||
output_snippet = smart_format(stdout_str, max_str_len=600, omit_str='\n\n[omitted long output]\n\n')
|
||||
yield f"[Status] {status_icon} Exit Code: {exit_code}\n[Stdout]\n{output_snippet}\n"
|
||||
if process.stdout: threading.Thread(target=process.stdout.close, daemon=True).start()
|
||||
return {
|
||||
"status": status,
|
||||
"stdout": smart_format(stdout_str, max_str_len=8000, omit_str='\n[omitted long output]\n'),
|
||||
"stdout": smart_format(stdout_str, max_str_len=10000, omit_str='\n\n[omitted long output]\n\n'),
|
||||
"exit_code": exit_code
|
||||
}
|
||||
except Exception as e:
|
||||
@@ -137,7 +137,7 @@ def web_scan(tabs_only=False, switch_tab_id=None, text_only=False):
|
||||
"active_tab": driver.default_session_id
|
||||
}
|
||||
}
|
||||
if not tabs_only: result["content"] = get_html(driver, cutlist=True, maxchars=28000, text_only=text_only)
|
||||
if not tabs_only: result["content"] = get_html(driver, cutlist=True, maxchars=38000, text_only=text_only)
|
||||
return result
|
||||
except Exception as e:
|
||||
return {"status": "error", "msg": format_error(e)}
|
||||
@@ -354,7 +354,7 @@ class GenericAgentHandler(BaseHandler):
|
||||
except: pass
|
||||
yield f"JS 执行结果:\n{smart_format(result)}\n"
|
||||
next_prompt = self._get_anchor_prompt(skip=args.get('_index', 0) > 0)
|
||||
return StepOutcome(smart_format(result, max_str_len=5000), next_prompt=next_prompt)
|
||||
return StepOutcome(smart_format(result, max_str_len=8000), next_prompt=next_prompt)
|
||||
|
||||
def do_file_patch(self, args, response):
|
||||
path = self._get_abs_path(args.get("path", ""))
|
||||
|
||||
25
llmcore.py
25
llmcore.py
@@ -15,32 +15,31 @@ proxy = mykeys.get("proxy", 'http://127.0.0.1:2082')
|
||||
proxies = {"http": proxy, "https": proxy} if proxy else None
|
||||
|
||||
def compress_history_tags(messages, keep_recent=10, max_len=800):
|
||||
"""Compress <thinking>/<tool_use>/<tool_result> tags in older messages to save tokens.
|
||||
Supports both prompt-style (ClaudeSession/LLMSession) and content-style (NativeClaudeSession) messages."""
|
||||
"""Compress <thinking>/<tool_use>/<tool_result> tags in older messages to save tokens."""
|
||||
compress_history_tags._cd = getattr(compress_history_tags, '_cd', 0) + 1
|
||||
if compress_history_tags._cd % 5 != 0: return messages
|
||||
_before = sum(len(json.dumps(m, ensure_ascii=False)) for m in messages)
|
||||
_pats = {tag: re.compile(rf'(<{tag}>)([\s\S]*?)(</{tag}>)') for tag in ('thinking', 'think', 'tool_use', 'tool_result')}
|
||||
_hist_pat = re.compile(r'<(history|key_info)>[\s\S]*?</\1>')
|
||||
def _trunc(text):
|
||||
text = _hist_pat.sub(lambda m: f'<{m.group(1)}>[...]</{m.group(1)}>', text)
|
||||
for pat in _pats.values(): text = pat.sub(lambda m: m.group(1) + m.group(2)[:max_len] + '...' + m.group(3) if len(m.group(2)) > max_len else m.group(0), text)
|
||||
return text
|
||||
for i, msg in enumerate(messages):
|
||||
if i >= len(messages) - keep_recent: break
|
||||
if 'prompt' in msg: msg['prompt'] = _trunc(msg['prompt'])
|
||||
elif 'content' in msg and 'prompt' not in msg:
|
||||
c = msg['content']
|
||||
if isinstance(c, str): msg['content'] = _trunc(c)
|
||||
elif isinstance(c, list):
|
||||
for block in c:
|
||||
if isinstance(block, dict) and block.get('type') == 'text' and isinstance(block.get('text'), str):
|
||||
block['text'] = _trunc(block['text'])
|
||||
c = msg['content']
|
||||
if isinstance(c, str): msg['content'] = _trunc(c)
|
||||
elif isinstance(c, list):
|
||||
for block in c:
|
||||
if isinstance(block, dict) and block.get('type') == 'text' and isinstance(block.get('text'), str):
|
||||
block['text'] = _trunc(block['text'])
|
||||
print(f"[Cut] {_before} -> {sum(len(json.dumps(m, ensure_ascii=False)) for m in messages)}")
|
||||
return messages
|
||||
|
||||
def _sanitize_leading_user_msg(msg):
|
||||
"""把 user 消息里的 tool_result 块改写成纯文本,避免孤立引用。
|
||||
history 统一使用 Claude content-block 格式:content 是 list of blocks。"""
|
||||
msg = dict(msg) # 浅拷贝外层 dict;content 在 L56 整体替换而非原地修改,故原对象的 content 不受影响
|
||||
msg = dict(msg) # 浅拷贝外层 dict
|
||||
content = msg.get('content')
|
||||
if not isinstance(content, list): return msg
|
||||
texts = []
|
||||
@@ -411,7 +410,7 @@ class BaseSession:
|
||||
self.api_key = cfg['apikey']
|
||||
self.api_base = cfg['apibase'].rstrip('/')
|
||||
self.default_model = cfg.get('model', '')
|
||||
self.context_win = cfg.get('context_win', 20000)
|
||||
self.context_win = cfg.get('context_win', 24000)
|
||||
self.history = []
|
||||
self.lock = threading.Lock()
|
||||
self.system = ""
|
||||
@@ -481,7 +480,7 @@ class LLMSession(BaseSession):
|
||||
class NativeClaudeSession(BaseSession):
|
||||
def __init__(self, cfg):
|
||||
super().__init__(cfg)
|
||||
self.context_win = cfg.get("context_win", 24000)
|
||||
self.context_win = cfg.get("context_win", 28000)
|
||||
self.no_system_prompt = cfg.get("no_system_prompt", False)
|
||||
|
||||
def raw_ask(self, messages, tools=None, system=None, model=None, temperature=0.5, max_tokens=6144):
|
||||
|
||||
Reference in New Issue
Block a user