Merge branch 'zustand/io/migration' of github.com:logspace-ai/langflow into zustand/io/migration

This commit is contained in:
igorrCarvalho 2024-02-27 21:05:15 -03:00
commit 9af9647fb3
37 changed files with 458 additions and 255 deletions

3
.vscode/launch.json vendored
View file

@ -17,6 +17,9 @@
], ],
"jinja": true, "jinja": true,
"justMyCode": true, "justMyCode": true,
"env": {
"LANGFLOW_LOG_LEVEL": "debug"
},
"envFile": "${workspaceFolder}/.env" "envFile": "${workspaceFolder}/.env"
}, },
{ {

View file

@ -1,6 +1,6 @@
import time import time
from typing import Optional
import uuid import uuid
from typing import TYPE_CHECKING, Optional
from fastapi import ( from fastapi import (
APIRouter, APIRouter,
@ -35,6 +35,9 @@ from langflow.services.chat.service import ChatService
from langflow.services.deps import get_chat_service, get_session from langflow.services.deps import get_chat_service, get_session
from langflow.services.monitor.utils import log_vertex_build from langflow.services.monitor.utils import log_vertex_build
if TYPE_CHECKING:
from langflow.graph.vertex.types import ChatVertex
router = APIRouter(tags=["Chat"]) router = APIRouter(tags=["Chat"])
@ -237,27 +240,40 @@ async def build_vertex_stream(
else: else:
graph = cache.get("result") graph = cache.get("result")
vertex = graph.get_vertex(vertex_id) vertex: "ChatVertex" = graph.get_vertex(vertex_id)
if not hasattr(vertex, "stream"):
raise ValueError(f"Vertex {vertex_id} does not support streaming")
if not vertex.pinned or not vertex._built: if not vertex.pinned or not vertex._built:
logger.debug(f"Streaming vertex {vertex_id}")
stream_data = StreamData( stream_data = StreamData(
event="message", event="message",
data={"message": "Building vertex"}, data={"message": f"Streaming vertex {vertex_id}"},
) )
yield str(stream_data) yield str(stream_data)
number_of_chunks = 0
async for chunk in vertex.stream(): async for chunk in vertex.stream():
stream_data = StreamData( stream_data = StreamData(
event="message", event="message",
data={"chunk": chunk}, data={"chunk": chunk},
) )
number_of_chunks += 1
yield str(stream_data) yield str(stream_data)
logger.debug(f"Number of chunks: {number_of_chunks}")
elif vertex.result is not None:
stream_data = StreamData(
event="message",
data={"chunk": vertex._built_result},
)
yield str(stream_data)
else: else:
raise ValueError(f"No result found for vertex {vertex_id}") raise ValueError(f"No result found for vertex {vertex_id}")
except Exception as exc: except Exception as exc:
logger.error(f"Error building vertex: {exc}")
yield str(StreamData(event="error", data={"error": str(exc)})) yield str(StreamData(event="error", data={"error": str(exc)}))
finally:
yield str(StreamData(event="close", data={"message": "Stream closed"})) logger.debug("Closing stream")
yield str(StreamData(event="close", data={"message": "Stream closed"}))
return StreamingResponse(stream_vertex(), media_type="text/event-stream") return StreamingResponse(stream_vertex(), media_type="text/event-stream")
except Exception as exc: except Exception as exc:

View file

@ -220,7 +220,9 @@ async def preload_flow(
raise HTTPException(status_code=500, detail=str(exc)) from exc raise HTTPException(status_code=500, detail=str(exc)) from exc
@router.post("/run/{flow_id}", response_model=ProcessResponse) @router.post(
"/run/{flow_id}", response_model=RunResponse, response_model_exclude_none=True
)
async def run_flow_with_caching( async def run_flow_with_caching(
session: Annotated[Session, Depends(get_session)], session: Annotated[Session, Depends(get_session)],
flow_id: str, flow_id: str,
@ -235,13 +237,13 @@ async def run_flow_with_caching(
session_data = await session_service.load_session(session_id) session_data = await session_service.load_session(session_id)
graph, artifacts = session_data if session_data else (None, None) graph, artifacts = session_data if session_data else (None, None)
task_result: Any = None task_result: Any = None
task_status = None
if not graph: if not graph:
raise ValueError("Graph not found in the session") raise ValueError("Graph not found in the session")
task_result = await run_graph( task_result = await run_graph(
graph, graph=graph,
session_id, flow_id=flow_id,
inputs, session_id=session_id,
inputs=inputs,
artifacts=artifacts, artifacts=artifacts,
session_service=session_service, session_service=session_service,
) )
@ -262,16 +264,15 @@ async def run_flow_with_caching(
graph_data = flow.data graph_data = flow.data
graph_data = process_tweaks(graph_data, tweaks) graph_data = process_tweaks(graph_data, tweaks)
task_result = await run_graph( task_result = await run_graph(
graph_data, graph=graph_data,
inputs, flow_id=flow_id,
tweaks, session_id=session_id,
session_id, inputs=inputs,
artifacts={},
session_service=session_service, session_service=session_service,
) )
return RunResponse( return RunResponse(outputs=task_result, session_id=session_id)
outputs=task_result, session_id=session_id, status=task_status
)
except sa.exc.StatementError as exc: except sa.exc.StatementError as exc:
# StatementError('(builtins.ValueError) badly formed hexadecimal UUID string') # StatementError('(builtins.ValueError) badly formed hexadecimal UUID string')
if "badly formed hexadecimal UUID string" in str(exc): if "badly formed hexadecimal UUID string" in str(exc):

View file

@ -4,7 +4,7 @@ from pathlib import Path
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
from uuid import UUID from uuid import UUID
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator, model_serializer
from langflow.services.database.models.api_key.model import ApiKeyRead from langflow.services.database.models.api_key.model import ApiKeyRead
from langflow.services.database.models.base import orjson_dumps from langflow.services.database.models.base import orjson_dumps
@ -70,9 +70,21 @@ class RunResponse(BaseModel):
"""Run response schema.""" """Run response schema."""
outputs: Optional[List[Any]] = None outputs: Optional[List[Any]] = None
status: Optional[str] = None
session_id: Optional[str] = None session_id: Optional[str] = None
@model_serializer(mode="wrap")
def serialize(self, handler):
# Serialize all the outputs if they are base models
if self.outputs:
serialized_outputs = []
for output in self.outputs:
if isinstance(output, BaseModel):
serialized_outputs.append(output.model_dump(exclude_none=True))
else:
serialized_outputs.append(output)
self.outputs = serialized_outputs
return handler(self)
class PreloadResponse(BaseModel): class PreloadResponse(BaseModel):
"""Preload response schema.""" """Preload response schema."""

View file

@ -31,7 +31,7 @@ class ConversationChainComponent(CustomComponent):
chain = ConversationChain(llm=llm) chain = ConversationChain(llm=llm)
else: else:
chain = ConversationChain(llm=llm, memory=memory) chain = ConversationChain(llm=llm, memory=memory)
result = chain.invoke(inputs) result = chain.invoke(input_value)
# result is an AIMessage which is a subclass of BaseMessage # result is an AIMessage which is a subclass of BaseMessage
# We need to check if it is a string or a BaseMessage # We need to check if it is a string or a BaseMessage
if hasattr(result, "content") and isinstance(result.content, str): if hasattr(result, "content") and isinstance(result.content, str):

View file

@ -1,11 +1,11 @@
from typing import Optional, Union from typing import Optional, Union
from langflow import CustomComponent
from langflow.field_typing import Text from langflow.field_typing import Text
from langflow.io.schema import ChatComponent
from langflow.schema import Record from langflow.schema import Record
class ChatOutput(CustomComponent): class ChatOutput(ChatComponent):
display_name = "Chat Output" display_name = "Chat Output"
description = "Used to send a message to the chat." description = "Used to send a message to the chat."

View file

@ -60,7 +60,7 @@ class AmazonBedrockComponent(CustomComponent):
) # type: ignore ) # type: ignore
except Exception as e: except Exception as e:
raise ValueError("Could not connect to AmazonBedrock API.") from e raise ValueError("Could not connect to AmazonBedrock API.") from e
message = output.invoke(inputs) message = output.invoke(input_value)
result = message.content if hasattr(message, "content") else message result = message.content if hasattr(message, "content") else message
self.status = result self.status = result
return result return result

View file

@ -77,7 +77,7 @@ class AnthropicLLM(CustomComponent):
) )
except Exception as e: except Exception as e:
raise ValueError("Could not connect to Anthropic API.") from e raise ValueError("Could not connect to Anthropic API.") from e
message = output.invoke(inputs) message = output.invoke(input_value)
result = message.content if hasattr(message, "content") else message result = message.content if hasattr(message, "content") else message
self.status = result self.status = result
return result return result

View file

@ -99,7 +99,7 @@ class AzureChatOpenAIComponent(CustomComponent):
) )
except Exception as e: except Exception as e:
raise ValueError("Could not connect to AzureOpenAI API.") from e raise ValueError("Could not connect to AzureOpenAI API.") from e
message = output.invoke(inputs) message = output.invoke(input_value)
result = message.content if hasattr(message, "content") else message result = message.content if hasattr(message, "content") else message
self.status = result self.status = result
return result return result

View file

@ -94,7 +94,7 @@ class QianfanChatEndpointComponent(CustomComponent):
) )
except Exception as e: except Exception as e:
raise ValueError("Could not connect to Baidu Qianfan API.") from e raise ValueError("Could not connect to Baidu Qianfan API.") from e
message = output.invoke(inputs) message = output.invoke(input_value)
result = message.content if hasattr(message, "content") else message result = message.content if hasattr(message, "content") else message
self.status = result self.status = result
return result return result

View file

@ -42,7 +42,7 @@ class CTransformersComponent(CustomComponent):
output = CTransformers( output = CTransformers(
model=model, model_file=model_file, model_type=model_type, config=config model=model, model_file=model_file, model_type=model_type, config=config
) )
message = output.invoke(inputs) message = output.invoke(input_value)
result = message.content if hasattr(message, "content") else message result = message.content if hasattr(message, "content") else message
self.status = result self.status = result
return result return result

View file

@ -43,7 +43,7 @@ class CohereComponent(CustomComponent):
max_tokens=max_tokens, max_tokens=max_tokens,
temperature=temperature, temperature=temperature,
) )
message = output.invoke(inputs) message = output.invoke(input_value)
result = message.content if hasattr(message, "content") else message result = message.content if hasattr(message, "content") else message
self.status = result self.status = result
return result return result

View file

@ -73,7 +73,7 @@ class GoogleGenerativeAIComponent(CustomComponent):
n=n or 1, n=n or 1,
google_api_key=SecretStr(google_api_key), google_api_key=SecretStr(google_api_key),
) )
message = output.invoke(inputs) message = output.invoke(input_value)
result = message.content if hasattr(message, "content") else message result = message.content if hasattr(message, "content") else message
self.status = result self.status = result
return result return result

