
    DWjN                    ^   U d Z ddlm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Zd$dZd%d	Zd&d
Zd'dZ eej&                  j)                  dd            ZdZdZdZdZ eh d      Z ej8                         Zi ddddZded<   i Z ded<   d(dZ!d)dZ"d)dZ#d*dZ$d+dZ%dD  cg c]  } ejM                  | ejN                          c} dD  cg c]  } ejM                  |        c} z   Z( ejL                  dejN                        Z)d,dZ*d,d Z+d-d!Z,d.d"Z-d# Z.yc c} w c c} w )/u  Entity Dossier — put a profile card of a person/company/concept on the HUD.

Two ways in:
  1. The brain's ``show_entity(name)`` tool (mcp__entity__show_entity) — Jarvis
     calls it when Ahmed says "show me Saud" / "who is X / tell me about X".
  2. AUTO-SHOW — main.py's listen loop calls ``maybe_autoshow(text)`` on every
     addressed utterance. When Ahmed names exactly ONE known entity (word-
     boundary match against a name set refreshed ~every 10 min) and that
     entity's per-entity cooldown is clear, its card pops on its own.

The card is delivered to the HUD as an ``entity_profile`` event:
    emit("entity_profile", profile={...})
The HUD renders the dossier from that JSON and can send back-channel actions
(control/entity_action.json → voice/control.py: drill-down, note, summary edit).

SERVER CONTRACT (memory service, GET /profile?name=X → dossier JSON with keys
entity, summary, read, quick, relations, timeline, facts_by_kind, tasks,
counts, also_matched):
  - a KNOWN-ABSENT entity comes back HTTP 200 with ``{"entity": null, ...}`` —
    that is "nothing known", NOT an error: we speak "I don't have anything on
    X yet" and do NOT emit the event.
  - an AMBIGUOUS name returns the best entity plus ``also_matched: [names]`` —
    we name the runner-up in the spoken confirmation so Ahmed can drill down.

Everything here degrades gracefully. The network may hang or the route may not
exist yet — every call has a hard timeout, swallows errors to a log line, and
NEVER blocks the voice loop (the name-set refresh and the auto-show fetch both
run on daemon threads; the brain tool wraps the fetch in asyncio.to_thread).
    )annotationsN)memory_control)emitc           	     ~   | xs dj                         } | rt        j                         sy	 t        j                  j                  | t        j                  d      }t        j                  dd| d      }t        |t              r|S dS # t        $ r'}t        d| d	t        |      dd
         Y d}~yd}~ww xY w)u2  GET /profile?name=X — the dossier JSON. Hard 3s ceiling. Returns None only
    on a network/timeout error or when memory isn't configured. A known-absent
    entity is NOT an error (the server answers 200 with {"entity": null}); that
    dict is returned as-is and the caller checks the ``entity`` field. NnamegroupGETz	/profile?   timeoutz$  [entity] profile fetch failed for : d   )stripr   enabledurllibparse	urlencode_GROUP_req	Exceptionprintstr
isinstancedict)r	   qsoutes       ?/Users/ahmed/devFolder/Ultron/claude-voice/voice/entity_show.pyfetch_profiler!   /   s    
 JBD~--/\\##N$9$9:<!!%9RD)91E S$'31T1  4THBs1vds|nMNs   AB 	B<B77B<c           	        | xs dj                         } | rt        j                         sy	 t        j                  dd| t        j                  dd      }t        |t              sy|j                  d      }t        |t              r|S dS # t
        $ r'}t        d| d	t        |      dd
         Y d}~yd}~ww xY w)u  POST /dossier {"name","group"} → {"composed": {...}|null, "cached": bool} —
    the LLM-COMPOSED dossier (a written narrative over the raw profile). Hard 25s
    ceiling: composition is 5-12s uncached, instant when cached. Returns the
    composed dict, or None on a 404 (route not deployed yet), timeout, network
    error, non-dict body, or a null composition — every one degrades to the HUD's
    raw rendering. Never raises.r   NPOSTz/dossierr      r   z&  [entity] dossier compose failed for r   r   composed)r   r   r   r   r   r   r   r   r   r   get)r	   r   r   r%   s       r    fetch_composedr'   A   s     JBD~--/!!JN$9$9: c4 wwz"H!(D18;t;  6thbQOPs   *B 	B=B88B=r   c                    t        |       }|$t        j                  t               t        |       }	 t	        d||d       y# t
        $ r$}t        dt        |      dd         Y d}~yd}~ww xY w)uj  STAGE 2 (daemon thread): compose the dossier off the hot path and swap it
    into the already-showing card with a SECOND ``entity_profile`` event. On
    success it carries ``composed=<dict> composing=False``; on failure/timeout/
    null it carries ``composed=None composing=False`` so the HUD drops the
    "composing…" spinner and falls back to rendering the raw profile. Never
    raises, never blocks a turn.

    One RETRY after a short wait: a slow first composition can outlive our
    timeout while still finishing (and caching) server-side — the second POST
    then returns the cached result instantly.Nentity_profileFprofiler%   	composingz!  [entity] composed emit failed: r   )r'   timesleep_COMPOSE_RETRY_Sr   r   r   r   )shownr+   r%   r   s       r    _compose_and_emitr1   Y   sr     e$H

