prediction attribute 统计和log改进. history下拉高度改进;某些demo从14b模型改为1.7b模型,更符合直觉
Browse files- backend/access_log.py +20 -6
- backend/api/prediction_attribute.py +57 -4
- client/src/css/attribution.scss +2 -4
- client/src/css/gen_attribute.scss +2 -3
- client/src/ts/attribution.ts +1 -0
- client/src/ts/attribution/densityAttributionSidebar.ts +2 -0
- client/src/ts/attribution/predictionAttributeClient.ts +19 -4
- client/src/ts/attribution/tokenGenAttributionRunner.ts +24 -3
- client/src/ts/chat.ts +1 -0
- client/src/ts/gen_attribute.ts +9 -1
- client/src/ts/start.ts +1 -0
- client/src/ts/utils/settingsMenuManager.ts +10 -1
- data/demo/public/CN/百科 克里斯蒂亚诺·罗纳尔多_qwen3-1.7b.json +0 -0
- data/demo/public/GPT-2 large unicorn text.json +0 -0
- data/demo/public/Wiki - Cristiano Ronaldo.json +0 -0
- server.yaml +10 -0
backend/access_log.py
CHANGED
|
@@ -213,6 +213,9 @@ def log_prediction_attribute_request(
|
|
| 213 |
target_prediction: Optional[str],
|
| 214 |
target_token_id: Optional[int],
|
| 215 |
model: str,
|
|
|
|
|
|
|
|
|
|
| 216 |
client_ip: str = None,
|
| 217 |
) -> int:
|
| 218 |
"""
|
|
@@ -227,19 +230,30 @@ def log_prediction_attribute_request(
|
|
| 227 |
_request_counter += 1
|
| 228 |
request_id = _request_counter
|
| 229 |
|
| 230 |
-
context_preview =
|
| 231 |
c_preview = _log_str_preview(context, context_preview)
|
| 232 |
if target_token_id is not None:
|
| 233 |
target_show = f"<token_id:{target_token_id}>"
|
| 234 |
else:
|
| 235 |
target_show = "<top-1>" if target_prediction is None else target_prediction
|
| 236 |
details = (
|
| 237 |
-
f"req_id={request_id}, model={model!r},
|
| 238 |
-
f"context_chars={len(context)}"
|
| 239 |
)
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
return request_id
|
| 244 |
|
| 245 |
|
|
|
|
| 213 |
target_prediction: Optional[str],
|
| 214 |
target_token_id: Optional[int],
|
| 215 |
model: str,
|
| 216 |
+
source_page: str,
|
| 217 |
+
flow_id: Optional[str] = None,
|
| 218 |
+
flow_step: Optional[int] = None,
|
| 219 |
client_ip: str = None,
|
| 220 |
) -> int:
|
| 221 |
"""
|
|
|
|
| 230 |
_request_counter += 1
|
| 231 |
request_id = _request_counter
|
| 232 |
|
| 233 |
+
context_preview = 200
|
| 234 |
c_preview = _log_str_preview(context, context_preview)
|
| 235 |
if target_token_id is not None:
|
| 236 |
target_show = f"<token_id:{target_token_id}>"
|
| 237 |
else:
|
| 238 |
target_show = "<top-1>" if target_prediction is None else target_prediction
|
| 239 |
details = (
|
| 240 |
+
f"req_id={request_id}, model={model!r}, source_page={source_page!r}, "
|
| 241 |
+
f"context='{c_preview}', target='{target_show}', context_chars={len(context)}"
|
| 242 |
)
|
| 243 |
+
if flow_id is not None:
|
| 244 |
+
details += f", flow_id={flow_id!r}, flow_step={flow_step}"
|
| 245 |
+
|
| 246 |
+
# 连续 flow 第 1 步后不再打印入站请求,避免日志噪声。
|
| 247 |
+
if flow_id is None or flow_step == 0:
|
| 248 |
+
_log_request("📥 prediction_attribute 请求", details, client_ip)
|
| 249 |
+
|
| 250 |
+
is_flow_request = source_page == "gen_attribute.html"
|
| 251 |
+
if is_flow_request:
|
| 252 |
+
if flow_step == 0:
|
| 253 |
+
_hit_api("causal_flow")
|
| 254 |
+
_hit_api("prediction_attribute")
|
| 255 |
+
else:
|
| 256 |
+
_hit_api(f"prediction_attribute__{source_page}")
|
| 257 |
return request_id
|
| 258 |
|
| 259 |
|
backend/api/prediction_attribute.py
CHANGED
|
@@ -26,6 +26,9 @@ def prediction_attribute(attribution_request):
|
|
| 26 |
target_prediction = attribution_request.get("target_prediction")
|
| 27 |
target_token_id = attribution_request.get("target_token_id")
|
| 28 |
model = attribution_request.get("model")
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
if context is None:
|
| 31 |
return {"success": False, "message": "Missing required field: context"}, 400
|
|
@@ -52,6 +55,46 @@ def prediction_attribute(attribution_request):
|
|
| 52 |
if model not in ("base", "instruct"):
|
| 53 |
return {"success": False, "message": 'model must be "base" or "instruct"'}, 400
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
client_ip = get_client_ip()
|
| 56 |
start_time = time.perf_counter()
|
| 57 |
request_id = log_prediction_attribute_request(
|
|
@@ -59,6 +102,9 @@ def prediction_attribute(attribution_request):
|
|
| 59 |
target_prediction=target_prediction,
|
| 60 |
target_token_id=target_token_id,
|
| 61 |
model=model,
|
|
|
|
|
|
|
|
|
|
| 62 |
client_ip=client_ip,
|
| 63 |
)
|
| 64 |
|
|
@@ -93,9 +139,16 @@ def prediction_attribute(attribution_request):
|
|
| 93 |
elapsed = time.perf_counter() - start_time
|
| 94 |
tokens = len(result.get("token_attribution", []))
|
| 95 |
target_token = result.get("target_token")
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
return {"success": True, **result}, 200
|
|
|
|
| 26 |
target_prediction = attribution_request.get("target_prediction")
|
| 27 |
target_token_id = attribution_request.get("target_token_id")
|
| 28 |
model = attribution_request.get("model")
|
| 29 |
+
source_page = attribution_request.get("source_page")
|
| 30 |
+
flow_id = attribution_request.get("flow_id")
|
| 31 |
+
flow_step = attribution_request.get("flow_step")
|
| 32 |
|
| 33 |
if context is None:
|
| 34 |
return {"success": False, "message": "Missing required field: context"}, 400
|
|
|
|
| 55 |
if model not in ("base", "instruct"):
|
| 56 |
return {"success": False, "message": 'model must be "base" or "instruct"'}, 400
|
| 57 |
|
| 58 |
+
allowed_source_pages = {
|
| 59 |
+
"analysis.html",
|
| 60 |
+
"chat.html",
|
| 61 |
+
"attribution.html",
|
| 62 |
+
"gen_attribute.html",
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
if source_page is None:
|
| 66 |
+
return {"success": False, "message": "Missing required field: source_page"}, 400
|
| 67 |
+
if not isinstance(source_page, str):
|
| 68 |
+
return {"success": False, "message": "source_page must be a string"}, 400
|
| 69 |
+
if source_page == "":
|
| 70 |
+
return {"success": False, "message": "source_page must not be empty"}, 400
|
| 71 |
+
if source_page not in allowed_source_pages:
|
| 72 |
+
return {
|
| 73 |
+
"success": False,
|
| 74 |
+
"message": "source_page must be one of: analysis.html, chat.html, attribution.html, gen_attribute.html",
|
| 75 |
+
}, 400
|
| 76 |
+
|
| 77 |
+
if flow_id is not None and not isinstance(flow_id, str):
|
| 78 |
+
return {"success": False, "message": "flow_id must be a string"}, 400
|
| 79 |
+
if flow_id == "":
|
| 80 |
+
return {"success": False, "message": "flow_id must not be empty"}, 400
|
| 81 |
+
if flow_step is not None and not isinstance(flow_step, int):
|
| 82 |
+
return {"success": False, "message": "flow_step must be an integer"}, 400
|
| 83 |
+
if flow_step is not None and flow_step < 0:
|
| 84 |
+
return {"success": False, "message": "flow_step must be >= 0"}, 400
|
| 85 |
+
|
| 86 |
+
is_causal_flow = source_page == "gen_attribute.html"
|
| 87 |
+
if is_causal_flow:
|
| 88 |
+
if flow_id is None:
|
| 89 |
+
return {"success": False, "message": "Missing required field: flow_id for causal flow"}, 400
|
| 90 |
+
if flow_step is None:
|
| 91 |
+
return {"success": False, "message": "Missing required field: flow_step for causal flow"}, 400
|
| 92 |
+
elif flow_id is not None or flow_step is not None:
|
| 93 |
+
return {
|
| 94 |
+
"success": False,
|
| 95 |
+
"message": "flow_id/flow_step are only allowed when source_page is gen_attribute.html",
|
| 96 |
+
}, 400
|
| 97 |
+
|
| 98 |
client_ip = get_client_ip()
|
| 99 |
start_time = time.perf_counter()
|
| 100 |
request_id = log_prediction_attribute_request(
|
|
|
|
| 102 |
target_prediction=target_prediction,
|
| 103 |
target_token_id=target_token_id,
|
| 104 |
model=model,
|
| 105 |
+
source_page=source_page,
|
| 106 |
+
flow_id=flow_id,
|
| 107 |
+
flow_step=flow_step,
|
| 108 |
client_ip=client_ip,
|
| 109 |
)
|
| 110 |
|
|
|
|
| 139 |
elapsed = time.perf_counter() - start_time
|
| 140 |
tokens = len(result.get("token_attribution", []))
|
| 141 |
target_token = result.get("target_token")
|
| 142 |
+
if flow_id is None:
|
| 143 |
+
print(
|
| 144 |
+
f"\t📤 API prediction_attribute response: req_id={request_id}, "
|
| 145 |
+
f"target={target_token!r}, tokens={tokens}, response_time={elapsed:.4f}s"
|
| 146 |
+
)
|
| 147 |
+
else:
|
| 148 |
+
print(
|
| 149 |
+
f"\t📤 API prediction_attribute response: req_id={request_id}, "
|
| 150 |
+
f"flow_id={flow_id!r}, flow_step={flow_step}, "
|
| 151 |
+
f"target={target_token!r}, tokens={tokens}, response_time={elapsed:.4f}s"
|
| 152 |
+
)
|
| 153 |
|
| 154 |
return {"success": True, **result}, 200
|
client/src/css/attribution.scss
CHANGED
|
@@ -14,11 +14,9 @@
|
|
| 14 |
}
|
| 15 |
}
|
| 16 |
|
| 17 |
-
// Cached history 下拉:覆盖
|
| 18 |
#attribution_cached_history_dropdown.semantic-search-history-dropdown {
|
| 19 |
-
height: min(32vh, 360px);
|
| 20 |
-
min-height: 120px;
|
| 21 |
-
max-height: none;
|
| 22 |
resize: vertical;
|
| 23 |
overflow-y: auto;
|
| 24 |
}
|
|
|
|
| 14 |
}
|
| 15 |
}
|
| 16 |
|
| 17 |
+
// Cached history 下拉:覆盖 mixin 的 max-height:200px;高度随条目、封顶 min(32vh,360px),并可竖向 resize
|
| 18 |
#attribution_cached_history_dropdown.semantic-search-history-dropdown {
|
| 19 |
+
max-height: min(32vh, 360px);
|
|
|
|
|
|
|
| 20 |
resize: vertical;
|
| 21 |
overflow-y: auto;
|
| 22 |
}
|
client/src/css/gen_attribute.scss
CHANGED
|
@@ -32,11 +32,10 @@
|
|
| 32 |
position: static;
|
| 33 |
}
|
| 34 |
|
|
|
|
| 35 |
#gen_attr_cached_history_dropdown.semantic-search-history-dropdown,
|
| 36 |
#gen_attr_cached_demos_dropdown.semantic-search-history-dropdown {
|
| 37 |
-
height: min(32vh, 360px);
|
| 38 |
-
min-height: 120px;
|
| 39 |
-
max-height: none;
|
| 40 |
resize: vertical;
|
| 41 |
overflow-y: auto;
|
| 42 |
}
|
|
|
|
| 32 |
position: static;
|
| 33 |
}
|
| 34 |
|
| 35 |
+
// Cached history / demos:随内容变高,封顶 min(32vh, 360px),竖向 resize;覆盖 chat mixin 的 max-height:200px
|
| 36 |
#gen_attr_cached_history_dropdown.semantic-search-history-dropdown,
|
| 37 |
#gen_attr_cached_demos_dropdown.semantic-search-history-dropdown {
|
| 38 |
+
max-height: min(32vh, 360px);
|
|
|
|
|
|
|
| 39 |
resize: vertical;
|
| 40 |
overflow-y: auto;
|
| 41 |
}
|
client/src/ts/attribution.ts
CHANGED
|
@@ -272,6 +272,7 @@ async function runAnalyze(options?: { forceRefresh?: boolean }): Promise<void> {
|
|
| 272 |
context,
|
| 273 |
targetPrediction: target,
|
| 274 |
model: currentAttributionModelVariant(),
|
|
|
|
| 275 |
forceRefresh,
|
| 276 |
});
|
| 277 |
applyAttributionResponse(context, json);
|
|
|
|
| 272 |
context,
|
| 273 |
targetPrediction: target,
|
| 274 |
model: currentAttributionModelVariant(),
|
| 275 |
+
sourcePage: 'attribution.html',
|
| 276 |
forceRefresh,
|
| 277 |
});
|
| 278 |
applyAttributionResponse(context, json);
|
client/src/ts/attribution/densityAttributionSidebar.ts
CHANGED
|
@@ -72,6 +72,7 @@ export type DensityAttributionSidebarOptions = {
|
|
| 72 |
getContextPrefix?: () => string;
|
| 73 |
/** 首页 base;Chat instruct */
|
| 74 |
predictionModelVariant: PredictionAttributeModelVariant;
|
|
|
|
| 75 |
};
|
| 76 |
|
| 77 |
/**
|
|
@@ -299,6 +300,7 @@ export function initDensityAttributionSidebar(options: DensityAttributionSidebar
|
|
| 299 |
context,
|
| 300 |
targetPrediction: selectedTarget,
|
| 301 |
model: options.predictionModelVariant,
|
|
|
|
| 302 |
forceRefresh: false,
|
| 303 |
});
|
| 304 |
finish(json);
|
|
|
|
| 72 |
getContextPrefix?: () => string;
|
| 73 |
/** 首页 base;Chat instruct */
|
| 74 |
predictionModelVariant: PredictionAttributeModelVariant;
|
| 75 |
+
sourcePage: 'analysis.html' | 'chat.html';
|
| 76 |
};
|
| 77 |
|
| 78 |
/**
|
|
|
|
| 300 |
context,
|
| 301 |
targetPrediction: selectedTarget,
|
| 302 |
model: options.predictionModelVariant,
|
| 303 |
+
sourcePage: options.sourcePage,
|
| 304 |
forceRefresh: false,
|
| 305 |
});
|
| 306 |
finish(json);
|
client/src/ts/attribution/predictionAttributeClient.ts
CHANGED
|
@@ -12,21 +12,35 @@ import {
|
|
| 12 |
} from './attributionResultCache';
|
| 13 |
|
| 14 |
const JSON_ERROR_SNIPPET_MAX = 160;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
export async function fetchPredictionAttribute(
|
| 17 |
apiBaseForRequests: string,
|
| 18 |
context: string,
|
| 19 |
targetPrediction: string | null,
|
| 20 |
model: PredictionAttributeModelVariant,
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
| 22 |
): Promise<AttributionApiResponse> {
|
| 23 |
-
const bodyObj: Record<string, unknown> = { context, model };
|
| 24 |
if (targetPrediction !== null) {
|
| 25 |
bodyObj.target_prediction = targetPrediction;
|
| 26 |
}
|
| 27 |
if (typeof targetTokenId === 'number' && Number.isInteger(targetTokenId) && targetTokenId >= 0) {
|
| 28 |
bodyObj.target_token_id = targetTokenId;
|
| 29 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
const res = await fetch(`${apiBaseForRequests}/api/prediction-attribute`, {
|
| 31 |
method: 'POST',
|
| 32 |
headers: { 'Content-Type': 'application/json' },
|
|
@@ -57,6 +71,7 @@ export type LoadPredictionAttributeWithCacheOptions = {
|
|
| 57 |
context: string;
|
| 58 |
targetPrediction: string;
|
| 59 |
model: PredictionAttributeModelVariant;
|
|
|
|
| 60 |
/** 与归因页「Force retry」一致:先按 entry 删缓存再请求 */
|
| 61 |
forceRefresh?: boolean;
|
| 62 |
};
|
|
@@ -67,7 +82,7 @@ export type LoadPredictionAttributeWithCacheOptions = {
|
|
| 67 |
export async function loadPredictionAttributeWithCache(
|
| 68 |
options: LoadPredictionAttributeWithCacheOptions
|
| 69 |
): Promise<AttributionApiResponse> {
|
| 70 |
-
const { apiBaseForRequests, context, targetPrediction, model, forceRefresh } = options;
|
| 71 |
if (forceRefresh) {
|
| 72 |
await removeCachedEntryByContentKey(entryKey(context, targetPrediction));
|
| 73 |
}
|
|
@@ -77,7 +92,7 @@ export async function loadPredictionAttributeWithCache(
|
|
| 77 |
return hit;
|
| 78 |
}
|
| 79 |
}
|
| 80 |
-
const json = await fetchPredictionAttribute(apiBaseForRequests, context, targetPrediction, model);
|
| 81 |
await save({ context, targetPrediction }, json, 'complete');
|
| 82 |
return json;
|
| 83 |
}
|
|
|
|
| 12 |
} from './attributionResultCache';
|
| 13 |
|
| 14 |
const JSON_ERROR_SNIPPET_MAX = 160;
|
| 15 |
+
export type PredictionAttributeSourcePage =
|
| 16 |
+
| 'analysis.html'
|
| 17 |
+
| 'chat.html'
|
| 18 |
+
| 'attribution.html'
|
| 19 |
+
| 'gen_attribute.html';
|
| 20 |
|
| 21 |
export async function fetchPredictionAttribute(
|
| 22 |
apiBaseForRequests: string,
|
| 23 |
context: string,
|
| 24 |
targetPrediction: string | null,
|
| 25 |
model: PredictionAttributeModelVariant,
|
| 26 |
+
sourcePage: PredictionAttributeSourcePage,
|
| 27 |
+
targetTokenId?: number,
|
| 28 |
+
flowId?: string,
|
| 29 |
+
flowStep?: number,
|
| 30 |
): Promise<AttributionApiResponse> {
|
| 31 |
+
const bodyObj: Record<string, unknown> = { context, model, source_page: sourcePage };
|
| 32 |
if (targetPrediction !== null) {
|
| 33 |
bodyObj.target_prediction = targetPrediction;
|
| 34 |
}
|
| 35 |
if (typeof targetTokenId === 'number' && Number.isInteger(targetTokenId) && targetTokenId >= 0) {
|
| 36 |
bodyObj.target_token_id = targetTokenId;
|
| 37 |
}
|
| 38 |
+
if (typeof flowId === 'string' && flowId.length > 0) {
|
| 39 |
+
bodyObj.flow_id = flowId;
|
| 40 |
+
}
|
| 41 |
+
if (typeof flowStep === 'number' && Number.isInteger(flowStep) && flowStep >= 0) {
|
| 42 |
+
bodyObj.flow_step = flowStep;
|
| 43 |
+
}
|
| 44 |
const res = await fetch(`${apiBaseForRequests}/api/prediction-attribute`, {
|
| 45 |
method: 'POST',
|
| 46 |
headers: { 'Content-Type': 'application/json' },
|
|
|
|
| 71 |
context: string;
|
| 72 |
targetPrediction: string;
|
| 73 |
model: PredictionAttributeModelVariant;
|
| 74 |
+
sourcePage: PredictionAttributeSourcePage;
|
| 75 |
/** 与归因页「Force retry」一致:先按 entry 删缓存再请求 */
|
| 76 |
forceRefresh?: boolean;
|
| 77 |
};
|
|
|
|
| 82 |
export async function loadPredictionAttributeWithCache(
|
| 83 |
options: LoadPredictionAttributeWithCacheOptions
|
| 84 |
): Promise<AttributionApiResponse> {
|
| 85 |
+
const { apiBaseForRequests, context, targetPrediction, model, sourcePage, forceRefresh } = options;
|
| 86 |
if (forceRefresh) {
|
| 87 |
await removeCachedEntryByContentKey(entryKey(context, targetPrediction));
|
| 88 |
}
|
|
|
|
| 92 |
return hit;
|
| 93 |
}
|
| 94 |
}
|
| 95 |
+
const json = await fetchPredictionAttribute(apiBaseForRequests, context, targetPrediction, model, sourcePage);
|
| 96 |
await save({ context, targetPrediction }, json, 'complete');
|
| 97 |
return json;
|
| 98 |
}
|
client/src/ts/attribution/tokenGenAttributionRunner.ts
CHANGED
|
@@ -7,6 +7,9 @@ import type { PromptTokenSpan } from './genAttributeDagPreprocess';
|
|
| 7 |
import type { CompletionFinishReason } from '../utils/generationEndReasonLabel';
|
| 8 |
import { fetchPredictionAttribute, fetchTokenize } from './predictionAttributeClient';
|
| 9 |
|
|
|
|
|
|
|
|
|
|
| 10 |
function splitCodePointPrefix(text: string, prefixLength: number): { prefix: string; rest: string } | null {
|
| 11 |
if (prefixLength < 0) return null;
|
| 12 |
const chars = Array.from(text);
|
|
@@ -45,12 +48,14 @@ export type TokenGenAttributionOptions = {
|
|
| 45 |
* `true`:停止;`false`(默认):切换为 top-1 继续生成,直到 maxTokens 或 EOS。
|
| 46 |
*/
|
| 47 |
stopAfterTeacherForcing?: boolean;
|
| 48 |
-
/** 最大生成 token 数,默认
|
| 49 |
maxTokens?: number;
|
| 50 |
/** 每生成一个 token 后的回调;`stepIndex` 从 0 起,与 {@link TokenGenAttributionHandle.getAllSteps} 下标一致 */
|
| 51 |
onStep: (step: TokenGenStep, stepIndex: number) => void;
|
| 52 |
onComplete: (reason: CompletionFinishReason) => void;
|
| 53 |
onError: (err: Error) => void;
|
|
|
|
|
|
|
| 54 |
};
|
| 55 |
|
| 56 |
export type TokenGenAttributionHandle = {
|
|
@@ -62,7 +67,14 @@ export type TokenGenAttributionHandle = {
|
|
| 62 |
};
|
| 63 |
|
| 64 |
export function startTokenGenAttribution(opts: TokenGenAttributionOptions): TokenGenAttributionHandle {
|
| 65 |
-
const {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
const tfOpt = opts.teacherForcingContinuation;
|
| 67 |
const forcingEnabled = typeof tfOpt === 'string' && tfOpt.length > 0;
|
| 68 |
const promptRegionEnd = initialContext.length;
|
|
@@ -164,7 +176,16 @@ export function startTokenGenAttribution(opts: TokenGenAttributionOptions): Toke
|
|
| 164 |
|
| 165 |
let response: AttributionApiResponse;
|
| 166 |
try {
|
| 167 |
-
response = await fetchPredictionAttribute(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
} catch (err) {
|
| 169 |
const error = err instanceof Error ? err : new Error(String(err));
|
| 170 |
opts.onError(error);
|
|
|
|
| 7 |
import type { CompletionFinishReason } from '../utils/generationEndReasonLabel';
|
| 8 |
import { fetchPredictionAttribute, fetchTokenize } from './predictionAttributeClient';
|
| 9 |
|
| 10 |
+
/** 与生成归因页(含 DAG)「Max tokens」输入框默认值一致 */
|
| 11 |
+
export const TOKEN_GEN_MAX_TOKENS_DEFAULT = 100;
|
| 12 |
+
|
| 13 |
function splitCodePointPrefix(text: string, prefixLength: number): { prefix: string; rest: string } | null {
|
| 14 |
if (prefixLength < 0) return null;
|
| 15 |
const chars = Array.from(text);
|
|
|
|
| 48 |
* `true`:停止;`false`(默认):切换为 top-1 继续生成,直到 maxTokens 或 EOS。
|
| 49 |
*/
|
| 50 |
stopAfterTeacherForcing?: boolean;
|
| 51 |
+
/** 最大生成 token 数,默认 {@link TOKEN_GEN_MAX_TOKENS_DEFAULT} */
|
| 52 |
maxTokens?: number;
|
| 53 |
/** 每生成一个 token 后的回调;`stepIndex` 从 0 起,与 {@link TokenGenAttributionHandle.getAllSteps} 下标一致 */
|
| 54 |
onStep: (step: TokenGenStep, stepIndex: number) => void;
|
| 55 |
onComplete: (reason: CompletionFinishReason) => void;
|
| 56 |
onError: (err: Error) => void;
|
| 57 |
+
/** 单次连续生成归因会话 ID;用于后端日志压缩与统计归类。 */
|
| 58 |
+
flowId: string;
|
| 59 |
};
|
| 60 |
|
| 61 |
export type TokenGenAttributionHandle = {
|
|
|
|
| 67 |
};
|
| 68 |
|
| 69 |
export function startTokenGenAttribution(opts: TokenGenAttributionOptions): TokenGenAttributionHandle {
|
| 70 |
+
const {
|
| 71 |
+
initialContext,
|
| 72 |
+
apiPrefix,
|
| 73 |
+
model,
|
| 74 |
+
maxTokens = TOKEN_GEN_MAX_TOKENS_DEFAULT,
|
| 75 |
+
stopAfterTeacherForcing = false,
|
| 76 |
+
flowId,
|
| 77 |
+
} = opts;
|
| 78 |
const tfOpt = opts.teacherForcingContinuation;
|
| 79 |
const forcingEnabled = typeof tfOpt === 'string' && tfOpt.length > 0;
|
| 80 |
const promptRegionEnd = initialContext.length;
|
|
|
|
| 176 |
|
| 177 |
let response: AttributionApiResponse;
|
| 178 |
try {
|
| 179 |
+
response = await fetchPredictionAttribute(
|
| 180 |
+
apiPrefix,
|
| 181 |
+
context,
|
| 182 |
+
null,
|
| 183 |
+
model,
|
| 184 |
+
'gen_attribute.html',
|
| 185 |
+
targetTokenId,
|
| 186 |
+
flowId,
|
| 187 |
+
steps.length,
|
| 188 |
+
);
|
| 189 |
} catch (err) {
|
| 190 |
const error = err instanceof Error ? err : new Error(String(err));
|
| 191 |
opts.onError(error);
|
client/src/ts/chat.ts
CHANGED
|
@@ -701,4 +701,5 @@ initDensityAttributionSidebar({
|
|
| 701 |
showToast,
|
| 702 |
getContextPrefix: () => currentPromptUsed,
|
| 703 |
predictionModelVariant: 'instruct',
|
|
|
|
| 704 |
});
|
|
|
|
| 701 |
showToast,
|
| 702 |
getContextPrefix: () => currentPromptUsed,
|
| 703 |
predictionModelVariant: 'instruct',
|
| 704 |
+
sourcePage: 'chat.html',
|
| 705 |
});
|
client/src/ts/gen_attribute.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
| 34 |
import {
|
| 35 |
createHydratedTokenGenHandle,
|
| 36 |
startTokenGenAttribution,
|
|
|
|
| 37 |
type TokenGenAttributionHandle,
|
| 38 |
type TokenGenStep,
|
| 39 |
} from './attribution/tokenGenAttributionRunner';
|
|
@@ -90,7 +91,7 @@ const showToast = createToast('#toast').show;
|
|
| 90 |
|
| 91 |
const GEN_ATTR_MODEL_VARIANT_STORAGE_KEY = 'info_radar_gen_attr_model_variant';
|
| 92 |
const GEN_ATTR_MAX_TOKENS_STORAGE_KEY = 'info_radar_gen_attr_max_tokens';
|
| 93 |
-
const GEN_ATTR_MAX_TOKENS_DEFAULT =
|
| 94 |
const GEN_ATTR_DAG_MEASURE_WIDTH_STORAGE_KEY = 'info_radar_gen_attr_dag_measure_width';
|
| 95 |
const GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY = 'info_radar_gen_attr_dag_layout_mode';
|
| 96 |
const GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_step_ms';
|
|
@@ -121,6 +122,12 @@ const GEN_ATTR_DAG_PLAYBACK_TOTAL_S_MAX = 3600;
|
|
| 121 |
const GENERATE_BTN_LABEL = 'Start';
|
| 122 |
const STOP_BTN_LABEL = 'Stop';
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
function readStoredModelVariant(): PredictionAttributeModelVariant {
|
| 125 |
try {
|
| 126 |
const v = localStorage.getItem(GEN_ATTR_MODEL_VARIANT_STORAGE_KEY);
|
|
@@ -1595,6 +1602,7 @@ async function runGeneration(): Promise<void> {
|
|
| 1595 |
apiPrefix: apiBaseForRequests,
|
| 1596 |
model: tokenizeModel,
|
| 1597 |
maxTokens,
|
|
|
|
| 1598 |
teacherForcingContinuation: teacherForcingText,
|
| 1599 |
stopAfterTeacherForcing: stopAfterTF,
|
| 1600 |
onStep(step, stepIndex) {
|
|
|
|
| 34 |
import {
|
| 35 |
createHydratedTokenGenHandle,
|
| 36 |
startTokenGenAttribution,
|
| 37 |
+
TOKEN_GEN_MAX_TOKENS_DEFAULT,
|
| 38 |
type TokenGenAttributionHandle,
|
| 39 |
type TokenGenStep,
|
| 40 |
} from './attribution/tokenGenAttributionRunner';
|
|
|
|
| 91 |
|
| 92 |
const GEN_ATTR_MODEL_VARIANT_STORAGE_KEY = 'info_radar_gen_attr_model_variant';
|
| 93 |
const GEN_ATTR_MAX_TOKENS_STORAGE_KEY = 'info_radar_gen_attr_max_tokens';
|
| 94 |
+
const GEN_ATTR_MAX_TOKENS_DEFAULT = TOKEN_GEN_MAX_TOKENS_DEFAULT;
|
| 95 |
const GEN_ATTR_DAG_MEASURE_WIDTH_STORAGE_KEY = 'info_radar_gen_attr_dag_measure_width';
|
| 96 |
const GEN_ATTR_DAG_LAYOUT_MODE_STORAGE_KEY = 'info_radar_gen_attr_dag_layout_mode';
|
| 97 |
const GEN_ATTR_DAG_PLAYBACK_STEP_MS_STORAGE_KEY = 'info_radar_gen_attr_dag_playback_step_ms';
|
|
|
|
| 122 |
const GENERATE_BTN_LABEL = 'Start';
|
| 123 |
const STOP_BTN_LABEL = 'Stop';
|
| 124 |
|
| 125 |
+
function createFlowId(): string {
|
| 126 |
+
const timePart = Date.now().toString(36).slice(-6);
|
| 127 |
+
const randPart = Math.random().toString(36).slice(2, 6);
|
| 128 |
+
return `${timePart}-${randPart}`;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
function readStoredModelVariant(): PredictionAttributeModelVariant {
|
| 132 |
try {
|
| 133 |
const v = localStorage.getItem(GEN_ATTR_MODEL_VARIANT_STORAGE_KEY);
|
|
|
|
| 1602 |
apiPrefix: apiBaseForRequests,
|
| 1603 |
model: tokenizeModel,
|
| 1604 |
maxTokens,
|
| 1605 |
+
flowId: createFlowId(),
|
| 1606 |
teacherForcingContinuation: teacherForcingText,
|
| 1607 |
stopAfterTeacherForcing: stopAfterTF,
|
| 1608 |
onStep(step, stepIndex) {
|
client/src/ts/start.ts
CHANGED
|
@@ -1007,6 +1007,7 @@ window.onload = () => {
|
|
| 1007 |
apiPrefix: api_prefix,
|
| 1008 |
showToast,
|
| 1009 |
predictionModelVariant: 'base',
|
|
|
|
| 1010 |
});
|
| 1011 |
|
| 1012 |
// 高亮清除事件监听已由 initHighlightClearListeners 处理
|
|
|
|
| 1007 |
apiPrefix: api_prefix,
|
| 1008 |
showToast,
|
| 1009 |
predictionModelVariant: 'base',
|
| 1010 |
+
sourcePage: 'analysis.html',
|
| 1011 |
});
|
| 1012 |
|
| 1013 |
// 高亮清除事件监听已由 initHighlightClearListeners 处理
|
client/src/ts/utils/settingsMenuManager.ts
CHANGED
|
@@ -383,7 +383,16 @@ export class SettingsMenuManager {
|
|
| 383 |
'attribution.html',
|
| 384 |
'gen_attribute.html',
|
| 385 |
] as const;
|
| 386 |
-
const API_ORDER = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
const OS_ORDER = ['ios', 'android', 'windows', 'macos', 'linux', 'unknown'] as const;
|
| 388 |
|
| 389 |
type VisitStatsRow = NonNullable<Awaited<ReturnType<TextAnalysisAPI['getVisitStats']>>>;
|
|
|
|
| 383 |
'attribution.html',
|
| 384 |
'gen_attribute.html',
|
| 385 |
] as const;
|
| 386 |
+
const API_ORDER = [
|
| 387 |
+
'analyze',
|
| 388 |
+
'analyze_semantic',
|
| 389 |
+
'chat',
|
| 390 |
+
'causal_flow',
|
| 391 |
+
'prediction_attribute',
|
| 392 |
+
'prediction_attribute__attribution.html',
|
| 393 |
+
'prediction_attribute__chat.html',
|
| 394 |
+
'prediction_attribute__analysis.html',
|
| 395 |
+
] as const;
|
| 396 |
const OS_ORDER = ['ios', 'android', 'windows', 'macos', 'linux', 'unknown'] as const;
|
| 397 |
|
| 398 |
type VisitStatsRow = NonNullable<Awaited<ReturnType<TextAnalysisAPI['getVisitStats']>>>;
|
data/demo/public/CN/百科 克里斯蒂亚诺·罗纳尔多_qwen3-1.7b.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/demo/public/GPT-2 large unicorn text.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/demo/public/Wiki - Cristiano Ronaldo.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
server.yaml
CHANGED
|
@@ -505,9 +505,19 @@ paths:
|
|
| 505 |
type: string
|
| 506 |
enum: [base, instruct]
|
| 507 |
description: base 使用主槽位(--model),instruct 使用语义槽位(--semantic_model)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
required:
|
| 509 |
- context
|
| 510 |
- model
|
|
|
|
| 511 |
responses:
|
| 512 |
200:
|
| 513 |
description: 返回各输入 token 对目标预测的归因分
|
|
|
|
| 505 |
type: string
|
| 506 |
enum: [base, instruct]
|
| 507 |
description: base 使用主槽位(--model),instruct 使用语义槽位(--semantic_model)
|
| 508 |
+
source_page:
|
| 509 |
+
type: string
|
| 510 |
+
description: 发起页面名(analysis.html / chat.html / attribution.html / gen_attribute.html)
|
| 511 |
+
flow_id:
|
| 512 |
+
type: string
|
| 513 |
+
description: 连续生成归因会话 ID;仅 source_page=gen_attribute.html 时允许
|
| 514 |
+
flow_step:
|
| 515 |
+
type: integer
|
| 516 |
+
description: 连续生成归因步骤(从 0 开始);仅 source_page=gen_attribute.html 时允许
|
| 517 |
required:
|
| 518 |
- context
|
| 519 |
- model
|
| 520 |
+
- source_page
|
| 521 |
responses:
|
| 522 |
200:
|
| 523 |
description: 返回各输入 token 对目标预测的归因分
|