Skip to content

Commit aa83614

Browse files
committed
refactor(core): simplify model json parsing
1 parent 51e75cf commit aa83614

13 files changed

Lines changed: 450 additions & 588 deletions

File tree

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
"dotenv": "^16.4.5",
9191
"fetch-socks": "^1.3.0",
9292
"js-yaml": "4.1.0",
93-
"jsonrepair": "3.12.0",
93+
"jsonrepair": "3.14.0",
9494
"mdast-util-from-markdown": "^2.0.2",
9595
"mdast-util-to-markdown": "^2.1.2",
9696
"openai": "6.3.0",

packages/core/src/ai-model/models/doubao.ts

Lines changed: 2 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,16 @@
11
import type { TModelFamily } from '@midscene/shared/env';
22
import { assert } from '@midscene/shared/utils';
3-
import { jsonrepair } from 'jsonrepair';
4-
import {
5-
extractJSONFromCodeBlock,
6-
safeParseJson,
7-
} from '../service-caller/json';
3+
import { parseModelResponseJson } from '../service-caller/json';
84
import {
95
type LocateResultValue,
106
unwrapCoordinateListLikeInput,
117
} from '../shared/model-locate-result';
128
import type {
139
ChatCompletionCallContext,
1410
ChatCompletionParamsResult,
15-
JsonParserContext,
16-
JsonParserSource,
1711
ModelAdapterDefinition,
1812
} from './types';
1913

