Backup automatico script del 2026-09-27 07:00
This commit is contained in:
1 parent
f347badf3b
commit
09dcbbdadb
24 files changed
+1164
-193
No files matched your search
@@ -6,6 +6,8 @@ import logging
|
||||
import math
|
||||
import os
|
||||
import sqlite3
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
@@ -13,8 +15,8 @@ LOGGER = logging.getLogger("loogle_mcp.vector_store")
|
||||
VECTOR_SIZE = 768
|
||||
_local = sqlite3.connect(":memory:", check_same_thread=False) # placeholder
|
||||
_qdrant_client = None
|
||||
_qdrant_checked = False
|
||||
_use_fallback = False
|
||||
_next_qdrant_check = 0.0 # monotonic: prossimo tentativo di connessione mentre si usa il fallback
|
||||
_qdrant_lock = threading.Lock()
|
||||
|
||||
|
||||
def _fallback_path() -> str:
|
||||
@@ -41,28 +43,45 @@ def _fallback_conn() -> sqlite3.Connection:
|
||||
|
||||
|
||||
def _get_qdrant():
|
||||
global _qdrant_client, _qdrant_checked, _use_fallback
|
||||
if _qdrant_checked:
|
||||
return None if _use_fallback else _qdrant_client
|
||||
_qdrant_checked = True
|
||||
"""Client Qdrant, oppure None (fallback SQLite).
|
||||
|
||||
Se Qdrant non risponde si usa il fallback, ma si riprova ogni QDRANT_RETRY_S secondi
|
||||
invece di restare sul fallback fino al riavvio.
|
||||
"""
|
||||
global _next_qdrant_check
|
||||
if _qdrant_client is not None or time.monotonic() < _next_qdrant_check:
|
||||
return _qdrant_client
|
||||
with _qdrant_lock:
|
||||
if _qdrant_client is not None or time.monotonic() < _next_qdrant_check:
|
||||
return _qdrant_client
|
||||
client = _connect_qdrant()
|
||||
if client is None:
|
||||
_next_qdrant_check = time.monotonic() + float(os.environ.get("QDRANT_RETRY_S", "60"))
|
||||
return client
|
||||
|
||||
|
||||
def _connect_qdrant():
|
||||
global _qdrant_client
|
||||
url = os.environ.get("QDRANT_URL", "").strip()
|
||||
if not url:
|
||||
_use_fallback = True
|
||||
LOGGER.warning("QDRANT_URL non impostato — fallback SQLite")
|
||||
return None
|
||||
try:
|
||||
import httpx
|
||||
from qdrant_client import QdrantClient
|
||||
from qdrant_client.http import models as qm
|
||||
|
||||
client = QdrantClient(url=url, timeout=60)
|
||||
# Sonda breve: un tentativo con Qdrant giù non deve bloccare la ricerca per 20s
|
||||
httpx.get(f"{url.rstrip('/')}/readyz", timeout=3.0).raise_for_status()
|
||||
client = QdrantClient(url=url, timeout=20)
|
||||
client.get_collections()
|
||||
_qdrant_client = client
|
||||
globals()["qm"] = qm
|
||||
LOGGER.info("Qdrant connesso: %s", url)
|
||||
return client
|
||||
except Exception as exc:
|
||||
_use_fallback = True
|
||||
LOGGER.warning("Qdrant non disponibile (%s) — fallback SQLite", exc)
|
||||
LOGGER.warning("Qdrant non disponibile (%s) — fallback SQLite, nuovo tentativo tra %ss",
|
||||
exc, os.environ.get("QDRANT_RETRY_S", "60"))
|
||||
return None
|
||||
|
||||
|
||||
@@ -92,6 +111,7 @@ def ctx_collection(username: str) -> str:
|
||||
SHARED_COLLECTION = "kb_shared_family"
|
||||
GITEA_SHARED_COLLECTION = "gitea_shared_family"
|
||||
APPS_SHARED_COLLECTION = "apps_shared_family"
|
||||
CTX_SHARED_COLLECTION = "ctx_shared_family"
|
||||
|
||||
|
||||
def gitea_collection(username: str) -> str:
|
||||
@@ -138,6 +158,11 @@ def upsert_chunks(
|
||||
|
||||
|
||||
def delete_by_doc(collection: str, doc_id: int) -> None:
|
||||
delete_where(collection, "doc_id", doc_id)
|
||||
|
||||
|
||||
def delete_where(collection: str, key: str, value) -> None:
|
||||
"""Elimina i punti il cui payload[key] == value."""
|
||||
client = _get_qdrant()
|
||||
if client is not None:
|
||||
from qdrant_client.http import models as qm
|
||||
@@ -147,7 +172,7 @@ def delete_by_doc(collection: str, doc_id: int) -> None:
|
||||
collection_name=collection,
|
||||
points_selector=qm.FilterSelector(
|
||||
filter=qm.Filter(
|
||||
must=[qm.FieldCondition(key="doc_id", match=qm.MatchValue(value=doc_id))]
|
||||
must=[qm.FieldCondition(key=key, match=qm.MatchValue(value=value))]
|
||||
)
|
||||
),
|
||||
)
|
||||
@@ -156,11 +181,42 @@ def delete_by_doc(collection: str, doc_id: int) -> None:
|
||||
rows = conn.execute("SELECT id,payload FROM vectors WHERE collection=?", (collection,)).fetchall()
|
||||
for row_id, payload_raw in rows:
|
||||
payload = json.loads(payload_raw)
|
||||
if payload.get("doc_id") == doc_id:
|
||||
if payload.get(key) == value:
|
||||
conn.execute("DELETE FROM vectors WHERE id=?", (row_id,))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def delete_matching(collection: str, key: str, value, predicate) -> int:
|
||||
"""Elimina i punti con payload[key] == value per cui predicate(payload) è vero."""
|
||||
client = _get_qdrant()
|
||||
if client is not None:
|
||||
from qdrant_client.http import models as qm
|
||||
|
||||
ensure_collection(collection)
|
||||
flt = qm.Filter(must=[qm.FieldCondition(key=key, match=qm.MatchValue(value=value))])
|
||||
ids, offset = [], None
|
||||
while True:
|
||||
points, offset = client.scroll(
|
||||
collection_name=collection, scroll_filter=flt, limit=256, offset=offset, with_payload=True
|
||||
)
|
||||
ids.extend(p.id for p in points if predicate(p.payload or {}))
|
||||
if offset is None:
|
||||
break
|
||||
if ids:
|
||||
client.delete(collection_name=collection, points_selector=qm.PointIdsList(points=ids))
|
||||
return len(ids)
|
||||
conn = _fallback_conn()
|
||||
rows = conn.execute("SELECT id,payload FROM vectors WHERE collection=?", (collection,)).fetchall()
|
||||
removed = 0
|
||||
for row_id, payload_raw in rows:
|
||||
payload = json.loads(payload_raw)
|
||||
if payload.get(key) == value and predicate(payload):
|
||||
conn.execute("DELETE FROM vectors WHERE id=?", (row_id,))
|
||||
removed += 1
|
||||
conn.commit()
|
||||
return removed
|
||||
|
||||
|
||||
def count_by_doc(collection: str, doc_id: int) -> int:
|
||||
client = _get_qdrant()
|
||||
if client is not None:
|
||||
|
||||