Merge branch 'zustand/io/migration' into cz/zustand/io/migration

This commit is contained in:
anovazzi1 2024-01-30 18:16:15 -03:00
commit 0f23df1c8d
24 changed files with 857 additions and 532 deletions

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "langflow" name = "langflow"
version = "0.6.5a9" version = "0.6.5a12"
description = "A Python package with a built-in web application" description = "A Python package with a built-in web application"
authors = ["Logspace <contact@logspace.ai>"] authors = ["Logspace <contact@logspace.ai>"]
maintainers = [ maintainers = [

View file

@ -0,0 +1,59 @@
"""Add unique constraints
Revision ID: b2fa308044b5
Revises: 0b8757876a7c
Create Date: 2024-01-26 13:31:14.797548
"""
from typing import Sequence, Union
import sqlalchemy as sa
import sqlmodel
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'b2fa308044b5'
down_revision: Union[str, None] = '0b8757876a7c'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
try:
op.drop_table('flowstyle')
with op.batch_alter_table('flow', schema=None) as batch_op:
batch_op.add_column(sa.Column('is_component', sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column('updated_at', sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column('folder', sqlmodel.sql.sqltypes.AutoString(), nullable=True))
batch_op.add_column(sa.Column('user_id', sqlmodel.sql.sqltypes.GUID(), nullable=True))
batch_op.create_index(batch_op.f('ix_flow_user_id'), ['user_id'], unique=False)
batch_op.create_foreign_key(None, 'user', ['user_id'], ['id'])
except Exception:
pass
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
try:
with op.batch_alter_table('flow', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='foreignkey')
batch_op.drop_index(batch_op.f('ix_flow_user_id'))
batch_op.drop_column('user_id')
batch_op.drop_column('folder')
batch_op.drop_column('updated_at')
batch_op.drop_column('is_component')
op.create_table('flowstyle',
sa.Column('color', sa.VARCHAR(), nullable=False),
sa.Column('emoji', sa.VARCHAR(), nullable=False),
sa.Column('flow_id', sa.CHAR(length=32), nullable=True),
sa.Column('id', sa.CHAR(length=32), nullable=False),
sa.ForeignKeyConstraint(['flow_id'], ['flow.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('id')
)
except Exception:
pass
# ### end Alembic commands ###

View file

@ -0,0 +1,50 @@
"""New fixes
Revision ID: bc2f01c40e4a
Revises: b2fa308044b5
Create Date: 2024-01-26 13:34:14.496769
"""
from typing import Sequence, Union
import sqlalchemy as sa
import sqlmodel
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'bc2f01c40e4a'
down_revision: Union[str, None] = 'b2fa308044b5'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
try:
with op.batch_alter_table('flow', schema=None) as batch_op:
batch_op.add_column(sa.Column('is_component', sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column('updated_at', sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column('folder', sqlmodel.sql.sqltypes.AutoString(), nullable=True))
batch_op.add_column(sa.Column('user_id', sqlmodel.sql.sqltypes.GUID(), nullable=True))
batch_op.create_index(batch_op.f('ix_flow_user_id'), ['user_id'], unique=False)
batch_op.create_foreign_key('flow_user_id_fkey'
, 'user', ['user_id'], ['id'])
except Exception:
pass
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
try:
with op.batch_alter_table('flow', schema=None) as batch_op:
batch_op.drop_constraint('flow_user_id_fkey', type_='foreignkey')
batch_op.drop_index(batch_op.f('ix_flow_user_id'))
batch_op.drop_column('user_id')
batch_op.drop_column('folder')
batch_op.drop_column('updated_at')
batch_op.drop_column('is_component')
except Exception:
pass
# ### end Alembic commands ###

View file

@ -4,7 +4,6 @@ from io import BytesIO
from fastapi import APIRouter, Depends, HTTPException, UploadFile from fastapi import APIRouter, Depends, HTTPException, UploadFile
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from langflow.api.v1.schemas import UploadFileResponse from langflow.api.v1.schemas import UploadFileResponse
from langflow.services.auth.utils import get_current_active_user from langflow.services.auth.utils import get_current_active_user
from langflow.services.database.models.flow import Flow from langflow.services.database.models.flow import Flow
@ -42,7 +41,7 @@ async def upload_file(
file_content = await file.read() file_content = await file.read()
file_name = file.filename or hashlib.sha256(file_content).hexdigest() file_name = file.filename or hashlib.sha256(file_content).hexdigest()
folder = flow_id folder = flow_id
storage_service.save_file(flow_id=folder, file_name=file_name, data=file_content) await storage_service.save_file(flow_id=folder, file_name=file_name, data=file_content)
return UploadFileResponse(flowId=flow_id, file_path=f"{folder}/{file_name}") return UploadFileResponse(flowId=flow_id, file_path=f"{folder}/{file_name}")
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@ -61,7 +60,7 @@ async def download_file(file_name: str, flow_id: str, storage_service: StorageSe
if not content_type: if not content_type:
raise HTTPException(status_code=500, detail=f"Content type not found for extension {extension}") raise HTTPException(status_code=500, detail=f"Content type not found for extension {extension}")
file_content = storage_service.get_file(flow_id=flow_id, file_name=file_name) file_content = await storage_service.get_file(flow_id=flow_id, file_name=file_name)
headers = { headers = {
"Content-Disposition": f"attachment; filename={file_name} filename*=UTF-8''{file_name}", "Content-Disposition": f"attachment; filename={file_name} filename*=UTF-8''{file_name}",
"Content-Type": "application/octet-stream", "Content-Type": "application/octet-stream",
@ -87,7 +86,7 @@ async def download_image(file_name: str, flow_id: str, storage_service: StorageS
elif not content_type.startswith("image"): elif not content_type.startswith("image"):
raise HTTPException(status_code=500, detail=f"Content type {content_type} is not an image") raise HTTPException(status_code=500, detail=f"Content type {content_type} is not an image")
file_content = storage_service.get_file(flow_id=flow_id, file_name=file_name) file_content = await storage_service.get_file(flow_id=flow_id, file_name=file_name)
return StreamingResponse(BytesIO(file_content), media_type=content_type) return StreamingResponse(BytesIO(file_content), media_type=content_type)
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@ -98,7 +97,7 @@ async def list_files(
flow_id: str = Depends(get_flow_id), storage_service: StorageService = Depends(get_storage_service) flow_id: str = Depends(get_flow_id), storage_service: StorageService = Depends(get_storage_service)
): ):
try: try:
files = storage_service.list_files(flow_id=flow_id) files = await storage_service.list_files(flow_id=flow_id)
return {"files": files} return {"files": files}
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@ -109,7 +108,7 @@ async def delete_file(
file_name: str, flow_id: str = Depends(get_flow_id), storage_service: StorageService = Depends(get_storage_service) file_name: str, flow_id: str = Depends(get_flow_id), storage_service: StorageService = Depends(get_storage_service)
): ):
try: try:
storage_service.delete_file(flow_id=flow_id, file_name=file_name) await storage_service.delete_file(flow_id=flow_id, file_name=file_name)
return {"message": f"File {file_name} deleted successfully"} return {"message": f"File {file_name} deleted successfully"}
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))

View file

@ -1,4 +1,4 @@
from fastapi import Request, Response, APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from sqlmodel import Session from sqlmodel import Session
@ -33,8 +33,8 @@ async def login_to_get_access_token(
if user: if user:
tokens = create_user_tokens(user_id=user.id, db=db, update_last_login=True) tokens = create_user_tokens(user_id=user.id, db=db, update_last_login=True)
response.set_cookie("refresh_token_lf", tokens["refresh_token"], httponly=True, secure=True, samesite="strict") response.set_cookie("refresh_token_lf", tokens["refresh_token"], httponly=True)
response.set_cookie("access_token_lf", tokens["access_token"], httponly=False, secure=True, samesite="strict") response.set_cookie("access_token_lf", tokens["access_token"], httponly=False)
return tokens return tokens
else: else:
raise HTTPException( raise HTTPException(
@ -50,7 +50,7 @@ async def auto_login(
): ):
if settings_service.auth_settings.AUTO_LOGIN: if settings_service.auth_settings.AUTO_LOGIN:
tokens = create_user_longterm_token(db) tokens = create_user_longterm_token(db)
response.set_cookie("access_token_lf", tokens["access_token"], httponly=False, secure=True, samesite="strict") response.set_cookie("access_token_lf", tokens["access_token"], httponly=False)
return tokens return tokens
raise HTTPException( raise HTTPException(
@ -67,8 +67,8 @@ async def refresh_token(request: Request, response: Response):
token = request.cookies.get("refresh_token_lf") token = request.cookies.get("refresh_token_lf")
if token: if token:
tokens = create_refresh_token(token) tokens = create_refresh_token(token)
response.set_cookie("refresh_token_lf", tokens["refresh_token"], httponly=True, secure=True, samesite="strict") response.set_cookie("refresh_token_lf", tokens["refresh_token"], httponly=True)
response.set_cookie("access_token_lf", tokens["access_token"], httponly=False, secure=True, samesite="strict") response.set_cookie("access_token_lf", tokens["access_token"], httponly=False)
return tokens return tokens
else: else:
raise HTTPException( raise HTTPException(

View file

@ -1,7 +1,9 @@
from langflow import CustomComponent from typing import Callable, Optional, Union
from langchain.chains import ConversationChain from langchain.chains import ConversationChain
from typing import Optional, Union, Callable
from langflow.field_typing import BaseLanguageModel, BaseMemory, Chain from langflow import CustomComponent
from langflow.field_typing import BaseLanguageModel, BaseMemory, Chain, Text
class ConversationChainComponent(CustomComponent): class ConversationChainComponent(CustomComponent):
@ -23,7 +25,17 @@ class ConversationChainComponent(CustomComponent):
self, self,
llm: BaseLanguageModel, llm: BaseLanguageModel,
memory: Optional[BaseMemory] = None, memory: Optional[BaseMemory] = None,
) -> Union[Chain, Callable]: inputs: dict = {},
) -> Union[Chain, Callable, Text]:
if memory is None: if memory is None:
return ConversationChain(llm=llm) chain = ConversationChain(llm=llm)
return ConversationChain(llm=llm, memory=memory) chain = ConversationChain(llm=llm, memory=memory)
result = chain.invoke(inputs)
# result is an AIMessage which is a subclass of BaseMessage
# We need to check if it is a string or a BaseMessage
if hasattr(result, "content") and isinstance(result.content, str):
return result.content
elif isinstance(result, str):
return result
return str(result)

View file

@ -1,8 +1,9 @@
from typing import Callable, Optional, Union from typing import Callable, Optional, Union
from langchain.chains import LLMChain from langchain.chains import LLMChain
from langflow import CustomComponent from langflow import CustomComponent
from langflow.field_typing import BaseLanguageModel, BaseMemory, BasePromptTemplate, Chain from langflow.field_typing import BaseLanguageModel, BaseMemory, BasePromptTemplate, Chain, Text
class LLMChainComponent(CustomComponent): class LLMChainComponent(CustomComponent):
@ -22,5 +23,5 @@ class LLMChainComponent(CustomComponent):
prompt: BasePromptTemplate, prompt: BasePromptTemplate,
llm: BaseLanguageModel, llm: BaseLanguageModel,
memory: Optional[BaseMemory] = None, memory: Optional[BaseMemory] = None,
) -> Union[Chain, Callable]: ) -> Union[Chain, Callable, Text]:
return LLMChain(prompt=prompt, llm=llm, memory=memory) return LLMChain(prompt=prompt, llm=llm, memory=memory)

View file

@ -1,8 +1,9 @@
from langflow import CustomComponent
from langchain.llms.base import BaseLLM from langchain.llms.base import BaseLLM
from langchain.prompts import PromptTemplate from langchain.prompts import PromptTemplate
from langchain.schema import Document from langchain_core.messages import BaseMessage
from langflow import CustomComponent
from langflow.field_typing import Text
class PromptRunner(CustomComponent): class PromptRunner(CustomComponent):
@ -18,11 +19,15 @@ class PromptRunner(CustomComponent):
"code": {"show": False}, "code": {"show": False},
} }
def build(self, llm: BaseLLM, prompt: PromptTemplate, inputs: dict = {}) -> Document: def build(self, llm: BaseLLM, prompt: PromptTemplate, inputs: dict = {}) -> Text:
chain = prompt | llm chain = prompt | llm
# The input is an empty dict because the prompt is already filled # The input is an empty dict because the prompt is already filled
result = chain.invoke(input=inputs) result_message: BaseMessage = chain.invoke(input=inputs)
if hasattr(result, "content"): if hasattr(result_message, "content"):
result = result.content result: str = result_message.content
elif isinstance(result_message, str):
result = result_message
else:
result = str(result_message)
self.repr_value = result self.repr_value = result
return Document(page_content=str(result)) return result

View file

@ -5,13 +5,15 @@ from uuid import UUID
import yaml import yaml
from cachetools import TTLCache, cachedmethod from cachetools import TTLCache, cachedmethod
from fastapi import HTTPException from fastapi import HTTPException
from langflow.interface.custom.code_parser.utils import ( from langflow.interface.custom.code_parser.utils import (
extract_inner_type_from_generic_alias, extract_inner_type_from_generic_alias,
extract_union_types_from_generic_alias, extract_union_types_from_generic_alias,
) )
from langflow.services.database.models.flow import Flow from langflow.services.database.models.flow import Flow
from langflow.services.database.utils import session_getter from langflow.services.database.utils import session_getter
from langflow.services.deps import get_credential_service, get_db_service from langflow.services.deps import get_credential_service, get_db_service, get_storage_service
from langflow.services.storage.service import StorageService
from langflow.utils import validate from langflow.utils import validate
from .component import Component from .component import Component
@ -34,6 +36,13 @@ class CustomComponent(Component):
self.cache = TTLCache(maxsize=1024, ttl=60) self.cache = TTLCache(maxsize=1024, ttl=60)
super().__init__(**data) super().__init__(**data)
def get_full_path(self, path: str) -> str:
storage_svc: "StorageService" = get_storage_service()
flow_id = path.split("/")[0]
file_name = path.split("/")[1]
return storage_svc.build_full_path(flow_id, file_name)
def custom_repr(self): def custom_repr(self):
if self.repr_value == "": if self.repr_value == "":
self.repr_value = self.status self.repr_value = self.status

View file

@ -119,8 +119,8 @@ async def instantiate_based_on_type(class_object, base_type, node_type, params,
async def instantiate_custom_component(node_type, class_object, params, user_id): async def instantiate_custom_component(node_type, class_object, params, user_id):
params_copy = params.copy() params_copy = params.copy()
class_object: "CustomComponent" = eval_custom_component_code(params_copy.pop("code")) class_object: Type["CustomComponent"] = eval_custom_component_code(params_copy.pop("code"))
custom_component = class_object(user_id=user_id) custom_component: "CustomComponent" = class_object(user_id=user_id)
if "retriever" in params_copy and hasattr(params_copy["retriever"], "as_retriever"): if "retriever" in params_copy and hasattr(params_copy["retriever"], "as_retriever"):
params_copy["retriever"] = params_copy["retriever"].as_retriever() params_copy["retriever"] = params_copy["retriever"].as_retriever()

View file

@ -1,31 +1,96 @@
from pathlib import Path from pathlib import Path
import aiofiles
from loguru import logger
from .service import StorageService from .service import StorageService
class LocalStorageService(StorageService): class LocalStorageService(StorageService):
def __init__(self, session_service, settings_service): """A service class for handling local storage operations."""
super().__init__(session_service, settings_service)
self.data_dir = settings_service.settings.CONFIG_DIR
def __init__(self, session_service, settings_service):
"""Initialize the local storage service with session and settings services."""
super().__init__(session_service, settings_service)
self.data_dir = Path(settings_service.settings.CONFIG_DIR)
self.set_ready() self.set_ready()
def save_file(self, flow_id: str, file_name: str, data: bytes): def build_full_path(self, flow_id: str, file_name: str) -> str:
folder_path = Path(f"{self.data_dir}/{flow_id}") """Build the full path of a file in the local storage."""
return str(self.data_dir / flow_id / file_name)
async def save_file(self, flow_id: str, file_name: str, data: bytes):
"""
Save a file in the local storage.
:param flow_id: The identifier for the flow.
:param file_name: The name of the file to be saved.
:param data: The byte content of the file.
:raises FileNotFoundError: If the specified flow does not exist.
:raises IsADirectoryError: If the file name is a directory.
:raises PermissionError: If there is no permission to write the file.
"""
folder_path = self.data_dir / flow_id
folder_path.mkdir(parents=True, exist_ok=True) folder_path.mkdir(parents=True, exist_ok=True)
with open(folder_path / file_name, "wb") as f: file_path = folder_path / file_name
f.write(data)
def get_file(self, flow_id: str, file_name: str) -> bytes: try:
with open(f"{self.data_dir}/{flow_id}/{file_name}", "rb") as f: async with aiofiles.open(file_path, "wb") as f:
return f.read() await f.write(data)
logger.info(f"File {file_name} saved successfully in flow {flow_id}.")
except Exception as e:
logger.error(f"Error saving file {file_name} in flow {flow_id}: {e}")
raise e
def list_files(self, flow_id: str): async def get_file(self, flow_id: str, file_name: str) -> bytes:
folder_path = Path(f"{self.data_dir}/{flow_id}") """
return [file.name for file in folder_path.iterdir() if file.is_file()] Retrieve a file from the local storage.
def delete_file(self, flow_id: str, file_name: str): :param flow_id: The identifier for the flow.
Path(f"{self.data_dir}/{flow_id}/{file_name}").unlink() :param file_name: The name of the file to be retrieved.
:return: The byte content of the file.
:raises FileNotFoundError: If the file does not exist.
"""
file_path = self.data_dir / flow_id / file_name
if not file_path.exists():
logger.warning(f"File {file_name} not found in flow {flow_id}.")
raise FileNotFoundError(f"File {file_name} not found in flow {flow_id}")
async with aiofiles.open(file_path, "rb") as f:
logger.info(f"File {file_name} retrieved successfully from flow {flow_id}.")
return await f.read()
async def list_files(self, flow_id: str):
"""
List all files in a specified flow.
:param flow_id: The identifier for the flow.
:return: A list of file names.
:raises FileNotFoundError: If the flow directory does not exist.
"""
folder_path = self.data_dir / flow_id
if not folder_path.exists() or not folder_path.is_dir():
logger.warning(f"Flow {flow_id} directory does not exist.")
raise FileNotFoundError(f"Flow {flow_id} directory does not exist.")
files = [file.name for file in folder_path.iterdir() if file.is_file()]
logger.info(f"Listed {len(files)} files in flow {flow_id}.")
return files
async def delete_file(self, flow_id: str, file_name: str):
"""
Delete a file from the local storage.
:param flow_id: The identifier for the flow.
:param file_name: The name of the file to be deleted.
"""
file_path = self.data_dir / flow_id / file_name
if file_path.exists():
file_path.unlink()
logger.info(f"File {file_name} deleted successfully from flow {flow_id}.")
else:
logger.warning(f"Attempted to delete non-existent file {file_name} in flow {flow_id}.")
def teardown(self): def teardown(self):
pass """Perform any cleanup operations when the service is being torn down."""
pass # No specific teardown actions required for local storage at the moment.

View file

@ -1,43 +1,89 @@
import boto3 import boto3
from botocore.exceptions import ClientError, NoCredentialsError from botocore.exceptions import ClientError, NoCredentialsError
from loguru import logger
from .service import StorageService from .service import StorageService
class S3StorageService(StorageService): class S3StorageService(StorageService):
def __init__(self, session_service, settings_service): """A service class for handling operations with AWS S3 storage."""
async def __init__(self, session_service, settings_service):
"""Initialize the S3 storage service with session and settings services."""
super().__init__(session_service, settings_service) super().__init__(session_service, settings_service)
self.bucket = "langflow" self.bucket = "langflow"
self.s3_client = boto3.client("s3") self.s3_client = boto3.client("s3")
self.set_ready() self.set_ready()
def save_file(self, folder: str, file_name: str, data): async def save_file(self, folder: str, file_name: str, data):
"""
Save a file to the S3 bucket.
:param folder: The folder in the bucket to save the file.
:param file_name: The name of the file to be saved.
:param data: The byte content of the file.
:raises Exception: If an error occurs during file saving.
"""
try: try:
self.s3_client.put_object(Bucket=self.bucket, Key=f"{folder}/{file_name}", Body=data) self.s3_client.put_object(Bucket=self.bucket, Key=f"{folder}/{file_name}", Body=data)
logger.info(f"File {file_name} saved successfully in folder {folder}.")
except NoCredentialsError: except NoCredentialsError:
raise Exception("Credentials not available for AWS S3.") logger.error("Credentials not available for AWS S3.")
raise
except ClientError as e: except ClientError as e:
raise Exception(f"An error occurred: {e}") logger.error(f"Error saving file {file_name} in folder {folder}: {e}")
raise
def get_file(self, folder: str, file_name: str): async def get_file(self, folder: str, file_name: str):
"""
Retrieve a file from the S3 bucket.
:param folder: The folder in the bucket where the file is stored.
:param file_name: The name of the file to be retrieved.
:return: The byte content of the file.
:raises Exception: If an error occurs during file retrieval.
"""
try: try:
response = self.s3_client.get_object(Bucket=self.bucket, Key=f"{folder}/{file_name}") response = self.s3_client.get_object(Bucket=self.bucket, Key=f"{folder}/{file_name}")
logger.info(f"File {file_name} retrieved successfully from folder {folder}.")
return response["Body"].read() return response["Body"].read()
except ClientError as e: except ClientError as e:
raise Exception(f"An error occurred: {e}") logger.error(f"Error retrieving file {file_name} from folder {folder}: {e}")
raise
def list_files(self, folder: str): async def list_files(self, folder: str):
"""
List all files in a specified folder of the S3 bucket.
:param folder: The folder in the bucket to list files from.
:return: A list of file names.
:raises Exception: If an error occurs during file listing.
"""
try: try:
response = self.s3_client.list_objects_v2(Bucket=self.bucket, Prefix=folder) response = self.s3_client.list_objects_v2(Bucket=self.bucket, Prefix=folder)
return [item["Key"] for item in response.get("Contents", []) if "/" not in item["Key"][len(folder) :]] files = [item["Key"] for item in response.get("Contents", []) if "/" not in item["Key"][len(folder) :]]
logger.info(f"{len(files)} files listed in folder {folder}.")
return files
except ClientError as e: except ClientError as e:
raise Exception(f"An error occurred: {e}") logger.error(f"Error listing files in folder {folder}: {e}")
raise
def delete_file(self, folder: str, file_name: str): async def delete_file(self, folder: str, file_name: str):
"""
Delete a file from the S3 bucket.
:param folder: The folder in the bucket where the file is stored.
:param file_name: The name of the file to be deleted.
:raises Exception: If an error occurs during file deletion.
"""
try: try:
self.s3_client.delete_object(Bucket=self.bucket, Key=f"{folder}/{file_name}") self.s3_client.delete_object(Bucket=self.bucket, Key=f"{folder}/{file_name}")
logger.info(f"File {file_name} deleted successfully from folder {folder}.")
except ClientError as e: except ClientError as e:
raise Exception(f"An error occurred: {e}") logger.error(f"Error deleting file {file_name} from folder {folder}: {e}")
raise
def teardown(self): async def teardown(self):
"""Perform any cleanup operations when the service is being torn down."""
# No specific teardown actions required for S3 storage at the moment.
pass pass

View file

@ -16,23 +16,26 @@ class StorageService(Service):
self.session_service = session_service self.session_service = session_service
self.set_ready() self.set_ready()
def build_full_path(self, flow_id: str, file_name: str) -> str:
pass
def set_ready(self): def set_ready(self):
self.ready = True self.ready = True
@abstractmethod @abstractmethod
def save_file(self, flow_id: str, file_name: str, data) -> None: async def save_file(self, flow_id: str, file_name: str, data) -> None:
pass pass
@abstractmethod @abstractmethod
def get_file(self, flow_id: str, file_name: str) -> bytes: async def get_file(self, flow_id: str, file_name: str) -> bytes:
pass pass
@abstractmethod @abstractmethod
def list_files(self, flow_id: str) -> list[str]: async def list_files(self, flow_id: str) -> list[str]:
pass pass
@abstractmethod @abstractmethod
def delete_file(self, flow_id: str, file_name: str) -> bool: async def delete_file(self, flow_id: str, file_name: str) -> bool:
pass pass
def teardown(self): def teardown(self):

File diff suppressed because it is too large Load diff

View file

@ -83,6 +83,13 @@ export default function GenericNode({
countHandles(); countHandles();
}, [data, data.node]); }, [data, data.node]);
useEffect(() => {
if (!selected) {
setInputName(false);
setInputDescription(false);
}
}, [selected]);
// State for outline color // State for outline color
const isBuilding = useFlowStore((state) => state.isBuilding); const isBuilding = useFlowStore((state) => state.isBuilding);
@ -383,14 +390,14 @@ export default function GenericNode({
) )
} }
> >
<div className="generic-node-status-position flex items-center"> <div className="generic-node-status-position flex items-center justify-center">
<IconComponent <IconComponent
name="Zap" name="Zap"
className={classNames( className={classNames(
validationStatus && validationStatus.valid validationStatus && validationStatus.valid
? "green-status" ? "green-status"
: "status-build-animation", : "status-build-animation",
"h-5 stroke-1" "absolute h-5 stroke-1"
)} )}
/> />
<IconComponent <IconComponent
@ -399,7 +406,7 @@ export default function GenericNode({
validationStatus && !validationStatus.valid validationStatus && !validationStatus.valid
? "red-status" ? "red-status"
: "status-build-animation", : "status-build-animation",
"h-5 stroke-1" "absolute h-5 stroke-1"
)} )}
/> />
<IconComponent <IconComponent
@ -408,7 +415,7 @@ export default function GenericNode({
!validationStatus || isBuilding !validationStatus || isBuilding
? "yellow-status" ? "yellow-status"
: "status-build-animation", : "status-build-animation",
"h-5 stroke-1" "absolute h-5 stroke-1"
)} )}
/> />
</div> </div>

View file

@ -1,42 +1,54 @@
import { cloneDeep } from "lodash";
import useFlowStore from "../../stores/flowStore";
import { IOInputProps } from "../../types/components"; import { IOInputProps } from "../../types/components";
import IOFileInput from "../IOInputs/FileInput";
import { Textarea } from "../ui/textarea"; import { Textarea } from "../ui/textarea";
export default function IOInputField({ export default function IOInputField({
inputType, inputType,
field, inputId,
updateValue,
}: IOInputProps): JSX.Element | undefined { }: IOInputProps): JSX.Element | undefined {
const nodes = useFlowStore((state) => state.nodes);
const setNode = useFlowStore((state) => state.setNode);
const node = nodes.find((node) => node.id === inputId);
function handleInputType() { function handleInputType() {
console.log(inputType); if (!node) return "no node found";
const handleUpdateValue = (e) => {
updateValue(e, "text");
};
switch (inputType) { switch (inputType) {
case "TextInput": case "TextInput":
return ( return (
<Textarea <Textarea
className="custom-scroll" className="h-full w-full custom-scroll"
placeholder={"Enter text..."} placeholder={"Enter text..."}
value={field?.value} value={node.data.node!.template["value"].value}
onChange={handleUpdateValue} onChange={(e) => {
e.target.value;
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["value"].value = e.target.value;
setNode(node.id, newNode);
}
}}
/> />
); );
case "FileLoader": case "fileLoader":
return <IOFileInput field={field} updateValue={updateValue} />; // return <IOFileInput />;
default: default:
return ( return (
<Textarea <Textarea
className="custom-scroll" className="h-full w-full custom-scroll"
placeholder={"Enter text..."} placeholder={"Enter text..."}
value={field?.value} value={node.data.node!.template["value"]}
onChange={handleUpdateValue} onChange={(e) => {
e.target.value;
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["value"].value = e.target.value;
setNode(node.id, newNode);
}
}}
/> />
); );
} }
} }
return <div className="h-full">{handleInputType()}</div>; return <div className="h-full w-full">{handleInputType()}</div>;
} }

