
    i#                         U d Z ddlZddlZddlZddlmZ ddlmZmZ  ej                  e
      Z ed      ZdZeed<   dZeed	<    G d
 d      Zy)u  
core/workers/faq_worker.py

FAQWorker — Static FAQ Lookup

Created by Story 5.10 (AIVA RLM Nexus PRD v2).

Responsibilities:
  1. Load FAQ knowledge base from data/aiva_faq.json
  2. Fuzzy-match the caller's utterance against FAQ questions using difflib
  3. Write the best-matching answer to Redis aiva:state:{session_id} under faq_answer
  4. Return {"answer": str, "matched": bool} — never None

Design notes:
- difflib.SequenceMatcher is used exclusively (no external fuzzy-matching libraries)
- Match threshold is configurable but defaults to 0.7
- Graceful degradation: missing FAQ file returns fallback answer without crashing
- All external I/O (file loading, Redis) is injectable for full test isolation
- No SQLite. No direct network calls in the class body.
    N)Path)AnyOptionalz(/mnt/e/genesis-system/data/aiva_faq.jsongffffff?MATCH_THRESHOLDzVI don't have that information right now, but I'll make sure the team gets back to you.FALLBACK_ANSWERc                       e Zd ZdZ	 	 ddee   dee   ddfdZdedefdZ	de
e   fd	Zd
ede
e   deee   ef   fdZdededdfdZy)	FAQWorkera|  
    Looks up the answer to a caller's FAQ from a JSON knowledge base and
    writes it to Redis for AIVA to speak.

    All external I/O (file system, Redis) is performed through injected
    objects so the worker is fully testable without real services.

    Args:
        faq_path:     Path to the FAQ JSON file.  Defaults to FAQ_KB_PATH.
                      Injectable via constructor for test isolation.
        redis_client: Object with an ``hset(key, field, value)`` method.
                      Compatible with redis.asyncio / aioredis clients.
                      If None, Redis storage is skipped with a warning log.
    Nfaq_pathredis_clientreturnc                 0    |xs t         | _        || _        y )N)FAQ_KB_PATH_path_redis)selfr
   r   s      0/mnt/e/genesis-system/core/workers/faq_worker.py__init__zFAQWorker.__init__?   s    
 ,
"    intentc                 l  K   t        |dd      }t        |dd      }| j                         }| j                  ||      \  }}|,|t        k\  r#|d   }d}t        j                  d|||d	          n#t        }d
}t        j                  d|||nd       | j                  ||       d{    ||dS 7 	w)u  
        Main entry point.  Called by SwarmRouter for every ANSWER_FAQ intent.

        Steps:
          1. Load FAQ KB from the configured JSON file path.
          2. Fuzzy-match the intent utterance against all FAQ questions.
          3. If best match score >= MATCH_THRESHOLD → use matched answer.
          4. Otherwise → use FALLBACK_ANSWER.
          5. Write the answer to Redis aiva:state:{session_id} under faq_answer.
          6. Return {"answer": str, "matched": bool}.

        Args:
            intent: An IntentSignal instance (duck-typed to avoid hard import).

        Returns:
            dict with keys ``answer`` (str) and ``matched`` (bool).  Never None.
        
session_idunknown	utterance NanswerTz+FAQ matched for session %s (score=%.3f): %squestionFu@   No FAQ match for session %s (best_score=%.3f) — using fallback        )r   matched)getattr	_load_faq_find_best_matchr   loggerinfor   _write_to_redis)	r   r   r   r   faqs
best_entry
best_scorer   r   s	            r   executezFAQWorker.executeK   s     $ "&,	B
 b9	~~!%!6!6y$!G
J!jO&C$X.F GKK=:&	 %FGKKR(4
# "":v666 W55 	7s   B&B4(B2)
B4c                    	 | j                   j                  d      }t        j                  |      }t	        |t
              s"t        j                  d| j                          g S |S # t        $ r% t        j                  d| j                          g cY S t        j                  $ r-}t        j                  d| j                   |       g cY d}~S d}~ww xY w)z
        Load FAQ entries from the configured JSON file.

        Returns:
            List of FAQ dicts with ``question`` and ``answer`` keys.
            Returns an empty list if the file is missing or malformed.
        zutf-8)encodingu3   FAQ file at %s is not a JSON array — returning []u)   FAQ file not found at %s — returning []z#FAQ file JSON parse error at %s: %sN)r   	read_textjsonloads
isinstancelistr"   warningFileNotFoundErrorJSONDecodeErrorerror)r   rawentriesexcs       r   r    zFAQWorker._load_faq|   s    	**&&&8CjjoGgt,TVZV`V`a	N  	NNF

SI## 	LL>

CPI	s*   A"A' %A' '+CC&"CCCr   r%   c                 "   |sy|j                         j                         }d}d}|D ]b  }|j                  dd      j                         j                         }|s4t        j                  d||      j                         }||kD  s_|}|}d ||fS )a  
        Find the best matching FAQ entry using difflib.SequenceMatcher.

        Comparison is case-insensitive to maximise recall.

        Args:
            utterance: The caller's raw utterance text.
            faqs:      List of FAQ dicts loaded from the knowledge base.

        Returns:
            Tuple of (best_entry, best_score).
            best_entry is None and best_score is 0.0 if faqs is empty.
        )Nr   Nr   r   r   )lowerstripgetdifflibSequenceMatcherratio)	r   r   r%   normalised_utterancer&   r'   entryquestion_textr=   s	            r   r!   zFAQWorker._find_best_match   s    $ (0668%)

 	#E!IIj"5;;=CCEM ++$ eg	  z!"
"
	# :%%r   r   r   c                   K   | j                   t        j                  d|       yd| }	 | j                   j                  |d|       t        j	                  d|       y# t
        $ r!}t        j                  d||       Y d}~yd}~ww xY ww)a  
        Write the FAQ answer to Redis under ``aiva:state:{session_id}``
        using the field name ``faq_answer``.

        If no redis_client was injected, logs a warning and returns silently.
        Redis storage is best-effort and must not block the call.

        Args:
            session_id: Identifies the AIVA conversation session.
            answer:     The FAQ answer string to store.
        Nu@   No redis_client injected — skipping Redis write for session %szaiva:state:
faq_answerz-FAQ answer written to Redis at %s[faq_answer]z6Failed to write FAQ answer to Redis for session %s: %s)r   r"   r0   hsetdebug	Exceptionr3   )r   r   r   keyr6   s        r   r$   zFAQWorker._write_to_redis   s      ;;NNR J<(	KKS,7LLH#N 	LLH 	s.   )B3A  B 	B
)B BB

B)NN)__name__
__module____qualname____doc__r   r   r   r   dictr(   r/   r    strtuplefloatr!   r$    r   r   r	   r	   /   s    " $(&*#4.# sm# 
	#+6C +6D +6b4: ,(&(& 4j(& 
x~u$	%	(&T S T r   r	   )rJ   r,   loggingr;   pathlibr   typingr   r   	getLoggerrG   r"   r   r   rN   __annotations__r   rL   r	   rO   r   r   <module>rU      sa   *      			8	$ => 4  i ir   