o
    y	i2{                     @   sx   d dl Z d dlZd dlmZmZmZ d dlmZ d dlm	Z	 G dd dZ
G dd dZG d	d
 d
ZG dd dZdS )    N)OptionalDictAny)Config)Logc                   @   s   e Zd ZdZededefddZedeee	f defddZ
edeee	f defd	d
Zedeee	f dee fddZedeee	f dee fddZdS )OpenAIEventHandleraU  
    Interprets and processes events received from the OpenAI Realtime API.
    
    - Determines which events should be logged.
    - Identifies and extracts audio deltas, speech start events, and item IDs from event payloads.
    
    Used by higher-level services to make sense of incoming OpenAI events and route them appropriately.
    
event_typereturnc                 C   s
   | t jv S )z(Check if an event type should be logged.)r   ZLOG_EVENT_TYPES)r    r
   services/openai_service.pyshould_log_event      
z#OpenAIEventHandler.should_log_eventeventc                 C   s   |  ddko
d| v S )z-Check if event is an audio delta from OpenAI.typezresponse.output_audio.deltadeltagetr   r
   r
   r   is_audio_delta_event   s   z'OpenAIEventHandler.is_audio_delta_eventc                 C   s   |  ddkS )z1Check if event indicates user speech has started.r   z!input_audio_buffer.speech_startedr   r   r
   r
   r   is_speech_started_event   s   z*OpenAIEventHandler.is_speech_started_eventc                 C   s   t | r
| dS dS )z&Extract audio delta from OpenAI event.r   N)r   r   r   r   r
   r
   r   extract_audio_delta"   s   

z&OpenAIEventHandler.extract_audio_deltac                 C   s
   |  dS )z"Extract item ID from OpenAI event.item_idr   r   r
   r
   r   extract_item_id)   r   z"OpenAIEventHandler.extract_item_idN)__name__
__module____qualname____doc__staticmethodstrboolr   r   r   r   r   r   r   r   r
   r
   r
   r   r      s    	 $r   c                   @   sv   e Zd ZdZeddededededeeef f
dd	Zedd
edeeef fddZ	edeeef fddZ
dS )OpenAISessionManageraY  
    Configures and initializes OpenAI Realtime API sessions.
    
    - Generates session update messages specifying model, audio formats, and system instructions.
    - Creates the initial conversation item (for AI-first greetings) and triggers responses.
    
    Ensures consistent and correct session setup for all OpenAI interactions.
    Nbusiness_idknowledge_contextcustom_system_messageconversation_contextr	   c                 C   s   |r|}n
t jjt jt jd}|r| r|d|  d| d7 }|r(|d| d7 }ddt jd	gd
did
diddd
diid|ddddddddig dddddddddddddddgddgd d!}|S )"a  
        Create a session update message for OpenAI Realtime API.
        
        Args:
            business_id: Business ID for context
            knowledge_context: Additional context from knowledge base
            custom_system_message: Custom system message for bot configuration
            conversation_context: Conversation context to prevent repetition
        
        Returns:
            Dictionary containing session configuration
        )COMPANY_NAMEAGENT_PHONE_NUMBERz\

IMPORTANT: Use the following business-specific information when answering questions about z:

z

When customers ask about services, features, or company information, prioritize this context over general knowledge. Be specific and detailed when discussing the company's offerings.z

CONVERSATION CONTEXT:
zb

Use this context to avoid repeating the same responses and to provide varied, helpful responses.zsession.updateZrealtimeaudior   z
audio/pcmuZ
server_vad)formatZturn_detectionr(   )inputoutputfunctionend_callz]Politely end the phone call when the caller says goodbye or requests to end the conversation.objectreasonstringz-Brief reason for ending, e.g., user said bye.)r   description)r   Z
propertiesrequired)r   namer0   Z
parameterstransfer_to_agentz[Transfer the call to a human agent when the caller requests to speak with a human or agent.z/Reason for transfer, e.g., user requested agentz+Phone number to transfer to in E.164 format)r.   agent_phoner4   )r   ZmodelZoutput_modalitiesr'   instructionsZtools)r   session)r   ZSYSTEM_MESSAGEr(   r%   r&   ZOPENAI_MODEL)r!   r"   r#   r$   Zsystem_messager6   r
   r
   r   create_session_update9   sX   	
-z*OpenAISessionManager.create_session_updatecompany_namec                 C   s8   | r| dkrd|  d}nd}dddd|d	gd
dS )a  
        Create an initial conversation item for AI-first interactions.
        
        Args:
            company_name: Dynamic company name for personalized greeting
        
        Returns:
            Dictionary containing initial conversation setup
        zMcube Telecom pvt Limitedz<Greet the user with 'Hello! I'm your AI voice assistant for z. How can I help you today?'zSGreet the user with 'Hello! I'm your AI voice assistant. How can I help you today?'conversation.item.createmessageuser
input_textr   textr   rolecontentr   itemr
   )r8   Zgreeting_textr
   r
   r    create_initial_conversation_item   s   z5OpenAISessionManager.create_initial_conversation_itemc                   C   s   ddiS )z
        Create a response trigger message.
        
        Returns:
            Dictionary to trigger OpenAI response generation
        r   response.creater
   r
   r
   r
   r   create_response_trigger   s   z,OpenAISessionManager.create_response_trigger)NNNNN)r   r   r   r   r   r   r   r   r7   rD   rF   r
   r
   r
   r   r    /   s    	*Mr    c                	   @   sr   e Zd ZdZedededeeef fddZ	ede
e ded	e
e defd
dZeded	edefddZdS )OpenAIConversationManagera  
    Manages conversation flow and interruption logic for OpenAI sessions.
    
    - Creates truncation events to interrupt/cut off ongoing AI responses.
    - Determines when interruptions should be processed based on marks and timing.
    - Calculates elapsed time for precise truncation.
    
    Used by the main service to support real-time, interactive voice experiences.
    r   audio_end_msr	   c                 C   s   d| d|dS )a  
        Create a conversation item truncation event.
        
        Args:
            item_id: ID of the item to truncate
            audio_end_ms: Timestamp where to truncate the audio
            
        Returns:
            Dictionary containing truncation command
        zconversation.item.truncater   )r   r   Zcontent_indexrI   r
   )r   rI   r
   r
   r   create_truncate_event   s
   z/OpenAIConversationManager.create_truncate_eventlast_assistant_item
mark_queueresponse_start_timestampc                 C   s   | duot |dko|duS )e  
        Determine if an interruption should be processed.
        
        Args:
            last_assistant_item: ID of the last assistant response
            mark_queue: Queue of pending marks
            response_start_timestamp: When the current response started
            
        Returns:
            True if interruption should be handled
        Nr   )len)rK   rL   rM   r
   r
   r   should_handle_interruption   s
   
z4OpenAIConversationManager.should_handle_interruptioncurrent_timestampc                 C   s   | | S )a  
        Calculate the elapsed time for audio truncation.
        
        Args:
            current_timestamp: Current media timestamp
            response_start_timestamp: When the response started
            
        Returns:
            Elapsed time in milliseconds
        r
   )rQ   rM   r
   r
   r   calculate_truncation_time   s   z3OpenAIConversationManager.calculate_truncation_timeN)r   r   r   r   r   r   intr   r   rJ   r   listr   rP   rR   r
   r
   r
   r   rH      s,    
 rH   c                	   @   s  e Zd ZdZdd Zd>dedededdfd	d
Zd?deddfddZdeee	f ddfddZ
deee	f defddZdeee	f deeee	f  fddZdeee	f defddZdeee	f defddZdeee	f defddZdeddfddZdeddfd d!Zd"eddfd#d$Zdeee	f defd%d&Zd@d'd(Zdefd)d*Zd+ee ddfd,d-Zd@d.d/Zd@d0d1Zdeee	f deeee	f  fd2d3Zdeee	f defd4d5Zd6ed7ed8eddfd9d:Zd8ee d;ed7ee defd<d=Z dS )AOpenAIServicea  
    Main service layer for all OpenAI Realtime API operations in the application.
    
    - Composes the event handler, session manager, and conversation manager.
    - Provides high-level methods to initialize sessions, send greetings, process/log events, extract audio, and handle interruptions.
    
    This is the primary interface for the rest of the application to interact with OpenAI, abstracting away lower-level event and session management details.
    c                 C   s:   t  | _t | _t | _i | _d| _d| _d | _	d | _
d S )NF)r    session_managerrH   conversation_managerr   event_handler_pending_tool_calls_pending_goodbye_goodbye_audio_heard_goodbye_item_id_goodbye_watchdogselfr
   r
   r   __init__   s   
