QuentinL52 commited on
Commit
99ba618
·
verified ·
1 Parent(s): fb3de3c

Update services/graph_service.py

Browse files
Files changed (1) hide show
  1. services/graph_service.py +121 -77
services/graph_service.py CHANGED
@@ -1,99 +1,143 @@
1
  import os
2
- from typing import TypedDict, Annotated, Sequence
 
 
 
3
  from langchain_openai import ChatOpenAI
4
- from langchain_core.messages import BaseMessage, AIMessage, HumanMessage
5
  from langchain.agents import create_openai_tools_agent, AgentExecutor
6
  from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
7
  from langgraph.graph import StateGraph, END
8
  from tools.analysis_tools import trigger_interview_analysis
9
 
10
- # --- 1. Définition de l'état du graphe ---
11
  class AgentState(TypedDict):
 
12
  user_id: str
13
  job_offer_id: str
14
- cv_document: dict
15
- job_offer: dict
16
- messages: Annotated[Sequence[BaseMessage], lambda x, y: x + y]
17
 
18
- # --- 2. Configuration de l'agent ---
19
- def create_agent_executor():
20
- # Le prompt instruit l'agent sur son rôle et quand utiliser l'outil
21
- prompt = ChatPromptTemplate.from_messages([
22
- ("system", """You are a friendly and professional HR recruiter named AIrh.
23
- Your role is to conduct a job interview based on the provided resume (cv_document) and the job description (job_offer).
24
- Engage with the user, ask relevant questions, and respond to their answers.
25
- When you feel the interview is complete, you MUST use the 'trigger_interview_analysis' tool to end the conversation.
26
- After calling the tool, say a final goodbye to the user."""),
27
- MessagesPlaceholder(variable_name="messages"),
28
- ])
29
-
30
- llm = ChatOpenAI(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4-turbo", temperature=0.7)
31
- tools = [trigger_interview_analysis]
32
- agent = create_openai_tools_agent(llm, tools, prompt)
33
- return AgentExecutor(agent=agent, tools=tools, verbose=True)
34
-
35
- # --- 3. Définition des nœuds du graphe ---
36
- agent_executor = create_agent_executor()
37
-
38
- def agent_node(state: AgentState):
39
- """Le nœud principal qui appelle l'agent pour répondre ou utiliser un outil."""
40
- response = agent_executor.invoke({
41
- "user_id": state["user_id"],
42
- "job_offer_id": state["job_offer_id"],
43
- "cv_document": state["cv_document"],
44
- "job_offer": state["job_offer"],
45
- "messages": state["messages"],
46
- "conversation_history": state["messages"] # Pour la compatibilité si l'outil en a besoin
47
- })
48
- return {"messages": [AIMessage(content=response["output"])]}
49
-
50
- # --- 4. Définition du routeur (Conditional Edge) ---
51
- def router(state: AgentState):
52
- """Décide du chemin à suivre après la réponse de l'agent."""
53
- last_message = state["messages"][-1]
54
- if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
55
- # Si l'agent a décidé d'appeler l'outil, on termine le graphe
56
- return "end"
57
- else:
58
- # Sinon, on continue la conversation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  return "continue"
60
 
61
- # --- 5. Construction du graphe ---
62
- def build_graph():
63
- graph = StateGraph(AgentState)
64
- graph.add_node("agent", agent_node)
65
- graph.add_conditional_edges(
66
- "agent",
67
- router,
68
- {"continue": "agent", "end": END}
69
- )
70
- graph.set_entry_point("agent")
71
- return graph.compile()
72
-
73
- # --- Service principal à appeler depuis l'API ---
74
- class GraphConversationManager:
75
- def __init__(self):
76
- self.graph = build_graph()
77
-
78
- def invoke(self, payload: dict):
79
- # Prépare les messages pour le format LangChain
80
- messages = [HumanMessage(content=m["content"]) if m["role"] == "user" else AIMessage(content=m["content"]) for m in payload["messages"]]
81
 
82
- state = {
83
- "user_id": payload["user_id"],
84
- "job_offer_id": payload["job_offer_id"],
85
- "cv_document": payload["cv_document"],
86
- "job_offer": payload["job_offer"],
87
- "messages": messages,
88
  }
89
 
90
- final_state = self.graph.invoke(state)
91
 
92
- # Détermine le statut final pour le front-end
93
  last_message = final_state['messages'][-1]
94
- status = "finished" if hasattr(last_message, 'tool_calls') and last_message.tool_calls else "interviewing"
95
 
96
  return {
97
  "response": last_message.content,
98
  "status": status
99
- }
 
1
  import os
2
+ import logging
3
+ import json
4
+ from typing import TypedDict, Annotated, Sequence, Dict, Any, List
5
+
6
  from langchain_openai import ChatOpenAI
7
+ from langchain_core.messages import BaseMessage, AIMessage, HumanMessage, SystemMessage
8
  from langchain.agents import create_openai_tools_agent, AgentExecutor
9
  from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
10
  from langgraph.graph import StateGraph, END
11
  from tools.analysis_tools import trigger_interview_analysis
12
 
 
13
  class AgentState(TypedDict):
14
+ messages: Annotated[Sequence[BaseMessage], lambda x, y: x + y]
15
  user_id: str
16
  job_offer_id: str
 
 
 
17
 
18
+ class GraphInterviewProcessor:
19
+ """
20
+ Cette classe encapsule la logique d'un entretien en utilisant LangGraph.
21
+ Elle prépare toutes les données nécessaires à l'initialisation, comme dans votre code original.
22
+ """
23
+ def __init__(self, payload: Dict[str, Any]):
24
+ logging.info("Initialisation de GraphInterviewProcessor...")
25
+
26
+ self.user_id = payload["user_id"]
27
+ self.job_offer_id = payload["job_offer_id"]
28
+ self.job_offer = payload["job_offer"]
29
+ self.cv_data = payload.get("cv_document", {}).get('candidat', {})
30
+
31
+ if not self.cv_data:
32
+ raise ValueError("Données du candidat non trouvées dans le payload.")
33
+
34
+ self.system_prompt_template = self._load_prompt_template('prompts/rag_prompt_old.txt')
35
+ self.formatted_cv_str = self._format_cv_for_prompt()
36
+ self.skills_summary = self._extract_skills_summary()
37
+ self.reconversion_info = self._extract_reconversion_info()
38
+
39
+ self.agent_executor = self._create_agent_executor()
40
+ self.graph = self._build_graph()
41
+ logging.info("GraphInterviewProcessor initialisé avec succès.")
42
+
43
+ def _load_prompt_template(self, file_path: str) -> str:
44
+ """Charge le template du prompt depuis un fichier."""
45
+ try:
46
+ with open(file_path, 'r', encoding='utf-8') as f:
47
+ return f.read()
48
+ except FileNotFoundError:
49
+ logging.error(f"Fichier prompt introuvable: {file_path}")
50
+ return "Vous êtes un assistant RH."
51
+
52
+ def _format_cv_for_prompt(self) -> str:
53
+ """Formate le CV pour l'injecter dans le prompt."""
54
+ return json.dumps(self.cv_data, indent=2, ensure_ascii=False)
55
+
56
+ def _extract_skills_summary(self) -> str:
57
+ """Extrait un résumé des compétences avec niveaux."""
58
+ competences = self.cv_data.get('analyse_competences', [])
59
+ if not competences:
60
+ return "Aucune analyse de compétences disponible."
61
+ summary = [f"{comp.get('skill', '')}: {comp.get('level', 'débutant')}" for comp in competences]
62
+ return "Niveaux de compétences du candidat: " + " | ".join(summary)
63
+
64
+ def _extract_reconversion_info(self) -> str:
65
+ """Extrait les informations de reconversion."""
66
+ reconversion = self.cv_data.get('reconversion', {})
67
+ if reconversion.get('is_reconversion'):
68
+ return f"CANDIDAT EN RECONVERSION: {reconversion.get('analysis', '')}"
69
+ return "Le candidat n'est pas identifié comme étant en reconversion."
70
+
71
+ def _create_agent_executor(self) -> AgentExecutor:
72
+ """Crée l'agent executor avec un prompt minimaliste."""
73
+ prompt = ChatPromptTemplate.from_messages([
74
+ SystemMessage(content="{system_prompt_content}"),
75
+ MessagesPlaceholder(variable_name="messages"),
76
+ MessagesPlaceholder(variable_name="agent_scratchpad"),
77
+ ])
78
+ llm = ChatOpenAI(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4o-mini", temperature=0.7)
79
+ tools = [trigger_interview_analysis]
80
+ agent = create_openai_tools_agent(llm, tools, prompt)
81
+ return AgentExecutor(agent=agent, tools=tools, verbose=True)
82
+
83
+ def _agent_node(self, state: AgentState):
84
+ """Prépare le prompt système dynamiquement et appelle l'agent."""
85
+ system_prompt_content = self.system_prompt_template.format(
86
+ entreprise=self.job_offer.get('entreprise', 'notre entreprise'),
87
+ poste=self.job_offer.get('poste', 'ce poste'),
88
+ mission=self.job_offer.get('mission', 'Non spécifiée'),
89
+ profil_recherche=self.job_offer.get('profil_recherche', 'Non spécifié'),
90
+ competences=self.job_offer.get('competences', 'Non spécifiées'),
91
+ pole=self.job_offer.get('pole', 'Non spécifié'),
92
+ cv=self.formatted_cv_str,
93
+ skills_analysis=self.skills_summary,
94
+ reconversion_analysis=self.reconversion_info
95
+ )
96
+
97
+ response = self.agent_executor.invoke({
98
+ "system_prompt_content": system_prompt_content,
99
+ "messages": state["messages"],
100
+ "user_id": state["user_id"],
101
+ "job_offer_id": state["job_offer_id"],
102
+ "conversation_history": state["messages"]
103
+ })
104
+ return {"messages": [response['output']]}
105
+
106
+ def _router(self, state: AgentState):
107
+ """Décide du chemin à suivre après la réponse de l'agent."""
108
+ last_message = state["messages"][-1]
109
+ if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
110
+ return "end"
111
  return "continue"
112
 
113
+ def _build_graph(self) -> any:
114
+ """Construit et compile le graphe d'états."""
115
+ graph = StateGraph(AgentState)
116
+ graph.add_node("agent", self._agent_node)
117
+ graph.add_conditional_edges(
118
+ "agent",
119
+ self._router,
120
+ {"continue": "agent", "end": END}
121
+ )
122
+ graph.set_entry_point("agent")
123
+ return graph.compile()
124
+
125
+ def invoke(self, messages: List[Dict[str, Any]]):
126
+ """Point d'entrée pour lancer une conversation dans le graphe."""
127
+ langchain_messages = [HumanMessage(content=m["content"]) if m["role"] == "user" else AIMessage(content=m["content"]) for m in messages]
 
 
 
 
 
128
 
129
+ initial_state = {
130
+ "user_id": self.user_id,
131
+ "job_offer_id": self.job_offer_id,
132
+ "messages": langchain_messages,
 
 
133
  }
134
 
135
+ final_state = self.graph.invoke(initial_state)
136
 
 
137
  last_message = final_state['messages'][-1]
138
+ status = "finished" if self._router(final_state) == 'end' else "interviewing"
139
 
140
  return {
141
  "response": last_message.content,
142
  "status": status
143
+ }