From a2a8bd781f1a7fe0cbe22dcd3e85ca358d3f764e Mon Sep 17 00:00:00 2001 From: hexastack Date: Mon, 28 Oct 2024 09:25:08 +0100 Subject: [PATCH 1/6] feat: train and log metrics/artifacts using mlflow --- nlu/Dockerfile | 44 +++++++---- nlu/boilerplate.py | 3 +- nlu/data_loaders/jisfdl.py | 48 ++++++------ nlu/data_loaders/tflcdl.py | 4 +- nlu/docker-compose.yml | 60 +++++++++++++++ nlu/docker/Dockerfile | 5 ++ nlu/docker/requirements.txt | 3 + nlu/encoded_texts.pkl | Bin 0 -> 520047 bytes nlu/models/intent_classifier.py | 132 ++++++++++++++++++++++++-------- nlu/requirements.txt | 1 + nlu/run.py | 2 +- nlu/test.py | 15 ++++ 12 files changed, 240 insertions(+), 77 deletions(-) create mode 100644 nlu/docker-compose.yml create mode 100644 nlu/docker/Dockerfile create mode 100644 nlu/docker/requirements.txt create mode 100644 nlu/encoded_texts.pkl create mode 100644 nlu/test.py diff --git a/nlu/Dockerfile b/nlu/Dockerfile index e460d962..9f0ca536 100644 --- a/nlu/Dockerfile +++ b/nlu/Dockerfile @@ -1,21 +1,31 @@ -FROM python:3.11.4 +# FROM python:3.11.4 +# +# # +# WORKDIR /app +# +# # +# COPY ./requirements.txt ./requirements.txt +# +# # Update pip +# RUN pip3 install --upgrade pip +# +# # Install deps +# RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt +# +# # Copy source code +# COPY . . +# +# # Entrypoint +# CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000"] -# -WORKDIR /app +FROM python:3-slim -# -COPY ./requirements.txt ./requirements.txt +WORKDIR /usr/src/app -# Update pip -RUN pip3 install --upgrade pip +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt -# Install deps -RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt - -# Copy source code -COPY . . - -EXPOSE 5000 - -# Entrypoint -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000"] +CMD mlflow server \ + --backend-store-uri ${BACKEND_URI} \ + --default-artifact-root ${ARTIFACT_ROOT} \ + --host 0.0.0.0 \ No newline at end of file diff --git a/nlu/boilerplate.py b/nlu/boilerplate.py index 8e7c35ad..1b6966fb 100644 --- a/nlu/boilerplate.py +++ b/nlu/boilerplate.py @@ -138,7 +138,7 @@ def extra_params(self, value): def save_dir(self): return self._save_dir - def save(self): + def save_model(self): """Save the model's weights.""" if self._ckpt is None: self._ckpt = tf.train.Checkpoint(model=self) @@ -153,6 +153,7 @@ def save(self): self.save_dir, "extra_params.json") with open(extra_params_path, "w") as f: json.dump(self.extra_params, f, indent=4, sort_keys=True) + return self def restore(self): """Restore the model's latest saved weights.""" diff --git a/nlu/data_loaders/jisfdl.py b/nlu/data_loaders/jisfdl.py index ce497918..50e8b01f 100644 --- a/nlu/data_loaders/jisfdl.py +++ b/nlu/data_loaders/jisfdl.py @@ -47,10 +47,10 @@ def encode_intents(self, intents, intent_map) -> tf.Tensor: def get_slot_from_token(self, token: str, slot_dict: Dict[str, str]): """ this function maps a token to its slot label""" # each token either belongs to a slot or has a null slot - for slot_label, value in slot_dict.items(): - if token in value: - return slot_label - return None + # for slot_label, value in slot_dict.items(): + # if token in value: + # return slot_label + # return None def encode_slots(self, tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast], all_slots: List[Dict[str, str]], all_texts: List[str], @@ -60,11 +60,11 @@ def encode_slots(self, tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizer shape=(len(all_texts), max_len), dtype=np.int32) # each slot is assigned to the tokenized sentence instead of the raw text # so that mapping a token to its slots is easier since we can use our bert tokenizer. - for idx, slot_names in enumerate(all_slots): - for slot_name, slot_text in slot_names.items(): - slot_names[slot_name] = tokenizer.tokenize(slot_text) - # we now assign the sentence's slot dictionary to its index in all_slots . - all_slots[idx] = slot_names + # for idx, slot_names in enumerate(all_slots): + # for slot_name, slot_text in slot_names.items(): + # slot_names[slot_name] = tokenizer.tokenize(slot_text) + # # we now assign the sentence's slot dictionary to its index in all_slots . + # all_slots[idx] = slot_names for idx, text in enumerate(all_texts): enc = [] # for this idx, to be added at the end to encoded_slots @@ -100,27 +100,27 @@ def parse_dataset_intents(self, data): # Filter examples by language lang = self.hparams.language - all_examples = data["common_examples"] + # all_examples = data["common_examples"] - if not bool(lang): - examples = all_examples - else: - examples = filter(lambda exp: any(e['entity'] == 'language' and e['value'] == lang for e in exp['entities']), all_examples) + # if not bool(lang): + # examples = all_examples + # else: + # examples = filter(lambda exp: any(e['entity'] == 'language' and e['value'] == lang for e in exp['entities']), all_examples) # Parse raw data - for exp in examples: + for exp in data: text = exp["text"] intent = exp["intent"] - entities = exp["entities"] + # entities = exp["entities"] - # Filter out language entities - slot_entities = filter( - lambda e: e["entity"] != "language", entities) - slots = {e["entity"]: e["value"] for e in slot_entities} - positions = [[e.get("start", -1), e.get("end", -1)] - for e in slot_entities] + # # Filter out language entities + # slot_entities = filter( + # lambda e: e["entity"] != "language", entities) + # slots = {e["entity"]: e["value"] for e in slot_entities} + # positions = [[e.get("start", -1), e.get("end", -1)] + # for e in slot_entities] - temp = JointRawData(k, intent, positions, slots, text) + temp = JointRawData(k, intent, [], [], text) k += 1 intents.append(temp) @@ -133,7 +133,7 @@ def __call__(self, tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast helper = JsonHelper() if self.method in ["fit", "train"]: - dataset = helper.read_dataset_json_file('train.json') + dataset = helper.read_dataset_json_file('english.json') train_data = self.parse_dataset_intents(dataset) return self._transform_dataset(train_data, tokenizer) elif self.method in ["evaluate"]: diff --git a/nlu/data_loaders/tflcdl.py b/nlu/data_loaders/tflcdl.py index bca3e2da..e3157cd7 100644 --- a/nlu/data_loaders/tflcdl.py +++ b/nlu/data_loaders/tflcdl.py @@ -72,8 +72,8 @@ def get_texts_and_languages(self, dataset: List[dict]): def preprocess_train_dataset(self) -> Tuple[np.ndarray, np.ndarray]: """Preprocessing the training set and fitting the proprocess steps in the process""" - json = self.json_helper.read_dataset_json_file("train.json") - dataset = json["common_examples"] + json = self.json_helper.read_dataset_json_file("english.json") + dataset = json # If a sentence has a language label, we include it in our dataset # Otherwise, we discard it. diff --git a/nlu/docker-compose.yml b/nlu/docker-compose.yml new file mode 100644 index 00000000..41b2872f --- /dev/null +++ b/nlu/docker-compose.yml @@ -0,0 +1,60 @@ +#version: '3.9' +#services: +# mlflow_postgres: +# image: bitnami/postgresql +# container_name: postgres_db +# environment: +# - POSTGRES_USER=postgres +# - POSTGRES_PASSWORD=postgres +# - POSTGRES_DB=mlflow_db +# volumes: +# - postgres_data:/var/lib/postgresql/data +# ports: +# - "5432:5432" +# minio: +# image: minio/minio +# ports: +# - "3000:9000" +# - "3001:9001" +# volumes: +# - minio_data:/data +# environment: +# MINIO_ROOT_USER: masoud +# MINIO_ROOT_PASSWORD: Strong#Pass#2022 +# command: server --console-address ":9001" /data +#volumes: +# postgres_data: { } +# minio_data: { } + +version: '3.9' +services: + mlflow_postgres: + image: bitnami/postgresql + container_name: postgres_db + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=mlflow_db + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + + mlflow_server: + restart: always + build: + context: ./docker + dockerfile: Dockerfile # Specify the Dockerfile explicitly + image: mlflow + container_name: mlflow_server + environment: + - BACKEND_STORE_URI=postgresql://postgres:postgres@mlflow_postgres:5432/mlflow_db # Connection string to Postgres + - ARTIFACT_STORE_URI=./mlruns # Local directory for storing artifacts + ports: + - "5002:5000" # Expose MLflow UI + volumes: + - ./mlruns:/mlruns # Mount local directory for MLflow artifacts + command: mlflow server --backend-store-uri postgresql://postgres:postgres@mlflow_postgres:5432/mlflow_db --default-artifact-root ./mlruns --host 0.0.0.0 --port 5000 + +volumes: + postgres_data: {} \ No newline at end of file diff --git a/nlu/docker/Dockerfile b/nlu/docker/Dockerfile new file mode 100644 index 00000000..76ee1fcf --- /dev/null +++ b/nlu/docker/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.11 + +# Install python package +COPY requirements.txt /tmp/ +RUN pip install --no-cache-dir -r /tmp/requirements.txt \ No newline at end of file diff --git a/nlu/docker/requirements.txt b/nlu/docker/requirements.txt new file mode 100644 index 00000000..a45d5c0d --- /dev/null +++ b/nlu/docker/requirements.txt @@ -0,0 +1,3 @@ +mlflow==2.16.2 +psycopg2-binary==2.9.10 +boto3==1.35.47 \ No newline at end of file diff --git a/nlu/encoded_texts.pkl b/nlu/encoded_texts.pkl new file mode 100644 index 0000000000000000000000000000000000000000..99856b2a5d6aca7d1bf9f6ec9b8ab7ccaaeb012e GIT binary patch literal 520047 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 O{PiDSe*fzqfA?P>o Date: Mon, 28 Oct 2024 14:37:03 +0100 Subject: [PATCH 2/6] feat: add mlflow to logs train artifacts/metrics --- cli/dist/index.js | 169 ++++++++++++++++++++++++++++++++ nlu/.gitignore | 4 +- nlu/Dockerfile | 44 ++++----- nlu/data_loaders/jisfdl.py | 69 ++++++------- nlu/data_loaders/tflcdl.py | 4 +- nlu/docker-compose.yml | 28 ------ nlu/encoded_texts.pkl | Bin 520047 -> 0 bytes nlu/models/intent_classifier.py | 21 ---- nlu/test.py | 15 --- 9 files changed, 227 insertions(+), 127 deletions(-) create mode 100755 cli/dist/index.js delete mode 100644 nlu/encoded_texts.pkl delete mode 100644 nlu/test.py diff --git a/cli/dist/index.js b/cli/dist/index.js new file mode 100755 index 00000000..3877070c --- /dev/null +++ b/cli/dist/index.js @@ -0,0 +1,169 @@ +#!/usr/bin/env node +import chalk from 'chalk'; +import { execSync } from 'child_process'; +import { Command } from 'commander'; +import figlet from 'figlet'; +import * as fs from 'fs'; +import * as path from 'path'; +console.log(figlet.textSync('Hexabot')); +// Configuration +const FOLDER = path.resolve(process.cwd(), './docker'); +/** + * Check if the docker folder exists, otherwise prompt the user to cd into the correct folder. + */ +const checkDockerFolder = () => { + if (!fs.existsSync(FOLDER)) { + console.error(chalk.red(`Error: The 'docker' folder is not found in the current directory.`)); + console.error(chalk.yellow(`Please make sure you're in the Hexabot project directory and try again.`)); + console.log(chalk.cyan(`Example: cd path/to/hexabot`)); + process.exit(1); // Exit the script if the folder is not found + } +}; +// Initialize Commander +const program = new Command(); +// Helper Functions +/** + * Generate Docker Compose file arguments based on provided services. + * @param services List of services + * @param type Optional type ('dev' | 'prod') + * @returns String of Docker Compose file arguments + */ +const generateComposeFiles = (services, type) => { + let files = [`-f ${path.join(FOLDER, 'docker-compose.yml')}`]; + services.forEach((service) => { + files.push(`-f ${path.join(FOLDER, `docker-compose.${service}.yml`)}`); + if (type) { + const serviceTypeFile = path.join(FOLDER, `docker-compose.${service}.${type}.yml`); + if (fs.existsSync(serviceTypeFile)) { + files.push(`-f ${serviceTypeFile}`); + } + } + }); + if (type) { + const mainTypeFile = path.join(FOLDER, `docker-compose.${type}.yml`); + if (fs.existsSync(mainTypeFile)) { + files.push(`-f ${mainTypeFile}`); + } + } + return files.join(' '); +}; +/** + * Execute a Docker Compose command. + * @param args Additional arguments for the docker compose command + */ +const dockerCompose = (args) => { + try { + execSync(`docker compose ${args}`, { stdio: 'inherit' }); + } + catch (error) { + console.error(chalk.red('Error executing Docker Compose command.')); + process.exit(1); + } +}; +/** + * Execute a Docker Exec command. + * @param container Container for the docker exec command + * @param options Additional options for the docker exec command + * @param command Command to be executed within the container + */ +const dockerExec = (container, command, options) => { + try { + execSync(`docker exec -it ${options} ${container} ${command}`, { + stdio: 'inherit', + }); + } + catch (error) { + console.error(chalk.red('Error executing Docker Exec command.')); + process.exit(1); + } +}; +/** + * Parse the comma-separated service list. + * @param serviceString Comma-separated list of services + * @returns Array of services + */ +const parseServices = (serviceString) => { + return serviceString + .split(',') + .map((service) => service.trim()) + .filter((s) => s); +}; +// Check if the docker folder exists +checkDockerFolder(); +// Commands +program + .name('Hexabot') + .description('A CLI to manage your Hexabot chatbot instance') + .version('1.0.0'); +program + .command('init') + .description('Initialize the environment by copying .env.example to .env') + .action(() => { + const envPath = path.join(FOLDER, '.env'); + const exampleEnvPath = path.join(FOLDER, '.env.example'); + if (fs.existsSync(envPath)) { + console.log(chalk.yellow('.env file already exists.')); + } + else { + fs.copyFileSync(exampleEnvPath, envPath); + console.log(chalk.green('Copied .env.example to .env')); + } +}); +program + .command('start') + .description('Start specified services with Docker Compose') + .option('--enable ', 'Comma-separated list of services to enable', '') + .action((options) => { + const services = parseServices(options.enable); + const composeArgs = generateComposeFiles(services); + dockerCompose(`${composeArgs} up -d`); +}); +program + .command('dev') + .description('Start specified services in development mode with Docker Compose') + .option('--enable ', 'Comma-separated list of services to enable', '') + .action((options) => { + const services = parseServices(options.enable); + const composeArgs = generateComposeFiles(services, 'dev'); + dockerCompose(`${composeArgs} up --build -d`); +}); +program + .command('migrate [args...]') + .description('Run database migrations') + .action((args) => { + const migrateArgs = args.join(' '); + dockerExec('api', `npm run migrate ${migrateArgs}`, '--user $(id -u):$(id -g)'); +}); +program + .command('start-prod') + .description('Start specified services in production mode with Docker Compose') + .option('--enable ', 'Comma-separated list of services to enable', '') + .action((options) => { + const services = parseServices(options.enable); + const composeArgs = generateComposeFiles(services, 'prod'); + dockerCompose(`${composeArgs} up -d`); +}); +program + .command('stop') + .description('Stop specified Docker Compose services') + .option('--enable ', 'Comma-separated list of services to stop', '') + .action((options) => { + const services = parseServices(options.enable); + const composeArgs = generateComposeFiles(services); + dockerCompose(`${composeArgs} down`); +}); +program + .command('destroy') + .description('Destroy specified Docker Compose services and remove volumes') + .option('--enable ', 'Comma-separated list of services to destroy', '') + .action((options) => { + const services = parseServices(options.enable); + const composeArgs = generateComposeFiles(services); + dockerCompose(`${composeArgs} down -v`); +}); +// Parse arguments +program.parse(process.argv); +// If no command is provided, display help +if (!process.argv.slice(2).length) { + program.outputHelp(); +} diff --git a/nlu/.gitignore b/nlu/.gitignore index 783e0c8f..fde3dfc3 100644 --- a/nlu/.gitignore +++ b/nlu/.gitignore @@ -20,4 +20,6 @@ Icon? # IDEs *.swp -.env \ No newline at end of file +.env +*.pkl +mlruns \ No newline at end of file diff --git a/nlu/Dockerfile b/nlu/Dockerfile index 9f0ca536..e460d962 100644 --- a/nlu/Dockerfile +++ b/nlu/Dockerfile @@ -1,31 +1,21 @@ -# FROM python:3.11.4 -# -# # -# WORKDIR /app -# -# # -# COPY ./requirements.txt ./requirements.txt -# -# # Update pip -# RUN pip3 install --upgrade pip -# -# # Install deps -# RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt -# -# # Copy source code -# COPY . . -# -# # Entrypoint -# CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000"] +FROM python:3.11.4 -FROM python:3-slim +# +WORKDIR /app -WORKDIR /usr/src/app +# +COPY ./requirements.txt ./requirements.txt -COPY requirements.txt ./ -RUN pip install --no-cache-dir -r requirements.txt +# Update pip +RUN pip3 install --upgrade pip -CMD mlflow server \ - --backend-store-uri ${BACKEND_URI} \ - --default-artifact-root ${ARTIFACT_ROOT} \ - --host 0.0.0.0 \ No newline at end of file +# Install deps +RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt + +# Copy source code +COPY . . + +EXPOSE 5000 + +# Entrypoint +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000"] diff --git a/nlu/data_loaders/jisfdl.py b/nlu/data_loaders/jisfdl.py index 50e8b01f..fbe5ba6f 100644 --- a/nlu/data_loaders/jisfdl.py +++ b/nlu/data_loaders/jisfdl.py @@ -4,7 +4,6 @@ import numpy as np from transformers import PreTrainedTokenizerFast, PreTrainedTokenizer - import boilerplate as tfbp from utils.json_helper import JsonHelper @@ -25,6 +24,8 @@ def __init__(self, id, intent, positions, slots, text): def __repr__(self): return str(json.dumps(self.__dict__, indent=2)) # type: ignore + + ## # JISFDL : Joint Intent and Slot Filling Model Data Loader ## @@ -47,24 +48,24 @@ def encode_intents(self, intents, intent_map) -> tf.Tensor: def get_slot_from_token(self, token: str, slot_dict: Dict[str, str]): """ this function maps a token to its slot label""" # each token either belongs to a slot or has a null slot - # for slot_label, value in slot_dict.items(): - # if token in value: - # return slot_label - # return None + for slot_label, value in slot_dict.items(): + if token in value: + return slot_label + return None def encode_slots(self, tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast], all_slots: List[Dict[str, str]], all_texts: List[str], - slot_map: Dict[str, int], max_len: int): + slot_map: Dict[str, int], max_len: int): encoded_slots = np.zeros( shape=(len(all_texts), max_len), dtype=np.int32) # each slot is assigned to the tokenized sentence instead of the raw text # so that mapping a token to its slots is easier since we can use our bert tokenizer. - # for idx, slot_names in enumerate(all_slots): - # for slot_name, slot_text in slot_names.items(): - # slot_names[slot_name] = tokenizer.tokenize(slot_text) - # # we now assign the sentence's slot dictionary to its index in all_slots . - # all_slots[idx] = slot_names + for idx, slot_names in enumerate(all_slots): + for slot_name, slot_text in slot_names.items(): + slot_names[slot_name] = tokenizer.tokenize(slot_text) + # we now assign the sentence's slot dictionary to its index in all_slots . + all_slots[idx] = slot_names for idx, text in enumerate(all_texts): enc = [] # for this idx, to be added at the end to encoded_slots @@ -89,7 +90,7 @@ def encode_slots(self, tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizer # now add to encoded_slots # the first and the last elements # in encoded text are special characters - encoded_slots[idx, 1:len(enc)+1] = enc + encoded_slots[idx, 1:len(enc) + 1] = enc return encoded_slots @@ -100,40 +101,42 @@ def parse_dataset_intents(self, data): # Filter examples by language lang = self.hparams.language - # all_examples = data["common_examples"] + all_examples = data["common_examples"] - # if not bool(lang): - # examples = all_examples - # else: - # examples = filter(lambda exp: any(e['entity'] == 'language' and e['value'] == lang for e in exp['entities']), all_examples) + if not bool(lang): + examples = all_examples + else: + examples = filter( + lambda exp: any(e['entity'] == 'language' and e['value'] == lang for e in exp['entities']), + all_examples) # Parse raw data - for exp in data: + for exp in examples: text = exp["text"] intent = exp["intent"] - # entities = exp["entities"] + entities = exp["entities"] - # # Filter out language entities - # slot_entities = filter( - # lambda e: e["entity"] != "language", entities) - # slots = {e["entity"]: e["value"] for e in slot_entities} - # positions = [[e.get("start", -1), e.get("end", -1)] - # for e in slot_entities] + # Filter out language entities + slot_entities = filter( + lambda e: e["entity"] != "language", entities) + slots = {e["entity"]: e["value"] for e in slot_entities} + positions = [[e.get("start", -1), e.get("end", -1)] + for e in slot_entities] - temp = JointRawData(k, intent, [], [], text) + temp = JointRawData(k, intent, positions, slots, text) k += 1 intents.append(temp) return intents - def __call__(self, tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast], model_params = None): + def __call__(self, tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast], model_params=None): # I have already transformed the train and test datasets to the new format using # the transform to new hidden method. helper = JsonHelper() if self.method in ["fit", "train"]: - dataset = helper.read_dataset_json_file('english.json') + dataset = helper.read_dataset_json_file('train.json') train_data = self.parse_dataset_intents(dataset) return self._transform_dataset(train_data, tokenizer) elif self.method in ["evaluate"]: @@ -143,7 +146,8 @@ def __call__(self, tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast else: raise ValueError("Unknown method!") - def _transform_dataset(self, dataset: List[JointRawData], tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast], model_params = None): + def _transform_dataset(self, dataset: List[JointRawData], + tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast], model_params=None): # We have to encode the texts using the tokenizer to create tensors for training # the classifier. texts = [d.text for d in dataset] @@ -167,7 +171,7 @@ def _transform_dataset(self, dataset: List[JointRawData], tokenizer: Union[PreTr intent_names = model_params["intent_names"] else: intent_names = None - + if "slot_names" in model_params: slot_names = model_params["slot_names"] else: @@ -201,15 +205,14 @@ def _transform_dataset(self, dataset: List[JointRawData], tokenizer: Union[PreTr max_len = len(encoded_texts["input_ids"][0]) # type: ignore all_slots = [td.slots for td in dataset] all_texts = [td.text for td in dataset] - + if slot_map: encoded_slots = self.encode_slots(tokenizer, - all_slots, all_texts, slot_map, max_len) + all_slots, all_texts, slot_map, max_len) else: encoded_slots = None return encoded_texts, encoded_intents, encoded_slots, intent_names, slot_names - def encode_text(self, text: str, tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast]): return self.encode_texts([text], tokenizer) diff --git a/nlu/data_loaders/tflcdl.py b/nlu/data_loaders/tflcdl.py index e3157cd7..bca3e2da 100644 --- a/nlu/data_loaders/tflcdl.py +++ b/nlu/data_loaders/tflcdl.py @@ -72,8 +72,8 @@ def get_texts_and_languages(self, dataset: List[dict]): def preprocess_train_dataset(self) -> Tuple[np.ndarray, np.ndarray]: """Preprocessing the training set and fitting the proprocess steps in the process""" - json = self.json_helper.read_dataset_json_file("english.json") - dataset = json + json = self.json_helper.read_dataset_json_file("train.json") + dataset = json["common_examples"] # If a sentence has a language label, we include it in our dataset # Otherwise, we discard it. diff --git a/nlu/docker-compose.yml b/nlu/docker-compose.yml index 41b2872f..d6470e2c 100644 --- a/nlu/docker-compose.yml +++ b/nlu/docker-compose.yml @@ -1,31 +1,3 @@ -#version: '3.9' -#services: -# mlflow_postgres: -# image: bitnami/postgresql -# container_name: postgres_db -# environment: -# - POSTGRES_USER=postgres -# - POSTGRES_PASSWORD=postgres -# - POSTGRES_DB=mlflow_db -# volumes: -# - postgres_data:/var/lib/postgresql/data -# ports: -# - "5432:5432" -# minio: -# image: minio/minio -# ports: -# - "3000:9000" -# - "3001:9001" -# volumes: -# - minio_data:/data -# environment: -# MINIO_ROOT_USER: masoud -# MINIO_ROOT_PASSWORD: Strong#Pass#2022 -# command: server --console-address ":9001" /data -#volumes: -# postgres_data: { } -# minio_data: { } - version: '3.9' services: mlflow_postgres: diff --git a/nlu/encoded_texts.pkl b/nlu/encoded_texts.pkl deleted file mode 100644 index 99856b2a5d6aca7d1bf9f6ec9b8ab7ccaaeb012e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 520047 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 O{PiDSe*fzqfA?P>o Date: Mon, 28 Oct 2024 14:41:33 +0100 Subject: [PATCH 3/6] fix: remove redundant file --- cli/dist/index.js | 169 ---------------------------------------------- 1 file changed, 169 deletions(-) delete mode 100755 cli/dist/index.js diff --git a/cli/dist/index.js b/cli/dist/index.js deleted file mode 100755 index 3877070c..00000000 --- a/cli/dist/index.js +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env node -import chalk from 'chalk'; -import { execSync } from 'child_process'; -import { Command } from 'commander'; -import figlet from 'figlet'; -import * as fs from 'fs'; -import * as path from 'path'; -console.log(figlet.textSync('Hexabot')); -// Configuration -const FOLDER = path.resolve(process.cwd(), './docker'); -/** - * Check if the docker folder exists, otherwise prompt the user to cd into the correct folder. - */ -const checkDockerFolder = () => { - if (!fs.existsSync(FOLDER)) { - console.error(chalk.red(`Error: The 'docker' folder is not found in the current directory.`)); - console.error(chalk.yellow(`Please make sure you're in the Hexabot project directory and try again.`)); - console.log(chalk.cyan(`Example: cd path/to/hexabot`)); - process.exit(1); // Exit the script if the folder is not found - } -}; -// Initialize Commander -const program = new Command(); -// Helper Functions -/** - * Generate Docker Compose file arguments based on provided services. - * @param services List of services - * @param type Optional type ('dev' | 'prod') - * @returns String of Docker Compose file arguments - */ -const generateComposeFiles = (services, type) => { - let files = [`-f ${path.join(FOLDER, 'docker-compose.yml')}`]; - services.forEach((service) => { - files.push(`-f ${path.join(FOLDER, `docker-compose.${service}.yml`)}`); - if (type) { - const serviceTypeFile = path.join(FOLDER, `docker-compose.${service}.${type}.yml`); - if (fs.existsSync(serviceTypeFile)) { - files.push(`-f ${serviceTypeFile}`); - } - } - }); - if (type) { - const mainTypeFile = path.join(FOLDER, `docker-compose.${type}.yml`); - if (fs.existsSync(mainTypeFile)) { - files.push(`-f ${mainTypeFile}`); - } - } - return files.join(' '); -}; -/** - * Execute a Docker Compose command. - * @param args Additional arguments for the docker compose command - */ -const dockerCompose = (args) => { - try { - execSync(`docker compose ${args}`, { stdio: 'inherit' }); - } - catch (error) { - console.error(chalk.red('Error executing Docker Compose command.')); - process.exit(1); - } -}; -/** - * Execute a Docker Exec command. - * @param container Container for the docker exec command - * @param options Additional options for the docker exec command - * @param command Command to be executed within the container - */ -const dockerExec = (container, command, options) => { - try { - execSync(`docker exec -it ${options} ${container} ${command}`, { - stdio: 'inherit', - }); - } - catch (error) { - console.error(chalk.red('Error executing Docker Exec command.')); - process.exit(1); - } -}; -/** - * Parse the comma-separated service list. - * @param serviceString Comma-separated list of services - * @returns Array of services - */ -const parseServices = (serviceString) => { - return serviceString - .split(',') - .map((service) => service.trim()) - .filter((s) => s); -}; -// Check if the docker folder exists -checkDockerFolder(); -// Commands -program - .name('Hexabot') - .description('A CLI to manage your Hexabot chatbot instance') - .version('1.0.0'); -program - .command('init') - .description('Initialize the environment by copying .env.example to .env') - .action(() => { - const envPath = path.join(FOLDER, '.env'); - const exampleEnvPath = path.join(FOLDER, '.env.example'); - if (fs.existsSync(envPath)) { - console.log(chalk.yellow('.env file already exists.')); - } - else { - fs.copyFileSync(exampleEnvPath, envPath); - console.log(chalk.green('Copied .env.example to .env')); - } -}); -program - .command('start') - .description('Start specified services with Docker Compose') - .option('--enable ', 'Comma-separated list of services to enable', '') - .action((options) => { - const services = parseServices(options.enable); - const composeArgs = generateComposeFiles(services); - dockerCompose(`${composeArgs} up -d`); -}); -program - .command('dev') - .description('Start specified services in development mode with Docker Compose') - .option('--enable ', 'Comma-separated list of services to enable', '') - .action((options) => { - const services = parseServices(options.enable); - const composeArgs = generateComposeFiles(services, 'dev'); - dockerCompose(`${composeArgs} up --build -d`); -}); -program - .command('migrate [args...]') - .description('Run database migrations') - .action((args) => { - const migrateArgs = args.join(' '); - dockerExec('api', `npm run migrate ${migrateArgs}`, '--user $(id -u):$(id -g)'); -}); -program - .command('start-prod') - .description('Start specified services in production mode with Docker Compose') - .option('--enable ', 'Comma-separated list of services to enable', '') - .action((options) => { - const services = parseServices(options.enable); - const composeArgs = generateComposeFiles(services, 'prod'); - dockerCompose(`${composeArgs} up -d`); -}); -program - .command('stop') - .description('Stop specified Docker Compose services') - .option('--enable ', 'Comma-separated list of services to stop', '') - .action((options) => { - const services = parseServices(options.enable); - const composeArgs = generateComposeFiles(services); - dockerCompose(`${composeArgs} down`); -}); -program - .command('destroy') - .description('Destroy specified Docker Compose services and remove volumes') - .option('--enable ', 'Comma-separated list of services to destroy', '') - .action((options) => { - const services = parseServices(options.enable); - const composeArgs = generateComposeFiles(services); - dockerCompose(`${composeArgs} down -v`); -}); -// Parse arguments -program.parse(process.argv); -// If no command is provided, display help -if (!process.argv.slice(2).length) { - program.outputHelp(); -} From b391d8210a2cad39e09a598f25b956de3d75b77c Mon Sep 17 00:00:00 2001 From: hexastack Date: Mon, 28 Oct 2024 16:10:14 +0100 Subject: [PATCH 4/6] fix: remove print statement --- nlu/models/intent_classifier.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nlu/models/intent_classifier.py b/nlu/models/intent_classifier.py index c4f0ae1d..8d3d15a6 100644 --- a/nlu/models/intent_classifier.py +++ b/nlu/models/intent_classifier.py @@ -168,8 +168,6 @@ def fit(self): self.extra_params["intent_names"] = intent_names mlflow.log_params(self.extra_params) model_instance = self.save_model() # Save the model using the internal method - - print(type(model_instance)) # Check if it's the expected Keras model type # Log the model in MLflow mlflow.keras.log_model(model_instance, "intent_classifier_model") # Register the model in MLflow's Model Registry From 676b4115d2740de2859fbee1c83b45c4c037e11e Mon Sep 17 00:00:00 2001 From: hexastack Date: Mon, 28 Oct 2024 18:37:00 +0100 Subject: [PATCH 5/6] feat: add new containers to root docker compose files --- nlu/docker-compose.yml | 5 +++-- nlu/docker/Dockerfile | 3 +++ nlu/models/intent_classifier.py | 3 +-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/nlu/docker-compose.yml b/nlu/docker-compose.yml index d6470e2c..d6413425 100644 --- a/nlu/docker-compose.yml +++ b/nlu/docker-compose.yml @@ -25,8 +25,9 @@ services: ports: - "5002:5000" # Expose MLflow UI volumes: - - ./mlruns:/mlruns # Mount local directory for MLflow artifacts + - mlruns:/mlruns # Mount local directory for MLflow artifacts command: mlflow server --backend-store-uri postgresql://postgres:postgres@mlflow_postgres:5432/mlflow_db --default-artifact-root ./mlruns --host 0.0.0.0 --port 5000 volumes: - postgres_data: {} \ No newline at end of file + postgres_data: {} + mlruns: \ No newline at end of file diff --git a/nlu/docker/Dockerfile b/nlu/docker/Dockerfile index 76ee1fcf..eed57768 100644 --- a/nlu/docker/Dockerfile +++ b/nlu/docker/Dockerfile @@ -2,4 +2,7 @@ FROM python:3.11 # Install python package COPY requirements.txt /tmp/ + +EXPOSE 5000 + RUN pip install --no-cache-dir -r /tmp/requirements.txt \ No newline at end of file diff --git a/nlu/models/intent_classifier.py b/nlu/models/intent_classifier.py index 8d3d15a6..c0c07755 100644 --- a/nlu/models/intent_classifier.py +++ b/nlu/models/intent_classifier.py @@ -36,8 +36,7 @@ 'fr': "dbmdz/bert-base-french-europeana-cased", } -mlflow.set_tracking_uri("http://0.0.0.0:5002") - +mlflow.set_tracking_uri("http://mlflow_server:5002") # 0.0.0.0 for local development @tfbp.default_export class IntentClassifier(tfbp.Model): default_hparams = { From 617622cf57631ed92a58a027b6c17ef49f5e5efa Mon Sep 17 00:00:00 2001 From: hexastack Date: Mon, 28 Oct 2024 18:38:29 +0100 Subject: [PATCH 6/6] feat: add new containers to docker compose files --- docker/.env.example | 7 +++++++ docker/docker-compose.nlu.dev.yml | 19 +++++++++++++++++++ docker/docker-compose.nlu.yml | 27 +++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/docker/.env.example b/docker/.env.example index f9daf1a9..f213a6ae 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -48,6 +48,13 @@ TFLC_REPO_ID=Hexastack/tflc INTENT_CLASSIFIER_REPO_ID=Hexastack/intent-classifier SLOT_FILLER_REPO_ID=Hexastack/slot-filler NLU_ENGINE_PORT=5000 +MLFLOW_SERVER_PORT=5002 +POSTGRES_DB_PORT=5432 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=mlflow_db +BACKEND_STORE_URI=postgresql://postgres:postgres@mlflow_postgres:5432/mlflow_db +ARTIFACT_STORE_URI=./mlruns # Frontend (Next.js) APP_FRONTEND_PORT=8080 diff --git a/docker/docker-compose.nlu.dev.yml b/docker/docker-compose.nlu.dev.yml index f4649846..be1f735f 100644 --- a/docker/docker-compose.nlu.dev.yml +++ b/docker/docker-compose.nlu.dev.yml @@ -8,3 +8,22 @@ services: pull_policy: build ports: - ${NLU_ENGINE_PORT}:5000 + mlflow-server: + build: + context: ../nlu/docker + dockerfile: Dockerfile + pull_policy: build + ports: + - ${MLFLOW_SERVER_PORT}:5000 + + mlflow_postgres: + image: bitnami/postgresql + container_name: postgres_db + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - ${POSTGRES_DB_PORT}:${POSTGRES_DB_PORT} diff --git a/docker/docker-compose.nlu.yml b/docker/docker-compose.nlu.yml index e715c7cb..04c7a69b 100644 --- a/docker/docker-compose.nlu.yml +++ b/docker/docker-compose.nlu.yml @@ -23,8 +23,35 @@ services: retries: 5 start_period: 10s + mlflow_postgres: + image: bitnami/postgresql + container_name: postgres_db + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=mlflow_db + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - ${POSTGRES_DB_PORT}:${POSTGRES_DB_PORT} + + mlflow_server: + restart: always + image: hexastack/hexabot-mlflow-server:latest + container_name: mlflow_server + environment: + - BACKEND_STORE_URI=${BACKEND_STORE_URI} # Connection string to Postgres + - ARTIFACT_STORE_URI=${ARTIFACT_STORE_URI} # Local directory for storing artifacts + ports: + - ${MLFLOW_SERVER_PORT}:5000 # Expose MLflow UI + volumes: + - mlruns:/mlruns # Mount local directory for MLflow artifacts + command: mlflow server --backend-store-uri postgresql://postgres:postgres@mlflow_postgres:5432/mlflow_db --default-artifact-root ./mlruns --host 0.0.0.0 --port 5000 + volumes: nlu-data: + postgres_data: + mlruns: networks: nlu-network: