feat: improve conductor files and runtime handling
This commit is contained in:
1 parent
b913a89ace
commit
6bed654dce
6 files changed
+195
-41
No files matched your search
@@ -110,6 +110,9 @@ class GenericAgent:
|
||||
if not self.is_running: return
|
||||
print('Abort current task...')
|
||||
self.stop_sig = True
|
||||
for sess in getattr(self.llmclient.backend, '_sessions', [self.llmclient.backend]):
|
||||
try: sess.active_response.close()
|
||||
except Exception: pass
|
||||
if self.handler is not None: self.handler.code_stop_signal.append(1)
|
||||
|
||||
def put_task(self, query, source="user", images=None):
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
|
||||
.composer { padding: 14px; border-top: 1px solid rgba(31,42,68,.075); display: flex; gap: 12px;
|
||||
align-items: end; background: rgba(255,255,255,.40); }
|
||||
.composer.dragging { background: rgba(59,130,246,.13); outline: 2px dashed #3b82f6; outline-offset: -6px; }
|
||||
textarea#input { flex: 1; height: 52px; max-height: 140px; resize: none; outline: none; color: var(--text);
|
||||
background: rgba(255,255,255,.92); border: 1px solid rgba(31,42,68,.12); border-radius: 18px;
|
||||
padding: 14px 15px; font: inherit; line-height: 1.35; }
|
||||
@@ -225,6 +226,19 @@
|
||||
button { height: 52px; min-width: 92px; border: 0; border-radius: 18px; color: white; font-weight: 750;
|
||||
cursor: pointer; background: linear-gradient(135deg, #3b82f6, #10b981);
|
||||
box-shadow: 0 10px 26px rgba(59,130,246,.22); }
|
||||
.file-open, .attach { min-width: 0; padding: 0 10px; box-shadow: none; }
|
||||
.file-open { height: 28px; border-radius: 9px; }
|
||||
.attach { width: 44px; height: 52px; font-size: 18px; border-radius: 18px; }
|
||||
.file-link { color: inherit; text-decoration: underline; cursor: pointer; }
|
||||
#fileDialog { width: min(1100px, 96vw); height: min(800px, 92vh); padding: 0; border: 0; border-radius: 18px; box-shadow: 0 24px 80px #0004; }
|
||||
#fileDialog::backdrop { background: #0f172a66; backdrop-filter: blur(3px); }
|
||||
.file-bar { display: flex; align-items: center; gap: 8px; padding: 12px; border-bottom: 1px solid var(--border); }
|
||||
.file-bar code { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.file-bar button { min-width: 0; height: 34px; padding: 0 12px; border-radius: 10px; box-shadow: none; }
|
||||
#fileBody { height: calc(100% - 59px); overflow: auto; padding: 12px; }
|
||||
.file-row { display: flex; gap: 10px; padding: 9px 11px; border-radius: 9px; cursor: pointer; }
|
||||
.file-row:hover { background: #eef2ff; } .file-row span:first-child { flex: 1; } .file-size { color: var(--muted); }
|
||||
.file-preview { line-height: 1.55; } .file-preview pre { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
|
||||
/* ---------- conductor 输出(横跨底部的独立卡片,默认收起) ---------- */
|
||||
.console {
|
||||
@@ -314,18 +328,29 @@
|
||||
<section class="pane chat">
|
||||
<div class="head">
|
||||
<div class="title">Conversation <select id="llm" aria-label="Conductor model" disabled></select></div>
|
||||
<div class="hint" id="wsStatus">connecting...</div>
|
||||
<div class="hint"><button class="file-open" id="fileOpen">📁 文件</button> · <span id="wsStatus">connecting...</span></div>
|
||||
</div>
|
||||
<div class="messages" id="messages"></div>
|
||||
<div class="composer">
|
||||
<textarea id="input" placeholder="告诉 Conductor 你想做什么……"></textarea>
|
||||
<button id="send">发送</button>
|
||||
<div class="composer" id="composer">
|
||||
<textarea id="input" placeholder="告诉 Conductor 你想做什么……(可拖入文件)"></textarea>
|
||||
<button class="attach" id="fileUpload" title="上传文件">📎</button>
|
||||
<button id="send">发送</button><input id="fileInput" type="file" multiple hidden>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div><!-- /app-grid -->
|
||||
</main>
|
||||
|
||||
<dialog id="fileDialog">
|
||||
<div class="file-bar"><button id="fileBack">↑</button><code id="filePath"></code><button id="fileClose">关闭</button></div>
|
||||
<div id="fileBody"></div>
|
||||
</dialog>
|
||||
<dialog id="downloadDialog" style="width:min(520px,90vw);border:0;border-radius:16px;padding:22px;box-shadow:0 24px 80px #0004">
|
||||
<strong>下载文件</strong>
|
||||
<code id="downloadPath" style="display:block;margin:16px 0;overflow-wrap:anywhere;color:var(--muted)"></code>
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px"><button id="downloadCancel">取消</button><button id="downloadConfirm">下载</button></div>
|
||||
</dialog>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script>
|
||||
const cardsEl = document.getElementById('cards');
|
||||
@@ -341,10 +366,48 @@
|
||||
const consoleBar = document.getElementById('consoleBar');
|
||||
const consoleBody= document.getElementById('consoleBody');
|
||||
const logCount = document.getElementById('logCount');
|
||||
let ws;
|
||||
const fileDialog = document.getElementById('fileDialog');
|
||||
const fileBody = document.getElementById('fileBody');
|
||||
const filePathEl = document.getElementById('filePath');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const composer = document.getElementById('composer');
|
||||
const downloadDialog = document.getElementById('downloadDialog');
|
||||
const downloadPath = document.getElementById('downloadPath');
|
||||
let ws, fileDir='', fileParent='';
|
||||
|
||||
function esc(s){ return String(s||'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
||||
function renderMd(s){ try { return marked.parse(s||'', { breaks: true, gfm: true }); } catch(e){ return esc(s); } }
|
||||
function fileUrl(path){ return '/file?path='+encodeURIComponent(path); }
|
||||
function humanSize(n){ return n==null?'':n<1024?n+' B':n<1048576?(n/1024).toFixed(1)+' KB':(n/1048576).toFixed(1)+' MB'; }
|
||||
async function browseFiles(path=''){
|
||||
const data=await fetch(path?'/files?path='+encodeURIComponent(path):'/files').then(r=>{if(!r.ok) throw Error(r.statusText); return r.json()});
|
||||
fileDir=data.path; fileParent=data.parent; filePathEl.textContent=fileDir;
|
||||
fileBody.innerHTML=data.items.map(x=>`<div class="file-row" data-path="${esc(x.path)}" data-dir="${x.dir?'1':''}"><span>${x.dir?'📁':'📄'} ${esc(x.name)}</span><span class="file-size">${humanSize(x.size)}</span></div>`).join('')||'<div class="empty">空目录</div>';
|
||||
}
|
||||
async function openFile(path){
|
||||
if(!/\.(md|markdown|txt|py|js|ts|json|ya?ml|css|html?|xml|csv|log|ini|cfg|sh|ps1|bat|c|cc|cpp|h|hpp|java|go|rs|sql)$/i.test(path)){
|
||||
downloadPath.textContent=path; downloadDialog.showModal();
|
||||
return;
|
||||
}
|
||||
const text=await fetch(fileUrl(path)).then(r=>{if(!r.ok) throw Error(r.statusText); return r.text()});
|
||||
filePathEl.textContent=path;
|
||||
fileBody.innerHTML=/\.(md|markdown)$/i.test(path)?`<div class="file-preview">${renderMd(text)}</div>`:'';
|
||||
if(!/\.(md|markdown)$/i.test(path)){ const pre=document.createElement('pre'); pre.textContent=text; fileBody.replaceChildren(pre); }
|
||||
}
|
||||
document.getElementById('downloadCancel').onclick=()=>downloadDialog.close();
|
||||
document.getElementById('downloadConfirm').onclick=()=>{
|
||||
const a=document.createElement('a'); a.href=fileUrl(downloadPath.textContent); a.download=downloadPath.textContent.split(/[\\/]/).pop();
|
||||
document.body.appendChild(a); a.click(); a.remove(); downloadDialog.close();
|
||||
};
|
||||
async function uploadFiles(files){
|
||||
const paths=[];
|
||||
for(const file of files){
|
||||
const r=await fetch('/file?name='+encodeURIComponent(file.name),{method:'POST',body:file});
|
||||
if(!r.ok) throw Error(await r.text());
|
||||
paths.push('`'+(await r.json()).path+'`');
|
||||
}
|
||||
inputEl.value+=(inputEl.value?'\n':'')+paths.join('\n'); checkLimit(); inputEl.focus();
|
||||
}
|
||||
|
||||
/* ---------- subagent 卡片 ---------- */
|
||||
function renderCards(items){
|
||||
@@ -414,11 +477,28 @@
|
||||
div.className = `msg ${cls}` + (role==='user' && !item.read ? ' unread' : '');
|
||||
if(item.id) div.dataset.id = item.id;
|
||||
div.innerHTML = renderMd(item.msg || '');
|
||||
(item.files||[]).forEach(f=>{ const code=[...div.querySelectorAll('code')].find(x=>x.textContent===(f.ref||f.path)); if(code){ const a=document.createElement('a'); a.className='file-link'; a.href=fileUrl(f.path); a.dataset.path=f.path; code.replaceWith(a); a.append(code); } });
|
||||
messagesEl.appendChild(div);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
function markAllRead(){ messagesEl.querySelectorAll('.msg.me.unread').forEach(el=>el.classList.remove('unread')); }
|
||||
|
||||
document.getElementById('fileOpen').onclick=()=>{ fileDialog.showModal(); browseFiles().catch(e=>fileBody.textContent=e); };
|
||||
document.getElementById('fileClose').onclick=()=>fileDialog.close();
|
||||
document.getElementById('fileBack').onclick=()=>browseFiles(filePathEl.textContent===fileDir?fileParent:fileDir);
|
||||
document.getElementById('fileUpload').onclick=()=>fileInput.click();
|
||||
fileBody.addEventListener('click', e=>{ const row=e.target.closest('.file-row'); if(row) (row.dataset.dir?browseFiles:openFile)(row.dataset.path); });
|
||||
messagesEl.addEventListener('click', async e=>{
|
||||
const a=e.target.closest('.file-link'); if(!a) return;
|
||||
e.preventDefault(); fileDialog.showModal();
|
||||
const path=a.dataset.path, parent=path.replace(/[\\\/][^\\\/]*$/, '');
|
||||
try { await browseFiles(parent); await openFile(path); } catch(err) { fileBody.textContent=err; }
|
||||
});
|
||||
fileInput.onchange=async()=>{ try { await uploadFiles(fileInput.files); } catch(e) { alert('上传失败:'+e); } fileInput.value=''; };
|
||||
['dragenter','dragover'].forEach(type=>composer.addEventListener(type,e=>{ e.preventDefault(); composer.classList.add('dragging'); }));
|
||||
['dragleave','drop'].forEach(type=>composer.addEventListener(type,e=>{ e.preventDefault(); composer.classList.remove('dragging'); }));
|
||||
composer.addEventListener('drop',e=>{ if(e.dataTransfer.files.length) uploadFiles(e.dataTransfer.files).catch(err=>alert('上传失败:'+err)); });
|
||||
|
||||
/* ---------- conductor 输出 ---------- */
|
||||
function fmtTs(ms){ try{ return new Date(ms).toLocaleTimeString('zh-CN',{hour12:false}); }catch(e){ return ''; } }
|
||||
function logItemHtml(r){
|
||||
|
||||
+70
-7
@@ -1,9 +1,9 @@
|
||||
import os, sys, re, time, json, uuid, queue, asyncio, threading
|
||||
import os, sys, re, time, json, uuid, queue, asyncio, threading, argparse, base64, secrets
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Any, Optional, List
|
||||
from contextlib import asynccontextmanager, suppress
|
||||
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, HTTPException
|
||||
from fastapi.responses import FileResponse, PlainTextResponse, JSONResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
@@ -27,6 +27,15 @@ HOST = "127.0.0.1"
|
||||
PORT = 8900
|
||||
HTML_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "conductor.html")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--host", default=HOST)
|
||||
parser.add_argument("--port", type=int, default=PORT)
|
||||
parser.add_argument("--key")
|
||||
parser.add_argument("--no-browser", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.host not in ("127.0.0.1", "localhost", "::1") and not args.key:
|
||||
parser.error("--key is required for non-loopback listening")
|
||||
|
||||
|
||||
def _settings_doc() -> dict:
|
||||
try:
|
||||
@@ -112,6 +121,19 @@ async def lifespan(app: FastAPI):
|
||||
app = FastAPI(title="Conductor", lifespan=lifespan)
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
||||
|
||||
class RemoteAuth:
|
||||
def __init__(self, app): self.app = app
|
||||
async def __call__(self, scope, receive, send):
|
||||
remote = (scope.get("client") or ("",))[0]
|
||||
got = dict(scope.get("headers", [])).get(b"authorization", b"")
|
||||
want = b"Basic " + base64.b64encode(f"conductor:{args.key}".encode())
|
||||
if args.key and remote not in ("127.0.0.1", "::1") and not secrets.compare_digest(got, want):
|
||||
if scope["type"] == "websocket": return await send({"type": "websocket.close", "code": 1008})
|
||||
return await PlainTextResponse("Unauthorized", 401, {"WWW-Authenticate": 'Basic realm="Conductor"'})(scope, receive, send)
|
||||
await self.app(scope, receive, send)
|
||||
|
||||
app.add_middleware(RemoteAuth)
|
||||
|
||||
class ChatIn(BaseModel):
|
||||
msg: str
|
||||
role: str = "conductor" # conductor | system | user
|
||||
@@ -199,8 +221,23 @@ async def broadcast(payload: dict):
|
||||
|
||||
def push_cards(): schedule_broadcast({"type": "subagents", "items": pool.snapshot()})
|
||||
|
||||
FILE_ROOT = os.path.dirname(ROOT)
|
||||
FILE_HOME = os.path.join(ROOT, "temp")
|
||||
UPLOAD_DIR = os.path.join(FILE_HOME, "uploads")
|
||||
|
||||
def file_path(path: str):
|
||||
path = os.path.abspath(os.path.expanduser(path))
|
||||
if os.path.commonpath((FILE_ROOT, path)) != FILE_ROOT: raise ValueError("path is above file browser root")
|
||||
return path
|
||||
|
||||
def add_chat(msg: str, role: str = "conductor", files: list = None, images: list = None):
|
||||
item = {"id": short_id(), "role": role, "msg": msg, "ts": now_ms(), "read": role != "user", "files": files or [], "images": images or []}
|
||||
files = list(files or [])
|
||||
known = {x.get("path") for x in files}
|
||||
for ref in re.findall(r"`([^`\n]+)`", msg):
|
||||
try: path = file_path(ref)
|
||||
except ValueError: continue
|
||||
if os.path.isfile(path) and path not in known: files.append({"ref": ref, "path": path, "name": os.path.basename(path)})
|
||||
item = {"id": short_id(), "role": role, "msg": msg, "ts": now_ms(), "read": role != "user", "files": files, "images": images or []}
|
||||
chat_messages.append(item)
|
||||
if len(chat_messages) > 200: del chat_messages[:-200]
|
||||
schedule_broadcast({"type": "chat", "item": item})
|
||||
@@ -318,6 +355,9 @@ Conductor API\tBase: http://{HOST}:{PORT}
|
||||
POST /chat\tbody: {{"msg": "..."}}\t给用户发消息
|
||||
POST /subagent\tbody: {{"prompt": "..."}}\t启动新subagent,返回 {{"id": "xxx"}};指定模型加参数llm(数字/名称)
|
||||
POST /approval\tbody: {{"prompt": "...", "source": "..."}}\t推一条待批任务到前端(后端不存),用户同意则直接派发为subagent
|
||||
GET /files?path=...\t列举目录(默认temp,最高到GA上一级)
|
||||
GET /file?path=...\t下载文件
|
||||
POST /file?name=...\t上传到temp/uploads
|
||||
POST /subagent/{{id}}\tbody: {{"action": "keyinfo", "msg": "..."}}\t注入key_info(agent下轮可见)
|
||||
POST /subagent/{{id}}\tbody: {{"action": "input", "msg": "..."}}\t对subagent输入下一条指令;指定模型加参数llm(数字/名称)
|
||||
POST /subagent/{{id}}\tbody: {{"action": "stop"}}\t中断执行但保留(可继续input)
|
||||
@@ -381,7 +421,8 @@ API: {base};requests,GET /readme查用法,GET /chat读未读对话,GET /
|
||||
- 改写prompt时严禁添加用户未提及的假设、工具、前提条件。只能精炼/结构化用户原意,不能脑补,只能做很小的改写
|
||||
|
||||
原则:
|
||||
- 信任subagent足够聪明,不要写具体步骤和容易探测的信息;能自己判断的自己判断,只在真正需要用户决策时打扰。\n
|
||||
- 信任subagent足够聪明,不要写具体步骤和容易探测的信息;能自己判断的自己判断,只在真正需要用户决策时打扰。
|
||||
- 向用户交付文件时,路径用反引号包裹。\n
|
||||
需要处理:
|
||||
{summary}"""
|
||||
|
||||
@@ -492,6 +533,28 @@ def conductor_token_stats():
|
||||
@app.get("/")
|
||||
def index(): return FileResponse(HTML_PATH)
|
||||
|
||||
@app.get("/files")
|
||||
def files(path: str = FILE_HOME):
|
||||
try: path = file_path(path)
|
||||
except ValueError as e: raise HTTPException(403, str(e))
|
||||
if not os.path.isdir(path): raise HTTPException(404, "directory not found")
|
||||
items = [{"name": x.name, "path": x.path, "dir": x.is_dir(), "size": x.stat().st_size if x.is_file() else None}
|
||||
for x in sorted(os.scandir(path), key=lambda x: (not x.is_dir(), x.name.lower()))]
|
||||
return {"path": path, "parent": path if path == FILE_ROOT else os.path.dirname(path), "items": items}
|
||||
|
||||
@app.get("/file")
|
||||
def get_file(path: str):
|
||||
try: path = file_path(path)
|
||||
except ValueError as e: raise HTTPException(403, str(e))
|
||||
return FileResponse(path, filename=os.path.basename(path))
|
||||
|
||||
@app.post("/file")
|
||||
async def put_file(name: str, request: Request):
|
||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||
path = os.path.join(UPLOAD_DIR, os.path.basename(name))
|
||||
with open(path, "wb") as f: f.write(await request.body())
|
||||
return {"name": os.path.basename(path), "path": path}
|
||||
|
||||
@app.get("/readme")
|
||||
def readme(): return PlainTextResponse(READMES["api"])
|
||||
|
||||
@@ -591,7 +654,7 @@ if __name__ == "__main__":
|
||||
import uvicorn
|
||||
# bridge 自启 conductor 时传 --no-browser:不在用户浏览器里弹一个独立 conductor UI,
|
||||
# 用户从桌面版「指挥家」页直接连过来即可。手动跑 conductor.py(没带 flag)保持原行为。
|
||||
if "--no-browser" not in sys.argv:
|
||||
if not args.no_browser:
|
||||
import webbrowser, threading
|
||||
threading.Timer(1.0, lambda: webbrowser.open(f"http://{HOST}:{PORT}")).start()
|
||||
uvicorn.run("conductor:app", host=HOST, port=PORT, reload=False)
|
||||
threading.Timer(1.0, lambda: webbrowser.open(f"http://127.0.0.1:{args.port}")).start()
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
+32
-27
@@ -300,8 +300,25 @@ def _poll_main_task(max_items=256):
|
||||
return done
|
||||
|
||||
|
||||
def _render_stat_badge(is_running, target=None):
|
||||
"""Render task usage/time into one stable element, without clearing its container."""
|
||||
if 'task_start_ts' not in st.session_state or not hasattr(llmcore, 'STATS'):
|
||||
return
|
||||
end_ts = time.time() if is_running else st.session_state.get('task_end_ts', time.time())
|
||||
secs = max(0, int(end_ts - st.session_state.task_start_ts))
|
||||
stats = dict(llmcore.STATS)
|
||||
short = lambda n: f'{n / 1000:.0f}k' if n >= 1000 else str(n)
|
||||
usage = ((f"{stats['session']} │ " if stats.get('session') else '') +
|
||||
f"{short(stats['ctx'])} chars·{stats['msgs']}msgs │ "
|
||||
f"in {short(stats.get('inp', 0))} toks·cached{short(stats.get('cached', 0))}·out{short(stats.get('out', 0))} │ "
|
||||
if 'ctx' in stats else '')
|
||||
renderer = target.markdown if target is not None else st.markdown
|
||||
renderer(f'<div class="ga-stat-badge">{usage}{secs // 60}:{secs % 60:02d}</div>',
|
||||
unsafe_allow_html=True)
|
||||
|
||||
|
||||
@st.fragment(run_every=timedelta(seconds=1))
|
||||
def render_main_stream(frozen_host, live_slot, render_state):
|
||||
def render_main_stream(frozen_host, live_slot, stats_slot, render_state):
|
||||
"""Append completed turns outside the fragment; redraw only the active turn."""
|
||||
done = _poll_main_task()
|
||||
if done is not None:
|
||||
@@ -312,7 +329,7 @@ def render_main_stream(frozen_host, live_slot, render_state):
|
||||
if st.session_state.get('loop_enabled'):
|
||||
b = get_controller()
|
||||
b['obj'] = st.session_state.get('loop_prompt_input', ''); b['ready'] = False; b['job'] = b['epoch']; b['ev'].set()
|
||||
# The final response is rendered once from history on the full-app rerun.
|
||||
# The final response and frozen stats are rendered on the full-app rerun.
|
||||
st.rerun(scope="app")
|
||||
|
||||
response = st.session_state.get("partial_response", "")
|
||||
@@ -324,17 +341,18 @@ def render_main_stream(frozen_host, live_slot, render_state):
|
||||
with frozen_host:
|
||||
render_segments([segments[render_state['frozen']]])
|
||||
render_state['frozen'] += 1
|
||||
# This external slot keeps a stable delta path; only the growing final segment changes.
|
||||
# These external slots keep stable delta paths; only their contents change.
|
||||
with live_slot.container():
|
||||
render_segments([segments[-1]], suffix=" ▌")
|
||||
_render_stat_badge(is_running=True, target=stats_slot)
|
||||
|
||||
|
||||
def mount_main_stream():
|
||||
def mount_main_stream(stats_slot):
|
||||
"""Create persistent targets captured by the fragment until the next full rerun."""
|
||||
with st.chat_message("assistant"):
|
||||
frozen_host = st.container()
|
||||
live_slot = st.empty()
|
||||
render_main_stream(frozen_host, live_slot, {'frozen': 0})
|
||||
render_main_stream(frozen_host, live_slot, stats_slot, {'frozen': 0})
|
||||
|
||||
if not hasattr(agent, "_ui_messages"): agent._ui_messages = st.session_state.get("messages", [])
|
||||
if "messages" not in st.session_state: st.session_state.messages = agent._ui_messages
|
||||
@@ -380,6 +398,10 @@ _js_ime_fix = ("" if os.name == 'nt' else
|
||||
"f();new MutationObserver(f).observe(d.body,{childList:1,subtree:1})}()")
|
||||
_embed_html(f'<script>{_js_ime_fix}</script>', height=0)
|
||||
|
||||
# Fixed target shared by full-app rendering and the one-second stream fragment.
|
||||
# The badge is position:fixed, so this placeholder does not affect its visual position.
|
||||
stats_slot = st.empty()
|
||||
|
||||
_injected = st.session_state.pop('_inject_prompt', None)
|
||||
prompt = st.chat_input("any task?") or _injected
|
||||
if prompt:
|
||||
@@ -454,33 +476,16 @@ if prompt:
|
||||
if hasattr(agent, '_pet_req') and not prompt.startswith('/'): agent._pet_req('state=walk')
|
||||
with st.chat_message("user"): st.markdown(prompt)
|
||||
_start_main_task(prompt)
|
||||
mount_main_stream()
|
||||
mount_main_stream(stats_slot)
|
||||
elif st.session_state.get('display_queue') is not None:
|
||||
# No new prompt but a task is mid-flight (typically a /btw rerun) — resume drain.
|
||||
mount_main_stream()
|
||||
mount_main_stream(stats_slot)
|
||||
|
||||
# Usage/time label remains after completion; while running only its fragment updates once/sec.
|
||||
# Usage/time label remains after completion. During a task render_main_stream refreshes it.
|
||||
_has_task_stats = 'task_start_ts' in st.session_state
|
||||
_is_running = st.session_state.get('display_queue') is not None
|
||||
if _has_task_stats:
|
||||
def _render_stat_badge():
|
||||
if not hasattr(llmcore, 'STATS'): return
|
||||
end_ts = time.time() if _is_running else st.session_state.get('task_end_ts', time.time())
|
||||
secs = max(0, int(end_ts - st.session_state.task_start_ts))
|
||||
stats = dict(llmcore.STATS)
|
||||
short = lambda n: f'{n / 1000:.0f}k' if n >= 1000 else str(n)
|
||||
usage = ((f"{stats['session']} │ " if stats.get('session') else '') +
|
||||
f"{short(stats['ctx'])} chars·{stats['msgs']}msgs │ "
|
||||
f"in {short(stats.get('inp', 0))} toks·cached{short(stats.get('cached', 0))}·out{short(stats.get('out', 0))} │ "
|
||||
if 'ctx' in stats else '')
|
||||
st.markdown(f'<div class="ga-stat-badge">{usage}{secs // 60}:{secs % 60:02d}</div>',
|
||||
unsafe_allow_html=True)
|
||||
if _is_running:
|
||||
@st.fragment(run_every=timedelta(seconds=1))
|
||||
def _live_stat_badge(): _render_stat_badge()
|
||||
_live_stat_badge()
|
||||
else:
|
||||
_render_stat_badge()
|
||||
if _has_task_stats and not _is_running:
|
||||
_render_stat_badge(is_running=False, target=stats_slot)
|
||||
|
||||
if _has_task_stats or _is_running:
|
||||
st.markdown(
|
||||
|
||||
@@ -312,7 +312,7 @@ class GenericAgentHandler(BaseHandler):
|
||||
cwd = os.path.normpath(os.path.abspath(raw_path))
|
||||
code_cwd = os.path.normpath(self.cwd)
|
||||
maxlen = self._get_tool_maxlen(10000, args)
|
||||
if timeout > 600: result = '[ERROR] Timeout must be <= 600 seconds, run time-consuming code in background subprocess and monitor it!'
|
||||
if timeout > 600: result = '[ERROR] Timeout must be <= 600 seconds; code not executed. Run time-consuming code in the background instead of waiting for it to finish in the foreground, verify it started successfully, and monitor it until completion or failure.'
|
||||
elif code_type == 'python' and _arg(args, "inline_eval", False, bool):
|
||||
ns = {'handler':self, 'parent':self.parent, 'history':json.dumps(self.parent.llmclient.backend.history)}
|
||||
old_cwd = os.getcwd()
|
||||
|
||||
+4
-1
@@ -174,7 +174,9 @@ def _parse_claude_sse(resp_lines):
|
||||
if current_block and current_block.get("type") == "text": current_block["text"] += text
|
||||
if text: yield text
|
||||
elif delta.get("type") == "thinking_delta":
|
||||
if current_block and current_block.get("type") == "thinking": current_block["thinking"] += delta.get("thinking", "")
|
||||
thinking = delta.get("thinking", "")
|
||||
if current_block and current_block.get("type") == "thinking": current_block["thinking"] += thinking
|
||||
if thinking: yield thinking
|
||||
elif delta.get("type") == "signature_delta":
|
||||
if current_block and current_block.get("type") == "thinking":
|
||||
current_block["signature"] = current_block.get("signature", "") + delta.get("signature", "")
|
||||
@@ -396,6 +398,7 @@ def _stream_with_retry(sess, url, headers, payload, parse_fn):
|
||||
try:
|
||||
with requests.post(url, headers=headers, json=payload, stream=sess.stream,
|
||||
timeout=(sess.connect_timeout, sess.read_timeout), proxies=sess.proxies, verify=sess.verify) as r:
|
||||
sess.active_response = r
|
||||
if r.status_code >= 400:
|
||||
#pathlib.Path(__file__).parent.joinpath('temp','bad_requests.json').write_text(json.dumps({"url":url,"headers":headers,"payload":payload,"t":time.time()},ensure_ascii=False),encoding='utf-8')
|
||||
d = _delay(r, attempt) if r.status_code in _RETRYABLE and attempt < sess.max_retries else None
|
||||
|
||||
Reference in new issue
Block a user