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:
parent
868cc4df76
commit
1530e61669
51 changed files with 1618 additions and 814 deletions
|
|
@ -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 });
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -38,3 +38,8 @@ export enum IOOutputTypes {
|
|||
STRING_LIST = "StringListOutput",
|
||||
DATA = "DataOutput",
|
||||
}
|
||||
|
||||
export enum EventDeliveryType {
|
||||
STREAMING = "streaming",
|
||||
POLLING = "polling",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> = (
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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: (
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
34
src/frontend/tests/utils/withEventDeliveryModes.ts
Normal file
34
src/frontend/tests/utils/withEventDeliveryModes.ts
Normal 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 });
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue