-
Notifications
You must be signed in to change notification settings - Fork 8.2k
feat: Add support for Ingestion and Retrieval of Knowledge Bases #9088
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 27 commits
9be2d30
941bc81
4df3225
c32d451
75409c1
1c9a2aa
de3ade8
5ea7224
c22e59b
ccd0f79
b9f9e01
c00f486
4ada462
cabf676
350461e
b0b62a3
7dad9d6
6a0f187
8da44b2
0d25004
1247bed
66da30e
5951200
d9c9cb9
75189e8
81367fb
d7940af
db49a96
5503c78
845f0a7
ef94bcf
6d82934
79e3425
b43333f
109363c
49c0db0
d7e5c33
bd1d91f
d5d2a5e
63dd4c9
14b87c4
8daab25
8268740
c3d286b
388e98a
2c78dd0
2adcc77
c4bf9bf
3b88885
6b3a349
4116cae
810c717
c883ae1
24e7715
dd8855b
2c02cc0
0d36985
77bc57f
98766fc
9c7fb6a
63fb9b9
049e39f
8ec1341
8adcd12
0ca5a67
f251c73
1def7f6
9146f7e
06211a6
d3a7120
67d5ae5
bc10c6e
bad02f3
fe36a36
d139d5b
4cb23b7
6ece64b
69aed9a
bd4ae10
1469ecf
a654109
5d0916d
a8ea48e
4440e08
93b5149
16555cd
1e66ae2
1c4c209
86c8e55
4b7de6d
4f49445
0a43c94
72d88c0
2048c42
cf7d64d
36fac5a
9341c41
45f14f7
e6ab6cb
542984b
4864640
3aeb0c5
8ab4368
80e223e
9058976
03a8c2e
96ee3f4
896bf61
d3fc9e8
5718eb3
b33a3c9
602f39d
502436d
00da454
76f0035
c9fbbdd
5dcf0b8
14909d9
41ba6ec
3662d50
20d4382
de4edf7
1e7ffce
6e7b061
8277cb6
ed009cd
8700133
aaaae03
02d4874
ae0d378
43ef981
9c21594
71eaf96
6ce2414
86334cf
d3d176f
dfcfe7b
e072f0d
6645b25
3efe3be
2dc9c55
8fa29e5
aacf468
f61689a
6416d51
b780edd
d20c2c6
8c40cf7
5536a3d
2a4dba8
de843c8
fb45847
c053983
3f24571
62a1023
e80a68e
663b819
414a7b9
6498a83
60c6da5
4516cca
9121c1d
9a9717a
1871c1d
d8f3d0f
7565e95
b62a7eb
706040f
4072499
4ace8d8
baeb113
11d7b17
dda21d7
fd1b2ae
b6b60fa
600e0e9
933233a
d88b479
fb5294c
ef664d8
9c90eeb
a37c8a8
0600f8c
f831d9b
71ef5f5
58044d0
4d49c95
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,259 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from http import HTTPStatus | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import pandas as pd | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from fastapi import APIRouter, HTTPException | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pydantic import BaseModel | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| router = APIRouter(tags=["Knowledge Bases"], prefix="/knowledge_bases") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| KNOWLEDGE_BASES_DIR = "~/.langflow/knowledge_bases" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class KnowledgeBaseInfo(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| embedding_provider: str | None = "Unknown" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| size: int = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| words: int = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| characters: int = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| chunks: int = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| avg_chunk_size: float = 0.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_kb_root_path() -> Path: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Get the knowledge bases root path.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Path(KNOWLEDGE_BASES_DIR).expanduser() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_directory_size(path: Path) -> int: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Calculate the total size of all files in a directory.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total_size = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for file_path in path.rglob("*"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if file_path.is_file(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total_size += file_path.stat().st_size | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except (OSError, PermissionError): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return total_size | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def detect_embedding_provider(kb_path: Path) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Detect the embedding provider from config files and directory structure.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Provider patterns to check for | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| provider_patterns = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "OpenAI": ["openai", "text-embedding-ada", "text-embedding-3"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "HuggingFace": ["sentence-transformers", "huggingface", "bert-"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Cohere": ["cohere", "embed-english", "embed-multilingual"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Google": ["palm", "gecko", "google"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Chroma": ["chroma"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Check JSON config files for provider information | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for config_file in kb_path.glob("*.json"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with config_file.open("r", encoding="utf-8") as f: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config_data = json.load(f) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance(config_data, dict): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config_str = json.dumps(config_data).lower() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Check for explicit provider fields first | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| provider_fields = ["embedding_provider", "provider", "embedding_model_provider"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for field in provider_fields: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if field in config_data: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| provider_value = str(config_data[field]).lower() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for provider, patterns in provider_patterns.items(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if any(pattern in provider_value for pattern in patterns): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return provider | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Check for model name patterns | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for provider, patterns in provider_patterns.items(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if any(pattern in config_str for pattern in patterns): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return provider | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Check failure on line 78 in src/backend/base/langflow/api/v1/knowledge_bases.py
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Fallback to directory structure | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (kb_path / "chroma").exists(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "Chroma" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (kb_path / "vectors.npy").exists(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "Local" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "Unknown" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_text_columns(df: pd.DataFrame, schema_data: list = None) -> list[str]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Get the text columns to analyze for word/character counts.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # First try schema-defined text columns | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if schema_data: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text_columns = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| col["column_name"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for col in schema_data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if col.get("vectorize", False) and col.get("data_type") == "string" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if text_columns: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [col for col in text_columns if col in df.columns] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Fallback to common text column names | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| common_names = ["text", "content", "document", "chunk"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text_columns = [col for col in df.columns if col.lower() in common_names] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if text_columns: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return text_columns | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Last resort: all string columns | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [col for col in df.columns if df[col].dtype == "object"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+169
to
+184
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⚡️Codeflash found 907% (9.07x) speedup for
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Test | Status |
|---|---|
| ⚙️ Existing Unit Tests | 🔘 None Found |
| 🌀 Generated Regression Tests | ✅ 48 Passed |
| ⏪ Replay Tests | 🔘 None Found |
| 🔎 Concolic Coverage Tests | 🔘 None Found |
| 📊 Tests Coverage | 100.0% |
🌀 Generated Regression Tests and Runtime
import pandas as pd
# imports
import pytest # used for our unit tests
from langflow.api.v1.knowledge_bases import get_text_columns
# unit tests
# ----------------------------
# BASIC TEST CASES
# ----------------------------
def test_schema_data_priority():
# Should use schema_data to select columns
df = pd.DataFrame({
"text": ["a", "b"],
"foo": ["x", "y"],
"bar": [1, 2]
})
schema = [
{"column_name": "foo", "vectorize": True, "data_type": "string"},
{"column_name": "bar", "vectorize": True, "data_type": "int"},
{"column_name": "text", "vectorize": False, "data_type": "string"}
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
def test_schema_data_multiple_columns():
# Multiple schema-defined text columns
df = pd.DataFrame({
"a": ["x", "y"],
"b": ["u", "v"],
"c": [1, 2]
})
schema = [
{"column_name": "a", "vectorize": True, "data_type": "string"},
{"column_name": "b", "vectorize": True, "data_type": "string"},
{"column_name": "c", "vectorize": True, "data_type": "int"}
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
def test_schema_data_column_not_in_df():
# Schema defines a column not in df; should ignore it
df = pd.DataFrame({
"foo": ["x", "y"],
})
schema = [
{"column_name": "foo", "vectorize": True, "data_type": "string"},
{"column_name": "missing", "vectorize": True, "data_type": "string"}
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
def test_fallback_common_names():
# No schema_data, should fallback to common names
df = pd.DataFrame({
"text": ["a", "b"],
"other": [1, 2]
})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_fallback_common_names_case_insensitive():
# Should match common names case-insensitively
df = pd.DataFrame({
"Text": ["a", "b"],
"Content": ["c", "d"],
"foo": [1, 2]
})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_fallback_all_string_columns():
# No schema, no common names, should fallback to all string columns
df = pd.DataFrame({
"foo": ["a", "b"],
"bar": [1, 2],
"baz": ["x", "y"]
})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_no_string_columns():
# No string columns at all
df = pd.DataFrame({
"a": [1, 2],
"b": [3.0, 4.0]
})
codeflash_output = get_text_columns(df); result = codeflash_output
# ----------------------------
# EDGE TEST CASES
# ----------------------------
def test_empty_dataframe():
# Empty DataFrame
df = pd.DataFrame()
codeflash_output = get_text_columns(df); result = codeflash_output
def test_empty_schema_data():
# Empty schema_data should fallback to others
df = pd.DataFrame({
"text": ["a", "b"],
"foo": ["x", "y"]
})
codeflash_output = get_text_columns(df, []); result = codeflash_output
def test_schema_data_all_missing_in_df():
# All schema-defined columns missing in df
df = pd.DataFrame({
"foo": ["a", "b"]
})
schema = [
{"column_name": "bar", "vectorize": True, "data_type": "string"},
{"column_name": "baz", "vectorize": True, "data_type": "string"}
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
def test_schema_vectorize_false():
# schema_data with vectorize=False should not be returned
df = pd.DataFrame({
"foo": ["a", "b"],
"bar": ["x", "y"]
})
schema = [
{"column_name": "foo", "vectorize": False, "data_type": "string"},
{"column_name": "bar", "vectorize": True, "data_type": "string"}
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
def test_schema_data_type_not_string():
# schema_data with non-string data_type should not be returned
df = pd.DataFrame({
"foo": ["a", "b"],
"bar": [1, 2]
})
schema = [
{"column_name": "foo", "vectorize": True, "data_type": "int"},
{"column_name": "bar", "vectorize": True, "data_type": "int"}
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
def test_mixed_dtype_columns():
# DataFrame with mixed dtypes, only object columns returned as fallback
df = pd.DataFrame({
"foo": ["a", "b"],
"bar": [1, 2],
"baz": [True, False],
"qux": [b"abc", b"def"]
})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_column_names_overlap():
# DataFrame with columns that are substrings of common names
df = pd.DataFrame({
"textual": ["a", "b"],
"contented": ["c", "d"],
"document": ["doc1", "doc2"]
})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_schema_with_extra_keys():
# schema_data with extra irrelevant keys
df = pd.DataFrame({
"foo": ["a", "b"],
"bar": ["x", "y"]
})
schema = [
{"column_name": "foo", "vectorize": True, "data_type": "string", "irrelevant": 123},
{"column_name": "bar", "vectorize": True, "data_type": "string", "something": "else"}
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
def test_schema_with_missing_keys():
# schema_data entries missing some keys; should handle gracefully
df = pd.DataFrame({
"foo": ["a", "b"],
"bar": ["x", "y"]
})
schema = [
{"column_name": "foo"}, # missing vectorize and data_type
{"column_name": "bar", "vectorize": True} # missing data_type
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
def test_column_names_with_spaces():
# Column names with spaces or special characters
df = pd.DataFrame({
"Text": ["a", "b"],
"text content": ["c", "d"],
"document": ["doc1", "doc2"]
})
codeflash_output = get_text_columns(df); result = codeflash_output
# ----------------------------
# LARGE SCALE TEST CASES
# ----------------------------
def test_large_number_of_columns_schema():
# Large DataFrame, many columns, schema selects a subset
columns = [f"col_{i}" for i in range(1000)]
df = pd.DataFrame({col: ["x", "y"] for col in columns})
# Mark every 100th column as vectorize=True, data_type=string
schema = [
{"column_name": f"col_{i}", "vectorize": True, "data_type": "string"}
for i in range(0, 1000, 100)
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
expected = [f"col_{i}" for i in range(0, 1000, 100)]
def test_large_number_of_common_names():
# DataFrame with many columns, some are common names
columns = [f"col_{i}" for i in range(995)] + ["text", "content", "document", "chunk", "foo"]
df = pd.DataFrame({col: ["a", "b"] for col in columns})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_large_number_of_string_columns():
# DataFrame with all columns as string type, no schema, no common names
columns = [f"col_{i}" for i in range(1000)]
df = pd.DataFrame({col: ["a", "b"] for col in columns})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_large_number_of_non_string_columns():
# DataFrame with all columns as int type, no string columns
columns = [f"col_{i}" for i in range(1000)]
df = pd.DataFrame({col: [i, i+1] for i, col in enumerate(columns)})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_large_schema_all_missing():
# Large schema, but none of the columns exist in df
df = pd.DataFrame({
"foo": ["a", "b"],
"bar": ["x", "y"]
})
schema = [
{"column_name": f"missing_{i}", "vectorize": True, "data_type": "string"}
for i in range(1000)
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
import pandas as pd
# imports
import pytest # used for our unit tests
from langflow.api.v1.knowledge_bases import get_text_columns
# unit tests
# -------------------- BASIC TEST CASES --------------------
def test_schema_data_priority_over_common_names():
# Schema data should take precedence over common names
df = pd.DataFrame({
"text": ["a", "b"],
"custom": ["c", "d"]
})
schema = [
{"column_name": "custom", "vectorize": True, "data_type": "string"},
{"column_name": "text", "vectorize": False, "data_type": "string"},
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
def test_schema_data_multiple_columns():
# Multiple schema columns, only those in df and matching criteria
df = pd.DataFrame({
"col1": ["a", "b"],
"col2": ["c", "d"],
"col3": [1, 2]
})
schema = [
{"column_name": "col1", "vectorize": True, "data_type": "string"},
{"column_name": "col2", "vectorize": True, "data_type": "string"},
{"column_name": "col3", "vectorize": True, "data_type": "int"},
{"column_name": "col4", "vectorize": True, "data_type": "string"}, # not in df
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
def test_schema_data_no_matching_columns():
# Schema data has no matching columns in df
df = pd.DataFrame({
"foo": ["a", "b"],
"bar": ["c", "d"]
})
schema = [
{"column_name": "baz", "vectorize": True, "data_type": "string"}
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
def test_common_names_fallback():
# No schema, but common names present
df = pd.DataFrame({
"text": ["a", "b"],
"other": [1, 2]
})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_common_names_case_insensitive():
# Common names with different casing
df = pd.DataFrame({
"Text": ["a", "b"],
"Content": ["c", "d"]
})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_all_string_columns_fallback():
# No schema, no common names, fallback to all string columns
df = pd.DataFrame({
"foo": ["a", "b"],
"bar": ["c", "d"],
"baz": [1, 2]
})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_no_string_columns():
# No string columns at all
df = pd.DataFrame({
"a": [1, 2],
"b": [3, 4]
})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_empty_dataframe():
# Empty DataFrame, no columns
df = pd.DataFrame()
codeflash_output = get_text_columns(df); result = codeflash_output
def test_schema_data_false_vectorize():
# Schema data with vectorize False should not be included
df = pd.DataFrame({
"foo": ["a", "b"],
"bar": ["c", "d"]
})
schema = [
{"column_name": "foo", "vectorize": False, "data_type": "string"},
{"column_name": "bar", "data_type": "string"}, # vectorize missing
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
def test_schema_data_non_string_type():
# Schema data with non-string type should not be included
df = pd.DataFrame({
"foo": ["a", "b"],
"bar": ["c", "d"]
})
schema = [
{"column_name": "foo", "vectorize": True, "data_type": "int"},
{"column_name": "bar", "vectorize": True, "data_type": "float"},
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
# -------------------- EDGE TEST CASES --------------------
def test_schema_data_column_not_in_df():
# Schema column not present in df
df = pd.DataFrame({
"foo": ["a", "b"]
})
schema = [
{"column_name": "bar", "vectorize": True, "data_type": "string"}
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
def test_common_names_subset():
# Only some common names present
df = pd.DataFrame({
"text": ["a", "b"],
"foo": ["c", "d"],
"document": ["e", "f"]
})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_column_names_overlap_with_common_names():
# Column name is a substring of a common name, but not equal
df = pd.DataFrame({
"tex": ["a", "b"],
"contented": ["c", "d"],
"chunk": ["e", "f"]
})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_schema_data_and_common_names_overlap():
# Schema picks a column that is also a common name
df = pd.DataFrame({
"text": ["a", "b"],
"foo": ["c", "d"]
})
schema = [
{"column_name": "text", "vectorize": True, "data_type": "string"}
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
def test_schema_data_empty_list():
# Schema data is an empty list, should fallback to common names
df = pd.DataFrame({
"text": ["a", "b"],
"foo": ["c", "d"]
})
codeflash_output = get_text_columns(df, schema_data=[]); result = codeflash_output
def test_schema_data_none():
# Schema data is None, should fallback to common names
df = pd.DataFrame({
"content": ["a", "b"],
"foo": ["c", "d"]
})
codeflash_output = get_text_columns(df, schema_data=None); result = codeflash_output
def test_schema_data_missing_keys():
# Schema data missing "vectorize" or "data_type" keys
df = pd.DataFrame({
"foo": ["a", "b"],
"bar": ["c", "d"]
})
schema = [
{"column_name": "foo"}, # missing both keys
{"column_name": "bar", "vectorize": True}, # missing data_type
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
def test_mixed_dtype_columns():
# DataFrame with mixed dtypes, only string columns should be returned
df = pd.DataFrame({
"foo": ["a", "b"],
"bar": [1, 2],
"baz": ["c", "d"],
"qux": [True, False]
})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_column_with_nan_values():
# String column with NaN values should still be considered string
df = pd.DataFrame({
"foo": ["a", None, "b"],
"bar": [1, 2, 3]
})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_column_with_all_nan_values():
# All NaN column with dtype object should be considered string
df = pd.DataFrame({
"foo": [None, None, None],
"bar": [1, 2, 3]
})
codeflash_output = get_text_columns(df); result = codeflash_output
def test_column_with_mixed_object_types():
# Object dtype, but not all string
df = pd.DataFrame({
"foo": ["a", 1, None],
"bar": [1, 2, 3]
})
# Pandas will infer object dtype for "foo"
codeflash_output = get_text_columns(df); result = codeflash_output
# -------------------- LARGE SCALE TEST CASES --------------------
def test_large_number_of_columns_schema():
# DataFrame with 1000 columns, schema defines 10 vectorized string columns
columns = [f"col{i}" for i in range(1000)]
data = {col: ["x", "y"] for col in columns}
df = pd.DataFrame(data)
schema = [
{"column_name": f"col{i}", "vectorize": True, "data_type": "string"}
for i in range(0, 1000, 100)
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
expected = [f"col{i}" for i in range(0, 1000, 100)]
def test_large_number_of_rows_and_string_columns():
# DataFrame with 500 rows and 10 string columns
data = {f"str_col{i}": ["word"] * 500 for i in range(10)}
data.update({f"int_col{i}": [i] * 500 for i in range(5)})
df = pd.DataFrame(data)
codeflash_output = get_text_columns(df); result = codeflash_output
expected = [f"str_col{i}" for i in range(10)]
def test_large_number_of_common_names():
# DataFrame with many columns, some of which are common names
columns = [f"foo{i}" for i in range(995)] + ["text", "content", "chunk", "document", "foo999"]
data = {col: ["x", "y"] for col in columns}
df = pd.DataFrame(data)
codeflash_output = get_text_columns(df); result = codeflash_output
expected = ["text", "content", "chunk", "document"]
def test_large_schema_no_matches():
# Large schema, but none of the columns exist in df
df = pd.DataFrame({
"a": ["x", "y"],
"b": ["z", "w"]
})
schema = [
{"column_name": f"col{i}", "vectorize": True, "data_type": "string"}
for i in range(100)
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
def test_large_schema_partial_matches():
# Large schema, some columns exist in df
columns = [f"col{i}" for i in range(100)]
df = pd.DataFrame({col: ["a", "b"] for col in columns})
schema = [
{"column_name": f"col{i}", "vectorize": True, "data_type": "string"}
for i in range(0, 100, 10)
]
codeflash_output = get_text_columns(df, schema); result = codeflash_output
expected = [f"col{i}" for i in range(0, 100, 10)]
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.To test or edit this optimization locally git merge codeflash/optimize-pr9088-2025-07-17T18.40.39
Click to see suggested changes
| text_columns = [ | |
| col["column_name"] | |
| for col in schema_data | |
| if col.get("vectorize", False) and col.get("data_type") == "string" | |
| ] | |
| if text_columns: | |
| return [col for col in text_columns if col in df.columns] | |
| # Fallback to common text column names | |
| common_names = ["text", "content", "document", "chunk"] | |
| text_columns = [col for col in df.columns if col.lower() in common_names] | |
| if text_columns: | |
| return text_columns | |
| # Last resort: all string columns | |
| return [col for col in df.columns if df[col].dtype == "object"] | |
| # Collect the schema-defined text columns | |
| text_columns = [ | |
| col["column_name"] | |
| for col in schema_data | |
| if col.get("vectorize", False) and col.get("data_type") == "string" | |
| ] | |
| if text_columns: | |
| df_cols_set = set(df.columns) | |
| # Filter only columns present in the dataframe | |
| return [col for col in text_columns if col in df_cols_set] | |
| # Fallback to common text column names (case-insensitive, set for O(1) lookup) | |
| common_names_set = {"text", "content", "document", "chunk"} | |
| # Build a list of columns whose lowercased names match any in the common_names_set | |
| text_columns = [col for col in df.columns if col.lower() in common_names_set] | |
| if text_columns: | |
| return text_columns | |
| # Last resort: all string columns (optimized using select_dtypes) | |
| return list(df.select_dtypes(include=["object"]).columns) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import math | ||
| from collections import Counter | ||
|
|
||
|
|
||
| def compute_tfidf(documents: list[str], query_terms: list[str]) -> list[float]: | ||
| """Compute TF-IDF scores for query terms across a collection of documents. | ||
|
|
||
| Args: | ||
| documents: List of document strings | ||
| query_terms: List of query terms to score | ||
|
|
||
| Returns: | ||
| List of TF-IDF scores for each document | ||
| """ | ||
| # Tokenize documents (simple whitespace splitting) | ||
| tokenized_docs = [doc.lower().split() for doc in documents] | ||
| n_docs = len(documents) | ||
|
|
||
| # Calculate document frequency for each term | ||
| document_frequencies = {} | ||
| for term in query_terms: | ||
| document_frequencies[term] = sum(1 for doc in tokenized_docs if term.lower() in doc) | ||
|
|
||
| scores = [] | ||
|
|
||
| for doc_tokens in tokenized_docs: | ||
| doc_score = 0.0 | ||
| doc_length = len(doc_tokens) | ||
| term_counts = Counter(doc_tokens) | ||
|
|
||
| for term in query_terms: | ||
| term_lower = term.lower() | ||
|
|
||
| # Term frequency (TF) | ||
| tf = term_counts[term_lower] / doc_length if doc_length > 0 else 0 | ||
|
|
||
| # Inverse document frequency (IDF) | ||
| idf = math.log(n_docs / document_frequencies[term]) if document_frequencies[term] > 0 else 0 | ||
|
|
||
| # TF-IDF score | ||
| doc_score += tf * idf | ||
|
|
||
erichare marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| scores.append(doc_score) | ||
|
|
||
| return scores | ||
|
|
||
|
|
||
| def compute_bm25(documents: list[str], query_terms: list[str], k1: float = 1.2, b: float = 0.75) -> list[float]: | ||
| """Compute BM25 scores for query terms across a collection of documents. | ||
|
|
||
| Args: | ||
| documents: List of document strings | ||
| query_terms: List of query terms to score | ||
| k1: Controls term frequency scaling (default: 1.2) | ||
| b: Controls document length normalization (default: 0.75) | ||
|
|
||
| Returns: | ||
| List of BM25 scores for each document | ||
| """ | ||
| # Tokenize documents | ||
| tokenized_docs = [doc.lower().split() for doc in documents] | ||
| n_docs = len(documents) | ||
|
|
||
| # Calculate average document length | ||
| avg_doc_length = sum(len(doc) for doc in tokenized_docs) / n_docs if n_docs > 0 else 0 | ||
|
|
||
| # Handle edge case where all documents are empty | ||
| if avg_doc_length == 0: | ||
| return [0.0] * n_docs | ||
|
|
||
| # Calculate document frequency for each term | ||
| document_frequencies = {} | ||
| for term in query_terms: | ||
| document_frequencies[term] = sum(1 for doc in tokenized_docs if term.lower() in doc) | ||
|
|
||
| scores = [] | ||
|
|
||
| for doc_tokens in tokenized_docs: | ||
| doc_score = 0.0 | ||
| doc_length = len(doc_tokens) | ||
| term_counts = Counter(doc_tokens) | ||
|
|
||
| for term in query_terms: | ||
| term_lower = term.lower() | ||
|
|
||
| # Term frequency in document | ||
| tf = term_counts[term_lower] | ||
|
|
||
| # Inverse document frequency (IDF) | ||
| idf = ( | ||
| math.log((n_docs - document_frequencies[term] + 0.5) / (document_frequencies[term] + 0.5)) | ||
| if document_frequencies[term] > 0 | ||
| else 0 | ||
| ) | ||
|
|
||
| # BM25 score calculation | ||
| numerator = tf * (k1 + 1) | ||
| denominator = tf + k1 * (1 - b + b * (doc_length / avg_doc_length)) | ||
|
|
||
| doc_score += idf * (numerator / denominator) | ||
|
|
||
erichare marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| scores.append(doc_score) | ||
|
|
||
| return scores | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add provider and embedding model info also.