@probelabs/probe 0.6.0-rc161 → 0.6.0-rc163

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.
@@ -7,6 +7,80 @@ import { createMessagePreview } from '../tools/common.js';
7
7
  import { validate, fixText, extractMermaidBlocks } from '@probelabs/maid';
8
8
  import Ajv from 'ajv';
9
9
 
10
+ /**
11
+ * Generate an example JSON object from a JSON schema
12
+ * @param {Object|string} schema - JSON schema object or string
13
+ * @param {Object} options - Options for generation
14
+ * @param {boolean} [options.debug=false] - Enable debug logging
15
+ * @returns {Object|null} - Example object, or null if schema cannot be processed
16
+ */
17
+ export function generateExampleFromSchema(schema, options = {}) {
18
+ const { debug = false } = options;
19
+
20
+ try {
21
+ const parsedSchema = typeof schema === 'string' ? JSON.parse(schema) : schema;
22
+
23
+ if (parsedSchema.type !== 'object' || !parsedSchema.properties) {
24
+ return null;
25
+ }
26
+
27
+ const exampleObj = {};
28
+ for (const [key, value] of Object.entries(parsedSchema.properties)) {
29
+ if (value.type === 'boolean') {
30
+ exampleObj[key] = false;
31
+ } else if (value.type === 'number') {
32
+ exampleObj[key] = 0;
33
+ } else if (value.type === 'string') {
34
+ exampleObj[key] = value.description || 'your answer here';
35
+ } else if (value.type === 'array') {
36
+ exampleObj[key] = [];
37
+ } else {
38
+ exampleObj[key] = {};
39
+ }
40
+ }
41
+
42
+ return exampleObj;
43
+ } catch (e) {
44
+ if (debug) {
45
+ console.error('[DEBUG] generateExampleFromSchema: Failed to parse schema:', e.message);
46
+ }
47
+ return null;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Generate schema instructions for AI to follow
53
+ * @param {Object|string} schema - JSON schema object or string
54
+ * @param {Object} options - Options for generation
55
+ * @param {boolean} [options.debug=false] - Enable debug logging
56
+ * @returns {string} - Formatted schema instructions
57
+ */
58
+ export function generateSchemaInstructions(schema, options = {}) {
59
+ const { debug = false } = options;
60
+
61
+ let instructions = '\n\nIMPORTANT: When you provide your final answer using attempt_completion, you MUST format it as valid JSON matching this schema:\n\n';
62
+
63
+ try {
64
+ const parsedSchema = typeof schema === 'string' ? JSON.parse(schema) : schema;
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
+ } catch (e) {
73
+ if (debug) {
74
+ console.error('[DEBUG] generateSchemaInstructions: Failed to parse schema:', e.message);
75
+ }
76
+ instructions += `${schema}\n\n`;
77
+ }
78
+
79
+ instructions += 'Your response inside attempt_completion must be ONLY valid JSON - no plain text, no explanations, no markdown.';
80
+
81
+ return instructions;
82
+ }
83
+
10
84
  /**
11
85
  * Recursively apply additionalProperties: false to all object schemas
12
86
  * This ensures strict validation at all nesting levels
@@ -100,6 +174,13 @@ export function decodeHtmlEntities(text) {
100
174
  /**
101
175
  * Clean AI response by extracting JSON content when response contains JSON
102
176
  * Only processes responses that contain JSON structures { or [
177
+ *
178
+ * NOTE: This function handles both JSON extraction AND content stripping.
179
+ * Future improvement: Consider splitting into separate functions for better separation of concerns:
180
+ * - extractJsonContent() - Find and extract JSON from response
181
+ * - stripNonJsonContent() - Remove explanatory text while preserving validation-relevant content
182
+ * This would allow validation to run on full responses before content is discarded.
183
+ *
103
184
  * @param {string} response - Raw AI response
104
185
  * @returns {string} - Cleaned response with JSON boundaries extracted if applicable
105
186
  */
@@ -59,10 +59,30 @@ export function extractThinkingContent(xmlString) {
59
59
  export function checkAttemptCompleteRecovery(cleanedXmlString, validTools = []) {
60
60
  // Check for <attempt_completion> with content (with or without closing tag)
61
61
  // This handles: "<attempt_completion>content" or "<attempt_completion>content</attempt_completion>"
62
- const attemptCompletionMatch = cleanedXmlString.match(/<attempt_completion>([\s\S]*?)(?:<\/attempt_completion>|$)/);
63
- if (attemptCompletionMatch) {
64
- const content = attemptCompletionMatch[1].trim();
65
- const hasClosingTag = cleanedXmlString.includes('</attempt_completion>');
62
+
63
+ // IMPORTANT: Use greedy match ([\s\S]*) instead of non-greedy ([\s\S]*?) to handle cases
64
+ // where the content contains the string "</attempt_completion>" (e.g., in regex patterns or code examples).
65
+ // We want to find the LAST occurrence of </attempt_completion>, not the first one.
66
+ const openTagIndex = cleanedXmlString.indexOf('<attempt_completion>');
67
+ if (openTagIndex !== -1) {
68
+ const afterOpenTag = cleanedXmlString.substring(openTagIndex + '<attempt_completion>'.length);
69
+ const closeTagIndex = cleanedXmlString.lastIndexOf('</attempt_completion>');
70
+
71
+ let content;
72
+ let hasClosingTag = false;
73
+
74
+ if (closeTagIndex !== -1 && closeTagIndex >= openTagIndex + '<attempt_completion>'.length) {
75
+ // Found a closing tag at or after the opening tag - extract content between them
76
+ content = cleanedXmlString.substring(
77
+ openTagIndex + '<attempt_completion>'.length,
78
+ closeTagIndex
79
+ ).trim();
80
+ hasClosingTag = true;
81
+ } else {
82
+ // No closing tag - use content from opening tag to end of string
83
+ content = afterOpenTag.trim();
84
+ hasClosingTag = false;
85
+ }
66
86
 
67
87
  if (content) {
68
88
  // If there's content after the tag, use it as the result
@@ -384,7 +384,22 @@ export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
384
384
  continue; // Tool not found, try next tool
385
385
  }
386
386
 
387
- let closeIndex = xmlString.indexOf(closeTag, openIndex + openTag.length);
387
+ // For attempt_completion, use lastIndexOf to find the LAST occurrence of closing tag
388
+ // This prevents issues where the content contains the closing tag string (e.g., in regex patterns)
389
+ // For other tools, use indexOf from the opening tag position
390
+ let closeIndex;
391
+ if (toolName === 'attempt_completion') {
392
+ // Find the last occurrence of the closing tag in the entire string
393
+ // This assumes attempt_completion doesn't have nested tags of the same name
394
+ closeIndex = xmlString.lastIndexOf(closeTag);
395
+ // Make sure the closing tag is after the opening tag
396
+ if (closeIndex !== -1 && closeIndex <= openIndex + openTag.length) {
397
+ closeIndex = -1; // Invalid, treat as no closing tag
398
+ }
399
+ } else {
400
+ closeIndex = xmlString.indexOf(closeTag, openIndex + openTag.length);
401
+ }
402
+
388
403
  let hasClosingTag = closeIndex !== -1;
389
404
 
390
405
  // If no closing tag found, use content until end of string