
    SiH              
          d Z ddlZddlZddlZddlZddlZddlZddlmZ dZ	dZ
dZ eh d      Zg dZd	Zd
edefdZdedee   fdZdededeeee   ee   f   fdZdedee   fdZdededeeee   f   fdZdededeeef   fdZdededefdZdee   fdZdee   dedefdZdedefdZ	 d&d
edededefd Zdej@                  fd!Z!d"eddfd#Z"d'd$Z#e$d%k(  r e#        yy)(u~   
eval runner 스크립트 — TDD GREEN 단계 구현
각 스킬의 evals.json을 로드하고 LLM 응답을 평가합니다.
    N)Pathz/home/jay/.claude/skillsu   (을|를|이|가|은|는|에서|에게|에게서|으로부터|으로|로부터|로|과|와|의|도|만|까지|부터|한테|서|마저|조차|밖에|이라도|라도|이나|나|이든|든)$u   (야|다|요|습니다|겠습니다|합니다|있습니다|없습니다|해야|해서|하고|하며|하여|하는|되는|된다|된|이며|통해야|포함되어야|준수하며)$>      꼭   고루   모두   미만   이내   이상   이하   초과   최대   최소   항상	   적어도	   최소한   독립적으로	   반드시)u	   무조건r   z100%u   절대u	   확실히g333333?
skill_namereturnc                 ^   t        t              }|| z  dz  dz  |dz  | z  dz  dz  g}d}|D ]  }|j                         s|} n |(t        d|  d|D cg c]  }t	        |       c}       t        |d      5 }t        j                  |      }ddd       |S c c}w # 1 sw Y   S xY w)	u  
    {SKILLS_BASE_PATH}/{skill_name}/evals/evals.json 파일을 로드합니다.

    탐색 순서:
    1. {SKILLS_BASE_PATH}/{skill_name}/evals/evals.json
    2. {SKILLS_BASE_PATH}/skills/{skill_name}/evals/evals.json (fallback)

    Returns:
        {"skill_name": ..., "evals": [...]}

    Raises:
        ValueError: 파일이 존재하지 않거나 JSON 파싱 실패 시
        json.JSONDecodeError: JSON 파싱 실패 시
    evals
evals.jsonskillsNz evals.json not found for skill 'z
'. Tried: utf-8encoding)r   SKILLS_BASE_PATHexists
ValueErrorstropenjsonload)r   basecandidate_paths
evals_pathpathpfdatas           L/home/jay/workspace/.worktrees/task-2117-dev1/tools/eval-runner/run_evals.py
load_evalsr+   A   s      !DzG#l2x*$w.=O
 #J ;;=J
 .zl.ZiIjUV#a&IjHkl
 	
 
j7	+ qyy|K JkKs   B=B""B,textc                 F   t        j                  d|       }g }t               }|D ]w  }t        j                  t        d|      }t        j
                  t        |      r9|t        v rBt        |      dk\  sQ||vsV|j                  |       |j                  |       y t        j                  d|       }|D ch c]  }|j                          }}|D ]E  }|j                         |vs|j                  |j                                |j                  |       G |S c c}w )u   텍스트에서 핵심 키워드를 추출합니다.

    - 한글: 조사/어미 제거 후 2자 이상 명사 위주
    - 영문: 3자 이상 단어
    - 부사/수량어 제외
    u   [가-힣]{2,}    z[A-Za-z]{3,})refindallsetsub_JOSA_PATTERNsearch_VERB_ENDING_PATTERN_EXCLUDED_WORDSlenaddappendlower)	r,   
raw_koreancleanedseenwordcleaned_wordenglish_wordsw
seen_lowers	            r*   _extract_keywordsrD   k   s     ,d3JGUD )vvmR699)<8?*|!l$&>HH\"NN<() JJ5M/67!AGGI7J7 !::<z)NN4::<(NN4 !
 N 8s   9Dresponseexpectedc                    t        |      }|sdg g fS | j                         }g }g }|D ]7  }|j                         |v r|j                  |       '|j                  |       9 t        |      t        |      z  }|t        k\  }|||fS )ud  
    expected에서 핵심 키워드를 추출하고, response에서 키워드 존재 여부를 확인합니다.

    Args:
        response: LLM 응답 문자열
        expected: 기대 출력 설명 (키워드 추출 대상)

    Returns:
        (pass여부, 매칭된_키워드, 미매칭_키워드)
        키워드 30% 이상 매칭이면 pass
    T)rD   r;   r:   r8   _KEYWORD_PASS_THRESHOLD)	rE   rF   keywordsresponse_lowermatchedmissedkw	pass_ratepasseds	            r*   keyword_matchrP      s     !*HR|^^%NGF 88:'NN2MM"	 Gs8},I11F7F""    boundaryc                 .    t        j                  d|       S )uj   boundary에서 '→ 스킬명' 또는 '-> 스킬명' 패턴의 라우팅 대상 목록을 추출합니다.u   (?:→|->)\s*([\w-]+))r0   r1   )rR   s    r*   _extract_routing_targetsrT      s     ::.99rQ   c                    g }t        |      }|D ]4  }|j                         | j                         v s$|j                  |       6 |j                         }t        D ]  }|| v s||v s|j                  |        t	        |      dk(  }||fS )u  
    boundary에서 금지어/금지 패턴을 추출하고 response에서 감지합니다.

    규칙:
    1. boundary에 '→ 스킬명' 패턴이 있으면, 응답에 해당 스킬명이 언급되면 금지
    2. boundary에 명시된 단정적 표현이 응답에 있으면 금지

    Args:
        response: LLM 응답 문자열
        boundary: 경계 조건 문자열

    Returns:
        (pass여부, 발견된_금지어_목록)
    r   )rT   r;   r:   _ABSOLUTE_EXPRESSIONSr8   )rE   rR   found_forbiddenrouting_targetstargetboundary_lowerexprrO   s           r*   check_forbiddenr\      s     "$O /x8O! +<<>X^^--""6*+
 ^^%N% )8 6""4() !Q&F?""rQ   c                 `    t        |      }|sy j                         }g d}|D ]<  }|j                         |v st         fd|D              }|r	d| dfc S d| dfc S  t        j                  d       }g d	}t         fd
|D              }	|r|	rdj                  |      }
d|
 dfS y)u   
    boundary에 '→ 스킬명' 패턴이 있는 경우 응답의 라우팅 적절성을 확인합니다.

    Args:
        response: LLM 응답 문자열
        boundary: 경계 조건 문자열

    Returns:
        (pass여부, 판정사유)
    )Tu   라우팅 규칙 없음)u   이용해주세요u   이용하세요u   담당합니다u   해당 스킬u	   라우팅u   전달u   문의u   스킬을 이용utilizerefercontactc              3   &   K   | ]  }|v  
 y wN ).0r'   rE   s     r*   	<genexpr>z check_routing.<locals>.<genexpr>   s      L1h L   Tu!    스킬로 올바르게 위임함Fu2    역할을 직접 수행함 (위임 표현 없음)u(   \d+[원만억%]|\d+\s*(CPC|CPM|ROAS|CTR))u   예산u   전략u	   타겟팅u   최적u   설정하세요u   배분u	   타겟은c              3   &   K   | ]  }|v  
 y wrb   rc   )rd   rM   rE   s     r*   re   z check_routing.<locals>.<genexpr>	  s     B"rX~Brf   z, u%    영역의 업무를 직접 수행함)Tu   범위 내 응답)rT   r;   anyr0   r5   join)rE   rR   rX   rJ   delegation_patternsrY   has_delegationbudget_patternstrategy_keywordshas_strategy
target_strs   `          r*   check_routingrp      s     /x8O.^^%N " \<<>^+  L8K LLNx'HIII(Z[[[\ YYJHUNmB0ABBL,YY/
$IJJJ$rQ   	eval_casec                     |j                  dd      }|j                  dd      }|j                  dg       }t        | |      \  }}}dj                  |      }t        | |      \  }	}
|xr |	}|||d|	|
dd	}|||d
S )u
  
    keyword_match + check_forbidden 통합 판정

    Args:
        response: LLM 응답 문자열
        eval_case: eval 케이스 dict (id, prompt, expected_output, assertions, files)

    Returns:
        {"passed": bool, "eval_id": int, "details": {...}}
    idr   expected_outputr.   
assertions )rO   rK   rL   )rO   rW   )rP   forbidden_check)rO   eval_iddetails)getrP   ri   r\   )rE   rq   rx   rt   ru   	kw_passedrK   rL   rR   forbidden_passedrW   overall_passedry   s                r*   evaluate_responser~     s     mmD!$Gmm$5r:O|R0J "/x!IIw xx
#H(7((K%o3#3N  
 '.

G ! rQ   c                     t        t              } | j                         sg S g }t        | j	                               D ]G  }|j                         s|dz  dz  }|j                         s-|j                  |j                         I |S )ud   
    SKILLS_BASE_PATH 하위에서 evals/evals.json이 있는 스킬 목록을 반환합니다.
    r   r   )r   r   r   sortediterdiris_dirr:   name)r#   r   	skill_dir
evals_files       r*   get_skill_listr   G  sz      !D;;=	FDLLN+ .	"W,|;J  "inn-	.
 MrQ   resultsoutput_pathc                 H   i }g }| D ]c  }|j                  dd      }||vrddd||<   ||   dxx   dz  cc<   |j                  d      r||   dxx   dz  cc<   S|j                  |       e i }|j                         D ]"  \  }}|d   }|d   }	|dkD  r|	|z  nd||	d	||<   $ t        |       }
t	        d
 | D              }|
dkD  r||
z  nd}|||
||d}t        j                  |dd      }t        |dd      5 }|j                  |       ddd       |S # 1 sw Y   |S xY w)u   
    결과를 JSON 문자열로 반환하고 output_path에 파일로 저장합니다.

    Args:
        results: 평가 결과 목록
        output_path: 출력 파일 경로

    Returns:
        JSON 문자열
    r   unknownr   )totalrO   r      rO           )rN   r   rO   c              3   D   K   | ]  }|j                  d       sd  ywrO   r   Nrz   rd   rs     r*   re   z"generate_report.<locals>.<genexpr>  s     =QQUU8_q=     )skill_resultstotal_pass_ratetotal_evalstotal_passedfailed_casesFr/   ensure_asciiindentrB   r   r   N)	rz   r:   itemsr8   sumr!   dumpsr    write)r   r   skill_statsr   resultr   r   statsr   rO   r   r   r   report_datareport_jsonr(   s                   r*   generate_reportr   ]  sr    $&K!L (ZZi8
[(01Q&?K
#J(A-(::h
#H-2-'( &(M(..0 

Egx+019%#%
j!
 g,K='==L4?!Ol[0O '*"$$K **[uQGK	k3	1 Q	  s   ;DD!promptc                     t        j                  d       t        j                  dd| ddgddd      }|j                  d	k7  rt        d
|j                  dd        |j                  j                         S )u   
    Claude CLI를 통해 LLM을 호출합니다. (OAuth 인증 사용)

    Args:
        prompt: 사용자 프롬프트

    Returns:
        LLM 응답 문자열

    Raises:
        RuntimeError: claude CLI 호출 실패 시
    r   claudez-pz--modelzclaude-haiku-4-5-20251001Tx   )capture_outputr,   timeoutr   u   claude CLI 호출 실패: Ni  )	timesleep
subprocessrun
returncodeRuntimeErrorstderrstdoutstrip)r   r   s     r*   call_llmr     sw     	JJqM^^	4,GH	F A7ds8K7LMNN==  rQ   dry_runverbosec           
         t        |       }|j                  dg       }|r,|rt        d|  dt        |       d       dt        |      | dS g }|D ]  }|j                  dd      }|r"t        d	|  d
|j                  dd       d       t	        |      }t        ||      }	| |	d<   |j                  |	       |si|	d   rdnd}
t        d|
         t        |      }t        d |D              }|dkD  r||z  nd}| ||||dS )uZ  
    특정 스킬의 eval을 실행합니다.

    Args:
        skill_name: 스킬 이름
        dry_run: True이면 API 호출 없이 구조 검증만 수행
        verbose: 상세 출력 여부

    Returns:
        dry_run=True이면 {"dry_run": True, "eval_count": N, "skill_name": ...}
        dry_run=False이면 평가 결과 dict
    r   z
[dry-run] z: u   개 eval 케이스 발견T)r   
eval_countr   r   r.   [z] eval #rs   ?u    실행 중...r   rO   PASSFAILu     → c              3   D   K   | ]  }|j                  d       sd  ywr   r   r   s     r*   re   z&run_evals_for_skill.<locals>.<genexpr>  s     7qquuX7r   r   r   )r   r   r   rO   rN   )r+   rz   printr8   r   r~   r:   r   )r   r   r   
evals_datar   r   rq   r   rE   r   statusr   rO   rN   s                 r*   run_evals_for_skillr     s6    J'JNN7B'EJzl"SZL8QRSe*$
 	
 G %	x,Aj\)--c*B)C>RSF#"8Y7)|v%h/VVFF6(#$% LE7G77F"'!)I ! rQ   c                      t        j                  d      } | j                  ddd       | j                  ddd	d
       | j                  ddd	d       | j                  ddd	dd       | S )u%   CLI 인수 파서를 생성합니다.uF   eval runner — 스킬 eval을 실행하고 결과를 평가합니다.)descriptionz--skill
SKILL_NAMEu   단일 스킬 실행)metavarhelpz--all
store_trueFu   전체 스킬 실행)actiondefaultr   z	--verboseu   상세 출력z	--dry-runr   u)   API 호출 없이 구조 검증만 수행)r   r   destr   )argparseArgumentParseradd_argument)parsers    r*   create_argument_parserr     s    $$1yzF
#  
 #	   	   8   MrQ   r   c                 B    t               }| |vrt        d|  d|       y)u   
    스킬명의 유효성을 검사합니다.

    Args:
        name: 스킬 이름

    Raises:
        ValueError: 유효하지 않은 스킬명이면
    'uK   '은(는) 유효하지 않은 스킬명입니다. 사용 가능한 스킬: N)r   r   )r   
skill_lists     r*   validate_skill_namer     s4      !J:1TF"qr|q}~ rQ   c                  B   t               } | j                         }|j                  s|j                  s| j	                          yg }|j                  r?t               }|st        d       y|j                  rJt        dt        |       d|        n/|j                  r#	 t        |j                         |j                  g}g }|D ]  }t        dd        t        d|        t        d       t        ||j                  |j                  	      }t        t        j                  |d
d             |j                  rz|j                  dg       D ]  }|j!                  |         |j                  s||ryddl}|j%                  dddd
d      5 }	|	j&                  }
ddd       t)        |
      }t        dd        t        d       t        d       t        |       t        d|
        yyy# t        $ r}t        d|        Y d}~yd}~ww xY w# 1 sw Y   yxY w)u   CLI 진입점.Nu(   실행 가능한 스킬이 없습니다.u   전체 스킬 u   개 실행: u   오류: 
z<============================================================u   스킬: )r   r   Fr/   r   r   r   rB   z.jsoneval_report_r   )modesuffixprefixdeleter   u   전체 결과 보고서u   
보고서 저장: )r   
parse_argsskillall
print_helpr   r   r   r8   r   r   r   r   r!   r   rz   r:   tempfileNamedTemporaryFiler   r   )r   argsskills_to_runeall_resultsr   r   r   r   r(   report_pathreports               r*   mainr   "  s   #%FD::dhh!Mxx&(<=<<N3}#5"6l=/RS		

+  K# 
&
8*o%&h$Zt||\djjeA>?||ZZ	2. &""1%&
& <<K((! ) 
 	! &&K	! !k:8*o'(hf$[M23# (<'  	HQC.!	,	! 	!s$   G1 H1	H:HHH__main__)FF)r   N)%__doc__r   r!   osr0   r   r   pathlibr   r   r4   r6   	frozensetr7   rV   rH   r   dictr+   listrD   tupleboolrP   rT   r\   rp   r~   r   r   r   r   r   r   r   r   __name__rc   rQ   r*   <module>r      s  
   	 	   - g _  * R   "3 "4 "T!C !DI !H#C #3 #5tCy$s)9S3T #H:s :tCy :#c #S #U4c?5K #B5%C 5%3 5%5s3C 5%z( ( ( (`S	 ,3T$Z 3c 3c 3v!S !S !D =B44"4594	4x 7 7 <@c @d @94x zF rQ   