Merge branch 'python_custom_node_component' of github.com:logspace-ai/langflow into python_custom_node_component
This commit is contained in:
commit
1ea05b3584
92 changed files with 4582 additions and 1520 deletions
|
|
@ -1,7 +1,7 @@
|
|||
from importlib import metadata
|
||||
from langflow.cache import cache_manager
|
||||
from langflow.processing.process import load_flow_from_json
|
||||
from langflow.utils.types import Prompt
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
try:
|
||||
__version__ = metadata.version(__package__)
|
||||
|
|
@ -10,4 +10,4 @@ except metadata.PackageNotFoundError:
|
|||
__version__ = ""
|
||||
del metadata # optional, avoids polluting the results of dir(__package__)
|
||||
|
||||
__all__ = ["load_flow_from_json", "cache_manager", "Prompt"]
|
||||
__all__ = ["load_flow_from_json", "cache_manager", "CustomComponent"]
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ import ast
|
|||
import inspect
|
||||
import traceback
|
||||
|
||||
from typing import Dict, Any, Type, Union
|
||||
from typing import Dict, Any, List, Type, Union
|
||||
from fastapi import HTTPException
|
||||
from langflow.interface.custom.schema import CallableCodeDetails, ClassCodeDetails
|
||||
|
||||
|
||||
class CodeSyntaxError(HTTPException):
|
||||
|
|
@ -54,13 +55,13 @@ class CodeParser:
|
|||
|
||||
return tree
|
||||
|
||||
def parse_node(self, node: ast.AST) -> None:
|
||||
def parse_node(self, node: Union[ast.stmt, ast.AST]) -> None:
|
||||
"""
|
||||
Parses an AST node and updates the data
|
||||
dictionary with the relevant information.
|
||||
"""
|
||||
if handler := self.handlers.get(type(node)):
|
||||
handler(node)
|
||||
if handler := self.handlers.get(type(node)): # type: ignore
|
||||
handler(node) # type: ignore
|
||||
|
||||
def parse_imports(self, node: Union[ast.Import, ast.ImportFrom]) -> None:
|
||||
"""
|
||||
|
|
@ -92,27 +93,73 @@ class CodeParser:
|
|||
"""
|
||||
Extracts details from a single function or method node.
|
||||
"""
|
||||
func = {
|
||||
"name": node.name,
|
||||
"doc": ast.get_docstring(node),
|
||||
"args": [],
|
||||
"body": [],
|
||||
"return_type": ast.unparse(node.returns) if node.returns else None,
|
||||
}
|
||||
func = CallableCodeDetails(
|
||||
name=node.name,
|
||||
doc=ast.get_docstring(node),
|
||||
args=[],
|
||||
body=[],
|
||||
return_type=ast.unparse(node.returns) if node.returns else None,
|
||||
)
|
||||
|
||||
# Handle positional arguments with default values
|
||||
defaults = [None] * (len(node.args.args) - len(node.args.defaults)) + [
|
||||
ast.unparse(default) if default else None for default in node.args.defaults
|
||||
func.args = self.parse_function_args(node)
|
||||
func.body = self.parse_function_body(node)
|
||||
|
||||
return func.dict()
|
||||
|
||||
def parse_function_args(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Parses the arguments of a function or method node.
|
||||
"""
|
||||
args = []
|
||||
|
||||
args += self.parse_positional_args(node)
|
||||
args += self.parse_varargs(node)
|
||||
args += self.parse_keyword_args(node)
|
||||
args += self.parse_kwargs(node)
|
||||
|
||||
return args
|
||||
|
||||
def parse_positional_args(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Parses the positional arguments of a function or method node.
|
||||
"""
|
||||
num_args = len(node.args.args)
|
||||
num_defaults = len(node.args.defaults)
|
||||
num_missing_defaults = num_args - num_defaults
|
||||
missing_defaults = [None] * num_missing_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
|
||||
# are any "None" values in the middle
|
||||
default_values = [
|
||||
None if value == "None" else value for value in default_values
|
||||
]
|
||||
|
||||
for arg, default in zip(node.args.args, defaults):
|
||||
func["args"].append(self.parse_arg(arg, default))
|
||||
defaults = missing_defaults + default_values
|
||||
|
||||
args = [
|
||||
self.parse_arg(arg, default)
|
||||
for arg, default in zip(node.args.args, defaults)
|
||||
]
|
||||
return args
|
||||
|
||||
def parse_varargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Parses the *args argument of a function or method node.
|
||||
"""
|
||||
args = []
|
||||
|
||||
# Handle *args
|
||||
if node.args.vararg:
|
||||
func["args"].append(self.parse_arg(node.args.vararg, None))
|
||||
args.append(self.parse_arg(node.args.vararg, None))
|
||||
|
||||
# Handle keyword-only arguments with default values
|
||||
return args
|
||||
|
||||
def parse_keyword_args(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Parses the keyword-only arguments of a function or method node.
|
||||
"""
|
||||
kw_defaults = [None] * (
|
||||
len(node.args.kwonlyargs) - len(node.args.kw_defaults)
|
||||
) + [
|
||||
|
|
@ -120,16 +167,28 @@ class CodeParser:
|
|||
for default in node.args.kw_defaults
|
||||
]
|
||||
|
||||
for arg, default in zip(node.args.kwonlyargs, kw_defaults):
|
||||
func["args"].append(self.parse_arg(arg, default))
|
||||
args = [
|
||||
self.parse_arg(arg, default)
|
||||
for arg, default in zip(node.args.kwonlyargs, kw_defaults)
|
||||
]
|
||||
return args
|
||||
|
||||
def parse_kwargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Parses the **kwargs argument of a function or method node.
|
||||
"""
|
||||
args = []
|
||||
|
||||
# Handle **kwargs
|
||||
if node.args.kwarg:
|
||||
func["args"].append(self.parse_arg(node.args.kwarg, None))
|
||||
args.append(self.parse_arg(node.args.kwarg, None))
|
||||
|
||||
for line in node.body:
|
||||
func["body"].append(ast.unparse(line))
|
||||
return func
|
||||
return args
|
||||
|
||||
def parse_function_body(self, node: ast.FunctionDef) -> List[str]:
|
||||
"""
|
||||
Parses the body of a function or method node.
|
||||
"""
|
||||
return [ast.unparse(line) for line in node.body]
|
||||
|
||||
def parse_assign(self, stmt):
|
||||
"""
|
||||
|
|
@ -164,29 +223,31 @@ class CodeParser:
|
|||
"""
|
||||
Extracts "classes" from the code, including inheritance and init methods.
|
||||
"""
|
||||
class_dict = {
|
||||
"name": node.name,
|
||||
"doc": ast.get_docstring(node),
|
||||
"bases": [ast.unparse(base) for base in node.bases],
|
||||
"attributes": [],
|
||||
"methods": [],
|
||||
}
|
||||
|
||||
class_details = ClassCodeDetails(
|
||||
name=node.name,
|
||||
doc=ast.get_docstring(node),
|
||||
bases=[ast.unparse(base) for base in node.bases],
|
||||
attributes=[],
|
||||
methods=[],
|
||||
init=None,
|
||||
)
|
||||
|
||||
for stmt in node.body:
|
||||
if isinstance(stmt, ast.Assign):
|
||||
if attr := self.parse_assign(stmt):
|
||||
class_dict["attributes"].append(attr)
|
||||
class_details.attributes.append(attr)
|
||||
elif isinstance(stmt, ast.AnnAssign):
|
||||
if attr := self.parse_ann_assign(stmt):
|
||||
class_dict["attributes"].append(attr)
|
||||
class_details.attributes.append(attr)
|
||||
elif isinstance(stmt, ast.FunctionDef):
|
||||
method, is_init = self.parse_function_def(stmt)
|
||||
if is_init:
|
||||
class_dict["init"] = method
|
||||
class_details.init = method
|
||||
else:
|
||||
class_dict["methods"].append(method)
|
||||
class_details.methods.append(method)
|
||||
|
||||
self.data["classes"].append(class_dict)
|
||||
self.data["classes"].append(class_details.dict())
|
||||
|
||||
def parse_global_vars(self, node: ast.Assign) -> None:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import ast
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from fastapi import HTTPException
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ class Component(BaseModel):
|
|||
"The name of the entrypoint function must be provided."
|
||||
)
|
||||
|
||||
code: str
|
||||
code: Optional[str]
|
||||
function_entrypoint_name = "build"
|
||||
field_config: dict = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,13 +20,22 @@ LANGCHAIN_BASE_TYPES = {
|
|||
"VectorStore": VectorStore,
|
||||
"Embeddings": Embeddings,
|
||||
"BaseRetriever": BaseRetriever,
|
||||
}
|
||||
|
||||
# Langchain base types plus Python base types
|
||||
CUSTOM_COMPONENT_SUPPORTED_TYPES = {
|
||||
**LANGCHAIN_BASE_TYPES,
|
||||
"str": str,
|
||||
"int": int,
|
||||
"float": float,
|
||||
"bool": bool,
|
||||
"list": list,
|
||||
"dict": dict,
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_CUSTOM_COMPONENT_CODE = """
|
||||
from langflow import Prompt
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow import CustomComponent
|
||||
|
||||
from langchain.llms.base import BaseLLM
|
||||
from langchain.chains import LLMChain
|
||||
|
|
@ -38,11 +47,12 @@ import requests
|
|||
class YourComponent(CustomComponent):
|
||||
display_name: str = "Your Component"
|
||||
description: str = "Your description"
|
||||
field_config = { "url": { "multiline": True, "required": True } }
|
||||
|
||||
def build(self, url: str, llm: BaseLLM, template: Prompt) -> Document:
|
||||
def build_config(self):
|
||||
return { "url": { "multiline": True, "required": True } }
|
||||
|
||||
def build(self, url: str, llm: BaseLLM, prompt: PromptTemplate) -> Document:
|
||||
response = requests.get(url)
|
||||
prompt = PromptTemplate.from_template(template)
|
||||
chain = LLMChain(llm=llm, prompt=prompt)
|
||||
result = chain.run(response.text[:300])
|
||||
return Document(page_content=str(result))
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
from typing import Callable, Optional
|
||||
from typing import Any, Callable, List, Optional
|
||||
from fastapi import HTTPException
|
||||
from langflow.interface.custom.constants import LANGCHAIN_BASE_TYPES
|
||||
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
|
||||
from langflow.interface.custom.component import Component
|
||||
|
||||
from langflow.utils import validate
|
||||
|
||||
from uuid import UUID
|
||||
from langflow.database.base import session_getter
|
||||
from langflow.database.models.flow import Flow
|
||||
from pydantic import Extra
|
||||
|
|
@ -17,14 +16,14 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
code_class_base_inheritance = "CustomComponent"
|
||||
function_entrypoint_name = "build"
|
||||
function: Optional[Callable] = None
|
||||
return_type_valid_list = list(LANGCHAIN_BASE_TYPES.keys())
|
||||
return_type_valid_list = list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys())
|
||||
repr_value: Optional[str] = ""
|
||||
|
||||
def __init__(self, **data):
|
||||
super().__init__(**data)
|
||||
|
||||
def custom_repr(self):
|
||||
return self.repr_value
|
||||
return str(self.repr_value)
|
||||
|
||||
def build_config(self):
|
||||
return self.field_config
|
||||
|
|
@ -44,13 +43,15 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
return True
|
||||
|
||||
def is_check_valid(self) -> bool:
|
||||
return self._class_template_validation(self.code)
|
||||
return self._class_template_validation(self.code) if self.code else False
|
||||
|
||||
def get_code_tree(self, code: str):
|
||||
return super().get_code_tree(code)
|
||||
|
||||
@property
|
||||
def get_function_entrypoint_args(self) -> str:
|
||||
if not self.code:
|
||||
return ""
|
||||
tree = self.get_code_tree(self.code)
|
||||
|
||||
component_classes = [
|
||||
|
|
@ -78,6 +79,8 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
|
||||
@property
|
||||
def get_function_entrypoint_return_type(self) -> str:
|
||||
if not self.code:
|
||||
return ""
|
||||
tree = self.get_code_tree(self.code)
|
||||
|
||||
component_classes = [
|
||||
|
|
@ -138,16 +141,19 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
def get_function(self):
|
||||
return validate.create_function(self.code, self.function_entrypoint_name)
|
||||
|
||||
def load_flow(self, flow_id: UUID = None):
|
||||
def load_flow(self, flow_id: str, tweaks: Optional[dict] = None) -> Any:
|
||||
from langflow.processing.process import build_sorted_vertices_with_caching
|
||||
from langflow.processing.process import process_tweaks
|
||||
|
||||
with session_getter() as session:
|
||||
data_graph = flow.data if (flow := session.get(Flow, flow_id)) else None
|
||||
if not data_graph:
|
||||
graph_data = flow.data if (flow := session.get(Flow, flow_id)) else None
|
||||
if not graph_data:
|
||||
raise ValueError(f"Flow {flow_id} not found")
|
||||
return build_sorted_vertices_with_caching(data_graph)
|
||||
if tweaks:
|
||||
graph_data = process_tweaks(graph_data=graph_data, tweaks=tweaks)
|
||||
return build_sorted_vertices_with_caching(graph_data)
|
||||
|
||||
def list_flows(self):
|
||||
def list_flows(self) -> List[Flow]:
|
||||
with session_getter() as session:
|
||||
flows = session.query(Flow).all()
|
||||
return flows
|
||||
|
|
|
|||
29
src/backend/langflow/interface/custom/schema.py
Normal file
29
src/backend/langflow/interface/custom/schema.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class ClassCodeDetails(BaseModel):
|
||||
"""
|
||||
A dataclass for storing details about a class.
|
||||
"""
|
||||
|
||||
name: str
|
||||
doc: Optional[str]
|
||||
bases: list
|
||||
attributes: list
|
||||
methods: list
|
||||
init: Optional[dict] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class CallableCodeDetails(BaseModel):
|
||||
"""
|
||||
A dataclass for storing details about a callable.
|
||||
"""
|
||||
|
||||
name: str
|
||||
doc: Optional[str]
|
||||
args: list
|
||||
body: list
|
||||
return_type: Optional[str]
|
||||
|
|
@ -1,17 +1,15 @@
|
|||
import contextlib
|
||||
import json
|
||||
from typing import Any, Callable, Dict, List, Sequence, Type
|
||||
from typing import Any, Callable, Dict, Sequence, Type
|
||||
|
||||
from langchain.agents import ZeroShotAgent
|
||||
from langchain.agents import agent as agent_module
|
||||
from langchain.agents.agent import AgentExecutor
|
||||
from langchain.agents.agent_toolkits.base import BaseToolkit
|
||||
from langchain.agents.tools import BaseTool
|
||||
from langflow.interface.initialize.llm import initialize_vertexai
|
||||
from langflow.interface.initialize.utils import handle_format_kwargs, handle_node_type
|
||||
|
||||
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 (
|
||||
|
|
@ -212,68 +210,8 @@ def instantiate_agent(node_type, class_object: Type[agent_module.Agent], params:
|
|||
|
||||
|
||||
def instantiate_prompt(node_type, class_object, params: Dict):
|
||||
if node_type == "ZeroShotPrompt":
|
||||
if "tools" not in params:
|
||||
params["tools"] = []
|
||||
return ZeroShotAgent.create_prompt(**params)
|
||||
elif "MessagePromptTemplate" in node_type:
|
||||
# Then we only need the template
|
||||
from_template_params = {
|
||||
"template": params.pop("prompt", params.pop("template", ""))
|
||||
}
|
||||
|
||||
if not from_template_params.get("template"):
|
||||
raise ValueError("Prompt template is required")
|
||||
prompt = class_object.from_template(**from_template_params)
|
||||
|
||||
elif node_type == "ChatPromptTemplate":
|
||||
prompt = class_object.from_messages(**params)
|
||||
else:
|
||||
prompt = class_object(**params)
|
||||
|
||||
format_kwargs: Dict[str, Any] = {}
|
||||
for input_variable in prompt.input_variables:
|
||||
if input_variable in params:
|
||||
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()
|
||||
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
|
||||
if len(variable) > 1:
|
||||
content = "\n".join(
|
||||
[item.page_content for item in variable if item.page_content]
|
||||
)
|
||||
else:
|
||||
content = variable[0].page_content
|
||||
# content could be a json list of strings
|
||||
with contextlib.suppress(json.JSONDecodeError):
|
||||
content = json.loads(content)
|
||||
if isinstance(content, list):
|
||||
content = ",".join([str(item) for item in content])
|
||||
format_kwargs[input_variable] = content
|
||||
# handle_keys will be a list but it does not exist yet
|
||||
# so we need to create it
|
||||
|
||||
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"] = []
|
||||
|
||||
# Add the handle_keys to the list
|
||||
format_kwargs["handle_keys"].append(input_variable)
|
||||
|
||||
params, prompt = handle_node_type(node_type, class_object, params)
|
||||
format_kwargs = handle_format_kwargs(prompt, params)
|
||||
return prompt, format_kwargs
|
||||
|
||||
|
||||
|
|
|
|||
103
src/backend/langflow/interface/initialize/utils.py
Normal file
103
src/backend/langflow/interface/initialize/utils.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import contextlib
|
||||
import json
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from langchain.agents import ZeroShotAgent
|
||||
|
||||
|
||||
from langchain.schema import Document, BaseOutputParser
|
||||
|
||||
|
||||
def handle_node_type(node_type, class_object, params: Dict):
|
||||
if node_type == "ZeroShotPrompt":
|
||||
params = check_tools_in_params(params)
|
||||
prompt = ZeroShotAgent.create_prompt(**params)
|
||||
elif "MessagePromptTemplate" in node_type:
|
||||
prompt = instantiate_from_template(class_object, params)
|
||||
elif node_type == "ChatPromptTemplate":
|
||||
prompt = class_object.from_messages(**params)
|
||||
else:
|
||||
prompt = class_object(**params)
|
||||
return params, prompt
|
||||
|
||||
|
||||
def check_tools_in_params(params: Dict):
|
||||
if "tools" not in params:
|
||||
params["tools"] = []
|
||||
return params
|
||||
|
||||
|
||||
def instantiate_from_template(class_object, params: Dict):
|
||||
from_template_params = {
|
||||
"template": params.pop("prompt", params.pop("template", ""))
|
||||
}
|
||||
if not from_template_params.get("template"):
|
||||
raise ValueError("Prompt template is required")
|
||||
return class_object.from_template(**from_template_params)
|
||||
|
||||
|
||||
def handle_format_kwargs(prompt, params: Dict):
|
||||
format_kwargs: Dict[str, Any] = {}
|
||||
for input_variable in prompt.input_variables:
|
||||
if input_variable in params:
|
||||
format_kwargs = handle_variable(params, input_variable, format_kwargs)
|
||||
return format_kwargs
|
||||
|
||||
|
||||
def handle_variable(params: Dict, input_variable: str, format_kwargs: Dict):
|
||||
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()
|
||||
elif is_instance_of_list_or_document(variable):
|
||||
format_kwargs = format_document(variable, input_variable, format_kwargs)
|
||||
if needs_handle_keys(variable):
|
||||
format_kwargs = add_handle_keys(input_variable, format_kwargs)
|
||||
return format_kwargs
|
||||
|
||||
|
||||
def is_instance_of_list_or_document(variable):
|
||||
return (
|
||||
isinstance(variable, List)
|
||||
and all(isinstance(item, Document) for item in variable)
|
||||
or isinstance(variable, Document)
|
||||
)
|
||||
|
||||
|
||||
def format_document(variable, input_variable: str, format_kwargs: Dict):
|
||||
variable = variable if isinstance(variable, List) else [variable]
|
||||
content = format_content(variable)
|
||||
format_kwargs[input_variable] = content
|
||||
return format_kwargs
|
||||
|
||||
|
||||
def format_content(variable):
|
||||
if len(variable) > 1:
|
||||
return "\n".join([item.page_content for item in variable if item.page_content])
|
||||
content = variable[0].page_content
|
||||
return try_to_load_json(content)
|
||||
|
||||
|
||||
def try_to_load_json(content):
|
||||
with contextlib.suppress(json.JSONDecodeError):
|
||||
content = json.loads(content)
|
||||
if isinstance(content, list):
|
||||
content = ",".join([str(item) for item in content])
|
||||
return content
|
||||
|
||||
|
||||
def needs_handle_keys(variable):
|
||||
return is_instance_of_list_or_document(variable) or (
|
||||
isinstance(variable, BaseOutputParser)
|
||||
and hasattr(variable, "get_format_instructions")
|
||||
)
|
||||
|
||||
|
||||
def add_handle_keys(input_variable: str, format_kwargs: Dict):
|
||||
if "handle_keys" not in format_kwargs:
|
||||
format_kwargs["handle_keys"] = []
|
||||
format_kwargs["handle_keys"].append(input_variable)
|
||||
return format_kwargs
|
||||
|
|
@ -55,7 +55,7 @@ TOOL_INPUTS = {
|
|||
show=True,
|
||||
value="",
|
||||
suffixes=[".json", ".yaml", ".yml"],
|
||||
fileTypes=["json", "yaml", "yml"],
|
||||
file_types=["json", "yaml", "yml"],
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,29 +48,3 @@ class PythonFunctionTool(Function, Tool):
|
|||
|
||||
class PythonFunction(Function):
|
||||
code: str
|
||||
|
||||
|
||||
class CustomComponent_old(BaseModel):
|
||||
code: str
|
||||
function: Optional[Callable] = None
|
||||
imports: Optional[str] = None
|
||||
|
||||
# Eval code and store the class
|
||||
def __init__(self, **data):
|
||||
super().__init__(**data)
|
||||
|
||||
# Validate the Class code
|
||||
@validator("code")
|
||||
def validate_func(cls, v):
|
||||
try:
|
||||
validate.eval_function(v)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
return v
|
||||
|
||||
def get_function(self):
|
||||
"""Get the function"""
|
||||
function_name = validate.extract_function_name(self.code)
|
||||
|
||||
return validate.create_function(self.code, function_name)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from langflow.interface.agents.base import agent_creator
|
||||
from langflow.interface.chains.base import chain_creator
|
||||
from langflow.interface.custom.constants import LANGCHAIN_BASE_TYPES
|
||||
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
|
||||
from langflow.interface.document_loaders.base import documentloader_creator
|
||||
from langflow.interface.embeddings.base import embedding_creator
|
||||
from langflow.interface.importing.utils import get_function_custom
|
||||
|
|
@ -98,6 +98,13 @@ def add_new_custom_field(
|
|||
display_name = field_config.pop("display_name", field_name)
|
||||
field_type = field_config.pop("field_type", field_type)
|
||||
field_type = process_type(field_type)
|
||||
field_value = field_config.pop("value", field_value)
|
||||
field_advanced = field_config.pop("advanced", False)
|
||||
|
||||
# If options is a list, then it's a dropdown
|
||||
# If options is None, then it's a list of strings
|
||||
is_list = isinstance(field_config.get("options"), list)
|
||||
field_config["is_list"] = is_list or field_config.get("is_list", False)
|
||||
|
||||
if "name" in field_config:
|
||||
warnings.warn(
|
||||
|
|
@ -114,7 +121,7 @@ def add_new_custom_field(
|
|||
value=field_value,
|
||||
show=True,
|
||||
required=required,
|
||||
advanced=False,
|
||||
advanced=field_advanced,
|
||||
placeholder=placeholder,
|
||||
display_name=display_name,
|
||||
**field_config,
|
||||
|
|
@ -126,8 +133,9 @@ def add_new_custom_field(
|
|||
|
||||
|
||||
# TODO: Move to correct place
|
||||
def add_code_field(template, raw_code):
|
||||
def add_code_field(template, raw_code, field_config):
|
||||
# Field with the Python code to allow update
|
||||
|
||||
code_field = {
|
||||
"code": {
|
||||
"dynamic": True,
|
||||
|
|
@ -138,7 +146,7 @@ def add_code_field(template, raw_code):
|
|||
"value": raw_code,
|
||||
"password": False,
|
||||
"name": "code",
|
||||
"advanced": False,
|
||||
"advanced": field_config.pop("advanced", False),
|
||||
"type": "code",
|
||||
"list": False,
|
||||
}
|
||||
|
|
@ -183,22 +191,30 @@ def update_display_name_and_description(frontend_node, template_config):
|
|||
frontend_node["description"] = template_config["description"]
|
||||
|
||||
|
||||
def build_field_config(custom_component):
|
||||
def build_field_config(custom_component: CustomComponent):
|
||||
"""Build the field configuration for a custom component"""
|
||||
|
||||
try:
|
||||
custom_class = get_function_custom(custom_component.code)
|
||||
return custom_class().build_config()
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"Error while building field config: {exc}")
|
||||
logger.error(f"Error while getting custom function: {str(exc)}")
|
||||
return {}
|
||||
|
||||
try:
|
||||
return custom_class().build_config()
|
||||
except Exception as exc:
|
||||
logger.error(f"Error while building field config: {str(exc)}")
|
||||
return {}
|
||||
|
||||
|
||||
def add_extra_fields(frontend_node, field_config, function_args):
|
||||
"""Add extra fields to the frontend node"""
|
||||
if function_args is None:
|
||||
if function_args is None or function_args == "":
|
||||
return
|
||||
|
||||
# sort function_args which is a list of dicts
|
||||
function_args.sort(key=lambda x: x["name"])
|
||||
|
||||
for extra_field in function_args:
|
||||
if "name" not in extra_field or extra_field["name"] == "self":
|
||||
continue
|
||||
|
|
@ -232,19 +248,19 @@ def get_field_properties(extra_field):
|
|||
|
||||
def add_base_classes(frontend_node, return_type):
|
||||
"""Add base classes to the frontend node"""
|
||||
if return_type not in LANGCHAIN_BASE_TYPES or return_type is None:
|
||||
if return_type not in CUSTOM_COMPONENT_SUPPORTED_TYPES or return_type is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": (
|
||||
"Invalid return type should be one of: "
|
||||
f"{list(LANGCHAIN_BASE_TYPES.keys())}"
|
||||
f"{list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys())}"
|
||||
),
|
||||
"traceback": traceback.format_exc(),
|
||||
},
|
||||
)
|
||||
|
||||
return_type_instance = LANGCHAIN_BASE_TYPES.get(return_type)
|
||||
return_type_instance = CUSTOM_COMPONENT_SUPPORTED_TYPES.get(return_type)
|
||||
base_classes = get_base_classes(return_type_instance)
|
||||
|
||||
for base_class in base_classes:
|
||||
|
|
@ -268,7 +284,9 @@ def build_langchain_template_custom_component(custom_component: CustomComponent)
|
|||
frontend_node, field_config, custom_component.get_function_entrypoint_args
|
||||
)
|
||||
|
||||
frontend_node = add_code_field(frontend_node, custom_component.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
|
||||
|
|
@ -287,8 +305,8 @@ def load_files_from_path(path: str):
|
|||
def build_and_validate_all_files(reader, file_list):
|
||||
"""Build and validate all files"""
|
||||
data = reader.build_component_menu_list(file_list)
|
||||
valid_components = reader.filter_loaded_components(data=data, with_errors=False)
|
||||
|
||||
valid_components = reader.filter_loaded_components(data=data, with_errors=False)
|
||||
invalid_components = reader.filter_loaded_components(data=data, with_errors=True)
|
||||
|
||||
return valid_components, invalid_components
|
||||
|
|
@ -341,12 +359,15 @@ def build_invalid_menu(invalid_components):
|
|||
.get(type(CustomComponent()).__name__)
|
||||
)
|
||||
|
||||
component_template["error"] = component.get("error", None)
|
||||
component_template.get("template").get("code")["value"] = component_code
|
||||
|
||||
invalid_menu[menu_name][component_name] = component_template
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"Error while creating custom component: {exc}")
|
||||
logger.error(
|
||||
f"Error while creating custom component [{component_name}]: {str(exc)}"
|
||||
)
|
||||
|
||||
return invalid_menu
|
||||
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ class CSVAgentNode(FrontendNode):
|
|||
name="path",
|
||||
value="",
|
||||
suffixes=[".csv"],
|
||||
fileTypes=["csv"],
|
||||
file_types=["csv"],
|
||||
),
|
||||
TemplateField(
|
||||
field_type="BaseLanguageModel",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ class FrontendNode(BaseModel):
|
|||
output_types: List[str] = []
|
||||
field_formatters: FieldFormatters = Field(default_factory=FieldFormatters)
|
||||
beta: bool = False
|
||||
error: Optional[str] = None
|
||||
|
||||
# field formatters is an instance attribute but it is not used in the class
|
||||
# so we need to create a method to get it
|
||||
|
|
@ -85,6 +86,7 @@ class FrontendNode(BaseModel):
|
|||
"output_types": self.output_types,
|
||||
"documentation": self.documentation,
|
||||
"beta": self.beta,
|
||||
"error": self.error,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ FORCE_SHOW_FIELDS = [
|
|||
"headers",
|
||||
"max_value_length",
|
||||
"max_tokens",
|
||||
"google_cse_id",
|
||||
]
|
||||
|
||||
DEFAULT_PROMPT = """
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ def build_file_field(
|
|||
name=name,
|
||||
value="",
|
||||
suffixes=suffixes,
|
||||
fileTypes=fileTypes,
|
||||
file_types=fileTypes,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class LLMFrontendNode(FrontendNode):
|
|||
name="credentials",
|
||||
value="",
|
||||
suffixes=[".json"],
|
||||
fileTypes=["json"],
|
||||
file_types=["json"],
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from docstring_parser import parse # type: ignore
|
|||
from langflow.template.frontend_node.constants import FORCE_SHOW_FIELDS
|
||||
from langflow.utils import constants
|
||||
from langflow.utils.logger import logger
|
||||
from multiprocess import cpu_count
|
||||
from multiprocess import cpu_count # type: ignore
|
||||
|
||||
|
||||
def build_template_from_function(
|
||||
|
|
@ -301,13 +301,15 @@ def get_type(value: Any) -> Union[str, type]:
|
|||
return _type if isinstance(_type, str) else _type.__name__
|
||||
|
||||
|
||||
def remove_optional_wrapper(_type: str) -> str:
|
||||
def remove_optional_wrapper(_type: Union[str, type]) -> str:
|
||||
"""
|
||||
Removes the 'Optional' wrapper from the type string.
|
||||
|
||||
Returns:
|
||||
The type string with the 'Optional' wrapper removed.
|
||||
"""
|
||||
if isinstance(_type, type):
|
||||
_type = str(_type)
|
||||
if "Optional" in _type:
|
||||
_type = _type.replace("Optional[", "")[:-1]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue