chouchouvs commited on
Commit
5c37037
·
verified ·
1 Parent(s): 6520d03

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +69 -86
main.py CHANGED
@@ -1,29 +1,30 @@
1
  # -*- coding: utf-8 -*-
2
  """
3
- HF Space - main.py de substitution pour tests Qdrant / indexation minimale (robuste + auto-refresh)
 
4
 
5
  Endpoints:
6
- - GET / → redirige vers UI_PATH (défaut: /ui)
7
- - GET /ui (UI_PATH) → UI Gradio
8
- - GET /health → healthcheck
9
- - GET /api → infos service
10
- - GET /debug/env → aperçu config (sans secrets)
11
- - POST /wipe?project_id=XXX → supprime la collection Qdrant
12
- - POST /index → lance un job d'indexation
13
- - GET /status/{job_id} → état + logs du job
14
- - GET /collections/{proj}/count → count points dans Qdrant
15
- - POST /query → recherche sémantique
16
 
17
  ENV:
18
- - QDRANT_URL, QDRANT_API_KEY (requis pour upsert)
19
- - COLLECTION_PREFIX (défaut "proj_")
20
- - EMB_PROVIDER ("hf" par défaut, "dummy" sinon)
21
- - HF_EMBED_MODEL (défaut "BAAI/bge-m3")
22
- - HUGGINGFACEHUB_API_TOKEN (si EMB_PROVIDER=hf)
23
- - EMB_FALLBACK_TO_DUMMY (true/false) → si vrai, bascule dummy si HF échoue
24
- - LOG_LEVEL (défaut DEBUG)
25
- - PORT (fourni par HF, défaut 7860)
26
- - UI_PATH (défaut "/ui")
27
  """
28
 
29
  from __future__ import annotations
@@ -32,6 +33,7 @@ import time
32
  import uuid
33
  import hashlib
34
  import logging
 
35
  import asyncio
36
  from typing import List, Dict, Any, Optional, Tuple
37
 
@@ -45,7 +47,7 @@ from fastapi.responses import RedirectResponse
45
  import gradio as gr
46
 
47
  # ------------------------------------------------------------------------------
48
- # Configuration & logs
49
  # ------------------------------------------------------------------------------
50
  LOG_LEVEL = os.getenv("LOG_LEVEL", "DEBUG").upper()
