@probelabs/probe 0.6.0-rc163 → 0.6.0-rc165

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -63,12 +63,6 @@ export function generateSchemaInstructions(schema, options = {}) {
63
63
  try {
64
64
  const parsedSchema = typeof schema === 'string' ? JSON.parse(schema) : schema;
65
65
  instructions += `${JSON.stringify(parsedSchema, null, 2)}\n\n`;
66
-
67
- // Generate example using helper function
68
- const exampleObj = generateExampleFromSchema(parsedSchema, { debug });
69
- if (exampleObj) {
70
- instructions += `Example:\n<attempt_completion>\n${JSON.stringify(exampleObj, null, 2)}\n</attempt_completion>\n\n`;
71
- }
72
66
  } catch (e) {
73
67
  if (debug) {
74
68
  console.error('[DEBUG] generateSchemaInstructions: Failed to parse schema:', e.message);
@@ -76,7 +70,7 @@ export function generateSchemaInstructions(schema, options = {}) {
76
70
  instructions += `${schema}\n\n`;
77
71
  }
78
72
 
79
- instructions += 'Your response inside attempt_completion must be ONLY valid JSON - no plain text, no explanations, no markdown.';
73
+ instructions += 'Your response inside attempt_completion must be ONLY valid JSON - no plain text, no explanations, no markdown.\n\nIMPORTANT: First complete the requested analysis/task thoroughly, then provide your final answer in the JSON format above.';
80
74
 
81
75
  return instructions;
82
76
  }
@@ -171,6 +165,74 @@ export function decodeHtmlEntities(text) {
171
165
  return decoded;
172
166
  }
173
167
 
168
+ /**
169
+ * Normalize JavaScript syntax to valid JSON syntax
170
+ * Converts single quotes to double quotes for strings in JSON-like structures
171
+ *
172
+ * @param {string} str - String that might contain JavaScript array/object syntax
173
+ * @returns {string} - String with single quotes normalized to double quotes
174
+ */
175
+ function normalizeJsonQuotes(str) {
176
+ if (!str || typeof str !== 'string') {
177
+ return str;
178
+ }
179
+
180
+ // Quick check: if there are no single quotes, no need to normalize
181
+ if (!str.includes("'")) {
182
+ return str;
183
+ }
184
+
185
+ let result = '';
186
+ let inDoubleQuote = false;
187
+ let inSingleQuote = false;
188
+ let escaped = false;
189
+
190
+ for (let i = 0; i < str.length; i++) {
191
+ const char = str[i];
192
+ const prevChar = i > 0 ? str[i - 1] : '';
193
+
194
+ // Handle escape sequences
195
+ if (escaped) {
196
+ result += char;
197
+ escaped = false;
198
+ continue;
199
+ }
200
+
201
+ if (char === '\\') {
202
+ escaped = true;
203
+ result += char;
204
+ continue;
205
+ }
206
+
207
+ // Track when we're inside double-quoted strings
208
+ if (char === '"' && !inSingleQuote) {
209
+ inDoubleQuote = !inDoubleQuote;
210
+ result += char;
211
+ continue;
212
+ }
213
+
214
+ // Convert single quotes to double quotes (when not inside double quotes)
215
+ if (char === "'" && !inDoubleQuote) {
216
+ // Check if this is a single quote inside a string value (like "It's")
217
+ // If we're already in a single-quoted string, toggle the state
218
+ if (inSingleQuote) {
219
+ // Closing single quote - convert to double quote
220
+ result += '"';
221
+ inSingleQuote = false;
222
+ } else {
223
+ // Opening single quote - convert to double quote
224
+ result += '"';
225
+ inSingleQuote = true;
226
+ }
227
+ continue;
228
+ }
229
+
230
+ result += char;
231
+ }
232
+
233
+ return result;
234
+ }
235
+
174
236
  /**
175
237
  * Clean AI response by extracting JSON content when response contains JSON
176
238
  * Only processes responses that contain JSON structures { or [
@@ -195,29 +257,36 @@ export function cleanSchemaResponse(response) {
195
257
  // Try with json language specifier
196
258
  const jsonBlockMatch = trimmed.match(/```json\s*\n([\s\S]*?)\n```/);
197
259
  if (jsonBlockMatch) {
198
- return jsonBlockMatch[1].trim();
260
+ return normalizeJsonQuotes(jsonBlockMatch[1].trim());
199
261
  }
200
262
 
201
263
  // Try any code block with JSON content
202
264
  const anyBlockMatch = trimmed.match(/```\s*\n([{\[][\s\S]*?[}\]])\s*```/);
203
265
  if (anyBlockMatch) {
204
- return anyBlockMatch[1].trim();
266
+ return normalizeJsonQuotes(anyBlockMatch[1].trim());
205
267
  }
206
268
 
207
269
  // Legacy patterns for more specific matching
208
270
  const codeBlockPatterns = [
209
271
  /```json\s*\n?([{\[][\s\S]*?[}\]])\s*\n?```/,
210
- /```\s*\n?([{\[][\s\S]*?[}\]])\s*\n?```/,
211
- /`([{\[][\s\S]*?[}\]])`/
272
+ /```\s*\n?([{\[][\s\S]*?[}\]])\s*\n?```/
212
273
  ];
213
274
 
214
275
  for (const pattern of codeBlockPatterns) {
215
276
  const match = trimmed.match(pattern);
216
277
  if (match) {
217
- return match[1].trim();
278
+ return normalizeJsonQuotes(match[1].trim());
218
279
  }
219
280
  }
220
281
 
282
+ // Single backtick pattern - ONLY if the entire input is just the code block
283
+ // This prevents extracting inline code from within JSON objects (e.g., `['*']` from markdown text)
284
+ const singleBacktickPattern = /^`([{\[][\s\S]*?[}\]])`$/;
285
+ const singleBacktickMatch = trimmed.match(singleBacktickPattern);
286
+ if (singleBacktickMatch) {
287
+ return normalizeJsonQuotes(singleBacktickMatch[1].trim());
288
+ }
289
+
221
290
  // Look for code block start followed immediately by JSON
222
291
  const codeBlockStartPattern = /```(?:json)?\s*\n?\s*([{\[])/;
223
292
  const codeBlockMatch = trimmed.match(codeBlockStartPattern);
@@ -242,7 +311,7 @@ export function cleanSchemaResponse(response) {
242
311
  }
243
312
 
244
313
  if (bracketCount === 0) {
245
- return trimmed.substring(startIndex, endIndex);
314
+ return normalizeJsonQuotes(trimmed.substring(startIndex, endIndex));
246
315
  }
247
316
  }
248
317
 
@@ -267,7 +336,8 @@ export function cleanSchemaResponse(response) {
267
336
  const isJsonArray = firstChar === '[' && lastChar === ']';
268
337
 
269
338
  if (isJsonObject || isJsonArray) {
270
- return cleaned;
339
+ // Normalize JavaScript syntax (single quotes) to valid JSON syntax (double quotes)
340
+ return normalizeJsonQuotes(cleaned);
271
341
  }
272
342
 
273
343
  return response; // Return original if no extractable JSON found
@@ -832,6 +902,76 @@ export function isMermaidSchema(schema) {
832
902
  return mermaidIndicators.some(indicator => indicator);
833
903
  }
834
904
 
905
+ /**
906
+ * Extract Mermaid diagrams from JSON string values
907
+ * Handles escaped newlines and backticks within JSON strings
908
+ * @param {string} response - Response that may contain JSON with mermaid blocks in string values
909
+ * @returns {Object} - {diagrams: Array, jsonPaths: Array, parsedJson: Object|null}
910
+ */
911
+ export function extractMermaidFromJson(response) {
912
+ if (!response || typeof response !== 'string') {
913
+ return { diagrams: [], jsonPaths: [], parsedJson: null };
914
+ }
915
+
916
+ // Try to extract JSON from code blocks first
917
+ let jsonContent = response.trim();
918
+ const jsonBlockMatch = jsonContent.match(/```json\s*\n([\s\S]*?)\n```/);
919
+ if (jsonBlockMatch) {
920
+ jsonContent = jsonBlockMatch[1].trim();
921
+ } else {
922
+ const anyBlockMatch = jsonContent.match(/```\s*\n([{\[][\s\S]*?[}\]])\s*```/);
923
+ if (anyBlockMatch) {
924
+ jsonContent = anyBlockMatch[1].trim();
925
+ }
926
+ }
927
+
928
+ // Try to parse as JSON
929
+ let parsedJson;
930
+ try {
931
+ parsedJson = JSON.parse(jsonContent);
932
+ } catch (e) {
933
+ return { diagrams: [], jsonPaths: [], parsedJson: null };
934
+ }
935
+
936
+ const diagrams = [];
937
+ const jsonPaths = [];
938
+
939
+ // Recursively search for mermaid diagrams in JSON string values
940
+ function searchObject(obj, path = []) {
941
+ if (typeof obj === 'string') {
942
+ // Look for mermaid code blocks in the string value
943
+ // Handle both escaped (\n) and literal newlines
944
+ const mermaidPattern = /```mermaid([^\n`]*?)(?:\n|\\n)([\s\S]*?)```/gi;
945
+ let match;
946
+
947
+ while ((match = mermaidPattern.exec(obj)) !== null) {
948
+ const attributes = match[1] ? match[1].trim() : '';
949
+ // Unescape the content (replace \\n with actual newlines)
950
+ const content = match[2].replace(/\\n/g, '\n');
951
+
952
+ diagrams.push({
953
+ content: content,
954
+ fullMatch: match[0],
955
+ startIndex: match.index,
956
+ endIndex: match.index + match[0].length,
957
+ attributes: attributes,
958
+ isInJson: true,
959
+ jsonPath: path.join('.')
960
+ });
961
+ jsonPaths.push(path.join('.'));
962
+ }
963
+ } else if (Array.isArray(obj)) {
964
+ obj.forEach((item, index) => searchObject(item, [...path, `[${index}]`]));
965
+ } else if (obj && typeof obj === 'object') {
966
+ Object.entries(obj).forEach(([key, value]) => searchObject(value, [...path, key]));
967
+ }
968
+ }
969
+
970
+ searchObject(parsedJson);
971
+
972
+ return { diagrams, jsonPaths, parsedJson };
973
+ }
974
+
835
975
  /**
836
976
  * Extract Mermaid diagrams from markdown code blocks with position tracking
837
977
  * @param {string} response - Response that may contain markdown with mermaid blocks
@@ -842,6 +982,16 @@ export function extractMermaidFromMarkdown(response) {
842
982
  return { diagrams: [], cleanedResponse: response };
843
983
  }
844
984
 
985
+ // First check if this looks like a JSON response - if so, use JSON-aware extraction
986
+ const trimmed = response.trim();
987
+ if ((trimmed.startsWith('{') || trimmed.startsWith('[')) ||
988
+ trimmed.includes('```json')) {
989
+ const jsonResult = extractMermaidFromJson(response);
990
+ if (jsonResult.diagrams.length > 0) {
991
+ return { diagrams: jsonResult.diagrams, cleanedResponse: response };
992
+ }
993
+ }
994
+
845
995
  // Find all mermaid code blocks with enhanced regex to capture more variations
846
996
  // This regex captures optional attributes on same line as ```mermaid, and all diagram content
847
997
  const mermaidBlockRegex = /```mermaid([^\n]*)\n([\s\S]*?)```/gi;
@@ -860,7 +1010,8 @@ export function extractMermaidFromMarkdown(response) {
860
1010
  fullMatch: match[0],
861
1011
  startIndex: match.index,
862
1012
  endIndex: match.index + match[0].length,
863
- attributes: attributes
1013
+ attributes: attributes,
1014
+ isInJson: false
864
1015
  });
865
1016
  }
866
1017
 
@@ -868,9 +1019,84 @@ export function extractMermaidFromMarkdown(response) {
868
1019
  return { diagrams, cleanedResponse: response };
869
1020
  }
870
1021
 
1022
+ /**
1023
+ * Replace mermaid diagrams in JSON string values with corrected versions
1024
+ * @param {string} originalResponse - Original response with JSON
1025
+ * @param {Array} correctedDiagrams - Array of corrected diagram objects with jsonPath
1026
+ * @returns {string} - Response with corrected diagrams properly escaped in JSON
1027
+ */
1028
+ export function replaceMermaidDiagramsInJson(originalResponse, correctedDiagrams) {
1029
+ if (!originalResponse || typeof originalResponse !== 'string') {
1030
+ return originalResponse;
1031
+ }
1032
+
1033
+ if (!correctedDiagrams || correctedDiagrams.length === 0) {
1034
+ return originalResponse;
1035
+ }
1036
+
1037
+ // Extract and parse JSON
1038
+ const jsonResult = extractMermaidFromJson(originalResponse);
1039
+ if (!jsonResult.parsedJson) {
1040
+ return originalResponse;
1041
+ }
1042
+
1043
+ let modifiedJson = jsonResult.parsedJson;
1044
+
1045
+ // Replace diagrams in the JSON object
1046
+ for (const diagram of correctedDiagrams) {
1047
+ if (!diagram.jsonPath || !diagram.isInJson) {
1048
+ continue;
1049
+ }
1050
+
1051
+ // Navigate to the path and replace the content
1052
+ const pathParts = diagram.jsonPath.split('.').filter(p => p);
1053
+ let current = modifiedJson;
1054
+
1055
+ for (let i = 0; i < pathParts.length - 1; i++) {
1056
+ const part = pathParts[i];
1057
+ if (part.startsWith('[') && part.endsWith(']')) {
1058
+ const index = parseInt(part.slice(1, -1), 10);
1059
+ current = current[index];
1060
+ } else {
1061
+ current = current[part];
1062
+ }
1063
+ }
1064
+
1065
+ // Get the last key/index
1066
+ const lastPart = pathParts[pathParts.length - 1];
1067
+ const attributesStr = diagram.attributes ? ` ${diagram.attributes}` : '';
1068
+ const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${diagram.content}\n\`\`\``;
1069
+
1070
+ if (lastPart.startsWith('[') && lastPart.endsWith(']')) {
1071
+ const index = parseInt(lastPart.slice(1, -1), 10);
1072
+ const originalString = current[index];
1073
+ // The fullMatch from extraction has unescaped newlines, so we need to match that
1074
+ current[index] = originalString.replace(diagram.fullMatch, newCodeBlock);
1075
+ } else {
1076
+ const originalString = current[lastPart];
1077
+ // The fullMatch from extraction has unescaped newlines, so we need to match that
1078
+ current[lastPart] = originalString.replace(diagram.fullMatch, newCodeBlock);
1079
+ }
1080
+ }
1081
+
1082
+ // Reconstruct the response with modified JSON
1083
+ const modifiedJsonString = JSON.stringify(modifiedJson, null, 2);
1084
+
1085
+ // Check if original was in a code block
1086
+ const trimmed = originalResponse.trim();
1087
+ if (trimmed.match(/```json\s*\n([\s\S]*?)\n```/)) {
1088
+ return originalResponse.replace(/```json\s*\n([\s\S]*?)\n```/, `\`\`\`json\n${modifiedJsonString}\n\`\`\``);
1089
+ } else if (trimmed.match(/```\s*\n([{\[][\s\S]*?[}\]])\s*```/)) {
1090
+ return originalResponse.replace(/```\s*\n([{\[][\s\S]*?[}\]])\s*```/, `\`\`\`\n${modifiedJsonString}\n\`\`\``);
1091
+ }
1092
+
1093
+ return modifiedJsonString;
1094
+ }
1095
+
871
1096
  /**
872
1097
  * Replace mermaid diagrams in original markdown with corrected versions
873
- * @param {string} originalResponse - Original response with markdown
1098
+ * Automatically detects JSON vs markdown format and uses appropriate replacement
1099
+ * @param {string} originalResponse - Original response with markdown or JSON
874
1100
  * @param {Array} correctedDiagrams - Array of corrected diagram objects
875
1101
  * @returns {string} - Response with corrected diagrams in original format
876
1102
  */
