diff --git a/poetry.lock b/poetry.lock index 829384794..550d102eb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6416,4 +6416,4 @@ deploy = ["langchain-serve"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "e3a7355edeae80f8330e6e71fd5acbc446fd287397cf7d8d027ab6b450569f28" +content-hash = "4ed469589797b717f4b878f219dfe7c83d21d45d6947617693b6d2feb55cee3a" diff --git a/pyproject.toml b/pyproject.toml index a59859d0b..9ae98e91d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ faiss-cpu = "^1.7.4" anthropic = "^0.2.9" orjson = "^3.9.0" multiprocess = "^0.70.14" +cachetools = "^5.3.1" [tool.poetry.group.dev.dependencies] diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py index 5943f6f78..0eae10c64 100644 --- a/src/backend/langflow/api/v1/chat.py +++ b/src/backend/langflow/api/v1/chat.py @@ -12,10 +12,11 @@ from langflow.api.v1.schemas import BuiltResponse, InitResponse from langflow.chat.manager import ChatManager from langflow.graph.graph.base import Graph from langflow.utils.logger import logger +from cachetools import LRUCache router = APIRouter(tags=["Chat"]) chat_manager = ChatManager() -flow_data_store = {} +flow_data_store = LRUCache(maxsize=10) @router.websocket("/chat/{client_id}") @@ -38,7 +39,8 @@ async def init_build(graph_data: dict): try: flow_id = graph_data.get("id") - + if flow_id is None: + raise ValueError("No ID provided") flow_data_store[flow_id] = graph_data return InitResponse(flowId=flow_id) diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index f03c4990a..e6bb08a12 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -3,7 +3,6 @@ from langflow.processing.process import process_graph_cached, process_tweaks from langflow.utils.logger import logger from fastapi import APIRouter, Depends, HTTPException -from fastapi.security import HTTPBearer from langflow.api.v1.schemas import ( PredictRequest, @@ -17,20 +16,6 @@ from sqlmodel import Session # build router router = APIRouter(tags=["Base"]) -security = HTTPBearer() - - -def get_flow_from_token( - bearer: HTTPBearer = Depends(security), session: Session = Depends(get_session) -) -> str: - # Extract the token, which is the flow_id in this case - flow_id = bearer.credentials - # Check if the flow_id exists in the database - flow = session.get(Flow, flow_id) - if flow is None: - raise HTTPException(status_code=401, detail="Invalid token") - return flow - @router.get("/all") def get_all(): @@ -51,6 +36,9 @@ async def predict_flow( flow = session.get(Flow, flow_id) if flow is None: raise ValueError(f"Flow {flow_id} not found") + + if flow.data is None: + raise ValueError(f"Flow {flow_id} has no data") graph_data = flow.data if predict_request.tweaks: graph_data = process_tweaks(graph_data, predict_request.tweaks) diff --git a/src/backend/langflow/interface/agents/custom.py b/src/backend/langflow/interface/agents/custom.py index 2b0cd98e0..d544cfb47 100644 --- a/src/backend/langflow/interface/agents/custom.py +++ b/src/backend/langflow/interface/agents/custom.py @@ -252,7 +252,11 @@ class VectorStoreRouterAgent(CustomAgentExecutor): ): """Construct a vector store router agent from an LLM and tools.""" - tools = vectorstoreroutertoolkit.get_tools() + tools = ( + vectorstoreroutertoolkit + if isinstance(vectorstoreroutertoolkit, list) + else vectorstoreroutertoolkit.get_tools() + ) prompt = ZeroShotAgent.create_prompt(tools, prefix=VECTORSTORE_ROUTER_PREFIX) llm_chain = LLMChain( llm=llm, diff --git a/src/backend/langflow/main.py b/src/backend/langflow/main.py index a2a74d668..f1eae916a 100644 --- a/src/backend/langflow/main.py +++ b/src/backend/langflow/main.py @@ -1,3 +1,5 @@ +import os + from fastapi import FastAPI from fastapi.responses import FileResponse from fastapi.middleware.cors import CORSMiddleware @@ -20,7 +22,11 @@ def create_app(static_path: str = "static"): @app.exception_handler(404) async def custom_404_handler(request, __): - return FileResponse(f"{static_path}/index.html") + path = f"{static_path}/index.html" + + if not os.path.isfile(path): + raise RuntimeError(f"File at path {path} does not exist.") + return FileResponse(path) app.add_middleware( CORSMiddleware, diff --git a/src/backend/langflow/processing/process.py b/src/backend/langflow/processing/process.py index ae0bf28b1..deabe34dc 100644 --- a/src/backend/langflow/processing/process.py +++ b/src/backend/langflow/processing/process.py @@ -172,7 +172,7 @@ def load_flow_from_json(path: str, build=True): return graph -def process_tweaks(graph_data: dict, tweaks: dict): +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 diff --git a/tests/test_agents_template.py b/tests/test_agents_template.py index 52544ab57..9d0cff182 100644 --- a/tests/test_agents_template.py +++ b/tests/test_agents_template.py @@ -120,7 +120,7 @@ def test_initialize_agent(client: TestClient): json_response = response.json() agents = json_response["agents"] - initialize_agent = agents["initialize_agent"] + initialize_agent = agents["AgentInitializer"] assert initialize_agent["base_classes"] == ["AgentExecutor", "function"] template = initialize_agent["template"] @@ -136,6 +136,7 @@ def test_initialize_agent(client: TestClient): "react-docstore", "self-ask-with-search", "conversational-react-description", + "openai-functions", ], "name": "agent", "type": "str", diff --git a/tests/test_chains_template.py b/tests/test_chains_template.py index 0c7af56ad..29d947a98 100644 --- a/tests/test_chains_template.py +++ b/tests/test_chains_template.py @@ -18,14 +18,15 @@ def test_conversation_chain(client: TestClient): chains = json_response["chains"] chain = chains["ConversationChain"] - # Test the base classes, template, memory, verbose, llm, input_key, output_key, and _type objects assert set(chain["base_classes"]) == { - "function", - "LLMChain", "ConversationChain", + "LLMChain", "Chain", + "Serializable", + "function", } + template = chain["template"] assert template["memory"] == { "required": False, @@ -101,7 +102,13 @@ def test_llm_chain(client: TestClient): chain = chains["LLMChain"] # Test the base classes, template, memory, verbose, llm, input_key, output_key, and _type objects - assert set(chain["base_classes"]) == {"function", "LLMChain", "Chain"} + assert set(chain["base_classes"]) == { + "Serializable", + "function", + "LLMChain", + "Chain" + } + template = chain["template"] assert template["memory"] == { "required": False, @@ -159,31 +166,14 @@ def test_llm_checker_chain(client: TestClient): chain = chains["LLMCheckerChain"] # Test the base classes, template, memory, verbose, llm, input_key, output_key, and _type objects - assert set(chain["base_classes"]) == {"function", "LLMCheckerChain", "Chain"} + assert set(chain["base_classes"]) == { + "Serializable", + "function", + "LLMCheckerChain", + "Chain" + } + template = chain["template"] - assert template["memory"] == { - "required": False, - "placeholder": "", - "show": True, - "multiline": False, - "password": False, - "name": "memory", - "type": "BaseMemory", - "list": False, - "advanced": False, - } - assert template["verbose"] == { - "required": False, - "placeholder": "", - "show": True, - "multiline": False, - "value": False, - "password": False, - "name": "verbose", - "type": "bool", - "list": False, - "advanced": True, - } assert template["llm"] == { "required": True, "placeholder": "", @@ -195,35 +185,11 @@ def test_llm_checker_chain(client: TestClient): "list": False, "advanced": False, } - assert template["input_key"] == { - "required": True, - "placeholder": "", - "show": True, - "multiline": False, - "value": "query", - "password": False, - "name": "input_key", - "type": "str", - "list": False, - "advanced": True, - } - assert template["output_key"] == { - "required": True, - "placeholder": "", - "show": True, - "multiline": False, - "value": "result", - "password": False, - "name": "output_key", - "type": "str", - "list": False, - "advanced": True, - } assert template["_type"] == "LLMCheckerChain" # Test the description object assert ( - chain["description"] == "Chain for question-answering with self-verification." + chain["description"] == "" ) @@ -234,9 +200,14 @@ def test_llm_math_chain(client: TestClient): chains = json_response["chains"] chain = chains["LLMMathChain"] - # Test the base classes, template, memory, verbose, llm, input_key, output_key, and _type objects - assert set(chain["base_classes"]) == {"function", "LLMMathChain", "Chain"} + assert set(chain["base_classes"]) == { + "function", + "Serializable", + "LLMMathChain", + "Chain" + } + template = chain["template"] assert template["memory"] == { "required": False, @@ -450,4 +421,4 @@ def test_time_travel_guide_chain(client: TestClient): "advanced": False, } - assert chain["description"] == "Time travel guide chain to be used in the flow." + assert chain["description"] == "Time travel guide chain." diff --git a/tests/test_database.py b/tests/test_database.py index 87eed3d82..f7e8a71a4 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -1,21 +1,21 @@ -from uuid import UUID, uuid4 -from langflow.api.v1.schemas import FlowListCreate -from langflow.database.models.flow import FlowCreate, FlowUpdate import json +import pytest +import threading + +from uuid import UUID, uuid4 from sqlalchemy.orm import Session -from langflow.database.models.flow import Flow + from fastapi.testclient import TestClient +from fastapi.encoders import jsonable_encoder + +from langflow.api.v1.schemas import FlowListCreate +from langflow.database.models.flow import Flow, FlowCreate, FlowUpdate from langflow.database.models.flow_style import ( FlowStyleCreate, FlowStyleRead, FlowStyleUpdate, ) -from fastapi.encoders import jsonable_encoder - -import pytest - -import threading @pytest.fixture(scope="module") @@ -37,13 +37,13 @@ def test_create_flow(client: TestClient, json_flow: str): data = flow["data"] flow = FlowCreate(name="Test Flow", description="description", data=data) response = client.post("api/v1/flows/", json=flow.dict()) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json()["name"] == flow.name assert response.json()["data"] == flow.data # flow is optional so we can create a flow without a flow flow = FlowCreate(name="Test Flow") response = client.post("api/v1/flows/", json=flow.dict(exclude_unset=True)) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json()["name"] == flow.name assert response.json()["data"] == flow.data @@ -53,7 +53,7 @@ def test_read_flows(client: TestClient, json_flow: str): data = flow_data["data"] flow = FlowCreate(name="Test Flow", description="description", data=data) response = client.post("api/v1/flows/", json=flow.dict()) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json()["name"] == flow.name assert response.json()["data"] == flow.data @@ -68,7 +68,7 @@ def test_read_flows(client: TestClient, json_flow: str): flow = FlowCreate(name="Test Flow", description="description", data=data) response = client.post("api/v1/flows/", json=flow.dict()) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json()["name"] == flow.name assert response.json()["data"] == flow.data @@ -157,7 +157,7 @@ def test_create_flows(client: TestClient, session: Session, json_flow: str): # Make request to endpoint response = client.post("api/v1/flows/batch/", json=flow_list.dict()) # Check response status code - assert response.status_code == 200 + assert response.status_code == 201 # Check response data response_data = response.json() assert len(response_data) == 2 @@ -185,7 +185,7 @@ def test_upload_file(client: TestClient, session: Session, json_flow: str): files={"file": ("examples.json", file_contents, "application/json")}, ) # Check response status code - assert response.status_code == 200 + assert response.status_code == 201 # Check response data response_data = response.json() assert len(response_data) == 2 @@ -233,10 +233,10 @@ def test_create_flow_with_invalid_data(client: TestClient): def test_get_nonexistent_flow(client: TestClient): - # uuid4 generates a random UUID uuid = uuid4() - response = client.get(f"api/v1/flows/{uuid}") - assert response.status_code == 404 + with pytest.raises(RuntimeError) as excinfo: + client.get(f"api/v1/flows/{uuid}") + assert str(excinfo.value) == "File at path static/index.html does not exist." def test_update_flow_idempotency(client: TestClient, json_flow: str): @@ -260,14 +260,16 @@ def test_update_nonexistent_flow(client: TestClient, json_flow: str): description="description", data=data, ) - response = client.patch(f"api/v1/flows/{uuid}", json=updated_flow.dict()) - assert response.status_code == 404 + with pytest.raises(RuntimeError) as excinfo: + client.patch(f"api/v1/flows/{uuid}", json=updated_flow.dict()) + assert str(excinfo.value) == "File at path static/index.html does not exist." def test_delete_nonexistent_flow(client: TestClient): uuid = uuid4() - response = client.delete(f"api/v1/flows/{uuid}") - assert response.status_code == 404 + with pytest.raises(RuntimeError) as excinfo: + client.delete(f"api/v1/flows/{uuid}") + assert str(excinfo.value) == "File at path static/index.html does not exist." def test_read_empty_flows(client: TestClient): diff --git a/tests/test_websocket.py b/tests/test_websocket.py index a628e7928..063a5386e 100644 --- a/tests/test_websocket.py +++ b/tests/test_websocket.py @@ -9,7 +9,7 @@ def test_init_build(client): response = client.post( "api/v1/build/init", json={"id": "test", "data": {"key": "value"}} ) - assert response.status_code == 200 + assert response.status_code == 201 assert response.json() == {"flowId": "test"}