20-
export function normalizeDoubaoJsonObject(
21-
obj: any,
22-
context: Pick<JsonParserContext, 'preserveStringValueKeys'> = {},
23-
): any {
24-
if (obj === null || obj === undefined) {
25-
return obj;
26-
}
27-
28-
if (Array.isArray(obj)) {
29-
return obj.map((item) => normalizeDoubaoJsonObject(item, context));
30-
}
31-
32-
if (typeof obj === 'object') {
33-
const normalized: any = {};
34-
for (const [key, value] of Object.entries(obj)) {
35-
const trimmedKey = key.trim();
36-
const preserveStringValue =
37-
context.preserveStringValueKeys?.includes(trimmedKey) ?? false;
38-
const normalizedValue =
39-
typeof value === 'string'
40-
? preserveStringValue
41-
? value
42-
: value.trim()
43-
: normalizeDoubaoJsonObject(value, context);
44-
normalized[trimmedKey] = normalizedValue;
45-
}
46-
return normalized;
47-
}
48-
49-
return typeof obj === 'string' ? obj.trim() : obj;
50-
}
51-
52-
export function shouldRepairDoubaoLocateJson(source: JsonParserSource) {
53-
return (
54-
source === 'locate' ||
55-
source === 'section-locator' ||
56-
source === 'planning-action-param'
57-
);
58-
}
59-
60-
export function preprocessDoubaoLocateJson(input: string) {
61-
if (input.includes('bbox')) {
62-
while (/\d+\s+\d+/.test(input)) {
63-
input = input.replace(/(\d+)\s+(\d+)/g, '$1,$2');
64-
}
65-
}
66-
return input;
67-
}
68-
69-
const doubaoJsonParser: ModelAdapterDefinition['jsonParser'] = (
70-
raw,
71-
context = { source: 'generic-object' },
72-
) => {
73-
const { source } = context;
74-
try {
75-
return safeParseJson(raw, context);
76-
} catch (firstError) {
77-
if (!shouldRepairDoubaoLocateJson(source)) {
78-
throw firstError;
79-
}
80-
81-
const jsonString = preprocessDoubaoLocateJson(
82-
extractJSONFromCodeBlock(raw),
83-
);
84-
try {
85-
return normalizeDoubaoJsonObject(
86-
JSON.parse(jsonrepair(jsonString)),
87-
context,
88-
);
89-
} catch (error) {
90-
throw Error(
91-
`failed to parse LLM response into JSON. Error - ${String(
92-
error ?? firstError ?? 'unknown error',
93-
)}. Response - \n ${raw}`,
94-
);
95-
}
96-
}
97-
};
98-
9914
export function parseDoubaoRawLocateValue(input: unknown): LocateResultValue {
10015
const bbox = unwrapCoordinateListLikeInput(input as any);
10116
if (typeof bbox === 'string') {
@@ -194,7 +109,7 @@ const buildDoubaoChatCompletionParams = (
194109
};
195110

196111
const doubaoVisionAdapter: ModelAdapterDefinition = {
197-
jsonParser: doubaoJsonParser,
112+
jsonParser: parseModelResponseJson,
198113
chatCompletion: {
199114
unsupportedUserConfig: ['reasoningBudget'],
200115
buildChatCompletionParams: buildDoubaoChatCompletionParams,

packages/core/src/ai-model/models/resolved.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { normalJsonParser } from '../service-caller/json';
1+
import { parseModelResponseJson } from '../service-caller/json';
22
import { createLocateResultAdapter } from '../shared/model-locate-result/factory';
33
import type { LocateResultAdapterDefinition } from '../shared/model-locate-result/types';
44
import { defaultExtractContentAndReasoning } from './chat-content';
@@ -40,7 +40,7 @@ function resolveJsonParser(
4040
jsonParser: ModelAdapterDefinition['jsonParser'],
4141
): JsonParser {
4242
if (!jsonParser || jsonParser === 'lenient-json') {
43-
return normalJsonParser;
43+
return parseModelResponseJson;
4444
}
4545

4646
if (typeof jsonParser === 'function') {

packages/core/src/ai-model/models/ui-tars/adapter.ts

Lines changed: 3 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,15 @@
11
import { type TModelFamily, UITarsModelVersion } from '@midscene/shared/env';
22
import { assert } from '@midscene/shared/utils';
3-
import { jsonrepair } from 'jsonrepair';
4-
import {
5-
extractJSONFromCodeBlock,
6-
safeParseJson,
7-
} from '../../service-caller/json';
3+
import { parseModelResponseJson } from '../../service-caller/json';
84
import {
95
type LocateResultValue,
106
unwrapCoordinateListLikeInput,
117
} from '../../shared/model-locate-result';
12-
import type {
13-
JsonParserContext,
14-
JsonParserSource,
15-
ModelAdapterDefinition,
16-
} from '../types';
8+
import type { ModelAdapterDefinition } from '../types';
179
import { uiTarsPlanning } from './planning';
1810

1911
const defaultVlmUiTarsReplanningCycleLimit = 40;
2012

21-
function normalizeJsonObject(
22-
obj: any,
23-
context: Pick<JsonParserContext, 'preserveStringValueKeys'> = {},
24-
): any {
25-
if (obj === null || obj === undefined) {
26-
return obj;
27-
}
28-
29-
if (Array.isArray(obj)) {
30-
return obj.map((item) => normalizeJsonObject(item, context));
31-
}
32-
33-
if (typeof obj === 'object') {
34-
const normalized: any = {};
35-
for (const [key, value] of Object.entries(obj)) {
36-
const trimmedKey = key.trim();
37-
const preserveStringValue =
38-
context.preserveStringValueKeys?.includes(trimmedKey) ?? false;
39-
const normalizedValue =
40-
typeof value === 'string'
41-
? preserveStringValue
42-
? value
43-
: value.trim()
44-
: normalizeJsonObject(value, context);
45-
normalized[trimmedKey] = normalizedValue;
46-
}
47-
return normalized;
48-
}
49-
50-
return typeof obj === 'string' ? obj.trim() : obj;
51-
}
52-
53-
function shouldRepairUiTarsLocateJson(source: JsonParserSource) {
54-
return (
55-
source === 'locate' ||
56-
source === 'section-locator' ||
57-
source === 'planning-action-param'
58-
);
59-
}
60-
61-
function preprocessUiTarsLocateJson(input: string) {
62-
if (input.includes('bbox')) {
63-
while (/\d+\s+\d+/.test(input)) {
64-
input = input.replace(/(\d+)\s+(\d+)/g, '$1,$2');
65-
}
66-
}
67-
return input;
68-
}
69-
70-
const uiTarsJsonParser: ModelAdapterDefinition['jsonParser'] = (
71-
raw,
72-
context = { source: 'generic-object' },
73-
) => {
74-
const { source } = context;
75-
try {
76-
return safeParseJson(raw, context);
77-
} catch (firstError) {
78-
if (!shouldRepairUiTarsLocateJson(source)) {
79-
throw firstError;
80-
}
81-
82-
const jsonString = preprocessUiTarsLocateJson(
83-
extractJSONFromCodeBlock(raw),
84-
);
85-
try {
86-
return normalizeJsonObject(JSON.parse(jsonrepair(jsonString)), context);
87-
} catch (error) {
88-
throw Error(
89-
`failed to parse LLM response into JSON. Error - ${String(
90-
error ?? firstError ?? 'unknown error',
91-
)}. Response - \n ${raw}`,
92-
);
93-
}
94-
}
95-
};
96-
9713
// UI-TARS has not received active updates for a long time, so this parser is
9814
// intentionally kept separate from Doubao even though the current logic is the
9915
// same. This avoids coupling UI-TARS behavior to future Doubao adapter changes.
@@ -167,7 +83,7 @@ function createUiTarsAdapter(
16783
uiTarsModelVersion: UITarsModelVersion,
16884
): ModelAdapterDefinition {
16985
return {
170-
jsonParser: uiTarsJsonParser,
86+
jsonParser: parseModelResponseJson,
17187
chatCompletion: {
17288
unsupportedUserConfig: [
17389
'reasoningEnabled',

packages/core/src/ai-model/prompt/extraction.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AIDataExtractionResponse, ServiceExtractParam } from '@/types';
22
import { getPreferredLanguage } from '@midscene/shared/env';
3-
import { safeParseJson } from '../service-caller/json';
3+
import { parseModelResponseJson } from '../service-caller/json';
44
import { extractXMLTag } from './util';
55

66
export function buildTypeQueryDemandValue(
@@ -38,7 +38,10 @@ export function parseXMLExtractionResponse<T>(
3838

3939
let data: T;
4040
try {
41-
data = safeParseJson(dataJsonStr) as T;
41+
data = parseModelResponseJson(dataJsonStr, {
42+
source: 'generic-object',
43+
requireObject: false,
44+
}) as T;
4245
} catch (e) {
4346
throw new Error(`Failed to parse data-json: ${e}`);
4447
}
@@ -47,7 +50,10 @@ export function parseXMLExtractionResponse<T>(
4750
let errors: string[] | undefined;
4851
if (errorsStr) {
4952
try {
50-
const parsedErrors = safeParseJson(errorsStr);
53+
const parsedErrors = parseModelResponseJson(errorsStr, {
54+
source: 'generic-object',
55+
requireObject: false,
56+
});
5157
if (Array.isArray(parsedErrors)) {
5258
errors = parsedErrors;
5359
}

packages/core/src/ai-model/service-caller/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ import {
5151
} from './request-timeout';
5252
export {
5353
extractJSONFromCodeBlock,
54-
normalJsonParser,
55-
safeParseJson,
54+
parseModelResponseJson,
5655
} from './json';
5756
export type { JsonParser } from './json';
5857

0 commit comments

Comments
 (0)