;PjI dZddlZddlZddlZddlZddlZddlZddlZddlmZddl m Z ddl Z ddl Z ddl ZddlmZddlmZee ej(j(dz e dZe d Ze d Ze d Zd Zd ZdZdZdZej<ddZej<ddZ ej<ddZ!ej<ddZ"da#defdZ$de de%fdZ&de de%fdZ'de de(e%e)dzffdZ*de de%fdZ+dZ,de%de)fd Z-de%de.e%fd!Z/d"e.e%de.e.e0fd#Z1de de%fd$Z2d%e%de3fd&Z4d%e%d'e%d(e)d)e5d*e%ddf d+Z6d%e%d'e%d"e.e%d,e.e.e0d(e)ddf d-Z7de d(e)de fd.Z8de%ddfd/Z9d%e%d'e%d(e)d)e5ddf d0Z:de de3fd1Z;de d%e%d(e)de3fd2Zddl>Z>e?e>jd4kDre;e e>jd4yeAd5yy)6u Pipeline di ingestione documenti per vault-secondbrain. Estrae testo, classifica con LLM, chunk + embedding → LanceDB, archivia in docs/. N)datetime)Path) load_dotenv)SentenceTransformerz.envz(/mnt/ssd/data/vault-secondbrain/vault.dbz'/mnt/ssd/data/vault-secondbrain/lancedbz%/mnt/ssd/data/vault-secondbrain/inboxz$/mnt/ssd/data/vault-secondbrain/docsiz;sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2zanthropic/claude-haiku-4-5i OPENROUTER_BASE_URLzhttps://openrouter.ai/api/v1OPENROUTER_API_KEYADRIAN_BOT_TOKENTELEGRAM_CHAT_IDreturnc8tttatSN) _embedderr EMBED_MODEL+/mnt/ssd/adrian/vault-secondbrain/ingest.py get_embedderr-s' 4 rpathc|jj}|dk(r t|S|dvr t|S|dvr|j dSt d|)N.pdf)z.docxz.doc)z.txtz.mdignore)errorszTipo file non supportato: )suffixlower _extract_pdf _extract_docx read_text ValueError)rrs r extract_textr!6sg [[   F D!! $ $T"" ? "~~X~..5fX>??rcddlm}|t|}g}|jD]'}|j xsd}|j |)dj |S)Nr) PdfReaderr  )pypdfr#strpagesr!appendjoin)rr#readerr'pagets rrrBsZ s4y !F E      %2 Q 99U rc ddl}ddlm}ddlm}t d|j |t|dd}g}|dd D]c}|}|j|d d |j|jj}|jd dd|ide|jddd tjtddt dddd|dgdddd} | j#| j%ddd d!j'} t)j*d"| t(j,} | st/d#| ddt%j0| j3} | j5d$d%xsd%} t d&t7| d'| | fS#t8$r}t d(|Yd}~y)d}~wwxYw)*uConverte PDF scansionato in immagini e le manda a Claude Vision. Ritorna (testo_estratto, classificazione) — classificazione può essere None se fallisce.rN)convert_from_path)BytesIOz[ingest] Vision in corso: jpeg)dpifmtJPEGU)formatquality image_urlurlzdata:image/jpeg;base64,)typer9textuAnalizza questo documento di Mauro Gagliardi (famiglia: moglie Nadia, figli Samuele e Francesco). Restituisci un JSON con questi campi: { "ambito": "lavoro" | "personale" | "famiglia", "categoria": "", "soggetto": "", "data_doc": "", "confident": true | false, "motivo": "", "testo_estratto": "" } Regole: - ambito lavoro: documenti aziendali Mediaset, buste paga, contratti di lavoro - ambito famiglia: documenti di Samuele, Francesco, Nadia - ambito personale: documenti intestati a Mauro ma non lavorativi - categoria = NATURA del documento (cosa è), NON la persona. Es: fattura dentista di Samuele → categoria='medico/odontoiatria', soggetto='samuele' Rispondi SOLO con il JSON.)r;r</chat/completionsBearer application/json Authorizationz Content-Typezanthropic/claude-sonnet-4-5userrolecontentimodelmessages temperature max_tokens<headersjsontimeoutchoicesmessagerE\{.*\}z#Nessun JSON nella risposta Vision: testo_estrattor z[ingest] Vision completato: z chars estrattiz[ingest] Vision fallito: )r N)base64 pdf2imager.ior/printnamer&save b64encodegetvaluedecoder(httpxpostOPENROUTER_URLOPENROUTER_KEYraise_for_statusrNstripresearchDOTALLr loadsgrouppoplen Exception)rrTr.r/imagesrEimgbufb64resprawmatchdatatestoes r _vision_pdfruLs+ &tyyk 23 s4ycv >FGbqz i VR0s||~.557#:3%!@A    NN ).zz/ 0#*>*:!; 2 7&,AB "     iik)$Q' 29=CCE )S"))4B3t9+NO Ozz%++-()2.4" ,SZLHId{  )!-.s?C9F99 GGGctddlm}|t|}djd|jDS)Nr)Documentr$c3jK|]+}|jjs|j-ywr)r<rb).0ps r z _extract_docx..s Fqvv||~QVVFs33)docxrwr&r) paragraphs)rrwdocs rrrs, 3t9 C 99FS^^F FFruuSei un assistente che classifica documenti personali di Mauro Gagliardi (famiglia: moglie Nadia, figli Samuele e Francesco). Analizza il testo fornito e restituisci un JSON con questi campi: {{ "ambito": "lavoro" | "personale" | "famiglia", "categoria": "", "soggetto": "", "data_doc": "", "confident": true | false, "motivo": "" }} Regole: - ambito "lavoro": documenti aziendali, contratti di lavoro, buste paga, comunicazioni ufficio Mediaset - ambito "famiglia": documenti che riguardano i figli (Samuele, Francesco) o la moglie (Nadia) - ambito "personale": documenti intestati a Mauro ma non lavorativi (casa, fiscale, medico, auto, ecc.) - categoria = NATURA del documento (cosa è il documento), NON la persona — es. una fattura dentista è "medico/odontoiatria" anche se intestata a Samuele - soggetto = persona fisica coinvolta (null se il documento riguarda Mauro stesso o è generico) - confident=false se il testo è troppo breve, illeggibile, o ambiguo - Rispondi SOLO con il JSON, nessun testo aggiuntivo. Testo documento (prime {n} parole): --- {testo} --- r<c h|dtj}tj|t |j } t jtddtddtd|dgdd d d }|j|jd dddj}tjd|tj}|st!d|dd tj"|j%S#t&$r}ddddd|dcYd}~Sd}~wwxYw)N)rsnr=r>r?r@rBrCrr0rFrLrPrQrErRz(Nessun JSON trovato nella risposta LLM: zda-organizzareinboxFzerrore classificazione: )ambito categoriadata_doc confidentmotivo) PREVIEW_CHARSrbCLASSIFY_PROMPTr7risplitr]r^r_r` LLM_MODELrarNrcrdrer rfrgrj)r<previewpromptrorErqrts rclassify_documentrs8>M"((*G  # #'S5I # JF zz/ 0#*>*:!; 2 #&,@A !     ))+i(+I6yAGGI )Wbii8GPTQT WX Xzz%++-((  & 04    sC D D1 D,&D1,D1cdj|j}|sgSg}d}|t|kr<|tz}|j ||||tt z z }|t|kr<|S)N r)r)rri CHUNK_SIZEr( CHUNK_OVERLAP)r<chunksstartends r chunk_textrst 88DJJL !D  F E #d) j  d5o& m++ #d)  Mrrc|sgSt}|j|dd}|Dcgc]}|jc}Scc}w)NFT)show_progress_barnormalize_embeddings)rencodetolist)rrGvecsvs r embed_chunksrs?  NE <<%d< SD $ %1AHHJ %% %sAcntj|jj}|ddS)N)hashlibsha256 read_bytes hexdigest)rhs rcompute_doc_idrs+t()335A Sb6Mrdoc_idctjt}|jd|fj }|j |duS)NzOSELECT id FROM items WHERE type='documento' AND json_extract(meta,'$.doc_id')=?)sqlite3connectVAULT_DBexecutefetchoneclose)rconnrows ris_already_ingestedrsG ??8 $D ,,Y   hj JJL d?r nome_fileclassificazionen_chunks path_archiviocd|}||d|d|d|jd|jdt|jjj dd|||jd d t j jd }tjt}|jd |d |jddtj|f|j|jy)Nzdoc-rrrsoggettor.ingeritorF) rrfonterrrrtipostatorrclassificato_auto ingested_atzBINSERT OR REPLACE INTO items(id, type, body, meta) VALUES(?,?,?,?) documentorr )getrrrlstriprutcnow isoformatrrrrrNdumpscommitr)rrrrritem_idmetars rsave_documentorsVHoG!(+$[1#'' 3#'' 3Y&&,,.55c:&,00eD(224 D ??8 $DLLL +228R@$**TBRS KKMJJLrvettoricTtjtt} |j d}|jd xsdddxs'ttjj } g} t#t%||D]\} \} } | j'||d| d| d|d|d|jd xsd|jd xsdt)|j*j-j/d| d | r|j1| yy#t $rrddl}|j|jd|j|jd|j|jd|j|jd|j|jd|j|jd|j|jd |j|jd |j|jd |j|jd |j|jd g }|jd|}YqwxYw)N documentirrchunk_idrsrrrrrrvettorei)schemar -04drr) rrrsrrrrrrr)lancedbrr&LANCEDB open_tablerjpyarrowrfieldutf8list_float32 create_tablerrryear enumeratezipr(rrrradd)rrrrrdbtblparannorecordsichunkvecs rsave_chunks_lancedbr%s- W &B:mmK($    + 1r2A 6 U#hoo>O>T>T:UDG$S%9:  B(,,Z8>Bi//557>>sC     C : HHX"'') , HHZ"'') , HHW"'') , HHW"'') , HHX"'') , HH["'') , HHZ"'') , HHZ"'') , HHV"'') , HHY"((2::<"= >   ook&o9:sD++E8J'&J'c|jdxsdddxs'ttjj}t |dz |dz |z }|j dd||jz }|jrE|j}|j}||dtjjd |z }tjt|t||S) Nrr rrrT)parentsexist_ok_z%H%M%S)rr&rrrDOCSmkdirrXexistsstemrstrftimeshutilmove)rrrdest_dirdestrrs r archive_filerQs    + 1r2A 6 U#hoo>O>T>T:UDoh///+2NNQUUH NN4$N/ dii D {{}yyTF!HOO$5$>$>x$H#I&RR KKD 3t9% Krctrtsy tjdtdt|dddy#t$rYywxYw)Nzhttps://api.telegram.org/botz /sendMessageHTML)chat_idr< parse_mode )rNrO)TELEGRAM_TOKEN TELEGRAM_CHATr]r^rj)r<s rtelegram_notifyrasI   *>*:, G*DO    s(8 AAc zd|}tjd|||d|d|jd|dd}|d|dd |d|jdr d |dd nd zd|d z}tjt }|j d|d||f|j|jy)Nzlimbo-limborrrpending)rrrrrrr limbo_statou — /z ()r z2 chunk in LanceDB. Da collegare ai nodi esistenti.zAINSERT OR IGNORE INTO items(id, type, body, meta) VALUES(?,?,?,?)nodo) rNrrrrrrrr)rrrrrrbodyrs r_create_limbo_noderpsvhG ::!(+$[1#'' 3   D +U?845Q{7S6TU2A2E2Ej2QR +,A .WY [ (M N O ??8 $DLLK &$% KKMJJLrc td|jt|}t|rtd|jy t |}d}|jso|jjd k(rRt|\}}|js4d |jd }td |jt |y|r|}n t|}td |dd|dd|dd|ds8t d|jd|dd|dd|jddd t|}t|}tdt|dt!||} td| |j"} | t$k7rQ| j'rAt)| j+s(| j-td| jt/||j|t|t1| t3||j|||t5||j|t|t d|jd|dd|ddt|d td!|jy"#t $r@}td|jd|t d|jd|Yd}~yd}~wwxYw)#zQ Processa un file da inbox/. Ritorna True se ingerito, False se saltato. [ingest] u[ingest] già ingerito, skip: Fzngest] errore estrazione z: ⚠️ Ingestione fallita:  Nru⚠️ Ingestione: uN — Vision non ha estratto testo (file corrotto?) File rimasto in inbox/.z$[ingest] Vision: testo vuoto, skip: [ingest] classificato: rrr (confident=rr🗂 Nuovo documento:  Proposta: z Motivo incertezza: r—u< (ingerito comunque — correggi i tag in vault.db se serve)z chunk, embedding donez[ingest] archiviato in: z"[ingest] rimossa directory vuota: ✅  ingerito  ·  chunk[ingest] completato: T)rWrXrrr!rjrrbrrrurrrrrirparentINBOXis_diranyiterdirrmdirrr&rr) rrr<rtvision_classificazionemsgrrrrrs r ingest_filers Idii[ !" D !F6" .tyyk:;D! " ::aP[@\?]^' 45Q 89 ; '+DII;7+H56a 8T7UV""1"5"5h"F!GHJ K  F6"G Ic&k]"8 9: o .D $TF +,[[F 6==?3v~~7G3H  26;;-@A6499os6{CIN 67OLvtyy/3v;G $))O 8 $ %Q{'C&DDV U[ ] !$)) -. s  +DII;b<=8 6!MNs J K&&6K!!K&c |td|jd t|}|j s]|j jdk(r@|jdd xsd }|j st d |jd ytd |d d|dd|dd|ds8t d|jd|d d|dd|jddd t|}t|}t||}|j}|tk7r9|jr)t!|j#s|j%t'||j|t)|t+|t-||j|||t/||j|t)|t d|jd|d d|ddt)|d td|jy#t$r%}t d|jd|Yd}~yd}~wwxYw)uACompleta l'ingestione con classificazione già fornita da Adrian.rz (classificazione da Adrian)r r NFrrSr u ⚠️ u7 — testo vuoto anche dopo classificazione Adrian.r rrrr rrr rz Motivo: rrz (ingerito comunque)rrrrrT)rWrXr!rjrrbrrrhrrrrrrrrrrrrir&rr) rrrr<rtrrrrs ringest_file_with_classifyrs5 Idii[ < =>D! ::Bzz| j 3jk l #OH$=#>aP[@\?]^' 45Q 89 ; '+DII;7+H56a 8T7UV&**8U;<=" #  F6"G o .D [[F 6==?3v~~7G3H 6499os6{CIN 67OLvtyy/3v;G $))O 8 $ %Q{'C&DDV U[ ] !$)) -. M 8 6!MNs H H;H66H;__main__z%Uso: python ingest.py )B__doc__rrNosrcrrtextwraprpathlibrr]rnumpynpdotenvrsentence_transformersr__file__rrrrrrrrrrgetenvr_r`rrrrr&r!rtupledictrurrrlistrfloatrrboolrintrrrrrrr__name__sysriargvrWrrrr5s   5 DN ! ! ( (6 12< = ; < 9 : 8 9  M ,  02PQ/4-r2-r2  ) @t @ @tFdFuS$+%56FRGGG:! C! D! L S T#Y &c&tDK'8&# 33 149=:'''T#Y'!%d5k!2'EI'NR'X t d t   #  $  ssTUX]a8GdGtGT+D+#++QU+\ z 388}qD!%& 56 r