✨ feat(chat.py): add error handling to stream_build function and improve log messages

✨ feat(schemas.py): add StreamData class to handle SSE messages
✨ feat(frontend): add RadialProgress and ProgressBar components
🔧 chore(frontend): add daisyui to project dependencies

🎨 style(headerComponent): comment out unused code
✨ feat(ui/progress.tsx): add Progress component to reuse in other components
✨ feat(types/components): add ProgressBarType and RadialProgressType to use in components
🎨 style(tailwind.config.js): add daisyui plugin to tailwind configuration to use in the project
This commit is contained in:
Cristhian Zanforlin Lousa 2023-06-22 19:48:37 -03:00
commit f3e6db4df1
11 changed files with 2345 additions and 1442 deletions

View file

@ -1,4 +1,3 @@
import json
from fastapi import ( from fastapi import (
APIRouter, APIRouter,
HTTPException, HTTPException,
@ -7,7 +6,7 @@ from fastapi import (
status, status,
) )
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from langflow.api.v1.schemas import BuiltResponse, InitResponse from langflow.api.v1.schemas import BuiltResponse, InitResponse, StreamData
from langflow.chat.manager import ChatManager from langflow.chat.manager import ChatManager
from langflow.graph.graph.base import Graph from langflow.graph.graph.base import Graph
@ -71,30 +70,37 @@ async def stream_build(flow_id: str):
"""Stream the build process based on stored flow data.""" """Stream the build process based on stored flow data."""
async def event_stream(flow_id): async def event_stream(flow_id):
final_response = json.dumps({"end_of_stream": True}) final_response = {"end_of_stream": True}
try: try:
if flow_id not in flow_data_store: if flow_id not in flow_data_store:
error_message = "Invalid session ID" error_message = "Invalid session ID"
yield f"data: {json.dumps({'error': error_message})}\n\n" yield str(StreamData(event="error", data={"error": error_message}))
return return
graph_data = flow_data_store[flow_id].get("data") graph_data = flow_data_store[flow_id].get("data")
if not graph_data: if not graph_data:
error_message = "No data provided" error_message = "No data provided"
yield f"data: {json.dumps({'error': error_message})}\n\n" yield str(StreamData(event="error", data={"error": error_message}))
return return
logger.debug("Building langchain object") logger.debug("Building langchain object")
try:
# Some error could happen when building the graph
graph = Graph.from_payload(graph_data) graph = Graph.from_payload(graph_data)
except Exception as exc:
logger.exception(exc)
error_message = str(exc)
yield str(StreamData(event="error", data={"error": error_message}))
return
number_of_nodes = len(graph.nodes) number_of_nodes = len(graph.nodes)
for i, vertex in enumerate(graph.generator_build(), 1): for i, vertex in enumerate(graph.generator_build(), 1):
try: try:
log_dict = { log_dict = {
"log": f"Building node {vertex.vertex_type}", "log": f"Building node {vertex.vertex_type}",
"progress": round(i / number_of_nodes, 2),
} }
yield f"data: {json.dumps(log_dict)}\n\n" yield str(StreamData(event="log", data=log_dict))
vertex.build() vertex.build()
params = vertex._built_object_repr() params = vertex._built_object_repr()
valid = True valid = True
@ -105,21 +111,21 @@ async def stream_build(flow_id: str):
params = str(exc) params = str(exc)
valid = False valid = False
response = json.dumps( response = {
{
"valid": valid, "valid": valid,
"params": params, "params": params,
"id": vertex.id, "id": vertex.id,
"progress": round(i / number_of_nodes, 2),
} }
)
yield f"data: {response}\n\n" yield str(StreamData(event="message", data=response))
chat_manager.set_cache(flow_id, graph.build()) chat_manager.set_cache(flow_id, graph.build())
except Exception as exc: except Exception as exc:
logger.error("Error while building the flow: %s", exc) logger.error("Error while building the flow: %s", exc)
yield f"error: {json.dumps({'error': str(exc)})}\n\n" yield str(StreamData(event="error", data={"error": str(exc)}))
finally: finally:
yield f"data: {final_response}\n\n" yield str(StreamData(event="message", data=final_response))
try: try:
return StreamingResponse(event_stream(flow_id), media_type="text/event-stream") return StreamingResponse(event_stream(flow_id), media_type="text/event-stream")

View file

@ -2,6 +2,7 @@ from pathlib import Path
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
from langflow.database.models.flow import FlowCreate, FlowRead from langflow.database.models.flow import FlowCreate, FlowRead
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator
import json
class GraphData(BaseModel): class GraphData(BaseModel):
@ -97,3 +98,9 @@ class UploadFileResponse(BaseModel):
flowId: str flowId: str
file_path: Path file_path: Path
class StreamData(BaseModel):
event: str
data: dict
def __str__(self) -> str:
return f"event: {self.event}\ndata: {json.dumps(self.data)}\n\n"

File diff suppressed because it is too large Load diff

View file

@ -8,15 +8,16 @@
"@headlessui/react": "^1.7.10", "@headlessui/react": "^1.7.10",
"@heroicons/react": "^2.0.15", "@heroicons/react": "^2.0.15",
"@mui/material": "^5.11.9", "@mui/material": "^5.11.9",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-menubar": "^1.0.3",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-menubar": "^1.0.3",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.6", "@radix-ui/react-tooltip": "^1.0.6",
"@tabler/icons-react": "^2.18.0", "@tabler/icons-react": "^2.18.0",
"@tailwindcss/forms": "^0.5.3", "@tailwindcss/forms": "^0.5.3",
@ -98,6 +99,7 @@
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.1",
"@vitejs/plugin-react-swc": "^3.0.0", "@vitejs/plugin-react-swc": "^3.0.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"daisyui": "^3.1.1",
"postcss": "^8.4.23", "postcss": "^8.4.23",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"typescript": "^5.0.2", "typescript": "^5.0.2",

View file

@ -0,0 +1,21 @@
import { ReactElement, useContext, useEffect, useRef, useState } from "react";
import { ProgressBarType } from "../../types/components";
import { Progress } from "../../components/ui/progress";
import { progressContext } from "../../contexts/ProgressContext";
import { setInterval } from "timers/promises";
export default function ProgressBarComponent({
value,
children,
}: ProgressBarType) {
const ref = useRef(0);
const reff = useRef();
const { progress } = useContext(progressContext);
useEffect(() => {
ref.current = progress * 100;
console.log(progress);
}, [progress]);
return <Progress className="h-2.5" value={ref.current} />;
}

View file

@ -0,0 +1,19 @@
import { useContext, useEffect, useRef, useState } from "react";
import { RadialProgressType } from "../../types/components";
export default function RadialProgressComponent({
value,
color,
}: RadialProgressType) {
const style = {
"--value": value * 100,
"--size": "1.5rem",
"--thickness": "2px",
} as React.CSSProperties;
return (
<div className={"radial-progress " + color} style={style}>
<strong className="text-[8px]">{value * 100}%</strong>
</div>
);
}

View file

@ -1,4 +1,4 @@
import { useContext } from "react"; import { useContext, useState } from "react";
import { Transition } from "@headlessui/react"; import { Transition } from "@headlessui/react";
import { Zap } from "lucide-react"; import { Zap } from "lucide-react";
import { validateNodes } from "../../../utils"; import { validateNodes } from "../../../utils";
@ -9,6 +9,8 @@ import { typesContext } from "../../../contexts/typesContext";
import { alertContext } from "../../../contexts/alertContext"; import { alertContext } from "../../../contexts/alertContext";
import { postBuildInit } from "../../../controllers/API"; import { postBuildInit } from "../../../controllers/API";
import RadialProgressComponent from "../../RadialProgress";
export default function BuildTrigger({ export default function BuildTrigger({
open, open,
flow, flow,
@ -20,9 +22,12 @@ export default function BuildTrigger({
setIsBuilt: any; setIsBuilt: any;
isBuilt: boolean; isBuilt: boolean;
}) { }) {
const { updateSSEData, isBuilding, setIsBuilding } = useSSE(); const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE();
const { reactFlowInstance } = useContext(typesContext); const { reactFlowInstance } = useContext(typesContext);
const { setErrorData, setSuccessData } = useContext(alertContext); const { setErrorData, setSuccessData } = useContext(alertContext);
const [isIconTouched, setIsIconTouched] = useState(false);
const eventClick = isBuilding ? "pointer-events-none" : "";
const [progress, setProgress] = useState(0);
async function handleBuild(flow: FlowType) { async function handleBuild(flow: FlowType) {
try { try {
@ -58,12 +63,10 @@ export default function BuildTrigger({
setIsBuilding(false); setIsBuilding(false);
} }
} }
async function streamNodeData(flow: FlowType) { async function streamNodeData(flow: FlowType) {
// Step 1: Make a POST request to send the flow data and receive a unique session ID // Step 1: Make a POST request to send the flow data and receive a unique session ID
const response = await postBuildInit(flow); const response = await postBuildInit(flow);
const { flowId } = response.data; const { flowId } = response.data;
// Step 2: Use the session ID to establish an SSE connection using EventSource // Step 2: Use the session ID to establish an SSE connection using EventSource
let validationResults = []; let validationResults = [];
let finished = false; let finished = false;
@ -83,19 +86,23 @@ export default function BuildTrigger({
return; return;
} else if (parsedData.log) { } else if (parsedData.log) {
// If the event is a log, log it // If the event is a log, log it
// TODO: implement the progress
setSuccessData({ title: parsedData.log }); setSuccessData({ title: parsedData.log });
setSuccessData({ title: parsedData.progress });
} else { } else {
// Otherwise, process the data // Otherwise, process the data
const isValid = processStreamResult(parsedData); const isValid = processStreamResult(parsedData);
setProgress(parsedData.progress);
validationResults.push(isValid); validationResults.push(isValid);
} }
}; };
eventSource.onerror = (error) => { eventSource.onerror = (error: any) => {
console.error("EventSource failed:", error); console.error("EventSource failed:", error);
eventSource.close(); eventSource.close();
if (error.data) {
const parsedData = JSON.parse(error.data);
setErrorData({ title: parsedData.error });
setIsBuilding(false);
}
}; };
// Step 3: Wait for the stream to finish // Step 3: Wait for the stream to finish
while (!finished) { while (!finished) {
@ -129,6 +136,14 @@ export default function BuildTrigger({
} }
} }
const handleMouseEnter = () => {
setIsIconTouched(true);
};
const handleMouseLeave = () => {
setIsIconTouched(false);
};
return ( return (
<Transition <Transition
show={!open} show={!open}
@ -142,17 +157,23 @@ export default function BuildTrigger({
> >
<div className={`fixed right-4` + (isBuilt ? " bottom-20" : " bottom-4")}> <div className={`fixed right-4` + (isBuilt ? " bottom-20" : " bottom-4")}>
<div <div
className="flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full shadow-md shadow-[#0000002a] hover:shadow-[#00000032] className={`${eventClick} flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full shadow-md shadow-[#0000002a] hover:shadow-[#00000032] bg-[#E2E7EE] dark:border-gray-600 cursor-pointer`}
bg-[#E2E7EE] dark:border-gray-600 cursor-pointer"
onClick={() => { onClick={() => {
handleBuild(flow); handleBuild(flow);
}} }}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
> >
<button> <button>
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
{isBuilding ? ( {isBuilding && progress < 1 ? (
// Render your loading animation here when isBuilding is true // Render your loading animation here when isBuilding is true
<Loading strokeWidth={1.5} style={{ color: "white" }} /> <RadialProgressComponent
color={"text-orange-400"}
value={progress}
></RadialProgressComponent>
) : isBuilding ? (
<Loading strokeWidth={1.5} style={{ color: "#fb923c" }} />
) : ( ) : (
<Zap className="sh-6 w-6 fill-orange-400 stroke-1 stroke-orange-400" /> <Zap className="sh-6 w-6 fill-orange-400 stroke-1 stroke-orange-400" />
)} )}

View file

@ -98,7 +98,7 @@ export default function Header() {
> >
<FaDiscord className="h-5 w-5" /> <FaDiscord className="h-5 w-5" />
</a> </a>
<Separator orientation="vertical" /> {/* <Separator orientation="vertical" />
<button <button
className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200" className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200"
onClick={() => { onClick={() => {
@ -110,7 +110,7 @@ export default function Header() {
) : ( ) : (
<MoonIcon className="h-5 w-5" /> <MoonIcon className="h-5 w-5" />
)} )}
</button> </button> */}
<button <button
className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200 relative" className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200 relative"
onClick={(event: React.MouseEvent<HTMLElement>) => { onClick={(event: React.MouseEvent<HTMLElement>) => {

View file

@ -0,0 +1,27 @@
"use client";
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "../../utils";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };

View file

@ -98,3 +98,14 @@ export type TooltipComponentType = {
| "top-start" | "top-start"
| "top"; | "top";
}; };
export type ProgressBarType = {
children?: ReactElement;
value?: number;
max?: number;
};
export type RadialProgressType = {
value?: number;
color?: string;
};

View file

@ -241,5 +241,6 @@ module.exports = {
}); });
}), }),
require("@tailwindcss/typography"), require("@tailwindcss/typography"),
require("daisyui"),
], ],
}; };