0xReLogic 4 дней назад
Родитель
Сommit
b0d3fe0459
4 измененных файлов с 50 добавлено и 1 удалено
  1. 3 0
      .env.example
  2. 1 1
      requirements.txt
  3. 3 0
      src/config.py
  4. 43 0
      src/main.py

+ 3 - 0
.env.example

@@ -34,6 +34,9 @@ LEANN_BACKEND=hnsw
 LEANN_LAZY_BUILD=true
 LEANN_RECOMPUTE_ON_SEARCH=true
 LEANN_WARMUP_ON_START=false
+LEANN_IDLE_BUILD=false
+LEANN_IDLE_SECONDS=300
+LEANN_IDLE_CHECK_INTERVAL=60
 
 # Performance
 MAX_TEXT_LENGTH=10000

+ 1 - 1
requirements.txt

@@ -4,7 +4,7 @@
 # Core dependencies
 fastapi>=0.104.1,<1.0.0
 uvicorn[standard]>=0.24.0,<1.0.0
-sentence-transformers>=2.2.2,<3.0.0
+sentence-transformers>=2.2.2
 pydantic>=2.5.0,<3.0.0
 pydantic-settings>=2.1.0,<3.0.0
 python-dotenv>=1.0.0,<2.0.0

+ 3 - 0
src/config.py

@@ -45,6 +45,9 @@ class Settings(BaseSettings):
     leann_lazy_build: bool = True
     leann_recompute_on_search: bool = True
     leann_warmup_on_start: bool = False
+    leann_idle_build: bool = False
+    leann_idle_seconds: int = 300
+    leann_idle_check_interval: int = 60
 
     # Performance
     max_text_length: int = 10000

+ 43 - 0
src/main.py

@@ -68,6 +68,9 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
         except Exception as e:
             logger.warning("LEANN init failed: %s", e)
 
+    app.state.last_request_ts = time.time()
+    app.state.leann_idle_building = False
+
     # Optionally trigger background re-embedding for mismatched dimensions
     reembed_task = None
     if settings.auto_reembed_on_start:
@@ -96,6 +99,40 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
                 logger.error(f"Error in periodic cache save: {e}")
 
     cache_task = asyncio.create_task(periodic_cache_save())
+
+    idle_leann_task = None
+    if settings.leann_enabled and settings.leann_idle_build:
+
+        async def idle_leann_builder() -> None:
+            index_path = Path(settings.leann_index_path).expanduser().resolve()
+            meta_path = Path(f"{index_path}.meta.json")
+            while True:
+                await asyncio.sleep(settings.leann_idle_check_interval)
+                last_ts = getattr(app.state, "last_request_ts", None)
+                if last_ts is None:
+                    app.state.last_request_ts = time.time()
+                    continue
+                idle_for = time.time() - float(last_ts)
+                if idle_for < settings.leann_idle_seconds:
+                    continue
+                if app.state.leann_idle_building:
+                    continue
+                if not db.leann_dirty and meta_path.exists():
+                    continue
+                app.state.leann_idle_building = True
+                try:
+                    logger.info("LEANN idle build: starting (idle_for=%.1fs)", idle_for)
+                    built = await asyncio.to_thread(db.build_leann_index)
+                    if built:
+                        logger.info("LEANN idle build: completed")
+                    else:
+                        logger.info("LEANN idle build: skipped")
+                except Exception as e:
+                    logger.warning("LEANN idle build failed: %s", e)
+                finally:
+                    app.state.leann_idle_building = False
+
+        idle_leann_task = asyncio.create_task(idle_leann_builder())
     logger.info("Server ready!")
 
     yield
@@ -105,6 +142,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
     cache_task.cancel()
     if "reembed_task" in locals() and reembed_task:
         reembed_task.cancel()
+    if "idle_leann_task" in locals() and idle_leann_task:
+        idle_leann_task.cancel()
     embedding_service.save_cache()
     db.close()
 
@@ -140,6 +179,10 @@ async def log_requests(
 ) -> Response:
     """Log all requests with timing."""
     start_time = time.time()
+    try:
+        request.app.state.last_request_ts = start_time
+    except Exception:
+        pass
     error = False
 
     try: