From 77621e76279c1a0e4d9c47f2046d61c2ff6866c2 Mon Sep 17 00:00:00 2001 From: Debsmita Santra Date: Tue, 5 May 2026 23:10:15 +0530 Subject: [PATCH 1/3] fix(lightspeed): fix tool call response --- .../lightspeed/.changeset/sour-seals-sort.md | 5 ++ .../formatToolResponseForMarkdown.test.ts | 48 ++++++++++++ .../utils/formatToolResponseForMarkdown.ts | 76 +++++++++++++++++-- 3 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 workspaces/lightspeed/.changeset/sour-seals-sort.md diff --git a/workspaces/lightspeed/.changeset/sour-seals-sort.md b/workspaces/lightspeed/.changeset/sour-seals-sort.md new file mode 100644 index 0000000000..9941464e91 --- /dev/null +++ b/workspaces/lightspeed/.changeset/sour-seals-sort.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-lightspeed': patch +--- + +fixed tool call response diff --git a/workspaces/lightspeed/plugins/lightspeed/src/utils/__tests__/formatToolResponseForMarkdown.test.ts b/workspaces/lightspeed/plugins/lightspeed/src/utils/__tests__/formatToolResponseForMarkdown.test.ts index 6e898bdd75..aa4d3a4af6 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/utils/__tests__/formatToolResponseForMarkdown.test.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/utils/__tests__/formatToolResponseForMarkdown.test.ts @@ -52,4 +52,52 @@ describe('formatToolResponseForMarkdown', () => { expect(out).toContain('```'); expect(out).toContain(long); }); + + it('unwraps SSE tool_result envelope with data prefix', () => { + const input = + 'data: {"event":"tool_result","data":{"id":"abc","status":"completed","content":"{\\"results\\":[{\\"score\\":1.23}]}"}}'; + const out = formatToolResponseForMarkdown(input); + expect(out).toContain('```json'); + expect(out).toContain('"results"'); + expect(out).toContain('"score": 1.23'); + expect(out).not.toContain('"event"'); + }); + + it('unwraps tool_result envelope object and formats content field', () => { + const input = JSON.stringify({ + event: 'tool_result', + data: { + id: 'fc_123', + status: 'completed', + content: JSON.stringify({ + results: [ + { + attributes: { + title: 'Sample title', + }, + }, + ], + }), + }, + }); + + const out = formatToolResponseForMarkdown(input); + expect(out).toContain('```json'); + expect(out).toContain('"results"'); + expect(out).toContain('"title": "Sample title"'); + expect(out).not.toContain('"event"'); + }); + + it('parses status-prefixed JSON payloads from tool result logs', () => { + const input = + '[completed] {"results":[{"attributes":{"title":"Evaluate project health using Scorecards"},"score":1.0647}]}'; + + const out = formatToolResponseForMarkdown(input); + expect(out).toContain('```json'); + expect(out).toContain('"results"'); + expect(out).toContain('"score": 1.0647'); + expect(out).toContain( + '"title": "Evaluate project health using Scorecards"', + ); + }); }); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/utils/formatToolResponseForMarkdown.ts b/workspaces/lightspeed/plugins/lightspeed/src/utils/formatToolResponseForMarkdown.ts index 5dd969a948..3a311212af 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/utils/formatToolResponseForMarkdown.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/utils/formatToolResponseForMarkdown.ts @@ -35,6 +35,68 @@ const prettyPrintToolJson = (value: unknown): string => { return JSON.stringify(value); }; +const tryParseJson = (value: string): unknown | undefined => { + try { + return JSON.parse(value); + } catch { + return undefined; + } +}; + +const formatPayload = (payload: unknown): string => { + if (typeof payload === 'string') { + const nested = tryParseJson(payload); + if (nested !== undefined && nested !== null && typeof nested === 'object') { + return `\`\`\`json\n${JSON.stringify(nested, null, 2)}\n\`\`\``; + } + if (payload.length > 120 || payload.includes('\n')) { + return `\`\`\`\n${payload}\n\`\`\``; + } + return payload; + } + + const body = prettyPrintToolJson(payload); + return `\`\`\`json\n${body}\n\`\`\``; +}; + +const extractToolResultPayload = (raw: string): unknown | undefined => { + const trimmed = raw.trim(); + const statusPrefixedMatch = trimmed.match(/^\[[^\]]+\]\s+([\s\S]+)$/); + if (statusPrefixedMatch?.[1]) { + const statusPayload = tryParseJson(statusPrefixedMatch[1].trim()); + if (statusPayload !== undefined) { + return statusPayload; + } + } + + const jsonSegment = trimmed.startsWith('data:') + ? trimmed.slice('data:'.length).trim() + : trimmed; + + const parsed = tryParseJson(jsonSegment); + if (parsed === undefined || parsed === null || typeof parsed !== 'object') { + return undefined; + } + + const parsedRecord = parsed as Record; + const isToolResultEvent = parsedRecord.event === 'tool_result'; + const eventData = + parsedRecord.data && typeof parsedRecord.data === 'object' + ? (parsedRecord.data as Record) + : undefined; + + if (isToolResultEvent && eventData && 'content' in eventData) { + const content = eventData.content; + if (typeof content === 'string') { + const nested = tryParseJson(content); + return nested ?? content; + } + return content; + } + + return parsed; +}; + export const formatToolResponseForMarkdown = (raw: string): string => { if (raw === null) return ''; const trimmed = raw.trim(); @@ -44,16 +106,16 @@ export const formatToolResponseForMarkdown = (raw: string): string => { return raw; } + const extractedPayload = extractToolResultPayload(trimmed); + if (extractedPayload !== undefined) { + return formatPayload(extractedPayload); + } + let parsed: unknown; try { parsed = JSON.parse(trimmed); } catch { - if (trimmed.length > 120 || trimmed.includes('\n')) { - return `\`\`\`\n${trimmed}\n\`\`\``; - } - return trimmed; + return formatPayload(trimmed); } - - const body = prettyPrintToolJson(parsed); - return `\`\`\`json\n${body}\n\`\`\``; + return formatPayload(parsed); }; From 783c16f256e959f632e6b1928b94a2ce320e2488 Mon Sep 17 00:00:00 2001 From: Debsmita Santra Date: Wed, 6 May 2026 15:37:27 +0530 Subject: [PATCH 2/3] fix sonarqube issues --- .../utils/formatToolResponseForMarkdown.ts | 84 +++++++++++-------- 1 file changed, 50 insertions(+), 34 deletions(-) diff --git a/workspaces/lightspeed/plugins/lightspeed/src/utils/formatToolResponseForMarkdown.ts b/workspaces/lightspeed/plugins/lightspeed/src/utils/formatToolResponseForMarkdown.ts index 3a311212af..fd415ed016 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/utils/formatToolResponseForMarkdown.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/utils/formatToolResponseForMarkdown.ts @@ -35,7 +35,7 @@ const prettyPrintToolJson = (value: unknown): string => { return JSON.stringify(value); }; -const tryParseJson = (value: string): unknown | undefined => { +const tryParseJson = (value: string) => { try { return JSON.parse(value); } catch { @@ -43,9 +43,28 @@ const tryParseJson = (value: string): unknown | undefined => { } }; +const deepParseJson = (value: unknown): unknown => { + let current = value; + + while (typeof current === 'string') { + try { + const parsed = JSON.parse(current); + + // stop if parsing doesn't change type + if (parsed === current) break; + + current = parsed; + } catch { + break; + } + } + + return current; +}; + const formatPayload = (payload: unknown): string => { if (typeof payload === 'string') { - const nested = tryParseJson(payload); + const nested = deepParseJson(payload); if (nested !== undefined && nested !== null && typeof nested === 'object') { return `\`\`\`json\n${JSON.stringify(nested, null, 2)}\n\`\`\``; } @@ -59,13 +78,18 @@ const formatPayload = (payload: unknown): string => { return `\`\`\`json\n${body}\n\`\`\``; }; +const STATUS_PREFIX_REGEX = /^\[([^\]]+)\]\s+([\s\S]+)$/; + const extractToolResultPayload = (raw: string): unknown | undefined => { const trimmed = raw.trim(); - const statusPrefixedMatch = trimmed.match(/^\[[^\]]+\]\s+([\s\S]+)$/); - if (statusPrefixedMatch?.[1]) { - const statusPayload = tryParseJson(statusPrefixedMatch[1].trim()); - if (statusPayload !== undefined) { - return statusPayload; + + const match = STATUS_PREFIX_REGEX.exec(trimmed); + if (match) { + const [, , payload] = match; + + const parsed = tryParseJson(payload.trim()); + if (parsed !== undefined) { + return parsed; } } @@ -74,23 +98,24 @@ const extractToolResultPayload = (raw: string): unknown | undefined => { : trimmed; const parsed = tryParseJson(jsonSegment); - if (parsed === undefined || parsed === null || typeof parsed !== 'object') { + if (!parsed || typeof parsed !== 'object') { return undefined; } const parsedRecord = parsed as Record; - const isToolResultEvent = parsedRecord.event === 'tool_result'; - const eventData = - parsedRecord.data && typeof parsedRecord.data === 'object' - ? (parsedRecord.data as Record) - : undefined; - - if (isToolResultEvent && eventData && 'content' in eventData) { - const content = eventData.content; + + if ( + parsedRecord.event === 'tool_result' && + parsedRecord.data && + typeof parsedRecord.data === 'object' && + 'content' in parsedRecord.data + ) { + const content = (parsedRecord.data as Record).content; + if (typeof content === 'string') { - const nested = tryParseJson(content); - return nested ?? content; + return deepParseJson(content); } + return content; } @@ -98,24 +123,15 @@ const extractToolResultPayload = (raw: string): unknown | undefined => { }; export const formatToolResponseForMarkdown = (raw: string): string => { - if (raw === null) return ''; - const trimmed = raw.trim(); - if (!trimmed) return ''; + if (!raw?.trim()) return ''; - if (/^```/m.test(trimmed)) { - return raw; - } + if (/^```/.test(raw)) return raw; - const extractedPayload = extractToolResultPayload(trimmed); - if (extractedPayload !== undefined) { - return formatPayload(extractedPayload); - } + const payload = extractToolResultPayload(raw); - let parsed: unknown; - try { - parsed = JSON.parse(trimmed); - } catch { - return formatPayload(trimmed); + if (payload !== undefined) { + return formatPayload(payload); } - return formatPayload(parsed); + + return formatPayload(raw); }; From 0129ac1dfcfdbc61d6683d1bfe8bb9ec544fa530 Mon Sep 17 00:00:00 2001 From: Debsmita Santra Date: Thu, 7 May 2026 20:57:48 +0530 Subject: [PATCH 3/3] fix tool-call codeblock in dark theme --- .../lightspeed/src/components/ToolCallContent.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/ToolCallContent.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/ToolCallContent.tsx index f6664a419f..8effe8cbdb 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/ToolCallContent.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/ToolCallContent.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { makeStyles } from '@material-ui/core'; import { Message } from '@patternfly/chatbot'; import { Content, @@ -37,6 +38,15 @@ interface ToolCallContentProps { role?: 'user' | 'bot'; } +const useStyles = makeStyles(() => ({ + codeBlock: { + '& .pf-chatbot__message-code-block': { + border: '1px solid var(--pf-t--global--border--color--default)', + borderRadius: 'var(--pf-t--global--border--radius--small)', + }, + }, +})); + /** * Lightweight component for rendering tool call expandable content. * Used inside PatternFly's ToolCall component's expandableContent prop. @@ -45,6 +55,7 @@ export const ToolCallContent = ({ toolCall, role = 'bot', }: ToolCallContentProps) => { + const classes = useStyles(); const { t } = useTranslation(); const formatExecutionTime = (seconds?: number): string => { @@ -225,7 +236,7 @@ export const ToolCallContent = ({ direction={{ default: 'column' }} spaceItems={{ default: 'spaceItemsXs' }} > - +