
    i1                        d Z ddlZddlZddlZddlmZmZ ddlmZ ddlm	Z	m
Z
  ej                  e      ZdZ ee
j                   e
j"                  h      Ze
D  ci c]  } | j&                  |  c} ZdZd	Z G d
 d      Zyc c} w )u  
core/intent/intent_classifier.py

IntentClassifier: sends caller utterances to Gemini Flash and parses the
JSON response into an IntentSignal.

Called by the AIVA voice pipeline immediately after speech-to-text.
Returns IntentSignal (never raises) — falls back to UNKNOWN on any error.

Story 5.03 adds an optional Redis cache layer:
  - Cache key: SHA256 hash of the utterance
  - TTL: 60 seconds
  - UNKNOWN results are never cached
  - If redis_client is None, caching is silently disabled

VERIFICATION_STAMP
Story: 5.02 + 5.03
Verified By: parallel-builder
Verified At: 2026-02-25
Tests: 17/17 (5.02) + 14/14 (5.03) = 31/31
Coverage: 100%
    N)datetimetimezone)Optional   )IntentSignal
IntentTypea  
You are AIVA's intent classifier. Analyze the utterance and classify the intent.

UTTERANCE: {utterance}
CONTEXT: {context}

VALID INTENT TYPES: book_job | qualify_lead | answer_faq | escalate_human | capture_memory | task_dispatch | unknown

Respond ONLY with valid JSON:
{{
  "intent_type": "book_job",
  "confidence": 0.92,
  "extracted_entities": {{"name": "George", "location": "Cairns", "service": "plumbing"}},
  "requires_swarm": true,
  "reasoning": "caller wants to book a plumbing job"
}}
zintent:cache:{utterance_hash}<   c            	           e Zd ZdZdZdddZ	 	 ddedededefd	Zdededefd
Z	dedededefdZ
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e   fdZdededdfdZy)IntentClassifiera  
    Classifies caller utterances via Gemini Flash.

    Usage (async):
        classifier = IntentClassifier(gemini_client)
        signal = await classifier.classify("I need a plumber", session_id="s1")

    The gemini_client must expose an async method:
        await client.generate_content_async(prompt: str) -> object
    where the returned object has a `.text` attribute containing the
    raw response string.

    If the client is synchronous (for testing), `classify` accepts any object
    that has a `.generate_content(prompt)` method returning an object with
    `.text`.  The public `classify` method is an async coroutine; for sync
    clients wrap the call via `asyncio.run()` or pytest-asyncio.
    zgemini-2.0-flashNreturnc                      || _         || _        y)a,  
        Args:
            gemini_client: Async (or sync-fallback) Gemini API client.
            redis_client:  Optional Redis client (e.g. redis.asyncio.Redis or
                           any object with async get/setex methods).
                           When None, caching is disabled.
        N)gemini_redis)selfgemini_clientredis_clients      6/mnt/e/genesis-system/core/intent/intent_classifier.py__init__zIntentClassifier.__init___   s     $"    	utterancecontext
session_idc                   K   | j                  |       d{   }||S | j                  ||      }d}	 | j                  j                  |       d{   }|j                  }| j                  |||      }	|	j                  t        j                  ur| j                  ||	       d{    |	S 7 7 ]# t
        $ rX 	 | j                  j                  |      }|j                  }n,# t        $ r }t        j                  d|       Y d}~nd}~ww xY wY t        $ r }t        j                  d|       Y d}~d}~ww xY w7 w)a  
        Send utterance + context to Gemini Flash; parse response into IntentSignal.

        When a Redis client was supplied at construction time, identical
        utterances return a cached IntentSignal within 60 s (Story 5.03).
        UNKNOWN results are never cached.

        Never raises.  Falls back to IntentType.UNKNOWN on any error.

        Args:
            utterance:  The raw caller text from speech-to-text.
            context:    Optional prior-turn context (call history summary, etc.)
            session_id: Caller session identifier for tracing.

        Returns:
            IntentSignal with confidence clamped to [0.0, 1.0] and
            requires_swarm forced True when intent is BOOK_JOB or TASK_DISPATCH.
        N zGemini sync call failed: %szGemini async call failed: %s)_get_cached_build_promptr   generate_content_asynctextAttributeErrorgenerate_content	Exceptionloggererror_parse_responseintent_typer   UNKNOWN_set_cached)
r   r   r   r   cachedpromptraw_responseresponseexcsignals
             r   classifyzIntentClassifier.classifyn   s.    2 ''	22M ##Iw7	>![[??GGH#==L %%lIzJ Z%7%77""9f5557 3 H 	AA;;77?'}} A:C@@A 	>LL7==	> 6s   E B/E B3 B1B3 $AE (D>)E 1B3 3	D;='C%$D;%	D.D	D;	DD;E D;D61E 6D;;E c                 0    t         j                  ||      S )z@Format CLASSIFICATION_PROMPT with utterance and context strings.)r   r   )CLASSIFICATION_PROMPTformat)r   r   r   s      r   r   zIntentClassifier._build_prompt   s    $++i+QQr   r*   c                    t         j                  }d}i }d}	 |j                         }|j                  d      r4|j	                         }	t        |	      dkD  rdj                  |	dd       n|}t        j                  |      }
