@ryanfw/prompt-orchestration-pipeline 0.12.0 → 0.13.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.
Files changed (60) hide show
  1. package/package.json +10 -1
  2. package/src/cli/analyze-task.js +51 -0
  3. package/src/cli/index.js +8 -0
  4. package/src/components/AddPipelineSidebar.jsx +144 -0
  5. package/src/components/AnalysisProgressTray.jsx +87 -0
  6. package/src/components/JobTable.jsx +4 -3
  7. package/src/components/Layout.jsx +142 -139
  8. package/src/components/MarkdownRenderer.jsx +149 -0
  9. package/src/components/PipelineDAGGrid.jsx +404 -0
  10. package/src/components/PipelineTypeTaskSidebar.jsx +96 -0
  11. package/src/components/SchemaPreviewPanel.jsx +97 -0
  12. package/src/components/StageTimeline.jsx +36 -0
  13. package/src/components/TaskAnalysisDisplay.jsx +227 -0
  14. package/src/components/TaskCreationSidebar.jsx +447 -0
  15. package/src/components/TaskDetailSidebar.jsx +119 -117
  16. package/src/components/TaskFilePane.jsx +94 -39
  17. package/src/components/ui/button.jsx +59 -27
  18. package/src/components/ui/sidebar.jsx +118 -0
  19. package/src/config/models.js +99 -67
  20. package/src/core/config.js +4 -1
  21. package/src/llm/index.js +129 -9
  22. package/src/pages/PipelineDetail.jsx +6 -6
  23. package/src/pages/PipelineList.jsx +214 -0
  24. package/src/pages/PipelineTypeDetail.jsx +234 -0
  25. package/src/providers/deepseek.js +76 -16
  26. package/src/providers/openai.js +61 -34
  27. package/src/task-analysis/enrichers/analysis-writer.js +62 -0
  28. package/src/task-analysis/enrichers/schema-deducer.js +145 -0
  29. package/src/task-analysis/enrichers/schema-writer.js +74 -0
  30. package/src/task-analysis/extractors/artifacts.js +137 -0
  31. package/src/task-analysis/extractors/llm-calls.js +176 -0
  32. package/src/task-analysis/extractors/stages.js +51 -0
  33. package/src/task-analysis/index.js +103 -0
  34. package/src/task-analysis/parser.js +28 -0
  35. package/src/task-analysis/utils/ast.js +43 -0
  36. package/src/ui/client/hooks/useAnalysisProgress.js +145 -0
  37. package/src/ui/client/index.css +64 -0
  38. package/src/ui/client/main.jsx +4 -0
  39. package/src/ui/client/sse-fetch.js +120 -0
  40. package/src/ui/dist/assets/index-cjHV9mYW.js +82578 -0
  41. package/src/ui/dist/assets/index-cjHV9mYW.js.map +1 -0
  42. package/src/ui/dist/assets/style-CoM9SoQF.css +180 -0
  43. package/src/ui/dist/index.html +2 -2
  44. package/src/ui/endpoints/create-pipeline-endpoint.js +194 -0
  45. package/src/ui/endpoints/pipeline-analysis-endpoint.js +246 -0
  46. package/src/ui/endpoints/pipeline-type-detail-endpoint.js +181 -0
  47. package/src/ui/endpoints/pipelines-endpoint.js +133 -0
  48. package/src/ui/endpoints/schema-file-endpoint.js +105 -0
  49. package/src/ui/endpoints/task-analysis-endpoint.js +104 -0
  50. package/src/ui/endpoints/task-creation-endpoint.js +114 -0
  51. package/src/ui/endpoints/task-save-endpoint.js +101 -0
  52. package/src/ui/express-app.js +45 -0
  53. package/src/ui/lib/analysis-lock.js +67 -0
  54. package/src/ui/lib/sse.js +30 -0
  55. package/src/ui/server.js +4 -0
  56. package/src/ui/utils/slug.js +31 -0
  57. package/src/ui/watcher.js +28 -2
  58. package/src/ui/dist/assets/index-B320avRx.js +0 -26613
  59. package/src/ui/dist/assets/index-B320avRx.js.map +0 -1
  60. package/src/ui/dist/assets/style-BYCoLBnK.css +0 -62