@@ -883,22 +1109,28 @@ export function replaceMermaidDiagramsInMarkdown(originalResponse, correctedDiag
883
1109
  return originalResponse;
884
1110
  }
885
1111
 
1112
+ // Check if any diagrams are in JSON format
1113
+ const hasJsonDiagrams = correctedDiagrams.some(d => d.isInJson);
1114
+ if (hasJsonDiagrams) {
1115
+ return replaceMermaidDiagramsInJson(originalResponse, correctedDiagrams);
1116
+ }
1117
+
886
1118
  let modifiedResponse = originalResponse;
887
-
1119
+
888
1120
  // Sort diagrams by start index in reverse order to preserve indices during replacement
889
1121
  const sortedDiagrams = [...correctedDiagrams].sort((a, b) => b.startIndex - a.startIndex);
890
-
1122
+
891
1123
  for (const diagram of sortedDiagrams) {
892
1124
  // Reconstruct the code block with original attributes if they existed
893
1125
  const attributesStr = diagram.attributes ? ` ${diagram.attributes}` : '';
894
1126
  const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${diagram.content}\n\`\`\``;
895
-
1127
+
896
1128
  // Replace the original code block
897
- modifiedResponse = modifiedResponse.slice(0, diagram.startIndex) +
898
- newCodeBlock +
1129
+ modifiedResponse = modifiedResponse.slice(0, diagram.startIndex) +
1130
+ newCodeBlock +
899
1131
  modifiedResponse.slice(diagram.endIndex);
900
1132
  }
901
-
1133
+
902
1134
  return modifiedResponse;
903
1135
  }
904
1136