Skip to content

Commit 7517db1

Browse files
committed
[feat] Enhance streaming capabilities to be per-port not per-task.
- Added support for streaming output in various AI tasks, including TextGenerationTask, TextQuestionAnswerTask, and TextRewriterTask, allowing real-time data processing. - Updated task registration and execution methods to accommodate new streaming functionalities, improving responsiveness. - Refactored package dependencies in bun.lock and package.json to use catalog references for better version management. - Introduced new streaming-related UI components and styles in the web examples, enhancing user experience during task execution. - Improved task graph handling for streaming events, ensuring accurate updates and visual feedback in the UI.
1 parent 897f4b5 commit 7517db1

34 files changed

Lines changed: 578 additions & 283 deletions

bun.lock

Lines changed: 16 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/cli/src/TaskCLI.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ export function AddBaseCommands(program: Command) {
296296
.option("--model-path <path>", "model path or URI")
297297
.option("--model-dtype <dtype>", "model dtype (default: auto)")
298298
.option("--model-pipeline <pipeline>", "override inferred pipeline type")
299+
.option("--stream", "enable streaming output")
299300
.action(async (text, options) => {
300301
let model: string | ModelConfig;
301302

@@ -337,6 +338,7 @@ export function AddBaseCommands(program: Command) {
337338
.option("--model-path <path>", "model path or URI")
338339
.option("--model-dtype <dtype>", "model dtype (default: auto)")
339340
.option("--model-pipeline <pipeline>", "override inferred pipeline type")
341+
.option("--stream", "enable streaming output")
340342
.action(async (text, options) => {
341343
let model: string | ModelConfig;
342344

@@ -511,6 +513,7 @@ export function AddBaseCommands(program: Command) {
511513
.option("--model-path <path>", "model path or URI")
512514
.option("--model-dtype <dtype>", "model dtype (default: auto)")
513515
.option("--model-pipeline <pipeline>", "override inferred pipeline type")
516+
.option("--stream", "enable streaming output")
514517
.action(async (question, context, options) => {
515518
let model: string | ModelConfig;
516519

examples/cli/src/components/TaskUI.tsx

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
*/
66

77
import { DownloadModelTask } from "@workglow/ai";
8-
import { ITask, ITaskGraph, TaskStatus } from "@workglow/task-graph";
8+
import { ITask, ITaskGraph, TaskStatus, type StreamEvent } from "@workglow/task-graph";
99
import { ArrayTask } from "@workglow/tasks";
1010
import type { FC } from "react";
11-
import { memo, useEffect, useState } from "react";
11+
import { memo, useEffect, useRef, useState } from "react";
1212
import { Box, Text } from "retuink";
1313
import { createBar, Spinner, symbols } from "./Elements";
1414

@@ -22,6 +22,10 @@ const StatusIcon = memo(
2222
sym = <Spinner color="yellow" />;
2323
}
2424

25+
if (status === TaskStatus.STREAMING) {
26+
sym = <Spinner color="cyan" />;
27+
}
28+
2529
if (status === TaskStatus.ABORTING) {
2630
sym = <Text color="yellow">{symbols.warning}</Text>;
2731
}
@@ -70,6 +74,9 @@ export const TaskUI: FC<{
7074
const [arrayProgress, setArrayProgress] = useState<{ completed: number; total: number } | null>(
7175
null
7276
);
77+
const [streamingText, setStreamingText] = useState<string>("");
78+
const [isStreaming, setIsStreaming] = useState<boolean>(false);
79+
const streamingTextRef = useRef<string>("");
7380

7481
useEffect(() => {
7582
const onStart = () => {
@@ -143,6 +150,29 @@ export const TaskUI: FC<{
143150
setStatus(TaskStatus.ABORTING);
144151
setError((prevErr) => (prevErr ? `${prevErr}\nAborted` : "Aborted"));
145152
};
153+
154+
const onStreamStart = () => {
155+
setStatus(TaskStatus.STREAMING);
156+
setIsStreaming(true);
157+
setStreamingText("");
158+
streamingTextRef.current = "";
159+
};
160+
161+
const onStreamChunk = (event: StreamEvent) => {
162+
if (event.type === "text-delta") {
163+
streamingTextRef.current += event.textDelta;
164+
setStreamingText(streamingTextRef.current);
165+
} else if (event.type === "snapshot") {
166+
const text = typeof event.data === "string" ? event.data : JSON.stringify(event.data);
167+
streamingTextRef.current = text;
168+
setStreamingText(text);
169+
}
170+
};
171+
172+
const onStreamEnd = () => {
173+
setIsStreaming(false);
174+
};
175+
146176
onRegenerate();
147177
const targets = graph.getTargetTasks(task.config.id);
148178
const unique = [...new Map(targets.map((t) => [t.config.id, t])).values()];
@@ -154,6 +184,9 @@ export const TaskUI: FC<{
154184
task.on("error", onError);
155185
task.on("regenerate", onRegenerate);
156186
task.on("abort", onAbort);
187+
task.on("stream_start", onStreamStart);
188+
task.on("stream_chunk", onStreamChunk);
189+
task.on("stream_end", onStreamEnd);
157190

158191
return () => {
159192
task.off("start", onStart);
@@ -162,6 +195,9 @@ export const TaskUI: FC<{
162195
task.off("error", onError);
163196
task.off("regenerate", onRegenerate);
164197
task.off("abort", onAbort);
198+
task.off("stream_start", onStreamStart);
199+
task.off("stream_chunk", onStreamChunk);
200+
task.off("stream_end", onStreamEnd);
165201
};
166202
}, [task, graph]);
167203

@@ -181,6 +217,12 @@ export const TaskUI: FC<{
181217
</Box>
182218
)}
183219

220+
{status === TaskStatus.STREAMING && (
221+
<Box marginLeft={2} flexShrink={1}>
222+
<Text color="cyan">[streaming]</Text>
223+
</Box>
224+
)}
225+
184226
{status === TaskStatus.PROCESSING && progress > 0 && (
185227
<Box marginLeft={2} flexShrink={1}>
186228
<Text dimColor>[{status}]</Text>
@@ -216,6 +258,11 @@ export const TaskUI: FC<{
216258
<Text color="gray">{`${symbols.arrowDashedRight} ${createBar(progress / 100, 10)} ${progressGenerationText ?? ""}`}</Text>
217259
</Box>
218260
)}
261+
{(status === TaskStatus.STREAMING || isStreaming) && streamingText && (
262+
<Box marginLeft={2}>
263+
<Text color="cyan" wrap="truncate">{`${symbols.arrowDashedRight} ${streamingText}`}</Text>
264+
</Box>
265+
)}
219266
{arrayProgress && (
220267
<Box marginLeft={2}>
221268
<Text color="gray">{`${symbols.arrowDashedRight} Processing array tasks: ${arrayProgress.completed}/${arrayProgress.total} completed ${createBar(arrayProgress.completed / arrayProgress.total, 10)}`}</Text>

examples/web/src/components/ProgressBar.tsx

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,29 @@ export const ProgressBar: React.FC<{
1313
progress: number;
1414
status: TaskStatus;
1515
showText: boolean;
16-
}> = ({ progress, status, showText }) => (
17-
<>
18-
<div className="w-full bg-[rgba(28,35,50,0.6)] rounded-full overflow-hidden h-2 my-2">
19-
<div
20-
className={`h-full rounded-full transition-[width] duration-300 ease-in-out ${
21-
status === TaskStatus.PROCESSING
22-
? "bg-gradient-to-r from-[#2a8af6] via-[#a853ba] to-[#2a8af6] bg-[length:200%_100%] animate-progress"
23-
: getStatusColorBg(status)
24-
}`}
25-
style={{
26-
width: `${Math.round(progress)}%`,
27-
}}
28-
/>
29-
</div>
30-
{showText && <div className="text-xs text-gray-500">Progress: {Math.round(progress)}%</div>}
31-
</>
32-
);
16+
}> = ({ progress, status, showText }) => {
17+
const isStreaming = status === TaskStatus.STREAMING;
18+
19+
return (
20+
<>
21+
<div className="w-full bg-[rgba(28,35,50,0.6)] rounded-full overflow-hidden h-2 my-2">
22+
<div
23+
className={`h-full rounded-full transition-[width] duration-300 ease-in-out ${
24+
isStreaming
25+
? "bg-blue-500 animate-streaming-pulse"
26+
: status === TaskStatus.PROCESSING
27+
? "bg-gradient-to-r from-[#2a8af6] via-[#a853ba] to-[#2a8af6] bg-[length:200%_100%] animate-progress"
28+
: getStatusColorBg(status)
29+
}`}
30+
style={{
31+
width: isStreaming ? "100%" : `${Math.round(progress)}%`,
32+
}}
33+
/>
34+
</div>
35+
{showText && !isStreaming && (
36+
<div className="text-xs text-gray-500">Progress: {Math.round(progress)}%</div>
37+
)}
38+
{showText && isStreaming && <div className="text-xs text-blue-400">Streaming...</div>}
39+
</>
40+
);
41+
};

examples/web/src/graph/RunGraphFlow.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,36 @@
332332
overflow-y: auto;
333333
}
334334

335+
/* Streaming progress bar */
336+
.progress-bar.streaming {
337+
background: #3b82f6;
338+
width: 100%;
339+
animation: streaming-pulse 1.5s ease-in-out infinite;
340+
}
341+
342+
@keyframes streaming-pulse {
343+
0%,
344+
100% {
345+
opacity: 1;
346+
}
347+
50% {
348+
opacity: 0.5;
349+
}
350+
}
351+
352+
.react-flow__node .wrapper.gradient.streaming:before {
353+
content: "";
354+
background: conic-gradient(
355+
from -160deg at 50% 50%,
356+
#3b82f6 0deg,
357+
#60a5fa 120deg,
358+
#3b82f6 240deg,
359+
rgba(59, 130, 246, 0) 360deg
360+
);
361+
animation: spinner 3s linear infinite;
362+
transform: translate(-50%, -50%) rotate(0deg);
363+
}
364+
335365
/* Transitions */
336366
.fade-in {
337367
animation: fade-in 0.3s ease-in-out;

examples/web/src/graph/RunGraphFlow.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ export const RunGraphFlow: React.FC<{
189189
unsubscribes.push(unsub);
190190
});
191191

192+
// Streaming status: update node when a task starts streaming
193+
const streamStartUnsub = task.subscribe("stream_start", () => updateNode(setNodes, task));
194+
unsubscribes.push(streamStartUnsub);
195+
192196
// Progress events (just node update)
193197
const progressUnsub = task.subscribe("progress", () => updateNode(setNodes, task));
194198
unsubscribes.push(progressUnsub);
@@ -201,8 +205,9 @@ export const RunGraphFlow: React.FC<{
201205
});
202206

203207
const dataflows = graph.getDataflows();
208+
const dataflowEvents = [...statusEvents, "streaming"] as const;
204209
dataflows.forEach((dataflow) => {
205-
statusEvents.forEach((eventName) => {
210+
dataflowEvents.forEach((eventName) => {
206211
const unsub = dataflow.subscribe(eventName, () => updateEdgeStatus(dataflow));
207212
unsubscribes.push(unsub);
208213
});

0 commit comments

Comments
 (0)