Merge branch 'form_io' into python_custom_node_component

This commit is contained in:
Gabriel Luiz Freitas Almeida 2023-07-07 01:36:18 -03:00
commit 320989870e
51 changed files with 1012 additions and 606 deletions

View file

@ -71,30 +71,68 @@ def validate_prompt(template: str):
except Exception as exc:
raise ValueError(str(exc)) from exc
# if len(input_variables) > 1:
# # If there's more than one input variable
return input_variables
def check_input_variables(input_variables: list):
invalid_chars = []
fixed_variables = []
wrong_variables = []
empty_variables = []
for variable in input_variables:
new_var = variable
for char in INVALID_CHARACTERS:
if char in variable:
invalid_chars.append(char)
new_var = new_var.replace(char, "")
# if variable is empty, then we should add that to the wrong variables
if not variable:
empty_variables.append(variable)
continue
# if variable starts with a number we should add that to the invalid chars
# and wrong variables
if variable[0].isdigit():
invalid_chars.append(variable[0])
new_var = new_var.replace(variable[0], "")
wrong_variables.append(variable)
else:
for char in INVALID_CHARACTERS:
if char in variable:
invalid_chars.append(char)
new_var = new_var.replace(char, "")
wrong_variables.append(variable)
fixed_variables.append(new_var)
if new_var != variable:
input_variables.remove(variable)
input_variables.append(new_var)
# If any of the input_variables is not in the fixed_variables, then it means that
# there are invalid characters in the input_variables
if any(var not in fixed_variables for var in input_variables):
raise ValueError(
f"Invalid input variables: {input_variables}. Please, use something like {fixed_variables} instead."
)
if any(var not in fixed_variables for var in input_variables):
error_message = build_error_message(
input_variables,
invalid_chars,
wrong_variables,
fixed_variables,
empty_variables,
)
raise ValueError(error_message)
return input_variables
def build_error_message(
input_variables, invalid_chars, wrong_variables, fixed_variables, empty_variables
):
input_variables_str = ", ".join([f"'{var}'" for var in input_variables])
error_string = f"Invalid input variables: {input_variables_str}. "
if wrong_variables and invalid_chars:
# fix the wrong variables replacing invalid chars and find them in the fixed variables
error_string_vars = "You can fix them by replacing the invalid characters: "
wvars = wrong_variables.copy()
for i, wrong_var in enumerate(wvars):
for char in invalid_chars:
wrong_var = wrong_var.replace(char, "")
if wrong_var in fixed_variables:
error_string_vars += f"'{wrong_variables[i]}' -> '{wrong_var}'"
error_string += error_string_vars
elif empty_variables:
error_string += f" There are {len(empty_variables)} empty variable{'s' if len(empty_variables) > 1 else ''}."
elif len(set(fixed_variables)) != len(fixed_variables):
error_string += "There are duplicate variables."
return error_string

View file

@ -1,22 +1,107 @@
import asyncio
from typing import Any
from langchain.callbacks.base import AsyncCallbackHandler, BaseCallbackHandler
from langflow.api.v1.schemas import ChatResponse
from typing import Any, Dict, List, Union
from fastapi import WebSocket
from langchain.schema import AgentAction, LLMResult, AgentFinish
# https://github.com/hwchase17/chat-langchain/blob/master/callback.py
class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
"""Callback handler for streaming LLM responses."""
def __init__(self, websocket):
def __init__(self, websocket: WebSocket):
self.websocket = websocket
async def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
resp = ChatResponse(message=token, type="stream", intermediate_steps="")
await self.websocket.send_json(resp.dict())
async def on_llm_start(
self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any
) -> Any:
"""Run when LLM starts running."""
async def on_llm_end(self, response: LLMResult, **kwargs: Any) -> Any:
"""Run when LLM ends running."""
async def on_llm_error(
self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
) -> Any:
"""Run when LLM errors."""
async def on_chain_start(
self, serialized: Dict[str, Any], inputs: Dict[str, Any], **kwargs: Any
) -> Any:
"""Run when chain starts running."""
async def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> Any:
"""Run when chain ends running."""
async def on_chain_error(
self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
) -> Any:
"""Run when chain errors."""
async def on_tool_start(
self, serialized: Dict[str, Any], input_str: str, **kwargs: Any
) -> Any:
"""Run when tool starts running."""
resp = ChatResponse(
message="",
type="stream",
intermediate_steps=f"Tool input: {input_str}",
)
await self.websocket.send_json(resp.dict())
async def on_tool_end(self, output: str, **kwargs: Any) -> Any:
"""Run when tool ends running."""
resp = ChatResponse(
message="",
type="stream",
intermediate_steps=f"Tool output: {output}",
)
await self.websocket.send_json(resp.dict())
async def on_tool_error(
self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
) -> Any:
"""Run when tool errors."""
async def on_text(self, text: str, **kwargs: Any) -> Any:
"""Run on arbitrary text."""
# This runs when first sending the prompt
# to the LLM, adding it will send the final prompt
# to the frontend
async def on_agent_action(self, action: AgentAction, **kwargs: Any):
log = f"Thought: {action.log}"
# if there are line breaks, split them and send them
# as separate messages
if "\n" in log:
logs = log.split("\n")
for log in logs:
resp = ChatResponse(message="", type="stream", intermediate_steps=log)
await self.websocket.send_json(resp.dict())
else:
resp = ChatResponse(message="", type="stream", intermediate_steps=log)
await self.websocket.send_json(resp.dict())
async def on_agent_finish(self, finish: AgentFinish, **kwargs: Any) -> Any:
"""Run on agent end."""
resp = ChatResponse(
message="",
type="stream",
intermediate_steps=finish.log,
)
await self.websocket.send_json(resp.dict())
class StreamingLLMCallbackHandler(BaseCallbackHandler):
"""Callback handler for streaming LLM responses."""

View file

@ -160,7 +160,13 @@ async def stream_build(flow_id: str):
input_keys_response = build_input_keys_response(
langchain_object, artifacts
)
yield str(StreamData(event="message", data=input_keys_response))
else:
input_keys_response = {
"input_keys": {},
"memory_keys": [],
"handle_keys": [],
}
yield str(StreamData(event="message", data=input_keys_response))
chat_manager.set_cache(flow_id, langchain_object)
# We need to reset the chat history

View file

@ -73,12 +73,14 @@ def add_new_variables_to_template(input_variables, prompt_request):
advanced=False,
multiline=True,
input_types=["Document", "BaseOutputParser"],
value="", # Set the value to empty string
)
if variable in prompt_request.frontend_node.template:
# Set the new field with the old value
template_field.value = prompt_request.frontend_node.template[variable][
"value"
]
prompt_request.frontend_node.template[variable] = template_field.to_dict()
# Check if variable is not already in the list before appending

View file

@ -23,7 +23,7 @@ async def process_graph(
try:
logger.debug("Generating result and thought")
result, intermediate_steps = await get_result_and_steps(
langchain_object, chat_inputs.message or "", websocket=websocket
langchain_object, chat_inputs.message, websocket=websocket
)
logger.debug("Generated result and intermediate_steps")
return result, intermediate_steps

View file

@ -164,8 +164,6 @@ memories:
prompts:
PromptTemplate:
documentation: "https://python.langchain.com/docs/modules/model_io/prompts/prompt_templates/"
ZeroShotPrompt:
documentation: "https://python.langchain.com/docs/modules/agents/how_to/custom_mrkl_agent"
textsplitters:
CharacterTextSplitter:
documentation: "https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/character_text_splitter"

View file

@ -2,9 +2,9 @@ from langflow.template import frontend_node
# These should always be instantiated
CUSTOM_NODES = {
"prompts": {
"ZeroShotPrompt": frontend_node.prompts.ZeroShotPromptNode(),
},
# "prompts": {
# "ZeroShotPrompt": frontend_node.prompts.ZeroShotPromptNode(),
# },
"tools": {
"PythonFunctionTool": frontend_node.tools.PythonFunctionToolNode(),
"PythonFunction": frontend_node.tools.PythonFunctionNode(),

View file

@ -183,6 +183,8 @@ class Vertex:
# and return the instance
try:
if self.base_type is None:
raise ValueError(f"Base type for node {self.vertex_type} not found")
result = loading.instantiate_class(
node_type=self.vertex_type,
base_type=self.base_type,
@ -224,4 +226,5 @@ class Vertex:
return id(self)
def _built_object_repr(self):
return repr(self._built_object)
# Add a message with an emoji, stars for sucess,
return "Built sucessfully ✨" if self._built_object else "Failed to build 😵‍💫"

View file

@ -201,6 +201,15 @@ class PromptVertex(Vertex):
self._build()
return self._built_object
def _built_object_repr(self):
if self.artifacts and hasattr(self._built_object, "format"):
# We'll build the prompt with the artifacts
# to show the user what the prompt looks like
# with the variables filled in
return self._built_object.format(**self.artifacts)
else:
return super()._built_object_repr()
class OutputParserVertex(Vertex):
def __init__(self, data: Dict):

View file

@ -6,13 +6,20 @@ from langflow.custom.customs import get_custom_nodes
from langflow.interface.agents.custom import CUSTOM_AGENTS
from langflow.interface.base import LangChainTypeCreator
from langflow.settings import settings
from langflow.template.frontend_node.agents import AgentFrontendNode
from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class
from langflow.utils.util import build_template_from_class, build_template_from_method
class AgentCreator(LangChainTypeCreator):
type_name: str = "agents"
from_method_nodes = {"ZeroShotAgent": "from_llm_and_tools"}
@property
def frontend_node_class(self) -> type[AgentFrontendNode]:
return AgentFrontendNode
@property
def type_to_loader_dict(self) -> Dict:
if self.type_dict is None:
@ -27,6 +34,13 @@ class AgentCreator(LangChainTypeCreator):
try:
if name in get_custom_nodes(self.type_name).keys():
return get_custom_nodes(self.type_name)[name]
elif name in self.from_method_nodes:
return build_template_from_method(
name,
type_to_cls_dict=self.type_to_loader_dict,
add_function=True,
method_name=self.from_method_nodes[name],
)
return build_template_from_class(
name, self.type_to_loader_dict, add_function=True
)

View file

@ -19,6 +19,8 @@ from langflow.interface.importing.utils import (
import_by_type,
)
from langflow.interface.custom_lists import CUSTOM_NODES
from langflow.interface.importing.utils import get_function, import_by_type
from langflow.interface.agents.base import agent_creator
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
@ -65,7 +67,7 @@ def convert_kwargs(params):
def instantiate_based_on_type(class_object, base_type, node_type, params):
if base_type == "agents":
return instantiate_agent(class_object, params)
return instantiate_agent(node_type, class_object, params)
elif base_type == "prompts":
return instantiate_prompt(node_type, class_object, params)
elif base_type == "tools":
@ -122,6 +124,12 @@ def instantiate_llm(node_type, class_object, params: Dict):
def instantiate_memory(node_type, class_object, params):
# process input_key and output_key to remove them if
# they are empty strings
for key in ["input_key", "output_key"]:
if key in params and not params[key]:
params.pop(key)
try:
if "retriever" in params and hasattr(params["retriever"], "as_retriever"):
params["retriever"] = params["retriever"].as_retriever()
@ -164,7 +172,16 @@ def instantiate_chains(node_type, class_object: Type[Chain], params: Dict):
return class_object(**params)
def instantiate_agent(class_object: Type[agent_module.Agent], params: Dict):
def instantiate_agent(node_type, class_object: Type[agent_module.Agent], params: Dict):
if node_type in agent_creator.from_method_nodes:
method = agent_creator.from_method_nodes[node_type]
if class_method := getattr(class_object, method, None):
agent = class_method(**params)
tools = params.get("tools", [])
return AgentExecutor.from_agent_and_tools(
agent=agent,
tools=tools,
)
return load_agent_executor(class_object, params)

View file

@ -90,7 +90,7 @@ class ToolCreator(LangChainTypeCreator):
def get_signature(self, name: str) -> Optional[Dict]:
"""Get the signature of a tool."""
base_classes = ["Tool"]
base_classes = ["Tool", "BaseTool"]
fields = []
params = []
tool_params = {}

View file

@ -13,6 +13,17 @@ NON_CHAT_AGENTS = {
}
class AgentFrontendNode(FrontendNode):
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
if field.name in ["suffix", "prefix"]:
field.show = True
if field.name == "Tools" and name == "ZeroShotAgent":
# field.
field.type_name = "BaseTool"
field.is_list = True
class SQLAgentNode(FrontendNode):
name: str = "SQLAgent"
template: Template = Template(

View file

@ -56,7 +56,7 @@ class ToolNode(FrontendNode):
],
)
description: str = "Converts a chain, agent or function into a tool."
base_classes: list[str] = ["Tool"]
base_classes: list[str] = ["Tool", "BaseTool"]
def to_dict(self):
return super().to_dict()

View file

@ -243,7 +243,11 @@ def format_dict(d, name: Optional[str] = None):
# Check for list type
if "List" in _type or "Sequence" in _type or "Set" in _type:
_type = _type.replace("List[", "")[:-1]
_type = (
_type.replace("List[", "")
.replace("Sequence[", "")
.replace("Set[", "")[:-1]
)
value["list"] = True
else:
value["list"] = False