@swizzy_ai/kit 1.0.3 → 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/logger.d.ts +1 -0
- package/dist/core/wizard/logger.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 +300 -182
- 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);
|
|
@@ -893,19 +924,38 @@ class BungeeExecutor {
|
|
|
893
924
|
|
|
894
925
|
class Logger {
|
|
895
926
|
constructor(id) {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
927
|
+
this.isFileSystemAvailable = false;
|
|
928
|
+
// Try to set up file logging, but gracefully handle environments without file system
|
|
929
|
+
try {
|
|
930
|
+
const logsDir = path__namespace.join(process.cwd(), '.wizard');
|
|
931
|
+
// Check if we can access the directory
|
|
932
|
+
try {
|
|
933
|
+
fs__namespace.accessSync(logsDir, fs__namespace.constants.F_OK);
|
|
934
|
+
}
|
|
935
|
+
catch {
|
|
936
|
+
// Directory doesn't exist, try to create it
|
|
937
|
+
fs__namespace.mkdirSync(logsDir, { recursive: true });
|
|
938
|
+
}
|
|
939
|
+
// If we get here, file system is available
|
|
940
|
+
this.isFileSystemAvailable = true;
|
|
941
|
+
this.logFilePath = path__namespace.join(logsDir, `${id}.log`);
|
|
942
|
+
}
|
|
943
|
+
catch (error) {
|
|
944
|
+
// File system operations failed (e.g., Cloudflare Workers, restricted environments)
|
|
945
|
+
this.isFileSystemAvailable = false;
|
|
946
|
+
// Don't log here to avoid recursion - console logging will be used as fallback
|
|
900
947
|
}
|
|
901
|
-
this.logFilePath = path__namespace.join(logsDir, `${id}.log`);
|
|
902
948
|
}
|
|
903
949
|
log(messageOrFn) {
|
|
904
|
-
if (!this.logFilePath)
|
|
905
|
-
return; // Early exit if logging disabled
|
|
906
950
|
const message = typeof messageOrFn === 'function' ? messageOrFn() : messageOrFn;
|
|
907
951
|
const content = `${new Date().toISOString()}: ${message}\n`;
|
|
908
|
-
this.
|
|
952
|
+
if (this.isFileSystemAvailable && this.logFilePath) {
|
|
953
|
+
this.appendToFile(content);
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
// Fallback to console logging when file system is not available
|
|
957
|
+
console.log('Wizard log:', content.trim());
|
|
958
|
+
}
|
|
909
959
|
}
|
|
910
960
|
appendToFile(content) {
|
|
911
961
|
if (!this.logFilePath)
|
|
@@ -918,7 +968,7 @@ class Logger {
|
|
|
918
968
|
}
|
|
919
969
|
}
|
|
920
970
|
async getLog() {
|
|
921
|
-
if (!this.logFilePath || !fs__namespace.existsSync(this.logFilePath))
|
|
971
|
+
if (!this.isFileSystemAvailable || !this.logFilePath || !fs__namespace.existsSync(this.logFilePath))
|
|
922
972
|
return '';
|
|
923
973
|
try {
|
|
924
974
|
return await fs__namespace.promises.readFile(this.logFilePath, 'utf8');
|
|
@@ -1124,6 +1174,7 @@ class Wizard {
|
|
|
1124
1174
|
this.currentStepIndex++;
|
|
1125
1175
|
return true;
|
|
1126
1176
|
case Wizard.STOP:
|
|
1177
|
+
this.isRunning = false; // Stop the entire wizard execution
|
|
1127
1178
|
return false;
|
|
1128
1179
|
case Wizard.RETRY:
|
|
1129
1180
|
return true;
|
|
@@ -1132,11 +1183,7 @@ class Wizard {
|
|
|
1132
1183
|
this.currentStepIndex++;
|
|
1133
1184
|
return true;
|
|
1134
1185
|
default:
|
|
1135
|
-
if (this.
|
|
1136
|
-
await this.bungeeExecutor.executeBungeePlan(signal.plan);
|
|
1137
|
-
return true;
|
|
1138
|
-
}
|
|
1139
|
-
else if (this.isStringSignal(signal) && signal.startsWith('GOTO ')) {
|
|
1186
|
+
if (this.isStringSignal(signal) && signal.startsWith('GOTO ')) {
|
|
1140
1187
|
const targetStepId = signal.substring(5);
|
|
1141
1188
|
const targetIndex = this.findStepIndex(targetStepId);
|
|
1142
1189
|
if (targetIndex !== -1) {
|
|
@@ -1147,10 +1194,33 @@ class Wizard {
|
|
|
1147
1194
|
}
|
|
1148
1195
|
return true;
|
|
1149
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
|
+
}
|
|
1150
1219
|
}
|
|
1151
1220
|
return true;
|
|
1152
1221
|
}
|
|
1153
1222
|
async initializeRun() {
|
|
1223
|
+
// Only wait for UI command if visualization server is actually running
|
|
1154
1224
|
if (this.visualizationServer) {
|
|
1155
1225
|
console.log('🎯 Waiting for UI to start wizard execution...');
|
|
1156
1226
|
this.sendToClients({ type: 'status_update', status: { waitingForStart: true, isStepMode: false } });
|
|
@@ -1303,7 +1373,7 @@ class Wizard {
|
|
|
1303
1373
|
instruction: step.instruction,
|
|
1304
1374
|
timestamp: stepStartTime
|
|
1305
1375
|
});
|
|
1306
|
-
const stepContext = step.getContext(this.contextManager.getContext());
|
|
1376
|
+
const stepContext = await step.getContext(this.contextManager.getContext());
|
|
1307
1377
|
let processedInstruction = step.instruction;
|
|
1308
1378
|
if (step.contextType === 'template' || step.contextType === 'both') {
|
|
1309
1379
|
processedInstruction = this.applyTemplate(step.instruction, stepContext);
|
|
@@ -1498,124 +1568,98 @@ class Wizard {
|
|
|
1498
1568
|
* Uses streaming for regular steps to provide real-time parsing and UI updates.
|
|
1499
1569
|
* TextStep and ComputeStep use different approaches (non-streaming).
|
|
1500
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
|
+
*/
|
|
1501
1575
|
async generateStepData(step, stepContext) {
|
|
1502
1576
|
const systemContext = this.systemPrompt ? `${this.systemPrompt}\n\n` : '';
|
|
1577
|
+
// Build context strings
|
|
1503
1578
|
const errorContext = this.workflowContext[`${step.id}_error`] ?
|
|
1504
|
-
`\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.` : '';
|
|
1505
1580
|
let processedInstruction = step.instruction;
|
|
1506
1581
|
if (step.contextType === 'template' || step.contextType === 'both') {
|
|
1507
1582
|
processedInstruction = this.applyTemplate(step.instruction, stepContext);
|
|
1508
1583
|
}
|
|
1509
|
-
this.log(() => `Processed instruction for step ${step.id}: ${processedInstruction}`);
|
|
1510
1584
|
let contextSection = '';
|
|
1511
1585
|
if (step.contextType === 'xml' || step.contextType === 'both' || !step.contextType) {
|
|
1512
|
-
contextSection = `\n\
|
|
1586
|
+
contextSection = `\n\n### CURRENT CONTEXT ###\n${this.objectToXml(stepContext)}`;
|
|
1513
1587
|
}
|
|
1514
|
-
|
|
1588
|
+
// --- Text Step Handling ---
|
|
1515
1589
|
if (step instanceof TextStep) {
|
|
1516
|
-
const prompt = `${systemContext}
|
|
1590
|
+
const prompt = `${systemContext}
|
|
1591
|
+
TASK: Generate content for step "${step.id}".
|
|
1517
1592
|
|
|
1518
|
-
|
|
1519
|
-
|
|
1593
|
+
INSTRUCTION:
|
|
1594
|
+
${processedInstruction}
|
|
1595
|
+
${contextSection}
|
|
1596
|
+
${errorContext}
|
|
1520
1597
|
|
|
1598
|
+
OUTPUT:
|
|
1521
1599
|
Generate the text response now.`;
|
|
1522
|
-
this.log(() => `Full prompt for step ${step.id}: ${prompt}`);
|
|
1523
1600
|
let fullText = '';
|
|
1524
|
-
|
|
1601
|
+
console.log("full prompt", prompt);
|
|
1602
|
+
const result = await this.llmClient.complete({
|
|
1525
1603
|
prompt,
|
|
1526
1604
|
model: step.model,
|
|
1527
1605
|
maxTokens: 1000,
|
|
1528
1606
|
temperature: 0.3,
|
|
1529
|
-
stream:
|
|
1607
|
+
stream: step.stream,
|
|
1530
1608
|
onChunk: (chunk) => {
|
|
1531
1609
|
fullText += chunk;
|
|
1532
|
-
|
|
1533
|
-
this.events.emit('step:chunk', {
|
|
1534
|
-
stepId: step.id,
|
|
1535
|
-
chunk: chunk,
|
|
1536
|
-
timestamp: Date.now()
|
|
1537
|
-
});
|
|
1610
|
+
this.events.emit('step:chunk', { stepId: step.id, chunk, timestamp: Date.now() });
|
|
1538
1611
|
},
|
|
1539
1612
|
onUsage: this.usageTracker.updateUsage.bind(this.usageTracker)
|
|
1540
1613
|
});
|
|
1541
|
-
|
|
1542
|
-
|
|
1614
|
+
if (result.text) {
|
|
1615
|
+
fullText = result.text;
|
|
1616
|
+
}
|
|
1543
1617
|
return fullText;
|
|
1544
1618
|
}
|
|
1545
|
-
//
|
|
1546
|
-
// This allows real-time processing of LLM responses as they arrive
|
|
1619
|
+
// --- Regular XML Step Handling ---
|
|
1547
1620
|
const parser = this.createStreamingXmlParser();
|
|
1548
1621
|
let latestResult = {};
|
|
1549
1622
|
const schemaDescription = SchemaUtils.describeSchema(step.schema, step.id);
|
|
1550
|
-
|
|
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.
|
|
1551
1628
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1629
|
+
=== INSTRUCTION ===
|
|
1630
|
+
${processedInstruction}
|
|
1631
|
+
${contextSection}
|
|
1632
|
+
${errorContext}
|
|
1554
1633
|
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
Return a plain XML response with a root <response> tag.
|
|
1560
|
-
CRITICAL: Every field MUST include tag-category="wizard" attribute. This is MANDATORY.
|
|
1561
|
-
Every field MUST also include a type attribute (e.g., type="string", type="number", type="boolean", type="array").
|
|
1562
|
-
|
|
1563
|
-
ARRAY FORMATTING RULES (CRITICAL):
|
|
1564
|
-
- Arrays MUST be valid JSON on a SINGLE line
|
|
1565
|
-
- Use double quotes, not single quotes: ["a", "b"] NOT ['a', 'b']
|
|
1566
|
-
- NO trailing commas: ["a", "b"] NOT ["a", "b",]
|
|
1567
|
-
- NO line breaks inside arrays
|
|
1568
|
-
- Example: <items tag-category="wizard" type="array">["apple", "banana", "orange"]
|
|
1569
|
-
|
|
1570
|
-
IMPORTANT PARSING RULES:
|
|
1571
|
-
- Fields with tag-category="wizard" do NOT need closing tags
|
|
1572
|
-
- Content ends when the next tag with tag-category="wizard" begins, OR when </response> is reached
|
|
1573
|
-
- This means you can include ANY content (including code with <>, XML snippets, etc.) without worrying about breaking the parser
|
|
1574
|
-
- 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"]
|
|
1575
1638
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
<name tag-category="wizard" type="string">John Smith
|
|
1579
|
-
<age tag-category="wizard" type="number">25
|
|
1580
|
-
<tags tag-category="wizard" type="array">["a", "b", "c"]
|
|
1581
|
-
</response>
|
|
1639
|
+
=== SCHEMA DEFINITION ===
|
|
1640
|
+
${schemaDescription}
|
|
1582
1641
|
|
|
1583
|
-
|
|
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.
|
|
1584
1646
|
|
|
1585
|
-
Generate the XML response now.`;
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
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({
|
|
1590
1651
|
prompt,
|
|
1591
1652
|
model: step.model,
|
|
1592
1653
|
maxTokens: 1000,
|
|
1593
|
-
temperature: 0.3,
|
|
1594
|
-
stream:
|
|
1654
|
+
temperature: 0.3, // Lower temp for precision
|
|
1655
|
+
stream: step.stream,
|
|
1595
1656
|
onChunk: (chunk) => {
|
|
1596
|
-
|
|
1597
|
-
this.events.emit('step:chunk', {
|
|
1598
|
-
stepId: step.id,
|
|
1599
|
-
chunk: chunk,
|
|
1600
|
-
timestamp: Date.now()
|
|
1601
|
-
});
|
|
1602
|
-
// Process each incoming chunk through the streaming parser
|
|
1657
|
+
this.events.emit('step:chunk', { stepId: step.id, chunk, timestamp: Date.now() });
|
|
1603
1658
|
const parseResult = parser.push(chunk);
|
|
1604
1659
|
if (parseResult && !parseResult.done) {
|
|
1605
1660
|
latestResult = parseResult.result;
|
|
1606
|
-
|
|
1607
|
-
this.
|
|
1608
|
-
stepId: step.id,
|
|
1609
|
-
data: latestResult,
|
|
1610
|
-
timestamp: Date.now()
|
|
1611
|
-
});
|
|
1612
|
-
// Stream partial results out to connected clients via WebSocket
|
|
1613
|
-
// This broadcasts real-time updates to UI and external consumers
|
|
1614
|
-
this.visualizationManager.sendStepUpdate({
|
|
1615
|
-
stepId: step.id,
|
|
1616
|
-
status: 'streaming',
|
|
1617
|
-
data: latestResult
|
|
1618
|
-
});
|
|
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 });
|
|
1619
1663
|
}
|
|
1620
1664
|
else if (parseResult?.done) {
|
|
1621
1665
|
latestResult = parseResult.result;
|
|
@@ -1623,20 +1667,19 @@ Generate the XML response now.`;
|
|
|
1623
1667
|
},
|
|
1624
1668
|
onUsage: this.usageTracker.updateUsage.bind(this.usageTracker)
|
|
1625
1669
|
});
|
|
1626
|
-
|
|
1627
|
-
|
|
1670
|
+
if (result.text) {
|
|
1671
|
+
latestResult = result.text;
|
|
1672
|
+
}
|
|
1628
1673
|
try {
|
|
1629
1674
|
return step.validate(latestResult);
|
|
1630
1675
|
}
|
|
1631
1676
|
catch (validationError) {
|
|
1632
|
-
|
|
1677
|
+
// Logic for repair remains the same...
|
|
1633
1678
|
try {
|
|
1634
1679
|
const repairedData = await this.repairSchemaData(latestResult, step.schema, validationError.message, step.id);
|
|
1635
|
-
this.log(() => `Repaired data for step ${step.id}: ${JSON.stringify(repairedData)}`);
|
|
1636
1680
|
return step.validate(repairedData);
|
|
1637
1681
|
}
|
|
1638
1682
|
catch (repairError) {
|
|
1639
|
-
this.log(() => `Repair failed for step ${step.id}: ${repairError.message}`);
|
|
1640
1683
|
return { __validationFailed: true, error: validationError.message };
|
|
1641
1684
|
}
|
|
1642
1685
|
}
|
|
@@ -1679,16 +1722,17 @@ Fix the data to match the schema and generate the XML response now.`;
|
|
|
1679
1722
|
return repairedJsonData;
|
|
1680
1723
|
}
|
|
1681
1724
|
/**
|
|
1682
|
-
* Creates
|
|
1725
|
+
* Creates an improved streaming XML parser for incremental processing of LLM responses.
|
|
1683
1726
|
*
|
|
1684
|
-
* This parser
|
|
1685
|
-
* 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".
|
|
1686
1729
|
*
|
|
1687
|
-
* Key
|
|
1688
|
-
* -
|
|
1689
|
-
* -
|
|
1690
|
-
* -
|
|
1691
|
-
* -
|
|
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
|
|
1692
1736
|
*
|
|
1693
1737
|
* @returns An object with a push method that accepts text chunks and returns parse results
|
|
1694
1738
|
*/
|
|
@@ -1697,57 +1741,131 @@ Fix the data to match the schema and generate the XML response now.`;
|
|
|
1697
1741
|
let inResponse = false; // Tracks if we've entered the <response> tag
|
|
1698
1742
|
const result = {}; // The final parsed JSON object
|
|
1699
1743
|
let currentField = null; // Currently parsing field
|
|
1744
|
+
let parseErrors = 0; // Track consecutive parse errors
|
|
1700
1745
|
return {
|
|
1701
1746
|
push: (chunk) => {
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
inResponse
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
if (currentField) {
|
|
1715
|
-
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
|
+
}
|
|
1716
1759
|
}
|
|
1717
|
-
//
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
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
|
+
}
|
|
1735
1844
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
currentField.content += buffer.slice(0, endIndex);
|
|
1740
|
-
result[currentField.name] = this.parseValueByType(currentField.content.trim(), currentField.type);
|
|
1741
|
-
return { done: true, result }; // Parsing complete
|
|
1845
|
+
// Reset error counter on successful processing
|
|
1846
|
+
if (processedSomething) {
|
|
1847
|
+
parseErrors = 0;
|
|
1742
1848
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
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
|
+
}
|
|
1747
1865
|
}
|
|
1866
|
+
// Return current result even with errors
|
|
1867
|
+
return { done: false, result: { ...result } };
|
|
1748
1868
|
}
|
|
1749
|
-
// Return partial result for UI updates
|
|
1750
|
-
return { done: false, result: { ...result } };
|
|
1751
1869
|
}
|
|
1752
1870
|
};
|
|
1753
1871
|
}
|