
    կj2&                        U d Z ddlmZ ddlmZmZ ddlmZmZm	Z	m
Z
mZ dZded<   e G d d	             Ze G d
 d             Zedddddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZy)u  두 VLM 을 각각 한 번씩 부르고 **합의는 안 하는** 실행부 (2026-08-27, #92).

사용자 지시:

    gpt sol vlm 은 성능이 안좋아 … **무조건 gemini 3.1 pro 와 grok 최신
    모델 둘을 사용해야해** / 아홉 전부 바꿔

## 이 모듈이 하는 것과 안 하는 것

    한다      두 alias 를 각각 **한 번씩** 부른다
              fallback 을 봉인한다
              모델별 raw·물리 모델·오류·토큰·비용을 **같은 모양으로** 남긴다

    안 한다   합의 · 기각 · 승자 선정 · 재시도
              한 모델 실패를 다른 모델 두 번째 호출로 메우기

★**합의를 여기 넣으면 안 된다** (2026-08-27 Codex 판정). 업무마다 실패의
 뜻이 다르다:

    binary defect gate  둘 다 broken 일 때만 기각, 불일치는 split
    ranking·selection   모델별 점수를 보존해 호출자 규칙으로 합산
    관찰·critique       모델별 관찰을 기록하고 기존 issue 계약으로 결합
    geometry·추출       구조화 ID 합의 — **좌표 평균 금지**

 예가 코드에 있다. `ref_image_pipeline` 의 두 호출자는 같은 운반층을 쓰는데
 하나(`validate_reference_image`)는 severe 면 **돈 내고 재생성**하고 다른
 하나(`compare_two_images`)는 이미 산 두 장 중 승자를 고른다. 같은 합의로
 묶으면 실패의 뜻이 섞인다.

## fallback 봉인이 왜 계약인가

`call_structured` 는 기본이 `enable_fallback=True` 라 Gemini/Grok 이 실패하면
**GPT Sol 이 다시 들어온다.** 그러면 사용자 결정을 조용히 어기고 기록에도
Sol 이 남지 않는다. 여기서 못박는다.
    )annotations)	dataclassfield)AnyDictListOptionalSequence)z
gemini-progroktupleDUAL_VLM_ALIASESc                  p    e Zd ZU dZded<   ded<   dZded<   dZd	ed
<    ee      Z	ded<   e
dd       Zy)OneCalluS   한 모델의 한 번 — 성공이든 실패든 **같은 모양**으로 남는다.straliasboolokNzOptional[Dict[str, Any]]payloadzOptional[str]error)default_factoryDict[str, Any]usagec                R    t        | j                  j                  d      xs d      S )u   응답이 말한 물리 모델 — 설정이 아니라.

        못 읽었으면 빈 문자열이다. **alias 로 대신 채우지 않는다** — 그러면
        「무엇이 답했는지」와 「무엇을 부르려 했는지」가 섞인다.
        physical_model )r   r   getselfs    N/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/llm/dual_vlm.pyr   zOneCall.physical_model7   s"     4::>>"239r::    )returnr   )__name__
__module____qualname____doc____annotations__r   r   r   dictr   propertyr    r    r   r   r   -   sE    ]JH(,G%,E=!$7E>7; ;r    r   c                  P    e Zd ZU dZded<   ed	d       Zed
d       ZddZddZ	y)
DualResultuN   두 호출의 결과. **판단은 안 들어 있다** — 호출자가 한다.zList[OneCall]callsc                h    t        | j                        xr t        d | j                  D              S )uM  둘 다 답했나.

        ★거짓이면 **호출자는 기각 권한이 없다.** 한쪽만 보고 돈을 더 쓰거나
         산출을 버리면, 실패한 모델이 무슨 말을 했을지 모르는 채 결정하는
         것이다. 이 값이 그 계약을 자료로 만든다 — 판정은 호출자 몫이다.
        c              3  4   K   | ]  }|j                     y wN)r   ).0cs     r   	<genexpr>z&DualResult.complete.<locals>.<genexpr>O   s     'Ajjs   )r   r,   allr   s    r   completezDualResult.completeG   s'     DJJAC'Adjj'A$AAr    c                    | j                   D cg c](  }|j                  s|j                  |j                  * c}S c c}w )u1   성공한 것만, **요청한 순서 그대로**.)r,   r   r   r   r1   s     r   payloadszDualResult.payloadsQ   s3     $(::P:a!)):O		:PPPs   ???c                L    | j                   D ]  }|j                  |k(  s|c S  y r/   )r,   r   )r   r   r1   s      r   by_aliaszDualResult.by_aliasV   s&    Aww%  r    c                D   d| j                   D cg c]  }|j                   c}| j                  | j                   D cg c]P  }|j                  |j                  |j                  |j
                  t        |j                        |j                  dR c}dS c c}w c c}w )uP   기록에 그대로 넣을 모양 — 모델별 raw 와 비용을 다 담는다.dual_vlm_v1)r   r   r   r   r   raw)contractaliasesr4   r,   )	r,   r   r4   r   r   r   r'   r   r   r6   s     r   
provenancezDualResult.provenance\   s     &)-4A4  $A WW&'&6&6$$WW "!'']99	 $	
 	
4s   BAB
N)r!   r   )r!   zList[Dict[str, Any]])r   r   r!   zOptional[OneCall])r!   r   )
r"   r#   r$   r%   r&   r(   r4   r7   r9   r?   r)   r    r   r+   r+   A   s?    XB B Q Q
r    r+   responseNg?F)r>   schema_nameopik_metadatatemperature
max_tokensparallelc          
         ddl m d f	d}
|	rt        |      dk  r t        |D cg c]
  } |
|       c}      S ddlm} ddlm} ddlm	} dd	l
m} dd
lm}  | | | ||
                        } |t        |            5 }|D ci c]  }||j                  ||       }}ddd       t        |D cg c]  }|   j                          c}      S c c}w c c}w # 1 sw Y   >xY wc c}w )uT  두 alias 를 **각각 한 번씩** 부른다.

    한 쪽이 죽어도 다른 쪽은 부른다 — 둘의 말을 다 듣는 것이 이 판의
    목적이고, 한쪽 실패를 이유로 나머지를 안 물으면 `complete=False` 인지
    「둘 다 나쁘다」인지 구분이 안 된다.

    ★**재시도하지 않는다.** 실패한 모델을 다시 부르면 그 모델이 두 번
     들어와 「독립 두 의견」이라는 전제가 깨지고 돈도 두 번 나간다.

    Args:
        aliases: 부를 순서. 기본은 사용자 지시가 정한 짝.
        step: 각 호출의 스텝 태그 — alias 별로 접미사를 붙여 기록에서
            갈린다(`<step>__gemini-pro`).
        parallel: 두 alias 를 **동시에** 부른다 (2026-08-29 사용자 지시).

            ★기본은 False 다 — **호출부가 켠다**(Codex 설계 리뷰). 이 함수는
             cine 온전성 검증 말고도 참조 검증·비교가 쓰므로 전역 기본을
             바꾸면 손대지 않은 자리의 동시성까지 달라진다.

            ★결과는 **완료 순서가 아니라 `aliases` 순서**로 담는다. 실패도
             같다 — 기록이 순서에 흔들리면 「어느 모델이 무엇을 말했나」가
             호출마다 달라진다.

            ★worker 에 ContextVar 셋을 **명시 전파**한다(예산·capture·trace).
             롤 생성 병렬(`multiroll_select.py:1491-1502`)과 같은 처리이고,
             그 주석이 「하나라도 빠뜨리면 그 축만 조용히 무너진다」고 적고
             있다.

            ★이것은 **산출 계약이 아니다** — 입력·모델·결과 순서가 같으므로
             지문을 움직이지 않는다.

    Returns:
        [[DualResult]] — 합의 없음. `complete` 와 모델별 raw 만 있다.
    r   )call_structuredc                   	 
 d|  }i }	  ||d| ii	dd|      }t        | d||      S # t        $ r4}t        | dt        |      j                   d| d d	 |
      cY d }~S d }~ww xY w)N__modelFr   )project_configrA   rB   rC   rD   enable_fallbacknum_retries
usage_sinkT)r   r   r   r   z: i  )r   r   r   r   )r   	Exceptiontyper"   )r   tagsinkr   excrG   rD   rB   response_schemarA   stepsystem_promptrC   user_prompts        r   _onezask_both.<locals>._one   s    b !	%]K #gu%56'+'% !& )G, 4MM 	c++,Bse4Tc: 	s   &3 	A0)A+%A0+A0   )r,   )ThreadPoolExecutor)bind_current_budget)bind_current_ledger)bind_current_trace)bind_current_generation_context)max_workersN)r   r   r!   r   )app.modules.llm.llm_clientrG   lenr+   concurrent.futuresrZ   app.core.image_call_budgetr[   app.core.send_ledgerr\   app.modules.llm.opik_tracer]   "app.services.image_capture.contextr^   submitresult)rU   rV   rW   rT   r>   rA   rB   rC   rD   rE   rX   arZ   r[   r\   r]   r^   boundpoolfuturesrG   s   ```` ````           @r   ask_bothrm   r   s    ^ ; B s7|a'' :'Qa' :;;5>8=  +,>t,DE	GHE 
G	55<=W1dkk%++W= 
6 'B'QWQZ..0'BCC- !;( > 
6	5 Cs)   C,C6C17C6
D1C66C?)rU   r   rV   r   rW   z'str | list'rT   r   r>   zSequence[str]rA   r   rB   zOptional[Dict]rC   floatrD   zOptional[int]rE   r   r!   r+   )r%   
__future__r   dataclassesr   r   typingr   r   r   r	   r
   r   r&   r   r+   rm   r)   r    r   <module>rr      s   "F # ( 6 6 1 % 0 ; ; ;& -
 -
 -
l .!$( $iD
iDiD iD $	iD iD iD "iD iD iD iD iDr    