
    iA              
         U d dl mZ d dlZd dlZd dlZd dlZd dlmZmZ d dl	m	Z	 d dl
mZ ddlmZmZ  ej                  e      Z ej$                  d      Z ej$                  d	      Z ej$                  d
      ZdZdddddZded<   d"dZd#dZd$dZd%dZe G d d             Z eej>                  ej@                  ejB                  ejD                  ejF                  ejH                  ejJ                  ejL                  g      Z' ej$                  dejP                        Z)dZ*d&dZ+ ej$                  dejP                        Z,d'dZ-d(dZ.dZ/	 	 	 	 	 	 	 	 	 	 d)d Z0	 	 	 d*	 	 	 	 	 	 	 	 	 d+d!Z1y),    )annotationsN)	dataclassfield)datetime)Optional   )ChatMessageMessageTypez010-\d{3,4}-\d{4}u   [가-힣]{2,6}u   유형\s*:\s*(.+)      보상   고지의무   약관   상품)r   r   r   r   zdict[str, str]_CATEGORY_KEYWORDSc                    t         j                  |       }|sy|j                  d      j                         }t        j                         D ]  \  }}||v s|c S  y)u?   #궁금증 메시지에서 유형 카테고리를 추출한다.   기타r   )_RE_CATEGORYsearchgroupstripr   items)contentm	type_textkeywordcategorys        J/home/jay/projects/insuwiki/scripts/kakao_knowledge/knowledge_extractor.py_extract_categoryr   ,   s]    G$A
  "I/557 iO     c                .    t         j                  d|       S )u'   전화번호 패턴을 마스킹한다.z***-****-****)	_RE_PHONEsub)texts    r   _mask_phoner$   8   s    ==$//r   c                    t         j                  |       }t               }g }|D ]9  }||vr"|j                  |       |j	                  |       t        |      dk\  s8 |S  |S )uJ   간단한 규칙 기반 한국어 명사 추출 (2~6글자 한글 단어).
   )_RE_KO_NOUNfindallsetaddappendlen)r#   wordsseenresultws        r   _extract_keywordsr1   =   sf    %EUDF D=HHQKMM!v;"M Mr   c                b    | r|sy	 t        j                  |  d| d      S # t        $ r Y yw xY w)u9   날짜 + 시간 문자열을 datetime으로 변환한다.N z%Y-%m-%d %H:%M)r   strptime
ValueError)datetime_strs     r   _parse_datetimer8   L   s@    x  D68*!57GHH s   " 	..c                  F    e Zd ZU  ee      Zded<   dZded<   dZded	<   y
)Thread)default_factorylist[ChatMessage]messages str
start_timeFboolhas_question_tagN)	__name__
__module____qualname__r   listr=   __annotations__r@   rB    r   r   r:   r:   [   s'    "'"=H=J"d"r   r:   u   ^(안녕하세요[~!.]*|감사합니다[~!.]*|고맙습니다[~!.]*|넵[~!.]*|네[~!.]*|👏+|🎉+|👍+|💪+|ㅋ{2,}|ㅎ{2,}|ㄴㄴ|ㅇㅇ|ㅜㅜ|ㅠㅠ|이사왔습니당?[~!.^]*|반갑습니다[~!.]*|잘 부탁드립니다[~!.]*)$)u   📚 한 발 앞서가는u   ━━━━━u   여기서 배우고c                    | j                   j                         }t        D ]  }||v s y t        j	                  |      ryy)uD   단순 인사/리액션/환영 등 노이즈 메시지인지 판별.TF)r   r   _WELCOME_PATTERNS_RE_NOISE_GREETINGmatch)msgr   pats      r   _is_noise_messagerO      sC    kk!G  '> (r   up   (질문\s*드립니다|질문드립니다|문의\s*드립니다|궁금합니다|여쭤봅니다|확인\s*부탁)c                    | D cg c]  }|j                   t        vs| }}|D cg c]  }t        |      r| }}|sg S g }t               }|D ]  }|j                  sQ|j                  j                  |       |j                   d|j                   |_        d|j                  v rd|_
        a|j                  d   }d}d|j                  v rd}nt        j                  |j                        rd}n|j                  |j                  k7  rd}net        |j                  |j                        }t        |j                  |j                        }	|r#|	r!|	|z
  j                         dz  }
|
t        k\  rd}|r|j                  |       t               }|j                  j                  |       |j                   d|j                   |_        d|j                  v s!t        j                  |j                        sd|_
        |j                  j                  |       d|j                  v sd|_
         |j                  r|j                  |       |D cg c]  }t!        |j                        dk\  s| c}S c c}w c c}w c c}w )uM  파싱된 메시지 리스트를 대화 스레드로 분리한다.

    분리 기준 (우선순위):
    1. #궁금증 태그 → 무조건 새 스레드
    2. 질문 패턴 (질문 드립니다, 문의 드립니다 등) → 새 스레드
    3. 날짜 변경 → 새 스레드
    4. 15분 이상 시간 gap → 새 스레드
    r3   
   #궁금증TF<      )type_EXCLUDED_TYPESrO   r:   r=   r+   r6   timer@   r   rB   _RE_QUESTION_PATTERNr   r8   total_seconds_THREAD_GAP_MINUTESr,   )r=   r   filteredthreadscurrent_threadrM   prev_msgshould_splitprev_dtcurr_dtgap_minutests               r   _split_into_threadsrd      s:    $Eaqvv_'DEHE#@a+<Q+?@H@	G#XN +7&&##**3/+.88*AchhZ(@N%s{{*26/!**2. 3;;&L "((5L XX&L &hmmX]]CG%chh9G7&0??ABF"55#'LNN>*#XN##**3/+.88*AchhZ(@N%s{{*.B.I.I#++.V26/##**3/s{{*26/W+7Z ~& 7!#ajj/Q"6A77w F@r 8s    JJJJJ:Jc                   | j                   }d}t        |      D ]  \  }}d|j                  v s|} n ||   }||dz   d }t        |j                        }	dj	                  d |D              }
d}|rEi |D ].  }j                  |j                  d      dz   |j                  <   0 t        fd	      }|j                  dd
 j                  dd      }|j                  dz   |
z   }t        |      }|D cg c]'  }d|j                   dt        |j                         ) }}| j                  r| j                  j                  d      d   nd}d|d||	dt        |j                        |
||||d|dS c c}w )uL   스레드에서 규칙 기반으로 wiki_entry 딕셔너리를 생성한다.r   rQ   r   N
c              3  F   K   | ]  }t        |j                          y wN)r$   r   .0r   s     r   	<genexpr>z/_build_wiki_entry_rule_based.<locals>.<genexpr>   s     HqK		2Hs   !r>   c                    |    S rh   rH   u
user_counts    r   <lambda>z._build_wiki_entry_rule_based.<locals>.<lambda>   s    z!} r   key2   r3   [] kakao-03dmediumidtitler   subcategoryquestionanswerexpertsource_datesource_chatkeywords
confidence
raw_thread)r=   	enumerater   r   joingetusermaxreplacer1   r$   r@   split)threadindexr   msgsquestion_idxir   question_msganswer_msgsr   answer_textr   r{   	full_textr   r   r   ro   s                    @r   _build_wiki_entry_rule_basedr      s   ??D L$ 1199$L
 %L|a')*K !!5!56H ))HKHHK F%'
 	?A!+!:Q!>Jqvv	?Z%<=   "%--dC8E $$s*[8I +H 9=34!AFF82k!)),-.J  6<5F5F&##))#.q1BK uSk" 4 45""  s   8,E8u  당신은 보험 전문 지식 정리 전문가입니다.
아래 보험설계사 오픈채팅방 대화 스레드를 분석하여 **하나의 Q&A 위키 항목**으로 정제하세요.

## 원칙
1. **원자적**: 하나의 질문-답변만 포함. 2개 이상 주제가 섞여있으면 가장 핵심 Q&A만 추출
2. **자기완결적**: 이 항목만 읽어도 완전히 이해 가능
3. **전문가 답변 중심**: 일반인 의견보다 전문가(손해사정사, 경력 설계사)의 답변을 우선
4. **노이즈 제거**: 인사, 이모티콘, 광고, 공지 내용 제외

## 출력 형식 (JSON)
{{
  "title": "핵심 주제를 한 문장으로 (예: 안저 광응고술의 수술 해당 여부)",
  "category": "보상|고지의무|약관|상품|기타",
  "question": "질문자의 핵심 궁금증 1~2문장 요약",
  "answer": "전문가 답변의 핵심 결론 2~4문장 요약. 근거나 판례도 포함.",
  "keywords": ["관련", "키워드", "5~8개"],
  "confidence": "high|medium|low",
  "is_noise": false
}}

만약 대화가 의미 있는 Q&A가 아니라면 (단순 인사, 공지, 광고 등):
{{
  "is_noise": true,
  "reason": "노이즈 사유"
}}

## 대화 스레드
{thread_text}c                   ddl }dj                  d | j                  D              }t        j	                  |      }	 |j                  j                  ddd|d	g
      }|j                  d   j                  j                         }|j                  d      r.t        j                  dd|      }t        j                  dd|      }|j                  |      }	|	j                  dd      r(t        j                  d||	j                  dd             yt!        |	j                  dd            }
t!        |	j                  dd            }t!        |	j                  dd            }t#        |	j                  dg             }t%        |
      dk  s|
j                  d      r#t        j'                  d|       t)        | ||      S ||k(  r#t        j'                  d|       t)        | ||      S | j                  }| j*                  r| j*                  j-                  d      d   nd}d}t%        |      dkD  rL|dd }i |D ].  }j                  |j.                  d      dz   |j.                  <   0 rt1        fd      }|D cg c]'  }d|j.                   d t3        |j                         ) }}d!|d"|
t!        |	j                  d#d$            t!        |	j                  d%d            |||||t%        |      d&k\  r|nt5        |dz   |z         t!        |	j                  d'd(            |d)S c c}w # t6        $ r.}t        j'                  d*||       t)        | ||      cY d}~S d}~ww xY w)+ul   Anthropic Haiku로 스레드를 분석한다. is_noise이면 None 반환. 실패 시 규칙 기반 fallback.r   Nrf   c              3  f   K   | ])  }d |j                    dt        |j                          + yw)rt   ru   N)r   r$   r   ri   s     r   rk   z+_analyze_thread_with_llm.<locals>.<genexpr>E  s0      34!AFF82k!)),-.s   /1)thread_textzclaude-haiku-4-5-20251001i   r   )roler   )model
max_tokensr=   z```z^```(?:json)?\n?r>   z\n?```$is_noiseFu&   스레드 %d: 노이즈로 판정 (%s)reasonr{   r}   r~   r      rQ   uA   스레드 %d: 제목 품질 불량 — 규칙 기반으로 전환uG   스레드 %d: 답변이 질문과 동일 — 규칙 기반으로 전환r3   r   c                    |    S rh   rH   rm   s    r   rp   z*_analyze_thread_with_llm.<locals>.<lambda>y  s    z!} r   rq   rt   ru   rv   rw   r   r   r|      r   rx   ry   uM   Anthropic API 분석 실패 (스레드 %d): %s — 규칙 기반으로 전환)jsonr   r=   _LLM_PROMPT_TEMPLATEformatcreater   r#   r   
startswithrer"   loadsr   loggerinfor?   rF   r,   warningr   r@   r   r   r   r$   r1   	Exception)r   clientr   r   _jsonr   promptresponseraw_textparsedr{   r}   r~   r   r   r   r   r   r   r   excro   s                        @r   _analyze_thread_with_llmr   <  sH    )) 8> K "(([(AFFH??))-%&9: * 

 !((+00668 u%vv12x@Hvvj"h7HX& ::j%(KK@%T\^`Iab FJJw+,vzz*b12VZZ"-.

:r23 u:>U--l;NN^`ef/{KK XNNdfkl/{KK9?9J9Jf''--c215PR t9q=qr(K)+J  C%/^^AFFA%>%B
166"CZ-DE =A
78axr+aii012

 

 5+&FJJz8<=vzz-<= &&$'MQ$6<MhY\n_eNe<ffjjx@A$
 	
	
&  Hfhmors+FE;GGHsE   CL0 B-L0  'L0 (BL0  ,L+,A>L0 +L0 0	M'9#M"M'"M'c                   t        |       }t        j                  dt        |             |sr|D cg c]  }|j                  s| }}t        j                  dt        |             g }t        |d      D ]#  \  }}	t        |	||      }
|j                  |
       % |S |xs t        j                  j                  d      }|s#t        j                  d       t        | d|      S 	 d	d
l}|j                  |      }g }d}t!        d	t        |      |      D ]q  }||||z    }|D ];  }	||j#                  |	      z   dz   }t%        |	|||      }
|
+|j                  |
       = ||z   t        |      k  s]t'        j(                  d       s |S c c}w # t        $ r.}t        j                  d|       t        | d|      cY d
}~S d
}~ww xY w)u  
    파싱된 메시지 리스트에서 보험 실무 지식을 추출한다.

    Parameters
    ----------
    messages:
        parse_kakao_chat() 로부터 얻은 ChatMessage 리스트
    use_llm:
        True이면 Anthropic Haiku로 고급 분석, False이면 규칙 기반만 사용
    api_key:
        Anthropic API 키. 없으면 환경변수 ANTHROPIC_API_KEY 참조
    source_chat:
        채팅방 이름 (wiki_entry의 source_chat 필드)

    Returns
    -------
    wiki_entry 딕셔너리 리스트
    u   총 %d개 스레드 분리됨u"   #궁금증 태그 스레드: %d개r   )startANTHROPIC_API_KEYu]   Anthropic API 키가 없습니다 (ANTHROPIC_API_KEY). 규칙 기반으로 fallback합니다.F)use_llmr   r   N)api_keyuO   Anthropic 클라이언트 초기화 실패: %s — 규칙 기반으로 fallbackr   )rd   r   r   r,   rB   r   r   r+   osenvironr   r   extract_knowledge	anthropic	Anthropicr   ranger   r   rW   sleep)r=   r   r   r   r\   rc   tagged_threadsresultsr   r   entryresolved_keyr   r   r   llm_results
batch_sizebatch_startbatchidxs                       r   r   r     s   0 "(+G
KK/W> %,C0B0B!CC8#n:MN ">; 	"IAv0KHENN5!	"  Abjjnn-@AL5	
 !5kRRS$$\$:
 !KJQGj9 	kJ&>? 	*FF 33a7C,VVS+NE ""5)		* #c'l2JJqM	 O D*  Shjmn 5kRRSs)   FF(F 	G
#G?G
G
)r   r?   returnr?   )r#   r?   r   r?   )r#   r?   r   z	list[str])r6   r?   r7   r?   r   zOptional[datetime])rM   r	   r   rA   )r=   r<   r   zlist[Thread])r   r:   r   intr   r?   r   dict)
r   r:   r   objectr   r   r   r?   r   zOptional[dict])FNr>   )
r=   r<   r   rA   r   z
str | Noner   r?   r   z
list[dict])2
__future__r   loggingr   r   rW   dataclassesr   r   r   typingr   modelsr	   r
   	getLoggerrC   r   compiler!   r'   r   rZ   r   rG   r   r$   r1   r8   r:   	frozensetJOINLEAVEKICKPHOTOEMOTICONVIDEOBOTSYSTEMrV   
IGNORECASErK   rJ   rO   rX   rd   r   r   r   r   rH   r   r   <module>r      s   "  	 	  (   ,			8	$ BJJ+,	 bjj*+ rzz./   "	& N 	0
 # # # 	  RZZW MM	  \ 
 "rzzwMM E8Z7| <THTHTH TH 	TH
 TH| 	DDD D 	D
 Dr   