
    i~V                         d Z ddlZddlZddlZddlZddlZddlZddlmZm	Z	  ej                  e      Z G d de      Z G d d      Zy)ub  
Story 3.08: PostCallEnricher — Gemini Enrichment
Story 3.09: PostCallEnricher — Qdrant Write + Deadline Handler
Story 3.10: PostCallEnricher — Postgres Write + Redis Cleanup
AIVA RLM Nexus PRD v2 — Track A

Story 3.08:
    Reads a call transcript from Redis (LRANGE aiva:transcript:{session_id}),
    sends it to Gemini 2.0 Flash for enrichment, and returns a parsed 7-field
    enrichment object.

Story 3.09:
    Adds the public enrich() entry point which orchestrates:
      1. Gemini enrichment (Story 3.08)
      2. 768-dim deterministic embedding of the summary
      3. Qdrant upsert into "aiva_conversations" collection
    Returns the Qdrant vector UUID on success, None on any failure (non-fatal).

Story 3.10:
    Extends enrich() with:
      4. Postgres UPDATE on royal_conversations with all enriched fields
         and memory_vector_id (from Qdrant)
      5. Redis DELETE of aiva:transcript:{session_id} ONLY after successful
         Postgres write (preserves data for retry on Postgres failure)

ALL external I/O (Redis, Gemini API, Qdrant, Postgres) is injected — zero
hardwired side effects.
    N)AnyOptionalc                       e Zd ZdZy)EnrichmentErrorz@Raised when Gemini enrichment fails and the fallback also fails.N)__name__
__module____qualname____doc__     :/mnt/e/genesis-system/core/enrichers/post_call_enricher.pyr   r   0   s    Jr   r   c                   *   e Zd ZdZdZdZdZ	 	 	 d#dededed	ed
df
dZ	ded
e
e   fdZded
efdZded
efdZded
efdZded
efdZded
efdZded
efdZded
efdZdededed
efdZdedede
e   d
efdZded
dfd Ze	 d$d!ed
efd"       Zy)%PostCallEnricheraX  
    Enriches a call transcript using Gemini 2.0 Flash.

    Reads transcript chunks from Redis, sends to Gemini, parses response.
    Designed for use after a Telnyx voice call ends.

    Usage:
        enricher = PostCallEnricher(redis_client=redis, gemini_api_key=api_key)
        result = await enricher._enrich_with_gemini(session_id)
    zgemini-2.0-flashsummaryentitiesdecisions_madeaction_itemsemotional_signal	key_factskinan_directivesaiva_conversationsNredis_clientgemini_api_keyqdrant_clientpostgres_poolreturnc                 <    || _         || _        || _        || _        y)a#  
        Args:
            redis_client:  An async Redis client with lrange() and delete() support.
            gemini_api_key: Google AI Studio API key for Gemini calls.
            qdrant_client: A qdrant_client.QdrantClient instance (or mock).
                           If None, Qdrant writes are skipped.
            postgres_pool: A psycopg2 connection pool (ThreadedConnectionPool or
                           SimpleConnectionPool) with getconn()/putconn() methods.
                           If None, Postgres writes are skipped.
        N)_redis_gemini_api_key_qdrant_postgres_pool)selfr   r   r   r   s        r   __init__zPostCallEnricher.__init__O   s"    " #-$+r   
session_idc                   K   	 | j                  |       d{   }|j	                  dd      }|| j                         d   k(  s|st        j                  d|       yt        t        j                               }| j                  |      }t        |      }||d<   ||d<   | j                  |||       d{   }|st        j                  d|       y| j                  |||       d{   }	|	r| j                  |       d{    |S t        j                  d	|       |S 7 # t        $ r!}t        j                  d||       Y d}~yd}~ww xY w7 7 k7 Rw)
u  
        Main entry point called by WebhookInterceptor on call.hangup.
        Must complete within 3 seconds.

        Steps:
        1. _enrich_with_gemini(session_id) — reads transcript, calls Gemini
        2. Embed enriched["summary"] into a 768-dim vector
        3. Upsert the enrichment payload to Qdrant aiva_conversations
        4. Persist enriched fields + memory_vector_id to Postgres royal_conversations
        5. Delete Redis transcript key (only on successful Postgres write)
        6. Return the Qdrant vector UUID string, or None on any failure

        Args:
            session_id: Telnyx/AIVA call session identifier.

        Returns:
            UUID string (Qdrant vector_id) on success, None on failure.
        Nz=PostCallEnricher.enrich: enrichment failed for session %s: %sr    uH   PostCallEnricher.enrich: skipping Qdrant write — empty call session %sr%   	vector_iduI   PostCallEnricher.enrich: Qdrant write failed for session %s — non-fataluf   PostCallEnricher.enrich: Postgres write failed for session %s — Redis transcript preserved for retry)_enrich_with_geminir   loggererrorget_empty_enrichmentinfostruuiduuid4_embed_textdict_write_to_qdrantwarning_persist_to_postgres_cleanup_redis)
r#   r%   enrichedexcr   r(   vectorpayloadsuccesspg_oks
             r   enrichzPostCallEnricher.enrichi   si    &	!55jAAH ,,y"-d,,.y99KKZ 

%	!!'*x. *(--iIINN[  //
HiPP %%j111  NN7 ] B 	LLO
 	0 J Q 2sn   ED( D%D( BE7E83E+E,EEE%D( (	E1EEEEEEc                   K   d| }| j                   j                  |dd       d{   }|s&t        j                  d|       | j	                         S | j                  |      }	 | j                  |       d{   }| j                  |      S 7 h7 # t        $ r.}t        j                  d||       t        d| d|       |d}~ww xY ww)	ug  
        Full enrichment pipeline for a finished call.

        Steps:
        1. LRANGE aiva:transcript:{session_id} 0 -1 from Redis
        2. Join chunks into a formatted transcript string
        3. Call Gemini 2.0 Flash with structured enrichment prompt
        4. Parse JSON response into the 7-field enrichment dict
        5. Return enrichment dict

        Returns a dict with exactly these keys:
            summary          (str)
            entities         (list[str])
            decisions_made   (list[str])
            action_items     (list[dict]) — each: {"task", "owner", "deadline"}
            emotional_signal (str)
            key_facts        (list[str])
            kinan_directives (list[str])

        On empty transcript: returns graceful empty enrichment.
        Raises EnrichmentError if Gemini fails AND the fallback also fails.
        aiva:transcript:r   Nz1PostCallEnricher: empty transcript for session %sz7PostCallEnricher: Gemini call failed for session %s: %sz%Gemini enrichment failed for session z: )r   lranger*   r.   r-   _build_transcript_string_call_gemini	Exceptionr+   r   _parse_gemini_response)r#   r%   	redis_key
raw_chunks
transcriptraw_responser9   s          r   r)   z$PostCallEnricher._enrich_with_gemini   s     . 'zl3	;;--iB??
KKKZX))++22:>

	!%!2!2:!>>L **<88) @ ? 	LLI
 "7
|2cUK	sE   &CB=C'B ;B<B  CB 	C)CCCrH   c                    g }|D ]|  }t        |t        t        f      r|j                  dd      }	 t	        j
                  |      }|j                  dd      }|j                  dd      }|j                  d| d	|        ~ d
j                  |      S # t        j                  t        f$ r |j                  t        |             Y w xY w)z
        Decodes Redis LRANGE entries and joins them into a readable transcript.

        Each chunk is a JSON string with at least {"speaker": ..., "text": ...}.
        Unknown formats are included as-is so no data is silently dropped.
        utf-8replace)errorsspeakerUNKNOWNtextr'   [z] 
)
isinstancebytes	bytearraydecodejsonloadsr,   appendJSONDecodeErrorAttributeErrorr/   join)r#   rH   linesentrychunkrO   rQ   s          r   rC   z)PostCallEnricher._build_transcript_string   s      
	)E%%!34WY?)

5)))Iy9yy,q	D623
	) yy ((.9 )SZ()s   AB3C
CrI   c                     d| dS )z3Builds the structured prompt for Gemini enrichment.ug  You are an expert call analyst for an AI voice agent system.
Analyse the following call transcript and return a JSON object with EXACTLY these 7 fields:

{
  "summary": "<1-3 sentence summary of the call>",
  "entities": ["<person/company/place names mentioned>"],
  "decisions_made": ["<any decisions agreed upon>"],
  "action_items": [
    {"task": "<what to do>", "owner": "<who>, "deadline": "<when or empty>"}
  ],
  "emotional_signal": "<positive|neutral|negative|frustrated|satisfied>",
  "key_facts": ["<important facts or data points>"],
  "kinan_directives": ["<any explicit instructions or requests for the system owner>"]
}

Rules:
- Return ONLY the raw JSON object — no markdown, no code fences.
- If a field has no relevant content, use an empty list [] or an empty string.
- action_items must always have task, owner, and deadline keys.

TRANSCRIPT:
---
z
---r   )r#   rI   s     r   _build_enrichment_promptz)PostCallEnricher._build_enrichment_prompt   s    ( l )	
r   c                   K   | j                  |      }	 ddlm} |j                  | j                         |j                  | j                        }|j                  |      }|j                  S # t        $ r Y nw xY wddl
}ddl}d| j                   d| j                   }t        j                  dd|igigdd	id
      j                  d      }|j                  j!                  ||dd	id      }	|j                  j#                  |	d      5 }
t        j$                  |
j'                         j)                  d            }ddd       n# 1 sw Y   nxY wd   d   d   d   d   d   S w)a[  
        Makes the actual Gemini API call using the REST endpoint.

        Uses google.generativeai if available, otherwise falls back to
        urllib-based REST call so there is no hard dependency on the SDK.

        Returns the raw text response from Gemini.
        Raises any exception from the API layer for the caller to handle.
        r   N)api_keyz8https://generativelanguage.googleapis.com/v1beta/models/z:generateContent?key=partsrQ   responseMimeTypezapplication/json)contentsgenerationConfigrL   zContent-TypePOST)dataheadersmethod   )timeout
candidatescontent)rb   google.generativeaigenerativeai	configurer    GenerativeModelGEMINI_MODELgenerate_contentrQ   ImportErrorurllib.requesturllib.errorrX   dumpsencoderequestRequesturlopenrY   readrW   )r#   rI   promptgenaimodelresponseurlliburlr;   reqrespbodys               r   rD   zPostCallEnricher._call_gemini  s     ..z:	/OOD$8$8O9))$*;*;<E--f5H==  		 	 G  !!6t7K7K6LN 	 **%(8'9:;%79K$L

 &/ 	 nn$$#%78	 % 
 ^^##C#4 	;::diik009:D	; 	; 	; L!!$Y/8;FCCsB   E+AA/ .E+/	A;8E+:A;;BE+3E	E+EE+rJ   c                 ,   	 t        j                  |      }t        |t              st	        d      | j                  |      S # t         j                  t        t        f$ r2}t        j                  d|       | j                  |      cY d}~S d}~ww xY w)z
        Parses Gemini JSON response into a validated 7-field enrichment dict.

        Falls back to {"summary": raw_response, "entities": [], ...} if the
        response is not valid JSON or is missing required fields.
        zResponse is not a JSON objectuE   PostCallEnricher: could not parse Gemini JSON (%s) — using fallbackr   N)rX   rY   rT   r3   
ValueError_normalise_enrichmentr[   KeyErrorr*   r5   r-   )r#   rJ   parsedr9   s       r   rF   z'PostCallEnricher._parse_gemini_responseB  s    
	@ZZ-Ffd+ !@AA--f55$$j(; 	@NNW )),)??	@s   A A B!'BBBr   c                    dt         dt        fd}dt         dt        fd}t        |j                  dd             ||j                  d             ||j                  d             ||j                  d	            t        |j                  d
