
    Oj                    H   d Z ddlmZ ddlZddlmZmZmZ ddlm	Z	m
Z
m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ddd	 	 	 ddZej4                  Zej6                  ZddZdddddZddZ G d dee      Z G d dee      Z  G d dee      Z!y)u!  GROUNDING-V2 §2-3.5 — 분류·계획 스텝.

계획 §1.8 의 **★A 경계**다:

    13.5  entity_merge          short_id 중복 제거 (DB canon 아님)
      ↓
    13.6  ★grounding_plan       검색 0 · short_id 기준 · Sol classifier · route 확정
      ↓
    13.7  entity_filter         보호 목록에 있으면 안 지운다  ← union 을 여기서 받는다

★모드별로 하는 일이 다르다 (계약 §12):

    legacy       **안 돈다** — manifest 의 `if_grounding_v2` 가 걸러 낸다
    shadow_plan  **안 돈다** — legacy 와 구별이 안 된다
    v2           체크포인트를 만든다 — `entity_filter` 가 읽는다

★``shadow_plan`` 을 파이프라인에 들여놓지 않는 이유: 이 스텝들은 production
step id 를 쓰고 DAG 에 하류가 걸려 있어, `force` 로 한 번 돌리면 `_execute`
전에 그 하류가 통째로 무효화된다. shadow 관측은 **저장 CP 만 읽는 offline
재생기**(§2-3a)가 한다.

★``classify_samples`` 를 **실제로 부르는 자리**다.
    )annotationsN)AnyDictList)GROUNDING_MODE_LEGACYGROUNDING_MODE_V2resolve_grounding_mode)
StepRunner)grounding_carry)SUBJECT_ID_CONTRACT_VERSION)_EntityStepMixin   expected_judgesexpected_samplesc               (   | sdgS g }dD ]C  }| D ch c]  }|xs i j                  |       }}|r	d|v sd|v s0|j                  | d       E |rt        d |D              }t        | D ch c]   }t        |xs i j                  d            " c}      }||k7  r|j                  d| d	|        |D ]g  }	| D cg c]%  }t        |xs i j                  d            |	k(  r|' }
}|s6t	        |
      |k7  sE|j                  |	 d
t	        |
       d| d       i |D 	ci c]P  }	|	| D ch c]A  }t        |xs i j                  d            |	k(  rt        |xs i j                  d            C c}R }}	}|j                         D cg c]  }|D ]  }|  }}}t	        |      dkD  rIt	        t        |            t	        |      k  r)|j                  d| d	t        t        |                    i }| D ]?  }|j                  t        |xs i j                  d            g       j                  |       A t        |j                               D ]  \  }	}dD ]  }|D ch c]  }|xs i j                  |       }}|rd|v sd|v r|j                  |	 d| d       Gt	        |      dkD  sV|j                  |	 d| dt        t        t        |                      |S c c}w c c}w c c}w c c}w c c}}	w c c}}w c c}w )u0  표본들이 **같은 것을 잰 것인지** 본다. 어긋나면 사유 목록.

    ★기록만 하면 「같은 것을 N회 쟀다」가 성립하는지 아무도 안 본다.
    측정 도구에만 있던 gate 를 여기로 올린다 (계획 §2-3.5 완료 조건).

    ★판정자가 여럿(패널)이면 **무너졌는지**까지 본다. production 은
    ``strict_single_attempt=False`` 라 tier fallback 으로 gpt 슬롯이 gemini 로
    넘어갈 수 있다. 그러면 실제 alias 는 둘 다 gemini 이고, 「판정자마다
    한 값」만 보는 gate 는 **한 모델 패널을 통과시킨다**(Codex).
    그래서 ①부탁한 집합 ②판정자별 표본 수 ③실제 물리 모델의 **다름**을 본다.
    무너지면 fail-closed — 「미확정」이지 통과가 아니다.
    u!   표본 지문이 하나도 없다)requested_judge_aliasjudge_model_aliasjudge_physical_modelN u    이 비었다c              3  2   K   | ]  }t        |        y wNstr).0as     T/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/steps/grounding_steps.py	<genexpr>z#check_provenance.<locals>.<genexpr>G   s     6oc!fo   r   u3   부탁한 판정자 집합이 다르다 — 기대 	   , 실제 u   : 표본이 u   개 — u   개여야 한다r      u0   패널이 한 모델로 무너졌다 — 부탁 )payload_hashprompt_versionr   : u    이 표본마다 다르다 )
getappendsortedr   lenvaluesset
setdefaultitemsmap)runsr   r   badcolrvalswantgotaliasgrpactualmsmflatby_judgegroups                    r   check_provenancer=   -   s>    344C(,01Dqbc"D1tt|rTzJJ#n-.	( 6o664P4ac17--(?@A4PQ$;JJLTFR[\_[`abE" Ld17--(?@AUJ dC LCH0@$@

g\#c(8<L;MM]^`	   $%  $e #'S#'a !'r/F!GHEQ qwBmm,BCD#'S S  $ 	 % #MMO8ObRRO8t9q=ST^c$i7JJB4&	RXY\]aYbRcQdeg &(HCb.E FGLSSTUV x~~/0uMC0561QW"MM#&D644<2:

eWBse>:;TQ

gRu$@CQUAW@XY[ N 1 JI 2 QLS % 9 7s6   K/.%K4?*K9'
L1AK>7LL	*L>Lc                @   ddl m} ddlm} t        j                  |       }|s |d|  dd      | |d}t        |      D ]  \  }}	  || |||d	
      }|d   ||<    |S # t        $ r2}	 |d|  d| d| dt        |	      j                   d|	 	d      |	d	}	~	ww xY w)u  팩의 **실효 바이트**를 지문에 넣을 수 있는 모양으로. 못 읽으면 그대로 표시.

    ★버전 문자열만 접으면 **같은 버전 디렉토리의 내용을 고쳤을 때** stale 을
    못 잡는다. 실제로 이 저장소에서 「팩에 스템만 추가했는데 지문이 안
    움직였다」를 두 번 냈다(`test_published_pack_ratchet` 참조).
    r   AppError)resolve_effectivezgrounding.pack_stems_unknownu.    의 지문 대상 파일을 안 적어 뒀다i  codemessagestatus_code)moduleversionN)kindrG   dbzgrounding.pack_unreadable/(u-   ) 팩을 못 읽어 지문을 못 만든다: r$   raw_content_hash)
app.core.errorsr@   app.modules.prompt_loaderrA   _PACK_FILESr%   r'   	Exceptiontype__name__)
rF   rG   r@   rA   stemsoutstemrH   effexcs
             r   _pack_fingerprintrX   o   s     );OOF#E/hLM
 	

 &,@CUm
d	#FDtW'+-C *+D	 $ J  	 0"81TF!D61^ I../r#8	
 	s   	A""	B+-BB)systemprompt)	a0_schemaschema)rY   )classify_schemar]   )rY   )claims_schemar]   )rY   )chunk_schemar]   )merge_systemr[   )merge_schemar]   )grounding_a0grounding_classifygrounding_claims_searchgrounding_chunkc                    dd l }dd l}|j                  |j                  | dd      j	                  d            j                         d d S )Nr   TF	sort_keysensure_asciizutf-8   )hashlibjsonsha256dumpsencode	hexdigest)payloadrl   _jsons      r   _hash_payloadrt      sF    >>Gt%@GGPik#2     c                  "    e Zd ZdZdddZddZy)GroundingA0Stepu  ★**거르기 전에** 원문에서 고증 후보를 건진다. 검색을 하지 않는다.

    뒤 단계의 추출 규칙들이(한 번만 나옴 · 착용물 · 고정 설비 · 텍스트만 다른
    종이류) **고증 대상을 정확히 같이 걸러 낸다.** 그래서 그 앞에 선다.

    ★`legacy` 에서는 manifest 의 `if_grounding_v2` 가 not_applicable 로
    걸러 **이 스텝 자체가 안 돈다.**
    c                   ddl m} t        | j                        }|t        k(  r!t
        j                  d       ddd|g ddddS | j                         }| j                  d      }|xs i j                  d      xs i } ||| j                  | j                  t        |j                  d	      xs d
      j                         t        |j                  d      xs d
      j                         | j                  | j                               }|j                  d      xs g }i }	|D ]:  }
|	j                  |
j                  dd      d      dz   |	|
j                  dd      <   < t
        j                  dt        |      |	t        |j                  d      xs g              t        |      t        |      t        |j                  d      xs g       | j!                         d|i|d|	idS )Nr   )collect_candidatesu5   grounding_a0: mode=legacy — 아무것도 안 한다T)mode
candidatesskippedsearch_calls)completed_countapplicable_countfailed_countdatavisual_world_rulesr   erar   region)
project_id
episode_idr   r   project_configopik_metadatar{   
owner_type?r!   u7   grounding_a0: 후보 %d개 %s · 지어낸 인용 %d건hallucinatedrz   owner_countsr~   r   r   config_hashr   )!app.modules.pipeline.grounding_a0ry   r	   r   r   loggerinfo_load_cleaned_text_load_prev_checkpointr%   r   r   r   stripbuild_opik_metadatar(   _config_hash)selfrz   ry   g_modefulltextrules_cprulesrT   candsownerscs              r   _executezGroundingA0Step._execute   s   H'(;(;<**KKOP'(aQR%+2$-.01 1 **,--.BCR$$V,2 4??EIIe$*+113uyy*0b1779..224
 %+!#A/5zz!%%c:RTU/VYZ/ZF155s+, MJCGGN,C,Ir(J	L  #5z #E
 7 =2>,,.VCsCNFC
 	
ru   c           	         ddl m} ddlm} t	         || j
                        |j                  t        t        d|j                        d      S )u   ★팩 바이트·계약 버전을 지문에 접는다.

        안 접으면 팩을 고쳐도 resume 이 **옛 체크포인트를 그대로 건너뛴다** —
        고친 것이 아무 데도 안 닿는다.
        r   compute_config_hash)rc   rc   )basea0_contractsubject_contractpack)
app.core.step_runnerr   app.modules.pipelinerc   rt   r   A0_CONTRACT_VERSIONr   rX   PROMPT_PACK_VERSION)r   r   _a0s      r   r   zGroundingA0Step._config_hash   sF     	=<'(;(;<22 ;%nc6M6MN	
  	ru   Nresumerz   r   returnDict[str, Any]r   r   )rR   
__module____qualname____doc__r   r    ru   r   rw   rw      s    !
Fru   rw   c                  v    e Zd ZdZd
ddZeddddddddddd
	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dd       Zdd	Zy)GroundingPlanStepu@   고증 후보 분류·계획. ★**검색을 하지 않는다.**c                ^   ddl m}m} ddlm} ddlm} ddlm} t        | j                        }|t        k(  r9t        j                  d       | j                  |g dd| j                               S | j!                  d	      }|xs i j#                  d
      xs i }	| j!                  d      }
|
xs i j#                  d
      xs i }t%        |j#                  d      xs d      j'                         }t%        |j#                  d      xs d      j'                         }|r|sddlm}  |dd|d|dd      | j!                  d      }|xs i j#                  d
      xs i j#                  d      xs g }t-        j.                  |	| j0                  | j2                  d	|      }|d   }|d   }|d   }|d   }|j#                  d      xs i }t        j                  dt5        |      ||       |j#                  dd      |j#                  d d      z   }|rt        j7                  d!|       |j#                  d"      xs d#D cg c]  }|j#                  d$      d%k(  r|d&    }} |||	j#                  d'      xs g |	j#                  d(      xs g |	j#                  d)      xs g d*|+      }|d,   s&t        j7                  d-|d.   t9        |d/                t;        |j#                  d0      xs d      t;        |j#                  d.      xs d      z
  }|dkD  rFt        j7                  d1||j#                  d0      |j#                  d.      |j#                  d2             |sXi }|D ]   }|j#                  |d3   d      d4z   ||d3   <   " | j                  |t=        |      d|||||| j                         5	      S  ||t>        ||| j                  | jA                         6      }|j#                  d7      xs g }t=        |j#                  d8      xs g       } tC        |t=        |      t>        9      }!| t=        |      k7  r |!jE                  d:t=        |       d;|         |!r#ddlm}  |d<d=d>jG                  |!      z   d?      g }"|D ]g  }#|d@   j#                  |#d&   g       }$ ||$      }|"jE                  i ||#d&   |#dA   |#dB   |#j#                  dC      xs i j#                  dD      dE       i |"|z   }%i }&|%D ]   }|&j#                  |d3   d      d4z   |&|d3   <   " tI        dF |%D              }'t        j                  dG|t5        |"      |&|'       | j                  ||%t5        |      |&|'|j#                  dHd      |j#                  d7      xs g ||||| j                         I      S c c}w )JNr   )DEFAULT_JUDGESclassify_samples)decide_route_from_samples)completeness_report)build_subjectu7   grounding_plan: mode=legacy — 아무것도 안 한다T)decidedsubject_countr|   r   entity_merger   r   r   r   r   r?   z$grounding_plan.missing_world_contextu4   visual_world_rules 에 시대/지역이 없다 (era=	, region=)  rB   rc   r{   r   r   source_stepa0_candidatessubjectsunboundcarry_reasonscarriedcandidate_ledgeru?   grounding_plan: A0 후보 %d개 중 %d개를 물려받았다 %s	ambiguousduplicate_surfaceu?   grounding_plan: 표면형이 모호해 결속 안 한 것 %d건rowsr   dispositionpromotedresearch_subject_id
characters	locationsprops)	characterlocationprop)promoted_idscompleteu=   grounding_plan: A0 후보 %d개가 산출에 안 남았다 %smissing_countmissingentity_missing_countu   grounding_plan: 엔티티 행이 없어진 A0 후보 %d개를 **승격으로 살렸다** — 추출이 지운 것이다. 조사는 계속되지만 그 사실은 남는다 (entity_missing=%d · missing=%d · promoted=%d)promoted_countrouter!   )r   r   countscompleteness
a0_carriedr   r   r   )samplesr   r   r   r   r.   judgesr   u@   분류기가 돌린 판정자가 계약과 다르다 — 계약 r    z"grounding_plan.provenance_mismatchu)   같은 것을 N회 잰 것이 아니다: z; i  
by_subjectsurface_formr   
provenanceshort_id)r   _surface_form_owner_type	_short_idc              3  D   K   | ]  }|j                  d       sd  yw)unstabler!   Nr%   )r   ds     r   r   z-GroundingPlanStep._execute.<locals>.<genexpr>  s     =%Q155+<q%s     u9   grounding_plan: mode=%s · %d후보 · %s · 흔들림 %dlogical_calls)r   r   r   unstable_countr   r.   r   r   r   r   r   )%)app.modules.pipeline.grounding_classifierr   r   &app.modules.pipeline.grounding_plannerr   &app.modules.pipeline.grounding_overlayr   &app.modules.pipeline.grounding_subjectr   r	   r   r   r   r   _wrapr   r   r%   r   r   rM   r@   _carrybuild_subjectsr   r   r(   warningr'   intlistDEFAULT_SAMPLESr   r=   r&   joinsum)(r   rz   r   r   r   r   r   r   merge_cpentitiesr   r   r   r   r@   a0_cpa0_candsbuiltr   r   r   r   r   _unboundr1   	_promotedreport_savedonlyr   smpr.   reportedr/   r   srecsfinalr   r   s(                                           r   r   zGroundingPlanStep._execute   s   	
 	UNH'(;(;<** KKQR::fb4*.*;*;*=  ? ? --n=N''/52--.BCR$$V,2%))E"(b)//1UYYx(.B/557&0 ;!!$y
!=	  **>:[b%%f-388FL"%%T__&h@ $	"o.	" !99%78>BUM7M	;!%%k15#''(;Q?@NN\#%  033F;ArA<Am,
: ,-A 	 < %X!l39r [17RLL)/R0
 "	#
 j!
 NNZ!/2F6);L4MO VZZ 67<1=&**_-234A:NNM 

#9:

?+VZZ8H-IK  $&D#'88AgJ#:Q#>QwZ   ::fd7m1%)+1g,9/?*.*;*;*=  ? ? o3v..224
 wwv$")/R0 0,. tN++JJ~./y
DE 09CdiiPSnT  )+A|$((+@)A2FD)$/ANN '()>'?!">!2 eeL17R<<ZH   '!!#A!'AgJ!:Q!>F1W: =%==OCL&(	< zzEX(''/15&BG':J))+  
 	
C<s    V*Nr   Fr   )
r   r   r   r.   r|   r   r   r   r   r   c                  |xs i t        fdt        j                  D              }t        |xs i j	                  d      xs d      |z   }|t        |      z   t        |D cg c]  }|j	                  d      s| c}      z
  }| ||xs i ||d|xs g g |	||xs i |xs i d}|rd|d<   | t        k(  r||d<   ||||
|d	S c c}w )
u  ★체크포인트 모양을 **다른 스텝과 같게** 만든다.

        `run()` 은 ``save_checkpoint({"status": ..., **result})`` 로 **평평하게**
        펼친다. 그래서 하류가 읽는 자리는 ``manifest["data"]`` 다 —
        평평하게 돌려주면 `entity_filter` 의 ``data.decided`` 가 **늘 비어**
        등록을 해도 보호가 하나도 안 걸린다. 실제로 그랬다.
        c              3  B   K   | ]  }j                  |d         yw)r   Nr   )r   r1   reasonss     r   r   z*GroundingPlanStep._wrap.<locals>.<genexpr>  s%      @(>1  '{{1a0(>s   r   r   r   )rz   r   r   r   r   r}   r.   r   r   r   unresolved_carry_countr   r   Tr|   r   r   )r   r   UNBOUND_REASONSr   r%   r(   r   )r   r   r   r   r   r   r.   r|   r   r   r   r   r   unresolved_carryr   r   donerr   r  s                     @r   r   zGroundingPlanStep._wrap  s%      %2  @(.(>(>@ @*//@EAF%&
 s7|+c3@!!%%(=">Aw3@ /A A *l,*JB$$&6(.B !1 6B#
" !%GI &&!(GI# $ $&
 	
73@s   %C
<C
c                   ddl m} ddlm} ddlm} ddlm} t         || j                        |t        j                  |t        t        t        d |j                  D              |j                   t#        d|j$                        d	      S )	u  ★팩·계약·**표본 수**·**판정자**·대상 모델을 접는다.

        표본 수를 바꾸면 판정이 달라지는데 지문이 안 움직이면 옛 판을 그대로
        쓴다. ★**판정자 목록**도 마찬가지다 — `gpt+gemini-pro` 를
        `gpt+grok` 으로 바꿔도 지문이 안 움직이면 resume 이 **옛 판정을 그대로
        건너뛴다**. 누가 답했는지가 바뀌었는데 다시 안 묻는 것이다 (Codex).
        r   r   )grounding_classifier)OVERLAY_CONTRACT_VERSIONPLANNER_CONTRACT_VERSIONc              3  2   K   | ]  }t        |        y wr   r   )r   js     r   r   z1GroundingPlanStep._config_hash.<locals>.<genexpr>  s     @-?SV-?r   rd   )	r   planner_contractcarry_contractoverlay_contractr   r   r   classifier_contractr   )r   r   r   r  r   r  r   r  rt   r   r   CARRY_CONTRACT_VERSIONr   r   r'   r   CLASSIFIER_CONTRACT_VERSIONrX   r   )r   r   _gcr  r  s        r   r   zGroundingPlanStep._config_hash  sz     	=DSS'(;(;< 8$;; 8 ;& @S-?-?@@#&#B#B &&:&)&=&=?#
  	ru   r   r   )r   r   r   zList[Dict[str, Any]]r   r   r   Dict[str, int] | Noner   r   r   r   r.   zList[Any] | Noner|   boolr   Dict[str, Any] | Noner   r   r   r   r   r"  r   r$  r   r   r   )rR   r   r   r   r   staticmethodr   r   r   ru   r   r   r      s    Jm
^ .2!#$t#4 !b598<=
+=
DG=
 =
,<=
 =
 4I=
 	=
 14	=

 3=
 !6=
 "=
 =
~ ru   r   c                      e Zd ZdZdZdZdZdddZddZd Z	dd	dd
Z
d ZddZd ZddZddZedddddddddddddd	 	 	 	 	 	 	 	 	 	 	 dd       Zy)GroundingResearchStepu  ★**출처 붙은 조사를 실제로 산다.** GROUNDING-V2 §2-4b.

    `grounding_plan` 이 `route="research"` 로 표시한 것만 조사한다 — `skip` 은
    안 산다. 그게 계획의 뜻이다.

    ## 시간 통제 세 겹 안에서 돈다 (§8.5)

    `research_run_scope(cap=, deadline_seconds=)` 로 열고, 그 안에서
    `search_claims` 가 호출마다 마감을 건다. 셋 중 어디에 걸리든 판정은 같다 —
    `time_capped` · `unresolved` · retryable. 무엇에 걸렸는지는 `limit_kind` 다.

    ## ★**main thread 만** 결과를 남긴다

    `call_with_deadline` 은 **취소가 아니라 포기**라, 버려진 worker 가 나중에
    값을 들고 온다. 그 값은 `search_claims` 가 이미 버렸고 여기서는 **돌아온
    행만** 저장한다 — worker 는 바깥 호출과 감사 기록만 한다.

    ## batch 크기는 **1** 이다

    §4b 실험이 네 크기를 다 돌고 「후보 없음 · 개별 호출 유지」로 끝났다.
    어긋남은 어느 크기에서도 0이었지만 **크기를 키울수록 조사량이 말랐다**
    (계약 통과 claim 19 → 16 → 10 → 9). 그래서 기본값을 안 바꾼다.
    <   g      @r!   c                F    ddl m} ddlm} ddlm} t         j                        }|t        k(  r)t        j                  d        j                  |g d      S  j                         \  }}|s|r)t        j                  dt        |      t        |             sEt        j                  d	t        |              j                  |g || j                         
      S  j!                        }t#        |j%                  d      xs i       }	t'        |d         }
t)        |d         }t)        |d         }D cg c]  }|d   |
v s| c}|s|r)t        j                  dt        |      t        |             s>t        j                  d        j                  |g |||||	 j                         
      S t+         j                  j%                  d      xs  j,                        }t/         j                  j%                  d      xs  j0                        }t        j                  dt              ||       d fd} |||      5 }|j3                   ||j4                         j7                          j8                   j:                   j=                         |	      }d d d        j%                  d      xs g }j?                         }|D cg c]  }|j%                  d      s| }}t        j                  d|j%                  dd      |t        |              j                  ||||j%                  dd      |j%                  dd      |||||	 j                                S c c}w # 1 sw Y   xY wc c}w )!Nr   )openai_client)research_run_scopere   u;   grounding_research: mode=legacy — 아무것도 안 한다T)r   r|   u@   grounding_research: 인용 없는 대상 %d · 못 세운 것 %du;   grounding_research: 살 대상이 없다 (인용 없음 %d))r   r   r   	no_sourcer   r   r   r   expected_input_hashesadmittedcappedalready_doner   uC   grounding_research: 이미 끝난 것 %d · 상한에 밀린 것 %du,   grounding_research: 새로 살 것이 없다)	r   r   r   r-  r   r2  deferredexpectedr   research_transmission_capresearch_run_deadline_secondsuF   grounding_research: 대상 %d개 · 전송 상한 %d · 마감 %.0f초c                8    j                  | g      z  y)u|  ★★**산 것을 한 호출마다 바로 남긴다.** 9개 중 7개째에 끊기면
            앞의 7개를 통째로 잃고 다시 사야 한다 — 실제로 그 부류의 결함을
            이 프로젝트에서 냈다.

            ★이 콜백은 `search_claims` 가 **main thread 에서** 부른다. 버려진
            worker 는 여기 못 온다.
            r.  N)_save_revisions)rowr   r   savedr   r   s    r   _persistz0GroundingResearchStep._execute.<locals>._persist]  s,     T))3%s17 * 9 9Eru   )capdeadline_seconds)max_retries)modelr   r   
batch_sizerI   r   on_batchbatches
limit_kinduX   grounding_research: 논리 %d회 · 물리 %s · 상한에 걸린 것 %d · 저장 %d행r   bought_calls)r   r   r   budgetr   rD  r-  r   r:  r2  r3  r4  r   ) app.core.openai_keysr*  app.core.research_call_budgetr+  r   re   r	   r   r   r   r   _wrap_research_research_targetsr   r(   r   _plan_admissiondictr%   r*   r   r   DEFAULT_TRANSMISSION_CAPfloatDEFAULT_RUN_DEADLINE_SECONDSsearch_claimsSDK_RETRIES_search_model
BATCH_SIZErI   r   snapshot)r   rz   r*  r+  gcsr   r-  r   	admissionr4  keepr3  alreadyr
  r<  deadliner;  rE  rT   r   spentr1   r1  r   r   r:  r   s   `                      @@@@r   r   zGroundingResearchStep._execute"  s   6DG'(;(;<**KKUV&&vB&EE484J4J4L1#vy'NN]y>3w<9KKUI(&&RS#W --/ ' 1 1 ((s6(J	 	&=>D"E9Z()	(+,y01'Lx!1-B+Ct+KAxLhKK]Gc(m5KKFG&&RS#W$x!t/@/@/B	 ' D D $%%))*EF 3224,,001PQ > ==?\M3	2
 
	9 
	9  C(Cv###//:H((*F??tww"668! $ #C D wwy!'R!!9TQUU<%8!T9 &GGOQ/FU	L ""3v(C3 84+<+<+> # @ 	@a MF DC :s%   	NN$AN1NNNc               <   ddl m}m} |D cg c]  }|d   	 }} |||D ci c]  }|d    ||       c}| j                  ||      | j	                         t        | j                  j                  d      xs d      xs d| j                        S c c}w c c}w )uY  무엇을 새로 살지. ★**바깥 호출 전에** 정한다.

        ★이미 끝난 행(`completed`)은 다시 안 산다. 상한을 넘은 것은 버리지 않고
        `capped` 로 남아 다음 주행이 **앞에서부터 이어간다** — 상한만 두고
        이어가는 계약이 없으면 영구 partial 이 된다.
        r   )plan_admissionsubject_payload_hashr   r.  research_subject_capN)subject_payload_hashesresearch_inputs	done_rowsr<  r@  )	%app.modules.pipeline.grounding_claimsr[  r\  _research_inputs
_done_rowsr   r   r%   rR  )r   r   r   r   r[  r\  r
  idss           r   rJ  z%GroundingResearchStep._plan_admission~  s    	2 2::Aq&':EM$OEM %&&;$<$8$;%<EM$O 11c&1Ioo'D''++,BCHqIQT( 	( ;$Os
   BB
c                v    ddl m} t        | dd      }|#|j                  | j                        }|| _        |S )u  ★★★**활성 DB 팩**을 한 번 읽어 세 자리가 같이 쓴다 (Codex).

        `search_claims(db=self.db)` 는 DB 에 올라간 prompt/schema 를 쓰는데,
        신원과 지문을 `db=None` 으로 읽으면 **셋이 갈린다**:
        ①admission 이 낸 hash 와 저장된 hash 가 달라 **재개마다 다시 사고**
        ②config 지문이 DB 변경을 못 봐 **완료 CP 를 건너뛴다**.
        r   r,  _pack_cacheN)rI   )r   re   getattr	load_packrI   rf  )r   rT  cacheds      r   _packzGroundingResearchStep._pack  s<     	H}d3>]]dgg].F%Dru   N)provc                  ddl m} ddlm} t	        |xs i       }|s| j                         nd}|||r|j                  d      n|d   |r|j                  d      n|d   |j                     d	   |r|j                  d
      n|d   |j                     d	   t        |      t        d|i      |j                  d      xs d|j                  d      xs | j                         |j                  d      xs | j                         d
S )u!  입력 신원 중 **대상과 무관한** 좌표. ★한 곳에서 만든다.

        ★★`prov` 를 주면 그 호출이 **실제로 쓴** 좌표를 쓴다. 입력 신원은
        「무엇을 요청했나」이고 provenance 는 「무엇이 답했나」라, 계약이 그
        일곱 쌍의 **일치**를 검사한다 — 저장할 때 살 때의 값을 그대로 쓰면
        팩이 바뀐 판에서 둘이 갈린다.
        ★프로덕션에서는 같은 주행이 같은 팩으로 사고 저장하므로 값이 같다.
        r   r,  r  Nprompt_pack_versionrG   prompt_raw_hashrS   rL   schema_raw_hashplannerprovideropenair?  )
r   r   claims_pack_versionrn  ro  policy_versionpolicy_contract_hashrq  r?  model_version)r   re   r   r  rK  rj  r%   SYSTEM_STEMSCHEMA_STEMr   rt   rQ  )r   r   r   rk  rT  r  pvr   s           r   rb  z&GroundingResearchStep._research_inputs  s     	H	& $*"#%tzz|4&') %'FF+@$A/3I=?'8 9 $Wcoo >$6!8=?'8 9 $Wcoo >$6!8!":;$145%7z*6hVVG_<(:(:(<VVG_D0B0B0D
 	
ru   c                f   ddl m} | j                  g S | j                  j                  |      j	                  |j
                  | j
                  k(  |j                  | j                  k(        j                         }|D cg c]&  }|j                  |j                  |j                  d( c}S c c}w )uS   이 에피소드의 **저장된 조사**. ★없으면 전부 새로 사게 된다.r   GroundingResearchRevision)r   research_input_hashstatus)app.models.projectr|  rI   queryfilterr   r   allr   r}  r~  )r   r|  r   r1   s       r   rc  z GroundingResearchStep._done_rows  s    @77?I781<<O1<<OQ 	 /34.2 )*(=(=()(=(=88%.24 	4 4s    +B.c                  ddl }ddlm}m} ddlm} ddlm}	m}
 |D ci c]  }|d   |
 }}|j                  |j                        j                         }d}|D ]  }t        |j                  d      xs i       }|s&t        j                  d|j                  d	             J|j                  d
      xs i j                  d      xs g }|j                  d	      xs g D ]A  }|j                  |      }|s|D cg c]6  }t        |t              st!        |j                  d      xs d      |k(  r|8 }}|r|d   j                  d      xs g ng }|r|d   j                  d      xs g ng } |
|      }| j#                  |||      } |	| j$                  | j&                  |||||i ||d   |d   dt!        |j)                               t+        |j                  d            |      }| j,                  !| j,                  j/                   |di |       |dz  }D  | j,                  |r| j,                  j1                          |S c c}w c c}w )u  산 것을 **정본 테이블**에 남긴다. ★판정은 `build_revision_row` 가 한다.

        ★호출부가 `decide_delta` 를 따로 부르고 결과만 넘기면 저장된 판정과
        저장된 claims 가 갈릴 수 있다 — 그래서 한 곳에서 한다.
        ★같은 시도를 두 번 안 넣는다(유일 제약). 재개하면 `attempt_id` 가
        달라져 새 행이 되고, 앞 행은 그대로 남는다.
        r   N)datetimetimezoner{  )build_revision_rowr\  r   r   uK   grounding_research: provenance 가 없는 행 — 저장하지 않는다 %s	requestedparsedresultsr   claimsgaps)r   r   rk  rt  rv  )rt  rv  rC  )r   r   r   r\  r_  r  r  r   
attempt_idtime_capped
created_atr!   r   )uuidr  r  r  r|  ra  r  r\  nowutc	isoformatrK  r%   r   r   
isinstancer   rb  r   r   uuid4r#  rI   addflush)r   r   r   r   r   r  r  r  r|  r  r\  r
  by_idr  nr9  rk  r  sidsubjr1   miner  r  sphrir  s                              r   r8  z%GroundingResearchStep._save_revisions  sX    	/@	66>?h()1,h?ll8<<(224C-34D   ;<?GGK<PRggh'-2229=CF,22yy~#) L6aZ4-@&; < BCsJ 6 L:>$q'++h/52B6:QF+1r +40 **s604 + 6*#4??(+),$&vD F$  F245E2F13O1D F  #4::<0 !%SWW\%: ;"%$& 77&GGKK 9 BE BCQM 3 b 771GGMMOm @"Ls   I>I%Ic                   | j                  d      xs i j                  d      xs i }|j                  d      xs g D ch c]'  }|j                  d      dk(  r|j                  d      ) }}| j                  d      xs i j                  d      xs i }t        |j                  d      xs d	      j                         }t        |j                  d
      xs d	      j                         }|r|sddlm}  |dd|d|dd      |sg ||g g fS | j                  d      xs i j                  d      xs i }| j                  d      xs i j                  d      xs i j                  d      xs g }	t        j                  || j                  | j                  d|	      }
|
d   D cg c]  }|d   |v r| }}|D cg c]0  }t        |j                  d      xs d	      j                         r|2 }}|D cg c]3  }t        |j                  d      xs d	      j                         s|d   5 }}t        ||D ch c]  }|d   	 c}z
        }|||||fS c c}w c c}w c c}w c c}w c c}w )u,  계획이 `research` 로 표시한 대상 + 그 **원문 인용**.

        ★subject 는 `grounding_plan` 과 **같은 함수**로 다시 세운다
        (`_carry.build_subjects`). 여기서 새로 발급하면 같은 대상에 다른 id 가
        붙어 계획과 조사가 갈린다.
        grounding_planr   r   r   researchr   r   r   r   r   r   r?   z(grounding_research.missing_world_contextu   시대/지역이 없다 (era=r   r   r   rB   r   rc   r{   r   r   source_quote)r   r%   r   r   rM   r@   r   r   r   r   r'   )r   planr   r3   r   r   r   r@   r   a0r  r
  pickedquotedr-  r   s                   r   rI  z'GroundingResearchStep._research_targets  s    **+;<B#f+$!# 	7;xx	7J7Pb7P 17P55>Z/ +,7P 1++,@AGR3v;%"$ 	%))E"(b)//1UYYx(.B/557&0 ?7wizQRS! !  sFB**..~>D"c&k(%' 	)).9?RDDVL SS&-*, 	%%T__&b: #:. 7.,-5 . 7 $ ?V~.4"5;;= V ?7= Fv!n 5 ;<BBD ,-v	 F 6 J6a#8!96 JJKsFIw66S167?F !Ks    ,I+I!5I&<8I+ I0
c                r    ddl m} t        | j                  j	                  d      xs |j
                        S )Nr   )settingsgrounding_search_model)app.core.configr  r   r   r%   openai_model)r   r  s     r   rQ  z#GroundingResearchStep._search_modelL  s4    ,4&&**+CD ,++- 	-ru   c                z   ddl }ddlm} ddlm} | j                         }t         || j                        |d   |d   |d   t        |d   j                               D ci c]  \  }}||j                  d	d
       c}}d|j                  |j                         dd      | j                  d      S c c}}w )u  ★팩 바이트·계약 버전에 더해 **요청 계약**까지 접는다.

        안 접으면 `strict` 나 `include` 를 고쳐도 resume 이 **옛 체크포인트를
        그대로 건너뛴다** — 고친 것이 아무 데도 안 닿는다.
        r   Nr   r,  rF   rG   pack_manifest_hashrS   rL   r   )rF   rG   manifestrS   TFrh   )r   r   requestr@  )rm   r   r   r   re   rj  rt   r   r'   r,   r%   ro   request_contractrR  )r   rs   r   rT  r   kvs          r   r   z"GroundingResearchStep._config_hashR  s     	<G zz|'(;(;<x.T)_ !56 '-T']-@-@-B&CE&Cda QUU#5r::&CE	 {{3#7#7#9T05 # 7//
  	Es   "B7r   r   F)r   r   rE  r   rD  r|   r-  r   r:  r2  r3  r4  r   c                  |xs g D cg c]  }|j                  d      s| }}|xs g D cg c]&  }|j                  d      r|j                  d      s|( }}t        |xs d      }t        |	xs d      }t        |xs g       t        |      z
  t        |      z
  t        |xs g       t        |      z   t        |      z   t        |      t        |      z   t        |      z   t        |      z   t        t        |xs d            z   |i d| d|d|d|dt        j                  d	|xs g d
|xs i d|d|dt        |D ch c]  }t        |j                  d             c}      dt        |      d|d|d|
dt        |xs d      dt        |xs d      dt        |xs i       dS c c}w c c}w c c}w )u9  ★체크포인트 모양을 다른 스텝과 같게. 하류가 읽는 자리는 `data` 다.

        ★**상한에 걸린 것을 실패로 센다.** 세지 않으면 「시간이 없어 못 산 것」이
        `completed` 로 지나가고, 하류가 그걸 「조사할 게 없었다」로 읽는다.
        rC  errorr   rz   r|   r   r   r@  r   rE  r   rD  limit_kindscapped_countr-  unbuiltsaved_revisionsr2  r3  r/  r   )r%   r   r(   r'  rR  r'   r   rK  )r   r   r   r   rE  r   rD  r|   r-  r   r:  r2  r3  r4  r   r1   r1  errorednsr8   s                       r   rH  z$GroundingResearchStep._wrap_researchn  s6    #jbjAjQUU<-@!jA#zrz BzeeGnQUU<-@ z B)/r"'-R "4:2V<s7|K #DJB#b' 9CG C ![3w<7#b'ACGK"4B#789& )7s$f 3>> 
	
 &,B    v06'8061 (+155+>'?06'8  9 F R 2 "5" \%7R 8#$ DR0%( (hn")=)
 	
 BB,'8s   F9F9+F>	!Gr   r   )r   r   r   r   )r   r   r   r   r   r   r   )r   r   r   r   r   r   r   r   rD  r   r|   r#  r:  r   r   r   r   r   )rR   r   r   r   rL  rN  rR  r   rJ  rj  rb  rc  r8  rI  rQ  r   r%  rH  r   ru   r   r'  r'     s    4  " $* JZ@x((  ?C 
B4DN27h-8 8:""+,e!%t!$(4$*,/
36/
%(/
7;/
 =@/

 %(/

 2@/
 /
ru   r'  )r.   z	List[Any]r   r   r   z	List[str])rF   r   rG   r   r   zDict[str, str])rr   r   r   r   )"r   
__future__r   loggingtypingr   r   r   app.core.grounding_moder   r   r	   r   r
   r   r   r   r   r   app.core.steps.entity_stepsr   	getLoggerrR   r   r   r=   build_carry_indexmatch_candidaterX   rO   rt   rw   r   r'  r   ru   r   <module>r     s   . #  " "  , : N 8			8	$
  :>-.9'*93<9| ,, (( P DO =4;&
 ;|R(* Rh^
,j ^
ru   