Consolidate feature flags to ALWRITY_ENABLED_FEATURES
Backend: - Add get_enabled_features() returning set from ALWRITY_ENABLED_FEATURES - Update router registry to use 'features' instead of 'profiles' - Support feature names: podcast, blog-writer, youtube, story-writer, etc - Update bootstrap gating to use enabled features - Update PODCAST_ONLY_DEMO_MODE to check new flag first - Add backwards compatibility with legacy env vars Frontend: - Update demoMode.ts to use REACT_APP_ENABLED_FEATURES - Add getEnabledFeatures() and isFeatureEnabled() utilities Usage: ALWRITY_ENABLED_FEATURES=all # All features (default) ALWRITY_ENABLED_FEATURES=podcast # Podcast only ALWRITY_ENABLED_FEATURES=podcast,core # Podcast + core features
This commit is contained in:
@@ -9,7 +9,9 @@ from typing import Iterable, Tuple
|
||||
from .feature_registry import FEATURE_GROUPS, PROFILE_GROUP_MAP
|
||||
|
||||
|
||||
ENV_FEATURE_PROFILE = "ALWRITY_FEATURE_TO_ENABLE"
|
||||
# Consolidated env var - supports both old and new format
|
||||
ENV_FEATURE_PROFILE = "ALWRITY_ENABLED_FEATURES"
|
||||
ENV_FEATURE_PROFILE_LEGACY = "ALWRITY_FEATURE_TO_ENABLE"
|
||||
DEFAULT_PROFILE = "all"
|
||||
|
||||
|
||||
@@ -22,7 +24,12 @@ class ExpandedFeatureProfile:
|
||||
|
||||
|
||||
class UnknownFeatureProfileError(ValueError):
|
||||
"""Raised when ALWRITY_FEATURE_TO_ENABLE contains unknown profile values."""
|
||||
"""Raised when ALWRITY_ENABLED_FEATURES contains unknown profile values."""
|
||||
|
||||
|
||||
def _get_env_value() -> str:
|
||||
"""Get the feature profile value from environment - new var takes precedence."""
|
||||
return os.getenv(ENV_FEATURE_PROFILE) or os.getenv(ENV_FEATURE_PROFILE_LEGACY) or DEFAULT_PROFILE
|
||||
|
||||
|
||||
def _normalize_values(raw_value: str | None) -> Tuple[str, ...]:
|
||||
@@ -44,11 +51,11 @@ def parse_feature_profiles(raw_value: str | None = None) -> Tuple[str, ...]:
|
||||
Raises UnknownFeatureProfileError when any profile is not registered.
|
||||
"""
|
||||
|
||||
selected_profiles = _normalize_values(raw_value if raw_value is not None else os.getenv(ENV_FEATURE_PROFILE))
|
||||
selected_profiles = _normalize_values(raw_value if raw_value is not None else _get_env_value())
|
||||
|
||||
unknown = sorted({profile for profile in selected_profiles if profile not in PROFILE_GROUP_MAP})
|
||||
unknown = sorted({profile for profile in selected_profiles if profile not in PROFILE_GROUP_MAP and profile not in FEATURE_GROUPS})
|
||||
if unknown:
|
||||
supported = ", ".join(sorted(PROFILE_GROUP_MAP))
|
||||
supported = ", ".join(sorted(set(PROFILE_GROUP_MAP.keys()) | set(FEATURE_GROUPS.keys())))
|
||||
unknown_display = ", ".join(unknown)
|
||||
raise UnknownFeatureProfileError(
|
||||
f"Unknown {ENV_FEATURE_PROFILE} value(s): {unknown_display}. Supported profiles: {supported}."
|
||||
@@ -64,14 +71,18 @@ def _dedupe_stable(items: Iterable[str]) -> Tuple[str, ...]:
|
||||
def expand_profiles(profiles: Tuple[str, ...]) -> ExpandedFeatureProfile:
|
||||
"""Expand profile names into a deduplicated group list."""
|
||||
|
||||
# Handle "all" specially - include all groups
|
||||
if "all" in profiles:
|
||||
return ExpandedFeatureProfile(profiles=("all",), groups=tuple(FEATURE_GROUPS.keys()))
|
||||
|
||||
# Otherwise expand via PROFILE_GROUP_MAP
|
||||
groups = _dedupe_stable(
|
||||
group
|
||||
for profile in profiles
|
||||
for group in PROFILE_GROUP_MAP[profile]
|
||||
for group in PROFILE_GROUP_MAP.get(profile, (profile,))
|
||||
)
|
||||
|
||||
missing_groups = sorted({group for group in groups if group not in FEATURE_GROUPS})
|
||||
if missing_groups:
|
||||
raise RuntimeError(f"Profile mapping references unknown groups: {', '.join(missing_groups)}")
|
||||
# Include FEATURE_GROUPS keys directly
|
||||
all_groups = _dedupe_stable(list(groups) + [g for g in groups if g in FEATURE_GROUPS])
|
||||
|
||||
return ExpandedFeatureProfile(profiles=profiles, groups=groups)
|
||||
return ExpandedFeatureProfile(profiles=profiles, groups=all_groups)
|
||||
|
||||
@@ -13,57 +13,57 @@ from loguru import logger
|
||||
|
||||
|
||||
CORE_ROUTER_REGISTRY = [
|
||||
{"name": "component_logic", "module": "api.component_logic", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "subscription", "module": "api.subscription", "attr": "router", "profiles": {"all", "default", "podcast"}},
|
||||
{"name": "step3_research", "module": "api.onboarding_utils.step3_routes", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "step4_assets", "module": "api.onboarding_utils.step4_asset_routes", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "step4_persona", "module": "api.onboarding_utils.step4_persona_routes_optimized", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "gsc_auth", "module": "routers.gsc_auth", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "wordpress_oauth", "module": "routers.wordpress_oauth", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "bing_oauth", "module": "routers.bing_oauth", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "bing_analytics", "module": "routers.bing_analytics", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "bing_analytics_storage", "module": "routers.bing_analytics_storage", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "seo_tools", "module": "routers.seo_tools", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "facebook_writer", "module": "api.facebook_writer.routers", "attr": "facebook_router", "profiles": {"all", "default"}},
|
||||
{"name": "linkedin", "module": "routers.linkedin", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "linkedin_image", "module": "api.linkedin_image_generation", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "brainstorm", "module": "api.brainstorm", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "hallucination_detector", "module": "api.hallucination_detector", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "writing_assistant", "module": "api.writing_assistant", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "content_planning", "module": "api.content_planning.api.router", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "user_data", "module": "api.user_data", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "user_environment", "module": "api.user_environment", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "strategy_copilot", "module": "api.content_planning.strategy_copilot", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "error_logging", "module": "routers.error_logging", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "frontend_env_manager", "module": "routers.frontend_env_manager", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "platform_analytics", "module": "routers.platform_analytics", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "bing_insights", "module": "routers.bing_insights", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "background_jobs", "module": "routers.background_jobs", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "component_logic", "module": "api.component_logic", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "subscription", "module": "api.subscription", "attr": "router", "features": {"all", "core", "podcast", "blog-writer", "youtube"}},
|
||||
{"name": "step3_research", "module": "api.onboarding_utils.step3_routes", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "step4_assets", "module": "api.onboarding_utils.step4_asset_routes", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "step4_persona", "module": "api.onboarding_utils.step4_persona_routes_optimized", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "gsc_auth", "module": "routers.gsc_auth", "attr": "router", "features": {"all", "core", "seo"}},
|
||||
{"name": "wordpress_oauth", "module": "routers.wordpress_oauth", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "bing_oauth", "module": "routers.bing_oauth", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "bing_analytics", "module": "routers.bing_analytics", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "bing_analytics_storage", "module": "routers.bing_analytics_storage", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "seo_tools", "module": "routers.seo_tools", "attr": "router", "features": {"all", "core", "seo"}},
|
||||
{"name": "facebook_writer", "module": "api.facebook_writer.routers", "attr": "facebook_router", "features": {"all", "core", "facebook"}},
|
||||
{"name": "linkedin", "module": "routers.linkedin", "attr": "router", "features": {"all", "core", "linkedin"}},
|
||||
{"name": "linkedin_image", "module": "api.linkedin_image_generation", "attr": "router", "features": {"all", "core", "linkedin"}},
|
||||
{"name": "brainstorm", "module": "api.brainstorm", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "hallucination_detector", "module": "api.hallucination_detector", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "writing_assistant", "module": "api.writing_assistant", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "content_planning", "module": "api.content_planning.api.router", "attr": "router", "features": {"all", "core", "content-planning"}},
|
||||
{"name": "user_data", "module": "api.user_data", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "user_environment", "module": "api.user_environment", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "strategy_copilot", "module": "api.content_planning.strategy_copilot", "attr": "router", "features": {"all", "core", "content-planning"}},
|
||||
{"name": "error_logging", "module": "routers.error_logging", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "frontend_env_manager", "module": "routers.frontend_env_manager", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "platform_analytics", "module": "routers.platform_analytics", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "bing_insights", "module": "routers.bing_insights", "attr": "router", "features": {"all", "core", "seo"}},
|
||||
{"name": "background_jobs", "module": "routers.background_jobs", "attr": "router", "features": {"all", "core"}},
|
||||
]
|
||||
|
||||
OPTIONAL_ROUTER_REGISTRY = [
|
||||
{"name": "blog_writer", "module": "api.blog_writer.router", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "story_writer", "module": "api.story_writer.router", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "wix", "module": "api.wix_routes", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "blog_seo_analysis", "module": "api.blog_writer.seo_analysis", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "persona", "module": "api.persona_routes", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "video_studio", "module": "api.video_studio.router", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "stability", "module": "routers.stability", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "stability_advanced", "module": "routers.stability_advanced", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "stability_admin", "module": "routers.stability_admin", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "images", "module": "api.images", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "image_studio", "module": "routers.image_studio", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "product_marketing", "module": "routers.product_marketing", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "campaign_creator", "module": "routers.campaign_creator", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "content_assets", "module": "api.content_assets.router", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "podcast", "module": "api.podcast.router", "attr": "router", "profiles": {"all", "default", "podcast"}},
|
||||
{"name": "youtube", "module": "api.youtube.router", "attr": "router", "profiles": {"all", "default"}, "include_kwargs": {"prefix": "/api"}},
|
||||
{"name": "research_config", "module": "api.research_config", "attr": "router", "profiles": {"all", "default"}, "include_kwargs": {"prefix": "/api/research", "tags": ["research"]}},
|
||||
{"name": "research_engine", "module": "api.research.router", "attr": "router", "profiles": {"all", "default"}, "include_kwargs": {"tags": ["Research Engine"]}},
|
||||
{"name": "scheduler_dashboard", "module": "api.scheduler_dashboard", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "oauth_token_monitoring", "module": "api.oauth_token_monitoring_routes", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "agents", "module": "api.agents_api", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "today_workflow", "module": "api.today_workflow", "attr": "router", "profiles": {"all", "default"}},
|
||||
{"name": "blog_writer", "module": "api.blog_writer.router", "attr": "router", "features": {"all", "blog-writer"}},
|
||||
{"name": "story_writer", "module": "api.story_writer.router", "attr": "router", "features": {"all", "story-writer"}},
|
||||
{"name": "wix", "module": "api.wix_routes", "attr": "router", "features": {"all"}},
|
||||
{"name": "blog_seo_analysis", "module": "api.blog_writer.seo_analysis", "attr": "router", "features": {"all", "blog-writer"}},
|
||||
{"name": "persona", "module": "api.persona_routes", "attr": "router", "features": {"all", "persona"}},
|
||||
{"name": "video_studio", "module": "api.video_studio.router", "attr": "router", "features": {"all", "video-studio"}},
|
||||
{"name": "stability", "module": "routers.stability", "attr": "router", "features": {"all", "image-studio"}},
|
||||
{"name": "stability_advanced", "module": "routers.stability_advanced", "attr": "router", "features": {"all", "image-studio"}},
|
||||
{"name": "stability_admin", "module": "routers.stability_admin", "attr": "router", "features": {"all", "image-studio"}},
|
||||
{"name": "images", "module": "api.images", "attr": "router", "features": {"all", "image-studio"}},
|
||||
{"name": "image_studio", "module": "routers.image_studio", "attr": "router", "features": {"all", "image-studio"}},
|
||||
{"name": "product_marketing", "module": "routers.product_marketing", "attr": "router", "features": {"all", "product-marketing"}},
|
||||
{"name": "campaign_creator", "module": "routers.campaign_creator", "attr": "router", "features": {"all"}},
|
||||
{"name": "content_assets", "module": "api.content_assets.router", "attr": "router", "features": {"all"}},
|
||||
{"name": "podcast", "module": "api.podcast.router", "attr": "router", "features": {"all", "podcast"}},
|
||||
{"name": "youtube", "module": "api.youtube.router", "attr": "router", "features": {"all", "youtube"}, "include_kwargs": {"prefix": "/api"}},
|
||||
{"name": "research_config", "module": "api.research_config", "attr": "router", "features": {"all", "research"}, "include_kwargs": {"prefix": "/api/research", "tags": ["research"]}},
|
||||
{"name": "research_engine", "module": "api.research.router", "attr": "router", "features": {"all", "research"}, "include_kwargs": {"tags": ["Research Engine"]}},
|
||||
{"name": "scheduler_dashboard", "module": "api.scheduler_dashboard", "attr": "router", "features": {"all", "scheduler"}},
|
||||
{"name": "oauth_token_monitoring", "module": "api.oauth_token_monitoring_routes", "attr": "router", "features": {"all", "core"}},
|
||||
{"name": "agents", "module": "api.agents_api", "attr": "router", "features": {"all"}},
|
||||
{"name": "today_workflow", "module": "api.today_workflow", "attr": "router", "features": {"all"}},
|
||||
]
|
||||
|
||||
OPTIONAL_MODULE_MATRIX = {
|
||||
@@ -81,15 +81,50 @@ class RouterManager:
|
||||
self.failed_routers = []
|
||||
self.skipped_routers = []
|
||||
|
||||
@staticmethod
|
||||
def get_enabled_features() -> set:
|
||||
"""Get enabled features from environment variable.
|
||||
|
||||
ALWRITY_ENABLED_FEATURES can be:
|
||||
- "all" - enable all features (default)
|
||||
- comma-separated list: "podcast,blog-writer,youtube"
|
||||
- single feature: "podcast"
|
||||
"""
|
||||
env_value = os.getenv(
|
||||
"ALWRITY_ENABLED_FEATURES",
|
||||
os.getenv("ALWRITY_FEATURE_PROFILE", os.getenv("ALWRITY_ROUTER_PROFILE", "all"))
|
||||
).strip().lower()
|
||||
|
||||
if not env_value or env_value == "all":
|
||||
return {"all"}
|
||||
|
||||
return {f.strip() for f in env_value.split(",") if f.strip()}
|
||||
|
||||
def _is_verbose(self) -> bool:
|
||||
return os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
|
||||
|
||||
def _get_profile(self) -> str:
|
||||
return os.getenv("ALWRITY_FEATURE_PROFILE", os.getenv("ALWRITY_ROUTER_PROFILE", os.getenv("ALWRITY_FEATURE_TO_ENABLE", "all"))).strip().lower() or "all"
|
||||
"""Legacy method - returns primary profile."""
|
||||
enabled = self.get_enabled_features()
|
||||
if "all" in enabled:
|
||||
return "all"
|
||||
# Return first feature as profile for backwards compatibility
|
||||
return list(enabled)[0] if enabled else "all"
|
||||
|
||||
def _should_include_router(self, registry_entry: Dict[str, Any], profile: str) -> bool:
|
||||
profiles = registry_entry.get("profiles", {"all", "default"})
|
||||
return profile in profiles or profile in {"all", "default"}
|
||||
def _should_include_router(self, registry_entry: Dict[str, Any], enabled_features: set) -> bool:
|
||||
"""Check if router should be included based on enabled features."""
|
||||
required_features = registry_entry.get("features", set())
|
||||
|
||||
# If "all" is enabled, include everything
|
||||
if "all" in enabled_features:
|
||||
return True
|
||||
|
||||
# If no required features specified, include by default
|
||||
if not required_features:
|
||||
return True
|
||||
|
||||
# Check if any required feature is enabled
|
||||
return bool(required_features & enabled_features)
|
||||
|
||||
def _load_router_from_registry(self, registry_entry: Dict[str, Any]):
|
||||
module = import_module(registry_entry["module"])
|
||||
@@ -120,15 +155,15 @@ class RouterManager:
|
||||
|
||||
def _include_registry_group(self, registry: List[Dict[str, Any]], group_name: str) -> bool:
|
||||
verbose = self._is_verbose()
|
||||
profile = self._get_profile()
|
||||
enabled_features = self.get_enabled_features()
|
||||
|
||||
try:
|
||||
if verbose:
|
||||
logger.info(f"Including {group_name} routers for profile '{profile}'...")
|
||||
logger.info(f"Including {group_name} routers with features: {enabled_features}...")
|
||||
|
||||
for entry in registry:
|
||||
if not self._should_include_router(entry, profile):
|
||||
reason = f"profile '{profile}' not in {entry.get('profiles', set())}"
|
||||
if not self._should_include_router(entry, enabled_features):
|
||||
reason = f"features {enabled_features} not matching {entry.get('features', set())}"
|
||||
self.skipped_routers.append({"name": entry["name"], "reason": reason})
|
||||
if verbose:
|
||||
logger.info(f"⏭️ Skipping {entry['name']}: {reason}")
|
||||
@@ -140,7 +175,7 @@ class RouterManager:
|
||||
except Exception as e:
|
||||
logger.warning(f"{entry['name']} router not mounted: {e}")
|
||||
|
||||
logger.info(f"✅ {group_name.capitalize()} routers processed for profile '{profile}'")
|
||||
logger.info(f"✅ {group_name.capitalize()} routers processed for features: {enabled_features}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -59,18 +59,47 @@ def _env_flag_enabled(*env_names: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
PODCAST_ONLY_DEMO_MODE = _env_flag_enabled(
|
||||
"ALWRITY_PODCAST_ONLY_DEMO_MODE",
|
||||
"PODCAST_ONLY_DEMO_MODE",
|
||||
)
|
||||
def get_enabled_features() -> set:
|
||||
"""Get enabled features from environment variable.
|
||||
|
||||
ALWRITY_ENABLED_FEATURES can be:
|
||||
- "all" - enable all features (default)
|
||||
- comma-separated list: "podcast,core"
|
||||
- single feature: "podcast"
|
||||
"""
|
||||
env_value = os.getenv(
|
||||
"ALWRITY_ENABLED_FEATURES",
|
||||
os.getenv("ALWRITY_FEATURE_PROFILE", os.getenv("ALWRITY_ROUTER_PROFILE", "all"))
|
||||
).strip().lower()
|
||||
|
||||
if not env_value or env_value == "all":
|
||||
return {"all"}
|
||||
|
||||
return {f.strip() for f in env_value.split(",") if f.strip()}
|
||||
|
||||
|
||||
def is_podcast_only_demo_mode() -> bool:
|
||||
return PODCAST_ONLY_DEMO_MODE
|
||||
"""Check if podcast-only mode is enabled via new or legacy flags."""
|
||||
# First check the new consolidated flag
|
||||
enabled = get_enabled_features()
|
||||
if "podcast" in enabled and "all" not in enabled:
|
||||
return True
|
||||
|
||||
# Fall back to legacy flags for backwards compatibility
|
||||
return _env_flag_enabled(
|
||||
"ALWRITY_PODCAST_ONLY_DEMO_MODE",
|
||||
"PODCAST_ONLY_DEMO_MODE"
|
||||
)
|
||||
|
||||
|
||||
def should_include_non_podcast_features() -> bool:
|
||||
return not is_podcast_only_demo_mode()
|
||||
"""Check if non-podcast features should be included."""
|
||||
enabled = get_enabled_features()
|
||||
return "all" in enabled or "core" in enabled or "blog-writer" in enabled
|
||||
|
||||
|
||||
# Legacy constant for backwards compatibility - prefer using get_enabled_features()
|
||||
PODCAST_ONLY_DEMO_MODE = is_podcast_only_demo_mode()
|
||||
|
||||
|
||||
# Set up clean logging for end users
|
||||
|
||||
@@ -26,42 +26,63 @@ class BootstrapResult:
|
||||
LINGUISTIC_REQUIRED_FEATURES = {"content_planning", "strategy_copilot", "facebook", "linkedin", "blog_writer", "persona"}
|
||||
|
||||
|
||||
def get_enabled_features() -> set:
|
||||
"""Get enabled features from environment variable.
|
||||
|
||||
ALWRITY_ENABLED_FEATURES can be:
|
||||
- "all" - enable all features (default)
|
||||
- comma-separated list: "podcast,blog-writer,youtube"
|
||||
- single feature: "podcast"
|
||||
"""
|
||||
env_value = os.getenv(
|
||||
"ALWRITY_ENABLED_FEATURES",
|
||||
os.getenv("ALWRITY_FEATURE_PROFILE", os.getenv("ALWRITY_ROUTER_PROFILE", "all"))
|
||||
).strip().lower()
|
||||
|
||||
if not env_value or env_value == "all":
|
||||
return {"all"}
|
||||
|
||||
return {f.strip() for f in env_value.split(",") if f.strip()}
|
||||
|
||||
|
||||
def get_active_profile() -> str:
|
||||
"""Get active profile from environment variables."""
|
||||
return os.getenv("ALWRITY_ACTIVE_PROFILE", os.getenv("ALWRITY_PROFILE", os.getenv("ALWRITY_FEATURE_PROFILE", os.getenv("ALWRITY_ROUTER_PROFILE", os.getenv("ALWRITY_FEATURE_TO_ENABLE", "all"))))).strip().lower() or "all"
|
||||
|
||||
|
||||
def get_loaded_features() -> set:
|
||||
"""Get loaded features from environment variables."""
|
||||
features_str = os.getenv("ALWRITY_LOADED_FEATURES", os.getenv("ALWRITY_ENABLED_FEATURES", os.getenv("ALWRITY_FEATURES", "")))
|
||||
if not features_str:
|
||||
return set()
|
||||
return {f.strip().lower() for f in features_str.split(",") if f.strip()}
|
||||
"""Legacy function - returns primary profile for backwards compatibility."""
|
||||
enabled = get_enabled_features()
|
||||
if "all" in enabled:
|
||||
return "all"
|
||||
return list(enabled)[0] if enabled else "all"
|
||||
|
||||
|
||||
def should_bootstrap_linguistic_models() -> bool:
|
||||
"""Decide whether to bootstrap linguistic models based on profile."""
|
||||
profile = get_active_profile()
|
||||
"""Decide whether to bootstrap linguistic models based on enabled features."""
|
||||
enabled_features = get_enabled_features()
|
||||
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
|
||||
|
||||
if profile in {"all", "default"}:
|
||||
if "all" in enabled_features:
|
||||
return True
|
||||
|
||||
loaded_features = get_loaded_features()
|
||||
if not loaded_features:
|
||||
return False
|
||||
# Map old profile names to features for backwards compatibility
|
||||
feature_mapping = {
|
||||
"podcast": "podcast",
|
||||
"youtube": "youtube",
|
||||
"planning": "content-planning",
|
||||
"default": "all"
|
||||
}
|
||||
|
||||
return bool(loaded_features & LINGUISTIC_REQUIRED_FEATURES)
|
||||
# Check if any linguistic-required feature is enabled
|
||||
linguistic_features = {"content_planning", "facebook", "linkedin", "blog-writer", "persona"}
|
||||
return bool(enabled_features & linguistic_features)
|
||||
|
||||
|
||||
def should_bootstrap_local_llm_models() -> bool:
|
||||
"""Decide whether to bootstrap local LLM models based on profile."""
|
||||
profile = get_active_profile()
|
||||
"""Decide whether to bootstrap local LLM models based on enabled features."""
|
||||
enabled_features = get_enabled_features()
|
||||
|
||||
if profile in {"all", "default"}:
|
||||
if "all" in enabled_features:
|
||||
return True
|
||||
|
||||
return profile not in {"podcast", "youtube", "planning"}
|
||||
# Skip LLM bootstrap for lean deployments
|
||||
return "core" in enabled_features or "podcast" in enabled_features
|
||||
|
||||
|
||||
def bootstrap_linguistic_models() -> BootstrapResult:
|
||||
|
||||
@@ -1,47 +1,79 @@
|
||||
/**
|
||||
* Demo mode detection utilities for podcast-only demo mode.
|
||||
* Consolidated feature mode detection utilities.
|
||||
*
|
||||
* Uses ALWRITY_ENABLED_FEATURES (backend) / REACT_APP_ENABLED_FEATURES (frontend)
|
||||
* Format: "all" or comma-separated features: "podcast,core"
|
||||
*/
|
||||
|
||||
const DEMO_MODE_STORAGE_KEYS = [
|
||||
const ENABLED_FEATURES_STORAGE_KEYS = [
|
||||
'app_mode',
|
||||
'enabled_features',
|
||||
'demo_mode',
|
||||
'podcast_only_demo_mode',
|
||||
];
|
||||
|
||||
const DEMO_MODE_ENV_KEYS = [
|
||||
const ENABLED_FEATURES_ENV_KEYS = [
|
||||
'REACT_APP_ENABLED_FEATURES',
|
||||
'REACT_APP_APP_MODE',
|
||||
'REACT_APP_DEMO_MODE',
|
||||
'REACT_APP_PODCAST_ONLY_DEMO_MODE',
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if podcast-only demo mode is enabled.
|
||||
* Checks localStorage first, then environment variables.
|
||||
* Get enabled features from localStorage or environment.
|
||||
* Returns a set of enabled feature names.
|
||||
*/
|
||||
export function isPodcastOnlyDemoMode(): boolean {
|
||||
// Check localStorage
|
||||
for (const key of DEMO_MODE_STORAGE_KEYS) {
|
||||
const value = (localStorage.getItem(key) || '').toLowerCase();
|
||||
if (value === 'true' || value === 'podcast-only' || value === 'podcast_only') {
|
||||
return true;
|
||||
export function getEnabledFeatures(): Set<string> {
|
||||
// Check localStorage first
|
||||
for (const key of ENABLED_FEATURES_STORAGE_KEYS) {
|
||||
const value = localStorage.getItem(key);
|
||||
if (value) {
|
||||
const features = value.toLowerCase().split(',').map(f => f.trim());
|
||||
if (features.includes('all')) {
|
||||
return new Set(['all']);
|
||||
}
|
||||
return new Set(features.filter(f => f));
|
||||
}
|
||||
}
|
||||
|
||||
// Check environment variables
|
||||
for (const key of DEMO_MODE_ENV_KEYS) {
|
||||
const value = (process.env[key] || '').toLowerCase();
|
||||
if (value === 'true' || value === 'podcast-only' || value === 'podcast_only') {
|
||||
return true;
|
||||
for (const key of ENABLED_FEATURES_ENV_KEYS) {
|
||||
const value = process.env[key];
|
||||
if (value) {
|
||||
const features = value.toLowerCase().split(',').map(f => f.trim());
|
||||
if (features.includes('all')) {
|
||||
return new Set(['all']);
|
||||
}
|
||||
return new Set(features.filter(f => f));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
// Default: all features enabled
|
||||
return new Set(['all']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific feature is enabled.
|
||||
*/
|
||||
export function isFeatureEnabled(feature: string): boolean {
|
||||
const enabled = getEnabledFeatures();
|
||||
return enabled.has('all') || enabled.has(feature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if podcast-only mode is enabled.
|
||||
* Returns true when podcast is enabled but not "all".
|
||||
*/
|
||||
export function isPodcastOnlyDemoMode(): boolean {
|
||||
const enabled = getEnabledFeatures();
|
||||
return enabled.has('podcast') && !enabled.has('all');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the app should skip onboarding entirely.
|
||||
* Returns true in podcast-only demo mode.
|
||||
* Returns true in podcast-only demo mode or when not using all features.
|
||||
*/
|
||||
export function shouldSkipOnboarding(): boolean {
|
||||
return isPodcastOnlyDemoMode();
|
||||
const enabled = getEnabledFeatures();
|
||||
return enabled.has('podcast') || !enabled.has('all');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user