amogaddy commited on
Commit
01c9829
Β·
verified Β·
1 Parent(s): 141969f

Sync aggiornamento

Browse files
Files changed (1) hide show
  1. app.py +584 -40
app.py CHANGED
@@ -1,50 +1,594 @@
1
- from fastapi import FastAPI
2
- from transformers import pipeline
 
 
3
  import uvicorn
4
- from scraper import search_and_extract
 
 
5
 
6
  app = FastAPI()
 
 
 
 
 
 
 
7
 
8
- # Carichiamo il modello GenerAI
9
- print("Caricamento modello amogaddy/GenerAI...")
10
- generator = pipeline("text-generation", model="amogaddy/GenerAI")
11
 
12
- @app.get("/")
13
- def health_check():
14
- """Endpoint di health‑check."""
15
- return {"status": "running", "model": "GenerAI"}
16
 
17
- @app.post("/ask")
18
- def ask(prompt: str):
19
- """Endpoint principale per la generazione di testo con ricerca web (RAG)."""
20
- # 1. Ricerca sul web per usare internet come database
21
- web_results = search_and_extract(prompt, max_results=2)
22
-
23
- context = ""
24
- sources = []
25
- if web_results:
26
- for r in web_results:
27
- context += f"Fonte [{r['title']}]: {r['text'][:500]}\n"
28
- sources.append(r['url'])
29
-
30
- # 2. Creazione del prompt finale con il contesto trovato su internet
31
- if context:
32
- final_prompt = f"Contesto trovato su internet:\n{context}\n\nRispondi alla seguente domanda in base al contesto: {prompt}"
33
- else:
34
- final_prompt = prompt
35
-
36
- # 3. Generazione della risposta tramite il modello
37
- output = generator(final_prompt, max_length=512, num_return_sequences=1)
38
- answer = output[0]["generated_text"]
39
-
40
- # 4. Rimuoviamo la parte del prompt per restituire solo la risposta generata
41
- if final_prompt in answer:
42
- answer = answer.replace(final_prompt, "").strip()
43
-
44
- return {
45
- "result": answer,
46
- "sources": sources
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  }
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  if __name__ == "__main__":
50
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.responses import StreamingResponse, HTMLResponse, Response
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from pydantic import BaseModel
5
  import uvicorn
6
+ import asyncio
7
+ import json
8
+ from brain import Brain
9
 
10
  app = FastAPI()
11
+ app.add_middleware(
12
+ CORSMiddleware,
13
+ allow_origins=["*"],
14
+ allow_methods=["*"],
15
+ allow_headers=["*"],
16
+ )
17
+ brain: Brain | None = None
18
 
19
+ BASE = "https://amogaddy-generai.hf.space"
 
 
20
 
21
+ # ── Shared layout ──────────────────────────────────────────────────────────────
 
 
 
22
 
