@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.
- package/package.json +10 -1
- package/src/cli/analyze-task.js +51 -0
- package/src/cli/index.js +8 -0
- package/src/components/AddPipelineSidebar.jsx +144 -0
- package/src/components/AnalysisProgressTray.jsx +87 -0
- package/src/components/JobTable.jsx +4 -3
- package/src/components/Layout.jsx +142 -139
- package/src/components/MarkdownRenderer.jsx +149 -0
- package/src/components/PipelineDAGGrid.jsx +404 -0
- package/src/components/PipelineTypeTaskSidebar.jsx +96 -0
- package/src/components/SchemaPreviewPanel.jsx +97 -0
- package/src/components/StageTimeline.jsx +36 -0
- package/src/components/TaskAnalysisDisplay.jsx +227 -0
- package/src/components/TaskCreationSidebar.jsx +447 -0
- package/src/components/TaskDetailSidebar.jsx +119 -117
- package/src/components/TaskFilePane.jsx +94 -39
- package/src/components/ui/button.jsx +59 -27
- package/src/components/ui/sidebar.jsx +118 -0
- package/src/config/models.js +99 -67
- package/src/core/config.js +4 -1
- package/src/llm/index.js +129 -9
- package/src/pages/PipelineDetail.jsx +6 -6
- package/src/pages/PipelineList.jsx +214 -0
- package/src/pages/PipelineTypeDetail.jsx +234 -0
- package/src/providers/deepseek.js +76 -16
- package/src/providers/openai.js +61 -34
- package/src/task-analysis/enrichers/analysis-writer.js +62 -0
- package/src/task-analysis/enrichers/schema-deducer.js +145 -0
- package/src/task-analysis/enrichers/schema-writer.js +74 -0
- package/src/task-analysis/extractors/artifacts.js +137 -0
- package/src/task-analysis/extractors/llm-calls.js +176 -0
- package/src/task-analysis/extractors/stages.js +51 -0
- package/src/task-analysis/index.js +103 -0
- package/src/task-analysis/parser.js +28 -0
- package/src/task-analysis/utils/ast.js +43 -0
- package/src/ui/client/hooks/useAnalysisProgress.js +145 -0
- package/src/ui/client/index.css +64 -0
- package/src/ui/client/main.jsx +4 -0
- package/src/ui/client/sse-fetch.js +120 -0
- package/src/ui/dist/assets/index-cjHV9mYW.js +82578 -0
- package/src/ui/dist/assets/index-cjHV9mYW.js.map +1 -0
- package/src/ui/dist/assets/style-CoM9SoQF.css +180 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/endpoints/create-pipeline-endpoint.js +194 -0
- package/src/ui/endpoints/pipeline-analysis-endpoint.js +246 -0
- package/src/ui/endpoints/pipeline-type-detail-endpoint.js +181 -0
- package/src/ui/endpoints/pipelines-endpoint.js +133 -0
- package/src/ui/endpoints/schema-file-endpoint.js +105 -0
- package/src/ui/endpoints/task-analysis-endpoint.js +104 -0
- package/src/ui/endpoints/task-creation-endpoint.js +114 -0
- package/src/ui/endpoints/task-save-endpoint.js +101 -0
- package/src/ui/express-app.js +45 -0
- package/src/ui/lib/analysis-lock.js +67 -0
- package/src/ui/lib/sse.js +30 -0
- package/src/ui/server.js +4 -0
- package/src/ui/utils/slug.js +31 -0
- package/src/ui/watcher.js +28 -2
- package/src/ui/dist/assets/index-B320avRx.js +0 -26613
- package/src/ui/dist/assets/index-B320avRx.js.map +0 -1
- 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
|
|
53
|
-
if (
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
+
}
|
package/src/providers/openai.js
CHANGED
|
@@ -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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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(
|
|
124
|
-
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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:
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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:
|
|
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
|
+
}
|