Ë
    ÿ�j¶ï  ã                   óì  — d Z ddlZddlZddlZddlZddlmZmZmZ ddlm	Z	m
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!m"Z"m#Z# ddl$m%Z% ddl&m'Z' ddl(m)Z*m+Z,m-Z.m/Z/ ddl0m)Z1m+Z2m-Z3m4Z4 ddl5m6Z6m7Z7m8Z8 ddl9m:Z: ddl;m<Z<m=Z=  ej|                  e?«      Z@dZAd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 eBe"«      d)d*œd+œg d,¢dœdœdœdd-d.ddd/dœdd0dœd1œd2d3gdœdœdœdd4d5dd6dd7dœid6gdœdœdœdd8d9ddd:dœdd;dœdd<dœd=œg d=¢dœdœdœdd>d?dd@dAddBddCgidBddCgidBdidDœdEgdœdFœid@gdœdœdœddGdHdddIdœddJdœdKœdLgdœdœdœddMdNdddOdœddPdœdQœdgdœdœdœddRdSdddTdœdUdVdœdUdWdœddXdYgdZd*œdd[dœd\œg d]¢dœdœdœdd^d_ddd`dœdUdadœdUdbdœddXdYgdcd*œddddœddedœddfdœdgœg dh¢dœdœdœddidjdddkdœddldœdmœdngdœdœdœgZCdo„ ZDdp„ ZEdq„ ZFdr„ ZGds„ ZHdt„ ZIdu„ ZJdveKfdw„ZLdx„ ZM eN«       ZOdŒdy„ZPd�dz„ZQd�d{„ZRd�d|„ZSd}„ ZTd~„ ZUd„ ZVd€ZW ed€¬�«      ZXd‚eYfdƒ„ZZd‚eYfd„„Z[d…e\fd†„Z]d‡„ Z^dˆ„ Z_d‰„ Z`eDeEeGeHeTeIeUeVe^e_e`dŠœZad‹„ Zby)Ž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)ÚdatetimeÚ	timedeltaÚtimezone)ÚZoneInfoÚZoneInfoNotFoundError)Ú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ÚTICKET_CATEGORIESÚget_tool_settings)Úget_embeddings)Úset_intervention_status)Úcancel_eventÚcheck_availabilityÚcreate_eventÚget_google_calendar_connection)r   r   r   Úget_teams_connection)Úmatch_productsÚsemantic_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. Not for a question asking which product/service to get, including a goal or problem phrased with no product named ('how can I improve my brand health') -- that is recommend_products, even without shopping words in the sentence.Ú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_ticketuæ  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. Any team notification email is also sent in the background -- tell the user their ticket has been logged/raised, never that the team has already been emailed or has already seen it.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 infozkWhich team should handle this: General enquiries, Sales, or Business. Pick from the conversation's content.)r+   r5   r,   )Ú	user_nameÚheadingÚcontentÚpriorityÚemailÚ
contact_noÚany_other_contact_mediumÚcategory©r7   r8   r9   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Úanswerr@   rA   Ú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. Sending happens in the background after this call returns -- tell the user you are sending it now ('I'm sending that over now'), never that it has already arrived.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ÚlabelrM   )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_namerC   rP   Ú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. This also covers advice-phrased requests for a recommendation, not only literal shopping requests -- 'what shirt should I wear', 'what should I get for a beach trip', 'what do you suggest for sensitive skin' are all still asking which product to get, just phrased as a question for advice; call this tool for those too, not search_knowledge_base. The same applies to a goal or outcome phrased as a problem, with NO product named -- 'how can I improve my brand health', 'how do I get more ROI from marketing', 'how do I grow my audience' are the visitor asking which of this business's products/services solves that, exactly the same as if they had said 'what do you have for X' -- call this tool, not search_knowledge_base, even though the sentence itself contains no shopping words. 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)   r=   r   a’  Check whether one specific time the user asked for is open, e.g. '9 AM tomorrow'. Use whenever the user asks to book a call/meeting/demo -- never invent or claim a time is open without calling this first. If that exact time is free, this confirms it -- go ahead and book it once you also have the visitor's email and name. If it is NOT free, this returns a small number of real nearby alternatives instead -- relay those to the user; do not invent alternatives of your own, and do not read out more than the tool gives you. This tool's result always includes a 'timezone' field -- ALWAYS state that zone explicitly when you relay any time from this result (e.g. '10:30 AM IST', not just '10:30 AM'), so the user is never left guessing which timezone a time is in. Never assume it matches the timezone the user asked in -- use the value this tool actually returned. Give the date and time exactly as a person would say them (a 1-12 hour, a minute, and AM/PM) -- do NOT convert to 24-hour time yourself; this tool does that conversion for you so you never have to. BEFORE your FIRST check_availability call in a booking conversation: if the user has not already told you what timezone or city they're in, ASK them directly (e.g. 'What timezone are you in, so I book the right time for you?') -- do not silently assume they mean the business's own timezone, and do not guess. Only proceed without asking if they say they don't know or don't care (in which case leave visitor_timezone out and it defaults to the business's own zone). Once you know the user's timezone (asked or stated), pass it as visitor_timezone (an IANA zone name, e.g. 'Asia/Kolkata', 'Europe/London', 'America/Los_Angeles') on EVERY check_availability and book_meeting call for the rest of THIS conversation -- never drop it on a later call, never switch between passing it and not passing it for what is meant to be the same request. Two calls for the same nominal time (e.g. both called '9 AM') must resolve to the exact same instant, or the user sees contradictory answers. If this returns no alternatives or an error, tell the user you could not find an open time and offer a ticket/human contact instead of inventing one.z†ISO 8601 date (YYYY-MM-DD) the user wants, resolved from their natural-language request yourself (e.g. 'tomorrow' -> tomorrow's date).Úintegerz‹The hour the user asked for, on a 1-12 clock, exactly as you would say it out loud (e.g. 9 for '9 AM'). Never convert this to 24-hour time.z“The minute the user asked for (0-59). Use 0 if they only gave an hour, e.g. 'tomorrow morning' -> a reasonable hour of your choosing with minute 0.ÚAMÚPMzMWhether the requested time is AM or PM, exactly as you would say it out loud.zúIANA timezone name (e.g. 'Asia/Kolkata') for the user's own timezone, established by asking them if they haven't already said. Once set for this conversation, pass the SAME value on every later call -- omit only if the user said they don't know/care.)ÚdateÚhour_12ÚminuteÚmeridiemÚvisitor_timezone)rU   rV   rW   rX   Úbook_meetinga
  Book a meeting at a specific time. Only call this after check_availability has shown this exact time as open and the user has picked it -- never call this on a time you have not already confirmed is free. This books immediately; there is no separate confirmation step after this call succeeds, so only call it once the user has clearly picked a time, not while they are still considering options. You must have the visitor's email and name before calling -- ask for whichever is missing first. Give the date and time exactly as a person would say them (a 1-12 hour, a minute, and AM/PM) -- do NOT convert to 24-hour time yourself; this tool does that conversion for you so you never have to. Pass the EXACT SAME visitor_timezone value you used on the check_availability call that confirmed this slot (asked or stated earlier in this conversation) -- do NOT convert the hour yourself, and do NOT drop or change the timezone between check_availability and book_meeting for what is meant to be the same slot. Leave it out only if you never established one for this conversation. On success this returns 'confirmed_start' and 'timezone' -- when you tell the user their meeting is booked, state the time WITH that exact timezone (e.g. 'booked for 10:30 AM IST'), not a bare time -- never assume it matches whatever zone the user originally asked in. If this returns an error because the slot was taken in the meantime (someone else booked it first), tell the user and offer to check availability again -- never silently pick a different time yourself.zvISO 8601 date (YYYY-MM-DD) of the slot the user picked, matching the date on that slot as shown by check_availability.z†The hour of the slot on a 1-12 clock, exactly as you would say it out loud (e.g. 2 for '2:30 PM'). Never convert this to 24-hour time.z5The minute of the slot (0-59), e.g. 30 for '2:30 PM'.zCWhether the slot is AM or PM, exactly as you would say it out loud.z½IANA timezone name (e.g. 'Asia/Kolkata') ONLY if the user explicitly named a timezone or city for this slot -- the same one passed to check_availability. Omit this field entirely otherwise.z-The visitor's email address, as they gave it.z$The visitor's name, as they gave it.)rU   rV   rW   rX   rY   Úvisitor_emailÚvisitor_name)rU   rV   rW   rX   r[   r\   Úcancel_meetingaÒ  Cancel a previously booked meeting. Ask for the visitor's email, and if they don't recall the exact time, roughly when it was (e.g. 'around 2pm ' or 'sometime tomorrow') -- the lookup searches by these, not by an exact ID, so the more specific they are the more reliably it finds the right one. If this reports it could not find exactly one matching meeting, tell the user and ask for more detail (a narrower time) rather than trying again with the same information.z/The email address the meeting was booked under.z]ISO 8601 datetime near when the meeting was, if the user recalls it. Omit if they don't know.)r[   Úapproximate_timer[   c              ƒ   ó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)rd   re   é   ©Úlimitz#Tool search_knowledge_base failed: T©Úexc_infozSearch failed.NÚmetadataÚ
top_imagesé   rK   r8   ÚsourceÚ
source_urlzInternal Knowledge Base)r8   rn   Ú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Úexra   ÚhitÚmetarp   Ú
image_urlsÚimgs               ú6/var/www/html/strategist-ai/app/services/chat/tools.pyÚexecute_search_knowledge_baser…     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              ƒ   óN  K  — |d   }|d   }|j                  d«      }|j                  d«      }	 t        ||«      }|�rš	 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   | j                  d«      xs |j                  d«      xs ddœ}t)        ||d   |«      }|rHt+        ||d   dd|d   › d�«      ƒ d{  –—†  	 t-        ||d   «      }|rt/        ||d¬«       d!|d   d"œS d#|d   d"œS t1        | «      }d$t3        j4                  «       j6                  dd% j9                  «       › �|d<   | j                  d«      xs d|d<   |r||d<   |r||d<   t;        |||¬&«      }|r4t+        ||d   d'd(|d   › �«      ƒ d{  –—†  	 t/        ||«       d*|d   d"œS d#dd"œS # t"        $ ra}t$        j'                  d|› �«       | d   |d   › d| d   › �| d   | j                  d«      xs |j                  d«      xs ddœ}Y d}~�Œvd}~ww xY w7 �ŒQ# t"        $ r+}t$        j'                  d|d   › d|› �d¬ «       Y d}~�Œ^d}~ww xY w7 ŒÅ# t"        $ r*}t$        j'                  d)|d   › d|› �d¬ «       Y d}~Œäd}~ww xY w# t<        $ 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.rd   Ú	thread_idr6   re   r7   r8   )Úold_headingÚold_contentÚnew_headingÚnew_contentÚuser)Úroler8   i   )ÚmodelÚmessagesÚmax_completion_tokensr   z```jsonr`   z```z

---
r9   r=   úGeneral enquiriesr>   zConsolidation LLM call failed: z

---
[Update]:
NÚ	ticket_idÚticket_updatedzTicket z& has been consolidated with new issuesT©Úupdatedz&Ticket update notification failed for ú: ri   r•   )Ústatusr’   ru   zTICK-é   )r‡   Únew_ticket_createdzA new ticket has been created: zTicket notification failed for Úcreatedz%Tool create_or_update_ticket failed: )rq   r   r
   ÚformatÚopenai_clientÚchatÚcompletionsÚcreater   Ú	LLM_MODELÚchoicesÚmessager8   rr   ÚreplaceÚjsonÚloadsrs   rt   ru   r   Ú_publish_ticket_eventr   Ú_notify_ticketrw   ÚuuidÚuuid4ÚhexÚupperr   r#   )r{   r|   rd   r‡   r6   re   ÚexistingÚcons_promptÚcons_responseÚrawÚconsÚupdate_dataÚcons_exÚsuccessÚfull_ticketr   Úticket_dataÚnew_ids                     r„   Úexecute_create_or_update_ticketr·   9  s,  è ø€ à�KÑ €IØ�KÑ €IØ—‘˜Ó$€IØ�g‰g�iÓ €GðG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Ø $§¡¨Ó 4Ò g¸¿¹ÀZÓ8PÒ gÐTgñ	�ô $ I¨x¸Ñ/DÀkÓRˆGÙÜ+¨I°xÀÑ7LÐN^Ø.5°h¸{Ñ6KÐ5LÐLrÐ,sóu÷ uð uð/Ü",¨Y¸ÀÑ8MÓ"N�KÙ"Ü& y°+ÀtÕLð #,¸(À;Ñ:OÑPÐPØ%°H¸[Ñ4IÑJÐJä˜4“jˆØ%*¬4¯:©:«<×+;Ñ+;¸B¸QÐ+?×+EÑ+EÓ+GÐ*HÐ#Iˆ�KÑ Ø"&§(¡(¨:Ó"6Ò"MÐ:Mˆ�JÑÙØ'0ˆK˜Ñ$ÙØ%,ˆK˜	Ñ"Ü˜y¨+ÀÔKˆÙÜ'¨	°;¸{Ñ3KÐMaØ*IÈ+ÐVaÑJbÐIcÐ(dóf÷ fð fðpÜ˜y¨+Ô6ð (°kÀ+Ñ6NÑOÐOØ!°Ñ5Ð5øôY ò Ü—‘Ð>¸w¸iÐHÔIà# I™Ø"*¨9Ñ"5Ð!6Ð6JÈ4ÐPYÉ?ÐJ[Ð\Ø $ ZÑ 0Ø $§¡¨Ó 4Ò g¸¿¹ÀZÓ8PÒ gÐTgñ	–ûðúðuúô !ò /Ü—L‘LÐ#IÈ(ÐS^ÑJ_ÐI`Ð`bÐceÐbfÐ!gØ)-ð !÷ /ò /ûð/úðfùô ò pÜ—‘Ð>¸{È;Ñ?WÐ>XÐXZÐ[]ÐZ^Ð_Ðjn�×oÑoûðpûô ò ØÜò 6Ü�‰Ð<¸R¸DÐAÈDˆÔQØ!°Ñ5Õ5ûð6üsñ   ‚-N%°M' Á D7J Å7-M' Æ$K5Æ%M' Æ*K8 Ç	M' ÇN%ÇM' ÇN%ÇBM' É)L/É*M' É/L1 É;M' ÊN%ÊM' ÊN%Ê	K2ÊAK-Ë'M' Ë-K2Ë2M' Ë8	L,Ì L'Ì!M' Ì'L,Ì,M' Ì1	M$Ì: MÍM' ÍM$Í$M' Í'N"Í9NÎN"ÎN%ÎN"Î"N%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)a  Notify the dashboard of a ticket change.

    publish_event() itself is fire-and-forget (see its docstring) -- a slow
    or stale Redis connection can no longer make this await hang, so this
    stays a plain await rather than wrapping it in a second background task.
    Útenant_z:ticketsÚsystem©r+   Úeventr¢   ÚdataNzFailed to publish z to redis: )r   r%   rs   rt   Úwarning)rd   r’   r¼   r¢   ÚfullÚws_errs         r„   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.rd   r‡   r6   Úhistoryr@   rA   )r@   rA   r‡   r6   c              3   ó8   K  — | ]  }|d    dk(  sŒ|d   –— Œ y­w)r�   rŒ   r8   N© ©Ú.0Úms     r„   ú	<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)r�   Ú	assistantr8   NrÄ   rÅ   s     r„   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_conversationrk   r—   ÚsavedzTool submit_feedback failed: ri   ru   )rq   ÚnextÚreversedr   rs   rt   ru   )	r{   r|   rd   r‡   r6   rÂ   Úfeedback_dataÚlast_interactionr   s	            r„   Ú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.rd   r‡   re   T)re   r¹   z:conversation_listrº   Úintervention_requestedz-AI has requested human assistance for thread rC   )r‡   rC   re   r»   Nz1Failed to publish intervention request to redis: r—   Úflaggedz$Tool request_human_takeover failed: ri   ru   )rq   r   r%   rs   rt   r¾   ru   )r{   r|   rd   r‡   re   rÀ   r   s          r„   Ú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.rH   r   Úattached)r—   rb   )rq   rv   Úlistrz   )r{   r|   rH   rb   s       r„   Úexecute_attach_resourcesrÝ   Å  s3   è ø€ à—‘˜Ó%€IÜ(¨´DÔ9ŒC�	ŒN¸q€EØ ¨5Ñ1Ð1ùs   ‚46c                 ó   — d| › d|› �S )Nzchat_emails:ú:rÄ   )rd   r‡   s     r„   Ú_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–   )	r$   rq   rà   Úintrs   rt   ru   r   ÚEMAIL_MAX_PER_THREAD)rd   r‡   r	   Úvaluer   s        r„   Ú_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Úexpirers   rt   ru   )rd   r‡   Úkeyr	   rb   r   s         r„   Ú_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                 óÂ   ‡ ‡‡‡‡‡— ˆˆˆˆˆˆ fd„}t        j                   |«       «      }t        j                  |«       |j	                  t        j
                  «       |S )a  Send in the background; the caller never waits on this.

    on_sent, if given, is an async callable run only after a confirmed
    successful send -- used to keep execute_send_email's per-thread rate
    counter accurate without blocking the tool-call round trip on it.
    c               “   ó|  •K  — 	 t         j                  j                  ˆˆˆˆˆfd„«      ƒ d {  –—† } | j                  d«      dk(  r‰r; ‰«       ƒ d {  –—†  y t        j                  d‰› d| j                  d«      › �«       y y 7 ŒV7 Œ3# t        $ r'}t        j                  d‰› d|› �d¬	«       Y d }~y d }~ww xY w­w)
Nc                  ó$   •— t        ‰‰‰ ‰‰¬«      S )N)Ú
first_namerd   )r   )rF   rñ   rE   rd   rD   s   €€€€€r„   ú<lambda>z7_fire_and_forget_email.<locals>._send.<locals>.<lambda>ü  s   ø€ œ
 8¨W°dØ.8ÀIôO€ rá   r—   ÚsentzBackground email to z	 failed: Údetailz	 raised: Tri   )ÚanyioÚ	to_threadÚrun_syncrq   rt   ru   rs   )Úresultr   rF   rñ   Úon_sentrE   rd   rD   s     €€€€€€r„   Ú_sendz%_fire_and_forget_email.<locals>._sendù  s¸   øè ø€ ð
	XÜ Ÿ?™?×3Ñ3÷OóP÷ PˆFð �z‰z˜(Ó# vÒ-ÙÙ!›)—O‘Oä—‘Ð3°H°:¸YÀvÇzÁzÐRZÓG[ÐF\Ð]Õ^ð ð	Pøð
 $ùô ò 	XÜ�L‰LÐ/°¨z¸À2À$ÐGÐRVˆL×WÑWûð	Xüs\   ƒB<…)B	 ®B¯$B	 ÁBÁB	 ÁB<Á*B	 ÂB<ÂB	 ÂB	 Â		B9ÂB4Â/B<Â4B9Â9B<)ÚasyncioÚcreate_taskÚ_background_email_tasksÚaddÚadd_done_callbackÚdiscard)rD   rE   rF   rd   rñ   rù   rú   Útasks   ``````  r„   Ú_fire_and_forget_emailr  ò  sK   ý€ ÷Xñ Xô ×Ñ™u›wÓ'€DÜ×Ñ Ô%Ø×ÑÔ2×:Ñ:Ô;Ø€Krá   c                 ó  — | d   }|rdnd}|› d| j                  d«      › d| d   › �}d|› �d	| j                  d«      › �d
| j                  d«      › �d| j                  d«      xs d› �g}| j                  d«      r|j                  d| d   › �«       | j                  d«      r|j                  d| d   › �«       |j                  d«       |j                  | j                  d«      xs d«       |dj                  |«      fS )Nr’   ÚUpdatedÚNewÚ r=   z	 ticket: r7   zTicket ID: z
Category: z
Priority: r9   zFrom: r6   ÚUnknownr:   zEmail: r;   zPhone: r`   r8   Ú
)rq   rx   Újoin)rµ   r•   r’   ÚverbrE   Úliness         r„   Ú_ticket_team_email_contentr    s  € Ø˜KÑ(€IÙ‰9 U€DØ��a˜Ÿ™¨
Ó3Ð4°I¸kÈ)Ñ>TÐ=UÐV€Gà
�i�[Ð!Ø
�[—_‘_ ZÓ0Ð1Ð2Ø
�[—_‘_ ZÓ0Ð1Ð2Ø
�—‘ Ó-Ò:°Ð;Ð<ð	€Eð ‡��wÔØ�‰�w˜{¨7Ñ3Ð4Ð5Ô6Ø‡��|Ô$Ø�‰�w˜{¨<Ñ8Ð9Ð:Ô;Ø	‡L�L�ÔØ	‡L�L�—‘ Ó+Ò1¨rÔ2Ø�D—I‘I˜eÓ$Ð$Ð$rá   c                 ód   — | d   }|rd|› �}d|› d| d   › d�}||fS d|› �}d|› d| d   › d�}||fS )	Nr’   u"   Your request has been updated â€” zEWe've added your latest message to your existing ticket.

Ticket ID: z

Summary: r7   z+

Our team will follow up with you shortly.u    We've received your request â€” zBThanks for reaching out. Your ticket has been logged.

Ticket ID: rÄ   )rµ   r•   r’   rE   rF   s        r„   Ú_ticket_customer_email_contentr    s“   € Ø˜KÑ(€IÙØ6°y°kÐBˆðØ#˜ð %Ø# IÑ.Ð/ð 08ð9ð 	ð �Dˆ=Ðð 5°Y°KÐ@ˆðØ#˜ð %Ø# IÑ.Ð/ð 08ð9ð 	ð �Dˆ=Ðrá   c           	      óp  — t        | «      j                  dd«      syt        | «      sy|j                  d«      xs d}t        | «      j                  dg «      D �cg c]H  }t	        |t
        «      r6|j                  d«      |k(  r"|j                  d«      r|j                  d«      ‘ŒJ }}|r%t        ||¬«      \  }}|D ]  }t        |||| «       Œ |j                  d«      xs d	j                  «       }	|	r<t        |	«      r0t        ||¬«      \  }}t        |	||| |j                  d
«      ¬«       yyyc c}w )a˜  Email the matching business team, and confirm receipt to the customer.

    Called on both ticket creation and ticket update -- see the module's
    calling code. `updated` only changes the email wording (new vs. updated);
    the routing logic is identical either way. A category with no configured
    business email sends nothing to the team; that is intentional, not a bug
    (see the design spec).
    ÚenabledTNr=   r‘   Úticket_emailsr:   r”   r`   r6   )rñ   )r   rq   r   r   rv   rw   r  r  rr   r   r  )
rd   rµ   r•   r=   ÚentryÚteam_emailsrE   rF   ÚaddressÚcustomer_emails
             r„   r§   r§   4  s9  € ô ˜iÓ(×,Ñ,¨Y¸Ô=ØÜ˜IÔ&Øà�‰˜zÓ*ÒAÐ.A€Hô 4EÀYÓ3O×3SÑ3SÐTcÐegÓ3hö l¨%Ü  ¬Ô-°%·)±)¸JÓ2GÈ8Ò2SÐX]×XaÑXaÐbiÔXjð —9‘9˜WÕ%ð l€Kð lñ
 Ü2°;ÈÔP‰ˆ�Ø"ò 	FˆGÜ" 7¨G°T¸9ÕEð	Fð "—o‘o gÓ.Ò4°"×;Ñ;Ó=€NÙœ.¨Ô8Ü6°{ÈGÔT‰ˆ�Ü˜~¨w¸¸iØ*5¯/©/¸+Ó*Fö	Hð 9€~ùòls   ÁAD3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        |d   «      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 |d   }|d   Šˆˆ	fd„}t        |‰«      ƒ d{  –—†  t        ‰	|‰||j                  d«      |¬«       d‰	dœS 7 Œ–7 Œ+­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.
    rD   r`   rE   rF   rd   r  Tz%send_email called while disabled for ru   zOEmailing is turned off for this business. Tell the user you cannot send emails.©r—   rô   zBEmail 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Æ   ÚmarkerrF   s     €r„   rÈ   z%execute_send_email.<locals>.<genexpr>p  s   øè ø€ Ò
@ fˆ6�T—Z‘Z“\Ô!Ñ
@ùs   ƒz2Refusing to email a no-answer response for thread r‡   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               “   óD   •K  — t         j                  d‰› d‰ › �«       y ­w)NzChat emailed ú for thread )rt   Úinfo)r‡   rD   s   €€r„   Ú_on_sentz$execute_send_email.<locals>._on_sent‡  s    øè ø€ Ü�‰�m H :¨\¸)¸ÐEÕFùs   ƒ r6   )rñ   rù   Úqueued)r—   Úto)rq   rr   r   rt   r¾   r   r   ÚanyÚREFUSAL_MARKERSr  r   rå   rç   rí   r  )
r{   r|   rE   rh   Úalready_sentrd   r   rF   r‡   rD   s
          @@@r„   Úexecute_send_emailr&  V  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Ø!ð)ñ*ð 	*ô ˜C Ñ,Ô-Ø!Ø^ñ`ð 	`ä˜(Ô#Ø!Ø˜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ô	
ð "ð?ñ@ð 	@ð �KÑ €IØ�KÑ €IõGô ˜Y¨	Ó
2×2Ð2Ü˜8 W¨d°IØ&)§g¡g¨kÓ&:ÀHõNà hÑ/Ð/ð+ Tøð$ 3ús%   …D6GÄ;GÄ<A,GÆ(GÆ)*GÇG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.rP   r`   ru   zNo file name given.r  c                  ó"   •— t        ‰ d   ‰«      S ©Nrd   r   )r|   rP   s   €€r„   rò   z'execute_send_document.<locals>.<lambda>š  s   ø€ ”˜c +Ñ.°	Ó:€ rá   Nz'send_document: no stored file matching ú for rd   Ú	not_foundzNo uploaded document named z-. Tell the visitor the file is not available.zsend_document: r  r‡   r—   ró   )rq   rr   rõ   rö   r÷   rt   r  )r{   r|   ÚlinkrP   s    ` @r„   Ú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                  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 Œ§7 Œ|c c}w ­w)zCReturn products from the tenant's own catalogue. Never invents one.r)   r`   c                  ó&   •— t        ‰ d   ‰d¬«      S ©Nrd   rm   rg   )r!   ©r|   r)   s   €€r„   rò   z,execute_recommend_products.<locals>.<lambda>´  s   ø€ Ô! # kÑ"2°EÀÔC€ rá   Nc                  ó&   •— t        ‰ d   ‰d¬«      S r0  )r    r1  s   €€r„   rò   z,execute_recommend_products.<locals>.<lambda>·  s   ø€ ”N 3 {Ñ#3°UÀ!ÔD€ rá   z$recommend_products: nothing matched r*  rd   ÚnonezWThis business has no matching products. Tell the visitor plainly and do not invent one.)r—   Úproductsrô   zrecommend_products: r  r‡   Úfound)r—   r4  )	rq   rr   rõ   rö   r÷   rt   r  r"   rz   )r{   r|   ÚrowsÚrr4  r)   s    `   @r„   Úexecute_recommend_productsr8  ¦  s  ùè ø€ à�X‰X�gÓÒ$ "×+Ñ+Ó-€Eô —‘×)Ñ)ÜCóE÷ E€DáÜ—_‘_×-Ñ-ÜDóF÷ FˆáÜ�‰Ð:¸5¸)ð DØ˜{Ñ+Ð,ð.ô 	/à ¨bð;ñ<ð 	<ð %)Ö)˜q”˜•
Ð)€HÐ)Ü
‡K�KÐ&¤s¨8£} o°\À#Ç'Á'È+ÓBVÐAWÐXÔYØ¨8Ñ4Ð4ðEøðFúò *ùs6   „A
C?ÁC6Á,C?Á;C8Á<.C?Â*C:Â<;C?Ã8C?Ã:C?é   )Úhoursr{   c                 óž   — | j                  d«      xs dj                  «       }|r	 t        |«      S |S # t        t        t
        f$ r Y |S w xY w)a�  The zone a requested time should be interpreted in: the visitor's
    explicitly-named zone if the model passed one and it's a real IANA name,
    otherwise the business's own connected-calendar zone.

    Callers must relay this SAME zone back in their response to the model
    (as the tool result's own "timezone" field) rather than always reporting
    the business's zone -- live testing found the tool result silently
    labelling a visitor-timezone request with the business's own zone name,
    which the model then relayed as if it matched the times it just gave,
    when the times were actually in a different zone entirely.
    rY   r`   )rq   rr   r   r   Ú
ValueErrorÚKeyError)r{   Úbusiness_tzÚvisitor_tz_names      r„   Ú_resolve_effective_timezoner@  Î  s]   € ð —x‘xÐ 2Ó3Ò9°r×@Ñ@ÓB€OÙð	Ü˜OÓ,Ð,ð Ðøô &¤z´8Ð<ò 	ØØÐð	ús   §
4 ´AÁAc                 ó:  — t        | |«      }	 t        j                  | j                  d«      xs d«      j	                  «       }t        | j                  d«      «      }t        | j                  d«      «      }| j                  d«      xs dj                  «       j                  «       }d|cxk  rdk  rn yd|cxk  rd	k  rn y|d
vry|dz  }|dk(  r|dz  }t        |j                  |j                  |j                  |||¬«      S # t        t        f$ r Y yw xY w)a¯  The date/hour_12/minute/meridiem the model gave us, as a real datetime
    -- or None if any field is missing or out of range.

    The model gives us the time exactly as a person would say it (e.g. 9,
    30, "AM") and we do the 12-hour-to-24-hour conversion ourselves,
    deterministically, rather than asking the model to compute and pass a
    24-hour datetime. Live testing found the model unreliable at that
    specific conversion (a systematic AM/PM slip), even after the tool
    description demanded it copy a value verbatim.

    Built in whatever _resolve_effective_timezone resolves to -- the
    visitor's explicitly-named zone if given, otherwise the business's own.
    rU   r`   rV   rW   rX   ré   é   r   é;   )rS   rT   NrT   )Útzinfo)r@  r   Úfromisoformatrq   rU   rä   rr   r«   ÚyearÚmonthÚdayÚ	TypeErrorr<  )r{   r>  ÚtzrH  rV   rW   rX   Úhour_24s           r„   Ú_resolve_requested_timerL  ã  s  € ô 
% T¨;Ó	7€BðÜ×$Ñ$ T§X¡X¨fÓ%5Ò%;¸Ó<×AÑAÓCˆÜ�d—h‘h˜yÓ)Ó*ˆÜ�T—X‘X˜hÓ'Ó(ˆØ—H‘H˜ZÓ(Ò.¨B×5Ñ5Ó7×=Ñ=Ó?ˆØ�WÔ" Ô"Øð -.°Ô,=¸2Ô,=Øð CKÐR^ÑB^ØØ˜B‘,ˆØ�tÒØ�r‰MˆGÜ˜Ÿ™ #§)¡)¨S¯W©W°g¸vÈbÔQÐQøÜ”zÐ"ò Ùðús$   ŽB(D Â7D ÃD Ã
=D ÄDÄDrd   c                 ó4  — t        | «      xs i }|j                  d«      xs i }|j                  d«      sdddddœfS |j                  d«      xs i }|j                  d«      }|dvrdddd	dœfS |d
k(  rt        | «      n
t        | «      }|sdddddœfS ||dfS )zÖThe active meeting provider's connection, or (None, None, error_dict) if
    the feature is off, nothing is selected, or the selected provider has
    no active connection. Mirrors execute_send_email's guard shape.ÚfeaturesrZ   Nru   z0Meeting booking is turned off for this business.r  Ú	providers)Úgoogle_calendarÚteamsz3No meeting provider is connected for this business.rP  z8The connected meeting provider is unavailable right now.)r   rq   r   r   )rd   Útool_settingsrN  rO  ÚproviderÚ
connections         r„   Ú_book_meeting_connectionrU    sê   € ô & iÓ0Ò6°B€MØ× Ñ  Ó,Ò2°€HØ�<‰<˜Ô'Ø�T gØ&XñZð Zð 	Zð ×!Ñ! +Ó.Ò4°"€IØ�}‰}˜^Ó,€HØÐ3Ñ3Ø�T gØ&[ñ]ð ]ð 	]ð @HÐK\Ò?\Ô0°Ô;Ü*¨9Ó5ð áØ�T gØ&`ñbð bð 	bð �Z Ð%Ð%rá   c           
   ƒ   ó~  ‡‡‡‡‡‡‡K  — t         j                  j                  ˆfd„«      ƒ d{  –—† \  }Š}|r|S ‰j                  d«      xs d}	 t	        |«      }t        | |«      Š‰€d	d
dœS ‰t        d¬«      z   Št        ‰j                  d|«      }|dk(  rt         nt"        Št%        j&                  t        j                  «      }‰|k\  rgt         j                  j                  ˆˆˆˆfd„«      ƒ d{  –—† }t)        ˆfd„|D «       «      r&d|d‰j+                  «       ‰j+                  «       dœdœS t-        |‰t.        z
  «      Š‰t.        z   Št         j                  j                  ˆˆˆˆfd„«      ƒ d{  –—† }	|	D �
cg c]0  }
t1        |
t2        «      sŒ|
j                  d«      sŒ&|
d   |k\  sŒ/|
‘Œ2 }	}
|	j5                  ˆfd„¬«       |	dt6         }	d|d|	D �
cg c]w  }
t1        |
t2        «      re|
j                  d«      r|
j                  d«      j+                  «       nd|
j                  d«      r|
j                  d«      j+                  «       nddœ‘Œy c}
dœS 7 �Œ1# t
        t        t        f$ r3 t        j                  d|›d‰d   › d�«       t        j                  }Y �ŒLw xY w7 �Œ¶7 �Œ6c c}
w c c}
w ­w)aB  Checks whether the exact time the user asked for is free on whichever
    meeting provider this tenant has active -- and if not, real nearby
    alternatives instead of a generic list of whatever's chronologically
    first in the day. An unrecognized timezone string degrades to UTC rather
    than failing the call.
    c                  ó    •— t        ‰ d   «      S r)  ©rU  ©r|   s   €r„   rò   z,execute_check_availability.<locals>.<lambda>#  ó   ø€ Ô(¨¨[Ñ)9Ó:€ rá   Nr   ÚUTCzUnrecognized timezone z for tenant rd   z"; using UTC for the search window.ru   úCdate, hour_12, minute, and meridiem are required and must be valid.r  é   ©Úminutesrì   rP  c                  ó:   •—  ‰ ‰‰‰t        d¬«      z   «      xs g S ©Nré   r^  ©r   )Úcheck_fnrT  Úrequested_endÚrequested_starts   €€€€r„   rò   z,execute_check_availability.<locals>.<lambda>?  s!   ø€ ‘H˜Z¨¸-Ì)Ð\]ÔJ^Ñ:^Ó_ÒeÐce€ rá   c              3   ój   •K  — | ]*  }t        |t        «      xr |j                  d «      ‰k(  –— Œ, y­w©ÚstartN©rv   rw   rq   )rÆ   Úsre  s     €r„   rÈ   z-execute_check_availability.<locals>.<genexpr>@  s-   øè ø€ Ò^ÈQŒz˜!œTÓ"ÒH q§u¡u¨W£~¸Ñ'HÓHÑ^ùó   ƒ03ÚokT)rh  Úend)r—   r   Ú	availableÚ	requestedc                  ó    •—  ‰ ‰‰‰«      xs g S r  rÄ   )rc  rT  Ú
search_endÚsearch_starts   €€€€r„   rò   z,execute_check_availability.<locals>.<lambda>L  s   ø€ ‘˜ \°:Ó>ÒDÀ"€ rá   rh  c                 óB   •— t        | d   ‰z
  j                  «       «      S )Nrh  )ÚabsÚtotal_seconds)rj  re  s    €r„   rò   z,execute_check_availability.<locals>.<lambda>N  s   ø€ œc 1 W¡:°Ñ#?×"NÑ"NÓ"PÓQ€ rá   )rì   Frm  )r—   r   rn  Únearby_slots)rõ   rö   r÷   rq   r   r   r<  r=  rt   r¾   r   ÚutcrL  r   ÚgetattrrD  Úgcal_check_availabilityÚteams_check_availabilityr   Únowr#  Ú	isoformatÚmaxÚ_NEARBY_SEARCH_WINDOWrv   rw   ÚsortÚ_MAX_AVAILABILITY_SLOTS)r{   r|   rS  ru   Útz_namerJ  Úeffective_tz_namer{  Úexact_slotsÚnearbyrj  rc  rT  rd  re  rq  rr  s    `         @@@@@@r„   Úexecute_check_availabilityr…    s   þè ø€ ô ).¯©×(@Ñ(@Û:ó)<÷ #<Ñ€Hˆj˜%áØˆà�n‰n˜ZÓ(Ò1¨E€GðÜ�gÓˆô .¨d°BÓ7€OØÐØ!Ø^ñ`ð 	`à#¤i¸Ô&;Ñ;€Mô
   × 6Ñ 6¸¸wÓGÐà*2Ð6GÒ*GÕ&ÔMe€HÜ
�,‰,”x—|‘|Ó
$€Cà˜#ÒÜ!ŸO™O×4Ñ4Þeóg÷ gˆäÓ^ÐR]Ô^Ô^Ø"Ð0AØ $Ø*9×*CÑ*CÓ*EØ(5×(?Ñ(?Ó(Añ!CñDð Dô �s˜OÔ.CÑCÓD€LØ Ô#8Ñ8€JÜ—?‘?×+Ñ+ÞDóF÷ F€FàÖ`�A¤:¨a´Õ#6¸1¿5¹5À½>ÈaÐPWÉjÐ\_ÓN_ŠaÐ`€FÐ`Ø
‡K�KÓQ€KÔRØÐ,Ô,Ð-€FàÐ(9Øð &,öDà !¬z¸!¼TÔ/Bð EFÇEÁEÈ'ÄN a§e¡e¨G£n×&>Ñ&>Ô&@ÐX\Ø@AÇÁÀeÄ A§E¡E¨%£L×$:Ñ$:Ô$<ÐRVóXò DñEð Eð_#<úô "¤:¬xÐ8ò Ü�‰Ð/°¨{¸,Ø˜kÑ*Ð+Ð+MðOô 	Pä�\‰\‹ðúð(gùðFûâ`ùòDùs…   ‰&J=¯I °!J=ÁI# ÁBJ=Ã7J-Ã8BJ=Å:J0Å;J=ÆJ3ÆJ3Æ+J3Æ4J3Æ8'J=ÇA<J8
ÉJ=É#AJ*Ê&J=Ê)J*Ê*J=Ê0J=Ê3
J=c              ƒ   ó2  ‡‡
‡‡‡‡‡‡K  — | j                  d«      xs dj                  «       Š| j                  d«      xs dj                  «       Š‰r‰sdddœS t        j                  j	                  ˆfd„«      ƒ d{  –—† \  }Š}|r|S ‰j                  d	«      xs d
}	 t        |«      }t        | |«      Š‰€dddœS t        ‰j                  d|«      }‰t        j                  t        j                  «      k  rdddœS ‰t!        d¬«      z   Š|dk(  rt"        nt$        Š
t        j                  j	                  ˆ
ˆˆˆfd„«      ƒ d{  –—† }t'        ˆfd„|D «       «      }|sdddœS |dk(  rt(        nt*        Št        j                  j	                  ˆˆˆˆˆˆfd„«      ƒ d{  –—† }	|	sdddœS d|	j                  d«      |	j                  d«      |‰j-                  «       dœS 7 �Œf# t        t        t        f$ r t        j                  }Y �Œbw xY w7 ŒÈ7 Œo­w)z?Books a slot immediately, after re-confirming it is still free.r[   r`   r\   ru   z1visitor_email and visitor_name are both required.r  c                  ó    •— t        ‰ d   «      S r)  rX  rY  s   €r„   rò   z&execute_book_meeting.<locals>.<lambda>`  rZ  rá   Nr   r[  r\  rì   zThat time has already passed.r]  r^  rP  c                  ó:   •—  ‰ ‰‰‰t        d¬«      z   «      xs g S ra  rb  )rc  rT  rm  rh  s   €€€€r„   rò   z&execute_book_meeting.<locals>.<lambda>v  s   ø€ ‘˜ U¨C´)ÀAÔ2FÑ,FÓGÒMÈ2€ rá   c              3   ój   •K  — | ]*  }t        |t        «      xr |j                  d «      ‰k(  –— Œ, y­wrg  ri  )rÆ   rj  rh  s     €r„   rÈ   z'execute_book_meeting.<locals>.<genexpr>w  s-   øè ø€ ÒYÈ”Z ¤4Ó(ÒD¨Q¯U©U°7«^¸uÑ-DÓDÑYùrk  zDThat time is no longer available. Offer to check availability again.c                  ó   •—  ‰‰ ‰‰‰‰«      S r  rÄ   )rT  Ú	create_fnrm  rh  r[   r\   s   €€€€€€r„   rò   z&execute_book_meeting.<locals>.<lambda>~  s   ø€ ‘	˜* e¨S°-ÀÓN€ rá   z&Could not book that meeting right now.rl  Úevent_idÚmeeting_link)r—   rŒ  r�  r   Úconfirmed_start)rq   rr   rõ   rö   r÷   r   r   r<  r=  r   rw  rL  rx  rD  r   r{  r   ry  rz  r#  Úgcal_create_eventÚteams_create_eventr|  )r{   r|   rS  ru   r�  rJ  r‚  Ú
free_slotsÚ
still_freeÚbookingrc  rT  r‹  rm  rh  r[   r\   s    `        @@@@@@@r„   Úexecute_book_meetingr”  X  s  ÿè ø€ à—X‘X˜oÓ.Ò4°"×;Ñ;Ó=€MØ—H‘H˜^Ó,Ò2°×9Ñ9Ó;€LÙ¡Ø!Ð-`ÑaÐaä(-¯©×(@Ñ(@Û:ó)<÷ #<Ñ€Hˆj˜%áØˆà�n‰n˜ZÓ(Ò1¨E€GðÜ�gÓˆô $ D¨"Ó-€EØ€}Ø!Ø^ñ`ð 	`ä §¡¨e°WÓ=ÐàŒx�|‰|œHŸL™LÓ)Ò)Ø!Ð-LÑMÐMØ
”) BÔ'Ñ
'€Cà*2Ð6GÒ*GÕ&ÔMe€HÜ—‘×/Ñ/ÞMóO÷ O€JäÓYÈjÔYÓY€JÙØ!Ø_ñað 	að &.Ð1BÒ%BÕ!ÔHZ€IÜ—O‘O×,Ñ,ßNÐNóP÷ P€GáØ!Ð-UÑVÐVà¨¯©°JÓ(?Ø"Ÿ;™; ~Ó6Ø(Ø#Ÿo™oÓ/ñ1ð 1ðG#<úô "¤:¬xÐ8ò Ü�\‰\‹ðúðOøðPúsW   ŠA5HÁ?G%Â !HÂ"G( Â-BHÅ
HÅAHÆ%HÆ&A HÇ($HÈHÈHÈHÈHc              ƒ   ó˜  ‡ ‡‡‡‡K  — t         j                  j                  ˆfd„«      ƒ d{  –—† \  }Š}|r|S dŠ‰ j                  d«      r	 t	        j
                  ‰ d   «      Š|dk(  rt        nt        Št         j                  j                  ˆ ˆˆˆfd„«      ƒ d{  –—† }|sdddœS d	d
iS 7 Œ�# t        t        f$ r dŠY Œ^w xY w7 Œ(­w)z.Finds and cancels a previously booked meeting.c                  ó    •— t        ‰ d   «      S r)  rX  rY  s   €r„   rò   z(execute_cancel_meeting.<locals>.<lambda>‹  rZ  rá   Nr^   rP  c                  ó:   •—  ‰‰‰ j                  dd«      ‰¬«      S )Nr[   r`   )Ú	near_time)rq   )r{   Ú	cancel_fnrT  r˜  s   €€€€r„   rò   z(execute_cancel_meeting.<locals>.<lambda>˜  s   ø€ ‘	˜* d§h¡h¨ÀÓ&CÈyÔY€ rá   ru   z6Could not find exactly one matching meeting to cancel.r  r—   rl  )
rõ   rö   r÷   rq   r   rE  rI  r<  Úgcal_cancel_eventÚteams_cancel_event)r{   r|   rS  ru   Ú	cancelledr™  rT  r˜  s   ``   @@@r„   Úexecute_cancel_meetingr�  ˆ  sØ   üè ø€ ä(-¯©×(@Ñ(@Û:ó)<÷ #<Ñ€Hˆj˜%áØˆà€IØ‡x�xÐ"Ô#ð	Ü ×.Ñ.¨tÐ4FÑ/GÓHˆIð &.Ð1BÒ%BÕ!ÔHZ€IÜ—o‘o×.Ñ.ÞYó[÷ [€IáØ!ØQñSð 	Sð �dÐÐð'#<ùô œ:Ð&ò 	ØŠIð	úð[ús?   ‡&C
­B/®C
ÁB1 Á&9C
ÂCÂ C
Â1CÃC
ÃCÃC
)r'   r1   r?   rB   r   rG   rO   rQ   r   rZ   r]   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: ru   zunknown toolr  )ÚTOOL_EXECUTORSrq   rt   ru   )r/   r{   r|   Úhandlers       r„   Údispatch_toolr¡  ¯  sM   è ø€ ä× Ñ  Ó&€GØ€Ü�‰Ð8¸¸Ð?Ô@Ø!¨^Ñ<Ð<Ù˜˜sÓ#×#Ð#Ð#ús   ‚AA
ÁAÁA
)NN)F)cÚ__doc__rû   r¤   Úloggingr¨   r   r   r   Úzoneinfor   r   rõ   Úapp.core.configr   Úapp.core.llm_clientr	   rœ   Úapp.core.promptsr
   Úapp.services.infra.databaser   r   r   r   r   r   Úapp.services.content.documentsr   Úapp.services.messaging.emailr   r   r   Ú app.services.chat.tools_settingsr   r   r   Úapp.services.infra.embeddingr   Ú#app.services.integrations.firestorer   Ú)app.services.integrations.google_calendarr   rš  r   ry  r   r�  r   Úapp.services.integrations.teamsr›  rz  r�  r   Úapp.services.catalog.productsr    r!   r"   Úapp.services.infra.quotasr#   Úapp.services.infra.redisr$   r%   Ú	getLoggerÚ__name__rt   r$  rÜ   ÚTOOL_SCHEMASr…   r·   r¦   rÕ   rÙ   rÝ   rà   rä   rç   rí   Úsetrý   r  r  r  r§   r&  r-  r8  r€  r~  rw   r@  rL  ry   rU  r…  r”  r�  rŸ  r¡  rÄ   rá   r„   ú<module>r·     s†  ðñó Û Û Û ß 2Ñ 2ß 4ã å $Ý 7Ý 8÷÷ õ 9ß UÑ Uß eÑ eÝ 7Ý G÷ó ÷ó ÷ñ õ 9ß Dà	ˆ×	Ñ	˜8Ó	$€ð€ð à+ð	[ð !àØ (ð(Oñðð %˜Iñ
ñ
ñð< à-ð2ð2 !à*2À=Ñ!QØ(0ÐAWÑXØ(0ÐA]Ñ^Ø)1Ò;TÑ UØ&.Ð?UÑVØ+3ÐDYÑ"ZØ9AÐRoÑ0pà (Ù $Ð%6Ó 7ðQñ	!ñò" Kñ'ñ70
ñ3ðj à%ðhð !à)1ÐB_Ñ `Ø'/Ð@\Ñ]ñð (¨Ð2ññ
ñð4 à,ð?ð !à xÐ@ZÑ[ðð &˜Jññ
ñð2 à ðð& !ð !)Ø'\ñ!ð
 !)Ø'iñ ð
 !)ðTññò$ <ñ)ñ++
ñ.ð` à&ð-ð" !àØ 'à$,à(.°¸6Ð0BÐ'CØ*0°8¸VÐ2DÐ)EØ*0°(Ð);ñ+ð
 *1¨	ñ"ñ"ðð )˜Mñ!ñ'%
ñ(ðT à#ð	Oð !ð !)ð)Qñ"ð !)Ø'Lññ
ð )˜Mññ
ñ ðD à(ð@ð. !ð !)Ø'\ñð
 !)Ø']ñ!ñ	ð %˜Iññ3'
ñ*ðX à(ð_ðD !ð !)ð(Iñð !*ð(Mñ ð !*ð(Yñð !)Ø!% t ð(Dñ!ð !)ð(]ñ)ñ3!òD FñI%ñIJ
ñMð^ à"ðAð2 !ð !)ð(=ñð !*ð(Gñ ð !*Ø'^ñð
 !)Ø!% t ð(:ñ!ð !)ð(Mñ)ð !)Ø'Vñ&ð
 !)Ø'Mñ%ñE&òN gñS*ñ7F
ñIðV à$ð(ð !ð !)Ø'Xñ&ð
 !)ð(Sñ)ñ
ð -Ð-ññ
ñð]€ò@"òJM6ò`Hò #ò.#ò*2ò2ð-¸#ó -ò Kñ ›%Ð óó4%ó&ó*HòD:0òz&ò&5ðB Ð ñ "¨Ô*Ð ð dó ð* $ó ð>&¨ó &ò2:Eòz-1ò`ð2 ;Ø>Ø.Ø<Ø$Ø0Ø*Ø4Ø4Ø(Ø,ñ€ó$rá   