Bugfix: add build status on backend shutdown (#1451)

This pull request introduces a new feature aimed at updating the build
flow status for each vertex when the backend is shut down. Additionally,
a minor refactor has been implemented in the interceptor to enhance code
readability.
This commit is contained in:
Gabriel Luiz Freitas Almeida 2024-02-20 20:38:40 -03:00 • committed by GitHub
commit 4cfb09a69a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 155 additions and 100 deletions

View file

@ -19,7 +19,7 @@ from langflow.api.v1.schemas import (
BuildStatus, BuildStatus,
BuiltResponse, BuiltResponse,
InitResponse, InitResponse,
ResultDict, ResultData,
StreamData, StreamData,
VertexBuildResponse, VertexBuildResponse,
VerticesOrderResponse, VerticesOrderResponse,
@ -318,6 +318,7 @@ async def build_vertex(
current_user=Depends(get_current_active_user), current_user=Depends(get_current_active_user),
): ):
"""Build a vertex instead of the entire graph.""" """Build a vertex instead of the entire graph."""
start_time = time.perf_counter()
try: try:
start_time = time.perf_counter() start_time = time.perf_counter()
cache = chat_service.get_cache(flow_id) cache = chat_service.get_cache(flow_id)
@ -327,7 +328,7 @@ async def build_vertex(
f"No cache found for {flow_id}. Building graph starting at {vertex_id}" f"No cache found for {flow_id}. Building graph starting at {vertex_id}"
) )
graph = build_and_cache_graph( graph = build_and_cache_graph(
flow_id=flow_id, session=get_session(), chat_service=chat_service flow_id=flow_id, session=next(get_session()), chat_service=chat_service
) )
else: else:
graph = cache.get("result") graph = cache.get("result")
@ -345,13 +346,9 @@ async def build_vertex(
# to the frontend # to the frontend
vertex.set_artifacts() vertex.set_artifacts()
artifacts = vertex.artifacts artifacts = vertex.artifacts
timedelta = time.perf_counter() - start_time result_dict = ResultData(
duration = format_elapsed_time(timedelta)
result_dict = ResultDict(
results=result_dict, results=result_dict,
artifacts=artifacts, artifacts=artifacts,
duration=duration,
timedelta=timedelta,
) )
vertex.set_result(result_dict) vertex.set_result(result_dict)
elif vertex.result is not None: elif vertex.result is not None:
@ -364,7 +361,7 @@ async def build_vertex(
except Exception as exc: except Exception as exc:
params = repr(exc) params = repr(exc)
valid = False valid = False
result_dict = ResultDict(results={}) result_dict = ResultData(results={})
artifacts = {} artifacts = {}
# If there's an error building the vertex # If there's an error building the vertex
# we need to clear the cache # we need to clear the cache
@ -377,6 +374,12 @@ async def build_vertex(
data=result_dict, data=result_dict,
artifacts=artifacts, artifacts=artifacts,
) )
timedelta = time.perf_counter() - start_time
duration = format_elapsed_time(timedelta)
result_dict.duration = duration
result_dict.timedelta = timedelta
return VertexBuildResponse( return VertexBuildResponse(
valid=valid, valid=valid,
params=params, params=params,

View file

@ -222,7 +222,7 @@ class VerticesOrderResponse(BaseModel):
ids: List[List[str]] ids: List[List[str]]
class ResultDict(BaseModel): class ResultData(BaseModel):
results: Optional[Any] = Field(default_factory=dict) results: Optional[Any] = Field(default_factory=dict)
artifacts: Optional[Any] = Field(default_factory=dict) artifacts: Optional[Any] = Field(default_factory=dict)
timedelta: Optional[float] = None timedelta: Optional[float] = None
@ -240,7 +240,7 @@ class VertexBuildResponse(BaseModel):
valid: bool valid: bool
params: Optional[str] params: Optional[str]
"""JSON string of the params.""" """JSON string of the params."""
data: ResultDict data: ResultData
"""Mapping of vertex ids to result dict containing the param name and result value.""" """Mapping of vertex ids to result dict containing the param name and result value."""
timestamp: Optional[datetime] = Field(default_factory=datetime.utcnow) timestamp: Optional[datetime] = Field(default_factory=datetime.utcnow)
"""Timestamp of the build.""" """Timestamp of the build."""

View file

@ -417,6 +417,7 @@ class Graph:
if edge not in edges: if edge not in edges:
edges.append(edge) edges.append(edge)
vertices = self.layered_topological_sort(vertices, edges) vertices = self.layered_topological_sort(vertices, edges)
vertices = self.sort_interface_components_first(vertices)
return self.sort_chat_inputs_first(vertices) return self.sort_chat_inputs_first(vertices)
def layered_topological_sort( def layered_topological_sort(
@ -522,11 +523,23 @@ class Graph:
# and sort the layers accordingly # and sort the layers accordingly
# InterfaceComponentTypes is an enum # InterfaceComponentTypes is an enum
# check all values of the enum and sort the layers # check all values of the enum and sort the layers
for layer in vertices: vertices = self.sort_interface_components_first(vertices)
layer.sort(
key=lambda x: any(
comp_type.value in x
for comp_type in InterfaceComponentTypes.__members__.values()
)
)
return self.sort_chat_inputs_first(vertices) return self.sort_chat_inputs_first(vertices)
def sort_interface_components_first(self, vertices: List[Vertex]) -> List[Vertex]:
"""Sorts the vertices in the graph so that vertices containing ChatInput or ChatOutput come first."""
def contains_interface_component(vertex):
return any(
component.value in vertex for component in InterfaceComponentTypes
)
# Sort each inner list so that vertices containing ChatInput or ChatOutput come first
sorted_vertices = [
sorted(
inner_list,
key=lambda vertex: not contains_interface_component(vertex),
)
for inner_list in vertices
]
return sorted_vertices

View file

@ -14,7 +14,7 @@ from langflow.utils.util import sync_to_async
from loguru import logger from loguru import logger
if TYPE_CHECKING: if TYPE_CHECKING:
from langflow.api.v1.schemas import ResultDict from langflow.api.v1.schemas import ResultData
from langflow.graph.edge.base import ContractEdge from langflow.graph.edge.base import ContractEdge
from langflow.graph.graph.base import Graph from langflow.graph.graph.base import Graph
@ -50,7 +50,7 @@ class Vertex:
self.parent_is_top_level = False self.parent_is_top_level = False
self.layer = None self.layer = None
self.should_run = True self.should_run = True
self.result: Optional["ResultDict"] = None self.result: Optional["ResultData"] = None
try: try:
self.is_interface_component = InterfaceComponentTypes(self.vertex_type) self.is_interface_component = InterfaceComponentTypes(self.vertex_type)
except ValueError: except ValueError:
@ -81,7 +81,7 @@ class Vertex:
) )
return edge_results return edge_results
def set_result(self, result: "ResultDict") -> None: def set_result(self, result: "ResultData") -> None:
self.result = result self.result = result
def get_built_result(self): def get_built_result(self):
@ -306,7 +306,7 @@ class Vertex:
params.pop(key, None) params.pop(key, None)
# Add _type to params # Add _type to params
self.params = params self.params = params
self._raw_params = params self._raw_params = params.copy()
async def _build(self, user_id=None): async def _build(self, user_id=None):
""" """

View file

@ -1,13 +1,12 @@
from typing import TYPE_CHECKING, Any, Dict, Optional, Type from typing import TYPE_CHECKING, Any, Dict, Optional, Type
import duckdb import duckdb
from langflow.services.deps import get_monitor_service
from loguru import logger from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
from langflow.services.deps import get_monitor_service
if TYPE_CHECKING: if TYPE_CHECKING:
from langflow.api.v1.schemas import ResultDict from langflow.api.v1.schemas import ResultData
INDEX_KEY = "index" INDEX_KEY = "index"
@ -44,7 +43,9 @@ def model_to_sql_column_definitions(model: Type[BaseModel]) -> dict:
return columns return columns
def drop_and_create_table_if_schema_mismatch(db_path: str, table_name: str, model: Type[BaseModel]): def drop_and_create_table_if_schema_mismatch(
db_path: str, table_name: str, model: Type[BaseModel]
):
with duckdb.connect(db_path) as conn: with duckdb.connect(db_path) as conn:
# Get the current schema from the database # Get the current schema from the database
try: try:
@ -64,8 +65,12 @@ def drop_and_create_table_if_schema_mismatch(db_path: str, table_name: str, mode
conn.execute(f"CREATE SEQUENCE seq_{table_name} START 1;") conn.execute(f"CREATE SEQUENCE seq_{table_name} START 1;")
except duckdb.CatalogException: except duckdb.CatalogException:
pass pass
desired_schema[INDEX_KEY] = f"INTEGER PRIMARY KEY DEFAULT NEXTVAL('seq_{table_name}')" desired_schema[INDEX_KEY] = (
columns_sql = ", ".join(f"{name} {data_type}" for name, data_type in desired_schema.items()) f"INTEGER PRIMARY KEY DEFAULT NEXTVAL('seq_{table_name}')"
)
columns_sql = ", ".join(
f"{name} {data_type}" for name, data_type in desired_schema.items()
)
create_table_sql = f"CREATE TABLE {table_name} ({columns_sql})" create_table_sql = f"CREATE TABLE {table_name} ({columns_sql})"
conn.execute(create_table_sql) conn.execute(create_table_sql)
@ -133,7 +138,7 @@ async def log_vertex_build(
vertex_id: str, vertex_id: str,
valid: bool, valid: bool,
params: Any, params: Any,
data: "ResultDict", data: "ResultData",
artifacts: Optional[dict] = None, artifacts: Optional[dict] = None,
): ):
try: try:

View file

@ -3,7 +3,7 @@ from typing import Callable
import socketio import socketio
from langflow.api.utils import format_elapsed_time from langflow.api.utils import format_elapsed_time
from langflow.api.v1.schemas import ResultDict, VertexBuildResponse from langflow.api.v1.schemas import ResultData, VertexBuildResponse
from langflow.graph.graph.base import Graph from langflow.graph.graph.base import Graph
from langflow.graph.vertex.base import StatelessVertex from langflow.graph.vertex.base import StatelessVertex
from langflow.services.database.models.flow.model import Flow from langflow.services.database.models.flow.model import Flow
@ -73,11 +73,16 @@ async def build_vertex(
artifacts = vertex.artifacts artifacts = vertex.artifacts
timedelta = time.perf_counter() - start_time timedelta = time.perf_counter() - start_time
duration = format_elapsed_time(timedelta) duration = format_elapsed_time(timedelta)
result_dict = ResultDict(results=result_dict, artifacts=artifacts, duration=duration, timedelta=timedelta) result_dict = ResultData(
results=result_dict,
artifacts=artifacts,
duration=duration,
timedelta=timedelta,
)
except Exception as exc: except Exception as exc:
params = str(exc) params = str(exc)
valid = False valid = False
result_dict = ResultDict(results={}) result_dict = ResultData(results={})
artifacts = {} artifacts = {}
set_cache(flow_id, graph) set_cache(flow_id, graph)
await log_vertex_build( await log_vertex_build(
@ -90,7 +95,9 @@ async def build_vertex(
) )
# Emit the vertex build response # Emit the vertex build response
response = VertexBuildResponse(valid=valid, params=params, id=vertex.id, data=result_dict) response = VertexBuildResponse(
valid=valid, params=params, id=vertex.id, data=result_dict
)
await sio.emit("vertex_build", data=response.model_dump(), to=sid) await sio.emit("vertex_build", data=response.model_dump(), to=sid)
except Exception as exc: except Exception as exc:

View file

@ -3,8 +3,10 @@ import { useContext, useEffect } from "react";
import { Cookies } from "react-cookie"; import { Cookies } from "react-cookie";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { renewAccessToken } from "."; import { renewAccessToken } from ".";
import { BuildStatus } from "../../constants/enums";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
// Create a new Axios instance // Create a new Axios instance
const api: AxiosInstance = axios.create({ const api: AxiosInstance = axios.create({
@ -24,45 +26,20 @@ function ApiInterceptor() {
async (error: AxiosError) => { async (error: AxiosError) => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
const accessToken = cookies.get("access_token_lf"); const accessToken = cookies.get("access_token_lf");
if (accessToken && !autoLogin) { if (accessToken && !autoLogin) {
authenticationErrorCount = authenticationErrorCount + 1; checkErrorCount();
if (authenticationErrorCount > 3) { await tryToRenewAccessToken(error);
authenticationErrorCount = 0;
logout();
}
try {
const res = await renewAccessToken();
if (res?.data?.access_token && res?.data?.refresh_token) {
login(res?.data?.access_token);
}
if (error?.config?.headers) {
delete error.config.headers["Authorization"];
error.config.headers["Authorization"] = `Bearer ${cookies.get(
"access_token_lf"
)}`;
const response = await axios.request(error.config);
return response;
}
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
logout();
} else {
console.error(error);
logout();
}
}
} }
if (!accessToken && error?.config?.url?.includes("login")) { if (!accessToken && error?.config?.url?.includes("login")) {
return Promise.reject(error); return Promise.reject(error);
} else {
logout();
} }
} else {
// if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) { return logout();
}
await clearBuildVerticesState(error);
return Promise.reject(error); return Promise.reject(error);
// }
}
} }
); );
@ -115,6 +92,41 @@ function ApiInterceptor() {
}; };
}, [accessToken, setErrorData]); }, [accessToken, setErrorData]);
function checkErrorCount() {
authenticationErrorCount = authenticationErrorCount + 1;
if (authenticationErrorCount > 3) {
authenticationErrorCount = 0;
logout();
}
}
async function tryToRenewAccessToken(error: AxiosError) {
try {
const res = await renewAccessToken();
if (res?.data?.access_token && res?.data?.refresh_token) {
login(res?.data?.access_token);
}
if (error?.config?.headers) {
delete error.config.headers["Authorization"];
error.config.headers["Authorization"] = `Bearer ${cookies.get(
"access_token_lf"
)}`;
const response = await axios.request(error.config);
return response;
}
} catch (error) {
logout();
}
}
async function clearBuildVerticesState(error) {
if (error?.response?.status === 500) {
const vertices = useFlowStore.getState().verticesBuild;
useFlowStore.getState().updateBuildStatus(vertices, BuildStatus.BUILT);
}
}
return null; return null;
} }

