feat: Add not contains filter operator in DataFrame Operations Component (#9415)

* 🐛 (dataframe_operations.py): Fix bug in DataFrameOperationsComponent where "not contains" filter option was missing, causing incorrect filtering behavior.

* [autofix.ci] apply automated fixes

* Update pyproject versions

* fix: Avoid namespace collision for Astra (#9544)

* fix: Avoid namespace collision for Astra

* [autofix.ci] apply automated fixes

* Update Vector Store RAG.json

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* fix: Revert to a working composio release for module import (#9569)

fix: revert to stable composio version

* fix: Knowledge base component refactor (#9543)

* fix: Knowledge base component refactor

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* Update styleUtils.ts

* Update ingestion.py

* [autofix.ci] apply automated fixes

* Fix ingestion of df

* [autofix.ci] apply automated fixes

* Update Knowledge Ingestion.json

* Fix one failing test

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* Revert composio versions for CI

* Revert "Revert composio versions for CI"

This reverts commit 9bcb694ea1e20d544cf5e17fed2f9f4c0a3c192b.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
Co-authored-by: Carlos Coelho <80289056+carlosrcoelho@users.noreply.github.com>

* fix: Fix env file handling in Windows build scripts (#9414)

fix .env load on windows script

Co-authored-by: Ítalo Johnny <italojohnnydosanjos@gmail.com>

* fix: update agent_llm display name to "Model Provider" in AgentComponent (#9564)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* 📝 (test_mcp_util.py): add a check to skip test if DeepWiki server is rate limiting requests to avoid false test failures

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jordan Frazier <jordan.frazier@datastax.com>
Co-authored-by: Eric Hare <ericrhare@gmail.com>
Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
Co-authored-by: Carlos Coelho <80289056+carlosrcoelho@users.noreply.github.com>
Co-authored-by: Ítalo Johnny <italojohnnydosanjos@gmail.com>
This commit is contained in:
Cristhian Zanforlin Lousa 2025-08-29 14:09:30 -03:00 committed by GitHub
commit ab017bafcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 3041 additions and 2970 deletions

View file

@ -1,6 +1,6 @@
[project]
name = "langflow"
version = "1.5.0.post2"
version = "1.6.0"
description = "A Python package with a built-in web application"
requires-python = ">=3.10,<3.14"
license = "MIT"

View file

@ -1,14 +1,14 @@
@echo off
echo Starting Langflow build and run process...
REM Check if .env file exists and set env file parameter
set "ENV_FILE_PARAM="
REM Check if .env file exists and set env file flag
set "USE_ENV_FILE="
REM Get the script directory and resolve project root
for %%I in ("%~dp0..\..") do set "PROJECT_ROOT=%%~fI"
set "ENV_PATH=%PROJECT_ROOT%\.env"
if exist "%ENV_PATH%" (
echo Found .env file at: %ENV_PATH%
set "ENV_FILE_PARAM=--env-file \"%ENV_PATH%\""
set "USE_ENV_FILE=1"
) else (
echo .env file not found at: %ENV_PATH%
echo Langflow will use default configuration
@ -85,8 +85,8 @@ echo Step 4: Running Langflow...
echo.
echo Attention: Wait until uvicorn is running before opening the browser
echo.
if defined ENV_FILE_PARAM (
uv run langflow run %ENV_FILE_PARAM%
if defined USE_ENV_FILE (
uv run --env-file "%ENV_PATH%" langflow run
) else (
uv run langflow run
)

View file

@ -87,7 +87,7 @@ Write-Host "`nStep 4: Running Langflow..." -ForegroundColor Yellow
Write-Host "`nAttention: Wait until uvicorn is running before opening the browser" -ForegroundColor Red
try {
if ($useEnvFile) {
& uv run langflow run --env-file $envPath
& uv run --env-file $envPath langflow run
} else {
& uv run langflow run
}

View file

@ -483,6 +483,7 @@ class AgentComponent(ToolCallingAgentComponent):
build_config.update(fields_to_add)
# Reset input types for agent_llm
build_config["agent_llm"]["input_types"] = []
build_config["agent_llm"]["display_name"] = "Model Provider"
elif field_value == "Custom":
# Delete all provider fields
self.delete_fields(build_config, ALL_PROVIDER_FIELDS)

View file

@ -3,8 +3,6 @@ from .csv_to_data import CSVToDataComponent
from .directory import DirectoryComponent
from .file import FileComponent
from .json_to_data import JSONToDataComponent
from .kb_ingest import KBIngestionComponent
from .kb_retrieval import KBRetrievalComponent
from .news_search import NewsSearchComponent
from .rss import RSSReaderComponent
from .sql_executor import SQLComponent
@ -18,8 +16,6 @@ __all__ = [
"DirectoryComponent",
"FileComponent",
"JSONToDataComponent",
"KBIngestionComponent",
"KBRetrievalComponent",
"NewsSearchComponent",
"RSSReaderComponent",
"SQLComponent",

View file

@ -8,9 +8,9 @@ if TYPE_CHECKING:
from .astra_assistant_manager import AstraAssistantManager
from .astra_db import AstraDBChatMemory
from .astra_vectorize import AstraVectorizeComponent
from .astradb import AstraDBVectorStoreComponent
from .astradb_cql import AstraDBCQLToolComponent
from .astradb_tool import AstraDBToolComponent
from .astradb_vectorstore import AstraDBVectorStoreComponent
from .create_assistant import AssistantsCreateAssistant
from .create_thread import AssistantsCreateThread
from .dotenv import Dotenv
@ -29,7 +29,7 @@ _dynamic_imports = {
"AstraDBCQLToolComponent": "astradb_cql",
"AstraDBChatMemory": "astra_db",
"AstraDBToolComponent": "astradb_tool",
"AstraDBVectorStoreComponent": "astradb",
"AstraDBVectorStoreComponent": "astradb_vectorstore",
"AstraVectorizeComponent": "astra_vectorize",
"Dotenv": "dotenv",
"GetEnvVar": "getenvvar",

View file

@ -0,0 +1,34 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from langflow.components._importing import import_mod
if TYPE_CHECKING:
from langflow.components.knowledge_bases.ingestion import KnowledgeIngestionComponent
from langflow.components.knowledge_bases.retrieval import KnowledgeRetrievalComponent
_dynamic_imports = {
"KnowledgeIngestionComponent": "ingestion",
"KnowledgeRetrievalComponent": "retrieval",
}
__all__ = ["KnowledgeIngestionComponent", "KnowledgeRetrievalComponent"]
def __getattr__(attr_name: str) -> Any:
"""Lazily import input/output components on attribute access."""
if attr_name not in _dynamic_imports:
msg = f"module '{__name__}' has no attribute '{attr_name}'"
raise AttributeError(msg)
try:
result = import_mod(attr_name, _dynamic_imports[attr_name], __spec__.parent)
except (ModuleNotFoundError, ImportError, AttributeError) as e:
msg = f"Could not import '{attr_name}' from '{__name__}': {e}"
raise AttributeError(msg) from e
globals()[attr_name] = result
return result
def __dir__() -> list[str]:
return list(__all__)

View file

@ -9,17 +9,18 @@ import uuid
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from typing import TYPE_CHECKING, Any
import pandas as pd
from cryptography.fernet import InvalidToken
from langchain_chroma import Chroma
from loguru import logger
from langflow.base.data.kb_utils import get_knowledge_bases
from langflow.base.knowledge_bases.knowledge_base_utils import get_knowledge_bases
from langflow.base.models.openai_constants import OPENAI_EMBEDDING_MODEL_NAMES
from langflow.components.processing.converter import convert_to_dataframe
from langflow.custom import Component
from langflow.io import BoolInput, DataFrameInput, DropdownInput, IntInput, Output, SecretStrInput, StrInput, TableInput
from langflow.io import BoolInput, DropdownInput, HandleInput, IntInput, Output, SecretStrInput, StrInput, TableInput
from langflow.schema.data import Data
from langflow.schema.dotdict import dotdict # noqa: TC001
from langflow.schema.table import EditMode
@ -27,6 +28,9 @@ from langflow.services.auth.utils import decrypt_api_key, encrypt_api_key
from langflow.services.database.models.user.crud import get_user_by_id
from langflow.services.deps import get_settings_service, get_variable_service, session_scope
if TYPE_CHECKING:
from langflow.schema.dataframe import DataFrame
HUGGINGFACE_MODEL_NAMES = ["sentence-transformers/all-MiniLM-L6-v2", "sentence-transformers/all-mpnet-base-v2"]
COHERE_MODEL_NAMES = ["embed-english-v3.0", "embed-multilingual-v3.0"]
@ -38,14 +42,14 @@ if not knowledge_directory:
KNOWLEDGE_BASES_ROOT_PATH = Path(knowledge_directory).expanduser()
class KBIngestionComponent(Component):
class KnowledgeIngestionComponent(Component):
"""Create or append to Langflow Knowledge from a DataFrame."""
# ------ UI metadata ---------------------------------------------------
display_name = "Knowledge Ingestion"
description = "Create or update knowledge in Langflow."
icon = "database"
name = "KBIngestion"
icon = "upload"
name = "KnowledgeIngestion"
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
@ -101,12 +105,17 @@ class KBIngestionComponent(Component):
required=True,
options=[],
refresh_button=True,
real_time_refresh=True,
dialog_inputs=asdict(NewKnowledgeBaseInput()),
),
DataFrameInput(
HandleInput(
name="input_df",
display_name="Data",
info="Table with all original columns (already chunked / processed).",
display_name="Input",
info=(
"Table with all original columns (already chunked / processed). "
"Accepts Data or DataFrame. If Data is provided, it is converted to a DataFrame automatically."
),
input_types=["Data", "DataFrame"],
required=True,
),
TableInput(
@ -171,7 +180,7 @@ class KBIngestionComponent(Component):
]
# ------ Outputs -------------------------------------------------------
outputs = [Output(display_name="DataFrame", name="dataframe", method="build_kb_info")]
outputs = [Output(display_name="Results", name="dataframe_output", method="build_kb_info")]
# ------ Internal helpers ---------------------------------------------
def _get_kb_root(self) -> Path:
@ -503,8 +512,8 @@ class KBIngestionComponent(Component):
async def build_kb_info(self) -> Data:
"""Main ingestion routine → returns a dict with KB metadata."""
try:
# Get source DataFrame
df_source: pd.DataFrame = self.input_df
input_value = self.input_df[0] if isinstance(self.input_df, list) else self.input_df
df_source: DataFrame = convert_to_dataframe(input_value)
# Validate column configuration (using Structured Output patterns)
config_list = self._validate_column_config(df_source)
@ -559,9 +568,8 @@ class KBIngestionComponent(Component):
return Data(data=meta)
except (OSError, ValueError, RuntimeError, KeyError) as e:
self.log(f"Error in KB ingestion: {e}")
self.status = f"❌ KB ingestion failed: {e}"
return Data(data={"error": str(e), "kb_name": self.knowledge_base})
msg = f"Error during KB ingestion: {e}"
raise RuntimeError(msg) from e
async def _get_api_key_variable(self, field_value: dict[str, Any]):
async with session_scope() as db:

View file

@ -7,7 +7,7 @@ from langchain_chroma import Chroma
from loguru import logger
from pydantic import SecretStr
from langflow.base.data.kb_utils import get_knowledge_bases
from langflow.base.knowledge_bases.knowledge_base_utils import get_knowledge_bases
from langflow.custom import Component
from langflow.io import BoolInput, DropdownInput, IntInput, MessageTextInput, Output, SecretStrInput
from langflow.schema.data import Data
@ -24,11 +24,11 @@ if not knowledge_directory:
KNOWLEDGE_BASES_ROOT_PATH = Path(knowledge_directory).expanduser()
class KBRetrievalComponent(Component):
class KnowledgeRetrievalComponent(Component):
display_name = "Knowledge Retrieval"
description = "Search and retrieve data from knowledge."
icon = "database"
name = "KBRetrieval"
icon = "download"
name = "KnowledgeRetrieval"
inputs = [
DropdownInput(
@ -51,6 +51,7 @@ class KBRetrievalComponent(Component):
name="search_query",
display_name="Search Query",
info="Optional search query to filter knowledge base data.",
tool_mode=True,
),
IntInput(
name="top_k",
@ -63,17 +64,24 @@ class KBRetrievalComponent(Component):
BoolInput(
name="include_metadata",
display_name="Include Metadata",
info="Whether to include all metadata and embeddings in the output. If false, only content is returned.",
info="Whether to include all metadata in the output. If false, only content is returned.",
value=True,
advanced=False,
),
BoolInput(
name="include_embeddings",
display_name="Include Embeddings",
info="Whether to include embeddings in the output. Only applicable if 'Include Metadata' is enabled.",
value=False,
advanced=True,
),
]
outputs = [
Output(
name="chroma_kb_data",
name="retrieve_data",
display_name="Results",
method="get_chroma_kb_data",
method="retrieve_data",
info="Returns the data from the selected knowledge base.",
),
]
@ -162,7 +170,7 @@ class KBRetrievalComponent(Component):
msg = f"Embedding provider '{provider}' is not supported for retrieval."
raise NotImplementedError(msg)
async def get_chroma_kb_data(self) -> DataFrame:
async def retrieve_data(self) -> DataFrame:
"""Retrieve data from the selected knowledge base by reading the Chroma collection.
Returns:
@ -212,16 +220,16 @@ class KBRetrievalComponent(Component):
# For each result, make it a tuple to match the expected output format
results = [(doc, 0) for doc in results] # Assign a dummy score of 0
# If metadata is enabled, get embeddings for the results
# If include_embeddings is enabled, get embeddings for the results
id_to_embedding = {}
if self.include_metadata and results:
if self.include_embeddings and results:
doc_ids = [doc[0].metadata.get("_id") for doc in results if doc[0].metadata.get("_id")]
# Only proceed if we have valid document IDs
if doc_ids:
# Access underlying client to get embeddings
collection = chroma._client.get_collection(name=self.knowledge_base)
embeddings_result = collection.get(where={"_id": {"$in": doc_ids}}, include=["embeddings", "metadatas"])
embeddings_result = collection.get(where={"_id": {"$in": doc_ids}}, include=["metadatas", "embeddings"])
# Create a mapping from document ID to embedding
for i, metadata in enumerate(embeddings_result.get("metadatas", [])):
@ -231,20 +239,16 @@ class KBRetrievalComponent(Component):
# Build output data based on include_metadata setting
data_list = []
for doc in results:
kwargs = {
"content": doc[0].page_content,
}
if self.search_query:
kwargs["_score"] = -1 * doc[1]
if self.include_metadata:
# Include all metadata, embeddings, and content
kwargs = {
"content": doc[0].page_content,
**doc[0].metadata,
}
if self.search_query:
kwargs["_score"] = -1 * doc[1]
kwargs.update(doc[0].metadata)
if self.include_embeddings:
kwargs["_embeddings"] = id_to_embedding.get(doc[0].metadata.get("_id"))
else:
# Only include content
kwargs = {
"content": doc[0].page_content,
}
data_list.append(Data(**kwargs))

View file

@ -79,7 +79,16 @@ class DataFrameOperationsComponent(Component):
DropdownInput(
name="filter_operator",
display_name="Filter Operator",
options=["equals", "not equals", "contains", "starts with", "ends with", "greater than", "less than"],
options=[
"equals",
"not equals",
"contains",
"not contains",
"starts with",
"ends with",
"greater than",
"less than",
],
value="equals",
info="The operator to apply for filtering rows.",
advanced=False,
@ -254,6 +263,8 @@ class DataFrameOperationsComponent(Component):
mask = column != filter_value
elif operator == "contains":
mask = column.astype(str).str.contains(str(filter_value), na=False)
elif operator == "not contains":
mask = ~column.astype(str).str.contains(str(filter_value), na=False)
elif operator == "starts with":
mask = column.astype(str).str.startswith(str(filter_value), na=False)
elif operator == "ends with":

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
[project]
name = "langflow-base"
version = "0.5.0.post2"
version = "0.6.0"
description = "A Python package with a built-in web application"
requires-python = ">=3.10,<3.14"
license = "MIT"

View file

@ -1,5 +1,5 @@
import pytest
from langflow.base.data.kb_utils import compute_bm25, compute_tfidf
from langflow.base.knowledge_bases.knowledge_base_utils import compute_bm25, compute_tfidf
class TestKBUtils:

View file

@ -594,6 +594,11 @@ class TestMCPSseClientWithDeepWikiServer:
# Test valid URL
valid_url = "https://mcp.deepwiki.com/sse"
is_valid, error = await sse_client.validate_url(valid_url)
# Either valid or accessible, or rate-limited (429) which indicates server is reachable
if not is_valid and "429" in error:
# Rate limiting indicates the server is accessible but limiting requests
# This is a transient network issue, not a test failure
pytest.skip(f"DeepWiki server is rate limiting requests: {error}")
assert is_valid or error == "" # Either valid or accessible
# Test invalid URL

View file

@ -2,25 +2,25 @@ import json
import uuid
from unittest.mock import AsyncMock, MagicMock, patch
import pandas as pd
import pytest
from langflow.base.data.kb_utils import get_knowledge_bases
from langflow.components.data.kb_ingest import KBIngestionComponent
from langflow.base.knowledge_bases.knowledge_base_utils import get_knowledge_bases
from langflow.components.knowledge_bases.ingestion import KnowledgeIngestionComponent
from langflow.schema.data import Data
from langflow.schema.dataframe import DataFrame
from tests.base import ComponentTestBaseWithoutClient
class TestKBIngestionComponent(ComponentTestBaseWithoutClient):
class TestKnowledgeIngestionComponent(ComponentTestBaseWithoutClient):
@pytest.fixture
def component_class(self):
"""Return the component class to test."""
return KBIngestionComponent
return KnowledgeIngestionComponent
@pytest.fixture(autouse=True)
def mock_knowledge_base_path(self, tmp_path):
"""Mock the knowledge base root path directly."""
with patch("langflow.components.data.kb_ingest.KNOWLEDGE_BASES_ROOT_PATH", tmp_path):
with patch("langflow.components.knowledge_bases.ingestion.KNOWLEDGE_BASES_ROOT_PATH", tmp_path):
yield
class MockUser:
@ -39,14 +39,14 @@ class TestKBIngestionComponent(ComponentTestBaseWithoutClient):
def setup_mocks(self, mock_user_data):
"""Mock the component's user_id attribute and User object."""
with (
patch.object(KBIngestionComponent, "user_id", mock_user_data["user_id"]),
patch.object(KnowledgeIngestionComponent, "user_id", mock_user_data["user_id"]),
patch(
"langflow.components.data.kb_ingest.get_user_by_id",
"langflow.components.knowledge_bases.ingestion.get_user_by_id",
new_callable=AsyncMock,
return_value=mock_user_data["user_obj"],
),
patch(
"langflow.base.data.kb_utils.get_user_by_id",
"langflow.base.knowledge_bases.knowledge_base_utils.get_user_by_id",
new_callable=AsyncMock,
return_value=mock_user_data["user_obj"],
),
@ -62,7 +62,7 @@ class TestKBIngestionComponent(ComponentTestBaseWithoutClient):
def default_kwargs(self, tmp_path, mock_user_id):
"""Return default kwargs for component instantiation."""
# Create a sample DataFrame
data_df = pd.DataFrame(
data_df = DataFrame(
{"text": ["Sample text 1", "Sample text 2"], "title": ["Title 1", "Title 2"], "category": ["cat1", "cat2"]}
)
@ -209,8 +209,8 @@ class TestKBIngestionComponent(ComponentTestBaseWithoutClient):
with pytest.raises(NotImplementedError, match="Custom embedding models not yet supported"):
component._build_embeddings("custom-model", "test-key")
@patch("langflow.components.data.kb_ingest.get_settings_service")
@patch("langflow.components.data.kb_ingest.encrypt_api_key")
@patch("langflow.components.knowledge_bases.ingestion.get_settings_service")
@patch("langflow.components.knowledge_bases.ingestion.encrypt_api_key")
def test_build_embedding_metadata(self, mock_encrypt, mock_get_settings, component_class, default_kwargs):
"""Test building embedding metadata."""
component = component_class(**default_kwargs)
@ -250,7 +250,7 @@ class TestKBIngestionComponent(ComponentTestBaseWithoutClient):
config_list = default_kwargs["column_config"]
# Mock Chroma to avoid actual vector store operations
with patch("langflow.components.data.kb_ingest.Chroma") as mock_chroma:
with patch("langflow.components.knowledge_bases.ingestion.Chroma") as mock_chroma:
mock_chroma_instance = MagicMock()
mock_chroma_instance.get.return_value = {"metadatas": []}
mock_chroma.return_value = mock_chroma_instance
@ -275,7 +275,7 @@ class TestKBIngestionComponent(ComponentTestBaseWithoutClient):
config_list = default_kwargs["column_config"]
# Mock Chroma with existing hash
with patch("langflow.components.data.kb_ingest.Chroma") as mock_chroma:
with patch("langflow.components.knowledge_bases.ingestion.Chroma") as mock_chroma:
# Simulate existing document with same hash
existing_hash = "some_existing_hash"
mock_chroma_instance = MagicMock()
@ -283,7 +283,7 @@ class TestKBIngestionComponent(ComponentTestBaseWithoutClient):
mock_chroma.return_value = mock_chroma_instance
# Mock hashlib to return the existing hash for first row
with patch("langflow.components.data.kb_ingest.hashlib.sha256") as mock_hash:
with patch("langflow.components.knowledge_bases.ingestion.hashlib.sha256") as mock_hash:
mock_hash_obj = MagicMock()
mock_hash_obj.hexdigest.side_effect = [existing_hash, "different_hash"]
mock_hash.return_value = mock_hash_obj
@ -309,8 +309,8 @@ class TestKBIngestionComponent(ComponentTestBaseWithoutClient):
assert component.is_valid_collection_name("invalid_") is False # Ends with underscore
assert component.is_valid_collection_name("invalid@name") is False # Invalid character
@patch("langflow.components.data.kb_ingest.json.loads")
@patch("langflow.components.data.kb_ingest.decrypt_api_key")
@patch("langflow.components.knowledge_bases.ingestion.json.loads")
@patch("langflow.components.knowledge_bases.ingestion.decrypt_api_key")
async def test_build_kb_info_success(self, mock_decrypt, mock_json_loads, component_class, default_kwargs):
"""Test successful KB info building."""
component = component_class(**default_kwargs)

View file

@ -5,23 +5,23 @@ from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from langflow.base.data.kb_utils import get_knowledge_bases
from langflow.components.data.kb_retrieval import KBRetrievalComponent
from langflow.base.knowledge_bases.knowledge_base_utils import get_knowledge_bases
from langflow.components.knowledge_bases.retrieval import KnowledgeRetrievalComponent
from pydantic import SecretStr
from tests.base import ComponentTestBaseWithoutClient
class TestKBRetrievalComponent(ComponentTestBaseWithoutClient):
class TestKnowledgeRetrievalComponent(ComponentTestBaseWithoutClient):
@pytest.fixture
def component_class(self):
"""Return the component class to test."""
return KBRetrievalComponent
return KnowledgeRetrievalComponent
@pytest.fixture(autouse=True)
def mock_knowledge_base_path(self, tmp_path):
"""Mock the knowledge base root path directly."""
with patch("langflow.components.data.kb_retrieval.KNOWLEDGE_BASES_ROOT_PATH", tmp_path):
with patch("langflow.components.knowledge_bases.retrieval.KNOWLEDGE_BASES_ROOT_PATH", tmp_path):
yield
class MockUser:
@ -40,14 +40,14 @@ class TestKBRetrievalComponent(ComponentTestBaseWithoutClient):
def setup_mocks(self, mock_user_data):
"""Mock the component's user_id attribute and User object."""
with (
patch.object(KBRetrievalComponent, "user_id", mock_user_data["user_id"]),
patch.object(KnowledgeRetrievalComponent, "user_id", mock_user_data["user_id"]),
patch(
"langflow.components.data.kb_retrieval.get_user_by_id",
"langflow.components.knowledge_bases.retrieval.get_user_by_id",
new_callable=AsyncMock,
return_value=mock_user_data["user_obj"],
),
patch(
"langflow.base.data.kb_utils.get_user_by_id",
"langflow.base.knowledge_bases.knowledge_base_utils.get_user_by_id",
new_callable=AsyncMock,
return_value=mock_user_data["user_obj"],
),
@ -138,7 +138,7 @@ class TestKBRetrievalComponent(ComponentTestBaseWithoutClient):
component = component_class(**default_kwargs)
kb_path = Path(default_kwargs["kb_root_path"]) / mock_user_id["user"] / default_kwargs["knowledge_base"]
with patch("langflow.components.data.kb_retrieval.decrypt_api_key") as mock_decrypt:
with patch("langflow.components.knowledge_bases.retrieval.decrypt_api_key") as mock_decrypt:
mock_decrypt.return_value = "decrypted_key"
metadata = component._get_kb_metadata(kb_path)
@ -185,7 +185,7 @@ class TestKBRetrievalComponent(ComponentTestBaseWithoutClient):
}
(kb_path / "embedding_metadata.json").write_text(json.dumps(metadata))
with patch("langflow.components.data.kb_retrieval.decrypt_api_key") as mock_decrypt:
with patch("langflow.components.knowledge_bases.retrieval.decrypt_api_key") as mock_decrypt:
mock_decrypt.side_effect = ValueError("Decryption failed")
result = component._get_kb_metadata(kb_path)
@ -327,7 +327,7 @@ class TestKBRetrievalComponent(ComponentTestBaseWithoutClient):
chunk_size=1000,
)
async def test_get_chroma_kb_data_no_metadata(self, component_class, default_kwargs, tmp_path, mock_user_id):
async def test_retrieve_data_no_metadata(self, component_class, default_kwargs, tmp_path, mock_user_id):
"""Test retrieving data when metadata is missing."""
# Remove metadata file
kb_path = tmp_path / mock_user_id["user"] / default_kwargs["knowledge_base"]
@ -338,10 +338,10 @@ class TestKBRetrievalComponent(ComponentTestBaseWithoutClient):
component = component_class(**default_kwargs)
with pytest.raises(ValueError, match="Metadata not found for knowledge base"):
await component.get_chroma_kb_data()
await component.retrieve_data()
def test_get_chroma_kb_data_path_construction(self, component_class, default_kwargs):
"""Test that get_chroma_kb_data constructs the correct paths."""
def test_retrieve_data_path_construction(self, component_class, default_kwargs):
"""Test that retrieve_data constructs the correct paths."""
component = component_class(**default_kwargs)
# Test that the component correctly builds the KB path
@ -354,17 +354,17 @@ class TestKBRetrievalComponent(ComponentTestBaseWithoutClient):
assert expanded_path.exists() # tmp_path should exist
# Verify method exists with correct parameters
assert hasattr(component, "get_chroma_kb_data")
assert hasattr(component, "retrieve_data")
assert hasattr(component, "search_query")
assert hasattr(component, "top_k")
assert hasattr(component, "include_embeddings")
async def test_get_chroma_kb_data_method_exists(self, component_class, default_kwargs):
"""Test that get_chroma_kb_data method exists and can be called."""
async def test_retrieve_data_method_exists(self, component_class, default_kwargs):
"""Test that retrieve_data method exists and can be called."""
component = component_class(**default_kwargs)
# Just verify the method exists and has the right signature
assert hasattr(component, "get_chroma_kb_data"), "Component should have get_chroma_kb_data method"
assert hasattr(component, "retrieve_data"), "Component should have retrieve_data method"
# Mock all external calls to avoid integration issues
with (
@ -377,7 +377,7 @@ class TestKBRetrievalComponent(ComponentTestBaseWithoutClient):
# This is a unit test focused on the component's internal logic
with contextlib.suppress(Exception):
await component.get_chroma_kb_data()
await component.retrieve_data()
# Verify internal methods were called
mock_get_metadata.assert_called_once()

View file

@ -211,6 +211,11 @@ export const SIDEBAR_CATEGORIES = [
{ display_name: "Agents", name: "agents", icon: "Bot" },
{ display_name: "Models", name: "models", icon: "BrainCog" },
{ display_name: "Data", name: "data", icon: "Database" },
{
display_name: "Knowledge Bases",
name: "knowledge_bases",
icon: "Library",
},
{ display_name: "Vector Stores", name: "vectorstores", icon: "Layers" },
{ display_name: "Processing", name: "processing", icon: "ListFilter" },
{ display_name: "Logic", name: "logic", icon: "ArrowRightLeft" },

View file

@ -97,7 +97,9 @@ test(
const nodesFromServer = astraStarterProject?.data.nodes.length;
expect(
edges === edgesFromServer || edges === edgesFromServer - 1,
edges === edgesFromServer ||
edges === edgesFromServer - 1 ||
edges === edgesFromServer - 2,
).toBeTruthy();
expect(nodes).toBe(nodesFromServer);
},

4
uv.lock generated
View file

@ -4975,7 +4975,7 @@ wheels = [
[[package]]
name = "langflow"
version = "1.5.0.post2"
version = "1.6.0"
source = { editable = "." }
dependencies = [
{ name = "aiofile" },
@ -5360,7 +5360,7 @@ dev = [
[[package]]
name = "langflow-base"
version = "0.5.0.post2"
version = "0.6.0"
source = { editable = "src/backend/base" }
dependencies = [
{ name = "aiofile" },