23
+ def page(title: str, content: str, active: str = "") -> str:
24
+ nav_items = [
25
+ ("/", "🏠", "Home"),
26
+ ("/chat", "πŸ’¬", "Chat"),
27
+ ("/instances/ui","🌐", "Istanze"),
28
+ ("/commands", "⚑", "Comandi"),
29
+ ("/docs", "πŸ“–", "API Docs"),
30
+ ]
31
+ nav_html = "".join(
32
+ f'<a href="{href}" class="nav-item{" active" if href == active else ""}">{icon} {label}</a>'
33
+ for href, icon, label in nav_items
34
+ )
35
+ return f"""<!DOCTYPE html>
36
+ <html lang="it">
37
+ <head>
38
+ <meta charset="UTF-8">
39
+ <meta name="viewport" content="width=device-width,initial-scale=1">
40
+ <title>GenerAI β€” {title}</title>
41
+ <style>
42
+ *{{box-sizing:border-box;margin:0;padding:0}}
43
+ body{{font-family:'Segoe UI',sans-serif;background:#0f1117;color:#e0e0e0;min-height:100vh}}
44
+ nav{{background:#13151f;border-bottom:1px solid #2a2d3e;display:flex;align-items:center;padding:0 24px;height:52px;gap:4px}}
45
+ nav .logo{{color:#7c9ef8;font-weight:700;font-size:18px;margin-right:20px}}
46
+ .nav-item{{color:#aaa;text-decoration:none;padding:6px 14px;border-radius:6px;font-size:14px;transition:.15s}}
47
+ .nav-item:hover{{background:#2a2d3e;color:#e0e0e0}}
48
+ .nav-item.active{{background:#7c9ef8;color:#000;font-weight:600}}
49
+ .content{{padding:32px;max-width:900px;margin:0 auto}}
50
+ h1{{color:#7c9ef8;margin-bottom:8px}}
51
+ .sub{{color:#888;margin-bottom:24px;font-size:14px}}
52
+ .card{{background:#1e2130;border-radius:10px;padding:20px;margin-bottom:16px}}
53
+ .card h2{{color:#a0c4ff;font-size:15px;margin-bottom:12px;border-left:3px solid #7c9ef8;padding-left:10px}}
54
+ .badge{{display:inline-block;padding:3px 10px;border-radius:12px;font-size:12px;font-weight:600}}
55
+ .badge.green{{background:#1a3a2a;color:#3cb371;border:1px solid #3cb371}}
56
+ .badge.red{{background:#3a1a1a;color:#e05555;border:1px solid #e05555}}
57
+ .badge.blue{{background:#1a2a3a;color:#7c9ef8;border:1px solid #7c9ef8}}
58
+ .btn{{display:inline-block;padding:8px 18px;border-radius:7px;text-decoration:none;font-size:13px;cursor:pointer;border:none;font-family:inherit}}
59
+ .btn-primary{{background:#7c9ef8;color:#000;font-weight:600}}
60
+ .btn-secondary{{background:#2a2d3e;color:#e0e0e0;border:1px solid #444}}
61
+ .btn:hover{{opacity:.85}}
62
+ input,textarea{{background:#13151f;border:1px solid #2a2d3e;color:#e0e0e0;border-radius:6px;padding:10px 12px;font-family:monospace;font-size:13px;width:100%}}
63
+ input:focus,textarea:focus{{outline:none;border-color:#7c9ef8}}
64
+ .cmd-text{{font-family:monospace;font-size:13px;background:#13151f;padding:10px 12px;border-radius:6px;word-break:break-all;color:#c8f0a0}}
65
+ .tab{{font-size:12px;padding:3px 10px;border-radius:4px;cursor:pointer;border:1px solid #444;background:#2a2d3e;color:#aaa}}
66
+ .tab.active{{background:#7c9ef8;color:#000;border-color:#7c9ef8}}
67
+ .copy-btn{{padding:5px 14px;background:#2a2d3e;border:1px solid #444;border-radius:5px;color:#aaa;cursor:pointer;font-size:12px;margin-top:8px}}
68
+ .copy-btn:hover{{background:#7c9ef8;color:#000}}
69
+ .copy-btn.copied{{background:#3cb371;color:#fff}}
70
+ table{{width:100%;border-collapse:collapse;font-size:13px}}
71
+ th{{color:#888;font-weight:500;padding:8px 12px;text-align:left;border-bottom:1px solid #2a2d3e}}
72
+ td{{padding:8px 12px;border-bottom:1px solid #1e2130}}
73
+ tr:hover td{{background:#1e2130}}
74
+ </style>
75
+ </head>
76
+ <body>
77
+ <nav>
78
+ <span class="logo">⚑ GenerAI</span>
79
+ {nav_html}
80
+ </nav>
81
+ <div class="content">
82
+ {content}
83
+ </div>
84
+ </body></html>"""
85
+
86
+
87
+ # ── Home ───────────────────────────────────────────────────────────────────────
88
+
89
+ @app.get("/", response_class=HTMLResponse)
90
+ async def home():
91
+ kb = brain.kb_size if brain else 0
92
+ status = "Online" if brain else "Avvio..."
93
+ badge = "green" if brain else "red"
94
+ content = f"""
95
+ <h1>GenerAI</h1>
96
+ <p class="sub">Assistente AI in italiano con memoria locale e ricerca web.</p>
97
+
98
+ <div class="card">
99
+ <h2>Stato sistema</h2>
100
+ <table>
101
+ <tr><th>Componente</th><th>Stato</th><th>Info</th></tr>
102
+ <tr><td>AI Brain</td><td><span class="badge {badge}">{status}</span></td><td>KB + Web search</td></tr>
103
+ <tr><td>Knowledge Base</td><td><span class="badge green">Online</span></td><td>{kb} documenti in memoria</td></tr>
104
+ <tr><td>Ricerca Web</td><td><span class="badge blue">SearXNG</span></td><td>Multi-istanza con fallback</td></tr>
105
+ </table>
106
+ </div>
107
+
108
+ <div class="card">
109
+ <h2>Accesso rapido</h2>
110
+ <div style="display:flex;gap:10px;flex-wrap:wrap">
111
+ <a href="/chat" class="btn btn-primary">πŸ’¬ Chat</a>
112
+ <a href="/instances/ui" class="btn btn-secondary">🌐 Istanze SearXNG</a>
113
+ <a href="/commands" class="btn btn-secondary">⚑ Comandi terminale</a>
114
+ <a href="/docs" class="btn btn-secondary">πŸ“– API Docs</a>
115
+ </div>
116
+ </div>
117
+ """
118
+ return HTMLResponse(page("Home", content, "/"))
119
+
120
+
121
+ # ── Chat ───────────────────────────────────────────────────────────────────────
122
+
123
+ @app.get("/chat", response_class=HTMLResponse)
124
+ def chat_page():
125
+ content = f"""
126
+ <h1>πŸ’¬ Chat</h1>
127
+ <p class="sub">Fai una domanda alla AI β€” vedrai i passaggi in tempo reale.</p>
128
+
129
+ <div class="card">
130
+ <textarea id="inp" rows="3" placeholder="Scrivi una domanda..."></textarea>
131
+ <div style="display:flex;gap:10px;margin-top:10px">
132
+ <button class="btn btn-primary" onclick="ask()">Invia</button>
133
+ <button class="btn btn-secondary" onclick="clear_()">Pulisci</button>
134
+ </div>
135
+ </div>
136
+
137
+ <div class="card" id="out-card" style="display:none">
138
+ <h2>Risposta</h2>
139
+ <div id="steps" style="font-size:13px;color:#888;margin-bottom:12px"></div>
140
+ <div id="result" style="white-space:pre-wrap;line-height:1.6"></div>
141
+ <div id="status-badge" style="margin-top:10px"></div>
142
+ </div>
143
+
144
+ <script>
145
+ function ask() {{
146
+ const q = document.getElementById('inp').value.trim();
147
+ if (!q) return;
148
+ document.getElementById('out-card').style.display = 'block';
149
+ document.getElementById('steps').innerHTML = '';
150
+ document.getElementById('result').innerHTML = '<span style="color:#888">...</span>';
151
+ document.getElementById('status-badge').innerHTML = '';
152
+
153
+ fetch('{BASE}/ask/stream', {{
154
+ method: 'POST',
155
+ headers: {{'Content-Type': 'application/json'}},
156
+ body: JSON.stringify({{prompt: q}})
157
+ }}).then(r => {{
158
+ const reader = r.body.getReader();
159
+ const dec = new TextDecoder();
160
+ function read() {{
161
+ reader.read().then(({{{{'done': done, 'value': value}}}}) => {{
162
+ if (done) return;
163
+ dec.decode(value).split('\\n').forEach(line => {{
164
+ if (!line.startsWith('data:')) return;
165
+ const d = JSON.parse(line.slice(5));
166
+ if (d.type === 'status') {{
167
+ document.getElementById('steps').innerHTML += d.message + '<br>';
168
+ }} else if (d.type === 'result') {{
169
+ document.getElementById('result').innerText = d.message;
170
+ const colors = {{local:'#3cb371', searched:'#7c9ef8', llm:'#f0a000', unknown:'#e05555', error:'#e05555'}};
171
+ const c = colors[d.status] || '#aaa';
172
+ document.getElementById('status-badge').innerHTML =
173
+ `<span class="badge" style="background:#1a1a2a;color:${{c}};border:1px solid ${{c}}">${{d.status}}</span>`;
174
+ }}
175
+ }});
176
+ read();
177
+ }});
178
+ }}
179
+ read();
180
+ }});
181
+ }}
182
+ function clear_() {{
183
+ document.getElementById('inp').value = '';
184
+ document.getElementById('out-card').style.display = 'none';
185
+ }}
186
+ document.getElementById('inp').addEventListener('keydown', e => {{
187
+ if (e.key === 'Enter' && e.ctrlKey) ask();
188
+ }});
189
+ </script>
190
+ """
191
+ return HTMLResponse(page("Chat", content, "/chat"))
192
+
193
+
194
+ # ── Istanze UI ─────────────────────���───────────────────────────────────────────
195
+
196
+ @app.get("/instances/ui", response_class=HTMLResponse)
197
+ def instances_ui():
198
+ content = f"""
199
+ <h1>🌐 Istanze SearXNG</h1>
200
+ <p class="sub">Testa tutte le istanze pubbliche e seleziona la piΓΉ veloce.</p>
201
+
202
+ <div class="card">
203
+ <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
204
+ <button class="btn btn-primary" onclick="testAll()">πŸ” Testa tutte</button>
205
+ <span id="scan-status" style="color:#888;font-size:13px"></span>
206
+ </div>
207
+ <div id="active-info" style="margin-top:12px;font-size:13px;color:#888"></div>
208
+ </div>
209
+
210
+ <div id="results-card" class="card" style="display:none">
211
+ <h2>Risultati <span id="count-badge"></span></h2>
212
+ <table>
213
+ <thead><tr><th>Istanza</th><th>Stato</th><th>VelocitΓ </th><th>Paese</th><th>Versione</th><th>Uptime</th><th>HTTPS</th><th>Motori</th><th>Azione</th></tr></thead>
214
+ <tbody id="inst-table"></tbody>
215
+ </table>
216
+ </div>
217
+
218
+ <script>
219
+ async function loadActive() {{
220
+ const r = await fetch('{BASE}/instances/active');
221
+ const d = await r.json();
222
+ document.getElementById('active-info').innerHTML =
223
+ 'Istanza attiva: <span style="color:#7c9ef8;font-family:monospace">' + d.active + '</span>';
224
+ }}
225
+
226
+ async function select(url) {{
227
+ const r = await fetch('{BASE}/instances/select?url=' + encodeURIComponent(url), {{method:'POST'}});
228
+ const d = await r.json();
229
+ if (d.ok) {{ alert('Impostata: ' + d.active); loadActive(); }}
230
+ else alert('Errore: ' + JSON.stringify(d));
231
+ }}
232
+
233
+ async function testAll() {{
234
+ document.getElementById('scan-status').innerText = 'Scansione in corso... (~30s)';
235
+ document.getElementById('results-card').style.display = 'none';
236
+ const r = await fetch('{BASE}/instances');
237
+ const d = await r.json();
238
+ document.getElementById('scan-status').innerText =
239
+ 'Completato: ' + d.online_with_results + ' online su ' + d.total_tested + ' testate';
240
+ document.getElementById('count-badge').innerHTML =
241
+ '<span class="badge green">' + d.online_with_results + ' online</span>';
242
+
243
+ const tbody = document.getElementById('inst-table');
244
+ tbody.innerHTML = '';
245
+ d.all.forEach(i => {{
246
+ const ok = i.online && i.results;
247
+ const color = ok ? '#3cb371' : i.online ? '#f0a000' : '#e05555';
248
+ const stato = ok ? 'βœ… Online' : i.online ? '⚠️ No risultati' : '❌ Offline';
249
+ const ms = i.ms ? i.ms + 'ms' : 'β€”';
250
+ const up = i.uptime_day != null ? i.uptime_day + '%' : 'β€”';
251
+ const grade = i.https_grade || 'β€”';
252
+ const gradeColor = grade.startsWith('A') ? '#3cb371' : grade === 'β€”' ? '#888' : '#f0a000';
253
+ const engines = (i.engines_live && i.engines_live.length
254
+ ? i.engines_live : i.engines_meta || []).join(', ') || 'β€”';
255
+ const btn = ok
256
+ ? `<button class="btn btn-secondary" style="font-size:11px;padding:3px 8px" onclick="select('${{i.url}}')">Seleziona</button>`
257
+ : 'β€”';
258
+ tbody.innerHTML += `<tr>
259
+ <td style="font-family:monospace;font-size:11px">${{i.url}}</td>
260
+ <td><span style="color:${{color}}">${{stato}}</span></td>
261
+ <td>${{ms}}</td>
262
+ <td>${{i.country || 'β€”'}}</td>
263
+ <td style="font-size:11px;color:#888">${{i.version || 'β€”'}}</td>
264
+ <td>${{up}}</td>
265
+ <td style="color:${{gradeColor}}">${{grade}}</td>
266
+ <td style="font-size:11px;color:#888;max-width:200px;overflow:hidden;text-overflow:ellipsis">${{engines}}</td>
267
+ <td>${{btn}}</td>
268
+ </tr>`;
269
+ }});
270
+ document.getElementById('results-card').style.display = 'block';
271
+ }}
272
+
273
+ loadActive();
274
+ </script>
275
+ """
276
+ return HTMLResponse(page("Istanze", content, "/instances/ui"))
277
+
278
+
279
+ # ── Comandi ────────────────────────────────────────────────────────────────────
280
+
281
+ @app.get("/commands", response_class=HTMLResponse)
282
+ def commands_page():
283
+ def cmd_block(bid, label, ps, cmd):
284
+ return f"""<div class="card" id="{bid}">
285
+ <div style="font-size:12px;color:#888;margin-bottom:8px">{label}</div>
286
+ <div style="display:flex;gap:6px;margin-bottom:8px">
287
+ <span class="tab active" id="{bid}-tab-ps" onclick="setTab('{bid}','ps')">PowerShell</span>
288
+ <span class="tab" id="{bid}-tab-cmd" onclick="setTab('{bid}','cmd')">CMD</span>
289
+ </div>
290
+ <div id="{bid}-ps" class="cmd-text">{ps}</div>
291
+ <div id="{bid}-cmd" class="cmd-text" style="display:none">{cmd}</div>
292
+ <div style="display:flex;gap:8px">
293
+ <button class="copy-btn" id="{bid}-btn-ps" onclick="copy('{bid}','ps')">Copia</button>
294
+ <button class="copy-btn" id="{bid}-btn-cmd" onclick="copy('{bid}','cmd')" style="display:none">Copia</button>
295
+ </div>
296
+ </div>"""
297
+
298
+ blocks = {
299
+ "Stato AI": [
300
+ cmd_block("b0","Controlla che la AI sia online",
301
+ f'Invoke-RestMethod "{BASE}/"',
302
+ f'curl "{BASE}/"'),
303
+ ],
304
+ "Chat": [
305
+ cmd_block("b1","Domanda (risposta completa)",
306
+ f'Invoke-RestMethod -Uri "{BASE}/ask" -Method POST -ContentType "application/json" -Body \'&lbrace;"prompt":"chi e napoleone?"&rbrace;\' | ConvertTo-Json',
307
+ f'curl -s -X POST "{BASE}/ask" -H "Content-Type: application/json" -d "{{\\"prompt\\":\\"chi e napoleone?\\"}}"'),
308
+ cmd_block("b2","Domanda in streaming (tempo reale)",
309
+ f'curl -N -X POST "{BASE}/ask/stream" -H "Content-Type: application/json" -d "{{\\"prompt\\":\\"chi e napoleone?\\"}}"',
310
+ f'curl -N -X POST "{BASE}/ask/stream" -H "Content-Type: application/json" -d "{{\\"prompt\\":\\"chi e napoleone?\\"}}"'),
311
+ ],
312
+ "Istanze SearXNG": [
313
+ cmd_block("b3","Testa tutte le istanze",
314
+ f'Invoke-RestMethod "{BASE}/instances" | ConvertTo-Json -Depth 5',
315
+ f'curl "{BASE}/instances"'),
316
+ cmd_block("b4","Vedi istanza attiva",
317
+ f'Invoke-RestMethod "{BASE}/instances/active"',
318
+ f'curl "{BASE}/instances/active"'),
319
+ cmd_block("b5","Seleziona istanza (cambia URL)",
320
+ f'Invoke-RestMethod -Uri "{BASE}/instances/select?url=https://searx.be" -Method POST',
321
+ f'curl -X POST "{BASE}/instances/select?url=https://searx.be"'),
322
+ ],
323
+ "Feedback": [
324
+ cmd_block("b6","Risposta utile",
325
+ f'Invoke-RestMethod -Uri "{BASE}/feedback?positive=true" -Method POST',
326
+ f'curl -X POST "{BASE}/feedback?positive=true"'),
327
+ cmd_block("b7","Risposta non utile",
328
+ f'Invoke-RestMethod -Uri "{BASE}/feedback?positive=false" -Method POST',
329
+ f'curl -X POST "{BASE}/feedback?positive=false"'),
330
+ ],
331
  }