View file

@ -45,7 +45,7 @@ class HuggingFaceEndpointsComponent(CustomComponent):
except Exception as e: except Exception as e:
raise ValueError("Could not connect to HuggingFace Endpoints API.") from e raise ValueError("Could not connect to HuggingFace Endpoints API.") from e
output = ChatHuggingFace(llm=llm) output = ChatHuggingFace(llm=llm)
message = output.invoke(inputs) message = output.invoke(input_value)alue)
result = message.content if hasattr(message, "content") else message result = message.content if hasattr(message, "content") else message
self.status = result self.status = result
return result return result

View file

@ -135,7 +135,7 @@ class LlamaCppComponent(CustomComponent):
verbose=verbose, verbose=verbose,
vocab_only=vocab_only, vocab_only=vocab_only,
) )
message = output.invoke(inputs) message = output.invoke(input_value)
result = message.content if hasattr(message, "content") else message result = message.content if hasattr(message, "content") else message
self.status = result self.status = result
return result return result

View file

@ -250,7 +250,7 @@ class ChatOllamaComponent(CustomComponent):
output = ChatOllama(**llm_params) # type: ignore output = ChatOllama(**llm_params) # type: ignore
except Exception as e: except Exception as e:
raise ValueError("Could not initialize Ollama LLM.") from e raise ValueError("Could not initialize Ollama LLM.") from e
message = output.invoke(inputs) message = output.invoke(input_value)
result = message.content if hasattr(message, "content") else message result = message.content if hasattr(message, "content") else message
self.status = result self.status = result
return result return result

View file

@ -80,7 +80,7 @@ class OpenAIModelComponent(CustomComponent):
temperature=temperature, temperature=temperature,
) )
message = model.invoke(inputs) message = model.invoke(input_value)
result = message.content if hasattr(message, "content") else message result = message.content if hasattr(message, "content") else message
self.status = result self.status = result
return result return result

View file

@ -92,7 +92,7 @@ class ChatVertexAIComponent(CustomComponent):
top_p=top_p, top_p=top_p,
verbose=verbose, verbose=verbose,
) )
message = output.invoke(inputs) message = output.invoke(input_value)
result = message.content if hasattr(message, "content") else message result = message.content if hasattr(message, "content") else message
self.status = result self.status = result
return result return result

View file

