KJ24 commited on
Commit
4449f3f
·
verified ·
1 Parent(s): 71af744

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +456 -270
app.py CHANGED
@@ -1,397 +1,518 @@
1
  """
2
- app.py v5.0 ADAPTÉ - FastAPI pour Chunking Sémantique Intelligent
3
 
4
- ADAPTATIONS MAJEURES v5.0:
5
- Compatible avec LlamaIndex moderne modulaire (llama-index-core)
6
- Import SmartChunkerPipeline adapté pour nouvelle architecture
7
- Méthodes corrigées pour nouvelle structure
8
- Health check v5.0 avec support modulaire
9
- ✅ Gestion erreurs améliorée pour compatibilité
10
- ✅ Optimisations HF Space gratuit renforcées
11
  ✅ Variables d'environnement sécurisées
 
 
12
  """
13
 
14
  import os
15
  import logging
16
  import time
 
 
 
17
  from fastapi import FastAPI, HTTPException, Request
18
  from fastapi.middleware.cors import CORSMiddleware
19
  from fastapi.responses import JSONResponse
20
  from pydantic import BaseModel, Field
21
  from typing import List, Dict, Any, Optional
22
- import asyncio
23
  from concurrent.futures import ThreadPoolExecutor
24
- import gc
25
 
26
  # Configuration logging optimisée
27
  logging.basicConfig(
28
  level=logging.INFO,
29
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
 
 
 
 
30
  )
31
  logger = logging.getLogger(__name__)
32
 
33
- # ✅ ADAPTATION: Import SmartChunkerPipeline v5.0 avec fallback
34
  try:
35
- from chunker_pipeline import SmartChunkerPipeline # Version moderne
36
  from schemas import ChunkRequest, ChunkResponse, ChunkMetadata
37
- logger.info("✅ Modules de chunking v5.0 importés avec succès (LlamaIndex moderne)")
38
  except ImportError as e:
39
- logger.error(f"❌ Erreur import modules chunking: {e}")
40
- # Fallback pour tests
41
- try:
42
- from pipeline import ChunkingPipeline as SmartChunkerPipeline
43
- from schemas import ChunkRequest, ChunkResponse, ChunkMetadata
44
- logger.warning("⚠️ Utilisation pipeline fallback - fonctionnalités limitées")
45
- except ImportError as e2:
46
- logger.error(f"❌ Erreur import fallback: {e2}")
47
- raise
48
 
49
- # ✅ Configuration sécurisée variables d'environnement HF Space
50
- os.environ["TOKENIZERS_PARALLELISM"] = "false"
51
- os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
52
- os.environ["TRANSFORMERS_VERBOSITY"] = "error"
53
- os.environ["HF_HOME"] = "/app/cache/huggingface"
54
- os.environ["TRANSFORMERS_CACHE"] = "/app/cache/transformers"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- # ✅ NOUVEAU: Configuration spécifique LlamaIndex moderne
57
- os.environ["LLAMA_INDEX_CACHE_DIR"] = "/app/cache/llamaindex"
58
 
59
- # Initialisation FastAPI avec optimisations v5.0
60
  app = FastAPI(
61
- title="Chunking Sémantique Intelligent API v5.0",
62
- description="API de découpage récursif hiérarchique avec parentalité - Powered by LlamaIndex Moderne + Chonkie",
63
- version="5.0.0",
 
 
 
 
 
 
 
 
 
 
 
64
  docs_url="/docs",
65
- redoc_url="/redoc"
 
 
 
 
 
66
  )
67
 
68
- # Configuration CORS étendue pour n8n et tests
69
  app.add_middleware(
70
  CORSMiddleware,
71
- allow_origins=["*"], # Pour n8n et tests
72
  allow_credentials=True,
73
- allow_methods=["GET", "POST", "OPTIONS"],
74
  allow_headers=["*"],
 
75
  )
76
 
77
- # ✅ Instance globale pipeline v5.0 (chargée une seule fois)
78
  pipeline = None
79
  executor = ThreadPoolExecutor(max_workers=1) # HF Space gratuit = 1 worker max
 
 
80
 
81
- # ✅ Middleware gestion erreurs globales
82
  @app.middleware("http")
83
- async def catch_exceptions_middleware(request: Request, call_next):
84
- """Middleware pour capturer et gérer les erreurs globales"""
 
 
 
 
 
 
85
  try:
86
  response = await call_next(request)
 
 
 
 
 
 
 
 
87
  return response
 
88
  except Exception as e:
89
- logger.error(f"❌ Erreur globale: {str(e)}")
 
 
90
  return JSONResponse(
91
  status_code=500,
92
  content={
93
  "error": "Erreur interne du serveur",
94
  "detail": str(e),
 
95
  "timestamp": time.time(),
96
- "version": "5.0.0"
 
97
  }
98
  )
99
 
 
100
  @app.on_event("startup")
101
  async def startup_event():
102
- """Initialisation pipeline v5.0 au démarrage avec LlamaIndex moderne"""
103
  global pipeline
 
104
  try:
105
- logger.info("🚀 Initialisation SmartChunkerPipeline v5.0 (LlamaIndex moderne)...")
 
 
 
 
 
 
 
 
 
106
 
107
- # ADAPTATION: SmartChunkerPipeline avec support modulaire
 
108
  pipeline = SmartChunkerPipeline()
109
  await pipeline.initialize()
110
 
111
- # Test santé initial avec méthodes adaptatives
112
- try:
113
- health = await pipeline.health_check_v4()
114
- logger.info(f"✅ Pipeline v5.0 initialisé - Status: {health['status']}")
115
- except AttributeError:
116
- # Fallback pour anciennes méthodes
117
- health = await pipeline.get_health_status() if hasattr(pipeline, 'get_health_status') else {"status": "unknown"}
118
- logger.info(f"✅ Pipeline v5.0 initialisé - Status: {health.get('status', 'initialized')}")
119
-
120
- # Log informations configuration avec fallback
121
- try:
122
- config_info = await pipeline.get_config_info_v4()
123
- logger.info(f"⚙️ LLM: {config_info.get('models', {}).get('llm_model', 'N/A')}")
124
- logger.info(f"🧬 Embedding: {config_info.get('models', {}).get('embedding_model', 'N/A')}")
125
- logger.info(f"🦛 Chonkie: {config_info.get('models', {}).get('chonkie_available', False)}")
126
- except AttributeError:
127
- logger.info("⚙️ Configuration détaillée non disponible (mode fallback)")
 
 
 
 
 
