diff --git a/poetry.lock b/poetry.lock index 97f1d4015..0bf920865 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1456,6 +1456,7 @@ files = [ [package.dependencies] appdirs = ">=1.4.4,<2.0.0" click = ">=8.1.7,<9.0.0" +crewai-tools = {version = ">=0.4.7,<0.5.0", optional = true, markers = "extra == \"tools\""} embedchain = ">=0.1.114,<0.2.0" instructor = "1.3.3" jsonref = ">=1.1.0,<2.0.0" @@ -1472,6 +1473,33 @@ regex = ">=2023.12.25,<2024.0.0" agentops = ["agentops (>=0.1.9,<0.2.0)"] tools = ["crewai-tools (>=0.4.7,<0.5.0)"] +[[package]] +name = "crewai-tools" +version = "0.4.8" +description = "Set of tools for the crewAI framework" +optional = false +python-versions = "<=3.13,>=3.10" +files = [ + {file = "crewai_tools-0.4.8-py3-none-any.whl", hash = "sha256:628b08515ee0e06c751da1dd66b0cff70c9b2644775891c8f59883cb5debfef4"}, + {file = "crewai_tools-0.4.8.tar.gz", hash = "sha256:ae190bd187f980163523c86ee7e1eb2ed78896f935d6caff98908dd7ab6c982b"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.12.3,<5.0.0" +chromadb = ">=0.4.22,<0.5.0" +docker = ">=7.1.0,<8.0.0" +docx2txt = ">=0.8,<0.9" +embedchain = ">=0.1.114,<0.2.0" +lancedb = ">=0.5.4,<0.6.0" +langchain = ">0.2,<=0.3" +openai = ">=1.12.0,<2.0.0" +pydantic = ">=2.6.1,<3.0.0" +pyright = ">=1.1.350,<2.0.0" +pytest = ">=8.0.0,<9.0.0" +pytube = ">=15.0.0,<16.0.0" +requests = ">=2.31.0,<3.0.0" +selenium = ">=4.18.1,<5.0.0" + [[package]] name = "cryptography" version = "42.0.8" @@ -1822,6 +1850,16 @@ files = [ {file = "docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e"}, ] +[[package]] +name = "docx2txt" +version = "0.8" +description = "A pure python-based utility to extract text and images from docx files." +optional = false +python-versions = "*" +files = [ + {file = "docx2txt-0.8.tar.gz", hash = "sha256:2c06d98d7cfe2d3947e5760a57d924e3ff07745b379c8737723922e7009236e5"}, +] + [[package]] name = "dspy-ai" version = "2.4.12" @@ -4453,6 +4491,39 @@ websocket-client = ">=0.32.0,<0.40.0 || >0.40.0,<0.41.dev0 || >=0.43.dev0" [package.extras] adal = ["adal (>=1.0.2)"] +[[package]] +name = "lancedb" +version = "0.5.7" +description = "lancedb" +optional = false +python-versions = ">=3.8" +files = [ + {file = "lancedb-0.5.7-py3-none-any.whl", hash = "sha256:6169966f715ef530be545950e1aaf9f3f160967e4ba7456cd67c9f30f678095d"}, + {file = "lancedb-0.5.7.tar.gz", hash = "sha256:878914b493f91d09a77b14f1528104741f273234cbdd6671be705f447701fd51"}, +] + +[package.dependencies] +attrs = ">=21.3.0" +cachetools = "*" +click = ">=8.1.7" +deprecation = "*" +overrides = ">=0.7" +pydantic = ">=1.10" +pylance = "0.9.18" +pyyaml = ">=6.0" +ratelimiter = ">=1.0,<2.0" +requests = ">=2.31.0" +retry = ">=0.9.2" +semver = ">=3.0" +tqdm = ">=4.27.0" + +[package.extras] +clip = ["open-clip", "pillow", "torch"] +dev = ["pre-commit", "ruff"] +docs = ["mkdocs", "mkdocs-jupyter", "mkdocs-material", "mkdocs-ultralytics-plugin (==0.0.44)", "mkdocstrings[python]"] +embeddings = ["InstructorEmbedding", "awscli (>=1.29.57)", "boto3 (>=1.28.57)", "botocore (>=1.31.57)", "cohere", "google.generativeai", "huggingface-hub", "open-clip-torch", "openai (>=1.6.1)", "pillow", "sentence-transformers", "torch"] +tests = ["aiohttp", "duckdb", "pandas (>=1.4)", "polars (>=0.19)", "pytest", "pytest-asyncio", "pytest-mock", "pytz"] + [[package]] name = "langchain" version = "0.2.8" @@ -6519,6 +6590,20 @@ files = [ {file = "orjson-3.10.0.tar.gz", hash = "sha256:ba4d8cac5f2e2cff36bea6b6481cdb92b38c202bcec603d6f5ff91960595a1ed"}, ] +[[package]] +name = "outcome" +version = "1.3.0.post0" +description = "Capture the outcome of Python function calls." +optional = false +python-versions = ">=3.7" +files = [ + {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, + {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, +] + +[package.dependencies] +attrs = ">=19.2.0" + [[package]] name = "overrides" version = "7.7.0" @@ -7278,6 +7363,17 @@ files = [ [package.extras] tests = ["pytest"] +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] + [[package]] name = "py-cpuinfo" version = "9.0.0" @@ -7615,6 +7711,30 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pylance" +version = "0.9.18" +description = "python wrapper for Lance columnar format" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pylance-0.9.18-cp38-abi3-macosx_10_15_x86_64.whl", hash = "sha256:fe2445d922c594d90e89111385106f6b152caab27996217db7bb4b8947eb0bea"}, + {file = "pylance-0.9.18-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:a2c424c50f5186edbbcc5a26f34063ed09d9a7390e28033395728ce02b5658f0"}, + {file = "pylance-0.9.18-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10af06edfde3e8451bf2251381d3980a0a164eab9d4c3d4dc8b6318969e958a6"}, + {file = "pylance-0.9.18-cp38-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:d8bb9045d7163cc966b9fe34a917044192be37a90915475b77461e5b7d89e442"}, + {file = "pylance-0.9.18-cp38-abi3-win_amd64.whl", hash = "sha256:5ea80b7bf70d992f3fe63bce2d2f064f742124c04eaedeb76baca408ded85a2c"}, +] + +[package.dependencies] +numpy = ">=1.22" +pyarrow = ">=12" + +[package.extras] +benchmarks = ["pytest-benchmark"] +dev = ["ruff (==0.2.2)"] +tests = ["datasets", "duckdb", "ml_dtypes", "pandas", "pillow", "polars[pandas,pyarrow]", "pytest", "tensorflow", "tqdm"] +torch = ["torch"] + [[package]] name = "pymilvus" version = "2.4.4" @@ -7789,6 +7909,24 @@ files = [ {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, ] +[[package]] +name = "pyright" +version = "1.1.371" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.371-py3-none-any.whl", hash = "sha256:cce52e42ff73943243e7e5e24f2a59dee81b97d99f4e3cf97370b27e8a1858cd"}, + {file = "pyright-1.1.371.tar.gz", hash = "sha256:777b508b92dda2db476214c400ce043aad8d8f3dd0e10d284c96e79f298308b5"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + [[package]] name = "pysbd" version = "0.3.4" @@ -7799,6 +7937,18 @@ files = [ {file = "pysbd-0.3.4-py3-none-any.whl", hash = "sha256:cd838939b7b0b185fcf86b0baf6636667dfb6e474743beeff878e9f42e022953"}, ] +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + [[package]] name = "pytest" version = "8.2.2" @@ -8534,6 +8684,20 @@ files = [ [package.extras] full = ["numpy"] +[[package]] +name = "ratelimiter" +version = "1.2.0.post0" +description = "Simple python rate limiting object" +optional = false +python-versions = "*" +files = [ + {file = "ratelimiter-1.2.0.post0-py3-none-any.whl", hash = "sha256:a52be07bc0bb0b3674b4b304550f10c769bbb00fead3072e035904474259809f"}, + {file = "ratelimiter-1.2.0.post0.tar.gz", hash = "sha256:5c395dcabdbbde2e5178ef3f89b568a3066454a6ddc223b76473dac22f89b4f7"}, +] + +[package.extras] +test = ["pytest (>=3.0)", "pytest-asyncio"] + [[package]] name = "realtime" version = "1.0.6" @@ -8752,6 +8916,21 @@ files = [ [package.dependencies] httpx = ">=0.21.0" +[[package]] +name = "retry" +version = "0.9.2" +description = "Easy to use retry decorator." +optional = false +python-versions = "*" +files = [ + {file = "retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606"}, + {file = "retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4"}, +] + +[package.dependencies] +decorator = ">=3.4.2" +py = ">=1.4.26,<2.0.0" + [[package]] name = "rich" version = "13.7.1" @@ -9158,6 +9337,36 @@ dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodest doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] test = ["Cython", "array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +[[package]] +name = "selenium" +version = "4.22.0" +description = "Official Python bindings for Selenium WebDriver" +optional = false +python-versions = ">=3.8" +files = [ + {file = "selenium-4.22.0-py3-none-any.whl", hash = "sha256:e424991196e9857e19bf04fe5c1c0a4aac076794ff5e74615b1124e729d93104"}, + {file = "selenium-4.22.0.tar.gz", hash = "sha256:903c8c9d61b3eea6fcc9809dc7d9377e04e2ac87709876542cc8f863e482c4ce"}, +] + +[package.dependencies] +certifi = ">=2021.10.8" +trio = ">=0.17,<1.0" +trio-websocket = ">=0.9,<1.0" +typing_extensions = ">=4.9.0" +urllib3 = {version = ">=1.26,<3", extras = ["socks"]} +websocket-client = ">=1.8.0" + +[[package]] +name = "semver" +version = "3.0.2" +description = "Python helper for Semantic Versioning (https://semver.org)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4"}, + {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, +] + [[package]] name = "sentence-transformers" version = "2.7.0" @@ -9345,6 +9554,17 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + [[package]] name = "soupsieve" version = "2.5" @@ -10028,6 +10248,42 @@ torchhub = ["filelock", "huggingface-hub (>=0.19.3,<1.0)", "importlib-metadata", video = ["av (==9.2.0)", "decord (==0.6.0)"] vision = ["Pillow (>=10.0.1,<=15.0)"] +[[package]] +name = "trio" +version = "0.26.0" +description = "A friendly Python library for async concurrency and I/O" +optional = false +python-versions = ">=3.8" +files = [ + {file = "trio-0.26.0-py3-none-any.whl", hash = "sha256:bb9c1b259591af941fccfbabbdc65bc7ed764bd2db76428454c894cd5e3d2032"}, + {file = "trio-0.26.0.tar.gz", hash = "sha256:67c5ec3265dd4abc7b1d1ab9ca4fe4c25b896f9c93dac73713778adab487f9c4"}, +] + +[package.dependencies] +attrs = ">=23.2.0" +cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +idna = "*" +outcome = "*" +sniffio = ">=1.3.0" +sortedcontainers = "*" + +[[package]] +name = "trio-websocket" +version = "0.11.1" +description = "WebSocket library for Trio" +optional = false +python-versions = ">=3.7" +files = [ + {file = "trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f"}, + {file = "trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"}, +] + +[package.dependencies] +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +trio = ">=0.11" +wsproto = ">=0.14" + [[package]] name = "triton" version = "2.3.1" @@ -10574,6 +10830,9 @@ files = [ {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] +[package.dependencies] +pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} + [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] @@ -11057,6 +11316,20 @@ files = [ {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] +[[package]] +name = "wsproto" +version = "1.2.0" +description = "WebSockets state-machine based protocol implementation" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] + +[package.dependencies] +h11 = ">=0.9.0,<1" + [[package]] name = "xlsxwriter" version = "3.2.0" @@ -11412,4 +11685,4 @@ local = ["ctransformers", "llama-cpp-python", "sentence-transformers"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "52ad29fb36dd6a302435cf69fe58d557c9fec119a07ef575ca9b5be5e1e66ee6" +content-hash = "3561ffbe8cc249b7ca946135d550991879ce9767e607f4a474f80119d2ffd5b2" diff --git a/pyproject.toml b/pyproject.toml index 65fc6b38e..dfe0b9af8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,13 +94,11 @@ langchain-aws = "^0.1.6" langchain-mongodb = "^0.1.6" kubernetes = "^30.1.0" firecrawl-py = "^0.0.16" - json-repair = "^0.25.2" - langchain-nvidia-ai-endpoints = "^0.1.2" langchain-google-calendar-tools = "^0.0.1" langchain-milvus = "^0.1.1" - +crewai = {extras = ["tools"], version = "^0.36.0"} [tool.poetry.group.dev.dependencies] diff --git a/src/backend/base/langflow/api/v1/endpoints.py b/src/backend/base/langflow/api/v1/endpoints.py index 48fb5a06e..84f047d77 100644 --- a/src/backend/base/langflow/api/v1/endpoints.py +++ b/src/backend/base/langflow/api/v1/endpoints.py @@ -598,8 +598,9 @@ async def custom_component_update( ] params = update_params_with_load_from_db_fields(cc_instance, params, load_from_db_fields) cc_instance.set_attributes(params) - updated_build_config = cc_instance.update_build_config( - build_config=code_request.get_template(), + updated_build_config = code_request.get_template() + cc_instance.update_build_config( + build_config=updated_build_config, field_value=code_request.field_value, field_name=code_request.field, ) diff --git a/src/backend/base/langflow/base/agents/crewai/__init__.py b/src/backend/base/langflow/base/agents/crewai/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/base/langflow/base/agents/crewai/crew.py b/src/backend/base/langflow/base/agents/crewai/crew.py new file mode 100644 index 000000000..a4c70a51c --- /dev/null +++ b/src/backend/base/langflow/base/agents/crewai/crew.py @@ -0,0 +1,80 @@ +from typing import Callable, List, Tuple, Union, cast + +from crewai import Agent, Crew, Process, Task # type: ignore +from crewai.task import TaskOutput # type: ignore +from langchain_core.agents import AgentAction, AgentFinish + +from langflow.custom import Component +from langflow.inputs.inputs import HandleInput, InputTypes +from langflow.io import BoolInput, IntInput, Output +from langflow.schema.data import Data +from langflow.schema.message import Message + + +class BaseCrewComponent(Component): + description: str = ( + "Represents a group of agents, defining how they should collaborate and the tasks they should perform." + ) + icon = "CrewAI" + + _base_inputs: list[InputTypes] = [ + IntInput(name="verbose", display_name="Verbose", value=0, advanced=True), + BoolInput(name="memory", display_name="Memory", value=False, advanced=True), + BoolInput(name="use_cache", display_name="Cache", value=True, advanced=True), + IntInput(name="max_rpm", display_name="Max RPM", value=100, advanced=True), + BoolInput(name="share_crew", display_name="Share Crew", value=False, advanced=True), + HandleInput( + name="function_calling_llm", + display_name="Function Calling LLM", + input_types=["LanguageModel"], + required=False, + advanced=True, + ), + ] + + outputs = [ + Output(display_name="Output", name="output", method="build_output"), + ] + + def task_is_valid(self, task_data: Data, crew_type: Process) -> Task: + return "task_type" in task_data and task_data.task_type == crew_type + + def get_tasks_and_agents(self) -> tuple[list[Task], list[Agent]]: + return self.tasks, self.agents + + def build_crew(self) -> Crew: + raise NotImplementedError("build_crew must be implemented in subclasses") + + def get_task_callback( + self, + ) -> Callable: + def task_callback(task_output: TaskOutput): + if self.vertex: + vertex_id = self.vertex.id + else: + vertex_id = self.display_name or self.__class__.__name__ + self.log(task_output.model_dump(), name=f"Task (Agent: {task_output.agent}) - {vertex_id}") + + return task_callback + + def get_step_callback( + self, + ) -> Callable: + def step_callback(agent_output: Union[AgentFinish, List[Tuple[AgentAction, str]]]): + _id = self.vertex.id if self.vertex else self.display_name + if isinstance(agent_output, AgentFinish): + messages = agent_output.messages + self.log(cast(dict, messages[0].to_json()), name=f"Finish (Agent: {_id})") + elif isinstance(agent_output, list): + _messages_dict = {f"Action {i}": action.messages for i, (action, _) in enumerate(agent_output)} + messages_dict = {k: v[0] if len(v) == 1 else v for k, v in _messages_dict.items()} + self.log(messages_dict, name=f"Step (Agent: {_id})") + + return step_callback + + async def build_output(self) -> Message: + crew = self.build_crew() + result = await crew.kickoff_async() + message = Message(text=result, sender="Machine") + self.status = "\n\n".join([result] + [str(message) for message in self._logs]) + return message diff --git a/src/backend/base/langflow/base/agents/crewai/tasks.py b/src/backend/base/langflow/base/agents/crewai/tasks.py new file mode 100644 index 000000000..8e6f6cb39 --- /dev/null +++ b/src/backend/base/langflow/base/agents/crewai/tasks.py @@ -0,0 +1,9 @@ +from crewai import Task # type: ignore + + +class SequentialTask(Task): + pass + + +class HierarchicalTask(Task): + pass diff --git a/src/backend/base/langflow/components/agents/CrewAIAgent.py b/src/backend/base/langflow/components/agents/CrewAIAgent.py new file mode 100644 index 000000000..5510a5823 --- /dev/null +++ b/src/backend/base/langflow/components/agents/CrewAIAgent.py @@ -0,0 +1,85 @@ +from crewai import Agent # type: ignore + +from langflow.custom import Component +from langflow.io import BoolInput, DictInput, HandleInput, MessageTextInput, Output + + +class CrewAIAgentComponent(Component): + display_name = "CrewAI Agent" + description = "Represents an agent of CrewAI." + documentation: str = "https://docs.crewai.com/how-to/LLM-Connections/" + icon = "CrewAI" + + inputs = [ + MessageTextInput(name="role", display_name="Role", info="The role of the agent."), + MessageTextInput(name="goal", display_name="Goal", info="The objective of the agent."), + MessageTextInput(name="backstory", display_name="Backstory", info="The backstory of the agent."), + HandleInput( + name="tools", + display_name="Tools", + input_types=["Tool"], + is_list=True, + info="Tools at agents disposal", + value=[], + ), + HandleInput( + name="llm", + display_name="Language Model", + info="Language model that will run the agent.", + input_types=["LanguageModel"], + ), + BoolInput( + name="memory", + display_name="Memory", + info="Whether the agent should have memory or not", + advanced=True, + value=True, + ), + BoolInput( + name="verbose", + display_name="Verbose", + advanced=True, + value=False, + ), + BoolInput( + name="allow_delegation", + display_name="Allow Delegation", + info="Whether the agent is allowed to delegate tasks to other agents.", + value=True, + ), + BoolInput( + name="allow_code_execution", + display_name="Allow Code Execution", + info="Whether the agent is allowed to execute code.", + value=False, + advanced=True, + ), + DictInput( + name="kwargs", + display_name="kwargs", + info="kwargs of agent.", + is_list=True, + advanced=True, + ), + ] + + outputs = [ + Output(display_name="Agent", name="output", method="build_output"), + ] + + def build_output(self) -> Agent: + kwargs = self.kwargs if self.kwargs else {} + agent = Agent( + role=self.role, + goal=self.goal, + backstory=self.backstory, + llm=self.llm, + verbose=self.verbose, + memory=self.memory, + tools=self.tools if self.tools else [], + allow_delegation=self.allow_delegation, + allow_code_execution=self.allow_code_execution, + **kwargs, + ) + self.status = repr(agent) + return agent diff --git a/src/backend/base/langflow/components/agents/HierarchicalCrew.py b/src/backend/base/langflow/components/agents/HierarchicalCrew.py new file mode 100644 index 000000000..b5160b2e5 --- /dev/null +++ b/src/backend/base/langflow/components/agents/HierarchicalCrew.py @@ -0,0 +1,37 @@ +from crewai import Crew, Process # type: ignore + +from langflow.base.agents.crewai.crew import BaseCrewComponent +from langflow.io import HandleInput + + +class HierarchicalCrewComponent(BaseCrewComponent): + display_name: str = "Hierarchical Crew" + description: str = ( + "Represents a group of agents, defining how they should collaborate and the tasks they should perform." + ) + + inputs = BaseCrewComponent._base_inputs + [ + HandleInput(name="agents", display_name="Agents", input_types=["Agent"], is_list=True), + HandleInput(name="tasks", display_name="Tasks", input_types=["HierarchicalTask"], is_list=True), + HandleInput(name="manager_llm", display_name="Manager LLM", input_types=["LanguageModel"], required=False), + HandleInput(name="manager_agent", display_name="Manager Agent", input_types=["Agent"], required=False), + ] + + def build_crew(self) -> Crew: + tasks, agents = self.get_tasks_and_agents() + crew = Crew( + agents=agents, + tasks=tasks, + process=Process.hierarchical, + verbose=self.verbose, + memory=self.memory, + cache=self.use_cache, + max_rpm=self.max_rpm, + share_crew=self.share_crew, + function_calling_llm=self.function_calling_llm, + manager_agent=self.manager_agent, + manager_llm=self.manager_llm, + step_callback=self.get_step_callback(), + task_callback=self.get_task_callback(), + ) + return crew diff --git a/src/backend/base/langflow/components/agents/SequentialCrew.py b/src/backend/base/langflow/components/agents/SequentialCrew.py new file mode 100644 index 000000000..600d66883 --- /dev/null +++ b/src/backend/base/langflow/components/agents/SequentialCrew.py @@ -0,0 +1,38 @@ +from crewai import Agent, Crew, Process, Task # type: ignore + +from langflow.base.agents.crewai.crew import BaseCrewComponent +from langflow.io import HandleInput +from langflow.schema.message import Message + + +class SequentialCrewComponent(BaseCrewComponent): + display_name: str = "Sequential Crew" + description: str = ( + "Represents a group of agents, defining how they should collaborate and the tasks they should perform." + ) + documentation: str = "https://docs.crewai.com/how-to/LLM-Connections/" + icon = "CrewAI" + + inputs = BaseCrewComponent._base_inputs + [ + HandleInput(name="tasks", display_name="Tasks", input_types=["SequentialTask"], is_list=True), + ] + + def get_tasks_and_agents(self) -> tuple[list[Task], list[Agent]]: + return self.tasks, [task.agent for task in self.tasks] + + def build_crew(self) -> Message: + tasks, agents = self.get_tasks_and_agents() + crew = Crew( + agents=agents, + tasks=tasks, + process=Process.sequential, + verbose=self.verbose, + memory=self.memory, + cache=self.use_cache, + max_rpm=self.max_rpm, + share_crew=self.share_crew, + function_calling_llm=self.function_calling_llm, + step_callback=self.get_step_callback(), + task_callback=self.get_task_callback(), + ) + return crew diff --git a/src/backend/base/langflow/components/helpers/HierarchicalTask.py b/src/backend/base/langflow/components/helpers/HierarchicalTask.py new file mode 100644 index 000000000..993126d36 --- /dev/null +++ b/src/backend/base/langflow/components/helpers/HierarchicalTask.py @@ -0,0 +1,43 @@ +from langflow.base.agents.crewai.tasks import HierarchicalTask +from langflow.custom import Component +from langflow.io import HandleInput, MessageTextInput, Output + + +class HierarchicalTaskComponent(Component): + display_name: str = "Hierarchical Task" + description: str = "Each task must have a description, an expected output and an agent responsible for execution." + icon = "CrewAI" + inputs = [ + MessageTextInput( + name="task_description", + display_name="Description", + info="Descriptive text detailing task's purpose and execution.", + ), + MessageTextInput( + name="expected_output", + display_name="Expected Output", + info="Clear definition of expected task outcome.", + ), + HandleInput( + name="tools", + display_name="Tools", + input_types=["Tool"], + is_list=True, + info="List of tools/resources limited for task execution. Uses the Agent tools by default.", + required=False, + advanced=True, + ), + ] + + outputs = [ + Output(display_name="Task", name="task_output", method="build_task"), + ] + + def build_task(self) -> HierarchicalTask: + task = HierarchicalTask( + description=self.task_description, + expected_output=self.expected_output, + tools=self.tools or [], + ) + self.status = task + return task diff --git a/src/backend/base/langflow/components/helpers/SequentialTask.py b/src/backend/base/langflow/components/helpers/SequentialTask.py new file mode 100644 index 000000000..5cf3f54e9 --- /dev/null +++ b/src/backend/base/langflow/components/helpers/SequentialTask.py @@ -0,0 +1,72 @@ +from langflow.base.agents.crewai.tasks import SequentialTask +from langflow.custom import Component +from langflow.io import BoolInput, HandleInput, MessageTextInput, Output + + +class SequentialTaskComponent(Component): + display_name: str = "Sequential Task" + description: str = "Each task must have a description, an expected output and an agent responsible for execution." + icon = "CrewAI" + inputs = [ + MessageTextInput( + name="task_description", + display_name="Description", + info="Descriptive text detailing task's purpose and execution.", + ), + MessageTextInput( + name="expected_output", + display_name="Expected Output", + info="Clear definition of expected task outcome.", + ), + HandleInput( + name="tools", + display_name="Tools", + input_types=["Tool"], + is_list=True, + info="List of tools/resources limited for task execution. Uses the Agent tools by default.", + required=False, + advanced=True, + ), + HandleInput( + name="agent", + display_name="Agent", + input_types=["Agent"], + info="CrewAI Agent that will perform the task", + required=True, + ), + HandleInput( + name="task", + display_name="Task", + input_types=["SequentialTask"], + info="CrewAI Task that will perform the task", + ), + BoolInput( + name="async_execution", + display_name="Async Execution", + value=True, + advanced=True, + info="Boolean flag indicating asynchronous task execution.", + ), + ] + + outputs = [ + Output(display_name="Task", name="task_output", method="build_task"), + ] + + def build_task(self) -> list[SequentialTask]: + tasks = [] + task = SequentialTask( + description=self.task_description, + expected_output=self.expected_output, + tools=self.agent.tools, + async_execution=False, + agent=self.agent, + ) + tasks.append(task) + self.status = task + if self.task: + if isinstance(self.task, list): + tasks.extend(self.task) + else: + tasks.append(self.task) + return tasks diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index aeddc6093..254c9fe7e 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -187,7 +187,7 @@ class Component(CustomComponent): if raw is None and isinstance(result, (dict, Data, str)): raw = result.data if isinstance(result, Data) else result artifact_type = get_artifact_type(artifact_value, result) - raw = post_process_raw(raw, artifact_type) + raw, artifact_type = post_process_raw(raw, artifact_type) artifact = {"repr": custom_repr, "raw": raw, "type": artifact_type} _artifacts[output.name] = artifact self._artifacts = _artifacts diff --git a/src/backend/base/langflow/custom/custom_component/custom_component.py b/src/backend/base/langflow/custom/custom_component/custom_component.py index a3b4c57c7..2527cbea0 100644 --- a/src/backend/base/langflow/custom/custom_component/custom_component.py +++ b/src/backend/base/langflow/custom/custom_component/custom_component.py @@ -192,11 +192,11 @@ class CustomComponent(BaseComponent): if self.repr_value == "": self.repr_value = self.status if isinstance(self.repr_value, dict): - self.repr_value = yaml.dump(self.repr_value) + return yaml.dump(self.repr_value) + if isinstance(self.repr_value, str): + return self.repr_value if isinstance(self.repr_value, BaseModel) and not isinstance(self.repr_value, Data): - self.repr_value = str(self.repr_value) - elif hasattr(self.repr_value, "to_json") and not isinstance(self.repr_value, Data): - self.repr_value = self.repr_value.to_json() + return str(self.repr_value) return self.repr_value def build_config(self): diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index 411c98190..09451bf3d 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -1,9 +1,10 @@ from enum import Enum from typing import Annotated, Any, Optional +from pydantic import BaseModel, ConfigDict, Field, PlainSerializer, field_validator, model_serializer + from langflow.field_typing.range_spec import RangeSpec from langflow.inputs.validators import CoalesceBool -from pydantic import BaseModel, ConfigDict, Field, PlainSerializer, field_validator, model_serializer class FieldTypes(str, Enum): diff --git a/src/backend/base/langflow/schema/artifact.py b/src/backend/base/langflow/schema/artifact.py index dce70f790..a238838ff 100644 --- a/src/backend/base/langflow/schema/artifact.py +++ b/src/backend/base/langflow/schema/artifact.py @@ -1,6 +1,9 @@ from enum import Enum from typing import Generator +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel + from langflow.schema import Data from langflow.schema.message import Message @@ -50,6 +53,12 @@ def post_process_raw(raw, artifact_type: str): if artifact_type == ArtifactType.STREAM.value: raw = "" elif artifact_type == ArtifactType.UNKNOWN.value and raw is not None: - raw = "Built Successfully ✨" - - return raw + if isinstance(raw, (BaseModel, dict)): + try: + raw = jsonable_encoder(raw) + artifact_type = ArtifactType.OBJECT.value + except Exception: + raw = "Built Successfully ✨" + else: + raw = "Built Successfully ✨" + return raw, artifact_type diff --git a/src/backend/base/langflow/services/storage/local.py b/src/backend/base/langflow/services/storage/local.py index 9ad9feafb..b4eb81e22 100644 --- a/src/backend/base/langflow/services/storage/local.py +++ b/src/backend/base/langflow/services/storage/local.py @@ -56,7 +56,7 @@ class LocalStorageService(StorageService): raise FileNotFoundError(f"File {file_name} not found in flow {flow_id}") with open(file_path, "rb") as f: - logger.info(f"File {file_name} retrieved successfully from flow {flow_id}.") + logger.debug(f"File {file_name} retrieved successfully from flow {flow_id}.") return f.read() async def list_files(self, flow_id: str): diff --git a/src/frontend/src/icons/CrewAI/CrewAiIcon.jsx b/src/frontend/src/icons/CrewAI/CrewAiIcon.jsx new file mode 100644 index 000000000..d2a02f8f3 --- /dev/null +++ b/src/frontend/src/icons/CrewAI/CrewAiIcon.jsx @@ -0,0 +1,58 @@ +const SvgCrewAiIcon = (props) => ( + + + + + + + + + + + +); +export default SvgCrewAiIcon; diff --git a/src/frontend/src/icons/CrewAI/crewai.svg b/src/frontend/src/icons/CrewAI/crewai.svg new file mode 100644 index 000000000..3b3192c3c --- /dev/null +++ b/src/frontend/src/icons/CrewAI/crewai.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/frontend/src/icons/CrewAI/index.tsx b/src/frontend/src/icons/CrewAI/index.tsx new file mode 100644 index 000000000..b3afaf3fc --- /dev/null +++ b/src/frontend/src/icons/CrewAI/index.tsx @@ -0,0 +1,9 @@ +import React, { forwardRef } from "react"; +import SvgCrewAiIcon from "./CrewAiIcon"; + +export const CrewAiIcon = forwardRef< + SVGSVGElement, + React.PropsWithChildren<{}> +>((props, ref) => { + return ; +}); diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 198f9eeb0..5990cc4f1 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -168,6 +168,7 @@ import { CassandraIcon } from "../icons/Cassandra"; import { ChromaIcon } from "../icons/ChromaIcon"; import { CohereIcon } from "../icons/Cohere"; import { CouchbaseIcon } from "../icons/Couchbase"; +import { CrewAiIcon } from "../icons/CrewAI"; import { ElasticsearchIcon } from "../icons/ElasticsearchStore"; import { EvernoteIcon } from "../icons/Evernote"; import { FBIcon } from "../icons/FacebookMessenger"; @@ -381,6 +382,7 @@ export const nodeIconsLucide: iconsType = { HuggingFace: HuggingFaceIcon, HuggingFaceEmbeddings: HuggingFaceIcon, IFixitLoader: IFixIcon, + CrewAI: CrewAiIcon, Meta: MetaIcon, Midjorney: MidjourneyIcon, MongoDBAtlasVectorSearch: MongoDBIcon,