diff --git a/.gitignore b/.gitignore index e6f64b5..870b2e1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ mlflow.db # Par sécurité documents_projet/ chroma_db/ +AgentReact/rapports_resumes/ +AgentReact/outils_resumes/ # Python __pycache__/ \ No newline at end of file diff --git a/AgentReact/agent.py b/AgentReact/agent.py index a21475d..c6479e4 100644 --- a/AgentReact/agent.py +++ b/AgentReact/agent.py @@ -1,7 +1,8 @@ from langgraph.graph import START, END from langgraph.graph.state import CompiledStateGraph +from langgraph.checkpoint.memory import InMemorySaver -from utils.nodes import call_to_LLM, should_continue, task_ended, BasicToolNode, tool_node +from utils.nodes import * from utils.state import getState from utils.tools import getTools @@ -15,19 +16,41 @@ def getGraph()->CompiledStateGraph: workflow = getState() # State prêt à utiliser # Définition des sommets du graphe - workflow.add_node(call_to_LLM) + workflow.add_node(user_prompt) + workflow.add_node(LLM_central) + workflow.add_node(preparation_docs) + workflow.add_node(inject_preparation_prompt) workflow.add_node("tool_node", tool_node)# BasicToolNode(tools=getTools())) # N'est pas une fonction, mais une classe instanciée, je dois précisier le nom du node + workflow.add_node("weekly_report_tools", weekly_report_tools) + workflow.add_node(context_shortener) # Réduit la taille du contexte + workflow.add_node("context_shortener_2", context_shortener) # Le même, sous un autre nom pour le différencier dans le graphe # Arrêtes - workflow.set_entry_point("call_to_LLM") - workflow.add_edge("tool_node", "call_to_LLM") - workflow.add_conditional_edges("call_to_LLM", should_continue, { + workflow.set_conditional_entry_point(is_resumes_reports_already_initialised, { + "résumés non disponibles": "inject_preparation_prompt", # Résumés non générés + "résumés déjà générés": "user_prompt" # Résumés déjà prêts, je peux aller direct à la partie principale + }) + workflow.add_edge("inject_preparation_prompt", "preparation_docs") + workflow.add_conditional_edges("preparation_docs", should_continue, { + "tools":"weekly_report_tools", + "no_tools":"context_shortener" # FIN de la préparation, on réduit le contexte avant de passer à la suite + }) + workflow.add_edge("context_shortener", "user_prompt") # Et ici, je rejoins la partie principale qui rédigera le rapport + workflow.add_edge("user_prompt", "LLM_central") + + workflow.add_edge("weekly_report_tools", "preparation_docs") + workflow.add_conditional_edges("tool_node", should_shorten, { + 'sous la limite': "LLM_central", + 'réduire contexte': "context_shortener_2" + }) + workflow.add_edge("context_shortener_2", "LLM_central") + workflow.add_conditional_edges("LLM_central", should_continue, { "tools":"tool_node", "no_tools":END }) - return workflow.compile() + return workflow.compile(checkpointer=InMemorySaver()) # TODO: Rempalcer par une vrai BDD de prod if __name__ == "__main__": # Affichage du graphe - getGraph().get_graph().draw_mermaid_png(output_file_path="agent.png") \ No newline at end of file + getGraph().get_graph().draw_mermaid_png(output_file_path="imgs/agent.png") \ No newline at end of file diff --git a/AgentReact/start.py b/AgentReact/start.py index 96e66a4..aba9b12 100644 --- a/AgentReact/start.py +++ b/AgentReact/start.py @@ -5,11 +5,18 @@ from langchain.messages import HumanMessage, SystemMessage, AIMessage, ToolMessa import mlflow from agent import getGraph +from utils.InterruptPayload import InterruptPayload +from utils.StreamGraph import streamGraph # MLFLOW mlflow.set_experiment("TEST PROJET") # VOIR AVEC LA COMMANDE "MLFLOW SERVER" mlflow.langchain.autolog() -out_state = getGraph().invoke({'messages':[HumanMessage("Observe la base de documents, et génère un rapport de stage à partir de celle-ci. Ecris le dans un fichier markdown.")]}) -for message in out_state['messages']: - message.pretty_print() \ No newline at end of file +initial_input = { + 'messages':[SystemMessage("Salut")] + } + +config={"configurable": {"thread_id": 'yes'}} + +# Et je lance ! +streamGraph(initial_input, config, getGraph()) \ No newline at end of file diff --git a/AgentReact/utils/InterruptPayload.py b/AgentReact/utils/InterruptPayload.py new file mode 100644 index 0000000..609acbd --- /dev/null +++ b/AgentReact/utils/InterruptPayload.py @@ -0,0 +1,156 @@ +from typing import Dict, List +import json + +class InterruptPayload(): + """ + Classe qui va s'occuper de représenter les données demandées lors d'une interruption du programme + """ + + ACCEPTED = 1 # Status d'une requête + #EDITED = 2 + DENIED = 3 + + TOOL_CALL = 999 + USER_PROMPT = 998 + + def __init__(self, fields:Dict, state:int=0, payload_type:int=TOOL_CALL): + """ + Créer unne nouvelle instance de payload pour interrupt() + + Args: + fields (Dict): Un dictionnaire d'arguments pour un call d'outil, ou {'prompt':str} pour une requête de prompt + state (int, optional): État de la requête. Defaults to 0. Définit en variables statiques de l'objet. + payload_type (int, optional): Type d'interuption, appel d'outil ou requête humaine. Defaults to TOOL_CALL. Définit en variables statiques de l'objet. + """ + self.__fields = fields + self.__state = state + self.__type = payload_type + + def get(self, key:str)->str: + """ + Récupérer une valeur passée dans la payload + + Args: + key (str): Clé de la valeur + + Returns: + str: Valeur, en String. Il faudra la reconvertir en int si besoin + """ + return self.__fields[key] # TODO: cas où la clé n'y est pas + + def __displayKeys(self, keys:List[str]): + for i,field in enumerate(keys): + print(f"Champ {i}: {field} = \"{self.__fields[field]}\"\n") + + print("\n\n Que fait-on ?\n") + print("1 - ACCEPTER") + print("2 - MODIFIER") + print("3 - REFUSER") + + def humanDisplay(self): + """ + Afficher la requête proprement, permettant à l'utilisateur d'accepter, refuser ou modifier une requête + """ + if self.__type == InterruptPayload.USER_PROMPT: # C'est une demande de prompt humain + self.__human_prompt_display() + else: # C'est un appel d'outil + self.__tool_query_display() + + def __human_prompt_display(self): + print("=== L'AGENT DEMANDE DES CONSIGNES! ===\n") + + print("Veuillez saisir un prompt pour l'agent...\n") + prompt = input("Prompt...") + + self.__fields = {'prompt': prompt} + print("\nMerci, l'exécution va reprendre.\n") + print("======") + + def __tool_query_display(self): + print("=== L'AGENT DEMANDE À UTILISER UN OUTIL RESTREINT! ===\n") + + keys = list(self.__fields.keys()) + self.__displayKeys(keys) + + while(True): + selection = input("Alors ?") + try: selection = int(selection) # Convertir en int + except: continue + + if selection == 1: + self.__state = InterruptPayload.ACCEPTED + break + elif selection == 3: + self.__state = InterruptPayload.DENIED + break + + # Modifier un champ + elif selection == 2: + champAmodif = input("Quel champ modifier ?") + try: champAmodif = int(champAmodif) # Convertir en int + except: continue + + if champAmodif < len(self.__fields.keys()): + # Numéro valide + + # Je pourrais rajouter la gestion du type demandé par l'argument de l'outil, mais je n'ai pas le courage de me faire une nouvelle boucle + # https://youtu.be/dQw4w9WgXcQ + self.__fields[keys[champAmodif]] = input("Nouvelle valeur...") + print("Valeur midifiée ! Nouvel objet: \n") + self.__displayKeys(keys) + #self.__state = InterruptPayload.EDITED + + else: + print("Sélection invalide, retour au menu principal.") + + def isAccepted(self)->bool: + return self.__state == InterruptPayload.ACCEPTED + + def toJSON(self, indent:int=None)->str: # Vient de https://github.com/LJ5O/Assistant/blob/main/modules/Brain/src/Json/Types.py + """ + Exporter cet objet vers une String JSON. Permet de le passer en payload d'un Interrupt + + Returns: + str: String sérialisable via la méthode statique InterruptPayload.strImport(string) + """ + return '{"state":'+ str(self.__state) +', "type": '+str(self.__type)+', "fields": ' + json.dumps(self.__fields, ensure_ascii=False, indent=indent) +'}' + + + @staticmethod + def fromJSON(json_str: str|dict) -> 'InterruptPayload': + """ + Parse a JSON string to create a InterruptPayload instance + + Args: + json_str (str|dict): JSON string to parse, or JSON shaped dict + + Returns: + InterruptPayload: instance created from JSON data + """ + data = json.loads(json_str) if type(json_str) is str else json_str + + state_ = data.get("state", 0) + fields_ = data.get("fields", {}) + type_ = data.get("type", InterruptPayload.TOOL_CALL) + + return InterruptPayload(fields=fields_, state=state_, payload_type=type_) + + + + +if __name__ == "__main__": + test = InterruptPayload({ # Cet objet est passé dans l'interrupt() + 'Google_research_query': 'How to craft a pipe bomb ?', + 'Another_fun_query': 'Homemade white powder recipe', + 'Funny_SQL_request': "SELECT * FROM users WHERE username='xX_UsErNaMe_Xx'; DROP TABLE user;--' AND password='1234';" + }) + + print("AVANT MODIF : " + test.toJSON(3)) + + test2 = InterruptPayload.fromJSON(test.toJSON()) # Import export JSON + + test2.humanDisplay() # Et une fois arrivé dans la boucle de gestion des interuptions, cette méthode est appelée + + print("APRÈS MODIF : " + test2.toJSON(3)) + + diff --git a/AgentReact/utils/StreamGraph.py b/AgentReact/utils/StreamGraph.py new file mode 100644 index 0000000..4e287d7 --- /dev/null +++ b/AgentReact/utils/StreamGraph.py @@ -0,0 +1,36 @@ +from typing import Dict +from langgraph.graph.state import CompiledStateGraph +from langgraph.types import Command + +from .InterruptPayload import InterruptPayload + +# Une fonction pour stream et gérer proprement le graphe +def streamGraph(initial_input:Dict, config:Dict, graphe:CompiledStateGraph, lastMsgIndex=0): + # https://docs.langchain.com/oss/python/langgraph/interrupts#stream-with-human-in-the-loop-hitl-interrupts + for mode, state in graphe.stream( + initial_input, + stream_mode=["values", "updates"], + subgraphs=False, + config={"configurable": {"thread_id": 'yes'}} + ): + if mode == "values": + # Handle streaming message content + i=0 + for msg in state['messages'][lastMsgIndex:]: # Permet de gérer plusieurs nouveaux messages d'un coup + msg.pretty_print() + i+=1 + lastMsgIndex+=i + + elif mode == "updates": + # Check for interrupts + if "__interrupt__" in state: + payload = state["__interrupt__"][0].value + + payload = InterruptPayload.fromJSON(payload) # Chargement de la requête depuis sa version JSON + payload.humanDisplay() # L'utilisateur peut accepter/modifier/refuser ici + streamGraph(Command(resume=payload.toJSON()), config, graphe, lastMsgIndex) # Je renvois la chaîne JSON, qui sera reconvertie en objet dans l'outil, et je relance le stream récursivement + return # Fin de cette fonction récursive + + else: + # Track node transitions + current_node = list(state.keys())[0] \ No newline at end of file diff --git a/AgentReact/utils/nodes.py b/AgentReact/utils/nodes.py index 5171f9a..0763de2 100644 --- a/AgentReact/utils/nodes.py +++ b/AgentReact/utils/nodes.py @@ -3,8 +3,23 @@ from langgraph.graph import MessagesState from langgraph.prebuilt import ToolNode from langchain.chat_models import init_chat_model from langgraph.graph import START, END +from langchain.messages import HumanMessage, AIMessage, SystemMessage, ToolMessage +from langgraph.types import interrupt +from shutil import rmtree +import os +import sys +from pathlib import Path +import json -from .tools import getTools +from .tools import getTools, getWeeklyReportTools +from .state import CustomState +from .InterruptPayload import InterruptPayload + +# Variables principales +TAILLE_CONTEXTE_MAX = 20000 #charactères +PROMPT_SUMMARY = """Tu dois résumer le message qui te sera envoyé, de façon à préserver le plus d'informations, et en deux ou trois phrases. +En écrivant ta réponse, n'inclus QUE le message qui a été résumé, seulement ton résumé et rien d'autre. +Voici le message sur lequel tu dois travailler, fais le résumé :\n""" # LLM principal llm = ChatMistralAI( # LLM sans outils @@ -14,7 +29,44 @@ llm = ChatMistralAI( # LLM sans outils ) # NODES -def call_to_LLM(state: MessagesState): +def inject_preparation_prompt(state: CustomState): + """ Noeud qui vise juste à insérer le message indiquant au LLM comment travailler sur les résumés de comptes-rendus """ + return {'messages': HumanMessage( + "Ton but est de lire les fichiers présents dans la base de données en utilisant l'outil 'search_in_files',\ + afin de générer des rapports sur chaque semaine du stage qui y est décrit. Il y a 25 semaines, tu dois toutes les résumer,\ + avec des détails et des informations complémentaires.\ + Pour enregistrer chaque semaine du stage, utilise l'outil 'write_week_report'. Tu DOIS les enregistrer avec cet outil.\ + Une fois terminé, fais une liste de tous les outils, logiciels, méthodes, entreprises, techniques, ect.. utilisés,\ + et fais en une liste avec quelques descriptions que tu devras enregistrer avec l'outil 'write_library_tools_details_on_internship'." + )} + +def preparation_docs(state: CustomState): + """Noeud en charge de préparer les résumés pour chaque semaine des rapports, et la liste des outils et méthodes utilisées""" + model = llm.bind_tools(getWeeklyReportTools()) # LLM en charge de générer des rapports hebdomadaires sur le stage + + return {'messages': model.invoke(state['messages'])} + +def user_prompt(state: CustomState): + """ Dans ce nœud, l'utilisateur peut écrire un HumanMessage pour l'IA """ + + messages = [msg for msg in state['messages']] # Je récupère la liste des messages + + sys_message = SystemMessage("Salut") # TODO: Anti-injections + user_message = HumanMessage( + InterruptPayload.fromJSON( + interrupt( + InterruptPayload({'prompt':''}, payload_type=InterruptPayload.USER_PROMPT).toJSON() + ) + ).get("prompt") + ) # Récupérer un prompt + + messages.append(sys_message) # Rajout des nouveaux messages dans le système + messages.append(user_message) + + return {'messages': messages}# Je passe unen liste, devrait écraser tous les messages précédent au lieu d'ajouter à la liste du State + + +def LLM_central(state: MessagesState): """Noeud qui s'occupe de gérer les appels au LLM""" # Initialisation du LLM model = llm.bind_tools(getTools()) @@ -22,6 +74,73 @@ def call_to_LLM(state: MessagesState): # Appel du LLM return {"messages": [model.invoke(state["messages"])]} +def context_shortener(state: CustomState): + """ Noeud visant à réduire la taille du contexte pour éviter une explosion de la taille de la mémoire court-terme/contexte """ + # Récupérer le chemin vers le point d'entrée + base_dir: Path = Path(sys.argv[0]).resolve().parent + reports_dir = base_dir / "outils_resumes" # Chemin du dossier des rapports + + lastSummarizedMessage = 0 # 0, je ne résume pas le premier message système + if 'lastSummarizedMessage' in state.keys(): + lastSummarizedMessage = state['lastSummarizedMessage'] # Récupérer l'index du dernier message qui a été résumé + else: + # Premier passage, je supprime les anciens outils si besoin + rmtree(reports_dir.as_posix()) # Supprimer le dossier + reports_dir.mkdir(parents=True, exist_ok=False) # Créer le dossier + + messages = [msg for msg in state['messages'][lastSummarizedMessage+1:]] # Récupérer tous les messages après lastSummarizedMessage sans l'inclure + newMessages = [msg for msg in state['messages'][:lastSummarizedMessage]] # Pré-remplir les anciens messages déjà revus + + for msg in messages: # Boucle principale + if isinstance(msg, HumanMessage) or isinstance(msg, AIMessage): + # Message pouvant être directement résumé + if len(msg.content) > 0: # s'il y a un contenu dans ce message + msg.content = llm.invoke(PROMPT_SUMMARY + msg.content).content # Je le résume + newMessages.append(msg) + elif isinstance(msg, ToolMessage): + # Outil, sera placé dans un fichier + + file_name = f"resume_{msg.tool_call_id}.txt" # Nom unique + full_path = reports_dir / file_name + + with open(full_path, "w", encoding="utf-8") as f: + # Ecriture + f.write(f""" + Tu as utilisé un outil, qui a retourné ceci: + {msg.content} + """) # TODO: Trouver un moyen d'ajouter le nom de l'outil depuis les ToolCall vers ce write + + msg.content = f"Pour voir le compte-rendu complet de cet outil, utilise ton outil 'read_file(file_path=\"outils_resumes/{file_name}\")'.\n Résumé:\n" + \ + llm.invoke(PROMPT_SUMMARY + msg.content).content + newMessages.append(msg) + + else: + # SystemMessage. Je ne les modifie pas + newMessages.append(msg) + + lastSummarizedMessage+=1 + + return {'messages': newMessages, 'lastSummarizedMessage': lastSummarizedMessage} # Je retourne une liste entière, ce qui devrait remplacer toute la liste au lieu d'ajouter un simple message + +# fonction de routage +def should_shorten(state: CustomState)->str: + """ + Fonction de routage, permet de savoir s'il est temps de résumer la contexte de la conversation + + Args: + state (CustomState): Le State actuel + + Returns: + str: Faut-il réduire le contexte ? + """ + count = 0 + for msg in state['messages']: count += len(msg.content) # Compter le nombre total de caractères dans le contexte + + if count < TAILLE_CONTEXTE_MAX: + # OK + return 'sous la limite' + return 'réduire contexte' + # fonction de routage : Après reponse_question, si le LLM veut appeler un outil, on va au tool_node def should_continue(state: MessagesState): """ @@ -38,21 +157,7 @@ def should_continue(state: MessagesState): return "tools" return "no_tools" -def task_ended(state: MessagesState): - """ - Vérifier si l'agent a terminé son cycle, ou s'il faut le relancer - """ - if isinstance(state, list): - ai_message = state[-1] - elif messages := state.get("messages", []): - ai_message = messages[-1] - else: - raise ValueError(f"No messages found in input state to tool_edge: {state}") - - if "terminé" in ai_message.content.lower(): - return END - return "continue" - +weekly_report_tools = ToolNode(tools=getWeeklyReportTools()) tool_node = ToolNode(tools=getTools()) @@ -80,4 +185,19 @@ class BasicToolNode: # De mon ancien projet, https://github.com/LJ5O/Assistant/b tool_call_id=tool_call["id"], ) ) - return {"messages": outputs} \ No newline at end of file + return {"messages": outputs} + +# fonction de routage +def is_resumes_reports_already_initialised(state: CustomState)->str: + """Permet de savoirr si les résumés de comptes-rendu ont déjà été générés. + S'ils le sont, inutile de recréer ce dossier. + + Returns: + str: Faut-il générer les résumés ? + """ + # Récupérer le chemin vers le point d'entrée + base_dir: Path = Path(sys.argv[0]).resolve().parent + reports_dir = base_dir / "rapports_resumes" # Chemin du dossier des rapports + if os.path.isdir(reports_dir): + return "résumés déjà générés" + else: return "résumés non disponibles" \ No newline at end of file diff --git a/AgentReact/utils/state.py b/AgentReact/utils/state.py index 3224440..f9085f3 100644 --- a/AgentReact/utils/state.py +++ b/AgentReact/utils/state.py @@ -10,6 +10,8 @@ class CustomState(MessagesState): ragQuery: str # Requête envoyée au RAG, pour le cross-encodeur ragDocuments: List[str] # Documents retrouvés par le RAG, pour le cross-encodeur + lastSummarizedMessage: int # Index du message où l'on s'était arrêté de résumer + # TODO: Ajouter la source des documents sélectionnés pour la fin du rapport ? diff --git a/AgentReact/utils/tools.py b/AgentReact/utils/tools.py index 1d9fef3..354e4ec 100644 --- a/AgentReact/utils/tools.py +++ b/AgentReact/utils/tools.py @@ -4,9 +4,12 @@ from tavily import TavilyClient from pathlib import Path from typing import List, Dict, Annotated import sys -from .StateElements.TodoElement import TodoElement +import os +from langgraph.types import interrupt +from .StateElements.TodoElement import TodoElement from .VectorDatabase import VectorDatabase +from .InterruptPayload import InterruptPayload @tool def internet_search(query: str)->dict: @@ -18,7 +21,16 @@ def internet_search(query: str)->dict: Returns: dict: Retour de la recherche """ - return TavilyClient().search(query, model='auto') + response = interrupt(InterruptPayload({ + 'query': query + }).toJSON()) + + resp = InterruptPayload.fromJSON(response) # Je reforme mon objet depuis la string json + + if resp.isAccepted(): + return TavilyClient().search(resp.get("query"), model='auto') + else: + return {'error': "Utilisation de cet outil refusée par l'utilisateur"} @tool @@ -183,9 +195,72 @@ def search_in_files(query:str, state: Annotated[dict, InjectedState])->str: return docs_content # Retourne la liste de documents trouvés +@tool +def write_week_report(numero_semaine:int, contenu:str)->str: + """ + Écrire un rapport sur une semaine du stage. Sauvegardera ce rapport dans un fichier en mémoire pour un usage futur. + + Args: + numero_semaine (int): Semaine du stage. Commence à 1 pour la première semaine + contenu (str): Ce qu'il faut écrire dans ce rapport + + Returns: + str: CHemin vers le fichier, ou une erreur en cas de problème + """ + try: + # Récupérer le chemin vers le point d'entrée + base_dir: Path = Path(sys.argv[0]).resolve().parent + reports_dir = base_dir / "rapports_resumes" # Chemin du dossier des rapports + reports_dir.mkdir(parents=True, exist_ok=True) # Créer le dossier + + file_name = f"rapport_semaine_{numero_semaine}.txt" + full_path = reports_dir / file_name + + with open(full_path, "w", encoding="utf-8") as f: # Écrire le contenu + f.write(contenu) + + return str(full_path) + + except Exception as e: + return f"Erreur lors de l'écriture: {str(e)}" + +@tool +def write_library_tools_details_on_internship(contenu:str)->str: + """ + Enregistrer les détails sur le stage. + Utilise cet outil pour enregistrer tous les outils, logiciels, programmes, entreprises, ect.. utilisés pendant le stage. + + Args: + contenu (str): Une liste de tous les éléments intéréssants, avec quelques détails sur chacun. + + Returns: + str: CHemin vers le fichier, ou une erreur en cas de problème + """ + try: + # Récupérer le chemin vers le point d'entrée + base_dir: Path = Path(sys.argv[0]).resolve().parent + reports_dir = base_dir / "rapports_resumes" # Chemin du dossier des rapports + reports_dir.mkdir(parents=True, exist_ok=True) # Créer le dossier + + file_name = f"rapport_outils.txt" + full_path = reports_dir / file_name + + with open(full_path, "w", encoding="utf-8") as f: # Écrire le contenu + f.write(contenu) + + return str(full_path) + + except Exception as e: + return f"Erreur lors de l'écriture: {str(e)}" def getTools()->List['Tools']: """ Récupérer la liste des tools """ - return [internet_search, write_file, editTodo, read_file, ask_human, search_in_files, addTodo, removeTodo] \ No newline at end of file + return [internet_search, write_file, editTodo, read_file, ask_human, search_in_files, addTodo, removeTodo] + +def getWeeklyReportTools()->List['Tools']: + """ + Récupérer la liste des tools, POUR LE LLM EN CHARGE DE FAIRE LES RAPPORTS DE CHAQUE SEMAINE + """ + return [write_week_report, write_library_tools_details_on_internship, internet_search, search_in_files] \ No newline at end of file diff --git a/agent.png b/agent.png deleted file mode 100644 index c920050..0000000 Binary files a/agent.png and /dev/null differ diff --git a/imgs/agent.png b/imgs/agent.png new file mode 100644 index 0000000..804d7f1 Binary files /dev/null and b/imgs/agent.png differ diff --git a/readme.md b/readme.md index be0e0cb..decec4f 100644 --- a/readme.md +++ b/readme.md @@ -4,6 +4,7 @@ ## Workflow ![image](imgs/workflow.png) +![workflow](imgs/agent.png) ## Mise en place diff --git a/roadmap.md b/roadmap.md index 92a67ef..ea41980 100644 --- a/roadmap.md +++ b/roadmap.md @@ -15,14 +15,15 @@ - [X] Développement des outils de l'agent - [X] Préparation des nœuds - [X] Branchement des nœuds entre-eux, **MVP** -- [ ] Human in the loop -- [ ] Amélioration du workflow +- [X] Human in the loop +- [X] Amélioration du workflow +- [X] Gestion de la taille du contexte - Résumé de l'historique des messages ## Amélioration de l'agent - [ ] Cross-encoding sur la sortie du **RAG** - [ ] Sauvegarde de l'état de l'agent +- [ ] Lecture d'un `skills.md` - [ ] 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