v0.1.4-p3
parent
7d747e6fac
commit
34988b2436
|
|
@ -1558,6 +1558,17 @@ def _clear_bot_sessions(bot_id: str) -> int:
|
||||||
return deleted
|
return deleted
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_bot_dashboard_direct_session(bot_id: str) -> Dict[str, Any]:
|
||||||
|
"""Truncate the dashboard:direct session file while preserving the workspace session root."""
|
||||||
|
root = _sessions_root(bot_id)
|
||||||
|
os.makedirs(root, exist_ok=True)
|
||||||
|
path = os.path.join(root, "dashboard_direct.jsonl")
|
||||||
|
existed = os.path.exists(path)
|
||||||
|
with open(path, "w", encoding="utf-8"):
|
||||||
|
pass
|
||||||
|
return {"path": path, "existed": existed}
|
||||||
|
|
||||||
|
|
||||||
def _read_env_store(bot_id: str) -> Dict[str, str]:
|
def _read_env_store(bot_id: str) -> Dict[str, str]:
|
||||||
path = _env_store_path(bot_id)
|
path = _env_store_path(bot_id)
|
||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
|
|
@ -3021,6 +3032,34 @@ def clear_bot_messages(bot_id: str, session: Session = Depends(get_session)):
|
||||||
return {"bot_id": bot_id, "deleted": deleted, "cleared_sessions": cleared_sessions}
|
return {"bot_id": bot_id, "deleted": deleted, "cleared_sessions": cleared_sessions}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/bots/{bot_id}/sessions/dashboard-direct/clear")
|
||||||
|
def clear_bot_dashboard_direct_session(bot_id: str, session: Session = Depends(get_session)):
|
||||||
|
bot = session.get(BotInstance, bot_id)
|
||||||
|
if not bot:
|
||||||
|
raise HTTPException(status_code=404, detail="Bot not found")
|
||||||
|
|
||||||
|
result = _clear_bot_dashboard_direct_session(bot_id)
|
||||||
|
if str(bot.docker_status or "").upper() == "RUNNING":
|
||||||
|
try:
|
||||||
|
docker_manager.send_command(bot_id, "/new")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
bot.updated_at = datetime.utcnow()
|
||||||
|
session.add(bot)
|
||||||
|
record_activity_event(
|
||||||
|
session,
|
||||||
|
bot_id,
|
||||||
|
"dashboard_session_cleared",
|
||||||
|
channel="dashboard",
|
||||||
|
detail="Cleared dashboard_direct session file",
|
||||||
|
metadata={"session_file": result["path"], "previously_existed": result["existed"]},
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
_invalidate_bot_detail_cache(bot_id)
|
||||||
|
return {"bot_id": bot_id, "cleared": True, "session_file": result["path"], "previously_existed": result["existed"]}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/bots/{bot_id}/logs")
|
@app.get("/api/bots/{bot_id}/logs")
|
||||||
def get_bot_logs(bot_id: str, tail: int = 300, session: Session = Depends(get_session)):
|
def get_bot_logs(bot_id: str, tail: int = 300, session: Session = Depends(get_session)):
|
||||||
bot = session.get(BotInstance, bot_id)
|
bot = session.get(BotInstance, bot_id)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
"""LiteLLM provider implementation for multi-provider support."""
|
"""LiteLLM provider implementation for multi-provider support."""
|
||||||
|
|
||||||
import ast
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
import string
|
import string
|
||||||
|
|
@ -20,49 +18,12 @@ from nanobot.providers.registry import find_by_model, find_gateway
|
||||||
_ALLOWED_MSG_KEYS = frozenset({"role", "content", "tool_calls", "tool_call_id", "name", "reasoning_content"})
|
_ALLOWED_MSG_KEYS = frozenset({"role", "content", "tool_calls", "tool_call_id", "name", "reasoning_content"})
|
||||||
_ANTHROPIC_EXTRA_KEYS = frozenset({"thinking_blocks"})
|
_ANTHROPIC_EXTRA_KEYS = frozenset({"thinking_blocks"})
|
||||||
_ALNUM = string.ascii_letters + string.digits
|
_ALNUM = string.ascii_letters + string.digits
|
||||||
_ARG_PATCH_VERBOSE_VALUES = {"1", "true", "yes", "on"}
|
|
||||||
|
|
||||||
def _short_tool_id() -> str:
|
def _short_tool_id() -> str:
|
||||||
"""Generate a 9-char alphanumeric ID compatible with all providers (incl. Mistral)."""
|
"""Generate a 9-char alphanumeric ID compatible with all providers (incl. Mistral)."""
|
||||||
return "".join(secrets.choice(_ALNUM) for _ in range(9))
|
return "".join(secrets.choice(_ALNUM) for _ in range(9))
|
||||||
|
|
||||||
|
|
||||||
def _should_log_argument_patch() -> bool:
|
|
||||||
value = str(os.getenv("DASHBOARD_LITELLM_PATCH_VERBOSE") or "").strip().lower()
|
|
||||||
return value in _ARG_PATCH_VERBOSE_VALUES
|
|
||||||
|
|
||||||
|
|
||||||
def _coerce_tool_arguments_json(value: Any) -> str:
|
|
||||||
"""Return provider-safe JSON text for OpenAI-style function.arguments."""
|
|
||||||
if value is None:
|
|
||||||
return "{}"
|
|
||||||
|
|
||||||
if isinstance(value, str):
|
|
||||||
text = value.strip()
|
|
||||||
if not text:
|
|
||||||
return "{}"
|
|
||||||
try:
|
|
||||||
json.loads(text)
|
|
||||||
return text
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
parsed = ast.literal_eval(text)
|
|
||||||
except Exception:
|
|
||||||
parsed = None
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
return json.dumps(parsed, ensure_ascii=False)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return json.dumps({"raw": text}, ensure_ascii=False)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return json.dumps(value, ensure_ascii=False)
|
|
||||||
except Exception:
|
|
||||||
return json.dumps({"raw": str(value)}, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
class LiteLLMProvider(LLMProvider):
|
class LiteLLMProvider(LLMProvider):
|
||||||
"""
|
"""
|
||||||
LLM provider using LiteLLM for multi-provider support.
|
LLM provider using LiteLLM for multi-provider support.
|
||||||
|
|
@ -222,7 +183,6 @@ class LiteLLMProvider(LLMProvider):
|
||||||
allowed = _ALLOWED_MSG_KEYS | extra_keys
|
allowed = _ALLOWED_MSG_KEYS | extra_keys
|
||||||
sanitized = LLMProvider._sanitize_request_messages(messages, allowed)
|
sanitized = LLMProvider._sanitize_request_messages(messages, allowed)
|
||||||
id_map: dict[str, str] = {}
|
id_map: dict[str, str] = {}
|
||||||
patched_arguments = 0
|
|
||||||
|
|
||||||
def map_id(value: Any) -> Any:
|
def map_id(value: Any) -> Any:
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
|
|
@ -240,26 +200,11 @@ class LiteLLMProvider(LLMProvider):
|
||||||
continue
|
continue
|
||||||
tc_clean = dict(tc)
|
tc_clean = dict(tc)
|
||||||
tc_clean["id"] = map_id(tc_clean.get("id"))
|
tc_clean["id"] = map_id(tc_clean.get("id"))
|
||||||
function = tc_clean.get("function")
|
|
||||||
if isinstance(function, dict) and "arguments" in function:
|
|
||||||
function_clean = dict(function)
|
|
||||||
original_arguments = function_clean.get("arguments")
|
|
||||||
normalized_arguments = _coerce_tool_arguments_json(original_arguments)
|
|
||||||
if original_arguments != normalized_arguments:
|
|
||||||
patched_arguments += 1
|
|
||||||
function_clean["arguments"] = normalized_arguments
|
|
||||||
tc_clean["function"] = function_clean
|
|
||||||
normalized_tool_calls.append(tc_clean)
|
normalized_tool_calls.append(tc_clean)
|
||||||
clean["tool_calls"] = normalized_tool_calls
|
clean["tool_calls"] = normalized_tool_calls
|
||||||
|
|
||||||
if "tool_call_id" in clean and clean["tool_call_id"]:
|
if "tool_call_id" in clean and clean["tool_call_id"]:
|
||||||
clean["tool_call_id"] = map_id(clean["tool_call_id"])
|
clean["tool_call_id"] = map_id(clean["tool_call_id"])
|
||||||
|
|
||||||
if patched_arguments and _should_log_argument_patch():
|
|
||||||
logger.info(
|
|
||||||
"Normalized {} historical tool/function argument payload(s) to JSON strings for LiteLLM",
|
|
||||||
patched_arguments,
|
|
||||||
)
|
|
||||||
return sanitized
|
return sanitized
|
||||||
|
|
||||||
async def chat(
|
async def chat(
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Cpu,
|
Cpu,
|
||||||
|
Eraser,
|
||||||
Eye,
|
Eye,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
FileText,
|
FileText,
|
||||||
|
|
@ -152,6 +153,7 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
|
||||||
const [usageLoading, setUsageLoading] = useState(false);
|
const [usageLoading, setUsageLoading] = useState(false);
|
||||||
const [usagePage, setUsagePage] = useState(1);
|
const [usagePage, setUsagePage] = useState(1);
|
||||||
const [usagePageSize, setUsagePageSize] = useState(() => readCachedPlatformPageSize(10));
|
const [usagePageSize, setUsagePageSize] = useState(() => readCachedPlatformPageSize(10));
|
||||||
|
const [pageSizeReady, setPageSizeReady] = useState(() => readCachedPlatformPageSize(0) > 0);
|
||||||
const [botListPage, setBotListPage] = useState(1);
|
const [botListPage, setBotListPage] = useState(1);
|
||||||
const [botListPageSize, setBotListPageSize] = useState(() => readCachedPlatformPageSize(10));
|
const [botListPageSize, setBotListPageSize] = useState(() => readCachedPlatformPageSize(10));
|
||||||
const [showCompactBotSheet, setShowCompactBotSheet] = useState(false);
|
const [showCompactBotSheet, setShowCompactBotSheet] = useState(false);
|
||||||
|
|
@ -203,6 +205,7 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify(error?.response?.data?.detail || (isZh ? '读取平台总览失败。' : 'Failed to load platform overview.'), { tone: 'error' });
|
notify(error?.response?.data?.detail || (isZh ? '读取平台总览失败。' : 'Failed to load platform overview.'), { tone: 'error' });
|
||||||
} finally {
|
} finally {
|
||||||
|
setPageSizeReady(true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -250,8 +253,9 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!pageSizeReady) return;
|
||||||
void loadUsage(1);
|
void loadUsage(1);
|
||||||
}, [usagePageSize]);
|
}, [pageSizeReady, usagePageSize]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBotListPage(1);
|
setBotListPage(1);
|
||||||
|
|
@ -366,6 +370,31 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearDashboardDirectSession = async (bot: BotState) => {
|
||||||
|
const targetId = String(bot.id || '').trim();
|
||||||
|
if (!targetId) return;
|
||||||
|
const ok = await confirm({
|
||||||
|
title: isZh ? '清除面板 Session' : 'Clear Dashboard Session',
|
||||||
|
message: isZh
|
||||||
|
? `确认清空 Bot ${targetId} 的 dashboard_direct.jsonl 内容?\n\n这会重置面板对话上下文;若 Bot 正在运行,还会同步切到新会话。`
|
||||||
|
: `Clear dashboard_direct.jsonl for Bot ${targetId}?\n\nThis resets the dashboard conversation context. If the bot is running, it will also switch to a fresh session.`,
|
||||||
|
tone: 'warning',
|
||||||
|
confirmText: isZh ? '清除' : 'Clear',
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
setOperatingBotId(targetId);
|
||||||
|
try {
|
||||||
|
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(targetId)}/sessions/dashboard-direct/clear`);
|
||||||
|
notify(isZh ? '面板 Session 已清空。' : 'Dashboard session cleared.', { tone: 'success' });
|
||||||
|
await refreshAll();
|
||||||
|
} catch (error: any) {
|
||||||
|
notify(error?.response?.data?.detail || (isZh ? '清空面板 Session 失败。' : 'Failed to clear dashboard session.'), { tone: 'error' });
|
||||||
|
} finally {
|
||||||
|
setOperatingBotId('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadResourceSnapshot = async (botId: string) => {
|
const loadResourceSnapshot = async (botId: string) => {
|
||||||
if (!botId) return;
|
if (!botId) return;
|
||||||
setResourceLoading(true);
|
setResourceLoading(true);
|
||||||
|
|
@ -391,8 +420,8 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
|
||||||
const overviewImages = overview?.summary.images;
|
const overviewImages = overview?.summary.images;
|
||||||
const overviewResources = overview?.summary.resources;
|
const overviewResources = overview?.summary.resources;
|
||||||
const usageSummary = usageData?.summary || overview?.usage.summary;
|
const usageSummary = usageData?.summary || overview?.usage.summary;
|
||||||
const usageItems = usageData?.items || overview?.usage.items || [];
|
const usageItems = pageSizeReady ? usageData?.items || [] : [];
|
||||||
const usageTotal = usageData?.total || 0;
|
const usageTotal = pageSizeReady ? usageData?.total || 0 : 0;
|
||||||
const usagePageCount = Math.max(1, Math.ceil(usageTotal / usagePageSize));
|
const usagePageCount = Math.max(1, Math.ceil(usageTotal / usagePageSize));
|
||||||
const selectedBotInfo = selectedBotDetail && selectedBotDetail.id === selectedBotId ? { ...selectedBot, ...selectedBotDetail } : selectedBot;
|
const selectedBotInfo = selectedBotDetail && selectedBotDetail.id === selectedBotId ? { ...selectedBot, ...selectedBotDetail } : selectedBot;
|
||||||
const lastActionPreview = selectedBotInfo?.last_action?.trim() || '';
|
const lastActionPreview = selectedBotInfo?.last_action?.trim() || '';
|
||||||
|
|
@ -502,6 +531,16 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
|
||||||
>
|
>
|
||||||
<Gauge size={14} />
|
<Gauge size={14} />
|
||||||
</LucentIconButton>
|
</LucentIconButton>
|
||||||
|
<LucentIconButton
|
||||||
|
className="btn btn-secondary btn-sm icon-btn"
|
||||||
|
type="button"
|
||||||
|
disabled={operatingBotId === selectedBotInfo.id}
|
||||||
|
onClick={() => void clearDashboardDirectSession(selectedBotInfo)}
|
||||||
|
tooltip={isZh ? '清除面板 Session' : 'Clear Dashboard Session'}
|
||||||
|
aria-label={isZh ? '清除面板 Session' : 'Clear Dashboard Session'}
|
||||||
|
>
|
||||||
|
<Eraser size={14} />
|
||||||
|
</LucentIconButton>
|
||||||
<LucentIconButton
|
<LucentIconButton
|
||||||
className="btn btn-danger btn-sm icon-btn"
|
className="btn btn-danger btn-sm icon-btn"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -622,6 +661,9 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
|
||||||
<span>{isZh ? '总计' : 'Total'}</span>
|
<span>{isZh ? '总计' : 'Total'}</span>
|
||||||
<span>{isZh ? '时间 / 来源' : 'Time / Source'}</span>
|
<span>{isZh ? '时间 / 来源' : 'Time / Source'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{!pageSizeReady ? (
|
||||||
|
<div className="ops-empty-inline">{isZh ? '正在同步分页设置...' : 'Syncing page size...'}</div>
|
||||||
|
) : null}
|
||||||
{usageItems.map((item) => (
|
{usageItems.map((item) => (
|
||||||
<div key={item.id} className="platform-usage-row">
|
<div key={item.id} className="platform-usage-row">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -644,10 +686,11 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{!usageLoading && usageItems.length === 0 ? (
|
{pageSizeReady && !usageLoading && usageItems.length === 0 ? (
|
||||||
<div className="ops-empty-inline">{isZh ? '暂无请求用量数据。' : 'No usage records yet.'}</div>
|
<div className="ops-empty-inline">{isZh ? '暂无请求用量数据。' : 'No usage records yet.'}</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
{pageSizeReady ? (
|
||||||
<div className="platform-usage-pager">
|
<div className="platform-usage-pager">
|
||||||
<span className="pager-status">{isZh ? `第 ${usagePage} / ${usagePageCount} 页,共 ${usageTotal} 条` : `Page ${usagePage} / ${usagePageCount}, ${usageTotal} rows`}</span>
|
<span className="pager-status">{isZh ? `第 ${usagePage} / ${usagePageCount} 页,共 ${usageTotal} 条` : `Page ${usagePage} / ${usagePageCount}, ${usageTotal} rows`}</span>
|
||||||
<div className="platform-usage-pager-actions">
|
<div className="platform-usage-pager-actions">
|
||||||
|
|
@ -673,6 +716,7 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
|
||||||
</LucentIconButton>
|
</LucentIconButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -720,7 +764,10 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="list-scroll platform-bot-list-scroll">
|
<div className="list-scroll platform-bot-list-scroll">
|
||||||
{pagedBots.map((bot) => {
|
{!pageSizeReady ? (
|
||||||
|
<div className="ops-bot-list-empty">{isZh ? '正在同步分页设置...' : 'Syncing page size...'}</div>
|
||||||
|
) : null}
|
||||||
|
{pageSizeReady ? pagedBots.map((bot) => {
|
||||||
const enabled = bot.enabled !== false;
|
const enabled = bot.enabled !== false;
|
||||||
const running = bot.docker_status === 'RUNNING';
|
const running = bot.docker_status === 'RUNNING';
|
||||||
const selected = bot.id === selectedBotId;
|
const selected = bot.id === selectedBotId;
|
||||||
|
|
@ -772,11 +819,12 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
}) : null}
|
||||||
{filteredBots.length === 0 ? (
|
{pageSizeReady && filteredBots.length === 0 ? (
|
||||||
<div className="ops-bot-list-empty">{isZh ? '暂无 Bot,或当前搜索没有结果。' : 'No bots found, or the current search returned no results.'}</div>
|
<div className="ops-bot-list-empty">{isZh ? '暂无 Bot,或当前搜索没有结果。' : 'No bots found, or the current search returned no results.'}</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
{pageSizeReady ? (
|
||||||
<div className="platform-usage-pager">
|
<div className="platform-usage-pager">
|
||||||
<span className="pager-status">{isZh ? `第 ${botListPage} / ${botListPageCount} 页,共 ${filteredBots.length} 个 Bot` : `Page ${botListPage} / ${botListPageCount}, ${filteredBots.length} bots`}</span>
|
<span className="pager-status">{isZh ? `第 ${botListPage} / ${botListPageCount} 页,共 ${filteredBots.length} 个 Bot` : `Page ${botListPage} / ${botListPageCount}, ${filteredBots.length} bots`}</span>
|
||||||
<div className="platform-usage-pager-actions">
|
<div className="platform-usage-pager-actions">
|
||||||
|
|
@ -802,6 +850,7 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
|
||||||
</LucentIconButton>
|
</LucentIconButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{!compactMode ? (
|
{!compactMode ? (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue