Ë
    qýuj‚u  ã                   ó  — d 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l
mZ ddlmZmZmZmZmZmZ ddlmZ ddlmZmZmZ dd	lmZ dd
lmZ ddlmZ ddlm Z m!Z! ddl"m#Z# ddl$m%Z%m&Z&  ejN                  e(«      Z)dZ*ddddddddœidgdœdœdœdddddddœdddœdddœdg d ¢d!œdd"dœdd#dœdd$dœd%œg d&¢dœdœdœdd'd(ddd)dœdd*dœd+œd,d-gdœdœdœdd.d/dd0dd1dœid0gdœdœdœdd2d3ddd4dœdd5dœdd6dœd7œg d7¢dœdœdœdd8d9dd:d;dd<dd=gid<dd=gid<did>œd?gdœd@œid:gdœdœdœddAdBdddCdœddDdœdEœdFgdœdœdœddGdHdddIdœddJdœdKœdgdœdœdœgZ+dL„ Z,dM„ Z-dN„ Z.dO„ Z/dP„ Z0dQ„ Z1dR„ Z2dSe3fdT„Z4dU„ Z5dV„ Z6dW„ Z7dX„ Z8e,e-e/e0e6e1e7e8dYœZ9dZ„ Z:y)[zžNative tool-calling layer: schemas + executors for actions the chat model can take.

Replaces the legacy fenced signal blocks (ticket_creation_signal, etc.).
é    N)Úsettings)Úclient)ÚTICKET_CONSOLIDATION_PROMPT)Úcreate_ticketÚ
get_ticketÚget_ticket_by_threadÚinsert_feedbackÚsearch_vector_dataÚupdate_ticket©Údownload_link)Úemail_configuredÚis_valid_emailÚ
send_email)Úget_email_settings)Úget_embeddings)Úset_intervention_status)Úsearch_productsÚto_card)ÚQuotaExceededError)Úget_redis_clientÚpublish_event)zdon't have enough informationzdo not have enough informationÚfunctionÚsearch_knowledge_baseuû  Search this business's knowledge base for facts. You have NO built-in knowledge about this business â€” its products, pricing, services, features, policies, hours, contact details or support processes are ONLY available through this tool. You MUST call it before answering any question about the business. If the results do not answer the question, call it again with a differently worded or more specific query before concluding you cannot answer. Returns matching content chunks, each with its source URL.ÚobjectÚqueryÚstringzeWhat to search for. Use the user's own wording, or a rephrasing targeting the specific fact you need.)ÚtypeÚdescription)r   Ú
propertiesÚrequired)Únamer   Ú
parameters)r   r   Úcreate_or_update_ticketu0  Create a support ticket, or update the session's existing ticket with a new issue. Use when the user reports an issue/complaint/bug, or wants to book a demo/meeting or contact the team/sales. ASK THE USER WHAT THE ISSUE IS before raising a ticket. Do not assemble the problem description from context or assume what went wrong â€” if they have only said something vague like 'it is broken', ask them what exactly is happening. The content field must be their own account of the problem, not your guess. Alongside that, collect their email address and phone number (and their name if not already known); ask for what is missing together rather than one question per turn. Never reuse an email or phone number that came from search results or a document â€” only what the user gives you here. You may write the short heading yourself, and set priority from how they describe the urgency. STOP ASKING once you have (a) their description of the problem and (b) their email and phone â€” call this tool immediately in that same turn. Do NOT then ask for a website URL, error messages, screenshots, what changed, or any other extra detail: the support team will follow up for those. One or two questions total, never an interrogation. If the session already has a ticket, do NOT re-ask contact details; just ask what the new issue is.zUser's namezShort ticket headingzDetailed issue description)ÚLowÚMediumÚHigh)r   ÚenumzUser's email addresszUser's phone numberzOptional other contact info)Ú	user_nameÚheadingÚcontentÚpriorityÚemailÚ
contact_noÚany_other_contact_medium©r*   r+   r,   Úsubmit_feedbackuû  Store the user's feedback about this conversation or the service. You MUST call this tool whenever the user expresses satisfaction or dissatisfaction â€” whether or not you asked them for it. That includes: any rating ('5 out of 5', '4/5', 'I'd give it a 3'), any verdict on whether you helped ('that was helpful', 'this didn't help at all', 'you've been useless'), and any praise or complaint about the service. Thanking the user in text WITHOUT calling this tool loses their feedback permanently â€” the reply text is not stored anywhere as feedback. Set `question` to what you asked, or to a short description of what they were reacting to if they volunteered it unprompted. Do NOT call this for a plain 'thanks'/'ok' that carries no opinion about the service.zThe feedback question askedzThe user's feedback answer)ÚquestionÚanswerr2   r3   Úrequest_human_takeoveruì  Notify a real human team member to join this conversation. This is the ONLY way a human is actually notified â€” saying so in text without calling this tool notifies no one. Call it immediately, in the same turn, whenever the user explicitly REQUESTS a human/real person/agent/someone from the team (e.g. 'can I speak to someone', 'I want to talk to a real human', 'this isn't helping, connect me with your team'), or when the request is too complex or unsafe for you to handle safely. Do NOT call this for a QUESTION merely asking about human handoff capability (e.g. 'can this be escalated to a person if needed?', 'do you support human takeover?') â€” answer questions like that from context instead. Keep helping the user until the human joins.ÚreasonzWhy human help is neededr   uÞ  Email information to the user. Use ONLY when the user explicitly asks for something to be emailed to them (e.g. 'email me this', 'can you send that to my inbox', 'mail me the pricing'). Before calling: you MUST have searched the knowledge base for the content, and you MUST have the user's email address and have confirmed it back to them in your previous message ('Shall I send it to x@y.com?'). Never guess or invent an address, and never email an address the user did not give in this conversation. The `body` must be written ONLY from knowledge-base search results and what was already said in this conversation â€” never invent facts, prices or policies to fill out the email. If the knowledge base does not contain the information, tell the user instead of sending a made-up email. NEVER offer to email, and never call this tool, when your answer is that you could not find the information â€” an email saying you do not know is worse than no email. Offer a ticket or a human instead.z3The recipient address, exactly as the user gave it.z@Short, specific subject line, e.g. 'GalaxiQ BrandForge pricing'.z¸Plain text body written from knowledge-base content. Use short paragraphs; '- ' at line start for bullets. Include the source URLs where relevant. No greeting placeholders like [Name].)Úto_emailÚsubjectÚbodyÚattach_resourcesu‘  Attach the source URLs/images your answer relied on so the frontend can render them as clickable buttons or image cards. Every result returned by search_knowledge_base carries a 'source' and may carry an 'images' list. ONLY attach a source that is a real web address starting with http:// or https://. A source that is an uploaded file name such as 'Pricing Sheet.docx' or 'policy.pdf' is NOT a URL -- never pass it here, because it produces a card that leads nowhere. To give the visitor an uploaded file, use send_document instead. You MUST call this tool, in the same turn as your text answer, whenever you used a search result that has a source URL or an image to produce your answer â€” this is REQUIRED, not optional, any time the context you drew on has a source. Never write the raw URL or image link directly into your reply text instead of calling this tool; the link belongs in the tool call, and your reply text should read naturally without it. Only include resources that directly relate to the current answer, and keep a URL and its matching image in the same object. Skip this tool only when no search result you actually used has a source URL or image.Ú	resourcesÚarrayr   Únull)ÚurlÚimageÚlabelr?   )r   ÚitemsÚsend_documentu%  Give the visitor the actual uploaded file â€” the brochure, price list, spec sheet or policy PDF itself â€” as a download link, rather than only describing what is in it. Call this when the visitor asks for the document ('do you have a brochure?', 'send me the pricing PDF', 'can I get that as a file?'), or when you have just answered from a document and handing them the original is obviously useful. Search the knowledge base FIRST: every result carries a 'source', and when that source is an uploaded file name (something ending in .pdf, .docx and so on) that is exactly what you pass as file_name. Never invent a file name â€” only use one you have actually seen in a search result. If the tool reports the document was not found, tell the visitor the file is not available and do not make up a link.z`Exact file name as it appeared in the 'source' field of a search result, e.g. 'pricing-2026.pdf'z#Briefly, what the visitor asked for)Ú	file_namer5   rB   Úrecommend_productsaÂ  Show the visitor actual products, plans or services as cards they can act on, with the business's own buttons. Call this when the visitor is asking WHICH thing to get rather than asking ABOUT something: 'what do you have', 'show me', 'do you sell X', 'which plan suits me', anything with a budget or a category. Do NOT call it for a question about a product they already named -- 'what does BrandForge do', 'does it integrate with Shopify' -- that is search_knowledge_base, and answering in prose is correct there. The tool returns only products this business actually has. If it returns none, say so plainly; never name a product, quote a price, or describe a button that did not come back from this tool.z3What the visitor is looking for, in their own wordsz4Category to narrow to, only if the visitor named one)r   Úcategoryc              ƒ   ó8  K  — | j                  d«      xs dj                  «       }|sg dddœS 	 t        ||d   |j                  d«      ¬«      }t        |d   |d	¬
«      }g }|xs g D ]Û  }|j                  d«      xs i }|j                  d«      xs g }	g }
|	dd D ]Z  }t        |t        «      r&|j                  d«      r|
j                  |d   «       Œ9t        |t        «      sŒJ|
j                  |«       Œ\ |j                  |j                  dd«      |j                  d«      xs |j                  d«      xs d|
dœ«       ŒÝ |t        |«      |sddœS ddœS # t        $ r ‚ t
        $ r*}t        j                  d|› �d¬«       g dddœcY d}~S d}~ww xY w­w)zBRetrieval as a model-chosen action. Returns content, not a status.r   Ú r   zEmpty query.)ÚresultsÚcountÚnoteÚ	tenant_idÚuser_id)rJ   rK   é   ©Úlimitz#Tool search_knowledge_base failed: T©Úexc_infozSearch failed.NÚmetadataÚ
top_imagesé   r=   r+   ÚsourceÚ
source_urlzInternal Knowledge Base)r+   rT   ÚimageszNNo matches. Try a different wording before telling the user you cannot answer.)ÚgetÚstripr   r
   r   Ú	ExceptionÚloggerÚerrorÚ
isinstanceÚdictÚappendÚstrÚlen)ÚargsÚctxr   Ú	embeddingÚhitsÚexrG   ÚhitÚmetarV   Ú
image_urlsÚimgs               ú6/var/www/html/strategist-ai/app/services/chat_tools.pyÚexecute_search_knowledge_baserk   2  s¶  è ø€ à�X‰X�gÓÒ$ "×+Ñ+Ó-€EÙØ¨°>ÑBÐBðEÜ" 5°C¸Ñ4DÈcÏgÉgÐV_ÓN`Ôaˆ	Ü! # kÑ"2°IÀQÔGˆð €GØŠz�rò ˆØ�w‰w�zÓ"Ò( bˆØ—‘˜,Ó'Ò-¨2ˆØˆ
Ø˜"˜1�:ò 	'ˆCÜ˜#œtÔ$¨¯©°¬Ø×!Ñ! # e¡*Õ-Ü˜C¤Õ%Ø×!Ñ! #Õ&ð		'ð
 	�‰Ø—w‘w˜y¨"Ó-Ø—h‘h˜xÓ(Ò_¨D¯H©H°\Ó,BÒ_ÐF_Ø ñ
õ 	ðð Ü�W“áð bñð ð &*ñ	ð øô+ ò ØÜò EÜ�‰Ð:¸2¸$Ð?È$ˆÔOØ¨Ð3CÑDÕDûðEüs<   ‚,F¯1E Á BFÃ&A5FÅFÅ-FÆFÆFÆFÆFc              ƒ   óL  K  — |d   }|d   }|j                  d«      }|j                  d«      }	 t        ||«      }|�rS	 t        j                  |d   |d   | d   | d   ¬«      }t        j
                  j                  j                  t        j                  d|d	œgd
¬«      }|j                  d   j                  j                  j                  «       }	|	j                  dd«      j                  dd«      j                  «       }	t        j                   |	«      }
|
j                  d| d   «      |
j                  d|d   › d| d   › �«      | d   dœ}t)        ||d   |«      }|r(t+        ||d   dd|d   › d�«      ƒ d{  –—†  d|d   dœS d|d   dœS t-        | «      }dt/        j0                  «       j2                  dd j5                  «       › �|d<   |r||d<   |r||d<   t7        |||¬«      }|r't+        ||d   d d!|d   › �«      ƒ d{  –—†  d"|d   dœS dddœS # t"        $ r:}t$        j'                  d|› �«       | d   |d   › d| d   › �| d   dœ}Y d}~�Œ
d}~ww xY w7 Œä7 ŒY# t8        $ r ‚ t"        $ r)}t$        j'                  d#|› �d$¬%«       dddœcY d}~S d}~ww xY w­w)&zDCreate a new ticket, or consolidate into the session's existing one.rJ   Ú	thread_idr)   rK   r*   r+   )Úold_headingÚold_contentÚnew_headingÚnew_contentÚuser)Úroler+   i   )ÚmodelÚmessagesÚmax_completion_tokensr   z```jsonrF   z```z

---
r,   r0   zConsolidation LLM call failed: z

---
[Update]:
NÚ	ticket_idÚticket_updatedzTicket z& has been consolidated with new issuesÚupdated)Ústatusrw   r[   zTICK-é   )rm   Únew_ticket_createdzA new ticket has been created: Úcreatedz%Tool create_or_update_ticket failed: TrO   )rW   r   r   ÚformatÚopenai_clientÚchatÚcompletionsÚcreater   Ú	LLM_MODELÚchoicesÚmessager+   rX   ÚreplaceÚjsonÚloadsrY   rZ   r[   r   Ú_publish_ticket_eventr]   ÚuuidÚuuid4ÚhexÚupperr   r   )ra   rb   rJ   rm   r)   rK   ÚexistingÚcons_promptÚcons_responseÚrawÚconsÚupdate_dataÚcons_exÚsuccessÚticket_dataÚnew_idre   s                    rj   Úexecute_create_or_update_ticketr˜   W  s  è ø€ à�KÑ €IØ�KÑ €IØ—‘˜Ó$€IØ�g‰g�iÓ €Gð36Ü'¨	°9Ó=ˆÚðÜ9×@Ñ@Ø (¨Ñ 3ÀÈ)ÑATØ $ Y¡¸TÀ)¹_ô�ô !.× 2Ñ 2× >Ñ >× EÑ EÜ"×,Ñ,Ø'-¸+ÑFÐGØ*-ð !Fó !�ð
 $×+Ñ+¨AÑ.×6Ñ6×>Ñ>×DÑDÓF�Ø—k‘k )¨RÓ0×8Ñ8¸ÀÓC×IÑIÓK�Ü—z‘z #“�à#Ÿx™x¨	°4¸	±?ÓCØ#Ÿx™x¨	°h¸yÑ6IÐ5JÈ)ÐTXÐYbÑTcÐSdÐ3eÓfØ $ ZÑ 0ñ�ô $ I¨x¸Ñ/DÀkÓRˆGÙÜ+¨I°xÀÑ7LÐN^Ø.5°h¸{Ñ6KÐ5LÐLrÐ,sóu÷ uð uà"+¸(À;Ñ:OÑPÐPØ%°H¸[Ñ4IÑJÐJä˜4“jˆØ%*¬4¯:©:«<×+;Ñ+;¸B¸QÐ+?×+EÑ+EÓ+GÐ*HÐ#Iˆ�KÑ ÙØ'0ˆK˜Ñ$ÙØ%,ˆK˜	Ñ"Ü˜y¨+ÀÔKˆÙÜ'¨	°;¸{Ñ3KÐMaØ*IÈ+ÐVaÑJbÐIcÐ(dóf÷ fð fà'°kÀ+Ñ6NÑOÐOØ!°Ñ5Ð5øô3 ò Ü—‘Ð>¸w¸iÐHÔIà# I™Ø"*¨9Ñ"5Ð!6Ð6JÈ4ÐPYÉ?ÐJ[Ð\Ø $ ZÑ 0ñ–ûðúðuøðfùô ò ØÜò 6Ü�‰Ð<¸R¸DÐAÈDˆÔQØ!°Ñ5Õ5ûð6üs¢   ‚-J$°I& Á DH Å-I& Å=I"Å>I& Æ	J$Æ
I& ÆJ$ÆA8I& È
I$ÈI& ÈJ$ÈI& ÈJ$È	IÈ%/IÉI& ÉIÉI& É$I& É&J!É8JÊJ!ÊJ$ÊJ!Ê!J$c              ƒ   óÌ   K  — 	 t        | |«      }|rt        d| › d�d|||dœ«      ƒ d {  –—†  y y 7 Œ# t        $ r%}t        j	                  d|› d|› �«       Y d }~y d }~ww xY w­w)NÚtenant_z:ticketsÚsystem©r   Úeventr…   ÚdatazFailed to publish z to redis: )r   r   rY   rZ   Úwarning)rJ   rw   r�   r…   ÚfullÚws_errs         rj   r‰   r‰   “  s�   è ø€ ðHÜ˜) YÓ/ˆÙÜ '¨)¨°HÐ =Ø)1¸EÈgÐ_cÑ dóf÷ fñ fð ðfùäò HÜ�‰Ð+¨E¨7°+¸f¸XÐF×GÑGûðHüs6   ‚A$„&3 ª1«3 ¯A$±3 ³	A!¼AÁA$ÁA!Á!A$c              ƒ   ó’  K  — |d   }|d   }|j                  d«      }|j                  dg «      }	 | d   | d   ||dœ}i }|r9t        d„ t        |«      D «       d	«      t        d
„ t        |«      D «       d	«      dœ}d|dœ|d<   t        ||«       ddiS # t        $ r(}t
        j                  d|› �d¬«       ddicY d	}~S d	}~ww xY w­w)z-Store explicit user feedback for this thread.rJ   rm   r)   Úhistoryr2   r3   )r2   r3   rm   r)   c              3   ó8   K  — | ]  }|d    dk(  sŒ|d   –— Œ y­w)rs   rr   r+   N© ©Ú.0Úms     rj   ú	<genexpr>z*execute_submit_feedback.<locals>.<genexpr>©  s#   è ø€ Ò(h¸!ÐTUÐV\ÑT]ÐagÓTg¨¨9­Ñ(hùó   ‚�
Nc              3   ó8   K  — | ]  }|d    dk(  sŒ|d   –— Œ y­w)rs   Ú	assistantr+   Nr¥   r¦   s     rj   r©   z*execute_submit_feedback.<locals>.<genexpr>ª  s#   è ø€ Ò)n¸1ÐUVÐW]ÑU^ÐbmÓUm¨!¨I­,Ñ)nùrª   )Úlast_user_queryÚlast_ai_responseT)Úis_explicit_feedbackÚrated_conversationrQ   rz   ÚsavedzTool submit_feedback failed: rO   r[   )rW   ÚnextÚreversedr	   rY   rZ   r[   )	ra   rb   rJ   rm   r)   r£   Úfeedback_dataÚlast_interactionre   s	            rj   Úexecute_submit_feedbackr¶   �  sï   è ø€ à�KÑ €IØ�KÑ €IØ—‘˜Ó$€IØ�g‰g�i Ó$€Gð#Ø%)¨*Ñ%5ÀÀhÁØ&/¸iñIˆàÐÙä#'Ñ(h¼xÈÓ?PÔ(hÐjnÓ#oÜ$(Ñ)nÄÈÓ@QÔ)nÐptÓ$uñ Ðð >BÐYiÑ$jˆ�jÑ!Ü˜	 =Ô1Ø˜'Ð"Ð"øÜò #Ü�‰Ð4°R°DÐ9ÀDˆÔIØ˜'Ð"Õ"ûð#üs5   ‚.C±A!B ÂCÂ	CÂB?Â9CÂ:CÂ?CÃCc           
   ƒ   ó˜  K  — |d   }|d   }|j                  d«      }	 t        ||d|¬«       	 t        d|› d�dd	d
|› �|| j                  d«      |dœdœ«      ƒ d{  –—†  ddiS 7 Œ# t        $ r"}t        j                  d|› �«       Y d}~Œ,d}~ww xY w# t        $ r(}t        j                  d|› �d¬«       ddicY d}~S d}~ww xY w­w)z@Flag the thread for human intervention and notify the dashboard.rJ   rm   rK   T)rK   rš   z:conversation_listr›   Úintervention_requestedz-AI has requested human assistance for thread r5   )rm   r5   rK   rœ   Nz1Failed to publish intervention request to redis: rz   Úflaggedz$Tool request_human_takeover failed: rO   r[   )rW   r   r   rY   rZ   rŸ   r[   )ra   rb   rJ   rm   rK   r¡   re   s          rj   Úexecute_request_human_takeoverrº   ´  sù   è ø€ à�KÑ €IØ�KÑ €IØ�g‰g�iÓ €Gð#Ü 	¨9°dÀGÕLð	YÜ '¨)¨Ð4FÐ GØ Ð+CØJÈ9È+ÐVØ&/¸4¿8¹8ÀHÓ;MÐZaÑbñJó ÷ ð ð ˜)Ð$Ð$ðùô
 ò 	YÜ�N‰NÐNÈvÈhÐW×XÑXûð	Yûô ò #Ü�‰Ð;¸B¸4Ð@È4ˆÔPØ˜'Ð"Õ"ûð#üso   ‚C
ŸB ¯.A( ÁA&ÁA( Á"B Á%C
Á&A( Á(	BÁ1BÂ	B ÂBÂB Â	CÂCÂ<CÂ=C
ÃCÃC
c              ƒ   óp   K  — | j                  d«      }t        |t        «      rt        |«      nd}d|dœS ­w)zJResources are collected by the chat loop; this only acknowledges the call.r:   r   Úattached)rz   rH   )rW   r\   Úlistr`   )ra   rb   r:   rH   s       rj   Úexecute_attach_resourcesr¾   É  s3   è ø€ à—‘˜Ó%€IÜ(¨´DÔ9ŒC�	ŒN¸q€EØ ¨5Ñ1Ð1ùs   ‚46c                 ó   — d| › d|› �S )Nzchat_emails:ú:r¥   )rJ   rm   s     rj   Ú_email_count_keyrÁ   Ð  s   € Ø˜)˜ A i [Ð1Ð1ó    Úreturnc              ƒ   ó
  K  — 	 t        «       }|j                  t        | |«      «      ƒ d{  –—† }t        |xs d«      S 7 Œ# t        $ r5}t
        j                  d|› d|› �«       t        j                  cY d}~S d}~ww xY w­w)a  How many emails this conversation has already sent.

    Only consulted when a cap is configured. Fails CLOSED on a Redis outage: if
    we cannot prove the conversation is under the limit, we refuse rather than
    risk turning the chat into a mail relay.
    Nr   zCould not read email count for ú: )	r   rW   rÁ   ÚintrY   rZ   r[   r   ÚEMAIL_MAX_PER_THREAD)rJ   rm   r   Úvaluere   s        rj   Ú_emails_sent_in_threadrÉ   Ô  su   è ø€ ð-Ü!Ó#ˆØ—j‘jÔ!1°)¸YÓ!GÓH×HˆÜ�5’:˜A‹Ðð Iùäò -Ü�‰Ð6°y°kÀÀBÀ4ÐHÔIÜ×,Ñ,Õ,ûð-üsC   ‚B„(A ¬A ­A ¿BÁ A Á	B Á*A;Á5B Á6BÁ;B Â Bc              ƒ   ó  K  — t        | |«      }	 t        «       }|j                  |«      ƒ d {  –—† }|dk(  r|j                  |d«      ƒ d {  –—†  y y 7 Œ%7 Œ# t        $ r%}t
        j                  d|› d|› �«       Y d }~y d }~ww xY w­w)Né   i€:	 z Could not record sent email for rÅ   )rÁ   r   ÚincrÚexpirerY   rZ   r[   )rJ   rm   Úkeyr   rH   re   s         rj   Ú_record_email_sentrÏ   ä  s‰   è ø€ Ü
˜9 iÓ
0€CðKÜ!Ó#ˆØ—k‘k #Ó&×&ˆØ�AŠ:Ø—-‘- Ð%5Ó6×6Ñ6ð ð 'øà6ùÜò KÜ�‰Ð7¸	°{À"ÀRÀDÐI×JÑJûðKüsP   ‚B	�A ®A¯A ÁAÁA ÁB	ÁA ÁA Á	BÁ!BÁ<B	ÂBÂB	c              ƒ   ó*  ‡‡‡‡K  — | j                  d«      xs dj                  «       Š| j                  d«      xs dŠ| j                  d«      xs dŠt        ‰d   «      j                  dd«      s t        j	                  d‰d   › �«       d	d
dœS t        «       sd	ddœS t        ‰«      s	d	d‰› d�dœS ‰j                  «       sd	ddœS t        ˆfd„t        D «       «      r,t        j                  d‰j                  d«      › �«       d	ddœS t        j                  }|dkD  rt        ‰d   ‰d   «      ƒ d{  –—† nd}|dkD  rB||k\  r=t        j	                  dt        j                  › d‰j                  d«      › �«       d	ddœS t        j                  j                  ˆˆˆˆfd„«      ƒ d{  –—† }|j                  d«      dk(  rDt!        ‰d   ‰d   «      ƒ d{  –—†  t        j                  d‰› d‰j                  d«      › �«       |S 7 ŒÔ7 Œ`7 Œ4­w)z¹Email knowledge-base content to an address the user gave in this conversation.

    Rate-limited per conversation so a misbehaving model cannot turn the chat
    into a mail relay.
    r6   rF   r7   r8   rJ   ÚenabledTz%send_email called while disabled for r[   zOEmailing is turned off for this business. Tell the user you cannot send emails.©rz   ÚdetailzBEmail is not configured. Tell the user you cannot email right now.ú'z;' is not a valid email address. Ask the user to confirm it.z Refusing to send an empty email.c              3   óB   •K  — | ]  }|‰j                  «       v –— Œ y ­w)N)Úlower)r§   Úmarkerr8   s     €rj   r©   z%execute_send_email.<locals>.<genexpr>	  s   øè ø€ Ò
@ fˆ6�T—Z‘Z“\Ô!Ñ
@ùs   ƒz2Refusing to email a no-answer response for thread rm   uÁ   There is nothing useful to email â€” the knowledge base did not answer this. Tell the user you could not find the information, and offer to raise a ticket or connect them with the team instead.r   NzEmail limit (z) reached for thread zfEmail limit reached for this conversation. Tell the user you cannot send any more emails in this chat.c                  óH   •— t        ‰‰‰ ‰j                  d«      ‰d   ¬«      S )Nr)   rJ   )Ú
first_namerJ   )r   rW   )r8   rb   r7   r6   s   €€€€rj   ú<lambda>z$execute_send_email.<locals>.<lambda>  s'   ø€ ´J¸xÈÐRVØ&)§g¡g¨kÓ&:ÀcÈ+ÑFVô5X€ rÂ   rz   ÚsentzChat emailed ú for thread )rW   rX   r   rZ   rŸ   r   r   ÚanyÚREFUSAL_MARKERSÚinfor   rÇ   rÉ   ÚanyioÚ	to_threadÚrun_syncrÏ   )ra   rb   rN   Úalready_sentÚresultr8   r7   r6   s    `   @@@rj   Úexecute_send_emailrå   ï  s=  ûè ø€ ð —‘˜Ó$Ò*¨×1Ñ1Ó3€HØ�h‰h�yÓ!Ò' R€GØ�8‰8�FÓÒ!˜r€Dä˜c +Ñ.Ó/×3Ñ3°I¸tÔDÜ�‰Ð>¸sÀ;Ñ?OÐ>PÐQÔRØ!ð)ñ*ð 	*ô ÔØ!Ø^ñ`ð 	`ä˜(Ô#Ø!Ø˜h˜ZÐ'bÐcñeð 	eà�:‰:Œ<Ø!Ð-OÑPÐPô Ó
@´Ô
@Ô@Ü�‰ÐHÈÏÉÐQ\ÓI]ÐH^Ð_Ô`Ø!ðRñSð 	Sô ×)Ñ)€EØW\Ð_`ÒW`Ô/°°KÑ0@À#ÀkÑBRÓS×SÐSÐfg€LØˆq‚y�\ UÒ*Ü�‰ØœH×9Ñ9Ð:Ð:OÐPS×PWÑPWÐXcÓPdÐOeÐfô	
ð "ð?ñ@ð 	@ô —?‘?×+Ñ+ö -Xó Y÷ Y€Fà‡z�z�(Ó˜vÒ%Ü   [Ñ!1°3°{Ñ3CÓD×DÐDÜ�‰�m H :¨\¸#¿'¹'À+Ó:NÐ9OÐPÔQØ€Mð TøðYøð 	Eús7   †D2HÄ8HÄ9A5HÆ.HÆ/-HÇHÇ1HÈHÈHc              ƒ   ót  ‡‡K  — | j                  d«      xs dj                  «       Š‰sdddœS t        j                  j	                  ˆˆfd„«      ƒ d{  –—† }|s't
        j                  d‰›d	‰d
   › �«       dd‰› d�dœS t
        j                  d|d   › d‰j                  d«      › �«       ddi|¥S 7 Œ`­w)zFTurn a file name into a temporary download link the visitor can click.rB   rF   r[   zNo file name given.rÒ   c                  ó"   •— t        ‰ d   ‰«      S )NrJ   r   )rb   rB   s   €€rj   rÚ   z'execute_send_document.<locals>.<lambda>,  s   ø€ ”˜c +Ñ.°	Ó:€ rÂ   Nz'send_document: no stored file matching ú for rJ   Ú	not_foundzNo uploaded document named z-. Tell the visitor the file is not available.zsend_document: rÜ   rm   rz   rÛ   )rW   rX   rà   rá   râ   rZ   rß   )ra   rb   ÚlinkrB   s    ` @rj   Úexecute_send_documentrë   %  sÝ   ùè ø€ à—‘˜+Ó&Ò,¨"×3Ñ3Ó5€IÙØ!Ð-BÑCÐCä—‘×)Ñ)Ü:ó<÷ <€DáÜ�‰Ð=¸i¸]ð KØ˜{Ñ+Ð,ð.ô 	/à%Ø7¸	°{ð CHð HñIð 	Iô ‡K�K�/ $ {Ñ"3Ð!4°LÀÇÁÈÓAUÐ@VÐWÔXØ�fÐ% Ð%Ð%ð<ús   „AB8ÁB6ÁA!B8c              ƒ   ó¤  ‡‡K  — | j                  d«      xs dj                  «       Št        j                  j	                  ˆˆfd„«      ƒ d{  –—† }|s$t
        j                  d‰›d‰d   › �«       dg d	d
œS |D �cg c]  }t        |«      ‘Œ }}t
        j                  dt        |«      › d‰j                  d«      › �«       d|dœS 7 Œzc c}w ­w)zCReturn products from the tenant's own catalogue. Never invents one.r   rF   c                  ó&   •— t        ‰ d   ‰d¬«      S )NrJ   rS   rM   )r   )rb   r   s   €€rj   rÚ   z,execute_recommend_products.<locals>.<lambda>>  s   ø€ ”  KÑ 0°%¸qÔA€ rÂ   Nz$recommend_products: nothing matched rè   rJ   ÚnonezWThis business has no matching products. Tell the visitor plainly and do not invent one.)rz   ÚproductsrÓ   zrecommend_products: rÜ   rm   Úfound)rz   rï   )	rW   rX   rà   rá   râ   rZ   rß   r   r`   )ra   rb   ÚrowsÚrrï   r   s    `   @rj   Úexecute_recommend_productsró   8  sÚ   ùè ø€ à�X‰X�gÓÒ$ "×+Ñ+Ó-€Eô —‘×)Ñ)ÜAóC÷ C€DáÜ�‰Ð:¸5¸)ð DØ˜{Ñ+Ð,ð.ô 	/à ¨bð;ñ<ð 	<ð %)Ö)˜q”˜•
Ð)€HÐ)Ü
‡K�KÐ&¤s¨8£} o°\À#Ç'Á'È+ÓBVÐAWÐXÔYØ¨8Ñ4Ð4ðCúò *ùs$   „A
CÁC	Á.CÁ=CÂ;CÃC)r   r$   r1   r4   r   r9   rA   rC   c              ƒ   ó˜   K  — t         j                  | «      }|€t        j                  d| › �«       dddœS  |||«      ƒ d{  –—† S 7 Œ­w)zQRoute a model tool call to its executor. Returns a JSON-serializable result dict.Nz!Unknown tool requested by model: r[   zunknown toolrÒ   )ÚTOOL_EXECUTORSrW   rZ   r[   )r"   ra   rb   Úhandlers       rj   Údispatch_toolr÷   W  sM   è ø€ ä× Ñ  Ó&€GØ€Ü�‰Ð8¸¸Ð?Ô@Ø!¨^Ñ<Ð<Ù˜˜sÓ#×#Ð#Ð#ús   ‚AA
ÁAÁA
);Ú__doc__r‡   ÚloggingrŠ   rà   Úapp.core.configr   Úapp.core.llm_clientr   r   Úapp.core.promptsr   Úapp.services.databaser   r   r   r	   r
   r   Úapp.services.documentsr   Úapp.services.email_servicer   r   r   Úapp.services.tools_settingsr   Úapp.services.embeddingr   Úapp.services.firestore_servicer   Úapp.services.productsr   r   Úapp.services.quotasr   Úapp.services.redis_servicer   r   Ú	getLoggerÚ__name__rZ   rÞ   ÚTOOL_SCHEMASrk   r˜   r‰   r¶   rº   r¾   rÁ   rÆ   rÉ   rÏ   rå   rë   ró   rõ   r÷   r¥   rÂ   rj   ú<module>r	     sV  ðñó Û Û ã å $Ý 7Ý 8÷÷ õ 1ß SÑ SÝ :Ý 1Ý Bß :Ý 2ß Fà	ˆ×	Ñ	˜8Ó	$€ð€ð à+ðMð !àØ (ð(Oñðð %˜Iñ
ñ
ñð6 à-ð$ð, !à*2À=Ñ!QØ(0ÐAWÑXØ(0ÐA]Ñ^Ø)1Ò;TÑ UØ&.Ð?UÑVØ+3ÐDYÑ"ZØ9AÐRoÑ0pñò ?ññ1%
ñ(ðT à%ðhð !à)1ÐB_Ñ `Ø'/Ð@\Ñ]ñð (¨Ð2ññ
ñð4 à,ð?ð !à xÐ@ZÑ[ðð &˜Jññ
ñð2 à ð?ð  !ð !)Ø'\ñ!ð
 !)Ø'iñ ð
 !)ðTññò$ <ñ)ñ%(
ñ+ðZ à&ð-ð" !àØ 'à$,à(.°¸6Ð0BÐ'CØ*0°8¸VÐ2DÐ)EØ*0°(Ð);ñ+ð
 *1¨	ñ"ñ"ðð )˜Mñ!ñ'%
ñ(ðT à#ð	Oð !ð !)ð)Qñ"ð !)Ø'Lññ
ð )˜Mññ
ñ ðD à(ð
@ð !ð !)Ø'\ñð
 !)Ø']ñ!ñ	ð %˜Iññ
ñ ðWL€ò^"òJ96òxHò#ò.#ò*2ò2ð-¸#ó -ò Kò3òl&ò&5ð( ;Ø>Ø.Ø<Ø$Ø0Ø*Ø4ñ	€ó$rÂ   