
    *QjKp                     F   U d Z ddlmZ ddlZddlZddlZddlZddZddZ	ddZ
ddZddZ	 	 	 d	 	 	 	 	 	 	 dd	Z	 	 d	 	 	 ddZdZdZddZdaddZddZddZdddZddZddZh dZddZ	 d	 	 	 ddZ edddddd d!dd"d#g       ed$d%dd&ddd'dd(dg       ed)d*dd+ddd,dd-dg       ed.d/i        ed0d1d2dd3did2g       ed4d5dd6ddd7ddg d8d9d:d;g        ed<d=i        ed>d?i        ed@dAdBd dCdig        edDdEddFdddGddHdIdJg       edKdLdIddMdidIg       edNdOi        edPdQddRdddSdddTddUddg       edVdWdddXdidg       edYdZi        ed[d\d]dd^did]g       ed_d`d]ddadid]g       edbdci        edddeddfdd
dgdg dhdiddjddkdldidmdnddoddpdlgdqd
drdg dsdiddtdddugdvd:dwdlgdqdxg dx       edydzi        ed{d|ddd}didg      gZd~ed<   ddZddZddZ ddZ!ddZ"ddZ#ddZ$ddZ%ddZ&ddZ'ddZ(ddZ)ddZ*ddZ+ddZ,ddZ-ddZ.ddZ/ddZ0ddZ1ddZ2ddZ3ddZ4ddZ5ddZ6ddZ7i ded$ed)e d.e!d0e"d4e$d<e%d>e&d@e'dDe)dKe+dNe,dPe-dVe/dYe0d[e1d_e2e3e5e6e7dZ8ddZ9y)u  Tool definitions + executors for the gateway brain.

Two backends, both reached over plain async HTTP (httpx):
  * The memory service (Graphiti/FalkorDB on Railway) — same endpoints and
    bearer-header shape as ``voice/memory_control.py`` / ``voice/tasks_control.py``
    (MEMORY_API_URL + MEMORY_API_KEY, group namespace MEMORY_GROUP).
  * Supabase (the Ultron master DB) via PostgREST RPCs — the exact call shape
    copied from ``dashboard/server.js``: POST ``{SUPABASE_REST}/rpc/<name>``
    with ``{apikey, Authorization: Bearer <SUPABASE_SECRET>}`` headers.

Tools exposed to the LLM (OpenAI function-calling schema, ``TOOL_SPECS``):
  memory_search, memory_remember,
  task_add, tasks_list, task_done,
  ultron_leads, ultron_team_activity, ultron_stats, ultron_contact_stats.

The HTTP is funnelled through ``_mem_request`` and ``_rpc`` so tests can
monkeypatch those two functions and never touch the network.
    )annotationsNc                 `    t         j                  j                  dd      j                  d      S )NMEMORY_API_URL /osenvirongetrstrip     ;/Users/ahmed/devFolder/Ultron/claude-voice/gateway/tools.py_mem_urlr      s#    ::>>*B/66s;;r   c                 B    t         j                  j                  dd      S )NMEMORY_API_KEYr   r	   r
   r   r   r   r   _mem_keyr   !   s    ::>>*B//r   c                 B    t         j                  j                  dd      S )NMEMORY_GROUPahmedr   r   r   r   _groupr   %   s    ::>>.'22r   c                 `    t         j                  j                  dd      j                  d      S )NSUPABASE_RESTr   r   r   r   r   r   _restr   )   s"    ::>>/2.55c::r   c                 B    t         j                  j                  dd      S )NSUPABASE_SECRETr   r   r   r   r   _sup_keyr   -   s    ::>>+R00r   c                  K   ddl }t               }|st        d      ||z   }|r%|dt        j                  j                  |      z   z  }ddi}t               }	|	rd|	 |d<   |j                  |	      4 d{   }
|
j                  | ||||nd
       d{   }|j                          |j                  }|rt        j                  |      ni cddd      d{    S 7 k7 L7 	# 1 d{  7  sw Y   yxY ww)z@Call the memory service. Returns parsed JSON ({} on empty body).r   NzMEMORY_API_URL not set?Content-Typeapplication/jsonBearer Authorizationtimeoutheadersjson)httpxr   RuntimeErrorurllibparse	urlencoder   AsyncClientrequestraise_for_statuscontentr)   loads)methodpathparamsbodyr&   r*   baseurlr(   keyclientrespraws                r   _mem_requestr>   2   s     
 :D344
+CsV\\++F33312G
*C
%,SE?    11V^^FC151At $ M Mll"%tzz#2 211M 2111sZ   A6C?8C$9C?<C*C&8C*C?C(C?&C*(C?*C<0C31C<8C?objectc                ~  K   ddl }t               }|st        d      t               }|d| dd}|j	                  |      4 d{   }|j                  | d|  ||xs i 	       d{   }|j                          |j                         cddd      d{    S 7 Z7 77 	# 1 d{  7  sw Y   yxY ww)
uD   Call a Supabase PostgREST RPC — same shape as dashboard/server.js.r   NzSUPABASE_REST not setr#   r"   )apikeyr$   r!   r%   z/rpc/r'   )r*   r   r+   r   r/   postr1   r)   )	fnargsr&   r*   restr:   r(   r;   r<   s	            r   _rpcrF   J   s      7D233
*C13G   11V[[D6rd!3W&*jb ! 2 2yy{	 2112 2111sZ   AB=B"B=!B(,B$-#B(B=B&B=$B(&B=(B:.B1/B:6B=)maybemightpossiblyperhapszi thinkznot surearoundroughlyapproximatelyzor soprobablyzcould bezi guess)	signed	confirmed
definitelyzfor sureagreedpaid	committed	finalizedz	locked intextc                    | xs dj                         t        fdt        D              ryt        fdt        D              ryy)Nr   c              3  &   K   | ]  }|v  
 y wNr   .0wts     r   	<genexpr>z_confidence.<locals>.<genexpr>e   s     
!5a165   g?c              3  &   K   | ]  }|v  
 y wrY   r   rZ   s     r   r^   z_confidence.<locals>.<genexpr>g   s     
"6a166r_   g      ?g      ?)lowerany_FIRM_HEDGE)rV   r]   s    @r   _confidencere   c   s:    	A

!5
!!

"6
""r   c                    | a y)zBmain registers an async callback fired after any routine mutation.N)_ROUTINE_HOOK)rC   s    r   set_routine_hookrh   s   s	     Mr   c                 b   K   t         	 t                d {    y y 7 # t        $ r Y y w xY wwrY   )rg   	Exceptionr   r   r   _notify_routine_changerk   y   s5      	/!! !! 		s(   /    /  	,/,/numberc                    dj                  d t        | xs d      D              }|syddj                  |dd       z   S )u   A phone number rendered for the model/logs so a full number is never
    echoed or read aloud — only the last three digits survive.r   c              3  B   K   | ]  }|j                         s|  y wrY   )isdigit)r[   chs     r   r^   zmask_number.<locals>.<genexpr>   s     D"3Brzz|R"3s   z(no number)zending  N)joinstr)rl   digitss     r   mask_numberrv      sA     WWD#fl"3DDFsxxrs,,,r   c                ^  K   	 t        ddt               | d       d{   }g }|j                  dg       D ]O  }|j                  d      xs |j                  d	      xs dj	                         }|s<|j                  d
