+
    Pj_:                    R   R t ^ RIHt ^ RIt^ RIt^ RIt^ RIHtHtH	t	 ]P                  ! R4      t^t]P                  ! R]P                  4      t^tRtR R lt ! R	 R
4      t ! R R4      t ! R R]4      t]	]].]],          3,          t ! R R4      tR R lt ! R R4      tR# )u  The brain: a streaming, tool-calling chat loop over a swappable LLM vendor.

Phase-1 vendor is DeepSeek (OpenAI-compatible chat completions), ported from
the pattern in ``voice/deepseek.py`` and extended with SSE streaming + tool
calling. The sentence-regrouping approach (start speaking sentence one while
the rest still generates) is ported from ``voice/llm.py``.

Vendor-swappable: ``Brain`` speaks a neutral event stream and OpenAI-shaped
message history. To add Anthropic later, implement ``LLMAdapter.stream`` to
translate that history to/from the Anthropic API and yield the same events —
nothing else changes.

Neutral adapter event contract (async iterator of dicts):
  {"type": "text", "text": "<delta>"}          # streamed assistant text
  {"type": "tool_calls", "calls": [ {id,name,arguments}, ... ]}  # 0..1 times
  {"type": "finish", "reason": "stop"|"tool_calls"|...}          # exactly once
)annotationsN)AsyncIterator	AwaitableCallableu   [.!?؟…]['\")\]]?\s|\nu   [*_`#~|]|^\s*[-•]\s+z<<ACT>>c                    V ^8  d   QhRRRR/# )   textstrreturn )formats   "brain.py__annotate__r   *   s     + + + +    c                J    \         P                  RV 4      P                  4       # )z=Remove markdown symbols before a sentence goes to the UI/TTS. )	_MD_STRIPsubstrip)r   s   &r   clean_for_ttsr   *   s    ==T"((**r   c                  >    ] tR t^/tRtR R ltR R ltR R ltRtR	# )
SentenceRegrouperzAccumulates streamed token deltas and emits complete sentences.

``push(delta)`` returns a list of finished sentences (cleaned, [emotion]
tags preserved); ``flush()`` returns whatever remains at end-of-turn.
c                   V ^8  d   QhRR/# r   r
   Noner   )r   s   "r   r   SentenceRegrouper.__annotate__6   s      $ r   c                	    R V n         R# )r   N_bufselfs   &r   __init__SentenceRegrouper.__init__6   s	    	r   c                    V ^8  d   QhRRRR/# )r   r   r	   r
   z	list[str]r   )r   s   "r   r   r   9   s     % % % %r   c                	   V ;P                   V,          un         . p \        P                  V P                   4      pV'       dt   V P                   RVP                  4        P	                  4       pV P                   VP                  4       R V n         V'       d   VP                  \        V4      4       K  K  \        V P                   4      \        8  d   V P                   P                  R^ \        4      pV^(8  d   TM\        pV P                   RV P	                  4       pV P                   VR V n         V'       d   VP                  \        V4      4       EK?  EKB   T Uu. uF  qf'       g   K  TNK  	  up# u upi )TN )
r   _SENTENCE_ENDsearchendr   appendr   len
_MAX_CHUNKrfind)r    r   outmchunkcutss   &&     r   pushSentenceRegrouper.push9   s   		T	$$TYY/A		)AEEG,224 IIaeegh/	JJ}U34 TYY*,iiooc1j9 2Xc:		$3--/ IIcdO	JJ}U34  $3a!3$$$s   #	E:1E:c                   V ^8  d   QhRR/# )r   r
   r	   r   )r   s   "r   r   r   N   s      s r   c                	>    \        V P                  4      pR V n        V# )r   )r   r   )r    tails   & r   flushSentenceRegrouper.flushN   s    TYY'	r   r   N)	__name__