128
 
129
- logger.info("🔧 LlamaIndex moderne modulaire configuré")
130
 
131
  except Exception as e:
132
- logger.error(f"❌ Erreur initialisation pipeline v5.0: {e}")
 
133
  raise
134
 
135
  @app.on_event("shutdown")
136
  async def shutdown_event():
137
- """Nettoyage à l'arrêt optimisé v5.0"""
138
  global pipeline, executor
 
139
  try:
140
- logger.info("🛑 Arrêt du service - nettoyage en cours...")
141
 
 
142
  if pipeline:
143
- # Nettoyage adaptatif selon les méthodes disponibles
144
- if hasattr(pipeline, 'cleanup'):
145
- await pipeline.cleanup()
146
- elif hasattr(pipeline, '_cleanup_memory_v4'):
147
- await pipeline._cleanup_memory_v4()
148
 
 
149
  if executor:
150
- executor.shutdown(wait=True)
 
151
 
152
  # Nettoyage mémoire final
153
  gc.collect()
154
 
155
- logger.info("✅ Nettoyage v5.0 terminé")
 
 
 
 
 
 
 
156
 
157
  except Exception as e:
158
- logger.error(f"⚠️ Erreur lors du nettoyage: {e}")
 
 
159
 
160
- @app.get("/")
161
  async def root():
