Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions workspaces/lightspeed/.changeset/sour-seals-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-lightspeed': patch
---

fixed tool call response
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import { makeStyles } from '@material-ui/core';
import { Message } from '@patternfly/chatbot';
import {
Content,
Expand All @@ -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.
Expand All @@ -45,6 +55,7 @@ export const ToolCallContent = ({
toolCall,
role = 'bot',
}: ToolCallContentProps) => {
const classes = useStyles();
const { t } = useTranslation();

const formatExecutionTime = (seconds?: number): string => {
Expand Down Expand Up @@ -225,7 +236,7 @@ export const ToolCallContent = ({
direction={{ default: 'column' }}
spaceItems={{ default: 'spaceItemsXs' }}
>
<FlexItem>
<FlexItem className={classes.codeBlock}>
<Message
content={formatToolResponseForMarkdown(
toolCall.response,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,52 @@
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}]}"}}';

Check warning on line 58 in workspaces/lightspeed/plugins/lightspeed/src/utils/__tests__/formatToolResponseForMarkdown.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`String.raw` should be used to avoid escaping `\`.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ35X1ip25ty236RxUPm&open=AZ35X1ip25ty236RxUPm&pullRequest=3049
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"',
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,103 @@
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 => {

Check warning on line 83 in workspaces/lightspeed/plugins/lightspeed/src/utils/formatToolResponseForMarkdown.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'unknown' overrides all other types in this union type.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ35X1gL25ty236RxUPj&open=AZ35X1gL25ty236RxUPj&pullRequest=3049
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<string, unknown>;

if (
parsedRecord.event === 'tool_result' &&
parsedRecord.data &&
typeof parsedRecord.data === 'object' &&
'content' in parsedRecord.data
) {
const content = (parsedRecord.data as Record<string, unknown>).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;

Check warning on line 128 in workspaces/lightspeed/plugins/lightspeed/src/utils/formatToolResponseForMarkdown.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'String#startsWith' method instead.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ38w3zas0zfgqOdu-3V&open=AZ38w3zas0zfgqOdu-3V&pullRequest=3049

const payload = extractToolResultPayload(raw);

if (payload !== undefined) {
return formatPayload(payload);
}

return formatPayload(raw);
};
Loading