View file

@ -431,6 +431,10 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
} }
}); });
}, },
updateVerticesBuild: (vertices: string[]) => {
set({ verticesBuild: vertices });
},
verticesBuild: [],
})); }));
export default useFlowStore; export default useFlowStore;

View file

@ -87,4 +87,6 @@ export type FlowStoreType = {
buildFlow: (nodeId?: string) => Promise<void>; buildFlow: (nodeId?: string) => Promise<void>;
getFlow: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport }; getFlow: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport };
updateBuildStatus: (nodeId: string[], status: BuildStatus) => void; updateBuildStatus: (nodeId: string[], status: BuildStatus) => void;
updateVerticesBuild: (vertices: string[]) => void;
verticesBuild: string[];
}; };

View file

@ -47,50 +47,59 @@ export async function buildVertices({
const verticesIds = vertices.flatMap((v) => v); const verticesIds = vertices.flatMap((v) => v);
useFlowStore.getState().updateBuildStatus(verticesIds, BuildStatus.TO_BUILD); useFlowStore.getState().updateBuildStatus(verticesIds, BuildStatus.TO_BUILD);
useFlowStore.getState().updateVerticesBuild(verticesIds);
// Set each vertex state to building // Set each vertex state to building
const buildResults: Array<boolean> = []; const buildResults: Array<boolean> = [];
for (let i = 0; i < vertices.length; i += 1) { for (let i = 0; i < vertices.length; i += 1) {
if (onBuildStart) onBuildStart(vertices[i]); if (onBuildStart) onBuildStart(vertices[i]);
await Promise.all( for (const id of vertices[i]) {
vertices[i].map(async (id) => { buildVertex(
flowId,
id,
onBuildUpdate,
onBuildError,
verticesIds,
buildResults
);
}
}
if (onBuildComplete) {
const allNodesValid = buildResults.every((result) => result);
onBuildComplete(allNodesValid);
}
}
async function buildVertex(
flowId,
id,
onBuildUpdate,
onBuildError,
verticesIds,
buildResults
) {
try { try {
// Set vertex state to building
const buildRes = await postBuildVertex(flowId, id); const buildRes = await postBuildVertex(flowId, id);
const buildData: VertexBuildTypeAPI = buildRes.data; const buildData: VertexBuildTypeAPI = buildRes.data;
if (onBuildUpdate) { if (onBuildUpdate) {
let data = {}; let data = {};
if (!buildData.valid) { if (!buildData.valid) {
if (onBuildError) { onBuildError!(
onBuildError(
"Error Building Component", "Error Building Component",
[buildData.params], [buildData.params],
verticesIds verticesIds
); );
} }
}
data[buildData.id] = buildData; data[buildData.id] = buildData;
onBuildUpdate({ data, id: buildData.id }); onBuildUpdate({ data, id: buildData.id });
} }
buildResults.push(buildData.valid); buildResults.push(buildData.valid);
} catch (error) { } catch (error) {
if (onBuildError) { onBuildError!(
console.log(error);
onBuildError(
"Error Building Component", "Error Building Component",
[ [(error as AxiosError<any>).response?.data?.detail ?? "Unknown Error"],
(error as AxiosError<any>).response?.data?.detail ??
"Unknown Error",
],
verticesIds verticesIds
); );
} }
} }
})
);
}
if (onBuildComplete) {
const allNodesValid = buildResults.every((result) => result);
onBuildComplete(allNodesValid);
}
}