Skip to content

Commit 04194db

Browse files
committed
refactor: parse tool response content adjustments
1 parent bd9af67 commit 04194db

3 files changed

Lines changed: 39 additions & 95 deletions

File tree

libs/mobile/chat/features/chat/src/lib/components/ai-message/component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export function ChatAiMessage({
6767
file.type === FileType.IMAGE ? [...acc, { type: file.type, url: `${apiUrl}${file.url}`, index }] : acc,
6868
[] as Array<AttachedImageWithIndex>,
6969
),
70-
[files],
70+
[apiUrl, files],
7171
);
7272

7373
const { handleImagePress, handleAllPhotosPress, selectedImageIndex, isPreviewVisible, handleCloseImagePress } =

libs/mobile/chat/features/chat/src/lib/components/tool-output-bottom-sheet/component.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { BottomSheetModal } from '@gorhom/bottom-sheet';
1+
import { BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
22
import { useTranslation } from '@ronas-it/react-native-common-modules/i18n';
33
import { Fragment, ReactElement, ReactNode, useRef } from 'react';
44
import {
55
AppBottomSheet,
6-
AppBottomSheetKeyboardAwareScrollView,
76
AppPressable,
87
AppSafeAreaView,
98
AppText,
@@ -41,14 +40,11 @@ export function ToolOutputBottomSheet({ toolName, input, output }: ToolOutputBot
4140
isModal={true}
4241
isScrollable={true}
4342
snapPoints={['100%']}
44-
className='px-0'
4543
renderTrigger={renderTrigger}
4644
content={
4745
<View className='flex-1 bg-background-primary'>
4846
<SheetHeader title={toolName} onGoBack={() => sheetRef.current?.close()} />
49-
<AppBottomSheetKeyboardAwareScrollView
50-
className='flex-1'
51-
contentContainerClassName='px-content-offset pb-safe pt-8 android:pb-24'>
47+
<BottomSheetScrollView className='flex-1' contentContainerClassName='pb-safe pt-8 android:pb-24'>
5248
<AppSafeAreaView edges={['bottom']}>
5349
{!!input && (
5450
<Fragment>
@@ -67,7 +63,7 @@ export function ToolOutputBottomSheet({ toolName, input, output }: ToolOutputBot
6763
{output}
6864
</AppText>
6965
</AppSafeAreaView>
70-
</AppBottomSheetKeyboardAwareScrollView>
66+
</BottomSheetScrollView>
7167
</View>
7268
}
7369
/>

libs/mobile/chat/features/chat/src/lib/utils/parse-response-message-content.ts

Lines changed: 35 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -16,61 +16,39 @@ export type ParseResponseMessageContentResult = {
1616
messageContent: string;
1717
};
1818

19-
const readQuotedAttr = (tag: string, attr: string): string | null => {
20-
const needle = `${attr}="`;
21-
const lower = tag.toLowerCase();
22-
const idx = lower.indexOf(needle.toLowerCase());
23-
24-
if (idx === -1) {
25-
return null;
26-
}
27-
28-
let i = idx + needle.length;
29-
let out = '';
30-
31-
while (i < tag.length) {
32-
const c = tag[i];
33-
34-
if (c === '\\' && i + 1 < tag.length) {
35-
const esc = tag[i + 1];
36-
37-
if (esc === 'n') {
38-
out += '\n';
39-
} else if (esc === 't') {
40-
out += '\t';
41-
} else if (esc === 'r') {
42-
out += '\r';
43-
} else if (esc === '"') {
44-
out += '"';
45-
} else if (esc === '\\') {
46-
out += '\\';
47-
} else if (esc === 'u' && /^[0-9a-fA-F]{4}/.test(tag.slice(i + 2, i + 6))) {
48-
out += String.fromCharCode(parseInt(tag.slice(i + 2, i + 6), 16));
49-
i += 6;
50-
51-
continue;
52-
} else {
53-
// e.g. `\&quot;` in attributes: keep `\`, let `&quot;` pass through for html decode
54-
out += c;
55-
i += 1;
56-
57-
continue;
58-
}
59-
60-
i += 2;
61-
62-
continue;
19+
const unescapeAttributeValue = (value: string): string =>
20+
value.replace(/\\(u[0-9a-fA-F]{4}|["'\\ntr])/g, (_, esc: string) => {
21+
switch (esc) {
22+
case 'n':
23+
return '\n';
24+
case 't':
25+
return '\t';
26+
case 'r':
27+
return '\r';
28+
case '"':
29+
return '"';
30+
case '\'':
31+
return '\'';
32+
case '\\':
33+
return '\\';
34+
default:
35+
return esc.startsWith('u') ? String.fromCharCode(parseInt(esc.slice(1), 16)) : esc;
6336
}
37+
});
6438

65-
if (c === '"') {
66-
break;
67-
}
39+
const parseTagAttributes = (tag: string): Record<string, string> => {
40+
const attrs: Record<string, string> = {};
41+
const attrRe = /([^\s=/>]+)\s*=\s*(?:"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)')/g;
6842

69-
out += c;
70-
i++;
43+
let match: RegExpExecArray | null;
44+
45+
while ((match = attrRe.exec(tag)) !== null) {
46+
const [, rawName, doubleQuoted, singleQuoted] = match;
47+
const rawValue = doubleQuoted ?? singleQuoted ?? '';
48+
attrs[rawName.toLowerCase()] = unescapeAttributeValue(rawValue);
7149
}
7250

73-
return out;
51+
return attrs;
7452
};
7553

7654
const indexAfterOpenDetailsTag = (s: string): number => {
@@ -79,7 +57,6 @@ const indexAfterOpenDetailsTag = (s: string): number => {
7957
if (!open) {
8058
return -1;
8159
}
82-
8360
let i = open[0].length;
8461
let inDouble = false;
8562
let escape = false;
@@ -90,28 +67,24 @@ const indexAfterOpenDetailsTag = (s: string): number => {
9067
if (escape) {
9168
escape = false;
9269
i++;
93-
9470
continue;
9571
}
9672

9773
if (c === '\\') {
9874
escape = true;
9975
i++;
100-
10176
continue;
10277
}
10378

10479
if (c === '"') {
10580
inDouble = !inDouble;
10681
i++;
107-
10882
continue;
10983
}
11084

11185
if (c === '>' && !inDouble) {
11286
return i + 1;
11387
}
114-
11588
i++;
11689
}
11790

@@ -147,30 +120,6 @@ const classifyAndNormalizePayload = (raw: string): { contentType: PayloadContent
147120
return { contentType: 'text', normalized: String(parsed) };
148121
};
149122

150-
const repairUtf8MisreadAsLatin1 = (s: string): string => {
151-
for (let j = 0; j < s.length; j++) {
152-
if (s.charCodeAt(j) > 255) {
153-
return s;
154-
}
155-
}
156-
157-
const bytes = new Uint8Array(s.length);
158-
159-
for (let j = 0; j < s.length; j++) {
160-
bytes[j] = s.charCodeAt(j);
161-
}
162-
163-
return new TextDecoder('utf-8', { fatal: false }).decode(bytes);
164-
};
165-
166-
const normalizeToolResultText = (s: string): string => {
167-
let t = String(s);
168-
169-
t = repairUtf8MisreadAsLatin1(t);
170-
171-
return t;
172-
};
173-
174123
const tryParseLeadingToolCallsDetails = (content: string): { tool: ToolData; rest: string } | null => {
175124
const leadingWs = content.match(/^\s*/)?.[0] ?? '';
176125
const fromDetails = content.slice(leadingWs.length);
@@ -186,8 +135,9 @@ const tryParseLeadingToolCallsDetails = (content: string): { tool: ToolData; res
186135
}
187136

188137
const openTag = fromDetails.slice(0, openEnd);
138+
const attrs = parseTagAttributes(openTag);
189139

190-
if (!/type\s*=\s*["']?tool_calls["']?/i.test(openTag)) {
140+
if ((attrs.type ?? '').toLowerCase() !== 'tool_calls') {
191141
return null;
192142
}
193143

@@ -197,14 +147,13 @@ const tryParseLeadingToolCallsDetails = (content: string): { tool: ToolData; res
197147
return null;
198148
}
199149

200-
const id = readQuotedAttr(openTag, 'id') ?? undefined;
201-
const toolName = readQuotedAttr(openTag, 'name') ?? '';
202-
const argsRaw = readQuotedAttr(openTag, 'arguments') ?? '';
203-
const resultRaw = readQuotedAttr(openTag, 'result') ?? '';
150+
const id = attrs.id ?? undefined;
151+
const toolName = attrs.name ?? '';
152+
const argsRaw = attrs.arguments ?? '';
153+
const resultRaw = attrs.result ?? '';
204154

205155
const outputPayload = classifyAndNormalizePayload(resultRaw);
206156
const input = parseObjectToString(parseJsonRecursive(decode(argsRaw).trim()));
207-
208157
const blockEnd = leadingWs.length + openEnd + closeMatch.index + closeMatch[0].length;
209158
const rest = content.slice(blockEnd).trimStart();
210159

@@ -213,7 +162,7 @@ const tryParseLeadingToolCallsDetails = (content: string): { tool: ToolData; res
213162
id,
214163
toolName,
215164
input,
216-
output: normalizeToolResultText(outputPayload.normalized),
165+
output: outputPayload.normalized,
217166
outputContentType: outputPayload.contentType,
218167
},
219168
rest,
@@ -230,7 +179,6 @@ export const parseResponseMessageContent = (content: string): ParseResponseMessa
230179
if (!next) {
231180
break;
232181
}
233-
234182
toolsData.push(next.tool);
235183
rest = next.rest;
236184
}

0 commit comments

Comments
 (0)