|        Q dj                  |      S 7 {# t        $ r Y yw xY ww)u  A short 'index' of recent long-term memories for the system prompt.

    The desktop reads a local memory/MEMORY.md; the phone has no filesystem
    memory, so we summarise the newest facts from the service instead. Never
    raises — returns "" on any failure.GETz	/memories)grouplimitr6   Nr   memoriesrV   fact- 
)r>   r   rj   r   stripappendrs   )rz   outlinesmtxts        r   memory_index_textr      s      28(U(KM M EWWZ$uuV}3f3::<LL2cU$ % 99UM s>   B-B BB AB-5'B-B 	B*'B-)B**B-c                    K   	 t        dddt               i       d{   } | j                  d      xs dj	                         S 7 '# t        $ r Y yw xY ww)zBAhmed's north-star profile from the memory service. "" on failure.rx   z/valuesry   r{   Nr   rV   )r>   r   rj   r   r   )r   s    r   values_textr      sX      	7FH:MNN GGFO!r((** O s7   AA	 AA	 #AA	 		AAAAc                   K   | xs dj                         }t        |      sy	 t        dd|t               ddt	        |      d       d{    y7 # t
        $ r Y yw xY ww)	zFire-and-forget passive save after an exchange, with a simple v1
    salience gate. Mirrors the desktop's auto-remember intent: only durable-
    looking utterances are stored, as observations. Never raises.r   NPOST	/remembergatewayobservationrV   ry   sourcekind
confidencer7   )r   _is_salientr>   r   re   rj   )	user_textr]   s     r   passive_rememberr      sm      
b!Aq>6;)*VX+4m/:1~!?@ 	@ 	@  s:   !A#(A AA A#A 	A A#A  A#>   	thank you
good nightgood morninghinookyobyeheyhmmsupyescoolnicenopeokaystopwaityeahhellocheersthanks	goodnightc                    | j                         }|j                         j                  d      t        |      dk  ryt        v ry|j                  d      rd|v rt        fddD              syy)	zCheap v1 gate: keep declarative, substantive utterances; drop greetings,
    one-liners, and bare questions (those are asks, not facts to store).z!.   Fr    rq   c              3  &   K   | ]  }|v  
 y wrY   r   )r[   clows     r   r^   z_is_salient.<locals>.<genexpr>   s      0KIAHIr_   )rememberz	note thatzfor the recordT)r   ra   r   len_TRIVIALendswithrb   )rV   r]   r   s     @r   r   r      sl     	

A
'')

4
 C
1v{
hzz#3!8C 0KI0K -Kr   namec                $    d| |d||xs g dddS )Nfunctionr?   )type
propertiesrequired)r   description
parameters)r   r   r   )r   r   r   r   s       r   _fnr      s.     & ($N
 r   memory_searchu  Search Ahmed's long-term memory by meaning and get back connected facts. Use whenever the answer might depend on something he told you before — a person, client, meeting, number, price, project or plan. Read every fact returned; the specific one is often not first.stringz$What to recall, in natural language.)r   r   integerz!Max facts to return (default 10).)querykr   memory_rememberu   Store a durable new fact Ahmed just told you (a preference, a person, a decision, a number). Saving is quick — acknowledge and move on. Do NOT use for transient chit-chat.zOne clear fact to remember.z1fact|preference|event|observation (default fact).)rV   r   task_adduu   Add a task or reminder to Ahmed's shared, cross-device list. Use for 'remind me to…', 'add a task', 'I need to…'.z0Short imperative task, e.g. 'Call the supplier'.zdISO local time for a timed reminder, e.g. 2026-07-08T17:00:00 (Asia/Riyadh). Omit for a plain to-do.)rV   due
