diff --git a/Makefile b/Makefile index 6c1989ee1..bfed91f37 100644 --- a/Makefile +++ b/Makefile @@ -44,13 +44,18 @@ install_backend: backend: make install_backend - poetry run uvicorn langflow.main:app --port 7860 --reload --log-level debug + poetry run uvicorn src.backend.langflow.main:app --port 7860 --reload --log-level debug build_and_run: echo 'Removing dist folder' rm -rf dist make build && poetry run pip install dist/*.tar.gz && poetry run langflow +build_and_install: + echo 'Removing dist folder' + rm -rf dist + make build && poetry run pip install dist/*.tar.gz + build_frontend: cd src/frontend && CI='' npm run build cp -r src/frontend/build src/backend/langflow/frontend diff --git a/README.md b/README.md index bd377896c..8e3930997 100644 --- a/README.md +++ b/README.md @@ -50,11 +50,11 @@ Alternatively, click the **"Open in Cloud Shell"** button below to launch Google Langflow integrates with langchain-serve to provide a one-command deployment to Jina AI Cloud. -Start by installing `langchain-serve` with +Start by installing `langchain-serve` with ```bash pip install -U langchain-serve -``` +``` Then, run: @@ -109,24 +109,38 @@ You can use Langflow directly on your browser, or use the API endpoints on Jina Show API usage (with python) ```python - import json - import requests +import requests - FLOW_PATH = "Time_traveller.json" +BASE_API_URL = "https://langflow-e3dd8820ec.wolf.jina.ai/api/v1/predict" +FLOW_ID = "864c4f98-2e59-468b-8e13-79cd8da07468" +# You can tweak the flow by adding a tweaks dictionary +# e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}} +TWEAKS = { + "ChatOpenAI-g4jEr": {}, + "ConversationChain-UidfJ": {} +} - # HOST = 'http://localhost:7860' - HOST = 'https://langflow-f1ed20e309.wolf.jina.ai' - API_URL = f'{HOST}/predict' +def run_flow(message: str, flow_id: str, tweaks: dict = None) -> dict: + """ + Run a flow with a given message and optional tweaks. - def predict(message): - with open(FLOW_PATH, "r") as f: - json_data = json.load(f) - payload = {'exported_flow': json_data, 'message': message} - response = requests.post(API_URL, json=payload) - return response.json() + :param message: The message to send to the flow + :param flow_id: The ID of the flow to run + :param tweaks: Optional tweaks to customize the flow + :return: The JSON response from the flow + """ + api_url = f"{BASE_API_URL}/{flow_id}" + payload = {"message": message} - predict('Take me to 1920s Bangalore') + if tweaks: + payload["tweaks"] = tweaks + + response = requests.post(api_url, json=payload) + return response.json() + +# Setup any tweaks you want to apply to the flow +print(run_flow("Your message", flow_id=FLOW_ID, tweaks=TWEAKS)) ``` ```json diff --git a/poetry.lock b/poetry.lock index c8843ccba..b5503c034 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3812,14 +3812,14 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest- [[package]] name = "pluggy" -version = "1.1.0" +version = "1.0.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" files = [ - {file = "pluggy-1.1.0-py3-none-any.whl", hash = "sha256:d81d19a3a88d82ed06998353ce5d5c02587ef07ee2d808ae63904ab0ccef0087"}, - {file = "pluggy-1.1.0.tar.gz", hash = "sha256:c500b592c5512df35622e4faf2135aa0b7e989c7d31344194b4afb9d5e47b1bf"}, + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] [package.extras] @@ -5036,14 +5036,14 @@ files = [ [[package]] name = "setuptools" -version = "67.8.0" +version = "68.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.8.0-py3-none-any.whl", hash = "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f"}, - {file = "setuptools-67.8.0.tar.gz", hash = "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102"}, + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, ] [package.extras] diff --git a/pyproject.toml b/pyproject.toml index dd2cdda71..27db6b115 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langflow" -version = "0.1.3" +version = "0.1.4" description = "A Python package with a built-in web application" authors = ["Logspace "] maintainers = [ diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index 5cefdadae..4fa2f4d17 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -11,6 +11,7 @@ from langflow.graph.vertex.types import ( from langflow.interface.tools.constants import FILE_TOOLS from langflow.utils import payload from langflow.utils.logger import logger +from langchain.chains.base import Chain class Graph: @@ -99,7 +100,7 @@ class Graph: ] return connected_nodes - def build(self) -> List[Vertex]: + def build(self) -> Chain: """Builds the graph.""" # Get root node root_node = payload.get_root_node(self) diff --git a/src/backend/langflow/processing/process.py b/src/backend/langflow/processing/process.py index c25f7b3d1..4f5295c60 100644 --- a/src/backend/langflow/processing/process.py +++ b/src/backend/langflow/processing/process.py @@ -1,5 +1,6 @@ import contextlib import io +from pathlib import Path from langchain.schema import AgentAction import json from langflow.interface.run import ( @@ -10,8 +11,7 @@ from langflow.interface.run import ( from langflow.utils.logger import logger from langflow.graph import Graph - -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union def fix_memory_inputs(langchain_object): @@ -20,22 +20,23 @@ def fix_memory_inputs(langchain_object): object's input variables. If so, it does nothing. Otherwise, it gets a possible new memory key using the get_memory_key function and updates the memory keys using the update_memory_keys function. """ - if hasattr(langchain_object, "memory") and langchain_object.memory is not None: - try: - if langchain_object.memory.memory_key in langchain_object.input_variables: - return - except AttributeError: - input_variables = ( - langchain_object.prompt.input_variables - if hasattr(langchain_object, "prompt") - else langchain_object.input_keys - ) - if langchain_object.memory.memory_key in input_variables: - return + if not hasattr(langchain_object, "memory") or langchain_object.memory is None: + return + try: + if langchain_object.memory.memory_key in langchain_object.input_variables: + return + except AttributeError: + input_variables = ( + langchain_object.prompt.input_variables + if hasattr(langchain_object, "prompt") + else langchain_object.input_keys + ) + if langchain_object.memory.memory_key in input_variables: + return - possible_new_mem_key = get_memory_key(langchain_object) - if possible_new_mem_key is not None: - update_memory_keys(langchain_object, possible_new_mem_key) + possible_new_mem_key = get_memory_key(langchain_object) + if possible_new_mem_key is not None: + update_memory_keys(langchain_object, possible_new_mem_key) def format_actions(actions: List[Tuple[AgentAction, str]]) -> str: @@ -131,57 +132,108 @@ def process_graph_cached(data_graph: Dict[str, Any], message: str): return {"result": str(result), "thought": thought.strip()} -def load_flow_from_json(path: str, build=True): - """Load flow from json file""" - # This is done to avoid circular imports +def load_flow_from_json( + input: Union[Path, str, dict], tweaks: Optional[dict] = None, build=True +): + """ + Load flow from a JSON file or a JSON object. - with open(path, "r", encoding="utf-8") as f: - flow_graph = json.load(f) - data_graph = flow_graph["data"] - nodes = data_graph["nodes"] - # Substitute ZeroShotPrompt with PromptTemplate - # nodes = replace_zero_shot_prompt_with_prompt_template(nodes) - # Add input variables - # nodes = payload.extract_input_variables(nodes) + :param input: JSON file path or JSON object + :param tweaks: Optional tweaks to be processed + :param build: If True, build the graph, otherwise return the graph object + :return: Langchain object or Graph object depending on the build parameter + """ + # If input is a file path, load JSON from the file + if isinstance(input, (str, Path)): + with open(input, "r", encoding="utf-8") as f: + flow_graph = json.load(f) + # If input is a dictionary, assume it's a JSON object + elif isinstance(input, dict): + flow_graph = input + else: + raise TypeError( + "Input must be either a file path (str) or a JSON object (dict)" + ) - # Nodes, edges and root node - edges = data_graph["edges"] + graph_data = flow_graph["data"] + if tweaks is not None: + graph_data = process_tweaks(graph_data, tweaks) + nodes = graph_data["nodes"] + edges = graph_data["edges"] graph = Graph(nodes, edges) + if build: langchain_object = graph.build() + if hasattr(langchain_object, "verbose"): langchain_object.verbose = True if hasattr(langchain_object, "return_intermediate_steps"): - # https://github.com/hwchase17/langchain/issues/2068 # Deactivating until we have a frontend solution # to display intermediate steps langchain_object.return_intermediate_steps = False + fix_memory_inputs(langchain_object) return langchain_object + return graph -def process_tweaks(graph_data: Dict, tweaks: Dict): - """This function is used to tweak the graph data using the node id and the tweaks dict""" - # the tweaks dict is a dict of dicts - # the key is the node id and the value is a dict of the tweaks - # the dict of tweaks contains the name of a certain parameter and the value to be tweaked +def validate_input( + graph_data: Dict[str, Any], tweaks: Dict[str, Dict[str, Any]] +) -> List[Dict[str, Any]]: + if not isinstance(graph_data, dict) or not isinstance(tweaks, dict): + raise ValueError("graph_data and tweaks should be dictionaries") + + nodes = graph_data.get("data", {}).get("nodes") or graph_data.get("nodes") + + if not isinstance(nodes, list): + raise ValueError( + "graph_data should contain a list of nodes under 'data' key or directly under 'nodes' key" + ) + + return nodes + + +def apply_tweaks(node: Dict[str, Any], node_tweaks: Dict[str, Any]) -> None: + template_data = node.get("data", {}).get("node", {}).get("template") + + if not isinstance(template_data, dict): + logger.warning( + f"Template data for node {node.get('id')} should be a dictionary" + ) + return + + for tweak_name, tweak_value in node_tweaks.items(): + if tweak_name and tweak_value and tweak_name in template_data: + template_data[tweak_name]["value"] = tweak_value + + +def process_tweaks( + graph_data: Dict[str, Any], tweaks: Dict[str, Dict[str, Any]] +) -> Dict[str, Any]: + """ + This function is used to tweak the graph data using the node id and the tweaks dict. + + :param graph_data: The dictionary containing the graph data. It must contain a 'data' key with + 'nodes' as its child or directly contain 'nodes' key. Each node should have an 'id' and 'data'. + :param tweaks: A dictionary where the key is the node id and the value is a dictionary of the tweaks. + The inner dictionary contains the name of a certain parameter as the key and the value to be tweaked. + + :return: The modified graph_data dictionary. + + :raises ValueError: If the input is not in the expected format. + """ + nodes = validate_input(graph_data, tweaks) - # We need to process the graph data to add the tweaks - if "data" not in graph_data and "nodes" in graph_data: - nodes = graph_data["nodes"] - else: - nodes = graph_data["data"]["nodes"] for node in nodes: - node_id = node["id"] - if node_id in tweaks: - node_tweaks = tweaks[node_id] - template_data = node["data"]["node"]["template"] - for tweak_name, tweake_value in node_tweaks.items(): - if tweak_name in template_data: - template_data[tweak_name]["value"] = tweake_value - print( - f"Something changed in node {node_id} with tweak {tweak_name} and value {tweake_value}" - ) + if isinstance(node, dict) and isinstance(node.get("id"), str): + node_id = node["id"] + if node_tweaks := tweaks.get(node_id): + apply_tweaks(node, node_tweaks) + else: + logger.warning( + "Each node should be a dictionary with an 'id' key of type str" + ) + return graph_data diff --git a/src/frontend/src/components/headerComponent/index.tsx b/src/frontend/src/components/headerComponent/index.tsx index 67ef327f2..0d9459f45 100644 --- a/src/frontend/src/components/headerComponent/index.tsx +++ b/src/frontend/src/components/headerComponent/index.tsx @@ -1,6 +1,6 @@ import { BellIcon, Home, Users2 } from "lucide-react"; -import { useContext } from "react"; -import { FaGithub } from "react-icons/fa"; +import { useContext, useEffect, useState } from "react"; +import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa"; import { Button } from "../ui/button"; import { TabsContext } from "../../contexts/tabsContext"; import AlertDropdown from "../../alerts/alertDropDown"; @@ -11,6 +11,7 @@ import { typesContext } from "../../contexts/typesContext"; import MenuBar from "./components/menuBar"; import { Link, useLocation, useParams } from "react-router-dom"; import { USER_PROJECTS_HEADER } from "../../constants"; +import { getRepoStars } from "../../controllers/API"; export default function Header() { const { flows, addFlow, tabId } = useContext(TabsContext); @@ -22,6 +23,16 @@ export default function Header() { const { notificationCenter, setNotificationCenter, setErrorData } = useContext(alertContext); const location = useLocation(); + + const [stars, setStars] = useState(null); + + useEffect(() => { + async function fetchStars() { + const starsCount = await getRepoStars("logspace-ai", "langflow"); + setStars(starsCount); + } + fetchStars(); + }, []); return (
@@ -57,22 +68,35 @@ export default function Header() {
-
- {/* - {/* */}
diff --git a/src/frontend/src/constants.tsx b/src/frontend/src/constants.tsx index 34d20211a..7017ac27e 100644 --- a/src/frontend/src/constants.tsx +++ b/src/frontend/src/constants.tsx @@ -121,11 +121,12 @@ export const getCurlCode = (flow: FlowType): string => { */ export const getPythonCode = (flow: FlowType): string => { const flowName = flow.name; + const tweaks = buildTweaks(flow); return `from langflow import load_flow_from_json - - flow = load_flow_from_json("${flowName}.json") - # Now you can use it like any chain - flow("Hey, have you heard of LangFlow?")`; +TWEAKS = ${JSON.stringify(tweaks, null, 2)} +flow = load_flow_from_json("${flowName}.json", tweaks=TWEAKS) +# Now you can use it like any chain +flow("Hey, have you heard of LangFlow?")`; }; /** diff --git a/src/frontend/src/controllers/API/index.ts b/src/frontend/src/controllers/API/index.ts index f0c36a215..28a41d578 100644 --- a/src/frontend/src/controllers/API/index.ts +++ b/src/frontend/src/controllers/API/index.ts @@ -18,6 +18,18 @@ export async function getAll(): Promise> { return await axios.get(`/api/v1/all`); } +const GITHUB_API_URL = "https://api.github.com"; + +export async function getRepoStars(owner, repo) { + try { + const response = await axios.get(`${GITHUB_API_URL}/repos/${owner}/${repo}`); + return response.data.stargazers_count; + } catch (error) { + console.error("Error fetching repository data:", error); + return null; + } +} + /** * Sends data to the API for prediction. * diff --git a/tests/test_loading.py b/tests/test_loading.py index 885eb7a82..11fa8e471 100644 --- a/tests/test_loading.py +++ b/tests/test_loading.py @@ -14,6 +14,15 @@ def test_load_flow_from_json(): assert isinstance(loaded, Chain) +def test_load_flow_from_json_with_tweaks(): + """Test loading a flow from a json file and applying tweaks""" + tweaks = {"dndnode_82": {"model_name": "test model"}} + loaded = load_flow_from_json(pytest.BASIC_EXAMPLE_PATH, tweaks=tweaks) + assert loaded is not None + assert isinstance(loaded, Chain) + assert loaded.llm.model_name == "test model" + + def test_get_root_node(): with open(pytest.BASIC_EXAMPLE_PATH, "r") as f: flow_graph = json.load(f)