Fix merge conflicts in backend code
This commit is contained in:
parent
06f0e0e0f8
commit
c57aadbbc6
6 changed files with 805 additions and 194 deletions
|
|
@ -2,14 +2,17 @@ from datetime import datetime
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlmodel import Session, select
|
|
||||||
|
|
||||||
from langflow.services.auth import utils as auth_utils
|
from langflow.services.auth import utils as auth_utils
|
||||||
from langflow.services.auth.utils import get_current_active_user
|
from langflow.services.auth.utils import get_current_active_user
|
||||||
from langflow.services.database.models.credential import Credential, CredentialCreate, CredentialRead, CredentialUpdate
|
from langflow.services.database.models.credential import (
|
||||||
from langflow.services.database.models.user.model import User
|
Credential,
|
||||||
|
CredentialCreate,
|
||||||
|
CredentialRead,
|
||||||
|
CredentialUpdate,
|
||||||
|
)
|
||||||
from langflow.services.database.models.user.model import User
|
from langflow.services.database.models.user.model import User
|
||||||
from langflow.services.deps import get_session, get_settings_service
|
from langflow.services.deps import get_session, get_settings_service
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
router = APIRouter(prefix="/credentials", tags=["Credentials"])
|
router = APIRouter(prefix="/credentials", tags=["Credentials"])
|
||||||
|
|
||||||
|
|
@ -26,7 +29,10 @@ def create_credential(
|
||||||
try:
|
try:
|
||||||
# check if credential name already exists
|
# check if credential name already exists
|
||||||
credential_exists = session.exec(
|
credential_exists = session.exec(
|
||||||
select(Credential).where(Credential.name == credential.name, Credential.user_id == current_user.id)
|
select(Credential).where(
|
||||||
|
Credential.name == credential.name,
|
||||||
|
Credential.user_id == current_user.id,
|
||||||
|
)
|
||||||
).first()
|
).first()
|
||||||
if credential_exists:
|
if credential_exists:
|
||||||
raise HTTPException(status_code=400, detail="Credential name already exists")
|
raise HTTPException(status_code=400, detail="Credential name already exists")
|
||||||
|
|
@ -105,7 +111,6 @@ def delete_credential(
|
||||||
).first()
|
).first()
|
||||||
if not db_credential:
|
if not db_credential:
|
||||||
raise HTTPException(status_code=404, detail="Credential not found")
|
raise HTTPException(status_code=404, detail="Credential not found")
|
||||||
|
|
||||||
session.delete(db_credential)
|
session.delete(db_credential)
|
||||||
session.commit()
|
session.commit()
|
||||||
return db_credential
|
return db_credential
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,49 @@
|
||||||
import ast
|
import ast
|
||||||
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
import types
|
import types
|
||||||
from typing import TYPE_CHECKING, Any, Coroutine, Dict, List, Optional
|
from enum import Enum
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
AsyncIterator,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
Iterator,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
)
|
||||||
|
|
||||||
from langflow.graph.utils import UnbuiltObject
|
from langflow.graph.schema import (
|
||||||
|
INPUT_COMPONENTS,
|
||||||
|
INPUT_FIELD_NAME,
|
||||||
|
OUTPUT_COMPONENTS,
|
||||||
|
InterfaceComponentTypes,
|
||||||
|
ResultData,
|
||||||
|
)
|
||||||
|
from langflow.graph.utils import UnbuiltObject, UnbuiltResult
|
||||||
|
from langflow.graph.vertex.utils import generate_result
|
||||||
from langflow.interface.initialize import loading
|
from langflow.interface.initialize import loading
|
||||||
from langflow.interface.listing import lazy_load_dict
|
from langflow.interface.listing import lazy_load_dict
|
||||||
|
from langflow.services.deps import get_storage_service
|
||||||
from langflow.utils.constants import DIRECT_TYPES
|
from langflow.utils.constants import DIRECT_TYPES
|
||||||
from langflow.utils.util import sync_to_async
|
from langflow.utils.schemas import ChatOutputResponse
|
||||||
|
from langflow.utils.util import sync_to_async, unescape_string
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from langflow.graph.edge.base import Edge
|
from langflow.graph.edge.base import ContractEdge
|
||||||
from langflow.graph.graph.base import Graph
|
from langflow.graph.graph.base import Graph
|
||||||
|
|
||||||
|
|
||||||
|
class VertexStates(str, Enum):
|
||||||
|
"""Vertex are related to it being active, inactive, or in an error state."""
|
||||||
|
|
||||||
|
ACTIVE = "active"
|
||||||
|
INACTIVE = "inactive"
|
||||||
|
ERROR = "error"
|
||||||
|
|
||||||
|
|
||||||
class Vertex:
|
class Vertex:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
@ -24,25 +53,109 @@ class Vertex:
|
||||||
is_task: bool = False,
|
is_task: bool = False,
|
||||||
params: Optional[Dict] = None,
|
params: Optional[Dict] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.graph = graph
|
# is_external means that the Vertex send or receives data from
|
||||||
|
# an external source (e.g the chat)
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
|
self.will_stream = False
|
||||||
|
self.updated_raw_params = False
|
||||||
self.id: str = data["id"]
|
self.id: str = data["id"]
|
||||||
|
self.is_state = False
|
||||||
|
self.is_input = any(input_component_name in self.id for input_component_name in INPUT_COMPONENTS)
|
||||||
|
self.is_output = any(output_component_name in self.id for output_component_name in OUTPUT_COMPONENTS)
|
||||||
|
self.has_session_id = None
|
||||||
|
self._custom_component = None
|
||||||
|
self.has_external_input = False
|
||||||
|
self.has_external_output = False
|
||||||
|
self.graph = graph
|
||||||
self._data = data
|
self._data = data
|
||||||
self.base_type: Optional[str] = base_type
|
self.base_type: Optional[str] = base_type
|
||||||
self._parse_data()
|
self._parse_data()
|
||||||
self._built_object = UnbuiltObject()
|
self._built_object = UnbuiltObject()
|
||||||
|
self._built_result = None
|
||||||
self._built = False
|
self._built = False
|
||||||
self.artifacts: Dict[str, Any] = {}
|
self.artifacts: Dict[str, Any] = {}
|
||||||
|
self.steps: List[Callable] = [self._build]
|
||||||
|
self.steps_ran: List[Callable] = []
|
||||||
self.task_id: Optional[str] = None
|
self.task_id: Optional[str] = None
|
||||||
self.is_task = is_task
|
self.is_task = is_task
|
||||||
self.params = params or {}
|
self.params = params or {}
|
||||||
self.parent_node_id: Optional[str] = self._data.get("parent_node_id")
|
self.parent_node_id: Optional[str] = self._data.get("parent_node_id")
|
||||||
self.load_from_db_fields: List[str] = []
|
self.load_from_db_fields: List[str] = []
|
||||||
self.parent_is_top_level = False
|
self.parent_is_top_level = False
|
||||||
|
self.layer = None
|
||||||
|
self.should_run = True
|
||||||
|
self.result: Optional[ResultData] = None
|
||||||
|
try:
|
||||||
|
self.is_interface_component = self.vertex_type in InterfaceComponentTypes
|
||||||
|
except ValueError:
|
||||||
|
self.is_interface_component = False
|
||||||
|
|
||||||
|
self.use_result = False
|
||||||
|
self.build_times: List[float] = []
|
||||||
|
self.state = VertexStates.ACTIVE
|
||||||
|
|
||||||
|
def update_graph_state(self, key, new_state, append: bool):
|
||||||
|
if append:
|
||||||
|
self.graph.append_state(key, new_state, caller=self.id)
|
||||||
|
else:
|
||||||
|
self.graph.update_state(key, new_state, caller=self.id)
|
||||||
|
|
||||||
|
def set_state(self, state: str):
|
||||||
|
self.state = VertexStates[state]
|
||||||
|
if self.state == VertexStates.INACTIVE and self.graph.in_degree_map[self.id] < 2:
|
||||||
|
# If the vertex is inactive and has only one in degree
|
||||||
|
# it means that it is not a merge point in the graph
|
||||||
|
self.graph.inactivated_vertices.add(self.id)
|
||||||
|
elif self.state == VertexStates.ACTIVE and self.id in self.graph.inactivated_vertices:
|
||||||
|
self.graph.inactivated_vertices.remove(self.id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def edges(self) -> List["Edge"]:
|
def avg_build_time(self):
|
||||||
|
return sum(self.build_times) / len(self.build_times) if self.build_times else 0
|
||||||
|
|
||||||
|
def add_build_time(self, time):
|
||||||
|
self.build_times.append(time)
|
||||||
|
|
||||||
|
def set_result(self, result: ResultData) -> None:
|
||||||
|
self.result = result
|
||||||
|
|
||||||
|
def get_built_result(self):
|
||||||
|
# If the Vertex.type is a power component
|
||||||
|
# then we need to return the built object
|
||||||
|
# instead of the result dict
|
||||||
|
if self.is_interface_component and not isinstance(self._built_object, UnbuiltObject):
|
||||||
|
result = self._built_object
|
||||||
|
# if it is not a dict or a string and hasattr model_dump then
|
||||||
|
# return the model_dump
|
||||||
|
if not isinstance(result, (dict, str)) and hasattr(result, "content"):
|
||||||
|
return result.content
|
||||||
|
return result
|
||||||
|
if isinstance(self._built_object, str):
|
||||||
|
self._built_result = self._built_object
|
||||||
|
|
||||||
|
if isinstance(self._built_result, UnbuiltResult):
|
||||||
|
return {}
|
||||||
|
return self._built_result if isinstance(self._built_result, dict) else {"result": self._built_result}
|
||||||
|
|
||||||
|
def set_artifacts(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def edges(self) -> List["ContractEdge"]:
|
||||||
return self.graph.get_vertex_edges(self.id)
|
return self.graph.get_vertex_edges(self.id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def predecessors(self) -> List["Vertex"]:
|
||||||
|
return self.graph.get_predecessors(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def successors(self) -> List["Vertex"]:
|
||||||
|
return self.graph.get_successors(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def successors_ids(self) -> List[str]:
|
||||||
|
return self.graph.successor_map.get(self.id, [])
|
||||||
|
|
||||||
def __getstate__(self):
|
def __getstate__(self):
|
||||||
return {
|
return {
|
||||||
"_data": self._data,
|
"_data": self._data,
|
||||||
|
|
@ -55,14 +168,20 @@ class Vertex:
|
||||||
"parent_node_id": self.parent_node_id,
|
"parent_node_id": self.parent_node_id,
|
||||||
"parent_is_top_level": self.parent_is_top_level,
|
"parent_is_top_level": self.parent_is_top_level,
|
||||||
"load_from_db_fields": self.load_from_db_fields,
|
"load_from_db_fields": self.load_from_db_fields,
|
||||||
|
"is_input": self.is_input,
|
||||||
|
"is_output": self.is_output,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __setstate__(self, state):
|
def __setstate__(self, state):
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
self._data = state["_data"]
|
self._data = state["_data"]
|
||||||
self.params = state["params"]
|
self.params = state["params"]
|
||||||
self.base_type = state["base_type"]
|
self.base_type = state["base_type"]
|
||||||
self.is_task = state["is_task"]
|
self.is_task = state["is_task"]
|
||||||
self.id = state["id"]
|
self.id = state["id"]
|
||||||
|
self.frozen = state.get("frozen", False)
|
||||||
|
self.is_input = state.get("is_input", False)
|
||||||
|
self.is_output = state.get("is_output", False)
|
||||||
self._parse_data()
|
self._parse_data()
|
||||||
if "_built_object" in state:
|
if "_built_object" in state:
|
||||||
self._built_object = state["_built_object"]
|
self._built_object = state["_built_object"]
|
||||||
|
|
@ -70,11 +189,17 @@ class Vertex:
|
||||||
else:
|
else:
|
||||||
self._built_object = UnbuiltObject()
|
self._built_object = UnbuiltObject()
|
||||||
self._built = False
|
self._built = False
|
||||||
|
if "_built_result" in state:
|
||||||
|
self._built_result = state["_built_result"]
|
||||||
|
else:
|
||||||
|
self._built_result = UnbuiltResult()
|
||||||
self.artifacts: Dict[str, Any] = {}
|
self.artifacts: Dict[str, Any] = {}
|
||||||
self.task_id: Optional[str] = None
|
self.task_id: Optional[str] = None
|
||||||
self.parent_node_id = state["parent_node_id"]
|
self.parent_node_id = state["parent_node_id"]
|
||||||
self.parent_is_top_level = state["parent_is_top_level"]
|
self.parent_is_top_level = state["parent_is_top_level"]
|
||||||
self.load_from_db_fields = state["load_from_db_fields"]
|
self.load_from_db_fields = state["load_from_db_fields"]
|
||||||
|
self.layer = state.get("layer")
|
||||||
|
self.steps = state.get("steps", [self._build])
|
||||||
|
|
||||||
def set_top_level(self, top_level_vertices: List[str]) -> None:
|
def set_top_level(self, top_level_vertices: List[str]) -> None:
|
||||||
self.parent_is_top_level = self.parent_node_id in top_level_vertices
|
self.parent_is_top_level = self.parent_node_id in top_level_vertices
|
||||||
|
|
@ -82,8 +207,15 @@ class Vertex:
|
||||||
def _parse_data(self) -> None:
|
def _parse_data(self) -> None:
|
||||||
self.data = self._data["data"]
|
self.data = self._data["data"]
|
||||||
self.output = self.data["node"]["base_classes"]
|
self.output = self.data["node"]["base_classes"]
|
||||||
|
self.display_name = self.data["node"].get("display_name", self.id.split("-")[0])
|
||||||
|
self.frozen = self.data["node"].get("frozen", False)
|
||||||
|
self.selected_output_type = self.data["node"].get("selected_output_type")
|
||||||
|
self.is_input = self.data["node"].get("is_input") or self.is_input
|
||||||
|
self.is_output = self.data["node"].get("is_output") or self.is_output
|
||||||
template_dicts = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)}
|
template_dicts = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)}
|
||||||
|
|
||||||
|
self.has_session_id = "session_id" in template_dicts
|
||||||
|
|
||||||
self.required_inputs = [
|
self.required_inputs = [
|
||||||
template_dicts[key]["type"] for key, value in template_dicts.items() if value["required"]
|
template_dicts[key]["type"] for key, value in template_dicts.items() if value["required"]
|
||||||
]
|
]
|
||||||
|
|
@ -135,8 +267,12 @@ 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 = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)}
|
template_dict = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)}
|
||||||
params = self.params.copy() if self.params else {}
|
params = {}
|
||||||
|
|
||||||
for edge in self.edges:
|
for edge in self.edges:
|
||||||
if not hasattr(edge, "target_param"):
|
if not hasattr(edge, "target_param"):
|
||||||
|
|
@ -152,6 +288,12 @@ class Vertex:
|
||||||
params[param_key] = []
|
params[param_key] = []
|
||||||
params[param_key].append(self.graph.get_vertex(edge.source_id))
|
params[param_key].append(self.graph.get_vertex(edge.source_id))
|
||||||
elif edge.target_id == self.id:
|
elif edge.target_id == self.id:
|
||||||
|
if isinstance(template_dict[param_key].get("value"), dict):
|
||||||
|
# we don't know the key of the dict but we need to set the value
|
||||||
|
# to the vertex that is the source of the edge
|
||||||
|
param_dict = template_dict[param_key]["value"]
|
||||||
|
params[param_key] = {key: self.graph.get_vertex(edge.source_id) for key in param_dict.keys()}
|
||||||
|
else:
|
||||||
params[param_key] = self.graph.get_vertex(edge.source_id)
|
params[param_key] = self.graph.get_vertex(edge.source_id)
|
||||||
|
|
||||||
load_from_db_fields = []
|
load_from_db_fields = []
|
||||||
|
|
@ -169,16 +311,18 @@ class Vertex:
|
||||||
# what is inside value.get('content')
|
# what is inside value.get('content')
|
||||||
# value.get('value') is the file name
|
# value.get('value') is the file name
|
||||||
if file_path := field.get("file_path"):
|
if file_path := field.get("file_path"):
|
||||||
params[field_name] = file_path
|
storage_service = get_storage_service()
|
||||||
|
flow_id, file_name = file_path.split("/")
|
||||||
|
full_path = storage_service.build_full_path(flow_id, file_name)
|
||||||
|
params[field_name] = full_path
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"File path not found for {self.vertex_type}")
|
raise ValueError(f"File path not found for {self.display_name}")
|
||||||
elif field.get("type") in DIRECT_TYPES and params.get(field_name) is None:
|
elif field.get("type") in DIRECT_TYPES and params.get(field_name) is None:
|
||||||
val = field.get("value")
|
val = field.get("value")
|
||||||
if field.get("type") == "code":
|
if field.get("type") == "code":
|
||||||
try:
|
try:
|
||||||
params[field_name] = ast.literal_eval(val) if val else None
|
params[field_name] = ast.literal_eval(val) if val else None
|
||||||
except Exception as exc:
|
except Exception:
|
||||||
logger.debug(f"Error parsing code: {exc}")
|
|
||||||
params[field_name] = val
|
params[field_name] = val
|
||||||
elif field.get("type") in ["dict", "NestedDict"]:
|
elif field.get("type") in ["dict", "NestedDict"]:
|
||||||
# When dict comes from the frontend it comes as a
|
# When dict comes from the frontend it comes as a
|
||||||
|
|
@ -198,6 +342,16 @@ class Vertex:
|
||||||
params[field_name] = float(val)
|
params[field_name] = float(val)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
params[field_name] = val
|
params[field_name] = val
|
||||||
|
params[field_name] = val
|
||||||
|
elif field.get("type") == "str" and val is not None:
|
||||||
|
# val may contain escaped \n, \t, etc.
|
||||||
|
# so we need to unescape it
|
||||||
|
if isinstance(val, list):
|
||||||
|
params[field_name] = [unescape_string(v) for v in val]
|
||||||
|
elif isinstance(val, str):
|
||||||
|
params[field_name] = unescape_string(val)
|
||||||
|
elif val is not None and val != "":
|
||||||
|
params[field_name] = val
|
||||||
|
|
||||||
elif val is not None and val != "":
|
elif val is not None and val != "":
|
||||||
params[field_name] = val
|
params[field_name] = val
|
||||||
|
|
@ -210,92 +364,226 @@ class Vertex:
|
||||||
else:
|
else:
|
||||||
params.pop(field_name, None)
|
params.pop(field_name, None)
|
||||||
# Add _type to params
|
# Add _type to params
|
||||||
self._raw_params = params
|
|
||||||
self.params = params
|
self.params = params
|
||||||
self.load_from_db_fields = load_from_db_fields
|
self.load_from_db_fields = load_from_db_fields
|
||||||
|
self._raw_params = params.copy()
|
||||||
|
|
||||||
|
def update_raw_params(self, new_params: Dict[str, str], overwrite: bool = False):
|
||||||
|
"""
|
||||||
|
Update the raw parameters of the vertex with the given new parameters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_params (Dict[str, Any]): The new parameters to update.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If any key in new_params is not found in self._raw_params.
|
||||||
|
"""
|
||||||
|
# First check if the input_value in _raw_params is not a vertex
|
||||||
|
if not new_params:
|
||||||
|
return
|
||||||
|
if any(isinstance(self._raw_params.get(key), Vertex) for key in new_params):
|
||||||
|
return
|
||||||
|
if not overwrite:
|
||||||
|
for key in new_params.copy():
|
||||||
|
if key not in self._raw_params:
|
||||||
|
new_params.pop(key)
|
||||||
|
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):
|
||||||
"""
|
"""
|
||||||
Initiate the build process.
|
Initiate the build process.
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Building {self.vertex_type}")
|
logger.debug(f"Building {self.display_name}")
|
||||||
await self._build_each_node_in_params_dict(user_id)
|
await self._build_each_vertex_in_params_dict(user_id)
|
||||||
await self._get_and_instantiate_class(user_id)
|
await self._get_and_instantiate_class(user_id)
|
||||||
self._validate_built_object()
|
self._validate_built_object()
|
||||||
|
|
||||||
self._built = True
|
self._built = True
|
||||||
|
|
||||||
async def _build_each_node_in_params_dict(self, user_id=None):
|
def extract_messages_from_artifacts(self, artifacts: Dict[str, Any]) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
Iterates over each node in the params dictionary and builds it.
|
Extracts messages from the artifacts.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
artifacts (Dict[str, Any]): The artifacts to extract messages from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[str]: The extracted messages.
|
||||||
"""
|
"""
|
||||||
for key, value in self.params.copy().items():
|
messages = []
|
||||||
if self._is_node(value):
|
for key, artifact in artifacts.items():
|
||||||
|
if not isinstance(artifact, dict):
|
||||||
|
continue
|
||||||
|
if "message" in artifact:
|
||||||
|
chat_output_response = ChatOutputResponse(
|
||||||
|
message=artifact["message"],
|
||||||
|
sender=artifact.get("sender"),
|
||||||
|
sender_name=artifact.get("sender_name"),
|
||||||
|
session_id=artifact.get("session_id"),
|
||||||
|
component_id=self.id,
|
||||||
|
)
|
||||||
|
messages.append(chat_output_response.model_dump(exclude_none=True))
|
||||||
|
|
||||||
|
return messages
|
||||||
|
|
||||||
|
def _finalize_build(self):
|
||||||
|
result_dict = self.get_built_result()
|
||||||
|
# We need to set the artifacts to pass information
|
||||||
|
# to the frontend
|
||||||
|
self.set_artifacts()
|
||||||
|
artifacts = self.artifacts
|
||||||
|
messages = self.extract_messages_from_artifacts(artifacts)
|
||||||
|
result_dict = ResultData(
|
||||||
|
results=result_dict,
|
||||||
|
artifacts=artifacts,
|
||||||
|
messages=messages,
|
||||||
|
component_display_name=self.display_name,
|
||||||
|
component_id=self.id,
|
||||||
|
)
|
||||||
|
self.set_result(result_dict)
|
||||||
|
|
||||||
|
async def _run(
|
||||||
|
self,
|
||||||
|
user_id: str,
|
||||||
|
inputs: Optional[dict] = None,
|
||||||
|
session_id: Optional[str] = None,
|
||||||
|
):
|
||||||
|
# user_id is just for compatibility with the other build methods
|
||||||
|
inputs = inputs or {}
|
||||||
|
# inputs = {key: value or "" for key, value in inputs.items()}
|
||||||
|
# if hasattr(self._built_object, "input_keys"):
|
||||||
|
# # test if all keys are in inputs
|
||||||
|
# # and if not add them with empty string
|
||||||
|
# # for key in self._built_object.input_keys:
|
||||||
|
# # if key not in inputs:
|
||||||
|
# # inputs[key] = ""
|
||||||
|
# if inputs == {} and hasattr(self._built_object, "prompt"):
|
||||||
|
# inputs = self._built_object.prompt.partial_variables
|
||||||
|
if isinstance(self._built_object, str):
|
||||||
|
self._built_result = self._built_object
|
||||||
|
|
||||||
|
result = await generate_result(self._built_object, inputs, self.has_external_output, session_id)
|
||||||
|
self._built_result = result
|
||||||
|
|
||||||
|
async def _build_each_vertex_in_params_dict(self, user_id=None):
|
||||||
|
"""
|
||||||
|
Iterates over each vertex in the params dictionary and builds it.
|
||||||
|
"""
|
||||||
|
for key, value in self._raw_params.items():
|
||||||
|
if self._is_vertex(value):
|
||||||
if value == self:
|
if value == self:
|
||||||
del self.params[key]
|
del self.params[key]
|
||||||
continue
|
continue
|
||||||
await self._build_node_and_update_params(key, value, user_id)
|
await self._build_vertex_and_update_params(
|
||||||
elif isinstance(value, list) and self._is_list_of_nodes(value):
|
key,
|
||||||
await self._build_list_of_nodes_and_update_params(key, value, user_id)
|
value,
|
||||||
|
)
|
||||||
|
elif isinstance(value, list) and self._is_list_of_vertices(value):
|
||||||
|
await self._build_list_of_vertices_and_update_params(key, value)
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
await self._build_dict_and_update_params(
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
elif key not in self.params or self.updated_raw_params:
|
||||||
|
self.params[key] = value
|
||||||
|
|
||||||
def _is_node(self, value):
|
async def _build_dict_and_update_params(
|
||||||
|
self,
|
||||||
|
key,
|
||||||
|
vertices_dict: Dict[str, "Vertex"],
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Iterates over a dictionary of vertices, builds each and updates the params dictionary.
|
||||||
|
"""
|
||||||
|
for sub_key, value in vertices_dict.items():
|
||||||
|
if not self._is_vertex(value):
|
||||||
|
self.params[key][sub_key] = value
|
||||||
|
else:
|
||||||
|
result = await value.get_result()
|
||||||
|
self.params[key][sub_key] = result
|
||||||
|
|
||||||
|
def _is_vertex(self, value):
|
||||||
"""
|
"""
|
||||||
Checks if the provided value is an instance of Vertex.
|
Checks if the provided value is an instance of Vertex.
|
||||||
"""
|
"""
|
||||||
return isinstance(value, Vertex)
|
return isinstance(value, Vertex)
|
||||||
|
|
||||||
def _is_list_of_nodes(self, value):
|
def _is_list_of_vertices(self, value):
|
||||||
"""
|
"""
|
||||||
Checks if the provided value is a list of Vertex instances.
|
Checks if the provided value is a list of Vertex instances.
|
||||||
"""
|
"""
|
||||||
return all(self._is_node(node) for node in value)
|
return all(self._is_vertex(vertex) for vertex in value)
|
||||||
|
|
||||||
async def get_result(self, user_id=None, timeout=None) -> Any:
|
async def get_result(
|
||||||
# Check if the Vertex was built already
|
self,
|
||||||
if self._built:
|
) -> Any:
|
||||||
return self._built_object
|
|
||||||
|
|
||||||
if self.is_task and self.task_id is not None:
|
|
||||||
task = self.get_task()
|
|
||||||
|
|
||||||
result = task.get(timeout=timeout)
|
|
||||||
if isinstance(result, Coroutine):
|
|
||||||
result = await result
|
|
||||||
if result is not None: # If result is ready
|
|
||||||
self._update_built_object_and_artifacts(result)
|
|
||||||
return self._built_object
|
|
||||||
else:
|
|
||||||
# Handle the case when the result is not ready (retry, throw exception, etc.)
|
|
||||||
pass
|
|
||||||
|
|
||||||
# If there's no task_id, build the vertex locally
|
|
||||||
await self.build(user_id=user_id)
|
|
||||||
return self._built_object
|
|
||||||
|
|
||||||
async def _build_node_and_update_params(self, key, node, user_id=None):
|
|
||||||
"""
|
"""
|
||||||
Builds a given node and updates the params dictionary accordingly.
|
Retrieves the result of the vertex.
|
||||||
|
|
||||||
|
This is a read-only method so it raises an error if the vertex has not been built yet.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The result of the vertex.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
return await self._get_result()
|
||||||
|
|
||||||
|
async def _get_result(self) -> Any:
|
||||||
|
"""
|
||||||
|
Retrieves the result of the built component.
|
||||||
|
|
||||||
|
If the component has not been built yet, a ValueError is raised.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The built result if use_result is True, else the built object.
|
||||||
|
"""
|
||||||
|
if not self._built:
|
||||||
|
raise ValueError(f"Component {self.display_name} has not been built yet")
|
||||||
|
return self._built_result if self.use_result else self._built_object
|
||||||
|
|
||||||
|
async def _build_vertex_and_update_params(self, key, vertex: "Vertex"):
|
||||||
|
"""
|
||||||
|
Builds a given vertex and updates the params dictionary accordingly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
result = await node.get_result(user_id)
|
result = await vertex.get_result()
|
||||||
self._handle_func(key, result)
|
self._handle_func(key, result)
|
||||||
if isinstance(result, list):
|
if isinstance(result, list):
|
||||||
self._extend_params_list_with_result(key, result)
|
self._extend_params_list_with_result(key, result)
|
||||||
self.params[key] = result
|
self.params[key] = result
|
||||||
|
|
||||||
async def _build_list_of_nodes_and_update_params(self, key, nodes: List["Vertex"], user_id=None):
|
async def _build_list_of_vertices_and_update_params(
|
||||||
|
self,
|
||||||
|
key,
|
||||||
|
vertices: List["Vertex"],
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Iterates over a list of nodes, builds each and updates the params dictionary.
|
Iterates over a list of vertices, builds each and updates the params dictionary.
|
||||||
"""
|
"""
|
||||||
self.params[key] = []
|
self.params[key] = []
|
||||||
for node in nodes:
|
for vertex in vertices:
|
||||||
built = await node.get_result(user_id)
|
result = await vertex.get_result()
|
||||||
if isinstance(built, list):
|
# Weird check to see if the params[key] is a list
|
||||||
if key not in self.params:
|
# because sometimes it is a Record and breaks the code
|
||||||
self.params[key] = []
|
if not isinstance(self.params[key], list):
|
||||||
self.params[key].extend(built)
|
self.params[key] = [self.params[key]]
|
||||||
|
|
||||||
|
if isinstance(result, list):
|
||||||
|
self.params[key].extend(result)
|
||||||
else:
|
else:
|
||||||
self.params[key].append(built)
|
try:
|
||||||
|
if self.params[key] == result:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.params[key].append(result)
|
||||||
|
except AttributeError as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise ValueError(
|
||||||
|
f"Params {key} ({self.params[key]}) is not a list and cannot be extended with {result}"
|
||||||
|
f"Error building vertex {self.display_name}: {str(e)}"
|
||||||
|
) from e
|
||||||
|
|
||||||
def _handle_func(self, key, result):
|
def _handle_func(self, key, result):
|
||||||
"""
|
"""
|
||||||
|
|
@ -324,26 +612,27 @@ class Vertex:
|
||||||
Gets the class from a dictionary and instantiates it with the params.
|
Gets the class from a dictionary and instantiates it with the params.
|
||||||
"""
|
"""
|
||||||
if self.base_type is None:
|
if self.base_type is None:
|
||||||
raise ValueError(f"Base type for node {self.vertex_type} not found")
|
raise ValueError(f"Base type for vertex {self.display_name} not found")
|
||||||
try:
|
try:
|
||||||
result = await loading.instantiate_class(
|
result = await loading.instantiate_class(
|
||||||
node_type=self.vertex_type,
|
|
||||||
base_type=self.base_type,
|
|
||||||
load_from_db_fields=self.load_from_db_fields,
|
|
||||||
params=self.params,
|
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
|
vertex=self,
|
||||||
)
|
)
|
||||||
self._update_built_object_and_artifacts(result)
|
self._update_built_object_and_artifacts(result)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception(exc)
|
logger.exception(exc)
|
||||||
raise ValueError(f"Error building node {self.vertex_type}(ID:{self.id}): {str(exc)}") from exc
|
|
||||||
|
raise ValueError(f"Error building vertex {self.display_name}: {str(exc)}") from exc
|
||||||
|
|
||||||
def _update_built_object_and_artifacts(self, result):
|
def _update_built_object_and_artifacts(self, result):
|
||||||
"""
|
"""
|
||||||
Updates the built object and its artifacts.
|
Updates the built object and its artifacts.
|
||||||
"""
|
"""
|
||||||
if isinstance(result, tuple):
|
if isinstance(result, tuple):
|
||||||
|
if len(result) == 2:
|
||||||
self._built_object, self.artifacts = result
|
self._built_object, self.artifacts = result
|
||||||
|
elif len(result) == 3:
|
||||||
|
self._custom_component, self._built_object, self.artifacts = result
|
||||||
else:
|
else:
|
||||||
self._built_object = result
|
self._built_object = result
|
||||||
|
|
||||||
|
|
@ -352,30 +641,105 @@ class Vertex:
|
||||||
Checks if the built object is None and raises a ValueError if so.
|
Checks if the built object is None and raises a ValueError if so.
|
||||||
"""
|
"""
|
||||||
if isinstance(self._built_object, UnbuiltObject):
|
if isinstance(self._built_object, UnbuiltObject):
|
||||||
raise ValueError(f"{self.vertex_type}: {self._built_object_repr()}")
|
raise ValueError(f"{self.display_name}: {self._built_object_repr()}")
|
||||||
elif self._built_object is None:
|
elif self._built_object is None:
|
||||||
message = f"{self.vertex_type} returned None."
|
message = f"{self.display_name} returned None."
|
||||||
if self.base_type == "custom_components":
|
if self.base_type == "custom_components":
|
||||||
message += " Make sure your build method returns a component."
|
message += " Make sure your build method returns a component."
|
||||||
|
|
||||||
logger.warning(message)
|
logger.warning(message)
|
||||||
|
elif isinstance(self._built_object, (Iterator, AsyncIterator)):
|
||||||
|
if self.display_name in ["Text Output"]:
|
||||||
|
raise ValueError(f"You are trying to stream to a {self.display_name}. Try using a Chat Output instead.")
|
||||||
|
|
||||||
async def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any:
|
def _reset(self, params_update: Optional[Dict[str, Any]] = None):
|
||||||
if not self._built or force:
|
self._built = False
|
||||||
await self._build(user_id, *args, **kwargs)
|
self._built_object = UnbuiltObject()
|
||||||
|
self._built_result = UnbuiltResult()
|
||||||
|
self.artifacts = {}
|
||||||
|
self.steps_ran = []
|
||||||
|
self._build_params()
|
||||||
|
|
||||||
|
def _is_chat_input(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def build_inactive(self):
|
||||||
|
# Just set the results to None
|
||||||
|
self._built = True
|
||||||
|
self._built_object = None
|
||||||
|
self._built_result = None
|
||||||
|
|
||||||
|
async def build(
|
||||||
|
self,
|
||||||
|
user_id=None,
|
||||||
|
inputs: Optional[Dict[str, Any]] = None,
|
||||||
|
requester: Optional["Vertex"] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> Any:
|
||||||
|
async with self._lock:
|
||||||
|
if self.state == VertexStates.INACTIVE:
|
||||||
|
# If the vertex is inactive, return None
|
||||||
|
self.build_inactive()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.frozen and self._built:
|
||||||
|
return self.get_requester_result(requester)
|
||||||
|
elif self._built and requester is not None:
|
||||||
|
# This means that the vertex has already been built
|
||||||
|
# and we are just getting the result for the requester
|
||||||
|
return await self.get_requester_result(requester)
|
||||||
|
self._reset()
|
||||||
|
|
||||||
|
if self._is_chat_input() and inputs:
|
||||||
|
inputs = {"input_value": inputs.get(INPUT_FIELD_NAME, "")}
|
||||||
|
self.update_raw_params(inputs, overwrite=True)
|
||||||
|
|
||||||
|
# Run steps
|
||||||
|
for step in self.steps:
|
||||||
|
if step not in self.steps_ran:
|
||||||
|
if inspect.iscoroutinefunction(step):
|
||||||
|
await step(user_id=user_id, **kwargs)
|
||||||
|
else:
|
||||||
|
step(user_id=user_id, **kwargs)
|
||||||
|
self.steps_ran.append(step)
|
||||||
|
|
||||||
|
self._finalize_build()
|
||||||
|
|
||||||
|
return await self.get_requester_result(requester)
|
||||||
|
|
||||||
|
async def get_requester_result(self, requester: Optional["Vertex"]):
|
||||||
|
# If the requester is None, this means that
|
||||||
|
# the Vertex is the root of the graph
|
||||||
|
if requester is None:
|
||||||
return self._built_object
|
return self._built_object
|
||||||
|
|
||||||
def add_edge(self, edge: "Edge") -> None:
|
# Get the requester edge
|
||||||
|
requester_edge = next((edge for edge in self.edges if edge.target_id == requester.id), None)
|
||||||
|
# Return the result of the requester edge
|
||||||
|
return (
|
||||||
|
None
|
||||||
|
if requester_edge is None
|
||||||
|
else await requester_edge.get_result_from_source(source=self, target=requester)
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_edge(self, edge: "ContractEdge") -> None:
|
||||||
if edge not in self.edges:
|
if edge not in self.edges:
|
||||||
self.edges.append(edge)
|
self.edges.append(edge)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Vertex(id={self.id}, data={self.data})"
|
return f"Vertex(display_name={self.display_name}, id={self.id}, data={self.data})"
|
||||||
|
|
||||||
def __eq__(self, __o: object) -> bool:
|
def __eq__(self, __o: object) -> bool:
|
||||||
try:
|
try:
|
||||||
return self.id == __o.id if isinstance(__o, Vertex) else False
|
if not isinstance(__o, Vertex):
|
||||||
|
return False
|
||||||
|
# We should create a more robust comparison
|
||||||
|
# for the Vertex class
|
||||||
|
ids_are_equal = self.id == __o.id
|
||||||
|
# self._data is a dict and we need to compare them
|
||||||
|
# to check if they are equal
|
||||||
|
data_are_equal = self.data == __o.data
|
||||||
|
return ids_are_equal and data_are_equal
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,42 @@
|
||||||
import operator
|
import operator
|
||||||
from typing import Any, Callable, List, Optional, Union
|
from pathlib import Path
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
ClassVar,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Sequence,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from cachetools import TTLCache, cachedmethod
|
from cachetools import TTLCache, cachedmethod
|
||||||
from fastapi import HTTPException
|
from langchain_core.documents import Document
|
||||||
|
|
||||||
from langflow.interface.custom.code_parser.utils import (
|
from langflow.interface.custom.code_parser.utils import (
|
||||||
extract_inner_type_from_generic_alias,
|
extract_inner_type_from_generic_alias,
|
||||||
extract_union_types_from_generic_alias,
|
extract_union_types_from_generic_alias,
|
||||||
)
|
)
|
||||||
|
from langflow.interface.custom.custom_component.component import Component
|
||||||
|
from langflow.schema import Record
|
||||||
|
from langflow.schema.dotdict import dotdict
|
||||||
from langflow.services.database.models.flow import Flow
|
from langflow.services.database.models.flow import Flow
|
||||||
from langflow.services.database.utils import session_getter
|
from langflow.services.database.utils import session_getter
|
||||||
from langflow.services.deps import get_credential_service, get_db_service
|
from langflow.services.deps import (
|
||||||
|
get_credential_service,
|
||||||
|
get_db_service,
|
||||||
|
get_storage_service,
|
||||||
|
)
|
||||||
|
from langflow.services.storage.service import StorageService
|
||||||
from langflow.utils import validate
|
from langflow.utils import validate
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlmodel import select
|
||||||
|
|
||||||
from .component import Component
|
if TYPE_CHECKING:
|
||||||
|
from langflow.graph.graph.base import Graph
|
||||||
|
from langflow.graph.vertex.base import Vertex
|
||||||
|
|
||||||
|
|
||||||
class CustomComponent(Component):
|
class CustomComponent(Component):
|
||||||
|
|
@ -35,6 +57,67 @@ class CustomComponent(Component):
|
||||||
_tree (Optional[dict]): The code tree of the custom component.
|
_tree (Optional[dict]): The code tree of the custom component.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
display_name: Optional[str] = None
|
||||||
|
"""The display name of the component. Defaults to None."""
|
||||||
|
description: Optional[str] = None
|
||||||
|
"""The description of the component. Defaults to None."""
|
||||||
|
icon: Optional[str] = None
|
||||||
|
"""The icon of the component. It should be an emoji. Defaults to None."""
|
||||||
|
is_input: Optional[bool] = None
|
||||||
|
"""The input state of the component. Defaults to None.
|
||||||
|
If True, the component must have a field named 'input_value'."""
|
||||||
|
is_output: Optional[bool] = None
|
||||||
|
"""The output state of the component. Defaults to None.
|
||||||
|
If True, the component must have a field named 'input_value'."""
|
||||||
|
code: Optional[str] = None
|
||||||
|
"""The code of the component. Defaults to None."""
|
||||||
|
field_config: dict = {}
|
||||||
|
"""The field configuration of the component. Defaults to an empty dictionary."""
|
||||||
|
field_order: Optional[List[str]] = None
|
||||||
|
"""The field order of the component. Defaults to an empty list."""
|
||||||
|
frozen: Optional[bool] = False
|
||||||
|
"""The default frozen state of the component. Defaults to False."""
|
||||||
|
build_parameters: Optional[dict] = None
|
||||||
|
"""The build parameters of the component. Defaults to None."""
|
||||||
|
selected_output_type: Optional[str] = None
|
||||||
|
"""The selected output type of the component. Defaults to None."""
|
||||||
|
vertex: Optional["Vertex"] = None
|
||||||
|
"""The edge target parameter of the component. Defaults to None."""
|
||||||
|
code_class_base_inheritance: ClassVar[str] = "CustomComponent"
|
||||||
|
function_entrypoint_name: ClassVar[str] = "build"
|
||||||
|
function: Optional[Callable] = None
|
||||||
|
repr_value: Optional[Any] = ""
|
||||||
|
user_id: Optional[Union[UUID, str]] = None
|
||||||
|
status: Optional[Any] = None
|
||||||
|
"""The status of the component. This is displayed on the frontend. Defaults to None."""
|
||||||
|
_flows_records: Optional[List[Record]] = None
|
||||||
|
|
||||||
|
def update_state(self, name: str, value: Any):
|
||||||
|
if not self.vertex:
|
||||||
|
raise ValueError("Vertex is not set")
|
||||||
|
try:
|
||||||
|
self.vertex.graph.update_state(name=name, record=value, caller=self.vertex.id)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Error updating state: {e}")
|
||||||
|
|
||||||
|
def append_state(self, name: str, value: Any):
|
||||||
|
if not self.vertex:
|
||||||
|
raise ValueError("Vertex is not set")
|
||||||
|
try:
|
||||||
|
self.vertex.graph.append_state(name=name, record=value, caller=self.vertex.id)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Error appending state: {e}")
|
||||||
|
|
||||||
|
def get_state(self, name: str):
|
||||||
|
if not self.vertex:
|
||||||
|
raise ValueError("Vertex is not set")
|
||||||
|
try:
|
||||||
|
return self.vertex.graph.get_state(name=name)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Error getting state: {e}")
|
||||||
|
|
||||||
|
_tree: Optional[dict] = None
|
||||||
|
|
||||||
def __init__(self, **data):
|
def __init__(self, **data):
|
||||||
"""
|
"""
|
||||||
Initializes a new instance of the CustomComponent class.
|
Initializes a new instance of the CustomComponent class.
|
||||||
|
|
@ -45,6 +128,29 @@ class CustomComponent(Component):
|
||||||
self.cache = TTLCache(maxsize=1024, ttl=60)
|
self.cache = TTLCache(maxsize=1024, ttl=60)
|
||||||
super().__init__(**data)
|
super().__init__(**data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_path(path: str) -> str:
|
||||||
|
"""Resolves the path to an absolute path."""
|
||||||
|
path_object = Path(path)
|
||||||
|
if path_object.parts[0] == "~":
|
||||||
|
path_object = path_object.expanduser()
|
||||||
|
elif path_object.is_relative_to("."):
|
||||||
|
path_object = path_object.resolve()
|
||||||
|
return str(path_object)
|
||||||
|
|
||||||
|
def get_full_path(self, path: str) -> str:
|
||||||
|
storage_svc: "StorageService" = get_storage_service()
|
||||||
|
|
||||||
|
flow_id, file_name = path.split("/", 1)
|
||||||
|
return storage_svc.build_full_path(flow_id, file_name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def graph(self):
|
||||||
|
return self.vertex.graph
|
||||||
|
|
||||||
|
def _get_field_order(self):
|
||||||
|
return self.field_order or list(self.field_config.keys())
|
||||||
|
|
||||||
def custom_repr(self):
|
def custom_repr(self):
|
||||||
"""
|
"""
|
||||||
Returns the custom representation of the custom component.
|
Returns the custom representation of the custom component.
|
||||||
|
|
@ -58,7 +164,7 @@ class CustomComponent(Component):
|
||||||
return yaml.dump(self.repr_value)
|
return yaml.dump(self.repr_value)
|
||||||
if isinstance(self.repr_value, str):
|
if isinstance(self.repr_value, str):
|
||||||
return self.repr_value
|
return self.repr_value
|
||||||
return str(self.repr_value)
|
return self.repr_value
|
||||||
|
|
||||||
def build_config(self):
|
def build_config(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -69,6 +175,15 @@ class CustomComponent(Component):
|
||||||
"""
|
"""
|
||||||
return self.field_config
|
return self.field_config
|
||||||
|
|
||||||
|
def update_build_config(
|
||||||
|
self,
|
||||||
|
build_config: dotdict,
|
||||||
|
field_value: Any,
|
||||||
|
field_name: Optional[str] = None,
|
||||||
|
):
|
||||||
|
build_config[field_name] = field_value
|
||||||
|
return build_config
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tree(self):
|
def tree(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -79,6 +194,78 @@ class CustomComponent(Component):
|
||||||
"""
|
"""
|
||||||
return self.get_code_tree(self.code or "")
|
return self.get_code_tree(self.code or "")
|
||||||
|
|
||||||
|
def to_records(self, data: Any, keys: Optional[List[str]] = None, silent_errors: bool = False) -> List[Record]:
|
||||||
|
"""
|
||||||
|
Converts input data into a list of Record objects.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (Any): The input data to be converted. It can be a single item or a sequence of items.
|
||||||
|
If the input data is a Langchain Document, text_key and data_key are ignored.
|
||||||
|
|
||||||
|
keys (List[str], optional): The keys to access the text and data values in each item.
|
||||||
|
It should be a list of strings where the first element is the text key and the second element is the data key.
|
||||||
|
Defaults to None, in which case the default keys "text" and "data" are used.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Record]: A list of Record objects.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the input data is not of a valid type or if the specified keys are not found in the data.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not keys:
|
||||||
|
keys = []
|
||||||
|
records = []
|
||||||
|
if not isinstance(data, Sequence):
|
||||||
|
data = [data]
|
||||||
|
for item in data:
|
||||||
|
data_dict = {}
|
||||||
|
if isinstance(item, Document):
|
||||||
|
data_dict = item.metadata
|
||||||
|
data_dict["text"] = item.page_content
|
||||||
|
elif isinstance(item, BaseModel):
|
||||||
|
model_dump = item.model_dump()
|
||||||
|
for key in keys:
|
||||||
|
if silent_errors:
|
||||||
|
data_dict[key] = model_dump.get(key, "")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
data_dict[key] = model_dump[key]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError(f"Key {key} not found in {item}")
|
||||||
|
|
||||||
|
elif isinstance(item, str):
|
||||||
|
data_dict = {"text": item}
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
data_dict = item.copy()
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid data type: {type(item)}")
|
||||||
|
|
||||||
|
records.append(Record(data=data_dict))
|
||||||
|
|
||||||
|
return records
|
||||||
|
|
||||||
|
def create_references_from_records(self, records: List[Record], include_data: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Create references from a list of records.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
records (List[dict]): A list of records, where each record is a dictionary.
|
||||||
|
include_data (bool, optional): Whether to include data in the references. Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A string containing the references in markdown format.
|
||||||
|
"""
|
||||||
|
if not records:
|
||||||
|
return ""
|
||||||
|
markdown_string = "---\n"
|
||||||
|
for record in records:
|
||||||
|
markdown_string += f"- Text: {record.text}"
|
||||||
|
if include_data:
|
||||||
|
markdown_string += f" Data: {record.data}"
|
||||||
|
markdown_string += "\n"
|
||||||
|
return markdown_string
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def get_function_entrypoint_args(self) -> list:
|
def get_function_entrypoint_args(self) -> list:
|
||||||
"""
|
"""
|
||||||
|
|
@ -93,17 +280,7 @@ class CustomComponent(Component):
|
||||||
|
|
||||||
args = build_method["args"]
|
args = build_method["args"]
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if arg.get("type") == "prompt":
|
if not arg.get("type") and arg.get("name") != "self":
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail={
|
|
||||||
"error": "Type hint Error",
|
|
||||||
"traceback": (
|
|
||||||
"Prompt type is not supported in the build method." " Try using PromptTemplate instead."
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
elif not arg.get("type") and arg.get("name") != "self":
|
|
||||||
# Set the type to Data
|
# Set the type to Data
|
||||||
arg["type"] = "Data"
|
arg["type"] = "Data"
|
||||||
return args
|
return args
|
||||||
|
|
@ -145,7 +322,10 @@ class CustomComponent(Component):
|
||||||
return_type = build_method["return_type"]
|
return_type = build_method["return_type"]
|
||||||
|
|
||||||
# If list or List is in the return type, then we remove it and return the inner type
|
# If list or List is in the return type, then we remove it and return the inner type
|
||||||
if hasattr(return_type, "__origin__") and return_type.__origin__ in [list, List]:
|
if hasattr(return_type, "__origin__") and return_type.__origin__ in [
|
||||||
|
list,
|
||||||
|
List,
|
||||||
|
]:
|
||||||
return_type = extract_inner_type_from_generic_alias(return_type)
|
return_type = extract_inner_type_from_generic_alias(return_type)
|
||||||
|
|
||||||
# If the return type is not a Union, then we just return it as a list
|
# If the return type is not a Union, then we just return it as a list
|
||||||
|
|
@ -265,7 +445,6 @@ class CustomComponent(Component):
|
||||||
|
|
||||||
return get_index
|
return get_index
|
||||||
|
|
||||||
@property
|
|
||||||
def get_function(self):
|
def get_function(self):
|
||||||
"""
|
"""
|
||||||
Gets the function associated with the custom component.
|
Gets the function associated with the custom component.
|
||||||
|
|
@ -275,18 +454,9 @@ class CustomComponent(Component):
|
||||||
"""
|
"""
|
||||||
return validate.create_function(self.code, self.function_entrypoint_name)
|
return validate.create_function(self.code, self.function_entrypoint_name)
|
||||||
|
|
||||||
async def load_flow(self, flow_id: str, tweaks: Optional[dict] = None) -> Any:
|
async def load_flow(self, flow_id: str, tweaks: Optional[dict] = None) -> "Graph":
|
||||||
"""
|
from langflow.graph.graph.base import Graph
|
||||||
Loads a flow with the specified ID and applies tweaks if provided.
|
from langflow.processing.process import process_tweaks
|
||||||
|
|
||||||
Args:
|
|
||||||
flow_id (str): The ID of the flow to load.
|
|
||||||
tweaks (Optional[dict]): The tweaks to apply to the flow.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Any: The loaded flow.
|
|
||||||
"""
|
|
||||||
from langflow.processing.process import build_sorted_vertices, process_tweaks
|
|
||||||
|
|
||||||
db_service = get_db_service()
|
db_service = get_db_service()
|
||||||
with session_getter(db_service) as session:
|
with session_getter(db_service) as session:
|
||||||
|
|
@ -295,62 +465,52 @@ class CustomComponent(Component):
|
||||||
raise ValueError(f"Flow {flow_id} not found")
|
raise ValueError(f"Flow {flow_id} not found")
|
||||||
if tweaks:
|
if tweaks:
|
||||||
graph_data = process_tweaks(graph_data=graph_data, tweaks=tweaks)
|
graph_data = process_tweaks(graph_data=graph_data, tweaks=tweaks)
|
||||||
return await build_sorted_vertices(graph_data, self.user_id)
|
graph = Graph.from_payload(graph_data, flow_id=flow_id)
|
||||||
|
return graph
|
||||||
|
|
||||||
def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Flow]:
|
async def run_flow(
|
||||||
"""
|
self,
|
||||||
Lists the flows associated with the custom component.
|
input_value: Union[str, list[str]],
|
||||||
|
flow_id: Optional[str] = None,
|
||||||
|
flow_name: Optional[str] = None,
|
||||||
|
tweaks: Optional[dict] = None,
|
||||||
|
) -> Any:
|
||||||
|
if not flow_id and not flow_name:
|
||||||
|
raise ValueError("Flow ID or Flow Name is required")
|
||||||
|
if not self._flows_records:
|
||||||
|
self.list_flows()
|
||||||
|
if not flow_id and self._flows_records:
|
||||||
|
flow_ids = [flow.data["id"] for flow in self._flows_records if flow.data["name"] == flow_name]
|
||||||
|
if not flow_ids:
|
||||||
|
raise ValueError(f"Flow {flow_name} not found")
|
||||||
|
elif len(flow_ids) > 1:
|
||||||
|
raise ValueError(f"Multiple flows found with the name {flow_name}")
|
||||||
|
flow_id = flow_ids[0]
|
||||||
|
|
||||||
Args:
|
if not flow_id:
|
||||||
get_session (Optional[Callable]): The function to get the session.
|
raise ValueError(f"Flow {flow_name} not found")
|
||||||
|
if isinstance(input_value, str):
|
||||||
|
input_value = [input_value]
|
||||||
|
graph = await self.load_flow(flow_id, tweaks)
|
||||||
|
input_value_dict = [{"input_value": input_val} for input_val in input_value]
|
||||||
|
return await graph.run(input_value_dict, stream=False)
|
||||||
|
|
||||||
Returns:
|
def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Record]:
|
||||||
List[Flow]: The list of flows associated with the custom component.
|
|
||||||
"""
|
|
||||||
if not self._user_id:
|
if not self._user_id:
|
||||||
raise ValueError("Session is invalid")
|
raise ValueError("Session is invalid")
|
||||||
try:
|
try:
|
||||||
get_session = get_session or session_getter
|
get_session = get_session or session_getter
|
||||||
db_service = get_db_service()
|
db_service = get_db_service()
|
||||||
with get_session(db_service) as session:
|
with get_session(db_service) as session:
|
||||||
flows = session.query(Flow).filter(Flow.user_id == self.user_id).all()
|
flows = session.exec(
|
||||||
return flows
|
select(Flow).where(Flow.user_id == self._user_id).where(Flow.is_component == False) # noqa
|
||||||
|
).all()
|
||||||
|
|
||||||
|
flows_records = [flow.to_record() for flow in flows]
|
||||||
|
self._flows_records = flows_records
|
||||||
|
return flows_records
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError("Session is invalid") from e
|
raise ValueError(f"Error listing flows: {e}")
|
||||||
|
|
||||||
async def get_flow(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
flow_name: Optional[str] = None,
|
|
||||||
flow_id: Optional[str] = None,
|
|
||||||
tweaks: Optional[dict] = None,
|
|
||||||
get_session: Optional[Callable] = None,
|
|
||||||
) -> Flow:
|
|
||||||
"""
|
|
||||||
Gets a flow with the specified name or ID and applies tweaks if provided.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
flow_name (Optional[str]): The name of the flow to get.
|
|
||||||
flow_id (Optional[str]): The ID of the flow to get.
|
|
||||||
tweaks (Optional[dict]): The tweaks to apply to the flow.
|
|
||||||
get_session (Optional[Callable]): The function to get the session.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Flow: The flow with the specified name or ID.
|
|
||||||
"""
|
|
||||||
get_session = get_session or session_getter
|
|
||||||
db_service = get_db_service()
|
|
||||||
with get_session(db_service) as session:
|
|
||||||
if flow_id:
|
|
||||||
flow = session.query(Flow).get(flow_id)
|
|
||||||
elif flow_name:
|
|
||||||
flow = (session.query(Flow).filter(Flow.name == flow_name).filter(Flow.user_id == self.user_id)).first()
|
|
||||||
else:
|
|
||||||
raise ValueError("Either flow_name or flow_id must be provided")
|
|
||||||
|
|
||||||
if not flow:
|
|
||||||
raise ValueError(f"Flow {flow_name or flow_id} not found")
|
|
||||||
return await self.load_flow(flow.id, tweaks)
|
|
||||||
|
|
||||||
def build(self, *args: Any, **kwargs: Any) -> Any:
|
def build(self, *args: Any, **kwargs: Any) -> Any:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -11,49 +11,60 @@ from langchain.chains.base import Chain
|
||||||
from langchain.document_loaders.base import BaseLoader
|
from langchain.document_loaders.base import BaseLoader
|
||||||
from langchain_community.vectorstores import VectorStore
|
from langchain_community.vectorstores import VectorStore
|
||||||
from langchain_core.documents import Document
|
from langchain_core.documents import Document
|
||||||
from loguru import logger
|
from langflow.interface.custom.eval import eval_custom_component_code
|
||||||
from pydantic import ValidationError
|
from langflow.interface.custom.utils import get_function
|
||||||
|
|
||||||
from langflow.interface.custom_lists import CUSTOM_NODES
|
from langflow.interface.custom_lists import CUSTOM_NODES
|
||||||
from langflow.interface.importing.utils import eval_custom_component_code, get_function, import_by_type
|
from langflow.interface.importing.utils import import_by_type
|
||||||
from langflow.interface.initialize.llm import initialize_vertexai
|
from langflow.interface.initialize.llm import initialize_vertexai
|
||||||
from langflow.interface.initialize.utils import handle_format_kwargs, handle_node_type, handle_partial_variables
|
from langflow.interface.initialize.utils import (
|
||||||
|
handle_format_kwargs,
|
||||||
|
handle_node_type,
|
||||||
|
handle_partial_variables,
|
||||||
|
)
|
||||||
from langflow.interface.initialize.vector_store import vecstore_initializer
|
from langflow.interface.initialize.vector_store import vecstore_initializer
|
||||||
from langflow.interface.output_parsers.base import output_parser_creator
|
from langflow.interface.output_parsers.base import output_parser_creator
|
||||||
from langflow.interface.retrievers.base import retriever_creator
|
from langflow.interface.retrievers.base import retriever_creator
|
||||||
from langflow.interface.toolkits.base import toolkits_creator
|
from langflow.interface.toolkits.base import toolkits_creator
|
||||||
from langflow.interface.utils import load_file_into_dict
|
from langflow.interface.utils import load_file_into_dict
|
||||||
from langflow.interface.wrappers.base import wrapper_creator
|
from langflow.interface.wrappers.base import wrapper_creator
|
||||||
|
from langflow.schema.schema import Record
|
||||||
from langflow.utils import validate
|
from langflow.utils import validate
|
||||||
|
from langflow.utils.util import unescape_string
|
||||||
|
from loguru import logger
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from langflow import CustomComponent
|
from langflow import CustomComponent
|
||||||
|
|
||||||
|
|
||||||
def build_vertex_in_params(params: Dict) -> Dict:
|
|
||||||
from langflow.graph.vertex.base import Vertex
|
from langflow.graph.vertex.base import Vertex
|
||||||
|
|
||||||
# If any of the values in params is a Vertex, we will build it
|
|
||||||
return {key: value.build() if isinstance(value, Vertex) else value for key, value in params.items()}
|
|
||||||
|
|
||||||
|
|
||||||
async def instantiate_class(
|
async def instantiate_class(
|
||||||
node_type: str, base_type: str, load_from_db_fields: list[str], params: Dict, user_id=None
|
vertex: "Vertex",
|
||||||
|
user_id=None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Instantiate class from module type and key, and params"""
|
"""Instantiate class from module type and key, and params"""
|
||||||
|
vertex_type = vertex.vertex_type
|
||||||
|
base_type = vertex.base_type
|
||||||
|
params = vertex.params
|
||||||
params = convert_params_to_sets(params)
|
params = convert_params_to_sets(params)
|
||||||
params = convert_kwargs(params)
|
params = convert_kwargs(params)
|
||||||
|
|
||||||
if node_type in CUSTOM_NODES:
|
if vertex_type in CUSTOM_NODES:
|
||||||
if custom_node := CUSTOM_NODES.get(node_type):
|
if custom_node := CUSTOM_NODES.get(vertex_type):
|
||||||
if hasattr(custom_node, "initialize"):
|
if hasattr(custom_node, "initialize"):
|
||||||
return custom_node.initialize(**params)
|
return custom_node.initialize(**params)
|
||||||
return custom_node(**params)
|
return custom_node(**params)
|
||||||
logger.debug(f"Instantiating {node_type} of type {base_type}")
|
logger.debug(f"Instantiating {vertex_type} of type {base_type}")
|
||||||
class_object = import_by_type(_type=base_type, name=node_type)
|
if not base_type:
|
||||||
|
raise ValueError("No base type provided for vertex")
|
||||||
|
class_object = import_by_type(_type=base_type, name=vertex_type)
|
||||||
return await instantiate_based_on_type(
|
return await instantiate_based_on_type(
|
||||||
class_object, base_type, node_type, load_from_db_fields, params, user_id=user_id
|
class_object=class_object,
|
||||||
|
base_type=base_type,
|
||||||
|
node_type=vertex_type,
|
||||||
|
params=params,
|
||||||
|
user_id=user_id,
|
||||||
|
vertex=vertex,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -81,7 +92,14 @@ def convert_kwargs(params):
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
async def instantiate_based_on_type(class_object, base_type, node_type, load_from_db_fields, params, user_id):
|
async def instantiate_based_on_type(
|
||||||
|
class_object,
|
||||||
|
base_type,
|
||||||
|
node_type,
|
||||||
|
params,
|
||||||
|
user_id,
|
||||||
|
vertex,
|
||||||
|
):
|
||||||
if base_type == "agents":
|
if base_type == "agents":
|
||||||
return instantiate_agent(node_type, class_object, params)
|
return instantiate_agent(node_type, class_object, params)
|
||||||
elif base_type == "prompts":
|
elif base_type == "prompts":
|
||||||
|
|
@ -108,14 +126,20 @@ async def instantiate_based_on_type(class_object, base_type, node_type, load_fro
|
||||||
return instantiate_chains(node_type, class_object, params)
|
return instantiate_chains(node_type, class_object, params)
|
||||||
elif base_type == "output_parsers":
|
elif base_type == "output_parsers":
|
||||||
return instantiate_output_parser(node_type, class_object, params)
|
return instantiate_output_parser(node_type, class_object, params)
|
||||||
elif base_type == "llms":
|
elif base_type == "models":
|
||||||
return instantiate_llm(node_type, class_object, params)
|
return instantiate_llm(node_type, class_object, params)
|
||||||
elif base_type == "retrievers":
|
elif base_type == "retrievers":
|
||||||
return instantiate_retriever(node_type, class_object, params)
|
return instantiate_retriever(node_type, class_object, params)
|
||||||
elif base_type == "memory":
|
elif base_type == "memory":
|
||||||
return instantiate_memory(node_type, class_object, params)
|
return instantiate_memory(node_type, class_object, params)
|
||||||
elif base_type == "custom_components":
|
elif base_type == "custom_components":
|
||||||
return await instantiate_custom_component(node_type, class_object, load_from_db_fields, params, user_id)
|
return await instantiate_custom_component(
|
||||||
|
node_type,
|
||||||
|
class_object,
|
||||||
|
params,
|
||||||
|
user_id,
|
||||||
|
vertex,
|
||||||
|
)
|
||||||
elif base_type == "wrappers":
|
elif base_type == "wrappers":
|
||||||
return instantiate_wrapper(node_type, class_object, params)
|
return instantiate_wrapper(node_type, class_object, params)
|
||||||
else:
|
else:
|
||||||
|
|
@ -137,11 +161,17 @@ def update_params_with_load_from_db_fields(custom_component: "CustomComponent",
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
async def instantiate_custom_component(node_type, class_object, load_from_db_fields, params, user_id):
|
async def instantiate_custom_component(node_type, class_object, params, user_id, vertex):
|
||||||
params_copy = params.copy()
|
params_copy = params.copy()
|
||||||
class_object: "CustomComponent" = eval_custom_component_code(params_copy.pop("code"))
|
class_object: Type["CustomComponent"] = eval_custom_component_code(params_copy.pop("code"))
|
||||||
custom_component = class_object(user_id=user_id)
|
custom_component: "CustomComponent" = class_object(
|
||||||
params_copy = update_params_with_load_from_db_fields(custom_component, params_copy, load_from_db_fields)
|
user_id=user_id,
|
||||||
|
parameters=params_copy,
|
||||||
|
vertex=vertex,
|
||||||
|
selected_output_type=vertex.selected_output_type,
|
||||||
|
)
|
||||||
|
params_copy = update_params_with_load_from_db_fields(custom_component, params_copy, vertex.load_from_db_fields)
|
||||||
|
|
||||||
if "retriever" in params_copy and hasattr(params_copy["retriever"], "as_retriever"):
|
if "retriever" in params_copy and hasattr(params_copy["retriever"], "as_retriever"):
|
||||||
params_copy["retriever"] = params_copy["retriever"].as_retriever()
|
params_copy["retriever"] = params_copy["retriever"].as_retriever()
|
||||||
|
|
||||||
|
|
@ -150,12 +180,14 @@ async def instantiate_custom_component(node_type, class_object, load_from_db_fie
|
||||||
|
|
||||||
if is_async:
|
if is_async:
|
||||||
# Await the build method directly if it's async
|
# Await the build method directly if it's async
|
||||||
built_object = await custom_component.build(**params_copy)
|
build_result = await custom_component.build(**params_copy)
|
||||||
else:
|
else:
|
||||||
# Call the build method directly if it's sync
|
# Call the build method directly if it's sync
|
||||||
built_object = custom_component.build(**params_copy)
|
build_result = custom_component.build(**params_copy)
|
||||||
|
custom_repr = custom_component.custom_repr()
|
||||||
return built_object, {"repr": custom_component.custom_repr()}
|
if not custom_repr and isinstance(build_result, (dict, Record, str)):
|
||||||
|
custom_repr = build_result
|
||||||
|
return custom_component, build_result, {"repr": custom_repr}
|
||||||
|
|
||||||
|
|
||||||
def instantiate_wrapper(node_type, class_object, params):
|
def instantiate_wrapper(node_type, class_object, params):
|
||||||
|
|
@ -387,7 +419,10 @@ def instantiate_textsplitter(
|
||||||
# separators might come in as an escaped string like \\n
|
# separators might come in as an escaped string like \\n
|
||||||
# so we need to convert it to a string
|
# so we need to convert it to a string
|
||||||
if "separators" in params:
|
if "separators" in params:
|
||||||
params["separators"] = params["separators"].encode().decode("unicode-escape")
|
if isinstance(params["separators"], str):
|
||||||
|
params["separators"] = unescape_string(params["separators"])
|
||||||
|
elif isinstance(params["separators"], list):
|
||||||
|
params["separators"] = [unescape_string(separator) for separator in params["separators"]]
|
||||||
text_splitter = class_object(**params)
|
text_splitter = class_object(**params)
|
||||||
else:
|
else:
|
||||||
from langchain.text_splitter import Language
|
from langchain.text_splitter import Language
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,10 @@ from typing import Any
|
||||||
from langchain.agents import AgentExecutor
|
from langchain.agents import AgentExecutor
|
||||||
from langchain.chains.base import Chain
|
from langchain.chains.base import Chain
|
||||||
from langchain_core.runnables import Runnable
|
from langchain_core.runnables import Runnable
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from langflow.api.v1.schemas import ChatMessage
|
from langflow.api.v1.schemas import ChatMessage
|
||||||
from langflow.interface.utils import try_setting_streaming_options
|
from langflow.interface.utils import try_setting_streaming_options
|
||||||
from langflow.processing.base import get_result_and_steps
|
from langflow.processing.base import get_result_and_steps
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
LANGCHAIN_RUNNABLES = (Chain, Runnable, AgentExecutor)
|
LANGCHAIN_RUNNABLES = (Chain, Runnable, AgentExecutor)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
from typing import Any, Callable, Optional, Union
|
from typing import Any, Callable, Optional, Union
|
||||||
|
|
||||||
from langflow.field_typing.range_spec import RangeSpec
|
from langflow.field_typing.range_spec import RangeSpec
|
||||||
from pydantic import BaseModel, ConfigDict, Field, field_serializer
|
from pydantic import (
|
||||||
|
BaseModel,
|
||||||
|
ConfigDict,
|
||||||
|
Field,
|
||||||
|
field_serializer,
|
||||||
|
field_validator,
|
||||||
|
model_serializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TemplateField(BaseModel):
|
class TemplateField(BaseModel):
|
||||||
|
|
@ -29,7 +36,7 @@ class TemplateField(BaseModel):
|
||||||
"""The value of the field. Default is None."""
|
"""The value of the field. Default is None."""
|
||||||
|
|
||||||
file_types: list[str] = Field(default=[], serialization_alias="fileTypes")
|
file_types: list[str] = Field(default=[], serialization_alias="fileTypes")
|
||||||
"""List of file types associated with the field. Default is an empty list. (duplicate)"""
|
"""List of file types associated with the field . Default is an empty list."""
|
||||||
|
|
||||||
file_path: Optional[str] = ""
|
file_path: Optional[str] = ""
|
||||||
"""The file path of the field if it is a file. Defaults to None."""
|
"""The file path of the field if it is a file. Defaults to None."""
|
||||||
|
|
@ -58,18 +65,38 @@ class TemplateField(BaseModel):
|
||||||
info: Optional[str] = ""
|
info: Optional[str] = ""
|
||||||
"""Additional information about the field to be shown in the tooltip. Defaults to an empty string."""
|
"""Additional information about the field to be shown in the tooltip. Defaults to an empty string."""
|
||||||
|
|
||||||
refresh: Optional[bool] = None
|
real_time_refresh: Optional[bool] = None
|
||||||
"""Specifies if the field should be refreshed. Defaults to False."""
|
"""Specifies if the field should have real time refresh. `refresh_button` must be False. Defaults to None."""
|
||||||
|
|
||||||
|
refresh_button: Optional[bool] = None
|
||||||
|
"""Specifies if the field should have a refresh button. Defaults to False."""
|
||||||
|
refresh_button_text: Optional[str] = None
|
||||||
|
"""Specifies the text for the refresh button. Defaults to None."""
|
||||||
|
|
||||||
range_spec: Optional[RangeSpec] = Field(default=None, serialization_alias="rangeSpec")
|
range_spec: Optional[RangeSpec] = Field(default=None, serialization_alias="rangeSpec")
|
||||||
"""Range specification for the field. Defaults to None."""
|
"""Range specification for the field. Defaults to None."""
|
||||||
|
|
||||||
load_from_db: bool = False
|
load_from_db: bool = False
|
||||||
"""Specifies if the field should be loaded from the database. Defaults to False."""
|
"""Specifies if the field should be loaded from the database. Defaults to False."""
|
||||||
|
title_case: bool = False
|
||||||
|
"""Specifies if the field should be displayed in title case. Defaults to True."""
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return self.model_dump(by_alias=True, exclude_none=True)
|
return self.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
|
@model_serializer(mode="wrap")
|
||||||
|
def serialize_model(self, handler):
|
||||||
|
result = handler(self)
|
||||||
|
# If the field is str, we add the Text input type
|
||||||
|
if self.field_type in ["str", "Text"]:
|
||||||
|
if "input_types" not in result:
|
||||||
|
result["input_types"] = ["Text"]
|
||||||
|
if self.field_type == "Text":
|
||||||
|
result["type"] = "str"
|
||||||
|
else:
|
||||||
|
result["type"] = self.field_type
|
||||||
|
return result
|
||||||
|
|
||||||
@field_serializer("file_path")
|
@field_serializer("file_path")
|
||||||
def serialize_file_path(self, value):
|
def serialize_file_path(self, value):
|
||||||
return value if self.field_type == "file" else ""
|
return value if self.field_type == "file" else ""
|
||||||
|
|
@ -79,3 +106,24 @@ class TemplateField(BaseModel):
|
||||||
if value == "float" and self.range_spec is None:
|
if value == "float" and self.range_spec is None:
|
||||||
self.range_spec = RangeSpec()
|
self.range_spec = RangeSpec()
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@field_serializer("display_name")
|
||||||
|
def serialize_display_name(self, value, _info):
|
||||||
|
# If display_name is not set, use name and convert to title case
|
||||||
|
# if title_case is True
|
||||||
|
if value is None:
|
||||||
|
# name is probably a snake_case string
|
||||||
|
# Ex: "file_path" -> "File Path"
|
||||||
|
value = self.name.replace("_", " ")
|
||||||
|
if self.title_case:
|
||||||
|
value = value.title()
|
||||||
|
return value
|
||||||
|
|
||||||
|
@field_validator("file_types")
|
||||||
|
def validate_file_types(cls, value):
|
||||||
|
if not isinstance(value, list):
|
||||||
|
raise ValueError("file_types must be a list")
|
||||||
|
return [
|
||||||
|
(f".{file_type}" if isinstance(file_type, str) and not file_type.startswith(".") else file_type)
|
||||||
|
for file_type in value
|
||||||
|
]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue