Ë
    qýujp(  ã                   ó`  — d Z ddlZddlZddlmZ ddlmZ ddlmZ ddl	m
Z
mZmZmZmZ ddlmZmZmZ ddlmZ dd	lmZ  ej.                  e«      Zd
ZdZdedefd„Z	 	 ddedededede f
d„Z!dedede fd„Z"ddededefd„Z#dedededefd„Z$dedede fd„Z%dede defd„Z&dedede fd„Z'y)a  The tenant's uploaded documents, as the settings page and the chat see them.

A document is an ingested file: its text lives in the knowledge base so the bot
can answer from it, and the original file sits in blob storage so the bot can
hand the actual PDF to a visitor who asks for it.

Both facts come from the same place -- the metadata on the file's knowledge base
rows -- so there is no separate documents table to keep in sync. The category is
one more key in that metadata.

Links given to visitors point at this service, not at blob storage, and never
expire -- a link left in a chat transcript still works months later. The real
storage link is a SAS token minted when the visitor clicks, valid for LINK_HOURS,
which keeps the storage account private and out of the chat payload.
é    N)Úquote)Úsql)Úsettings)Údelete_vector_data_by_sourceÚget_db_connectionÚget_source_ids_by_nameÚinsert_summaryÚinsert_vector_data)Ú
chunk_textÚgenerate_summaryÚget_embeddings)Úextract_text_from_bytes)Ústorage_serviceé   ÚUncategorisedÚ	tenant_idÚreturnc                 óN   — | j                  d|f«       | j                  «       d   S )zÌSome tenants have a schema but were never ingested into.

    Checking the schema alone is not enough: querying a missing table raises and
    would log a stack trace on every call for those tenants.
    zzSELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = %s AND table_name = 'strategist_knowledge_base')r   )ÚexecuteÚfetchone)Úcurr   s     ú5/var/www/html/strategist-ai/app/services/documents.pyÚ_table_existsr   (   s.   € ð ‡K�Kð	Pà	ˆôð
 �<‰<‹>˜!ÑÐó    ÚuploadsÚcategoryÚuser_idc           
      ó:  — |xs dj                  «       xs t        }g d}}|D �]$  \  }}t        | |d«      D ]H  }t        j	                  d|› d|› d| › �«       t        | |«       	 t        j                  | |› d|› �«       ŒJ t        ||«      }
||
d
z   z  }t        t        j                  «       «      }||d|dœ}|r||d<   t        |
«      }t        j	                  dt        |«      › d|› d|› d�«       |D ]  }t!        || |¬«      }t#        | |||«       Œ  t        j$                  | |› d|› �|«       |j'                  |||dœ«       �Œ' t)        || |¬«      }t+        | d|«       ||dœS # t        $ r&}	t        j                  d|› d|	› �«       Y d	}	~	�Œ^d	}	~	ww xY w)a—  Index the uploaded files and store the originals.

    `uploads` is a list of (file_name, content_bytes). Re-uploading a name
    replaces the previous version, chunks and blob alike, so the tenant does not
    end up with two copies answering the same question.

    Returns the generated summary and a row per stored document, so a caller can
    show what was added without a second listing request.
    Ú ÚfilezReplacing existing file ú (z) in Ú_z"Could not delete the old blob for ú: NÚ
)ÚsourceÚ	source_idÚingestion_typer   r   zCreated z chunks for ú))r   r   )r&   Ú	file_namer   Úfiles)ÚsummaryÚ	documents)ÚstripÚDEFAULT_CATEGORYr   ÚloggerÚinfor   r   Údelete_fileÚ	ExceptionÚwarningr   ÚstrÚuuidÚuuid4r   Úlenr   r
   Úupload_fileÚappendr   r	   )r   r   r   r   ÚstoredÚall_textr)   ÚcontentÚold_idÚexÚtextr&   ÚmetadataÚchunksÚchunkÚ	embeddingr+   s                    r   Úingest_documentsrD   6   sÖ  € ð ’˜B×%Ñ%Ó'Ò;Ô+;€HØ˜2ˆH€Fà%ó .Ñˆ	�7Ü,¨Y¸	À6ÓJò 	WˆFÜ�K‰KÐ2°9°+¸RÀ¸xÀuÈYÈKÐXÔYÜ(¨°FÔ;ðWÜ×+Ñ+¨I¸&¸ÀÀ9À+Ð7NÕOð		Wô ' w°	Ó:ˆØ�D˜4‘KÑˆäœŸ
™
›Ó%ˆ	Ø'°iØ&,¸(ñDˆáØ")ˆH�YÑä˜DÓ!ˆÜ�‰�hœs 6›{˜m¨<¸	°{À"ÀYÀKÈqÐQÔRØò 	FˆEÜ& u¸	È7ÔSˆIÜ˜y¨%°¸HÕEð	Fô 	×#Ñ# I°)°¸A¸i¸[Ð/IÈ7ÔSØ�‰ I¸IØ#+ñ-ö 	.ð3.ô8 ˜x°9ÀgÔN€GÜ�9˜g wÔ/Ø¨VÑ4Ð4øô1 ò WÜ—‘Ð!CÀIÀ;ÈbÐQSÐPTÐU×VÒVûðWús   Á%E+Å+	FÅ4FÆFr&   c                 óZ  ‡— t        ˆfd„t        | «      D «       d«      }|si S 	 t        j                  | ‰› d|d   › �«       t        | ‰«      }t
        j                  d|d   › d|› d	| › �«       ‰|d   |d
œS # t        $ r(}t
        j                  d|d   › d|› �«       Y d}~Œbd}~ww xY w)a  Remove a document's knowledge base rows and its stored file.

    The file name is looked up here rather than asked of the caller: the generic
    delete leaves the blob behind when the caller omits it, which quietly
    accumulates orphaned files nobody can reach.
    c              3   ó4   •K  — | ]  }|d    ‰k(  sŒ|–— Œ y­w©r&   N© ©Ú.0Údr&   s     €r   ú	<genexpr>z"delete_document.<locals>.<genexpr>l   s   øè ø€ ÒV˜¸!¸K¹.ÈIÓ:U”!ÑVùs   ƒ‘Nr"   r)   zCould not delete the blob for r#   zDeleted document r!   z chunks) from )r&   r)   Údeleted_chunks)	ÚnextÚlist_documentsr   r1   r2   r/   r3   r   r0   )r   r&   Úmatchr>   Údeleteds    `   r   Údelete_documentrR   e   sÐ   ø€ ô ÓVœ^¨IÓ6ÔVÐX\Ó]€EÙØˆ	ðTÜ×#Ñ# I°)°¸A¸eÀKÑ>PÐ=QÐ/RÔSô
 +¨9°iÓ@€GÜ
