o
    i/                    @   s,  U 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Zd dlmZ d dlm	Z	 d dl
m
Z
 d dlmZmZmZmZ d dlmZ d dlZd dlZd dlmZmZmZmZmZ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'm(Z( d dl)m*Z* d dl+m,Z, d dl-m.Z. d dl/m0Z0 d dl1m2Z2 d dl3m4Z4 d dl5m6Z6 d dl7m8Z8m9Z9m:Z:m;Z; d dl<m=Z= ej>ej?dd e@eAZBdeeCef fddZDeD ZEe*eEZ)e.eEZ-e=eEZ<e0e-ZFe2eEZGe4eEZ3i ZHeeCeeCef f eId< dZJG dd de'ZKG dd de'ZLG d d! d!e'ZMG d"d# d#e'ZNG d$d% d%e'ZOG d&d' d'e'ZPd(eeC deCfd)d*ZQd+eeCef d,eCdeRfd-d.ZSedd/fd(eeC deeCef fd0d1ZTd,eCd+eeCef deeCef fd2d3ZUdd5eeC d6eRdeRfd7d8ZVd+eeCef d,eCdeRfd9d:ZWd,eCd+eeCef ddfd;d<ZXdeeCef fd=d>ZYd?edeeC fd@dAZZdBedeeeCef  fdCdDZ[d5edefdEdFZ\dGeeCef dHeeCef deeCef fdIdJZ]dKdL Z^dMdN Z_d,eCfdOdPZ`d,eCdeeeCef  fdQdRZad,eCdSeCdeeeCef  fdTdUZbd,eCdSeCdeCfdVdWZcdXeddeeC fdYdZZedSeCfd[d\Zfd,eCd]eeC d^egdeeCef fd_d`Zhd,eCdaeCdeeCef fdbdcZiddeeCef deeCef fdedfZjd,eCdSeCdeeCef fdgdhZkddeeCef d,eCdSeCdeeeCef  fdidjZld,eCdkeCdlegdeeCef fdmdnZmdd,eCdpegdeeCef fdqdrZnd,eCdaeCdSeCdeeCef fdsdtZodBedeeeCef  fdudvZpdwedeeC fdxdyZqdzeeeCef  d{eCdee fd|d}ZrdzeeeCef  deeC fd~dZsdBed?edeeeCef  fddZtddzed?edee deRfddZuddzeeeCef  deeC deeC fddZvdzeeCef deeCef fddZwdeRfddZxd,eCde6fddZy			4d	d,eCdeeC degdeRdeeCef f
ddZzedddZ{e{j|edgddgdgd d d dZ}e_  e{~ddefddZe{ddd Ze{dedddeeTfd,eCd+eeCef fddZe{deeTfd,eCd+eeCef fddZe{deeTfd,eCd+eeCef fddZe{deeTfd,eCdBeKd+eeCef fddZe{deeTfd,eCdBeLd+eeCef fddZe{deeTfd,eCdBeMd+eeCef fddZe{deddddeeTfd,eCdeCd^egd+eeCef fddZe{dedd/eddddeeTfd,eCd]eeC d^egd+eeCef fddZe{deeTfd,eCd]eCd+eeCef fddÄZe{dġeeTfd,eCdBeNd+eeCef fddƄZe{dǡedddeddddeeTfd,eCdkeCdlegd+eeCef fdd˄Ze{d̡edodddeeTfd,eCdpegd+eeCef fddτZe{dСeeTfd,eCdSeCdBeOd+eeCef fdd҄Ze{dӡeddded4d/eeTfd,eCdSeCdedeCdeRd+eeCef fddׄZe{dӡeeTfd,eCdSeCd+eeCef fddلZe{dڡedodddeeTfd,eCdSeCd^egd+eeCef fdd܄Ze{dݡeeTfd,eCdBePd+eeCef fdd߄Ze{dedd/eeTfd,eCdaeCdSeCd+eeCef fddZe{ddBeeCef fddZe{ddBeeCef defddZe{deeTedd/fd+eeCef d(eeC fddZe{deeTfd+eeCef fddZe{deeTfd,eCd+eeCef fddZe{deeTfdBeeCef ded+eeCef fddZe{ddd Ze{dd,eCfddZe{ded4d/edd/ed4d/fd,eCdeRdegdeRfdd Ze{dedd/ed4d/edd/ed4d/fd,eCdeeC deRdegdeRf
ddZe{dedd/fd,eCdeeC fddZe{dedd/edd/edd/edd/ed d/fd,eCdeeC deeC d	eeC d^egd
egfddZe{dd,eCdeCfddZe{dedod/fd,eCd^egfddZe{dedd/fd,eCdeeC fddZe{dededd/fd,eCdeCdeeC fddZe{dedd/edd/edd/edd/edd/ed d/fd,eCdeeg deeC deeC deeC d^egd
egfd d!Ze{d"d,eCdeCfd#d$Ze{d%d,eCdeCfd&d'Ze{d(d,eCdeCfd)d*Ze{d+d,eCdeCd,egdBeeCef fd-d.Ze{d/edod/fd,eCd^egfd0d1Ze{d2dBeeCef fd3d4Ze{d5edd/edd/ed d/ed4d/edd/fd,eCdeeC d^egd
egd6eRdeeC fd7d8Ze{d9d,eCd:eCfd;d<Ze{d=d,eCd:eCfd>d?Ze{d@edfd,eCd:eCdAeeCef fdBdCZe{dDedfd,eCd:eCdAeeCef fdEdFZe{dGd,eCd:eCfdHdIZe{dGedfd,eCd:eCdAeeCef fdJdKZe{dLd,eCdMegfdNdOZe{dPd,eCd:eCfdQdRZe{dPedfd,eCd:eCdAeeCef fdSdTZe{dUedfd,eCdVegdAeeCef fdWdXZe{dUd,eCdVegfdYdZZe{d[edd/edd/fd,eCd:eCdeeC d\eRfd]d^Ze{d_edd/edd/fd,eCdeeC deeC fd`daZe{dbedd/edd/fd,eCdeeC deeC fdcddZe{deedd/edd/fd,eCdeeC deeC fdfdgZe{dhedid/edjd/fd,eCdkeCdlegfdldmZe{dnedd/edd/fd,eCdeeC deeC fdodpZe{dqedd/edd/edd/fd,eCdeeC deeC deeC fdrdsZe{dtedud/edd/edd/fd,eCd^egdeeC deeC fdvdwZe{dxd,eCdeCfdydzZe{d{d,eCfd|d}Ze{d~d,eCfddZe{dd,eCfddZe{dedd/edd/edd/edd/fd,eCdeCdeeg deeC deeC f
ddZe{ddBeeCef fddZe{ddBeeCef fddZe{dd,eCfddZe{dd,eCdBeeCef fddZe{deeTfd,eCd+eeCef fddZe{dedeeTfd,eCdAeeCef d+eeCef fddZe{dedfd,eCdefddZe{dd,eCdegfddZe{dd,eCdegfddZe{dd,eCfddZe{dd,eCfddZe{dedd/edd/fd,eCdeeC deeC fddZe{dd,eCdegfddZe{dd,eCdBeeCef fddZe{dd,eCdegdBeeCef fddZe{dd,eCdegfddZe{dd,eCdegfddZe{dedfd,eCdeCfddZe{dd,eCdeCfddZe{dád,eCdBeeCef fdĐdńZe{dơd,eCfdǐdȄZe{dɡedd/edd/ed4d/eeTfd,eCdeeC degdeRd+eeCef f
dʐd˄Ze{d̡eeTfd,eCd+eeCef fd͐d΄Ze{d̡eeTfd,eCdBeeCef d+eeCef fdϐdЄZe{dѡeeTfd,eCd+eeCef fdҐdӄZe{d̡eeTfd,eCd+eeCef fdԐdՄZe{d֡eeTfd,eCdBeeCef d+eeCef fdאd؄Ze{d١eeTfd,eCdeCd+eeCef fdېd܄Ze{dݡeeTfd,eCdBeeCef d+eeCef fdސd߄Ze{d١eeTfd,eCdeCdBeeCef d+eeCef fddZe{d١eeTfd,eCdeCd+eeCef fddZe{dd,eCdeCfddZe{deeTfd+eeCef fddZe{deeTfdBeeCef ded+eeCef fddZe{ddd Ze{deeTfded+eeCef fddZe{deeTfd]egdBeeCef d+eeCef fddZe{deeTfded+eeCef fddZe{deeTfd,eCded+eeCef fddZe{deeTfd,eCd+eeCef fddZe{deeTfd,eCd+eeCef fd dZe{dedd/edd/edd/eeTfd^egd]eeg deeC d+eeCef fddZe{ddBeeCef defddZe{d	edfd
eCfddZe{deeTfdBeeCef ded+eeCef fddZe{dedd/eeTfd,eeC d+eeCef fddZe{deeTfdegded+eeCef fddZegeddZdZd,eCdeCfddZdeCdeCdeRfddZd eeCef deeCef fd!d"Zd
d,eCdeCdeeC deeC d^egf
d$d%Zdd,eCdeCdeeC deeC degf
d&d'Ze{d(edd/ed4d/eeTfd,eCd^egd)eRd+eeCef fd*d+Ze{d,edd/edd/ed#d/ed4d/eeTfd,eCdeeC deeC d^egd)eRd+eeCef fd-d.Z e{d/edd/edd/eeTfd,eCdeeC deeC d+eeCef fd0d1Ze{d2eeTfd,eCdBeeCef d+eeCef fd3d4Zd5d6d7d8d9d:d;ZeeCef eId<< 			#		dd=eeCef d>eCdeCdeeC deeC d^egd?eeeC  d@eeeC  fdAdBZe{dCeeTfd,eCd+eeCef fdDdEZe{dCeeTfd,eCdBeeCef d+eeCef fdFdGZe{dHeeTfd,eCdIeCd+eeCef fdJdKZe{dLedd/edd/edddMeddNdOeddPdOeeTfd,eCdeeC deeC d^egdeeC dQeeC d+eeCef fdRdSZe{dTeeTfd,eCdBeeCef d+eeCef fdUdVZ	e{dWeeTfd,eCded+eeCef fdXdYZ
e{dZed[d/edd/edd/edd/edd/fd,eCdeCdeeC d\eeC d]eeC d^eeC fd_d`Ze{dad,eCdBeeCef fdbdcZe{ddd,eCdBeeCef fdedfZe{dgd,eCdheCfdidjZe{dkd,eCdBeeCef fdldmZG dndo doe'Ze{dpeeTfd,eCd+eeCef fdqdrZe{dpeeTfd,eCdAed+eeCef fdsdtZe{dueeTfd,eCd+eeCef fdvdwZG dxdy dye'Ze{dzeeTfd,eCd+eeCef fd{d|Ze{dzeeTfd,eCdAed+eeCef fd}d~Ze{deeTfd,eCdeCd+eeCef fddZe{dedddedudddedd/edd/edd/edd/edd/eeTfd,eCdegdegdeeC deeC deeC deeC deeC d+eeCef fddZe{deeTfd,eCdeCd+eeCef fddZG dd de'ZdeeCef deeCef fddZe{deeTfd+eeCef fddZe{dedeeTfd,eCdAeeCef d+eeCef fddZe{deeTfd,eCdAed+eeCef fddZe{deeTfd+eeCef fddZd dl Z!deCfddZ"deCfddZ#deCfddZ$e{ddd Z%e{ddeCfddZ&e{ddd Z'e{ddd Z(e{deddddedd/fdeCdegdeeC fddZ)G dd de'Z*e{de* fdeCdAe*fddZ+e{deeTfd+eeCef fddZ,e{dáeeTfded+eeCef fdĐdńZ-e{dơdǐdȄ Z.e{dɡd,eCfdʐd˄Z/ed̐d͡Z0dΐZ1d,dddАdќdadddҐdќdӐdddԐdќdՐdddאdќdؐdd4dِdќddd4dڐdќdېdd4dܐdќdݐdd4dސdќdߐdd4ddќd	dd4ddќddd4ddќddd4ddќddd4ddќddd4ddќddd4ddќgZ2d,eCddfddZ3d5edeeC fddZ4d,eCdaeCdeCdeRfddZ5G dd de'Z6e{ddd Z7e{dedd/fdAe6deeC fddZ8e{dedd/fdBeeCef deeC fddZ9e{d edd/edd/edd/edd/edd/fd,eCdeeC deeC deeC deeC d	eeC fddZ:e{dedd/edd/edd/edd/edd/fd,eCdeeC deeC deeC deeC d	eeC fddZ;dS (      N)BytesIO)StringIO)datetime)AnyDictListOptional)unquote)	BodyDependsFastAPIFileHeaderHTTPExceptionQueryRequest
UploadFile)CORSMiddleware)jsonable_encoder)JSONResponsePlainTextResponseStreamingResponse)	BaseModelField)AuthHandler)Config)DatabaseHandler)AnalyticsService)QualityParametersHandler)ObjectionClassificationsHandlerLeadSquaredService)AGENT_PROMPTSget_agent_instructionget_default_catalognormalize_agent)
RAGHandlerz4%(asctime)s - %(name)s - %(levelname)s - %(message)s)levelformatreturnc                  C   s,   i } t tD ]}| rtt|| |< q| S N)dirr   isuppergetattr)cfgkey r0   (/var/www/html/pca-backend/fastapi_app.py_build_config_dict$   s   r2   _PRESALES_MAP_CACHE,  c                   @   s,   e Zd ZU eedZeeee	f  e
d< dS )IngestDocumentsRequest)default_factory	documentsN)__name__
__module____qualname__r   listr7   r   r   strr   __annotations__r0   r0   r0   r1   r5   7   s   
 "r5   c                   @   sF   e Zd ZU dZeed< dZeed< dZeed< dZ	e
ee  ed< dS )	IngestTranscriptsRequestTpresales_only  limitFoverwrite_existingNcallids)r8   r9   r:   r?   boolr=   rA   intrB   rC   r   r   r<   r0   r0   r0   r1   r>   ;   s
   
 r>   c                   @   s   e Zd ZU eed< eed< dZeee  ed< dZ	ee ed< dZ
ee ed< dZee ed< dZeeeef  ed< dZeeeef  ed	< d
Zee ed< dS )QueryRequestuser_idmessageNquery_embeddingconversation_idtop_kmin_similaritymetadataprofile_updateslead_quality_agent
agent_type)r8   r9   r:   r<   r=   rI   r   r   floatrJ   rK   rE   rL   rM   r   r   rN   rP   r0   r0   r0   r1   rF   B   s   
 rF   c                   @   sB   e Zd ZU eed< dZee ed< dZee ed< dZe	ed< dS )	ScoreCallRequestcall_idsystemrG   rO   rP   F	force_llmN)
r8   r9   r:   r<   r=   rG   r   rP   rU   rD   r0   r0   r0   r1   rR   N   s
   
 rR   c                   @   s   e Zd ZU dZee ed< dZee ed< dZ	ee ed< dZ
ee ed< dZee ed< dZee ed< dZeeeef  ed< dS )	AgentConfigUpsertRequestNdisplay_name
is_enabled	is_lockedsystem_promptprovider
model_nameruntime_config)r8   r9   r:   rW   r   r<   r=   rX   rD   rY   rZ   r[   r\   r]   r   r   r0   r0   r0   r1   rV   U   s   
 rV   c                   @   s2   e Zd ZU eed< dZee ed< dZeed< dS )AgentAnalyzeCallRequestrS   rO   rP   Fforce_refreshN)	r8   r9   r:   r<   r=   rP   r   r_   rD   r0   r0   r0   r1   r^   _   s   
 r^   authorizationc                 C   s,   | r|  dstddd| ddd S )NzBearer   zMissing authorization headerstatus_codedetail    )
startswithr   replacestrip)r`   r0   r0   r1   _extract_bearer_tokene   s   rj   userbidc                 C   sP   | sdS |  drdS |  dg pg }|D ]}t| dt|kr% dS qdS )NF	is_masterT
businessesrl   getr<   )rk   rl   rn   businessr0   r0   r1   _has_business_accessk   s   
rr   )defaultc                 C   s&   t | }t|}|stddd|S )Nra   zInvalid or expired tokenrb   )rj   auth_handlervalidate_tokenr   )r`   tokenrk   r0   r0   r1   
_auth_userw   s
   
rw   c                 C   s    t || stdd|  d|S )N  zAccess denied to business rb   )rr   r   rl   rk   r0   r0   r1   _auth_user_for_bid   s   
rz   Fvaluers   c                 C   s    | d u r|S t |   dv S )N)1trueyesyon)r<   ri   lower)r{   rs   r0   r0   r1   _as_bool   s   r   c                 C   sl   | sdS |  drdS |  dg pg D ] }t| dd  }t| dt|kr3|dkr3 dS qdS )	NFrm   Trn   rolere   rl   admin)rp   r<   ri   r   )rk   rl   rq   r   r0   r0   r1   user_has_business_admin   s   
r   c                 C   s   t || stdddd S )Nrx   Business admin access requiredrb   )r   r   ry   r0   r0   r1   !_require_business_admin_or_master   s   
r   c                   C   sf   t dtddtt dtddt dtdd	t d
tddt dtddddS )NSYNC_SOURCE_DB_HOSTDB_HOST	127.0.0.1SYNC_SOURCE_DB_PORTDB_PORT  SYNC_SOURCE_DB_USERDB_USERr   SYNC_SOURCE_DB_PASSWORDDB_PASSWORDre   SYNC_SOURCE_DB_NAMEDB_NAMEvoicebot_clusterutf8mb4hostportrk   passworddatabasecharset)osgetenvCONFIGrp   rE   r0   r0   r0   r1   get_sync_source_db_config   s   r   phonec                 C   s   d dd t| p	dD }|sg S t|dkr|dd  n|}|d| |h}t|dkr?|d| d| d	| h d
d |D S )Nre   c                 s   s    | ]	}|  r|V  qd S r*   )isdigit).0chr0   r0   r1   	<genexpr>   s    z,_normalize_phone_variants.<locals>.<genexpr>
   i+91z+910c                 S      g | ]}|r|qS r0   r0   r   vr0   r0   r1   
<listcomp>       z-_normalize_phone_variants.<locals>.<listcomp>)joinr<   lenupdate)r   digitscore10variantsr0   r0   r1   _normalize_phone_variants   s   "r   payloadc                 C   sN   | d u rg S t | tr| S t | tr%dD ]}| |}t |tr$|  S qg S )Nr   Datar;   data
isinstancer;   dictrp   r   r/   r{   r0   r0   r1   _extract_lsq_lead_list   s   



r   c                 C   sJ   t | tr	|  S t | trdd |  D S t | tr#dd | D S | S )Nc                 S   s   i | ]\}}t |t|qS r0   )r<   _to_json_safer   kr   r0   r0   r1   
<dictcomp>       z!_to_json_safe.<locals>.<dictcomp>c                 S      g | ]}t |qS r0   )r   r   r0   r0   r1   r      r   z!_to_json_safe.<locals>.<listcomp>)r   r   	isoformatr   itemsr;   r{   r0   r0   r1   r      s   


r   detailscrmc                 C   s  |  dpg }tdd |D }tdd |D }t|tr#| dnd }|  d|p,i  dp4|  dt|  dp;d	t|t|t|  d
pId	|  dpPdt|  dpWd	t|trdt| dndt|trqt| dnd|pui  d|p{i  ddS )Ncallsc                 s   s,    | ]}t |d d dkrdV  qdS )call_statusre   ANSWERrf   N)r<   rp   upperr   callr0   r0   r1   r      s   * z*_build_customer_profile.<locals>.<genexpr>c                 s   s"    | ]}t |d rdV  qdS )has_transcriptrf   N)rD   rp   r   r0   r0   r1   r      s     lead
lead_phone
owner_nametotal_conversationsr   avg_quality_scoretalk_listen_ratioN/Atotal_duration_seconds	connectedFmatchedstatusnext_task_due_date)r   r   r   answered_callstranscript_callsr   r   r   crm_connectedcrm_matched
crm_statuscrm_next_task_due_date)rp   sumr   r   rE   rQ   rD   )r   r   r   r   r   crm_leadr0   r0   r1   _build_customer_profile   s"   r   c                
   C   sJ   t jtddttddtddtddtd	d
