
    uj%                         d Z ddlZddl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 ddlmZ ddlmZ ddlmZmZmZ ddlmZ  ej.                  e      ZdZ G d	 d
      Zy)uM   Gemini 텍스트 분석 클라이언트 — 멀티턴 대화 기반 REST API.    N)AnyDictListOptional)settings)get_next_key)ambient_call_metarecord_provider_callresolve_step_name)log_llm_callz]https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}c                   D   e Zd ZdZ	 	 ddedz  dedz  ddfdZddZdefdZdd	Zde	e
eef      fd
Z	 ddedededee   ddf
dZ	 	 	 	 ddedee
eef      dedee   dede
eef   ez  fdZ	 	 ddede
eef   dedee   de
eef   f
dZede
eef   defd       Zy)GeminiTextClientuX   Gemini REST API 클라이언트 — 멀티턴 대화 + 구조화 출력 + 자동 로깅.Napi_keymodelreturnc                 `    || _         |xs t        j                  | _        g | _        i | _        y N)_fixed_api_keyr   gemini_text_model_model_history_ctx)selfr   r   s      X/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/llm/gemini_text_client.py__init__zGeminiTextClient.__init__   s,    
 &9x99.0$&	    c                 <    | j                   j                  |       | S )uN   로깅 컨텍스트 설정: project_id, episode_id, operation_type, step_name.)r   update)r   kwargss     r   set_contextzGeminiTextClient.set_context&   s    		 r   c                 F    | j                   r| j                   S t               S r   )r   r   r   s    r   _get_api_keyzGeminiTextClient._get_api_key+   s    &&&~r   c                     g | _         y r   )r   r"   s    r   reset_historyzGeminiTextClient.reset_history0   s	    r   c                 ,    t        | j                        S r   )listr   r"   s    r   get_historyzGeminiTextClient.get_history3   s    DMM""r   statususer_messageduration_mserrorc           
         t        t                     }dD ]*  }| j                  j                  |      }|!||vs&|||<   , t	        t        | j                  j                  d      |      | j                  |||||d       y)u  Opik 에만 보탠다 — DB 는 이 자리의 ``log_llm_call`` 이 이미 남긴다.

        ★이 클라이언트는 Gemini REST 직접 호출이라 **litellm 콜백이 안 닿는다**
        (2026-08-07). DB 에만 남고 Opik 에는 한 줄도 없었다.
        )
project_id
episode_id	step_nameNr0   F)stepr   promptr)   r+   metar,   to_db)dictr	   r   getr
   r   r   )r   r)   r*   r+   r,   r3   keyvalues           r   _trace_opikzGeminiTextClient._trace_opik6   sy     %'(<CIIMM#&E S_!S	 = 	"499==#=tD++l6#$e5	Jr   response_schemaschema_namesystem_instructiontemperaturec                    | j                   j                  dd|igd       | j                   |t        j                  dd}|r
dd|igi|d<   |rd|d	   d
<   ||d	   d<   | j	                         }t
        j                  | j                  |      }t        j                  j                  |t        j                  |      j                  d      ddid      }	t        j                  }
t        j                  }d}t!        j"                         }t%        d|dz         D ]`  }	 t        j                  j'                  |	|
      5 }t        j(                  |j+                         j-                  d            }ddd        n t;        t!        j"                         |z
  dz        }t=        d)| j                  ||dt?        |      |d| j@                   | jC                  d||t?        |             t3        d |       t;        t!        j"                         |z
  dz        }| jK                        }| j                   j                  d!d|igd       |jM                  d"i       }|jM                  d#      }|jM                  d$      }t=        d)| j                  ||||||d%d&| j@                   | jC                  d%||       |r	 t        j(                  |      S |S # 1 sw Y   ixY w# t        j.                  j0                  $ rx}|j+                         j-                  dd      }t3        d|j4                   d|       }|j4                  dv r||k  r|j4                  dk(  r| j6                  sx| j	                         }t
        j                  | j                  |      }t        j                  j                  |t        j                  |      j                  d      ddid      }	t!        j8                  d|z         Y d}~t;        t!        j"                         |z
  dz        }t=        d)| j                  ||dt?        |      |d| j@                   | jC                  d||t?        |             ||d}~wt        j.                  jD                  tF        jH                  f$ r}|}||k  rt!        j8                  d|z         Y d}~t;        t!        j"                         |z
  dz        }t=        d)| j                  ||dt?        |      |d| j@                   | jC                  d||t?        |             t3        d|       |d}~ww xY w# t        jN                  $ r}t3        d'|dd(        |d}~ww xY w)*u:   멀티턴 대화에서 한 턴을 보내고 응답 받기.usertext)roleparts)r=   maxOutputTokens)contentsgenerationConfigrB   systemInstructionzapplication/jsonrE   responseMimeTyperesponseJsonSchema)r   r   zutf-8zContent-TypePOST)dataheadersmethodN      )timeoutreplace)errorszGemini text API error z: >             rU   i  r,   )
model_nameuser_promptsystem_promptr)   error_messager+   )r,   z&Gemini text API failed after retries: zGemini text API failed: r   usageMetadatapromptTokenCountcandidatesTokenCountsuccess)rW   rY   rX   output_textr+   input_tokensoutput_tokensr)   z-Gemini structured output was not valid JSON:
rV    )(r   appendr   llm_max_output_tokensr#   GEMINI_API_URL_TEMPLATEformatr   urllibrequestRequestjsondumpsencodellm_timeout_textllm_max_retriestime	monotonicrangeurlopenloadsreaddecoder,   	HTTPErrorRuntimeErrorcoder   sleepintr   strr   r9   URLErrorsocketrO   _extract_textr6   JSONDecodeError)r   r*   r:   r;   r<   r=   bodyr   urlreqrO   max_retries
last_errorstart_msattemptresponsepayloadexc
error_textdurationr@   usager`   ra   s                           r   sendzGeminiTextClient.sendG   sa    	|,-
 	 *#+#A#A! 
 6#567)D$% ;MD#$%78=LD#$%9:##%%,,4;;,Pnn$$D!((1#%78	 % 
 ++..'+
>>#Qa0G.^^++C+AX"jj)?)?)HIG B	 1b DNN,x74?@H ;;L0!*o8 ))	 WlH#&z?  4!9*FGG(83t;<!!'*tn%
 	 OR0yy!34		"89 	 
	
{{,$ %'
	
 ii
	
 	L(;zz$'' { BA <<)) * XXZ..wy.I
),SXXJbE
 8888W=Sxx3t/B/B"&"3"3"55<<4;;X_<`$nn44djj&6&=&=g&F%35G$HQW 5  JJq7{+ 08 ;tCD #{{"4W"%j/x ii	   ,'*: ! 8 c)LL))6>>:  
k)JJq7{+ 08 ;tCD #{{"4W"%j/x ii	   ,'*: ! 8"<SEBv '' "DT$3ZLQsc   !!K)3K5K)U K&	!K))UC:Q:A3Q::0U*UA?UUV )U;;V c                 ~    | j                  ||||      }t        |t              st        dt	        |             |S )u)   구조화 출력 전용 편의 메서드.)r*   r:   r;   r<   zExpected dict, got )r   
isinstancer5   rw   type)r   r*   r:   r;   r<   results         r   send_structuredz GeminiTextClient.send_structured   sK     %+#1	  
 &$'!4T&\NCDDr   r   c                    | j                  dg       }|sW| j                  di       }|j                  d      }|rt        d|       t        dt        j                  |       d d        |d   j                  di       }|j                  d	g       }|D cg c]  }d
|v s|j                  d
d       }}|s$t        dt        j                  |       d d        dj	                  |      S c c}w )N
candidatespromptFeedbackblockReasonzGemini blocked: zGemini returned no candidates: rV   r   contentrB   r@    z#Gemini response has no text parts: )r6   rw   rj   rk   join)r   r   feedbackblock_reasonr   rB   ptextss           r   r~   zGeminiTextClient._extract_text   s    [[r2
{{#3R8H#<<6L"%5l^#DEE!@GATUYVYAZ@[\]]Q-##Ir2GR(,1AEqVq[vr"EA!DTZZPWEXY]Z]E^D_`aawwu~	 Bs   	C.!C.)NN)r   r   )r   Nr   )Nr   Ng?)r   N)__name__
__module____qualname____doc__r{   r   r    r#   r%   r   r   r   r(   rz   r   r9   floatr   r   staticmethodr~   rb   r   r   r   r      s   b # 	't	' Tz	' 
		'
c 
#T$sCx.1 # ,0J# JS Js J#C=J48J( 59%,0 LL "$sCx.1L 	L
 %SML L 
c3h#	Ld &,0 c3h 	
 %SM 
c3h$ tCH~ #  r   r   )r   rj   loggingr}   ro   urllib.errorrg   urllib.requesttypingr   r   r   r   app.core.configr   app.modules.llm.gemini_key_poolr   app.modules.llm.image_tracerr	   r
   r   app.modules.llm.llm_loggerr   	getLoggerr   loggerre   r   rb   r   r   <module>r      s^    S       , , $ 8@ @ 3			8	$ d 
_ _r   