feat(yuanbao-adapter): match chat SSE by question and keep answer breaks
Filter captured `/api/chat/*` event-stream responses down to the one whose request prompt matches the current question, avoiding cross-talk between concurrent conversations. Preserve blank lines when sanitizing answer candidates so paragraph breaks survive, and always trust the parsed SSE answer over the DOM scrape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,11 +9,13 @@ const {
|
|||||||
extractCitationMarkerIndexes,
|
extractCitationMarkerIndexes,
|
||||||
extractQuestionText,
|
extractQuestionText,
|
||||||
findUnresolvedCitationIndexes,
|
findUnresolvedCitationIndexes,
|
||||||
|
isCurrentYuanbaoChatSSE,
|
||||||
mergeText,
|
mergeText,
|
||||||
normalizeCitationMarkers,
|
normalizeCitationMarkers,
|
||||||
normalizeSourceUrl,
|
normalizeSourceUrl,
|
||||||
parseYuanbaoCaptures,
|
parseYuanbaoCaptures,
|
||||||
resolveCitationsFromMarkers,
|
resolveCitationsFromMarkers,
|
||||||
|
selectBestAnswerText,
|
||||||
} = __yuanbaoTestUtils
|
} = __yuanbaoTestUtils
|
||||||
|
|
||||||
describe('yuanbao adapter helpers', () => {
|
describe('yuanbao adapter helpers', () => {
|
||||||
@@ -67,6 +69,77 @@ describe('yuanbao adapter helpers', () => {
|
|||||||
expect(mergeText(first, repeated)).toBe(repeated)
|
expect(mergeText(first, repeated)).toBe(repeated)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps a valid API SSE answer even when the DOM fallback is longer', () => {
|
||||||
|
const apiAnswer =
|
||||||
|
'### 核心结论\n\n混动车兼顾补能效率与低速油耗。\n\n- 适合长途出行\n- 适合没有固定充电桩的用户'
|
||||||
|
const domFallback =
|
||||||
|
'页面工具栏 联网搜索 深度思考 已完成搜索。混动车兼顾补能效率与低速油耗,适合长途出行,也适合没有固定充电桩的用户。这里还有页面推荐问题、操作按钮和其他更长的界面文本。'
|
||||||
|
|
||||||
|
expect(selectBestAnswerText(apiAnswer, domFallback)).toBe(apiAnswer)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses the page RPA answer only when API SSE has no usable answer', () => {
|
||||||
|
const domFallback = '### 页面兜底\n\n- 仅在 SSE 缺失时使用'
|
||||||
|
|
||||||
|
expect(selectBestAnswerText(null, domFallback)).toBe(domFallback)
|
||||||
|
expect(selectBestAnswerText(' ', domFallback)).toBe(domFallback)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('captures only the current POST chat SSE response', () => {
|
||||||
|
const question = '今天是几号?'
|
||||||
|
const requestBody = JSON.stringify({
|
||||||
|
prompt: question,
|
||||||
|
supportFunctions: ['autoInternetSearch'],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isCurrentYuanbaoChatSSE(
|
||||||
|
'https://yuanbao.tencent.com/api/chat/conversation-1',
|
||||||
|
'POST',
|
||||||
|
'text/event-stream',
|
||||||
|
requestBody,
|
||||||
|
question,
|
||||||
|
),
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
isCurrentYuanbaoChatSSE(
|
||||||
|
'https://yuanbao.tencent.com/api/user/agent/conversation/promptSug',
|
||||||
|
'POST',
|
||||||
|
'application/json',
|
||||||
|
requestBody,
|
||||||
|
question,
|
||||||
|
),
|
||||||
|
).toBe(false)
|
||||||
|
expect(
|
||||||
|
isCurrentYuanbaoChatSSE(
|
||||||
|
'https://yuanbao.tencent.com/api/chat/conversation-1',
|
||||||
|
'POST',
|
||||||
|
'text/event-stream',
|
||||||
|
JSON.stringify({ prompt: '另一个问题' }),
|
||||||
|
question,
|
||||||
|
),
|
||||||
|
).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('builds Markdown only from top-level SSE text frames, excluding deep-search process text', () => {
|
||||||
|
const summary = parseYuanbaoCaptures([
|
||||||
|
{
|
||||||
|
url: 'https://yuanbao.tencent.com/api/chat/conversation-1',
|
||||||
|
status: 200,
|
||||||
|
contentType: 'text/event-stream',
|
||||||
|
body: [
|
||||||
|
'data: {"type":"deepSearch","contents":[{"type":"text","msg":"搜索过程不应进入最终回答"}]}',
|
||||||
|
'data: {"type":"text","msg":"### 核心结论\\n\\n"}',
|
||||||
|
'data: {"type":"text","msg":"正文。\\n\\n- 条目 A\\n- 条目 B"}',
|
||||||
|
'data: {"type":"meta","endConv":true,"traceId":"trace-1"}',
|
||||||
|
].join('\n\n'),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(summary.answer).toBe('### 核心结论\n\n正文。\n\n- 条目 A\n- 条目 B')
|
||||||
|
expect(summary.providerRequestID).toBe('trace-1')
|
||||||
|
})
|
||||||
|
|
||||||
it('resolves citation markers against the best available source pool', () => {
|
it('resolves citation markers against the best available source pool', () => {
|
||||||
const sources = [
|
const sources = [
|
||||||
{ url: 'https://example.com/1', normalized_url: 'https://example.com/1' },
|
{ url: 'https://example.com/1', normalized_url: 'https://example.com/1' },
|
||||||
|
|||||||
@@ -350,21 +350,22 @@ function sanitizeAnswerCandidate(value: string | null): string | null {
|
|||||||
|
|
||||||
const repaired = repairPotentialMojibake(normalized) ?? normalized
|
const repaired = repairPotentialMojibake(normalized) ?? normalized
|
||||||
const collapsed = collapseTokenFragmentRuns(repaired)
|
const collapsed = collapseTokenFragmentRuns(repaired)
|
||||||
const lines = collapsed
|
const lines = collapsed.split(/\r?\n/)
|
||||||
.split(/\r?\n+/)
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
if (lines.length <= 1) {
|
if (lines.length <= 1) {
|
||||||
return collapsed
|
return collapsed
|
||||||
}
|
}
|
||||||
|
|
||||||
const keptLines = lines.filter((line) => !looksLikeNonAnswerNoise(line))
|
const keptLines = lines.filter((line) => !line.trim() || !looksLikeNonAnswerNoise(line.trim()))
|
||||||
if (!keptLines.length) {
|
if (!keptLines.some((line) => line.trim())) {
|
||||||
return collapsed
|
return collapsed
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleaned = keptLines.join('\n')
|
return (
|
||||||
return answerQualityScore(cleaned) >= answerQualityScore(collapsed) ? cleaned : collapsed
|
keptLines
|
||||||
|
.join('\n')
|
||||||
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
.trim() || collapsed
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectBestAnswerText(
|
function selectBestAnswerText(
|
||||||
@@ -379,12 +380,7 @@ function selectBestAnswerText(
|
|||||||
if (!dom) {
|
if (!dom) {
|
||||||
return parsed
|
return parsed
|
||||||
}
|
}
|
||||||
|
return parsed
|
||||||
if (parsed.includes(dom) && answerQualityScore(dom) >= answerQualityScore(parsed) - 24) {
|
|
||||||
return dom
|
|
||||||
}
|
|
||||||
|
|
||||||
return answerQualityScore(dom) > answerQualityScore(parsed) ? dom : parsed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveCitationsFromMarkers(
|
function resolveCitationsFromMarkers(
|
||||||
@@ -469,6 +465,16 @@ function getDirectString(record: Record<string, unknown>, keys: string[]): strin
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDirectTextFragment(record: Record<string, unknown>, keys: string[]): string | null {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = record[key]
|
||||||
|
if (typeof value === 'string' && value.length > 0) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeSourceUrl(value: string | null): string | null {
|
function normalizeSourceUrl(value: string | null): string | null {
|
||||||
const input = normalizeText(value)
|
const input = normalizeText(value)
|
||||||
if (!input) {
|
if (!input) {
|
||||||
@@ -844,7 +850,52 @@ function safePageURL(page: PlaywrightPage): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachYuanbaoResponseCapture(page: PlaywrightPage): {
|
function isCurrentYuanbaoChatSSE(
|
||||||
|
url: string,
|
||||||
|
method: string,
|
||||||
|
contentType: string | null,
|
||||||
|
requestBody: string | null,
|
||||||
|
questionText: string,
|
||||||
|
): boolean {
|
||||||
|
if (method.toUpperCase() !== 'POST' || !contentType || !/text\/event-stream/i.test(contentType)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedURL = new URL(url)
|
||||||
|
if (
|
||||||
|
parsedURL.hostname !== 'yuanbao.tencent.com' ||
|
||||||
|
!/^\/api\/chat\/[^/]+\/?$/.test(parsedURL.pathname)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requestBody) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(requestBody) as unknown
|
||||||
|
if (!isRecord(payload)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const expected = normalizeText(questionText)
|
||||||
|
const candidates = [payload.prompt, payload.displayPrompt]
|
||||||
|
.map((value) => normalizeText(value))
|
||||||
|
.filter((value): value is string => value !== null)
|
||||||
|
return Boolean(expected && candidates.some((candidate) => candidate === expected))
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachYuanbaoResponseCapture(
|
||||||
|
page: PlaywrightPage,
|
||||||
|
questionText: string,
|
||||||
|
): {
|
||||||
captures: YuanbaoCaptureRecord[]
|
captures: YuanbaoCaptureRecord[]
|
||||||
flush: (timeoutMs: number) => Promise<void>
|
flush: (timeoutMs: number) => Promise<void>
|
||||||
pendingCount: () => number
|
pendingCount: () => number
|
||||||
@@ -868,17 +919,16 @@ function attachYuanbaoResponseCapture(page: PlaywrightPage): {
|
|||||||
|
|
||||||
const responseHandler = (response: PlaywrightResponse) => {
|
const responseHandler = (response: PlaywrightResponse) => {
|
||||||
const url = normalizeOptionalString(response.url())
|
const url = normalizeOptionalString(response.url())
|
||||||
if (!url || !url.startsWith('https://yuanbao.tencent.com/')) {
|
if (!url) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = response.headers()
|
const headers = response.headers()
|
||||||
const contentType = normalizeOptionalString(headers['content-type']) ?? null
|
const contentType = normalizeOptionalString(headers['content-type']) ?? null
|
||||||
const shouldRead =
|
const request = response.request()
|
||||||
url.includes('/api/') ||
|
if (
|
||||||
response.status() >= 400 ||
|
!isCurrentYuanbaoChatSSE(url, request.method(), contentType, request.postData(), questionText)
|
||||||
(contentType !== null && /(json|event-stream|text\/plain)/i.test(contentType))
|
) {
|
||||||
if (!shouldRead) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1011,14 +1061,19 @@ function resolveYuanbaoSourceFieldBucket(key: string): 'citation' | 'search' | n
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function walkYuanbaoPayload(value: unknown, summary: YuanbaoSummary, depth = 0): void {
|
function walkYuanbaoPayload(
|
||||||
|
value: unknown,
|
||||||
|
summary: YuanbaoSummary,
|
||||||
|
depth = 0,
|
||||||
|
allowAnswer = false,
|
||||||
|
): void {
|
||||||
if (depth > 12 || value == null) {
|
if (depth > 12 || value == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
for (const item of value) {
|
for (const item of value) {
|
||||||
walkYuanbaoPayload(item, summary, depth + 1)
|
walkYuanbaoPayload(item, summary, depth + 1, false)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1045,10 +1100,12 @@ function walkYuanbaoPayload(value: unknown, summary: YuanbaoSummary, depth = 0):
|
|||||||
summary.contentTypes.add(type)
|
summary.contentTypes.add(type)
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'text':
|
case 'text':
|
||||||
summary.answer = mergeText(
|
if (allowAnswer) {
|
||||||
summary.answer,
|
summary.answer = mergeText(
|
||||||
getDirectString(value, ['msg', 'text', 'content']),
|
summary.answer,
|
||||||
)
|
getDirectTextFragment(value, ['msg', 'text', 'content']),
|
||||||
|
)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 'think':
|
case 'think':
|
||||||
summary.reasoning = mergeText(
|
summary.reasoning = mergeText(
|
||||||
@@ -1080,7 +1137,7 @@ function walkYuanbaoPayload(value: unknown, summary: YuanbaoSummary, depth = 0):
|
|||||||
collectSourceBucket(value.thinkDeepSections, summary.citations)
|
collectSourceBucket(value.thinkDeepSections, summary.citations)
|
||||||
break
|
break
|
||||||
case 'deepSearch':
|
case 'deepSearch':
|
||||||
walkYuanbaoPayload(value.contents, summary, depth + 1)
|
walkYuanbaoPayload(value.contents, summary, depth + 1, false)
|
||||||
break
|
break
|
||||||
case 'step':
|
case 'step':
|
||||||
collectSourceBucket(value.thinkDeepSections, summary.citations)
|
collectSourceBucket(value.thinkDeepSections, summary.citations)
|
||||||
@@ -1099,36 +1156,36 @@ function walkYuanbaoPayload(value: unknown, summary: YuanbaoSummary, depth = 0):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(value.content)) {
|
if (Array.isArray(value.content)) {
|
||||||
walkYuanbaoPayload(value.content, summary, depth + 1)
|
walkYuanbaoPayload(value.content, summary, depth + 1, false)
|
||||||
}
|
}
|
||||||
if (Array.isArray(value.contents)) {
|
if (Array.isArray(value.contents)) {
|
||||||
walkYuanbaoPayload(value.contents, summary, depth + 1)
|
walkYuanbaoPayload(value.contents, summary, depth + 1, false)
|
||||||
}
|
}
|
||||||
if (Array.isArray(value.speechesV2)) {
|
if (Array.isArray(value.speechesV2)) {
|
||||||
walkYuanbaoPayload(value.speechesV2, summary, depth + 1)
|
walkYuanbaoPayload(value.speechesV2, summary, depth + 1, false)
|
||||||
}
|
}
|
||||||
if (Array.isArray(value.convs)) {
|
if (Array.isArray(value.convs)) {
|
||||||
walkYuanbaoPayload(value.convs, summary, depth + 1)
|
walkYuanbaoPayload(value.convs, summary, depth + 1, false)
|
||||||
}
|
}
|
||||||
if (Array.isArray(value.docs)) {
|
if (Array.isArray(value.docs)) {
|
||||||
walkYuanbaoPayload(value.docs, summary, depth + 1)
|
walkYuanbaoPayload(value.docs, summary, depth + 1, false)
|
||||||
}
|
}
|
||||||
if (Array.isArray(value.citations)) {
|
if (Array.isArray(value.citations)) {
|
||||||
walkYuanbaoPayload(value.citations, summary, depth + 1)
|
walkYuanbaoPayload(value.citations, summary, depth + 1, false)
|
||||||
}
|
}
|
||||||
if (Array.isArray(value.ref_docs)) {
|
if (Array.isArray(value.ref_docs)) {
|
||||||
walkYuanbaoPayload(value.ref_docs, summary, depth + 1)
|
walkYuanbaoPayload(value.ref_docs, summary, depth + 1, false)
|
||||||
}
|
}
|
||||||
if (Array.isArray(value.thinkDeepSections)) {
|
if (Array.isArray(value.thinkDeepSections)) {
|
||||||
walkYuanbaoPayload(value.thinkDeepSections, summary, depth + 1)
|
walkYuanbaoPayload(value.thinkDeepSections, summary, depth + 1, false)
|
||||||
}
|
}
|
||||||
if (Array.isArray(value.list)) {
|
if (Array.isArray(value.list)) {
|
||||||
walkYuanbaoPayload(value.list, summary, depth + 1)
|
walkYuanbaoPayload(value.list, summary, depth + 1, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const nested of Object.values(value)) {
|
for (const nested of Object.values(value)) {
|
||||||
if (Array.isArray(nested) || isRecord(nested)) {
|
if (Array.isArray(nested) || isRecord(nested)) {
|
||||||
walkYuanbaoPayload(nested, summary, depth + 1)
|
walkYuanbaoPayload(nested, summary, depth + 1, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1155,7 +1212,7 @@ function parseYuanbaoCaptures(captures: YuanbaoCaptureRecord[]): YuanbaoSummary
|
|||||||
const candidates = extractJSONCandidates(capture.body)
|
const candidates = extractJSONCandidates(capture.body)
|
||||||
summary.candidateCount += candidates.length
|
summary.candidateCount += candidates.length
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
walkYuanbaoPayload(candidate, summary)
|
walkYuanbaoPayload(candidate, summary, 0, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1551,7 +1608,7 @@ const yuanbaoQueryInPage = async (parameters: {
|
|||||||
for (const scope of scopes) {
|
for (const scope of scopes) {
|
||||||
const nodes = Array.from(
|
const nodes = Array.from(
|
||||||
scope.querySelectorAll(
|
scope.querySelectorAll(
|
||||||
"button, [role='button'], label, [tabindex], [dt-button-id], [data-button-id]",
|
"button, [role='button'], [role='menuitem'], [role='option'], li.t-dropdown__item, label, [tabindex], [dt-button-id], [data-button-id]",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
@@ -1585,7 +1642,29 @@ const yuanbaoQueryInPage = async (parameters: {
|
|||||||
await wait(delayMs)
|
await wait(delayMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasActiveComposerMode = (pattern: RegExp, shell: HTMLElement | null): boolean => {
|
||||||
|
const scopes: ParentNode[] = shell ? [shell, document] : [document]
|
||||||
|
const seen = new Set<Element>()
|
||||||
|
for (const scope of scopes) {
|
||||||
|
const atoms = Array.from(scope.querySelectorAll('.application-blot-ai-atom'))
|
||||||
|
for (const atom of atoms) {
|
||||||
|
if (seen.has(atom) || !isVisible(atom)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen.add(atom)
|
||||||
|
const text = normalize(atom.textContent)
|
||||||
|
if (text && pattern.test(text)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const readToggleStateByPattern = (pattern: RegExp, shell: HTMLElement | null): boolean | null => {
|
const readToggleStateByPattern = (pattern: RegExp, shell: HTMLElement | null): boolean | null => {
|
||||||
|
if (hasActiveComposerMode(pattern, shell)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
const target = findInteractiveByText(pattern, shell)
|
const target = findInteractiveByText(pattern, shell)
|
||||||
return target ? toggleState(target) : null
|
return target ? toggleState(target) : null
|
||||||
}
|
}
|
||||||
@@ -1611,29 +1690,23 @@ const yuanbaoQueryInPage = async (parameters: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ensureWebSearchEnabled = async (shell: HTMLElement | null): Promise<boolean | null> => {
|
const ensureWebSearchEnabled = async (shell: HTMLElement | null): Promise<boolean | null> => {
|
||||||
const directState = await ensureToggleEnabledByPattern(webSearchPattern, shell)
|
if (hasActiveComposerMode(webSearchPattern, shell)) {
|
||||||
if (directState === true) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolsButton = findInteractiveByText(toolsPattern, shell)
|
const toolsButton = findInteractiveByText(toolsPattern, shell)
|
||||||
if (!toolsButton) {
|
if (!toolsButton) {
|
||||||
return directState
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
await clickInteractive(toolsButton, 220)
|
await clickInteractive(toolsButton, 220)
|
||||||
const menuItem = findInteractiveByText(webSearchPattern, null)
|
const menuItem = findInteractiveByText(webSearchPattern, null)
|
||||||
if (!menuItem) {
|
if (!menuItem) {
|
||||||
return directState
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
let menuState = toggleState(menuItem)
|
await clickInteractive(menuItem, 300)
|
||||||
if (menuState !== true) {
|
return hasActiveComposerMode(webSearchPattern, shell)
|
||||||
await clickInteractive(menuItem, 220)
|
|
||||||
menuState = toggleState(menuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
return menuState ?? true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const setComposerText = (composer: HTMLElement, text: string) => {
|
const setComposerText = (composer: HTMLElement, text: string) => {
|
||||||
@@ -2265,6 +2338,9 @@ const yuanbaoQueryInPage = async (parameters: {
|
|||||||
|
|
||||||
if (enableWebSearch) {
|
if (enableWebSearch) {
|
||||||
lastKnownWebSearchEnabled = await ensureWebSearchEnabled(composerShell)
|
lastKnownWebSearchEnabled = await ensureWebSearchEnabled(composerShell)
|
||||||
|
if (lastKnownWebSearchEnabled !== true) {
|
||||||
|
return fail('yuanbao_web_search_enable_failed', bodyText(), composerTop)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setComposerText(composer, questionText)
|
setComposerText(composer, questionText)
|
||||||
@@ -2349,7 +2425,7 @@ export const yuanbaoAdapter: MonitorAdapter = {
|
|||||||
context.reportProgress('yuanbao.page_ready')
|
context.reportProgress('yuanbao.page_ready')
|
||||||
await ensurePageOnYuanbaoChat(page, context.signal)
|
await ensurePageOnYuanbaoChat(page, context.signal)
|
||||||
|
|
||||||
const capture = attachYuanbaoResponseCapture(page)
|
const capture = attachYuanbaoResponseCapture(page, questionText)
|
||||||
let pageResult: YuanbaoPageQueryResult
|
let pageResult: YuanbaoPageQueryResult
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -2370,15 +2446,13 @@ export const yuanbaoAdapter: MonitorAdapter = {
|
|||||||
const parsed = parseYuanbaoCaptures(capture.captures)
|
const parsed = parseYuanbaoCaptures(capture.captures)
|
||||||
const pendingCaptureCount = capture.pendingCount()
|
const pendingCaptureCount = capture.pendingCount()
|
||||||
const domCitations = buildOrderedDomSourceItems(pageResult.domLinks)
|
const domCitations = buildOrderedDomSourceItems(pageResult.domLinks)
|
||||||
const searchResults = dedupeSourceItems(parsed.searchResults)
|
const searchResults = parsed.searchResults.length
|
||||||
let answerWithMarkers = selectBestAnswerText(parsed.answer, pageResult.domAnswer)
|
? dedupeSourceItems(parsed.searchResults)
|
||||||
if (
|
: dedupeSourceItems(domCitations)
|
||||||
pageResult.inlineCitationIndexes.length > 0 &&
|
const apiAnswer = sanitizeAnswerCandidate(parsed.answer)
|
||||||
extractCitationMarkerIndexes(answerWithMarkers).length === 0 &&
|
const domAnswer = sanitizeAnswerCandidate(pageResult.domAnswer)
|
||||||
extractCitationMarkerIndexes(pageResult.domAnswer).length > 0
|
const answerWithMarkers = selectBestAnswerText(apiAnswer, domAnswer)
|
||||||
) {
|
const responseSource = apiAnswer ? 'api_sse' : domAnswer ? 'page_rpa' : null
|
||||||
answerWithMarkers = pageResult.domAnswer
|
|
||||||
}
|
|
||||||
const citationMarkerIndexes = extractCitationMarkerIndexes(
|
const citationMarkerIndexes = extractCitationMarkerIndexes(
|
||||||
answerWithMarkers ?? pageResult.domAnswer,
|
answerWithMarkers ?? pageResult.domAnswer,
|
||||||
)
|
)
|
||||||
@@ -2387,20 +2461,22 @@ export const yuanbaoAdapter: MonitorAdapter = {
|
|||||||
: 0
|
: 0
|
||||||
const markerCitations = dedupeSourceItems(
|
const markerCitations = dedupeSourceItems(
|
||||||
resolveCitationsFromMarkers(answerWithMarkers ?? pageResult.domAnswer, [
|
resolveCitationsFromMarkers(answerWithMarkers ?? pageResult.domAnswer, [
|
||||||
domCitations,
|
|
||||||
parsed.citations,
|
parsed.citations,
|
||||||
parsed.searchResults,
|
parsed.searchResults,
|
||||||
dedupeSourceItems([...domCitations, ...parsed.citations, ...parsed.searchResults]),
|
domCitations,
|
||||||
|
dedupeSourceItems([...parsed.citations, ...parsed.searchResults, ...domCitations]),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
const answer = normalizeCitationMarkers(answerWithMarkers)
|
const answer = normalizeCitationMarkers(answerWithMarkers)
|
||||||
const citations =
|
const citations =
|
||||||
domCitations.length >= maxCitationMarkerIndex
|
parsed.citations.length >= maxCitationMarkerIndex
|
||||||
? appendUniqueSourceItems(domCitations, [...parsed.citations, ...markerCitations])
|
? appendUniqueSourceItems(parsed.citations, [...markerCitations, ...domCitations])
|
||||||
: appendUniqueSourceItems(
|
: domCitations.length >= maxCitationMarkerIndex
|
||||||
dedupeSourceItems([...parsed.citations, ...markerCitations]),
|
? appendUniqueSourceItems(domCitations, [...parsed.citations, ...markerCitations])
|
||||||
domCitations,
|
: appendUniqueSourceItems(
|
||||||
)
|
dedupeSourceItems([...parsed.citations, ...markerCitations]),
|
||||||
|
domCitations,
|
||||||
|
)
|
||||||
const inlineCitationCandidates = dedupeSourceItems(
|
const inlineCitationCandidates = dedupeSourceItems(
|
||||||
citationMarkerIndexes
|
citationMarkerIndexes
|
||||||
.map((index) => citations[index - 1] ?? null)
|
.map((index) => citations[index - 1] ?? null)
|
||||||
@@ -2482,6 +2558,7 @@ export const yuanbaoAdapter: MonitorAdapter = {
|
|||||||
request_id: requestID,
|
request_id: requestID,
|
||||||
conversation_id: conversationID,
|
conversation_id: conversationID,
|
||||||
answer,
|
answer,
|
||||||
|
response_source: responseSource,
|
||||||
reasoning,
|
reasoning,
|
||||||
citation_count: citations.length,
|
citation_count: citations.length,
|
||||||
search_result_count: searchResults.length,
|
search_result_count: searchResults.length,
|
||||||
@@ -2494,6 +2571,7 @@ export const yuanbaoAdapter: MonitorAdapter = {
|
|||||||
model_label: pageResult.modelLabel,
|
model_label: pageResult.modelLabel,
|
||||||
deep_think_enabled: pageResult.deepThinkEnabled,
|
deep_think_enabled: pageResult.deepThinkEnabled,
|
||||||
web_search_enabled: pageResult.webSearchEnabled,
|
web_search_enabled: pageResult.webSearchEnabled,
|
||||||
|
response_source: responseSource,
|
||||||
candidate_count: parsed.candidateCount,
|
candidate_count: parsed.candidateCount,
|
||||||
capture_count: capture.captures.length,
|
capture_count: capture.captures.length,
|
||||||
capture_pending_after_flush: pendingCaptureCount,
|
capture_pending_after_flush: pendingCaptureCount,
|
||||||
@@ -2518,6 +2596,7 @@ export const __yuanbaoTestUtils = {
|
|||||||
extractCitationMarkerIndexes,
|
extractCitationMarkerIndexes,
|
||||||
extractQuestionText,
|
extractQuestionText,
|
||||||
findUnresolvedCitationIndexes,
|
findUnresolvedCitationIndexes,
|
||||||
|
isCurrentYuanbaoChatSSE,
|
||||||
mergeText,
|
mergeText,
|
||||||
normalizeCitationMarkers,
|
normalizeCitationMarkers,
|
||||||
normalizeSourceUrl,
|
normalizeSourceUrl,
|
||||||
|
|||||||
Reference in New Issue
Block a user