From 933e600e9d60d2ebddb465a4f308cb7d5dba912b Mon Sep 17 00:00:00 2001 From: Edwin Jose Date: Fri, 21 Nov 2025 14:00:55 -0500 Subject: [PATCH] Add support for Anthropic, Ollama, and Watsonx config Introduces fields and validation for Anthropic API key, Ollama endpoint, and IBM watsonx.ai API key, endpoint, and project ID in environment management and configuration screens. Updates validation utilities and config UI to support these providers, allowing users to set and validate credentials and endpoints for additional AI services. --- src/tui/managers/env_manager.py | 60 +++++++++++- src/tui/screens/config.py | 160 +++++++++++++++++++++++++++++++- src/tui/utils/validation.py | 21 +++++ 3 files changed, 237 insertions(+), 4 deletions(-) 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: