@swizzy_ai/kit 1.0.4 → 1.0.5
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.
- package/dist/core/wizard/bungee/builder.d.ts +13 -2
- package/dist/core/wizard/bungee/builder.d.ts.map +1 -1
- package/dist/core/wizard/bungee/executor.d.ts +1 -1
- package/dist/core/wizard/bungee/executor.d.ts.map +1 -1
- package/dist/core/wizard/bungee/types.d.ts +6 -1
- package/dist/core/wizard/bungee/types.d.ts.map +1 -1
- package/dist/core/wizard/steps/base.d.ts +4 -2
- package/dist/core/wizard/steps/base.d.ts.map +1 -1
- package/dist/core/wizard/steps/text.d.ts +1 -0
- package/dist/core/wizard/steps/text.d.ts.map +1 -1
- package/dist/core/wizard/wizard.d.ts +13 -8
- package/dist/core/wizard/wizard.d.ts.map +1 -1
- package/dist/index.d.ts +37 -13
- package/dist/index.js +270 -173
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -47,6 +47,7 @@ class Step {
|
|
|
47
47
|
this.beforeRun = config.beforeRun;
|
|
48
48
|
this.afterRun = config.afterRun;
|
|
49
49
|
this.model = config.model;
|
|
50
|
+
this.stream = config.stream ?? true; // Default to streaming enabled
|
|
50
51
|
}
|
|
51
52
|
validate(data) {
|
|
52
53
|
const result = this.schema.safeParse(data);
|
|
@@ -55,8 +56,8 @@ class Step {
|
|
|
55
56
|
}
|
|
56
57
|
return result.data;
|
|
57
58
|
}
|
|
58
|
-
getContext(workflowContext) {
|
|
59
|
-
return this.context ? this.context(workflowContext) : workflowContext;
|
|
59
|
+
async getContext(workflowContext) {
|
|
60
|
+
return this.context ? await this.context(workflowContext) : workflowContext;
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -380,18 +381,27 @@ class BungeeBuilder {
|
|
|
380
381
|
/**
|
|
381
382
|
* Add a single step execution.
|
|
382
383
|
*/
|
|
383
|
-
this.add = (stepId) => {
|
|
384
|
-
this._plan.destinations.push({ type: 'step', targetId: stepId });
|
|
384
|
+
this.add = (stepId, config) => {
|
|
385
|
+
this._plan.destinations.push({ type: 'step', targetId: stepId, config });
|
|
385
386
|
return this;
|
|
386
387
|
};
|
|
387
388
|
/**
|
|
388
389
|
* Add multiple executions based on count with config function.
|
|
389
390
|
*/
|
|
390
|
-
this.batch = (stepId, count, configFn) => {
|
|
391
|
+
this.batch = (stepId, count, configFn, options) => {
|
|
391
392
|
for (let i = 0; i < count; i++) {
|
|
392
|
-
this._plan.destinations.push({ type: 'step', targetId: stepId });
|
|
393
|
+
this._plan.destinations.push({ type: 'step', targetId: stepId, config: configFn(i) });
|
|
394
|
+
}
|
|
395
|
+
// Apply batch-specific options
|
|
396
|
+
if (options?.optimistic !== undefined) {
|
|
397
|
+
this._plan.optimistic = options.optimistic;
|
|
398
|
+
}
|
|
399
|
+
if (options?.returnToAnchor !== undefined) {
|
|
400
|
+
this._plan.returnToAnchor = options.returnToAnchor;
|
|
401
|
+
}
|
|
402
|
+
if (options?.failWizardOnFailure !== undefined) {
|
|
403
|
+
this._plan.failWizardOnFailure = options.failWizardOnFailure;
|
|
393
404
|
}
|
|
394
|
-
this._plan.configFn = configFn;
|
|
395
405
|
return this;
|
|
396
406
|
};
|
|
397
407
|
/**
|
|
@@ -401,6 +411,22 @@ class BungeeBuilder {
|
|
|
401
411
|
if (options.concurrency !== undefined) {
|
|
402
412
|
this._plan.concurrency = options.concurrency;
|
|
403
413
|
}
|
|
414
|
+
if (options.optimistic !== undefined) {
|
|
415
|
+
this._plan.optimistic = options.optimistic;
|
|
416
|
+
}
|
|
417
|
+
if (options.returnToAnchor !== undefined) {
|
|
418
|
+
this._plan.returnToAnchor = options.returnToAnchor;
|
|
419
|
+
}
|
|
420
|
+
if (options.failWizardOnFailure !== undefined) {
|
|
421
|
+
this._plan.failWizardOnFailure = options.failWizardOnFailure;
|
|
422
|
+
}
|
|
423
|
+
return this;
|
|
424
|
+
};
|
|
425
|
+
/**
|
|
426
|
+
* Set completion callback.
|
|
427
|
+
*/
|
|
428
|
+
this.onComplete = (callback) => {
|
|
429
|
+
this._plan.onComplete = callback;
|
|
404
430
|
return this;
|
|
405
431
|
};
|
|
406
432
|
/**
|
|
@@ -428,8 +454,8 @@ class SchemaUtils {
|
|
|
428
454
|
const shape = schema._def.shape();
|
|
429
455
|
const fields = Object.entries(shape).map(([key, fieldSchema]) => {
|
|
430
456
|
const type = this.getSchemaType(fieldSchema);
|
|
431
|
-
|
|
432
|
-
return `${key}: ${type}
|
|
457
|
+
this.getXmlExample(key, type);
|
|
458
|
+
return `${key}: ${type}`;
|
|
433
459
|
});
|
|
434
460
|
description = `Object with fields:\n${fields.join('\n')}`;
|
|
435
461
|
}
|
|
@@ -479,8 +505,8 @@ class SchemaUtils {
|
|
|
479
505
|
}
|
|
480
506
|
static getXmlExample(key, type) {
|
|
481
507
|
switch (type) {
|
|
482
|
-
case 'string': return `<${key} tag-category="wizard" type="string">
|
|
483
|
-
case 'number': return `<${key} tag-category="wizard" type="number">
|
|
508
|
+
case 'string': return `<${key} tag-category="wizard" type="string">[your text should be here]`;
|
|
509
|
+
case 'number': return `<${key} tag-category="wizard" type="number">[number should be here]`;
|
|
484
510
|
case 'boolean': return `<${key} tag-category="wizard" type="boolean">true`;
|
|
485
511
|
case 'array': return `<${key} tag-category="wizard" type="array">["item1", "item2"]`;
|
|
486
512
|
default:
|
|
@@ -488,7 +514,7 @@ class SchemaUtils {
|
|
|
488
514
|
const values = type.split(': ')[1].split(', ');
|
|
489
515
|
return `<${key} tag-category="wizard" type="string">${values[0]}`;
|
|
490
516
|
}
|
|
491
|
-
return `<${key} tag-category="wizard" type="object"><subfield type="string">value</subfield>`;
|
|
517
|
+
return `<${key} tag-category="wizard" type="object"><subfield type="string">[text value should be here]</subfield>`;
|
|
492
518
|
}
|
|
493
519
|
}
|
|
494
520
|
static objectToXml(obj, rootName = 'context') {
|
|
@@ -803,24 +829,33 @@ class BungeeExecutor {
|
|
|
803
829
|
// Launch worker
|
|
804
830
|
const workerPromise = this.launchBungeeWorker(plan, i);
|
|
805
831
|
activeWorkers.add(workerPromise);
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
832
|
+
}
|
|
833
|
+
// Wait for all workers to complete unless optimistic
|
|
834
|
+
if (!plan.optimistic) {
|
|
835
|
+
try {
|
|
836
|
+
await Promise.all(activeWorkers);
|
|
837
|
+
}
|
|
838
|
+
catch (error) {
|
|
839
|
+
if (plan.failWizardOnFailure !== false) { // Default true
|
|
840
|
+
throw error; // Re-throw to stop wizard
|
|
814
841
|
}
|
|
842
|
+
// If failWizardOnFailure is false, just log and continue
|
|
843
|
+
console.error(`Bungee plan ${plan.id} had failures but continuing:`, error);
|
|
815
844
|
}
|
|
816
845
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
846
|
+
if (plan.onComplete) {
|
|
847
|
+
return plan.onComplete(this.wizard);
|
|
848
|
+
}
|
|
849
|
+
if (plan.returnToAnchor !== false) { // Default true
|
|
850
|
+
console.log(`✅ Bungee plan ${plan.id} completed, returning to anchor ${plan.anchorId}`);
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
console.log(`✅ Bungee plan ${plan.id} completed, proceeding to next step`);
|
|
854
|
+
}
|
|
820
855
|
}
|
|
821
856
|
async launchBungeeWorker(plan, index) {
|
|
822
857
|
const destination = plan.destinations[index];
|
|
823
|
-
const telescope =
|
|
858
|
+
const telescope = destination.config || {};
|
|
824
859
|
const workerId = `${plan.id}_${destination.targetId}_${index}_${Date.now()}`;
|
|
825
860
|
const telescopeContext = this.createTelescopeContext(this.wizard.workflowContext, telescope);
|
|
826
861
|
const promise = this.executeWorkerStep(destination.targetId, telescopeContext);
|
|
@@ -837,10 +872,6 @@ class BungeeExecutor {
|
|
|
837
872
|
try {
|
|
838
873
|
await promise;
|
|
839
874
|
}
|
|
840
|
-
catch (error) {
|
|
841
|
-
console.error(`Bungee worker ${workerId} failed:`, error);
|
|
842
|
-
this.wizard.workflowContext[`${workerId}_error`] = error.message;
|
|
843
|
-
}
|
|
844
875
|
finally {
|
|
845
876
|
// Clean up
|
|
846
877
|
const planWorkers = this.bungeeWorkers.get(plan.id);
|
|
@@ -848,8 +879,10 @@ class BungeeExecutor {
|
|
|
848
879
|
planWorkers.delete(workerId);
|
|
849
880
|
if (planWorkers.size === 0) {
|
|
850
881
|
this.bungeeWorkers.delete(plan.id);
|
|
851
|
-
// Trigger reentry to anchor
|
|
852
|
-
|
|
882
|
+
// Trigger reentry to anchor if configured
|
|
883
|
+
if (plan.returnToAnchor !== false) {
|
|
884
|
+
this.pendingReentry.add(plan.anchorId);
|
|
885
|
+
}
|
|
853
886
|
}
|
|
854
887
|
}
|
|
855
888
|
}
|
|
@@ -872,9 +905,7 @@ class BungeeExecutor {
|
|
|
872
905
|
return await step.update(stepData, telescopeContext, actions);
|
|
873
906
|
}
|
|
874
907
|
mergeWorkerResults(updates, telescope) {
|
|
875
|
-
|
|
876
|
-
this.wizard.workflowContext[key] = value;
|
|
877
|
-
});
|
|
908
|
+
this.wizard.updateContext(updates);
|
|
878
909
|
}
|
|
879
910
|
async retriggerAnchor(anchorId) {
|
|
880
911
|
const anchorStep = this.wizard.findStep(anchorId);
|
|
@@ -1152,11 +1183,7 @@ class Wizard {
|
|
|
1152
1183
|
this.currentStepIndex++;
|
|
1153
1184
|
return true;
|
|
1154
1185
|
default:
|
|
1155
|
-
if (this.
|
|
1156
|
-
await this.bungeeExecutor.executeBungeePlan(signal.plan);
|
|
1157
|
-
return true;
|
|
1158
|
-
}
|
|
1159
|
-
else if (this.isStringSignal(signal) && signal.startsWith('GOTO ')) {
|
|
1186
|
+
if (this.isStringSignal(signal) && signal.startsWith('GOTO ')) {
|
|
1160
1187
|
const targetStepId = signal.substring(5);
|
|
1161
1188
|
const targetIndex = this.findStepIndex(targetStepId);
|
|
1162
1189
|
if (targetIndex !== -1) {
|
|
@@ -1167,6 +1194,28 @@ class Wizard {
|
|
|
1167
1194
|
}
|
|
1168
1195
|
return true;
|
|
1169
1196
|
}
|
|
1197
|
+
else if (this.isBungeeJumpSignal(signal)) {
|
|
1198
|
+
try {
|
|
1199
|
+
const result = await this.bungeeExecutor.executeBungeePlan(signal.plan);
|
|
1200
|
+
if (result) {
|
|
1201
|
+
return await this.handleFlowControlSignal(result);
|
|
1202
|
+
}
|
|
1203
|
+
if (signal.plan.returnToAnchor === false) {
|
|
1204
|
+
this.currentStepIndex++; // Proceed to next step when not returning to anchor
|
|
1205
|
+
}
|
|
1206
|
+
return true;
|
|
1207
|
+
}
|
|
1208
|
+
catch (error) {
|
|
1209
|
+
console.error('Bungee plan failed:', error);
|
|
1210
|
+
this.workflowContext[`bungee_error`] = error.message;
|
|
1211
|
+
if (signal.plan.failWizardOnFailure !== false) { // Default true
|
|
1212
|
+
this.isRunning = false; // Stop the wizard on bungee failure
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
1215
|
+
// If failWizardOnFailure is false, continue
|
|
1216
|
+
return true;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1170
1219
|
}
|
|
1171
1220
|
return true;
|
|
1172
1221
|
}
|
|
@@ -1324,7 +1373,7 @@ class Wizard {
|
|
|
1324
1373
|
instruction: step.instruction,
|
|
1325
1374
|
timestamp: stepStartTime
|
|
1326
1375
|
});
|
|
1327
|
-
const stepContext = step.getContext(this.contextManager.getContext());
|
|
1376
|
+
const stepContext = await step.getContext(this.contextManager.getContext());
|
|
1328
1377
|
let processedInstruction = step.instruction;
|
|
1329
1378
|
if (step.contextType === 'template' || step.contextType === 'both') {
|
|
1330
1379
|
processedInstruction = this.applyTemplate(step.instruction, stepContext);
|
|
@@ -1519,124 +1568,98 @@ class Wizard {
|
|
|
1519
1568
|
* Uses streaming for regular steps to provide real-time parsing and UI updates.
|
|
1520
1569
|
* TextStep and ComputeStep use different approaches (non-streaming).
|
|
1521
1570
|
*/
|
|
1571
|
+
/**
|
|
1572
|
+
* Generates data for a wizard step by calling the LLM.
|
|
1573
|
+
* CLEANED VERSION: Separates formatting rules from logic to prevent hallucination.
|
|
1574
|
+
*/
|
|
1522
1575
|
async generateStepData(step, stepContext) {
|
|
1523
1576
|
const systemContext = this.systemPrompt ? `${this.systemPrompt}\n\n` : '';
|
|
1577
|
+
// Build context strings
|
|
1524
1578
|
const errorContext = this.workflowContext[`${step.id}_error`] ?
|
|
1525
|
-
`\n\
|
|
1579
|
+
`\n\n!!! PREVIOUS ERROR (Attempt ${this.workflowContext[`${step.id}_retryCount`] || 1}) !!!\nThe previous output caused this error: ${this.workflowContext[`${step.id}_error`]}\nYOU MUST FIX THIS.` : '';
|
|
1526
1580
|
let processedInstruction = step.instruction;
|
|
1527
1581
|
if (step.contextType === 'template' || step.contextType === 'both') {
|
|
1528
1582
|
processedInstruction = this.applyTemplate(step.instruction, stepContext);
|
|
1529
1583
|
}
|
|
1530
|
-
this.log(() => `Processed instruction for step ${step.id}: ${processedInstruction}`);
|
|
1531
1584
|
let contextSection = '';
|
|
1532
1585
|
if (step.contextType === 'xml' || step.contextType === 'both' || !step.contextType) {
|
|
1533
|
-
contextSection = `\n\
|
|
1586
|
+
contextSection = `\n\n### CURRENT CONTEXT ###\n${this.objectToXml(stepContext)}`;
|
|
1534
1587
|
}
|
|
1535
|
-
|
|
1588
|
+
// --- Text Step Handling ---
|
|
1536
1589
|
if (step instanceof TextStep) {
|
|
1537
|
-
const prompt = `${systemContext}
|
|
1590
|
+
const prompt = `${systemContext}
|
|
1591
|
+
TASK: Generate content for step "${step.id}".
|
|
1538
1592
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1593
|
+
INSTRUCTION:
|
|
1594
|
+
${processedInstruction}
|
|
1595
|
+
${contextSection}
|
|
1596
|
+
${errorContext}
|
|
1541
1597
|
|
|
1598
|
+
OUTPUT:
|
|
1542
1599
|
Generate the text response now.`;
|
|
1543
|
-
this.log(() => `Full prompt for step ${step.id}: ${prompt}`);
|
|
1544
1600
|
let fullText = '';
|
|
1545
|
-
|
|
1601
|
+
console.log("full prompt", prompt);
|
|
1602
|
+
const result = await this.llmClient.complete({
|
|
1546
1603
|
prompt,
|
|
1547
1604
|
model: step.model,
|
|
1548
1605
|
maxTokens: 1000,
|
|
1549
1606
|
temperature: 0.3,
|
|
1550
|
-
stream:
|
|
1607
|
+
stream: step.stream,
|
|
1551
1608
|
onChunk: (chunk) => {
|
|
1552
1609
|
fullText += chunk;
|
|
1553
|
-
|
|
1554
|
-
this.events.emit('step:chunk', {
|
|
1555
|
-
stepId: step.id,
|
|
1556
|
-
chunk: chunk,
|
|
1557
|
-
timestamp: Date.now()
|
|
1558
|
-
});
|
|
1610
|
+
this.events.emit('step:chunk', { stepId: step.id, chunk, timestamp: Date.now() });
|
|
1559
1611
|
},
|
|
1560
1612
|
onUsage: this.usageTracker.updateUsage.bind(this.usageTracker)
|
|
1561
1613
|
});
|
|
1562
|
-
|
|
1563
|
-
|
|
1614
|
+
if (result.text) {
|
|
1615
|
+
fullText = result.text;
|
|
1616
|
+
}
|
|
1564
1617
|
return fullText;
|
|
1565
1618
|
}
|
|
1566
|
-
//
|
|
1567
|
-
// This allows real-time processing of LLM responses as they arrive
|
|
1619
|
+
// --- Regular XML Step Handling ---
|
|
1568
1620
|
const parser = this.createStreamingXmlParser();
|
|
1569
1621
|
let latestResult = {};
|
|
1570
1622
|
const schemaDescription = SchemaUtils.describeSchema(step.schema, step.id);
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
SCHEMA REQUIREMENTS:
|
|
1577
|
-
${schemaDescription}
|
|
1578
|
-
|
|
1579
|
-
REQUIRED OUTPUT FORMAT:
|
|
1580
|
-
Return a plain XML response with a root <response> tag.
|
|
1581
|
-
CRITICAL: Every field MUST include tag-category="wizard" attribute. This is MANDATORY.
|
|
1582
|
-
Every field MUST also include a type attribute (e.g., type="string", type="number", type="boolean", type="array").
|
|
1623
|
+
// CLEANER PROMPT STRUCTURE
|
|
1624
|
+
const prompt = `${systemContext}
|
|
1625
|
+
=== GOAL ===
|
|
1626
|
+
You are an intelligent agent executing step: "${step.id}".
|
|
1627
|
+
Your task is to generate data that satisfies the INSTRUCTION below based on the CONTEXT.
|
|
1583
1628
|
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
- NO line breaks inside arrays
|
|
1589
|
-
- Example: <items tag-category="wizard" type="array">["apple", "banana", "orange"]
|
|
1629
|
+
=== INSTRUCTION ===
|
|
1630
|
+
${processedInstruction}
|
|
1631
|
+
${contextSection}
|
|
1632
|
+
${errorContext}
|
|
1590
1633
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
- Only fields marked with tag-category="wizard" will be parsed
|
|
1634
|
+
=== RESPONSE FORMAT ===
|
|
1635
|
+
You must output a VALID XML object inside a <response> tag.
|
|
1636
|
+
1. Every field must have: tag-category="wizard" and a type attribute (string, number, boolean, array).
|
|
1637
|
+
2. Arrays must be single-line JSON: <tags tag-category="wizard" type="array">["a", "b"]
|
|
1596
1638
|
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
<name tag-category="wizard" type="string">John Smith
|
|
1600
|
-
<age tag-category="wizard" type="number">25
|
|
1601
|
-
<tags tag-category="wizard" type="array">["a", "b", "c"]
|
|
1602
|
-
</response>
|
|
1639
|
+
=== SCHEMA DEFINITION ===
|
|
1640
|
+
${schemaDescription}
|
|
1603
1641
|
|
|
1604
|
-
|
|
1642
|
+
*** CRITICAL RULES ***
|
|
1643
|
+
1. Do NOT copy values from the schema definition or examples above.
|
|
1644
|
+
2. Generate NEW values based strictly on the "INSTRUCTION" and "CURRENT CONTEXT".
|
|
1645
|
+
3. If the instruction implies a selection (like an ID), ensure the ID exists in the Context.
|
|
1605
1646
|
|
|
1606
|
-
Generate the XML response now.`;
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
await this.llmClient.complete({
|
|
1647
|
+
Generate the XML <response> now.`;
|
|
1648
|
+
// console.log(prompt); // Uncomment to debug the cleaner prompt
|
|
1649
|
+
step.stream !== false;
|
|
1650
|
+
const result = await this.llmClient.complete({
|
|
1611
1651
|
prompt,
|
|
1612
1652
|
model: step.model,
|
|
1613
1653
|
maxTokens: 1000,
|
|
1614
|
-
temperature: 0.3,
|
|
1615
|
-
stream:
|
|
1654
|
+
temperature: 0.3, // Lower temp for precision
|
|
1655
|
+
stream: step.stream,
|
|
1616
1656
|
onChunk: (chunk) => {
|
|
1617
|
-
|
|
1618
|
-
this.events.emit('step:chunk', {
|
|
1619
|
-
stepId: step.id,
|
|
1620
|
-
chunk: chunk,
|
|
1621
|
-
timestamp: Date.now()
|
|
1622
|
-
});
|
|
1623
|
-
// Process each incoming chunk through the streaming parser
|
|
1657
|
+
this.events.emit('step:chunk', { stepId: step.id, chunk, timestamp: Date.now() });
|
|
1624
1658
|
const parseResult = parser.push(chunk);
|
|
1625
1659
|
if (parseResult && !parseResult.done) {
|
|
1626
1660
|
latestResult = parseResult.result;
|
|
1627
|
-
|
|
1628
|
-
this.
|
|
1629
|
-
stepId: step.id,
|
|
1630
|
-
data: latestResult,
|
|
1631
|
-
timestamp: Date.now()
|
|
1632
|
-
});
|
|
1633
|
-
// Stream partial results out to connected clients via WebSocket
|
|
1634
|
-
// This broadcasts real-time updates to UI and external consumers
|
|
1635
|
-
this.visualizationManager.sendStepUpdate({
|
|
1636
|
-
stepId: step.id,
|
|
1637
|
-
status: 'streaming',
|
|
1638
|
-
data: latestResult
|
|
1639
|
-
});
|
|
1661
|
+
this.events.emit('step:streaming', { stepId: step.id, data: latestResult, timestamp: Date.now() });
|
|
1662
|
+
this.visualizationManager.sendStepUpdate({ stepId: step.id, status: 'streaming', data: latestResult });
|
|
1640
1663
|
}
|
|
1641
1664
|
else if (parseResult?.done) {
|
|
1642
1665
|
latestResult = parseResult.result;
|
|
@@ -1644,20 +1667,19 @@ Generate the XML response now.`;
|
|
|
1644
1667
|
},
|
|
1645
1668
|
onUsage: this.usageTracker.updateUsage.bind(this.usageTracker)
|
|
1646
1669
|
});
|
|
1647
|
-
|
|
1648
|
-
|
|
1670
|
+
if (result.text) {
|
|
1671
|
+
latestResult = result.text;
|
|
1672
|
+
}
|
|
1649
1673
|
try {
|
|
1650
1674
|
return step.validate(latestResult);
|
|
1651
1675
|
}
|
|
1652
1676
|
catch (validationError) {
|
|
1653
|
-
|
|
1677
|
+
// Logic for repair remains the same...
|
|
1654
1678
|
try {
|
|
1655
1679
|
const repairedData = await this.repairSchemaData(latestResult, step.schema, validationError.message, step.id);
|
|
1656
|
-
this.log(() => `Repaired data for step ${step.id}: ${JSON.stringify(repairedData)}`);
|
|
1657
1680
|
return step.validate(repairedData);
|
|
1658
1681
|
}
|
|
1659
1682
|
catch (repairError) {
|
|
1660
|
-
this.log(() => `Repair failed for step ${step.id}: ${repairError.message}`);
|
|
1661
1683
|
return { __validationFailed: true, error: validationError.message };
|
|
1662
1684
|
}
|
|
1663
1685
|
}
|
|
@@ -1700,16 +1722,17 @@ Fix the data to match the schema and generate the XML response now.`;
|
|
|
1700
1722
|
return repairedJsonData;
|
|
1701
1723
|
}
|
|
1702
1724
|
/**
|
|
1703
|
-
* Creates
|
|
1725
|
+
* Creates an improved streaming XML parser for incremental processing of LLM responses.
|
|
1704
1726
|
*
|
|
1705
|
-
* This parser
|
|
1706
|
-
* fields marked with tag-category="wizard"
|
|
1727
|
+
* This parser is designed to handle partial chunks robustly and provides better error recovery.
|
|
1728
|
+
* It processes XML chunks as they arrive, extracting fields marked with tag-category="wizard".
|
|
1707
1729
|
*
|
|
1708
|
-
* Key
|
|
1709
|
-
* -
|
|
1710
|
-
* -
|
|
1711
|
-
* -
|
|
1712
|
-
* -
|
|
1730
|
+
* Key improvements:
|
|
1731
|
+
* - Better partial tag handling
|
|
1732
|
+
* - More robust regex matching
|
|
1733
|
+
* - Improved buffer management
|
|
1734
|
+
* - Better error recovery for malformed chunks
|
|
1735
|
+
* - State machine approach for parsing
|
|
1713
1736
|
*
|
|
1714
1737
|
* @returns An object with a push method that accepts text chunks and returns parse results
|
|
1715
1738
|
*/
|
|
@@ -1718,57 +1741,131 @@ Fix the data to match the schema and generate the XML response now.`;
|
|
|
1718
1741
|
let inResponse = false; // Tracks if we've entered the <response> tag
|
|
1719
1742
|
const result = {}; // The final parsed JSON object
|
|
1720
1743
|
let currentField = null; // Currently parsing field
|
|
1744
|
+
let parseErrors = 0; // Track consecutive parse errors
|
|
1721
1745
|
return {
|
|
1722
1746
|
push: (chunk) => {
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
inResponse
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
if (currentField) {
|
|
1736
|
-
result[currentField.name] = this.parseValueByType(currentField.content.trim(), currentField.type);
|
|
1747
|
+
try {
|
|
1748
|
+
buffer += chunk;
|
|
1749
|
+
// Wait for <response> tag to start parsing
|
|
1750
|
+
if (!inResponse) {
|
|
1751
|
+
const responseStart = buffer.indexOf('<response>');
|
|
1752
|
+
if (responseStart !== -1) {
|
|
1753
|
+
inResponse = true;
|
|
1754
|
+
buffer = buffer.slice(responseStart + 10); // Remove <response> from buffer
|
|
1755
|
+
}
|
|
1756
|
+
else {
|
|
1757
|
+
return null; // Still waiting for response start
|
|
1758
|
+
}
|
|
1737
1759
|
}
|
|
1738
|
-
//
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1760
|
+
// Process buffer for wizard tags
|
|
1761
|
+
let processedSomething = false;
|
|
1762
|
+
// Continue processing while we have data
|
|
1763
|
+
while (buffer.length > 0) {
|
|
1764
|
+
// If we have a current field, try to accumulate content
|
|
1765
|
+
if (currentField) {
|
|
1766
|
+
// Look for the next wizard tag or end of response
|
|
1767
|
+
const nextWizardTag = buffer.match(/<\w+\s+[^>]*tag-category=["']wizard["'][^>]*>/);
|
|
1768
|
+
const responseEnd = buffer.indexOf('</response>');
|
|
1769
|
+
if (nextWizardTag && nextWizardTag.index !== undefined) {
|
|
1770
|
+
// Found next tag, finalize current field
|
|
1771
|
+
const contentEnd = nextWizardTag.index;
|
|
1772
|
+
currentField.content += buffer.slice(0, contentEnd);
|
|
1773
|
+
result[currentField.name] = this.parseValueByType(currentField.content.trim(), currentField.type);
|
|
1774
|
+
currentField = null;
|
|
1775
|
+
buffer = buffer.slice(contentEnd);
|
|
1776
|
+
processedSomething = true;
|
|
1777
|
+
}
|
|
1778
|
+
else if (responseEnd !== -1) {
|
|
1779
|
+
// End of response, finalize current field
|
|
1780
|
+
currentField.content += buffer.slice(0, responseEnd);
|
|
1781
|
+
result[currentField.name] = this.parseValueByType(currentField.content.trim(), currentField.type);
|
|
1782
|
+
return { done: true, result }; // Parsing complete
|
|
1783
|
+
}
|
|
1784
|
+
else {
|
|
1785
|
+
// No complete field yet, but we might have a partial tag at the end
|
|
1786
|
+
// Check if buffer ends with a partial wizard tag
|
|
1787
|
+
const partialTagMatch = buffer.match(/<\w+\s+[^>]*tag-category=["']wizard["'][^>]*$/);
|
|
1788
|
+
if (partialTagMatch) {
|
|
1789
|
+
// Buffer ends with partial tag, keep it for next chunk
|
|
1790
|
+
break;
|
|
1791
|
+
}
|
|
1792
|
+
else {
|
|
1793
|
+
// Safe to accumulate entire buffer
|
|
1794
|
+
currentField.content += buffer;
|
|
1795
|
+
buffer = '';
|
|
1796
|
+
processedSomething = true;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
else {
|
|
1801
|
+
// No current field, look for a new wizard tag
|
|
1802
|
+
const tagMatch = buffer.match(Wizard.WIZARD_TAG_PATTERN);
|
|
1803
|
+
if (tagMatch && tagMatch.index === 0) {
|
|
1804
|
+
// Tag starts at beginning of buffer
|
|
1805
|
+
const typeMatch = tagMatch[2].match(/type=["']([^"']+)["']/);
|
|
1806
|
+
currentField = {
|
|
1807
|
+
name: tagMatch[1],
|
|
1808
|
+
type: typeMatch?.[1]?.toLowerCase() || 'string',
|
|
1809
|
+
content: ''
|
|
1810
|
+
};
|
|
1811
|
+
buffer = buffer.slice(tagMatch[0].length);
|
|
1812
|
+
processedSomething = true;
|
|
1813
|
+
}
|
|
1814
|
+
else if (tagMatch && tagMatch.index !== undefined && tagMatch.index > 0) {
|
|
1815
|
+
// Tag exists but not at start - might be partial
|
|
1816
|
+
const partialTagMatch = buffer.match(/<\w+\s+[^>]*tag-category=["']wizard["'][^>]*$/);
|
|
1817
|
+
if (partialTagMatch) {
|
|
1818
|
+
// Buffer ends with partial tag, wait for more data
|
|
1819
|
+
break;
|
|
1820
|
+
}
|
|
1821
|
+
else {
|
|
1822
|
+
// Tag is in middle, process up to it
|
|
1823
|
+
// This shouldn't happen in well-formed XML, but handle gracefully
|
|
1824
|
+
buffer = buffer.slice(tagMatch.index);
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
else {
|
|
1829
|
+
// No wizard tag found
|
|
1830
|
+
if (buffer.includes('</response>')) {
|
|
1831
|
+
// End of response without finalizing a field
|
|
1832
|
+
return { done: true, result };
|
|
1833
|
+
}
|
|
1834
|
+
// Check for partial tag at end
|
|
1835
|
+
const partialTagMatch = buffer.match(/<\w+\s+[^>]*tag-category=["']wizard["'][^>]*$/);
|
|
1836
|
+
if (partialTagMatch) {
|
|
1837
|
+
break; // Wait for more data
|
|
1838
|
+
}
|
|
1839
|
+
// No partial tag, buffer might contain non-wizard content
|
|
1840
|
+
// This is unusual but we'll keep it for now
|
|
1841
|
+
break;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1756
1844
|
}
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
currentField.content += buffer.slice(0, endIndex);
|
|
1761
|
-
result[currentField.name] = this.parseValueByType(currentField.content.trim(), currentField.type);
|
|
1762
|
-
return { done: true, result }; // Parsing complete
|
|
1845
|
+
// Reset error counter on successful processing
|
|
1846
|
+
if (processedSomething) {
|
|
1847
|
+
parseErrors = 0;
|
|
1763
1848
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1849
|
+
// Return partial result for UI updates
|
|
1850
|
+
return { done: false, result: { ...result } };
|
|
1851
|
+
}
|
|
1852
|
+
catch (error) {
|
|
1853
|
+
parseErrors++;
|
|
1854
|
+
console.warn(`Streaming XML parse error (attempt ${parseErrors}):`, error instanceof Error ? error.message : String(error));
|
|
1855
|
+
// If we have too many consecutive errors, try to recover
|
|
1856
|
+
if (parseErrors > 3) {
|
|
1857
|
+
console.error('Too many parse errors, attempting recovery');
|
|
1858
|
+
// Try to find next valid wizard tag and restart from there
|
|
1859
|
+
const recoveryMatch = buffer.match(/<\w+\s+[^>]*tag-category=["']wizard["'][^>]*>/);
|
|
1860
|
+
if (recoveryMatch && recoveryMatch.index !== undefined && recoveryMatch.index > 0) {
|
|
1861
|
+
buffer = buffer.slice(recoveryMatch.index);
|
|
1862
|
+
parseErrors = 0; // Reset error counter
|
|
1863
|
+
return { done: false, result: { ...result } };
|
|
1864
|
+
}
|
|
1768
1865
|
}
|
|
1866
|
+
// Return current result even with errors
|
|
1867
|
+
return { done: false, result: { ...result } };
|
|
1769
1868
|
}
|
|
1770
|
-
// Return partial result for UI updates
|
|
1771
|
-
return { done: false, result: { ...result } };
|
|
1772
1869
|
}
|
|
1773
1870
|
};
|
|
1774
1871
|
}
|