diff --git a/src/tui/managers/env_manager.py b/src/tui/managers/env_manager.py index 04ba0a2a..8bfb8369 100644 --- a/src/tui/managers/env_manager.py +++ b/src/tui/managers/env_manager.py @@ -35,6 +35,13 @@ class EnvConfig: langflow_ingest_flow_id: str = "5488df7c-b93f-4f87-a446-b67028bc0813" langflow_url_ingest_flow_id: str = "72c3d17c-2dac-4a73-b48a-6518473d7830" + # Provider API keys and endpoints + anthropic_api_key: str = "" + ollama_endpoint: str = "" + watsonx_api_key: str = "" + watsonx_endpoint: str = "" + watsonx_project_id: str = "" + # OAuth settings google_oauth_client_id: str = "" google_oauth_client_secret: str = "" @@ -128,6 +135,11 @@ class EnvManager: # Map env vars to config attributes attr_map = { "OPENAI_API_KEY": "openai_api_key", + "ANTHROPIC_API_KEY": "anthropic_api_key", + "OLLAMA_ENDPOINT": "ollama_endpoint", + "WATSONX_API_KEY": "watsonx_api_key", + "WATSONX_ENDPOINT": "watsonx_endpoint", + "WATSONX_PROJECT_ID": "watsonx_project_id", "OPENSEARCH_PASSWORD": "opensearch_password", "LANGFLOW_SECRET_KEY": "langflow_secret_key", "LANGFLOW_SUPERUSER": "langflow_superuser", @@ -197,6 +209,30 @@ class EnvManager: "Invalid OpenAI API key format (should start with sk-)" ) + # Import validation functions for new provider fields + from ..utils.validation import validate_anthropic_api_key + + # Validate Anthropic API key format if provided + if self.config.anthropic_api_key: + if not validate_anthropic_api_key(self.config.anthropic_api_key): + self.config.validation_errors["anthropic_api_key"] = ( + "Invalid Anthropic API key format (should start with sk-ant-)" + ) + + # Validate Ollama endpoint if provided + if self.config.ollama_endpoint: + if not validate_url(self.config.ollama_endpoint): + self.config.validation_errors["ollama_endpoint"] = ( + "Invalid Ollama endpoint URL format" + ) + + # Validate IBM watsonx.ai endpoint if provided + if self.config.watsonx_endpoint: + if not validate_url(self.config.watsonx_endpoint): + self.config.validation_errors["watsonx_endpoint"] = ( + "Invalid IBM watsonx.ai endpoint URL format" + ) + # Validate documents paths only if provided (optional) if self.config.openrag_documents_paths: is_valid, error_msg, _ = validate_documents_paths( @@ -289,9 +325,6 @@ class EnvManager: f.write(f"LANGFLOW_URL_INGEST_FLOW_ID={self._quote_env_value(self.config.langflow_url_ingest_flow_id)}\n") f.write(f"NUDGES_FLOW_ID={self._quote_env_value(self.config.nudges_flow_id)}\n") f.write(f"OPENSEARCH_PASSWORD={self._quote_env_value(self.config.opensearch_password)}\n") - # Only write OpenAI API key if provided (can be set during onboarding instead) - if self.config.openai_api_key: - f.write(f"OPENAI_API_KEY={self._quote_env_value(self.config.openai_api_key)}\n") f.write( f"OPENRAG_DOCUMENTS_PATHS={self._quote_env_value(self.config.openrag_documents_paths)}\n" ) @@ -300,6 +333,27 @@ class EnvManager: ) f.write("\n") + # Provider API keys and endpoints (optional - can be set during onboarding) + provider_vars = [] + if self.config.openai_api_key: + provider_vars.append(("OPENAI_API_KEY", self.config.openai_api_key)) + if self.config.anthropic_api_key: + provider_vars.append(("ANTHROPIC_API_KEY", self.config.anthropic_api_key)) + if self.config.ollama_endpoint: + provider_vars.append(("OLLAMA_ENDPOINT", self.config.ollama_endpoint)) + if self.config.watsonx_api_key: + provider_vars.append(("WATSONX_API_KEY", self.config.watsonx_api_key)) + if self.config.watsonx_endpoint: + provider_vars.append(("WATSONX_ENDPOINT", self.config.watsonx_endpoint)) + if self.config.watsonx_project_id: + provider_vars.append(("WATSONX_PROJECT_ID", self.config.watsonx_project_id)) + + if provider_vars: + f.write("# AI Provider API Keys and Endpoints\n") + for var_name, var_value in provider_vars: + f.write(f"{var_name}={self._quote_env_value(var_value)}\n") + f.write("\n") + # Ingestion settings f.write("# Ingestion settings\n") f.write(f"DISABLE_INGEST_WITH_LANGFLOW={self._quote_env_value(self.config.disable_ingest_with_langflow)}\n") diff --git a/src/tui/screens/config.py b/src/tui/screens/config.py index 52d2b291..83107538 100644 --- a/src/tui/screens/config.py +++ b/src/tui/screens/config.py @@ -20,7 +20,13 @@ from rich.text import Text from pathlib import Path from ..managers.env_manager import EnvManager -from ..utils.validation import validate_openai_api_key, validate_documents_paths +from ..utils.validation import ( + validate_openai_api_key, + validate_anthropic_api_key, + validate_ollama_endpoint, + validate_watsonx_endpoint, + validate_documents_paths, +) from pathlib import Path @@ -37,6 +43,45 @@ class OpenAIKeyValidator(Validator): return self.failure("Invalid OpenAI API key format (should start with sk-)") +class AnthropicKeyValidator(Validator): + """Validator for Anthropic API keys.""" + + def validate(self, value: str) -> ValidationResult: + if not value: + return self.success() + + if validate_anthropic_api_key(value): + return self.success() + else: + return self.failure("Invalid Anthropic API key format (should start with sk-ant-)") + + +class OllamaEndpointValidator(Validator): + """Validator for Ollama endpoint URLs.""" + + def validate(self, value: str) -> ValidationResult: + if not value: + return self.success() + + if validate_ollama_endpoint(value): + return self.success() + else: + return self.failure("Invalid Ollama endpoint URL format") + + +class WatsonxEndpointValidator(Validator): + """Validator for IBM watsonx.ai endpoint URLs.""" + + def validate(self, value: str) -> ValidationResult: + if not value: + return self.success() + + if validate_watsonx_endpoint(value): + return self.success() + else: + return self.failure("Invalid watsonx.ai endpoint URL format") + + class DocumentsPathValidator(Validator): """Validator for documents paths.""" @@ -227,6 +272,107 @@ class ConfigScreen(Screen): yield Button("Show", id="toggle-openai-key", variant="default") yield Static(" ") + # Anthropic API Key + yield Label("Anthropic API Key") + yield Static( + Text("Get a key: https://console.anthropic.com/settings/keys", style="dim"), + classes="helper-text", + ) + yield Static( + Text("Can also be provided during onboarding", style="dim italic"), + classes="helper-text", + ) + current_value = getattr(self.env_manager.config, "anthropic_api_key", "") + with Horizontal(id="anthropic-key-row"): + input_widget = Input( + placeholder="sk-ant-...", + value=current_value, + password=True, + validators=[AnthropicKeyValidator()], + id="input-anthropic_api_key", + ) + yield input_widget + self.inputs["anthropic_api_key"] = input_widget + yield Button("Show", id="toggle-anthropic-key", variant="default") + yield Static(" ") + + # Ollama Endpoint + yield Label("Ollama Base URL") + yield Static( + Text("Endpoint of your Ollama server", style="dim"), + classes="helper-text", + ) + yield Static( + Text("Can also be provided during onboarding", style="dim italic"), + classes="helper-text", + ) + current_value = getattr(self.env_manager.config, "ollama_endpoint", "") + input_widget = Input( + placeholder="http://localhost:11434", + value=current_value, + validators=[OllamaEndpointValidator()], + id="input-ollama_endpoint", + ) + yield input_widget + self.inputs["ollama_endpoint"] = input_widget + yield Static(" ") + + # IBM watsonx.ai API Key + yield Label("IBM watsonx.ai API Key") + yield Static( + Text("Get a key: https://cloud.ibm.com/iam/apikeys", style="dim"), + classes="helper-text", + ) + yield Static( + Text("Can also be provided during onboarding", style="dim italic"), + classes="helper-text", + ) + current_value = getattr(self.env_manager.config, "watsonx_api_key", "") + with Horizontal(id="watsonx-key-row"): + input_widget = Input( + placeholder="", + value=current_value, + password=True, + id="input-watsonx_api_key", + ) + yield input_widget + self.inputs["watsonx_api_key"] = input_widget + yield Button("Show", id="toggle-watsonx-key", variant="default") + yield Static(" ") + + # IBM watsonx.ai Endpoint + yield Label("IBM watsonx.ai Endpoint") + yield Static( + Text("Example: https://us-south.ml.cloud.ibm.com", style="dim"), + classes="helper-text", + ) + current_value = getattr(self.env_manager.config, "watsonx_endpoint", "") + input_widget = Input( + placeholder="https://us-south.ml.cloud.ibm.com", + value=current_value, + validators=[WatsonxEndpointValidator()], + id="input-watsonx_endpoint", + ) + yield input_widget + self.inputs["watsonx_endpoint"] = input_widget + yield Static(" ") + + # IBM watsonx.ai Project ID + yield Label("IBM watsonx.ai Project ID") + yield Static( + Text("Find in your IBM Cloud project settings", style="dim"), + classes="helper-text", + ) + current_value = getattr(self.env_manager.config, "watsonx_project_id", "") + input_widget = Input( + placeholder="", + value=current_value, + id="input-watsonx_project_id", + ) + yield input_widget + self.inputs["watsonx_project_id"] = input_widget + yield Static(" ") + # Add OAuth fields only in full mode if self.mode == "full": # Google OAuth Client ID @@ -550,6 +696,18 @@ class ConfigScreen(Screen): if input_widget: input_widget.password = not input_widget.password event.button.label = "🙈" if not input_widget.password else "👁" + elif event.button.id == "toggle-anthropic-key": + # Toggle Anthropic API key visibility + input_widget = self.inputs.get("anthropic_api_key") + if input_widget: + input_widget.password = not input_widget.password + event.button.label = "Hide" if not input_widget.password else "Show" + elif event.button.id == "toggle-watsonx-key": + # Toggle watsonx API key visibility + input_widget = self.inputs.get("watsonx_api_key") + if input_widget: + input_widget.password = not input_widget.password + event.button.label = "Hide" if not input_widget.password else "Show" def action_generate(self) -> None: """Generate secure passwords for admin accounts.""" diff --git a/src/tui/utils/validation.py b/src/tui/utils/validation.py index c9764d00..c91c4f00 100644 --- a/src/tui/utils/validation.py +++ b/src/tui/utils/validation.py @@ -63,6 +63,27 @@ def validate_openai_api_key(key: str) -> bool: return key.startswith("sk-") and len(key) > 20 +def validate_anthropic_api_key(key: str) -> bool: + """Validate Anthropic API key format.""" + if not key: + return False + return key.startswith("sk-ant-") and len(key) > 20 + + +def validate_ollama_endpoint(endpoint: str) -> bool: + """Validate Ollama endpoint URL format.""" + if not endpoint: + return False + return validate_url(endpoint) + + +def validate_watsonx_endpoint(endpoint: str) -> bool: + """Validate IBM watsonx.ai endpoint URL format.""" + if not endpoint: + return False + return validate_url(endpoint) + + def validate_google_oauth_client_id(client_id: str) -> bool: """Validate Google OAuth client ID format.""" if not client_id: