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'")
|