
    Pj_:                    8   d Z ddlmZ ddlZddlZddlZddlmZmZm	Z	  ej                  d      ZdZ ej                  dej                        ZdZd	Zdd
Z G d d      Z G d d      Z G d de      Ze	eegee   f   Z G d d      ZddZ G d d      Zy)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|\n   u   [*_`#~|]|^\s*[-•]\s+   z<<ACT>>c                J    t         j                  d|       j                         S )z=Remove markdown symbols before a sentence goes to the UI/TTS. )	_MD_STRIPsubstrip)texts    ;/Users/ahmed/devFolder/Ultron/claude-voice/gateway/brain.pyclean_for_ttsr   *   s    ==T"((**    c                  (    e Zd ZdZddZddZddZy)	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                    d| _         y Nr
   )_bufselfs    r   __init__zSentenceRegrouper.__init__6   s	    	r   c                   | xj                   |z  c_         g }	 t        j                  | j                         }|rj| j                   d |j                          j	                         }| j                   |j                         d  | _         |r|j                  t        |             nt        | j                         t        kD  r|| j                   j                  ddt              }|dkD  r|nt        }| j                   d | j	                         }| j                   |d  | _         |r|j                  t        |             nn&|D cg c]  }|s|	 c}S c c}w )N r   (   )
r   _SENTENCE_ENDsearchendr   appendr   len
_MAX_CHUNKrfind)r   r   outmchunkcutss          r   pushz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Ec                >    t        | j                        }d| _        |S r   )r   r   )r   tails     r   flushzSentenceRegrouper.flushN   s    TYY'	r   NreturnNone)r   strr.   z	list[str])r.   r0   )__name__
__module____qualname____doc__r   r)   r,    r   r   r   r   /   s    %*r   r   c                  (    e Zd ZdZ	 	 	 	 ddZddZy)
LLMAdapterzCVendor interface. Implement ``stream`` to yield the neutral events.c                   K   t         wN)NotImplementedError)r   messagestoolss      r   streamzLLMAdapter.streamX   s     !!s   	c                   K   y wr9   r5   r   s    r   aclosezLLMAdapter.aclose\   s	     s   Nr;   
list[dict]r<   list[dict] | Noner.   zAsyncIterator[dict]r-   )r1   r2   r3   r4   r=   r?   r5   r   r   r7   r7   U   s    M"-"2E"r   r7   c                  8    e Zd ZdZdddZd Zd	dZ	 	 	 	 d
dZy)DeepSeekAdapterz8DeepSeek / OpenAI-compatible streaming chat completions.Nc                   t         j                  j                  dd      j                  d      | _        |xs  t         j                  j                  dd      | _        t        t         j                  j                  dd            | _        t        t         j                  j                  dd	            | _	        || _
        |d u | _        y )
NDEEPSEEK_URLzhttps://api.deepseek.com/MODELzdeepseek-v4-proBRAIN_TEMPERATUREz0.6BRAIN_MAX_TOKENS1024)osenvirongetrstripbasemodelfloattemperatureint
max_tokens_client_owns_client)r   clientrQ   s      r   r   zDeepSeekAdapter.__init__c   s    JJNN688>s 		HbjjnnW6GH
 0CU!KLbjjnn-?HI"dNr   c                    | j                   *dd l}|j                  |j                  d            | _         | j                   S )Nr   g      ^@)timeout)rV   httpxAsyncClientTimeout)r   r[   s     r   _ensure_clientzDeepSeekAdapter._ensure_clientl   s7    << ,,U]]55I,JDL||r   c                   K   | j                   r7| j                  *| j                  j                          d {    d | _        y y y 7 wr9   )rW   rV   r?   r   s    r   r?   zDeepSeekAdapter.acloser   s@     !9,,%%'''DL ":'s   6A	AA	c           
    P  K   t         j                  j                  dd      }|st        d      | j                  |d| j
                  | j                  d}|r
||d<   d|d<   | j                         }d	| d
d}i }d }|j                  d| j                  dz   ||      4 d {   }	|	j                          |	j                         2 3 d {   }
|
r|
j                  d      s|
t        d      d  j                         }|dk(  r n'	 t        j                   |      }|j                  d      xs i g}|d   }|j                  d      xs i }|j                  d      rd|d   d |j                  d      xs g D ]  }|j                  dd      }|j%                  |dddd      }|j                  d      r|d   |d<   |j                  d      xs i }|j                  d      r|d   |d<   |j                  d      s|dxx   |d   z  cc<    |j                  d      sa|d   }hd d d       d {    |r"dt'        |      D cg c]  }||   	 c}d d |xs d!d" y 7 7 # t"        $ r Y w xY w6 Y7 M# 1 d {  7  sw Y   ]xY wc c}w w)#NDEEPSEEK_API_KEYr
   zDEEPSEEK_API_KEY not setT)rQ   r;   r=   rS   rU   r<   autotool_choicezBearer zapplication/json)AuthorizationzContent-TypePOSTz/chat/completions)jsonheaderszdata:z[DONE]choicesr   deltacontentr   )typer   
tool_callsindex)idname	argumentsrn   functionro   rp   finish_reason)rk   callsfinishstop)rk   reason)rL   rM   rN   RuntimeErrorrQ   rS   rU   r^   r=   rP   raise_for_statusaiter_lines
startswithr!   r   rf   loads
ValueError
setdefaultsorted)r   r;   r<   keypayloadrX   rg   tool_bufrt   resplinedataobjrh   choiceri   tcidxslotfnis                        r   r=   zDeepSeekAdapter.streamw   s    jjnn/49::ZZ ++//
 $GG%+GM"$$&&-cUO#57$&==5H)H&-w ! @ @CG!!#"..0 5d4??7#;CLM*0028#**T*C ''),4 

7+1r99Y'#)53CDD 99\28b8B&&!,C#..BDFDvvd|%'XT

+1rBvvf~')&zVvvk*[)R_<) 9 ::o.#O4F=@ @> '282BC2BQXa[2BCE E6+;V<<E@5 "  1@ @ @ @B Ds   B"J&$I2%J&(!J	JI5J9JI8 C$J#J*J1J&<J
=J&J!J&5J8	JJJJ
J&JJJJ&)NN)rQ   z
str | Noner.   r/   r-   r@   )r1   r2   r3   r4   r   r^   r?   r=   r5   r   r   rD   rD   `   s)    B+ 
6=-6=2E6=r   rD   c                  2    e Zd ZdZ	 	 	 	 ddZ	 d	 	 	 ddZy)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                6    || _         |xs d | _        || _        y r9   )adapter
tool_specs	tool_exec)r   r   r   r   s       r   r   zBrain.__init__   s    $,"r   Nc                 K   d}t        t              D ]  }t               }d}g }| j                  j	                  || j
                        2 3 d{   }|j                  d      }	|	dk(  r5||s
d}	  |        ||d   z  }|j                  |d         D ]  }
|
 	 T|	dk(  sZ|j                  d      xs g }p y7 m# t        $ r Y Rw xY w6 |j                         }|r| |s&|j                         r|j                  d	|d
        y|j                  d	|xs dt        |      D cg c]G  \  }}|j                  d      xs d| d|j                  dd      |j                  d      xs dddI nc c}}w c}}d       t        |      D ]  \  }}	 t        j                  |j                  d      xs d      }t        |t               si }n# t"        $ r i }Y nw xY w|j                  dd      }	 | j$                  r| j%                  ||       d{  7  nd| d}n*# t        $ r}d| dt'        |      dd  }Y d}~nd}~ww xY w|j                  d|j                  d      xs d| |d        nw)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
   Nrk   r   Trl   rs   	assistantrolerj   rn   call_rq   ro   rp   z{})ro   rp   )rn   rk   rq   )r   rj   rl   ztool 'z' is unavailablez
