merge fix
This commit is contained in:
commit
34f5a1300a
73 changed files with 2165 additions and 1531 deletions
|
|
@ -197,7 +197,7 @@ def format_elapsed_time(elapsed_time: float) -> str:
|
|||
return f"{minutes} {minutes_unit}, {seconds} {seconds_unit}"
|
||||
|
||||
|
||||
def build_and_cache_graph(
|
||||
async def build_and_cache_graph(
|
||||
flow_id: str,
|
||||
session: Session,
|
||||
chat_service: "ChatService",
|
||||
|
|
@ -212,7 +212,7 @@ def build_and_cache_graph(
|
|||
graph = other_graph
|
||||
else:
|
||||
graph = graph.update(other_graph)
|
||||
chat_service.set_cache(flow_id, graph)
|
||||
await chat_service.set_cache(flow_id, graph)
|
||||
return graph
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -58,9 +58,9 @@ async def get_vertices(
|
|||
try:
|
||||
# First, we need to check if the flow_id is in the cache
|
||||
graph = None
|
||||
if cache := chat_service.get_cache(flow_id):
|
||||
if cache := await chat_service.get_cache(flow_id):
|
||||
graph = cache.get("result")
|
||||
graph = build_and_cache_graph(flow_id, session, chat_service, graph)
|
||||
graph = await build_and_cache_graph(flow_id, session, chat_service, graph)
|
||||
if stop_component_id or start_component_id:
|
||||
try:
|
||||
vertices = graph.sort_vertices(stop_component_id, start_component_id)
|
||||
|
|
@ -98,11 +98,11 @@ async def build_vertex(
|
|||
next_vertices_ids = []
|
||||
try:
|
||||
start_time = time.perf_counter()
|
||||
cache = chat_service.get_cache(flow_id)
|
||||
cache = await chat_service.get_cache(flow_id)
|
||||
if not cache:
|
||||
# If there's no cache
|
||||
logger.warning(f"No cache found for {flow_id}. Building graph starting at {vertex_id}")
|
||||
graph = build_and_cache_graph(flow_id=flow_id, session=next(get_session()), chat_service=chat_service)
|
||||
graph = await build_and_cache_graph(flow_id=flow_id, session=next(get_session()), chat_service=chat_service)
|
||||
else:
|
||||
graph = cache.get("result")
|
||||
result_data_response = ResultDataResponse(results={})
|
||||
|
|
@ -121,8 +121,11 @@ async def build_vertex(
|
|||
artifacts = vertex.artifacts
|
||||
else:
|
||||
raise ValueError(f"No result found for vertex {vertex_id}")
|
||||
next_vertices_ids = vertex.successors_ids
|
||||
next_vertices_ids = [v for v in next_vertices_ids if graph.should_run_vertex(v)]
|
||||
async with chat_service._cache_locks[flow_id] as lock:
|
||||
graph.remove_from_predecessors(vertex_id)
|
||||
next_vertices_ids = vertex.successors_ids
|
||||
next_vertices_ids = [v for v in next_vertices_ids if graph.should_run_vertex(v)]
|
||||
await chat_service.set_cache(flow_id=flow_id, data=graph, lock=lock)
|
||||
|
||||
result_data_response = ResultDataResponse(**result_dict.model_dump())
|
||||
|
||||
|
|
@ -134,7 +137,7 @@ async def build_vertex(
|
|||
artifacts = {}
|
||||
# If there's an error building the vertex
|
||||
# we need to clear the cache
|
||||
chat_service.clear_cache(flow_id)
|
||||
await chat_service.clear_cache(flow_id)
|
||||
|
||||
# Log the vertex build
|
||||
if not vertex.will_stream:
|
||||
|
|
@ -157,7 +160,7 @@ async def build_vertex(
|
|||
inactivated_vertices = list(graph.inactivated_vertices)
|
||||
graph.reset_inactivated_vertices()
|
||||
graph.reset_activated_vertices()
|
||||
chat_service.set_cache(flow_id, graph)
|
||||
await chat_service.set_cache(flow_id, graph)
|
||||
|
||||
# graph.stop_vertex tells us if the user asked
|
||||
# to stop the build of the graph at a certain vertex
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from http import HTTPStatus
|
||||
from typing import Annotated, Any, List, Optional, Union
|
||||
from typing import Annotated, List, Optional, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, UploadFile, status
|
||||
|
|
@ -8,14 +8,16 @@ from sqlmodel import Session, select
|
|||
|
||||
from langflow.api.utils import update_frontend_node_with_template_values
|
||||
from langflow.api.v1.schemas import (
|
||||
CustomComponentCode,
|
||||
CustomComponentRequest,
|
||||
InputValueRequest,
|
||||
ProcessResponse,
|
||||
RunResponse,
|
||||
TaskStatusResponse,
|
||||
Tweaks,
|
||||
UpdateCustomComponentRequest,
|
||||
UploadFileResponse,
|
||||
)
|
||||
from langflow.graph.schema import RunOutputs
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.interface.custom.directory_reader import DirectoryReader
|
||||
from langflow.interface.custom.utils import build_custom_component_template
|
||||
|
|
@ -24,12 +26,7 @@ from langflow.services.auth.utils import api_key_security, get_current_active_us
|
|||
from langflow.services.cache.utils import save_uploaded_file
|
||||
from langflow.services.database.models.flow import Flow
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.deps import (
|
||||
get_session,
|
||||
get_session_service,
|
||||
get_settings_service,
|
||||
get_task_service,
|
||||
)
|
||||
from langflow.services.deps import get_session, get_session_service, get_settings_service, get_task_service
|
||||
from langflow.services.session.service import SessionService
|
||||
from langflow.services.task.service import TaskService
|
||||
|
||||
|
|
@ -56,8 +53,8 @@ def get_all(
|
|||
async def run_flow_with_caching(
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
flow_id: str,
|
||||
inputs: Optional[List[InputValueRequest]] = None,
|
||||
outputs: Optional[List[str]] = None,
|
||||
inputs: Optional[List[InputValueRequest]] = [],
|
||||
outputs: Optional[List[str]] = [],
|
||||
tweaks: Annotated[Optional[Tweaks], Body(embed=True)] = None, # noqa: F821
|
||||
stream: Annotated[bool, Body(embed=True)] = False, # noqa: F821
|
||||
session_id: Annotated[Union[None, str], Body(embed=True)] = None, # noqa: F821
|
||||
|
|
@ -102,25 +99,20 @@ async def run_flow_with_caching(
|
|||
This endpoint facilitates complex flow executions with customized inputs, outputs, and configurations, catering to diverse application requirements.
|
||||
"""
|
||||
try:
|
||||
if inputs is not None:
|
||||
input_values: list[dict[str, Union[str, list[str]]]] = [_input.model_dump() for _input in inputs]
|
||||
else:
|
||||
input_values = [{}]
|
||||
|
||||
if outputs is None:
|
||||
outputs = []
|
||||
|
||||
if session_id:
|
||||
session_data = await session_service.load_session(session_id, flow_id=flow_id)
|
||||
graph, artifacts = session_data if session_data else (None, None)
|
||||
task_result: Any = None
|
||||
task_result: List[RunOutputs] = []
|
||||
if not graph:
|
||||
raise ValueError("Graph not found in the session")
|
||||
task_result, session_id = await run_graph(
|
||||
graph=graph,
|
||||
flow_id=flow_id,
|
||||
session_id=session_id,
|
||||
inputs=input_values,
|
||||
inputs=inputs,
|
||||
outputs=outputs,
|
||||
artifacts=artifacts,
|
||||
session_service=session_service,
|
||||
|
|
@ -142,7 +134,7 @@ async def run_flow_with_caching(
|
|||
graph=graph_data,
|
||||
flow_id=flow_id,
|
||||
session_id=session_id,
|
||||
inputs=input_values,
|
||||
inputs=inputs,
|
||||
outputs=outputs,
|
||||
artifacts={},
|
||||
session_service=session_service,
|
||||
|
|
@ -253,12 +245,12 @@ def get_version():
|
|||
|
||||
@router.post("/custom_component", status_code=HTTPStatus.OK)
|
||||
async def custom_component(
|
||||
raw_code: CustomComponentCode,
|
||||
raw_code: CustomComponentRequest,
|
||||
user: User = Depends(get_current_active_user),
|
||||
):
|
||||
component = CustomComponent(code=raw_code.code)
|
||||
|
||||
built_frontend_node = build_custom_component_template(component, user_id=user.id)
|
||||
built_frontend_node, _ = build_custom_component_template(component, user_id=user.id)
|
||||
|
||||
built_frontend_node = update_frontend_node_with_template_values(built_frontend_node, raw_code.frontend_node)
|
||||
return built_frontend_node
|
||||
|
|
@ -275,23 +267,46 @@ async def reload_custom_component(path: str, user: User = Depends(get_current_ac
|
|||
raise ValueError(content)
|
||||
|
||||
extractor = CustomComponent(code=content)
|
||||
return build_custom_component_template(extractor, user_id=user.id)
|
||||
frontend_node, _ = build_custom_component_template(extractor, user_id=user.id)
|
||||
return frontend_node
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
|
||||
@router.post("/custom_component/update", status_code=HTTPStatus.OK)
|
||||
async def custom_component_update(
|
||||
raw_code: CustomComponentCode,
|
||||
code_request: UpdateCustomComponentRequest,
|
||||
user: User = Depends(get_current_active_user),
|
||||
):
|
||||
component = CustomComponent(code=raw_code.code)
|
||||
"""
|
||||
Update a custom component with the provided code request.
|
||||
|
||||
component_node = build_custom_component_template(
|
||||
component,
|
||||
user_id=user.id,
|
||||
update_field=raw_code.field,
|
||||
update_field_value=raw_code.field_value,
|
||||
)
|
||||
# Update the field
|
||||
return component_node
|
||||
This endpoint generates the CustomComponentFrontendNode normally but then runs the `update_build_config` method
|
||||
on the latest version of the template. This ensures that every time it runs, it has the latest version of the template.
|
||||
|
||||
Args:
|
||||
code_request (CustomComponentRequest): The code request containing the updated code for the custom component.
|
||||
user (User, optional): The user making the request. Defaults to the current active user.
|
||||
|
||||
Returns:
|
||||
dict: The updated custom component node.
|
||||
|
||||
"""
|
||||
try:
|
||||
component = CustomComponent(code=code_request.code)
|
||||
|
||||
component_node, cc_instance = build_custom_component_template(
|
||||
component,
|
||||
user_id=user.id,
|
||||
)
|
||||
|
||||
updated_build_config = cc_instance.update_build_config(
|
||||
build_config=code_request.get_template(),
|
||||
field_value=code_request.field_value,
|
||||
field_name=code_request.field,
|
||||
)
|
||||
component_node["template"] = updated_build_config
|
||||
|
||||
return component_node
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
|
|
|||
|
|
@ -211,7 +211,5 @@ async def download_file(
|
|||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Download all flows as a file."""
|
||||
flows = read_flows(
|
||||
current_user=current_user, session=session, settings_service=settings_service
|
||||
)
|
||||
flows = read_flows(current_user=current_user, session=session, settings_service=settings_service)
|
||||
return FlowListRead(flows=flows)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlmodel import Session
|
||||
|
||||
from langflow.api.v1.schemas import Token
|
||||
from langflow.services.auth.utils import (
|
||||
authenticate_user,
|
||||
|
|
@ -8,7 +10,6 @@ from langflow.services.auth.utils import (
|
|||
create_user_tokens,
|
||||
)
|
||||
from langflow.services.deps import get_session, get_settings_service
|
||||
from sqlmodel import Session
|
||||
|
||||
router = APIRouter(tags=["Login"])
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ async def login_to_get_access_token(
|
|||
httponly=auth_settings.REFRESH_HTTPONLY,
|
||||
samesite=auth_settings.REFRESH_SAME_SITE,
|
||||
secure=auth_settings.REFRESH_SECURE,
|
||||
expires=auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES*60,
|
||||
expires=auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES * 60,
|
||||
)
|
||||
response.set_cookie(
|
||||
"access_token_lf",
|
||||
|
|
@ -48,7 +49,7 @@ async def login_to_get_access_token(
|
|||
httponly=auth_settings.ACCESS_HTTPONLY,
|
||||
samesite=auth_settings.ACCESS_SAME_SITE,
|
||||
secure=auth_settings.ACCESS_SECURE,
|
||||
expires=auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES*60,
|
||||
expires=auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||||
)
|
||||
return tokens
|
||||
else:
|
||||
|
|
@ -74,7 +75,7 @@ async def auto_login(
|
|||
httponly=auth_settings.ACCESS_HTTPONLY,
|
||||
samesite=auth_settings.ACCESS_SAME_SITE,
|
||||
secure=auth_settings.ACCESS_SECURE,
|
||||
expires=auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES*60,
|
||||
expires=auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||||
)
|
||||
return tokens
|
||||
|
||||
|
|
@ -98,10 +99,10 @@ async def refresh_token(request: Request, response: Response, settings_service=D
|
|||
response.set_cookie(
|
||||
"refresh_token_lf",
|
||||
tokens["refresh_token"],
|
||||
httponly=auth_settings.REFRESH_TOKEN_HTTPONLY,
|
||||
httponly=auth_settings.REFRESH_HTTPONLY,
|
||||
samesite=auth_settings.REFRESH_SAME_SITE,
|
||||
secure=auth_settings.REFRESH_SECURE,
|
||||
expires=auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES*60,
|
||||
expires=auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES * 60,
|
||||
)
|
||||
response.set_cookie(
|
||||
"access_token_lf",
|
||||
|
|
@ -109,7 +110,7 @@ async def refresh_token(request: Request, response: Response, settings_service=D
|
|||
httponly=auth_settings.ACCESS_HTTPONLY,
|
||||
samesite=auth_settings.ACCESS_SAME_SITE,
|
||||
secure=auth_settings.ACCESS_SECURE,
|
||||
expires=auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES*60,
|
||||
expires=auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||||
)
|
||||
return tokens
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -4,8 +4,17 @@ from pathlib import Path
|
|||
from typing import Any, Dict, List, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field, RootModel, field_validator, model_serializer
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
RootModel,
|
||||
field_validator,
|
||||
model_serializer,
|
||||
)
|
||||
|
||||
from langflow.graph.schema import RunOutputs
|
||||
from langflow.schema import dotdict
|
||||
from langflow.services.database.models.api_key.model import ApiKeyRead
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
from langflow.services.database.models.flow import FlowCreate, FlowRead
|
||||
|
|
@ -49,7 +58,7 @@ class ProcessResponse(BaseModel):
|
|||
class RunResponse(BaseModel):
|
||||
"""Run response schema."""
|
||||
|
||||
outputs: Optional[List[Any]] = None
|
||||
outputs: Optional[List[RunOutputs]] = []
|
||||
session_id: Optional[str] = None
|
||||
|
||||
@model_serializer(mode="wrap")
|
||||
|
|
@ -161,13 +170,21 @@ class StreamData(BaseModel):
|
|||
return f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n"
|
||||
|
||||
|
||||
class CustomComponentCode(BaseModel):
|
||||
class CustomComponentRequest(BaseModel):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
code: str
|
||||
field: Optional[str] = None
|
||||
field_value: Optional[Any] = None
|
||||
frontend_node: Optional[dict] = None
|
||||
|
||||
|
||||
class UpdateCustomComponentRequest(CustomComponentRequest):
|
||||
field: str
|
||||
field_value: Optional[Union[str, int, float, bool, dict, list]] = None
|
||||
template: dict
|
||||
|
||||
def get_template(self):
|
||||
return dotdict(self.template)
|
||||
|
||||
|
||||
class CustomComponentResponseError(BaseModel):
|
||||
detail: str
|
||||
traceback: str
|
||||
|
|
@ -244,7 +261,7 @@ class VerticesBuiltResponse(BaseModel):
|
|||
|
||||
|
||||
class InputValueRequest(BaseModel):
|
||||
components: Optional[List[str]] = None
|
||||
components: Optional[List[str]] = []
|
||||
input_value: Optional[str] = None
|
||||
|
||||
# add an example
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ TEXT_FILE_TYPES = [
|
|||
"xml",
|
||||
"html",
|
||||
"htm",
|
||||
"pdf",
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -54,9 +55,7 @@ def retrieve_file_paths(
|
|||
|
||||
glob = "**/*" if recursive else "*"
|
||||
paths = walk_level(path_obj, depth) if depth else path_obj.glob(glob)
|
||||
file_paths = [
|
||||
Text(p) for p in paths if p.is_file() and match_types(p) and is_not_hidden(p)
|
||||
]
|
||||
file_paths = [Text(p) for p in paths if p.is_file() and match_types(p) and is_not_hidden(p)]
|
||||
|
||||
return file_paths
|
||||
|
||||
|
|
@ -85,19 +84,28 @@ def read_text_file(file_path: str) -> str:
|
|||
return f.read()
|
||||
|
||||
|
||||
def parse_pdf_to_text(file_path: str) -> str:
|
||||
from pypdf import PdfReader # type: ignore
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
reader = PdfReader(f)
|
||||
return "\n\n".join([page.extract_text() for page in reader.pages])
|
||||
|
||||
|
||||
def parse_text_file_to_record(file_path: str, silent_errors: bool) -> Optional[Record]:
|
||||
try:
|
||||
text = read_text_file(file_path)
|
||||
if file_path.endswith(".pdf"):
|
||||
text = parse_pdf_to_text(file_path)
|
||||
else:
|
||||
text = read_text_file(file_path)
|
||||
# if file is json, yaml, or xml, we can parse it
|
||||
if file_path.endswith(".json"):
|
||||
|
||||
text = json.loads(text)
|
||||
elif file_path.endswith(".yaml") or file_path.endswith(".yml"):
|
||||
|
||||
text = yaml.safe_load(text)
|
||||
elif file_path.endswith(".xml"):
|
||||
|
||||
text = ET.fromstring(text)
|
||||
xml_element = ET.fromstring(text)
|
||||
text = ET.tostring(xml_element, encoding="unicode")
|
||||
except Exception as e:
|
||||
if not silent_errors:
|
||||
raise ValueError(f"Error loading file {file_path}: {e}") from e
|
||||
|
|
@ -116,10 +124,7 @@ def get_elements(
|
|||
if use_multithreading:
|
||||
records = parallel_load_records(file_paths, silent_errors, max_concurrency)
|
||||
else:
|
||||
records = [
|
||||
partition_file_to_record(file_path, silent_errors)
|
||||
for file_path in file_paths
|
||||
]
|
||||
records = [partition_file_to_record(file_path, silent_errors) for file_path in file_paths]
|
||||
records = list(filter(None, records))
|
||||
return records
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class ChatComponent(CustomComponent):
|
|||
"session_id": {
|
||||
"display_name": "Session ID",
|
||||
"info": "If provided, the message will be stored in the memory.",
|
||||
"advanced": True,
|
||||
},
|
||||
"return_record": {
|
||||
"display_name": "Return Record",
|
||||
|
|
|
|||
70
src/backend/langflow/components/agents/ReActAgent.py
Normal file
70
src/backend/langflow/components/agents/ReActAgent.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# from typing import Dict, List
|
||||
|
||||
# import dspy
|
||||
|
||||
# from langflow import CustomComponent
|
||||
# from langflow.field_typing import Text
|
||||
|
||||
|
||||
# class ReActAgentComponent(CustomComponent):
|
||||
# display_name = "ReAct Agent"
|
||||
# description = "A component to create a ReAct Agent."
|
||||
# icon = "user-secret"
|
||||
|
||||
# def build_config(self):
|
||||
# return {
|
||||
# "input_value": {
|
||||
# "display_name": "Input",
|
||||
# "input_types": ["Text"],
|
||||
# "info": "The input value for the ReAct Agent.",
|
||||
# },
|
||||
# "instructions": {
|
||||
# "display_name": "Instructions",
|
||||
# "info": "The Prompt.",
|
||||
# },
|
||||
# "inputs": {
|
||||
# "display_name": "Inputs",
|
||||
# "info": "The Name and Description of the Input Fields.",
|
||||
# },
|
||||
# "outputs": {
|
||||
# "display_name": "Outputs",
|
||||
# "info": "The Name and Description of the Output Fields.",
|
||||
# },
|
||||
# }
|
||||
|
||||
# def build(
|
||||
# self,
|
||||
# input_value: List[dict],
|
||||
# instructions: Text,
|
||||
# inputs: List[dict],
|
||||
# outputs: List[Dict],
|
||||
# ) -> Text:
|
||||
# # inputs is a list of dictionaries where the key is the name of the input
|
||||
# # and the value is the description of the input
|
||||
# input_fields = (
|
||||
# {}
|
||||
# ) # dict[str, FieldInfo] InputField and OutputField are subclasses of pydantic.Field
|
||||
# for input_dict in inputs:
|
||||
# for name, description in input_dict.items():
|
||||
# prefix = name if ":" in name else f"{name}:"
|
||||
# input_fields[name] = dspy.InputField(
|
||||
# prefix=prefix, description=description
|
||||
# )
|
||||
|
||||
# output_fields = {} # dict[str, FieldInfo]
|
||||
# for output_dict in outputs:
|
||||
# for name, description in output_dict.items():
|
||||
# prefix = name if ":" in name else f"{name}:"
|
||||
# output_fields[name] = dspy.OutputField(
|
||||
# prefix=prefix, description=description
|
||||
# )
|
||||
|
||||
# signature = dspy.make_signature(inputs, instructions=instructions)
|
||||
# agent = dspy.ReAct(
|
||||
# signature=signature,
|
||||
# )
|
||||
# inputs_dict = {}
|
||||
# for input_dict in input_value:
|
||||
# inputs_dict.update(input_dict)
|
||||
|
||||
# result = agent(inputs_dict)
|
||||
|
|
@ -56,9 +56,7 @@ class APIRequest(CustomComponent):
|
|||
data = body if body else None
|
||||
payload = json.dumps(data)
|
||||
try:
|
||||
response = await client.request(
|
||||
method, url, headers=headers, content=payload, timeout=timeout
|
||||
)
|
||||
response = await client.request(method, url, headers=headers, content=payload, timeout=timeout)
|
||||
try:
|
||||
result = response.json()
|
||||
except Exception:
|
||||
|
|
@ -108,13 +106,10 @@ class APIRequest(CustomComponent):
|
|||
bodies = [body.data]
|
||||
if len(urls) != len(bodies):
|
||||
# add bodies with None
|
||||
bodies += [None] * (len(urls) - len(bodies))
|
||||
bodies += [None] * (len(urls) - len(bodies)) # type: ignore
|
||||
async with httpx.AsyncClient() as client:
|
||||
results = await asyncio.gather(
|
||||
*[
|
||||
self.make_request(client, method, u, headers, rec, timeout)
|
||||
for u, rec in zip(urls, bodies)
|
||||
]
|
||||
*[self.make_request(client, method, u, headers, rec, timeout) for u, rec in zip(urls, bodies)]
|
||||
)
|
||||
self.status = results
|
||||
return results
|
||||
|
|
|
|||
|
|
@ -53,20 +53,14 @@ class DirectoryComponent(CustomComponent):
|
|||
silent_errors: bool = False,
|
||||
use_multithreading: bool = True,
|
||||
) -> List[Optional[Record]]:
|
||||
|
||||
resolved_path = self.resolve_path(path)
|
||||
file_paths = retrieve_file_paths(resolved_path, load_hidden, recursive, depth)
|
||||
loaded_records = []
|
||||
|
||||
if use_multithreading:
|
||||
loaded_records = parallel_load_records(
|
||||
file_paths, silent_errors, max_concurrency
|
||||
)
|
||||
loaded_records = parallel_load_records(file_paths, silent_errors, max_concurrency)
|
||||
else:
|
||||
loaded_records = [
|
||||
parse_text_file_to_record(file_path, silent_errors)
|
||||
for file_path in file_paths
|
||||
]
|
||||
loaded_records = [parse_text_file_to_record(file_path, silent_errors) for file_path in file_paths]
|
||||
loaded_records = list(filter(None, loaded_records))
|
||||
self.status = loaded_records
|
||||
return loaded_records
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class URLComponent(CustomComponent):
|
|||
"urls": {"display_name": "URL"},
|
||||
}
|
||||
|
||||
async def build(
|
||||
def build(
|
||||
self,
|
||||
urls: list[str],
|
||||
) -> list[Record]:
|
||||
|
|
|
|||
|
|
@ -21,9 +21,7 @@ class ExtractKeyFromRecordComponent(CustomComponent):
|
|||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self, record: Record, keys: list[str], silent_error: bool = True
|
||||
) -> Record:
|
||||
def build(self, record: Record, keys: list[str], silent_error: bool = True) -> Record:
|
||||
"""
|
||||
Extracts the keys from a record.
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,10 @@ from .ExtractDataFromRecord import ExtractKeyFromRecordComponent
|
|||
from .GetNotified import GetNotifiedComponent
|
||||
from .ListFlows import ListFlowsComponent
|
||||
from .MergeRecords import MergeRecordsComponent
|
||||
from .MessageHistory import MessageHistoryComponent
|
||||
from .Notify import NotifyComponent
|
||||
from .RunFlow import RunFlowComponent
|
||||
from .RunnableExecutor import RunnableExecComponent
|
||||
from .SQLExecutor import SQLExecutorComponent
|
||||
from .TextToRecord import TextToRecordComponent
|
||||
|
||||
__all__ = [
|
||||
"ClearMessageHistoryComponent",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ class UpdateRecordComponent(CustomComponent):
|
|||
"new_data": {
|
||||
"display_name": "New Data",
|
||||
"info": "The new data to update the record with.",
|
||||
"input_types": ["Text"],
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
from .CustomComponent import Component
|
||||
from .DocumentToRecord import DocumentToRecordComponent
|
||||
from .IDGenerator import UUIDGeneratorComponent
|
||||
from .MessageHistory import MessageHistoryComponent
|
||||
from .PythonFunction import PythonFunctionComponent
|
||||
from .RecordsAsText import RecordsAsTextComponent
|
||||
from .TextToRecord import TextToRecordComponent
|
||||
from .UpdateRecord import UpdateRecordComponent
|
||||
|
||||
__all__ = [
|
||||
"Component",
|
||||
"UpdateRecordComponent",
|
||||
"DocumentToRecordComponent",
|
||||
"UUIDGeneratorComponent",
|
||||
"PythonFunctionComponent",
|
||||
"RecordsAsTextComponent",
|
||||
"TextToRecordComponent",
|
||||
"MessageHistoryComponent",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -24,9 +24,7 @@ class PromptComponent(CustomComponent):
|
|||
|
||||
prompt_template = PromptTemplate.from_template(Text(template))
|
||||
kwargs = dict_values_to_string(kwargs)
|
||||
kwargs = {
|
||||
k: "\n".join(v) if isinstance(v, list) else v for k, v in kwargs.items()
|
||||
}
|
||||
kwargs = {k: "\n".join(v) if isinstance(v, list) else v for k, v in kwargs.items()}
|
||||
try:
|
||||
formated_prompt = prompt_template.format(**kwargs)
|
||||
except Exception as exc:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from langchain_community.chat_models.cohere import ChatCohere
|
||||
from pydantic.v1 import SecretStr
|
||||
|
||||
from langflow.components.models.base.model import LCModelComponent
|
||||
from langflow.field_typing import Text
|
||||
|
|
@ -44,8 +45,9 @@ class CohereComponent(LCModelComponent):
|
|||
temperature: float = 0.75,
|
||||
stream: bool = False,
|
||||
) -> Text:
|
||||
api_key = SecretStr(cohere_api_key)
|
||||
output = ChatCohere( # type: ignore
|
||||
cohere_api_key=cohere_api_key,
|
||||
cohere_api_key=api_key,
|
||||
temperature=temperature,
|
||||
)
|
||||
return self.get_result(output=output, stream=stream, input_value=input_value)
|
||||
|
|
|
|||
|
|
@ -122,7 +122,9 @@ class ContractEdge(Edge):
|
|||
return
|
||||
|
||||
if not source._built:
|
||||
await source.build()
|
||||
# The system should be read-only, so we should not be building vertices
|
||||
# that are not already built.
|
||||
raise ValueError(f"Source vertex {source.id} is not built.")
|
||||
|
||||
if self.matched_type == "Text":
|
||||
self.result = source._built_result
|
||||
|
|
@ -132,7 +134,7 @@ class ContractEdge(Edge):
|
|||
target.params[self.target_param] = self.result
|
||||
self.is_fulfilled = True
|
||||
|
||||
async def get_result(self, source: "Vertex", target: "Vertex"):
|
||||
async def get_result_from_source(self, source: "Vertex", target: "Vertex"):
|
||||
# Fulfill the contract if it has not been fulfilled.
|
||||
if not self.is_fulfilled:
|
||||
await self.honor(source, target)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from langflow.graph.edge.base import ContractEdge
|
|||
from langflow.graph.graph.constants import lazy_load_vertex_dict
|
||||
from langflow.graph.graph.state_manager import GraphStateManager
|
||||
from langflow.graph.graph.utils import process_flow
|
||||
from langflow.graph.schema import INPUT_FIELD_NAME, InterfaceComponentTypes
|
||||
from langflow.graph.schema import INPUT_FIELD_NAME, InterfaceComponentTypes, RunOutputs
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
from langflow.graph.vertex.types import (
|
||||
ChatVertex,
|
||||
|
|
@ -162,15 +162,16 @@ class Graph:
|
|||
if vertex is None:
|
||||
raise ValueError(f"Vertex {vertex_id} not found")
|
||||
vertex.update_raw_params({"session_id": session_id})
|
||||
# Process the graph
|
||||
try:
|
||||
await self.process()
|
||||
self.increment_run_count()
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise ValueError(f"Error running graph: {exc}") from exc
|
||||
# Get the outputs
|
||||
vertex_outputs = []
|
||||
for vertex_id in self._is_output_vertices:
|
||||
vertex = self.get_vertex(vertex_id)
|
||||
for vertex in self.vertices:
|
||||
if vertex is None:
|
||||
raise ValueError(f"Vertex {vertex_id} not found")
|
||||
|
||||
|
|
@ -178,17 +179,18 @@ class Graph:
|
|||
await vertex.consume_async_generator()
|
||||
if not outputs or (vertex.display_name in outputs or vertex.id in outputs):
|
||||
vertex_outputs.append(vertex.result)
|
||||
|
||||
return vertex_outputs
|
||||
|
||||
async def run(
|
||||
self,
|
||||
inputs: list[Dict[str, Union[str, list[str]]]],
|
||||
inputs: list[Dict[str, str]],
|
||||
inputs_components: Optional[list[list[str]]] = None,
|
||||
outputs: Optional[list[str]] = None,
|
||||
session_id: Optional[str] = None,
|
||||
stream: bool = False,
|
||||
) -> List[List[Optional["ResultData"]]]:
|
||||
) -> List[RunOutputs]:
|
||||
"""Runs the graph with the given inputs."""
|
||||
|
||||
# inputs is {"message": "Hello, world!"}
|
||||
# we need to go through self.inputs and update the self._raw_params
|
||||
# of the vertices that are inputs
|
||||
|
|
@ -196,30 +198,24 @@ class Graph:
|
|||
vertex_outputs = []
|
||||
if not isinstance(inputs, list):
|
||||
inputs = [inputs]
|
||||
for input_dict in inputs:
|
||||
components: Union[str, list[str]] = input_dict.get("components", [])
|
||||
|
||||
if not isinstance(components, list):
|
||||
components = [components]
|
||||
|
||||
if INPUT_FIELD_NAME not in input_dict:
|
||||
input_value = ""
|
||||
else:
|
||||
_input_value = input_dict[INPUT_FIELD_NAME]
|
||||
if isinstance(_input_value, str):
|
||||
input_value = _input_value
|
||||
else:
|
||||
raise ValueError(f"Invalid input value: {input_value}. Expected string")
|
||||
for run_inputs, components in zip(inputs, inputs_components or []):
|
||||
if components and not isinstance(components, list):
|
||||
raise ValueError(f"Invalid components value: {components}. Expected list")
|
||||
elif components is None:
|
||||
components = []
|
||||
|
||||
if not isinstance(run_inputs.get(INPUT_FIELD_NAME, ""), str):
|
||||
raise ValueError(f"Invalid input value: {run_inputs.get(INPUT_FIELD_NAME)}. Expected string")
|
||||
run_outputs = await self._run(
|
||||
inputs={INPUT_FIELD_NAME: input_value},
|
||||
inputs=run_inputs,
|
||||
input_components=components,
|
||||
outputs=outputs or [],
|
||||
stream=stream,
|
||||
session_id=session_id or "",
|
||||
)
|
||||
logger.debug(f"Run outputs: {run_outputs}")
|
||||
vertex_outputs.append(run_outputs)
|
||||
run_output_object = RunOutputs(inputs=run_inputs, outputs=run_outputs)
|
||||
logger.debug(f"Run outputs: {run_output_object}")
|
||||
vertex_outputs.append(run_output_object)
|
||||
return vertex_outputs
|
||||
|
||||
# vertices_layers is a list of lists ordered by the order the vertices
|
||||
|
|
@ -240,6 +236,7 @@ class Graph:
|
|||
|
||||
def build_graph_maps(self):
|
||||
self.predecessor_map, self.successor_map = self.build_adjacency_maps()
|
||||
|
||||
self.in_degree_map = self.build_in_degree()
|
||||
self.parent_child_map = self.build_parent_child_map()
|
||||
|
||||
|
|
@ -295,6 +292,15 @@ class Graph:
|
|||
successor_map[edge.source_id].append(edge.target_id)
|
||||
return predecessor_map, successor_map
|
||||
|
||||
def build_run_map(self):
|
||||
run_map = defaultdict(list)
|
||||
# The run map gets the predecessor_map and maps the info like this:
|
||||
# {vertex_id: every id that contains the vertex_id in the predecessor_map}
|
||||
for vertex_id, predecessors in self.predecessor_map.items():
|
||||
for predecessor in predecessors:
|
||||
run_map[predecessor].append(vertex_id)
|
||||
return run_map
|
||||
|
||||
@classmethod
|
||||
def from_payload(cls, payload: Dict, flow_id: Optional[str] = None) -> "Graph":
|
||||
"""
|
||||
|
|
@ -939,16 +945,37 @@ class Graph:
|
|||
# save the only the rest
|
||||
self.vertices_layers = vertices_layers[1:]
|
||||
self.vertices_to_run = {vertex_id for vertex_id in chain.from_iterable(vertices_layers)}
|
||||
self.run_map, self.run_predecessors = (
|
||||
self.build_run_map(),
|
||||
self.predecessor_map.copy(),
|
||||
)
|
||||
|
||||
# Return just the first layer
|
||||
return first_layer
|
||||
|
||||
def vertex_has_no_more_predecessors(self, vertex_id: str) -> bool:
|
||||
"""Returns whether a vertex has no more predecessors."""
|
||||
return not self.run_predecessors.get(vertex_id)
|
||||
|
||||
def should_run_vertex(self, vertex_id: str) -> bool:
|
||||
"""Returns whether a component should be run."""
|
||||
should_run = vertex_id in self.vertices_to_run
|
||||
# the self.run_map is a map of vertex_id to a list of predecessors
|
||||
# each time a vertex is run, we remove it from the list of predecessors
|
||||
# if a vertex has no more predecessors, it should be run
|
||||
should_run = vertex_id in self.vertices_to_run and self.vertex_has_no_more_predecessors(vertex_id)
|
||||
|
||||
if should_run:
|
||||
self.vertices_to_run.remove(vertex_id)
|
||||
# remove the vertex from the run_map
|
||||
self.remove_from_predecessors(vertex_id)
|
||||
return should_run
|
||||
|
||||
def remove_from_predecessors(self, vertex_id: str):
|
||||
predecessors = self.run_map.get(vertex_id, [])
|
||||
for predecessor in predecessors:
|
||||
if vertex_id in self.run_predecessors[predecessor]:
|
||||
self.run_predecessors[predecessor].remove(vertex_id)
|
||||
|
||||
def sort_interface_components_first(self, vertices_layers: List[List[str]]) -> List[List[str]]:
|
||||
"""Sorts the vertices in the graph so that vertices containing ChatInput or ChatOutput come first."""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from enum import Enum
|
||||
from typing import Any, Optional
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_serializer
|
||||
|
||||
|
|
@ -13,6 +13,8 @@ class ResultData(BaseModel):
|
|||
messages: Optional[list[ChatOutputResponse]] = Field(default_factory=list)
|
||||
timedelta: Optional[float] = None
|
||||
duration: Optional[str] = None
|
||||
component_display_name: Optional[str] = None
|
||||
component_id: Optional[str] = None
|
||||
|
||||
@field_serializer("results")
|
||||
def serialize_results(self, value):
|
||||
|
|
@ -48,3 +50,8 @@ OUTPUT_COMPONENTS = [
|
|||
]
|
||||
|
||||
INPUT_FIELD_NAME = "input_value"
|
||||
|
||||
|
||||
class RunOutputs(BaseModel):
|
||||
inputs: dict = Field(default_factory=dict)
|
||||
outputs: List[Optional[ResultData]] = Field(default_factory=list)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import ast
|
||||
import asyncio
|
||||
import inspect
|
||||
import types
|
||||
from enum import Enum
|
||||
|
|
@ -7,7 +8,6 @@ from typing import (
|
|||
Any,
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
Coroutine,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
|
|
@ -56,17 +56,13 @@ class Vertex:
|
|||
) -> None:
|
||||
# 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.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.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
|
||||
|
|
@ -106,17 +102,11 @@ class Vertex:
|
|||
|
||||
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 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
|
||||
):
|
||||
elif self.state == VertexStates.ACTIVE and self.id in self.graph.inactivated_vertices:
|
||||
self.graph.inactivated_vertices.remove(self.id)
|
||||
|
||||
@property
|
||||
|
|
@ -133,9 +123,7 @@ class Vertex:
|
|||
# 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
|
||||
):
|
||||
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
|
||||
|
|
@ -147,11 +135,7 @@ class Vertex:
|
|||
|
||||
if isinstance(self._built_result, UnbuiltResult):
|
||||
return {}
|
||||
return (
|
||||
self._built_result
|
||||
if isinstance(self._built_result, dict)
|
||||
else {"result": self._built_result}
|
||||
)
|
||||
return self._built_result if isinstance(self._built_result, dict) else {"result": self._built_result}
|
||||
|
||||
def set_artifacts(self) -> None:
|
||||
pass
|
||||
|
|
@ -188,6 +172,7 @@ class Vertex:
|
|||
}
|
||||
|
||||
def __setstate__(self, state):
|
||||
self._lock = asyncio.Lock()
|
||||
self._data = state["_data"]
|
||||
self.params = state["params"]
|
||||
self.base_type = state["base_type"]
|
||||
|
|
@ -225,31 +210,19 @@ class Vertex:
|
|||
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 = [
|
||||
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"]
|
||||
]
|
||||
self.optional_inputs = [
|
||||
template_dicts[key]["type"]
|
||||
for key, value in template_dicts.items()
|
||||
if not value["required"]
|
||||
template_dicts[key]["type"] for key, value in template_dicts.items() if not value["required"]
|
||||
]
|
||||
# Add the template_dicts[key]["input_types"] to the optional_inputs
|
||||
self.optional_inputs.extend(
|
||||
[
|
||||
input_type
|
||||
for value in template_dicts.values()
|
||||
for input_type in value.get("input_types", [])
|
||||
]
|
||||
[input_type for value in template_dicts.values() for input_type in value.get("input_types", [])]
|
||||
)
|
||||
|
||||
template_dict = self.data["node"]["template"]
|
||||
|
|
@ -296,11 +269,7 @@ class Vertex:
|
|||
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 = {}
|
||||
|
||||
for edge in self.edges:
|
||||
|
|
@ -321,10 +290,7 @@ class Vertex:
|
|||
# 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()
|
||||
}
|
||||
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)
|
||||
|
||||
|
|
@ -360,11 +326,7 @@ class Vertex:
|
|||
# list of dicts, so we need to convert it to a dict
|
||||
# before passing it to the build method
|
||||
if isinstance(val, list):
|
||||
params[key] = {
|
||||
k: v
|
||||
for item in value.get("value", [])
|
||||
for k, v in item.items()
|
||||
}
|
||||
params[key] = {k: v for item in value.get("value", []) for k, v in item.items()}
|
||||
elif isinstance(val, dict):
|
||||
params[key] = val
|
||||
elif value.get("type") == "int" and val is not None:
|
||||
|
|
@ -423,7 +385,7 @@ class Vertex:
|
|||
Initiate the build process.
|
||||
"""
|
||||
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)
|
||||
self._validate_built_object()
|
||||
|
||||
|
|
@ -466,6 +428,8 @@ class Vertex:
|
|||
results=result_dict,
|
||||
artifacts=artifacts,
|
||||
messages=messages,
|
||||
component_display_name=self.display_name,
|
||||
component_id=self.id,
|
||||
)
|
||||
self.set_result(result_dict)
|
||||
|
||||
|
|
@ -489,116 +453,126 @@ class Vertex:
|
|||
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
|
||||
)
|
||||
result = await generate_result(self._built_object, inputs, self.has_external_output, session_id)
|
||||
self._built_result = result
|
||||
|
||||
async def _build_each_node_in_params_dict(self, user_id=None):
|
||||
async def _build_each_vertex_in_params_dict(self, user_id=None):
|
||||
"""
|
||||
Iterates over each node in the params dictionary and builds it.
|
||||
Iterates over each vertex in the params dictionary and builds it.
|
||||
"""
|
||||
for key, value in self._raw_params.items():
|
||||
if self._is_node(value):
|
||||
if self._is_vertex(value):
|
||||
if value == self:
|
||||
del self.params[key]
|
||||
continue
|
||||
await self._build_node_and_update_params(key, value, user_id)
|
||||
elif isinstance(value, list) and self._is_list_of_nodes(value):
|
||||
await self._build_list_of_nodes_and_update_params(key, value, user_id)
|
||||
await self._build_vertex_and_update_params(
|
||||
key,
|
||||
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, user_id)
|
||||
await self._build_dict_and_update_params(
|
||||
key,
|
||||
value,
|
||||
)
|
||||
elif key not in self.params or self.updated_raw_params:
|
||||
self.params[key] = value
|
||||
|
||||
async def _build_dict_and_update_params(
|
||||
self, key, nodes_dict: Dict[str, "Vertex"], user_id=None
|
||||
self,
|
||||
key,
|
||||
vertices_dict: Dict[str, "Vertex"],
|
||||
):
|
||||
"""
|
||||
Iterates over a dictionary of nodes, builds each and updates the params dictionary.
|
||||
Iterates over a dictionary of vertices, builds each and updates the params dictionary.
|
||||
"""
|
||||
for sub_key, value in nodes_dict.items():
|
||||
if not self._is_node(value):
|
||||
for sub_key, value in vertices_dict.items():
|
||||
if not self._is_vertex(value):
|
||||
self.params[key][sub_key] = value
|
||||
else:
|
||||
built = await value.get_result(requester=self, user_id=user_id)
|
||||
self.params[key][sub_key] = built
|
||||
result = await value.get_result()
|
||||
self.params[key][sub_key] = result
|
||||
|
||||
def _is_node(self, value):
|
||||
def _is_vertex(self, value):
|
||||
"""
|
||||
Checks if the provided value is an instance of 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.
|
||||
"""
|
||||
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, requester: Optional["Vertex"] = None, user_id=None, timeout=None
|
||||
self,
|
||||
) -> Any:
|
||||
# PLEASE REVIEW THIS IF STATEMENT
|
||||
# Check if the Vertex was built already
|
||||
if self._built:
|
||||
return self._built_object if not self.use_result else self._built_result
|
||||
|
||||
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(requester=requester, user_id=user_id)
|
||||
return self._built_object
|
||||
|
||||
async def _build_node_and_update_params(self, key, node: "Vertex", 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(requester=self, user_id=user_id)
|
||||
result = await vertex.get_result()
|
||||
self._handle_func(key, result)
|
||||
if isinstance(result, list):
|
||||
self._extend_params_list_with_result(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] = []
|
||||
for node in nodes:
|
||||
built = await node.get_result(requester=self, user_id=user_id)
|
||||
for vertex in vertices:
|
||||
result = await vertex.get_result()
|
||||
# Weird check to see if the params[key] is a list
|
||||
# because sometimes it is a Record and breaks the code
|
||||
if not isinstance(self.params[key], list):
|
||||
self.params[key] = [self.params[key]]
|
||||
|
||||
if isinstance(built, list):
|
||||
self.params[key].extend(built)
|
||||
if isinstance(result, list):
|
||||
self.params[key].extend(result)
|
||||
else:
|
||||
try:
|
||||
if self.params[key] == built:
|
||||
if self.params[key] == result:
|
||||
continue
|
||||
|
||||
self.params[key].append(built)
|
||||
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 {built}"
|
||||
f"Error building node {self.display_name}: {str(e)}"
|
||||
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):
|
||||
|
|
@ -628,12 +602,9 @@ class Vertex:
|
|||
Gets the class from a dictionary and instantiates it with the params.
|
||||
"""
|
||||
if self.base_type is None:
|
||||
raise ValueError(f"Base type for node {self.display_name} not found")
|
||||
raise ValueError(f"Base type for vertex {self.display_name} not found")
|
||||
try:
|
||||
result = await loading.instantiate_class(
|
||||
node_type=self.vertex_type,
|
||||
base_type=self.base_type,
|
||||
params=self.params,
|
||||
user_id=user_id,
|
||||
vertex=self,
|
||||
)
|
||||
|
|
@ -641,9 +612,7 @@ class Vertex:
|
|||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
|
||||
raise ValueError(
|
||||
f"Error building node {self.display_name}: {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):
|
||||
"""
|
||||
|
|
@ -671,9 +640,7 @@ class Vertex:
|
|||
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."
|
||||
)
|
||||
raise ValueError(f"You are trying to stream to a {self.display_name}. Try using a Chat Output instead.")
|
||||
|
||||
def _reset(self, params_update: Optional[Dict[str, Any]] = None):
|
||||
self._built = False
|
||||
|
|
@ -699,33 +666,34 @@ class Vertex:
|
|||
requester: Optional["Vertex"] = None,
|
||||
**kwargs,
|
||||
) -> Any:
|
||||
if self.state == VertexStates.INACTIVE:
|
||||
# If the vertex is inactive, return None
|
||||
self.build_inactive()
|
||||
return
|
||||
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.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)
|
||||
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)
|
||||
# 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()
|
||||
self._finalize_build()
|
||||
|
||||
return await self.get_requester_result(requester)
|
||||
|
||||
|
|
@ -736,14 +704,12 @@ class Vertex:
|
|||
return self._built_object
|
||||
|
||||
# Get the requester edge
|
||||
requester_edge = next(
|
||||
(edge for edge in self.edges if edge.target_id == requester.id), None
|
||||
)
|
||||
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(source=self, target=requester)
|
||||
else await requester_edge.get_result_from_source(source=self, target=requester)
|
||||
)
|
||||
|
||||
def add_edge(self, edge: "ContractEdge") -> None:
|
||||
|
|
@ -751,9 +717,7 @@ class Vertex:
|
|||
self.edges.append(edge)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"Vertex(display_name={self.display_name}, 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:
|
||||
try:
|
||||
|
|
@ -774,8 +738,4 @@ class Vertex:
|
|||
|
||||
def _built_object_repr(self):
|
||||
# Add a message with an emoji, stars for sucess,
|
||||
return (
|
||||
"Built sucessfully ✨"
|
||||
if self._built_object is not None
|
||||
else "Failed to build 😵💫"
|
||||
)
|
||||
return "Built sucessfully ✨" if self._built_object is not None else "Failed to build 😵💫"
|
||||
|
|
|
|||
|
|
@ -124,11 +124,9 @@ class DocumentLoaderVertex(Vertex):
|
|||
# show how many documents are in the list?
|
||||
|
||||
if not isinstance(self._built_object, UnbuiltObject):
|
||||
avg_length = sum(
|
||||
len(record.text)
|
||||
for record in self._built_object
|
||||
if hasattr(record, "text")
|
||||
) / len(self._built_object)
|
||||
avg_length = sum(len(record.text) for record in self._built_object if hasattr(record, "text")) / len(
|
||||
self._built_object
|
||||
)
|
||||
return f"""{self.display_name}({len(self._built_object)} records)
|
||||
\nAvg. Record Length (characters): {int(avg_length)}
|
||||
Records: {self._built_object[:3]}..."""
|
||||
|
|
@ -201,9 +199,7 @@ class TextSplitterVertex(Vertex):
|
|||
# show how many documents are in the list?
|
||||
|
||||
if not isinstance(self._built_object, UnbuiltObject):
|
||||
avg_length = sum(len(doc.page_content) for doc in self._built_object) / len(
|
||||
self._built_object
|
||||
)
|
||||
avg_length = sum(len(doc.page_content) for doc in self._built_object) / len(self._built_object)
|
||||
return f"""{self.vertex_type}({len(self._built_object)} documents)
|
||||
\nAvg. Document Length (characters): {int(avg_length)}
|
||||
\nDocuments: {self._built_object[:3]}..."""
|
||||
|
|
@ -250,27 +246,18 @@ class PromptVertex(Vertex):
|
|||
user_id = kwargs.get("user_id", None)
|
||||
tools = kwargs.get("tools", [])
|
||||
if not self._built or force:
|
||||
if (
|
||||
"input_variables" not in self.params
|
||||
or self.params["input_variables"] is None
|
||||
):
|
||||
if "input_variables" not in self.params or self.params["input_variables"] is None:
|
||||
self.params["input_variables"] = []
|
||||
# Check if it is a ZeroShotPrompt and needs a tool
|
||||
if "ShotPrompt" in self.vertex_type:
|
||||
tools = (
|
||||
[tool_node.build(user_id=user_id) for tool_node in tools]
|
||||
if tools is not None
|
||||
else []
|
||||
)
|
||||
tools = [tool_node.build(user_id=user_id) for tool_node in tools] if tools is not None else []
|
||||
# flatten the list of tools if it is a list of lists
|
||||
# first check if it is a list
|
||||
if tools and isinstance(tools, list) and isinstance(tools[0], list):
|
||||
tools = flatten_list(tools)
|
||||
self.params["tools"] = tools
|
||||
prompt_params = [
|
||||
key
|
||||
for key, value in self.params.items()
|
||||
if isinstance(value, str) and key != "format_instructions"
|
||||
key for key, value in self.params.items() if isinstance(value, str) and key != "format_instructions"
|
||||
]
|
||||
else:
|
||||
prompt_params = ["template"]
|
||||
|
|
@ -280,20 +267,14 @@ class PromptVertex(Vertex):
|
|||
prompt_text = self.params[param]
|
||||
variables = extract_input_variables_from_prompt(prompt_text)
|
||||
self.params["input_variables"].extend(variables)
|
||||
self.params["input_variables"] = list(
|
||||
set(self.params["input_variables"])
|
||||
)
|
||||
self.params["input_variables"] = list(set(self.params["input_variables"]))
|
||||
elif isinstance(self.params, dict):
|
||||
self.params.pop("input_variables", None)
|
||||
|
||||
await self._build(user_id=user_id)
|
||||
|
||||
def _built_object_repr(self):
|
||||
if (
|
||||
not self.artifacts
|
||||
or self._built_object is None
|
||||
or not hasattr(self._built_object, "format")
|
||||
):
|
||||
if not self.artifacts or self._built_object is None or not hasattr(self._built_object, "format"):
|
||||
return super()._built_object_repr()
|
||||
elif isinstance(self._built_object, UnbuiltObject):
|
||||
return super()._built_object_repr()
|
||||
|
|
@ -305,9 +286,7 @@ class PromptVertex(Vertex):
|
|||
# so the prompt format doesn't break
|
||||
artifacts.pop("handle_keys", None)
|
||||
try:
|
||||
if not hasattr(self._built_object, "template") and hasattr(
|
||||
self._built_object, "prompt"
|
||||
):
|
||||
if not hasattr(self._built_object, "template") and hasattr(self._built_object, "prompt"):
|
||||
template = self._built_object.prompt.template
|
||||
else:
|
||||
template = self._built_object.template
|
||||
|
|
@ -315,11 +294,7 @@ class PromptVertex(Vertex):
|
|||
if value:
|
||||
replace_key = "{" + key + "}"
|
||||
template = template.replace(replace_key, value)
|
||||
return (
|
||||
template
|
||||
if isinstance(template, str)
|
||||
else f"{self.vertex_type}({template})"
|
||||
)
|
||||
return template if isinstance(template, str) else f"{self.vertex_type}({template})"
|
||||
except KeyError:
|
||||
return str(self._built_object)
|
||||
|
||||
|
|
@ -354,14 +329,8 @@ class ChatVertex(Vertex):
|
|||
return f"Task {self.task_id} is not running"
|
||||
if self.artifacts:
|
||||
# dump as a yaml string
|
||||
artifacts = {
|
||||
k.title().replace("_", " "): v
|
||||
for k, v in self.artifacts.items()
|
||||
if v is not None
|
||||
}
|
||||
yaml_str = yaml.dump(
|
||||
artifacts, default_flow_style=False, allow_unicode=True
|
||||
)
|
||||
artifacts = {k.title().replace("_", " "): v for k, v in self.artifacts.items() if v is not None}
|
||||
yaml_str = yaml.dump(artifacts, default_flow_style=False, allow_unicode=True)
|
||||
return yaml_str
|
||||
return super()._built_object_repr()
|
||||
|
||||
|
|
|
|||
|
|
@ -366,9 +366,10 @@ class CustomComponent(Component):
|
|||
|
||||
if not flow_id:
|
||||
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_value}]
|
||||
input_value_dict = [{"input_value": input_val} for input_val in input_value]
|
||||
return await graph.run(input_value_dict, stream=False)
|
||||
|
||||
def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Record]:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import contextlib
|
|||
import re
|
||||
import traceback
|
||||
import warnings
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
|
@ -155,6 +155,9 @@ def add_new_custom_field(
|
|||
field_value = field_config.pop("value", field_value)
|
||||
field_advanced = field_config.pop("advanced", False)
|
||||
|
||||
if field_type == "Dict":
|
||||
field_type = "dict"
|
||||
|
||||
if field_type == "bool" and field_value is None:
|
||||
field_value = False
|
||||
|
||||
|
|
@ -240,14 +243,12 @@ def get_field_dict(field: Union[TemplateField, dict]):
|
|||
def run_build_config(
|
||||
custom_component: CustomComponent,
|
||||
user_id: Optional[Union[str, UUID]] = None,
|
||||
update_field=None,
|
||||
update_field_value=None,
|
||||
):
|
||||
) -> Tuple[dict, CustomComponent]:
|
||||
"""Build the field configuration for a custom component"""
|
||||
|
||||
try:
|
||||
if custom_component.code is None:
|
||||
return {}
|
||||
raise ValueError("Code is None")
|
||||
elif isinstance(custom_component.code, str):
|
||||
custom_class = eval_custom_component_code(custom_component.code)
|
||||
else:
|
||||
|
|
@ -271,38 +272,6 @@ def run_build_config(
|
|||
# as a dict with the same keys as TemplateField
|
||||
field_dict = get_field_dict(field)
|
||||
build_config[field_name] = field_dict
|
||||
# This has to be done to set refresh if options or value are callable
|
||||
if update_field is not None and field_name != update_field:
|
||||
build_config = update_field_dict(
|
||||
custom_component_instance=custom_instance,
|
||||
field_dict=field_dict,
|
||||
build_config=build_config,
|
||||
call=False,
|
||||
)
|
||||
continue
|
||||
try:
|
||||
build_config = update_field_dict(
|
||||
custom_component_instance=custom_instance,
|
||||
field_dict=field_dict,
|
||||
build_config=build_config,
|
||||
update_field=update_field,
|
||||
update_field_value=update_field_value,
|
||||
call=True,
|
||||
)
|
||||
build_config[field_name] = field_dict
|
||||
except Exception as exc:
|
||||
logger.error(f"Error while getting build_config: {str(exc)}")
|
||||
if isinstance(exc, UpdateBuildConfigError):
|
||||
message = str(exc)
|
||||
else:
|
||||
message = f"Error while getting build_config: {str(exc)}"
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": message,
|
||||
"traceback": traceback.format_exc(),
|
||||
},
|
||||
) from exc
|
||||
|
||||
return build_config, custom_instance
|
||||
|
||||
|
|
@ -355,9 +324,7 @@ def add_code_field(frontend_node: CustomComponentFrontendNode, raw_code, field_c
|
|||
def build_custom_component_template(
|
||||
custom_component: CustomComponent,
|
||||
user_id: Optional[Union[str, UUID]] = None,
|
||||
update_field: Optional[str] = None,
|
||||
update_field_value: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
) -> Tuple[Dict[str, Any], CustomComponent]:
|
||||
"""Build a custom component template for the langchain"""
|
||||
try:
|
||||
frontend_node = build_frontend_node(custom_component.template_config)
|
||||
|
|
@ -365,8 +332,6 @@ def build_custom_component_template(
|
|||
field_config, custom_instance = run_build_config(
|
||||
custom_component,
|
||||
user_id=user_id,
|
||||
update_field=update_field,
|
||||
update_field_value=update_field_value,
|
||||
)
|
||||
|
||||
entrypoint_args = custom_component.get_function_entrypoint_args
|
||||
|
|
@ -380,7 +345,7 @@ def build_custom_component_template(
|
|||
|
||||
reorder_fields(frontend_node, custom_instance._get_field_order())
|
||||
|
||||
return frontend_node.to_dict(add_name=False)
|
||||
return frontend_node.to_dict(add_name=False), custom_instance
|
||||
except Exception as exc:
|
||||
if isinstance(exc, HTTPException):
|
||||
raise exc
|
||||
|
|
@ -400,7 +365,7 @@ def create_component_template(component):
|
|||
|
||||
component_extractor = CustomComponent(code=component_code)
|
||||
|
||||
component_template = build_custom_component_template(component_extractor)
|
||||
component_template, _ = build_custom_component_template(component_extractor)
|
||||
if not component_template["output_types"] and component_output_types:
|
||||
component_template["output_types"] = component_output_types
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import inspect
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Sequence, Type
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Sequence, Type
|
||||
|
||||
import orjson
|
||||
from langchain.agents import agent as agent_module
|
||||
|
|
@ -40,27 +40,29 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
async def instantiate_class(
|
||||
node_type: str,
|
||||
base_type: str,
|
||||
params: Dict,
|
||||
vertex: "Vertex",
|
||||
user_id=None,
|
||||
vertex: Optional["Vertex"] = None,
|
||||
) -> Any:
|
||||
"""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_kwargs(params)
|
||||
|
||||
if node_type in CUSTOM_NODES:
|
||||
if custom_node := CUSTOM_NODES.get(node_type):
|
||||
if vertex_type in CUSTOM_NODES:
|
||||
if custom_node := CUSTOM_NODES.get(vertex_type):
|
||||
if hasattr(custom_node, "initialize"):
|
||||
return custom_node.initialize(**params)
|
||||
return custom_node(**params)
|
||||
logger.debug(f"Instantiating {node_type} of type {base_type}")
|
||||
class_object = import_by_type(_type=base_type, name=node_type)
|
||||
logger.debug(f"Instantiating {vertex_type} of type {base_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(
|
||||
class_object=class_object,
|
||||
base_type=base_type,
|
||||
node_type=node_type,
|
||||
node_type=vertex_type,
|
||||
params=params,
|
||||
user_id=user_id,
|
||||
vertex=vertex,
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@ CUSTOM_TOOLS = {
|
|||
"PythonFunctionTool": PythonFunctionTool,
|
||||
}
|
||||
|
||||
OTHER_TOOLS = {
|
||||
tool: import_class(f"langchain_community.tools.{tool}") for tool in tools.__all__
|
||||
}
|
||||
OTHER_TOOLS = {tool: import_class(f"langchain_community.tools.{tool}") for tool in tools.__all__}
|
||||
|
||||
ALL_TOOLS_NAMES = {
|
||||
**_BASE_TOOLS,
|
||||
|
|
|
|||
|
|
@ -11,13 +11,14 @@ from loguru import logger
|
|||
from pydantic import BaseModel
|
||||
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.graph.schema import INPUT_FIELD_NAME, RunOutputs
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.interface.run import get_memory_key, update_memory_keys
|
||||
from langflow.services.session.service import SessionService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.api.v1.schemas import Tweaks
|
||||
from langflow.api.v1.schemas import InputValueRequest, Tweaks
|
||||
|
||||
|
||||
def fix_memory_inputs(langchain_object):
|
||||
|
|
@ -200,31 +201,43 @@ async def run_graph(
|
|||
flow_id: str,
|
||||
stream: bool,
|
||||
session_id: Optional[str] = None,
|
||||
inputs: Optional[list[dict[str, Union[List[str], str]]]] = None,
|
||||
inputs: Optional[List["InputValueRequest"]] = None,
|
||||
outputs: Optional[List[str]] = None,
|
||||
artifacts: Optional[Dict[str, Any]] = None,
|
||||
session_service: Optional[SessionService] = None,
|
||||
):
|
||||
) -> tuple[List[RunOutputs], str]:
|
||||
"""Run the graph and generate the result"""
|
||||
inputs = inputs or []
|
||||
if isinstance(graph, dict):
|
||||
graph_data = graph
|
||||
graph = Graph.from_payload(graph, flow_id=flow_id)
|
||||
else:
|
||||
graph_data = graph._graph_data
|
||||
if session_id is None and session_service is not None:
|
||||
session_id = session_service.generate_key(session_id=flow_id, data_graph=graph_data)
|
||||
if inputs is None:
|
||||
inputs = [{}]
|
||||
session_id_str = session_service.generate_key(session_id=flow_id, data_graph=graph_data)
|
||||
elif session_id is not None:
|
||||
session_id_str = session_id
|
||||
else:
|
||||
raise ValueError("session_id or session_service must be provided")
|
||||
components = []
|
||||
inputs_list = []
|
||||
for input_value_request in inputs:
|
||||
if input_value_request.input_value is None:
|
||||
logger.warning("InputValueRequest input_value cannot be None, defaulting to an empty string.")
|
||||
input_value_request.input_value = ""
|
||||
components.append(input_value_request.components or [])
|
||||
inputs_list.append({INPUT_FIELD_NAME: input_value_request.input_value})
|
||||
|
||||
run_outputs = await graph.run(
|
||||
inputs,
|
||||
inputs_list,
|
||||
components,
|
||||
outputs or [],
|
||||
stream=stream,
|
||||
session_id=session_id or "",
|
||||
session_id=session_id_str or "",
|
||||
)
|
||||
if session_id and session_service:
|
||||
session_service.update_session(session_id, (graph, artifacts))
|
||||
return run_outputs, session_id
|
||||
if session_id_str and session_service:
|
||||
session_service.update_session(session_id_str, (graph, artifacts))
|
||||
return run_outputs, session_id_str
|
||||
|
||||
|
||||
def validate_input(
|
||||
|
|
@ -272,6 +285,9 @@ def process_tweaks(graph_data: Dict[str, Any], tweaks: Union["Tweaks", Dict[str,
|
|||
|
||||
:raises ValueError: If the input is not in the expected format.
|
||||
"""
|
||||
if not isinstance(tweaks, dict):
|
||||
tweaks = tweaks.model_dump()
|
||||
|
||||
nodes = validate_input(graph_data, tweaks)
|
||||
nodes_map = {node.get("id"): node for node in nodes}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class Record(BaseModel):
|
|||
_default_value: str = ""
|
||||
|
||||
@model_validator(mode="before")
|
||||
def validate_data(values):
|
||||
def validate_data(cls, values):
|
||||
if not values.get("data"):
|
||||
values["data"] = {}
|
||||
# Any other keyword should be added to the data dictionary
|
||||
|
|
@ -73,9 +73,7 @@ class Record(BaseModel):
|
|||
return self.data.get(key, self._default_value)
|
||||
except KeyError:
|
||||
# Fallback to default behavior to raise AttributeError for undefined attributes
|
||||
raise AttributeError(
|
||||
f"'{type(self).__name__}' object has no attribute '{key}'"
|
||||
)
|
||||
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{key}'")
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
"""
|
||||
|
|
|
|||
14
src/backend/langflow/services/cache/__init__.py
vendored
14
src/backend/langflow/services/cache/__init__.py
vendored
|
|
@ -1,9 +1,17 @@
|
|||
from . import factory, service
|
||||
from langflow.services.cache.service import InMemoryCache
|
||||
from langflow.services.cache.service import (
|
||||
AsyncInMemoryCache,
|
||||
BaseCacheService,
|
||||
RedisCache,
|
||||
ThreadingInMemoryCache,
|
||||
)
|
||||
|
||||
from . import factory, service
|
||||
|
||||
__all__ = [
|
||||
"factory",
|
||||
"service",
|
||||
"InMemoryCache",
|
||||
"ThreadingInMemoryCache",
|
||||
"AsyncInMemoryCache",
|
||||
"BaseCacheService",
|
||||
"RedisCache",
|
||||
]
|
||||
|
|
|
|||
80
src/backend/langflow/services/cache/base.py
vendored
80
src/backend/langflow/services/cache/base.py
vendored
|
|
@ -1,4 +1,7 @@
|
|||
import abc
|
||||
import asyncio
|
||||
import threading
|
||||
from typing import Optional
|
||||
|
||||
from langflow.services.base import Service
|
||||
|
||||
|
|
@ -11,7 +14,7 @@ class BaseCacheService(Service):
|
|||
name = "cache_service"
|
||||
|
||||
@abc.abstractmethod
|
||||
def get(self, key):
|
||||
def get(self, key, lock: Optional[threading.Lock] = None):
|
||||
"""
|
||||
Retrieve an item from the cache.
|
||||
|
||||
|
|
@ -23,7 +26,7 @@ class BaseCacheService(Service):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def set(self, key, value):
|
||||
def set(self, key, value, lock: Optional[threading.Lock] = None):
|
||||
"""
|
||||
Add an item to the cache.
|
||||
|
||||
|
|
@ -33,7 +36,7 @@ class BaseCacheService(Service):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def upsert(self, key, value):
|
||||
def upsert(self, key, value, lock: Optional[threading.Lock] = None):
|
||||
"""
|
||||
Add an item to the cache if it doesn't exist, or update it if it does.
|
||||
|
||||
|
|
@ -43,7 +46,7 @@ class BaseCacheService(Service):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete(self, key):
|
||||
def delete(self, key, lock: Optional[threading.Lock] = None):
|
||||
"""
|
||||
Remove an item from the cache.
|
||||
|
||||
|
|
@ -52,7 +55,7 @@ class BaseCacheService(Service):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def clear(self):
|
||||
def clear(self, lock: Optional[threading.Lock] = None):
|
||||
"""
|
||||
Clear all items from the cache.
|
||||
"""
|
||||
|
|
@ -96,3 +99,70 @@ class BaseCacheService(Service):
|
|||
Args:
|
||||
key: The key of the item to remove.
|
||||
"""
|
||||
|
||||
|
||||
class AsyncBaseCacheService(Service):
|
||||
"""
|
||||
Abstract base class for a async cache.
|
||||
"""
|
||||
|
||||
name = "cache_service"
|
||||
|
||||
@abc.abstractmethod
|
||||
async def get(self, key, lock: Optional[asyncio.Lock] = None):
|
||||
"""
|
||||
Retrieve an item from the cache.
|
||||
|
||||
Args:
|
||||
key: The key of the item to retrieve.
|
||||
|
||||
Returns:
|
||||
The value associated with the key, or None if the key is not found.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def set(self, key, value, lock: Optional[asyncio.Lock] = None):
|
||||
"""
|
||||
Add an item to the cache.
|
||||
|
||||
Args:
|
||||
key: The key of the item.
|
||||
value: The value to cache.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def upsert(self, key, value, lock: Optional[asyncio.Lock] = None):
|
||||
"""
|
||||
Add an item to the cache if it doesn't exist, or update it if it does.
|
||||
|
||||
Args:
|
||||
key: The key of the item.
|
||||
value: The value to cache.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def delete(self, key, lock: Optional[asyncio.Lock] = None):
|
||||
"""
|
||||
Remove an item from the cache.
|
||||
|
||||
Args:
|
||||
key: The key of the item to remove.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def clear(self, lock: Optional[asyncio.Lock] = None):
|
||||
"""
|
||||
Clear all items from the cache.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def __contains__(self, key):
|
||||
"""
|
||||
Check if the key is in the cache.
|
||||
|
||||
Args:
|
||||
key: The key of the item to check.
|
||||
|
||||
Returns:
|
||||
True if the key is in the cache, False otherwise.
|
||||
"""
|
||||
|
|
|
|||
13
src/backend/langflow/services/cache/factory.py
vendored
13
src/backend/langflow/services/cache/factory.py
vendored
|
|
@ -1,6 +1,11 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from langflow.services.cache.service import BaseCacheService, InMemoryCache, RedisCache
|
||||
from langflow.services.cache.service import (
|
||||
AsyncInMemoryCache,
|
||||
BaseCacheService,
|
||||
RedisCache,
|
||||
ThreadingInMemoryCache,
|
||||
)
|
||||
from langflow.services.factory import ServiceFactory
|
||||
from langflow.utils.logger import logger
|
||||
|
||||
|
|
@ -29,7 +34,9 @@ class CacheServiceFactory(ServiceFactory):
|
|||
logger.debug("Redis cache is connected")
|
||||
return redis_cache
|
||||
logger.warning("Redis cache is not connected, falling back to in-memory cache")
|
||||
return InMemoryCache()
|
||||
return ThreadingInMemoryCache()
|
||||
|
||||
elif settings_service.settings.CACHE_TYPE == "memory":
|
||||
return InMemoryCache()
|
||||
return ThreadingInMemoryCache()
|
||||
elif settings_service.settings.CACHE_TYPE == "async":
|
||||
return AsyncInMemoryCache()
|
||||
|
|
|
|||
115
src/backend/langflow/services/cache/service.py
vendored
115
src/backend/langflow/services/cache/service.py
vendored
|
|
@ -1,16 +1,17 @@
|
|||
import asyncio
|
||||
import pickle
|
||||
import threading
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from langflow.services.base import Service
|
||||
from langflow.services.cache.base import BaseCacheService
|
||||
from langflow.services.cache.base import AsyncBaseCacheService, BaseCacheService
|
||||
|
||||
|
||||
class InMemoryCache(BaseCacheService, Service):
|
||||
|
||||
class ThreadingInMemoryCache(BaseCacheService, Service):
|
||||
"""
|
||||
A simple in-memory cache using an OrderedDict.
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ class InMemoryCache(BaseCacheService, Service):
|
|||
self.max_size = max_size
|
||||
self.expiration_time = expiration_time
|
||||
|
||||
def get(self, key):
|
||||
def get(self, key, lock: Optional[threading.Lock] = None):
|
||||
"""
|
||||
Retrieve an item from the cache.
|
||||
|
||||
|
|
@ -59,7 +60,7 @@ class InMemoryCache(BaseCacheService, Service):
|
|||
Returns:
|
||||
The value associated with the key, or None if the key is not found or the item has expired.
|
||||
"""
|
||||
with self._lock:
|
||||
with lock or self._lock:
|
||||
return self._get_without_lock(key)
|
||||
|
||||
def _get_without_lock(self, key):
|
||||
|
|
@ -80,7 +81,7 @@ class InMemoryCache(BaseCacheService, Service):
|
|||
self.delete(key)
|
||||
return None
|
||||
|
||||
def set(self, key, value, pickle=False):
|
||||
def set(self, key, value, lock: Optional[threading.Lock] = None):
|
||||
"""
|
||||
Add an item to the cache.
|
||||
|
||||
|
|
@ -90,7 +91,7 @@ class InMemoryCache(BaseCacheService, Service):
|
|||
key: The key of the item.
|
||||
value: The value to cache.
|
||||
"""
|
||||
with self._lock:
|
||||
with lock or self._lock:
|
||||
if key in self._cache:
|
||||
# Remove existing key before re-inserting to update order
|
||||
self.delete(key)
|
||||
|
|
@ -98,12 +99,10 @@ class InMemoryCache(BaseCacheService, Service):
|
|||
# Remove least recently used item
|
||||
self._cache.popitem(last=False)
|
||||
# pickle locally to mimic Redis
|
||||
if pickle:
|
||||
value = pickle.dumps(value)
|
||||
|
||||
self._cache[key] = {"value": value, "time": time.time()}
|
||||
|
||||
def upsert(self, key, value):
|
||||
def upsert(self, key, value, lock: Optional[threading.Lock] = None):
|
||||
"""
|
||||
Inserts or updates a value in the cache.
|
||||
If the existing value and the new value are both dictionaries, they are merged.
|
||||
|
|
@ -112,7 +111,7 @@ class InMemoryCache(BaseCacheService, Service):
|
|||
key: The key of the item.
|
||||
value: The value to insert or update.
|
||||
"""
|
||||
with self._lock:
|
||||
with lock or self._lock:
|
||||
existing_value = self._get_without_lock(key)
|
||||
if existing_value is not None and isinstance(existing_value, dict) and isinstance(value, dict):
|
||||
existing_value.update(value)
|
||||
|
|
@ -120,7 +119,7 @@ class InMemoryCache(BaseCacheService, Service):
|
|||
|
||||
self.set(key, value)
|
||||
|
||||
def get_or_set(self, key, value):
|
||||
def get_or_set(self, key, value, lock: Optional[threading.Lock] = None):
|
||||
"""
|
||||
Retrieve an item from the cache. If the item does not exist,
|
||||
set it with the provided value.
|
||||
|
|
@ -132,27 +131,27 @@ class InMemoryCache(BaseCacheService, Service):
|
|||
Returns:
|
||||
The cached value associated with the key.
|
||||
"""
|
||||
with self._lock:
|
||||
with lock or self._lock:
|
||||
if key in self._cache:
|
||||
return self.get(key)
|
||||
self.set(key, value)
|
||||
return value
|
||||
|
||||
def delete(self, key):
|
||||
def delete(self, key, lock: Optional[threading.Lock] = None):
|
||||
"""
|
||||
Remove an item from the cache.
|
||||
|
||||
Args:
|
||||
key: The key of the item to remove.
|
||||
"""
|
||||
with self._lock:
|
||||
with lock or self._lock:
|
||||
self._cache.pop(key, None)
|
||||
|
||||
def clear(self):
|
||||
def clear(self, lock: Optional[threading.Lock] = None):
|
||||
"""
|
||||
Clear all items from the cache.
|
||||
"""
|
||||
with self._lock:
|
||||
with lock or self._lock:
|
||||
self._cache.clear()
|
||||
|
||||
def __contains__(self, key):
|
||||
|
|
@ -323,3 +322,85 @@ class RedisCache(BaseCacheService, Service):
|
|||
def __repr__(self):
|
||||
"""Return a string representation of the RedisCache instance."""
|
||||
return f"RedisCache(expiration_time={self.expiration_time})"
|
||||
|
||||
|
||||
class AsyncInMemoryCache(AsyncBaseCacheService, Service):
|
||||
def __init__(self, max_size=None, expiration_time=3600):
|
||||
self.cache = OrderedDict()
|
||||
|
||||
self.lock = asyncio.Lock()
|
||||
self.max_size = max_size
|
||||
self.expiration_time = expiration_time
|
||||
|
||||
async def get(self, key, lock: Optional[asyncio.Lock] = None):
|
||||
if not lock:
|
||||
async with self.lock:
|
||||
return await self._get(key)
|
||||
else:
|
||||
return await self._get(key)
|
||||
|
||||
async def _get(self, key):
|
||||
item = self.cache.get(key, None)
|
||||
if item and (time.time() - item["time"] < self.expiration_time):
|
||||
self.cache.move_to_end(key)
|
||||
return pickle.loads(item["value"]) if isinstance(item["value"], bytes) else item["value"]
|
||||
if item:
|
||||
await self.delete(key)
|
||||
return None
|
||||
|
||||
async def set(self, key, value, lock: Optional[asyncio.Lock] = None):
|
||||
if not lock:
|
||||
async with self.lock:
|
||||
await self._set(
|
||||
key,
|
||||
value,
|
||||
)
|
||||
else:
|
||||
await self._set(
|
||||
key,
|
||||
value,
|
||||
)
|
||||
|
||||
async def _set(self, key, value):
|
||||
if self.max_size and len(self.cache) >= self.max_size:
|
||||
self.cache.popitem(last=False)
|
||||
self.cache[key] = {"value": value, "time": time.time()}
|
||||
self.cache.move_to_end(key)
|
||||
|
||||
async def delete(self, key, lock: Optional[asyncio.Lock] = None):
|
||||
if not lock:
|
||||
async with self.lock:
|
||||
await self._delete(key)
|
||||
else:
|
||||
await self._delete(key)
|
||||
|
||||
async def _delete(self, key):
|
||||
if key in self.cache:
|
||||
del self.cache[key]
|
||||
|
||||
async def clear(self, lock: Optional[asyncio.Lock] = None):
|
||||
if not lock:
|
||||
async with self.lock:
|
||||
await self._clear()
|
||||
else:
|
||||
await self._clear()
|
||||
|
||||
async def _clear(self):
|
||||
self.cache.clear()
|
||||
|
||||
async def upsert(self, key, value, lock: Optional[asyncio.Lock] = None):
|
||||
if not lock:
|
||||
async with self.lock:
|
||||
await self._upsert(key, value)
|
||||
else:
|
||||
await self._upsert(key, value)
|
||||
|
||||
async def _upsert(self, key, value):
|
||||
existing_value = await self.get(key)
|
||||
if existing_value is not None and isinstance(existing_value, dict) and isinstance(value, dict):
|
||||
existing_value.update(value)
|
||||
value = existing_value
|
||||
await self.set(key, value)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.cache
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
from typing import Any
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
from typing import Any, Optional
|
||||
|
||||
from langflow.services.base import Service
|
||||
from langflow.services.deps import get_cache_service
|
||||
|
|
@ -8,30 +10,30 @@ class ChatService(Service):
|
|||
name = "chat_service"
|
||||
|
||||
def __init__(self):
|
||||
self._cache_locks = defaultdict(asyncio.Lock)
|
||||
self.cache_service = get_cache_service()
|
||||
|
||||
def set_cache(self, client_id: str, data: Any) -> bool:
|
||||
async def set_cache(self, flow_id: str, data: Any, lock: Optional[asyncio.Lock] = None) -> bool:
|
||||
"""
|
||||
Set the cache for a client.
|
||||
"""
|
||||
# client_id is the flow id but that already exists in the cache
|
||||
# so we need to change it to something else
|
||||
|
||||
result_dict = {
|
||||
"result": data,
|
||||
"type": type(data),
|
||||
}
|
||||
self.cache_service.upsert(client_id, result_dict)
|
||||
return client_id in self.cache_service
|
||||
await self.cache_service.upsert(flow_id, result_dict, lock=lock or self._cache_locks[flow_id])
|
||||
return flow_id in self.cache_service
|
||||
|
||||
def get_cache(self, client_id: str) -> Any:
|
||||
async def get_cache(self, flow_id: str, lock: Optional[asyncio.Lock] = None) -> Any:
|
||||
"""
|
||||
Get the cache for a client.
|
||||
"""
|
||||
return self.cache_service.get(client_id)
|
||||
return await self.cache_service.get(flow_id, lock=lock or self._cache_locks[flow_id])
|
||||
|
||||
def clear_cache(self, client_id: str):
|
||||
async def clear_cache(self, flow_id: str, lock: Optional[asyncio.Lock] = None):
|
||||
"""
|
||||
Clear the cache for a client.
|
||||
"""
|
||||
self.cache_service.delete(client_id)
|
||||
self.cache_service.delete(flow_id, lock=lock or self._cache_locks[flow_id])
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import datetime
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
|
@ -37,7 +36,7 @@ class AuthSettings(BaseSettings):
|
|||
NEW_USER_IS_ACTIVE: bool = False
|
||||
SUPERUSER: str = DEFAULT_SUPERUSER
|
||||
SUPERUSER_PASSWORD: str = DEFAULT_SUPERUSER_PASSWORD
|
||||
|
||||
|
||||
REFRESH_SAME_SITE: str = "none"
|
||||
"""The SameSite attribute of the refresh token cookie."""
|
||||
REFRESH_SECURE: bool = True
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class Settings(BaseSettings):
|
|||
|
||||
DEV: bool = False
|
||||
DATABASE_URL: Optional[str] = None
|
||||
CACHE_TYPE: str = "memory"
|
||||
CACHE_TYPE: str = "async"
|
||||
REMOVE_API_KEYS: bool = False
|
||||
COMPONENTS_PATH: List[str] = []
|
||||
LANGCHAIN_CACHE: str = "InMemoryCache"
|
||||
|
|
|
|||
|
|
@ -20,12 +20,8 @@ def remove_ansi_escape_codes(text):
|
|||
return re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", text)
|
||||
|
||||
|
||||
def build_template_from_function(
|
||||
name: str, type_to_loader_dict: Dict, add_function: bool = False
|
||||
):
|
||||
classes = [
|
||||
item.__annotations__["return"].__name__ for item in type_to_loader_dict.values()
|
||||
]
|
||||
def build_template_from_function(name: str, type_to_loader_dict: Dict, add_function: bool = False):
|
||||
classes = [item.__annotations__["return"].__name__ for item in type_to_loader_dict.values()]
|
||||
|
||||
# Raise error if name is not in chains
|
||||
if name not in classes:
|
||||
|
|
@ -46,10 +42,8 @@ def build_template_from_function(
|
|||
for name_, value_ in value.__repr_args__():
|
||||
if name_ == "default_factory":
|
||||
try:
|
||||
variables[class_field_items]["default"] = (
|
||||
get_default_factory(
|
||||
module=_class.__base__.__module__, function=value_
|
||||
)
|
||||
variables[class_field_items]["default"] = get_default_factory(
|
||||
module=_class.__base__.__module__, function=value_
|
||||
)
|
||||
except Exception:
|
||||
variables[class_field_items]["default"] = None
|
||||
|
|
@ -57,9 +51,7 @@ def build_template_from_function(
|
|||
variables[class_field_items][name_] = value_
|
||||
|
||||
variables[class_field_items]["placeholder"] = (
|
||||
docs.params[class_field_items]
|
||||
if class_field_items in docs.params
|
||||
else ""
|
||||
docs.params[class_field_items] if class_field_items in docs.params else ""
|
||||
)
|
||||
# Adding function to base classes to allow
|
||||
# the output to be a function
|
||||
|
|
@ -74,9 +66,7 @@ def build_template_from_function(
|
|||
}
|
||||
|
||||
|
||||
def build_template_from_class(
|
||||
name: str, type_to_cls_dict: Dict, add_function: bool = False
|
||||
):
|
||||
def build_template_from_class(name: str, type_to_cls_dict: Dict, add_function: bool = False):
|
||||
classes = [item.__name__ for item in type_to_cls_dict.values()]
|
||||
|
||||
# Raise error if name is not in chains
|
||||
|
|
@ -100,11 +90,9 @@ def build_template_from_class(
|
|||
for name_, value_ in value.__repr_args__():
|
||||
if name_ == "default_factory":
|
||||
try:
|
||||
variables[class_field_items]["default"] = (
|
||||
get_default_factory(
|
||||
module=_class.__base__.__module__,
|
||||
function=value_,
|
||||
)
|
||||
variables[class_field_items]["default"] = get_default_factory(
|
||||
module=_class.__base__.__module__,
|
||||
function=value_,
|
||||
)
|
||||
except Exception:
|
||||
variables[class_field_items]["default"] = None
|
||||
|
|
@ -112,9 +100,7 @@ def build_template_from_class(
|
|||
variables[class_field_items][name_] = value_
|
||||
|
||||
variables[class_field_items]["placeholder"] = (
|
||||
docs.params[class_field_items]
|
||||
if class_field_items in docs.params
|
||||
else ""
|
||||
docs.params[class_field_items] if class_field_items in docs.params else ""
|
||||
)
|
||||
base_classes = get_base_classes(_class)
|
||||
# Adding function to base classes to allow
|
||||
|
|
@ -146,9 +132,7 @@ def build_template_from_method(
|
|||
|
||||
# Check if the method exists in this class
|
||||
if not hasattr(_class, method_name):
|
||||
raise ValueError(
|
||||
f"Method {method_name} not found in class {class_name}"
|
||||
)
|
||||
raise ValueError(f"Method {method_name} not found in class {class_name}")
|
||||
|
||||
# Get the method
|
||||
method = getattr(_class, method_name)
|
||||
|
|
@ -167,14 +151,8 @@ def build_template_from_method(
|
|||
"_type": _type,
|
||||
**{
|
||||
name: {
|
||||
"default": (
|
||||
param.default if param.default != param.empty else None
|
||||
),
|
||||
"type": (
|
||||
param.annotation
|
||||
if param.annotation != param.empty
|
||||
else None
|
||||
),
|
||||
"default": (param.default if param.default != param.empty else None),
|
||||
"type": (param.annotation if param.annotation != param.empty else None),
|
||||
"required": param.default == param.empty,
|
||||
}
|
||||
for name, param in params.items()
|
||||
|
|
@ -261,9 +239,7 @@ def sync_to_async(func):
|
|||
return async_wrapper
|
||||
|
||||
|
||||
def format_dict(
|
||||
dictionary: Dict[str, Any], class_name: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
def format_dict(dictionary: Dict[str, Any], class_name: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Formats a dictionary by removing certain keys and modifying the
|
||||
values of other keys.
|
||||
|
|
@ -349,9 +325,7 @@ def check_list_type(_type: str, value: Dict[str, Any]) -> str:
|
|||
The modified type string.
|
||||
"""
|
||||
if any(list_type in _type for list_type in ["List", "Sequence", "Set"]):
|
||||
_type = (
|
||||
_type.replace("List[", "").replace("Sequence[", "").replace("Set[", "")[:-1]
|
||||
)
|
||||
_type = _type.replace("List[", "").replace("Sequence[", "").replace("Set[", "")[:-1]
|
||||
value["list"] = True
|
||||
else:
|
||||
value["list"] = False
|
||||
|
|
@ -454,9 +428,7 @@ def set_headers_value(value: Dict[str, Any]) -> None:
|
|||
value["value"] = """{"Authorization": "Bearer <token>"}"""
|
||||
|
||||
|
||||
def add_options_to_field(
|
||||
value: Dict[str, Any], class_name: Optional[str], key: str
|
||||
) -> None:
|
||||
def add_options_to_field(value: Dict[str, Any], class_name: Optional[str], key: str) -> None:
|
||||
"""
|
||||
Adds options to the field based on the class name and key.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -102,35 +102,46 @@ export default function App() {
|
|||
)}
|
||||
</ErrorBoundary>
|
||||
<div></div>
|
||||
<div className="app-div z-40">
|
||||
{tempNotificationList.map((alert) => (
|
||||
<div key={alert.id}>
|
||||
{alert.type === "error" ? (
|
||||
<ErrorAlert
|
||||
key={alert.id}
|
||||
title={alert.title}
|
||||
list={alert.list}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
) : alert.type === "notice" ? (
|
||||
<NoticeAlert
|
||||
key={alert.id}
|
||||
title={alert.title}
|
||||
link={alert.link}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
) : (
|
||||
<SuccessAlert
|
||||
key={alert.id}
|
||||
title={alert.title}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="app-div">
|
||||
<div className="flex flex-col-reverse" style={{ zIndex: 999 }}>
|
||||
{tempNotificationList.map((alert) => (
|
||||
<div key={alert.id}>
|
||||
{alert.type === "error" && (
|
||||
<ErrorAlert
|
||||
key={alert.id}
|
||||
title={alert.title}
|
||||
list={alert.list}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="z-40 flex flex-col-reverse">
|
||||
{tempNotificationList.map((alert) => (
|
||||
<div key={alert.id}>
|
||||
{alert.type === "notice" ? (
|
||||
<NoticeAlert
|
||||
key={alert.id}
|
||||
title={alert.title}
|
||||
link={alert.link}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
) : (
|
||||
alert.type === "success" && (
|
||||
<SuccessAlert
|
||||
key={alert.id}
|
||||
title={alert.title}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { NodeToolbar } from "reactflow";
|
||||
import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
|
|
@ -9,6 +10,7 @@ import Loading from "../../components/ui/loading";
|
|||
import { Textarea } from "../../components/ui/textarea";
|
||||
import Xmark from "../../components/ui/xmark";
|
||||
import {
|
||||
NATIVE_CATEGORIES,
|
||||
RUN_TIMESTAMP_PREFIX,
|
||||
STATUS_BUILD,
|
||||
STATUS_BUILDING,
|
||||
|
|
@ -20,6 +22,7 @@ import { useDarkStore } from "../../stores/darkStore";
|
|||
import useFlowStore from "../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { useTypesStore } from "../../stores/typesStore";
|
||||
import { APIClassType } from "../../types/api";
|
||||
import { validationStatusType } from "../../types/components";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
|
||||
|
|
@ -39,10 +42,12 @@ export default function GenericNode({
|
|||
yPos: number;
|
||||
}): JSX.Element {
|
||||
const types = useTypesStore((state) => state.types);
|
||||
const templates = useTypesStore((state) => state.templates);
|
||||
const deleteNode = useFlowStore((state) => state.deleteNode);
|
||||
const flowPool = useFlowStore((state) => state.flowPool);
|
||||
const buildFlow = useFlowStore((state) => state.buildFlow);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
|
||||
const [inputName, setInputName] = useState(false);
|
||||
|
|
@ -51,6 +56,7 @@ export default function GenericNode({
|
|||
const [nodeDescription, setNodeDescription] = useState(
|
||||
data.node?.description!
|
||||
);
|
||||
const [isOutdated, setIsOutdated] = useState(false);
|
||||
const buildStatus = useFlowStore(
|
||||
(state) => state.flowBuildStatus[data.id]?.status
|
||||
);
|
||||
|
|
@ -65,6 +71,52 @@ export default function GenericNode({
|
|||
|
||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||
|
||||
useEffect(() => {
|
||||
// This one should run only once
|
||||
// first check if data.type in NATIVE_CATEGORIES
|
||||
// if not return
|
||||
if (
|
||||
!NATIVE_CATEGORIES.includes(types[data.type]) ||
|
||||
!data.node?.template?.code?.value
|
||||
)
|
||||
return;
|
||||
const thisNodeTemplate = templates[data.type].template;
|
||||
// if the template does not have a code key
|
||||
// return
|
||||
if (!thisNodeTemplate.code) return;
|
||||
const currentCode = thisNodeTemplate.code?.value;
|
||||
const thisNodesCode = data.node!.template?.code?.value;
|
||||
if (currentCode !== thisNodesCode) {
|
||||
setIsOutdated(true);
|
||||
} else {
|
||||
setIsOutdated(false);
|
||||
}
|
||||
// template.code can be undefined
|
||||
}, [data.node?.template?.code?.value]);
|
||||
|
||||
const updateNodeCode = useCallback(
|
||||
(newNodeClass: APIClassType, code: string, name: string) => {
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
node: newNodeClass,
|
||||
description: newNodeClass.description ?? data.node!.description,
|
||||
display_name: newNodeClass.display_name ?? data.node!.display_name,
|
||||
};
|
||||
|
||||
newNode.data.node.template[name].value = code;
|
||||
setIsOutdated(false);
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
updateNodeInternals(data.id);
|
||||
},
|
||||
[data.id, data.node, setNode, setIsOutdated]
|
||||
);
|
||||
|
||||
if (!data.node!.template) {
|
||||
setErrorData({
|
||||
title: `Error in component ${data.node!.display_name}`,
|
||||
|
|
@ -305,6 +357,8 @@ export default function GenericNode({
|
|||
showNode={showNode}
|
||||
openAdvancedModal={false}
|
||||
onCloseAdvancedModal={() => {}}
|
||||
updateNodeCode={updateNodeCode}
|
||||
isOutdated={isOutdated}
|
||||
selected={selected}
|
||||
></NodeToolbarComponent>
|
||||
</NodeToolbar>
|
||||
|
|
|
|||
|
|
@ -55,8 +55,6 @@ export default function CodeTabsComponent({
|
|||
const dark = useDarkStore((state) => state.dark);
|
||||
const unselectAll = useFlowStore((state) => state.unselectAll);
|
||||
|
||||
const setNodes = useFlowStore((state) => state.setNodes);
|
||||
|
||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import FlowSettingsModal from "../../../../modals/flowSettingsModal";
|
|||
import useAlertStore from "../../../../stores/alertStore";
|
||||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { useStoreStore } from "../../../../stores/storeStore";
|
||||
import { cn } from "../../../../utils/utils";
|
||||
import ShadTooltip from "../../../ShadTooltipComponent";
|
||||
import IconComponent from "../../../genericIconComponent";
|
||||
|
|
@ -34,10 +33,8 @@ export const MenuBar = ({
|
|||
const redo = useFlowsManagerStore((state) => state.redo);
|
||||
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
const n = useFlowStore((state) => state.nodes);
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
|
||||
const hasApiKey = useStoreStore((state) => state.hasApiKey);
|
||||
const validApiKey = useStoreStore((state) => state.validApiKey);
|
||||
const navigate = useNavigate();
|
||||
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||
|
||||
|
|
@ -46,7 +43,6 @@ export const MenuBar = ({
|
|||
addFlow(true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
// saveFlowStyleInDataBase();
|
||||
} catch (err) {
|
||||
setErrorData(err as { title: string; list?: Array<string> });
|
||||
}
|
||||
|
|
@ -65,7 +61,7 @@ export const MenuBar = ({
|
|||
<div className="round-button-div">
|
||||
<button
|
||||
onClick={() => {
|
||||
removeFunction(n);
|
||||
removeFunction(nodes);
|
||||
navigate(-1);
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default function Header(): JSX.Element {
|
|||
const removeFlow = useFlowsManagerStore((store) => store.removeFlow);
|
||||
const hasStore = useStoreStore((state) => state.hasStore);
|
||||
const { id } = useParams();
|
||||
const n = useFlowStore((state) => state.nodes);
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
|
||||
const dark = useDarkStore((state) => state.dark);
|
||||
const setDark = useDarkStore((state) => state.setDark);
|
||||
|
|
@ -57,7 +57,7 @@ export default function Header(): JSX.Element {
|
|||
return (
|
||||
<div className="header-arrangement">
|
||||
<div className="header-start-display lg:w-[30%]">
|
||||
<Link to="/" onClick={() => checkForChanges(n)}>
|
||||
<Link to="/" onClick={() => checkForChanges(nodes)}>
|
||||
<span className="ml-4 text-2xl">⛓️</span>
|
||||
</Link>
|
||||
<MenuBar removeFunction={checkForChanges} />
|
||||
|
|
@ -74,7 +74,7 @@ export default function Header(): JSX.Element {
|
|||
}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
checkForChanges(n);
|
||||
checkForChanges(nodes);
|
||||
}}
|
||||
>
|
||||
<IconComponent name="Home" className="h-4 w-4" />
|
||||
|
|
@ -100,7 +100,7 @@ export default function Header(): JSX.Element {
|
|||
variant={location.pathname === "/store" ? "primary" : "secondary"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
checkForChanges(n);
|
||||
checkForChanges(nodes);
|
||||
}}
|
||||
>
|
||||
<IconComponent name="Store" className="h-4 w-4" />
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const DialogContent = React.forwardRef<
|
|||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 flex w-full max-w-lg translate-x-[-50%] translate-y-[-50%] flex-col gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
|
||||
"fixed z-50 flex w-full max-w-lg flex-col gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] sm:rounded-lg md:w-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ export default function RenameLabel(props) {
|
|||
}
|
||||
}
|
||||
resizeInput();
|
||||
return () => {
|
||||
if(isRename) document.removeEventListener("keydown", () => {});
|
||||
};
|
||||
}, [isRename]);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export const COMPONENTS_ERROR_ALERT = "Error getting components.";
|
|||
|
||||
// NOTICE
|
||||
export const NOCHATOUTPUT_NOTICE_ALERT =
|
||||
"There is no ChatOutput node in the flow.";
|
||||
"There is no ChatOutput Component in the flow.";
|
||||
export const API_WARNING_NOTICE_ALERT =
|
||||
"Warning: Critical data, JSON file may include API keys.";
|
||||
export const COPIED_NOTICE_ALERT = "API Key copied!";
|
||||
|
|
|
|||
|
|
@ -740,6 +740,16 @@ export const PRIORITY_SIDEBAR_ORDER = [
|
|||
"helpers",
|
||||
"experimental",
|
||||
];
|
||||
export const NATIVE_CATEGORIES = [
|
||||
"inputs",
|
||||
"outputs",
|
||||
"prompts",
|
||||
"data",
|
||||
"prompt",
|
||||
"models",
|
||||
"helpers",
|
||||
"experimental",
|
||||
];
|
||||
/*
|
||||
Data ingestion
|
||||
Basic Prompting
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import axios, { AxiosError, AxiosInstance } from "axios";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { Cookies } from "react-cookie";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { renewAccessToken } from ".";
|
||||
import { BuildStatus } from "../../constants/enums";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
|
|
@ -17,26 +16,28 @@ function ApiInterceptor() {
|
|||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
let { accessToken, login, logout, authenticationErrorCount, autoLogin } =
|
||||
useContext(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
const cookies = new Cookies();
|
||||
|
||||
useEffect(() => {
|
||||
const interceptor = api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
if (error.response?.status === 401) {
|
||||
const accessToken = cookies.get("access_token_lf");
|
||||
if (error.response?.status === 403 || error.response?.status === 401) {
|
||||
if (!autoLogin) {
|
||||
const stillRefresh = checkErrorCount();
|
||||
if (!stillRefresh) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
const acceptedRequest = await tryToRenewAccessToken(error);
|
||||
|
||||
if (accessToken && !autoLogin) {
|
||||
checkErrorCount();
|
||||
await tryToRenewAccessToken(error);
|
||||
const accessToken = cookies.get("access_token_lf");
|
||||
|
||||
if (!accessToken && error?.config?.url?.includes("login")) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
return acceptedRequest;
|
||||
}
|
||||
|
||||
if (!accessToken && error?.config?.url?.includes("login")) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
return logout();
|
||||
}
|
||||
await clearBuildVerticesState(error);
|
||||
return Promise.reject(error);
|
||||
|
|
@ -98,11 +99,15 @@ function ApiInterceptor() {
|
|||
if (authenticationErrorCount > 3) {
|
||||
authenticationErrorCount = 0;
|
||||
logout();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function tryToRenewAccessToken(error: AxiosError) {
|
||||
try {
|
||||
if (window.location.pathname.includes("/login")) return;
|
||||
const res = await renewAccessToken();
|
||||
if (res?.data?.access_token && res?.data?.refresh_token) {
|
||||
login(res?.data?.access_token);
|
||||
|
|
@ -116,7 +121,9 @@ function ApiInterceptor() {
|
|||
return response;
|
||||
}
|
||||
} catch (error) {
|
||||
clearBuildVerticesState(error);
|
||||
logout();
|
||||
return Promise.reject("Authentication error");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { BASE_URL_API } from "../../constants/constants";
|
|||
import { api } from "../../controllers/API/api";
|
||||
import {
|
||||
APIObjectType,
|
||||
APITemplateType,
|
||||
Component,
|
||||
LoginType,
|
||||
Users,
|
||||
|
|
@ -369,11 +370,13 @@ export async function postCustomComponent(
|
|||
|
||||
export async function postCustomComponentUpdate(
|
||||
code: string,
|
||||
template: APITemplateType,
|
||||
field: string,
|
||||
field_value: any
|
||||
): Promise<AxiosResponse<APIClassType>> {
|
||||
return await api.post(`${BASE_URL_API}custom_component/update`, {
|
||||
code,
|
||||
template,
|
||||
field,
|
||||
field_value,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const SvgBotMessageSquare = (props) => (
|
|||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-bot-message-square"
|
||||
className="lucide lucide-bot-message-square"
|
||||
{...props}
|
||||
>
|
||||
<path d="M12 6V2H8" />
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import {
|
|||
import { postCustomComponent, postValidateCode } from "../../controllers/API";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { useDarkStore } from "../../stores/darkStore";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import { CodeErrorDataTypeAPI } from "../../types/api";
|
||||
import { codeAreaModalPropsType } from "../../types/components";
|
||||
import BaseModal from "../baseModal";
|
||||
|
|
@ -45,14 +44,12 @@ export default function CodeAreaModal({
|
|||
? [myOpen, mySetOpen]
|
||||
: useState(false);
|
||||
const dark = useDarkStore((state) => state.dark);
|
||||
const unselectAll = useFlowStore((state) => state.unselectAll);
|
||||
const [height, setHeight] = useState<string | null>(null);
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const [error, setError] = useState<{
|
||||
detail: CodeErrorDataTypeAPI;
|
||||
} | null>(null);
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
|
||||
useEffect(() => {
|
||||
// if nodeClass.template has more fields other than code and dynamic is true
|
||||
|
|
|
|||
|
|
@ -250,10 +250,20 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
setSearch(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<div className="search-icon">
|
||||
<div
|
||||
className="search-icon "
|
||||
onClick={() => {
|
||||
if (search) {
|
||||
setFilterData(data);
|
||||
setSearch("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
name="Search"
|
||||
className={"h-5 w-5 stroke-[1.5] text-primary"}
|
||||
name={search ? "X" : "Search"}
|
||||
className={`h-5 w-5 stroke-[1.5] text-primary ${
|
||||
search ? "cursor-pointer" : "cursor-default"
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "../../../../components/ui/select-custom";
|
||||
import { postCustomComponent } from "../../../../controllers/API";
|
||||
import ConfirmationModal from "../../../../modals/ConfirmationModal";
|
||||
import EditNodeModal from "../../../../modals/EditNodeModal";
|
||||
import ShareModal from "../../../../modals/shareModal";
|
||||
|
|
@ -18,6 +19,7 @@ import { useDarkStore } from "../../../../stores/darkStore";
|
|||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { useStoreStore } from "../../../../stores/storeStore";
|
||||
import { useTypesStore } from "../../../../stores/typesStore";
|
||||
import { APIClassType } from "../../../../types/api";
|
||||
import { nodeToolbarPropsType } from "../../../../types/components";
|
||||
import { FlowType } from "../../../../types/flow";
|
||||
|
|
@ -28,6 +30,7 @@ import {
|
|||
updateFlowPosition,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import { classNames, cn } from "../../../../utils/utils";
|
||||
import ToolbarSelectItem from "./toolbarSelectItem";
|
||||
|
||||
export default function NodeToolbarComponent({
|
||||
data,
|
||||
|
|
@ -38,8 +41,10 @@ export default function NodeToolbarComponent({
|
|||
showNode,
|
||||
name = "code",
|
||||
selected,
|
||||
updateNodeCode,
|
||||
setShowState,
|
||||
onCloseAdvancedModal,
|
||||
isOutdated,
|
||||
}: nodeToolbarPropsType): JSX.Element {
|
||||
const nodeLength = Object.keys(data.node!.template).filter(
|
||||
(templateField) =>
|
||||
|
|
@ -56,7 +61,7 @@ export default function NodeToolbarComponent({
|
|||
data.node.template[templateField].type === "dict" ||
|
||||
data.node.template[templateField].type === "NestedDict")
|
||||
).length;
|
||||
|
||||
const templates = useTypesStore((state) => state.templates);
|
||||
const hasStore = useStoreStore((state) => state.hasStore);
|
||||
const hasApiKey = useStoreStore((state) => state.hasApiKey);
|
||||
const validApiKey = useStoreStore((state) => state.validApiKey);
|
||||
|
|
@ -70,6 +75,7 @@ export default function NodeToolbarComponent({
|
|||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
const setNodes = useFlowStore((state) => state.setNodes);
|
||||
|
||||
const setEdges = useFlowStore((state) => state.setEdges);
|
||||
const unselectAll = useFlowStore((state) => state.unselectAll);
|
||||
const saveComponent = useFlowsManagerStore((state) => state.saveComponent);
|
||||
|
|
@ -79,7 +85,6 @@ export default function NodeToolbarComponent({
|
|||
const [showModalAdvanced, setShowModalAdvanced] = useState(false);
|
||||
const [showconfirmShare, setShowconfirmShare] = useState(false);
|
||||
const [showOverrideModal, setShowOverrideModal] = useState(false);
|
||||
|
||||
const [flowComponent, setFlowComponent] = useState<FlowType>();
|
||||
|
||||
const openInNewTab = (url) => {
|
||||
|
|
@ -170,6 +175,37 @@ export default function NodeToolbarComponent({
|
|||
paneY: nodes.find((node) => node.id === data.id)?.position.y,
|
||||
}
|
||||
);
|
||||
break;
|
||||
case "update":
|
||||
takeSnapshot();
|
||||
// to update we must get the code from the templates in useTypesStore
|
||||
const thisNodeTemplate = templates[data.type].template;
|
||||
// if the template does not have a code key
|
||||
// return
|
||||
if (!thisNodeTemplate.code) return;
|
||||
|
||||
const currentCode = thisNodeTemplate.code.value;
|
||||
if (data.node) {
|
||||
postCustomComponent(currentCode, data.node)
|
||||
.then((apiReturn) => {
|
||||
const { data } = apiReturn;
|
||||
if (data && updateNodeCode) {
|
||||
updateNodeCode(data, currentCode, "code");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
newNode.data = {
|
||||
...data,
|
||||
};
|
||||
newNode.data.node.template.code.value = currentCode;
|
||||
return newNode;
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
|
@ -411,93 +447,59 @@ export default function NodeToolbarComponent({
|
|||
<SelectContent>
|
||||
{nodeLength > 0 && (
|
||||
<SelectItem value={nodeLength === 0 ? "disabled" : "advanced"}>
|
||||
<div className="flex" data-testid="edit-button-modal">
|
||||
<IconComponent
|
||||
name="Settings2"
|
||||
className="relative top-0.5 mr-2 h-4 w-4 "
|
||||
/>{" "}
|
||||
<span className="">Edit</span>{" "}
|
||||
{navigator.userAgent.toUpperCase().includes("MAC") ? (
|
||||
<IconComponent
|
||||
name="Command"
|
||||
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
|
||||
></IconComponent>
|
||||
) : (
|
||||
<span className="absolute right-[1.15rem] top-[0.40em] stroke-2">
|
||||
Ctrl +{" "}
|
||||
</span>
|
||||
)}
|
||||
<span className="absolute right-2 top-[0.46em]">E</span>
|
||||
</div>
|
||||
<ToolbarSelectItem
|
||||
keyboardKey="E"
|
||||
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
|
||||
shift={false}
|
||||
value={"Edit"}
|
||||
icon={"Settings2"}
|
||||
/>
|
||||
</SelectItem>
|
||||
)}
|
||||
<SelectItem value={"duplicate"}>
|
||||
<div className="flex" data-testid="duplicate-button-modal">
|
||||
<IconComponent
|
||||
name="Copy"
|
||||
className="relative top-0.5 mr-2 h-4 w-4"
|
||||
/>
|
||||
Duplicate
|
||||
{navigator.userAgent.toUpperCase().includes("MAC") ? (
|
||||
<IconComponent
|
||||
name="Command"
|
||||
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
|
||||
></IconComponent>
|
||||
) : (
|
||||
<span className="absolute right-[1.20rem] top-[0.40em] stroke-2">
|
||||
Ctrl +{" "}
|
||||
</span>
|
||||
)}
|
||||
<span className="absolute right-2 top-[0.4em]">D</span>
|
||||
</div>{" "}
|
||||
<ToolbarSelectItem
|
||||
keyboardKey="D"
|
||||
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
|
||||
shift={false}
|
||||
value={"Duplicate"}
|
||||
icon={"Copy"}
|
||||
/>
|
||||
</SelectItem>
|
||||
<SelectItem value={"copy"}>
|
||||
<div className="flex">
|
||||
<IconComponent
|
||||
name="Clipboard"
|
||||
className="relative top-0.5 mr-2 h-4 w-4 "
|
||||
/>{" "}
|
||||
<span className="">Copy</span>{" "}
|
||||
{navigator.userAgent.toUpperCase().includes("MAC") ? (
|
||||
<IconComponent
|
||||
name="Command"
|
||||
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
|
||||
></IconComponent>
|
||||
) : (
|
||||
<span className="absolute right-[1.15rem] top-[0.40em] stroke-2">
|
||||
Ctrl +{" "}
|
||||
</span>
|
||||
)}
|
||||
<span className="absolute right-2 top-[0.4em]">C</span>
|
||||
</div>
|
||||
<ToolbarSelectItem
|
||||
keyboardKey="C"
|
||||
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
|
||||
shift={false}
|
||||
value={"Copy"}
|
||||
icon={"Clipboard"}
|
||||
/>
|
||||
</SelectItem>
|
||||
{isOutdated && (
|
||||
<SelectItem value={"update"}>
|
||||
<ToolbarSelectItem
|
||||
keyboardKey="U"
|
||||
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
|
||||
shift={false}
|
||||
value={"Update"}
|
||||
icon={"Code"}
|
||||
/>
|
||||
</SelectItem>
|
||||
)}
|
||||
{hasStore && (
|
||||
<SelectItem
|
||||
value={"Share"}
|
||||
disabled={!hasApiKey || !validApiKey}
|
||||
>
|
||||
<div className="flex" data-testid="share-button-modal">
|
||||
<IconComponent
|
||||
name="Share3"
|
||||
className="relative top-0.5 -m-1 mr-1 h-6 w-6"
|
||||
/>{" "}
|
||||
Share{" "}
|
||||
{navigator.userAgent.toUpperCase().includes("MAC") ? (
|
||||
<IconComponent
|
||||
name="Command"
|
||||
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
|
||||
></IconComponent>
|
||||
) : (
|
||||
<span className="absolute right-[2.10rem] top-[0.43em] stroke-2">
|
||||
Ctrl
|
||||
</span>
|
||||
)}
|
||||
<IconComponent
|
||||
name="ArrowBigUp"
|
||||
className="absolute right-[1.09rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
|
||||
></IconComponent>
|
||||
<span className="absolute right-2 top-[0.45em]">S</span>
|
||||
</div>{" "}
|
||||
<ToolbarSelectItem
|
||||
keyboardKey="S"
|
||||
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
|
||||
shift={true}
|
||||
value={"Share"}
|
||||
icon={"Share3"}
|
||||
styleObj={{
|
||||
iconClasses: "relative top-0.5 -m-1 mr-1 h-6 w-6",
|
||||
}}
|
||||
/>
|
||||
</SelectItem>
|
||||
)}
|
||||
{!hasStore && (
|
||||
|
|
@ -515,28 +517,13 @@ export default function NodeToolbarComponent({
|
|||
value={"documentation"}
|
||||
disabled={data.node?.documentation === ""}
|
||||
>
|
||||
<div className="flex">
|
||||
<IconComponent
|
||||
name="FileText"
|
||||
className="relative top-0.5 mr-2 h-4 w-4 "
|
||||
/>{" "}
|
||||
<span className="">Docs</span>{" "}
|
||||
{navigator.userAgent.toUpperCase().includes("MAC") ? (
|
||||
<IconComponent
|
||||
name="Command"
|
||||
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
|
||||
></IconComponent>
|
||||
) : (
|
||||
<span className="absolute right-[2.10rem] top-[0.43em] stroke-2">
|
||||
Ctrl
|
||||
</span>
|
||||
)}
|
||||
<IconComponent
|
||||
name="ArrowBigUp"
|
||||
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
|
||||
></IconComponent>
|
||||
<span className="absolute right-2 top-[0.43em]">D</span>
|
||||
</div>
|
||||
<ToolbarSelectItem
|
||||
keyboardKey="D"
|
||||
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
|
||||
shift={true}
|
||||
value={"Docs"}
|
||||
icon={"FileText"}
|
||||
/>
|
||||
</SelectItem>
|
||||
{isMinimal && (
|
||||
<SelectItem value={"show"}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import { useEffect } from "react";
|
||||
import ForwardedIconComponent from "../../../../../components/genericIconComponent";
|
||||
import { toolbarSelectItemProps } from "../../../../../types/components";
|
||||
|
||||
export default function ToolbarSelectItem({
|
||||
shift,
|
||||
isMac,
|
||||
keyboardKey,
|
||||
value,
|
||||
icon,
|
||||
styleObj,
|
||||
}: toolbarSelectItemProps) {
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<ForwardedIconComponent
|
||||
name={icon}
|
||||
className={`relative top-0.5 mr-2 h-4 w-4 ${styleObj?.iconClasses}`}
|
||||
/>{" "}
|
||||
<span className={styleObj?.valueClasses}>{value}</span>{" "}
|
||||
{isMac ? (
|
||||
<ForwardedIconComponent
|
||||
name="Command"
|
||||
className={`absolute right-[${shift ? "2rem" : "1.15rem"}] top-[0.65em] h-3.5 w-3.5 stroke-2 ${styleObj?.commandClasses}`}
|
||||
></ForwardedIconComponent>
|
||||
) : (
|
||||
<span className={`absolute right-[${shift ? "2.10rem" : "1.15rem"}] top-[0.43em] stroke-2 ${styleObj?.ctrlClasses}`}>
|
||||
{shift ? (
|
||||
"Ctrl"
|
||||
) : (
|
||||
"Ctrl +"
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{shift && (
|
||||
<ForwardedIconComponent
|
||||
name="ArrowBigUp"
|
||||
className={`absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2 ${styleObj?.shiftClasses}`}
|
||||
/>
|
||||
)}
|
||||
<span className={`absolute right-2 top-[0.43em] ${styleObj?.keyClasses}`}>{keyboardKey}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -147,7 +147,7 @@ export default function StorePage(): JSX.Element {
|
|||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response.status === 403 || err.response.status === 401) {
|
||||
if (err.response?.status === 403 || err.response?.status === 401) {
|
||||
setValidApiKey(false);
|
||||
} else {
|
||||
setSearchData([]);
|
||||
|
|
|
|||
|
|
@ -584,7 +584,7 @@
|
|||
}
|
||||
|
||||
.app-div {
|
||||
@apply fixed bottom-5 left-5 flex flex-col-reverse;
|
||||
@apply absolute bottom-5 left-5 flex flex-col-reverse;
|
||||
}
|
||||
|
||||
.chat-input-modal-txtarea {
|
||||
|
|
|
|||
|
|
@ -491,7 +491,13 @@ export type nodeToolbarPropsType = {
|
|||
openAdvancedModal?: boolean;
|
||||
onCloseAdvancedModal?: (close: boolean) => void;
|
||||
selected: boolean;
|
||||
updateNodeCode?: (
|
||||
newNodeClass: APIClassType,
|
||||
code: string,
|
||||
name: string
|
||||
) => void;
|
||||
setShowState: (show: boolean | SetStateAction<boolean>) => void;
|
||||
isOutdated?: boolean;
|
||||
};
|
||||
|
||||
export type parsedDataType = {
|
||||
|
|
@ -682,3 +688,19 @@ export type IOFileInputProps = {
|
|||
field: TemplateVariableType;
|
||||
updateValue: (e: any, type: string) => void;
|
||||
};
|
||||
|
||||
export type toolbarSelectItemProps = {
|
||||
isMac: boolean;
|
||||
shift: boolean;
|
||||
keyboardKey: string;
|
||||
value: string;
|
||||
icon: string;
|
||||
styleObj?: {
|
||||
iconClasses?: string;
|
||||
commandClasses?: string;
|
||||
shiftClasses?: string;
|
||||
ctrlClasses?: string;
|
||||
keyClasses?: string;
|
||||
valueClasses?: string;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,9 +9,15 @@ export const handleUpdateValues = async (name: string, data: NodeDataType) => {
|
|||
console.error("Code not found in the template");
|
||||
return;
|
||||
}
|
||||
const template = data.node?.template;
|
||||
if (!template) {
|
||||
console.error("No template found in the node.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let newTemplate = await postCustomComponentUpdate(
|
||||
code,
|
||||
template,
|
||||
name,
|
||||
data.node?.template[name]?.value
|
||||
)
|
||||
|
|
|
|||
|
|
@ -264,7 +264,7 @@ export function buildTweaks(flow: FlowType) {
|
|||
export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
|
||||
if (!node.data?.node?.template || !Object.keys(node.data.node.template)) {
|
||||
return [
|
||||
"We've noticed a potential issue with a node in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!",
|
||||
"We've noticed a potential issue with a Component in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!",
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@ export const nodeNames: { [char: string]: string } = {
|
|||
};
|
||||
|
||||
export const nodeIconsLucide: iconsType = {
|
||||
X: X,
|
||||
Notify: Bell,
|
||||
ListFlows: Group,
|
||||
ClearMessageHistory: FileClock,
|
||||
|
|
|
|||
|
|
@ -386,7 +386,7 @@ export function getCurlCode(
|
|||
-H 'Content-Type: application/json'\\${
|
||||
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
|
||||
}
|
||||
-d '{"inputs": ${inputs}, "tweaks": ${
|
||||
-d '{"inputs": [${inputs}], "tweaks": ${
|
||||
tweak && tweak.length > 0
|
||||
? buildTweakObject(tweak)
|
||||
: JSON.stringify(tweaks, null, 2)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue