Changeset e4c61dd for server.py


Ignore:
Timestamp:
01/24/26 18:03:24 (6 months ago)
Author:
istevanoska <ilinastevanoska@…>
Branches:
master
Children:
a762898
Parents:
d42aac3
Message:

Prototype 1.1

File:
1 edited

Legend:

Unmodified
Added
Removed
  • server.py

    rd42aac3 re4c61dd  
    4747
    4848from flask_cors import CORS
    49 
    50 load_dotenv()
     49from dotenv import load_dotenv
     50from pathlib import Path
     51
     52BASE_DIR = Path(__file__).resolve().parent
     53load_dotenv(BASE_DIR / ".env")
     54
     55print("GRAFANA_API_TOKEN =", os.environ.get("GRAFANA_API_TOKEN"))
     56
    5157
    5258# ----------------------------
     
    6167JWT_ISSUER = os.environ.get("JWT_ISSUER", "netintel")
    6268COOKIE_SECURE = os.environ.get("COOKIE_SECURE", "0") == "1"  # set 1 only on HTTPS
     69GRAFANA_TOKEN = os.environ.get("GRAFANA_TOKEN", "")
    6370
    6471# CORS origins (add your LAN origins if needed)
     
    11201127# Simple RAG chat
    11211128# ----------------------------
    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)
    11831188def ask_llm_with_context(question, context):
    11841189    if not client:
    11851190        return "Немаш поставено OPENAI_API_KEY на серверот."
    11861191
    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}]}],
    11921205    )
     1206
     1207    text = getattr(resp, "output_text", "") or ""
     1208    return text.strip() or "Нема доволно информации во логовите."
     1209
     1210def 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()
     1222def 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
     1256def 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
     1279def 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
     1320def 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
     1395from urllib.parse import urlparse
     1396
     1397def _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()
     1408def 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
     1479def 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
     1553def 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
     1572def 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
     1609def 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 = """
     1622SQLite 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
     1648Scope (вметни во WHERE):
     1649- tenant_id = :tenant_id
     1650- env = :env
     1651- computer_name = :computer_name (ако не е null)
     1652
     1653Врати 1-3 SELECT/WITH queries за да се добијат потребните податоци.
     1654""".strip()
    11931655
    11941656    completion = client.chat.completions.create(
    11951657        model="gpt-4.1-mini",
    11961658        messages=[
    1197             {"role": "system", "content": system_msg},
    1198             {"role": "user", "content": f"Прашање: {question}\n\nЛогови:\n{context or 'Нема логови.'}"},
     1659            {"role": "system", "content": schema},
     1660            {"role": "user", "content": user_msg},
    11991661        ],
    1200         temperature=0.2,
     1662        temperature=0.1,
    12011663    )
    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        }
    12031697
    12041698
     
    12081702    try:
    12091703        tenant_id = request.user["tenant_id"]
     1704        env = request.headers.get("X-Env", "default")  # важно!
    12101705        data = request.get_json(force=True, silent=True) or {}
     1706
    12111707        question = (data.get("question") or "").strip()
    12121708        computer_name = data.get("computer_name") or None
     
    12151711            return jsonify({"error": "No question provided"}), 400
    12161712
    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
    12201728
    12211729    except Exception as e:
    12221730        traceback.print_exc()
    12231731        return jsonify({"error": str(e)}), 500
     1732
    12241733
    12251734
Note: See TracChangeset for help on using the changeset viewer.