diff --git a/.gitignore b/.gitignore index fcb1181..66f79f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ # Personnal usage test.ipynb +test.py + +# Hydra +outputs/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/pyproject.toml b/pyproject.toml index b1bd031..2e6eef6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.13" dependencies = [ "duckdb>=1.4.4", "fastparquet>=2025.12.0", + "hydra-core>=1.3.2", "ipykernel>=7.1.0", "ipywidgets>=8.1.8", "jupyter-cache>=1.0.1", diff --git a/src/agents/NaiveCode2Text/code_retrieval/code_sampler.py b/src/agents/NaiveCode2Text/code_retrieval/code_sampler.py index 8eef679..7c50a2e 100644 --- a/src/agents/NaiveCode2Text/code_retrieval/code_sampler.py +++ b/src/agents/NaiveCode2Text/code_retrieval/code_sampler.py @@ -1,41 +1,13 @@ import polars as pl import s3fs -import numpy as np - - -def sample_codes( - fs: s3fs.S3FileSystem, - population_path: str, - code_column: str, - n_codes: int - ) -> np.ndarray: - """ - Sample codes with replacement using dataframes from Polars. - - Args: - fs (S3FileSystem): The filesystem for importation. - population_path (str): The path of the parquet file of the population. - code_column (str): The name of the column for codes. - n_codes (int): The number of codes to sample. - - Returns: - numpy.ndarray: An array of n_codes codes sampled with replacement. - """ - - with fs.open(population_path, 'rb') as f: - df = pl.read_parquet(f) - - sampled = df.select(code_column).sample(n=n_codes, with_replacement=True) - - return sampled[code_column].to_numpy() def sample_codes_lazy( fs: s3fs.S3FileSystem, population_path: str, code_column: str, - n_codes: str - ) -> np.ndarray: + n_codes: int + ) -> list: """ Sample codes with replacement using lazyframes from Polars. @@ -46,7 +18,7 @@ def sample_codes_lazy( n_codes (int): The number of codes to sample. Returns: - numpy.ndarray: An array of n_codes codes sampled with replacement. + list: A list of n_codes codes sampled with replacement. """ with fs.open(population_path, 'rb') as f: @@ -68,4 +40,4 @@ def sample_codes_lazy( df = sampled.collect() - return df[code_column].to_numpy() + return df[code_column].to_list() diff --git a/src/agents/NaiveCode2Text/code_retrieval/code_specifier.py b/src/agents/NaiveCode2Text/code_retrieval/code_specifier.py index 894edf4..f33fddf 100644 --- a/src/agents/NaiveCode2Text/code_retrieval/code_specifier.py +++ b/src/agents/NaiveCode2Text/code_retrieval/code_specifier.py @@ -1,8 +1,49 @@ -from src.neo4j_graph.graph import Graph +import logging + +from langchain_neo4j import Neo4jGraph + +logger = logging.getLogger(__name__) + + +def get_all_codes( + graph: Neo4jGraph, + n_samples_per_code: int + ) -> list: + """ + Sample codes with replacement using lazyframes from Polars. + + Args: + fs (S3FileSystem): The filesystem for importation. + population_path (str): The path of the parquet file of the population. + code_column (str): The name of the column for codes. + n_codes (int): The number of codes to sample. + + Returns: + numpy.ndarray: An array of n_codes codes sampled with replacement. + """ + + query = """ + MATCH (n) + WHERE n.FINAL = 1 + RETURN DISTINCT n.CODE; + """ + + response = graph.query(query) + + if not response: + logger.warn("No response in get_all_codes") + return [] + + result = [] + + for r in response: + result += [r["n.CODE"]] * n_samples_per_code + + return result def get_code_information( - graph: Graph, + graph: Neo4jGraph, code: str ) -> dict: """ @@ -22,36 +63,81 @@ def get_code_information( OPTIONAL MATCH (node)-[:HAS_CHILD]->(child) WITH node, parent, collect({code: child.CODE, name: child.NAME}) as children RETURN node.CODE as code, - node.LEVEL as level, - node.NAME as name, - node.text as description, - node.Includes as includes, - node.IncludesAlso as includes_also, - node.Excludes as excludes, - node.Implementation_rule as implementation_rule, - parent.CODE as parent_code, - children, - size(children) as children_count - """ - result = graph.graph.query(query, params={"code": code}) + node.LEVEL as level, + node.NAME as name, + node.text as description, + node.Includes as includes, + node.IncludesAlso as includes_also, + node.Excludes as excludes, + node.Implementation_rule as implementation_rule, + parent.CODE as parent_code, + children, + size(children) as children_count + """ + result = graph.query(query, params={"code": code}) if not result: - print("No result in get_code_information") - return () + logger.warn("No result in get_code_information") + return [] return result[0] -def NAF_to_NACE( - code: str - ) -> str: +def get_code_list_information( + graph: Neo4jGraph, + code_list: list[str] + ) -> dict: """ - For the case of NAF code format (DDDDL), transform it into NACE (DD.DD). + Retrieve code specifications from a Neo4j graph for multiple codes. Args: - code (str): The code in NAF format to transform. + graph (Neo4jGraph): The Neo4j graph. + code_list (list[str]): List of codes to retrieve. Returns: - str: The code in NACE format. + dict: Dictionary of code information keyed by code. """ - return code[0:2] + '.' + code[2:4] + + if not code_list: + return {} + + query = """ + MATCH (node) + WHERE node.CODE IN $code_list + OPTIONAL MATCH (node)<-[:HAS_CHILD]-(parent) + OPTIONAL MATCH (node)-[:HAS_CHILD]->(child) + WITH node, parent, collect({code: child.CODE, name: child.NAME}) AS children + RETURN node.CODE AS code, + node.LEVEL AS level, + node.NAME AS name, + node.text AS description, + node.Includes AS includes, + node.IncludesAlso AS includes_also, + node.Excludes AS excludes, + node.Implementation_rule AS implementation_rule, + parent.CODE AS parent_code, + children, + size(children) AS children_count + """ + + result = graph.query(query, params={"code_list": code_list}) + + if not result: + logger.warn("No result in get_code_list_information") + return {} + + # Transformer la liste de résultats en dictionnaire de dictionnaires + code_dict = {} + for record in result: + code_key = record["code"] + code_dict[code_key] = { + "code": code_key, + "name": record["name"], + "description": record["description"], + "includes": record["includes"], + "includes_also": record["includes_also"], + "excludes": record["excludes"], + "implementation_rule": record["implementation_rule"] + } + + return code_dict diff --git a/src/agents/NaiveCode2Text/config_naive.py b/src/agents/NaiveCode2Text/config_naive.py deleted file mode 100644 index a5648fb..0000000 --- a/src/agents/NaiveCode2Text/config_naive.py +++ /dev/null @@ -1,34 +0,0 @@ -# For code sampling -POPULATION_PATH = "projet-ape/data/08112022_27102024/naf2025/split/df_train.parquet" -CODE_COLUMN = "nace2025" - -# For prompt creation -PROMPT_PATH = "src/agents/NaiveCode2Text/prompts/" - -# To retrieve specifications of every code correctly: -INCLUDES_DIVIDER = "\n-" -EXAMPLES_DIVIDER = "\n" -EXCLUDE_DIVIDER = "\n" - -# Randomization for specifications -RANDOM_SPEC_SAMPLING = True -RANDOM_INCLUDES_GEOM_PROB = 0.3 -RANDOM_INCLUDES_MIN = 1 -RANDOM_INCLUDES_MAX = None # None = up to the max number of includes -RANDOM_EXAMPLES_GEOM_PROB = 0.2 -RANDOM_EXAMPLES_MIN = 1 -RANDOM_EXAMPLES_MAX = None # None = up to the max number of examples per include - -# Exportation -OUTPUT_PATH = "projet-ape/synthetic_data_test/naive/" -OUTPUT_FORMAT = ".parquet" # .txt or .parquet -BATCH_SIZE = 5 # If choosing .parquet output format - -# LLM Hyperparameters -MODEL = "gpt-oss:20b" -TEMPERATURE = 1.8 -LANGUAGE = "English" - -# Generation specifications -N_CODES = 12 # Number of codes to sample -NB_LABELS = 10 # Number of labels to generate per code diff --git a/src/agents/NaiveCode2Text/data_preprocessing/NAF_preprocessing.py b/src/agents/NaiveCode2Text/data_preprocessing/NAF_preprocessing.py new file mode 100644 index 0000000..cb179aa --- /dev/null +++ b/src/agents/NaiveCode2Text/data_preprocessing/NAF_preprocessing.py @@ -0,0 +1,49 @@ +""" +Useful if the code of the input data for sampling is different from the one +of the notice (DD.DDL). +""" + + +def NAF_to_NACE( + code: str + ) -> str: + """ + For the case of NAF code format (DDDDL), transform it into NACE (DD.DD). + + Args: + code (str): The code in NAF format to transform. + + Returns: + str: The code in NACE format. + """ + return code[0:2] + '.' + code[2:4] + + +def to_proper_NAF( + code: str + ) -> str: + """ + For the case of bad NAF code format (DDDDL), transform it into proper NAF (DD.DDL). + + Args: + code (str): The code in bad NAF format to transform. + + Returns: + str: The code in proper NAF format. + """ + return code[0:2] + '.' + code[2:] + + +def to_bad_NAF( + code: str + ) -> str: + """ + For the case of proper NAF code format (DD.DDL), transform it into bad NAF (DDDDL). + + Args: + code (str): The code in proper NAF format to transform. + + Returns: + str: The code in bad NAF format. + """ + return code[0:2] + code[3:] diff --git a/src/agents/NaiveCode2Text/fewshot/fewshot_prompt_builder.py b/src/agents/NaiveCode2Text/fewshot/fewshot_prompt_builder.py new file mode 100644 index 0000000..5da41dc --- /dev/null +++ b/src/agents/NaiveCode2Text/fewshot/fewshot_prompt_builder.py @@ -0,0 +1,69 @@ +import logging + +logger = logging.getLogger(__name__) + + +def build_fewshot_system_prompt( + prompt_path: str, + language: str = "English", + nb_labels: int = 10, + ) -> str: + """ + Import system prompt for fewshots with the correct language and specify the number of labels. + + Args: + prompt_path (str): The path for importation. + language (str): English or French. + nb_labels (int): The number of labels to generate. + + Returns: + str: The system prompt. + """ + if language == "French": + suffix = "_fr" + elif language == "English": + suffix = "_en" + else: + logger.warn("Supported languages are English and French. Switching to English...") + suffix = "_en" + + # Importing prompt + file_path = prompt_path + "system_prompt_fewshot" + suffix + ".txt" + with open(file=file_path, mode='r') as f: + system_prompt = f.read() + + # Indicating the correct number of labels + system_prompt = system_prompt.replace("{nb_labels}", str(nb_labels)) + return system_prompt + + +def add_fewshot_user_prompt( + fewshot: list, + language: str = "English" + ) -> str: + """ + Add few-shot to user prompt with the correct language. + + Args: + fewshot (list): The list of exampls to give for few-shot. + language (str): English or French. + + Returns: + str: The text to add to user prompt. + """ + + if len(fewshot) == 0: + return "" + + if language == "English": + prompt = "\nHere are examples of labels for the code:" + elif language == "French": + prompt = "\nVoici des exemples de libellés pour le code:" + else: + logger.warn("Supported languages are English and French. Switching to English...") + prompt = "\nHere are examples of labels for the code:" + + for example in fewshot: + prompt += "\n- " + example + + return prompt diff --git a/src/agents/NaiveCode2Text/fewshot/fewshot_sampler.py b/src/agents/NaiveCode2Text/fewshot/fewshot_sampler.py new file mode 100644 index 0000000..400b3a5 --- /dev/null +++ b/src/agents/NaiveCode2Text/fewshot/fewshot_sampler.py @@ -0,0 +1,112 @@ +import polars as pl +import s3fs +import numpy as np + + +def sample_fewshot_lazy( + fs: s3fs.S3FileSystem, + population_path: str, + code_column: str, + code: str, + label_column: str, + n_fewshot: int + ) -> np.ndarray: + """ + Sample examples of labels of a certain code using lazyframes from Polars. + + Args: + fs (S3FileSystem): The filesystem for importation. + population_path (str): The path of the parquet file of the population. + code_column (str): The name of the column for codes. + code (str): The code to sample examples from. + label_column (str): The name of the column for labels. + n_fewshot (int): The number of examples to sample. + + Returns: + numpy.ndarray: An array of n_fewshot labels of code sampled without replacement. + """ + + with fs.open(population_path, "rb") as f: + lf = ( + pl.scan_parquet(f) + .filter(pl.col(code_column) == code) + .select(label_column) + .with_row_index("row_id") + ) + + n_available = lf.select(pl.len()).collect().item() + + n_fewshot = min(n_available, n_fewshot) + + random_ids = ( + pl.Series("row_id", range(n_available)) + .sample(n=n_fewshot, with_replacement=False) + .to_frame() + .lazy() + ) + + sampled = lf.join(random_ids, on="row_id", how="inner") + + df = sampled.collect() + + return df[label_column].to_numpy() + + +def sample_fewshot_lazy_multi( + fs: s3fs.S3FileSystem, + population_path: str, + code_column: str, + codes: list[str], + label_column: str, + n_fewshot: int, + ) -> list[np.ndarray]: + """ + IID sampling per occurrence of codes. + Same code appearing multiple times will generate independent samples. + Robust when n_available < n_fewshot. + Single parquet scan. + """ + + unique_codes = list(set(codes)) + + with fs.open(population_path, "rb") as f: + lf = ( + pl.scan_parquet(f) + .filter(pl.col(code_column).is_in(unique_codes)) + .select([code_column, label_column]) + ) + + grouped = ( + lf.group_by(code_column) + .agg(pl.col(label_column).alias("labels")) + .collect() + ) + + # Mapping code -> numpy array of all available labels + label_pool = { + row[code_column]: np.array(row["labels"]) + for row in grouped.iter_rows(named=True) + } + + # iid sampling for each occurrence + results = [] + + for code in codes: + labels = label_pool.get(code) + + if labels is None or len(labels) == 0: + results.append(np.array([])) + continue + + n_available = len(labels) + n_sample = min(n_available, n_fewshot) + + sampled = np.random.choice( + labels, + size=n_sample, + replace=False, + ) + + results.append(sampled) + + return results diff --git a/src/agents/NaiveCode2Text/label_generation/label_exportation.py b/src/agents/NaiveCode2Text/label_generation/label_exportation.py new file mode 100644 index 0000000..352a841 --- /dev/null +++ b/src/agents/NaiveCode2Text/label_generation/label_exportation.py @@ -0,0 +1,230 @@ +import logging +import re +from datetime import datetime + +import pandas as pd +import s3fs + +logger = logging.getLogger(__name__) + + +def create_file_name( + output_path: str, + output_format: str, + temperature: float, + language: str, + exhaustive_sampling: bool = False, + use_fewshot: bool = False, + n_fewshot: int = 0, + model_name: str = None, + model: str = None, + ) -> str: + """ + Create the full output_path with an automatic name representing the config inputs. + + Args: + output_path (str): the folder to upload the file in + output_format (str): the format (.txt, .parquet or terminal) + temperature: float, + language: str, + exhaustive_sampling: bool = False, + use_fewshot: bool = False, + n_fewshot: int = 0, + model_name: str = None, + model: str = None, + + Returns: + str: The generated output file path, or an empty string if + ``output_format`` is ``"terminal"``. + """ + if output_format == "terminal": + return "" + + date = datetime.today().strftime('%Y-%m-%d') + + temp_string = f"_temp{temperature}".replace(".", "") + + selected_model = model_name or model or "" + if selected_model: + model_string = "_" + selected_model + else: + model_string = "" + + model_string = re.sub(r"[^a-zA-Z0-9_-]", "-", model_string) + + if use_fewshot: + if n_fewshot > 0: + fewshot_string = f"_fewshot{n_fewshot}" + else: + fewshot_string = "_fewshot" + else: + fewshot_string = "" + + if exhaustive_sampling: + exhaust_string = "_exhaustive" + else: + exhaust_string = "" + + file_name = date + model_string + temp_string + fewshot_string + exhaust_string + + assert output_format in [".txt", ".parquet"], "Output format should be either .txt or .parquet" + + final_path = output_path + file_name + output_format + + return final_path + + +def export_to_txt( + codes: list, + names: list, + labels: list, + file_path: str, + generation_time: float + ) -> bool: + """ + Save results to file .txt + The number of codes used for generation should not exceed 100. + Else, upload in .parquet format + + Args: + codes (list): List of codes generated. + names (list): List of names for the codes generated + labels (list of lists): List of labels for each code + file_path (str): The .txt file path. + generation_time (float): The time it took to generate. + + Returns: + bool: True if the file has been correctly saved. + """ + if len(labels) == 0 or len(labels) > 100: + return False + + nb_labels = len(labels) * len(labels[0]) + + with open(file_path, "w", encoding="utf-8") as f: + f.write(f"{nb_labels} wordings have been generated in {generation_time:.2f} sec.\n\n") + f.write("=" * 36 + "\n") + + for code, name, generated_labels in zip(codes, names, labels): + f.write(f"Code: {code}\n") + f.write(f"Name: {name}\n") + f.write("Result:\n") + + for j, label in enumerate(generated_labels): + f.write(f"{j}. {label}\n") + + f.write("\n" + "=" * 36 + "\n") + + return True + + +def export_to_parquet( + codes: list, + names: list, + labels: list, + file_path: str, + fs: s3fs.S3FileSystem + ) -> bool: + """ + Save results to file .txt + The number of codes used for generation should not exceed 100. + Else, upload in .parquet format + + Args: + codes (list): List of codes used for the generation. + names (list): List of names for the codes used for the generation. + labels (list of lists): List of generated labels for each code. + file_path (str): The .parquet file path. + fs (float): The filesystem for exportation. + + Returns: + bool: True if the file has been correctly saved. + """ + + # Basic validation + assert (len(codes) == len(names) == len(labels)), \ + "Codes, names, and labels must have the same length." + + assert len(codes) != 0, "Empty input: nothing to export." + + # Flatten structure + rows = [] + + for code, name, generated_labels in zip(codes, names, labels): + + assert isinstance(generated_labels, list), "Labels must be stored in a list." + + for label in generated_labels: + rows.append({ + "code": code, + "name": name, + "label": label + }) + + # Save to parquet + df = pd.DataFrame(rows) + + if fs is None: + try: + existing_df = pd.read_parquet(file_path) + df = pd.concat([existing_df, df], ignore_index=True) + except FileNotFoundError: + logger.info(f"No file found at location {file_path}, creating...") + else: + try: + with fs.open(file_path, 'rb') as f: + existing_df = pd.read_parquet(f) + df = pd.concat([existing_df, df], ignore_index=True) + except FileNotFoundError: + logger.info(f"No file found at location {file_path}, creating...") + + df.to_parquet( + file_path, + engine="pyarrow", + index=False, + filesystem=fs + ) + + return True + + +def print_in_terminal( + codes: list, + names: list, + labels: list, + generation_time: float + ) -> bool: + """ + Save results to file .txt + The number of codes used for generation should not exceed 100. + Else, upload in .parquet format + + Args: + codes (list): List of codes generated. + names (list): List of names for the codes generated + labels (list of lists): List of labels for each code + file_path (str): The .txt file path. + generation_time (float): The time it took to generate. + + Returns: + bool: True if the file has been correctly saved. + """ + if len(labels) == 0 or len(labels) > 100: + return False + + nb_labels = len(labels) * len(labels[0]) + + logger.info(f"{nb_labels} wordings have been generated in {generation_time:.2f} sec.\n\n") + logger.info("=" * 36 + "\n") + + for code, name, generated_labels in zip(codes, names, labels): + logger.info(f"Code: {code}\n") + logger.info(f"Name: {name}\n") + logger.info("Result:\n") + + for j, label in enumerate(generated_labels): + logger.info(f"{j}. {label}\n") + + logger.info("\n" + "=" * 36 + "\n") + + return True diff --git a/src/agents/NaiveCode2Text/label_generation/label_generator.py b/src/agents/NaiveCode2Text/label_generation/label_generator.py new file mode 100644 index 0000000..5413611 --- /dev/null +++ b/src/agents/NaiveCode2Text/label_generation/label_generator.py @@ -0,0 +1,142 @@ +import logging +import asyncio + +from openai import OpenAI, AsyncOpenAI +from pydantic import BaseModel, Field, create_model +from typing import List, Type + +logger = logging.getLogger(__name__) + + +def build_label_generation_model(nb_labels: int) -> Type[BaseModel]: + """ + Dynamically build a Pydantic model enforcing exactly n_labels. + + Args: + nb_labels (int): The number of labels. + + Returns: + Type[BaseModel]: The expected format for output. + """ + + return create_model( + "LabelGeneration", + labels=( + List[str], + Field( + ..., + min_items=nb_labels, + max_items=nb_labels, + description=f"Exactly {nb_labels} generated labels" + ) + ) + ) + + +def ask_model( + system_prompt: str, + user_prompt: str, + llm_client: OpenAI, + model: str, + temperature: float, + LabelGeneration: Type[BaseModel] + ) -> str: + """ + Dialogue with the model. + The model and the temperature are to be configured in the config.py file. + + Args: + system_prompt (str): The system prompt (orders). + user_prompts (str): The user prompt (adapted to a specific case). + llm_client (OpenAI): The client to connect to (initialized with your logins) + model (str): The model to talk to. + temperatur (float): Temperature for the answer, between 0 and 2. + LabelGeneration (Type[BaseModel]): The expected format for output. + + Returns: + str: The answer returned by the model. + """ + response = llm_client.chat.completions.parse( + model=model, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt} + ], + temperature=temperature, + response_format=LabelGeneration + ) + + if response.choices: + return response.choices[0].message.parsed + + else: + logger.warn("The LLM did not return an answer.") + return None + + +async def ask_model_async( + system_prompt: str, + user_prompt: str, + llm_client: AsyncOpenAI, + model: str, + temperature: float, + LabelGeneration: Type[BaseModel] + ) -> str: + """ + Dialogue with the model. + The model and the temperature are to be configured in the config.py file. + + Args: + system_prompt (str): The system prompt (orders). + user_prompts (str): The user prompt (adapted to a specific case). + llm_client (OpenAI): The client to connect to (initialized with your logins) + model (str): The model to talk to. + temperatur (float): Temperature for the answer, between 0 and 2. + LabelGeneration (Type[BaseModel]): The expected format for output. + + Returns: + str: The answer returned by the model. + """ + response = await llm_client.chat.completions.parse( + model=model, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt} + ], + temperature=temperature, + response_format=LabelGeneration + ) + + if response.choices: + return response.choices[0].message.parsed + + else: + logger.warn("The LLM did not return an answer.") + return None + + +async def ask_model_multiple( + system_prompt: str, + user_prompts: list, + llm_client: AsyncOpenAI, + model: str, + temperature: float, + LabelGeneration: Type[BaseModel], + max_concurrency: int = 15 + ) -> list: + + semaphore = asyncio.Semaphore(max_concurrency) + + async def limited_call(prompt): + async with semaphore: + return await ask_model_async( + system_prompt=system_prompt, + user_prompt=prompt, + llm_client=llm_client, + model=model, + temperature=temperature, + LabelGeneration=LabelGeneration + ) + + tasks = [limited_call(p) for p in user_prompts] + return await asyncio.gather(*tasks) diff --git a/src/agents/NaiveCode2Text/naive_code2text.py b/src/agents/NaiveCode2Text/naive_code2text.py index dfb413a..3010119 100644 --- a/src/agents/NaiveCode2Text/naive_code2text.py +++ b/src/agents/NaiveCode2Text/naive_code2text.py @@ -1,159 +1,375 @@ import os import logging import time +import traceback +import asyncio from dotenv import load_dotenv import s3fs -from openai import OpenAI - -from src.agents.NaiveCode2Text.config_naive import \ - MODEL, TEMPERATURE, OUTPUT_PATH, N_CODES, POPULATION_PATH, CODE_COLUMN, \ - OUTPUT_FORMAT, BATCH_SIZE, LANGUAGE, NB_LABELS, PROMPT_PATH, \ - INCLUDES_DIVIDER, EXAMPLES_DIVIDER, EXCLUDE_DIVIDER, RANDOM_SPEC_SAMPLING, \ - RANDOM_INCLUDES_GEOM_PROB, RANDOM_INCLUDES_MIN, RANDOM_INCLUDES_MAX, \ - RANDOM_EXAMPLES_GEOM_PROB, RANDOM_EXAMPLES_MIN, RANDOM_EXAMPLES_MAX -from src.agents.NaiveCode2Text.prompts import prompt_builder, label_generator +from openai import AsyncOpenAI +from langchain_neo4j import Neo4jGraph +import hydra +from omegaconf import DictConfig + from src.agents.NaiveCode2Text.code_retrieval import code_sampler, code_specifier -from src.neo4j_graph.graph import Graph, Neo4JConfig +from src.agents.NaiveCode2Text.data_preprocessing import NAF_preprocessing +from src.agents.NaiveCode2Text.fewshot import fewshot_prompt_builder, fewshot_sampler +from src.agents.NaiveCode2Text.label_generation import label_generator, label_exportation +from src.agents.NaiveCode2Text.prompt_creation import system_prompt_builder, \ + exhaustive_user_prompt_builder, random_user_prompt_builder -# Logger -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) -# Environment -load_dotenv(override=True) +@hydra.main( + version_base=None, + config_path="runtime_config", + config_name="config" + ) +def main(cfg: DictConfig): + + # ======================== INIT ========================== + # Logger + root_logger = logging.getLogger() + root_logger.setLevel(logging.INFO) + + root_logger.handlers.clear() + handler = logging.StreamHandler() + + formatter = logging.Formatter( + "%(asctime)s | %(levelname)s | %(message)s" + ) + + handler.setFormatter(formatter) + root_logger.addHandler(handler) + + # Environment + load_dotenv(override=True) + + root_logger.info("Initialization...") -if __name__ == "__main__": # Clock for speed testing - if OUTPUT_FORMAT == ".txt": + if cfg["export"]["output_format"] in [".txt", "terminal"]: start = time.perf_counter() # Access configurations FS = s3fs.S3FileSystem( - client_kwargs={'endpoint_url': os.environ["AWS_ENDPOINT_URL"]}, + client_kwargs={'endpoint_url': "https://" + os.environ["AWS_S3_ENDPOINT"]}, key=os.environ["AWS_ACCESS_KEY_ID"], secret=os.environ["AWS_SECRET_ACCESS_KEY"], token=os.environ["AWS_SESSION_TOKEN"] ) - LLM_API_KEY = os.environ["LLM_API_KEY"] - LLM_URL = os.environ["LLM_URL"] - LLM_CLIENT = OpenAI(api_key=LLM_API_KEY, base_url=LLM_URL) - - # Sampling from original data - logger.info("Sampling from data...") - code_list = code_sampler.sample_codes_lazy( - fs=FS, - population_path=POPULATION_PATH, - code_column=CODE_COLUMN, - n_codes=N_CODES - ) - - # NAF to NACE - logger.info("Transforming codes from NAF to NACE...") - code_list = [code_specifier.NAF_to_NACE(code) for code in code_list] - # Neo4j connection - logger.info("Connecting to Neo4j graph...") - notice_graph = Graph(Neo4JConfig( + NOTICE_GRAPH = Neo4jGraph( url=os.environ["NEO4J_URL"], username=os.environ["NEO4J_USERNAME"], password=os.environ["NEO4J_PWD"] - )) + ) - # Define an automatic name for output - file_name = f"generation_{MODEL}_temp{TEMPERATURE}".replace(":", "-").replace(".", "") \ - + OUTPUT_FORMAT - FINAL_PATH = OUTPUT_PATH + file_name + # Connecting to Generation Client + URL_GENERATION_API = os.environ["URL_GENERATION_API"] + LLM_CLIENT = AsyncOpenAI(base_url=URL_GENERATION_API) - # Prompt generation - logger.info("Generating prompts...") + # Model set up + LabelGenerationModel = label_generator.build_label_generation_model( + nb_labels=cfg["main"]["n_labels_per_gen"] + ) - name_list = [] - label_list = [] + # Automatic name for output + FINAL_PATH = label_exportation.create_file_name( + output_path=cfg["export"]["output_path"], + output_format=cfg["export"]["output_format"], + temperature=cfg["llm"]["temperature"], + language=cfg["main"]["language"], + exhaustive_sampling=cfg["main"]["exhaustive_sampling"], + use_fewshot=cfg["main"]["use_fewshot"], + n_fewshot=cfg["fewshot"]["n_fewshot"], + model_name=cfg["export"]["model_name"], + model=cfg["llm"]["model"], + ) - # Model set up - LabelGenerationModel = label_generator.build_label_generation_model(NB_LABELS) + # ======================== SAMPLING CODES ========================== + root_logger.info("Sampling from data...") + + code_list = [] - system_prompt = prompt_builder.build_system_prompt( - prompt_path=PROMPT_PATH, - language=LANGUAGE, - nb_labels=NB_LABELS + if cfg["main"]["exhaustive_sampling"]: + code_list += code_specifier.get_all_codes( + graph=NOTICE_GRAPH, + n_samples_per_code=cfg["exhaustivity"]["min_prompts_per_code"] + ) + + code_list = [NAF_preprocessing.to_bad_NAF(code) for code in code_list] + + N_EXHAUSTIVE = len(code_list) + + if cfg["main"]["n_random_codes"] > 0: + code_list += code_sampler.sample_codes_lazy( + fs=FS, + population_path=cfg["sampling"]["population_path"], + code_column=cfg["sampling"]["code_column"], + n_codes=cfg["main"]["n_random_codes"] + ) + + # NAF to NACE + if cfg["naf"]["convert_naf_to_nace"]: + root_logger.info("Transforming codes from NAF to NACE...") + new_code_list = [NAF_preprocessing.NAF_to_NACE(code) for code in code_list] + + elif cfg["naf"]["convert_to_proper_naf"]: + root_logger.info("Transforming codes to proper NAF...") + new_code_list = [NAF_preprocessing.to_proper_NAF(code) for code in code_list] + + else: + new_code_list = code_list + + # ======================== FEW-SHOT ========================== + if cfg["main"]["use_fewshot"]: + root_logger.info("Sampling examples for few-shot...") + codes_fewshot = fewshot_sampler.sample_fewshot_lazy_multi( + fs=FS, + population_path=cfg["sampling"]["population_path"], + code_column=cfg["sampling"]["code_column"], + codes=code_list, + label_column=cfg["fewshot"]["label_column"], + n_fewshot=cfg["fewshot"]["n_fewshot"] ) - for i, code in enumerate(code_list): - logger.info(f"Processing step {i+1}...") + # ======================== CODE DETAILS ========================== + root_logger.info("Getting details of every code...") + unique_codes = list(set(new_code_list)) - # Get code details from Neo4j - code_details = code_specifier.get_code_information( - graph=notice_graph, - code=code - ) + code_details = code_specifier.get_code_list_information( + graph=NOTICE_GRAPH, + code_list=unique_codes + ) + + # ======================== PROMPT CREATION ========================== + root_logger.info("Creating prompts...") - # For exportation purpose - name_list.append(code_details["name"]) - - # Build prompts - user_prompt = prompt_builder.build_user_prompt( - code_details=code_details, - language=LANGUAGE, - nb_labels=NB_LABELS, - includes_divider=INCLUDES_DIVIDER, - examples_divider=EXAMPLES_DIVIDER, - excludes_divider=EXCLUDE_DIVIDER, - random_spec_sampling=RANDOM_SPEC_SAMPLING, - random_includes_geom_prob=RANDOM_INCLUDES_GEOM_PROB, - random_includes_min=RANDOM_INCLUDES_MIN, - random_includes_max=RANDOM_INCLUDES_MAX, - random_examples_geom_prob=RANDOM_EXAMPLES_GEOM_PROB, - random_examples_min=RANDOM_EXAMPLES_MIN, - random_examples_max=RANDOM_EXAMPLES_MAX + # System prompt + if cfg["main"]["use_fewshot"]: + system_prompt = fewshot_prompt_builder.build_fewshot_system_prompt( + prompt_path=cfg["prompt"]["prompt_path"], + language=cfg["main"]["language"], + nb_labels=cfg["main"]["n_labels_per_gen"] + ) + else: + system_prompt = system_prompt_builder.build_system_prompt( + prompt_path=cfg["prompt"]["prompt_path"], + language=cfg["main"]["language"], + nb_labels=cfg["main"]["n_labels_per_gen"] ) - # Ask the chatbot - generation = label_generator.ask_model( - system_prompt=system_prompt, - user_prompt=user_prompt, - llm_client=LLM_CLIENT, - model=MODEL, - temperature=TEMPERATURE, - LabelGeneration=LabelGenerationModel + valid_items = [] + + # User prompt + for i, new_code in enumerate(new_code_list): + fewshot = codes_fewshot[i] if cfg["main"]["use_fewshot"] else None + code_spec = code_details[new_code] + + try: + + # First for the exhaustivity part + if cfg["main"]["exhaustive_sampling"] and i < N_EXHAUSTIVE: + user_prompts = exhaustive_user_prompt_builder.build_user_prompts( + code_details=code_spec, + language=cfg["main"]["language"], + nb_labels=cfg["main"]["n_labels_per_gen"], + includes_divider=cfg["spec"]["includes_divider"], + examples_divider=cfg["spec"]["examples_divider"], + excludes_divider=cfg["spec"]["excludes_divider"], + n_spec=cfg["exhaustivity"]["n_spec_per_prompt"] + ) + + for user_prompt in user_prompts: + if cfg["main"]["use_fewshot"]: + user_prompt += fewshot_prompt_builder.add_fewshot_user_prompt( + fewshot=fewshot, + language=cfg["main"]["language"] + ) + + valid_items.append({ + "code": new_code, + "name": code_spec["name"], + "prompt": user_prompt + }) + + # For the random part + elif cfg["main"]["n_random_codes"] > 0: + user_prompt = random_user_prompt_builder.build_user_prompt( + code_details=code_spec, + language=cfg["main"]["language"], + nb_labels=cfg["main"]["n_labels_per_gen"], + includes_divider=cfg["spec"]["includes_divider"], + examples_divider=cfg["spec"]["examples_divider"], + excludes_divider=cfg["spec"]["excludes_divider"], + random_includes_geom_prob=cfg["random"]["includes"]["geom_prob"], + random_includes_min=cfg["random"]["includes"]["min"], + random_includes_max=cfg["random"]["includes"]["max"], + random_examples_geom_prob=cfg["random"]["examples"]["geom_prob"], + random_examples_min=cfg["random"]["examples"]["min"], + random_examples_max=cfg["random"]["examples"]["max"] + ) + + if cfg["main"]["use_fewshot"]: + user_prompt += fewshot_prompt_builder.add_fewshot_user_prompt( + fewshot=fewshot, + language=cfg["main"]["language"] + ) + + valid_items.append({ + "code": new_code, + "name": code_spec["name"], + "prompt": user_prompt + }) + + except Exception as e: + root_logger.warning(f"Error preparing code {new_code}, skipping...\nDetails: {e}") + root_logger.info(traceback.format_exc()) + continue + + # ======================== LABEL GENERATION ========================== + root_logger.info(f"""Generating {len(valid_items)*cfg["main"]["n_labels_per_gen"]} labels...""") + + results_buffer = [] + n_batches = len(valid_items) // cfg["llm"]["generation_batch_size"] + if len(valid_items) % cfg["llm"]["generation_batch_size"] > 0: + n_batches += 1 + + for i in range(0, len(valid_items), cfg["llm"]["generation_batch_size"]): + root_logger.info( + f"""Processing batch {(i // cfg["llm"]["generation_batch_size"]) + 1}/{n_batches}...""" ) - label_list.append(generation.labels) + batch = valid_items[i:i + cfg["llm"]["generation_batch_size"]] + prompts = [item["prompt"] for item in batch] - if OUTPUT_FORMAT == ".parquet" and (i+1) % BATCH_SIZE == 0: - logger.info("Saving intermediate results...") - label_generator.export_to_parquet( - codes=code_list[i+1-BATCH_SIZE:i+1], - names=name_list, - labels=label_list, - file_path=FINAL_PATH, - fs=FS + try: + generations = asyncio.run( + label_generator.ask_model_multiple( + system_prompt=system_prompt, + user_prompts=prompts, + llm_client=LLM_CLIENT, + model=cfg["llm"]["model"], + temperature=cfg["llm"]["temperature"], + LabelGeneration=LabelGenerationModel, + max_concurrency=len(prompts) + ) + ) + + for item, generation in zip(batch, generations): + results_buffer.append({ + "code": item["code"], + "name": item["name"], + "labels": generation.labels + }) + + except Exception as e: + root_logger.warning(f"Batch generation failed, retrying... Details: {e}") + root_logger.info(traceback.format_exc()) + try: + generations = asyncio.run( + label_generator.ask_model_multiple( + system_prompt=system_prompt, + user_prompts=prompts, + llm_client=LLM_CLIENT, + model=cfg["llm"]["model"], + temperature=cfg["llm"]["temperature"], + LabelGeneration=LabelGenerationModel, + max_concurrency=len(prompts) + ) + ) + + for item, generation in zip(batch, generations): + results_buffer.append({ + "code": item["code"], + "name": item["name"], + "labels": generation.labels + }) + + root_logger.info("The new trial was succesful.") + + except Exception as ex: + root_logger.warning(f"Batch generation failed, skipping... Details: {ex}") + root_logger.info(traceback.format_exc()) + + # ======================== INTERMEDIATE SAVE ========================== + if (cfg["export"]["output_format"] == ".parquet") and \ + (len(results_buffer) >= cfg["export"]["save_batch_size"]): + root_logger.info("Saving intermediate results...") + + try: + codes = [r["code"] for r in results_buffer] + names = [r["name"] for r in results_buffer] + labels = [r["labels"] for r in results_buffer] + + label_exportation.export_to_parquet( + codes=codes, + names=names, + labels=labels, + file_path=FINAL_PATH, + fs=FS ) - label_list = [] - name_list = [] + + except Exception as e: + root_logger.warning(f"Buffer exportation failed, dropped... Details: {e}") + root_logger.info(traceback.format_exc()) + + results_buffer = [] end = time.perf_counter() - if OUTPUT_FORMAT == ".txt": - logger.info("Saving results to txt...") - label_generator.export_to_txt( - codes=code_list, - names=name_list, - labels=label_list, + # ======================== FINAL SAVE ========================== + + if cfg["export"]["output_format"] == ".txt": + root_logger.info("Saving results to txt...") + + codes = [r["code"] for r in results_buffer] + names = [r["name"] for r in results_buffer] + labels = [r["labels"] for r in results_buffer] + + label_exportation.export_to_txt( + codes=codes, + names=names, + labels=labels, file_path=FINAL_PATH, generation_time=end-start ) - elif OUTPUT_FORMAT == ".parquet": - logger.info("Saving final results...") - first_unsaved_index = BATCH_SIZE*(N_CODES//BATCH_SIZE) - if first_unsaved_index < N_CODES: - label_generator.export_to_parquet( - codes=code_list[BATCH_SIZE*(N_CODES//BATCH_SIZE):], - names=name_list, - labels=label_list, + elif cfg["export"]["output_format"] == "terminal": + root_logger.info("Printing the results...") + + codes = [r["code"] for r in results_buffer] + names = [r["name"] for r in results_buffer] + labels = [r["labels"] for r in results_buffer] + + label_exportation.print_in_terminal( + codes=codes, + names=names, + labels=labels, + generation_time=end-start + ) + + elif cfg["export"]["output_format"] == ".parquet" and results_buffer: + root_logger.info("Saving final remaining results...") + + try: + codes = [r["code"] for r in results_buffer] + names = [r["name"] for r in results_buffer] + labels = [r["labels"] for r in results_buffer] + + label_exportation.export_to_parquet( + codes=codes, + names=names, + labels=labels, file_path=FINAL_PATH, fs=FS - ) + ) + + except Exception as e: + root_logger.warning(f"Buffer exportation failed, dropped... Details: {e}") + root_logger.info(traceback.format_exc()) + + +if __name__ == "__main__": + main() diff --git a/src/agents/NaiveCode2Text/prompt_creation/exhaustive_user_prompt_builder.py b/src/agents/NaiveCode2Text/prompt_creation/exhaustive_user_prompt_builder.py new file mode 100644 index 0000000..b8fd0c5 --- /dev/null +++ b/src/agents/NaiveCode2Text/prompt_creation/exhaustive_user_prompt_builder.py @@ -0,0 +1,241 @@ +""" +Use to retrieve all specifications from code information. +""" + +import logging + +logger = logging.getLogger(__name__) + + +def split_spec_and_group( + all_spec: list, + examples_divider: str, + n_spec: int + ) -> list: + """ + Select all specifications from a list and group them by chunks. + Handle the case where specifications contain examples. + One example counts as one spec. + If there are more examples than n_spec, split the examples and repeat the spec + in the next prompt. + If n_spec is equal to 1, examples are not split. + + Args: + all_spec (list): The list of the specifications to sample from. + examples_divider (str): Divider for examples inside each specification. + n_spec (int): The max number of specifications per group + + Returns: + list of list: Every group of spec with examples taken from the original list + """ + # First check to unintended selection + if len(all_spec) == 0: + return all_spec + + assert n_spec >= 1, f"n_spec ({n_spec}) should be greater or equal than 1" + if n_spec == 1: + return all_spec + + final_spec = [] + current_spec = [] + + # Then, select examples to keep within each spec that remains + for spec in all_spec: + spec = spec.split(examples_divider) + main_spec = spec[0] + current_spec.append(main_spec) + + # If the spec contains examples + if len(spec) >= 2: + + # Spec should not be without its examples + if len(current_spec) == n_spec: + final_spec.append(current_spec[-1:]) + current_spec = [main_spec] + + examples = spec[1:] + + # Add examples until n_spec is reached + for example in examples: + current_spec.append(example) + if len(current_spec) == n_spec: + final_spec.append(current_spec) + current_spec = [main_spec] + + if len(current_spec) == n_spec: + final_spec.append(current_spec) + current_spec = [] + + if len(current_spec) >= 1: + final_spec.append(current_spec) + + return final_spec + + +def build_single_user_prompt( + includes: list, + excludes: list, + code_details: dict, + language: str = "English", + nb_labels: int = 10 + ) -> str: + """ + Build one user prompt from given code details and specifications. + + Args: + includes (list): list of includes to specify + excludes (list): list of excludes to specify + code_details (dict): A dictionnary that includes details about the code. + Necessary keys: + - code: the code itself + - name: the title of the code + These keys should be the same as in the Neo4j database. + language (str): English or French + nb_labels (int): The number of labels to generate. + + Returns: + str: User prompt for the code + """ + # For English + if language == "English": + user_prompt = "I would like to generate labels corresponding to the following code:" + + user_prompt += f"""\n\nCode : {code_details["code"]}""" + + if code_details["name"]: + user_prompt += f"""\n\nTitle : {code_details["name"]}""" + + if len(includes) >= 1: + user_prompt += "\n\nIncluded domains:" + for include in includes: + user_prompt += include + + if len(excludes) >= 1: + user_prompt += "\n\nExcluded domains:" + for exclude in excludes: + user_prompt += "\n" + exclude + + user_prompt += f"\n\nInstruction:\nGenerate {nb_labels} different, diverse, and realistic " \ + + "labels that strictly correspond to this code, fully complying with the official" \ + + " description." + + # For French + elif language == "French": + user_prompt = "Je souhaite générer des libellés correspondant au code suivant :" + + user_prompt += f"""\n\nCode : {code_details["code"]}""" + + if code_details["name"]: + user_prompt += f"""\n\nTitre : {code_details["name"]}""" + + if len(includes) >= 1: + user_prompt += "\n\n Domaines inclus :" + for include in includes: + user_prompt += include + + if len(excludes) >= 1: + user_prompt += "\n\n Domaines exclus :" + for exclude in excludes: + user_prompt += "\n" + exclude + + user_prompt += f"\n\n Consigne :\nGénère {nb_labels} libellés différents, variés et" \ + + " réalistes correspondant strictement à ce code, en respectant la notice." + + return user_prompt + + +def build_user_prompts( + code_details: dict, + language: str = "English", + nb_labels: int = 10, + includes_divider: str = "\n-", + examples_divider: str = "\n", + excludes_divider: str = "\n", + n_spec: int = 5 + ) -> list: + """ + Build all user prompts for the code, each with mach n_spec specifications. + + Args: + code_details (dict): A dictionnary that includes details about the code. + Necessary keys: + - code: the code itself + - name: the title of the code + - includes: what the code includes + - includes_also: what the code includes also + - excludes: what the code excludes + These keys should be the same as in the Neo4j database. + language (str): English or French + nb_labels (int): The number of labels to generate. + includes_divider (str): Divider for includes inside includes and includes_also. + examples_divider (str): Divider for examples inside each include. + n_spec (int): Number of specifications desired per prompt. + + Returns: + list[str]: User prompts for the same code + """ + includes_list = [] + + # Extracting useful information + if code_details["includes"]: + all_includes = code_details["includes"].split(includes_divider) + + if len(all_includes) >= 2: # Introduction sentence + all_includes = all_includes[1:] + + includes_list += all_includes + + # Case with includes_also: extend the Includes + if code_details["includes_also"]: + all_includes_also = code_details["includes_also"].split(includes_divider) + + if len(all_includes_also) >= 2: # Introduction sentence + all_includes_also = all_includes_also[1:] + + includes_list += all_includes_also + + # Group + if len(includes_list) >= 1: + group_includes = split_spec_and_group( + all_spec=includes_list, + examples_divider=examples_divider, + n_spec=n_spec + ) + else: + group_includes = [] + + # Add all excludes + if code_details["excludes"]: + all_excludes = code_details["excludes"].split(excludes_divider) + if len(all_excludes) >= 2: + all_excludes = all_excludes[1:] + else: + all_excludes = [] + + user_prompts = [] + + # If no include, still creates a prompt + if len(group_includes) == 0: + user_prompt = build_single_user_prompt( + includes=[], + excludes=all_excludes, + code_details=code_details, + language=language, + nb_labels=nb_labels + ) + + user_prompts.append(user_prompt) + + else: + for includes in group_includes: + user_prompt = build_single_user_prompt( + includes=includes, + excludes=all_excludes, + code_details=code_details, + language=language, + nb_labels=nb_labels + ) + + user_prompts.append(user_prompt) + + return user_prompts diff --git a/src/agents/NaiveCode2Text/prompts/prompt_builder.py b/src/agents/NaiveCode2Text/prompt_creation/random_user_prompt_builder.py similarity index 83% rename from src/agents/NaiveCode2Text/prompts/prompt_builder.py rename to src/agents/NaiveCode2Text/prompt_creation/random_user_prompt_builder.py index b37835b..1497256 100644 --- a/src/agents/NaiveCode2Text/prompts/prompt_builder.py +++ b/src/agents/NaiveCode2Text/prompt_creation/random_user_prompt_builder.py @@ -1,3 +1,7 @@ +""" +Use to pick specifications randomly from code information. +""" + import logging import numpy.random as npr @@ -72,15 +76,22 @@ def split_spec_and_select( Returns: list: Random spec with examples sampled from the original list """ + # First check to unintended selection + if len(all_spec) == 0: + return all_spec + final_spec = [] # First, select spec to keep - random_spec = select_random_items( - all_items=all_spec, - min_items=spec_min, - max_items=spec_max, - geom_prob=spec_geom_prob - ) + if len(all_spec) >= 2: + random_spec = select_random_items( + all_items=all_spec, + min_items=spec_min, + max_items=spec_max, + geom_prob=spec_geom_prob + ) + else: + random_spec = all_spec # Then, select examples to keep within each spec that remains for spec in random_spec: @@ -109,41 +120,6 @@ def split_spec_and_select( return final_spec -def build_system_prompt( - prompt_path: str, - language: str = "English", - nb_labels: int = 10, - ) -> str: - """ - Import system prompt with the correct language and specify the number of labels. - - Args: - prompt_path (str): The path for importation. - language (str): English or French. - nb_labels (int): The number of labels to generate. - - Returns: - str: The system prompt. - """ - - # Selecting language - if language == "French": - suffix = "_fr" - elif language == "English": - suffix = "_en" - else: - logger.warn("Supported languages are English and French. Switching to English...") - - # Importing prompt - file_path = prompt_path + "system_prompt" + suffix + ".txt" - with open(file=file_path, mode='r') as f: - system_prompt = f.read() - - # Indicating the correct number of labels - system_prompt = system_prompt.replace("{nb_labels}", str(nb_labels)) - return system_prompt - - def build_user_prompt( code_details: dict, language: str = "English", @@ -151,7 +127,6 @@ def build_user_prompt( includes_divider: str = "\n-", examples_divider: str = "\n", excludes_divider: str = "\n", - random_spec_sampling: bool = False, random_includes_geom_prob: float = 0.7, random_includes_min: int = 1, random_includes_max: int = None, @@ -192,13 +167,21 @@ def build_user_prompt( """ # Extracting useful information if code_details["includes"]: - all_includes = code_details["includes"].split(includes_divider)[1:] + all_includes = code_details["includes"].split(includes_divider) + + if len(all_includes) >= 2: # Introduction sentence + all_includes = all_includes[1:] # Case with includes_also: extend the Includes if code_details["includes_also"]: - all_includes += code_details["includes_also"].split(includes_divider)[1:] + all_includes_also = code_details["includes_also"].split(includes_divider) + + if len(all_includes_also) >= 2: # Introduction sentence + all_includes_also = all_includes_also[1:] + + all_includes += all_includes_also - if random_spec_sampling: + if len(all_includes) >= 2: # Select includes randomly random_includes = split_spec_and_select( all_spec=all_includes, @@ -218,7 +201,9 @@ def build_user_prompt( # Add all excludes if code_details["excludes"]: - all_excludes = code_details["excludes"].split(excludes_divider)[1:] + all_excludes = code_details["excludes"].split(excludes_divider) + if len(all_excludes) >= 2: + all_excludes = all_excludes[1:] else: all_excludes = [] @@ -241,9 +226,9 @@ def build_user_prompt( for exclude in all_excludes: user_prompt += "\n" + exclude - user_prompt += f"\n\nInstruction:\nGenerate {nb_labels} different, diverse, and realistic \ - labels that strictly correspond to this code, fully complying with the official \ - description." + user_prompt += f"\n\nInstruction:\nGenerate {nb_labels} different, diverse, and realistic" \ + + "labels that strictly correspond to this code, fully complying with the official" \ + + " description." # For French elif language == "French": @@ -264,7 +249,7 @@ def build_user_prompt( for exclude in all_excludes: user_prompt += "\n" + exclude - user_prompt += f"\n\n Consigne :\nGénère {nb_labels} libellés différents, variés et \ - réalistes correspondant strictement à ce code, en respectant la notice." + user_prompt += f"\n\n Consigne :\nGénère {nb_labels} libellés différents, variés et" \ + + " réalistes correspondant strictement à ce code, en respectant la notice." return user_prompt diff --git a/src/agents/NaiveCode2Text/prompt_creation/system_prompt_builder.py b/src/agents/NaiveCode2Text/prompt_creation/system_prompt_builder.py new file mode 100644 index 0000000..56afe67 --- /dev/null +++ b/src/agents/NaiveCode2Text/prompt_creation/system_prompt_builder.py @@ -0,0 +1,39 @@ +import logging + +logger = logging.getLogger(__name__) + + +def build_system_prompt( + prompt_path: str, + language: str = "English", + nb_labels: int = 10, + ) -> str: + """ + Import system prompt with the correct language and specify the number of labels. + + Args: + prompt_path (str): The path for importation. + language (str): English or French. + nb_labels (int): The number of labels to generate. + + Returns: + str: The system prompt. + """ + + # Selecting language + if language == "French": + suffix = "_fr" + elif language == "English": + suffix = "_en" + else: + logger.warn("Supported languages are English and French. Switching to English...") + suffix = "_en" + + # Importing prompt + file_path = prompt_path + "system_prompt" + suffix + ".txt" + with open(file=file_path, mode='r') as f: + system_prompt = f.read() + + # Indicating the correct number of labels + system_prompt = system_prompt.replace("{nb_labels}", str(nb_labels)) + return system_prompt diff --git a/src/agents/NaiveCode2Text/prompts/system_prompt_en.txt b/src/agents/NaiveCode2Text/prompt_creation/system_prompt_en.txt similarity index 87% rename from src/agents/NaiveCode2Text/prompts/system_prompt_en.txt rename to src/agents/NaiveCode2Text/prompt_creation/system_prompt_en.txt index 8d95a42..6874d5b 100644 --- a/src/agents/NaiveCode2Text/prompts/system_prompt_en.txt +++ b/src/agents/NaiveCode2Text/prompt_creation/system_prompt_en.txt @@ -15,6 +15,7 @@ Mandatory constraints: - lexically diverse (no trivial rewordings) - structurally varied (short phrases, long descriptions, nominal forms, technical wording, administrative phrasing, business-style wording, etc.) - realistic in a professional context + - of size less than 20 words. 4. Never mention the code in the labels. @@ -27,4 +28,8 @@ Mandatory constraints: 8. Include different levels of specificity (general to highly specific cases). Important: -If a potential label risks falling into an excluded domain, you must discard it. \ No newline at end of file +If a potential label risks falling into an excluded domain, you must discard it. +Use plain English. +Do not use special characters. +No multilingual content. +Only ASCII characters. \ No newline at end of file diff --git a/src/agents/NaiveCode2Text/prompt_creation/system_prompt_fewshot_en.txt b/src/agents/NaiveCode2Text/prompt_creation/system_prompt_fewshot_en.txt new file mode 100644 index 0000000..8cbbfe2 --- /dev/null +++ b/src/agents/NaiveCode2Text/prompt_creation/system_prompt_fewshot_en.txt @@ -0,0 +1,80 @@ +You are an expert in classification systems and synthetic data generation for training coding models. + +Your task is to generate realistic labels that STRICTLY correspond to a given code, based on its official description. + +Mandatory constraints: + +1. You must produce exactly {nb_labels} labels. + +2. Each label must: + - align with the title and included domains + - strictly comply with the criteria defined in the description + - never fall under any excluded domains + +3. The labels must be: + - lexically diverse (no trivial rewordings) + - structurally varied (short phrases, long descriptions, nominal forms, technical wording, administrative phrasing, business-style wording, etc.) + - realistic in a professional context + - of size less than 20 words. + +4. Never mention the code in the labels. + +5. Do not explain your reasoning. + +6. Output ONLY the list of the {nb_labels} labels. + +7. Avoid semantic duplicates. + +8. Include different levels of specificity (general to highly specific cases). + + +You will receive one or more example labels. + +Implicitly identify the stylistic dominating pattern of provided examples and STRICTLY apply it to new labels. + + +Mandatory imitation rules: + +1. Copy the level of language (technical, administrative, informal, vague, precise, etc.). + +2. Reproduce the degree of verbosity (short, very short, long, highly detailed). + +3. Respect the dominant syntactic structure (nominal phrase, infinitive form, professional phrasing, telegraphic style, etc.). + +4. Reproduce the tone (impersonal, descriptive, imperative, declarative, etc.). + +5. Preserve the format: + + - use or absence of capitalization + - punctuation + - absence or presence of articles + - telegraphic style if applicable + +6. If the examples contain spelling mistakes, approximations, awkward phrasing, or minor inconsistencies, reproduce them realistically. + +7. If the examples are homogeneous, maintain that homogeneity. + +8. If the examples are heterogeneous, reproduce that heterogeneity. + +Absolute priority for examples: +Reproducing the style of the examples takes precedence over previously requested stylistic variation. + +Do not correct the examples. +Do not normalize. +Do not improve the style. +Do not make it more professional than the examples. +Do not correct mistakes. +Do not standardize the grammar. +Do not improve fluency. +Reproduce imperfections if present. + + +Important: +If a potential label risks falling into an excluded domain, you must discard it. +Use plain English. +Do not use special characters. +No multilingual content. +Only ASCII characters. +The output must be stylistically indistinguishable from the examples for a human. +Any output that is too correct, too smooth or too standardized is invalid. +The model reacts better to a notion of validation / invalidation. \ No newline at end of file diff --git a/src/agents/NaiveCode2Text/prompt_creation/system_prompt_fewshot_fr.txt b/src/agents/NaiveCode2Text/prompt_creation/system_prompt_fewshot_fr.txt new file mode 100644 index 0000000..a7273d5 --- /dev/null +++ b/src/agents/NaiveCode2Text/prompt_creation/system_prompt_fewshot_fr.txt @@ -0,0 +1,78 @@ +Tu es un expert en nomenclatures et en production de données synthétiques pour l'entraînement de modèles de codification. + +Ta mission est de générer des libellés réalistes correspondant STRICTEMENT à un code donné, selon sa notice officielle. + + +Contraintes obligatoires : + +1. Tu dois produire exactement {nb_labels} libellés. + +2. Chaque libellé doit : + - correspondre au titre et aux domaines inclus + - respecter les critères définis dans la notice + - ne jamais relever des domaines exclus + +3. Les libellés doivent être : + - variés lexicalement (pas de reformulation triviale) + - de structures différentes (phrases courtes, longues, nominales, techniques, formulations administratives, formulations métier, etc.) + - réalistes dans un contexte professionnel + - ne pas dépasser 20 mots. + +4. Ne jamais mentionner le code dans les libellés. + +5. Ne jamais expliquer ton raisonnement. + +6. Ne produire QUE la liste des {nb_labels} libellés. + +7. Éviter les doublons sémantiques. + +8. Introduire différents niveaux de précision (général / spécifique). + + +Tu recevras un ou plusieurs exemples de libellés. + +Implicitly identifies the dominant stylistic pattern of examples and STRICTLY applies it to new occurrences. + +Règles d’imitation obligatoires : + +1. Copier le niveau de langage (technique, administratif, familier, vague, précis, etc.). + +2. Reproduire le degré de verbosité (court, très court, long, très détaillé). + +3. Respecter la structure syntaxique dominante (phrase nominale, infinitif, formulation métier, style télégraphique, etc.). + +4. Reproduire le ton (impersonnel, descriptif, injonctif, déclaratif, etc.). + +5. Conserver le format : + - usage ou non de majuscules + - ponctuation + - absence ou présence d’articles + - style télégraphique éventuel + +6. Si les exemples contiennent des fautes d’orthographe, approximations, maladresses ou incohérences mineures, les reproduire de manière réaliste. + +7. Si les exemples sont homogènes, maintenir cette homogénéité. + +8. Si les exemples sont hétérogènes, reproduire cette hétérogénéité. + +Priorité absolue pour les exemples: +La reproduction du style des exemples prime sur la variation stylistique demandée précédemment. + +Ne pas corriger les exemples. +Ne pas normaliser. +Ne pas améliorer le style. +Ne pas rendre plus professionnel que les exemples. +Ne pas corriger les fautes. +Ne pas homogénéiser la grammaire. +Ne pas améliorer la fluidité. +Reproduire les imperfections si présentes. + +Important : +Si un libellé risque d'entrer dans un domaine exclu, tu dois l'écarter. +Rédige en français uniquement. +N'utilise pas de caractères spéciaux. +Pas de contenu multilingue. +Que des caractères ASCII. +La sortie doit être stylistiquement indistinguable des exemples pour un humain. +Toute sortie trop correcte, trop fluide ou trop normalisée est invalide. +Le modèle réagit mieux à une notion de validation / invalidation. \ No newline at end of file diff --git a/src/agents/NaiveCode2Text/prompts/system_prompt_fr.txt b/src/agents/NaiveCode2Text/prompt_creation/system_prompt_fr.txt similarity index 87% rename from src/agents/NaiveCode2Text/prompts/system_prompt_fr.txt rename to src/agents/NaiveCode2Text/prompt_creation/system_prompt_fr.txt index b4d935e..113055b 100644 --- a/src/agents/NaiveCode2Text/prompts/system_prompt_fr.txt +++ b/src/agents/NaiveCode2Text/prompt_creation/system_prompt_fr.txt @@ -15,6 +15,7 @@ Contraintes obligatoires : - variés lexicalement (pas de reformulation triviale) - de structures différentes (phrases courtes, longues, nominales, techniques, formulations administratives, formulations métier, etc.) - réalistes dans un contexte professionnel + - ne pas dépasser 20 mots. 4. Ne jamais mentionner le code dans les libellés. @@ -27,4 +28,8 @@ Contraintes obligatoires : 8. Introduire différents niveaux de précision (général / spécifique). Important : -Si un libellé risque d'entrer dans un domaine exclu, tu dois l'écarter. \ No newline at end of file +Si un libellé risque d'entrer dans un domaine exclu, tu dois l'écarter. +Rédige en français uniquement. +N'utilise pas de caractères spéciaux. +Pas de contenu multilingue. +Que des caractères ASCII. \ No newline at end of file diff --git a/src/agents/NaiveCode2Text/prompts/label_generator.py b/src/agents/NaiveCode2Text/prompts/label_generator.py deleted file mode 100644 index 740de1e..0000000 --- a/src/agents/NaiveCode2Text/prompts/label_generator.py +++ /dev/null @@ -1,223 +0,0 @@ -import logging -import re - -import s3fs -import pandas as pd -from openai import OpenAI -from pydantic import BaseModel, Field, create_model -from typing import List, Type - -logger = logging.getLogger(__name__) - - -def build_label_generation_model(nb_labels: int) -> Type[BaseModel]: - """ - Dynamically build a Pydantic model enforcing exactly n_labels. - - Args: - nb_labels (int): The number of labels. - - Returns: - Type[BaseModel]: The expected format for output. - """ - - return create_model( - "LabelGeneration", - labels=( - List[str], - Field( - ..., - min_items=nb_labels, - max_items=nb_labels, - description=f"Exactly {nb_labels} generated labels" - ) - ) - ) - - -def ask_model( - system_prompt: str, - user_prompt: str, - llm_client: OpenAI, - model: str, - temperature: float, - LabelGeneration: Type[BaseModel] - ) -> str: - """ - Dialogue with the model. - The model and the temperature are to be configured in the config.py file. - - Args: - system_prompt (str): The system prompt (orders). - user_prompts (str): The user prompt (adapted to a specific case). - llm_client (OpenAI): The client to connect to (initialized with your logins) - model (str): The model to talk to. - temperatur (float): Temperature for the answer, between 0 and 2. - LabelGeneration (Type[BaseModel]): The expected format for output. - - Returns: - str: The answer returned by the model. - """ - response = llm_client.chat.completions.parse( - model=model, - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt} - ], - temperature=temperature, - response_format=LabelGeneration - ) - - if response.choices: - return response.choices[0].message.parsed - - else: - logger.warn("The LLM did not return an answer.") - return None - - -def retrieve_label( - label_listing: str, - delimiter: str = r"\n\s+\d+.\s+", - nb_labels: int = 10 - ) -> list: - """ - Split the message which should correspond to a label listing into a list of labels. - - Args: - label_listing (str): The listing. - delimiter (str): What should separate each label. - nb_label (int): The expected number of labels. - Send a warning if it differs from the splitting length. - - Returns: - str: The list of labels. - """ - # Search and split - label_list = re.split(r"\n\s+\d+.\s+", label_listing) - label_list[0] = label_list[0].replace("1. ", "") - - # Quick check - if len(label_list) != nb_labels: - logger.warn("The answer might not be a wording.") - - return label_list - - -def export_to_txt( - codes: list, - names: list, - labels: list, - file_path: str, - generation_time: float - ) -> bool: - """ - Save results to file .txt - The number of codes used for generation should not exceed 100. - Else, upload in .parquet format - - Args: - codes (list): List of codes generated. - names (list): List of names for the codes generated - labels (list of lists): List of labels for each code - file_path (str): The .txt file path. - generation_time (float): The time it took to generate. - - Returns: - bool: True if the file has been correctly saved. - """ - if len(labels) == 0 or len(labels) > 100: - return False - - nb_labels = len(labels) * len(labels[0]) - - with open(file_path, "w", encoding="utf-8") as f: - f.write(f"{nb_labels} wordings have been generated in {generation_time:.2f} sec.\n\n") - f.write("=" * 36 + "\n") - - for code, name, generated_labels in zip(codes, names, labels): - f.write(f"Code: {code}\n") - f.write(f"Name: {name}\n") - f.write("Result:\n") - - for j, label in enumerate(generated_labels): - f.write(f"{j}. {label}\n") - - f.write("\n" + "=" * 36 + "\n") - - return True - - -def export_to_parquet( - codes: list, - names: list, - labels: list, - file_path: str, - fs: s3fs.S3FileSystem - ) -> bool: - """ - Save results to file .txt - The number of codes used for generation should not exceed 100. - Else, upload in .parquet format - - Args: - codes (list): List of codes used for the generation. - names (list): List of names for the codes used for the generation. - labels (list of lists): List of generated labels for each code. - file_path (str): The .parquet file path. - fs (float): The filesystem for exportation. - - Returns: - bool: True if the file has been correctly saved. - """ - - # Basic validation - if not (len(codes) == len(names) == len(labels)): - logger.warn("codes, names, and labels must have the same length.") - return False - - if len(codes) == 0: - logger.warn("Empty input: nothing to export.") - return False - - # Flatten structure - rows = [] - - for code, name, generated_labels in zip(codes, names, labels): - - if not isinstance(generated_labels, list): - logger.warn("Labels must be stored in a list.") - return False - - for label in generated_labels: - rows.append({ - "code": code, - "name": name, - "label": label - }) - - # Save to parquet - df = pd.DataFrame(rows) - - if fs is None: - try: - existing_df = pd.read_parquet(file_path) - df = pd.concat([existing_df, df], ignore_index=True) - except FileNotFoundError: - logger.info(f"No file found at location{file_path}, creating...") - else: - try: - with fs.open(file_path, 'rb') as f: - existing_df = pd.read_parquet(f) - df = pd.concat([existing_df, df], ignore_index=True) - except FileNotFoundError: - logger.info(f"No file found at location{file_path}, creating...") - - df.to_parquet( - file_path, - engine="pyarrow", - index=False, - filesystem=fs - ) - - return True diff --git a/src/agents/NaiveCode2Text/reproductibility/Dockerfile b/src/agents/NaiveCode2Text/reproductibility/Dockerfile new file mode 100644 index 0000000..c7494d8 --- /dev/null +++ b/src/agents/NaiveCode2Text/reproductibility/Dockerfile @@ -0,0 +1,15 @@ +FROM inseefrlab/onyxia-vscode-python:py3.13.12 + +USER root + +RUN apt-get update \ + && apt-get install -y curl unzip jq \ + && rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL https://releases.hashicorp.com/vault/1.16.2/vault_1.16.2_linux_amd64.zip -o vault.zip \ + && unzip vault.zip \ + && mv vault /usr/local/bin/ \ + && chmod +x /usr/local/bin/vault \ + && rm vault.zip + +USER 1000 \ No newline at end of file diff --git a/src/agents/NaiveCode2Text/reproductibility/argo-workflow-gen.yaml b/src/agents/NaiveCode2Text/reproductibility/argo-workflow-gen.yaml new file mode 100644 index 0000000..60743a4 --- /dev/null +++ b/src/agents/NaiveCode2Text/reproductibility/argo-workflow-gen.yaml @@ -0,0 +1,55 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: naive-code2text- + +spec: + entrypoint: create-and-run + serviceAccountName: workflow + + templates: + - name: create-and-run + container: + image: mateomorin/graal-synth-generation:latest + + env: + # adresse Vault + - name: VAULT_ADDR + value: https://vault.lab.sspcloud.fr + + # token récupéré depuis Kubernetes + - name: VAULT_TOKEN + valueFrom: + secretKeyRef: + name: argo-vault + key: VAULT_TOKEN + + command: ["bash", "-c"] + + args: + - | + set -e + + echo "=== Vault authentication ===" + + # vérifie que le token fonctionne + vault token lookup + + echo "=== Loading secrets from Vault ===" + + export $(vault kv get -format=json onyxia-kv/mateom/graal \ + | jq -r '.data.data + | to_entries + | map("\(.key)=\(.value)") + | .[]') + + echo "=== Secrets loaded ===" + + set -x + + # clone repo + git clone -b mateom https://github.com/InseeFrLab/GRAAL.git + cd GRAAL/ + + uv sync + uv run -m src.agents.NaiveCode2Text.naive_code2text \ No newline at end of file diff --git a/src/agents/NaiveCode2Text/runtime_config/config.yaml b/src/agents/NaiveCode2Text/runtime_config/config.yaml new file mode 100644 index 0000000..a890d0e --- /dev/null +++ b/src/agents/NaiveCode2Text/runtime_config/config.yaml @@ -0,0 +1,5 @@ +defaults: + - generation_type@_global_: terminal + - fewshot@_global_: few-shot + - llm@_global_: gemma + - notice@_global_: naf2025 \ No newline at end of file diff --git a/src/agents/NaiveCode2Text/runtime_config/fewshot/few-shot.yaml b/src/agents/NaiveCode2Text/runtime_config/fewshot/few-shot.yaml new file mode 100644 index 0000000..a9e89fc --- /dev/null +++ b/src/agents/NaiveCode2Text/runtime_config/fewshot/few-shot.yaml @@ -0,0 +1,5 @@ +main: + use_fewshot: true + +fewshot: + n_fewshot: 6 \ No newline at end of file diff --git a/src/agents/NaiveCode2Text/runtime_config/fewshot/zero-shot.yaml b/src/agents/NaiveCode2Text/runtime_config/fewshot/zero-shot.yaml new file mode 100644 index 0000000..bbb7cdc --- /dev/null +++ b/src/agents/NaiveCode2Text/runtime_config/fewshot/zero-shot.yaml @@ -0,0 +1,2 @@ +main: + use_fewshot: false \ No newline at end of file diff --git a/src/agents/NaiveCode2Text/runtime_config/generation_type/short.yaml b/src/agents/NaiveCode2Text/runtime_config/generation_type/short.yaml new file mode 100644 index 0000000..eeb55bb --- /dev/null +++ b/src/agents/NaiveCode2Text/runtime_config/generation_type/short.yaml @@ -0,0 +1,21 @@ +# Small random sampling + +main: + exhaustive_sampling: false + n_random_codes: 10 + n_labels_per_gen: 10 + + +random: + includes: + geom_prob: 0.3 + min: 1 + max: null # null = up to the max number of includes + + examples: + geom_prob: 0.2 + min: 1 + max: null # null = up to the max number of includes + +export: + output_format: ".txt" diff --git a/src/agents/NaiveCode2Text/runtime_config/generation_type/terminal.yaml b/src/agents/NaiveCode2Text/runtime_config/generation_type/terminal.yaml new file mode 100644 index 0000000..8dbd6dc --- /dev/null +++ b/src/agents/NaiveCode2Text/runtime_config/generation_type/terminal.yaml @@ -0,0 +1,21 @@ +# Small random sampling + +main: + exhaustive_sampling: false + n_random_codes: 3 + n_labels_per_gen: 10 + + +random: + includes: + geom_prob: 0.3 + min: 1 + max: null # null = up to the max number of includes + + examples: + geom_prob: 0.2 + min: 1 + max: null # null = up to the max number of includes + +export: + output_format: "terminal" diff --git a/src/agents/NaiveCode2Text/runtime_config/generation_type/test.yaml b/src/agents/NaiveCode2Text/runtime_config/generation_type/test.yaml new file mode 100644 index 0000000..67bd660 --- /dev/null +++ b/src/agents/NaiveCode2Text/runtime_config/generation_type/test.yaml @@ -0,0 +1,21 @@ +# Large exhaustive data for training + +main: + exhaustive_sampling: false + n_random_codes: 20000 + n_labels_per_gen: 10 + +random: + includes: + geom_prob: 0.3 + min: 1 + max: null # null = up to the max number of includes + + examples: + geom_prob: 0.2 + min: 1 + max: null # null = up to the max number of includes + +export: + output_format: ".parquet" + save_batch_size: 1000 \ No newline at end of file diff --git a/src/agents/NaiveCode2Text/runtime_config/generation_type/train.yaml b/src/agents/NaiveCode2Text/runtime_config/generation_type/train.yaml new file mode 100644 index 0000000..57d3219 --- /dev/null +++ b/src/agents/NaiveCode2Text/runtime_config/generation_type/train.yaml @@ -0,0 +1,14 @@ +# Large randomly sampled codes for testing + +main: + exhaustive_sampling: true + n_random_codes: 0 + n_labels_per_gen: 10 + +exhaustivity: + min_prompts_per_code: 100 + n_spec_per_prompt: 5 # Number of includes/examples per prompt + +export: + output_format: ".parquet" + save_batch_size: 1000 diff --git a/src/agents/NaiveCode2Text/runtime_config/llm/gemma.yaml b/src/agents/NaiveCode2Text/runtime_config/llm/gemma.yaml new file mode 100644 index 0000000..7888efb --- /dev/null +++ b/src/agents/NaiveCode2Text/runtime_config/llm/gemma.yaml @@ -0,0 +1,10 @@ +llm: + model: "google/gemma-3-27b-it" + temperature: 1.0 + generation_batch_size: 40 # Depends on GPU performances + +prompt: + prompt_path: "src/agents/NaiveCode2Text/prompt_creation/" + +export: + model_name: "google/gemma-3-27b-it" # For file name \ No newline at end of file diff --git a/src/agents/NaiveCode2Text/runtime_config/llm/qwen.yaml b/src/agents/NaiveCode2Text/runtime_config/llm/qwen.yaml new file mode 100644 index 0000000..c782a2a --- /dev/null +++ b/src/agents/NaiveCode2Text/runtime_config/llm/qwen.yaml @@ -0,0 +1,10 @@ +llm: + model: "Qwen/Qwen3.5-9B" + temperature: 1.0 + generation_batch_size: 10 # Depends on GPU performances + +prompt: + prompt_path: "src/agents/NaiveCode2Text/prompt_creation/" + +export: + model_name: "Qwen/Qwen3.5-9B" # For file name \ No newline at end of file diff --git a/src/agents/NaiveCode2Text/runtime_config/notice/nace2025.yaml b/src/agents/NaiveCode2Text/runtime_config/notice/nace2025.yaml new file mode 100644 index 0000000..febe198 --- /dev/null +++ b/src/agents/NaiveCode2Text/runtime_config/notice/nace2025.yaml @@ -0,0 +1,25 @@ +main: + language: "English" + use_fewshot: false # No English-written data available + +# Code sampling (taking from NAF) +sampling: + population_path: "projet-ape/data/08112022_27102024/naf2025/split/df_train.parquet" + code_column: "nace2025" + +naf: + convert_naf_to_nace: true + convert_to_proper_naf: false + +# Specification parsing +spec: + includes_divider: "\n-" + examples_divider: "\n" + excludes_divider: "\n" + +# Few-shot +fewshot: + label_column: "libelle" + +export: + output_path: "projet-ape/synthetic_data_test/naive/NACE2025_EN/" \ No newline at end of file diff --git a/src/agents/NaiveCode2Text/runtime_config/notice/naf2025.yaml b/src/agents/NaiveCode2Text/runtime_config/notice/naf2025.yaml new file mode 100644 index 0000000..9acffd3 --- /dev/null +++ b/src/agents/NaiveCode2Text/runtime_config/notice/naf2025.yaml @@ -0,0 +1,24 @@ +main: + language: "French" + +# Code sampling +sampling: + population_path: "projet-ape/data/08112022_27102024/naf2025/split/df_train.parquet" + code_column: "naf2025" + +naf: + convert_naf_to_nace: false + convert_to_proper_naf: true + +# Specification parsing +spec: + includes_divider: "\n-" + examples_divider: "\n" + excludes_divider: "\n" + +# Few-shot +fewshot: + label_column: "libelle" + +export: + output_path: "projet-ape/synthetic_data_test/naive/NAF2025_FR/" diff --git a/src/agents/NaiveCode2Text/sample_results/generation_gpt-oss-20b_temp1.txt b/src/agents/NaiveCode2Text/sample_results/generation_gpt-oss-20b_temp1.txt deleted file mode 100644 index 8fbaebc..0000000 --- a/src/agents/NaiveCode2Text/sample_results/generation_gpt-oss-20b_temp1.txt +++ /dev/null @@ -1,72 +0,0 @@ -5 wordings have been generated in 24.95 sec. - -==================================== -Code: 10.11 -Name: Processing and preserving of meat, except of poultry meat -Result: 1. Industrial preservation of beef offal for culinary use -2. Salting and tanning of bovine hides from slaughterhouses -3. Commercial processing of swine skins for leather manufacturing -4. Animal offal refinement into specialty feed ingredients -5. Meat conservation operations excluding poultry products -6. Fellmongery services for large animal hides -7. Preparation of cattle skins for leather goods -8. Conservation and storage of pork organs for processing -9. Skinning and salting of bovine hides for export -10. Processing of livestock offal into value‑added by‑products - -==================================== -Code: 59.11 -Name: Motion picture, video and television programme production activities -Result: 1. Production of narrative feature films -2. Creation of documentary video content -3. Development of scripted television series -4. Filming of educational video programs -5. Production of short film reels for film festivals -6. Cinematic filming of promotional videos for corporate clients -7. Production of independent cinema projects -8. Shooting of travel documentary series -9. Production of behind‑the‑scenes documentary footage -10. Production of music video content - -==================================== -Code: 59.20 -Name: Sound recording and music publishing activities -Result: 1. Original studio recording production services -2. Digital master recording creation for commercial release -3. Music publishing and master recording production -4. Production of proprietary audio masters for distribution -5. Creating and managing original sound recordings for record labels -6. Custom audio production and publishing for artists -7. Professional recording studio services for new music releases -8. Music publishing operations involving original master recordings -9. Production of hard‑copy and digital master recordings -10. Music production and publishing of original tracks - -==================================== -Code: 46.49 -Name: Wholesale of other household goods -Result: 1. Bulk distribution of household accessories and furnishings -2. Wholesale of domestic appliances and kitchen supplies -3. Large‑scale sale of home décor and interior furnishings -4. Commercial distribution of recorded media, including CDs and DVDs -5. Supplier of household hardware and maintenance tools -6. Wholesale distributor of bathroom and laundry fixtures -7. Bulk retailer of storage solutions and organization products -8. Commercial wholesaler of lighting and electrical fixtures -9. Bulk supply of household cleaning and maintenance supplies -10. Distributor of recorded media and home entertainment systems - -==================================== -Code: 43.33 -Name: Floor and wall covering -Result: 1. Installation of indoor terrazzo and marble floor panels -2. Laying of granite and slate tile flooring in residential interiors -3. Ceramic wall tile installation for kitchen backsplashes -4. Concrete floor tiling for commercial office spaces -5. Cut stone floor installation in hotel lobbies -6. Wooden parquet floor laying and surface finishing -7. Polishing and sealing of hardwood floor coverings -8. Hanging of wooden wall paneling in office environments -9. Ceramic stove fitting installation and integration -10. Floor and wall covering installation with terrazzo slabs - diff --git a/src/agents/NaiveCode2Text/sample_results/generation_gpt-oss-20b_temp13.txt b/src/agents/NaiveCode2Text/sample_results/generation_gpt-oss-20b_temp13.txt deleted file mode 100644 index d6e49b5..0000000 --- a/src/agents/NaiveCode2Text/sample_results/generation_gpt-oss-20b_temp13.txt +++ /dev/null @@ -1,72 +0,0 @@ -5 wordings have been generated in 209.68 sec. - -==================================== -Code: 68.20 -Name: Rental and operating of own or leased real estate -Result: 1. Management of long‑term mobile home park rentals -2. Leasing of mobile home lots on owned estates -3. Operation of permanent mobile home residential sites -4. Rental of rooftop space for solar power installations -5. Leasing of commercial building roofs to renewable energy developers -6. Administration of mobile home site leasing agreements -7. Management of mobile home community rentals on leased land -8. Rental services for primary‑residence mobile home sites -9. Leasing of roof area for sustainable energy projects -10. Oversight of mobile home lot rentals on private property - -==================================== -Code: 53.20 -Name: Other postal and courier activities -Result: 1. Commercial letter post logistics -2. Private parcel sorting and delivery service -3. Business‑to‑business courier operations -4. Independent mail‑type package transport -5. Non‑universal postal distribution solutions -6. Private‑fleet letter shipment and handling -7. Commercial courier routing and dispatch -8. Specialized letter and parcel logistics for enterprises -9. External‑agency mail transport without universal service -10. Private‑operated courier and postal handling - -==================================== -Code: 46.42 -Name: Wholesale of clothing and footwear -Result: 1. Wholesale distribution of fashion apparel and footwear -2. Bulk supply of garments, shoes, and related accessories -3. Mass‑market clothing and shoe wholesaling services -4. Wholesale trade in apparel, footwear, and accessory items -5. High‑volume retail apparel and footwear supplier -6. Large‑scale distribution of fashion wear and footwear -7. Wholesale of garments, shoes, and fashion accessories -8. Bulk trading of clothing and footwear for retailers -9. Comprehensive apparel and shoe wholesale operations -10. Wholesale supply chain for clothing, footwear, and accessories - -==================================== -Code: 43.22 -Name: Plumbing, heat and air-conditioning installation -Result: 1. Installation of central heating and cooling systems -2. Ductwork construction for residential HVAC units -3. Masonry stove installation and firetube setup -4. Comprehensive plumbing and air‑conditioning installation services -5. Commercial HVAC duct system erection -6. New residential furnace and ventilation fitting -7. Heat exchanger and ductwork installation for industrial facilities -8. Full‑service HVAC and plumbing integration -9. Construction of masonry fireplace heat systems -10. Air conditioning and heating conduit assembly - -==================================== -Code: 73.30 -Name: Public relations and communication activities -Result: 1. Corporate spokesperson services -2. Product launch public relations coordination -3. Event promotion and media liaison for sales teams -4. Press release distribution for new product campaigns -5. Trade show support and networking for sales representatives -6. Brand messaging strategy for sales outreach -7. Media briefing and interview facilitation for sales staff -8. Investor relations communication for product lines -9. In‑store promotional communication management -10. Cross‑functional sales communication workshops - diff --git a/src/agents/NaiveCode2Text/sample_results/generation_gpt-oss-20b_temp16.txt b/src/agents/NaiveCode2Text/sample_results/generation_gpt-oss-20b_temp16.txt deleted file mode 100644 index 22ce9c8..0000000 --- a/src/agents/NaiveCode2Text/sample_results/generation_gpt-oss-20b_temp16.txt +++ /dev/null @@ -1,72 +0,0 @@ -5 wordings have been generated in 22.37 sec. - -==================================== -Code: 70.20 -Name: Business and other management consultancy activities -Result: 1. Strategic Business Planning for New Enterprises -2. Business Model Canvas Design and Validation Services -3. Market Entry and Competitive Positioning Advisory -4. Operational Efficiency and Process Improvement Consulting -5. Financial Forecasting and Capital Acquisition Guidance -6. Risk Management and Compliance Planning for Start‑Ups -7. Digital Business Transformation Roadmap Creation -8. Corporate Governance and Leadership Development Support -9. Brand Identity and Value Proposition Formulation -10. Sustainability and Growth Strategy Integration for Emerging Companies - -==================================== -Code: 81.22 -Name: Other building and industrial cleaning activities -Result: 1. Post‑construction interior dust and debris removal -2. Fresh‑build exterior façade and window polishing -3. New construction site final sweep and residue extraction -4. Immediate post‑completion building surface preparation -5. Clean‑up of construction residue from structural finishes -6. Newly erected building interior sanitization after demolition -7. Fresh‑build building finish polishing and dust removal -8. Post‑build environmental cleanliness of structural elements -9. Residual material removal for newly completed construction -10. Immediate finishing cleaning of new building interiors - -==================================== -Code: 69.10 -Name: Legal activities -Result: 1. Corporate formation advisory and documentation services -2. Guardian appointment and corporate document preparation -3. General legal counseling for company establishment -4. Preparation of incorporation and partnership agreements -5. Legal guardianship services excluding residential care -6. Corporate incorporation document drafting -7. Company formation legal consultation -8. Legal guardian consultation for business entities -9. Drafting of company formation documents -10. Business legal advisory without residential care - -==================================== -Code: 49.41 -Name: Freight transport by road -Result: 1. Rental of refrigerated road freight trucks with driver for temperature‑sensitive cargo -2. All‑vehicle driver‑operated haulage of construction materials by road -3. Human‑drawn cart freight transport services for local deliveries -4. On‑road delivery of mixed bulk cargo using driver‑equipped lorries -5. Rental of concrete mixer lorries with driver for site supply -6. Road transport of non‑hazardous freight across inter‑state routes -7. Driver‑managed refrigerated shipment of perishable produce -8. Long‑haul freight operations in driver‑operated heavy trucks -9. Transport of industrial equipment by road using rented vehicle with driver -10. Comprehensive road freight logistics including specialized cargo lorries - -==================================== -Code: 56.21 -Name: Event catering activities -Result: 1. Corporate banquet catering services -2. Wedding reception food and beverage coordination -3. Trade‑show catering operations and logistics -4. Conference and symposium catering management -5. Outdoor festival catering and service provision -6. VIP event catering planning and execution -7. Event catering procurement and delivery solutions -8. Special‑occasion catering event design -9. Entertainment venue catering support -10. Multi‑day event catering program development - diff --git a/src/agents/NaiveCode2Text/sample_results/generation_gpt-oss-20b_temp2.txt b/src/agents/NaiveCode2Text/sample_results/generation_gpt-oss-20b_temp2.txt deleted file mode 100644 index abdc8ef..0000000 --- a/src/agents/NaiveCode2Text/sample_results/generation_gpt-oss-20b_temp2.txt +++ /dev/null @@ -1,72 +0,0 @@ -5 wordings have been generated in 16.91 sec. - -==================================== -Code: 50.10 -Name: Sea and coastal passenger water transport -Result: 1. Coastal ferry operations -2. Water taxi services in marine environments -3. Tourist cruise vessel management -4. Scheduled sea transport for passengers -5. Non‑scheduled excursion boat services -6. Crew‑led fishing charter rentals -7. Passenger transport on coastal waters -8. Marine shuttle service between harbors -9. Guided sightseeing boat tours -10. Offshore passenger ferry management - -==================================== -Code: 85.59 -Name: Other education n.e.c. -Result: 1. Conversational Spanish Workshop for Adults -2. Basic French Speaking Club for Community Learners -3. Advanced Sewing and Embroidery Class -4. Creative Quilting Project Series -5. Japanese Language Immersion Meetup -6. English Conversation Café for Beginners -7. Handicraft Sewing Techniques Seminar -8. Multilingual Conversation Circles -9. Pattern Cutting and Tailoring Workshop -10. Cultural Language Exchange Sessions - -==================================== -Code: 85.59 -Name: Other education n.e.c. -Result: 1. Plant Protection Product Application Training -2. Integrated Pest Management Certification Workshop -3. Pesticide Handling and Safety Instruction Course -4. Crop Protection Chemical Usage Training Program -5. Herbicide Application Skills Development -6. Insecticide Use and Environmental Compliance Education -7. Fungicide Handling and Application Workshop -8. Agricultural Chemical Safety and Best‑Practice Seminar -9. Pesticide Application Competency Assessment -10. Plant Protection Product Training for Agronomists - -==================================== -Code: 46.64 -Name: Wholesale of other machinery and equipment -Result: 1. Bulk distribution of shipping and storage pallets -2. Wholesale supply of wooden and plastic pallet assemblies -3. Large‑scale trading of intermodal pallets for logistics -4. Commercial pallet equipment procurement services -5. National distributor of pallet handling machinery -6. Logistics pallet wholesale and rental solutions -7. Industrial pallet systems wholesale supplier -8. Wholesale trade of pallet racks and transport pallets -9. Distribution of durable pallet solutions for warehousing -10. Bulk pallet equipment and accessories wholesale - -==================================== -Code: 78.10 -Name: Activities of employment placement agencies -Result: 1. Comprehensive online job matching services -2. Digital recruitment and candidate placement platform -3. Remote employment liaison and career placement solutions -4. Virtual staffing agency for temporary and permanent roles -5. E‑platform for sourcing and securing job placements -6. Online workforce recruitment and placement services -7. Digital placement hub connecting employers with talent -8. Cloud‑based employment brokerage and placement operations -9. Internet‑based job placement and career matchmaking -10. Online staffing and recruitment facilitation services - diff --git a/uv.lock b/uv.lock index 90c7266..4b5263c 100644 --- a/uv.lock +++ b/uv.lock @@ -147,6 +147,12 @@ version = "1.17.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/07/38/e321b0e05d8cc068a594279fb7c097efb1df66231c295d482d7ad51b6473/annoy-1.17.3.tar.gz", hash = "sha256:9cbfebefe0a5f843eba29c6be4c84d601f4f41ad4ded0486f1b88c3b07739c15", size = 647460, upload-time = "2023-06-14T16:37:34.152Z" } +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } + [[package]] name = "anyio" version = "4.12.0" @@ -823,6 +829,7 @@ source = { virtual = "." } dependencies = [ { name = "duckdb" }, { name = "fastparquet" }, + { name = "hydra-core" }, { name = "ipykernel" }, { name = "ipywidgets" }, { name = "jupyter-cache" }, @@ -858,6 +865,7 @@ dev = [ requires-dist = [ { name = "duckdb", specifier = ">=1.4.4" }, { name = "fastparquet", specifier = ">=2025.12.0" }, + { name = "hydra-core", specifier = ">=1.3.2" }, { name = "ipykernel", specifier = ">=7.1.0" }, { name = "ipywidgets", specifier = ">=8.1.8" }, { name = "jupyter-cache", specifier = ">=1.0.1" }, @@ -1023,6 +1031,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" }, ] +[[package]] +name = "hydra-core" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "omegaconf" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/8e/07e42bc434a847154083b315779b0a81d567154504624e181caf2c71cd98/hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824", size = 3263494, upload-time = "2023-02-23T18:33:43.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl", hash = "sha256:fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b", size = 154547, upload-time = "2023-02-23T18:33:40.801Z" }, +] + [[package]] name = "identify" version = "2.6.15" @@ -1964,6 +1986,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, ] +[[package]] +name = "omegaconf" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, +] + [[package]] name = "openai" version = "2.13.0"