|
j                  dd	      }t        j                  t        |      j                         t         j                        }|
j                  d
d      }| j                  |      }|
j                  di       }t        |t              r|ni }t!        |
j                  dd            }|t.        v rd}t1        ||||||t3        j4                  t6        j8                        |      S # t        j"                  t$        t&        t(        f$ r7}t*        j-                  d||       t         j                  }d}i }d}Y d}~d}~ww xY w)a  
        Parse Gemini JSON response into IntentSignal.

        Guarantees:
        - Never raises.
        - confidence clamped to [0.0, 1.0].
        - requires_swarm forced True for BOOK_JOB and TASK_DISPATCH.
        - Falls back to UNKNOWN on any parse error.
                Fz```   
r   r%   unknown
confidenceextracted_entitiesrequires_swarmzoIntentClassifier._parse_response: could not parse Gemini response (falling back to UNKNOWN). Error: %s. Raw: %rNTr   r   r%   r8   r9   r:   
created_atraw_gemini_response)r   r&   strip
startswith
splitlineslenjoinjsonloadsget_VALUE_TO_INTENTstrlower_clamp_confidence
isinstancedictboolJSONDecodeError	TypeError
ValueErrorr   r"   warningSWARM_REQUIRED_INTENTSr   r   nowr   utc)r   r*   r   r   r%   r8   r9   r:   cleanlinesdataraw_typeraw_confentities_rawr,   s                  r   r$   z IntentClassifier._parse_response   s    !((
#%#	# &&(E&((*25e*q.		%"+.eE*D xxy9H*..s8}/B/B/DjFXFXYK xxc2H//9J  88$8"=L1;L$1OUW "$((+;U"CDN 00!N!#!1)||HLL1 ,	
 		
! $$i^L 
	#NN@	 %,,KJ!#"N
	#s   DE1 1#G-GGc                 ~    	 t        t        dt        dt        |                        S # t        t        f$ r Y yw xY w)zBClamp confidence to [0.0, 1.0]. Returns 0.0 for non-numeric input.r3   g      ?)floatmaxminrN   rO   )r   values     r   rI   z"IntentClassifier._clamp_confidence   s;    	S#c5<"89:::& 		s   '* <<c                 f    t        j                  |j                  d            j                         S )zEReturn the SHA256 hex digest of the utterance string (UTF-8 encoded).zutf-8)hashlibsha256encode	hexdigestr   r   s     r   _utterance_hashz IntentClassifier._utterance_hash   s%    ~~i..w78BBDDr   c                 L    t         j                  | j                  |            S )z2Build the Redis cache key from the utterance hash.)utterance_hash)_INTENT_CACHE_KEY_TEMPLATEr1   re   rd   s     r   
_cache_keyzIntentClassifier._cache_key  s(    )00//	: 1 
 	
r   r-   c           	          |j                   |j                  |j                  j                  |j                  |j
                  |j                  |j                  j                         |j                  dS )z.Serialize an IntentSignal to a JSON-safe dict.r;   )
r   r   r%   r^   r8   r9   r:   r<   	isoformatr=   )r   r-   s     r   _signal_to_dictz IntentClassifier._signal_to_dict	  se     !++))!--33 ++"(";";$33 ++557#)#=#=	
 		
r   rV   c                    t        |d   |d   t        j                  |d   t        j                        t        |d         |j                  di       t        |d         t        j                  |d         |j                  d      	      S )
z=Deserialize a dict (from Redis JSON) back to an IntentSignal.r   r   r%   r8   r9   r:   r<   r=   r;   )	r   rF   rE   r   r&   r[   rL   r   fromisoformat)r   rV   s     r   _dict_to_signalz IntentClassifier._dict_to_signal  s    L);'(,,T--@*BTBTUT,/0#xx(<bA%5 67--d<.@A $)> ?	
 		
r   c                 <  K   | j                   y	 | j                  |      }| j                   j                  |       d{   }|yt        j                  |      }| j                  |      S 7 -# t        $ r }t        j                  d|       Y d}~yd}~ww xY ww)u   
        Hash utterance → check Redis → return cached IntentSignal or None.

        Returns None when:
        - redis_client is not configured, OR
        - key does not exist in Redis, OR
        - any Redis/deserialization error occurs.
        Nz&IntentClassifier cache read failed: %s)	r   ri   rE   rC   rD   ro   r!   r"   rP   )r   r   keyrawrV   r,   s         r   r   zIntentClassifier._get_cached#  s      ;;		//),C,,C{::c?D''--	 -
  	NNCSI	sL   B/A0  A.A0 B%A0 -B.A0 0	B9BBBBc                 @  K   | j                   y	 | j                  |      }t        j                  | j	                  |            }| j                   j                  |t        |       d{    y7 # t        $ r }t        j                  d|       Y d}~yd}~ww xY ww)u  
        Hash utterance → serialize IntentSignal → write to Redis with 60 s TTL.

        Silently swallows errors so a cache failure never breaks classification.
        UNKNOWN results must NOT be passed here (caller is responsible for the guard).
        Nz'IntentClassifier cache write failed: %s)
r   ri   rC   dumpsrl   setexINTENT_CACHE_TTLr!   r"   rP   )r   r   r-   rq   payloadr,   s         r   r'   zIntentClassifier._set_cached9  s      ;;	K//),Cjj!5!5f!=>G++##C)97CCC 	KNNDcJJ	KsA   BAA2 *A0+A2 /B0A2 2	B;BBBB)N)r   N)r   r   )__name__
__module____qualname____doc__GEMINI_MODELr   rG   r   r.   r   r$   r[   rI   re   ri   rK   rl   ro   r   r   r'    r   r   r   r   J   s'   $ &L	#$ 	44 4 	4
 
4tRs RS RS RF
F
 F
 	F

 
F
P% E E E
C 
C 

l 
t 

D 
\ 
3 8L3I ,K3 K K Kr   r   )r{   r`   rC   loggingr   r   typingr   intent_signalr   r   	getLoggerrx   r"   r0   	frozensetBOOK_JOBTASK_DISPATCHrQ   r^   rF   rh   rv   r   )members   0r   <module>r      s   .    '  3			8	$ & #J$7$79Q9Q#RS  8BBVFLL&(B  =  }K }K Cs   B