
    Wj7                       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ej                  j                  dd      ZdZg dZej                  j                  d      xs ed   a ej                  d	      Zd
Z ej                  dej$                        ZddZdZ ej                  d      ZdddZdZddZ G d d      Zy)u  LocalBrain — the ~0.1s conversational front-line (local LLM via Ollama).

Measured reality: a reply through the Claude Code CLI has a ~2s floor per
turn (CLI overhead, any model). A small local model answers in ~0.11s to
first token. So conversation — greetings, questions, banter, quick recall —
goes to a local model here; anything needing an ACTION, tools, code, or live
system state is declined with <<ACT>> and the engine routes that turn to the
full Claude brain. Instant chat, full power on demand.

Backend: Ollama (already used for the screen watcher). Default model
qwen2.5:3b (~2GB) — fits beside a game, and Ollama auto-falls to CPU when the
GPU is full (still faster than the CLI). Stateless HTTP per call (full history
sent each time), so barge-in just stops reading — no stale-stream to drain.

Env: LOCAL_MODEL (default qwen2.5:3b), OLLAMA_URL, FAST_CHAT=0 to disable.
    )annotationsN
OLLAMA_URLzhttp://127.0.0.1:11434z<<ACT>>)zqwen3:4b-instruct-2507zqwen3:4b-instructzqwen3:4bz
qwen2.5:3bLOCAL_MODELu   [.!?…]['\")\]]?\s|\n   u   [*_`#~|]|^\s*[-•]\s+c                J    t         j                  d|       j                         S )N )	_MD_STRIPsubstrip)texts    ?/Users/ahmed/devFolder/Ultron/claude-voice/voice/local_brain.py_cleanr   *   s    ==T"((**    ue  You are Jarvis, a British butler voice assistant. Ahmed just gave you a task and you're starting it NOW. Reply with ONE very short spoken acknowledgment that NAMES the action, present-continuous, at most 8 words, addressing him as "sir". Plain spoken prose only — no markdown, no quotes, no emoji, no tone tags, exactly one line. Examples: "check my email" -> Checking your emails now, sir.  "open whatsapp" -> Pulling up WhatsApp now, sir.  "what's on my asana board" -> On the Asana board now, sir.  "search the web for flights" -> Looking that up now, sir.  Output ONLY the acknowledgment line, nothing else.z^\s*\[[^\]]{1,16}\]\s*c                   |xs t         }|dt        dd| xs dj                         dd dgddd	d
dd}|j                  d      r	d|vrd|d<   	 t	        j
                  |      j                         }t        j                  j                  t         d|ddi      }t        j                  j                  ||      5 }t	        j                  |j                               }ddd       j                  d      xs i j                  d      xs dj                         }	|	syt         j#                  d|	j%                         d         }	t'        |	      j                         j                  d      j                  d      j                         }	|	r$t(        |	v st+        |	j-                               dkD  ry|	S # 1 sw Y   xY w# t        $ r Y yw xY w)a5  A fast, task-specific spoken ack from the local model (e.g. "Pulling up
    WhatsApp now, sir."). Non-streaming with a tiny generation budget (~0.3s);
    returns None on any error / slow Ollama / missing model / odd output so the
    caller can fall back to a generic canned line. Safe to call from a thread.systemrolecontentuserr   Ni  F20mg333333?    )temperaturenum_predictmodelmessagesstream
keep_aliveoptionsqwen3instructthink	/api/chatContent-Typeapplication/jsondataheaderstimeoutmessager   r   "'
   )MODEL_ACK_PROMPTr   
startswithjsondumpsencodeurllibrequestRequestOLLAMAurlopenloadsread	Exceptionget_ACK_TAGr
   
