@ryanfw/prompt-orchestration-pipeline 0.14.0 → 0.14.2
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/package.json +1 -1
- package/src/components/ui/RestartJobModal.jsx +24 -14
- package/src/core/pipeline-runner.js +30 -24
- package/src/llm/index.js +13 -2
- package/src/providers/anthropic.js +4 -1
- package/src/providers/base.js +27 -2
- package/src/providers/deepseek.js +8 -3
- package/src/providers/gemini.js +5 -2
- package/src/providers/openai.js +13 -5
- package/src/providers/zhipu.js +4 -1
- package/src/ui/dist/assets/{index-cjHV9mYW.js → index-B5HMRkR9.js} +22 -11
- package/src/ui/dist/assets/{index-cjHV9mYW.js.map → index-B5HMRkR9.js.map} +1 -1
- package/src/ui/dist/index.html +1 -1
- package/src/ui/endpoints/job-control-endpoints.js +72 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryanfw/prompt-orchestration-pipeline",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"description": "A Prompt-orchestration pipeline (POP) is a framework for building, running, and experimenting with complex chains of LLM tasks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/ui/server.js",
|
|
@@ -130,25 +130,35 @@ export function RestartJobModal({
|
|
|
130
130
|
Cancel
|
|
131
131
|
</Button>
|
|
132
132
|
|
|
133
|
-
{taskId
|
|
133
|
+
{taskId ? (
|
|
134
|
+
<>
|
|
135
|
+
<Button
|
|
136
|
+
variant="outline"
|
|
137
|
+
onClick={() => onConfirm({ singleTask: false })}
|
|
138
|
+
disabled={isSubmitting}
|
|
139
|
+
className="min-w-[120px]"
|
|
140
|
+
>
|
|
141
|
+
{isSubmitting ? "Restarting..." : "Restart entire pipeline"}
|
|
142
|
+
</Button>
|
|
143
|
+
<Button
|
|
144
|
+
variant="default"
|
|
145
|
+
onClick={() => onConfirm({ singleTask: true })}
|
|
146
|
+
disabled={isSubmitting}
|
|
147
|
+
className="min-w-[120px]"
|
|
148
|
+
>
|
|
149
|
+
{isSubmitting ? "Running..." : "Re-run this task"}
|
|
150
|
+
</Button>
|
|
151
|
+
</>
|
|
152
|
+
) : (
|
|
134
153
|
<Button
|
|
135
|
-
variant="
|
|
136
|
-
onClick={() => onConfirm({ singleTask:
|
|
154
|
+
variant="destructive"
|
|
155
|
+
onClick={() => onConfirm({ singleTask: false })}
|
|
137
156
|
disabled={isSubmitting}
|
|
138
|
-
className="min-w-[
|
|
157
|
+
className="min-w-[80px]"
|
|
139
158
|
>
|
|
140
|
-
{isSubmitting ? "
|
|
159
|
+
{isSubmitting ? "Restarting..." : "Restart"}
|
|
141
160
|
</Button>
|
|
142
161
|
)}
|
|
143
|
-
|
|
144
|
-
<Button
|
|
145
|
-
variant="destructive"
|
|
146
|
-
onClick={() => onConfirm({ singleTask: false })}
|
|
147
|
-
disabled={isSubmitting}
|
|
148
|
-
className="min-w-[80px]"
|
|
149
|
-
>
|
|
150
|
-
{isSubmitting ? "Restarting..." : "Restart"}
|
|
151
|
-
</Button>
|
|
152
162
|
</Flex>
|
|
153
163
|
</div>
|
|
154
164
|
</div>
|
|
@@ -18,6 +18,8 @@ import { createJobLogger } from "./logger.js";
|
|
|
18
18
|
import { LogEvent, LogFileExtension } from "../config/log-events.js";
|
|
19
19
|
import { decideTransition } from "./lifecycle-policy.js";
|
|
20
20
|
|
|
21
|
+
const getTaskName = (t) => (typeof t === "string" ? t : t.name);
|
|
22
|
+
|
|
21
23
|
const ROOT = process.env.PO_ROOT || process.cwd();
|
|
22
24
|
const DATA_DIR = path.join(ROOT, process.env.PO_DATA_DIR || "pipeline-data");
|
|
23
25
|
const CURRENT_DIR =
|
|
@@ -104,6 +106,8 @@ const pipeline = JSON.parse(await fs.readFile(PIPELINE_DEF_PATH, "utf8"));
|
|
|
104
106
|
// Validate pipeline format early with a friendly error message
|
|
105
107
|
validatePipelineOrThrow(pipeline, PIPELINE_DEF_PATH);
|
|
106
108
|
|
|
109
|
+
const taskNames = pipeline.tasks.map(getTaskName);
|
|
110
|
+
|
|
107
111
|
const tasks = (await loadFreshModule(TASK_REGISTRY)).default;
|
|
108
112
|
|
|
109
113
|
const status = JSON.parse(await fs.readFile(tasksStatusPath, "utf8"));
|
|
@@ -123,21 +127,21 @@ logger.group("Pipeline execution", {
|
|
|
123
127
|
|
|
124
128
|
// Helper function to check if all upstream dependencies are completed
|
|
125
129
|
function areDependenciesReady(taskName) {
|
|
126
|
-
const taskIndex =
|
|
130
|
+
const taskIndex = taskNames.indexOf(taskName);
|
|
127
131
|
if (taskIndex === -1) return false;
|
|
128
132
|
|
|
129
|
-
const upstreamTasks =
|
|
133
|
+
const upstreamTasks = taskNames.slice(0, taskIndex);
|
|
130
134
|
return upstreamTasks.every(
|
|
131
135
|
(upstreamTask) => status.tasks[upstreamTask]?.state === TaskState.DONE
|
|
132
136
|
);
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
try {
|
|
136
|
-
for (const taskName of
|
|
140
|
+
for (const taskName of taskNames) {
|
|
137
141
|
// Skip tasks before startFromTask when targeting a specific restart point
|
|
138
142
|
if (
|
|
139
143
|
startFromTask &&
|
|
140
|
-
|
|
144
|
+
taskNames.indexOf(taskName) < taskNames.indexOf(startFromTask)
|
|
141
145
|
) {
|
|
142
146
|
logger.log("Skipping task before restart point", {
|
|
143
147
|
taskName,
|
|
@@ -158,30 +162,32 @@ try {
|
|
|
158
162
|
continue;
|
|
159
163
|
}
|
|
160
164
|
|
|
161
|
-
// Check lifecycle policy before starting task
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const lifecycleDecision = decideTransition({
|
|
166
|
-
op: "start",
|
|
167
|
-
taskState: currentTaskState,
|
|
168
|
-
dependenciesReady,
|
|
169
|
-
});
|
|
165
|
+
// Check lifecycle policy before starting task (skip when startFromTask is set - user explicitly requested this task)
|
|
166
|
+
if (!startFromTask) {
|
|
167
|
+
const currentTaskState = status.tasks[taskName]?.state || "pending";
|
|
168
|
+
const dependenciesReady = areDependenciesReady(taskName);
|
|
170
169
|
|
|
171
|
-
|
|
172
|
-
logger.warn("lifecycle_block", {
|
|
173
|
-
jobId,
|
|
174
|
-
taskId: taskName,
|
|
170
|
+
const lifecycleDecision = decideTransition({
|
|
175
171
|
op: "start",
|
|
176
|
-
|
|
172
|
+
taskState: currentTaskState,
|
|
173
|
+
dependenciesReady,
|
|
177
174
|
});
|
|
178
175
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
176
|
+
if (!lifecycleDecision.ok) {
|
|
177
|
+
logger.warn("lifecycle_block", {
|
|
178
|
+
jobId,
|
|
179
|
+
taskId: taskName,
|
|
180
|
+
op: "start",
|
|
181
|
+
reason: lifecycleDecision.reason,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Create typed error for endpoints to handle
|
|
185
|
+
const lifecycleError = new Error(lifecycleDecision.reason);
|
|
186
|
+
lifecycleError.httpStatus = 409;
|
|
187
|
+
lifecycleError.error = "unsupported_lifecycle";
|
|
188
|
+
lifecycleError.reason = lifecycleDecision.reason;
|
|
189
|
+
throw lifecycleError;
|
|
190
|
+
}
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
logger.log("Starting task", { taskName });
|
package/src/llm/index.js
CHANGED
|
@@ -407,6 +407,17 @@ export async function chat(options) {
|
|
|
407
407
|
}
|
|
408
408
|
} else if (provider === "gemini") {
|
|
409
409
|
console.log("[llm] Using Gemini provider");
|
|
410
|
+
|
|
411
|
+
// Infer JSON format if not explicitly provided
|
|
412
|
+
const effectiveResponseFormat =
|
|
413
|
+
responseFormat === undefined ||
|
|
414
|
+
responseFormat === null ||
|
|
415
|
+
responseFormat === ""
|
|
416
|
+
? shouldInferJsonFormat(messages)
|
|
417
|
+
? "json_object"
|
|
418
|
+
: undefined
|
|
419
|
+
: responseFormat;
|
|
420
|
+
|
|
410
421
|
const geminiArgs = {
|
|
411
422
|
messages,
|
|
412
423
|
model: model || "gemini-2.5-flash",
|
|
@@ -421,8 +432,8 @@ export async function chat(options) {
|
|
|
421
432
|
});
|
|
422
433
|
if (topP !== undefined) geminiArgs.topP = topP;
|
|
423
434
|
if (stop !== undefined) geminiArgs.stop = stop;
|
|
424
|
-
if (
|
|
425
|
-
geminiArgs.responseFormat =
|
|
435
|
+
if (effectiveResponseFormat !== undefined) {
|
|
436
|
+
geminiArgs.responseFormat = effectiveResponseFormat;
|
|
426
437
|
}
|
|
427
438
|
|
|
428
439
|
console.log("[llm] Calling geminiChat()...");
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
extractMessages,
|
|
3
3
|
isRetryableError,
|
|
4
4
|
sleep,
|
|
5
|
+
stripMarkdownFences,
|
|
5
6
|
tryParseJSON,
|
|
6
7
|
ensureJsonResponseFormat,
|
|
7
8
|
ProviderJsonParseError,
|
|
@@ -77,10 +78,12 @@ export async function anthropicChat({
|
|
|
77
78
|
|
|
78
79
|
// Extract text from response.content blocks
|
|
79
80
|
const blocks = Array.isArray(data?.content) ? data.content : [];
|
|
80
|
-
const
|
|
81
|
+
const rawText = blocks
|
|
81
82
|
.filter((b) => b?.type === "text" && typeof b.text === "string")
|
|
82
83
|
.map((b) => b.text)
|
|
83
84
|
.join("");
|
|
85
|
+
// Always strip markdown fences first to prevent parse failures
|
|
86
|
+
const text = stripMarkdownFences(rawText);
|
|
84
87
|
console.log("[Anthropic] Response text length:", text.length);
|
|
85
88
|
|
|
86
89
|
// Parse JSON - this is required for all calls
|
package/src/providers/base.js
CHANGED
|
@@ -33,6 +33,25 @@ export async function sleep(ms) {
|
|
|
33
33
|
return new Promise((r) => setTimeout(r, ms));
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Strip markdown code fences from text unconditionally.
|
|
38
|
+
* Handles ```json, ```JSON, and plain ``` with or without newlines.
|
|
39
|
+
* @param {string} text - The text to strip fences from
|
|
40
|
+
* @returns {string} The cleaned text, or original if not a string
|
|
41
|
+
*/
|
|
42
|
+
export function stripMarkdownFences(text) {
|
|
43
|
+
if (typeof text !== "string") return text;
|
|
44
|
+
const trimmed = text.trim();
|
|
45
|
+
if (trimmed.startsWith("```")) {
|
|
46
|
+
// Remove opening fence (```json, ```JSON, or just ```)
|
|
47
|
+
let cleaned = trimmed.replace(/^```(?:json|JSON)?\s*\n?/, "");
|
|
48
|
+
// Remove closing fence
|
|
49
|
+
cleaned = cleaned.replace(/\n?```\s*$/, "");
|
|
50
|
+
return cleaned.trim();
|
|
51
|
+
}
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
|
|
36
55
|
export function tryParseJSON(text) {
|
|
37
56
|
try {
|
|
38
57
|
return JSON.parse(text);
|
|
@@ -85,7 +104,12 @@ export class ProviderJsonModeError extends Error {
|
|
|
85
104
|
* Error thrown when JSON parsing fails and should not be retried
|
|
86
105
|
*/
|
|
87
106
|
export class ProviderJsonParseError extends Error {
|
|
88
|
-
constructor(
|
|
107
|
+
constructor(
|
|
108
|
+
provider,
|
|
109
|
+
model,
|
|
110
|
+
sample,
|
|
111
|
+
message = "Failed to parse JSON response"
|
|
112
|
+
) {
|
|
89
113
|
super(message);
|
|
90
114
|
this.name = "ProviderJsonParseError";
|
|
91
115
|
this.provider = provider;
|
|
@@ -109,8 +133,9 @@ export function ensureJsonResponseFormat(responseFormat, providerName) {
|
|
|
109
133
|
}
|
|
110
134
|
|
|
111
135
|
// Check for valid JSON format types
|
|
112
|
-
const isValidJsonFormat =
|
|
136
|
+
const isValidJsonFormat =
|
|
113
137
|
responseFormat === "json" ||
|
|
138
|
+
responseFormat === "json_object" ||
|
|
114
139
|
responseFormat?.type === "json_object" ||
|
|
115
140
|
responseFormat?.type === "json_schema";
|
|
116
141
|
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
extractMessages,
|
|
3
3
|
isRetryableError,
|
|
4
4
|
sleep,
|
|
5
|
+
stripMarkdownFences,
|
|
5
6
|
tryParseJSON,
|
|
6
7
|
ensureJsonResponseFormat,
|
|
7
8
|
ProviderJsonParseError,
|
|
@@ -24,11 +25,12 @@ export async function deepseekChat({
|
|
|
24
25
|
throw new Error("DeepSeek API key not configured");
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
// Determine if JSON mode is requested
|
|
28
|
+
// Determine if JSON mode is requested (handle both object and string formats)
|
|
28
29
|
const isJsonMode =
|
|
29
30
|
responseFormat?.type === "json_object" ||
|
|
30
31
|
responseFormat?.type === "json_schema" ||
|
|
31
|
-
responseFormat === "json"
|
|
32
|
+
responseFormat === "json" ||
|
|
33
|
+
responseFormat === "json_object";
|
|
32
34
|
|
|
33
35
|
const { systemMsg, userMsg } = extractMessages(messages);
|
|
34
36
|
|
|
@@ -84,7 +86,10 @@ export async function deepseekChat({
|
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
const data = await response.json();
|
|
87
|
-
const
|
|
89
|
+
const rawContent = data.choices[0].message.content;
|
|
90
|
+
|
|
91
|
+
// Always strip markdown fences first to prevent parse failures
|
|
92
|
+
const content = stripMarkdownFences(rawContent);
|
|
88
93
|
|
|
89
94
|
// Parse JSON only in JSON mode; return raw string for text mode
|
|
90
95
|
if (isJsonMode) {
|
package/src/providers/gemini.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
extractMessages,
|
|
3
3
|
isRetryableError,
|
|
4
4
|
sleep,
|
|
5
|
+
stripMarkdownFences,
|
|
5
6
|
tryParseJSON,
|
|
6
7
|
ensureJsonResponseFormat,
|
|
7
8
|
ProviderJsonParseError,
|
|
@@ -32,7 +33,7 @@ export async function geminiChat(options) {
|
|
|
32
33
|
frequencyPenalty,
|
|
33
34
|
presencePenalty,
|
|
34
35
|
stop,
|
|
35
|
-
maxRetries = 3
|
|
36
|
+
maxRetries = 3,
|
|
36
37
|
} = options;
|
|
37
38
|
|
|
38
39
|
// Validate response format (Gemini only supports JSON mode)
|
|
@@ -171,7 +172,9 @@ export async function geminiChat(options) {
|
|
|
171
172
|
throw new Error("No content returned from Gemini API");
|
|
172
173
|
}
|
|
173
174
|
|
|
174
|
-
const
|
|
175
|
+
const rawText = candidate.content.parts[0].text;
|
|
176
|
+
// Always strip markdown fences first to prevent parse failures
|
|
177
|
+
const text = stripMarkdownFences(rawText);
|
|
175
178
|
console.log(`[Gemini] Text length: ${text.length}`);
|
|
176
179
|
|
|
177
180
|
// Parse JSON if required
|
package/src/providers/openai.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
extractMessages,
|
|
4
4
|
isRetryableError,
|
|
5
5
|
sleep,
|
|
6
|
+
stripMarkdownFences,
|
|
6
7
|
tryParseJSON,
|
|
7
8
|
ensureJsonResponseFormat,
|
|
8
9
|
ProviderJsonParseError,
|
|
@@ -53,11 +54,12 @@ export async function openaiChat({
|
|
|
53
54
|
console.log("[OpenAI] System message length:", systemMsg.length);
|
|
54
55
|
console.log("[OpenAI] User message length:", userMsg.length);
|
|
55
56
|
|
|
56
|
-
// Determine if JSON mode is requested
|
|
57
|
+
// Determine if JSON mode is requested (handle both object and string formats)
|
|
57
58
|
const isJsonMode =
|
|
58
59
|
responseFormat?.json_schema ||
|
|
59
60
|
responseFormat?.type === "json_object" ||
|
|
60
|
-
responseFormat === "json"
|
|
61
|
+
responseFormat === "json" ||
|
|
62
|
+
responseFormat === "json_object";
|
|
61
63
|
|
|
62
64
|
let lastError;
|
|
63
65
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
@@ -100,7 +102,9 @@ export async function openaiChat({
|
|
|
100
102
|
|
|
101
103
|
console.log("[OpenAI] Calling responses.create...");
|
|
102
104
|
const resp = await openai.responses.create(responsesReq);
|
|
103
|
-
const
|
|
105
|
+
const rawText = resp.output_text ?? "";
|
|
106
|
+
// Always strip markdown fences first to prevent parse failures
|
|
107
|
+
const text = stripMarkdownFences(rawText);
|
|
104
108
|
console.log("[OpenAI] Response received, text length:", text.length);
|
|
105
109
|
|
|
106
110
|
// Approximate usage (tests don't assert exact values)
|
|
@@ -161,7 +165,9 @@ export async function openaiChat({
|
|
|
161
165
|
|
|
162
166
|
console.log("[OpenAI] Calling chat.completions.create...");
|
|
163
167
|
const classicRes = await openai.chat.completions.create(classicReq);
|
|
164
|
-
const
|
|
168
|
+
const rawClassicText = classicRes?.choices?.[0]?.message?.content ?? "";
|
|
169
|
+
// Always strip markdown fences first to prevent parse failures
|
|
170
|
+
const classicText = stripMarkdownFences(rawClassicText);
|
|
165
171
|
console.log(
|
|
166
172
|
"[OpenAI] Response received, text length:",
|
|
167
173
|
classicText.length
|
|
@@ -228,7 +234,9 @@ export async function openaiChat({
|
|
|
228
234
|
}
|
|
229
235
|
|
|
230
236
|
const classicRes = await openai.chat.completions.create(classicReq);
|
|
231
|
-
const
|
|
237
|
+
const rawText = classicRes?.choices?.[0]?.message?.content ?? "";
|
|
238
|
+
// Always strip markdown fences first to prevent parse failures
|
|
239
|
+
const text = stripMarkdownFences(rawText);
|
|
232
240
|
|
|
233
241
|
// Parse JSON only in JSON mode; return raw string for text mode
|
|
234
242
|
if (isJsonMode) {
|
package/src/providers/zhipu.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
extractMessages,
|
|
3
3
|
isRetryableError,
|
|
4
4
|
sleep,
|
|
5
|
+
stripMarkdownFences,
|
|
5
6
|
tryParseJSON,
|
|
6
7
|
ensureJsonResponseFormat,
|
|
7
8
|
ProviderJsonParseError,
|
|
@@ -104,7 +105,9 @@ export async function zhipuChat({
|
|
|
104
105
|
console.log("[Zhipu] Response received from Zhipu API");
|
|
105
106
|
|
|
106
107
|
// Extract text from response
|
|
107
|
-
const
|
|
108
|
+
const rawText = data?.choices?.[0]?.message?.content || "";
|
|
109
|
+
// Always strip markdown fences first to prevent parse failures
|
|
110
|
+
const text = stripMarkdownFences(rawText);
|
|
108
111
|
console.log("[Zhipu] Response text length:", text.length);
|
|
109
112
|
|
|
110
113
|
// Parse JSON - this is required for all calls
|
|
@@ -24711,17 +24711,28 @@ function RestartJobModal({
|
|
|
24711
24711
|
children: "Cancel"
|
|
24712
24712
|
}
|
|
24713
24713
|
),
|
|
24714
|
-
taskId
|
|
24715
|
-
|
|
24716
|
-
|
|
24717
|
-
|
|
24718
|
-
|
|
24719
|
-
|
|
24720
|
-
|
|
24721
|
-
|
|
24722
|
-
|
|
24723
|
-
|
|
24724
|
-
|
|
24714
|
+
taskId ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
|
|
24715
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
24716
|
+
Button,
|
|
24717
|
+
{
|
|
24718
|
+
variant: "outline",
|
|
24719
|
+
onClick: () => onConfirm({ singleTask: false }),
|
|
24720
|
+
disabled: isSubmitting,
|
|
24721
|
+
className: "min-w-[120px]",
|
|
24722
|
+
children: isSubmitting ? "Restarting..." : "Restart entire pipeline"
|
|
24723
|
+
}
|
|
24724
|
+
),
|
|
24725
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
24726
|
+
Button,
|
|
24727
|
+
{
|
|
24728
|
+
variant: "default",
|
|
24729
|
+
onClick: () => onConfirm({ singleTask: true }),
|
|
24730
|
+
disabled: isSubmitting,
|
|
24731
|
+
className: "min-w-[120px]",
|
|
24732
|
+
children: isSubmitting ? "Running..." : "Re-run this task"
|
|
24733
|
+
}
|
|
24734
|
+
)
|
|
24735
|
+
] }) : /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
24725
24736
|
Button,
|
|
24726
24737
|
{
|
|
24727
24738
|
variant: "destructive",
|