' failed:    tool)r   tool_call_idrj   )range_MAX_TOOL_ROUNDSr   r   r=   r   rN   	Exceptionr)   r,   r   r    	enumeraterf   r{   
isinstancedictr|   r   r0   )r   r;   on_tokenfired_roundregrouptext_accrs   evkindsentencer+   r   cargsro   resultes                     r   runz	Brain.run   s     ,-F')GH "E LL//$//J 2bvvf~6>+E $!$J 6
*H$+LLF$<& %=\)FF7O1rE# .t 	m2  ) ! ! K ==?D
>>#OO[X$NO OO##+t
 !*% 0	 !11 55;5E!+z*+%%*;/0uu[/A/IT"KL !1		 	 "%(1::aeeK&8&@DAD%dD1!! DuuVR(E!% %)NN4$>>> &tf,<=  ! E%dV:c!fTcl^DFE12t1K%s,2!4 5 )s   A	JCB=CJ1B?8,J%J=C?	CJ
CA"J-AE:9"J:GJG%"J$G%%J;!H)HH)(J)	I2IJI2J)r   r7   r   rB   r   zToolExecutor | Noner.   r/   r9   )r;   rA   r   Callable[[], None] | Noner.   zAsyncIterator[str])r1   r2   r3   r4   r   r   r5   r   r   r   r      s9    #/#48# 9=B5B)Br   r   c                    g }| D ]_  }|j                  d      }|j                  d      }|dv s*t        |t              s;|j                         sL|j	                  ||d       a |S )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   rj   )userr   r   )rN   r   r0   r   r    )historyr$   r%   r   rj   s        r   _fast_historyr     sa    
 CuuV}%%	"((Z-EMMOJJ9:  Jr   c                  6    e Zd ZdZddZ	 	 ddZ	 d	 	 	 d	dZy)
