diff --git a/AgentReact/utils/VectorDatabase.py b/AgentReact/utils/VectorDatabase.py new file mode 100644 index 0000000..c1d9f0e --- /dev/null +++ b/AgentReact/utils/VectorDatabase.py @@ -0,0 +1,45 @@ +from langchain_huggingface import HuggingFaceEmbeddings +from langchain_chroma import Chroma # TODO plus tard, ramplacer par PG Vector +import sys +from pathlib import Path + +# Permet de garder ChromaDB en mémoire. +# Cette classe est un Singleton, il n'y en aura qu'une seule et unique instance à tout moment +# https://refactoring.guru/design-patterns/singleton +class VectorDatabase: + instance = None + + def __new__(cls): # Selon https://www.geeksforgeeks.org/python/singleton-pattern-in-python-a-complete-guide/ + if cls.instance is None: + cls.instance = super().__new__(cls) + # J'initialise les attributs à None ici, permet de tester si la classe a déjà été init une première fois ou non + cls.instance.__embeddings = None + cls.instance.__chroma = None + return cls.instance + + def __init__(self): + if self.__embeddings is not None: return + + base_dir:str = Path(sys.argv[0]).resolve().parent.as_posix() # Récupérer le chemin vers le point d'entrée du programme + bdd_path:str = base_dir + "/chroma_db/" + + self.__embeddings = HuggingFaceEmbeddings(model_name="jinaai/jina-embeddings-v3", model_kwargs={"trust_remote_code": True}) + self.__chroma = Chroma( + persist_directory=bdd_path, + embedding_function=self.__embeddings + ) + + def getChroma(self)->Chroma: + return self.__chroma + + def getEmbeddings(self)->'Embeddings Hugging Face': + return self.__embeddings + +if __name__ == "__main__": + + test1 = VectorDatabase() + print('TEST 1 INIT') + test2 = VectorDatabase() + + print(test1 is test2) + assert test1 is test2 \ No newline at end of file diff --git a/AgentReact/utils/tools.py b/AgentReact/utils/tools.py index 503531f..8dfb8e0 100644 --- a/AgentReact/utils/tools.py +++ b/AgentReact/utils/tools.py @@ -1,6 +1,12 @@ from langchain.tools import tool +from langgraph.prebuilt import InjectedState from tavily import TavilyClient -from typing import List +from pathlib import Path +from typing import List, Dict +import sys +from .StateElements.TodoElement import TodoElement + +from .VectorDatabase import VectorDatabase @tool def internet_search(query: str)->dict: @@ -16,22 +22,169 @@ def internet_search(query: str)->dict: @tool -def write_file(content: str) -> str: +def write_file(file_path:str, content: str, append:bool=True) -> str: """ - Écrire les données dans un fichier + Ecrire et ajouter du texte dans un fichier. Args: - content (str): Contenu du fichier à écrire + file_path (str): Chemin d'accès relatif vers le fichier à écrire. + content (str): Contenu à écrire dans le fichier. + append (bool, optional): Faut-il AJOUTER(True) au fichier, ou REMPLACER son contenu(False) ? True par défaut. Returns: - str: Résultat de l'écriture + str: Le chemin d'accès relatif vers le fichier en cas de réussite, ou une erreur en cas d'echec """ - print("==ECRITURE FICHIER==") - print(content) - return "Fichier écrit" + try: + base_dir:str = Path(sys.argv[0]).resolve().parent.as_posix() # Récupérer le chemin vers le point d'entrée du programme + file_path:str = base_dir + (file_path if file_path.startswith('/') else f'/{file_path}') # Puis générer le chemin vers le fichier + + mode = "a" if append else "w" # Mode d'écriture + with open(full_path, mode, encoding="utf-8") as f: # Puis j'écris + f.write(content) + + return str(full_path) + + except Exception as e: + return f"Erreur lors de l'écriture: {str(e)}" + +@tool +def editTodo(index:int, state:int, state: Annotated[dict, InjectedState])->bool: # https://stackoverflow.com/a/79525434 + """ + Modifier l'état d'une tâche (TODO) + + Args: + index (int): Index de la tâche à modifier, en commançant à 0 pour la première tâche. + state (int): Nouvel état. 0 pour "non commencé, 1 pour "en cours", 2 pour "complété" + + Returns: + bool: Réussite de l'opération, ou non. + """ + if len(state["todo"]) <= index: + # Erreur, l'index est trop grand + return False + + state["todo"][index].state = state # Modification de l'état de cette tâche + + # Toutes les tâches complétées ? + found = False + for task in state["todo"]: # Pour chaque tâche + if task.state != 2: # Si elle n'est pas terminée + found = True + break + + if not found: state["todo"] = [] # Toutes les tâches terminées, je peux clear la TODO list du state + return True + +@tool +def addTodo(name:str, description:str, state: Annotated[dict, InjectedState])->bool: + """ + Ajouter une nouvelle tâche/TODO + + Args: + name (str): Nom de cette tâche + description (str): Une ou deux phrases pour décrire le travail à effectuer dans ce TODO + + Returns: + bool: Réussite de l'opération, ou non + """ + if state["todo"] is None: state["todo"] = [] + + state["todo"].append(TodoElement(name, description)) + +@tool +def removeTodo(index:int, state: Annotated[dict, InjectedState])->bool: + """ + Retirer une tâche/TODO de la liste des tâches + + Args: + index (int): Position de la tâche dans la liste, commence à 0 pour le premier TODO + + Returns: + bool: Réussite de l'opération, ou non + """ + if len(state["todo"]) <= index: + # Erreur, l'index est trop grand + return False + + state['todo'].pop(index) + return True + +@tool +def read_file(file_path: str) -> str: + """ + Lire le contenu d'un fichier texte. + + Args: + file_path (str): Chemin d'accès relatif vers le fichier à lire. + + Returns: + str: Le contenu du fichier, ou un message d'erreur. + """ + try: + base_dir:str = Path(sys.argv[0]).resolve().parent.as_posix() # Récupérer le chemin vers le point d'entrée du programme + file_path:str = base_dir + (file_path if file_path.startswith('/') else f'/{file_path}') # Puis générer le chemin vers le fichier + + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + + return content + + except Exception as e: + return f"Erreur lors de la lecture : {str(e)}" + +@tool +def ask_human(request:str)->str: + """ + Demander quelque chose à un assistant humain. Permet d'obtenir des informations supplémentaires, + ou qu'une action soit réalisée. + + Args: + request (str): Ce qui est demandé à l'humain + + Returns: + str: Réponse de l'humain + """ + print("--- L'IA A BESOIN D'UN HUMAIN ! ---") + print(f"L'IA demande : {request}") + + user_response = input("Réponse humaine: ") # Input bloque le système en attendant l'humain + # J'aurais possiblement utiliser d'autres approches comme https://docs.langchain.com/oss/javascript/langchain/human-in-the-loop + # Mais Human in the loop se place AVANT l'outil. Ici, l'outil consiste justement à demander quelque chose à un humain. + + print("-------") + + return user_response + +@tool +def search_in_files(query:str, state: Annotated[dict, InjectedState])->List[str]: + """ + Rechercher quelque chose dans les documents enregistrés localement. + Dans le cas actuel, ces documents sont des rapports hebdomadaires de stage. + + Args: + query (str): La requête recherchée. + + Returns: + List[str]: Échantillons de documents correspondants. + """ + bdd = VectorDatabase() # Récupère l'unique instance de cette BDD, c'est un SIngleton + + retrieved_docs = bdd.getChroma().similarity_search(prompt, k=5) # 5 documents + + # Conversion des documents en texte + docs_content = "\n".join( + [f"Document {i+1}:\n{doc.page_content}" for i,doc in enumerate(retrieved_docs)] + ) + + # Sauvegarde des données dans le State + state["ragQuery"] = query + state["ragDocuments"] = docs_content + + return docs_content # Retourne la liste de documents trouvés + def getTools()->List['Tools']: """ Récupérer la liste des tools """ - return [internet_search, write_file] \ No newline at end of file + return [internet_search, write_file, editTodo, read_file, ask_human, search_in_files, addTodo, removeTodo] \ No newline at end of file diff --git a/roadmap.md b/roadmap.md index 2215d44..a9eaf74 100644 --- a/roadmap.md +++ b/roadmap.md @@ -12,13 +12,15 @@ ## Mise en place de l'agent - [X] Préparation du `State` -- [ ] Développement des outils de l'agent +- [X] Développement des outils de l'agent - [ ] Préparation des nœuds - [ ] Branchement des nœuds entre-eux ## Amélioration de l'agent +- [ ] Cross-encoding sur la sortie du **RAG** - [ ] Sauvegarde de l'état de l'agent - [ ] Système de redémarrage après un arrêt +- [ ] Gestion de la taille du contexte - Résumé de l'historique des messages - [ ] Détection de *prompt injection* - [ ] Génération d'un PDF en sortie du système