@ -4,6 +4,7 @@ from loguru import logger
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from langflow.graph.edge.utils import build_clean_params from langflow.graph.edge.utils import build_clean_params
from langflow.graph.schema import INPUT_FIELD_NAME
from langflow.services.deps import get_monitor_service from langflow.services.deps import get_monitor_service
from langflow.services.monitor.utils import log_message from langflow.services.monitor.utils import log_message
@ -12,7 +13,9 @@ if TYPE_CHECKING:
class SourceHandle(BaseModel): class SourceHandle(BaseModel):
baseClasses: List[str] = Field(..., description="List of base classes for the source handle.") baseClasses: List[str] = Field(
..., description="List of base classes for the source handle."
)
dataType: str = Field(..., description="Data type for the source handle.") dataType: str = Field(..., description="Data type for the source handle.")
id: str = Field(..., description="Unique identifier for the source handle.") id: str = Field(..., description="Unique identifier for the source handle.")
@ -20,7 +23,9 @@ class SourceHandle(BaseModel):
class TargetHandle(BaseModel): class TargetHandle(BaseModel):
fieldName: str = Field(..., description="Field name for the target handle.") fieldName: str = Field(..., description="Field name for the target handle.")
id: str = Field(..., description="Unique identifier for the target handle.") id: str = Field(..., description="Unique identifier for the target handle.")
inputTypes: Optional[List[str]] = Field(None, description="List of input types for the target handle.") inputTypes: Optional[List[str]] = Field(
None, description="List of input types for the target handle."
)
type: str = Field(..., description="Type of the target handle.") type: str = Field(..., description="Type of the target handle.")
@ -49,16 +54,24 @@ class Edge:
def validate_handles(self, source, target) -> None: def validate_handles(self, source, target) -> None:
if self.target_handle.inputTypes is None: if self.target_handle.inputTypes is None:
self.valid_handles = self.target_handle.type in self.source_handle.baseClasses self.valid_handles = (
self.target_handle.type in self.source_handle.baseClasses
)
else: else:
self.valid_handles = ( self.valid_handles = (
any(baseClass in self.target_handle.inputTypes for baseClass in self.source_handle.baseClasses) any(
baseClass in self.target_handle.inputTypes
for baseClass in self.source_handle.baseClasses
)
or self.target_handle.type in self.source_handle.baseClasses or self.target_handle.type in self.source_handle.baseClasses
) )
if not self.valid_handles: if not self.valid_handles:
logger.debug(self.source_handle) logger.debug(self.source_handle)
logger.debug(self.target_handle) logger.debug(self.target_handle)
raise ValueError(f"Edge between {source.vertex_type} and {target.vertex_type} " f"has invalid handles") raise ValueError(
f"Edge between {source.vertex_type} and {target.vertex_type} "
f"has invalid handles"
)
def __setstate__(self, state): def __setstate__(self, state):
self.source_id = state["source_id"] self.source_id = state["source_id"]
@ -75,7 +88,11 @@ class Edge:
# Both lists contain strings and sometimes a string contains the value we are # Both lists contain strings and sometimes a string contains the value we are
# looking for e.g. comgin_out=["Chain"] and target_reqs=["LLMChain"] # looking for e.g. comgin_out=["Chain"] and target_reqs=["LLMChain"]
# so we need to check if any of the strings in source_types is in target_reqs # so we need to check if any of the strings in source_types is in target_reqs
self.valid = any(output in target_req for output in self.source_types for target_req in self.target_reqs) self.valid = any(
output in target_req
for output in self.source_types
for target_req in self.target_reqs
)
# Get what type of input the target node is expecting # Get what type of input the target node is expecting
self.matched_type = next( self.matched_type = next(
@ -86,7 +103,10 @@ class Edge:
if no_matched_type: if no_matched_type:
logger.debug(self.source_types) logger.debug(self.source_types)
logger.debug(self.target_reqs) logger.debug(self.target_reqs)
raise ValueError(f"Edge between {source.vertex_type} and {target.vertex_type} " f"has no matched type") raise ValueError(
f"Edge between {source.vertex_type} and {target.vertex_type} "
f"has no matched type"
)
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
@ -98,7 +118,11 @@ class Edge:
return hash(self.__repr__()) return hash(self.__repr__())
def __eq__(self, __value: object) -> bool: def __eq__(self, __value: object) -> bool:
return self.__repr__() == __value.__repr__() if isinstance(__value, Edge) else False return (
self.__repr__() == __value.__repr__()
if isinstance(__value, Edge)
else False
)
class ContractEdge(Edge): class ContractEdge(Edge):
@ -137,14 +161,15 @@ class ContractEdge(Edge):
log_transaction(self, source, target, "success") log_transaction(self, source, target, "success")
# If the target vertex is a power component we log messages # If the target vertex is a power component we log messages
if target.vertex_type == "ChatOutput" and ( if target.vertex_type == "ChatOutput" and (
isinstance(target.params.get("message"), str) or isinstance(target.params.get("message"), dict) isinstance(target.params.get(INPUT_FIELD_NAME), str)
or isinstance(target.params.get(INPUT_FIELD_NAME), dict)
): ):
if target.params.get("message") == "": if target.params.get("message") == "":
return self.result return self.result
await log_message( await log_message(
sender=target.params.get("sender", ""), sender=target.params.get("sender", ""),
sender_name=target.params.get("sender_name", ""), sender_name=target.params.get("sender_name", ""),
message=target.params.get("message", {}), message=target.params.get(INPUT_FIELD_NAME, {}),
session_id=target.params.get("session_id", ""), session_id=target.params.get("session_id", ""),
artifacts=target.artifacts, artifacts=target.artifacts,
) )
@ -154,7 +179,9 @@ class ContractEdge(Edge):
return f"{self.source_id} -[{self.target_param}]-> {self.target_id}" return f"{self.source_id} -[{self.target_param}]-> {self.target_id}"
def log_transaction(edge: ContractEdge, source: "Vertex", target: "Vertex", status, error=None): def log_transaction(
edge: ContractEdge, source: "Vertex", target: "Vertex", status, error=None
):
try: try:
monitor_service = get_monitor_service() monitor_service = get_monitor_service()
clean_params = build_clean_params(target) clean_params = build_clean_params(target)

View file

@ -8,7 +8,7 @@ from loguru import logger
from langflow.graph.edge.base import ContractEdge from langflow.graph.edge.base import ContractEdge
from langflow.graph.graph.constants import lazy_load_vertex_dict from langflow.graph.graph.constants import lazy_load_vertex_dict
from langflow.graph.graph.utils import process_flow from langflow.graph.graph.utils import process_flow
from langflow.graph.schema import InterfaceComponentTypes from langflow.graph.schema import INPUT_FIELD_NAME, InterfaceComponentTypes
from langflow.graph.vertex.base import Vertex from langflow.graph.vertex.base import Vertex
from langflow.graph.vertex.types import ( from langflow.graph.vertex.types import (
ChatVertex, ChatVertex,
@ -33,8 +33,6 @@ class Graph:
edges: List[Dict[str, str]], edges: List[Dict[str, str]],
flow_id: Optional[str] = None, flow_id: Optional[str] = None,
) -> None: ) -> None:
self.inputs = []
self.outputs = []
self._vertices = nodes self._vertices = nodes
self._edges = edges self._edges = edges
self.raw_graph_data = {"nodes": nodes, "edges": edges} self.raw_graph_data = {"nodes": nodes, "edges": edges}
@ -77,7 +75,7 @@ class Graph:
async def _run(self, inputs: Dict[str, str]) -> List["ResultData"]: async def _run(self, inputs: Dict[str, str]) -> List["ResultData"]:
"""Runs the graph with the given inputs.""" """Runs the graph with the given inputs."""
for vertex_id in self.inputs: for vertex_id in self._is_input_vertices:
vertex = self.get_vertex(vertex_id) vertex = self.get_vertex(vertex_id)
if vertex is None: if vertex is None:
raise ValueError(f"Vertex {vertex_id} not found") raise ValueError(f"Vertex {vertex_id} not found")
@ -89,7 +87,7 @@ class Graph:
logger.exception(exc) logger.exception(exc)
raise ValueError(f"Error running graph: {exc}") from exc raise ValueError(f"Error running graph: {exc}") from exc
outputs = [] outputs = []
for vertex_id in self.outputs: for vertex_id in self._is_output_vertices:
vertex = self.get_vertex(vertex_id) vertex = self.get_vertex(vertex_id)
if vertex is None: if vertex is None:
raise ValueError(f"Vertex {vertex_id} not found") raise ValueError(f"Vertex {vertex_id} not found")
@ -104,11 +102,11 @@ class Graph:
# of the vertices that are inputs # of the vertices that are inputs
# if the value is a list, we need to run multiple times # if the value is a list, we need to run multiple times
outputs = [] outputs = []
inputs_values = inputs.get("input_value") inputs_values = inputs.get(INPUT_FIELD_NAME)
if not isinstance(inputs_values, list): if not isinstance(inputs_values, list):
inputs_values = [inputs_values] inputs_values = [inputs_values]
for input_value in inputs_values: for input_value in inputs_values:
run_outputs = await self._run({"input_value": input_value}) run_outputs = await self._run({INPUT_FIELD_NAME: input_value})
logger.debug(f"Run outputs: {run_outputs}") logger.debug(f"Run outputs: {run_outputs}")
outputs.extend(run_outputs) outputs.extend(run_outputs)
return outputs return outputs
@ -317,28 +315,6 @@ class Graph:
# Now that we have the vertices and edges # Now that we have the vertices and edges
# We need to map the vertices that are connected to # We need to map the vertices that are connected to
# to ChatVertex instances # to ChatVertex instances
self._map_chat_vertices()
def _map_chat_vertices(self) -> None:
"""Maps the vertices that are connected to ChatVertex instances."""
# For each edge, we need to check if the source or target vertex is a ChatVertex
# If it is, we need to update the other vertex `is_external` attribute
# and store the id of the ChatVertex in the attributes self.inputs and self.outputs
for edge in self.edges:
source_vertex = self.get_vertex(edge.source_id)
target_vertex = self.get_vertex(edge.target_id)
if isinstance(source_vertex, ChatVertex):
# The source vertex is a ChatVertex
# thus the target vertex is an external vertex
# and the source vertex is an input
target_vertex.has_external_input = True
self.inputs.append(source_vertex.id)
if isinstance(target_vertex, ChatVertex):
# The target vertex is a ChatVertex
# thus the source vertex is an external vertex
# and the target vertex is an output
source_vertex.has_external_output = True
self.outputs.append(target_vertex.id)
def remove_vertex(self, vertex_id: str) -> None: def remove_vertex(self, vertex_id: str) -> None:
"""Removes a vertex from the graph.""" """Removes a vertex from the graph."""
@ -443,13 +419,15 @@ class Graph:
async def _execute_tasks(self, tasks): async def _execute_tasks(self, tasks):
"""Executes tasks in parallel, handling exceptions for each task.""" """Executes tasks in parallel, handling exceptions for each task."""
results = [] results = []
for task in asyncio.as_completed(tasks): for i, task in enumerate(asyncio.as_completed(tasks)):
try: try:
result = await task result = await task
results.append(result) results.append(result)
except Exception as e: except Exception as e:
# Log the exception along with the task name for easier debugging # Log the exception along with the task name for easier debugging
task_name = task.get_name() # task_name = task.get_name()
# coroutine has not attribute get_name
task_name = tasks[i].get_name()
logger.error(f"Task {task_name} failed with exception: {e}") logger.error(f"Task {task_name} failed with exception: {e}")
return results return results

View file

@ -35,3 +35,5 @@ OUTPUT_COMPONENTS = [
InterfaceComponentTypes.ChatOutput, InterfaceComponentTypes.ChatOutput,
InterfaceComponentTypes.TextOutput, InterfaceComponentTypes.TextOutput,
] ]
INPUT_FIELD_NAME = "input_value"

View file

@ -44,7 +44,7 @@ class Vertex:
) -> None: ) -> None:
# is_external means that the Vertex send or receives data from # is_external means that the Vertex send or receives data from
# an external source (e.g the chat) # an external source (e.g the chat)
self.updated_raw_params = False
self.id: str = data["id"] self.id: str = data["id"]
self.is_input = any( self.is_input = any(
input_component_name in self.id for input_component_name in INPUT_COMPONENTS input_component_name in self.id for input_component_name in INPUT_COMPONENTS
@ -285,6 +285,10 @@ class Vertex:
if self.graph is None: if self.graph is None:
raise ValueError("Graph not found") raise ValueError("Graph not found")
if self.updated_raw_params:
self.updated_raw_params = False
return
template_dict = { template_dict = {
key: value key: value
for key, value in self.data["node"]["template"].items() for key, value in self.data["node"]["template"].items()
@ -386,10 +390,11 @@ class Vertex:
Raises: Raises:
ValueError: If any key in new_params is not found in self._raw_params. ValueError: If any key in new_params is not found in self._raw_params.
""" """
for key in new_params: # First check if the input_value in _raw_params is not a vertex
if key not in self._raw_params: if any(isinstance(self._raw_params.get(key), Vertex) for key in new_params):
raise ValueError(f"Key {key} not found in raw params") return
self._raw_params.update(new_params) self._raw_params.update(new_params)
self.updated_raw_params = True
async def _build(self, user_id=None): async def _build(self, user_id=None):
""" """
@ -451,6 +456,8 @@ class Vertex:
await self._build_node_and_update_params(key, value, user_id) await self._build_node_and_update_params(key, value, user_id)
elif isinstance(value, list) and self._is_list_of_nodes(value): elif isinstance(value, list) and self._is_list_of_nodes(value):
await self._build_list_of_nodes_and_update_params(key, value, user_id) await self._build_list_of_nodes_and_update_params(key, value, user_id)
elif key not in self.params:
self.params[key] = value
def _is_node(self, value): def _is_node(self, value):
""" """
@ -586,7 +593,7 @@ class Vertex:
logger.warning(message) logger.warning(message)
def _reset(self): def _reset(self, params_update: Optional[Dict[str, Any]] = None):
self._built = False self._built = False
self._built_object = UnbuiltObject() self._built_object = UnbuiltObject()
self._built_result = UnbuiltResult() self._built_result = UnbuiltResult()

View file

@ -1,11 +1,12 @@
import ast import ast
import json import json
from typing import (AsyncIterator, Callable, Dict, Iterator, List, Optional, from typing import AsyncIterator, Callable, Dict, Iterator, List, Optional, Union
Union)
import yaml import yaml
from langchain_core.messages import AIMessage from langchain_core.messages import AIMessage
from loguru import logger
from langflow.graph.schema import INPUT_FIELD_NAME
from langflow.graph.utils import UnbuiltObject, flatten_list from langflow.graph.utils import UnbuiltObject, flatten_list
from langflow.graph.vertex.base import StatefulVertex, StatelessVertex from langflow.graph.vertex.base import StatefulVertex, StatelessVertex
from langflow.interface.utils import extract_input_variables_from_prompt from langflow.interface.utils import extract_input_variables_from_prompt
@ -362,7 +363,7 @@ class ChatVertex(StatelessVertex):
artifacts = None artifacts = None
sender = self.params.get("sender", None) sender = self.params.get("sender", None)
sender_name = self.params.get("sender_name", None) sender_name = self.params.get("sender_name", None)
message = self.params.get("message", None) message = self.params.get(INPUT_FIELD_NAME, None)
stream_url = None stream_url = None
if isinstance(self._built_object, AIMessage): if isinstance(self._built_object, AIMessage):
artifacts = ChatOutputResponse.from_message( artifacts = ChatOutputResponse.from_message(
@ -396,7 +397,7 @@ class ChatVertex(StatelessVertex):
if artifacts: if artifacts:
self.artifacts = artifacts.model_dump() self.artifacts = artifacts.model_dump()
if isinstance(self._built_object, (AsyncIterator, Iterator)): if isinstance(self._built_object, (AsyncIterator, Iterator)):
if self.params["as_record"]: if self.params["return_record"]:
self._built_object = Record(text=message, data=self.artifacts) self._built_object = Record(text=message, data=self.artifacts)
else: else:
self._built_object = message self._built_object = message
@ -406,7 +407,7 @@ class ChatVertex(StatelessVertex):
await super()._run(*args, **kwargs) await super()._run(*args, **kwargs)
async def stream(self): async def stream(self):
iterator = self.params.get("message", None) iterator = self.params.get(INPUT_FIELD_NAME, None)
if not isinstance(iterator, (AsyncIterator, Iterator)): if not isinstance(iterator, (AsyncIterator, Iterator)):
raise ValueError("The message must be an iterator or an async iterator.") raise ValueError("The message must be an iterator or an async iterator.")
is_async = isinstance(iterator, AsyncIterator) is_async = isinstance(iterator, AsyncIterator)
@ -423,15 +424,17 @@ class ChatVertex(StatelessVertex):
message = message.text if hasattr(message, "text") else message message = message.text if hasattr(message, "text") else message
yield message yield message
complete_message += message complete_message += message
self._built_object = Record(text=complete_message, data=self.artifacts)
self._built_result = complete_message
# Update artifacts with the message
# and remove the stream_url
self.artifacts = ChatOutputResponse( self.artifacts = ChatOutputResponse(
message=complete_message, message=complete_message,
sender=self.params.get("sender", ""), sender=self.params.get("sender", ""),
sender_name=self.params.get("sender_name", ""), sender_name=self.params.get("sender_name", ""),
).model_dump() ).model_dump()
self.params[INPUT_FIELD_NAME] = complete_message
self._built_object = Record(text=complete_message, data=self.artifacts)
self._built_result = complete_message
# Update artifacts with the message
# and remove the stream_url
logger.debug(f"Streamed message: {complete_message}")
await log_message( await log_message(
sender=self.params.get("sender", ""), sender=self.params.get("sender", ""),
@ -441,6 +444,9 @@ class ChatVertex(StatelessVertex):
artifacts=self.artifacts, artifacts=self.artifacts,
) )
self._validate_built_object()
self._built = True
class RoutingVertex(StatelessVertex): class RoutingVertex(StatelessVertex):
def __init__(self, data: Dict, graph): def __init__(self, data: Dict, graph):

View file

@ -6,6 +6,9 @@ from typing import Any, Dict, List, Type, Union
from cachetools import TTLCache, cachedmethod, keys from cachetools import TTLCache, cachedmethod, keys
from fastapi import HTTPException from fastapi import HTTPException
from loguru import logger
from langflow.interface.custom.eval import eval_custom_component_code
from langflow.interface.custom.schema import CallableCodeDetails, ClassCodeDetails from langflow.interface.custom.schema import CallableCodeDetails, ClassCodeDetails
@ -92,7 +95,9 @@ class CodeParser:
elif isinstance(node, ast.ImportFrom): elif isinstance(node, ast.ImportFrom):
for alias in node.names: for alias in node.names:
if alias.asname: if alias.asname:
self.data["imports"].append((node.module, f"{alias.name} as {alias.asname}")) self.data["imports"].append(
(node.module, f"{alias.name} as {alias.asname}")
)
else: else:
self.data["imports"].append((node.module, alias.name)) self.data["imports"].append((node.module, alias.name))
@ -141,7 +146,9 @@ class CodeParser:
return_type = None return_type = None
if node.returns: if node.returns:
return_type_str = ast.unparse(node.returns) return_type_str = ast.unparse(node.returns)
eval_env = self.construct_eval_env(return_type_str, tuple(self.data["imports"])) eval_env = self.construct_eval_env(
return_type_str, tuple(self.data["imports"])
)
try: try:
return_type = eval(return_type_str, eval_env) return_type = eval(return_type_str, eval_env)
@ -183,14 +190,22 @@ class CodeParser:
num_defaults = len(node.args.defaults) num_defaults = len(node.args.defaults)
num_missing_defaults = num_args - num_defaults num_missing_defaults = num_args - num_defaults
missing_defaults = [None] * num_missing_defaults missing_defaults = [None] * num_missing_defaults
default_values = [ast.unparse(default).strip("'") if default else None for default in node.args.defaults] default_values = [
ast.unparse(default).strip("'") if default else None
for default in node.args.defaults
]
# Now check all default values to see if there # Now check all default values to see if there
# are any "None" values in the middle # are any "None" values in the middle
default_values = [None if value == "None" else value for value in default_values] default_values = [
None if value == "None" else value for value in default_values
]
defaults = missing_defaults + default_values defaults = missing_defaults + default_values
args = [self.parse_arg(arg, default) for arg, default in zip(node.args.args, defaults)] args = [
self.parse_arg(arg, default)
for arg, default in zip(node.args.args, defaults)
]
return args return args
def parse_varargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]: def parse_varargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
@ -208,11 +223,17 @@ class CodeParser:
""" """
Parses the keyword-only arguments of a function or method node. Parses the keyword-only arguments of a function or method node.
""" """
kw_defaults = [None] * (len(node.args.kwonlyargs) - len(node.args.kw_defaults)) + [ kw_defaults = [None] * (
ast.unparse(default) if default else None for default in node.args.kw_defaults len(node.args.kwonlyargs) - len(node.args.kw_defaults)
) + [
ast.unparse(default) if default else None
for default in node.args.kw_defaults
] ]
args = [self.parse_arg(arg, default) for arg, default in zip(node.args.kwonlyargs, kw_defaults)] args = [
self.parse_arg(arg, default)
for arg, default in zip(node.args.kwonlyargs, kw_defaults)
]
return args return args
def parse_kwargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]: def parse_kwargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
@ -268,15 +289,28 @@ class CodeParser:
method = self.parse_callable_details(stmt) method = self.parse_callable_details(stmt)
return (method, True) if stmt.name == "__init__" else (method, False) return (method, True) if stmt.name == "__init__" else (method, False)
def get_base_classes(self):
"""
Returns the base classes of the custom component class.
"""
try:
bases = self.execute_and_inspect_classes(self.code)
except Exception as e:
# If the code cannot be executed, return an empty list
logger.exception(e)
bases = []
raise e
return bases
def parse_classes(self, node: ast.ClassDef) -> None: def parse_classes(self, node: ast.ClassDef) -> None:
""" """
Extracts "classes" from the code, including inheritance and init methods. Extracts "classes" from the code, including inheritance and init methods.
""" """
bases = self.get_base_classes() or [ast.unparse(b) for b in node.bases]
class_details = ClassCodeDetails( class_details = ClassCodeDetails(
name=node.name, name=node.name,
doc=ast.get_docstring(node), doc=ast.get_docstring(node),
bases=[ast.unparse(base) for base in node.bases], bases=bases,
attributes=[], attributes=[],
methods=[], methods=[],
init=None, init=None,
@ -303,11 +337,25 @@ class CodeParser:
Extracts global variables from the code. Extracts global variables from the code.
""" """
global_var = { global_var = {
"targets": [t.id if hasattr(t, "id") else ast.dump(t) for t in node.targets], "targets": [
t.id if hasattr(t, "id") else ast.dump(t) for t in node.targets
],
"value": ast.unparse(node.value), "value": ast.unparse(node.value),
} }
self.data["global_vars"].append(global_var) self.data["global_vars"].append(global_var)
def execute_and_inspect_classes(self, code: str):
custom_component_class = eval_custom_component_code(code)
custom_component = custom_component_class()
dunder_class = custom_component.__class__
# Get the base classes at two levels of inheritance
bases = []
for base in dunder_class.__bases__:
bases.append(base.__name__)
for bases_base in base.__bases__:
bases.append(bases_base.__name__)
return bases
def parse_code(self) -> Dict[str, Any]: def parse_code(self) -> Dict[str, Any]:
""" """
Runs all parsing operations and returns the resulting data. Runs all parsing operations and returns the resulting data.

View file

@ -27,14 +27,18 @@ from langflow.utils import validate
from langflow.utils.util import get_base_classes from langflow.utils.util import get_base_classes
def add_output_types(frontend_node: CustomComponentFrontendNode, return_types: List[str]): def add_output_types(
frontend_node: CustomComponentFrontendNode, return_types: List[str]
):
"""Add output types to the frontend node""" """Add output types to the frontend node"""
for return_type in return_types: for return_type in return_types:
if return_type is None: if return_type is None:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail={ detail={
"error": ("Invalid return type. Please check your code and try again."), "error": (
"Invalid return type. Please check your code and try again."
),
"traceback": traceback.format_exc(), "traceback": traceback.format_exc(),
}, },
) )
@ -63,14 +67,18 @@ def reorder_fields(frontend_node: CustomComponentFrontendNode, field_order: List
frontend_node.template.fields = reordered_fields frontend_node.template.fields = reordered_fields
def add_base_classes(frontend_node: CustomComponentFrontendNode, return_types: List[str]): def add_base_classes(
frontend_node: CustomComponentFrontendNode, return_types: List[str]
):
"""Add base classes to the frontend node""" """Add base classes to the frontend node"""
for return_type_instance in return_types: for return_type_instance in return_types:
if return_type_instance is None: if return_type_instance is None:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail={ detail={
"error": ("Invalid return type. Please check your code and try again."), "error": (
"Invalid return type. Please check your code and try again."
),
"traceback": traceback.format_exc(), "traceback": traceback.format_exc(),
}, },
) )
@ -145,10 +153,14 @@ def add_new_custom_field(
# If options is a list, then it's a dropdown # If options is a list, then it's a dropdown
# If options is None, then it's a list of strings # If options is None, then it's a list of strings
is_list = isinstance(field_config.get("options"), list) is_list = isinstance(field_config.get("options"), list)
field_config["is_list"] = is_list or field_config.get("is_list", False) or field_contains_list field_config["is_list"] = (
is_list or field_config.get("is_list", False) or field_contains_list
)
if "name" in field_config: if "name" in field_config:
warnings.warn("The 'name' key in field_config is used to build the object and can't be changed.") warnings.warn(
"The 'name' key in field_config is used to build the object and can't be changed."
)
required = field_config.pop("required", field_required) required = field_config.pop("required", field_required)
placeholder = field_config.pop("placeholder", "") placeholder = field_config.pop("placeholder", "")
@ -179,7 +191,9 @@ def add_extra_fields(frontend_node, field_config, function_args):
if "name" not in extra_field or extra_field["name"] == "self": if "name" not in extra_field or extra_field["name"] == "self":
continue continue
field_name, field_type, field_value, field_required = get_field_properties(extra_field) field_name, field_type, field_value, field_required = get_field_properties(
extra_field
)
config = field_config.get(field_name, {}) config = field_config.get(field_name, {})
frontend_node = add_new_custom_field( frontend_node = add_new_custom_field(
frontend_node, frontend_node,
@ -217,7 +231,9 @@ def run_build_config(
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail={ detail={
"error": ("Invalid type convertion. Please check your code and try again."), "error": (
"Invalid type convertion. Please check your code and try again."
),
"traceback": traceback.format_exc(), "traceback": traceback.format_exc(),
}, },
) from exc ) from exc
@ -245,7 +261,9 @@ def run_build_config(
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail={ detail={
"error": ("Invalid type convertion. Please check your code and try again."), "error": (
"Invalid type convertion. Please check your code and try again."
),
"traceback": traceback.format_exc(), "traceback": traceback.format_exc(),
}, },
) from exc ) from exc
@ -300,16 +318,24 @@ def build_custom_component_template(
frontend_node = build_frontend_node(custom_component.template_config) frontend_node = build_frontend_node(custom_component.template_config)
logger.debug("Updated attributes") logger.debug("Updated attributes")
field_config, custom_instance = run_build_config(custom_component, user_id=user_id, update_field=update_field) field_config, custom_instance = run_build_config(
custom_component, user_id=user_id, update_field=update_field
)
logger.debug("Built field config") logger.debug("Built field config")
entrypoint_args = custom_component.get_function_entrypoint_args entrypoint_args = custom_component.get_function_entrypoint_args
add_extra_fields(frontend_node, field_config, entrypoint_args) add_extra_fields(frontend_node, field_config, entrypoint_args)
frontend_node = add_code_field(frontend_node, custom_component.code, field_config.get("code", {})) frontend_node = add_code_field(
frontend_node, custom_component.code, field_config.get("code", {})
)
add_base_classes(frontend_node, custom_component.get_function_entrypoint_return_type) add_base_classes(
add_output_types(frontend_node, custom_component.get_function_entrypoint_return_type) frontend_node, custom_component.get_function_entrypoint_return_type
)
add_output_types(
frontend_node, custom_component.get_function_entrypoint_return_type
)
logger.debug("Added base classes") logger.debug("Added base classes")
reorder_fields(frontend_node, custom_instance._get_field_order()) reorder_fields(frontend_node, custom_instance._get_field_order())
@ -321,7 +347,9 @@ def build_custom_component_template(
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail={ detail={
"error": ("Invalid type convertion. Please check your code and try again."), "error": (
"Invalid type convertion. Please check your code and try again."
),
"traceback": traceback.format_exc(), "traceback": traceback.format_exc(),
}, },
) from exc ) from exc
@ -345,7 +373,9 @@ def build_custom_components(settings_service):
if not settings_service.settings.COMPONENTS_PATH: if not settings_service.settings.COMPONENTS_PATH:
return {} return {}
logger.info(f"Building custom components from {settings_service.settings.COMPONENTS_PATH}") logger.info(
f"Building custom components from {settings_service.settings.COMPONENTS_PATH}"
)
custom_components_from_file = {} custom_components_from_file = {}
processed_paths = set() processed_paths = set()
for path in settings_service.settings.COMPONENTS_PATH: for path in settings_service.settings.COMPONENTS_PATH:
@ -356,7 +386,9 @@ def build_custom_components(settings_service):
custom_component_dict = build_custom_component_list_from_path(path_str) custom_component_dict = build_custom_component_list_from_path(path_str)
if custom_component_dict: if custom_component_dict:
category = next(iter(custom_component_dict)) category = next(iter(custom_component_dict))
logger.info(f"Loading {len(custom_component_dict[category])} component(s) from category {category}") logger.info(
f"Loading {len(custom_component_dict[category])} component(s) from category {category}"
)
custom_components_from_file = merge_nested_dicts_with_renaming( custom_components_from_file = merge_nested_dicts_with_renaming(
custom_components_from_file, custom_component_dict custom_components_from_file, custom_component_dict
) )
@ -400,8 +432,9 @@ def sanitize_field_config(field_config: Dict):
def build_component(component): def build_component(component):
"""Build a single component.""" """Build a single component."""
component_name = determine_component_name(component) component_name = determine_component_name(component)
logger.debug(f"Building component: {component_name}")
component_template = create_component_template(component) component_template = create_component_template(component)
logger.debug(f"Building component: {component_name, component.get('output_types')}")
return component_name, component_template return component_name, component_template

View file

@ -48,27 +48,28 @@ class ChatComponent(CustomComponent):
raise ValueError( raise ValueError(
"All of session_id, sender, and sender_name must be provided." "All of session_id, sender, and sender_name must be provided."
) )
if isinstance(message, Record):
if not record: record = message
record = [] record.data.update(
if not session_id or not sender or not sender_name: {
raise ValueError "session_id": session_id,
for text in text: "sender": sender,
record = Record( "sender_name": sender_name,
text=text, }
data={ )
"session_id": session_id, else:
"sender": sender, record = Record(
"sender_name": sender_name, text=message,
}, data={
) "session_id": session_id,
record.append(record) "sender": sender,
elif isinstance(record, Record): "sender_name": sender_name,
record = [record] },
)
self.status = record self.status = record
record = add_messages(record) records = add_messages([record])
return record return records[0]
def build( def build(
self, self,

View file

@ -269,6 +269,7 @@ async def build_graph_and_generate_result(
async def run_graph( async def run_graph(
graph: Union["Graph", dict], graph: Union["Graph", dict],
flow_id: str,
session_id: str, session_id: str,
inputs: Optional[Union[dict, List[dict]]] = None, inputs: Optional[Union[dict, List[dict]]] = None,
artifacts: Optional[Dict[str, Any]] = None, artifacts: Optional[Dict[str, Any]] = None,
@ -276,7 +277,8 @@ async def run_graph(
): ):
"""Run the graph and generate the result""" """Run the graph and generate the result"""
if isinstance(graph, dict): if isinstance(graph, dict):
graph = Graph.from_payload(graph) graph = Graph.from_payload(graph, flow_id=flow_id)
outputs = await graph.run(inputs) outputs = await graph.run(inputs)
if session_id and session_service: if session_id and session_service:
session_service.update_session(session_id, (graph, artifacts)) session_service.update_session(session_id, (graph, artifacts))

View file

@ -25,7 +25,10 @@ def patching(record):
def configure(log_level: Optional[str] = None, log_file: Optional[Path] = None): def configure(log_level: Optional[str] = None, log_file: Optional[Path] = None):
if os.getenv("LANGFLOW_LOG_LEVEL") in VALID_LOG_LEVELS and log_level is None: if (
os.getenv("LANGFLOW_LOG_LEVEL", "").upper() in VALID_LOG_LEVELS
and log_level is None
):
log_level = os.getenv("LANGFLOW_LOG_LEVEL") log_level = os.getenv("LANGFLOW_LOG_LEVEL")
if log_level is None: if log_level is None:
log_level = "INFO" log_level = "INFO"

View file

@ -45,7 +45,9 @@ def validate_code(code):
# Evaluate the function definition # Evaluate the function definition
for node in tree.body: for node in tree.body:
if isinstance(node, ast.FunctionDef): if isinstance(node, ast.FunctionDef):
code_obj = compile(ast.Module(body=[node], type_ignores=[]), "<string>", "exec") code_obj = compile(
ast.Module(body=[node], type_ignores=[]), "<string>", "exec"
)
try: try:
exec(code_obj) exec(code_obj)
except Exception as e: except Exception as e:
@ -89,15 +91,23 @@ def execute_function(code, function_name, *args, **kwargs):
exec_globals, exec_globals,
locals(), locals(),
) )
exec_globals[alias.asname or alias.name] = importlib.import_module(alias.name) exec_globals[alias.asname or alias.name] = importlib.import_module(
alias.name
)
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
raise ModuleNotFoundError(f"Module {alias.name} not found. Please install it and try again.") from e raise ModuleNotFoundError(
f"Module {alias.name} not found. Please install it and try again."
) from e
function_code = next( function_code = next(
node for node in module.body if isinstance(node, ast.FunctionDef) and node.name == function_name node
for node in module.body
if isinstance(node, ast.FunctionDef) and node.name == function_name
) )
function_code.parent = None function_code.parent = None
code_obj = compile(ast.Module(body=[function_code], type_ignores=[]), "<string>", "exec") code_obj = compile(
ast.Module(body=[function_code], type_ignores=[]), "<string>", "exec"
)
try: try:
exec(code_obj, exec_globals, locals()) exec(code_obj, exec_globals, locals())
except Exception as exc: except Exception as exc:
@ -124,15 +134,23 @@ def create_function(code, function_name):
if isinstance(node, ast.Import): if isinstance(node, ast.Import):
for alias in node.names: for alias in node.names:
try: try:
exec_globals[alias.asname or alias.name] = importlib.import_module(alias.name) exec_globals[alias.asname or alias.name] = importlib.import_module(
alias.name
)
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
raise ModuleNotFoundError(f"Module {alias.name} not found. Please install it and try again.") from e raise ModuleNotFoundError(
f"Module {alias.name} not found. Please install it and try again."
) from e
function_code = next( function_code = next(
node for node in module.body if isinstance(node, ast.FunctionDef) and node.name == function_name node
for node in module.body
if isinstance(node, ast.FunctionDef) and node.name == function_name
) )
function_code.parent = None function_code.parent = None
code_obj = compile(ast.Module(body=[function_code], type_ignores=[]), "<string>", "exec") code_obj = compile(
ast.Module(body=[function_code], type_ignores=[]), "<string>", "exec"
)
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
exec(code_obj, exec_globals, locals()) exec(code_obj, exec_globals, locals())
exec_globals[function_name] = locals()[function_name] exec_globals[function_name] = locals()[function_name]
@ -194,9 +212,13 @@ def prepare_global_scope(code, module):
if isinstance(node, ast.Import): if isinstance(node, ast.Import):
for alias in node.names: for alias in node.names:
try: try:
exec_globals[alias.asname or alias.name] = importlib.import_module(alias.name) exec_globals[alias.asname or alias.name] = importlib.import_module(
alias.name
)
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
raise ModuleNotFoundError(f"Module {alias.name} not found. Please install it and try again.") from e raise ModuleNotFoundError(
f"Module {alias.name} not found. Please install it and try again."
) from e
elif isinstance(node, ast.ImportFrom) and node.module is not None: elif isinstance(node, ast.ImportFrom) and node.module is not None:
try: try:
imported_module = importlib.import_module(node.module) imported_module = importlib.import_module(node.module)
@ -217,7 +239,11 @@ def extract_class_code(module, class_name):
:param class_name: Name of the class to extract :param class_name: Name of the class to extract
:return: AST node of the specified class :return: AST node of the specified class
""" """
class_code = next(node for node in module.body if isinstance(node, ast.ClassDef) and node.name == class_name) class_code = next(
node
for node in module.body
if isinstance(node, ast.ClassDef) and node.name == class_name
)
class_code.parent = None class_code.parent = None
return class_code return class_code
@ -230,7 +256,9 @@ def compile_class_code(class_code):
:param class_code: AST node of the class :param class_code: AST node of the class
:return: Compiled code object of the class :return: Compiled code object of the class
""" """
code_obj = compile(ast.Module(body=[class_code], type_ignores=[]), "<string>", "exec") code_obj = compile(
ast.Module(body=[class_code], type_ignores=[]), "<string>", "exec"
)
return code_obj return code_obj
@ -274,7 +302,9 @@ def get_default_imports(code_string):
langflow_imports = list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys()) langflow_imports = list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys())
necessary_imports = find_names_in_code(code_string, langflow_imports) necessary_imports = find_names_in_code(code_string, langflow_imports)
langflow_module = importlib.import_module("langflow.field_typing") langflow_module = importlib.import_module("langflow.field_typing")
default_imports.update({name: getattr(langflow_module, name) for name in necessary_imports}) default_imports.update(
{name: getattr(langflow_module, name) for name in necessary_imports}
)
return default_imports return default_imports

View file

@ -1,5 +1,5 @@
import Convert from "ansi-to-html"; import Convert from "ansi-to-html";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState, useRef } from "react";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import rehypeMathjax from "rehype-mathjax"; import rehypeMathjax from "rehype-mathjax";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
@ -12,12 +12,14 @@ import IconComponent from "../../../components/genericIconComponent";
import { chatMessagePropsType } from "../../../types/components"; import { chatMessagePropsType } from "../../../types/components";
import { classNames } from "../../../utils/utils"; import { classNames } from "../../../utils/utils";
import FileCard from "../fileComponent"; import FileCard from "../fileComponent";
import useFlowStore from "../../../stores/flowStore";
export default function ChatMessage({ export default function ChatMessage({
chat, chat,
lockChat, lockChat,
lastMessage, lastMessage,
updateChat, updateChat,
setLockChat
}: chatMessagePropsType): JSX.Element { }: chatMessagePropsType): JSX.Element {
const convert = new Convert({ newline: true }); const convert = new Convert({ newline: true });
const [hidden, setHidden] = useState(true); const [hidden, setHidden] = useState(true);
@ -29,6 +31,16 @@ export default function ChatMessage({
const chatMessageString = chat.message ? chat.message.toString() : ""; const chatMessageString = chat.message ? chat.message.toString() : "";
const [chatMessage, setChatMessage] = useState(chatMessageString); const [chatMessage, setChatMessage] = useState(chatMessageString);
const [isStreaming, setIsStreaming] = useState(false); const [isStreaming, setIsStreaming] = useState(false);
const eventSource = useRef<EventSource | undefined>(undefined);
const updateFlowPool = useFlowStore((state) => state.updateFlowPool);
const chatMessageRef = useRef(chatMessage);
// Sync ref with state
useEffect(() => {
chatMessageRef.current = chatMessage;
}, [chatMessage]);
// The idea now is that chat.stream_url MAY be a URL if we should stream the output of the chat // The idea now is that chat.stream_url MAY be a URL if we should stream the output of the chat
// probably the message is empty when we have a stream_url // probably the message is empty when we have a stream_url
@ -36,49 +48,52 @@ export default function ChatMessage({
const streamChunks = (url: string) => { const streamChunks = (url: string) => {
setIsStreaming(true); // Streaming starts setIsStreaming(true); // Streaming starts
return new Promise<boolean>((resolve, reject) => { return new Promise<boolean>((resolve, reject) => {
const eventSource = new EventSource(url); eventSource.current = new EventSource(url);
eventSource.onmessage = (event) => { eventSource.current.onmessage = (event) => {
let parsedData = JSON.parse(event.data); let parsedData = JSON.parse(event.data);
if (parsedData.chunk) { if (parsedData.chunk) {
setChatMessage((prev) => prev + parsedData.chunk); setChatMessage((prev) => prev + parsedData.chunk);
} }
}; };
eventSource.onerror = (event) => { eventSource.current.onerror = (event) => {
setIsStreaming(false);
eventSource.current?.close();
setStreamUrl(undefined);
reject(new Error("Streaming failed")); reject(new Error("Streaming failed"));
setIsStreaming(false);
eventSource.close();
}; };
eventSource.addEventListener("close", (event) => { eventSource.current.addEventListener("close", (event) => {
setStreamUrl(null); // Update state to reflect the stream is closed setStreamUrl(undefined); // Update state to reflect the stream is closed
resolve(true); eventSource.current?.close();
setIsStreaming(false); setIsStreaming(false);
eventSource.close(); resolve(true);
}); });
}); });
}; };
useEffect(() => { useEffect(() => {
if (streamUrl && chat.message === "") { console.log("chatMessage", chatMessage);
if (streamUrl && !isStreaming) {
setLockChat(true);
streamChunks(streamUrl) streamChunks(streamUrl)
.then(() => { .then(() => {
setLockChat(false);
if (updateChat) { if (updateChat) {
updateChat(chat, chatMessage, streamUrl); updateChat(chat, chatMessageRef.current);
} }
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
setLockChat(false);
}); });
} }
}, [streamUrl]); }, [streamUrl, chatMessage]);
useEffect(() => { useEffect(() => {
// This effect is specifically for calling updateChat after streaming ends return () => {
if (!isStreaming && streamUrl) { eventSource.current?.close();
if (updateChat) {
updateChat(chat, chatMessage, streamUrl);
}
} }
}, [isStreaming]); }, [])
useEffect(() => { useEffect(() => {
const element = document.getElementById("last-chat-message"); const element = document.getElementById("last-chat-message");
@ -207,7 +222,7 @@ dark:prose-invert"
}, },
]} ]}
activeTab={"0"} activeTab={"0"}
setActiveTab={() => {}} setActiveTab={() => { }}
/> />
) : ( ) : (
<code className={className} {...props}> <code className={className} {...props}>
@ -264,33 +279,33 @@ dark:prose-invert"
<span className="prose text-primary word-break-break-word dark:prose-invert"> <span className="prose text-primary word-break-break-word dark:prose-invert">
{promptOpen {promptOpen
? template?.split("\n")?.map((line, index) => { ? template?.split("\n")?.map((line, index) => {
const regex = /{([^}]+)}/g; const regex = /{([^}]+)}/g;
let match; let match;
let parts: Array<JSX.Element | string> = []; let parts: Array<JSX.Element | string> = [];
let lastIndex = 0; let lastIndex = 0;
while ((match = regex.exec(line)) !== null) { while ((match = regex.exec(line)) !== null) {
// Push text up to the match // Push text up to the match
if (match.index !== lastIndex) { if (match.index !== lastIndex) {
parts.push(line.substring(lastIndex, match.index)); parts.push(line.substring(lastIndex, match.index));
} }
// Push div with matched text // Push div with matched text
if (chat.message[match[1]]) { if (chat.message[match[1]]) {
parts.push( parts.push(
<span className="chat-message-highlight"> <span className="chat-message-highlight">
{chat.message[match[1]]} {chat.message[match[1]]}
</span> </span>
); );
} }
// Update last index // Update last index
lastIndex = regex.lastIndex; lastIndex = regex.lastIndex;
} }
// Push text after the last match // Push text after the last match
if (lastIndex !== line.length) { if (lastIndex !== line.length) {
parts.push(line.substring(lastIndex)); parts.push(line.substring(lastIndex));
} }
return <p>{parts}</p>; return <p>{parts}</p>;
}) })
: chatMessage} : chatMessage}
</span> </span>
</> </>

View file

@ -34,6 +34,7 @@ export default function NewChatView({
const inputIds = inputs.map((obj) => obj.id); const inputIds = inputs.map((obj) => obj.id);
const outputIds = outputs.map((obj) => obj.id); const outputIds = outputs.map((obj) => obj.id);
const outputTypes = outputs.map((obj) => obj.type); const outputTypes = outputs.map((obj) => obj.type);
const updateFlowPool = useFlowStore((state)=>state.updateFlowPool)
useEffect(() => { useEffect(() => {
if (!outputTypes.includes("ChatOutput")) { if (!outputTypes.includes("ChatOutput")) {
@ -67,14 +68,12 @@ export default function NewChatView({
const { sender, message, sender_name, stream_url } = output.data const { sender, message, sender_name, stream_url } = output.data
.artifacts as ChatOutputType; .artifacts as ChatOutputType;
const componentId = output.id + index;
const is_ai = sender === "Machine" || sender === null; const is_ai = sender === "Machine" || sender === null;
return { return {
isSend: !is_ai, isSend: !is_ai,
message: message, message: message,
sender_name, sender_name,
id: componentId, componentId: output.id,
stream_url: stream_url, stream_url: stream_url,
}; };
} catch (e) { } catch (e) {
@ -83,7 +82,7 @@ export default function NewChatView({
isSend: false, isSend: false,
message: "Error parsing message", message: "Error parsing message",
sender_name: "Error", sender_name: "Error",
id: output.id + index, componentId: output.id,
}; };
} }
}); });
@ -120,27 +119,24 @@ export default function NewChatView({
function updateChat( function updateChat(
chat: ChatMessageType, chat: ChatMessageType,
message: string, message: string,
stream_url: string | null stream_url?: string
) { ) {
if (message === "") return; if (message === "") return;
console.log(`updateChat: ${message}`); chat.message = message;
console.log("chatHistory:", chatHistory);
chat.message = message;
chat.stream_url = stream_url;
// chat is one of the chatHistory // chat is one of the chatHistory
setChatHistory((oldChatHistory) => { updateFlowPool(chat.componentId,{message,sender_name:chat.sender_name??"Bot",sender:"Machine"})
const index = oldChatHistory.findIndex((ch) => ch.id === chat.id); // setChatHistory((oldChatHistory) => {
// const index = oldChatHistory.findIndex((ch) => ch.id === chat.id);
if (index === -1) return oldChatHistory; // if (index === -1) return oldChatHistory;
let newChatHistory = _.cloneDeep(oldChatHistory); // let newChatHistory = _.cloneDeep(oldChatHistory);
newChatHistory = [ // newChatHistory = [
...newChatHistory.slice(0, index), // ...newChatHistory.slice(0, index),
chat, // chat,
...newChatHistory.slice(index + 1), // ...newChatHistory.slice(index + 1),
]; // ];
console.log("newChatHistory:", newChatHistory); // console.log("newChatHistory:", newChatHistory);
return newChatHistory; // return newChatHistory;
}); // });
} }
return ( return (
@ -164,10 +160,11 @@ export default function NewChatView({
{chatHistory?.length > 0 ? ( {chatHistory?.length > 0 ? (
chatHistory.map((chat, index) => ( chatHistory.map((chat, index) => (
<ChatMessage <ChatMessage
setLockChat={setLockChat}
lockChat={lockChat} lockChat={lockChat}
chat={chat} chat={chat}
lastMessage={chatHistory.length - 1 === index ? true : false} lastMessage={chatHistory.length - 1 === index ? true : false}
key={`${chat.id}-${index}`} key={`${chat.componentId}-${index}`}
updateChat={updateChat} updateChat={updateChat}
/> />
)) ))

View file

@ -19,7 +19,7 @@ import {
sourceHandleType, sourceHandleType,
targetHandleType, targetHandleType,
} from "../types/flow"; } from "../types/flow";
import { FlowStoreType } from "../types/zustand/flow"; import { ChatOutputType, FlowPoolObjectType, FlowStoreType, chatInputType } from "../types/zustand/flow";
import { buildVertices } from "../utils/buildUtils"; import { buildVertices } from "../utils/buildUtils";
import { import {
cleanEdges, cleanEdges,
@ -51,7 +51,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
setFlowPool: (flowPool) => { setFlowPool: (flowPool) => {
set({ flowPool }); set({ flowPool });
}, },
addDataToFlowPool: (data: any, nodeId: string) => { addDataToFlowPool: (data: FlowPoolObjectType, nodeId: string) => {
let newFlowPool = cloneDeep({ ...get().flowPool }); let newFlowPool = cloneDeep({ ...get().flowPool });
if (!newFlowPool[nodeId]) newFlowPool[nodeId] = [data]; if (!newFlowPool[nodeId]) newFlowPool[nodeId] = [data];
else { else {
@ -59,6 +59,28 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
} }
get().setFlowPool(newFlowPool); get().setFlowPool(newFlowPool);
}, },
updateFlowPool:(nodeId:string,data:FlowPoolObjectType| ChatOutputType | chatInputType,buildId?:string)=>{
let newFlowPool = cloneDeep({ ...get().flowPool });
if (!newFlowPool[nodeId]){
return;
}
else {
let index = newFlowPool[nodeId].length-1;
if(buildId){
index = newFlowPool[nodeId].findIndex((flow)=>flow.id===buildId);
}
//check if the data is a flowpool object
if((data as FlowPoolObjectType).data?.artifacts!==undefined){
newFlowPool[nodeId][index] = (data as FlowPoolObjectType);
}
//update data artifact
else
{
newFlowPool[nodeId][index].data.artifacts = data;
}
}
get().setFlowPool(newFlowPool);
},
CleanFlowPool: () => { CleanFlowPool: () => {
get().setFlowPool({}); get().setFlowPool({});
}, },
@ -394,12 +416,13 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
} }
function handleBuildUpdate( function handleBuildUpdate(
vertexBuildData: VertexBuildTypeAPI, vertexBuildData: VertexBuildTypeAPI,
status: BuildStatus status: BuildStatus,
buildId:string
) { ) {
if (vertexBuildData && vertexBuildData.inactive_vertices) { if (vertexBuildData && vertexBuildData.inactive_vertices) {
get().removeFromVerticesBuild(vertexBuildData.inactive_vertices); get().removeFromVerticesBuild(vertexBuildData.inactive_vertices);
} }
get().addDataToFlowPool(vertexBuildData, vertexBuildData.id); get().addDataToFlowPool({...vertexBuildData,buildId}, vertexBuildData.id);
useFlowStore.getState().updateBuildStatus([vertexBuildData.id], status); useFlowStore.getState().updateBuildStatus([vertexBuildData.id], status);
} }
await updateFlowInDatabase({ await updateFlowInDatabase({

View file

@ -9,7 +9,7 @@ export type ChatMessageType = {
files?: Array<{ data: string; type: string; data_type: string }>; files?: Array<{ data: string; type: string; data_type: string }>;
prompt?: string; prompt?: string;
chatKey?: string; chatKey?: string;
id?: string; componentId: string;
stream_url?: string | null; stream_url?: string | null;
sender_name?: string; sender_name?: string;
}; };

View file

@ -524,10 +524,11 @@ export type chatMessagePropsType = {
chat: ChatMessageType; chat: ChatMessageType;
lockChat: boolean; lockChat: boolean;
lastMessage: boolean; lastMessage: boolean;
setLockChat: (lock: boolean) => void;
updateChat: ( updateChat: (
chat: ChatMessageType, chat: ChatMessageType,
message: string, message: string,
stream_url: string stream_url?: string
) => void; ) => void;
}; };
@ -632,9 +633,9 @@ export type validationStatusType = {
id: string; id: string;
data: object | any; data: object | any;
params: string; params: string;
progress: number; progress?: number;
valid: boolean; valid: boolean;
duration: string; duration?: string;
}; };
export type ApiKey = { export type ApiKey = {

View file

@ -18,16 +18,18 @@ export type ChatOutputType = {
message: string; message: string;
sender: string; sender: string;
sender_name: string; sender_name: string;
stream_url?: string;
}; };
export type FlowPoolObjectType = { export type FlowPoolObjectType = {
timestamp: string; timestamp: string;
valid: boolean; valid: boolean;
params: any; params: any;
data: { artifacts: any; results: any | ChatOutputType | chatInputType }; data: { artifacts: any | ChatOutputType | chatInputType; results: any | ChatOutputType | chatInputType };
duration: string; duration?: string;
progress: number; progress?: number;
id: string; id: string;
buildId: string;
}; };
export type FlowPoolType = { export type FlowPoolType = {
@ -40,7 +42,7 @@ export type FlowStoreType = {
outputs: Array<{ type: string; id: string }>; outputs: Array<{ type: string; id: string }>;
hasIO: boolean; hasIO: boolean;
setFlowPool: (flowPool: FlowPoolType) => void; setFlowPool: (flowPool: FlowPoolType) => void;
addDataToFlowPool: (data: any, nodeId: string) => void; addDataToFlowPool: (data: FlowPoolObjectType, nodeId: string) => void;
CleanFlowPool: () => void; CleanFlowPool: () => void;
isBuilding: boolean; isBuilding: boolean;
isPending: boolean; isPending: boolean;
@ -92,4 +94,5 @@ export type FlowStoreType = {
updateBuildStatus: (nodeId: string[], status: BuildStatus) => void; updateBuildStatus: (nodeId: string[], status: BuildStatus) => void;
revertBuiltStatusFromBuilding: () => void; revertBuiltStatusFromBuilding: () => void;
flowBuildStatus: { [key: string]: BuildStatus }; flowBuildStatus: { [key: string]: BuildStatus };
updateFlowPool: (nodeId:string, data:FlowPoolObjectType | ChatOutputType | chatInputType,buildId?:string) => void;
}; };

View file

@ -9,7 +9,7 @@ type BuildVerticesParams = {
flowId: string; // Assuming FlowType is the type for your flow flowId: string; // Assuming FlowType is the type for your flow
nodeId?: string | null; // Assuming nodeId is of type string, and it's optional nodeId?: string | null; // Assuming nodeId is of type string, and it's optional
onGetOrderSuccess?: () => void; onGetOrderSuccess?: () => void;
onBuildUpdate?: (data: VertexBuildTypeAPI, status: BuildStatus) => void; // Replace any with the actual type if it's not any onBuildUpdate?: (data: VertexBuildTypeAPI, status: BuildStatus,buildId:string) => void; // Replace any with the actual type if it's not any
onBuildComplete?: (allNodesValid: boolean) => void; onBuildComplete?: (allNodesValid: boolean) => void;
onBuildError?: (title, list, idList: string[]) => void; onBuildError?: (title, list, idList: string[]) => void;
onBuildStart?: (idList: string[]) => void; onBuildStart?: (idList: string[]) => void;
@ -48,7 +48,7 @@ export async function buildVertices({
let orderResponse; let orderResponse;
try { try {
orderResponse = await getVerticesOrder(flowId, nodeId); orderResponse = await getVerticesOrder(flowId, nodeId);
} catch (error) { } catch (error:any) {
console.log(error); console.log(error);
setErrorData({ setErrorData({
title: "Oops! Looks like you missed something", title: "Oops! Looks like you missed something",
@ -59,6 +59,7 @@ export async function buildVertices({
} }
if (onGetOrderSuccess) onGetOrderSuccess(); if (onGetOrderSuccess) onGetOrderSuccess();
let verticesOrder: Array<Array<string>> = orderResponse.data.ids; let verticesOrder: Array<Array<string>> = orderResponse.data.ids;
const runId = orderResponse.data.run_id;
let vertices_layers: Array<Array<string>> = []; let vertices_layers: Array<Array<string>> = [];
let stop = false; let stop = false;
if (validateNodes) { if (validateNodes) {
@ -102,14 +103,14 @@ export async function buildVertices({
onBuildUpdate onBuildUpdate
) { ) {
// If it is, skip building and set the state to inactive // If it is, skip building and set the state to inactive
onBuildUpdate(getInactiveVertexData(id), BuildStatus.INACTIVE); onBuildUpdate(getInactiveVertexData(id), BuildStatus.INACTIVE,runId);
buildResults.push(false); buildResults.push(false);
continue; continue;
} }
await buildVertex({ await buildVertex({
flowId, flowId,
id, id,
onBuildUpdate, onBuildUpdate:(data: VertexBuildTypeAPI, status: BuildStatus) => {if(onBuildUpdate) onBuildUpdate(data, status,runId)},
onBuildError, onBuildError,
verticesIds, verticesIds,
buildResults, buildResults,

View file

@ -13,7 +13,7 @@ import {
tweakType, tweakType,
} from "../types/components"; } from "../types/components";
import { FlowType, NodeType } from "../types/flow"; import { FlowType, NodeType } from "../types/flow";
import { FlowState, FlowsState } from "../types/tabs"; import { FlowState } from "../types/tabs";
import { buildTweaks } from "./reactflowUtils"; import { buildTweaks } from "./reactflowUtils";
export function classNames(...classes: Array<string>): string { export function classNames(...classes: Array<string>): string {
@ -217,12 +217,8 @@ export function groupByFamily(
})); }));
} }
export function buildInputs(flowState?: FlowState): string { export function buildInputs(): string {
return flowState && return '{"input_value": "message"}';
flowState.input_keys &&
Object.keys(flowState.input_keys!).length > 0
? JSON.stringify(flowState.input_keys)
: '{"input": "message"}';
} }
export function getRandomElement<T>(array: T[]): T { export function getRandomElement<T>(array: T[]): T {
@ -323,7 +319,7 @@ export function getPythonApiCode(
// node.data.id // node.data.id
// } // }
const tweaks = buildTweaks(flow); const tweaks = buildTweaks(flow);
const inputs = buildInputs(flowState); const inputs = buildInputs();
return `import requests return `import requests
from typing import Optional from typing import Optional
@ -382,12 +378,10 @@ export function getCurlCode(
): string { ): string {
const flowId = flow.id; const flowId = flow.id;
const tweaks = buildTweaks(flow); const tweaks = buildTweaks(flow);
const inputs = buildInputs(flowState); const inputs = buildInputs();
return `curl -X POST \\ return `curl -X POST \\
${window.location.protocol}//${ ${window.location.protocol}//${window.location.host}/api/v1/run/${flowId} \\
window.location.host
}/api/v1/process/${flowId} \\
-H 'Content-Type: application/json'\\${ -H 'Content-Type: application/json'\\${
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : "" !isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
} }
@ -410,7 +404,7 @@ export function getPythonCode(
): string { ): string {
const flowName = flow.name; const flowName = flow.name;
const tweaks = buildTweaks(flow); const tweaks = buildTweaks(flow);
const inputs = buildInputs(flowState); const inputs = buildInputs();
return `from langflow import load_flow_from_json return `from langflow import load_flow_from_json
TWEAKS = ${ TWEAKS = ${
tweak && tweak.length > 0 tweak && tweak.length > 0
@ -435,7 +429,7 @@ export function getWidgetCode(
): string { ): string {
const flowId = flow.id; const flowId = flow.id;
const flowName = flow.name; const flowName = flow.name;
const inputs = buildInputs(flowState); const inputs = buildInputs();
let chat_input_field = getChatInputField(flow, flowState); let chat_input_field = getChatInputField(flow, flowState);
return `<script src="https://cdn.jsdelivr.net/gh/logspace-ai/langflow-embedded-chat@main/dist/build/static/js/bundle.min.js"></script> return `<script src="https://cdn.jsdelivr.net/gh/logspace-ai/langflow-embedded-chat@main/dist/build/static/js/bundle.min.js"></script>