‡K�KÐ# E¨+Ñ$6Ð#7°r¸'¸À.ÐQZÐP[Ð\Ô]Ø"°°{Ñ1CØ%ñ'ð 'øô ò Tä�‰Ð7¸¸kÑ8JÐ7KÈ2ÈbÈTÐR×SÑSûðTús   ¥A9 Á9	B*ÂB%Â%B*c           	      ó.  — t        «       }	 |j                  «       5 }t        || «      sg cddd«       |j                  «        S t	        j
                  d«      j                  t	        j                  | «      «      }|j                  |«       g }|j                  «       D ]b  \  }}}}	|r|sŒ|xs t        }|r"|j                  «       |j                  «       k7  rŒ;|j                  ||||	r|	j                  «       nddœ«       Œd |cddd«       |j                  «        S # 1 sw Y   nxY wnE# t        $ r9}
t        j!                  d| › d|
› �d¬«       g cY d}
~
|j                  «        S d}
~
ww xY w	 |j                  «        y# |j                  «        w xY w)z1Every uploaded file for the tenant, newest first.Nak  
                SELECT metadata->>'source_id',
                       metadata->>'source',
                       metadata->>'category',
                       MIN(created_at)
                FROM {}.strategist_knowledge_base
                WHERE metadata->>'ingestion_type' = 'file'
                GROUP BY 1, 2, 3
                ORDER BY 4 DESC
            )r&   r)   r   Úuploaded_atzCould not list documents for r#   T©Úexc_info)r   Úcursorr   Úcloser   ÚSQLÚformatÚ
