Spaces:
Runtime error
Runtime error
Update app.py
Browse files
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 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
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=
|
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=
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 =
|
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.
|
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 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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'
|
|
|
|
|
395 |
use_azure = True if model_ai == 'Azure Intelligence' else False
|
396 |
-
number_pages_split = st.sidebar.slider('Split Pagine', 1, 30,
|
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(
|
422 |
-
|
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 |
-
|
|
|
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'")
|