ó
    À‘´jÆ  ã                  ó4  • S r SSKJr  SSKrSSKJrJrJrJrJ	r	J
r
  SSKJr  SSKJrJr  SSKJrJr  SSKJrJr   " S	 S
\5      rS\R.                  " \R1                  5       SS9 S3r\
 " S S\	5      5       r " S S5      r " S S5      r\S   rSSS jjrg)u7  
TableClassifier Protocol â€” decides whether a Camelot candidate is an
actual table.

Each candidate (its extracted markdown) gets a binary is_table
decision plus a short reason. In the Camelot LLM pipeline
(`quber.core.extractors.camelot.llm`), rejected candidates are dropped
before unification and correction, so the correction stage only sees
inputs that are actually tables â€” keeps stream-flavor noise (footnote
lists, narrative paragraphs, TOC entries, layout artifacts) out of the
correction step. In the dual flow (`quber.core.extractors.dual.camelot`)
the verdict is stored as the candidate's classification and nothing is
dropped.

Kept separate from `LLMClient` so the classifier can use a different
(cheaper/faster) model than the structure-correction agent, and so
retry semantics don't bleed across roles.
é    )ÚannotationsN)ÚAnyÚDictÚLiteralÚOptionalÚProtocolÚruntime_checkable)Úlogger)Ú	BaseModelÚField)ÚLangSmithTracerÚusage_metadata_from)ÚDEFAULT_LLM_MODELÚget_settingsc                  óD   • \ rS rSr% \" SS9rS\S'   \" SSS9rS	\S
'   Srg)ÚClassifierResulté    a.  True if the candidate is an actual data table (a structured grid with a header row identifying columns and at least one row of discrete data values). False for footnote lists, narrative paragraphs, Table-of-Contents entries, single-row layout artifacts, or any block that lacks a header/data structure.)ÚdescriptionÚboolÚis_tableÚ zHOne short sentence justifying the decision; what made it a table or not.)Údefaultr   ÚstrÚreason© N)	Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__r   r   Ú__annotations__r   Ú__static_attributes__r   ó    Ú5/home/mande/repo/quber/src/quber/agents/classifier.pyr   r       s1   ‡ Ùð%ñ€Hˆdó ñ ØØ_ñ€FˆCö r"   r   aÿ  You are classifying a candidate text block extracted from a PDF page.

Decide whether the candidate is an *actual data table*:

- *Yes* if it has a header row identifying columns AND at least one row
  of discrete data values arranged in a rectangular grid.
- *No* otherwise. NOT a table: footnote lists, Table-of-Contents
  entries, narrative paragraphs, bullet lists formatted as cells,
  single-row layout artifacts (decorative banners, section dividers),
  callout boxes, or any block where the cells contain flowing prose
  rather than discrete values.

Bias toward strict: when the block is borderline, return False. Missing
a borderline real table is cheaper than polluting downstream with a
fake one.

Return a JSON object conforming exactly to this schema:

é   )ÚindentuE   

Return only the JSON object â€” no prose, no markdown code fences.
c                  ó   • \ rS rSrSS jrSrg)ÚTableClassifieréM   c              ƒ  ó   #   • g 7f©Nr   ©ÚselfÚmarkdowns     r#   ÚclassifyÚTableClassifier.classifyO   s   é € Àùs   ‚r   N©r-   r   Úreturnr   )r   r   r   r   r.   r!   r   r"   r#   r'   r'   M   s   † çDr"   r'   c                  óN   • \ rS rSrSr   S       S	S jjrS
S jrSS jrSrg)ÚPydanticAIClassifieréR   u*  TableClassifier backed by pydantic-ai with an Anthropic model.

Auth resolution mirrors `PydanticAIClient`: `ANTHROPIC_AUTH_TOKEN`
(OAuth subscription) preferred over `ANTHROPIC_API_KEY`. Model
defaults to Haiku 4.5 â€” classification is a simple judgement and
doesn't benefit from a larger model.
Nc                ó¤  • SSK Jn  SSKJn  SSKJn  [        5       R                  nU=(       d    UR                  =(       d    [        nU=(       d    UR                  nU=(       d    UR                  n	U(       a  SSKJn
  U
" X5      nOU	(       a  U" U	S9nU" XS9nO[        S5      eXl        U" U[        [         S	9U l        [%        S
S9U l        g )Nr   )ÚAgent)ÚAnthropicModel)ÚAnthropicProvider)Úmake_oauth_anthropic_model)Úapi_key)ÚproviderztPydanticAIClassifier: neither ANTHROPIC_AUTH_TOKEN nor ANTHROPIC_API_KEY is set. Provide one via env or constructor.)Úoutput_typeÚsystem_promptzquber-classifier)Úrun_name)Úpydantic_air6   Úpydantic_ai.models.anthropicr7   Úpydantic_ai.providers.anthropicr8   r   ÚllmÚmodelr   Úanthropic_auth_tokenÚanthropic_api_keyÚquber.agents._oauth_gater9   ÚRuntimeErrorr   ÚCLASSIFY_TABLE_PROMPTÚagentr   Útracer)r,   rC   Ú
auth_tokenr:   r6   r7   r8   Úllm_settingsÚresolved_authÚresolved_keyr9   Ú
anth_modelr;   s                r#   Ú__init__ÚPydanticAIClassifier.__init__[   sµ   € õ 	&Ý?ÝEä#“~×)Ñ)ˆØ×@˜×+Ñ+×@Ô/@ˆØ"×G l×&GÑ&GˆØ×@ ,×"@Ñ"@ˆæÝKá3°EÓI‰JÞÙ(°Ñ>ˆHÙ'¨ÑA‰JäðPóð ð
 Œ
ÙØÜ(Ü/ñ
ˆŒ
ô &Ð/AÑBˆ�r"   c                ó*   • SS[         S.SSUS./S./0$ )NÚmessagesÚsystem©ÚroleÚcontentÚuserÚtext)ÚtyperY   )rH   r+   s     r#   Útrace_inputsÚ!PydanticAIClassifier.trace_inputs�   s-   € àØ!Ô.CÑDØ°fÀhÑ-OÐ,PÑQðð
ð 	
r"   c              ƒ  óF  #   • U R                  U5      n U R                  R                  SX R                  S9 IS h  v•N nU R                  R                  U5      I S h  v•N nUR                  nSUR                  5       S./UR                  UR                  [        UR                  5      S.Ul        S S S 5      IS h  v•N   U$  NŠ Ni N
! , IS h  v•N  (       d  f       W$ = f! [         a+  n[        R                  " SU5        [!        SSS	9s S nA$ S nAff = f7f)
NÚclassify_table)rC   Ú	assistantrU   )rS   r   r   Úusage_metadataz:classify: LLM call failed; default-accepting candidate: {}Tz$default-accept on classifier failure©r   r   )r[   rJ   Úllm_runrC   rI   ÚrunÚoutputÚmodel_dump_jsonr   r   r   ÚusageÚoutputsÚ	Exceptionr
   Úwarningr   )r,   r-   Úinputsrc   Úresultrd   Úexcs          r#   r.   ÚPydanticAIClassifier.classify‰   sí   é € Ø×"Ñ" 8Ó,ˆð	bØ—{‘{×*Ñ*Ð+;¸VÏ:É:Ð*×VÑVÐZ]Ø#Ÿz™zŸ~™~¨hÓ7×7�ØŸ™�à*5À&×BXÑBXÓBZÑ![Ð \Ø &§¡Ø$Ÿm™mÜ&9¸&¿,¹,Ó&Gñ	�”÷ W×Vð ˆMñ WÙ7÷ W×V×Vð ˆMûÜó 	bÜ�NŠNÐWÐY\Ô]Ü#¨TÐ:`ÑaÕaûð	büs™   ‚D!•(C) ½C¾C) ÁCÁ C
Á!ACÂ6C) ÃCÃC) ÃD!ÃC) Ã
CÃC) ÃC&ÃCÃC&Ã!C) Ã%D!Ã&C) Ã)
DÃ3 DÄDÄD!ÄDÄD!)rI   rC   rJ   )NNN)rC   úOptional[str]rK   rn   r:   rn   r1   ÚNone)r-   r   r1   zDict[str, Any]r0   )	r   r   r   r   Ú__doc__rP   r[   r.   r!   r   r"   r#   r3   r3   R   sN   † ñð  $Ø$(Ø!%ð	$Càð$Cð "ð$Cð ð	$Cð
 
õ$CôL
÷br"   r3   c                  ó0   • \ rS rSrSrSSS jjrS	S jrSrg)
ÚMockClassifieré›   z-Returns a canned ClassifierResult. For tests.Nc                ó4   • U=(       d
    [        SSS9U l        g )NTÚmockra   )r   rk   )r,   rk   s     r#   rP   ÚMockClassifier.__init__ž   s   € Ø×NÔ 0¸$ÀvÑ Nˆ�r"   c              ƒ  ó&   #   • UnU R                   $ 7fr*   ©rk   )r,   r-   Ú_s      r#   r.   ÚMockClassifier.classify¡   s   é € ØˆØ�{‰{Ðùs   ‚rx   r*   )rk   zOptional[ClassifierResult]r1   ro   r0   )r   r   r   r   rp   rP   r.   r!   r   r"   r#   rr   rr   ›   s   † Ù7öO÷r"   rr   )Úapiru   c                ó°   • U =(       d    [        5       R                  R                  nUS:X  a
  [        5       $ US:X  a
  [	        5       $ [        SU< S35      e)Nr{   ru   z"Unknown QUBER_CLASSIFIER_BACKEND: z. Expected api|mock.)r   rB   Úclassifier_backendr3   rr   Ú
ValueError)ÚbackendÚselecteds     r#   Úget_classifierr�   ©   sR   € Ø×?œ,›.×,Ñ,×?Ñ?€HØ�5ÓÜ#Ó%Ð%Ø�6ÓÜÓÐÜ
Ð9¸(¹ÐEYÐZÓ
[Ð[r"   r*   )r   zOptional[ClassifierBackend]r1   r'   )rp   Ú
__future__r   ÚjsonÚtypingr   r   r   r   r   r	   Úlogurur
   Úpydanticr   r   Úquber.agents.langsmith_tracerr   r   Úquber.settingsr   r   r   ÚdumpsÚmodel_json_schemarH   r'   r3   rr   ÚClassifierBackendr�   r   r"   r#   Ú<module>rŒ      s©   ðñõ& #ã ß L× Lå ß %ç Nß :ô�yô ð(ð& ‡‚Ð×.Ñ.Ó0¸Ñ;Ð <ð =ð'Ð ð2 ôE�hó Eó ðE÷Fbñ Fb÷Rñ ð ˜MÑ*Ð ÷\r"   