feat: add functionality to stream or poll events from the build process (#5940)

* refactor: simplify TaskService by removing Celery integration

* refactor: enhance AnyIO backend task management and error handling

* refactor: restructure flow building process and enhance event handling

* feat: implement QueueService for managing job queues and tasks

* feat: Add QueueService for managing job queues and task lifecycle

* feat!: Enhance flow building with QueueService integration for job management

* revert changes to async session

* feat: Integrate QueueService into lifespan management for task handling

* refactor: Enhance QueueService with robust lifecycle management and cleanup mechanisms

* refactor: Simplify docstring for get_queue_service function

* refactor: Update import statements in queue factory for clarity

* test: Improve chat endpoint tests with comprehensive build flow scenarios

* refactor: Streamline flow build event generation and queue management

* refactor: Improve flow build process with modular event handling and URL parameter management

* test: Enhance loop component tests with flow build and event handling utilities

* fix: Improve transaction logging with null flow_id handling and debug logging

* fix: Remove unnecessary db.refresh() in transaction logging

* feat: Add polling mode for build events with optional streaming

* feat: Add event delivery configuration option to settings

* feat: Implement polling for build events with configuration-driven streaming

* refactor: Enhance queue cleanup with improved logging and error handling

* test: Improve build event stream assertion with detailed error reporting

* test: Add benchmark test for build flow polling mechanism

* refactor: Remove redundant end event in flow event generation

* test: Update test_component_tool_with_api_key to use async flow

* Convert test method to async
* Update graph start method to use async_start()
* Add client parameter to test method
* Modify tool retrieval to use async method

* test: Add session ID generation in agent component test

* fix: Enhance error handling in LCAgentComponent for ExceptionWithMessageError

Add null checks to prevent potential AttributeError when handling agent message deletion

* test: Refactor test assertion for model name options in agent component

Simplify the assertion for checking "sonnet" in model name options by extracting the options to a variable first

* refactor: Improve type hints and imports in AnyIOTaskResult

* Add `__future__` import for type annotations
* Use conditional import for `Callable` and `TracebackType`
* Explicitly type `_traceback` attribute with `TracebackType | None`

* fix: Add null checks for event task in build events streaming

* refactor: Extract build and disconnect handling into separate modules

This commit separates the build and disconnect handling logic from the chat API module into dedicated files:

- Created `langflow/api/build.py` to house the flow generation and vertex building logic
- Created `langflow/api/disconnect.py` to manage the custom streaming response with disconnect handling
- Removed duplicate code from `langflow/api/v1/chat.py`
- Improved code organization and modularity

* refactor: Extract flow build and event handling methods in build API

This commit introduces two new methods in the build API to improve code organization and reusability:

- `start_flow_build()`: Centralizes the logic for creating a queue and starting a flow build task
- `get_flow_events_response()`: Handles retrieving flow events for both streaming and polling modes

The changes simplify the chat API endpoint implementations and reduce code duplication

* refactor: Deprecate /task endpoint and add informative error message

This commit marks the /task endpoint as deprecated and raises an HTTP 400 error with a clear message indicating the endpoint will be removed in a future version. The changes improve API clarity and guide users towards the recommended /run endpoint.

* refactor: Update log_transaction function return type annotation

Modify the return type hint for log_transaction to allow for potential None return, improving type safety and clarity in the transactions logging method.

* feat: Add configurable event delivery streaming option

Introduce support for configurable event delivery mode (streaming/polling) across frontend components:

- Add `shouldStreamEvents()` method in NodeStatus and IOModal
- Update flowStore to accept a `stream` parameter
- Modify buildFlowVerticesWithFallback to use the stream parameter directly
- Remove hardcoded polling logic in buildUtils

* fix: Add null check for flow_id in log_transaction function

Prevent logging transactions without a flow_id by returning None, ensuring data integrity and avoiding potential database errors

* chore: Update changes-filter to include chat API path

* fix: Add error handling and null checks in flow transaction deletion

Improve robustness of transaction and vertex build deletion by:
- Adding a null check for flow_id in delete_transactions_by_flow_id
- Wrapping deletion operations in try-except blocks to prevent failures
- Logging debug messages for any deletion errors

* fix: Remove redundant commit in vertex builds deletion

Remove unnecessary db.commit() from delete_vertex_builds_by_flow_id to prevent duplicate commits and simplify database transaction handling

* fix: Improve transaction logging with debug message and return value

Move debug logging from CRUD layer to utils to provide more context about logged transactions and ensure proper error handling

* fix: Improve error handling and transaction cleanup in active_user fixture

Enhance user and transaction cleanup process in test fixture by:
- Adding separate try-except blocks for transaction/vertex build deletion and user deletion
- Adding debug logging for potential errors during cleanup
- Ensuring proper session commits for each operation

* propagate parent task CancelledError instead of supressing it

Co-authored-by: Christophe Bornet <cbornet@hotmail.com>

* refactor: Rename queue service to job queue service

Restructure job queue management by:
- Renaming QueueService to JobQueueService
- Moving queue-related files to a new job_queue directory
- Updating import paths and service type references
- Enhancing job queue service with more robust async job management

* fix: Correct ServiceType enum reference for job queue service

Update the service type constant to match the recently renamed JobQueueService, ensuring consistent service type referencing across the application.

* refactor: Enhance JobQueueService with comprehensive logging and documentation

Improve the JobQueueService implementation by:
- Adding detailed docstrings with clear explanations of methods and attributes
- Implementing comprehensive logging throughout the service
- Enhancing error handling and logging for queue and task management
- Providing more context in log messages for debugging and monitoring

* docs: Improve stop method docstring for JobQueueService

Enhance the documentation for the stop method by:
- Providing a more detailed and precise description of the shutdown process
- Clarifying the steps involved in gracefully stopping the service
- Improving the explanation of resource cleanup and task cancellation

* fix: Properly handle task cancellation and propagate exceptions in JobQueueService

* fix: Enhance error handling and raise appropriate exceptions in JobQueueService methods

* improve docstring

Co-authored-by: Christophe Bornet <cbornet@hotmail.com>

* fix: Remove redundant exception raises in JobQueueService methods and improve cleanup logic

* fix: Improve logging during job cleanup and handle exceptions more appropriately

* feat: add utility to run tests with multiple event delivery modes

* feat: integrate withEventDeliveryModes utility in multiple test files

* refactor: replace status code assertions with httpx codes for clarity

* remove noqa comment and change argument name

Co-authored-by: Christophe Bornet <cbornet@hotmail.com>

* refactor: streamline event polling logic in get_flow_events_response

* refactor: use getattr for safer session attribute access

Co-author: @cbornet

* feat: add is_started method to JobQueueService

* refactor: modify JobQueueService start method and main.py queue service initialization

* feat: add ready state and teardown method to JobQueueService

* refactor: simplify job queue cleanup logic in JobQueueService

* refactor: improve error logging in active_user fixture

* refactor: improve AnyIO task management with TaskGroup and CancelScope

* refactor: Implement LimitedBackgroundTasks for controlled vertex build logging (#6312)

* feat: implement LimitedBackgroundTasks for controlled vertex build logging

* refactor: replace BackgroundTasks with LimitedBackgroundTasks in build_flow endpoint

* refactor: improve task cancellation error handling in JobQueueService

* refactor: Rename LimitedBackgroundTasks to LimitVertexBuildBackgroundTasks

* feat: Add EventDeliveryType enum for event delivery methods

* feat: Add polling constants for endpoint and streaming status

* refactor: Update buildFlowVerticesWithFallback to use polling constants

* refactor: Update event delivery handling to use EventDeliveryType enum

* [autofix.ci] apply automated fixes

* fix: Improve error handling for cancelled build tasks

Refactor generate_flow_events to properly handle and propagate CancelledError
- Remove unnecessary task creation and manual cancellation
- Directly await _build_vertex instead of creating a separate task
- Improve exception logging for cancelled tasks
- Ensure CancelledError is raised instead of being silently handled

* fix: trigger event_manager.on_end after error handling in generate_flow_events

---------

Co-authored-by: Christophe Bornet <cbornet@hotmail.com>
Co-authored-by: anovazzi1 <otavio2204@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Gabriel Luiz Freitas Almeida 2025-02-18 11:24:39 -03:00 • committed by GitHub
commit 1530e61669
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 1618 additions and 814 deletions

View file

@ -6,7 +6,8 @@ import ShadTooltip from "@/components/common/shadTooltipComponent";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ICON_STROKE_WIDTH } from "@/constants/constants";
import { BuildStatus } from "@/constants/enums";
import { BuildStatus, EventDeliveryType } from "@/constants/enums";
import { useGetConfig } from "@/controllers/API/queries/config/use-get-config";
import { track } from "@/customization/utils/analytics";
import { useDarkStore } from "@/stores/darkStore";
import useFlowStore from "@/stores/flowStore";
@ -69,11 +70,16 @@ export default function NodeStatus({
const isBuilding = useFlowStore((state) => state.isBuilding);
const setNode = useFlowStore((state) => state.setNode);
const version = useDarkStore((state) => state.version);
const config = useGetConfig();
const shouldStreamEvents = () => {
// Get from useGetConfig store
return config.data?.event_delivery === EventDeliveryType.STREAMING;
};
function handlePlayWShortcut() {
if (buildStatus === BuildStatus.BUILDING || isBuilding || !selected) return;
setValidationStatus(null);
buildFlow({ stopNodeId: nodeId });
buildFlow({ stopNodeId: nodeId, stream: shouldStreamEvents() });
}
const play = useShortcutsStore((state) => state.play);
@ -164,7 +170,7 @@ export default function NodeStatus({
return;
}
if (buildStatus === BuildStatus.BUILDING || isBuilding) return;
buildFlow({ stopNodeId: nodeId });
buildFlow({ stopNodeId: nodeId, stream: shouldStreamEvents() });
track("Flow Build - Clicked", { stopNodeId: nodeId });
};

View file

@ -715,7 +715,7 @@ export const STORE_TITLE = "Langflow Store";
export const NO_API_KEY = "You don't have an API key.";
export const INSERT_API_KEY = "Insert your Langflow API key.";
export const INVALID_API_KEY = "Your API key is not valid. ";
export const CREATE_API_KEY = `Don’t have an API key? Sign up at`;
export const CREATE_API_KEY = `Don't have an API key? Sign up at`;
export const STATUS_BUILD = "Build to validate status.";
export const STATUS_INACTIVE = "Execution blocked";
export const STATUS_BUILDING = "Building...";
@ -1005,3 +1005,10 @@ export const ICON_STROKE_WIDTH = 1.5;
export const DEFAULT_PLACEHOLDER = "Type something...";
export const DEFAULT_TOOLSET_PLACEHOLDER = "Used as a tool";
export const POLLING_MESSAGES = {
ENDPOINT_NOT_AVAILABLE: "Endpoint not available",
STREAMING_NOT_SUPPORTED: "Streaming not supported",
} as const;
export const POLLING_INTERVAL = 100; // milliseconds between polling attempts

View file

@ -38,3 +38,8 @@ export enum IOOutputTypes {
STRING_LIST = "StringListOutput",
DATA = "DataOutput",
}
export enum EventDeliveryType {
STREAMING = "streaming",
POLLING = "polling",
}

View file

@ -1,3 +1,4 @@
import { EventDeliveryType } from "@/constants/enums";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import { useUtilityStore } from "@/stores/utilityStore";
import axios from "axios";
@ -13,6 +14,7 @@ export interface ConfigResponse {
health_check_max_retries: number;
max_file_size_upload: number;
feature_flags: Record<string, any>;
event_delivery: EventDeliveryType;
}
export const useGetConfig: useQueryFunctionType<undefined, ConfigResponse> = (

View file

@ -1,4 +1,5 @@
import { Separator } from "@/components/ui/separator";
import { EventDeliveryType } from "@/constants/enums";
import { useGetConfig } from "@/controllers/API/queries/config/use-get-config";
import {
useDeleteMessages,
useGetMessagesQuery,
@ -16,7 +17,6 @@ import { IOModalPropsType } from "../../types/components";
import { cn } from "../../utils/utils";
import BaseModal from "../baseModal";
import { ChatViewWrapper } from "./components/chat-view-wrapper";
import ChatView from "./components/chatView/chat-view";
import { SelectedViewField } from "./components/selected-view-field";
import { SidebarOpenView } from "./components/sidebar-open-view";
@ -136,6 +136,11 @@ export default function IOModal({
const chatValue = useUtilityStore((state) => state.chatValueStore);
const setChatValue = useUtilityStore((state) => state.setChatValueStore);
const config = useGetConfig();
function shouldStreamEvents() {
return config.data?.event_delivery === EventDeliveryType.STREAMING;
}
const sendMessage = useCallback(
async ({
@ -154,6 +159,7 @@ export default function IOModal({
files: files,
silent: true,
session: sessionId,
stream: shouldStreamEvents(),
}).catch((err) => {
console.error(err);
});

View file

@ -594,6 +594,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
files,
silent,
session,
stream = true,
}: {
startNodeId?: string;
stopNodeId?: string;
@ -601,6 +602,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
files?: string[];
silent?: boolean;
session?: string;
stream?: boolean;
}) => {
get().setIsBuilding(true);
const currentFlow = useFlowsManagerStore.getState().currentFlow;
@ -825,6 +827,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
nodes: get().nodes || undefined,
edges: get().edges || undefined,
logBuilds: get().onFlowPage,
stream,
});
get().setIsBuilding(false);
get().revertBuiltStatusFromBuilding();

View file

@ -146,6 +146,7 @@ export type FlowStoreType = {
files,
silent,
session,
stream,
}: {
startNodeId?: string;
stopNodeId?: string;
@ -153,6 +154,7 @@ export type FlowStoreType = {
files?: string[];
silent?: boolean;
session?: string;
stream?: boolean;
}) => Promise<void>;
getFlow: () => { nodes: Node[]; edges: EdgeType[]; viewport: Viewport };
updateVerticesBuild: (

View file

@ -1,4 +1,8 @@
import { BASE_URL_API } from "@/constants/constants";
import {
BASE_URL_API,
POLLING_INTERVAL,
POLLING_MESSAGES,
} from "@/constants/constants";
import { performStreamingRequest } from "@/controllers/API/api";
import { useMessagesStore } from "@/stores/messagesStore";
import { Edge, Node } from "@xyflow/react";
@ -34,6 +38,7 @@ type BuildVerticesParams = {
edges?: Edge[];
logBuilds?: boolean;
session?: string;
stream?: boolean;
};
function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI {
@ -124,10 +129,15 @@ export async function buildFlowVerticesWithFallback(
params: BuildVerticesParams,
) {
try {
return await buildFlowVertices(params);
// Use shouldUsePolling() to determine stream mode
return await buildFlowVertices({ ...params });
} catch (e: any) {
if (e.message === "Endpoint not available") {
return await buildVertices(params);
if (
e.message === POLLING_MESSAGES.ENDPOINT_NOT_AVAILABLE ||
e.message === POLLING_MESSAGES.STREAMING_NOT_SUPPORTED
) {
// Fallback to polling
return await buildFlowVertices({ ...params, stream: false });
}
throw e;
}
@ -135,6 +145,63 @@ export async function buildFlowVerticesWithFallback(
const MIN_VISUAL_BUILD_TIME_MS = 300;
async function pollBuildEvents(
url: string,
buildResults: Array<boolean>,
verticesStartTimeMs: Map<string, number>,
callbacks: {
onBuildStart?: (idList: VertexLayerElementType[]) => void;
onBuildUpdate?: (data: any, status: BuildStatus, buildId: string) => void;
onBuildComplete?: (allNodesValid: boolean) => void;
onBuildError?: (
title: string,
list: string[],
idList?: VertexLayerElementType[],
) => void;
onGetOrderSuccess?: () => void;
onValidateNodes?: (nodes: string[]) => void;
},
): Promise<void> {
let isDone = false;
while (!isDone) {
const response = await fetch(`${url}?stream=false`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error("Error polling build events");
}
const data = await response.json();
if (!data.event) {
// No event in this request, try again
await new Promise((resolve) => setTimeout(resolve, 100));
continue;
}
// Process the event
const event = JSON.parse(data.event);
await onEvent(
event.event,
event.data,
buildResults,
verticesStartTimeMs,
callbacks,
);
// Check if this was the end event or if we got a null value
if (event.event === "end" || data.event === null) {
isDone = true;
}
// Add a small delay between polls to avoid overwhelming the server
await new Promise((resolve) => setTimeout(resolve, POLLING_INTERVAL));
}
}
export async function buildFlowVertices({
flowId,
input_value,
@ -152,18 +219,26 @@ export async function buildFlowVertices({
edges,
logBuilds,
session,
stream = true,
}: BuildVerticesParams) {
const inputs = {};
let url = `${BASE_URL_API}build/${flowId}/flow?`;
let buildUrl = `${BASE_URL_API}build/${flowId}/flow`;
const queryParams = new URLSearchParams();
if (startNodeId) {
url = `${url}&start_component_id=${startNodeId}`;
queryParams.append("start_component_id", startNodeId);
}
if (stopNodeId) {
url = `${url}&stop_component_id=${stopNodeId}`;
queryParams.append("stop_component_id", stopNodeId);
}
if (logBuilds !== undefined) {
url = `${url}&log_builds=${logBuilds}`;
queryParams.append("log_builds", logBuilds.toString());
}
if (queryParams.toString()) {
buildUrl = `${buildUrl}?${queryParams.toString()}`;
}
const postData = {};
if (files) {
postData["files"] = files;
@ -184,182 +259,272 @@ export async function buildFlowVertices({
postData["inputs"] = inputs;
}
const buildResults: Array<boolean> = [];
try {
// First, start the build and get the job ID
const buildResponse = await fetch(buildUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(postData),
});
const verticesStartTimeMs: Map<string, number> = new Map();
const onEvent = async (type, data): Promise<boolean> => {
const onStartVertices = (ids: Array<string>) => {
useFlowStore.getState().updateBuildStatus(ids, BuildStatus.TO_BUILD);
if (onBuildStart)
onBuildStart(ids.map((id) => ({ id: id, reference: id })));
ids.forEach((id) => verticesStartTimeMs.set(id, Date.now()));
};
console.log("type", type);
console.log("data", data);
switch (type) {
case "vertices_sorted": {
const verticesToRun = data.to_run;
const verticesIds = data.ids;
onStartVertices(verticesIds);
let verticesLayers: Array<Array<VertexLayerElementType>> =
verticesIds.map((id: string) => {
return [{ id: id, reference: id }];
});
useFlowStore.getState().updateVerticesBuild({
verticesLayers,
verticesIds,
verticesToRun,
});
if (onValidateNodes) {
try {
onValidateNodes(data.to_run);
if (onGetOrderSuccess) onGetOrderSuccess();
useFlowStore.getState().setIsBuilding(true);
return true;
} catch (e) {
useFlowStore.getState().setIsBuilding(false);
return false;
}
}
return true;
if (!buildResponse.ok) {
if (buildResponse.status === 404) {
throw new Error("Flow not found");
}
case "end_vertex": {
const buildData = data.build_data;
const startTimeMs = verticesStartTimeMs.get(buildData.id);
if (startTimeMs) {
const delta = Date.now() - startTimeMs;
if (delta < MIN_VISUAL_BUILD_TIME_MS) {
// this is a visual trick to make the build process look more natural
await new Promise((resolve) =>
setTimeout(resolve, MIN_VISUAL_BUILD_TIME_MS - delta),
);
}
}
throw new Error("Error starting build process");
}
if (onBuildUpdate) {
if (!buildData.valid) {
// lots is a dictionary with the key the output field name and the value the log object
// logs: { [key: string]: { message: any; type: string }[] };
const errorMessages = Object.keys(buildData.data.outputs).flatMap(
(key) => {
const outputs = buildData.data.outputs[key];
if (Array.isArray(outputs)) {
return outputs
.filter((log) => isErrorLogType(log.message))
.map((log) => log.message.errorMessage);
}
if (!isErrorLogType(outputs.message)) {
return [];
}
return [outputs.message.errorMessage];
},
);
onBuildError!("Error Building Component", errorMessages, [
const { job_id } = await buildResponse.json();
// Then stream the events
const eventsUrl = `${BASE_URL_API}build/${job_id}/events`;
const buildResults: Array<boolean> = [];
const verticesStartTimeMs: Map<string, number> = new Map();
if (stream) {
return performStreamingRequest({
method: "GET",
url: eventsUrl,
onData: async (event) => {
const type = event["event"];
const data = event["data"];
return await onEvent(type, data, buildResults, verticesStartTimeMs, {
onBuildStart,
onBuildUpdate,
onBuildComplete,
onBuildError,
onGetOrderSuccess,
onValidateNodes,
});
},
onError: (statusCode) => {
if (statusCode === 404) {
throw new Error("Build job not found");
}
throw new Error("Error processing build events");
},
onNetworkError: (error: Error) => {
if (error.name === "AbortError") {
onBuildStopped && onBuildStopped();
return;
}
onBuildError!("Error Building Component", [
"Network error. Please check the connection to the server.",
]);
},
});
} else {
const callbacks = {
onBuildStart,
onBuildUpdate,
onBuildComplete,
onBuildError,
onGetOrderSuccess,
onValidateNodes,
};
return pollBuildEvents(
eventsUrl,
buildResults,
verticesStartTimeMs,
callbacks,
);
}
} catch (error) {
console.error("Build process error:", error);
onBuildError!("Error Building Flow", [
(error as Error).message || "An unexpected error occurred",
]);
throw error;
}
}
/**
* Handles various build events and calls corresponding callbacks.
*
* @param {string} type - The event type.
* @param {any} data - The event data.
* @param {boolean[]} buildResults - Array tracking build results.
* @param {Map<string, number>} verticesStartTimeMs - Map tracking start times for vertices.
* @param {Object} callbacks - Object containing callback functions.
* @param {(idList: VertexLayerElementType[]) => void} [callbacks.onBuildStart] - Callback when vertices start building.
* @param {(data: any, status: BuildStatus, buildId: string) => void} [callbacks.onBuildUpdate] - Callback for build updates.
* @param {(allNodesValid: boolean) => void} [callbacks.onBuildComplete] - Callback when build completes.
* @param {(title: string, list: string[], idList?: VertexLayerElementType[]) => void} [callbacks.onBuildError] - Callback on build errors.
* @param {() => void} [callbacks.onGetOrderSuccess] - Callback for successful ordering.
* @param {(nodes: string[]) => void} [callbacks.onValidateNodes] - Callback to validate nodes.
* @param {(lock: boolean) => void} [callbacks.setLockChat] - Callback to lock/unlock chat.
* @returns {Promise<boolean>} Promise that resolves to true if the event was handled successfully.
*/
async function onEvent(
type: string,
data: any,
buildResults: boolean[],
verticesStartTimeMs: Map<string, number>,
callbacks: {
onBuildStart?: (idList: VertexLayerElementType[]) => void;
onBuildUpdate?: (data: any, status: BuildStatus, buildId: string) => void;
onBuildComplete?: (allNodesValid: boolean) => void;
onBuildError?: (
title: string,
list: string[],
idList?: VertexLayerElementType[],
) => void;
onGetOrderSuccess?: () => void;
onValidateNodes?: (nodes: string[]) => void;
},
): Promise<boolean> {
const {
onBuildStart,
onBuildUpdate,
onBuildComplete,
onBuildError,
onGetOrderSuccess,
onValidateNodes,
} = callbacks;
// Helper to update status and register start times for an array of vertex IDs.
const onStartVertices = (ids: Array<string>) => {
useFlowStore.getState().updateBuildStatus(ids, BuildStatus.TO_BUILD);
if (onBuildStart) {
onBuildStart(ids.map((id) => ({ id: id, reference: id })));
}
ids.forEach((id) => verticesStartTimeMs.set(id, Date.now()));
};
switch (type) {
case "vertices_sorted": {
const verticesToRun = data.to_run;
const verticesIds = data.ids;
onStartVertices(verticesIds);
const verticesLayers: Array<Array<VertexLayerElementType>> =
verticesIds.map((id: string) => [{ id: id, reference: id }]);
useFlowStore.getState().updateVerticesBuild({
verticesLayers,
verticesIds,
verticesToRun,
});
if (onValidateNodes) {
try {
onValidateNodes(data.to_run);
if (onGetOrderSuccess) onGetOrderSuccess();
useFlowStore.getState().setIsBuilding(true);
return true;
} catch (e) {
useFlowStore.getState().setIsBuilding(false);
return false;
}
}
return true;
}
case "end_vertex": {
const buildData = data.build_data;
const startTimeMs = verticesStartTimeMs.get(buildData.id);
if (startTimeMs) {
const delta = Date.now() - startTimeMs;
if (delta < MIN_VISUAL_BUILD_TIME_MS) {
// Ensure a minimum visual build time for a smoother UI experience.
await new Promise((resolve) =>
setTimeout(resolve, MIN_VISUAL_BUILD_TIME_MS - delta),
);
}
}
if (onBuildUpdate) {
if (!buildData.valid) {
// Aggregate error messages from the build outputs.
const errorMessages = Object.keys(buildData.data.outputs).flatMap(
(key) => {
const outputs = buildData.data.outputs[key];
if (Array.isArray(outputs)) {
return outputs
.filter((log) => isErrorLogType(log.message))
.map((log) => log.message.errorMessage);
}
if (!isErrorLogType(outputs.message)) {
return [];
}
return [outputs.message.errorMessage];
},
);
onBuildError &&
onBuildError("Error Building Component", errorMessages, [
{ id: buildData.id },
]);
onBuildUpdate(buildData, BuildStatus.ERROR, "");
buildResults.push(false);
return false;
} else {
onBuildUpdate(buildData, BuildStatus.BUILT, "");
buildResults.push(true);
}
onBuildUpdate(buildData, BuildStatus.ERROR, "");
buildResults.push(false);
return false;
} else {
onBuildUpdate(buildData, BuildStatus.BUILT, "");
buildResults.push(true);
}
}
await useFlowStore.getState().clearEdgesRunningByNodes();
await useFlowStore.getState().clearEdgesRunningByNodes();
if (buildData.next_vertices_ids) {
if (isStringArray(buildData.next_vertices_ids)) {
useFlowStore
.getState()
.setCurrentBuildingNodeId(buildData?.next_vertices_ids ?? []);
useFlowStore
.getState()
.updateEdgesRunningByNodes(
buildData?.next_vertices_ids ?? [],
true,
);
}
onStartVertices(buildData.next_vertices_ids);
if (buildData.next_vertices_ids) {
if (isStringArray(buildData.next_vertices_ids)) {
useFlowStore
.getState()
.setCurrentBuildingNodeId(buildData.next_vertices_ids ?? []);
useFlowStore
.getState()
.updateEdgesRunningByNodes(buildData.next_vertices_ids ?? [], true);
}
return true;
onStartVertices(buildData.next_vertices_ids);
}
case "add_message": {
//adds a message to the messsage table
useMessagesStore.getState().addMessage(data);
return true;
}
case "token": {
// flushSync and timeout is needed to avoid react batched updates
setTimeout(() => {
flushSync(() => {
useMessagesStore.getState().updateMessageText(data.id, data.chunk);
});
}, 10);
return true;
}
case "remove_message": {
useMessagesStore.getState().removeMessage(data);
return true;
}
case "end": {
const allNodesValid = buildResults.every((result) => result);
onBuildComplete!(allNodesValid);
useFlowStore.getState().setIsBuilding(false);
return true;
}
case "error": {
if (data?.category === "error") {
useMessagesStore.getState().addMessage(data);
if (!data?.properties?.source?.id) {
onBuildError!("Error Building Flow", [data.text]);
}
}
buildResults.push(false);
return true;
}
case "build_start":
useFlowStore
.getState()
.updateBuildStatus([data.id], BuildStatus.BUILDING);
break;
case "build_end":
useFlowStore.getState().updateBuildStatus([data.id], BuildStatus.BUILT);
break;
default:
return true;
return true;
}
return true;
};
return performStreamingRequest({
method: "POST",
url,
body: postData,
onData: async (event) => {
const type = event["event"];
const data = event["data"];
return await onEvent(type, data);
},
onError: (statusCode) => {
if (statusCode === 404) {
throw new Error("Endpoint not available");
case "add_message": {
// Add a message to the messages store.
useMessagesStore.getState().addMessage(data);
return true;
}
case "token": {
// Use flushSync with a timeout to avoid React batching issues.
setTimeout(() => {
flushSync(() => {
useMessagesStore.getState().updateMessageText(data.id, data.chunk);
});
}, 10);
return true;
}
case "remove_message": {
useMessagesStore.getState().removeMessage(data);
return true;
}
case "end": {
const allNodesValid = buildResults.every((result) => result);
onBuildComplete && onBuildComplete(allNodesValid);
useFlowStore.getState().setIsBuilding(false);
return true;
}
case "error": {
if (data?.category === "error") {
useMessagesStore.getState().addMessage(data);
// Use a falsy check to correctly determine if the source ID is missing.
if (!data?.properties?.source?.id) {
onBuildError && onBuildError("Error Building Flow", [data.text]);
}
}
throw new Error("Error Building Component");
},
onNetworkError: (error: Error) => {
if (error.name === "AbortError") {
onBuildStopped && onBuildStopped();
return;
}
onBuildError!("Error Building Component", [
"Network error. Please check the connection to the server.",
]);
},
});
buildResults.push(false);
return true;
}
case "build_start":
useFlowStore
.getState()
.updateBuildStatus([data.id], BuildStatus.BUILDING);
break;
case "build_end":
useFlowStore.getState().updateBuildStatus([data.id], BuildStatus.BUILT);
break;
default:
return true;
}
return true;
}
export async function buildVertices({

View file

@ -3,8 +3,9 @@ import * as dotenv from "dotenv";
import path from "path";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
test(
withEventDeliveryModes(
"Basic Prompting (Hello, World)",
{ tag: ["@release", "@starter-projects"] },
async ({ page }) => {

View file

@ -3,8 +3,9 @@ import * as dotenv from "dotenv";
import path from "path";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
test(
withEventDeliveryModes(
"Blog Writer",
{ tag: ["@release", "@starter-projects"] },
async ({ page }) => {

View file

@ -5,8 +5,9 @@ import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { getAllResponseMessage } from "../../utils/get-all-response-message";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { waitForOpenModalWithChatInput } from "../../utils/wait-for-open-modal";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
test(
withEventDeliveryModes(
"Custom Component Generator",
{ tag: ["@release", "@starter-projects"] },
async ({ page }) => {

View file

@ -3,8 +3,9 @@ import * as dotenv from "dotenv";
import path from "path";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
test(
withEventDeliveryModes(
"Document Q&A",
{ tag: ["@release", "@starter-projects"] },
async ({ page }) => {

View file

@ -2,18 +2,14 @@ import { expect, test } from "@playwright/test";
import * as dotenv from "dotenv";
import { readFileSync } from "fs";
import path from "path";
import { addNewApiKeys } from "../../utils/add-new-api-keys";
import { adjustScreenView } from "../../utils/adjust-screen-view";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { buildDataTransfer } from "../../utils/build-data-transfer";
import { getAllResponseMessage } from "../../utils/get-all-response-message";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { removeOldApiKeys } from "../../utils/remove-old-api-keys";
import { selectGptModel } from "../../utils/select-gpt-model";
import { updateOldComponents } from "../../utils/update-old-components";
import { waitForOpenModalWithoutChatInput } from "../../utils/wait-for-open-modal";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
test(
withEventDeliveryModes(
"Image Sentiment Analysis",
{ tag: ["@release", "@starter-projects"] },
async ({ page }) => {

View file

@ -5,8 +5,9 @@ import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { getAllResponseMessage } from "../../utils/get-all-response-message";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { waitForOpenModalWithChatInput } from "../../utils/wait-for-open-modal";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
test(
withEventDeliveryModes(
"Market Research",
{ tag: ["@release", "@starter-projects"] },
async ({ page }) => {

View file

@ -3,8 +3,9 @@ import * as dotenv from "dotenv";
import path from "path";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
test(
withEventDeliveryModes(
"Memory Chatbot",
{ tag: ["@release", "@starter-projects"] },
async ({ page }) => {

View file

@ -1,17 +1,13 @@
import { expect, test } from "@playwright/test";
import * as dotenv from "dotenv";
import path from "path";
import { addNewApiKeys } from "../../utils/add-new-api-keys";
import { adjustScreenView } from "../../utils/adjust-screen-view";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { getAllResponseMessage } from "../../utils/get-all-response-message";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { removeOldApiKeys } from "../../utils/remove-old-api-keys";
import { selectGptModel } from "../../utils/select-gpt-model";
import { updateOldComponents } from "../../utils/update-old-components";
import { waitForOpenModalWithChatInput } from "../../utils/wait-for-open-modal";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
test(
withEventDeliveryModes(
"Prompt Chaining",
{ tag: ["@release", "@starter-projects"] },
async ({ page }) => {

View file

@ -1,17 +1,13 @@
import { expect, test } from "@playwright/test";
import * as dotenv from "dotenv";
import path from "path";
import { addNewApiKeys } from "../../utils/add-new-api-keys";
import { adjustScreenView } from "../../utils/adjust-screen-view";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { getAllResponseMessage } from "../../utils/get-all-response-message";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { removeOldApiKeys } from "../../utils/remove-old-api-keys";
import { selectGptModel } from "../../utils/select-gpt-model";
import { updateOldComponents } from "../../utils/update-old-components";
import { waitForOpenModalWithoutChatInput } from "../../utils/wait-for-open-modal";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
test(
withEventDeliveryModes(
"SEO Keyword Generator",
{ tag: ["@release", "@starter-projects"] },
async ({ page }) => {

View file

@ -1,17 +1,13 @@
import { expect, test } from "@playwright/test";
import * as dotenv from "dotenv";
import path from "path";
import { addNewApiKeys } from "../../utils/add-new-api-keys";
import { adjustScreenView } from "../../utils/adjust-screen-view";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { getAllResponseMessage } from "../../utils/get-all-response-message";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { removeOldApiKeys } from "../../utils/remove-old-api-keys";
import { selectGptModel } from "../../utils/select-gpt-model";
import { updateOldComponents } from "../../utils/update-old-components";
import { waitForOpenModalWithoutChatInput } from "../../utils/wait-for-open-modal";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
test(
withEventDeliveryModes(
"SaaS Pricing",
{ tag: ["@release", "@starter-projects"] },
async ({ page }) => {

View file

@ -3,8 +3,9 @@ import * as dotenv from "dotenv";
import path from "path";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
test(
withEventDeliveryModes(
"Simple Agent",
{ tag: ["@release", "@starter-projects"] },
async ({ page }) => {

View file

@ -1,15 +1,11 @@
import { expect, Page, test } from "@playwright/test";
import * as dotenv from "dotenv";
import path from "path";
import { addNewApiKeys } from "../../utils/add-new-api-keys";
import { adjustScreenView } from "../../utils/adjust-screen-view";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { removeOldApiKeys } from "../../utils/remove-old-api-keys";
import { selectGptModel } from "../../utils/select-gpt-model";
import { updateOldComponents } from "../../utils/update-old-components";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
test(
withEventDeliveryModes(
"Travel Planning Agent",
{ tag: ["@release", "@starter-projects"] },
async ({ page }) => {

View file

@ -1,17 +1,13 @@
import { expect, test } from "@playwright/test";
import * as dotenv from "dotenv";
import path from "path";
import { addNewApiKeys } from "../../utils/add-new-api-keys";
import { adjustScreenView } from "../../utils/adjust-screen-view";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { getAllResponseMessage } from "../../utils/get-all-response-message";
import { initialGPTsetup } from "../../utils/initialGPTsetup";
import { removeOldApiKeys } from "../../utils/remove-old-api-keys";
import { selectGptModel } from "../../utils/select-gpt-model";
import { updateOldComponents } from "../../utils/update-old-components";
import { waitForOpenModalWithoutChatInput } from "../../utils/wait-for-open-modal";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
test(
withEventDeliveryModes(
"Twitter Thread Generator",
{ tag: ["@release", "@starter-projects"] },
async ({ page }) => {

View file

@ -1,9 +1,10 @@
import { expect, Page, test } from "@playwright/test";
import { expect, test } from "@playwright/test";
import path from "path";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
import { extractAndCleanCode } from "../../utils/extract-and-clean-code";
import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes";
test(
withEventDeliveryModes(
"Vector Store RAG",
{ tag: ["@release", "@starter-projects"] },
async ({ page }) => {

View file

@ -0,0 +1,34 @@
import { Page, test } from "@playwright/test";
type TestFunction = (args: { page: Page }) => Promise<void>;
type TestConfig = Parameters<typeof test>[1];
/**
* Wraps a test function to run it with both streaming and polling event delivery modes.
*
* @param title The test title
* @param config The test configuration (tags, etc)
* @param testFn The test function to wrap
*/
export function withEventDeliveryModes(
title: string,
config: TestConfig,
testFn: TestFunction,
) {
const eventDeliveryModes = ["streaming", "polling"] as const;
for (const eventDelivery of eventDeliveryModes) {
test(`${title} - ${eventDelivery}`, config, async ({ page }) => {
// Intercept the config request and modify the event_delivery setting
await page.route("**/api/v1/config", async (route) => {
const response = await route.fetch();
const json = await response.json();
json.event_delivery = eventDelivery;
await route.fulfill({ response, json });
});
// Run the original test function
await testFn({ page });
});
}
}