fix(memhub): clear pre-existing dev lint errors and flaky SDK dedup tests
Three things blocking CI that came in with dev and are unrelated to the sedimentation switch itself: - biome errors in DebugLogPanel.tsx (import order) and ModelUsageView.tsx (refreshToken is an intentional refresh trigger, annotated like the existing modelVersion trigger in R6Settings). - test_extractor_prompt_override.py's fake LLM now accepts keyword args the real pipeline passes since the usage audit landed (audit_stage). - the fake embeddings in the intention dedup tests used the process-random hash(), so two different texts collided roughly one run in 32 and were misread as duplicates; switched to a deterministic md5 index.
This commit is contained in:
1 parent
0822e7137e
commit
887583b858
6 files changed
+8891
-132
No files matched your search
@@ -26,11 +26,11 @@ import type {
|
||||
DebugLogLevel,
|
||||
DebugLogSnapshot,
|
||||
} from "./debug-log-types";
|
||||
import { ModelUsageView } from "./ModelUsageView";
|
||||
import {
|
||||
rendererDebugLogs,
|
||||
subscribeRendererDebugLogs,
|
||||
} from "./renderer-debug-log";
|
||||
import { ModelUsageView } from "./ModelUsageView";
|
||||
import "./debug-log-panel.css";
|
||||
|
||||
const LEVELS: Array<{ value: DebugLogLevel | "all"; label: string }> = [
|
||||
@@ -201,7 +201,11 @@ export function DebugLogPanel() {
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="debug-panel-tabs" role="tablist" aria-label="诊断页面">
|
||||
<div
|
||||
className="debug-panel-tabs"
|
||||
role="tablist"
|
||||
aria-label="诊断页面"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
@@ -222,120 +226,125 @@ export function DebugLogPanel() {
|
||||
{tab === "logs" ? (
|
||||
<div className="debug-log-content">
|
||||
<div className="debug-log-toolbar">
|
||||
<label className="debug-search">
|
||||
<Search size={15} aria-hidden="true" />
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="搜索事件、组件、错误码或内容"
|
||||
aria-label="搜索日志"
|
||||
/>
|
||||
</label>
|
||||
<select
|
||||
aria-label="日志来源"
|
||||
value={service}
|
||||
onChange={(event) =>
|
||||
setService(event.target.value as typeof service)
|
||||
}
|
||||
>
|
||||
<option value="all">全部来源</option>
|
||||
{Object.entries(SERVICE_LABELS).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<label className="debug-capture-level">
|
||||
<span>采集级别</span>
|
||||
<select
|
||||
aria-label="采集级别"
|
||||
value={captureLevel}
|
||||
disabled={capturePending}
|
||||
onChange={(event) => {
|
||||
const next = event.target.value as DiagnosticLogLevel;
|
||||
setCapturePending(true);
|
||||
void setDiagnosticLogLevel(next)
|
||||
.then(setCaptureLevel)
|
||||
.then(() => refresh())
|
||||
.catch(() => setReadState("error"))
|
||||
.finally(() => setCapturePending(false));
|
||||
}}
|
||||
>
|
||||
<option value="error">Error</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="debug">Debug</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<fieldset className="debug-levels">
|
||||
<legend>显示级别</legend>
|
||||
{LEVELS.map((option) => (
|
||||
<button
|
||||
type="button"
|
||||
key={option.value}
|
||||
aria-pressed={level === option.value}
|
||||
onClick={() => setLevel(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</fieldset>
|
||||
<div className="debug-log-status" aria-live="polite">
|
||||
<span>{filtered.length} 条匹配</span>
|
||||
<span>{live ? "实时刷新" : "已暂停"}</span>
|
||||
{snapshot?.truncated ? <span>日志过多,已截断</span> : null}
|
||||
{readState === "error" ? (
|
||||
<span className="debug-read-error">读取失败</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className="debug-log-list"
|
||||
ref={listRef}
|
||||
onScroll={handleListScroll}
|
||||
>
|
||||
{filtered.length ? (
|
||||
filtered.map((entry) => {
|
||||
const isExpanded = expanded.has(entry.id);
|
||||
return (
|
||||
<article
|
||||
className={`debug-log-row level-${entry.level}`}
|
||||
key={entry.id}
|
||||
<label className="debug-search">
|
||||
<Search size={15} aria-hidden="true" />
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="搜索事件、组件、错误码或内容"
|
||||
aria-label="搜索日志"
|
||||
/>
|
||||
</label>
|
||||
<select
|
||||
aria-label="日志来源"
|
||||
value={service}
|
||||
onChange={(event) =>
|
||||
setService(event.target.value as typeof service)
|
||||
}
|
||||
>
|
||||
<option value="all">全部来源</option>
|
||||
{Object.entries(SERVICE_LABELS).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<label className="debug-capture-level">
|
||||
<span>采集级别</span>
|
||||
<select
|
||||
aria-label="采集级别"
|
||||
value={captureLevel}
|
||||
disabled={capturePending}
|
||||
onChange={(event) => {
|
||||
const next = event.target.value as DiagnosticLogLevel;
|
||||
setCapturePending(true);
|
||||
void setDiagnosticLogLevel(next)
|
||||
.then(setCaptureLevel)
|
||||
.then(() => refresh())
|
||||
.catch(() => setReadState("error"))
|
||||
.finally(() => setCapturePending(false));
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="debug-log-summary"
|
||||
onClick={() => toggleExpanded(entry.id)}
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={14} />
|
||||
) : (
|
||||
<ChevronRight size={14} />
|
||||
)}
|
||||
<time>{logTime(entry.timestamp)}</time>
|
||||
<span className="debug-log-level">{entry.level}</span>
|
||||
<span className="debug-log-service">
|
||||
{SERVICE_LABELS[entry.service]}
|
||||
</span>
|
||||
<span className="debug-log-event">
|
||||
{entry.message || `${entry.component} · ${entry.event}`}
|
||||
</span>
|
||||
<span className="debug-log-outcome">{entry.outcome}</span>
|
||||
</button>
|
||||
{isExpanded ? (
|
||||
<pre>{JSON.stringify(entry, null, 2)}</pre>
|
||||
) : null}
|
||||
</article>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="debug-log-empty">
|
||||
{readState === "loading" ? "正在读取日志…" : "没有匹配的日志"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<option value="error">Error</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="debug">Debug</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<fieldset className="debug-levels">
|
||||
<legend>显示级别</legend>
|
||||
{LEVELS.map((option) => (
|
||||
<button
|
||||
type="button"
|
||||
key={option.value}
|
||||
aria-pressed={level === option.value}
|
||||
onClick={() => setLevel(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</fieldset>
|
||||
<div className="debug-log-status" aria-live="polite">
|
||||
<span>{filtered.length} 条匹配</span>
|
||||
<span>{live ? "实时刷新" : "已暂停"}</span>
|
||||
{snapshot?.truncated ? <span>日志过多,已截断</span> : null}
|
||||
{readState === "error" ? (
|
||||
<span className="debug-read-error">读取失败</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className="debug-log-list"
|
||||
ref={listRef}
|
||||
onScroll={handleListScroll}
|
||||
>
|
||||
{filtered.length ? (
|
||||
filtered.map((entry) => {
|
||||
const isExpanded = expanded.has(entry.id);
|
||||
return (
|
||||
<article
|
||||
className={`debug-log-row level-${entry.level}`}
|
||||
key={entry.id}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="debug-log-summary"
|
||||
onClick={() => toggleExpanded(entry.id)}
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={14} />
|
||||
) : (
|
||||
<ChevronRight size={14} />
|
||||
)}
|
||||
<time>{logTime(entry.timestamp)}</time>
|
||||
<span className="debug-log-level">{entry.level}</span>
|
||||
<span className="debug-log-service">
|
||||
{SERVICE_LABELS[entry.service]}
|
||||
</span>
|
||||
<span className="debug-log-event">
|
||||
{entry.message ||
|
||||
`${entry.component} · ${entry.event}`}
|
||||
</span>
|
||||
<span className="debug-log-outcome">
|
||||
{entry.outcome}
|
||||
</span>
|
||||
</button>
|
||||
{isExpanded ? (
|
||||
<pre>{JSON.stringify(entry, null, 2)}</pre>
|
||||
) : null}
|
||||
</article>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="debug-log-empty">
|
||||
{readState === "loading"
|
||||
? "正在读取日志…"
|
||||
: "没有匹配的日志"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ModelUsageView live={live} refreshToken={usageRefreshToken} />
|
||||
|
||||
@@ -40,10 +40,7 @@ interface ModelUsageViewProps {
|
||||
refreshToken: number;
|
||||
}
|
||||
|
||||
export function ModelUsageView({
|
||||
live,
|
||||
refreshToken,
|
||||
}: ModelUsageViewProps) {
|
||||
export function ModelUsageView({ live, refreshToken }: ModelUsageViewProps) {
|
||||
const [rangeMinutes, setRangeMinutes] = useState(60);
|
||||
const [report, setReport] = useState<ModelUsageReport | null>(null);
|
||||
const [state, setState] = useState<"loading" | "ready" | "error">("loading");
|
||||
@@ -59,6 +56,7 @@ export function ModelUsageView({
|
||||
}
|
||||
}, [rangeMinutes]);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: refreshToken is the explicit refresh trigger.
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load, refreshToken]);
|
||||
@@ -69,7 +67,10 @@ export function ModelUsageView({
|
||||
return () => window.clearInterval(timer);
|
||||
}, [live, load]);
|
||||
|
||||
const stages = useMemo(() => summarizeStages(report?.by_minute ?? []), [report]);
|
||||
const stages = useMemo(
|
||||
() => summarizeStages(report?.by_minute ?? []),
|
||||
[report],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="model-usage-view">
|
||||
@@ -100,7 +101,10 @@ export function ModelUsageView({
|
||||
</div>
|
||||
|
||||
<div className="model-usage-metrics">
|
||||
<Metric label="总 Tokens" value={formatTokens(report?.summary.total_tokens)} />
|
||||
<Metric
|
||||
label="总 Tokens"
|
||||
value={formatTokens(report?.summary.total_tokens)}
|
||||
/>
|
||||
<Metric
|
||||
label="Avg Prompt"
|
||||
value={formatTokens(report?.summary.avg_prompt_tokens)}
|
||||
@@ -109,10 +113,16 @@ export function ModelUsageView({
|
||||
label="Avg Completion"
|
||||
value={formatTokens(report?.summary.avg_completion_tokens)}
|
||||
/>
|
||||
<Metric label="请求数" value={String(report?.summary.request_count ?? 0)} />
|
||||
<Metric
|
||||
label="请求数"
|
||||
value={String(report?.summary.request_count ?? 0)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section className="model-usage-chart-card" aria-label="各 Prompt 每分钟 Token 曲线">
|
||||
<section
|
||||
className="model-usage-chart-card"
|
||||
aria-label="各 Prompt 每分钟 Token 曲线"
|
||||
>
|
||||
<div className="model-usage-section-title">
|
||||
<strong>各 Prompt 花费曲线</strong>
|
||||
<span>每分钟 · Total Tokens</span>
|
||||
@@ -164,12 +174,19 @@ function UsageChart({ rows }: { rows: ModelUsageMinute[] }) {
|
||||
const plotWidth = width - pad.left - pad.right;
|
||||
const plotHeight = height - pad.top - pad.bottom;
|
||||
const lookup = new Map(
|
||||
rows.map((row) => [`${row.minute}\u0000${row.prompt_stage}`, row.total_tokens]),
|
||||
rows.map((row) => [
|
||||
`${row.minute}\u0000${row.prompt_stage}`,
|
||||
row.total_tokens,
|
||||
]),
|
||||
);
|
||||
const max = Math.max(1, ...rows.map((row) => row.total_tokens));
|
||||
const x = (index: number) =>
|
||||
pad.left + (minutes.length <= 1 ? plotWidth / 2 : (index / (minutes.length - 1)) * plotWidth);
|
||||
const y = (value: number) => pad.top + plotHeight - (value / max) * plotHeight;
|
||||
pad.left +
|
||||
(minutes.length <= 1
|
||||
? plotWidth / 2
|
||||
: (index / (minutes.length - 1)) * plotWidth);
|
||||
const y = (value: number) =>
|
||||
pad.top + plotHeight - (value / max) * plotHeight;
|
||||
|
||||
if (!rows.length)
|
||||
return <div className="model-usage-chart-empty">暂无曲线数据</div>;
|
||||
@@ -204,7 +221,9 @@ function UsageChart({ rows }: { rows: ModelUsageMinute[] }) {
|
||||
return (
|
||||
<g key={stage}>
|
||||
<polyline
|
||||
points={points.map((point) => `${point.x},${y(point.value)}`).join(" ")}
|
||||
points={points
|
||||
.map((point) => `${point.x},${y(point.value)}`)
|
||||
.join(" ")}
|
||||
stroke={stageColor(stage)}
|
||||
/>
|
||||
{points.map((point) => (
|
||||
@@ -243,7 +262,13 @@ function UsageChart({ rows }: { rows: ModelUsageMinute[] }) {
|
||||
function summarizeStages(rows: ModelUsageMinute[]) {
|
||||
const stages = new Map<
|
||||
string,
|
||||
{ name: string; requests: number; prompt: number; completion: number; total: number }
|
||||
{
|
||||
name: string;
|
||||
requests: number;
|
||||
prompt: number;
|
||||
completion: number;
|
||||
total: number;
|
||||
}
|
||||
>();
|
||||
for (const row of rows) {
|
||||
const stage = stages.get(row.prompt_stage) ?? {
|
||||
@@ -270,7 +295,8 @@ function stageColor(stage: string): string {
|
||||
const known = Object.keys(STAGE_LABELS).indexOf(stage);
|
||||
if (known >= 0) return COLORS[known % COLORS.length];
|
||||
let hash = 0;
|
||||
for (const character of stage) hash = (hash * 31 + character.charCodeAt(0)) | 0;
|
||||
for (const character of stage)
|
||||
hash = (hash * 31 + character.charCodeAt(0)) | 0;
|
||||
return COLORS[Math.abs(hash) % COLORS.length];
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class _CapturingLLM:
|
||||
self.last_messages = None
|
||||
|
||||
async def complete_messages(self, *, messages, max_tokens, temperature=None,
|
||||
tools=None, tool_choice=None):
|
||||
tools=None, tool_choice=None, **kwargs):
|
||||
self.last_messages = messages
|
||||
return LLMResponse(
|
||||
content='{"memory": [], "intentions": [], "basic_info": {}}',
|
||||
|
||||
@@ -13,6 +13,7 @@ L7_INTENTION 层去重(0805 事故修复 · v2)单测。
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import uuid
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
@@ -54,7 +55,9 @@ class _FakeEmbedByHash:
|
||||
|
||||
def vec_for(self, text: str) -> List[float]:
|
||||
v = [0.0] * self.dims
|
||||
v[abs(hash(text)) % self.dims] = 1.0
|
||||
# 确定性哈希:内建 hash() 每个进程随机化,会让不同文案偶发撞格、
|
||||
# 被误判成复本,测试随 PYTHONHASHSEED 变 flaky。
|
||||
v[int(hashlib.md5(text.encode()).hexdigest(), 16) % self.dims] = 1.0
|
||||
return v
|
||||
|
||||
async def embed_batch(self, texts):
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import shutil
|
||||
import tempfile
|
||||
import uuid
|
||||
@@ -179,8 +180,10 @@ async def test_writer_batch_dedup_e2e():
|
||||
self.dims = dims
|
||||
|
||||
def _vec(self, text):
|
||||
# 确定性哈希:内建 hash() 每个进程随机化,两条不同文案约有 1/dims
|
||||
# 概率撞进同一格,被误判成复本,整个测试随 PYTHONHASHSEED 变 flaky。
|
||||
v = [0.0] * self.dims
|
||||
v[abs(hash(text)) % self.dims] = 1.0
|
||||
v[int(hashlib.md5(text.encode()).hexdigest(), 16) % self.dims] = 1.0
|
||||
return v
|
||||
|
||||
async def embed_batch(self, texts):
|
||||
|
||||
Reference in new issue
Block a user