View file

@ -0,0 +1,47 @@
import { cloneDeep } from "lodash";
import useFlowStore from "../../stores/flowStore";
import { IOOutputProps } from "../../types/components";
import { Textarea } from "../ui/textarea";
export default function IOOutputView({
outputType,
outputId,
}: IOOutputProps): JSX.Element | undefined {
const nodes = useFlowStore((state) => state.nodes);
const setNode = useFlowStore((state) => state.setNode);
const flowPool = useFlowStore((state) => state.flowPool);
const node = nodes.find((node) => node.id === outputId);
function handleOutputType() {
if (!node) return "no node found";
switch (outputType) {
case "TextOutput":
return (
<Textarea
className="h-full w-full custom-scroll"
placeholder={"Enter text..."}
// update to real value on flowPool
value={flowPool[node.id][flowPool[node.id].length - 1].data.results}
readOnly
/>
);
default:
return (
<Textarea
className="h-full w-full custom-scroll"
placeholder={"Enter text..."}
value={node.data.node!.template["value"]}
onChange={(e) => {
e.target.value;
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["value"].value = e.target.value;
setNode(node.id, newNode);
}
}}
/>
);
}
}
return <div className="h-full w-full">{handleOutputType()}</div>;
}

View file

@ -1,10 +1,11 @@
import { cloneDeep } from "lodash";
import { ReactNode, useState } from "react"; import { ReactNode, useState } from "react";
import useFlowStore from "../../stores/flowStore"; import useFlowStore from "../../stores/flowStore";
import { NodeType } from "../../types/flow"; import { NodeType } from "../../types/flow";
import { extractTypeFromLongId } from "../../utils/utils"; import { isInputType, isOutputType } from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import AccordionComponent from "../AccordionComponent"; import AccordionComponent from "../AccordionComponent";
import IOInputField from "../IOInputField"; import IOInputField from "../IOInputField";
import IOOutputView from "../IOOutputView";
import IconComponent from "../genericIconComponent"; import IconComponent from "../genericIconComponent";
import NewChatView from "../newChatView"; import NewChatView from "../newChatView";
import { Badge } from "../ui/badge"; import { Badge } from "../ui/badge";
@ -16,32 +17,91 @@ export default function IOView(): JSX.Element {
const outputIds = outputs.map((obj) => obj.id); const outputIds = outputs.map((obj) => obj.id);
const nodes = useFlowStore((state) => state.nodes); const nodes = useFlowStore((state) => state.nodes);
const setNode = useFlowStore((state) => state.setNode); const setNode = useFlowStore((state) => state.setNode);
const options = inputIds.concat(outputIds); const categories = getCategories();
//TODO: show output options for view const [selectedCategory, setSelectedCategory] = useState<string>(
const [selectedView, setSelectedView] = useState<ReactNode>( categories[0]
handleSelectChange(options[0])
); );
// if (outputTypes.includes("ChatOutput")) { const [showChat, setShowChat] = useState<boolean>(false);
// return <NewChatView />; const [selectedView, setSelectedView] = useState<{
// } type: string;
function handleSelectChange(selected: string) { id?: string;
const type = extractTypeFromLongId(selected); }>(handleInitialView());
return <NewChatView />;
switch (type) { function handleInitialView() {
case "ChatOutput": if (outputs.map((output) => output.type).includes("ChatOutput")) {
return <NewChatView />; return { type: "ChatOutput" };
break;
} }
return { type: "" };
} }
console.log(inputs);
function getCategories() {
const categories: string[] = [];
if (inputs.length > 0) categories.push("Inputs");
if (outputs.filter((output) => output.type !== "ChatOutput").length > 0)
categories.push("Outputs");
return categories;
}
function handleSelectChange(): ReactNode {
const { type, id } = selectedView;
if (type === "ChatOutput") return <NewChatView />;
if (isInputType(type))
return <IOInputField inputId={id!} inputType={type} />;
if (isOutputType(type))
return <IOOutputView outputId={id!} outputType={type} />;
else return <div>no view selected</div>;
}
function UpdateAccordion() {
return selectedCategory === "Inputs" ? inputs : outputs;
}
return ( return (
<div className="form-modal-iv-box"> <div className="form-modal-iv-box">
<div className="mr-6 flex h-full w-2/6 flex-col justify-start overflow-auto scrollbar-hide"> <div className="mr-6 flex h-full w-2/6 flex-col justify-start overflow-auto scrollbar-hide">
<div className="file-component-arrangement"> <div className="flex items-start gap-4 py-2">
<IconComponent name="Variable" className=" file-component-variable" /> {categories.map((category, index) => {
<span className="file-component-variables-span text-md">Inputs</span> return (
//hide chat button if chat is alredy on the view
<button
onClick={() => setSelectedCategory(category)}
className={classNames(
"cursor flex items-center rounded-md rounded-b-none px-1",
category == selectedCategory
? "border border-b-0 border-muted-foreground"
: "hover:bg-muted-foreground"
)}
key={index}
>
<IconComponent
name="Variable"
className=" file-component-variable"
/>
<span className="file-component-variables-span text-md">
{category}
</span>
</button>
);
})}
{selectedView.type !== "ChatOutput" && (
<button
onClick={() => setSelectedView({ type: "ChatOutput" })}
className={
"cursor flex items-center rounded-md rounded-b-none px-1 hover:bg-muted-foreground"
}
key={"chat"}
>
<IconComponent
name="Variable"
className=" file-component-variable"
/>
<span className="file-component-variables-span text-md">
Chat
</span>
</button>
)}
</div> </div>
{inputs {UpdateAccordion()
.filter((input) => input.type !== "ChatInput") .filter((input) => input.type !== "ChatInput")
.map((input, index) => { .map((input, index) => {
const node: NodeType = nodes.find((node) => node.id === input.id)!; const node: NodeType = nodes.find((node) => node.id === input.id)!;
@ -54,11 +114,17 @@ export default function IOView(): JSX.Element {
{input.id} {input.id}
</Badge> </Badge>
<div <div
className="-mb-1" className="-mb-1 pr-4"
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
setSelectedView({ type: input.type, id: input.id });
}} }}
></div> >
<IconComponent
className="h-4 w-4"
name="ScreenShare"
></IconComponent>
</div>
</div> </div>
} }
key={index} key={index}
@ -66,30 +132,7 @@ export default function IOView(): JSX.Element {
> >
<div className="file-component-tab-column"> <div className="file-component-tab-column">
{node && ( {node && (
<IOInputField <IOInputField inputType={input.type} inputId={input.id} />
field={
node.data.node!.template["value"] ||
node.data.node!.template["file_path"]["value"]
}
inputType={input.type}
updateValue={(e, type) => {
if (type === "file") {
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["file_path"].value =
e;
setNode(node.id, newNode);
}
} else {
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["value"].value =
e.target.value;
setNode(node.id, newNode);
}
}
}}
/>
)} )}
</div> </div>
</AccordionComponent> </AccordionComponent>
@ -97,7 +140,7 @@ export default function IOView(): JSX.Element {
); );
})} })}
</div> </div>
{selectedView} {handleSelectChange()}
</div> </div>
); );
} }