332
 
333
+ sections_html = ""
334
+ for section, items in blocks.items():
335
+ icon = {"Stato AI":"🟒","Chat":"πŸ’¬","Istanze SearXNG":"🌐","Feedback":"πŸ‘"}.get(section,"")
336
+ sections_html += f'<div style="margin-bottom:28px"><h2 style="color:#a0c4ff;font-size:15px;margin-bottom:12px;border-left:3px solid #7c9ef8;padding-left:10px">{icon} {section}</h2>{"".join(items)}</div>'
337
+
338
+ content = f"""
339
+ <h1>⚑ Comandi terminale</h1>
340
+ <p class="sub">Scegli PowerShell o CMD, copia il comando. Oppure scarica lo script completo.</p>
341
+ <div style="display:flex;gap:10px;margin-bottom:28px;flex-wrap:wrap">
342
+ <a href="/scripts/powershell" download="generai.ps1" class="btn btn-primary">⬇ generai.ps1</a>
343
+ <a href="/scripts/batch" download="generai.bat" class="btn btn-secondary">⬇ generai.bat</a>
344
+ </div>
345
+
346
+ {sections_html}
347
+
348
+ <script>
349
+ function setTab(id, tab) {{
350
+ ['ps','cmd'].forEach(t => {{
351
+ document.getElementById(id+'-'+t).style.display = t===tab?'block':'none';
352
+ document.getElementById(id+'-btn-'+t).style.display = t===tab?'inline-block':'none';
353
+ document.getElementById(id+'-tab-'+t).classList.toggle('active', t===tab);
354
+ }});
355
+ }}
356
+ function copy(id, tab) {{
357
+ navigator.clipboard.writeText(document.getElementById(id+'-'+tab).innerText);
358
+ const btn = document.getElementById(id+'-btn-'+tab);
359
+ btn.innerText='Copiato!'; btn.classList.add('copied');
360
+ setTimeout(()=>{{btn.innerText='Copia';btn.classList.remove('copied')}}, 1500);
361
+ }}
362
+ </script>
363
+ """
364
+ return HTMLResponse(page("Comandi", content, "/commands"))
365
+
366
+
367
+ # ── Script downloads ───────────────────────────────────────────────────────────
368
+
369
+ @app.get("/scripts/powershell")
370
+ def script_ps():
371
+ script = f"""# GenerAI β€” Script PowerShell | .\generai.ps1
372
+ $BASE = "{BASE}"
373
+
374
+ function Ask($d) {{
375
+ $r = Invoke-RestMethod -Uri "$BASE/ask" -Method POST -ContentType "application/json" -Body (@{{prompt=$d}}|ConvertTo-Json)
376
+ Write-Host "`n[$($r.status)]" -ForegroundColor Cyan; Write-Host $r.result
377
+ }}
378
+ function AskStream($d) {{
379
+ curl -s -N -X POST "$BASE/ask/stream" -H "Content-Type: application/json" -d (@{{prompt=$d}}|ConvertTo-Json -Compress)
380
+ }}
381
+ function Stato {{ Invoke-RestMethod "$BASE/" | ConvertTo-Json }}
382
+ function Istanze {{
383
+ Write-Host "Scansione istanze..." -ForegroundColor Yellow
384
+ $r = Invoke-RestMethod "$BASE/instances"
385
+ Write-Host "Online: $($r.online_with_results)/$($r.total_tested)" -ForegroundColor Green
386
+ $r.best | ForEach-Object {{ Write-Host " $($_.url) $($_.ms)ms" -ForegroundColor Green }}
387
+ Write-Host "Attiva: $($r.active)" -ForegroundColor Cyan
388
+ }}
389
+ function Seleziona($url) {{
390
+ $r = Invoke-RestMethod -Uri "$BASE/instances/select?url=$url" -Method POST
391
+ Write-Host "Impostata: $($r.active)" -ForegroundColor Green
392
+ }}
393
+ function Feedback($p) {{
394
+ Invoke-RestMethod -Uri "$BASE/feedback?positive=$p" -Method POST | Out-Null
395
+ Write-Host "Feedback inviato!" -ForegroundColor Green
396
+ }}
397
+
398
+ while ($true) {{
399
+ Write-Host "`n===== GENERAI =====" -ForegroundColor Magenta
400
+ Write-Host "1. Domanda 2. Streaming 3. Stato 4. Istanze 5. Seleziona istanza 6/7. Feedback 0. Esci"
401
+ switch (Read-Host "Scelta") {{
402
+ "1" {{ Ask (Read-Host "Domanda") }}
403
+ "2" {{ AskStream (Read-Host "Domanda") }}
404
+ "3" {{ Stato }}
405
+ "4" {{ Istanze }}
406
+ "5" {{ Seleziona (Read-Host "URL istanza") }}
407
+ "6" {{ Feedback "true" }}
408
+ "7" {{ Feedback "false" }}
409
+ "0" {{ exit }}
410
+ default {{ Write-Host "Scelta non valida" -ForegroundColor Red }}
411
+ }}
412
+ }}
413
+ """
414
+ return Response(content=script, media_type="text/plain",
415
+ headers={"Content-Disposition": "attachment; filename=generai.ps1"})
416
+
417
+ @app.get("/scripts/batch")
418
+ def script_bat():
419
+ script = f"""@echo off
420
+ chcp 65001 >nul
421
+ set BASE={BASE}
422
+ :menu
423
+ echo.
424
+ echo ===== GENERAI =====
425
+ echo 1.Domanda 2.Streaming 3.Stato 4.Istanze 5.Seleziona istanza 0.Esci
426
+ set /p s=Scelta:
427
+ if "%s%"=="1" (set /p d=Domanda: & curl -s -X POST "%BASE%/ask" -H "Content-Type: application/json" -d "{{\\"prompt\\":\\"%d%\\"}}" & echo.) & goto menu
428
+ if "%s%"=="2" (set /p d=Domanda: & curl -N -X POST "%BASE%/ask/stream" -H "Content-Type: application/json" -d "{{\\"prompt\\":\\"%d%\\"}}" & echo.) & goto menu
429
+ if "%s%"=="3" (curl -s "%BASE%/" & echo.) & goto menu
430
+ if "%s%"=="4" (curl -s "%BASE%/instances" & echo.) & goto menu
431
+ if "%s%"=="5" (set /p u=URL: & curl -s -X POST "%BASE%/instances/select?url=%u%" & echo.) & goto menu
432
+ if "%s%"=="0" exit /b
433
+ goto menu
434
+ """
435
+ return Response(content=script, media_type="text/plain",
436
+ headers={"Content-Disposition": "attachment; filename=generai.bat"})
437
+
438
+
439
+ # ── API endpoints ──────────────────────────────────────────────────────────────
440
+
441
+ class AskRequest(BaseModel):
442
+ prompt: str
443
+
444
+ @app.post("/ask")
445
+ async def ask(req: AskRequest):
446
+ if not brain:
447
+ raise HTTPException(status_code=503, detail="Brain non ancora pronto")
448
+ answer, status = await brain.ask(req.prompt)
449
+ return {"result": answer, "status": status}
450
+
451
+ @app.post("/ask/stream")
452
+ async def ask_stream(req: AskRequest):
453
+ if not brain:
454
+ raise HTTPException(status_code=503, detail="Brain non ancora pronto")
455
+
456
+ async def generate():
457
+ queue: asyncio.Queue = asyncio.Queue()
458
+
459
+ async def on_status(msg: str):
460
+ await queue.put(("status", msg))
461
+
462
+ async def run():
463
+ answer, status = await brain.ask(req.prompt, on_status=on_status)
464
+ await queue.put(("result", answer, status))
465
+
466
+ task = asyncio.create_task(run())
467
+ while True:
468
+ item = await queue.get()
469
+ if item[0] == "status":
470
+ yield f"data: {json.dumps({'type':'status','message':item[1]})}\n\n"
471
+ elif item[0] == "result":
472
+ yield f"data: {json.dumps({'type':'result','message':item[1],'status':item[2]})}\n\n"
473
+ break
474
+ await task
475
+
476
+ return StreamingResponse(generate(), media_type="text/event-stream")
477
+
478
+ @app.get("/instances")
479
+ async def instances_json():
480
+ import requests as req, time
481
+ from scraper import get_active_instance
482
+
483
+ def fetch_list():
484
+ try:
485
+ r = req.get("https://searx.space/data/instances.json", timeout=10)
486
+ data = r.json().get("instances", {})
487
+ result = {}
488
+ for url, info in data.items():
489
+ if info.get("network_type", "normal") != "normal":
490
+ continue
491
+ result[url.rstrip("/")] = {
492
+ "version": info.get("version", "?"),
493
+ "network": info.get("network_type", "normal"),
494
+ "uptime": round(info.get("uptime", {}).get("uptimeDay", 0), 1) if isinstance(info.get("uptime"), dict) else None,
495
+ "engines": list(info.get("engines", {}).keys())[:8],
496
+ "country": info.get("network", {}).get("country", "?") if isinstance(info.get("network"), dict) else "?",
497
+ "https": info.get("tls", {}).get("grade", "?") if isinstance(info.get("tls"), dict) else "?",
498
+ }
499
+ return result
500
+ except Exception:
501
+ return {
502
+ "https://searx.be": {"version":"?","network":"normal","uptime":None,"engines":[],"country":"BE","https":"?"},
503
+ "https://search.bus-hit.me": {"version":"?","network":"normal","uptime":None,"engines":[],"country":"?","https":"?"},
504
+ "https://searxng.site": {"version":"?","network":"normal","uptime":None,"engines":[],"country":"?","https":"?"},
505
+ }
506
+
507
+ def check(url, meta):
508
+ try:
509
+ start = time.time()
510
+ r = req.get(f"{url}/search", params={"q":"test","format":"json"}, timeout=5,
511
+ headers={"User-Agent":"GenerAI/1.0"})
512
+ ms = int((time.time()-start)*1000)
513
+ online = r.status_code == 200
514
+ try:
515
+ results_data = r.json()
516
+ has_results = len(results_data.get("results", [])) > 0
517
+ engines_used = list({e for res in results_data.get("results", []) for e in res.get("engines", [])})
518
+ except Exception:
519
+ has_results = False
520
+ engines_used = []
521
+ return {
522
+ "url": url,
523
+ "online": online,
524
+ "results": has_results,
525
+ "ms": ms,
526
+ "version": meta.get("version", "?"),
527
+ "country": meta.get("country", "?"),
528
+ "uptime_day": meta.get("uptime"),
529
+ "https_grade": meta.get("https", "?"),
530
+ "engines_live": engines_used[:6],
531
+ "engines_meta": meta.get("engines", []),
532
+ "network": meta.get("network", "normal"),
533
+ }
534
+ except Exception as e:
535
+ return {
536
+ "url": url, "online": False, "results": False, "ms": None,
537
+ "version": meta.get("version","?"), "country": meta.get("country","?"),
538
+ "uptime_day": meta.get("uptime"), "https_grade": meta.get("https","?"),
539
+ "engines_live": [], "engines_meta": meta.get("engines",[]),
540
+ "network": meta.get("network","normal"), "error": str(e)[:60],
541
+ }
542
+
543
+ meta_map = await asyncio.to_thread(fetch_list)
544
+ checks = await asyncio.gather(*[asyncio.to_thread(check, u, m) for u, m in meta_map.items()])
545
+ srt = sorted(checks, key=lambda x: (not x["online"], not x.get("results"), x.get("ms") or 9999))
546
+ online = [c for c in srt if c["online"] and c.get("results")]
547
+ return {"active": get_active_instance(), "total_tested": len(srt),
548
+ "online_with_results": len(online), "best": online[:5], "all": srt}
549
+
550
+ @app.get("/instances/active")
551
+ def get_active():
552
+ from scraper import get_active_instance
553
+ return {"active": get_active_instance()}
554
+
555
+ @app.get("/engines")
556
+ def get_engines():
557
+ from scraper import get_engines
558
+ return {"engines": get_engines(), "available": ["brave","bing","google","duckduckgo","torch","yahoo","startpage","qwant"]}
559
+
560
+ @app.post("/engines")
561
+ def set_engines(engines: str):
562
+ from scraper import set_engines
563
+ set_engines(engines)
564
+ return {"ok": True, "engines": engines}
565
+
566
+ @app.post("/instances/select")
567
+ async def select_instance(url: str):
568
+ import requests as req
569
+ from scraper import set_active_instance
570
+ def test(u):
571
+ try:
572
+ r = req.get(f"{u}/search", params={"q":"test","format":"json"}, timeout=5,
573
+ headers={"User-Agent":"GenerAI/1.0"})
574
+ return r.status_code == 200
575
+ except Exception:
576
+ return False
577
+ if not await asyncio.to_thread(test, url):
578
+ raise HTTPException(status_code=400, detail=f"Istanza non raggiungibile: {url}")
579
+ set_active_instance(url)
580
+ return {"ok": True, "active": url}
581
+
582
+ @app.post("/feedback")
583
+ def feedback(positive: bool):
584
+ if brain:
585
+ brain.give_feedback(positive)
586
+ return {"ok": True}
587
+
588
+ @app.on_event("startup")
589
+ async def startup():
590
+ global brain
591
+ brain = Brain()
592
+
593
  if __name__ == "__main__":
594
  uvicorn.run(app, host="0.0.0.0", port=7860)