
     j%                       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m	Z	m
Z
mZ  ej                  e      Z ej                  dej                         ZdZ G d de      Zdd	Zd
 ZddZddZddZddd	 	 	 	 	 	 	 	 	 	 	 	 	 ddZy)u  Qwen VLM 판정 클라이언트 — DashScope OpenAI 호환 직접 호출 (2026-08-10).

`backend/qwen_vlm_pilot.py` 에서 검증된 부품의 프로덕션 이관이다. Router
(`call_structured`)를 타지 않는 이유: 이 저장소의 구조화 호출은
`response_format={"type":"json_schema","strict":true}` 를 보내는데 DashScope 는
그것을 지원하지 않는다 — `json_object` 만 되고, 메시지 어딘가에 "json" 이라는
낱말이 없으면 400, thinking 모드는 구조화 출력과 충돌한다. 그래서 스키마
전문을 시스템에 동봉하고 여기서 `jsonschema` 로 검증한 뒤, 실패하면 오류를
되먹여 한 번 다시 묻는다.

★litellm 우회 직접 호출이므로 **모든 API 왕복을 `record_provider_call` 로
남긴다** (llm_call_log+Opik — 2026-08-07 전수 기록 지시). 우회 호출 AST
스캐너(`test_all_provider_calls_are_traced`)가 이 배선을 검사한다.
    )annotationsN)AnyDictListOptionalz```(?:json)?\s*(.*?)\s*```u   

OUTPUT FORMAT — return one single json object and nothing else. No prose
before or after it, no markdown code fence. It must match this json schema
exactly, including every required key:

{schema}c                      e Zd Zy)QwenNotConfiguredN)__name__
__module____qualname__     U/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/llm/qwen_vlm_client.pyr	   r	   &   s    r   r	   c                 \    ddl m}  t        | j                  xs dj	                               S Nr   settings )app.core.configr   booldashscope_api_keystripr   s    r   qwen_configuredr   *   s$    (++1r88:;;r   c                     ddl m}  | j                  xs dj                         }|st	        d      ddlm}  ||| j                  xs dj                         d      S )Nr   r   r   uJ   DASHSCOPE_API_KEY 가 비어 있다 — backend/.env 에 키를 넣어라.)OpenAIg     r@)api_keybase_urltimeout)r   r   r   r   r	   openair   dashscope_base_url)r   keyr   s      r   _clientr"   0   s`    (%%+
2
2
4CX
 	
 --3::< r   c                 J    ddl m}  | j                  xs dj                         S r   )r   r   qwen_vlm_modelr   r   s    r   
qwen_modelr%   A   s    (##)r0022r   c                h   | xs dj                         } 	 t        j                  |       S # t        j                  $ r Y nw xY wt        j                  |       }|r>	 t        j                  |j                  d            S # t        j                  $ r Y nw xY w| j                  d      }|dk\  rtd\  }}}t        | |d |      D ]\  \  }}|r|rd}n|dk(  rd	}n|d
k(  rd}|d
k(  rd	}%|dk(  r|dz  }0|dk(  s6|dz  }|dk(  sAt        j                  | ||dz          c S  t        d| dd       )ud  추론 텍스트가 앞뒤에 섞여 와도 본체 json 을 꺼낸다.

    thinking 모드가 content 에 추론을 흘리는 사례가 보고돼 있어 관용 파싱이
    필요하다. ①코드펜스 안 ②첫 `{` 부터 균형 잡힌 마지막 `}` 까지 순으로
    시도. (pilot 검증 부품 — 256샷 실측에서 전 샷 파싱 성공)
    r      {r   )r   FFNF\T"}u   json 을 찾지 못했다:    )
r   jsonloadsJSONDecodeError_FENCEsearchgroupfind	enumerate
ValueError)textmstartdepthin_strescichs           r   extract_jsonr>   G   sP    JBDzz$ dA	::aggaj))## 		IIcNEz,vstEF|U3EArC4ZC3Y"FSys
s
A:::d5Q&788! 4" 24:.A
BBs!   + A A#B   BBc                2    dj                  d | D              S )ud   기록용 프롬프트 — 텍스트 파트만 잇는다 (base64 이미지는 DB 에 안 싣는다).
c              3     K   | ]7  }|j                  d       dk(  st        |j                  d      xs d       9 yw)typer6   r   N)getstr).0ps     r   	<genexpr>z"_prompt_text_of.<locals>.<genexpr>r   s5      */Q155=F3JAEE&MR %s   ?#?)join)partss    r   _prompt_text_ofrJ   p   s     99 */  r   r'   )	max_retryopik_metadatac          
         ddl m}m} ddlm}mm}	 t               t               c}
|t        j                  t        j                  |dd            z   }d|d	d
|d	g}t         |             |xs i j                         D ]  \  }}|	|vs||<     |	       t        |      ddd	 	 	 d) fd}d}t!        |dz         D ]  }d}ddidfD ]J  }t#        j"                         }	  |
j$                  j&                  j(                  d*|ddid|rd|ini } n |t1        d      t-        t#        j"                         z
  dz        }	 |j2                  d   j4                  j6                  xs d}t9        |t:              s!t=        dt?        |      j@                         	 	 tC        |      } |||        |d||        |c S  t1        d(      # t*        $ r`}t-        t#        j"                         |z
  dz        }|dt/        |      vr |d|t/        |      dd          |d|d       Y d}~kd}~ww xY w# t*        $ r} |d|d|dd         d}~ww xY w# tD        |t        jF                  f$ rP}t?        |      j@                   d!| dd } |d|d"|        ||k\  r |d#|dd$ d	d
d%| d&d	gz  }Y d}~d}~wt*        $ r} |d|d'|dd         d}~ww xY w)+u  Qwen 에 멀티모달 판정을 묻고 스키마 검증을 통과한 payload 를 돌려준다.

    parts 는 `call_structured` 멀티모달 parts 와 같은 형식(`png_part`/
    `ref_parts` 산출 — OpenAI chat `image_url` data URI) — 실측 갤러리에서
    같은 parts 를 Gemini·Qwen 두 경로에 태워 호환 확인됨.

    스키마 검증 실패 = 오류를 되먹여 교정 재질의 `max_retry` 회. 네트워크
    재시도는 여기 없다 — 필요가 실측되면 그때 더한다(선제 확장 금지).
    r   )ValidationErrorvalidate)ambient_call_metarecord_provider_callresolve_step_nameF   )ensure_asciiindent)schemasystem)rolecontentuserN)outputerrorr\   c               *     | |d	||
       y )N	dashscope)
stepmodelpromptstatusduration_msmetaprovider	operationoutput_textr\   r   )
rb   dur_msr[   r\   
log_promptrd   r`   rQ   r_   step_tags
       r   _recordz$ask_qwen_structured.<locals>._record   s#    U:fTKF%	Ar   r   r'   enable_thinkingrB   json_object)r`   messagesresponse_format
extra_bodyi  400i  )r\   u3   enable_thinking 미지원(400) — 빼고 재시도u:   qwen 호출이 응답 없이 폴백 루프를 벗어났다u'   message.content 가 str 이 아니다: zunexpected response shape: i  success)r[   z: zschema violation: 	assistanti  z,That reply did not satisfy the json schema: z'
Return the corrected json object only.z"unexpected post-response failure: unreachable)
rb   rD   rh   intr[   Optional[str]r\   rv   returnNoner   )$
jsonschemarN   rO   app.modules.llm.image_tracerrP   rQ   rR   r"   r%   JSON_CLAUSEformatr-   dumpsdictitemsrJ   rangetimechatcompletionscreate	Exceptionru   reprRuntimeErrorchoicesmessagerY   
isinstancerD   	TypeErrorrB   r
   r>   r5   r/   )rj   rW   rI   rV   rK   rL   rN   _js_validaterP   rR   client
sys_promptrn   kvrk   last_errattemptrespextrat0excdurrY   payloadri   rd   r`   rQ   r_   s   `                        @@@@@r   ask_qwen_structuredr   w   s   $ ED D Iz|MFE+,,zz&uQ? - A AJ j1E*&H !#$D$"++-1=Qd]DG . Xt,D 'JEI'+A$A04A A HQ' )%0$7EBU5v{{..55 %%+]$; 16e,2	  8" <[\\499;#t+,		ll1o--55;Ggs+=G}--.01 1 ,	"7+G&)Is73NW (H }
%%m  U499;+t34=Ec$:GSS	$3@SU UU*  	GS7w?EG	 OT-A-AB 	s),,-Ru5ds;HGS*<XJ(GH)# $$@Bj HJ H  		 GS>sgFtLN		sW   :2G) AI: I8)	I2AII	I5I00I58L AKL )K;;L )rw   r   )rw   rD   )r6   rD   rw   Dict[str, Any])rI   List[Dict[str, Any]]rw   rD   )rj   rD   rW   rD   rI   r   rV   r   rK   ru   rL   zOptional[Dict[str, Any]]rw   r   )__doc__
__future__r   r-   loggingrer   typingr   r   r   r   	getLoggerr
   loggercompileSr0   r{   r   r	   r   r"   r%   r>   rJ   r   r   r   r   <module>r      s    #   	  , ,			8	$	1244	8	 	<"3&CR .2q&q&q&  q& 	q& q& ,q& q&r   