Repository: hand-e-fr/OpenHosta (language: python)

Issue:
Merge v1.1 to v1.2beta 2



Discussion / hints:

Fix patch:
diff --git a/CHAGELOG.md b/CHANGELOG.md
similarity index 81%
rename from CHAGELOG.md
rename to CHANGELOG.md
index e7ff99e..054c21a 100644
--- a/CHAGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,34 @@
 
 All significant changes to this project will be documented in this file.
 
+## **v1.2rc-1  10/10/2024**
+
+### **New Features**
+
+1. **TrainingSet Management**
+   Manage training datasets effortlessly with new tools:
+   - **`.visualize`**: Inspect current data visually.
+   - **`.add`**: Add new examples.
+
+2. **Enhanced `predict` Attributes**
+   New functionalities for `predict`:
+   - **`.retrain`**: Retrain models with specified parameters.
+   - **`.continue_train`**: Continue training with existing weights.
+   - **`.emulate`**: Run predictions through an LLM.
+
+### **Enhancements**
+
+- **Expanded Dataset Support**: `load_training_example` (*previously `load_examples`*) supports JSON, JSONL, and CSV formats for easier integration.
+  
+- **Verbose Mode in `predict`**: Track detailed model training and set target losses with **get_loss**.
+
+### **Fixes**
+
+- **CUDA Compatibility**: `predict` now works with CUDA-enabled GPUs (device ID selection pending).
+
 ---
 
+
 ## **v1.1.1** 10/07/24
 
 - **Features**
@@ -23,7 +49,7 @@ All significant changes to this project will be documented in this file.
   - Re-added `diagramm` attributs but decrepated
   - Added explicitly a neutral response in the `emulate` prompt (None)
 
-## **v1.1-rc4** 27/09/24
+## **v1.1-rc4** 09/27/24
 
 - **Feature**
   - Added `suggest` function. Works the same as the `__suggest__` attributs but in a function
@@ -37,7 +63,7 @@ All significant changes to this project will be documented in this file.
 - **Fixes**
   - `suggest` attribute `diagramm` is now `diagram`
   
-## **v1.1-rc3** 26/09/23
+## **v1.1-rc3** 09/26/23
 
 - **Fixes**
   - `emulate` now works when emulated function is called inside another one
@@ -49,7 +75,7 @@ All significant changes to this project will be documented in this file.
   - Added a Makefile for cleaning and packaging and tests
 ---
 
-## **v1.1** 13/09/2024
+## **v1.1** 09/13/2024
 
 - **Fixes**
   - the `emulate` function is now decorator-resistant.
@@ -89,7 +115,7 @@ All significant changes to this project will be documented in this file.
 
 ---
 
-## **v1.0** 29/08/2024:
+## **v1.0** 08/29/2024:
 
 - **Features**
   - Function *emulate* to emulate a function by LLM.
diff --git a/README.md b/README.md
index 6a574ac..7ef22dd 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
 # OpenHosta 
-v1.1.2 - Open-Source Project
+v1.2.0 - Opensource Project
 
 **- The future of development is human -**
 
diff --git a/doc/Docs.md b/doc/Docs.md
index f3d45da..d23a425 100644
--- a/doc/Docs.md
+++ b/doc/Docs.md
@@ -1,7 +1,7 @@
 # Documentation
 ___
 