dt jjddS )Nr   r   r   r   r   r   r   re   r   r   r   Tr   r   rk   r   r   r   cursorclass
autocommit)pymysqlconnectr   rp   rE   cursors
DictCursorr0   r0   r0   r1   _db_conn   s   



r   c                  C   sF   t  } z|  }|d |d |d W |   d S |   w )Na  
            CREATE TABLE IF NOT EXISTS rag_agent_configs (
                id BIGINT AUTO_INCREMENT PRIMARY KEY,
                bid VARCHAR(50) NOT NULL,
                agent_type VARCHAR(100) NOT NULL,
                display_name VARCHAR(150) NOT NULL,
                is_enabled BOOLEAN DEFAULT FALSE,
                is_locked BOOLEAN DEFAULT TRUE,
                system_prompt LONGTEXT NOT NULL,
                provider VARCHAR(100) DEFAULT 'auto',
                model_name VARCHAR(150) DEFAULT '',
                runtime_config JSON,
                updated_by VARCHAR(100) DEFAULT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                UNIQUE KEY uniq_bid_agent (bid, agent_type),
                INDEX idx_bid_enabled (bid, is_enabled)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
            a8  
            CREATE TABLE IF NOT EXISTS rag_agent_call_analysis (
                id BIGINT AUTO_INCREMENT PRIMARY KEY,
                bid VARCHAR(50) NOT NULL,
                call_id VARCHAR(100) NOT NULL,
                agent_type VARCHAR(100) NOT NULL,
                status ENUM('completed','failed') DEFAULT 'completed',
                model_name VARCHAR(150) DEFAULT '',
                summary TEXT,
                result_json JSON,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                UNIQUE KEY uniq_bid_call_agent (bid, call_id, agent_type),
                INDEX idx_bid_created (bid, created_at)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
            a@  
            CREATE TABLE IF NOT EXISTS rag_agent_knowledge_uploads (
                id BIGINT AUTO_INCREMENT PRIMARY KEY,
                bid VARCHAR(50) NOT NULL,
                agent_type VARCHAR(100) NOT NULL,
                version_no INT NOT NULL,
                filename VARCHAR(255) NOT NULL,
                page_count INT NOT NULL,
                char_count INT NOT NULL,
                knowledge_bands JSON,
                compiled_prompt LONGTEXT,
                uploaded_by VARCHAR(100) DEFAULT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                UNIQUE KEY uniq_bid_agent_version (bid, agent_type, version_no),
                INDEX idx_bid_agent_created (bid, agent_type, created_at)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
            )r   cursorexecuteclose)connr   r0   r0   r1   _ensure_agent_tables   s   r   c                 C   s   t  }t }zF| }|D ]8}|dt| t|d t|d |dr&dnd|dr.dndt|d tj|d	p?d
ddddf qW |  d S |  w )NaQ  
                INSERT INTO rag_agent_configs (
                    bid, agent_type, display_name, is_enabled, is_locked, system_prompt, provider, model_name, runtime_config
                )
                VALUES (%s, %s, %s, %s, %s, %s, 'auto', '', %s)
                ON DUPLICATE KEY UPDATE agent_type = agent_type
                rP   rW   rX   rf   r   rY   instructionr]   g?g?)temperaturetop_pTensure_ascii)	r$   r   r   r   r<   rp   jsondumpsr   )rl   catalogr   r   itemr0   r0   r1   _ensure_default_agent_configs-  s$   


r   c              	   C   s   t |  t }z| }|dt| f | pg }W |  n|  w |D ]#}t|dtrKzt	
|d |d< W q( tyJ   i |d< Y q(w q(t|S )Nz
            SELECT bid, agent_type, display_name, is_enabled, is_locked, system_prompt, provider, model_name, runtime_config, updated_at
            FROM rag_agent_configs
            WHERE bid = %s
            ORDER BY id ASC
            r]   )r   r   r   r   r<   fetchallr   r   rp   r   loads	Exceptionr   )rl   r   r   rowsrowr0   r0   r1   _get_agent_configsI  s&   	r  rP   c                 C   s0   t | D ]}t|dt|kr|  S qd S )NrP   )r  r<   rp   )rl   rP   r   r0   r0   r1   _get_agent_configc  s
   r  c                 C   s.   t | |}|r|drt|dS t|S )NrZ   )r  rp   r<   r#   )rl   rP   r.   r0   r0   r1   "_resolve_agent_instruction_for_bidj  s   
r  	pdf_bytesc                 C   sh  d }dD ]9}z#t |}|t| }g }|jD ]}|| p d  q|W   S  ty= } z|}W Y d }~qd }~ww z]tt	
d| }|dkrOtd| jddd}	t	d	|	}
t|
d
krh|
d
d  n|	g}g }t|D ]'}|t|k r}|| nd}t	
d|}ddd |D  }||pd qq|W S  ty } ztdd| d| dd }~ww )N)pypdfPyPDF2re   s   /Type\s*/Page\br   zNo /Type /Page markers foundlatin1ignore)errorsz/Type\s*/Page\brf   z\(([^()]*)\) c                 S   s   g | ]	}t d d|qS )z\\[nrtbf()\\]r
  )resub)r   litr0   r0   r1   r         z&_extract_pdf_pages.<locals>.<listcomp>  zPDF parser is unavailable on the server and fallback extraction failed. Install 'pypdf' inside backend venv for reliable parsing. Last parser error: z; fallback error: rb   )
__import__	PdfReaderr   pagesappendextract_textri   r   r   r  findall
ValueErrordecodesplitranger   r   )r  parser_errormodule_namemodulereaderr  pageexc
page_count	text_blobpage_splitscandidate_sectionsidxsegmentliteralscleanedfallback_excr0   r0   r1   _extract_pdf_pagesq  sN   

r)  c                 C   s   | dkrt dd|  dd S )Npresales_lead_agent  zaThis endpoint is currently enabled only for Raj Mehta (presales_lead_agent). Received agent_type=rb   )r   )rP   r0   r0   r1   _assert_raj_presales_agent  s   r,  rG   rA   c              	   C   s   t  }z.| }|r|dt| t|t|f n|dt| t|f | p+g }W |  n|  w |D ]#}t|dtr\zt	
|d |d< W q9 ty[   i |d< Y q9w q9dt|iS )Na_  
                SELECT c.conversation_id, c.user_id, c.metadata, c.updated_at,
                    (SELECT content FROM rag_messages m WHERE m.bid = c.bid AND m.conversation_id = c.conversation_id AND m.role = 'user' ORDER BY m.id ASC LIMIT 1) AS first_user_message,
                    (SELECT content FROM rag_messages m WHERE m.bid = c.bid AND m.conversation_id = c.conversation_id ORDER BY m.id DESC LIMIT 1) AS last_message
                FROM rag_conversations c
                WHERE c.bid = %s AND c.user_id = %s
                ORDER BY c.updated_at DESC
                LIMIT %s
                aL  
                SELECT c.conversation_id, c.user_id, c.metadata, c.updated_at,
                    (SELECT content FROM rag_messages m WHERE m.bid = c.bid AND m.conversation_id = c.conversation_id AND m.role = 'user' ORDER BY m.id ASC LIMIT 1) AS first_user_message,
                    (SELECT content FROM rag_messages m WHERE m.bid = c.bid AND m.conversation_id = c.conversation_id ORDER BY m.id DESC LIMIT 1) AS last_message
                FROM rag_conversations c
                WHERE c.bid = %s
                ORDER BY c.updated_at DESC
                LIMIT %s
                rM   conversations)r   r   r   r<   rE   r   r   r   rp   r   r   r   r   )rl   rG   rA   r   r   r   r   r0   r0   r1   _list_conversations  s.   		r.  rS   c                 C   s^   t | |pi }t | |pi }t | |pi }t | |pi }t|t|t|t|dS )N)r   
transcript	analyticsbant)
db_handlerget_call_by_idget_call_transcriptget_call_analyticsget_bant_analysisr   )rl   rS   r   r/  r0  r1  r0   r0   r1   _collect_call_context  s   r7  contextc           	   	   C   s   |  dpi }|  dpi }|  dpi }| d}| d}| dp%d}| dp,i }| dp3d	}||d ur;|nd
|p?d|d urI|dk rIdnd||| ddg ddS )Nr0  r1  r/  quality_score	sentimentsummaryz+Call analyzed using existing call metadata.profilere   r   unknown<   mediumlowlanguage)r<  r;  transcript_language)zMReview objection handling moments and add specific rebuttal playbook entries.z5Track repeat intent and follow-up commitments in CRM.z9Coach agent using this call transcript and quality notes.)r;  r9  r:  compliance_riskcustomer_profilerecommended_actionsrp   )	r8  r0  r1  r/  qualityr:  r;  r<  profile_summaryr0   r0   r1   _score_call_fallback  s$   

rI  c                 C   sV   t | |pi }|dpi }t|tsi }t|dpdt|dp$d |dS )Nr]   r[   autor\   re   r[   r\   r]   )r  rp   r   r   r<   ri   )rl   rP   r.   runtime_cfgr0   r0   r1   _resolve_agent_model_settings  s   
rM  c           
      C   s   |  dpi  d}|sd S t||}t|| dtj| dd }tj||d |d p-d |d d}|s7d S |d	}|d
}|dksM|dksM||krOd S zt	|||d  }	W n
 t
yf   Y d S w t|	tsnd S |	S )Nr/  z
Return STRICT JSON with keys: summary, quality_score, sentiment, compliance_risk, customer_profile, recommended_actions.
quality_score must be 0-100 numeric.

CALL CONTEXT:
Tr   r[   r\   r]   rK  {}rf   )rp   rM  r  r   r   rag_handler_invoke_chat_modelfindrfindr   r   r   r   )
r8  rl   rP   transcript_textsettingspromptanswerstartendparsedr0   r0   r1   _score_call_with_llm  s8   




r\  agent_iddaysc           	   
   C   s   |  d}|  d}t jtddttddtddtd	d
tdddt jjdd}z#| }d| d| d}|||t|f |	 pLi }W |
  n|
  w |t|t|t  d dS )N
_raw_calls_callanalyticsr   r   r   r   r   r   r   re   r   r   r   Tr   a>  
            SELECT
                COUNT(*) AS total_calls,
                AVG(a.quality_score) AS avg_quality_score,
                AVG(a.agent_speak_percentage) AS avg_agent_speak_percentage,
                AVG(a.customer_speak_percentage) AS avg_customer_speak_percentage,
                SUM(CASE WHEN a.sentiment = 'positive' THEN 1 ELSE 0 END) AS positive_calls,
                SUM(CASE WHEN a.sentiment = 'neutral' THEN 1 ELSE 0 END) AS neutral_calls,
                SUM(CASE WHEN a.sentiment = 'negative' THEN 1 ELSE 0 END) AS negative_calls
            FROM `z` r
            LEFT JOIN `z` a ON r.callid = a.callid
            WHERE COALESCE(NULLIF(r.agentname, ''), NULLIF(r.agent_callinfo, ''), 'Unknown') = %s
              AND r.call_starttime >= DATE_SUB(NOW(), INTERVAL %s DAY)
        Z)r]  window_daysmetricsgenerated_at)r   r   r   rp   rE   r   r   r   r   fetchoner   r   r   utcnowr   )	rl   r]  r^  table_callstable_analyticsr   r   queryr   r0   r0   r1   _agent_report*  s6   






	
rj  r   
limit_runsc              
   C   s   t jtddttddtddtddtd	d
dt jjdd}z+| }|dt	| f |
 p8i }|dt	| t|f | pJg }W |  n|  w t	| t|t|dS )Nr   r   r   r   r   r   r   re   r   r   r   Tr   a*  
            SELECT bid, status, last_run_started_at, last_run_finished_at, last_duration_ms,
                   processed_calls, ingested_documents, ingested_chunks, skipped, last_error, updated_at
            FROM rag_ingestion_progress
            WHERE bid = %s
            LIMIT 1
            a,  
            SELECT id, bid, started_at, finished_at, duration_ms, status,
                   processed_calls, ingested_documents, ingested_chunks, skipped, error, created_at
            FROM rag_ingestion_runs
            WHERE bid = %s
            ORDER BY id DESC
            LIMIT %s
            )rl   currentrecent_runs)r   r   r   rp   rE   r   r   r   r   r<   re  r   r   r   )rl   rk  r   r   rl  runsr0   r0   r1   _ingestion_progressS  s0   





ro  c                 C   s  t | |}|dpi }|dstdd| dt| |}| dtj|dd }t| |}tj||d |d	 p;d |d
 dpBd}|	d}	|
d}
|	dksY|
dksY|
|	kriddg g g d|d d id}n&zt||	|
d  }W n ty   ddg g g d|d d id}Y nw t }z2| }|dt| t|t|t|d	 ptddt|dpdtj|ddf W |  n|  w t| t|t|t|t  d dS )Nr/    !Transcript not found for call_id=rb   z
Return STRICT JSON with keys: summary, score, risks, opportunities, actions, model_data.
score must be 0-100 numeric.

CALL CONTEXT:
Tr   r[   r\   r]   rK  re   rN  rO  rP  zModel returned non-JSON output.r   raw_response  )r;  scorerisksopportunitiesactions
model_datarf   zModel output parse failed.a  
            INSERT INTO rag_agent_call_analysis (bid, call_id, agent_type, status, model_name, summary, result_json)
            VALUES (%s, %s, %s, 'completed', %s, %s, %s)
            ON DUPLICATE KEY UPDATE
                status='completed',
                model_name=VALUES(model_name),
                summary=VALUES(summary),
                result_json=VALUES(result_json),
                updated_at=CURRENT_TIMESTAMP
            RAG_CHAT_MODELr;  ra  )rl   rS   rP   resultrd  )r7  rp   r   r  r   r   rM  rQ  rR  rS  rT  r   r   r   r   r   r<   r   r   r   r   rf  r   )rl   rS   rP   r8  transcript_objagent_instructionrW  rV  response_textrY  rZ  
result_objr   r   r0   r0   r1   _run_agent_call_analysis|  sv   





	
	

r  c                 C   sf   | d u rd S t | tr| r| d S d S t | tr1dD ]}| |}t |tr.|r.|d   S q| S d S )Nr   r   r   r   r0   r0   r1   _extract_lsq_lead_record  s   


r  valuesc                  G   s0   | D ]}|d u r	qt | }|r|  S qd S r*   r<   ri   )r  r{   textr0   r0   r1   _first_present_str  s   r  recordkeysc                 G   s   t | tsd S |D ]}|| v r| |dvr| |  S q	|D ]!}|  D ]\}}t| t| kr@|dvr@|    S q&q d S )NNre   )r   r   rp   r   r<   r   )r  r  r/   
record_keyr{   r0   r0   r1   
_lsq_field  s   
 r  c                 C   sn   t | tsd S tt| dd}tt| dd}ddd ||fD  p&d }tt| dd	d
d|t| ddS )N	FirstName
first_nameLastName	last_namer
  c                 S   r   r0   r0   )r   partr0   r0   r1   r     r   z%_lsq_display_name.<locals>.<listcomp>ProspectNameprospect_nameContactNamecontact_nameNamename)r   r   r  r  r   ri   )r  firstlast	full_namer0   r0   r1   _lsq_display_name  s   
 
r  c                 C   s   t t|}|st| S t| }|s t| }t|tr|gng }|D ]}t|ddddd}|s0q"t t|}||r?|  S q"d S )NPhoneMobilePhoneNumberr   mobile)setr   r  r   r   r   r  intersection)r   r   target_variantsleadssingler   r   lead_variantsr0   r0   r1   _select_lsq_record_for_phone  s    
r  fallback_phonec                 C   sl   t t|}|s
dS t| trt| dddddnd }|r't|t t|S |r4t|t t|S dS )NFr  r  r  r   r  )r  r   r   r   r  rD   r  )r  r   r  r  r   r0   r0   r1   _record_matches_phone  s    r  c                 C   s   t | tsd S tt| ddddd}|r|S tt| ddd|p!d	}tt| d
dp+d	}t| p1d	}| d| d| }t|d S )N
ProspectIDLeadIdIdlead_id
ProspectIdr  r  r  re   EmailAddressemail|zutf-8)	r   r   r  r  r  hashlibsha256encode	hexdigest)r  r  r  r   r  r  digest_sourcer0   r0   r1   _lsq_external_id"  s   
r  c              
   C   sF   | t | t| ddt| dddt| dddt| d	d
dt| dddS )Nr  r  r  r  r  
LeadStatusStatuslead_status	OwnerNameOwnerr   NextTaskDueDater   rawr  r  r   r   r   r   )r  r  )r  r0   r0   r1   _crm_payload_from_lsq1  s   

r  c                   C   s   t tdd  dkS )NALLOW_CRM_WRITEfalser}   )r<   r   r   ri   r   r0   r0   r0   r1   _crm_write_allowed=  s   r  c                 C   sV   t | d}|r|dr|dr|dstdddt|d |d |dd	S )
Nleadsquared
access_key
secret_key	is_activer+  2LeadSquared integration not configured or inactiverb   api_hostr  r  r  )r2  get_crm_credentialsrp   r   r!   )rl   credsr0   r0   r1   !_get_lsq_service_for_bid_or_errorA  s   "r     	groupname	row_countr_   c                  C   s  t dtt|pdd}|  d|pd d| }tt }t|}|r9|s9|t|dd tkr9|di S t| d	}|rP|d
rP|drP|ds^dddg g g dddddS t	|d
 |d |dd}	|	
dd|di}
|
dsdd|
dpdg g g dddddS t|
d}i }t }|D ] }t|ddd}|sqt|D ]}|| ||vr|||< qqtj| t||d}i }i }g }|D ]}|dpd}|dpd}|d pd}d }t|D ]}||}|r nq||vr|dt t d!||< ||vr|t dt d"||< t|d#p d}|| d$  |7  < || d% | || d& | || d$  |7  < || d' | || d& | ||||||d(t|t|d)d*d+t|d,d-d.d/ qd0d1 | D }|jd2d3 dd4 g }| D ]%}t|d' }||d ||r|d nd t|d$ t|d& d5 q|jd6d3 dd4 ddd7|||t|td8d9 |D t|dd}||d:t|< |S );Nr   r  r  :re   tsr   r   r  r  r  r  Fr  )lsq_leads_fetchedmatched_customersmapped_rows)successr   rH   groupsagents	customersstatsr  r  Paging)OffsetRowCountr  TrH   z&Failed to fetch leads from LeadSquaredr  r  r  )rl   customer_numbersr  r  -	agentnamecustomer_callinfo)r  
totalCallsmatchedCustomersr  )r  
groupnamesr  r  total_callsr  r  r  r  	last_callr  r  r   r  r  r  )r  r  r  r  r  lsq_namelsq_owner_name
lsq_statusc                 S   s6   g | ]}|d  t |d t|d t|d dqS )r  r  r  r  )r  r  r  agentsCount)rE   r   )r   valr0   r0   r1   r     s    


z:_get_presales_mapping_from_leadsquared.<locals>.<listcomp>c                 S      | d S Nr  r0   xr0   r0   r1   <lambda>      z8_get_presales_mapping_from_leadsquared.<locals>.<lambda>)r/   reverse)r  r  r  r  r  c                 S   r  r  r0   r  r0   r0   r1   r    r  z2Pre-sales mapping generated from LeadSquared leadsc                 S      h | ]}|d  qS )r  r0   r   r   r0   r0   r1   	<setcomp>  r   z9_get_presales_mapping_from_leadsquared.<locals>.<setcomp>)r  r   )maxminrE   timer3   rp   _PRESALES_MAP_CACHE_TTL_SECONDSr2  r  r!   search_leadsr   r  r  r   addget_group_agent_customer_rowsr;   r  r  r  sortsortedr   ) rl   r  r  r_   safe_row_count	cache_keynowcachedr  service
lsq_searchr  phone_to_leadall_phone_variantsr   r   variantr   group_indexagent_indexcustomer_rowsr   groupagentcustomer_phonematched_leadr  r  r  r  r  responser0   r0   r1   &_get_presales_mapping_from_leadsquaredL  s   
 "








	

	r  zMCube AI FastAPIz1.0.0)titleversion*T)allow_originsallow_credentialsallow_methodsallow_headers)requests_totalerrors_totalhttprequestc                    s   t d  d7  < t }z	|| I d H }W n ty&   t d  d7  <  w tt | d d}td| jj|j	| t
||jd< |S )Nr  rf   r  r@      zpath=%s status=%s latency_ms=%szX-Process-Time-Ms)_metricsr  perf_counterr   roundloggerinfourlpathrc   r<   headers)r  	call_nextstartedr  
latency_msr0   r0   r1   metrics_and_latency  s   r$  z/healthc                   C   s   ddt   d tdS )Nhealthyzmcube-ai-fastapira  )r   r  	timestamprc  )r   rf  r   r  r0   r0   r0   r1   health  s
   r'  z/rag/agents.rf   )
min_lengthc                 C   s   t | | dt| iS )Nr  )rz   r  ry   r0   r0   r1   list_agents  s   
r)  z/rag/{bid}/agents/catalogc              	   C   s:   t | | t| t g dg dttddgddS )N)rJ  ollamabedrock)z
qwen2.5:7bz	gemma2:9bzdeepseek-r1:8bry  re   )r*  r+  )rl   r   	providerssuggested_models)rz   r<   r$   r   rp   ry   r0   r0   r1   rag_agent_catalog  s   
r.  z/rag/{bid}/ollama/modelsc              
   C   s   t | | ttddd}ztj| ddd}|  |jr&| ni }W n t	yA } zt
dd| d	| d
d }~ww |dpHg }dd |D }|||dS )NOLLAMA_BASE_URLzhttp://127.0.0.1:11434/z	/api/tags   )timeout  z$Failed to fetch models from Ollama (z): rb   modelsc                 S   s$   g | ]}| d rt| d qS )r  ro   r   r   r0   r0   r1   r   +  s   $ z%rag_ollama_models.<locals>.<listcomp>)ollama_base_urlr4  r  )rz   r<   r   rp   rstriprequestsraise_for_statuscontentr   r   r   )rl   rk   base_urlr  r   r  r4  namesr0   r0   r1   rag_ollama_models  s   
r=  z/rag/{bid}/documentsc                 C   s4   t | | |jstdddt| |j}d|dS )Nr+  zdocuments array is requiredrb   r  )r   	ingestion)rz   r7   r   rQ  ingest_documents)rl   r   rk   rz  r0   r0   r1   rag_ingest_documents/  s
   

r@  z/rag/{bid}/ingest-transcriptsc                 C   s*   t | | tj| |jt|j|j|jdS )Nrl   r?   rA   rB   rC   )rz   rQ  backfill_transcriptsr?   rE   rA   rB   rC   )rl   r   rk   r0   r0   r1   rag_ingest_transcripts8  s   
rC  z/rag/{bid}/queryc           	      C   s`  t | | |jstddd|jr|j stdddt|j}t| |p'i }|dr6td| dd|dd	u rFtd| d
dt	| |}t
|jpPi }||d< t|dp]d|d< t|dphd|d< |dpri |d< ttdd}t|pdd | |d< tt|pd|k|d< tj| |j|j|j|j|j|j||jd	}||d< |S )Nr+  zuser_id is requiredrb   zmessage is requiredrY   rx    is locked for this businessrX   F! is not enabled for this businessrP   r[   rJ  llm_providerr\   re   llm_model_namer]   llm_runtime_configRAG_AGENT_INSTRUCTION_MAX_CHARSi.  r|  agent_instruction_truncated)	rl   rG   rH   rI   rJ   rK   rL   rM   rN   )rz   rG   r   rH   ri   r%   rP   r  rp   r  r   rM   r<   rE   r   r   rQ  ri  rI   rJ   rK   rL   rN   )	rl   r   rk   rP   	agent_cfgr|  rM   max_instruction_charsrz  r0   r0   r1   	rag_queryD  sB   



rM  z*/rag/{bid}/conversations/{conversation_id}   r@   )rs   gelerJ   c                 C   s$   t | | tj| ||d}||dS )NrA   )rJ   messages)rz   rQ  get_conversation_messages)rl   rJ   rA   rk   rR  r0   r0   r1   rag_get_conversationl  s   

rT  z/rag/{bid}/conversations2   c                 C   s   t | | t| ||dS )N)rl   rG   rA   )rz   r.  )rl   rG   rA   rk   r0   r0   r1   rag_list_conversationsx  s   
rV  z/rag/{bid}/profiles/{user_id}c                 C   s   t | | t| |S r*   )rz   rQ  get_user_profile)rl   rG   rk   r0   r0   r1   rag_get_profile  s   
rX  z/rag/{bid}/score-callc                 C   s   t | | t| |j}|dpi }|ds!tdd|j dt|| t|j}|jr5|s5tddd|p:t	|}t
| |jt|j|t  d dS )	Nr/  rp  rq  rb   r3  zLLM scoring failedra  )rl   rS   rP   	scorecardrd  )rz   r7  rS   rp   r   r\  r%   rP   rU   rI  r<   r   rf  r   )rl   r   rk   r8  r{  scoredr  r0   r0   r1   rag_score_call  s   


r[  z/rag/{bid}/agent-reportr1  im  c                 C   s   t | | t| ||S r*   )rz   rj  )rl   r]  r^  rk   r0   r0   r1   rag_agent_report  s   
r\  z/rag/{bid}/ingestion-progressd   c                 C   s   t | | t| |dS )N)rk  )rz   ro  )rl   rk  rk   r0   r0   r1   rag_ingestion_progress  s   
r^  z%/rag/{bid}/agents/config/{agent_type}c                 C   s  t | | t|}t|  t| |pi }|stdd| d|jd ur'|jn|d|jd ur5t|jntt	|d|j
d urGt|j
ntt	|d|jd urW|jn|d|jd urc|jn|d|jd uro|jn|d	|jd ur{|jn|d
pi d}t }zM| }|dt|d t|d t|d t|d t|d pdt|d	 pdtj|d
 ddt|dp|dpdt| t|f
 W |  n|  w dt| |dS )Nrp  Unknown agent type: rb   rW   rX   rY   rZ   r[   r\   r]   )rW   rX   rY   rZ   r[   r\   r]   an  
            UPDATE rag_agent_configs
            SET display_name = %s,
                is_enabled = %s,
                is_locked = %s,
                system_prompt = %s,
                provider = %s,
                model_name = %s,
                runtime_config = %s,
                updated_by = %s
            WHERE bid = %s AND agent_type = %s
            rJ  re   Tr   usernameidrT   ok)r   r  )rz   r%   r   r  r   rW   rp   rX   rE   rD   rY   rZ   r[   r\   r]   r   r   r   r<   r   r   r   )rl   rP   r   rk   existing
update_objr   r   r0   r0   r1   rag_upsert_agent_config  sB   
$$	



re  z,/rag/{bid}/agents/{agent_type}/knowledge-pdffilenameappend_to_existing_promptc                    sB  t | | t|}t| |rt| dstdddt|  t| |p)i }|s5tdd| dt|j	
dp=d }d	|vrKtdd
d| I dH }|sZtdddt|dkrftdddt|}	t|	}
|
dk rxtddddd |	D }t|stddddd t|D }ddd |D }|stdddt|}d| }|r|
dr|
d d| }n| d| }t|
dpi }t|t|
dp|
dpdt  d |
t|d|d < ||d!< ||d"< t }d}z| }|d#t| t|f | pi }t|
d$pd%d }||d  d&< |d't|tj|d(d)t|
dp@|
dp@dt| t|f |d*t| t|t|t|t|
tt|tj|d(d)t|t|
dpx|
dpxdf	 W |  n|  w d+t| t|t|t||
t|d,t| |d-S ).z
    Upload a knowledge PDF containing prompt + knowledge bands for an agent.
    For current rollout, this endpoint is restricted to the pre-sales lead agent.
    z.pdfr+  zOnly .pdf files are supportedrb   rp  r_  content-typere   zapplication/pdfz,Request content-type must be application/pdfNzUploaded PDF is emptyi   zPDF exceeds 50MB limitrf   z*No pages were detected in the uploaded PDFc                 S   s   g | ]
}t |pd  qS )re   r  )r   r  r0   r0   r1   r         z2rag_upload_agent_knowledge_pdf.<locals>.<listcomp>z*Could not extract usable text from the PDFc                 S   s   g | ]\}}|d  |dqS )rf   )r  r  r0   )r   r$  r  r0   r0   r1   r     r   z

c                 S   s,   g | ]}|d  rd|d  d|d   qS )r  z[Knowledge Band Page r  z]
r0   r5  r0   r0   r1   r     s   , z0No valid knowledge-band content found in the PDFz]Use the following uploaded prompt and knowledge bands as the primary rubric for evaluation.

rZ   r]   r`  ra  rT   ra  )rf  uploaded_byuploaded_atr   
char_countknowledge_pdf_metaknowledge_bandsknowledge_compiled_promptz
            SELECT COALESCE(MAX(version_no), 0) AS max_version
            FROM rag_agent_knowledge_uploads
            WHERE bid = %s AND agent_type = %s
            max_versionr   
version_noz
            UPDATE rag_agent_configs
            SET system_prompt = %s,
                runtime_config = %s,
                updated_by = %s
            WHERE bid = %s AND agent_type = %s
            Tr   a  
            INSERT INTO rag_agent_knowledge_uploads (
                bid, agent_type, version_no, filename, page_count, char_count,
                knowledge_bands, compiled_prompt, uploaded_by
            )
            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
            rb  )rf  r   rl  )r   rl   rP   rq  filer  )rz   r%   r,  r<   r   endswithr   r   r  r   rp   bodyr   r)  any	enumerater   r#   r   r   rf  r   r   r   r   re  rE   r   r   r   )rl   rP   r  rf  rg  rk   rc  content_type
file_bytes
page_textsr   cleaned_pagesrn  compiled_knowledgebase_instructionknowledge_instructionrZ   r]   r   rq  r   r   r0   r0   r1   rag_upload_agent_knowledge_pdf  s   


r~  c              	   C   s   t | | t|}t| t| |pi }|dpi }|dp!i }|dp(g }|dp/d}|s>dt| t|ddd	S dt| t|d
||t|dd dd	S )zS
    Preview latest uploaded knowledge PDF extraction for agent configuration.
    r]   rm  rn  ro  re   rb  FN)r   rl   rP   has_uploaded_knowledgeknowledge_pdfTi  )metarn  compiled_prompt_preview)rz   r%   r,  r  rp   r<   )rl   rP   rk   r.   r]   r  rn  compiledr0   r0   r1   rag_get_agent_knowledge_pdfv  s0   
		r  z4/rag/{bid}/agents/{agent_type}/knowledge-pdf/historyc                 C   s   t | | t|}t| t }z| }|dt| t|t|f | p)g }W |	  n|	  w dt| t|t
|dS )zI
    List previous uploaded versions for knowledge PDF on the agent.
    z
            SELECT version_no, filename, page_count, char_count, uploaded_by, created_at
            FROM rag_agent_knowledge_uploads
            WHERE bid = %s AND agent_type = %s
            ORDER BY version_no DESC
            LIMIT %s
            rb  )r   rl   rP   versions)rz   r%   r,  r   r   r   r<   rE   r   r   r   )rl   rP   rA   rk   r   r   r   r0   r0   r1   #rag_get_agent_knowledge_pdf_history  s"   


r  z/rag/{bid}/agents/analyze-callc                 C   s  t | | t|j}t| |pi }|drtd| dd|ddu r/td| dd|jst }z| }|	dt
| t
|jt
|f | }W |  n|  w |rt|d	t
rxzt|d	 |d	< W n	 tyw   Y nw d
t|dS dt| |j|dS )NrY   rx   rD  rb   rX   FrE  z
                SELECT bid, call_id, agent_type, model_name, summary, result_json, updated_at
                FROM rag_agent_call_analysis
                WHERE bid = %s AND call_id = %s AND agent_type = %s
                LIMIT 1
                result_jsonr  )r   analysis	generated)rz   r%   rP   r  rp   r   r_   r   r   r   r<   rS   re  r   r   r   r   r   r   r  )rl   r   rk   rP   rK  r   r   r  r0   r0   r1   rag_agent_analyze_call  s4   



	r  z$/rag/{bid}/agents/analysis/{call_id}rO   c                 C   s   t | | t|}t }z| }|dt| t|t|f | }W |  n|  w |s7tdddt	|
dtrTzt|d |d< W n	 tyS   Y nw dt|iS )Nz
            SELECT bid, call_id, agent_type, model_name, summary, result_json, updated_at
            FROM rag_agent_call_analysis
            WHERE bid = %s AND call_id = %s AND agent_type = %s
            LIMIT 1
            rp  zNo analysis foundrb   r  r  )rz   r%   r   r   r   r<   re  r   r   r   rp   r   r   r   r   )rl   rS   rP   rk   r   r   r   r0   r0   r1   rag_get_agent_analysis  s(   

	r  z/auth/registerc              
      st   g d}|D ]}|  |std| ddqtj| d | d | d | d |  d	|  d
dd\}}t||dS )N)rl   r`  r  r   r+   is requiredrb   rl   r`  r  r   r  r   rk   )rl   r`  r  r   r  r   rc   r:  )rp   r   rt   create_userr   )r   required_fieldsfieldrz  rc   r0   r0   r1   auth_register  s   


r  z/auth/loginc                    sb   |  dr|  dstdddtj| d | d |jr |jjnd |j dd\}}t||dS )	Nr`  r   r+  z"Username and password are requiredrb   
User-Agent)
ip_address
user_agentr  )rp   r   rt   loginclientr   r   r   )r   r  rz  rc   r0   r0   r1   
auth_login   s   

r  z/auth/logoutc                 C   s"   t |}t|\}}t||dS )Nr  )rj   rt   logoutr   )rk   r`   rv   rz  rc   r0   r0   r1   auth_logout-  s   r  z/auth/mec                 C   s   d| iS )Nrk   r0   )rk   r0   r0   r1   auth_me4     r  z/auth/users/{bid}c                 C   s,   t | | t| \}}t|td|idS )Nusersr  )rz   rt   get_users_by_businessr   r   )rl   rk   r  rc   r0   r0   r1   auth_users_for_business9  s   
r  z/business-admin/users/createc           	   	      s2  |  d}|stdddt|t|stddddD ]}|  |s-td| ddqt|  d	d
p6d
  }|dvrBd
}tj| d | d | d |  dpV|  d|dd\}}|dkr|d }tj|t||d tj	| d| ddd| d  d| |j
r|j
jnd|j dd t||dS )z
    Create a new user and assign access to the given business (`bid`).
    Business admins can do this for their own `bid`. Master admins can also do this.
    rl   r+  bid is requiredrb   rx   r   r`  r  r   r  r   rk   )rk   r   r`  r  r   r  r  Fr`  r  r   r  r   rm      ra  rG   rl   r   r  Created new user: z	 for bid Nr  rG   r`  activity_typedescriptionr  r  r  )rp   r   r   r<   ri   r   rt   r  assign_business_accesslog_activityr  r   r   r   )	r   r  rk   rl   r  business_rolerz  rc   rG   r0   r0   r1   business_admin_users_create@  sB   


	
	r  z/list-businessesc                   C   s   t  S r*   )r2  get_all_businessesr0   r0   r0   r1   list_businesseso  r  r  z/businesses/{bid}/infoc                 C   s   t | }|stddd|S )Nrp  zBusiness not foundrb   )r2  get_business_infor   )rl   r  r0   r0   r1   business_infot  s   
r  z/groupnames/{bid}r?   lsq_row_countc                 C   s8   |rt | d ||d}|dg }|pt| S t| S )Nrl   r  r  r_   r  )r  rp   r2  get_all_groupnames)rl   r?   r  r_   mappingr  r0   r0   r1   r  |  s   
r  z/agentnames/{bid}c                 C   sF   |rt | |||d}dd |dg D }|pt| |S t| |S )Nr  c                 S   s    g | ]}| d r| d qS )r  rF  r5  r0   r0   r1   r          zagentnames.<locals>.<listcomp>r  )r  rp   r2  get_agent_names)rl   r  r?   r  r_   r  r  r0   r0   r1   
agentnames  s   r  z/location-stats/{bid}c                 C      t | |S r*   )r2  get_location_stats)rl   r  r0   r0   r1   location_stats     r  z/location-calls/{bid}	directionr   offsetc                 C   s   t | |||||S r*   )r2  get_filtered_raw_calls)rl   r  r  r   rA   r  r0   r0   r1   location_calls  s   	r  z/raw-calls/{bid}/{callid}callidc                 C       t | |}|stddd|S )Nrp  Call not foundrb   )r2  get_raw_call_detailsr   )rl   r  r   r0   r0   r1   raw_call_details     r  z/analytics/{bid}/pendingc                 C   s   t | |}t||dS )N)countr   )r2  get_calls_for_analysisr   )rl   rA   r   r0   r0   r1   pending_analytics  s   r  z/analytics/{bid}/dashboardc              
   C   s   zPt  B}| .}|d|  d | s1i g g g g g g dW  d    W  d    W S W d    n1 s;w   Y  W d    n1 sJw   Y  W n	 tyY   Y nw t | |t | |t | |t 	| |t 
| |t | |t | |dS )NSHOW TABLES LIKE '_raw_calls')overviewsentiment_by_locationquality_by_locationquality_by_agentcall_purposesconcerns_frequencybusy_locations)r2  get_connectionr   r   re  r   get_analytics_overviewget_sentiment_by_locationget_quality_by_locationget_quality_by_agentget_call_purpose_frequencyget_concerns_frequencyget_busy_locations)rl   r  r   curr0   r0   r1   analytics_dashboard  s0   








r  z#/analytics/{bid}/calls-by-objection	objectionc                 C      t | ||S r*   )r2  get_calls_by_objection)rl   r  r  r0   r0   r1   analytics_calls_by_objection  s   r  z/calls/{bid}r   sales_intent	date_fromdate_toc           
      C   sb   i }|d ur
||d< |r||d< |r||d< |r||d< t | |||}t | |}	||	||dS )Nr   r  r  r  )r   totalrA   r  )r2  	get_callsget_calls_count)
rl   r   r  r  r  rA   r  filtersr   total_countr0   r0   r1   
calls_list  s   
r  z/calls/{bid}/{callid}c                 C   s  t | |}|stdddt | |}|rX|dd|d< |dd|d< |d}|rFt|trFzt|}W n tj	yE   g }Y nw ||d< |d	|d	< |d
|d
< t 
| |}|r|dd|d< |dd|d< |dd|d< |dd|d< |d|d< |dd|d< |d|d< |d|d< |d|d< |d|d< |d|d< t | |}|r|d|d< |d|d< |S )Nrp  r  rb   r/  re   transcriptsrA  speaker_segmentsnum_speakersdurationr;  call_purposeobjections_concernsobjection_typer9  r:  
sentimentsparameter_scoresr   agent_speak_percentagecustomer_speak_percentagedead_air_percentager<  bant_profilebant_summary)r2  r3  r   r4  rp   r   r<   r   r   JSONDecodeErrorr5  r6  )rl   r  r   transcript_datar  analytics_data	bant_datar0   r0   r1   call_details  sF   
r  z/bant/{bid}/{callid}c                 C   r  )Nrp  zBANT not foundrb   )r2  r6  r   )rl   r  r1  r0   r0   r1   bant_details!  r  r  z /calls/{bid}/{callid}/transcriptc                 C   s"   t | | t | | d|dS )NzTranscript deleted successfully)rH   r  )r2  delete_transcriptreset_transcription_status)rl   r  r0   r0   r1   delete_call_transcript)  s   
r  z-/calls/{bid}/{callid}/segment/{segment_index}segment_indexc                 C   s@   t |dd }|stdddt| ||| d||dS )Nr  re   r+  zText cannot be emptyrb   zSegment updated successfully)rH   r  r  )r<   rp   ri   r   r2  update_speaker_segment_text)rl   r  r  r   new_textr0   r0   r1   patch_call_segment0  s
   r  z/calls/{bid}/recentc                 C   r  r*   )r2  get_recent_calls)rl   rA   r0   r0   r1   recent_calls9  r  r  z/calls/searchc                 C   sD   |  d}|  dd}t|  dd}|stdddt|||S )	Nrl   ri  re   rA   rU  r+  zBusiness ID is requiredrb   )rp   rE   r   r2  search_calls)r   rl   ri  rA   r0   r0   r1   calls_search>  s   
r  z/leads/{bid}transcripts_onlyc                 C   s8   t j| |||||d}|dg t|dd||dS )N)rl   r  rA   r  r  r  r  r  r   )r  r  rA   r  )r2  get_leads_listrp   rE   )rl   r  rA   r  r  r  rz  r0   r0   r1   
leads_listH  s   	"r	  z'/leads/{bid}/{lead_phone:path}/insightsr   c                 C   s(   t |}t| |}|stddd|S )zSReturn cached lead insights (unified across all calls) or 404 if not yet generated.rp  zFInsights not yet generated. POST to /insights/generate to create them.rb   )r	   r2  get_lead_insightsr   )rl   r   decoded_phonerz  r0   r0   r1   r
  W  s
   r
  z0/leads/{bid}/{lead_phone:path}/insights/generatec              
   C   s   ddl m} t|}z|t| |}d|dW S  ty* } ztdt|dd}~w tyH } ztj	d| ||dd	 td
d| dd}~ww )zITrigger AI generation of unified lead insights. Overwrites cached result.r   )generate_lead_insightsT)r  insightsr+  rb   Nz-Lead insights generation failed for %s/%s: %sexc_infor  Generation failed: )
lead_insightsr  r	   r2  r  r   r<   r   r  error)rl   r   r  r  r  r  r0   r0   r1   generate_lead_insights_endpointa  s   r  z4/leads/{bid}/{lead_phone:path}/rich-summary/generatert  c              
   C   s   ddl m} |d}|stdddz|t| |}d|dW S  ty3 } ztdt|dd	}~w tyQ } ztj	d
| ||dd tdd| dd	}~ww )zMGenerate and save a rich_summary for a specific callid. Body: {callid: '...'}r   )generate_and_save_rich_summaryr  r+  zcallid is requiredrb   T)r  rich_summaryNz,Rich summary generation failed for %s/%s: %sr  r  r  )
r  r  rp   r   r2  r  r<   r   r  r  )rl   r   rt  r  r  rz  r  r0   r0   r1   generate_call_rich_summaryp  s   
r  z&/leads/{bid}/{lead_phone:path}/profilec           
      C   s4  ddl }t|}tj| d|d}|pi dpi }t|tr5zddl}||}W n t	y4   i }Y nw i |dd |
 D }|pEi dpT|| d	  }	tj| d|	|d
pe|pbi d|dpp|pmi d|dp{|pxi d||dp|pi d|dp|pi d|d
 ddiS )z6Alias so PUT /profile also stays before the catch-all.r   Nr  rl   r[   r   lead_payloadc                 S   s&   i | ]\}}|d ur|dkr||qS r  r0   r   r0   r0   r1   r     s   & z-update_lead_profile_alias.<locals>.<dictcomp>external_lead_idz|manualr  	lead_namer   r  r  r   )
rl   r[   r  r  r   r  phone_primaryr  r   r  r  T)r  r	   r2  get_cached_crm_lead_by_phonerp   r   r<   r   r   r   r   r  r  r  upsert_crm_lead_cache)
rl   r   rt  _hlr  rc  existing_payload_jsonmerged_payloadeidr0   r0   r1   update_lead_profile_alias  s4   
&r#  z$/leads/{bid}/{lead_phone:path}/notesc                 C      t |}t| |}d|iS )Nnotes)r	   r2  get_lead_notes)rl   r   r  r%  r0   r0   r1   r&       r&  c                 C   sN   t |}|dp
d }|stddd|d}t| |||}|ddS )	Nr:  re   r+  zcontent is requiredrb   
created_byz
Note savedra  rH   )r	   rp   ri   r   r2  add_lead_note)rl   r   rt  r  r:  r(  note_idr0   r0   r1   r*    s   

r*  z/leads/{bid}/notes/{note_id}r+  c                 C   $   t || }|stdddddiS )Nrp  zNote not foundrb   rH   zNote deleted)r2  delete_lead_noter   )rl   r+  deletedr0   r0   r1   r-       r-  z$/leads/{bid}/{lead_phone:path}/tasksc                 C   r$  )Ntasks)r	   r2  get_lead_tasks)rl   r   r  r0  r0   r0   r1   r1    r'  r1  c                 C   s^   t |}|dp
d }|stddd|dpd }|d}t| ||||}|dd	S )
Nr  re   r+  ztitle is requiredrb   due_dater(  zTask createdr)  )r	   rp   ri   r   r2  add_lead_task)rl   r   rt  r  r  r2  r(  task_idr0   r0   r1   r3    s   

r3  z/leads/{bid}/tasks/{task_id}r4  c                 C   s6   t |dd}t|| |}|stdddddiS )NdoneFrp  Task not foundrb   rH   zTask updated)rD   rp   r2  update_lead_taskr   )rl   r4  rt  r5  updatedr0   r0   r1   r7    s
   r7  c                 C   r,  )Nrp  r6  rb   rH   zTask deleted)r2  delete_lead_taskr   )rl   r4  r.  r0   r0   r1   r9    r/  r9  z/leads/{bid}/{lead_phone:path}include_crmc                 C   s~  t |}tj| ||d}|stddddddd d}|r2t| d}|r2|dr2|d	r2|d
r2d|d< tj| d|d}|rSt|d|d dsSd }|r}d|d< |dp_i |d|d|d|d|d|dd|d< nzddlm	}	 |	|d |d	 |dpdd}
|

|}|dr|dr|d d }|d pd! }|d"pd! }|d# |  p|d$pd }|d%p|d&p|}dd l}|d'p||  }tj| d|||d(|d)||d*|d+	 d|d< |||d)||d*|d(|d,d|d< nd-|d.< W n ty1   d-|d.< Y nw ||d/< t|||d0< |S )1N)rl   r   r  rp  zLead not foundrb   Fr  )r   r   r[   r   r  r  r  Tr   r  r  )r  r   r  r   r  r  r  r  r   r   r  r   r   r    r  $https://api-in21.leadsquared.com/v2/r  r  r   r  re   r  r
  r  r  r  r  r  r  r  )	rl   r[   r  r  r   r  r  r  r  r  z"No CRM record found for this lead.rH   r   rD  )r	   r2  get_lead_detailsr   r  rp   r  r  leadsquared_servicer!   search_lead_by_phoneri   r  r  r  r  r  r   r   )rl   r   r  r:  r  r   r   r  r  r!   lsqlivelead_recr  r  r  	phone_valr  r"  r0   r0   r1   lead_detail  s   *
	
rC  z/analytics/{bid}/statsc                 C   r  r*   )analytics_serviceget_call_statisticsrl   r  r  r0   r0   r1   analytics_stats5     rG  z/analytics/{bid}/sentimentc                 C   r  r*   )rD  get_sentiment_distributionrF  r0   r0   r1   analytics_sentiment:  rH  rJ  z/analytics/{bid}/intentc                 C   r  r*   )rD  get_intent_distributionrF  r0   r0   r1   analytics_intent?  rH  rL  z/analytics/{bid}/trendsday   periodc                 C   r  r*   )rD  
get_trends)rl   rO  r^  r0   r0   r1   analytics_trendsD  rH  rQ  z/analytics/{bid}/agentsc                 C   r  r*   )rD  get_agent_performancerF  r0   r0   r1   analytics_agentsI  rH  rS  z/analytics/{bid}/leaderboardc                 C   s   t | |||}|t|dS )z7Agent leaderboard ranked by average call quality score.)leaderboardtotal_agents)r2  get_agent_leaderboardr   )rl   r  r  r  r   r0   r0   r1   analytics_leaderboardN  s   rW  z/analytics/{bid}/keywords   c                 C   s   t | |||S r*   )rD  get_top_keywords)rl   rA   r  r  r0   r0   r1   analytics_keywordsZ  s   rZ  z/analytics/{bid}/{callid}c                 C   r  )Nrp  z!Analytics not found for this callrb   )r2  r5  r   )rl   r  r0  r0   r0   r1   call_analyticsd  r  r[  z/queue-calls/{bid}c                 C   s0   t j| ddidd}dt| dt|| dS )Nr   r   r@   rQ  zQueued z calls for processing)rH   r  business_id)r2  r  r   )rl   r   r0   r0   r1   queue_callsl  s   r]  z/process-calls/{bid}c                 C   s   t dd| ddS )N   zProcessing started)rH   r\  r  )r   rl   r0   r0   r1   process_callsr  s   r`  z/transcripts/{bid}c                 C   
   t | S r*   )r2  get_transcriptsr_  r0   r0   r1   rb  w     
rb  z/export/{bid}/callsr   r(   c           
      C   s   dd |||d  D }tj| |dd}|dkrKt }|r2tj||d  d}|  || d	d
|  dt	
 d di}	t| d|	dS |S )Nc                 S      i | ]\}}|d ur||qS r*   r0   r   r0   r0   r1   r     r   z export_calls.<locals>.<dictcomp>)r   r  r  i'  rQ  csvr   )
fieldnamesContent-Dispositionzattachment; filename=calls__z%Y%m%d.csvtext/csv)r:  
media_typer   )r   r2  r  r   re  
DictWriterr  writeheader	writerowsr   r  strftimer   getvalue)
rl   r(   r   r  r  r  r   outputwriterr   r0   r0   r1   export_calls|  s   
 rs  z/webhook/call-updatec              
   C   s   |  d}|  d}|r|stdddt|||  di }|rOztj|ddd	|gd
 W ddiS  tyN } ztd||| W Y d }~ddiS d }~ww tddd)Nrl   r  r+  zbid and callid are requiredrb   r   Frf   TrA  z%RAG auto-ingest skipped for %s/%s: %srH   zCall updated successfullyr  zFailed to update call)	rp   r   r2  update_callrQ  rB  r   r  warning)r   rl   r  r  rag_excr0   r0   r1   webhook_call_update  s   

rw  z/webhook/conversation-summaryc                 C   sX   |  d}|  d}|  d}|r|stdddt|||}|s(tdddd	d
iS )Nr\  r  transfer_reasonr+  z#business_id and callid are requiredrb   r  zFailed to save summaryrH   zSummary saved successfully)rp   r   r2  save_conversation_summary)r   rl   r  rx  r  r0   r0   r1   webhook_conversation_summary  s   


rz  z/quality-parameters/{bid}c                 C   ra  r*   )quality_params_handlerget_parametersr_  r0   r0   r1   quality_parameters_list  rc  r}  c                 C   s   t | |pi }d|dS )NzParameter saved successfully)rH   parameter_id)r{  save_parameter)rl   r   r~  r0   r0   r1   quality_parameters_save  s   
r  z/data-capture-fields/{bid}c                 C      t | | t| }d|iS )uP   Field definitions for Settings → Data Capture Points and lead-insights schema.fields)rz   r2  get_data_capture_fields)rl   rk   r   r0   r0   r1   data_capture_fields_get  s   

r  c                 C   sL   t | | t| | t|tr|dng }t| |pg  dt| dS )Nr  T)r  r  )rz   r   r   r   rp   r2  replace_data_capture_fieldsr  )rl   rt  rk   r  r0   r0   r1   data_capture_fields_put  s
   

r  z /quality-parameters/{bid}/uploadrr  c              
      s   |s	t dddz| I d H }t| ||jpd}ddi|W S  ty5 } zt dt|dd }~w tyL } zt	d| t dd	dd }~ww )
Nr+  zNo file providedrb   re   rH   z(Quality parameters uploaded successfullyz$Quality parameters upload failed: %sr  z#Failed to upload quality parameters)
r   readr{  import_parameters_from_filerf  r  r<   r   r  	exception)rl   rr  r:  rz  er0   r0   r1   quality_parameters_upload  s   r  z$/quality-parameters/{bid}/{param_id}param_idc                 C   r  )Nrp  zParameter not foundrb   )r{  get_parameter_by_idr   )rl   r  	parameterr0   r0   r1   quality_parameter_get  r  r  c                 C   $   t | |}|stdddddiS )Nrp  z+Parameter not found or could not be deletedrb   rH   zParameter deleted successfully)r{  delete_parameterr   )rl   r  r  r0   r0   r1   quality_parameter_delete  r/  r  z /quality-parameters/{bid}/groupsc                 C   ra  r*   )r{  get_parameter_groupsr_  r0   r0   r1   quality_groups  rc  r  z%/quality-parameters/{bid}/total-scorec                 C   s   dt | iS )Ntotal_score)r{  calculate_total_possible_scorer_  r0   r0   r1   quality_total_score  rH  r  z /objection-classifications/{bid}business_typer  c                 C   s*   |d u rd nt | dk}t| ||S )Nr}   )r<   r   objection_handlerget_all_classifications)rl   r  r  activer0   r0   r1   objection_classifications  s   r  z4/objection-classifications/{bid}/{classification_id}classification_idc                 C   r  )Nrp  Classification not foundrb   )r  get_classification_by_idr   )rl   r  classificationr0   r0   r1   objection_get_by_id  r  r  c                 C   sB   |r| dstdddt| || dd}td|dd	d
S )Ncategory_namer+  zcategory_name is requiredrb   r(  r   r  z#Classification created successfullyr)  r  )rp   r   r  create_classificationr   )rl   r   r  r0   r0   r1   objection_create	  s   r  c              	   C   s8   t | ||pi |p
i dd}|stdddddiS )N
updated_byr   rp  z+Classification not found or no changes maderb   rH   z#Classification updated successfully)r  update_classificationrp   r   )rl   r  r   r  r0   r0   r1   objection_update	  s    r  c                 C   r  )Nrp  r  rb   rH   z#Classification deleted successfully)r  delete_classificationr   rl   r  r  r0   r0   r1   objection_delete	  r/  r  z;/objection-classifications/{bid}/{classification_id}/togglec                 C   r  )Nrp  r  rb   rH   z*Classification status toggled successfully)r  toggle_active_statusr   r  r0   r0   r1   objection_toggle	  r/  r  z'/objection-classifications/{bid}/searchqc                 C   r  r*   )r  search_classifications)rl   r  r0   r0   r1   objection_search%	  r  r  z7/objection-classifications/{bid}/by-severity/{severity}severityc                 C   s    |dvr
t dddt| |S )N)r@  r?  highcriticalr+  zInvalid severity levelrb   )r   r  get_classifications_by_severity)rl   r  r0   r0   r1   objection_by_severity*	  s   r  z)/objection-classifications/{bid}/classifyc                 C   s*   |pi  d}|stdddt| |S )Nobjection_textr+  zobjection_text is requiredrb   )rp   r   r  classify_objection)rl   r   r  r0   r0   r1   objection_classify1	  s   r  z+/objection-classifications/{bid}/statisticsc                 C   ra  r*   )r  get_statisticsr_  r0   r0   r1   objection_statistics9	  rc  r  z'/crm/{bid}/leadsquared/presales-mappingc                 C   s:   t | | t| |||d}t|drd|dS d|dS )N)r  r  r_   r  rN  r+  r  )rz   r  r   rp   )rl   r  r  r_   rk   r  r0   r0   r1   crm_presales_mapping>	  s   
 r  z"/crm/{bid}/leadsquared/integrationc                 C   s8   t | | t| d}|stdddd ddS d|dS )	Nr  rp  Fz!LeadSquared integration not found)r  rH   r   r  T)r  r   )rz   r2  get_crm_integrationr   )rl   rk   integrationr0   r0   r1   crm_lsq_get_integrationK	  s
   

r  c              
   C   s   t | | t| | t|dd }t|dd }t|dd p)d}t|dd}|r6|s<tdd	d
tj| d|||||dpJi d dddS )Nlsq_access_keyre   lsq_secret_keylsq_api_hostr;  r  Tr+  z.lsq_access_key and lsq_secret_key are requiredrb   r  config)rl   r[   r  r  r  r  r  z*LeadSquared integration saved successfullyr  rH   )	rz   r   r<   rp   ri   rD   r   r2  upsert_crm_integration)rl   r   rk   r  r  r  r  r0   r0   r1   crm_lsq_save_integrationT	  s$   


	r  z'/crm/{bid}/leadsquared/integration/testc                 C   s   t | | t| d}|r|dr|ds tdddddS t|d |d |d	d
}| }|dr?t| d |S td|dS )Nr  r  r  rp  Fz&LeadSquared integration not configuredr  r  r  r  r  r+  )rz   r2  r  rp   r   r!   test_connectionmark_crm_integration_tested)rl   rk   r  r  rz  r0   r0   r1   crm_lsq_test_integrationj	  s   

r  c                 C   s6   t | | t| | t| d}d|rddS ddS )Nr  Tz,LeadSquared integration deleted successfullyz%LeadSquared integration did not existr  )rz   r   r2  delete_crm_integration)rl   rk   removedr0   r0   r1   crm_lsq_delete_integrationx	  s   

r  z#/crm/{bid}/leadsquared/leads/searchc                 C   s    t | | t| }||pi S r*   )rz   r  r  )rl   r   rk   r  r0   r0   r1   crm_lsq_search_leads	  s   
r  z&/crm/{bid}/leadsquared/leads/{lead_id}r  c                 C   s<   t | | t| }||}t|drd|dS d|dS )Nr  rN  r+  r  )rz   r  get_leadr   rp   rl   r  rk   r  rz  r0   r0   r1   crm_lsq_get_lead	  s   

 r  z/crm/{bid}/leadsquared/leadsc                 C   sR   t | | t stdddt| }||pi }t|dr$d|dS d|dS Nrx   z!CRM write operations are disabledrb   r  rN  r+  r  )rz   r  r   r  create_leadr   rp   )rl   r   rk   r  rz  r0   r0   r1   crm_lsq_create_lead	  s   
 r  c                 C   sT   t | | t stdddt| }|||pi }t|dr%d|dS d|dS r  )rz   r  r   r  update_leadr   rp   )rl   r  r   rk   r  rz  r0   r0   r1   crm_lsq_update_lead	  s   
 r  c                 C   sN   t | | t stdddt| }||}t|dr"d|dS d|dS r  )rz   r  r   r  delete_leadr   rp   r  r0   r0   r1   crm_lsq_delete_lead	  s   

 r  z/audio/proxy/{bid}/{callid}c                    s|   t | |}|p	i d}|stdddtj|ddd  jdkr(td	d
d fdd}t|  jddddddS )NfileUrlrp  zAudio file not foundrb   Tr>  )streamr2  rN  r3  zFailed to fetch audio filec                  3   s"     j ddD ]} | r| V  qd S )Ni    )
chunk_size)iter_content)chunkr  r0   r1   _iter	  s   zaudio_proxy.<locals>._iterzContent-Typez
audio/mpegbyteszpublic, max-age=3600)zAccept-RangeszCache-Controlrk  r   )r2  r3  rp   r   r8  rc   r   r   )rl   r  r   file_urlr  r0   r  r1   audio_proxy	  s   
r  z/admin/usersc                 C   s6   |  dstdddt \}}t|td|idS )Nrm   rx   Master admin access requiredrb   r  r  )rp   r   rt   get_all_usersr   r   )rk   r  rc   r0   r0   r1   admin_users	  s   
r  z/admin/users/createc              
      s<  | dstddddD ]}|  |std| ddqtj| d | d	 | d
 |  d|  dd|  ddd\}}|dkr|d }|  dd}|  drw| d D ]"}t|tr`| dn|}	t|trm| d|n|}
