amogaddy commited on
Commit
ef43e9f
Β·
verified Β·
1 Parent(s): 57fadb3

Upload brain.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. brain.py +212 -0
brain.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+ from knowledge_base import KnowledgeBase
4
+ from scraper import search_and_extract
5
+ from errors import get_logger, GenerAIError, ErrorCode, fmt_exc
6
+
7
+ log = get_logger("brain")
8
+
9
+ LOCAL_THRESHOLD = 0.40
10
+
11
+ # Modello HuggingFace da usare (fine-tunato o pubblico)
12
+ # Imposta HF_MODEL=tuo_username/generai-model nel file .env o come variabile d'ambiente
13
+ HF_MODEL = os.environ.get("HF_MODEL", "")
14
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
15
+
16
+
17
+ def _rerank(results: list, query: str) -> list:
18
+ words = set(query.lower().split())
19
+ for r in results:
20
+ stored_q = r["metadata"].get("query", "")
21
+ overlap = len(words & set(stored_q.lower().split()))
22
+ r["score"] = r["distance"] - overlap * 0.12
23
+ ranked = sorted(results, key=lambda x: x["score"])
24
+ if ranked:
25
+ log.debug("Rerank: best score=%.3f (dist=%.3f)", ranked[0]["score"], ranked[0]["distance"])
26
+ return ranked
27
+
28
+
29
+ def _extract_sentences(text: str, query: str, max_chars: int = 700) -> str:
30
+ words = set(query.lower().split())
31
+ sentences = [s.strip() for s in text.replace("\n", ". ").split(".") if len(s.strip()) > 20]
32
+ scored = [(sum(1 for w in words if w in s.lower()), s) for s in sentences]
33
+ scored.sort(key=lambda x: -x[0])
34
+ result = ""
35
+ for _, s in scored:
36
+ if len(result) + len(s) + 2 > max_chars:
37
+ break
38
+ result += s + ". "
39
+ return result.strip() or text[:max_chars]
40
+
41
+
42
+ # ── HuggingFace LLM client (opzionale) ────────────────────────────────────────
43
+
44
+ class _HFClient:
45
+ """Wrapper leggero per HuggingFace Inference API."""
46
+
47
+ SYSTEM = (
48
+ "Sei GenerAI, un assistente AI specializzato in lingua italiana. "
49
+ "Rispondi in modo chiaro, preciso e sempre in italiano. "
50
+ "Se ti viene fornito del contesto, basati su quello per rispondere."
51
+ )
52
+
53
+ def __init__(self, model: str, token: str):
54
+ try:
55
+ from huggingface_hub import InferenceClient
56
+ self._client = InferenceClient(model=model, token=token or None)
57
+ self._model = model
58
+ log.info("HuggingFace LLM pronto: %s", model)
59
+ except ImportError:
60
+ raise GenerAIError(
61
+ ErrorCode.BRAIN_ASK_FAILED,
62
+ "huggingface_hub non installato. Esegui: pip install huggingface-hub",
63
+ )
64
+ except Exception as e:
65
+ raise GenerAIError(
66
+ ErrorCode.BRAIN_ASK_FAILED,
67
+ f"Impossibile connettersi a HuggingFace ({model}): {fmt_exc(e)}",
68
+ cause=e,
69
+ )
70
+
71
+ def generate(self, question: str, context: str = "") -> str:
72
+ user_msg = question
73
+ if context:
74
+ user_msg = f"Contesto:\n{context}\n\nDomanda: {question}"
75
+
76
+ messages = [
77
+ {"role": "system", "content": self.SYSTEM},
78
+ {"role": "user", "content": user_msg},
79
+ ]
80
+ log.debug("HF generate β€” modello=%s, context_len=%d", self._model, len(context))
81
+ try:
82
+ response = self._client.chat_completion(
83
+ messages=messages,
84
+ max_tokens=512,
85
+ temperature=0.3,
86
+ )
87
+ return response.choices[0].message.content.strip()
88
+ except Exception as e:
89
+ raise GenerAIError(
90
+ ErrorCode.BRAIN_ASK_FAILED,
91
+ f"Generazione HF fallita: {fmt_exc(e)}",
92
+ cause=e,
93
+ )
94
+
95
+
96
+ # ── Brain ──────────────────────────────────────────────────────────────────────
97
+
98
+ class Brain:
99
+ def __init__(self):
100
+ log.info("Avvio Brain...")
101
+ try:
102
+ self.kb = KnowledgeBase()
103
+ except GenerAIError as e:
104
+ e.log(log)
105
+ raise
106
+
107
+ self._last_doc_id: str | None = None
108
+
109
+ # Carica client HF se configurato
110
+ self._hf: _HFClient | None = None
111
+ if HF_MODEL:
112
+ try:
113
+ self._hf = _HFClient(HF_MODEL, HF_TOKEN)
114
+ except GenerAIError as e:
115
+ e.log(log)
116
+ log.warning("Fallback alla ricerca semantica (HF non disponibile).")
117
+ else:
118
+ log.info("HF_MODEL non impostato β€” uso ricerca semantica locale.")
119
+
120
+ log.info("Brain pronto. ModalitΓ : %s", "LLM+KB" if self._hf else "KB+Web")
121
+
122
+ async def ask(self, question: str) -> tuple[str, str]:
123
+ """
124
+ Returns (answer, status).
125
+ status: "local" | "searched" | "archived" | "unknown" | "error" | "llm"
126
+ """
127
+ self._last_doc_id = None
128
+ log.info("Domanda: %r", question)
129
+
130
+ # ── 1. Ricerca KB locale ───────────────────────────────────────────────
131
+ try:
132
+ results = _rerank(self.kb.search(question, n_results=12), question)
133
+ except Exception as e:
134
+ log.warning("KB search fallita: %s", fmt_exc(e))
135
+ results = []
136
+
137
+ best_context = ""
138
+ best_source = ""
139
+
140
+ if results and results[0]["score"] < LOCAL_THRESHOLD:
141
+ best = results[0]
142
+ best_context = best["answer"]
143
+ best_source = best["metadata"].get("source", "")
144
+ log.info("Match KB locale (score=%.3f)", results[0]["score"])
145
+
146
+ # Se c'Γ¨ un LLM, usa il contesto KB per generare una risposta migliore
147
+ if self._hf:
148
+ return await self._llm_answer(question, best_context, best_source, status="llm")
149
+
150
+ answer = _extract_sentences(best_context, question)
151
+ if best_source and best_source != "grammatica_italiana":
152
+ answer += f"\n\n*Fonte: {best_source}*"
153
+ return answer, "local"
154
+
155
+ # ── 2. Ricerca web ─────────────────────────────────────────────────────
156
+ log.info("Nessun match locale β€” ricerca web...")
157
+ try:
158
+ web_results = await asyncio.to_thread(search_and_extract, question)
159
+ except Exception as e:
160
+ err = GenerAIError(ErrorCode.BRAIN_ASK_FAILED, f"Ricerca web fallita: {fmt_exc(e)}", cause=e)
161
+ err.log(log)
162
+ return err.user_message(), "error"
163
+
164
+ if not web_results:
165
+ log.warning("Nessun risultato web per: %r", question)
166
+ return (
167
+ "Non ho trovato informazioni su questo argomento nel mio database nΓ© sul web. "
168
+ "Prova a riformulare la domanda.",
169
+ "unknown",
170
+ )
171
+
172
+ combined = "\n\n---\n\n".join(f"[{r['title']}]\n{r['text']}" for r in web_results)
173
+ sources = ", ".join(r["url"] for r in web_results if r.get("url"))
174
+
175
+ # ── 3. Genera risposta (LLM o estrazione testuale) ─────────────────────
176
+ if self._hf:
177
+ answer, status = await self._llm_answer(question, combined, sources, status="llm")
178
+ else:
179
+ answer = _extract_sentences(combined, question)
180
+ if sources:
181
+ answer += f"\n\n*Fonte: {sources}*"
182
+ status = "searched"
183
+
184
+ return answer, status
185
+
186
+ async def _llm_answer(
187
+ self, question: str, context: str, source: str, status: str
188
+ ) -> tuple[str, str]:
189
+ try:
190
+ answer = await asyncio.to_thread(self._hf.generate, question, context)
191
+ if source and source != "grammatica_italiana":
192
+ answer += f"\n\n*Fonte: {source}*"
193
+ log.info("Risposta generata da LLM (%d chars)", len(answer))
194
+ return answer, status
195
+ except GenerAIError as e:
196
+ e.log(log)
197
+ # Fallback all'estrazione testuale
198
+ log.warning("Fallback a estrazione testuale.")
199
+ answer = _extract_sentences(context, question)
200
+ if source and source != "grammatica_italiana":
201
+ answer += f"\n\n*Fonte: {source}*"
202
+ return answer, "searched"
203
+
204
+ def give_feedback(self, positive: bool):
205
+ if self._last_doc_id:
206
+ self.kb.reinforce(self._last_doc_id, positive)
207
+ else:
208
+ log.debug("give_feedback chiamato senza _last_doc_id.")
209
+
210
+ @property
211
+ def kb_size(self) -> int:
212
+ return self.kb.count()