refactor: move tests folder structure and update pytest commands (#2785)
* refactor: move tests folder to src/backend * chore(Makefile): update pytest commands to run tests from the correct directory paths for unit and integration tests * refactor: update file path in test_custom_component.py The file path in the test_custom_component.py file has been updated to use the correct relative path to the component_multiple_outputs.py file. This change ensures that the test code can access the correct file and improves the reliability of the test.
This commit is contained in:
parent
afdff210b3
commit
0122a50a35
56 changed files with 5 additions and 5 deletions
466
src/backend/tests/conftest.py
Normal file
466
src/backend/tests/conftest.py
Normal file
|
|
@ -0,0 +1,466 @@
|
|||
import json
|
||||
import os.path
|
||||
import shutil
|
||||
|
||||
# we need to import tmpdir
|
||||
import tempfile
|
||||
from contextlib import contextmanager, suppress
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, AsyncGenerator
|
||||
|
||||
import orjson
|
||||
import pytest
|
||||
from dotenv import load_dotenv
|
||||
from fastapi.testclient import TestClient
|
||||
from httpx import AsyncClient
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
from sqlmodel.pool import StaticPool
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.initial_setup.setup import STARTER_FOLDER_NAME
|
||||
from langflow.services.auth.utils import get_password_hash
|
||||
from langflow.services.database.models.api_key.model import ApiKey
|
||||
from langflow.services.database.models.flow.model import Flow, FlowCreate
|
||||
from langflow.services.database.models.folder.model import Folder
|
||||
from langflow.services.database.models.user.model import User, UserCreate
|
||||
from langflow.services.database.utils import session_getter
|
||||
from langflow.services.deps import get_db_service
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.database.service import DatabaseService
|
||||
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line("markers", "noclient: don't create a client for this test")
|
||||
config.addinivalue_line("markers", "load_flows: load the flows for this test")
|
||||
config.addinivalue_line("markers", "api_key_required: run only if the api key is set in the environment variables")
|
||||
data_path = Path(__file__).parent.absolute() / "data"
|
||||
|
||||
pytest.BASIC_EXAMPLE_PATH = data_path / "basic_example.json"
|
||||
pytest.COMPLEX_EXAMPLE_PATH = data_path / "complex_example.json"
|
||||
pytest.OPENAPI_EXAMPLE_PATH = data_path / "Openapi.json"
|
||||
pytest.GROUPED_CHAT_EXAMPLE_PATH = data_path / "grouped_chat.json"
|
||||
pytest.ONE_GROUPED_CHAT_EXAMPLE_PATH = data_path / "one_group_chat.json"
|
||||
pytest.VECTOR_STORE_GROUPED_EXAMPLE_PATH = data_path / "vector_store_grouped.json"
|
||||
pytest.WEBHOOK_TEST = data_path / "WebhookTest.json"
|
||||
|
||||
pytest.BASIC_CHAT_WITH_PROMPT_AND_HISTORY = data_path / "BasicChatwithPromptandHistory.json"
|
||||
pytest.CHAT_INPUT = data_path / "ChatInputTest.json"
|
||||
pytest.TWO_OUTPUTS = data_path / "TwoOutputsTest.json"
|
||||
pytest.VECTOR_STORE_PATH = data_path / "Vector_store.json"
|
||||
pytest.SIMPLE_API_TEST = data_path / "SimpleAPITest.json"
|
||||
pytest.CODE_WITH_SYNTAX_ERROR = """
|
||||
def get_text():
|
||||
retun "Hello World"
|
||||
"""
|
||||
|
||||
# validate that all the paths are correct and the files exist
|
||||
for path in [
|
||||
pytest.BASIC_EXAMPLE_PATH,
|
||||
pytest.COMPLEX_EXAMPLE_PATH,
|
||||
pytest.OPENAPI_EXAMPLE_PATH,
|
||||
pytest.GROUPED_CHAT_EXAMPLE_PATH,
|
||||
pytest.ONE_GROUPED_CHAT_EXAMPLE_PATH,
|
||||
pytest.VECTOR_STORE_GROUPED_EXAMPLE_PATH,
|
||||
pytest.BASIC_CHAT_WITH_PROMPT_AND_HISTORY,
|
||||
pytest.CHAT_INPUT,
|
||||
pytest.TWO_OUTPUTS,
|
||||
pytest.VECTOR_STORE_PATH,
|
||||
]:
|
||||
assert path.exists(), f"File {path} does not exist. Available files: {list(data_path.iterdir())}"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
async def async_client() -> AsyncGenerator:
|
||||
from langflow.main import create_app
|
||||
|
||||
app = create_app()
|
||||
async with AsyncClient(app=app, base_url="http://testserver") as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture(name="session")
|
||||
def session_fixture():
|
||||
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
|
||||
|
||||
class Config:
|
||||
broker_url = "redis://localhost:6379/0"
|
||||
result_backend = "redis://localhost:6379/0"
|
||||
|
||||
|
||||
@pytest.fixture(name="load_flows_dir")
|
||||
def load_flows_dir():
|
||||
tempdir = tempfile.TemporaryDirectory()
|
||||
yield tempdir.name
|
||||
|
||||
|
||||
@pytest.fixture(name="distributed_env")
|
||||
def setup_env(monkeypatch):
|
||||
monkeypatch.setenv("LANGFLOW_CACHE_TYPE", "redis")
|
||||
monkeypatch.setenv("LANGFLOW_REDIS_HOST", "result_backend")
|
||||
monkeypatch.setenv("LANGFLOW_REDIS_PORT", "6379")
|
||||
monkeypatch.setenv("LANGFLOW_REDIS_DB", "0")
|
||||
monkeypatch.setenv("LANGFLOW_REDIS_EXPIRE", "3600")
|
||||
monkeypatch.setenv("LANGFLOW_REDIS_PASSWORD", "")
|
||||
monkeypatch.setenv("FLOWER_UNAUTHENTICATED_API", "True")
|
||||
monkeypatch.setenv("BROKER_URL", "redis://result_backend:6379/0")
|
||||
monkeypatch.setenv("RESULT_BACKEND", "redis://result_backend:6379/0")
|
||||
monkeypatch.setenv("C_FORCE_ROOT", "true")
|
||||
|
||||
|
||||
@pytest.fixture(name="distributed_client")
|
||||
def distributed_client_fixture(session: Session, monkeypatch, distributed_env):
|
||||
# Here we load the .env from ../deploy/.env
|
||||
from langflow.core import celery_app
|
||||
|
||||
db_dir = tempfile.mkdtemp()
|
||||
db_path = Path(db_dir) / "test.db"
|
||||
monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}")
|
||||
monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "false")
|
||||
# monkeypatch langflow.services.task.manager.USE_CELERY to True
|
||||
# monkeypatch.setattr(manager, "USE_CELERY", True)
|
||||
monkeypatch.setattr(celery_app, "celery_app", celery_app.make_celery("langflow", Config))
|
||||
|
||||
# def get_session_override():
|
||||
# return session
|
||||
|
||||
from langflow.main import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
# app.dependency_overrides[get_session] = get_session_override
|
||||
with TestClient(app) as client:
|
||||
yield client
|
||||
app.dependency_overrides.clear()
|
||||
monkeypatch.undo()
|
||||
|
||||
|
||||
def get_graph(_type="basic"):
|
||||
"""Get a graph from a json file"""
|
||||
|
||||
if _type == "basic":
|
||||
path = pytest.BASIC_EXAMPLE_PATH
|
||||
elif _type == "complex":
|
||||
path = pytest.COMPLEX_EXAMPLE_PATH
|
||||
elif _type == "openapi":
|
||||
path = pytest.OPENAPI_EXAMPLE_PATH
|
||||
|
||||
with open(path, "r") as f:
|
||||
flow_graph = json.load(f)
|
||||
data_graph = flow_graph["data"]
|
||||
nodes = data_graph["nodes"]
|
||||
edges = data_graph["edges"]
|
||||
return Graph(nodes, edges)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def basic_graph_data():
|
||||
with open(pytest.BASIC_EXAMPLE_PATH, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def basic_graph():
|
||||
return get_graph()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def complex_graph():
|
||||
return get_graph("complex")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def openapi_graph():
|
||||
return get_graph("openapi")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def json_flow():
|
||||
with open(pytest.BASIC_EXAMPLE_PATH, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def grouped_chat_json_flow():
|
||||
with open(pytest.GROUPED_CHAT_EXAMPLE_PATH, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def one_grouped_chat_json_flow():
|
||||
with open(pytest.ONE_GROUPED_CHAT_EXAMPLE_PATH, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vector_store_grouped_json_flow():
|
||||
with open(pytest.VECTOR_STORE_GROUPED_EXAMPLE_PATH, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def json_flow_with_prompt_and_history():
|
||||
with open(pytest.BASIC_CHAT_WITH_PROMPT_AND_HISTORY, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def json_simple_api_test():
|
||||
with open(pytest.SIMPLE_API_TEST, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def json_vector_store():
|
||||
with open(pytest.VECTOR_STORE_PATH, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def json_webhook_test():
|
||||
with open(pytest.WEBHOOK_TEST, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@pytest.fixture(name="client", autouse=True)
|
||||
def client_fixture(session: Session, monkeypatch, request, load_flows_dir):
|
||||
# Set the database url to a test database
|
||||
if "noclient" in request.keywords:
|
||||
yield
|
||||
else:
|
||||
db_dir = tempfile.mkdtemp()
|
||||
db_path = Path(db_dir) / "test.db"
|
||||
monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}")
|
||||
monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "false")
|
||||
if "load_flows" in request.keywords:
|
||||
shutil.copyfile(
|
||||
pytest.BASIC_EXAMPLE_PATH, os.path.join(load_flows_dir, "c54f9130-f2fa-4a3e-b22a-3856d946351b.json")
|
||||
)
|
||||
monkeypatch.setenv("LANGFLOW_LOAD_FLOWS_PATH", load_flows_dir)
|
||||
monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "true")
|
||||
|
||||
from langflow.main import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
# app.dependency_overrides[get_session] = get_session_override
|
||||
with TestClient(app) as client:
|
||||
yield client
|
||||
# app.dependency_overrides.clear()
|
||||
monkeypatch.undo()
|
||||
# clear the temp db
|
||||
with suppress(FileNotFoundError):
|
||||
db_path.unlink()
|
||||
|
||||
|
||||
# create a fixture for session_getter above
|
||||
@pytest.fixture(name="session_getter")
|
||||
def session_getter_fixture(client):
|
||||
@contextmanager
|
||||
def blank_session_getter(db_service: "DatabaseService"):
|
||||
with Session(db_service.engine) as session:
|
||||
yield session
|
||||
|
||||
yield blank_session_getter
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner():
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_user(client):
|
||||
user_data = UserCreate(
|
||||
username="testuser",
|
||||
password="testpassword",
|
||||
)
|
||||
response = client.post("/api/v1/users", json=user_data.model_dump())
|
||||
assert response.status_code == 201
|
||||
return response.json()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def active_user(client):
|
||||
db_manager = get_db_service()
|
||||
with session_getter(db_manager) as session:
|
||||
user = User(
|
||||
username="activeuser",
|
||||
password=get_password_hash("testpassword"),
|
||||
is_active=True,
|
||||
is_superuser=False,
|
||||
)
|
||||
# check if user exists
|
||||
if active_user := session.exec(select(User).where(User.username == user.username)).first():
|
||||
return active_user
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def logged_in_headers(client, active_user):
|
||||
login_data = {"username": active_user.username, "password": "testpassword"}
|
||||
response = client.post("/api/v1/login", data=login_data)
|
||||
assert response.status_code == 200
|
||||
tokens = response.json()
|
||||
a_token = tokens["access_token"]
|
||||
return {"Authorization": f"Bearer {a_token}"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def flow(client, json_flow: str, active_user):
|
||||
from langflow.services.database.models.flow.model import FlowCreate
|
||||
|
||||
loaded_json = json.loads(json_flow)
|
||||
flow_data = FlowCreate(name="test_flow", data=loaded_json.get("data"), user_id=active_user.id)
|
||||
|
||||
flow = Flow.model_validate(flow_data)
|
||||
with session_getter(get_db_service()) as session:
|
||||
session.add(flow)
|
||||
session.commit()
|
||||
session.refresh(flow)
|
||||
|
||||
return flow
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def json_chat_input():
|
||||
with open(pytest.CHAT_INPUT, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def json_two_outputs():
|
||||
with open(pytest.TWO_OUTPUTS, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def added_flow_with_prompt_and_history(client, json_flow_with_prompt_and_history, logged_in_headers):
|
||||
flow = orjson.loads(json_flow_with_prompt_and_history)
|
||||
data = flow["data"]
|
||||
flow = FlowCreate(name="Basic Chat", description="description", data=data)
|
||||
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["name"] == flow.name
|
||||
assert response.json()["data"] == flow.data
|
||||
return response.json()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def added_flow_chat_input(client, json_chat_input, logged_in_headers):
|
||||
flow = orjson.loads(json_chat_input)
|
||||
data = flow["data"]
|
||||
flow = FlowCreate(name="Chat Input", description="description", data=data)
|
||||
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["name"] == flow.name
|
||||
assert response.json()["data"] == flow.data
|
||||
return response.json()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def added_flow_two_outputs(client, json_two_outputs, logged_in_headers):
|
||||
flow = orjson.loads(json_two_outputs)
|
||||
data = flow["data"]
|
||||
flow = FlowCreate(name="Two Outputs", description="description", data=data)
|
||||
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["name"] == flow.name
|
||||
assert response.json()["data"] == flow.data
|
||||
return response.json()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def added_vector_store(client, json_vector_store, logged_in_headers):
|
||||
vector_store = orjson.loads(json_vector_store)
|
||||
data = vector_store["data"]
|
||||
vector_store = FlowCreate(name="Vector Store", description="description", data=data)
|
||||
response = client.post("api/v1/flows/", json=vector_store.model_dump(), headers=logged_in_headers)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["name"] == vector_store.name
|
||||
assert response.json()["data"] == vector_store.data
|
||||
return response.json()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def added_webhook_test(client, json_webhook_test, logged_in_headers):
|
||||
webhook_test = orjson.loads(json_webhook_test)
|
||||
data = webhook_test["data"]
|
||||
webhook_test = FlowCreate(
|
||||
name="Webhook Test", description="description", data=data, endpoint_name=webhook_test["endpoint_name"]
|
||||
)
|
||||
response = client.post("api/v1/flows/", json=webhook_test.model_dump(), headers=logged_in_headers)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["name"] == webhook_test.name
|
||||
assert response.json()["data"] == webhook_test.data
|
||||
return response.json()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def created_api_key(active_user):
|
||||
hashed = get_password_hash("random_key")
|
||||
api_key = ApiKey(
|
||||
name="test_api_key",
|
||||
user_id=active_user.id,
|
||||
api_key="random_key",
|
||||
hashed_api_key=hashed,
|
||||
)
|
||||
db_manager = get_db_service()
|
||||
with session_getter(db_manager) as session:
|
||||
if existing_api_key := session.exec(select(ApiKey).where(ApiKey.api_key == api_key.api_key)).first():
|
||||
return existing_api_key
|
||||
session.add(api_key)
|
||||
session.commit()
|
||||
session.refresh(api_key)
|
||||
return api_key
|
||||
|
||||
|
||||
@pytest.fixture(name="simple_api_test")
|
||||
def get_simple_api_test(client, logged_in_headers, json_simple_api_test):
|
||||
# Once the client is created, we can get the starter project
|
||||
# Just create a new flow with the simple api test
|
||||
flow = orjson.loads(json_simple_api_test)
|
||||
data = flow["data"]
|
||||
flow = FlowCreate(name="Simple API Test", data=data, description="Simple API Test")
|
||||
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["name"] == flow.name
|
||||
assert response.json()["data"] == flow.data
|
||||
return response.json()
|
||||
|
||||
|
||||
@pytest.fixture(name="starter_project")
|
||||
def get_starter_project(active_user):
|
||||
# once the client is created, we can get the starter project
|
||||
with session_getter(get_db_service()) as session:
|
||||
flow = session.exec(
|
||||
select(Flow)
|
||||
.where(Flow.folder.has(Folder.name == STARTER_FOLDER_NAME))
|
||||
.where(Flow.name == "Basic Prompting (Hello, World)")
|
||||
).first()
|
||||
if not flow:
|
||||
raise ValueError("No starter project found")
|
||||
|
||||
new_flow_create = FlowCreate(
|
||||
name=flow.name,
|
||||
description=flow.description,
|
||||
data=flow.data,
|
||||
user_id=active_user.id,
|
||||
)
|
||||
new_flow = Flow.model_validate(new_flow_create, from_attributes=True)
|
||||
session.add(new_flow)
|
||||
session.commit()
|
||||
session.refresh(new_flow)
|
||||
new_flow_dict = new_flow.model_dump()
|
||||
return new_flow_dict
|
||||
File diff suppressed because one or more lines are too long
918
src/backend/tests/data/ChatInputTest.json
Normal file
918
src/backend/tests/data/ChatInputTest.json
Normal file
|
|
@ -0,0 +1,918 @@
|
|||
{
|
||||
"name": "ChatInputTest",
|
||||
"description": "",
|
||||
"data": {
|
||||
"nodes": [
|
||||
{
|
||||
"width": 384,
|
||||
"height": 359,
|
||||
"id": "PromptTemplate-IKKOx",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 880,
|
||||
"y": 646.9375
|
||||
},
|
||||
"data": {
|
||||
"type": "PromptTemplate",
|
||||
"node": {
|
||||
"template": {
|
||||
"output_parser": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "output_parser",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "BaseOutputParser",
|
||||
"list": false
|
||||
},
|
||||
"input_variables": {
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "input_variables",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "str",
|
||||
"list": true,
|
||||
"value": [
|
||||
"input"
|
||||
]
|
||||
},
|
||||
"partial_variables": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "partial_variables",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "code",
|
||||
"list": false
|
||||
},
|
||||
"template": {
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": true,
|
||||
"password": false,
|
||||
"name": "template",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "prompt",
|
||||
"list": false,
|
||||
"value": "Input: {input}\nAI:"
|
||||
},
|
||||
"template_format": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": "f-string",
|
||||
"password": false,
|
||||
"name": "template_format",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "str",
|
||||
"list": false
|
||||
},
|
||||
"validate_template": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": true,
|
||||
"password": false,
|
||||
"name": "validate_template",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "bool",
|
||||
"list": false
|
||||
},
|
||||
"_type": "PromptTemplate",
|
||||
"input": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": true,
|
||||
"value": "",
|
||||
"password": false,
|
||||
"name": "input",
|
||||
"display_name": "input",
|
||||
"advanced": false,
|
||||
"input_types": [
|
||||
"Document",
|
||||
"BaseOutputParser",
|
||||
"str"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "str",
|
||||
"list": false
|
||||
}
|
||||
},
|
||||
"description": "A prompt template for a language model.",
|
||||
"base_classes": [
|
||||
"BasePromptTemplate",
|
||||
"PromptTemplate",
|
||||
"StringPromptTemplate"
|
||||
],
|
||||
"name": "",
|
||||
"display_name": "PromptTemplate",
|
||||
"documentation": "https://python.langchain.com/docs/modules/model_io/prompts/prompt_templates/",
|
||||
"custom_fields": {
|
||||
"": [
|
||||
"input"
|
||||
],
|
||||
"template": [
|
||||
"input"
|
||||
]
|
||||
},
|
||||
"output_types": [],
|
||||
"field_formatters": {
|
||||
"formatters": {
|
||||
"openai_api_key": {}
|
||||
},
|
||||
"base_formatters": {
|
||||
"kwargs": {},
|
||||
"optional": {},
|
||||
"list": {},
|
||||
"dict": {},
|
||||
"union": {},
|
||||
"multiline": {},
|
||||
"show": {},
|
||||
"password": {},
|
||||
"default": {},
|
||||
"headers": {},
|
||||
"dict_code_file": {},
|
||||
"model_fields": {
|
||||
"MODEL_DICT": {
|
||||
"OpenAI": [
|
||||
"text-davinci-003",
|
||||
"text-davinci-002",
|
||||
"text-curie-001",
|
||||
"text-babbage-001",
|
||||
"text-ada-001"
|
||||
],
|
||||
"ChatOpenAI": [
|
||||
"gpt-3.5-turbo-0613",
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-16k-0613",
|
||||
"gpt-3.5-turbo-16k",
|
||||
"gpt-4-0613",
|
||||
"gpt-4-32k-0613",
|
||||
"gpt-4",
|
||||
"gpt-4-32k"
|
||||
],
|
||||
"Anthropic": [
|
||||
"claude-v1",
|
||||
"claude-v1-100k",
|
||||
"claude-instant-v1",
|
||||
"claude-instant-v1-100k",
|
||||
"claude-v1.3",
|
||||
"claude-v1.3-100k",
|
||||
"claude-v1.2",
|
||||
"claude-v1.0",
|
||||
"claude-instant-v1.1",
|
||||
"claude-instant-v1.1-100k",
|
||||
"claude-instant-v1.0"
|
||||
],
|
||||
"ChatAnthropic": [
|
||||
"claude-v1",
|
||||
"claude-v1-100k",
|
||||
"claude-instant-v1",
|
||||
"claude-instant-v1-100k",
|
||||
"claude-v1.3",
|
||||
"claude-v1.3-100k",
|
||||
"claude-v1.2",
|
||||
"claude-v1.0",
|
||||
"claude-instant-v1.1",
|
||||
"claude-instant-v1.1-100k",
|
||||
"claude-instant-v1.0"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"beta": false,
|
||||
"error": null
|
||||
},
|
||||
"id": "PromptTemplate-IKKOx"
|
||||
},
|
||||
"selected": false,
|
||||
"positionAbsolute": {
|
||||
"x": 880,
|
||||
"y": 646.9375
|
||||
},
|
||||
"dragging": false
|
||||
},
|
||||
{
|
||||
"width": 384,
|
||||
"height": 307,
|
||||
"id": "LLMChain-e2dhN",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 1449.330344958542,
|
||||
"y": 880.1760221487797
|
||||
},
|
||||
"data": {
|
||||
"type": "LLMChain",
|
||||
"node": {
|
||||
"template": {
|
||||
"callbacks": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "callbacks",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "langchain.callbacks.base.BaseCallbackHandler",
|
||||
"list": true
|
||||
},
|
||||
"llm": {
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "llm",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "BaseLanguageModel",
|
||||
"list": false
|
||||
},
|
||||
"memory": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "memory",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "BaseMemory",
|
||||
"list": false
|
||||
},
|
||||
"output_parser": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "output_parser",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "BaseLLMOutputParser",
|
||||
"list": false
|
||||
},
|
||||
"prompt": {
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "prompt",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "BasePromptTemplate",
|
||||
"list": false
|
||||
},
|
||||
"llm_kwargs": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "llm_kwargs",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "code",
|
||||
"list": false
|
||||
},
|
||||
"metadata": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "metadata",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "code",
|
||||
"list": false
|
||||
},
|
||||
"output_key": {
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"value": "text",
|
||||
"password": false,
|
||||
"name": "output_key",
|
||||
"advanced": true,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "str",
|
||||
"list": false
|
||||
},
|
||||
"return_final_only": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": true,
|
||||
"password": false,
|
||||
"name": "return_final_only",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "bool",
|
||||
"list": false
|
||||
},
|
||||
"tags": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "tags",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "str",
|
||||
"list": true
|
||||
},
|
||||
"verbose": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": false,
|
||||
"password": false,
|
||||
"name": "verbose",
|
||||
"advanced": true,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "bool",
|
||||
"list": false
|
||||
},
|
||||
"_type": "LLMChain"
|
||||
},
|
||||
"description": "Chain to run queries against LLMs.",
|
||||
"base_classes": [
|
||||
"Chain",
|
||||
"LLMChain",
|
||||
"function",
|
||||
"Text"
|
||||
],
|
||||
"display_name": "LLMChain",
|
||||
"custom_fields": {},
|
||||
"output_types": [],
|
||||
"documentation": "https://python.langchain.com/docs/modules/chains/foundational/llm_chain",
|
||||
"beta": false,
|
||||
"error": null
|
||||
},
|
||||
"id": "LLMChain-e2dhN"
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 1449.330344958542,
|
||||
"y": 880.1760221487797
|
||||
}
|
||||
},
|
||||
{
|
||||
"width": 384,
|
||||
"height": 621,
|
||||
"id": "ChatOpenAI-2I57f",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 393.3551923753797,
|
||||
"y": 1061.025177453298
|
||||
},
|
||||
"data": {
|
||||
"type": "ChatOpenAI",
|
||||
"node": {
|
||||
"template": {
|
||||
"callbacks": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "callbacks",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "langchain.callbacks.base.BaseCallbackHandler",
|
||||
"list": true
|
||||
},
|
||||
"cache": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "cache",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "bool",
|
||||
"list": false
|
||||
},
|
||||
"client": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "client",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "Any",
|
||||
"list": false
|
||||
},
|
||||
"max_retries": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": 6,
|
||||
"password": false,
|
||||
"name": "max_retries",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "int",
|
||||
"list": false
|
||||
},
|
||||
"max_tokens": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": true,
|
||||
"name": "max_tokens",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "int",
|
||||
"list": false,
|
||||
"value": ""
|
||||
},
|
||||
"metadata": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "metadata",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "code",
|
||||
"list": false
|
||||
},
|
||||
"model_kwargs": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "model_kwargs",
|
||||
"advanced": true,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "code",
|
||||
"list": false
|
||||
},
|
||||
"model_name": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"value": "gpt-3.5-turbo-0613",
|
||||
"password": false,
|
||||
"options": [
|
||||
"gpt-3.5-turbo-0613",
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-16k-0613",
|
||||
"gpt-3.5-turbo-16k",
|
||||
"gpt-4-0613",
|
||||
"gpt-4-32k-0613",
|
||||
"gpt-4",
|
||||
"gpt-4-32k"
|
||||
],
|
||||
"name": "model_name",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "str",
|
||||
"list": true
|
||||
},
|
||||
"n": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": 1,
|
||||
"password": false,
|
||||
"name": "n",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "int",
|
||||
"list": false
|
||||
},
|
||||
"openai_api_base": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "openai_api_base",
|
||||
"display_name": "OpenAI API Base",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "\nThe base URL of the OpenAI API. Defaults to https://api.openai.com/v1.\n\nYou can change this to use other APIs like JinaChat, LocalAI and Prem.\n",
|
||||
"type": "str",
|
||||
"list": false
|
||||
},
|
||||
"openai_api_key": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"value": "",
|
||||
"password": true,
|
||||
"name": "openai_api_key",
|
||||
"display_name": "OpenAI API Key",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "str",
|
||||
"list": false
|
||||
},
|
||||
"openai_organization": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "openai_organization",
|
||||
"display_name": "OpenAI Organization",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "str",
|
||||
"list": false
|
||||
},
|
||||
"openai_proxy": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "openai_proxy",
|
||||
"display_name": "OpenAI Proxy",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "str",
|
||||
"list": false
|
||||
},
|
||||
"request_timeout": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "request_timeout",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "float",
|
||||
"list": false
|
||||
},
|
||||
"streaming": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": false,
|
||||
"password": false,
|
||||
"name": "streaming",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "bool",
|
||||
"list": false
|
||||
},
|
||||
"tags": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "tags",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "str",
|
||||
"list": true
|
||||
},
|
||||
"temperature": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"value": 0.7,
|
||||
"password": false,
|
||||
"name": "temperature",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "float",
|
||||
"list": false
|
||||
},
|
||||
"tiktoken_model_name": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "tiktoken_model_name",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "str",
|
||||
"list": false
|
||||
},
|
||||
"verbose": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": false,
|
||||
"password": false,
|
||||
"name": "verbose",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "bool",
|
||||
"list": false
|
||||
},
|
||||
"_type": "ChatOpenAI"
|
||||
},
|
||||
"description": "`OpenAI` Chat large language models API.",
|
||||
"base_classes": [
|
||||
"BaseChatModel",
|
||||
"ChatOpenAI",
|
||||
"BaseLanguageModel",
|
||||
"BaseLLM"
|
||||
],
|
||||
"display_name": "ChatOpenAI",
|
||||
"custom_fields": {},
|
||||
"output_types": [],
|
||||
"documentation": "https://python.langchain.com/docs/modules/model_io/models/chat/integrations/openai",
|
||||
"beta": false,
|
||||
"error": null
|
||||
},
|
||||
"id": "ChatOpenAI-2I57f"
|
||||
},
|
||||
"selected": false,
|
||||
"positionAbsolute": {
|
||||
"x": 393.3551923753797,
|
||||
"y": 1061.025177453298
|
||||
},
|
||||
"dragging": false
|
||||
},
|
||||
{
|
||||
"width": 384,
|
||||
"height": 359,
|
||||
"id": "ChatInput-207IY",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 415.1018926651509,
|
||||
"y": 506.62736462360317
|
||||
},
|
||||
"data": {
|
||||
"type": "ChatInput",
|
||||
"node": {
|
||||
"template": {
|
||||
"code": {
|
||||
"dynamic": true,
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": true,
|
||||
"value": "from typing import Optional\nfrom langflow.custom import CustomComponent\n\n\nclass ChatInput(CustomComponent):\n display_name = \"Chat Input\"\n\n def build(self, message: Optional[str] = \"\") -> str:\n return message\n",
|
||||
"password": false,
|
||||
"name": "code",
|
||||
"advanced": false,
|
||||
"type": "code",
|
||||
"list": false
|
||||
},
|
||||
"_type": "CustomComponent",
|
||||
"message": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"value": "",
|
||||
"password": false,
|
||||
"name": "message",
|
||||
"display_name": "message",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "str",
|
||||
"list": false
|
||||
}
|
||||
},
|
||||
"description": "Used to get user input from the chat.",
|
||||
"base_classes": [
|
||||
"str"
|
||||
],
|
||||
"display_name": "Chat Input",
|
||||
"custom_fields": {
|
||||
"message": null
|
||||
},
|
||||
"output_types": [
|
||||
"ChatInput"
|
||||
],
|
||||
"documentation": "",
|
||||
"beta": true,
|
||||
"error": null
|
||||
},
|
||||
"id": "ChatInput-207IY"
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 415.1018926651509,
|
||||
"y": 506.62736462360317
|
||||
}
|
||||
},
|
||||
{
|
||||
"width": 384,
|
||||
"height": 389,
|
||||
"id": "ChatOutput-1jlJy",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 2002.8008888732943,
|
||||
"y": 926.1397178702218
|
||||
},
|
||||
"data": {
|
||||
"type": "ChatOutput",
|
||||
"node": {
|
||||
"template": {
|
||||
"code": {
|
||||
"dynamic": true,
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": true,
|
||||
"value": "from typing import Optional, Text\nfrom langflow.api.v1.schemas import ChatMessage\nfrom langflow.services.utils import get_chat_manager\nfrom langflow.custom import CustomComponent\nfrom anyio.from_thread import start_blocking_portal\nfrom loguru import logger\n\n\nclass ChatOutput(CustomComponent):\n display_name = \"Chat Output\"\n description = \"Used to send a message to the chat.\"\n\n field_config = {\n \"code\": {\n \"show\": False,\n }\n }\n\n def build_config(self):\n return {\"message\": {\"input_types\": [\"Text\"]}}\n\n def build(self, message: Optional[Text], is_ai: bool = False) -> Text:\n if not message:\n return \"\"\n try:\n chat_manager = get_chat_manager()\n chat_message = ChatMessage(message=message, is_bot=is_ai)\n # send_message is a coroutine\n # run in a thread safe manner\n with start_blocking_portal() as portal:\n portal.call(chat_manager.send_message, chat_message)\n chat_manager.chat_history.add_message(\n chat_manager.cache_manager.current_client_id, chat_message\n )\n except Exception as exc:\n logger.exception(exc)\n logger.debug(f\"Error sending message to chat: {exc}\")\n self.repr_value = message\n return message\n",
|
||||
"password": false,
|
||||
"name": "code",
|
||||
"advanced": false,
|
||||
"type": "code",
|
||||
"list": false
|
||||
},
|
||||
"_type": "CustomComponent",
|
||||
"is_ai": {
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"value": true,
|
||||
"password": false,
|
||||
"name": "is_ai",
|
||||
"display_name": "is_ai",
|
||||
"advanced": false,
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "bool",
|
||||
"list": false
|
||||
},
|
||||
"message": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "message",
|
||||
"display_name": "message",
|
||||
"advanced": false,
|
||||
"input_types": [
|
||||
"Text"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"type": "Text",
|
||||
"list": false
|
||||
}
|
||||
},
|
||||
"description": "Used to send a message to the chat.",
|
||||
"base_classes": [
|
||||
"str"
|
||||
],
|
||||
"display_name": "Chat Output",
|
||||
"custom_fields": {
|
||||
"is_ai": null,
|
||||
"message": null
|
||||
},
|
||||
"output_types": [
|
||||
"ChatOutput"
|
||||
],
|
||||
"documentation": "",
|
||||
"beta": true,
|
||||
"error": null
|
||||
},
|
||||
"id": "ChatOutput-1jlJy"
|
||||
},
|
||||
"selected": true,
|
||||
"dragging": false,
|
||||
"positionAbsolute": {
|
||||
"x": 2002.8008888732943,
|
||||
"y": 926.1397178702218
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "PromptTemplate-IKKOx",
|
||||
"sourceHandle": "PromptTemplate|PromptTemplate-IKKOx|BasePromptTemplate|PromptTemplate|StringPromptTemplate",
|
||||
"target": "LLMChain-e2dhN",
|
||||
"targetHandle": "BasePromptTemplate|prompt|LLMChain-e2dhN",
|
||||
"style": {
|
||||
"stroke": "#555"
|
||||
},
|
||||
"className": "",
|
||||
"animated": false,
|
||||
"id": "reactflow__edge-PromptTemplate-IKKOxPromptTemplate|PromptTemplate-IKKOx|StringPromptTemplate|BasePromptTemplate|PromptTemplate-LLMChain-e2dhNBasePromptTemplate|prompt|LLMChain-e2dhN"
|
||||
},
|
||||
{
|
||||
"source": "ChatOpenAI-2I57f",
|
||||
"sourceHandle": "ChatOpenAI|ChatOpenAI-2I57f|BaseChatModel|ChatOpenAI|BaseLanguageModel|BaseLLM",
|
||||
"target": "LLMChain-e2dhN",
|
||||
"targetHandle": "BaseLanguageModel|llm|LLMChain-e2dhN",
|
||||
"style": {
|
||||
"stroke": "#555"
|
||||
},
|
||||
"className": "",
|
||||
"animated": false,
|
||||
"id": "reactflow__edge-ChatOpenAI-2I57fChatOpenAI|ChatOpenAI-2I57f|BaseChatModel|ChatOpenAI|BaseLanguageModel|BaseLLM-LLMChain-e2dhNBaseLanguageModel|llm|LLMChain-e2dhN"
|
||||
},
|
||||
{
|
||||
"source": "ChatInput-207IY",
|
||||
"sourceHandle": "ChatInput|ChatInput-207IY|str",
|
||||
"target": "PromptTemplate-IKKOx",
|
||||
"targetHandle": "Document;BaseOutputParser;str|input|PromptTemplate-IKKOx",
|
||||
"style": {
|
||||
"stroke": "#555"
|
||||
},
|
||||
"className": "",
|
||||
"animated": false,
|
||||
"id": "reactflow__edge-ChatInput-207IYChatInput|ChatInput-207IY|str-PromptTemplate-IKKOxDocument;BaseOutputParser;str|input|PromptTemplate-IKKOx"
|
||||
},
|
||||
{
|
||||
"source": "LLMChain-e2dhN",
|
||||
"sourceHandle": "LLMChain|LLMChain-e2dhN|Chain|LLMChain|function|Text",
|
||||
"target": "ChatOutput-1jlJy",
|
||||
"targetHandle": "Text|message|ChatOutput-1jlJy",
|
||||
"style": {
|
||||
"stroke": "#555"
|
||||
},
|
||||
"className": "stroke-foreground stroke-connection",
|
||||
"animated": true,
|
||||
"id": "reactflow__edge-LLMChain-e2dhNLLMChain|LLMChain-e2dhN|Chain|LLMChain|function|Text-ChatOutput-1jlJyText|message|ChatOutput-1jlJy"
|
||||
}
|
||||
],
|
||||
"viewport": {
|
||||
"x": -141.98308184453367,
|
||||
"y": -104.98637616656356,
|
||||
"zoom": 0.4788209787464315
|
||||
}
|
||||
},
|
||||
"id": "b3388ab9-b5dc-4447-b560-79caef40faa5",
|
||||
"user_id": "c65bfea3-3eea-4e71-8fc4-106238eb0583"
|
||||
}
|
||||
445
src/backend/tests/data/Openapi.json
Normal file
445
src/backend/tests/data/Openapi.json
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
{
|
||||
"description": "",
|
||||
"name": "openapi",
|
||||
"id": "1",
|
||||
"data": {
|
||||
"nodes": [
|
||||
{
|
||||
"width": 384,
|
||||
"height": 311,
|
||||
"id": "dndnode_19",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": -207.85635949789724,
|
||||
"y": -105.73915116823618
|
||||
},
|
||||
"data": {
|
||||
"type": "JsonToolkit",
|
||||
"node": {
|
||||
"template": {
|
||||
"spec": {
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "spec",
|
||||
"type": "JsonSpec",
|
||||
"list": false
|
||||
},
|
||||
"_type": "JsonToolkit"
|
||||
},
|
||||
"description": "Toolkit for interacting with a JSON spec.",
|
||||
"base_classes": [
|
||||
"BaseToolkit"
|
||||
]
|
||||
},
|
||||
"id": "dndnode_19",
|
||||
"value": null
|
||||
},
|
||||
"selected": false,
|
||||
"positionAbsolute": {
|
||||
"x": -207.85635949789724,
|
||||
"y": -105.73915116823618
|
||||
},
|
||||
"dragging": false
|
||||
},
|
||||
{
|
||||
"width": 384,
|
||||
"height": 351,
|
||||
"id": "dndnode_32",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 745.308873444751,
|
||||
"y": -37.007911201107675
|
||||
},
|
||||
"data": {
|
||||
"type": "OpenAPIToolkit",
|
||||
"node": {
|
||||
"template": {
|
||||
"json_agent": {
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "json_agent",
|
||||
"type": "AgentExecutor",
|
||||
"list": false
|
||||
},
|
||||
"requests_wrapper": {
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "requests_wrapper",
|
||||
"type": "TextRequestsWrapper",
|
||||
"list": false
|
||||
},
|
||||
"_type": "OpenAPIToolkit"
|
||||
},
|
||||
"description": "Toolkit for interacting with a OpenAPI api.",
|
||||
"base_classes": [
|
||||
"BaseToolkit"
|
||||
]
|
||||
},
|
||||
"id": "dndnode_32",
|
||||
"value": null
|
||||
},
|
||||
"selected": false,
|
||||
"positionAbsolute": {
|
||||
"x": 745.308873444751,
|
||||
"y": -37.007911201107675
|
||||
},
|
||||
"dragging": false
|
||||
},
|
||||
{
|
||||
"width": 384,
|
||||
"height": 351,
|
||||
"id": "dndnode_33",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 281.30887344475104,
|
||||
"y": 2.9920887988923255
|
||||
},
|
||||
"data": {
|
||||
"type": "JsonAgent",
|
||||
"node": {
|
||||
"template": {
|
||||
"toolkit": {
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "toolkit",
|
||||
"type": "BaseToolkit",
|
||||
"list": false
|
||||
},
|
||||
"llm": {
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "llm",
|
||||
"type": "BaseLanguageModel",
|
||||
"list": false
|
||||
},
|
||||
"_type": "JsonAgent"
|
||||
},
|
||||
"description": "Construct a json agent from an LLM and tools.",
|
||||
"base_classes": [
|
||||
"AgentExecutor"
|
||||
]
|
||||
},
|
||||
"id": "dndnode_33",
|
||||
"value": null
|
||||
},
|
||||
"selected": false,
|
||||
"dragging": false,
|
||||
"positionAbsolute": {
|
||||
"x": 281.30887344475104,
|
||||
"y": 2.9920887988923255
|
||||
}
|
||||
},
|
||||
{
|
||||
"width": 384,
|
||||
"height": 349,
|
||||
"id": "dndnode_34",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 301.30887344475104,
|
||||
"y": 532.9920887988924
|
||||
},
|
||||
"data": {
|
||||
"type": "TextRequestsWrapper",
|
||||
"node": {
|
||||
"template": {
|
||||
"headers": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": true,
|
||||
"value": "{'Authorization':\n 'Bearer <token>'}",
|
||||
"password": false,
|
||||
"name": "headers",
|
||||
"type": "code",
|
||||
"list": false
|
||||
},
|
||||
"aiosession": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "aiosession",
|
||||
"type": "ClientSession",
|
||||
"list": false
|
||||
},
|
||||
"_type": "TextRequestsWrapper"
|
||||
},
|
||||
"description": "Lightweight wrapper around requests library.",
|
||||
"base_classes": [
|
||||
"TextRequestsWrapper"
|
||||
]
|
||||
},
|
||||
"id": "dndnode_34",
|
||||
"value": null
|
||||
},
|
||||
"positionAbsolute": {
|
||||
"x": 301.30887344475104,
|
||||
"y": 532.9920887988924
|
||||
}
|
||||
},
|
||||
{
|
||||
"width": 384,
|
||||
"height": 407,
|
||||
"id": "dndnode_35",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": -754.691126555249,
|
||||
"y": -37.00791120110762
|
||||
},
|
||||
"data": {
|
||||
"type": "JsonSpec",
|
||||
"node": {
|
||||
"template": {
|
||||
"path": {
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"value": "api-with-examples.yaml",
|
||||
"suffixes": [
|
||||
".json",
|
||||
".yaml",
|
||||
".yml"
|
||||
],
|
||||
"password": false,
|
||||
"name": "path",
|
||||
"type": "file",
|
||||
"list": false,
|
||||
"fileTypes": [
|
||||
"json",
|
||||
"yaml",
|
||||
"yml"
|
||||
],
|
||||
"file_path": "api-with-examples.yaml"
|
||||
},
|
||||
"max_value_length": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"value": "4000",
|
||||
"password": false,
|
||||
"name": "max_value_length",
|
||||
"type": "int",
|
||||
"list": false
|
||||
},
|
||||
"_type": "JsonSpec"
|
||||
},
|
||||
"description": "",
|
||||
"base_classes": [
|
||||
"Tool",
|
||||
"JsonSpec"
|
||||
]
|
||||
},
|
||||
"id": "dndnode_35",
|
||||
"value": null
|
||||
},
|
||||
"selected": false,
|
||||
"dragging": false,
|
||||
"positionAbsolute": {
|
||||
"x": -754.691126555249,
|
||||
"y": -37.00791120110762
|
||||
}
|
||||
},
|
||||
{
|
||||
"width": 384,
|
||||
"height": 563,
|
||||
"id": "dndnode_36",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": -310.69112655524896,
|
||||
"y": 514.9920887988924
|
||||
},
|
||||
"data": {
|
||||
"type": "ChatOpenAI",
|
||||
"node": {
|
||||
"template": {
|
||||
"cache": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "cache",
|
||||
"type": "bool",
|
||||
"list": false
|
||||
},
|
||||
"verbose": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": false,
|
||||
"password": false,
|
||||
"name": "verbose",
|
||||
"type": "bool",
|
||||
"list": false
|
||||
},
|
||||
"client": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "client",
|
||||
"type": "Any",
|
||||
"list": false
|
||||
},
|
||||
"model_name": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"value": "gpt-3.5-turbo",
|
||||
"password": false,
|
||||
"name": "model_name",
|
||||
"type": "str",
|
||||
"list": false
|
||||
},
|
||||
"model_kwargs": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "model_kwargs",
|
||||
"type": "code",
|
||||
"list": false
|
||||
},
|
||||
"openai_api_key": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "openai_api_key",
|
||||
"type": "str",
|
||||
"list": false,
|
||||
"value": "sk-"
|
||||
},
|
||||
"max_retries": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": 6,
|
||||
"password": false,
|
||||
"name": "max_retries",
|
||||
"type": "int",
|
||||
"list": false
|
||||
},
|
||||
"prefix_messages": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "prefix_messages",
|
||||
"type": "Any",
|
||||
"list": true
|
||||
},
|
||||
"streaming": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": false,
|
||||
"password": false,
|
||||
"name": "streaming",
|
||||
"type": "bool",
|
||||
"list": false
|
||||
},
|
||||
"_type": "ChatOpenAI"
|
||||
},
|
||||
"description": "Wrapper around OpenAI Chat large language models.To use, you should have the ``openai`` python package installed, and theenvironment variable ``OPENAI_API_KEY`` set with your API key.Any parameters that are valid to be passed to the openai.create call can be passedin, even if not explicitly saved on this class.",
|
||||
"base_classes": [
|
||||
"BaseLanguageModel",
|
||||
"BaseLLM"
|
||||
]
|
||||
},
|
||||
"id": "dndnode_36",
|
||||
"value": null
|
||||
},
|
||||
"selected": false,
|
||||
"dragging": false,
|
||||
"positionAbsolute": {
|
||||
"x": -310.69112655524896,
|
||||
"y": 514.9920887988924
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "dndnode_19",
|
||||
"sourceHandle": "JsonToolkit|dndnode_19|BaseToolkit",
|
||||
"target": "dndnode_33",
|
||||
"targetHandle": "BaseToolkit|toolkit|dndnode_33",
|
||||
"className": "animate-pulse",
|
||||
"id": "reactflow__edge-dndnode_19JsonToolkit|dndnode_19|BaseToolkit-dndnode_33BaseToolkit|toolkit|dndnode_33",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"source": "dndnode_33",
|
||||
"sourceHandle": "JsonAgent|dndnode_33|AgentExecutor",
|
||||
"target": "dndnode_32",
|
||||
"targetHandle": "AgentExecutor|json_agent|dndnode_32",
|
||||
"className": "animate-pulse",
|
||||
"id": "reactflow__edge-dndnode_33JsonAgent|dndnode_33|AgentExecutor-dndnode_32AgentExecutor|json_agent|dndnode_32",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"source": "dndnode_34",
|
||||
"sourceHandle": "TextRequestsWrapper|dndnode_34|TextRequestsWrapper",
|
||||
"target": "dndnode_32",
|
||||
"targetHandle": "TextRequestsWrapper|requests_wrapper|dndnode_32",
|
||||
"className": "animate-pulse",
|
||||
"id": "reactflow__edge-dndnode_34RequestsWrapper|dndnode_34|TextRequestsWrapper-dndnode_32RequestsWrapper|requests_wrapper|dndnode_32",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"source": "dndnode_35",
|
||||
"sourceHandle": "JsonSpec|dndnode_35|Tool|JsonSpec",
|
||||
"target": "dndnode_19",
|
||||
"targetHandle": "JsonSpec|spec|dndnode_19",
|
||||
"className": "animate-pulse",
|
||||
"id": "reactflow__edge-dndnode_35JsonSpec|dndnode_35|Tool|JsonSpec-dndnode_19JsonSpec|spec|dndnode_19",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"source": "dndnode_36",
|
||||
"sourceHandle": "ChatOpenAI|dndnode_36|BaseLanguageModel|BaseLLM",
|
||||
"target": "dndnode_33",
|
||||
"targetHandle": "BaseLanguageModel|llm|dndnode_33",
|
||||
"className": "animate-pulse",
|
||||
"id": "reactflow__edge-dndnode_36OpenAIChat|dndnode_36|BaseLanguageModel|BaseLLM-dndnode_33BaseLanguageModel|llm|dndnode_33"
|
||||
}
|
||||
],
|
||||
"viewport": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"zoom": 1
|
||||
}
|
||||
},
|
||||
"chat": [
|
||||
{
|
||||
"message": "test",
|
||||
"isSend": true
|
||||
}
|
||||
]
|
||||
}
|
||||
567
src/backend/tests/data/SimpleAPITest.json
Normal file
567
src/backend/tests/data/SimpleAPITest.json
Normal file
|
|
@ -0,0 +1,567 @@
|
|||
{
|
||||
"id": "e9380216-9300-41a1-bc35-7ee92fe4b30d",
|
||||
"data": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "ChatInput-irFJf",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 180,
|
||||
"y": 200.296875
|
||||
},
|
||||
"data": {
|
||||
"type": "ChatInput",
|
||||
"node": {
|
||||
"template": {
|
||||
"_type": "Component",
|
||||
"files": {
|
||||
"trace_as_metadata": true,
|
||||
"file_path": "",
|
||||
"fileTypes": [
|
||||
"txt",
|
||||
"md",
|
||||
"mdx",
|
||||
"csv",
|
||||
"json",
|
||||
"yaml",
|
||||
"yml",
|
||||
"xml",
|
||||
"html",
|
||||
"htm",
|
||||
"pdf",
|
||||
"docx",
|
||||
"py",
|
||||
"sh",
|
||||
"sql",
|
||||
"js",
|
||||
"ts",
|
||||
"tsx",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"png",
|
||||
"bmp",
|
||||
"image"
|
||||
],
|
||||
"list": true,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "",
|
||||
"name": "files",
|
||||
"display_name": "Files",
|
||||
"advanced": true,
|
||||
"dynamic": false,
|
||||
"info": "Files to be sent with the message.",
|
||||
"title_case": false,
|
||||
"type": "file"
|
||||
},
|
||||
"code": {
|
||||
"type": "code",
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"list": false,
|
||||
"show": true,
|
||||
"multiline": true,
|
||||
"value": "from langflow.base.data.utils import IMG_FILE_TYPES, TEXT_FILE_TYPES\nfrom langflow.base.io.chat import ChatComponent\nfrom langflow.inputs import BoolInput\nfrom langflow.io import DropdownInput, FileInput, MessageTextInput, MultilineInput, Output\nfrom langflow.memory import store_message\nfrom langflow.schema.message import Message\n\n\nclass ChatInput(ChatComponent):\n display_name = \"Chat Input\"\n description = \"Get chat inputs from the Playground.\"\n icon = \"ChatInput\"\n name = \"ChatInput\"\n\n inputs = [\n MultilineInput(\n name=\"input_value\",\n display_name=\"Text\",\n value=\"\",\n info=\"Message to be passed as input.\",\n ),\n BoolInput(\n name=\"store_message\",\n display_name=\"Store Messages\",\n info=\"Store the message in the history.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[\"Machine\", \"User\"],\n value=\"User\",\n info=\"Type of sender.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Name of the sender.\",\n value=\"User\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"session_id\", display_name=\"Session ID\", info=\"Session ID for the message.\", advanced=True\n ),\n FileInput(\n name=\"files\",\n display_name=\"Files\",\n file_types=TEXT_FILE_TYPES + IMG_FILE_TYPES,\n info=\"Files to be sent with the message.\",\n advanced=True,\n is_list=True,\n ),\n ]\n outputs = [\n Output(display_name=\"Message\", name=\"message\", method=\"message_response\"),\n ]\n\n def message_response(self) -> Message:\n message = Message(\n text=self.input_value,\n sender=self.sender,\n sender_name=self.sender_name,\n session_id=self.session_id,\n files=self.files,\n )\n\n if self.session_id and isinstance(message, Message) and isinstance(message.text, str):\n store_message(\n message,\n flow_id=self.graph.flow_id,\n )\n self.message.value = message\n\n self.status = message\n return message\n",
|
||||
"fileTypes": [],
|
||||
"file_path": "",
|
||||
"password": false,
|
||||
"name": "code",
|
||||
"advanced": true,
|
||||
"dynamic": true,
|
||||
"info": "",
|
||||
"load_from_db": false,
|
||||
"title_case": false
|
||||
},
|
||||
"input_value": {
|
||||
"trace_as_input": true,
|
||||
"multiline": true,
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "",
|
||||
"name": "input_value",
|
||||
"display_name": "Text",
|
||||
"advanced": false,
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "Message to be passed as input.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
},
|
||||
"sender": {
|
||||
"trace_as_metadata": true,
|
||||
"options": [
|
||||
"Machine",
|
||||
"User"
|
||||
],
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "User",
|
||||
"name": "sender",
|
||||
"display_name": "Sender Type",
|
||||
"advanced": true,
|
||||
"dynamic": false,
|
||||
"info": "Type of sender.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
},
|
||||
"sender_name": {
|
||||
"trace_as_input": true,
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "User",
|
||||
"name": "sender_name",
|
||||
"display_name": "Sender Name",
|
||||
"advanced": true,
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "Name of the sender.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
},
|
||||
"session_id": {
|
||||
"trace_as_input": true,
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "",
|
||||
"name": "session_id",
|
||||
"display_name": "Session ID",
|
||||
"advanced": true,
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "Session ID for the message.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
},
|
||||
"store_message": {
|
||||
"trace_as_metadata": true,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": true,
|
||||
"name": "store_message",
|
||||
"display_name": "Store Messages",
|
||||
"advanced": true,
|
||||
"dynamic": false,
|
||||
"info": "Store the message in the history.",
|
||||
"title_case": false,
|
||||
"type": "bool"
|
||||
}
|
||||
},
|
||||
"description": "Get chat inputs from the Playground.",
|
||||
"icon": "ChatInput",
|
||||
"base_classes": [
|
||||
"Message"
|
||||
],
|
||||
"display_name": "Chat Input",
|
||||
"documentation": "",
|
||||
"custom_fields": {},
|
||||
"output_types": [],
|
||||
"pinned": false,
|
||||
"conditional_paths": [],
|
||||
"frozen": false,
|
||||
"outputs": [
|
||||
{
|
||||
"types": [
|
||||
"Message"
|
||||
],
|
||||
"selected": "Message",
|
||||
"name": "message",
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
"value": "__UNDEFINED__",
|
||||
"cache": true
|
||||
}
|
||||
],
|
||||
"field_order": [
|
||||
"input_value",
|
||||
"store_message",
|
||||
"sender",
|
||||
"sender_name",
|
||||
"session_id",
|
||||
"files"
|
||||
],
|
||||
"beta": false,
|
||||
"edited": false
|
||||
},
|
||||
"id": "ChatInput-irFJf",
|
||||
"description": "Get chat inputs from the Playground.",
|
||||
"display_name": "Chat Input"
|
||||
},
|
||||
"selected": false,
|
||||
"width": 384,
|
||||
"height": 309
|
||||
},
|
||||
{
|
||||
"id": "TextInput-tcoZg",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 186,
|
||||
"y": 549.296875
|
||||
},
|
||||
"data": {
|
||||
"type": "TextInput",
|
||||
"node": {
|
||||
"template": {
|
||||
"_type": "Component",
|
||||
"code": {
|
||||
"type": "code",
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"list": false,
|
||||
"show": true,
|
||||
"multiline": true,
|
||||
"value": "from langflow.base.io.text import TextComponent\nfrom langflow.io import MessageTextInput, Output\nfrom langflow.schema.message import Message\n\n\nclass TextInputComponent(TextComponent):\n display_name = \"Text Input\"\n description = \"Get text inputs from the Playground.\"\n icon = \"type\"\n name = \"TextInput\"\n\n inputs = [\n MessageTextInput(\n name=\"input_value\",\n display_name=\"Text\",\n info=\"Text to be passed as input.\",\n ),\n ]\n outputs = [\n Output(display_name=\"Text\", name=\"text\", method=\"text_response\"),\n ]\n\n def text_response(self) -> Message:\n message = Message(\n text=self.input_value,\n )\n return message\n",
|
||||
"fileTypes": [],
|
||||
"file_path": "",
|
||||
"password": false,
|
||||
"name": "code",
|
||||
"advanced": true,
|
||||
"dynamic": true,
|
||||
"info": "",
|
||||
"load_from_db": false,
|
||||
"title_case": false
|
||||
},
|
||||
"input_value": {
|
||||
"trace_as_input": true,
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "AI",
|
||||
"name": "input_value",
|
||||
"display_name": "Text",
|
||||
"advanced": false,
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "Text to be passed as input.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
}
|
||||
},
|
||||
"description": "Get text inputs from the Playground.",
|
||||
"icon": "type",
|
||||
"base_classes": [
|
||||
"Message"
|
||||
],
|
||||
"display_name": "Text Input",
|
||||
"documentation": "",
|
||||
"custom_fields": {},
|
||||
"output_types": [],
|
||||
"pinned": false,
|
||||
"conditional_paths": [],
|
||||
"frozen": false,
|
||||
"outputs": [
|
||||
{
|
||||
"types": [
|
||||
"Message"
|
||||
],
|
||||
"selected": "Message",
|
||||
"name": "text",
|
||||
"display_name": "Text",
|
||||
"method": "text_response",
|
||||
"value": "__UNDEFINED__",
|
||||
"cache": true
|
||||
}
|
||||
],
|
||||
"field_order": [
|
||||
"input_value"
|
||||
],
|
||||
"beta": false,
|
||||
"edited": false
|
||||
},
|
||||
"id": "TextInput-tcoZg"
|
||||
},
|
||||
"selected": true,
|
||||
"width": 384,
|
||||
"height": 309,
|
||||
"positionAbsolute": {
|
||||
"x": 186,
|
||||
"y": 549.296875
|
||||
},
|
||||
"dragging": false
|
||||
},
|
||||
{
|
||||
"id": "ChatOutput-dJRst",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 820,
|
||||
"y": 224.296875
|
||||
},
|
||||
"data": {
|
||||
"type": "ChatOutput",
|
||||
"node": {
|
||||
"template": {
|
||||
"_type": "Component",
|
||||
"code": {
|
||||
"type": "code",
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"list": false,
|
||||
"show": true,
|
||||
"multiline": true,
|
||||
"value": "from langflow.base.io.chat import ChatComponent\nfrom langflow.inputs import BoolInput\nfrom langflow.io import DropdownInput, MessageTextInput, Output\nfrom langflow.memory import store_message\nfrom langflow.schema.message import Message\n\n\nclass ChatOutput(ChatComponent):\n display_name = \"Chat Output\"\n description = \"Display a chat message in the Playground.\"\n icon = \"ChatOutput\"\n name = \"ChatOutput\"\n\n inputs = [\n MessageTextInput(\n name=\"input_value\",\n display_name=\"Text\",\n info=\"Message to be passed as output.\",\n ),\n BoolInput(\n name=\"store_message\",\n display_name=\"Store Messages\",\n info=\"Store the message in the history.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[\"Machine\", \"User\"],\n value=\"Machine\",\n advanced=True,\n info=\"Type of sender.\",\n ),\n MessageTextInput(\n name=\"sender_name\", display_name=\"Sender Name\", info=\"Name of the sender.\", value=\"AI\", advanced=True\n ),\n MessageTextInput(\n name=\"session_id\", display_name=\"Session ID\", info=\"Session ID for the message.\", advanced=True\n ),\n MessageTextInput(\n name=\"data_template\",\n display_name=\"Data Template\",\n value=\"{text}\",\n advanced=True,\n info=\"Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.\",\n ),\n ]\n outputs = [\n Output(display_name=\"Message\", name=\"message\", method=\"message_response\"),\n ]\n\n def message_response(self) -> Message:\n message = Message(\n text=self.input_value,\n sender=self.sender,\n sender_name=self.sender_name,\n session_id=self.session_id,\n )\n if self.session_id and isinstance(message, Message) and isinstance(message.text, str):\n store_message(\n message,\n flow_id=self.graph.flow_id,\n )\n self.message.value = message\n\n self.status = message\n return message\n",
|
||||
"fileTypes": [],
|
||||
"file_path": "",
|
||||
"password": false,
|
||||
"name": "code",
|
||||
"advanced": true,
|
||||
"dynamic": true,
|
||||
"info": "",
|
||||
"load_from_db": false,
|
||||
"title_case": false
|
||||
},
|
||||
"data_template": {
|
||||
"trace_as_input": true,
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "{text}",
|
||||
"name": "data_template",
|
||||
"display_name": "Data Template",
|
||||
"advanced": true,
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
},
|
||||
"input_value": {
|
||||
"trace_as_input": true,
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "",
|
||||
"name": "input_value",
|
||||
"display_name": "Text",
|
||||
"advanced": false,
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "Message to be passed as output.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
},
|
||||
"sender": {
|
||||
"trace_as_metadata": true,
|
||||
"options": [
|
||||
"Machine",
|
||||
"User"
|
||||
],
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "Machine",
|
||||
"name": "sender",
|
||||
"display_name": "Sender Type",
|
||||
"advanced": true,
|
||||
"dynamic": false,
|
||||
"info": "Type of sender.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
},
|
||||
"sender_name": {
|
||||
"trace_as_input": true,
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "",
|
||||
"name": "sender_name",
|
||||
"display_name": "Sender Name",
|
||||
"advanced": true,
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "Name of the sender.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
},
|
||||
"session_id": {
|
||||
"trace_as_input": true,
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "",
|
||||
"name": "session_id",
|
||||
"display_name": "Session ID",
|
||||
"advanced": true,
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "Session ID for the message.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
},
|
||||
"store_message": {
|
||||
"trace_as_metadata": true,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": true,
|
||||
"name": "store_message",
|
||||
"display_name": "Store Messages",
|
||||
"advanced": true,
|
||||
"dynamic": false,
|
||||
"info": "Store the message in the history.",
|
||||
"title_case": false,
|
||||
"type": "bool"
|
||||
}
|
||||
},
|
||||
"description": "Display a chat message in the Playground.",
|
||||
"icon": "ChatOutput",
|
||||
"base_classes": [
|
||||
"Message"
|
||||
],
|
||||
"display_name": "Chat Output",
|
||||
"documentation": "",
|
||||
"custom_fields": {},
|
||||
"output_types": [],
|
||||
"pinned": false,
|
||||
"conditional_paths": [],
|
||||
"frozen": false,
|
||||
"outputs": [
|
||||
{
|
||||
"types": [
|
||||
"Message"
|
||||
],
|
||||
"selected": "Message",
|
||||
"name": "message",
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
"value": "__UNDEFINED__",
|
||||
"cache": true
|
||||
}
|
||||
],
|
||||
"field_order": [
|
||||
"input_value",
|
||||
"store_message",
|
||||
"sender",
|
||||
"sender_name",
|
||||
"session_id",
|
||||
"data_template"
|
||||
],
|
||||
"beta": false,
|
||||
"edited": false
|
||||
},
|
||||
"id": "ChatOutput-dJRst",
|
||||
"description": "Display a chat message in the Playground.",
|
||||
"display_name": "Chat Output"
|
||||
},
|
||||
"selected": false,
|
||||
"width": 384,
|
||||
"height": 403,
|
||||
"positionAbsolute": {
|
||||
"x": 820,
|
||||
"y": 224.296875
|
||||
},
|
||||
"dragging": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "ChatInput-irFJf",
|
||||
"sourceHandle": "{œdataTypeœ:œChatInputœ,œidœ:œChatInput-irFJfœ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}",
|
||||
"target": "ChatOutput-dJRst",
|
||||
"targetHandle": "{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-dJRstœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}",
|
||||
"data": {
|
||||
"targetHandle": {
|
||||
"fieldName": "input_value",
|
||||
"id": "ChatOutput-dJRst",
|
||||
"inputTypes": [
|
||||
"Message"
|
||||
],
|
||||
"type": "str"
|
||||
},
|
||||
"sourceHandle": {
|
||||
"dataType": "ChatInput",
|
||||
"id": "ChatInput-irFJf",
|
||||
"name": "message",
|
||||
"output_types": [
|
||||
"Message"
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "reactflow__edge-ChatInput-irFJf{œdataTypeœ:œChatInputœ,œidœ:œChatInput-irFJfœ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-dJRst{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-dJRstœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}",
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"source": "TextInput-tcoZg",
|
||||
"sourceHandle": "{œdataTypeœ:œTextInputœ,œidœ:œTextInput-tcoZgœ,œnameœ:œtextœ,œoutput_typesœ:[œMessageœ]}",
|
||||
"target": "ChatOutput-dJRst",
|
||||
"targetHandle": "{œfieldNameœ:œsender_nameœ,œidœ:œChatOutput-dJRstœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}",
|
||||
"data": {
|
||||
"targetHandle": {
|
||||
"fieldName": "sender_name",
|
||||
"id": "ChatOutput-dJRst",
|
||||
"inputTypes": [
|
||||
"Message"
|
||||
],
|
||||
"type": "str"
|
||||
},
|
||||
"sourceHandle": {
|
||||
"dataType": "TextInput",
|
||||
"id": "TextInput-tcoZg",
|
||||
"name": "text",
|
||||
"output_types": [
|
||||
"Message"
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "reactflow__edge-TextInput-tcoZg{œdataTypeœ:œTextInputœ,œidœ:œTextInput-tcoZgœ,œnameœ:œtextœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-dJRst{œfieldNameœ:œsender_nameœ,œidœ:œChatOutput-dJRstœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}",
|
||||
"className": ""
|
||||
}
|
||||
],
|
||||
"viewport": {
|
||||
"x": -117,
|
||||
"y": -69,
|
||||
"zoom": 1
|
||||
}
|
||||
},
|
||||
"description": "Nurture NLP Nodes Here.",
|
||||
"name": "Simple API Test",
|
||||
"last_tested_version": "1.0.9",
|
||||
"endpoint_name": null,
|
||||
"is_component": false
|
||||
}
|
||||
1024
src/backend/tests/data/TwoOutputsTest.json
Normal file
1024
src/backend/tests/data/TwoOutputsTest.json
Normal file
File diff suppressed because it is too large
Load diff
1283
src/backend/tests/data/Vector_store.json
Normal file
1283
src/backend/tests/data/Vector_store.json
Normal file
File diff suppressed because it is too large
Load diff
749
src/backend/tests/data/WebhookTest.json
Normal file
749
src/backend/tests/data/WebhookTest.json
Normal file
|
|
@ -0,0 +1,749 @@
|
|||
{
|
||||
"id": "b00c375e-c858-42b5-a352-561d3f40bd15",
|
||||
"data": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "CustomComponent-aF0h1",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 888.0012384532345,
|
||||
"y": 272.41352212880344
|
||||
},
|
||||
"data": {
|
||||
"type": "CustomComponent",
|
||||
"node": {
|
||||
"template": {
|
||||
"_type": "Component",
|
||||
"code": {
|
||||
"type": "code",
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"list": false,
|
||||
"show": true,
|
||||
"multiline": true,
|
||||
"value": "# from langflow.field_typing import Data\nfrom langflow.custom import Component\nfrom langflow.io import StrInput\nfrom langflow.schema import Data\nfrom langflow.io import Output\nfrom pathlib import Path\nimport aiofiles\n\nclass CustomComponent(Component):\n display_name = \"Async Component\"\n description = \"Use as a template to create your own component.\"\n documentation: str = \"http://docs.langflow.org/components/custom\"\n icon = \"custom_components\"\n\n inputs = [\n StrInput(name=\"input_value\", display_name=\"Input Value\", value=\"Hello, World!\", input_types=[\"Data\"]),\n ]\n\n outputs = [\n Output(display_name=\"Output\", name=\"output\", method=\"build_output\"),\n ]\n\n async def build_output(self) -> Data:\n if isinstance(self.input_value, Data):\n data = self.input_value\n else:\n data = Data(value=self.input_value)\n \n if \"path\" in data:\n path = self.resolve_path(data.path)\n path_obj = Path(path)\n async with aiofiles.open(path, \"w\") as f:\n await f.write(data.model_dump())\n \n self.status = data\n return data",
|
||||
"fileTypes": [],
|
||||
"file_path": "",
|
||||
"password": false,
|
||||
"name": "code",
|
||||
"advanced": true,
|
||||
"dynamic": true,
|
||||
"info": "",
|
||||
"load_from_db": false,
|
||||
"title_case": false
|
||||
},
|
||||
"input_value": {
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "Hello, World!",
|
||||
"name": "input_value",
|
||||
"display_name": "Input Value",
|
||||
"advanced": false,
|
||||
"input_types": [
|
||||
"Data"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
}
|
||||
},
|
||||
"description": "Use as a template to create your own component.",
|
||||
"icon": "custom_components",
|
||||
"base_classes": [
|
||||
"Data"
|
||||
],
|
||||
"display_name": "Custom Component",
|
||||
"documentation": "http://docs.langflow.org/components/custom",
|
||||
"custom_fields": {},
|
||||
"output_types": [],
|
||||
"pinned": false,
|
||||
"conditional_paths": [],
|
||||
"frozen": false,
|
||||
"outputs": [
|
||||
{
|
||||
"types": [
|
||||
"Data"
|
||||
],
|
||||
"selected": "Data",
|
||||
"name": "output",
|
||||
"display_name": "Output",
|
||||
"method": "build_output",
|
||||
"value": "__UNDEFINED__",
|
||||
"cache": true,
|
||||
"hidden": false
|
||||
}
|
||||
],
|
||||
"field_order": [
|
||||
"input_value"
|
||||
],
|
||||
"beta": false,
|
||||
"edited": false
|
||||
},
|
||||
"id": "CustomComponent-aF0h1",
|
||||
"description": "Use as a template to create your own component.",
|
||||
"display_name": "Custom Component"
|
||||
},
|
||||
"selected": false,
|
||||
"width": 384,
|
||||
"height": 337,
|
||||
"positionAbsolute": {
|
||||
"x": 888.0012384532345,
|
||||
"y": 272.41352212880344
|
||||
},
|
||||
"dragging": false
|
||||
},
|
||||
{
|
||||
"id": "Webhook-BeRcd",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 418,
|
||||
"y": 270.2890625
|
||||
},
|
||||
"data": {
|
||||
"type": "Webhook",
|
||||
"node": {
|
||||
"template": {
|
||||
"_type": "Component",
|
||||
"code": {
|
||||
"type": "code",
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"list": false,
|
||||
"show": true,
|
||||
"multiline": true,
|
||||
"value": "import json\n\nfrom langflow.custom import Component\nfrom langflow.io import MultilineInput, Output\nfrom langflow.schema import Data\n\n\nclass WebhookComponent(Component):\n display_name = \"Webhook Input\"\n description = \"Defines a webhook input for the flow.\"\n name = \"Webhook\"\n\n inputs = [\n MultilineInput(\n name=\"data\",\n display_name=\"Data\",\n info=\"Use this field to quickly test the webhook component by providing a JSON payload.\",\n )\n ]\n outputs = [\n Output(display_name=\"Data\", name=\"output_data\", method=\"build_data\"),\n ]\n\n def build_data(self) -> Data:\n message: str | Data = \"\"\n if not self.data:\n self.status = \"No data provided.\"\n return Data(data={})\n try:\n body = json.loads(self.data or \"{}\")\n except json.JSONDecodeError:\n body = {\"payload\": self.data}\n message = f\"Invalid JSON payload. Please check the format.\\n\\n{self.data}\"\n data = Data(data=body)\n if not message:\n message = data\n self.status = message\n return data\n",
|
||||
"fileTypes": [],
|
||||
"file_path": "",
|
||||
"password": false,
|
||||
"name": "code",
|
||||
"advanced": true,
|
||||
"dynamic": true,
|
||||
"info": "",
|
||||
"load_from_db": false,
|
||||
"title_case": false
|
||||
},
|
||||
"data": {
|
||||
"trace_as_input": true,
|
||||
"multiline": true,
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "{\"test\":1}",
|
||||
"name": "data",
|
||||
"display_name": "Data",
|
||||
"advanced": false,
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "Use this field to quickly test the webhook component by providing a JSON payload.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
}
|
||||
},
|
||||
"description": "Defines a webhook input for the flow.",
|
||||
"base_classes": [
|
||||
"Data"
|
||||
],
|
||||
"display_name": "Webhook Input",
|
||||
"documentation": "",
|
||||
"custom_fields": {},
|
||||
"output_types": [],
|
||||
"pinned": false,
|
||||
"conditional_paths": [],
|
||||
"frozen": false,
|
||||
"outputs": [
|
||||
{
|
||||
"types": [
|
||||
"Data"
|
||||
],
|
||||
"selected": "Data",
|
||||
"name": "output_data",
|
||||
"display_name": "Data",
|
||||
"method": "build_data",
|
||||
"value": "__UNDEFINED__",
|
||||
"cache": true,
|
||||
"hidden": false
|
||||
}
|
||||
],
|
||||
"field_order": [
|
||||
"data"
|
||||
],
|
||||
"beta": false,
|
||||
"edited": false
|
||||
},
|
||||
"id": "Webhook-BeRcd",
|
||||
"description": "Defines a webhook input for the flow.",
|
||||
"display_name": "Webhook Input"
|
||||
},
|
||||
"selected": false,
|
||||
"width": 384,
|
||||
"height": 309,
|
||||
"dragging": true,
|
||||
"positionAbsolute": {
|
||||
"x": 418,
|
||||
"y": 270.2890625
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ChatInput-QivBB",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 419.7235078147726,
|
||||
"y": 646.9863203129902
|
||||
},
|
||||
"data": {
|
||||
"type": "ChatInput",
|
||||
"node": {
|
||||
"template": {
|
||||
"_type": "Component",
|
||||
"files": {
|
||||
"trace_as_metadata": true,
|
||||
"file_path": "",
|
||||
"fileTypes": [
|
||||
"txt",
|
||||
"md",
|
||||
"mdx",
|
||||
"csv",
|
||||
"json",
|
||||
"yaml",
|
||||
"yml",
|
||||
"xml",
|
||||
"html",
|
||||
"htm",
|
||||
"pdf",
|
||||
"docx",
|
||||
"py",
|
||||
"sh",
|
||||
"sql",
|
||||
"js",
|
||||
"ts",
|
||||
"tsx",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"png",
|
||||
"bmp",
|
||||
"image"
|
||||
],
|
||||
"list": true,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "",
|
||||
"name": "files",
|
||||
"display_name": "Files",
|
||||
"advanced": true,
|
||||
"dynamic": false,
|
||||
"info": "Files to be sent with the message.",
|
||||
"title_case": false,
|
||||
"type": "file"
|
||||
},
|
||||
"code": {
|
||||
"type": "code",
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"list": false,
|
||||
"show": true,
|
||||
"multiline": true,
|
||||
"value": "from langflow.base.data.utils import IMG_FILE_TYPES, TEXT_FILE_TYPES\nfrom langflow.base.io.chat import ChatComponent\nfrom langflow.io import DropdownInput, FileInput, MessageTextInput, MultilineInput, Output\nfrom langflow.schema.message import Message\n\n\nclass ChatInput(ChatComponent):\n display_name = \"Chat Input\"\n description = \"Get chat inputs from the Playground.\"\n icon = \"ChatInput\"\n name = \"ChatInput\"\n\n inputs = [\n MultilineInput(\n name=\"input_value\",\n display_name=\"Text\",\n value=\"\",\n info=\"Message to be passed as input.\",\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[\"Machine\", \"User\"],\n value=\"User\",\n info=\"Type of sender.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Name of the sender.\",\n value=\"User\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"session_id\", display_name=\"Session ID\", info=\"Session ID for the message.\", advanced=True\n ),\n FileInput(\n name=\"files\",\n display_name=\"Files\",\n file_types=TEXT_FILE_TYPES + IMG_FILE_TYPES,\n info=\"Files to be sent with the message.\",\n advanced=True,\n is_list=True,\n ),\n ]\n outputs = [\n Output(display_name=\"Message\", name=\"message\", method=\"message_response\"),\n ]\n\n def message_response(self) -> Message:\n message = Message(\n text=self.input_value,\n sender=self.sender,\n sender_name=self.sender_name,\n session_id=self.session_id,\n files=self.files,\n )\n if self.session_id and isinstance(message, Message) and isinstance(message.text, str):\n self.store_message(message)\n self.message.value = message\n\n self.status = message\n return message\n",
|
||||
"fileTypes": [],
|
||||
"file_path": "",
|
||||
"password": false,
|
||||
"name": "code",
|
||||
"advanced": true,
|
||||
"dynamic": true,
|
||||
"info": "",
|
||||
"load_from_db": false,
|
||||
"title_case": false
|
||||
},
|
||||
"input_value": {
|
||||
"trace_as_input": true,
|
||||
"multiline": true,
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "Should not run",
|
||||
"name": "input_value",
|
||||
"display_name": "Text",
|
||||
"advanced": false,
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "Message to be passed as input.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
},
|
||||
"sender": {
|
||||
"trace_as_metadata": true,
|
||||
"options": [
|
||||
"Machine",
|
||||
"User"
|
||||
],
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "User",
|
||||
"name": "sender",
|
||||
"display_name": "Sender Type",
|
||||
"advanced": true,
|
||||
"dynamic": false,
|
||||
"info": "Type of sender.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
},
|
||||
"sender_name": {
|
||||
"trace_as_input": true,
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "User",
|
||||
"name": "sender_name",
|
||||
"display_name": "Sender Name",
|
||||
"advanced": true,
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "Name of the sender.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
},
|
||||
"session_id": {
|
||||
"trace_as_input": true,
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "",
|
||||
"name": "session_id",
|
||||
"display_name": "Session ID",
|
||||
"advanced": true,
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "Session ID for the message.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
}
|
||||
},
|
||||
"description": "Get chat inputs from the Playground.",
|
||||
"icon": "ChatInput",
|
||||
"base_classes": [
|
||||
"Message"
|
||||
],
|
||||
"display_name": "Chat Input",
|
||||
"documentation": "",
|
||||
"custom_fields": {},
|
||||
"output_types": [],
|
||||
"pinned": false,
|
||||
"conditional_paths": [],
|
||||
"frozen": false,
|
||||
"outputs": [
|
||||
{
|
||||
"types": [
|
||||
"Message"
|
||||
],
|
||||
"selected": "Message",
|
||||
"name": "message",
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
"value": "__UNDEFINED__",
|
||||
"cache": true,
|
||||
"hidden": false
|
||||
}
|
||||
],
|
||||
"field_order": [
|
||||
"input_value",
|
||||
"sender",
|
||||
"sender_name",
|
||||
"session_id",
|
||||
"files"
|
||||
],
|
||||
"beta": false,
|
||||
"edited": false
|
||||
},
|
||||
"id": "ChatInput-QivBB"
|
||||
},
|
||||
"selected": false,
|
||||
"width": 384,
|
||||
"height": 309,
|
||||
"positionAbsolute": {
|
||||
"x": 419.7235078147726,
|
||||
"y": 646.9863203129902
|
||||
},
|
||||
"dragging": false
|
||||
},
|
||||
{
|
||||
"id": "ChatOutput-mN2VY",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 884.7327265656637,
|
||||
"y": 662.4287265670896
|
||||
},
|
||||
"data": {
|
||||
"type": "ChatOutput",
|
||||
"node": {
|
||||
"template": {
|
||||
"_type": "Component",
|
||||
"code": {
|
||||
"type": "code",
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"list": false,
|
||||
"show": true,
|
||||
"multiline": true,
|
||||
"value": "from langflow.base.io.chat import ChatComponent\nfrom langflow.io import DropdownInput, MessageTextInput, Output\nfrom langflow.schema.message import Message\n\n\nclass ChatOutput(ChatComponent):\n display_name = \"Chat Output\"\n description = \"Display a chat message in the Playground.\"\n icon = \"ChatOutput\"\n name = \"ChatOutput\"\n\n inputs = [\n MessageTextInput(\n name=\"input_value\",\n display_name=\"Text\",\n info=\"Message to be passed as output.\",\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[\"Machine\", \"User\"],\n value=\"Machine\",\n advanced=True,\n info=\"Type of sender.\",\n ),\n MessageTextInput(\n name=\"sender_name\", display_name=\"Sender Name\", info=\"Name of the sender.\", value=\"AI\", advanced=True\n ),\n MessageTextInput(\n name=\"session_id\", display_name=\"Session ID\", info=\"Session ID for the message.\", advanced=True\n ),\n MessageTextInput(\n name=\"data_template\",\n display_name=\"Data Template\",\n value=\"{text}\",\n advanced=True,\n info=\"Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.\",\n ),\n ]\n outputs = [\n Output(display_name=\"Message\", name=\"message\", method=\"message_response\"),\n ]\n\n def message_response(self) -> Message:\n message = Message(\n text=self.input_value,\n sender=self.sender,\n sender_name=self.sender_name,\n session_id=self.session_id,\n )\n if self.session_id and isinstance(message, Message) and isinstance(message.text, str):\n self.store_message(message)\n self.message.value = message\n\n self.status = message\n return message\n",
|
||||
"fileTypes": [],
|
||||
"file_path": "",
|
||||
"password": false,
|
||||
"name": "code",
|
||||
"advanced": true,
|
||||
"dynamic": true,
|
||||
"info": "",
|
||||
"load_from_db": false,
|
||||
"title_case": false
|
||||
},
|
||||
"data_template": {
|
||||
"trace_as_input": true,
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "{text}",
|
||||
"name": "data_template",
|
||||
"display_name": "Data Template",
|
||||
"advanced": true,
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
},
|
||||
"input_value": {
|
||||
"trace_as_input": true,
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "",
|
||||
"name": "input_value",
|
||||
"display_name": "Text",
|
||||
"advanced": false,
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "Message to be passed as output.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
},
|
||||
"sender": {
|
||||
"trace_as_metadata": true,
|
||||
"options": [
|
||||
"Machine",
|
||||
"User"
|
||||
],
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "Machine",
|
||||
"name": "sender",
|
||||
"display_name": "Sender Type",
|
||||
"advanced": true,
|
||||
"dynamic": false,
|
||||
"info": "Type of sender.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
},
|
||||
"sender_name": {
|
||||
"trace_as_input": true,
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "AI",
|
||||
"name": "sender_name",
|
||||
"display_name": "Sender Name",
|
||||
"advanced": true,
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "Name of the sender.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
},
|
||||
"session_id": {
|
||||
"trace_as_input": true,
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "",
|
||||
"name": "session_id",
|
||||
"display_name": "Session ID",
|
||||
"advanced": true,
|
||||
"input_types": [
|
||||
"Message"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "Session ID for the message.",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
}
|
||||
},
|
||||
"description": "Display a chat message in the Playground.",
|
||||
"icon": "ChatOutput",
|
||||
"base_classes": [
|
||||
"Message"
|
||||
],
|
||||
"display_name": "Chat Output",
|
||||
"documentation": "",
|
||||
"custom_fields": {},
|
||||
"output_types": [],
|
||||
"pinned": false,
|
||||
"conditional_paths": [],
|
||||
"frozen": false,
|
||||
"outputs": [
|
||||
{
|
||||
"types": [
|
||||
"Message"
|
||||
],
|
||||
"selected": "Message",
|
||||
"name": "message",
|
||||
"display_name": "Message",
|
||||
"method": "message_response",
|
||||
"value": "__UNDEFINED__",
|
||||
"cache": true
|
||||
}
|
||||
],
|
||||
"field_order": [
|
||||
"input_value",
|
||||
"sender",
|
||||
"sender_name",
|
||||
"session_id",
|
||||
"data_template"
|
||||
],
|
||||
"beta": false,
|
||||
"edited": false
|
||||
},
|
||||
"id": "ChatOutput-mN2VY"
|
||||
},
|
||||
"selected": false,
|
||||
"width": 384,
|
||||
"height": 309,
|
||||
"positionAbsolute": {
|
||||
"x": 884.7327265656637,
|
||||
"y": 662.4287265670896
|
||||
},
|
||||
"dragging": false
|
||||
},
|
||||
{
|
||||
"id": "CustomComponent-Ntw7h",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 1396.7134608749789,
|
||||
"y": 284.91367968123217
|
||||
},
|
||||
"data": {
|
||||
"type": "CustomComponent",
|
||||
"node": {
|
||||
"template": {
|
||||
"_type": "Component",
|
||||
"code": {
|
||||
"type": "code",
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"list": false,
|
||||
"show": true,
|
||||
"multiline": true,
|
||||
"value": "# from langflow.field_typing import Data\nfrom langflow.custom import Component\nfrom langflow.io import StrInput\nfrom langflow.schema import Data\nfrom langflow.io import Output\nfrom pathlib import Path\nimport httpx\nclass CustomComponent(Component):\n display_name = \"Async Component\"\n description = \"Use as a template to create your own component.\"\n documentation: str = \"http://docs.langflow.org/components/custom\"\n icon = \"custom_components\"\n\n inputs = [\n StrInput(name=\"input_value\", display_name=\"Input Value\", value=\"Hello, World!\", input_types=[\"Data\"]),\n ]\n\n outputs = [\n Output(display_name=\"Output\", name=\"output\", method=\"build_output\"),\n ]\n\n async def build_output(self) -> Data:\n async with httpx.AsyncClient() as client:\n response = await client.get(\"https://www.google.com\")\n response.raise_for_status()\n return Data(response=response.text)",
|
||||
"fileTypes": [],
|
||||
"file_path": "",
|
||||
"password": false,
|
||||
"name": "code",
|
||||
"advanced": true,
|
||||
"dynamic": true,
|
||||
"info": "",
|
||||
"load_from_db": false,
|
||||
"title_case": false
|
||||
},
|
||||
"input_value": {
|
||||
"trace_as_metadata": true,
|
||||
"load_from_db": false,
|
||||
"list": false,
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"value": "Hello, World!",
|
||||
"name": "input_value",
|
||||
"display_name": "Input Value",
|
||||
"advanced": false,
|
||||
"input_types": [
|
||||
"Data"
|
||||
],
|
||||
"dynamic": false,
|
||||
"info": "",
|
||||
"title_case": false,
|
||||
"type": "str"
|
||||
}
|
||||
},
|
||||
"description": "Use as a template to create your own component.",
|
||||
"icon": "custom_components",
|
||||
"base_classes": [],
|
||||
"display_name": "Custom Component",
|
||||
"documentation": "http://docs.langflow.org/components/custom",
|
||||
"custom_fields": {},
|
||||
"output_types": [],
|
||||
"pinned": false,
|
||||
"conditional_paths": [],
|
||||
"frozen": false,
|
||||
"outputs": [
|
||||
{
|
||||
"types": [],
|
||||
"name": "output",
|
||||
"display_name": "Output",
|
||||
"method": "build_output",
|
||||
"value": "__UNDEFINED__",
|
||||
"cache": true
|
||||
}
|
||||
],
|
||||
"field_order": [
|
||||
"input_value"
|
||||
],
|
||||
"beta": false,
|
||||
"edited": true
|
||||
},
|
||||
"id": "CustomComponent-Ntw7h",
|
||||
"description": "Use as a template to create your own component.",
|
||||
"display_name": "Custom Component"
|
||||
},
|
||||
"selected": true,
|
||||
"width": 384,
|
||||
"height": 337,
|
||||
"positionAbsolute": {
|
||||
"x": 1396.7134608749789,
|
||||
"y": 284.91367968123217
|
||||
},
|
||||
"dragging": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "Webhook-BeRcd",
|
||||
"sourceHandle": "{œdataTypeœ:œWebhookœ,œidœ:œWebhook-BeRcdœ,œnameœ:œoutput_dataœ,œoutput_typesœ:[œDataœ]}",
|
||||
"target": "CustomComponent-aF0h1",
|
||||
"targetHandle": "{œfieldNameœ:œinput_valueœ,œidœ:œCustomComponent-aF0h1œ,œinputTypesœ:[œDataœ],œtypeœ:œstrœ}",
|
||||
"data": {
|
||||
"targetHandle": {
|
||||
"fieldName": "input_value",
|
||||
"id": "CustomComponent-aF0h1",
|
||||
"inputTypes": [
|
||||
"Data"
|
||||
],
|
||||
"type": "str"
|
||||
},
|
||||
"sourceHandle": {
|
||||
"dataType": "Webhook",
|
||||
"id": "Webhook-BeRcd",
|
||||
"name": "output_data",
|
||||
"output_types": [
|
||||
"Data"
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "reactflow__edge-Webhook-BeRcd{œdataTypeœ:œWebhookœ,œidœ:œWebhook-BeRcdœ,œnameœ:œoutput_dataœ,œoutput_typesœ:[œDataœ]}-CustomComponent-aF0h1{œfieldNameœ:œinput_valueœ,œidœ:œCustomComponent-aF0h1œ,œinputTypesœ:[œDataœ],œtypeœ:œstrœ}",
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"source": "ChatInput-QivBB",
|
||||
"sourceHandle": "{œdataTypeœ:œChatInputœ,œidœ:œChatInput-QivBBœ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}",
|
||||
"target": "ChatOutput-mN2VY",
|
||||
"targetHandle": "{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-mN2VYœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}",
|
||||
"data": {
|
||||
"targetHandle": {
|
||||
"fieldName": "input_value",
|
||||
"id": "ChatOutput-mN2VY",
|
||||
"inputTypes": [
|
||||
"Message"
|
||||
],
|
||||
"type": "str"
|
||||
},
|
||||
"sourceHandle": {
|
||||
"dataType": "ChatInput",
|
||||
"id": "ChatInput-QivBB",
|
||||
"name": "message",
|
||||
"output_types": [
|
||||
"Message"
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "reactflow__edge-ChatInput-QivBB{œdataTypeœ:œChatInputœ,œidœ:œChatInput-QivBBœ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-mN2VY{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-mN2VYœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}",
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"source": "CustomComponent-aF0h1",
|
||||
"sourceHandle": "{œdataTypeœ:œCustomComponentœ,œidœ:œCustomComponent-aF0h1œ,œnameœ:œoutputœ,œoutput_typesœ:[œDataœ]}",
|
||||
"target": "CustomComponent-Ntw7h",
|
||||
"targetHandle": "{œfieldNameœ:œinput_valueœ,œidœ:œCustomComponent-Ntw7hœ,œinputTypesœ:[œDataœ],œtypeœ:œstrœ}",
|
||||
"data": {
|
||||
"targetHandle": {
|
||||
"fieldName": "input_value",
|
||||
"id": "CustomComponent-Ntw7h",
|
||||
"inputTypes": [
|
||||
"Data"
|
||||
],
|
||||
"type": "str"
|
||||
},
|
||||
"sourceHandle": {
|
||||
"dataType": "CustomComponent",
|
||||
"id": "CustomComponent-aF0h1",
|
||||
"name": "output",
|
||||
"output_types": [
|
||||
"Data"
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "reactflow__edge-CustomComponent-aF0h1{œdataTypeœ:œCustomComponentœ,œidœ:œCustomComponent-aF0h1œ,œnameœ:œoutputœ,œoutput_typesœ:[œDataœ]}-CustomComponent-Ntw7h{œfieldNameœ:œinput_valueœ,œidœ:œCustomComponent-Ntw7hœ,œinputTypesœ:[œDataœ],œtypeœ:œstrœ}"
|
||||
}
|
||||
],
|
||||
"viewport": {
|
||||
"x": -7.6743264594028915,
|
||||
"y": 186.6544574916296,
|
||||
"zoom": 0.520510798842055
|
||||
}
|
||||
},
|
||||
"description": "The Power of Language at Your Fingertips.",
|
||||
"name": "Webhook Test",
|
||||
"last_tested_version": "1.0.7",
|
||||
"endpoint_name": "webhook-test",
|
||||
"is_component": false
|
||||
}
|
||||
510
src/backend/tests/data/basic_example.json
Normal file
510
src/backend/tests/data/basic_example.json
Normal file
|
|
@ -0,0 +1,510 @@
|
|||
{
|
||||
"description": "",
|
||||
"name": "BasicExample",
|
||||
"id": "a53f9130-f2fa-4a3e-b22a-3856d946351a",
|
||||
"data": {
|
||||
"nodes": [
|
||||
{
|
||||
"width": 384,
|
||||
"height": 267,
|
||||
"id": "dndnode_81",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 1030,
|
||||
"y": 694
|
||||
},
|
||||
"data": {
|
||||
"type": "TimeTravelGuideChain",
|
||||
"node": {
|
||||
"template": {
|
||||
"llm": {
|
||||
"required": true,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "llm",
|
||||
"advanced": false,
|
||||
"type": "BaseLanguageModel",
|
||||
"list": false
|
||||
},
|
||||
"memory": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "memory",
|
||||
"advanced": false,
|
||||
"type": "BaseChatMemory",
|
||||
"list": false
|
||||
},
|
||||
"_type": "TimeTravelGuideChain"
|
||||
},
|
||||
"description": "Time travel guide chain to be used in the flow.",
|
||||
"base_classes": [
|
||||
"LLMChain",
|
||||
"BaseCustomChain",
|
||||
"TimeTravelGuideChain",
|
||||
"Chain",
|
||||
"ConversationChain"
|
||||
]
|
||||
},
|
||||
"id": "dndnode_81",
|
||||
"value": null
|
||||
},
|
||||
"selected": false,
|
||||
"positionAbsolute": {
|
||||
"x": 1030,
|
||||
"y": 694
|
||||
},
|
||||
"dragging": false
|
||||
},
|
||||
{
|
||||
"width": 384,
|
||||
"height": 597,
|
||||
"id": "dndnode_82",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 520,
|
||||
"y": 732
|
||||
},
|
||||
"data": {
|
||||
"type": "OpenAI",
|
||||
"node": {
|
||||
"template": {
|
||||
"cache": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "cache",
|
||||
"advanced": false,
|
||||
"type": "bool",
|
||||
"list": false
|
||||
},
|
||||
"verbose": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "verbose",
|
||||
"advanced": false,
|
||||
"type": "bool",
|
||||
"list": false
|
||||
},
|
||||
"callbacks": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "callbacks",
|
||||
"advanced": false,
|
||||
"type": "langchain.callbacks.base.BaseCallbackHandler",
|
||||
"list": true
|
||||
},
|
||||
"client": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "client",
|
||||
"advanced": false,
|
||||
"type": "Any",
|
||||
"list": false
|
||||
},
|
||||
"model_name": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"value": "text-davinci-003",
|
||||
"password": false,
|
||||
"options": [
|
||||
"text-davinci-003",
|
||||
"text-davinci-002",
|
||||
"text-curie-001",
|
||||
"text-babbage-001",
|
||||
"text-ada-001"
|
||||
],
|
||||
"name": "model_name",
|
||||
"advanced": false,
|
||||
"type": "str",
|
||||
"list": true
|
||||
},
|
||||
"temperature": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"value": 0.7,
|
||||
"password": false,
|
||||
"name": "temperature",
|
||||
"advanced": false,
|
||||
"type": "float",
|
||||
"list": false
|
||||
},
|
||||
"max_tokens": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"value": 256,
|
||||
"password": true,
|
||||
"name": "max_tokens",
|
||||
"advanced": false,
|
||||
"type": "int",
|
||||
"list": false
|
||||
},
|
||||
"top_p": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": 1,
|
||||
"password": false,
|
||||
"name": "top_p",
|
||||
"advanced": false,
|
||||
"type": "float",
|
||||
"list": false
|
||||
},
|
||||
"frequency_penalty": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": 0,
|
||||
"password": false,
|
||||
"name": "frequency_penalty",
|
||||
"advanced": false,
|
||||
"type": "float",
|
||||
"list": false
|
||||
},
|
||||
"presence_penalty": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": 0,
|
||||
"password": false,
|
||||
"name": "presence_penalty",
|
||||
"advanced": false,
|
||||
"type": "float",
|
||||
"list": false
|
||||
},
|
||||
"n": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": 1,
|
||||
"password": false,
|
||||
"name": "n",
|
||||
"advanced": false,
|
||||
"type": "int",
|
||||
"list": false
|
||||
},
|
||||
"best_of": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": 1,
|
||||
"password": false,
|
||||
"name": "best_of",
|
||||
"advanced": false,
|
||||
"type": "int",
|
||||
"list": false
|
||||
},
|
||||
"model_kwargs": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "model_kwargs",
|
||||
"advanced": true,
|
||||
"type": "code",
|
||||
"list": false
|
||||
},
|
||||
"openai_api_key": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"value": null,
|
||||
"password": true,
|
||||
"name": "openai_api_key",
|
||||
"display_name": "OpenAI API Key",
|
||||
"advanced": false,
|
||||
"type": "str",
|
||||
"list": false
|
||||
},
|
||||
"openai_api_base": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"password": true,
|
||||
"name": "openai_api_base",
|
||||
"advanced": false,
|
||||
"type": "str",
|
||||
"list": false
|
||||
},
|
||||
"openai_organization": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "openai_organization",
|
||||
"advanced": false,
|
||||
"type": "str",
|
||||
"list": false
|
||||
},
|
||||
"batch_size": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": 20,
|
||||
"password": false,
|
||||
"name": "batch_size",
|
||||
"advanced": false,
|
||||
"type": "int",
|
||||
"list": false
|
||||
},
|
||||
"request_timeout": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "request_timeout",
|
||||
"advanced": false,
|
||||
"type": "float",
|
||||
"list": false
|
||||
},
|
||||
"logit_bias": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "logit_bias",
|
||||
"advanced": false,
|
||||
"type": "code",
|
||||
"list": false
|
||||
},
|
||||
"max_retries": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": 6,
|
||||
"password": false,
|
||||
"name": "max_retries",
|
||||
"advanced": false,
|
||||
"type": "int",
|
||||
"list": false
|
||||
},
|
||||
"streaming": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": false,
|
||||
"password": false,
|
||||
"name": "streaming",
|
||||
"advanced": false,
|
||||
"type": "bool",
|
||||
"list": false
|
||||
},
|
||||
"allowed_special": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": [],
|
||||
"password": false,
|
||||
"name": "allowed_special",
|
||||
"advanced": false,
|
||||
"type": "Literal'all'",
|
||||
"list": true
|
||||
},
|
||||
"disallowed_special": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": "all",
|
||||
"password": false,
|
||||
"name": "disallowed_special",
|
||||
"advanced": false,
|
||||
"type": "Literal'all'",
|
||||
"list": false
|
||||
},
|
||||
"_type": "OpenAI"
|
||||
},
|
||||
"description": "Wrapper around OpenAI large language models.",
|
||||
"base_classes": [
|
||||
"BaseLLM",
|
||||
"OpenAI",
|
||||
"BaseOpenAI",
|
||||
"BaseLanguageModel"
|
||||
]
|
||||
},
|
||||
"id": "dndnode_82",
|
||||
"value": null
|
||||
},
|
||||
"selected": false,
|
||||
"positionAbsolute": {
|
||||
"x": 520,
|
||||
"y": 732
|
||||
},
|
||||
"dragging": false
|
||||
},
|
||||
{
|
||||
"width": 384,
|
||||
"height": 273,
|
||||
"id": "dndnode_83",
|
||||
"type": "genericNode",
|
||||
"position": {
|
||||
"x": 512,
|
||||
"y": 402
|
||||
},
|
||||
"data": {
|
||||
"type": "ConversationBufferMemory",
|
||||
"node": {
|
||||
"template": {
|
||||
"chat_memory": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "chat_memory",
|
||||
"advanced": false,
|
||||
"type": "BaseChatMessageHistory",
|
||||
"list": false
|
||||
},
|
||||
"output_key": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "output_key",
|
||||
"advanced": false,
|
||||
"type": "str",
|
||||
"list": false
|
||||
},
|
||||
"input_key": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "input_key",
|
||||
"advanced": false,
|
||||
"type": "str",
|
||||
"list": false
|
||||
},
|
||||
"return_messages": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"password": false,
|
||||
"name": "return_messages",
|
||||
"advanced": false,
|
||||
"type": "bool",
|
||||
"list": false
|
||||
},
|
||||
"human_prefix": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": "Human",
|
||||
"password": false,
|
||||
"name": "human_prefix",
|
||||
"advanced": false,
|
||||
"type": "str",
|
||||
"list": false
|
||||
},
|
||||
"ai_prefix": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": false,
|
||||
"multiline": false,
|
||||
"value": "AI",
|
||||
"password": false,
|
||||
"name": "ai_prefix",
|
||||
"advanced": false,
|
||||
"type": "str",
|
||||
"list": false
|
||||
},
|
||||
"memory_key": {
|
||||
"required": false,
|
||||
"placeholder": "",
|
||||
"show": true,
|
||||
"multiline": false,
|
||||
"value": "history",
|
||||
"password": false,
|
||||
"name": "memory_key",
|
||||
"advanced": false,
|
||||
"type": "str",
|
||||
"list": false
|
||||
},
|
||||
"_type": "ConversationBufferMemory"
|
||||
},
|
||||
"description": "Buffer for storing conversation memory.",
|
||||
"base_classes": [
|
||||
"ConversationBufferMemory",
|
||||
"BaseChatMemory",
|
||||
"BaseMemory"
|
||||
]
|
||||
},
|
||||
"id": "dndnode_83",
|
||||
"value": null
|
||||
},
|
||||
"selected": false,
|
||||
"positionAbsolute": {
|
||||
"x": 512,
|
||||
"y": 402
|
||||
},
|
||||
"dragging": false
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "dndnode_82",
|
||||
"sourceHandle": "OpenAI|dndnode_82|BaseLLM|OpenAI|BaseOpenAI|BaseLanguageModel",
|
||||
"target": "dndnode_81",
|
||||
"targetHandle": "BaseLanguageModel|llm|dndnode_81",
|
||||
"className": "animate-pulse",
|
||||
"id": "reactflow__edge-dndnode_82OpenAI|dndnode_82|BaseLLM|OpenAI|BaseOpenAI|BaseLanguageModel-dndnode_81BaseLanguageModel|llm|dndnode_81",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"source": "dndnode_83",
|
||||
"sourceHandle": "ConversationBufferMemory|dndnode_83|ConversationBufferMemory|BaseChatMemory|BaseMemory",
|
||||
"target": "dndnode_81",
|
||||
"targetHandle": "BaseChatMemory|memory|dndnode_81",
|
||||
"className": "animate-pulse",
|
||||
"id": "reactflow__edge-dndnode_83ConversationBufferMemory|dndnode_83|ConversationBufferMemory|BaseChatMemory|BaseMemory-dndnode_81BaseChatMemory|memory|dndnode_81"
|
||||
}
|
||||
],
|
||||
"viewport": {
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"zoom": 0.5
|
||||
}
|
||||
},
|
||||
"last_tested_version": "0.6.2"
|
||||
}
|
||||
1
src/backend/tests/data/complex_example.json
Normal file
1
src/backend/tests/data/complex_example.json
Normal file
File diff suppressed because one or more lines are too long
16
src/backend/tests/data/component.py
Normal file
16
src/backend/tests/data/component.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import random
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
|
||||
|
||||
class TestComponent(CustomComponent):
|
||||
def refresh_values(self):
|
||||
# This is a function that will be called every time the component is updated
|
||||
# and should return a list of random strings
|
||||
return [f"Random {random.randint(1, 100)}" for _ in range(5)]
|
||||
|
||||
def build_config(self):
|
||||
return {"param": {"display_name": "Param", "options": self.refresh_values}}
|
||||
|
||||
def build(self, param: int):
|
||||
return param
|
||||
20
src/backend/tests/data/component_multiple_outputs.py
Normal file
20
src/backend/tests/data/component_multiple_outputs.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from langflow.custom import Component
|
||||
from langflow.inputs.inputs import IntInput, MessageTextInput
|
||||
from langflow.template.field.base import Output
|
||||
|
||||
|
||||
class MultipleOutputsComponent(Component):
|
||||
inputs = [
|
||||
MessageTextInput(display_name="Input", name="input"),
|
||||
IntInput(display_name="Number", name="number"),
|
||||
]
|
||||
outputs = [
|
||||
Output(display_name="Certain Output", name="certain_output", method="certain_output"),
|
||||
Output(display_name="Other Output", name="other_output", method="other_output"),
|
||||
]
|
||||
|
||||
def certain_output(self) -> str:
|
||||
return f"This is my string input: {self.input}"
|
||||
|
||||
def other_output(self) -> int:
|
||||
return f"This is my int input multiplied by 2: {self.number * 2}"
|
||||
22
src/backend/tests/data/component_nested_call.py
Normal file
22
src/backend/tests/data/component_nested_call.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from random import randint
|
||||
|
||||
from langflow.custom import Component
|
||||
from langflow.inputs.inputs import IntInput, MessageTextInput
|
||||
from langflow.template.field.base import Output
|
||||
|
||||
|
||||
class MultipleOutputsComponent(Component):
|
||||
inputs = [
|
||||
MessageTextInput(display_name="Input", name="input"),
|
||||
IntInput(display_name="Number", name="number"),
|
||||
]
|
||||
outputs = [
|
||||
Output(display_name="Certain Output", name="certain_output", method="certain_output"),
|
||||
Output(display_name="Other Output", name="other_output", method="other_output"),
|
||||
]
|
||||
|
||||
def certain_output(self) -> int:
|
||||
return randint(0, self.number)
|
||||
|
||||
def other_output(self) -> int:
|
||||
return self.certain_output()
|
||||
17
src/backend/tests/data/component_with_templatefield.py
Normal file
17
src/backend/tests/data/component_with_templatefield.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import random
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import Input
|
||||
|
||||
|
||||
class TestComponent(CustomComponent):
|
||||
def refresh_values(self):
|
||||
# This is a function that will be called every time the component is updated
|
||||
# and should return a list of random strings
|
||||
return [f"Random {random.randint(1, 100)}" for _ in range(5)]
|
||||
|
||||
def build_config(self):
|
||||
return {"param": Input(display_name="Param", options=self.refresh_values)}
|
||||
|
||||
def build(self, param: int):
|
||||
return param
|
||||
1
src/backend/tests/data/grouped_chat.json
Normal file
1
src/backend/tests/data/grouped_chat.json
Normal file
File diff suppressed because one or more lines are too long
1302
src/backend/tests/data/one_group_chat.json
Normal file
1302
src/backend/tests/data/one_group_chat.json
Normal file
File diff suppressed because it is too large
Load diff
1
src/backend/tests/data/vector_store_grouped.json
Normal file
1
src/backend/tests/data/vector_store_grouped.json
Normal file
File diff suppressed because one or more lines are too long
0
src/backend/tests/integration/__init__.py
Normal file
0
src/backend/tests/integration/__init__.py
Normal file
0
src/backend/tests/integration/astra/__init__.py
Normal file
0
src/backend/tests/integration/astra/__init__.py
Normal file
290
src/backend/tests/integration/astra/test_astra_component.py
Normal file
290
src/backend/tests/integration/astra/test_astra_component.py
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
import os
|
||||
|
||||
from langchain_astradb import AstraDBVectorStore, CollectionVectorServiceOptions
|
||||
from langflow.components.embeddings.AstraVectorize import AstraVectorizeComponent
|
||||
import pytest
|
||||
from integration.utils import MockEmbeddings, check_env_vars
|
||||
from langchain_core.documents import Document
|
||||
|
||||
# from langflow.components.memories.AstraDBMessageReader import AstraDBMessageReaderComponent
|
||||
# from langflow.components.memories.AstraDBMessageWriter import AstraDBMessageWriterComponent
|
||||
from langflow.components.vectorstores.AstraDB import AstraVectorStoreComponent
|
||||
from langflow.schema.data import Data
|
||||
|
||||
COLLECTION = "test_basic"
|
||||
SEARCH_COLLECTION = "test_search"
|
||||
# MEMORY_COLLECTION = "test_memory"
|
||||
VECTORIZE_COLLECTION = "test_vectorize"
|
||||
VECTORIZE_COLLECTION_OPENAI = "test_vectorize_openai"
|
||||
VECTORIZE_COLLECTION_OPENAI_WITH_AUTH = "test_vectorize_openai_auth"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def astra_fixture(request):
|
||||
"""
|
||||
Sets up the astra collection and cleans up after
|
||||
"""
|
||||
try:
|
||||
from langchain_astradb import AstraDBVectorStore
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Could not import langchain Astra DB integration package. Please install it with `pip install langchain-astradb`."
|
||||
)
|
||||
|
||||
store = AstraDBVectorStore(
|
||||
collection_name=request.param,
|
||||
embedding=MockEmbeddings(),
|
||||
api_endpoint=os.getenv("ASTRA_DB_API_ENDPOINT"),
|
||||
token=os.getenv("ASTRA_DB_APPLICATION_TOKEN"),
|
||||
)
|
||||
|
||||
yield
|
||||
|
||||
store.delete_collection()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not check_env_vars("ASTRA_DB_APPLICATION_TOKEN", "ASTRA_DB_API_ENDPOINT"),
|
||||
reason="missing astra env vars",
|
||||
)
|
||||
@pytest.mark.parametrize("astra_fixture", [COLLECTION], indirect=True)
|
||||
def test_astra_setup(astra_fixture):
|
||||
application_token = os.getenv("ASTRA_DB_APPLICATION_TOKEN")
|
||||
api_endpoint = os.getenv("ASTRA_DB_API_ENDPOINT")
|
||||
embedding = MockEmbeddings()
|
||||
|
||||
component = AstraVectorStoreComponent()
|
||||
component.build(
|
||||
token=application_token,
|
||||
api_endpoint=api_endpoint,
|
||||
collection_name=COLLECTION,
|
||||
embedding=embedding,
|
||||
)
|
||||
component.build_vector_store()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not check_env_vars("ASTRA_DB_APPLICATION_TOKEN", "ASTRA_DB_API_ENDPOINT"),
|
||||
reason="missing astra env vars",
|
||||
)
|
||||
@pytest.mark.parametrize("astra_fixture", [SEARCH_COLLECTION], indirect=True)
|
||||
def test_astra_embeds_and_search(astra_fixture):
|
||||
application_token = os.getenv("ASTRA_DB_APPLICATION_TOKEN")
|
||||
api_endpoint = os.getenv("ASTRA_DB_API_ENDPOINT")
|
||||
embedding = MockEmbeddings()
|
||||
|
||||
documents = [Document(page_content="test1"), Document(page_content="test2")]
|
||||
records = [Data.from_document(d) for d in documents]
|
||||
|
||||
component = AstraVectorStoreComponent()
|
||||
component.build(
|
||||
token=application_token,
|
||||
api_endpoint=api_endpoint,
|
||||
collection_name=SEARCH_COLLECTION,
|
||||
embedding=embedding,
|
||||
ingest_data=records,
|
||||
search_input="test1",
|
||||
number_of_results=1,
|
||||
)
|
||||
component.build_vector_store()
|
||||
records = component.search_documents()
|
||||
|
||||
assert len(records) == 1
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not check_env_vars("ASTRA_DB_APPLICATION_TOKEN", "ASTRA_DB_API_ENDPOINT"),
|
||||
reason="missing astra env vars",
|
||||
)
|
||||
def test_astra_vectorize():
|
||||
store = None
|
||||
try:
|
||||
options = {"provider": "nvidia", "modelName": "NV-Embed-QA", "parameters": {}, "authentication": {}}
|
||||
store = AstraDBVectorStore(
|
||||
collection_name=VECTORIZE_COLLECTION,
|
||||
api_endpoint=os.getenv("ASTRA_DB_API_ENDPOINT"),
|
||||
token=os.getenv("ASTRA_DB_APPLICATION_TOKEN"),
|
||||
collection_vector_service_options=CollectionVectorServiceOptions.from_dict(options),
|
||||
)
|
||||
|
||||
application_token = os.getenv("ASTRA_DB_APPLICATION_TOKEN")
|
||||
api_endpoint = os.getenv("ASTRA_DB_API_ENDPOINT")
|
||||
|
||||
documents = [Document(page_content="test1"), Document(page_content="test2")]
|
||||
records = [Data.from_document(d) for d in documents]
|
||||
|
||||
vectorize = AstraVectorizeComponent()
|
||||
vectorize.build(provider="NVIDIA", model_name="NV-Embed-QA")
|
||||
vectorize_options = vectorize.build_options()
|
||||
|
||||
component = AstraVectorStoreComponent()
|
||||
component.build(
|
||||
token=application_token,
|
||||
api_endpoint=api_endpoint,
|
||||
collection_name=VECTORIZE_COLLECTION,
|
||||
ingest_data=records,
|
||||
embedding=vectorize_options,
|
||||
search_input="test",
|
||||
number_of_results=2,
|
||||
)
|
||||
component.build_vector_store()
|
||||
records = component.search_documents()
|
||||
|
||||
assert len(records) == 2
|
||||
finally:
|
||||
if store is not None:
|
||||
store.delete_collection()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not check_env_vars("ASTRA_DB_APPLICATION_TOKEN", "ASTRA_DB_API_ENDPOINT", "OPENAI_API_KEY"),
|
||||
reason="missing env vars",
|
||||
)
|
||||
def test_astra_vectorize_with_provider_api_key():
|
||||
"""tests vectorize using an openai api key"""
|
||||
store = None
|
||||
try:
|
||||
application_token = os.getenv("ASTRA_DB_APPLICATION_TOKEN")
|
||||
api_endpoint = os.getenv("ASTRA_DB_API_ENDPOINT")
|
||||
options = {"provider": "openai", "modelName": "text-embedding-3-small", "parameters": {}, "authentication": {}}
|
||||
store = AstraDBVectorStore(
|
||||
collection_name=VECTORIZE_COLLECTION_OPENAI,
|
||||
api_endpoint=api_endpoint,
|
||||
token=application_token,
|
||||
collection_vector_service_options=CollectionVectorServiceOptions.from_dict(options),
|
||||
collection_embedding_api_key=os.getenv("OPENAI_API_KEY"),
|
||||
)
|
||||
documents = [Document(page_content="test1"), Document(page_content="test2")]
|
||||
records = [Data.from_document(d) for d in documents]
|
||||
|
||||
vectorize = AstraVectorizeComponent()
|
||||
vectorize.build(
|
||||
provider="OpenAI", model_name="text-embedding-3-small", provider_api_key=os.getenv("OPENAI_API_KEY")
|
||||
)
|
||||
vectorize_options = vectorize.build_options()
|
||||
|
||||
component = AstraVectorStoreComponent()
|
||||
component.build(
|
||||
token=application_token,
|
||||
api_endpoint=api_endpoint,
|
||||
collection_name=VECTORIZE_COLLECTION_OPENAI,
|
||||
ingest_data=records,
|
||||
embedding=vectorize_options,
|
||||
search_input="test",
|
||||
)
|
||||
component.build_vector_store()
|
||||
records = component.search_documents()
|
||||
assert len(records) == 2
|
||||
finally:
|
||||
if store is not None:
|
||||
store.delete_collection()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not check_env_vars("ASTRA_DB_APPLICATION_TOKEN", "ASTRA_DB_API_ENDPOINT", "OPENAI_API_KEY"),
|
||||
reason="missing env vars",
|
||||
)
|
||||
def test_astra_vectorize_passes_authentication():
|
||||
"""tests vectorize using the authentication parameter"""
|
||||
store = None
|
||||
try:
|
||||
application_token = os.getenv("ASTRA_DB_APPLICATION_TOKEN")
|
||||
api_endpoint = os.getenv("ASTRA_DB_API_ENDPOINT")
|
||||
options = {
|
||||
"provider": "openai",
|
||||
"modelName": "text-embedding-3-small",
|
||||
"parameters": {},
|
||||
"authentication": {"providerKey": "providerKey"},
|
||||
}
|
||||
store = AstraDBVectorStore(
|
||||
collection_name=VECTORIZE_COLLECTION_OPENAI_WITH_AUTH,
|
||||
api_endpoint=api_endpoint,
|
||||
token=application_token,
|
||||
collection_vector_service_options=CollectionVectorServiceOptions.from_dict(options),
|
||||
)
|
||||
documents = [Document(page_content="test1"), Document(page_content="test2")]
|
||||
records = [Data.from_document(d) for d in documents]
|
||||
|
||||
vectorize = AstraVectorizeComponent()
|
||||
vectorize.build(
|
||||
provider="OpenAI", model_name="text-embedding-3-small", authentication={"providerKey": "providerKey"}
|
||||
)
|
||||
vectorize_options = vectorize.build_options()
|
||||
|
||||
component = AstraVectorStoreComponent()
|
||||
component.build(
|
||||
token=application_token,
|
||||
api_endpoint=api_endpoint,
|
||||
collection_name=VECTORIZE_COLLECTION_OPENAI_WITH_AUTH,
|
||||
ingest_data=records,
|
||||
embedding=vectorize_options,
|
||||
search_input="test",
|
||||
)
|
||||
component.build_vector_store()
|
||||
records = component.search_documents()
|
||||
assert len(records) == 2
|
||||
finally:
|
||||
if store is not None:
|
||||
store.delete_collection()
|
||||
|
||||
|
||||
# @pytest.mark.skipif(
|
||||
# not check_env_vars("ASTRA_DB_APPLICATION_TOKEN", "ASTRA_DB_API_ENDPOINT"),
|
||||
# reason="missing astra env vars",
|
||||
# )
|
||||
# def test_astra_memory():
|
||||
# application_token = os.getenv("ASTRA_DB_APPLICATION_TOKEN")
|
||||
# api_endpoint = os.getenv("ASTRA_DB_API_ENDPOINT")
|
||||
|
||||
# writer = AstraDBMessageWriterComponent()
|
||||
# reader = AstraDBMessageReaderComponent()
|
||||
|
||||
# input_value = Data.from_document(
|
||||
# Document(
|
||||
# page_content="memory1",
|
||||
# metadata={"session_id": 1, "sender": "human", "sender_name": "Bob"},
|
||||
# )
|
||||
# )
|
||||
# writer.build(
|
||||
# input_value=input_value,
|
||||
# session_id=1,
|
||||
# token=application_token,
|
||||
# api_endpoint=api_endpoint,
|
||||
# collection_name=MEMORY_COLLECTION,
|
||||
# )
|
||||
|
||||
# # verify reading w/ same session id pulls the same record
|
||||
# records = reader.build(
|
||||
# session_id=1,
|
||||
# token=application_token,
|
||||
# api_endpoint=api_endpoint,
|
||||
# collection_name=MEMORY_COLLECTION,
|
||||
# )
|
||||
# assert len(records) == 1
|
||||
# assert isinstance(records[0], Data)
|
||||
# content = records[0].get_text()
|
||||
# assert content == "memory1"
|
||||
|
||||
# # verify reading w/ different session id does not pull the same record
|
||||
# records = reader.build(
|
||||
# session_id=2,
|
||||
# token=application_token,
|
||||
# api_endpoint=api_endpoint,
|
||||
# collection_name=MEMORY_COLLECTION,
|
||||
# )
|
||||
# assert len(records) == 0
|
||||
|
||||
# # Cleanup store - doing here rather than fixture (see https://github.com/langchain-ai/langchain-datastax/pull/36)
|
||||
# try:
|
||||
# from langchain_astradb import AstraDBVectorStore
|
||||
# except ImportError:
|
||||
# raise ImportError(
|
||||
# "Could not import langchain Astra DB integration package. Please install it with `pip install langchain-astradb`."
|
||||
# )
|
||||
# store = AstraDBVectorStore(
|
||||
# collection_name=MEMORY_COLLECTION,
|
||||
# embedding=MockEmbeddings(),
|
||||
# api_endpoint=api_endpoint,
|
||||
# token=application_token,
|
||||
# )
|
||||
# store.delete_collection()
|
||||
88
src/backend/tests/integration/langflow.py
Normal file
88
src/backend/tests/integration/langflow.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi import status
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from langflow.graph.schema import RunOutputs
|
||||
from langflow.initial_setup.setup import load_starter_projects
|
||||
from langflow.load import run_flow_from_json
|
||||
|
||||
|
||||
@pytest.mark.api_key_required
|
||||
def test_run_flow_with_caching_success(client: TestClient, starter_project, created_api_key):
|
||||
flow_id = starter_project["id"]
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
payload = {
|
||||
"input_value": "value1",
|
||||
"input_type": "text",
|
||||
"output_type": "text",
|
||||
"tweaks": {"parameter_name": "value"},
|
||||
"stream": False,
|
||||
}
|
||||
response = client.post(f"/api/v1/run/{flow_id}", json=payload, headers=headers)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert "outputs" in data
|
||||
assert "session_id" in data
|
||||
|
||||
|
||||
@pytest.mark.api_key_required
|
||||
def test_run_flow_with_caching_invalid_flow_id(client: TestClient, created_api_key):
|
||||
invalid_flow_id = uuid4()
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
payload = {"input_value": "", "input_type": "text", "output_type": "text", "tweaks": {}, "stream": False}
|
||||
response = client.post(f"/api/v1/run/{invalid_flow_id}", json=payload, headers=headers)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
data = response.json()
|
||||
assert "detail" in data
|
||||
assert f"Flow identifier {invalid_flow_id} not found" in data["detail"]
|
||||
|
||||
|
||||
@pytest.mark.api_key_required
|
||||
def test_run_flow_with_caching_invalid_input_format(client: TestClient, starter_project, created_api_key):
|
||||
flow_id = starter_project["id"]
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
payload = {"input_value": {"key": "value"}, "input_type": "text", "output_type": "text", "tweaks": {}}
|
||||
response = client.post(f"/api/v1/run/{flow_id}", json=payload, headers=headers)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
|
||||
@pytest.mark.api_key_required
|
||||
def test_run_flow_with_invalid_tweaks(client, starter_project, created_api_key):
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
flow_id = starter_project["id"]
|
||||
payload = {
|
||||
"input_value": "value1",
|
||||
"input_type": "text",
|
||||
"output_type": "text",
|
||||
"tweaks": {"invalid_tweak": "value"},
|
||||
}
|
||||
response = client.post(f"/api/v1/run/{flow_id}", json=payload, headers=headers)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
|
||||
@pytest.mark.api_key_required
|
||||
def test_run_with_inputs_and_outputs(client, starter_project, created_api_key):
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
flow_id = starter_project["id"]
|
||||
payload = {
|
||||
"input_value": "value1",
|
||||
"input_type": "text",
|
||||
"output_type": "text",
|
||||
"tweaks": {"parameter_name": "value"},
|
||||
"stream": False,
|
||||
}
|
||||
response = client.post(f"/api/v1/run/{flow_id}", json=payload, headers=headers)
|
||||
assert response.status_code == status.HTTP_200_OK, response.text
|
||||
|
||||
|
||||
@pytest.mark.noclient
|
||||
@pytest.mark.api_key_required
|
||||
def test_run_flow_from_json_object():
|
||||
"""Test loading a flow from a json file and applying tweaks"""
|
||||
_, projects = zip(*load_starter_projects())
|
||||
project = [project for project in projects if "Basic Prompting" in project["name"]][0]
|
||||
results = run_flow_from_json(project, input_value="test", fallback_to_env_vars=True)
|
||||
assert results is not None
|
||||
assert all(isinstance(result, RunOutputs) for result in results)
|
||||
35
src/backend/tests/integration/utils.py
Normal file
35
src/backend/tests/integration/utils.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import os
|
||||
from typing import List
|
||||
|
||||
from langflow.field_typing import Embeddings
|
||||
|
||||
|
||||
def check_env_vars(*vars):
|
||||
"""
|
||||
Check if all specified environment variables are set.
|
||||
|
||||
Args:
|
||||
*vars (str): The environment variables to check.
|
||||
|
||||
Returns:
|
||||
bool: True if all environment variables are set, False otherwise.
|
||||
"""
|
||||
return all(os.getenv(var) for var in vars)
|
||||
|
||||
|
||||
class MockEmbeddings(Embeddings):
|
||||
def __init__(self):
|
||||
self.embedded_documents = None
|
||||
self.embedded_query = None
|
||||
|
||||
@staticmethod
|
||||
def mock_embedding(text: str):
|
||||
return [len(text) / 2, len(text) / 5, len(text) / 10]
|
||||
|
||||
def embed_documents(self, texts: List[str]) -> List[List[float]]:
|
||||
self.embedded_documents = texts
|
||||
return [self.mock_embedding(text) for text in texts]
|
||||
|
||||
def embed_query(self, text: str) -> List[float]:
|
||||
self.embedded_query = text
|
||||
return self.mock_embedding(text)
|
||||
125
src/backend/tests/locust/locustfile.py
Normal file
125
src/backend/tests/locust/locustfile.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import random
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
import orjson
|
||||
from locust import FastHttpUser, between, task
|
||||
from rich import print
|
||||
|
||||
|
||||
class NameTest(FastHttpUser):
|
||||
wait_time = between(1, 5)
|
||||
|
||||
with open("names.txt", "r") as file:
|
||||
names = [line.strip() for line in file.readlines()]
|
||||
|
||||
headers: dict = {}
|
||||
|
||||
def poll_task(self, task_id, sleep_time=1):
|
||||
while True:
|
||||
with self.rest(
|
||||
"GET",
|
||||
f"/task/{task_id}",
|
||||
name="task_status",
|
||||
headers=self.headers,
|
||||
) as response:
|
||||
status = response.js.get("status")
|
||||
print(f"Poll Response: {response.js}")
|
||||
if status == "SUCCESS":
|
||||
return response.js.get("result")
|
||||
elif status in ["FAILURE", "REVOKED"]:
|
||||
raise ValueError(f"Task failed with status: {status}")
|
||||
time.sleep(sleep_time)
|
||||
|
||||
def process(self, name, flow_id, payload):
|
||||
task_id = None
|
||||
print(f"Processing {payload}")
|
||||
with self.rest(
|
||||
"POST",
|
||||
f"/process/{flow_id}",
|
||||
json=payload,
|
||||
name="process",
|
||||
headers=self.headers,
|
||||
) as response:
|
||||
print(response.js)
|
||||
if response.status_code != 200:
|
||||
response.failure("Process call failed")
|
||||
raise ValueError("Process call failed")
|
||||
task_id = response.js.get("id")
|
||||
session_id = response.js.get("session_id")
|
||||
assert task_id, "Inner Task ID not found"
|
||||
|
||||
assert task_id, "Task ID not found"
|
||||
result = self.poll_task(task_id)
|
||||
print(f"Result for {name}: {result}")
|
||||
|
||||
return result, session_id
|
||||
|
||||
@task
|
||||
def send_name_and_check(self):
|
||||
name = random.choice(self.names)
|
||||
|
||||
payload1 = {
|
||||
"inputs": {"text": f"Hello, My name is {name}"},
|
||||
"sync": False,
|
||||
}
|
||||
result1, session_id = self.process(name, self.flow_id, payload1)
|
||||
|
||||
payload2 = {
|
||||
"inputs": {"text": "What is my name? Please, answer like this: Your name is <name>"},
|
||||
"session_id": session_id,
|
||||
"sync": False,
|
||||
}
|
||||
result2, session_id = self.process(name, self.flow_id, payload2)
|
||||
|
||||
assert f"Your name is {name}" in str(result2), "Name not found in response"
|
||||
|
||||
def on_start(self):
|
||||
print("Starting")
|
||||
login_data = {"username": "superuser", "password": "superuser"}
|
||||
response = httpx.post(f"{self.host}/login", data=login_data)
|
||||
print(response.json())
|
||||
|
||||
tokens = response.json()
|
||||
print(tokens)
|
||||
a_token = tokens["access_token"]
|
||||
logged_in_headers = {"Authorization": f"Bearer {a_token}"}
|
||||
print("Logged in")
|
||||
with open(
|
||||
Path(__file__).parent.parent / "data" / "BasicChatwithPromptandHistory.json",
|
||||
"r",
|
||||
) as f:
|
||||
json_flow = f.read()
|
||||
flow = orjson.loads(json_flow)
|
||||
data = flow["data"]
|
||||
# Create test data
|
||||
flow = {"name": "Flow 1", "description": "description", "data": data}
|
||||
print("Creating flow")
|
||||
# Make request to endpoint
|
||||
response = httpx.post(
|
||||
f"{self.host}/flows/",
|
||||
json=flow,
|
||||
headers=logged_in_headers,
|
||||
)
|
||||
self.flow_id = response.json()["id"]
|
||||
print(f"Flow ID: {self.flow_id}")
|
||||
|
||||
# read all users
|
||||
response = httpx.get(
|
||||
f"{self.host}/users/",
|
||||
headers=logged_in_headers,
|
||||
)
|
||||
print(response.json())
|
||||
user_id = next(
|
||||
(user["id"] for user in response.json()["users"] if user["username"] == "superuser"),
|
||||
None,
|
||||
)
|
||||
# Create api key
|
||||
response = httpx.post(
|
||||
f"{self.host}/api_key/",
|
||||
json={"user_id": user_id},
|
||||
headers=logged_in_headers,
|
||||
)
|
||||
print(response.json())
|
||||
self.headers["x-api-key"] = response.json()["api_key"]
|
||||
5
src/backend/tests/locust/names.txt
Normal file
5
src/backend/tests/locust/names.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Bob
|
||||
Alice
|
||||
John
|
||||
Gabriel
|
||||
Lily
|
||||
672
src/backend/tests/test_endpoints.py
Normal file
672
src/backend/tests/test_endpoints.py
Normal file
|
|
@ -0,0 +1,672 @@
|
|||
import time
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi import status
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from langflow.custom.directory_reader.directory_reader import DirectoryReader
|
||||
from langflow.services.deps import get_settings_service
|
||||
|
||||
|
||||
def run_post(client, flow_id, headers, post_data):
|
||||
response = client.post(
|
||||
f"api/v1/process/{flow_id}",
|
||||
headers=headers,
|
||||
json=post_data,
|
||||
)
|
||||
assert response.status_code == 200, response.json()
|
||||
return response.json()
|
||||
|
||||
|
||||
# Helper function to poll task status
|
||||
def poll_task_status(client, headers, href, max_attempts=20, sleep_time=1):
|
||||
for _ in range(max_attempts):
|
||||
task_status_response = client.get(
|
||||
href,
|
||||
headers=headers,
|
||||
)
|
||||
if task_status_response.status_code == 200 and task_status_response.json()["status"] == "SUCCESS":
|
||||
return task_status_response.json()
|
||||
time.sleep(sleep_time)
|
||||
return None # Return None if task did not complete in time
|
||||
|
||||
|
||||
PROMPT_REQUEST = {
|
||||
"name": "string",
|
||||
"template": "string",
|
||||
"frontend_node": {
|
||||
"template": {},
|
||||
"description": "string",
|
||||
"base_classes": ["string"],
|
||||
"name": "",
|
||||
"display_name": "",
|
||||
"documentation": "",
|
||||
"custom_fields": {},
|
||||
"output_types": [],
|
||||
"field_formatters": {
|
||||
"formatters": {"openai_api_key": {}},
|
||||
"base_formatters": {
|
||||
"kwargs": {},
|
||||
"optional": {},
|
||||
"list": {},
|
||||
"dict": {},
|
||||
"union": {},
|
||||
"multiline": {},
|
||||
"show": {},
|
||||
"password": {},
|
||||
"default": {},
|
||||
"headers": {},
|
||||
"dict_code_file": {},
|
||||
"model_fields": {
|
||||
"MODEL_DICT": {
|
||||
"OpenAI": [
|
||||
"text-davinci-003",
|
||||
"text-davinci-002",
|
||||
"text-curie-001",
|
||||
"text-babbage-001",
|
||||
"text-ada-001",
|
||||
],
|
||||
"ChatOpenAI": [
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4-0125-preview",
|
||||
"gpt-4-1106-preview",
|
||||
"gpt-4-vision-preview",
|
||||
"gpt-3.5-turbo-0125",
|
||||
"gpt-3.5-turbo-1106",
|
||||
],
|
||||
"Anthropic": [
|
||||
"claude-v1",
|
||||
"claude-v1-100k",
|
||||
"claude-instant-v1",
|
||||
"claude-instant-v1-100k",
|
||||
"claude-v1.3",
|
||||
"claude-v1.3-100k",
|
||||
"claude-v1.2",
|
||||
"claude-v1.0",
|
||||
"claude-instant-v1.1",
|
||||
"claude-instant-v1.1-100k",
|
||||
"claude-instant-v1.0",
|
||||
],
|
||||
"ChatAnthropic": [
|
||||
"claude-v1",
|
||||
"claude-v1-100k",
|
||||
"claude-instant-v1",
|
||||
"claude-instant-v1-100k",
|
||||
"claude-v1.3",
|
||||
"claude-v1.3-100k",
|
||||
"claude-v1.2",
|
||||
"claude-v1.0",
|
||||
"claude-instant-v1.1",
|
||||
"claude-instant-v1.1-100k",
|
||||
"claude-instant-v1.0",
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# def test_process_flow_invalid_api_key(client, flow, monkeypatch):
|
||||
# # Mock de process_graph_cached
|
||||
# from langflow.api.v1 import endpoints
|
||||
# from langflow.services.database.models.api_key import crud
|
||||
|
||||
# settings_service = get_settings_service()
|
||||
# settings_service.auth_settings.AUTO_LOGIN = False
|
||||
|
||||
# async def mock_process_graph_cached(*args, **kwargs):
|
||||
# return Result(result={}, session_id="session_id_mock")
|
||||
|
||||
# def mock_update_total_uses(*args, **kwargs):
|
||||
# return created_api_key
|
||||
|
||||
# monkeypatch.setattr(endpoints, "process_graph_cached", mock_process_graph_cached)
|
||||
# monkeypatch.setattr(crud, "update_total_uses", mock_update_total_uses)
|
||||
|
||||
# headers = {"x-api-key": "invalid_api_key"}
|
||||
|
||||
# post_data = {
|
||||
# "inputs": {"key": "value"},
|
||||
# "tweaks": None,
|
||||
# "clear_cache": False,
|
||||
# "session_id": None,
|
||||
# }
|
||||
|
||||
# response = client.post(f"api/v1/process/{flow.id}", headers=headers, json=post_data)
|
||||
|
||||
# assert response.status_code == 403
|
||||
# assert response.json() == {"detail": "Invalid or missing API key"}
|
||||
|
||||
|
||||
# def test_process_flow_invalid_id(client, monkeypatch, created_api_key):
|
||||
# async def mock_process_graph_cached(*args, **kwargs):
|
||||
# return Result(result={}, session_id="session_id_mock")
|
||||
|
||||
# from langflow.api.v1 import endpoints
|
||||
|
||||
# monkeypatch.setattr(endpoints, "process_graph_cached", mock_process_graph_cached)
|
||||
|
||||
# api_key = created_api_key.api_key
|
||||
# headers = {"x-api-key": api_key}
|
||||
|
||||
# post_data = {
|
||||
# "inputs": {"key": "value"},
|
||||
# "tweaks": None,
|
||||
# "clear_cache": False,
|
||||
# "session_id": None,
|
||||
# }
|
||||
|
||||
# invalid_id = uuid.uuid4()
|
||||
# response = client.post(f"api/v1/process/{invalid_id}", headers=headers, json=post_data)
|
||||
|
||||
# assert response.status_code == 404
|
||||
# assert f"Flow {invalid_id} not found" in response.json()["detail"]
|
||||
|
||||
|
||||
# def test_process_flow_without_autologin(client, flow, monkeypatch, created_api_key):
|
||||
# # Mock de process_graph_cached
|
||||
# from langflow.api.v1 import endpoints
|
||||
# from langflow.services.database.models.api_key import crud
|
||||
|
||||
# settings_service = get_settings_service()
|
||||
# settings_service.auth_settings.AUTO_LOGIN = False
|
||||
|
||||
# async def mock_process_graph_cached(*args, **kwargs):
|
||||
# return Result(result={}, session_id="session_id_mock")
|
||||
|
||||
# def mock_process_graph_cached_task(*args, **kwargs):
|
||||
# return Result(result={}, session_id="session_id_mock")
|
||||
|
||||
# # The task function is ran like this:
|
||||
# # if not self.use_celery:
|
||||
# # return None, await task_func(*args, **kwargs)
|
||||
# # if not hasattr(task_func, "apply"):
|
||||
# # raise ValueError(f"Task function {task_func} does not have an apply method")
|
||||
# # task = task_func.apply(args=args, kwargs=kwargs)
|
||||
# # result = task.get()
|
||||
# # return task.id, result
|
||||
# # So we need to mock the task function to return a task object
|
||||
# # and then mock the task object to return a result
|
||||
# # maybe a named tuple would be better here
|
||||
# task = namedtuple("task", ["id", "get"])
|
||||
# mock_process_graph_cached_task.apply = lambda *args, **kwargs: task(
|
||||
# id="task_id_mock", get=lambda: Result(result={}, session_id="session_id_mock")
|
||||
# )
|
||||
|
||||
# def mock_update_total_uses(*args, **kwargs):
|
||||
# return created_api_key
|
||||
|
||||
# monkeypatch.setattr(endpoints, "process_graph_cached", mock_process_graph_cached)
|
||||
# monkeypatch.setattr(crud, "update_total_uses", mock_update_total_uses)
|
||||
# monkeypatch.setattr(endpoints, "process_graph_cached_task", mock_process_graph_cached_task)
|
||||
|
||||
# api_key = created_api_key.api_key
|
||||
# headers = {"x-api-key": api_key}
|
||||
|
||||
# # Dummy POST data
|
||||
# post_data = {
|
||||
# "inputs": {"input": "value"},
|
||||
# "tweaks": None,
|
||||
# "clear_cache": False,
|
||||
# "session_id": None,
|
||||
# }
|
||||
|
||||
# # Make the request to the FastAPI TestClient
|
||||
|
||||
# response = client.post(f"api/v1/process/{flow.id}", headers=headers, json=post_data)
|
||||
|
||||
# # Check the response
|
||||
# assert response.status_code == 200, response.json()
|
||||
# assert response.json()["result"] == {}, response.json()
|
||||
# assert response.json()["session_id"] == "session_id_mock", response.json()
|
||||
|
||||
|
||||
# def test_process_flow_fails_autologin_off(client, flow, monkeypatch):
|
||||
# # Mock de process_graph_cached
|
||||
# from langflow.api.v1 import endpoints
|
||||
# from langflow.services.database.models.api_key import crud
|
||||
|
||||
# settings_service = get_settings_service()
|
||||
# settings_service.auth_settings.AUTO_LOGIN = False
|
||||
|
||||
# async def mock_process_graph_cached(*args, **kwargs):
|
||||
# return Result(result={}, session_id="session_id_mock")
|
||||
|
||||
# async def mock_update_total_uses(*args, **kwargs):
|
||||
# return created_api_key
|
||||
|
||||
# monkeypatch.setattr(endpoints, "process_graph_cached", mock_process_graph_cached)
|
||||
# monkeypatch.setattr(crud, "update_total_uses", mock_update_total_uses)
|
||||
|
||||
# headers = {"x-api-key": "api_key"}
|
||||
|
||||
# # Dummy POST data
|
||||
# post_data = {
|
||||
# "inputs": {"key": "value"},
|
||||
# "tweaks": None,
|
||||
# "clear_cache": False,
|
||||
# "session_id": None,
|
||||
# }
|
||||
|
||||
# # Make the request to the FastAPI TestClient
|
||||
|
||||
# response = client.post(f"api/v1/process/{flow.id}", headers=headers, json=post_data)
|
||||
|
||||
# # Check the response
|
||||
# assert response.status_code == 403, response.json()
|
||||
# assert response.json() == {"detail": "Invalid or missing API key"}
|
||||
|
||||
|
||||
def test_get_all(client: TestClient, logged_in_headers):
|
||||
response = client.get("api/v1/all", headers=logged_in_headers)
|
||||
assert response.status_code == 200
|
||||
settings = get_settings_service().settings
|
||||
dir_reader = DirectoryReader(settings.components_path[0])
|
||||
files = dir_reader.get_files()
|
||||
# json_response is a dict of dicts
|
||||
all_names = [component_name for _, components in response.json().items() for component_name in components]
|
||||
json_response = response.json()
|
||||
# We need to test the custom nodes
|
||||
assert len(all_names) <= len(
|
||||
files
|
||||
) # Less or equal because we might have some files that don't have the dependencies installed
|
||||
assert "ChatInput" in json_response["inputs"]
|
||||
assert "Prompt" in json_response["prompts"]
|
||||
assert "ChatOutput" in json_response["outputs"]
|
||||
|
||||
|
||||
def test_post_validate_code(client: TestClient):
|
||||
# Test case with a valid import and function
|
||||
code1 = """
|
||||
import math
|
||||
|
||||
def square(x):
|
||||
return x ** 2
|
||||
"""
|
||||
response1 = client.post("api/v1/validate/code", json={"code": code1})
|
||||
assert response1.status_code == 200
|
||||
assert response1.json() == {"imports": {"errors": []}, "function": {"errors": []}}
|
||||
|
||||
# Test case with an invalid import and valid function
|
||||
code2 = """
|
||||
import non_existent_module
|
||||
|
||||
def square(x):
|
||||
return x ** 2
|
||||
"""
|
||||
response2 = client.post("api/v1/validate/code", json={"code": code2})
|
||||
assert response2.status_code == 200
|
||||
assert response2.json() == {
|
||||
"imports": {"errors": ["No module named 'non_existent_module'"]},
|
||||
"function": {"errors": []},
|
||||
}
|
||||
|
||||
# Test case with a valid import and invalid function syntax
|
||||
code3 = """
|
||||
import math
|
||||
|
||||
def square(x)
|
||||
return x ** 2
|
||||
"""
|
||||
response3 = client.post("api/v1/validate/code", json={"code": code3})
|
||||
assert response3.status_code == 200
|
||||
assert response3.json() == {
|
||||
"imports": {"errors": []},
|
||||
"function": {"errors": ["expected ':' (<unknown>, line 4)"]},
|
||||
}
|
||||
|
||||
# Test case with invalid JSON payload
|
||||
response4 = client.post("api/v1/validate/code", json={"invalid_key": code1})
|
||||
assert response4.status_code == 422
|
||||
|
||||
# Test case with an empty code string
|
||||
response5 = client.post("api/v1/validate/code", json={"code": ""})
|
||||
assert response5.status_code == 200
|
||||
assert response5.json() == {"imports": {"errors": []}, "function": {"errors": []}}
|
||||
|
||||
# Test case with a syntax error in the code
|
||||
code6 = """
|
||||
import math
|
||||
|
||||
def square(x)
|
||||
return x ** 2
|
||||
"""
|
||||
response6 = client.post("api/v1/validate/code", json={"code": code6})
|
||||
assert response6.status_code == 200
|
||||
assert response6.json() == {
|
||||
"imports": {"errors": []},
|
||||
"function": {"errors": ["expected ':' (<unknown>, line 4)"]},
|
||||
}
|
||||
|
||||
|
||||
VALID_PROMPT = """
|
||||
I want you to act as a naming consultant for new companies.
|
||||
|
||||
Here are some examples of good company names:
|
||||
|
||||
- search engine, Google
|
||||
- social media, Facebook
|
||||
- video sharing, YouTube
|
||||
|
||||
The name should be short, catchy and easy to remember.
|
||||
|
||||
What is a good name for a company that makes {product}?
|
||||
"""
|
||||
|
||||
INVALID_PROMPT = "This is an invalid prompt without any input variable."
|
||||
|
||||
|
||||
def test_valid_prompt(client: TestClient):
|
||||
PROMPT_REQUEST["template"] = VALID_PROMPT
|
||||
response = client.post("api/v1/validate/prompt", json=PROMPT_REQUEST)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["input_variables"] == ["product"]
|
||||
|
||||
|
||||
def test_invalid_prompt(client: TestClient):
|
||||
PROMPT_REQUEST["template"] = INVALID_PROMPT
|
||||
response = client.post(
|
||||
"api/v1/validate/prompt",
|
||||
json=PROMPT_REQUEST,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["input_variables"] == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"prompt,expected_input_variables",
|
||||
[
|
||||
("{color} is my favorite color.", ["color"]),
|
||||
("The weather is {weather} today.", ["weather"]),
|
||||
("This prompt has no variables.", []),
|
||||
("{a}, {b}, and {c} are variables.", ["a", "b", "c"]),
|
||||
],
|
||||
)
|
||||
def test_various_prompts(client, prompt, expected_input_variables):
|
||||
PROMPT_REQUEST["template"] = prompt
|
||||
response = client.post("api/v1/validate/prompt", json=PROMPT_REQUEST)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["input_variables"] == expected_input_variables
|
||||
|
||||
|
||||
def test_get_vertices_flow_not_found(client, logged_in_headers):
|
||||
uuid = uuid4()
|
||||
response = client.post(f"/api/v1/build/{uuid}/vertices", headers=logged_in_headers)
|
||||
assert response.status_code == 500
|
||||
|
||||
|
||||
def test_get_vertices(client, added_flow_with_prompt_and_history, logged_in_headers):
|
||||
flow_id = added_flow_with_prompt_and_history["id"]
|
||||
response = client.post(f"/api/v1/build/{flow_id}/vertices", headers=logged_in_headers)
|
||||
assert response.status_code == 200
|
||||
assert "ids" in response.json()
|
||||
# The response should contain the list in this order
|
||||
# ['ConversationBufferMemory-Lu2Nb', 'PromptTemplate-5Q0W8', 'ChatOpenAI-vy7fV', 'LLMChain-UjBh1']
|
||||
# The important part is before the - (ConversationBufferMemory, PromptTemplate, ChatOpenAI, LLMChain)
|
||||
ids = [_id.split("-")[0] for _id in response.json()["ids"]]
|
||||
assert ids == [
|
||||
"ChatOpenAI",
|
||||
"PromptTemplate",
|
||||
"ConversationBufferMemory",
|
||||
]
|
||||
|
||||
|
||||
def test_build_vertex_invalid_flow_id(client, logged_in_headers):
|
||||
uuid = uuid4()
|
||||
response = client.post(f"/api/v1/build/{uuid}/vertices/vertex_id", headers=logged_in_headers)
|
||||
assert response.status_code == 500
|
||||
|
||||
|
||||
def test_build_vertex_invalid_vertex_id(client, added_flow_with_prompt_and_history, logged_in_headers):
|
||||
flow_id = added_flow_with_prompt_and_history["id"]
|
||||
response = client.post(f"/api/v1/build/{flow_id}/vertices/invalid_vertex_id", headers=logged_in_headers)
|
||||
assert response.status_code == 500
|
||||
|
||||
|
||||
@pytest.mark.api_key_required
|
||||
def test_successful_run_no_payload(client, simple_api_test, created_api_key):
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
flow_id = simple_api_test["id"]
|
||||
response = client.post(f"/api/v1/run/{flow_id}", headers=headers)
|
||||
assert response.status_code == status.HTTP_200_OK, response.text
|
||||
# Add more assertions here to validate the response content
|
||||
json_response = response.json()
|
||||
assert "session_id" in json_response
|
||||
assert "outputs" in json_response
|
||||
outer_outputs = json_response["outputs"]
|
||||
assert len(outer_outputs) == 1
|
||||
outputs_dict = outer_outputs[0]
|
||||
assert len(outputs_dict) == 2
|
||||
assert "inputs" in outputs_dict
|
||||
assert "outputs" in outputs_dict
|
||||
assert outputs_dict.get("inputs") == {"input_value": ""}
|
||||
assert isinstance(outputs_dict.get("outputs"), list)
|
||||
assert len(outputs_dict.get("outputs")) == 1
|
||||
ids = [output.get("component_id") for output in outputs_dict.get("outputs")]
|
||||
assert all(["ChatOutput" in _id for _id in ids])
|
||||
display_names = [output.get("component_display_name") for output in outputs_dict.get("outputs")]
|
||||
assert all([name in display_names for name in ["Chat Output"]])
|
||||
output_results_has_results = all("results" in output.get("results") for output in outputs_dict.get("outputs"))
|
||||
inner_results = [output.get("results") for output in outputs_dict.get("outputs")]
|
||||
|
||||
assert all([result is not None for result in inner_results]), (outputs_dict, output_results_has_results)
|
||||
|
||||
|
||||
def test_successful_run_with_output_type_text(client, simple_api_test, created_api_key):
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
flow_id = simple_api_test["id"]
|
||||
payload = {
|
||||
"output_type": "text",
|
||||
}
|
||||
response = client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload)
|
||||
assert response.status_code == status.HTTP_200_OK, response.text
|
||||
# Add more assertions here to validate the response content
|
||||
json_response = response.json()
|
||||
assert "session_id" in json_response
|
||||
assert "outputs" in json_response
|
||||
outer_outputs = json_response["outputs"]
|
||||
assert len(outer_outputs) == 1
|
||||
outputs_dict = outer_outputs[0]
|
||||
assert len(outputs_dict) == 2
|
||||
assert "inputs" in outputs_dict
|
||||
assert "outputs" in outputs_dict
|
||||
assert outputs_dict.get("inputs") == {"input_value": ""}
|
||||
assert isinstance(outputs_dict.get("outputs"), list)
|
||||
assert len(outputs_dict.get("outputs")) == 1
|
||||
ids = [output.get("component_id") for output in outputs_dict.get("outputs")]
|
||||
assert all(["ChatOutput" in _id for _id in ids]), ids
|
||||
display_names = [output.get("component_display_name") for output in outputs_dict.get("outputs")]
|
||||
assert all([name in display_names for name in ["Chat Output"]]), display_names
|
||||
inner_results = [output.get("results") for output in outputs_dict.get("outputs")]
|
||||
expected_keys = ["message"]
|
||||
assert all([key in result for result in inner_results for key in expected_keys]), outputs_dict
|
||||
|
||||
|
||||
def test_successful_run_with_output_type_any(client, simple_api_test, created_api_key):
|
||||
# This one should have both the ChatOutput and TextOutput components
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
flow_id = simple_api_test["id"]
|
||||
payload = {
|
||||
"output_type": "any",
|
||||
}
|
||||
response = client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload)
|
||||
assert response.status_code == status.HTTP_200_OK, response.text
|
||||
# Add more assertions here to validate the response content
|
||||
json_response = response.json()
|
||||
assert "session_id" in json_response
|
||||
assert "outputs" in json_response
|
||||
outer_outputs = json_response["outputs"]
|
||||
assert len(outer_outputs) == 1
|
||||
outputs_dict = outer_outputs[0]
|
||||
assert len(outputs_dict) == 2
|
||||
assert "inputs" in outputs_dict
|
||||
assert "outputs" in outputs_dict
|
||||
assert outputs_dict.get("inputs") == {"input_value": ""}
|
||||
assert isinstance(outputs_dict.get("outputs"), list)
|
||||
assert len(outputs_dict.get("outputs")) == 1
|
||||
ids = [output.get("component_id") for output in outputs_dict.get("outputs")]
|
||||
assert all(["ChatOutput" in _id or "TextOutput" in _id for _id in ids]), ids
|
||||
display_names = [output.get("component_display_name") for output in outputs_dict.get("outputs")]
|
||||
assert all([name in display_names for name in ["Chat Output"]]), display_names
|
||||
inner_results = [output.get("results") for output in outputs_dict.get("outputs")]
|
||||
expected_keys = ["message"]
|
||||
assert all([key in result for result in inner_results for key in expected_keys]), outputs_dict
|
||||
|
||||
|
||||
def test_successful_run_with_output_type_debug(client, simple_api_test, created_api_key):
|
||||
# This one should return outputs for all components
|
||||
# Let's just check the amount of outputs(there should be 7)
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
flow_id = simple_api_test["id"]
|
||||
payload = {
|
||||
"output_type": "debug",
|
||||
}
|
||||
response = client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload)
|
||||
assert response.status_code == status.HTTP_200_OK, response.text
|
||||
# Add more assertions here to validate the response content
|
||||
json_response = response.json()
|
||||
assert "session_id" in json_response
|
||||
assert "outputs" in json_response
|
||||
outer_outputs = json_response["outputs"]
|
||||
assert len(outer_outputs) == 1
|
||||
outputs_dict = outer_outputs[0]
|
||||
assert len(outputs_dict) == 2
|
||||
assert "inputs" in outputs_dict
|
||||
assert "outputs" in outputs_dict
|
||||
assert outputs_dict.get("inputs") == {"input_value": ""}
|
||||
assert isinstance(outputs_dict.get("outputs"), list)
|
||||
assert len(outputs_dict.get("outputs")) == 3
|
||||
|
||||
|
||||
def test_successful_run_with_input_type_text(client, simple_api_test, created_api_key):
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
flow_id = simple_api_test["id"]
|
||||
payload = {
|
||||
"input_type": "text",
|
||||
"output_type": "debug",
|
||||
"input_value": "value1",
|
||||
}
|
||||
response = client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload)
|
||||
assert response.status_code == status.HTTP_200_OK, response.text
|
||||
# Add more assertions here to validate the response content
|
||||
json_response = response.json()
|
||||
assert "session_id" in json_response
|
||||
assert "outputs" in json_response
|
||||
outer_outputs = json_response["outputs"]
|
||||
assert len(outer_outputs) == 1
|
||||
outputs_dict = outer_outputs[0]
|
||||
assert len(outputs_dict) == 2
|
||||
assert "inputs" in outputs_dict
|
||||
assert "outputs" in outputs_dict
|
||||
assert outputs_dict.get("inputs") == {"input_value": "value1"}
|
||||
assert isinstance(outputs_dict.get("outputs"), list)
|
||||
assert len(outputs_dict.get("outputs")) == 3
|
||||
# Now we get all components that contain TextInput in the component_id
|
||||
text_input_outputs = [output for output in outputs_dict.get("outputs") if "TextInput" in output.get("component_id")]
|
||||
assert len(text_input_outputs) == 1
|
||||
# Now we check if the input_value is correct
|
||||
# We get text key twice because the output is now a Message
|
||||
assert all(
|
||||
[output.get("results").get("text").get("text") == "value1" for output in text_input_outputs]
|
||||
), text_input_outputs
|
||||
|
||||
|
||||
def test_successful_run_with_input_type_chat(client, simple_api_test, created_api_key):
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
flow_id = simple_api_test["id"]
|
||||
payload = {
|
||||
"input_type": "chat",
|
||||
"output_type": "debug",
|
||||
"input_value": "value1",
|
||||
}
|
||||
response = client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload)
|
||||
assert response.status_code == status.HTTP_200_OK, response.text
|
||||
# Add more assertions here to validate the response content
|
||||
json_response = response.json()
|
||||
assert "session_id" in json_response
|
||||
assert "outputs" in json_response
|
||||
outer_outputs = json_response["outputs"]
|
||||
assert len(outer_outputs) == 1
|
||||
outputs_dict = outer_outputs[0]
|
||||
assert len(outputs_dict) == 2
|
||||
assert "inputs" in outputs_dict
|
||||
assert "outputs" in outputs_dict
|
||||
assert outputs_dict.get("inputs") == {"input_value": "value1"}
|
||||
assert isinstance(outputs_dict.get("outputs"), list)
|
||||
assert len(outputs_dict.get("outputs")) == 3
|
||||
# Now we get all components that contain TextInput in the component_id
|
||||
chat_input_outputs = [output for output in outputs_dict.get("outputs") if "ChatInput" in output.get("component_id")]
|
||||
assert len(chat_input_outputs) == 1
|
||||
# Now we check if the input_value is correct
|
||||
assert all(
|
||||
[output.get("results").get("message").get("text") == "value1" for output in chat_input_outputs]
|
||||
), chat_input_outputs
|
||||
|
||||
|
||||
def test_invalid_run_with_input_type_chat(client, simple_api_test, created_api_key):
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
flow_id = simple_api_test["id"]
|
||||
payload = {
|
||||
"input_type": "chat",
|
||||
"output_type": "debug",
|
||||
"input_value": "value1",
|
||||
"tweaks": {"Chat Input": {"input_value": "value2"}},
|
||||
}
|
||||
response = client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST, response.text
|
||||
assert "If you pass an input_value to the chat input, you cannot pass a tweak with the same name." in response.text
|
||||
|
||||
|
||||
def test_successful_run_with_input_type_any(client, simple_api_test, created_api_key):
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
flow_id = simple_api_test["id"]
|
||||
payload = {
|
||||
"input_type": "any",
|
||||
"output_type": "debug",
|
||||
"input_value": "value1",
|
||||
}
|
||||
response = client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload)
|
||||
assert response.status_code == status.HTTP_200_OK, response.text
|
||||
# Add more assertions here to validate the response content
|
||||
json_response = response.json()
|
||||
assert "session_id" in json_response
|
||||
assert "outputs" in json_response
|
||||
outer_outputs = json_response["outputs"]
|
||||
assert len(outer_outputs) == 1
|
||||
outputs_dict = outer_outputs[0]
|
||||
assert len(outputs_dict) == 2
|
||||
assert "inputs" in outputs_dict
|
||||
assert "outputs" in outputs_dict
|
||||
assert outputs_dict.get("inputs") == {"input_value": "value1"}
|
||||
assert isinstance(outputs_dict.get("outputs"), list)
|
||||
assert len(outputs_dict.get("outputs")) == 3
|
||||
# Now we get all components that contain TextInput or ChatInput in the component_id
|
||||
any_input_outputs = [
|
||||
output
|
||||
for output in outputs_dict.get("outputs")
|
||||
if "TextInput" in output.get("component_id") or "ChatInput" in output.get("component_id")
|
||||
]
|
||||
assert len(any_input_outputs) == 2
|
||||
# Now we check if the input_value is correct
|
||||
all_result_dicts = [output.get("results") for output in any_input_outputs]
|
||||
all_message_or_text_dicts = [
|
||||
result_dict.get("message", result_dict.get("text")) for result_dict in all_result_dicts
|
||||
]
|
||||
assert all(
|
||||
[message_or_text_dict.get("text") == "value1" for message_or_text_dict in all_message_or_text_dicts]
|
||||
), any_input_outputs
|
||||
|
||||
|
||||
def test_invalid_flow_id(client, created_api_key):
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
flow_id = "invalid-flow-id"
|
||||
response = client.post(f"/api/v1/run/{flow_id}", headers=headers)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND, response.text
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
flow_id = UUID(int=0)
|
||||
response = client.post(f"/api/v1/run/{flow_id}", headers=headers)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND, response.text
|
||||
# Check if the error detail is as expected
|
||||
76
src/backend/tests/test_messages_endpoints.py
Normal file
76
src/backend/tests/test_messages_endpoints.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from langflow.memory import add_messagetables
|
||||
|
||||
# Assuming you have these imports available
|
||||
from langflow.services.database.models.message import MessageCreate, MessageRead, MessageUpdate
|
||||
from langflow.services.database.models.message.model import MessageTable
|
||||
from langflow.services.deps import session_scope
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def created_message():
|
||||
with session_scope() as session:
|
||||
message = MessageCreate(text="Test message", sender="User", sender_name="User", session_id="session_id")
|
||||
messagetable = MessageTable.model_validate(message, from_attributes=True)
|
||||
messagetables = add_messagetables([messagetable], session)
|
||||
message_read = MessageRead.model_validate(messagetables[0], from_attributes=True)
|
||||
return message_read
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def created_messages(session):
|
||||
with session_scope() as session:
|
||||
messages = [
|
||||
MessageCreate(text="Test message 1", sender="User", sender_name="User", session_id="session_id2"),
|
||||
MessageCreate(text="Test message 2", sender="User", sender_name="User", session_id="session_id2"),
|
||||
MessageCreate(text="Test message 3", sender="User", sender_name="User", session_id="session_id2"),
|
||||
]
|
||||
messagetables = [MessageTable.model_validate(message, from_attributes=True) for message in messages]
|
||||
message_list = add_messagetables(messagetables, session)
|
||||
|
||||
return message_list
|
||||
|
||||
|
||||
def test_delete_messages(client: TestClient, created_messages, logged_in_headers):
|
||||
response = client.request(
|
||||
"DELETE", "api/v1/monitor/messages", json=[str(msg.id) for msg in created_messages], headers=logged_in_headers
|
||||
)
|
||||
assert response.status_code == 204, response.text
|
||||
assert response.reason_phrase == "No Content"
|
||||
|
||||
|
||||
def test_update_message(client: TestClient, logged_in_headers, created_message):
|
||||
message_id = created_message.id
|
||||
message_update = MessageUpdate(text="Updated content")
|
||||
response = client.put(
|
||||
f"api/v1/monitor/messages/{message_id}", json=message_update.model_dump(), headers=logged_in_headers
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
updated_message = MessageRead(**response.json())
|
||||
assert updated_message.text == "Updated content"
|
||||
|
||||
|
||||
def test_update_message_not_found(client: TestClient, logged_in_headers):
|
||||
non_existent_id = UUID("00000000-0000-0000-0000-000000000000")
|
||||
message_update = MessageUpdate(text="Updated content")
|
||||
response = client.put(
|
||||
f"api/v1/monitor/messages/{non_existent_id}", json=message_update.model_dump(), headers=logged_in_headers
|
||||
)
|
||||
assert response.status_code == 404, response.text
|
||||
assert response.json()["detail"] == "Message not found"
|
||||
|
||||
|
||||
def test_delete_messages_session(client: TestClient, created_messages, logged_in_headers):
|
||||
session_id = "session_id2"
|
||||
response = client.delete(f"api/v1/monitor/messages/session/{session_id}", headers=logged_in_headers)
|
||||
assert response.status_code == 204
|
||||
assert response.reason_phrase == "No Content"
|
||||
|
||||
assert len(created_messages) == 3
|
||||
response = client.get("api/v1/monitor/messages", headers=logged_in_headers)
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) == 0
|
||||
129
src/backend/tests/test_schema.py
Normal file
129
src/backend/tests/test_schema.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
from typing import Union
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from langflow.template import Input, Output
|
||||
from langflow.template.field.base import UNDEFINED
|
||||
from langflow.type_extraction.type_extraction import post_process_type
|
||||
|
||||
|
||||
@pytest.fixture(name="client", autouse=True)
|
||||
def client_fixture():
|
||||
pass
|
||||
|
||||
|
||||
class TestInput:
|
||||
def test_field_type_str(self):
|
||||
input_obj = Input(field_type="str")
|
||||
assert input_obj.field_type == "str"
|
||||
|
||||
def test_field_type_type(self):
|
||||
input_obj = Input(field_type=int)
|
||||
assert input_obj.field_type == "int"
|
||||
|
||||
def test_invalid_field_type(self):
|
||||
with pytest.raises(ValidationError):
|
||||
Input(field_type=123)
|
||||
|
||||
def test_serialize_field_type(self):
|
||||
input_obj = Input(field_type="str")
|
||||
assert input_obj.serialize_field_type("str", None) == "str"
|
||||
|
||||
def test_validate_type_string(self):
|
||||
input_obj = Input(field_type="str")
|
||||
assert input_obj.field_type == "str"
|
||||
|
||||
def test_validate_type_class(self):
|
||||
input_obj = Input(field_type=int)
|
||||
assert input_obj.field_type == "int"
|
||||
|
||||
def test_post_process_type_function(self):
|
||||
assert post_process_type(int) == [int]
|
||||
assert post_process_type(list[int]) == [int]
|
||||
assert post_process_type(Union[int, str]) == [int, str]
|
||||
|
||||
def test_input_to_dict(self):
|
||||
input_obj = Input(field_type="str")
|
||||
assert input_obj.to_dict() == {
|
||||
"type": "str",
|
||||
"required": False,
|
||||
"placeholder": "",
|
||||
"list": False,
|
||||
"show": True,
|
||||
"multiline": False,
|
||||
"fileTypes": [],
|
||||
"file_path": "",
|
||||
"password": False,
|
||||
"advanced": False,
|
||||
"title_case": False,
|
||||
"dynamic": False,
|
||||
"info": "",
|
||||
"input_types": ["Text"],
|
||||
"load_from_db": False,
|
||||
}
|
||||
|
||||
|
||||
class TestOutput:
|
||||
def test_output_default(self):
|
||||
output_obj = Output(name="test_output")
|
||||
assert output_obj.name == "test_output"
|
||||
assert output_obj.value == UNDEFINED
|
||||
assert output_obj.cache is True
|
||||
|
||||
def test_output_add_types(self):
|
||||
output_obj = Output(name="test_output")
|
||||
output_obj.add_types(["str", "int"])
|
||||
assert output_obj.types == ["str", "int"]
|
||||
|
||||
def test_output_set_selected(self):
|
||||
output_obj = Output(name="test_output", types=["str", "int"])
|
||||
output_obj.set_selected()
|
||||
assert output_obj.selected == "str"
|
||||
|
||||
def test_output_to_dict(self):
|
||||
output_obj = Output(name="test_output")
|
||||
assert output_obj.to_dict() == {
|
||||
"types": [],
|
||||
"name": "test_output",
|
||||
"display_name": "test_output",
|
||||
"cache": True,
|
||||
"value": "__UNDEFINED__",
|
||||
}
|
||||
|
||||
def test_output_validate_display_name(self):
|
||||
output_obj = Output(name="test_output")
|
||||
assert output_obj.display_name == "test_output"
|
||||
|
||||
def test_output_validate_model(self):
|
||||
output_obj = Output(name="test_output", value="__UNDEFINED__")
|
||||
assert output_obj.validate_model() == output_obj
|
||||
|
||||
|
||||
class TestPostProcessType:
|
||||
def test_int_type(self):
|
||||
assert post_process_type(int) == [int]
|
||||
|
||||
def test_list_int_type(self):
|
||||
assert post_process_type(list[int]) == [int]
|
||||
|
||||
def test_union_type(self):
|
||||
assert post_process_type(Union[int, str]) == [int, str]
|
||||
|
||||
def test_custom_type(self):
|
||||
class CustomType:
|
||||
pass
|
||||
|
||||
assert post_process_type(CustomType) == [CustomType]
|
||||
|
||||
def test_list_custom_type(self):
|
||||
class CustomType:
|
||||
pass
|
||||
|
||||
assert post_process_type(list[CustomType]) == [CustomType]
|
||||
|
||||
def test_union_custom_type(self):
|
||||
class CustomType:
|
||||
pass
|
||||
|
||||
assert post_process_type(Union[CustomType, int]) == [CustomType, int]
|
||||
232
src/backend/tests/test_user.py
Normal file
232
src/backend/tests/test_user.py
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from langflow.services.auth.utils import create_super_user, get_password_hash
|
||||
from langflow.services.database.models.user import UserUpdate
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.database.utils import session_getter
|
||||
from langflow.services.deps import get_db_service, get_settings_service
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def super_user(client):
|
||||
settings_manager = get_settings_service()
|
||||
auth_settings = settings_manager.auth_settings
|
||||
with session_getter(get_db_service()) as session:
|
||||
return create_super_user(
|
||||
db=session,
|
||||
username=auth_settings.SUPERUSER,
|
||||
password=auth_settings.SUPERUSER_PASSWORD,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def super_user_headers(client, super_user):
|
||||
settings_service = get_settings_service()
|
||||
auth_settings = settings_service.auth_settings
|
||||
login_data = {
|
||||
"username": auth_settings.SUPERUSER,
|
||||
"password": auth_settings.SUPERUSER_PASSWORD,
|
||||
}
|
||||
response = client.post("/api/v1/login", data=login_data)
|
||||
assert response.status_code == 200
|
||||
tokens = response.json()
|
||||
a_token = tokens["access_token"]
|
||||
return {"Authorization": f"Bearer {a_token}"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def deactivated_user():
|
||||
with session_getter(get_db_service()) as session:
|
||||
user = User(
|
||||
username="deactivateduser",
|
||||
password=get_password_hash("testpassword"),
|
||||
is_active=False,
|
||||
is_superuser=False,
|
||||
last_login_at=datetime.now(),
|
||||
)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
def test_user_waiting_for_approval(
|
||||
client,
|
||||
):
|
||||
# Create a user that is not active and has never logged in
|
||||
with session_getter(get_db_service()) as session:
|
||||
user = User(
|
||||
username="waitingforapproval",
|
||||
password=get_password_hash("testpassword"),
|
||||
is_active=False,
|
||||
last_login_at=None,
|
||||
)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
login_data = {"username": "waitingforapproval", "password": "testpassword"}
|
||||
response = client.post("/api/v1/login", data=login_data)
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "Waiting for approval"
|
||||
|
||||
|
||||
def test_deactivated_user_cannot_login(client, deactivated_user):
|
||||
login_data = {"username": deactivated_user.username, "password": "testpassword"}
|
||||
response = client.post("/api/v1/login", data=login_data)
|
||||
assert response.status_code == 401, response.json()
|
||||
assert response.json()["detail"] == "Inactive user", response.text
|
||||
|
||||
|
||||
def test_deactivated_user_cannot_access(client, deactivated_user, logged_in_headers):
|
||||
# Assuming the headers for deactivated_user
|
||||
response = client.get("/api/v1/users", headers=logged_in_headers)
|
||||
assert response.status_code == 403, response.json()
|
||||
assert response.json()["detail"] == "The user doesn't have enough privileges", response.text
|
||||
|
||||
|
||||
def test_data_consistency_after_update(client, active_user, logged_in_headers, super_user_headers):
|
||||
user_id = active_user.id
|
||||
update_data = UserUpdate(is_active=False)
|
||||
|
||||
response = client.patch(f"/api/v1/users/{user_id}", json=update_data.model_dump(), headers=super_user_headers)
|
||||
assert response.status_code == 200, response.json()
|
||||
|
||||
# Fetch the updated user from the database
|
||||
response = client.get("/api/v1/users/whoami", headers=logged_in_headers)
|
||||
assert response.status_code == 401, response.json()
|
||||
assert response.json()["detail"] == "User not found or is inactive."
|
||||
|
||||
|
||||
def test_data_consistency_after_delete(client, test_user, super_user_headers):
|
||||
user_id = test_user.get("id")
|
||||
response = client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers)
|
||||
assert response.status_code == 200, response.json()
|
||||
|
||||
# Attempt to fetch the deleted user from the database
|
||||
response = client.get("/api/v1/users", headers=super_user_headers)
|
||||
assert response.status_code == 200
|
||||
assert all(user["id"] != user_id for user in response.json()["users"])
|
||||
|
||||
|
||||
def test_inactive_user(client):
|
||||
# Create a user that is not active and has a last_login_at value
|
||||
with session_getter(get_db_service()) as session:
|
||||
user = User(
|
||||
username="inactiveuser",
|
||||
password=get_password_hash("testpassword"),
|
||||
is_active=False,
|
||||
last_login_at=datetime(2023, 1, 1, 0, 0, 0),
|
||||
)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
login_data = {"username": "inactiveuser", "password": "testpassword"}
|
||||
response = client.post("/api/v1/login", data=login_data)
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"] == "Inactive user"
|
||||
|
||||
|
||||
def test_add_user(client, test_user):
|
||||
assert test_user["username"] == "testuser"
|
||||
|
||||
|
||||
# This is not used in the Frontend at the moment
|
||||
# def test_read_current_user(client: TestClient, active_user):
|
||||
# # First we need to login to get the access token
|
||||
# login_data = {"username": "testuser", "password": "testpassword"}
|
||||
# response = client.post("/api/v1/login", data=login_data)
|
||||
# assert response.status_code == 200
|
||||
|
||||
# headers = {"Authorization": f"Bearer {response.json()['access_token']}"}
|
||||
|
||||
# response = client.get("/api/v1/user", headers=headers)
|
||||
# assert response.status_code == 200, response.json()
|
||||
# assert response.json()["username"] == "testuser"
|
||||
|
||||
|
||||
def test_read_all_users(client, super_user_headers):
|
||||
response = client.get("/api/v1/users", headers=super_user_headers)
|
||||
assert response.status_code == 200, response.json()
|
||||
assert isinstance(response.json()["users"], list)
|
||||
|
||||
|
||||
def test_normal_user_cant_read_all_users(client, logged_in_headers):
|
||||
response = client.get("/api/v1/users", headers=logged_in_headers)
|
||||
assert response.status_code == 403, response.json()
|
||||
assert response.json() == {"detail": "The user doesn't have enough privileges"}
|
||||
|
||||
|
||||
def test_patch_user(client, active_user, logged_in_headers):
|
||||
user_id = active_user.id
|
||||
update_data = UserUpdate(
|
||||
username="newname",
|
||||
)
|
||||
|
||||
response = client.patch(f"/api/v1/users/{user_id}", json=update_data.model_dump(), headers=logged_in_headers)
|
||||
assert response.status_code == 200, response.json()
|
||||
update_data = UserUpdate(
|
||||
profile_image="new_image",
|
||||
)
|
||||
|
||||
response = client.patch(f"/api/v1/users/{user_id}", json=update_data.model_dump(), headers=logged_in_headers)
|
||||
assert response.status_code == 200, response.json()
|
||||
|
||||
|
||||
def test_patch_reset_password(client, active_user, logged_in_headers):
|
||||
user_id = active_user.id
|
||||
update_data = UserUpdate(
|
||||
password="newpassword",
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/users/{user_id}/reset-password",
|
||||
json=update_data.model_dump(),
|
||||
headers=logged_in_headers,
|
||||
)
|
||||
assert response.status_code == 200, response.json()
|
||||
# Now we need to test if the new password works
|
||||
login_data = {"username": active_user.username, "password": "newpassword"}
|
||||
response = client.post("/api/v1/login", data=login_data)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_patch_user_wrong_id(client, active_user, logged_in_headers):
|
||||
user_id = "wrong_id"
|
||||
update_data = UserUpdate(
|
||||
username="newname",
|
||||
)
|
||||
|
||||
response = client.patch(f"/api/v1/users/{user_id}", json=update_data.model_dump(), headers=logged_in_headers)
|
||||
assert response.status_code == 422, response.json()
|
||||
json_response = response.json()
|
||||
detail = json_response["detail"]
|
||||
error = detail[0]
|
||||
assert error["loc"] == ["path", "user_id"]
|
||||
assert error["type"] == "uuid_parsing"
|
||||
|
||||
|
||||
def test_delete_user(client, test_user, super_user_headers):
|
||||
user_id = test_user["id"]
|
||||
response = client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"detail": "User deleted"}
|
||||
|
||||
|
||||
def test_delete_user_wrong_id(client, test_user, super_user_headers):
|
||||
user_id = "wrong_id"
|
||||
response = client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers)
|
||||
assert response.status_code == 422
|
||||
json_response = response.json()
|
||||
detail = json_response["detail"]
|
||||
error = detail[0]
|
||||
assert error["loc"] == ["path", "user_id"]
|
||||
assert error["type"] == "uuid_parsing"
|
||||
|
||||
|
||||
def test_normal_user_cant_delete_user(client, test_user, logged_in_headers):
|
||||
user_id = test_user["id"]
|
||||
response = client.delete(f"/api/v1/users/{user_id}", headers=logged_in_headers)
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "The user doesn't have enough privileges"}
|
||||
59
src/backend/tests/test_webhook.py
Normal file
59
src/backend/tests/test_webhook.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def check_openai_api_key_in_environment_variables():
|
||||
pass
|
||||
|
||||
|
||||
def test_webhook_endpoint(client, added_webhook_test):
|
||||
# The test is as follows:
|
||||
# 1. The flow when run will get a "path" from the payload and save a file with the path as the name.
|
||||
# We will create a temporary file path and send it to the webhook endpoint, then check if the file exists.
|
||||
# 2. we will delete the file, then send an invalid payload to the webhook endpoint and check if the file exists.
|
||||
endpoint_name = added_webhook_test["endpoint_name"]
|
||||
endpoint = f"api/v1/webhook/{endpoint_name}"
|
||||
# Create a temporary file
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
file_path = Path(tmp) / "test_file.txt"
|
||||
|
||||
payload = {"path": str(file_path)}
|
||||
|
||||
response = client.post(endpoint, json=payload)
|
||||
assert response.status_code == 202
|
||||
assert file_path.exists()
|
||||
|
||||
assert not file_path.exists()
|
||||
|
||||
# Send an invalid payload
|
||||
payload = {"invalid_key": "invalid_value"}
|
||||
response = client.post(endpoint, json=payload)
|
||||
assert response.status_code == 202
|
||||
assert not file_path.exists()
|
||||
|
||||
|
||||
def test_webhook_flow_on_run_endpoint(client, added_webhook_test, created_api_key):
|
||||
endpoint_name = added_webhook_test["endpoint_name"]
|
||||
endpoint = f"api/v1/run/{endpoint_name}?stream=false"
|
||||
# Just test that "Random Payload" returns 202
|
||||
# returns 202
|
||||
payload = {
|
||||
"output_type": "any",
|
||||
}
|
||||
response = client.post(endpoint, headers={"x-api-key": created_api_key.api_key}, json=payload)
|
||||
assert response.status_code == 200, response.json()
|
||||
|
||||
|
||||
def test_webhook_with_random_payload(client, added_webhook_test):
|
||||
endpoint_name = added_webhook_test["endpoint_name"]
|
||||
endpoint = f"api/v1/webhook/{endpoint_name}"
|
||||
# Just test that "Random Payload" returns 202
|
||||
# returns 202
|
||||
response = client.post(
|
||||
endpoint,
|
||||
json="Random Payload",
|
||||
)
|
||||
assert response.status_code == 202
|
||||
0
src/backend/tests/unit/__init__.py
Normal file
0
src/backend/tests/unit/__init__.py
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import pickle
|
||||
from collections import defaultdict
|
||||
|
||||
import pytest
|
||||
|
||||
from langflow.graph.graph.runnable_vertices_manager import RunnableVerticesManager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def data():
|
||||
run_map: defaultdict(list) = {"A": ["B", "C"], "B": ["D"], "C": ["D"], "D": []}
|
||||
run_predecessors: defaultdict(set) = {"A": set(), "B": {"A"}, "C": {"A"}, "D": {"B", "C"}}
|
||||
vertices_to_run: set = {"A", "B", "C"}
|
||||
vertices_being_run = {"A"}
|
||||
return {
|
||||
"run_map": run_map,
|
||||
"run_predecessors": run_predecessors,
|
||||
"vertices_to_run": vertices_to_run,
|
||||
"vertices_being_run": vertices_being_run,
|
||||
}
|
||||
|
||||
|
||||
def test_to_dict(data):
|
||||
result = RunnableVerticesManager.from_dict(data).to_dict()
|
||||
|
||||
assert all(key in result.keys() for key in data.keys())
|
||||
|
||||
|
||||
def test_from_dict(data):
|
||||
result = RunnableVerticesManager.from_dict(data)
|
||||
|
||||
assert isinstance(result, RunnableVerticesManager)
|
||||
|
||||
|
||||
def test_from_dict_without_run_map__bad_case(data):
|
||||
data.pop("run_map")
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
RunnableVerticesManager.from_dict(data)
|
||||
|
||||
|
||||
def test_from_dict_without_run_predecessors__bad_case(data):
|
||||
data.pop("run_predecessors")
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
RunnableVerticesManager.from_dict(data)
|
||||
|
||||
|
||||
def test_from_dict_without_vertices_to_run__bad_case(data):
|
||||
data.pop("vertices_to_run")
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
RunnableVerticesManager.from_dict(data)
|
||||
|
||||
|
||||
def test_from_dict_without_vertices_being_run__bad_case(data):
|
||||
data.pop("vertices_being_run")
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
RunnableVerticesManager.from_dict(data)
|
||||
|
||||
|
||||
def test_pickle(data):
|
||||
manager = RunnableVerticesManager.from_dict(data)
|
||||
|
||||
binary = pickle.dumps(manager)
|
||||
result = pickle.loads(binary)
|
||||
|
||||
assert result.run_map == manager.run_map
|
||||
assert result.run_predecessors == manager.run_predecessors
|
||||
assert result.vertices_to_run == manager.vertices_to_run
|
||||
assert result.vertices_being_run == manager.vertices_being_run
|
||||
|
||||
|
||||
def test_update_run_state(data):
|
||||
manager = RunnableVerticesManager.from_dict(data)
|
||||
run_predecessors = {"E": {"D"}}
|
||||
vertices_to_run = {"D"}
|
||||
|
||||
manager.update_run_state(run_predecessors, vertices_to_run)
|
||||
|
||||
assert "D" in manager.run_map
|
||||
assert "D" in manager.vertices_to_run
|
||||
assert "D" in manager.run_predecessors["E"]
|
||||
|
||||
|
||||
def test_is_vertex_runnable(data):
|
||||
manager = RunnableVerticesManager.from_dict(data)
|
||||
vertex_id = "A"
|
||||
is_active = True
|
||||
|
||||
result = manager.is_vertex_runnable(vertex_id, is_active)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_is_vertex_runnable__wrong_is_active(data):
|
||||
manager = RunnableVerticesManager.from_dict(data)
|
||||
vertex_id = "A"
|
||||
is_active = False
|
||||
|
||||
result = manager.is_vertex_runnable(vertex_id, is_active)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_is_vertex_runnable__wrong_vertices_to_run(data):
|
||||
manager = RunnableVerticesManager.from_dict(data)
|
||||
vertex_id = "D"
|
||||
is_active = True
|
||||
|
||||
result = manager.is_vertex_runnable(vertex_id, is_active)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_is_vertex_runnable__wrong_run_predecessors(data):
|
||||
manager = RunnableVerticesManager.from_dict(data)
|
||||
vertex_id = "C"
|
||||
is_active = True
|
||||
|
||||
result = manager.is_vertex_runnable(vertex_id, is_active)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_are_all_predecessors_fulfilled(data):
|
||||
manager = RunnableVerticesManager.from_dict(data)
|
||||
vertex_id = "A"
|
||||
|
||||
result = manager.are_all_predecessors_fulfilled(vertex_id)
|
||||
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_are_all_predecessors_fulfilled__wrong(data):
|
||||
manager = RunnableVerticesManager.from_dict(data)
|
||||
vertex_id = "D"
|
||||
|
||||
result = manager.are_all_predecessors_fulfilled(vertex_id)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_remove_from_predecessors(data):
|
||||
manager = RunnableVerticesManager.from_dict(data)
|
||||
vertex_id = "A"
|
||||
|
||||
manager.remove_from_predecessors(vertex_id)
|
||||
|
||||
assert all(vertex_id not in predecessors for predecessors in manager.run_predecessors.values())
|
||||
|
||||
|
||||
def test_build_run_map(data):
|
||||
manager = RunnableVerticesManager.from_dict(data)
|
||||
vertices_to_run = {}
|
||||
predecessor_map = {"Z": set(), "X": {"Z"}, "Y": {"Z"}, "W": {"X", "Y"}}
|
||||
|
||||
manager.build_run_map(predecessor_map, vertices_to_run)
|
||||
|
||||
assert all(v in manager.run_map.keys() for v in ["Z", "X", "Y"])
|
||||
assert "W" not in manager.run_map.keys()
|
||||
|
||||
|
||||
def test_update_vertex_run_state(data):
|
||||
manager = RunnableVerticesManager.from_dict(data)
|
||||
vertex_id = "C"
|
||||
is_runnable = True
|
||||
|
||||
manager.update_vertex_run_state(vertex_id, is_runnable)
|
||||
|
||||
assert vertex_id in manager.vertices_to_run
|
||||
|
||||
|
||||
def test_update_vertex_run_state__bad_case(data):
|
||||
manager = RunnableVerticesManager.from_dict(data)
|
||||
vertex_id = "C"
|
||||
is_runnable = False
|
||||
|
||||
manager.update_vertex_run_state(vertex_id, is_runnable)
|
||||
|
||||
assert vertex_id not in manager.vertices_being_run
|
||||
|
||||
|
||||
def test_remove_vertex_from_runnables(data):
|
||||
manager = RunnableVerticesManager.from_dict(data)
|
||||
vertex_id = "C"
|
||||
|
||||
manager.remove_vertex_from_runnables(vertex_id)
|
||||
|
||||
assert vertex_id not in manager.vertices_being_run
|
||||
|
||||
|
||||
def test_add_to_vertices_being_run(data):
|
||||
manager = RunnableVerticesManager.from_dict(data)
|
||||
vertex_id = "C"
|
||||
|
||||
manager.add_to_vertices_being_run(vertex_id)
|
||||
|
||||
assert vertex_id in manager.vertices_being_run
|
||||
122
src/backend/tests/unit/graph/graph/test_utils.py
Normal file
122
src/backend/tests/unit/graph/graph/test_utils.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import pytest
|
||||
|
||||
from langflow.graph.graph import utils
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def graph():
|
||||
return {
|
||||
"A": {"successors": ["B"], "predecessors": []},
|
||||
"B": {"successors": ["D"], "predecessors": ["A", "C"]},
|
||||
"C": {"successors": ["B", "I"], "predecessors": ["N"]},
|
||||
"D": {"successors": ["E", "F"], "predecessors": ["B"]},
|
||||
"E": {"successors": ["G"], "predecessors": ["D"]},
|
||||
"F": {"successors": ["G", "H"], "predecessors": ["D"]},
|
||||
"G": {"successors": [], "predecessors": ["E", "F"]},
|
||||
"H": {"successors": [], "predecessors": ["F"]},
|
||||
"I": {"successors": ["M"], "predecessors": ["C", "J"]},
|
||||
"J": {"successors": ["I", "K"], "predecessors": ["N"]},
|
||||
"K": {"successors": ["Q", "P", "O"], "predecessors": ["J", "L"]},
|
||||
"L": {"successors": ["K"], "predecessors": []},
|
||||
"M": {"successors": [], "predecessors": ["I"]},
|
||||
"N": {"successors": ["C", "J"], "predecessors": []},
|
||||
"O": {"successors": ["R"], "predecessors": ["K"]},
|
||||
"P": {"successors": ["U"], "predecessors": ["K"]},
|
||||
"Q": {"successors": ["V"], "predecessors": ["K"]},
|
||||
"R": {"successors": ["S"], "predecessors": ["O"]},
|
||||
"S": {"successors": ["T"], "predecessors": ["R"]},
|
||||
"T": {"successors": [], "predecessors": ["S"]},
|
||||
"U": {"successors": ["W"], "predecessors": ["P"]},
|
||||
"V": {"successors": ["Y"], "predecessors": ["Q"]},
|
||||
"W": {"successors": ["X"], "predecessors": ["U"]},
|
||||
"X": {"successors": [], "predecessors": ["W"]},
|
||||
"Y": {"successors": ["Z"], "predecessors": ["V"]},
|
||||
"Z": {"successors": [], "predecessors": ["Y"]},
|
||||
}
|
||||
|
||||
|
||||
def test_get_successors_a(graph):
|
||||
vertex_id = "A"
|
||||
|
||||
result = utils.get_successors(graph, vertex_id)
|
||||
|
||||
assert set(result) == {"A", "B", "D", "E", "F", "H", "G"}
|
||||
|
||||
|
||||
def test_get_successors_z(graph):
|
||||
vertex_id = "Z"
|
||||
|
||||
result = utils.get_successors(graph, vertex_id)
|
||||
|
||||
assert set(result) == {"Z"}
|
||||
|
||||
|
||||
def test_sort_up_to_vertex_n_is_start(graph):
|
||||
vertex_id = "N"
|
||||
|
||||
result = utils.sort_up_to_vertex(graph, vertex_id, is_start=True)
|
||||
# Result shoud be all the vertices
|
||||
assert set(result) == set(graph.keys())
|
||||
|
||||
|
||||
def test_sort_up_to_vertex_z(graph):
|
||||
vertex_id = "Z"
|
||||
|
||||
result = utils.sort_up_to_vertex(graph, vertex_id)
|
||||
|
||||
assert set(result) == {"L", "N", "J", "K", "Q", "V", "Y", "Z"}
|
||||
|
||||
|
||||
def test_sort_up_to_vertex_x(graph):
|
||||
vertex_id = "X"
|
||||
|
||||
result = utils.sort_up_to_vertex(graph, vertex_id)
|
||||
|
||||
assert set(result) == {"L", "N", "J", "K", "P", "U", "W", "X"}
|
||||
|
||||
|
||||
def test_sort_up_to_vertex_t(graph):
|
||||
vertex_id = "T"
|
||||
|
||||
result = utils.sort_up_to_vertex(graph, vertex_id)
|
||||
|
||||
assert set(result) == {"L", "N", "J", "K", "O", "R", "S", "T"}
|
||||
|
||||
|
||||
def test_sort_up_to_vertex_m(graph):
|
||||
vertex_id = "M"
|
||||
|
||||
result = utils.sort_up_to_vertex(graph, vertex_id)
|
||||
|
||||
assert set(result) == {"N", "C", "J", "I", "M"}
|
||||
|
||||
|
||||
def test_sort_up_to_vertex_h(graph):
|
||||
vertex_id = "H"
|
||||
|
||||
result = utils.sort_up_to_vertex(graph, vertex_id)
|
||||
|
||||
assert set(result) == {"N", "C", "A", "B", "D", "F", "H"}
|
||||
|
||||
|
||||
def test_sort_up_to_vertex_g(graph):
|
||||
vertex_id = "G"
|
||||
|
||||
result = utils.sort_up_to_vertex(graph, vertex_id)
|
||||
|
||||
assert set(result) == {"N", "C", "A", "B", "D", "F", "E", "G"}
|
||||
|
||||
|
||||
def test_sort_up_to_vertex_a(graph):
|
||||
vertex_id = "A"
|
||||
|
||||
result = utils.sort_up_to_vertex(graph, vertex_id)
|
||||
|
||||
assert set(result) == {"A"}
|
||||
|
||||
|
||||
def test_sort_up_to_vertex_invalid_vertex(graph):
|
||||
vertex_id = "7"
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
utils.sort_up_to_vertex(graph, vertex_id)
|
||||
45
src/backend/tests/unit/test_api_key.py
Normal file
45
src/backend/tests/unit/test_api_key.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import pytest
|
||||
from langflow.services.database.models.api_key import ApiKeyCreate
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api_key(client, logged_in_headers, active_user):
|
||||
api_key = ApiKeyCreate(name="test-api-key")
|
||||
|
||||
response = client.post("api/v1/api_key", data=api_key.model_dump_json(), headers=logged_in_headers)
|
||||
assert response.status_code == 200, response.text
|
||||
return response.json()
|
||||
|
||||
|
||||
def test_get_api_keys(client, logged_in_headers, api_key):
|
||||
response = client.get("api/v1/api_key", headers=logged_in_headers)
|
||||
assert response.status_code == 200, response.text
|
||||
data = response.json()
|
||||
assert "total_count" in data
|
||||
assert "user_id" in data
|
||||
assert "api_keys" in data
|
||||
assert any("test-api-key" in api_key["name"] for api_key in data["api_keys"])
|
||||
# assert all api keys in data["api_keys"] are masked
|
||||
assert all("**" in api_key["api_key"] for api_key in data["api_keys"])
|
||||
|
||||
|
||||
def test_create_api_key(client, logged_in_headers):
|
||||
api_key_name = "test-api-key"
|
||||
response = client.post("api/v1/api_key", json={"name": api_key_name}, headers=logged_in_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "name" in data and data["name"] == api_key_name
|
||||
assert "api_key" in data
|
||||
# When creating the API key is returned which is
|
||||
# the only time the API key is unmasked
|
||||
assert "**" not in data["api_key"]
|
||||
|
||||
|
||||
def test_delete_api_key(client, logged_in_headers, active_user, api_key):
|
||||
# Assuming a function to create a test API key, returning the key ID
|
||||
api_key_id = api_key["id"]
|
||||
response = client.delete(f"api/v1/api_key/{api_key_id}", headers=logged_in_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["detail"] == "API Key deleted"
|
||||
# Optionally, add a follow-up check to ensure that the key is actually removed from the database
|
||||
47
src/backend/tests/unit/test_cache.py
Normal file
47
src/backend/tests/unit/test_cache.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from langflow.graph import Graph
|
||||
|
||||
|
||||
def get_graph(_type="basic"):
|
||||
"""Get a graph from a json file"""
|
||||
if _type == "basic":
|
||||
path = pytest.BASIC_EXAMPLE_PATH
|
||||
elif _type == "complex":
|
||||
path = pytest.COMPLEX_EXAMPLE_PATH
|
||||
elif _type == "openapi":
|
||||
path = pytest.OPENAPI_EXAMPLE_PATH
|
||||
|
||||
with open(path, "r") as f:
|
||||
flow_graph = json.load(f)
|
||||
return flow_graph["data"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def basic_data_graph():
|
||||
return get_graph()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def complex_data_graph():
|
||||
return get_graph("complex")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def openapi_data_graph():
|
||||
return get_graph("openapi")
|
||||
|
||||
|
||||
def langchain_objects_are_equal(obj1, obj2):
|
||||
return str(obj1) == str(obj2)
|
||||
|
||||
|
||||
# Test build_graph
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_graph(client, basic_data_graph):
|
||||
graph = Graph.from_payload(basic_data_graph)
|
||||
assert graph is not None
|
||||
assert len(graph.vertices) == len(basic_data_graph["nodes"])
|
||||
assert len(graph.edges) == len(basic_data_graph["edges"])
|
||||
37
src/backend/tests/unit/test_cli.py
Normal file
37
src/backend/tests/unit/test_cli.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
from pathlib import Path
|
||||
from tempfile import tempdir
|
||||
|
||||
import pytest
|
||||
from langflow.__main__ import app
|
||||
from langflow.services import deps
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def default_settings():
|
||||
return [
|
||||
"--backend-only",
|
||||
"--no-open-browser",
|
||||
]
|
||||
|
||||
|
||||
def test_components_path(runner, client, default_settings):
|
||||
# Create a foldr in the tmp directory
|
||||
|
||||
temp_dir = Path(tempdir)
|
||||
# create a "components" folder
|
||||
temp_dir = temp_dir / "components"
|
||||
temp_dir.mkdir(exist_ok=True)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["run", "--components-path", str(temp_dir), *default_settings],
|
||||
)
|
||||
assert result.exit_code == 0, result.stdout
|
||||
settings_service = deps.get_settings_service()
|
||||
assert str(temp_dir) in settings_service.settings.components_path
|
||||
|
||||
|
||||
def test_superuser(runner, client, session):
|
||||
result = runner.invoke(app, ["superuser"], input="admin\nadmin\n")
|
||||
assert result.exit_code == 0, result.stdout
|
||||
assert "Superuser created successfully." in result.stdout
|
||||
528
src/backend/tests/unit/test_custom_component.py
Normal file
528
src/backend/tests/unit/test_custom_component.py
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
import ast
|
||||
import types
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langflow.custom import Component, CustomComponent
|
||||
from langflow.custom.code_parser.code_parser import CodeParser, CodeSyntaxError
|
||||
from langflow.custom.custom_component.base_component import BaseComponent, ComponentCodeNullError
|
||||
from langflow.custom.utils import build_custom_component_template
|
||||
from langflow.services.database.models.flow import Flow, FlowCreate
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def code_component_with_multiple_outputs():
|
||||
with open("src/backend/tests/data/component_multiple_outputs.py", "r") as f:
|
||||
code = f.read()
|
||||
return Component(code=code)
|
||||
|
||||
|
||||
code_default = """
|
||||
from langflow.custom import CustomComponent
|
||||
|
||||
from langflow.field_typing import BaseLanguageModel
|
||||
from langchain.chains import LLMChain
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain_core.documents import Document
|
||||
|
||||
import requests
|
||||
|
||||
class YourComponent(CustomComponent):
|
||||
display_name: str = "Your Component"
|
||||
description: str = "Your description"
|
||||
field_config = { "url": { "multiline": True, "required": True } }
|
||||
|
||||
def build(self, url: str, llm: BaseLanguageModel) -> Document:
|
||||
return Document(page_content="Hello World")
|
||||
"""
|
||||
|
||||
|
||||
def test_code_parser_init():
|
||||
"""
|
||||
Test the initialization of the CodeParser class.
|
||||
"""
|
||||
parser = CodeParser(code_default)
|
||||
assert parser.code == code_default
|
||||
|
||||
|
||||
def test_code_parser_get_tree():
|
||||
"""
|
||||
Test the __get_tree method of the CodeParser class.
|
||||
"""
|
||||
parser = CodeParser(code_default)
|
||||
tree = parser.get_tree()
|
||||
assert isinstance(tree, ast.AST)
|
||||
|
||||
|
||||
def test_code_parser_syntax_error():
|
||||
"""
|
||||
Test the __get_tree method raises the
|
||||
CodeSyntaxError when given incorrect syntax.
|
||||
"""
|
||||
code_syntax_error = "zzz import os"
|
||||
|
||||
parser = CodeParser(code_syntax_error)
|
||||
with pytest.raises(CodeSyntaxError):
|
||||
parser.get_tree()
|
||||
|
||||
|
||||
def test_component_init():
|
||||
"""
|
||||
Test the initialization of the Component class.
|
||||
"""
|
||||
component = BaseComponent(code=code_default, function_entrypoint_name="build")
|
||||
assert component.code == code_default
|
||||
assert component.function_entrypoint_name == "build"
|
||||
|
||||
|
||||
def test_component_get_code_tree():
|
||||
"""
|
||||
Test the get_code_tree method of the Component class.
|
||||
"""
|
||||
component = BaseComponent(code=code_default, function_entrypoint_name="build")
|
||||
tree = component.get_code_tree(component.code)
|
||||
assert "imports" in tree
|
||||
|
||||
|
||||
def test_component_code_null_error():
|
||||
"""
|
||||
Test the get_function method raises the
|
||||
ComponentCodeNullError when the code is empty.
|
||||
"""
|
||||
component = BaseComponent(code="", function_entrypoint_name="")
|
||||
with pytest.raises(ComponentCodeNullError):
|
||||
component.get_function()
|
||||
|
||||
|
||||
def test_custom_component_init():
|
||||
"""
|
||||
Test the initialization of the CustomComponent class.
|
||||
"""
|
||||
function_entrypoint_name = "build"
|
||||
|
||||
custom_component = CustomComponent(code=code_default, function_entrypoint_name=function_entrypoint_name)
|
||||
assert custom_component.code == code_default
|
||||
assert custom_component.function_entrypoint_name == function_entrypoint_name
|
||||
|
||||
|
||||
def test_custom_component_build_template_config():
|
||||
"""
|
||||
Test the build_template_config property of the CustomComponent class.
|
||||
"""
|
||||
custom_component = CustomComponent(code=code_default, function_entrypoint_name="build")
|
||||
config = custom_component.build_template_config()
|
||||
assert isinstance(config, dict)
|
||||
|
||||
|
||||
def test_custom_component_get_function():
|
||||
"""
|
||||
Test the get_function property of the CustomComponent class.
|
||||
"""
|
||||
custom_component = CustomComponent(code="def build(): pass", function_entrypoint_name="build")
|
||||
my_function = custom_component.get_function()
|
||||
assert isinstance(my_function, types.FunctionType)
|
||||
|
||||
|
||||
def test_code_parser_parse_imports_import():
|
||||
"""
|
||||
Test the parse_imports method of the CodeParser
|
||||
class with an import statement.
|
||||
"""
|
||||
parser = CodeParser(code_default)
|
||||
tree = parser.get_tree()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
parser.parse_imports(node)
|
||||
assert "requests" in parser.data["imports"]
|
||||
|
||||
|
||||
def test_code_parser_parse_imports_importfrom():
|
||||
"""
|
||||
Test the parse_imports method of the CodeParser
|
||||
class with an import from statement.
|
||||
"""
|
||||
parser = CodeParser("from os import path")
|
||||
tree = parser.get_tree()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
parser.parse_imports(node)
|
||||
assert ("os", "path") in parser.data["imports"]
|
||||
|
||||
|
||||
def test_code_parser_parse_functions():
|
||||
"""
|
||||
Test the parse_functions method of the CodeParser class.
|
||||
"""
|
||||
parser = CodeParser("def test(): pass")
|
||||
tree = parser.get_tree()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
parser.parse_functions(node)
|
||||
assert len(parser.data["functions"]) == 1
|
||||
assert parser.data["functions"][0]["name"] == "test"
|
||||
|
||||
|
||||
def test_code_parser_parse_classes():
|
||||
"""
|
||||
Test the parse_classes method of the CodeParser class.
|
||||
"""
|
||||
parser = CodeParser("class Test: pass")
|
||||
tree = parser.get_tree()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ClassDef):
|
||||
parser.parse_classes(node)
|
||||
assert len(parser.data["classes"]) == 1
|
||||
assert parser.data["classes"][0]["name"] == "Test"
|
||||
|
||||
|
||||
def test_code_parser_parse_global_vars():
|
||||
"""
|
||||
Test the parse_global_vars method of the CodeParser class.
|
||||
"""
|
||||
parser = CodeParser("x = 1")
|
||||
tree = parser.get_tree()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Assign):
|
||||
parser.parse_global_vars(node)
|
||||
assert len(parser.data["global_vars"]) == 1
|
||||
assert parser.data["global_vars"][0]["targets"] == ["x"]
|
||||
|
||||
|
||||
def test_component_get_function_valid():
|
||||
"""
|
||||
Test the get_function method of the Component
|
||||
class with valid code and function_entrypoint_name.
|
||||
"""
|
||||
component = BaseComponent(code="def build(): pass", function_entrypoint_name="build")
|
||||
my_function = component.get_function()
|
||||
assert callable(my_function)
|
||||
|
||||
|
||||
def test_custom_component_get_function_entrypoint_args():
|
||||
"""
|
||||
Test the get_function_entrypoint_args
|
||||
property of the CustomComponent class.
|
||||
"""
|
||||
custom_component = CustomComponent(code=code_default, function_entrypoint_name="build")
|
||||
args = custom_component.get_function_entrypoint_args
|
||||
assert len(args) == 3
|
||||
assert args[0]["name"] == "self"
|
||||
assert args[1]["name"] == "url"
|
||||
assert args[2]["name"] == "llm"
|
||||
|
||||
|
||||
def test_custom_component_get_function_entrypoint_return_type():
|
||||
"""
|
||||
Test the get_function_entrypoint_return_type
|
||||
property of the CustomComponent class.
|
||||
"""
|
||||
|
||||
custom_component = CustomComponent(code=code_default, function_entrypoint_name="build")
|
||||
return_type = custom_component.get_function_entrypoint_return_type
|
||||
assert return_type == [Document]
|
||||
|
||||
|
||||
def test_custom_component_get_main_class_name():
|
||||
"""
|
||||
Test the get_main_class_name property of the CustomComponent class.
|
||||
"""
|
||||
custom_component = CustomComponent(code=code_default, function_entrypoint_name="build")
|
||||
class_name = custom_component.get_main_class_name
|
||||
assert class_name == "YourComponent"
|
||||
|
||||
|
||||
def test_custom_component_get_function_valid():
|
||||
"""
|
||||
Test the get_function property of the CustomComponent
|
||||
class with valid code and function_entrypoint_name.
|
||||
"""
|
||||
custom_component = CustomComponent(code="def build(): pass", function_entrypoint_name="build")
|
||||
my_function = custom_component.get_function
|
||||
assert callable(my_function)
|
||||
|
||||
|
||||
def test_code_parser_parse_arg_no_annotation():
|
||||
"""
|
||||
Test the parse_arg method of the CodeParser class without an annotation.
|
||||
"""
|
||||
parser = CodeParser("")
|
||||
arg = ast.arg(arg="x", annotation=None)
|
||||
result = parser.parse_arg(arg, None)
|
||||
assert result["name"] == "x"
|
||||
assert "type" not in result
|
||||
|
||||
|
||||
def test_code_parser_parse_arg_with_annotation():
|
||||
"""
|
||||
Test the parse_arg method of the CodeParser class with an annotation.
|
||||
"""
|
||||
parser = CodeParser("")
|
||||
arg = ast.arg(arg="x", annotation=ast.Name(id="int", ctx=ast.Load()))
|
||||
result = parser.parse_arg(arg, None)
|
||||
assert result["name"] == "x"
|
||||
assert result["type"] == "int"
|
||||
|
||||
|
||||
def test_code_parser_parse_callable_details_no_args():
|
||||
"""
|
||||
Test the parse_callable_details method of the
|
||||
CodeParser class with a function with no arguments.
|
||||
"""
|
||||
parser = CodeParser("")
|
||||
node = ast.FunctionDef(
|
||||
name="test",
|
||||
args=ast.arguments(args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]),
|
||||
body=[],
|
||||
decorator_list=[],
|
||||
returns=None,
|
||||
)
|
||||
result = parser.parse_callable_details(node)
|
||||
assert result["name"] == "test"
|
||||
assert len(result["args"]) == 0
|
||||
|
||||
|
||||
def test_code_parser_parse_assign():
|
||||
"""
|
||||
Test the parse_assign method of the CodeParser class.
|
||||
"""
|
||||
parser = CodeParser("")
|
||||
stmt = ast.Assign(targets=[ast.Name(id="x", ctx=ast.Store())], value=ast.Num(n=1))
|
||||
result = parser.parse_assign(stmt)
|
||||
assert result["name"] == "x"
|
||||
assert result["value"] == "1"
|
||||
|
||||
|
||||
def test_code_parser_parse_ann_assign():
|
||||
"""
|
||||
Test the parse_ann_assign method of the CodeParser class.
|
||||
"""
|
||||
parser = CodeParser("")
|
||||
stmt = ast.AnnAssign(
|
||||
target=ast.Name(id="x", ctx=ast.Store()),
|
||||
annotation=ast.Name(id="int", ctx=ast.Load()),
|
||||
value=ast.Num(n=1),
|
||||
simple=1,
|
||||
)
|
||||
result = parser.parse_ann_assign(stmt)
|
||||
assert result["name"] == "x"
|
||||
assert result["value"] == "1"
|
||||
assert result["annotation"] == "int"
|
||||
|
||||
|
||||
def test_code_parser_parse_function_def_not_init():
|
||||
"""
|
||||
Test the parse_function_def method of the
|
||||
CodeParser class with a function that is not __init__.
|
||||
"""
|
||||
parser = CodeParser("")
|
||||
stmt = ast.FunctionDef(
|
||||
name="test",
|
||||
args=ast.arguments(args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]),
|
||||
body=[],
|
||||
decorator_list=[],
|
||||
returns=None,
|
||||
)
|
||||
result, is_init = parser.parse_function_def(stmt)
|
||||
assert result["name"] == "test"
|
||||
assert not is_init
|
||||
|
||||
|
||||
def test_code_parser_parse_function_def_init():
|
||||
"""
|
||||
Test the parse_function_def method of the
|
||||
CodeParser class with an __init__ function.
|
||||
"""
|
||||
parser = CodeParser("")
|
||||
stmt = ast.FunctionDef(
|
||||
name="__init__",
|
||||
args=ast.arguments(args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]),
|
||||
body=[],
|
||||
decorator_list=[],
|
||||
returns=None,
|
||||
)
|
||||
result, is_init = parser.parse_function_def(stmt)
|
||||
assert result["name"] == "__init__"
|
||||
assert is_init
|
||||
|
||||
|
||||
def test_component_get_code_tree_syntax_error():
|
||||
"""
|
||||
Test the get_code_tree method of the Component class
|
||||
raises the CodeSyntaxError when given incorrect syntax.
|
||||
"""
|
||||
component = BaseComponent(code="import os as", function_entrypoint_name="build")
|
||||
with pytest.raises(CodeSyntaxError):
|
||||
component.get_code_tree(component.code)
|
||||
|
||||
|
||||
def test_custom_component_class_template_validation_no_code():
|
||||
"""
|
||||
Test the _class_template_validation method of the CustomComponent class
|
||||
raises the HTTPException when the code is None.
|
||||
"""
|
||||
custom_component = CustomComponent(code=None, function_entrypoint_name="build")
|
||||
with pytest.raises(TypeError):
|
||||
custom_component.get_function()
|
||||
|
||||
|
||||
def test_custom_component_get_code_tree_syntax_error():
|
||||
"""
|
||||
Test the get_code_tree method of the CustomComponent class
|
||||
raises the CodeSyntaxError when given incorrect syntax.
|
||||
"""
|
||||
custom_component = CustomComponent(code="import os as", function_entrypoint_name="build")
|
||||
with pytest.raises(CodeSyntaxError):
|
||||
custom_component.get_code_tree(custom_component.code)
|
||||
|
||||
|
||||
def test_custom_component_get_function_entrypoint_args_no_args():
|
||||
"""
|
||||
Test the get_function_entrypoint_args property of
|
||||
the CustomComponent class with a build method with no arguments.
|
||||
"""
|
||||
my_code = """
|
||||
class MyMainClass(CustomComponent):
|
||||
def build():
|
||||
pass"""
|
||||
|
||||
custom_component = CustomComponent(code=my_code, function_entrypoint_name="build")
|
||||
args = custom_component.get_function_entrypoint_args
|
||||
assert len(args) == 0
|
||||
|
||||
|
||||
def test_custom_component_get_function_entrypoint_return_type_no_return_type():
|
||||
"""
|
||||
Test the get_function_entrypoint_return_type property of the
|
||||
CustomComponent class with a build method with no return type.
|
||||
"""
|
||||
my_code = """
|
||||
class MyClass(CustomComponent):
|
||||
def build():
|
||||
pass"""
|
||||
|
||||
custom_component = CustomComponent(code=my_code, function_entrypoint_name="build")
|
||||
return_type = custom_component.get_function_entrypoint_return_type
|
||||
assert return_type == []
|
||||
|
||||
|
||||
def test_custom_component_get_main_class_name_no_main_class():
|
||||
"""
|
||||
Test the get_main_class_name property of the
|
||||
CustomComponent class when there is no main class.
|
||||
"""
|
||||
my_code = """
|
||||
def build():
|
||||
pass"""
|
||||
|
||||
custom_component = CustomComponent(code=my_code, function_entrypoint_name="build")
|
||||
class_name = custom_component.get_main_class_name
|
||||
assert class_name == ""
|
||||
|
||||
|
||||
def test_custom_component_build_not_implemented():
|
||||
"""
|
||||
Test the build method of the CustomComponent
|
||||
class raises the NotImplementedError.
|
||||
"""
|
||||
custom_component = CustomComponent(code="def build(): pass", function_entrypoint_name="build")
|
||||
with pytest.raises(NotImplementedError):
|
||||
custom_component.build()
|
||||
|
||||
|
||||
def test_build_config_no_code():
|
||||
component = CustomComponent(code=None)
|
||||
|
||||
assert component.get_function_entrypoint_args == []
|
||||
assert component.get_function_entrypoint_return_type == []
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def component(client, active_user):
|
||||
return CustomComponent(
|
||||
user_id=active_user.id,
|
||||
field_config={
|
||||
"fields": {
|
||||
"llm": {"type": "str"},
|
||||
"url": {"type": "str"},
|
||||
"year": {"type": "int"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_flow(db):
|
||||
flow_data = {
|
||||
"nodes": [{"id": "1"}, {"id": "2"}],
|
||||
"edges": [{"source": "1", "target": "2"}],
|
||||
}
|
||||
|
||||
# Create flow
|
||||
flow = FlowCreate(id=uuid4(), name="Test Flow", description="Fixture flow", data=flow_data)
|
||||
|
||||
# Add to database
|
||||
db.add(flow)
|
||||
db.commit()
|
||||
|
||||
yield flow
|
||||
|
||||
# Clean up
|
||||
db.delete(flow)
|
||||
db.commit()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def db(app):
|
||||
# Setup database for tests
|
||||
yield app.db
|
||||
|
||||
# Teardown
|
||||
app.db.drop_all()
|
||||
|
||||
|
||||
def test_list_flows_return_type(component):
|
||||
flows = component.list_flows()
|
||||
assert isinstance(flows, list)
|
||||
|
||||
|
||||
def test_list_flows_flow_objects(component):
|
||||
flows = component.list_flows()
|
||||
assert all(isinstance(flow, Flow) for flow in flows)
|
||||
|
||||
|
||||
def test_build_config_return_type(component):
|
||||
config = component.build_config()
|
||||
assert isinstance(config, dict)
|
||||
|
||||
|
||||
def test_build_config_has_fields(component):
|
||||
config = component.build_config()
|
||||
assert "fields" in config
|
||||
|
||||
|
||||
def test_build_config_fields_dict(component):
|
||||
config = component.build_config()
|
||||
assert isinstance(config["fields"], dict)
|
||||
|
||||
|
||||
def test_build_config_field_keys(component):
|
||||
config = component.build_config()
|
||||
assert all(isinstance(key, str) for key in config["fields"])
|
||||
|
||||
|
||||
def test_build_config_field_values_dict(component):
|
||||
config = component.build_config()
|
||||
assert all(isinstance(value, dict) for value in config["fields"].values())
|
||||
|
||||
|
||||
def test_build_config_field_value_keys(component):
|
||||
config = component.build_config()
|
||||
field_values = config["fields"].values()
|
||||
assert all("type" in value for value in field_values)
|
||||
|
||||
|
||||
def test_custom_component_multiple_outputs(code_component_with_multiple_outputs, active_user):
|
||||
frontnd_node_dict, _ = build_custom_component_template(code_component_with_multiple_outputs, active_user.id)
|
||||
assert frontnd_node_dict["outputs"][0]["types"] == ["Text"]
|
||||
140
src/backend/tests/unit/test_data_class.py
Normal file
140
src/backend/tests/unit/test_data_class.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import pytest
|
||||
from langchain_core.documents import Document
|
||||
from langflow.schema import Data
|
||||
|
||||
|
||||
def test_data_initialization():
|
||||
record = Data(text_key="msg", data={"msg": "Hello, World!", "extra": "value"})
|
||||
assert record.msg == "Hello, World!"
|
||||
assert record.extra == "value"
|
||||
|
||||
|
||||
def test_validate_data_with_extra_keys():
|
||||
record = Data(dummy_key="dummy", data={"key": "value"})
|
||||
assert record.data["dummy_key"] == "dummy"
|
||||
assert "dummy_key" in record.data
|
||||
assert record.key == "value"
|
||||
|
||||
|
||||
def test_conversion_to_document():
|
||||
record = Data(data={"text": "Sample text", "meta": "data"})
|
||||
document = record.to_lc_document()
|
||||
assert document.page_content == "Sample text"
|
||||
assert document.metadata == {"meta": "data"}
|
||||
|
||||
|
||||
def test_conversion_from_document():
|
||||
document = Document(page_content="Doc content", metadata={"meta": "info"})
|
||||
record = Data.from_document(document)
|
||||
assert record.text == "Doc content"
|
||||
assert record.meta == "info"
|
||||
|
||||
|
||||
def test_add_method_for_strings():
|
||||
record1 = Data(data={"text": "Hello"})
|
||||
record2 = Data(data={"text": " World"})
|
||||
combined = record1 + record2
|
||||
assert combined.text == "Hello World"
|
||||
|
||||
|
||||
def test_add_method_for_integers():
|
||||
record1 = Data(data={"number": 5})
|
||||
record2 = Data(data={"number": 10})
|
||||
combined = record1 + record2
|
||||
assert combined.number == 15
|
||||
|
||||
|
||||
def test_add_method_with_non_overlapping_keys():
|
||||
record1 = Data(data={"text": "Hello"})
|
||||
record2 = Data(data={"number": 10})
|
||||
combined = record1 + record2
|
||||
assert combined.text == "Hello"
|
||||
assert combined.number == 10
|
||||
|
||||
|
||||
def test_custom_attribute_get_set_del():
|
||||
record = Data()
|
||||
record.custom_attr = "custom_value"
|
||||
assert record.custom_attr == "custom_value"
|
||||
del record.custom_attr
|
||||
with pytest.raises(AttributeError):
|
||||
_ = record.custom_attr
|
||||
|
||||
|
||||
def test_deep_copy():
|
||||
import copy
|
||||
|
||||
record1 = Data(data={"text": "Hello", "number": 10})
|
||||
record2 = copy.deepcopy(record1)
|
||||
assert record2.text == "Hello"
|
||||
assert record2.number == 10
|
||||
record2.text = "World"
|
||||
assert record1.text == "Hello" # Ensure original is unchanged
|
||||
|
||||
|
||||
def test_custom_attribute_setting_and_getting():
|
||||
record = Data()
|
||||
record.dynamic_attribute = "Dynamic Value"
|
||||
assert record.dynamic_attribute == "Dynamic Value"
|
||||
|
||||
|
||||
def test_str_and_dir_methods():
|
||||
record = Data(text_key="text", data={"text": "Test Text", "key": "value"})
|
||||
assert "Test Text" in str(record)
|
||||
assert "key" in dir(record)
|
||||
assert "data" in dir(record)
|
||||
|
||||
|
||||
def test_dir_includes_data_keys():
|
||||
record = Data(data={"text": "Hello", "new_attr": "value"})
|
||||
dir_output = dir(record)
|
||||
|
||||
# Check for standard attributes
|
||||
assert "data" in dir_output
|
||||
assert "text_key" in dir_output
|
||||
assert "__add__" in dir_output # Checking for a method
|
||||
|
||||
# Check for dynamic attributes from data
|
||||
assert "text" in dir_output
|
||||
assert "new_attr" in dir_output
|
||||
|
||||
# Optionally, verify that dynamically added attributes are listed
|
||||
record.dynamic_attr = "dynamic"
|
||||
assert "dynamic_attr" in dir_output or "dynamic_attr" in dir(record) # To account for the change
|
||||
|
||||
|
||||
def test_dir_reflects_attribute_deletion():
|
||||
record = Data(data={"removable": "I can be removed"})
|
||||
assert "removable" in dir(record)
|
||||
|
||||
# Delete the attribute and check again
|
||||
del record.removable
|
||||
assert "removable" not in dir(record)
|
||||
|
||||
|
||||
def test_get_text_with_text_key():
|
||||
data = {"text": "Hello, World!"}
|
||||
schema = Data(data=data, text_key="text", default_value="default")
|
||||
result = schema.get_text()
|
||||
assert result == "Hello, World!"
|
||||
|
||||
|
||||
def test_get_text_without_text_key():
|
||||
data = {"other_key": "Hello, World!"}
|
||||
schema = Data(data=data, text_key="text", default_value="default")
|
||||
result = schema.get_text()
|
||||
assert result == "default"
|
||||
|
||||
|
||||
def test_get_text_with_empty_data():
|
||||
data = {}
|
||||
schema = Data(data=data, text_key="text", default_value="default")
|
||||
result = schema.get_text()
|
||||
assert result == "default"
|
||||
|
||||
|
||||
def test_get_text_with_none_data():
|
||||
data = None
|
||||
schema = Data(data=data, text_key="text", default_value="default")
|
||||
result = schema.get_text()
|
||||
assert result == "default"
|
||||
201
src/backend/tests/unit/test_data_components.py
Normal file
201
src/backend/tests/unit/test_data_components.py
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
from dictdiffer import diff
|
||||
from httpx import Response
|
||||
|
||||
from langflow.components import data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api_request():
|
||||
# This fixture provides an instance of APIRequest for each test case
|
||||
return data.APIRequestComponent()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_successful_get_request(api_request):
|
||||
# Mocking a successful GET request
|
||||
url = "https://example.com/api/test"
|
||||
method = "GET"
|
||||
mock_response = {"success": True}
|
||||
respx.get(url).mock(return_value=Response(200, json=mock_response))
|
||||
|
||||
# Making the request
|
||||
result = await api_request.make_request(client=httpx.AsyncClient(), method=method, url=url)
|
||||
|
||||
# Assertions
|
||||
assert result.data["status_code"] == 200
|
||||
assert result.data["result"] == mock_response
|
||||
|
||||
|
||||
def test_parse_curl(api_request):
|
||||
# Arrange
|
||||
field_value = (
|
||||
"curl -X GET https://example.com/api/test -H 'Content-Type: application/json' -d '{\"key\": \"value\"}'"
|
||||
)
|
||||
build_config = {
|
||||
"method": {"value": ""},
|
||||
"urls": {"value": []},
|
||||
"headers": {},
|
||||
"body": {},
|
||||
}
|
||||
# Act
|
||||
new_build_config = api_request.parse_curl(field_value, build_config.copy())
|
||||
|
||||
# Assert
|
||||
assert new_build_config["method"]["value"] == "GET"
|
||||
assert new_build_config["urls"]["value"] == ["https://example.com/api/test"]
|
||||
assert new_build_config["headers"]["value"] == {"Content-Type": "application/json"}
|
||||
assert new_build_config["body"]["value"] == {"key": "value"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_failed_request(api_request):
|
||||
# Mocking a failed GET request
|
||||
url = "https://example.com/api/test"
|
||||
method = "GET"
|
||||
respx.get(url).mock(return_value=Response(404))
|
||||
|
||||
# Making the request
|
||||
result = await api_request.make_request(client=httpx.AsyncClient(), method=method, url=url)
|
||||
|
||||
# Assertions
|
||||
assert result.data["status_code"] == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_timeout(api_request):
|
||||
# Mocking a timeout
|
||||
url = "https://example.com/api/timeout"
|
||||
method = "GET"
|
||||
respx.get(url).mock(side_effect=httpx.TimeoutException(message="Timeout", request=None))
|
||||
|
||||
# Making the request
|
||||
result = await api_request.make_request(client=httpx.AsyncClient(), method=method, url=url, timeout=1)
|
||||
|
||||
# Assertions
|
||||
assert result.data["status_code"] == 408
|
||||
assert result.data["error"] == "Request timed out"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_build_with_multiple_urls(api_request):
|
||||
# This test depends on having a working internet connection and accessible URLs
|
||||
# It's better to mock these requests using respx or a similar library
|
||||
|
||||
# Setup for multiple URLs
|
||||
method = "GET"
|
||||
urls = ["https://example.com/api/one", "https://example.com/api/two"]
|
||||
# You would mock these requests similarly to the single request tests
|
||||
for url in urls:
|
||||
respx.get(url).mock(return_value=Response(200, json={"success": True}))
|
||||
|
||||
# Do I have to mock the async client?
|
||||
#
|
||||
|
||||
# Execute the build method
|
||||
api_request.set_attributes(
|
||||
{
|
||||
"method": method,
|
||||
"urls": urls,
|
||||
}
|
||||
)
|
||||
results = await api_request.make_requests()
|
||||
|
||||
# Assertions
|
||||
assert len(results) == len(urls)
|
||||
|
||||
|
||||
@patch("langflow.components.data.Directory.parallel_load_data")
|
||||
@patch("langflow.components.data.Directory.retrieve_file_paths")
|
||||
@patch("langflow.components.data.DirectoryComponent.resolve_path")
|
||||
def test_directory_component_build_with_multithreading(
|
||||
mock_resolve_path, mock_retrieve_file_paths, mock_parallel_load_data
|
||||
):
|
||||
# Arrange
|
||||
directory_component = data.DirectoryComponent()
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
depth = 1
|
||||
max_concurrency = 2
|
||||
load_hidden = False
|
||||
recursive = True
|
||||
silent_errors = False
|
||||
use_multithreading = True
|
||||
|
||||
mock_resolve_path.return_value = path
|
||||
mock_retrieve_file_paths.return_value = [
|
||||
os.path.join(path, file) for file in os.listdir(path) if file.endswith(".py")
|
||||
]
|
||||
mock_parallel_load_data.return_value = [Mock()]
|
||||
|
||||
# Act
|
||||
directory_component.set_attributes(
|
||||
{
|
||||
"path": path,
|
||||
"depth": depth,
|
||||
"max_concurrency": max_concurrency,
|
||||
"load_hidden": load_hidden,
|
||||
"recursive": recursive,
|
||||
"silent_errors": silent_errors,
|
||||
"use_multithreading": use_multithreading,
|
||||
}
|
||||
)
|
||||
directory_component.load_directory()
|
||||
|
||||
# Assert
|
||||
mock_resolve_path.assert_called_once_with(path)
|
||||
mock_retrieve_file_paths.assert_called_once_with(path, load_hidden, recursive, depth)
|
||||
mock_parallel_load_data.assert_called_once_with(
|
||||
mock_retrieve_file_paths.return_value, silent_errors, max_concurrency
|
||||
)
|
||||
|
||||
|
||||
def test_directory_without_mocks():
|
||||
directory_component = data.DirectoryComponent()
|
||||
from langflow.initial_setup import setup
|
||||
from langflow.initial_setup.setup import load_starter_projects
|
||||
|
||||
_, projects = zip(*load_starter_projects())
|
||||
# the setup module has a folder where the projects are stored
|
||||
# the contents of that folder are in the projects variable
|
||||
# the directory component can be used to load the projects
|
||||
# and we can validate if the contents are the same as the projects variable
|
||||
setup_path = Path(setup.__file__).parent / "starter_projects"
|
||||
directory_component.set_attributes({"path": str(setup_path), "use_multithreading": False})
|
||||
results = directory_component.load_directory()
|
||||
assert len(results) == len(projects)
|
||||
# each result is a Data that contains the content attribute
|
||||
# each are dict that are exactly the same as one of the projects
|
||||
for i, result in enumerate(results):
|
||||
file_dict = json.loads(result.text)
|
||||
assert file_dict in projects, list(diff(file_dict, projects[i]))
|
||||
|
||||
# in ../docs/docs/components there are many mdx files
|
||||
# check if the directory component can load them
|
||||
# just check if the number of results is the same as the number of files
|
||||
directory_component = data.DirectoryComponent()
|
||||
docs_path = Path(__file__).parent.parent.parent.parent.parent / "docs" / "docs" / "Components"
|
||||
directory_component.set_attributes({"path": str(docs_path), "use_multithreading": False})
|
||||
results = directory_component.load_directory()
|
||||
docs_files = list(docs_path.glob("*.md")) + list(docs_path.glob("*.json"))
|
||||
assert len(results) == len(docs_files)
|
||||
|
||||
|
||||
def test_url_component():
|
||||
url_component = data.URLComponent()
|
||||
url_component.set_attributes({"urls": ["https://langflow.org"]})
|
||||
# the url component can be used to load the contents of a website
|
||||
_data = url_component.fetch_content()
|
||||
assert all(value.data for value in _data)
|
||||
assert all(value.text for value in _data)
|
||||
assert all(value.source for value in _data)
|
||||
281
src/backend/tests/unit/test_database.py
Normal file
281
src/backend/tests/unit/test_database.py
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
from uuid import UUID, uuid4
|
||||
|
||||
import orjson
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlmodel import Session
|
||||
|
||||
from langflow.api.v1.schemas import FlowListCreate
|
||||
from langflow.initial_setup.setup import load_starter_projects, load_flows_from_directory
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
from langflow.services.database.models.flow import Flow, FlowCreate, FlowUpdate
|
||||
from langflow.services.database.utils import session_getter
|
||||
from langflow.services.deps import get_db_service
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def json_style():
|
||||
# class FlowStyleBase(SQLModel):
|
||||
# color: str = Field(index=True)
|
||||
# emoji: str = Field(index=False)
|
||||
# flow_id: UUID = Field(default=None, foreign_key="flow.id")
|
||||
return orjson_dumps(
|
||||
{
|
||||
"color": "red",
|
||||
"emoji": "👍",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_create_flow(client: TestClient, json_flow: str, active_user, logged_in_headers):
|
||||
flow = orjson.loads(json_flow)
|
||||
data = flow["data"]
|
||||
flow = FlowCreate(name=str(uuid4()), description="description", data=data)
|
||||
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||
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.model_dump(exclude_unset=True), headers=logged_in_headers)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["name"] == flow.name
|
||||
assert response.json()["data"] == flow.data
|
||||
|
||||
|
||||
def test_read_flows(client: TestClient, json_flow: str, active_user, logged_in_headers):
|
||||
flow_data = orjson.loads(json_flow)
|
||||
data = flow_data["data"]
|
||||
flow = FlowCreate(name=str(uuid4()), description="description", data=data)
|
||||
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["name"] == flow.name
|
||||
assert response.json()["data"] == flow.data
|
||||
|
||||
flow = FlowCreate(name=str(uuid4()), description="description", data=data)
|
||||
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["name"] == flow.name
|
||||
assert response.json()["data"] == flow.data
|
||||
|
||||
response = client.get("api/v1/flows/", headers=logged_in_headers)
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) > 0
|
||||
|
||||
|
||||
def test_read_flow(client: TestClient, json_flow: str, active_user, logged_in_headers):
|
||||
flow = orjson.loads(json_flow)
|
||||
data = flow["data"]
|
||||
flow = FlowCreate(name="Test Flow", description="description", data=data)
|
||||
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||
flow_id = response.json()["id"] # flow_id should be a UUID but is a string
|
||||
# turn it into a UUID
|
||||
flow_id = UUID(flow_id)
|
||||
|
||||
response = client.get(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == flow.name
|
||||
assert response.json()["data"] == flow.data
|
||||
|
||||
|
||||
def test_update_flow(client: TestClient, json_flow: str, active_user, logged_in_headers):
|
||||
flow = orjson.loads(json_flow)
|
||||
data = flow["data"]
|
||||
|
||||
flow = FlowCreate(name="Test Flow", description="description", data=data)
|
||||
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||
|
||||
flow_id = response.json()["id"]
|
||||
updated_flow = FlowUpdate(
|
||||
name="Updated Flow",
|
||||
description="updated description",
|
||||
data=data,
|
||||
)
|
||||
response = client.patch(f"api/v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == updated_flow.name
|
||||
assert response.json()["description"] == updated_flow.description
|
||||
# assert response.json()["data"] == updated_flow.data
|
||||
|
||||
|
||||
def test_delete_flow(client: TestClient, json_flow: str, active_user, logged_in_headers):
|
||||
flow = orjson.loads(json_flow)
|
||||
data = flow["data"]
|
||||
flow = FlowCreate(name="Test Flow", description="description", data=data)
|
||||
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||
flow_id = response.json()["id"]
|
||||
response = client.delete(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["message"] == "Flow deleted successfully"
|
||||
|
||||
|
||||
def test_delete_flows(client: TestClient, json_flow: str, active_user, logged_in_headers):
|
||||
# Create ten flows
|
||||
number_of_flows = 10
|
||||
flows = [FlowCreate(name=f"Flow {i}", description="description", data={}) for i in range(number_of_flows)]
|
||||
flow_ids = []
|
||||
for flow in flows:
|
||||
response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
|
||||
assert response.status_code == 201
|
||||
flow_ids.append(response.json()["id"])
|
||||
|
||||
response = client.request("DELETE", "api/v1/flows/", headers=logged_in_headers, json=flow_ids)
|
||||
assert response.status_code == 200, response.content
|
||||
assert response.json().get("deleted") == number_of_flows
|
||||
|
||||
|
||||
def test_create_flows(client: TestClient, session: Session, json_flow: str, logged_in_headers):
|
||||
flow = orjson.loads(json_flow)
|
||||
data = flow["data"]
|
||||
# Create test data
|
||||
flow_list = FlowListCreate(
|
||||
flows=[
|
||||
FlowCreate(name="Flow 1", description="description", data=data),
|
||||
FlowCreate(name="Flow 2", description="description", data=data),
|
||||
]
|
||||
)
|
||||
# Make request to endpoint
|
||||
response = client.post("api/v1/flows/batch/", json=flow_list.dict(), headers=logged_in_headers)
|
||||
# Check response status code
|
||||
assert response.status_code == 201
|
||||
# Check response data
|
||||
response_data = response.json()
|
||||
assert len(response_data) == 2
|
||||
assert response_data[0]["name"] == "Flow 1"
|
||||
assert response_data[0]["description"] == "description"
|
||||
assert response_data[0]["data"] == data
|
||||
assert response_data[1]["name"] == "Flow 2"
|
||||
assert response_data[1]["description"] == "description"
|
||||
assert response_data[1]["data"] == data
|
||||
|
||||
|
||||
def test_upload_file(client: TestClient, session: Session, json_flow: str, logged_in_headers):
|
||||
flow = orjson.loads(json_flow)
|
||||
data = flow["data"]
|
||||
# Create test data
|
||||
flow_list = FlowListCreate(
|
||||
flows=[
|
||||
FlowCreate(name="Flow 1", description="description", data=data),
|
||||
FlowCreate(name="Flow 2", description="description", data=data),
|
||||
]
|
||||
)
|
||||
file_contents = orjson_dumps(flow_list.dict())
|
||||
response = client.post(
|
||||
"api/v1/flows/upload/",
|
||||
files={"file": ("examples.json", file_contents, "application/json")},
|
||||
headers=logged_in_headers,
|
||||
)
|
||||
# Check response status code
|
||||
assert response.status_code == 201
|
||||
# Check response data
|
||||
response_data = response.json()
|
||||
assert len(response_data) == 2
|
||||
assert response_data[0]["name"] == "Flow 1"
|
||||
assert response_data[0]["description"] == "description"
|
||||
assert response_data[0]["data"] == data
|
||||
assert response_data[1]["name"] == "Flow 2"
|
||||
assert response_data[1]["description"] == "description"
|
||||
assert response_data[1]["data"] == data
|
||||
|
||||
|
||||
def test_download_file(
|
||||
client: TestClient,
|
||||
session: Session,
|
||||
json_flow,
|
||||
active_user,
|
||||
logged_in_headers,
|
||||
):
|
||||
flow = orjson.loads(json_flow)
|
||||
data = flow["data"]
|
||||
# Create test data
|
||||
flow_list = FlowListCreate(
|
||||
flows=[
|
||||
FlowCreate(name="Flow 1", description="description", data=data),
|
||||
FlowCreate(name="Flow 2", description="description", data=data),
|
||||
]
|
||||
)
|
||||
db_manager = get_db_service()
|
||||
with session_getter(db_manager) as session:
|
||||
for flow in flow_list.flows:
|
||||
flow.user_id = active_user.id
|
||||
db_flow = Flow.model_validate(flow, from_attributes=True)
|
||||
session.add(db_flow)
|
||||
session.commit()
|
||||
# Make request to endpoint
|
||||
response = client.get("api/v1/flows/download/", headers=logged_in_headers)
|
||||
# Check response status code
|
||||
assert response.status_code == 200, response.json()
|
||||
# Check response data
|
||||
response_data = response.json()["flows"]
|
||||
starter_projects = load_starter_projects()
|
||||
number_of_projects = len(starter_projects) + len(flow_list.flows)
|
||||
assert len(response_data) == number_of_projects, response_data
|
||||
assert response_data[0]["name"] == "Flow 1"
|
||||
assert response_data[0]["description"] == "description"
|
||||
assert response_data[0]["data"] == data
|
||||
assert response_data[1]["name"] == "Flow 2"
|
||||
assert response_data[1]["description"] == "description"
|
||||
assert response_data[1]["data"] == data
|
||||
|
||||
|
||||
def test_create_flow_with_invalid_data(client: TestClient, active_user, logged_in_headers):
|
||||
flow = {"name": "a" * 256, "data": "Invalid flow data"}
|
||||
response = client.post("api/v1/flows/", json=flow, headers=logged_in_headers)
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_get_nonexistent_flow(client: TestClient, active_user, logged_in_headers):
|
||||
uuid = uuid4()
|
||||
response = client.get(f"api/v1/flows/{uuid}", headers=logged_in_headers)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_update_flow_idempotency(client: TestClient, json_flow: str, active_user, logged_in_headers):
|
||||
flow_data = orjson.loads(json_flow)
|
||||
data = flow_data["data"]
|
||||
flow_data = FlowCreate(name="Test Flow", description="description", data=data)
|
||||
response = client.post("api/v1/flows/", json=flow_data.dict(), headers=logged_in_headers)
|
||||
flow_id = response.json()["id"]
|
||||
updated_flow = FlowCreate(name="Updated Flow", description="description", data=data)
|
||||
response1 = client.put(f"api/v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers)
|
||||
response2 = client.put(f"api/v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers)
|
||||
assert response1.json() == response2.json()
|
||||
|
||||
|
||||
def test_update_nonexistent_flow(client: TestClient, json_flow: str, active_user, logged_in_headers):
|
||||
flow_data = orjson.loads(json_flow)
|
||||
data = flow_data["data"]
|
||||
uuid = uuid4()
|
||||
updated_flow = FlowCreate(
|
||||
name="Updated Flow",
|
||||
description="description",
|
||||
data=data,
|
||||
)
|
||||
response = client.patch(f"api/v1/flows/{uuid}", json=updated_flow.model_dump(), headers=logged_in_headers)
|
||||
assert response.status_code == 404, response.text
|
||||
|
||||
|
||||
def test_delete_nonexistent_flow(client: TestClient, active_user, logged_in_headers):
|
||||
uuid = uuid4()
|
||||
response = client.delete(f"api/v1/flows/{uuid}", headers=logged_in_headers)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_read_only_starter_projects(client: TestClient, active_user, logged_in_headers):
|
||||
response = client.get("api/v1/flows/", headers=logged_in_headers)
|
||||
starter_projects = load_starter_projects()
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) == len(starter_projects)
|
||||
|
||||
|
||||
@pytest.mark.load_flows
|
||||
def test_load_flows(client: TestClient, load_flows_dir):
|
||||
response = client.get("api/v1/flows/c54f9130-f2fa-4a3e-b22a-3856d946351b")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "BasicExample"
|
||||
# re-run to ensure updates work well
|
||||
load_flows_from_directory()
|
||||
response = client.get("api/v1/flows/c54f9130-f2fa-4a3e-b22a-3856d946351b")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "BasicExample"
|
||||
104
src/backend/tests/unit/test_files.py
Normal file
104
src/backend/tests/unit/test_files.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from langflow.services.deps import get_storage_service
|
||||
from langflow.services.storage.service import StorageService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_storage_service():
|
||||
# Create a mock instance of StorageService
|
||||
service = MagicMock(spec=StorageService)
|
||||
# Setup mock behaviors for the service methods as needed
|
||||
service.save_file.return_value = None
|
||||
service.get_file.return_value = b"file content" # Binary content for files
|
||||
service.list_files.return_value = ["file1.txt", "file2.jpg"]
|
||||
service.delete_file.return_value = None
|
||||
return service
|
||||
|
||||
|
||||
def test_upload_file(client, mock_storage_service, created_api_key, flow):
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
# Replace the actual storage service with the mock
|
||||
client.app.dependency_overrides[get_storage_service] = lambda: mock_storage_service
|
||||
|
||||
response = client.post(
|
||||
f"api/v1/files/upload/{flow.id}",
|
||||
files={"file": ("test.txt", b"test content")},
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.json() == {
|
||||
"flowId": str(flow.id),
|
||||
"file_path": f"{flow.id}/test.txt",
|
||||
}
|
||||
|
||||
|
||||
def test_download_file(client, mock_storage_service, created_api_key, flow):
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
client.app.dependency_overrides[get_storage_service] = lambda: mock_storage_service
|
||||
|
||||
response = client.get(f"api/v1/files/download/{flow.id}/test.txt", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.content == b"file content"
|
||||
|
||||
|
||||
def test_list_files(client, mock_storage_service, created_api_key, flow):
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
client.app.dependency_overrides[get_storage_service] = lambda: mock_storage_service
|
||||
|
||||
response = client.get(f"api/v1/files/list/{flow.id}", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"files": ["file1.txt", "file2.jpg"]}
|
||||
|
||||
|
||||
def test_delete_file(client, mock_storage_service, created_api_key, flow):
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
client.app.dependency_overrides[get_storage_service] = lambda: mock_storage_service
|
||||
|
||||
response = client.delete(f"api/v1/files/delete/{flow.id}/test.txt", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "File test.txt deleted successfully"}
|
||||
|
||||
|
||||
def test_file_operations(client, created_api_key, flow):
|
||||
headers = {"x-api-key": created_api_key.api_key}
|
||||
flow_id = flow.id
|
||||
file_name = "test.txt"
|
||||
file_content = b"Hello, world!"
|
||||
|
||||
# Step 1: Upload the file
|
||||
response = client.post(
|
||||
f"api/v1/files/upload/{flow_id}",
|
||||
files={"file": (file_name, file_content)},
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.json() == {
|
||||
"flowId": str(flow_id),
|
||||
"file_path": f"{flow_id}/{file_name}",
|
||||
}
|
||||
|
||||
# Step 2: List files in the folder
|
||||
response = client.get(f"api/v1/files/list/{flow_id}", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert file_name in response.json()["files"]
|
||||
|
||||
# Step 3: Download the file and verify its content
|
||||
|
||||
response = client.get(f"api/v1/files/download/{flow_id}/{file_name}", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.content == file_content
|
||||
# the headers are application/octet-stream
|
||||
assert response.headers["content-type"] == "application/octet-stream"
|
||||
# mime_type is inside media_type
|
||||
|
||||
# Step 4: Delete the file
|
||||
response = client.delete(f"api/v1/files/delete/{flow_id}/{file_name}", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": f"File {file_name} deleted successfully"}
|
||||
|
||||
# Verify that the file is indeed deleted
|
||||
response = client.get(f"api/v1/files/list/{flow_id}", headers=headers)
|
||||
assert file_name not in response.json()["files"]
|
||||
57
src/backend/tests/unit/test_frontend_nodes.py
Normal file
57
src/backend/tests/unit/test_frontend_nodes.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import pytest
|
||||
|
||||
from langflow.template.field.base import Input
|
||||
from langflow.template.frontend_node.base import FrontendNode
|
||||
from langflow.template.template.base import Template
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_template_field() -> Input:
|
||||
return Input(name="test_field", field_type="str")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_template(sample_template_field: Input) -> Template:
|
||||
return Template(type_name="test_template", fields=[sample_template_field])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_frontend_node(sample_template: Template) -> FrontendNode:
|
||||
return FrontendNode(
|
||||
template=sample_template,
|
||||
description="test description",
|
||||
base_classes=["base_class1", "base_class2"],
|
||||
name="test_frontend_node",
|
||||
)
|
||||
|
||||
|
||||
def test_template_field_defaults(sample_template_field: Input):
|
||||
assert sample_template_field.field_type == "str"
|
||||
assert sample_template_field.required is False
|
||||
assert sample_template_field.placeholder == ""
|
||||
assert sample_template_field.is_list is False
|
||||
assert sample_template_field.show is True
|
||||
assert sample_template_field.multiline is False
|
||||
assert sample_template_field.value is None
|
||||
assert sample_template_field.file_types == []
|
||||
assert sample_template_field.file_path == ""
|
||||
assert sample_template_field.password is False
|
||||
assert sample_template_field.name == "test_field"
|
||||
|
||||
|
||||
def test_template_to_dict(sample_template: Template, sample_template_field: Input):
|
||||
template_dict = sample_template.to_dict()
|
||||
assert template_dict["_type"] == "test_template"
|
||||
assert len(template_dict) == 2 # _type and test_field
|
||||
assert "test_field" in template_dict
|
||||
assert "type" in template_dict["test_field"]
|
||||
assert "required" in template_dict["test_field"]
|
||||
|
||||
|
||||
def test_frontend_node_to_dict(sample_frontend_node: FrontendNode):
|
||||
node_dict = sample_frontend_node.to_dict()
|
||||
assert len(node_dict) == 1
|
||||
assert "test_frontend_node" in node_dict
|
||||
assert "description" in node_dict["test_frontend_node"]
|
||||
assert "template" in node_dict["test_frontend_node"]
|
||||
assert "base_classes" in node_dict["test_frontend_node"]
|
||||
418
src/backend/tests/unit/test_graph.py
Normal file
418
src/backend/tests/unit/test_graph.py
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
import copy
|
||||
import json
|
||||
import pickle
|
||||
from typing import Type, Union
|
||||
|
||||
import pytest
|
||||
|
||||
from langflow.graph import Graph
|
||||
from langflow.graph.edge.base import Edge
|
||||
from langflow.graph.graph.utils import (
|
||||
find_last_node,
|
||||
process_flow,
|
||||
set_new_target_handle,
|
||||
ungroup_node,
|
||||
update_source_handle,
|
||||
update_target_handle,
|
||||
update_template,
|
||||
)
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
from langflow.initial_setup.setup import load_starter_projects
|
||||
from langflow.utils.payload import get_root_vertex
|
||||
|
||||
# Test cases for the graph module
|
||||
|
||||
# now we have three types of graph:
|
||||
# BASIC_EXAMPLE_PATH, COMPLEX_EXAMPLE_PATH, OPENAPI_EXAMPLE_PATH
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_template():
|
||||
return {
|
||||
"field1": {"proxy": {"field": "some_field", "id": "node1"}},
|
||||
"field2": {"proxy": {"field": "other_field", "id": "node2"}},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_nodes():
|
||||
return [
|
||||
{
|
||||
"id": "node1",
|
||||
"data": {"node": {"template": {"some_field": {"show": True, "advanced": False, "name": "Name1"}}}},
|
||||
},
|
||||
{
|
||||
"id": "node2",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"other_field": {
|
||||
"show": False,
|
||||
"advanced": True,
|
||||
"display_name": "DisplayName2",
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "node3",
|
||||
"data": {"node": {"template": {"unrelated_field": {"show": True, "advanced": True}}}},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_node_by_type(graph, node_type: Type[Vertex]) -> Union[Vertex, None]:
|
||||
"""Get a node by type"""
|
||||
return next((node for node in graph.vertices if isinstance(node, node_type)), None)
|
||||
|
||||
|
||||
def test_graph_structure(basic_graph):
|
||||
assert isinstance(basic_graph, Graph)
|
||||
assert len(basic_graph.vertices) > 0
|
||||
assert len(basic_graph.edges) > 0
|
||||
for node in basic_graph.vertices:
|
||||
assert isinstance(node, Vertex)
|
||||
for edge in basic_graph.edges:
|
||||
assert isinstance(edge, Edge)
|
||||
source_vertex = basic_graph.get_vertex(edge.source_id)
|
||||
target_vertex = basic_graph.get_vertex(edge.target_id)
|
||||
assert source_vertex in basic_graph.vertices
|
||||
assert target_vertex in basic_graph.vertices
|
||||
|
||||
|
||||
def test_circular_dependencies(basic_graph):
|
||||
assert isinstance(basic_graph, Graph)
|
||||
|
||||
def check_circular(node, visited):
|
||||
visited.add(node)
|
||||
neighbors = basic_graph.get_vertices_with_target(node)
|
||||
for neighbor in neighbors:
|
||||
if neighbor in visited:
|
||||
return True
|
||||
if check_circular(neighbor, visited.copy()):
|
||||
return True
|
||||
return False
|
||||
|
||||
for node in basic_graph.vertices:
|
||||
assert not check_circular(node, set())
|
||||
|
||||
|
||||
def test_invalid_node_types():
|
||||
graph_data = {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "1",
|
||||
"data": {
|
||||
"node": {
|
||||
"base_classes": ["BaseClass"],
|
||||
"template": {
|
||||
"_type": "InvalidNodeType",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"edges": [],
|
||||
}
|
||||
with pytest.raises(Exception):
|
||||
Graph(graph_data["nodes"], graph_data["edges"])
|
||||
|
||||
|
||||
def test_get_vertices_with_target(basic_graph):
|
||||
"""Test getting connected nodes"""
|
||||
assert isinstance(basic_graph, Graph)
|
||||
# Get root node
|
||||
root = get_root_vertex(basic_graph)
|
||||
assert root is not None
|
||||
connected_nodes = basic_graph.get_vertices_with_target(root.id)
|
||||
assert connected_nodes is not None
|
||||
|
||||
|
||||
def test_get_node_neighbors_basic(basic_graph):
|
||||
"""Test getting node neighbors"""
|
||||
|
||||
assert isinstance(basic_graph, Graph)
|
||||
# Get root node
|
||||
root = get_root_vertex(basic_graph)
|
||||
assert root is not None
|
||||
neighbors = basic_graph.get_vertex_neighbors(root)
|
||||
assert neighbors is not None
|
||||
assert isinstance(neighbors, dict)
|
||||
# Root Node is an Agent, it requires an LLMChain and tools
|
||||
# We need to check if there is a Chain in the one of the neighbors'
|
||||
# data attribute in the type key
|
||||
assert any("ConversationBufferMemory" in neighbor.data["type"] for neighbor, val in neighbors.items() if val)
|
||||
|
||||
assert any("OpenAI" in neighbor.data["type"] for neighbor, val in neighbors.items() if val)
|
||||
|
||||
|
||||
def test_get_node(basic_graph):
|
||||
"""Test getting a single node"""
|
||||
node_id = basic_graph.vertices[0].id
|
||||
node = basic_graph.get_vertex(node_id)
|
||||
assert isinstance(node, Vertex)
|
||||
assert node.id == node_id
|
||||
|
||||
|
||||
def test_build_nodes(basic_graph):
|
||||
"""Test building nodes"""
|
||||
|
||||
assert len(basic_graph.vertices) == len(basic_graph._vertices)
|
||||
for node in basic_graph.vertices:
|
||||
assert isinstance(node, Vertex)
|
||||
|
||||
|
||||
def test_build_edges(basic_graph):
|
||||
"""Test building edges"""
|
||||
assert len(basic_graph.edges) == len(basic_graph._edges)
|
||||
for edge in basic_graph.edges:
|
||||
assert isinstance(edge, Edge)
|
||||
assert isinstance(edge.source_id, str)
|
||||
assert isinstance(edge.target_id, str)
|
||||
|
||||
|
||||
def test_get_root_vertex(client, basic_graph, complex_graph):
|
||||
"""Test getting root node"""
|
||||
assert isinstance(basic_graph, Graph)
|
||||
root = get_root_vertex(basic_graph)
|
||||
assert root is not None
|
||||
assert isinstance(root, Vertex)
|
||||
assert root.data["type"] == "TimeTravelGuideChain"
|
||||
# For complex example, the root node is a ZeroShotAgent too
|
||||
assert isinstance(complex_graph, Graph)
|
||||
root = get_root_vertex(complex_graph)
|
||||
assert root is not None
|
||||
assert isinstance(root, Vertex)
|
||||
assert root.data["type"] == "ZeroShotAgent"
|
||||
|
||||
|
||||
def test_validate_edges(basic_graph):
|
||||
"""Test validating edges"""
|
||||
|
||||
assert isinstance(basic_graph, Graph)
|
||||
# all edges should be valid
|
||||
assert all(edge.valid for edge in basic_graph.edges)
|
||||
|
||||
|
||||
def test_matched_type(basic_graph):
|
||||
"""Test matched type attribute in Edge"""
|
||||
assert isinstance(basic_graph, Graph)
|
||||
# all edges should be valid
|
||||
assert all(edge.valid for edge in basic_graph.edges)
|
||||
# all edges should have a matched_type attribute
|
||||
assert all(hasattr(edge, "matched_type") for edge in basic_graph.edges)
|
||||
# The matched_type attribute should be in the source_types attr
|
||||
assert all(edge.matched_type in edge.source_types for edge in basic_graph.edges)
|
||||
|
||||
|
||||
def test_build_params(basic_graph):
|
||||
"""Test building params"""
|
||||
|
||||
assert isinstance(basic_graph, Graph)
|
||||
# all edges should be valid
|
||||
assert all(edge.valid for edge in basic_graph.edges)
|
||||
# all edges should have a matched_type attribute
|
||||
assert all(hasattr(edge, "matched_type") for edge in basic_graph.edges)
|
||||
# The matched_type attribute should be in the source_types attr
|
||||
assert all(edge.matched_type in edge.source_types for edge in basic_graph.edges)
|
||||
# Get the root node
|
||||
root = get_root_vertex(basic_graph)
|
||||
# Root node is a TimeTravelGuideChain
|
||||
# which requires an llm and memory
|
||||
assert root is not None
|
||||
assert isinstance(root.params, dict)
|
||||
assert "llm" in root.params
|
||||
assert "memory" in root.params
|
||||
|
||||
|
||||
# def test_wrapper_node_build(openapi_graph):
|
||||
# wrapper_node = get_node_by_type(openapi_graph, WrapperVertex)
|
||||
# assert wrapper_node is not None
|
||||
# built_object = wrapper_node.build()
|
||||
# assert built_object is not None
|
||||
|
||||
|
||||
def test_find_last_node(grouped_chat_json_flow):
|
||||
grouped_chat_data = json.loads(grouped_chat_json_flow).get("data")
|
||||
nodes, edges = grouped_chat_data["nodes"], grouped_chat_data["edges"]
|
||||
last_node = find_last_node(nodes, edges)
|
||||
assert last_node is not None # Replace with the actual expected value
|
||||
assert last_node["id"] == "LLMChain-pimAb" # Replace with the actual expected value
|
||||
|
||||
|
||||
def test_ungroup_node(grouped_chat_json_flow):
|
||||
grouped_chat_data = json.loads(grouped_chat_json_flow).get("data")
|
||||
group_node = grouped_chat_data["nodes"][2] # Assuming the first node is a group node
|
||||
base_flow = copy.deepcopy(grouped_chat_data)
|
||||
ungroup_node(group_node["data"], base_flow)
|
||||
# after ungroup_node is called, the base_flow and grouped_chat_data should be different
|
||||
assert base_flow != grouped_chat_data
|
||||
# assert node 2 is not a group node anymore
|
||||
assert base_flow["nodes"][2]["data"]["node"].get("flow") is None
|
||||
# assert the edges are updated
|
||||
assert len(base_flow["edges"]) > len(grouped_chat_data["edges"])
|
||||
assert base_flow["edges"][0]["source"] == "ConversationBufferMemory-kUMif"
|
||||
assert base_flow["edges"][0]["target"] == "LLMChain-2P369"
|
||||
assert base_flow["edges"][1]["source"] == "PromptTemplate-Wjk4g"
|
||||
assert base_flow["edges"][1]["target"] == "LLMChain-2P369"
|
||||
assert base_flow["edges"][2]["source"] == "ChatOpenAI-rUJ1b"
|
||||
assert base_flow["edges"][2]["target"] == "LLMChain-2P369"
|
||||
|
||||
|
||||
def test_process_flow(grouped_chat_json_flow):
|
||||
grouped_chat_data = json.loads(grouped_chat_json_flow).get("data")
|
||||
|
||||
processed_flow = process_flow(grouped_chat_data)
|
||||
assert processed_flow is not None
|
||||
assert isinstance(processed_flow, dict)
|
||||
assert "nodes" in processed_flow
|
||||
assert "edges" in processed_flow
|
||||
|
||||
|
||||
def test_process_flow_one_group(one_grouped_chat_json_flow):
|
||||
grouped_chat_data = json.loads(one_grouped_chat_json_flow).get("data")
|
||||
# There should be only one node
|
||||
assert len(grouped_chat_data["nodes"]) == 1
|
||||
# Get the node, it should be a group node
|
||||
group_node = grouped_chat_data["nodes"][0]
|
||||
node_data = group_node["data"]["node"]
|
||||
assert node_data.get("flow") is not None
|
||||
template_data = node_data["template"]
|
||||
assert any("openai_api_key" in key for key in template_data.keys())
|
||||
# Get the openai_api_key dict
|
||||
openai_api_key = next(
|
||||
(template_data[key] for key in template_data.keys() if "openai_api_key" in key),
|
||||
None,
|
||||
)
|
||||
assert openai_api_key is not None
|
||||
assert openai_api_key["value"] == "test"
|
||||
|
||||
processed_flow = process_flow(grouped_chat_data)
|
||||
assert processed_flow is not None
|
||||
assert isinstance(processed_flow, dict)
|
||||
assert "nodes" in processed_flow
|
||||
assert "edges" in processed_flow
|
||||
|
||||
# Now get the node that has ChatOpenAI in its id
|
||||
chat_openai_node = next((node for node in processed_flow["nodes"] if "ChatOpenAI" in node["id"]), None)
|
||||
assert chat_openai_node is not None
|
||||
assert chat_openai_node["data"]["node"]["template"]["openai_api_key"]["value"] == "test"
|
||||
|
||||
|
||||
def test_process_flow_vector_store_grouped(vector_store_grouped_json_flow):
|
||||
grouped_chat_data = json.loads(vector_store_grouped_json_flow).get("data")
|
||||
nodes = grouped_chat_data["nodes"]
|
||||
assert len(nodes) == 4
|
||||
# There are two group nodes in this flow
|
||||
# One of them is inside the other totalling 7 nodes
|
||||
# 4 nodes grouped, one of these turns into 1 normal node and 1 group node
|
||||
# This group node has 2 nodes inside it
|
||||
|
||||
processed_flow = process_flow(grouped_chat_data)
|
||||
assert processed_flow is not None
|
||||
processed_nodes = processed_flow["nodes"]
|
||||
assert len(processed_nodes) == 7
|
||||
assert isinstance(processed_flow, dict)
|
||||
assert "nodes" in processed_flow
|
||||
assert "edges" in processed_flow
|
||||
edges = processed_flow["edges"]
|
||||
# Expected keywords in source and target fields
|
||||
expected_keywords = [
|
||||
{"source": "VectorStoreInfo", "target": "VectorStoreAgent"},
|
||||
{"source": "ChatOpenAI", "target": "VectorStoreAgent"},
|
||||
{"source": "OpenAIEmbeddings", "target": "Chroma"},
|
||||
{"source": "Chroma", "target": "VectorStoreInfo"},
|
||||
{"source": "WebBaseLoader", "target": "RecursiveCharacterTextSplitter"},
|
||||
{"source": "RecursiveCharacterTextSplitter", "target": "Chroma"},
|
||||
]
|
||||
|
||||
for idx, expected_keyword in enumerate(expected_keywords):
|
||||
for key, value in expected_keyword.items():
|
||||
assert (
|
||||
value in edges[idx][key].split("-")[0]
|
||||
), f"Edge {idx}, key {key} expected to contain {value} but got {edges[idx][key]}"
|
||||
|
||||
|
||||
def test_update_template(sample_template, sample_nodes):
|
||||
# Making a deep copy to keep original sample_nodes unchanged
|
||||
nodes_copy = copy.deepcopy(sample_nodes)
|
||||
update_template(sample_template, nodes_copy)
|
||||
|
||||
# Now, validate the updates.
|
||||
node1_updated = next((n for n in nodes_copy if n["id"] == "node1"), None)
|
||||
node2_updated = next((n for n in nodes_copy if n["id"] == "node2"), None)
|
||||
node3_updated = next((n for n in nodes_copy if n["id"] == "node3"), None)
|
||||
|
||||
assert node1_updated["data"]["node"]["template"]["some_field"]["show"] is True
|
||||
assert node1_updated["data"]["node"]["template"]["some_field"]["advanced"] is False
|
||||
assert node1_updated["data"]["node"]["template"]["some_field"]["display_name"] == "Name1"
|
||||
|
||||
assert node2_updated["data"]["node"]["template"]["other_field"]["show"] is False
|
||||
assert node2_updated["data"]["node"]["template"]["other_field"]["advanced"] is True
|
||||
assert node2_updated["data"]["node"]["template"]["other_field"]["display_name"] == "DisplayName2"
|
||||
|
||||
# Ensure node3 remains unchanged
|
||||
assert node3_updated == sample_nodes[2]
|
||||
|
||||
|
||||
# Test `update_target_handle`
|
||||
def test_update_target_handle_proxy():
|
||||
new_edge = {
|
||||
"data": {
|
||||
"targetHandle": {
|
||||
"type": "some_type",
|
||||
"proxy": {"id": "some_id", "field": ""},
|
||||
}
|
||||
}
|
||||
}
|
||||
g_nodes = [{"id": "some_id", "data": {"node": {"flow": None}}}]
|
||||
group_node_id = "group_id"
|
||||
updated_edge = update_target_handle(new_edge, g_nodes, group_node_id)
|
||||
assert updated_edge["data"]["targetHandle"] == new_edge["data"]["targetHandle"]
|
||||
|
||||
|
||||
# Test `set_new_target_handle`
|
||||
def test_set_new_target_handle():
|
||||
proxy_id = "proxy_id"
|
||||
new_edge = {"target": None, "data": {"targetHandle": {}}}
|
||||
target_handle = {"type": "type_1", "proxy": {"field": "field_1"}}
|
||||
node = {
|
||||
"data": {
|
||||
"node": {
|
||||
"flow": True,
|
||||
"template": {"field_1": {"proxy": {"field": "new_field", "id": "new_id"}}},
|
||||
}
|
||||
}
|
||||
}
|
||||
set_new_target_handle(proxy_id, new_edge, target_handle, node)
|
||||
assert new_edge["target"] == "proxy_id"
|
||||
assert new_edge["data"]["targetHandle"]["fieldName"] == "field_1"
|
||||
assert new_edge["data"]["targetHandle"]["proxy"] == {
|
||||
"field": "new_field",
|
||||
"id": "new_id",
|
||||
}
|
||||
|
||||
|
||||
# Test `update_source_handle`
|
||||
def test_update_source_handle():
|
||||
new_edge = {"source": None, "data": {"sourceHandle": {"id": None}}}
|
||||
flow_data = {
|
||||
"nodes": [{"id": "some_node"}, {"id": "last_node"}],
|
||||
"edges": [{"source": "some_node"}],
|
||||
}
|
||||
updated_edge = update_source_handle(new_edge, flow_data["nodes"], flow_data["edges"])
|
||||
assert updated_edge["source"] == "last_node"
|
||||
assert updated_edge["data"]["sourceHandle"]["id"] == "last_node"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pickle_graph():
|
||||
starter_projects = load_starter_projects()
|
||||
data = starter_projects[0][1]["data"]
|
||||
graph = Graph.from_payload(data)
|
||||
assert isinstance(graph, Graph)
|
||||
pickled = pickle.dumps(graph)
|
||||
assert pickled is not None
|
||||
unpickled = pickle.loads(pickled)
|
||||
assert unpickled is not None
|
||||
79
src/backend/tests/unit/test_helper_components.py
Normal file
79
src/backend/tests/unit/test_helper_components.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
from langflow.components import helpers
|
||||
from langflow.custom.utils import build_custom_component_template
|
||||
from langflow.schema import Data
|
||||
|
||||
# def test_update_data_component():
|
||||
# # Arrange
|
||||
# update_data_component = helpers.UpdateDataComponent()
|
||||
|
||||
# # Act
|
||||
# new_data = {"new_key": "new_value"}
|
||||
# existing_data = Data(data={"existing_key": "existing_value"})
|
||||
# result = update_data_component.build(existing_data, new_data)
|
||||
# assert result.data == {"existing_key": "existing_value", "new_key": "new_value"}
|
||||
# assert result.existing_key == "existing_value"
|
||||
# assert result.new_key == "new_value"
|
||||
|
||||
|
||||
# def test_document_to_data_component():
|
||||
# # Arrange
|
||||
# document_to_data_component = helpers.DocumentsToDataComponent()
|
||||
|
||||
# # Act
|
||||
# # Replace with your actual test data
|
||||
# document = Document(page_content="key: value", metadata={"url": "https://example.com"})
|
||||
# result = document_to_data_component.build(document)
|
||||
|
||||
# # Assert
|
||||
# # Replace with your actual expected result
|
||||
# assert result == [Data(data={"text": "key: value", "url": "https://example.com"})]
|
||||
|
||||
|
||||
def test_uuid_generator_component():
|
||||
# Arrange
|
||||
uuid_generator_component = helpers.IDGeneratorComponent()
|
||||
uuid_generator_component.code = open(helpers.IDGenerator.__file__, "r").read()
|
||||
|
||||
frontend_node, _ = build_custom_component_template(uuid_generator_component)
|
||||
|
||||
# Act
|
||||
build_config = frontend_node.get("template")
|
||||
field_name = "unique_id"
|
||||
build_config = uuid_generator_component.update_build_config(build_config, None, field_name)
|
||||
unique_id = build_config["unique_id"]["value"]
|
||||
result = uuid_generator_component.build(unique_id)
|
||||
|
||||
# Assert
|
||||
# UUID should be a string of length 36
|
||||
assert isinstance(result, str)
|
||||
assert len(result) == 36
|
||||
|
||||
|
||||
def test_data_as_text_component():
|
||||
# Arrange
|
||||
data_as_text_component = helpers.ParseDataComponent()
|
||||
|
||||
# Act
|
||||
# Replace with your actual test data
|
||||
data = [Data(data={"key": "value", "bacon": "eggs"})]
|
||||
template = "Data:{data} -- Bacon:{bacon}"
|
||||
data_as_text_component.set_attributes({"data": data, "template": template})
|
||||
result = data_as_text_component.parse_data()
|
||||
|
||||
# Assert
|
||||
# Replace with your actual expected result
|
||||
assert result.text == "Data:{'key': 'value', 'bacon': 'eggs'} -- Bacon:eggs"
|
||||
|
||||
|
||||
# def test_text_to_data_component():
|
||||
# # Arrange
|
||||
# text_to_data_component = helpers.CreateDataComponent()
|
||||
|
||||
# # Act
|
||||
# # Replace with your actual test data
|
||||
# dict_with_text = {"field_1": {"key": "value"}}
|
||||
# result = text_to_data_component.build(number_of_fields=1, **dict_with_text)
|
||||
|
||||
# # Assert
|
||||
# # Replace with your actual expected result
|
||||
# assert result == Data(data={"key": "value"})
|
||||
88
src/backend/tests/unit/test_initial_setup.py
Normal file
88
src/backend/tests/unit/test_initial_setup.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from sqlmodel import select
|
||||
|
||||
from langflow.initial_setup.setup import STARTER_FOLDER_NAME, get_project_data, load_starter_projects
|
||||
from langflow.services.database.models.folder.model import Folder
|
||||
from langflow.services.deps import session_scope
|
||||
|
||||
|
||||
def test_load_starter_projects():
|
||||
projects = load_starter_projects()
|
||||
assert isinstance(projects, list)
|
||||
assert all(isinstance(project[1], dict) for project in projects)
|
||||
assert all(isinstance(project[0], Path) for project in projects)
|
||||
|
||||
|
||||
def test_get_project_data():
|
||||
projects = load_starter_projects()
|
||||
for _, project in projects:
|
||||
(
|
||||
project_name,
|
||||
project_description,
|
||||
project_is_component,
|
||||
updated_at_datetime,
|
||||
project_data,
|
||||
project_icon,
|
||||
project_icon_bg_color,
|
||||
) = get_project_data(project)
|
||||
assert isinstance(project_name, str)
|
||||
assert isinstance(project_description, str)
|
||||
assert isinstance(project_is_component, bool)
|
||||
assert isinstance(updated_at_datetime, datetime)
|
||||
assert isinstance(project_data, dict)
|
||||
assert isinstance(project_icon, str) or project_icon is None
|
||||
assert isinstance(project_icon_bg_color, str) or project_icon_bg_color is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_or_update_starter_projects():
|
||||
with session_scope() as session:
|
||||
# Get the number of projects returned by load_starter_projects
|
||||
num_projects = len(load_starter_projects())
|
||||
|
||||
# Get the number of projects in the database
|
||||
folder = session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME)).first()
|
||||
assert folder is not None
|
||||
num_db_projects = len(folder.flows)
|
||||
|
||||
# Check that the number of projects in the database is the same as the number of projects returned by load_starter_projects
|
||||
assert num_db_projects == num_projects
|
||||
|
||||
|
||||
# Some starter projects require integration
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_starter_projects_can_run_successfully(client):
|
||||
# with session_scope() as session:
|
||||
# # Run the function to create or update projects
|
||||
# create_or_update_starter_projects()
|
||||
|
||||
# # Get the number of projects returned by load_starter_projects
|
||||
# num_projects = len(load_starter_projects())
|
||||
|
||||
# # Get the number of projects in the database
|
||||
# num_db_projects = session.exec(select(func.count(Flow.id)).where(Flow.folder == STARTER_FOLDER_NAME)).one()
|
||||
|
||||
# # Check that the number of projects in the database is the same as the number of projects returned by load_starter_projects
|
||||
# assert num_db_projects == num_projects
|
||||
|
||||
# # Get all the starter projects
|
||||
# projects = session.exec(select(Flow).where(Flow.folder == STARTER_FOLDER_NAME)).all()
|
||||
# graphs: list[tuple[str, Graph]] = []
|
||||
# for project in projects:
|
||||
# # Add tweaks to make file_path work
|
||||
# tweaks = {"path": __file__}
|
||||
# graph_data = process_tweaks(project.data, tweaks)
|
||||
# graph_object = Graph.from_payload(graph_data, flow_id=project.id)
|
||||
# graphs.append((project.name, graph_object))
|
||||
# assert len(graphs) == len(projects)
|
||||
# for name, graph in graphs:
|
||||
# outputs = await graph.arun(
|
||||
# inputs={},
|
||||
# outputs=[],
|
||||
# session_id="test",
|
||||
# )
|
||||
# assert all(isinstance(output, RunOutputs) for output in outputs), f"Project {name} error: {outputs}"
|
||||
# delete_messages(session_id="test")
|
||||
107
src/backend/tests/unit/test_kubernetes_secrets.py
Normal file
107
src/backend/tests/unit/test_kubernetes_secrets.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from kubernetes.client import V1ObjectMeta, V1Secret
|
||||
from base64 import b64encode
|
||||
from uuid import UUID
|
||||
|
||||
from langflow.services.variable.kubernetes_secrets import KubernetesSecretManager, encode_user_id
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_kube_config(mocker):
|
||||
mocker.patch("kubernetes.config.load_kube_config")
|
||||
mocker.patch("kubernetes.config.load_incluster_config")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def secret_manager(mock_kube_config):
|
||||
return KubernetesSecretManager(namespace="test-namespace")
|
||||
|
||||
|
||||
def test_create_secret(secret_manager, mocker):
|
||||
mocker.patch.object(
|
||||
secret_manager.core_api,
|
||||
"create_namespaced_secret",
|
||||
return_value=V1Secret(metadata=V1ObjectMeta(name="test-secret")),
|
||||
)
|
||||
|
||||
secret_manager.create_secret(name="test-secret", data={"key": "value"})
|
||||
secret_manager.core_api.create_namespaced_secret.assert_called_once_with(
|
||||
"test-namespace",
|
||||
V1Secret(
|
||||
api_version="v1",
|
||||
kind="Secret",
|
||||
metadata=V1ObjectMeta(name="test-secret"),
|
||||
type="Opaque",
|
||||
data={"key": b64encode("value".encode()).decode()},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_get_secret(secret_manager, mocker):
|
||||
mock_secret = V1Secret(data={"key": b64encode("value".encode()).decode()})
|
||||
mocker.patch.object(secret_manager.core_api, "read_namespaced_secret", return_value=mock_secret)
|
||||
|
||||
secret_data = secret_manager.get_secret(name="test-secret")
|
||||
secret_manager.core_api.read_namespaced_secret.assert_called_once_with("test-secret", "test-namespace")
|
||||
assert secret_data == {"key": "value"}
|
||||
|
||||
|
||||
def test_delete_secret(secret_manager, mocker):
|
||||
mocker.patch.object(secret_manager.core_api, "delete_namespaced_secret", return_value=MagicMock(status="Success"))
|
||||
|
||||
secret_manager.delete_secret(name="test-secret")
|
||||
secret_manager.core_api.delete_namespaced_secret.assert_called_once_with("test-secret", "test-namespace")
|
||||
|
||||
|
||||
def test_encode_uuid():
|
||||
uuid = UUID("123e4567-e89b-12d3-a456-426614174000")
|
||||
result = encode_user_id(uuid)
|
||||
assert result == "uuid-123e4567-e89b-12d3-a456-426614174000"
|
||||
assert len(result) < 253
|
||||
assert result[0].isalnum()
|
||||
assert result[-1].isalnum()
|
||||
|
||||
|
||||
def test_encode_string():
|
||||
string_id = "user@example.com"
|
||||
result = encode_user_id(string_id)
|
||||
# assert (result.isalnum() or '-' in result or '_' in result)
|
||||
assert len(result) < 253
|
||||
assert result[0].isalnum()
|
||||
assert result[-1].isalnum()
|
||||
|
||||
|
||||
def test_long_string():
|
||||
long_string = "a" * 300
|
||||
result = encode_user_id(long_string)
|
||||
assert len(result) <= 253
|
||||
|
||||
|
||||
def test_starts_with_non_alphanumeric():
|
||||
non_alnum_start = "+user123"
|
||||
result = encode_user_id(non_alnum_start)
|
||||
assert result[0].isalnum()
|
||||
|
||||
|
||||
def test_ends_with_non_alphanumeric():
|
||||
non_alnum_end = "user123+"
|
||||
result = encode_user_id(non_alnum_end)
|
||||
assert result[-1].isalnum()
|
||||
|
||||
|
||||
def test_email_address():
|
||||
email = "User.Name@Example.com"
|
||||
result = encode_user_id(email)
|
||||
assert result.isalnum() or "-" in result or "_" in result
|
||||
assert len(result) < 253
|
||||
assert result[0].isalnum()
|
||||
assert result[-1].isalnum()
|
||||
|
||||
|
||||
def test_uuid_case_insensitivity():
|
||||
uuid_upper = UUID("123E4567-E89B-12D3-A456-426614174000")
|
||||
uuid_lower = UUID("123e4567-e89b-12d3-a456-426614174000")
|
||||
result_upper = encode_user_id(uuid_upper)
|
||||
result_lower = encode_user_id(uuid_lower)
|
||||
assert result_upper == result_lower
|
||||
32
src/backend/tests/unit/test_loading.py
Normal file
32
src/backend/tests/unit/test_loading.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import pytest
|
||||
|
||||
from langflow.graph import Graph
|
||||
from langflow.initial_setup.setup import load_starter_projects
|
||||
from langflow.load import load_flow_from_json
|
||||
|
||||
|
||||
@pytest.mark.noclient
|
||||
def test_load_flow_from_json():
|
||||
"""Test loading a flow from a json file"""
|
||||
loaded = load_flow_from_json(pytest.BASIC_EXAMPLE_PATH)
|
||||
assert loaded is not None
|
||||
assert isinstance(loaded, Graph)
|
||||
|
||||
|
||||
@pytest.mark.noclient
|
||||
def test_load_flow_from_json_with_tweaks():
|
||||
"""Test loading a flow from a json file and applying tweaks"""
|
||||
tweaks = {"dndnode_82": {"model_name": "gpt-3.5-turbo-16k-0613"}}
|
||||
loaded = load_flow_from_json(pytest.BASIC_EXAMPLE_PATH, tweaks=tweaks)
|
||||
assert loaded is not None
|
||||
assert isinstance(loaded, Graph)
|
||||
|
||||
|
||||
@pytest.mark.noclient
|
||||
def test_load_flow_from_json_object():
|
||||
"""Test loading a flow from a json file and applying tweaks"""
|
||||
_, projects = zip(*load_starter_projects())
|
||||
project = projects[0]
|
||||
loaded = load_flow_from_json(project)
|
||||
assert loaded is not None
|
||||
assert isinstance(loaded, Graph)
|
||||
100
src/backend/tests/unit/test_logger.py
Normal file
100
src/backend/tests/unit/test_logger.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import pytest
|
||||
import os
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
from langflow.utils.logger import SizedLogBuffer
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sized_log_buffer():
|
||||
return SizedLogBuffer()
|
||||
|
||||
|
||||
def test_init_default():
|
||||
buffer = SizedLogBuffer()
|
||||
assert buffer.max == 0
|
||||
assert buffer._max_readers == 20
|
||||
|
||||
|
||||
def test_init_with_env_variable():
|
||||
with patch.dict(os.environ, {"LANGFLOW_LOG_RETRIEVER_BUFFER_SIZE": "100"}):
|
||||
buffer = SizedLogBuffer()
|
||||
assert buffer.max == 100
|
||||
|
||||
|
||||
def test_write(sized_log_buffer):
|
||||
message = json.dumps({"text": "Test log", "record": {"time": {"timestamp": 1625097600.1244334}}})
|
||||
sized_log_buffer.max = 1 # Set max size to 1 for testing
|
||||
sized_log_buffer.write(message)
|
||||
assert len(sized_log_buffer.buffer) == 1
|
||||
assert 1625097600124 == sized_log_buffer.buffer[0][0]
|
||||
assert "Test log" == sized_log_buffer.buffer[0][1]
|
||||
|
||||
|
||||
def test_write_overflow(sized_log_buffer):
|
||||
sized_log_buffer.max = 2
|
||||
messages = [json.dumps({"text": f"Log {i}", "record": {"time": {"timestamp": 1625097600 + i}}}) for i in range(3)]
|
||||
for message in messages:
|
||||
sized_log_buffer.write(message)
|
||||
|
||||
assert len(sized_log_buffer.buffer) == 2
|
||||
assert 1625097601000 == sized_log_buffer.buffer[0][0]
|
||||
assert 1625097602000 == sized_log_buffer.buffer[1][0]
|
||||
|
||||
|
||||
def test_len(sized_log_buffer):
|
||||
sized_log_buffer.max = 3
|
||||
messages = [json.dumps({"text": f"Log {i}", "record": {"time": {"timestamp": 1625097600 + i}}}) for i in range(3)]
|
||||
for message in messages:
|
||||
sized_log_buffer.write(message)
|
||||
|
||||
assert len(sized_log_buffer) == 3
|
||||
|
||||
|
||||
def test_get_after_timestamp(sized_log_buffer):
|
||||
sized_log_buffer.max = 5
|
||||
messages = [json.dumps({"text": f"Log {i}", "record": {"time": {"timestamp": 1625097600 + i}}}) for i in range(5)]
|
||||
for message in messages:
|
||||
sized_log_buffer.write(message)
|
||||
|
||||
result = sized_log_buffer.get_after_timestamp(1625097602000, lines=2)
|
||||
assert len(result) == 2
|
||||
assert 1625097603000 in result
|
||||
assert 1625097602000 in result
|
||||
|
||||
|
||||
def test_get_before_timestamp(sized_log_buffer):
|
||||
sized_log_buffer.max = 5
|
||||
messages = [json.dumps({"text": f"Log {i}", "record": {"time": {"timestamp": 1625097600 + i}}}) for i in range(5)]
|
||||
for message in messages:
|
||||
sized_log_buffer.write(message)
|
||||
|
||||
result = sized_log_buffer.get_before_timestamp(1625097603000, lines=2)
|
||||
assert len(result) == 2
|
||||
assert 1625097601000 in result
|
||||
assert 1625097602000 in result
|
||||
|
||||
|
||||
def test_get_last_n(sized_log_buffer):
|
||||
sized_log_buffer.max = 5
|
||||
messages = [json.dumps({"text": f"Log {i}", "record": {"time": {"timestamp": 1625097600 + i}}}) for i in range(5)]
|
||||
for message in messages:
|
||||
sized_log_buffer.write(message)
|
||||
|
||||
result = sized_log_buffer.get_last_n(3)
|
||||
assert len(result) == 3
|
||||
assert 1625097602000 in result
|
||||
assert 1625097603000 in result
|
||||
assert 1625097604000 in result
|
||||
|
||||
|
||||
def test_enabled(sized_log_buffer):
|
||||
assert not sized_log_buffer.enabled()
|
||||
sized_log_buffer.max = 1
|
||||
assert sized_log_buffer.enabled()
|
||||
|
||||
|
||||
def test_max_size(sized_log_buffer):
|
||||
assert sized_log_buffer.max_size() == 0
|
||||
sized_log_buffer.max = 100
|
||||
assert sized_log_buffer.max_size() == 100
|
||||
45
src/backend/tests/unit/test_login.py
Normal file
45
src/backend/tests/unit/test_login.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import pytest
|
||||
from langflow.services.auth.utils import get_password_hash
|
||||
from langflow.services.database.models.user import User
|
||||
from langflow.services.deps import session_scope
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_user():
|
||||
return User(
|
||||
username="testuser",
|
||||
password=get_password_hash("testpassword"), # Assuming password needs to be hashed
|
||||
is_active=True,
|
||||
is_superuser=False,
|
||||
)
|
||||
|
||||
|
||||
def test_login_successful(client, test_user):
|
||||
# Adding the test user to the database
|
||||
try:
|
||||
with session_scope() as session:
|
||||
session.add(test_user)
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
response = client.post("api/v1/login", data={"username": "testuser", "password": "testpassword"})
|
||||
assert response.status_code == 200
|
||||
assert "access_token" in response.json()
|
||||
|
||||
|
||||
def test_login_unsuccessful_wrong_username(client):
|
||||
response = client.post("api/v1/login", data={"username": "wrongusername", "password": "testpassword"})
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"] == "Incorrect username or password"
|
||||
|
||||
|
||||
def test_login_unsuccessful_wrong_password(client, test_user, session):
|
||||
# Adding the test user to the database
|
||||
session.add(test_user)
|
||||
session.commit()
|
||||
|
||||
response = client.post("api/v1/login", data={"username": "testuser", "password": "wrongpassword"})
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"] == "Incorrect username or password"
|
||||
102
src/backend/tests/unit/test_messages.py
Normal file
102
src/backend/tests/unit/test_messages.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import pytest
|
||||
|
||||
from langflow.memory import add_messages, add_messagetables, delete_messages, get_messages, store_message
|
||||
from langflow.schema.message import Message
|
||||
|
||||
# Assuming you have these imports available
|
||||
from langflow.services.database.models.message import MessageCreate, MessageRead
|
||||
from langflow.services.database.models.message.model import MessageTable
|
||||
from langflow.services.deps import session_scope
|
||||
from langflow.services.tracing.utils import convert_to_langchain_type
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def created_message():
|
||||
with session_scope() as session:
|
||||
message = MessageCreate(text="Test message", sender="User", sender_name="User", session_id="session_id")
|
||||
messagetable = MessageTable.model_validate(message, from_attributes=True)
|
||||
messagetables = add_messagetables([messagetable], session)
|
||||
message_read = MessageRead.model_validate(messagetables[0], from_attributes=True)
|
||||
return message_read
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def created_messages(session):
|
||||
with session_scope() as session:
|
||||
messages = [
|
||||
MessageCreate(text="Test message 1", sender="User", sender_name="User", session_id="session_id2"),
|
||||
MessageCreate(text="Test message 2", sender="User", sender_name="User", session_id="session_id2"),
|
||||
MessageCreate(text="Test message 3", sender="User", sender_name="User", session_id="session_id2"),
|
||||
]
|
||||
messagetables = [MessageTable.model_validate(message, from_attributes=True) for message in messages]
|
||||
messagetables = add_messagetables(messagetables, session)
|
||||
messages_read = [
|
||||
MessageRead.model_validate(messagetable, from_attributes=True) for messagetable in messagetables
|
||||
]
|
||||
return messages_read
|
||||
|
||||
|
||||
def test_get_messages():
|
||||
add_messages(
|
||||
[
|
||||
Message(text="Test message 1", sender="User", sender_name="User", session_id="session_id2"),
|
||||
Message(text="Test message 2", sender="User", sender_name="User", session_id="session_id2"),
|
||||
]
|
||||
)
|
||||
messages = get_messages(sender="User", session_id="session_id2", limit=2)
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "Test message 1"
|
||||
assert messages[1].text == "Test message 2"
|
||||
|
||||
|
||||
def test_add_messages():
|
||||
message = Message(text="New Test message", sender="User", sender_name="User", session_id="new_session_id")
|
||||
messages = add_messages(message)
|
||||
assert len(messages) == 1
|
||||
assert messages[0].text == "New Test message"
|
||||
|
||||
|
||||
def test_add_messagetables(session):
|
||||
messages = [MessageTable(text="New Test message", sender="User", sender_name="User", session_id="new_session_id")]
|
||||
added_messages = add_messagetables(messages, session)
|
||||
assert len(added_messages) == 1
|
||||
assert added_messages[0].text == "New Test message"
|
||||
|
||||
|
||||
def test_delete_messages(session):
|
||||
session_id = "session_id2"
|
||||
delete_messages(session_id)
|
||||
messages = session.query(MessageTable).filter(MessageTable.session_id == session_id).all()
|
||||
assert len(messages) == 0
|
||||
|
||||
|
||||
def test_store_message():
|
||||
message = Message(text="Stored message", sender="User", sender_name="User", session_id="stored_session_id")
|
||||
stored_messages = store_message(message)
|
||||
assert len(stored_messages) == 1
|
||||
assert stored_messages[0].text == "Stored message"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method_name", ["message", "convert_to_langchain_type"])
|
||||
def test_convert_to_langchain(method_name):
|
||||
def convert(value):
|
||||
if method_name == "message":
|
||||
return value.to_lc_message()
|
||||
elif method_name == "convert_to_langchain_type":
|
||||
return convert_to_langchain_type(value)
|
||||
else:
|
||||
raise ValueError(f"Invalid method: {method_name}")
|
||||
|
||||
lc_message = convert(Message(text="Test message 1", sender="User", sender_name="User", session_id="session_id2"))
|
||||
assert lc_message.content == "Test message 1"
|
||||
assert lc_message.type == "human"
|
||||
|
||||
lc_message = convert(Message(text="Test message 2", sender="AI", session_id="session_id2"))
|
||||
assert lc_message.content == "Test message 2"
|
||||
assert lc_message.type == "ai"
|
||||
|
||||
iterator = iter(["stream", "message"])
|
||||
lc_message = convert(Message(text=iterator, sender="AI", session_id="session_id2"))
|
||||
assert lc_message.content == ""
|
||||
assert lc_message.type == "ai"
|
||||
assert len(list(iterator)) == 2
|
||||
303
src/backend/tests/unit/test_process.py
Normal file
303
src/backend/tests/unit/test_process.py
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
import pytest
|
||||
from langflow.processing.process import process_tweaks
|
||||
from langflow.services.deps import get_session_service
|
||||
|
||||
|
||||
def test_no_tweaks():
|
||||
graph_data = {
|
||||
"data": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node1",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"param1": {"value": 1},
|
||||
"param2": {"value": 2},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "node2",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"param1": {"value": 3},
|
||||
"param2": {"value": 4},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
tweaks = {}
|
||||
result = process_tweaks(graph_data, tweaks)
|
||||
assert result == graph_data
|
||||
|
||||
|
||||
def test_single_tweak():
|
||||
graph_data = {
|
||||
"data": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node1",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"param1": {"value": 1, "type": "int"},
|
||||
"param2": {"value": 2, "type": "int"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "node2",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"param1": {"value": 3, "type": "int"},
|
||||
"param2": {"value": 4, "type": "int"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
tweaks = {"node1": {"param1": 5}}
|
||||
expected_result = {
|
||||
"data": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node1",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"param1": {"value": 5, "type": "int"},
|
||||
"param2": {"value": 2, "type": "int"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "node2",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"param1": {"value": 3, "type": "int"},
|
||||
"param2": {"value": 4, "type": "int"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
result = process_tweaks(graph_data, tweaks)
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
def test_multiple_tweaks():
|
||||
graph_data = {
|
||||
"data": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node1",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"param1": {"value": 1, "type": "int"},
|
||||
"param2": {"value": 2, "type": "int"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "node2",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"param1": {"value": 3, "type": "int"},
|
||||
"param2": {"value": 4, "type": "int"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
tweaks = {
|
||||
"node1": {"param1": 5, "param2": 6},
|
||||
"node2": {"param1": 7},
|
||||
}
|
||||
expected_result = {
|
||||
"data": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node1",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"param1": {"value": 5, "type": "int"},
|
||||
"param2": {"value": 6, "type": "int"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "node2",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"param1": {"value": 7, "type": "int"},
|
||||
"param2": {"value": 4, "type": "int"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
result = process_tweaks(graph_data, tweaks)
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
# Test twekas that just pass the param and value but no node id.
|
||||
# This is a new feature that was added to the process_tweaks function
|
||||
def test_tweak_no_node_id():
|
||||
graph_data = {
|
||||
"data": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node1",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"param1": {"value": 1, "type": "int"},
|
||||
"param2": {"value": 2, "type": "int"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "node2",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"param1": {"value": 3, "type": "int"},
|
||||
"param2": {"value": 4, "type": "int"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
tweaks = {"param1": 5}
|
||||
expected_result = {
|
||||
"data": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node1",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"param1": {"value": 5, "type": "int"},
|
||||
"param2": {"value": 2, "type": "int"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "node2",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"param1": {"value": 5, "type": "int"},
|
||||
"param2": {"value": 4, "type": "int"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
result = process_tweaks(graph_data, tweaks)
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
def test_tweak_not_in_template():
|
||||
graph_data = {
|
||||
"data": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node1",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"param1": {"value": 1, "type": "int"},
|
||||
"param2": {"value": 2, "type": "int"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "node2",
|
||||
"data": {
|
||||
"node": {
|
||||
"template": {
|
||||
"param1": {"value": 3, "type": "int"},
|
||||
"param2": {"value": 4, "type": "int"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
tweaks = {"node1": {"param3": 5}}
|
||||
result = process_tweaks(graph_data, tweaks)
|
||||
assert result == graph_data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_langchain_object_with_cached_session(client, basic_graph_data):
|
||||
# Provide a non-existent session_id
|
||||
session_service = get_session_service()
|
||||
session_id1 = "non-existent-session-id"
|
||||
graph1, artifacts1 = await session_service.load_session(session_id1, basic_graph_data)
|
||||
# Use the new session_id to get the langchain_object again
|
||||
graph2, artifacts2 = await session_service.load_session(session_id1, basic_graph_data)
|
||||
|
||||
assert graph1 == graph2
|
||||
assert artifacts1 == artifacts2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_langchain_object_with_no_cached_session(client, basic_graph_data):
|
||||
# Provide a non-existent session_id
|
||||
session_service = get_session_service()
|
||||
session_id1 = "non-existent-session-id"
|
||||
session_id = session_service.build_key(session_id1, basic_graph_data)
|
||||
graph1, artifacts1 = await session_service.load_session(session_id, data_graph=basic_graph_data, flow_id="flow_id")
|
||||
# Clear the cache
|
||||
await session_service.clear_session(session_id)
|
||||
# Use the new session_id to get the graph again
|
||||
graph2, artifacts2 = await session_service.load_session(session_id, data_graph=basic_graph_data, flow_id="flow_id")
|
||||
|
||||
# Since the cache was cleared, objects should be different
|
||||
assert id(graph1) != id(graph2)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_langchain_object_without_session_id(client, basic_graph_data):
|
||||
# Provide a non-existent session_id
|
||||
session_service = get_session_service()
|
||||
session_id1 = None
|
||||
graph1, artifacts1 = await session_service.load_session(session_id1, data_graph=basic_graph_data, flow_id="flow_id")
|
||||
# Use the new session_id to get the langchain_object again
|
||||
graph2, artifacts2 = await session_service.load_session(session_id1, data_graph=basic_graph_data, flow_id="flow_id")
|
||||
|
||||
assert graph1 == graph2
|
||||
132
src/backend/tests/unit/test_setup_superuser.py
Normal file
132
src/backend/tests/unit/test_setup_superuser.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from langflow.services.settings.constants import (
|
||||
DEFAULT_SUPERUSER,
|
||||
DEFAULT_SUPERUSER_PASSWORD,
|
||||
)
|
||||
from langflow.services.utils import teardown_superuser
|
||||
|
||||
# @patch("langflow.services.deps.get_session")
|
||||
# @patch("langflow.services.utils.create_super_user")
|
||||
# @patch("langflow.services.deps.get_settings_service")
|
||||
# # @patch("langflow.services.utils.verify_password")
|
||||
# def test_setup_superuser(
|
||||
# mock_get_session, mock_create_super_user, mock_get_settings_service
|
||||
# ):
|
||||
# # Test when AUTO_LOGIN is True
|
||||
# calls = []
|
||||
# mock_settings_service = Mock()
|
||||
# mock_settings_service.auth_settings.AUTO_LOGIN = True
|
||||
# mock_settings_service.auth_settings.SUPERUSER = DEFAULT_SUPERUSER
|
||||
# mock_settings_service.auth_settings.SUPERUSER_PASSWORD = DEFAULT_SUPERUSER_PASSWORD
|
||||
# mock_get_settings_service.return_value = mock_settings_service
|
||||
# mock_session = Mock()
|
||||
# mock_session.query.return_value.filter.return_value.first.return_value = (
|
||||
# mock_session
|
||||
# )
|
||||
# # return value of get_session is a generator
|
||||
# mock_get_session.return_value = iter([mock_session, mock_session, mock_session])
|
||||
# setup_superuser(mock_settings_service, mock_session)
|
||||
# mock_session.query.assert_called_once_with(User)
|
||||
# # Set return value of filter to be None
|
||||
# mock_session.query.return_value.filter.return_value.first.return_value = None
|
||||
# actual_expr = mock_session.query.return_value.filter.call_args[0][0]
|
||||
# expected_expr = User.username == DEFAULT_SUPERUSER
|
||||
|
||||
# assert str(actual_expr) == str(expected_expr)
|
||||
# create_call = call(
|
||||
# db=mock_session, username=DEFAULT_SUPERUSER, password=DEFAULT_SUPERUSER_PASSWORD
|
||||
# )
|
||||
# calls.append(create_call)
|
||||
# # mock_create_super_user.assert_has_calls(calls)
|
||||
# assert 1 == mock_create_super_user.call_count
|
||||
|
||||
# def reset_mock_credentials():
|
||||
# mock_settings_service.auth_settings.SUPERUSER = DEFAULT_SUPERUSER
|
||||
# mock_settings_service.auth_settings.SUPERUSER_PASSWORD = (
|
||||
# DEFAULT_SUPERUSER_PASSWORD
|
||||
# )
|
||||
|
||||
# ADMIN_USER_NAME = "admin_user"
|
||||
# # Test when username and password are default
|
||||
# mock_settings_service.auth_settings = Mock()
|
||||
# mock_settings_service.auth_settings.AUTO_LOGIN = False
|
||||
# mock_settings_service.auth_settings.SUPERUSER = ADMIN_USER_NAME
|
||||
# mock_settings_service.auth_settings.SUPERUSER_PASSWORD = "password"
|
||||
# mock_settings_service.auth_settings.reset_credentials = Mock(
|
||||
# side_effect=reset_mock_credentials
|
||||
# )
|
||||
|
||||
# mock_get_settings_service.return_value = mock_settings_service
|
||||
|
||||
# setup_superuser(mock_settings_service, mock_session)
|
||||
# mock_session.query.assert_called_with(User)
|
||||
# actual_expr = mock_session.query.return_value.filter.call_args[0][0]
|
||||
# expected_expr = User.username == ADMIN_USER_NAME
|
||||
|
||||
# assert str(actual_expr) == str(expected_expr)
|
||||
# create_call = call(db=mock_session, username=ADMIN_USER_NAME, password="password")
|
||||
# calls.append(create_call)
|
||||
# # mock_create_super_user.assert_has_calls(calls)
|
||||
# assert 2 == mock_create_super_user.call_count
|
||||
# # Test that superuser credentials are reset
|
||||
# mock_settings_service.auth_settings.reset_credentials.assert_called_once()
|
||||
# assert mock_settings_service.auth_settings.SUPERUSER != ADMIN_USER_NAME
|
||||
# assert mock_settings_service.auth_settings.SUPERUSER_PASSWORD != "password"
|
||||
|
||||
# # Test when superuser already exists
|
||||
# mock_settings_service.auth_settings.AUTO_LOGIN = False
|
||||
# mock_settings_service.auth_settings.SUPERUSER = ADMIN_USER_NAME
|
||||
# mock_settings_service.auth_settings.SUPERUSER_PASSWORD = "password"
|
||||
# mock_user = Mock()
|
||||
# mock_user.is_superuser = True
|
||||
# mock_session.query.return_value.filter.return_value.first.return_value = mock_user
|
||||
# setup_superuser(mock_settings_service, mock_session)
|
||||
# mock_session.query.assert_called_with(User)
|
||||
# actual_expr = mock_session.query.return_value.filter.call_args[0][0]
|
||||
# expected_expr = User.username == ADMIN_USER_NAME
|
||||
|
||||
# assert str(actual_expr) == str(expected_expr)
|
||||
|
||||
|
||||
@patch("langflow.services.deps.get_settings_service")
|
||||
@patch("langflow.services.deps.get_session")
|
||||
def test_teardown_superuser_default_superuser(mock_get_session, mock_get_settings_service):
|
||||
mock_settings_service = MagicMock()
|
||||
mock_settings_service.auth_settings.AUTO_LOGIN = True
|
||||
mock_settings_service.auth_settings.SUPERUSER = DEFAULT_SUPERUSER
|
||||
mock_settings_service.auth_settings.SUPERUSER_PASSWORD = DEFAULT_SUPERUSER_PASSWORD
|
||||
mock_get_settings_service.return_value = mock_settings_service
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_user = MagicMock()
|
||||
mock_user.is_superuser = True
|
||||
mock_session.query.return_value.filter.return_value.first.return_value = mock_user
|
||||
mock_get_session.return_value = iter([mock_session])
|
||||
|
||||
teardown_superuser(mock_settings_service, mock_session)
|
||||
|
||||
mock_session.query.assert_not_called()
|
||||
|
||||
|
||||
@patch("langflow.services.deps.get_settings_service")
|
||||
@patch("langflow.services.deps.get_session")
|
||||
def test_teardown_superuser_no_default_superuser(mock_get_session, mock_get_settings_service):
|
||||
ADMIN_USER_NAME = "admin_user"
|
||||
mock_settings_service = MagicMock()
|
||||
mock_settings_service.auth_settings.AUTO_LOGIN = False
|
||||
mock_settings_service.auth_settings.SUPERUSER = ADMIN_USER_NAME
|
||||
mock_settings_service.auth_settings.SUPERUSER_PASSWORD = "password"
|
||||
mock_get_settings_service.return_value = mock_settings_service
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_user = MagicMock()
|
||||
mock_user.is_superuser = False
|
||||
mock_session.query.return_value.filter.return_value.first.return_value = mock_user
|
||||
mock_get_session.return_value = [mock_session]
|
||||
|
||||
teardown_superuser(mock_settings_service, mock_session)
|
||||
|
||||
mock_session.query.assert_not_called()
|
||||
mock_session.delete.assert_not_called()
|
||||
mock_session.commit.assert_not_called()
|
||||
112
src/backend/tests/unit/test_telemetry.py
Normal file
112
src/backend/tests/unit/test_telemetry.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import pytest
|
||||
import threading
|
||||
from langflow.services.telemetry.opentelemetry import OpenTelemetry
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
|
||||
fixed_labels = {"flow_id": "this_flow_id", "service": "this", "user": "that"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def opentelemetry_instance():
|
||||
return OpenTelemetry()
|
||||
|
||||
|
||||
def test_init(opentelemetry_instance):
|
||||
assert isinstance(opentelemetry_instance, OpenTelemetry)
|
||||
assert len(opentelemetry_instance._metrics) > 1
|
||||
assert len(opentelemetry_instance._metrics) == len(opentelemetry_instance._metrics_registry) == 2
|
||||
assert "file_uploads" in opentelemetry_instance._metrics
|
||||
|
||||
|
||||
def test_gauge(opentelemetry_instance):
|
||||
opentelemetry_instance.update_gauge("file_uploads", 1024, fixed_labels)
|
||||
|
||||
|
||||
def test_gauge_with_counter_method(opentelemetry_instance):
|
||||
with pytest.raises(ValueError, match="Metric 'file_uploads' is not a counter"):
|
||||
opentelemetry_instance.increment_counter(metric_name="file_uploads", value=1, labels=fixed_labels)
|
||||
|
||||
|
||||
def test_gauge_with_historgram_method(opentelemetry_instance):
|
||||
with pytest.raises(ValueError, match="Metric 'file_uploads' is not a histogram"):
|
||||
opentelemetry_instance.observe_histogram("file_uploads", 1, fixed_labels)
|
||||
|
||||
|
||||
def test_gauge_with_up_down_counter_method(opentelemetry_instance):
|
||||
with pytest.raises(ValueError, match="Metric 'file_uploads' is not an up down counter"):
|
||||
opentelemetry_instance.up_down_counter("file_uploads", 1, labels=fixed_labels)
|
||||
|
||||
|
||||
def test_increment_counter(opentelemetry_instance):
|
||||
opentelemetry_instance.increment_counter(metric_name="num_files_uploaded", value=5, labels=fixed_labels)
|
||||
|
||||
|
||||
def test_increment_counter_empty_label(opentelemetry_instance):
|
||||
with pytest.raises(ValueError, match="Labels must be provided for the metric"):
|
||||
opentelemetry_instance.increment_counter(metric_name="num_files_uploaded", value=5, labels={})
|
||||
|
||||
|
||||
def test_increment_counter_missing_mandatory_label(opentelemetry_instance):
|
||||
with pytest.raises(ValueError, match="Missing required labels: {'flow_id'}"):
|
||||
opentelemetry_instance.increment_counter(metric_name="num_files_uploaded", value=5, labels={"service": "one"})
|
||||
|
||||
|
||||
def test_increment_counter_unregisted_metric(opentelemetry_instance):
|
||||
with pytest.raises(ValueError, match="Metric 'num_files_uploaded_1' is not registered"):
|
||||
opentelemetry_instance.increment_counter(metric_name="num_files_uploaded_1", value=5, labels=fixed_labels)
|
||||
|
||||
|
||||
def test_opentelementry_singleton(opentelemetry_instance):
|
||||
opentelemetry_instance_2 = OpenTelemetry()
|
||||
assert opentelemetry_instance is opentelemetry_instance_2
|
||||
|
||||
opentelemetry_instance_3 = OpenTelemetry(prometheus_enabled=False)
|
||||
assert opentelemetry_instance is opentelemetry_instance_3
|
||||
assert opentelemetry_instance.prometheus_enabled == opentelemetry_instance_3.prometheus_enabled
|
||||
|
||||
|
||||
def test_missing_labels(opentelemetry_instance):
|
||||
with pytest.raises(ValueError, match="Labels must be provided for the metric"):
|
||||
opentelemetry_instance.increment_counter(metric_name="num_files_uploaded", labels=None, value=1.0)
|
||||
with pytest.raises(ValueError, match="Labels must be provided for the metric"):
|
||||
opentelemetry_instance.up_down_counter("num_files_uploaded", 1, None)
|
||||
with pytest.raises(ValueError, match="Labels must be provided for the metric"):
|
||||
opentelemetry_instance.update_gauge(metric_name="num_files_uploaded", value=1.0, labels=dict())
|
||||
with pytest.raises(ValueError, match="Labels must be provided for the metric"):
|
||||
opentelemetry_instance.observe_histogram("num_files_uploaded", 1, dict())
|
||||
|
||||
|
||||
def test_multithreaded_singleton():
|
||||
def create_instance():
|
||||
return OpenTelemetry()
|
||||
|
||||
# Create instances in multiple threads
|
||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||
futures = [executor.submit(create_instance) for _ in range(100)]
|
||||
instances = [future.result() for future in as_completed(futures)]
|
||||
|
||||
# Check that all instances are the same
|
||||
first_instance = instances[0]
|
||||
for instance in instances[1:]:
|
||||
assert instance is first_instance
|
||||
|
||||
|
||||
def test_multithreaded_singleton_race_condition():
|
||||
# This test simulates a potential race condition
|
||||
start_event = threading.Event()
|
||||
|
||||
def create_instance():
|
||||
start_event.wait() # Wait for all threads to be ready
|
||||
return OpenTelemetry()
|
||||
|
||||
# Create instances in multiple threads, all starting at the same time
|
||||
with ThreadPoolExecutor(max_workers=100) as executor:
|
||||
futures = [executor.submit(create_instance) for _ in range(100)]
|
||||
start_event.set() # Start all threads simultaneously
|
||||
instances = [future.result() for future in as_completed(futures)]
|
||||
|
||||
# Check that all instances are the same
|
||||
first_instance = instances[0]
|
||||
for instance in instances[1:]:
|
||||
assert instance is first_instance
|
||||
93
src/backend/tests/unit/test_template.py
Normal file
93
src/backend/tests/unit/test_template.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import importlib
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import pytest
|
||||
from langflow.utils.util import build_template_from_function, get_base_classes, get_default_factory
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# Dummy classes for testing purposes
|
||||
class Parent(BaseModel):
|
||||
"""Parent Class"""
|
||||
|
||||
parent_field: str
|
||||
|
||||
|
||||
class Child(Parent):
|
||||
"""Child Class"""
|
||||
|
||||
child_field: int
|
||||
|
||||
|
||||
class ExampleClass1(BaseModel):
|
||||
"""Example class 1."""
|
||||
|
||||
def __init__(self, data: Optional[List[int]] = None):
|
||||
self.data = data or [1, 2, 3]
|
||||
|
||||
|
||||
class ExampleClass2(BaseModel):
|
||||
"""Example class 2."""
|
||||
|
||||
def __init__(self, data: Optional[Dict[str, int]] = None):
|
||||
self.data = data or {"a": 1, "b": 2, "c": 3}
|
||||
|
||||
|
||||
def example_loader_1() -> ExampleClass1:
|
||||
"""Example loader function 1."""
|
||||
return ExampleClass1()
|
||||
|
||||
|
||||
def example_loader_2() -> ExampleClass2:
|
||||
"""Example loader function 2."""
|
||||
return ExampleClass2()
|
||||
|
||||
|
||||
def test_build_template_from_function():
|
||||
type_to_loader_dict = {
|
||||
"example1": example_loader_1,
|
||||
"example2": example_loader_2,
|
||||
}
|
||||
|
||||
# Test with valid name
|
||||
result = build_template_from_function("ExampleClass1", type_to_loader_dict)
|
||||
|
||||
assert result is not None
|
||||
assert "template" in result
|
||||
assert "description" in result
|
||||
assert "base_classes" in result
|
||||
|
||||
# Test with add_function=True
|
||||
result_with_function = build_template_from_function("ExampleClass1", type_to_loader_dict, add_function=True)
|
||||
assert result_with_function is not None
|
||||
assert "Callable" in result_with_function["base_classes"]
|
||||
|
||||
# Test with invalid name
|
||||
with pytest.raises(ValueError, match=r".* not found"):
|
||||
build_template_from_function("NonExistent", type_to_loader_dict)
|
||||
|
||||
|
||||
# Test get_base_classes
|
||||
def test_get_base_classes():
|
||||
base_classes_parent = get_base_classes(Parent)
|
||||
base_classes_child = get_base_classes(Child)
|
||||
|
||||
assert "Parent" in base_classes_parent
|
||||
assert "Child" in base_classes_child
|
||||
assert "Parent" in base_classes_child
|
||||
|
||||
|
||||
# Test get_default_factory
|
||||
def test_get_default_factory():
|
||||
module_name = "langflow.utils.util"
|
||||
function_repr = "<function dummy_function>"
|
||||
|
||||
def dummy_function():
|
||||
return "default_value"
|
||||
|
||||
# Add dummy_function to your_module
|
||||
setattr(importlib.import_module(module_name), "dummy_function", dummy_function)
|
||||
|
||||
default_value = get_default_factory(module_name, function_repr)
|
||||
|
||||
assert default_value == "default_value"
|
||||
104
src/backend/tests/unit/test_validate_code.py
Normal file
104
src/backend/tests/unit/test_validate_code.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from requests.exceptions import MissingSchema
|
||||
|
||||
from langflow.utils.validate import create_function, execute_function, extract_function_name, validate_code
|
||||
|
||||
|
||||
def test_create_function():
|
||||
code = """
|
||||
from pathlib import Path
|
||||
|
||||
def my_function(x: str) -> Path:
|
||||
return Path(x)
|
||||
"""
|
||||
|
||||
function_name = extract_function_name(code)
|
||||
function = create_function(code, function_name)
|
||||
result = function("test")
|
||||
assert result == Path("test")
|
||||
|
||||
|
||||
def test_validate_code():
|
||||
# Test case with a valid import and function
|
||||
code1 = """
|
||||
import math
|
||||
|
||||
def square(x):
|
||||
return x ** 2
|
||||
"""
|
||||
errors1 = validate_code(code1)
|
||||
assert errors1 == {"imports": {"errors": []}, "function": {"errors": []}}
|
||||
|
||||
# Test case with an invalid import and valid function
|
||||
code2 = """
|
||||
import non_existent_module
|
||||
|
||||
def square(x):
|
||||
return x ** 2
|
||||
"""
|
||||
errors2 = validate_code(code2)
|
||||
assert errors2 == {
|
||||
"imports": {"errors": ["No module named 'non_existent_module'"]},
|
||||
"function": {"errors": []},
|
||||
}
|
||||
|
||||
# Test case with a valid import and invalid function syntax
|
||||
code3 = """
|
||||
import math
|
||||
|
||||
def square(x)
|
||||
return x ** 2
|
||||
"""
|
||||
errors3 = validate_code(code3)
|
||||
assert errors3 == {
|
||||
"imports": {"errors": []},
|
||||
"function": {"errors": ["expected ':' (<unknown>, line 4)"]},
|
||||
}
|
||||
|
||||
|
||||
def test_execute_function_success():
|
||||
code = """
|
||||
import math
|
||||
|
||||
def my_function(x):
|
||||
return math.sin(x) + 1
|
||||
"""
|
||||
result = execute_function(code, "my_function", 0.5)
|
||||
assert result == 1.479425538604203
|
||||
|
||||
|
||||
def test_execute_function_missing_module():
|
||||
code = """
|
||||
import some_missing_module
|
||||
|
||||
def my_function(x):
|
||||
return some_missing_module.some_function(x)
|
||||
"""
|
||||
with pytest.raises(ModuleNotFoundError):
|
||||
execute_function(code, "my_function", 0.5)
|
||||
|
||||
|
||||
def test_execute_function_missing_function():
|
||||
code = """
|
||||
import math
|
||||
|
||||
def my_function(x):
|
||||
return math.some_missing_function(x)
|
||||
"""
|
||||
with pytest.raises(AttributeError):
|
||||
execute_function(code, "my_function", 0.5)
|
||||
|
||||
|
||||
def test_execute_function_missing_schema():
|
||||
code = """
|
||||
import requests
|
||||
|
||||
def my_function(x):
|
||||
return requests.get(x).text
|
||||
"""
|
||||
with mock.patch("requests.get", side_effect=MissingSchema):
|
||||
with pytest.raises(MissingSchema):
|
||||
execute_function(code, "my_function", "invalid_url")
|
||||
15
src/backend/tests/unit/text_experimental_components.py
Normal file
15
src/backend/tests/unit/text_experimental_components.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from langflow.components import prototypes
|
||||
|
||||
|
||||
def test_python_function_component():
|
||||
# Arrange
|
||||
python_function_component = prototypes.PythonFunctionComponent()
|
||||
|
||||
# Act
|
||||
# function must be a string representation
|
||||
function = "def function():\n return 'Hello, World!'"
|
||||
# result is the callable function
|
||||
result = python_function_component.build(function)
|
||||
|
||||
# Assert
|
||||
assert result() == "Hello, World!"
|
||||
Loading…
Add table
Add a link
Reference in a new issue