MatteoScript commited on
Commit
6917646
·
verified ·
1 Parent(s): c6d1cc3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +75 -46
app.py CHANGED
@@ -19,45 +19,49 @@ import fitz
19
  import re
20
  import io
21
  from collections import Counter
22
- import secrets
23
 
24
  st.set_page_config(page_title="Import Fatture AI✨")
 
25
 
26
- from streamlit_google_auth import Authenticate
27
- google_auth_str = os.getenv("JSON_GOOGLE_SECRET")
28
- google_auth_data = json.loads(google_auth_str)
29
- with open("google_credentials.json", "w") as f:
30
- json.dump(google_auth_data, f, indent=4)
31
-
32
- authenticator = Authenticate(
33
- secret_credentials_path='google_credentials.json',
34
- cookie_name=f"llm_pdf_digest_{secrets.token_hex(4)}",
35
- cookie_key=secrets.token_hex(32),
36
- redirect_uri=os.getenv("URL_REDIRECT"),
37
- )
 
 
 
 
 
38
 
39
- st.title("Import Fatture AI ✨")
40
  with st.expander("Guida completa"):
41
  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.
42
 
43
  ## Funzionalità Principali
44
 
45
  - **Caricamento e Gestione dei Documenti**
46
- - Supporta il caricamento di file PDF, JPG, JPEG e PNG tramite un’interfaccia Streamlit.
47
- - 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.
48
 
49
  - **Conversione dei Dati**
50
- - **Upload e Inoltro a Gemini**: I file vengono caricati e inviati al rispettivo servizio AI.
51
- - **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).
52
 
53
  - **Validazione e Verifica**
54
- - **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.
55
- - **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.
56
- - **Filtraggio Articoli**: Vengono mantenuti solo gli articoli compatibili con i criteri specifici (codici articolo e importi non nulli).
57
 
58
  - **Visualizzazione e Highlighting**
59
- - I dati validati vengono mostrati in formato tabellare e in JSON.
60
- - Se il documento è un PDF, il sistema evidenzia graficamente (con rettangoli rossi) i testi relativi agli articoli compatibili, semplificando il controllo visivo.
61
 
62
  ## Avvertenze per l'Operatore
63
 
@@ -75,14 +79,6 @@ st.write("🤖 **Sfrutta l'AI di Gemini:** Per ogni documento, estrae i dati in
75
  st.write("✅ **Mostra Articoli Compatibili:** Filtra e visualizza solo gli articoli che rispettano i criteri richiesti.")
76
  st.write("🔍 **Anteprima Documento:** Visualizza un'anteprima del documento evidenziando gli articoli compatibili.")
77
 
78
- authenticator.check_authentification()
79
- authenticator.login()
80
-
81
- if not st.session_state.get('connected'):
82
- with st.sidebar:
83
- st.title("Login")
84
- st.write("Seleziona l'account aziendale per accedere")
85
- st.stop()
86
 
87
  GENERATION_CONFIG = settings_ai.GENERATION_CONFIG
88
  SYSTEM_INSTRUCTION = settings_ai.SYSTEM_INSTRUCTION
@@ -92,7 +88,7 @@ API_KEY_GEMINI = settings_ai.API_KEY_GEMINI
92
  # Configura il modello Gemini
93
  genai.configure(api_key=API_KEY_GEMINI)
94
  model = genai.GenerativeModel(
95
- model_name="gemini-2.0-flash",
96
  generation_config=GENERATION_CONFIG,
97
  system_instruction=SYSTEM_INSTRUCTION
98
  )
@@ -119,7 +115,7 @@ def wait_for_files_active(files):
119
  print("\n...all files ready")
120
 
121
  # Chiamata API Gemini
122
- def send_message_to_gemini(chat_session, message, max_attempts=3):
123
  """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. """
124
  for attempt in range(max_attempts):
125
  try:
@@ -238,7 +234,14 @@ def process_document_splitted(file_path: str, chunk_label: str, use_azure: bool
238
  files = [upload_to_gemini(file_path, mime_type=mime_type)]
239
  wait_for_files_active(files)
240
  chat_history = [{ "role": "user","parts": [files[0]]}]
241
- chat_session = model.start_chat(history=chat_history)
 
 
 
 
 
 
 
242
  max_validation_attempts = 3
243
  max_number_reprocess = 3
244
  chunk_document = None
@@ -289,7 +292,7 @@ def process_document(path_file: str, number_pages_split: int, use_azure: bool =
289
  if mime_type is None:
290
  mime_type = "application/octet-stream"
291
  if use_azure:
292
- number_pages_split = 2
293
  if not path_file.lower().endswith(".pdf"):
294
  print("File non PDF: elaborazione come immagine.")
295
  documento_finale = process_document_splitted(path_file, chunk_label="(immagine)", use_azure=use_azure)
@@ -325,14 +328,31 @@ def process_document(path_file: str, number_pages_split: int, use_azure: bool =
325
  if documento_finale is None:
326
  raise RuntimeError("Nessun documento elaborato.")
327
 
328
- # Controlli aggiuntivi: Se esiste un AVE non possono esistere altri articoli non ave. Se articoli DOPPI segnalo!
329
  if any(articolo.CodiceArticolo.startswith("AVE") for articolo in documento_finale.Articoli):
330
  documento_finale.Articoli = [articolo for articolo in documento_finale.Articoli if articolo.CodiceArticolo.startswith("AVE")]
331
- combinazioni = [(articolo.CodiceArticolo, articolo.TotaleNonIvato) for articolo in documento_finale.Articoli]
332
- conta_combinazioni = Counter(combinazioni)
333
- for articolo in documento_finale.Articoli:
334
- if conta_combinazioni[(articolo.CodiceArticolo, articolo.TotaleNonIvato)] > 1:
335
- articolo.Verificato = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  return documento_finale
337
 
338
  # Analizza Fattura con AZURE
@@ -367,11 +387,15 @@ def parse_invoice_to_documento_azure(result) -> Documento:
367
  if items_field and items_field.value_array:
368
  for item in items_field.value_array:
369
  product_code_field = item.value_object.get("ProductCode")
 
 
 
370
  codice_articolo = product_code_field.value_string if product_code_field and product_code_field.value_string else ""
371
  amount_field = item.value_object.get("Amount")
372
  totale_non_ivato = amount_field.value_currency.amount if amount_field and amount_field.value_currency else 0.0
373
  articolo = Articolo(
374
  CodiceArticolo=codice_articolo,
 
375
  TotaleNonIvato=totale_non_ivato,
376
  Verificato=None
377
  )
@@ -391,9 +415,11 @@ def main():
391
  #st.set_page_config(page_title="Import Fatture AI", page_icon="✨")
392
  st.sidebar.title("Caricamento File")
393
  uploaded_files = st.sidebar.file_uploader("Seleziona uno o più PDF", type=["pdf", "jpg", "jpeg", "png"], accept_multiple_files=True)
394
- model_ai = st.sidebar.selectbox("Modello", ['Gemini Flash 2.0']) # 'Azure Intelligence'])
 
 
395
  use_azure = True if model_ai == 'Azure Intelligence' else False
396
- number_pages_split = st.sidebar.slider('Split Pagine', 1, 30, 2, help="Numero suddivisione pagine del PDF. Più il numero è basso e più il modello AI è preciso, più è alto più è veloce")
397
  if st.sidebar.button("Importa", type="primary", use_container_width=True):
398
  if not uploaded_files:
399
  st.warning("Nessun file caricato!")
@@ -417,15 +443,18 @@ def main():
417
  f"- **Articoli Compatibili**: {len(doc.Articoli)}\n"
418
  f"- **Totale Documento**: {format_euro(doc.TotaleImponibile)}\n"
419
  )
 
 
420
  if totale_non_ivato_non_verificato > 0:
421
- st.error(f"Totale Ave Non Verificato: {format_euro(totale_non_ivato_verificato)}")
422
- elif totale_non_ivato != 0:
423
  st.success(f"Totale Ave Verificato: {format_euro(totale_non_ivato_verificato)}")
424
  df = pd.DataFrame([{k: v for k, v in Articolo.model_dump().items() if k != ""} for Articolo in doc.Articoli])
425
  if 'Verificato' in df.columns:
426
  df['Verificato'] = df['Verificato'].apply(lambda x: "✅" if x == 1 else "❌" if x == 0 else "❓" if x == 2 else x)
427
  if totale_non_ivato > 0:
428
- st.dataframe(df, use_container_width=True ,column_config={"TotaleNonIvato": st.column_config.NumberColumn("Totale non Ivato",format="€ %.2f")})
 
429
  st.json(doc.model_dump(), expanded=False)
430
  if totale_non_ivato == 0:
431
  st.info(f"Non sono presenti articoli 'AVE'")
 
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
 
 
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
 
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
  )
 
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:
 
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
 
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)
 
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
 
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
  )
 
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', 'Azure Intelligence'])
419
+ if model_ai == 'Gemini 2.5 Pro':
420
+ st.session_state.model = "gemini-2.5-pro-exp-03-25"
421
  use_azure = True if model_ai == 'Azure Intelligence' else False
422
+ 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")
423
  if st.sidebar.button("Importa", type="primary", use_container_width=True):
424
  if not uploaded_files:
425
  st.warning("Nessun file caricato!")
 
443
  f"- **Articoli Compatibili**: {len(doc.Articoli)}\n"
444
  f"- **Totale Documento**: {format_euro(doc.TotaleImponibile)}\n"
445
  )
446
+ if totale_non_ivato > doc.TotaleImponibile and doc.TotaleImponibile > 0:
447
+ st.warning("Totale Ave maggiore di Totale Merce")
448
  if totale_non_ivato_non_verificato > 0:
449
+ st.error(f"Totale Ave Non Verificato: {format_euro(totale_non_ivato_non_verificato)}")
450
+ if totale_non_ivato > 0:
451
  st.success(f"Totale Ave Verificato: {format_euro(totale_non_ivato_verificato)}")
452
  df = pd.DataFrame([{k: v for k, v in Articolo.model_dump().items() if k != ""} for Articolo in doc.Articoli])
453
  if 'Verificato' in df.columns:
454
  df['Verificato'] = df['Verificato'].apply(lambda x: "✅" if x == 1 else "❌" if x == 0 else "❓" if x == 2 else x)
455
  if totale_non_ivato > 0:
456
+ df["TotaleNonIvato"] = df["TotaleNonIvato"].apply(format_euro)
457
+ st.dataframe(df, use_container_width=True)
458
  st.json(doc.model_dump(), expanded=False)
459
  if totale_non_ivato == 0:
460
  st.info(f"Non sono presenti articoli 'AVE'")