d             ||j                  d             ||j                  d            dS )z
        Ensures every required field is present and has the correct type.
        Missing/wrong-type fields are replaced with safe defaults.
        valr   c                 b    t        | t              r| D cg c]  }t        |       c}S g S c c}w N)rT   listr/   )r   items     r   _list_of_strz<PostCallEnricher._normalise_enrichment.<locals>._list_of_strZ  s+    #t$.12dD	22I 3s   ,c                    g }t        | t              s|S | D ]s  }t        |t              s|j                  t	        |j                  dd            t	        |j                  dd            t	        |j                  dd            d       u |S )Ntaskr'   ownerdeadline)r   r   r   )rT   r   r3   rZ   r/   r,   )r   resultr   s      r   _list_of_action_itemszEPostCallEnricher._normalise_enrichment.<locals>._list_of_action_items_  s    Fc4( dD)MM$'(<$=%('2)>%?(+DHHZ,D(E Mr   r   r'   r   r   r   r   neutralr   r   r   )r   r   r/   r,   )r#   r   r   r   s       r   r   z&PostCallEnricher._normalise_enrichmentU  s    
	c 	d 	
	s 	t 	  6::i45$VZZ
%;<*6::6F+GH1&**^2LM #FJJ/A9$M N%fjj&=> ,VZZ8J-K L
 	
r   rQ   c           
      &   t        j                  |j                               j                         }d}t	        j
                  |t        |      z        }||z  d| }t        d|d      D cg c]!  }t        j                  d|||dz          d   # }}|D cg c]  }t	        j                  |      r|nd }}t        d |D        d	      xs d}	t	        j                  |	      r|	dk(  rd}	|dd
 D cg c]  }||	z  	 c}S c c}w c c}w c c}w )a  
        Creates a 768-dim deterministic embedding vector from text.

        Uses a hash-based approach (SHA-256 repeated) for testability and
        zero external dependencies.  In production, swap the body with a
        real Gemini text-embedding-004 call without changing the interface.

        Args:
            text: The text to embed (typically the call summary).

        Returns:
            List of 768 floats normalised to [-1, 1].
        i   Nr      fg        c              3   2   K   | ]  }t        |        y wr   )abs).0vs     r   	<genexpr>z/PostCallEnricher._embed_text.<locals>.<genexpr>  s     .!s1v.s   g      ?)defaulti   )hashlibsha256r{   digestmathceillenrangestructunpackisfinitemax)
r#   rQ   hneeded_bytesrepeat_countextendedivaluesr   max_vals
             r   r2   zPostCallEnricher._embed_textx  s    NN4;;=)002yyA!67$m|4 1lA.
 MM#xAE23A6
 

 ;AAQt}}Q'!S0AA.v.<C}}W%CG%+DS\2G22

 B 3s   ,&D D	5Dr(   r:   r;   c                 Z  K   | j                   t        j                  d       y	 ddlm} | j                   j                  | j                   ||||      g       t        j                  d|| j                         y	# t        $ r }t        j                  d
|       Y d}~yd}~ww xY ww)a  
        Upserts a single point into the aiva_conversations Qdrant collection.

        Non-fatal: all exceptions are caught and logged; caller receives False.

        Args:
            vector_id: UUID string to use as the Qdrant point ID.
            vector:    768-dim float list.
            payload:   Dict attached as Qdrant point payload (enrichment fields).

        Returns:
            True on successful upsert, False on any failure.
        NuI   PostCallEnricher._write_to_qdrant: no Qdrant client injected — skippingFr   )PointStruct)idr:   r;   )collection_namepointsz4PostCallEnricher._write_to_qdrant: upserted %s to %sTuC   PostCallEnricher._write_to_qdrant: upsert failed (%s) — non-fatal)
r!   r*   r5   qdrant_client.modelsr   upsert_QDRANT_COLLECTIONr.   rE   r+   )r#   r(   r:   r;   r   r9   s         r   r4   z!PostCallEnricher._write_to_qdrant  s       <<NN[ 	8LL $ 7 7#yQR    KKF''
  	LLU 	s/   #B+AA? >B+?	B(B#B+#B((B+r8   c                 ^  K   | j                   t        j                  d       yd}|j                  dd      t	        j
                  |j                  dg             t	        j
                  |j                  dg             t	        j
                  |j                  d	g             |j                  d
