Ë
    ,Þêjr-  ã                  óØ  — d Z ddlmZ ddl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  ej                  e«      ZdZ ed	¬
«       G d„ d«      «       Z G d„ d«      Z ej(                  d«      Z ej(                  d«      Z ej(                  d«      Z ej(                  d«      Z ej(                  dej2                  «      Z ej(                  dej2                  «      Zdd„Zdd„Zy)u›  LLM-driven document classifier for auto-categorization.

Provides structured JSON classification of documents via Ollama /api/chat.
All prompts enforce strict JSON output with temperature=0 for determinism.
ClassificationResult has all fields mandatory â€” never empty/None.

Research references:
- ByteRover (arXiv:2604.01599) â€” hierarchical Context Tree, knowledge in markdown
- Memex (jagdeepvirdi/memex) â€” Ollama auto-classify, confidence scores, category review
- secondmem â€” AI classification into topic markdown files with JSON schema
- Grimore-MD â€” two-tier change detection, RRF fusion
- RAGraph â€” entity/topic resolution, graph-based organization
é    )ÚannotationsN)Ú	dataclass)ÚAny)ÚHTTPStatusError)ÚEmbeddingConfiguM  You are classifying a technical document into a knowledge hierarchy.
Return ONLY a valid JSON object matching this exact schema â€” no prose, no markdown, no extra keys.

{
  "topic": "ONE of: ai, architecture, security, research, notes, reports, general",
  "subtopic": "short free-text domain within the topic (e.g. 'audit', 'design', 'networking')",
  "tags": ["TAG1", "TAG2", ...],  // 2-5 tags max, lowercase, relevant to content
  "summary": "2-3 sentence summary of the document's core content",
  "confidence": "FLOAT string 0.0-1.0 (e.g. "0.87") â€” your confidence in this classification",
  "entities": ["ENTITY1", "ENTITY2", ...],  // 0-4 named entities extracted from text
  "intent": "ONE of: actionable, reference, idea"
}

Temperature must be 0 for deterministic output. Do NOT invent tags or entities not supported by the text.
T)Úfrozenc                  óX   — e Zd ZU dZded<   ded<   ded<   ded<   ded	<   ded
<   ded<   y)ÚClassificationResultuÛ   Result of LLM-driven document classification.

    All fields are mandatory â€” never None/empty. If the LLM output is
    malformed, the caller must handle the exception rather than producing
    a partial result.
    ÚstrÚtopicÚsubtopicz	list[str]ÚtagsÚsummaryÚfloatÚ
confidenceÚentitiesÚintentN)Ú__name__Ú
__module__Ú__qualname__Ú__doc__Ú__annotations__© ó    úM/home/oem/CyberBrain26-unified/src/cyberbrain26/knowledge_plane/classifier.pyr
   r
   6   s.   … ñð ƒJØƒMØ
ƒOØƒLØÓØÓØ„Kr   r
   c                  óh   — e Zd ZdZddd„Zed„ «       Zdddœ	 	 	 	 	 	 	 dd„Zdd„Zdd	„Z	dd
„Z
dd„Zy)ÚLLMClassifieraÁ  Classify documents using a local Ollama model.

    Uses Ollama /api/chat endpoint with strict JSON prompt and
    temperature=0 for deterministic output. All fields in
    ClassificationResult are mandatory.

    Research-backed design:
    - ByteRover: hierarchical memory via agent-native classification
    - Memex: confidence-scored auto-classification, categorical review queue
    - secondmem: AI classification into topic markdown files
    Nc                ó8   — |xs
 t        «       | _        d | _        y ©N)r   ÚconfigÚ_client)Úselfr    s     r   Ú__init__zLLMClassifier.__init__Z   s   € ØÒ1¤Ó 1ˆŒØˆ�r   c                óœ   — | j                   €5t        d«      j                  | j                  j                  d¬«      | _         | j                   S )z4Lazily create httpx client for Ollama chat endpoint.Úhttpxg      N@)Úbase_urlÚtimeout)r!   Ú
__import__ÚClientr    Úollama_base_url©r"   s    r   ÚclientzLLMClassifier.client^   sF   € ð �<‰<ÐÜ% gÓ.×5Ñ5ØŸ™×4Ñ4Øð 6ó ˆDŒLð �|‰|Ðr   ç        ©ÚtemperatureÚnum_ctxc               ó  — | j                   j                  dt        › d|› �dœg||xs ddœddœ}	 | j                  j	                  d|¬	«      }|j                  «        |j                  «       }|j                  d
i «      j                  dd«      }t        j                  |«      }t        |t        «      st        dt        |«      › �«      ‚|S # t        t        j                  t        f$ r}	t        j!                  d|	«       ‚ d}	~	ww xY w)z6Call Ollama /api/chat and return parsed JSON response.Úuserz

Document text:
)ÚroleÚcontenti    r.   Újson)ÚmodelÚmessagesÚoptionsÚformatz	/api/chat)r5   Úmessager4   z{}zExpected dict from LLM, got zLLM classification failed: %sN)r    Ú
chat_modelÚCLASSIFY_PROMPTr,   ÚpostÚraise_for_statusr5   ÚgetÚloadsÚ
isinstanceÚdictÚ
ValueErrorÚtyper   ÚJSONDecodeErrorÚloggerÚerror)
r"   Útextr/   r0   ÚpayloadÚresponseÚdatar4   ÚparsedÚexcs
             r   Ú_call_ollamazLLMClassifier._call_ollamah   sý   € ð
 —[‘[×+Ñ+ð #Ü"1Ð!2Ð2FÀtÀfÐMñðð (3¸wºÈ$ÑOØñ
#
ˆð	Ø—{‘{×'Ñ'¨¸'Ð'ÓBˆHØ×%Ñ%Ô'Ø—=‘=“?ˆDà—h‘h˜y¨"Ó-×1Ñ1°)¸TÓBˆGÜ—Z‘Z Ó(ˆFÜ˜f¤dÔ+Ü Ð#?ÄÀVÃ¸~Ð!NÓOÐOØˆMøÜ¤×!5Ñ!5´zÐBò 	Ü�L‰LÐ8¸#Ô>Øûð	ús   ±BC ÃDÃ,DÄDc           	     ód  — | j                  |«      }h d£}|t        |j                  «       «      z
  }|rt        d|› �«      ‚t	        |d   t
        «      rt        d„ |d   D «       «      st        dt        |d   «      › �«      ‚t	        |d   t
        «      rt        d„ |d   D «       «      st        dt        |d   «      › �«      ‚|d	   t        d
«      D �cg c]  }t        |dz  «      ‘Œ c}ddgz   vr+	 t        |d	   «      }d|cxk  rdk  sn t        d|› �«      ‚	 ddddœ}|d   j                  «       }||vr,t        dt        |j                  «       «      › d|d   › d�«      ‚||   }	h d£}
|d   j                  «       }||
vrt        j                  d|«       d}n|}|d   r|d   nd}|d   dd  }t        |«      d!k  r|d"d#gdd!t        |«      z
   z   }|d   dd$ }t        |d	   «      }t!        ||||d%   |||	¬&«      S c c}w # t        t        f$ r t        d|d	   › d�«      d‚w xY w)'uV  Run LLM classification on document text.

        Returns ClassificationResult with all fields mandatory.
        If the LLM fails (network error, malformed output), raises
        RuntimeError â€” caller must decide whether to retry, stage for
        review, or assign to fallback category.

        Design rationale (per Memex and ByteRover research):
        - temperature=0 ensures deterministic output across runs
        - format=json forces Ollama to return structured data
        - all fields mandatory â€” no empty/None values in result
        - 2-5 tags max prevents tag spray
        >   r   r   r   r   r   r   r   zLLM output missing keys: r   c              3  ó<   K  — | ]  }t        |t        «      –— Œ y ­wr   ©rA   r   )Ú.0Úts     r   ú	<genexpr>z2LLMClassifier.classify_document.<locals>.<genexpr>�   s   è ø€ Ò7`Èq¼
À1Äc×8JÑ7`ùó   ‚ztags must be list of str, got r   c              3  ó<   K  — | ]  }t        |t        «      –— Œ y ­wr   rQ   )rR   Úes     r   rT   z2LLMClassifier.classify_document.<locals>.<genexpr>Ÿ   s   è ø€ Ò;hÐST¼JÀqÌ#×<NÑ;hùrU   z"entities must be list of str, got r   ée   éd   z0.0z1.0r-   g      ð?z confidence must be 0.0-1.0, got z0confidence must be parseable float string, got 'ú'NÚ
actionableÚ	referenceÚidea)r[   r\   r]   r   zintent must be one of z, got '>   ÚaiÚnotesÚgeneralÚreportsÚresearchÚsecurityÚarchitecturer   z.Topic '%s' not in allowed set, using 'general'r`   r   é   é   Ú	knowledger^   é   r   )r   r   r   r   r   r   r   )rN   ÚsetÚkeysrC   rA   ÚlistÚallrD   Úranger   r   Ú	TypeErrorÚlowerrF   ÚwarningÚlenr
   )r"   rH   ÚrawÚrequired_keysÚmissingÚiÚconfidence_valÚ
intent_mapÚ
intent_rawr   Úallowed_topicsÚtopic_lowerr   r   r   r   s                   r   Úclassify_documentzLLMClassifier.classify_document†   s   € ð ×Ñ Ó%ˆò eˆØ¤# c§h¡h£j£/Ñ1ˆÙÜÐ8¸¸	ÐBÓCÐCô ˜#˜f™+¤tÔ,´CÑ7`ÐTWÐX^ÑT_Ô7`Ô4`ÜÐ=¼dÀ3ÀvÁ;Ó>OÐ=PÐQÓRÐRÜ˜#˜j™/¬4Ô0¼Ñ;hÐX[Ð\fÑXgÔ;hÔ8hÜÐAÄ$ÀsÈ:ÁÓBWÐAXÐYÓZÐZØˆ|Ñ¼¸s»Ö$D°A¤S¨¨3©¥ZÒ$DÈÈuÀ~Ñ$UÑUðÜ!& s¨<Ñ'8Ó!9�Ø˜nÔ3°Ô3Ü$Ð'GÈÐGWÐ%XÓYÐYð 4ð %1¸{ÐTZÑ[ˆ
Ø˜‘]×(Ñ(Ó*ˆ
Ø˜ZÑ'ÜÐ5´d¸:¿?¹?Ó;LÓ6MÐ5NÈgÐVYÐZbÑVcÐUdÐdeÐfÓgÐgØ˜JÑ'ˆò gˆØ˜'‘l×(Ñ(Ó*ˆØ˜nÑ,Ü�N‰NÐKÈ[ÔYØ‰EàˆEð '*¨*¢o�3�z’?¸9ˆð �6‰{˜2˜AˆˆÜˆt‹9�qŠ=à˜;¨Ð-¨o°´C¸³I±Ð>Ñ>ˆDð �z‘? 2 AÐ&ˆä˜s <Ñ0Ó1ˆä#ØØØØ˜	‘NØ%ØØô
ð 	
ùòS %Eøô ¤	Ð*ò Ü ØFÀsÈ<ÑGXÐFYÐYZÐ[óàððús   ÃHÃ6)H È#H/c                ó`   — | j                   �"| j                   j                  «        d| _         yy)zClose the httpx client.N)r!   Úcloser+   s    r   r}   zLLMClassifier.closeÔ   s(   € à�<‰<Ð#Ø�L‰L×ÑÔ ØˆD�Lð $r   c                ó   — | S r   r   r+   s    r   Ú	__enter__zLLMClassifier.__enter__Ú   s   € Øˆr   c                ó$   — | j                  «        y r   )r}   )r"   Ú_excs     r   Ú__exit__zLLMClassifier.__exit__Ý   s   € Ø�
‰
�r   r   )r    zEmbeddingConfig | NoneÚreturnÚNone)rH   r   r/   r   r0   z
int | Nonerƒ   zdict[str, Any])rH   r   rƒ   r
   )rƒ   r„   )rƒ   r   )r�   Úobjectrƒ   r„   )r   r   r   r   r#   Úpropertyr,   rN   r{   r}   r   r‚   r   r   r   r   r   M   sb   „ ñ
ôð ñó ðð 25ÈDñØðØ).ðØ?Iðà	óó<L
ó\ óôr   r   z.[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}z,(\+\d{1,3}[- ]?)?\d{3,4}[- ]?\d{3}[- ]?\d{4}z\(?:4[0-9]{12}|5[1-5][0-9]{14}|3[47][0-9]{13}|3[0-9]{15}|\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4})z\d{3}-?\d{2}-?\d{4}z0\b(?:patient|diagnosis|prescription|HIPAA|PHI)\bzE(?:api[_-]?key|token|secret)\s*[:=]\s*['\"]?[A-Za-z0-9\-_]{16,}['\"]?c                ó¶  — | j                  dd«      j                  dd«      }t        j                  |«      s*t        j                  |«      st        j                  |«      ryt
        j                  |«      du}t        j                  |«      duxs t        j                  |«      du}|r|ryt        j                  |«      €t        j                  |«      �yy	)
aì  Detect document sensitivity from plain text.

    Returns one of: "public", "personal", "sensitive", "restricted"
    based on heuristic PII pattern matching. Deterministic, no LLM call.
    Used per MemStrata research for automated fact classification.

    Priority order (first match wins):
    1. RESTRICTED: credit card, SSN, API credentials
    2. SENSITIVE: medical/health terms + personal identifiers
    3. PERSONAL: email addresses, phone numbers
    4. PUBLIC: nothing matched
    ú
ú úÚ Ú
restrictedNÚ	sensitiveÚpersonalÚpublic)ÚreplaceÚ_CREDIT_CARD_REÚsearchÚ_SSN_REÚ_CREDENTIAL_REÚ_MEDICAL_REÚ	_EMAIL_REÚ	_PHONE_RE)rH   Ú
normalizedÚhas_medicalÚhas_personals       r   Údetect_sensitivityr›   î   sÁ   € ð —‘˜d CÓ(×0Ñ0°°rÓ:€Jô ×Ñ˜jÔ)¬W¯^©^¸JÔ-GÌ>×K`ÑK`ÐakÔKlØô ×$Ñ$ ZÓ0¸Ð<€KÜ×#Ñ# JÓ/°tÐ;Òg¼y×?OÑ?OÐPZÓ?[ÐcgÐ?g€LÙ‘|Øô ×Ñ˜
Ó#Ð/´9×3CÑ3CÀJÓ3OÐ3[Øàr   c                ó^   — 	 | j                  dd¬«      }t        |«      S # t        $ r Y yw xY w)z=Detect sensitivity from raw bytes by decoding as UTF-8 first.zutf-8r�   )Úerrorsr�   )ÚdecodeÚ	Exceptionr›   )r4   rH   s     r   Údetect_sensitivity_from_bytesr      s:   € ðØ�~‰~˜g¨iˆ~Ó8ˆô ˜dÓ#Ð#øô ò Ùðús   ‚   	,«,)rH   r   rƒ   r   )r4   Úbytesrƒ   r   )r   Ú
__future__r   r5   ÚloggingÚreÚdataclassesr   Útypingr   r%   r   Ú-cyberbrain26.knowledge_plane.embedding_configr   Ú	getLoggerr   rF   r<   r
   r   Úcompiler–   r—   r‘   r“   Ú
IGNORECASEr•   r”   r›   r    r   r   r   ú<module>r«      sê   ðñõ #ã Û Û 	Ý !Ý å !å Ià	ˆ×	Ñ	˜8Ó	$€ð€ñ* �$Ô÷ð ó ð÷,Qñ Qðr ˆB�J‰JÐHÓI€	ØˆB�J‰JÐFÓG€	Ø�"—*‘*Ð|Ó}€Ø
ˆ"�*‰*Ð+Ó
,€Øˆb�j‰jÐLÈbÏmÉmÓ\€Ø�—‘ÐdÐfh×fsÑfsÓt€óôJ$r   