
    j	)                        d Z ddl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	m
Z
mZmZ  ee      j                         j                  j                  j                  Zej"                  j%                  d eedz               ej"                  j%                  d ee              ej(                  ej*                  d        ej,                  e      Zd	efd
Z	 dde
ee	f   dedee
ee
ee	f   f      d	efdZ	 ddee
ee	f      dee
ee
ee	f   f      d	e
ee	f   fdZdee   d	efdZde
ee	f   d	dfdZde
ee	f   de
ee	f   d	dfdZ ddZ!edk(  r e!        yy)u*  CLI — 스텝별 캐시 적중 실측 (읽기 전용).

축 A1(정적/동적 순서 재배열)의 효과 판정 SOT. 재배열 **전**과 **후**를
같은 제공사·같은 시간 간격·같은 호출 순서에서 한 번씩 재고, cached_tokens
의 변화만 재배열 효과로 인정한다. 문헌 수치는 기대값으로 쓰지 않는다.

    # 기준선 (재배열 전)
    python tools/opik_prompt_audit/cache_baseline.py         --since 2026-08-15T00:00:00 --label before
    # 재배열 후
    python tools/opik_prompt_audit/cache_baseline.py         --since 2026-08-16T00:00:00 --label after
    # 대조
    python tools/opik_prompt_audit/cache_baseline.py --compare before.json after.json

읽기만 한다 — Opik 에 쓰지 않고 저장소도 건드리지 않는다(출력 JSON 제외).
    N)datetime)Path)AnyDictListOptionalbackendz%(message)s)levelformatreturnc                  $    ddl m}  t        |       S )uH   스텝 이름 사전 — trace 태그에서 스텝을 고를 때 쓴다.r   STEP_MANIFEST)app.core.step_manifestr   setr   s    T/Users/manta/Documents/Projects/TheRoad-I1/tools/opik_prompt_audit/cache_baseline.py_known_stepsr   "   s    4}    traceknowntrace_indexc                    ddl m}m} |	 || |      n ||       }||v r|S | j                  d      xs g }|D cg c]	  }||v s| }}t	        |      dk(  r|d   S t	        |      dkD  rt        |      d   S |S c c}w )uL  행(span/trace)의 스텝 이름.

    ★`trace_index` 를 주면 span 으로 보고 부모에서 얻는다(2026-08-24) —
    litellm span 이름은 절대 비지 않아 그것만 보면 부모를 영영 안 본다.

    1순위는 기존 감사 도구와 같은 `step_of` (metadata.trace_name) 다 —
    두 리포트의 스텝 이름이 갈리면 대조가 안 된다. trace_name 은 주행
    태그(run_tag) 가 있을 때만 실리므로, 없으면 태그 중 스텝 사전에 있는
    이름을 쓴다(`build_opik_metadata` 가 태그 첫 칸에 step_id 를 싣는다).
    r   )step_ofstep_of_calltags   )#tools.opik_prompt_audit.audit.fetchr   r   getlensorted)	r   r   r   r   r   namer   thitss	            r   	step_namer$   (   s     J & ,,3EN 	u}99V"D*t!qEzAtD*
4yA~Aw
4y1}d|AK +s   	A<A<tracesc                    ddl m}m} t               }i }| D ]  }t	        |||      }|j                  |ddddddi d      }|dxx   dz  cc<   |d   j                   ||      d      dz   |d    ||      <    ||      }	|	d   |d	xx   dz  cc<   ||dxx   |	d   z  cc<   |	d
   |dxx   dz  cc<   |d
xx   |	d
   z  cc<   |	d   |dxx   |	d   z  cc<    |j                         D ]y  }|d   |d	   z
  }
|
|d<   |
rt        |d   |
z        nd|d<   |d   r|d   rt        |d
   |d   z  d      nd|d<   t        t        |d   j                         d             |d<   { t        t        |j                         d             S )uC  스텝별 캐시 집계.

    한 **span** = 한 발송으로 센다(2026-08-24 — 감사 도구와 같은 단위).
    v1 호출은 trace 1 + span 1 로 남아 있어 둘을 합치면 두 배가 된다.
    usage 가 없는 행(이미지 생성 등)은 `calls_no_usage` 로 따로 세고
    토큰 합에서 뺀다.
    r   )model_ofread_tokens)callscalls_no_usagecalls_cache_reportedprompt_tokenscached_tokenscache_write_tokensmodelsr)   r   r/   r,   Nr*   r-   r+   r.   calls_with_usageprompt_tokens_avg   cache_hit_ratec                     | d    S )Nr    kvs    r   <lambda>zcollect.<locals>.<lambda>t   s
    "Q%r   )keyc                     | d   d    S )Nr   r,   r5   r6   s    r   r8   zcollect.<locals>.<lambda>u   s    RU?5K4Kr   ))tools.opik_prompt_audit.audit.cache_usager'   r(   r   r$   
setdefaultr   valuesrounddictr    items)r%   r   r'   r(   r   rowsr   steprowtokbilleds              r   collectrF   C   s    PNE&(D{3ood$%"#%
  	G),X):):8E?A)NQR)RHhuo&% ' !Q&!OO 44+&'1,' C$88 #$0$%-A)BB%/ 2 {{}W$4 55"(4:E#o&/0 	 
 )*s?/C #o&_)==qAIM 	 3x=&&(.?@BH  tzz|)KLMMr   ratec                     | dS | dz  ddS )Nu	   미보고d   z5.1f%r5   )rG   s    r   	_fmt_raterK   x   s    ,;Atcz$.?q,AAr   payloadc                 *   | d   }t        d|d    d|d    d|j                  d|j                  d             d	|d
    d	       ddddddddddddddddddddd}t        |       t        dt        |      z         dx}}| d   j                         D ]r  \  }}t        |d d dd|d   dd|d   d d|d!   d"d|d#   d d|d$   d"dt	        |d%         dd&t        t        |d'         d(              ||d!   z  }||d$   z  }t t        dt        |      z         t        d) | d   j                         D              }|r|r||z  nd }t        d*ddd+d,d|d d|d"dd+d-d|d"dt	        |      d       |st        d.       y y )/Nwindowu   
창: sincez ~ untilu	     호출 
call_counttrace_countu   건  (수집 collected_at)   스텝28 u   호출z>5u   캐시보고z>7u   입력토큰z>10u   평균u   캐시토큰u	   적중률u     모델-r   steps   r0   5dr+   7dr,   10dr1   r-   r3   z  r/   ?c              3   &   K   | ]	  }|d      yw)r+   Nr5   ).0rs     r   	<genexpr>zprint_table.<locals>.<genexpr>   s     P6O1+,6Os   u   합계 57u  
★ 이 창의 어떤 호출도 캐시 토큰을 싣지 않았다 — 적중률 0% 가 아니라 '기록 없음'이다.
  재배열 전/후 대조를 하려면 먼저 usage 에 캐시 항목이 실리는지 확인해야 한다(제공사 응답 또는 기록 경로).)	printr   r   r@   rK   nextitersumr=   )	rL   wheadtotal_prompttotal_cachedrB   ra   reportedrG   s	            r   print_tablero   |   s3   A	GAgJ<s1W:,ilAEER_L`8a7b c(), -m1XbM>"*=Q~c>RRSm1^C0+b1AKD	$K	#D	/"##L<7#))+acr2a"4 5b9)*2.a/0B3/Gq&'+1Q-?,DA1-./32d1X;'-.	0 	1
 	/**/** , 
#D	/Pgg6F6M6M6OPPH-5,L<'TD	XbM2a&(2aS/A2a&#a	$35 6 M 	N r   beforeafterc                     t        d| d    d| d   d    d|d    d|d   d    d	       dd	d
ddd
ddd
ddd
dd	}t        |       t        dt        |      z         t        t        | d         t        |d         z        D ]y  }| d   j	                  |      }|d   j	                  |      }t        |d d d	d
|rt        |d         nddd
|rt        |d         nddd
|r|d   nddd
|r|d   ndd	       { t        d       y )Nz	
before: labelz (rN   rO   z)   after: rT   rU   rV   rW   u   적중률 beforez>13u   적중률 afterz>12u   평균 입력 beforez>15u   평균 입력 afterz>14rX   rY   rZ   r3   u   없음r1   r   15d14dun   
★ 두 창의 제공사·모델·호출 순서가 같을 때만 이 차이를 재배열 효과로 읽는다.)rf   r   r    r   r   rK   )rp   rq   rk   rB   bas         r   print_comparerx      sn   	Jvg'r&*:7*C)D EW~&bx)A(B!E Fm1/4A6G5LA%c*!,A#+FHD	$K	#D	/s6'?+c%..AAB7O%'Nt$cr2a45)A./08SI45)A./08SI,-!'(1c:!,-!'(1c:	< 	= C 
 " #r   c                     t        j                  d      } | j                  dd       | j                  dd       | j                  d	d
d       | j                  dd d       | j                  dd d       | j                  dd        | j                  dd        | j                  dddd       | j                         }|j                  rt        j                  t        |j                  d         j                  d            }t        j                  t        |j                  d         j                  d            }t        ||       y |j                  st        d      dd l}|j                  t        dz         ddlm} |j"                  xs |j$                  xs dj'                  d      }|st        d       |j(                  xs |j*                  }|j,                  xs |j.                  }dd!lm}	m}
m}  |
||||j                  |j8                        } |	||||j                  |j8                        } |||      }|j:                  |j                  |j8                  |t=        |      t=        |      t=        |      t=        |      z
  t=        |      t?        j@                         jC                  d"#      d$tE        ||      d%}|jF                  rt        |jF                        n2t        d&z  t?        j@                         d'd(z  d)|j:                   d*z  }|jH                  jK                  d+d+,       |jM                  t        jN                  |d-d.      d/       tQ        |       tS        d0|        y )1Nu   스텝별 캐시 적중 실측)descriptionz--sinceu,   ISO 시작 시각 (예: 2026-08-15T00:00:00))helpz--until9999)defaultz--labelbaselineu   이 창의 이름)r}   r{   z--outu   출력 JSON 경로z
--opik-urlu#   OPIK_URL_OVERRIDE 대신 쓸 주소z--workspacez	--projectz	--compare   )BEFOREAFTERu1   이미 수집한 JSON 두 개를 대조만 한다)nargsmetavarr{   r   zutf-8r   u+   --since 가 필요하다 (또는 --compare)r	   )settingsrc   /uz   OPIK 주소가 없다 — backend/.env 의 OPIK_URL_OVERRIDE 또는 --opik-url 로 준다 (셀프 호스팅 전용 도구))fetch_spansfetch_trace_indexselect_call_rowsseconds)timespec)rO   rP   projectrQ   	raw_countexcluded_test_rowsrR   rS   )rs   rN   rY   artifactz%Y%m%d_prompt_cache_baselinecache_z.jsonT)parentsexist_okF)ensure_asciiindent)encodingz
JSON: )*argparseArgumentParseradd_argument
parse_argscomparejsonloadsr   	read_textrx   rO   
SystemExitoschdirROOTapp.core.configr   opik_urlopik_url_overriderstrip	workspaceopik_workspacer   opik_project_namer   r   r   r   rP   rs   r   r   now	isoformatrF   outparentmkdir
write_textdumpsro   rf   )apargsrp   rq   r   r   base_urlr   r   r   r   r   _trace_index_rawr%   rL   r   s                    r   mainr      s   		 	 -M	NBOOI$ROSOOIvO.OOIz8KOLOOGT0DOEOOL$>  @OOM4O0OOKO.OOKq2EL  N==?D||Da1;;GDE

4Q0::7CDfe$::FGGHHTI(A!;!;ArII#NHEF 	F 9("9"9Ill8h88G 
 %)Wdjj$**>LxGTZZLDdL1FZZZZf+T"%d)c&k"9v;$LLN44i4H

 .G  !HH$txx.zx||~f55KLL4::,e
$	%  JJTD1NN

7q9G  M	HSE
r   __main__)N)r   N)"__doc__r   r   loggingsysr   pathlibr   typingr   r   r   r   __file__resolver   r   pathinsertstrbasicConfigINFO	getLogger__name__loggerr   r   r$   rF   floatrK   ro   rx   r   r5   r   r   <module>r      s  "    
   , ,H~&&--44 3ti'( ) 3t9    ',,} =			8	$c  BFT#s(^ C #Dd38n)<$=>JM8 @D2NDc3h( 2N!$sDcN':";<2Nc3h2NjBHUO B BNc3h ND N:#$sCx. #c3h #D #&CL zF r   