🐛 fix(flows.py): change Flow.from_orm() to Flow.model_validate() to ensure data integrity and validation 🐛 fix(users.py): remove unused import statements to improve code cleanliness and maintainability 🐛 fix(users.py): change User.from_orm() to User.model_validate() to ensure data integrity and validation 🐛 fix(LLMChain.py): remove unused import statements to improve code cleanliness and maintainability 🐛 fix(LLMChain.py): remove unnecessary line breaks to improve code readability 🐛 fix(base.py): remove unused import statements to improve code cleanliness and maintainability 🐛 fix(base.py): remove unnecessary line breaks to improve code readability 🐛 fix(base.py): fix condition to append vertex_id to top_level_vertices to avoid appending non-string values 🐛 fix(vertex/base.py): add parent_node_id attribute to Vertex class to support hierarchical graph structures 🐛 fix(base.py): remove unused import statements to improve code cleanliness and maintainability 🚀 feat(GroupTest): add a new node for a simple chat with a custom prompt template and conversational memory buffer ℹ️ This commit adds a new node to the GroupTest project. The node is a genericNode with the following properties: - Width: 384 - Height: 621 - ID: ChatOpenAI-rUJ1b - Type: genericNode - Position: x: 170.87326389541306, y: 465.8628482073749 - 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 🔧 chore: fix formatting issue in code 📝 docs: update documentation link for `OpenAI` Chat large language models API 🔧 chore: update prompt template configuration in LLMChain node 📝 docs: add documentation link for PromptTemplate in the description 📝 chore(grouped_chat.json): add grouped_chat.json test data file This commit adds the `grouped_chat.json` file to the `tests/data` directory. The file contains a JSON object representing grouped chat data. This file is necessary for testing and will be used in the test suite. 📝 chore(one_group_chat.json): add one_group_chat.json test data file This commit adds the one_group_chat.json file, which contains a simple chat with a custom prompt template and conversational memory buffer. This file is used for testing purposes. 🔧 chore: update node configuration for ConversationBufferMemory, ChatOpenAI, and LLMChain 📝 docs: update documentation links for ConversationBufferMemory and LLMChain 🔧 fix: update prompt template in LLMChain to include conversation history and text input variables 🔧 fix: update ConversationBufferMemory node to include description and documentation link 🎨 style: format and organize code for better readability and maintainability 🆕 feat(Vector Store): add Vector Store agent and Vector Store Info node The Vector Store agent allows querying a Vector Store. It can be used to construct an agent from a Vector Store. The Vector Store Info node provides information about a Vector Store. The Vector Store agent and Vector Store Info node are added to support the functionality of querying a Vector Store. 🔧 chore: update configuration options in the OpenAI API client The configuration options in the OpenAI API client have been updated. This commit includes changes to the following options: - `max_tokens`: Removed the `required` flag and set `show` to `true` - `metadata`: Set `show` to `false` - `model_kwargs`: Set `show` to `true` and `advanced` to `true` - `model_name`: Added 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` - `n`: Removed the `show` flag - `openai_api_base`: Added `display_name` as "OpenAI API Base" and updated `info` with additional details - `openai_api_key`: Removed the `required` flag and set `show` to `true` - `openai_organization`: Removed the `show` flag - `openai_proxy`: Removed the `show` flag - `request_timeout`: Removed the `show` flag - `streaming`: Removed the `show` flag - `tags`: Removed the `show` flag - `temperature`: Removed the `show` flag - `tiktoken_model_name`: Removed the `show` flag - `verbose`: Removed the `show` flag 🔧 chore: update configuration for ChatOpenAI and Chroma nodes The configuration for the ChatOpenAI and Chroma nodes has been updated. This includes changes to the allowed_special, disallowed_special, chunk_size, client, deployment, embedding_ctx_length, and max_retries properties. These changes were made to improve the functionality and performance of the nodes. 🔧 chore(config): update OpenAIEmbeddings-YwSvx configuration options The OpenAIEmbeddings-YwSvx configuration options have been updated to include new fields and values. This commit updates the configuration file to reflect these changes. 🔧 chore(config): update configuration options for OpenAIEmbeddings and Chroma 🔧 chore(config): update configuration options for OpenAIEmbeddings and Chroma to improve flexibility and customization 🔧 chore: update configuration options for RecursiveCharacterTextSplitter and WebBaseLoader in flow The configuration options for RecursiveCharacterTextSplitter and WebBaseLoader in the flow have been updated. The changes include: - Persist Directory - Chroma: The persist directory option for Chroma has been modified. - Search Kwargs - Chroma: The search kwargs option for Chroma has been modified. - Chunk Overlap - RecursiveCharacterTextSplitter: The chunk overlap option for RecursiveCharacterTextSplitter has been modified. - Chunk Size - RecursiveCharacterTextSplitter: The chunk size option for RecursiveCharacterTextSplitter has been modified. - Separator Type - RecursiveCharacterTextSplitter: The separator type option for RecursiveCharacterTextSplitter has been modified. - Separator - RecursiveCharacterTextSplitter: The separator option for RecursiveCharacterTextSplitter has been modified. - Metadata - WebBaseLoader: The metadata option for WebBaseLoader has been modified. - Web Page - WebBaseLoader: The web page option for WebBaseLoader has been modified. 🔧 chore(OpenAIEmbeddings): update OpenAIEmbeddings configuration options The OpenAIEmbeddings node configuration options have been updated to include the following changes: - `allowed_special` and `disallowed_special` now accept a list of values instead of a single value - `chunk_size` now accepts an integer value - `deployment` now accepts a string value - `embedding_ctx_length` now accepts an integer value - `headers` now supports multiline values - `max_retries` now accepts an integer value - `model` now accepts a string value - `model_kwargs` now accepts code input - `openai_api_base` now accepts a password input - `openai_api_key` now accepts a password input - `openai_api_type` now accepts a password input - `openai_api_version` now accepts a password input - `openai_organization` has been removed from the configuration options 🔧 chore: update OpenAIEmbeddings configuration options in the UI The OpenAIEmbeddings configuration options in the UI have been updated to include the following changes: - Added the `openai_organization` option to specify the OpenAI organization. - Added the `openai_proxy` option to configure the OpenAI proxy. - Added the `request_timeout` option to set the request timeout. - Added the `show_progress_bar` option to control the visibility of the progress bar. - Changed the `tiktoken_model_name` option to be a password field. - Updated the documentation link for OpenAIEmbeddings. This commit updates the configuration options to improve the usability and functionality of the OpenAIEmbeddings module in the UI. 🔧 chore: clean up unused code and remove unnecessary fields in the configuration file 📝 docs: update documentation link for the Chroma vectorstore module 🔧 chore: update configuration options for RecursiveCharacterTextSplitter in flow The configuration options for the RecursiveCharacterTextSplitter node in the flow have been updated. The following changes were made: - `chunk_size` option: The default value has been changed to 1000. - `separator_type` option: The available options have been updated to include "Text", "cpp", "go", "html", "java", "js", "latex", "markdown", "php", "proto", "python", "rst", "ruby", "rust", "scala", "sol", and "swift". - `separators` option: The default value has been changed to ".". These changes were made to improve the usability and flexibility of the RecursiveCharacterTextSplitter node in the flow. 📝 chore(vector_store_grouped.json): add vector_store_grouped.json test data file 🔀 chore(vector_store_grouped.json): add vector_store_grouped.json test data file 🔨 refactor(test_graph.py): reformat import statements and improve code readability 🔨 refactor(test_prompts_template.py): change dynamic attribute to True for input variables, output parser, partial variables, template, and validate template 🔨 refactor(test_template.py): reformat import statements and remove duplicate import of BaseModel 🔨 refactor(test_template.py): update value for options in format_dict test
319 lines
9.7 KiB
Python
319 lines
9.7 KiB
Python
import json
|
|
|
|
# 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 fastapi.testclient import TestClient
|
|
from httpx import AsyncClient
|
|
from sqlmodel import Session, SQLModel, create_engine
|
|
from sqlmodel.pool import StaticPool
|
|
from typer.testing import CliRunner
|
|
|
|
from langflow.graph.graph.base import Graph
|
|
from langflow.services.auth.utils import get_password_hash
|
|
from langflow.services.database.models.flow.model import Flow, FlowCreate
|
|
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
|
|
|
|
|
|
def pytest_configure():
|
|
pytest.BASIC_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "basic_example.json"
|
|
pytest.COMPLEX_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "complex_example.json"
|
|
pytest.OPENAPI_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "Openapi.json"
|
|
pytest.GROUPED_CHAT_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "grouped_chat.json"
|
|
pytest.ONE_GROUPED_CHAT_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "one_group_chat.json"
|
|
pytest.VECTOR_STORE_GROUPED_EXAMPLE_PATH = Path(__file__).parent.absolute() / "data" / "vector_store_grouped.json"
|
|
|
|
pytest.BASIC_CHAT_WITH_PROMPT_AND_HISTORY = (
|
|
Path(__file__).parent.absolute() / "data" / "BasicChatwithPromptandHistory.json"
|
|
)
|
|
pytest.VECTOR_STORE_PATH = Path(__file__).parent.absolute() / "data" / "Vector_store.json"
|
|
pytest.CODE_WITH_SYNTAX_ERROR = """
|
|
def get_text():
|
|
retun "Hello World"
|
|
"""
|
|
|
|
|
|
@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="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_vector_store():
|
|
with open(pytest.VECTOR_STORE_PATH, "r") as f:
|
|
return f.read()
|
|
|
|
|
|
@pytest.fixture(name="client", autouse=True)
|
|
def client_fixture(session: Session, monkeypatch):
|
|
# Set the database url to a test database
|
|
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")
|
|
|
|
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.query(User).filter(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,
|
|
description="description",
|
|
)
|
|
flow = Flow.model_validate(flow_data.model_dump())
|
|
with session_getter(get_db_service()) as session:
|
|
session.add(flow)
|
|
session.commit()
|
|
session.refresh(flow)
|
|
|
|
return flow
|
|
|
|
|
|
@pytest.fixture
|
|
def added_flow(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_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 test_component_code():
|
|
path = Path(__file__).parent.absolute() / "data" / "component.py"
|
|
# load the content as a string
|
|
with open(path, "r") as f:
|
|
return f.read()
|
|
|
|
|
|
@pytest.fixture
|
|
def test_component_with_templatefield_code():
|
|
path = Path(__file__).parent.absolute() / "data" / "component_with_templatefield.py"
|
|
# load the content as a string
|
|
with open(path, "r") as f:
|
|
return f.read()
|