162
- """Point d'entrée racine avec informations service v5.0"""
 
 
163
  return {
164
- "service": "Chunking Sémantique Intelligent API",
165
- "version": "5.0.0",
166
- "status": "running",
167
- "architecture": "LlamaIndex Moderne Modulaire",
 
 
168
  "features": [
169
- "Chunking sémantique avec Chonkie",
170
- "LlamaIndex moderne (llama-index-core)",
171
- "Hiérarchie récursive intelligente",
172
- "Relations bidirectionnelles",
173
- "Export Obsidian [[Titre]], id",
174
- "Agents spécialisés IA",
175
- "100% gratuit (HuggingFace local)"
176
- ],
177
- "endpoints": [
178
- "GET / - Informations service",
179
- "GET /health - Vérification santé",
180
- "GET /config - Configuration système",
181
- "POST /chunk - Chunking principal",
182
- "POST /chunk-batch - Chunking par lots",
183
- "POST /test - Test de fonctionnement"
184
  ],
185
- "documentation": "/docs",
186
- "compatible_with": [
187
- "llama-index-core (moderne)",
188
- "llama-index-embeddings-huggingface",
189
- "chonkie >= 0.1.0"
190
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  }
192
 
193
- @app.get("/health")
194
  async def health_check():
195
- """Vérification santé complète v5.0 avec fallbacks adaptatifs"""
196
  try:
197
  if pipeline is None:
198
  return {
199
- "status": "error",
200
  "message": "Pipeline non initialisé",
201
- "version": "5.0.0",
202
- "timestamp": time.time()
 
 
203
  }
204
 
205
- # ✅ ADAPTATION: Health check avec fallbacks multiples
206
- health_result = None
207
- memory_info = None
208
 
209
- # Tentative méthode v4.0
210
- try:
211
- health_result = await pipeline.health_check_v4()
212
- except AttributeError:
213
- # Fallback méthode alternative
214
- try:
215
- health_result = await pipeline.get_health_status()
216
- except AttributeError:
217
- # Fallback basique
218
- health_result = {
219
- "status": "running",
220
- "checks": {"initialization": True},
221
- "version": "5.0.0"
222
- }
223
-
224
- # Tentative récupération mémoire
225
  try:
226
- memory_info = pipeline.get_memory_usage_v4()
227
- except AttributeError:
228
- try:
229
- memory_info = pipeline.get_memory_usage()
230
- except AttributeError:
231
- memory_info = {"status": "monitoring_unavailable"}
 
 
 
 
 
 
 
232
 
233
  return {
234
  **health_result,
 
235
  "memory_info": memory_info,
236
- "version": "5.0.0",
237
- "architecture": "LlamaIndex Moderne"
 
 
 
 
 
238
  }
239
 
240
  except Exception as e:
241
- logger.error(f"❌ Erreur health check v5.0: {e}")
242
  return {
243
- "status": "error",
244
  "message": f"Erreur health check: {str(e)}",
245
- "version": "5.0.0",
246
- "timestamp": time.time()
 
247
  }
248
 
249
- @app.get("/config")
250
  async def get_config():
251
- """ Informations configuration système v5.0 avec adaptation modulaire"""
252
  try:
253
  if pipeline is None:
254
  raise HTTPException(status_code=503, detail="Pipeline non initialisé")
255
 
256
- # ADAPTATION: Configuration avec fallbacks
257
- config_info = {}
258
 
259
- try:
260
- config_info = await pipeline.get_config_info_v4()
261
- except AttributeError:
262
- # Fallback configuration basique
263
- config_info = {
264
- "version": "5.0.0",
265
- "architecture": "LlamaIndex Moderne Modulaire",
266
- "models": {
267
- "llm_available": hasattr(pipeline, 'llm'),
268
- "embedding_available": hasattr(pipeline, 'embed_model'),
269
- "chonkie_available": hasattr(pipeline, 'chonkie_semantic')
270
- },
271
- "chunking_config": {
272
- "pipeline_type": "SmartChunkerPipeline",
273
- "initialized": pipeline._is_initialized if hasattr(pipeline, '_is_initialized') else True
274
- }
275
- }
276
 
277
  return {
278
  **config_info,
279
- "api_version": "5.0.0",
280
- "timestamp": time.time(),
281
- "llamaindex_architecture": "moderne_modulaire"
282
  }
283
 
284
  except Exception as e:
285
- logger.error(f"❌ Erreur récupération config v5.0: {e}")
286
  raise HTTPException(status_code=500, detail=f"Erreur config: {str(e)}")
287
 
288
- @app.post("/chunk", response_model=ChunkResponse)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  async def chunk_text(request: ChunkRequest):
290
  """
291
- Point d'entrée principal chunking sémantique intelligent v5.0
292
 
293
- Traitement récursif hiérarchique avec LlamaIndex moderne:
294
- - llama-index-core + llama-index-embeddings-huggingface
295
- - Chonkie SemanticChunker + RecursiveChunker
296
- - Relations bidirectionnelles complètes
297
  - Export Obsidian format [[Titre]], id
298
- - Génération base connaissance agents IA
 
 
299
  """
300
  if pipeline is None:
301
- raise HTTPException(status_code=503, detail="Pipeline non initialisé")
 
 
 
302
 
303
  start_time = time.time()
304
 
305
  try:
306
- logger.info(f"📝 Début chunking v5.0: {request.titre or 'Sans titre'}")
307
 
308
  # Validation entrées renforcée
309
  if not request.text or len(request.text.strip()) < 10:
310
  raise HTTPException(
311
  status_code=400,
312
- detail="Le texte doit contenir au moins 10 caractères"
313
  )
314
 
315
- if len(request.text) > 500000: # 500k caractères max pour HF Space gratuit
 
 
316
  raise HTTPException(
317
  status_code=400,
318
- detail="Texte trop long (max 500,000 caractères pour Space gratuit)"
319
  )
320
 
321
- # ADAPTATION: Appel méthode avec fallbacks
322
- result = None
323
-
324
- # Tentative méthode v4.0
325
- try:
326
- result = await pipeline.process_text(request)
327
- except AttributeError:
328
- # Fallback méthode alternative
329
- try:
330
- result = await pipeline.process_text_sync(request)
331
- except AttributeError:
332
- raise HTTPException(
333
- status_code=500,
334
- detail="Méthode de traitement non disponible dans cette version du pipeline"
335
- )
336
 
337
  processing_time = time.time() - start_time
338
- logger.info(f"✅ Chunking v5.0 terminé: {result.total_chunks} chunks en {processing_time:.2f}s")
 
 
 
 
 
339
 
340
  return result
341
 
342
  except HTTPException:
343
- # Re-lever les HTTPException sans modification
344
  raise
345
  except Exception as e:
346
- logger.error(f"❌ Erreur chunking v5.0: {str(e)}")
347
 
348
- # Nettoyage mémoire en cas d'erreur avec fallback
349
  try:
350
- if hasattr(pipeline, '_cleanup_memory_v4'):
351
- await pipeline._cleanup_memory_v4()
352
- elif hasattr(pipeline, '_cleanup_memory'):
353
- await pipeline._cleanup_memory()
354
  except:
355
  pass
356
 
357
- gc.collect()
358
-
359
  raise HTTPException(
360
  status_code=500,
361
- detail=f"Erreur traitement chunking v5.0: {str(e)}"
362
  )
363
 
364
- @app.post("/chunk-batch")
365
  async def chunk_batch(requests: List[ChunkRequest]):
366
- """✅ Traitement par lots optimisé v5.0 (limité HF Space gratuit)"""
 
 
 
 
 
 
367
 
368
  # Validation limite batch pour Space gratuit
369
  max_batch_size = 3
370
  if len(requests) > max_batch_size:
371
  raise HTTPException(
372
  status_code=400,
373
- detail=f"Maximum {max_batch_size} textes par lot sur HF Space gratuit"
374
  )
375
 
376
  if pipeline is None:
377
- raise HTTPException(status_code=503, detail="Pipeline non initialisé")
378
 
379
  start_time = time.time()
380
  results = []
381
 
382
  try:
383
- logger.info(f"📦 Début chunking batch v5.0: {len(requests)} textes")
384
 
385
  for idx, request in enumerate(requests):
386
  try:
387
- # Traitement individuel avec méthodes adaptatives
388
- result = None
389
-
390
- try:
391
- result = await pipeline.process_text(request)
392
- except AttributeError:
393
- result = await pipeline.process_text_sync(request)
394
 
 
395
  results.append({
396
  "success": True,
397
  "index": idx,
@@ -399,122 +520,187 @@ async def chunk_batch(requests: List[ChunkRequest]):
399
  "result": result
400
  })
401
 
 
 
 
 
402
  except Exception as e:
403
- logger.error(f"❌ Erreur chunking batch item {idx}: {e}")
404
  results.append({
405
  "success": False,
406
  "index": idx,
407
- "source_id": request.source_id,
408
  "error": str(e)
409
  })
410
 
411
  total_time = time.time() - start_time
412
  successful_results = [r for r in results if r["success"]]
413
 
414
- # Nettoyage mémoire après batch
415
  try:
416
- if hasattr(pipeline, '_cleanup_memory_v4'):
417
- await pipeline._cleanup_memory_v4()
418
  except:
419
  pass
420
 
421
- logger.info(f"✅ Batch v5.0 terminé: {len(successful_results)}/{len(requests)} succès en {total_time:.2f}s")
 
 
 
422
 
423
  return {
424
  "results": results,
425
- "total_processed": len(requests),
426
- "successful": len(successful_results),
427
- "failed": len(requests) - len(successful_results),
428
- "total_processing_time": total_time,
429
- "version": "5.0.0"
 
 
 
 
 
430
  }
431
 
432
  except Exception as e:
433
- logger.error(f"❌ Erreur chunking batch v5.0: {e}")
434
  gc.collect()
435
  raise HTTPException(
436
  status_code=500,
437
- detail=f"Erreur traitement batch v5.0: {str(e)}"
438
  )
439
 
440
- # ✅ Endpoint test adapté pour v5.0
441
- @app.post("/test")
442
  async def test_chunking():
443
- """Endpoint de test pour validation déploiement v5.0"""
444
  if pipeline is None:
445
- raise HTTPException(status_code=503, detail="Pipeline non initialisé")
446
 
447
  try:
448
- # Test avec texte simple
449
  test_request = ChunkRequest(
450
- text="Ceci est un test de chunking sémantique intelligent v5.0. "
451
- "Le système utilise LlamaIndex moderne avec llama-index-core. "
452
- "Il intègre Chonkie pour le découpage sémantique avancé. "
453
- "Il génère des relations hiérarchiques bidirectionnelles. "
454
- "L'export Obsidian utilise le format [[Titre]], id. "
455
- "Les agents IA reçoivent une base de connaissance structurée.",
456
- titre="Test Chunking v5.0",
457
- source_id="test_v5",
458
- include_metadata=True
 
 
 
 
 
 
 
459
  )
460
 
461
- # Test avec méthodes adaptatives
462
- result = None
463
- try:
464
- result = await pipeline.process_text(test_request)
465
- except AttributeError:
466
- result = await pipeline.process_text_sync(test_request)
 
 
 
 
 
 
 
 
 
467
 
468
  return {
469
- "test_status": "success",
470
- "chunks_generated": result.total_chunks,
471
- "processing_time": result.processing_time,
472
- "features_tested": [
473
- "LlamaIndex moderne (llama-index-core)",
474
- "Chunking sémantique Chonkie",
475
- "Relations hiérarchiques",
476
- "Export Obsidian",
477
- "Base connaissance agents"
 
 
 
 
 
 
478
  ],
479
- "version": "5.0.0",
480
- "architecture": "LlamaIndex Moderne Modulaire"
481
  }
482
 
483
  except Exception as e:
484
- logger.error(f"❌ Erreur test chunking v5.0: {e}")
485
  raise HTTPException(
486
  status_code=500,
487
- detail=f"Test échoué v5.0: {str(e)}"
488
  )
489
 
490
- # ✅ Gestion erreur 404 personnalisée
 
 
 
 
 
 
 
 
 
 
 
491
  @app.exception_handler(404)
492
  async def not_found_handler(request: Request, exc):
 
493
  return JSONResponse(
494
  status_code=404,
495
  content={
496
- "error": "Endpoint non trouvé",
497
  "message": f"L'endpoint {request.url.path} n'existe pas",
498
- "available_endpoints": ["/", "/health", "/config", "/chunk", "/chunk-batch", "/test"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  "documentation": "/docs",
500
- "version": "5.0.0",
501
- "architecture": "LlamaIndex Moderne"
502
  }
503
  )
504
 
505
- # Configuration pour démarrage Uvicorn optimisé HF Space
506
  if __name__ == "__main__":
507
  import uvicorn
508
 
 
 
509
  # Configuration optimisée pour HF Space gratuit
510
  uvicorn.run(
511
  "app:app",
512
  host="0.0.0.0",
513
- port=7860,
514
- reload=False, # Production mode
515
  access_log=False, # Économie ressources
516
  log_level="info",
517
  workers=1, # HF Space gratuit = 1 worker
518
  timeout_keep_alive=30,
519
- limit_concurrency=10 # Limite connexions simultanées
 
520
  )
 
1
  """
2
+ app.py v4.0 FINAL - FastAPI pour Chunking Sémantique Intelligent
3
 
4
+ CORRECTIONS ET AMÉLIORATIONS:
5
+ Import SmartChunkerPipeline (correct)
6
+ Méthodes synchronisées avec chunker_pipeline.py
7
+ Gestion d'erreurs robuste
8
+ Endpoints optimisés pour n8n
 
 
9
  ✅ Variables d'environnement sécurisées
10
+ ✅ Monitoring et health checks complets
11
+ ✅ Configuration HF Space gratuit optimisée
12
  """
13
 
14
  import os
15
  import logging
16
  import time
17
+ import asyncio
18
+ import gc
19
+ from pathlib import Path
20
  from fastapi import FastAPI, HTTPException, Request
21
  from fastapi.middleware.cors import CORSMiddleware
22
  from fastapi.responses import JSONResponse
23
  from pydantic import BaseModel, Field
24
  from typing import List, Dict, Any, Optional
 
25
  from concurrent.futures import ThreadPoolExecutor
 
26
 
27
  # Configuration logging optimisée
28
  logging.basicConfig(
29
  level=logging.INFO,
30
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
31
+ handlers=[
32
+ logging.StreamHandler(),
33
+ logging.FileHandler("/app/logs/app.log", mode="a") if os.path.exists("/app/logs") else logging.StreamHandler()
34
+ ]
35
  )
36
  logger = logging.getLogger(__name__)
37
 
38
+ # ✅ IMPORTS PRINCIPAUX - Vérification de compatibilité
39
  try:
40
+ from chunker_pipeline import SmartChunkerPipeline
41
  from schemas import ChunkRequest, ChunkResponse, ChunkMetadata
42
+ logger.info("✅ Modules chunking v4.0 importés avec succès")
43
  except ImportError as e:
44
+ logger.error(f"❌ ERREUR CRITIQUE - Import modules chunking: {e}")
45
+ logger.error("Vérifiez que les fichiers chunker_pipeline.py et schemas.py existent")
46
+ raise
 
 
 
 
 
 
47
 
48
+ # ✅ CONFIGURATION ENVIRONNEMENT HF SPACE SÉCURISÉE
49
+ def setup_environment():
50
+ """Configuration optimisée pour Hugging Face Space gratuit"""
51
+
52
+ # Cache HuggingFace optimisé
53
+ cache_base = "/app/cache"
54
+ os.environ["HF_HOME"] = f"{cache_base}/huggingface"
55
+ os.environ["TRANSFORMERS_CACHE"] = f"{cache_base}/transformers"
56
+ os.environ["HF_HUB_CACHE"] = f"{cache_base}/hub"
57
+
58
+ # Optimisations performance
59
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
60
+ os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
61
+ os.environ["TRANSFORMERS_VERBOSITY"] = "error"
62
+ os.environ["PYTHONUNBUFFERED"] = "1"
63
+
64
+ # Création dossiers cache sécurisés
65
+ cache_dirs = [
66
+ f"{cache_base}/huggingface",
67
+ f"{cache_base}/transformers",
68
+ f"{cache_base}/hub",
69
+ f"{cache_base}/llm",
70
+ f"{cache_base}/embeddings",
71
+ "/app/logs"
72
+ ]
73
+
74
+ for cache_dir in cache_dirs:
75
+ try:
76
+ os.makedirs(cache_dir, exist_ok=True)
77
+ os.chmod(cache_dir, 0o755)
78
+ except Exception as e:
79
+ logger.warning(f"⚠️ Impossible de créer {cache_dir}: {e}")
80
+
81
+ logger.info("✅ Environnement HF Space configuré")
82
 
83
+ # Configuration environnement
84
+ setup_environment()
85
 
86
+ # INITIALISATION FASTAPI OPTIMISÉE
87
  app = FastAPI(
88
+ title="🧠 Chunking Sémantique Intelligent API",
89
+ description="""
90
+ **API de découpage récursif hiérarchique avec parentalité**
91
+
92
+ 🚀 **Fonctionnalités:**
93
+ - Chunking sémantique avec Chonkie + LlamaIndex
94
+ - Relations bidirectionnelles parent/enfant
95
+ - Export Obsidian format [[Titre]], id
96
+ - Base connaissance pour agents IA spécialisés
97
+ - 100% gratuit sur HuggingFace Space
98
+
99
+ 🔧 **Optimisé pour n8n et automation**
100
+ """,
101
+ version="4.0.0",
102
  docs_url="/docs",
103
+ redoc_url="/redoc",
104
+ openapi_tags=[
105
+ {"name": "chunking", "description": "Endpoints de chunking principal"},
106
+ {"name": "monitoring", "description": "Santé et configuration"},
107
+ {"name": "test", "description": "Tests et validation"}
108
+ ]
109
  )
110
 
111
+ # CORS ÉTENDU POUR N8N ET INTÉGRATIONS
112
  app.add_middleware(
113
  CORSMiddleware,
114
+ allow_origins=["*"], # Nécessaire pour n8n
115
  allow_credentials=True,
116
+ allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
117
  allow_headers=["*"],
118
+ expose_headers=["*"]
119
  )
120
 
121
+ # ✅ VARIABLES GLOBALES
122
  pipeline = None
123
  executor = ThreadPoolExecutor(max_workers=1) # HF Space gratuit = 1 worker max
124
+ startup_time = time.time()
125
+ request_count = 0
126
 
127
+ # ✅ MIDDLEWARE MONITORING ET SÉCURITÉ
128
  @app.middleware("http")
129
+ async def monitoring_middleware(request: Request, call_next):
130
+ """Middleware pour monitoring et gestion erreurs globales"""
131
+ global request_count
132
+ start_time = time.time()
133
+ request_count += 1
134
+
135
+ # Headers sécurité
136
+ response = None
137
  try:
138
  response = await call_next(request)
139
+ response.headers["X-API-Version"] = "4.0.0"
140
+ response.headers["X-Powered-By"] = "Chunking-Semantic-AI"
141
+
142
+ # Log performance
143
+ process_time = time.time() - start_time
144
+ if process_time > 5.0: # Log requêtes lentes
145
+ logger.warning(f"⚠️ Requête lente: {request.url.path} - {process_time:.2f}s")
146
+
147
  return response
148
+
149
  except Exception as e:
150
+ logger.error(f"❌ Erreur middleware {request.url.path}: {str(e)}")
151
+
152
+ # Réponse d'erreur structurée
153
  return JSONResponse(
154
  status_code=500,
155
  content={
156
  "error": "Erreur interne du serveur",
157
  "detail": str(e),
158
+ "path": str(request.url.path),
159
  "timestamp": time.time(),
160
+ "request_id": request_count,
161
+ "version": "4.0.0"
162
  }
163
  )
164
 
165
+ # ✅ ÉVÉNEMENTS LIFECYCLE
166
  @app.on_event("startup")
167
  async def startup_event():
168
+ """Initialisation complète au démarrage"""
169
  global pipeline
170
+
171
  try:
172
+ logger.info("🚀 === DÉMARRAGE API CHUNKING SÉMANTIQUE v4.0 ===")
173
+
174
+ # Vérification espace disque
175
+ import shutil
176
+ total, used, free = shutil.disk_usage("/app")
177
+ free_gb = free / (1024**3)
178
+ logger.info(f"💾 Espace libre: {free_gb:.1f}GB")
179
+
180
+ if free_gb < 1.0:
181
+ logger.warning("⚠️ Espace disque faible (<1GB)")
182
 
183
+ # Initialisation pipeline principal
184
+ logger.info("🔧 Initialisation SmartChunkerPipeline...")
185
  pipeline = SmartChunkerPipeline()
186
  await pipeline.initialize()
187
 
188
+ # Vérification santé
189
+ health = await pipeline.health_check_v4()
190
+ logger.info(f"🏥 Status santé: {health['status']}")
191
+
192
+ if health['status'] != 'healthy':
193
+ logger.warning(f"⚠️ Pipeline en mode dégradé: {health['status']}")
194
+
195
+ # Configuration système
196
+ config_info = await pipeline.get_config_info_v4()
197
+ logger.info(f"🧠 LLM: {config_info['models']['llm_model']}")
198
+ logger.info(f"🔤 Embedding: {config_info['models']['embedding_model']}")
199
+ logger.info(f"🦛 Chonkie: {'✅' if config_info['models']['chonkie_available'] else '❌'}")
200
+
201
+ # Test rapide de fonctionnement
202
+ test_request = ChunkRequest(
203
+ text="Test d'initialisation du système de chunking.",
204
+ titre="Test Init",
205
+ source_id="init_test"
206
+ )
207
+
208
+ test_result = await pipeline.process_text(test_request)
209
+ logger.info(f"✅ Test init: {test_result.total_chunks} chunks générés")
210
 
211
+ logger.info("🎉 API Chunking Sémantique v4.0 prête !")
212
 
213
  except Exception as e:
214
+ logger.error(f"❌ ERREUR CRITIQUE lors du démarrage: {e}")
215
+ logger.error("Le service ne pourra pas fonctionner correctement")
216
  raise
217
 
218
  @app.on_event("shutdown")
219
  async def shutdown_event():
220
+ """Nettoyage propre à l'arrêt"""
221
  global pipeline, executor
222
+
223
  try:
224
+ logger.info("🛑 Arrêt du service en cours...")
225
 
226
+ # Nettoyage pipeline
227
  if pipeline:
228
+ await pipeline.cleanup()
229
+ logger.info("✅ Pipeline nettoyé")
 
 
 
230
 
231
+ # Nettoyage executor
232
  if executor:
233
+ executor.shutdown(wait=True, timeout=10)
234
+ logger.info("✅ Executor fermé")
235
 
236
  # Nettoyage mémoire final
237
  gc.collect()
238
 
239
+ # Statistiques finales
240
+ uptime = time.time() - startup_time
241
+ logger.info(f"📊 Statistiques finales:")
242
+ logger.info(f" - Temps de fonctionnement: {uptime:.1f}s")
243
+ logger.info(f" - Requêtes traitées: {request_count}")
244
+ logger.info(f" - Moyenne: {request_count/uptime:.2f} req/s")
245
+
246
+ logger.info("✅ Arrêt propre terminé")
247
 
248
  except Exception as e:
249
+ logger.error(f"⚠️ Erreur lors de l'arrêt: {e}")
250
+
251
+ # ✅ ENDPOINTS PRINCIPAUX
252
 
253
+ @app.get("/", tags=["monitoring"])
254
  async def root():
255
+ """Page d'accueil avec informations complètes du service"""
256
+ uptime = time.time() - startup_time
257
+
258
  return {
259
+ "service": "🧠 Chunking Sémantique Intelligent API",
260
+ "version": "4.0.0",
261
+ "status": "🟢 Opérationnel" if pipeline else "🔴 Non initialisé",
262
+ "uptime_seconds": round(uptime, 1),
263
+ "requests_processed": request_count,
264
+
265
  "features": [
266
+ "🧩 Chunking sémantique avec Chonkie",
267
+ "🏗️ Hiérarchie récursive intelligente",
268
+ "🔗 Relations bidirectionnelles parent/enfant",
269
+ "📝 Export Obsidian format [[Titre]], id",
270
+ "🤖 Base connaissance pour agents IA spécialisés",
271
+ "💰 100% gratuit sur HuggingFace Space",
272
+ "🔄 Optimisé pour n8n et automation"
 
 
 
 
 
 
 
 
273
  ],
274
+
275
+ "endpoints": {
276
+ "chunking": [
277
+ "POST /chunk - Chunking principal",
278
+ "POST /chunk-batch - Traitement par lots"
279
+ ],
280
+ "monitoring": [
281
+ "GET /health - Vérification santé détaillée",
282
+ "GET /config - Configuration système",
283
+ "GET /stats - Statistiques d'usage"
284
+ ],
285
+ "test": [
286
+ "POST /test - Test de validation",
287
+ "GET /ping - Test connectivité simple"
288
+ ]
289
+ },
290
+
291
+ "documentation": {
292
+ "interactive": "/docs",
293
+ "redoc": "/redoc"
294
+ },
295
+
296
+ "support": {
297
+ "n8n_compatible": True,
298
+ "max_text_length": "500,000 caractères",
299
+ "max_batch_size": 3,
300
+ "response_format": "JSON structuré"
301
+ }
302
  }
303
 
304
+ @app.get("/health", tags=["monitoring"])
305
  async def health_check():
306
+ """Vérification santé complète et détaillée"""
307
  try:
308
  if pipeline is None:
309
  return {
310
+ "status": "🔴 error",
311
  "message": "Pipeline non initialisé",
312
+ "version": "4.0.0",
313
+ "timestamp": time.time(),
314
+ "uptime": time.time() - startup_time,
315
+ "critical": True
316
  }
317
 
318
+ # Health check pipeline
319
+ health_result = await pipeline.health_check_v4()
 
320
 
321
+ # Informations mémoire
322
+ memory_info = pipeline.get_memory_usage_v4()
323
+
324
+ # Statistiques système
325
+ import psutil
 
 
 
 
 
 
 
 
 
 
 
326
  try:
327
+ cpu_percent = psutil.cpu_percent(interval=1)
328
+ memory_percent = psutil.virtual_memory().percent
329
+ except:
330
+ cpu_percent = 0
331
+ memory_percent = 0
332
+
333
+ # Status coloré
334
+ status_map = {
335
+ "healthy": "🟢 healthy",
336
+ "degraded": "🟡 degraded",
337
+ "unhealthy": "🔴 unhealthy",
338
+ "error": "🔴 error"
339
+ }
340
 
341
  return {
342
  **health_result,
343
+ "status": status_map.get(health_result['status'], health_result['status']),
344
  "memory_info": memory_info,
345
+ "system_info": {
346
+ "cpu_percent": cpu_percent,
347
+ "memory_percent": memory_percent,
348
+ "uptime": time.time() - startup_time,
349
+ "requests_processed": request_count
350
+ },
351
+ "version": "4.0.0"
352
  }
353
 
354
  except Exception as e:
355
+ logger.error(f"❌ Erreur health check: {e}")
356
  return {
357
+ "status": "🔴 error",
358
  "message": f"Erreur health check: {str(e)}",
359
+ "version": "4.0.0",
360
+ "timestamp": time.time(),
361
+ "critical": True
362
  }
363
 
364
+ @app.get("/config", tags=["monitoring"])
365
  async def get_config():
366
+ """Configuration système détaillée"""
367
  try:
368
  if pipeline is None:
369
  raise HTTPException(status_code=503, detail="Pipeline non initialisé")
370
 
371
+ config_info = await pipeline.get_config_info_v4()
 
372
 
373
+ # Ajout informations runtime
374
+ runtime_info = {
375
+ "python_version": f"{os.sys.version_info.major}.{os.sys.version_info.minor}.{os.sys.version_info.micro}",
376
+ "platform": os.name,
377
+ "workers": 1,
378
+ "max_request_size": "500KB",
379
+ "cache_enabled": True,
380
+ "environment": "HuggingFace Space"
381
+ }
 
 
 
 
 
 
 
 
382
 
383
  return {
384
  **config_info,
385
+ "runtime_info": runtime_info,
386
+ "api_version": "4.0.0",
387
+ "timestamp": time.time()
388
  }
389
 
390
  except Exception as e:
391
+ logger.error(f"❌ Erreur récupération config: {e}")
392
  raise HTTPException(status_code=500, detail=f"Erreur config: {str(e)}")
393
 
394
+ @app.get("/stats", tags=["monitoring"])
395
+ async def get_stats():
396
+ """Statistiques d'usage détaillées"""
397
+ uptime = time.time() - startup_time
398
+ avg_requests_per_minute = (request_count / uptime) * 60 if uptime > 0 else 0
399
+
400
+ return {
401
+ "service_stats": {
402
+ "uptime_seconds": round(uptime, 1),
403
+ "uptime_formatted": f"{int(uptime//3600)}h {int((uptime%3600)//60)}m {int(uptime%60)}s",
404
+ "total_requests": request_count,
405
+ "avg_requests_per_minute": round(avg_requests_per_minute, 2)
406
+ },
407
+ "system_health": {
408
+ "pipeline_initialized": pipeline is not None,
409
+ "memory_usage": pipeline.get_memory_usage_v4() if pipeline else "N/A"
410
+ },
411
+ "version": "4.0.0",
412
+ "timestamp": time.time()
413
+ }
414
+
415
+ @app.post("/chunk", response_model=ChunkResponse, tags=["chunking"])
416
  async def chunk_text(request: ChunkRequest):
417
  """
418
+ 🧠 ENDPOINT PRINCIPAL - Chunking sémantique intelligent
419
 
420
+ **Fonctionnalités:**
421
+ - Chunking sémantique avec Chonkie + LlamaIndex
422
+ - Relations hiérarchiques bidirectionnelles
 
423
  - Export Obsidian format [[Titre]], id
424
+ - Base connaissance pour agents IA
425
+
426
+ **Optimisé pour n8n et automation**
427
  """
428
  if pipeline is None:
429
+ raise HTTPException(
430
+ status_code=503,
431
+ detail="❌ Pipeline non initialisé - Redémarrez le service"
432
+ )
433
 
434
  start_time = time.time()
435
 
436
  try:
437
+ logger.info(f"📝 Début chunking: {request.titre or 'Sans titre'} ({len(request.text)} chars)")
438
 
439
  # Validation entrées renforcée
440
  if not request.text or len(request.text.strip()) < 10:
441
  raise HTTPException(
442
  status_code=400,
443
+ detail="Le texte doit contenir au moins 10 caractères"
444
  )
445
 
446
+ # Limite HF Space gratuit
447
+ max_length = 500000
448
+ if len(request.text) > max_length:
449
  raise HTTPException(
450
  status_code=400,
451
+ detail=f"Texte trop long ({len(request.text)} chars). Maximum: {max_length:,} caractères"
452
  )
453
 
454
+ # Traitement principal
455
+ result = await pipeline.process_text(request)
 
 
 
 
 
 
 
 
 
 
 
 
 
456
 
457
  processing_time = time.time() - start_time
458
+
459
+ # Log succès
460
+ logger.info(
461
+ f"✅ Chunking terminé: {result.total_chunks} chunks, "
462
+ f"{result.total_tokens} tokens en {processing_time:.2f}s"
463
+ )
464
 
465
  return result
466
 
467
  except HTTPException:
 
468
  raise
469
  except Exception as e:
470
+ logger.error(f"❌ Erreur chunking: {str(e)}")
471
 
472
+ # Nettoyage mémoire d'urgence
473
  try:
474
+ await pipeline._cleanup_memory_v4()
475
+ gc.collect()
 
 
476
  except:
477
  pass
478
 
 
 
479
  raise HTTPException(
480
  status_code=500,
481
+ detail=f"Erreur traitement: {str(e)}"
482
  )
483
 
484
+ @app.post("/chunk-batch", tags=["chunking"])
485
  async def chunk_batch(requests: List[ChunkRequest]):
486
+ """
487
+ 📦 Traitement par lots optimisé pour HF Space gratuit
488
+
489
+ **Limites:**
490
+ - Maximum 3 textes par lot
491
+ - Traitement séquentiel pour économiser la mémoire
492
+ """
493
 
494
  # Validation limite batch pour Space gratuit
495
  max_batch_size = 3
496
  if len(requests) > max_batch_size:
497
  raise HTTPException(
498
  status_code=400,
499
+ detail=f"Maximum {max_batch_size} textes par lot sur HF Space gratuit"
500
  )
501
 
502
  if pipeline is None:
503
+ raise HTTPException(status_code=503, detail="Pipeline non initialisé")
504
 
505
  start_time = time.time()
506
  results = []
507
 
508
  try:
509
+ logger.info(f"📦 Début batch: {len(requests)} textes")
510
 
511
  for idx, request in enumerate(requests):
512
  try:
513
+ logger.info(f" 📝 Traitement {idx+1}/{len(requests)}: {request.titre or 'Sans titre'}")
 
 
 
 
 
 
514
 
515
+ result = await pipeline.process_text(request)
516
  results.append({
517
  "success": True,
518
  "index": idx,
 
520
  "result": result
521
  })
522
 
523
+ # Nettoyage entre chaque traitement
524
+ if idx < len(requests) - 1: # Pas pour le dernier
525
+ await pipeline._cleanup_memory_v4()
526
+
527
  except Exception as e:
528
+ logger.error(f"❌ Erreur batch item {idx}: {e}")
529
  results.append({
530
  "success": False,
531
  "index": idx,
532
+ "source_id": request.source_id or f"item_{idx}",
533
  "error": str(e)
534
  })
535
 
536
  total_time = time.time() - start_time
537
  successful_results = [r for r in results if r["success"]]
538
 
539
+ # Nettoyage final
540
  try:
541
+ await pipeline._cleanup_memory_v4()
 
542
  except:
543
  pass
544
 
545
+ logger.info(
546
+ f"✅ Batch terminé: {len(successful_results)}/{len(requests)} succès "
547
+ f"en {total_time:.2f}s"
548
+ )
549
 
550
  return {
551
  "results": results,
552
+ "summary": {
553
+ "total_processed": len(requests),
554
+ "successful": len(successful_results),
555
+ "failed": len(requests) - len(successful_results),
556
+ "success_rate": f"{(len(successful_results)/len(requests)*100):.1f}%",
557
+ "total_processing_time": round(total_time, 2),
558
+ "avg_time_per_item": round(total_time / len(requests), 2)
559
+ },
560
+ "version": "4.0.0",
561
+ "timestamp": time.time()
562
  }
563
 
564
  except Exception as e:
565
+ logger.error(f"❌ Erreur batch global: {e}")
566
  gc.collect()
567
  raise HTTPException(
568
  status_code=500,
569
+ detail=f"Erreur traitement batch: {str(e)}"
570
  )
571
 
572
+ @app.post("/test", tags=["test"])
 
573
  async def test_chunking():
574
+ """🧪 Test de validation du déploiement"""
575
  if pipeline is None:
576
+ raise HTTPException(status_code=503, detail="Pipeline non initialisé")
577
 
578
  try:
 
579
  test_request = ChunkRequest(
580
+ text="""
581
+ Ceci est un test complet de chunking sémantique intelligent v4.0.
582
+
583
+ Le système utilise Chonkie pour le découpage sémantique avancé.
584
+ Il génère des relations hiérarchiques bidirectionnelles entre les chunks.
585
+
586
+ L'export Obsidian utilise le format [[Titre]], id pour les liens.
587
+ Les agents IA reçoivent une base de connaissance parfaitement structurée.
588
+
589
+ Ce test valide toutes les fonctionnalités principales du système.
590
+ """,
591
+ titre="Test Validation v4.0",
592
+ source_id="validation_test_v4",
593
+ include_metadata=True,
594
+ export_obsidian=True,
595
+ export_agents=True
596
  )
597
 
598
+ start_time = time.time()
599
+ result = await pipeline.process_text(test_request)
600
+ test_time = time.time() - start_time
601
+
602
+ # Vérifications détaillées
603
+ checks = {
604
+ "chunking_functional": result.total_chunks > 0,
605
+ "metadata_extracted": len(result.chunks[0].metadata.keywords) > 0 if result.chunks else False,
606
+ "hierarchy_built": len(result.hierarchy) > 0,
607
+ "obsidian_export": result.obsidian_export is not None,
608
+ "agent_knowledge": result.agent_knowledge is not None,
609
+ "processing_time_ok": test_time < 30 # Moins de 30s
610
+ }
611
+
612
+ success_rate = sum(checks.values()) / len(checks) * 100
613
 
614
  return {
615
+ "test_status": "✅ SUCCESS" if success_rate == 100 else "⚠️ PARTIAL",
616
+ "success_rate": f"{success_rate:.1f}%",
617
+ "results": {
618
+ "chunks_generated": result.total_chunks,
619
+ "tokens_processed": result.total_tokens,
620
+ "processing_time": round(test_time, 2),
621
+ "hierarchy_levels": len(result.hierarchy)
622
+ },
623
+ "checks": checks,
624
+ "features_validated": [
625
+ "✅ Chunking sémantique Chonkie" if checks["chunking_functional"] else "❌ Chunking failed",
626
+ "✅ Extraction métadonnées" if checks["metadata_extracted"] else "❌ Metadata failed",
627
+ "✅ Relations hiérarchiques" if checks["hierarchy_built"] else "❌ Hierarchy failed",
628
+ "✅ Export Obsidian" if checks["obsidian_export"] else "❌ Obsidian failed",
629
+ "✅ Base agents IA" if checks["agent_knowledge"] else "❌ Agents failed"
630
  ],
631
+ "version": "4.0.0",
632
+ "timestamp": time.time()
633
  }
634
 
635
  except Exception as e:
636
+ logger.error(f"❌ Test validation échoué: {e}")
637
  raise HTTPException(
638
  status_code=500,
639
+ detail=f"Test échoué: {str(e)}"
640
  )
641
 
642
+ @app.get("/ping", tags=["test"])
643
+ async def ping():
644
+ """🏓 Test de connectivité simple"""
645
+ return {
646
+ "ping": "pong",
647
+ "timestamp": time.time(),
648
+ "version": "4.0.0",
649
+ "status": "🟢 Opérationnel" if pipeline else "🔴 Non initialisé"
650
+ }
651
+
652
+ # ✅ GESTION D'ERREURS PERSONNALISÉE
653
+
654
  @app.exception_handler(404)
655
  async def not_found_handler(request: Request, exc):
656
+ """Gestionnaire 404 personnalisé"""
657
  return JSONResponse(
658
  status_code=404,
659
  content={
660
+ "error": "Endpoint non trouvé",
661
  "message": f"L'endpoint {request.url.path} n'existe pas",
662
+ "available_endpoints": {
663
+ "chunking": ["/chunk", "/chunk-batch"],
664
+ "monitoring": ["/health", "/config", "/stats"],
665
+ "test": ["/test", "/ping"],
666
+ "docs": ["/docs", "/redoc"]
667
+ },
668
+ "suggestion": "Consultez /docs pour la documentation complète",
669
+ "version": "4.0.0"
670
+ }
671
+ )
672
+
673
+ @app.exception_handler(422)
674
+ async def validation_exception_handler(request: Request, exc):
675
+ """Gestionnaire erreurs de validation Pydantic"""
676
+ return JSONResponse(
677
+ status_code=422,
678
+ content={
679
+ "error": "❌ Erreur de validation",
680
+ "message": "Les données envoyées ne respectent pas le format attendu",
681
+ "detail": str(exc),
682
+ "hint": "Vérifiez la structure de votre requête JSON",
683
  "documentation": "/docs",
684
+ "version": "4.0.0"
 
685
  }
686
  )
687
 
688
+ # POINT D'ENTRÉE PRINCIPAL
689
  if __name__ == "__main__":
690
  import uvicorn
691
 
692
+ logger.info("🚀 Démarrage direct du serveur...")
693
+
694
  # Configuration optimisée pour HF Space gratuit
695
  uvicorn.run(
696
  "app:app",
697
  host="0.0.0.0",
698
+ port=7860, # Port standard HF Space
699
+ reload=False, # Mode production
700
  access_log=False, # Économie ressources
701
  log_level="info",
702
  workers=1, # HF Space gratuit = 1 worker
703
  timeout_keep_alive=30,
704
+ limit_concurrency=10, # Limite connexions simultanées
705
+ timeout_graceful_shutdown=30
706
  )