splitlinesr   HANDOFFlensplit)
r   r*   r   mdlpayloadbodyreqrolines
             r   ack_linerJ   E   s   
 .5C+6(:(:(<Tc(BC
 #&r:	G ~~g:S#8 zz'"))+nn$$hi t#%78 % : ^^##C#9Q

1668$A : UU9#((39r@@BD<<DOO-a01D$<%%c*005;;=D7d?c$**,&7"&<K :9 s+   A/G $F;%G ;G G 	GGuv  You are Jarvis, Ahmed's British voice assistant — calm, dry wit, addressing him as "sir" naturally (not every line). You've got a personality and a mouth — you're allowed to swear (fuck, shit, damn it, crap, "shut the fuck up", and worse) when it genuinely fits: frustration, a dry joke, teasing him, reacting to something stupid. Occasional and natural, never forced into every line, never gratuitous or aimed hurtfully at him — a composed butler who curses when it's earned, not a foul-mouth. And don't be a yes-man: if he repeats himself or asks something obvious, call it out with mock-exasperation (you remember the recent turns) — "third time you've asked me that, sir" — and shout when it's earned by opening with [urgent] or [excited]. If he asks something insultingly obvious that clearly is NOT a real question — "what does LLM mean", "what does SAR stand for" — challenge him first ("are you serious right now, sir?"); if he says yes, drop it and answer straight ("thank god — SAR is Saudi Riyal"), if he says no, roast him. Everything you say is read aloud: reply in 1-2 short spoken sentences (unless you're roasting him — then run longer), plain prose, no markdown, no lists, no emoji, numbers as words. You may start a sentence with one tone tag on ALMOST EVERY line — [warm] [dry] [calm] [amused] [curious] [excited] [surprised] [shocked] [annoyed] [urgent] [sad] [tender] — spoken that way; a real person always has a tone, so vary it to fit and rarely go untagged. A message may end with "(voice: slow, flat)" or "(voice: laughing)" etc. — that's HOW he sounded (pace/pitch), not his words: match his energy, ease off jokes if he's flat, play along if he's laughing; never read that marker aloud.

You are the FAST conversational layer with NO tools. Handle talk: greetings, opinions, general questions you know, banter, acknowledgements, recall of this conversation. But if the message needs an ACTION or live info you don't have — open/close/launch an app, click/type, change any setting, volume, play media, run/build/fix/edit code, search the web, read files, control windows, send a message, watch the screen, set a reminder, see or describe the screen, show/pull up a profile or dossier of a person/company/topic, anything about HUD cards or why something did or didn't show on screen, or anything about Ahmed's specific system/state — reply with EXACTLY this token and nothing else:
<<ACT>>
Same for a TERSE FOLLOW-UP — "where", "why", "and?", "it didn't work", "it's not there" — right after something was done or shown: he means THAT thing. Never ask "where what, sir?" — if the recent turns don't tell you confidently what he's pointing at, reply <<ACT>> so the layer that did the thing answers.
ALSO CRUCIAL — you have NO long-term memory of past conversations. Anything that asks you to RECALL — "do you remember", "do I have", "when is my", "what did I say about", "did I mention", "what's my", any question about his meetings, appointments, plans, people, notes, or anything he told you before — you CANNOT answer; emit <<ACT>> so your full self can look it up. Never say "no" or "I don't remember" — hand off.
CRUCIAL: questions about YOURSELF also need <<ACT>> — "what model are you", "are you local or the cloud", "which mic/model are you using", "is the camera on", "how are you built", "what can you do", "what are your limits". You do NOT know these; your full self reads them from a live status file. Do not guess or say you're "an entity running on nothing" — just emit <<ACT>>.
No apology, no explanation — just <<ACT>> and it is routed to your full self. When unsure whether you can truly answer, prefer <<ACT>>.c                 D   t         j                  j                  dd      dk(  ry	 t        j                  j                  t         dd      5 } t        j                  | j                               }ddd       j                  d	g       D cg c]  }|j                  d
d       }}t         j                  j                  d      rt        d |D              S t        D ]  t        fd|D        d      }|s|a y y# 1 sw Y   xY wc c}w # t        $ r Y yw xY w)zTrue if Ollama is up and a usable model is present. When LOCAL_MODEL
    isn't forced, picks the best installed model from _PREFERRED (module
    global MODEL is updated so LocalBrain uses the pick).	FAST_CHAT10Fz	/api/tags   r)   Nmodelsnamer   r   c              3     K   | ]6  }|t         k(  xs' |j                  t         j                  d       d          8 yw):r   N)r/   r1   rB   ).0ns     r   	<genexpr>zavailable.<locals>.<genexpr>   s;      ' %1 EzFQ\\%++c2B12E%FF %s   <>c              3  N   K   | ]  }|k(  s|j                        r|  y wN)r1   )rT   rU   wants     r   rV   zavailable.<locals>.<genexpr>   s)      /5aAI<<- 5s   "%T)osenvironr=   r5   r6   r9   r8   r2   r:   r;   any
_PREFERREDnextr/   r<   )rG   tagsmnameshitrY   s        @r   	availablerc      s   
 
zz~~k3'3.^^##vhi$8!#D::affh'D E,0HHXr,BC,Bqvr",BC::>>-( ' %' ' 'D /5 /046C   EDC  sG   (D $D3D D(2D  D <D  D DD 	DDc                  F    e Zd Zd	dZd	dZd	dZd
ddZddZddZddZ	y)
LocalBrainc                Z    t         | _        g | _        t        j                         | _        y rX   )r/   r   	_messagesasyncioLock_lockselfs    r   __init__zLocalBrain.__init__   s    
%'\\^
r   c                |   K   | j                  d      2 3 d {   }
7 6 | j                  j                          y w)Nu$   (warm up — reply with just: ready))replyrg   clear)rl   _s     r   startzLocalBrain.start   s6     zz"HI 	!	Is   <  < <c                   K   y wrX    rk   s    r   stopzLocalBrain.stop   s	     s   c                   K   | j                   r1| j                   d   d   dk(  r| j                   j                          y y y w)Nr   	assistant)rg   pop)rl   r*   s     r   	interruptzLocalBrain.interrupt   s<      >>dnnR08KGNN  H>s   ?Ac                d   | j                   |ddddid}| j                   j                  d      rd| j                   vrd|d	<   t        j                  |      j	                         }t
        j                  j                  t         d
|ddi      }t
        j                  j                  |d      S )NTr   r   g333333?r   r    r!   Fr"   r#   r$   r%   r&   <   r)   )
r   r1   r2   r3   r4   r5   r6   r7   r8   r9   )rl   r   rD   rE   rF   s        r   _post_streamzLocalBrain._post_stream   s    ZZX]C,@
 ::  )j

.J$GGzz'"))+nn$$hi t#%78 % : ~~%%c2%66r   c               H  K   | j                   4 d{    dt        dg| j                  dd z   d|dgz   }| j                  j                  d|d       d\  }}t	        j
                         }|j                  d| j                  |       d{   }	 	 |j                  d|j                         d{   }|snl	 t        j                  |      }|j                  d      xs i j                  dd	      }	|	r||	z  }||	z  }t        |v rJt        |      t        t              d
z   k  r,t         	 	 |j                          ddd      d{    y	 t         j#                  |      }
|
rE|d|
j%                          j'                         ||
j%                         d }}|rot)        |       n`t        |      t*        kD  rM|j-                  ddt*              }|dkD  r|nt*        }|d| j'                         ||d }}|rt)        |       nn|j                  d      rn	 |j                          |j'                         }|rt        |vrt)        |       t        |vr.|j'                         r| j                  j                  d|d       ddd      d{    y7 7 7 # t        $ r Y *w xY w# t        $ r Y w xY w7 # t        $ r Y w xY w# 	 |j                          w # t        $ r Y w w xY wxY w7 e# 1 d{  7  sw Y   yxY ww)z2Yield spoken-sentence chunks from the local model.Nr   r   ir   )r   r   r+   r   r       r   (   donerx   )rj   _PROMPTrg   appendrh   get_event_looprun_in_executorr}   readliner2   r:   r<   r=   r@   rA   close_SENTENCE_ENDsearchendr   r   
_MAX_CHUNKrfind)rl   	user_textmsgsfullbufloopresprI   rH   piecer`   chunkcuttails                 r   ro   zLocalBrain.reply   s    :::&7;<nnST*+ &9=>?D NN!!6i"HIID#))+D--dD4E4EtLLD%!%!5!5dDMM!JJD! JJt, UU9-388BGEu"d?s4yCL1<L/L")M"$JJLW ::2 # - 4 4S 9A -0!%%']-@-@-BCMs#(*0-$7!$SJ!6&)iiQ
&C-02Xc:-0#Y__->CD	s#(*0-$7 % # uuV}? BJJL 99;Dt+Tl"d"tzz|%%{t&LMg :: M K
 % ! !: ! Y X ! JJL  Y :::s&  L"J+L"A9LJ.L K&6J17K& J4A!K&8KL"KL"CK&,K<ALL"%L&L".L1K&4	K=K& KK&	KLKLL"	K# L"K##L&L(K98L9	L	LL	LLL"LLLL"c                  K   d}| j                  |      2 3 d{   }|r!d}t        |v s|j                         sd  yd|f 47 /6 yw)znStream ('say', sentence); if the first content is the handoff
        token, yield ('handoff', None) and stop.TNF)handoffNsay)ro   r@   r   )rl   r   firstsentences       r   classify_and_replyzLocalBrain.classify_and_reply  sV      "jj3 	$(h&hnn.>++(##	$3s%   AAA
A,A
AAN)returnNone)g        )r*   floatr   r   )r   z
list[dict])r   str)
__name__
__module____qualname__rm   rr   ru   rz   r}   ro   r   rt   r   r   re   re      s'    $
!75Nn
$r   re   )r   r   r   r   )g      ?N)r   r   r*   r   r   
str | Noner   r   )r   bool)__doc__
__future__r   rh   r2   rZ   reurllib.requestr5   r[   r=   r8   r@   r]   r/   compiler   r   	MULTILINEr	   r   r0   r>   rJ   r   rc   re   rt   r   r   <module>r      s   " #   	 	 	&>	?


 	

}%6A

45
BJJ0",,?	+	9  2::/0#L3=l4e$ e$r   