Consolidate on ALWRITY_ENABLED_FEATURES - remove all legacy support

Backend:
- Remove all legacy env var fallbacks (ALWRITY_FEATURE_PROFILE, ALWRITY_ROUTER_PROFILE, etc)
- Remove get_active_profile() from start_alwrity_backend.py
- Remove _env_flag_enabled() from app.py
- Use ALWRITY_ENABLED_FEATURES as single source of truth

Frontend:
- demoMode.ts now uses only REACT_APP_ENABLED_FEATURES
- Removed all legacy fallback keys (app_mode, demo_mode, podcast_only_demo_mode)

Usage:
  ALWRITY_ENABLED_FEATURES=podcast     # Podcast only
  ALWRITY_ENABLED_FEATURES=all        # All features (default)
This commit is contained in:
ajaysi
2026-03-31 18:51:30 +05:30
parent edd92ec85b
commit 49e0ee8e9e
5 changed files with 42 additions and 94 deletions

View File

@@ -9,10 +9,8 @@ from typing import Iterable, Tuple
from .feature_registry import FEATURE_GROUPS, PROFILE_GROUP_MAP from .feature_registry import FEATURE_GROUPS, PROFILE_GROUP_MAP
# Consolidated env var - supports both old and new format ENV_ENABLED_FEATURES = "ALWRITY_ENABLED_FEATURES"
ENV_FEATURE_PROFILE = "ALWRITY_ENABLED_FEATURES" DEFAULT_FEATURES = "all"
ENV_FEATURE_PROFILE_LEGACY = "ALWRITY_FEATURE_TO_ENABLE"
DEFAULT_PROFILE = "all"
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -24,31 +22,31 @@ class ExpandedFeatureProfile:
class UnknownFeatureProfileError(ValueError): class UnknownFeatureProfileError(ValueError):
"""Raised when ALWRITY_ENABLED_FEATURES contains unknown profile values.""" """Raised when ALWRITY_ENABLED_FEATURES contains unknown feature values."""
def _get_env_value() -> str: def _get_env_value() -> str:
"""Get the feature profile value from environment - new var takes precedence.""" """Get the enabled features value from environment."""
return os.getenv(ENV_FEATURE_PROFILE) or os.getenv(ENV_FEATURE_PROFILE_LEGACY) or DEFAULT_PROFILE return os.getenv(ENV_ENABLED_FEATURES) or DEFAULT_FEATURES
def _normalize_values(raw_value: str | None) -> Tuple[str, ...]: def _normalize_values(raw_value: str | None) -> Tuple[str, ...]:
if not raw_value or not raw_value.strip(): if not raw_value or not raw_value.strip():
return (DEFAULT_PROFILE,) return (DEFAULT_FEATURES,)
normalized = tuple( normalized = tuple(
value.strip().lower() value.strip().lower()
for value in raw_value.split(",") for value in raw_value.split(",")
if value.strip() if value.strip()
) )
return normalized or (DEFAULT_PROFILE,) return normalized or (DEFAULT_FEATURES,)
def parse_feature_profiles(raw_value: str | None = None) -> Tuple[str, ...]: def parse_feature_profiles(raw_value: str | None = None) -> Tuple[str, ...]:
"""Parse and validate profile names from env/raw input. """Parse and validate feature names from env/raw input.
Supports comma-separated profile names, e.g. `core,podcast`. Supports comma-separated feature names, e.g. `podcast,core`.
Raises UnknownFeatureProfileError when any profile is not registered. Raises UnknownFeatureProfileError when any feature is not registered.
""" """
selected_profiles = _normalize_values(raw_value if raw_value is not None else _get_env_value()) selected_profiles = _normalize_values(raw_value if raw_value is not None else _get_env_value())
@@ -58,7 +56,7 @@ def parse_feature_profiles(raw_value: str | None = None) -> Tuple[str, ...]:
supported = ", ".join(sorted(set(PROFILE_GROUP_MAP.keys()) | set(FEATURE_GROUPS.keys()))) supported = ", ".join(sorted(set(PROFILE_GROUP_MAP.keys()) | set(FEATURE_GROUPS.keys())))
unknown_display = ", ".join(unknown) unknown_display = ", ".join(unknown)
raise UnknownFeatureProfileError( raise UnknownFeatureProfileError(
f"Unknown {ENV_FEATURE_PROFILE} value(s): {unknown_display}. Supported profiles: {supported}." f"Unknown {ENV_ENABLED_FEATURES} value(s): {unknown_display}. Supported: {supported}."
) )
return selected_profiles return selected_profiles

View File

@@ -89,8 +89,6 @@ class RouterManager:
- "all" - enable all features (default) - "all" - enable all features (default)
- comma-separated: "podcast,blog-writer,youtube" - comma-separated: "podcast,blog-writer,youtube"
- single feature: "podcast" - single feature: "podcast"
DEPRECATED: ALWRITY_FEATURE_PROFILE, ALWRITY_ROUTER_PROFILE, ALWRITY_FEATURE_TO_ENABLE
""" """
env_value = os.getenv("ALWRITY_ENABLED_FEATURES", "all").strip().lower() env_value = os.getenv("ALWRITY_ENABLED_FEATURES", "all").strip().lower()

View File

@@ -49,16 +49,6 @@ load_dotenv(project_root / '.env') # root .env (fallback)
load_dotenv() # CWD .env (fallback) load_dotenv() # CWD .env (fallback)
def _env_flag_enabled(*env_names: str) -> bool:
"""Return True when any provided env var is set to a truthy value."""
truthy_values = {"1", "true", "yes", "on"}
for env_name in env_names:
value = os.getenv(env_name)
if value and value.strip().lower() in truthy_values:
return True
return False
def get_enabled_features() -> set: def get_enabled_features() -> set:
"""Get enabled features from ALWRITY_ENABLED_FEATURES env var. """Get enabled features from ALWRITY_ENABLED_FEATURES env var.
@@ -66,8 +56,6 @@ def get_enabled_features() -> set:
- "all" - enable all features (default) - "all" - enable all features (default)
- comma-separated: "podcast,core" - comma-separated: "podcast,core"
- single feature: "podcast" - single feature: "podcast"
DEPRECATED: ALWRITY_FEATURE_PROFILE, ALWRITY_ROUTER_PROFILE, ALWRITY_FEATURE_TO_ENABLE
""" """
env_value = os.getenv("ALWRITY_ENABLED_FEATURES", "all").strip().lower() env_value = os.getenv("ALWRITY_ENABLED_FEATURES", "all").strip().lower()
@@ -78,26 +66,18 @@ def get_enabled_features() -> set:
def is_podcast_only_demo_mode() -> bool: def is_podcast_only_demo_mode() -> bool:
"""Check if podcast-only mode is enabled via new or legacy flags.""" """Check if podcast-only mode is enabled."""
# First check the new consolidated flag
enabled = get_enabled_features() enabled = get_enabled_features()
if "podcast" in enabled and "all" not in enabled: return "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: def should_include_non_podcast_features() -> bool:
"""Check if non-podcast features should be included.""" """Check if non-podcast features should be included."""
enabled = get_enabled_features() enabled = get_enabled_features()
return "all" in enabled or "core" in enabled or "blog-writer" in enabled return "all" in enabled or "core" in enabled
# Legacy constant for backwards compatibility - prefer using get_enabled_features() # Legacy constant for backwards compatibility
PODCAST_ONLY_DEMO_MODE = is_podcast_only_demo_mode() PODCAST_ONLY_DEMO_MODE = is_podcast_only_demo_mode()

View File

@@ -33,8 +33,6 @@ def get_enabled_features() -> set:
- "all" - enable all features (default) - "all" - enable all features (default)
- comma-separated: "podcast,blog-writer,youtube" - comma-separated: "podcast,blog-writer,youtube"
- single feature: "podcast" - single feature: "podcast"
DEPRECATED: ALWRITY_FEATURE_PROFILE, ALWRITY_ROUTER_PROFILE, ALWRITY_FEATURE_TO_ENABLE
""" """
env_value = os.getenv("ALWRITY_ENABLED_FEATURES", "all").strip().lower() env_value = os.getenv("ALWRITY_ENABLED_FEATURES", "all").strip().lower()
@@ -44,14 +42,6 @@ def get_enabled_features() -> set:
return {f.strip() for f in env_value.split(",") if f.strip()} return {f.strip() for f in env_value.split(",") if f.strip()}
def get_active_profile() -> str:
"""Legacy function - use get_enabled_features() instead."""
enabled = get_enabled_features()
if "all" in enabled:
return "all"
return list(enabled)[0] if enabled else "all"
def should_bootstrap_linguistic_models() -> bool: def should_bootstrap_linguistic_models() -> bool:
"""Decide whether to bootstrap linguistic models based on enabled features.""" """Decide whether to bootstrap linguistic models based on enabled features."""
enabled_features = get_enabled_features() enabled_features = get_enabled_features()
@@ -220,10 +210,11 @@ def bootstrap_local_llm_models() -> BootstrapResult:
BOOTSTRAP_RESULTS = [] BOOTSTRAP_RESULTS = []
if __name__ == "__main__": if __name__ == "__main__":
profile = get_active_profile() enabled_features = get_enabled_features()
os.environ["ALWRITY_ACTIVE_PROFILE"] = profile features_str = ",".join(sorted(enabled_features))
os.environ["ALWRITY_ENABLED_FEATURES"] = features_str
print(f"\n📋 Active profile: {profile}") print(f"\n📋 Enabled features: {features_str}")
if should_bootstrap_linguistic_models(): if should_bootstrap_linguistic_models():
result = bootstrap_linguistic_models() result = bootstrap_linguistic_models()
@@ -240,11 +231,11 @@ if __name__ == "__main__":
else: else:
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true" verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
if verbose: if verbose:
print("⏭️ Skipping local LLM model bootstrap (profile-gated)") print("⏭️ Skipping local LLM model bootstrap (feature-gated)")
BOOTSTRAP_RESULTS.append(BootstrapResult(name="local_llm_models", success=True, skipped=True, reason="profile_gated")) BOOTSTRAP_RESULTS.append(BootstrapResult(name="local_llm_models", success=True, skipped=True, reason="feature_gated"))
summary = { summary = {
"active_profile": profile, "enabled_features": features_str,
"bootstraps": [asdict(r) for r in BOOTSTRAP_RESULTS] "bootstraps": [asdict(r) for r in BOOTSTRAP_RESULTS]
} }
os.environ["ALWRITY_BOOTSTRAP_SUMMARY"] = json.dumps(summary) os.environ["ALWRITY_BOOTSTRAP_SUMMARY"] = json.dumps(summary)