__module____qualname____firstlineno____doc__r!   r2   r7   __static_attributes__r   r   r   r   r   /   s    %* r   r   c                  2    ] tR t^UtRtR R ltR R ltRtR# )
LLMAdapterzCVendor interface. Implement ``stream`` to yield the neutral events.c               $    V ^8  d   QhRRRRRR/# r   messages
list[dict]toolslist[dict] | Noner
   zAsyncIterator[dict]r   )r   s   "r   r   LLMAdapter.__annotate__X   s#     " "Z "-"2E"r   c                	   "   \         h5iN)NotImplementedError)r    rC   rE   s   &&&r   streamLLMAdapter.streamX   s     !!s   	c                   V ^8  d   QhRR/# r   r   )r   s   "r   r   rG   \   s      d r   c                	   "   R # 5irI   r   r   s   &r   acloseLLMAdapter.aclose\   s     s   r   N)r9   r:   r;   r<   r=   rK   rO   r>   r   r   r   r@   r@   U   s    M" r   r@   c                  H    ] tR t^`tRtRR R lltR tR R ltR R	 ltR
t	R# )DeepSeekAdapterz8DeepSeek / OpenAI-compatible streaming chat completions.Nc                    V ^8  d   QhRRRR/# )r   modelz
str | Noner
   r   r   )r   s   "r   r   DeepSeekAdapter.__annotate__c   s     + +: + +r   c                	   \         P                  P                  R R4      P                  R4      V n        T;'       g!    \         P                  P                  RR4      V n        \        \         P                  P                  RR4      4      V n        \        \         P                  P                  RR4      4      V n	        Wn
        VR	J V n        R	# )
DEEPSEEK_URLzhttps://api.deepseek.com/MODELzdeepseek-v4-proBRAIN_TEMPERATUREz0.6BRAIN_MAX_TOKENS1024N)osenvirongetrstripbaserT   floattemperatureint
max_tokens_client_owns_client)r    clientrT   s   &&&r   r!   DeepSeekAdapter.__init__c   s    JJNN688>s 		HHbjjnnW6GH
 0CU!KLbjjnn-?HI"dNr   c                	    V P                   f+   ^ R IpVP                  VP                  R4      R7      V n         V P                   # )Ng      ^@)timeout)rf   httpxAsyncClientTimeout)r    rl   s   & r   _ensure_clientDeepSeekAdapter._ensure_clientl   s7    << ,,U]]55I,JDL||r   c                   V ^8  d   QhRR/# r   r   )r   s   "r   r   rU   r   s        d  r   c                	   "   V P                   '       d<   V P                  e,   V P                  P                  4       G R j  xL
  R V n        R # R # R #  L5irI   )rg   rf   rO   r   s   &r   rO   DeepSeekAdapter.acloser   sB     !9,,%%'''DL ":'s   >A AAc               $    V ^8  d   QhRRRRRR/# rB   r   )r   s   "r   r   rU   w   s#     6= 6=Z 6=-6=2E6=r   c               	  "   \         P                  P                  R R4      pV'       g   \        R4      hRV P                  RVRRRV P
                  RV P                  /pV'       d
   W$R	&   R
VR&   V P                  4       pRRV 2RR/p/ pRpVP                  RV P                  R,           WFR7      ;_uu_4       GRj  xL
 p	V	P                  4        V	P                  4         Rj  xL
  p
V
'       d   V
P                  R4      '       g   K+  V
\        R4      R P                  4       pVR8X  d    EM \        P                   ! V4      pTP                  R4      ;'       g    / .pT^ ,          pTP                  R4      ;'       g    / pTP                  R4      '       d   RRRTR,          /5x  TP                  R4      ;'       g    .  F  pTP                  R^ 4      pTP%                  TRRRRRR/4      pTP                  R4      '       d   TR,          TR&   TP                  R 4      ;'       g    / pTP                  R4      '       d   TR,          TR&   TP                  R4      '       g   K  TR;;,          TR,          ,          uu&   K  	  TP                  R!4      '       g   EK  TR!,          pEK  RRR4      GRj  xL
  V'       d*   RRR"\'        V4       Uu. uF  pVV,          NK  	  up/5x  RR#R$T;'       g    R%/5x  R#  ELR EL,  \"         d     EKB  i ; iEDALr Lf  + GRj  xL 
 '       g   i     L}; iu upi 5i)&DEEPSEEK_API_KEYr   zDEEPSEEK_API_KEY not setrT   rC   rK   Trc   re   rE   autotool_choiceAuthorizationzBearer zContent-Typezapplication/jsonNPOSTz/chat/completions)jsonheaderszdata:z[DONE]choicesdeltacontenttyper   
tool_callsindexidname	argumentsfunctionfinish_reasoncallsfinishreasonstop)r]   r^   r_   RuntimeErrorrT   rc   re   ro   rK   ra   raise_for_statusaiter_lines
startswithr*   r   r{   loads
ValueError
setdefaultsorted)r    rC   rE   keypayloadrh   r|   tool_bufr   resplinedataobjr}   choicer~   tcidxslotfnis   &&&                  r   rK   DeepSeekAdapter.streamw   s    jjnn/49::TZZd4++$//
 $G%+GM"$$&"gcUO!#57$&==5H)H&- ! @ @ @CG!!#"..0 5 5d4??7#;#;CLM*0028#**T*C ''),44 

7+11r99Y''!6653CDD 99\288b8B&&!,C#..dBKDFDvvd||%'XT

+11rBvvf~~')&zVvvk**[)R_<) 9 ::o..#O4F=@ @> <&2BC2BQXa[[2BCE Ex6+;+;V<<E@5 "  1@ @ @ @B Ds   A%M(AM?L M!L/$L*(L
)L*, L/(L/6LL/$$L/	L/#'L/A*L/6L/ L/54L//L/;ML-MM#M
5M
ML*L'	"L/&L'	'L/-M/M	5L86
M	M	M)rf   rg   ra   re   rT   rc   )NN)
r9   r:   r;   r<   r=   r!   ro   rO   rK   r>   r   r   r   rR   rR   `   s    B+ 
6= 6=r   rR   c                  6    ] tR t^tRtR R ltRR R lltRtR# )	Braina  Runs the streaming tool-calling loop for one user turn.