FastLaneuE  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                     || _         || _        y r9   )r   system_prompt)r   r   r   s      r   r   zFastLane.__init__(  s    *r   c                H    d| j                   dgt        |      z   d|dgz   S )Nsystemr   r   )r   r   )r   r   user_contents      r   build_messageszFastLane.build_messages,  s5    "t/A/ABC()"|<=> 	?r   Nc               t  K   | j                  ||      }t               }d}d}d}| j                  j                  |d       }		 |	2 3 d {   }
|
j	                  d      dk7  r||s
d}	  |        ||
d   z  }|s/t        |v r'd  t        |	dd       }|	  |        d {    y y |j                  |
d         D ]$  }t        |v s|j                         sd}d|f & 7 # t
        $ r Y w xY w7 S# t
        $ r Y y w xY w6 |j                         }|s?t        |v r7d 	 t        |	dd       }|!	  |        d {  7   y # t
        $ r Y y w xY wy |rt        |vr	d}d|f |s7d 	 t        |	dd       }|!	  |        d {  7   y # t
        $ r Y y w xY wy |j                  t        d      j                         }|rd	|f t        |	dd       }|!	  |        d {  7   y # t
        $ r Y y w xY wy # t        |	dd       }|!	  |        d {  7   w # t
        $ r Y w w xY ww xY ww)
Nr
   Frk   r   T)handoffNr?   sayfinal)r   r   r   r=   rN   r   FAST_HANDOFFgetattrr)   r   r,   replace)r   r   r   r   r;   r   fullspoker   r=   r   r?   r   r+   	committeds                  r   r   zFastLane.run2  s     &&w=#% $$Xt4(	" ,b66&>V+' E 
 6
"!5++* VXt4F! (NN "+ !(RZ 8H#x/x~~7G  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  ?H8H DC%	DH )C'0H H8
C8 "C6#C8 'H8)<H %D'	C30H 2C33H 6C8 8	DH8DH8 H (H88
E	 EE	 H8		EH8EH8H 4H8
F FF H8	F!H8 F!!H8%-H H8"
G3 ,G/-G3 2H83	G?<H8>G??H8H5
H%H!H%$H5%	H1.H50H11H55H8)r   r7   r   r0   r.   r/   )r   rA   r   r0   r.   rA   r9   )r   rA   r   r0   r   r   r.   zAsyncIterator[tuple])r1   r2   r3   r4   r   r   r   r5   r   r   r   r     s3     +?&? 9=353+3r   r   )r   r0   r.   r0   )r   rA   r.   rA   )r4   
__future__r   rf   rL   retypingr   r   r   compiler   r"   	MULTILINEr   r   r   r   r   r7   rD   r0   r   ToolExecutorr   r   r   r5   r   r   <module>r      s   $ #  	 	 5 5
 

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