"""`deepeval login` / `deepeval logout` Typer commands.""" import re import webbrowser from typing import List, Optional, Union from uuid import uuid4 import typer from rich import print from deepeval.cli.auth.api import ( ExistingProjectKeyRequest, NewUserOnboardingRequest, ) from deepeval.cli.auth.flow import ( AuthFlowError, browser_pairing_login, complete_cli_onboarding, get_cli_onboarding_context, prompt_select, prompt_text, ) from deepeval.cli.diagnose import resolve_setting_source from deepeval.cli.dotenv_handler import DotenvHandler from deepeval.cli.utils import ( PROD, WWW, coerce_blank_to_none, handle_save_result, render_confident_banner, with_utm, ) from deepeval.config.settings import dotenv_search_paths, get_settings from deepeval.config.utils import read_dotenv_file from deepeval.telemetry import capture_login_event from deepeval.test_run.test_run import LATEST_TEST_RUN_FILE_PATH from deepeval.utils import delete_file_if_exists LOGIN_HELP = ( "Log in to Confident AI. Opens the platform for authentication, then " "completes organization and project setup in the terminal. " f"You can still manage project API keys at {with_utm(PROD, medium='cli', content='login_help_text')}. " "The key is saved to your environment variables, typically .env.local, " "unless a different path is provided with --save." ) # Keep the manual browser-and-paste flow available as an emergency rollback. USE_BROWSER_PAIRING_LOGIN = True REGION_CHOICES = [ ("πŸ‡ΊπŸ‡Έ United States (US)", "US"), ("πŸ‡ͺπŸ‡Ί European Union (EU)", "EU"), ] API_KEY_PATTERN = re.compile( r"^confident_(?Pus|eu)_(?Porg|proj|global)_" r"[A-Za-z0-9+/]+={0,2}$", re.IGNORECASE, ) def _prompt_and_persist_region(save: Optional[str]) -> None: """Pick the data region before the pairing session is created, so the session, the browser page, and all subsequent polling hit the same regional backend. Persisted like `deepeval set-confident-region`.""" settings = get_settings() current = (settings.CONFIDENT_REGION or "US").upper() choices = sorted(REGION_CHOICES, key=lambda choice: choice[1] != current) region = prompt_select("Select your Confident AI data region:", choices) if region == settings.CONFIDENT_REGION: return with settings.edit(save=save): settings.CONFIDENT_REGION = region def _print_api_key_location() -> None: settings_url = PROD print( "Find your project API key at " f"[link={settings_url}]{settings_url}[/link] under " "[bold]Project Settings > API Keys[/bold]." ) def _get_pasted_api_key_warnings( api_key: str, configured_region: Optional[str] ) -> List[str]: match = API_KEY_PATTERN.fullmatch(api_key.strip()) if match is None: return [] key_region = match.group("region").upper() key_scope = match.group("scope").lower() warnings: List[str] = [] if configured_region and key_region != configured_region.upper(): warnings.append( f"This API key is for the {key_region} region, but DeepEval is " f"configured to use {configured_region.upper()}. The region " "configured in DeepEval must match the key's region in " "Confident AI." ) if key_scope == "org": warnings.append( "This is an organization API key, which cannot be used to log in " "to DeepEval. Use a project API key from Project Settings > API " "Keys instead." ) return warnings def _warn_for_pasted_api_key(api_key: str) -> None: configured_region = get_settings().CONFIDENT_REGION for warning in _get_pasted_api_key_warnings(api_key, configured_region): print(f"⚠️ {warning}") def _prompt_paste_api_key() -> str: while True: api_key = coerce_blank_to_none( typer.prompt("πŸ” Enter your project API key", hide_input=True) ) if api_key: _warn_for_pasted_api_key(api_key) return api_key print("❌ Project API key cannot be empty. Please try again.\n") def _open_platform_and_prompt_api_key() -> str: platform_url = with_utm( PROD, medium="cli", content="login_api_key_browser_open" ) print("\n🌐 Opening Confident AI in your browser...") webbrowser.open(platform_url) print( "(open this link if your browser did not open: " f"[link={platform_url}]{platform_url}[/link])" ) return _prompt_paste_api_key() def _prompt_required(message: str, default: Optional[str] = None) -> str: while True: value = coerce_blank_to_none(prompt_text(message, default=default)) if value: return value print(f"❌ {message} cannot be empty. Please try again.\n") def _complete_browser_cli_login() -> Optional[str]: authorization = browser_pairing_login() if authorization is None: return None try: context = get_cli_onboarding_context(authorization.setup_token) request: Union[NewUserOnboardingRequest, ExistingProjectKeyRequest] if context.state == "new_user": existing_name = context.user.name if context.user else None name_default = ( existing_name if existing_name and existing_name != "New User" else None ) print("\n[bold]Let's set up your workspace.[/bold]") user_name = _prompt_required("Your name", default=name_default) organization_name = _prompt_required("Organization name") project_name = _prompt_required( "Project name", default="My First Project" ) print( "\nYour organization and project will be created as " f"[bold]{organization_name}[/bold] / " f"[bold]{project_name}[/bold]." ) if not typer.confirm("Continue?", default=True): raise AuthFlowError("Setup cancelled.") request = NewUserOnboardingRequest( user_name=user_name, organization_name=organization_name, project_name=project_name, ) else: projects = [ project for project in context.projects if project.can_create_api_key ] if not projects: raise AuthFlowError( "You do not have permission to create an API key for any " "project in this organization." ) project_id = prompt_select( "Select a project:", [(project.name, project.id) for project in projects], ) request = ExistingProjectKeyRequest(project_id=project_id) return complete_cli_onboarding( authorization.setup_token, request, idempotency_key=str(uuid4()), ) except AuthFlowError as error: print(f"\n⚠️ CLI onboarding could not be completed: {error}") return None def _resolve_login_key(save: Optional[str]) -> str: if not USE_BROWSER_PAIRING_LOGIN: return _open_platform_and_prompt_api_key() method = prompt_select( "How would you like to log in to Confident AI?", [ ("Log in via your browser", "browser"), ("Paste a project API key", "paste"), ], ) if method == "paste": _print_api_key_location() return _prompt_paste_api_key() _prompt_and_persist_region(save) key = _complete_browser_cli_login() if key: return key print("\nNo problem β€” paste a project API key from the platform instead.") _print_api_key_location() return _prompt_paste_api_key() def login_command( save: Optional[str] = typer.Option( None, "-s", "--save", help="Where to persist settings. Format: dotenv[:path]. Defaults to .env.local. If omitted, login still writes to .env.local.", ), api_key: Optional[str] = typer.Option( None, "--api-key", help="Log in non-interactively with a project API key instead of the prompts.", ), ): api_key = coerce_blank_to_none(api_key) with capture_login_event() as span: completed = False try: settings = get_settings() save = save or settings.DEEPEVAL_DEFAULT_SAVE or "dotenv:.env.local" # Resolve the key from the CLI flag or the active interactive flow. if api_key is not None: key = api_key _warn_for_pasted_api_key(key) else: key = _resolve_login_key(save) with settings.edit(save=save) as edit_ctx: settings.CONFIDENT_API_KEY = key handled, path, updated = edit_ctx.result if updated: if not handled and save is not None: # invalid --save format (unsupported) print( "Unsupported --save option. Use --save=dotenv[:path]." ) elif path: # persisted to a file print( f"Saved environment variables to {path} (ensure it's git-ignored)." ) completed = True print("\n[bold]Welcome to[/bold]") render_confident_banner() print( "πŸŽ‰πŸ₯³ Congratulations! You've successfully logged in! :raising_hands:" ) quickstart_url = with_utm( f"{WWW}/docs/llm-evaluation/quickstart", medium="cli", content="login_success_quickstart", ) print( "You're now using DeepEval with [rgb(106,0,255)]Confident AI[/rgb(106,0,255)]. " "Follow our quickstart tutorial here: " f"[bold][link={quickstart_url}]{quickstart_url}[/link][/bold]" ) except Exception as e: completed = False print(f"Login failed: {e}") finally: if getattr(span, "set_attribute", None): span.set_attribute("completed", completed) def logout_command( save: Optional[str] = typer.Option( None, "-s", "--save", help="Where to remove the saved key from. Use format dotenv[:path]. If omitted, uses DEEPEVAL_DEFAULT_SAVE or .env.local. The JSON keystore is always cleared.", ), quiet: bool = typer.Option( False, "-q", "--quiet", help="Suppress printing to the terminal (useful for CI).", ), ): """ Log out of Confident AI. Behavior: - Always clears the Confident API key from the JSON keystore and process env. - Removes the key from every dotenv file deepeval auto-loads (.env, .env.{APP_ENV}, .env.local), plus the --save target if one is given. - If the key is exported by the shell itself, deepeval cannot unset it; a warning with the fix is printed instead of a success message. """ settings = get_settings() # Capture provenance before clearing anything: once files are wiped we # can no longer tell a shell export apart from a file-loaded value. key_source = resolve_setting_source("CONFIDENT_API_KEY") save = save or settings.DEEPEVAL_DEFAULT_SAVE or "dotenv:.env.local" with settings.edit(save=save) as edit_ctx: settings.CONFIDENT_API_KEY = None handled, path, updated = edit_ctx.result # The --save target is a single file; also sweep the rest of the dotenv # search path so a lower-precedence file (e.g. .env) can't silently log # the user back in on the next run. for dotenv_path in dotenv_search_paths(): if dotenv_path.is_file() and "CONFIDENT_API_KEY" in read_dotenv_file( dotenv_path ): DotenvHandler(dotenv_path).unset(["CONFIDENT_API_KEY"]) if not quiet: print(f"Removed Confident AI key(s) from {dotenv_path}.") shell_export = key_source == "process environment" if ( handle_save_result( handled=handled, path=path, updates=updated, save=save, quiet=quiet, updated_msg="Removed Confident AI key(s) from {path}.", tip_msg=None, ) and not shell_export ): print("\nπŸŽ‰πŸ₯³ You've successfully logged out! :raising_hands: ") if shell_export and not quiet: print( "\n[yellow]⚠ CONFIDENT_API_KEY is exported by your shell, which " "deepeval cannot unset β€” this terminal will still be logged " "in.[/yellow]" ) print( " Finish logging out with: " "[bold cyan]unset CONFIDENT_API_KEY[/bold cyan] " "[dim](and remove it from your shell profile if it's set " "there)[/dim]" ) delete_file_if_exists(LATEST_TEST_RUN_FILE_PATH)