View File

@@ -1,56 +1,37 @@
/** /**
* Consolidated feature mode detection utilities. * Consolidated feature mode detection utilities.
* *
* Primary: REACT_APP_ENABLED_FEATURES (format: "all" or "podcast,core") * Primary env var: REACT_APP_ENABLED_FEATURES
* * Format: "all" or comma-separated: "podcast,core"
* DEPRECATED (fallback order):
* - REACT_APP_APP_MODE
* - REACT_APP_DEMO_MODE
* - REACT_APP_PODCAST_ONLY_DEMO_MODE
*/ */
const ENABLED_FEATURES_STORAGE_KEYS = [ const PRIMARY_STORAGE_KEY = 'enabled_features';
'enabled_features', // Primary const PRIMARY_ENV_KEY = 'REACT_APP_ENABLED_FEATURES';
'app_mode',
'demo_mode',
'podcast_only_demo_mode',
];
const ENABLED_FEATURES_ENV_KEYS = [
'REACT_APP_ENABLED_FEATURES', // Primary - use this!
'REACT_APP_APP_MODE', // DEPRECATED
'REACT_APP_DEMO_MODE', // DEPRECATED
'REACT_APP_PODCAST_ONLY_DEMO_MODE', // DEPRECATED
];
/** /**
* Get enabled features from localStorage or environment. * Get enabled features from localStorage or environment.
* Returns a set of enabled feature names. * Returns a Set of enabled feature names.
*/ */
export function getEnabledFeatures(): Set<string> { export function getEnabledFeatures(): Set<string> {
// Check localStorage first // Check localStorage first
for (const key of ENABLED_FEATURES_STORAGE_KEYS) { const storageValue = localStorage.getItem(PRIMARY_STORAGE_KEY);
const value = localStorage.getItem(key); if (storageValue) {
if (value) { const features = storageValue.toLowerCase().split(',').map(f => f.trim());
const features = value.toLowerCase().split(',').map(f => f.trim());
if (features.includes('all')) { if (features.includes('all')) {
return new Set(['all']); return new Set(['all']);
} }
return new Set(features.filter(f => f)); return new Set(features.filter(f => f));
} }
}
// Check environment variables // Check environment variable
for (const key of ENABLED_FEATURES_ENV_KEYS) { const envValue = process.env[PRIMARY_ENV_KEY];
const value = process.env[key]; if (envValue) {
if (value) { const features = envValue.toLowerCase().split(',').map(f => f.trim());
const features = value.toLowerCase().split(',').map(f => f.trim());
if (features.includes('all')) { if (features.includes('all')) {
return new Set(['all']); return new Set(['all']);
} }
return new Set(features.filter(f => f)); return new Set(features.filter(f => f));
} }
}
// Default: all features enabled // Default: all features enabled
return new Set(['all']); return new Set(['all']);
@@ -74,8 +55,8 @@ export function isPodcastOnlyDemoMode(): boolean {
} }
/** /**
* Check if the app should skip onboarding entirely. * Check if the app should skip onboarding.
* Returns true in podcast-only demo mode or when not using all features. * Returns true in podcast-only mode.
*/ */
export function shouldSkipOnboarding(): boolean { export function shouldSkipOnboarding(): boolean {
const enabled = getEnabledFeatures(); const enabled = getEnabledFeatures();