#$!%(Bw%	1 B1#a&#,@AABs   A 	A0A++A0c                   | xs dj                         } | sd| g fS t        |       }|d| g fS |j                  d      sd| g fS |j                  d      xs i }t        |j                  d      xs |       j                         xs | }|j                  d      xs g D cg c]4  }t        |      j                         rt        |      j                         6 }}	 t	        d|dd	
       t        |       t        j                  t        ||fd	      j                          d||fS c c}w # t
        $ r)}t        dt        |      dd         d||fcY d}~S d}~ww xY w)u  The TRUTHFUL core every entry point shares. STAGE 1: fetch /profile
    (≤3s) and emit the card with ``composed=None composing=True`` — it pops
    instantly. STAGE 2: a daemon thread POSTs /dossier and a second emit swaps
    the composed narrative in. Returns (status, shown_name, also_matched):
      'shown'   — the card IS on the HUD
      'unknown' — no such entity in memory; NO card was emitted
      'error'   — memory unreachable / emit failed; NO card on screen
    Callers must only claim a card is visible when status == 'shown' — the old
    fire-and-forget version let the brain say "on your HUD, sir" while nothing
    showed (Ahmed asked for "reval clients", not an entity, and got gaslit).r   errorNentityunknownr	   also_matchedr)   Tr*   z  [entity] emit failed: r   targetargsdaemonr0   )r   r!   r&   r   r   r   r   _mark_shown	threadingThreadr1   start)r	   r+   entr0   aalsor   s          r    _showrB   o   sc    JBDr""D!Gr"";;x 4$$
++h

%2C'4(..08DE%,[[%@%FB%F %Fq1v||~ FLLN%FD &wN -UG4D ""'%'UD!!  &(Q78%%&s$    9D'D, ,	E5EEEc                    | xs dj                         } | syt        |       \  }}}|dk(  rd| dS |dk(  rd| dS |r$t        |      d	k(  r|d
   n|d
    d}d| d| dS d| dS )u  Brain-tool / control-file entry: run ``_show`` and return a short SPOKEN
    confirmation string (plain prose, TTS-safe) the MOMENT the card is up — it
    NEVER waits for composition. Callers on the event loop wrap it in
    asyncio.to_thread / a daemon thread. Never raises.r   zI need a name to show, sir.r5   zI don't have anything on z
 yet, sir.r3   zI couldn't reach memory for u   , sir — no card, I'm afraid.   r   z and otherszShowing u   , sir — I also know a .z's profile, sir.)r   rB   len)r	   statusr0   rA   extras        r    show_entityrI      s    
 JBD,+FE4*5'<<-eW4RSSt9>Q$q'+/F% 8qAAeW,--    ENTITY_COMPOSE_RETRY_S8g     @   r   >L   allandanyarebutcandidforr&   gothadhasherhimhishowletnewnotnowoneourr   seeshesirthetwowaswhowhyyepyesyoucalldoesfromhaveherehomeintojustlikeneednopeokaysometaskteamtextthatthemtheythiswantwerewhatwhenwillwithworkyeahyourahmedemailphonetherethesethosetodaywherejarvispleasethankstonighttomorrow        F)mappatternat
refreshing_NAMES	_COOLDOWNc                    t         5  t        j                         t        | j	                         <   d d d        y # 1 sw Y   y xY wN)_names_lockr-   	monotonicr   lower)r	   s    r    r;   r;      s&    	"&.."2	$**, 
s	   *:Ac                    	 t         j                  j                  dt        j                  i      } t        j
                  dd|  d      }i }|j                  d      xs g D ]  }t        |j                  d      xs d      j                  d	      s1t        |j                  d
      xs d      j                         }t        |      t        k  s|j                         t        v r|||j                         <    d}|rCt        d |D        t        d      }t        j                   ddj#                  |      z   dz         }t$        5  |t&        d<   |t&        d<   t)        j(                         t&        d<   ddd       t+        dt        |       d       t$        5  dt&        d<   ddd       y# 1 sw Y   :xY w# t,        $ r[}t+        dt        |      dd         t$        5  t)        j(                         t&        d<   ddd       n# 1 sw Y   nxY wY d}~d}~ww xY w# 1 sw Y   yxY w# t$        5  dt&        d<   ddd       w # 1 sw Y   w xY wxY w)u\  Rebuild the known-entity matcher from GET /graph — the lightest endpoint
    that enumerates entities (/export is a full dump). Entity nodes come back as
    ``{"id": "ent:<key>", "text": <name>, "kind": "entity:<type>"}``. Runs on a
    daemon thread with a bounded timeout; on any failure the last good matcher
    stays in place. Never raises.r
   r   z/graph?   r   nodeskindr   zentity:r}   Nc              3  F   K   | ]  }t        j                  |        y wr   )reescape).0ts     r    	<genexpr>z!_refresh_names.<locals>.<genexpr>   s     6A299Q<s   !T)keyreversez
(?<!\w)(?:|z)(?!\w)r   r   r   z'  [entity] known-entity set refreshed (z names)z$  [entity] name-set refresh failed: r   Fr   )r   r   r   r   r   r   r&   r   
startswithr   rF   _MIN_LENr   _STOPsortedr   compilejoinr   r   r-   r   r   )r   r   toksnnmr   altsr   s           r    _refresh_namesr      s   )\\##Wn.C.C$DE!!%72$C'''"(b(AquuV}*+66yAQUU6](b)//1B2w!RXXZ5%8!D )  66CNDjj$!?*!LMG F5M 'F999;F4L  	7D	{'JK #(F<  [ [
  '4SVDS\NCD99;F4L [['
 [[#(F<  [[s~   E	F> .F29 F> 
H%2F;7F> >	H" H'H	HH	HH1 H""H1 %H.1I8
I	IIIc                 <   t        j                         syt        5  t        j                         t        d   z
  t
        k  } | s	t        d   r
	 ddd       ydt        d<   ddd       t        j                  t        d      j                          y# 1 sw Y   3xY w)u   Kick a background refresh if the matcher is stale and none is in flight.
    Non-blocking — returns at once; the daemon thread does the network.Nr   r   T)r8   r:   )
r   r   r   r-   r   
_REFRESH_Sr<   r=   r   r>   )recents    r    _maybe_refreshr      sw     !!#	))+t,
:VL) 
  $|	 

 N48>>@ 
s   2B	BBc                   t                | xs dj                         }|sg S t        5  t        d   }t        d   }ddd       g S g }t	               }|j                  |      D ]Y  }j                  |j                  d      |j                  d            }||vs8|j                  |       |j                  |       [ |S # 1 sw Y   xY w)u1  Known entity display-names named in ``text``, by Unicode word-boundary
    match against the cached name set (2+ chars, generic words skipped). Does NO
    network on the hot path — it only kicks a background refresh — and returns
    [] until the set is first populated. Safe to call every utterance.r   r   r   Nr   )
r   r   r   r   setfinditerr&   r
   addappend)r}   r   r   mappinghitsseenmdisps           r    mentioned_entitiesr      s    
 	A		#- 
 	DDa {{1771:qwwqz2tHHTNKK	 !
 K 
s   CCc                     	 ddl m}  t        t        | dd      xr | j                  j
                        S # t        $ r Y yw xY w)u   True while a background worker is running — auto-show stays out of the
    way during an active task dispatch (best-effort, never raises).r   
agent_poolpoolNF)voicer   boolgetattrr   _runningr   r   s    r    _agents_busyr     sF    $GJ5 1"002 	2 s   36 	AA)z\b(?:pull up|bring up|show me|open up)\s+(?:everything\s+(?:we|you)\s+know\s+about\s+|everything\s+about\s+|the\s+profile\s+(?:of|for|on)\s+|a\s+profile\s+(?:of|for|on)\s+)?(.{2,60}?)[\s.!?]*$z\b(?:who\s+is|who's|tell me (?:everything )?about|what do (?:we|you) know about|everything (?:we|you) know about)\s+(.{2,60}?)[\s.!?]*$)uN   (?:من هو|مين هو|وريني|ورني|اعرض)\s+(.{2,60}?)[\s.!?؟]*$uF   (?:كل (?:شي|شيء) (?:نعرفه? )?عن)\s+(.{2,60}?)[\s.!?؟]*$zV^(?:the|a|his|her|their|this|that|please|profile of|about)\s+|\s+(?:please|now|again)$c                   	 t         D ]  }|j                  | xs d      }|s|j                  d      xs dj                         }d}|r3||k7  r.|t        j                  d|      j                         }}|r||k7  r.t        |      dk  rt        |      }|rt        |t              n|}t        j                         }|t        j                  d|j                         z   d      z
  dk  rd	|fc S |t        d|j                         z   <   t        |      \  }}	}
||	fc S  	 y# t        $ r$}t!        d
t#        |      dd         Y d}~yd}~ww xY w)u  If the utterance is an explicit profile ask, show the card INLINE
    (≤3s fetch — truth beats speed here) and return (status, name):
      ('shown', X)   — the card IS on the HUD
      ('unknown', X) — no such entity; nothing was shown
      ('error', X)   — memory unreachable; nothing was shown
    Returns None when the utterance isn't a profile ask. Prefers a KNOWN
    entity named in the tail; otherwise sends the tail itself — the server
    resolves fuzzily.r   rD   NrM   r   q:r   g      $@r0   z!  [entity] explicit_ask skipped: r   )_EXPLICIT_RESsearchr
   r   _TAIL_STRIPsubrF   r   maxr-   r   r   r&   r   rB   r   r   r   )r}   rxr   tailprevknownr	   r`   rG   r0   _alsor   s               r    explicit_askr   2  sF   B-B		$*"%AGGAJ$"++-DD44<!;??2t#<#B#B#Dd 44<4y1}&t,E*/3u#&TD.."CY]]4$**,#6<<tC&-0IdTZZ\)*#(; FE5E?"#  (   B1#a&#,@AABs+   A;D& >A5D& 4.D& #D& &	E/EEc                8    t        |       }|r|S t        |        y)u  main.py's ONE integration line: deterministic explicit-ask intercept
    first (always wins), mention auto-show as the fallback. Returns
    (status, name) for an EXPLICIT ask — 'shown' means the card is genuinely
    on the HUD (brain: acknowledge in one line); 'unknown'/'error' mean NO
    card is visible (brain: say so, never claim otherwise). Returns None on a
    non-ask (a passive mention auto-show still pops its card silently and
    needs no brain note).N)r   maybe_autoshow)r}   ress     r    handle_utterancer   S  s!     t
C

4rJ   c                   	 t        |       }|syt        |t              }t        j                         }|t
        j                  d|j                         z   d      z
  dk  ry|t
        d|j                         z   <   |t
        |j                         <   t        j                  t        |fd      j                          y# t        $ r$}t        dt        |      dd	         Y d}~yd}~ww xY w)
u  DETERMINISTIC card on an explicit ask. Called by the brain's
    memory_search tool: when Ahmed asks memory about something that names a
    known entity ("pull up everything about AIN"), the dossier card MUST
    appear — regardless of which tool the brain chose to answer with. Picks
    the LONGEST entity match in the query, fires on a daemon thread, and only
    guards against immediate double-fires (30s), not the 10-min auto-show
    cooldown — an explicit ask always re-shows. Never raises, never blocks.Nr   r   r   g      >@Tr7   z#  [entity] show_for_query skipped: r   )r   r   rF   r-   r   r   r&   r   r<   r=   rI   r>   r   r   r   )querynamesr	   r`   r   s        r    show_for_queryr   b  s    D"5)5c"nntdjjl2C884?),	$%&"%	$**,4'$GMMO D3CF4CL>BCCDs$   C  AC  #AC   	C-	C((C-c                   	 t         j                  j                  dd      dk(  ryt               ryt	        |       D cg c]  }t        |      t        k\  s| }}t        |      dk7  ry|d   }t        j                         }t        5  |t        j                  |j                         d      z
  t        k  r
	 ddd       y|t        |j                         <   ddd       t        j                  t        |fd	      j!                          yc c}w # 1 sw Y   :xY w# t"        $ r$}t%        d
t'        |      dd         Y d}~yd}~ww xY w)u]  AUTO-SHOW entry point for main.py's listen loop — ONE line of integration.
    If Ahmed named exactly ONE strong, known entity and its 10-min cooldown is
    clear, pop its card. Gated by env ENTITY_AUTOSHOW (default on) and suppressed
    while a background agent is running. Never blocks (the fetch runs on a daemon
    thread) and never raises.ENTITY_AUTOSHOW10NrD   r   r   Tr7   z  [entity] auto-show skipped: r   )osenvironr&   r   r   rF   _STRONG_LENr-   r   r   r   r   _COOLDOWN_Sr<   r=   rI   r>   r   r   r   )r}   hstrongr	   r`   r   s         r    r   r   y  s   ?::>>+S1S8>/5O5Q;9N!5Ov;!aynnY]]4::<55C [ '*Idjjl#  	4'$GMMO P
 [
  ?.s1vds|n=>>?sb   #D 
D D DDD +D 
0D:D D3D D DD 	E(EEc                 r    ddl ddlm} m} dd |dddt        i      dfd       } | d	d
|g      S )u   The in-process ``entity`` MCP server — ONE brain tool, show_entity.
    Follows the memory/agent-pool pattern; dict-shorthand marks ``name``
    required.r   N)create_sdk_mcp_servertoolc                    dd| dgiS )Ncontentr}   )typer}    )msgs    r    _textzbuild_server.<locals>._text  s    VS9:;;rJ   rI   uG  Show a full profile card (dossier) of a person/company/project/concept on Ahmed's screen: summary, contacts, relations, timeline of every interaction. ALWAYS call this when he says 'show me X', 'pull up X', 'pull up everything about X', 'who is X', or 'tell me about X' — the card is the answer; speak only a brief highlight.r	   c                  K   t        | j                  dd            j                         }|s d      S 	 j                  t        |       d {   } |      S 7 # t
        $ r!} dt        |      d d        cY d }~S d }~ww xY ww)Nr	   r   z"show_entity failed: no name given.zshow_entity failed:    )r   r&   r   	to_threadrI   r   )r9   r	   r   r   r   asyncios       r    show_entity_toolz&build_server.<locals>.show_entity_tool  s      488FB'(..0=>>	@))+t<<C Sz = 	@/At~>??	@sF   4BA  AA  BA   	B
)B?B
 BB

Br4   z1.0.0)r	   versiontools)r   r   returnr   )r9   r   r  r   )r   claude_agent_sdkr   r   r   )r   r   r  r   r   s      @@r    build_serverr    sT     << 
-O
 3- !w/?.@ rJ   )r	   r   r  zdict | None)r0   r   r+   r   r  None)r	   r   r  ztuple[str, str, list])r	   r   r  r   )r	   r   r  r  )r  r  )r}   r   r  z	list[str])r  r   )r}   r   r  ztuple | None)r   r   r  r  )r}   r   r  r  )/__doc__
__future__r   r   r   r<   r-   urllib.parser   r   r   voice.eventsr   r!   r'   r1   rB   rI   floatr   r&   r/   r   r   r   r   	frozensetr   Lockr   r   __annotations__r   r;   r   r   r   r   r   
IGNORECASEr   r   r   r   r   r   r  )ps   0r    <module>r     sg  < # 	 	      2$<0B, "F.. (@#FG 

 	 
 
	 innd#UK K	4 3
")J
A0 8  8!Ar}}- 8   bjjm  	 bjj !#0
BD.?<is   .#D%D*