- Timestamp:
- 01/24/26 18:03:24 (6 months ago)
- Branches:
- master
- Children:
- a762898
- Parents:
- d42aac3
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
server.py
rd42aac3 re4c61dd 47 47 48 48 from flask_cors import CORS 49 50 load_dotenv() 49 from dotenv import load_dotenv 50 from pathlib import Path 51 52 BASE_DIR = Path(__file__).resolve().parent 53 load_dotenv(BASE_DIR / ".env") 54 55 print("GRAFANA_API_TOKEN =", os.environ.get("GRAFANA_API_TOKEN")) 56 51 57 52 58 # ---------------------------- … … 61 67 JWT_ISSUER = os.environ.get("JWT_ISSUER", "netintel") 62 68 COOKIE_SECURE = os.environ.get("COOKIE_SECURE", "0") == "1" # set 1 only on HTTPS 69 GRAFANA_TOKEN = os.environ.get("GRAFANA_TOKEN", "") 63 70 64 71 # CORS origins (add your LAN origins if needed) … … 1120 1127 # Simple RAG chat 1121 1128 # ---------------------------- 1122 def build_rag_context(tenant_id: int, question: str, computer_name=None, limit_per_table=10): 1123 conn = db() 1124 c = conn.cursor() 1125 1126 params = [tenant_id] 1127 comp_clause = "" 1128 if computer_name: 1129 comp_clause = "AND c2.name = ?" 1130 params.append(computer_name) 1131 1132 # sysmon 1133 c.execute(f""" 1134 SELECT s.timestamp, s.event_id, s.event_type, s.message 1135 FROM sysmon_events s 1136 JOIN computers c2 ON c2.id = s.computer_id 1137 WHERE c2.tenant_id = ? {comp_clause} 1138 ORDER BY s.timestamp DESC 1139 LIMIT ? 1140 """, (*params, limit_per_table)) 1141 sysmon_rows = c.fetchall() 1142 1143 # perf 1144 c.execute(f""" 1145 SELECT h.timestamp, h.cpu_usage, h.ram_usage, h.disk_usage, h.network_sent_mb, h.network_recv_mb 1146 FROM computer_history h 1147 JOIN computers c2 ON c2.id = h.computer_id 1148 WHERE c2.tenant_id = ? {comp_clause} 1149 ORDER BY h.timestamp DESC 1150 LIMIT ? 1151 """, (*params, limit_per_table)) 1152 perf_rows = c.fetchall() 1153 1154 # proc 1155 c.execute(f""" 1156 SELECT p.timestamp, p.pid, p.name, p.cpu_percent, p.memory_mb 1157 FROM computer_processes_current p 1158 JOIN computers c2 ON c2.id = p.computer_id 1159 WHERE c2.tenant_id = ? {comp_clause} 1160 ORDER BY p.timestamp DESC 1161 LIMIT ? 1162 """, (*params, limit_per_table)) 1163 proc_rows = c.fetchall() 1164 1165 conn.close() 1166 1167 lines = [] 1168 for r in sysmon_rows: 1169 lines.append(f"[SYSMON] {r['timestamp']} | Event {r['event_id']} ({r['event_type']}): {r['message']}") 1170 for r in perf_rows: 1171 lines.append( 1172 f"[PERF] {r['timestamp']} | CPU {r['cpu_usage']}% | RAM {r['ram_usage']}% | Disk {r['disk_usage']}% | " 1173 f"Net sent {r['network_sent_mb']}MB / recv {r['network_recv_mb']}MB" 1174 ) 1175 for r in proc_rows: 1176 lines.append( 1177 f"[PROC] {r['timestamp']} | PID {r['pid']} | {r['name']} | CPU {r['cpu_percent']}% | RAM {r['memory_mb']}MB" 1178 ) 1179 1180 return "\n".join(lines) 1181 1182 1129 # def build_rag_context(tenant_id: int, question: str, computer_name=None, limit_per_table=10): 1130 # conn = db() 1131 # c = conn.cursor() 1132 # 1133 # params = [tenant_id] 1134 # comp_clause = "" 1135 # if computer_name: 1136 # comp_clause = "AND c2.name = ?" 1137 # params.append(computer_name) 1138 # 1139 # # sysmon 1140 # c.execute(f""" 1141 # SELECT s.timestamp, s.event_id, s.event_type, s.message 1142 # FROM sysmon_events s 1143 # JOIN computers c2 ON c2.id = s.computer_id 1144 # WHERE c2.tenant_id = ? {comp_clause} 1145 # ORDER BY s.timestamp DESC 1146 # LIMIT ? 1147 # """, (*params, limit_per_table)) 1148 # sysmon_rows = c.fetchall() 1149 # 1150 # # perf 1151 # c.execute(f""" 1152 # SELECT h.timestamp, h.cpu_usage, h.ram_usage, h.disk_usage, h.network_sent_mb, h.network_recv_mb 1153 # FROM computer_history h 1154 # JOIN computers c2 ON c2.id = h.computer_id 1155 # WHERE c2.tenant_id = ? {comp_clause} 1156 # ORDER BY h.timestamp DESC 1157 # LIMIT ? 1158 # """, (*params, limit_per_table)) 1159 # perf_rows = c.fetchall() 1160 # 1161 # # proc 1162 # c.execute(f""" 1163 # SELECT p.timestamp, p.pid, p.name, p.cpu_percent, p.memory_mb 1164 # FROM computer_processes_current p 1165 # JOIN computers c2 ON c2.id = p.computer_id 1166 # WHERE c2.tenant_id = ? {comp_clause} 1167 # ORDER BY p.timestamp DESC 1168 # LIMIT ? 1169 # """, (*params, limit_per_table)) 1170 # proc_rows = c.fetchall() 1171 # 1172 # conn.close() 1173 # 1174 # lines = [] 1175 # for r in sysmon_rows: 1176 # lines.append(f"[SYSMON] {r['timestamp']} | Event {r['event_id']} ({r['event_type']}): {r['message']}") 1177 # for r in perf_rows: 1178 # lines.append( 1179 # f"[PERF] {r['timestamp']} | CPU {r['cpu_usage']}% | RAM {r['ram_usage']}% | Disk {r['disk_usage']}% | " 1180 # f"Net sent {r['network_sent_mb']}MB / recv {r['network_recv_mb']}MB" 1181 # ) 1182 # for r in proc_rows: 1183 # lines.append( 1184 # f"[PROC] {r['timestamp']} | PID {r['pid']} | {r['name']} | CPU {r['cpu_percent']}% | RAM {r['memory_mb']}MB" 1185 # ) 1186 # 1187 # return "\n".join(lines) 1183 1188 def ask_llm_with_context(question, context): 1184 1189 if not client: 1185 1190 return "Немаш поставено OPENAI_API_KEY на серверот." 1186 1191 1187 system_msg = ( 1188 "Ти си асистент за безбедност и систем мониторинг. " 1189 "Одговараш базирано ИСКЛУЧИВО на дадените логови. " 1190 "Ако нема доволно податоци, кажи дека нема доволно информации. " 1191 "Кога корисникот прашува за процеси и нивна потрошувачка, користи ги [PROC] редовите." 1192 prompt = f"""Ти си асистент за безбедност и систем мониторинг. 1193 Одговарај базирано ИСКЛУЧИВО на дадените логови. 1194 Ако нема доволно податоци, кажи: 'Нема доволно информации во логовите.'. 1195 1196 Прашање: {question} 1197 1198 Логови: 1199 {context or 'Нема логови.'} 1200 """ 1201 1202 resp = client.responses.create( 1203 model="gpt-4.1-mini", 1204 input=[{"role": "user", "content": [{"type": "input_text", "text": prompt}]}], 1192 1205 ) 1206 1207 text = getattr(resp, "output_text", "") or "" 1208 return text.strip() or "Нема доволно информации во логовите." 1209 1210 def require_grafana(): 1211 def deco(fn): 1212 @wraps(fn) 1213 def wrap(*args, **kwargs): 1214 tok = request.headers.get("X-Grafana-Token", "") 1215 if not GRAFANA_TOKEN or tok != GRAFANA_TOKEN: 1216 return jsonify({"error": "Unauthorized"}), 401 1217 return fn(*args, **kwargs) 1218 return wrap 1219 return deco 1220 @app.route("/api/public/stats", methods=["GET"]) 1221 @require_grafana() 1222 def grafana_stats(): 1223 # ако сакаш само една околина: 1224 env = request.args.get("env", "default") 1225 1226 conn = db() 1227 c = conn.cursor() 1228 1229 # вкупно sysmon во 24h (за сите tenants, или ако сакаш само 1 tenant додади филтер) 1230 c.execute(""" 1231 SELECT COUNT(*) AS n 1232 FROM sysmon_events s 1233 JOIN computers c2 ON c2.id = s.computer_id 1234 WHERE s.timestamp > datetime('now','-24 hours') 1235 AND c2.env_name = ? 1236 """, (env,)) 1237 total_sysmon = int(c.fetchone()["n"] or 0) 1238 1239 c.execute(""" 1240 SELECT COUNT(*) AS n 1241 FROM network_connections n 1242 JOIN computers c2 ON c2.id = n.computer_id 1243 WHERE n.timestamp > datetime('now','-24 hours') 1244 AND c2.env_name = ? 1245 """, (env,)) 1246 total_net = int(c.fetchone()["n"] or 0) 1247 1248 conn.close() 1249 return jsonify({ 1250 "total_sysmon_events": total_sysmon, 1251 "total_network_connections": total_net, 1252 "server_time": datetime.now().isoformat(), 1253 "env": env, 1254 }) 1255 1256 def require_grafana_token(fn): 1257 @wraps(fn) 1258 def wrapper(*args, **kwargs): 1259 expected = os.environ.get("GRAFANA_API_TOKEN") or "" 1260 1261 # 1) пробај Authorization: Bearer <token> 1262 auth = request.headers.get("Authorization", "") 1263 token = "" 1264 if auth.startswith("Bearer "): 1265 token = auth.replace("Bearer ", "").strip() 1266 1267 # 2) ако нема header, пробај query param ?token= 1268 if not token: 1269 token = (request.args.get("token") or "").strip() 1270 1271 if not expected or token != expected: 1272 return jsonify({"error": "Invalid Grafana token"}), 401 1273 1274 return fn(*args, **kwargs) 1275 return wrapper 1276 1277 @app.route("/api/public/ips", methods=["GET"]) 1278 @require_grafana_token 1279 def api_public_ips(): 1280 tenant_id = request.args.get("tenant_id", type=int) 1281 env = request.args.get("env", "default") 1282 hours = int(request.args.get("hours", "24")) 1283 limit = int(request.args.get("limit", "200")) 1284 1285 if not tenant_id: 1286 return jsonify({"error": "Missing tenant_id"}), 400 1287 1288 conn = db() 1289 c = conn.cursor() 1290 1291 c.execute(""" 1292 SELECT DISTINCT 1293 substr(nc.remote_address, 1, 1294 CASE 1295 WHEN instr(nc.remote_address, ':') > 0 1296 THEN instr(nc.remote_address, ':') - 1 1297 ELSE length(nc.remote_address) 1298 END 1299 ) AS ip 1300 FROM network_connections nc 1301 JOIN computers c2 ON c2.id = nc.computer_id 1302 WHERE c2.tenant_id = ? 1303 AND c2.env_name = ? 1304 AND nc.timestamp > datetime('now', ?) 1305 AND nc.remote_address IS NOT NULL 1306 AND nc.remote_address != '' 1307 ORDER BY ip 1308 LIMIT ? 1309 """, (tenant_id, env, f"-{hours} hours", limit)) 1310 1311 ips = [r["ip"] for r in c.fetchall()] 1312 conn.close() 1313 1314 return jsonify({"ips": ips}) 1315 1316 1317 1318 @app.route("/api/public/sankey", methods=["GET"]) 1319 @require_grafana_token 1320 def api_public_sankey(): 1321 tenant_id = int(request.args.get("tenant_id", "1")) 1322 env = request.args.get("env", "office1") 1323 hours = int(request.args.get("hours", "24")) 1324 limit = int(request.args.get("limit", "20")) 1325 1326 computers = (request.args.get("computers") or "all").strip() 1327 ips = (request.args.get("ips") or "all").strip() 1328 1329 # normalize grafana All values 1330 if computers in ("", "__all"): 1331 computers = "all" 1332 if ips in ("", "__all"): 1333 ips = "all" 1334 1335 comp_list = [c.strip() for c in computers.split(",") if c.strip()] if computers != "all" else [] 1336 ip_list = [i.strip() for i in ips.split(",") if i.strip()] if ips != "all" else [] 1337 1338 comp_clause = "" 1339 ip_clause = "" 1340 params = [tenant_id, env, f"-{hours} hours"] 1341 1342 # ✅ target без port за IPv4: "1.2.3.4:443" -> "1.2.3.4" 1343 # ⚠️ за IPv6 ќе го остави како што е (за да не го расипеме со split по ':') 1344 target_expr = """ 1345 CASE 1346 WHEN nc.remote_address LIKE '%:%' AND nc.remote_address NOT LIKE '%:%:%' 1347 THEN substr(nc.remote_address, 1, instr(nc.remote_address, ':') - 1) 1348 ELSE nc.remote_address 1349 END 1350 """ 1351 1352 if comp_list: 1353 comp_clause = "AND comp.name IN (" + ",".join(["?"] * len(comp_list)) + ")" 1354 params.extend(comp_list) 1355 1356 # ✅ Филтер по IP без порт (истиот expr) 1357 if ip_list: 1358 ip_clause = f"AND ({target_expr}) IN (" + ",".join(["?"] * len(ip_list)) + ")" 1359 params.extend(ip_list) 1360 1361 # LIMIT на крај 1362 params.append(limit) 1363 1364 conn = db() 1365 c = conn.cursor() 1366 1367 c.execute(f""" 1368 SELECT 1369 comp.name AS source, 1370 ({target_expr}) AS target, 1371 COUNT(*) AS value 1372 FROM network_connections nc 1373 JOIN computers comp ON comp.id = nc.computer_id 1374 WHERE comp.tenant_id = ? 1375 AND comp.env_name = ? 1376 AND nc.timestamp > datetime('now', ?) 1377 AND nc.remote_address IS NOT NULL 1378 AND nc.remote_address != '' 1379 1380 -- ✅ тргни loopback spam (овие прават да изгледа како да нема линии) 1381 AND nc.remote_address NOT LIKE '127.0.0.1%' 1382 AND nc.remote_address NOT LIKE '::1%' 1383 1384 {comp_clause} 1385 {ip_clause} 1386 GROUP BY comp.name, target 1387 ORDER BY value DESC 1388 LIMIT ? 1389 """, params) 1390 1391 rows = [dict(r) for r in c.fetchall()] 1392 conn.close() 1393 return jsonify(rows) 1394 1395 from urllib.parse import urlparse 1396 1397 def _ip_only(addr: str): 1398 # addr може да е "1.2.3.4:443" или "hostname:port" 1399 if not addr: 1400 return None 1401 # ако има повеќе ':' (ipv6), ќе го оставиме целото; за ipv4 split е ок 1402 if addr.count(":") == 1: 1403 return addr.split(":")[0] 1404 return addr 1405 1406 @app.route("/api/graph/net", methods=["GET"]) 1407 @require_user() 1408 def api_graph_net(): 1409 tenant_id = request.user["tenant_id"] 1410 env = request.args.get("env") or request.headers.get("X-Env", "default") 1411 computer = request.args.get("computer") # optional 1412 since_hours = int(request.args.get("since_hours") or 24) 1413 1414 conn = db() 1415 c = conn.cursor() 1416 1417 params = [tenant_id, env, since_hours] 1418 comp_clause = "" 1419 if computer: 1420 comp_clause = "AND c2.name = ?" 1421 params.append(computer) 1422 1423 # Вади top remote IP по број на конекции 1424 c.execute(f""" 1425 SELECT 1426 c2.name AS computer_name, 1427 n.remote_address AS remote_address, 1428 COUNT(*) AS cnt 1429 FROM network_connections n 1430 JOIN computers c2 ON c2.id = n.computer_id 1431 WHERE c2.tenant_id = ? 1432 AND c2.env_name = ? 1433 AND n.timestamp > datetime('now', '-' || ? || ' hours') 1434 {comp_clause} 1435 AND n.remote_address IS NOT NULL 1436 AND n.remote_address <> '' 1437 GROUP BY c2.name, n.remote_address 1438 ORDER BY cnt DESC 1439 LIMIT 200 1440 """, params) 1441 1442 rows = c.fetchall() 1443 conn.close() 1444 1445 # Build NodeGraph-like structure 1446 nodes = {} 1447 edges = [] 1448 1449 def add_node(node_id, title, ntype): 1450 if node_id not in nodes: 1451 nodes[node_id] = {"id": node_id, "title": title, "subTitle": ntype} 1452 1453 for r in rows: 1454 comp_name = r["computer_name"] 1455 remote = _ip_only(r["remote_address"]) or r["remote_address"] 1456 cnt = int(r["cnt"] or 0) 1457 1458 comp_id = f"comp:{comp_name}" 1459 ip_id = f"ip:{remote}" 1460 1461 add_node(comp_id, comp_name, "computer") 1462 add_node(ip_id, remote, "remote_ip") 1463 1464 edges.append({ 1465 "id": f"{comp_id}->{ip_id}", 1466 "source": comp_id, 1467 "target": ip_id, 1468 "mainStat": str(cnt), 1469 }) 1470 1471 return jsonify({ 1472 "nodes": list(nodes.values()), 1473 "edges": edges, 1474 "meta": {"env": env, "computer": computer, "since_hours": since_hours} 1475 }) 1476 1477 @app.route("/api/public/netgraph", methods=["GET"]) 1478 @require_grafana_token 1479 def api_public_netgraph(): 1480 tenant_id = int(request.args.get("tenant_id", "1")) 1481 env = request.args.get("env", "default") 1482 hours = int(request.args.get("hours", "6")) 1483 limit_edges = int(request.args.get("limit", "200")) 1484 computer = request.args.get("computer") # <-- NEW (optional) 1485 1486 conn = db() 1487 c = conn.cursor() 1488 1489 comp_clause = "" 1490 params = [tenant_id, env, f"-{hours} hours"] 1491 1492 if computer and computer != "all": 1493 comp_clause = "AND comp.name = ?" 1494 params.append(computer) 1495 1496 params.append(limit_edges) 1497 1498 c.execute(f""" 1499 SELECT 1500 comp.name AS src, 1501 nc.remote_address AS remote, 1502 COUNT(*) AS cnt 1503 FROM network_connections nc 1504 JOIN computers comp ON comp.id = nc.computer_id 1505 WHERE comp.tenant_id = ? 1506 AND comp.env_name = ? 1507 AND nc.timestamp > datetime('now', ?) 1508 {comp_clause} 1509 AND nc.remote_address IS NOT NULL 1510 AND nc.remote_address != '' 1511 GROUP BY comp.name, nc.remote_address 1512 ORDER BY cnt DESC 1513 LIMIT ? 1514 """, params) 1515 1516 rows = c.fetchall() 1517 conn.close() 1518 1519 def ip_only(addr: str) -> str: 1520 if not addr: 1521 return "" 1522 if addr.startswith("[") and "]" in addr: 1523 return addr[1:addr.index("]")] 1524 if ":" in addr: 1525 return addr.split(":")[0] 1526 return addr 1527 1528 nodes = {} 1529 edges = [] 1530 1531 for r in rows: 1532 src = r["src"] 1533 remote_ip = ip_only(r["remote"]) 1534 cnt = int(r["cnt"] or 0) 1535 if not remote_ip: 1536 continue 1537 1538 nodes[src] = {"id": src, "title": src, "subTitle": env, "mainStat": "PC"} 1539 ip_id = f"IP:{remote_ip}" 1540 if ip_id not in nodes: 1541 nodes[ip_id] = {"id": ip_id, "title": remote_ip, "subTitle": "remote", "mainStat": "IP"} 1542 1543 edges.append({ 1544 "id": f"{src}->{ip_id}", 1545 "source": src, 1546 "target": ip_id, 1547 "mainStat": cnt 1548 }) 1549 1550 return jsonify({"nodes": list(nodes.values()), "edges": edges}) 1551 @app.route("/api/public/computers", methods=["GET"]) 1552 @require_grafana_token 1553 def api_public_computers(): 1554 tenant_id = int(request.args.get("tenant_id", "1")) 1555 env = request.args.get("env", "default") 1556 1557 conn = db() 1558 c = conn.cursor() 1559 c.execute(""" 1560 SELECT name 1561 FROM computers 1562 WHERE tenant_id = ? AND env_name = ? 1563 ORDER BY name 1564 """, (tenant_id, env)) 1565 names = [r["name"] for r in c.fetchall()] 1566 conn.close() 1567 1568 return jsonify({"computers": names}) 1569 1570 @app.route("/api/public/stats", methods=["GET"]) 1571 @require_grafana_token 1572 def public_stats(): 1573 # ⚠️ ако сакаш tenant по tenant, ќе мора да додадеш tenant_id како query param, 1574 # или да имаш 1 tenant во проект за сега. 1575 tenant_id = request.args.get("tenant_id", type=int) 1576 if not tenant_id: 1577 return jsonify({"error": "Missing tenant_id"}), 400 1578 1579 conn = db() 1580 c = conn.cursor() 1581 1582 c.execute(""" 1583 SELECT COUNT(*) AS n 1584 FROM sysmon_events s 1585 JOIN computers c2 ON c2.id = s.computer_id 1586 WHERE s.timestamp > datetime('now','-24 hours') 1587 AND c2.tenant_id = ? 1588 """, (tenant_id,)) 1589 total_sysmon = int(c.fetchone()["n"] or 0) 1590 1591 c.execute(""" 1592 SELECT COUNT(*) AS n 1593 FROM network_connections n 1594 JOIN computers c2 ON c2.id = n.computer_id 1595 WHERE n.timestamp > datetime('now','-24 hours') 1596 AND c2.tenant_id = ? 1597 """, (tenant_id,)) 1598 total_net = int(c.fetchone()["n"] or 0) 1599 1600 conn.close() 1601 1602 return jsonify({ 1603 "total_sysmon_events": total_sysmon, 1604 "total_network_connections": total_net, 1605 "server_time": datetime.now().isoformat(), 1606 }) 1607 1608 1609 def ask_llm_sql_generator(question: str, tenant_id: int, env: str, computer_name: str | None): 1610 """ 1611 Returns dict: { "queries": [...], "notes": "...", "scope": {...} } 1612 Only SELECT/WITH queries, no modifications. 1613 """ 1614 if not client: 1615 return { 1616 "queries": [], 1617 "notes": "Немаш поставено OPENAI_API_KEY на серверот.", 1618 "scope": {"tenant_id": tenant_id, "env": env, "computer_name": computer_name}, 1619 } 1620 1621 schema = """ 1622 SQLite schema (важни табели): 1623 - computers(id, tenant_id, env_name, name, user, ip, os, first_seen, last_seen, sysmon_available) 1624 - computer_history(computer_id, cpu_usage, ram_usage, disk_usage, network_sent_mb, network_recv_mb, timestamp) 1625 - computer_processes_current(computer_id, pid, name, cpu_percent, memory_mb, username, cmdline, timestamp) 1626 - computer_processes_history(computer_id, pid, name, cpu_percent, memory_mb, username, cmdline, timestamp) 1627 - sysmon_events(computer_id, event_id, event_type, message, timestamp, details) 1628 - network_connections(computer_id, pid, local_address, remote_address, status, process_name, timestamp) 1629 1630 Правила: 1631 - Врати САМО валиден JSON. 1632 - JSON формат: {"queries":[...], "notes":"..."}. 1633 - queries мора да бидат само SELECT или WITH (никако INSERT/UPDATE/DELETE/ALTER/DROP). 1634 - максимум 3 queries. 1635 - користи placeholders: :tenant_id, :env, :computer_name (ако е дадено). 1636 - ако нема доволно инфо -> queries празно и notes објаснува. 1637 """.strip() 1638 1639 user_scope = { 1640 "tenant_id": tenant_id, 1641 "env": env, 1642 "computer_name": computer_name, 1643 } 1644 1645 user_msg = f""" 1646 Прашање од корисник: {question} 1647 1648 Scope (вметни во WHERE): 1649 - tenant_id = :tenant_id 1650 - env = :env 1651 - computer_name = :computer_name (ако не е null) 1652 1653 Врати 1-3 SELECT/WITH queries за да се добијат потребните податоци. 1654 """.strip() 1193 1655 1194 1656 completion = client.chat.completions.create( 1195 1657 model="gpt-4.1-mini", 1196 1658 messages=[ 1197 {"role": "system", "content": s ystem_msg},1198 {"role": "user", "content": f"Прашање: {question}\n\nЛогови:\n{context or 'Нема логови.'}"},1659 {"role": "system", "content": schema}, 1660 {"role": "user", "content": user_msg}, 1199 1661 ], 1200 temperature=0. 2,1662 temperature=0.1, 1201 1663 ) 1202 return completion.choices[0].message.content 1664 1665 raw = completion.choices[0].message.content or "" 1666 1667 # Safe parse JSON 1668 try: 1669 out = json.loads(raw) 1670 if not isinstance(out, dict): 1671 raise ValueError("Not an object") 1672 queries = out.get("queries") or [] 1673 notes = out.get("notes") or "" 1674 1675 # hard safety filter: keep only SELECT/WITH 1676 safe_queries = [] 1677 for q in queries: 1678 if not isinstance(q, str): 1679 continue 1680 qs = q.strip().lower() 1681 if qs.startswith("select") or qs.startswith("with"): 1682 # block obvious multi-statement 1683 if ";" in q.strip().rstrip(";"): 1684 continue 1685 safe_queries.append(q.strip()) 1686 1687 return {"queries": safe_queries[:3], "notes": notes, "scope": user_scope} 1688 1689 except Exception: 1690 # fallback ако AI не врати JSON 1691 return { 1692 "queries": [], 1693 "notes": "AI не врати валиден JSON. Обиди се повторно (или намали прашање).", 1694 "scope": user_scope, 1695 "raw": raw[:1200], 1696 } 1203 1697 1204 1698 … … 1208 1702 try: 1209 1703 tenant_id = request.user["tenant_id"] 1704 env = request.headers.get("X-Env", "default") # важно! 1210 1705 data = request.get_json(force=True, silent=True) or {} 1706 1211 1707 question = (data.get("question") or "").strip() 1212 1708 computer_name = data.get("computer_name") or None … … 1215 1711 return jsonify({"error": "No question provided"}), 400 1216 1712 1217 ctx = build_rag_context(tenant_id, question, computer_name=computer_name, limit_per_table=10) 1218 ans = ask_llm_with_context(question, ctx) 1219 return jsonify({"answer": ans or ""}), 200 1713 out = ask_llm_sql_generator( 1714 question=question, 1715 tenant_id=tenant_id, 1716 env=env, 1717 computer_name=computer_name, 1718 ) 1719 1720 # Тука не извршуваме SQL, само враќаме query за копирање. 1721 return jsonify({ 1722 "ok": True, 1723 "queries": out.get("queries", []), 1724 "notes": out.get("notes", ""), 1725 "scope": out.get("scope", {}), 1726 # "raw": out.get("raw") # ако сакаш debug 1727 }), 200 1220 1728 1221 1729 except Exception as e: 1222 1730 traceback.print_exc() 1223 1731 return jsonify({"error": str(e)}), 500 1732 1224 1733 1225 1734
Note:
See TracChangeset
for help on using the changeset viewer.
