diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b152ad1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +notebooks/* linguist-vendored + +linguist-detectable=false + +*.ipynb linguist-detectable=false + +# Enforce Unix newlines +*.py text eol=lf diff --git a/ner.py b/ner.py index 3b0ac49..817deae 100755 --- a/ner.py +++ b/ner.py @@ -24,29 +24,29 @@ def build(config: PipelineConfig): def train(config: PipelineConfig): """Train the NER model.""" - trainer = NameModel(config) + name_model = NameModel(config) data_path = Path(config.paths.data_dir) / config.data.output_files["ner_data"] if not data_path.exists(): logging.info("NER data not found. Building dataset first...") build(config) - trainer.create_blank_model("fr") - data = trainer.load_data(str(data_path)) + name_model.create_blank_model("fr") + data = name_model.load_data(str(data_path)) split_idx = int(len(data) * 0.9) train_data, eval_data = data[:split_idx], data[split_idx:] logging.info(f"Training with {len(train_data)} examples, evaluating on {len(eval_data)}") - trainer.train( + name_model.train( data=train_data, epochs=config.processing.epochs, batch_size=config.processing.batch_size, dropout_rate=0.3, ) - trainer.evaluate(eval_data) + name_model.evaluate(eval_data) - model_path = trainer.save() + model_path = name_model.save() logging.info(f"Model saved to: {model_path}") diff --git a/pages/1_๐Ÿ“Š_Dashboard.py b/pages/1_๐Ÿ“Š_Dashboard.py deleted file mode 100644 index e69de29..0000000 diff --git a/pages/2_๐Ÿ“‹_Data_Overview.py b/pages/2_๐Ÿ“‹_Data_Overview.py deleted file mode 100644 index e69de29..0000000 diff --git a/pages/3_โš™๏ธ_Data_Processing.py b/pages/3_โš™๏ธ_Data_Processing.py deleted file mode 100644 index e69de29..0000000 diff --git a/pages/4_๐Ÿงช_Experiments.py b/pages/4_๐Ÿงช_Experiments.py deleted file mode 100644 index e69de29..0000000 diff --git a/pages/5_๐Ÿ“ˆ_Results_Analysis.py b/pages/5_๐Ÿ“ˆ_Results_Analysis.py deleted file mode 100644 index e69de29..0000000 diff --git a/pages/6_๐Ÿ”ฎ_Predictions.py b/pages/6_๐Ÿ”ฎ_Predictions.py deleted file mode 100644 index e69de29..0000000 diff --git a/pages/7_โš™๏ธ_Configuration.py b/pages/7_โš™๏ธ_Configuration.py deleted file mode 100644 index e69de29..0000000 diff --git a/pages/README.md b/pages/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/processing/annotate.py b/processing/annotate.py deleted file mode 100644 index e69de29..0000000 diff --git a/processing/ner/ner_data_builder.py b/processing/ner/ner_data_builder.py deleted file mode 100644 index e69de29..0000000 diff --git a/processing/ner/ner_name_tagger.py b/processing/ner/ner_name_tagger.py deleted file mode 100644 index e69de29..0000000 diff --git a/processing/pipeline.py b/processing/pipeline.py index 3d3d608..79c5ee0 100644 --- a/processing/pipeline.py +++ b/processing/pipeline.py @@ -1,8 +1,8 @@ import logging +import time +from typing import Dict, Any import pandas as pd -from typing import Dict, Any -import time from processing.batch.batch_config import BatchConfig from processing.batch.batch_processor import BatchProcessor @@ -49,9 +49,6 @@ class Pipeline: "processed_batches": step.state.processed_batches, "total_batches": step.state.total_batches, "failed_batches": len(step.state.failed_batches), - "completion_percentage": ( - step.state.processed_batches / max(1, step.state.total_batches) - ) - * 100, + "completion_percentage": (step.state.processed_batches / max(1, step.state.total_batches)) * 100, } return progress diff --git a/processing/prepare.py b/processing/prepare.py deleted file mode 100644 index e69de29..0000000 diff --git a/processing/steps/feature_extraction_step.py b/processing/steps/feature_extraction_step.py index b64ede6..aa6c9fb 100644 --- a/processing/steps/feature_extraction_step.py +++ b/processing/steps/feature_extraction_step.py @@ -7,7 +7,7 @@ import pandas as pd from core.config.pipeline_config import PipelineConfig from core.utils.region_mapper import RegionMapper -from processing.ner.ner_name_tagger import NERNameTagger +from processing.ner.name_tagger import NameTagger from processing.steps import PipelineStep @@ -27,7 +27,7 @@ class FeatureExtractionStep(PipelineStep): def __init__(self, pipeline_config: PipelineConfig): super().__init__("feature_extraction", pipeline_config) self.region_mapper = RegionMapper() - self.name_tagger = NERNameTagger() + self.name_tagger = NameTagger() @classmethod def requires_batch_mutation(cls) -> bool: diff --git a/processing/steps/ner_annotation_step.py b/processing/steps/ner_annotation_step.py index 410d806..f2c92c3 100644 --- a/processing/steps/ner_annotation_step.py +++ b/processing/steps/ner_annotation_step.py @@ -6,7 +6,7 @@ from typing import Dict import pandas as pd from core.config.pipeline_config import PipelineConfig -from processing.ner.ner_name_model import NERNameModel +from processing.ner.name_model import NameModel from processing.steps import PipelineStep, NameAnnotation @@ -19,7 +19,7 @@ class NERAnnotationStep(PipelineStep): self.model_name = "drc_ner_model" self.model_path = pipeline_config.paths.models_dir / "drc_ner_model" - self.ner_trainer = NERNameModel(pipeline_config) + self.name_model = NameModel(pipeline_config) self.ner_config = pipeline_config.annotation.ner # Statistics @@ -35,19 +35,19 @@ class NERAnnotationStep(PipelineStep): try: if self.model_path.exists(): logging.info(f"Loading NER model from {self.model_path}") - self.ner_trainer.load(str(self.model_path)) + self.name_model.load(str(self.model_path)) logging.info("NER model loaded successfully") else: logging.warning(f"NER model not found at {self.model_path}") logging.warning("NER annotation will be skipped. Train the model first.") - self.ner_trainer.nlp = None + self.name_model.nlp = None except Exception as e: logging.error(f"Failed to load NER model: {e}") - self.ner_trainer.nlp = None + self.name_model.nlp = None def analyze_name(self, name: str) -> Dict: """Analyze a name with retry logic""" - if self.ner_trainer.nlp is None: + if self.name_model.nlp is None: return { "identified_name": None, "identified_surname": None, @@ -62,7 +62,7 @@ class NERAnnotationStep(PipelineStep): start_time = time.time() # Get NER predictions - prediction = self.ner_trainer.predict(name.lower()) + prediction = self.name_model.predict(name.lower()) entities = prediction.get("entities", []) elapsed_time = time.time() - start_time diff --git a/research/base_model.py b/research/base_model.py index 8fff57d..6bbdb93 100644 --- a/research/base_model.py +++ b/research/base_model.py @@ -41,14 +41,14 @@ class BaseModel(ABC): @abstractmethod def cross_validate( - self, X: pd.DataFrame, y: pd.Series, cv_folds: int = 5 + self, X: pd.DataFrame, y: pd.Series, cv_folds: int = 5 ) -> Dict[str, float] | dict[str, np.floating[Any]]: """Perform cross-validation and return average scores""" pass @abstractmethod def generate_learning_curve( - self, X: pd.DataFrame, y: pd.Series, train_sizes: List[float] = None + self, X: pd.DataFrame, y: pd.Series, train_sizes: List[float] = None ) -> Dict[str, Any]: """Generate learning curve data for the model""" pass diff --git a/research/experiment/experiment_runner.py b/research/experiment/experiment_runner.py index 7cc3201..b7fd7b7 100644 --- a/research/experiment/experiment_runner.py +++ b/research/experiment/experiment_runner.py @@ -158,12 +158,12 @@ class ExperimentRunner: @classmethod def _create_prediction_examples( - cls, - X_test: pd.DataFrame, - y_test: pd.Series, - predictions: np.ndarray, - model: BaseModel, - n_examples: int = 10, + cls, + X_test: pd.DataFrame, + y_test: pd.Series, + predictions: np.ndarray, + model: BaseModel, + n_examples: int = 10, ) -> List[Dict]: """Create prediction examples for analysis""" examples = [] @@ -237,7 +237,7 @@ class ExperimentRunner: return None def compare_experiments( - self, experiment_ids: List[str], metric: str = "accuracy" + self, experiment_ids: List[str], metric: str = "accuracy" ) -> pd.DataFrame: """Compare experiments and return analysis""" comparison_df = self.tracker.compare_experiments(experiment_ids) diff --git a/research/experiment/experiment_tracker.py b/research/experiment/experiment_tracker.py index bdb3081..edce523 100644 --- a/research/experiment/experiment_tracker.py +++ b/research/experiment/experiment_tracker.py @@ -7,7 +7,6 @@ from typing import Optional, Dict, List import pandas as pd from core.config import PipelineConfig, get_config - from research.experiment import ExperimentConfig, ExperimentStatus from research.experiment.experiement_result import ExperimentResult @@ -78,10 +77,10 @@ class ExperimentTracker: return self._results.get(experiment_id) def list_experiments( - self, - status: Optional[ExperimentStatus] = None, - tags: Optional[List[str]] = None, - model_type: Optional[str] = None, + self, + status: Optional[ExperimentStatus] = None, + tags: Optional[List[str]] = None, + model_type: Optional[str] = None, ) -> List[ExperimentResult]: """List experiments with optional filtering""" results = list(self._results.values()) @@ -98,7 +97,7 @@ class ExperimentTracker: return sorted(results, key=lambda x: x.start_time, reverse=True) def get_best_experiment( - self, metric: str = "accuracy", dataset: str = "test", filters: Optional[Dict] = None + self, metric: str = "accuracy", dataset: str = "test", filters: Optional[Dict] = None ) -> Optional[ExperimentResult]: """Get the best experiment based on a metric""" experiments = self.list_experiments() @@ -160,8 +159,8 @@ class ExperimentTracker: """Export all results to CSV""" if output_path is None: output_path = ( - self.experiments_dir - / f"experiments_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" + self.experiments_dir + / f"experiments_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" ) rows = [] diff --git a/research/experiment/feature_extractor.py b/research/experiment/feature_extractor.py index 916c69d..be7f5af 100644 --- a/research/experiment/feature_extractor.py +++ b/research/experiment/feature_extractor.py @@ -43,7 +43,7 @@ class FeatureExtractor: return features_df def _extract_single_feature( - self, df: pd.DataFrame, feature_type: FeatureType + self, df: pd.DataFrame, feature_type: FeatureType ) -> Union[pd.Series, pd.DataFrame]: """Extract a single type of feature""" if feature_type == FeatureType.FULL_NAME: diff --git a/research/model_trainer.py b/research/model_trainer.py index 5d908ee..7218955 100644 --- a/research/model_trainer.py +++ b/research/model_trainer.py @@ -27,13 +27,13 @@ class ModelTrainer: self.models_dir.mkdir(parents=True, exist_ok=True) def train_single_model( - self, - model_name: str, - model_type: str = "logistic_regression", - features: List[str] = None, - model_params: Dict[str, Any] = None, - tags: List[str] = None, - save_artifacts: bool = True, + self, + model_name: str, + model_type: str = "logistic_regression", + features: List[str] = None, + model_params: Dict[str, Any] = None, + tags: List[str] = None, + save_artifacts: bool = True, ) -> str: """ Train a single model and save its artifacts. @@ -75,7 +75,7 @@ class ModelTrainer: return experiment_id def train_multiple_models( - self, base_name: str, model_configs: List[Dict[str, Any]], save_all: bool = True + self, base_name: str, model_configs: List[Dict[str, Any]], save_all: bool = True ) -> List[str]: """ Train multiple models with different configurations. diff --git a/research/neural_network_model.py b/research/neural_network_model.py index 927995f..6baf1dc 100644 --- a/research/neural_network_model.py +++ b/research/neural_network_model.py @@ -83,7 +83,7 @@ class NeuralNetworkModel(BaseModel): return self def cross_validate( - self, X: pd.DataFrame, y: pd.Series, cv_folds: int = 5 + self, X: pd.DataFrame, y: pd.Series, cv_folds: int = 5 ) -> dict[str, np.floating[Any]]: features_df = self.feature_extractor.extract_features(X) X_prepared = self.prepare_features(features_df) @@ -140,7 +140,7 @@ class NeuralNetworkModel(BaseModel): } def generate_learning_curve( - self, X: pd.DataFrame, y: pd.Series, train_sizes: List[float] = None + self, X: pd.DataFrame, y: pd.Series, train_sizes: List[float] = None ) -> Dict[str, Any]: """Generate learning curve data for the model""" logging.info(f"Generating learning curve for {self.__class__.__name__}") diff --git a/research/traditional_model.py b/research/traditional_model.py index 099c35a..bd10ec8 100644 --- a/research/traditional_model.py +++ b/research/traditional_model.py @@ -93,7 +93,7 @@ class TraditionalModel(BaseModel): return results def generate_learning_curve( - self, X: pd.DataFrame, y: pd.Series, train_sizes: List[float] = None + self, X: pd.DataFrame, y: pd.Series, train_sizes: List[float] = None ) -> Dict[str, Any]: """Generate learning curve data for the model""" logging.info(f"Generating learning curve for {self.__class__.__name__}") diff --git a/web/app.py b/web/app.py index 9b5b487..9789162 100644 --- a/web/app.py +++ b/web/app.py @@ -2,6 +2,7 @@ import argparse import sys from pathlib import Path + import streamlit as st # Add parent directory to Python path to access core modules @@ -13,13 +14,6 @@ from core.utils.data_loader import DataLoader from processing.monitoring.pipeline_monitor import PipelineMonitor from research.experiment.experiment_runner import ExperimentRunner from research.experiment.experiment_tracker import ExperimentTracker -from web.interfaces.configuration import Configuration -from web.interfaces.dashboard import Dashboard -from web.interfaces.data_overview import DataOverview -from web.interfaces.data_processing import DataProcessing -from web.interfaces.experiments import Experiments -from web.interfaces.predictions import Predictions -from web.interfaces.results_analysis import ResultsAnalysis # Page configuration st.set_page_config( @@ -53,12 +47,10 @@ class StreamlitApp: self.config = config initialize_session_state(config) - def run(self): - st.title("๐Ÿ‡จ๐Ÿ‡ฉ DRC NERS Pipeline") - st.markdown( - "A Culturally-Aware NLP System for Congolese Name Analysis and Gender Inference" - ) - + @classmethod + def run(cls): + st.title("๐Ÿ‡จ๐Ÿ‡ฉ DRC NERS Platform") + st.markdown("A Culturally-Aware NLP System for Congolese Name Analysis and Gender Inference") st.markdown( """ ## Overview @@ -67,7 +59,7 @@ class StreamlitApp: data. This project introduces a comprehensive pipeline for Congolese name analysis with a large-scale dataset of over 5 million names from the Democratic Republic of Congo (DRC) annotated with gender and demographic metadata. - """ + """ ) diff --git a/web/interfaces/experiments.py b/web/interfaces/experiments.py index dd258c2..5278124 100644 --- a/web/interfaces/experiments.py +++ b/web/interfaces/experiments.py @@ -13,7 +13,7 @@ from research.model_registry import list_available_models class Experiments: def __init__( - self, config, experiment_tracker: ExperimentTracker, experiment_runner: ExperimentRunner + self, config, experiment_tracker: ExperimentTracker, experiment_runner: ExperimentRunner ): self.config = config self.experiment_tracker = experiment_tracker @@ -113,18 +113,18 @@ class Experiments: ) def _handle_experiment_submission( - self, - exp_name: str, - description: str, - model_type: str, - selected_features: List[str], - model_params: Dict[str, Any], - test_size: float, - cv_folds: int, - tags: str, - filter_province: str, - min_words: int, - max_words: int, + self, + exp_name: str, + description: str, + model_type: str, + selected_features: List[str], + model_params: Dict[str, Any], + test_size: float, + cv_folds: int, + tags: str, + filter_province: str, + min_words: int, + max_words: int, ): """Handle experiment form submission""" if not exp_name: @@ -209,7 +209,7 @@ class Experiments: # Display experiments for i, exp in enumerate(experiments): with st.expander( - f"{exp.config.name} - {exp.status.value} - {exp.start_time.strftime('%Y-%m-%d %H:%M')}" + f"{exp.config.name} - {exp.status.value} - {exp.start_time.strftime('%Y-%m-%d %H:%M')}" ): self._display_experiment_details(exp, i) @@ -230,7 +230,8 @@ class Experiments: return experiments - def _display_experiment_details(self, exp, index: int): + @classmethod + def _display_experiment_details(cls, exp, index: int): """Display details for a single experiment""" col1, col2, col3 = st.columns(3) @@ -295,13 +296,13 @@ class Experiments: ) def run_batch_experiments( - self, - base_name: str, - model_types: List[str], - ngram_ranges: str, - feature_combinations: List[str], - test_sizes: str, - tags: str, + self, + base_name: str, + model_types: List[str], + ngram_ranges: str, + feature_combinations: List[str], + test_sizes: str, + tags: str, ): """Run batch experiments with parameter combinations""" with st.spinner("Running batch experiments..."): @@ -368,64 +369,3 @@ class Experiments: except Exception as e: st.error(f"Error running batch experiments: {e}") - - def run_baseline_experiments(self): - """Run baseline experiments""" - with st.spinner("Running baseline experiments..."): - try: - builder = ExperimentBuilder() - experiments = builder.create_baseline_experiments() - experiment_ids = self.experiment_runner.run_experiment_batch(experiments) - - st.success(f"Completed {len(experiment_ids)} baseline experiments") - - # Show quick comparison - if experiment_ids: - comparison = self.experiment_runner.compare_experiments(experiment_ids) - st.write("**Results Summary:**") - st.dataframe( - comparison[["name", "model_type", "test_accuracy"]], - use_container_width=True, - ) - - except Exception as e: - st.error(f"Error running baseline experiments: {e}") - - def run_ablation_study(self): - """Run feature ablation study""" - with st.spinner("Running ablation study..."): - try: - builder = ExperimentBuilder() - experiments = builder.create_feature_ablation_study() - experiment_ids = self.experiment_runner.run_experiment_batch(experiments) - - st.success(f"Completed {len(experiment_ids)} ablation experiments") - - except Exception as e: - st.error(f"Error running ablation study: {e}") - - def run_component_study(self): - """Run name component study""" - with st.spinner("Running component study..."): - try: - builder = ExperimentBuilder() - experiments = builder.create_name_component_study() - experiment_ids = self.experiment_runner.run_experiment_batch(experiments) - - st.success(f"Completed {len(experiment_ids)} component experiments") - - except Exception as e: - st.error(f"Error running component study: {e}") - - def run_province_study(self): - """Run province-specific study""" - with st.spinner("Running province study..."): - try: - builder = ExperimentBuilder() - experiments = builder.create_province_specific_study() - experiment_ids = self.experiment_runner.run_experiment_batch(experiments) - - st.success(f"Completed {len(experiment_ids)} province experiments") - - except Exception as e: - st.error(f"Error running province study: {e}") diff --git a/web/interfaces/log_reader.py b/web/interfaces/log_reader.py index 3305316..465d561 100644 --- a/web/interfaces/log_reader.py +++ b/web/interfaces/log_reader.py @@ -38,7 +38,7 @@ class LogReader: # Parse log entries from the end entries = [] - for line in reversed(lines[-count * 2 :]): # Read more lines in case some don't match + for line in reversed(lines[-count * 2:]): # Read more lines in case some don't match entry = self._parse_log_line(line.strip()) if entry: entries.append(entry) diff --git a/web/interfaces/predictions.py b/web/interfaces/predictions.py index b3804d1..9ea9e3b 100644 --- a/web/interfaces/predictions.py +++ b/web/interfaces/predictions.py @@ -13,7 +13,7 @@ from research.experiment.experiment_tracker import ExperimentTracker class Predictions: def __init__( - self, config, experiment_tracker: ExperimentTracker, experiment_runner: ExperimentRunner + self, config, experiment_tracker: ExperimentTracker, experiment_runner: ExperimentRunner ): self.config = config self.experiment_tracker = experiment_tracker @@ -114,7 +114,7 @@ class Predictions: return None def _display_single_prediction_results( - self, prediction: str, confidence: Optional[float], experiment, name_input: str + self, prediction: str, confidence: Optional[float], experiment, name_input: str ): """Display single prediction results""" col1, col2 = st.columns(2) @@ -300,7 +300,7 @@ class Predictions: return pd.DataFrame() def _run_dataset_prediction( - self, df: pd.DataFrame, experiment, sample_size: int, compare_with_actual: bool + self, df: pd.DataFrame, experiment, sample_size: int, compare_with_actual: bool ): """Run dataset prediction and display results""" with st.spinner("Running predictions..."): diff --git a/web/interfaces/results_analysis.py b/web/interfaces/results_analysis.py index aa3d52c..7db6ae7 100644 --- a/web/interfaces/results_analysis.py +++ b/web/interfaces/results_analysis.py @@ -12,7 +12,7 @@ from research.experiment.experiment_tracker import ExperimentTracker class ResultsAnalysis: def __init__( - self, config, experiment_tracker: ExperimentTracker, experiment_runner: ExperimentRunner + self, config, experiment_tracker: ExperimentTracker, experiment_runner: ExperimentRunner ): self.config = config self.experiment_tracker = experiment_tracker diff --git a/web/pages/1_๐Ÿ“Š_Dashboard.py b/web/pages/1_๐Ÿ“Š_Dashboard.py index 3cb186c..f3b3f34 100644 --- a/web/pages/1_๐Ÿ“Š_Dashboard.py +++ b/web/pages/1_๐Ÿ“Š_Dashboard.py @@ -1,5 +1,6 @@ import sys from pathlib import Path + import streamlit as st # Add parent directory to Python path to access core modules diff --git a/web/pages/2_๐Ÿ“‹_Data_Overview.py b/web/pages/2_๐Ÿ“‹_Data_Overview.py index 8a520e1..aae723c 100644 --- a/web/pages/2_๐Ÿ“‹_Data_Overview.py +++ b/web/pages/2_๐Ÿ“‹_Data_Overview.py @@ -1,5 +1,6 @@ import sys from pathlib import Path + import streamlit as st # Add parent directory to Python path to access core modules diff --git a/web/pages/3_โš™๏ธ_Data_Processing.py b/web/pages/3_โš™๏ธ_Data_Processing.py index d028daf..78c71b7 100644 --- a/web/pages/3_โš™๏ธ_Data_Processing.py +++ b/web/pages/3_โš™๏ธ_Data_Processing.py @@ -1,5 +1,6 @@ import sys from pathlib import Path + import streamlit as st # Add parent directory to Python path to access core modules diff --git a/web/pages/4_๐Ÿงช_Experiments.py b/web/pages/4_๐Ÿงช_Experiments.py index 880b5d9..29bb680 100644 --- a/web/pages/4_๐Ÿงช_Experiments.py +++ b/web/pages/4_๐Ÿงช_Experiments.py @@ -1,5 +1,6 @@ import sys from pathlib import Path + import streamlit as st # Add parent directory to Python path to access core modules diff --git a/web/pages/5_๐Ÿ“ˆ_Results_Analysis.py b/web/pages/5_๐Ÿ“ˆ_Results_Analysis.py index 593dc8a..04525ee 100644 --- a/web/pages/5_๐Ÿ“ˆ_Results_Analysis.py +++ b/web/pages/5_๐Ÿ“ˆ_Results_Analysis.py @@ -1,5 +1,6 @@ import sys from pathlib import Path + import streamlit as st # Add parent directory to Python path to access core modules diff --git a/web/pages/6_๐Ÿ”ฎ_Predictions.py b/web/pages/6_๐Ÿ”ฎ_Predictions.py index 1fa3a2a..b414613 100644 --- a/web/pages/6_๐Ÿ”ฎ_Predictions.py +++ b/web/pages/6_๐Ÿ”ฎ_Predictions.py @@ -1,5 +1,6 @@ import sys from pathlib import Path + import streamlit as st # Add parent directory to Python path to access core modules diff --git a/web/pages/7_โš™๏ธ_Configuration.py b/web/pages/7_โš™๏ธ_Configuration.py index abd2f8e..c5e0e47 100644 --- a/web/pages/7_โš™๏ธ_Configuration.py +++ b/web/pages/7_โš™๏ธ_Configuration.py @@ -1,5 +1,6 @@ import sys from pathlib import Path + import streamlit as st # Add parent directory to Python path to access core modules