o
    zSoj]                     @  s   U d Z ddlmZ ddlZddlZddlZddlmZmZm	Z	m
Z
mZmZ eeZdZdZeh dZdZd	Zd
ddiddidg dddddgdddidddgiddidddgidg dddZded< G dd de	ZdS )uL   LLM vía OpenAI Responses API (cloud) o Chat Completions (Ollama / proxies).    )annotationsN)ChatMessageConversationContextLanguageModelLlmFatalErrorLlmQuotaExceededLlmReplyu   Sos un asistente telefónico en español rioplatense. Respondé de forma breve y clara, apto para voz (1-3 oraciones). No uses markdown ni listas largas.)idnombreapellidopolizaproductoplan_actualprecio_actualplan_ofrecidoprecio_ofrecidoplan_retencionmonto_aseguradomonedacreado>   quota_exceededinsufficient_quotarate_limit_exceededbilling_hard_limit_reachedresultado_llamadau5  Extrae el resultado de la conversación de retención telefónica. Usá los datos de póliza conocidos cuando estén disponibles; no inventes id ni número de póliza. Clasificá motivo como uno de: Economicos, Desconocimiento, Fallecimiento, Otro. resultado es "retencion" si el cliente aceptó cambio de plan o descuento; "baja" si se confirmó la baja del seguro. plan_anterior es el plan que tenía al inicio. plan_nuevo es el plan aceptado o null. descuento_aceptado es true solo si aceptó la bonificación; descuento_aplicado es "20" en ese caso, si no null.objecttypestring)
EconomicosDesconocimientoFallecimientoOtro)r   enumbaja	retencionnullboolean)r	   r   motivo	resultadoplan_anterior
plan_nuevodescuento_aceptadodescuento_aplicadoF)r   
propertiesrequiredadditionalPropertiesdictRESULTADO_LLAMADA_SCHEMAc                   @  s`  e Zd ZU dZdZded< dZded< dd	d
ddddddddddddfd$d%Zdgd&d'Zdhd(d)Z	did,d-Z
edjd/d0Zedid1d2Zedkd5d6Zedld8d9Zdmd<d=Zednd@dAZddBdodDdEZdpdHdIZddBdodJdKZdddLdqdOdPZdddLdrdQdRZdsdTdUZdsdVdWZddBdodXdYZddZdtd^d_Zdud`daZdudbdcZdgdddeZdS )vOpenAiLanguageModelopenaiFbool_quota_blocked str_quota_block_reasonzgpt-4.1-minigffffff?i  g      ?g        https://api.openai.com/v1g      N@N)modeltemperature
max_tokenstop_pfrequency_penaltypresence_penaltyassistant_idsystem_promptbase_urltimeoutprovider_nameuse_assistantsuse_responsesapi_keyr:   r;   floatr<   intr=   r>   r?   r@   rA   rB   rC   rD   rE   bool | NonerF   returnNonec                C  sp  |pd   | _|
pdd| _|pd  }|s%| jdkr!d}ntd|| _|| _|| _|| _	|| _
|| _|| _|	p=t  | _|d urKt|| _n| jdkoTd| jv | _|ra|  ratd |rhtd	 d| _i | _t | _d
| j dd}| jrtd| j| j| j ntd| j| j| j tj| j||d| _d| jv o| j | _| jdkrdt_dt_d S d S )Nr3   r9   /r6   ollamaz6OPENAI_API_KEY es obligatorio para LLM_PROVIDER=openaizapi.openai.comu   OPENAI_ASSISTANT_ID está definido pero se ignora: Assistants API deprecada; se usa Responses API. La personalidad va en BOT_SYSTEM_PROMPT / bot_system_prompt.txt.zFuse_assistants=True ignorado: Assistants API reemplazada por ResponseszBearer zapplication/json)AuthorizationzContent-Typez/LLM provider=%s modo=responses model=%s base=%sz6LLM provider=%s modo=chat.completions model=%s base=%s)rB   headersrC   F)striplowerrD   rstriprB   
ValueErrorrG   r:   r;   r<   r=   r>   r?   DEFAULT_SYSTEMrA   r4   _use_responsesloggerwarningr@   _response_idsset_poliza_seededinfohttpxAsyncClient_client_send_penaltiesr2   r5   r8   )selfrG   r:   r;   r<   r=   r>   r?   r@   rA   rB   rC   rD   rE   rF   keyrP    rc   8/var/www/html/bot-asterisk-ari/backend/llm/openai_llm.py__init__g   sv   




zOpenAiLanguageModel.__init__c              
     s   | j dkrdS z%| jjd| jdddgddd	d
I dH }|  td| j| j W dS  tyH } zt	d| j| W Y d}~dS d}~ww )zHPing corto a chat.completions para cargar el modelo en memoria (Ollama).rN   N/chat/completionsuserokrolecontent   r   )r:   messagesr<   r;   jsonz.LLM warmup OK provider=ollama model=%s base=%su.   LLM warmup falló provider=ollama model=%s: %s)
rD   r_   postr:   raise_for_statusrW   r\   rB   	ExceptionrX   )ra   responseexcrc   rc   rd   warmup   s4   

	
zOpenAiLanguageModel.warmupc                 C  s   | j S )z?Solo comportamiento permanente (Responses: campo instructions).)rA   ra   rc   rc   rd   _build_instructions   s   z'OpenAiLanguageModel._build_instructionscontextConversationContext | Nonec                 C  s4   | j g}| |}|r|| ddd |D S )uI   System message para Chat Completions (Ollama): prompt + datos dinámicos.
c                 s  s    | ]}|r|V  qd S Nrc   ).0prc   rc   rd   	<genexpr>   s    z4OpenAiLanguageModel._build_system.<locals>.<genexpr>)rA   _context_input_textappendjoin)ra   rx   partscontext_blockrc   rc   rd   _build_system   s
   

z!OpenAiLanguageModel._build_systemdict | Nonec                 C  sV   | r| j sd S | j }i }tD ]}|dkr!|dp| jpd||< q||||< q|S )Nr	   r6   )r   _POLIZA_INPUT_KEYSgetdocument_id)rx   datapayloadrb   rc   rc   rd   _poliza_payload   s   
z#OpenAiLanguageModel._poliza_payloadc                 C  st   g }|  |}|r|dtj|ddd  n|r&|jr&|d|j d |r5|jr5|d|j d d|S )	uQ   Texto de contexto dinámico (póliza / documento / llamante) para input o system.uH   Datos del cliente (fuente CRM; usá estos valores y no inventes otros):
F   ensure_asciiindentz,El documento/DNI asociado a esta llamada es .u   Número del llamante: rz   )r   r   ro   dumpsr   caller_numberr   )clsrx   r   r   rc   rc   rd   r      s   



z'OpenAiLanguageModel._context_input_textrm   list[ChatMessage]c                 C  s4   t | D ]}|jdkr|j r|j   S qdS )Nrg   r6   )reversedrj   rk   rQ   )rm   msgrc   rc   rd   _latest_user_text  s
   z%OpenAiLanguageModel._latest_user_text
list[dict]c                 C  sP   g }| D ]!}|j dkrq|jpd }|sq|j dvrq||j |d q|S )Nsystemr6   )rg   	assistant	developerri   )rj   rk   rQ   r   )rm   itemsr   rk   rc   rc   rd   _history_as_input  s   

z%OpenAiLanguageModel._history_as_inputcall_idinput_itemsc                 C  sH   || j v r|S | |}|s|S | j | dd| dd}|g|S )uI   Inyecta el JSON de póliza una sola vez al inicio de la cadena Responses.rg   z<Contexto interno de la llamada (no es el cliente hablando):
u+   
Cuando el cliente hable, usá estos datos.ri   )r[   r   add)ra   r   rx   r   blockseedrc   rc   rd   _seed_context_input  s   


z'OpenAiLanguageModel._seed_context_inputr   r0   c                 C  s   |  d}t|tr| r| S g }|  dpg D ]7}t|ts#q| ddkr+q| dp1g D ]}t|ts:q2| ddv rQ| dpGd}|rQ|t| q2qd	| S )
Noutput_textoutputr   messagerk   )r   textr   r6   rz   )r   
isinstancer7   rQ   r0   r   r   )r   directr   itemr   r   rc   rc   rd   _extract_output_text3  s&   


z(OpenAiLanguageModel._extract_output_textrx   r   c             
     s~   t jrtt jp	dz| jr| j||dI d H W S | j||dI d H W S  ty,     tj	y> } z| 
|  d }~ww )Nz5OpenAI bloqueado por cuota agotada (circuito abierto)r   )r2   r5   r   r8   rV   _reply_responses_reply_chatr   r]   HTTPStatusError_raise_if_quota_http)ra   rm   rx   rt   rc   rc   rd   replyG  s"   
zOpenAiLanguageModel.replyrt   httpx.HTTPStatusErrorc                 C  s   d}|j jd d }z |j  }|dpi }t|dpd}t|dp'|}W n	 ty3   Y nw | d|  }|j jdv sO|tv sOd|v sOd	|v rdd
t	_
|t	_td| t||p`dd|d S )Nr6   ,  errorcoder    )i  i  zexceeded your current quotar   Tu?   OpenAI CUOTA AGOTADA — circuito abierto, no más requests: %sr   r   )rs   r   ro   r   r7   rr   rR   status_code_QUOTA_CODESr2   r5   r8   rW   r   r   )ra   rt   r   r   r   errloweredrc   rc   rd   r   \  s.   
z(OpenAiLanguageModel._raise_if_quota_httpc             	     sR  |r|j ndp	d}| j|}|r!| |}|stddS |}n| |}|s-tddS | |||}| j|  || j	| j
| jdd}|rJ||d< | d|I dH }	|	d	}
|
r|
d
vr|	dpdi }td|
 d|d d|d t|dp|
pdd|	d}t|tr|r|| j|< td|||pd | |	}t||	dS )uK   Responses API: instructions fijas + input dinámico + previous_response_id.r6   _default)r   T)r:   instructionsinputr;   max_output_tokensr=   storeprevious_response_id
/responsesNstatus	completedr   zOpenAI Responses status= code=r    msg=r   response_failedr   r	   z4OpenAI response chain call=%s response_id=%s prev=%s-r   raw)r   rY   r   r   r   r   r   r:   rw   r;   r<   r=   
_post_jsonr   r7   r   rW   debugr   )ra   rm   rx   r   previous_id	user_textinput_payloadr   bodyr   r   r   response_idr   rc   rc   rd   r   u  sZ   



	



z$OpenAiLanguageModel._reply_responsesrx   
transcriptr   list[str] | Nonec          
      C  s   g }|  |}|r|dtj|ddd  n|r%|jr%|d|j  |r2|dd|  n6g }|D ]%}|jdkr>q6|jpBd	 }|sHq6|jd
krOdnd}	||	 d|  q6|rh|dd|  |sldS |d d|S )Nu   Datos de póliza conocidos:
Fr   r   zDocumento/DNI: u   Transcripción de la llamada:
rz   r   r6   rg   clienteagentez: uf   No hay transcripción. Extraé el mejor resultado posible con campos vacíos o null según el esquema.u2   Extraé el resultado estructurado de esta llamada.z

)	r   r   ro   r   r   r   rj   rk   rQ   )
ra   rm   rx   r   r   r   linesr   rk   speakerrc   rc   rd   _build_extract_input  s:   




z(OpenAiLanguageModel._build_extract_inputc          	   
     sJ  t jrtd dS |s|sdS | j|||d}z.| jr+| |I dH }| |}n| |I dH }|	di gd 	di 	dd
 }W n3 tyO     tjyl } z| | td	 W Y d}~dS d}~w tyz   td
 Y dS w |sdS zt|}W n tjy   td|dd  Y dS w t|tsdS |S )z;Fin de llamada: Responses con text.format.type=json_schema.z4extract_call_result omitido: circuito OpenAI abiertoNr   choicesr   r   rk   r6   zextract_call_result HTTP erroru   extract_call_result fallóu'   extract_call_result: JSON inválido: %sr   )r2   r5   rW   rX   r   rV   _extract_responsesr   _extract_chatr   rQ   r   r]   r   r   	exceptionrr   ro   loadsJSONDecodeErrorr   r0   )	ra   rm   rx   r   
input_textr   r   rt   parsedrc   rc   rd   extract_call_result  s\   



z'OpenAiLanguageModel.extract_call_resultr   c                   s   | j t|dtd| jdddtdtdid}| d|I d	H }|d
}|rP|dvrP|dp1i }td| d|d d|d t	|dpL|pLdd|S )zAPOST /responses con text.format json_schema (structured outputs).r     Tformatjson_schema)r   namestrictschema)r:   r   r   r;   r   r   r   r   Nr   r   r   zOpenAI extract status=r   r   r   r   r   r   )
r:   RESULTADO_LLAMADA_INSTRUCTIONSminr<   RESULTADO_LLAMADA_NAMEr1   r   r   r   r7   )ra   r   r   r   r   r   rc   rc   rd   r   
  s8   

z&OpenAiLanguageModel._extract_responsesc              	     s   | j dtdd|dgdtd| jdtdtdd	d
}| jjd|dI dH }|jdkr>ddi|d< | jjd|dI dH }|jdkrS| 	t
jd|j |j|d |  | S )zLChat Completions (Ollama): pide JSON; schema estricto si el proxy lo acepta.r   ri   rg   r   r   r   T)r   r   r   )r   r   )r:   rm   r;   r<   response_formatrf   rn   N  r   json_objectr   HTTP requestrs   )r:   r   r   r<   r   r1   r_   rp   r   r   r]   r   r   rq   ro   )ra   r   r   rs   rc   rc   rd   r   (  s8   



z!OpenAiLanguageModel._extract_chatc          	        s   d|  |dg}|D ]}|jdkrq||j|jd q| j|| j| j| jd}| jr8| j	|d< | j
|d< | jjd|dI dH }|jd	krX| tjd
|j |j|d |  | }|di gd di dd }t||dS )z6Chat Completions: Ollama y proxies compatibles OpenAI.r   ri   )r:   rm   r;   r<   r=   r>   r?   rf   rn   Nr   r   r   r   r   r   rk   r6   r   )r   rj   r   rk   r:   r;   r<   r=   r`   r>   r?   r_   rp   r   r   r]   r   r   rq   ro   r   rQ   r   )	ra   rm   rx   payload_messagesr   r   rs   r   r   rc   rc   rd   r   K  sB   




zOpenAiLanguageModel._reply_chat)paramspathr   r   c                  s   | j j|||dI d H }|jdkr;td||j|jd d  z| tjd|j |j	|d W n t
y:    w |  | S )N)ro   r   r   u   OpenAI POST %s → %s %sr   r   r   )r_   rp   r   rW   r   r   r   r]   r   r   r   rq   ro   )ra   r   r   r   rs   rc   rc   rd   r   w  s,   

zOpenAiLanguageModel._post_jsonc                 C  s   | j |d | j| dS )zELimpia la cadena previous_response_id de una llamada (nombre legacy).NrY   popr[   discardra   r   rc   rc   rd   drop_thread  s   zOpenAiLanguageModel.drop_threadc                 C  s   | j |d  | j| d S r{   r   r   rc   rc   rd   drop_response_chain  s   z'OpenAiLanguageModel.drop_response_chainc                   s*   | j   | j  | j I d H  d S r{   )rY   clearr[   r_   acloserv   rc   rc   rd   close  s   

zOpenAiLanguageModel.close)rG   r7   r:   r7   r;   rH   r<   rI   r=   rH   r>   rH   r?   rH   r@   r7   rA   r7   rB   r7   rC   rH   rD   r7   rE   rJ   rF   rJ   rK   rL   )rK   rL   )rK   r7   )rx   ry   rK   r7   )rx   ry   rK   r   )rm   r   rK   r7   )rm   r   rK   r   )r   r7   rx   ry   r   r   rK   r   )r   r0   rK   r7   )rm   r   rx   ry   rK   r   )rt   r   rK   rL   )rm   r   rx   ry   r   r   rK   r7   )rm   r   rx   ry   r   r   rK   r   )r   r7   rK   r0   )r   r7   r   r0   r   r   rK   r0   )r   r7   rK   rL   )__name__
__module____qualname__rD   r5   __annotations__r8   re   ru   rw   r   staticmethodr   classmethodr   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   rc   rc   rc   rd   r2   a   sh   
 
Z



<+
6
'-

r2   )__doc__
__future__r   ro   loggingr]   llm.protocolsr   r   r   r   r   r   	getLoggerr   rW   rU   r   	frozensetr   r   r   r1   r  r2   rc   rc   rc   rd   <module>   sD     
	



 