t||	|
 qTtj| d| ddd| d  |jr|jj	nd |j
 dd t||dS )Nrm   rx   r  rb   r  r+  r  r`  r  r   r  r   rk   Fr  r  ra  rn   rl   r  r  r  r  r  )rp   r   rt   r  r   r   r  r  r  r   r   r   )r   r  rk   r  rz  rc   rG   default_rolerq   rl   r   r0   r0   r1   admin_users_create	  s@   






r  z/admin/businessesc                  C   s    t  \} }t|td| idS )Nrn   r  )rt   r  r   r   )rn   rc   r0   r0   r1   admin_businesses	  s   r  z/admin/businesses/createc              	      s   | dstddd|  I d H }| dr| ds#tdddtj|d |d | d	d
\}}|dkr_tj| d| ddd|d  d|d  d| jrV| jjnd | j dd t	||dS )Nrm   rx   r  rb   rl   r  r+  zbid and name are requiredr  rl   r  r  r  ra  r`  create_businesszCreated new business: z (ID: )r  r  r  )
rp   r   r   rt   r  r  r  r   r   r   )r  rk   r   rz  rc   r0   r0   r1   admin_businesses_create	  s"   
"
r  z!/admin/users/{user_id}/businessesc                 C   sX   | dstddd| dstdddtj| |d | dd	d
\}}t||dS )Nrm   rx   r  rb   rl   r+  r  r   rk   r  r  )rp   r   rt   r  r   )rG   r   rk   rz  rc   r0   r0   r1   admin_assign_business
  s   

 r  z/admin/onboard-businessc              
      s  | dstddd|  I dH }| dpd }|s$tdd	dt| d
p+d }|szKt *}| }|d dd |	 D }W d   n1 sTw   Y  W d   n1 scw   Y  dd |D }|rytt
|d nd}W n ty   ddl}	t|	dd}Y nw tj||| dd\}
}|dvrt||
 dddzt| W n ty } ztd| d|  W Y d}~nd}~ww tdd|  }tdd|dp|}|}d}	 zGt 8}| #}|d|f | s	 W d   W d   W n8W d   n	1 sw   Y  W d   n	1 s/w   Y  W n
 ty?   Y nw | d| }|d7 }qd }| d!}tj||||d"d#d$\}}|dvrot|| dd%d|d& }tj||d"d' tj| d&| d(d)d*| d+| d,| | jr| jjnd| j d-d. td/||| d| d0| d1| d2| d3g|||d4d5d6S )7zMOne-shot business onboarding: create business + login credentials + 4 tables.rm   rx   r  rb   Nr  re   r+  zname is requiredrl   zSELECT bid FROM businessesc                 S   s   g | ]}|d  qS r_  r0   r   rr0   r0   r1   r   #
  r   z*admin_onboard_business.<locals>.<listcomp>c                 S   s    g | ]}t | rt|qS r0   )r<   r   rE   )r   br0   r0   r1   r   $
  r  rf   1000r   rs  i'  r  r  )rN  r  r  zFailed to create businessz Could not create bant table for : z	[^a-z0-9]rh  z_+Tz1SELECT id FROM business_users WHERE username = %s1234z@pcaa.localr   Fr  zFailed to create userra  r  r`  onboard_businesszOnboarded business: z (BID: z	), user: r  r  r  r_  _sarvamresponser`  _bant)r`  r   r  )rl   r  r  tables_createdcredentialsr  )rp   r   r   ri   r<   r2  r  r   r   r   r  r   randomrandintrt   r  ensure_bant_tabler  ru  r  r  r   re  r  r  r  r  r   r   r   )r  rk   r   r  rl   r   r  rc  numericr  
biz_result
biz_statusr  base_usernamer`  suffixdefault_passwordr  user_resultuser_statusnew_user_idr0   r0   r1   admin_onboard_business
  s   



"






"
r  z+/admin/businesses/{bid}/orchestration/startc                    s&  | dstddd| I dH }t| dd}t| dd}t| d	d
}t| dd}tjtjt	}tj
|d|  d}	d|  d}
td|  d\}}}|rdtdd|ddS |rhdnd}|d
krsd| nd}d|  d| d|
 d|  d|  d|  d|  d| d| d | d | d!|  d"}t|	d#}|| W d   n1 sw   Y  d
dl}t|	t|	j|jB |jB |jB  tjd$|	g|tjtjd%d&}tj| d'| d(d)d*|  d+|j d,| d-| d.| d/|jr|jjnd|j d0d1 td%|j| |	d2dS )3zHGenerate and start a per-BID orchestration loop as a background process.rm   rx   r  rb   Ntranscribe_limitrX  analyze_limitmax_per_dayr   skip_ingestForchestrator_loop_.sh/tmp/orchestrator_loop_.lockalready_runningr"  reasonpidr:  z--skip-ingestre   z--max-per-day z8#!/bin/bash
# Auto-generated orchestration loop for BID z
cd z

LOCKFILE="z"
if [ -f "$LOCKFILE" ]; then
    PID=$(cat "$LOCKFILE")
    if ps -p $PID > /dev/null 2>&1; then
        echo "Orchestrator for BID z already running (PID: $PID)."
        exit 1
    fi
fi
echo $$ > "$LOCKFILE"
trap "rm -f $LOCKFILE" EXIT

echo "Starting orchestration loop for BID z.."
while true; do
    echo "--- [$(date)] BID zL orchestration run ---"
    python3 orchestrate_pipeline.py \
        --bid z9 \
        --ingest-limit 0 \
        --transcribe-limit z \
        --analyze-limit z \
        z
    echo "--- [$(date)] BID z( sleeping 5 min ---"
    sleep 300
done
wbashTcwdstdoutstderrstart_new_sessionra  r`  start_orchestrationz#Started orchestration loop for BID  (PID z, transcribe=z
, analyze=z, max_per_day=r  r  r  )r"  r  rl   script)rp   r   r   rE   rD   r   r  dirnameabspath__file__r   _check_processr   openwritestatchmodst_modeS_IEXECS_IXGRPS_IXOTH_subprocessPopenDEVNULLrt   r  r  r  r   r   )rl   r  rk   r   r  r  r  r  base_dirscript_pathlockfiler  existing_pidrh  	skip_flagmax_per_day_flagscript_contentf_statprocr0   r0   r1   start_business_orchestrationm
  sz   
&$
r4  z,/admin/businesses/{bid}/orchestration/statusc                 C   sb   t d|  d\}}}d|  d}tjtjt}tj|d|  d}| ||tj|dS )z>Check if the orchestration loop for a specific BID is running.r  r	  r
  r  )rl   runningr  script_exists)r  r   r  r  r  r  r   exists)rl   rk   r5  r  cmdr,  r*  r+  r0   r0   r1   !get_business_orchestration_status
  s   
r9  z*/admin/businesses/{bid}/orchestration/stopc              
   C   s   | dstdddtd|  d\}}}|r|s"tddd	d
S z,ddl}t||j tj	| d| ddd|  d| dddd td|dd
W S  t
y_   tddd	d
 Y S  tyr } ztdt|dd}~ww )z/Stop the orchestration loop for a specific BID.rm   rx   r  rb   r  r	  Fnot_running)stoppedr  r  r   Nra  r`  stop_orchestrationz#Stopped orchestration loop for BID r  r  r  T)r;  r  process_not_foundr  )rp   r   r  r   signalr   killSIGTERMrt   r  ProcessLookupErrorr   r<   )rl   rk   r5  r  rh  r>  r  r0   r0   r1   stop_business_orchestration
  s*   
rB  z/admin/activity-logr  c                 C   s:   | dstdddtj| ||d\}}t|d|idS )Nrm   rx   r  rb   )rA   rG   r  logsr  )rp   r   rt   get_activity_logr   )rA   rG   r  rk   rC  rc   r0   r0   r1   admin_activity_log
  s   
rE  z/api/embed/tokenc           	      C   s   | pi  d}| p
i  d}|r|stdddt|t|}|s(tddd|j d}| d	rM|rMd
d t|d	 dD }||vrMtdddtjt||d |d d}t dd}|| d| dt||d dS )Napi_keyrl   r+  zapi_key and bid are requiredrb   ra   z1Invalid API key or unauthorized for this businessOriginallowed_originsc                 S      g | ]}|  qS r0   ri   )r   or0   r0   r1   r   
  r   zembed_token.<locals>.<listcomp>,rx   zOrigin not allowedpartner_namera  )rl   rM  
api_key_idEMBED_BASE_URLzhttp://localhost:6174z/#/embed?token=i  )rv   	embed_url
expires_inrl   rM  )	rp   r   rt   validate_api_keyr<   r   r  generate_embed_tokenr   )	r   r  rF  rl   
key_recordoriginallowedrv   embed_base_urlr0   r0   r1   embed_token
  s   "rX  z/api/embed/validaterv   c                 C   s.   t | }|stdddd|d |d dS )Nra   zInvalid or expired embed tokenrb   Trl   rM  )validrl   rM  )rt   validate_embed_tokenr   )rv   rz  r0   r0   r1   embed_validate   s   
r[  z/admin/embed-keysc              	   C   s   | dstddd|  d}|  d}|r|stdddtjt|||  d	|  d
d\}}tj|d |d dd| d| d|jrJ|jjnd |j dd t	||dS )Nrm   rx   r  rb   rl   rM  r+  z!bid and partner_name are requiredrH  
expires_at)rl   rM  rH  r\  ra  r`  create_embed_keyz#Created embed API key for business z (partner: r  r  r  r  )
rp   r   rt   create_embed_api_keyr<   r  r  r   r   r   )r   r  rk   rl   rM  rz  rc   r0   r0   r1   admin_embed_keys_create  s*   




r_  c                 C   s6   | dstdddtj| d\}}t|d|idS )Nrm   rx   r  rb   r_  r  r  )rp   r   rt   list_embed_api_keysr   )rl   rk   r  rc   r0   r0   r1   admin_embed_keys_list!  s   
ra  z/admin/embed-keys/{key_id}key_idc              	   C   sj   | dstdddt| \}}tj|d |d dd|  |jr&|jjnd |j d	d
 t||dS )Nrm   rx   r  rb   ra  r`  revoke_embed_keyzRevoked embed API key r  r  r  )	rp   r   rt   revoke_embed_api_keyr  r  r   r   r   )rb  r  rk   rz  rc   r0   r0   r1   admin_embed_keys_revoke)  s   

re  CALL_SYNC_CACHE_TTL_SECONDS3600v5
table_kindc                 C   s\   |dkr| d| dgn	| d| dg}|D ]}|  d|f |  r+|  S qd S )Nhistory_callhistory_call_history_callarchive_call_archiveSHOW TABLES LIKE %sr   re  )r   rl   ri  
candidates	candidater0   r0   r1   _resolve_source_call_table=  s   0rs  
table_namecolumn_namec                 C   s"   |  d| d|f |  d uS )NzSHOW COLUMNS FROM `z	` LIKE %srp  )r   rt  ru  r0   r0   r1   _table_has_columnF  s   rv  r   c                 C   s4   i }|   D ]\}}t|tr| n|||< q|S r*   )r   r   r   r   )r   rq  r/   r{   r0   r0   r1   _serialize_call_sync_rowK  s   rw  r  c                 C   st  t  }tjdi |}z|tjj}t|| |}|s$g d fW |  S t||d}	t||d}
t||d}t||d}t||d}g }|
rK|	d |	rV|rV|rV|	d |r]|	d |rd|	d	 |rod
d
| dnd}d| d| d}t| g}|r|r|d7 }|||g |d7 }|	t| ||t| | pg }dd |D |fW |  S |  w )Nr  r  callfromcalltoclicktocalldid1NULLIF(TRIM(CAST(customer_callinfo AS CHAR)), '')CASE WHEN LOWER(TRIM(CAST(direction AS CHAR))) = 'outbound' THEN NULLIF(TRIM(CAST(callto AS CHAR)), '') WHEN LOWER(TRIM(CAST(direction AS CHAR))) = 'inbound' THEN NULLIF(TRIM(CAST(callfrom AS CHAR)), '') ELSE NULL END&NULLIF(TRIM(CAST(callto AS CHAR)), '').NULLIF(TRIM(CAST(clicktocalldid AS CHAR)), '')	COALESCE(, r  NULLz
            SELECT
                callid, %s as bid, agentname, groupname, starttime, endtime, dialstatus, direction, filename,
                zP as customer_callinfo, countrycode, emp_phone, clicktocalldid
            FROM `z"`
            WHERE 1 = 1
        & AND DATE(starttime) BETWEEN %s AND %s! ORDER BY starttime DESC LIMIT %sc                 S   r   r0   rw  r  r0   r0   r1   r     r   z'_fetch_source_calls.<locals>.<listcomp>r0   )r   r   r   r   r   r   rs  r   rv  r  r   r<   extendrE   r   tupler   )rl   ri  r  r  rA   source_configr   r   rt  has_directionhas_customer_callinfohas_callfrom
has_calltohas_clicktocallcustomer_candidatescustomer_exprri  paramsr   r0   r0   r1   _fetch_source_callsR  sP   
(



r  c                 C   s   t  }tjdi |}zI|tjj}t|| |}|s"W |  dS d| d}g }	|r9|r9|d7 }|	||g |	|t
|	 | pFi }
t|
dpNdW |  S |  w )Nr   zSELECT COUNT(*) as total FROM `z` WHERE 1 = 1r  r  r0   )r   r   r   r   r   r   rs  r   r  r   r  re  rE   rp   )rl   ri  r  r  r  r   r   rt  ri  r  r   r0   r0   r1   _fetch_source_count  s"   
r  z/sync/cache/{bid}/historyrefreshc           	      C   s   t | | t| | dt d|  d| }|s$t|}|r$d|d< |S t| d|d\}}|sRdd d	g t  d
d|  d|  dgt	 
dt	 
ddd	S d|t||t  d
d}tj|| d|td |S )N	callsync:r  :history:limit:T	cache_hitrj  rQ  call_historyr   Fz5Source call history table not found for this businessrl  rk  r   r   r   r   )	sourcetabler  records	cached_atr  rH   expected_tablesconfigured_source_db)r  r  r  r  r  r  r  rl   r  r   ttl_secondsrz   r   CALL_SYNC_CACHE_SCHEMA_VERSIONr2  get_call_sync_cacher  r   r  r   r   rp   r   upsert_call_sync_cacherf  )	rl   rA   r  rk   r  r  r  rt  r   r0   r0   r1   sync_cache_history  s:   



r  z/sync/cache/{bid}/archivec                 C   s  t | | t| | |r|r| d| nd}dt d|  d| d| }|s4t|}|r4d|d< |S t| d|||d	\}	}
|
sfd
d dg ||t  dd|  d|  dgt	 
dt	 
dddS d
|
t|	|	||t  dd}tj|| d
|td |S )Nr  recentr  	:archive::limit:Tr  archiver  r  rA   call_archiver   Fz5Source call archive table not found for this businessrm  rn  r   r   r  )r  r  r  r  r  r  r  r  rH   r  r  )r  r  r  r  r  r  r  r  r  r  )rl   r  r  rA   r  rk   	range_keyr  r  r  rt  r   r0   r0   r1   sync_cache_archive  sP   
	




r  z/sync/check-count/{bid}c                 C   sZ   t | | t| | t| d}|r|rt| d||dnd}d|id|it|| ||dS )Nrj  r  )r  r  r   r  )callhistorycallarchiver  r  r  )rz   r   r  rE   )rl   r  r  rk   history_totalarchive_totalr0   r0   r1   sync_check_count  s   



r  z/sync/calls/{bid}c              
   C   s$  t | | t| | |d}|d}t|dd}|r?|r?t| d|||d\}}d}dt d	|  d
| d	| d| 
}	nt| d|d\}}d}dt d	|  d| }	||t||||t 	 d}
|rqt
j|	| ||
td |sd| d|d |	ddS dt| d| t||||	ddS )Nr  r  rA   r  r  r  r  r  r  r  r  rj  rQ  r  r  )r  r  r  r  r  r  r  r  zSource table not found for r   mysql)rH   cached_countr  r  r  	stored_inzSuccessfully cached z calls from )rz   r   rp   rE   r  r  r   r   r  r   r2  r  rf  )rl   r   rk   r  r  rA   r  rt  r  r  payload_objr0   r0   r1   
sync_calls  s>   



$
	&r  z10.40.180.35r   root4Tq73tXMcUbEJ5Q3t3	mcube_cl1r   r   _MCUBE2_DB_CONFIG	db_config
source_bid
directionsdialstatusesc                 C   s   t jdi | }z|t jj}	t|	||}
|
s&|dkrdnd}t|	||}
|
s1g dfW |  S t|	|
d}t|	|
d}t|	|
d}t|	|
d}t|	|
d}g }|rX|d	 |rc|rc|rc|d
 |rj|d |rq|d |r|dd	| dnd}d| d|
 d}t
|g}|r|r|d7 }|||g |rd	dgt| }|d| d7 }|| |r|rd	dgt| }|d| d7 }|dd |D  |d7 }|t| |	|t| |	 pg }dd |D |
fW |  S |  w )z=Like _fetch_source_calls but uses an explicit DB config dict.rj  r  Nr  r  rx  ry  rz  r{  r|  r}  r~  r  r  r  r  z
            SELECT callid, %s as bid, agentname, groupname, starttime, endtime,
                   dialstatus, direction, filename,
                   zc as customer_callinfo,
                   countrycode, emp_phone, clicktocalldid
            FROM `z` WHERE 1=1
        r  %sz AND dialstatus IN (z AND LOWER(direction) IN (c                 S   rI  r0   )r   r   dr0   r0   r1   r   ~  r   z3_fetch_source_calls_with_config.<locals>.<listcomp>r  c                 S   r   r0   r  r  r0   r0   r1   r     r   r0   )r   r   r   r   r   rs  r   rv  r  r   r<   r  r   rE   r   r  r   )r  r  ri  r  r  rA   r  r  r   r   rt  alt_kindr  r  r  r  r  r  r  ri  r  placeholdersr   r0   r0   r1   _fetch_source_calls_with_config@  sd   
1




r  z/telephony/{bid}/integrationsc                 C   r  )Nintegrations)rz   r2  get_telephony_integrations)rl   rk   r  r0   r0   r1   telephony_list  s   

r  c           
   
      s&  t | | |d}|stdddt|dp| }i }|dkrzz0tjdi tddi}| }|d	| d
f |	 }|
  |sQtdd| ddW n tyZ     typ }	 ztddt|	 dd }	~	ww dd t D }tj| |||d d| d|  dtd  ddS )Nr[   r+  zprovider is requiredrb   r  mcube2connect_timeout   ro  rk  zTable zA_callhistory not found on Mcube 2.0 server. Check the source BID.r  z'Could not connect to Mcube 2.0 server: c                 S   s   i | ]\}}|d kr||qS )r   r0   r   r0   r0   r1   r     r   z%telephony_connect.<locals>.<dictcomp>)r  r  Tz connected for BID z
 (server: r   r  r  r0   )r   rp   r   r<   r   r   r  r   r   re  r   r   r   r2  save_telephony_integration)
rl   r   rk   r[   r  r  r   r  foundr  r0   r0   r1   telephony_connect  s:   


 r  z(/telephony/{bid}/integrations/{provider}r[   c                 C   s&   t | | t| | d| ddS )NTz disconnectedr  )r   r2  delete_telephony_integration)rl   r[   rk   r0   r0   r1   telephony_disconnect  s   
r  z/telephony/{bid}/preview)rs   rP  z1Comma-separated directions, e.g. Outbound,inbound)rs   r  zBComma-separated statuses, e.g. ANSWER,BUSY. Omit for all statuses.
dialstatusc                 C   s  t | | t| | t| }dd |D }|sg dd ddS |d }	|	d }
|	dp-| }|r:dd |d	D nd }|rHd
d |d	D nd }|
dkr|rT|rTdnd}z"tt|||||||d\}}|t||
||td ||||dd	W S  t	y } zt
ddt| dd }~ww |
dkrzt||||}|t||
|| ddW S  t	y } zt
ddt| dd }~ww t
dd|
 d)Nc                 S   s   g | ]	}| d r|qS )r  rF  )r   ir0   r0   r1   r     r  z%telephony_preview.<locals>.<listcomp>r   zSNo telephony provider connected. Go to Telephony Integration and connect one first.)r  r  r[   r  r[   r  c                 S      g | ]
}|  r|  qS r0   rJ  r  r0   r0   r1   r     ri  rL  c                 S   r  r0   rJ  )r   sr0   r0   r1   r     ri  r  r  rj  )r  r  r   )r  r  )	r  r  r[   r  r  serverr  r  r  r  z Failed to fetch from Mcube 2.0: rb   mcube_classicr_  )r  r  r[   r  r  z$Failed to fetch from Mcube Classic: r+  z$Preview not supported for provider: )rz   r   r2  r  rp   r  r  r  r   r   r   r<   get_raw_calls_preview)rl   r  r  rA   r  r  rk   r  r  r  r[   r  r  r  ri  r  rt  r  rz  r0   r0   r1   telephony_preview  sZ   






r  z/telephony/{bid}/sync-to-dbc                    s\   t | | t| | |dg }|sddddS t| |}d|d  d|  d|d	< |S )
Nr  r   zNo records provided)insertedskippedrH   zPushed r  z records to r_  rH   )rz   r   rp   r2  insert_raw_calls_from_telephony)rl   r   rk   r  rz  r0   r0   r1   telephony_sync_to_db  s   

r  z/sync/upload/{bid}c                    s"   t | | t| | tddd)Ni  zExcel multipart upload requires python-multipart in this runtime. Use admin JSON business create path or install python-multipart.rb   )rz   r   r   )rl   r  rk   r0   r0   r1   sync_upload_raw_calls  s   

r  z/transcription/calls/{bid}r   r  
start_dateend_datec                 C   s   t  }zo| }dg}g }	|dkr|d |	| |r'|d |	| |r3|d |	| |r?|d |	| |rK|d |	| d|  d	d
| d}
||
t|	 | pdg }t|t|dW |	  S |	  w )Nzcall_status = 'ANSWER'allzstatus = %szgroupname = %szagentname = %szDATE(call_starttime) >= %szDATE(call_starttime) <= %sz
            SELECT callid, bid, agentname, groupname, call_starttime, call_endtime, call_status, status,
                   transcription_status, transcription_requested, selected_for_processing
            FROM `z_raw_calls`
            WHERE  AND zH
            ORDER BY call_starttime DESC
            LIMIT 500
        )r   r  )
r   r   r  r   r   r  r   r   r   r   )rl   r   r  r  r  r  r   r   wherer  ri  r   r0   r0   r1   transcription_calls  s:   	









r  z/transcription/trigger/{bid}c                 C   s   | dg }|stdddt }z&| }ddgt| }d|  d| d	}||| |j}W |  n|  w d
| d|dS )NrC   r+  No call IDs providedrb   rL  r  z
            UPDATE `z_raw_calls`
            SET transcription_requested = 1, transcription_status = 'pending', selected_for_processing = 1
            WHERE callid IN (z
)
        zSuccessfully queued z calls for transcription)rH   queued_count)	rp   r   r   r   r   r   r   rowcountr   )rl   r   rC   r   r   r  ri  affectedr0   r0   r1   transcription_trigger>  s    r  z%/transcription/trigger-combined/{bid}c                 C   s4   | dg }t| d|i}t| d|i}d||dS )NrC   zCombined trigger executed)rH   transcriptionr  )rp   r  analysis_trigger)rl   r   rC   tar0   r0   r1   transcription_trigger_combinedS  s   r  z&/transcription/status/{bid}/{batch_id}batch_idc                 C   s   t | t |dddS )N	completedz-Legacy batch status endpoint is now immediate)rl   r  r   rH   )r<   )rl   r  r0   r0   r1   transcription_status[  s   r  z/analysis/trigger/{bid}c                 C   s  | dg }|stdddzddlm} |t}W n ty/ } z	tdd| dd }~ww d}d}g }|D ]}	zmt| |	}
|
sQ||	 d	 |d
7 }W q8|
 ddkrf||	 d |d
7 }W q8|
 d}|s{||	 d |d
7 }W q8|
 d}|rt	|t
rt|}|
 dp|
 d}|j| |	||pg |d |d
7 }W q8 ty } z||	 d|  |d
7 }W Y d }~q8d }~ww d| d| d||d}|r|d d |d< |S )NrC   r+  r  rb   r   )CallAnalyzerr  zFailed to initialize analyzer: z: Call not foundrf   r   r   z: Call not answeredr  z: No transcript availabler  r  duration_seconds)rl   r  r/  r  actual_durationr  zAnalysis completed: z successful, z failed)rH   success_counterror_countr   r	  )rp   r   analyze_calls_with_parametersr  r   r   r2  r  r  r   r<   r   r   analyze_call)rl   r   rC   r  analyzerr  r  r  r	  r  	call_datar/  r  r  response_datar0   r0   r1   r  `  sX   


r  c                   @   s   e Zd ZU dZee ed< dZee ed< dZ	ee
 ed< dZee ed< dZee ed< dZee ed< dZee ed< dZee ed	< dZee
 ed
< dZee
 ed< dZee
 ed< dZee
 ed< dZee ed< dZee ed< dZee
 ed< dS )PipelineConfigRequestNpipeline_enabledsource_db_hostsource_db_portsource_db_usersource_db_passwordsource_db_namestt_providerstt_api_keymin_call_duration_s
sync_batchtranscribe_batchsync_interval_slead_filter_enabledcrm_providerlookback_days)r8   r9   r:   r  r   rD   r=   r  r<   r  rE   r  r  r   r  r  r  r  r  r  r  r  r	  r0   r0   r0   r1   r    s    
 r  z/pipeline/{bid}/configc                 C   s   t | | t  t| }|s| ddS t|}dD ]}||r&d||< qdD ]}t||dr;||  ||< q)d|d< |S )	NF)rl   r7  )source_db_password_encstt_api_key_encz***
created_at
updated_atr   Tr7  )rz   r2  %ensure_business_pipeline_config_tableget_pipeline_configr   rp   hasattrr   )rl   rk   r.   safer   colr0   r0   r1   r    s    



r  c                 C   sD   t | | t  dd |  D }t| | dd|  dS )Nc                 S   rd  r*   r0   r   r0   r0   r1   r     r   z(save_pipeline_config.<locals>.<dictcomp>TzPipeline config saved for bid r  )r   r2  r  r   r   save_pipeline_config)rl   rt  rk   r   r0   r0   r1   r    s
   
r  z/pipeline/{bid}/statusc           
      C   s  t | | t  t  t| }t| d|  }t| d}i }z>t|  |  d}t &}| }|	d| d |
 pEg D ]
}	|	d ||	d < qFW d   n1 s[w   Y  W n	 tyj   Y nw | |rvt|d	d
nd
|r}t|nd|rt|ndd|dS )zLReturn pipeline operational status: watermarks, pending counts, last errors.
call_sync_	lsq_leads_call_recordsz9SELECT status, COUNT(*) AS cnt
                    FROM `z` GROUP BY statuscntr   Nr  F)	call_syncr  )rl   r  
watermarkscall_record_counts)rz   r2  r  ensure_sync_watermarks_tabler  get_sync_watermarkensure_call_records_tabler  r   r   r   r   rD   rp   r<   )
rl   rk   r.   call_sync_wmlsq_leads_wmcountsr  r   r   r   r0   r0   r1   get_pipeline_status  s@   




r"  c                   @   s   e Zd ZU eed< dZee ed< dZee ed< dZ	ee ed< dZ
ee ed	< dZee ed
< dZee ed< dZee ed< dZee ed< dS )AgentConfigRequest
agent_nameTagent_enabledr+  model_provideramazon.nova-lite-v1:0model_idNrZ   user_prompt_templateoutput_schemag?r   i   
max_tokens)r8   r9   r:   r<   r=   r%  r   rD   r&  r(  rZ   r)  r*  r   rQ   r+  rE   r0   r0   r0   r1   r#    s   
 r#  z/agents/{bid}c                 C   sZ   t | | t  t| }|D ]}dD ]}t||dr&||  ||< qq| |dS )Nr  r   )rl   r  )rz   r2  "ensure_business_agent_config_tableget_agent_configsr  rp   r   )rl   rk   configsr.   r  r0   r0   r1   list_agent_configs  s   


r/  c                 C   sB   t | | t  dd |  D }t| |}d||jdS )Nc                 S   rd  r*   r0   r   r0   r0   r1   r     r   z1create_or_update_agent_config.<locals>.<dictcomp>T)r  ra  r$  )r   r2  r,  r   r   save_agent_configr$  )rl   rt  rk   r   row_idr0   r0   r1   create_or_update_agent_config  s
   
r2  z/agents/{bid}/{agent_name}r$  c                 C   s<   t | | t| |}|stdd| d|  dd|dS )Nrp  zAgent 'z' not found for bid rb   T)r  r.  )r   r2  delete_agent_configr   )rl   r$  rk   r.  r0   r0   r1   r3    s
   

r3  z/call-records/{bid})rs   rO  r  	page_sizesearchc	              
   C   sl   t | | zt|  tj| |||||||d}	|	W S  ty5 }
 ztjd| |
dd tdddd }
~
ww )N)rl   r  r4  r   r$  r5  r  r  z"list_call_records error bid=%s: %sTr  r  zFailed to fetch call recordsrb   )rz   r2  r  get_call_records_listr   r  r  r   )rl   r  r4  r   r$  r5  r  r  rk   rz  r  r0   r0   r1   list_call_records/  s&   


r7  z!/call-records/{bid}/{callid:path}c              
   C   s   t | | zt|  t| |}|stdd| dd|W S  ty'     tyB } ztjd| ||dd tdd	dd }~ww )
Nrp  zCall record z
 not foundrb   z1get_call_record_detail error bid=%s callid=%s: %sTr  r  zFailed to fetch call record)rz   r2  r  get_call_record_detailr   r   r  r  )rl   r  rk   r  r  r0   r0   r1   r8  N  s   

r8  c                   @   sn   e Zd ZU dZee ed< dZee ed< dZ	ee ed< dZ
ee ed< dZee ed< dZee ed< dS )SttPipelineConfigUpdateNenabledraw_calls_id_colraw_calls_url_col
batch_sizepoll_interval_sr%  )r8   r9   r:   r:  r   rD   r=   r;  r<   r<  r=  rE   r>  r%  r0   r0   r0   r1   r9  f  s   
 r9  r.   c              	   C   sZ   | d }t j|d}i | t| d|dd|dd|dd|ddd	d
S )Nrl   r_  r:  pendingr   
processingr5  failed)r?  r@  r5  rA  )r:  	job_stats)r2  get_stt_job_statsrD   rp   )r.   rl   r  r0   r0   r1   _stt_bid_row_with_statso  s   



rD  z/stt-pipeline/bidsc                 C   s   |  dstdddz;dd t D }t }|D ]}||vr/|ddd	d
dd d d d	||< qdd t| dd dD }|t|dW S  ty` } zt	j
d|dd tdddd }~ww )Nrm   rx   r  rb   c                 S   s   i | ]}|d  |qS r_  r0   )r   cr0   r0   r1   r         z*stt_pipeline_list_bids.<locals>.<dictcomp>Fra  recording_urlr   r1  )	rl   r:  r;  r<  r=  r>  r%  r  r  c                 S   r   r0   )rD  r   r0   r0   r1   r     r   z*stt_pipeline_list_bids.<locals>.<listcomp>c                 S   r  )Nrl   r0   r  r0   r0   r1   r    r  z(stt_pipeline_list_bids.<locals>.<lambda>)r/   )bidsr  z stt_pipeline_list_bids error: %sTr  r  z Failed to list STT pipeline bids)rp   r   r2  get_stt_pipeline_bid_configsdiscover_stt_raw_call_bidsr  r  r   r   r  r  )rk   r.  
discoveredrl   rz  r  r0   r0   r1   stt_pipeline_list_bids~  s2   

rL  z/stt-pipeline/bids/{bid}/togglec              
   C   s   | dst|| stdddt| dd}zt| | | |dW S  ty? } ztjd| |d	d
 tdddd }~ww )Nrm   rx   (Business admin or master access requiredrb   r:  F)rl   r:  z(stt_pipeline_toggle_bid error bid=%s: %sTr  r  z!Failed to toggle STT pipeline bid)	rp   r   r   rD   r2  toggle_stt_pipeline_bidr   r  r  )rl   rt  rk   r:  r  r0   r0   r1   stt_pipeline_toggle_bid  s   rO  z/stt-pipeline/bids/{bid}/configc              
   C   s   | dst|| stddddd |  D }zt| | t| p,d| i|}t|W S  t	yL } zt
jd| |d	d
 tdddd }~ww )Nrm   rx   rM  rb   c                 S   rd  r*   r0   r   r0   r0   r1   r     r   z.stt_pipeline_update_config.<locals>.<dictcomp>rl   z+stt_pipeline_update_config error bid=%s: %sTr  r  z$Failed to update STT pipeline config)rp   r   r   r   r   r2  upsert_stt_pipeline_bid_configget_stt_pipeline_bid_configrD  r   r  r  )rl   rt  rk   r   r.   r  r0   r0   r1   stt_pipeline_update_config  s   
rR  z/stt-pipeline/statsc              
   C   s   |  dstdddz!t }| dd| dd| dd| d	dt| d
W S  tyF } ztjd|dd tdddd }~ww )Nrm   rx   r  rb   r?  r   r@  r5  rA  )r?  r@  r5  rA  r  zstt_pipeline_stats error: %sTr  r  z Failed to get STT pipeline stats)	rp   r   r2  rC  r   r  r   r  r  )rk   r  r  r0   r0   r1   stt_pipeline_stats  s   





rS  keywordc                 C   s   zGt jddgddd}|j D ]4}| |v rDd|vrD|dd}t|dkr,t|d nd}t|dkr:|d  n|}d||f  W S qW d	S  tyQ   Y d	S w )
zFReturn (running, pid, command) for the first process matching keyword.psauxT)capture_outputr  grepNr   rf   )FNN)	r'  runr  
splitlinesr  r   rE   ri   r   )rT  rz  linepartsr  r8  r0   r0   r1   r    s   r  
queue_namec           	   
   C   s   z1ddl }tdd}||j|dd}| }|j| dd}|jj}|jj	}|
  ||dfW S  tyJ } zddt|fW  Y d}~S d}~ww )	zCReturn (message_count, consumer_count, error) for a RabbitMQ queue.r   NRABBITMQ_HOST	localhost   r   socket_timeoutT)queuepassive)pikar   r   BlockingConnectionConnectionParameterschannelqueue_declaremethodmessage_countconsumer_countr   r   r<   )	r]  re  rmq_hostr   r   r  msgs	consumersr  r0   r0   r1   _get_rabbitmq_queue_info  s   rp  r  c                 C   sr   z.t j| }|dk rd| dfW S |dk r"d|d ddfW S d|d d ddfW S  ty8   Y dS w )	Ni   Tz Bi   z.1fz KBz MB)Fr   )r   r  getsizer   )r  sizer0   r0   r1   _log_size_str  s   rs  z/pca/statusc               
   C   s"  t d\} }}t d\}}}t d\}}}|st d\}}}dddd|||dd	d
dd| |o0|  ||ddddd|||dg}	td\}
}}dd|
||dg}i }zgt Y}| E}|d | }d}i }|D ](}tt|t	rw|d n|d }t
t|t	r|d n|d }|||< ||7 }qk||d}W d   n1 sw   Y  W d   n1 sw   Y  W n ty } zdt|i}W Y d}~nd}~ww tjtjt}dddddd d!d"d#dg}|D ]}ttj||d$ \}}||d%< ||d&< q|	|||t  d' d(S ))uH   PCA Master Panel — live status of pipeline jobs, queues, DB, and logs.orchestrate_pipeline.pyorchestrator_loop.shrun.py --workerrabbitmq_transcription_workerzOrchestrator Looporchestrator-loopz,Runs orchestrate_pipeline.py every 5 minutes)r  job_keyr  r  r5  r  commandzOrchestrate Pipelineorchestrate-pipelinez2Ingests calls, queues STT jobs, triggers analytics)r  ry  r  r  r5  	scheduledr  rz  zSTT Worker (run.py)
stt-workerz<Consumes RabbitMQ jobs and calls Sarvam AI for transcriptionstt_jobsz'STT transcription job queue (Sarvam AI))r  r  rR  ro  r  TSELECT status, COUNT(*) as cnt FROM `1713_raw_calls` GROUP BY status ORDER BY statusr   r   r  rf   r  	by_statusNr  zOrchestration Logorchestration.logorchestrationu:   All orchestration events — ingestion, queuing, analytics)r  rf  log_keyr  zAnalytics Updates Loganalytics_updates.loganalytics_updatesz*Per-call analytics success/failure recordsrf  r7  rr  ra  )jobsqueues	db_statusrC  r&  )r  rp  r2  r  r   r   r   r<   r   r   rE   r   r   r  r  r  r  rs  r   r   rf  r   )orch_runningorch_pidorch_cmdloop_runningloop_pidloop_cmdstt_runningstt_pidstt_cmdr  stt_msgsstt_consumersstt_errr  r  r   r  r   r  r  r   r  rE  r  r*  	log_fileslfr7  size_strr0   r0   r1   
pca_status  s   






r  z/pca/queue/{queue_name}/detailsc              
   C   s  dh}| |vrt dddt| \}}}i }g }zt }| }|d i }	d}
| D ](}tt|t	r<|d n|d }t
t|t	rK|d n|d	 }||	|< |
|7 }
q0|
|	d
}|d | D ]C}t|t	rrt	|n|d |d	 |d |d |d |d d}|drt|d nd|d< |drt|d nd|d< || qgW d   n1 sw   Y  W d   n1 sw   Y  W n ty } zdt|i}W Y d}~nd}~ww | |||d||t  d dS )zADetailed view of a RabbitMQ queue + matching DB record breakdown.r~  rp  zUnknown queuerb   r  r   r   r  rf   r  zSELECT callid, agentname, groupname, call_starttime, call_endtime, fileurl FROM `1713_raw_calls` WHERE status = 1 ORDER BY call_starttime DESC LIMIT 100r  r`     r  )r  r  r  call_starttimecall_endtimefileurlr  Nr  r  )rR  ro  r  ra  )r]  rc  db_breakdownqueued_callsr&  )r   rp  r2  r  r   r   r   r<   r   r   rE   rp   r  r   r   rf  r   )r]  allowed_queuesrn  ro  q_errr  r  r   r  r  r  r   r  rE  r  r  r0   r0   r1   pca_queue_detailsc  s\   




r  z/pca/failuresc                  C   s  t jt jt} ddg}g }g }g }zt }| }|D ]}d| d}d| d}	|d| d| d |	 D ]}
t
|
trJt|
ni }|d	rYt|d	 |d	< || q?|d| d| d
 |	 D ]}
t
|
trzt|
ni }|d	rt|d	 |d	< || qo|d| d| d|	 d |	 D ]}
t
|
trt|
ni }|d	rt|d	 |d	< || qq!W d   n1 sw   Y  W d   n1 sw   Y  W n ty } zdt|ig}W Y d}~nd}~ww i }dD ]J\}}t j| |}z0t|d}| }W d   n	1 s!w   Y  dd |dd D dd }|||< W q  tyJ   g ||< Y q w ||||t|t|t|dt  d dS )zPReturn failed/stuck call records across all active BIDs + error lines from logs.17137491`_raw_calls`_sarvamresponse`z?SELECT callid, agentname, groupname, call_starttime, fileurl, 'z' as bid FROM z9 WHERE status = -2 ORDER BY call_starttime DESC LIMIT 100r  z9 WHERE status = -1 ORDER BY call_starttime DESC LIMIT 100zISELECT r.callid, r.agentname, r.groupname, r.call_starttime, r.fileurl, 'z r LEFT JOIN z s ON r.callid = s.callid WHERE r.status = 2 AND (s.transcript IS NULL OR s.transcript = '') ORDER BY r.call_starttime DESC LIMIT 50Nr  ))r  r  )r  r  r  c                 S   s.   g | ]}d |v sd|v sd|v r| dqS )ERRORFAILWARNING
r7  r   lr0   r0   r1   r     s
    z pca_failures.<locals>.<listcomp>i0i)stt_failed_countinvalid_url_countno_transcript_countra  )
stt_failedinvalid_urlanalytics_no_transcript
log_errorsr;  r&  )r   r  r  r  r  r2  r  r   r   r   r   r   rp   r<   r  r   r   r  	readlinesr   r   rf  r   )r*  active_bidsr  r  r  r   r  rl   	raw_table
resp_tabler   r  r  r  r  rf  r  r1  	all_linesr	  r0   r0   r1   pca_failures  s   




*

r  z/pca/api-creditsc               
   C   s  ddl } g }tdd}dt|d}|rzg| jdd|id	d
iddddd}|jdddr5| ni }|di dd}|j	dksJ|dv rSd|d< d|d< n(|dkr`d|d< d|d< n|j	dv rnd|d< d|d< nd |d< d!|j	 d"|d< W n% t
y } zd|d< d#| |d< W Y d}~nd}~ww d$|d< d%|d< d&|d'< d(|d)< || td*d}td+d,}	td-d.}
d/t||
|	d0d1d2}|rXz.ddl}|jd3|	|td4dd5}|jd6|	|td4dd5}|jd7d8 d|d< d|d< W nb t
yW } zMt|}d9|v sd:|v sd;|v r"d|d< d<|d< n+d=|v r0d>|d< d?|d< nd@|v r>dA|d< dB|d< nd|d< d#|ddC  |d< W Y d}~nd}~ww d$|d< d%|d< || |t  dD dES )Fz8Check status and key validity of external API providers.r   NSARVAM_SUBSCRIPTION_KEYre   z	Sarvam AI)r[   key_set$https://api.sarvam.ai/speech-to-textapi-subscription-keyrr  z	probe.wavs   RIFF    WAVEfmt z	audio/wav
saarika:v2hi-INmodellanguage_code   r   filesr   r2  rh  application/jsonr  codera   unauthorizedinvalid_api_keyinvalid_keyr   Invalid Keystatus_labelinsufficient_quota_error	exhaustedCredits ExhaustedrN  r+  i  r  Activer=  Unknown (HTTP r  Error: not_configuredNot Configuredhttps://dashboard.sarvam.aidashboard_urlz%saaras:v2.5 (batch STT + diarization)r  AWS_ACCESS_KEY_ID
AWS_REGIONz	us-east-1AWS_NOVA_MODELr'  zAWS Bedrockpay_per_usez+https://console.aws.amazon.com/billing/home)r[   r  r  regionbilling_typer  zbedrock-runtimeAWS_SECRET_ACCESS_KEY)region_nameaws_access_key_idaws_secret_access_keyr+  TEXT)byOutputModalityInvalidClientTokenIdInvalidSignatureAuthFailurezInvalid CredentialsExpiredTokenExceptionexpiredzToken ExpiredAccessDeniedExceptionaccess_deniedzAccess Deniedx   ra  )r,  r&  )r8  r   r   rD   postr   rp   rg   r   rc   r   r  boto3r  list_foundation_modelsr<   r   rf  r   )	_requestsresults
sarvam_keysarvam_infor  rt  err_coder  aws_key
aws_region
nova_modelaws_infor  r  bdcmsgr0   r0   r1   pca_api_credits  s    












r  z/pca/logs/{log_key})rs   rP  rO  r  linesfilterc              
   C   s,  ddd}| |vrt dddtjtjt}tj|||  }tj|s/| g ddd	S zRt|d
}|	 }W d   n1 sDw   Y  |dkrUdd |D }n|dkradd |D }n|}dd || d D }	| ||  ||	t
|t
|t
|	dW S  ty }
 zt dt|
dd}
~
ww )z/Return the last N lines of a pipeline log file.r  r  )r  r  rp  zUnknown log filerb   r   zLog file not found)r  r  total_linesr  r  Nr	  c                 S       g | ]}d |v sd|v r|qS )r  r  r0   r  r0   r0   r1   r   e  r  z pca_log_tail.<locals>.<listcomp>warningsc                 S   r  )r  WARNr0   r  r0   r0   r1   r   g  r  c                 S   s   g | ]}| d qS )r  r  r  r0   r0   r1   r   j  rF  )r  rf  r   r  r  filtered_totalreturned_linesr  )r   r   r  r  r  r  r   r7  r  r  r   r   r<   )r  r  r   rV  r*  log_pathr1  r  filteredtailr  r0   r0   r1   pca_log_tailP  s>   
	r
  c                   @   s2   e Zd ZU dZeed< dZeed< dZe	ed< dS )PCAStartJobRequestr  rl   rU  rA   Fignore_watermarkN)
r8   r9   r:   rl   r<   r=   rA   rE   r  rD   r0   r0   r0   r1   r  x  s   
 r  z/pca/jobs/{job_key}/startry  c              
   C   sb  t jt jt}t j|ddd}t j|}t j|ddd}t j|s*d}dddg|d	d
d|d	d|ddg|d	d}| |vrNtdd|  d||  }t	|d \}}}	|rcdd|dS | dkr~dd
dt
|jdt
|jg}
|jr}|
d n|d }
ztj|
|d tjtjdd}d|jd|
d W S  ty } z	td!d"| dd}~ww )#z6Start a pipeline job as a detached background process.z..zcall-proccessingstt_pipelinevenvbinpython3ru  r  )check_keywordr8  r  rt  Nrv  zrun.pyz--worker)rx  r{  r}  rp  zUnknown job: rb   r  Fr  r  r{  z--bidz--limitz--ignore-watermarkr8  r  Tr  r
  )r"  r  rz  r  zFailed to start job: )r   r  r  r  r  r   normpathr7  r   r  r<   rl   rA   r  r  r'  r(  r)  r  r   )ry  rt  r*  stt_dirvenv_python_JOBSjob_defr  r-  rh  r8  r3  r  r0   r0   r1   pca_start_job~  sX   
r  z/pca/stt/configc           
   
   C   sn  |  dstdddddl}tdd}t|d	kr)|dd
 d |dd  n	|r1dt| nd}d}d}|rzX|jdd|iddidddd
d}|j dddrY|	 ni }| di  dd}|j
dksn|dv rsd\}}n|dkr|d \}}n|j
d!v rd"\}}n
d#d$|j
 d%}}W n ty }	 zdd&|	 }}W Y d}	~	nd}	~	ww |t|||d'd(d)S )*z@Return current STT key status (masked) for the master STT panel.rm   rx   Master admin requiredrb   r   Nr  re      r  u   …r  r  r  r  r  rr  r  r  r  r  r  rh  r  r  r  ra   r  )r  r  r  )r  r  r  )r  r  r=  r  r  r  zsaaras:v2.5r  )
key_maskedr  r   r  r  r  )rp   r   r8  r   r   r   r  r   rg   r   rc   r   rD   )
rk   r  r/   maskedr   r  r  rt  r  r  r0   r0   r1   pca_get_stt_config  sB   
< 



r  z/pca/stt/update-keyc              
      s  | dstddd|  I dH }| dpd }|s$tdd	d|tjd
< tjtjtj	t
d}zjtj|rt|d}| }W d   n1 sSw   Y  d}g }|D ]}	|	dsj|	drv|d| d d}q^||	 q^|s|d| d t|d}|| W d   n1 sw   Y  W n ty }
 ztd|
  W Y d}
~
nd}
~
ww tj| d| dddddd ddiS )zDReplace the Sarvam subscription key in .env and process environment.rm   rx   r  rb   Nr/   re   r+  zkey is requiredr  z.envr  FzSARVAM_SUBSCRIPTION_KEY=zSARVAM_SUBSCRIPTION_KEY =r  Tr  z&Could not persist Sarvam key to .env: ra  r`  update_stt_keyz#Updated Sarvam STT subscription keyr  r8  )rp   r   r   ri   r   environr  r   r  r  r  r7  r  r  rg   r  
writelinesr   r  ru  rt   r  )r  rk   r   new_keyenv_pathr1  r  r  	new_linesr[  r  r0   r0   r1   pca_update_stt_key  sL   


r$  z/pca/businesses-overviewc                  C   s`  t  \} }|dkst| tsg } td\}}}td\}}}|s(td\}}}td\}}}g }zt }| }| D ]}	t|	d }
td|
 d\}}}|
|		d	d
|
 t
|		ddd||di dd	}|d|
 d | rd|d< |d|
 d i }d}| D ](}t|trt|n|d |d d}t|d }t|d }|||< ||7 }q||d< ||d< |d|
 d | }|rtt|tr|d n|d nd|d< || q>W d   n1 sw   Y  W d   n1 sw   Y  W n ty } zt|g i dW  Y d}~S d}~ww ||||d t  d! d"S )#zAAll businesses with pipeline stats for the master overview table.rN  ru  rv  rw  rt  rl   r  r	  r  z	Business r  TFr   )	rl   r  r  has_pipeliner  r  r  r  calls_todayr  r  r%  z%SELECT status, COUNT(*) as cnt FROM `z+_raw_calls` GROUP BY status ORDER BY statusrf   r   r  r   r  r  r  zSELECT COUNT(*) as cnt FROM `z2_raw_calls` WHERE DATE(call_starttime) = CURDATE()r&  N)r  rn   global_jobs)r  r  r  ra  )rn   r(  r&  )rt   r  r   r;   r  r2  r  r   r<   rp   rD   r   re  r   r   rE   r  r   r   rf  r   )businesses_listrc   r  rh  r  r  result_businessesr   r  bizrl   r  entryr  r  r   r  r  rE  r  r0   r0   r1   pca_businesses_overview  sv   

$
*"r-  z/pca/bid/{bid}/statsc              
   C   s  d| i}z-t  }| }|d|  d | s5| d|  dW  d   W  d   W S d|  d}|d	| d
 i }d}| D ](}t|trWt|n|d |d d}t|d }	t	|d }
|
||	< ||
7 }qL||d< ||d< |d| d | }dd |D |d< |d| d | }dd |D |d< |d| d | }|rt	t|tr|d n|d nd|d< |d|  d | r|d|  d  | }|rt
t|tr|d! n|d nd"}t|d|d!< nd"|d!< W d   n1 sw   Y  W d   W |S W d   W |S 1 s+w   Y  W |S  tyL } zt||d#< W Y d}~|S d}~ww )$zTPer-BID detailed stats: status breakdown, calls per day/hour, transcription minutes.rl   r  r  zNo raw_calls table for BID )rl   r  Nr  r  z$SELECT status, COUNT(*) as cnt FROM z  GROUP BY status ORDER BY statusr   rf   r'  r   r  r  r  zb
                    SELECT DATE(call_starttime) as day, COUNT(*) as cnt
                    FROM z
                    WHERE call_starttime >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
                    GROUP BY DATE(call_starttime)
                    ORDER BY day
                c                 S   sJ   g | ]!}t t|tr|d  n|d tt|tr|d n|d dqS )rM  r   r  rf   )dater  )r<   r   r   rE   r  r0   r0   r1   r   j      z!pca_bid_stats.<locals>.<listcomp>calls_per_dayza
                    SELECT HOUR(call_starttime) as hr, COUNT(*) as cnt
                    FROM z
                    WHERE DATE(call_starttime) = CURDATE()
                    GROUP BY HOUR(call_starttime)
                    ORDER BY hr
                c                 S   sJ   g | ]!}t t|tr|d  n|d t t|tr|d n|d dqS )hrr   r  rf   )hourr  )rE   r   r   r  r0   r0   r1   r   y  r/  calls_per_hourzSELECT COUNT(*) as cnt FROM z' WHERE DATE(call_starttime) = CURDATE()r&  z_sarvamresponse'z>SELECT COALESCE(SUM(duration), 0) / 60 as total_minutes FROM `r  total_minutesg        r  )r2  r  r   r   re  r   r   r   r<   rE   rQ   r  r   )rl   rz  r   r  r  r  r  r   r  r  rE  r   minsr  r0   r0   r1   pca_bid_statsG  sv   
$




*
& EEEr6  WEBHOOK_SECRETre   a*  
CREATE TABLE IF NOT EXISTS `{bid}_raw_calls` (
    id                      INT AUTO_INCREMENT PRIMARY KEY,
    bid                     INT NOT NULL,
    callid                  VARCHAR(100) NOT NULL,
    fileurl                 TEXT,
    status                  INT DEFAULT 0 COMMENT '0=ingested,1=queued,2=transcribed,3=analyzed,-1=invalid_url,-2=stt_failed',
    agentname               VARCHAR(100),
    groupname               VARCHAR(255),
    call_starttime          DATETIME,
    call_endtime            DATETIME,
    call_status             VARCHAR(50),
    agent_callinfo          VARCHAR(100),
    customer_callinfo       VARCHAR(50),
    direction               VARCHAR(20) DEFAULT 'inbound',
    duration_seconds        INT,
    extra_fields            JSON,
    webhook_source          VARCHAR(100),
    transcription_requested TINYINT(1) DEFAULT 0,
    transcription_status    VARCHAR(20) DEFAULT 'not_requested',
    selected_for_processing TINYINT(1) DEFAULT 0,
    synced_at               DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at              DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uq_callid (callid),
    INDEX idx_bid (bid),
    INDEX idx_status (status),
    INDEX idx_agentname (agentname),
    INDEX idx_call_starttime (call_starttime),
    INDEX idx_call_status (call_status),
    INDEX idx_direction (direction),
    INDEX idx_transcription_status (transcription_status),
    INDEX idx_selected_for_processing (selected_for_processing)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
stringz>Business ID (numeric string). Routes to {bid}_raw_calls table.)r  typerequiredr  z3Unique call identifier from the telephony platform.rG  z*Full URL to the call recording audio file.
start_timer   z2Call start time (YYYY-MM-DD HH:MM:SS or ISO-8601).end_timez0Call end time (YYYY-MM-DD HH:MM:SS or ISO-8601).z'Name of the agent who handled the call.agent_groupz Team/group the agent belongs to.agent_phonez Agent's phone number (callfrom).r	  z!Customer's phone number (callto).zFTelephony dial status (e.g. ANSWER, NO ANSWER, BUSY). Default: ANSWER.z6Call direction: inbound or outbound. Default: inbound.r  integerzTotal call duration in seconds.extra_fieldsobjectzBAny additional key-value data to store as JSON alongside the call.
auto_queuebooleanuG   If true, immediately publish to STT queue (status→1). Default: false.r  zDLabel identifying the sending system (e.g. mcube, exotel, ozonetel).c           
      C   s   t dt|}|  J}|| tdd}|d|| df dd | D }g d}|D ]\}}}	||vrM|d	| d
| d| d|	 d	 q2W d   dS 1 sYw   Y  dS )zOCreate {bid}_raw_calls if it doesn't exist; add new columns to existing tables.z{bid}r   r   z^SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %sr_  c                 S   r  )COLUMN_NAMEr0   r  r0   r0   r1   r    r   z*_ensure_raw_calls_table.<locals>.<setcomp>))r  INTr  )r@  JSONr  )webhook_sourcezVARCHAR(100)r@  zALTER TABLE `z_raw_calls` ADD COLUMN `z` z AFTER `r  N)_RAW_CALLS_DDLrh   r<   r   r   r   rp   r   )
r   rl   ddlr  db_nameexisting_cols	_NEW_COLScol_namecol_def	after_colr0   r0   r1   _ensure_raw_calls_table  s$   

"rP  c                 C   s   | du rdS t | tr8|  } | sdS | dd} d| v r'| d| d } n| dr2| dd } | dd S t | trB| dS t| S )	z:Parse a datetime value to MySQL-compatible string or None.NTr
  r   ra  rP     z%Y-%m-%d %H:%M:%S)r   r<   ri   rh   indexrs  r   ro  r   r0   r0   r1   _parse_datetime  s   



rT  c           
   
   C   s   zBt dd}t dd}ddl}||j|dd}| }|j|d	d
 t| ||d}|j	d|||j
ddd |  W d	S  ty] }	 ztd| ||	 W Y d}	~	dS d}	~	ww )zFPublish a job to the RabbitMQ stt_jobs queue. Returns True on success.r^  r_  RABBITMQ_QUEUEr~  r   Nr  ra  T)rc  durable)rl   rS   rG  re   r  )delivery_mode)exchangerouting_keyrt  
propertiesz4Webhook: failed to publish to RabbitMQ for %s/%s: %sF)r   r   re  rf  rg  rh  ri  r   r   basic_publishBasicPropertiesr   r   r  ru  )
rl   rS   rG  rm  	rmq_queue_pikarmq_connrh  r   _excr0   r0   r1   _publish_to_stt_queue  s*   
ra  c                   @   s   e Zd ZU eed< eed< eed< eed< dZee ed< dZee ed< dZee ed< dZ	ee ed	< dZ
ee ed
< dZee ed< dZee ed< dZee ed< dZeeeef  ed< dZeed< dZee ed< dS )WebhookCallIngestRequestrl   rS   rG  r;  Nr<  r$  r=  r>  r	  r   r   inboundr  r  r@  FrB  r  )r8   r9   r:   r<   r=   r<  r   r$  r=  r>  r	  r   r  r  rE   r@  r   r   rB  rD   r  r0   r0   r0   r1   rb    s    
 rb  z/webhook/call-ingest/schemac                   C   s>   dddt dddddd	d
ddddddddddddddddS )z=Return the accepted field schema for the call-ingest webhook.zPOST /webhook/call-ingestz8X-Webhook-Secret header (matches WEBHOOK_SECRET env var)zPush a call record into PCAA. The record is inserted into {bid}_raw_calls (status=0 ingested). If auto_queue=true it is immediately published to the stt_jobs RabbitMQ queue (status=1) to trigger transcription and analytics.u*   Ingested — record stored, not yet queuedu;   Queued — published to stt_jobs queue, awaiting STT workeru#   Transcribed — STT worker finishedu(   Analyzed — analytics pipeline finishedu,   Invalid URL — recording file not reachableu0   STT Failed — Sarvam/Deepgram returned an error)r   r|   23z-1z-2r  C9876543zXhttps://recordings.mcube.com/mcubefiles112/classic/2026/03/1713/inbound/call_9876543.mp3z2026-03-17 10:30:00z2026-03-17 10:35:00zRahul SharmazPresales Team
9876543210
9123456789r   rc  r4   Tmcube)rl   rS   rG  r;  r<  r$  r=  r>  r	  r   r  r  rB  r  )endpointauthenticationr  r  status_lifecycleexample_payload)WEBHOOK_FIELD_SCHEMAr0   r0   r0   r1   webhook_call_ingest_schema0  s6   	ro  z/webhook/call-ingestx_webhook_secretc                 C   s@  t stddd|t krtdddt| j }t| j }| jp$d }|r-| s3tddd|s;tdd	d|sCtdd
dt| j	}|sPtdddt| j
}| jr^t| jnd}z~t p}t|| | jrpdnd}	d| d}
| J}||
t||||	| jpd| jpd||| jpd| jpd| jpd| jpd| j|| jpd| jrdnd| jrdnd| jrdndf |j}W d   n1 sw   Y  W d   n1 sw   Y  W n% ty     ty } zt d||| tdd| dd}~ww d}d}| jrYt!|||}|sYd}z7t (}| }|d| d|f W d   n	1 s8w   Y  W d   n	1 sHw   Y  W n
 tyX   Y nw |dkr`dnd}| jrm|rkdndnd}dd d!d"d#"|t|}t#d$||||| d%||||||t$% & d& d'}|r||d(< |S ))a  
    Ingest a single call record pushed by any telephony platform.

    - Validates the webhook secret.
    - Ensures {bid}_raw_calls table exists.
    - Inserts/updates the record (idempotent on callid).
    - If auto_queue=true, publishes to stt_jobs queue immediately.
      z/Webhook not configured (WEBHOOK_SECRET not set)rb   ra   Invalid webhook secretre   r+  z&bid must be a non-empty numeric stringzcall_id is requiredzrecording_url is requiredz3start_time is required and must be a valid datetimeNrf   r   z
                INSERT INTO `ak  _raw_calls`
                    (bid, callid, fileurl, status, agentname, groupname,
                     call_starttime, call_endtime, call_status,
                     agent_callinfo, customer_callinfo, direction,
                     duration_seconds, extra_fields, webhook_source,
                     transcription_requested, transcription_status, selected_for_processing)
                VALUES
                    (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
                ON DUPLICATE KEY UPDATE
                    fileurl              = VALUES(fileurl),
                    agentname            = VALUES(agentname),
                    groupname            = VALUES(groupname),
                    call_starttime       = VALUES(call_starttime),
                    call_endtime         = VALUES(call_endtime),
                    call_status          = VALUES(call_status),
                    agent_callinfo       = VALUES(agent_callinfo),
                    customer_callinfo    = VALUES(customer_callinfo),
                    direction            = VALUES(direction),
                    duration_seconds     = VALUES(duration_seconds),
                    extra_fields         = VALUES(extra_fields),
                    webhook_source       = VALUES(webhook_source),
                    updated_at           = CURRENT_TIMESTAMP
            r   rc  queuednot_requestedz2Webhook call-ingest DB error bid=%s call_id=%s: %sr  zDatabase error: Fz2RabbitMQ publish failed; record stored at status=0zUPDATE `z_raw_calls` SET status=0, transcription_requested=0, transcription_status='not_requested', selected_for_processing=0 WHERE callid=%sr  r8  r  ingestedqueued_for_stttranscribedanalyzed)r   rf   r  r`  zDWebhook call-ingest: bid=%s call_id=%s action=%s status=%s queued=%sTra  )r  actionrl   rS   r   r  rv  r&  ru  )'_WEBHOOK_SECRETr   r<   rl   ri   rS   rG  r   rT  r;  r<  r@  r   r   r2  r  rP  rB  r   r   rE   r$  r=  r   r>  r	  r  r  r  r  r   r  r  ra  rp   r  r   rf  r   )rt  rp  rl   rS   rG  r;  r<  
extra_jsonr   initial_status
upsert_sqlr  r  r  rs  queue_errorry  final_statusr  r  r0   r0   r1   webhook_call_ingestW  s   




4




r  z/webhook/call-ingest/batchc           	      C   s^  t stddd|t krtddd| dg }t|tr|s%tdddt|d	kr1tdd
dg }|D ]\}ztdi |}t||d}||j	dd| W q5 tyq } z||ddd|j
d W Y d}~q5d}~w ty } z||dddt|d W Y d}~q5d}~ww tdd |D }t||t|| |t  d dS )z
    Batch-ingest multiple call records in one request.
    Body: { "calls": [ <WebhookCallIngestRequest>, ... ] }
    Each call is processed independently; partial success is returned.
    rq  zWebhook not configuredrb   ra   rr  r   r+  zcalls must be a non-empty listr  z#Maximum 500 calls per batch request)rp  T)rS   r  rS   ?F)rS   r  r  Nc                 s   s    | ]
}| d rdV  qdS )r  rf   NrF  r  r0   r0   r1   r     s    z,webhook_call_ingest_batch.<locals>.<genexpr>ra  )r  	succeededrA  r  r&  r0   )rz  r   rp   r   r;   r   rb  r  r  rS   rd   r   r<   r   r   rf  r   )	r   rp  	calls_rawr  r  r   rz  r  r  r0   r0   r1   webhook_call_ingest_batch  s:   
(*
r  z/export/{bid}/transcriptionsc                    s  d|  d}d|  d}dg}g  |r| d   | |r)| d   | |r7| d   |  |rC| d   | |rQ| d	   |d
  dd| }	d| d| d|	 d fdd}
d|  dt d d}t|
 ddd| didS )z=Stream a CSV of all transcribed calls with their transcripts.r  r  r  z/s.transcript IS NOT NULL AND s.transcript != ''r.groupname = %sr.direction = %sr.call_status = %sr.call_starttime >= %sr.call_starttime <= %s	 23:59:59WHERE r  a  
        SELECT
            r.callid,
            r.agentname,
            r.direction,
            r.call_status,
            r.call_starttime,
            r.groupname,
            s.duration,
            s.language,
            s.num_speakers,
            s.transcript
        FROM  r
        JOIN z" s ON r.callid = s.callid
        ,
        ORDER BY r.call_starttime DESC
    c                  3   sZ   t  } t| }|g d |  V  | d | d t }|	t
jj`}|  |D ]O}||dd|dd|dd|ddt|dd|d	d|d
d|dd|dd|dpmdddg
 |  V  | d | d q4W d    n1 sw   Y  W d    d S W d    d S 1 sw   Y  d S )N)
Call IDAgent	DirectionCall Status	Date/TimeLocation/GroupzDuration (s)LanguagezNum Speakers
Transcriptr   r  re   r  r  r   r  r  r  rA  r  r/  r  r
  )r   re  rr  writerowrp  seektruncater2  r  r   r   r   r   r   rp   r<   rh   )rq  rr  r   r  r   r  sqlr0   r1   generateD  s:   











"z'export_transcriptions.<locals>.generatetranscriptions_rh  %Y%m%d_%H%M%Sri  rj  rg  attachment; filename=""r  )r  r   r   r   rf  ro  r   )rl   r  r  r  r  r   r  sarvam_table
conditionsr  r  rf  r0   r  r1   export_transcriptions  sD   








r  z/export/{bid}/qualityc                    s  d|  d}d|  d}dg}g }	|r| d |	 | |r)| d |	 | |r7| d |	 |  |rC| d |	 | |rQ| d	 |	 |d
  dd| }
d| d| d|
 d}g g  t }t i}|tjj	R}|
||	 |D ]A}|d}|rzt|trt|n|}W n ty   i }Y nw i }||d< |D ]}||vr  | || q | qW d   n1 sw   Y  W d   n1 sw   Y   fdd}d|  dt d d}t| ddd| didS )zSStream a CSV of all analyzed calls with quality scores and per-parameter breakdown.r  r  z_callanalytics`za.quality_score IS NOT NULLr  r  r  r  r  r  r  r  a  
        SELECT
            r.callid,
            r.agentname,
            r.direction,
            r.call_status,
            r.call_starttime,
            r.groupname,
            a.quality_score,
            a.total_possible_score,
            a.sentiment,
            a.call_purpose,
            a.summary,
            a.talk_listen_ratio,
            a.agent_speak_percentage,
            a.customer_speak_percentage,
            a.dead_air_percentage,
            a.parameter_scores
        FROM r  z" a ON r.callid = a.callid
        r  r  _param_scoresNc                  3   s   t  } t| }g d}g } D ]}|| d| d| dg7 }q|||  |  V  | d | d D ]}|dpDd}|dpKd}|r^tt	|t	| d d	 d
nd}|dd|dd|dd|ddt
|dd|dd||||dd|dd|dpddd|dd|dd|dd|ddg}	|d }
g } D ] }|
|i }||dd|dd|dpdddg7 }q||	|  |  V  | d | d q<d S ) N)r  r  r  r  r  r  zQuality ScorezTotal Possible ScorezScore %	SentimentzCall PurposeSummaryzTalk-Listen RatiozAgent Talk %zCustomer Talk %z
Dead Air %z (Score)z (Max)z (Reasoning)r   r9  total_possible_scorer]  rf   %re   r  r  r  r   r  r  r:  r  r;  r  r
  r   r  r  r  r  rt  r   	max_score	reasoning)r   re  rr  r  rp  r  r  rp   r  rQ   r<   rh   )rq  rr  base_headersparam_headerspnamer   qstps	score_pctbase_rowrU  param_cellspdataall_param_namesr   r0   r1   r    sX   
"
(













z export_quality.<locals>.generatequality_report_rh  r  ri  rj  rg  r  r  r  )r  r   r   r  r2  r  r   r   r   r   r   rp   r   r<   r   r   r   r  r   rf  ro  r   )rl   r  r  r  r  r   r  analytics_tabler  r  r  r  seen_paramsr   r  r   ps_rawrU  r  r  rf  r0   r  r1   export_qualityi  sx   












5r  )F)r   r*   )Nr  F)NNr  )NN)NNr  NN(<  r   loggingr  r  r   re  r  ior   r   r   typingr   r   r   r   urllib.parser	   r   r8  fastapir
   r   r   r   r   r   r   r   r   fastapi.middleware.corsr   fastapi.encodersr   fastapi.responsesr   r   r   pydanticr   r   rt   r   r  r   r2  r   r0  r   quality_parameters_handlerr   r  r   r=  r!   mcube_ai_agentsr"   r#   r$   r%   rQ  r&   basicConfigINFO	getLoggerr8   r  r<   r2   r   rD  r{  r3   r=   r  r5   r>   rF   rR   rV   r^   rj   rD   rr   rw   rz   r   r   r   r   r   r   r   r   r   r   r   r  r  r  r  r)  r,  rE   r.  r7  rI  rM  r\  rj  ro  r  r  r  r  r  r  r  r  r  r  r  r  appadd_middlewarer  
middlewarer$  rp   r'  r)  r.  r=  r  r@  rC  rM  rT  rV  rX  r[  r\  r^  putre  r~  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  deleter  patchr  r  r  r	  r
  r  r  r#  r&  r*  r-  r1  r3  r7  r9  rC  rG  rJ  rL  rQ  rS  rW  rZ  r[  r]  r`  rb  rs  rw  rz  r}  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r4  r9  rB  rE  rX  r[  r_  ra  re  r   rf  r  rs  rv  rw  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r"  r#  r/  r2  r3  r7  r8  r9  rD  rL  rO  rR  rS  
subprocessr'  r  rp  rs  r  r  r  r  r
  r  r  r  r$  r-  r6  rz  rH  rn  rP  rT  ra  rb  ro  r  r  r  r  r0   r0   r0   r1   <module>   s  
 ,

(&.
B",&*"."$")")Q&
"""."

 

	



((('


((



	
8

 

(
&
&
!0 $0.


&


"
&



(

,
"

	

	
0


#
0


	
0
0


O:
:
:
4
:



	








 
(



"




:

 
&


"

 



(
4
(
(
4
.
4
	:
	.
	
$
4
!
(
4
	(
Z,
K(
(


 

4
4
.	(442


&


4



.



H*
6
0


:






+"
"

"1





,







	

*	
&





&

`
4
T
b'
(
:&
"*
+
>W%"

& 


,
X