File size: 5,569 Bytes
3e0117c
 
 
 
 
b2dd427
3e0117c
78d89bf
346dab1
3e0117c
 
 
 
b2dd427
3e0117c
 
346dab1
 
438d4f9
 
 
3e0117c
 
 
 
 
b2dd427
 
 
 
3e0117c
 
 
 
 
 
 
 
 
 
78d89bf
 
3e0117c
 
b2dd427
3e0117c
78d89bf
 
 
 
3e0117c
 
 
 
 
 
 
 
 
 
78d89bf
3e0117c
78d89bf
3e0117c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78d89bf
 
 
 
 
 
346dab1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78d89bf
3e0117c
 
 
 
 
 
 
 
346dab1
 
 
 
 
 
 
3e0117c
 
 
 
 
 
346dab1
3e0117c
 
 
 
 
 
 
346dab1
 
3e0117c
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
from typing import TypedDict, Annotated, Sequence
from langchain_core.messages import BaseMessage
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from neo4j_utils import unified_search
from typing import TypedDict, Sequence, List, Dict, Optional, Annotated
from config import llm
import re
from evaluations.correctness import correctness

class GraphState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], add_messages]
    query: str
    relevant_docs: List[Dict[str, Optional[Dict[str, float]]]]  # Résultats de la recherche hybride
    neo4j_results: list  # Résultats de la recherche Neo4j
    response: str
    score: Optional[float] 
    evaluation_explanation: Optional[str]
    k: int 
    alpha: float
    similarity_threshold: float


def retrieve_unified(state: GraphState) -> dict:
    """Récupération unifiée : recherche hybride (Pinecone + BM25) + Neo4j."""
    # Appeler la fonction unified_search
    unified_results = unified_search(state["query"],
        alpha=state.get("alpha"), 
        k=state.get("k"), 
        similarity_threshold=state.get("similarity_threshold"))
    
    # Séparer les résultats hybrides et Neo4j
    hybrid_results = [result for result in unified_results if result["type"] == "text"]
    neo4j_results = [result for result in unified_results if result["type"] == "entity"]
    
    return {
        "relevant_docs": hybrid_results,
        "neo4j_results": neo4j_results
    }



def generate_response(state: GraphState) -> dict:
    """Génération de réponse en combinant informations sémantiques, mots-clés et données Neo4j."""
    context = "\n\n".join([doc["content"] for doc in state["relevant_docs"] if doc["type"] == "text"])
    
    neo4j_context = "\n\n".join([
        str(record["content"]) for record in state["neo4j_results"]
        if not any(keyword in str(record["content"]) for keyword in ["MATCH", "RETURN", "CREATE", "MERGE", "WHERE", "SET"])
    ])
    
    prompt = f"""
    Vous êtes un expert en analyse de texte et en données structurées.
    Votre tâche consiste à répondre à la question de l'utilisateur en utilisant les informations pertinentes fournies. 
    Intégrez à la fois les éléments sémantiques (contexte, sens), les mots-clés exacts et les données structurées pour fournir une réponse fluide et cohérente.
    
    **Instructions supplémentaires** :
    - Utilisez les mots-clés pertinents de manière naturelle dans votre réponse.
    - Expliquez les concepts en vous appuyant sur le contexte sémantique.
    - Intégrez les données structurées (entités, relations) de manière claire et concise.
    - Évitez de générer des reponses redondantes, faites une fusion entre les élements sémantiques et les elements structurées.
    - Ne mentionnez pas explicitement les termes "recherche sémantique", "recherche par mots-clés" ou "base de données Neo4j".
    - **Ne fournissez pas la requête Cypher dans la réponse.**
    
    **Informations pertinentes trouvées (texte)** :
    {context}
    
    **Informations pertinentes trouvées (données structurées)** :
    {neo4j_context}
    
    **Question de l'utilisateur** :
    {state["query"]}
    
    **Réponse :**
    [Fournissez une réponse cohérente qui intègre à la fois les éléments sémantiques, les mots-clés pertinents et les données structurées sans les distinguer explicitement.]
    """
    
    response = llm.invoke(prompt)
    
    # **Nettoyage avancé : suppression des requêtes Cypher dans la réponse**
    response_cleaned = re.sub(r"(MATCH|RETURN|CREATE|MERGE|WHERE|SET)[\s\S]*?[.;]", "", response.content, flags=re.IGNORECASE).strip()
    
    return {"response": response_cleaned}


def evaluate_response(state: GraphState) -> dict:
    """Évalue la réponse générée en comparant avec la vérité terrain (LangSmith)."""
    
    inputs = {"question": state["query"]}
    outputs = {"answer": state["response"]}
    
    try:
        score = correctness(inputs, outputs)
        explanation = f"La réponse a obtenu un score de {score}/10. Voici l'explication de l'évaluation..."
    except Exception as e:
        score = 0
        explanation = f"Erreur lors de l'évaluation : {str(e)}"
    
    return {"score": score, "evaluation_explanation": explanation}

    

def post_process_response(state: GraphState) -> dict:
    """Nettoie et valide la réponse."""
    response = state["response"].strip()
    
    # Vérifier si la réponse est pertinente
    if not response or response.lower() in ["je ne sais pas", "i don't know"]:
        response = "Désolé, je n'ai pas trouvé d'informations pertinentes pour votre question."

    evaluation = evaluate_response(state)
    return {
        "response": response,
        "score": evaluation["score"],  # Ajout du score
        "evaluation_explanation": evaluation["evaluation_explanation"]  # Explication
    }


# Construction du graphe
graph_builder = StateGraph(GraphState)

# Ajouter les nœuds
graph_builder.add_node("evaluate", evaluate_response)
graph_builder.add_node("retrieve", retrieve_unified) 
graph_builder.add_node("generate", generate_response)
graph_builder.add_node("post_process", post_process_response)

# Définir les transitions
graph_builder.set_entry_point("retrieve")
graph_builder.add_edge("retrieve", "generate")
graph_builder.add_edge("generate", "evaluate")
graph_builder.add_edge("evaluate", "post_process")
graph_builder.add_edge("post_process", END)

# Compiler le graphe
agent = graph_builder.compile()