
    `Dj5                       U d Z 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 ddl	m
Z
mZ ddlmZ ddlmZmZmZmZmZ  ej(                  e      ZdZed    d	Zd$d
Zddddddd	 	 	 	 	 	 	 	 	 	 	 	 	 d%dZd&dZ	 	 	 	 	 	 	 	 d'dZ ed       G d d             Z e
dd      Zded<   da  ejB                         Z"d Z#d(dZ$d)dZ%d*dZ&d Z'i Z(ded<   ddd	 	 	 	 	 	 	 d+dZ)d,dZ*d-d Z+d.d!Z,edd"	 	 	 	 	 	 	 	 	 	 	 d/d#       Z-y)0uN  Opik 기록 계층의 단일 창구 — uid·축 태그·trace scope.

litellm 의 opik 통합이 `metadata["opik"]` 에서 읽는 키는 넷뿐이다:
`project_name` · `current_span_data` · `tags` · `thread_id`.
`trace_name` 은 litellm 소스에 없다 — trace 이름은 항상
`response_obj["object"]`("chat.completion")로 못박혀 있다.

그래서 이름 있는 trace 는 **우리가 만들고**, litellm 호출에는
`current_span_data={"trace_id": <우리 것>}` 을 실어 **span 만** 붙게 한다.

설계: docs/superpowers/specs/2026-08-23-opik-trace-taxonomy-design.md
    )annotationsN)contextmanager)
ContextVarToken)	dataclass)AnyDictIteratorListOptionalstepopkindmodelproviderstatus:c            
     n   t        j                         } d}t        | |      \  }}t        |dz  |      \  }}t        |dz  |      \  }}|dz  }t        j	                  t        j                  d      d      dz  }d|z  }	t        j                  d	      }
|d
d|dd|dd|	dd|
j                          	S )u  Opik trace/span id — **UUIDv7 이어야 한다**.

    2026-08-23 실측: uuid4 를 주면 서버가
    `400 "Trace id must be a version 7 UUID"` 로 거부한다.
    litellm 도 같은 방식(`litellm.integrations.opik.utils.create_uuid7`)을 쓴다.
    그쪽 함수를 빌리지 않는 이유: litellm 내부 경로라 판이 바뀌면 조용히
    사라진다 — 기록이 죽는 자리를 남의 사정에 걸지 않는다.
    l     Ys       i p     bigi?  i      z>08x-z>04x)timetime_nsdivmodint
from_bytesosurandomhex)nssixteen_secst1rest1t2rest2t3_seqt4rands              P/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/llm/opik_trace.pynew_trace_uidr0   $   s     
B!Lr<(IBu{L1IB5B;-EB'MB
..A
.
7C
SB::a=DYa4y"T!Bt9Adhhj\BB    c                    | |||||d}g }t         D ]O  }|j                  |      }	|	t        |	      j                         }
|
s3| d|
 }||vs?|j	                  |       Q |S )u  축을 접두사로 못박은 태그 목록.

    지금은 스텝·모델·제공자·프로젝트·에피소드·상태가 **한 자루**에 섞여
    (실측 81종) 태그를 봐도 그게 무슨 축인지 모른다. 접두사를 붙이면 축이
    갈린다.

    ★프로젝트 이름·에피소드 제목은 여기에 **안 넣는다** — 한글이고
    카디널리티가 커진다. 그것은 metadata 로 간다.

    ★litellm 이 span 태그에 제공자 이름을 **맨 이름으로 덧붙인다**
    (`extract_tags` 의 `tags.append(custom_llm_provider)`). 그것은 못 막는다.
    trace 태그는 우리가 전부 만드므로 깨끗하다.
    r   r   )_AXIS_ORDERgetstrstripappend)r   r   r   r   r   r   valuesoutaxisvtexttags               r/   build_axis_tagsr>   9   s}    , "d(fFFCJJt91v||~avc>JJsO  Jr1   c                J    t        |       t        fdt        D              S )u+  허용 축 접두사가 붙은 태그인가.

    ★`":" in tag` 로 보면 안 된다 — 동적 이름·URL·시각 표기가 축 태그로
    둔갑해 고카디널리티 맨 태그가 그대로 통과한다
    (`http://192.168.0.9:5173/x`, `붉은 벽:2층`). 2026-08-24 Codex 재리뷰.
    c              3  F   K   | ]  }j                  | d         yw)r   N)
startswith).0r:   r<   s     r/   	<genexpr>zis_axis_tag.<locals>.<genexpr>g   s!     C{tt$qz*{s   !)r5   anyr3   )r=   r<   s    @r/   is_axis_tagrE   _   s     s8DC{CCCr1   c                v    dj                  d | xs d|xs dfD              }|xs ddd xs d}|r| d| S |S )u=  주행 묶음 키 — **에피소드 단위**로 고정한다.

    지금은 `build_opik_context` 가 부를 때마다 run_tag 를 새로 만들고
    (`analysis_dispatch_service.py:47`), 단일 스텝 API 가 그것을 스텝마다
    부른다(`api/v1/steps.py:253`). 215샷 주행을 30번 재개하면 thread 가
    31개로 갈린다.

    에피소드에서 바로 만들면 재개·재기동·단일 스텝 호출이 전부 같은 값이
    되고, 저장할 것도 없다. 한 dispatch 를 따로 보고 싶으면 metadata 의
    `run_tag` 로 가른다.
    r+   c              3  &   K   | ]	  }|s|  y wN )rB   xs     r/   rC   z$episode_thread_id.<locals>.<genexpr>x   s     NH!AAHs    N   unknown)join)project_nameepisode_title
