datacipen commited on
Commit
554a256
·
verified ·
1 Parent(s): ad3f5d7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +461 -450
app.py CHANGED
@@ -1,451 +1,462 @@
1
- """
2
- Application Chainlit pour l'Agent Collaboratif LangGraph
3
- ========================================================
4
-
5
- Intégration complète avec:
6
- - Chainlit 2.8.1
7
- - Official Data Layer (PostgreSQL/Supabase)
8
- - LangSmith monitoring
9
- - Starters avec icônes
10
- - Chain of Thought visible
11
- - Style personnalisé (dark theme)
12
- """
13
-
14
- import os
15
- import json
16
- import asyncio
17
- from typing import Dict, Any, List, Optional
18
-
19
- import chainlit as cl
20
- from chainlit.types import ThreadDict, Starter
21
- #from langsmith import Client
22
- #from langsmith.run_helpers import traceable
23
- from langsmith import traceable
24
-
25
- # Import du module agent (votre code existant)
26
- # On suppose que le code est dans agent_collaboratif_avid.py
27
- from agent_collaboratif_avid import (
28
- run_collaborative_agent,
29
- retriever_manager,
30
- PINECONE_INDEX_NAME,
31
- OPENAI_MODEL_NAME,
32
- SIMILARITY_TOP_K,
33
- MAX_VALIDATION_LOOPS
34
- )
35
-
36
- # =============================================================================
37
- # CONFIGURATION LANGSMITH
38
- # =============================================================================
39
-
40
- LANGCHAIN_API_KEY = os.environ.get("LANGCHAIN_API_KEY")
41
- LANGSMITH_PROJECT = os.environ.get("LANGSMITH_PROJECT")
42
-
43
- if LANGCHAIN_API_KEY:
44
- os.environ["LANGCHAIN_TRACING_V2"] = "true"
45
- #os.environ["LANGCHAIN_ENDPOINT"] = "https://api.smith.langchain.com"
46
- os.environ["LANGCHAIN_API_KEY"] = LANGCHAIN_API_KEY
47
- os.environ["LANGCHAIN_PROJECT"] = LANGSMITH_PROJECT
48
- #langsmith_client = Client()
49
- print(f"✅ LangSmith activé - Projet: {LANGSMITH_PROJECT}")
50
- else:
51
- print("⚠️ LANGCHAIN_API_KEY non définie - Monitoring désactivé")
52
- langsmith_client = None
53
-
54
-
55
- # =============================================================================
56
- # FONCTIONS AUXILIAIRES POUR L'AFFICHAGE
57
- # =============================================================================
58
-
59
- async def send_cot_step(step_name: str, content: str, status: str = "running"):
60
- """Envoie une étape du Chain of Thought."""
61
- step = cl.Step(
62
- name=step_name,
63
- type="tool",
64
- show_input=True
65
- )
66
- step.output = content
67
-
68
- if status == "done":
69
- step.is_error = False
70
- elif status == "error":
71
- step.is_error = True
72
-
73
- await step.send()
74
- return step
75
-
76
- async def display_query_analysis(analysis: Dict[str, Any]):
77
- """Affiche l'analyse de la requête."""
78
- content = f"""**Bases identifiées:** {', '.join(analysis.get('databases_to_query', []))}
79
-
80
- **Priorités:**
81
- {json.dumps(analysis.get('priorities', {}), indent=2, ensure_ascii=False)}
82
-
83
- **Résumé:** {analysis.get('analysis_summary', 'N/A')}
84
- """
85
- await send_cot_step("🔍 Analyse de la requête", content, "done")
86
-
87
- async def display_collection(info_list: List[Dict[str, Any]]):
88
- """Affiche les informations collectées."""
89
- content_parts = []
90
-
91
- for info in info_list:
92
- content_parts.append(f"""
93
- **📦 Base:** {info['database']}
94
- **Catégorie:** {info['category']}
95
- **Priorité:** {info['priority']}
96
- **Résultats:** {info['results_count']}
97
- """)
98
-
99
- content = "\n".join(content_parts)
100
- await send_cot_step("📊 Collecte d'informations", content, "done")
101
-
102
- async def display_validation(validation: Dict[str, Any], iteration: int):
103
- """Affiche les résultats de validation."""
104
- content = f"""**Itération:** {iteration}
105
- **Score de confiance:** {validation.get('confidence_score', 0)}%
106
- **Validé:** {'✅ Oui' if validation.get('is_valid') else '❌ Non'}
107
-
108
- **Hallucinations détectées:** {len(validation.get('hallucinations_detected', []))}
109
- """
110
-
111
- if validation.get('hallucinations_detected'):
112
- content += "\n**Problèmes:**\n"
113
- for hall in validation['hallucinations_detected']:
114
- content += f"- {hall}\n"
115
-
116
- status = "done" if validation.get('is_valid') else "error"
117
- await send_cot_step(f"Validation (#{iteration})", content, status)
118
-
119
- async def display_similar_info(similar_info: List[Dict[str, Any]]):
120
- """Affiche les informations similaires."""
121
- if not similar_info:
122
- return
123
-
124
- # Regrouper par base
125
- grouped = {}
126
- for item in similar_info:
127
- db = item['database']
128
- if db not in grouped:
129
- grouped[db] = []
130
- grouped[db].append(item)
131
-
132
- elements = []
133
-
134
- for db_name, items in grouped.items():
135
- content_parts = [f"### 📚 {db_name.upper()}\n"]
136
- content_parts.append(f"**Catégorie:** {items[0]['category']}")
137
- content_parts.append(f"**Résultats:** {len(items)}\n")
138
-
139
- for idx, item in enumerate(items[:3], 1): # Limiter à 3 par base
140
- score = item.get('score', 'N/A')
141
- content_parts.append(f"**{idx}. Score:** {score}")
142
-
143
- content_preview = item['content'][:200]
144
- if len(item['content']) > 200:
145
- content_preview += "..."
146
- content_parts.append(f"**Contenu:** {content_preview}\n")
147
-
148
- # Créer un élément Chainlit
149
- element = cl.Text(
150
- name=f"similar_{db_name}",
151
- content="\n".join(content_parts),
152
- display="side"
153
- )
154
- elements.append(element)
155
-
156
- if elements:
157
- await cl.Message(
158
- content="💡 **Informations similaires trouvées dans d'autres bases**",
159
- elements=elements
160
- ).send()
161
-
162
- # =============================================================================
163
- # FONCTION PRINCIPALE TRACÉE AVEC LANGSMITH
164
- # =============================================================================
165
-
166
- @traceable(name="agent_collaboratif_query", project_name=LANGSMITH_PROJECT)
167
- async def process_query_with_tracing(query: str, thread_id: str) -> Dict[str, Any]:
168
- """Traite la requête avec traçage LangSmith."""
169
-
170
- # Import du workflow
171
- from agent_collaboratif_avid import AgentState, create_agent_workflow
172
- from langchain_core.messages import HumanMessage
173
-
174
- app = create_agent_workflow()
175
-
176
- initial_state = {
177
- "messages": [HumanMessage(content=query)],
178
- "user_query": query,
179
- "query_analysis": {},
180
- "collected_information": [],
181
- "validation_results": [],
182
- "final_response": "",
183
- "iteration_count": 0,
184
- "errors": [],
185
- "additional_information": []
186
- }
187
-
188
- # Analyse de la requête
189
- await send_cot_step("🔄 Démarrage", "Initialisation du workflow...", "running")
190
-
191
- final_state = await app.ainvoke(initial_state)
192
-
193
-
194
- # Affichage progressif
195
- if final_state.get("query_analysis"):
196
- await display_query_analysis(final_state["query_analysis"])
197
-
198
- if final_state.get("collected_information"):
199
- await send_cot_step("📊 Collecte d'informations", "Collecte d'informations...", "running")
200
- await display_collection(final_state["collected_information"])
201
-
202
- if final_state.get("validation_results"):
203
- for idx, validation in enumerate(final_state["validation_results"], 1):
204
- await display_validation(validation, idx)
205
-
206
- # Informations similaires
207
- if final_state.get("additional_information"):
208
- await display_similar_info(final_state["additional_information"])
209
-
210
- result = {
211
- "query": query,
212
- "query_analysis": final_state.get("query_analysis", {}),
213
- "collected_information": final_state.get("collected_information", []),
214
- "validation_results": final_state.get("validation_results", []),
215
- "final_response": final_state.get("final_response", ""),
216
- "iteration_count": final_state.get("iteration_count", 0),
217
- "errors": final_state.get("errors", []),
218
- "additional_information": final_state.get("additional_information", []),
219
- "sources_used": [
220
- info["database"]
221
- for info in final_state.get("collected_information", [])
222
- ],
223
- "pinecone_index": PINECONE_INDEX_NAME
224
- }
225
-
226
- return result
227
-
228
- # =============================================================================
229
- # CALLBACKS CHAINLIT
230
- # =============================================================================
231
-
232
- #@cl.on_chat_start
233
- #async def start():
234
- # """Initialisation de la session chat."""
235
-
236
- # Message de bienvenue avec style
237
- # welcome_msg = f"""# 🎓 Agent Collaboratif - Université Gustave Eiffel
238
-
239
- #Bienvenue ! Je suis votre assistant spécialisé en **Ville Durable**.
240
-
241
- ## 🔧 Configuration
242
- #- **Index Pinecone:** `{PINECONE_INDEX_NAME}`
243
- #- **Modèle:** `{OPENAI_MODEL_NAME}`
244
- #- **Top K résultats:** `{SIMILARITY_TOP_K}`
245
- #- **Max validations:** `{MAX_VALIDATION_LOOPS}`
246
-
247
- ## 💡 Fonctionnalités
248
- #✅ Recherche multi-bases vectorielles
249
- #✅ Validation anti-hallucination
250
- #✅ Suggestions d'informations connexes
251
- #✅ Traçage LangSmith actif
252
-
253
- #**Choisissez un starter ou posez votre question !**
254
- #"""
255
-
256
- # await cl.Message(content=welcome_msg).send()
257
-
258
- # Sauvegarder les métadonnées de session
259
- # cl.user_session.set("session_started", True)
260
- # cl.user_session.set("query_count", 0)
261
-
262
- @cl.set_starters
263
- async def set_starters():
264
- """Configure les starters avec icônes."""
265
- #return [cl.Starter(label=s["label"], message=s["message"], icon=s["icon"]) for s in STARTERS]
266
- return [
267
- cl.Starter(
268
- label= "🔬 Laboratoires & Mobilité",
269
- message= "Quels sont les laboratoires de l'université Gustave Eiffel travaillant sur la mobilité urbaine durable?",
270
- #icon= "/public/icons/lab.svg"
271
- ),
272
- cl.Starter(
273
- label= "🎓 Formations Master",
274
- message= "Je cherche des formations en master sur l'aménagement urbain et le développement durable",
275
- #icon= "/public/icons/education.svg"
276
- ),
277
- cl.Starter(
278
- label= "🤝 Collaborations Recherche",
279
- message= "Quels laboratoires ont des axes de recherche similaires en énergie et pourraient collaborer?",
280
- #icon= "/public/icons/collaboration.svg"
281
- ),
282
- cl.Starter(
283
- label= "⚙️ Équipements Lab",
284
- message= "Liste les équipements disponibles dans les laboratoires travaillant sur la qualité de l'air",
285
- #icon= "/public/icons/equipment.svg"
286
- ),
287
- cl.Starter(
288
- label= "📚 Publications Récentes",
289
- message= "Trouve des publications récentes sur la transition énergétique dans les villes",
290
- #icon= "/public/icons/publications.svg"
291
- ),
292
- cl.Starter(
293
- label= "👥 Auteurs & Labs",
294
- message= "Qui sont les auteurs qui publient sur la mobilité douce et dans quels laboratoires?",
295
- #icon= "/public/icons/authors.svg"
296
- ),
297
- cl.Starter(
298
- label= "📖 Urbanisme Durable",
299
- message= "Quelles publications traitent de l'urbanisme durable et quand ont-elles été publiées?",
300
- #icon= "/public/icons/urban.svg"
301
- ),
302
- cl.Starter(
303
- label= "🏙️ Ville Intelligente",
304
- message= "Compare les formations et les laboratoires sur le thème de la ville intelligente",
305
- #icon= "/public/icons/smart-city.svg"
306
- ),
307
- cl.Starter(
308
- label= "🌍 Résilience Urbaine",
309
- message= "Identifie les opportunités de partenariats entre laboratoires sur la résilience urbaine",
310
- #icon= "/public/icons/resilience.svg"
311
- ),
312
- cl.Starter(
313
- label= "♻️ Économie Circulaire",
314
- message= "Quelles sont les compétences enseignées dans les formations liées à l'économie circulaire?",
315
- #icon= "/public/icons/circular.svg"
316
- )
317
- ]
318
-
319
- @cl.on_message
320
- async def main(message: cl.Message):
321
- """Traitement du message utilisateur."""
322
-
323
- query = message.content
324
- thread_id = cl.context.session.thread_id
325
-
326
- # Incrémenter le compteur
327
- query_count = cl.user_session.get("query_count", 0) + 1
328
- cl.user_session.set("query_count", query_count)
329
-
330
- # Message de traitement
331
- processing_msg = cl.Message(content="")
332
- await processing_msg.send()
333
-
334
- try:
335
- # Traitement avec affichage du COT
336
- result = await process_query_with_tracing(query, thread_id)
337
-
338
- # Réponse finale
339
- final_response = result["final_response"]
340
-
341
- # Métadonnées
342
- metadata_parts = [
343
- f"\n\n---\n### 📊 Métadonnées du traitement",
344
- f"**Sources consultées:** {', '.join(result['sources_used']) if result['sources_used'] else 'Aucune'}",
345
- f"**Itérations:** {result['iteration_count']}",
346
- ]
347
-
348
- if result['validation_results']:
349
- last_val = result['validation_results'][-1]
350
- metadata_parts.append(f"**Confiance finale:** {last_val.get('confidence_score', 0)}%")
351
-
352
- metadata_parts.append(f"**Requête n°:** {query_count}")
353
-
354
- full_response = final_response + "\n".join(metadata_parts)
355
-
356
- # Mise à jour du message
357
- processing_msg.content = full_response
358
- await processing_msg.update()
359
-
360
- # Sauvegarder dans l'historique de session
361
- cl.user_session.set(f"query_{query_count}", {
362
- "query": query,
363
- "response": final_response,
364
- "sources": result['sources_used']
365
- })
366
-
367
- except Exception as e:
368
- error_msg = f"❌ **Erreur lors du traitement:**\n\n```\n{str(e)}\n```"
369
- processing_msg.content = error_msg
370
- await processing_msg.update()
371
-
372
- # Log dans LangSmith si disponible
373
- #if langsmith_client:
374
- # langsmith_client.create_feedback(
375
- # run_id=thread_id,
376
- # key="error",
377
- # score=0,
378
- # comment=str(e)
379
- # )
380
-
381
- @cl.on_chat_resume
382
- async def on_chat_resume(thread: ThreadDict):
383
- """Reprise d'une conversation existante."""
384
-
385
- thread_id = thread["id"]
386
-
387
- resume_msg = f"""# 🔄 Conversation reprise
388
-
389
- **Thread ID:** `{thread_id}`
390
-
391
- Vous pouvez continuer votre conversation ou poser une nouvelle question.
392
- """
393
-
394
- await cl.Message(content=resume_msg).send()
395
-
396
- @cl.on_stop
397
- async def on_stop():
398
- """Callback à l'arrêt de l'exécution."""
399
- await cl.Message(content="⏹️ Traitement interrompu par l'utilisateur.").send()
400
-
401
- @cl.on_chat_end
402
- async def on_chat_end():
403
- """Callback à la fin de la session."""
404
- query_count = cl.user_session.get("query_count", 0)
405
-
406
- end_msg = f"""# 👋 Session terminée
407
-
408
- Merci d'avoir utilisé l'agent collaboratif !
409
-
410
- **Statistiques de session:**
411
- - **Requêtes traitées:** {query_count}
412
- - **Index Pinecone:** {PINECONE_INDEX_NAME}
413
- """
414
-
415
- await cl.Message(content=end_msg).send()
416
-
417
- # =============================================================================
418
- # CONFIGURATION DE L'AUTHENTIFICATION (Optionnel)
419
- # =============================================================================
420
-
421
- @cl.password_auth_callback
422
- def auth_callback(username: str, password: str) -> Optional[cl.User]:
423
- """
424
- Callback d'authentification (optionnel).
425
- À configurer selon vos besoins.
426
- """
427
- # Exemple simple (à remplacer par votre logique)
428
- if username == "admin" and password == "password":
429
- return cl.User(
430
- identifier=username,
431
- metadata={"role": "admin", "provider": "credentials"}
432
- )
433
- return None
434
-
435
- # =============================================================================
436
- # CONFIGURATION DU DATA LAYER (Supabase/PostgreSQL)
437
- # =============================================================================
438
-
439
- """
440
- Pour activer le Data Layer avec Supabase, créez un fichier .env:
441
-
442
- CHAINLIT_AUTH_SECRET=your-secret-key
443
- LITERAL_API_KEY=your-literal-api-key
444
- LITERAL_API_URL=https://cloud.getliteral.ai
445
-
446
- Ou configurez PostgreSQL directement:
447
-
448
- DATABASE_URL=postgresql://user:password@host:port/dbname
449
-
450
- Le Data Layer sera automatiquement activé si ces variables sont définies.
 
 
 
 
 
 
 
 
 
 
 
451
  """
 
