3 Commits

Author SHA1 Message Date
23e18d6a88 TodoElement est maintenant sérializable
Requis pour le state
2026-02-10 19:12:53 +01:00
f1caea0323 TODO liste avant LLM
Affichée juste avant dans un System message
2026-02-10 19:00:12 +01:00
d575fdb511 Skill.md tool 2026-02-10 16:51:12 +01:00
6 changed files with 158 additions and 27 deletions

48
AgentReact/skills.md Normal file
View File

@@ -0,0 +1,48 @@
# Fichier des skills et compétences
Ce fichier vise à t'expliquer comment réaliser certaines étapes de la génération du rapport de stage.
---
## Creation_plan
Voici un plan que tu peux utiliser pour générer un rapport de stage:
```
# Rapport de stage au sein de l'entreprise [NOM]
## Sommaire
- Introduction
- L'entreprise [NOM]
- État de l'art
- Le stage
- Semaine 1
- Semaine 2
- ...
- Semaine n
- Conclusions et résultats
- Sources
## Introduction
Ecris une courte intro, de deux ou trois paragraphes.
## L'entreprise [NOM]
Décris l'entreprise, ce qu'elle fait, ses clients, son secteur, ...
Fais deux ou trois paragraphes.
## État de l'art
Regarde quels outils, techniques, supports ont été utilisés, décris-les et explique. Décris aussi les dernières avancées dans le domaine du rapport de stage et les outils utilisés pour le mener à bien, avec un paragraphe par outil, technique ou support.
## Le stage
Décris globalement le stage et introduis le en un paragraphe
### Semaine 1
Fais deux ou trois paragraphes par semaine pour expliquer ce qui a été fait, ce qui devra suivre, comment ça a été fait, ect...
### Semaine n
## Conclusion et résultats
Reprends ici les découvertes faites pendant le stage, fais ressortir les résultats et les enseignements du stage, en deux à quatre paragraphes
## Sources
Liste ici les sources que tu as utilisé pour rédiger l'ensemble du document
```

View File

