@o-lang/olang 1.0.20 → 1.0.22
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/cli.js +18 -11
- package/package.json +1 -1
- package/src/parser.js +51 -8
- package/src/runtime.js +170 -3
package/cli.js
CHANGED
|
@@ -64,25 +64,32 @@ async function builtInMathResolver(action, context) {
|
|
|
64
64
|
builtInMathResolver.resolverName = 'builtInMathResolver';
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
* Resolver chaining
|
|
67
|
+
* Resolver chaining: returns the FIRST resolver that returns a non-undefined result.
|
|
68
68
|
*/
|
|
69
69
|
function createResolverChain(resolvers, verbose = false) {
|
|
70
|
-
const chain = resolvers.slice();
|
|
71
70
|
const wrapped = async (action, context) => {
|
|
72
|
-
let
|
|
73
|
-
|
|
71
|
+
for (let i = 0; i < resolvers.length; i++) {
|
|
72
|
+
const resolver = resolvers[i];
|
|
74
73
|
try {
|
|
75
|
-
const res = await
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
const res = await resolver(action, context);
|
|
75
|
+
if (res !== undefined) {
|
|
76
|
+
// Store result in context for debugging
|
|
77
|
+
context[`__resolver_${i}`] = res;
|
|
78
|
+
if (verbose) {
|
|
79
|
+
console.log(`[✅ ${resolver.resolverName || 'anonymous'}] handled action`);
|
|
80
|
+
}
|
|
81
|
+
return res;
|
|
82
|
+
}
|
|
78
83
|
} catch (e) {
|
|
79
|
-
console.error(` ❌ Resolver ${
|
|
84
|
+
console.error(` ❌ Resolver ${resolver.resolverName || 'anonymous'} failed:`, e.message);
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
|
-
if (verbose)
|
|
83
|
-
|
|
87
|
+
if (verbose) {
|
|
88
|
+
console.log(`[⏭️] No resolver handled action: "${action}"`);
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
84
91
|
};
|
|
85
|
-
wrapped._chain =
|
|
92
|
+
wrapped._chain = resolvers;
|
|
86
93
|
return wrapped;
|
|
87
94
|
}
|
|
88
95
|
|
package/package.json
CHANGED
package/src/parser.js
CHANGED
|
@@ -29,25 +29,50 @@ function parse(code, fileName = null) {
|
|
|
29
29
|
let line = lines[i];
|
|
30
30
|
|
|
31
31
|
// ✅ FIXED: Match both "Allow resolvers" and "Allowed resolvers"
|
|
32
|
-
const allowMatch = line.match(/^Allow
|
|
32
|
+
const allowMatch = line.match(/^Allow resolvers\s*:\s*$/i);
|
|
33
33
|
if (allowMatch) {
|
|
34
34
|
i++;
|
|
35
|
-
// Read all subsequent non-empty lines until a new top-level section (starts with capital letter)
|
|
36
35
|
while (i < lines.length) {
|
|
37
36
|
const nextLine = lines[i].trim();
|
|
38
|
-
// Stop if line is empty or
|
|
37
|
+
// Stop if line is empty or looks like a new top-level section
|
|
39
38
|
if (nextLine === '' || /^[A-Z][a-z]/.test(nextLine)) {
|
|
40
39
|
break;
|
|
41
40
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
// Strip optional YAML list marker: "- name" → "name"
|
|
42
|
+
const cleanVal = nextLine.replace(/^\-\s*/, '').trim();
|
|
43
|
+
if (cleanVal) {
|
|
44
|
+
workflow.allowedResolvers.push(cleanVal);
|
|
45
|
+
workflow.resolverPolicy.declared.push(cleanVal);
|
|
45
46
|
}
|
|
46
47
|
i++;
|
|
47
48
|
}
|
|
48
49
|
continue;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// ✅ NEW: Parse database Persist steps
|
|
53
|
+
const dbPersistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+db:([^\s]+)$/i);
|
|
54
|
+
if (dbPersistMatch) {
|
|
55
|
+
workflow.steps.push({
|
|
56
|
+
type: 'persist-db',
|
|
57
|
+
source: dbPersistMatch[1].trim(),
|
|
58
|
+
collection: dbPersistMatch[2].trim()
|
|
59
|
+
});
|
|
60
|
+
i++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ✅ Parse file Persist steps
|
|
65
|
+
const persistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+"([^"]+)"$/i);
|
|
66
|
+
if (persistMatch) {
|
|
67
|
+
workflow.steps.push({
|
|
68
|
+
type: 'persist',
|
|
69
|
+
source: persistMatch[1].trim(),
|
|
70
|
+
destination: persistMatch[2].trim()
|
|
71
|
+
});
|
|
72
|
+
i++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
51
76
|
// ---- Math step patterns (standalone) ----
|
|
52
77
|
let mathAdd = line.match(/^Add\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i);
|
|
53
78
|
if (mathAdd) {
|
|
@@ -66,7 +91,7 @@ function parse(code, fileName = null) {
|
|
|
66
91
|
workflow.__requiresMath = true;
|
|
67
92
|
workflow.steps.push({
|
|
68
93
|
type: 'calculate',
|
|
69
|
-
expression: `subtract({${
|
|
94
|
+
expression: `subtract({${mathSub[2]}}, {${mathSub[1]}})`, // Fixed: was mathAdd
|
|
70
95
|
saveAs: mathSub[3].trim()
|
|
71
96
|
});
|
|
72
97
|
i++;
|
|
@@ -229,7 +254,7 @@ function parse(code, fileName = null) {
|
|
|
229
254
|
}
|
|
230
255
|
|
|
231
256
|
// ---------------------------
|
|
232
|
-
// Parse nested blocks (
|
|
257
|
+
// Parse nested blocks (updated)
|
|
233
258
|
// ---------------------------
|
|
234
259
|
function parseBlock(lines) {
|
|
235
260
|
const steps = [];
|
|
@@ -269,6 +294,24 @@ function parseBlock(lines) {
|
|
|
269
294
|
if (askMatch) {
|
|
270
295
|
steps.push({ type: 'ask', target: askMatch[1].trim(), saveAs: null, constraints: {} });
|
|
271
296
|
}
|
|
297
|
+
// ✅ Parse file Persist in blocks
|
|
298
|
+
const persistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+"([^"]+)"$/i);
|
|
299
|
+
if (persistMatch) {
|
|
300
|
+
steps.push({
|
|
301
|
+
type: 'persist',
|
|
302
|
+
source: persistMatch[1].trim(),
|
|
303
|
+
destination: persistMatch[2].trim()
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
// ✅ NEW: Parse database Persist in blocks
|
|
307
|
+
const dbPersistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+db:([^\s]+)$/i);
|
|
308
|
+
if (dbPersistMatch) {
|
|
309
|
+
steps.push({
|
|
310
|
+
type: 'persist-db',
|
|
311
|
+
source: dbPersistMatch[1].trim(),
|
|
312
|
+
collection: dbPersistMatch[2].trim()
|
|
313
|
+
});
|
|
314
|
+
}
|
|
272
315
|
}
|
|
273
316
|
return steps;
|
|
274
317
|
}
|
package/src/runtime.js
CHANGED
|
@@ -16,6 +16,80 @@ class RuntimeAPI {
|
|
|
16
16
|
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
|
|
17
17
|
this.disallowedLogFile = path.join(logsDir, 'disallowed_resolvers.json');
|
|
18
18
|
this.disallowedAttempts = [];
|
|
19
|
+
|
|
20
|
+
// ✅ NEW: Database client setup
|
|
21
|
+
this.dbClient = null;
|
|
22
|
+
this._initDbClient();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ✅ NEW: Initialize database client
|
|
26
|
+
_initDbClient() {
|
|
27
|
+
const dbType = process.env.OLANG_DB_TYPE; // 'postgres', 'mysql', 'mongodb', 'sqlite'
|
|
28
|
+
|
|
29
|
+
if (!dbType) return; // DB persistence disabled
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
switch (dbType.toLowerCase()) {
|
|
33
|
+
case 'postgres':
|
|
34
|
+
case 'postgresql':
|
|
35
|
+
const { Pool } = require('pg');
|
|
36
|
+
this.dbClient = {
|
|
37
|
+
type: 'postgres',
|
|
38
|
+
client: new Pool({
|
|
39
|
+
host: process.env.DB_HOST || 'localhost',
|
|
40
|
+
port: parseInt(process.env.DB_PORT) || 5432,
|
|
41
|
+
user: process.env.DB_USER,
|
|
42
|
+
password: process.env.DB_PASSWORD,
|
|
43
|
+
database: process.env.DB_NAME
|
|
44
|
+
})
|
|
45
|
+
};
|
|
46
|
+
break;
|
|
47
|
+
|
|
48
|
+
case 'mysql':
|
|
49
|
+
const mysql = require('mysql2/promise');
|
|
50
|
+
this.dbClient = {
|
|
51
|
+
type: 'mysql',
|
|
52
|
+
client: mysql.createPool({
|
|
53
|
+
host: process.env.DB_HOST || 'localhost',
|
|
54
|
+
port: parseInt(process.env.DB_PORT) || 3306,
|
|
55
|
+
user: process.env.DB_USER,
|
|
56
|
+
password: process.env.DB_PASSWORD,
|
|
57
|
+
database: process.env.DB_NAME
|
|
58
|
+
})
|
|
59
|
+
};
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case 'mongodb':
|
|
63
|
+
const { MongoClient } = require('mongodb');
|
|
64
|
+
const uri = process.env.MONGO_URI || `mongodb://${process.env.DB_HOST || 'localhost'}:${process.env.DB_PORT || 27017}`;
|
|
65
|
+
this.dbClient = {
|
|
66
|
+
type: 'mongodb',
|
|
67
|
+
client: new MongoClient(uri)
|
|
68
|
+
};
|
|
69
|
+
break;
|
|
70
|
+
|
|
71
|
+
case 'sqlite':
|
|
72
|
+
const Database = require('better-sqlite3');
|
|
73
|
+
const dbPath = process.env.SQLITE_PATH || './olang.db';
|
|
74
|
+
const dbDir = path.dirname(path.resolve(dbPath));
|
|
75
|
+
if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
|
|
76
|
+
this.dbClient = {
|
|
77
|
+
type: 'sqlite',
|
|
78
|
+
client: new Database(dbPath)
|
|
79
|
+
};
|
|
80
|
+
break;
|
|
81
|
+
|
|
82
|
+
default:
|
|
83
|
+
throw new Error(`Unsupported database type: ${dbType}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (this.verbose) {
|
|
87
|
+
console.log(`🗄️ Database client initialized: ${dbType}`);
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
this.addWarning(`Failed to initialize DB client: ${e.message}`);
|
|
91
|
+
this.dbClient = null;
|
|
92
|
+
}
|
|
19
93
|
}
|
|
20
94
|
|
|
21
95
|
// -----------------------------
|
|
@@ -86,7 +160,7 @@ class RuntimeAPI {
|
|
|
86
160
|
const gt = cond.match(/^\{(.+)\}\s+greater than\s+(\d+\.?\d*)$/);
|
|
87
161
|
if (gt) return parseFloat(this.getNested(ctx, gt[1])) > parseFloat(gt[2]);
|
|
88
162
|
const lt = cond.match(/^\{(.+)\}\s+less than\s+(\d+\.?\d*)$/);
|
|
89
|
-
if (lt) return parseFloat(this.getNested(ctx, lt[1])) < parseFloat(
|
|
163
|
+
if (lt) return parseFloat(this.getNested(ctx, lt[1])) < parseFloat(lt[2]);
|
|
90
164
|
return Boolean(this.getNested(ctx, cond.replace(/\{|\}/g, '')));
|
|
91
165
|
}
|
|
92
166
|
|
|
@@ -265,6 +339,95 @@ class RuntimeAPI {
|
|
|
265
339
|
this.emit('debrief', { agent: step.agent, message: step.message });
|
|
266
340
|
break;
|
|
267
341
|
}
|
|
342
|
+
|
|
343
|
+
// ✅ File Persist step handler
|
|
344
|
+
case 'persist': {
|
|
345
|
+
const sourceValue = this.getNested(this.context, step.source);
|
|
346
|
+
if (sourceValue === undefined) {
|
|
347
|
+
this.addWarning(`Cannot persist undefined value from "${step.source}" to "${step.destination}"`);
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const outputPath = path.resolve(process.cwd(), step.destination);
|
|
352
|
+
const outputDir = path.dirname(outputPath);
|
|
353
|
+
if (!fs.existsSync(outputDir)) {
|
|
354
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let content;
|
|
358
|
+
if (step.destination.endsWith('.json')) {
|
|
359
|
+
content = JSON.stringify(sourceValue, null, 2);
|
|
360
|
+
} else {
|
|
361
|
+
content = String(sourceValue);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fs.writeFileSync(outputPath, content, 'utf8');
|
|
365
|
+
|
|
366
|
+
if (this.verbose) {
|
|
367
|
+
console.log(`💾 Persisted "${step.source}" to ${step.destination}`);
|
|
368
|
+
}
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ✅ NEW: Database persist handler
|
|
373
|
+
case 'persist-db': {
|
|
374
|
+
if (!this.dbClient) {
|
|
375
|
+
this.addWarning(`DB persistence skipped (no DB configured). Set OLANG_DB_TYPE env var.`);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const sourceValue = this.getNested(this.context, step.source);
|
|
380
|
+
if (sourceValue === undefined) {
|
|
381
|
+
this.addWarning(`Cannot persist undefined value from "${step.source}" to DB collection "${step.collection}"`);
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
switch (this.dbClient.type) {
|
|
387
|
+
case 'postgres':
|
|
388
|
+
case 'mysql':
|
|
389
|
+
if (this.dbClient.type === 'postgres') {
|
|
390
|
+
await this.dbClient.client.query(
|
|
391
|
+
`INSERT INTO "${step.collection}" (workflow_name, data, created_at) VALUES ($1, $2, NOW())`,
|
|
392
|
+
[this.context.workflow_name || 'unknown', JSON.stringify(sourceValue)]
|
|
393
|
+
);
|
|
394
|
+
} else {
|
|
395
|
+
await this.dbClient.client.execute(
|
|
396
|
+
`INSERT INTO ?? (workflow_name, data, created_at) VALUES (?, ?, NOW())`,
|
|
397
|
+
[step.collection, this.context.workflow_name || 'unknown', JSON.stringify(sourceValue)]
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
break;
|
|
401
|
+
|
|
402
|
+
case 'mongodb':
|
|
403
|
+
const db = this.dbClient.client.db(process.env.DB_NAME || 'olang');
|
|
404
|
+
await db.collection(step.collection).insertOne({
|
|
405
|
+
workflow_name: this.context.workflow_name || 'unknown',
|
|
406
|
+
sourceValue,
|
|
407
|
+
created_at: new Date()
|
|
408
|
+
});
|
|
409
|
+
break;
|
|
410
|
+
|
|
411
|
+
case 'sqlite':
|
|
412
|
+
const stmt = this.dbClient.client.prepare(
|
|
413
|
+
`INSERT INTO ${step.collection} (workflow_name, data, created_at) VALUES (?, ?, ?)`
|
|
414
|
+
);
|
|
415
|
+
stmt.run(
|
|
416
|
+
this.context.workflow_name || 'unknown',
|
|
417
|
+
JSON.stringify(sourceValue),
|
|
418
|
+
new Date().toISOString()
|
|
419
|
+
);
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (this.verbose) {
|
|
424
|
+
console.log(`🗄️ Persisted "${step.source}" to DB collection ${step.collection}`);
|
|
425
|
+
}
|
|
426
|
+
} catch (e) {
|
|
427
|
+
this.addWarning(`DB persist failed for "${step.source}": ${e.message}`);
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
268
431
|
}
|
|
269
432
|
|
|
270
433
|
if (this.verbose) {
|
|
@@ -274,7 +437,11 @@ class RuntimeAPI {
|
|
|
274
437
|
}
|
|
275
438
|
|
|
276
439
|
async executeWorkflow(workflow, inputs, agentResolver) {
|
|
277
|
-
|
|
440
|
+
// ✅ Inject workflow name into context
|
|
441
|
+
this.context = {
|
|
442
|
+
...inputs,
|
|
443
|
+
workflow_name: workflow.name
|
|
444
|
+
};
|
|
278
445
|
this.workflowSteps = workflow.steps;
|
|
279
446
|
this.allowedResolvers = new Set(workflow.allowedResolvers || []);
|
|
280
447
|
|
|
@@ -313,4 +480,4 @@ async function execute(workflow, inputs, agentResolver, verbose = false) {
|
|
|
313
480
|
return rt.executeWorkflow(workflow, inputs, agentResolver);
|
|
314
481
|
}
|
|
315
482
|
|
|
316
|
-
module.exports = { execute, RuntimeAPI };
|
|
483
|
+
module.exports = { execute, RuntimeAPI };
|