@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.
@@ -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
+ }