@@ -1,3 +1,5 @@
import json
# Classes utilisées pour représenter des données # Classes utilisées pour représenter des données
class TodoElement(): class TodoElement():
STATE_NOT_STARTED = 0 # Sorte d'enum qui représente l'état d'une tâche STATE_NOT_STARTED = 0 # Sorte d'enum qui représente l'état d'une tâche
@@ -7,10 +9,10 @@ class TodoElement():
name: str name: str
state: int state: int
def __init__(self, name:str, description:str=None): def __init__(self, name:str, description:str=None, state:int=0):
self.name = name self.name = name
self.description = description self.description = description
self.state = TodoElement.STATE_NOT_STARTED self.state = state
def __str__(self)->str: def __str__(self)->str:
""" """
@@ -33,8 +35,39 @@ class TodoElement():
else: else:
return "Inconnu" return "Inconnu"
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 dans le State
Returns:
str: String sérialisable via la méthode statique TodoElement.strImport(string)
"""
return '{"name":"'+ str(self.name) +'", "desc": "'+str(self.description)+'", "state": ' + str(self.state) +'}'
@staticmethod
def fromJSON(json_str: str|dict) -> 'InterruptPayload':
"""
Parse a JSON string to create a TodoElement instance
Args:
json_str (str|dict): JSON string to parse, or JSON shaped dict
Returns:
TodoElement: instance created from JSON data
"""
data = json.loads(json_str) if type(json_str) is str else json_str
nom_ = data.get("name", "undefined")
desc_ = data.get("desc", "undefined")
state_ = data.get("state", TodoElement.STATE_NOT_STARTED)
return TodoElement(nom_, desc_, state_)
if __name__ == "__main__": if __name__ == "__main__":
test = TodoElement("TEST tâche", "OUI") test = TodoElement("TEST tâche", "OUI")
test.state = TodoElement.STATE_STARTED test.state = TodoElement.STATE_STARTED
print(test) print(test)
print([str(test)]) print([str(test)])
print(test.toJSON())
print(TodoElement.fromJSON(test.toJSON()))

View File

@@ -14,6 +14,7 @@ import json
from .tools import getTools, getWeeklyReportTools from .tools import getTools, getWeeklyReportTools
from .state import CustomState from .state import CustomState
from .InterruptPayload import InterruptPayload from .InterruptPayload import InterruptPayload
from .StateElements.TodoElement import TodoElement
# Variables principales # Variables principales
TAILLE_CONTEXTE_MAX = 20000 #charactères TAILLE_CONTEXTE_MAX = 20000 #charactères
@@ -66,10 +67,17 @@ def user_prompt(state: CustomState):
return {'messages': messages}# Je passe unen liste, devrait écraser tous les messages précédent au lieu d'ajouter à la liste du State 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): def LLM_central(state: CustomState):
"""Noeud qui s'occupe de gérer les appels au LLM""" """Noeud qui s'occupe de gérer les appels au LLM"""
# Initialisation du LLM # Initialisation du LLM
model = llm.bind_tools(getTools()) model = llm.bind_tools(getTools())
#print(state)
if "todo" in state.keys(): # S'il y a des TODO, je l'ajoute avant le prompt au LLM
if len(state['todo'])>0:
sysmsg = SystemMessage(f"Voici la liste des tâches en cours : {str([f"{i}: {str(TodoElement.fromJSON(todo))}\n" for i,todo in enumerate(state['todo'])])}")
print(sysmsg.content)
return {"messages": [model.invoke(state["messages"] + [AIMessage('.'), sysmsg])]} # AIMessage pour que Msitrail ne refuse pas la requête avec un 400
# Appel du LLM # Appel du LLM
return {"messages": [model.invoke(state["messages"])]} return {"messages": [model.invoke(state["messages"])]}
@@ -142,7 +150,7 @@ def should_shorten(state: CustomState)->str:
return 'réduire contexte' return 'réduire contexte'
# fonction de routage : Après reponse_question, si le LLM veut appeler un outil, on va au tool_node # fonction de routage : Après reponse_question, si le LLM veut appeler un outil, on va au tool_node
def should_continue(state: MessagesState): def should_continue(state: CustomState):
""" """
Vérifier s'il y a un appel aux outils dans le dernier message Vérifier s'il y a un appel aux outils dans le dernier message
""" """

View File

@@ -1,11 +1,9 @@
from langgraph.graph import StateGraph, MessagesState from langgraph.graph import StateGraph, MessagesState
from typing import List from typing import List
from .StateElements.TodoElement import TodoElement
class CustomState(MessagesState): class CustomState(MessagesState):
todo: List[TodoElement] # Les tâches en cours todo: List[str] # Les tâches en cours, au format JSON
ragQuery: str # Requête envoyée au RAG, pour le cross-encodeur 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 ragDocuments: List[str] # Documents retrouvés par le RAG, pour le cross-encodeur

View File

@@ -1,5 +1,8 @@
from langchain.tools import tool from langchain.tools import tool
from langgraph.prebuilt import InjectedState from langgraph.prebuilt import InjectedState
from langchain_core.tools import InjectedToolCallId
from langchain_core.messages import ToolMessage
from langgraph.types import Command
from tavily import TavilyClient from tavily import TavilyClient
from pathlib import Path from pathlib import Path
from typing import List, Dict, Annotated from typing import List, Dict, Annotated
@@ -60,21 +63,20 @@ def write_file(file_path:str, content: str, append:bool=True) -> str:
return f"Erreur lors de l'écriture: {str(e)}" return f"Erreur lors de l'écriture: {str(e)}"
@tool @tool
def editTodo(index:int, todoState:int, state: Annotated[dict, InjectedState])->bool: # https://stackoverflow.com/a/79525434 def editTodo(index:int, todoState:int, state: Annotated[dict, InjectedState], tool_call_id: Annotated[str, InjectedToolCallId])->Command: # https://stackoverflow.com/a/79525434
""" """
Modifier l'état d'une tâche (TODO) Modifier l'état d'une tâche (TODO)
Args: Args:
index (int): Index de la tâche à modifier, en commançant à 0 pour la première tâche. index (int): Index de la tâche à modifier, en commançant à 0 pour la première tâche.
todoState (int): Nouvel état. 0 pour "non commencé, 1 pour "en cours", 2 pour "complété" todoState (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 "todo" not in state.keys(): return Command(update={"messages": [ToolMessage(content="Echec!", tool_call_id=tool_call_id)]})
if len(state["todo"]) <= index: if len(state["todo"]) <= index:
# Erreur, l'index est trop grand # Erreur, l'index est trop grand
return False return Command(update={"messages": [ToolMessage(content="Index en dehors de la liste, echec!", tool_call_id=tool_call_id)]})
state["todo"] = [TodoElement.fromJSON(e) for e in state["todo"]] # Convertion vers de vraies instances
state["todo"][index].state = todoState # Modification de l'état de cette tâche state["todo"][index].state = todoState # Modification de l'état de cette tâche
# Toutes les tâches complétées ? # Toutes les tâches complétées ?
@@ -85,42 +87,47 @@ def editTodo(index:int, todoState:int, state: Annotated[dict, InjectedState])->b
break break
if not found: state["todo"] = [] # Toutes les tâches terminées, je peux clear la TODO list du state if not found: state["todo"] = [] # Toutes les tâches terminées, je peux clear la TODO list du state
return True return Command(update={
"messages": [ToolMessage(content="Réussite!", tool_call_id=tool_call_id)],
"todo": [x.toJSON() for x in state["todo"]] # Update du state, # medium.com/@o39joey/a-comprehensive-guide-to-langgraph-managing-agent-state-with-tools-ae932206c7d7
})
@tool @tool
def addTodo(name:str, description:str, state: Annotated[dict, InjectedState])->bool: def addTodo(name:str, description:str, state: Annotated[dict, InjectedState], tool_call_id: Annotated[str, InjectedToolCallId])->Command:
""" """
Ajouter une nouvelle tâche/TODO Ajouter une nouvelle tâche/TODO
Args: Args:
name (str): Nom de cette tâche name (str): Nom de cette tâche
description (str): Une ou deux phrases pour décrire le travail à effectuer dans ce TODO 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"] = [] if "todo" not in state.keys(): state["todo"] = []
state["todo"] = [TodoElement.fromJSON(e) for e in state["todo"]] # Convertion vers de vraies instances
state["todo"].append(TodoElement(name, description)) state["todo"].append(TodoElement(name, description))
return True return Command(update={
"messages": [ToolMessage(content="Réussite!", tool_call_id=tool_call_id)],
"todo": [x.toJSON() for x in state["todo"]] # Update du state, # medium.com/@o39joey/a-comprehensive-guide-to-langgraph-managing-agent-state-with-tools-ae932206c7d7
})
@tool @tool
def removeTodo(index:int, state: Annotated[dict, InjectedState])->bool: def removeTodo(index:int, state: Annotated[dict, InjectedState], tool_call_id: Annotated[str, InjectedToolCallId])->Command:
""" """
Retirer une tâche/TODO de la liste des tâches Retirer une tâche/TODO de la liste des tâches
Args: Args:
index (int): Position de la tâche dans la liste, commence à 0 pour le premier TODO 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 "todo" not in state.keys(): return Command(update={"messages": [ToolMessage(content="Echec!", tool_call_id=tool_call_id)]})
if len(state["todo"]) <= index: if len(state["todo"]) <= index:
# Erreur, l'index est trop grand # Erreur, l'index est trop grand
return False return Command(update={"messages": [ToolMessage(content="Index en dehors de la liste, echec!", tool_call_id=tool_call_id)]})
state['todo'].pop(index) state['todo'].pop(index)
return True return Command(update={
"messages": [ToolMessage(content="Réussite!", tool_call_id=tool_call_id)],
"todo": [x for x in state["todo"]] # Update du state, # medium.com/@o39joey/a-comprehensive-guide-to-langgraph-managing-agent-state-with-tools-ae932206c7d7
})
@tool @tool
def read_file(file_path: str) -> str: def read_file(file_path: str) -> str:
@@ -145,6 +152,43 @@ def read_file(file_path: str) -> str:
except Exception as e: except Exception as e:
return f"Erreur lors de la lecture : {str(e)}" return f"Erreur lors de la lecture : {str(e)}"
@tool
def get_skill(skill_name:str=None)->str:
"""
Obtenir un skill, la description de comment faire quelque chose.
Args:
skill_name (str, optional): Nom du skill recherché. Si ce n'est pas donné, listera les skills disponibles.
Returns:
str: Sans nom de skill, la liste de ceux disponibles. Si un nom de skill est donné, l'ensemble de ce skill.
"""
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
full_path:str = base_dir + "/skills.md" # Puis générer le chemin vers le fichier
with open(full_path, "r", encoding="utf-8") as f:
content = f.read()
if skill_name is None:
# Liste des skills
names = []
for part in content.split("---")[1:]: # Pas besoin de la première partie
names.append(part.splitlines()[1].split(' ')[1]) # Récupérer le nom du skill à la seconde ligne
return str(names)
else:
# Récupérer un skill
for part in content.split("---")[1:]:
if skill_name.lower() in part.lower(): # Dégueulasse pour l'opti mais c'est rapide à implémenter
# Si c'est ce skill qui est recherché
return f"{content.split("---")[0]}\n\n{part}"
return "Ce skill n'existe pas ! Regarde la liste des skills en rappelant cet outil sans arguments !"
except Exception as e:
return f"Erreur lors de la lecture : {str(e)}"
@tool @tool
def ask_human(request:str)->str: def ask_human(request:str)->str:
""" """
@@ -257,7 +301,7 @@ def getTools()->List['Tools']:
""" """
Récupérer la liste des tools Récupérer la liste des tools
""" """
return [internet_search, write_file, editTodo, read_file, ask_human, search_in_files, addTodo, removeTodo] return [internet_search, write_file, editTodo, read_file, ask_human, search_in_files, addTodo, removeTodo, get_skill]
def getWeeklyReportTools()->List['Tools']: def getWeeklyReportTools()->List['Tools']:
""" """

View File

@@ -22,7 +22,7 @@
## Amélioration de l'agent ## Amélioration de l'agent
- [ ] Cross-encoding sur la sortie du **RAG** - [ ] Cross-encoding sur la sortie du **RAG**
- [ ] Sauvegarde de l'état de l'agent - [ ] Sauvegarde de l'état de l'agent
- [ ] Lecture d'un `skills.md` - [X] Lecture d'un `skills.md`
- [ ] Système de redémarrage après un arrêt - [ ] Système de redémarrage après un arrêt
- [ ] Détection de *prompt injection* - [ ] Détection de *prompt injection*
- [ ] Génération d'un PDF en sortie du système - [ ] Génération d'un PDF en sortie du système