@ryanfw/prompt-orchestration-pipeline 0.0.1
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 +290 -0
- package/package.json +51 -0
- package/src/api/index.js +220 -0
- package/src/cli/index.js +70 -0
- package/src/core/config.js +345 -0
- package/src/core/environment.js +56 -0
- package/src/core/orchestrator.js +335 -0
- package/src/core/pipeline-runner.js +182 -0
- package/src/core/retry.js +83 -0
- package/src/core/task-runner.js +305 -0
- package/src/core/validation.js +100 -0
- package/src/llm/README.md +345 -0
- package/src/llm/index.js +320 -0
- package/src/providers/anthropic.js +117 -0
- package/src/providers/base.js +71 -0
- package/src/providers/deepseek.js +122 -0
- package/src/providers/openai.js +314 -0
- package/src/ui/README.md +86 -0
- package/src/ui/public/app.js +260 -0
- package/src/ui/public/index.html +53 -0
- package/src/ui/public/style.css +341 -0
- package/src/ui/server.js +230 -0
- package/src/ui/state.js +67 -0
- package/src/ui/watcher.js +85 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { createLLM, getLLMEvents } from "../llm/index.js";
|
|
4
|
+
import { loadEnvironment } from "./environment.js";
|
|
5
|
+
import { getConfig } from "./config.js";
|
|
6
|
+
|
|
7
|
+
/** Canonical order using the field terms we discussed */
|
|
8
|
+
const ORDER = [
|
|
9
|
+
"ingestion",
|
|
10
|
+
"preProcessing",
|
|
11
|
+
"promptTemplating",
|
|
12
|
+
"inference",
|
|
13
|
+
"parsing",
|
|
14
|
+
"validateStructure",
|
|
15
|
+
"validateQuality",
|
|
16
|
+
"critique",
|
|
17
|
+
"refine",
|
|
18
|
+
"finalValidation",
|
|
19
|
+
"integration",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Runs a pipeline by loading a module that exports functions keyed by ORDER.
|
|
24
|
+
*/
|
|
25
|
+
export async function runPipeline(modulePath, initialContext = {}) {
|
|
26
|
+
if (!initialContext.envLoaded) {
|
|
27
|
+
await loadEnvironment();
|
|
28
|
+
initialContext.envLoaded = true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!initialContext.llm) {
|
|
32
|
+
initialContext.llm = createLLM({
|
|
33
|
+
defaultProvider: initialContext.modelConfig?.defaultProvider || "openai",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const config = getConfig();
|
|
38
|
+
const llmMetrics = [];
|
|
39
|
+
const llmEvents = getLLMEvents();
|
|
40
|
+
|
|
41
|
+
const onLLMComplete = (metric) => {
|
|
42
|
+
llmMetrics.push({
|
|
43
|
+
...metric,
|
|
44
|
+
task: context.taskName,
|
|
45
|
+
stage: context.currentStage,
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
llmEvents.on("llm:request:complete", onLLMComplete);
|
|
50
|
+
llmEvents.on("llm:request:error", (m) =>
|
|
51
|
+
llmMetrics.push({ ...m, failed: true })
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const abs = toAbsFileURL(modulePath);
|
|
55
|
+
// Add cache busting to force module reload
|
|
56
|
+
const modUrl = `${abs.href}?t=${Date.now()}`;
|
|
57
|
+
const mod = await import(modUrl);
|
|
58
|
+
const tasks = mod.default ?? mod;
|
|
59
|
+
|
|
60
|
+
const context = { ...initialContext, currentStage: null };
|
|
61
|
+
const logs = [];
|
|
62
|
+
let needsRefinement = false;
|
|
63
|
+
let refinementCount = 0;
|
|
64
|
+
const maxRefinements = config.taskRunner.maxRefinementAttempts;
|
|
65
|
+
|
|
66
|
+
do {
|
|
67
|
+
needsRefinement = false;
|
|
68
|
+
let preRefinedThisCycle = false;
|
|
69
|
+
|
|
70
|
+
for (const stage of ORDER) {
|
|
71
|
+
context.currentStage = stage;
|
|
72
|
+
const fn = tasks[stage];
|
|
73
|
+
if (typeof fn !== "function") {
|
|
74
|
+
logs.push({ stage, skipped: true, refinementCycle: refinementCount });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
refinementCount > 0 &&
|
|
80
|
+
["ingestion", "preProcessing"].includes(stage)
|
|
81
|
+
) {
|
|
82
|
+
logs.push({
|
|
83
|
+
stage,
|
|
84
|
+
skipped: true,
|
|
85
|
+
reason: "refinement-cycle",
|
|
86
|
+
refinementCycle: refinementCount,
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (
|
|
92
|
+
refinementCount > 0 &&
|
|
93
|
+
!preRefinedThisCycle &&
|
|
94
|
+
!context.refined &&
|
|
95
|
+
(stage === "validateStructure" || stage === "validateQuality")
|
|
96
|
+
) {
|
|
97
|
+
for (const s of ["critique", "refine"]) {
|
|
98
|
+
const f = tasks[s];
|
|
99
|
+
if (typeof f !== "function") {
|
|
100
|
+
logs.push({
|
|
101
|
+
stage: s,
|
|
102
|
+
skipped: true,
|
|
103
|
+
reason: "pre-refine-missing",
|
|
104
|
+
refinementCycle: refinementCount,
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const sStart = performance.now();
|
|
109
|
+
try {
|
|
110
|
+
const r = await f(context);
|
|
111
|
+
if (r && typeof r === "object") Object.assign(context, r);
|
|
112
|
+
const sMs = +(performance.now() - sStart).toFixed(2);
|
|
113
|
+
logs.push({
|
|
114
|
+
stage: s,
|
|
115
|
+
ok: true,
|
|
116
|
+
ms: sMs,
|
|
117
|
+
refinementCycle: refinementCount,
|
|
118
|
+
reason: "pre-validate",
|
|
119
|
+
});
|
|
120
|
+
} catch (error) {
|
|
121
|
+
const sMs = +(performance.now() - sStart).toFixed(2);
|
|
122
|
+
const errInfo = normalizeError(error);
|
|
123
|
+
logs.push({
|
|
124
|
+
stage: s,
|
|
125
|
+
ok: false,
|
|
126
|
+
ms: sMs,
|
|
127
|
+
error: errInfo,
|
|
128
|
+
refinementCycle: refinementCount,
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
failedStage: s,
|
|
133
|
+
error: errInfo,
|
|
134
|
+
logs,
|
|
135
|
+
context,
|
|
136
|
+
refinementAttempts: refinementCount,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
preRefinedThisCycle = true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (preRefinedThisCycle && (stage === "critique" || stage === "refine")) {
|
|
144
|
+
logs.push({
|
|
145
|
+
stage,
|
|
146
|
+
skipped: true,
|
|
147
|
+
reason: "already-pre-refined",
|
|
148
|
+
refinementCycle: refinementCount,
|
|
149
|
+
});
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const start = performance.now();
|
|
154
|
+
try {
|
|
155
|
+
const result = await fn(context);
|
|
156
|
+
if (result && typeof result === "object")
|
|
157
|
+
Object.assign(context, result);
|
|
158
|
+
|
|
159
|
+
const ms = +(performance.now() - start).toFixed(2);
|
|
160
|
+
logs.push({ stage, ok: true, ms, refinementCycle: refinementCount });
|
|
161
|
+
|
|
162
|
+
if (
|
|
163
|
+
(stage === "validateStructure" || stage === "validateQuality") &&
|
|
164
|
+
context.validationFailed &&
|
|
165
|
+
refinementCount < maxRefinements
|
|
166
|
+
) {
|
|
167
|
+
needsRefinement = true;
|
|
168
|
+
context.validationFailed = false;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
const ms = +(performance.now() - start).toFixed(2);
|
|
173
|
+
const errInfo = normalizeError(error);
|
|
174
|
+
logs.push({
|
|
175
|
+
stage,
|
|
176
|
+
ok: false,
|
|
177
|
+
ms,
|
|
178
|
+
error: errInfo,
|
|
179
|
+
refinementCycle: refinementCount,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
(stage === "validateStructure" || stage === "validateQuality") &&
|
|
184
|
+
refinementCount < maxRefinements
|
|
185
|
+
) {
|
|
186
|
+
context.lastValidationError = errInfo;
|
|
187
|
+
needsRefinement = true;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
failedStage: stage,
|
|
194
|
+
error: errInfo,
|
|
195
|
+
logs,
|
|
196
|
+
context,
|
|
197
|
+
refinementAttempts: refinementCount,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (needsRefinement) {
|
|
203
|
+
refinementCount++;
|
|
204
|
+
logs.push({
|
|
205
|
+
stage: "refinement-trigger",
|
|
206
|
+
refinementCycle: refinementCount,
|
|
207
|
+
reason: context.lastValidationError
|
|
208
|
+
? "validation-error"
|
|
209
|
+
: "validation-failed-flag",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
} while (needsRefinement && refinementCount <= maxRefinements);
|
|
213
|
+
|
|
214
|
+
// Only fail on validationFailed if we actually have validation functions
|
|
215
|
+
const hasValidation =
|
|
216
|
+
typeof tasks.validateStructure === "function" ||
|
|
217
|
+
typeof tasks.validateQuality === "function";
|
|
218
|
+
|
|
219
|
+
if (context.validationFailed && hasValidation) {
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
failedStage: "final-validation",
|
|
223
|
+
error: { message: "Validation failed after all refinement attempts" },
|
|
224
|
+
logs,
|
|
225
|
+
context,
|
|
226
|
+
refinementAttempts: refinementCount,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
llmEvents.off("llm:request:complete", onLLMComplete);
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
ok: true,
|
|
234
|
+
logs,
|
|
235
|
+
context,
|
|
236
|
+
refinementAttempts: refinementCount,
|
|
237
|
+
llmMetrics,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function runPipelineWithModelRouting(
|
|
242
|
+
modulePath,
|
|
243
|
+
initialContext = {},
|
|
244
|
+
modelConfig = {}
|
|
245
|
+
) {
|
|
246
|
+
const context = {
|
|
247
|
+
...initialContext,
|
|
248
|
+
modelConfig,
|
|
249
|
+
availableModels: modelConfig.models || ["default"],
|
|
250
|
+
currentModel: modelConfig.defaultModel || "default",
|
|
251
|
+
};
|
|
252
|
+
return runPipeline(modulePath, context);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function selectModel(taskType, complexity, speed = "normal") {
|
|
256
|
+
const modelMap = {
|
|
257
|
+
"simple-fast": "gpt-3.5-turbo",
|
|
258
|
+
"simple-accurate": "gpt-4",
|
|
259
|
+
"complex-fast": "gpt-4",
|
|
260
|
+
"complex-accurate": "gpt-4-turbo",
|
|
261
|
+
specialized: "claude-3-opus",
|
|
262
|
+
};
|
|
263
|
+
const key =
|
|
264
|
+
complexity === "high"
|
|
265
|
+
? speed === "fast"
|
|
266
|
+
? "complex-fast"
|
|
267
|
+
: "complex-accurate"
|
|
268
|
+
: speed === "fast"
|
|
269
|
+
? "simple-fast"
|
|
270
|
+
: "simple-accurate";
|
|
271
|
+
return modelMap[key] || "gpt-4";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function toAbsFileURL(p) {
|
|
275
|
+
if (!path.isAbsolute(p)) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Task module path must be absolute. Received: ${p}\n` +
|
|
278
|
+
`Hint: Task paths should be resolved by pipeline-runner.js using the task registry.`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return pathToFileURL(p);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function normalizeError(err) {
|
|
285
|
+
if (err instanceof Error)
|
|
286
|
+
return { name: err.name, message: err.message, stack: err.stack };
|
|
287
|
+
return { message: String(err) };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// CLI shim (optional)
|
|
291
|
+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
292
|
+
const modulePath = process.argv[2] || "./tasks/index.js";
|
|
293
|
+
const initJson = process.argv[3];
|
|
294
|
+
const initialContext = initJson ? JSON.parse(initJson) : {};
|
|
295
|
+
runPipeline(modulePath, initialContext)
|
|
296
|
+
.then((result) => {
|
|
297
|
+
const code = result.ok ? 0 : 1;
|
|
298
|
+
console.log(JSON.stringify(result, null, 2));
|
|
299
|
+
process.exit(code);
|
|
300
|
+
})
|
|
301
|
+
.catch((e) => {
|
|
302
|
+
console.error("Runner failed:", e);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import { getConfig } from "./config.js";
|
|
3
|
+
|
|
4
|
+
const ajv = new Ajv({ allErrors: true });
|
|
5
|
+
|
|
6
|
+
// JSON schema for seed file structure - uses config for validation rules
|
|
7
|
+
function getSeedSchema() {
|
|
8
|
+
const config = getConfig();
|
|
9
|
+
return {
|
|
10
|
+
type: "object",
|
|
11
|
+
required: ["name", "data"],
|
|
12
|
+
properties: {
|
|
13
|
+
name: {
|
|
14
|
+
type: "string",
|
|
15
|
+
minLength: config.validation.seedNameMinLength,
|
|
16
|
+
maxLength: config.validation.seedNameMaxLength,
|
|
17
|
+
pattern: config.validation.seedNamePattern,
|
|
18
|
+
description: "Job name (alphanumeric, hyphens, underscores only)",
|
|
19
|
+
},
|
|
20
|
+
data: {
|
|
21
|
+
type: "object",
|
|
22
|
+
description: "Job data payload",
|
|
23
|
+
},
|
|
24
|
+
metadata: {
|
|
25
|
+
type: "object",
|
|
26
|
+
description: "Optional metadata",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validates a seed file structure
|
|
35
|
+
* @param {object} seed - The seed object to validate
|
|
36
|
+
* @returns {object} Validation result with { valid: boolean, errors?: array }
|
|
37
|
+
*/
|
|
38
|
+
export function validateSeed(seed) {
|
|
39
|
+
// Check if seed is an object
|
|
40
|
+
if (!seed || typeof seed !== "object") {
|
|
41
|
+
return {
|
|
42
|
+
valid: false,
|
|
43
|
+
errors: [
|
|
44
|
+
{
|
|
45
|
+
message: "Seed must be a valid JSON object",
|
|
46
|
+
path: "",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Compile schema with current config values
|
|
53
|
+
const seedSchema = getSeedSchema();
|
|
54
|
+
const validateSeedSchema = ajv.compile(seedSchema);
|
|
55
|
+
const valid = validateSeedSchema(seed);
|
|
56
|
+
|
|
57
|
+
if (!valid) {
|
|
58
|
+
return {
|
|
59
|
+
valid: false,
|
|
60
|
+
errors: validateSeedSchema.errors.map((err) => ({
|
|
61
|
+
message: err.message,
|
|
62
|
+
path: err.instancePath || err.dataPath || "",
|
|
63
|
+
params: err.params,
|
|
64
|
+
keyword: err.keyword,
|
|
65
|
+
})),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { valid: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Formats validation errors into a human-readable message
|
|
74
|
+
* @param {array} errors - Array of validation errors
|
|
75
|
+
* @returns {string} Formatted error message
|
|
76
|
+
*/
|
|
77
|
+
export function formatValidationErrors(errors) {
|
|
78
|
+
if (!errors || errors.length === 0) {
|
|
79
|
+
return "Unknown validation error";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const messages = errors.map((err) => {
|
|
83
|
+
const path = err.path ? `at '${err.path}'` : "";
|
|
84
|
+
return ` - ${err.message} ${path}`.trim();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return `Seed validation failed:\n${messages.join("\n")}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validates seed and throws if invalid
|
|
92
|
+
* @param {object} seed - The seed object to validate
|
|
93
|
+
* @throws {Error} If validation fails
|
|
94
|
+
*/
|
|
95
|
+
export function validateSeedOrThrow(seed) {
|
|
96
|
+
const result = validateSeed(seed);
|
|
97
|
+
if (!result.valid) {
|
|
98
|
+
throw new Error(formatValidationErrors(result.errors));
|
|
99
|
+
}
|
|
100
|
+
}
|