Skip to content

Commit ac956d6

Browse files
committed
fix: render plain string message content without extra quotes in Trace Details (Arize-ai#11294)
Root cause: Arize-ai#10941 fixed the tool-return React error by running all message content in SpanDetails through formatContentAsString(). That function was moved from playgroundUtils (where it was only used for tool content) and was designed to JSON.stringify plain strings. Using it for every message in the trace UI made system/user/model text show with extra quotes and escaped newlines. Fix: return plain string content as-is from formatContentAsString when it is not double-stringified JSON or a non-string JSON value. Tool results (objects/arrays) still get pretty-printed via JSON.stringify. Other call sites (playground, messageSchemas, ChatTemplateMessageCard) only pass tool content, so behavior there is unchanged.
1 parent 8887efb commit ac956d6

File tree

3 files changed

+38
-13
lines changed

3 files changed

+38
-13
lines changed

app/src/pages/trace/SpanDetails.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,7 +1417,9 @@ function DocumentItem({
14171417

14181418
function LLMMessage({ message }: { message: AttributeMessage }) {
14191419
const messageContent = message[MessageAttributePostfixes.content];
1420-
const normalizedContent = formatContentAsString(messageContent);
1420+
const normalizedContent = formatContentAsString(messageContent, {
1421+
unquotePlainString: true,
1422+
});
14211423
// as of multi-modal models, a message can also be a list
14221424
const messagesContents = message[MessageAttributePostfixes.contents];
14231425
const toolCalls = message[MessageAttributePostfixes.tool_calls]
@@ -1756,7 +1758,9 @@ function MessageContentListItem({
17561758
}) {
17571759
const { message_content } = messageContentAttribute;
17581760
const text = message_content?.text;
1759-
const normalizedText = text ? formatContentAsString(text) : undefined;
1761+
const normalizedText = text
1762+
? formatContentAsString(text, { unquotePlainString: true })
1763+
: undefined;
17601764
const image = message_content?.image;
17611765
const imageUrl = image?.image?.url;
17621766

app/src/utils/__tests__/jsonUtils.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,11 @@ describe("formatContentAsString", () => {
108108
const content = `"\\"Hello, world!\\""`;
109109
expect(formatContentAsString(content)).toBe(`"Hello, world!"`);
110110
});
111+
112+
it("should return 'undefined' when content is undefined", () => {
113+
expect(formatContentAsString(undefined)).toBe("undefined");
114+
expect(formatContentAsString(undefined, { unquotePlainString: true })).toBe(
115+
"undefined"
116+
);
117+
});
111118
});

app/src/utils/jsonUtils.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,24 @@ export function jsonStringToFlatObject(
104104
/**
105105
* Formats content of any type into a string suitable for rendering.
106106
*
107-
* Handles double-stringified JSON, pretty-prints objects and arrays,
108-
* and preserves valid top-level JSON values.
107+
* By default returns valid JSON (plain strings are JSON-quoted). Use unquotePlainString for readable display.
108+
*
109+
* - Strings: Unwraps double-stringified JSON (e.g. "\"{\\"foo\\":1}\"") and pretty-prints;
110+
* otherwise returns plain string JSON-quoted (valid JSON) or unquoted when unquotePlainString is true.
111+
* - Objects/arrays: Pretty-printed JSON.
112+
* - Primitives (number, boolean, null): String form (e.g. "123", "true", "null").
113+
* - undefined: Returns the string "undefined".
109114
*
110115
* @param content - the content to format
116+
* @param options.unquotePlainString - when true, plain string content is returned as-is (unquoted) for readable display; default false returns valid JSON (quoted)
111117
* @returns a formatted string representation of the content
112118
*/
113-
export function formatContentAsString(content?: unknown): string {
119+
export function formatContentAsString(
120+
content?: unknown,
121+
options?: { unquotePlainString?: boolean }
122+
): string {
123+
const unquotePlainString = options?.unquotePlainString ?? false;
124+
114125
if (typeof content === "string") {
115126
const isDoubleStringified =
116127
content.startsWith('"{') ||
@@ -130,16 +141,19 @@ export function formatContentAsString(content?: unknown): string {
130141
} catch {
131142
// If parsing fails, fall through
132143
}
133-
// If the content is a valid non-string top level json value, return it as-is
134-
// https://datatracker.ietf.org/doc/html/rfc7159#section-3
135-
// 0-9 { [ null false true
136-
// a regex that matches possible top level json values, besides strings
137-
const nonStringStart = /^\s*[0-9{[]|true|false|null/.test(content);
138-
if (nonStringStart) {
144+
// Plain text string or unparseable content
145+
if (unquotePlainString) {
139146
return content;
140147
}
148+
return JSON.stringify(content);
141149
}
142150

143-
// For any content that doesn't match the json spec for a top level value, stringify it with pretty printing
144-
return JSON.stringify(content, null, 2);
151+
// Objects, arrays, and primitives: pretty-print as JSON when possible
152+
try {
153+
const out = JSON.stringify(content, null, 2);
154+
if (out !== undefined) return out;
155+
} catch {
156+
// BigInt and other non-serializable values
157+
}
158+
return String(content);
145159
}

0 commit comments

Comments
 (0)