diff --git a/modules/EcmInvoiceOuts/ai/analysisAI.py b/modules/EcmInvoiceOuts/ai/analysisAI.py index 77327e87..dc56c30c 100644 --- a/modules/EcmInvoiceOuts/ai/analysisAI.py +++ b/modules/EcmInvoiceOuts/ai/analysisAI.py @@ -1,31 +1,224 @@ #!/usr/bin/env python3 -import os, sys, json +# -*- coding: utf-8 -*- + +""" +analysisAI.py — pobiera dane z MySQL, liczy preagregaty, renderuje HTML i dokłada analizę AI. + +ZMIENNE ŚRODOWISKOWE (mają domyślne wartości): + OPENAI_API_KEY - klucz do OpenAI (gdy pusty -> skrypt pokaże wersję bez AI) + OPENAI_MODEL - np. gpt-4.1 (domyślnie) + MYSQL_HOST - host MySQL (domyślnie: localhost) + MYSQL_USER - użytkownik MySQL (domyślnie: root) + MYSQL_PASSWORD - hasło MySQL (domyślnie: rootpassword) + MYSQL_DATABASE - nazwa bazy (domyślnie: preDb_0dcc87940d3655fa574b253df04ca1c3) + MYSQL_PORT - port MySQL (domyślnie: 3306) + PERIOD_FROM - data od (YYYY-MM-DD); gdy brak -> poprzedni pełny miesiąc + PERIOD_TO - data do (YYYY-MM-DD, exclusive); gdy brak -> pierwszy dzień bieżącego miesiąca + INVOICE_TYPE - typ dokumentu (domyślnie: normal) +""" + +import os, sys, json, math, time, warnings +from datetime import date, timedelta + +API_KEY = "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" + + +# Wycisz ostrzeżenie urllib3 (LibreSSL na macOS itp.) +try: + from urllib3.exceptions import NotOpenSSLWarning + warnings.filterwarnings("ignore", category=NotOpenSSLWarning) +except Exception: + pass + +import requests +import mysql.connector from preaggregates import compute_preaggregates, serialize_for_ai -try: - import mysql.connector -except Exception as e: - sys.stderr.write("MySQL connector not available: %s\n" % e) - sys.exit(1) +# --------- utils --------- -def getenv(key, default=None): - return os.environ.get(key, default) +def getenv(k, d=None): + return os.environ.get(k, d) + +def last_full_month_bounds(): + """Zwraca (from_iso, to_iso) dla poprzedniego pełnego miesiąca.""" + today_first = date.today().replace(day=1) + to_dt = today_first + prev_last = today_first - timedelta(days=1) + from_dt = prev_last.replace(day=1) + return from_dt.isoformat(), to_dt.isoformat() + +def compact_table(table, limit=30): + """Przytnij listę rekordów i znormalizuj liczby (NaN/Inf -> None).""" + out = [] + if not table: + return out + for i, row in enumerate(table): + if i >= int(limit): break + new = {} + for k, v in row.items(): + if isinstance(v, float): + new[k] = round(v, 6) if math.isfinite(v) else None + else: + new[k] = v + out.append(new) + return out + +def build_ai_payload(serialized, period_label): + """Kompaktowy JSON do AI (ograniczone rozmiary).""" + return { + "kpis_hint": {"period_label": period_label}, + "daily_sales": compact_table(serialized.get("daily_sales"), 30), + "product_summary": compact_table(serialized.get("product_summary"), 50), + "customer_summary": compact_table(serialized.get("customer_summary"), 50), + "top10_products_by_sales": compact_table(serialized.get("top10_products_by_sales"), 10), + "top10_customers_by_sales": compact_table(serialized.get("top10_customers_by_sales"), 10), + "product_daily_sample": compact_table(serialized.get("product_daily"), 40), + } + +def call_openai_chat(api_key, model, system_prompt, user_payload_json, + temperature=0.3, connect_timeout=10, read_timeout=90, max_retries=3): + """Wywołanie Chat Completions (retry + backoff). Zwraca HTML (sekcję) od AI.""" + url = "https://api.openai.com/v1/chat/completions" + headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} + body = { + "model": model, + "messages": [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": "Dane (JSON):\n\n" + user_payload_json}, + ], + "temperature": temperature, + # "max_tokens": 1200, # możesz odkomentować, aby ograniczyć długość odpowiedzi + } + last_err = None + for attempt in range(1, int(max_retries) + 1): + try: + r = requests.post(url, headers=headers, json=body, timeout=(connect_timeout, read_timeout)) + if 200 <= r.status_code < 300: + data = r.json() + return data.get("choices", [{}])[0].get("message", {}).get("content", "") + last_err = RuntimeError("OpenAI HTTP {}: {}".format(r.status_code, r.text)) + except requests.exceptions.RequestException as e: + last_err = e + time.sleep(min(2 ** attempt, 10)) + raise RuntimeError("OpenAI request failed: {}".format(last_err)) + +def fmt_money(v): + try: + return "{:,.2f}".format(float(v)).replace(",", " ").replace(".", ",") + except Exception: + return str(v) + +def html_table(records, title=None, max_rows=20): + """Proste generowanie tabeli HTML z listy dict-ów.""" + if not records: + return '
{}
{}