MatteoScript commited on
Commit
c146fd5
·
verified ·
1 Parent(s): 5b25eff

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +477 -467
app.py CHANGED
@@ -1,480 +1,490 @@
1
  import streamlit as st
2
- import time
3
- import google.generativeai as genai
4
- from pydantic import ValidationError
5
- import mimetypes
6
  import os
7
- import settings_ai
8
- from settings_ai import Documento, Articolo
9
- import pandas as pd
10
- from PyPDF2 import PdfReader, PdfWriter
11
- import json
12
- from azure.core.credentials import AzureKeyCredential
13
- from azure.ai.documentintelligence import DocumentIntelligenceClient
14
- from azure.ai.documentintelligence.models import AnalyzeDocumentRequest
15
- from streamlit_pdf_viewer import pdf_viewer
16
- import io
17
- from PyPDF2 import PdfReader, PdfWriter
18
- import fitz
19
- import re
20
  import io
21
- from collections import Counter
22
-
23
- st.set_page_config(page_title="Import Fatture AI✨")
24
- st.title("Import Fatture AI ✨")
25
-
26
- # Gestionione LOGIN
27
- if "logged" not in st.session_state:
28
- st.session_state.logged = False
29
- st.session_state.model = "gemini-2.0-flash"
30
- if st.session_state.logged == False:
31
- login_placeholder = st.empty()
32
- with login_placeholder.container():
33
- container = st.container(border=True)
34
- username = container.text_input('Username')
35
- password = container.text_input('Passowrd', type='password')
36
- login = container.button(' Login ', type='primary')
37
- if not login or username != os.getenv("LOGIN_USER") or password != os.getenv("LOGIN_PASSWORD"):
38
- if login:
39
- st.error('Password Errata')
40
- st.stop()
41
- st.session_state.logged = True
42
- login_placeholder.empty()
43
-
44
- with st.expander("Guida completa"):
45
- st.write("""Questa applicazione Python, basata su Streamlit, integra servizi di intelligenza artificiale di Gemini per automatizzare l'estrazione e la validazione dei dati dalle fatture. Il sistema gestisce documenti in vari formati (PDF, immagini) e li elabora in maniera modulare per facilitare la conversione e la verifica delle informazioni.
46
-
47
- ## Funzionalità Principali
48
-
49
- - **Caricamento e Gestione dei Documenti**
50
- - Supporta il caricamento di file PDF, JPG, JPEG e PNG tramite un’interfaccia Streamlit.
51
- - Se il file è un PDF con più pagine, viene suddiviso in sezioni (configurabile tramite uno slider) per una gestione più efficace. Più il numero è basso più il risultato è preciso.
52
-
53
- - **Conversione dei Dati**
54
- - **Upload e Inoltro a Gemini**: I file vengono caricati e inviati al rispettivo servizio AI.
55
- - **Estrazione dei Dati**: Il sistema invia il documento a un modello di generazione AI per ottenere una rappresentazione JSON contenente i dati (ad es. numero di documento, data, totale imponibile e articoli).
56
-
57
- - **Validazione e Verifica**
58
- - **Validazione JSON**: Utilizza Pydantic per verificare la correttezza della struttura e dei dati estratti. In caso di errori, il documento viene riprocessato fino a 3 volte per cercare di correggere le anomalie.
59
- - **Verifica Incrociata dei Contenuti**: Per i PDF, viene estratto il testo con PyPDF2 e confrontato con i codici articolo per assicurarsi che i dati siano effettivamente presenti nel documento.
60
- - **Filtraggio Articoli**: Vengono mantenuti solo gli articoli compatibili con i criteri specifici (codici articolo e importi non nulli).
61
-
62
- - **Visualizzazione e Highlighting**
63
- - I dati validati vengono mostrati in formato tabellare e in JSON.
64
- - Se il documento è un PDF, il sistema evidenzia graficamente (con rettangoli rossi) i testi relativi agli articoli compatibili, semplificando il controllo visivo.
65
-
66
- ## Avvertenze per l'Operatore
67
-
68
- - **Controllo Manuale Obbligatorio**:
69
- L'operatore **deve sempre verificare** i dati elaborati.
70
- Nonostante il riprocessamento automatico in caso di errori, è fondamentale controllare la correttezza dei dati estratti e la validità degli articoli.
71
-
72
- - **Validazione dei Dati**:
73
- L'operazione di validazione tramite Pydantic e la verifica incrociata sul contenuto del PDF sono cruciali.
74
- Assicurarsi che non vi siano discrepanze, specialmente nei casi in cui alcuni articoli risultino non verificati.
75
- """)
76
-
77
- st.write("📄 **Legge più PDF:** Carica più file PDF contemporaneamente")
78
- st.write("🤖 **Sfrutta l'AI di Gemini:** Per ogni documento, estrae i dati in formato JSON e in formato tabellare.")
79
- st.write("✅ **Mostra Articoli Compatibili:** Filtra e visualizza solo gli articoli che rispettano i criteri richiesti.")
80
- st.write("🔍 **Anteprima Documento:** Visualizza un'anteprima del documento evidenziando gli articoli compatibili.")
81
-
82
-
83
- GENERATION_CONFIG = settings_ai.GENERATION_CONFIG
84
- SYSTEM_INSTRUCTION = settings_ai.SYSTEM_INSTRUCTION
85
- USER_MESSAGE = settings_ai.USER_MESSAGE
86
- API_KEY_GEMINI = settings_ai.API_KEY_GEMINI
87
-
88
- # Configura il modello Gemini
89
- genai.configure(api_key=API_KEY_GEMINI)
90
- model = genai.GenerativeModel(
91
- model_name=st.session_state.model,
92
- generation_config=GENERATION_CONFIG,
93
- system_instruction=SYSTEM_INSTRUCTION
94
- )
95
-
96
- # Upload File a GEMINI
97
- def upload_to_gemini(path: str, mime_type: str = None):
98
- """Carica un file su Gemini e ne ritorna l'oggetto file."""
99
- file = genai.upload_file(path, mime_type=mime_type)
100
- print(f"Uploaded file '{file.display_name}' as: {file.uri}")
101
- return file
102
-
103
- # Attesa Upload Files
104
- def wait_for_files_active(files):
105
- """Attende che i file siano nello stato ACTIVE su Gemini."""
106
- print("Waiting for file processing...")
107
- for name in (f.name for f in files):
108
- file_status = genai.get_file(name)
109
- while file_status.state.name == "PROCESSING":
110
- print(".", end="", flush=True)
111
- time.sleep(10)
112
- file_status = genai.get_file(name)
113
- if file_status.state.name != "ACTIVE":
114
- raise Exception(f"File {file_status.name} failed to process")
115
- print("\n...all files ready")
116
-
117
- # Chiamata API Gemini
118
- def send_message_to_gemini(chat_session, message, max_attempts=5):
119
- """Tenta di inviare il messaggio tramite la chat_session, riprovando fino a max_attempts in caso di eccezioni, con un delay di 10 secondi tra i tentativi. """
120
- for attempt in range(max_attempts):
121
- try:
122
- print(f"Generazione AI con PROMPT: {message}")
123
- response_local = chat_session.send_message(message)
124
- return response_local
125
- except Exception as e:
126
- print(f"Errore in send_message (tentativo {attempt+1}/{max_attempts}): {e}")
127
- if attempt < max_attempts - 1:
128
- print("Riprovo tra 10 secondi...")
129
- time.sleep(10)
130
- raise RuntimeError(f"Invio messaggio fallito dopo {max_attempts} tentativi.")
131
-
132
- # Unisce i rettangoli evidenziati (se codice e descrizione stanno su linee diverse viene mostrato un solo rettangolo evidenziato)
133
- def merge_intervals(intervals):
134
- """Unisce gli intervalli sovrapposti. Gli intervalli sono tuple (y0, y1)."""
135
- if not intervals:
136
- return []
137
- intervals.sort(key=lambda x: x[0])
138
- merged = [intervals[0]]
139
- for current in intervals[1:]:
140
- last = merged[-1]
141
- if current[0] <= last[1]:
142
- merged[-1] = (last[0], max(last[1], current[1]))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  else:
144
- merged.append(current)
145
- return merged
146
-
147
- # Evidenzia le corrispondenze
148
- def highlight_text_in_pdf(input_pdf_bytes, text_list):
149
- """Crea rettangoli rossi che evidenziano i testi trovati, unendo gli intervalli sovrapposti in un unico rettangolo. """
150
- pdf_document = fitz.open(stream=input_pdf_bytes, filetype="pdf")
151
- patterns = []
152
- for text in text_list:
153
- text_pattern = re.escape(text).replace(r'\ ', r'\s*')
154
- patterns.append(text_pattern)
155
- pattern = r'\b(' + '|'.join(patterns) + r')\b'
156
- regex = re.compile(pattern, re.IGNORECASE)
157
- highlight_color = (1, 0, 0) # rosso
158
- for page in pdf_document:
159
- page_text = page.get_text()
160
- matches = list(regex.finditer(page_text))
161
- intervals = []
162
- for match in matches:
163
- match_text = match.group(0)
164
- text_instances = page.search_for(match_text)
165
- for inst in text_instances:
166
- text_height = inst.y1 - inst.y0
167
- new_y0 = inst.y0 - 0.1 * text_height
168
- new_y1 = inst.y1 + 0.1 * text_height
169
- intervals.append((new_y0, new_y1))
170
- merged_intervals = merge_intervals(intervals)
171
- for y0, y1 in merged_intervals:
172
- full_width_rect = fitz.Rect(page.rect.x0, y0, page.rect.x1, y1)
173
- rect_annot = page.add_rect_annot(full_width_rect)
174
- rect_annot.set_colors(stroke=highlight_color, fill=highlight_color)
175
- rect_annot.set_opacity(0.15)
176
- rect_annot.update()
177
- output_stream = io.BytesIO()
178
- pdf_document.save(output_stream)
179
- pdf_document.close()
180
- output_stream.seek(0)
181
- return output_stream
182
-
183
- # Formattazione Euro
184
- def format_euro(amount):
185
- formatted = f"{amount:,.2f}"
186
- formatted = formatted.replace(",", "X").replace(".", ",").replace("X", ".")
187
- return f"€ {formatted}"
188
-
189
- # Testo da PDF
190
- def pdf_to_text(path_file: str) -> str:
191
- """ Estrae e concatena il testo da tutte le pagine del PDF. """
192
- reader = PdfReader(path_file)
193
- full_text = ""
194
- for page in reader.pages:
195
- page_text = page.extract_text() or ""
196
- full_text += page_text + "\n"
197
- return full_text
198
-
199
- # Funzione che verifica se gli articoli sono corretti (facendo un partsing PDF to TEXT)
200
- def verify_articles(file_path: str, chunk_document):
201
- ''' La funzione trasforma il PDF in TESTO e cerca se ogni articolo è presente (al netto degli spazi) '''
202
- if not file_path.lower().endswith(".pdf"):
203
- for articolo in chunk_document.Articoli:
204
- articolo.Verificato = 2
205
  return None
206
- pdf_text = pdf_to_text(file_path)
207
- if '□' in pdf_text:
208
- for articolo in chunk_document.Articoli:
209
- articolo.Verificato = 2
210
  return None
211
- for articolo in chunk_document.Articoli:
212
- articolo.Verificato = 1 if articolo.CodiceArticolo and (articolo.CodiceArticolo in pdf_text) else 0
213
- if articolo.Verificato == 0:
214
- articolo.Verificato = 1 if articolo.CodiceArticolo and (articolo.CodiceArticolo.replace(" ", "") in pdf_text.replace(" ", "")) else 0
215
- if not any(articolo.Verificato == 0 for articolo in chunk_document.Articoli):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  return None
217
- unverified_articles = [articolo for articolo in chunk_document.Articoli if articolo.Verificato == 0]
218
- json_unverified_articles = json.dumps([articolo.model_dump() for articolo in unverified_articles])
219
- return json_unverified_articles
220
-
221
- # Funzione ausiliaria che elabora un file (o un chunk) inviandolo tramite send_message_to_gemini
222
- def process_document_splitted(file_path: str, chunk_label: str, use_azure: bool = False) -> Documento:
223
- """ Elabora il file (o il chunk) inviandolo a Gemini e processando la risposta:
224
- - Determina il mime type.
225
- - Effettua l'upload tramite upload_to_gemini e attende che il file sia attivo.
226
- - Avvia una chat con il file caricato e invia il messaggio utente.
227
- - Tenta fino a 3 volte di validare il JSON ottenuto, filtrando gli Articoli.
228
- - Se presenti errori, RIPROCESSA il documento 3 volta passando il risultato precedente, In questo modo riesce a gestire gli errori in modo più preciso!
229
- Ritorna l'istanza di Documento validata. """
230
- mime_type, _ = mimetypes.guess_type(file_path)
231
- if mime_type is None:
232
- mime_type = "application/octet-stream"
233
- if not use_azure:
234
- files = [upload_to_gemini(file_path, mime_type=mime_type)]
235
- wait_for_files_active(files)
236
- chat_history = [{ "role": "user","parts": [files[0]]}]
237
- for attempt in range(3):
238
- try:
239
- chat_session = model.start_chat(history=chat_history)
240
- break
241
- except Exception as e:
242
- print(f"Errore nello Start chat")
243
- time.sleep(10)
244
-
245
- max_validation_attempts = 3
246
- max_number_reprocess = 3
247
- chunk_document = None
248
-
249
- for i in range(max_number_reprocess):
250
- print(f"Reprocessamento {i+1} di {max_number_reprocess} per il chunk {chunk_label}")
251
- response = None
252
- for attempt in range(max_validation_attempts):
253
- message = USER_MESSAGE
254
- if i > 0:
255
- message += f". Attenzione, RIPROVA perché i seguenti articoli sono da ESCLUDERE in quanto ERRATI! {json_unverified_articles}"
256
- if not use_azure:
257
- response = send_message_to_gemini(chat_session, message)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  else:
259
- chunk_document = analyze_invoice_azure(file_path)
260
- try:
261
- if not use_azure:
262
- chunk_document = Documento.model_validate_json(response.text)
263
- chunk_document.Articoli = [
264
- art for art in chunk_document.Articoli
265
- if art.CodiceArticolo.startswith(("AVE", "AV", "3V", "44"))
266
- and art.TotaleNonIvato != 0
267
- and art.CodiceArticolo not in ("AVE", "AV", "3V", "44")
268
- ]
269
- break
270
- except ValidationError as ve:
271
- print(f"Errore di validazione {chunk_label} (tentativo {attempt+1}/{max_validation_attempts}): {ve}")
272
- if attempt < max_validation_attempts - 1:
273
- print("Riprovo tra 5 secondi...")
274
- time.sleep(5)
275
- else:
276
- raise RuntimeError(f"Superato il numero massimo di tentativi di validazione {chunk_label}.")
277
-
278
- json_unverified_articles = verify_articles(file_path, chunk_document)
279
- if not json_unverified_articles:
280
- return chunk_document
281
- return chunk_document
282
-
283
- # Funzione principale che elabora il documento
284
- def process_document(path_file: str, number_pages_split: int, use_azure: bool = False) -> Documento:
285
- """ Elabora il documento in base al tipo di file:
286
- 1. Se il file non è un PDF, lo tratta "così com'è" (ad esempio, come immagine) e ne processa il contenuto tramite process_document_splitted.
287
- 2. Se il file è un PDF e contiene più di 5 pagine, lo divide in chunk da 5 pagine.
288
- Per ogni chunk viene effettuato l’upload, viene elaborato il JSON e validato.
289
- I chunk successivi vengono aggregati nel Documento finale e, se il PDF ha più
290
- di 5 pagine, al termine il campo TotaleMerce viene aggiornato con il valore riportato dall’ultimo chunk. """
291
- mime_type, _ = mimetypes.guess_type(path_file)
292
- if mime_type is None:
293
- mime_type = "application/octet-stream"
294
- if use_azure:
295
- number_pages_split = 1
296
- if not path_file.lower().endswith(".pdf"):
297
- print("File non PDF: elaborazione come immagine.")
298
- documento_finale = process_document_splitted(path_file, chunk_label="(immagine)", use_azure=use_azure)
299
- return documento_finale
300
-
301
- reader = PdfReader(path_file)
302
- total_pages = len(reader.pages)
303
- documento_finale = None
304
- ultimo_totale_merce = None
305
-
306
- for chunk_index in range(0, total_pages, number_pages_split):
307
- writer = PdfWriter()
308
- for page in reader.pages[chunk_index:chunk_index + number_pages_split]:
309
- writer.add_page(page)
310
- temp_filename = "temp_chunk_"
311
- if use_azure:
312
- temp_filename+="azure_"
313
- temp_filename += f"{chunk_index // number_pages_split}.pdf"
314
- with open(temp_filename, "wb") as temp_file:
315
- writer.write(temp_file)
316
- chunk_label = f"(chunk {chunk_index // number_pages_split})"
317
- chunk_document = process_document_splitted(temp_filename, chunk_label=chunk_label, use_azure=use_azure)
318
- if hasattr(chunk_document, "TotaleImponibile"):
319
- ultimo_totale_merce = chunk_document.TotaleImponibile
320
- if documento_finale is None:
321
- documento_finale = chunk_document
322
- else:
323
- documento_finale.Articoli.extend(chunk_document.Articoli)
324
- os.remove(temp_filename)
325
-
326
- if total_pages > number_pages_split and ultimo_totale_merce is not None:
327
- documento_finale.TotaleImponibile = ultimo_totale_merce
328
- if documento_finale is None:
329
- raise RuntimeError("Nessun documento elaborato.")
330
-
331
- # Controlli aggiuntivi: Se esiste un AVE non possono esistere altri articoli non ave.
332
- if any(articolo.CodiceArticolo.startswith("AVE") for articolo in documento_finale.Articoli):
333
- documento_finale.Articoli = [articolo for articolo in documento_finale.Articoli if articolo.CodiceArticolo.startswith("AVE")]
334
- # Controllo occorrenze di doppioni
335
- if path_file.lower().endswith(".pdf"):
336
- pdf_text = pdf_to_text(path_file)
337
- pdf_text = pdf_text.replace(" ", "")
338
- occorrenze = {}
339
- for articolo in documento_finale.Articoli:
340
- codice_clean = articolo.CodiceArticolo.replace(" ", "")
341
- if codice_clean not in occorrenze:
342
- occorrenze[codice_clean] = pdf_text.count(codice_clean)
343
- articoli_contati = {}
344
- for articolo in documento_finale.Articoli:
345
- codice_clean = articolo.CodiceArticolo.replace(" ", "")
346
- if codice_clean in pdf_text:
347
- print(codice_clean)
348
- print(occorrenze[codice_clean])
349
- articoli_contati[codice_clean] = articoli_contati.get(codice_clean, 0) + 1
350
- if articoli_contati[codice_clean] <= occorrenze.get(codice_clean, 0):
351
- articolo.Verificato = True
352
- else:
353
- articolo.Verificato = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  else:
355
- articolo.Verificato = False
356
- return documento_finale
357
-
358
- # Analizza Fattura con AZURE
359
- def analyze_invoice_azure(file_path: str):
360
- """Invia il file (dal percorso specificato) al servizio prebuilt-invoice e restituisce il risultato dell'analisi."""
361
- # Apri il file in modalità binaria e leggi il contenuto
362
- with open(file_path, "rb") as file:
363
- file_data = file.read()
364
- client = DocumentIntelligenceClient(endpoint=settings_ai.ENDPOINT_AZURE, credential=AzureKeyCredential(settings_ai.API_AZURE))
365
- poller = client.begin_analyze_document("prebuilt-invoice", body=file_data)
366
- result = poller.result()
367
- return parse_invoice_to_documento_azure(result)
368
-
369
- # Parsing Fattura con AZURE
370
- def parse_invoice_to_documento_azure(result) -> Documento:
371
- """ Parssa il risultato dell'analisi e mappa i campi rilevanti nel modello Pydantic Documento. """
372
- if not result.documents:
373
- raise ValueError("Nessun documento analizzato trovato.")
374
- invoice = result.documents[0]
375
- invoice_id_field = invoice.fields.get("InvoiceId")
376
- numero_documento = invoice_id_field.value_string if invoice_id_field and invoice_id_field.value_string else ""
377
- invoice_date_field = invoice.fields.get("InvoiceDate")
378
- data_str = invoice_date_field.value_date.isoformat() if invoice_date_field and invoice_date_field.value_date else ""
379
- subtotal_field = invoice.fields.get("SubTotal")
380
- if subtotal_field and subtotal_field.value_currency:
381
- totale_imponibile = subtotal_field.value_currency.amount
382
- else:
383
- invoice_total_field = invoice.fields.get("InvoiceTotal")
384
- totale_imponibile = invoice_total_field.value_currency.amount if invoice_total_field and invoice_total_field.value_currency else 0.0
385
- articoli = []
386
- items_field = invoice.fields.get("Items")
387
- if items_field and items_field.value_array:
388
- for item in items_field.value_array:
389
- product_code_field = item.value_object.get("ProductCode")
390
- description_field = str(item.value_object.get("Description").get("content"))
391
- if not description_field:
392
- description_field = ""
393
- codice_articolo = product_code_field.value_string if product_code_field and product_code_field.value_string else ""
394
- amount_field = item.value_object.get("Amount")
395
- totale_non_ivato = amount_field.value_currency.amount if amount_field and amount_field.value_currency else 0.0
396
- articolo = Articolo(
397
- CodiceArticolo=codice_articolo,
398
- DescrizioneArticolo=description_field,
399
- TotaleNonIvato=totale_non_ivato,
400
- Verificato=None
401
  )
402
- articoli.append(articolo)
403
-
404
- documento = Documento(
405
- TipoDocumento="Fattura",
406
- NumeroDocumento=numero_documento,
407
- Data=data_str,
408
- TotaleImponibile=totale_imponibile,
409
- Articoli=articoli
410
- )
411
- return documento
412
-
413
- # Front-End con Streamlit
414
  def main():
415
- #st.set_page_config(page_title="Import Fatture AI", page_icon="✨")
416
- st.sidebar.title("Caricamento File")
417
- uploaded_files = st.sidebar.file_uploader("Seleziona uno o più PDF", type=["pdf", "jpg", "jpeg", "png"], accept_multiple_files=True)
418
- model_ai = st.sidebar.selectbox("Modello", ['Gemini Flash 2.0', #'Gemini 2.5 Pro',
419
- 'Azure Intelligence'])
420
- if model_ai == 'Gemini 2.5 Pro':
421
- st.session_state.model = "gemini-2.5-pro-exp-03-25"
422
- use_azure = True if model_ai == 'Azure Intelligence' else False
423
- number_pages_split = st.sidebar.slider('Split Pagine', 1, 30, 1, help="Numero suddivisione pagine del PDF. Più il numero è basso e più il modello AI è preciso, più è alto più è veloce")
424
- if st.sidebar.button("Importa", type="primary", use_container_width=True):
425
- if not uploaded_files:
426
- st.warning("Nessun file caricato!")
427
- else:
428
- for uploaded_file in uploaded_files:
429
- st.subheader(f"📄 {uploaded_file.name}")
430
- file_path = uploaded_file.name
431
- with open(file_path, "wb") as f:
432
- f.write(uploaded_file.getbuffer())
433
-
434
- with st.spinner(f"Elaborazione in corso"):
435
- try:
436
- doc = process_document(uploaded_file.name, number_pages_split, use_azure=use_azure)
437
- totale_non_ivato_verificato = sum(articolo.TotaleNonIvato for articolo in doc.Articoli if articolo.Verificato == 1)
438
- totale_non_ivato_non_verificato = sum(articolo.TotaleNonIvato for articolo in doc.Articoli if articolo.Verificato != 1)
439
- totale_non_ivato = totale_non_ivato_verificato + totale_non_ivato_non_verificato
440
- st.write(
441
- f"- **Tipo**: {doc.TipoDocumento}\n"
442
- f"- **Numero**: {doc.NumeroDocumento}\n"
443
- f"- **Data**: {doc.Data}\n"
444
- f"- **Articoli Compatibili**: {len(doc.Articoli)}\n"
445
- f"- **Totale Documento**: {format_euro(doc.TotaleImponibile)}\n"
446
- )
447
- if totale_non_ivato > doc.TotaleImponibile and doc.TotaleImponibile > 0:
448
- st.warning("Totale Ave maggiore di Totale Merce")
449
- if totale_non_ivato_non_verificato > 0:
450
- st.error(f"Totale Ave Non Verificato: {format_euro(totale_non_ivato_non_verificato)}")
451
- if totale_non_ivato > 0:
452
- st.success(f"Totale Ave Verificato: {format_euro(totale_non_ivato_verificato)}")
453
- df = pd.DataFrame([{k: v for k, v in Articolo.model_dump().items() if k != ""} for Articolo in doc.Articoli])
454
- if 'Verificato' in df.columns:
455
- df['Verificato'] = df['Verificato'].apply(lambda x: "✅" if x == 1 else "❌" if x == 0 else "❓" if x == 2 else x)
456
- if totale_non_ivato > 0:
457
- df["TotaleNonIvato"] = df["TotaleNonIvato"].apply(format_euro)
458
- st.dataframe(df, use_container_width=True)
459
- st.json(doc.model_dump(), expanded=False)
460
- if totale_non_ivato == 0:
461
- st.info(f"Non sono presenti articoli 'AVE'")
462
- if uploaded_file and file_path.lower().endswith(".pdf"):
463
- list_art = list_art = [articolo.CodiceArticolo for articolo in doc.Articoli] + [articolo.DescrizioneArticolo for articolo in doc.Articoli]
464
- if list_art:
465
- new_pdf = highlight_text_in_pdf(uploaded_file.getvalue(), list_art)
466
- pdf_viewer(input=new_pdf.getvalue(), width=1200)
467
- else:
468
- pdf_viewer(input=uploaded_file.getvalue(), width=1200)
469
- else:
470
- st.image(file_path)
471
- st.divider()
472
- except Exception as e:
473
- st.error(f"Errore durante l'elaborazione di {uploaded_file.name}: {e}")
474
- finally:
475
- if os.path.exists(file_path):
476
- os.remove(file_path)
477
 
478
  if __name__ == "__main__":
479
- st.divider()
480
- main()
 
1
  import streamlit as st
2
+ from openai import OpenAI
3
+ import openpyxl
4
+ from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
 
5
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  import io
7
+ import traceback
8
+ import sys
9
+ from dotenv import load_dotenv
10
+ import json
11
+
12
+ st.set_page_config(layout="wide", page_title="Generatore Excel AI")
13
+
14
+ load_dotenv()
15
+ API_KEY = os.getenv("API_HUGGINGFACE")
16
+ BASE_URL = "https://matteoscript-ai.hf.space/v1/"
17
+ MODEL_NAME = "gemini-2.5-flash-preview-05-20" # Sostituisci con il tuo modello preferito
18
+
19
+ if not API_KEY:
20
+ st.error("API Key Hugging Face non trovata. Assicurati che il file .env sia configurato correttamente con API_HUGGINGFACE.")
21
+ st.stop()
22
+
23
+ client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
24
+
25
+ DEFAULT_EXCEL_FILENAME = "excel_ai.xlsx"
26
+ MAX_CORRECTION_ATTEMPTS = 5
27
+ PROMPT_FOR_SHEET_PLANNER = """
28
+ Sei un assistente AI specializzato nella pianificazione di documenti Excel multi-foglio.
29
+ Data una richiesta generale dell'utente e un numero di fogli desiderato, il tuo compito è proporre:
30
+ 1. Un nome univoco e descrittivo per ciascun foglio (massimo 30 caratteri, evita caratteri speciali non ammessi nei nomi dei fogli Excel come / ? * [ ]).
31
+ 2. Una breve descrizione (1-3 frasi) dello scopo, del contenuto principale di ciascun foglio e delle possibili interazioni/collegamenti con altri fogli. Questa descrizione verrà poi usata come base per generare il contenuto dettagliato del foglio.
32
+ La tua risposta DEVE essere un array JSON di oggetti. Ogni oggetto rappresenta un foglio e DEVE avere i campi "sheet_name" (stringa) e "sheet_purpose" (stringa).
33
+ Il numero di oggetti nell'array DEVE corrispondere esattamente al numero di fogli specificato dall'utente.
34
+ Esempio di richiesta utente: "Report vendite trimestrale (3 fogli): 1. Input dati grezzi vendite. 2. Riepilogo vendite per prodotto, che legga da input. 3. Riepilogo vendite per regione, che legga anch'esso da input."
35
+ Esempio di TUA risposta JSON:
36
+ [
37
+ {"sheet_name": "Input Vendite", "sheet_purpose": "Foglio per l'inserimento manuale o l'importazione dei dati grezzi di vendita, includendo data, ID prodotto, quantità, prezzo, regione."},
38
+ {"sheet_name": "Riep Prodotti", "sheet_purpose": "Analizza i dati dal foglio 'Input Vendite' per mostrare un riepilogo delle vendite per prodotto. Include totali, medie e forse grafici. Utilizza formule per aggregare i dati da 'Input Vendite'."},
39
+ {"sheet_name": "Riep Regioni", "sheet_purpose": "Analizza i dati dal foglio 'Input Vendite' per visualizzare le performance di vendita per regione. Potrebbe includere somme condizionali basate sui dati di 'Input Vendite'."}
40
+ ]
41
+ Non includere ```json o ``` all'inizio o alla fine della tua risposta. Fornisci solo l'array JSON.
42
+ """
43
+
44
+ BASE_PROMPT_NO_BACKTICKS = """
45
+ Sei un esperto generatore di SCRIPT PYTHON per MODIFICARE un oggetto Workbook openpyxl esistente (fornito in una variabile chiamata wb). Il tuo compito è aggiungere e popolare UN SINGOLO FOGLIO in questo Workbook, usando la libreria openpyxl.
46
+
47
+ IL TUO SCRIPT NON DEVE ASSOLUTAMENTE:
48
+ - Creare un nuovo Workbook (NON usare openpyxl.Workbook()). L'oggetto Workbook ti viene fornito e si chiama wb.
49
+ - Salvare il Workbook (NON usare wb.save()). Il salvataggio sarà gestito esternamente.
50
+ - Includere istruzioni di import globali. Le funzioni e classi necessarie (wb, Font, PatternFill, Border, Side, Alignment) saranno già disponibili nello scope.
51
+
52
+ IL TUO SCRIPT PYTHON DEVE:
53
+ - Aspettarsi che un oggetto openpyxl.Workbook (wb) sia già definito.
54
+ - Creare un nuovo foglio o accedere a uno esistente usando il TITOLO_FOGLIO_RICHIESTO:
55
+ sheet_title = "TITOLO_FOGLIO_RICHIESTO" # Questo sarà il nome fornito
56
+ if sheet_title in wb.sheetnames:
57
+ sheet = wb[sheet_title]
58
+ # Pulisci il foglio se deve essere sovrascritto (valori e stili)
59
+ for row_idx in range(1, sheet.max_row + 1):
60
+ for col_idx in range(1, sheet.max_column + 1):
61
+ cell = sheet.cell(row=row_idx, column=col_idx)
62
+ cell.value = None
63
+ cell.style = openpyxl.styles.Style() # Reset stile base
64
+ else:
65
+ sheet = wb.create_sheet(title=sheet_title)
66
+ - Popolare l'oggetto sheet con dati, header, formule e formattazione come da richiesta.
67
+ - Usare stili Material Design e font moderni.
68
+ - Includere FORMULE Excel, specialmente se la richiesta implica calcoli o aggregazioni.
69
+
70
+ CONTESTO DEI FOGLI PRECEDENTI (SE FORNITO):
71
+ Se nella richiesta utente qui sotto trovi una sezione "CONTESTO DEI FOGLI PRECEDENTI", essa conterrà gli script Python usati per generare i fogli precedenti.
72
+ Usa questo per creare FORMULE EXCEL CHE COLLEGANO IL FOGLIO CORRENTE AI FOGLI PRECEDENTI.
73
+ Esempio: sheet['B1'].value = "='DatiVendite'!E10"
74
+ Analizza gli script precedenti per nomi dei fogli e struttura dati. I nomi dei fogli nelle formule DEVONO CORRISPONDERE ESATTAMENTE.
75
+
76
+ REQUISITI CODICE GENERATO:
77
+ - SENZA commenti python (#).
78
+ - Massimo un ritorno a capo vuoto consecutivo.
79
+ - Gestione errori interni al try/except (usare 'pass', non 'raise e').
80
+ - NON impostare sola lettura.
81
+ - NON usare Alignment se non strettamente necessario.
82
+ - Genera dati di esempio realistici (5-10 righe) se non diversamente specificato.
83
+ - Applica formattazione ad header e celle chiave.
84
+ """
85
+
86
+ PROMPT_FOR_SCRIPT_CORRECTION = """
87
+ Sei un esperto programmatore Python specializzato nella libreria openpyxl e nel debugging di script che manipolano file Excel.
88
+ Ti è stato fornito uno script Python che ha fallito durante l'esecuzione mentre tentava di aggiungere e popolare un foglio in un workbook openpyxl esistente ('wb').
89
+ Ti sono stati forniti anche l'errore esatto e il traceback.
90
+
91
+ IL TUO COMPITO È:
92
+ 1. Analizzare attentamente lo script originale, l'errore e il traceback.
93
+ 2. Identificare la causa dell'errore.
94
+ 3. Correggere lo script Python. Lo script corretto deve ancora rispettare TUTTE le seguenti regole originali:
95
+ - DEVE operare su un oggetto Workbook openpyxl esistente chiamato 'wb'.
96
+ - NON DEVE creare un nuovo Workbook (NON usare openpyxl.Workbook()).
97
+ - NON DEVE salvare il Workbook (NON usare wb.save()).
98
+ - NON DEVE includere istruzioni di import globali (es. 'import openpyxl'). Le classi necessarie (wb, Font, PatternFill, Border, Side, Alignment) sono già nello scope.
99
+ - DEVE creare/accedere al foglio specificato da TITOLO_FOGLIO_RICHIESTO. Il nome del foglio è critico, non alterarlo a meno che l'errore non sia specificamente legato ad esso e la correzione sia ovvia.
100
+ - DEVE popolare il foglio come descritto in DESCRIZIONE_ORIGINALE_FOGLIO.
101
+ - Se era presente un CONTESTO_FOGLI_PRECEDENTI, lo script corretto deve ancora poter utilizzare quel contesto per eventuali formule inter-foglio.
102
+ - DEVE essere SENZA commenti python (#).
103
+ - DEVE avere al massimo un ritorno a capo vuoto consecutivo.
104
+ - DEVE gestire gli errori specifici del foglio internamente con un blocco try/except che termina con 'pass' (non 'raise e'). L'intero script dovrebbe essere avvolto in un try/except.
105
+ - NON DEVE usare la classe Alignment esplicitamente a meno che non sia fondamentale e parte della descrizione originale.
106
+ - DEVE mantenere l'obiettivo originale del foglio. Non rimuovere funzionalità a meno che non siano la causa diretta dell'errore e non ci sia una correzione ovvia.
107
+ 4. Restituisci SOLO lo script Python CORRETTO E COMPLETO, senza alcuna spiegazione aggiuntiva, commenti o ```python.
108
+
109
+ INFORMAZIONI FORNITE:
110
+ - TITOLO_FOGLIO_RICHIESTO: "{sheet_name}"
111
+ - DESCRIZIONE_ORIGINALE_FOGLIO: "{sheet_purpose}"
112
+ - CONTESTO_FOGLI_PRECEDENTI (se applicabile):
113
+ {previous_scripts_context}
114
+ - SCRIPT_ORIGINALE_FALLITO:
115
+ ---
116
+ {original_script}
117
+ ---
118
+ - ERRORE_E_TRACEBACK:
119
+ ---
120
+ {error_traceback}
121
+ ---
122
+
123
+ Restituisci solo il codice Python corretto. Non aggiungere spiegazioni prima o dopo il codice. Assicurati che lo script sia completo e pronto per essere eseguito.
124
+ """
125
+
126
+ def initialize_session_state():
127
+ defaults = {
128
+ "initial_excel_request": "Business Plan di lancio prodotto per un'azienda Loyalty specializzata nella GDO",
129
+ "num_sheets_requested": 1,
130
+ "ai_sheet_plan": None,
131
+ "workbook_object": None,
132
+ "generated_scripts_for_sheets": [],
133
+ "final_excel_file_path": None,
134
+ "final_excel_filename": DEFAULT_EXCEL_FILENAME,
135
+ "process_log": [],
136
+ "error_message": "",
137
+ "warning_message": "",
138
+ "generation_started": False
139
+ }
140
+ for key, value in defaults.items():
141
+ if key not in st.session_state:
142
+ st.session_state[key] = value
143
+
144
+ def log_message(message, level="info"):
145
+ log_entry = f"[{level.upper()}] {message}\n"
146
+ st.session_state.process_log.append(log_entry)
147
+ if level == "error":
148
+ st.session_state.error_message += message + "\n"
149
+ elif level == "warning":
150
+ st.session_state.warning_message += message + "\n"
151
+
152
+ def call_openai_for_sheet_plan(overall_request, num_sheets):
153
+ st.session_state.error_message = ""
154
+ st.session_state.warning_message = ""
155
+ try:
156
+ user_content = f"Richiesta generale dell'utente: \"{overall_request}\"\nNumero di fogli desiderato: {num_sheets}"
157
+ completion = client.chat.completions.create(
158
+ model=MODEL_NAME,
159
+ messages=[
160
+ {"role": "system", "content": PROMPT_FOR_SHEET_PLANNER},
161
+ {"role": "user", "content": user_content}
162
+ ],
163
+ temperature=0.3,
164
+ response_format={"type": "json_object"}
165
+ )
166
+ response_content = completion.choices[0].message.content.strip()
167
+ log_message(f"Risposta grezza AI (piano fogli): {response_content}")
168
+ planned_sheets = json.loads(response_content)
169
+ if isinstance(planned_sheets, list) and all(isinstance(item, dict) and "sheet_name" in item and "sheet_purpose" in item for item in planned_sheets):
170
+ if len(planned_sheets) == num_sheets: return planned_sheets
171
+ else:
172
+ msg = f"AI ha proposto {len(planned_sheets)} fogli, richiesti {num_sheets}. Verifica."
173
+ log_message(msg, level="warning"); st.session_state.warning_message = msg
174
+ return planned_sheets
175
  else:
176
+ msg = "Struttura JSON piano fogli AI non valida."
177
+ log_message(msg, level="error"); st.session_state.error_message = msg
178
+ return None
179
+ except json.JSONDecodeError as e:
180
+ msg = f"Errore decodifica JSON piano fogli: {e}. Risposta: {response_content}"
181
+ log_message(msg, level="error"); st.session_state.error_message = msg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  return None
183
+ except Exception as e:
184
+ log_message(f"Errore OpenAI piano fogli: {e}", level="error"); st.session_state.error_message = f"Errore OpenAI pianificazione: {e}"
 
 
185
  return None
186
+
187
+ def call_openai_for_sheet_script(sheet_purpose, sheet_name, prev_scripts_ctx):
188
+ st.session_state.error_message = ""
189
+ try:
190
+ user_content = (
191
+ f"TITOLO_FOGLIO_RICHIESTO: '{sheet_name}'.\n"
192
+ f"Descrizione per '{sheet_name}':\n{sheet_purpose}\n\n"
193
+ )
194
+ if prev_scripts_ctx:
195
+ user_content += f"CONTESTO_FOGLI_PRECEDENTI:\n{prev_scripts_ctx}\n"
196
+
197
+ completion = client.chat.completions.create(
198
+ model=MODEL_NAME,
199
+ messages=[
200
+ {"role": "system", "content": BASE_PROMPT_NO_BACKTICKS},
201
+ {"role": "user", "content": user_content}
202
+ ],
203
+ temperature=0.1,
204
+ )
205
+ script = completion.choices[0].message.content.strip()
206
+ if script.startswith("```python"): script = script[9:]
207
+ if script.endswith("```"): script = script[:-3]
208
+ return script.strip()
209
+ except Exception as e:
210
+ st.session_state.error_message = f"Errore OpenAI script foglio '{sheet_name}': {e}"
211
+ log_message(st.session_state.error_message, level="error")
212
  return None
213
+
214
+ def call_openai_for_script_correction(original_script, error_traceback, sheet_name, sheet_purpose, prev_scripts_ctx):
215
+ st.session_state.error_message = "" # Resetta per la chiamata di correzione
216
+ log_message(f"Tentativo di correzione AI per foglio '{sheet_name}'.")
217
+ try:
218
+ prompt = PROMPT_FOR_SCRIPT_CORRECTION.format(
219
+ sheet_name=sheet_name,
220
+ sheet_purpose=sheet_purpose,
221
+ previous_scripts_context=prev_scripts_ctx if prev_scripts_ctx else "Nessuno.",
222
+ original_script=original_script,
223
+ error_traceback=error_traceback
224
+ )
225
+ completion = client.chat.completions.create(
226
+ model=MODEL_NAME,
227
+ messages=[{"role": "user", "content": prompt}], # Il system prompt è inglobato per semplicità qui
228
+ temperature=0.2, # Leggermente più creativo per trovare soluzioni
229
+ )
230
+ corrected_script = completion.choices[0].message.content.strip()
231
+ if corrected_script.startswith("```python"): corrected_script = corrected_script[9:]
232
+ if corrected_script.endswith("```"): corrected_script = corrected_script[:-3]
233
+ log_message(f"Script corretto proposto dall'AI per '{sheet_name}'.")
234
+ return corrected_script.strip()
235
+ except Exception as e:
236
+ st.session_state.error_message = f"Errore OpenAI correzione script per '{sheet_name}': {e}"
237
+ log_message(st.session_state.error_message, level="error")
238
+ return None
239
+
240
+ def execute_single_sheet_script(script_code, workbook_obj, sheet_name_being_processed):
241
+ #st.session_state.error_message = "" # Non resettare qui, l'errore può venire da OpenAI
242
+ current_error_message_for_exec = ""
243
+ capture_out = io.StringIO()
244
+ original_stdout = sys.stdout
245
+ sys.stdout = capture_out
246
+ execution_success = False
247
+ script_globals = {
248
+ 'wb': workbook_obj, 'openpyxl': openpyxl,
249
+ 'Font': Font, 'PatternFill': PatternFill,
250
+ 'Border': Border, 'Side': Side, 'Alignment': Alignment
251
+ }
252
+ detailed_error_for_correction = ""
253
+ try:
254
+ exec(script_code, script_globals)
255
+ execution_success = True
256
+ except Exception as e:
257
+ tb_str = traceback.format_exc()
258
+ current_error_message_for_exec = f"Errore esecuzione script per foglio '{sheet_name_being_processed}':\n{type(e).__name__}: {e}\nTraceback:\n{tb_str}"
259
+ detailed_error_for_correction = f"{type(e).__name__}: {e}\n{tb_str}"
260
+ # Non loggare qui come errore fatale subito, potrebbe essere corretto
261
+ finally:
262
+ sys.stdout = original_stdout
263
+ execution_log = capture_out.getvalue()
264
+ capture_out.close()
265
+
266
+ if not execution_success: # Solo se fallisce, aggiorna l'errore di session_state
267
+ st.session_state.error_message += current_error_message_for_exec + "\n"
268
+
269
+
270
+ return execution_success, execution_log, detailed_error_for_correction
271
+
272
+
273
+ def render_sidebar():
274
+ with st.sidebar:
275
+ st.header("🤖 Input per l'AI")
276
+ st.session_state.initial_excel_request = st.text_area(
277
+ "📝 Descrivi l'Excel",
278
+ value=st.session_state.initial_excel_request, height=200,
279
+ help="Sii descrittivo. Specifica se i fogli devono leggere dati da altri."
280
+ )
281
+ st.session_state.num_sheets_requested = st.number_input(
282
+ "🔢 Numero di fogli:", min_value=1, max_value=20,
283
+ value=st.session_state.num_sheets_requested, step=1
284
+ )
285
+
286
+ def render_main_content():
287
+ st.title("📊 Generatore Excel AI")
288
+ st.markdown("Definisci richiesta e numero Fogli, poi l'AI **pianificherà, genererà, collegherà** e tenterà di auto-correggere gli script dei fogli")
289
+
290
+ if st.sidebar.button("💡 Pianifica Fogli con AI", type="primary", use_container_width=True):
291
+ st.session_state.ai_sheet_plan = None
292
+ st.session_state.generated_scripts_for_sheets = []
293
+ st.session_state.process_log = []
294
+ st.session_state.error_message = ""
295
+ st.session_state.warning_message = ""
296
+ st.session_state.final_excel_file_path = None
297
+ st.session_state.generation_started = False
298
+ log_message("Avvio pianificazione fogli...")
299
+ with st.spinner("L'AI sta pianificando i fogli..."):
300
+ plan = call_openai_for_sheet_plan(st.session_state.initial_excel_request, st.session_state.num_sheets_requested)
301
+ if plan:
302
+ st.session_state.ai_sheet_plan = plan
303
+ log_message(f"Piano fogli ricevuto: {len(plan)} fogli.")
304
+ st.success(f"✅ L'AI ha pianificato {len(plan)} fogli! Rivedi/modifica descrizioni.")
305
  else:
306
+ log_message("Fallimento pianificazione.", level="error")
307
+ st.error(f"Impossibile pianificare. {st.session_state.error_message}")
308
+ st.rerun()
309
+
310
+ if st.session_state.ai_sheet_plan:
311
+ st.markdown("---")
312
+ st.subheader("📋 Piano Fogli Proposto dall'AI (Modificabile)")
313
+ st.caption("Modifica descrizioni se necessario, specialmente per chiarire collegamenti.")
314
+ for i, sheet_def in enumerate(st.session_state.ai_sheet_plan):
315
+ with st.expander(f"Foglio {i+1}: **{sheet_def['sheet_name']}**", expanded=True):
316
+ new_purpose = st.text_area(
317
+ f"Scopo/Contenuto per '{sheet_def['sheet_name']}':",
318
+ value=sheet_def['sheet_purpose'], key=f"sheet_purpose_edit_{i}", height=120
319
+ )
320
+ if new_purpose != sheet_def['sheet_purpose']:
321
+ st.session_state.ai_sheet_plan[i]['sheet_purpose'] = new_purpose
322
+
323
+ if st.button(" Genera, Assembla e Auto-Correggi Excel ✨", type="primary", use_container_width=True, disabled=st.session_state.generation_started):
324
+ st.session_state.generation_started = True
325
+ # Conserva log pianificazione, pulisce log di vecchie generazioni
326
+ planning_logs = [log for log in st.session_state.process_log if "pianificazione" in log.lower() or "piano fogli ricevuto" in log.lower()]
327
+ st.session_state.process_log = planning_logs
328
+ st.session_state.error_message = ""
329
+ st.session_state.warning_message = ""
330
+ st.session_state.generated_scripts_for_sheets = [] # Visualizzazione finale
331
+ st.session_state.final_excel_file_path = None
332
+
333
+ st.session_state.workbook_object = openpyxl.Workbook()
334
+ if st.session_state.workbook_object.sheetnames:
335
+ st.session_state.workbook_object.remove(st.session_state.workbook_object.active)
336
+ log_message("Workbook inizializzato per generazione.")
337
+
338
+ generated_scripts_history_for_context = [] # Per passare contesto all'AI
339
+ all_sheets_successful = True
340
+ progress_bar = st.progress(0)
341
+ num_total_sheets = len(st.session_state.ai_sheet_plan)
342
+
343
+ for i, sheet_def in enumerate(st.session_state.ai_sheet_plan):
344
+ sheet_name = sheet_def["sheet_name"]
345
+ sheet_purpose = sheet_def["sheet_purpose"]
346
+ progress_text = f"Foglio {i+1}/{num_total_sheets}: '{sheet_name}'"
347
+ log_message(f"--- Inizio {progress_text} ---")
348
+ progress_bar.progress((i + 1) / num_total_sheets, text=f"Generando: {progress_text}")
349
+
350
+ previous_scripts_context_str = ""
351
+ if generated_scripts_history_for_context:
352
+ parts = ["'''\nScript fogli precedenti (riferimento/collegamenti):\n"]
353
+ for idx, prev_info in enumerate(generated_scripts_history_for_context):
354
+ parts.append(f"--- Script Foglio '{prev_info['name']}' (Indice {idx}) ---\n{prev_info['script']}\n")
355
+ parts.append("'''\n")
356
+ previous_scripts_context_str = "\n".join(parts)
357
+
358
+ current_script_for_sheet = None
359
+ correction_attempts = 0
360
+ sheet_successfully_processed = False
361
+
362
+ # Tentativo iniziale di generazione script
363
+ spinner_msg = f"🤖 AI genera script per '{sheet_name}' (contesto: {len(generated_scripts_history_for_context)} fogli)..."
364
+ with st.spinner(spinner_msg):
365
+ current_script_for_sheet = call_openai_for_sheet_script(sheet_purpose, sheet_name, previous_scripts_context_str)
366
+
367
+ if current_script_for_sheet:
368
+ log_message(f"Script iniziale per '{sheet_name}' generato.")
369
+
370
+ # Ciclo di esecuzione e correzione
371
+ while correction_attempts <= MAX_CORRECTION_ATTEMPTS and not sheet_successfully_processed:
372
+ exec_spinner_msg = f"⚙️ Esecuzione script per '{sheet_name}'"
373
+ if correction_attempts > 0:
374
+ exec_spinner_msg += f" (tentativo correzione {correction_attempts})"
375
+
376
+ with st.spinner(exec_spinner_msg):
377
+ success, exec_log, error_details_for_correction = execute_single_sheet_script(
378
+ current_script_for_sheet, st.session_state.workbook_object, sheet_name
379
+ )
380
+ if exec_log: log_message(f"Log esecuzione per '{sheet_name}':\n{exec_log}")
381
+
382
+ if success:
383
+ log_message(f"Foglio '{sheet_name}' aggiunto/modificato con successo.")
384
+ st.session_state.generated_scripts_for_sheets.append({"name": sheet_name, "script": current_script_for_sheet, "status": "Successo"})
385
+ generated_scripts_history_for_context.append({"name": sheet_name, "script": current_script_for_sheet})
386
+ sheet_successfully_processed = True
387
+ else:
388
+ log_message(f"⚠️ Fallimento esecuzione script per '{sheet_name}'. Errore: {error_details_for_correction}", level="warning")
389
+ correction_attempts += 1
390
+ if correction_attempts <= MAX_CORRECTION_ATTEMPTS:
391
+ log_message(f"Tentativo {correction_attempts}/{MAX_CORRECTION_ATTEMPTS} di auto-correzione AI per '{sheet_name}'.")
392
+ with st.spinner(f"🤖 AI tenta correzione per '{sheet_name}' (errore: {error_details_for_correction.splitlines()[0]}..."):
393
+ corrected_script_candidate = call_openai_for_script_correction(
394
+ current_script_for_sheet, error_details_for_correction,
395
+ sheet_name, sheet_purpose, previous_scripts_context_str
396
+ )
397
+ if corrected_script_candidate:
398
+ log_message(f"Nuovo script corretto proposto per '{sheet_name}'.")
399
+ current_script_for_sheet = corrected_script_candidate
400
+ # Loop rieseguirà execute_single_sheet_script
401
+ else:
402
+ log_message(f"AI non ha fornito script corretto per '{sheet_name}'. Tentativo {correction_attempts} fallito.", level="error")
403
+ st.session_state.generated_scripts_for_sheets.append({"name": sheet_name, "script": current_script_for_sheet, "status": f"Fallito dopo correzione (AI non ha corretto)", "error": error_details_for_correction})
404
+ break # Esce dal ciclo di correzione se AI non dà nulla
405
+ else:
406
+ log_message(f"Limite tentativi ({MAX_CORRECTION_ATTEMPTS}) di correzione raggiunto per '{sheet_name}'. Errore finale: {error_details_for_correction}", level="error")
407
+ st.session_state.generated_scripts_for_sheets.append({"name": sheet_name, "script": current_script_for_sheet, "status": f"Fallito (max tentativi)", "error": error_details_for_correction})
408
+ else: # Fallimento generazione script iniziale
409
+ log_message(f"⚠️ Fallimento generazione script iniziale per '{sheet_name}'. {st.session_state.error_message}", level="error")
410
+ st.session_state.generated_scripts_for_sheets.append({"name": sheet_name, "script": "# ERRORE GENERAZIONE SCRIPT", "status": "Fallimento Generazione", "error": st.session_state.error_message})
411
+
412
+ if not sheet_successfully_processed:
413
+ all_sheets_successful = False
414
+ break # Interrompe generazione se un foglio fallisce definitivamente
415
+ log_message(f"--- Fine Foglio {i+1}: '{sheet_name}' ---")
416
+ progress_bar.empty()
417
+
418
+ if all_sheets_successful and st.session_state.workbook_object:
419
+ try:
420
+ if os.path.exists(st.session_state.final_excel_filename): os.remove(st.session_state.final_excel_filename)
421
+ st.session_state.workbook_object.save(st.session_state.final_excel_filename)
422
+ st.session_state.final_excel_file_path = st.session_state.final_excel_filename
423
+ log_message(f"Workbook '{st.session_state.final_excel_filename}' salvato.")
424
+ st.success(f"✅ Excel '{st.session_state.final_excel_filename}' generato!")
425
+ except Exception as e:
426
+ log_message(f"❌ Errore salvataggio Workbook: {e}", level="error"); st.session_state.error_message = f"Errore salvataggio: {e}"
427
+ elif not all_sheets_successful:
428
+ st.error(f"Processo interrotto. Excel non salvato. {st.session_state.error_message}")
429
  else:
430
+ st.warning("Nessun foglio elaborato o problema sconosciuto.")
431
+ st.session_state.generation_started = False
432
+ st.rerun()
433
+
434
+ if st.session_state.warning_message: st.warning(st.session_state.warning_message)
435
+ # Mostra errore principale solo se non c'è un file di successo e non siamo in fase di generazione attiva
436
+ if st.session_state.error_message and not st.session_state.final_excel_file_path and not st.session_state.generation_started:
437
+ st.error(f"Si sono verificati errori durante l'ultimo processo: {st.session_state.error_message}")
438
+
439
+ if st.session_state.generated_scripts_for_sheets:
440
+ st.markdown("---")
441
+ with st.expander("🔍 Vedi Script Generati per i Fogli (e Stato Esecuzione)"):
442
+ for item in st.session_state.generated_scripts_for_sheets:
443
+ # Modifica questa riga:
444
+ sheet_name_display = item.get('name', 'Nome Foglio Mancante')
445
+ status_display = item.get('status', 'Stato Sconosciuto')
446
+ script_display = item.get('script', '# Script non disponibile')
447
+ error_display = item.get('error', None)
448
+
449
+ st.subheader(f"Script per '{sheet_name_display}' - Stato: {status_display}")
450
+ st.code(script_display, language="python")
451
+ if error_display: # Controlla se la chiave 'error' esiste e ha un valore
452
+ st.error(f"Dettaglio Errore Finale per '{sheet_name_display}':\n{error_display}")
453
+
454
+ if st.session_state.process_log:
455
+ st.markdown("---")
456
+ with st.expander("📝 Log del Processo Dettagliato", expanded=False):
457
+ st.text_area("Log:", "".join(st.session_state.process_log), height=300, disabled=True)
458
+
459
+ render_download_section()
460
+ render_footer()
461
+
462
+ def render_download_section():
463
+ if st.session_state.final_excel_file_path and os.path.exists(st.session_state.final_excel_file_path):
464
+ st.markdown("---")
465
+ st.header("📥 Download File Excel Finale")
466
+ try:
467
+ with open(st.session_state.final_excel_file_path, "rb") as fp: excel_bytes = fp.read()
468
+ st.download_button(
469
+ label=f"💾 Scarica {st.session_state.final_excel_filename}", data=excel_bytes,
470
+ file_name=st.session_state.final_excel_filename,
471
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
472
+ use_container_width=True, type="primary"
 
 
 
473
  )
474
+ except Exception as e:
475
+ st.error(f"Impossibile leggere il file per il download: {e}")
476
+ log_message(f"Errore lettura file per download: {e}", level="error")
477
+ elif st.session_state.ai_sheet_plan and not st.session_state.generation_started:
478
+ st.info("Una volta generato l'Excel, il download apparirà qui.")
479
+
480
+ def render_footer():
481
+ st.markdown("---")
482
+ st.markdown("💡 **Nota:** L'AI tenta di collegare i fogli e auto-correggere errori. Verifica sempre i risultati e le formule.")
483
+
 
 
484
  def main():
485
+ initialize_session_state()
486
+ render_sidebar()
487
+ render_main_content()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
 
489
  if __name__ == "__main__":
490
+ main()