@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,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized configuration management for Prompt Orchestration Pipeline
|
|
3
|
+
*
|
|
4
|
+
* This module provides a single source of truth for all configuration values,
|
|
5
|
+
* supporting both environment variables and config file overrides.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { promises as fs } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default configuration values
|
|
13
|
+
* These can be overridden by environment variables or config file
|
|
14
|
+
*/
|
|
15
|
+
export const defaultConfig = {
|
|
16
|
+
orchestrator: {
|
|
17
|
+
shutdownTimeout: 2000,
|
|
18
|
+
processSpawnRetries: 3,
|
|
19
|
+
processSpawnRetryDelay: 1000,
|
|
20
|
+
lockFileTimeout: 5000,
|
|
21
|
+
watchDebounce: 100,
|
|
22
|
+
watchStabilityThreshold: 200,
|
|
23
|
+
watchPollInterval: 50,
|
|
24
|
+
},
|
|
25
|
+
taskRunner: {
|
|
26
|
+
maxRefinementAttempts: 2,
|
|
27
|
+
stageTimeout: 300000,
|
|
28
|
+
llmRequestTimeout: 60000,
|
|
29
|
+
},
|
|
30
|
+
llm: {
|
|
31
|
+
defaultProvider: "openai",
|
|
32
|
+
defaultModel: "gpt-5-chat-latest",
|
|
33
|
+
maxConcurrency: 5,
|
|
34
|
+
retryMaxAttempts: 3,
|
|
35
|
+
retryBackoffMs: 1000,
|
|
36
|
+
},
|
|
37
|
+
ui: {
|
|
38
|
+
port: 3000,
|
|
39
|
+
host: "localhost",
|
|
40
|
+
heartbeatInterval: 30000,
|
|
41
|
+
maxRecentChanges: 10,
|
|
42
|
+
},
|
|
43
|
+
paths: {
|
|
44
|
+
root: process.env.PO_ROOT || process.cwd(),
|
|
45
|
+
dataDir: "pipeline-data",
|
|
46
|
+
configDir: "pipeline-config",
|
|
47
|
+
pendingDir: "pending",
|
|
48
|
+
currentDir: "current",
|
|
49
|
+
completeDir: "complete",
|
|
50
|
+
},
|
|
51
|
+
validation: {
|
|
52
|
+
seedNameMinLength: 1,
|
|
53
|
+
seedNameMaxLength: 100,
|
|
54
|
+
seedNamePattern: "^[a-zA-Z0-9-_]+$",
|
|
55
|
+
},
|
|
56
|
+
logging: {
|
|
57
|
+
level: "info",
|
|
58
|
+
format: "json",
|
|
59
|
+
destination: "stdout",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Current loaded configuration
|
|
65
|
+
* Initialized with defaults, then overridden by environment and config file
|
|
66
|
+
*/
|
|
67
|
+
let currentConfig = null;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Load configuration from environment variables
|
|
71
|
+
* Environment variables take precedence over defaults
|
|
72
|
+
*/
|
|
73
|
+
function loadFromEnvironment(config) {
|
|
74
|
+
const envConfig = { ...config };
|
|
75
|
+
|
|
76
|
+
// Orchestrator settings
|
|
77
|
+
if (process.env.PO_SHUTDOWN_TIMEOUT) {
|
|
78
|
+
envConfig.orchestrator.shutdownTimeout = parseInt(
|
|
79
|
+
process.env.PO_SHUTDOWN_TIMEOUT,
|
|
80
|
+
10
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (process.env.PO_PROCESS_SPAWN_RETRIES) {
|
|
84
|
+
envConfig.orchestrator.processSpawnRetries = parseInt(
|
|
85
|
+
process.env.PO_PROCESS_SPAWN_RETRIES,
|
|
86
|
+
10
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (process.env.PO_LOCK_FILE_TIMEOUT) {
|
|
90
|
+
envConfig.orchestrator.lockFileTimeout = parseInt(
|
|
91
|
+
process.env.PO_LOCK_FILE_TIMEOUT,
|
|
92
|
+
10
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (process.env.PO_WATCH_DEBOUNCE) {
|
|
96
|
+
envConfig.orchestrator.watchDebounce = parseInt(
|
|
97
|
+
process.env.PO_WATCH_DEBOUNCE,
|
|
98
|
+
10
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Task runner settings
|
|
103
|
+
if (process.env.PO_MAX_REFINEMENT_ATTEMPTS) {
|
|
104
|
+
envConfig.taskRunner.maxRefinementAttempts = parseInt(
|
|
105
|
+
process.env.PO_MAX_REFINEMENT_ATTEMPTS,
|
|
106
|
+
10
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (process.env.PO_STAGE_TIMEOUT) {
|
|
110
|
+
envConfig.taskRunner.stageTimeout = parseInt(
|
|
111
|
+
process.env.PO_STAGE_TIMEOUT,
|
|
112
|
+
10
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (process.env.PO_LLM_REQUEST_TIMEOUT) {
|
|
116
|
+
envConfig.taskRunner.llmRequestTimeout = parseInt(
|
|
117
|
+
process.env.PO_LLM_REQUEST_TIMEOUT,
|
|
118
|
+
10
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// LLM settings
|
|
123
|
+
if (process.env.PO_DEFAULT_PROVIDER) {
|
|
124
|
+
envConfig.llm.defaultProvider = process.env.PO_DEFAULT_PROVIDER;
|
|
125
|
+
}
|
|
126
|
+
if (process.env.PO_DEFAULT_MODEL) {
|
|
127
|
+
envConfig.llm.defaultModel = process.env.PO_DEFAULT_MODEL;
|
|
128
|
+
}
|
|
129
|
+
if (process.env.PO_MAX_CONCURRENCY) {
|
|
130
|
+
envConfig.llm.maxConcurrency = parseInt(process.env.PO_MAX_CONCURRENCY, 10);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// UI settings
|
|
134
|
+
if (process.env.PO_UI_PORT || process.env.PORT) {
|
|
135
|
+
envConfig.ui.port = parseInt(
|
|
136
|
+
process.env.PO_UI_PORT || process.env.PORT,
|
|
137
|
+
10
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (process.env.PO_UI_HOST) {
|
|
141
|
+
envConfig.ui.host = process.env.PO_UI_HOST;
|
|
142
|
+
}
|
|
143
|
+
if (process.env.PO_HEARTBEAT_INTERVAL) {
|
|
144
|
+
envConfig.ui.heartbeatInterval = parseInt(
|
|
145
|
+
process.env.PO_HEARTBEAT_INTERVAL,
|
|
146
|
+
10
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Path settings
|
|
151
|
+
if (process.env.PO_ROOT) {
|
|
152
|
+
envConfig.paths.root = process.env.PO_ROOT;
|
|
153
|
+
}
|
|
154
|
+
if (process.env.PO_DATA_DIR) {
|
|
155
|
+
envConfig.paths.dataDir = process.env.PO_DATA_DIR;
|
|
156
|
+
}
|
|
157
|
+
if (process.env.PO_CONFIG_DIR) {
|
|
158
|
+
envConfig.paths.configDir = process.env.PO_CONFIG_DIR;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Logging settings
|
|
162
|
+
if (process.env.PO_LOG_LEVEL) {
|
|
163
|
+
envConfig.logging.level = process.env.PO_LOG_LEVEL;
|
|
164
|
+
}
|
|
165
|
+
if (process.env.PO_LOG_FORMAT) {
|
|
166
|
+
envConfig.logging.format = process.env.PO_LOG_FORMAT;
|
|
167
|
+
}
|
|
168
|
+
if (process.env.PO_LOG_DESTINATION) {
|
|
169
|
+
envConfig.logging.destination = process.env.PO_LOG_DESTINATION;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return envConfig;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Deep merge two configuration objects
|
|
177
|
+
*/
|
|
178
|
+
function deepMerge(target, source) {
|
|
179
|
+
const result = { ...target };
|
|
180
|
+
|
|
181
|
+
for (const key in source) {
|
|
182
|
+
if (
|
|
183
|
+
source[key] &&
|
|
184
|
+
typeof source[key] === "object" &&
|
|
185
|
+
!Array.isArray(source[key])
|
|
186
|
+
) {
|
|
187
|
+
result[key] = deepMerge(target[key] || {}, source[key]);
|
|
188
|
+
} else {
|
|
189
|
+
result[key] = source[key];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Load configuration from a JSON file
|
|
198
|
+
* Returns null if file doesn't exist
|
|
199
|
+
*/
|
|
200
|
+
async function loadFromFile(configPath) {
|
|
201
|
+
try {
|
|
202
|
+
const content = await fs.readFile(configPath, "utf8");
|
|
203
|
+
return JSON.parse(content);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error.code === "ENOENT") {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Failed to load config file ${configPath}: ${error.message}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Validate configuration values
|
|
216
|
+
* Throws if configuration is invalid
|
|
217
|
+
*/
|
|
218
|
+
function validateConfig(config) {
|
|
219
|
+
const errors = [];
|
|
220
|
+
|
|
221
|
+
// Validate numeric values are positive
|
|
222
|
+
if (config.orchestrator.shutdownTimeout <= 0) {
|
|
223
|
+
errors.push("orchestrator.shutdownTimeout must be positive");
|
|
224
|
+
}
|
|
225
|
+
if (config.orchestrator.processSpawnRetries < 0) {
|
|
226
|
+
errors.push("orchestrator.processSpawnRetries must be non-negative");
|
|
227
|
+
}
|
|
228
|
+
if (config.taskRunner.maxRefinementAttempts < 0) {
|
|
229
|
+
errors.push("taskRunner.maxRefinementAttempts must be non-negative");
|
|
230
|
+
}
|
|
231
|
+
if (config.llm.maxConcurrency <= 0) {
|
|
232
|
+
errors.push("llm.maxConcurrency must be positive");
|
|
233
|
+
}
|
|
234
|
+
if (config.ui.port < 1 || config.ui.port > 65535) {
|
|
235
|
+
errors.push("ui.port must be between 1 and 65535");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Validate provider
|
|
239
|
+
const validProviders = ["openai", "deepseek", "anthropic"];
|
|
240
|
+
if (!validProviders.includes(config.llm.defaultProvider)) {
|
|
241
|
+
errors.push(
|
|
242
|
+
`llm.defaultProvider must be one of: ${validProviders.join(", ")}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Validate log level
|
|
247
|
+
const validLogLevels = ["debug", "info", "warn", "error"];
|
|
248
|
+
if (!validLogLevels.includes(config.logging.level)) {
|
|
249
|
+
errors.push(`logging.level must be one of: ${validLogLevels.join(", ")}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (errors.length > 0) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`Configuration validation failed:\n${errors.map((e) => ` - ${e}`).join("\n")}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Initialize and load configuration
|
|
261
|
+
*
|
|
262
|
+
* Priority order (highest to lowest):
|
|
263
|
+
* 1. Environment variables
|
|
264
|
+
* 2. Config file (if provided)
|
|
265
|
+
* 3. Default values
|
|
266
|
+
*
|
|
267
|
+
* @param {Object} options - Configuration options
|
|
268
|
+
* @param {string} options.configPath - Path to config file (optional)
|
|
269
|
+
* @param {boolean} options.validate - Whether to validate config (default: true)
|
|
270
|
+
* @returns {Object} Loaded configuration
|
|
271
|
+
*/
|
|
272
|
+
export async function loadConfig(options = {}) {
|
|
273
|
+
const { configPath, validate = true } = options;
|
|
274
|
+
|
|
275
|
+
// Start with defaults
|
|
276
|
+
let config = JSON.parse(JSON.stringify(defaultConfig));
|
|
277
|
+
|
|
278
|
+
// Load from config file if provided
|
|
279
|
+
if (configPath) {
|
|
280
|
+
const fileConfig = await loadFromFile(configPath);
|
|
281
|
+
if (fileConfig) {
|
|
282
|
+
config = deepMerge(config, fileConfig);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Override with environment variables
|
|
287
|
+
config = loadFromEnvironment(config);
|
|
288
|
+
|
|
289
|
+
// Validate if requested
|
|
290
|
+
if (validate) {
|
|
291
|
+
validateConfig(config);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Cache the loaded config
|
|
295
|
+
currentConfig = config;
|
|
296
|
+
|
|
297
|
+
return config;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get the current configuration
|
|
302
|
+
* Loads default config if not already loaded
|
|
303
|
+
*
|
|
304
|
+
* @returns {Object} Current configuration
|
|
305
|
+
*/
|
|
306
|
+
export function getConfig() {
|
|
307
|
+
if (!currentConfig) {
|
|
308
|
+
// Load defaults synchronously for first access
|
|
309
|
+
currentConfig = loadFromEnvironment(
|
|
310
|
+
JSON.parse(JSON.stringify(defaultConfig))
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
return currentConfig;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Reset configuration to defaults
|
|
318
|
+
* Useful for testing
|
|
319
|
+
*/
|
|
320
|
+
export function resetConfig() {
|
|
321
|
+
currentConfig = null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get a specific configuration value by path
|
|
326
|
+
*
|
|
327
|
+
* @param {string} path - Dot-separated path (e.g., "orchestrator.shutdownTimeout")
|
|
328
|
+
* @param {*} defaultValue - Default value if path not found
|
|
329
|
+
* @returns {*} Configuration value
|
|
330
|
+
*/
|
|
331
|
+
export function getConfigValue(path, defaultValue = undefined) {
|
|
332
|
+
const config = getConfig();
|
|
333
|
+
const parts = path.split(".");
|
|
334
|
+
let value = config;
|
|
335
|
+
|
|
336
|
+
for (const part of parts) {
|
|
337
|
+
if (value && typeof value === "object" && part in value) {
|
|
338
|
+
value = value[part];
|
|
339
|
+
} else {
|
|
340
|
+
return defaultValue;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return value;
|
|
345
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { config } from "dotenv";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
|
|
5
|
+
export async function loadEnvironment(options = {}) {
|
|
6
|
+
const rootDir = options.rootDir || process.cwd();
|
|
7
|
+
const envFiles = options.envFiles || [".env", ".env.local"];
|
|
8
|
+
const loaded = [];
|
|
9
|
+
|
|
10
|
+
for (const envFile of envFiles) {
|
|
11
|
+
const envPath = path.join(rootDir, envFile);
|
|
12
|
+
if (fs.existsSync(envPath)) {
|
|
13
|
+
config({ path: envPath, override: true });
|
|
14
|
+
loaded.push(envFile);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const warnings = validateEnvironment();
|
|
19
|
+
return { loaded, warnings, config: getEnvironmentConfig() };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function validateEnvironment() {
|
|
23
|
+
const warnings = [];
|
|
24
|
+
const commonKeys = [
|
|
25
|
+
"OPENAI_API_KEY",
|
|
26
|
+
"ANTHROPIC_API_KEY",
|
|
27
|
+
"DEEPSEEK_API_KEY",
|
|
28
|
+
"GEMINI_API_KEY",
|
|
29
|
+
];
|
|
30
|
+
const foundKeys = commonKeys.filter((key) => process.env[key]);
|
|
31
|
+
if (foundKeys.length === 0) {
|
|
32
|
+
warnings.push("No LLM API keys found in environment.");
|
|
33
|
+
}
|
|
34
|
+
return warnings;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getEnvironmentConfig() {
|
|
38
|
+
return {
|
|
39
|
+
openai: {
|
|
40
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
41
|
+
organization: process.env.OPENAI_ORGANIZATION,
|
|
42
|
+
baseURL: process.env.OPENAI_BASE_URL,
|
|
43
|
+
},
|
|
44
|
+
anthropic: {
|
|
45
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
46
|
+
baseURL: process.env.ANTHROPIC_BASE_URL,
|
|
47
|
+
},
|
|
48
|
+
deepseek: {
|
|
49
|
+
apiKey: process.env.DEEPSEEK_API_KEY,
|
|
50
|
+
},
|
|
51
|
+
gemini: {
|
|
52
|
+
apiKey: process.env.GEMINI_API_KEY,
|
|
53
|
+
baseURL: process.env.GEMINI_BASE_URL,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|