-Documentation for version: **1.1.2**
+Documentation for version: **1.2.0
 
 Welcome to **OpenHosta** documentation :). Here you'll find all the **explanations** you need to understand the library, as well as **usage examples** and advanced **configuration** methods for the most complex tasks. You'll also find explanations of the source code for those interested in **contributing** to this project. Check the [Google Colab](https://colab.research.google.com/drive/1XKrPrhLlYJD-ULTA8WHzIMqTXkb3iIpb?usp=sharing) **test files** to help you take your first steps in discovering OpenHosta.
 
@@ -79,6 +79,10 @@ Let's **get started**! First here's the **table of contents** to help you naviga
     - ["suggest" Function](#suggest-function)
       - [Usage](#usage)
       - [Output Examples](#output-examples)
+    - ["predict" Function](#predict-function)
+      - [Parameters](#predict-function-parameters)
+      - [Additional fonctionalities](#additional-predict-functionalities)
+      - [Output setting](#training-output)
     - ["thought" Function](#thought-function)
     - ["example" Function](#example-function)
     - [Advanced configuration](#advanced-configuration)
@@ -163,7 +167,7 @@ Note that some features like `thought` or `__suggest__` specifically use the def
 config.set_default_model(my_model)
 ```
 
-### "emulate" Function
+## `emulate` Function
 
 The *emulate* function is the main feature of OpenHosta. This is the function that allows you to emulate functions with AI, i.e. the instructions will be executed in an LLM and not directly in your computer. Here's how to use it.
 
@@ -306,6 +310,10 @@ You can also retrieve the entire LLM response by storing the output of the `sugg
 
 Note that this feature uses the default model.
 
+You can also retrieve the entire LLM response by storing the output of the `suggest` function.
+
+Note that this feature uses the default model.
+
 #### Output Examples
 
 - **Enhanced prompt:**
@@ -328,7 +336,163 @@ graph LR
     I --> J[End]
 ```
 
-### "thought" Function
+
+---
+
+## `predict` Function
+
+The `predict` function is the second major feature of OpenHosta, designed to enable the dynamic creation of models for specific functions. While it shares similarities with the `emulate` function, instead of making API calls to a large language model (LLM), `predict` generates an internal model—currently supporting only linear regression.
+
+### How `predict` Works
+
+The `predict` function allows users to train a model automatically by providing a set of training examples. It simplifies model-building by handling the training process directly within a Python function. 
+
+At this time, `predict` has a few **limitations** to be aware of:
+
+- **Supported Input Types**: Only `int` and `float` types are allowed as inputs.
+- **Return Type**: The function returns output as a `float`.
+- **Model Type**: Currently, the function builds a simple linear regression model with a single output.
+- **Training Examples**: You must provide at least one example for the model to be trained correctly.
+
+### Limitations and Known Issues
+
+- Since `predict` is still in its Release Candidate (RC) phase, some instability and bugs might occur. 
+- If you encounter any issues, please help improve the functionality by reporting them :)
+---
+
+
+- *Below is a practical example demonstrating how to use `predict` to build a model that estimates a person's chance of dying based on their age*:
+
+```python
+from OpenHosta import predict, example
+
+def find_chance_of_die(age: float) -> float:
+    """
+    This function predicts the chance of dying in a percentage value from 0 to 1, 
+    based on the age (with the baseline year starting at 1900).
+    """
+    # We forced some interpolation not real data
+    example(age=124.0, hosta_out=0.99)
+    example(age=100.5, hosta_out=0.20)
+    example(age=55.0,  hosta_out=0.60)
+    example(age=45.0,  hosta_out=0.10)
+    example(age=24.8,  hosta_out=0.20)
+    example(age=8.0,   hosta_out=0.01)
+    return predict()
+
+x = find_chance_of_die(124.0)
+print(x)
+```
+For `example` documentation, please go to this [link](#example-function)
+
+### `predict` Function Parameters
+
+The `predict` function includes several parameters that allow you to fine-tune the model's behavior. Below is a list of these parameters:
+
+- **`epochs` (int)**: 
+   Defines how many times the model iterates over the training set. Increasing the number of epochs may lead to better model convergence at the cost of longer training times. The default value is 2 times the dataset size, calculated based on the batch size.
+   
+- **`complexity` (int)**: 
+   Sets the level of complexity for the model, which influences the number of weights based on the length of the input. The default value is `5`.
+   
+- **`normalization` (bool)**: 
+   Enables or disables data normalization. When set to `True`, the input data will be normalized based on the `norm_min` and `norm_max` values. The default is `False`.
+   
+- **`norm_min` (float)**: 
+   Defines the minimum value for data normalization. This value helps scale input data to a normalized range. The default is `0.1` for value that are different than 0.
+   
+- **`norm_max` (float)**: 
+   Specifies the maximum value for data normalization. This value sets the upper bound for the normalized range. The default is `1.0`.
+   
+- **`verbose` (bool)**: 
+   Enables or disables verbose output during training. When set to `True`, detailed progress information, including loss values, will be displayed. The default is `False`.
+   
+- **`batch_size` (int)**: 
+   Defines the number of training examples to be used in one iteration. By default, it is set to `5%` of the dataset size or `len(dataset)` if the dataset size is too small for 5%.
+
+---
+
+### Additional `predict` Functionalities
+
+The `predict` function also comes with several methods designed to enhance the user experience when building and refining an *Hosta model*. Below are the key methods you can use to interact with and further train your models:
+
+#### 1. `retrain`
+
+The `retrain` method allows you to retrain the model from scratch. It takes several directive parameters:
+
+- **`epochs`**: Specifies the number of training epochs.
+- **`get_loss`**: Defines a target loss for the model to reach during training.
+- **`verbose`**: Displays detailed training information (if set to `True`).
+
+#### Example:
+```python 
+find_chance_of_die.retrain(epochs=150, get_loss=0.001, verbose=True)
+```
+
+#### 2. `continue_train`
+
+The `continue_train` method allows you to continue training the model using the current weights, rather than starting from scratch. It also accepts directive parameters:
+
+- **`epochs`**: Specifies how many additional epochs you want to train the model for.
+- **`get_loss`**: Defines a target loss value for the model to reach during continued training.
+- **`verbose`**: Displays training progress information (if set to `True`).
+
+#### Example:
+```python 
+find_chance_of_die.continue_train(epochs=150, get_loss=0.001, verbose=True)
+```
+
+#### 3. `emulate`
+
+The `emulate` function makes an API call to a Large Language Model (LLM) to assist in answering predictions made by the `predict` function. For more details, check the documentation of [predict](#predict-function). 
+
+#### Example:
+```python
+find_chance_of_die.emulate(124.0)
+```
+
+--- 
+
+
+### TrainingSet Management
+
+The `TrainingSet` feature offers easy tools for managing training datasets in `hosta_injected` functions:
+
+- **`.visualize`**: View the current dataset and its examples.
+- **`.add`**: Add new examples to the dataset.
+
+#### Example:
+
+You can generate and add data to your training set like so:
+
+```python
+
+def cos_plus_sin_generator():
+    for i in range(0, 10):
+        for j in range(0, 10):
+            cos_value = math.cos(i) ** i
+            sin_value = math.sin(j) ** j
+            training_maths.add(cos=i, sin=j, hosta_out=cos_value + sin_value)
+            # Add data to the training set
+
+cos_plus_sin_generator()
+training_maths.visualize()  # Visualize the dataset
+```
+
+This allows you to both populate and inspect your training data with ease.
+
+---
+
+### Training Output of predict
+
+When training the model using `predict`, a corresponding folder will be created under `__hostachache__`. This folder will contain:
+- `config.json`: Configuration file describing model parameters like structure, training data, etc.
+- `model.pth`: The serialized weights of the trained model.
+- `normalization.json`: Values for data normalization to ensure consistent input/output scaling.
+
+These files are used to manage the model, its saved state, and how incoming data will be normalized before being processed.
+
+## `thought` Function
 
 **Lambda** functions in Python provide a way to create small, anonymous functions. These are defined using the lambda keyword and can have any number of input parameters but only a single expression.
 
@@ -368,7 +532,7 @@ print(x._return_type) # int
 
 **Note** : ***this feature uses the default model.***
 
-### "example" Function
+## `example` Function
 
 The "example" function is designed to enhance the context of a function for a LLM by adding examples to it. This functionality is encapsulated in the `example` function.
 
@@ -376,7 +540,9 @@ The "example" function is designed to enhance the context of a function for a LL
 
 - **Versatile**: The "example" function can be used both inside and outside a function to specify examples.
 - **Save**: The "example" function provides a tool called `save_examples` that can store all the examples added to a specified function in a ***JSONL*** file.
-- **Load**: The function also offers a tool called `load_examples` to load a ***JSONL*** file into the context of the function.
+- **Load**: The function also offers a tool called `load_training_example` to load a file into the context of the function especially for `predict function`. 
+
+***Notes : `load_examples` can load `csv`, `json` or `jsonl` file for the moment*** 
 
 Here's how it works: 
 
@@ -396,12 +562,12 @@ example(text="Hello World !", language="japanese", hosta_out="こんにちは世
 print(translate("Hello World !", "French"))
 ```
 
-The "example" function will verify the correlation between the specified input and the parameters of the function. The output should be specified only in the *hosta_out* parameter. If the example are used outside a function, please use the *hosta_func* parameter to specify a function.
+The `example` function will verify the correlation between the specified input and the parameters of the function. The output should be specified only in the *hosta_out* parameter. If the example are used outside a function, please use the *hosta_func* parameter to specify a function.
 
-Now here's how works `save_examples` and `load_examples`
+Now here's how works `save_examples` and `load_training_example`
 
 ```python
-from OpenHosta import save_examples, load_examples
+from OpenHosta import save_examples, load_training_example
 
 save_examples(hosta_func=translate, hosta_path="translate_func_example")
 
@@ -413,8 +579,7 @@ def another_translate(text:str, language:str)->str:
     """
     return emulate()
 
-load_examples(hosta_path="translate_func_example.jsonl", hosta_func=another_translate)
-# add the jsonl at the end of the path !
+load_training_example(hosta_path="translate_func_example.jsonl", hosta_func=another_translate)
 ```
 
 output of the `translate_func_example.jsonl`
diff --git a/pyproject.toml b/pyproject.toml
index 18f4a3b..ca5f911 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "OpenHosta"
-version = "1.1.2"
+version = "1.2.0
 description = "Open-Source programming project IA integretion in developement environnement"
 keywords = ["AI", "GPT", "Natural language", "Autommatic", "Easy"]
 authors = [
@@ -30,7 +30,9 @@ dependencies = [
   "pydantic>=2.8.2",
   "tiktoken>=0.7.0",
   "jsonschema>=4.23.0",
-  "typing-extensions>=4.12.2"
+  "typing-extensions>=4.12.2",
+  "numpy>=2.1.1",
+  "torch>=2.3.1"
 ]
 
 [project.urls]
diff --git a/src/OpenHosta/OpenHosta.py b/src/OpenHosta/OpenHosta.py
index 259c989..762efae 100644
--- a/src/OpenHosta/OpenHosta.py
+++ b/src/OpenHosta/OpenHosta.py
@@ -5,23 +5,28 @@
 )
 
 from .emulate import _exec_emulate
+from .predict import _exec_predict
+from .trainset import TrainingSet
 from . import config
 from .thought import thought
 from .exec import HostaInjector
-from .example import example, load_examples, save_examples
+from .example import example, save_examples, load_training_example
 from .enhancer import suggest
 
 emulate = HostaInjector(_exec_emulate)
+predict = HostaInjector(_exec_predict)
 
 __all__ = (
     "emulate",
     "thought",
     "example", 
-    "load_examples", 
     "save_examples",
+    "load_training_example",
+    "TrainingSet",
     "config", 
     "Model", 
     "DefaultManager",
-    "suggest"
+    "suggest",
+    "predict"
 )
 
diff --git a/src/OpenHosta/__init__.py b/src/OpenHosta/__init__.py
index 56ad7c5..44e0ee6 100644
--- a/src/OpenHosta/__init__.py
+++ b/src/OpenHosta/__init__.py
@@ -1,3 +1,3 @@
 from .OpenHosta import *
 
-__version__ = "1.1.2"
\ No newline at end of file
+__version__ = "1.2.0"
diff --git a/src/OpenHosta/build.py b/src/OpenHosta/build.py
new file mode 100644
index 0000000..9b56069
--- /dev/null
+++ b/src/OpenHosta/build.py
@@ -0,0 +1,10 @@
+from .config import Model, DefaultManager
+
+def _exec_build(
+        _function_infos : dict = None,
+        _function_obj: object = None,
+        model: Model = None,
+
+):
+    pass
+    
diff --git a/src/OpenHosta/builder.py b/src/OpenHosta/builder.py
new file mode 100644
index 0000000..304e9dd
--- /dev/null
+++ b/src/OpenHosta/builder.py
@@ -0,0 +1,57 @@
+import json
+import os
+import torch
+
+from .model import CustomLinearModel
+
+class Builder():
+    def __init__(self, hidden_dir):
+
+        self.hidden_dir = hidden_dir
+
+
+    def build(self, len_input, len_output, complexity, config, optimizer, loss):
+        assert len_input > 0, "Input size must be greater than 0"
+        assert len_output > 0, "Output size must be greater than 0"
+
+        if complexity == None:
+            complexity = 5
+        if optimizer != None:
+            print("\033[93mWarning: The change of optimizer is not available for now, AdamW is actually used.\033[0m")
+            optimizer = "AdamW"
+        if loss != None:
+            print("\033[93mWarning: The change of loss are not available for now, Smooth1Loss is actually used.\033[0m")
+            loss = "SmoothL1Loss"
+
+        if config == None:
+            config = {
+                "architecture": "LinearRegression",
+                "input_size": len_input,
+                "hidden_size_1": len_input * (2 * complexity),
+                "hidden_size_2": len_input * (4 * complexity),
+                "hidden_size_3": len_input * (2 * complexity),
+                "output_size": len_output,
+                "optimizer": optimizer,
+                "loss": loss
+            }
+
+        config_json = json.dumps(config)        
+        config_path = os.path.join(self.hidden_dir, "config.json")
+        
+        with open(config_path, "w") as f:
+            f.write(config_json)
+        return config["architecture"]
+
+    def load_inference(self, config_path, weight_path, inference):
+        with open(config_path, "r") as file:
+            config = json.load(file)
+  
+        model = CustomLinearModel(config, self.hidden_dir)
+        model.load_state_dict(torch.load(weight_path, weights_only=True))
+        output = model.forward(inference)
+        return output
+
+    def trains(self, config, train, val, epochs, verbose, get_loss, continue_training):
+        
+        model = CustomLinearModel(config, self.hidden_dir)
+        model.train(train, val, epochs, self.hidden_dir, verbose, get_loss, continue_training)
diff --git a/src/OpenHosta/cache.py b/src/OpenHosta/cache.py
index a0de4a6..23896c5 100644
--- a/src/OpenHosta/cache.py
+++ b/src/OpenHosta/cache.py
@@ -13,7 +13,7 @@
 
 
 class Hostacache:
-    def __init__(self, func, cache_id, value) -> None:
+    def __init__(self, func, cache_id=None, value=None) -> None:
         self.func = func
         self.cache_id = cache_id
         self.value = value
@@ -23,64 +23,74 @@ def __init__(self, func, cache_id, value) -> None:
             "return_type": "",
             "return_caller": "",
             "function_call": "",
+            "function_args": {},
             "function_locals": {},
             "ho_example": [],
             "ho_example_id": 0,
+            "ho_example_links": [],
             "ho_cothougt": [],
             "ho_cothougt_id": 0,
+            "ho_data": [],
+            "ho_data_id": 0,
         }
 
-    def __call__(self):
+    def create_hosta_cache(self):
         func_name = self.func.__name__
         path_name = os.path.join(CACHE_DIR, f"{func_name}.openhc")
 
+        if self.cache_id is None:
+            if os.path.exists(path_name):
+                with open(path_name, "rb") as f:
+                    cached_data = pickle.load(f)
+                    return cached_data
+            else:
+                return self._parse_and_create_cache_file(path_name)
+
         if os.path.exists(path_name):
             with open(path_name, "rb") as f:
                 cached_data = pickle.load(f)
             assert self.cache_id in cached_data, "Cache ID not found in cache file"
-
-            if self._is_value_already_in_example(self.value, cached_data) == False:
-                cached_data[str(self.cache_id)].append(self.value)
-                cached_data[f"{str(self.cache_id)}" + "_id"] = self._get_hashFunction(
-                    str(cached_data[str(self.cache_id)]), 0, 0
-                )
-                cached_data["hash_function"] = self._get_hashFunction(
-                    cached_data["function_def"],
-                    cached_data["ho_example_id"],
-                    cached_data["ho_cothougt_id"],
-                )
-                with open(path_name, "wb") as f:
-                    pickle.dump(cached_data, f)
-            return
+            if self.value is not None:
+                if not self._is_value_already_in_example(self.value, cached_data):
+                    cached_data[str(self.cache_id)].append(self.value)
+                    cached_data[f"{str(self.cache_id)}_id"] = self._get_hashFunction(
+                        str(cached_data[str(self.cache_id)]), 0, 0
+                    )
+                    cached_data["hash_function"] = self._get_hashFunction(
+                        cached_data["function_def"],
+                        cached_data["ho_example_id"],
+                        cached_data["ho_cothougt_id"],
+                    )
+                    with open(path_name, "wb") as f:
+                        pickle.dump(cached_data, f)
+
+            return cached_data
+
+        return self._parse_and_create_cache_file(path_name)
+
+    def _parse_and_create_cache_file(self, path_name):
+        """ When cache_id is None or cache doesn't exist, create a cache just for function metadata """
         hosta_args = self._get_argsFunction(self.func)
         with open(path_name, "wb") as f:
             pickle.dump(hosta_args, f)
-        return
-
-    def _is_value_already_in_example(self, value, cached_data):
-        for item in cached_data["ho_example"]:
-            if isinstance(item, dict):
-                if item == value:
-                    return True
-            elif isinstance(item, list):
-                for sub_item in item:
-                    if sub_item == value:
-                        return True
-        return False
-
-    def _get_hashFunction(self, func_def: str, nb_example: int, nb_thought: int) -> str:
-        combined = f"{func_def}{nb_example}{nb_thought}"
-        return hashlib.md5(combined.encode()).hexdigest()
+        return hosta_args
 
     def _get_argsFunction(self, func_obj):
         self.infos_cache["function_def"], func_prot = self._get_functionDef(func_obj)
         self.infos_cache["return_type"], self.infos_cache["return_caller"] = (
             self._get_functionReturnType(func_obj)
         )
-        self.infos_cache[self.cache_id].append(self.value)
-        self.infos_cache[f"{str(self.cache_id)}" + "_id"] = self._get_hashFunction(
-            str(self.infos_cache[str(self.cache_id)]), 0, 0
-        )
+
+        if self.cache_id is not None and self.value is not None:
+            if self.cache_id in self.infos_cache:
+                self.infos_cache[self.cache_id].append(self.value)
+            else:
+                self.infos_cache[self.cache_id] = [self.value]
+
+            self.infos_cache[f"{self.cache_id}_id"] = self._get_hashFunction(
+                str(self.infos_cache[self.cache_id]), 0, 0
+            )
+
         self.infos_cache["hash_function"] = self._get_hashFunction(
             self.infos_cache["function_def"],
             self.infos_cache["ho_example_id"],
@@ -88,6 +98,29 @@ def _get_argsFunction(self, func_obj):
         )
         return self.infos_cache
 
+    def _is_value_already_in_example(self, value, cached_data):
+        if self.cache_id not in cached_data:
+            print("Cache ID not found in cache file")
+            return False
+
+        def recursive_check(item, value):
+            if isinstance(item, dict):
+                if item == value or any(recursive_check(v, value) for v in item.values()):
+                    return True
+            elif isinstance(item, list):
+                return any(recursive_check(sub_item, value) for sub_item in item)
+            else:
+                return item == value
+
+        for item in cached_data[self.cache_id]:
+            if recursive_check(item, value):
+                return True
+        return False
+
+    def _get_hashFunction(self, func_def: str, nb_example: int, nb_thought: int) -> str:
+        combined = f"{func_def}{nb_example}{nb_thought}"
+        return hashlib.md5(combined.encode()).hexdigest()
+
     def _get_functionDef(self, func: Callable) -> str:
         sig = inspect.signature(func)
 
@@ -107,7 +140,10 @@ def _get_functionDef(self, func: Callable) -> str:
             if sig.return_annotation != inspect.Signature.empty
             else ""
         )
-        definition = f"def {func_name}({func_params}):{func_return}\n    '''\n    {func.__doc__}\n    '''"
+        definition = (
+            f"```python\ndef {func_name}({func_params}):{func_return}\n"
+            f"    \"\"\"\n\t{func.__doc__}\n    \"\"\"\n```"
+        )
         prototype = f"def {func_name}({func_params}):{func_return}"
         return definition, prototype
 
@@ -161,5 +197,4 @@ def _get_functionReturnType(self, func: Callable) -> Dict[str, Any]:
                 "Hosta_return_shema", return_hosta_type_any=(Any, ...)
             )
             return_type = No_return_specified.model_json_schema()
-
-        return return_type, return_caller
+        return return_type, return_caller
\ No newline at end of file
diff --git a/src/OpenHosta/config.py b/src/OpenHosta/config.py
index 42623e3..2546623 100644
--- a/src/OpenHosta/config.py
+++ b/src/OpenHosta/config.py
@@ -42,6 +42,7 @@ def __init__(self, model: str = None, base_url: str = None, api_key: str = None)
             frozenset: lambda x: frozenset(x),
             tuple: lambda x: tuple(x),
             bool: lambda x: bool(x),
+            type(None): lambda x: None,
         }
 
         if any(var is None for var in (model, base_url)):
@@ -85,7 +86,6 @@ def api_call(
             "Content-Type": "application/json",
             "Authorization": f"Bearer {self.api_key}",
         }
-
         self._last_request = l_body        
 
         try:
@@ -101,7 +101,7 @@ def api_call(
         return response
 
     def request_handler(self, response, return_type, return_caller):
-        l_ret = ""
+        l_ret = None
 
         data = response.json()
         json_string = data["choices"][0]["message"]["content"]
@@ -122,7 +122,8 @@ def request_handler(self, response, return_type, return_caller):
         if "return_hosta_type" in return_type["properties"]:
             if return_caller in self.conversion_function:
                 convert_function = self.conversion_function[return_caller]
-                l_ret = convert_function(l_ret_data["return"])
+                if l_ret_data["return"] is not None:
+                    l_ret = convert_function(l_ret_data["return"])
             else:
                 l_ret = l_ret_data["return"]
 
@@ -140,7 +141,7 @@ def request_handler(self, response, return_type, return_caller):
                 for m in self.__last_request["messages"]:
                     sys.stderr.write(" "+m["role"]+">\n=======\n", m["content"][0]["text"])
                 sys.stderr.write("Answer>\n=======\n",  l_ret_data["return"])
-                lret = None
+                l_ret = None
 
         else:
             l_ret = l_ret_data["return"]
diff --git a/src/OpenHosta/datapreparator.py b/src/OpenHosta/datapreparator.py
new file mode 100644
index 0000000..ad21f6d
--- /dev/null
+++ b/src/OpenHosta/datapreparator.py
@@ -0,0 +1,241 @@
+import torch
+from torch.utils.data import DataLoader
+import os
+import json
+import csv
+import numpy as np
+
+from .encoder import HostaEncoder
+from .decoder import HostaDecoder
+
+class Datapreparator():
+    def __init__(self, norm_max, norm_min, encoder=None, decoder=None):
+        self.encoder = encoder if encoder else HostaEncoder()
+        self.decoder = decoder if decoder else HostaDecoder()
+
+        if norm_min:
+            self.norm_min = norm_min
+        else:
+            self.norm_min = 0.1
+        if norm_max:
+            self.norm_max = norm_max
+        else:
+            self.norm_max = 1.0
+    
+        self.data_min_nonzero = None
+        self.data_max = None
+        self.data_min = None
+        self.data_range = None
+
+        self.prediction_min_nonzero = None
+        self.prediction_max = None
+        self.prediction_min = None
+        self.prediction_range = None
+
+    def prepare_input(self, in_value):
+        input_data = []
+        for key, value in in_value.items():
+            if isinstance(value, dict):
+                for sub_key, sub_value in value.items():
+                    parsed_value = self.encoder.encode(sub_value)
+                    input_data.extend(parsed_value)
+            elif isinstance(value, list):
+                for item in value:
+                    parsed_value = self.encoder.encode(item)
+                    input_data.extend(parsed_value)
+            else:
+                parsed_value = self.encoder.encode(value)
+                input_data.extend(parsed_value)
+        return input_data
+
+    def prepare(self, function_infos, prediction):
+        train = []
+        val = []
+        if function_infos["ho_example"] == [] and function_infos["ho_data"] == []:
+            raise ValueError("No example provided please provide at least one example for the model")
+
+        if function_infos["ho_data"] != []:
+            for example in function_infos["ho_data"]:
+                value = self.parse_dict(example, prediction)
+                train.extend(value)
+            if function_infos["ho_example"] != []:
+                for example in function_infos["ho_example"]:
+                    value = self.parse_dict(example, prediction)
+                    val.extend(value)
+        else:
+            for example in function_infos["ho_example"]:
+                value = self.parse_dict(example, prediction)
+                train.extend(value)
+        return train, val
+
+    def normalize_dataset(self, train, val):
+        dataset = train + val if val != [] else train
+        data_values = [example[0] for example in dataset]
+        prediction_values = [example[1] for example in dataset]
+
+        data_array = np.array(data_values)
+        prediction_array = np.array(prediction_values)
+
+        negative_data = np.any(data_array < 0, axis=0)
+        negative_prediction = np.any(prediction_array < 0, axis=0)
+
+        self.data_min_nonzero = np.array([
+            np.min(data_array[:, i][data_array[:, i] > 0]) if not negative_data[i] and np.any(data_array[:, i] > 0) else 0
+            for i in range(data_array.shape[1])])
+        self.data_max = data_array.max(axis=0)
+        self.data_min = data_array.min(axis=0)
+
+        self.prediction_min_nonzero = np.array([
+            np.min(prediction_array[:, i][prediction_array[:, i] > 0]) if not negative_prediction[i] and np.any(prediction_array[:, i] > 0) else 0
+            for i in range(prediction_array.shape[1])])
+        self.prediction_max = prediction_array.max(axis=0)
+        self.prediction_min = prediction_array.min(axis=0)
+
+        self.data_range = self.data_max - self.data_min_nonzero
+        self.data_range[self.data_range == 0] = 1 
+
+        self.prediction_range = self.prediction_max - self.prediction_min_nonzero
+        self.prediction_range[self.prediction_range == 0] = 1
+
+        normalized_data = np.zeros_like(data_array)
+        for i in range(data_array.shape[1]):
+            zero_mask = data_array[:, i] == 0
+            normalized_data[:, i] = np.where(zero_mask, 0.0, self.norm_min + ((data_array[:, i] - self.data_min_nonzero[i]) / self.data_range[i]) * (self.norm_max - self.norm_min))
+
+        normalized_prediction = np.zeros_like(prediction_array)
+        for i in range(prediction_array.shape[1]):
+            zero_mask = prediction_array[:, i] == 0
+            normalized_prediction[:, i] = np.where(zero_mask, 0.0, self.norm_min + ((prediction_array[:, i] - self.prediction_min_nonzero[i]) / self.prediction_range[i]) * (self.norm_max - self.norm_min))
+
+        # Maybe unwrap the tolist, stay for now because only work after with list 
+        normalized_dataset = list(zip(normalized_data.tolist(), normalized_prediction.tolist()))
+        train = normalized_dataset[:len(train)]
+        val = normalized_dataset[len(train):] if val else None
+        return train, val
+
+    def normalize_inference(self, inference_data):
+        inference_data = np.array(inference_data)
+
+        normalized_inference = np.zeros_like(inference_data)
+        for i in range(len(inference_data)):
+            if inference_data[i] == 0:
+                normalized_inference[i] = 0.0
+            else:
+                normalized_inference[i] = self.norm_min + ((inference_data[i] - self.data_min_nonzero[i]) / self.data_range[i]) * (self.norm_max - self.norm_min)
+            
+        return normalized_inference.tolist()
+    
+    def denormalize_prediction(self, prediction):
+        prediction = prediction.detach().cpu().numpy()
+
+        denormalized_prediction = np.zeros_like(prediction)
+        for i in range(len(prediction)):
+            if prediction[i] == 0:
+                denormalized_prediction[i] = 0.0
+            else:
+                denormalized_prediction[i] = self.prediction_min_nonzero[i] + ((prediction[i] - self.norm_min) / (self.norm_max - self.norm_min)) * self.prediction_range[i]
+    
+        return denormalized_prediction.tolist()
+
+    def save_normalization_params(self, path):
+        params = {
+            'norm_min': self.norm_min,
+            'norm_max': self.norm_max,
+            'data_min_nonzero': self.data_min_nonzero.tolist(),
+            'data_max': self.data_max.tolist(),
+            'data_min': self.data_min.tolist(),
+            'data_range': self.data_range.tolist(),
+            'prediction_min_nonzero': self.prediction_min_nonzero.tolist(),
+            'prediction_max': self.prediction_max.tolist(),
+            'prediction_min': self.prediction_min.tolist(),
+            'prediction_range': self.prediction_range.tolist()
+        }
+        with open(path, 'w') as f:
+            json.dump(params, f)
+
+    def load_normalization_params(self, path):
+        try:
+            with open(path, 'r') as f:
+                params = json.load(f)
+                self.norm_min = params['norm_min']
+                self.norm_max = params['norm_max']
+                self.data_min_nonzero = np.array(params['data_min_nonzero'])
+                self.data_max = np.array(params['data_max'])
+                self.data_min = np.array(params['data_min'])
+                self.data_range = np.array(params['data_range'])
+                self.prediction_min_nonzero = np.array(params['prediction_min_nonzero'])
+                self.prediction_max = np.array(params['prediction_max'])
+                self.prediction_min = np.array(params['prediction_min'])
+                self.prediction_range = np.array(params['prediction_range'])
+        except Exception as e:
+            raise IOError(f"An error occurred while loading the normalization parameters: {e}")
+
+    def convert(self, inference):
+        return torch.tensor(inference, dtype=torch.float32)
+    
+    def split(self, train_normalization, val_normalization, batch_size):
+        datatensor = []
+
+        for examples in train_normalization:
+            feature_tensor = torch.tensor(examples[0], dtype=torch.float32)
+            label_tensor = torch.tensor(examples[1], dtype=torch.float32)
+
+            tensor = [feature_tensor, label_tensor]
+            datatensor.append(tensor)
+
+        train = DataLoader(datatensor, batch_size=batch_size, shuffle=True)
+
+        if val_normalization:
+            valtensor = []
+            for examples in val_normalization:
+                feature_tensor = torch.tensor(examples[0], dtype=torch.float32)
+                label_tensor = torch.tensor(examples[1], dtype=torch.float32)
+
+                tensor = [feature_tensor, label_tensor]
+                valtensor.append(tensor)
+            val = DataLoader(valtensor, batch_size=batch_size, shuffle=False)
+        else : val = None
+        return train, val
+
+    def parse_dict(self, example, prediction):
+        dataset = []
+        input_data = []
+        output_data = []
+        for key, value in example.items():
+            if key in prediction or key == "hosta_out":
+                parsed_value = self.encoder.encode(value)
+                output_data.extend(parsed_value)
+            else:
+                parsed_value = self.encoder.encode(value)
+                input_data.extend(parsed_value)
+        dataset.append([input_data, output_data])
+        return dataset
+
+def open_file(ho_examples):
+    list_of_examples = []
+    for path in ho_examples:
+        _, file_extension = os.path.splitext(path)
+        try:
+            if file_extension == '.jsonl':
+                with open(path, "r") as file:
+                    for line in file:
+                        example = json.loads(line.strip())
+                        list_of_examples.append(example)
+
+            elif file_extension == '.csv':
+                with open(path, "r", newline='') as file:
+                    csv_reader = csv.DictReader(file)
+                    for row in csv_reader:
+                        list_of_examples.append(row)
+
+            elif file_extension == '.txt':
+                with open(path, "r") as file:
+                    for line in file:
+                        list_of_examples.append(line.strip())
+            
+            else:
+                raise ValueError("Unsupported file type. Please provide a JSONL, CSV, or TXT file.")
+
+        except Exception as e:
+            raise IOError(f"An error occurred while processing the file: {e}")
+    return list_of_examples
diff --git a/src/OpenHosta/decoder.py b/src/OpenHosta/decoder.py
new file mode 100644
index 0000000..c76aadb
--- /dev/null
+++ b/src/OpenHosta/decoder.py
@@ -0,0 +1,3 @@
+class HostaDecoder():
+    def __init__(self) -> None:
+        pass
diff --git a/src/OpenHosta/encoder.py b/src/OpenHosta/encoder.py
new file mode 100644
index 0000000..a975ad0
--- /dev/null
+++ b/src/OpenHosta/encoder.py
@@ -0,0 +1,36 @@
+from typing import Any
+
+class HostaEncoder():
+    def __init__(self) -> None:
+        None
+
+    def encode(self, value: Any):
+        if type(value) == int:
+            return IntEncoder.encoder(value)
+        elif type(value) == float:
+            return FloatEncoder.encoder(value)
+        elif type(value) == str:
+            try:
+                convert_type = FloatEncoder.encoder(value)
+                return convert_type
+            except:
+                raise ValueError("String cannot be converted to float (numbers in string only supported for now)")
+        else:
+            raise ValueError("Type not supported")
+
+
+class IntEncoder(HostaEncoder):
+    def __init__(self) -> None:
+        super().__init__()
+
+    def encoder(data):
+        data_encode = int(data)
+        return [data_encode]
+
+class FloatEncoder(HostaEncoder):
+    def __init__(self) -> None:
+        super().__init__()
+
+    def encoder(data):
+        data_encode = float(data)
+        return [data_encode]
diff --git a/src/OpenHosta/enhancer.py b/src/OpenHosta/enhancer.py
index 7018163..08a3cb8 100644
--- a/src/OpenHosta/enhancer.py
+++ b/src/OpenHosta/enhancer.py
@@ -68,7 +68,6 @@ def _build_attributes(func: object, last_enh) -> int:
         func.review = last_enh["review"]
         func.advanced = last_enh["advanced"]
         func.diagram = last_enh["mermaid"]
-        func.diagramm = last_enh["mermaid"]
     return 0
 
 
diff --git a/src/OpenHosta/example.py b/src/OpenHosta/example.py
index 6c13b87..4a75954 100644
--- a/src/OpenHosta/example.py
+++ b/src/OpenHosta/example.py
@@ -2,7 +2,10 @@
 import pickle
 import os
 import json
+import csv
+from typing import Callable
 
+from .errors import FrameError
 from .cache import Hostacache
 
 CACHE_DIR = "__hostacache__"
@@ -16,16 +19,13 @@ def example(*args, hosta_func=None, hosta_out=None, **kwargs):
 
     if hosta_func is None:
         try:
-            func_frame = inspect.currentframe().f_back
-            func_name = func_frame.f_code.co_name
-            func = func_frame.f_globals[func_name]
+            func, _ = _extend_scope()
         except:
-            raise ValueError(f"Please provide hosta_func for specifying the function")
-
+            raise ValueError("Please provide hosta_func for specifying the function")
     elif callable(hosta_func):
         func = hosta_func
     else:
-        raise ValueError(f"Please provide hosta_func for specifying the function")
+        raise ValueError("Please provide hosta_func for specifying the function")
 
     try:
         sig = inspect.signature(func)
@@ -33,60 +33,73 @@ def example(*args, hosta_func=None, hosta_out=None, **kwargs):
             input_type[param.name] = param.annotation
         output_type["hosta_out"] = sig.return_annotation
     except:
-        raise ValueError(f"Function does not have signature")
-    if args != ():
+        raise ValueError("Function does not have a signature")
+
+    type_verificator(args, kwargs, input_type, output_type, hosta_out, func, example_dict)
+
+    cache_id = "ho_example"
+    cache = Hostacache(func, cache_id, example_dict)
+    cache.create_hosta_cache()
+
+
+def type_verificator(args, kwargs, input_type, output_type, hosta_out, func, example_dict):
+    """ 
+    Validates the types of both positional and keyword arguments, as well as the return value.
+    """
+    
+    if args:
         if len(args) != len(input_type):
             raise ValueError(
-                f"Too many arguments for function {func.__name__}, please provide {len(input_type)} arguments, use hosta_out for output"
+                f"Too many arguments for function {func.__name__}, "
+                f"expected {len(input_type)} arguments, use hosta_out for output."
             )
 
         for i, arg in enumerate(args):
             param_name = list(input_type.keys())[i]
-
             expected_type = input_type[param_name]
+            
             if not isinstance(arg, expected_type):
-                raise ValueError(
-                    f"Argument {arg} does NOT match the expected type {expected_type} for parameter {param_name}. For function {func.__name__}"
+                raise TypeError(
+                    f"Argument {arg} does NOT match the expected type "
+                    f"{expected_type} for parameter {param_name} in function {func.__name__}."
                 )
             example_dict[param_name] = arg
 
     else:
         if len(kwargs) != len(input_type):
             raise ValueError(
-                f"Too many arguments for function {func.__name__}, please provide {len(input_type)} arguments, use hosta_out for output"
+                f"Mismatch in number of keyword arguments for function '{func.__name__}', "
+                f"expected {len(input_type)} arguments, use hosta_out for output."
             )
 
         for key, value in kwargs.items():
             expected_type = input_type[key]
+            
             if not isinstance(value, expected_type):
-                raise ValueError(
-                    f"Argument {value} does NOT match the expected type {expected_type} for parameter {key}. For function {func.__name__}"
+                raise TypeError(
+                    f"Keyword argument {value} does NOT match the expected type "
+                    f"{expected_type} for parameter {key} in function {func.__name__}."
                 )
             example_dict[key] = value
 
     if hosta_out is None:
-        raise ValueError(f"Please provide hosta_out for output")
+        raise ValueError("Please provide hosta_out for output.")
     else:
-        expected_type = output_type["hosta_out"]
-        if not isinstance(hosta_out, expected_type):
-            raise ValueError(
-                f"Output {hosta_out} does NOT match the expected type {expected_type}. For function {func.__name__}"
+        expected_output_type = output_type["hosta_out"]
+        if not isinstance(hosta_out, expected_output_type):
+            raise TypeError(
+                f"Output {hosta_out} does NOT match the expected type "
+                f"{expected_output_type} for function {func.__name__}."
             )
         example_dict["hosta_out"] = hosta_out
 
-    cache_id = "ho_example"
-    cache = Hostacache(func, cache_id, example_dict)
-    cache()
-
 
 def save_examples(hosta_func=None, hosta_path=None):
     cached_data = {}
 
     if hosta_func is None:
         try:
-            func_frame = inspect.currentframe().f_back
-            func_name = func_frame.f_code.co_name
-            func = func_frame.f_globals[func_name]
+            func, _ = _extend_scope()
         except:
             raise ValueError(f"Please provide hosta_func for specifying the function")
 
@@ -104,49 +117,140 @@ def save_examples(hosta_func=None, hosta_path=None):
     func_name = func.__name__
     path_name = os.path.join(CACHE_DIR, f"{func_name}.openhc")
 
+
     try:
         if os.path.exists(path_name):
             with open(path_name, "rb") as f:
                 cached_data = pickle.load(f)
-                with open(total_path, "w") as t:
+                with open(total_path, "a") as t:
                     for dict in cached_data["ho_example"]:
                         t.write(json.dumps(dict) + "\n")
+                        t.write(json.dumps(dict) + "\n")
         else:
             raise ValueError(f"Could not found the cache at {path_name}")
     except Exception as e:
         raise ValueError(f"Could not found the cache at {path_name}") from e
 
 
-def load_examples(hosta_func=None, hosta_path=None):
-    if hosta_func is None:
+def load_training_example(hosta_path: str, hosta_func: callable) -> dict:
+    """
+    Load the training example from the cache.
+    """
+    func_name = hosta_func.__name__
+    path_name = os.path.join(CACHE_DIR, f"{func_name}.openhc")
+
+    cached_data = None
+
+    if os.path.exists(path_name):
         try:
-            func_frame = inspect.currentframe().f_back
-            func_name = func_frame.f_code.co_name
-            func = func_frame.f_globals[func_name]
-        except:
-            raise ValueError(f"Please provide hosta_func for specifying the function")
+            with open(path_name, "rb") as f:
+                cached_data = pickle.load(f)
+        except (pickle.PickleError, IOError) as e:
+            raise ValueError(f"Error loading cache from {path_name}") from e
+    else:
+        cache = Hostacache(hosta_func, None)
+        cache.create_hosta_cache()
+        with open(path_name, "rb") as f:
+            cached_data = pickle.load(f)
 
-    elif callable(hosta_func):
-        func = hosta_func
+    _, file_extension = os.path.splitext(hosta_path)
+    if file_extension not in ['.json', '.jsonl', '.csv']:
+        raise ValueError("Unsupported file type. Please provide a JSON or JSONL or CSV file.")
+
+    try:
+        with open(hosta_path, 'r') as file:
+            if file_extension == '.json':
+                data = json.load(file)
+                if isinstance(data, list):
+                    for item in data:
+                        if item not in cached_data['ho_data']:
+                            cached_data['ho_data'].append(item)
+                else:
+                    if data not in cached_data['ho_data']:
+                        cached_data['ho_data'].append(data)
+            elif file_extension == '.jsonl':
+                for line in file:
+                    item = json.loads(line)
+                    if item not in cached_data['ho_data']:
+                        cached_data['ho_data'].append(item)
+            elif file_extension == '.csv':
+                reader = csv.DictReader(file)
+                for row in reader:
+                    if row not in cached_data['ho_data']:
+                        cached_data['ho_data'].append(row)
+        with open(path_name, "wb") as f:
+            pickle.dump(cached_data, f)
+    except (IOError, json.JSONDecodeError) as e:
+        raise ValueError(f"Error loading data from {hosta_path}") from e
+    return cached_data
+
+
+def _extend_scope() -> Callable:
+    func: Callable = None
+    current = None
+    step = None
+    caller = None
+
+    current = inspect.currentframe()
+    if current is None:
+        raise FrameError("Current frame is None")
+    step = current.f_back
+    if step is None:
+        raise FrameError("Caller[lvl1] frame is None")
+    caller = step.f_back
+    if caller is None:
+        raise FrameError("Caller[lvl2] frame is None")
+
+    caller_name = caller.f_code.co_name
+    caller_code = caller.f_code
+    l_caller = caller
+    
+    if "self" in caller.f_locals:
+        obj = caller.f_locals["self"]
+        func = getattr(obj, caller_name, None)
+        if func:
+            func = inspect.unwrap(func)
     else:
-        raise ValueError(f"Please provide hosta_func for specifying the function")
+        while func is None and l_caller.f_back is not None:
+            for obj in l_caller.f_back.f_locals.values():
+                found = False
+                try:
+                    if hasattr(obj, "__code__"):
+                        found = True
+                except:
+                    continue       
+                if found and obj.__code__ == caller_code:
+                    func = obj
+                    break
+            if func is None:
+                l_caller = l_caller.f_back
+        if func is None:
+            func = caller.f_globals.get(caller_name)
+            if func:
+                func = inspect.unwrap(func)
+    
+    if func is None or not callable(func):
+        raise FrameError("The emulated function cannot be found.")
 
-    if hosta_path is None:
-        raise ValueError(
-            f"Please provide hosta_path for specifying the path to load the cache"
-        )
+    return func, caller
 
-    list_of_examples = []
 
-    try:
-        with open(hosta_path, "r") as file:
-            for line in file:
-                hosta_example = json.loads(line.strip())
-                list_of_examples.append(hosta_example)
-    except Exception:
-            raise IOError("Please provide a Json or a JsonL file only.")
+EXAMPLE_DOC = """
+A utility function that performs runtime type validation on a given function's arguments and output.
 
-    cache_id = "ho_example"
-    value = list_of_examples
-    cache = Hostacache(func, cache_id, value)
-    cache()
+Parameters:
+    *args: 
+        Positional arguments to validate against the input types of the provided function (hosta_func).
+    **kwargs: 
+        Keyword arguments (passed by name) to validate against the input types of the provided function.
+    hosta_func (function, optional but recommended): 
+        The function whose signature will be used for input/output type validation.
+    hosta_out (object): 
+        The expected output of hosta_func, to be validated against the return type annotation.
+
+Raises:
+    ValueError: 
+        If the number of arguments provided does not match the expected number as per the function's signature.
+    TypeError: 
+        If the type of any argument or output does not match the expected type.
+"""
\ No newline at end of file
diff --git a/src/OpenHosta/exec.py b/src/OpenHosta/exec.py
index c057a83..bbb4bf3 100644
--- a/src/OpenHosta/exec.py
+++ b/src/OpenHosta/exec.py
@@ -8,8 +8,11 @@
 from pydantic import BaseModel, create_model
 import copy
 
+import functools
+
 from .enhancer import enhance
 from .errors import FrameError
+from .predict import continue_train, to_emulate, retrain
 
 
 CACHE_DIR = "__hostacache__"
@@ -30,11 +33,15 @@ def __call__(self, *args, **kwargs):
             "return_type": "",
             "return_caller": "",
             "function_call": "",
+            "function_args": {},
             "function_locals": {},
             "ho_example": [],
             "ho_example_id": 0,
+            "ho_example_links": [],
             "ho_cothougt": [],
             "ho_cothougt_id": 0,
+            "ho_data": [],
+            "ho_data_id" : 0
         }
         func_obj, caller = self._extend_scope()
         func_name = func_obj.__name__
@@ -51,20 +58,21 @@ def __call__(self, *args, **kwargs):
                 cached_data["ho_cothougt_id"],
             )
 
+            self._attach_attributs(func_obj, func_prot)
             if function_hash == cached_data["hash_function"]:
-                cached_data["function_call"], cached_data["function_locals"] = (
+                cached_data["function_call"], cached_data["function_locals"], cached_data["function_args"] = (
                     self._get_functionCall(func_obj, caller)
                 )
-                self._attach_attributs(func_obj, func_prot)
                 return self.exec(cached_data, func_obj, *args, **kwargs)
 
         hosta_args = self._get_argsFunction(func_obj)
         with open(path_name, "wb") as f:
             res = pickle.dump(hosta_args, f)
         # TODO : fix the function locals because he didn't load in the cache
-        hosta_args["function_call"], hosta_args["function_locals"] = (
+        hosta_args["function_call"], hosta_args["function_locals"], hosta_args["function_args"] = (
             self._get_functionCall(func_obj, caller)
         )
+        self._attach_attributs(func_obj, hosta_args["function_def"])
         return self.exec(hosta_args, func_obj, *args, **kwargs)
 
     def _get_hashFunction(self, func_def: str, nb_example: int, nb_thought: int) -> str:
@@ -82,7 +90,6 @@ def _get_argsFunction(self, func_obj):
             self.infos_cache["ho_example_id"],
             self.infos_cache["ho_cothougt_id"],
         )
-        self._attach_attributs(func_obj, func_prot)
         return self.infos_cache
 
     def _extend_scope(self) -> Callable:
@@ -189,7 +196,7 @@ def _get_functionCall(self, func: Callable, caller) -> str:
         )
 
         call = f"{func.__name__}({args_str})"
-        return call, locals
+        return call, locals, values_args
 
     def _inspect_returnType(self, func: Callable) -> str:
         sig = inspect.signature(func)
@@ -245,7 +252,21 @@ def _get_functionReturnType(self, func: Callable) -> Dict[str, Any]:
 
         return return_type, return_caller
 
-    def _attach_attributs(self, func: Callable, prototype: str):
+    def _attach_attributs(self, func: Callable, prototype: str)->None:
+        """
+        Attach additional attributes to a function.
+
+        Args:
+            func (Callable): The target function to which the attributes are attached.
+            prototype (str): A string representing the prototype (used as an example).
+
+        Returns:
+            Callable: The target function wrapped with the attached attributes.
+        """
         if "bound method" not in str(func):
             setattr(func, "__suggest__", enhance)
-            setattr(func, "_prot", prototype)
+            setattr(func, "_prot", prototype) 
+            setattr(func, "continue_train", functools.partial(continue_train, func_obj=func))
+            setattr(func, "retrain", functools.partial(retrain, func_obj=func))
+            setattr(func, "emulate", functools.partial(to_emulate, func_obj=func))
+
diff --git a/src/OpenHosta/model.py b/src/OpenHosta/model.py
new file mode 100644
index 0000000..f49acde
--- /dev/null
+++ b/src/OpenHosta/model.py
@@ -0,0 +1,102 @@
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+import time
+import json
+
+class CustomLinearModel(nn.Module):
+
+    def __init__(self, config, hidden_dir):
+        super().__init__()
+        self.hidden_dir = hidden_dir
+        self.path = hidden_dir+"/config.json"
+        if config == None:
+            try:
+                with open(self.path, 'r') as f:
+                    self.config = json.load(f)
+            except Exception as e:
+                raise Exception("Config file not found please check the path : ", self.path)
+        else:
+            self.config = config
+
+        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+        self.create_model(self.config)
+
+        self.loss = nn.SmoothL1Loss()
+        self.optimizer = torch.optim.AdamW(self.parameters(), lr=0.001)
+        self.to(self.device)
+
+    def create_model(self, config):
+
+        input_size = config["input_size"]
+        output_size = config["output_size"]
+
+        hidden_sizes = []
+        for key in config:
+            if key.startswith("hidden_size_"):
+                layer_num_str = key.split("_")[-1]
+                if layer_num_str.isdigit():
+                    layer_num = int(layer_num_str)
+                    hidden_sizes.append((layer_num, config[key]))
+        hidden_sizes.sort(key=lambda x: x[0])
+
+        layer_sizes = [input_size] + [size for _, size in hidden_sizes] + [output_size]
+
+        for idx in range(len(layer_sizes) - 1):
+            in_features = layer_sizes[idx]
+            out_features = layer_sizes[idx + 1]
+            self.add_module(f"fc{idx + 1}", nn.Linear(in_features, out_features, dtype=torch.float32))
+
+        return 
+
+    def forward(self, x):
+        x = x.to(self.device)
+        num_layers = len(self.config) - 4
+        for idx in range(1, num_layers):
+            layer = getattr(self, f"fc{idx}")
+            x = F.relu(layer(x))
+
+        layer = getattr(self, f"fc{num_layers}")
+        x = layer(x)
+        return x
+    
+
+    def train(self, train, val,  epochs, path, verbose=False, get_loss=None, continue_training=False):
+        get_loss=0.0 if get_loss is None else get_loss
+        
+        if continue_training:
+            try:
+                self.load_state_dict(torch.load(path+"/model.pth", weights_only=True))
+                if verbose:
+                    print(f"\033[93mModel loaded from {path}/model.pth\033[0m")
+            except Exception as e:
+                raise Exception(f"Model weight not found at {path}/model.pth")
+
+        total_start = time.time()
+
+        for epoch in range(epochs):
+            epoch_start = time.time()
+            for X_train, y_train in train:
+                X_train, y_train = X_train.to(self.device), y_train.to(self.device)
+                self.optimizer.zero_grad()
+                output = self.forward(X_train)
+
+                loss = self.loss(output, y_train)
+                loss.backward()
+                self.optimizer.step()
+            epoch_end = time.time()
+            epoch_time = epoch_end - epoch_start
+            if verbose:
+                print(f"\033[94m{epoch}/{epochs} -> Loss: {loss.item()} in {epoch_time} sec\033[0m", flush=True)
+
+            if loss.item() < get_loss:
+                if verbose:
+                    print(f"\033[93mLoss target achieved at epoch {epoch} with loss {loss.item()} in {epoch_time} sec\033[0m", flush=True)
+                break
+            
+        total_end = time.time()
+        total_time = total_end - total_start
+        if verbose:
+            print(f"\033[92mTraining complete : Loss: {loss.item()} in a total of {total_time} sec\033[0m", flush=True)
+
+        torch.save(self.state_dict(), path+"/model.pth")
diff --git a/src/OpenHosta/predict.py b/src/OpenHosta/predict.py
new file mode 100644
index 0000000..c468a02
--- /dev/null
+++ b/src/OpenHosta/predict.py
@@ -0,0 +1,195 @@
+import os
+
+import pickle
+from .cache import Hostacache
+from .builder import Builder
+from .datapreparator import Datapreparator
+from .example import type_verificator
+from .emulate import _exec_emulate
+
+from typing import Any
+import inspect
+
+CACHE_DIR = "__hostacache__"
+os.makedirs(CACHE_DIR, exist_ok=True)
+
+def _exec_predict(
+        _function_infos: dict = None,
+        _function_obj: object = None,
+
+        encoder = None,
+        decoder = None,
+        verbose: bool = False,
+        prediction: list = [],
+        complexity: int = None,
+        config: dict = None,
+        optimizer: str = None,
+        loss: str = None,
+        epochs: int = None,
+        get_loss: float = 0.0,
+        batch_size: int = None,
+        force_train: bool = False,
+        norm_max: float = None,
+        norm_min: float = None,
+        continue_training: bool = False,
+        normalization: bool = False
+):
+    hidden_dir = os.path.join(CACHE_DIR, f".model_{_function_obj.__name__}_{_function_infos['hash_function']}")
+    os.makedirs(hidden_dir, exist_ok=True)
+
+    config_path = os.path.join(hidden_dir, "config.json")
+    weight_path = os.path.join(hidden_dir, "model.pth")
+    normalisation_path = os.path.join(hidden_dir, "normalisation.json")
+
+    preparator = Datapreparator(norm_max, norm_min, encoder, decoder)
+    builder = Builder(hidden_dir)
+
+    if not os.path.exists(config_path) or not os.path.exists(weight_path) or force_train==True:
+
+        train, val = preparator.prepare(_function_infos, prediction)
+ 
+        if normalization:  
+            train, val = preparator.normalize_dataset(train,val)
+            preparator.save_normalization_params(normalisation_path)
+        len_input = len(train[0][0])
+        len_output = len(train[0][1])
+        builder.build(len_input, len_output, complexity, config, optimizer, loss)
+        if batch_size is None:
+            batch_size = int(0.05 * len(train)) if 0.05 * len(train) > 1 else len(train) # 5% of the dataset or len(train) if len(train)
+        else:
+            batch_size = batch_size
+        save_len = len(train)
+        train, eval = preparator.split(train, val, batch_size)
+        epochs = int(2*save_len / batch_size if batch_size != save_len else 2*save_len) if epochs is None else epochs
+        assert epochs > 0, "epochs must be greater than 0 now it's {epochs}"
+        builder.trains(config, train, eval, epochs=epochs, verbose=verbose, get_loss=get_loss, continue_training=continue_training)
+    else:
+        if verbose:
+            print("\033[93mModel already trained, skipping training\033[0m")
+        if normalization:
+            preparator.load_normalization_params(normalisation_path)
+    if _function_infos["function_args"] != {}:
+        inference = preparator.prepare_input(_function_infos["function_args"])
+        if normalization:
+            inference = preparator.normalize_inference(inference)
+        torch_inference = preparator.convert(inference)
+
+        prediction = builder.load_inference(config_path, weight_path, torch_inference)
+        if normalization:
+            prediction_denormalize = preparator.denormalize_prediction(prediction)
+            result = float(prediction_denormalize[0])
+        else:
+            result = float(prediction.detach().cpu().numpy()[0])
+        return result
+
+
+def continue_train(func_obj, epochs=None, get_loss=None, verbose=False):
+    """
+    Continue the training of the model
+    - Reload a pth and add a dataset or not for the model
+    save a new pth after the training decided in the emulate or not or in this function also (diff parameters
+    of training and not architecture)
+    """
+    infos_cache = load_cache(func_obj)
+    return _exec_predict(_function_infos=infos_cache, _function_obj=func_obj, force_train=True ,continue_training=True, epochs=epochs, get_loss=get_loss, verbose=verbose)
+
+
+def get_input_types_from_signature(func_obj):
+    """
+    Extract input type from function signature
+    """
+    signature = inspect.signature(func_obj)
+    input_type = {}
+    for name, param in signature.parameters.items():
+        if param.annotation != inspect.Parameter.empty:
+            input_type[name] = param.annotation
+        else:
+            input_type[name] = Any
+    return input_type
+
+
+def emulate_verificator(args, kwargs, input_type, func, example_dict):
+    """
+    Vérifie les types des arguments positionnels et nommés lors de l'appel à emulate.
+    Met à jour example_dict avec les valeurs validées.
+    """
+    param_names = list(input_type.keys())
+    
+    total_args_provided = len(args) + len(kwargs)
+    total_args_expected = len(param_names)
+    
+    if total_args_provided != total_args_expected:
+        raise ValueError(
+            f"Incorrect number of arguments for function '{func.__name__}', "
+            f"expected {total_args_expected}, got {total_args_provided}."
+        )
+    
+    for i, arg in enumerate(args):
+        param_name = param_names[i]
+        expected_type = input_type[param_name]
+        
+        if not isinstance(arg, expected_type):
+            raise TypeError(
+                f"Positional argument '{param_name}'={arg} does not match the expected type "
+                f"{expected_type} in function '{func.__name__}'."
+            )
+        example_dict[param_name] = arg
+    
+    for key, value in kwargs.items():
+        if key not in input_type:
+            raise ValueError(
+                f"Unexpected named argument '{key}' for function '{func.__name__}'."
+            )
+        expected_type = input_type[key]
+        
+        if not isinstance(value, expected_type):
+            raise TypeError(
+                f"Named argument '{key}'={value} does not match the expected type "
+                f"{expected_type} in function '{func.__name__}'."
+            )
+        example_dict[key] = value
+
+
+def to_emulate(*args, func_obj, model=None, l_creativity=None, l_diversity=None, **kwargs):
+    """
+    Emulate the function with the given arguments and keyword arguments.
+    """
+    infos_cache = load_cache(func_obj)
+    input_type = get_input_types_from_signature(func_obj)
+
+    example_dict = {}
+
+    emulate_verificator(args=args, kwargs=kwargs, input_type=input_type, func=func_obj, example_dict=example_dict)
+    infos_cache["function_args"] = example_dict
+    infos_cache["function_call"] = f"{func_obj.__name__}({', '.join([f'{k}={v}' for k, v in example_dict.items()])})"
+    return _exec_emulate(_infos=infos_cache, _obj=func_obj, model=model, l_creativity=l_creativity, l_diversity=l_diversity)
+
+
+def load_cache(func_obj):
+    func_name = func_obj.__name__
+    path_name = os.path.join(CACHE_DIR, f"{func_name}.openhc")
+
+    if os.path.exists(path_name):
+        with open(path_name, "rb") as f:
+            cached_data = pickle.load(f)
+        return cached_data
+    else:
+        raise ValueError(f"Cache not found for function '{func_name}'.")
+
+
+def retrain(func_obj=None, force_train=True, epochs=None, get_loss=None, verbose=False):
+
+    infos_cache = load_cache(func_obj)
+    return _exec_predict(_function_infos=infos_cache, _function_obj=func_obj, force_train=force_train, epochs=epochs, get_loss=get_loss, verbose=verbose)
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/OpenHosta/requirements.txt b/src/OpenHosta/requirements.txt
new file mode 100644
index 0000000..32c71f3
--- /dev/null
+++ b/src/OpenHosta/requirements.txt
@@ -0,0 +1,5 @@
+Flask==3.0.3
+PyYAML==6.0.2
+pydantic==2.8.2
+Jinja2==3.1.4
+pyreadline3==3.4.1
\ No newline at end of file
diff --git a/src/OpenHosta/thought.py b/src/OpenHosta/thought.py
index 7547f73..ef059de 100644
--- a/src/OpenHosta/thought.py
+++ b/src/OpenHosta/thought.py
@@ -73,3 +73,4 @@ def inner_func(*args, **kwargs):
         return result
 
     return inner_func
+ 
\ No newline at end of file
diff --git a/src/OpenHosta/trainset.py b/src/OpenHosta/trainset.py
new file mode 100644
index 0000000..c2b1cfc
--- /dev/null
+++ b/src/OpenHosta/trainset.py
@@ -0,0 +1,59 @@
+import inspect
+
+from .cache import Hostacache
+from .example import type_verificator
+from .config import DefaultManager
+from .predict import load_cache
+
+l_default = DefaultManager.get_default_model()
+
+class TrainingSet():
+    def __init__(self, func : callable):
+        assert callable(func), "Please provide an hosta-injected function"
+        self.func = func
+        self.infos_cache = None
+
+    def visualize(self):
+        """
+        function for visualize the training set idk how for now
+        maybe let the hostashpère do it
+        """
+        hosta_cache = load_cache(self.func)
+        print("ho_example:")
+        for i in range(len(hosta_cache["ho_example"])):
+            print(hosta_cache["ho_example"][i])
+        print("ho_data:")
+        for i in range(len(hosta_cache["ho_data"])):
+            print(hosta_cache["ho_data"][i])
+        return [hosta_cache["ho_example"], hosta_cache["ho_data"]]
+
+    def add(self, *args, hosta_out=None,**kwargs):
+        """
+        function for add an example to the training set
+        """
+        input_type = {}
+        output_type = {}
+        data_dict = {}
+        hosta_func = self.func
+
+        if hosta_out is None:
+            raise ValueError("Please provide hosta_out for output.")
+        if hosta_func is None:
+                raise ValueError("Please provide hosta_func for specifying the function")
+        elif callable(hosta_func):
+            func = hosta_func
+        else:
+            raise ValueError("Please provide hosta_func for specifying the function")
+
+        try:
+            sig = inspect.signature(func)
+            for param in sig.parameters.values():
+                input_type[param.name] = param.annotation
+            output_type["hosta_out"] = sig.return_annotation
+        except:
+            raise ValueError("Function does not have a signature")
+
+        type_verificator(args, kwargs, input_type, output_type, hosta_out, func, data_dict)
+        cache_id = "ho_data"
+        cache = Hostacache(func, cache_id, data_dict)
+        cache.create_hosta_cache()


Test patch:
diff --git a/tests/functionnalTests/test_mandatory.py b/tests/functionnalTests/test_mandatory.py
index 63c6e04..7573b9a 100644
--- a/tests/functionnalTests/test_mandatory.py
+++ b/tests/functionnalTests/test_mandatory.py
@@ -2,8 +2,9 @@
 import time as t
 from pydantic import BaseModel
 from typing import Callable
+import numpy as np
 
-from OpenHosta import config, emulate, thought
+from OpenHosta import config, emulate, thought, example, load_training_example, predict, TrainingSet
 
 g_apiKey = ""
 
@@ -32,6 +33,71 @@ class User(BaseModel):
     email: str
     age: int
 
+class TestPredict:
+    def test_basic_input_predict(self):
+        def predict_1(a: int) -> float:
+            """
+            This function predicts the id of a letter
+            """
+            example(1, hosta_out=1.0)
+            example(2, hosta_out=2.0)
+            example(3, hosta_out=3.0)
+            return predict()
+        
+        ret_1 = predict_1(1)
+        assert type(ret_1) == float
+
+
+    def test_mutiple_input_predict(self):
+        def predict_2(a: int, b:int) -> float:
+            """
+            This function predict the id of sum of two letter
+            """
+            example(a=1, b=2, hosta_out=3.0, hosta_func=predict_2)
+            return predict()
+        ret_2 = predict_2(1, 2)
+        assert type(ret_2) == float
+
+    def test_attributs_predict(self, capsys):
+        def predict_3(a:int) -> float:
+            """
+            transform an int into a float
+            """
+            example(1, hosta_out=1.0)
+            return predict()
+
+        ret = predict_3(1)
+        predict_3.retrain(epochs=15, verbose=False)
+        ret_after = predict_3(1)
+        assert ret != ret_after
+
+        ret_emulate = predict_3.emulate(1)
+        print(ret_emulate)
+        assert type(ret_emulate) == float
+
+        predict_3.continue_train(epochs=1500, get_loss=0.01, verbose=True)
+        stdout = capsys.readouterr()
+        assert "Loss target achieved at epoch" in stdout.out
+
+class TestTrainingset:
+    def test_training_function(self):
+        len_sublist = 0
+        def function_to_test(a: int, b: int) -> float:
+            """
+            This function returns the sum of two integers.
+            """
+            return predict()
+        example(a=1, b=2, hosta_out=3.0, hosta_func=function_to_test)
+
+        training_test = TrainingSet(function_to_test)
+        training_test.add(a=2, b=3, hosta_out=5.0)
+        dataset = training_test.visualize()
+
+        expected_values = [{'a': 1, 'b': 2, 'hosta_out': 3}, {'a': 2, 'b': 3, 'hosta_out': 5}]
+
+        for expected in expected_values:
+            assert expected in dataset[0] or expected in dataset[1], f"{expected} n'est pas présent dans le dataset"
+
 class TestEmulate:
        
     def test_FeatureModelInParameter(self):
@@ -190,7 +256,7 @@ def multiply(a:int, b:int)->int:
         assert not multiply.advanced is None
         assert not multiply.enhanced_prompt is None
         assert not multiply.review is None
-        assert not multiply.diagramm is None
+        assert not multiply.diagram is None
         
     def test_rc2(self):