episode_idheadtails        r/   episode_thread_idrT   j   sU     88N 2M4GRHNND"bq!.YD#dV1TF--r1   T)frozenc                  4    e Zd ZU dZded<   ded<   dZded<   y)TraceHandleuS   지금 열려 있는 trace 의 손잡이. frozen — scope 안에서 안 바뀐다.r5   uidnameNOptional[str]	thread_id)__name__
__module____qualname____doc____annotations__r[   rI   r1   r/   rW   rW   }   s    ]	H
I#I}#r1   rW   opik_trace_ctx)defaultz!ContextVar[Optional[TraceHandle]]
_trace_ctxc                 ~   t         t         S t        5  t         ddl} ddlm} |j
                  rTt        j                  j                  d|j
                         t        j                  j                  d|j                         | j                  |j                        a ddd       t         S # 1 sw Y   t         S xY w)u  Opik SDK 클라이언트 — 프로젝트 이름을 명시해서 만든다.

    ★인자 없이 만들면 기본 프로젝트로 쌓여 텍스트 호출과 갈린다
    (2026-08-07 에 실제로 그래서 「이미지 기록이 통째로 없다」고 잘못 판단했다).
    Nr   settingsOPIK_URL_OVERRIDEOPIK_WORKSPACE)rO   )_client_client_lockopikapp.core.configrf   opik_url_overrider!   environ
setdefaultopik_workspaceOpikopik_project_name)rk   rf   s     r/   _get_clientrs      s     	?0))

%%')C)CE

%%$h&=&=?iiX-G-GiHG 
 N 
 Ns   BB..B<c                 *    t         j                         S )u,   지금 열려 있는 trace (없으면 None).)rc   r4   rI   r1   r/   current_traceru      s    >>r1   c                ,    t         j                  |       S )uM   worker thread 명시 전파용 — 호출자가 reset_trace 로 되돌린다.)rc   set)handles    r/   
bind_tracery      s    >>&!!r1   c                .    t         j                  |        y rH   )rc   reset)tokens    r/   reset_tracer}      s    Ur1   c                Z     ddl }t               |j                          fd       }|S )ub  지금 trace 를 캡처해 worker thread 안에서 다시 세우는 wrapper.

    `ContextVar` 는 thread 를 넘지 않는다. 병렬 롤은
    `multiroll_select.py:1385` 에서 budget·generation_context 를 명시
    전파하는데, trace 핸들은 **세 번째 ContextVar** 라 같이 실어야 한다.
    안 실으면 worker 호출이 부모 없이 떨어져 **계층이 병렬 구간에서만
    조용히 무너진다.**

    ★worker 가 끝나면 반드시 되돌린다 — thread pool 은 thread 를 재사용해서,
    안 되돌리면 다음 작업이 남의 부모를 물려받는다.
    r   Nc                     t         j                        }	  | i |t         j                  |       S # t         j                  |       w xY wrH   )rc   rw   r{   )argskwargsr|   capturedfns      r/   _wrappedz$bind_current_trace.<locals>._wrapped   s@    x(	$t&v&U#JU#s	   5 A)	functoolsru   wraps)r   r   r   r   s   `  @r/   bind_current_tracer      s1     H__R$ $ Or1   Dict[str, Any]_LIVE)outputerrorc               2   | y	 t         j                  | j                  d      }|yi }|||d<   |rdt        |      dd dd|d<   |r |j                  d	i | |j                          y# t        $ r }t        j                  d|       Y d}~yd}~ww xY w)
u)   trace 를 닫는다. 실패는 삼킨다.Nr   PipelineErrori  rK   )exception_typemessage	traceback
error_infou#   finish_trace 실패 (non-fatal): %srI   )	r   poprX   r5   updateend	Exceptionloggerdebug)rx   r   r   livepayloadexcs         r/   finish_tracer      s     ~AyyT*<"$ &GH"1u:ds+%GL!
 DKK"'"
 A:C@@As   "A- AA- -	B6BBc                 ^    ddl m}  t        | dd      syt               }||j                  S dS )u   지금 열려 있는 trace 의 uid — 세 곳에 같이 적을 값.

    Opik trace id 를 그대로 쓴다. 별도 uid 를 하나 더 만들면 둘이 어긋날
    자리가 생긴다 — 같은 것을 두 이름으로 부르지 않는다.
    r   re   opik_trace_v2_enabledFN)rl   rf   getattrru   rX   )rf   rx   s     r/   current_shot_uidr      s2     )84e<_F+6::55r1   c                ~   g }| j                  d      xs g D ]U  }t        |t              s|j                  |j                  d      |j                  d      |j                  d      d       W g }| j                  d      xs g D ]E  }t        |t              s|j                  |j                  d      |j                  d      d       G | j                  d	      xs i }t        |t              rt	        |j                  d
      xs g       nd}	 ddlm}  ||       }| j                  d      | j                  d      | j                  d      | j                  d      || j                  d      | j                  d      | j                  d      |||| j                  d      t        | j                  d      t              r-t        | j                  d      xs i j                  d            ndt        | j                  d            dS # t        $ r#}	t        j                  d|	       d}Y d}	~	d}	~	ww xY w)u1  샷 trace 의 `output` 에 실을 **영향 요약**.

    ★프롬프트 전문·판정 전문은 **안 싣는다.** 그것은 span 에 이미 있다 —
    두 벌로 저장하면 어느 쪽이 진짜인지 갈린다. 여기에는 「무엇이 들어갔고
    무엇이 골랐나」만 남긴다.
    refslabelasset_idrole)r   r   r   verdictsscore)r   r   critiqueissuesr   )fix_stage_wonu+   fix_stage_won 판정 실패 (non-fatal): %sFNshot_run_uidinput_fingerprintref_mode
share_planselectedrankingtotalsfix_skip_reasoncineappliedneeds_reshoot)r   r   r   r   r   r   r   r   r   issue_countfix_appliedr   cine_appliedr   )r4   
isinstancedictr7   len!app.modules.pipeline.still_reciper   r   r   r   bool)
recordr   rr   r;   r   r   r   r   r   s
             r/   build_influence_summaryr     s    Djj &B&!T"aeeGn!"z!2UU6], 	- ' Hjj$**aOOaeeGnquuW~NO + zz*%+H7A$8#hll8,23 C#F+ 

>2#ZZ(;<JJz*jj.JJz*::i(**X&""!::&78fjj($/ fjj06B;;IFG5:fjj9: 	  BCHs   H 	H<H77H<c                    t               }|y	 t        j                  |j                        }||j	                  |        yy# t
        $ r }t        j                  d|       Y d}~yd}~ww xY w)uI   지금 열려 있는 trace 의 output 을 채운다. 실패는 삼킨다.N)r   u*   update_trace_output 실패 (non-fatal): %s)ru   r   r4   rX   r   r   r   r   )r   rx   r   r   s       r/   update_trace_outputr   7  sf    _F~Hyy$KKvK&  HA3GGHs   3A 	A-A((A-)
input_datac           	   #  R  K   ddl m} t        |dd      sd yd}	 t               }t	               j                  || t        |      t        |      ||xs i       }|t        |<   t        || |      }|d yt        j                  |      }
	 | t        |       	 t        j#                  |
       y# t        $ r"}	t        j                  d|	       d}Y d}	~	id}	~	ww xY w# t        $ r}	t        |t!        |	      	        d}	~	ww xY w# t        j#                  |
       w xY ww)
u  이름 있는 trace 를 열고 scope 에 세운다.

    설정이 꺼져 있으면 **아무것도 안 하고 None 을 준다** — 그 경우 하위
    호출은 지금처럼 각자 trace 를 만든다(바이트 동일).

    실패해도 None 을 줄 뿐 예외를 안 낸다 — 부모가 없으면 하위가 홀로
    설 뿐이고, 그것이 기록 없는 것보다 낫다.
    r   re   r   FN)idrY   tagsmetadatar[   input)rX   rY   r[   u!   open_trace 실패 (non-fatal): %s)r   )rl   rf   r   r0   rs   tracelistr   r   rW   r   r   r   rc   rw   r   r5   r{   )rY   r   r   r[   r   rf   rx   rX   r   r   r|   s              r/   
open_tracer   D  s    " )84e<
$(Fo}""DJ(^y" # 

 c
49E
 ~
NN6"E 
 	V#  8#>  V3s8, 	se   D'AB7 4D'C% D !D'7	C" CD'C""D'%	D
.DD

D D$$D')returnr5   )r   rZ   r   rZ   r   rZ   r   rZ   r   rZ   r   rZ   r   	List[str])r=   objectr   r   )rO   r5   rP   r5   rQ   r5   r   r5   )r   Optional[TraceHandle])rx   r   r   r   )r|   r   r   None)rx   r   r   Optional[Dict[str, Any]]r   rZ   r   r   )r   rZ   )r   r   r   r   )r   r   r   r   )rY   r5   r   r   r   r   r[   rZ   r   r   r   zIterator[Optional[TraceHandle]]).r_   
__future__r   loggingr!   	threadingr   
contextlibr   contextvarsr   r   dataclassesr   typingr   r	   r
   r   r   	getLoggerr\   r   r3   STEP_TAG_PREFIXr0   r>   rE   rT   rW   rc   r`   ri   Lockrj   rs   ru   ry   r}   r   r   r   r   r   r   r   rI   r1   r/   <module>r      s   #  	   % ) ! 6 6			8	$ D !^$A&C. " #
# 	# 	#
 # # # #LD..),.:=..& $$ $ $ 1;d1
-  y~~0
"
> ~  (,	A!A %A 	A
 
A:63l
H  ,01 
1  1  	1 
 1  )1  %1  1 r1   