amogaddy commited on
Commit
33d4a41
Β·
verified Β·
1 Parent(s): ef43e9f

Upload knowledge_base.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. knowledge_base.py +183 -0
knowledge_base.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import chromadb
2
+ from chromadb.utils.embedding_functions import SentenceTransformerEmbeddingFunction
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import shutil
7
+ from errors import get_logger, GenerAIError, ErrorCode, fmt_exc
8
+
9
+ log = get_logger("knowledge_base")
10
+
11
+ DB_PATH = "./database"
12
+ COUNTS_FILE = f"{DB_PATH}/query_counts.json"
13
+ SEEDED_FLAG = f"{DB_PATH}/.seeded_v2"
14
+
15
+ EMBED_MODEL = "paraphrase-multilingual-MiniLM-L12-v2"
16
+
17
+
18
+ def _load_counts() -> dict:
19
+ if os.path.exists(COUNTS_FILE):
20
+ try:
21
+ with open(COUNTS_FILE) as f:
22
+ data = json.load(f)
23
+ log.debug("Caricati %d contatori da %s", len(data), COUNTS_FILE)
24
+ return data
25
+ except Exception as e:
26
+ log.warning("[%s] Impossibile leggere %s β€” %s. Uso contatori vuoti.", ErrorCode.KB_INIT_FAILED.value, COUNTS_FILE, fmt_exc(e))
27
+ return {}
28
+ return {}
29
+
30
+
31
+ def _save_counts(counts: dict):
32
+ os.makedirs(DB_PATH, exist_ok=True)
33
+ try:
34
+ with open(COUNTS_FILE, "w") as f:
35
+ json.dump(counts, f)
36
+ except Exception as e:
37
+ log.error("[%s] Impossibile salvare contatori: %s", ErrorCode.KB_WRITE_FAILED.value, fmt_exc(e))
38
+
39
+
40
+ def _normalize(text: str) -> str:
41
+ return " ".join(text.lower().strip().split()[:12])
42
+
43
+
44
+ class KnowledgeBase:
45
+ ARCHIVE_THRESHOLD = 3
46
+
47
+ def __init__(self):
48
+ log.info("Inizializzazione KnowledgeBase in: %s", os.path.abspath(DB_PATH))
49
+ try:
50
+ os.makedirs(DB_PATH, exist_ok=True)
51
+ except Exception as e:
52
+ raise GenerAIError(ErrorCode.KB_INIT_FAILED, f"Impossibile creare la directory DB: {fmt_exc(e)}", cause=e)
53
+
54
+ # Wipe old schema if needed
55
+ if not os.path.exists(SEEDED_FLAG):
56
+ old_flag = f"{DB_PATH}/.seeded"
57
+ if os.path.exists(old_flag):
58
+ os.remove(old_flag)
59
+ log.info("Rimosso flag vecchio schema.")
60
+ chroma_dir = os.path.join(DB_PATH, "chroma.sqlite3")
61
+ if os.path.exists(chroma_dir):
62
+ os.remove(chroma_dir)
63
+ log.info("Rimosso database ChromaDB obsoleto.")
64
+
65
+ try:
66
+ log.debug("Caricamento modello embedding: %s", EMBED_MODEL)
67
+ ef = SentenceTransformerEmbeddingFunction(model_name=EMBED_MODEL)
68
+ self._client = chromadb.PersistentClient(path=DB_PATH)
69
+ self._col = self._client.get_or_create_collection(
70
+ name="generai",
71
+ embedding_function=ef,
72
+ metadata={"hnsw:space": "cosine"},
73
+ )
74
+ log.info("ChromaDB pronto. Documenti in memoria: %d", self._col.count())
75
+ except Exception as e:
76
+ raise GenerAIError(ErrorCode.KB_INIT_FAILED, f"ChromaDB non avviato: {fmt_exc(e)}", cause=e)
77
+
78
+ self._counts = _load_counts()
79
+ self._seed_if_needed()
80
+
81
+ def _seed_if_needed(self):
82
+ if os.path.exists(SEEDED_FLAG):
83
+ return
84
+ log.info("Seeding grammatica italiana...")
85
+ try:
86
+ from seed_italian import GRAMMAR_SEED
87
+ for item in GRAMMAR_SEED:
88
+ doc_id = hashlib.md5(item["q"].encode()).hexdigest()
89
+ self._col.upsert(
90
+ documents=[item["q"]],
91
+ ids=[doc_id],
92
+ metadatas=[{
93
+ "query": item["q"],
94
+ "answer": item["a"],
95
+ "source": "grammatica_italiana",
96
+ "feedback_score": 5,
97
+ }],
98
+ )
99
+ open(SEEDED_FLAG, "w").close()
100
+ log.info("Grammatica italiana caricata: %d regole.", len(GRAMMAR_SEED))
101
+ except Exception as e:
102
+ raise GenerAIError(ErrorCode.KB_SEED_FAILED, f"Seeding fallito: {fmt_exc(e)}", cause=e)
103
+
104
+ # ── Public API ─────────────────────────────────────────────────────────────
105
+
106
+ def search(self, query: str, n_results: int = 3) -> list[dict]:
107
+ count = self._col.count()
108
+ if count == 0:
109
+ log.debug("KB vuota, nessuna ricerca eseguita.")
110
+ return []
111
+ try:
112
+ res = self._col.query(
113
+ query_texts=[query],
114
+ n_results=min(n_results, count),
115
+ include=["metadatas", "distances"],
116
+ )
117
+ metas = res["metadatas"][0]
118
+ dists = res["distances"][0]
119
+ log.debug("KB search per %r β†’ %d risultati (best dist=%.3f)", query, len(metas), dists[0] if dists else -1)
120
+ return [
121
+ {"answer": metas[i].get("answer", ""), "metadata": metas[i], "distance": dists[i]}
122
+ for i in range(len(metas))
123
+ ]
124
+ except Exception as e:
125
+ err = GenerAIError(ErrorCode.KB_SEARCH_FAILED, f"Ricerca KB fallita: {fmt_exc(e)}", cause=e)
126
+ err.log(log)
127
+ return []
128
+
129
+ def increment_and_should_archive(self, query: str) -> bool:
130
+ key = _normalize(query)
131
+ self._counts[key] = self._counts.get(key, 0) + 1
132
+ _save_counts(self._counts)
133
+ count = self._counts[key]
134
+ log.debug("Query %r vista %d volte (threshold=%d)", key, count, self.ARCHIVE_THRESHOLD)
135
+ return count >= self.ARCHIVE_THRESHOLD
136
+
137
+ def get_query_count(self, query: str) -> int:
138
+ return self._counts.get(_normalize(query), 0)
139
+
140
+ def add(self, query: str, content: str, source_url: str = "") -> str:
141
+ doc_id = hashlib.md5(_normalize(query).encode()).hexdigest()
142
+ try:
143
+ self._col.upsert(
144
+ documents=[query],
145
+ ids=[doc_id],
146
+ metadatas=[{
147
+ "query": query[:300],
148
+ "answer": content[:4000],
149
+ "source": source_url[:500],
150
+ "feedback_score": 0,
151
+ }],
152
+ )
153
+ log.info("Documento archiviato (id=%s) per query: %r", doc_id[:8], query[:60])
154
+ except Exception as e:
155
+ err = GenerAIError(ErrorCode.KB_WRITE_FAILED, f"Impossibile salvare in KB: {fmt_exc(e)}", cause=e)
156
+ err.log(log)
157
+ return doc_id
158
+
159
+ def reinforce(self, doc_id: str, positive: bool):
160
+ action = "rinforzo positivo" if positive else "eliminazione"
161
+ log.info("Feedback (%s) per doc_id=%s", action, doc_id[:8])
162
+ try:
163
+ result = self._col.get(ids=[doc_id], include=["metadatas"])
164
+ if not result["ids"]:
165
+ log.warning("[%s] doc_id=%s non trovato in KB.", ErrorCode.KB_REINFORCE_FAILED.value, doc_id[:8])
166
+ return
167
+ if positive:
168
+ meta = result["metadatas"][0]
169
+ meta["feedback_score"] = meta.get("feedback_score", 0) + 1
170
+ self._col.update(ids=[doc_id], metadatas=[meta])
171
+ log.debug("feedback_score aggiornato a %d", meta["feedback_score"])
172
+ else:
173
+ self._col.delete(ids=[doc_id])
174
+ key = _normalize(result["metadatas"][0].get("query", ""))
175
+ self._counts.pop(key, None)
176
+ _save_counts(self._counts)
177
+ log.info("Documento eliminato dalla KB.")
178
+ except Exception as e:
179
+ err = GenerAIError(ErrorCode.KB_REINFORCE_FAILED, f"Feedback fallito: {fmt_exc(e)}", cause=e)
180
+ err.log(log)
181
+
182
+ def count(self) -> int:
183
+ return self._col.count()