openagentflow 0.1.0
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/LICENSE +21 -0
- package/README.md +262 -0
- package/adapters/langgraph/demo_template.js +104 -0
- package/adapters/langgraph/index.js +363 -0
- package/adapters/langgraph/templates.js +502 -0
- package/cli/env.js +193 -0
- package/cli/index.js +580 -0
- package/compiler/compiler.js +134 -0
- package/compiler/index.js +7 -0
- package/compiler/ir-generator.js +142 -0
- package/compiler/validator.js +594 -0
- package/examples/hello.oaf +14 -0
- package/examples/software-dev.oaf +60 -0
- package/examples/summarize-input.json +3 -0
- package/examples/summarize.oaf +28 -0
- package/examples/support-triage-input.json +3 -0
- package/examples/support-triage.oaf +49 -0
- package/package.json +44 -0
- package/parser/ast.js +240 -0
- package/parser/index.js +9 -0
- package/parser/lexer.js +376 -0
- package/parser/parser.js +505 -0
package/cli/index.js
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenAgentFlow CLI
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* oaf parse <file> Parse an .oaf file and print the AST
|
|
8
|
+
* oaf validate <file> Validate an .oaf file (syntax + semantics)
|
|
9
|
+
* oaf compile <file> [--target T] [-o file] Compile an .oaf file to IR or runtime code
|
|
10
|
+
* oaf run <file> [--target T] Compile and execute via a runtime adapter
|
|
11
|
+
* oaf graph <file> Print the workflow graph in DOT format
|
|
12
|
+
* oaf --help Show this help message
|
|
13
|
+
* oaf --version Show version
|
|
14
|
+
*
|
|
15
|
+
* Targets:
|
|
16
|
+
* ir (default for compile) Output IR as JSON
|
|
17
|
+
* langgraph Generate executable LangGraph Python code
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
|
|
21
|
+
import { resolve, basename, join } from 'path';
|
|
22
|
+
import { execSync, spawn } from 'child_process';
|
|
23
|
+
import os from 'os';
|
|
24
|
+
import { Compiler } from '../compiler/compiler.js';
|
|
25
|
+
import { LangGraphAdapter } from '../adapters/langgraph/index.js';
|
|
26
|
+
import { resolveEnvHierarchy, setupAuth } from './env.js';
|
|
27
|
+
|
|
28
|
+
// ─── ANSI Colors ───────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const colors = {
|
|
31
|
+
reset: '\x1b[0m',
|
|
32
|
+
bold: '\x1b[1m',
|
|
33
|
+
red: '\x1b[31m',
|
|
34
|
+
green: '\x1b[32m',
|
|
35
|
+
yellow: '\x1b[33m',
|
|
36
|
+
blue: '\x1b[34m',
|
|
37
|
+
cyan: '\x1b[36m',
|
|
38
|
+
dim: '\x1b[2m',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function printBanner() {
|
|
44
|
+
console.log(`${colors.cyan}${colors.bold}OpenAgentFlow${colors.reset} ${colors.dim}v0.1.0${colors.reset}`);
|
|
45
|
+
console.log();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function printUsage() {
|
|
49
|
+
printBanner();
|
|
50
|
+
console.log(`${colors.bold}Usage:${colors.reset}`);
|
|
51
|
+
console.log(` oaf parse <file.oaf> Parse and print the AST`);
|
|
52
|
+
console.log(` oaf validate <file.oaf> Validate syntax and semantics`);
|
|
53
|
+
console.log(` oaf compile <file.oaf> [--target T] [-o F] Compile to IR or runtime code`);
|
|
54
|
+
console.log(` oaf run <file.oaf> [--target T] Compile and execute workflow`);
|
|
55
|
+
console.log(` oaf graph <file.oaf> Output workflow graph (DOT format)`);
|
|
56
|
+
console.log(` oaf auth Set up API keys interactively`);
|
|
57
|
+
console.log();
|
|
58
|
+
console.log(`${colors.bold}Options:${colors.reset}`);
|
|
59
|
+
console.log(` --target, -t <target> Compilation target: ir (default), langgraph`);
|
|
60
|
+
console.log(` --input, -i <file> Path to JSON file containing initial workflow state`);
|
|
61
|
+
console.log(` -o <file> Output file (for compile command)`);
|
|
62
|
+
console.log(` --help, -h Show this help message`);
|
|
63
|
+
console.log(` --version, -v Show version`);
|
|
64
|
+
console.log();
|
|
65
|
+
console.log(`${colors.bold}Targets:${colors.reset}`);
|
|
66
|
+
console.log(` ir Output IR as JSON (default)`);
|
|
67
|
+
console.log(` langgraph Generate executable LangGraph Python code`);
|
|
68
|
+
console.log();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readSource(filePath) {
|
|
72
|
+
const absPath = resolve(filePath);
|
|
73
|
+
try {
|
|
74
|
+
return { source: readFileSync(absPath, 'utf-8'), filename: basename(absPath) };
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error(`${colors.red}Error:${colors.reset} Cannot read file: ${absPath}`);
|
|
77
|
+
console.error(` ${err.message}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function loadInputFile(filePath) {
|
|
83
|
+
if (!filePath) return null;
|
|
84
|
+
const absPath = resolve(filePath);
|
|
85
|
+
try {
|
|
86
|
+
const raw = readFileSync(absPath, 'utf-8');
|
|
87
|
+
const parsed = JSON.parse(raw);
|
|
88
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
89
|
+
console.error(`${colors.red}Error:${colors.reset} Input JSON file must contain a JSON object (dictionary): ${absPath}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
return parsed;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error(`${colors.red}Error:${colors.reset} Cannot read or parse input JSON file: ${absPath}`);
|
|
95
|
+
console.error(` ${err.message}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function printDiagnostics(validation, filename) {
|
|
101
|
+
for (const diag of validation.diagnostics) {
|
|
102
|
+
const color = diag.severity === 'ERROR' ? colors.red : colors.yellow;
|
|
103
|
+
console.log(`${color}[${diag.severity}]${colors.reset} ${filename}:${diag.line}:${diag.column} — ${diag.message}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parse CLI args to extract named flags.
|
|
109
|
+
* Returns { positional: string[], flags: Map<string, string> }
|
|
110
|
+
*/
|
|
111
|
+
function parseArgs(args) {
|
|
112
|
+
const positional = [];
|
|
113
|
+
const flags = new Map();
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < args.length; i++) {
|
|
116
|
+
const arg = args[i];
|
|
117
|
+
|
|
118
|
+
if (arg === '--target' || arg === '-t') {
|
|
119
|
+
flags.set('target', args[++i] ?? '');
|
|
120
|
+
} else if (arg === '--runtime') {
|
|
121
|
+
flags.set('target', args[++i] ?? '');
|
|
122
|
+
} else if (arg === '--input' || arg === '-i') {
|
|
123
|
+
flags.set('input', args[++i] ?? '');
|
|
124
|
+
} else if (arg === '--output' || arg === '-o') {
|
|
125
|
+
flags.set('output', args[++i] ?? '');
|
|
126
|
+
} else if (arg === '--demo') {
|
|
127
|
+
flags.set('demo', 'true');
|
|
128
|
+
} else if (arg.startsWith('--target=')) {
|
|
129
|
+
flags.set('target', arg.split('=')[1]);
|
|
130
|
+
} else if (arg.startsWith('--runtime=')) {
|
|
131
|
+
flags.set('target', arg.split('=')[1]);
|
|
132
|
+
} else if (arg.startsWith('--input=')) {
|
|
133
|
+
flags.set('input', arg.split('=')[1]);
|
|
134
|
+
} else if (arg.startsWith('-i=')) {
|
|
135
|
+
flags.set('input', arg.split('=')[1]);
|
|
136
|
+
} else if (arg.startsWith('--output=')) {
|
|
137
|
+
flags.set('output', arg.split('=')[1]);
|
|
138
|
+
} else if (arg.startsWith('-o=')) {
|
|
139
|
+
flags.set('output', arg.split('=')[1]);
|
|
140
|
+
} else {
|
|
141
|
+
positional.push(arg);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { positional, flags };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Compile the source and return the CompilationResult, exiting on failure.
|
|
150
|
+
*/
|
|
151
|
+
function compileSource(filePath) {
|
|
152
|
+
const { source, filename } = readSource(filePath);
|
|
153
|
+
const compiler = new Compiler(source, filename);
|
|
154
|
+
const result = compiler.compile();
|
|
155
|
+
|
|
156
|
+
if (result.status !== 'success') {
|
|
157
|
+
if (result.error) {
|
|
158
|
+
console.error(`${colors.red}✗ Compilation failed:${colors.reset} ${result.error.message}`);
|
|
159
|
+
}
|
|
160
|
+
if (result.validation) {
|
|
161
|
+
printDiagnostics(result.validation, filename);
|
|
162
|
+
}
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { result, filename };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const SUPPORTED_TARGETS = ['ir', 'langgraph'];
|
|
170
|
+
|
|
171
|
+
// ─── Commands ──────────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* oaf parse <file>
|
|
175
|
+
* Tokenize and parse, then print the AST as JSON.
|
|
176
|
+
*/
|
|
177
|
+
function cmdParse(filePath) {
|
|
178
|
+
const { source, filename } = readSource(filePath);
|
|
179
|
+
const compiler = new Compiler(source, filename);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const tokens = compiler.lex();
|
|
183
|
+
const ast = compiler.parse(tokens);
|
|
184
|
+
|
|
185
|
+
console.log(`${colors.green}✓${colors.reset} Parsed ${colors.bold}${filename}${colors.reset} successfully.`);
|
|
186
|
+
console.log();
|
|
187
|
+
console.log(JSON.stringify(ast, null, 2));
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.error(`${colors.red}✗ Parse failed:${colors.reset} ${err.message}`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* oaf validate <file>
|
|
196
|
+
* Parse and run semantic validation.
|
|
197
|
+
*/
|
|
198
|
+
function cmdValidate(filePath) {
|
|
199
|
+
resolveEnvHierarchy(filePath);
|
|
200
|
+
const { source, filename } = readSource(filePath);
|
|
201
|
+
const compiler = new Compiler(source, filename);
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const tokens = compiler.lex();
|
|
205
|
+
const ast = compiler.parse(tokens);
|
|
206
|
+
const validation = compiler.validate(ast);
|
|
207
|
+
|
|
208
|
+
if (validation.diagnostics.length > 0) {
|
|
209
|
+
printDiagnostics(validation, filename);
|
|
210
|
+
console.log();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (validation.isValid) {
|
|
214
|
+
console.log(`${colors.green}✓${colors.reset} ${colors.bold}${filename}${colors.reset} is valid.`);
|
|
215
|
+
if (validation.warnings.length > 0) {
|
|
216
|
+
console.log(` ${colors.yellow}${validation.warnings.length} warning(s)${colors.reset}`);
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
console.log(`${colors.red}✗${colors.reset} ${colors.bold}${filename}${colors.reset} has ${validation.errors.length} error(s).`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.error(`${colors.red}✗ Validation failed:${colors.reset} ${err.message}`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* oaf compile <file> [--target T] [-o output]
|
|
230
|
+
* Full pipeline: parse → validate → IR → (optionally) adapter.
|
|
231
|
+
*
|
|
232
|
+
* Targets:
|
|
233
|
+
* ir — Output IR as JSON (default)
|
|
234
|
+
* langgraph — Generate Python code via LangGraph adapter
|
|
235
|
+
*/
|
|
236
|
+
function cmdCompile(filePath, flags, positional = []) {
|
|
237
|
+
resolveEnvHierarchy(filePath);
|
|
238
|
+
const target = flags.get('target') ?? 'ir';
|
|
239
|
+
const outputFile = flags.get('output');
|
|
240
|
+
const rawInputPath = flags.get('input') || positional[2] || null;
|
|
241
|
+
const inputData = loadInputFile(rawInputPath);
|
|
242
|
+
|
|
243
|
+
if (!SUPPORTED_TARGETS.includes(target)) {
|
|
244
|
+
console.error(`${colors.red}Error:${colors.reset} Unknown target "${target}". Supported: ${SUPPORTED_TARGETS.join(', ')}`);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const { result, filename } = compileSource(filePath);
|
|
249
|
+
let output;
|
|
250
|
+
|
|
251
|
+
if (target === 'ir') {
|
|
252
|
+
output = JSON.stringify(result.ir, null, 2);
|
|
253
|
+
} else if (target === 'langgraph') {
|
|
254
|
+
try {
|
|
255
|
+
const adapter = new LangGraphAdapter(result.ir, { input: inputData });
|
|
256
|
+
output = adapter.generate();
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error(`${colors.red}✗ LangGraph generation failed:${colors.reset} ${err.message}`);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (outputFile) {
|
|
264
|
+
const absOutput = resolve(outputFile);
|
|
265
|
+
try {
|
|
266
|
+
writeFileSync(absOutput, output, 'utf-8');
|
|
267
|
+
console.log(`${colors.green}✓${colors.reset} Compiled ${colors.bold}${filename}${colors.reset} → ${colors.bold}${basename(absOutput)}${colors.reset} (target: ${target})`);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
console.error(`${colors.red}Error:${colors.reset} Cannot write to: ${absOutput}`);
|
|
270
|
+
console.error(` ${err.message}`);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
console.log(output);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* oaf run <file> [--target T] [--input F]
|
|
280
|
+
* Compile to a runtime target and execute via subprocess.
|
|
281
|
+
*
|
|
282
|
+
* Currently supports: langgraph (default for run command)
|
|
283
|
+
*/
|
|
284
|
+
function cmdRun(filePath, flags, positional = []) {
|
|
285
|
+
resolveEnvHierarchy(filePath);
|
|
286
|
+
// --- DEMO HOOK (Cleanly removable) ---
|
|
287
|
+
const isDemoMode = flags.get('demo') === 'true' || process.env.OAF_DEMO === '1';
|
|
288
|
+
if (isDemoMode) process.env.OAF_DEMO = '1';
|
|
289
|
+
// -------------------------------------
|
|
290
|
+
|
|
291
|
+
const target = flags.get('target') ?? 'langgraph';
|
|
292
|
+
const rawInputPath = flags.get('input') || positional[2] || null;
|
|
293
|
+
const inputPath = rawInputPath ? resolve(rawInputPath) : null;
|
|
294
|
+
const inputData = loadInputFile(rawInputPath);
|
|
295
|
+
|
|
296
|
+
if (target === 'ir') {
|
|
297
|
+
console.error(`${colors.red}Error:${colors.reset} Cannot execute IR directly. Use --target langgraph`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (target !== 'langgraph') {
|
|
302
|
+
console.error(`${colors.red}Error:${colors.reset} Unknown runtime target "${target}". Supported: langgraph`);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const { result, filename } = compileSource(filePath);
|
|
307
|
+
|
|
308
|
+
// Pre-flight check 1: Python runtime existence
|
|
309
|
+
const pythonExe = getPythonCommand();
|
|
310
|
+
try {
|
|
311
|
+
execSync(`${pythonExe} --version`, { stdio: 'ignore' });
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.error(`${colors.red}Error:${colors.reset} Python runtime not found ("${pythonExe}"). Please install Python 3.8+ and ensure it is in your PATH.`);
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Pre-flight check 2: API Keys & OAF_DEFAULT_MODEL
|
|
318
|
+
if (!isDemoMode) {
|
|
319
|
+
const overrideModel = process.env.OAF_OVERRIDE_MODEL;
|
|
320
|
+
for (const agent of result.ir.agents) {
|
|
321
|
+
const targetModel = overrideModel || agent.model;
|
|
322
|
+
if ((targetModel === null || targetModel === undefined) && !process.env.OAF_DEFAULT_MODEL) {
|
|
323
|
+
console.error(`${colors.red}Error:${colors.reset} No model specified for agent "${agent.id}" and no default model configured. Please specify a 'model' property in your agent definition or set the OAF_DEFAULT_MODEL environment variable.`);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let provider = overrideModel ? null : agent.provider;
|
|
328
|
+
if (!provider && targetModel) {
|
|
329
|
+
if (targetModel.startsWith('claude-')) provider = 'anthropic';
|
|
330
|
+
else if (targetModel.startsWith('gpt-') || targetModel.startsWith('o1') || targetModel.startsWith('o3')) provider = 'openai';
|
|
331
|
+
else if (targetModel.startsWith('gemini-') || targetModel.startsWith('gemma-')) provider = 'gemini';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (provider === 'anthropic' && !process.env.ANTHROPIC_API_KEY) {
|
|
335
|
+
console.error(`${colors.red}Error:${colors.reset} Missing required API key "ANTHROPIC_API_KEY" for agent "${agent.id}" (provider: anthropic).`);
|
|
336
|
+
console.error(`Looked in order of priority:`);
|
|
337
|
+
console.error(` 1. Inline CLI overrides`);
|
|
338
|
+
console.error(` 2. Local Project .env`);
|
|
339
|
+
console.error(` 3. System Environment Variables`);
|
|
340
|
+
console.error(` 4. Global OAF Store (~/.oaf/.env)`);
|
|
341
|
+
console.error(`Run \`oaf auth\` to set up your global credentials or create a local .env file.`);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
} else if (provider === 'openai' && !process.env.OPENAI_API_KEY) {
|
|
344
|
+
console.error(`${colors.red}Error:${colors.reset} Missing required API key "OPENAI_API_KEY" for agent "${agent.id}" (provider: openai).`);
|
|
345
|
+
console.error(`Looked in order of priority:`);
|
|
346
|
+
console.error(` 1. Inline CLI overrides`);
|
|
347
|
+
console.error(` 2. Local Project .env`);
|
|
348
|
+
console.error(` 3. System Environment Variables`);
|
|
349
|
+
console.error(` 4. Global OAF Store (~/.oaf/.env)`);
|
|
350
|
+
console.error(`Run \`oaf auth\` to set up your global credentials or create a local .env file.`);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
} else if (provider === 'gemini' && !process.env.GOOGLE_API_KEY) {
|
|
353
|
+
console.error(`${colors.red}Error:${colors.reset} Missing required API key "GOOGLE_API_KEY" for agent "${agent.id}" (provider: gemini).`);
|
|
354
|
+
console.error(`Looked in order of priority:`);
|
|
355
|
+
console.error(` 1. Inline CLI overrides`);
|
|
356
|
+
console.error(` 2. Local Project .env`);
|
|
357
|
+
console.error(` 3. System Environment Variables`);
|
|
358
|
+
console.error(` 4. Global OAF Store (~/.oaf/.env)`);
|
|
359
|
+
console.error(`Run \`oaf auth\` to set up your global credentials or create a local .env file.`);
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!process.env.GOOGLE_API_KEY && !process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY && result.ir.agents.length > 0) {
|
|
365
|
+
console.error(`${colors.red}Error:${colors.reset} No LLM API key configured. Set GOOGLE_API_KEY (Gemini), OPENAI_API_KEY (OpenAI), or ANTHROPIC_API_KEY (Anthropic) to execute workflows.`);
|
|
366
|
+
console.error(`Looked in order of priority:`);
|
|
367
|
+
console.error(` 1. Inline CLI overrides`);
|
|
368
|
+
console.error(` 2. Local Project .env`);
|
|
369
|
+
console.error(` 3. System Environment Variables`);
|
|
370
|
+
console.error(` 4. Global OAF Store (~/.oaf/.env)`);
|
|
371
|
+
console.error(`Run \`oaf auth\` to set up your global credentials or create a local .env file.`);
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Generate Python code
|
|
377
|
+
let pythonCode;
|
|
378
|
+
try {
|
|
379
|
+
const adapter = new LangGraphAdapter(result.ir, { input: inputData });
|
|
380
|
+
pythonCode = adapter.generate();
|
|
381
|
+
} catch (err) {
|
|
382
|
+
console.error(`${colors.red}✗ LangGraph generation failed:${colors.reset} ${err.message}`);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
console.log(`${colors.green}✓${colors.reset} Compiled ${colors.bold}${filename}${colors.reset} (target: ${target})`);
|
|
387
|
+
console.log(`${colors.cyan}▶${colors.reset} Executing workflow via Python subprocess...`);
|
|
388
|
+
console.log();
|
|
389
|
+
|
|
390
|
+
// Execute Python code using a clean temporary file inside os.tmpdir()
|
|
391
|
+
const tmpFile = join(os.tmpdir(), `oaf_run_${Date.now()}_${Math.random().toString(36).slice(2)}.py`);
|
|
392
|
+
try {
|
|
393
|
+
writeFileSync(tmpFile, pythonCode, 'utf-8');
|
|
394
|
+
const pyArgs = [tmpFile];
|
|
395
|
+
if (inputPath) {
|
|
396
|
+
pyArgs.push('--input', inputPath);
|
|
397
|
+
}
|
|
398
|
+
const child = spawn(pythonExe, pyArgs, {
|
|
399
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
400
|
+
env: { ...process.env },
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
let stdout = '';
|
|
404
|
+
let stderr = '';
|
|
405
|
+
|
|
406
|
+
const cleanup = () => {
|
|
407
|
+
try { if (existsSync(tmpFile)) unlinkSync(tmpFile); } catch (e) {}
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
child.stdout.on('data', (data) => {
|
|
411
|
+
const text = data.toString();
|
|
412
|
+
stdout += text;
|
|
413
|
+
process.stdout.write(text);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
child.stderr.on('data', (data) => {
|
|
417
|
+
const text = data.toString();
|
|
418
|
+
stderr += text;
|
|
419
|
+
process.stderr.write(text);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
child.on('close', (code) => {
|
|
423
|
+
cleanup();
|
|
424
|
+
console.log();
|
|
425
|
+
if (code === 0) {
|
|
426
|
+
console.log(`${colors.green}✓${colors.reset} Workflow execution completed successfully.`);
|
|
427
|
+
} else {
|
|
428
|
+
console.error(`${colors.red}✗ Workflow execution failed${colors.reset} (exit code: ${code})`);
|
|
429
|
+
if (stderr.includes('ModuleNotFoundError')) {
|
|
430
|
+
console.error();
|
|
431
|
+
console.error(`${colors.yellow}Hint:${colors.reset} Missing Python dependencies. Install with:`);
|
|
432
|
+
console.error(` pip install langgraph langchain-openai`);
|
|
433
|
+
}
|
|
434
|
+
if (stderr.includes('OPENAI_API_KEY')) {
|
|
435
|
+
console.error();
|
|
436
|
+
console.error(`${colors.yellow}Hint:${colors.reset} Set your OpenAI API key:`);
|
|
437
|
+
console.error(` export OPENAI_API_KEY='your-api-key-here'`);
|
|
438
|
+
}
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
child.on('error', (err) => {
|
|
444
|
+
cleanup();
|
|
445
|
+
if (err.code === 'ENOENT') {
|
|
446
|
+
console.error(`${colors.red}Error:${colors.reset} Python not found. Please install Python 3.8+ and ensure it is in your PATH.`);
|
|
447
|
+
} else {
|
|
448
|
+
console.error(`${colors.red}Error:${colors.reset} Failed to spawn Python process: ${err.message}`);
|
|
449
|
+
}
|
|
450
|
+
process.exit(1);
|
|
451
|
+
});
|
|
452
|
+
} catch (err) {
|
|
453
|
+
try { if (existsSync(tmpFile)) unlinkSync(tmpFile); } catch (e) {}
|
|
454
|
+
console.error(`${colors.red}✗ Failed to execute Python subprocess:${colors.reset} ${err.message}`);
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function getPythonCommand() {
|
|
460
|
+
if (process.env.VIRTUAL_ENV) {
|
|
461
|
+
const venvWin = join(process.env.VIRTUAL_ENV, 'Scripts', 'python.exe');
|
|
462
|
+
const venvPosix = join(process.env.VIRTUAL_ENV, 'bin', 'python');
|
|
463
|
+
if (existsSync(venvWin)) return venvWin;
|
|
464
|
+
if (existsSync(venvPosix)) return venvPosix;
|
|
465
|
+
}
|
|
466
|
+
const localWin = join(process.cwd(), '.venv', 'Scripts', 'python.exe');
|
|
467
|
+
const localPosix = join(process.cwd(), '.venv', 'bin', 'python');
|
|
468
|
+
if (existsSync(localWin)) return localWin;
|
|
469
|
+
if (existsSync(localPosix)) return localPosix;
|
|
470
|
+
return 'python';
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* oaf auth
|
|
475
|
+
* Interactive prompt to save API keys to ~/.oaf/.env
|
|
476
|
+
*/
|
|
477
|
+
function cmdAuth() {
|
|
478
|
+
setupAuth();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* oaf graph <file>
|
|
483
|
+
* Compile and output the workflow graph in Graphviz DOT format.
|
|
484
|
+
*/
|
|
485
|
+
function cmdGraph(filePath) {
|
|
486
|
+
const { result, filename } = compileSource(filePath);
|
|
487
|
+
const ir = result.ir;
|
|
488
|
+
const lines = [];
|
|
489
|
+
lines.push('digraph workflow {');
|
|
490
|
+
lines.push(' rankdir=TB;');
|
|
491
|
+
lines.push(' node [shape=box, style="rounded,filled", fillcolor="#e8f4f8", fontname="sans-serif"];');
|
|
492
|
+
lines.push(' edge [color="#555555"];');
|
|
493
|
+
lines.push('');
|
|
494
|
+
lines.push(` // Workflow: ${ir.workflow.name}`);
|
|
495
|
+
lines.push(' __start__ [label="START", shape=circle, fillcolor="#4CAF50", fontcolor=white, style=filled];');
|
|
496
|
+
lines.push(' __end__ [label="END", shape=doublecircle, fillcolor="#f44336", fontcolor=white, style=filled];');
|
|
497
|
+
lines.push('');
|
|
498
|
+
|
|
499
|
+
// Agent nodes
|
|
500
|
+
for (const agent of ir.agents) {
|
|
501
|
+
const label = agent.id;
|
|
502
|
+
lines.push(` ${agent.id} [label="${label}"];`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
lines.push('');
|
|
506
|
+
|
|
507
|
+
// Start → entrypoint
|
|
508
|
+
if (ir.graph.entrypoint) {
|
|
509
|
+
lines.push(` __start__ -> ${ir.graph.entrypoint};`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Agent-to-agent edges
|
|
513
|
+
for (const edge of ir.graph.edges) {
|
|
514
|
+
lines.push(` ${edge.source} -> ${edge.target};`);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Terminal → end
|
|
518
|
+
for (const terminal of ir.graph.terminals) {
|
|
519
|
+
lines.push(` ${terminal} -> __end__;`);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
lines.push('}');
|
|
523
|
+
|
|
524
|
+
console.log(lines.join('\n'));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ─── Main ──────────────────────────────────────────────────────────────────────
|
|
528
|
+
|
|
529
|
+
function main() {
|
|
530
|
+
const rawArgs = process.argv.slice(2);
|
|
531
|
+
|
|
532
|
+
if (rawArgs.length === 0 || rawArgs.includes('--help') || rawArgs.includes('-h')) {
|
|
533
|
+
printUsage();
|
|
534
|
+
process.exit(0);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
|
|
538
|
+
console.log('0.1.0');
|
|
539
|
+
process.exit(0);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const { positional, flags } = parseArgs(rawArgs);
|
|
543
|
+
const command = positional[0];
|
|
544
|
+
const filePath = positional[1];
|
|
545
|
+
|
|
546
|
+
if (command === 'auth') {
|
|
547
|
+
cmdAuth();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (!filePath && ['parse', 'validate', 'compile', 'run', 'graph'].includes(command)) {
|
|
552
|
+
console.error(`${colors.red}Error:${colors.reset} Missing file argument.`);
|
|
553
|
+
console.error(` Usage: oaf ${command} <file.oaf>`);
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
switch (command) {
|
|
558
|
+
case 'parse':
|
|
559
|
+
cmdParse(filePath);
|
|
560
|
+
break;
|
|
561
|
+
case 'validate':
|
|
562
|
+
cmdValidate(filePath);
|
|
563
|
+
break;
|
|
564
|
+
case 'compile':
|
|
565
|
+
cmdCompile(filePath, flags, positional);
|
|
566
|
+
break;
|
|
567
|
+
case 'run':
|
|
568
|
+
cmdRun(filePath, flags, positional);
|
|
569
|
+
break;
|
|
570
|
+
case 'graph':
|
|
571
|
+
cmdGraph(filePath);
|
|
572
|
+
break;
|
|
573
|
+
default:
|
|
574
|
+
console.error(`${colors.red}Error:${colors.reset} Unknown command "${command}".`);
|
|
575
|
+
printUsage();
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
main();
|