tasks_listz[List Ahmed's open tasks and reminders. Use for 'what are my tasks', 'what do I have to do'.	task_donez=Mark a task or reminder complete by its id (from tasks_list).idzThe task id.ultron_leadszSearch the Ultron lead database (Saudi business leads scraped from Google Maps). Use when Ahmed asks about leads, businesses, or prospects in a city.z.Free-text search (company/category), optional.z+City filter, e.g. 'Al Khobar'. Default all.)allr   mobileu?   'mobile' = has a WhatsApp-able Saudi 05… number. Default all.)r   enumr   )r   cityphone_filterultron_team_activityzWho on the agency team contacted which leads recently (last 30 days). Use for 'what has the team been doing', outreach activity.ultron_statszThe Ultron dashboard funnel/summary numbers (total leads, verified emails, phones, recent haul). Use for 'how many leads do we have'.ultron_contact_statszsOutreach contact volume per day over the last N days (per client). Use for 'how much outreach did we do this week'.daysz&Look-back window in days (default 30).
save_placeu   Remember a place by a short label so Ahmed can later say 'navigate to the office'. Use when he names a location — 'this is my gym', 'save home as…'. Store an address, or GPS as 'lat,lng' (e.g. from a new-location prompt).z'Short name, e.g. 'work office', 'home'.z>A street address, OR 'lat,lng' coordinates like '26.30,50.20'.)labeladdress_or_coordsr   r   	get_placez]Look up a saved place by its label. Use before navigating or when he refers to a named place.zThe place label.list_placesz;List every place Ahmed has saved (labels + where they are).save_contactu   Remember a person's phone number so Ahmed can later say 'call my father'. Use when he gives you a name + number. Numbers are private — just confirm you saved it, never read the number back.z#Who this is, e.g. 'father', 'Sara'.zTheir phone number.z1Optional relationship, e.g. 'family', 'supplier'.)r   rl   relationget_contactu   Look up a saved contact by name. Returns the name/relation and a MASKED number (last digits only) — to actually dial, use call_contact.zWho to look up.list_contactszRList everyone in Ahmed's saved phonebook (names + relations; numbers stay masked).sms_watch_addu   Add a sender to the SMS watch-list — texts from it get forwarded to you to summarise/act on. Senders like 'SNB-AlAhli', 'Barq', or '*' for ALL senders.senderz#Sender id, or '*' for every sender.sms_watch_removez(Remove a sender from the SMS watch-list.zSender id to stop watching.sms_watch_listz1List the SMS senders currently on the watch-list.routine_createu   Create a routine that fires automatically — e.g. 'each morning at seven give me a brief of my tasks' (time 07:00 daily → speak_brief tasks) or 'when I get to the office, read my tasks' (place trigger). Resolve times yourself in 24h Asia/Riyadh.z(Short unique name, e.g. 'morning brief'.zWhen it fires.timeplaceboth)r   r   z24h time 'HH:MM' (time/both).arrayr   z1Weekdays mon..sun; omit or ['daily'] = every day.)r   itemsr   z!A saved place label (place/both).)r   atr   r   )r   r   r   r   zWhat Jarvis does when it fires.speak_briefsay_textrunz!For say_text/run: what to say/do.taskszFor speak_brief: which brief.)r   rV   briefr   triggeractionroutine_listz;List Ahmed's routines (name, when they fire, what they do).routine_cancelz Cancel/delete a routine by name.zThe routine name.z
list[dict]
TOOL_SPECSc           
     V  K   t        | j                  dd            j                         }|syt        dt	        t        | j                  d      xs d      d            }t        dd	||t               d
       d {   }|j                  dg       }|syg }|D ]w  }|j                  d      rdnd}|j                  d      xs dd d }|j                  d|j                  dd       | |rd| dndz   d|j                  dd       dz          y dj                  |      S 7 w)Nr   r   zmemory_search failed: no query.   r   
      rx   z/search)qr   ry   r{   factsz (nothing relevant in memory yet)linkedz [connected]	refers_tor~   r}   z  (re: )  [id r   r    ]r   )
rt   r   r   maxminintr>   r   r   rs   )	rD   r   r   r   r   r   ftagwhens	            r   _memory_searchr    s.    DHHWb!"((*A0As3txx}*+R01AUI*+!fh$GI ICGGGR E1E !hnRk"(b#2.r!%%+,SE2-1'$q)r;dC 0134 	5  99UIs   A<D)>D'?B)D)c                "  K   t        | j                  dd            j                         }|syt        | j                  d      xs d      j                         xs d}t        dd|t	               d|t        |      d	
       d {    y7 w)NrV   r   z memory_remember failed: no text.r   r}   r   r   r   r   r   zRemembered.)rt   r   r   r>   r   re   )rD   rV   r   s      r   _memory_rememberr    s     txx#$**,D1txx)6*002<fD
v{%)FH'0$+6t+<>? ? ? 	?s   BBBBc                :  K   t        | j                  dd            j                         }|sy|t               dd}| j                  d      rt        | d         |d<   t	        dd|	       d {    d
| |j                  d      rd|d    dz   S dz   S 7 (w)NrV   r   ztask_add failed: no task text.jarvis)rV   ry   r   r   r   z/taskr   zAdded: z (reminder set for r   )rt   r   r   r   r>   )rD   rV   r7   s      r   	_task_addr    s     txx#$**,D/68x@Dxx$u+&U
vwT
222dV7;xx$T%[M3P QLNP Q 3s   A.B0B1)Bc                ~  K   t        dddt               i       d {   }|j                  dg       }|syg }|D ]k  }|j                  d      rd|d   d d	 j                  d
d       dnd}|j	                  d|j                  dd       | d|j                  dd       d       m dj                  |      S 7 w)Nrx   z/tasksry   r{   r   zNo open tasks.r   z  (due    Trq   r   r   r~   rV   r   r   r    r   r   )r>   r   r   replacer   rs   )_argsr   r   r   r]   r   s         r   _tasks_listr    s     UHgvx5HI
ICGGGR EE%%, 5#2..sC89;$& 	r!%%+,SEdC8H7IKL  99U Js   B=B;BB=c                   K   t        | j                  dd            j                         }|syt        dd|t	               d       d {    y7 w)	Nr   r   ztask_done failed: no task id.r   z
/task/done)r   ry   r   zMarked done.)rt   r   r   r>   r   )rD   tids     r   
_task_doner    sL     
dhhtR 
!
'
'
)C.
v|vx2P
QQQ Rs   A	AAAc                P    t        | t              r| S t        | t              r| gS g S )zERPCs return either a list of rows or a single JSON object. Normalise.)
isinstancelistdict)results    r   _rowsr    s'    &$&$xIr   c                  K   t        | j                  dd            j                         }t        | j                  d      xs d      j                         xs d}t        | j                  d      xs d      j                         xs d}t        d|d|dd|d ddd	d

       d {   }t	        |      }|syg }|d d D ]  }|j                  d      xs |j                  d      xs d}|g}	|j                  d      r|	j                  t        |d                |j                  d      r|	j                  t        |d                |j                  d      r|	j                  t        |d                |j                  ddj                  |	      z           t        |       ddj                  |      z   S 7 w)Nr   r   r   r   r   search_leadsF   r   )
r   
p_categoryp_cityp_fresh
p_verifiedp_phonep_clientp_assigned_onlylimoffzNo matching leads.companyr   z	(unnamed)categoryphoner~   u    · z leads:
r   )rt   r   r   rF   r  r   rs   r   )
rD   r   r   r)  resrowsr   rr   partss
             r   _ultron_leadsr.    s    DHHWb!"((*Atxx(5)//1:UD(1E288:CeE^et%U!	&  C :D#E#2YuuY?155=?K55LLQz]+,55=LLQvY(55>LLQwZ)TFKK../  $i[	"TYYu%555)s   B$G &F='DG c                   K   t        dddi       d {   }t        |      }|syt        j                  |d d d      S 7 ,w)Nget_team_activityp_range30dz%No team activity in the last 30 days.   Fensure_ascii)rF   r  r)   dumps)r  r*  r+  s      r   _ultron_team_activityr7    sF     (9e*<=
