diff --git a/src/backend/langflow/api/utils.py b/src/backend/langflow/api/utils.py index bd566896d..aa9598814 100644 --- a/src/backend/langflow/api/utils.py +++ b/src/backend/langflow/api/utils.py @@ -24,24 +24,30 @@ def remove_api_keys(flow: dict): return flow -def build_input_keys_response(langchain_object): +def build_input_keys_response(langchain_object, artifacts): """Build the input keys response.""" input_keys_response = { "input_keys": {key: "" for key in langchain_object.input_keys}, "memory_keys": [], + "handle_keys": artifacts.get("handle_keys", []), } + + # Set the input keys values from artifacts + for key, value in artifacts.items(): + if key in input_keys_response["input_keys"]: + input_keys_response["input_keys"][key] = value # If the object has memory, that memory will have a memory_variables attribute # memory variables should be removed from the input keys if hasattr(langchain_object, "memory") and hasattr( langchain_object.memory, "memory_variables" ): # Remove memory variables from input keys - input_keys_response["input_keys"] = [ - key - for key in input_keys_response["input_keys"] + input_keys_response["input_keys"] = { + key: value + for key, value in input_keys_response["input_keys"].items() if key not in langchain_object.memory.memory_variables - ] + } # Add memory variables to memory_keys input_keys_response["memory_keys"] = langchain_object.memory.memory_variables diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py index de3c33e01..b394afc99 100644 --- a/src/backend/langflow/api/v1/chat.py +++ b/src/backend/langflow/api/v1/chat.py @@ -72,6 +72,7 @@ async def stream_build(flow_id: str): async def event_stream(flow_id): final_response = {"end_of_stream": True} + artifacts = {} try: if flow_id not in flow_data_store: error_message = "Invalid session ID" @@ -108,6 +109,11 @@ async def stream_build(flow_id: str): logger.debug( f"Building node {params[:50]}{'...' if len(params) > 50 else ''}" ) + if vertex.artifacts: + # The artifacts will be prompt variables + # passed to build_input_keys_response + # to set the input_keys values + artifacts.update(vertex.artifacts) except Exception as exc: params = str(exc) valid = False @@ -124,13 +130,16 @@ async def stream_build(flow_id: str): langchain_object = graph.build() # Now we need to check the input_keys to send them to the client if hasattr(langchain_object, "input_keys"): - input_keys_response = build_input_keys_response(langchain_object) + input_keys_response = build_input_keys_response( + langchain_object, artifacts + ) yield str(StreamData(event="message", data=input_keys_response)) chat_manager.set_cache(flow_id, langchain_object) # We need to reset the chat history chat_manager.chat_history.empty_history(flow_id) except Exception as exc: + logger.exception(exc) logger.error("Error while building the flow: %s", exc) yield str(StreamData(event="error", data={"error": str(exc)})) finally: diff --git a/src/backend/langflow/api/v1/validate.py b/src/backend/langflow/api/v1/validate.py index b5b886816..23fad5711 100644 --- a/src/backend/langflow/api/v1/validate.py +++ b/src/backend/langflow/api/v1/validate.py @@ -39,10 +39,11 @@ def post_validate_prompt(prompt: ValidatePromptRequest): try: template_field = TemplateField( name=variable, + display_name=variable, field_type="str", show=True, advanced=False, - input_types=["BaseLoader"], + input_types=["BaseLoader", "BaseOutputParser"], ) prompt.frontend_node.template[variable] = template_field.to_dict() diff --git a/src/backend/langflow/config.yaml b/src/backend/langflow/config.yaml index 0896bb77d..22b337807 100644 --- a/src/backend/langflow/config.yaml +++ b/src/backend/langflow/config.yaml @@ -142,6 +142,6 @@ vectorstores: - MongoDBAtlasVectorSearch wrappers: - RequestsWrapper - # - ChatPromptTemplate - # - SystemMessagePromptTemplate - # - HumanMessagePromptTemplate +output_parsers: + - StructuredOutputParser + - ResponseSchema diff --git a/src/backend/langflow/graph/edge/base.py b/src/backend/langflow/graph/edge/base.py index 08f084a5c..341c3c78f 100644 --- a/src/backend/langflow/graph/edge/base.py +++ b/src/backend/langflow/graph/edge/base.py @@ -6,9 +6,15 @@ if TYPE_CHECKING: class Edge: - def __init__(self, source: "Vertex", target: "Vertex"): + def __init__(self, source: "Vertex", target: "Vertex", edge: dict): self.source: "Vertex" = source self.target: "Vertex" = target + self.source_handle = edge.get("sourceHandle", "") + self.target_handle = edge.get("targetHandle", "") + # 'BaseLoader;BaseOutputParser|documents|PromptTemplate-zmTlD' + # target_param is documents + self.target_param = self.target_handle.split("|")[1] + self.validate_edge() def validate_edge(self) -> None: diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index 46425ddf6..65fff3239 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -179,7 +179,7 @@ class Graph: raise ValueError(f"Source node {edge['source']} not found") if target is None: raise ValueError(f"Target node {edge['target']} not found") - edges.append(Edge(source, target)) + edges.append(Edge(source, target, edge)) return edges def _get_vertex_class(self, node_type: str, node_lc_type: str) -> Type[Vertex]: diff --git a/src/backend/langflow/graph/graph/constants.py b/src/backend/langflow/graph/graph/constants.py index 3398f253f..7775c50ff 100644 --- a/src/backend/langflow/graph/graph/constants.py +++ b/src/backend/langflow/graph/graph/constants.py @@ -1,18 +1,5 @@ from langflow.graph.vertex.base import Vertex -from langflow.graph.vertex.types import ( - AgentVertex, - ChainVertex, - DocumentLoaderVertex, - EmbeddingVertex, - LLMVertex, - MemoryVertex, - PromptVertex, - TextSplitterVertex, - ToolVertex, - ToolkitVertex, - VectorStoreVertex, - WrapperVertex, -) +from langflow.graph.vertex import types from langflow.interface.agents.base import agent_creator from langflow.interface.chains.base import chain_creator from langflow.interface.document_loaders.base import documentloader_creator @@ -25,22 +12,24 @@ from langflow.interface.toolkits.base import toolkits_creator from langflow.interface.tools.base import tool_creator from langflow.interface.vector_store.base import vectorstore_creator from langflow.interface.wrappers.base import wrapper_creator +from langflow.interface.output_parsers.base import output_parser_creator from typing import Dict, Type VERTEX_TYPE_MAP: Dict[str, Type[Vertex]] = { - **{t: PromptVertex for t in prompt_creator.to_list()}, - **{t: AgentVertex for t in agent_creator.to_list()}, - **{t: ChainVertex for t in chain_creator.to_list()}, - **{t: ToolVertex for t in tool_creator.to_list()}, - **{t: ToolkitVertex for t in toolkits_creator.to_list()}, - **{t: WrapperVertex for t in wrapper_creator.to_list()}, - **{t: LLMVertex for t in llm_creator.to_list()}, - **{t: MemoryVertex for t in memory_creator.to_list()}, - **{t: EmbeddingVertex for t in embedding_creator.to_list()}, - **{t: VectorStoreVertex for t in vectorstore_creator.to_list()}, - **{t: DocumentLoaderVertex for t in documentloader_creator.to_list()}, - **{t: TextSplitterVertex for t in textsplitter_creator.to_list()}, + **{t: types.PromptVertex for t in prompt_creator.to_list()}, + **{t: types.AgentVertex for t in agent_creator.to_list()}, + **{t: types.ChainVertex for t in chain_creator.to_list()}, + **{t: types.ToolVertex for t in tool_creator.to_list()}, + **{t: types.ToolkitVertex for t in toolkits_creator.to_list()}, + **{t: types.WrapperVertex for t in wrapper_creator.to_list()}, + **{t: types.LLMVertex for t in llm_creator.to_list()}, + **{t: types.MemoryVertex for t in memory_creator.to_list()}, + **{t: types.EmbeddingVertex for t in embedding_creator.to_list()}, + **{t: types.VectorStoreVertex for t in vectorstore_creator.to_list()}, + **{t: types.DocumentLoaderVertex for t in documentloader_creator.to_list()}, + **{t: types.TextSplitterVertex for t in textsplitter_creator.to_list()}, + **{t: types.OutputParserVertex for t in output_parser_creator.to_list()}, } diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index 61c50eda5..a7fcf88b5 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -1,14 +1,11 @@ -from langflow.utils.constants import DIRECT_TYPES from langflow.interface.initialize import loading from langflow.interface.listing import ALL_TYPES_DICT from langflow.utils.logger import logger from langflow.utils.util import sync_to_async -import contextlib import inspect import types -import warnings from typing import Any, Dict, List, Optional from typing import TYPE_CHECKING @@ -25,6 +22,7 @@ class Vertex: self._parse_data() self._built_object = None self._built = False + self.artifacts: Dict[str, Any] = {} def _parse_data(self) -> None: self.data = self._data["data"] @@ -45,6 +43,14 @@ class Vertex: for key, value in template_dicts.items() if not value["required"] ] + # Add the template_dicts[key]["input_types"] to the optional_inputs + self.optional_inputs.extend( + [ + input_type + for value in template_dicts.values() + for input_type in value.get("input_types", []) + ] + ) template_dict = self.data["node"]["template"] self.vertex_type = ( @@ -60,6 +66,7 @@ class Vertex: break def _build_params(self): + # sourcery skip: merge-list-append, remove-redundant-if # Some params are required, some are optional # but most importantly, some params are python base classes # like str and others are LangChain objects like LLMChain, BasePromptTemplate @@ -80,8 +87,19 @@ class Vertex: if isinstance(value, dict) } params = {} + + for edge in self.edges: + param_key = edge.target_param + if param_key in template_dict: + if template_dict[param_key]["list"]: + if param_key not in params: + params[param_key] = [] + params[param_key].append(edge.source) + else: + params[param_key] = edge.source + for key, value in template_dict.items(): - if key == "_type": + if key == "_type" or not value.get("show"): continue # If the type is not transformable to a python base class # then we need to get the edge that connects to this node @@ -92,45 +110,8 @@ class Vertex: file_path = value.get("file_path") params[key] = file_path - - elif value.get("type") not in DIRECT_TYPES: - # Get the edge that connects to this node - edges = [ - edge - for edge in self.edges - if edge.target == self and edge.matched_type in value["type"] - ] - - # Get the output of the node that the edge connects to - # if the value['list'] is True, then there will be more - # than one time setting to params[key] - # so we need to append to a list if it exists - # or create a new list if it doesn't - - if value["required"] and not edges: - # If a required parameter is not found, raise an error - raise ValueError( - f"Required input {key} for module {self.vertex_type} not found" - ) - elif value["list"]: - # If this is a list parameter, append all sources to a list - params[key] = [edge.source for edge in edges] - elif edges: - # If a single parameter is found, use its source - params[key] = edges[0].source - - elif value["required"] or value.get("value"): - # If value does not have value this still passes - # but then gives a keyError - # so we need to check if value has value - new_value = value.get("value") - if new_value is None: - warnings.warn(f"Value for {key} in {self.vertex_type} is None. ") - if value.get("type") == "int": - with contextlib.suppress(TypeError, ValueError): - new_value = int(new_value) # type: ignore - params[key] = new_value - + elif value.get("type") in ["str", "prompt"] and params.get(key) is None: + params[key] = value.get("value") # Add _type to params self.params = params @@ -195,11 +176,17 @@ class Vertex: # and return the instance try: - self._built_object = loading.instantiate_class( + result = loading.instantiate_class( node_type=self.vertex_type, base_type=self.base_type, params=self.params, ) + # Result could be the _built_object or + # (_built_object, dict) tuple + if isinstance(result, tuple): + self._built_object, self.artifacts = result + else: + self._built_object = result except Exception as exc: raise ValueError( f"Error building node {self.vertex_type}: {str(exc)}" diff --git a/src/backend/langflow/graph/vertex/types.py b/src/backend/langflow/graph/vertex/types.py index 775a2ebd2..af2081217 100644 --- a/src/backend/langflow/graph/vertex/types.py +++ b/src/backend/langflow/graph/vertex/types.py @@ -194,3 +194,8 @@ class PromptVertex(Vertex): self._build() return self._built_object + + +class OutputParserVertex(Vertex): + def __init__(self, data: Dict): + super().__init__(data, base_type="output_parsers") diff --git a/src/backend/langflow/interface/importing/utils.py b/src/backend/langflow/interface/importing/utils.py index 3ba40ba31..c2325378e 100644 --- a/src/backend/langflow/interface/importing/utils.py +++ b/src/backend/langflow/interface/importing/utils.py @@ -45,6 +45,7 @@ def import_by_type(_type: str, name: str) -> Any: "documentloaders": import_documentloader, "textsplitters": import_textsplitter, "utilities": import_utility, + "output_parsers": import_output_parser, } if _type == "llms": key = "chat" if "chat" in name.lower() else "llm" @@ -55,6 +56,11 @@ def import_by_type(_type: str, name: str) -> Any: return loaded_func(name) +def import_output_parser(output_parser: str) -> Any: + """Import output parser from output parser name""" + return import_module(f"from langchain.output_parsers import {output_parser}") + + def import_chat_llm(llm: str) -> BaseChatModel: """Import chat llm from llm name""" return import_class(f"langchain.chat_models.{llm}") diff --git a/src/backend/langflow/interface/initialize/loading.py b/src/backend/langflow/interface/initialize/loading.py index 26890fc1c..53679ea9a 100644 --- a/src/backend/langflow/interface/initialize/loading.py +++ b/src/backend/langflow/interface/initialize/loading.py @@ -1,5 +1,5 @@ import json -from typing import Any, Callable, Dict, Sequence +from typing import Any, Callable, Dict, List, Sequence from langchain.agents import ZeroShotAgent from langchain.agents import agent as agent_module @@ -8,6 +8,7 @@ from langchain.agents.agent_toolkits.base import BaseToolkit from langchain.agents.tools import BaseTool from langflow.interface.initialize.vector_store import vecstore_initializer +from langchain.schema import Document, BaseOutputParser from pydantic import ValidationError from langflow.interface.importing.utils import ( @@ -18,6 +19,7 @@ from langflow.interface.importing.utils import ( from langflow.interface.custom_lists import CUSTOM_NODES from langflow.interface.toolkits.base import toolkits_creator from langflow.interface.chains.base import chain_creator +from langflow.interface.output_parsers.base import output_parser_creator from langflow.interface.utils import load_file_into_dict from langflow.utils import validate @@ -76,10 +78,21 @@ def instantiate_based_on_type(class_object, base_type, node_type, params): return instantiate_utility(node_type, class_object, params) elif base_type == "chains": return instantiate_chains(node_type, class_object, params) + elif base_type == "output_parsers": + return instantiate_output_parser(node_type, class_object, params) else: return class_object(**params) +def instantiate_output_parser(node_type, class_object, params): + if node_type in output_parser_creator.from_method_nodes: + method = output_parser_creator.from_method_nodes[node_type] + if class_method := getattr(class_object, method, None): + return class_method(**params) + raise ValueError(f"Method {method} not found in {class_object}") + return class_object(**params) + + def instantiate_chains(node_type, class_object, params): if "retriever" in params and hasattr(params["retriever"], "as_retriever"): params["retriever"] = params["retriever"].as_retriever() @@ -116,19 +129,45 @@ def instantiate_prompt(node_type, class_object, params): prompt = class_object(**params) - # Now we go through input_variables - # Check if they are in params, if so - # get their values and set them format_kwargs = {} for input_variable in prompt.input_variables: if input_variable in params: - input_value = params[input_variable] - format_kwargs[input_variable] = input_value + variable = params[input_variable] + if isinstance(variable, str): + format_kwargs[input_variable] = variable + elif isinstance(variable, BaseOutputParser) and hasattr( + variable, "get_format_instructions" + ): + format_kwargs[input_variable] = variable.get_format_instructions() + # check if is a list of Document + elif isinstance(variable, List) and all( + isinstance(item, Document) for item in variable + ): + # Format document to contain page_content and metadata + # as one string separated by a newline + format_kwargs[input_variable] = "\n".join( + [ + f"Document:{item.page_content}\nMetadata:{item.metadata}" + for item in variable + ] + ) + # handle_keys will be a list but it does not exist yet + # so we need to create it - if format_kwargs: - prompt = prompt.partial(**format_kwargs) + if ( + isinstance(variable, List) + and all(isinstance(item, Document) for item in variable) + ) or ( + isinstance(variable, BaseOutputParser) + and hasattr(variable, "get_format_instructions") + ): + if "handle_keys" not in format_kwargs: + format_kwargs["handle_keys"] = [] - return prompt + # Add the handle_keys to the list + format_kwargs["handle_keys"].append(input_variable) + + return prompt, format_kwargs def instantiate_tool(node_type, class_object, params): diff --git a/src/backend/langflow/interface/listing.py b/src/backend/langflow/interface/listing.py index 3d73105c2..bb8e6127b 100644 --- a/src/backend/langflow/interface/listing.py +++ b/src/backend/langflow/interface/listing.py @@ -11,6 +11,7 @@ from langflow.interface.tools.base import tool_creator from langflow.interface.utilities.base import utility_creator from langflow.interface.vector_store.base import vectorstore_creator from langflow.interface.wrappers.base import wrapper_creator +from langflow.interface.output_parsers.base import output_parser_creator def get_type_dict(): @@ -28,6 +29,7 @@ def get_type_dict(): "embeddings": embedding_creator.to_list(), "textSplitters": textsplitter_creator.to_list(), "utilities": utility_creator.to_list(), + "outputParsers": output_parser_creator.to_list(), } diff --git a/src/backend/langflow/interface/output_parsers/__init__.py b/src/backend/langflow/interface/output_parsers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/interface/output_parsers/base.py b/src/backend/langflow/interface/output_parsers/base.py new file mode 100644 index 000000000..79cbdd98c --- /dev/null +++ b/src/backend/langflow/interface/output_parsers/base.py @@ -0,0 +1,64 @@ +from typing import Dict, List, Optional, Type + +from langchain import output_parsers + +from langflow.interface.base import LangChainTypeCreator +from langflow.interface.importing.utils import import_class +from langflow.settings import settings +from langflow.template.frontend_node.output_parsers import OutputParserFrontendNode +from langflow.utils.logger import logger +from langflow.utils.util import build_template_from_class, build_template_from_method + + +class OutputParserCreator(LangChainTypeCreator): + type_name: str = "output_parsers" + from_method_nodes = { + "StructuredOutputParser": "from_response_schemas", + } + + @property + def frontend_node_class(self) -> Type[OutputParserFrontendNode]: + return OutputParserFrontendNode + + @property + def type_to_loader_dict(self) -> Dict: + if self.type_dict is None: + self.type_dict = { + output_parser_name: import_class( + f"langchain.output_parsers.{output_parser_name}" + ) + # if output_parser_name is not lower case it is a class + for output_parser_name in output_parsers.__all__ + } + self.type_dict = { + name: output_parser + for name, output_parser in self.type_dict.items() + if name in settings.output_parsers or settings.dev + } + return self.type_dict + + def get_signature(self, name: str) -> Optional[Dict]: + try: + if name in self.from_method_nodes: + return build_template_from_method( + name, + type_to_cls_dict=self.type_to_loader_dict, + method_name=self.from_method_nodes[name], + ) + else: + return build_template_from_class( + name, + type_to_cls_dict=self.type_to_loader_dict, + ) + except ValueError as exc: + # raise ValueError("OutputParser not found") from exc + logger.error(f"OutputParser {name} not found: {exc}") + except AttributeError as exc: + logger.error(f"OutputParser {name} not loaded: {exc}") + return None + + def to_list(self) -> List[str]: + return list(self.type_to_loader_dict.keys()) + + +output_parser_creator = OutputParserCreator() diff --git a/src/backend/langflow/interface/run.py b/src/backend/langflow/interface/run.py index a3efe2b0c..3ce5da615 100644 --- a/src/backend/langflow/interface/run.py +++ b/src/backend/langflow/interface/run.py @@ -14,6 +14,23 @@ def build_langchain_object_with_caching(data_graph): return graph.build() +@memoize_dict(maxsize=10) +def build_sorted_vertices_with_caching(data_graph): + """ + Build langchain object from data_graph. + """ + + logger.debug("Building langchain object") + graph = Graph.from_payload(data_graph) + sorted_vertices = graph.topological_sort() + artifacts = {} + for vertex in sorted_vertices: + vertex.build() + if vertex.artifacts: + artifacts.update(vertex.artifacts) + return graph.build(), artifacts + + def build_langchain_object(data_graph): """ Build langchain object from data_graph. diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py index 704be06fd..06fa4b257 100644 --- a/src/backend/langflow/interface/types.py +++ b/src/backend/langflow/interface/types.py @@ -11,6 +11,7 @@ from langflow.interface.tools.base import tool_creator from langflow.interface.utilities.base import utility_creator from langflow.interface.vector_store.base import vectorstore_creator from langflow.interface.wrappers.base import wrapper_creator +from langflow.interface.output_parsers.base import output_parser_creator from langflow.template.field.base import TemplateField from langflow.template.frontend_node.tools import CustomComponentNode @@ -47,6 +48,7 @@ def build_langchain_types_dict(): # sourcery skip: dict-assign-update-to-union documentloader_creator, textsplitter_creator, utility_creator, + output_parser_creator, ] all_types = {} diff --git a/src/backend/langflow/processing/process.py b/src/backend/langflow/processing/process.py index abf7a00b8..3ccb3a8b1 100644 --- a/src/backend/langflow/processing/process.py +++ b/src/backend/langflow/processing/process.py @@ -2,7 +2,7 @@ from pathlib import Path from langchain.schema import AgentAction import json from langflow.interface.run import ( - build_langchain_object_with_caching, + build_sorted_vertices_with_caching, get_memory_key, update_memory_keys, ) @@ -88,8 +88,16 @@ def process_graph_cached(data_graph: Dict[str, Any], inputs: Optional[dict] = No with PromptTemplate,then run the graph and return the result and thought. """ # Load langchain object - langchain_object = build_langchain_object_with_caching(data_graph) + langchain_object, artifacts = build_sorted_vertices_with_caching(data_graph) logger.debug("Loaded LangChain object") + if inputs is None: + inputs = {} + for ( + key, + value, + ) in artifacts.items(): + if key not in inputs or not inputs[key]: + inputs[key] = value if langchain_object is None: # Raise user facing error diff --git a/src/backend/langflow/settings.py b/src/backend/langflow/settings.py index e3644e84c..8fe508210 100644 --- a/src/backend/langflow/settings.py +++ b/src/backend/langflow/settings.py @@ -19,6 +19,7 @@ class Settings(BaseSettings): toolkits: List[str] = [] textsplitters: List[str] = [] utilities: List[str] = [] + output_parsers: List[str] = [] dev: bool = False database_url: str = "sqlite:///./langflow.db" cache: str = "InMemoryCache" @@ -48,6 +49,10 @@ class Settings(BaseSettings): self.toolkits = new_settings.toolkits or [] self.textsplitters = new_settings.textsplitters or [] self.utilities = new_settings.utilities or [] + self.embeddings = new_settings.embeddings or [] + self.vectorstores = new_settings.vectorstores or [] + self.documentloaders = new_settings.documentloaders or [] + self.output_parsers = new_settings.output_parsers or [] self.dev = dev def update_settings(self, **kwargs): diff --git a/src/backend/langflow/template/frontend_node/output_parsers.py b/src/backend/langflow/template/frontend_node/output_parsers.py new file mode 100644 index 000000000..e9b4d3706 --- /dev/null +++ b/src/backend/langflow/template/frontend_node/output_parsers.py @@ -0,0 +1,10 @@ +from typing import Optional +from langflow.template.field.base import TemplateField +from langflow.template.frontend_node.base import FrontendNode + + +class OutputParserFrontendNode(FrontendNode): + @staticmethod + def format_field(field: TemplateField, name: Optional[str] = None) -> None: + FrontendNode.format_field(field, name) + field.show = True diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 1c5d83a02..af3af0aa7 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -25,6 +25,7 @@ import { nodeColors } from "../../../../utils"; import ShadTooltip from "../../../../components/ShadTooltipComponent"; import { PopUpContext } from "../../../../contexts/popUpContext"; import ToggleShadComponent from "../../../../components/toggleShadComponent"; +import * as _ from "lodash"; export default function ParameterComponent({ left, @@ -36,6 +37,7 @@ export default function ParameterComponent({ type, name = "", required = false, + optionalHandle = null, }: ParameterComponentType) { const ref = useRef(null); const refHtml = useRef(null); @@ -73,6 +75,7 @@ export default function ParameterComponent({ return { ...prev, [tabId]: { + ...prev[tabId], isPending: true, formKeysData: prev[tabId].formKeysData, }, @@ -85,7 +88,7 @@ export default function ParameterComponent({ refHtml.current = groupedObj.map((item, i) => ( 0 ? "items-center flex mt-3" : "items-center flex" )} @@ -132,13 +135,14 @@ export default function ParameterComponent({ {required ? " *" : ""} {left && - (type === "str" || + ((type === "str" || type === "bool" || type === "float" || type === "code" || type === "prompt" || type === "file" || - type === "int") ? ( + type === "int") && !optionalHandle + ) ? ( <> ) : ( -
+
) : ( <> diff --git a/src/frontend/src/components/chatComponent/index.tsx b/src/frontend/src/components/chatComponent/index.tsx index cd3c9c789..8cdc3a981 100644 --- a/src/frontend/src/components/chatComponent/index.tsx +++ b/src/frontend/src/components/chatComponent/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import { useNodes } from "reactflow"; import { ChatType } from "../../types/chat"; import ChatTrigger from "./chatTrigger"; @@ -7,11 +7,13 @@ import BuildTrigger from "./buildTrigger"; import { getBuildStatus } from "../../controllers/API"; import { NodeType } from "../../types/flow"; import FormModal from "../../modals/formModal"; +import { TabsContext } from "../../contexts/tabsContext"; +import * as _ from "lodash"; export default function Chat({ flow }: ChatType) { const [open, setOpen] = useState(false); - const [openForm, setOpenForm] = useState(false); const [isBuilt, setIsBuilt] = useState(false); + const {tabsState} = useContext(TabsContext); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { @@ -44,21 +46,19 @@ export default function Chat({ flow }: ChatType) { const prevNodesRef = useRef(); const nodes = useNodes(); useEffect(() => { - const prevNodes = prevNodesRef.current; const currentNodes = nodes.map( - (node: NodeType) => node.data.node.template + (node: NodeType) => _.cloneDeep(node.data.node.template) ); - if ( - prevNodes && - JSON.stringify(prevNodes) !== JSON.stringify(currentNodes) + tabsState && tabsState[flow.id] && tabsState[flow.id].isPending + && JSON.stringify(prevNodes) !== JSON.stringify(currentNodes) ) { setIsBuilt(false); } prevNodesRef.current = currentNodes; - }, [nodes]); + }, [tabsState]); return ( <> diff --git a/src/frontend/src/components/promptComponent/index.tsx b/src/frontend/src/components/promptComponent/index.tsx index 59c4227ef..3999d2786 100644 --- a/src/frontend/src/components/promptComponent/index.tsx +++ b/src/frontend/src/components/promptComponent/index.tsx @@ -5,6 +5,9 @@ import GenericModal from "../../modals/genericModal"; import { TypeModal } from "../../utils"; import { INPUT_STYLE } from "../../constants"; import { ExternalLink } from "lucide-react"; +import { postValidatePrompt } from "../../controllers/API"; +import { typesContext } from "../../contexts/typesContext"; +import * as _ from "lodash"; export default function PromptAreaComponent({ setNodeClass, @@ -14,8 +17,10 @@ export default function PromptAreaComponent({ disabled, editNode = false, }: TextAreaComponentType) { - const [myValue, setMyValue] = useState(value); + const [myValue, setMyValue] = useState(""); const { openPopUp } = useContext(PopUpContext); + const { reactFlowInstance } = useContext(typesContext); + useEffect(() => { if (disabled) { setMyValue(""); @@ -24,8 +29,19 @@ export default function PromptAreaComponent({ }, [disabled, onChange]); useEffect(() => { - setMyValue(value); - }, [value]); + if (value !== "" && myValue !== value && reactFlowInstance) { // only executed once + setMyValue(value); + postValidatePrompt(value, nodeClass) + .then((apiReturn) => { + if (apiReturn.data) { + setNodeClass(apiReturn.data.frontend_node); + // need to update reactFlowInstance to re-render the nodes. + reactFlowInstance.setEdges(_.cloneDeep(reactFlowInstance.getEdges())); + } + }) + .catch((error) => {}); + } + }, [reactFlowInstance]); return (
{ - if (disabled) { - setEnabled(false); - } - }, [disabled, setEnabled]); let scaleX, scaleY; switch (size) { case "small": @@ -32,11 +27,12 @@ export default function ToggleShadComponent({ scaleY = 1; } return ( -
+
{ diff --git a/src/frontend/src/constants.tsx b/src/frontend/src/constants.tsx index 76b345ed5..4d634400b 100644 --- a/src/frontend/src/constants.tsx +++ b/src/frontend/src/constants.tsx @@ -173,7 +173,7 @@ export const DESCRIPTIONS: string[] = [ "Your Hub for Text Generation.", "Promptly Ingenious!", "Building Linguistic Labyrinths.", - "LangFlow: Create, Chain, Communicate.", + "Create, Chain, Communicate.", "Connect the Dots, Craft Language.", "Interactive Language Weaving.", "Generate, Innovate, Communicate.", @@ -183,6 +183,46 @@ export const DESCRIPTIONS: string[] = [ "Nurture NLP Nodes Here.", "Conversational Cartography Unlocked.", "Design, Develop, Dialogize.", + "Unleashing Linguistic Creativity.", + "Graph Your Way to Great Conversations.", + "The Power of Language at Your Fingertips.", + "Sculpting Language with Precision.", + "Where Language Meets Logic.", + "Building Intelligent Interactions.", + "Your Passport to Linguistic Landscapes.", + "Create, Curate, Communicate with LangFlow.", + "Flow into the Future of Language.", + "Mapping Meaningful Conversations.", + "Unravel the Art of Articulation.", + "Language Engineering Excellence.", + "Navigate the Networks of Conversation.", + "Crafting Conversations, One Node at a Time.", + "The Pinnacle of Prompt Generation.", + "Language Models, Mapped and Mastered.", + "Powerful Prompts, Perfectly Positioned.", + "Innovation in Interaction with LangFlow.", + "Your Toolkit for Text Generation.", + "Unfolding Linguistic Possibilities.", + "Building Powerful Solutions with Language Models.", + "Uncover Business Opportunities with NLP.", + "Harness the Power of Conversational AI.", + "Transform Your Business with Smart Dialogues.", + "Craft Meaningful Interactions, Generate Value.", + "Unleashing Business Potential through Language Engineering.", + "Empowering Enterprises with Intelligent Interactions.", + "Driving Innovation in Business Communication.", + "Catalyzing Business Growth through Conversational AI.", + "Text Generation Meets Business Transformation.", + "Navigate the Linguistic Landscape, Discover Opportunities.", + "Create Powerful Connections, Boost Business Value.", + "Empowering Communication, Enabling Opportunities.", + "Advanced NLP for Groundbreaking Business Solutions.", + "Innovation in Interaction, Revolution in Revenue.", + "Maximize Impact with Intelligent Conversations.", + "Beyond Text Generation - Unleashing Business Opportunities.", + "Unlock the Power of AI in Your Business Conversations.", + "Crafting Dialogues that Drive Business Success.", + "Engineered for Excellence, Built for Business.", ]; /** @@ -256,8 +296,49 @@ export const ADJECTIVES: string[] = [ "thirsty", "tiny", "trusting", + "bubbly", + "charming", + "cheerful", + "comical", + "dazzling", + "delighted", + "dynamic", + "effervescent", + "enthusiastic", + "exuberant", + "fluffy", + "friendly", + "funky", + "giddy", + "giggly", + "gleeful", + "goofy", + "graceful", + "grinning", + "hilarious", + "inquisitive", + "joyous", + "jubilant", + "lively", + "mirthful", + "mischievous", + "optimistic", + "peppy", + "perky", + "playful", + "quirky", + "radiant", + "sassy", + "silly", + "spirited", + "sprightly", + "twinkly", + "upbeat", + "vibrant", + "witty", + "zany", + "zealous", ]; - /** * Nouns for the name of the flow * @constant @@ -406,6 +487,38 @@ export const NOUNS: string[] = [ "wright", "yalow", "yonath", + "coulomb", + "degrasse", + "dewey", + "edison", + "eratosthenes", + "faraday", + "galton", + "gauss", + "herschel", + "hubble", + "joule", + "kaku", + "kepler", + "khayyam", + "lavoisier", + "maxwell", + "mendel", + "mendeleev", + "ohm", + "pascal", + "planck", + "riemann", + "schrodinger", + "sagan", + "tesla", + "tyson", + "volta", + "watt", + "weber", + "wien", + "zoBell", + "zuse", ]; /** diff --git a/src/frontend/src/contexts/tabsContext.tsx b/src/frontend/src/contexts/tabsContext.tsx index b847f4ecd..c60650a48 100644 --- a/src/frontend/src/contexts/tabsContext.tsx +++ b/src/frontend/src/contexts/tabsContext.tsx @@ -189,26 +189,28 @@ export function TabsProvider({ children }: { children: ReactNode }) { } function processFlowEdges(flow) { - if(!flow.data || !flow.data.edges) return; + if (!flow.data || !flow.data.edges) return; flow.data.edges.forEach((edge) => { edge.className = ""; edge.style = { stroke: "#555555" }; }); } - function updateDisplay_name(node:NodeType,template:APIClassType) { - node.data.node.display_name = template["display_name"]?template["display_name"]:node.data.type; + function updateDisplay_name(node: NodeType, template: APIClassType) { + node.data.node.display_name = template["display_name"] + ? template["display_name"] + : node.data.type; } function processFlowNodes(flow) { - if(!flow.data || !flow.data.nodes) return; - flow.data.nodes.forEach((node:NodeType) => { + if (!flow.data || !flow.data.nodes) return; + flow.data.nodes.forEach((node: NodeType) => { const template = templates[node.data.type]; if (!template) { setErrorData({ title: `Unknown node type: ${node.data.type}` }); return; } if (Object.keys(template["template"]).length > 0) { - updateDisplay_name(node,template); + updateDisplay_name(node, template); updateNodeBaseClasses(node, template); updateNodeEdges(flow, node, template); updateNodeDescription(node, template); @@ -217,11 +219,15 @@ export function TabsProvider({ children }: { children: ReactNode }) { }); } - function updateNodeBaseClasses(node:NodeType,template:APIClassType) { + function updateNodeBaseClasses(node: NodeType, template: APIClassType) { node.data.node.base_classes = template["base_classes"]; } - function updateNodeEdges(flow:FlowType, node:NodeType,template:APIClassType) { + function updateNodeEdges( + flow: FlowType, + node: NodeType, + template: APIClassType + ) { flow.data.edges.forEach((edge) => { if (edge.source === node.id) { edge.sourceHandle = edge.sourceHandle @@ -233,11 +239,11 @@ export function TabsProvider({ children }: { children: ReactNode }) { }); } - function updateNodeDescription(node:NodeType,template:APIClassType) { + function updateNodeDescription(node: NodeType, template: APIClassType) { node.data.node.description = template["description"]; } - function updateNodeTemplate(node:NodeType,template:APIClassType) { + function updateNodeTemplate(node: NodeType, template: APIClassType) { node.data.node.template = updateTemplate( template["template"] as unknown as APITemplateType, node.data.node.template as APITemplateType @@ -302,28 +308,39 @@ export function TabsProvider({ children }: { children: ReactNode }) { * If the file type is application/json, the file is read and parsed into a JSON object. * The resulting JSON object is passed to the addFlow function. */ - function uploadFlow(newProject?: boolean) { - // create a file input - const input = document.createElement("input"); - input.type = "file"; - input.accept = ".json"; - // add a change event listener to the file input - input.onchange = (e: Event) => { - // check if the file type is application/json - if ((e.target as HTMLInputElement).files[0].type === "application/json") { - // get the file from the file input - const file = (e.target as HTMLInputElement).files[0]; - // read the file as text - file.text().then((text) => { - // parse the text into a JSON object - let flow: FlowType = JSON.parse(text); + function uploadFlow(newProject?: boolean, file?: File) { + if (file) { + file.text().then((text) => { + // parse the text into a JSON object + let flow: FlowType = JSON.parse(text); - addFlow(flow, newProject); - }); - } - }; - // trigger the file input click event to open the file dialog - input.click(); + addFlow(flow, newProject); + }); + } else { + // create a file input + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".json"; + // add a change event listener to the file input + input.onchange = (e: Event) => { + // check if the file type is application/json + if ( + (e.target as HTMLInputElement).files[0].type === "application/json" + ) { + // get the file from the file input + const currentfile = (e.target as HTMLInputElement).files[0]; + // read the file as text + currentfile.text().then((text) => { + // parse the text into a JSON object + let flow: FlowType = JSON.parse(text); + + addFlow(flow, newProject); + }); + } + }; + // trigger the file input click event to open the file dialog + input.click(); + } } function uploadFlows() { diff --git a/src/frontend/src/modals/formModal/chatInput/index.tsx b/src/frontend/src/modals/formModal/chatInput/index.tsx index 50d42dd3f..3bcdec887 100644 --- a/src/frontend/src/modals/formModal/chatInput/index.tsx +++ b/src/frontend/src/modals/formModal/chatInput/index.tsx @@ -53,7 +53,7 @@ export default function ChatInput({ lockChat ? " bg-input text-black dark:bg-gray-700 dark:text-gray-300" : " bg-white-200 text-black dark:bg-gray-900 dark:text-gray-300", - "p-4 form-input block w-full custom-scroll rounded-md border-gray-300 dark:border-gray-600 pr-12 sm:text-sm" + + "p-4 form-input block w-full custom-scroll rounded-md border-gray-300 dark:border-gray-600 pr-16 sm:text-sm" + INPUT_STYLE )} placeholder={"Send a message..."} @@ -62,7 +62,7 @@ export default function ChatInput({
!tabsState[flow.id].formKeysData.handle_keys.some((j) => j === k) + ) + ] + ); const [chatHistory, setChatHistory] = useState([]); const { reactFlowInstance } = useContext(typesContext); const { setErrorData, setNoticeData } = useContext(alertContext); - const { tabsState } = useContext(TabsContext); const ws = useRef(null); const [lockChat, setLockChat] = useState(false); const isOpen = useRef(open); const messagesRef = useRef(null); const id = useRef(flow.id); - const [chatKey, setChatKey] = useState(0); + const [chatKey, setChatKey] = useState( + Object.keys(tabsState[flow.id].formKeysData.input_keys).find( + (k) => !tabsState[flow.id].formKeysData.handle_keys.some((j) => j === k) + ) + ); useEffect(() => { if (messagesRef.current) { @@ -64,12 +87,6 @@ export default function FormModal({ }, [open]); useEffect(() => { id.current = flow.id; - - if (tabsState[flow.id].formKeysData) { - setKeysValue( - Array(tabsState[flow.id].formKeysData.input_keys.length).fill("") - ); - } }, [flow.id, tabsState[flow.id], tabsState[flow.id].formKeysData]); var isStream = false; @@ -166,13 +183,13 @@ export default function FormModal({ chatItem.files ? { isSend: !chatItem.is_bot, - message: chatItem.is_bot ? chatItem.message : formatMessage(chatItem.message), + message: formatMessage(chatItem.message), thought: chatItem.intermediate_steps, files: chatItem.files, } : { isSend: !chatItem.is_bot, - message: chatItem.is_bot ? chatItem.message : formatMessage(chatItem.message), + message: formatMessage(chatItem.message), thought: chatItem.intermediate_steps, } ); @@ -305,6 +322,7 @@ export default function FormModal({ }, [open]); function formatMessage(inputs: any): string { if (inputs) { + if (typeof inputs == "string") return inputs; // inputs is a object with the keys and values being input_keys and keysValue // so the formated message is a string with the keys and values separated by ": " let message = ""; @@ -325,13 +343,7 @@ export default function FormModal({ let nodeValidationErrors = validateNodes(reactFlowInstance); if (nodeValidationErrors.length === 0) { setLockChat(true); - // Message variable makes a object with the keys being the names from tabsState[flow.id].formKeysData.input_keys and the values being the keysValue of the correspondent index - let keys = tabsState[flow.id].formKeysData.input_keys; // array of keys - let values = keysValue.map((k, i) => (i == chatKey ? chatValue : k)); // array of values - let inputs = keys.reduce((object, key, index) => { - object[key] = values[index]; - return object; - }, {}); + let inputs = tabsState[id.current].formKeysData.input_keys; setChatValue(""); const message = formatMessage(inputs); addChatHistory(message, true); @@ -342,6 +354,11 @@ export default function FormModal({ name: flow.name, description: flow.description, }); + setTabsState((old) => { + let newTabsState = _.cloneDeep(old); + newTabsState[id.current].formKeysData.input_keys[chatKey] = ""; + return newTabsState; + }); } else { setErrorData({ title: "Oops! Looks like you missed some required information:", @@ -365,13 +382,12 @@ export default function FormModal({ setOpen(x); } - function handleOnCheckedChange(checked: boolean, index: number) { + function handleOnCheckedChange(checked: boolean, i: string) { if (checked === true) { - setChatKey(index); + setChatKey(i); + setChatValue(tabsState[flow.id].formKeysData.input_keys[i]); } } - - console.log(chatHistory) return ( @@ -388,47 +404,90 @@ export default function FormModal({ {CHAT_FORM_DIALOG_SUBTITLE} -
-
-
- - - Input Variables - +
+
+
+
+ + + Input Variables + +
+
+ + Chat Input + +
- - {tabsState[id.current].formKeysData.input_keys.map((i, k) => ( - - {i} - -
-
- - - handleOnCheckedChange(value, k) - } - size="small" - disabled={false} - /> -
- -
-
-
- ))} + + {Object.keys(tabsState[id.current].formKeysData.input_keys).map( + (i, k) => ( +
+ + +
+ + {i} + + +
{ + event.stopPropagation(); + }} + > + + handleOnCheckedChange(value, i) + } + size="small" + disabled={tabsState[ + id.current + ].formKeysData.handle_keys.some((t) => t === i)} + /> +
+
+
+ +
+ {tabsState[ + id.current + ].formKeysData.handle_keys.some((t) => t === i) && ( +
+ Source: Component +
+ )} + +
+
+
+
+ ) + )} {tabsState[id.current].formKeysData.memory_keys.map((i, k) => (
-
{i}
+
+ + {i} + +
Used as Memory Key
@@ -437,14 +496,19 @@ export default function FormModal({
-
- -
+
+ +
{ + setChatValue(value); + setTabsState((old) => { + let newTabsState = _.cloneDeep(old); + newTabsState[id.current].formKeysData.input_keys[ + chatKey + ] = chatValue; + return newTabsState; + }); + }} inputRef={ref} />
diff --git a/src/frontend/src/modals/promptModal/index.tsx b/src/frontend/src/modals/promptModal/index.tsx deleted file mode 100644 index 2ab1c140a..000000000 --- a/src/frontend/src/modals/promptModal/index.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { Dialog, Transition } from "@headlessui/react"; -import { XMarkIcon, DocumentTextIcon } from "@heroicons/react/24/outline"; -import { Fragment, useContext, useRef, useState } from "react"; -import { PopUpContext } from "../../contexts/popUpContext"; -import { darkContext } from "../../contexts/darkContext"; -import { postValidatePrompt } from "../../controllers/API"; -import { alertContext } from "../../contexts/alertContext"; -export default function PromptAreaModal({ - value, - setValue, -}: { - setValue: (value: string) => void; - value: string; -}) { - const [open, setOpen] = useState(true); - const [myValue, setMyValue] = useState(value); - const { dark } = useContext(darkContext); - const { setErrorData, setSuccessData } = useContext(alertContext); - const { closePopUp } = useContext(PopUpContext); - const ref = useRef(); - function setModalOpen(x: boolean) { - setOpen(x); - if (x === false) { - setTimeout(() => { - closePopUp(); - }, 300); - } - } - return ( - - - -
- - -
-
- - -
- -
-
-
-
-
-
- - Edit Prompt - -
-
-
-
-
-