Skip to content

Commit aeb76cf

Browse files
feat: Add Openrag feature flag changing starter project name (#10068)
* init default folder change * hide getting started progress * ui testing * add mcp test coverage * [autofix.ci] apply automated fixes * Update to DEFAULT_FOLDER_NAME env var * add new ff for getting started progress --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 485358c commit aeb76cf

15 files changed

Lines changed: 379 additions & 17 deletions

File tree

.secrets.baseline

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,23 +1287,23 @@
12871287
"filename": "src/frontend/src/constants/constants.ts",
12881288
"hashed_secret": "19a2fbd0dd38b4097f419c962342ef5e109eab07",
12891289
"is_verified": false,
1290-
"line_number": 734,
1290+
"line_number": 737,
12911291
"is_secret": false
12921292
},
12931293
{
12941294
"type": "Secret Keyword",
12951295
"filename": "src/frontend/src/constants/constants.ts",
12961296
"hashed_secret": "3806954324550e26ef5de85d007f1746825a073c",
12971297
"is_verified": false,
1298-
"line_number": 735,
1298+
"line_number": 738,
12991299
"is_secret": false
13001300
},
13011301
{
13021302
"type": "Secret Keyword",
13031303
"filename": "src/frontend/src/constants/constants.ts",
13041304
"hashed_secret": "c04f8fbf55c9096907a982750b1c6b0e4c1dd658",
13051305
"is_verified": false,
1306-
"line_number": 1029,
1306+
"line_number": 913,
13071307
"is_secret": false
13081308
}
13091309
],
@@ -1402,5 +1402,5 @@
14021402
}
14031403
]
14041404
},
1405-
"generated_at": "2025-06-20T19:21:57Z"
1405+
"generated_at": "2025-09-30T22:09:16Z"
14061406
}

src/backend/base/langflow/api/v1/schemas.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,8 @@ class ConfigResponse(BaseModel):
374374
event_delivery: Literal["polling", "streaming", "direct"]
375375
webhook_auth_enable: bool
376376
voice_mode_available: bool
377+
default_folder_name: str
378+
hide_getting_started_progress: bool
377379

378380
@classmethod
379381
def from_settings(cls, settings: Settings, auth_settings) -> "ConfigResponse":
@@ -386,6 +388,10 @@ def from_settings(cls, settings: Settings, auth_settings) -> "ConfigResponse":
386388
Returns:
387389
ConfigResponse: An instance populated with configuration and feature flag values.
388390
"""
391+
import os
392+
393+
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
394+
389395
return cls(
390396
feature_flags=FEATURE_FLAGS,
391397
serialization_max_items_length=settings.max_items_length,
@@ -401,6 +407,8 @@ def from_settings(cls, settings: Settings, auth_settings) -> "ConfigResponse":
401407
event_delivery=settings.event_delivery,
402408
voice_mode_available=settings.voice_mode_available,
403409
webhook_auth_enable=auth_settings.WEBHOOK_AUTH_ENABLE,
410+
default_folder_name=DEFAULT_FOLDER_NAME,
411+
hide_getting_started_progress=os.getenv("HIDE_GETTING_STARTED_PROGRESS", "").lower() == "true",
404412
)
405413

406414

src/backend/base/langflow/initial_setup/setup.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@
3737
from langflow.initial_setup.constants import STARTER_FOLDER_DESCRIPTION, STARTER_FOLDER_NAME
3838
from langflow.services.auth.utils import create_super_user
3939
from langflow.services.database.models.flow.model import Flow, FlowCreate
40-
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
40+
from langflow.services.database.models.folder.constants import (
41+
DEFAULT_FOLDER_DESCRIPTION,
42+
DEFAULT_FOLDER_NAME,
43+
LEGACY_FOLDER_NAMES,
44+
)
4145
from langflow.services.database.models.folder.model import Folder, FolderCreate, FolderRead
4246
from langflow.services.deps import get_settings_service, get_storage_service, get_variable_service, session_scope
4347

@@ -1027,23 +1031,55 @@ async def get_or_create_default_folder(session: AsyncSession, user_id: UUID) ->
10271031
10281032
Uses an idempotent insertion approach to handle concurrent creation gracefully.
10291033
1034+
If the DEFAULT_FOLDER_NAME env var is set to a custom value (e.g., "OpenRAG"), this function
1035+
will check for legacy folder names and migrate them to avoid duplicates.
1036+
10301037
This implementation avoids an external distributed lock and works with both SQLite and PostgreSQL.
10311038
10321039
Args:
10331040
session (AsyncSession): The active database session.
10341041
user_id (UUID): The ID of the user who owns the folder.
10351042
10361043
Returns:
1037-
UUID: The ID of the default folder.
1044+
FolderRead: The default folder for the user.
10381045
"""
1046+
# First, check if the current default folder exists
10391047
stmt = select(Folder).where(Folder.user_id == user_id, Folder.name == DEFAULT_FOLDER_NAME)
10401048
result = await session.exec(stmt)
10411049
folder = result.first()
10421050
if folder:
10431051
return FolderRead.model_validate(folder, from_attributes=True)
10441052

1053+
# Check if a legacy folder exists and migrate it if the name is different from default
1054+
if DEFAULT_FOLDER_NAME not in LEGACY_FOLDER_NAMES:
1055+
for legacy_name in LEGACY_FOLDER_NAMES:
1056+
if legacy_name == DEFAULT_FOLDER_NAME:
1057+
continue # Skip if legacy name is the same as current default
1058+
1059+
legacy_stmt = select(Folder).where(Folder.user_id == user_id, Folder.name == legacy_name)
1060+
legacy_result = await session.exec(legacy_stmt)
1061+
legacy_folder = legacy_result.first()
1062+
1063+
if legacy_folder:
1064+
# Migrate the legacy folder by renaming it
1065+
await logger.ainfo(
1066+
f"Migrating legacy folder '{legacy_name}' to '{DEFAULT_FOLDER_NAME}' for user {user_id}"
1067+
)
1068+
legacy_folder.name = DEFAULT_FOLDER_NAME
1069+
legacy_folder.description = DEFAULT_FOLDER_DESCRIPTION
1070+
session.add(legacy_folder)
1071+
try:
1072+
await session.commit()
1073+
await session.refresh(legacy_folder)
1074+
return FolderRead.model_validate(legacy_folder, from_attributes=True)
1075+
except sa.exc.IntegrityError:
1076+
# If there's a conflict, rollback and proceed to create new folder
1077+
await session.rollback()
1078+
break
1079+
1080+
# If no existing folder found, create a new one
10451081
try:
1046-
folder_obj = Folder(user_id=user_id, name=DEFAULT_FOLDER_NAME)
1082+
folder_obj = Folder(user_id=user_id, name=DEFAULT_FOLDER_NAME, description=DEFAULT_FOLDER_DESCRIPTION)
10471083
session.add(folder_obj)
10481084
await session.commit()
10491085
await session.refresh(folder_obj)
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
1+
import os
2+
13
DEFAULT_FOLDER_DESCRIPTION = "Manage your own flows. Download and upload projects."
2-
DEFAULT_FOLDER_NAME = "Starter Project"
4+
# Use DEFAULT_FOLDER_NAME env var, defaulting to "Starter Project" if not set
5+
DEFAULT_FOLDER_NAME = os.getenv("DEFAULT_FOLDER_NAME", "Starter Project")
6+
7+
# Legacy folder names that may exist from previous installations
8+
LEGACY_FOLDER_NAMES = ["My Collection", "Starter Project"]

src/backend/tests/unit/api/utils/test_config_utils.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,3 +529,209 @@ async def test_cross_user_mcp_server_access_prevention(
529529

530530
response_two = await client.get(f"/api/v2/mcp/servers/{server_name}", headers={"x-api-key": user_two_api_key})
531531
assert response_two.json() is None
532+
533+
534+
class TestMCPWithDefaultFolderName:
535+
"""Test MCP configuration with different DEFAULT_FOLDER_NAME values."""
536+
537+
@pytest.mark.asyncio
538+
async def test_mcp_finds_default_folder_standard_mode(self, client: AsyncClient): # noqa: ARG002
539+
"""Test that MCP finds the correct folder in standard mode (Starter Project)."""
540+
user_id = uuid4()
541+
project_id = uuid4()
542+
flow_id = uuid4()
543+
544+
async with session_scope() as session:
545+
# Create user
546+
user = User(id=user_id, username=f"test_default_folder_{user_id}", password="hashed_password") # noqa: S106
547+
session.add(user)
548+
549+
# Create folder with DEFAULT_FOLDER_NAME (should match current setting)
550+
folder = Folder(id=project_id, name=DEFAULT_FOLDER_NAME, user_id=user_id, description="Test folder")
551+
session.add(folder)
552+
553+
# Create flow in folder
554+
flow = Flow(
555+
id=flow_id,
556+
name="Test Flow",
557+
description="A test flow",
558+
folder_id=project_id,
559+
user_id=user_id,
560+
is_component=False,
561+
mcp_enabled=None,
562+
)
563+
session.add(flow)
564+
565+
await session.commit()
566+
567+
try:
568+
async with session_scope() as session:
569+
from langflow.services.deps import get_settings_service
570+
571+
settings_service = get_settings_service()
572+
original_setting = settings_service.settings.add_projects_to_mcp_servers
573+
574+
try:
575+
settings_service.settings.add_projects_to_mcp_servers = True
576+
await auto_configure_starter_projects_mcp(session)
577+
578+
# Verify folder was found and processed
579+
updated_folder = await session.get(Folder, project_id)
580+
assert updated_folder is not None
581+
assert updated_folder.name == DEFAULT_FOLDER_NAME
582+
583+
finally:
584+
settings_service.settings.add_projects_to_mcp_servers = original_setting
585+
586+
finally:
587+
# Cleanup
588+
async with session_scope() as session:
589+
flow_to_delete = await session.get(Flow, flow_id)
590+
if flow_to_delete:
591+
await session.delete(flow_to_delete)
592+
folder_to_delete = await session.get(Folder, project_id)
593+
if folder_to_delete:
594+
await session.delete(folder_to_delete)
595+
user_to_delete = await session.get(User, user_id)
596+
if user_to_delete:
597+
await session.delete(user_to_delete)
598+
await session.commit()
599+
600+
@pytest.mark.asyncio
601+
async def test_mcp_with_legacy_folder_after_migration(self, client: AsyncClient): # noqa: ARG002
602+
"""Test that MCP finds migrated folders after setting custom DEFAULT_FOLDER_NAME."""
603+
user_id = uuid4()
604+
project_id = uuid4()
605+
flow_id = uuid4()
606+
607+
# Only run this test when DEFAULT_FOLDER_NAME is set to custom value (e.g., "OpenRAG")
608+
if DEFAULT_FOLDER_NAME in ["Starter Project", "My Collection"]:
609+
pytest.skip("Test only applicable when DEFAULT_FOLDER_NAME is set to custom value")
610+
611+
async with session_scope() as session:
612+
# Create user
613+
user = User(id=user_id, username=f"test_migrated_{user_id}", password="hashed_password") # noqa: S106
614+
session.add(user)
615+
616+
# Create folder with legacy name that will be migrated
617+
legacy_folder = Folder(id=project_id, name="Starter Project", user_id=user_id, description="Legacy folder")
618+
session.add(legacy_folder)
619+
620+
# Create flow in folder
621+
flow = Flow(
622+
id=flow_id,
623+
name="Test Flow in Legacy Folder",
624+
description="A test flow",
625+
folder_id=project_id,
626+
user_id=user_id,
627+
is_component=False,
628+
mcp_enabled=None,
629+
)
630+
session.add(flow)
631+
632+
await session.commit()
633+
634+
try:
635+
# Trigger migration by calling get_or_create_default_folder
636+
from langflow.initial_setup.setup import get_or_create_default_folder
637+
638+
async with session_scope() as session:
639+
migrated_folder = await get_or_create_default_folder(session, user_id)
640+
assert migrated_folder.name == DEFAULT_FOLDER_NAME
641+
assert migrated_folder.id == project_id # Same folder, renamed
642+
643+
# Now test that MCP can find the migrated folder
644+
async with session_scope() as session:
645+
from langflow.services.deps import get_settings_service
646+
647+
settings_service = get_settings_service()
648+
original_setting = settings_service.settings.add_projects_to_mcp_servers
649+
650+
try:
651+
settings_service.settings.add_projects_to_mcp_servers = True
652+
await auto_configure_starter_projects_mcp(session)
653+
654+
# Verify MCP found the migrated folder
655+
updated_folder = await session.get(Folder, project_id)
656+
assert updated_folder is not None
657+
assert updated_folder.name == DEFAULT_FOLDER_NAME
658+
659+
finally:
660+
settings_service.settings.add_projects_to_mcp_servers = original_setting
661+
662+
finally:
663+
# Cleanup
664+
async with session_scope() as session:
665+
flow_to_delete = await session.get(Flow, flow_id)
666+
if flow_to_delete:
667+
await session.delete(flow_to_delete)
668+
folder_to_delete = await session.get(Folder, project_id)
669+
if folder_to_delete:
670+
await session.delete(folder_to_delete)
671+
user_to_delete = await session.get(User, user_id)
672+
if user_to_delete:
673+
await session.delete(user_to_delete)
674+
await session.commit()
675+
676+
@pytest.mark.asyncio
677+
async def test_mcp_skips_wrong_folder_name(self, client: AsyncClient): # noqa: ARG002
678+
"""Test that MCP skips folders that don't match DEFAULT_FOLDER_NAME."""
679+
user_id = uuid4()
680+
project_id = uuid4()
681+
flow_id = uuid4()
682+
683+
async with session_scope() as session:
684+
# Create user
685+
user = User(id=user_id, username=f"test_wrong_folder_{user_id}", password="hashed_password") # noqa: S106
686+
session.add(user)
687+
688+
# Create folder with different name
689+
folder = Folder(id=project_id, name="Some Other Folder", user_id=user_id, description="Wrong folder")
690+
session.add(folder)
691+
692+
# Create flow in folder
693+
flow = Flow(
694+
id=flow_id,
695+
name="Test Flow",
696+
description="A test flow",
697+
folder_id=project_id,
698+
user_id=user_id,
699+
is_component=False,
700+
mcp_enabled=None,
701+
)
702+
session.add(flow)
703+
704+
await session.commit()
705+
706+
try:
707+
async with session_scope() as session:
708+
from langflow.services.deps import get_settings_service
709+
710+
settings_service = get_settings_service()
711+
original_setting = settings_service.settings.add_projects_to_mcp_servers
712+
713+
try:
714+
settings_service.settings.add_projects_to_mcp_servers = True
715+
# Should not raise an error, just skip this user
716+
await auto_configure_starter_projects_mcp(session)
717+
718+
# Verify flow was NOT configured (still None or False)
719+
updated_flow = await session.get(Flow, flow_id)
720+
assert updated_flow.mcp_enabled in [None, False]
721+
722+
finally:
723+
settings_service.settings.add_projects_to_mcp_servers = original_setting
724+
725+
finally:
726+
# Cleanup
727+
async with session_scope() as session:
728+
flow_to_delete = await session.get(Flow, flow_id)
729+
if flow_to_delete:
730+
await session.delete(flow_to_delete)
731+
folder_to_delete = await session.get(Folder, project_id)
732+
if folder_to_delete:
733+
await session.delete(folder_to_delete)
734+
user_to_delete = await session.get(User, user_id)
735+
if user_to_delete:
736+
await session.delete(user_to_delete)
737+
await session.commit()

src/backend/tests/unit/initial_setup/test_setup_functions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from uuid import uuid4
33

44
import pytest
5-
from langflow.initial_setup.setup import DEFAULT_FOLDER_NAME, get_or_create_default_folder, session_scope
5+
from langflow.initial_setup.setup import get_or_create_default_folder, session_scope
6+
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
67
from langflow.services.database.models.folder.model import FolderRead
78

89

src/frontend/src/components/core/folderSidebarComponent/components/sideBarFolderButtons/components/header-buttons.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { SidebarTrigger } from "@/components/ui/sidebar";
44
import { useUpdateUser } from "@/controllers/API/queries/auth";
55
import CustomGetStartedProgress from "@/customization/components/custom-get-started-progress";
66
import useAuthStore from "@/stores/authStore";
7+
import { useUtilityStore } from "@/stores/utilityStore";
78
import { AddFolderButton } from "./add-folder-button";
89
import { UploadFolderButton } from "./upload-folder-button";
910

@@ -19,6 +20,9 @@ export const HeaderButtons = ({
1920
addNewFolder: () => void;
2021
}) => {
2122
const userData = useAuthStore((state) => state.userData);
23+
const hideGettingStartedProgress = useUtilityStore(
24+
(state) => state.hideGettingStartedProgress,
25+
);
2226

2327
const [isDismissedDialog, setIsDismissedDialog] = useState(
2428
userData?.optins?.dialog_dismissed,
@@ -55,7 +59,7 @@ export const HeaderButtons = ({
5559

5660
return (
5761
<>
58-
{!isDismissedDialog && userData && (
62+
{!hideGettingStartedProgress && !isDismissedDialog && userData && (
5963
<>
6064
<CustomGetStartedProgress
6165
userData={userData!}

0 commit comments

Comments
 (0)