Identifierr   Úfetchallr.   Úlowerr9   Ú	isoformatr2   r/   Úerror)r   r   Úconnr   Úqueryr,   r&   ÚnameÚcatÚcreatedr>   s              r   rO   rO   |   s  € äÓ€Dð!Ø�[‰[‹]ð 	˜cÜ   iÔ0Ø÷	ð 	ð@ 	�
‰
�ô; —G‘Gð 	ó 	÷ ‘œŸ™ yÓ1Ó2ð ð �K‰K˜ÔØˆIØ14·±³ò Ñ-�	˜4  gÙ ©ØØÒ-Ô-�Ù §	¡	£¨x¯~©~Ó/?Ò ?ØØ× Ñ Ø!*Ø!%Ø #Ù:A 7×#4Ñ#4Ô#6Àtñ	"õ ðð ÷7	ð 	ð@ 	�
‰
�÷A	ð 	úð 	øô8 ò Ü�‰Ð4°Y°K¸rÀ"ÀÐFÐQUˆÔVØŒ	à�
‰
�ûð	úð9	ð@ 	�
‰
�øˆ�
‰
�úsX   ŒD+ œDª	D+ ÁC DÄ	D+ ÄD'Ä#D+ Ä*F Ä+	E-Ä4E(ÅE-ÅF Å(E-Å-F ÆFc                 óò  — |xs dj                  «       xs t        }t        «       }	 |j                  «       5 }t	        || «      s	 ddd«       |j                  «        yt        j                  d«      j                  t        j                  | «      «      }|j                  |||f«       |j                  }ddd«       |j                  «        t        j                  d|› d› d|› d| › d	�	«       |d
kD  |j                  «        S # 1 sw Y   ŒPxY w# t        $ rG}|j!                  «        t        j#                  d|› d|› �d¬«       Y d}~|j                  «        yd}~ww xY w# |j                  «        w xY w)zGRe-categorise a document. Every chunk of the file carries the metadata.r   NFzÑ
                UPDATE {}.strategist_knowledge_base
                SET metadata = jsonb_set(metadata::jsonb, '{{category}}', to_jsonb(%s::text))
                WHERE metadata->>'source_id' = %s
            zSet category 'z' on z chunks of r!   r(   r   zCould not set category for r#   TrU   )r-   r.   r   rW   r   rX   r   rY   rZ   r[   r   ÚrowcountÚcommitr/   r0   r2   Úrollbackr_   )r   r&   r   r`   r   ra   Úchangedr>   s           r   Úset_categoryrj   £   sG  € à’˜B×%Ñ%Ó'Ò;Ô+;€HÜÓ€DðØ�[‰[‹]ð 		#˜cÜ   iÔ0Ø÷		#ð$ 	�
‰
�ô —G‘Gð ó ÷ ‘œŸ™ yÓ1Ó2ð	 ð
 �K‰K˜ ¨)Ð4Ô5Ø—l‘lˆG÷		#ð 	�‰ŒÜ�‰�n X J¨e°G°9¸KÈ	À{ÐRTÐU^ÐT_Ð_`ÐaÔbØ˜‰{ð 	�
‰
�÷%		#ð 		#ûô ò Ø�‰ŒÜ�‰Ð2°9°+¸RÀ¸tÐDÈtˆÔTÛà�
‰
�ûðûð
 	�
‰
�úsM   ¨D ¸DÁD ÁADÂ6>D ÄDÄ
D Ä	E!Ä-EÅE$ ÅE!Å!E$ Å$E6c                 óH   ‡— ‰si S t        ˆfd„t        | «      D «       i «      S )zBOne document by its id, or {} if this tenant has no such document.c              3   ó2   •K  — | ]  }|d    ‰k(  r|–— Œ y­wrG   rH   rI   s     €r   rL   z find_document.<locals>.<genexpr>Á   s$   øè ø€ ò 1�qØ�k‘N iÒ/ô ñ 1ùs   ƒ)rN   rO   )r   r&   s    `r   Úfind_documentrm   ½   s,   ø€ áØˆ	Üó 1œN¨9Ó5ô 1Ø24ó6ð 6r   Údocumentc                 óV   — |syt        j                  | |d   › d|d   › �t        ¬«      S )zÌA short-lived direct link to the stored file.

    Minted at the moment someone asks for it, never handed out in advance, so
    the window in which it is useful to anyone who intercepts it is small.
    r   r&   r"   r)   )Úhours)r   Údownload_urlÚ
LINK_HOURS)r   rn   s     r   Úblob_urlrs   Å   s=   € ñ ØÜ×'Ñ'Ø�h˜{Ñ+Ð,¨A¨h°{Ñ.CÐ-DÐEÌZôYð Yr   r)   c                 ó  ‡— |xs dj                  «       j                  «       Š‰si S t        | «      }t        ˆfd„|D «       d«      }|sn|D �cg c]  }‰|d   j                  «       v sŒ|‘Œ }}t	        |«      dk(  r|d   }n5t	        |«      dkD  r't
        j                  |›dt	        |«      › d| › d	�«       |si S |d   |d
   |d   t        j                  j                  d«      › dt        | «      › dt        |d   «      › �dœS c c}w )a¡  Resolve a file name to a permanent link the visitor can be given.

    The chat model knows file names because search results carry them as
    `source`, so that is what it passes here.

    The returned URL points at this service, not at blob storage. It does not
    expire, so a link sitting in a chat transcript still works next month; the
    real storage link is generated when the visitor actually clicks.
    r   c              3   óP   •K  — | ]  }|d    j                  «       ‰k(  sŒ|–— Œ y­w)r)   N)r]   )rJ   rK   Úwanteds     €r   rL   z download_link.<locals>.<genexpr>à   s%   øè ø€ ÒK˜¨!¨K©.×*>Ñ*>Ó*@ÀFÓ*J”!ÑKùs   ƒ&Ÿ&Nr)   é   r   z	 matches z documents for z; refusing to guess.r   r&   ú/z/documents/download?tenant_id=z&source_id=)r)   r   r&   Úurl)r-   r]   rO   rN   r7   r/   r0   r   ÚPUBLIC_BASE_URLÚrstripr   )r   r)   r,   rP   rK   Ú
candidatesrv   s         @r   Údownload_linkr}   Ñ   s/  ø€ ð Šo˜2×$Ñ$Ó&×,Ñ,Ó.€FÙØˆ	ä˜yÓ)€IÜÓK˜YÔKÈTÓR€Eáð "+ÖO˜A¨f¸¸+¹×8LÑ8LÓ8NÒ.N’aÐOˆ
ÐOô ˆz‹?˜aÒØ˜q‘M‰EÜ�‹_˜qÒ Ü�K‰K˜9˜- y´°Z³Ð0AÀØ$˜+Ð%9ð;ô <ñ Øˆ	à˜{Ñ+Ø˜jÑ)Ø˜{Ñ+Ü×/Ñ/×6Ñ6°sÓ;Ð<ð ="Ü"'¨	Ó"2Ð!3°;¼uÀUÈ;ÑEWÓ?XÐ>Yð[ñ]ð ]ùò Ps   ÁDÁ)D)NN)N)(Ú__doc__Úloggingr5   Úurllib.parser   Úpsycopg2r   Úapp.core.configr   Úapp.services.databaser   r   r   r	   r
   Úapp.services.embeddingr   r   r   Úapp.services.extractionr   Úapp.services.storager   Ú	getLoggerÚ__name__r/   rr   r.   r4   Úboolr   ÚlistÚdictrD   rR   rO   rj   rm   rs   r}   rH   r   r   ú<module>rŒ      sG  ðñó Û Ý å å $÷õ ÷ PÑ OÝ ;Ý 0à	ˆ×	Ñ	˜8Ó	$€à€
ð #Ð ð #ð ¨$ó ð EIØ$(ñ,5 ð ,5¨dð ,5¸cð ,5Ø!ð,5Ø-1ó,5ð^'˜sð '¨sð '°tó 'ñ.$˜cð $¨Sð $¸Dó $ðN˜Cð ¨Cð ¸3ð À4ó ð46˜Sð 6¨Sð 6°Tó 6ð	Y˜ð 	Y tð 	Y°ó 	Yð']˜Sð ']¨Sð ']°Tô ']r   