Skip to content

Commit 0d9f7d9

Browse files
committed
feat(json): add function to strip markdown and explanatory text from LLM responses
1 parent 4bc31f4 commit 0d9f7d9

File tree

2 files changed

+87
-48
lines changed

2 files changed

+87
-48
lines changed

packages/core/src/executors/sampling/base-sampling-executor.ts

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -122,21 +122,7 @@ export abstract class BaseSamplingExecutor {
122122
this.currentIteration < this.maxIterations;
123123
this.currentIteration++
124124
) {
125-
// Create a span for each iteration
126-
const iterationSpan: Span | null = this.tracingEnabled
127-
? startSpan(
128-
"mcpc.sampling_iteration",
129-
{
130-
iteration: this.currentIteration + 1,
131-
agent: this.name,
132-
systemPrompt: systemPrompt(),
133-
maxTokens: String(Number.MAX_SAFE_INTEGER),
134-
maxIterations: this.maxIterations,
135-
messages: JSON.stringify(this.conversationHistory),
136-
},
137-
loopSpan ?? undefined,
138-
)
139-
: null;
125+
let iterationSpan: Span | null = null;
140126

141127
try {
142128
const response = await this.server.createMessage({
@@ -155,11 +141,20 @@ export abstract class BaseSamplingExecutor {
155141
try {
156142
parsedData = parseJSON(responseContent.trim(), true);
157143
} catch (parseError) {
158-
if (iterationSpan) {
159-
iterationSpan.addEvent("parse_error", {
160-
error: String(parseError),
161-
});
162-
}
144+
// Create span for parse error iteration
145+
iterationSpan = this.tracingEnabled
146+
? startSpan(
147+
"mcpc.sampling_iteration.parse_error",
148+
{
149+
iteration: this.currentIteration + 1,
150+
agent: this.name,
151+
error: String(parseError),
152+
maxIterations: this.maxIterations,
153+
},
154+
loopSpan ?? undefined,
155+
)
156+
: null;
157+
163158
this.addParsingErrorToHistory(responseContent, parseError);
164159
if (iterationSpan) endSpan(iterationSpan);
165160
continue;
@@ -177,24 +172,27 @@ export abstract class BaseSamplingExecutor {
177172

178173
const action = parsedData["action"];
179174

180-
// If an action name is present, record it as an attribute on the iteration span for easier tracing/debugging.
181-
if (action && typeof action === "string") {
182-
// Update the span name to include the action for clearer traces.
183-
try {
184-
const safeAction = String(action).replace(/\s+/g, "_");
185-
// updateName is part of the OpenTelemetry Span API
186-
if (
187-
iterationSpan &&
188-
typeof (iterationSpan as any).updateName === "function"
189-
) {
190-
(iterationSpan as any).updateName(
191-
`mcpc.sampling_iteration.${safeAction}`,
192-
);
193-
}
194-
} catch {
195-
// Ignore any errors while updating span name
196-
}
197-
}
175+
// Create span with action name
176+
const actionStr = action && typeof action === "string"
177+
? String(action)
178+
: "unknown_action";
179+
const spanName = `mcpc.sampling_iteration.${actionStr}`;
180+
181+
iterationSpan = this.tracingEnabled
182+
? startSpan(
183+
spanName,
184+
{
185+
iteration: this.currentIteration + 1,
186+
agent: this.name,
187+
action: actionStr,
188+
systemPrompt: systemPrompt(),
189+
maxTokens: String(Number.MAX_SAFE_INTEGER),
190+
maxIterations: this.maxIterations,
191+
messages: JSON.stringify(this.conversationHistory),
192+
},
193+
loopSpan ?? undefined,
194+
)
195+
: null;
198196

199197
// Minimal self-healing: ensure required fields exist
200198
if (!action || typeof parsedData["decision"] !== "string") {

packages/utils/src/json.ts

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,38 @@
11
import { jsonrepair } from "jsonrepair";
22

33
/**
4-
* Attempts to parse JSON with a repair function if initial parse fails.
4+
* Strips common markdown code fences and explanatory text from LLM responses
5+
*/
6+
function stripMarkdownAndText(text: string): string {
7+
// Remove leading/trailing whitespace
8+
text = text.trim();
9+
10+
// Remove markdown code fences: ```json ... ``` or ```...```
11+
text = text.replace(/^```(?:json)?\s*\n?/i, "");
12+
text = text.replace(/\n?```\s*$/, "");
13+
14+
// Remove common LLM prefixes like "Here is the JSON:" or "Response:"
15+
text = text.replace(
16+
/^(?:here is|here's|response|result|output|json):\s*/i,
17+
"",
18+
);
19+
20+
// Try to find JSON object/array boundaries if there's surrounding text
21+
const jsonMatch = text.match(/(\{[\s\S]*\}|\[[\s\S]*\])/);
22+
if (jsonMatch) {
23+
text = jsonMatch[1];
24+
}
25+
26+
return text.trim();
27+
}
28+
29+
/**
30+
* Attempts to parse JSON with automatic cleanup and repair if initial parse fails.
31+
* Handles common LLM output formats like:
32+
* - ```json{"key":"value"}```
33+
* - "Here is: {"key":"value"}"
34+
* - Markdown code fences
35+
* - Malformed JSON that can be repaired
536
*/
637
export function parseJSON<T, U extends boolean = false>(
738
text: string,
@@ -11,17 +42,27 @@ export function parseJSON<T, U extends boolean = false>(
1142
return JSON.parse(text) as T;
1243
} catch (_error) {
1344
try {
14-
const repairedText = jsonrepair(text);
15-
console.warn(
16-
`Failed to parse JSON, attempting to repair, result: ${text}`,
17-
);
18-
if (throwError) {
19-
throw _error;
45+
// First attempt: strip markdown and explanatory text
46+
const cleanedText = stripMarkdownAndText(text);
47+
try {
48+
return JSON.parse(cleanedText) as T;
49+
} catch {
50+
// Second attempt: repair the cleaned JSON
51+
const repairedText = jsonrepair(cleanedText);
52+
console.warn(
53+
`Failed to parse JSON, cleaned and repaired. Original: ${
54+
text.slice(0, 100)
55+
}...`,
56+
);
57+
return JSON.parse(repairedText) as T;
2058
}
21-
return JSON.parse(repairedText) as T;
22-
} catch {
59+
} catch (_repairError) {
2360
if (throwError) {
24-
throw new Error("Failed to parse repaired JSON");
61+
throw new Error(
62+
`Failed to parse JSON after cleanup and repair. Original error: ${
63+
_error instanceof Error ? _error.message : String(_error)
64+
}`,
65+
);
2566
}
2667
return null as T;
2768
}

0 commit comments

Comments
 (0)