@@ -0,0 +1,234 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { useParams } from "react-router-dom";
3
+ import { Box, Flex, Text } from "@radix-ui/themes";
4
+ import Layout from "../components/Layout.jsx";
5
+ import PageSubheader from "../components/PageSubheader.jsx";
6
+ import { Button } from "../components/ui/button.jsx";
7
+
8
+ import PipelineDAGGrid from "../components/PipelineDAGGrid.jsx";
9
+ import TaskCreationSidebar from "../components/TaskCreationSidebar.jsx";
10
+ import { AnalysisProgressTray } from "../components/AnalysisProgressTray.jsx";
11
+ import { useAnalysisProgress } from "../ui/client/hooks/useAnalysisProgress.js";
12
+
13
+ export default function PipelineTypeDetail() {
14
+ const { slug } = useParams();
15
+ const [pipeline, setPipeline] = useState(null);
16
+ const [loading, setLoading] = useState(true);
17
+ const [error, setError] = useState(null);
18
+ const [sidebarOpen, setSidebarOpen] = useState(false);
19
+ const [trayDismissed, setTrayDismissed] = useState(false);
20
+
21
+ const { status, startAnalysis, reset, ...progressState } =
22
+ useAnalysisProgress();
23
+
24
+ const handleAnalyze = () => {
25
+ setTrayDismissed(false);
26
+ startAnalysis(slug);
27
+ };
28
+
29
+ useEffect(() => {
30
+ const fetchPipeline = async () => {
31
+ if (!slug) {
32
+ setError("No pipeline slug provided");
33
+ setLoading(false);
34
+ return;
35
+ }
36
+
37
+ try {
38
+ setLoading(true);
39
+ const response = await fetch(
40
+ `/api/pipelines/${encodeURIComponent(slug)}`
41
+ );
42
+
43
+ if (!response.ok) {
44
+ const errorData = await response.json().catch(() => ({}));
45
+ throw new Error(
46
+ errorData.message || `Failed to load pipeline: ${response.status}`
47
+ );
48
+ }
49
+
50
+ const data = await response.json();
51
+ if (!data.ok) {
52
+ throw new Error(data.message || "Failed to load pipeline");
53
+ }
54
+
55
+ setPipeline(data.data);
56
+ } catch (err) {
57
+ setError(err.message || "Failed to load pipeline");
58
+ } finally {
59
+ setLoading(false);
60
+ }
61
+ };
62
+
63
+ fetchPipeline();
64
+ }, [slug]);
65
+
66
+ // Handle missing slug
67
+ if (!slug) {
68
+ return (
69
+ <Layout
70
+ pageTitle="Pipeline Details"
71
+ breadcrumbs={[
72
+ { label: "Home", href: "/" },
73
+ { label: "Pipelines", href: "/pipelines" },
74
+ ]}
75
+ >
76
+ <Flex align="center" justify="center" className="min-h-64">
77
+ <Box className="text-center">
78
+ <Text size="5" weight="medium" color="red" className="mb-2">
79
+ No pipeline slug provided
80
+ </Text>
81
+ </Box>
82
+ </Flex>
83
+ </Layout>
84
+ );
85
+ }
86
+
87
+ // Loading state
88
+ if (loading) {
89
+ return (
90
+ <Layout
91
+ pageTitle="Pipeline Details"
92
+ breadcrumbs={[
93
+ { label: "Home", href: "/" },
94
+ { label: "Pipelines", href: "/pipelines" },
95
+ ]}
96
+ >
97
+ <Flex align="center" justify="center" className="min-h-64">
98
+ <Box className="text-center">
99
+ <Text size="5" weight="medium" className="mb-2">
100
+ Loading pipeline details...
101
+ </Text>
102
+ </Box>
103
+ </Flex>
104
+ </Layout>
105
+ );
106
+ }
107
+
108
+ // Error state
109
+ if (error) {
110
+ return (
111
+ <Layout
112
+ pageTitle="Pipeline Details"
113
+ breadcrumbs={[
114
+ { label: "Home", href: "/" },
115
+ { label: "Pipelines", href: "/pipelines" },
116
+ ]}
117
+ >
118
+ <Flex align="center" justify="center" className="min-h-64">
119
+ <Box className="text-center">
120
+ <Text size="5" weight="medium" color="red" className="mb-2">
121
+ Failed to load pipeline
122
+ </Text>
123
+ <Text size="2" color="gray" className="mt-2">
124
+ {error}
125
+ </Text>
126
+ </Box>
127
+ </Flex>
128
+ </Layout>
129
+ );
130
+ }
131
+
132
+ // No pipeline data
133
+ if (!pipeline) {
134
+ return (
135
+ <Layout
136
+ pageTitle="Pipeline Details"
137
+ breadcrumbs={[
138
+ { label: "Home", href: "/" },
139
+ { label: "Pipelines", href: "/pipelines" },
140
+ ]}
141
+ >
142
+ <Flex align="center" justify="center" className="min-h-64">
143
+ <Box className="text-center">
144
+ <Text size="5" weight="medium" className="mb-2">
145
+ Pipeline not found
146
+ </Text>
147
+ </Box>
148
+ </Flex>
149
+ </Layout>
150
+ );
151
+ }
152
+
153
+ const pageTitle = pipeline.name || "Pipeline Details";
154
+ const breadcrumbs = [
155
+ { label: "Home", href: "/" },
156
+ { label: "Pipelines", href: "/pipelines" },
157
+ { label: pipeline.name || slug },
158
+ ];
159
+
160
+ return (
161
+ <Layout pageTitle={pageTitle} breadcrumbs={breadcrumbs}>
162
+ <PageSubheader breadcrumbs={breadcrumbs} maxWidth="max-w-7xl">
163
+ <Flex gap="3" align="center">
164
+ <Text size="2" color="gray">
165
+ Slug: {slug}
166
+ </Text>
167
+ <Button
168
+ variant="solid"
169
+ size="md"
170
+ onClick={() => setSidebarOpen(true)}
171
+ >
172
+ Add Task
173
+ </Button>
174
+ <Button
175
+ variant="outline"
176
+ size="md"
177
+ onClick={handleAnalyze}
178
+ disabled={status === "connecting" || status === "running"}
179
+ >
180
+ Analyze Pipeline
181
+ </Button>
182
+ </Flex>
183
+ </PageSubheader>
184
+
185
+ {/* Pipeline description */}
186
+ {pipeline.description && (
187
+ <Box className="mb-6">
188
+ <Text size="2" color="gray" className="leading-relaxed">
189
+ {pipeline.description}
190
+ </Text>
191
+ </Box>
192
+ )}
193
+
194
+ {/* Pipeline DAG - will be implemented in step 5 */}
195
+ <Box className="bg-gray-50 rounded-lg p-4">
196
+ <Text size="3" weight="medium" className="mb-4">
197
+ Pipeline Tasks
198
+ </Text>
199
+
200
+ {pipeline.tasks && pipeline.tasks.length > 0 ? (
201
+ <Box>
202
+ <Text size="2" color="gray" className="mb-4">
203
+ {pipeline.tasks.length} task
204
+ {pipeline.tasks.length !== 1 ? "s" : ""} defined
205
+ </Text>
206
+
207
+ <PipelineDAGGrid items={pipeline.tasks} pipelineSlug={slug} />
208
+ </Box>
209
+ ) : (
210
+ <Box className="mb-4">
211
+ <Text size="2" color="gray">
212
+ No tasks defined for this pipeline
213
+ </Text>
214
+ </Box>
215
+ )}
216
+ </Box>
217
+
218
+ <TaskCreationSidebar
219
+ isOpen={sidebarOpen}
220
+ onClose={() => setSidebarOpen(false)}
221
+ pipelineSlug={slug}
222
+ />
223
+
224
+ {!trayDismissed && (
225
+ <AnalysisProgressTray
226
+ {...progressState}
227
+ status={status}
228
+ pipelineSlug={slug}
229
+ onDismiss={() => setTrayDismissed(true)}
230
+ />
231
+ )}
232
+ </Layout>
233
+ );
234
+ }
@@ -12,20 +12,24 @@ export async function deepseekChat({
12
12
  model = "deepseek-chat",
13
13
  temperature = 0.7,
14
14
  maxTokens,
15
- responseFormat,
15
+ responseFormat = "json_object",
16
16
  topP,
17
17
  frequencyPenalty,
18
18
  presencePenalty,
19
19
  stop,
20
+ stream = false,
20
21
  maxRetries = 3,
21
22
  }) {
22
- // Enforce JSON mode - reject calls without proper JSON responseFormat
23
- ensureJsonResponseFormat(responseFormat, "DeepSeek");
24
-
25
23
  if (!process.env.DEEPSEEK_API_KEY) {
26
24
  throw new Error("DeepSeek API key not configured");
27
25
  }
28
26
 
27
+ // Determine if JSON mode is requested
28
+ const isJsonMode =
29
+ responseFormat?.type === "json_object" ||
30
+ responseFormat?.type === "json_schema" ||
31
+ responseFormat === "json";
32
+
29
33
  const { systemMsg, userMsg } = extractMessages(messages);
30
34
 
31
35
  let lastError;
@@ -47,10 +51,11 @@ export async function deepseekChat({
47
51
  frequency_penalty: frequencyPenalty,
48
52
  presence_penalty: presencePenalty,
49
53
  stop,
54
+ stream,
50
55
  };
51
56
 
52
- // Add response format - this is now required for all calls
53
- if (responseFormat?.type === "json_object" || responseFormat === "json") {
57
+ // Add response format only for JSON mode (streaming uses text mode)
58
+ if (isJsonMode && !stream) {
54
59
  requestBody.response_format = { type: "json_object" };
55
60
  }
56
61
 
@@ -73,22 +78,35 @@ export async function deepseekChat({
73
78
  throw { status: response.status, ...error };
74
79
  }
75
80
 
81
+ // Streaming mode - return async generator for real-time chunks
82
+ if (stream) {
83
+ return createStreamGenerator(response.body);
84
+ }
85
+
76
86
  const data = await response.json();
77
87
  const content = data.choices[0].message.content;
78
88
 
79
- // Parse JSON - this is now required for all calls
80
- const parsed = tryParseJSON(content);
81
- if (!parsed) {
82
- throw new ProviderJsonParseError(
83
- "DeepSeek",
84
- model,
85
- content.substring(0, 200),
86
- "Failed to parse JSON response from DeepSeek API"
87
- );
89
+ // Parse JSON only in JSON mode; return raw string for text mode
90
+ if (isJsonMode) {
91
+ const parsed = tryParseJSON(content);
92
+ if (!parsed) {
93
+ throw new ProviderJsonParseError(
94
+ "DeepSeek",
95
+ model,
96
+ content.substring(0, 200),
97
+ "Failed to parse JSON response from DeepSeek API"
98
+ );
99
+ }
100
+ return {
101
+ content: parsed,
102
+ usage: data.usage,
103
+ raw: data,
104
+ };
88
105
  }
89
106
 
107
+ // Text mode - return raw string
90
108
  return {
91
- content: parsed,
109
+ content,
92
110
  usage: data.usage,
93
111
  raw: data,
94
112
  };
@@ -107,3 +125,45 @@ export async function deepseekChat({
107
125
 
108
126
  throw lastError || new Error(`Failed after ${maxRetries + 1} attempts`);
109
127
  }
128
+
129
+ /**
130
+ * Create async generator for streaming DeepSeek responses.
131
+ * DeepSeek uses Server-Sent Events format with "data:" prefix.
132
+ */
133
+ async function* createStreamGenerator(stream) {
134
+ const decoder = new TextDecoder();
135
+ const reader = stream.getReader();
136
+ let buffer = "";
137
+
138
+ try {
139
+ while (true) {
140
+ const { done, value } = await reader.read();
141
+ if (done) break;
142
+
143
+ buffer += decoder.decode(value, { stream: true });
144
+ const lines = buffer.split("\n");
145
+ buffer = lines.pop(); // Keep incomplete line
146
+
147
+ for (const line of lines) {
148
+ if (line.startsWith("data: ")) {
149
+ const data = line.slice(6);
150
+ if (data === "[DONE]") continue;
151
+
152
+ try {
153
+ const parsed = JSON.parse(data);
154
+ const content = parsed.choices?.[0]?.delta?.content;
155
+ // Skip only truly empty chunks; preserve whitespace-only content
156
+ if (content !== undefined && content !== null && content !== "") {
157
+ yield { content };
158
+ }
159
+ } catch (e) {
160
+ // Skip malformed JSON
161
+ console.warn("[deepseek] Failed to parse stream chunk:", e);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ } finally {
167
+ reader.releaseLock();
168
+ }
169
+ }
@@ -33,7 +33,7 @@ export async function openaiChat({
33
33
  temperature,
34
34
  maxTokens,
35
35
  max_tokens, // Explicitly destructure to prevent it from being in ...rest
36
- responseFormat,
36
+ responseFormat = "json_object",
37
37
  seed,
38
38
  stop,
39
39
  topP,
@@ -46,9 +46,6 @@ export async function openaiChat({
46
46
  console.log("[OpenAI] Model:", model);
47
47
  console.log("[OpenAI] Response format:", responseFormat);
48
48
 
49
- // Enforce JSON mode - reject calls without proper JSON responseFormat
50
- ensureJsonResponseFormat(responseFormat, "OpenAI");
51
-
52
49
  const openai = getClient();
53
50
  if (!openai) throw new Error("OpenAI API key not configured");
54
51
 
@@ -56,6 +53,12 @@ export async function openaiChat({
56
53
  console.log("[OpenAI] System message length:", systemMsg.length);
57
54
  console.log("[OpenAI] User message length:", userMsg.length);
58
55
 
56
+ // Determine if JSON mode is requested
57
+ const isJsonMode =
58
+ responseFormat?.json_schema ||
59
+ responseFormat?.type === "json_object" ||
60
+ responseFormat === "json";
61
+
59
62
  let lastError;
60
63
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
61
64
  if (attempt > 0) await sleep(Math.pow(2, attempt) * 1000);
@@ -109,19 +112,27 @@ export async function openaiChat({
109
112
  total_tokens: promptTokens + completionTokens,
110
113
  };
111
114
 
112
- // Parse JSON - this is now required for all calls
113
- const parsed = tryParseJSON(text);
114
- if (!parsed) {
115
- throw new ProviderJsonParseError(
116
- "OpenAI",
117
- model,
118
- text.substring(0, 200),
119
- "Failed to parse JSON response from Responses API"
115
+ // Parse JSON only in JSON mode; return raw string for text mode
116
+ if (isJsonMode) {
117
+ const parsed = tryParseJSON(text);
118
+ if (!parsed) {
119
+ throw new ProviderJsonParseError(
120
+ "OpenAI",
121
+ model,
122
+ text.substring(0, 200),
123
+ "Failed to parse JSON response from Responses API"
124
+ );
125
+ }
126
+ console.log(
127
+ "[OpenAI] Returning response from Responses API (JSON mode)"
120
128
  );
129
+ return { content: parsed, text, usage, raw: resp };
121
130
  }
122
131
 
123
- console.log("[OpenAI] Returning response from Responses API");
124
- return { content: parsed, text, usage, raw: resp };
132
+ console.log(
133
+ "[OpenAI] Returning response from Responses API (text mode)"
134
+ );
135
+ return { content: text, text, usage, raw: resp };
125
136
  }
126
137
 
127
138
  // ---------- CLASSIC CHAT COMPLETIONS path (non-GPT-5) ----------
@@ -156,19 +167,27 @@ export async function openaiChat({
156
167
  classicText.length
157
168
  );
158
169
 
159
- // Parse JSON - this is now required for all calls
160
- const classicParsed = tryParseJSON(classicText);
161
- if (!classicParsed) {
162
- throw new ProviderJsonParseError(
163
- "OpenAI",
164
- model,
165
- classicText.substring(0, 200),
166
- "Failed to parse JSON response from Classic API"
167
- );
170
+ // Parse JSON only in JSON mode; return raw string for text mode
171
+ if (isJsonMode) {
172
+ const classicParsed = tryParseJSON(classicText);
173
+ if (!classicParsed) {
174
+ throw new ProviderJsonParseError(
175
+ "OpenAI",
176
+ model,
177
+ classicText.substring(0, 200),
178
+ "Failed to parse JSON response from Classic API"
179
+ );
180
+ }
181
+ return {
182
+ content: classicParsed,
183
+ text: classicText,
184
+ usage: classicRes?.usage,
185
+ raw: classicRes,
186
+ };
168
187
  }
169
188
 
170
189
  return {
171
- content: classicParsed,
190
+ content: classicText,
172
191
  text: classicText,
173
192
  usage: classicRes?.usage,
174
193
  raw: classicRes,
@@ -211,19 +230,27 @@ export async function openaiChat({
211
230
  const classicRes = await openai.chat.completions.create(classicReq);
212
231
  const text = classicRes?.choices?.[0]?.message?.content ?? "";
213
232
 
214
- // Parse JSON - this is now required for all calls
215
- const parsed = tryParseJSON(text);
216
- if (!parsed) {
217
- throw new ProviderJsonParseError(
218
- "OpenAI",
219
- model,
220
- text.substring(0, 200),
221
- "Failed to parse JSON response from fallback Classic API"
222
- );
233
+ // Parse JSON only in JSON mode; return raw string for text mode
234
+ if (isJsonMode) {
235
+ const parsed = tryParseJSON(text);
236
+ if (!parsed) {
237
+ throw new ProviderJsonParseError(
238
+ "OpenAI",
239
+ model,
240
+ text.substring(0, 200),
241
+ "Failed to parse JSON response from fallback Classic API"
242
+ );
243
+ }
244
+ return {
245
+ content: parsed,
246
+ text,
247
+ usage: classicRes?.usage,
248
+ raw: classicRes,
249
+ };
223
250
  }
224
251
 
225
252
  return {
226
- content: parsed,
253
+ content: text,
227
254
  text,
228
255
  usage: classicRes?.usage,
229
256
  raw: classicRes,
@@ -0,0 +1,62 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Write task analysis file to the analysis/ directory.
6
+ *
7
+ * @param {string} pipelinePath - Path to pipeline directory
8
+ * @param {string} taskName - Task name (e.g., "research")
9
+ * @param {object} analysisData - Task analysis object containing { taskFilePath, stages, artifacts, models }
10
+ */
11
+ export async function writeAnalysisFile(pipelinePath, taskName, analysisData) {
12
+ // Validate that analysisData contains all required properties
13
+ if (!analysisData || typeof analysisData !== "object") {
14
+ throw new Error(
15
+ `Invalid analysisData: expected an object but got ${typeof analysisData}`
16
+ );
17
+ }
18
+
19
+ if (
20
+ !analysisData.taskFilePath ||
21
+ typeof analysisData.taskFilePath !== "string"
22
+ ) {
23
+ throw new Error(
24
+ `Invalid analysisData.taskFilePath: expected a string but got ${typeof analysisData.taskFilePath}`
25
+ );
26
+ }
27
+
28
+ if (!Array.isArray(analysisData.stages)) {
29
+ throw new Error(
30
+ `Invalid analysisData.stages: expected an array but got ${typeof analysisData.stages}`
31
+ );
32
+ }
33
+
34
+ if (
35
+ !analysisData.artifacts ||
36
+ typeof analysisData.artifacts !== "object" ||
37
+ Array.isArray(analysisData.artifacts)
38
+ ) {
39
+ throw new Error(
40
+ `Invalid analysisData.artifacts: expected an object but got ${typeof analysisData.artifacts}`
41
+ );
42
+ }
43
+
44
+ if (!Array.isArray(analysisData.models)) {
45
+ throw new Error(
46
+ `Invalid analysisData.models: expected an array but got ${typeof analysisData.models}`
47
+ );
48
+ }
49
+
50
+ const analysisDir = path.join(pipelinePath, "analysis");
51
+ await fs.mkdir(analysisDir, { recursive: true });
52
+
53
+ const output = {
54
+ ...analysisData,
55
+ analyzedAt: new Date().toISOString(),
56
+ };
57
+
58
+ await fs.writeFile(
59
+ path.join(analysisDir, `${taskName}.analysis.json`),
60
+ JSON.stringify(output, null, 2)
61
+ );
62
+ }