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>
This commit is contained in:
Mike Fortman
2025-10-02 16:53:49 -05:00
committed by GitHub
parent 485358c040
commit aeb76cf1af
15 changed files with 379 additions and 17 deletions

View File

@ -1287,7 +1287,7 @@
"filename": "src/frontend/src/constants/constants.ts",
"hashed_secret": "19a2fbd0dd38b4097f419c962342ef5e109eab07",
"is_verified": false,
"line_number": 734,
"line_number": 737,
"is_secret": false
},
{
@ -1295,7 +1295,7 @@
"filename": "src/frontend/src/constants/constants.ts",
"hashed_secret": "3806954324550e26ef5de85d007f1746825a073c",
"is_verified": false,
"line_number": 735,
"line_number": 738,
"is_secret": false
},
{
@ -1303,7 +1303,7 @@
"filename": "src/frontend/src/constants/constants.ts",
"hashed_secret": "c04f8fbf55c9096907a982750b1c6b0e4c1dd658",
"is_verified": false,
"line_number": 1029,
"line_number": 913,
"is_secret": false
}
],
@ -1402,5 +1402,5 @@
}
]
},
"generated_at": "2025-06-20T19:21:57Z"
"generated_at": "2025-09-30T22:09:16Z"
}

View File

@ -374,6 +374,8 @@ class ConfigResponse(BaseModel):
event_delivery: Literal["polling", "streaming", "direct"]
webhook_auth_enable: bool
voice_mode_available: bool
default_folder_name: str
hide_getting_started_progress: bool
@classmethod
def from_settings(cls, settings: Settings, auth_settings) -> "ConfigResponse":
@ -386,6 +388,10 @@ class ConfigResponse(BaseModel):
Returns:
ConfigResponse: An instance populated with configuration and feature flag values.
"""
import os
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
return cls(
feature_flags=FEATURE_FLAGS,
serialization_max_items_length=settings.max_items_length,
@ -401,6 +407,8 @@ class ConfigResponse(BaseModel):
event_delivery=settings.event_delivery,
voice_mode_available=settings.voice_mode_available,
webhook_auth_enable=auth_settings.WEBHOOK_AUTH_ENABLE,
default_folder_name=DEFAULT_FOLDER_NAME,
hide_getting_started_progress=os.getenv("HIDE_GETTING_STARTED_PROGRESS", "").lower() == "true",
)

View File

@ -37,7 +37,11 @@ from sqlmodel.ext.asyncio.session import AsyncSession
from langflow.initial_setup.constants import STARTER_FOLDER_DESCRIPTION, STARTER_FOLDER_NAME
from langflow.services.auth.utils import create_super_user
from langflow.services.database.models.flow.model import Flow, FlowCreate
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
from langflow.services.database.models.folder.constants import (
DEFAULT_FOLDER_DESCRIPTION,
DEFAULT_FOLDER_NAME,
LEGACY_FOLDER_NAMES,
)
from langflow.services.database.models.folder.model import Folder, FolderCreate, FolderRead
from langflow.services.deps import get_settings_service, get_storage_service, get_variable_service, session_scope
@ -1027,6 +1031,9 @@ async def get_or_create_default_folder(session: AsyncSession, user_id: UUID) ->
Uses an idempotent insertion approach to handle concurrent creation gracefully.
If the DEFAULT_FOLDER_NAME env var is set to a custom value (e.g., "OpenRAG"), this function
will check for legacy folder names and migrate them to avoid duplicates.
This implementation avoids an external distributed lock and works with both SQLite and PostgreSQL.
Args:
@ -1034,16 +1041,45 @@ async def get_or_create_default_folder(session: AsyncSession, user_id: UUID) ->
user_id (UUID): The ID of the user who owns the folder.
Returns:
UUID: The ID of the default folder.
FolderRead: The default folder for the user.
"""
# First, check if the current default folder exists
stmt = select(Folder).where(Folder.user_id == user_id, Folder.name == DEFAULT_FOLDER_NAME)
result = await session.exec(stmt)
folder = result.first()
if folder:
return FolderRead.model_validate(folder, from_attributes=True)
# Check if a legacy folder exists and migrate it if the name is different from default
if DEFAULT_FOLDER_NAME not in LEGACY_FOLDER_NAMES:
for legacy_name in LEGACY_FOLDER_NAMES:
if legacy_name == DEFAULT_FOLDER_NAME:
continue # Skip if legacy name is the same as current default
legacy_stmt = select(Folder).where(Folder.user_id == user_id, Folder.name == legacy_name)
legacy_result = await session.exec(legacy_stmt)
legacy_folder = legacy_result.first()
if legacy_folder:
# Migrate the legacy folder by renaming it
await logger.ainfo(
f"Migrating legacy folder '{legacy_name}' to '{DEFAULT_FOLDER_NAME}' for user {user_id}"
)
legacy_folder.name = DEFAULT_FOLDER_NAME
legacy_folder.description = DEFAULT_FOLDER_DESCRIPTION
session.add(legacy_folder)
try:
await session.commit()
await session.refresh(legacy_folder)
return FolderRead.model_validate(legacy_folder, from_attributes=True)
except sa.exc.IntegrityError:
# If there's a conflict, rollback and proceed to create new folder
await session.rollback()
break
# If no existing folder found, create a new one
try:
folder_obj = Folder(user_id=user_id, name=DEFAULT_FOLDER_NAME)
folder_obj = Folder(user_id=user_id, name=DEFAULT_FOLDER_NAME, description=DEFAULT_FOLDER_DESCRIPTION)
session.add(folder_obj)
await session.commit()
await session.refresh(folder_obj)

View File

@ -1,2 +1,8 @@
import os
DEFAULT_FOLDER_DESCRIPTION = "Manage your own flows. Download and upload projects."
DEFAULT_FOLDER_NAME = "Starter Project"
# Use DEFAULT_FOLDER_NAME env var, defaulting to "Starter Project" if not set
DEFAULT_FOLDER_NAME = os.getenv("DEFAULT_FOLDER_NAME", "Starter Project")
# Legacy folder names that may exist from previous installations
LEGACY_FOLDER_NAMES = ["My Collection", "Starter Project"]

View File

@ -529,3 +529,209 @@ class TestMultiUserMCPServerAccess:
response_two = await client.get(f"/api/v2/mcp/servers/{server_name}", headers={"x-api-key": user_two_api_key})
assert response_two.json() is None
class TestMCPWithDefaultFolderName:
"""Test MCP configuration with different DEFAULT_FOLDER_NAME values."""
@pytest.mark.asyncio
async def test_mcp_finds_default_folder_standard_mode(self, client: AsyncClient): # noqa: ARG002
"""Test that MCP finds the correct folder in standard mode (Starter Project)."""
user_id = uuid4()
project_id = uuid4()
flow_id = uuid4()
async with session_scope() as session:
# Create user
user = User(id=user_id, username=f"test_default_folder_{user_id}", password="hashed_password") # noqa: S106
session.add(user)
# Create folder with DEFAULT_FOLDER_NAME (should match current setting)
folder = Folder(id=project_id, name=DEFAULT_FOLDER_NAME, user_id=user_id, description="Test folder")
session.add(folder)
# Create flow in folder
flow = Flow(
id=flow_id,
name="Test Flow",
description="A test flow",
folder_id=project_id,
user_id=user_id,
is_component=False,
mcp_enabled=None,
)
session.add(flow)
await session.commit()
try:
async with session_scope() as session:
from langflow.services.deps import get_settings_service
settings_service = get_settings_service()
original_setting = settings_service.settings.add_projects_to_mcp_servers
try:
settings_service.settings.add_projects_to_mcp_servers = True
await auto_configure_starter_projects_mcp(session)
# Verify folder was found and processed
updated_folder = await session.get(Folder, project_id)
assert updated_folder is not None
assert updated_folder.name == DEFAULT_FOLDER_NAME
finally:
settings_service.settings.add_projects_to_mcp_servers = original_setting
finally:
# Cleanup
async with session_scope() as session:
flow_to_delete = await session.get(Flow, flow_id)
if flow_to_delete:
await session.delete(flow_to_delete)
folder_to_delete = await session.get(Folder, project_id)
if folder_to_delete:
await session.delete(folder_to_delete)
user_to_delete = await session.get(User, user_id)
if user_to_delete:
await session.delete(user_to_delete)
await session.commit()
@pytest.mark.asyncio
async def test_mcp_with_legacy_folder_after_migration(self, client: AsyncClient): # noqa: ARG002
"""Test that MCP finds migrated folders after setting custom DEFAULT_FOLDER_NAME."""
user_id = uuid4()
project_id = uuid4()
flow_id = uuid4()
# Only run this test when DEFAULT_FOLDER_NAME is set to custom value (e.g., "OpenRAG")
if DEFAULT_FOLDER_NAME in ["Starter Project", "My Collection"]:
pytest.skip("Test only applicable when DEFAULT_FOLDER_NAME is set to custom value")
async with session_scope() as session:
# Create user
user = User(id=user_id, username=f"test_migrated_{user_id}", password="hashed_password") # noqa: S106
session.add(user)
# Create folder with legacy name that will be migrated
legacy_folder = Folder(id=project_id, name="Starter Project", user_id=user_id, description="Legacy folder")
session.add(legacy_folder)
# Create flow in folder
flow = Flow(
id=flow_id,
name="Test Flow in Legacy Folder",
description="A test flow",
folder_id=project_id,
user_id=user_id,
is_component=False,
mcp_enabled=None,
)
session.add(flow)
await session.commit()
try:
# Trigger migration by calling get_or_create_default_folder
from langflow.initial_setup.setup import get_or_create_default_folder
async with session_scope() as session:
migrated_folder = await get_or_create_default_folder(session, user_id)
assert migrated_folder.name == DEFAULT_FOLDER_NAME
assert migrated_folder.id == project_id # Same folder, renamed
# Now test that MCP can find the migrated folder
async with session_scope() as session:
from langflow.services.deps import get_settings_service
settings_service = get_settings_service()
original_setting = settings_service.settings.add_projects_to_mcp_servers
try:
settings_service.settings.add_projects_to_mcp_servers = True
await auto_configure_starter_projects_mcp(session)
# Verify MCP found the migrated folder
updated_folder = await session.get(Folder, project_id)
assert updated_folder is not None
assert updated_folder.name == DEFAULT_FOLDER_NAME
finally:
settings_service.settings.add_projects_to_mcp_servers = original_setting
finally:
# Cleanup
async with session_scope() as session:
flow_to_delete = await session.get(Flow, flow_id)
if flow_to_delete:
await session.delete(flow_to_delete)
folder_to_delete = await session.get(Folder, project_id)
if folder_to_delete:
await session.delete(folder_to_delete)
user_to_delete = await session.get(User, user_id)
if user_to_delete:
await session.delete(user_to_delete)
await session.commit()
@pytest.mark.asyncio
async def test_mcp_skips_wrong_folder_name(self, client: AsyncClient): # noqa: ARG002
"""Test that MCP skips folders that don't match DEFAULT_FOLDER_NAME."""
user_id = uuid4()
project_id = uuid4()
flow_id = uuid4()
async with session_scope() as session:
# Create user
user = User(id=user_id, username=f"test_wrong_folder_{user_id}", password="hashed_password") # noqa: S106
session.add(user)
# Create folder with different name
folder = Folder(id=project_id, name="Some Other Folder", user_id=user_id, description="Wrong folder")
session.add(folder)
# Create flow in folder
flow = Flow(
id=flow_id,
name="Test Flow",
description="A test flow",
folder_id=project_id,
user_id=user_id,
is_component=False,
mcp_enabled=None,
)
session.add(flow)
await session.commit()
try:
async with session_scope() as session:
from langflow.services.deps import get_settings_service
settings_service = get_settings_service()
original_setting = settings_service.settings.add_projects_to_mcp_servers
try:
settings_service.settings.add_projects_to_mcp_servers = True
# Should not raise an error, just skip this user
await auto_configure_starter_projects_mcp(session)
# Verify flow was NOT configured (still None or False)
updated_flow = await session.get(Flow, flow_id)
assert updated_flow.mcp_enabled in [None, False]
finally:
settings_service.settings.add_projects_to_mcp_servers = original_setting
finally:
# Cleanup
async with session_scope() as session:
flow_to_delete = await session.get(Flow, flow_id)
if flow_to_delete:
await session.delete(flow_to_delete)
folder_to_delete = await session.get(Folder, project_id)
if folder_to_delete:
await session.delete(folder_to_delete)
user_to_delete = await session.get(User, user_id)
if user_to_delete:
await session.delete(user_to_delete)
await session.commit()

View File

@ -2,7 +2,8 @@ import asyncio
from uuid import uuid4
import pytest
from langflow.initial_setup.setup import DEFAULT_FOLDER_NAME, get_or_create_default_folder, session_scope
from langflow.initial_setup.setup import get_or_create_default_folder, session_scope
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
from langflow.services.database.models.folder.model import FolderRead

View File

@ -4,6 +4,7 @@ import { SidebarTrigger } from "@/components/ui/sidebar";
import { useUpdateUser } from "@/controllers/API/queries/auth";
import CustomGetStartedProgress from "@/customization/components/custom-get-started-progress";
import useAuthStore from "@/stores/authStore";
import { useUtilityStore } from "@/stores/utilityStore";
import { AddFolderButton } from "./add-folder-button";
import { UploadFolderButton } from "./upload-folder-button";
@ -19,6 +20,9 @@ export const HeaderButtons = ({
addNewFolder: () => void;
}) => {
const userData = useAuthStore((state) => state.userData);
const hideGettingStartedProgress = useUtilityStore(
(state) => state.hideGettingStartedProgress,
);
const [isDismissedDialog, setIsDismissedDialog] = useState(
userData?.optins?.dialog_dismissed,
@ -55,7 +59,7 @@ export const HeaderButtons = ({
return (
<>
{!isDismissedDialog && userData && (
{!hideGettingStartedProgress && !isDismissedDialog && userData && (
<>
<CustomGetStartedProgress
userData={userData!}

View File

@ -7,8 +7,8 @@ import {
SelectItem,
SelectTrigger,
} from "@/components/ui/select-custom";
import { DEFAULT_FOLDER } from "@/constants/constants";
import type { FolderType } from "@/pages/MainPage/entities";
import { useUtilityStore } from "@/stores/utilityStore";
import { cn } from "@/utils/utils";
import { handleSelectChange } from "../helpers/handle-select-change";
import { FolderSelectItem } from "./folder-select-item";
@ -28,6 +28,7 @@ export const SelectOptions = ({
handleSelectFolderToRename: (folder: FolderType) => void;
checkPathName: (folderId: string) => boolean;
}) => {
const defaultFolderName = useUtilityStore((state) => state.defaultFolderName);
return (
<div>
<Select
@ -60,7 +61,7 @@ export const SelectOptions = ({
</SelectTrigger>
</ShadTooltip>
<SelectContent align="end" alignOffset={-16} position="popper">
{item.name !== DEFAULT_FOLDER && (
{item.name !== defaultFolderName && (
<SelectItem
id="rename-button"
value="rename"

View File

@ -13,7 +13,6 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { DEFAULT_FOLDER } from "@/constants/constants";
import { useUpdateUser } from "@/controllers/API/queries/auth";
import {
usePatchFolders,
@ -41,6 +40,7 @@ import type { FolderType } from "../../../../../pages/MainPage/entities";
import useAlertStore from "../../../../../stores/alertStore";
import useFlowsManagerStore from "../../../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../../../stores/foldersStore";
import { useUtilityStore } from "../../../../../stores/utilityStore";
import { handleKeyDown } from "../../../../../utils/reactflowUtils";
import { cn } from "../../../../../utils/utils";
import useFileDrop from "../../hooks/use-on-file-drop";
@ -86,6 +86,7 @@ const SideBarFoldersButtonsComponent = ({
const folderIdDragging = useFolderStore((state) => state.folderIdDragging);
const myCollectionId = useFolderStore((state) => state.myCollectionId);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const defaultFolderName = useUtilityStore((state) => state.defaultFolderName);
const folderId = useParams().folderId ?? myCollectionId ?? "";
@ -275,7 +276,7 @@ const SideBarFoldersButtonsComponent = ({
};
const handleDoubleClick = (event, item) => {
if (item.name === DEFAULT_FOLDER) {
if (item.name === defaultFolderName) {
return;
}

View File

@ -558,7 +558,10 @@ export const NOUNS: string[] = [
*/
export const USER_PROJECTS_HEADER = "My Collection";
// This will be dynamically set based on the RUN_WITH_OPENRAG feature flag
// The actual value is determined by the backend configuration
export const DEFAULT_FOLDER = "Starter Project";
export const OPENRAG_FOLDER = "OpenRAG";
export const MAX_MCP_SERVER_NAME_LENGTH = 30;
@ -741,7 +744,7 @@ export const STATUS_INACTIVE = "Execution blocked";
export const STATUS_BUILDING = "Building...";
export const SAVED_HOVER = "Last saved: ";
export const RUN_TIMESTAMP_PREFIX = "Last Run: ";
export const STARTER_FOLDER_NAME = "Starter Projects";
export const PRIORITY_SIDEBAR_ORDER = [
"saved_components",
"inputs",

View File

@ -23,6 +23,8 @@ export interface ConfigResponse {
event_delivery: EventDeliveryType;
webhook_auth_enable: boolean;
voice_mode_available: boolean;
default_folder_name: string;
hide_getting_started_progress: boolean;
}
export const useGetConfig: useQueryFunctionType<undefined, ConfigResponse> = (
@ -49,6 +51,12 @@ export const useGetConfig: useQueryFunctionType<undefined, ConfigResponse> = (
const setWebhookAuthEnable = useUtilityStore(
(state) => state.setWebhookAuthEnable,
);
const setDefaultFolderName = useUtilityStore(
(state) => state.setDefaultFolderName,
);
const setHideGettingStartedProgress = useUtilityStore(
(state) => state.setHideGettingStartedProgress,
);
const { query } = UseRequestProcessor();
@ -72,6 +80,10 @@ export const useGetConfig: useQueryFunctionType<undefined, ConfigResponse> = (
);
setEventDelivery(data.event_delivery ?? EventDeliveryType.POLLING);
setWebhookAuthEnable(data.webhook_auth_enable ?? true);
setDefaultFolderName(data.default_folder_name ?? "Starter Project");
setHideGettingStartedProgress(
data.hide_getting_started_progress ?? false,
);
}
return data;
};

View File

@ -1,7 +1,7 @@
import { DEFAULT_FOLDER } from "@/constants/constants";
import type { FolderType } from "@/pages/MainPage/entities";
import useAuthStore from "@/stores/authStore";
import { useFolderStore } from "@/stores/foldersStore";
import { useUtilityStore } from "@/stores/utilityStore";
import type { useQueryFunctionType } from "@/types/api";
import { api } from "../../api";
import { getURL } from "../../helpers/constants";
@ -15,6 +15,7 @@ export const useGetFoldersQuery: useQueryFunctionType<
const setMyCollectionId = useFolderStore((state) => state.setMyCollectionId);
const setFolders = useFolderStore((state) => state.setFolders);
const defaultFolderName = useUtilityStore((state) => state.defaultFolderName);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
@ -23,7 +24,7 @@ export const useGetFoldersQuery: useQueryFunctionType<
const res = await api.get(`${getURL("PROJECTS")}/`);
const data = res.data;
const myCollectionId = data?.find((f) => f.name === DEFAULT_FOLDER)?.id;
const myCollectionId = data?.find((f) => f.name === defaultFolderName)?.id;
setMyCollectionId(myCollectionId);
setFolders(data);

View File

@ -40,6 +40,8 @@ describe("useUtilityStore", () => {
currentSessionId: "",
eventDelivery: EventDeliveryType.POLLING,
webhookAuthEnable: true,
defaultFolderName: "Starter Project",
hideGettingStartedProgress: false,
});
});
@ -61,6 +63,8 @@ describe("useUtilityStore", () => {
expect(result.current.currentSessionId).toBe("");
expect(result.current.eventDelivery).toBe(EventDeliveryType.POLLING);
expect(result.current.webhookAuthEnable).toBe(true);
expect(result.current.defaultFolderName).toBe("Starter Project");
expect(result.current.hideGettingStartedProgress).toBe(false);
});
});
@ -556,6 +560,75 @@ describe("useUtilityStore", () => {
});
});
describe("setDefaultFolderName", () => {
it("should set defaultFolderName", () => {
const { result } = renderHook(() => useUtilityStore());
act(() => {
result.current.setDefaultFolderName("OpenRAG");
});
expect(result.current.defaultFolderName).toBe("OpenRAG");
});
it("should change defaultFolderName", () => {
const { result } = renderHook(() => useUtilityStore());
act(() => {
result.current.setDefaultFolderName("OpenRAG");
});
expect(result.current.defaultFolderName).toBe("OpenRAG");
act(() => {
result.current.setDefaultFolderName("My Custom Folder");
});
expect(result.current.defaultFolderName).toBe("My Custom Folder");
});
it("should handle various folder names", () => {
const { result } = renderHook(() => useUtilityStore());
const folderNames = [
"Starter Project",
"OpenRAG",
"My Collection",
"Custom Folder",
];
folderNames.forEach((folderName) => {
act(() => {
result.current.setDefaultFolderName(folderName);
});
expect(result.current.defaultFolderName).toBe(folderName);
});
});
});
describe("setHideGettingStartedProgress", () => {
it("should set hideGettingStartedProgress to true", () => {
const { result } = renderHook(() => useUtilityStore());
act(() => {
result.current.setHideGettingStartedProgress(true);
});
expect(result.current.hideGettingStartedProgress).toBe(true);
});
it("should toggle hideGettingStartedProgress", () => {
const { result } = renderHook(() => useUtilityStore());
act(() => {
result.current.setHideGettingStartedProgress(true);
});
expect(result.current.hideGettingStartedProgress).toBe(true);
act(() => {
result.current.setHideGettingStartedProgress(false);
});
expect(result.current.hideGettingStartedProgress).toBe(false);
});
});
describe("state interactions", () => {
it("should handle multiple state updates", () => {
const { result } = renderHook(() => useUtilityStore());

View File

@ -51,4 +51,10 @@ export const useUtilityStore = create<UtilityStoreType>((set, get) => ({
webhookAuthEnable: true,
setWebhookAuthEnable: (webhookAuthEnable: boolean) =>
set({ webhookAuthEnable }),
defaultFolderName: "Starter Project",
setDefaultFolderName: (defaultFolderName: string) =>
set({ defaultFolderName }),
hideGettingStartedProgress: false,
setHideGettingStartedProgress: (hideGettingStartedProgress: boolean) =>
set({ hideGettingStartedProgress }),
}));

View File

@ -30,4 +30,8 @@ export type UtilityStoreType = {
setSerializationMaxItemsLength: (serializationMaxItemsLength: number) => void;
webhookAuthEnable: boolean;
setWebhookAuthEnable: (webhookAuthEnable: boolean) => void;
defaultFolderName: string;
setDefaultFolderName: (defaultFolderName: string) => void;
hideGettingStartedProgress: boolean;
setHideGettingStartedProgress: (hideGettingStartedProgress: boolean) => void;
};