Skip to content

Commit 6a94df7

Browse files
lucaseduoliautofix-ci[bot]github-advanced-security[bot]coderabbitai[bot]CodeRabbit
authored
feat: disable langflow ingestion via UI (#1623)
* Added disable langflow ingestion setting on backend * Added option to disable langflow ingestion in frontend * Fixed ingestion not going to tasks * style: ruff format (auto) * fix frontend lint errors * fixed type errors * Potential fix for pull request finding 'CodeQL / Information exposure through an exception' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fixed mypy findings * fixed lint * fix: apply CodeRabbit auto-fixes Fixed 4 file(s) based on 6 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai> * fix: apply CodeRabbit auto-fixes Fixed 2 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai> * style: ruff format (auto) * fix mypi * added acl support * style: ruff format (auto) * fix mypy * fixed integration tests * pass temp file paths as empty to not delete default files on error * wait for opensearch * fix session manager jwt * fix lint * fix spaces * fix health check * fix kwargs * style: ruff format (auto) * fixed nitpicks from coderabbit * style: ruff format (auto) * fix ruff * fix result * fix types * Match connector_file_id in ACL/metadata updates (#1704) Ensure ACL and metadata updates target both Langflow and non-Langflow chunks by matching document IDs against multiple fields. Introduce _build_id_query and id_fields parameters to should_update_acl, update_document_acl, and batch_update_acls so updates can match document_id and connector_file_id. Stop overwriting chunk owner during ACL syncs (only allowed_users/allowed_groups are updated) and set owner at ingest to the syncing user in TaskProcessor. Adjust tests to expect the combined bool query and make minor typing/import/formatting cleanups. * mypy fix --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <noreply@coderabbit.ai> Co-authored-by: Edwin Jose <edwin.jose@datastax.com> Co-authored-by: Edwin Jose <edwinjose900@gmail.com>
1 parent 7fbdb56 commit 6a94df7

28 files changed

Lines changed: 1033 additions & 258 deletions

frontend/app/api/mutations/useUpdateSettingsMutation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface UpdateSettingsRequest {
1818
table_structure?: boolean;
1919
ocr?: boolean;
2020
picture_descriptions?: boolean;
21+
disable_ingest_with_langflow?: boolean;
2122
embedding_model?: string;
2223
embedding_provider?: string;
2324

frontend/app/api/queries/useGetConversationsQuery.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ export interface ConversationHistoryResponse {
4848
export const useGetConversationsQuery = (
4949
endpoint: EndpointType,
5050
refreshTrigger?: number,
51-
options?: Omit<UseQueryOptions, "queryKey" | "queryFn">,
51+
options?: Omit<
52+
UseQueryOptions<ChatConversation[], Error, ChatConversation[]>,
53+
"queryKey" | "queryFn"
54+
>,
5255
) => {
5356
const queryClient = useQueryClient();
5457
const { isOnboardingComplete } = useChat();

frontend/app/api/queries/useGetNudgesQuery.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ export interface NudgeQueryParams {
2525

2626
export const useGetNudgesQuery = (
2727
params: NudgeQueryParams | null = {},
28-
options?: Omit<UseQueryOptions, "queryKey" | "queryFn">,
28+
options?: Omit<
29+
UseQueryOptions<Nudge[], Error, Nudge[]>,
30+
"queryKey" | "queryFn"
31+
>,
2932
) => {
3033
const { chatId, filters, limit, scoreThreshold } = params ?? {};
3134
const queryClient = useQueryClient();

frontend/app/api/queries/useGetSettingsQuery.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface KnowledgeSettings {
1818
table_structure?: boolean;
1919
ocr?: boolean;
2020
picture_descriptions?: boolean;
21+
disable_ingest_with_langflow?: boolean;
2122
}
2223

2324
export interface ProviderSettings {

frontend/app/settings/_components/ingest-settings-section.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export function IngestSettingsSection() {
4646
const [ocr, setOcr] = useState<boolean>(false);
4747
const [pictureDescriptions, setPictureDescriptions] =
4848
useState<boolean>(false);
49+
const [disableIngestWithLangflow, setDisableIngestWithLangflow] =
50+
useState<boolean>(false);
4951

5052
const { data: settings = {} } = useGetSettingsQuery({
5153
enabled: isAuthenticated || isNoAuthMode,
@@ -146,13 +148,22 @@ export function IngestSettingsSection() {
146148
setPictureDescriptions(settings.knowledge.picture_descriptions);
147149
}, [settings.knowledge?.picture_descriptions]);
148150

151+
useEffect(() => {
152+
if (settings.knowledge?.disable_ingest_with_langflow !== undefined)
153+
setDisableIngestWithLangflow(
154+
settings.knowledge.disable_ingest_with_langflow,
155+
);
156+
}, [settings.knowledge?.disable_ingest_with_langflow]);
157+
149158
const k = settings.knowledge;
150159
const knowledgeIngestDirty =
151160
chunkSize !== (k?.chunk_size ?? chunkSize) ||
152161
chunkOverlap !== (k?.chunk_overlap ?? chunkOverlap) ||
153162
tableStructure !== (k?.table_structure ?? tableStructure) ||
154163
ocr !== (k?.ocr ?? ocr) ||
155-
pictureDescriptions !== (k?.picture_descriptions ?? pictureDescriptions);
164+
pictureDescriptions !== (k?.picture_descriptions ?? pictureDescriptions) ||
165+
disableIngestWithLangflow !==
166+
(k?.disable_ingest_with_langflow ?? disableIngestWithLangflow);
156167

157168
const handleEmbeddingModelChange = (newModel: string, provider?: string) => {
158169
if (newModel && provider) {
@@ -195,6 +206,7 @@ export function IngestSettingsSection() {
195206
table_structure: tableStructure,
196207
ocr,
197208
picture_descriptions: pictureDescriptions,
209+
disable_ingest_with_langflow: disableIngestWithLangflow,
198210
},
199211
{ onSuccess: () => setChunkValidationError(null) },
200212
);
@@ -238,6 +250,7 @@ export function IngestSettingsSection() {
238250
setTableStructure(DEFAULT_KNOWLEDGE_SETTINGS.table_structure);
239251
setOcr(DEFAULT_KNOWLEDGE_SETTINGS.ocr);
240252
setPictureDescriptions(DEFAULT_KNOWLEDGE_SETTINGS.picture_descriptions);
253+
setDisableIngestWithLangflow(false);
241254
setChunkValidationError(null);
242255
closeDialog();
243256
})
@@ -429,6 +442,25 @@ export function IngestSettingsSection() {
429442
</div>
430443
</div>
431444
<div>
445+
<div className="flex items-center justify-between py-3 border-b border-border">
446+
<div className="flex-1">
447+
<Label
448+
htmlFor="disable-ingest-with-langflow"
449+
className="text-base font-medium cursor-pointer pb-3"
450+
>
451+
Disable Langflow Ingestion
452+
</Label>
453+
<div className="text-sm text-muted-foreground">
454+
Bypass Langflow for document ingestion and use traditional
455+
processing.
456+
</div>
457+
</div>
458+
<Switch
459+
id="disable-ingest-with-langflow"
460+
checked={disableIngestWithLangflow}
461+
onCheckedChange={setDisableIngestWithLangflow}
462+
/>
463+
</div>
432464
<div className="flex items-center justify-between py-3 border-b border-border">
433465
<div className="flex-1">
434466
<Label

scripts/setup-e2e.sh

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ done
8484

8585
echo "Waiting for Backend (via proxy)..."
8686
ELAPSED=0
87-
until curl -s http://localhost:8000/health >/dev/null; do
87+
until [ "$(curl -s http://localhost:8000/search/health -o /dev/null -w "%{http_code}")" -eq 200 ]; do
8888
sleep 5
8989
ELAPSED=$((ELAPSED + 5))
9090
if [ $ELAPSED -ge $TIMEOUT ]; then
@@ -95,4 +95,16 @@ until curl -s http://localhost:8000/health >/dev/null; do
9595
echo "Waiting for Backend... (${ELAPSED}s/${TIMEOUT}s)"
9696
done
9797

98+
echo "Waiting for OpenSearch security configuration to be applied..."
99+
ELAPSED=0
100+
until ${CONTAINER_RUNTIME} logs os 2>&1 | grep -q "Security configuration applied successfully" || [ $ELAPSED -ge 60 ]; do
101+
sleep 2
102+
ELAPSED=$((ELAPSED + 2))
103+
done
104+
if [ $ELAPSED -ge 60 ]; then
105+
echo "WARNING: OpenSearch security configuration wait timed out (60s)"
106+
else
107+
echo "OpenSearch security configuration applied successfully"
108+
fi
109+
98110
echo "Infrastructure Ready!"

src/api/health.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ async def health_check(request: Request):
1717
return JSONResponse({"status": "ok"}, status_code=200)
1818

1919

20-
async def opensearch_health_ready(request):
20+
async def opensearch_health_ready(request: Request):
2121
"""Readiness probe: verifies OpenSearch dependency is reachable."""
2222
from config.settings import IBM_AUTH_ENABLED, OPENSEARCH_URL
2323

src/api/router.py

Lines changed: 105 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
"""Router endpoints that automatically route based on configuration settings."""
22

33
import json
4-
import os
54
import tempfile
65

76
from fastapi import Depends, File, Form, UploadFile
87
from fastapi.responses import JSONResponse
98

10-
from config.settings import DISABLE_INGEST_WITH_LANGFLOW
9+
from config.settings import get_openrag_config
1110
from dependencies import (
1211
get_current_user,
1312
get_document_service,
@@ -40,21 +39,22 @@ async def upload_ingest_router(
4039
- If DISABLE_INGEST_WITH_LANGFLOW is True: uses traditional OpenRAG upload
4140
- If DISABLE_INGEST_WITH_LANGFLOW is False (default): uses Langflow upload-ingest via task service
4241
"""
42+
disable_ingest_with_langflow = get_openrag_config().knowledge.disable_ingest_with_langflow
4343
logger.debug(
4444
"Router upload_ingest endpoint called",
45-
disable_langflow_ingest=DISABLE_INGEST_WITH_LANGFLOW,
45+
disable_langflow_ingest=disable_ingest_with_langflow,
4646
)
4747

48-
if DISABLE_INGEST_WITH_LANGFLOW:
49-
logger.debug("Routing to traditional OpenRAG upload")
50-
# Route to traditional upload — just take the first file
51-
from api.upload import upload as traditional_upload_fn
52-
53-
return await traditional_upload_fn(
54-
file=file[0] if file else None,
55-
document_service=document_service,
48+
if disable_ingest_with_langflow:
49+
logger.debug("Routing to traditional OpenRAG upload via task service")
50+
return await _traditional_upload_ingest_task(
51+
upload_files=file,
52+
replace_duplicates=replace_duplicates.lower() == "true",
53+
create_filter=create_filter.lower() == "true",
5654
session_manager=session_manager,
55+
task_service=task_service,
5756
user=user,
57+
settings_json=settings_json,
5858
)
5959

6060
logger.debug("Routing to Langflow upload-ingest pipeline via task service")
@@ -72,6 +72,96 @@ async def upload_ingest_router(
7272
)
7373

7474

75+
async def _traditional_upload_ingest_task(
76+
upload_files: list[UploadFile],
77+
replace_duplicates: bool,
78+
create_filter: bool,
79+
session_manager,
80+
task_service,
81+
user: User,
82+
settings_json: str | None = None,
83+
):
84+
"""Task-based traditional upload and ingest for single/multiple files"""
85+
try:
86+
if not upload_files:
87+
return JSONResponse({"error": "Missing files"}, status_code=400)
88+
89+
settings = None
90+
if settings_json:
91+
try:
92+
settings = json.loads(settings_json)
93+
except json.JSONDecodeError as e:
94+
return JSONResponse({"error": f"Invalid settings JSON: {e}"}, status_code=400)
95+
96+
user_id = user.user_id
97+
user_name = user.name
98+
user_email = user.email
99+
jwt_token = user.jwt_token
100+
101+
temp_file_paths = []
102+
original_filenames = []
103+
104+
try:
105+
for upload_file in upload_files:
106+
content = await upload_file.read()
107+
original_filenames.append(upload_file.filename)
108+
# Generate unique temp file to avoid collisions
109+
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".tmp")
110+
temp_path = temp_file.name
111+
temp_file.close()
112+
with open(temp_path, "wb") as f:
113+
f.write(content)
114+
temp_file_paths.append(temp_path)
115+
116+
file_path_to_original_filename = dict(
117+
zip(temp_file_paths, original_filenames, strict=True)
118+
)
119+
120+
# Ensure the search index exists before creating the upload task
121+
from api.documents import _ensure_index_exists
122+
123+
await _ensure_index_exists(jwt_token)
124+
125+
task_id = await task_service.create_upload_task(
126+
user_id=user_id,
127+
file_paths=temp_file_paths,
128+
jwt_token=jwt_token,
129+
owner_name=user_name,
130+
owner_email=user_email,
131+
original_filenames=file_path_to_original_filename,
132+
replace_duplicates=replace_duplicates,
133+
settings=settings,
134+
)
135+
136+
return JSONResponse(
137+
{
138+
"task_id": task_id,
139+
"message": f"Upload task created for {len(upload_files)} file(s)",
140+
"file_count": len(upload_files),
141+
"create_filter": create_filter,
142+
"filename": original_filenames[0] if len(original_filenames) == 1 else None,
143+
},
144+
status_code=202,
145+
)
146+
147+
except Exception:
148+
from utils.file_utils import safe_unlink
149+
150+
for temp_path in temp_file_paths:
151+
safe_unlink(temp_path)
152+
raise
153+
154+
except Exception as e:
155+
logger.error("Task-based traditional upload_ingest failed", error=str(e))
156+
import traceback
157+
158+
logger.error("Full traceback", traceback=traceback.format_exc())
159+
error_msg = str(e)
160+
if "AuthenticationException" in error_msg or "access denied" in error_msg.lower():
161+
return JSONResponse({"error": "Access denied"}, status_code=403)
162+
return JSONResponse({"error": "An internal error has occurred."}, status_code=500)
163+
164+
75165
async def _langflow_upload_ingest_task(
76166
upload_files: list[UploadFile],
77167
session_id,
@@ -113,13 +203,13 @@ async def _langflow_upload_ingest_task(
113203
original_filenames = []
114204

115205
try:
116-
temp_dir = tempfile.gettempdir()
117-
118206
for upload_file in upload_files:
119207
content = await upload_file.read()
120208
original_filenames.append(upload_file.filename)
121-
safe_filename = upload_file.filename.replace(" ", "_").replace("/", "_")
122-
temp_path = os.path.join(temp_dir, safe_filename)
209+
# Generate unique temp file to avoid collisions
210+
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".tmp")
211+
temp_path = temp_file.name
212+
temp_file.close()
123213
with open(temp_path, "wb") as f:
124214
f.write(content)
125215
temp_file_paths.append(temp_path)

src/api/settings/endpoints.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ async def get_settings(
211211
ocr=knowledge_config.ocr,
212212
picture_descriptions=knowledge_config.picture_descriptions,
213213
index_name=knowledge_config.index_name,
214+
disable_ingest_with_langflow=knowledge_config.disable_ingest_with_langflow,
214215
),
215216
agent=AgentConfig(
216217
llm_model=agent_config.llm_model,
@@ -461,6 +462,15 @@ async def update_settings(
461462
except Exception as e:
462463
logger.error(f"Failed to update docling settings in flow: {str(e)}")
463464

465+
if body.disable_ingest_with_langflow is not None:
466+
current_config.knowledge.disable_ingest_with_langflow = (
467+
body.disable_ingest_with_langflow
468+
)
469+
config_updated = True
470+
logger.info(
471+
f"Disable Langflow ingestion changed to {body.disable_ingest_with_langflow}"
472+
)
473+
464474
if body.chunk_size is not None:
465475
effective_overlap = (
466476
body.chunk_overlap

src/api/settings/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class SettingsUpdateBody(BaseModel):
2121
table_structure: bool | None = None
2222
ocr: bool | None = None
2323
picture_descriptions: bool | None = None
24+
disable_ingest_with_langflow: bool | None = None
2425
embedding_model: str | None = Field(None, min_length=1)
2526
embedding_provider: str | None = Field(None, pattern="^(openai|watsonx|ollama)$")
2627
index_name: str | None = Field(None, min_length=1)
@@ -128,6 +129,7 @@ class KnowledgeConfig(BaseModel):
128129
ocr: bool | None
129130
picture_descriptions: bool | None
130131
index_name: str | None
132+
disable_ingest_with_langflow: bool | None
131133

132134

133135
class AgentConfig(BaseModel):

0 commit comments

Comments
 (0)