mirror of https://github.com/kortix-ai/suna.git
rm flags
This commit is contained in:
parent
84c83f29d8
commit
764ab6bb1b
|
@ -130,108 +130,6 @@ REDIS_PASSWORD=
|
|||
|
||||
---
|
||||
|
||||
## Feature Flags
|
||||
|
||||
The backend includes a Redis-backed feature flag system that allows you to control feature availability without code deployments.
|
||||
|
||||
### Setup
|
||||
|
||||
The feature flag system uses the existing Redis service and is automatically available when Redis is running.
|
||||
|
||||
### CLI Management
|
||||
|
||||
Use the CLI tool to manage feature flags:
|
||||
|
||||
```bash
|
||||
cd backend/flags
|
||||
python setup.py <command> [arguments]
|
||||
```
|
||||
|
||||
#### Available Commands
|
||||
|
||||
**Enable a feature flag:**
|
||||
|
||||
```bash
|
||||
python setup.py enable test_flag "Test decsription"
|
||||
```
|
||||
|
||||
**Disable a feature flag:**
|
||||
|
||||
```bash
|
||||
python setup.py disable test_flag
|
||||
```
|
||||
|
||||
**List all feature flags:**
|
||||
|
||||
```bash
|
||||
python setup.py list
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
|
||||
Feature flags are accessible via REST API:
|
||||
|
||||
**Get all feature flags:**
|
||||
|
||||
```bash
|
||||
GET /feature-flags
|
||||
```
|
||||
|
||||
**Get specific feature flag:**
|
||||
|
||||
```bash
|
||||
GET /feature-flags/{flag_name}
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"test_flag": {
|
||||
"enabled": true,
|
||||
"description": "Test flag",
|
||||
"updated_at": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Backend Integration
|
||||
|
||||
Use feature flags in your Python code:
|
||||
|
||||
```python
|
||||
from flags.flags import is_enabled
|
||||
|
||||
# Check if a feature is enabled
|
||||
if await is_enabled('test_flag'):
|
||||
# Feature-specific logic
|
||||
pass
|
||||
|
||||
# With fallback value
|
||||
enabled = await is_enabled('new_feature', default=False)
|
||||
```
|
||||
|
||||
### Current Feature Flags
|
||||
|
||||
The system currently supports these feature flags:
|
||||
|
||||
- **`custom_agents`**: Controls custom agent creation and management
|
||||
- **`agent_marketplace`**: Controls agent marketplace functionality
|
||||
|
||||
### Error Handling
|
||||
|
||||
The feature flag system includes robust error handling:
|
||||
|
||||
- If Redis is unavailable, flags default to `False`
|
||||
- API endpoints return empty objects on Redis errors
|
||||
- CLI operations show clear error messages
|
||||
|
||||
### Caching
|
||||
|
||||
- Backend operations are direct Redis calls (no caching)
|
||||
- Frontend includes 5-minute caching for performance
|
||||
- Use `clearCache()` in frontend to force refresh
|
||||
|
||||
---
|
||||
|
||||
## Production Setup
|
||||
|
|
|
@ -5,7 +5,6 @@ from utils.auth_utils import get_current_user_id_from_jwt
|
|||
from utils.logger import logger
|
||||
from utils.config import config, EnvMode
|
||||
from utils.pagination import PaginationParams
|
||||
from flags.flags import is_enabled
|
||||
from models import model_manager
|
||||
|
||||
from ..models import (
|
||||
|
@ -24,11 +23,6 @@ async def update_agent(
|
|||
agent_data: AgentUpdateRequest,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("custom_agents"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Custom agent currently disabled. This feature is not available at the moment."
|
||||
)
|
||||
logger.debug(f"Updating agent {agent_id} for user: {user_id}")
|
||||
|
||||
# Debug logging for icon fields
|
||||
|
@ -443,11 +437,6 @@ async def update_agent(
|
|||
|
||||
@router.delete("/agents/{agent_id}")
|
||||
async def delete_agent(agent_id: str, user_id: str = Depends(get_current_user_id_from_jwt)):
|
||||
if not await is_enabled("custom_agents"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Custom agent currently disabled. This feature is not available at the moment."
|
||||
)
|
||||
logger.debug(f"Deleting agent: {agent_id}")
|
||||
client = await utils.db.client
|
||||
|
||||
|
@ -525,11 +514,6 @@ async def get_agents(
|
|||
tools: Optional[str] = Query(None, description="Comma-separated list of tools to filter by"),
|
||||
content_type: Optional[str] = Query(None, description="Content type filter: 'agents', 'templates', or None for agents only")
|
||||
):
|
||||
if not await is_enabled("custom_agents"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Custom agents currently disabled. This feature is not available at the moment."
|
||||
)
|
||||
try:
|
||||
from .agent_service import AgentService, AgentFilters
|
||||
|
||||
|
@ -587,11 +571,6 @@ async def get_agents(
|
|||
|
||||
@router.get("/agents/{agent_id}", response_model=AgentResponse)
|
||||
async def get_agent(agent_id: str, user_id: str = Depends(get_current_user_id_from_jwt)):
|
||||
if not await is_enabled("custom_agents"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Custom agents currently disabled. This feature is not available at the moment."
|
||||
)
|
||||
|
||||
logger.debug(f"Fetching agent {agent_id} for user: {user_id}")
|
||||
|
||||
|
@ -721,11 +700,6 @@ async def create_agent(
|
|||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
logger.debug(f"Creating new agent for user: {user_id}")
|
||||
if not await is_enabled("custom_agents"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Custom agents currently disabled. This feature is not available at the moment."
|
||||
)
|
||||
client = await utils.db.client
|
||||
|
||||
from ..utils import check_agent_count_limit
|
||||
|
|
|
@ -5,7 +5,6 @@ from uuid import uuid4
|
|||
|
||||
from utils.auth_utils import get_current_user_id_from_jwt
|
||||
from utils.logger import logger
|
||||
from flags.flags import is_enabled
|
||||
from templates.template_service import MCPRequirementValue, ConfigType, ProfileId, QualifiedName
|
||||
|
||||
from ..models import JsonAnalysisRequest, JsonAnalysisResponse, JsonImportRequestModel, JsonImportResponse
|
||||
|
@ -409,11 +408,6 @@ async def analyze_json_for_import(
|
|||
"""Analyze imported JSON to determine required credentials and configurations"""
|
||||
logger.debug(f"Analyzing JSON for import - user: {user_id}")
|
||||
|
||||
if not await is_enabled("custom_agents"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Custom agents currently disabled. This feature is not available at the moment."
|
||||
)
|
||||
|
||||
try:
|
||||
import_service = JsonImportService(utils.db)
|
||||
|
@ -433,11 +427,6 @@ async def import_agent_from_json(
|
|||
):
|
||||
logger.debug(f"Importing agent from JSON - user: {user_id}")
|
||||
|
||||
if not await is_enabled("custom_agents"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Custom agents currently disabled. This feature is not available at the moment."
|
||||
)
|
||||
|
||||
client = await utils.db.client
|
||||
from ..utils import check_agent_count_limit
|
||||
|
|
|
@ -6,7 +6,6 @@ from fastapi import APIRouter, HTTPException, Depends, Request, Body
|
|||
|
||||
from utils.auth_utils import get_current_user_id_from_jwt
|
||||
from utils.logger import logger
|
||||
from flags.flags import is_enabled
|
||||
from sandbox.sandbox import get_or_start_sandbox
|
||||
from services.supabase import DBConnection
|
||||
from agentpress.thread_manager import ThreadManager
|
||||
|
@ -319,8 +318,6 @@ async def get_agent_tools(
|
|||
agent_id: str,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("custom_agents"):
|
||||
raise HTTPException(status_code=403, detail="Custom agents currently disabled")
|
||||
|
||||
logger.debug(f"Fetching enabled tools for agent: {agent_id} by user: {user_id}")
|
||||
client = await utils.db.client
|
||||
|
|
|
@ -316,14 +316,14 @@ class PromptManager:
|
|||
# Construct a well-formatted knowledge base section
|
||||
kb_section = f"""
|
||||
|
||||
=== AGENT KNOWLEDGE BASE ===
|
||||
NOTICE: The following is your specialized knowledge base. This information should be considered authoritative for your responses and should take precedence over general knowledge when relevant.
|
||||
=== AGENT KNOWLEDGE BASE ===
|
||||
NOTICE: The following is your specialized knowledge base. This information should be considered authoritative for your responses and should take precedence over general knowledge when relevant.
|
||||
|
||||
{kb_result.data}
|
||||
{kb_result.data}
|
||||
|
||||
=== END AGENT KNOWLEDGE BASE ===
|
||||
=== END AGENT KNOWLEDGE BASE ===
|
||||
|
||||
IMPORTANT: Always reference and utilize the knowledge base information above when it's relevant to user queries. This knowledge is specific to your role and capabilities."""
|
||||
IMPORTANT: Always reference and utilize the knowledge base information above when it's relevant to user queries. This knowledge is specific to your role and capabilities."""
|
||||
|
||||
system_content += kb_section
|
||||
else:
|
||||
|
|
|
@ -195,11 +195,6 @@ Approach each research task methodically, starting with broad searches and then
|
|||
|
||||
client = await self.db.client
|
||||
|
||||
from flags.flags import is_enabled
|
||||
if not await is_enabled("custom_agents"):
|
||||
return self.fail_response(
|
||||
"Custom agents are currently disabled. This feature is not available at the moment."
|
||||
)
|
||||
|
||||
from agent.utils import check_agent_count_limit
|
||||
limit_check = await check_agent_count_limit(client, account_id)
|
||||
|
|
|
@ -23,7 +23,6 @@ from agent import api as agent_api
|
|||
|
||||
from sandbox import api as sandbox_api
|
||||
from services import billing as billing_api
|
||||
from flags import api as feature_flags_api
|
||||
from services import transcription as transcription_api
|
||||
import sys
|
||||
from services import email_api
|
||||
|
@ -159,7 +158,6 @@ api_router = APIRouter()
|
|||
api_router.include_router(agent_api.router)
|
||||
api_router.include_router(sandbox_api.router)
|
||||
api_router.include_router(billing_api.router)
|
||||
api_router.include_router(feature_flags_api.router)
|
||||
api_router.include_router(api_keys_api.router)
|
||||
|
||||
from mcp_module import api as mcp_api
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
from fastapi import APIRouter
|
||||
from utils.logger import logger
|
||||
from .flags import list_flags, is_enabled, get_flag_details
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/feature-flags")
|
||||
async def get_feature_flags():
|
||||
try:
|
||||
flags = await list_flags()
|
||||
return {"flags": flags}
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching feature flags: {str(e)}")
|
||||
return {"flags": {}}
|
||||
|
||||
@router.get("/feature-flags/{flag_name}")
|
||||
async def get_feature_flag(flag_name: str):
|
||||
try:
|
||||
enabled = await is_enabled(flag_name)
|
||||
details = await get_flag_details(flag_name)
|
||||
return {
|
||||
"flag_name": flag_name,
|
||||
"enabled": enabled,
|
||||
"details": details
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching feature flag {flag_name}: {str(e)}")
|
||||
return {
|
||||
"flag_name": flag_name,
|
||||
"enabled": False,
|
||||
"details": None
|
||||
}
|
|
@ -1,181 +0,0 @@
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from services import redis
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FeatureFlagManager:
|
||||
def __init__(self):
|
||||
"""Initialize with existing Redis service"""
|
||||
self.flag_prefix = "feature_flag:"
|
||||
self.flag_list_key = "feature_flags:list"
|
||||
|
||||
async def set_flag(self, key: str, enabled: bool, description: str = "") -> bool:
|
||||
"""Set a feature flag to enabled or disabled"""
|
||||
try:
|
||||
flag_key = f"{self.flag_prefix}{key}"
|
||||
flag_data = {
|
||||
'enabled': str(enabled).lower(),
|
||||
'description': description,
|
||||
'updated_at': datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
# Use the existing Redis service
|
||||
redis_client = await redis.get_client()
|
||||
await redis_client.hset(flag_key, mapping=flag_data)
|
||||
await redis_client.sadd(self.flag_list_key, key)
|
||||
|
||||
logger.debug(f"Set feature flag {key} to {enabled}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to set feature flag {key}: {e}")
|
||||
return False
|
||||
|
||||
async def is_enabled(self, key: str) -> bool:
|
||||
"""Check if a feature flag is enabled"""
|
||||
try:
|
||||
flag_key = f"{self.flag_prefix}{key}"
|
||||
redis_client = await redis.get_client()
|
||||
enabled = await redis_client.hget(flag_key, 'enabled')
|
||||
return enabled == 'true' if enabled else False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check feature flag {key}: {e}")
|
||||
# Return False by default if Redis is unavailable
|
||||
return False
|
||||
|
||||
async def get_flag(self, key: str) -> Optional[Dict[str, str]]:
|
||||
"""Get feature flag details"""
|
||||
try:
|
||||
flag_key = f"{self.flag_prefix}{key}"
|
||||
redis_client = await redis.get_client()
|
||||
flag_data = await redis_client.hgetall(flag_key)
|
||||
return flag_data if flag_data else None
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get feature flag {key}: {e}")
|
||||
return None
|
||||
|
||||
async def delete_flag(self, key: str) -> bool:
|
||||
"""Delete a feature flag"""
|
||||
try:
|
||||
flag_key = f"{self.flag_prefix}{key}"
|
||||
redis_client = await redis.get_client()
|
||||
deleted = await redis_client.delete(flag_key)
|
||||
if deleted:
|
||||
await redis_client.srem(self.flag_list_key, key)
|
||||
logger.debug(f"Deleted feature flag: {key}")
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete feature flag {key}: {e}")
|
||||
return False
|
||||
|
||||
async def list_flags(self) -> Dict[str, bool]:
|
||||
"""List all feature flags with their status"""
|
||||
try:
|
||||
redis_client = await redis.get_client()
|
||||
flag_keys = await redis_client.smembers(self.flag_list_key)
|
||||
flags = {}
|
||||
|
||||
for key in flag_keys:
|
||||
flags[key] = await self.is_enabled(key)
|
||||
|
||||
return flags
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list feature flags: {e}")
|
||||
return {}
|
||||
|
||||
async def get_all_flags_details(self) -> Dict[str, Dict[str, str]]:
|
||||
"""Get all feature flags with detailed information"""
|
||||
try:
|
||||
redis_client = await redis.get_client()
|
||||
flag_keys = await redis_client.smembers(self.flag_list_key)
|
||||
flags = {}
|
||||
|
||||
for key in flag_keys:
|
||||
flag_data = await self.get_flag(key)
|
||||
if flag_data:
|
||||
flags[key] = flag_data
|
||||
|
||||
return flags
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get all flags details: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
_flag_manager: Optional[FeatureFlagManager] = None
|
||||
|
||||
|
||||
def get_flag_manager() -> FeatureFlagManager:
|
||||
"""Get the global feature flag manager instance"""
|
||||
global _flag_manager
|
||||
if _flag_manager is None:
|
||||
_flag_manager = FeatureFlagManager()
|
||||
return _flag_manager
|
||||
|
||||
|
||||
# Async convenience functions
|
||||
async def set_flag(key: str, enabled: bool, description: str = "") -> bool:
|
||||
return await get_flag_manager().set_flag(key, enabled, description)
|
||||
|
||||
|
||||
async def is_enabled(key: str) -> bool:
|
||||
return await get_flag_manager().is_enabled(key)
|
||||
|
||||
|
||||
async def enable_flag(key: str, description: str = "") -> bool:
|
||||
return await set_flag(key, True, description)
|
||||
|
||||
|
||||
async def disable_flag(key: str, description: str = "") -> bool:
|
||||
return await set_flag(key, False, description)
|
||||
|
||||
|
||||
async def delete_flag(key: str) -> bool:
|
||||
return await get_flag_manager().delete_flag(key)
|
||||
|
||||
|
||||
async def list_flags() -> Dict[str, bool]:
|
||||
return await get_flag_manager().list_flags()
|
||||
|
||||
|
||||
async def get_flag_details(key: str) -> Optional[Dict[str, str]]:
|
||||
return await get_flag_manager().get_flag(key)
|
||||
|
||||
|
||||
# Feature Flags
|
||||
|
||||
# Custom agents feature flag
|
||||
custom_agents = True
|
||||
|
||||
# MCP module feature flag
|
||||
mcp_module = True
|
||||
|
||||
# Templates API feature flag
|
||||
templates_api = True
|
||||
|
||||
# Triggers API feature flag
|
||||
triggers_api = True
|
||||
|
||||
# Workflows API feature flag
|
||||
workflows_api = True
|
||||
|
||||
# Knowledge base feature flag
|
||||
knowledge_base = True
|
||||
|
||||
# Pipedream integration feature flag
|
||||
pipedream = True
|
||||
|
||||
# Credentials API feature flag
|
||||
credentials_api = True
|
||||
|
||||
# Suna default agent feature flag
|
||||
suna_default_agent = True
|
||||
|
||||
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import argparse
|
||||
import asyncio
|
||||
from flags import enable_flag, disable_flag, is_enabled, list_flags, delete_flag, get_flag_details
|
||||
|
||||
async def enable_command(flag_name: str, description: str = ""):
|
||||
if await enable_flag(flag_name, description):
|
||||
print(f"✓ Enabled flag: {flag_name}")
|
||||
if description:
|
||||
print(f" Description: {description}")
|
||||
else:
|
||||
print(f"✗ Failed to enable flag: {flag_name}")
|
||||
|
||||
|
||||
async def disable_command(flag_name: str, description: str = ""):
|
||||
if await disable_flag(flag_name, description):
|
||||
print(f"✓ Disabled flag: {flag_name}")
|
||||
if description:
|
||||
print(f" Description: {description}")
|
||||
else:
|
||||
print(f"✗ Failed to disable flag: {flag_name}")
|
||||
|
||||
|
||||
async def list_command():
|
||||
flags = await list_flags()
|
||||
|
||||
if not flags:
|
||||
print("No feature flags found.")
|
||||
return
|
||||
|
||||
print("Feature Flags:")
|
||||
print("-" * 50)
|
||||
|
||||
for flag_name, enabled in flags.items():
|
||||
details = await get_flag_details(flag_name)
|
||||
description = details.get('description', 'No description') if details else 'No description'
|
||||
updated_at = details.get('updated_at', 'Unknown') if details else 'Unknown'
|
||||
|
||||
status_icon = "✓" if enabled else "✗"
|
||||
status_text = "ENABLED" if enabled else "DISABLED"
|
||||
|
||||
print(f"{status_icon} {flag_name}: {status_text}")
|
||||
print(f" Description: {description}")
|
||||
print(f" Updated: {updated_at}")
|
||||
print()
|
||||
|
||||
|
||||
async def status_command(flag_name: str):
|
||||
details = await get_flag_details(flag_name)
|
||||
|
||||
if not details:
|
||||
print(f"✗ Flag '{flag_name}' not found.")
|
||||
return
|
||||
|
||||
enabled = await is_enabled(flag_name)
|
||||
status_icon = "✓" if enabled else "✗"
|
||||
status_text = "ENABLED" if enabled else "DISABLED"
|
||||
|
||||
print(f"Flag: {flag_name}")
|
||||
print(f"Status: {status_icon} {status_text}")
|
||||
print(f"Description: {details.get('description', 'No description')}")
|
||||
print(f"Updated: {details.get('updated_at', 'Unknown')}")
|
||||
|
||||
|
||||
async def delete_command(flag_name: str):
|
||||
if not await get_flag_details(flag_name):
|
||||
print(f"✗ Flag '{flag_name}' not found.")
|
||||
return
|
||||
|
||||
confirm = input(f"Are you sure you want to delete flag '{flag_name}'? (y/N): ")
|
||||
if confirm.lower() in ['y', 'yes']:
|
||||
if await delete_flag(flag_name):
|
||||
print(f"✓ Deleted flag: {flag_name}")
|
||||
else:
|
||||
print(f"✗ Failed to delete flag: {flag_name}")
|
||||
else:
|
||||
print("Cancelled.")
|
||||
|
||||
|
||||
async def toggle_command(flag_name: str, description: str = ""):
|
||||
current_status = await is_enabled(flag_name)
|
||||
|
||||
if current_status:
|
||||
await disable_command(flag_name, description)
|
||||
else:
|
||||
await enable_command(flag_name, description)
|
||||
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Feature Flag Management Tool",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
python setup.py enable new_ui "Enable new user interface"
|
||||
python setup.py disable beta_features "Disable beta features"
|
||||
python setup.py list
|
||||
python setup.py status new_ui
|
||||
python setup.py toggle maintenance_mode "Toggle maintenance mode"
|
||||
python setup.py delete old_feature
|
||||
"""
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
||||
|
||||
# Enable command
|
||||
enable_parser = subparsers.add_parser('enable', help='Enable a feature flag')
|
||||
enable_parser.add_argument('flag_name', help='Name of the feature flag')
|
||||
enable_parser.add_argument('description', nargs='?', default='', help='Optional description')
|
||||
|
||||
# Disable command
|
||||
disable_parser = subparsers.add_parser('disable', help='Disable a feature flag')
|
||||
disable_parser.add_argument('flag_name', help='Name of the feature flag')
|
||||
disable_parser.add_argument('description', nargs='?', default='', help='Optional description')
|
||||
|
||||
# List command
|
||||
subparsers.add_parser('list', help='List all feature flags')
|
||||
|
||||
# Status command
|
||||
status_parser = subparsers.add_parser('status', help='Show status of a feature flag')
|
||||
status_parser.add_argument('flag_name', help='Name of the feature flag')
|
||||
|
||||
# Delete command
|
||||
delete_parser = subparsers.add_parser('delete', help='Delete a feature flag')
|
||||
delete_parser.add_argument('flag_name', help='Name of the feature flag')
|
||||
|
||||
# Toggle command
|
||||
toggle_parser = subparsers.add_parser('toggle', help='Toggle a feature flag')
|
||||
toggle_parser.add_argument('flag_name', help='Name of the feature flag')
|
||||
toggle_parser.add_argument('description', nargs='?', default='', help='Optional description')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
try:
|
||||
if args.command == 'enable':
|
||||
await enable_command(args.flag_name, args.description)
|
||||
elif args.command == 'disable':
|
||||
await disable_command(args.flag_name, args.description)
|
||||
elif args.command == 'list':
|
||||
await list_command()
|
||||
elif args.command == 'status':
|
||||
await status_command(args.flag_name)
|
||||
elif args.command == 'delete':
|
||||
await delete_command(args.flag_name)
|
||||
elif args.command == 'toggle':
|
||||
await toggle_command(args.flag_name, args.description)
|
||||
except KeyboardInterrupt:
|
||||
print("\nOperation cancelled.")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
|
@ -6,7 +6,6 @@ from utils.auth_utils import get_current_user_id_from_jwt, verify_agent_access
|
|||
from services.supabase import DBConnection
|
||||
from knowledge_base.file_processor import FileProcessor
|
||||
from utils.logger import logger
|
||||
from flags.flags import is_enabled
|
||||
|
||||
router = APIRouter(prefix="/knowledge-base", tags=["knowledge-base"])
|
||||
|
||||
|
@ -72,11 +71,6 @@ async def get_agent_knowledge_base(
|
|||
include_inactive: bool = False,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("knowledge_base"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This feature is not available at the moment."
|
||||
)
|
||||
|
||||
"""Get all knowledge base entries for an agent"""
|
||||
try:
|
||||
|
@ -130,11 +124,6 @@ async def create_agent_knowledge_base_entry(
|
|||
entry_data: CreateKnowledgeBaseEntryRequest,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("knowledge_base"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This feature is not available at the moment."
|
||||
)
|
||||
|
||||
"""Create a new knowledge base entry for an agent"""
|
||||
try:
|
||||
|
@ -185,11 +174,6 @@ async def upload_file_to_agent_kb(
|
|||
file: UploadFile = File(...),
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("knowledge_base"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This feature is not available at the moment."
|
||||
)
|
||||
|
||||
"""Upload and process a file for agent knowledge base"""
|
||||
try:
|
||||
|
@ -244,11 +228,6 @@ async def update_knowledge_base_entry(
|
|||
entry_data: UpdateKnowledgeBaseEntryRequest,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("knowledge_base"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This feature is not available at the moment."
|
||||
)
|
||||
|
||||
"""Update an agent knowledge base entry"""
|
||||
try:
|
||||
|
@ -317,11 +296,6 @@ async def delete_knowledge_base_entry(
|
|||
entry_id: str,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("knowledge_base"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This feature is not available at the moment."
|
||||
)
|
||||
|
||||
"""Delete an agent knowledge base entry"""
|
||||
try:
|
||||
|
@ -357,11 +331,6 @@ async def get_knowledge_base_entry(
|
|||
entry_id: str,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("knowledge_base"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This feature is not available at the moment."
|
||||
)
|
||||
"""Get a specific agent knowledge base entry"""
|
||||
try:
|
||||
client = await db.client
|
||||
|
@ -409,11 +378,6 @@ async def get_agent_processing_jobs(
|
|||
limit: int = 10,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("knowledge_base"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This feature is not available at the moment."
|
||||
)
|
||||
|
||||
"""Get processing jobs for an agent"""
|
||||
try:
|
||||
|
@ -506,11 +470,6 @@ async def get_agent_knowledge_base_context(
|
|||
max_tokens: int = 4000,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("knowledge_base"):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This feature is not available at the moment."
|
||||
)
|
||||
|
||||
"""Get knowledge base context for agent prompts"""
|
||||
try:
|
||||
|
|
|
@ -11,7 +11,6 @@ import hmac
|
|||
from services.supabase import DBConnection
|
||||
from utils.auth_utils import get_current_user_id_from_jwt
|
||||
from utils.logger import logger
|
||||
from flags.flags import is_enabled
|
||||
from utils.config import config
|
||||
from services.billing import check_billing_status, can_use_model
|
||||
|
||||
|
@ -215,8 +214,6 @@ async def sync_triggers_to_version_config(agent_id: str):
|
|||
|
||||
@router.get("/providers")
|
||||
async def get_providers():
|
||||
if not await is_enabled("agent_triggers"):
|
||||
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
|
||||
|
||||
try:
|
||||
provider_service = get_provider_service(db)
|
||||
|
@ -233,8 +230,6 @@ async def get_agent_triggers(
|
|||
agent_id: str,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("agent_triggers"):
|
||||
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
|
||||
|
||||
await verify_agent_access(agent_id, user_id)
|
||||
|
||||
|
@ -273,8 +268,6 @@ async def get_agent_triggers(
|
|||
async def get_all_user_triggers(
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
if not await is_enabled("agent_triggers"):
|
||||
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
|
||||
|
||||
try:
|
||||
client = await db.client
|
||||
|
@ -368,8 +361,6 @@ async def get_agent_upcoming_runs(
|
|||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
"""Get upcoming scheduled runs for agent triggers"""
|
||||
if not await is_enabled("agent_triggers"):
|
||||
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
|
||||
|
||||
await verify_agent_access(agent_id, user_id)
|
||||
|
||||
|
@ -442,8 +433,6 @@ async def create_agent_trigger(
|
|||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
"""Create a new trigger for an agent"""
|
||||
if not await is_enabled("agent_triggers"):
|
||||
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
|
||||
|
||||
await verify_agent_access(agent_id, user_id)
|
||||
|
||||
|
@ -491,8 +480,6 @@ async def get_trigger(
|
|||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
"""Get a trigger by ID"""
|
||||
if not await is_enabled("agent_triggers"):
|
||||
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
|
||||
|
||||
try:
|
||||
trigger_service = get_trigger_service(db)
|
||||
|
@ -532,8 +519,6 @@ async def update_trigger(
|
|||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
"""Update a trigger"""
|
||||
if not await is_enabled("agent_triggers"):
|
||||
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
|
||||
|
||||
try:
|
||||
trigger_service = get_trigger_service(db)
|
||||
|
@ -585,8 +570,6 @@ async def delete_trigger(
|
|||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
"""Delete a trigger"""
|
||||
if not await is_enabled("agent_triggers"):
|
||||
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
|
||||
|
||||
try:
|
||||
trigger_service = get_trigger_service(db)
|
||||
|
@ -619,8 +602,6 @@ async def trigger_webhook(
|
|||
request: Request
|
||||
):
|
||||
"""Handle incoming webhook for a trigger"""
|
||||
if not await is_enabled("agent_triggers"):
|
||||
raise HTTPException(status_code=403, detail="Agent triggers are not enabled")
|
||||
|
||||
try:
|
||||
# Simple header-based auth using a shared secret
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { agentPlaygroundFlagFrontend } from '@/flags';
|
||||
import { isFlagEnabled } from '@/lib/feature-flags';
|
||||
import { Metadata } from 'next';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|||
|
||||
import { useAgents, useUpdateAgent, useDeleteAgent, useOptimisticAgentUpdate, useAgentDeletionState } from '@/hooks/react-query/agents/use-agents';
|
||||
import { useMarketplaceTemplates, useInstallTemplate, useMyTemplates, useUnpublishTemplate, usePublishTemplate, useCreateTemplate, useDeleteTemplate } from '@/hooks/react-query/secure-mcp/use-secure-mcp';
|
||||
import { useFeatureFlag } from '@/lib/feature-flags';
|
||||
import { useAuth } from '@/components/AuthProvider';
|
||||
|
||||
import { StreamlinedInstallDialog } from '@/components/agents/installation/streamlined-install-dialog';
|
||||
|
@ -44,16 +43,8 @@ interface PublishDialogData {
|
|||
|
||||
export default function AgentsPage() {
|
||||
const { user } = useAuth();
|
||||
const { enabled: customAgentsEnabled, loading: agentsFlagLoading } = useFeatureFlag("custom_agents");
|
||||
const { enabled: agentMarketplaceEnabled, loading: marketplaceFlagLoading } = useFeatureFlag("agent_marketplace");
|
||||
const router = useRouter();
|
||||
const flagLoading = agentsFlagLoading || marketplaceFlagLoading;
|
||||
|
||||
useEffect(() => {
|
||||
if (!flagLoading && !customAgentsEnabled) {
|
||||
router.replace("/dashboard");
|
||||
}
|
||||
}, [flagLoading, customAgentsEnabled, router]);
|
||||
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [editingAgentId, setEditingAgentId] = useState<string | null>(null);
|
||||
|
@ -549,29 +540,7 @@ export default function AgentsPage() {
|
|||
};
|
||||
};
|
||||
|
||||
if (flagLoading) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="container max-w-7xl mx-auto px-4 py-8">
|
||||
<AgentsPageHeader />
|
||||
</div>
|
||||
|
||||
<div className="sticky top-0 z-50 bg-background/95 backdrop-blur-md border-b border-border/40 shadow-sm">
|
||||
<div className="container max-w-7xl mx-auto px-4 py-4">
|
||||
<TabsNavigation activeTab={activeTab} onTabChange={handleTabChange} onCreateAgent={handleCreateNewAgent} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container max-w-7xl mx-auto px-4 py-8">
|
||||
<LoadingSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!customAgentsEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
|
|
|
@ -3,7 +3,6 @@ import { DashboardContent } from "../../../components/dashboard/dashboard-conten
|
|||
import { BackgroundAALChecker } from "@/components/auth/background-aal-checker";
|
||||
import { Suspense } from "react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { isFlagEnabled } from "@/lib/feature-flags";
|
||||
|
||||
export default async function DashboardPage() {
|
||||
return (
|
||||
|
|
|
@ -4,8 +4,6 @@ import React, { useState, useEffect } from 'react';
|
|||
import { Key, Plus, Trash2, Copy, Shield, ExternalLink, Sparkles } from 'lucide-react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useFeatureFlag } from '@/lib/feature-flags';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
|
@ -59,9 +57,6 @@ interface NewAPIKeyData {
|
|||
}
|
||||
|
||||
export default function APIKeysPage() {
|
||||
const { enabled: customAgentsEnabled, loading: flagLoading } =
|
||||
useFeatureFlag('custom_agents');
|
||||
const router = useRouter();
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
const [newKeyData, setNewKeyData] = useState<NewAPIKeyData>({
|
||||
title: '',
|
||||
|
@ -73,11 +68,6 @@ export default function APIKeysPage() {
|
|||
const [showCreatedKey, setShowCreatedKey] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (!flagLoading && !customAgentsEnabled) {
|
||||
router.replace('/dashboard');
|
||||
}
|
||||
}, [flagLoading, customAgentsEnabled, router]);
|
||||
|
||||
// Fetch API keys
|
||||
const {
|
||||
|
@ -211,26 +201,6 @@ export default function APIKeysPage() {
|
|||
return new Date(expiresAt) < new Date();
|
||||
};
|
||||
|
||||
if (flagLoading) {
|
||||
return (
|
||||
<div className="container mx-auto max-w-6xl px-6 py-6">
|
||||
<div className="space-y-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-12 bg-muted rounded-lg"></div>
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={index} className="h-32 bg-muted rounded-lg"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!customAgentsEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-6xl px-6 py-6">
|
||||
|
|
|
@ -5,40 +5,9 @@ import {
|
|||
Zap
|
||||
} from 'lucide-react';
|
||||
import { ComposioConnectionsSection } from '../../../../components/agents/composio/composio-connections-section';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useFeatureFlag } from '@/lib/feature-flags';
|
||||
import { PageHeader } from '@/components/ui/page-header';
|
||||
|
||||
export default function AppProfilesPage() {
|
||||
const { enabled: customAgentsEnabled, loading: flagLoading } = useFeatureFlag("custom_agents");
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!flagLoading && !customAgentsEnabled) {
|
||||
router.replace("/dashboard");
|
||||
}
|
||||
}, [flagLoading, customAgentsEnabled, router]);
|
||||
|
||||
if (flagLoading) {
|
||||
return (
|
||||
<div className="container mx-auto max-w-7xl px-4 py-8">
|
||||
<div className="space-y-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-32 bg-muted rounded-3xl"></div>
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={index} className="h-32 bg-muted rounded-lg"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!customAgentsEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-7xl px-4 py-8">
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import { maintenanceNoticeFlag } from '@/lib/edge-flags';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
const maintenanceNotice = await maintenanceNoticeFlag();
|
||||
return NextResponse.json(maintenanceNotice);
|
||||
}
|
|
@ -19,7 +19,6 @@ import {
|
|||
import { useAuth } from '@/components/AuthProvider';
|
||||
import { useAuthMethodTracking } from '@/lib/stores/auth-tracking';
|
||||
import { toast } from 'sonner';
|
||||
import { useFeatureFlag } from '@/lib/feature-flags';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
|
@ -41,7 +40,6 @@ function LoginContent() {
|
|||
const mode = searchParams.get('mode');
|
||||
const returnUrl = searchParams.get('returnUrl');
|
||||
const message = searchParams.get('message');
|
||||
const { enabled: customAgentsEnabled } = useFeatureFlag("custom_agents");
|
||||
|
||||
const isSignUp = mode === 'signup';
|
||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||
|
@ -287,7 +285,7 @@ function LoginContent() {
|
|||
</div>
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="mb-4 flex items-center flex-col gap-3 sm:gap-4 justify-center">
|
||||
{customAgentsEnabled && <ReleaseBadge className='mb-2 sm:mb-4' text="Custom Agents, Playbooks, and more!" link="/changelog" />}
|
||||
<ReleaseBadge className='mb-2 sm:mb-4' text="Custom Agents, Playbooks, and more!" link="/changelog" />
|
||||
<h1 className="text-xl sm:text-2xl font-semibold text-foreground text-center leading-tight">
|
||||
{isSignUp ? 'Create your account' : 'Log into your account'}
|
||||
</h1>
|
||||
|
|
|
@ -28,7 +28,6 @@ import { useThreadQuery } from '@/hooks/react-query/threads/use-threads';
|
|||
import { normalizeFilenameToNFC } from '@/lib/utils/unicode';
|
||||
import { KortixLogo } from '../sidebar/kortix-logo';
|
||||
import { AgentRunLimitDialog } from '@/components/thread/agent-run-limit-dialog';
|
||||
import { useFeatureFlag } from '@/lib/feature-flags';
|
||||
import { CustomAgentsSection } from './custom-agents-section';
|
||||
import { toast } from 'sonner';
|
||||
import { ReleaseBadge } from '../auth/release-badge';
|
||||
|
@ -102,7 +101,6 @@ export function DashboardContent() {
|
|||
} = useDashboardTour();
|
||||
|
||||
// Feature flag for custom agents section
|
||||
const { enabled: customAgentsEnabled } = useFeatureFlag('custom_agents');
|
||||
|
||||
// Fetch agents to get the selected agent's name
|
||||
const { data: agentsResponse } = useAgents({
|
||||
|
@ -341,7 +339,7 @@ export function DashboardContent() {
|
|||
<div className="flex flex-col h-screen w-full overflow-hidden">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="min-h-full flex flex-col">
|
||||
{customAgentsEnabled && (
|
||||
{(
|
||||
<div className="flex justify-center px-4 pt-4 md:pt-8">
|
||||
<ReleaseBadge text="Custom Agents, Playbooks, and more!" link="/agents?tab=my-agents" />
|
||||
</div>
|
||||
|
@ -376,7 +374,7 @@ export function DashboardContent() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{enabledEnvironment && customAgentsEnabled && (
|
||||
{enabledEnvironment && (
|
||||
<div className="w-full px-4 pb-8" data-tour="custom-agents">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<CustomAgentsSection
|
||||
|
|
|
@ -15,7 +15,6 @@ import { DeleteOperationProvider } from '@/contexts/DeleteOperationContext';
|
|||
import { StatusOverlay } from '@/components/ui/status-overlay';
|
||||
import { MaintenanceNotice } from './maintenance-notice';
|
||||
import { MaintenanceBanner } from './maintenance-banner';
|
||||
import { useMaintenanceNoticeQuery } from '@/hooks/react-query/edge-flags';
|
||||
|
||||
import { useProjects, useThreads } from '@/hooks/react-query/sidebar/use-sidebar';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
|
@ -29,7 +28,6 @@ interface DashboardLayoutContentProps {
|
|||
export default function DashboardLayoutContent({
|
||||
children,
|
||||
}: DashboardLayoutContentProps) {
|
||||
const maintenanceNoticeQuery = useMaintenanceNoticeQuery();
|
||||
// const [showPricingAlert, setShowPricingAlert] = useState(false)
|
||||
const [showMaintenanceAlert, setShowMaintenanceAlert] = useState(false);
|
||||
const { data: accounts } = useAccounts();
|
||||
|
@ -80,31 +78,7 @@ export default function DashboardLayoutContent({
|
|||
}
|
||||
}, [user, isLoading, router]);
|
||||
|
||||
if (maintenanceNoticeQuery.data?.enabled) {
|
||||
const now = new Date();
|
||||
const startTime = new Date(maintenanceNoticeQuery.data.startTime);
|
||||
const endTime = new Date(maintenanceNoticeQuery.data.endTime);
|
||||
|
||||
if (now > startTime) {
|
||||
return (
|
||||
<div className="w-screen h-screen flex items-center justify-center">
|
||||
<div className="max-w-xl">
|
||||
<MaintenanceNotice endTime={endTime.toISOString()} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mantenanceBanner: React.ReactNode | null = null;
|
||||
if (maintenanceNoticeQuery.data?.enabled) {
|
||||
mantenanceBanner = (
|
||||
<MaintenanceBanner
|
||||
startTime={maintenanceNoticeQuery.data.startTime}
|
||||
endTime={maintenanceNoticeQuery.data.endTime}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Show loading state while checking auth or health
|
||||
if (isLoading || isCheckingHealth) {
|
||||
|
|
|
@ -52,7 +52,6 @@ import {
|
|||
import { createClient } from '@/lib/supabase/client';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { isLocalMode } from '@/lib/config';
|
||||
import { useFeatureFlag } from '@/lib/feature-flags';
|
||||
import { clearUserLocalStorage } from '@/lib/utils/clear-local-storage';
|
||||
|
||||
export function NavUserWithTeams({
|
||||
|
@ -69,7 +68,6 @@ export function NavUserWithTeams({
|
|||
const { data: accounts } = useAccounts();
|
||||
const [showNewTeamDialog, setShowNewTeamDialog] = React.useState(false);
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { enabled: customAgentsEnabled, loading: flagLoading } = useFeatureFlag("custom_agents");
|
||||
|
||||
// Prepare personal account and team accounts
|
||||
const personalAccount = React.useMemo(
|
||||
|
@ -295,7 +293,7 @@ export function NavUserWithTeams({
|
|||
Billing
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{!flagLoading && customAgentsEnabled && (
|
||||
{(
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/settings/credentials">
|
||||
<Plug className="h-4 w-4" />
|
||||
|
@ -303,7 +301,7 @@ export function NavUserWithTeams({
|
|||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{!flagLoading && customAgentsEnabled && (
|
||||
{(
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/settings/api-keys">
|
||||
<Key className="h-4 w-4" />
|
||||
|
|
|
@ -41,7 +41,6 @@ import { Button } from '@/components/ui/button';
|
|||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { useFeatureFlags } from '@/lib/feature-flags';
|
||||
import posthog from 'posthog-js';
|
||||
// Floating mobile menu button component
|
||||
function FloatingMobileMenuButton() {
|
||||
|
@ -88,9 +87,6 @@ export function SidebarLeft({
|
|||
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const { flags, loading: flagsLoading } = useFeatureFlags(['custom_agents', 'agent_marketplace']);
|
||||
const customAgentsEnabled = flags.custom_agents;
|
||||
const marketplaceEnabled = flags.agent_marketplace;
|
||||
const [showNewAgentDialog, setShowNewAgentDialog] = useState(false);
|
||||
|
||||
// Close mobile menu on page navigation
|
||||
|
@ -201,7 +197,7 @@ export function SidebarLeft({
|
|||
</span>
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
{!flagsLoading && customAgentsEnabled && (
|
||||
{(
|
||||
<SidebarMenu>
|
||||
<Collapsible
|
||||
defaultOpen={true}
|
||||
|
|
|
@ -9,7 +9,6 @@ import { VoiceRecorder } from './voice-recorder';
|
|||
import { UnifiedConfigMenu } from './unified-config-menu';
|
||||
import { canAccessModel, SubscriptionStatus } from './_use-model-selection';
|
||||
import { isLocalMode } from '@/lib/config';
|
||||
import { useFeatureFlag } from '@/lib/feature-flags';
|
||||
import { TooltipContent } from '@/components/ui/tooltip';
|
||||
import { Tooltip } from '@/components/ui/tooltip';
|
||||
import { TooltipProvider, TooltipTrigger } from '@radix-ui/react-tooltip';
|
||||
|
@ -94,7 +93,6 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
|
|||
) => {
|
||||
const [billingModalOpen, setBillingModalOpen] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { enabled: customAgentsEnabled, loading: flagsLoading } = useFeatureFlag('custom_agents');
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
@ -158,7 +156,7 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
|
|||
};
|
||||
|
||||
const renderDropdown = () => {
|
||||
const showAdvancedFeatures = isLoggedIn && (enableAdvancedConfig || (customAgentsEnabled && !flagsLoading));
|
||||
const showAdvancedFeatures = isLoggedIn && enableAdvancedConfig;
|
||||
// Don't render dropdown components until after hydration to prevent ID mismatches
|
||||
if (!mounted) {
|
||||
return <div className="flex items-center gap-2 h-8" />; // Placeholder with same height
|
||||
|
|
|
@ -20,7 +20,6 @@ import { ShareModal } from "@/components/sidebar/share-modal"
|
|||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { projectKeys } from "@/hooks/react-query/sidebar/keys";
|
||||
import { threadKeys } from "@/hooks/react-query/threads/keys";
|
||||
import { useFeatureFlags } from "@/lib/feature-flags";
|
||||
|
||||
interface ThreadSiteHeaderProps {
|
||||
threadId?: string;
|
||||
|
@ -53,8 +52,6 @@ export function SiteHeader({
|
|||
const [showKnowledgeBase, setShowKnowledgeBase] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
const { flags, loading: flagsLoading } = useFeatureFlags(['knowledge_base']);
|
||||
const knowledgeBaseEnabled = flags.knowledge_base;
|
||||
|
||||
const isMobile = useIsMobile() || isMobileView
|
||||
const updateProjectMutation = useUpdateProject()
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import { isLocalMode } from './lib/config';
|
||||
|
||||
export const agentPlaygroundFlagFrontend = isLocalMode();
|
||||
export const marketplaceFlagFrontend = isLocalMode();
|
||||
export const agentPlaygroundEnabled = isLocalMode();
|
||||
export const marketplaceEnabled = isLocalMode();
|
|
@ -1,6 +1,5 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { isFlagEnabled } from '@/lib/feature-flags';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
|
||||
|
||||
|
@ -20,10 +19,6 @@ interface AgentToolsResponse {
|
|||
}
|
||||
|
||||
const fetchAgentTools = async (agentId: string): Promise<AgentToolsResponse> => {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { createClient } from "@/lib/supabase/client";
|
||||
import { isFlagEnabled } from "@/lib/feature-flags";
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
|
||||
|
||||
|
@ -169,10 +168,6 @@ export type AgentUpdateRequest = {
|
|||
|
||||
export const getAgents = async (params: AgentsParams = {}): Promise<AgentsResponse> => {
|
||||
try {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
@ -217,10 +212,6 @@ export const getAgents = async (params: AgentsParams = {}): Promise<AgentsRespon
|
|||
|
||||
export const getAgent = async (agentId: string): Promise<Agent> => {
|
||||
try {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
@ -251,10 +242,6 @@ export const getAgent = async (agentId: string): Promise<Agent> => {
|
|||
|
||||
export const createAgent = async (agentData: AgentCreateRequest): Promise<Agent> => {
|
||||
try {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
@ -297,10 +284,6 @@ export const createAgent = async (agentData: AgentCreateRequest): Promise<Agent>
|
|||
|
||||
export const updateAgent = async (agentId: string, agentData: AgentUpdateRequest): Promise<Agent> => {
|
||||
try {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
@ -332,10 +315,6 @@ export const updateAgent = async (agentId: string, agentData: AgentUpdateRequest
|
|||
|
||||
export const deleteAgent = async (agentId: string): Promise<void> => {
|
||||
try {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
@ -363,10 +342,6 @@ export const deleteAgent = async (agentId: string): Promise<void> => {
|
|||
|
||||
export const getThreadAgent = async (threadId: string): Promise<ThreadAgentResponse> => {
|
||||
try {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
@ -399,10 +374,6 @@ export const getThreadAgent = async (threadId: string): Promise<ThreadAgentRespo
|
|||
|
||||
export const getAgentVersions = async (agentId: string): Promise<AgentVersion[]> => {
|
||||
try {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
@ -434,10 +405,6 @@ export const createAgentVersion = async (
|
|||
data: AgentVersionCreateRequest
|
||||
): Promise<AgentVersion> => {
|
||||
try {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
@ -472,10 +439,6 @@ export const activateAgentVersion = async (
|
|||
versionId: string
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
@ -508,10 +471,6 @@ export const getAgentVersion = async (
|
|||
versionId: string
|
||||
): Promise<AgentVersion> => {
|
||||
try {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { createClient } from "@/lib/supabase/client";
|
||||
import { isFlagEnabled } from "@/lib/feature-flags";
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
|
||||
|
||||
|
@ -144,10 +143,6 @@ export const generateLLMWorkflowPrompt = (workflow: AgentWorkflow): string => {
|
|||
|
||||
export const getAgentWorkflows = async (agentId: string): Promise<AgentWorkflow[]> => {
|
||||
try {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
@ -178,10 +173,6 @@ export const getAgentWorkflows = async (agentId: string): Promise<AgentWorkflow[
|
|||
|
||||
export const createAgentWorkflow = async (agentId: string, workflow: CreateWorkflowRequest): Promise<AgentWorkflow> => {
|
||||
try {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
@ -217,10 +208,6 @@ export const updateAgentWorkflow = async (
|
|||
workflow: UpdateWorkflowRequest
|
||||
): Promise<AgentWorkflow> => {
|
||||
try {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
@ -252,10 +239,6 @@ export const updateAgentWorkflow = async (
|
|||
|
||||
export const deleteAgentWorkflow = async (agentId: string, workflowId: string): Promise<void> => {
|
||||
try {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
@ -293,10 +276,6 @@ export const executeWorkflow = async (
|
|||
message?: string;
|
||||
}> => {
|
||||
try {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
@ -332,10 +311,6 @@ export const getWorkflowExecutions = async (
|
|||
limit: number = 20
|
||||
): Promise<WorkflowExecution[]> => {
|
||||
try {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { createQueryHook, createQueryKeys } from '@/hooks/use-query';
|
||||
import { IMaintenanceNotice } from '@/lib/edge-flags';
|
||||
|
||||
const maintenanceNoticeKeysBase = ['maintenance-notice'] as const;
|
||||
|
||||
export const maintenanceNoticeKeys = createQueryKeys({
|
||||
all: maintenanceNoticeKeysBase,
|
||||
});
|
||||
|
||||
export const useMaintenanceNoticeQuery = createQueryHook(
|
||||
maintenanceNoticeKeys.all,
|
||||
async (): Promise<IMaintenanceNotice> => {
|
||||
const response = await fetch('/api/edge-flags');
|
||||
return response.json();
|
||||
},
|
||||
{
|
||||
staleTime: 30 * 1000,
|
||||
refetchInterval: 60 * 1000,
|
||||
refetchOnWindowFocus: true,
|
||||
retry: 3,
|
||||
},
|
||||
);
|
|
@ -1,55 +0,0 @@
|
|||
import { flag } from 'flags/next';
|
||||
import { getAll } from '@vercel/edge-config';
|
||||
|
||||
export type IMaintenanceNotice =
|
||||
| {
|
||||
enabled: true;
|
||||
startTime: string; // Date
|
||||
endTime: string; // Date
|
||||
}
|
||||
| {
|
||||
enabled: false;
|
||||
startTime?: undefined;
|
||||
endTime?: undefined;
|
||||
};
|
||||
|
||||
export const maintenanceNoticeFlag = flag({
|
||||
key: 'maintenance-notice',
|
||||
async decide() {
|
||||
try {
|
||||
if (!process.env.EDGE_CONFIG) {
|
||||
return { enabled: false } as const;
|
||||
}
|
||||
|
||||
const flags = await getAll([
|
||||
'maintenance-notice_start-time',
|
||||
'maintenance-notice_end-time',
|
||||
'maintenance-notice_enabled',
|
||||
]);
|
||||
|
||||
if (!flags['maintenance-notice_enabled']) {
|
||||
return { enabled: false } as const;
|
||||
}
|
||||
|
||||
const startTime = new Date(flags['maintenance-notice_start-time']);
|
||||
const endTime = new Date(flags['maintenance-notice_end-time']);
|
||||
|
||||
if (isNaN(startTime.getTime()) || isNaN(endTime.getTime())) {
|
||||
throw new Error(
|
||||
`Invalid maintenance notice start or end time: ${flags['maintenance-notice_start-time']} or ${flags['maintenance-notice_end-time']}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
startTime: startTime.toISOString(),
|
||||
endTime: endTime.toISOString(),
|
||||
} as const;
|
||||
} catch (cause) {
|
||||
console.error(
|
||||
new Error('Failed to get maintenance notice flag', { cause }),
|
||||
);
|
||||
return { enabled: false } as const;
|
||||
}
|
||||
},
|
||||
});
|
|
@ -1,333 +0,0 @@
|
|||
import React from 'react';
|
||||
import { useQuery, useQueries } from '@tanstack/react-query';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
|
||||
|
||||
export interface FeatureFlag {
|
||||
flag_name: string;
|
||||
enabled: boolean;
|
||||
details?: {
|
||||
description?: string;
|
||||
updated_at?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface FeatureFlagsResponse {
|
||||
flags: Record<string, boolean>;
|
||||
}
|
||||
|
||||
const flagCache = new Map<string, { value: boolean; timestamp: number }>();
|
||||
const CACHE_DURATION = 5 * 60 * 1000;
|
||||
|
||||
let globalFlagsCache: { flags: Record<string, boolean>; timestamp: number } | null = null;
|
||||
|
||||
export class FeatureFlagManager {
|
||||
private static instance: FeatureFlagManager;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): FeatureFlagManager {
|
||||
if (!FeatureFlagManager.instance) {
|
||||
FeatureFlagManager.instance = new FeatureFlagManager();
|
||||
}
|
||||
return FeatureFlagManager.instance;
|
||||
}
|
||||
|
||||
async isEnabled(flagName: string): Promise<boolean> {
|
||||
try {
|
||||
const cached = flagCache.get(flagName);
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
|
||||
return cached.value;
|
||||
}
|
||||
const response = await fetch(`${API_URL}/feature-flags/${flagName}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.warn(`Failed to fetch feature flag ${flagName}: ${response.status}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const data: FeatureFlag = await response.json();
|
||||
|
||||
flagCache.set(flagName, {
|
||||
value: data.enabled,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
return data.enabled;
|
||||
} catch (error) {
|
||||
console.error(`Error checking feature flag ${flagName}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getFlagDetails(flagName: string): Promise<FeatureFlag | null> {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/feature-flags/${flagName}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`Failed to fetch feature flag details for ${flagName}: ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data: FeatureFlag = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching feature flag details for ${flagName}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getAllFlags(): Promise<Record<string, boolean>> {
|
||||
try {
|
||||
if (globalFlagsCache && Date.now() - globalFlagsCache.timestamp < CACHE_DURATION) {
|
||||
return globalFlagsCache.flags;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_URL}/feature-flags`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`Failed to fetch all feature flags: ${response.status}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
const data: FeatureFlagsResponse = await response.json();
|
||||
globalFlagsCache = {
|
||||
flags: data.flags,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
Object.entries(data.flags).forEach(([flagName, enabled]) => {
|
||||
flagCache.set(flagName, {
|
||||
value: enabled,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
return data.flags;
|
||||
} catch (error) {
|
||||
console.error('Error fetching all feature flags:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
clearCache(): void {
|
||||
flagCache.clear();
|
||||
globalFlagsCache = null;
|
||||
}
|
||||
|
||||
async preloadFlags(flagNames: string[]): Promise<void> {
|
||||
try {
|
||||
const promises = flagNames.map(flagName => this.isEnabled(flagName));
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
console.error('Error preloading feature flags:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const featureFlagManager = FeatureFlagManager.getInstance();
|
||||
|
||||
export const isEnabled = (flagName: string): Promise<boolean> => {
|
||||
return featureFlagManager.isEnabled(flagName);
|
||||
};
|
||||
|
||||
export const isFlagEnabled = isEnabled;
|
||||
|
||||
export const getFlagDetails = (flagName: string): Promise<FeatureFlag | null> => {
|
||||
return featureFlagManager.getFlagDetails(flagName);
|
||||
};
|
||||
|
||||
export const getAllFlags = (): Promise<Record<string, boolean>> => {
|
||||
return featureFlagManager.getAllFlags();
|
||||
};
|
||||
|
||||
export const clearFlagCache = (): void => {
|
||||
featureFlagManager.clearCache();
|
||||
};
|
||||
|
||||
export const preloadFlags = (flagNames: string[]): Promise<void> => {
|
||||
return featureFlagManager.preloadFlags(flagNames);
|
||||
};
|
||||
|
||||
// React Query key factories
|
||||
export const featureFlagKeys = {
|
||||
all: ['feature-flags'] as const,
|
||||
flag: (flagName: string) => [...featureFlagKeys.all, 'flag', flagName] as const,
|
||||
flagDetails: (flagName: string) => [...featureFlagKeys.all, 'details', flagName] as const,
|
||||
allFlags: () => [...featureFlagKeys.all, 'allFlags'] as const,
|
||||
};
|
||||
|
||||
// Query functions
|
||||
const fetchFeatureFlag = async (flagName: string): Promise<boolean> => {
|
||||
const response = await fetch(`${API_URL}/feature-flags/${flagName}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch feature flag ${flagName}: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: FeatureFlag = await response.json();
|
||||
return data.enabled;
|
||||
};
|
||||
|
||||
const fetchFeatureFlagDetails = async (flagName: string): Promise<FeatureFlag> => {
|
||||
const response = await fetch(`${API_URL}/feature-flags/${flagName}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch feature flag details for ${flagName}: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: FeatureFlag = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
const fetchAllFeatureFlags = async (): Promise<Record<string, boolean>> => {
|
||||
const response = await fetch(`${API_URL}/feature-flags`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch all feature flags: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: FeatureFlagsResponse = await response.json();
|
||||
return data.flags;
|
||||
};
|
||||
|
||||
// React Query Hooks
|
||||
export const useFeatureFlag = (flagName: string, options?: {
|
||||
enabled?: boolean;
|
||||
staleTime?: number;
|
||||
gcTime?: number;
|
||||
refetchOnWindowFocus?: boolean;
|
||||
}) => {
|
||||
const query = useQuery({
|
||||
queryKey: featureFlagKeys.flag(flagName),
|
||||
queryFn: () => fetchFeatureFlag(flagName),
|
||||
staleTime: options?.staleTime ?? 5 * 60 * 1000, // 5 minutes
|
||||
gcTime: options?.gcTime ?? 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: options?.refetchOnWindowFocus ?? false,
|
||||
enabled: options?.enabled ?? true,
|
||||
retry: (failureCount, error) => {
|
||||
// Don't retry on 4xx errors, but retry on network errors
|
||||
if (error instanceof Error && error.message.includes('4')) {
|
||||
return false;
|
||||
}
|
||||
return failureCount < 3;
|
||||
},
|
||||
meta: {
|
||||
errorMessage: `Failed to fetch feature flag: ${flagName}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Return backward-compatible interface
|
||||
return {
|
||||
enabled: query.data ?? false,
|
||||
loading: query.isLoading,
|
||||
// Also expose React Query properties for advanced usage
|
||||
...query,
|
||||
};
|
||||
};
|
||||
|
||||
export const useFeatureFlagDetails = (flagName: string, options?: {
|
||||
enabled?: boolean;
|
||||
staleTime?: number;
|
||||
gcTime?: number;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: featureFlagKeys.flagDetails(flagName),
|
||||
queryFn: () => fetchFeatureFlagDetails(flagName),
|
||||
staleTime: options?.staleTime ?? 5 * 60 * 1000, // 5 minutes
|
||||
gcTime: options?.gcTime ?? 10 * 60 * 1000, // 10 minutes
|
||||
enabled: options?.enabled ?? true,
|
||||
retry: (failureCount, error) => {
|
||||
if (error instanceof Error && error.message.includes('4')) {
|
||||
return false;
|
||||
}
|
||||
return failureCount < 3;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useAllFeatureFlags = (options?: {
|
||||
enabled?: boolean;
|
||||
staleTime?: number;
|
||||
gcTime?: number;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: featureFlagKeys.allFlags(),
|
||||
queryFn: fetchAllFeatureFlags,
|
||||
staleTime: options?.staleTime ?? 5 * 60 * 1000, // 5 minutes
|
||||
gcTime: options?.gcTime ?? 10 * 60 * 1000, // 10 minutes
|
||||
enabled: options?.enabled ?? true,
|
||||
retry: (failureCount, error) => {
|
||||
if (error instanceof Error && error.message.includes('4')) {
|
||||
return false;
|
||||
}
|
||||
return failureCount < 3;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useFeatureFlags = (flagNames: string[], options?: {
|
||||
enabled?: boolean;
|
||||
staleTime?: number;
|
||||
gcTime?: number;
|
||||
}) => {
|
||||
const queries = useQueries({
|
||||
queries: flagNames.map((flagName) => ({
|
||||
queryKey: featureFlagKeys.flag(flagName),
|
||||
queryFn: () => fetchFeatureFlag(flagName),
|
||||
staleTime: options?.staleTime ?? 5 * 60 * 1000, // 5 minutes
|
||||
gcTime: options?.gcTime ?? 10 * 60 * 1000, // 10 minutes
|
||||
enabled: options?.enabled ?? true,
|
||||
retry: (failureCount: number, error: Error) => {
|
||||
if (error.message.includes('4')) {
|
||||
return false;
|
||||
}
|
||||
return failureCount < 3;
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
// Transform the results into a more convenient format
|
||||
const flags = React.useMemo(() => {
|
||||
const result: Record<string, boolean> = {};
|
||||
flagNames.forEach((flagName, index) => {
|
||||
const query = queries[index];
|
||||
result[flagName] = query.data ?? false;
|
||||
});
|
||||
return result;
|
||||
}, [queries, flagNames]);
|
||||
|
||||
const loading = queries.some(query => query.isLoading);
|
||||
const error = queries.find(query => query.error)?.error?.message ?? null;
|
||||
|
||||
return { flags, loading, error };
|
||||
};
|
|
@ -1,16 +1,10 @@
|
|||
import { createClient } from '@/lib/supabase/client';
|
||||
import { IApiClient } from '../repositories/interfaces';
|
||||
import { isFlagEnabled } from '@/lib/feature-flags';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
|
||||
|
||||
export class SupabaseApiClient implements IApiClient {
|
||||
private async getAuthHeaders(): Promise<Record<string, string>> {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
throw new Error('Custom agents is not enabled');
|
||||
}
|
||||
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
|
|
Loading…
Reference in New Issue