=C:D6::d3Bie44	 >s   AA-Ac                n   K   t        dddi       d {   }t        j                  |d      d d S 7 w)Nget_dashboard_statsr1  r   Fr4    )rF   r)   r6  )r  r*  s     r   _ultron_statsr;    s9     *Y,>?
?C::c.u55 @s   535c                   K   t        | j                  d      xs d      }t        dd| di       d {   }t        j                  |d      d d S 7 w)	Nr      get_contact_statsr1  dFr4  r:  )r  r   rF   r)   r6  )rD   r   r*  s      r   _ultron_contact_statsr@    sW     txx%2&D(9aj*AB
BC::c.u55 Cs   3AAAc                >   t        | xs d      j                  dd      j                  d      D cg c]  }|s|	 }}t        |      dk7  ry	 t	        |d         t	        |d         }}d|cxk  rd	k  rn yd
|cxk  rdk  r||fS  yyc c}w # t
        $ r Y yw xY w)z6Parse 'lat,lng' (or 'lat lng') into floats, else None.r   rq   ,   Nr   r   iZ   iL   )rt   r  splitr   float
ValueError)spr-  latlngs        r   _parse_coordsrM    s    AG,,S#6<<SAGA1QQAEG
5zQq?E%(OS cR !C.3.Cx / H
  s   BBB 	BBc                d  K   t        | j                  dd            j                         }t        | j                  dd            j                         }|r|syd|i}t        |      }|r|\  |d<   |d<   n||d<   t	        j
                  t        j                  |       d {    d| d	S 7 
w)
Nr   r   r   z>save_place failed: need a label and an address or coordinates.rK  rL  addresszSaved ''.)rt   r   r   rM  phone_storeput_item
KIND_PLACE)rD   r   wherepayloadcoordss        r   _save_placerW    s     "%&,,.E,b1288:EOe$G5!F)/&"	


{55w
???UG2 @s   B!B0#B.$B0c                    | j                  d      r| j                  d       d| d    S d| v r#d| v r| j                  d       d| d    d| d    S t        | j                  dd            S )NrO  r   z: rK  rL  , r   )r   rt   )rJ  s    r   
_fmt_placerZ  !  sw    uuY%%.!AiL>22zeqj%%.!AeH:R%z::quuWb!""r   c                   K   t        | j                  dd            j                         }t        j                  t        j
                  |       d {   }|sd| dS t        |      S 7 w)Nr   r   zNo place saved as 'rP  )rt   r   r   rQ  get_itemrS  rZ  )rD   r   rJ  s      r   
_get_placer]  )  sa     "%&,,.E"";#9#95AAA$UG2..a= 	B   AA-A+A-c                   K   t        j                  t         j                         d {   }|sydj                  d |D              S 7 w)NzNo places saved yet.r   c              3  8   K   | ]  }d t        |      z     ywr~   N)rZ  )r[   rJ  s     r   r^   z_list_places.<locals>.<genexpr>5  s     95aTJqM)5   )rQ  
list_itemsrS  rs   r  r   s     r   _list_placesre  1  s@     (()?)?@@E%9995999 A   'AA	 Ac                  K   t        | j                  dd            j                         }t        | j                  dd            j                         }|r|sy||d}t        | j                  dd            j                         }|r||d<   t        j                  t        j
                  |       d {    d| dS 7 
w)	Nr   r   rl   z.save_contact failed: need a name and a number.)r   rl   r   zSaved .)rt   r   r   rQ  rR  KIND_CONTACT)rD   r   rl   rU  rels        r   _save_contactrk  9  s     txx#$**,D(B'(..0Fv?v.G
dhhz2&
'
-
-
/C
!



