datacipen commited on
Commit
050a20c
·
verified ·
1 Parent(s): 61cdf32

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +451 -0
app.py ADDED
@@ -0,0 +1,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
+
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
+ """