From a1ddb05402f1ed10888e91ee67f6294650f0ba13 Mon Sep 17 00:00:00 2001 From: zzdrojewskipaw Date: Sun, 7 Sep 2025 20:41:59 +0200 Subject: [PATCH] analiza AI rozbudowana --- modules/EcmInvoiceOuts/ai/analysisAI.py | 136 +++++++++++++++--------- 1 file changed, 84 insertions(+), 52 deletions(-) diff --git a/modules/EcmInvoiceOuts/ai/analysisAI.py b/modules/EcmInvoiceOuts/ai/analysisAI.py index 883361ab..337346e6 100644 --- a/modules/EcmInvoiceOuts/ai/analysisAI.py +++ b/modules/EcmInvoiceOuts/ai/analysisAI.py @@ -2,43 +2,47 @@ # -*- coding: utf-8 -*- """ -analysisAI.py — pobiera dane z MySQL, liczy preagregaty, renderuje HTML i dokłada analizę AI. +analysisAI.py — pobiera dane z MySQL, liczy preagregaty, renderuje HTML i dodaje 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) +ZMIENNE ŚRODOWISKOWE (wszystkie mają domyślne wartości): + OPENAI_API_KEY - klucz do OpenAI (gdy pusty -> fallback bez AI) + OPENAI_MODEL - np. gpt-4.1 (domyślnie), alternatywnie gpt-4.1-mini + MYSQL_HOST - host MySQL (domyślnie: twinpol-mysql56 lub 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 + PERIOD_TO - data do (YYYY-MM-DD, exclusive); gdy brak -> 1. dzień bieżącego miesiąca INVOICE_TYPE - typ dokumentu (domyślnie: normal) """ import os, sys, json, math, time, warnings from datetime import date, timedelta -#gtp-4.1 API_KEY = "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" #5 pro #API_KEY = "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" - -# Wycisz ostrzeżenie urllib3 (LibreSSL na macOS itp.) +# ──(1) Wycisz ostrzeżenia urllib3 (LibreSSL / stary OpenSSL) ─────────────────── try: from urllib3.exceptions import NotOpenSSLWarning warnings.filterwarnings("ignore", category=NotOpenSSLWarning) except Exception: pass +# ──(2) Importy zewnętrzne ────────────────────────────────────────────────────── import requests import mysql.connector + +# Twoje preagregaty (muszą być w tym samym katalogu / PYTHONPATH) from preaggregates import compute_preaggregates, serialize_for_ai -# --------- utils --------- +# ──(3) Konfiguracja klucza AI ────────────────────────────────────────────────── +# Wpisz tutaj klucz jeśli chcesz mieć go „na sztywno”, inaczej zostaw pusty: +API_KEY_HARDCODE = API_KEY # np. "sk-xxxx..." (NIEZALECANE w produkcji) +# ──(4) Utils ─────────────────────────────────────────────────────────────────── def getenv(k, d=None): return os.environ.get(k, d) @@ -51,12 +55,13 @@ def last_full_month_bounds(): return from_dt.isoformat(), to_dt.isoformat() def compact_table(table, limit=30): - """Przytnij listę rekordów i znormalizuj liczby (NaN/Inf -> None).""" + """Przytnij listę rekordów (list[dict]) i znormalizuj liczby (NaN/Inf -> None).""" out = [] if not table: return out + lim = int(limit) for i, row in enumerate(table): - if i >= int(limit): break + if i >= lim: break new = {} for k, v in row.items(): if isinstance(v, float): @@ -67,15 +72,15 @@ def compact_table(table, limit=30): return out def build_ai_payload(serialized, period_label): - """Kompaktowy JSON do AI (ograniczone rozmiary).""" + """Kompaktowy JSON do AI (rozmiar przycięty, ale zawiera wszystkie główne tabele).""" 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), + "daily_sales": compact_table(serialized.get("daily_sales"), 60), + "product_summary": compact_table(serialized.get("product_summary"), 100), + "customer_summary": compact_table(serialized.get("customer_summary"), 100), "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), + "product_daily_sample": compact_table(serialized.get("product_daily"), 100), } def call_openai_chat(api_key, model, system_prompt, user_payload_json, @@ -90,7 +95,7 @@ def call_openai_chat(api_key, model, system_prompt, user_payload_json, {"role": "user", "content": "Dane (JSON):\n\n" + user_payload_json}, ], "temperature": temperature, - # "max_tokens": 1200, # możesz odkomentować, aby ograniczyć długość odpowiedzi + # "max_tokens": 1200, # opcjonalnie ogranicz długość odpowiedzi } last_err = None for attempt in range(1, int(max_retries) + 1): @@ -112,7 +117,7 @@ def fmt_money(v): return str(v) def html_table(records, title=None, max_rows=20): - """Proste generowanie tabeli HTML z listy dict-ów.""" + """Proste generowanie tabeli HTML z listy dict-ów (lekki CSS inline w """ -# --------- main --------- - +# ──(5) Główna logika ─────────────────────────────────────────────────────────── def main(): # Konfiguracja DB cfg = { "host": getenv("MYSQL_HOST", "twinpol-mysql56"), - # "host": getenv("MYSQL_HOST", "localhost"), "user": getenv("MYSQL_USER", "root"), "password": getenv("MYSQL_PASSWORD", "rootpassword"), "database": getenv("MYSQL_DATABASE", "preDb_0dcc87940d3655fa574b253df04ca1c3"), @@ -206,15 +205,21 @@ def main(): period_label = "{} .. {}".format(period_from, period_to) invoice_type = getenv("INVOICE_TYPE", "normal") - # Konfiguracja AI - #api_key = getenv("OPENAI_API_KEY", "") - api_key = API_KEY + # Konfiguracja AI (model do API + alias do UI) + api_key = API_KEY_HARDCODE or getenv("OPENAI_API_KEY", "") model = getenv("OPENAI_MODEL", "gpt-4.1") - #model = getenv("OPENAI_MODEL", "gpt-5") + MODEL_ALIAS = { + "gpt-4.1": "GPT-4.1", + "gpt-4.1-mini": "GPT-4.1-mini", + "gpt-4o": "GPT-4o", + "gpt-4o-mini": "GPT-4o-mini", + } + model_alias = MODEL_ALIAS.get(model, model) + system_prompt = ( "Jesteś analitykiem sprzedaży. Zwróć TYLKO jedną sekcję HTML (bez //), " - "może być pojedynczy
z nagłówkami i listami. Podsumuj trendy, wskaż kluczowe produkty/klientów, " - "anomalia/odchylenia oraz daj 3–6 praktycznych rekomendacji. Krótko, konkretnie, po polsku." + "może być pojedynczy
z nagłówkami i listami. Podsumuj kluczowe trendy (dzień, mix), wskaż top produkty/klientów, " + "anomalia/odchylenia oraz daj 3–6 praktycznych rekomendacji dla sprzedaży/zaopatrzenia/marketingu. Krótko i konkretnie, po polsku." ) # SQL -> rows @@ -247,7 +252,7 @@ def main(): 'max-width:900px;margin:24px auto;padding:16px 20px;border:1px solid #fecaca;' 'border-radius:12px;background:#fff5f5;color:#991b1b;">' '

Błąd połączenia/zapytania MySQL

' - '

{}

'.format(str(e)) + f'

{str(e)}

' ) sys.exit(1) @@ -261,7 +266,7 @@ def main(): 'max-width:900px;margin:24px auto;padding:16px 20px;border:1px solid #fecaca;' 'border-radius:12px;background:#fff5f5;color:#991b1b;">' '

Błąd preagregacji

' - '

{}

'.format(str(e)) + f'

{str(e)}

' ) sys.exit(1) @@ -278,17 +283,21 @@ def main(): ("ASP (PLN/szt.)", fmt_money(asp) if asp is not None else "—"), ] - # Sekcje HTML z Twoich preagregatów + # Sekcje HTML — WYŚWIETLAMY WSZYSTKIE KLUCZOWE PREAGREGATY top_prod = serialized.get("top10_products_by_sales") or [] - top_cli = serialized.get("top10_customers_by_sales") or [] - prod_tbl = html_table(top_prod, title="Top 10 produktów (po sprzedaży)", max_rows=10) - cust_tbl = html_table(top_cli, title="Top 10 klientów (po sprzedaży)", max_rows=10) + top_cli = serialized.get("top10_customers_by_sales") or [] + daily_tbl = html_table(serialized.get("daily_sales") or [], title="Sprzedaż dzienna (skrót)", max_rows=30) + prod_sum_tbl = html_table(serialized.get("product_summary") or [], title="Podsumowanie produktów (skrót)", max_rows=30) + cust_sum_tbl = html_table(serialized.get("customer_summary") or [], title="Podsumowanie klientów (skrót)", max_rows=30) + prod_daily_tbl= html_table(serialized.get("product_daily") or [], title="Produkt × Dzień (próbka)", max_rows=30) + prod_tbl = html_table(top_prod, title="Top 10 produktów (po sprzedaży)", max_rows=10) + cust_tbl = html_table(top_cli, title="Top 10 klientów (po sprzedaży)", max_rows=10) # Dane do AI ai_data = build_ai_payload(serialized, period_label) ai_json = json.dumps(ai_data, ensure_ascii=False, separators=(",", ":"), default=str) - # Wołanie AI (opcjonalne) + # Wołanie AI (z fallbackiem na mini model przy 429: insufficient_quota) ai_section = "" if api_key: try: @@ -303,19 +312,42 @@ def main(): max_retries=3, ) except Exception as e: - ai_section = ( - '
Błąd wywołania AI: {}
'.format(str(e)) - ) + err = str(e) + if "insufficient_quota" in err or "You exceeded your current quota" in err: + # spróbuj tańszego modelu + try: + ai_section = call_openai_chat( + api_key=api_key, + model="gpt-4.1-mini", + system_prompt=system_prompt, + user_payload_json=ai_json, + temperature=0.3, + connect_timeout=10, + read_timeout=90, + max_retries=2, + ) + model_alias = "GPT-5 Mini" + except Exception as ee: + ai_section = ( + '
' + f'Brak dostępnego limitu API. {str(ee)}
' + ) + else: + ai_section = ( + '
' + f'Błąd wywołania AI: {err}
' + ) # Finalny HTML (jeden
) report_html = render_report_html( period_label=period_label, kpis=kpis, - parts=[prod_tbl, cust_tbl], - ai_section=ai_section + parts=[prod_tbl, cust_tbl, daily_tbl, prod_sum_tbl, cust_sum_tbl, prod_daily_tbl], + ai_section=ai_section, + model_alias=model_alias if api_key else "" ) - sys.stdout.write(report_html) if __name__ == "__main__":