1
+ """
2
+ Application Chainlit pour l'Agent Collaboratif LangGraph
3
+ ========================================================
4
+
5
+ Intégration complète avec:
6
+ - Chainlit 2.8.1
7
+ - Official Data Layer (PostgreSQL/Supabase)
8
+ - LangSmith monitoring
9
+ - Starters avec icônes
10
+ - Chain of Thought visible
11
+ - Style personnalisé (dark theme)
12
+ """
13
+
14
+ import os
15
+ import json
16
+ import asyncio
17
+ from typing import Dict, Any, List, Optional
18
+
19
+ import chainlit as cl
20
+ from chainlit.types import ThreadDict, Starter
21
+ #from langsmith import Client
22
+ #from langsmith.run_helpers import traceable
23
+ from langsmith import traceable
24
+
25
+ # Import du module agent (votre code existant)
26
+ # On suppose que le code est dans agent_collaboratif_avid.py
27
+ from agent_collaboratif_avid import (
28
+ run_collaborative_agent,
29
+ retriever_manager,
30
+ PINECONE_INDEX_NAME,
31
+ OPENAI_MODEL_NAME,
32
+ SIMILARITY_TOP_K,
33
+ MAX_VALIDATION_LOOPS
34
+ )
35
+ from chainlit.data.sql_alchemy import SQLAlchemyDataLayer
36
+ from supabase import Client
37
+
38
+ SUPABASE_URL = os.environ.get("SUPABASE_URL")
39
+ SUPABASE_ANON_KEY = os.environ.get("SUPABASE_ANON_KEY")
40
+ CONNIFO = os.environ.get("CONNIFO")
41
+
42
+ storage_client = Client(SUPABASE_URL, SUPABASE_ANON_KEY)
43
+ @cl.data_layer
44
+ def get_data_layer():
45
+ return SQLAlchemyDataLayer(conninfo=CONNIFO, storage_provider=storage_client)
46
+
47
+ # =============================================================================
48
+ # CONFIGURATION LANGSMITH
49
+ # =============================================================================
50
+
51
+ LANGCHAIN_API_KEY = os.environ.get("LANGCHAIN_API_KEY")
52
+ LANGSMITH_PROJECT = os.environ.get("LANGSMITH_PROJECT")
53
+
54
+ if LANGCHAIN_API_KEY:
55
+ os.environ["LANGCHAIN_TRACING_V2"] = "true"
56
+ #os.environ["LANGCHAIN_ENDPOINT"] = "https://api.smith.langchain.com"
57
+ os.environ["LANGCHAIN_API_KEY"] = LANGCHAIN_API_KEY
58
+ os.environ["LANGCHAIN_PROJECT"] = LANGSMITH_PROJECT
59
+ #langsmith_client = Client()
60
+ print(f" LangSmith activé - Projet: {LANGSMITH_PROJECT}")
61
+ else:
62
+ print("⚠️ LANGCHAIN_API_KEY non définie - Monitoring désactivé")
63
+ langsmith_client = None
64
+
65
+
66
+ # =============================================================================
67
+ # FONCTIONS AUXILIAIRES POUR L'AFFICHAGE
68
+ # =============================================================================
69
+
70
+ async def send_cot_step(step_name: str, content: str, status: str = "running"):
71
+ """Envoie une étape du Chain of Thought."""
72
+ step = cl.Step(
73
+ name=step_name,
74
+ type="tool",
75
+ show_input=True
76
+ )
77
+ step.output = content
78
+
79
+ if status == "done":
80
+ step.is_error = False
81
+ elif status == "error":
82
+ step.is_error = True
83
+
84
+ await step.send()
85
+ return step
86
+
87
+ async def display_query_analysis(analysis: Dict[str, Any]):
88
+ """Affiche l'analyse de la requête."""
89
+ content = f"""**Bases identifiées:** {', '.join(analysis.get('databases_to_query', []))}
90
+
91
+ **Priorités:**
92
+ {json.dumps(analysis.get('priorities', {}), indent=2, ensure_ascii=False)}
93
+
94
+ **Résumé:** {analysis.get('analysis_summary', 'N/A')}
95
+ """
96
+ await send_cot_step("🔍 Analyse de la requête", content, "done")
97
+
98
+ async def display_collection(info_list: List[Dict[str, Any]]):
99
+ """Affiche les informations collectées."""
100
+ content_parts = []
101
+
102
+ for info in info_list:
103
+ content_parts.append(f"""
104
+ **📦 Base:** {info['database']}
105
+ **Catégorie:** {info['category']}
106
+ **Priorité:** {info['priority']}
107
+ **Résultats:** {info['results_count']}
108
+ """)
109
+
110
+ content = "\n".join(content_parts)
111
+ await send_cot_step("📊 Collecte d'informations", content, "done")
112
+
113
+ async def display_validation(validation: Dict[str, Any], iteration: int):
114
+ """Affiche les résultats de validation."""
115
+ content = f"""**Itération:** {iteration}
116
+ **Score de confiance:** {validation.get('confidence_score', 0)}%
117
+ **Validé:** {'Oui' if validation.get('is_valid') else '❌ Non'}
118
+
119
+ **Hallucinations détectées:** {len(validation.get('hallucinations_detected', []))}
120
+ """
121
+
122
+ if validation.get('hallucinations_detected'):
123
+ content += "\n**Problèmes:**\n"
124
+ for hall in validation['hallucinations_detected']:
125
+ content += f"- {hall}\n"
126
+
127
+ status = "done" if validation.get('is_valid') else "error"
128
+ await send_cot_step(f"✅ Validation (#{iteration})", content, status)
129
+
130
+ async def display_similar_info(similar_info: List[Dict[str, Any]]):
131
+ """Affiche les informations similaires."""
132
+ if not similar_info:
133
+ return
134
+
135
+ # Regrouper par base
136
+ grouped = {}
137
+ for item in similar_info:
138
+ db = item['database']
139
+ if db not in grouped:
140
+ grouped[db] = []
141
+ grouped[db].append(item)
142
+
143
+ elements = []
144
+
145
+ for db_name, items in grouped.items():
146
+ content_parts = [f"### 📚 {db_name.upper()}\n"]
147
+ content_parts.append(f"**Catégorie:** {items[0]['category']}")
148
+ content_parts.append(f"**Résultats:** {len(items)}\n")
149
+
150
+ for idx, item in enumerate(items[:3], 1): # Limiter à 3 par base
151
+ score = item.get('score', 'N/A')
152
+ content_parts.append(f"**{idx}. Score:** {score}")
153
+
154
+ content_preview = item['content'][:200]
155
+ if len(item['content']) > 200:
156
+ content_preview += "..."
157
+ content_parts.append(f"**Contenu:** {content_preview}\n")
158
+
159
+ # Créer un élément Chainlit
160
+ element = cl.Text(
161
+ name=f"similar_{db_name}",
162
+ content="\n".join(content_parts),
163
+ display="side"
164
+ )
165
+ elements.append(element)
166
+
167
+ if elements:
168
+ await cl.Message(
169
+ content="💡 **Informations similaires trouvées dans d'autres bases**",
170
+ elements=elements
171
+ ).send()
172
+
173
+ # =============================================================================
174
+ # FONCTION PRINCIPALE TRACÉE AVEC LANGSMITH
175
+ # =============================================================================
176
+
177
+ @traceable(name="agent_collaboratif_query", project_name=LANGSMITH_PROJECT)
178
+ async def process_query_with_tracing(query: str, thread_id: str) -> Dict[str, Any]:
179
+ """Traite la requête avec traçage LangSmith."""
180
+
181
+ # Import du workflow
182
+ from agent_collaboratif_avid import AgentState, create_agent_workflow
183
+ from langchain_core.messages import HumanMessage
184
+
185
+ app = create_agent_workflow()
186
+
187
+ initial_state = {
188
+ "messages": [HumanMessage(content=query)],
189
+ "user_query": query,
190
+ "query_analysis": {},
191
+ "collected_information": [],
192
+ "validation_results": [],
193
+ "final_response": "",
194
+ "iteration_count": 0,
195
+ "errors": [],
196
+ "additional_information": []
197
+ }
198
+
199
+ # Analyse de la requête
200
+ await send_cot_step("🔄 Démarrage", "Initialisation du workflow...", "running")
201
+
202
+ final_state = await app.ainvoke(initial_state)
203
+
204
+
205
+ # Affichage progressif
206
+ if final_state.get("query_analysis"):
207
+ await display_query_analysis(final_state["query_analysis"])
208
+
209
+ if final_state.get("collected_information"):
210
+ await send_cot_step("📊 Collecte d'informations", "Collecte d'informations...", "running")
211
+ await display_collection(final_state["collected_information"])
212
+
213
+ if final_state.get("validation_results"):
214
+ for idx, validation in enumerate(final_state["validation_results"], 1):
215
+ await display_validation(validation, idx)
216
+
217
+ # Informations similaires
218
+ if final_state.get("additional_information"):
219
+ await display_similar_info(final_state["additional_information"])
220
+
221
+ result = {
222
+ "query": query,
223
+ "query_analysis": final_state.get("query_analysis", {}),
224
+ "collected_information": final_state.get("collected_information", []),
225
+ "validation_results": final_state.get("validation_results", []),
226
+ "final_response": final_state.get("final_response", ""),
227
+ "iteration_count": final_state.get("iteration_count", 0),
228
+ "errors": final_state.get("errors", []),
229
+ "additional_information": final_state.get("additional_information", []),
230
+ "sources_used": [
231
+ info["database"]
232
+ for info in final_state.get("collected_information", [])
233
+ ],
234
+ "pinecone_index": PINECONE_INDEX_NAME
235
+ }
236
+
237
+ return result
238
+
239
+ # =============================================================================
240
+ # CALLBACKS CHAINLIT
241
+ # =============================================================================
242
+
243
+ #@cl.on_chat_start
244
+ #async def start():
245
+ # """Initialisation de la session chat."""
246
+
247
+ # Message de bienvenue avec style
248
+ # welcome_msg = f"""# 🎓 Agent Collaboratif - Université Gustave Eiffel
249
+
250
+ #Bienvenue ! Je suis votre assistant spécialisé en **Ville Durable**.
251
+
252
+ ## 🔧 Configuration
253
+ #- **Index Pinecone:** `{PINECONE_INDEX_NAME}`
254
+ #- **Modèle:** `{OPENAI_MODEL_NAME}`
255
+ #- **Top K résultats:** `{SIMILARITY_TOP_K}`
256
+ #- **Max validations:** `{MAX_VALIDATION_LOOPS}`
257
+
258
+ ## 💡 Fonctionnalités
259
+ #✅ Recherche multi-bases vectorielles
260
+ #✅ Validation anti-hallucination
261
+ #✅ Suggestions d'informations connexes
262
+ #✅ Traçage LangSmith actif
263
+
264
+ #**Choisissez un starter ou posez votre question !**
265
+ #"""
266
+
267
+ # await cl.Message(content=welcome_msg).send()
268
+
269
+ # Sauvegarder les métadonnées de session
270
+ # cl.user_session.set("session_started", True)
271
+ # cl.user_session.set("query_count", 0)
272
+
273
+ @cl.set_starters
274
+ async def set_starters():
275
+ """Configure les starters avec icônes."""
276
+ #return [cl.Starter(label=s["label"], message=s["message"], icon=s["icon"]) for s in STARTERS]
277
+ return [
278
+ cl.Starter(
279
+ label= "🔬 Laboratoires & Mobilité",
280
+ message= "Quels sont les laboratoires de l'université Gustave Eiffel travaillant sur la mobilité urbaine durable?",
281
+ #icon= "/public/icons/lab.svg"
282
+ ),
283
+ cl.Starter(
284
+ label= "🎓 Formations Master",
285
+ message= "Je cherche des formations en master sur l'aménagement urbain et le développement durable",
286
+ #icon= "/public/icons/education.svg"
287
+ ),
288
+ cl.Starter(
289
+ label= "🤝 Collaborations Recherche",
290
+ message= "Quels laboratoires ont des axes de recherche similaires en énergie et pourraient collaborer?",
291
+ #icon= "/public/icons/collaboration.svg"
292
+ ),
293
+ cl.Starter(
294
+ label= "⚙️ Équipements Lab",
295
+ message= "Liste les équipements disponibles dans les laboratoires travaillant sur la qualité de l'air",
296
+ #icon= "/public/icons/equipment.svg"
297
+ ),
298
+ cl.Starter(
299
+ label= "📚 Publications Récentes",
300
+ message= "Trouve des publications récentes sur la transition énergétique dans les villes",
301
+ #icon= "/public/icons/publications.svg"
302
+ ),
303
+ cl.Starter(
304
+ label= "👥 Auteurs & Labs",
305
+ message= "Qui sont les auteurs qui publient sur la mobilité douce et dans quels laboratoires?",
306
+ #icon= "/public/icons/authors.svg"
307
+ ),
308
+ cl.Starter(
309
+ label= "📖 Urbanisme Durable",
310
+ message= "Quelles publications traitent de l'urbanisme durable et quand ont-elles été publiées?",
311
+ #icon= "/public/icons/urban.svg"
312
+ ),
313
+ cl.Starter(
314
+ label= "🏙️ Ville Intelligente",
315
+ message= "Compare les formations et les laboratoires sur le thème de la ville intelligente",
316
+ #icon= "/public/icons/smart-city.svg"
317
+ ),
318
+ cl.Starter(
319
+ label= "🌍 Résilience Urbaine",
320
+ message= "Identifie les opportunités de partenariats entre laboratoires sur la résilience urbaine",
321
+ #icon= "/public/icons/resilience.svg"
322
+ ),
323
+ cl.Starter(
324
+ label= "♻️ Économie Circulaire",
325
+ message= "Quelles sont les compétences enseignées dans les formations liées à l'économie circulaire?",
326
+ #icon= "/public/icons/circular.svg"
327
+ )
328
+ ]
329
+
330
+ @cl.on_message
331
+ async def main(message: cl.Message):
332
+ """Traitement du message utilisateur."""
333
+
334
+ query = message.content
335
+ thread_id = cl.context.session.thread_id
336
+
337
+ # Incrémenter le compteur
338
+ query_count = cl.user_session.get("query_count", 0) + 1
339
+ cl.user_session.set("query_count", query_count)
340
+
341
+ # Message de traitement
342
+ processing_msg = cl.Message(content="")
343
+ await processing_msg.send()
344
+
345
+ try:
346
+ # Traitement avec affichage du COT
347
+ result = await process_query_with_tracing(query, thread_id)
348
+
349
+ # Réponse finale
350
+ final_response = result["final_response"]
351
+
352
+ # Métadonnées
353
+ metadata_parts = [
354
+ f"\n\n---\n### 📊 Métadonnées du traitement",
355
+ f"**Sources consultées:** {', '.join(result['sources_used']) if result['sources_used'] else 'Aucune'}",
356
+ f"**Itérations:** {result['iteration_count']}",
357
+ ]
358
+
359
+ if result['validation_results']:
360
+ last_val = result['validation_results'][-1]
361
+ metadata_parts.append(f"**Confiance finale:** {last_val.get('confidence_score', 0)}%")
362
+
363
+ metadata_parts.append(f"**Requête n°:** {query_count}")
364
+
365
+ full_response = final_response + "\n".join(metadata_parts)
366
+
367
+ # Mise à jour du message
368
+ processing_msg.content = full_response
369
+ await processing_msg.update()
370
+
371
+ # Sauvegarder dans l'historique de session
372
+ cl.user_session.set(f"query_{query_count}", {
373
+ "query": query,
374
+ "response": final_response,
375
+ "sources": result['sources_used']
376
+ })
377
+
378
+ except Exception as e:
379
+ error_msg = f"❌ **Erreur lors du traitement:**\n\n```\n{str(e)}\n```"
380
+ processing_msg.content = error_msg
381
+ await processing_msg.update()
382
+
383
+ # Log dans LangSmith si disponible
384
+ #if langsmith_client:
385
+ # langsmith_client.create_feedback(
386
+ # run_id=thread_id,
387
+ # key="error",
388
+ # score=0,
389
+ # comment=str(e)
390
+ # )
391
+
392
+ @cl.on_chat_resume
393
+ async def on_chat_resume(thread: ThreadDict):
394
+ """Reprise d'une conversation existante."""
395
+
396
+ thread_id = thread["id"]
397
+
398
+ resume_msg = f"""# 🔄 Conversation reprise
399
+
400
+ **Thread ID:** `{thread_id}`
401
+
402
+ Vous pouvez continuer votre conversation ou poser une nouvelle question.
403
+ """
404
+
405
+ await cl.Message(content=resume_msg).send()
406
+
407
+ @cl.on_stop
408
+ async def on_stop():
409
+ """Callback à l'arrêt de l'exécution."""
410
+ await cl.Message(content="⏹️ Traitement interrompu par l'utilisateur.").send()
411
+
412
+ @cl.on_chat_end
413
+ async def on_chat_end():
414
+ """Callback à la fin de la session."""
415
+ query_count = cl.user_session.get("query_count", 0)
416
+
417
+ end_msg = f"""# 👋 Session terminée
418
+
419
+ Merci d'avoir utilisé l'agent collaboratif !
420
+
421
+ **Statistiques de session:**
422
+ - **Requêtes traitées:** {query_count}
423
+ - **Index Pinecone:** {PINECONE_INDEX_NAME}
424
+ """
425
+
426
+ await cl.Message(content=end_msg).send()
427
+
428
+ # =============================================================================
429
+ # CONFIGURATION DE L'AUTHENTIFICATION (Optionnel)
430
+ # =============================================================================
431
+
432
+ @cl.password_auth_callback
433
+ def auth_callback(username: str, password: str) -> Optional[cl.User]:
434
+ """
435
+ Callback d'authentification (optionnel).
436
+ À configurer selon vos besoins.
437
+ """
438
+ # Exemple simple (à remplacer par votre logique)
439
+ if username == "admin" and password == "password":
440
+ return cl.User(
441
+ identifier=username,
442
+ metadata={"role": "admin", "provider": "credentials"}
443
+ )
444
+ return None
445
+
446
+ # =============================================================================
447
+ # CONFIGURATION DU DATA LAYER (Supabase/PostgreSQL)
448
+ # =============================================================================
449
+
450
+ """
451
+ Pour activer le Data Layer avec Supabase, créez un fichier .env:
452
+
453
+ CHAINLIT_AUTH_SECRET=your-secret-key
454
+ LITERAL_API_KEY=your-literal-api-key
455
+ LITERAL_API_URL=https://cloud.getliteral.ai
456
+
457
+ Ou configurez PostgreSQL directement:
458
+
459
+ DATABASE_URL=postgresql://user:password@host:port/dbname
460
+
461
+ Le Data Layer sera automatiquement activé si ces variables sont définies.
462
  """