zOpenAIService.__init__Nr!   r"   r#   r	   c                    s2   | j |||}td| ||I dH  dS )a`  
        Initialize OpenAI session with proper configuration.
        
        Args:
            connection_manager: WebSocket connection manager
            business_id: Business ID for context
            knowledge_context: Additional context from knowledge base
            custom_system_message: Custom system message for bot configuration
        zSending session updateN)rV   r7   r   jsonsend_to_openai)r_   connection_managerr!   r"   r#   Zsession_updater
   r
   r   initialize_session	  s   
z OpenAIService.initialize_sessionr8   c                    s\   t d| d ddddddgd	d
}||I dH  | j }||I dH  dS )z
        Trigger AI to greet using the system message template.
        
        Args:
            connection_manager: WebSocket connection manager
            company_name: Dynamic company name for personalized greeting
        u;   🤖 AI will greet using system message template (Company: )r9   r:   r;   r<   zStart the conversationr=   r?   rB   N)r   inforb   rV   rF   )r_   rc   r8   Zgreeting_itemZresponse_triggerr
   r
   r   send_initial_greeting  s   
z#OpenAIService.send_initial_greetingr   c                 C   s2   | j |ddrtd|d  | dS dS )zy
        Process OpenAI event for logging if needed.
        
        Args:
            event: OpenAI event data
        r    zReceived event: N)rX   r   r   r   r   r_   r   r
   r
   r   process_event_for_logging5  s   z'OpenAIService.process_event_for_loggingc                 C   sf   | d}|dv rdS |dkr1| dpi }| dpg }|D ]}t|tr0| ddkr0 dS qdS )	z7Return True if the event is a tool call from the model.r   )&response.function_call.arguments.delta response.function_call.completedTresponse.doneresponser*   function_callF)r   
isinstancedict)r_   r   etyperespr*   rC   r
   r
   r   is_tool_call?  s   
zOpenAIService.is_tool_callc              	   C   s  | d}|dkr2| dp| dpd}| dd}| j|d| dd	}|d
  |7  < dS |dkr{| dpA| dpAd}| j|d}|du rOdS z|d
 r[t|d
 ni }W n tym   d|d
 i}Y nw | dpw| d|dS |dkr| dpi }| dpg }	|	D ]=}
t|
tr|
 ddkr|
 d}|
 d}zt|t	rt|n|pi }W n ty   d|i}Y nw ||d  S qdS )z
        Accumulate streamed tool call arguments until completion.
        Returns the completed call payload when finished.
        r   rk   call_ididdefaultr   rh   r2   )argsr2   rx   Nrl   Z_raw)r2   	argumentsrm   rn   r*   ro   ry   )
r   rY   
setdefaultpopra   loads	Exceptionrp   rq   r   )r_   r   rr   ru   r   bufZpayloadrx   rs   r*   rC   r2   Zraw_argsr
   r
   r   accumulate_tool_callM  sB   


 z"OpenAIService.accumulate_tool_call	tool_callc                    sL   |sdS | d}|dkr| ||I dH S |dkr$| ||I dH S dS )z
        Handle supported tool calls. Returns True if a tool was handled.
        Currently supports: end_call, transfer_to_agent
        Fr2   r,   Nr3   )r   _handle_end_call_tool_handle_transfer_tool)r_   rc   r   r2   r
   r
   r   maybe_handle_tool_calls  s   
z$OpenAIService.maybe_handle_tool_callc                    s   | dpi }t|tr| dnd}t|}| jr#td dS td | ||I dH  d| _d| _	d| _
| | dS )zHandle end_call tool call.ry   r.   Nz4End-call already pending; ignoring duplicate requestFz(Queueing farewell response before hangupT)r   rp   rq   r   Zbuild_end_call_farewellrZ   r   rf   _send_goodbye_responser[   r\   _start_goodbye_watchdog)r_   rc   r   rx   r.   Zfarewellr
   r
   r   r     s   



z#OpenAIService._handle_end_call_toolc           
   
      s  ze| dpi }t|tr| dnd}t|tr| dnd}|s.tj}td|  ddlm} |j	|j
j|d	d
}||I dH  td| d| d | ||I dH  | ||I dH  W d	S  ty } ztd|  ddl}	|	  W Y d}~dS d}~ww )z#Handle transfer_to_agent tool call.ry   r4   Nr.   zUser requested agentz%Using configured agent phone number: r   )McubeServiceT)Zshow_original_caller_idu    🔄 Call transferred to agent: z
 (Reason: re   zError handling transfer: F)r   rp   rq   r   r&   r   rf   Zservices.mcube_servicer   Zcreate_transfer_messagestate	stream_idZsend_to_mcube&_send_conversation_summary_on_transfer _update_database_transfer_statusr}   error	traceback	print_exc)
r_   rc   r   rx   Ztransfer_tor.   r   Ztransfer_messageer   r
   r
   r   r     s4   z#OpenAIService._handle_transfer_toolr.   c              
      s   z[ddl m} ddlm} d}|j D ]\}}t|dr+|jjj	|jj	kr+|} nq|s6t
d W dS ||}||jpAd|jpEd|I dH }	|	rUt
d W dS t
d	 W dS  ty }
 zt
d
|
  ddl}|  W Y d}
~
dS d}
~
ww )z<Send conversation summary when call is transferred to agent.r   )conversation_summary_servicecall_managerNrc   z0No active session found for conversation summaryZUnknownu7   ✅ Conversation summary sent successfully for transferu7   ⚠️ Failed to send conversation summary for transferz0Error sending conversation summary on transfer: )Z%services.conversation_summary_servicer   services.call_managerr   active_sessionsitemshasattrrc   r   r   r   warningZget_session_summary_dataZsend_conversation_summaryr!   ru   rf   r}   r   r   r   )r_   rc   r.   r   r   current_session
session_idr6   Zsession_datasuccessr   r   r
   r
   r   r     s8   


z4OpenAIService._send_conversation_summary_on_transfertransfer_numberc              
      s  zddl m} d}|j D ]\}}t|dr%|jjj|jjkr%|} nq|s0t	d W dS t
jr|jr|jrzL|j4 I dH 2 |j|j|j|}|r`td|j  td|  n	t	d|j  W d  I dH  n1 I dH syw   Y  W W dS W W dS  ty } ztd	|  W Y d}~W dS d}~ww td
 W dS  ty }	 ztd|	  ddl}
|
  W Y d}	~	dS d}	~	ww )zGUpdate database with transfer status when call is transferred to agent.r   r   Nrc   z2No active session found for transfer status updateu>   ✅ Database transfer status updated successfully for session u   📊 Transfer number: u=   ⚠️ Failed to update database transfer status for session z)Database error updating transfer status: zFDatabase update skipped - database disabled or session not initializedz)Error updating database transfer status: )r   r   r   r   r   rc   r   r   r   r   r   ZDATABASE_ENABLEDZdb_session_idr!   Z_db_update_lockZdatabase_serviceZupdate_transfer_statusrf   r   r}   r   r   r   )r_   rc   r   r   r   r   r6   r   Zdb_errorr   r   r
   r
   r   r     sJ   
8 z.OpenAIService._update_database_transfer_statusr>   c              
      sf   z| dd|idI dH  W dS  ty2 } ztd|  d| _d| _W Y d}~dS d}~ww )zSend a final assistant response (audio) with the provided text before hangup.
        Uses response.create with inline instructions so the model speaks immediately without tool calls.
        rE   r5   )r   rn   Nz"Failed to queue goodbye response: TF)rb   r}   r   r   rZ   r[   )r_   rc   r>   r   r
   r
   r   r     s   z$OpenAIService._send_goodbye_responsec                 C   s   | j r| jsdS |d}|dkrdS |dkr{| jsZ|dp i }|dp'g D ]/}t|trW|ddkrW|d	d
krW|dpCg D ]}t|trV|ddkrV  dS qDq(dS |dp`i }|dpgg D ]}t|trz|d| jkrz dS qhdS )zOReturn True if we should finalize hangup after the goodbye audio has completed.Fr   zresponse.output_audio.doneTrm   rn   r*   r:   r@   Z	assistantrA   Zoutput_audiorv   )rZ   r[   r   r\   rp   rq   )r_   r   rr   rs   rC   cr
   r
   r   should_finalize_on_event+  s*   
&z&OpenAIService.should_finalize_on_eventc              
      s$  d| _ d| _d| _|   ztdttdd d t	ttddI dH  W n	 t
y2   Y nw t r{z)ddlm} |tjtj}t|jd	d}|r_td
d|i ||jdd W n t
yz } ztd|  W Y d}~nd}~ww z|jddI dH  W dS  t
y   Y dS w )zWAfter goodbye audio is finished, clear/close and optionally complete the call via REST.FNzGrace sleep before hangup: ZEND_CALL_GRACE_SECONDSg      ?sr   )Clientcall_sidzCompleting call via Twilio RESTZcallSidZ	completed)statusz$Optional Twilio REST hangup failed: zassistant completed)r.   )rZ   r[   r\   _cancel_goodbye_watchdogr   rf   getattrr   asynciosleepr}   Zhas_twilio_credentialsZtwilio.restr   ZTWILIO_ACCOUNT_SIDZTWILIO_AUTH_TOKENr   r   Zcallsupdater   Zclose_twilio_connection)r_   rc   r   Zclientr   r   r
   r
   r   finalize_goodbyeE  s:   zOpenAIService.finalize_goodbyec                 C   s   | j S )zFReturn True if a farewell has been queued and we await its completion.)rZ   r^   r
   r
   r   is_goodbye_pendinga  s   z OpenAIService.is_goodbye_pendingr   c                 C   s,   | j rd| _|r| js|| _|   dS dS )zVMark that we've begun receiving audio for the goodbye message and capture its item_id.TN)rZ   r[   r\   r   )r_   r   r
   r
   r   mark_goodbye_audio_hearde  s   
z&OpenAIService.mark_goodbye_audio_heardc                    sT      zttdd fdd}t| _W dS  ty)   d_Y dS w )zLStart a watchdog that finalizes the call if no goodbye audio starts in time.ZEND_CALL_WATCHDOG_SECONDS   c                      s`   z$t I d H  jr js#td  I d H  W d S W d S W d S  ty/   Y d S w )Nz3Goodbye audio not detected in time; finalizing call)r   r   rZ   r[   r   rf   r   r}   r
   rc   r_   Ztimeoutr
   r   _watcht  s   
z5OpenAIService._start_goodbye_watchdog.<locals>._watchN)r   r   r   r   Zcreate_taskr]   r}   )r_   rc   r   r
   r   r   r   n  s   	z%OpenAIService._start_goodbye_watchdogc                 C   s$   | j r| j  s| j   d | _ d S rG   )r]   ZdoneZcancelr^   r
   r
   r   r     s   

z&OpenAIService._cancel_goodbye_watchdogc                 C   s*   | j |sdS | j || j |dS )z
        Extract relevant data from OpenAI audio response.
        
        Args:
            event: OpenAI event data
            
        Returns:
            Dictionary with audio delta and item ID, or None
        N)r   r   )rX   r   r   r   ri   r
   r
   r   extract_audio_response_data  s
   


z)OpenAIService.extract_audio_response_datac                 C   s   | j |S )z
        Check if event indicates user speech started (interruption).
        
        Args:
            event: OpenAI event data
            
        Returns:
            True if speech started
        )rX   r   ri   r
   r
   r   is_speech_started  s   
zOpenAIService.is_speech_startedrQ   rM   rK   c                    sj   | j ||}tjr$td| d| d| d td| d| d | j ||}||I dH  dS )a]  
        Handle conversation interruption by truncating the current response.
        
        Args:
            connection_manager: WebSocket connection manager
            current_timestamp: Current media timestamp
            response_start_timestamp: When the response started
            last_assistant_item: ID of the item to truncate
        z)Calculating elapsed time for truncation: z - z = mszTruncating item with ID: z, Truncated at: N)rW   rR   r   ZSHOW_TIMING_MATHprintrJ   rb   )r_   rc   rQ   rM   rK   Zelapsed_timeZtruncate_eventr
   r
   r   handle_interruption  s   z!OpenAIService.handle_interruptionrL   c                 C   s   | j |||S )rN   )rW   rP   )r_   rK   rL   rM   r
   r
   r   should_process_interruption  s   z)OpenAIService.should_process_interruption)NNNrG   )r	   N)!r   r   r   r   r`   r   rd   rg   r   r   rj   r   rt   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   rS   r   rT   r   r
   r
   r
   r   rU      sP    	

&&*'+

	
&
rU   )ra   r   typingr   r   r   Zconfigr   Zservices.log_utilsr   r   r    rH   rU   r
   r
   r
   r   <module>   s    ' E