51
  logging.basicConfig(
@@ -58,26 +60,20 @@ QDRANT_URL = os.getenv("QDRANT_URL", "").rstrip("/")
58
  QDRANT_API_KEY = os.getenv("QDRANT_API_KEY", "")
59
  COLLECTION_PREFIX = os.getenv("COLLECTION_PREFIX", "proj_").strip() or "proj_"
60
 
61
- EMB_PROVIDER = os.getenv("EMB_PROVIDER", "hf").lower() # "hf" | "dummy"
62
  HF_EMBED_MODEL = os.getenv("HF_EMBED_MODEL", "BAAI/bge-m3")
63
  HF_TOKEN = os.getenv("HUGGINGFACEHUB_API_TOKEN", "")
64
-
65
  EMB_FALLBACK_TO_DUMMY = os.getenv("EMB_FALLBACK_TO_DUMMY", "false").lower() in ("1","true","yes","on")
66
 
67
- UI_PATH = os.getenv("UI_PATH", "/ui") # UI montée ici par défaut
68
 
69
  if not QDRANT_URL or not QDRANT_API_KEY:
70
  LOG.warning("QDRANT_URL / QDRANT_API_KEY non fournis : l'upsert échouera.")
71
-
72
- if EMB_PROVIDER == "hf" and not HF_TOKEN:
73
- LOG.warning(
74
- "EMB_PROVIDER=hf sans HUGGINGFACEHUB_API_TOKEN. "
75
- "→ soit définis le token, soit mets EMB_PROVIDER=dummy, "
76
- "soit active EMB_FALLBACK_TO_DUMMY=true."
77
- )
78
 
79
  # ------------------------------------------------------------------------------
80
- # Schémas Pydantic
81
  # ------------------------------------------------------------------------------
82
  class FileItem(BaseModel):
83
  path: str
@@ -96,9 +92,6 @@ class QueryRequest(BaseModel):
96
  text: str
97
  top_k: int = Field(5, ge=1, le=100)
98
 
99
- # ------------------------------------------------------------------------------
100
- # Job store (en mémoire)
101
- # ------------------------------------------------------------------------------
102
  class JobState(BaseModel):
103
  job_id: str
104
  project_id: str
@@ -121,7 +114,7 @@ class JobState(BaseModel):
121
  JOBS: Dict[str, JobState] = {}
122
 
123
  # ------------------------------------------------------------------------------
124
- # Utilitaires
125
  # ------------------------------------------------------------------------------
126
  def hash8(s: str) -> str:
127
  return hashlib.sha256(s.encode("utf-8")).hexdigest()[:16]
@@ -134,7 +127,6 @@ def l2_normalize(vec: List[float]) -> List[float]:
134
  return arr.astype(np.float32).tolist()
135
 
136
  def flatten_any(x: Any) -> List[float]:
137
- """Aplatis potentiels [[...]] ou [[[...]]] en 1D."""
138
  if isinstance(x, (list, tuple)):
139
  if len(x) > 0 and isinstance(x[0], (list, tuple)):
140
  return flatten_any(x[0])
@@ -142,7 +134,6 @@ def flatten_any(x: Any) -> List[float]:
142
  raise ValueError("Embedding vector mal formé")
143
 
144
  def chunk_text(text: str, chunk_size: int, overlap: int) -> List[Tuple[int, int, str]]:
145
- """Retourne [(start, end, chunk)] et ignore les fragments < 30 chars."""
146
  text = text or ""
147
  if not text.strip():
148
  return []
@@ -160,10 +151,9 @@ def chunk_text(text: str, chunk_size: int, overlap: int) -> List[Tuple[int, int,
160
  return res
161
 
162
  # ------------------------------------------------------------------------------
163
- # Qdrant helpers
164
  # ------------------------------------------------------------------------------
165
  async def ensure_collection(client: httpx.AsyncClient, coll: str, vector_size: int) -> None:
166
- """Crée la collection Qdrant (distance=Cosine), ou la recrée si dim mismatch."""
167
  url = f"{QDRANT_URL}/collections/{coll}"
168
  r = await client.get(url, headers={"api-key": QDRANT_API_KEY}, timeout=20)
169
  recreate = False
@@ -201,24 +191,15 @@ async def qdrant_count(client: httpx.AsyncClient, coll: str) -> int:
201
 
202
  async def qdrant_search(client: httpx.AsyncClient, coll: str, vector: List[float], limit: int = 5) -> Dict[str, Any]:
203
  url = f"{QDRANT_URL}/collections/{coll}/points/search"
204
- r = await client.post(
205
- url,
206
- headers={"api-key": QDRANT_API_KEY},
207
- json={"vector": vector, "limit": limit, "with_payload": True},
208
- timeout=30,
209
- )
210
  if r.status_code != 200:
211
  raise HTTPException(status_code=500, detail=f"Qdrant search échoué: {r.text}")
212
  return r.json()
213
 
214
  # ------------------------------------------------------------------------------
215
- # Embeddings (HF Inference ou dummy)
216
  # ------------------------------------------------------------------------------
217
  def _maybe_prefix_for_model(texts: List[str], model_name: str) -> List[str]:
218
- """
219
- E5 attend en pratique des préfixes 'query: ' (ou 'passage: ' / 'document: ').
220
- On préfixe automatiquement si le modèle contient 'e5'.
221
- """
222
  m = (model_name or "").lower()
223
  if "e5" in m:
224
  return [("query: " + t) for t in texts]
@@ -270,7 +251,7 @@ async def embed_texts(client: httpx.AsyncClient, texts: List[str]) -> List[List[
270
  return embed_dummy(texts, dim=128)
271
 
272
  # ------------------------------------------------------------------------------
273
- # Pipeline d'indexation (robuste)
274
  # ------------------------------------------------------------------------------
275
  async def run_index_job(job: JobState, req: IndexRequest) -> None:
276
  try:
@@ -282,12 +263,6 @@ async def run_index_job(job: JobState, req: IndexRequest) -> None:
282
  f"provider={EMB_PROVIDER} model={HF_EMBED_MODEL}"
283
  )
284
 
285
- # Dédup global par hash du texte de fichier
286
- file_hashes = [hash8(f.text) for f in req.files]
287
- uniq = len(set(file_hashes))
288
- if uniq != len(file_hashes):
289
- job.log(f"Attention: {len(file_hashes)-uniq} fichier(s) ont un texte identique (hash dupliqué).")
290
-
291
  # Chunking
292
  records: List[Dict[str, Any]] = []
293
  for f in req.files:
@@ -301,7 +276,6 @@ async def run_index_job(job: JobState, req: IndexRequest) -> None:
301
  records.append({"payload": payload, "raw": ch})
302
  job.total_chunks = len(records)
303
  job.log(f"Total chunks = {job.total_chunks}")
304
-
305
  if job.total_chunks == 0:
306
  job.stage = "failed"
307
  job.errors.append("Aucun chunk à indexer.")
@@ -314,11 +288,12 @@ async def run_index_job(job: JobState, req: IndexRequest) -> None:
314
  vec_dim = len(warmup_vec)
315
  job.log(f"Warmup embeddings dim={vec_dim}")
316
 
317
- # Collection Qdrant
318
  coll = f"{COLLECTION_PREFIX}{req.project_id}"
319
  await ensure_collection(client, coll, vector_size=vec_dim)
320
  job.log(f"Collection prête: {coll} (dim={vec_dim})")
321
 
 
322
  job.stage = "upserting"
323
  batch_points: List[Dict[str, Any]] = []
324
 
@@ -360,8 +335,29 @@ async def run_index_job(job: JobState, req: IndexRequest) -> None:
360
  job.finished_at = time.time()
361
  job.log(f"❌ Exception: {e}")
362
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  # ------------------------------------------------------------------------------
364
- # FastAPI app + endpoints
365
  # ------------------------------------------------------------------------------
366
  fastapi_app = FastAPI(title="Remote Indexer - Minimal Test Space")
367
  fastapi_app.add_middleware(
@@ -378,13 +374,11 @@ async def health():
378
  @fastapi_app.get("/api")
379
  async def api_info():
380
  return {
381
- "ok": True,
382
- "service": "remote-indexer-min",
383
  "qdrant": bool(QDRANT_URL),
384
- "emb_provider": EMB_PROVIDER,
385
- "hf_model": HF_EMBED_MODEL,
386
- "ui_path": UI_PATH,
387
  "fallback_to_dummy": EMB_FALLBACK_TO_DUMMY,
 
388
  }
389
 
390
  @fastapi_app.get("/debug/env")
@@ -399,7 +393,6 @@ async def debug_env():
399
  "collection_prefix": COLLECTION_PREFIX,
400
  }
401
 
402
- # Redirige "/" → UI_PATH (ex.: /ui).
403
  @fastapi_app.get("/")
404
  async def root_redirect():
405
  return RedirectResponse(url=UI_PATH, status_code=307)
@@ -419,12 +412,8 @@ async def wipe(project_id: str = Query(..., min_length=1)):
419
  async def index(req: IndexRequest):
420
  if not QDRANT_URL or not QDRANT_API_KEY:
421
  raise HTTPException(status_code=400, detail="QDRANT_URL / QDRANT_API_KEY requis")
422
- job_id = uuid.uuid4().hex[:12]
423
- job = JobState(job_id=job_id, project_id=req.project_id)
424
- JOBS[job_id] = job
425
- asyncio.create_task(run_index_job(job, req))
426
- job.log(f"Job {job_id} créé pour project {req.project_id}")
427
- return {"job_id": job_id, "project_id": req.project_id}
428
 
429
  @fastapi_app.get("/status/{job_id}")
430
  async def status(job_id: str):
@@ -453,7 +442,7 @@ async def query(req: QueryRequest):
453
  return data
454
 
455
  # ------------------------------------------------------------------------------
456
- # Gradio UI (avec Status + Auto-refresh)
457
  # ------------------------------------------------------------------------------
458
  def _default_two_docs() -> List[Dict[str, str]]:
459
  a = "Alpha bravo charlie delta echo foxtrot golf hotel india. " * 3
@@ -462,7 +451,7 @@ def _default_two_docs() -> List[Dict[str, str]]:
462
 
463
  async def ui_wipe(project: str):
464
  try:
465
- resp = await wipe(project) # appelle la route interne
466
  return f"✅ Wipe ok — collection {resp['collection']} supprimée."
467
  except Exception as e:
468
  LOG.exception("wipe UI error")
@@ -479,10 +468,8 @@ async def ui_index_sample(project: str, chunk_size: int, overlap: int, batch_siz
479
  store_text=store_text,
480
  )
481
  try:
482
- data = await index(req)
483
- job_id = data["job_id"]
484
- # On retourne ET le message ET le job_id pour remplir le champ
485
- return f"🚀 Job lancé: {job_id}", job_id
486
  except ValidationError as ve:
487
  return f"❌ Payload invalide: {ve}", ""
488
  except Exception as e:
@@ -530,7 +517,7 @@ async def ui_query(project: str, text: str, topk: int):
530
  LOG.exception("query UI error")
531
  return f"❌ Query erreur: {e}"
532
 
533
- with gr.Blocks(title="Remote Indexer - Minimal Test", analytics_enabled=False) as ui:
534
  gr.Markdown("## 🔬 Remote Indexer — Tests sans console\n"
535
  "Wipe → Index 2 docs → Status → Count → Query\n"
536
  f"- **Embeddings**: `{EMB_PROVIDER}` (model: `{HF_EMBED_MODEL}`)\n"
@@ -561,21 +548,17 @@ with gr.Blocks(title="Remote Indexer - Minimal Test", analytics_enabled=False) a
561
  query_btn = gr.Button("🔎 Query")
562
  query_out = gr.Textbox(lines=10, label="Résultats Query", interactive=False)
563
 
564
- # Liens UI
565
  wipe_btn.click(ui_wipe, inputs=[project_tb], outputs=[out_log])
566
- # index renvoie (message, job_id)
567
  index_btn.click(ui_index_sample, inputs=[project_tb, chunk_size, overlap, batch_size, store_text], outputs=[out_log, jobid_tb])
568
  count_btn.click(ui_count, inputs=[project_tb], outputs=[out_log])
569
 
570
- # Status bouton manuel
571
  status_btn.click(ui_status, inputs=[jobid_tb], outputs=[out_log])
572
-
573
- # Auto-refresh avec Timer (toutes les 2s si coché)
574
  timer = gr.Timer(2.0, active=False)
575
  timer.tick(ui_status, inputs=[jobid_tb], outputs=[out_log])
576
  auto_chk.change(lambda x: gr.update(active=x), inputs=auto_chk, outputs=timer)
577
 
578
- # Monte l'UI Gradio sur la FastAPI au chemin UI_PATH
 
579
  app = gr.mount_gradio_app(fastapi_app, ui, path=UI_PATH)
580
 
581
  if __name__ == "__main__":
 
1
  # -*- coding: utf-8 -*-
2
  """
3
+ Remote Indexer (HF Space) Qdrant + embeddings (HF ou dummy)
4
+ Version: worker-thread (pas d'asyncio.create_task dans l'UI), robust logging.
5
 
6
  Endpoints:
7
+ - GET / → redirige vers UI_PATH (défaut: /ui)
8
+ - GET /ui → UI Gradio
9
+ - GET /health → healthcheck
10
+ - GET /api → infos service
11
+ - GET /debug/env → aperçu config (sans secrets)
12
+ - POST /wipe?project_id=XXX
13
+ - POST /index
14
+ - GET /status/{job_id}
15
+ - GET /collections/{project_id}/count
16
+ - POST /query
17
 
18
  ENV:
19
+ - QDRANT_URL, QDRANT_API_KEY (requis pour upsert)
20
+ - COLLECTION_PREFIX (défaut "proj_")
21
+ - EMB_PROVIDER ("hf" | "dummy"; défaut "hf")
22
+ - HF_EMBED_MODEL (défaut "BAAI/bge-m3")
23
+ - HUGGINGFACEHUB_API_TOKEN (si EMB_PROVIDER=hf)
24
+ - EMB_FALLBACK_TO_DUMMY (true/false) → bascule dummy si HF échoue
25
+ - LOG_LEVEL (défaut DEBUG)
26
+ - UI_PATH (défaut "/ui")
27
+ - PORT (défaut 7860)
28
  """
29
 
30
  from __future__ import annotations
 
33
  import uuid
34
  import hashlib
35
  import logging
36
+ import threading
37
  import asyncio
38
  from typing import List, Dict, Any, Optional, Tuple
39
 
 
47
  import gradio as gr
48
 
49
  # ------------------------------------------------------------------------------
50
+ # Config & logs
51
  # ------------------------------------------------------------------------------
52
  LOG_LEVEL = os.getenv("LOG_LEVEL", "DEBUG").upper()
53
  logging.basicConfig(
 
60
  QDRANT_API_KEY = os.getenv("QDRANT_API_KEY", "")
61
  COLLECTION_PREFIX = os.getenv("COLLECTION_PREFIX", "proj_").strip() or "proj_"
62
 
63
+ EMB_PROVIDER = os.getenv("EMB_PROVIDER", "hf").lower()
64
  HF_EMBED_MODEL = os.getenv("HF_EMBED_MODEL", "BAAI/bge-m3")
65
  HF_TOKEN = os.getenv("HUGGINGFACEHUB_API_TOKEN", "")
 
66
  EMB_FALLBACK_TO_DUMMY = os.getenv("EMB_FALLBACK_TO_DUMMY", "false").lower() in ("1","true","yes","on")
67
 
68
+ UI_PATH = os.getenv("UI_PATH", "/ui")
69
 
70
  if not QDRANT_URL or not QDRANT_API_KEY:
71
  LOG.warning("QDRANT_URL / QDRANT_API_KEY non fournis : l'upsert échouera.")
72
+ if EMB_PROVIDER == "hf" and not HF_TOKEN and not EMB_FALLBACK_TO_DUMMY:
73
+ LOG.warning("EMB_PROVIDER=hf sans HUGGINGFACEHUB_API_TOKEN (pas de fallback) → préférer EMB_PROVIDER=dummy ou EMB_FALLBACK_TO_DUMMY=true.")
 
 
 
 
 
74
 
75
  # ------------------------------------------------------------------------------
76
+ # Models
77
  # ------------------------------------------------------------------------------
78
  class FileItem(BaseModel):
79
  path: str
 
92
  text: str
93
  top_k: int = Field(5, ge=1, le=100)
94
 
 
 
 
95
  class JobState(BaseModel):
96
  job_id: str
97
  project_id: str
 
114
  JOBS: Dict[str, JobState] = {}
115
 
116
  # ------------------------------------------------------------------------------
117
+ # Utils
118
  # ------------------------------------------------------------------------------
119
  def hash8(s: str) -> str:
120
  return hashlib.sha256(s.encode("utf-8")).hexdigest()[:16]
 
127
  return arr.astype(np.float32).tolist()
128
 
129
  def flatten_any(x: Any) -> List[float]:
 
130
  if isinstance(x, (list, tuple)):
131
  if len(x) > 0 and isinstance(x[0], (list, tuple)):
132
  return flatten_any(x[0])
 
134
  raise ValueError("Embedding vector mal formé")
135
 
136
  def chunk_text(text: str, chunk_size: int, overlap: int) -> List[Tuple[int, int, str]]:
 
137
  text = text or ""
138
  if not text.strip():
139
  return []
 
151
  return res
152
 
153
  # ------------------------------------------------------------------------------
154
+ # Qdrant
155
  # ------------------------------------------------------------------------------
156
  async def ensure_collection(client: httpx.AsyncClient, coll: str, vector_size: int) -> None:
 
157
  url = f"{QDRANT_URL}/collections/{coll}"
158
  r = await client.get(url, headers={"api-key": QDRANT_API_KEY}, timeout=20)
159
  recreate = False
 
191
 
192
  async def qdrant_search(client: httpx.AsyncClient, coll: str, vector: List[float], limit: int = 5) -> Dict[str, Any]:
193
  url = f"{QDRANT_URL}/collections/{coll}/points/search"
194
+ r = await client.post(url, headers={"api-key": QDRANT_API_KEY}, json={"vector": vector, "limit": limit, "with_payload": True}, timeout=30)
 
 
 
 
 
195
  if r.status_code != 200:
196
  raise HTTPException(status_code=500, detail=f"Qdrant search échoué: {r.text}")
197
  return r.json()
198
 
199
  # ------------------------------------------------------------------------------
200
+ # Embeddings
201
  # ------------------------------------------------------------------------------
202
  def _maybe_prefix_for_model(texts: List[str], model_name: str) -> List[str]:
 
 
 
 
203
  m = (model_name or "").lower()
204
  if "e5" in m:
205
  return [("query: " + t) for t in texts]
 
251
  return embed_dummy(texts, dim=128)
252
 
253
  # ------------------------------------------------------------------------------
254
+ # Core: run_index_job (async) + worker thread wrapper
255
  # ------------------------------------------------------------------------------
256
  async def run_index_job(job: JobState, req: IndexRequest) -> None:
257
  try:
 
263
  f"provider={EMB_PROVIDER} model={HF_EMBED_MODEL}"
264
  )
265
 
 
 
 
 
 
 
266
  # Chunking
267
  records: List[Dict[str, Any]] = []
268
  for f in req.files:
 
276
  records.append({"payload": payload, "raw": ch})
277
  job.total_chunks = len(records)
278
  job.log(f"Total chunks = {job.total_chunks}")
 
279
  if job.total_chunks == 0:
280
  job.stage = "failed"
281
  job.errors.append("Aucun chunk à indexer.")
 
288
  vec_dim = len(warmup_vec)
289
  job.log(f"Warmup embeddings dim={vec_dim}")
290
 
291
+ # Collection
292
  coll = f"{COLLECTION_PREFIX}{req.project_id}"
293
  await ensure_collection(client, coll, vector_size=vec_dim)
294
  job.log(f"Collection prête: {coll} (dim={vec_dim})")
295
 
296
+ # Upsert
297
  job.stage = "upserting"
298
  batch_points: List[Dict[str, Any]] = []
299
 
 
335
  job.finished_at = time.time()
336
  job.log(f"❌ Exception: {e}")
337
 
338
+ def _run_job_in_thread(job: JobState, req: IndexRequest) -> None:
339
+ """Exécute l'async run_index_job dans un thread dédié avec son propre event loop."""
340
+ def _runner():
341
+ try:
342
+ asyncio.run(run_index_job(job, req))
343
+ except Exception as e:
344
+ job.stage = "failed"
345
+ job.errors.append(str(e))
346
+ job.finished_at = time.time()
347
+ job.log(f"❌ Thread exception: {e}")
348
+ t = threading.Thread(target=_runner, daemon=True)
349
+ t.start()
350
+
351
+ def create_and_start_job(req: IndexRequest) -> JobState:
352
+ job_id = uuid.uuid4().hex[:12]
353
+ job = JobState(job_id=job_id, project_id=req.project_id)
354
+ JOBS[job_id] = job
355
+ job.log(f"Job {job_id} créé pour project {req.project_id}")
356
+ _run_job_in_thread(job, req)
357
+ return job
358
+
359
  # ------------------------------------------------------------------------------
360
+ # FastAPI app
361
  # ------------------------------------------------------------------------------
362
  fastapi_app = FastAPI(title="Remote Indexer - Minimal Test Space")
363
  fastapi_app.add_middleware(
 
374
  @fastapi_app.get("/api")
375
  async def api_info():
376
  return {
377
+ "ok": True, "service": "remote-indexer-min",
 
378
  "qdrant": bool(QDRANT_URL),
379
+ "emb_provider": EMB_PROVIDER, "hf_model": HF_EMBED_MODEL,
 
 
380
  "fallback_to_dummy": EMB_FALLBACK_TO_DUMMY,
381
+ "ui_path": UI_PATH,
382
  }
383
 
384
  @fastapi_app.get("/debug/env")
 
393
  "collection_prefix": COLLECTION_PREFIX,
394
  }
395
 
 
396
  @fastapi_app.get("/")
397
  async def root_redirect():
398
  return RedirectResponse(url=UI_PATH, status_code=307)
 
412
  async def index(req: IndexRequest):
413
  if not QDRANT_URL or not QDRANT_API_KEY:
414
  raise HTTPException(status_code=400, detail="QDRANT_URL / QDRANT_API_KEY requis")
415
+ job = create_and_start_job(req)
416
+ return {"job_id": job.job_id, "project_id": job.project_id}
 
 
 
 
417
 
418
  @fastapi_app.get("/status/{job_id}")
419
  async def status(job_id: str):
 
442
  return data
443
 
444
  # ------------------------------------------------------------------------------
445
+ # Gradio UI (avec auto-refresh)
446
  # ------------------------------------------------------------------------------
447
  def _default_two_docs() -> List[Dict[str, str]]:
448
  a = "Alpha bravo charlie delta echo foxtrot golf hotel india. " * 3
 
451
 
452
  async def ui_wipe(project: str):
453
  try:
454
+ resp = await wipe(project)
455
  return f"✅ Wipe ok — collection {resp['collection']} supprimée."
456
  except Exception as e:
457
  LOG.exception("wipe UI error")
 
468
  store_text=store_text,
469
  )
470
  try:
471
+ job = create_and_start_job(req) # ← lance dans le thread dédié
472
+ return f"🚀 Job lancé: {job.job_id}", job.job_id
 
 
473
  except ValidationError as ve:
474
  return f"❌ Payload invalide: {ve}", ""
475
  except Exception as e:
 
517
  LOG.exception("query UI error")
518
  return f"❌ Query erreur: {e}"
519
 
520
+ with gr.Blocks(title="Remote Indexer Tests sans console", analytics_enabled=False) as ui:
521
  gr.Markdown("## 🔬 Remote Indexer — Tests sans console\n"
522
  "Wipe → Index 2 docs → Status → Count → Query\n"
523
  f"- **Embeddings**: `{EMB_PROVIDER}` (model: `{HF_EMBED_MODEL}`)\n"
 
548
  query_btn = gr.Button("🔎 Query")
549
  query_out = gr.Textbox(lines=10, label="Résultats Query", interactive=False)
550
 
 
551
  wipe_btn.click(ui_wipe, inputs=[project_tb], outputs=[out_log])
 
552
  index_btn.click(ui_index_sample, inputs=[project_tb, chunk_size, overlap, batch_size, store_text], outputs=[out_log, jobid_tb])
553
  count_btn.click(ui_count, inputs=[project_tb], outputs=[out_log])
554
 
 
555
  status_btn.click(ui_status, inputs=[jobid_tb], outputs=[out_log])
 
 
556
  timer = gr.Timer(2.0, active=False)
557
  timer.tick(ui_status, inputs=[jobid_tb], outputs=[out_log])
558
  auto_chk.change(lambda x: gr.update(active=x), inputs=auto_chk, outputs=timer)
559
 
560
+ # Monte l'UI Gradio
561
+ fastapi_app.mount("/static", gr.routes.App.get_blocks().static_files) # pour servir les assets si nécessaire
562
  app = gr.mount_gradio_app(fastapi_app, ui, path=UI_PATH)
563
 
564
  if __name__ == "__main__":