{77
AAAD6 Bs   B4C6C7Cc           	         | j                  d      r	d| d    dnd}| j                  d       | dt        | j                  dd             S )Nr   z (r   r   r   u    — rl   )r   rv   )r   rj  s     r   _fmt_contactrm  F  sO    #$55#4Bq}oQ
"CeeFm_SE{15523F'G&HIIr   c                   K   t        | j                  dd            j                         }t        j                  t        j
                  |       d {   }|sd| dS t        |      S 7 w)Nr   r   zNo contact saved for 'rP  )rt   r   r   rQ  r\  ri  rm  )rD   r   r   s      r   _get_contactro  K  sa     txx#$**,D"";#;#;TBBA'vR00? 	Cr^  c                   K   t        j                  t         j                         d {   }|sydj                  d |D              S 7 w)NzNo contacts saved yet.r   c              3  8   K   | ]  }d t        |      z     ywra  )rm  )r[   r   s     r   r^   z!_list_contacts.<locals>.<genexpr>W       ;UTLO+Urb  )rQ  rc  ri  rs   rd  s     r   _list_contactsrs  S  s@     (()A)ABBE'99;U;;; Crf  c                   K   t        | j                  dd            j                         }|syt        j                  t        j
                  d|i       d {    |dk(  ryd| dS 7 w)Nr   r   z sms_watch_add failed: no sender.*zWatching all SMS senders now.zWatching texts from rh  )rt   r   r   rQ  rR  KIND_SMS_WATCH)rD   r   s     r   _sms_watch_addrw  [  sj     (B'(..0F1


{99Hf;M
NNN}.!&++ Os   AA+A)A+c                   K   t        | j                  dd            j                         }|syt        j                  t        j
                  |       d {   }|rd| dS | dS 7 w)Nr   r   z#sms_watch_remove failed: no sender.zStopped watching rh  z wasn't on the list.)rt   r   r   rQ  delete_itemrv  )rD   r   ns      r   _sms_watch_remover{  e  sj     (B'(..0F4%%k&@&@&IIA,-vha(RfX=Q3RR 	Js   AA*A(A*c                   K   t        j                  t         j                         d {   }|syddj                  d |D              z   S 7 "w)NzThe SMS watch-list is empty.z
Watching: rY  c              3  R   K   | ]  }t        |j                  d d             ! yw)r   r   N)rt   r   )r[   is     r   r^   z"_sms_watch_list.<locals>.<genexpr>q  s!     #LeCh(;$<es   %')rQ  rc  rv  rs   rd  s     r   _sms_watch_listr  m  sE     (()C)CDDE-$))#Le#LLLL Es   'AA#Ac                   | j                  d      xs i }| j                  d      xs i }|j                  dd      }|j                  d      r|d|d    z  }|j                  d      r|ddj                  |d         z   z  }|j                  d	      r|d
|d	    z  }|j                  dd      }|j                  d      r|d|d    z  }n |j                  d      r|d|d   d d  dz  }| j                  d       d| d| S )Nr   r   r   r    r   rq   r   r   r   z @r   :rV   z "(   "r   u
    — when u    → )r   rs   )r,  trigactr  whats        r   _fmt_routiner  u  s   55!rD
%%/
RC88FC Dxx~!DJ<  xxchhtF|,,,xx"T']O$$7763D
www!CL>""	#c&k#2&'r**eeFm_JtfE$88r   c                  K   t        | j                  dd            j                         }| j                  d      }| j                  d      }|r t        |t              rt        |t              sy|j                  d      dvry|j                  d      d	vry
|d   dv r*t        |j                  dd            j                         sy|d   dv r*t        |j                  dd            j                         syt        j                  t
        j                  |||d       d {    t                d {    d| dS 7 7 w)Nr   r   r   r   z<routine_create failed: need a name, a trigger and an action.r   r   z@routine_create failed: trigger.type must be time, place or both.r   zHroutine_create failed: action.type must be speak_brief, say_text or run.)r   r   r   z=routine_create failed: a time/both trigger needs a time 'at'.)r   r   r   z@routine_create failed: a place/both trigger needs a place label.r   z	Routine 'z' set.)	rt   r   r   r  r  rQ  rR  KIND_ROUTINErk   )rD   r   r   r   s       r   _routine_creater    s-    txx#$**,Dhhy!GXXhFz'40