``messages`` is the full OpenAI-shaped list (system + history + the new
user message). It is MUTATED in place: assistant/tool messages produced
during the turn are appended, so the caller's history stays current.
c               (    V ^8  d   QhRRRRRRRR/# )	r   adapterr@   
tool_specsrF   	tool_execzToolExecutor | Noner
   r   r   )r   s   "r   r   Brain.__annotate__   s+     # #
 #8I #/#48#r   c                	@    Wn         T;'       g    R V n        W0n        R # rI   )r   r   r   )r    r   r   r   s   &&&&r   r!   Brain.__init__   s    $,,"r   Nc               $    V ^8  d   QhRRRRRR/# )r   rC   rD   on_tokenCallable[[], None] | Noner
   zAsyncIterator[str]r   )r   s   "r   r   r      s(     B B* B5B)Br   c                 "   Rp\        \        4       F  p\        4       pRp. pV P                  P	                  WP
                  4        Rj  xL
  pVP                  R4      p	V	R8X  dL   Ve   V'       g   Rp V! 4        WhR,          ,          pVP                  VR,          4       F  p
V
5x  K
  	  Km  V	R8X  g   Kv  VP                  R4      ;'       g    . pK  	  R#  L  \         d     Lni ; iDTP                  4       pT'       d   T5x  T'       g/   TP                  4       '       d   TP                  R	R
RT/4        R# TP                  R	R
RT;'       g    RR\        T4       UUu. uFV  w  rRTP                  R4      ;'       g    RT 2RRRRTP                  RR4      RTP                  R4      ;'       g    R//NKX  	  Mu uppi upp/4       \        T4       EF  w  r \        P                  ! TP                  R4      ;'       g    R4      p\        T\         4      '       g   / pM  \"         d    / p Mi ; iTP                  RR4      p T P$                  '       d   T P%                  Y4      G Rj  xL 
 MRT R2pM0  \         d#   pRT R\'        T4      R,           2p Rp?MRp?ii ; iTP                  R	RRTP                  R4      ;'       g    RT 2RT/4       EK  	  EK  5i)u   Yield reply sentences (raw, [emotion] tags intact) as they complete.

``on_token`` (optional) fires once, on the FIRST streamed text token of
the turn — the caller uses it for time-to-first-token timing.Fr   Nr   r   Tr   r   role	assistantr   r   call_r   r   r   z{}ztool 'z' is unavailablez
' failed: :N   Ntooltool_call_id)range_MAX_TOOL_ROUNDSr   r   rK   r   r_   	Exceptionr2   r7   r   r)   	enumerater{   r   
isinstancedictr   r   r	   )r    rC   r   fired_roundregrouptext_accr   evkindsentencer6   r   cargsr   resultes   &&&               r   run	Brain.run   s     ,-F')GH "E LL////J 2 2bvvf~6>+E $!$J 6
*H$+LLF$<& %=\)FF7O11rE# .t 	m2  ) ! ! K ==?D
>>##OOV[)X$NO OO8++t !*% 0	 !1 155;55E!+vz&!%%*;"-quu[/A/I/IT"KL !1		 	 "%(::aeeK&8&@&@DAD%dD11!! DuuVR(E!% %)NN4$>>> &tf,<=  ! E%dV:c!fTl^DFE!/t1K1K%s!*F!4 5 )s   AK:
C4C!C4%K:8K:;C#9K: K:	K:!C4#C1.K:0C11K:K:K:0.K:K:0G2G	G!K:.'H6H65K:6IK:IK:J.JJJK:J<J72K:7J<<(K:%K:)r   r   r   rI   )r9   r:   r;   r<   r=   r!   r   r>   r   r   r   r   r      s    #B Br   r   c                    V ^8  d   QhRRRR/# )r   historyrD   r
   r   )r   s   "r   r   r     s      : * r   c                    . pV  Fs  pVP                  R4      pVP                  R4      pVR9   g   K.  \        V\        4      '       g   KF  VP                  4       '       g   K^  VP	                  RVRV/4       Ku  	  V# )u"  The fast lane's view of the SHARED history: plain user/assistant text
turns only. Tool-call scaffolding produced by full-brain turns (assistant
messages with ``content=None`` + ``tool_calls``, and ``tool`` results) is
dropped — the fast model has no tools and just needs the conversation.r   r   )userr   )r_   r   r	   r   r)   )r   r-   r.   r   r   s   &    r   _fast_historyr     sj    
 CuuV}%%	"((Z-E-EMMOOJJi9:  Jr   c                  B    ] tR tRtRtR R ltR R ltRR R	 lltR
tR# )FastLanei  u  The cheap, tool-less front lane over a fast model (deepseek-v4-flash).

Streams spoken sentences from a compact-persona prompt; if the model emits
the escalation sentinel ``<<ACT>>`` (per that prompt) it signals a handoff
so the caller silently reruns the turn on the full ``Brain``. Ported from
the desktop's ``voice/local_brain.py`` two-lane design.

``run`` yields tuples:
  ``("say", sentence)``  a spoken sentence (raw, [emotion] tag intact)
  ``("handoff", None)``  escalate — emitted ALONE and FIRST, then the
                         generator stops (nothing spoken, nothing to commit)
  ``("final", text)``    LAST on a clean finish — the full assistant text
                         (sentinel stripped) for the caller to commit to the
                         shared history
c               $    V ^8  d   QhRRRRRR/# )r   r   r@   system_promptr	   r
   r   r   )r   s   "r   r   FastLane.__annotate__(  s!     + +
 +3 +4 +r   c                	    Wn         W n        R # rI   r   r   )r    r   r   s   &&&r   r!   FastLane.__init__(  s    *r   c               $    V ^8  d   QhRRRRRR/# )r   r   rD   user_contentr	   r
   r   )r   s   "r   r   r   ,  s"     ? ?j ? ?&?r   c                	\    R RRV P                   /.\        V4      ,           R RRV/.,           # )r   systemr   r   )r   r   )r    r   r   s   &&&r   build_messagesFastLane.build_messages,  s=    (It/A/ABC()FI|<=> 	?r   Nc               (    V ^8  d   QhRRRRRRRR/# )	r   r   rD   r   r	   r   r   r
   zAsyncIterator[tuple]r   )r   s   "r   r   r   2  s*     3 3 33 353+3r   c               	L  "   V P                  W4      p\        4       pR pRpRpV P                  P                  VR4      p	 V	  Rj  xL
  p
V
P	                  R4      R8w  d   K"  Ve   V'       g   Rp V! 4        WjR,          ,          pV'       g7   \        V9   d,   R	5x   \        V	RR4      pVe    V! 4       G Rj  xL
  R# R# VP                  V
R,          4       F/  p\        V9   g   VP                  4       '       g   K&  RpRV35x  K1  	  K   L  \
         d     Li ; i Lh  \
         d     R# i ; iDTP                  4       pT'       gJ   \        T9   d?   R	5x   \        T	RR4      pTe&    T! 4       G Rj  xL 
  R#   \
         d     R# i ; iR# T'       d   \        T9  d
   RpRT35x  T'       g?   R	5x   \        T	RR4      pTe&    T! 4       G Rj  xL 
  R#   \
         d     R# i ; iR# TP                  \        R 4      P                  4       pT'       d   RT35x  \        T	RR4      pTe&    T! 4       G Rj  xL 
  R#   \
         d     R# i ; iR#   \        T	RR4      pTe$    T! 4       G Rj  xL 
  i   \
         d     i i ; ii ; i5i)
r   FNr   r   TrO   sayfinal)handoffN)r   r   r   rK   r_   r   FAST_HANDOFFgetattrr2   r   r7   replace)r    r   r   r   rC   r   fullspoker   rK   r   rO   r   r6   	committeds   &&&&           r   r   FastLane.run2  s     &&w=#% $$Xt4(	" , ,b66&>V+' E 
 6
"!5++* VXt4F! (NN "+ !(RZ 8H#x/x~~7G7G  E (++	 !9, % < #  M #& ==?D\T1'' VXt4F! (NN   " D0dm#''
 VXt4F! (NN   " \26<<>I	**VXt4F! (NN   " VXt4F! (NN   "s  >J$I+ D<DD<%I+ 1I+ 4D;(I+ #J$5
D* ?D( D* J$;I+ I+ D<D%"I+ $D%%I+ (D* *D95J$8D99J$<I+ I+ &J$8
F
 FF
 J$
FJ$FJ$I+ &I+ I+ J$
G, $G'%G, *J$,G;7J$:G;;J$ +I+ ,I+ 3J$
I II J$I&"J$%I&&J$+J!>
JJ	JJ!JJ!JJ!!J$r   rI   )	r9   r:   r;   r<   r=   r!   r   r   r>   r   r   r   r   r     s     +?3 3r   r   )r=   
__future__r   r{   r]   retypingr   r   r   compiler&   r+   	MULTILINEr   r   r   r   r   r@   rR   r	   r   ToolExecutorr   r   r   r   r   r   <module>r      s   $ #  	 	 5 5
 

67
 JJ0",,?	  +
" "L M=j M=b dYs^34P PhN Nr   