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/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' }} > - + { 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..fd415ed016 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/utils/formatToolResponseForMarkdown.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/utils/formatToolResponseForMarkdown.ts @@ -35,25 +35,103 @@ const prettyPrintToolJson = (value: unknown): string => { return JSON.stringify(value); }; -export const formatToolResponseForMarkdown = (raw: string): string => { - if (raw === null) return ''; - const trimmed = raw.trim(); - if (!trimmed) return ''; +const tryParseJson = (value: string) => { + try { + return JSON.parse(value); + } catch { + return undefined; + } +}; - if (/^```/m.test(trimmed)) { - return raw; +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; + } } - let parsed: unknown; - try { - parsed = JSON.parse(trimmed); - } catch { - if (trimmed.length > 120 || trimmed.includes('\n')) { - return `\`\`\`\n${trimmed}\n\`\`\``; + return current; +}; + +const formatPayload = (payload: unknown): string => { + if (typeof payload === 'string') { + const nested = deepParseJson(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 trimmed; + return payload; } - const body = prettyPrintToolJson(parsed); + const body = prettyPrintToolJson(payload); return `\`\`\`json\n${body}\n\`\`\``; }; + +const STATUS_PREFIX_REGEX = /^\[([^\]]+)\]\s+([\s\S]+)$/; + +const extractToolResultPayload = (raw: string): unknown | undefined => { + const trimmed = raw.trim(); + + const match = STATUS_PREFIX_REGEX.exec(trimmed); + if (match) { + const [, , payload] = match; + + const parsed = tryParseJson(payload.trim()); + if (parsed !== undefined) { + return parsed; + } + } + + const jsonSegment = trimmed.startsWith('data:') + ? trimmed.slice('data:'.length).trim() + : trimmed; + + const parsed = tryParseJson(jsonSegment); + if (!parsed || typeof parsed !== 'object') { + return undefined; + } + + const parsedRecord = parsed as Record; + + 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') { + return deepParseJson(content); + } + + return content; + } + + return parsed; +}; + +export const formatToolResponseForMarkdown = (raw: string): string => { + if (!raw?.trim()) return ''; + + if (/^```/.test(raw)) return raw; + + const payload = extractToolResultPayload(raw); + + if (payload !== undefined) { + return formatPayload(payload); + } + + return formatPayload(raw); +};