648PM{{6";;Qzz&!CCYv**3w{{47L3M3S3S3UNv++CGR8P4Q4W4W4YQ


{77(,*0 23 3 3 !
"""tfF##	3 #s$   D$E&E'E8E9EEc                   K   t        j                  t         j                         d {   }|sydj                  d |D              S 7 w)NzNo routines set.r   c              3  8   K   | ]  }d t        |      z     ywra  )r  )r[   r,  s     r   r^   z _routine_list.<locals>.<genexpr>  rr  rb  )rQ  rc  r  rs   rd  s     r   _routine_listr    s@     (()A)ABBE!99;U;;; Crf  c                  K   t        | j                  dd            j                         }|syt        j                  t        j
                  |       d {   }t                d {    |rd| dS d| dS 7 $7 w)Nr   r   zroutine_cancel failed: no name.zCancelled 'rP  zNo routine named ')rt   r   r   rQ  ry  r  rk   )rD   r   rz  s      r   _routine_cancelr    s{     txx#$**,D0%%k&>&>EEA
 
"""%&[b!I.@b,II 	F"s$   AA?A;A?(A=)A?=A?)r   r   r   r   c                   K   t         j                  |       }|d|  dS 	  ||xs i        d{   S 7 # t        $ r}|  dt        |      dd  cY d}~S d}~ww xY ww)zEDispatch a tool call. Returns a compact string for the model to read.Nzunknown tool ''z	 failed:    )
_EXECUTORSr   rj   rt   )r   rD   rC   es       r   executer    sk     		B	zvQ''0
^### 0yQ//0s=   A 7 57 A 7 	A AAA AA )returnrt   )NNg      4@)r4   rt   r5   rt   r6   dict | Noner7   r  r&   rG  r  r  )Ng      >@)rC   rt   rD   r  r&   rG  r  r?   )rV   rt   r  rG  )r  None)rl   rt   r  rt   )r   )rz   r  r  rt   )r   rt   r  r  )rV   rt   r  boolrY   )
r   rt   r   rt   r   r  r   zlist[str] | Noner  r  )rD   r  r  rt   )r  r  r  rt   )r  r?   r  r  )rI  rt   r  ztuple[float, float] | None)rJ  r  r  rt   )r   r  r  rt   )r,  r  r  rt   )r   rt   rD   r  r  rt   ):__doc__
__future__r   r)   r	   urllib.parser,   rQ  r   r   r   r   r   r>   rF   rd   rc   re   rg   rh   rk   rv   r   r   r   r   r   r   r   __annotations__r  r  r  r  r  r  r.  r7  r;  r@  rM  rW  rZ  r]  re  rk  rm  ro  rs  rw  r{  r  r  r  r  r  r  r  r   r   r   <module>r     s.  & #  	  <03;1 .2+/(,.*.(. !&. 26.0 -1 $)/$
!	0 -&+ ( &*".2" 	I $"HJ AC	D 
		 	+ #!>@"!TV	W 
 
	;"!SU!!/0	1 
	 	"
 G	@A	 	 $"RT"!NP"*":*FG		H 	 	E
 	M
 	;	)!IK 	L
 	  $"KM'//PQ	R
 
%&
( 	#	84FGH			
 E
 	A #!FH$5JK&&34	5 

 	L	(3DEF		
 	

 	 
H#HJ 	K	
 2	H5RST	
 ;
 	' #!KM%$4*2*C"E(0/N P*1VX<N2J"K ,43B#C
$D #)+ %#D)1)K!M)10S!U*2WI1P"R#S "(	*!	+4 	&?'@ E
 *	(3FGH	{a
J aN(		Q
6656
6#:
J
<,SM9$$*<J^' 	 +	
  M 1 M 1 +  < M < ^  ^!" )#$ &%!%+
20r   