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

Issue:
Refacto ebatt fix
Hello Merlin,

Voici mes propositions de modification pour la beta. 

Je te laisse décider si tu les intègres en l'état dans la RC ou si tu veux changer des détails.

Emmanuel 


Discussion / hints:

Fix patch:
diff --git a/.flake8 b/.flake8
deleted file mode 100644
index 93c3a6e..0000000
--- a/.flake8
+++ /dev/null
@@ -1,14 +0,0 @@
-[flake8]
-
-filename =
-    src/OpenHosta.py
-    src/setup.py
-extend-exclude =
-    src/__pycache__/
-    jipynb/
-max_line_length = 99
-ident-size = 3
-disable_noqa = True
-statistics = True
-output-file = errors.txt
-doctests = True
\ No newline at end of file
diff --git a/.github/workflows/quality_check.yml b/.github/workflows/quality_check.yml
new file mode 100644
index 0000000..c96d87c
--- /dev/null
+++ b/.github/workflows/quality_check.yml
@@ -0,0 +1,123 @@
+name: Python Quality Check
+
+on:
+  push:
+    branches:
+      - main
+      - dev
+  pull_request:
+    branches:
+      - main
+      - dev
+
+jobs:
+  setup:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      
+      - name: Set up Python 3.12
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.12"
+          
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install .[dev,pydantic]
+          
+      - name: Cache dependencies
+        uses: actions/cache@v4
+        with:
+          path: ~/.cache/pip
+          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+          restore-keys: |
+            ${{ runner.os }}-pip-
+
+  code-quality:
+    needs: setup
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+        with:
+          python-version: "3.12"
+          
+      - name: Install dependencies
+        run: pip install .[dev,pydantic]
+        
+      - name: Run autopep8
+        run: python -m autopep8 --recursive --in-place src/OpenHosta
+        
+      - name: Run pyflakes
+        run: python -m pyflakes src/OpenHosta/
+        continue-on-error: true
+        
+      - name: Run isort
+        run: python -m isort --check-only --diff src/OpenHosta
+        continue-on-error: true
+
+  static-analysis:
+    needs: setup
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+        with:
+          python-version: "3.12"
+          
+      - name: Install dependencies
+        run: pip install .[dev,pydantic]
+        
+      - name: Run mypy
+        run: python -m mypy src/OpenHosta
+        continue-on-error: true
+        
+      - name: Run pylint
+        run: python -m pylint --rcfile=pyproject.toml src/OpenHosta
+        continue-on-error: true
+        
+      - name: Run bandit
+        run: python -m bandit -c pyproject.toml -r src/OpenHosta -f txt
+        continue-on-error: true
+
+  unit-tests:
+    needs: setup
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+        with:
+          python-version: "3.12"
+          
+      - name: Install dependencies
+        run: pip install .[tests]
+        
+      - name: Run unit tests
+        run: python -m pytest tests/unitTests -v --cov=OpenHosta.core
+
+  functional-tests:
+    needs: setup
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+        with:
+          python-version: "3.12"
+          
+      - name: Install dependencies
+        run: pip install .[tests]
+        
+      - name: Run functional tests
+        run: python -m pytest tests/functionnalTests -v --cov=OpenHosta
+
+  notify:
+    needs: [code-quality, static-analysis, unit-tests, functional-tests]
+    runs-on: ubuntu-latest
+    if: always()
+    steps:
+      - name: Check for failures
+        if: contains(needs.*.result, 'failure')
+        run: |
+          echo "Some jobs have failed. Check the logs above for more details."
+          exit 1
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 553075f..8b11efb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,9 @@ __hostacache__/
 .pytest_cache
 src/OpenHosta.egg-info
 build/
-.pypirc
\ No newline at end of file
+.pypirc
+mypy/
+.mypy_cache/
+linter.py
+*pt.py
+tests/reports/*report.txt
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 933d490..d2f7511 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,36 @@ All significant changes to this project will be documented in this file.
 
 ---
 
+## **v2.0-beta2**
+- **Added**
+  - Introduced `BaseEncoder` as an Abstract Base Class (ABC) for all specific encoders, providing a standardized interface.
+  - Added `BoolEncoder` to handle encoding of boolean values.
+  - Introduced `PredictConfig` data class to encapsulate parameters for the `predict` function.
+  - Initial implementation of the `HostaCache` class in `core/cache.py` with generic type support.
+  - Initial implementation of the `ModelCachedData` and `HostaModelCache` classes in `predict/cache.py`.
+- **Changed**
+  - Refactored `IntEncoder` and `FloatEncoder` to inherit from `BaseEncoder` and implement the encode method.
+  - Updated `HostaEncoder` to use a dictionary (`self.encoders`) for mapping data types to their corresponding encoders. This allows for easier extension and maintenance.
+  - Improved type handling in `HostaEncoder.encode` method for better extensibility and readability.
+  - Refactored `predict` function to accept a single `PredictConfig` object instead of multiple parameters. This change improves readability and maintainability of the function.
+- **Fixed**
+  - Enhanced exception handling in `FloatEncoder` to provide more informative error messages.
+  - Removed unnecessary constructors from encoder classes, streamlining the code.
+
+## **v2.0-beta1**
+
+- **Features**
+  - Added `max_tokens` args for emulate
+  - Added `use_locals/self_as_ctx` args for emulate for clarity and modularity
+  - `thought` is now used to add chain of thoughts in a emulated function
+  - `PromptManager` is now available for users to change all meta-prompt.
+
+- **Changes**
+  - `thought` function become `thinkof`
+  - `suggest` is removed
+  - `creativity` become `temperature` and `diversity` become `top_p`
+  - There's no more `hostacache` for emulated functions
+
 ## **OpenHosta v1.2.1 - 10/14/24**
 
 - **Fixes**
diff --git a/Makefile b/Makefile
index e331e08..ac0c0cd 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,4 @@
+
 PACKAGE_NAME := OpenHosta
 SRC_DIR := src/OpenHosta
 TEST_DIR := tests
@@ -50,9 +51,6 @@ help:
 
 install: all
 	@$(L_SHELL) $(WRITE) '$(TAG) [INSTALL]' $(COLOR)
-	@$(L_SHELL) $(WRITE) '$(TAG) Installing dependencies...' $(COLOR)
-	@$(PIP) install requirements.txt > $(NULL_DEVICE)
-	@$(PIP) install requirements-dev.txt > $(NULL_DEVICE)
 	@$(L_SHELL) $(WRITE) '$(TAG) Installing package: $(PACKAGE_NAME)...' $(COLOR)
 	@$(PIP) install . > $(NULL_DEVICE)
 	@$(L_SHELL) $(WRITE) '$(TAG) Succesfully installed $(PACKAGE_NAME) !' $(COLOR)
@@ -64,29 +62,6 @@ build: clean
 upload: build
 	$(PYTHON) -m twine upload dist/* --verbose
 
-ftests: clean
-	@$(L_SHELL) $(WRITE) '$(TAG) Installing package: $(PACKAGE_NAME)...' $(COLOR) 
-	@$(PIP) install . > $(NULL_DEVICE)
-	@$(L_SHELL) $(WRITE) '$(TAG) Running functionnal tests...' $(COLOR) 
-	@$(L_SHELL) $(SET_ENV)
-	@-$(L_SHELL) "$(PYTEST) .\\tests\\functionnalTests\\test_mandatory.py $(ARGS)"
-	@$(L_SHELL) $(UNSET_ENV)
-	@$(L_SHELL) $(WRITE) '$(TAG) Uninstalling package: $(PACKAGE_NAME)...' $(COLOR) 
-	@$(PIP) uninstall -y OpenHosta > $(NULL_DEVICE)
-	@$(L_SHELL) $(WRITE) '$(TAG) Succesfully ran functionnal tests !' $(COLOR)
-
-utests: clean
-	@$(L_SHELL) $(WRITE) '$(TAG) Installing package: $(PACKAGE_NAME)...' $(COLOR) 
-	@$(PIP) install . > $(NULL_DEVICE)
-	@$(L_SHELL) $(WRITE) '$(TAG) Running unit tests...' $(COLOR) 
-	@$(L_SHELL) $(SET_ENV)
-	@-$(L_SHELL) "$(PYTEST) .\\tests\\unitTests\\test_exec.py $(ARGS)"
-	@$(L_SHELL) $(UNSET_ENV)
-	@$(L_SHELL) $(WRITE) '$(TAG) Uninstalling package: $(PACKAGE_NAME)...' $(COLOR) 
-	@$(PIP) uninstall -y OpenHosta > $(NULL_DEVICE)
-	@$(L_SHELL) $(WRITE) '$(TAG) Succesfully ran unit tests !' $(COLOR)
-
-
 clean:
 	@$(L_SHELL) $(WRITE) '$(TAG) [CLEAN]' $(COLOR)
 	@$(L_SHELL) $(WRITE) '$(TAG) Cleaning repository...' $(COLOR)
@@ -96,6 +71,8 @@ clean:
 	@$(L_SHELL) $(FIND) 'dist' | $(RM)
 	@$(L_SHELL) $(FIND) 'OpenHosta.egg-info' | $(RM)
 	@$(L_SHELL) $(FIND) '.pytest_cache' | $(RM)
+	@$(L_SHELL) $(FIND) '.mypy_cache' | $(RM)
+	@$(L_SHELL) $(FIND) '.coverage' | $(RM)
 	@$(L_SHELL) $(WRITE) '$(TAG) Uninstalling package: $(PACKAGE_NAME)...' $(COLOR) 
 	@$(PIP) uninstall -y OpenHosta > $(NULL_DEVICE)
 	@$(L_SHELL) $(CLEAR)
diff --git a/README.md b/README.md
index e0693af..d802994 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
 # OpenHosta 
-v1.2.1 - Opensource Project
+v2.0-beta2 - Opensource Project
 
 **- The future of development is human -**
 
@@ -12,11 +12,7 @@ For this project, we have adopted a [Code of Conduct](CODE_OF_CONDUCT.md) to ens
 - [OpenHosta](#openhosta)
   - [Table of Content](#table-of-content)
   - [How to install OpenHosta ?](#how-to-install-openhosta-)
-    - [Prerequisites](#prerequisites)
-    - [Installation](#installation)
-      - [Via pip](#via-pip)
-      - [Via git (Developper version)](#via-git-developper-version)
-    - [Example](#example)
+  - [Example](#example)
   - [Further information](#further-information)
     - [Contributing](#contributing)
     - [License](#license)
@@ -26,81 +22,21 @@ For this project, we have adopted a [Code of Conduct](CODE_OF_CONDUCT.md) to ens
 
 ## How to install OpenHosta ?
 
-### Prerequisites
+You can install OpenHosta either via pip or via GitHub.
 
-1. **Python 3.10 | 3.11 | 3.12**
-   - Download and install Python from [python.org](https://www.python.org/downloads/).
-
-2. **pip**
-   - pip is generally included with Python. Verify its installation with:
-     ```sh
-     pip --version
-     ```
-
-3. **Git**
-   - Download and install Git from [git-scm.com](https://git-scm.com/downloads).
-
-4. **Virtual Environment (optional)**
-   - Create and activate a virtual environment:
-     ```bash
-     python -m venv env
-     ```
-   - Activate the virtual environement:
-      ```bash
-      .\env\Scripts\activate # Windows
-      source env/bin/activate # macOS/Linux
-      ```
-
-5. **API Key**
-   - **API Key**: Log in to your OpenAI account from [openai.com](https://openai.com/), then create your API key. For further information, you can check this [tuto](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key).
-
-### Installation
-
-#### Via pip
-
-1. Run the following command to install OpenHosta directly:
- 
 ```sh
-pip install openhosta
+pip install OpenHosta[all]
 ```
 
-2. After the installation, you can verify that OpenHosta is installed correctly by running:
+or
 
 ```sh
-pip show openhosta
-```
-
-#### Via git (Developper version)
-
-1. Clone the **Git repository** to your local machine using the following command:
-
-```bash
 git clone git@github.com:hand-e-fr/OpenHosta.git
 ```
 
-2. Navigate to the **directory** of the cloned project:
-
-```bash
-cd OpenHosta
-```
-
-3. Ensure you have installed the necessary **dependencies** before starting.
-
-```bash
-pip install .
-```
-
-4. Check that you have the correct version from Python. 
-
-```python
-import OpenHosta
-
-OpenHosta.__version__
-```
-
-This way you have all the documentation and source code to understand our project
+**See the full installation guide [here](docs/installation.md)**
 
-### Example
+## Example
 
 ```python
 from OpenHosta import emulate, config
@@ -116,7 +52,7 @@ def translate(text:str, language:str)->str:
 result = translate("Hello World!", "French")
 print(result)
 ```
-You check OpenHosta's [documentation](doc/Docs.md) for more detailled informations or exemple
+You check OpenHosta's [documentation](docs/doc.md) for more detailled informations or exemple
 
 ## Further information
 
diff --git a/doc/PMAC.md b/docs/PMAC.md
similarity index 100%
rename from doc/PMAC.md
rename to docs/PMAC.md
diff --git a/doc/Docs.md b/docs/doc.md
similarity index 51%
rename from doc/Docs.md
rename to docs/doc.md
index d026261..e363e28 100644
--- a/doc/Docs.md
+++ b/docs/doc.md
@@ -1,7 +1,7 @@
 # Documentation
 ___
 
-Documentation for version: **1.2.1
+Documentation for version: **2.0-beta2**
 
 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.
 
@@ -9,9 +9,9 @@ For this project, we have adopted a [Code of Conduct](CODE_OF_CONDUCT.md) to ens
 
 ___
 
-### Introduction
+## Introduction
 
-#### First Step
+### First Step
 
 OpenHosta is a **Python library** designed to facilitate the integration of **LLMs** into the developer's environment, by adding a layer to the Python programming language without distorting it. It is based on the [**PMAC**](PMAC.md) concept, reimagining the **compilation** process in languages. All our functionalities respect the **syntax and paradigm** of this language. 
 
@@ -23,7 +23,7 @@ We've already mentioned a few concepts about **AI** or **computer science**. If
 
 Finally, if you like the project and are thinking of contributing, please refer to our [Contribution Guide](CONTRIBUTING.md)
 
-#### Why use OpenHosta?
+### Why use OpenHosta?
 
 - **Beyond programming**
 
@@ -39,7 +39,7 @@ We are an Open-Source project. We believe this philosophy contributes to the **s
 
 ---
 
-##### *Legal Framework*
+### *Legal Framework*
 
 The use of AI in a production context raises important **legal** issues. It is essential to take these issues into account to ensure the compliance of your **deployment**.
 
@@ -60,52 +60,41 @@ For more information, please consult the following links:
 
 Let's **get started**! First here's the **table of contents** to help you navigate through the various sections of the documentation.
 
-### Table of Content
+## Table of Content
 
 - [Documentation](#documentation)
-    - [Introduction](#introduction)
-      - [First Step](#first-step)
-      - [Why use OpenHosta?](#why-use-openhosta)
-        - [*Legal Framework*](#legal-framework)
-    - [Table of Content](#table-of-content)
-  - [Features](#features)
+  - [Introduction](#introduction)
+    - [First Step](#first-step)
+    - [Why use OpenHosta?](#why-use-openhosta)
+    - [*Legal Framework*](#legal-framework)
+  - [Table of Content](#table-of-content)
+  - [Get Started](#get-started)
     - [OpenHosta Example](#openhosta-example)
-    - [Get Started](#get-started)
-      - [Librairie Import](#librairie-import)
-      - [Basic Setup](#basic-setup)
+    - [Install OpenHosta](#install-openhosta)
+    - [Librairie Import](#librairie-import)
+    - [Basic Setup](#basic-setup)
   - [`emulate` Function](#emulate-function)
-      - [Supported types \& Pydantic](#supported-types--pydantic)
+    - [Supported types \& Pydantic](#supported-types--pydantic)
       - [Integration Details](#integration-details)
-    - ["suggest" Function](#suggest-function)
-      - [Usage](#usage)
-      - [Output Examples](#output-examples)
-  - [`predict` Function](#predict-function)
-    - [How `predict` Works](#how-predict-works)
-    - [Limitations and Known Issues](#limitations-and-known-issues)
-    - [`predict` Function Parameters](#predict-function-parameters)
-    - [Additional `predict` Functionalities](#additional-predict-functionalities)
-      - [1. `retrain`](#1-retrain)
-      - [Example:](#example)
-      - [2. `continue_train`](#2-continue_train)
-      - [Example:](#example-1)
-      - [3. `emulate`](#3-emulate)
-      - [Example:](#example-2)
-    - [TrainingSet Management](#trainingset-management)
-      - [Example:](#example-3)
-    - [Training Output of predict](#training-output-of-predict)
-  - [`thought` Function](#thought-function)
-  - [`example` Function](#example-function)
-    - [Advanced configuration](#advanced-configuration)
-      - [Introduction](#introduction-1)
+    - [Body Functions](#body-functions)
+      - [`Example`](#example)
+      - [`Thought`](#thought)
+  - [`thinkof` Function](#thinkof-function)
+  - [`ask` function](#ask-function)
+  - [Advanced configuration](#advanced-configuration)
+    - [Models](#models)
       - [Inheriting from the Model Class](#inheriting-from-the-model-class)
       - [Custom LLM Call Function](#custom-llm-call-function)
       - [Custom Response Handling Function](#custom-response-handling-function)
       - [Create a new instance](#create-a-new-instance)
-    - [References](#references)
+    - [Prompts](#prompts)
+      - [Edit the prompt](#edit-the-prompt)
+      - [Show me the prompt !](#show-me-the-prompt-)
+  - [References](#references)
 
 ---
 
-## Features
+## Get Started
 
 For each part, you'll find functional examples to illustrate the features. If you have any questions, don't hesitate to visit the “Discussion” tab on GitHub.
 
@@ -114,8 +103,6 @@ For each part, you'll find functional examples to illustrate the features. If yo
 ```python
 from OpenHosta import emulate, config
 
-config.set_default_apiKey("put-your-api-key-here")
-
 def translate(text:str, language:str)->str:
     """
     This function translates the text in the “text” parameter into the language specified in the “language” parameter.
@@ -126,21 +113,22 @@ result = translate("Hello World!", "French")
 print(result)
 ```
 
-### Get Started
+### Install OpenHosta
+
+The installations process is described step-by-step in the [installation guide](installation.md).
 
 Once you've installed the OpenHosta library, you're ready to get started. We'll import the library and then look at the basic configurations.
 
-#### Librairie Import
+### Librairie Import
 
 ```python
 from OpenHosta import *
 ```
 
 We recommend this import method, as it gives you all the important and stable features:
-  - Emulate function
-  - Thought function
-  - Example function
-  - \_\_suggest\_\_ attributes
+  - All the tools to emulate Python function (`emulate` and all it's attached functions)
+  - All the tools to create specialized model (`predict` and all it's attached functions)
+  - All the others features useful in `OpenHosta` (`ask`, `thinkof`, tools for data generation...)
   - Configuration tools
 
 But you can also import modules one by one.
@@ -149,19 +137,21 @@ But you can also import modules one by one.
 from OpenHosta import emulate, config
 ```
 
-#### Basic Setup
+### Basic Setup
 
 This section focuses on the *config* module.
 
 As previously mentioned, a default model is automatically assigned: GPT-4o. To use it, you first need to enter your API key.
 
+Two methods for that, either you set an environment variable (see the [Installation guide](installation.md)), or you can set it with the following function:
+
 ```python
 config.set_default_apiKey("put-your-api-key-here")
 ```
 
 Once you've done that, all OpenHosta's features are ready to use.
 
-If you wish to use another model, you'll need to create an instance of the *Model* class.
+If you want to use another model, you'll need to create an instance of the *Model* class.
 
 ```python
 my_model = config.Model(
@@ -171,7 +161,7 @@ my_model = config.Model(
 )
 ```
 
-Note that some features like `thought` or `__suggest__` specifically use the default model. So if you want to change it, use this.
+Note that some features like `thinkof` or `LLMSyntheticDataGenerator` specifically use the default model. So if you want to change it, use this:
 
 ```python
 config.set_default_model(my_model)
@@ -181,13 +171,16 @@ config.set_default_model(my_model)
 
 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.
 
-Emulate is used inside a function or a class method, after the “return”. What it does is take the function's documentation as a “prompt” to emulate it. The way in which you write the function is therefore crucial to ensure that “emulate” works properly.
+Emulate is used inside a function or a class method. What it does is it takes the function's documentation as a “prompt” to emulate it. The way in which you write the function is therefore crucial to ensure that “emulate” works properly.
 
 Here's what you need to know:
-  - **The function prototype** is one of the elements sent to LLM. Its different fields must therefore appear clearly. Give a meaningful and precise name to your function. It's also a good idea to specify the type of arguments and the type of return to reduce the uncertainty related to LLM.
-  
+  - **The function prototype** is one of the elements sent to LLM. Its different fields must therefore appear clearly. Give a meaningful and precise name to your function. It's also a good idea to annotate your function by specifiing the type of arguments and the type of return. It gives to the LLM the exact format in which it must respond, therefore enhancing it's performance.
+  If you're not familliar with the Python annotation system, please check the [typing module documentation](https://docs.python.org/3/library/typing.html)
+
 ```python
-def function(a:int, b:dict)->str:
+from typing import List
+
+def example_function(a:int, b:str)->List[str]:
 ```
   - **The doctring** is the other key element. This is where you describe the behavior of the function. Be precise and concise. Describe the input parameters and the nature of the output, as in a docstring. Feel free to try out lots of things, prompt engineering is not a closed science. :)
 
@@ -215,16 +208,27 @@ def find_name_age(sentence:str, id:dict)->dict:
 
 Note that, as seen above, you can pass a previously configured model as an emulate parameter.
 
-Be careful, you can put regular instructions in your function and they will be executed. However, `emulate` retrieves all the variables local to the functions and gives them to the LLM as a context.
+Be careful, you can put regular instructions in your function and they will be executed. However, `emulate` can retrieves all the variables local to the functions and gives them to the LLM as a context with the `use_locals_as_ctx` parameter (set at `True`). If your emulated function is a class method, `emulate` can retrieves all the attributes of this class to also give them as context with the `use_self_as_ctx` parameter.
+
+Another parameter of `emulate` is `post_callback`. It take a callable as input and allows you to execute it to the output of the LLM. This can be useful for logging, error handling or formatting multiple emulated function's output with a single function. The callable passed take one argument which is the output of the LLM and must return another value. If the output of the LLM won't change during the execution of your callback, just return the value given in argument. 
 
-emulate also accepts two other arguments: `creativity` and `diversity`. It correspond to the "temperature" and "top_p" parameters of LLMs. These values range from 0 to 1 (inclusive). For more information, please refer to the official [OpenAI documentation](https://openai.com/).
+`emulate` also accepts other unspecified arguments: they correspond to the parameter of the LLM.
+You can find theses in your model's offcial documentation, but here's a few common:
+  - `temperature`
+  - `top_p`
+  - `max_tokens` 
+  - (...)
 
-#### Supported types & Pydantic
+### Supported types & Pydantic
 
 `emulate` support for the **typing** module: You can now use specific return types from the typing module, including [`List`, `Dict`, `Tuple`, `Set`, `FrozenSet`, `Deque`, `Iterable`, `Sequence`, `Mapping`, `Union`, `Optional`, `Literal`].
 
+> **Note:**
+> Annotate your function's return value as `Optional` tends to make the LLM respond with `None` in case of an inconsistent input. This can become useful when using `OpenHosta` in larger and more complex programm. 
+
 ```python
 from OpenHosta import emulate
+from typing import Dict, Tuple, List
 
 def analyze_text(text: str) -> Dict[str, List[Tuple[int, str]]]:
     """Analyze text to map each word to a list of tuples containing word length and word."""
@@ -240,8 +244,6 @@ print(type(analysis))
 It also includes a verification output that checks and converts the output to the specified type if necessary. Supported types include **Pydantic** models, all types from the **typing** module mentioned above, as well as built-in types such as `dict`, `int`, `float`, `str`, `list`, `set`, `frozenset`, `tuple`, and `bool`.
 The `complex` type is not supported. If you need to use complex numbers, please pass them as a `tuple` and manually convert them after processing.
 
-For more information about Typing module, please check the official [Typing documentation](https://docs.python.org/3/library/typing.html)
-
 OpenHosta integrates with Pydantic, a library for data validation and settings management using Python type annotations. This integration allows you to use Pydantic models directly within `emulate`, ensuring data consistency and validation.
 
 Pydantic provides a way to define data models using Python types. It automatically validates and converts input data to match these types, ensuring that your application processes data safely and accurately.
@@ -257,18 +259,17 @@ OpenHosta supports Pydantic typing by accepting Pydantic models as input of an e
 - **Data Parsing:** Converts input data to specified types, simplifying data handling.
 
 The Pydantic model will be automatically processed by our tools and the LLM to guarantee a stable output.
+The Pydantic model cannot be defined inside a function, as this will produce an error.
 Let's take the same example, but using this feature:
 
 ```python
 from pydantic import BaseModel
-from OpenHosta import emulate, config
+from OpenHosta import emulate
 
 class Person(BaseModel):
     name: str
     age: int
 
-config.set_default_api_key("put-your-api-key-here")
-
 def find_first_name(sentence:str)->Person:
     """
     This function find in a text the name and the age of a person.
@@ -283,226 +284,73 @@ def find_first_name(sentence:str)->Person:
     return emulate()
 ```
 
-Note that the Pydantic model cannot be defined inside a function, as this will produce an error.
-
-### "suggest" Function
+### Body Functions
 
-When you use the emulate function, an attribute is automatically attached. This attribute is a function giving you hints on how to improve your prompt, and a diagram visualization tool. This tool uses the default model to operate.
+In addition to the docstring-as-prompt, you can enhance your prompting with specialized functions. This way you can try different techniques like Zero/Few-shot prompting or Chain-of-Thought. If you're not familliar with theses concept, please check the [Reference](#references) section.
 
-#### Usage
+#### `Example`
 
-Here's how to use it:
+The first of the body function is `example`. It allow you to give example to LLM.
 
 ```python
-def find_occurence_of_a_word(word :str, text: str) -> int:
+from OpenHosta import emulate, example
+
+def is_positive(sentence:str)->bool:
     """
-    This function takes a word and a text and returns
-    the number of times the word appears in the text.
+    This function return True if the sentence in parameter is positive, False otherwise. 
     """
+    example(sentence="Marc got a good mark in his exam.", hosta_out=True)
+    example(sentence="The weather is awful today !", hosta_out=False)
     return emulate()
 
-find_occurence_of_a_word("Hello", "Hello World Hello!")
-
-print(suggest(multpily)) # to have the raw message
-print(multiply.diagram) # to have just the diagram
-
-find_occurence_of_a_word.__suggest__(find_occurence_of_a_word) # same
-print(find_occurence_of_a_word.advanced)
-```
+print(is_positive("I can do it !")) # True
 
-In this example, you can see that after calling the emulated function, we can call `suggest`, which takes as arguments the function object to which it is hooked. After that, we have four new attributes at our disposal:
-  - `enhanced prompt`: It's a proposal to improve the function's prompt. 
-  - `review`: This is the analysis provided by the AI for its prompt improvement. Can be useful to understand its reasoning.
-  - `advanced`: Similar to `enhanced prompt` but adds an iteration. The AI will then try to solve advanced problems according to context or other factors. Especially useful in the most complex cases.
-  - `diagram`: Gives a Mermaid diagram showing the stages of AI thinking. Useful if you want to try coding the function yourself.
-
-You can also retrieve the entire LLM response by storing the output of the `suggest` function.
-
-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:**
-This function takes two inputs: a word (string) and a text (string). It returns an integer representing the number of times the specified word appears in the given text. The function should be case-insensitive, meaning it should count occurrences of the word regardless of whether it is in uppercase or lowercase. Additionally, the function should handle punctuation properly, ensuring that words followed by punctuation marks are still counted correctly. The function should also include error handling to manage cases where the inputs are not strings or are empty.
-- **Review:**
-The prompt is clear but lacks specificity in handling edge cases and punctuation. It also doesn't specify how to handle different forms of the word, such as plural or possessive forms. Furthermore, it doesn't mention whether the function should count overlapping occurrences of the word. The prompt could benefit from more detailed requirements to ensure robustness and accuracy.
-- **Advanced:**
-This function takes two inputs: a word (string) and a text (string). It returns an integer representing the number of times the specified word appears in the given text. The function should be case-insensitive, meaning it should count occurrences of the word regardless of whether it is in uppercase or lowercase. It should handle punctuation properly, ensuring that words followed by punctuation marks are still counted correctly. The function should not count overlapping occurrences of the word. Additionally, the function should include error handling to manage cases where the inputs are not strings or are empty. It should also handle different forms of the word, such as plural or possessive forms, by considering only exact matches of the word.`
-- **Mermaid diagramm**
-```mermaid
-graph LR
-    A[Start] --> B[Receive inputs: word and text]
-    B --> C{Are inputs valid?}
-    C -- No --> D[Return error]
-    C -- Yes --> E[Convert both inputs to lowercase]
-    E --> F[Remove punctuation from text]
-    F --> G[Split text into words]
-    G --> H[Count exact matches of the word]
-    H --> I[Return the count]
-    I --> J[End]
+# It will be write as follow in the final prompt:
+#######
+# Here are some examples of expected input and output:
+#[{'in_': {'sentence': 'Marc got a good mark in his exam.'}, 'out': True}, {'in_': {'sentence': 'The weather is awful today !'}, 'out': False}]
 ```
 
+As shown above, `example` takes two types of argument. There are input parameters which must be named (kwargs) and exactly be the same number and type as in the function's definition. Finally, there's the “hosta_out” parameter. This corresponds to the expected LLM output.
 
----
-
-## `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 :)
----
+Giving inconsistent examples can severely impact LLM performance. But it can be a very good tool if it's used properly. 
 
+#### `Thought`
 
-- *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*:
+The second body function is `thought`. It allows you to create chain of thought inside your prompt to enhance it's performance for more complex tasks.
 
 ```python
-from OpenHosta import predict, example
+from OpenHosta import emulate, thought
+from typing import List, Optional
 
-def find_chance_of_die(age: float) -> float:
+def car_advice(query:str, car_available:List[str])->Optional[str]:
     """
-    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).
+    This function gives the best advice for the query in parameter. It must return the best car in the list "car_available" fitting the user's needs.
+    If no car fit the user's needs, this function returns None.
     """
-    # 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
+    thought("identify the context and the need of the user")
+    thought("Look at the car available to find a car matching his needs")
+    thought("Return the name of the most relevant car, if no car is matching return None")
+    return emulate()
 
-cos_plus_sin_generator()
-training_maths.visualize()  # Visualize the dataset
+car_list = [
+    "Lamborghini Aventador LP700-4",
+    "Volkswagen Coccinelle type 1",
+    "Ford Mustang 2024"
+]
+
+print(car_advice("I lives in the center of a big city with a lot of traffic, what do you recommend ?", car_list))
+# Volkswagen Coccinelle type 1
+print(car_advice("I would two buy a new car, but I would like an electric because I don't want to ruin my planet.", car_list))
+# None
+
+# It will be write as follow in the final prompt:
+#####
+# To solve the request, you have to follow theses intermediate steps. Give only the final result, don't give the result of theses intermediate steps:
+# [{'task': 'identify the context and the need of the user'}, {'task': 'Look at the car available to find a car matching his needs'}, {'task': 'Return the most relevant car, if no car is matching return None'}, {'task': 'identify the context and the need of the user'}, {'task': 'Look at the car available to find a car matching his needs'}, {'task': 'Return the most relevant car, if no car is matching return None'}]
 ```
 
-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
+## `thinkof` 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.
 
@@ -513,103 +361,80 @@ These files are used to manage the model, its saved state, and how incoming data
   
 For more information, please check https://python-reference.readthedocs.io/en/latest/docs/operators/lambda.html
 
-In an aim to integrate with Python syntax, we've developed the `thought` function. It replicates the same behavior as lambda.
+In an aim to integrate with Python syntax, we've developed the `thinkof` function. It replicates the same behavior as lambda.
 
 Here's how it works:
 
 ```python
-from OpenHosta import thought
+from OpenHosta import thinkof
 
-x = thought("Is it a masculine name")
+x = thinkof("Is it a masculine name")
 print(x("John"))  # True
 
-result = thought("Multiply by two")(2)
+result = thinkof("Multiply by two")(2)
 print(result)   # 4
 ```
 
-In the example above, we can see two distinct ways of using `thought`. In the first example, you can store a lambda function in a variable and then use it. You can also call it directly by enclosing the arguments behind it in brackets. `thought` accepts multiple arguments and all types native to python. However, the content of the first bracket is always a string.
+In the example above, we can see two distinct ways of using `thinkof`. In the first example, you can store a lambda function in a variable and then use it. You can also call it directly by enclosing the arguments behind it in brackets. `thinkof` accepts multiple arguments and all types native to python. However, the content of the first bracket is always a string.
 
-The `thought` function has an initial pre-compilation stage where it predicts the type of the return value by making an initial call to an LLM. Execution time can therefore be increased.
+The `thinkof` function has an initial pre-compilation stage where it predicts the type of the return value by making an initial call to an LLM. Execution time can therefore be increased the first time using the function, then the type is stored and reused for the next execution.
 You can retrieve the predicted return type with the `_return_type` attribute attached to the object:
 
 ```python
-from OpenHosta import thought
+from OpenHosta import thinkof
 
-x = thought("Adds all integers")
+x = thinkof("Adds all integers")
 ret = x(2 ,3 ,6)
 print(x._return_type) # int
 ```
 
 **Note** : ***this feature uses the default model.***
 
-## `example` Function
+## `ask` 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.
-
-**Key Characteristics**
-
-- **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_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: 
+The function `ask` is a sort of a *side* function In OpenHosta. Its only use is to make a simple LLM call without the OpenHosta's meta-prompt. It simplies the process af an API call.
 
 ```python
-from OpenHosta import emulate, example
-
-def translate(text:str, language:str)->str:
-    """
-    This function translates the text in the “text” parameter into the language specified in the “language” parameter.
-    """
-    example("Bonjour Monde !", "portuguese", hosta_out="ola mundo" )
-    return emulate()
-
-
-example(text="Hello World !", language="japanese", hosta_out="こんにちは世界!", hosta_func=translate)
-
-print(translate("Hello World !", "French"))
+from OpenHosta import ask, Model
+
+print(
+    ask(
+        system="You're a helpful assistant."
+        user="Write me a cool story."
+        max_tokens=200
+    )
+)
 ```
 
-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_training_example`
+The "traditional" would be like this:
 
 ```python
-from OpenHosta import save_examples, load_training_example
-
-save_examples(hosta_func=translate, hosta_path="translate_func_example")
+import openai
 
-#######
-
-def another_translate(text:str, language:str)->str:
-    """
-    This function translates the text in the “text” parameter into the language specified in the “language” parameter.
-    """
-    return emulate()
+openai.api_key = "your-api-key-here"
 
-load_training_example(hosta_path="translate_func_example.jsonl", hosta_func=another_translate)
-```
+messages = [
+    {"role": "system", "content": "You are a helpful assistant."},
+    {"role": "user", "content": "Write me a cool story."}
+]
 
-output of the `translate_func_example.jsonl`
+response = openai.ChatCompletion.create(
+    model="gpt-4o",
+    messages=messages
+)
 
-```JsonL
-{"text": "Hello World !", "language": "japanese", "hosta_out": "こんにちは世界!"}
-{"text": "Bonjour Monde !", "language": "portuguese", "hosta_out": "ola mundo"}
+print(response['choices'][0]['message']['content'])
 ```
 
+As seen above takes 2 or more argument. The two first arguments are mandatory. `system` correspond to the system prompt to the LLM, same as the `user` parameter. You can also set the `model` parameter to a custom Model instance. It also handle all LLM parmaters (`max_tokens`, `n`, `top_p`...).
 
-**Notes**: *All examples provided for a function are stored in a directory at the root of your environment. You can see it as* ***\_\_hostacache__***.
-
-
-
-### Advanced configuration
+**Note** : ***this feature uses the default model.***
 
+## Advanced configuration
 
-#### Introduction
+### Models
 
-This section explains how to customize the program to make its own LLM call and response handling functions. This can be useful if you need a specific functionality that is not provided by the standard library, or to enable compatibility with a specific LLM.
+This section explains how to customize the program to make its own LLM call and response handling functions. This can be useful if you need a specific functionality that is not provided by the standard library, or to enable compatibility with a specific LLM. 
 
 #### Inheriting from the Model Class
 
@@ -714,14 +539,64 @@ ret = x("Hello World")
 print(ret)
 ```
 
+### Prompts
+
+#### Edit the prompt
+
+`emulate` works by putting the emulated function's parsed data in a meta-prompt designed to give the best performance and ensure a constant ouptput format. But you can different parts of this meta-prompt. Here's how you do it.
+
+You first need to import `EMULATE_PROMPT`
+
+```python
+from OpenHosta import EMULATE_PROMPT
+```
+
+This is an instance of a class conatining attributes. These attributes are all the separated parts (named "shards") of the prompt, that are then combined to build the final prompt. You'll only need to change theses attributes to change automatically the prompt.
+
+Here's all the shards:
+- **CTX_MAIN**: The main context gived to the LLM.
+- **CTX_SEP1**: The separator between the context and the example.
+- **CTX_EXAMPLE**: The examples decribing the overall functioning of OpenHosta.
+- **CTX_SEP2**: The separator between the context and emulated function's infos. 
+- **PRE_DEF**: The sentence introducing the function's definition.
+- **PRE_TYPE**: The sentence introducing the function's annotations.
+- **PRE_SCHEMA**: The sentence introducing the function's return type's schema.
+- **PRE_LOCALS**: The sentence introducing the function's local variables (Optional).
+- **PRE_SELF**: The sentence introducing the function's local attributes (Optional).
+- **PRE_EXAMPLE**: The sentence introducing the examples given by the user (Optional). 
+- **PRE_COT**: The sentence introducing the Chain-of-Thought (Optional).
+- **USER_SEP**: The separator between the system prompt and the user prompt.
+
+You can find all the separated values by visiting the [meta_prompt.py](../src/OpenHosta/utils/meta_prompt.py) file.
+
+To see te full filled prompt send to the LLM, you can also use the `print_last_prompt` function:
+
+```python
+from OpenHosta import emulate, print_last_prompt
+
+def multiply(a:int, b:int)->int:
+    """
+    This function multiplies two integers in parameter.
+    """
+    return emulate()
+
+multiply(5, 6)
+print_last_prompt(multiply)
+```
+
+#### Show me the prompt !
+
 ---
 
-### References
+## References
 
 - **LLM**: https://en.wikipedia.org/wiki/Large_language_model
 - **GPT-4o**: https://en.wikipedia.org/wiki/GPT-4o
 - **AI**: https://en.wikipedia.org/wiki/Artificial_intelligence
 - **NLP**: https://en.wikipedia.org/wiki/Natural_language_processing
+- **Zero-Shot Prompting**: https://www.promptingguide.ai/techniques/zeroshot
+- **Few-Shot Prompting**: https://www.promptingguide.ai/techniques/fewshot
+- **Chain-of-Thought**: https://www.promptingguide.ai/techniques/cot
 
 ---
 
diff --git a/docs/installation.md b/docs/installation.md
new file mode 100644
index 0000000..aa2a67b
--- /dev/null
+++ b/docs/installation.md
@@ -0,0 +1,139 @@
+# Installation
+
+The `OpenHosta` python package contains multiple features which you can install via PyPI or GitHub. However, you may need to install other packages depending on how you use `OpenHosta`. Indeed, some `OpenHosta` features require additional dependencies to be used. You'll also need to set up your API key for easier use.
+All this will be detailed in this document.
+
+## Prerequisites
+
+1. **Python 3.10 | 3.11 | 3.12**
+   - Download and install Python from [python.org](https://www.python.org/downloads/).
+
+2. **pip**
+   - `pip` is generally included with Python. Verify its installation with:
+     ```sh
+     pip --version
+     ```
+
+## OpenHosta Installation
+
+### **Install using `pip`**:
+  1. Install the package
+  ```sh
+  pip install -U OpenHosta[all]
+  ```
+  2. Verify installation
+  ```sh
+  pip show OpenHosta
+  ```
+### **Install using `GitHub`**
+  1. Clone the Git repository:
+
+  ```sh
+  git clone git@github.com:hand-e-fr/OpenHosta.git
+  ```
+
+  1. Navigate to the directory:
+
+  ```sh
+  cd OpenHosta
+  ```
+
+  3. Install the package.
+
+  ```sh
+  pip install .[all]
+  ```
+
+  4. Verify installation
+
+  ```sh
+  python -c "import OpenHosta; print(OpenHosta.__version__)"
+  ```
+
+Installing OpenHosta with GitHub gives you access to all the source code, allowing you to take over the tool and perhaps contribute to the project, but the pip approach is simpler for classic OpenHosta use.
+
+## Additional Dependencies
+
+You can install additional dependencies to use deeper features of `OpenHosta`. You'll need to add the following options.
+
+> **Note**
+> `OpenHosta` base features are "Pure Python". This means that it contains 0 dependencies. It include the `emulate` function and all its related functionalities (`config`, `example`, `thought`), the `thinkof` function and the `ask` function. 
+> However, you don't have access to `predict` and the `pydantic` compatibility.
+```sh
+pip install -U OpenHosta # With pip
+```
+*or* 
+```sh
+pip install . # With GitHub
+```
+
+### `predict`
+
+Adds the `predict` function and all its features.
+```sh
+pip install -U OpenHosta[predict]
+```
+
+### `pydantic`
+
+Adds the `pydantic` compatibility with all functions.
+```sh
+pip install OpenHosta[pydantic]
+```
+*or*
+```sh
+pip install pydantic # Basically the same
+```
+
+### `dev`
+
+*Not inclued in "all"*
+
+Adds all the useful OpenHosta development tools for those who'd like to contribute.
+```sh
+pip install .[dev] # Useful only with the GitHub install, yon won't need it if you're not interested in contributing for OpenHosta 
+```
+
+### `tests`
+
+*Not inclued in "all"*
+
+Adds all the package used for executing the `OpenHosta`'s tests.
+```sh
+pip install .[tests] # Also only useful with the GitHub install
+```
+
+### Combining Options
+
+All options can be combined as needed.
+
+For example, if you're a contributor, it might be useful to install `dev` and `tests` packages:
+```sh
+pip install .[dev, tests]
+```
+Note that the `pydantic` and `predict` packages combined are the same as `all`.
+
+## API Key Setup
+
+1. Get API key from your favorite provider. As default model we use OpenAI `GPT-4o`, you can get a key from https://platform.openai.com/account/api-keys
+2. Then all you have to do is set a environment variable containing this API key:
+   - Windows:
+    ```sh
+    setx OPENAI_API_KEY "your-openai-api-key"
+    ```
+   - MacOS/Linux:
+    ```sh
+    # in your .bashrc or .zshrc
+    export OPENAI_API_KEY='your-anthropic-api-key'
+    ```
+
+## Common Issues
+
+If you encounter a problem, try these few method that may fix it:
+
+- Update pip: ``pip install --upgrade pip``
+- Use virtual environment
+- Try ``pip3`` instead of ``pip``
+- Use ``sudo`` (Unix) or run as administrator (Windows) if permission errors occur
+
+For more help and if the problem persist, please file an issue on GitHub.
diff --git a/pyproject.toml b/pyproject.toml
index fd7be4f..1ec95fb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "OpenHosta"
-version = "1.2.1"
+version = "2.0beta2"
 description = "Open-Source programming project IA integretion in developement environnement"
 keywords = ["AI", "GPT", "Natural language", "Autommatic", "Easy"]
 authors = [
@@ -26,13 +26,34 @@ classifiers = [
     "Topic :: Software Development :: Code Generators"
 ]
 dependencies = [
-  "requests>=2.32.3",
-  "pydantic>=2.8.2",
-  "tiktoken>=0.7.0",
-  "jsonschema>=4.23.0",
-  "typing-extensions>=4.12.2",
-  "numpy>=2.1.1",
-  "torch>=2.3.1"
+    "requests>=2.32.3"
+]
+
+[project.optional-dependencies]
+all = [
+    "pydantic>=2.8.2",
+    "torch>=2.5.1"
+]
+pydantic = [
+    "pydantic>=2.8.2"
+]
+dev = [ 
+    "mypy>=1.13.0",
+    "isort>=5.13.2",
+    "autopep8>=2.3.1",
+    "pylint>=3.3.1",
+    "pyflakes>=3.2.0",
+    "bandit>=1.7.10"
+]
+tests = [
+    "pytest>=8.3.2",
+    "pytest-cov>=5.0.0",
+    "pillow>=11.0.0",
+    "pydantic>=2.8.2",
+    "Flask>=3.0.3"
+]
+predict = [
+    "torch>=2.5.1"
 ]
 
 [project.urls]
@@ -46,11 +67,24 @@ package-dir = { "" = "src" }
 where = ["src"]
 
 [tool.setuptools.package-data]
-"OpenHosta" = ["*.json"]
-
-[tool.pytest.ini_options]
-testpaths = [
-    'tests/functionnalTests',
-    'tests/unitTests',
-    'tests/performanceTests'
-]
\ No newline at end of file
+"OpenHosta" = ["utils/*.json"]
+
+[tool.bandit]
+skips = ["B112"]
+targets = ["src/OpenHosta"]
+recursive = true
+
+[tool.pylint]
+disable = [
+    "C0111",  
+    "C0103",  
+    "C0114",  
+    "C0115",  
+    "C0116",  
+    "R0903",
+    "C0415"
+]
+max-line-length = 120
+
+[tool.mypy]
+disable_error_code = ["attr-defined"]
\ No newline at end of file
diff --git a/src/.gitignore b/src/.gitignore
deleted file mode 100644
index cf1e07b..0000000
--- a/src/.gitignore
+++ /dev/null
@@ -1,1 +0,0 @@
-OpenHosta.egg-info/*
\ No newline at end of file
diff --git a/src/OpenHosta/OpenHosta.py b/src/OpenHosta/OpenHosta.py
index 762efae..2d40c68 100644
--- a/src/OpenHosta/OpenHosta.py
+++ b/src/OpenHosta/OpenHosta.py
@@ -1,32 +1,44 @@
-from .config import Model, DefaultManager
+from __future__ import annotations
+from .exec.ask import ask
+from .exec.predict.model import ArchitectureType
+from .exec.predict.predict_config import PredictConfig
+from .exec.predict.predict import predict
+from .utils.meta_prompt import EMULATE_PROMPT
+from .exec.thinkof import thinkof
+from .exec.thought import thought
+from .exec.example import example
+from .exec.emulate import emulate
+from .core import config
+from .core.config import Model, DefaultManager
+from .utils.meta_prompt import print_last_prompt
+
+import os
 
 DefaultManager.set_default_model(
-    Model(model="gpt-4o", base_url="https://api.openai.com/v1/chat/completions")
+    Model(model="gpt-4o", base_url="https://api.openai.com/v1/chat/completions",
+          api_key=os.getenv("OPENAI_API_KEY") or None)
 )
 
-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, save_examples, load_training_example
-from .enhancer import suggest
-
-emulate = HostaInjector(_exec_emulate)
-predict = HostaInjector(_exec_predict)
+from .core import config
+from .exec.emulate import emulate
+from .exec.example import example
+from .exec.thought import thought
+from .exec.thinkof import thinkof
+from .exec.predict.predict import predict
+from .exec.predict.predict_config import PredictConfig
+from .exec.predict.model import ArchitectureType
+from .exec.ask import ask
 
-__all__ = (
+all = (
+    "config",
     "emulate",
     "thought",
-    "example", 
-    "save_examples",
-    "load_training_example",
-    "TrainingSet",
-    "config", 
-    "Model", 
-    "DefaultManager",
-    "suggest",
-    "predict"
+    "example",
+    "thinkof",
+    "ask",
+    "EMULATE_PROMPT",
+    "predict",
+    "ModelSchema",
+    "ArchitectureType",
+    "print_last_prompt"
 )
-
diff --git a/src/OpenHosta/__init__.py b/src/OpenHosta/__init__.py
index da9ccef..4f70251 100644
--- a/src/OpenHosta/__init__.py
+++ b/src/OpenHosta/__init__.py
@@ -1,3 +1,3 @@
 from .OpenHosta import *
 
-__version__ = "1.2.1"
+__version__ = "2.0-beta1"
diff --git a/src/OpenHosta/analytics.py b/src/OpenHosta/analytics.py
deleted file mode 100644
index 9b59686..0000000
--- a/src/OpenHosta/analytics.py
+++ /dev/null
@@ -1,127 +0,0 @@
-import tiktoken
-from enum import Enum
-import time as t
-import sys
-import json
-
-from .config import Model, DefaultManager
-from .prompt import PromptMananger
-
-_x = PromptMananger()
-
-_estimate_prompt = _x.get_prompt("estimate")
-
-l_default = DefaultManager.get_default_model()
-
-
-class ModelAnalizer(Model):
-
-    _default_input_cost: int = 0.005
-    _default_output_cost: int = 0.015
-    _default_token_perSec = 63.32
-    _default_latency = 0.48
-
-    def __init__(
-        self,
-        name: str,
-        input_cost: float,
-        output_cost: float,
-        latency: float,
-        token_perSec: float,
-    ):
-        self.name = self._default_name if name is None else name
-        self.input_cost = self._default_input_cost if input_cost is None else input_cost
-        self.output_cost = (
-            self._default_output_cost if output_cost is None else output_cost
-        )
-        self.latency = self._default_latency if latency is None else latency
-        self.token_perSec = (
-            self._default_token_perSec if token_perSec is None else token_perSec
-        )
-        self.tokenizer = tiktoken.get_encoding("cl100k_base")
-
-    def get_input_cost(self):
-        return self.input_cost
-
-    def get_output_cost(self):
-        return self.output_cost
-
-    def get_latency(self):
-        return self.latency
-
-    def get_token_perSec(self):
-        return self.token_perSec
-
-    def _estimate_output_token(self, function_doc: str, function_call: str):
-        global _estimate_prompt, l_default
-
-        try:
-            if not _estimate_prompt:
-                raise ValueError("ValueError -> emulate empty values")
-        except ValueError as v:
-            sys.stderr.write(f"[ESTIMATE_ERROR]: {v}")
-            return None
-
-        l_user_prompt = (
-            "\n Here's the fonction documentation:\n"
-            + f"{function_doc}\n"
-            + "Here's the function call:\n"
-            + f"{function_call}\n"
-        )
-
-        response = l_default._api_call(
-            sys_prompt=_estimate_prompt,
-            user_prompt=l_user_prompt,
-            creativity=0.2,
-            diversity=0.2,
-        )
-
-        if response.status_code == 200:
-            data = response.json()
-            json_string = data["choices"][0]["message"]["content"]
-            try:
-                l_ret_data = json.loads(json_string)
-
-            except json.JSONDecodeError as e:
-                sys.stderr.write(f"JSONDecodeError: {e}")
-                l_cleand = "\n".join(json_string.split("\n")[1:-1])
-                l_ret_data = json.loads(l_cleand)
-
-            l_ret = l_ret_data["tokens"]
-        else:
-            sys.stderr.write(f"Error {response.status_code}: {response.text}")
-            l_ret = None
-
-        return l_ret
-
-    def _compute_request_cost(self, input_text, output_token):
-        input_tokens = self.tokenizer.encode(input_text)
-        num_input_tokens = len(input_tokens)
-        num_output_tokens = output_token
-        cost_input = (num_input_tokens / 1000) * self.input_cost
-        cost_output = (num_output_tokens / 1000) * self.output_cost
-        total_cost = cost_input + cost_output
-        return total_cost
-
-    def _compute_request_duration(self, output_token):
-        total = self.latency
-        total += self.token_perSec / output_token
-        total += 0.5  # Processing duration margin
-        return total
-
-
-def request_timer(func):
-    def wrapper(*args, **kwargs):
-        g_c = "\033[94m"
-        n = "\033[0m"
-        bold = "\033[1m"
-
-        start = t.time()
-        rv = func(*args, **kwargs)
-        end = t.time()
-
-        duration = end - start
-        print(f"{g_c}{bold}Execution time of {func.__name__}: {duration:.2f}s{n}")
-        return rv
-
-    return wrapper
diff --git a/src/OpenHosta/build.py b/src/OpenHosta/build.py
deleted file mode 100644
index 9b56069..0000000
--- a/src/OpenHosta/build.py
+++ /dev/null
@@ -1,10 +0,0 @@
-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
deleted file mode 100644
index 304e9dd..0000000
--- a/src/OpenHosta/builder.py
+++ /dev/null
@@ -1,57 +0,0 @@
-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
deleted file mode 100644
index 23896c5..0000000
--- a/src/OpenHosta/cache.py
+++ /dev/null
@@ -1,200 +0,0 @@
-import pickle
-import os
-import hashlib
-import inspect
-from typing import Callable, Dict, Any, get_origin, get_args
-import typing
-import collections
-from pydantic import BaseModel, create_model
-
-
-CACHE_DIR = "__hostacache__"
-os.makedirs(CACHE_DIR, exist_ok=True)
-
-
-class Hostacache:
-    def __init__(self, func, cache_id=None, value=None) -> None:
-        self.func = func
-        self.cache_id = cache_id
-        self.value = value
-        self.infos_cache = {
-            "hash_function": "",
-            "function_def": "",
-            "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 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.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 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)
-        )
-
-        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"],
-            self.infos_cache["ho_cothougt_id"],
-        )
-        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)
-
-        func_name = func.__name__
-        func_params = ", ".join(
-            [
-                (
-                    f"{param_name}: {param.annotation.__name__}"
-                    if param.annotation != inspect.Parameter.empty
-                    else param_name
-                )
-                for param_name, param in sig.parameters.items()
-            ]
-        )
-        func_return = (
-            f" -> {sig.return_annotation.__name__}"
-            if sig.return_annotation != inspect.Signature.empty
-            else ""
-        )
-        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
-
-    def _inspect_returnType(self, func: Callable) -> str:
-        sig = inspect.signature(func)
-
-        if sig.return_annotation != inspect.Signature.empty:
-            return sig.return_annotation
-        else:
-            return None
-
-    def _get_typingOrigin(self, return_type) -> bool:
-        origin = get_origin(return_type)
-        return origin in {
-            list,
-            dict,
-            tuple,
-            set,
-            frozenset,
-            typing.Union,
-            typing.Optional,
-            typing.Literal,
-            collections.deque,
-            collections.abc.Iterable,
-            collections.abc.Sequence,
-            collections.abc.Mapping,
-        }
-
-    def _get_functionReturnType(self, func: Callable) -> Dict[str, Any]:
-        return_caller = self._inspect_returnType(func)
-        return_type = None
-
-        if return_caller is not None:
-            if self._get_typingOrigin(return_caller):
-                return_caller_origin = get_origin(return_caller)
-                return_caller_args = get_args(return_caller)
-                combined = return_caller_origin[return_caller_args]
-                new_model = create_model(
-                    "Hosta_return_shema", return_hosta_type_typing=(combined, ...)
-                )
-                return_type = new_model.model_json_schema()
-            elif issubclass(return_caller, BaseModel):
-                return_type = return_caller.model_json_schema()
-            else:
-                new_model = create_model(
-                    "Hosta_return_shema", return_hosta_type=(return_caller, ...)
-                )
-                return_type = new_model.model_json_schema()
-        else:
-            No_return_specified = create_model(
-                "Hosta_return_shema", return_hosta_type_any=(Any, ...)
-            )
-            return_type = No_return_specified.model_json_schema()
-        return return_type, return_caller
\ No newline at end of file
diff --git a/src/OpenHosta/config.py b/src/OpenHosta/config.py
deleted file mode 100644
index 2546623..0000000
--- a/src/OpenHosta/config.py
+++ /dev/null
@@ -1,202 +0,0 @@
-import sys
-import requests
-import json
-import re
-from jsonschema import validate
-from pydantic import BaseModel
-from typing import get_origin, get_args
-
-from .errors import ApiKeyError
-
-
-def is_valid_url(url):
-    regex = re.compile(
-        r"^(?:http|ftp)s?://"
-        r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|"
-        r"localhost|"
-        r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|"
-        r"\[?[A-F0-9]*:[A-F0-9:]+\]?)"
-        r"(?::\d+)?"
-        r"(?:/?|[/?]\S+)$",
-        re.IGNORECASE,
-    )
-    return re.match(regex, url) is not None
-
-
-class Model:
-
-    _SYS_PROMPT = ""
-
-    def __init__(self, model: str = None, base_url: str = None, api_key: str = None):
-        self.model = model
-        self.base_url = base_url
-        self.api_key = api_key
-        self._last_request = None
-        
-        self.conversion_function = {
-            str: lambda x: str(x),
-            int: lambda x: int(x),
-            float: lambda x: float(x),
-            list: lambda x: list(x),
-            set: lambda x: set(x),
-            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)):
-            sys.stderr.write(f"[CONFIG_ERROR] Empty values.")
-            return
-        elif not is_valid_url(self.base_url):
-            sys.stderr.write(f"[CONFIG_ERROR] Invalid URL.")
-            return
-
-    def __str__(self) -> str:
-        return (
-            f"[OpenHosta] <{self.__class__.__module__}.{self.__class__.__name__} object at {hex(id(self))}>\n"
-            f"Model: {self.model}\n"
-            f"Base_url: {self.base_url}\n"
-            "Infos:\n"
-        )
-
-    def api_call(
-        self, sys_prompt: str, user_prompt: str, creativity: float, diversity: float
-    )->requests.models.Response:
-        if self.api_key is None or not self.api_key:
-            raise ApiKeyError("Empty API key.")
-
-        l_body = {
-            "model": self.model,
-            "messages": [
-                {
-                    "role": "system",
-                    "content": [{"type": "text", "text": sys_prompt}],
-                },
-                {
-                    "role": "user",
-                    "content": [{"type": "text", "text": user_prompt}],
-                },
-            ],
-            "response_format": {"type": "json_object"},
-            "temperature": creativity if creativity is not None else 0.7,
-            "top_p": diversity if diversity is not None else 0.7,
-        }
-        headers = {
-            "Content-Type": "application/json",
-            "Authorization": f"Bearer {self.api_key}",
-        }
-        self._last_request = l_body        
-
-        try:
-            response = requests.post(self.base_url, json=l_body, headers=headers)
-            response.raise_for_status()
-        except requests.exceptions.RequestException as e:
-            sys.stderr.write(f"\n[CALL_ERROR] Request failed:\n{e}\n\n")
-        if response.status_code != 200:
-            if "invalid_api_key" in str(response.text):
-                raise ApiKeyError("Incorrect API key.")
-            else:    
-                sys.stderr.write(f"[CALL_ERROR] API call was unsuccessful.\nStatus code: {response.status_code}:\n{response.text}")
-        return response
-
-    def request_handler(self, response, return_type, return_caller):
-        l_ret = None
-
-        data = response.json()
-        json_string = data["choices"][0]["message"]["content"]
-
-        try:
-            l_ret_data = json.loads(json_string)
-            # validate(
-            #     instance=l_ret_data.get("return", {}),
-            #     schema=return_type.get("properties", {}),
-            # )                                                 # REFACTO
-        except json.JSONDecodeError as e:
-            sys.stderr.write(f"JSONDecodeError: {e}")
-            l_cleand = "\n".join(json_string.split("\n")[1:-1])
-            l_ret_data = json.loads(l_cleand)
-            l_ret = l_ret_data["return"]
-            return l_ret
-
-        if "return_hosta_type" in return_type["properties"]:
-            if return_caller in self.conversion_function:
-                convert_function = self.conversion_function[return_caller]
-                if l_ret_data["return"] is not None:
-                    l_ret = convert_function(l_ret_data["return"])
-            else:
-                l_ret = l_ret_data["return"]
-
-        elif "return_hosta_type_typing" in return_type["properties"]:
-            l_ret = self.convert_to_type(l_ret_data["return"], return_caller)
-
-        elif "return_hosta_type_any" in return_type["properties"]:
-            l_ret = l_ret_data["return"]
-
-        elif issubclass(return_caller, BaseModel):
-            try:
-                l_ret = return_caller(**l_ret_data["return"])
-            except:
-                sys.stderr.write("Unable t parse answer: ", l_ret_data["return"])
-                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"])
-                l_ret = None
-
-        else:
-            l_ret = l_ret_data["return"]
-
-        return l_ret
-
-    def convert_to_type(self, data, type):
-        origin = get_origin(type)
-        args = get_args(type)
-
-        if origin != None:
-            if origin in self.conversion_function:
-                convert_function = self.conversion_function[origin]
-                return convert_function(self.convert_to_type(d, args[0]) for d in data)
-
-        return data
-
-
-class DefaultModel:
-    _instance = None
-
-    def __new__(cls, *args, **kwargs):
-        if cls._instance is None:
-            cls._instance = super(DefaultModel, cls).__new__(cls, *args, **kwargs)
-        return cls._instance
-
-    def __init__(self):
-        if not hasattr(self, "model"):
-            self.model = None
-
-    def set_default_model(self, new):
-        if isinstance(new, Model):
-            self.model = new
-        else:
-            sys.stderr.write("[CONFIG_ERROR] Invalid model instance.\n")
-
-    def set_default_apiKey(self, api_key=None):
-        if api_key is not None or isinstance(api_key, str):
-            self.model.api_key = api_key
-        else:
-            sys.stderr.write("[CONFIG_ERROR] Invalid API key.")
-
-    def get_default_model(self):
-        return self.model
-
-
-DefaultManager = DefaultModel()
-
-
-def set_default_model(new):
-    DefaultManager.set_default_model(new)
-
-
-def set_default_apiKey(api_key=None):
-    DefaultManager.set_default_apiKey(api_key)
-
-
-__all__ = Model, set_default_apiKey, set_default_model, DefaultManager
diff --git a/src/OpenHosta/core/__init__.py b/src/OpenHosta/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/OpenHosta/core/analizer.py b/src/OpenHosta/core/analizer.py
new file mode 100644
index 0000000..127d95e
--- /dev/null
+++ b/src/OpenHosta/core/analizer.py
@@ -0,0 +1,295 @@
+from __future__ import annotations
+
+from typing import (
+    Any,
+    Dict,
+    get_args,
+    get_origin,
+    Union,
+    List,
+    Tuple,
+    Mapping,
+    Sequence,
+    Collection,
+    Literal,
+    Final,
+    Type,
+    Annotated,
+    ClassVar,
+    Protocol,
+    AnyStr,
+    ByteString,
+    Set,
+    FrozenSet,
+    AbstractSet,
+    Optional,
+    Callable,
+    OrderedDict,
+    TypeVar
+)
+from types import MethodType, NoneType
+import inspect
+from types import FrameType
+from typing import Callable, Tuple, List, Dict, Any, Optional, Type
+
+from ..utils.import_handler import is_pydantic
+
+all = (
+    "FuncAnalizer"
+)
+
+
+class FuncAnalizer:
+    """
+    A class for inspecting the signature, definition, and call information of a function.
+
+    Args:
+        func (Callable): The function to inspect.
+
+    Attributes:
+        func (Callable): The function to inspect.
+        sig (Signature): The signature of the function.
+    """
+
+    def __init__(self, func: Union[Callable, MethodType] | None, caller_frame: FrameType | None):
+        """
+        Initialize the function inspector with the given function.
+
+        Args:
+            func (Callable): The function to inspect.
+            caller_frame (FrameType): The function's frame in which it is called.
+        """
+        try:
+            self.func = func
+            self.sig = inspect.signature(func)
+            _, _, _, self.values = inspect.getargvalues(caller_frame)
+        except Exception as e:
+            raise AttributeError(
+                f"[_FuncInspector.__init__] Invalid arguments:\n{e}")
+
+    @property
+    def func_def(self) -> str:
+        """
+        Build the string representing the function's definition.
+
+        Returns:
+            The string representing the function's definition.
+        """
+        func_name = self.func.__name__
+        func_params = ", ".join(
+            [
+                (
+                    f"{param_name}: {param.annotation.__name__}"
+                    if param.annotation != inspect.Parameter.empty
+                    else param_name
+                )
+                for param_name, param in self.sig.parameters.items()
+            ]
+        )
+        func_return = (
+            f" -> {self.sig.return_annotation.__name__}"
+            if self.sig.return_annotation != inspect.Signature.empty
+            else ""
+        )
+        definition = (
+            f"```python\ndef {func_name}({func_params}):{func_return}\n"
+            f"    \"\"\"\n\t{self.func.__doc__}\n    \"\"\"\n```"
+        )
+        return definition
+
+    @property
+    def func_locals(self) -> Tuple[Optional[Dict[str, Any]], Any]:
+        """
+        Get the attributs and local variables of the function call.
+
+        Returns:
+            A tuple containing the bound arguments, local variables, and local attributs.
+        """
+        values_locals = {k: v for k, v in self.values.items()
+                         if k not in self.sig.parameters}
+        values_locals.pop('self', None)
+
+        values_self = None
+        if hasattr(self.func, '__self__'):
+            values_self = getattr(self.func.__self__, '__dict__', None)
+        return values_locals or None, values_self
+
+    @property
+    def func_call(self) -> Tuple[str, OrderedDict[str, Any]]:
+        """
+        Build a string representing the function call.
+
+        Returns:
+            A tuple containing a string representing the function call and the bound arguments.
+        """
+        values_args = {k: v for k, v in self.values.items()
+                       if k in self.sig.parameters}
+
+        bound_args = self.sig.bind_partial(**values_args)
+        bound_args.apply_defaults()
+
+        bound_arguments = bound_args.arguments
+
+        args_str = ", ".join(
+            f"{k}={v!r}" for k, v in bound_arguments.items()
+        )
+        call_str = f"{self.func.__name__}({args_str})"
+        return call_str, bound_arguments
+
+    @property
+    def func_type(self) -> Tuple[List[Any], Any]:
+        """
+        Get the _inputs and _outputs types of the function.
+
+        Returns:
+            A tuple containing the _inputs types and _outputs type.
+        """
+        input_types = [
+            param.annotation for param in self.sig.parameters.values()
+        ]
+        output_type = self.sig.return_annotation if self.sig.return_annotation != inspect.Signature.empty else None
+        return input_types, output_type
+
+    def _get_type_schema(self, tp: Any) -> Dict[str, Any]:
+        """
+        Generate a JSON schema for a given type.
+
+        Args:
+            tp: The type to generate the schema for.
+
+        Returns:
+            The JSON schema for the type.
+        """
+        if tp == Any:
+            return {"type": "any"}
+
+        origin = get_origin(tp)
+        args = get_args(tp)
+
+        if origin in (Union, Optional):
+            if len(args) == 2 and (NoneType in args or type(None) in args):
+                main_type = args[0] if args[1] in (
+                    NoneType, type(None)) else args[1]
+                return {
+                    "anyOf": [
+                        self._get_type_schema(main_type),
+                        {"type": "null"}
+                    ]
+                }
+            return {"anyOf": [self._get_type_schema(arg) for arg in args]}
+
+        if origin in (list, List, Sequence, Collection):
+            return {
+                "type": "array",
+                "items": self._get_type_schema(args[0]) if args else {"type": "any"}
+            }
+
+        if origin in (dict, Dict, Mapping):
+            return {
+                "type": "object",
+                "additionalProperties": self._get_type_schema(args[1]) if args else {"type": "any"}
+            }
+
+        if origin is Final:
+            return self._get_type_schema(args[0]) if args else {"type": "any"}
+
+        if origin is Type:
+            return {"type": "object", "format": "type"}
+
+        if origin is ClassVar:
+            return self._get_type_schema(args[0]) if args else {"type": "any"}
+
+        if origin is Annotated:
+            return self._get_type_schema(args[0])
+
+        if origin is Literal:
+            return {"enum": list(args)}
+
+        if origin in (tuple, Tuple):
+            if len(args) == 2 and args[1] is ...:
+                return {
+                    "type": "array",
+                    "items": self._get_type_schema(args[0])
+                }
+            return {
+                "type": "array",
+                "prefixItems": [self._get_type_schema(arg) for arg in args],
+                "items": False
+            }
+
+        if origin in (set, Set, frozenset, FrozenSet, AbstractSet):
+            return {
+                "type": "array",
+                "uniqueItems": True,
+                "items": self._get_type_schema(args[0]) if args else {"type": "any"}
+            }
+
+        if hasattr(tp, "__annotations__") and isinstance(tp, type) and hasattr(tp, "__total__"):
+            properties = {
+                key: self._get_type_schema(value)
+                for key, value in tp.__annotations__.items()
+            }
+            required = [key for key in properties.keys()] if getattr(
+                tp, "__total__", True) else []
+            return {
+                "type": "object",
+                "properties": properties,
+                "required": required,
+                "additionalProperties": False
+            }
+
+        if isinstance(tp, TypeVar):
+            if tp.__constraints__:
+                return {
+                    "anyOf": [
+                        self._get_type_schema(constraint)
+                        for constraint in tp.__constraints__
+                    ]
+                }
+            elif tp.__bound__:
+                return self._get_type_schema(tp.__bound__)
+            else:
+                return {"type": "any"}
+
+        if isinstance(tp, type) and issubclass(tp, Protocol):
+            return {"type": "object", "format": "protocol"}
+
+        if tp is int:
+            return {"type": "integer"}
+        if tp is float:
+            return {"type": "number"}
+        if tp is str or tp is AnyStr:
+            return {"type": "string"}
+        if tp is list:
+            return {"type": "list"}
+        if tp is dict:
+            return {"type": "dict"}
+        if tp is bool:
+            return {"type": "boolean"}
+        if tp is NoneType or tp is None:
+            return {"type": "null"}
+        if tp is bytes or tp is bytearray or tp is ByteString:
+            return {"type": "string", "format": "binary"}
+
+        raise ValueError(f"[_get_type_schema] Unsupported type: {tp}")
+
+    @property
+    def func_schema(self) -> Dict[str, Any]:
+        """
+        Get the JSON schema of the function's return type.
+
+        Returns:
+            The JSON schema of the function's return type.
+        """
+        return_caller = self.sig.return_annotation if self.sig.return_annotation != inspect.Signature.empty else None
+
+        if return_caller is not None:
+            if is_pydantic:
+                from .pydantic_usage import get_pydantic_schema
+
+                pyd_sch = get_pydantic_schema(return_caller)
+                if pyd_sch is not None:
+                    return pyd_sch
+            return self._get_type_schema(return_caller)
+        else:
+            return self._get_type_schema(Any)
diff --git a/src/OpenHosta/core/checker.py b/src/OpenHosta/core/checker.py
new file mode 100644
index 0000000..633af28
--- /dev/null
+++ b/src/OpenHosta/core/checker.py
@@ -0,0 +1,114 @@
+from __future__ import annotations
+
+from types import NoneType
+from typing import Type, Any, Dict, Optional, Callable, TypeVar
+
+from .hosta import Func
+from ..utils.import_handler import is_pydantic
+
+T = TypeVar('T')
+
+
+class HostaChecker:
+    """
+    A class used to check and convert the _outputs of a Language Model (LLM) to the type specified in a function's annotation.
+
+    Args:
+        func (Func): A function object that contains the type annotations for the LLM _outputs.
+        data (dict): A dictionary containing the LLM _outputs data to be checked and converted.
+
+    Attributes:
+        func (Func): The function object containing the type annotations for the LLM _outputs.
+        data (dict): The LLM _outputs data to be checked and converted.
+        checked (Any): The checked and converted data. If `data` contains a "return" key, its value is used as the checked data. Otherwise, `data` is used as the checked data.
+        is_passed (bool): A flag indicating whether the checked data should be converted or not. It is set to True if `data` contains a "return" key.
+    """
+
+    def __init__(self, func: Func, data: dict):
+        self.func = func
+        self.data = data
+        try:
+            self.checked = self.data["return"]
+            self.is_passed = True
+        except KeyError:
+            self.checked = self.data
+            self.is_passed = False
+
+    def _default(self, x: Any) -> Any:
+        """
+        A default conversion function that returns the _inputs as is.
+
+        Args:
+            x (Any): The _inputs data to be converted.
+
+        Returns:
+            Any: The _inputs data as is.
+        """
+        return x
+
+    def convert(self, typ: Type[T]) -> Dict[Type[T], Optional[Callable[[Any], T]]]:
+        """
+        A method to create a conversion function for a given type.
+
+        Args:
+            typ (Type[T]): The type for which a conversion function needs to be created.
+
+        Returns:
+            Dict[Type[T], Optional[Callable[[Any], T]]]: A dictionary mapping types to their corresponding conversion functions.
+        """
+        convertMap = {
+            NoneType: lambda x: None,
+            str: lambda x: str(x),
+            int: lambda x: int(x),
+            float: lambda x: float(x),
+            list: lambda x: list(x),
+            set: lambda x: set(x),
+            frozenset: lambda x: frozenset(x),
+            tuple: lambda x: tuple(x),
+            bool: lambda x: bool(x),
+            dict: lambda x: dict(x),
+            complex: lambda x: complex(x),
+            bytes: lambda x: bytes(x),
+        }
+        if typ not in convertMap.keys():
+            return self._default.__func__
+        return convertMap[typ]
+
+    def convert_annotated(self) -> Any:
+        """
+        A method to convert the checked data based on the type annotations of the function.
+
+        Returns:
+            Any: The converted checked data based on the type annotations.
+        """
+        if getattr(self.func.f_type[1], '__module__', None) == 'typing':
+            pass  # Make a deep checking when annotated (see below)
+        return self.checked
+        #     origin = get_origin(self.func.f_type[1])
+        #     args = get_args(self.func.f_type[1])
+
+        #     if origin != None:
+        #         if origin in self.convert:
+        #             convert_function = self.convert[origin]
+        #             return convert_function(self.convert_to_type(d, args[0]) for d in self.checked)
+        #     return self.checked
+        # else:
+        #     return self.checked
+
+    def check(self) -> Any:
+        """
+        A method to check and convert the _inputs data based on the function's type annotations and Pydantic model annotations.
+
+            Returns:
+                Any: The checked and converted data. If `data` contains a "return" key, its value is used as the checked data. Otherwise, `data` is used as the checked data.
+        """
+        if self.checked == "None" or self.checked is None:
+            return None
+        if self.is_passed:
+            self.checked = self.convert(self.func.f_type[1])(self.checked)
+            self.checked = self.convert_annotated()
+            if is_pydantic:
+                from .pydantic_usage import convert_pydantic
+
+                self.checked = convert_pydantic(self.func.f_type[1], self.checked)
+        return self.checked
diff --git a/src/OpenHosta/core/config.py b/src/OpenHosta/core/config.py
new file mode 100644
index 0000000..65284b8
--- /dev/null
+++ b/src/OpenHosta/core/config.py
@@ -0,0 +1,158 @@
+from __future__ import annotations
+
+import sys
+import json
+import re
+import sys
+import requests
+from typing import Any, Dict
+
+from .checker import HostaChecker
+from .pydantic_usage import Func
+from ..utils.errors import ApiKeyError, RequestError
+
+
+def is_valid_url(url: str) -> bool:
+    regex = re.compile(
+        r"^(?:http|ftp)s?://"
+        r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|"
+        r"localhost|"
+        r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|"
+        r"\[?[A-F0-9]*:[A-F0-9:]+\]?)"
+        r"(?::\d+)?"
+        r"(?:/?|[/?]\S+)$",
+        re.IGNORECASE,
+    )
+    return re.match(regex, url) is not None
+
+
+class Model:
+
+    _SYS_PROMPT = ""
+
+    def __init__(self, model: str = None, base_url: str = None, api_key: str = None):
+        self.model = model
+        self.base_url = base_url
+        self.api_key = api_key
+        self._last_request = None
+        self._used_tokens = 0
+        self._nb_requests = 0
+
+        if any(var is None for var in (model, base_url)):
+            raise ValueError(f"[Model.__init__] Missing values.")
+        elif not is_valid_url(self.base_url):
+            raise ValueError(f"[Model.__init__] Invalid URL.")
+
+    def simple_api_call(
+        self,
+        sys_prompt: str,
+        user_prompt: str,
+        json_form: bool = True,
+        **llm_args
+    ) -> Dict:
+        return self.api_call(
+            [
+                {"role": "system", "content": sys_prompt},
+                {"role": "user", "content": user_prompt}
+            ],
+            json_form,
+            **llm_args
+        )
+
+    def api_call(
+        self,
+        messages: list[dict[str, str]],
+        json_form: bool = True,
+        **llm_args
+    ) -> Dict:
+        if self.api_key is None or not self.api_key:
+            raise ApiKeyError("[model.api_call] Empty API key.")
+
+        l_body = {
+            "model": self.model,
+            "messages": messages,
+        }
+        headers = {
+            "Content-Type": "application/json"
+        }
+        
+        if "azure.com" in self.base_url:
+            headers |= {"api-key": f"{self.api_key}"}
+        else:
+            headers |= {"Authorization": f"Bearer {self.api_key}"}
+ 
+        if json_form:
+            l_body["response_format"] = {"type": "json_object"}
+        for key, value in llm_args.items():
+            l_body[key] = value
+        try:
+            response = requests.post(self.base_url, headers=headers, json=l_body, timeout=30)
+
+            if response.status_code  != 200:
+                response_text = response.text
+                if "invalid_api_key" in response_text:
+                    raise ApiKeyError("[Model.api_call] Incorrect API key.")
+                else:
+                    raise RequestError(
+                        f"[Model.api_call] API call was unsuccessful.\n"
+                        f"Status code: {response.status_code }:\n{response_text}"
+                    )
+            self._nb_requests += 1
+            return response.json()
+        except Exception as e:
+            raise RequestError(f"[Model.api_call] Request failed:\n{e}\n\n")
+
+    def request_handler(self, response: Dict, func: Func) -> Any:
+        json_string = response["choices"][0]["message"]["content"]
+        if "usage" in response:
+            self._used_tokens += int(response["usage"]["total_tokens"])
+        try:
+            l_ret_data = json.loads(json_string)
+        except json.JSONDecodeError as e:
+            sys.stderr.write(
+                f"[Model.request_handler] JSONDecodeError: {e}\nContinuing the process.")
+            l_cleand = "\n".join(json_string.split("\n")[1:-1])
+            l_ret_data = json.loads(l_cleand)
+        return HostaChecker(func, l_ret_data).check()
+
+
+class DefaultModel:
+    _instance = None
+
+    def __new__(cls, *args, **kwargs):
+        if cls._instance is None:
+            cls._instance = super(DefaultModel, cls).__new__(
+                cls, *args, **kwargs)
+        return cls._instance
+
+    def __init__(self):
+        if not hasattr(self, "model"):
+            self.model = None
+
+    def set_default_model(self, new):
+        if isinstance(new, Model):
+            self.model = new
+        else:
+            sys.stderr.write("[CONFIG_ERROR] Invalid model instance.\n")
+
+    def set_default_apiKey(self, api_key=None):
+        if api_key is not None or isinstance(api_key, str):
+            self.model.api_key = api_key
+        else:
+            sys.stderr.write("[CONFIG_ERROR] Invalid API key.")
+
+    def get_default_model(self):
+        return self.model
+
+
+DefaultManager = DefaultModel()
+
+
+def set_default_model(new):
+    DefaultManager.set_default_model(new)
+
+
+def set_default_apiKey(api_key=None):
+    DefaultManager.set_default_apiKey(api_key)
+
+# anntations, errors handling, doc, all
diff --git a/src/OpenHosta/core/hosta.py b/src/OpenHosta/core/hosta.py
new file mode 100644
index 0000000..37933b7
--- /dev/null
+++ b/src/OpenHosta/core/hosta.py
@@ -0,0 +1,217 @@
+from __future__ import annotations
+
+from typing import List, Optional
+
+from .analizer import FuncAnalizer
+from .inspector import HostaInspector
+from .pydantic_usage import Func
+from ..utils.errors import InvalidStructureError
+from ..utils.hosta_type import MemoryNode, MemKey, MemValue, ExampleType, CotType
+import hashlib
+
+all = (
+    "Hosta",
+    "Func",
+    "UseType",
+    "MemKey",
+    "MemValue",
+    "MemoryNode"
+)
+
+
+class Hosta(HostaInspector):
+    """
+    Hosta is a class that extends HostaInspector and provides functionality for analyzing
+    and storing information about the calling function.
+
+    This class implements a singleton pattern and uses introspection to gather details
+    about the callable that called the function that instantiated it.
+
+    If many function instantiated it in the same callable, the first function will create
+    an instance and attach it to the callable so that the next function can retrieve it.
+
+    Attributes:
+        _initialized (bool): Flag to track if the instance has been initialized.
+        _obj (tuple): Stores the result of the _extend method call.
+        _infos (Func): Stores detailed information about the analyzed function.
+    """
+    _initialized = False
+    _obj = None
+
+    def __new__(cls, *args, **kwargs) -> 'Hosta':
+        """
+        Create a new instance of Hosta or return the existing one if already created.
+
+        This method implements the singleton pattern, ensuring only one instance of Hosta exists.
+        It also handles the initialization of the instance when first created.
+
+        Args:
+            *args: Variable length argument list.
+            **kwargs: Arbitrary keyword arguments.
+            > it's the arguments passed to __init__()
+
+        Returns:
+            Hosta: The single instance of the Hosta class.
+        """
+        cls._obj = cls._extend()
+        if cls._obj[0] is None:
+            raise InvalidStructureError(
+                "[Hosta.__new__] The function {} must be called in a function/method."
+                .format(cls._extend(back_level=2)[0].__name__)
+            )
+        if (hasattr(cls._obj[0], "Hosta")):
+            return cls._obj[0].Hosta
+        instance = super().__new__(cls)
+        cls._attach(cls._obj[0], {"Hosta": instance})
+        return instance
+
+    def __init__(self, *, caller_analysis: bool = True):
+        """
+        Initialize the Hosta instance.
+
+        This method is called after __new__ and sets up the instance attributes.
+        It also triggers the function analysis if caller_analysis is True.
+
+        Args:
+            caller_analysis (bool): If True, analyze the calling function. Defaults to True.
+        """
+        if not self._initialized:
+            super().__init__()
+            self._initialized = True
+            self._infos = Func()
+            if caller_analysis:
+                self._get_infos_func()
+
+    def _get_infos_func(self) -> None:
+        """
+        Analyze and store information about the calling function.
+
+        This method uses FuncAnalizer to extract various details about the function
+        that called the Hosta instance, including its name, definition, call signature,
+        arguments, types, and local variables. This informations are useful in all OpenHosta's function.
+
+        The extracted information is stored in the _infos attribute of the instance.
+        """
+        analizer = FuncAnalizer(self._obj[0], self._obj[1])
+        self._infos.f_obj = self._obj[0]
+        self._infos.f_name = self._obj[0].__name__
+        self._infos.f_doc = self._obj[0].__doc__
+        self._infos.f_def = analizer.func_def
+        self._infos.f_call, self._infos.f_args = analizer.func_call
+        self._infos.f_type = analizer.func_type
+        self._infos.f_locals, self._infos.f_self = analizer.func_locals
+        self._infos.f_schema = analizer.func_schema
+        self._infos.f_sig = analizer.sig
+        
+    def _update_call(self):
+        analizer = FuncAnalizer(self._obj[0], self._obj[1])
+        self._infos.f_call, self._infos.f_args = analizer.func_call
+        return self
+        
+
+    def _bdy_add(self, key: MemKey, value: MemValue) -> None:
+        """
+        Add a new memory node to the function's memory.
+
+        This method creates a new MemoryNode with the given key and value,
+        and appends it to the function's memory list. If the memory list
+        doesn't exist, it initializes it.
+
+        Args:
+            key (MemKey): The type of memory node ('ex', 'cot', or 'use').
+            value (MemValue): The value to be stored in the memory node.
+        """
+        seen: List[MemKey] = []
+        previous: MemKey = None
+
+        if self._infos.f_mem is None:
+            self._infos.f_mem = []
+            id = 0
+        else:
+            id = 0
+            for node in self._infos.f_mem:
+                if node.key == key:
+                    id += 1
+        new = MemoryNode(key=key, id=id, value=value)
+        self._infos.f_mem.append(new)
+        previous = new
+        for node in self._infos.f_mem:
+            if node.key not in seen:
+                seen.append(node.key)
+                previous = node
+            elif node.key in seen and node.key == previous.key:
+                previous = node
+            else:
+                raise InvalidStructureError(
+                    "[Hosta._bdy_add] Inconsistent function structure. Place your OpenHosta functions per block.")
+
+    def _bdy_get(self, key: MemKey) -> List[MemoryNode]:
+        """
+        Retrieve memory nodes of a specific type from the function's memory.
+
+        This method searches through the function's memory list and returns
+        all nodes that match the given key.
+
+        Args:
+            key (MemKey): The type of memory node to retrieve ('ex', 'cot', or 'use').
+
+        Returns:
+            List[MemoryNode]: A list of memory nodes matching the key, or None if no matches are found.
+        """
+        l_list: List[MemoryNode] = []
+
+        if self._infos.f_mem is None:
+            return None
+        for node in self._infos.f_mem:
+            if node.key == key:
+                l_list.append(node)
+        return l_list if l_list != [] else None
+
+    @property
+    def example(self) -> Optional[List[ExampleType]]:
+        """
+        Retrieve all example nodes from the function's memory.
+
+        This property method uses _bdy_get to fetch all memory nodes with the 'ex' key.
+
+        Returns:
+            Optional[List[ExampleType]]: A list of example nodes, or None if no examples are found.
+        """
+        nodes = self._bdy_get(key="ex")
+        return [node.value for node in nodes] if nodes else None
+
+    @property
+    def cot(self) -> Optional[List[CotType]]:
+        """
+        Retrieve all chain-of-thought (cot) nodes from the function's memory.
+
+        This property method uses _bdy_get to fetch all memory nodes with the 'cot' key.
+
+        Returns:
+            Optional[List[CotType]]: A list of chain-of-thought nodes, or None if no cot nodes are found.
+        """
+        nodes = self._bdy_get(key="cot")
+        return [node.value for node in nodes] if nodes else None
+
+    @property
+    def infos(self):
+        return self._infos
+
+    @staticmethod
+    def hash_func(func: Func) -> str:
+        """
+        Generate a hash value for a function without use builtin python hash function.
+
+        This method generates a hash value for a function using a custom algorithm
+        Hashed by func.f_doc, func.f_def, func.f_call, func.f_args, func.f_type, func.f_schema, func.f_locals, func.f_self
+
+        Args:
+            func (object): The function to hash.
+
+        Returns:
+            str: The hash value of the function.
+        """
+        return hashlib.md5(
+            str(func.f_def).encode() +
+            str(func.f_type).encode()
+        ).hexdigest()
diff --git a/src/OpenHosta/core/inspector.py b/src/OpenHosta/core/inspector.py
new file mode 100644
index 0000000..ffc6819
--- /dev/null
+++ b/src/OpenHosta/core/inspector.py
@@ -0,0 +1,166 @@
+from __future__ import annotations
+
+from typing import Tuple, Callable, Optional, Dict, Any, Union
+from types import FrameType, CodeType, MethodType
+import inspect
+
+from ..utils.errors import FrameError
+
+all = (
+    "HostaInspector"
+)
+
+
+class HostaInspector:
+    """
+    This class is the parent class for a lot of OpenHosta functionnality.
+    It provides methods which are used in many cases.
+    """
+
+    def __init__(self):
+        pass
+
+    @staticmethod
+    def _extend(*, back_level: int = 3) -> Tuple[Union[Callable, MethodType], FrameType]:
+        """
+        Retrieves the callable object and the frame from which this method is called.
+
+        This method navigates up the call stack to find the function or method that called it.
+        It can retrieve the callable object from both class methods and standalone functions.
+
+        This method uses introspection to examine the call stack and local variables.
+        It may not work correctly in all execution environments or with all types of callable objects.
+
+        Args:
+            - back_level (int, optional): The number of frames to go back in the call stack. 
+                Defaults to 2. Must be a non-zero positive integer.
+
+        Returns:
+            Tuple[Callable, FrameType]: A tuple containing:
+                - The callable object (function or method) that called this method.
+                - The frame object of the caller.
+        """
+        if back_level <= 0 or not isinstance(back_level, int):
+            raise ValueError(
+                f"[HostaInspector._extend] back_level must a non-zero positive integers.")
+
+        def _get_obj_from_class(caller: FrameType) -> Optional[Callable]:
+            """
+            Search for the callable object when it is called within a class method.
+
+            This function attempts to retrieve the method from the 'self' object
+            in the caller's local variables. It's designed for internal use only, 
+            within the _extend method.
+
+            Args:
+                caller (FrameType): The frame object of the calling method.
+
+            Returns:
+                Callable: The unwrapped method object if found, otherwise None.
+            """
+            func: Union[Callable, MethodType]
+
+            obj = caller.f_locals["self"]
+            func = getattr(obj, caller_name, None)
+            return inspect.unwrap(func) if func else None
+
+        def _get_obj_from_func(
+            caller: FrameType,
+            code: CodeType,
+            name: str
+        ) -> Optional[Callable]:
+            """
+            Search for the callable object when it is called within a function.
+
+            This function traverses the call stack, examining local and global variables
+            to find the function that matches the given code object. It's designed for
+            internal use only, within the _extend method.
+
+            Args:
+                - caller (FrameType): The frame object of the calling function.
+                - code (CodeType): The code object of the calling function.
+                - name (str): The name of the calling function.
+
+            Returns:
+                Callable: The unwrapped function object if found, otherwise None.
+            """
+            func: Union[Callable, MethodType]
+            l_caller: FrameType = caller
+
+            while not l_caller.f_back is None:
+                for obj in l_caller.f_back.f_locals.values():
+                    try:
+                        if hasattr(obj, "__code__"):
+                            if obj.__code__ == code:
+                                return obj
+                    except:
+                        continue
+                l_caller = l_caller.f_back
+            func = caller.f_globals.get(name)
+            return inspect.unwrap(func) if func else None
+
+        func: Union[Callable, MethodType]
+        current: Optional[FrameType] = inspect.currentframe()
+
+        if current is None:
+            raise FrameError(
+                f"[HostaInspector._extend] Current frame can't be found")
+        for k in range(back_level):
+            current = current.f_back
+            if current is None:
+                raise FrameError(
+                    f"[HostaInspector._extend] Frame can't be found (level: {k})")
+
+        caller = current
+        caller_name = caller.f_code.co_name
+        caller_code = caller.f_code
+        caller_args = inspect.getargvalues(caller)
+
+        is_likely_method = "self" in caller.f_locals or\
+            'cls' in caller.f_locals or\
+            (caller_args.args and caller_args.args[0] in ['self', 'cls'])
+        if is_likely_method:
+            func = _get_obj_from_class(caller)
+        else:
+            func = _get_obj_from_func(caller, caller_code, caller_name)
+
+        if func is not None and not callable(func):
+            raise FrameError(
+                "[HostaInspector._extend] The foud object isn't a callable.")
+
+        return (func, caller)
+
+    @staticmethod
+    def _attach(obj: Callable, attr: Dict[str, Any]) -> Optional[bool]:
+        """
+        Attaches attributes to a function or method.
+
+        This method attempts to add new attributes to a callable object (function or method).
+        For methods, it attaches the attributes to the underlying function object.
+        Only supports attaching to functions and methods. Other callable types will raise an AttributeError.
+
+        Args:
+            - obj (Callable): The function or method to which the attribute will be attached.
+            - attr (Dict[str, Any]): The dictionary of the attributes to attach.
+
+        Return:
+            Optional[bool]: Returns True if the attribute was successfully attached, raise an Exception otherwise. 
+        """
+        if not callable(obj) or not isinstance(attr, dict):
+            raise ValueError("[HostaInspector._attach] Invalid arguments")
+
+        def attr_parser(obj: Callable, attr: Dict[str, Any]) -> None:
+            for key, value in attr.items():
+                setattr(obj, key, value)
+
+        if inspect.ismethod(obj):
+            if hasattr(obj, "__func__"):
+                attr_parser(obj.__func__, attr)
+                return True
+            raise AttributeError(
+                f"[HostaInspector._attach] Failed to attach attributs. \"__func__\" attribut is missing.")
+        elif inspect.isfunction(obj):
+            attr_parser(obj, attr)
+            return True
+        raise AttributeError(
+            f"[HostaInspector._attach] Failed to attach attributs. Object's type not supported: {type(obj)}.")
diff --git a/src/OpenHosta/core/memory.py b/src/OpenHosta/core/memory.py
new file mode 100644
index 0000000..964c211
--- /dev/null
+++ b/src/OpenHosta/core/memory.py
@@ -0,0 +1,32 @@
+import os
+from typing import Optional
+
+class HostaMemory:
+    """
+    Base class for persistent memory management.
+    """
+    _instances = {}
+    CACHE_DIR = "__hostacache__"
+    
+    def __init__(self, base_path: Optional[str] = None, **kwargs):
+        pass
+
+    def __new__(cls, base_path: Optional[str] = None, **kwargs):
+        if cls not in cls._instances:
+            cls._instances[cls] = super(HostaMemory, cls).__new__(cls)
+            cls._instances[cls]._initialized = False
+        if base_path is not None and not cls._instances[cls]._initialized:
+            cls._instances[cls]._initialize(base_path)
+        return cls._instances[cls]
+    
+    def _initialize(self, base_path: str) -> None:
+        """Initializes the hostacache directory"""
+        self.cache_root = os.path.join(base_path, self.CACHE_DIR)
+        self._initialized = True
+        self._ensure_directory_exists(self.cache_root)
+
+
+    def _ensure_directory_exists(self, directory) -> None:
+        """Creates the base directory if necessary"""
+        if not os.path.exists(directory):
+            os.makedirs(directory)
\ No newline at end of file
diff --git a/src/OpenHosta/core/pydantic_usage.py b/src/OpenHosta/core/pydantic_usage.py
new file mode 100644
index 0000000..807e3cc
--- /dev/null
+++ b/src/OpenHosta/core/pydantic_usage.py
@@ -0,0 +1,75 @@
+from __future__ import annotations
+
+import inspect
+from typing import Optional, Dict, Tuple, List, Any, get_args, get_origin
+
+from ..utils.hosta_type import MemoryNode
+from ..utils.import_handler import is_pydantic
+
+if is_pydantic:
+    from pydantic import BaseModel, Field, ConfigDict, create_model
+
+
+    def convert_pydantic(caller, checked) -> Optional[BaseModel]:
+        """
+        A method to convert the checked data based on the Pydantic model annotations of the function.
+
+        Returns:
+            Optional[BaseModel]: The converted checked data based on the Pydantic model annotations.
+        """
+        try:
+            if issubclass(caller, BaseModel):
+                return caller(**checked)
+            return checked
+        except:
+            return checked
+
+
+    class Func(BaseModel):
+        """
+        Func is a Pydantic model representing a function's metadata.
+        Useful for the executive functions and the post-processing.
+        """
+        model_config = ConfigDict(arbitrary_types_allowed=True)
+        f_obj: Optional[object] = Field(default=None)
+        f_def: str = Field(default="", description="Simple definition of the function, e.g., 'def func(a:int, b:str)->int:'")
+        f_name: str = Field(default="", description="Name of the function, e.g., 'func'")
+        f_doc: str = Field(default="", description="Documentation of the function, e.g., 'This function returns the sum of two integers.'")
+        f_call: str = Field(default="", description="Actual call of the function, e.g., 'func(1, 'hello')'")
+        f_args: Dict[str, Any] = Field(default_factory=dict, description="Arguments of the function, e.g., {'a': 1, 'b': 'hello'}")
+        f_type: Tuple[List[Any], Any] = Field(default_factory=lambda: ([], None), description="Desired type of the _inputs and _outputs of the function")
+        f_schema: Dict[str, Any] = Field(default_factory=dict, description="Dictionary describing the function's return type (in case of pydantic).")
+        f_sig: Optional[inspect.Signature] = Field(default=None, description="Signature of the function")
+        f_locals: Optional[Dict[str, Any]] = Field(default=None, description="Local variables within the function's scope")
+        f_self: Optional[Dict[str, Any]] = Field(default=None)
+        f_mem: Optional[List[MemoryNode]] = Field(default=None, description="Memory nodes associated with the function, contains examples, chain of thought...")
+
+
+    def get_pydantic_schema(return_caller) -> Optional[Dict[str, Any]]:
+        """
+        Get the JSON schema of the function's return type.
+
+        Returns:
+            The JSON schema of the function's return type.
+        """
+        try:
+            if issubclass(return_caller, BaseModel):
+                return return_caller.model_json_schema()
+            return None
+        except:
+            return None
+
+else:
+    class Func:
+        f_obj: Optional[object] = None
+        f_def: str = ""
+        f_name: str = ""
+        f_doc: str = ""
+        f_call: str = ""
+        f_args: Dict[str, Any] = {}
+        f_type: Tuple[List[Any], Any] = ([], None)
+        f_schema: Dict[str, Any] = {}
+        f_sig: Optional[inspect.Signature] = None
+        f_locals: Optional[Dict[str, Any]] = None
+        f_self: Optional[Dict[str, Any]] = None
+        f_mem: Optional[List[MemoryNode]] = None
diff --git a/src/OpenHosta/datapreparator.py b/src/OpenHosta/datapreparator.py
deleted file mode 100644
index ad21f6d..0000000
--- a/src/OpenHosta/datapreparator.py
+++ /dev/null
@@ -1,241 +0,0 @@
-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
deleted file mode 100644
index c76aadb..0000000
--- a/src/OpenHosta/decoder.py
+++ /dev/null
@@ -1,3 +0,0 @@
-class HostaDecoder():
-    def __init__(self) -> None:
-        pass
diff --git a/src/OpenHosta/emulate.py b/src/OpenHosta/emulate.py
deleted file mode 100644
index 42e25e1..0000000
--- a/src/OpenHosta/emulate.py
+++ /dev/null
@@ -1,85 +0,0 @@
-import sys
-import inspect
-
-from .prompt import PromptMananger
-from .config import Model, DefaultManager
-
-
-_x = PromptMananger()
-
-_emulator_pre_prompt = _x.get_prompt("emulate")
-
-l_default = DefaultManager.get_default_model()
-
-
-def build_user_prompt(_infos: dict = None):
-    filler = lambda pre, value: f"**{pre}**\n{str(value)}\n\n" if value is not None and value != [] else ""
-    
-    user_prompt = (
-        "---\n\n## Function infos\n\n"
-        + filler("Here's the function definition:", _infos["function_def"])
-        + filler("Here's the function's locals variables which you can use as additional information to give your answer:", _infos["function_locals"])
-        + "To fill in the \"return\" value in the output JSON, create your response according to the specified JSON Schema. Make sure not to change the key \"return.\"\n\n"
-        + filler("JSON Schema to be used for \"return\" structure", _infos["return_type"])
-        + filler("Here are some examples of expected input and output:", _infos["ho_example"])
-        + "---\n"
-    )
-    
-    return user_prompt
-
-
-def _exec_emulate(
-    _infos: dict = None,
-    _obj: object = None,
-    model: Model = None,
-    l_creativity: float = None,
-    l_diversity: float = None,
-):
-    global _emulator_pre_prompt
-
-    _function_return_caller = _infos["return_caller"]
-    _function_return = _infos["return_type"]
-    
-    if model is None:
-        model = DefaultManager.get_default_model()
-
-    try:
-        if not isinstance(_emulator_pre_prompt, str) or not _emulator_pre_prompt:
-            raise ValueError("Invalid prompt.")
-        if (l_creativity is not None and (l_creativity < 0 or l_creativity > 1)) or (
-            l_diversity is not None and (l_diversity < 0 or l_diversity > 1)
-        ):
-            raise ValueError("Emulate out of range values (0<creativity|diversity<1)")
-    except ValueError as v:
-        sys.stderr.write(f"[EMULATE_ERROR]: {v}")
-        return None
-
-    function_infos = build_user_prompt(_infos)
-
-    response = model.api_call(
-        sys_prompt=f"{_emulator_pre_prompt}\n{function_infos}\n",
-        user_prompt=_infos["function_call"],
-        creativity=l_creativity,
-        diversity=l_diversity,
-    )
-    
-    if _obj is not None and inspect.isfunction(_obj):
-        setattr(_obj, "_last_response", response.json())
-        
-    if response.status_code == 200:
-        l_ret = model.request_handler(
-            response, _function_return, _function_return_caller
-        )
-
-    else:
-        sys.stderr.write(f"Error {response.status_code}: {response.text}")
-        l_ret = None
-        
-    if _obj is not None and inspect.isfunction(_obj):
-        setattr(
-            _obj, 
-            "_last_request", 
-            f"{_emulator_pre_prompt}\n{function_infos}\n{_infos['function_call']}"
-        )
-
-    return l_ret
diff --git a/src/OpenHosta/encoder.py b/src/OpenHosta/encoder.py
deleted file mode 100644
index a975ad0..0000000
--- a/src/OpenHosta/encoder.py
+++ /dev/null
@@ -1,36 +0,0 @@
-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
deleted file mode 100644
index 08a3cb8..0000000
--- a/src/OpenHosta/enhancer.py
+++ /dev/null
@@ -1,100 +0,0 @@
-import json
-import sys
-from typing import Callable
-
-from .prompt import PromptMananger
-from .config import DefaultManager
-
-
-_x = PromptMananger()
-_enhancer_pre_prompt = _x.get_prompt("enhance")
-
-l_default = DefaultManager.get_default_model()
-
-
-def _ai_call_enh(sys_prompt: str, func_prot: str, func_doc: str):
-    global l_default
-
-    l_user_prompt = (
-        "\nHere's my python function's prototype:\n---\n"
-        + func_prot
-        + "\n---\n"
-        + "\nHere's my python function's prompt:\n---\n"
-        + func_doc
-        + "\n---\n"
-    )
-
-    response = l_default.api_call(
-        sys_prompt=sys_prompt, user_prompt=l_user_prompt, creativity=0.8, diversity=0.8
-    )
-
-    response_data = response.json()
-    return response_data["choices"][0]["message"]["content"]
-
-
-def _parse_data(response: str, last_enh: dict) -> dict:
-    try:
-        l_ret_data = json.loads(response)
-
-    except json.JSONDecodeError as e:
-        sys.stderr.write(f"JSONDecodeError: {e}")
-        l_cleand = "\n".join(response.split("\n")[1:-1])
-        l_ret_data = json.loads(l_cleand)
-
-    last_enh["enhanced"] = l_ret_data["enhanced"]
-    last_enh["review"] = l_ret_data["review"]
-    last_enh["advanced"] = l_ret_data["advanced"]
-    last_enh["mermaid"] = l_ret_data["mermaid"]
-    return last_enh
-
-
-def _build_attributes(func: object, last_enh) -> int:
-    try:
-        if not func.__name__ or not type(func.__name__) is str:
-            raise ValueError("ValueError -> function name")
-        if not last_enh["enhanced"] or not type(last_enh["enhanced"]) is str:
-            raise ValueError("ValueError -> enhanced output")
-        if not last_enh["review"] or not type(last_enh["review"]) is str:
-            raise ValueError("ValueError -> review output")
-        if not last_enh["advanced"] or not type(last_enh["advanced"]) is str:
-            raise ValueError("ValueError -> seggested output")
-        if not last_enh["mermaid"] or not type(last_enh["mermaid"]) is str:
-            raise ValueError("ValueError -> mermaid output")
-    except ValueError as e:
-        sys.stderr.write(f"[BUILD_ERROR] {e}")
-        return -1
-    finally:
-        func.enhanced_prompt = last_enh["enhanced"]
-        func.review = last_enh["review"]
-        func.advanced = last_enh["advanced"]
-        func.diagram = last_enh["mermaid"]
-    return 0
-
-
-def enhance(func):
-    global _enhancer_pre_prompt
-
-    last_enh: dict = {
-        "enhanced": None,
-        "review": None,
-        "advanced": None,
-        "mermaid": None,
-    }
-
-    func_name, func_doc = func.__name__, func.__doc__
-
-    last_return = _ai_call_enh(_enhancer_pre_prompt, func._prot, func_doc)
-
-    last_enh = _parse_data(last_return, last_enh)
-
-    _build_attributes(func, last_enh)
-    return last_enh
-
-def suggest(func:Callable):
-    if not callable(func):
-        raise ValueError("Suggest arguments must be a callable.")
-    try:
-        full = func.__suggest__(func)
-    except AttributeError:
-        raise AttributeError(f"The “__suggest__” attribute is not defined. The function {func.__name__} might not have been called.")
-    return full
diff --git a/src/OpenHosta/example.py b/src/OpenHosta/example.py
deleted file mode 100644
index 4a75954..0000000
--- a/src/OpenHosta/example.py
+++ /dev/null
@@ -1,256 +0,0 @@
-import inspect
-import pickle
-import os
-import json
-import csv
-from typing import Callable
-
-from .errors import FrameError
-from .cache import Hostacache
-
-CACHE_DIR = "__hostacache__"
-os.makedirs(CACHE_DIR, exist_ok=True)
-
-
-def example(*args, hosta_func=None, hosta_out=None, **kwargs):
-    input_type = {}
-    output_type = {}
-    example_dict = {}
-
-    if hosta_func is None:
-        try:
-            func, _ = _extend_scope()
-        except:
-            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, 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__}, "
-                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 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"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 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("Please provide hosta_out for output.")
-    else:
-        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
-
-
-def save_examples(hosta_func=None, hosta_path=None):
-    cached_data = {}
-
-    if hosta_func is None:
-        try:
-            func, _ = _extend_scope()
-        except:
-            raise ValueError(f"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")
-
-    if hosta_path is None:
-        raise ValueError(
-            f"Please provide hosta_path for specifying the path to save the cache"
-        )
-    total_path = f"{hosta_path}" + ".jsonl"
-
-    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, "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_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:
-            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)
-
-    _, 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:
-        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.")
-
-    return func, caller
-
-
-EXAMPLE_DOC = """
-A utility function that performs runtime type validation on a given function's arguments and output.
-
-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
deleted file mode 100644
index bbb4bf3..0000000
--- a/src/OpenHosta/exec.py
+++ /dev/null
@@ -1,272 +0,0 @@
-import pickle
-import os
-import hashlib
-import inspect
-from typing import Callable, Dict, Any, get_origin, get_args
-import typing
-import collections
-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__"
-os.makedirs(CACHE_DIR, exist_ok=True)
-
-class HostaInjector:
-    def __init__(self, exec):
-        if not callable(exec):
-            raise TypeError("Executive function must be a function.")
-
-        self.exec = exec
-        self.infos_cache = {}
-
-    def __call__(self, *args, **kwargs):
-        self.infos_cache = {
-            "hash_function": "",
-            "function_def": "",
-            "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__
-        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)
-
-            function_def, func_prot = self._get_functionDef(func_obj)
-            function_hash = self._get_hashFunction(
-                function_def,
-                cached_data["ho_example_id"],
-                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_args"] = (
-                    self._get_functionCall(func_obj, caller)
-                )
-                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_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:
-        combined = f"{func_def}{nb_example}{nb_thought}"
-        return hashlib.md5(combined.encode()).hexdigest()
-
-    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["hash_function"] = self._get_hashFunction(
-            self.infos_cache["function_def"],
-            self.infos_cache["ho_example_id"],
-            self.infos_cache["ho_cothougt_id"],
-        )
-        return self.infos_cache
-
-    def _extend_scope(self) -> 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:
-            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.")
-
-        return func, caller
-
-    def _get_functionDef(self, func: Callable) -> str:
-        sig = inspect.signature(func)
-
-        func_name = func.__name__
-        func_params = ", ".join(
-            [
-                (
-                    f"{param_name}: {param.annotation.__name__}"
-                    if param.annotation != inspect.Parameter.empty
-                    else param_name
-                )
-                for param_name, param in sig.parameters.items()
-            ]
-        )
-        func_return = (
-            f" -> {sig.return_annotation.__name__}"
-            if sig.return_annotation != inspect.Signature.empty
-            else ""
-        )
-        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
-
-    def _get_functionCall(self, func: Callable, caller) -> str:
-        locals = None
-        _, _, _, values = inspect.getargvalues(caller)
-
-        sig = inspect.signature(func)
-
-        values_args = copy.deepcopy(values)
-        values_locals = copy.deepcopy(values)
-        for values_name in values.keys():
-            if values_name not in sig.parameters.keys():
-                values_args.pop(values_name)
-            else:
-                values_locals.pop(values_name)
-
-        if "self" in values_locals.keys():
-            values_locals.pop("self")
-
-        if values_locals != {}:
-            locals = copy.deepcopy(values_locals)
-
-        bound_args = sig.bind_partial(**values_args)
-        bound_args.apply_defaults()
-
-        args_str = ", ".join(
-            f"{name}={value!r}" if name in bound_args.kwargs else f"{value!r}"
-            for name, value in bound_args.arguments.items()
-        )
-
-        call = f"{func.__name__}({args_str})"
-        return call, locals, values_args
-
-    def _inspect_returnType(self, func: Callable) -> str:
-        sig = inspect.signature(func)
-
-        if sig.return_annotation != inspect.Signature.empty:
-            return sig.return_annotation
-        else:
-            return None
-
-    def _get_typingOrigin(self, return_type) -> bool:
-        origin = get_origin(return_type)
-        return origin in {
-            list,
-            dict,
-            tuple,
-            set,
-            frozenset,
-            typing.Union,
-            typing.Annotated,
-            typing.Optional,
-            typing.Literal,
-            collections.deque,
-            collections.abc.Iterable,
-            collections.abc.Sequence,
-            collections.abc.Mapping,
-        }
-
-    def _get_functionReturnType(self, func: Callable) -> Dict[str, Any]:
-        return_caller = self._inspect_returnType(func)
-        return_type = None
-
-        if return_caller is not None:
-            if self._get_typingOrigin(return_caller):
-                return_caller_origin = get_origin(return_caller)
-                return_caller_args = get_args(return_caller)
-                combined = return_caller_origin[return_caller_args]
-                new_model = create_model(
-                    "Hosta_return_shema", return_hosta_type_typing=(combined, ...)
-                )
-                return_type = new_model.model_json_schema()
-            elif issubclass(return_caller, BaseModel):
-                return_type = return_caller.model_json_schema()
-            else:
-                new_model = create_model(
-                    "Hosta_return_shema", return_hosta_type=(return_caller, ...)
-                )
-                return_type = new_model.model_json_schema()
-        else:
-            No_return_specified = create_model(
-                "Hosta_return_shema", return_hosta_type_any=(Any, ...)
-            )
-            return_type = No_return_specified.model_json_schema()
-
-        return return_type, return_caller
-
-    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, "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/exec/__init__.py b/src/OpenHosta/exec/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/OpenHosta/exec/ask.py b/src/OpenHosta/exec/ask.py
new file mode 100644
index 0000000..16c0c3b
--- /dev/null
+++ b/src/OpenHosta/exec/ask.py
@@ -0,0 +1,34 @@
+from __future__ import annotations
+
+from typing import Any, Optional
+
+from ..core.config import Model, DefaultManager
+from ..utils.errors import RequestError
+
+
+def ask(
+    *,
+    user: str,
+    system: Optional[str] = None,
+    model: Optional[Model] = None,
+    **api_args
+) -> Any:
+    if model is None:
+        model = DefaultManager.get_default_model()
+    if system is None:
+        system = "You are an helpful assistant."
+
+    response = model.simple_api_call(
+        system,
+        user,
+        False,
+        **api_args
+    )
+
+    try:
+        data = response.json()
+        res = data["choices"][0]["message"]["content"]
+        setattr(ask, "_last_tokens", data["usage"]["total_tokens"])
+        return res
+    except Exception as e:
+        raise RequestError(f"[ask] Request failed:\n{e}")
diff --git a/src/OpenHosta/exec/emulate.py b/src/OpenHosta/exec/emulate.py
new file mode 100644
index 0000000..c75bae7
--- /dev/null
+++ b/src/OpenHosta/exec/emulate.py
@@ -0,0 +1,89 @@
+from __future__ import annotations
+
+from typing import Any, Optional, Callable
+
+from ..core.config import Model, DefaultManager
+from ..core.hosta import Hosta, Func
+from ..utils.meta_prompt import EMULATE_PROMPT
+
+
+def _build_user_prompt(
+        _infos: Func = None,
+        x: Hosta = None,
+        use_locals_as_ctx: Optional[bool] = False,
+        use_self_as_ctx: Optional[bool] = False,
+):
+    def filler(
+        pre, value): return f"**{pre}**\n{str(value)}\n\n" if value is not None and value != [] else ""
+
+    user_prompt = (
+        filler(EMULATE_PROMPT.PRE_DEF, _infos.f_def)
+        + filler(EMULATE_PROMPT.PRE_TYPE, _infos.f_type[1])
+        + filler(EMULATE_PROMPT.PRE_SCHEMA, _infos.f_schema)
+    )
+    if use_locals_as_ctx:
+        user_prompt = (
+            user_prompt + filler(EMULATE_PROMPT.PRE_LOCALS, _infos.f_locals))
+    if use_self_as_ctx:
+        user_prompt = (
+            user_prompt + filler(EMULATE_PROMPT.PRE_SELF, _infos.f_self))
+    if x:
+        user_prompt = (
+            user_prompt
+            + filler(EMULATE_PROMPT.PRE_EXAMPLE, x.example)
+            + filler(EMULATE_PROMPT.PRE_COT, x.cot)
+        )
+    user_prompt = (user_prompt + EMULATE_PROMPT.USER_SEP)
+    return user_prompt
+
+
+def emulate(
+        _infos: Optional[Func] = None,
+        *,
+        model: Optional[Model] = None,
+        use_locals_as_ctx: bool = False,
+        use_self_as_ctx: bool = False,
+        post_callback: Optional[Callable] = None,
+        **llm_args
+) -> Any:
+    x = None
+    l_ret: Any = None
+
+    if _infos is None:
+        x = Hosta()
+        _infos = getattr(x._update_call(), "_infos")
+    func_prompt: str = _build_user_prompt(
+        _infos, x, use_locals_as_ctx, use_self_as_ctx)
+
+    if model is None:
+        model = DefaultManager.get_default_model()
+
+    if x:
+        x._attach(_infos.f_obj, {
+            "_last_request": None,
+            "_last_response": None
+        })
+
+    try:
+        if x:
+            _infos.f_obj._last_request = {
+                    'sys_prompt':f"{EMULATE_PROMPT!r}\n{func_prompt}\n",
+                    'user_prompt':_infos.f_call} | llm_args
+        
+        response = model.simple_api_call(
+            sys_prompt=f"{EMULATE_PROMPT!r}\n{func_prompt}\n",
+            user_prompt=_infos.f_call,
+            **llm_args
+        )
+
+        if x:
+            _infos.f_obj._last_response=response
+        
+        l_ret = model.request_handler(response, _infos)
+        if post_callback is not None:
+            l_ret = post_callback(l_ret)
+    except NameError as e:
+        raise NotImplementedError(
+            f"[emulate]: {e}\nModel object does not have the required methods.")
+
+    return l_ret
diff --git a/src/OpenHosta/exec/example.py b/src/OpenHosta/exec/example.py
new file mode 100644
index 0000000..10b97a1
--- /dev/null
+++ b/src/OpenHosta/exec/example.py
@@ -0,0 +1,31 @@
+from __future__ import annotations
+
+from typing import Any, get_args
+
+from ..core.hosta import Hosta, ExampleType
+
+
+def example(*args, hosta_out: Any = None, **kwargs):
+    x = Hosta()
+    if args != ():
+        raise ValueError(
+            "[example] The arguments in example function must keyword only arguments, with keywords matching with the name of the calling function's arguments")
+    if type(hosta_out) != x._infos.f_type[1] \
+    and hosta_out not in get_args(x._infos.f_type[1]) \
+    and type(hosta_out) not in get_args(x._infos.f_type[1]):
+        raise ValueError("[example] hosta_out's type doesn't match with the calling function's return type:\n\t{} instead of {}.".format(
+            type(hosta_out),
+            x._infos.f_type[1]
+        ))
+    if len(kwargs) != len(x._infos.f_type[0]):
+        print(x._infos.f_type[1])
+        raise ValueError("[example] Invalid number of argument. Expected {}, got {}".format(
+            len(x._infos.f_type[0]), len(kwargs)))
+    for (k1, v1), (k2, v2) in zip(kwargs.items(), x._infos.f_args.items()):
+        if k1 != k2:
+            raise ValueError(
+                "[example] Invalid arguments name: Expected {}, got {}".format(k2, k1))
+        if type(v1) != type(v2):
+            raise ValueError("[example] Invalid arguments type: Expected {}, got {}".format(
+                type(v2), type(v1)))
+    x._bdy_add('ex', ExampleType(in_=kwargs, out=hosta_out))
diff --git a/src/OpenHosta/exec/predict/__init__.py b/src/OpenHosta/exec/predict/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/OpenHosta/exec/predict/dataset/__init__.py b/src/OpenHosta/exec/predict/dataset/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/OpenHosta/exec/predict/dataset/dataset.py b/src/OpenHosta/exec/predict/dataset/dataset.py
new file mode 100644
index 0000000..38b2519
--- /dev/null
+++ b/src/OpenHosta/exec/predict/dataset/dataset.py
@@ -0,0 +1,337 @@
+import csv
+import json
+import os
+from enum import Enum
+from typing import List, Optional, Any, Dict
+
+import torch
+
+from .sample_type import Sample
+from ..encoder.simple_encoder import SimpleEncoder
+
+
+class SourceType(Enum):
+    """
+    Enum for different types of sources for the dataset.
+    """
+    CSV = "csv"
+    JSONL = "jsonl"
+    JSON = "json"
+class HostaDataset:
+    def __init__(self, verbose: int = 1):
+        self.path: Optional[str] = None  # Path to the file
+        self.data: List[Sample] = []  # List of Sample objects
+        self.dictionary: Dict[str, int] = {}  # Dictionary for mapping str to id
+        self.inference: Optional[Sample] = None  # Inference data for understanding the data
+        self.verbose: int = verbose  # Verbose level for debugging
+        self._encoder: Optional[SimpleEncoder] = None  # Will store the encoder instance
+
+    def add(self, sample: Sample):
+        """
+        Add a Sample object to the dataset.
+        """
+        self.data.append(sample)
+    def convert_data(self, batch_size: int, shuffle: bool, train_set_size: float = 0.8) -> tuple:
+        """
+        Save the dataset to a file in the specified format and convert it into dataloader for training.
+        """
+        if not isinstance(self.data[0].input, torch.Tensor):
+            self.tensorify()
+        
+        inputs = torch.stack([sample.input for sample in self.data])
+        if all(sample.output is not None for sample in self.data):
+            outputs = torch.stack([sample.output for sample in self.data])
+            dataset = torch.utils.data.TensorDataset(inputs, outputs)
+        else:
+            dataset = torch.utils.data.TensorDataset(inputs)
+        
+        return self._create_dataloaders(dataset, batch_size, shuffle, train_set_size)
+
+    def save_data(self, file_path: str):
+        """
+        Sauvegarde le dataset au format JSON
+        """
+        data_to_save = {
+            'data': [
+                {
+                    '_inputs': sample.input.tolist() if isinstance(sample.input, torch.Tensor) else sample.input,
+                    '_outputs': sample.output.tolist() if isinstance(sample.output, torch.Tensor) else sample.output
+                }
+                for sample in self.data
+            ]
+        }
+        with open(file_path, 'w') as f:
+            json.dump(data_to_save, f)
+
+    def load_data(self, file_path: str):
+        """
+        Charge un dataset depuis un fichier JSON
+        """
+        with open(file_path, 'r') as f:
+            data_dict = json.load(f)
+        
+        for sample_dict in data_dict['data']:
+            self.add(Sample(sample_dict))
+        
+    def _create_dataloaders(self, dataset, batch_size: int, shuffle: bool, train_set_size: float):
+        """
+        Méthode utilitaire pour créer les dataloaders
+        """
+        train_size = int(train_set_size * len(dataset))
+        
+        train_dataset = torch.utils.data.Subset(dataset, range(train_size))
+        val_dataset = torch.utils.data.Subset(dataset, range(train_size, len(dataset)))
+        
+        return (
+            torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=shuffle),
+            torch.utils.data.DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
+        )
+
+
+    @staticmethod
+    def from_data(data_path: str, batch_size: int, shuffle: bool, train_set_size: float = 0.8, verbose: int = 1) -> tuple:
+        """
+        Load a dataset from a file and convert it into dataloader for training.
+        """
+        dataset = HostaDataset(verbose)
+        dataset.load_data(data_path)
+        return dataset.convert_data(batch_size, shuffle, train_set_size)
+
+
+    def save(self, path: str, source_type: SourceType = SourceType.CSV, elements: Optional[List[Sample]] = None):
+        """
+        Save the dataset or specific elements to a file in the specified format.
+        Converts Sample objects back to dictionaries for storage.
+
+        Args:
+            path: Path where to save the file
+            source_type: Type of file format to save (CSV, JSONL, or PICKLE)
+            elements: Optional list of Sample objects to save. If None, saves entire dataset
+        """
+        self.path = path
+        data_to_save = elements if elements is not None else self.data
+
+        # Convert Samples to dictionaries for saving
+        dict_data = []
+        for sample in data_to_save:
+            sample_dict = {}
+            for i, input_value in enumerate(sample.input):
+                sample_dict[f'input_{i}'] = input_value
+            if sample.output is not None:
+                sample_dict['_outputs'] = sample.output
+            dict_data.append(sample_dict)
+
+        if source_type == SourceType.CSV:
+            with open(path, 'w', newline='', encoding='utf-8') as f:
+                if not dict_data:
+                    return
+                writer = csv.DictWriter(f, fieldnames=dict_data[0].keys())
+                writer.writeheader()
+                writer.writerows(dict_data)
+
+        elif source_type == SourceType.JSONL:
+            with open(path, 'w', encoding='utf-8') as f:
+                for row in dict_data:
+                    json.dump(row, f)
+                    f.write('\n')
+
+        else:
+            raise ValueError(f"Unsupported source type: {source_type}")
+
+    def convert_files(self, path: str, source_type: Optional[SourceType] = None) -> List[Sample]:
+        """
+        Load dataset from a file and convert each row to a Sample object.
+
+        Args:
+            path: Path to the source file
+            source_type: Type of file to load (CSV, JSONL, or PICKLE)
+
+        Returns:
+            self.data will be updated with the loaded data
+        """
+        if not os.path.exists(path):
+            raise FileNotFoundError(f"File not found: {path}")
+        self.path = path # Save the pass for acces easily later
+
+        if source_type is None:
+            if path.endswith('.csv'):
+                source_type = SourceType.CSV
+            elif path.endswith('.jsonl'):
+                source_type = SourceType.JSONL
+            else:
+                raise ValueError(f"Please specify the source type for the file: {path}")
+
+        if source_type == SourceType.CSV:
+            with open(path, 'r') as f:
+                reader = csv.DictReader(f)
+                for row in reader:
+                    # Convert string numbers to float if possible
+                    processed_row = {}
+                    for key, value in row.items():
+                        try:
+                            processed_row[key] = float(value)
+                        except ValueError:
+                            processed_row[key] = value
+                    self.data.append(Sample(processed_row))
+
+        elif source_type == SourceType.JSONL:
+            with open(path, 'r') as f:
+                for line in f:
+                    record = json.loads(line)
+                    if not isinstance(record, dict):
+                        record = {'input_0': record}
+                    self.data.append(Sample(record))
+
+        else:
+            raise ValueError(f"Unsupported source type: {source_type}")
+        return self.data
+
+    def convert_list(self, data: list) -> List[Sample]:
+        """
+        Create a dataset from a list.
+
+        Args:
+            data: List of dictionaries or tuples/lists representing Sample _inputs and _outputs.
+                  Each item should either be:
+                  - a dict with keys for _inputs (e.g., 'input_0', 'input_1', ...) and optional '_outputs', or
+                  - a tuple/list where the first part is _inputs(s) and the last item is _outputs (optional).
+
+        Returns:
+            HostaDataset instance
+        """
+
+        for entry in data:
+            if isinstance(entry, dict):
+                # If the entry is already a dictionary, let's assume it has the keys in the right structure
+                self.add(Sample(entry))
+            elif isinstance(entry, (list, tuple)):
+                # If it's a list or tuple, we assume it's structured as (_inputs..., [_outputs])
+                inputs = list(entry[:-1])  # All but last element are _inputs
+                output = entry[-1] if len(entry) > 1 else None  # Last element could be _outputs if present
+                sample_dict = {f'input_{i}': input_value for i, input_value in enumerate(inputs)}
+                if output is not None:
+                    sample_dict['_outputs'] = output
+                self.add(Sample(sample_dict))
+            else:
+                raise ValueError(f"Unsupported data format in list entry: {entry}")
+
+    def encode(self, max_tokens: int) -> None:
+        """
+        Encode le dataset d'entraînement et crée le dictionnaire
+        """
+        if self._encoder is None:
+            self._encoder = SimpleEncoder()
+        self.data = self._encoder.encode(self.data, max_tokens=max_tokens)
+        self.dictionary = self._encoder.dictionary
+
+    def encode_inference(self) -> None:
+        """
+        Encode les données d'inférence avec le dictionnaire existant
+        """
+        if self.dictionary is None:
+            raise ValueError("No dictionary available. Call encode() first on training data")
+        
+        self._encoder = SimpleEncoder(existing_dict=self.dictionary)
+        self.inference = self._encoder.encode([self.inference], max_tokens=10)[0]
+
+    def tensorify(self, dtype=None) -> None:
+        """
+        Convertit le dataset d'entraînement en tenseurs
+        """
+        if dtype is None:
+            dtype = torch.float32
+            
+        for sample in self.data:
+            if not isinstance(sample.input, torch.Tensor):
+                sample._inputs = torch.tensor(sample.input, dtype=dtype)
+            
+            if sample.output is not None and not isinstance(sample.output, torch.Tensor):
+                if isinstance(sample.output, (int, float)):
+                    sample._outputs = torch.tensor(sample.output, dtype=dtype)
+                else:
+                    sample._outputs = torch.tensor(sample.output, dtype=dtype)
+
+    def tensorify_inference(self, dtype=None) -> None:
+        """
+        Convertit les données d'inférence en tenseurs
+        """
+        if dtype is None:
+            dtype = torch.float32
+        
+        if not isinstance(self.inference.input, torch.Tensor):
+            self.inference._inputs = torch.tensor(self.inference.input, dtype=dtype)
+
+    def prepare_inference(self, inference_data: dict) -> None:
+        """
+        Prépare les données d'inférence en les encodant et les convertissant en tenseurs
+        """
+        self.inference = Sample(inference_data)
+        self.encode_inference()
+        self.tensorify_inference()
+
+    @staticmethod
+    def from_input(inference_data: dict, verbose: int = 0) -> 'HostaDataset':
+        """
+        Crée un dataset à partir de données d'inférence
+        """
+        dataset = HostaDataset(verbose)
+        dataset.prepare_inference(inference_data)
+        return dataset
+
+    def decode(self, predictions: List[Any], func_f_type: Any) -> List[Any]:
+        """
+        Decode the model predictions based on the function's return type.
+        """
+        if self._encoder is None:
+            raise ValueError("Dataset must be encoded before decoding")
+        
+        # Check if func_f_type is a typing.Literal
+        # if isinstance(func_f_type, typing._GenericAlias) and get_origin() is Literal:
+
+        # if get_origin(func_f_type) is Literal:
+            # Return decoded predictions using the encoder
+            # return [self._encoder.decode_prediction(pred) for pred in predictions]
+        # else:
+        decoded_predictions = []
+        for pred in predictions:
+            pred_value = pred.detach().cpu().numpy()
+            # Convert pred_value to the expected type
+            # Handle scalar and array predictions
+            if pred_value.size == 1:
+                pred_scalar = pred_value.item()
+            else:
+                pred_scalar = pred_value
+            try:
+                converted_pred = func_f_type(pred_scalar)
+            except (TypeError, ValueError):
+                converted_pred = pred_scalar  # Return as is if conversion fails
+            decoded_predictions.append(converted_pred)
+            if func_f_type != list:
+                decoded_predictions = decoded_predictions[0]
+        return decoded_predictions
+        
+    @staticmethod
+    def from_files(path: str, source_type: Optional[SourceType], verbose: int = 1) -> 'HostaDataset':
+        """
+        Load a dataset from a file.
+        """
+        dataset = HostaDataset(verbose)
+        dataset.convert_files(path, source_type)
+        return dataset
+    
+    @staticmethod
+    def from_list(data: list, verbose: int) -> 'HostaDataset':
+        """
+        Create a dataset from a list.
+        """
+        dataset = HostaDataset(verbose)
+        dataset.convert_list(data)
+        return dataset
+
+
+    @staticmethod
+    def __len__(self):
+        return len(self.data)
+
+    def __iter__(self):
+        return iter(self.data)
diff --git a/src/OpenHosta/exec/predict/dataset/oracle.py b/src/OpenHosta/exec/predict/dataset/oracle.py
new file mode 100644
index 0000000..abccebd
--- /dev/null
+++ b/src/OpenHosta/exec/predict/dataset/oracle.py
@@ -0,0 +1,220 @@
+import inspect
+from typing import Optional, Dict, Any, List, Type, Union, get_args, Literal
+
+from ....core.config import Model, DefaultManager
+from ....core.hosta import Func
+
+_PROMPT = "{func_name}{signature}:\n    \"\"\"{docstring}\"\"\"\n\nIMPORTANT RULES:\n1. Input values should respect the type hints\n2. Output values MUST be diverse - avoid generating the same output repeatedly\n3. Each row must be in CSV format\n4. For text outputs, enclose them in double quotes\n5. NO MORE THAN 20% of outputs should be the same value\n6. Generate inputs across the entire possible range\n7. Ensure proper formatting for {return_type} output type"
+
+
+
+class LLMSyntheticDataGenerator:
+    """Generates synthetic data using a Language Model."""
+
+
+    @staticmethod
+    def _validate_row(row: str, expected_fields: List[Type]) -> Optional[List[Union[str, float]]]:
+        try:
+            values = row.strip().split(',')
+
+            if len(values) != len(expected_fields):
+                return None
+
+            result = []
+
+            for value, expected_type in zip(values, expected_fields):
+                if expected_type == str:
+                    result.append(value)
+
+                elif expected_type == float:
+                    result.append(float(value))
+
+                elif expected_type == int:
+                    result.append(float(value))  # Convert to integer
+
+                elif expected_type == bool:
+                    if value.lower() == "true":
+                        result.append(True)
+                    elif value.lower() == "false":
+                        result.append(False)
+                    else:
+                        return None
+
+                elif getattr(expected_type, '__origin__', None) is Literal:
+                    valid_literals = get_args(expected_type)
+                    if value in valid_literals:
+                        result.append(value)
+                    else:
+                        return None  # Invalid Literal
+
+                elif getattr(expected_type, '__origin__', None) is Union and type(None) in get_args(expected_type):
+                    non_none_types = [t for t in get_args(expected_type) if t is not type(None)]
+                    for t in non_none_types:
+                        if t == int:
+                            try:
+                                result.append(int(value))
+                                break
+                            except ValueError:
+                                continue
+                        elif t == float:
+                            try:
+                                result.append(float(value))
+                                break
+                            except ValueError:
+                                continue
+                        elif t == str:
+                            result.append(value)
+                            break
+                    else:
+                        return None
+                else:
+                    return None
+            return result
+        except (ValueError, TypeError):
+            return None
+
+    @staticmethod
+    def _format_example(input_val: Any, output_val: Any) -> str:
+        """
+        Format a single example based on _inputs/_outputs types.
+        """
+        if isinstance(input_val, (list, tuple)):
+            input_str = ','.join(map(str, input_val))
+        else:
+            input_str = str(input_val)
+
+        if isinstance(output_val, str):
+            output_str = f'{output_val}'  # Enclose strings in quotes
+        else:
+            output_str = str(output_val)
+
+        return f"{input_str},{output_str}"
+
+    @staticmethod
+    def _build_user_prompt(
+            examples: List[Dict],
+            func: Func,
+            output_type: Type,
+            examples_in_req: int,
+    ):
+        user_prompt = ""
+
+        if examples:
+            user_prompt += "\nReference examples (Input → Output):\n"
+            for input_val, output_val in examples.items():
+                user_prompt += f"{LLMSyntheticDataGenerator._format_example(input_val, output_val)}\n"
+
+        user_prompt += f"\n\nGenerate {examples_in_req} new DIVERSE _inputs-_outputs pairs, one per line, in CSV format"
+        if output_type == str:
+            user_prompt += " (remember to enclose string _outputs in quotes ex: \"_outputs\")"
+        user_prompt += ":\n"
+
+        user_prompt += f"{','.join(func.f_sig.parameters.keys())},_outputs"
+        user_prompt += f"\n{','.join([str(f"\n- {a} is type {b.annotation.__name__ if b.annotation != inspect.Parameter.empty else 'Any'}")\
+                                      for a, b in func.f_sig.parameters.items()])}\n"
+        user_prompt += f"- _outputs is type {output_type.__name__}\n"
+
+        return user_prompt
+
+
+    @staticmethod
+    def generate_synthetic_data(
+            func: Func, # The function to generate data for
+            request_amounts: int = 3, # Amount of requests to the model
+            examples_in_req: int = 50, # Examples amount in each request
+            model: Optional[Model] = None # Model to use for data generation
+    ) -> List[Dict]:
+        input_types: Dict[str, Type] = dict(zip(func.f_args.keys(), func.f_type[0]))
+        output_type: Type = func.f_type[1]
+        examples: List[Dict] = []
+        if func.f_mem:
+            for ex in func.f_mem:
+                ex_inputes = list(ex.value["in_"].values())
+                ex_output = ex.value["out"]
+                to_append = {}
+                for i, key in enumerate(input_types.keys()):
+                    to_append[key] = ex_inputes[i]
+                to_append["_outputs"] = ex_output
+
+        if not model:
+            model = DefaultManager.get_default_model()
+
+        try:
+            assert input_types and len(input_types) > 0, "Input types must be provided."
+            assert output_type, "Output type must be provided."
+            assert request_amounts > 0, "Request amount must be greater than 0."
+            assert examples_in_req > 0, "Examples amount in each request must be greater than 0."
+            assert model, "Model must be provided."
+        except AssertionError as e:
+            raise ValueError(f"Invalid parameters: {e}")
+
+        prompt: str = (
+            _PROMPT
+            .replace("{func_name}", func.f_name)
+            .replace("{signature}", str(func.f_sig))
+            .replace("{docstring}", func.f_doc)
+        )
+        prompt += LLMSyntheticDataGenerator._build_user_prompt(examples, func, output_type, examples_in_req)
+
+        generated_data: List = []
+        result: List[Dict] = []
+        conversation_history: List = []
+        attempts = 0
+        expected_fields = len(input_types) + 1
+
+        conversation_history.append({
+            "role": "system",
+            "content": "You are a data generation assistant focused on creating diverse, realistic data. Avoid repetitive patterns."
+        })
+
+        while attempts < request_amounts:
+            try:
+                content = prompt
+
+                # Add information about already generated data
+                if generated_data:
+                    already_generated = "\nAlready generated data (please avoid these exact combinations):\n"
+                    for row in generated_data:
+                        already_generated += f"{','.join(map(str, row))}\n"
+                    content += already_generated
+
+                # Add the user message to conversation history
+                conversation_history.append({
+                    "role": "user",
+                    "content": content
+                })
+
+                response = model.api_call(
+                    messages=conversation_history,
+                    temperature=1.0,
+                    json_form=False,
+                )
+
+                # Add the assistant's response to conversation history
+                conversation_history.append({
+                    "role": "assistant",
+                    "content": response["choices"][0]["message"]["content"]
+                })
+
+                rows = response["choices"][0]["message"]["content"].strip().split('\n')
+
+                for row in rows:
+                    cleaned_row = LLMSyntheticDataGenerator._validate_row(row, list(input_types.values()) + [output_type])
+                    if cleaned_row:
+                        if cleaned_row not in generated_data:
+                            dictrow = dict(zip(input_types.keys(), cleaned_row[:-1]))
+                            dictrow["_outputs"] = cleaned_row[-1]
+                            result.append(dictrow)
+                            generated_data.append(cleaned_row)
+
+                # Keep conversation history manageable by keeping only last 10 messages
+                if len(conversation_history) > 10:
+                    # Always keep the system message
+                    conversation_history = [conversation_history[0]] + conversation_history[-9:]
+
+            except Exception as e:
+                print(f"Error during generation: {e} line {e.__traceback__.tb_lineno}")
+
+            attempts += 1
+
+        return result
diff --git a/src/OpenHosta/exec/predict/dataset/sample_type.py b/src/OpenHosta/exec/predict/dataset/sample_type.py
new file mode 100644
index 0000000..67277db
--- /dev/null
+++ b/src/OpenHosta/exec/predict/dataset/sample_type.py
@@ -0,0 +1,67 @@
+from typing import List, Any, Optional
+
+# from pydantic import BaseModel
+
+
+class Sample:
+    """
+    A class to handle data samples for machine learning.
+    Expects a dictionary where all keys except '_outputs' are considered as _inputs.
+    
+    Example:
+        data = {
+            'feature1': [1, 2],     # Any key except '_outputs' is considered _inputs
+            'feature2': {'a': 3},    # Can contain any nested structure
+            'feature3': 4,           # Can contain any primitive type
+            'feature4': BaseModel(), # Can contain Pydantic models 
+            '_outputs': 9      # Optional _outputs
+        }
+        sample = Sample(data)
+    """
+    
+    def __init__(self, data: dict):
+        if not isinstance(data, dict):
+            raise TypeError("Data must be a dictionary")
+            
+        self._inputs: List[Any] = []
+        self._outputs: Optional[Any] = None
+        
+        output_data = data.pop('_outputs', None)
+        if output_data is not None:
+            output_flattened = self._flatten_data(output_data)
+            self._outputs = output_flattened[0] if len(output_flattened) == 1 else output_flattened
+
+        for value in data.values():
+            self.input.extend(self._flatten_data(value))
+
+    def _flatten_data(self, data: Any) -> List[Any]:
+        """
+        Flatten any nested data structure into a list.
+        Handles: BaseModel, dict, list/tuple, primitive types
+        """
+        # if isinstance(data, BaseModel):
+        #     return self._flatten_data(data.model_dump())
+        if isinstance(data, dict):
+            result = []
+            for value in data.values():
+                result.extend(self._flatten_data(value))
+            return result
+        if isinstance(data, (list, tuple)):
+            result = []
+            for item in data:
+                result.extend(self._flatten_data(item))
+            return result
+        return [data]
+
+    @property
+    def input(self) -> List[Any]:
+        """Get the _inputs features"""
+        return self._inputs
+
+    @property
+    def output(self) -> Optional[Any]:
+        """Get the _outputs label (None if no _outputs was provided)"""
+        return self._outputs
+
+    def __repr__(self) -> str:
+        return f"Sample(_inputs={self.input}, _outputs={self.output})"
diff --git a/src/OpenHosta/exec/predict/encoder/__init__.py b/src/OpenHosta/exec/predict/encoder/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/OpenHosta/exec/predict/encoder/base_encoder.py b/src/OpenHosta/exec/predict/encoder/base_encoder.py
new file mode 100644
index 0000000..b052f40
--- /dev/null
+++ b/src/OpenHosta/exec/predict/encoder/base_encoder.py
@@ -0,0 +1,13 @@
+from abc import ABC, abstractmethod
+from typing import Union,Any
+
+class BaseEncoder(ABC):
+    @abstractmethod
+    def encode(self, data: Any) -> Union[int, float]:
+        """Encode a single value"""
+        pass
+
+    @abstractmethod
+    def decode(self, encoded_value: Any) -> Any:
+        """Decode a prediction back to its original type"""
+        pass
diff --git a/src/OpenHosta/exec/predict/encoder/simple_encoder.py b/src/OpenHosta/exec/predict/encoder/simple_encoder.py
new file mode 100644
index 0000000..a738320
--- /dev/null
+++ b/src/OpenHosta/exec/predict/encoder/simple_encoder.py
@@ -0,0 +1,126 @@
+from typing import List, Any, Dict, Union
+
+from .base_encoder import BaseEncoder
+from ..dataset.sample_type import Sample
+
+
+class NumericEncoder(BaseEncoder):
+    def encode(self, value: Union[int, float]) -> float:
+        return float(value)
+
+    def decode(self, encoded_value: float) -> Union[int, float]:
+        return encoded_value
+
+class BooleanEncoder(BaseEncoder):
+    def encode(self, value: bool) -> int:
+        return int(value)
+
+    def decode(self, encoded_value: int) -> bool:
+        return bool(encoded_value)
+
+class StringEncoder(BaseEncoder):
+    def __init__(self, existing_dict: Dict[str, int] = None):
+        """
+        Initialize with optional existing dictionary.
+        If existing_dict is provided, we're in inference mode.
+        """
+        self.inference_mode = existing_dict is not None
+        self.word_to_id = {'<UNK>': 0} if existing_dict is None else existing_dict
+        self.id_to_word = {v: k for k, v in self.word_to_id.items()}
+        self.next_id = max(self.word_to_id.values()) + 1 if self.word_to_id else 1
+        self.max_tokens = None
+
+    def set_max_tokens(self, max_tokens: int):
+        """Set maximum length for encoded sequences"""
+        self.max_tokens = max_tokens
+
+    def encode(self, value: str) -> List[int]:
+        """
+        Encode a string into a list of integers.
+        For classification (_outputs), returns a single integer.
+        For _inputs features, returns a list of integers of length max_tokens.
+        """
+        if self.max_tokens is None:
+            raise ValueError("max_tokens must be set before encoding")
+
+        words = str(value).lower().strip().split()
+        encoded = []
+
+        for word in words:
+            if not self.inference_mode and word not in self.word_to_id:
+                self.word_to_id[word] = self.next_id
+                self.id_to_word[self.next_id] = word
+                self.next_id += 1
+            encoded.append(self.word_to_id.get(word, 0))
+
+        if len(encoded) > self.max_tokens:
+            return encoded[:self.max_tokens]
+        return encoded + [0] * (self.max_tokens - len(encoded))
+
+    def decode(self, encoded_value: Union[int, List[int]]) -> str:
+        """
+        Decode either a single integer (classification) or list of integers (features)
+        """
+        if isinstance(encoded_value, (int, float)):
+            return self.id_to_word.get(int(encoded_value), '<UNK>')
+
+        words = []
+        for idx in encoded_value:
+            if idx != 0:  # Skip padding
+                words.append(self.id_to_word.get(idx, '<UNK>'))
+        return ' '.join(words)
+
+class SimpleEncoder:
+    def __init__(self, existing_dict: Dict[str, int] = None):
+        self.string_encoder = StringEncoder(existing_dict)
+        self.feature_types = {}
+        self.encoders = {
+            str: self.string_encoder,
+            int: NumericEncoder(),
+            float: NumericEncoder(),
+            bool: BooleanEncoder()
+        }
+
+    def encode(self, samples: List[Sample], max_tokens: int) -> List[Sample]:
+        self.string_encoder.set_max_tokens(max_tokens)
+        
+        encoded_samples = []
+        for sample in samples:
+            encoded_input = []
+            for idx, value in enumerate(sample.input):
+                encoder = self.encoders[type(value)]
+                self.feature_types[idx] = type(value)
+                encoded_value = encoder.encode(value)
+                if isinstance(encoded_value, list):
+                    encoded_input.extend(encoded_value)
+                else:
+                    encoded_input.append(encoded_value)
+
+            encoded_output = None
+            if sample.output is not None:
+                if isinstance(sample.output, str):
+                    print("\033[93mWarning: Multiple string _outputs not supported, only using the first word will be used for _outputs\033[0m")
+                output_idx = len(sample.input)
+                encoder = self.encoders[type(sample.output)]
+                self.feature_types[output_idx] = type(sample.output)
+                encoded_output = encoder.encode(sample.output)
+                # Like multiple str _outputs not supported only use the first str _outputs
+                if isinstance(encoded_output, list):
+                    encoded_output = encoded_output[0]
+
+            encoded_samples.append(Sample({
+                '_inputs': encoded_input,
+                '_outputs': encoded_output
+            }))
+
+        return encoded_samples
+
+    def decode_prediction(self, prediction: Any, position: int) -> Any:
+        if position not in self.feature_types:
+            raise ValueError(f"Unknown feature position: {position}")
+
+        return self.encoders[self.feature_types[position]].decode(prediction)
+
+    @property
+    def dictionary(self) -> Dict[str, int]:
+        return self.string_encoder.word_to_id
diff --git a/src/OpenHosta/exec/predict/model/__init__.py b/src/OpenHosta/exec/predict/model/__init__.py
new file mode 100644
index 0000000..92fbcca
--- /dev/null
+++ b/src/OpenHosta/exec/predict/model/__init__.py
@@ -0,0 +1,9 @@
+from .hosta_model import HostaModel
+from .model_provider import HostaModelProvider
+from .neural_network_types import ArchitectureType
+
+all = (
+    'ArchitectureType',
+    'HostaModel',
+    'HostaModelProvider',
+)
\ No newline at end of file
diff --git a/src/OpenHosta/exec/predict/model/builtins/__init__.py b/src/OpenHosta/exec/predict/model/builtins/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/OpenHosta/exec/predict/model/builtins/classification.py b/src/OpenHosta/exec/predict/model/builtins/classification.py
new file mode 100644
index 0000000..d08aabd
--- /dev/null
+++ b/src/OpenHosta/exec/predict/model/builtins/classification.py
@@ -0,0 +1,146 @@
+### NOT IMPLEMENTED YET ###
+
+from typing import Optional
+
+import torch
+from torch import nn
+from torch import optim
+
+from ..hosta_model import HostaModel
+from ..neural_network import NeuralNetwork
+from .....utils.torch_nn_utils import custom_optimizer_to_pytorch, custom_loss_to_pytorch, custom_layer_to_pytorch
+
+
+class Classification(HostaModel):
+    def __init__(self, neural_network: Optional[NeuralNetwork], input_size: int, output_size: int, complexity: int, num_classes: int, device: Optional[str] = None):
+        super().__init__(device)
+
+        self.complexity = complexity
+        self.num_classes = num_classes
+        self.verbose = True
+        self.layers = []
+        if neural_network is None or neural_network.layers is None or len(neural_network.layers) == 0:
+            transition_value = int(((input_size * output_size) / 2) * self.complexity)
+
+            input_layer = int(input_size * (2 * self.complexity))
+            if input_size > output_size:
+                hidden_layer_1 = int(transition_value / output_size)
+            else:
+                hidden_layer_1 = transition_value
+
+            # Define simple fully connected architecture
+            self.layers.append(nn.Linear(input_size, input_layer))
+            self.layers.append(nn.ReLU())  # Apply ReLU after first layer
+            self.layers.append(nn.Linear(input_layer, hidden_layer_1))
+            self.layers.append(nn.ReLU())  # Apply ReLU after second layer
+            self.layers.append(nn.Linear(hidden_layer_1, output_size))
+        else:
+            # Use custom user-defined layers from neural network definition if available
+            self.layers = [custom_layer_to_pytorch(layer) for layer in neural_network.layers]
+
+        for i, layer in enumerate(self.layers):
+            setattr(self, f'fc{i + 1}', layer)
+
+        # Set the loss function for classification
+        if neural_network is None or neural_network.loss_function is None:
+            if num_classes == 2:
+                self.loss = nn.BCEWithLogitsLoss()  # For binary classification
+            else:
+                self.loss = nn.CrossEntropyLoss()  # For multi-class classification
+        else:
+            self.loss = custom_loss_to_pytorch(neural_network.loss_function)
+
+        # Set the optimizer
+        if neural_network is None or neural_network.optimizer is None:
+            self.optimizer = optim.Adam(self.parameters(), lr=0.001)
+        else:
+            self.optimizer = custom_optimizer_to_pytorch(neural_network.optimizer, self, lr=0.001)
+
+        # Move model to the selected device (CPU or GPU)
+        self.to(self.device)
+
+
+    def trainer(self, train_set, epochs):
+        self.train()
+
+        for epoch in range(epochs):
+            running_loss = 0.0
+            correct = 0
+            total = 0
+            for inputs, labels in train_set:
+                inputs, labels = inputs.to(self.device), labels.to(self.device)
+
+                # Zero parameter gradients
+                self.optimizer.zero_grad()
+
+                # Forward pass
+                outputs = self(inputs)
+
+                if self.num_classes == 2:
+                    preds = (torch.sigmoid(outputs) > 0.5).float()
+                else:
+                    preds = torch.argmax(outputs, dim=1)
+
+                # Compute Loss
+                loss = self.loss(outputs, labels)
+                loss.backward()
+                self.optimizer.step()
+
+                running_loss += loss.item()
+
+                # Calculate accuracy
+                if self.num_classes == 2:
+                    correct += (preds == labels).sum().item()
+                else:
+                    correct += (preds == labels.argmax(dim=1)).sum().item()
+
+                total += labels.size(0)
+
+            accuracy = correct / total
+            if self.verbose:
+                print(f"Epoch {epoch + 1}/{epochs}, Loss: {running_loss / len(train_set):.4f}, Accuracy: {accuracy * 100:.2f}%")
+
+    def validate(self, validation_set):
+        """Validate the model's performance"""
+        self.eval()  # Set model to evaluation mode
+        validation_loss = 0.0
+        correct = 0
+        total = 0
+        with torch.no_grad():
+            for inputs, labels in validation_set:
+                inputs, labels = inputs.to(self.device), labels.to(self.device)
+                outputs = self(inputs)
+
+                loss = self.loss(outputs, labels)
+                validation_loss += loss.item()
+
+                # For Classification Metrics (like binary or multi-class accuracy)
+                if self.num_classes == 2:
+                    # Binary classification: Apply sigmoid and threshold at 0.5
+                    preds = (torch.sigmoid(outputs) > 0.5).float()
+                    correct += (preds == labels).sum().item()
+                else:
+                    # Multi-class classification: Use argmax to get class labels
+                    preds = torch.softmax(outputs, dim=1)
+                    correct += (preds == labels.argmax(dim=1)).sum().item()
+
+                total += labels.size(0)
+
+        avg_val_loss = validation_loss / len(validation_set)
+        accuracy = correct / total
+        print(f"Validation Loss: {avg_val_loss:.4f}, Accuracy: {accuracy * 100:.2f}%")
+
+        return avg_val_loss, accuracy
+
+
+    def inference(self, x):
+        """Make prediction on a _inputs inference the model"""
+        self.eval()
+        with torch.no_grad():
+            x = x.to(self.device)
+            outputs = self(x)
+            if self.num_classes == 2:
+                prediction = (torch.sigmoid(outputs) > 0.5).float()
+            else:
+                prediction = torch.softmax(outputs, dim=1)
+            return prediction.cpu()
diff --git a/src/OpenHosta/exec/predict/model/builtins/linear_regression.py b/src/OpenHosta/exec/predict/model/builtins/linear_regression.py
new file mode 100644
index 0000000..9838899
--- /dev/null
+++ b/src/OpenHosta/exec/predict/model/builtins/linear_regression.py
@@ -0,0 +1,97 @@
+from typing import Optional
+
+import torch
+from torch import nn
+from torch import optim
+
+from ..hosta_model import HostaModel
+from ..neural_network import NeuralNetwork
+from .....utils.torch_nn_utils import custom_optimizer_to_pytorch, custom_loss_to_pytorch, custom_layer_to_pytorch
+
+
+class LinearRegression(HostaModel):
+    def __init__(self, neural_network: Optional[NeuralNetwork], input_size: int, output_size: int, complexity: int, device: Optional[str] = None):
+        super().__init__(device)
+
+        self.complexity = complexity
+
+        self.layers = []
+        if neural_network is None or neural_network.layers is None or len(neural_network.layers) == 0:
+            transition_value = int(((input_size * output_size) / 2) * self.complexity)
+
+            input_layer = int(input_size * (2 * self.complexity))
+            if input_size > output_size:
+                hidden_layer_1 = int(transition_value / output_size)
+            else:
+                hidden_layer_1 = transition_value
+
+            self.layers.append(nn.Linear(input_size, input_layer))
+            self.layers.append(nn.ReLU())
+            self.layers.append(nn.Linear(input_layer, hidden_layer_1))
+            self.layers.append(nn.ReLU())
+            self.layers.append(nn.Linear(hidden_layer_1, output_size))
+        else:
+            self.layers = [custom_layer_to_pytorch(layer) for layer in neural_network.layers]
+
+        for i, layer in enumerate(self.layers):
+            setattr(self, f'fc{i + 1}', layer)
+
+        # Set the loss function
+        if neural_network is None or neural_network.loss_function is None:
+            self.loss = nn.MSELoss()
+        else:
+            self.loss = custom_loss_to_pytorch(neural_network.loss_function)
+
+        # Set the optimizer
+        if neural_network is None or neural_network.optimizer is None:
+            self.optimizer = optim.Adam(self.parameters(), lr=0.001)
+        else:
+            self.optimizer = custom_optimizer_to_pytorch(neural_network.optimizer, self, lr=0.001)
+
+        # Move model to the selected device (CPU or GPU)
+        self.to(self.device)
+
+    def trainer(self, train_set, epochs, verbose=False):
+        self.train()
+
+        for epoch in range(epochs):
+            running_loss = 0.0
+            for inputs, labels in train_set:
+                # Move _inputs and labels to the right device
+                inputs, labels = inputs.to(self.device), labels.to(self.device)
+
+                # Zero the parameter gradients
+                self.optimizer.zero_grad()
+
+                # Forward pass
+                outputs = self(inputs)
+                
+                loss = self.loss(outputs, labels)
+
+                # Backward pass and update
+                loss.backward()
+                self.optimizer.step()
+
+                running_loss += loss.item()
+            # if verbose:
+            print(f"Epoch {epoch + 1}/{epochs}, Loss: {running_loss / len(train_set)}")
+
+    def validate(self, validation_set):
+        """Validate the model on a given validation set."""
+        self.eval()  # Set model to eval mode (disable dropout, etc.)
+        validation_loss = 0.0
+        with torch.no_grad():  # No need to track gradients during validation
+            for inputs, labels in validation_set:
+                inputs, labels = inputs.to(self.device), labels.to(self.device)
+                outputs = self(inputs)
+                loss = self.loss(outputs, labels)
+                validation_loss += loss.item()
+        return validation_loss / len(validation_set)
+
+    def inference(self, x):
+        """Make predictions for the given test set."""
+        self.eval()
+        with torch.no_grad():
+            x = x.to(self.device)
+            outputs = self(x)
+            return outputs
diff --git a/src/OpenHosta/exec/predict/model/hosta_model.py b/src/OpenHosta/exec/predict/model/hosta_model.py
new file mode 100644
index 0000000..08df057
--- /dev/null
+++ b/src/OpenHosta/exec/predict/model/hosta_model.py
@@ -0,0 +1,33 @@
+from abc import ABC
+from typing import Optional
+
+import torch
+from torch import nn
+
+
+class HostaModel(ABC, nn.Module):
+    def __init__(self, device: Optional[str]):
+        self.device = device if device is not None else ('cuda' if torch.cuda.is_available() else 'cpu')
+        self.layers = []
+        super().__init__()
+
+    def trainer(self, train_set, epochs):
+        pass
+
+    def forward(self, x):
+        for layer in self.layers:
+            x = layer(x)
+        return x
+
+    def validate(self, validation_set):
+        pass
+
+    def inference(self, x):
+        pass
+
+    def init_weights(self, path: str):
+        self.load_state_dict(torch.load(path, weights_only=True, map_location=self.device))
+        self.eval()
+
+    def save_weights(self, path: str):
+        torch.save(self.state_dict(), path, )
diff --git a/src/OpenHosta/exec/predict/model/model_provider.py b/src/OpenHosta/exec/predict/model/model_provider.py
new file mode 100644
index 0000000..dc221c2
--- /dev/null
+++ b/src/OpenHosta/exec/predict/model/model_provider.py
@@ -0,0 +1,34 @@
+from typing import Optional, Literal, get_origin
+
+from .builtins.classification import Classification
+from .builtins.linear_regression import LinearRegression
+from .hosta_model import HostaModel
+from .neural_network import NeuralNetwork
+from .neural_network_types import ArchitectureType
+from ..predict_config import PredictConfig
+from ....core.hosta import Func
+from ....utils.torch_nn_utils import type_size
+
+
+class HostaModelProvider:
+    @staticmethod
+    def from_hosta_func(func: Func, config: Optional[PredictConfig], architecture: Optional[NeuralNetwork], path: str, verbose: int) -> Optional[HostaModel]:
+        input_size = 0
+        for arg in func.f_type[0]:
+            input_size += type_size(arg, config.max_tokens)
+        output_size = type_size(func.f_type[1], config.max_tokens)
+        hosta_model: Optional[HostaModel] = None
+        if config is not None and config.model_type is not None:
+            if config.model_type == ArchitectureType.LINEAR_REGRESSION:
+                hosta_model = LinearRegression(architecture, input_size, output_size, config.complexity)
+            elif config.model_type == ArchitectureType.CLASSIFICATION:
+                hosta_model = Classification(architecture, input_size, output_size, config.complexity, 1)
+        else:
+            if get_origin(func.f_type[1]) == Literal:
+                hosta_model = Classification(architecture, input_size, output_size, 4, 1)
+            else:
+                hosta_model = LinearRegression(architecture, input_size, output_size, 4)
+
+        with open(path, 'w') as file:
+            file.write(NeuralNetwork.from_torch_nn(hosta_model).to_json())
+        return hosta_model
diff --git a/src/OpenHosta/exec/predict/model/neural_network.py b/src/OpenHosta/exec/predict/model/neural_network.py
new file mode 100644
index 0000000..ae61278
--- /dev/null
+++ b/src/OpenHosta/exec/predict/model/neural_network.py
@@ -0,0 +1,155 @@
+import json
+from typing import Optional
+
+from torch import nn
+
+from .neural_network_types import LayerType, OptimizerAlgorithm, LossFunction, Layer
+from ....utils.torch_nn_utils import pytorch_layer_to_custom, pytorch_loss_to_custom, pytorch_optimizer_to_custom
+
+
+class NeuralNetwork:
+    def __init__(self):
+        """
+        Initialize a NeuralNetwork object.
+        """
+        self.layers: list[Layer] = []
+        self.loss_function: Optional[LossFunction] = None
+        self.optimizer: Optional[OptimizerAlgorithm] = None
+
+    def add_layer(self, layer: Layer):
+        """
+        Add a layer to the neural network.
+
+        :param layer: The layer to be added.
+        :type layer: Layer
+        :raises TypeError: If the _inputs is not an instance of Layer.
+        """
+        if not isinstance(layer, Layer):
+            raise TypeError("Expected a Layer instance")
+        self.layers.append(layer)
+
+    def summary(self):
+        """
+        Print a summary of the neural network layers.
+        """
+        for i, layer in enumerate(self.layers):
+            print(f"Layer {i + 1}: {layer}")
+
+    def set_loss_function(self, loss_function: LossFunction):
+        """
+        Set the loss function for the neural network.
+
+        :param loss_function: The loss function to be set.
+        :type loss_function: LossFunction
+        """
+        self.loss_function = loss_function
+
+    def set_optimizer(self, optimizer: OptimizerAlgorithm):
+        """
+        Set the optimizer for the neural network.
+
+        :param optimizer: The optimizer to be set.
+        :type optimizer: OptimizerAlgorithm
+        """
+        self.optimizer = optimizer
+
+    def to_json(self) -> str:
+        """
+        Convert the neural network configuration to a JSON string.
+
+        :return: JSON string representation of the neural network
+        :rtype: str
+        """
+        network_dict = {
+            "layers": [
+                layer.to_json()
+                for layer in self.layers
+            ]
+        }
+        if self.loss_function is not None:
+            network_dict["loss_function"] = self.loss_function.name
+        if self.optimizer is not None:
+            network_dict["optimizer"] = self.optimizer.name
+        return json.dumps(network_dict, indent=2)
+
+    @classmethod
+    def from_json(cls, json_str: str) -> 'NeuralNetwork':
+        """
+        Create a neural network from a JSON string configuration.
+
+        :param json_str: JSON string containing the neural network configuration
+        :type json_str: str
+        :return: A new NeuralNetwork instance
+        :rtype: NeuralNetwork
+        :raises ValueError: If the JSON string is invalid or contains invalid configuration
+        """
+        try:
+            network_dict = json.loads(json_str)
+            network = cls()
+
+            if network_dict.get("loss_function", None) is not None:
+                network.loss_function = LossFunction[network_dict.get("loss_function", None)]
+            else:
+                network.loss_function = None
+
+            if network_dict.get("optimizer", None) is not None:
+                network.optimizer = OptimizerAlgorithm[network_dict.get("optimizer", None)]
+            else:
+                network.optimizer = None
+
+            # Add layers
+            for layer_dict in network_dict.get("layers", []):
+                layer = Layer(
+                    layer_type=LayerType[layer_dict.get("layer_type", None)],
+                    in_features=layer_dict.get("in_features"),
+                    out_features=layer_dict.get("out_features"),
+                    kernel_size=layer_dict.get("kernel_size"),
+                    stride=layer_dict.get("stride"),
+                    padding=layer_dict.get("padding"),
+                    dropout=layer_dict.get("dropout")
+                )
+                network.add_layer(layer)
+
+            return network
+
+        except (json.JSONDecodeError, KeyError, ValueError) as e:
+            raise ValueError(f"Invalid JSON configuration: {str(e)}")
+
+
+    @classmethod
+    def from_torch_nn(cls, torch_model: nn.Module, loss_fn=None, optimizer=None) -> 'NeuralNetwork':
+        """
+        Creates a NeuralNetwork instance from a torch.nn.Module model,
+        with optional loss function and optimizer mappings.
+
+        :param torch_model: The PyTorch neural network model.
+        :param loss_fn: PyTorch loss function instance (optional).
+        :param optimizer: PyTorch optimizer instance (optional).
+        :return: A NeuralNetwork instance.
+        """
+        network = cls()
+
+        # Iterating through the PyTorch model's children (layers)
+        for layer in torch_model.children():
+            try:
+                nn_layer = pytorch_layer_to_custom(layer)
+                network.add_layer(nn_layer)
+            except ValueError as e:
+                # print(f"Skipping unsupported layer: {e}")
+                pass
+
+        # Set loss function and optimizer if specified
+        if loss_fn is not None:
+            try:
+                network.loss_function = pytorch_loss_to_custom(loss_fn)
+            except ValueError as e:
+                print(f"Skipping unsupported loss function: {e}")
+
+        # Set optimizer if specified
+        if optimizer is not None:
+            try:
+                network.optimizer = pytorch_optimizer_to_custom(optimizer)
+            except ValueError as e:
+                print(f"Skipping unsupported optimizer: {e}")
+
+        return network
diff --git a/src/OpenHosta/exec/predict/model/neural_network_types.py b/src/OpenHosta/exec/predict/model/neural_network_types.py
new file mode 100644
index 0000000..1baae4d
--- /dev/null
+++ b/src/OpenHosta/exec/predict/model/neural_network_types.py
@@ -0,0 +1,154 @@
+from enum import Enum
+from typing import Optional, Union
+
+class ArchitectureType(Enum):
+    """
+    Enum for different built-in architectures for neural networks.
+    """
+    LINEAR_REGRESSION = 1
+    CLASSIFICATION = 2
+
+
+class LayerType(Enum):
+    """
+    Enum for different types of layers in a neural network.
+    https://pytorch.org/docs/stable/nn.html
+    """
+    LINEAR = 1
+    CONV2D = 2
+    RELU = 3
+    DROPOUT = 4
+    BATCHNORM1D = 5
+    BATCHNORM2D = 6
+    MAXPOOL2D = 7
+    AVGPOOL2D = 8
+    SIGMOID = 9
+    SOFTMAX = 10
+    TANH = 11
+
+class OptimizerAlgorithm(Enum):
+    """
+    Enum for different types of optimizers in a neural network.
+    https://pytorch.org/docs/stable/optim.html#algorithms
+    """
+    ADADELTA = 1
+    ADAFACTOR = 2
+    ADAGRAD = 3
+    ADAM = 4
+    ADAMW = 5
+    SPARSEADAM = 6
+    ADAMAX = 7
+    ASGD = 8
+    LBFGS = 9
+    NADAM = 10
+    RADAM = 11
+    RMSPROP = 12
+    RPROP = 13
+    SGD = 14
+
+class Device(Enum):
+    """
+    Enum for different types of devices to run the neural network
+    """
+    CPU = "cpu"
+    CUDA = "cuda"
+
+class LossFunction(Enum):
+    """
+    Enum for different types of loss functions in a neural network.
+    https://pytorch.org/docs/stable/nn#loss-functions
+    """
+    L1_LOSS = 1
+    MSE_LOSS = 2
+    CROSS_ENTROPY_LOSS = 3
+    CTC_LOSS = 4
+    NLL_LOSS = 5
+    POISSON_NLL_LOSS = 6
+    GAUSSIAN_NLL_LOSS = 7
+    KL_DIV_LOSS = 8
+    BCE_LOSS = 9
+    BCE_WITH_LOGITS_LOSS = 10
+    MARGIN_RANKING_LOSS = 11
+    HINGE_EMBEDDING_LOSS = 12
+    MULTI_LABEL_MARGIN_LOSS = 13
+    HUBER_LOSS = 14
+    SMOOTH_L1_LOSS = 15
+    SOFT_MARGIN_LOSS = 16
+    MULTI_LABEL_SOFT_MARGIN_LOSS = 17
+    COSINE_EMBEDDING_LOSS = 18
+    MULTI_MARGIN_LOSS = 19
+    TRIPLET_MARGIN_LOSS = 20
+    TRIPLET_MARGIN_WITH_DISTANCE_LOSS = 21
+
+class Layer:
+    """
+    Initialize a Layer object.
+
+    :param layer_type: The type of the layer.
+    :param in_features: Number of _inputs features or channels.
+    :param out_features: Number of _outputs features or channels.
+    :param kernel_size: Size of the kernel/filter.
+    :param stride: Stride of the kernel/filter.
+    :param padding: Padding added to the _inputs.
+    :param dropout: Dropout rate.
+    """
+    def __init__(
+        self,
+        layer_type: LayerType,
+        in_features: Optional[int] = None,
+        out_features: Optional[int] = None,
+        kernel_size: Optional[Union[int, tuple[int, int]]] = None,
+        stride: Optional[Union[int, tuple[int, int]]] = None,
+        padding: Optional[Union[int, str]] = None,
+        dropout: Optional[float] = None,
+    ):
+        self.layer_type: LayerType = layer_type
+        self.in_features: Optional[int] = in_features
+        self.out_features: Optional[int] = out_features
+        self.kernel_size: Optional[Union[int, tuple[int, int]]] = kernel_size
+        self.stride: Optional[Union[int, tuple[int, int]]] = stride
+        self.padding: Optional[Union[int, str]] = padding
+        self.dropout: Optional[float] = dropout
+
+    def __repr__(self):
+        """
+        Return a string representation of the Layer object.
+
+        :return: String representation of the Layer object.
+        :rtype: str
+        """
+        return (
+            f"Layer(type={self.layer_type}, "
+            f"in_features={self.in_features}, "
+            f"out_features={self.out_features}, "
+            f"kernel_size={self.kernel_size}, "
+            f"stride={self.stride}, "
+            f"padding={self.padding}, "
+            f"dropout={self.dropout})"
+        )
+
+    def to_json(self):
+        """
+        Convert the layer configuration to a JSON string.
+
+        :return: JSON string representation of the layer
+        :rtype: str
+        """
+        json = {}
+
+        if self.layer_type is not None:
+            json["layer_type"] = self.layer_type.name
+        if self.in_features is not None:
+            json["in_features"] = self.in_features
+        if self.out_features is not None:
+            json["out_features"] = self.out_features
+        if self.kernel_size is not None:
+            json["kernel_size"] = self.kernel_size
+        if self.stride is not None:
+            json["stride"] = self.stride
+        if self.padding is not None:
+            json["padding"] = self.padding
+        if self.dropout is not None:
+            json["dropout"] = self.dropout
+
+        return json
diff --git a/src/OpenHosta/exec/predict/predict.py b/src/OpenHosta/exec/predict/predict.py
new file mode 100644
index 0000000..c5d7e2f
--- /dev/null
+++ b/src/OpenHosta/exec/predict/predict.py
@@ -0,0 +1,160 @@
+import os
+from typing import Union, Optional
+
+from .dataset.dataset import HostaDataset, SourceType
+from .dataset.oracle import LLMSyntheticDataGenerator
+from .model import HostaModel
+from .model.model_provider import HostaModelProvider
+from .model.neural_network import NeuralNetwork
+from .predict_config import PredictConfig
+from .predict_memory import PredictMemory, File
+from ...core.config import Model, DefaultModel
+from ...core.hosta import Hosta, Func
+
+
+def predict(
+    config: PredictConfig = PredictConfig(),
+    oracle: Optional[Union[Model, HostaDataset]] = None,
+    verbose: int = 0 
+) -> Union[int, float, bool, str]:
+    """
+    Predicts a result using an existing model or by creating a new one.
+    
+    Args:
+        config: Model configuration
+        oracle: Reference model or dataset for data generation
+        verbose: Enables detailed logging
+    
+    Returns:
+        Model prediction
+    """
+    assert config is not None, "Please provide a valid configuration not None"
+    assert verbose is not None and 0 <= verbose <= 2, "Please provide a valid verbose level (0, 1 or 2) default is 0"
+
+    func: Func = getattr(Hosta(), "_infos")
+
+    name = config.name if config and config.name else str(func.f_name)
+    base_path = config.path if config and config.path else os.getcwd()
+    memory: PredictMemory = PredictMemory.load(base_path=base_path, name=name)
+
+    dataset: Optional[HostaDataset] = None
+    
+    hosta_model: HostaModel = get_hosta_model(memory.architecture, func, config, verbose)
+    if verbose == 2:
+        print(f"[\033[92mArchitecture\033[0m] loaded, type : {type(hosta_model).__name__}")
+        
+    if not load_weights(memory, hosta_model, verbose):
+        train_model(config, memory, hosta_model, dataset, oracle, func, verbose)
+    
+    if dataset is None:
+        dataset = HostaDataset.from_input(func.f_args, verbose)
+    else:
+        dataset.prepare_inference(func.f_args)
+    torch_prediction = hosta_model.inference(dataset.inference.input)
+    prediction = dataset.decode(torch_prediction, func_f_type=func.f_type[1])
+    if predict is list:
+        return prediction[0]
+    else:
+        return prediction
+
+
+def get_hosta_model(architecture_file: File, func: Func, config: Optional[PredictConfig] = None, verbose: int = 0) -> HostaModel:
+    """
+    Load or create a new model.
+    """
+    architecture: Optional[NeuralNetwork] = None
+
+    if architecture_file.exist:
+        with open(architecture_file.path, "r") as file:
+            json = file.read()
+        architecture = NeuralNetwork.from_json(json)
+        if verbose == 2:
+            print(f"[\033[92mArchitecture\033[0m] found at {architecture_file.path}")
+    else:
+        if verbose == 2:
+            print(f"[\033[93mArchitecture\033[0m] not found, creating one")
+    return HostaModelProvider.from_hosta_func(func, config, architecture, architecture_file.path, verbose)
+
+
+def load_weights(memory: PredictMemory, hosta_model: HostaModel, verbose :int) -> bool:
+    """
+    Load weights if they exist.
+    """
+    if memory.weights.exist:
+
+        if verbose == 2:
+            print(f"[\033[92mWeights\033[0m] found at {memory.weights.path}")
+        hosta_model.init_weights(memory.weights.path)
+        return True
+
+    if verbose == 2:
+        print(f"[\033[92mWeights\033[0m] not found generate new ones")
+    return False 
+
+
+def train_model(config: PredictConfig, memory: PredictMemory, model: HostaModel, dataset: HostaDataset, oracle: Optional[Union[Model, HostaDataset]], func: Func, verbose: int) -> None:
+    """
+    Prepare the data and train the model.
+    """
+    if memory.data.exist:
+        if verbose == 2:
+            print(f"[\033[92mData\033[0m] found at {memory.data.path}")
+        train_set, val_set = HostaDataset.from_data(memory.data.path, batch_size=1, shuffle=True, train_set_size=0.8, verbose=verbose) # verbose will prcess all the example and add it to val_set
+    else:
+        if verbose == 2:
+            print(f"[\033[93mData\033[0m] not processed, preparing data")
+        train_set, val_set = prepare_dataset(config, memory, dataset, func, oracle, verbose)
+
+    if config.epochs is None:
+        config.epochs = int(2 * len(train_set.dataset) / config.batch_size if config.batch_size != len(train_set.dataset)\
+                                else 2 * len(train_set.dataset))
+        assert config.epochs > 0, f"epochs must be greater than 0 now it's {config.epochs}"
+
+    model.trainer(train_set, epochs=config.epochs)
+
+    if verbose > 0:
+        model.validate(val_set)
+    
+    model.save_weights(memory.weights.path)
+
+
+def prepare_dataset(config: PredictConfig, memory: PredictMemory, dataset: HostaDataset, func: Func, oracle: Optional[Union[Model, HostaDataset]], verbose: int) -> tuple:
+    """
+    Prepare the dataset for training.
+    """
+    if config.dataset_path is not None:
+        if verbose == 2:
+            print(f"[\033[92mDataset\033[0m] found at {config.dataset_path}")
+        dataset = HostaDataset.from_files(config.dataset_path, SourceType.CSV, verbose) # or JSONL jsp comment faire la détection la
+    else :
+        if verbose == 2:
+            print(f"[\033[93mDataset\033[0m] not found, generate data")
+        dataset = generate_data(func, oracle, verbose)
+        dataset.save(os.path.join(memory.predict_dir, "generated_data.csv"), SourceType.CSV)
+        if verbose == 2:
+            print(f"[\033[92mDataset\033[0m] generated!")
+    dataset.encode(max_tokens=10)
+    dataset.tensorify()
+    dataset.save_data(memory.data.path)
+
+    if config.batch_size is None:
+        config.batch_size = int(0.05 * len(dataset.data)) if 0.05 * len(dataset.data) > 1 else len(dataset.data)
+    train_set, val_set = dataset.convert_data(batch_size=config.batch_size, shuffle=True, train_set_size=0.8)
+
+    if verbose == 2:
+        print(f"[\033[92mDataset\033[0m] processed and saved at {memory.data.path}")
+    return train_set, val_set
+
+
+def generate_data(func: Func, oracle: Optional[Union[Model, HostaDataset]], verbose: int) -> HostaDataset:
+    """
+    Generate data for training.
+    """
+    data = LLMSyntheticDataGenerator.generate_synthetic_data(
+        func=func,
+        request_amounts=3,  # TODO: make it a parameter
+        examples_in_req=80,  # TODO: make it a parameter
+        model=oracle if oracle is not None else DefaultModel().get_default_model()
+    )
+    return HostaDataset.from_list(data, verbose)
+
diff --git a/src/OpenHosta/exec/predict/predict_config.py b/src/OpenHosta/exec/predict/predict_config.py
new file mode 100644
index 0000000..645b7ca
--- /dev/null
+++ b/src/OpenHosta/exec/predict/predict_config.py
@@ -0,0 +1,66 @@
+from typing import Optional
+
+from .model.neural_network_types import ArchitectureType
+
+
+class PredictConfig:
+    def __init__(self,
+                 model_type: ArchitectureType = None,
+                 name: str = None,
+                 path: str = None,
+                 version: str = None,
+                 complexity: int = 4,
+                 max_tokens: int = 10,
+                 epochs: Optional[int] = None,
+                 batch_size: Optional[int] = None,
+                 learning_rate: Optional[float] = None,
+                 get_loss: Optional[float] = None,
+                 dataset_path: Optional[str] = None
+             ):
+        self.model_type: ArchitectureType = model_type
+
+        self.name: str = name
+        self.path: str = path
+        self.version: str = version
+
+        self.complexity: int = complexity
+        self.max_tokens: int = max_tokens
+
+        self.batch_size: int = batch_size
+        self.epochs: int = epochs
+        self.learning_rate: float = learning_rate
+        self.get_loss: float = get_loss
+        self.dataset_path: str = dataset_path
+
+    def to_json(self):
+        return f"""{{
+            "name": "{self.name}",
+            "model_type": "{self.model_type}",
+            "weight_path": "{self.path}",
+            "version": "{self.version}",
+            "complexity": {self.complexity},
+            "max_tokens": {self.max_tokens},
+            "epochs": {self.epochs},
+            "batch_size": {self.batch_size},
+            "learning_rate": {self.learning_rate},
+            "get_loss": {self.get_loss},
+            "dataset_path": "{self.dataset_path}",
+        }}"""
+
+    @staticmethod
+    def from_json(json_str: str):
+        import json
+        data = json.loads(json_str)
+        return PredictConfig(
+            name=data["name"],
+            model_type=ArchitectureType(data["model_type"]),
+            path=data["path"],
+            version=data["version"],
+            complexity=data["complexity"],
+            max_tokens=data["max_tokens"],
+            epochs=data["epochs"],
+            batch_size=data["batch_size"],
+            learning_rate=data["learning_rate"],
+            get_loss=data["get_loss"],
+            dataset_path=data["dataset_path"],
+        )
diff --git a/src/OpenHosta/exec/predict/predict_memory.py b/src/OpenHosta/exec/predict/predict_memory.py
new file mode 100644
index 0000000..74ebd04
--- /dev/null
+++ b/src/OpenHosta/exec/predict/predict_memory.py
@@ -0,0 +1,82 @@
+import os
+from enum import Enum
+from typing import Optional, Dict, NamedTuple
+
+from ...core.memory import HostaMemory
+
+# 1. Structures de base
+File = NamedTuple("File", [("exist", bool), ("path", str)])
+
+class PredictFileType(Enum):
+    """Enumaration for different types of files in the prediction memory."""
+    ARCHITECTURE = "model.json"
+    WEIGHTS = "weights.pth"
+    DICTIONARY = "dictionary.txt"
+    DATA = "data.json"
+    SUMMARY = "summary.txt"
+
+class PredictMemory(HostaMemory):
+    """
+    This module defines the PredictMemory class, which manages the structure of files for prediction purposes. 
+    It inherits from HostaMemory, which handles the main cache directory.
+
+    He uses the File structure to store the status of each file and his path.
+    """
+    def __init__(self, base_path: Optional[str] = None, *, name: str = None, **kwargs):
+        super().__init__(base_path=base_path, **kwargs)
+        if name is None:
+            raise ValueError("name must be specified")
+        self.name = name
+        self.paths: Dict[PredictFileType, str] = {}
+        self.files: Dict[PredictFileType, File] = {}
+
+    @staticmethod
+    def load(base_path: Optional[str] = None, name: str = None) -> 'PredictMemory':
+        """
+        Static method to create or load a memory.
+        Args:
+            base_path: Base path for the memory.
+            name: Name of the memory.
+        Returns:
+            PredictMemory instance.
+        """
+        memory = PredictMemory(base_path=base_path, name=name)
+        memory._initialize_predict_directory
+        memory._check_files()
+        return memory
+
+    @property
+    def _initialize_predict_directory(self) -> None:
+        """
+        Initializes the directory and file structure for predictions.
+        """
+        self.predict_dir = os.path.join(self.cache_root, self.name)
+        self._ensure_directory_exists(self.predict_dir)
+        self.paths = {
+            file_type: os.path.join(self.predict_dir, file_type.value)
+            for file_type in PredictFileType
+        }
+
+    def _check_files(self) -> None:
+        """
+        Checks the status of all files.
+        """
+        for file_type in PredictFileType:
+            path = self.paths[file_type]
+            exists = os.path.exists(path) and os.path.getsize(path) > 0
+            self.files[file_type] = File(exist=exists, path=path)
+
+    @property
+    def architecture(self) -> File: return self.files[PredictFileType.ARCHITECTURE]
+
+    @property
+    def weights(self) -> File: return self.files[PredictFileType.WEIGHTS]
+
+    @property
+    def data(self) -> File: return self.files[PredictFileType.DATA]
+
+    @property
+    def summary(self) -> File: return self.files[PredictFileType.SUMMARY]
+
+    @property
+    def dictionary(self) -> File: return self.files[PredictFileType.DICTIONARY]
diff --git a/src/OpenHosta/exec/thinkof.py b/src/OpenHosta/exec/thinkof.py
new file mode 100644
index 0000000..6c36b53
--- /dev/null
+++ b/src/OpenHosta/exec/thinkof.py
@@ -0,0 +1,54 @@
+from __future__ import annotations
+
+import json
+from pydoc import locate
+
+from .emulate import emulate
+from ..core.config import DefaultManager
+from ..core.hosta import Func
+from ..utils.errors import RequestError
+from ..utils.meta_prompt import THOUGHT_PROMPT
+
+
+def guess_type(key: str, *args) -> object:
+    l_default = DefaultManager.get_default_model()
+
+    l_user_prompt = (
+        "Here's the function behavior:\n"
+        + f"{key}\n"
+        + "Here's the arguments:\n"
+        + f"{args}\n"
+    )
+
+    response = l_default.simple_api_call(
+        sys_prompt=f"{THOUGHT_PROMPT!r}{THOUGHT_PROMPT.USER_SEP}",
+        user_prompt=l_user_prompt,
+        temperature=0.5,
+    )
+
+    type_json = response["choices"][0]["message"]["content"]
+    type_dict = json.loads(type_json)
+    type_str = str(type_dict["type"])
+
+    return locate(type_str)
+
+
+def thinkof(key):
+
+    def inner_func(*args, **kwargs):
+        _infos: Func = Func()
+
+        if not hasattr(inner_func, "_return_type"):
+            setattr(inner_func, "_return_type", guess_type(key, *args))
+
+        _infos.f_def = key
+        _infos.f_call = str(*args)
+        _infos.f_type = ([], inner_func._return_type)
+
+        try:
+            result = emulate(_infos=_infos)
+        except Exception as e:
+            raise RequestError(f"[thinkof] Cannot emulate the function.\n{e}")
+        return result
+
+    return inner_func
diff --git a/src/OpenHosta/exec/thought.py b/src/OpenHosta/exec/thought.py
new file mode 100644
index 0000000..2b393b2
--- /dev/null
+++ b/src/OpenHosta/exec/thought.py
@@ -0,0 +1,8 @@
+from __future__ import annotations
+
+from ..core.hosta import Hosta, CotType
+
+
+def thought(task: str) -> None:
+    x = Hosta()
+    x._bdy_add('cot', CotType(task=task))
diff --git a/src/OpenHosta/exec/use.py b/src/OpenHosta/exec/use.py
new file mode 100644
index 0000000..943c676
--- /dev/null
+++ b/src/OpenHosta/exec/use.py
@@ -0,0 +1,34 @@
+from __future__ import annotations
+
+from typing import Any, Callable, Union
+
+from ..core.hosta import Hosta, UseType
+
+all = (
+    "use",
+    "VAR",
+    "TOOL",
+    "RAG",
+    "DB"
+)
+
+
+class VAR:
+    pass
+
+
+class TOOL:
+    pass
+
+
+class RAG:
+    pass
+
+
+class DB:
+    pass
+
+
+def use(obj: Union[Callable, Any], typ: Union[VAR, TOOL, RAG, DB], title: str):
+    x = Hosta()
+    x._bdy_add('use', UseType())
diff --git a/src/OpenHosta/model.py b/src/OpenHosta/model.py
deleted file mode 100644
index f49acde..0000000
--- a/src/OpenHosta/model.py
+++ /dev/null
@@ -1,102 +0,0 @@
-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
deleted file mode 100644
index c468a02..0000000
--- a/src/OpenHosta/predict.py
+++ /dev/null
@@ -1,195 +0,0 @@
-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/prompt.py b/src/OpenHosta/prompt.py
deleted file mode 100644
index a6be5cd..0000000
--- a/src/OpenHosta/prompt.py
+++ /dev/null
@@ -1,97 +0,0 @@
-import os
-import json
-import sys
-
-
-class PromptMananger:
-    def __init__(self, json_path=None):
-        if json_path is None:
-            try:
-                self.path = os.path.join(os.path.dirname(__file__), "prompt.json")
-            except Exception as e:
-                self.path = ""
-                sys.stderr.write(f"[JSON_ERROR] Impossible to find prompt.json:\n{e}")
-                return
-        else:
-            self.path = json_path
-
-        try:
-            with open(self.path, "r", encoding="utf-8") as file:
-                self.json = json.load(file)
-                self.prompts = {item["key"]: item for item in self.json["prompts"]}
-        except FileNotFoundError:
-            sys.stderr.write(f"[JSON_ERROR] File not found: {self.path}\n")
-            self.prompts = {}
-        except json.JSONDecodeError as e:
-            sys.stderr.write(f"[JSON_ERROR] JSON decode error:\n{e}\n")
-            self.prompts = {}
-
-    def get_prompt(self, key):
-        prompt = self.prompts.get(key)
-        if prompt:
-            return prompt["text"]
-        sys.stderr.write(f"[JSON_ERROR] Prompt not found\n")
-        return None
-
-    def get_prompt_details(self, key):
-        prompt = self.prompts.get(key)
-        if prompt:
-            return prompt
-        sys.stderr.write(f"[JSON_ERROR] Prompt not found\n")
-        return None
-
-    def set_prompt(self, name: str, category: str, version: str, filepath: str):
-        json_filepath = "prompt.json"
-        new = {"key": "", "text": "", "category": "", "version": ""}
-
-        try:
-            with open(filepath, "r", encoding="utf-8") as file:
-                prompt = file.read()
-        except FileNotFoundError:
-            sys.stderr.write(f"File {filepath} not found.")
-        except IOError as e:
-            sys.stderr.write(f"File error: {e}")
-        else:
-            try:
-                with open(json_filepath, "r", encoding="utf-8") as json_file:
-                    data = json.load(json_file)
-            except FileNotFoundError:
-                sys.stderr.write(f"File {json_filepath} not found.")
-            except IOError as e:
-                sys.stderr.write(f"File error: {e}")
-            except Exception as e:
-                sys.stderr.write(e)
-            else:
-                new["key"], new["category"], new["version"] = name, category, version
-                new["text"] = prompt
-
-                found = False
-                for elem in data["prompts"]:
-                    if elem["key"] == new["key"]:
-                        elem["text"] = new["text"]
-                        elem["version"] = new["version"]
-                        found = True
-                        break
-
-                if not found:
-                    data["prompts"].append(new)
-
-                try:
-                    with open(json_filepath, "w", encoding="utf-8") as json_file:
-                        json.dump(data, json_file, ensure_ascii=False, indent=4)
-                except FileNotFoundError:
-                    sys.stderr.write(f"File {json_filepath} not found.")
-                except IOError as e:
-                    sys.stderr.write(f"File error: {e}")
-                except Exception as e:
-                    sys.stderr.write(e)
-                else:
-                    print(f"{name} prompt has been added to {json_filepath}")
-
-    def show_prompt(self, key):
-        prompt = self.prompts.get(key)
-        if prompt:
-            print(prompt["text"])
-            return prompt["text"]
-        sys.stderr.write(f"[JSON_ERROR] Prompt not found\n")
-        return None
diff --git a/src/OpenHosta/requirements.txt b/src/OpenHosta/requirements.txt
deleted file mode 100644
index 32c71f3..0000000
--- a/src/OpenHosta/requirements.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-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
deleted file mode 100644
index ef059de..0000000
--- a/src/OpenHosta/thought.py
+++ /dev/null
@@ -1,76 +0,0 @@
-import sys
-import json
-from pydoc import locate
-from pydantic import create_model
-
-from .emulate import _exec_emulate
-from .config import DefaultManager
-from .prompt import PromptMananger
-
-l_default = DefaultManager.get_default_model()
-_x = PromptMananger()
-_thought_sys_prompt = _x.get_prompt("thought")
-
-
-def thought(key):
-    _function_infos = {
-        "function_def": "",
-        "function_call": "",
-        "return_type": None,
-        "return_type": None,
-        "ho_example": None,
-        "function_locals": None,
-        "return_caller": None,
-    }
-
-    def inner_func(*args, **kwargs):
-        global l_default, _thought_sys_prompt
-
-        l_user_prompt = (
-            "Here's the function behavior:\n"
-            + f"{key}\n"
-            + "Here's the arguments:\n"
-            + f"{args}\n"
-        )
-
-        response = l_default.api_call(
-            sys_prompt=_thought_sys_prompt,
-            user_prompt=l_user_prompt,
-            creativity=0.5,
-            diversity=0.5,
-        )
-
-        data = response.json()
-        type_json = data["choices"][0]["message"]["content"]
-        type_dict = json.loads(type_json)
-        type_str = str(type_dict["type"])
-
-        _function_infos["return_caller"] = locate(type_str)
-        setattr(inner_func, "_return_type", _function_infos["return_caller"])
-
-        new_model = create_model(
-            "Hosta_return_shema",
-            return_hosta_type=(_function_infos["return_caller"], ...),
-        )
-        _function_infos["return_type"] = new_model.model_json_schema()
-
-        typed = (
-            str(args)
-            + "\n"
-            + 'Here\'s the return type that must respect for your response. The python type is in the key "type" of this JSON schema:\n'
-            + str(type_dict)
-            + "\n"
-        )
-
-        try:
-            _function_infos["function_def"] = key
-            _function_infos["function_call"] = typed
-            result = _exec_emulate(_function_infos, inner_func)
-        except Exception as e:
-            sys.stderr.write(f"{e}")
-            sys.stderr.write("[LMDA_ERROR]")
-            result = None
-        return result
-
-    return inner_func
- 
\ No newline at end of file
diff --git a/src/OpenHosta/trainset.py b/src/OpenHosta/trainset.py
deleted file mode 100644
index c2b1cfc..0000000
--- a/src/OpenHosta/trainset.py
+++ /dev/null
@@ -1,59 +0,0 @@
-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()
diff --git a/src/OpenHosta/utils/__init__.py b/src/OpenHosta/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/OpenHosta/errors.py b/src/OpenHosta/utils/errors.py
similarity index 58%
rename from src/OpenHosta/errors.py
rename to src/OpenHosta/utils/errors.py
index 92bd1ff..8820269 100644
--- a/src/OpenHosta/errors.py
+++ b/src/OpenHosta/utils/errors.py
@@ -1,29 +1,44 @@
+from __future__ import annotations
+
 from typing import Literal
 
 __all__ = (
     "RequestError",
     "ApiKeyError",
-    "FrameError"
+    "FrameError",
+    "InvalidStructureError"
 )
 
 OhErrorCodes = Literal[
     ""
 ]
 
+
 class OhErrorMixin(Exception):
     """ Base class for other customs exceptions """
-    def __init__(self, message:str):
+
+    def __init__(self, message: str):
         super().__init__(message)
         self.message = message
-        
-    def __str__(self)->str:
+
+    def __str__(self) -> str:
         return f"{self.message}\n"
-    
+
+
 class RequestError(OhErrorMixin):
     """ Raised when a request to a llm went wrong """
 
+
 class ApiKeyError(RequestError):
     """ Raised when API key is missing or incorrect """
 
+
 class FrameError(OhErrorMixin):
-    """ Raised when the frame inspection fail """
\ No newline at end of file
+    """ Raised when the frame inspection fail """
+
+
+class InvalidStructureError(OhErrorMixin):
+    """ Raised when the bosy's function aren't placed correctly """
+
+
+# création d'agent + multimodal + tools
diff --git a/src/OpenHosta/utils/hosta_type.py b/src/OpenHosta/utils/hosta_type.py
new file mode 100644
index 0000000..87b9874
--- /dev/null
+++ b/src/OpenHosta/utils/hosta_type.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Any, Literal, Union, TypedDict
+
+
+class ExampleType(TypedDict):
+    in_: Any
+    out: Any
+
+
+class CotType(TypedDict):
+    task: str
+
+
+class UseType(TypedDict):
+    pass
+
+
+MemKey = Literal["ex", "cot", "use"]
+MemValue = Union[CotType, ExampleType, UseType]
+
+
+@dataclass
+class MemoryNode:
+    key: MemKey
+    id: int
+    value: MemValue
diff --git a/src/OpenHosta/utils/import_handler.py b/src/OpenHosta/utils/import_handler.py
new file mode 100644
index 0000000..6694f2b
--- /dev/null
+++ b/src/OpenHosta/utils/import_handler.py
@@ -0,0 +1,13 @@
+from __future__ import annotations
+
+all = (
+    "is_pydantic"
+    "is_torch"
+)
+
+is_pydantic = False
+try:
+    import pydantic
+    is_pydantic = True
+except ImportError:
+    is_pydantic = False
diff --git a/src/OpenHosta/utils/meta_prompt.py b/src/OpenHosta/utils/meta_prompt.py
new file mode 100644
index 0000000..7791ca0
--- /dev/null
+++ b/src/OpenHosta/utils/meta_prompt.py
@@ -0,0 +1,73 @@
+from __future__ import annotations
+
+from typing import List, Callable
+
+def print_last_prompt(func:Callable):
+    if hasattr(func, "_last_request"):
+        if "sys_prompt" in func._last_request:
+            print("[SYSTEM PROMPT]")
+            print(func._last_request["sys_prompt"])
+        if "user_prompt" in func._last_request:
+            print("[USER PROMPT]")
+            print(func._last_request["user_prompt"])
+    else:
+        print("No prompt found for this function.")
+
+class MetaPrompt:
+
+    def __init__(self, shards: List[str]):
+        for shard in shards:
+            if not isinstance(shard, str):
+                raise ValueError(
+                    "[MetaPrompt.__init__] Shards must be strings.")
+            try:
+                setattr(self, "_{}".format(shard.upper()), "")
+            except:
+                raise AttributeError(
+                    f"[MetaPrompt.__init__] Failed to initialize {shard} attributs")
+
+    def __repr__(self):
+        ctx = {}
+        for key, value in self.__dict__.items():
+            if key.startswith("CTX_"):
+                ctx[key] = value
+        return "\n".join(ctx.values())
+
+
+EMULATE_PROMPT = MetaPrompt([
+    "CTX_MAIN",
+    "CTX_SEP1"
+    "CTX_EXAMPLE",
+    "CTX_SEP2"
+    "PRE_DEF",
+    "PRE_TYPE",
+    "PRE_SCHEMA",
+    "PRE_LOCALS",
+    "PRE_SELF",
+    "PRE_EXAMPLE",
+    "PRE_COT",
+    "USER_SEP"
+])
+EMULATE_PROMPT.CTX_MAIN = "## Context\n\nYou will act as an emulator of impossible-to-code functions. I will provide you with the description of the function using Python's way of declaring functions, but I won't provide the function body as I don't know how to code it. It might even be impossible to code. Therefore, you should not try to write the body. Instead, directly imagine the function output.\n\nIn the conversation, I will directly write the function call as if it was called in Python. You should directly answer with whatever you believe would be a good return for the function.\n\nIf the output is documented as a Python structure, you should translate it to JSON.\nYou should encode the return in valid JSON format, without comments, using the following format:\n```\n{\"return\":\"...\"}\n```\n\nThe output must be of the same type as that specified in the function call. If you don't have enough information or don't know how to answer, the output should be “None”. \n\nAny assumptions made should be reasonable based on the provided function description and should take into account the error handling of the function."  # pylint: disable=attribute-defined-outside-init
+EMULATE_PROMPT.CTX_SEP1 = "\n---\n"  # pylint: disable=attribute-defined-outside-init
+EMULATE_PROMPT.CTX_EXAMPLE = "## Examples\n\n**Example function definition:**\n```python\ndef example_function(a: int, b: dict) -> int:\n\t\"\"\"\n\tThis is an example function.\n\tIt adds two numbers.\n\t\"\"\"\n\treturn emulate()\n```\n\n**Example emulated function call:**\n```python\nresult = example_function(3, {\"value\": 7})\n```\n\n**Expected JSON output:**\n{\"return\": 10}"  # pylint: disable=attribute-defined-outside-init
+EMULATE_PROMPT.CTX_SEP2 = "\n---\n\n## Function infos\n"  # pylint: disable=attribute-defined-outside-init
+EMULATE_PROMPT.PRE_DEF = "Here's the function definition:"  # pylint: disable=attribute-defined-outside-init
+EMULATE_PROMPT.PRE_TYPE = "Here's the type annotation of the function:"  # pylint: disable=attribute-defined-outside-init
+EMULATE_PROMPT.PRE_SCHEMA = "If it isn't a native type, here's a schema describing the type annotation:"  # pylint: disable=attribute-defined-outside-init
+EMULATE_PROMPT.PRE_LOCALS = "Here's the function's locals variables which you can use as additional information to give your answer:"  # pylint: disable=attribute-defined-outside-init
+EMULATE_PROMPT.PRE_SELF = "Here's the method's class attributs variables which you can use as additional information to give your answer:"  # pylint: disable=attribute-defined-outside-init
+EMULATE_PROMPT.PRE_EXAMPLE = "Here are some examples of expected input and output:"  # pylint: disable=attribute-defined-outside-init
+EMULATE_PROMPT.PRE_COT = "To solve the request, you have to follow theses intermediate steps. Give only the final result, don't give the result of theses intermediate steps:"  # pylint: disable=attribute-defined-outside-init
+EMULATE_PROMPT.USER_SEP = "\n---\n"  # pylint: disable=attribute-defined-outside-init
+
+THOUGHT_PROMPT = MetaPrompt([
+    "CTX_MAIN",
+    "CXT_SEP",
+    "CTX_EXAMPLE",
+    "USER_SEP"
+])
+THOUGHT_PROMPT.CTX_PROMPT = "You will act as an emulator of impossible-to-code functions. I will provide you with the description of the function using Python's way of declaring functions, but I won't provide the function body as I don't know how to code it. It might even be impossible to code. Therefore, you should not try to write the body. Instead, directly imagine the function output.\n\nIn the conversation, I will directly write the function behavior as a sentence, and the argument passed to the function.\n\nYour objective is to find the Python data type to be returned by the function. Take into account the function's behavior, the wording and intent of the sentence, and the arguments given. You must give your answer without any comment and in the following JSON schema:\n```\n{\"type\": \"\"}\n```\n\nTo fill in the type key, you need to follow Python syntax, such as \"int\" or \"str\", depending on your answer."  # pylint: disable=attribute-defined-outside-init
+THOUGHT_PROMPT.CTX_SEP = "\n---\n"  # pylint: disable=attribute-defined-outside-init
+THOUGHT_PROMPT.CTX_EXAMPLE = "Here are a few examples:\n\nFunction behavior: \"Is a positive number\"\nArgument: 2\nExpected response: {\"type\": \"bool\"}\n\nFunction behavior: \"Multiply a number by 2\"\nArgument: 10\nExpected response: {\"type\": \"int\"}\n\nFunction behavior: \"Reverse a string\"\nArgument: \"Hello World!\"\nExpected response: {\"type\": \"str\"}\n\nFunction behavior: \"Sorts a list in ascending order\"\nArgument: (10, 5, 7, 12, 3)\nExpected response: {\"type\": \"list\"}"  # pylint: disable=attribute-defined-outside-init
+THOUGHT_PROMPT.USER_SEP = "\n---\n"  # pylint: disable=attribute-defined-outside-init
diff --git a/src/OpenHosta/utils/progress_bar.py b/src/OpenHosta/utils/progress_bar.py
new file mode 100644
index 0000000..b5e3f96
--- /dev/null
+++ b/src/OpenHosta/utils/progress_bar.py
@@ -0,0 +1,22 @@
+
+def print_progress_bar(iteration, total, prefix='', suffix='', decimals=1, length=25, fill='-', print_end="\r"):
+    """
+    Call in a loop to create terminal progress bar
+    @params:
+        iteration   - Required  : current iteration (Int)
+        total       - Required  : total iterations (Int)
+        prefix      - Optional  : prefix string (Str)
+        suffix      - Optional  : suffix string (Str)
+        decimals    - Optional  : positive number of decimals in percent complete (Int)
+        length      - Optional  : character length of bar (Int)
+        fill        - Optional  : bar fill character (Str)
+        print_end   - Optional  : end character (e.g. "\r", "\r\n") (Str)
+    """
+    percent = ("{0:." + str(decimals) + "f}").format(100 *
+                                                     (iteration / float(total)))
+    filled_length = int(length * iteration // total)
+    bar = fill * filled_length + '-' * (length - filled_length)
+    print(f'\r{prefix}  {bar}  {percent}% {suffix}', end=print_end)
+    # Print New Line on Complete
+    if iteration == total:
+        print()
diff --git a/src/OpenHosta/prompt.json b/src/OpenHosta/utils/prompt.json
similarity index 85%
rename from src/OpenHosta/prompt.json
rename to src/OpenHosta/utils/prompt.json
index 9e752fd..fdfe2ce 100644
--- a/src/OpenHosta/prompt.json
+++ b/src/OpenHosta/utils/prompt.json
@@ -20,9 +20,21 @@
         },
         {
             "key": "emulate",
-            "text": "## Context\n\nYou will act as an emulator of impossible-to-code functions. I will provide you with the description of the function using Python's way of declaring functions, but I won't provide the function body as I don't know how to code it. It might even be impossible to code. Therefore, you should not try to write the body. Instead, directly imagine the function output.\n\nIn the conversation, I will directly write the function call as if it was called in Python. You should directly answer with whatever you believe would be a good return for the function.\n\nIf the output is documented as a Python structure, you should translate it to JSON.\nYou should encode the return in valid JSON format, without comments, using the following format:\n```\n{\"return\":\"...\"}\n```\n\nThe output must be of the same type as that specified in the function call. If you don't have enough information or don't know how to answer, the output should be “None”. \n\nAny assumptions made should be reasonable based on the provided function description and should take into account the error handling of the function.\n\n---\n\n## Examples\n\n**Example function definition:**\n```python\ndef example_function(a: int, b: dict) -> int:\n\t\"\"\"\n\tThis is an example function.\n\tIt adds two numbers.\n\t\"\"\"\n\treturn emulate()\n```\n\n**Example emulated function call:**\n```python\nresult = example_function(3, {\\\"value\\\": 7})\n```\n\n**Expected JSON output:**\n{\"return\": 10}\n",
+            "text": "## Context\n\nYou will act as an emulator of impossible-to-code functions. I will provide you with the description of the function using Python's way of declaring functions, but I won't provide the function body as I don't know how to code it. It might even be impossible to code. Therefore, you should not try to write the body. Instead, directly imagine the function output.\n\nIn the conversation, I will directly write the function call as if it was called in Python. You should directly answer with whatever you believe would be a good return for the function.\n\nIf the output is documented as a Python structure, you should translate it to JSON.\nYou should encode the return in valid JSON format, without comments, using the following format:\n```\n{\"return\":\"...\"}\n```\n\nThe output must be of the same type as that specified in the function call. If you don't have enough information or don't know how to answer, the output should be “None”. \n\nAny assumptions made should be reasonable based on the provided function description and should take into account the error handling of the function.\n\n---\n\n## Examples\n\n**Example function definition:**\n```python\ndef example_function(a: int, b: dict) -> int:\n\t\"\"\"\n\tThis is an example function.\n\tIt adds two numbers.\n\t\"\"\"\n\treturn emulate()\n```\n\n**Example emulated function call:**\n```python\nresult = example_function(3, {\"value\": 7})\n```\n\n**Expected JSON output:**\n{\"return\": 10}\n",
             "category": "executive",
             "version": "v1.2"
+        },
+        {
+            "key": "synthetic_data_generator",
+            "text": "{func_name}{signature}:\n    \"\"\"{docstring}\"\"\"\n\nIMPORTANT RULES:\n1. Input values should respect the type hints\n2. Output values MUST be diverse - avoid generating the same output repeatedly\n3. Each row must be in CSV format\n4. For text outputs, enclose them in double quotes\n5. NO MORE THAN 20% of outputs should be the same value\n6. Generate inputs across the entire possible range\n7. Ensure proper formatting for {return_type} output type",
+            "category": "executive",
+            "version": "v1.0"
+        },
+        {
+            "key": "synthetic_data_generator",
+            "text": "{func_name}{signature}:\n    \"\"\"{docstring}\"\"\"\n\nIMPORTANT RULES:\n1. Input values should respect the type hints\n2. Output values MUST be diverse - avoid generating the same output repeatedly\n3. Each row must be in CSV format\n4. For text outputs, enclose them in double quotes\n5. NO MORE THAN 20% of outputs should be the same value\n6. Generate inputs across the entire possible range\n7. Ensure proper formatting for {return_type} output type",
+            "category": "executive",
+            "version": ""
         }
     ]
 }
\ No newline at end of file
diff --git a/src/OpenHosta/utils/torch_nn_utils.py b/src/OpenHosta/utils/torch_nn_utils.py
new file mode 100644
index 0000000..0763130
--- /dev/null
+++ b/src/OpenHosta/utils/torch_nn_utils.py
@@ -0,0 +1,272 @@
+from typing import Union
+
+from torch import nn
+from torch import optim
+
+from ..exec.predict.model.neural_network_types import LayerType, LossFunction, OptimizerAlgorithm, Layer
+
+
+def pytorch_layer_to_custom(layer) -> Layer:
+    """
+    Maps a PyTorch layer instance to a custom Layer representation.
+
+    :param layer: PyTorch layer object.
+    :return: A custom Layer object representing the PyTorch layer.
+    """
+    if isinstance(layer, nn.Linear):
+        return Layer(
+            layer_type=LayerType.LINEAR,
+            in_features=layer.in_features,
+            out_features=layer.out_features,
+        )
+
+    elif isinstance(layer, nn.Conv2d):
+        return Layer(
+            layer_type=LayerType.CONV2D,
+            in_features=layer.in_channels,
+            out_features=layer.out_channels,
+            kernel_size=layer.kernel_size,
+            stride=layer.stride,
+            padding=layer.padding,
+        )
+
+    elif isinstance(layer, nn.Dropout):
+        return Layer(
+            layer_type=LayerType.DROPOUT,
+            dropout=layer.p  # Dropout rate
+        )
+
+    elif isinstance(layer, nn.ReLU):
+        return Layer(
+            layer_type=LayerType.RELU
+        )
+
+    elif isinstance(layer, nn.BatchNorm1d):
+        return Layer(
+            layer_type=LayerType.BATCHNORM1D,
+            out_features=layer.num_features
+        )
+
+    elif isinstance(layer, nn.BatchNorm2d):
+        return Layer(
+            layer_type=LayerType.BATCHNORM2D,
+            out_features=layer.num_features
+        )
+
+    elif isinstance(layer, nn.MaxPool2d):
+        return Layer(
+            layer_type=LayerType.MAXPOOL2D,
+            kernel_size=layer.kernel_size,
+            stride=layer.stride,
+            padding=layer.padding,
+        )
+
+    elif isinstance(layer, nn.AvgPool2d):
+        return Layer(
+            layer_type=LayerType.AVGPOOL2D,
+            kernel_size=layer.kernel_size,
+            stride=layer.stride,
+            padding=layer.padding,
+        )
+
+    elif isinstance(layer, nn.Sigmoid):
+        return Layer(
+            layer_type=LayerType.SIGMOID
+        )
+
+    elif isinstance(layer, nn.Softmax):
+        return Layer(
+            layer_type=LayerType.SOFTMAX
+        )
+
+    elif isinstance(layer, nn.Tanh):
+        return Layer(
+            layer_type=LayerType.TANH
+        )
+
+    else:
+        raise ValueError(f"Unsupported PyTorch layer type: {layer.__class__.__name__}")
+
+
+def custom_layer_to_pytorch(layer: Layer) -> Union[nn.Module, None]:
+    """
+    Maps a custom Layer instance to a PyTorch layer.
+
+    :param layer: The custom Layer instance.
+    :return: The PyTorch layer instance.
+    """
+    if layer.layer_type == LayerType.LINEAR:
+        return nn.Linear(layer.in_features, layer.out_features)
+
+    elif layer.layer_type == LayerType.CONV2D:
+        return nn.Conv2d(
+            in_channels=layer.in_features,
+            out_channels=layer.out_features,
+            kernel_size=layer.kernel_size,
+            stride=layer.stride,
+            padding=layer.padding
+        )
+
+    elif layer.layer_type == LayerType.DROPOUT:
+        return nn.Dropout(p=layer.dropout)
+
+    elif layer.layer_type == LayerType.RELU:
+        return nn.ReLU()
+
+    elif layer.layer_type == LayerType.BATCHNORM1D:
+        return nn.BatchNorm1d(num_features=layer.out_features)
+
+    elif layer.layer_type == LayerType.BATCHNORM2D:
+        return nn.BatchNorm2d(num_features=layer.out_features)
+
+    elif layer.layer_type == LayerType.MAXPOOL2D:
+        return nn.MaxPool2d(
+            kernel_size=layer.kernel_size,
+            stride=layer.stride,
+            padding=layer.padding
+        )
+
+    elif layer.layer_type == LayerType.AVGPOOL2D:
+        return nn.AvgPool2d(
+            kernel_size=layer.kernel_size,
+            stride=layer.stride,
+            padding=layer.padding
+        )
+
+    elif layer.layer_type == LayerType.SIGMOID:
+        return nn.Sigmoid()
+
+    elif layer.layer_type == LayerType.SOFTMAX:
+        return nn.Softmax()
+
+    elif layer.layer_type == LayerType.TANH:
+        return nn.Tanh()
+
+    else:
+        return None
+
+
+_LOSS_FUNC_MAP = {
+    nn.L1Loss: LossFunction.L1_LOSS,
+    nn.MSELoss: LossFunction.MSE_LOSS,
+    nn.CrossEntropyLoss: LossFunction.CROSS_ENTROPY_LOSS,
+    nn.CTCLoss: LossFunction.CTC_LOSS,
+    nn.NLLLoss: LossFunction.NLL_LOSS,
+    nn.PoissonNLLLoss: LossFunction.POISSON_NLL_LOSS,
+    nn.GaussianNLLLoss: LossFunction.GAUSSIAN_NLL_LOSS,
+    nn.KLDivLoss: LossFunction.KL_DIV_LOSS,
+    nn.BCELoss: LossFunction.BCE_LOSS,
+    nn.BCEWithLogitsLoss: LossFunction.BCE_WITH_LOGITS_LOSS,
+    nn.MarginRankingLoss: LossFunction.MARGIN_RANKING_LOSS,
+    nn.HingeEmbeddingLoss: LossFunction.HINGE_EMBEDDING_LOSS,
+    nn.HuberLoss: LossFunction.HUBER_LOSS,
+    nn.SmoothL1Loss: LossFunction.SMOOTH_L1_LOSS,
+    nn.CosineEmbeddingLoss: LossFunction.COSINE_EMBEDDING_LOSS,
+    nn.MultiLabelSoftMarginLoss: LossFunction.MULTI_LABEL_SOFT_MARGIN_LOSS,
+    nn.TripletMarginLoss: LossFunction.TRIPLET_MARGIN_LOSS,
+    nn.MultiMarginLoss: LossFunction.MULTI_MARGIN_LOSS,
+    nn.SoftMarginLoss: LossFunction.SOFT_MARGIN_LOSS,
+    nn.MultiLabelMarginLoss: LossFunction.MULTI_LABEL_MARGIN_LOSS,
+    nn.TripletMarginWithDistanceLoss: LossFunction.TRIPLET_MARGIN_WITH_DISTANCE_LOSS,
+}
+
+
+def pytorch_loss_to_custom(loss_instance) -> LossFunction:
+    """
+    Maps a PyTorch loss function instance to a custom LossFunction enum.
+
+    :param loss_instance: PyTorch loss function object.
+    :return: The custom LossFunction enum.
+    """
+
+    loss_type = type(loss_instance)
+    if loss_type in _LOSS_FUNC_MAP:
+        return _LOSS_FUNC_MAP[loss_type]
+    raise ValueError(f"Unsupported PyTorch loss function: {loss_type.__name__}")
+
+def custom_loss_to_pytorch(loss_function: LossFunction) -> Union[nn.Module, None]:
+    """
+    Maps a custom LossFunction enum to a PyTorch loss function.
+
+    :param loss_function: The custom LossFunction enum.
+    :return: The PyTorch loss function instance.
+    """
+
+    if loss_function in _LOSS_FUNC_MAP:
+        return _LOSS_FUNC_MAP[loss_function]
+    return None
+
+
+_OPTIMIZER_MAP = {
+    optim.Adadelta: OptimizerAlgorithm.ADADELTA,
+    optim.Adagrad: OptimizerAlgorithm.ADAGRAD,
+    optim.Adam: OptimizerAlgorithm.ADAM,
+    optim.AdamW: OptimizerAlgorithm.ADAMW,
+    optim.Adamax: OptimizerAlgorithm.ADAMAX,
+    optim.SparseAdam: OptimizerAlgorithm.SPARSEADAM,
+    optim.ASGD: OptimizerAlgorithm.ASGD,
+    optim.RMSprop: OptimizerAlgorithm.RMSPROP,
+    optim.Rprop: OptimizerAlgorithm.RPROP,
+    optim.SGD: OptimizerAlgorithm.SGD,
+    optim.LBFGS: OptimizerAlgorithm.LBFGS,
+    optim.NAdam: OptimizerAlgorithm.NADAM,
+    optim.RAdam: OptimizerAlgorithm.RADAM,
+}
+
+def pytorch_optimizer_to_custom(optimizer_instance) -> OptimizerAlgorithm:
+    """
+    Maps a PyTorch optimizer instance to a custom OptimizerAlgorithm enum.
+
+    :param optimizer_instance: PyTorch optimizer.
+    :return: The custom OptimizerAlgorithm enum.
+    """
+
+    optimizer_type = type(optimizer_instance)
+    if optimizer_type in _OPTIMIZER_MAP:
+        return _OPTIMIZER_MAP[optimizer_type]
+    raise ValueError(f"Unsupported PyTorch optimizer: {optimizer_type.__name__}")
+
+
+def custom_optimizer_to_pytorch(optimizer_algorithm: OptimizerAlgorithm, model: nn.Module, **kwargs) -> Union[optim.Optimizer, None]:
+    """
+    Maps a custom OptimizerAlgorithm enum to a PyTorch optimizer.
+
+    :param optimizer_algorithm: The custom OptimizerAlgorithm enum.
+    :param model: The PyTorch model.
+    :return: The PyTorch optimizer instance.
+    """
+
+    if optimizer_algorithm in _OPTIMIZER_MAP:
+        return _OPTIMIZER_MAP[optimizer_algorithm](model.parameters(), **kwargs)
+    return None
+
+def type_size(data, tokens_size=10):
+    """
+    Calculate the _inputs/_outputs size based on the type of the _inputs data.
+
+    Parameters:
+        data: Can be of type int, float, list, tuple, numpy array, PyTorch tensor, set, dict, or string.
+
+    Returns:
+        The size (number of elements) of the given data.
+    """
+    if data is str:
+        return tokens_size
+    elif data is int:
+        return 1
+    elif data is float:
+        return 1
+    elif data is bool:
+        return 1
+    elif data is list:
+        return len(data) * type_size(data[0]) if data else 0
+    elif data is tuple:
+        return sum(type_size(item) for item in data)
+    elif data is set:
+        return sum(type_size(item) for item in data)
+    elif data is dict:
+        return sum(type_size(k) + type_size(v) for k, v in data.items())
+    # elif isinstance(data, typing._GenericAlias) and get_origin(data) is Literal:
+    #     return len(data.__args__)
+    else:
+        raise TypeError(f'Unsupported data type: {type(data)}')


Test patch:
diff --git a/tests/functionnalTests/test_ask.py b/tests/functionnalTests/test_ask.py
new file mode 100644
index 0000000..bb377e8
--- /dev/null
+++ b/tests/functionnalTests/test_ask.py
@@ -0,0 +1,1 @@
+import pytest
\ No newline at end of file
diff --git a/tests/functionnalTests/test_bdy.py b/tests/functionnalTests/test_bdy.py
new file mode 100644
index 0000000..bb377e8
--- /dev/null
+++ b/tests/functionnalTests/test_bdy.py
@@ -0,0 +1,1 @@
+import pytest
\ No newline at end of file
diff --git a/tests/functionnalTests/test_emulate.py b/tests/functionnalTests/test_emulate.py
new file mode 100644
index 0000000..5871ed8
--- /dev/null
+++ b/tests/functionnalTests/test_emulate.py
@@ -0,0 +1,1 @@
+import pytest
diff --git a/tests/functionnalTests/test_mandatory.py b/tests/functionnalTests/test_mandatory.py
index 7573b9a..42b18e4 100644
--- a/tests/functionnalTests/test_mandatory.py
+++ b/tests/functionnalTests/test_mandatory.py
@@ -2,9 +2,9 @@
 import time as t
 from pydantic import BaseModel
 from typing import Callable
-import numpy as np
+# import numpy as np
 
-from OpenHosta import config, emulate, thought, example, load_training_example, predict, TrainingSet
+from OpenHosta import config, emulate, thinkof
 
 g_apiKey = ""
 
@@ -33,70 +33,70 @@ 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()
+# 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
+#         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_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()
+#     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 = 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
+#         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
+#         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)
+# 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()
+#         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}]
+#         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"
+#         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:
        
@@ -231,33 +231,6 @@ def generateAccount(name:str, email:str, age:int)->User:
     def test_FeaturePydanticLocal(self):
         pass
         
-    def test_FeatureSuggestDefaultModel(self):
-        
-        my_model = config.Model(
-            model="gpt-4o-mini",
-            base_url="https://api.openai.com/v1/chat/completions",
-            api_key=g_apiKey
-        )
-        
-        config.set_default_model(my_model)
-        
-        def multiply(a:int, b:int)->int:
-            """
-            This function multiplies two integers in parameter.
-            """
-            return emulate()
-        
-        ret = multiply(2, 3)
-        ret_suggest = multiply.__suggest__(multiply)
-        ret_model = multiply._last_response["model"]
-        
-        assert isinstance(ret_suggest, dict)
-        assert "gpt-4o-mini" in ret_model
-        assert not multiply.advanced is None
-        assert not multiply.enhanced_prompt is None
-        assert not multiply.review is None
-        assert not multiply.diagram is None
-        
     def test_rc2(self):
         
         def multiply(a:int, b:int)->int:
@@ -272,14 +245,14 @@ def main():
         res = main()
         assert res == 6
 
-class TestThought:
+class Testthinkof:
     
     def test_BasicDirect(self):
-        ret = thought("Multiply by 2")(8)
+        ret = thinkof("Multiply by 2")(8)
         assert ret == 16
     
     def test_BasicIndirect(self):
-        x = thought("Translate in English")
+        x = thinkof("Translate in English")
         ret = x("Bonjour Monde!")
         assert isinstance(x, Callable)
         assert ret == "Hello World!"
@@ -292,7 +265,7 @@ def test_BasicIndirect(self):
         ("return a random bool in python", "", bool),
     ])
     def test_BasicTyped(self, prompt, args, expected):
-        ret = thought(prompt)(args)
+        ret = thinkof(prompt)(args)
         assert isinstance(ret, expected)
         
     @pytest.mark.parametrize("prompt, args, expected", [
@@ -303,16 +276,16 @@ def test_BasicTyped(self, prompt, args, expected):
         ("Is a positive number", 6, bool),
     ])
     def test_FeaturePredict(self, prompt, args, expected):
-        x = thought(prompt)
+        x = thinkof(prompt)
         ret = x(args)
         assert x._return_type is expected
 
     def test_FeatureMultiArgs(self):
-        x = thought("Combine each word of a sentence in a string")
+        x = thinkof("Combine each word of a sentence in a string")
         ret = x("Hello", ", how are you ?", " Nice to meet you !")
         assert ret == "Hello, how are you ? Nice to meet you !"
     
-    def test_FeatureChainOfThought(self):
+    def test_FeatureChainOfthinkof(self):
         pass
     
     def test_FeatureDefaultModel(self):
@@ -324,7 +297,7 @@ def test_FeatureDefaultModel(self):
         
         config.set_default_model(my_model)
         
-        x = thought("Is a masculine name")
+        x = thinkof("Is a masculine name")
         ret = x("Max")
         print(x._last_response)
         ret_model = x._last_response["model"]
diff --git a/tests/functionnalTests/test_predict.py b/tests/functionnalTests/test_predict.py
new file mode 100644
index 0000000..bb377e8
--- /dev/null
+++ b/tests/functionnalTests/test_predict.py
@@ -0,0 +1,1 @@
+import pytest
\ No newline at end of file
diff --git a/tests/functionnalTests/test_thinkof.py b/tests/functionnalTests/test_thinkof.py
new file mode 100644
index 0000000..bb377e8
--- /dev/null
+++ b/tests/functionnalTests/test_thinkof.py
@@ -0,0 +1,1 @@
+import pytest
\ No newline at end of file
diff --git a/tests/reports/.gitkeep b/tests/reports/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/runner.py b/tests/runner.py
new file mode 100644
index 0000000..61c6814
--- /dev/null
+++ b/tests/runner.py
@@ -0,0 +1,156 @@
+import subprocess as sp
+import sys
+import time
+import threading
+
+if len(sys.argv) != 2:
+    sys.exit("Error: Missing required arguments\nUsage: runner.py [SPINNER_TYPE]")
+
+SPINNERS = {
+    'classic': ['/', '-', '\\', '|'],
+    'dots': ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
+    'arrows': ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'],
+    'balloons': ['．', 'o', 'O', '@', '*'],
+    'bouncing': ['⠁', '⠂', '⠄', '⠂'],
+    'clock': ['🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚', '🕛'],
+    'simple': ['.  ', '.. ', '...', ' ..', '  .', '   '],
+    'box': ['▖', '▘', '▝', '▗'],
+    'star': ['✶', '✸', '✹', '✺', '✹', '✷'],
+}
+
+def spinner(stop_event, message):
+    chars = SPINNERS[sys.argv[1]]
+    i = 0
+    while not stop_event.is_set():
+        sys.stdout.write(f'\r{message} {chars[i % len(chars)]}')
+        sys.stdout.flush()
+        time.sleep(0.2)
+        i += 1
+    sys.stdout.write('\r' + ' ' * (len(message) + 2) + '\r')
+    sys.stdout.flush()
+
+def run_with_spinner(command, message, **kwargs):
+    stop_spinner = threading.Event()
+    spinner_thread = threading.Thread(target=spinner, args=(stop_spinner, message))
+    spinner_thread.start()
+    
+    result = sp.run(command, **kwargs)
+    
+    stop_spinner.set()
+    spinner_thread.join()
+    print(f'\r{message} ✓')
+    return result
+
+run_with_spinner(
+    "make clean", 
+    "Cleaning repository", 
+    stdout=sp.DEVNULL
+)
+run_with_spinner(
+    "py -3.12 -m pip install .[dev,pydantic]", 
+    "Installing dependencies", 
+    stdout=sp.DEVNULL
+)
+
+run_with_spinner(
+    "py -3.12 -m autopep8 --recursive --in-place src/OpenHosta",
+    "Running autopep8",
+    check=False,
+    shell=True,
+)
+
+with open("tests/reports/pyflakes_report.txt", 'w', encoding='utf-8') as file:
+    run_with_spinner(
+        "py -3.12 -m pyflakes src/OpenHosta/",
+        "Running pyflakes",
+        check=False,
+        shell=True,
+        stdout=file,
+        stderr=file
+    )
+
+with open("tests/reports/isort_report.txt", 'w', encoding='utf-8') as file:
+    run_with_spinner(
+        "py -3.12 -m isort --check-only src/OpenHosta",
+        "Running isort",
+        check=False,
+        shell=True,
+        stdout=file,
+        stderr=file
+    )
+
+with open("tests/reports/mypy_report.txt", 'w', encoding='utf-8') as file:
+    run_with_spinner(
+        "py -3.12 -m mypy src/OpenHosta",
+        "Running mypy",
+        check=False,
+        shell=True,
+        stdout=file,
+        stderr=file
+    )
+
+with open("tests/reports/pylint_report.txt", 'w', encoding='utf-8') as file:
+    run_with_spinner(
+        "py -3.12 -m pylint --rcfile=pyproject.toml src/OpenHosta",
+        "Running pylint",
+        check=False,
+        shell=True,
+        stdout=file,
+        stderr=file
+    )
+    
+with open("tests/reports/bandit_report.txt", 'w', encoding='utf-8') as file:
+    run_with_spinner(
+        "py -3.12 -m bandit -c pyproject.toml -r src/OpenHosta",
+        "Running bandit",
+        check=False,
+        shell=True,
+        stdout=file,
+        stderr=file
+    )
+    
+with open("tests/reports/unittest_report.txt", 'w', encoding='utf-8') as file:
+    run_with_spinner(
+        "py -3.12 -m pytest tests/unitTests -v --cov=OpenHosta.core",
+        "Running unit-tests",
+        check=False,
+        shell=True,
+        stdout=file,
+        stderr=file
+    )
+    
+with open("tests/reports/functest_report.txt", 'w', encoding='utf-8') as file:
+    run_with_spinner(
+        "py -3.12 -m pytest tests/functionnalTests -v --cov=OpenHosta",
+        "Running functionnal-tests",
+        check=False,
+        shell=True,
+        stdout=file,
+        stderr=file
+    )
+    
+run_with_spinner("py -3.12 -m pip uninstall -y OpenHosta", "Uninstalling dependencies", stdout=sp.DEVNULL)
+
+
+# filename = ""
+# color = lambda x: 'red' if str(x) == " error" else ('blue' if str(x) == " note" else 'white')
+
+# with open(file="mypy/stdout.txt", mode='r', encoding='utf-8') as src:
+#     with open(file="mypy/parsed.md", mode='w', encoding='utf-8') as tar:
+#         for line in src:
+#             s_line = line.split(':', 3)
+#             try: 
+#                 len(s_line) 
+#                 s_line[3]
+#             except: continue
+    
+#             if s_line[0] == "C": continue
+            
+#             if s_line[0] != filename:
+#                 filename = s_line[0]
+#                 tar.write(f"\n### {filename.split('/')[-1].replace('_', '-')}\n\n")
+#             tar.write(f"- <font color=\'{color(s_line[2])}\'>[line: {s_line[1]}]</font>")
+#             tar.write(f": {s_line[3]}\n")
+    
+
+    
\ No newline at end of file
diff --git a/tests/unitTests/test_analizer.py b/tests/unitTests/test_analizer.py
new file mode 100644
index 0000000..4e507c4
--- /dev/null
+++ b/tests/unitTests/test_analizer.py
@@ -0,0 +1,162 @@
+import pytest
+import inspect
+import re
+from typing import Optional, List, Union
+from pydantic import BaseModel, Field
+from PIL import Image
+
+from OpenHosta.core.analizer import FuncAnalizer as FA
+
+
+class TestInspector:
+    
+    def test_InitSimple(self):
+        def tmp():
+            return inspect.currentframe()
+        x = FA(tmp, tmp())
+        assert type(x) == FA
+        
+    def test_InitInvalidArgs(self):
+        with pytest.raises(AttributeError, match=re.escape("[_FuncInspector.__init__] Invalid arguments:\n'hi' is not a callable object")):
+            FA("hi", 3)
+            
+    def test_FuncDef(self):
+        def testfunc():
+            return inspect.currentframe()
+    
+        frame = testfunc()
+        x = FA(testfunc, frame)
+        my = "```python\n        def testfunc():\n            return inspect.currentframe()\n\n```"
+        assert x.func_def == my
+        
+    def test_FuncLocalsNoVal(self):
+        def testfunc():
+            return inspect.currentframe()
+    
+        frame = testfunc()
+        x = FA(testfunc, frame)
+        assert x.func_locals == (None, None)
+        
+    def test_FuncLocalsSimple(self):
+        def testfunc():
+            a = 2
+            b = "hi"
+            return inspect.currentframe()
+    
+        frame = testfunc()
+        x = FA(testfunc, frame)
+        assert x.func_locals == ({"a": 2, "b": "hi"}, None)
+
+    def test_FuncLocalsSelf(self):
+        class TMP:
+            def __init__(self):
+                self.c = 3.14
+            
+            def testfunc(self):
+                a = 2
+                b = "hi"
+                return inspect.currentframe()
+    
+        tmp = TMP()
+        frame = tmp.testfunc()
+        x = FA(tmp.testfunc, frame)
+        assert x.func_locals == ({"a": 2, "b": "hi"}, {"c": 3.14})
+        
+    def test_FuncCallSimple(self):
+        def testfunc(a:int, b:str):
+            return inspect.currentframe()
+    
+        frame = testfunc(2, "hi")
+        x = FA(testfunc, frame)
+        assert x.func_call == ("testfunc(a=2, b='hi')", {'a': 2, 'b': 'hi'})
+
+    def test_FuncCallNoArgs(self):
+        def testfunc():
+            return inspect.currentframe()
+    
+        frame = testfunc()
+        x = FA(testfunc, frame)
+        assert x.func_call == ("testfunc()", {})
+        
+    def test_FuncCallComplexArgs(self):
+        class TMP(BaseModel):
+            var:str = Field(default="", description="It's a var :)")
+        def testfunc(a:TMP, b:Image):
+            return inspect.currentframe()
+    
+        ins = TMP()
+        img = Image.new('RGB', (10, 10), 'white')
+        frame = testfunc(ins, img)
+        x = FA(testfunc, frame)
+        assert x.func_call == (f'testfunc(a={ins!r}, b={img!r})', {"a": ins, "b": img})
+    
+    def test_FuncTypeSimple(self):
+        def testfunc(a:int, b:str)->list:
+            return inspect.currentframe()
+    
+        frame = testfunc(2, "hi")
+        x = FA(testfunc, frame)
+        assert x.func_type == ([int, str], list)
+        
+    def test_FuncTypeArgsWithNoType(self):
+        def testfunc(a, b):
+            return inspect.currentframe()
+    
+        frame = testfunc(2, "hi")
+        x = FA(testfunc, frame)
+        assert x.func_type == ([inspect._empty, inspect._empty], None)
+        
+    def test_FuncTypeNoArgs(self):
+        def testfunc():
+            return inspect.currentframe()
+    
+        frame = testfunc()
+        x = FA(testfunc, frame)
+        assert x.func_type == ([], None)
+        
+    def test_FuncTypeComplexArgs(self):
+        class TMP(BaseModel):
+            var:str = Field(default="", description="It's a var :)")
+        def testfunc(a:TMP, b:Image)->Optional[List[int]]:
+            return inspect.currentframe()
+    
+        ins = TMP()
+        img = Image.new('RGB', (10, 10), 'white')
+        frame = testfunc(ins, img)
+        x = FA(testfunc, frame)
+        assert x.func_type == ([type(ins), Image], Optional[List[int]])
+        
+    def test_FuncSchemaBuiltIn(self):
+        def testfunc(a:int, b:int)->list:
+            return inspect.currentframe()
+        
+        frame = testfunc(2, "hi")
+        x = FA(testfunc, frame)
+        assert x.func_schema == {'properties': {'annotation': {'items': {}, 'title': 'Annotation', 'type': 'array'}}, 'required': ['annotation'], 'title': 'return_schema', 'type': 'object'}
+    
+    def test_FuncSchemaAnnotated(self):
+        def testfunc(a:Union[int, str])->Optional[List[float]]:
+            return inspect.currentframe()
+        
+        frame = testfunc(2)
+        x = FA(testfunc, frame)
+        assert x.func_schema == {'properties': {'annotation': {'anyOf': [{'items': {'type': 'number'}, 'type': 'array'}, {'type': 'null'}], 'title': 'Annotation'}}, 'required': ['annotation'], 'title': 'return_schema', 'type': 'object'}
+    
+    def test_FuncSchemaPydantic(self):
+        class TMP(BaseModel):
+            var1:str = Field(default="", description="It's one var :)")
+            var2:int = Field(default=0, description="It's two var :)")
+        def testfunc(a:TMP)->TMP:
+            return inspect.currentframe()
+        
+        frame = testfunc(2)
+        x = FA(testfunc, frame)
+        assert x.func_schema == {'properties': {'var1': {'default': '', 'description': "It's one var :)", 'title': 'Var1', 'type': 'string'}, 'var2': {'default': 0, 'description': "It's two var :)", 'title': 'Var2', 'type': 'integer'}}, 'title': 'TMP', 'type': 'object'}
+    
+    def test_FuncSchemaNoType(self):
+        def testfunc():
+            return inspect.currentframe()
+        
+        frame = testfunc()
+        x = FA(testfunc, frame)
+        assert x.func_schema == {'properties': {'annotation': {'title': 'Annotation'}}, 'required': ['annotation'], 'title': 'return_shema', 'type': 'object'}
diff --git a/tests/unitTests/test_checker.py b/tests/unitTests/test_checker.py
new file mode 100644
index 0000000..bb377e8
--- /dev/null
+++ b/tests/unitTests/test_checker.py
@@ -0,0 +1,1 @@
+import pytest
\ No newline at end of file
diff --git a/tests/unitTests/test_config.py b/tests/unitTests/test_config.py
new file mode 100644
index 0000000..bb377e8
--- /dev/null
+++ b/tests/unitTests/test_config.py
@@ -0,0 +1,1 @@
+import pytest
\ No newline at end of file
diff --git a/tests/unitTests/test_exec.py b/tests/unitTests/test_exec.py
deleted file mode 100644
index 1f310a8..0000000
--- a/tests/unitTests/test_exec.py
+++ /dev/null
@@ -1,69 +0,0 @@
-import sys
-import pytest
-from unittest.mock import patch
-import inspect
-
-from OpenHosta import exec
-
-class TestExtendScope:
-    
-    def test_basic(self):
-        def printHello():
-            print("Hello")
-
-        x =  exec.HostaInjector(printHello)
-
-        class Test:
-            def test(self):
-                return x._extend_scope()
-
-        def caller():
-            y = Test()
-            return y.test()
-
-        res = caller()
-        assert res[0] == caller
-        
-    def test_Flask(self):
-        from flask import Flask, request, session
-        
-        def printHello():
-            print("Hello")
-
-        x =  exec.HostaInjector(printHello)
-
-        class Test:
-            def test(self):
-                return x._extend_scope()
-
-        def caller():
-            y = Test()
-            return y.test()
-
-        res = caller()
-        del sys.modules["flask"]
-        assert res[0] == caller
-        
-    def test_ErrorHandling(self):
-        def printHello():
-            print("Hello")
-
-        x =  exec.HostaInjector(printHello)
-
-        class Test:
-            def test(self):
-                return x._extend_scope()
-
-        def caller():
-            y = Test()
-            return y.test()
-
-        current = inspect.currentframe()
-        print(f"INTERNAL: {current.f_locals}")
-        current.f_locals["caller"] = "hello"
-        current.f_locals.pop("caller")
-        print(f"INTERNAL: {current.f_locals}")
-
-        with patch("exec.hasattr", return_value=False):
-            res = caller()
-        assert res == caller
\ No newline at end of file
diff --git a/tests/unitTests/test_hosta.py b/tests/unitTests/test_hosta.py
new file mode 100644
index 0000000..b53647a
--- /dev/null
+++ b/tests/unitTests/test_hosta.py
@@ -0,0 +1,3 @@
+import pytest
+
+        
\ No newline at end of file
diff --git a/tests/unitTests/test_inspector.py b/tests/unitTests/test_inspector.py
new file mode 100644
index 0000000..e0943b6
--- /dev/null
+++ b/tests/unitTests/test_inspector.py
@@ -0,0 +1,108 @@
+import pytest
+import inspect
+import re
+import sys
+from unittest.mock import patch
+
+from OpenHosta.core.inspector import HostaInspector as HI
+from OpenHosta.utils.errors import FrameError
+
+class TestExtend:
+    
+    def test_BasicMethod(self):
+        class TMP:
+            def tmp_extend(self):
+                return HI._extend()
+        def tmp():
+            x = TMP()
+            return x.tmp_extend()
+        def tmp2():
+            return (tmp(), inspect.currentframe())
+        
+        res = tmp2()
+        assert res[0] == (tmp2, res[1])
+        
+    def test_BasicFunc(self):
+        pass
+        
+    def test_LowBackLevel(self):    
+        def tmp():
+            return (HI._extend(back_level=1), inspect.currentframe())
+        
+        res = tmp()
+        
+        assert res[0] == (tmp, res[1])
+        
+    def test_InvalidArgs(self):
+        with pytest.raises(ValueError, match=re.escape("[HostaInspector._extend] back_level must a non-zero positive integers.")):
+            HI._extend(back_level=0)
+            HI._extend(back_level=-1)
+            HI._extend(back_level="Hi mom")
+    
+    def test_FuncWithFlask(self):
+        from flask import Flask, request, session
+
+        def tmp():
+            return (HI._extend(back_level=1), inspect.currentframe())
+        
+        res = tmp()
+        del sys.modules["flask"]
+        assert res[0] == (tmp, res[1])
+
+    def test_FuncWithGlobals(self):
+        pass # TODO
+    
+    def test_FBackNotFound(self):
+        with pytest.raises(FrameError, match=re.escape("[HostaInspector._extend] Frame can't be found (level: 33)")):
+            HI._extend(back_level=100)
+    
+    def test_FuncNotCallable(self):
+        pass # TODO
+    
+class TestAttach:
+    
+    def test_OneAttr(self):
+        def tmp():
+            return
+        HI._attach(tmp, {"nb": 3})
+        assert tmp.nb == 3
+            
+    def test_ManyAttr(self):
+        def tmp():
+            return
+        HI._attach(tmp, {
+            "a": 1,
+            "b": "hi",
+            "c": tmp
+        })
+        assert tmp.a == 1
+        assert tmp.b == "hi"
+        assert tmp.c == tmp
+    
+    def test_InvalidArgs(self):
+        def tmp():
+            return
+        with pytest.raises(ValueError, match=re.escape("[HostaInspector._attach] Invalid arguments")):
+            HI._attach(tmp, "6")
+            HI._attach("a", 1)
+    
+    def test_BasicMethod(self):
+        class TMP:
+            def tmp(self):
+                return
+        x = TMP()
+        HI._attach(x.tmp, {"a": 1})
+        assert x.tmp.a == 1
+    
+    def test_InvalidMethod(self):
+        def tmp():
+            return
+        with patch('inspect.ismethod', return_value=True):
+            with pytest.raises(AttributeError, match=re.escape("[HostaInspector._attach] Failed to attach attributs. \"__func__\" attribut is missing.")):
+                HI._attach(tmp, {"a": 1})
+    
+    def test_InvalidCallable(self):
+        class TMP:
+            pass
+        with pytest.raises(AttributeError, match=re.escape(f"[HostaInspector._attach] Failed to attach attributs. Object's type not supported: {type(TMP)}.")):
+            HI._attach(TMP, {"a": 1})