Compare commits
21 Commits
RAG
...
dcfca0f813
| Author | SHA1 | Date | |
|---|---|---|---|
| dcfca0f813 | |||
|
33be2a7ac8
|
|||
|
8655359add
|
|||
|
8b32c0ac64
|
|||
|
bdf5b7dd98
|
|||
|
e0bd50a15b
|
|||
|
82a5491188
|
|||
|
ea314e5c5c
|
|||
|
fc7f692ba3
|
|||
|
52bf2d5a82
|
|||
|
fa19f804d8
|
|||
|
a9ff56c122
|
|||
|
523cea84fe
|
|||
|
29054a2b6d
|
|||
| 986e395a23 | |||
|
633726b2a0
|
|||
|
1c2f0728ea
|
|||
|
14b8664106
|
|||
|
1f96b9a408
|
|||
|
dc746edeea
|
|||
|
af253b71dd
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,6 +5,8 @@ mlflow.db
|
|||||||
# Par sécurité
|
# Par sécurité
|
||||||
documents_projet/
|
documents_projet/
|
||||||
chroma_db/
|
chroma_db/
|
||||||
|
AgentReact/rapports_resumes/
|
||||||
|
AgentReact/outils_resumes/
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
from langgraph.graph import START, END
|
from langgraph.graph import START, END
|
||||||
from langgraph.graph.state import CompiledStateGraph
|
from langgraph.graph.state import CompiledStateGraph
|
||||||
|
from langgraph.checkpoint.memory import InMemorySaver
|
||||||
|
|
||||||
from utils.nodes import reponse_question, tool_node, should_continue
|
from utils.nodes import *
|
||||||
from utils.state import getState
|
from utils.state import getState
|
||||||
|
from utils.tools import getTools
|
||||||
|
|
||||||
def getGraph()->CompiledStateGraph:
|
def getGraph()->CompiledStateGraph:
|
||||||
"""
|
"""
|
||||||
@@ -14,19 +16,41 @@ def getGraph()->CompiledStateGraph:
|
|||||||
workflow = getState() # State prêt à utiliser
|
workflow = getState() # State prêt à utiliser
|
||||||
|
|
||||||
# Définition des sommets du graphe
|
# Définition des sommets du graphe
|
||||||
workflow.add_node(reponse_question)
|
workflow.add_node(user_prompt)
|
||||||
workflow.add_node("tool_node", tool_node) # N'est pas une fonction, mais une classe instanciée, je dois précisier le nom du node
|
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
|
# Arrêtes
|
||||||
workflow.set_entry_point("reponse_question")
|
workflow.set_conditional_entry_point(is_resumes_reports_already_initialised, {
|
||||||
workflow.add_edge("tool_node", "reponse_question")
|
"résumés non disponibles": "inject_preparation_prompt", # Résumés non générés
|
||||||
workflow.add_conditional_edges("reponse_question", should_continue, {
|
"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",
|
"tools":"tool_node",
|
||||||
END:END
|
"no_tools":END
|
||||||
})
|
})
|
||||||
|
|
||||||
return workflow.compile()
|
return workflow.compile(checkpointer=InMemorySaver()) # TODO: Rempalcer par une vrai BDD de prod
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Affichage du graphe
|
# Affichage du graphe
|
||||||
getGraph().get_graph().draw_mermaid_png(output_file_path="agent.png")
|
getGraph().get_graph().draw_mermaid_png(output_file_path="imgs/agent.png")
|
||||||
@@ -5,11 +5,18 @@ from langchain.messages import HumanMessage, SystemMessage, AIMessage, ToolMessa
|
|||||||
import mlflow
|
import mlflow
|
||||||
|
|
||||||
from agent import getGraph
|
from agent import getGraph
|
||||||
|
from utils.InterruptPayload import InterruptPayload
|
||||||
|
from utils.StreamGraph import streamGraph
|
||||||
|
|
||||||
# MLFLOW
|
# MLFLOW
|
||||||
mlflow.set_experiment("TEST PROJET") # VOIR AVEC LA COMMANDE "MLFLOW SERVER"
|
mlflow.set_experiment("TEST PROJET") # VOIR AVEC LA COMMANDE "MLFLOW SERVER"
|
||||||
mlflow.langchain.autolog()
|
mlflow.langchain.autolog()
|
||||||
|
|
||||||
out_state = getGraph().invoke({'messages':[HumanMessage("What's the price for bitcoin ?")]})
|
initial_input = {
|
||||||
for message in out_state['messages']:
|
'messages':[SystemMessage("Salut")]
|
||||||
message.pretty_print()
|
}
|
||||||
|
|
||||||
|
config={"configurable": {"thread_id": 'yes'}}
|
||||||
|
|
||||||
|
# Et je lance !
|
||||||
|
streamGraph(initial_input, config, getGraph())
|
||||||
156
AgentReact/utils/InterruptPayload.py
Normal file
156
AgentReact/utils/InterruptPayload.py
Normal file
@@ -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))
|
||||||
|
|
||||||
|
|
||||||
40
AgentReact/utils/StateElements/TodoElement.py
Normal file
40
AgentReact/utils/StateElements/TodoElement.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Classes utilisées pour représenter des données
|
||||||
|
class TodoElement():
|
||||||
|
STATE_NOT_STARTED = 0 # Sorte d'enum qui représente l'état d'une tâche
|
||||||
|
STATE_STARTED = 1
|
||||||
|
STATE_COMPLETED = 2
|
||||||
|
|
||||||
|
name: str
|
||||||
|
state: int
|
||||||
|
|
||||||
|
def __init__(self, name:str, description:str=None):
|
||||||
|
self.name = name
|
||||||
|
self.description = description
|
||||||
|
self.state = TodoElement.STATE_NOT_STARTED
|
||||||
|
|
||||||
|
def __str__(self)->str:
|
||||||
|
"""
|
||||||
|
Affiche la tâche, son nom et son statut
|
||||||
|
Affichera aussi la description de la tâche si elle a été définie ET est en cours
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Représentation écrite de la tâche
|
||||||
|
"""
|
||||||
|
return f"Tâche \"{self.name}\": {self.__getStateName()}." + \
|
||||||
|
(f" Description: {self.description}" if self.description and self.state == TodoElement.STATE_STARTED else '')
|
||||||
|
|
||||||
|
def __getStateName(self)->str:
|
||||||
|
if self.state == TodoElement.STATE_NOT_STARTED:
|
||||||
|
return "Non commencée"
|
||||||
|
elif self.state == TodoElement.STATE_STARTED:
|
||||||
|
return "En cours"
|
||||||
|
elif self.state == TodoElement.STATE_COMPLETED:
|
||||||
|
return "Terminée"
|
||||||
|
else:
|
||||||
|
return "Inconnu"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test = TodoElement("TEST tâche", "OUI")
|
||||||
|
test.state = TodoElement.STATE_STARTED
|
||||||
|
print(test)
|
||||||
|
print([str(test)])
|
||||||
36
AgentReact/utils/StreamGraph.py
Normal file
36
AgentReact/utils/StreamGraph.py
Normal file
@@ -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]
|
||||||
23
AgentReact/utils/VectorDatabase.py
Normal file
23
AgentReact/utils/VectorDatabase.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from langchain_huggingface import HuggingFaceEmbeddings
|
||||||
|
from langchain_chroma import Chroma # TODO plus tard, ramplacer par PG Vector
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
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/"
|
||||||
|
|
||||||
|
EMBEDDINGS = HuggingFaceEmbeddings(model_name="intfloat/multilingual-e5-large", model_kwargs={"trust_remote_code": True})
|
||||||
|
CHROMA = Chroma(
|
||||||
|
persist_directory=bdd_path,
|
||||||
|
embedding_function=EMBEDDINGS
|
||||||
|
)
|
||||||
|
|
||||||
|
class VectorDatabase: # Classe pour récupérer la BDD
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getChroma()->Chroma:
|
||||||
|
return CHROMA
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getEmbeddings()->'Embeddings Hugging Face':
|
||||||
|
return EMBEDDINGS
|
||||||
@@ -3,8 +3,23 @@ from langgraph.graph import MessagesState
|
|||||||
from langgraph.prebuilt import ToolNode
|
from langgraph.prebuilt import ToolNode
|
||||||
from langchain.chat_models import init_chat_model
|
from langchain.chat_models import init_chat_model
|
||||||
from langgraph.graph import START, END
|
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 principal
|
||||||
llm = ChatMistralAI( # LLM sans outils
|
llm = ChatMistralAI( # LLM sans outils
|
||||||
@@ -14,21 +29,122 @@ llm = ChatMistralAI( # LLM sans outils
|
|||||||
)
|
)
|
||||||
|
|
||||||
# NODES
|
# NODES
|
||||||
def reponse_question(state: MessagesState):
|
def inject_preparation_prompt(state: CustomState):
|
||||||
"""Noeud qui réponds à la question, en s'aidant si besoin des outils à disposition"""
|
""" 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
|
# Initialisation du LLM
|
||||||
model = llm.bind_tools(getTools())
|
model = llm.bind_tools(getTools())
|
||||||
|
|
||||||
# Appel du LLM
|
# Appel du LLM
|
||||||
return {"messages": [model.invoke(state["messages"])]}
|
return {"messages": [model.invoke(state["messages"])]}
|
||||||
|
|
||||||
tool_node = ToolNode(tools=getTools()) # Node gérant les outils
|
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
|
||||||
|
|
||||||
# fonction de routage : Après reponse_question, si le LLM veut appeler un outil, on va au tool_node, sinon on termine
|
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):
|
def should_continue(state: MessagesState):
|
||||||
"""
|
"""
|
||||||
Use in the conditional_edge to route to the ToolNode if the last message
|
Vérifier s'il y a un appel aux outils dans le dernier message
|
||||||
has tool calls. Otherwise, route to the end.
|
|
||||||
"""
|
"""
|
||||||
if isinstance(state, list):
|
if isinstance(state, list):
|
||||||
ai_message = state[-1]
|
ai_message = state[-1]
|
||||||
@@ -39,4 +155,49 @@ def should_continue(state: MessagesState):
|
|||||||
|
|
||||||
if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:
|
if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:
|
||||||
return "tools"
|
return "tools"
|
||||||
return END
|
return "no_tools"
|
||||||
|
|
||||||
|
weekly_report_tools = ToolNode(tools=getWeeklyReportTools())
|
||||||
|
tool_node = ToolNode(tools=getTools())
|
||||||
|
|
||||||
|
|
||||||
|
class BasicToolNode: # De mon ancien projet, https://github.com/LJ5O/Assistant/blob/main/modules/Brain/src/LLM/graph/nodes/BasicToolNode.py
|
||||||
|
"""A node that runs the tools requested in the last AIMessage."""
|
||||||
|
|
||||||
|
def __init__(self, tools: list) -> None:
|
||||||
|
self.tools_by_name = {tool.name: tool for tool in tools}
|
||||||
|
|
||||||
|
def __call__(self, inputs: dict):
|
||||||
|
if messages := inputs.get("messages", []):
|
||||||
|
message = messages[-1]
|
||||||
|
else:
|
||||||
|
raise ValueError("No message found in input")
|
||||||
|
outputs = []
|
||||||
|
for tool_call in message.tool_calls:
|
||||||
|
#print(tool_call["args"])
|
||||||
|
tool_result = self.tools_by_name[tool_call["name"]].invoke(
|
||||||
|
tool_call["args"]
|
||||||
|
)
|
||||||
|
outputs.append(
|
||||||
|
ToolMessage(
|
||||||
|
content=json.dumps(tool_result),
|
||||||
|
name=tool_call["name"],
|
||||||
|
tool_call_id=tool_call["id"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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"
|
||||||
@@ -1,7 +1,19 @@
|
|||||||
from langgraph.graph import StateGraph, MessagesState
|
from langgraph.graph import StateGraph, MessagesState
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .StateElements.TodoElement import TodoElement
|
||||||
|
|
||||||
|
|
||||||
|
class CustomState(MessagesState):
|
||||||
|
todo: List[TodoElement] # Les tâches en cours
|
||||||
|
|
||||||
|
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 ?
|
||||||
|
|
||||||
class hjgzefvuiyguhzfvihuozdef(MessagesState): # J'ai du mal à nommer mes classes ._.
|
|
||||||
pass
|
|
||||||
|
|
||||||
def getState()->StateGraph:
|
def getState()->StateGraph:
|
||||||
"""
|
"""
|
||||||
@@ -10,4 +22,4 @@ def getState()->StateGraph:
|
|||||||
Returns:
|
Returns:
|
||||||
StateGraph: prêt à utiliser
|
StateGraph: prêt à utiliser
|
||||||
"""
|
"""
|
||||||
return StateGraph(hjgzefvuiyguhzfvihuozdef)
|
return StateGraph(CustomState)
|
||||||
@@ -1,6 +1,15 @@
|
|||||||
from langchain.tools import tool
|
from langchain.tools import tool
|
||||||
|
from langgraph.prebuilt import InjectedState
|
||||||
from tavily import TavilyClient
|
from tavily import TavilyClient
|
||||||
from typing import List
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Annotated
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from langgraph.types import interrupt
|
||||||
|
|
||||||
|
from .StateElements.TodoElement import TodoElement
|
||||||
|
from .VectorDatabase import VectorDatabase
|
||||||
|
from .InterruptPayload import InterruptPayload
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
def internet_search(query: str)->dict:
|
def internet_search(query: str)->dict:
|
||||||
@@ -12,26 +21,246 @@ def internet_search(query: str)->dict:
|
|||||||
Returns:
|
Returns:
|
||||||
dict: Retour de la recherche
|
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
|
@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:
|
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:
|
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==")
|
try:
|
||||||
print(content)
|
base_dir:str = Path(sys.argv[0]).resolve().parent.as_posix() # Récupérer le chemin vers le point d'entrée du programme
|
||||||
return "Fichier écrit"
|
full_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, todoState: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.
|
||||||
|
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 len(state["todo"]) <= index:
|
||||||
|
# Erreur, l'index est trop grand
|
||||||
|
return False
|
||||||
|
|
||||||
|
state["todo"][index].state = todoState # 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))
|
||||||
|
return True
|
||||||
|
|
||||||
|
@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
|
||||||
|
full_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(full_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])->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:
|
||||||
|
str: Échantillons de documents correspondants, concaténés en une seule chaîne de caractères.
|
||||||
|
"""
|
||||||
|
bdd = VectorDatabase.getChroma() # Récupère l'unique instance de cette BDD, c'est un SIngleton
|
||||||
|
|
||||||
|
retrieved_docs = bdd.similarity_search(query, 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"] = retrieved_docs
|
||||||
|
|
||||||
|
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']:
|
def getTools()->List['Tools']:
|
||||||
"""
|
"""
|
||||||
Récupérer la liste des tools
|
Récupérer la liste des tools
|
||||||
"""
|
"""
|
||||||
return [internet_search, write_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]
|
||||||
@@ -43,7 +43,7 @@ print("===")
|
|||||||
# Création du modèle d'embeddings
|
# Création du modèle d'embeddings
|
||||||
# https://docs.langchain.com/oss/python/integrations/text_embedding/huggingfacehub
|
# https://docs.langchain.com/oss/python/integrations/text_embedding/huggingfacehub
|
||||||
# https://huggingface.co/jinaai/jina-clip-v2
|
# https://huggingface.co/jinaai/jina-clip-v2
|
||||||
embeddings = HuggingFaceEmbeddings(model_name="jinaai/jina-embeddings-v3", model_kwargs={"trust_remote_code": True})
|
embeddings = HuggingFaceEmbeddings(model_name="intfloat/multilingual-e5-large", model_kwargs={"trust_remote_code": True})
|
||||||
|
|
||||||
# Stockage des embeddings dans ChromaDB dans un dossier local "chroma_db"
|
# Stockage des embeddings dans ChromaDB dans un dossier local "chroma_db"
|
||||||
vectorstore = Chroma.from_documents(documents=chunks,embedding=embeddings, persist_directory=base_dir.as_posix()+"/chroma_db/",) # https://docs.langchain.com/oss/python/integrations/vectorstores/chroma
|
vectorstore = Chroma.from_documents(documents=chunks,embedding=embeddings, persist_directory=base_dir.as_posix()+"/chroma_db/",) # https://docs.langchain.com/oss/python/integrations/vectorstores/chroma
|
||||||
|
|||||||
BIN
imgs/agent.png
Normal file
BIN
imgs/agent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 293 KiB |
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
## Mise en place
|
## Mise en place
|
||||||
|
|
||||||
|
|||||||
13
roadmap.md
13
roadmap.md
@@ -11,13 +11,18 @@
|
|||||||
- [X] Lecture des documents et mise en base de données vectorielle
|
- [X] Lecture des documents et mise en base de données vectorielle
|
||||||
|
|
||||||
## Mise en place de l'agent
|
## Mise en place de l'agent
|
||||||
- [ ] Préparation du `State`
|
- [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
|
- [X] Préparation des nœuds
|
||||||
- [ ] Branchement des nœuds entre-eux
|
- [X] Branchement des nœuds entre-eux, **MVP**
|
||||||
|
- [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
|
## Amélioration de l'agent
|
||||||
|
- [ ] 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`
|
||||||
- [ ] 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
|
||||||
|
|||||||
Reference in New Issue
Block a user