View file

@ -40,6 +40,7 @@ export default function newChatView(): JSX.Element {
useEffect(() => { useEffect(() => {
const chatOutputResponses: FlowPoolObjectType[] = []; const chatOutputResponses: FlowPoolObjectType[] = [];
outputIds.forEach((outputId) => { outputIds.forEach((outputId) => {
console.log("rodou", flowPool[outputId]);
if (outputId.includes("ChatOutput")) { if (outputId.includes("ChatOutput")) {
if (flowPool[outputId] && flowPool[outputId].length > 0) { if (flowPool[outputId] && flowPool[outputId].length > 0) {
chatOutputResponses.push(...flowPool[outputId]); chatOutputResponses.push(...flowPool[outputId]);

View file

@ -9,6 +9,7 @@ import {
applyNodeChanges, applyNodeChanges,
} from "reactflow"; } from "reactflow";
import { create } from "zustand"; import { create } from "zustand";
import { updateFlowInDatabase } from "../controllers/API";
import { import {
NodeDataType, NodeDataType,
NodeType, NodeType,
@ -45,12 +46,19 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
set({ flowPool }); set({ flowPool });
}, },
addDataToFlowPool: (data: any, nodeId: string) => { addDataToFlowPool: (data: any, nodeId: string) => {
const currentFlow = useFlowsManagerStore.getState().currentFlow;
let newFlowPool = cloneDeep({ ...get().flowPool }); let newFlowPool = cloneDeep({ ...get().flowPool });
if (!newFlowPool[nodeId]) newFlowPool[nodeId] = [data]; if (!newFlowPool[nodeId]) newFlowPool[nodeId] = [data];
else { else {
newFlowPool[nodeId].push(data); newFlowPool[nodeId].push(data);
} }
get().setFlowPool(newFlowPool); get().setFlowPool(newFlowPool);
if (currentFlow) {
window.sessionStorage.setItem(
`${currentFlow!.id}`,
JSON.stringify(newFlowPool)
);
}
}, },
CleanFlowPool: () => { CleanFlowPool: () => {
get().setFlowPool({}); get().setFlowPool({});
@ -59,16 +67,23 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
set({ isPending }); set({ isPending });
}, },
resetFlow: ({ nodes, edges, viewport }) => { resetFlow: ({ nodes, edges, viewport }) => {
const currentFlow = useFlowsManagerStore.getState().currentFlow;
let flowPool = {};
if (currentFlow) {
flowPool = JSON.parse(
window.sessionStorage.getItem(`${currentFlow!.id}`) ?? "{}"
);
}
let newEdges = cleanEdges(nodes, edges); let newEdges = cleanEdges(nodes, edges);
const { inputs, outputs } = getInputsAndOutputs(nodes); const { inputs, outputs } = getInputsAndOutputs(nodes);
set({ set({
nodes, nodes,
edges: newEdges, edges: newEdges,
flowState: undefined, flowState: undefined,
inputs, inputs,
outputs, outputs,
hasIO: inputs.length > 0 && outputs.length > 0, hasIO: inputs.length > 0 || outputs.length > 0,
flowPool,
}); });
get().reactFlowInstance!.setViewport(viewport); get().reactFlowInstance!.setViewport(viewport);
}, },
@ -109,7 +124,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
flowState: undefined, flowState: undefined,
inputs, inputs,
outputs, outputs,
hasIO: inputs.length > 0 && outputs.length > 0, hasIO: inputs.length > 0 || outputs.length > 0,
}); });
const flowsManager = useFlowsManagerStore.getState(); const flowsManager = useFlowsManagerStore.getState();
@ -330,6 +345,16 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
function handleBuildUpdate(data: any) { function handleBuildUpdate(data: any) {
get().addDataToFlowPool(data.data[data.id], data.id); get().addDataToFlowPool(data.data[data.id], data.id);
} }
await updateFlowInDatabase({
data: {
nodes: get().nodes,
edges: get().edges,
viewport: get().reactFlowInstance?.getViewport()!,
},
id: currentFlow!.id,
name: currentFlow!.name,
description: currentFlow!.description,
});
return buildVertices({ return buildVertices({
flowId: currentFlow!.id, flowId: currentFlow!.id,
nodeId, nodeId,

View file

@ -290,7 +290,7 @@
@apply hidden h-4 w-4 animate-spin rounded-full bg-ring opacity-0; @apply hidden h-4 w-4 animate-spin rounded-full bg-ring opacity-0;
} }
.generic-node-status { .generic-node-status {
@apply opacity-100; @apply opacity-100 animate-wiggle;
} }
.green-status { .green-status {
@apply generic-node-status text-status-green fill-status-green; @apply generic-node-status text-status-green fill-status-green;
@ -302,7 +302,7 @@
@apply generic-node-status text-status-yellow fill-status-yellow; @apply generic-node-status text-status-yellow fill-status-yellow;
} }
.status-build-animation { .status-build-animation {
@apply hidden animate-spin text-ring opacity-0; @apply opacity-0;
} }
.status-div { .status-div {
@apply absolute w-4 duration-200 ease-in-out; @apply absolute w-4 duration-200 ease-in-out;
@ -1012,7 +1012,7 @@
} }
.beta-badge-wrapper { .beta-badge-wrapper {
@apply absolute right-0 top-0 h-16 w-16 overflow-hidden rounded-tr-lg; @apply absolute right-0 top-0 h-16 w-16 overflow-hidden rounded-tr-lg pointer-events-none;
} }
.beta-badge-content { .beta-badge-content {
@apply mt-2 w-24 rotate-45 bg-beta-background text-center text-xs font-semibold text-beta-foreground; @apply mt-2 w-24 rotate-45 bg-beta-background text-center text-xs font-semibold text-beta-foreground;

View file

@ -649,8 +649,11 @@ export type dropdownButtonPropsType = {
export type IOInputProps = { export type IOInputProps = {
inputType: string; inputType: string;
field: TemplateVariableType; inputId: string;
updateValue: (e: any, type: string) => void; };
export type IOOutputProps = {
outputType: string;
outputId: string;
}; };
export type IOFileInputProps = { export type IOFileInputProps = {

View file

@ -677,6 +677,7 @@ export function validateSelection(
if (selection.edges.length === 0) { if (selection.edges.length === 0) {
selection.edges = edges; selection.edges = edges;
} }
// get only edges that are connected to the nodes in the selection // get only edges that are connected to the nodes in the selection
// first creates a set of all the nodes ids // first creates a set of all the nodes ids
let nodesSet = new Set(selection.nodes.map((n) => n.id)); let nodesSet = new Set(selection.nodes.map((n) => n.id));
@ -692,7 +693,17 @@ export function validateSelection(
if (selection.nodes.length < 2) { if (selection.nodes.length < 2) {
errorsArray.push("Please select more than one node"); errorsArray.push("Please select more than one node");
} }
if (
selection.nodes.some(
(node) =>
isInputNode(node.data as NodeDataType) ||
isOutputNode(node.data as NodeDataType)
)
) {
errorsArray.push(
"Please select only nodes that are not input or output nodes"
);
}
//check if there are two or more nodes with free outputs //check if there are two or more nodes with free outputs
if ( if (
selection.nodes.filter( selection.nodes.filter(
@ -1296,3 +1307,11 @@ export function isInputNode(nodeData: NodeDataType): boolean {
export function isOutputNode(nodeData: NodeDataType): boolean { export function isOutputNode(nodeData: NodeDataType): boolean {
return OUTPUT_TYPES.has(nodeData.type); return OUTPUT_TYPES.has(nodeData.type);
} }
export function isInputType(type: string): boolean {
return INPUT_TYPES.has(type);
}
export function isOutputType(type: string): boolean {
return OUTPUT_TYPES.has(type);
}

View file

@ -77,6 +77,7 @@ import {
Save, Save,
SaveAll, SaveAll,
Scissors, Scissors,
ScreenShare,
Search, Search,
Settings2, Settings2,
Share, Share,
@ -388,4 +389,5 @@ export const nodeIconsLucide: iconsType = {
TerminalIcon, TerminalIcon,
Repeat, Repeat,
io: ArrowDownUp, io: ArrowDownUp,
ScreenShare,
}; };