d      t	        j
                  |j                  dg             t	        j
                  |j                  dg             ||f	}d}	 | j                   j                         }|j                         5 }|j                  ||       ddd       |j                          t        j                  d||       	 || j                   j                  |       yy# 1 sw Y   PxY w# t        $ rb}t        j                  d||       |!	 |j                          n# t        $ r Y nw xY wY d}~|| j                   j                  |       yyd}~ww xY w# || j                   j                  |       w w xY ww)u  
        Updates the royal_conversations row for session_id in Postgres.

        Columns written:
            transcript_raw       — raw enriched summary string
            enriched_entities    — JSON-serialised list of entity strings
            decisions_made       — JSON-serialised list of decision strings
            action_items         — JSON-serialised list of action-item dicts
            emotional_signal     — string signal (positive/negative/neutral/…)
            key_facts            — JSON-serialised list of fact strings
            kinan_directives     — JSON-serialised list of directive strings
            memory_vector_id     — Qdrant UUID returned from Story 3.09

        Uses parameterized SQL (%s placeholders — NO string formatting in SQL).
        Uses getconn/putconn in a try/finally block.

        Returns:
            True on successful UPDATE, False on any failure or if no pool injected.
        NuM   PostCallEnricher._persist_to_postgres: no Postgres pool injected — skippingFzUPDATE royal_conversations SET transcript_raw = %s, enriched_entities = %s, decisions_made = %s, action_items = %s, emotional_signal = %s, key_facts = %s, kinan_directives = %s, memory_vector_id = %s WHERE session_id = %sr   r'   r   r   r   r   r   r   r   zHPostCallEnricher._persist_to_postgres: updated session %s (vector_id=%s)TzGPostCallEnricher._persist_to_postgres: UPDATE failed for session %s: %s)r"   r*   r5   r,   rX   rz   getconncursorexecutecommitr.   putconnrE   r+   rollback)	r#   r%   r8   r(   _SQLparamsconncurr9   s	            r   r6   z%PostCallEnricher._persist_to_postgres  s    2 &NN_ 	$ 	 LLB'JJx||J34JJx||$4b9:JJx||NB78LL+Y7JJx||K45JJx||$6;<

 	2&&..0D *#D&)*KKMKKZ
  ##++D1  -* *  	LLY
 MMO  ##++D1  	 ##++D1  s~   DH-*F .F/F 1H-FF 	H%H?GH	GHGHH
 #H-HH
 
 H**H-c                    K   d| }	 | j                   j                  |       d{    t        j                  d|       y7 # t        $ r!}t        j                  d||       Y d}~yd}~ww xY ww)z
        Deletes aiva:transcript:{session_id} from Redis after successful Postgres write.

        Failure is non-fatal: logged as a warning, exception not propagated.
        r@   Nz/PostCallEnricher._cleanup_redis: deleted key %sz<PostCallEnricher._cleanup_redis: failed to delete key %s: %s)r   deleter*   r.   rE   r5   )r#   r%   rG   r9   s       r   r7   zPostCallEnricher._cleanup_redis  sr      'zl3	
	++$$Y///KKA9 0  	NNN 	s>   A2A AA A2A 	A/A*%A2*A//A2r   c                     | g g g dg g dS )z:Returns a valid enrichment dict with empty/default fields.r   r   r   r   s    r   r-   z"PostCallEnricher._empty_enrichment+  s#       ) "
 	
r   )r'   NN)u%   Empty call — no transcript captured)r   r   r	   r
   ru   _ENRICHMENT_FIELDSr   r   r/   r$   r   r>   r3   r)   r   rC   rb   rD   rF   r   r2   boolr4   r6   r7   staticmethodr-   r   r   r   r   r   5   s   	 &L .
 !!!,, , 	,
 , 
,4Bs Bx} BP,9C ,9D ,9d 4  C  *
3 
3 
6-DS -DS -D^@3 @4 @&!
D !
T !
F3 3 3<((&*(59(	(\R2R2 R2 C=	R2
 
R2hs t & >

	
 
r   r   )r
   r   rX   loggingr   r   r0   typingr   r   	getLoggerr   r*   rE   r   r   r   r   r   <module>r      sK   8        			8	$	i 	
C
 C
r   