diff --git a/.gitignore b/.gitignore index cf98110..e391b9c 100644 --- a/.gitignore +++ b/.gitignore @@ -176,3 +176,5 @@ cython_debug/ # PyPI configuration file .pypirc + +.fake \ No newline at end of file diff --git a/README.md b/README.md index 0dfa42f..409a925 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ pip install -e . ### Initialization -> **⚠️ IMPORTANT:** +> **[WARN] IMPORTANT:** > * If you don't have an existing configuration, first run `corebrain --configure` > * If you need to generate a new API key, use `corebrain --create` > * Never share your API key in public repositories. Use environment variables instead. diff --git a/corebrain/CLI-UI b/corebrain/CLI-UI index 1f4167d..df302b6 160000 --- a/corebrain/CLI-UI +++ b/corebrain/CLI-UI @@ -1 +1 @@ -Subproject commit 1f4167dfc3e9d8b8c654983912b8cb4139d63224 +Subproject commit df302b61afc220f9123e4c818cd3569ff46e3547 diff --git a/corebrain/cli/auth/api_keys.py b/corebrain/cli/auth/api_keys.py index 5be72f0..1b773c4 100644 --- a/corebrain/cli/auth/api_keys.py +++ b/corebrain/cli/auth/api_keys.py @@ -125,7 +125,7 @@ def fetch_api_keys(api_url: str, api_token: str, user_data: Dict[str, Any]) -> O # Verify if the key is active if selected_key.get('active') != True: - print_colored("⚠️ The selected API key is not active. Select another one.", "yellow") + print_colored("[WARN] The selected API key is not active. Select another one.", "yellow") continue # Get information of the selected key @@ -133,10 +133,10 @@ def fetch_api_keys(api_url: str, api_token: str, user_data: Dict[str, Any]) -> O key_value = selected_key.get('key', None) if not key_value: - print_colored("⚠️ The selected API key does not have a valid value.", "yellow") + print_colored("[WARN] The selected API key does not have a valid value.", "yellow") continue - print_colored(f"✅ You selected: {key_name}", "green") + print_colored(f"[OK] You selected: {key_name}", "green") print_colored("Wait while we assign the API key to your SDK...", "yellow") return key_value @@ -216,7 +216,7 @@ def get_api_key_id_from_token(sso_token: str, api_token: str, api_url: str) -> O key_id = key_data.get("id") return key_id else: - print_colored("⚠️ Could not find the API key ID", "yellow") + print_colored("[WARN] Could not find the API key ID", "yellow") return None except Exception as e: @@ -269,7 +269,7 @@ def exchange_sso_token_for_api_token(api_url: str, sso_token: str, user_data: Di print_colored("The response does not contain a valid API token", "red") return None - print_colored("✅ API token successfully obtained", "green") + print_colored("[OK] API token successfully obtained", "green") return api_token except Exception as e: print_colored(f"Error processing JSON response: {str(e)}", "red") diff --git a/corebrain/cli/auth/sso.py b/corebrain/cli/auth/sso.py index cee535f..ed3f8d5 100644 --- a/corebrain/cli/auth/sso.py +++ b/corebrain/cli/auth/sso.py @@ -293,14 +293,14 @@ def handler_factory(*args, **kwargs): # Verify if authentication was completed if auth_completed.is_set(): - print_colored("✅ SSO authentication completed successfully!", "green") + print_colored("[OK] SSO authentication completed successfully!", "green") return result["sso_token"], session_data['user'] else: - print_colored(f"❌ Could not complete SSO authentication in {timeout_seconds} seconds.", "red") + print_colored(f"[ERROR] Could not complete SSO authentication in {timeout_seconds} seconds.", "red") print_colored("You can try again or use a token manually.", "yellow") return None, None, None except Exception as e: - print_colored(f"❌ Error during SSO authentication: {str(e)}", "red") + print_colored(f"[ERROR] Error during SSO authentication: {str(e)}", "red") return None, None, None finally: # Stop the server @@ -405,7 +405,7 @@ def handler_factory(*args, **kwargs): if 'user' in session_data: user_data = session_data['user'] - print_colored("✅ SSO authentication completed successfully!", "green") + print_colored("[OK] SSO authentication completed successfully!", "green") # Get and select an API key api_url = os.environ.get("COREBRAIN_API_URL", DEFAULT_API_URL) @@ -416,7 +416,7 @@ def handler_factory(*args, **kwargs): api_token = exchange_sso_token_for_api_token(api_url, result["sso_token"], user_data) if not api_token: - print_colored("⚠️ Could not obtain an API Token with the SSO Token", "yellow") + print_colored("[WARN] Could not obtain an API Token with the SSO Token", "yellow") return None, None, None # Now that we have the API Token, we get the available API Keys @@ -426,21 +426,21 @@ def handler_factory(*args, **kwargs): # We return the selected api_key return api_key_selected, user_data, api_token else: - print_colored("⚠️ Could not obtain an API Key. Create a new one using the command", "yellow") + print_colored("[WARN] Could not obtain an API Key. Create a new one using the command", "yellow") return None, user_data, api_token else: - print_colored("❌ No valid token was obtained during authentication.", "red") + print_colored("[ERROR] No valid token was obtained during authentication.", "red") return None, None, None # We don't have a token or user data - print_colored("❌ Authentication did not produce a valid token.", "red") + print_colored("[ERROR] Authentication did not produce a valid token.", "red") return None, None, None else: - print_colored(f"❌ Could not complete SSO authentication in {timeout_seconds} seconds.", "red") + print_colored(f"[ERROR] Could not complete SSO authentication in {timeout_seconds} seconds.", "red") print_colored("You can try again or use a token manually.", "yellow") return None, None, None except Exception as e: - print_colored(f"❌ Error during SSO authentication: {str(e)}", "red") + print_colored(f"[ERROR] Error during SSO authentication: {str(e)}", "red") return None, None, None finally: # Stop the server diff --git a/corebrain/cli/commands.py b/corebrain/cli/commands.py index a10363a..f1f7d4f 100644 --- a/corebrain/cli/commands.py +++ b/corebrain/cli/commands.py @@ -44,18 +44,18 @@ def authentication(): sso_token, sso_user = authenticate_with_sso(sso_url) if sso_token: try: - print_colored("✅ Returning SSO Token.", "green") + print_colored("[OK] Returning SSO Token.", "green") print_colored(f"{sso_token}", "blue") - print_colored("✅ Returning User data.", "green") + print_colored("[OK] Returning User data.", "green") print_colored(f"{sso_user}", "blue") return sso_token, sso_user except Exception as e: - print_colored("❌ Could not return SSO Token or SSO User data.", "red") + print_colored("[ERROR] Could not return SSO Token or SSO User data.", "red") return sso_token, sso_user else: - print_colored("❌ Could not authenticate with SSO.", "red") + print_colored("[ERROR] Could not authenticate with SSO.", "red") return None, None def authentication_with_api_key_return(): @@ -64,17 +64,17 @@ def authentication_with_api_key_return(): if api_token: try: - print_colored("✅ User authenticated and SDK is now connected to API.", "green") - print_colored("✅ Returning User data.", "green") + print_colored("[OK] User authenticated and SDK is now connected to API.", "green") + print_colored("[OK] Returning User data.", "green") print_colored(f"{user_data}", "blue") return api_key_selected, user_data, api_token except Exception as e: - print_colored("❌ Could not return SSO Token or SSO User data.", "red") + print_colored("[ERROR] Could not return SSO Token or SSO User data.", "red") return api_key_selected, user_data, api_token else: - print_colored("❌ Could not authenticate with SSO.", "red") + print_colored("[ERROR] Could not authenticate with SSO.", "red") return None, None, None # Argument parser configuration @@ -91,9 +91,14 @@ def authentication_with_api_key_return(): parser.add_argument("--configure", action="store_true", help="Configure the Corebrain SDK") parser.add_argument("--list-configs", action="store_true", help="List available configurations") parser.add_argument("--show-schema", action="store_true", help="Display database schema for a configuration") - parser.add_argument("--woami",action="store_true",help="Display information about the current user") + parser.add_argument("--whoami",action="store_true",help="Display information about the current user") parser.add_argument("--gui", action="store_true", help="Check setup and launch the web interface") + + # Added after: [ERROR] Error when downloading data about user 'Namespace' object has no attribute 'api_key' + parser.add_argument("--api-key", type=str, help="Corebrain API key") + parser.add_argument("--token", type=str, help="Corebrain API token") + args = parser.parse_args(argv) # Common variables @@ -170,13 +175,13 @@ def check_port(host, port, service_name): result = sock.connect_ex((host, port)) sock.close() if result == 0: - print_colored(f"✅ {service_name} is running on {host}:{port}", "green") + print_colored(f"[OK] {service_name} is running on {host}:{port}", "green") return True else: - print_colored(f"❌ {service_name} is not accessible on {host}:{port}", "red") + print_colored(f"[ERROR] {service_name} is not accessible on {host}:{port}", "red") return False except Exception as e: - print_colored(f"❌ Error checking {service_name}: {str(e)}", "red") + print_colored(f"[ERROR] Error checking {service_name}: {str(e)}", "red") return False def check_url(url, service_name): @@ -184,13 +189,13 @@ def check_url(url, service_name): try: response = requests.get(url, timeout=10) if response.status_code < 500: - print_colored(f"✅ {service_name} is accessible at {url}", "green") + print_colored(f"[OK] {service_name} is accessible at {url}", "green") return True else: - print_colored(f"❌ {service_name} returned status {response.status_code} at {url}", "red") + print_colored(f"[ERROR] {service_name} returned status {response.status_code} at {url}", "red") return False except Exception as e: - print_colored(f"❌ {service_name} is not accessible at {url}: {str(e)}", "red") + print_colored(f"[ERROR] {service_name} is not accessible at {url}: {str(e)}", "red") return False def check_library(library_name, min_version): @@ -216,18 +221,18 @@ def check_library(library_name, min_version): parts = import_name.split('.') spec = importlib.util.find_spec(parts[0]) if spec is None: - print_colored(f"❌ {package_name} is not installed", "red") + print_colored(f"[ERROR] {package_name} is not installed", "red") return False # Try to import the full module path try: __import__(import_name) except ImportError: - print_colored(f"❌ {package_name} is not installed", "red") + print_colored(f"[ERROR] {package_name} is not installed", "red") return False else: spec = importlib.util.find_spec(import_name) if spec is None: - print_colored(f"❌ {package_name} is not installed", "red") + print_colored(f"[ERROR] {package_name} is not installed", "red") return False # Try to get version using different methods @@ -254,23 +259,23 @@ def check_library(library_name, min_version): if installed_version is None: raise Exception("Version not found") - print_colored(f"✅ {package_name} {installed_version} is installed", "green") + print_colored(f"[OK] {package_name} {installed_version} is installed", "green") return True except Exception: # If version check fails, at least we know the module can be imported - print_colored(f"✅ {package_name} is installed (version check failed)", "yellow") + print_colored(f"[OK] {package_name} is installed (version check failed)", "yellow") return True except Exception as e: - print_colored(f"❌ Error checking {package_name}: {str(e)}", "red") + print_colored(f"[ERROR] Error checking {package_name}: {str(e)}", "red") return False # Determine if in development or production mode api_url = os.environ.get("COREBRAIN_API_URL") or DEFAULT_API_URL is_development = "localhost" in api_url or "127.0.0.1" in api_url or api_url == DEFAULT_API_URL - print_colored("🔍 Checking system status...", "blue") + print_colored("[SEARCH] Checking system status...", "blue") print_colored(f"Mode: {'Development' if is_development else 'Production'}", "blue") print_colored(f"API URL: {api_url}", "blue") print() @@ -289,7 +294,7 @@ def check_library(library_name, min_version): ] # Check libraries - print_colored("📚 Checking required libraries:", "blue") + print_colored("[LIBRARIES] Checking required libraries:", "blue") for library in required_libraries: if not check_library(library, library.split('>=')[1] if '>=' in library else None): all_checks_passed = False @@ -297,7 +302,7 @@ def check_library(library_name, min_version): # Check services based on mode if is_development: - print_colored("🔧 Development mode - Checking local services:", "blue") + print_colored("[BUILD] Development mode - Checking local services:", "blue") # Check local API server if not check_url(api_url, "API Server"): @@ -312,7 +317,7 @@ def check_library(library_name, min_version): all_checks_passed = False else: - print_colored("🌐 Production mode - Checking remote services:", "blue") + print_colored("[SERVICES] Production mode - Checking remote services:", "blue") # Check production API server if not check_url("https://api.etedata.com", "API Server (Production)"): @@ -325,10 +330,10 @@ def check_library(library_name, min_version): print() if all_checks_passed: - print_colored("✅ All system checks passed!", "green") + print_colored("[OK] All system checks passed!", "green") return 0 else: - print_colored("❌ Some system checks failed. Please review the issues above.", "red") + print_colored("[ERROR] Some system checks failed. Please review the issues above.", "red") return 1 if args.authentication: @@ -407,10 +412,10 @@ def check_library(library_name, min_version): print_colored("Please complete the login process in the browser.", "blue") input("\nPress Enter when you've completed the process or to cancel...") - print_colored("✅ SSO authentication test completed!", "green") + print_colored("[OK] SSO authentication test completed!", "green") return 0 except Exception as e: - print_colored(f"❌ Error during test: {str(e)}", "red") + print_colored(f"[ERROR] Error during test: {str(e)}", "red") return 1 @@ -446,7 +451,7 @@ def check_library(library_name, min_version): sso_token, sso_user = authentication() # Authentica use with SSO if sso_token and sso_user: - print_colored("✅ Enter to create an user and API Key.", "green") + print_colored("[OK] Enter to create an user and API Key.", "green") # Get API URL from environment or use default api_url = os.environ.get("COREBRAIN_API_URL", DEFAULT_API_URL) @@ -489,18 +494,18 @@ def check_library(library_name, min_version): # Check if the request was successful print("response API: ", response) if response.status_code == 200: - print_colored("✅ User and API Key created successfully!", "green") + print_colored("[OK] User and API Key created successfully!", "green") return 0 else: - print_colored(f"❌ Error creating user: {response.text}", "red") + print_colored(f"[ERROR] Error creating user: {response.text}", "red") return 1 except requests.exceptions.RequestException as e: - print_colored(f"❌ Error connecting to API: {str(e)}", "red") + print_colored(f"[ERROR] Error connecting to API: {str(e)}", "red") return 1 else: - print_colored("❌ Could not create the user or the API KEY.", "red") + print_colored("[ERROR] Could not create the user or the API KEY.", "red") return 1 if args.configure or args.list_configs or args.show_schema: @@ -785,7 +790,7 @@ def check_library(library_name, min_version): # print_colored(f"Configuration with ID '{args.config_id}' not found", "red") # return 1 - # print_colored(f"✅ Validating configuration: {args.config_id}", "blue") + # print_colored(f"[OK] Validating configuration: {args.config_id}", "blue") # Create a temporary Corebrain instance to validate # from corebrain.core.client import Corebrain @@ -795,16 +800,16 @@ def check_library(library_name, min_version): # db_config=config, # skip_verification=True # ) - # print_colored("✅ Configuration validation passed!", "green") + # print_colored("[OK] Configuration validation passed!", "green") # print_colored(f"Database type: {config.get('type', 'Unknown')}", "blue") # print_colored(f"Engine: {config.get('engine', 'Unknown')}", "blue") # return 0 # except Exception as validation_error: - # print_colored(f"❌ Configuration validation failed: {str(validation_error)}", "red") + # print_colored(f"[ERROR] Configuration validation failed: {str(validation_error)}", "red") # return 1 # except Exception as e: - # print_colored(f"❌ Error during validation: {str(e)}", "red") + # print_colored(f"[ERROR] Error during validation: {str(e)}", "red") # return 1 #if args.export_config: @@ -882,15 +887,15 @@ def check_library(library_name, min_version): # with open(output_file, 'w', encoding='utf-8') as f: # json.dump(config, f, indent=2, default=str) - # print_colored(f"✅ Configuration exported to: {output_file}", "green") + # print_colored(f"[OK] Configuration exported to: {output_file}", "green") # return 0 # except Exception as e: - # print_colored(f"❌ Error exporting configuration: {str(e)}", "red") + # print_colored(f"[ERROR] Error exporting configuration: {str(e)}", "red") # return 1 - if args.woami: + if args.whoami: """ Display information about the currently authenticated user. @@ -906,7 +911,7 @@ def check_library(library_name, min_version): 4. COREBRAIN_API_TOKEN environment variable 5. SSO authentication (if no other credentials found) - Usage: corebrain --woami [--api-key ] [--token ] [--sso-url ] + Usage: corebrain --whoami [--api-key ] [--token ] [--sso-url ] Information displayed: - User ID and email @@ -936,12 +941,12 @@ def check_library(library_name, min_version): for k, v in user_data.items(): print(f"{k}: {v}") else: - print_colored("❌ Can't find data about user, be sure that you are logged into --login.", "red") + print_colored("[ERROR] Can't find data about user, be sure that you are logged into --login.", "red") return 1 return 0 except Exception as e: - print_colored(f"❌ Error when downloading data about user {str(e)}", "red") + print_colored(f"[ERROR] Error when downloading data about user {str(e)}", "red") return 1 if args.gui: @@ -1012,7 +1017,7 @@ def run_cmd(cmd, cwd=None): cli_ui_path = corebrain_root / "CLI-UI" client_path = cli_ui_path / "client" server_path = cli_ui_path / "server" - api_path = corebrain_root / "wrappers" / "csharp_cli_api" + api_path = corebrain_root / "wrappers" / "csharp_cli_api" / "src" / "CorebrainCLIAPI" # Path validation if not client_path.exists(): @@ -1065,7 +1070,7 @@ def run_in_background_silent(cmd, cwd): else: # If no option was specified, show help parser.print_help() - print_colored("\nTip: Use 'corebrain --login' to login via SSO.", "blue") + print_colored("\nTip: Use 'corebrain --authentication' to login via SSO.", "blue") return 0 except Exception as e: diff --git a/corebrain/cli/config.py b/corebrain/cli/config.py index cf8a175..93dc21c 100644 --- a/corebrain/cli/config.py +++ b/corebrain/cli/config.py @@ -266,10 +266,10 @@ def test_database_connection(api_token: str, db_config: Dict[str, Any], api_url: client.close() # If we got here, the connection was successful - print_colored("✅ Database connection successful!", "green") + print_colored("[OK] Database connection successful!", "green") return True except Exception as e: - print_colored(f"❌ Error connecting to the database: {str(e)}", "red") + print_colored(f"[ERROR] Error connecting to the database: {str(e)}", "red") return False def select_excluded_tables(api_token: str, db_config: Dict[str, Any], api_url: Optional[str] = None, user_data: Optional[Dict[str, Any]] = None) -> List[str]: @@ -347,9 +347,9 @@ def save_configuration(sso_token: str, api_key: str, db_config: Dict[str, Any], # 2. Verify that the configuration was saved locally saved_config = config_manager.get_config(api_key, config_id) if not saved_config: - print_colored("⚠️ Could not verify local saving of configuration", "yellow") + print_colored("[WARN] Could not verify local saving of configuration", "yellow") else: - print_colored("✅ Configuration saved locally successfully", "green") + print_colored("[OK] Configuration saved locally successfully", "green") # 3. Try to sync with the server try: @@ -394,19 +394,19 @@ def save_configuration(sso_token: str, api_key: str, db_config: Dict[str, Any], ) if response.status_code in [200, 201, 204]: - print_colored("✅ Configuration successfully synced with server", "green") + print_colored("[OK] Configuration successfully synced with server", "green") else: - print_colored(f"⚠️ Error syncing with server (Code: {response.status_code})", "yellow") + print_colored(f"[WARN] Error syncing with server (Code: {response.status_code})", "yellow") print_colored(f"Response: {response.text[:200]}...", "yellow") except Exception as e: - print_colored(f"⚠️ Error syncing with server: {str(e)}", "yellow") + print_colored(f"[WARN] Error syncing with server: {str(e)}", "yellow") print_colored("The configuration is still saved locally", "green") return True except Exception as e: - print_colored(f"❌ Error saving configuration: {str(e)}", "red") + print_colored(f"[ERROR] Error saving configuration: {str(e)}", "red") return False def configure_sdk(api_token: str, api_key: str, api_url: Optional[str] = None, sso_url: Optional[str] = None, user_data: Optional[Dict[str, Any]] = None) -> None: @@ -443,7 +443,7 @@ def configure_sdk(api_token: str, api_key: str, api_url: Optional[str] = None, s # PHASE 5: Verify database connection print_colored("\n5. Verifying database connection...", "blue") if not test_database_connection(api_key, db_config, api_url, user_data): - print_colored("❌ Configuration not completed due to connection errors.", "red") + print_colored("[ERROR] Configuration not completed due to connection errors.", "red") return # PHASE 6: Define non-accessible tables/collections @@ -457,7 +457,7 @@ def configure_sdk(api_token: str, api_key: str, api_url: Optional[str] = None, s # Save the configuration if not save_configuration(api_token, api_key, db_config, api_url): - print_colored("❌ Error saving configuration.", "red") + print_colored("[ERROR] Error saving configuration.", "red") return """ # * --> Deactivated @@ -466,12 +466,12 @@ def configure_sdk(api_token: str, api_key: str, api_url: Optional[str] = None, s print_colored("\n8. Testing natural language query...", "blue") test_natural_language_query(api_key, db_config, api_url, user_data) except Exception as e: - print_colored(f"⚠️ Could not perform the query test: {str(e)}", "yellow") + print_colored(f"[WARN] Could not perform the query test: {str(e)}", "yellow") print_colored("This does not affect the saved configuration.", "yellow") """ # Final message - print_colored("\n✅ Configuration completed successfully!", "green") + print_colored("\n[OK] Configuration completed successfully!", "green") print_colored(f"\nYou can use this SDK in your code with:", "blue") print(f""" from corebrain import init diff --git a/corebrain/cli/utils.py b/corebrain/cli/utils.py index 6c0ccac..90de467 100644 --- a/corebrain/cli/utils.py +++ b/corebrain/cli/utils.py @@ -374,7 +374,7 @@ def finish(self, message: Optional[str] = None) -> None: elapsed = time.time() - self.start_time msg = message or f"{self.current_task} completed" - print_colored(f"✅ {msg} in {elapsed:.2f}s", "green") + print_colored(f"[OK] {msg} in {elapsed:.2f}s", "green") self.reset() @@ -393,7 +393,7 @@ def fail(self, message: Optional[str] = None) -> None: elapsed = time.time() - self.start_time msg = message or f"{self.current_task} failed" - print_colored(f"❌ {msg} after {elapsed:.2f}s", "red") + print_colored(f"[ERROR] {msg} after {elapsed:.2f}s", "red") self.reset() diff --git a/corebrain/core/client.py b/corebrain/core/client.py index 7a83c57..a584527 100644 --- a/corebrain/core/client.py +++ b/corebrain/core/client.py @@ -473,7 +473,7 @@ def _extract_db_schema(self, detail_level: str = "full", specific_collections: L Returns: Dictionary with the database structure organized by tables/collections """ - logger.info(f"Extrayendo esquema de base de datos. Tipo: {self.db_config['type']}, Motor: {self.db_config.get('engine')}") + logger.info(f"Extracting database schema. Type: {self.db_config['type']}, Engine: {self.db_config.get('engine')}") db_type = self.db_config["type"].lower() schema = { @@ -481,53 +481,53 @@ def _extract_db_schema(self, detail_level: str = "full", specific_collections: L "database": self.db_config.get("database", ""), "tables": {}, "total_collections": 0, # Add total counter - "included_collections": 0 # Included counter + "included_collections": 0 # Counter for included ones } excluded_tables = set(self.db_config.get("excluded_tables", [])) - logger.info(f"Tablas excluidas: {excluded_tables}") + logger.info(f"Excluded tables: {excluded_tables}") try: if db_type == "sql": engine = self.db_config.get("engine", "").lower() - logger.info(f"Procesando base de datos SQL con motor: {engine}") + logger.info(f"Processing SQL database with engine: {engine}") if engine in ["sqlite", "mysql", "postgresql"]: cursor = self.db_connection.cursor() if engine == "sqlite": - logger.info("Obteniendo tablas de SQLite") - # Get list of tables + logger.info("Getting SQLite tables") + # Get table listing cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") tables = cursor.fetchall() - logger.info(f"Tablas encontradas en SQLite: {tables}") + logger.info(f"Tables found in SQLite: {tables}") elif engine == "mysql": - logger.info("Obteniendo tablas de MySQL") + logger.info("Getting MySQL tables") cursor.execute("SHOW TABLES;") tables = cursor.fetchall() - logger.info(f"Tablas encontradas en MySQL: {tables}") + logger.info(f"Tables found in MySQL: {tables}") elif engine == "postgresql": - logger.info("Obteniendo tablas de PostgreSQL") + logger.info("Getting PostgreSQL tables") cursor.execute(""" SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'; """) tables = cursor.fetchall() - logger.info(f"Tablas encontradas en PostgreSQL: {tables}") + logger.info(f"Tables found in PostgreSQL: {tables}") # Process the found tables for table in tables: table_name = table[0] - logger.info(f"Procesando tabla: {table_name}") + logger.info(f"Processing table: {table_name}") # Skip excluded tables if table_name in excluded_tables: - logger.info(f"Saltando tabla excluida: {table_name}") + logger.info(f"Skipping excluded table: {table_name}") continue try: - # Get column information based on the engine + # Get column information according to engine if engine == "sqlite": cursor.execute(f"PRAGMA table_info({table_name});") elif engine == "mysql": @@ -540,9 +540,9 @@ def _extract_db_schema(self, detail_level: str = "full", specific_collections: L """) columns = cursor.fetchall() - logger.info(f"Columnas encontradas para {table_name}: {columns}") + logger.info(f"Columns found for {table_name}: {columns}") - # Column structure according to the engine + # Column structure according to engine if engine == "sqlite": column_info = [{"name": col[1], "type": col[2]} for col in columns] elif engine == "mysql": @@ -557,18 +557,18 @@ def _extract_db_schema(self, detail_level: str = "full", specific_collections: L } except Exception as e: - logger.error(f"Error procesando tabla {table_name}: {str(e)}") + logger.error(f"Error processing table {table_name}: {str(e)}") else: # Using SQLAlchemy - logger.info("Usando SQLAlchemy para obtener el esquema") + logger.info("Using SQLAlchemy to get schema") inspector = inspect(self.db_connection) table_names = inspector.get_table_names() - logger.info(f"Tablas encontradas con SQLAlchemy: {table_names}") + logger.info(f"Tables found with SQLAlchemy: {table_names}") for table_name in table_names: if table_name in excluded_tables: - logger.info(f"Saltando tabla excluida: {table_name}") + logger.info(f"Skipping excluded table: {table_name}") continue try: @@ -580,12 +580,12 @@ def _extract_db_schema(self, detail_level: str = "full", specific_collections: L "sample_data": [] } except Exception as e: - logger.error(f"Error procesando tabla {table_name} con SQLAlchemy: {str(e)}") + logger.error(f"Error processing table {table_name} with SQLAlchemy: {str(e)}") elif db_type in ["nosql", "mongodb"]: - logger.info("Procesando base de datos MongoDB") + logger.info("Processing MongoDB database") if not hasattr(self, 'db_connection') or self.db_connection is None: - logger.error("La conexión a MongoDB no está disponible") + logger.error("MongoDB connection is not available") return schema try: @@ -593,9 +593,9 @@ def _extract_db_schema(self, detail_level: str = "full", specific_collections: L try: collection_names = self.db_connection.list_collection_names() schema["total_collections"] = len(collection_names) - logger.info(f"Colecciones encontradas en MongoDB: {collection_names}") + logger.info(f"Collections found in MongoDB: {collection_names}") except Exception as e: - logger.error(f"Error al obtener colecciones MongoDB: {str(e)}") + logger.error(f"Error getting MongoDB collections: {str(e)}") return schema # If we only want the names @@ -606,12 +606,12 @@ def _extract_db_schema(self, detail_level: str = "full", specific_collections: L # Process each collection for collection_name in collection_names: if collection_name in excluded_tables: - logger.info(f"Saltando colección excluida: {collection_name}") + logger.info(f"Skipping excluded collection: {collection_name}") continue try: collection = self.db_connection[collection_name] - # Convert table dictionary to list + # Get a document to infer structure first_doc = collection.find_one() if first_doc: @@ -625,20 +625,20 @@ def _extract_db_schema(self, detail_level: str = "full", specific_collections: L "fields": fields, "doc_count": collection.estimated_document_count() } - logger.info(f"Procesada colección {collection_name} con {len(fields)} campos") + logger.info(f"Processed collection {collection_name} with {len(fields)} fields") else: - logger.info(f"Colección {collection_name} está vacía") + logger.info(f"Collection {collection_name} is empty") schema["tables"][collection_name] = { "fields": [], "doc_count": 0 } except Exception as e: - logger.error(f"Error procesando colección {collection_name}: {str(e)}") + logger.error(f"Error processing collection {collection_name}: {str(e)}") except Exception as e: - logger.error(f"Error general procesando MongoDB: {str(e)}") + logger.error(f"General error processing MongoDB: {str(e)}") - # Convert table dictionary to list + # Convert the table dictionary to a list table_list = [] for table_name, table_info in schema["tables"].items(): table_data = {"name": table_name} @@ -646,13 +646,13 @@ def _extract_db_schema(self, detail_level: str = "full", specific_collections: L table_list.append(table_data) schema["tables_list"] = table_list - logger.info(f"Esquema final - Tablas encontradas: {len(schema['tables'])}") - logger.info(f"Nombres de tablas: {list(schema['tables'].keys())}") + logger.info(f"Final schema - Tables found: {len(schema['tables'])}") + logger.info(f"Table names: {list(schema['tables'].keys())}") return schema except Exception as e: - logger.error(f"Error al extraer el esquema de la base de datos: {str(e)}") + logger.error(f"Error extracting database schema: {str(e)}") return {"type": db_type, "tables": {}, "tables_list": []} def list_collections_name(self) -> List[str]: @@ -694,8 +694,8 @@ def ask(self, question: str, **kwargs) -> Dict: # Validate that the schema has tables/collections if not schema.get("tables"): - print("Error: No se encontraron tablas/colecciones en la base de datos") - return {"error": True, "explanation": "No se encontraron tablas/colecciones en la base de datos"} + print("Error: No tables/collections found in the database") + return {"error": True, "explanation": "No tables/collections found in the database"} # Get table names available for validation available_tables = set() @@ -751,7 +751,7 @@ def ask(self, question: str, **kwargs) -> Dict: # Check answer if response.status_code != 200: - error_msg = f"Error {response.status_code} al realizar la consulta" + error_msg = f"Error {response.status_code} while performing query" try: error_data = response.json() if isinstance(error_data, dict): @@ -771,7 +771,7 @@ def ask(self, question: str, **kwargs) -> Dict: if "query" not in api_response: return { "error": True, - "explanation": "La API no generó una consulta válida." + "explanation": "The API did not generate a valid query." } # If the query should be executed but the API did not @@ -789,7 +789,7 @@ def ask(self, question: str, **kwargs) -> Dict: if isinstance(sql_candidate, str): query_value = sql_candidate else: - raise CorebrainError(f"La consulta SQL generada no es un string: {query_value}") + raise CorebrainError(f"The generated SQL query is not a string: {query_value}") # Prepare the consultation with the appropriate format query_to_execute = { @@ -812,9 +812,9 @@ def ask(self, question: str, **kwargs) -> Dict: # Validate collection name if not collection_name: - raise CorebrainError("No se especificó colección y no se encontraron colecciones en el esquema") + raise CorebrainError("No collection specified and no collections found in schema") if not isinstance(collection_name, str) or not collection_name.strip(): - raise CorebrainError("Nombre de colección inválido: debe ser un string no vacío") + raise CorebrainError("Invalid collection name: must be a non-empty string") # Add collection to query query_to_execute["collection"] = collection_name @@ -871,15 +871,15 @@ def ask(self, question: str, **kwargs) -> Dict: if explanation_response.status_code == 200: explanation_result = explanation_response.json() - api_response["explanation"] = explanation_result.get("explanation", "No se pudo generar una explicación.") + api_response["explanation"] = explanation_result.get("explanation", "Could not generate an explanation.") else: api_response["explanation"] = self._generate_fallback_explanation(query_to_execute, query_result) except Exception as explain_error: - logger.error(f"Error al obtener explicación: {str(explain_error)}") + logger.error(f"Error getting explanation: {str(explain_error)}") api_response["explanation"] = self._generate_fallback_explanation(query_to_execute, query_result) except Exception as e: - error_msg = f"Error al ejecutar la consulta: {str(e)}" + error_msg = f"Error executing query: {str(e)}" logger.error(error_msg) return { "error": True, @@ -911,10 +911,10 @@ def ask(self, question: str, **kwargs) -> Dict: # For MongoDB or generic api_response["explanation"] = self._generate_generic_explanation(api_response["query"], result_data) else: - api_response["explanation"] = "La consulta se ha ejecutado correctamente." + api_response["explanation"] = "The query executed successfully." except Exception as exp_fix_error: - logger.error(f"Error al corregir explicación: {str(exp_fix_error)}") - api_response["explanation"] = "La consulta se ha ejecutado correctamente." + logger.error(f"Error correcting explanation: {str(exp_fix_error)}") + api_response["explanation"] = "The query executed successfully." # Prepare the final response result = { @@ -945,16 +945,16 @@ def ask(self, question: str, **kwargs) -> Dict: return result except httpx.TimeoutException: - return {"error": True, "explanation": "Tiempo de espera agotado al conectar con el servidor."} + return {"error": True, "explanation": "Timeout waiting to connect to server."} except httpx.RequestError as e: - return {"error": True, "explanation": f"Error de conexión con el servidor: {str(e)}"} + return {"error": True, "explanation": f"Connection error with server: {str(e)}"} except Exception as e: import traceback error_details = traceback.format_exc() - logger.error(f"Error inesperado en ask(): {error_details}") - return {"error": True, "explanation": f"Error inesperado: {str(e)}"} + logger.error(f"Unexpected error in ask(): {error_details}") + return {"error": True, "explanation": f"Unexpected error: {str(e)}"} def _generate_fallback_explanation(self, query, results): """ @@ -978,7 +978,7 @@ def _generate_fallback_explanation(self, query, results): # Generic Fallback result_count = len(results) if isinstance(results, list) else (1 if results else 0) - return f"La consulta devolvió {result_count} resultados." + return f"The query returned {result_count} results." def _generate_sql_explanation(self, sql_query, results): """ @@ -1009,28 +1009,28 @@ def _generate_sql_explanation(self, sql_query, results): if "join" in sql_lower: if len(tables) > 1: if "where" in sql_lower: - return f"Se encontraron {result_count} registros que cumplen con los criterios especificados, relacionando información de las tablas {', '.join(tables)}." + return f"Found {result_count} records that meet the specified criteria, relating information from tables {', '.join(tables)}." else: - return f"Se obtuvieron {result_count} registros relacionando información de las tablas {', '.join(tables)}." + return f"Found {result_count} records relating information from tables {', '.join(tables)}." else: - return f"Se obtuvieron {result_count} registros relacionando datos entre tablas." + return f"Found {result_count} records relating data between tables." elif "where" in sql_lower: - return f"Se encontraron {result_count} registros que cumplen con los criterios de búsqueda." + return f"Found {result_count} records that meet the search criteria." else: - return f"La consulta devolvió {result_count} registros de la base de datos." + return f"The query returned {result_count} records from the database." # For other types of queries (INSERT, UPDATE, DELETE) if "insert" in sql_lower: - return "Se insertaron correctamente los datos en la base de datos." + return "Data inserted successfully into the database." elif "update" in sql_lower: - return "Se actualizaron correctamente los datos en la base de datos." + return "Data updated successfully in the database." elif "delete" in sql_lower: - return "Se eliminaron correctamente los datos de la base de datos." + return "Data deleted successfully from the database." - # Generic Fallback - return f"La consulta SQL se ejecutó correctamente y devolvió {result_count} resultados." + # Fallback genérico + return f"The SQL query executed successfully and returned {result_count} results." def _generate_mongodb_explanation(self, query, results): @@ -1044,29 +1044,29 @@ def _generate_mongodb_explanation(self, query, results): Returns: Generated explanation """ - collection = query.get("collection", "la colección") + collection = query.get("collection", "the collection") operation = query.get("operation", "find") result_count = len(results) if isinstance(results, list) else (1 if results else 0) # Generate explanation according to the operation if operation == "find": - return f"Se encontraron {result_count} documentos en la colección {collection} que coinciden con los criterios de búsqueda." + return f"Found {result_count} documents in the {collection} that meet the search criteria." elif operation == "findOne": if result_count > 0: - return f"Se encontró el documento solicitado en la colección {collection}." + return f"Found the requested document in the {collection}." else: - return f"No se encontró ningún documento en la colección {collection} que coincida con los criterios." + return f"No documents found in the {collection} that meet the criteria." elif operation == "aggregate": - return f"La agregación en la colección {collection} devolvió {result_count} resultados." + return f"The aggregation in the {collection} returned {result_count} results." elif operation == "insertOne": - return f"Se ha insertado correctamente un nuevo documento en la colección {collection}." + return f"A new document inserted successfully into the {collection}." elif operation == "updateOne": - return f"Se ha actualizado correctamente un documento en la colección {collection}." + return f"A document updated successfully in the {collection}." elif operation == "deleteOne": - return f"Se ha eliminado correctamente un documento de la colección {collection}." + return f"A document deleted successfully from the {collection}." - # Generic Fallback - return f"La operación {operation} se ejecutó correctamente en la colección {collection} y devolvió {result_count} resultados." + # Fallback genérico + return f"The {operation} operation executed successfully in the {collection} and returned {result_count} results." def _generate_generic_explanation(self, query, results): @@ -1083,11 +1083,11 @@ def _generate_generic_explanation(self, query, results): result_count = len(results) if isinstance(results, list) else (1 if results else 0) if result_count == 0: - return "La consulta no devolvió ningún resultado." + return "The query returned no results." elif result_count == 1: - return "La consulta devolvió 1 resultado." + return "The query returned 1 result." else: - return f"La consulta devolvió {result_count} resultados." + return f"The query returned {result_count} results." def close(self) -> None: diff --git a/corebrain/core/test_utils.py b/corebrain/core/test_utils.py index 7097e39..46b7957 100644 --- a/corebrain/core/test_utils.py +++ b/corebrain/core/test_utils.py @@ -20,12 +20,12 @@ def generate_test_question_from_schema(schema: Dict[str, Any]) -> str: Generated test question """ if not schema or not schema.get("tables"): - return "¿Cuáles son las tablas disponibles?" + return "What are the available tables?" tables = schema["tables"] if not tables: - return "¿Cuáles son las tablas disponibles?" + return "What are the available tables?" # Select a random table table = random.choice(tables) @@ -33,12 +33,12 @@ def generate_test_question_from_schema(schema: Dict[str, Any]) -> str: # Determine the type of question question_types = [ - f"¿Cuántos registros hay en la tabla {table_name}?", - f"Muestra los primeros 5 registros de {table_name}", - f"¿Cuáles son los campos de la tabla {table_name}?", + f"How many records are in the {table_name} table?", + f"Show the first 5 records from {table_name}", + f"What are the fields in the {table_name} table?", ] - # Getting columns by structure (SQL vs NoSQL) + # Get columns according to structure (SQL vs NoSQL) columns = [] if "columns" in table and table["columns"]: columns = table["columns"] @@ -51,8 +51,8 @@ def generate_test_question_from_schema(schema: Dict[str, Any]) -> str: # Add specific questions with columns question_types.extend([ - f"¿Cuál es el valor máximo de {column_name} en {table_name}?", - f"¿Cuáles son los valores únicos de {column_name} en {table_name}?", + f"What is the maximum value of {column_name} in {table_name}?", + f"What are the unique values of {column_name} in {table_name}?", ]) return random.choice(question_types) @@ -71,16 +71,16 @@ def test_natural_language_query(api_token: str, db_config: Dict[str, Any], api_u True if the test is successful, False otherwise """ try: - print_colored("\nRealizando prueba de consulta en lenguaje natural...", "blue") + print_colored("\nPerforming natural language query test...", "blue") # Dynamic import to avoid circular imports from db.schema_file import extract_db_schema - # Generate a test question based on the extracted schema directly + # Generate a test question based on the directly extracted schema schema = extract_db_schema(db_config) - print("REcoge esquema: ", schema) + print("Retrieved schema: ", schema) question = generate_test_question_from_schema(schema) - print(f"Pregunta de prueba: {question}") + print(f"Test question: {question}") # Prepare the data for the request api_url = api_url or DEFAULT_API_URL @@ -93,7 +93,7 @@ def test_natural_language_query(api_token: str, db_config: Dict[str, Any], api_u # Build endpoint for the query endpoint = f"{api_url}/api/database/sdk/query" - # Data for consultation + # Data for the query request_data = { "question": question, "db_schema": schema, @@ -109,7 +109,7 @@ def test_natural_language_query(api_token: str, db_config: Dict[str, Any], api_u timeout = 15.0 # Reduced maximum waiting time try: - print_colored("Enviando consulta al API...", "blue") + print_colored("Sending query to API...", "blue") response = http_session.post( endpoint, headers=headers, @@ -123,19 +123,19 @@ def test_natural_language_query(api_token: str, db_config: Dict[str, Any], api_u # Check if there is an explanation in the result if "explanation" in result: - print_colored("\nRespuesta:", "green") + print_colored("\nResponse:", "green") print(result["explanation"]) - print_colored("\n✅ Prueba de consulta exitosa!", "green") + print_colored("\n[OK] Query test successful!", "green") return True else: # If there is no explanation but the API responds, it may be a different format print_colored("\nRespuesta recibida del API (formato diferente al esperado):", "yellow") print(json.dumps(result, indent=2)) - print_colored("\n⚠️ La API respondió, pero con un formato diferente al esperado.", "yellow") + print_colored("\n[WARN] The API responded, but with a different format than expected.", "yellow") return True else: - print_colored(f"❌ Error en la respuesta: Código {response.status_code}", "red") + print_colored(f"[ERROR] Error in response: Code {response.status_code}", "red") try: error_data = response.json() print(json.dumps(error_data, indent=2)) @@ -144,14 +144,14 @@ def test_natural_language_query(api_token: str, db_config: Dict[str, Any], api_u return False except http_session.TimeoutException: - print_colored("⚠️ Timeout al realizar la consulta. El API puede estar ocupado o no disponible.", "yellow") - print_colored("Esto no afecta a la configuración guardada.", "yellow") + print_colored("[WARN] Timeout while performing query. The API may be busy or unavailable.", "yellow") + print_colored("This does not affect the saved configuration.", "yellow") return False except http_session.RequestError as e: - print_colored(f"⚠️ Error de conexión: {str(e)}", "yellow") - print_colored("Verifica la URL de la API y tu conexión a internet.", "yellow") + print_colored(f"[WARN] Connection error: {str(e)}", "yellow") + print_colored("Check the API URL and your internet connection.", "yellow") return False except Exception as e: - print_colored(f"❌ Error al realizar la consulta: {str(e)}", "red") + print_colored(f"[ERROR] Error performing query: {str(e)}", "red") return False \ No newline at end of file diff --git a/corebrain/db/schema_file.py b/corebrain/db/schema_file.py index c5979c3..eba7b72 100644 --- a/corebrain/db/schema_file.py +++ b/corebrain/db/schema_file.py @@ -67,7 +67,7 @@ def extract_db_schema(db_config: Dict[str, Any]) -> Dict[str, Any]: # Get the database db_name = db_config.get("database", "") if not db_name: - _print_colored("⚠️ Database name not specified", "yellow") + _print_colored("[WARN] Database name not specified", "yellow") return schema try: @@ -308,9 +308,9 @@ def extract_schema_to_file(api_key: str, config_id: Optional[str] = None, output try: with open(output_path, 'w', encoding='utf-8') as f: json.dump(schema, f, indent=2, default=str) - _print_colored(f"✅ Schema extracted and saved in: {output_path}", "green") + _print_colored(f"[OK] Schema extracted and saved in: {output_path}", "green") except Exception as e: - _print_colored(f"❌ Error saving the file: {str(e)}", "red") + _print_colored(f"[ERROR] Error saving the file: {str(e)}", "red") return False # Show a summary of the tables/collections found @@ -323,7 +323,7 @@ def extract_schema_to_file(api_key: str, config_id: Optional[str] = None, output return True except Exception as e: - _print_colored(f"❌ Error extracting schema: {str(e)}", "red") + _print_colored(f"[ERROR] Error extracting schema: {str(e)}", "red") return False def show_db_schema(api_token: str, config_id: Optional[str] = None, api_url: Optional[str] = None) -> None: @@ -535,7 +535,7 @@ def show_db_schema(api_token: str, config_id: Optional[str] = None, api_url: Opt if len(sample_data) > 2: print(f" ... ({len(sample_data) - 2} more documents)") - _print_colored("\n✅ Schema extracted correctly!", "green") + _print_colored("\n[OK] Schema extracted correctly!", "green") # Ask if you want to save the schema in a file save_option = input("\nDo you want to save the schema in a file? (s/n): ").strip().lower() @@ -544,12 +544,12 @@ def show_db_schema(api_token: str, config_id: Optional[str] = None, api_url: Opt try: with open(filename, 'w') as f: json.dump(schema, f, indent=2, default=str) - _print_colored(f"\n✅ Schema saved in: {filename}", "green") + _print_colored(f"\n[OK] Schema saved in: {filename}", "green") except Exception as e: - _print_colored(f"❌ Error saving the file: {str(e)}", "red") + _print_colored(f"[ERROR] Error saving the file: {str(e)}", "red") except Exception as e: - _print_colored(f"❌ Error showing the schema: {str(e)}", "red") + _print_colored(f"[ERROR] Error showing the schema: {str(e)}", "red") import traceback traceback.print_exc() diff --git a/corebrain/wrappers/csharp/CorebrainCS.Tests/InteractiveTests.cs b/corebrain/wrappers/csharp/CorebrainCS.Tests/InteractiveTests.cs new file mode 100644 index 0000000..c2eb13e --- /dev/null +++ b/corebrain/wrappers/csharp/CorebrainCS.Tests/InteractiveTests.cs @@ -0,0 +1,36 @@ +using System.Threading; +using Xunit; +using CorebrainCS; + +public class InteractiveTests +{ + [Fact] + public void Should_Handle_MultiStage_Flow() + { + var corebrain = new CorebrainCS.CorebrainCS( + pythonPath: "../../../../venv/Scripts/python.exe", + scriptPath: "../../../cli", + verbose: true + ); + + using var session = corebrain.StartInteractiveSession(); + + // Etap 1: Rozpoczęcie konfiguracji + session.SendCommand("--configure"); + Thread.Sleep(500); + + // Etap 2: Wybór typu bazy danych + session.SendCommand("2"); // MongoDB + Thread.Sleep(500); + + // Etap 3: Podanie connection stringa + session.SendCommand("mongodb://localhost:27017"); + Thread.Sleep(500); + + // Etap 4: Potwierdzenie + session.SendCommand("Y"); + + var output = session.GetOutput(); + Assert.Contains("Configuration saved", output); + } +} \ No newline at end of file diff --git a/corebrain/wrappers/csharp/CorebrainCS/CorebrainCS.cs b/corebrain/wrappers/csharp/CorebrainCS/CorebrainCS.cs index a484a6d..b438908 100644 --- a/corebrain/wrappers/csharp/CorebrainCS/CorebrainCS.cs +++ b/corebrain/wrappers/csharp/CorebrainCS/CorebrainCS.cs @@ -10,7 +10,8 @@ ///Path to the python which works with the corebrain cli, for example if you create the ./.venv you pass the path to the ./.venv python executable /// Path to the corebrain cli script, if you installed it globally you just pass the `corebrain` path /// -public class CorebrainCS(string pythonPath = "python", string scriptPath = "corebrain", bool verbose = false) { +public class CorebrainCS(string pythonPath = "python", string scriptPath = "corebrain", bool verbose = false) +{ private readonly string _pythonPath = Path.GetFullPath(pythonPath); private readonly string _scriptPath = Path.GetFullPath(scriptPath); private readonly bool _verbose = verbose; @@ -19,7 +20,8 @@ public class CorebrainCS(string pythonPath = "python", string scriptPath = "core /// Shows help message with all available commands /// /// - public string Help() { + public string Help() + { return ExecuteCommand("--help"); } @@ -27,7 +29,8 @@ public string Help() { /// Shows the current version of the Corebrain SDK /// /// - public string Version() { + public string Version() + { return ExecuteCommand("--version"); } @@ -40,7 +43,8 @@ public string Version() { /// - SSO Server status /// - MongoDB status /// - Required libraries installation - public string CheckStatus() { + public string CheckStatus() + { return ExecuteCommand("--check-status"); } /// @@ -50,10 +54,12 @@ public string CheckStatus() { /// /// /// - public string CheckStatus(string? apiUrl = null, string? token = null) { + public string CheckStatus(string? apiUrl = null, string? token = null) + { var args = new List { "--check-status" }; - if (!string.IsNullOrEmpty(apiUrl)) { + if (!string.IsNullOrEmpty(apiUrl)) + { if (!Uri.IsWellFormedUriString(apiUrl, UriKind.Absolute)) throw new ArgumentException("Invalid API URL format", nameof(apiUrl)); @@ -73,12 +79,15 @@ public string CheckStatus(string? apiUrl = null, string? token = null) { /// /// /// - public string Authentication(string username, string password) { - if (string.IsNullOrWhiteSpace(username)) { + public string Authentication(string username, string password) + { + if (string.IsNullOrWhiteSpace(username)) + { throw new ArgumentException("Username cannot be empty or whitespace", nameof(username)); } - if (string.IsNullOrWhiteSpace(password)) { + if (string.IsNullOrWhiteSpace(password)) + { throw new ArgumentException("Password cannot be empty or whitespace", nameof(password)); } @@ -95,8 +104,10 @@ public string Authentication(string username, string password) { /// /// /// - public string AuthenticationWithToken(string token) { - if (string.IsNullOrWhiteSpace(token)) { + public string AuthenticationWithToken(string token) + { + if (string.IsNullOrWhiteSpace(token)) + { throw new ArgumentException("Token cannot be empty or whitespace", nameof(token)); } @@ -108,7 +119,8 @@ public string AuthenticationWithToken(string token) { /// Creates a new user account and generates an associated API Key /// /// - public string CreateUser() { + public string CreateUser() + { return ExecuteCommand("--create-user"); } @@ -116,7 +128,8 @@ public string CreateUser() { /// Launches the configuration wizard for setting up database connections /// /// - public string Configure() { + public string Configure() + { return ExecuteCommand("--configure"); } @@ -124,7 +137,8 @@ public string Configure() { /// Lists all available database configurations /// /// - public string ListConfigs() { + public string ListConfigs() + { return ExecuteCommand("--list-configs"); } @@ -132,7 +146,8 @@ public string ListConfigs() { /// Displays the database schema for a configured database /// /// - public string ShowSchema() { + public string ShowSchema() + { return ExecuteCommand("--show-schema"); } @@ -140,24 +155,30 @@ public string ShowSchema() { /// Displays information about the currently authenticated user /// /// - public string WhoAmI() { - return ExecuteCommand("--woami"); + public string WhoAmI() + { + return ExecuteCommand("--whoami"); } /// /// Launches the web-based graphical user interface /// /// - public string Gui() { + public string Gui() + { return ExecuteCommand("--gui"); } - private string ExecuteCommand(string arguments) { - if (_verbose) { + public string ExecuteCommand(string arguments) + { + if (_verbose) + { Console.WriteLine($"Executing: {_pythonPath} {_scriptPath} {arguments}"); } - var process = new Process { - StartInfo = new ProcessStartInfo { + var process = new Process + { + StartInfo = new ProcessStartInfo + { FileName = _pythonPath, Arguments = $"\"{_scriptPath}\" {arguments}", RedirectStandardOutput = true, @@ -172,18 +193,31 @@ private string ExecuteCommand(string arguments) { var error = process.StandardError.ReadToEnd(); process.WaitForExit(); - if (_verbose) { + if (_verbose) + { Console.WriteLine("Command output:"); Console.WriteLine(output); - if (!string.IsNullOrEmpty(error)) { + if (!string.IsNullOrEmpty(error)) + { Console.WriteLine("Error output:\n" + error); } } - if (!string.IsNullOrEmpty(error)) { + if (!string.IsNullOrEmpty(error)) + { throw new InvalidOperationException($"Python CLI error: {error}"); } return output.Trim(); } + + /// + /// Starts an interactive session with the Corebrain CLI + /// /// This allows for multi-stage interactions similar to a REPL (Read-Eval-Print Loop). + /// + /// An instance of that can be used to send commands and receive output. + public InteractiveSession StartInteractiveSession() + { + return new InteractiveSession(_pythonPath, _scriptPath, _verbose); + } } diff --git a/corebrain/wrappers/csharp/CorebrainCS/Services/InteractiveSession.cs b/corebrain/wrappers/csharp/CorebrainCS/Services/InteractiveSession.cs new file mode 100644 index 0000000..4c16290 --- /dev/null +++ b/corebrain/wrappers/csharp/CorebrainCS/Services/InteractiveSession.cs @@ -0,0 +1,61 @@ +using System.Diagnostics; +using System.Text; + +namespace CorebrainCS; + +public class InteractiveSession : IDisposable +{ + private readonly Process _process; + private readonly StreamWriter _stdin; + private readonly StringBuilder _outputBuffer = new(); + private readonly bool _verbose; + + public InteractiveSession(string pythonPath, string scriptPath, bool verbose) + { + _verbose = verbose; + _process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = pythonPath, + Arguments = $"\"{scriptPath}\" --interactive", + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + _process.Start(); + _stdin = _process.StandardInput; + _process.BeginOutputReadLine(); + _process.OutputDataReceived += (_, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + _outputBuffer.AppendLine(e.Data); + if (_verbose) Console.WriteLine($"[INTERACTIVE] {e.Data}"); + } + }; + } + + public void SendCommand(string command) + { + _stdin.WriteLine(command); + _stdin.Flush(); + } + + public string GetOutput() + { + var result = _outputBuffer.ToString(); + _outputBuffer.Clear(); + return result.Trim(); + } + + public void Dispose() + { + _process.Kill(); + _process.Dispose(); + } +} \ No newline at end of file diff --git a/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/CommandController.cs b/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/CommandController.cs index e0236d2..af223f7 100644 --- a/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/CommandController.cs +++ b/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/CommandController.cs @@ -15,6 +15,8 @@ public class CommandController : ControllerBase { public CommandController(IOptions settings) { var config = settings.Value; + var pythonPath = config.PythonPath ?? "python"; + var scriptPath = config.ScriptPath ?? "corebrain"; _corebrain = new CorebrainCS( config.PythonPath, config.ScriptPath, @@ -50,10 +52,10 @@ public IActionResult ExecuteCommand([FromBody] CommandRequest request) { try { var result = _corebrain.ExecuteCommand(request.Arguments); - return Ok(result); + return Ok(new { output = result }); } catch (Exception ex) { - return StatusCode(500, $"Error executing command: {ex.Message}"); + return StatusCode(500, new { error = ex.Message }); } } diff --git a/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/Controllers/InteractiveController.cs b/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/Controllers/InteractiveController.cs new file mode 100644 index 0000000..cf81d30 --- /dev/null +++ b/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/Controllers/InteractiveController.cs @@ -0,0 +1,58 @@ +using CorebrainCS; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace CorebrainCLIAPI.Controllers; + +[ApiController] +[Route("api/interactive")] +public class InteractiveController : ControllerBase +{ + public static readonly ConcurrentDictionary _sessions = new(); + + private readonly CorebrainCS.CorebrainCS _corebrain; + + public InteractiveController(CorebrainCS.CorebrainCS corebrain) + { + _corebrain = corebrain; + } + + [HttpPost("start")] + public IActionResult StartSession() + { + var sessionId = Guid.NewGuid().ToString(); + var session = _corebrain.StartInteractiveSession(); + _sessions.TryAdd(sessionId, session); + return Ok(new { SessionId = sessionId }); + } + + [HttpPost("{sessionId}/command")] + public IActionResult SendCommand( + [FromRoute] string sessionId, + [FromBody] string command) + { + if (!_sessions.TryGetValue(sessionId, out var session)) + return NotFound("Session not found"); + + session.SendCommand(command); + return Ok(session.GetOutput()); + } + + [HttpDelete("{sessionId}")] + public IActionResult EndSession([FromRoute] string sessionId) + { + if (_sessions.TryRemove(sessionId, out var session)) + { + session.Dispose(); + return NoContent(); + } + return NotFound(); + } + + // Helper for middleware + public static bool SessionExists(string sessionId) + { + return _sessions.ContainsKey(sessionId); + } +} \ No newline at end of file diff --git a/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/CorebrainSettings.cs b/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/CorebrainSettings.cs index 6de197e..4794541 100644 --- a/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/CorebrainSettings.cs +++ b/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/CorebrainSettings.cs @@ -5,20 +5,15 @@ namespace CorebrainCLIAPI; /// public class CorebrainSettings { + public CorebrainSettings() { } - /// - /// Gets or sets the path to the Python executable (e.g., "./.venv/Scripts/python"). - /// - public string PythonPath { get; set; } + public string PythonPath { get; set; } + public string ScriptPath { get; set; } + public bool Verbose { get; set; } - /// - /// Gets or sets the path to the Corebrain CLI script or the command name if installed globally (e.g., "corebrain"). - /// - public string ScriptPath { get; set; } - - /// - /// Gets or sets a value indicating whether verbose logging is enabled. - /// Default is false. - /// - public bool Verbose { get; set; } = false; + public CorebrainSettings(string pythonPath, string scriptPath) + { + PythonPath = pythonPath; + ScriptPath = scriptPath; + } } diff --git a/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/Models/InteractiveCommandRequest.cs b/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/Models/InteractiveCommandRequest.cs new file mode 100644 index 0000000..e69de29 diff --git a/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/Program.cs b/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/Program.cs index 3ddd6a1..290d37e 100644 --- a/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/Program.cs +++ b/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/Program.cs @@ -1,7 +1,9 @@ using System.Reflection; using CorebrainCLIAPI; +using CorebrainCLIAPI.Controllers; using Microsoft.OpenApi.Models; + var builder = WebApplication.CreateBuilder(args); // CORS policy to allow requests from the frontend @@ -18,8 +20,10 @@ // Swagger / OpenAPI builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(c => { - c.SwaggerDoc("v1", new OpenApiInfo { +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo + { Title = "Corebrain CLI API", Version = "v1", Description = "ASP.NET Core Web API for interfacing with Corebrain CLI commands" @@ -27,22 +31,46 @@ var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); - if (File.Exists(xmlPath)) { + if (File.Exists(xmlPath)) + { c.IncludeXmlComments(xmlPath); } }); +// Middleware to log requests and responses +builder.Services.AddSingleton(); + var app = builder.Build(); + +app.Use(async (context, next) => +{ + if (context.Request.Path.StartsWithSegments("/api/interactive")) + { + var sessionId = context.Request.RouteValues["sessionId"]?.ToString(); + + if (sessionId != null && !InteractiveController.SessionExists(sessionId)) + { + context.Response.StatusCode = 404; + await context.Response.WriteAsync("Session expired or invalid"); + return; + } + } + await next(); +}); + + // Middleware pipeline app.UseCors("AllowFrontend"); -if (app.Environment.IsDevelopment()) { +if (app.Environment.IsDevelopment()) +{ app.UseSwagger(); app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Corebrain CLI API v1")); } +// Configure the HTTP request pipeline app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); diff --git a/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/Properties/launchSettings.json b/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/Properties/launchSettings.json index cf5accf..1283803 100644 --- a/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/Properties/launchSettings.json +++ b/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/Properties/launchSettings.json @@ -8,7 +8,7 @@ "applicationUrl": "http://localhost:5140", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + } }, "https": { "commandName": "Project", @@ -18,6 +18,13 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "Interactive": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Interactive", + "SESSION_TIMEOUT": "600" + } } } } diff --git a/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/appsettings.json b/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/appsettings.json index 0ab3335..715bd95 100644 --- a/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/appsettings.json +++ b/corebrain/wrappers/csharp_cli_api/src/CorebrainCLIAPI/appsettings.json @@ -9,6 +9,7 @@ "CorebrainSettings": { "PythonPath": "../../../../../venv/Scripts/python.exe", "ScriptPath": "../../../../cli", - "Verbose": false + "Verbose": false, + "SessionTimeout": 300 } } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e70049e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "[Tecnología] SDK", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/pyproject.toml b/pyproject.toml index 03bb8ad..436d24b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ [project.optional-dependencies] postgres = ["psycopg2-binary>=2.9.0"] - = ["pymongo>=4.4.0"] +mongodb = ["pymongo>=4.4.0"] mysql = ["mysql-connector-python>=8.0.23"] all_db = [ "psycopg2-binary>=2.9.0", diff --git a/setup.ps1 b/setup.ps1 index 3d031a4..f63f8c8 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -2,4 +2,4 @@ python -m venv venv .\venv\Scripts\Activate.ps1 -pip install -e ".[dev,all_db]" \ No newline at end of file +pip install -e ".[dev,all_db,sphinx-copybutton]" \ No newline at end of file