Skip to content

Commit ea8616e

Browse files
committed
fix(search): strip an MCP answer card only when its body parses as JSON
1 parent 45522d5 commit ea8616e

2 files changed

Lines changed: 34 additions & 12 deletions

File tree

‎apps/sim/lib/knowledge/application/chat-citations.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ describe('stripInteractiveCards', () => {
1515
const prose = 'Wrap choices in <options>your list</options> tags, or use <question> to ask.'
1616
expect(stripInteractiveCards(prose)).toBe(prose)
1717
})
18+
it('keeps braces in prose that are not a JSON payload', () => {
19+
const prose =
20+
'See <workspace_resource>{the Q4 report}</workspace_resource>, and <options>{bold} styling applies.'
21+
expect(stripInteractiveCards(prose)).toBe(prose)
22+
})
23+
it('keeps a closed card whose payload is not valid JSON, and the text after it', () => {
24+
const text = 'Before <options>{"a": oops}</options> after.'
25+
expect(stripInteractiveCards(text)).toBe(text)
26+
})
1827
it('drops a card left open before its JSON payload', () => {
1928
expect(stripInteractiveCards('Connect here <credential>{"type":"link"')).toBe('Connect here ')
2029
})

‎apps/sim/lib/knowledge/application/chat-citations.ts‎

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,35 @@ export interface SearchChatCitation {
1212
const CARD_TAGS = 'options|question|usage_upgrade|credential|workspace_resource'
1313
/** A JSON string owns its escaped quotes and any tag-shaped text inside it. */
1414
const JSON_STRING = '"(?:\\\\.|[^"\\\\\\r\\n])*"'
15-
/**
16-
* A card whose body is a JSON value, a thinking block, or a card opener left unclosed before a
17-
* JSON payload. A tag-shaped pair or opener in ordinary prose is not a card and stays.
18-
*/
19-
const INTERACTIVE_CARD = new RegExp(
20-
[
21-
`<(${CARD_TAGS})>\\s*[[{](?:${JSON_STRING}|[^"<])*?[\\]}]\\s*</\\1>`,
22-
'<thinking>[\\s\\S]*?</thinking>',
23-
`<(?:${CARD_TAGS})>\\s*[[{][\\s\\S]*$`,
24-
].join('|'),
15+
/** A closed card candidate; its body is removed only when it parses as a JSON payload. */
16+
const CLOSED_CARD = new RegExp(
17+
`<(${CARD_TAGS})>(\\s*[[{](?:${JSON_STRING}|[^"<])*?[\\]}]\\s*)</\\1>`,
2518
'g'
2619
)
20+
/** A card the model left open, followed by the start of a JSON object with a quoted key. */
21+
const UNCLOSED_CARD = new RegExp(
22+
`<(${CARD_TAGS})>(?![\\s\\S]*</\\1>)\\s*(?:\\{\\s*"|\\[\\s*[{"])[\\s\\S]*$`
23+
)
24+
const THINKING = /<thinking>[\s\S]*?<\/thinking>/g
25+
26+
function isJsonPayload(body: string): boolean {
27+
try {
28+
const value: unknown = JSON.parse(body)
29+
return typeof value === 'object' && value !== null
30+
} catch {
31+
return false
32+
}
33+
}
2734

28-
/** Removes interactive Chat cards so a text-only surface receives only the answer prose. */
35+
/**
36+
* Removes interactive Chat cards so a text-only surface receives only the answer prose. A tag
37+
* pair whose body is not a JSON payload is prose that happens to look like a card, and stays.
38+
*/
2939
export function stripInteractiveCards(content: string): string {
30-
return content.replace(INTERACTIVE_CARD, '')
40+
return content
41+
.replace(CLOSED_CARD, (card, _tag: string, body: string) => (isJsonPayload(body) ? '' : card))
42+
.replace(THINKING, '')
43+
.replace(UNCLOSED_CARD, '')
3144
}
3245

3346
/** Resolves Assistant source tags only against successful, bounded retrieval evidence. */

0 commit comments

Comments
 (0)