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
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAgentFlow — LangGraph Adapter
|
|
3
|
+
*
|
|
4
|
+
* Transforms the OpenAgentFlow IR into executable LangGraph Python code.
|
|
5
|
+
* Generates a complete, self-contained Python script that defines a
|
|
6
|
+
* StateGraph workflow using the LangGraph API.
|
|
7
|
+
*
|
|
8
|
+
* Pipeline: IR → [LangGraph Adapter] → Python code (LangGraph StateGraph)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
generateHeaderTemplate,
|
|
13
|
+
generateImportsTemplate,
|
|
14
|
+
generateStateClassTemplate,
|
|
15
|
+
generateLlmHelperTemplate,
|
|
16
|
+
generateAgentNodeTemplate,
|
|
17
|
+
generateGraphBuilderTemplate,
|
|
18
|
+
generateMainTemplate,
|
|
19
|
+
} from './templates.js';
|
|
20
|
+
|
|
21
|
+
// ─── IR Type → Python Type Mapping ─────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const PRIMITIVE_TYPE_MAP = {
|
|
24
|
+
string: 'str',
|
|
25
|
+
int: 'int',
|
|
26
|
+
float: 'float',
|
|
27
|
+
bool: 'bool',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convert an IR type descriptor (e.g. "list<string>", "map<string,int>")
|
|
32
|
+
* into a Python typing annotation.
|
|
33
|
+
* @param {string} irType
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
function irTypeToPython(irType) {
|
|
37
|
+
// Primitives
|
|
38
|
+
if (PRIMITIVE_TYPE_MAP[irType]) {
|
|
39
|
+
return PRIMITIVE_TYPE_MAP[irType];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// list<T>
|
|
43
|
+
const listMatch = irType.match(/^list<(.+)>$/);
|
|
44
|
+
if (listMatch) {
|
|
45
|
+
return `List[${irTypeToPython(listMatch[1])}]`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// map<K,V> — need to handle nested generics carefully
|
|
49
|
+
const mapMatch = irType.match(/^map<(.+)>$/);
|
|
50
|
+
if (mapMatch) {
|
|
51
|
+
const inner = mapMatch[1];
|
|
52
|
+
const splitIdx = findTopLevelComma(inner);
|
|
53
|
+
if (splitIdx !== -1) {
|
|
54
|
+
const keyType = inner.substring(0, splitIdx);
|
|
55
|
+
const valType = inner.substring(splitIdx + 1);
|
|
56
|
+
return `Dict[${irTypeToPython(keyType)}, ${irTypeToPython(valType)}]`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fallback
|
|
61
|
+
return 'Any';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Find the index of the top-level comma in a type string,
|
|
66
|
+
* respecting nested angle brackets.
|
|
67
|
+
*/
|
|
68
|
+
function findTopLevelComma(str) {
|
|
69
|
+
let depth = 0;
|
|
70
|
+
for (let i = 0; i < str.length; i++) {
|
|
71
|
+
if (str[i] === '<') depth++;
|
|
72
|
+
else if (str[i] === '>') depth--;
|
|
73
|
+
else if (str[i] === ',' && depth === 0) return i;
|
|
74
|
+
}
|
|
75
|
+
return -1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Convert an agent ID to a valid Python function name (snake_case).
|
|
80
|
+
* @param {string} id
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
function toSnakeCase(id) {
|
|
84
|
+
return id
|
|
85
|
+
.replace(/([A-Z])/g, (m, c, i) => (i > 0 ? '_' : '') + c.toLowerCase())
|
|
86
|
+
.replace(/[^a-z0-9_]/g, '_')
|
|
87
|
+
.replace(/_+/g, '_')
|
|
88
|
+
.toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Escape a string for use inside a Python triple-quoted string.
|
|
93
|
+
* @param {string} str
|
|
94
|
+
* @returns {string}
|
|
95
|
+
*/
|
|
96
|
+
function escapePythonTripleQuote(str) {
|
|
97
|
+
return str
|
|
98
|
+
.replace(/\\/g, '\\\\')
|
|
99
|
+
.replace(/"""/g, '\\"\\"\\"');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── LangGraph Adapter ────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
export class LangGraphAdapter {
|
|
105
|
+
/**
|
|
106
|
+
* @param {object} ir - The OpenAgentFlow IR document
|
|
107
|
+
* @param {object} [options] - Adapter options
|
|
108
|
+
* @param {object} [options.input] - Initial state values loaded from file or CLI
|
|
109
|
+
*/
|
|
110
|
+
constructor(ir, options = {}) {
|
|
111
|
+
this.ir = ir;
|
|
112
|
+
this.options = options;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Generate LangGraph Python code from the IR.
|
|
117
|
+
* @returns {string} Generated Python source code
|
|
118
|
+
* @throws {Error} If the IR contains unsupported features
|
|
119
|
+
*/
|
|
120
|
+
generate() {
|
|
121
|
+
const compat = this.checkCompatibility();
|
|
122
|
+
if (!compat.supported) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`IR is not compatible with LangGraph: ${compat.issues.join('; ')}`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Build intermediate generation model separating compiler logic from templates
|
|
129
|
+
const model = this._buildGenerationModel();
|
|
130
|
+
|
|
131
|
+
// Compose final Python script using reusable templates
|
|
132
|
+
const sections = [
|
|
133
|
+
generateHeaderTemplate(model.header),
|
|
134
|
+
generateImportsTemplate(model.imports),
|
|
135
|
+
generateStateClassTemplate(model.stateClass),
|
|
136
|
+
generateLlmHelperTemplate(model.llmHelper),
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
for (const agentNode of model.agents) {
|
|
140
|
+
sections.push(generateAgentNodeTemplate(agentNode));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
sections.push(generateGraphBuilderTemplate(model.graphBuilder));
|
|
144
|
+
sections.push(generateMainTemplate(model.main));
|
|
145
|
+
|
|
146
|
+
return sections.join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Validate that the IR can be compiled to LangGraph.
|
|
151
|
+
* @returns {{ supported: boolean, issues: string[] }}
|
|
152
|
+
*/
|
|
153
|
+
checkCompatibility() {
|
|
154
|
+
const issues = [];
|
|
155
|
+
|
|
156
|
+
if (!this.ir.graph.entrypoint) {
|
|
157
|
+
issues.push('Missing entrypoint in IR graph');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (this.ir.graph.terminals.length === 0) {
|
|
161
|
+
issues.push('No terminal nodes in IR graph');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!this.ir.agents || this.ir.agents.length === 0) {
|
|
165
|
+
issues.push('No agents defined in IR');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
supported: issues.length === 0,
|
|
170
|
+
issues,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build intermediate generation model from IR.
|
|
176
|
+
* Owns compiler logic, IR inspection, validation, type conversion, and structure building.
|
|
177
|
+
* @returns {object}
|
|
178
|
+
*/
|
|
179
|
+
_buildGenerationModel() {
|
|
180
|
+
const vars = this.ir.state?.variables ?? [];
|
|
181
|
+
const inputData = this.options?.input || {};
|
|
182
|
+
if (this.options?.input) {
|
|
183
|
+
this._validateInputData(inputData, vars);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const header = {
|
|
187
|
+
workflowName: this.ir.workflow.name,
|
|
188
|
+
version: this.ir.version,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const typingImports = new Set(['TypedDict', 'Optional']);
|
|
192
|
+
let needsOperator = false;
|
|
193
|
+
for (const v of vars) {
|
|
194
|
+
if (v.type.includes('list<')) typingImports.add('List');
|
|
195
|
+
if (v.type.includes('map<')) typingImports.add('Dict');
|
|
196
|
+
if ((v.options ?? []).some(opt => opt.name === 'reducer')) {
|
|
197
|
+
typingImports.add('Annotated');
|
|
198
|
+
needsOperator = true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const imports = {
|
|
202
|
+
typingImports: Array.from(typingImports),
|
|
203
|
+
needsLlmProviders: this.ir.agents.length > 0,
|
|
204
|
+
needsOperator,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const stateClass = {
|
|
208
|
+
fields: vars.map(v => ({
|
|
209
|
+
name: v.name,
|
|
210
|
+
pyType: irTypeToPython(v.type),
|
|
211
|
+
required: (v.options ?? []).some(opt => opt.name === 'required'),
|
|
212
|
+
reducer: (v.options ?? []).some(opt => opt.name === 'reducer'),
|
|
213
|
+
})),
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const defaultAgent = this.ir.agents[0]; // todo: handle default config in a proper way
|
|
217
|
+
const llmHelper = {
|
|
218
|
+
defaultModel: defaultAgent?.model ?? null,
|
|
219
|
+
defaultTemperature: defaultAgent?.temperature != null ? defaultAgent.temperature : 0.7,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const agents = this.ir.agents.map(agent => ({
|
|
223
|
+
fnName: `${toSnakeCase(agent.id)}_node`,
|
|
224
|
+
id: agent.id,
|
|
225
|
+
model: agent.model ?? null,
|
|
226
|
+
temperature: agent.temperature != null ? agent.temperature : 0.7,
|
|
227
|
+
provider: agent.provider ?? null,
|
|
228
|
+
escapedInstructions: escapePythonTripleQuote(agent.instructions),
|
|
229
|
+
inputs: agent.inputs ?? [],
|
|
230
|
+
outputs: agent.outputs ?? [],
|
|
231
|
+
}));
|
|
232
|
+
|
|
233
|
+
const graphBuilder = {
|
|
234
|
+
nodes: this.ir.agents.map(agent => ({
|
|
235
|
+
id: agent.id,
|
|
236
|
+
fnName: `${toSnakeCase(agent.id)}_node`,
|
|
237
|
+
})),
|
|
238
|
+
entrypoint: this.ir.graph.entrypoint,
|
|
239
|
+
edges: this.ir.graph.edges ?? [],
|
|
240
|
+
terminals: this.ir.graph.terminals ?? [],
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const requiredFields = vars
|
|
244
|
+
.filter(v => (v.options ?? []).some(opt => opt.name === 'required'))
|
|
245
|
+
.map(v => v.name);
|
|
246
|
+
|
|
247
|
+
const main = {
|
|
248
|
+
workflowName: this.ir.workflow.name,
|
|
249
|
+
initialStateFields: vars.map(v => {
|
|
250
|
+
const isRequired = (v.options ?? []).some(opt => opt.name === 'required');
|
|
251
|
+
let defaultVal;
|
|
252
|
+
if (inputData[v.name] !== undefined) {
|
|
253
|
+
defaultVal = this._toPythonLiteral(inputData[v.name]);
|
|
254
|
+
} else if (isRequired) {
|
|
255
|
+
defaultVal = 'None';
|
|
256
|
+
} else {
|
|
257
|
+
defaultVal = this._pythonDefault(v.type);
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
name: v.name,
|
|
261
|
+
defaultVal,
|
|
262
|
+
};
|
|
263
|
+
}),
|
|
264
|
+
requiredFields,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
header,
|
|
269
|
+
imports,
|
|
270
|
+
stateClass,
|
|
271
|
+
llmHelper,
|
|
272
|
+
agents,
|
|
273
|
+
graphBuilder,
|
|
274
|
+
main,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Convert a JavaScript value to a Python literal string.
|
|
280
|
+
*/
|
|
281
|
+
_toPythonLiteral(val) {
|
|
282
|
+
if (val === null || val === undefined) return 'None';
|
|
283
|
+
if (typeof val === 'boolean') return val ? 'True' : 'False';
|
|
284
|
+
if (typeof val === 'number') return String(val);
|
|
285
|
+
if (typeof val === 'string') return JSON.stringify(val);
|
|
286
|
+
if (Array.isArray(val)) {
|
|
287
|
+
const items = val.map(item => this._toPythonLiteral(item));
|
|
288
|
+
return `[${items.join(', ')}]`;
|
|
289
|
+
}
|
|
290
|
+
if (typeof val === 'object') {
|
|
291
|
+
const entries = Object.entries(val).map(([k, v]) => `${JSON.stringify(k)}: ${this._toPythonLiteral(v)}`);
|
|
292
|
+
return `{${entries.join(', ')}}`;
|
|
293
|
+
}
|
|
294
|
+
return JSON.stringify(val);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Validate initial state input data against workflow state variables.
|
|
299
|
+
*/
|
|
300
|
+
_validateInputData(inputData, vars) {
|
|
301
|
+
const varMap = new Map(vars.map(v => [v.name, v]));
|
|
302
|
+
|
|
303
|
+
// 1. Unknown keys
|
|
304
|
+
for (const key of Object.keys(inputData)) {
|
|
305
|
+
if (!varMap.has(key)) {
|
|
306
|
+
throw new Error(`Input JSON contains variable "${key}" which is not defined in workflow state`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 2. Type compatibility
|
|
311
|
+
for (const [key, val] of Object.entries(inputData)) {
|
|
312
|
+
if (val === null || val === undefined) continue;
|
|
313
|
+
const varDef = varMap.get(key);
|
|
314
|
+
const irType = varDef.type;
|
|
315
|
+
|
|
316
|
+
let valid = true;
|
|
317
|
+
let actualType = typeof val;
|
|
318
|
+
if (Array.isArray(val)) actualType = 'list';
|
|
319
|
+
|
|
320
|
+
if (irType === 'string') {
|
|
321
|
+
valid = (typeof val === 'string');
|
|
322
|
+
} else if (irType === 'int') {
|
|
323
|
+
valid = (typeof val === 'number' && Number.isInteger(val));
|
|
324
|
+
if (!valid && typeof val === 'number') actualType = 'float';
|
|
325
|
+
} else if (irType === 'float') {
|
|
326
|
+
valid = (typeof val === 'number');
|
|
327
|
+
} else if (irType === 'bool') {
|
|
328
|
+
valid = (typeof val === 'boolean');
|
|
329
|
+
} else if (irType.startsWith('list<')) {
|
|
330
|
+
valid = Array.isArray(val);
|
|
331
|
+
} else if (irType.startsWith('map<')) {
|
|
332
|
+
valid = (typeof val === 'object' && val !== null && !Array.isArray(val));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (!valid) {
|
|
336
|
+
throw new Error(`Type mismatch for state variable "${key}": expected ${irType}, found ${actualType}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 3. Required variables check (if OAF_INPUT_FILE is not set)
|
|
341
|
+
if (!process.env.OAF_INPUT_FILE) {
|
|
342
|
+
for (const v of vars) {
|
|
343
|
+
const isRequired = (v.options ?? []).some(opt => opt.name === 'required');
|
|
344
|
+
if (isRequired && inputData[v.name] === undefined) {
|
|
345
|
+
throw new Error(`Missing required initial state variable: "${v.name}"`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Return a Python default value for an IR type descriptor.
|
|
353
|
+
*/
|
|
354
|
+
_pythonDefault(irType) {
|
|
355
|
+
if (irType === 'string') return '""';
|
|
356
|
+
if (irType === 'int') return '0';
|
|
357
|
+
if (irType === 'float') return '0.0';
|
|
358
|
+
if (irType === 'bool') return 'False';
|
|
359
|
+
if (irType.startsWith('list<')) return '[]';
|
|
360
|
+
if (irType.startsWith('map<')) return '{}';
|
|
361
|
+
return 'None';
|
|
362
|
+
}
|
|
363
|
+
}
|