Ashhar commited on
Commit
e6dd180
1 Parent(s): 83a5cfb

email support

Browse files
Files changed (5) hide show
  1. app.py +119 -98
  2. icons/error.png +0 -0
  3. icons/mail.png +0 -0
  4. requirements.txt +2 -1
  5. tools.py +160 -26
app.py CHANGED
@@ -5,8 +5,9 @@ import pytz
5
  import time
6
  import json
7
  import re
 
 
8
  from transformers import AutoTokenizer
9
- from gradio_client import Client
10
  from tools import toolsInfo
11
 
12
  from dotenv import load_dotenv
@@ -18,6 +19,7 @@ if useGpt4:
18
  from openai import OpenAI
19
  client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
20
  MODEL = "gpt-4o-mini"
 
21
  MAX_CONTEXT = 128000
22
  tokenizer = AutoTokenizer.from_pretrained("Xenova/gpt-4o")
23
  else:
@@ -26,7 +28,8 @@ else:
26
  api_key=os.environ.get("GROQ_API_KEY"),
27
  )
28
  MODEL = "llama-3.1-70b-versatile"
29
- MODEL = "llama3-groq-70b-8192-tool-use-preview"
 
30
  MAX_CONTEXT = 8000
31
  tokenizer = AutoTokenizer.from_pretrained("Xenova/Meta-Llama-3.1-Tokenizer")
32
 
@@ -37,18 +40,62 @@ def countTokens(text):
37
  return len(tokens)
38
 
39
 
40
- SYSTEM_MSG = f"""
41
- You are a personalized email generator for cold outreach. You take the user through a workflow. Step by step.
 
42
 
43
- - You ask for industry of the recipient
44
- - His/her role
45
- - More details about the recipient
 
 
46
 
47
- Highlight the exact entity you're requesting for by making it CAPITAL CASE.
48
- Once collected, you store these info in a Google Sheet
 
 
 
 
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  """
51
 
 
52
  USER_ICON = "icons/man.png"
53
  ASSISTANT_ICON = "icons/magic-wand-1.png"
54
  TOOL_ICON = "icons/check.png"
@@ -124,7 +171,7 @@ def __isInvalidResponse(response: str):
124
  return True
125
 
126
  # lots of paragraphs
127
- if len(re.findall(r'\n\n', response)) > 15:
128
  return True
129
 
130
 
@@ -165,7 +212,9 @@ def __getMessages():
165
 
166
 
167
  tools = [
168
- toolsInfo["saveInGSheet"]["schema"]
 
 
169
  ]
170
 
171
 
@@ -180,14 +229,10 @@ def __showToolResponse(toolResponseDisplay: dict):
180
  width=30
181
  )
182
  with col2:
183
- st.markdown(
184
- f"""
185
- <div class='code'>
186
- {msg}
187
- </div>
188
- """,
189
- unsafe_allow_html=True
190
- )
191
 
192
 
193
  def __process_stream_chunk(chunk):
@@ -199,25 +244,6 @@ def __process_stream_chunk(chunk):
199
  return None
200
 
201
 
202
- def __addToolCallsToMsgs(toolCalls):
203
- st.session_state.messages.append(
204
- {
205
- "role": "assistant",
206
- "tool_calls": [
207
- {
208
- "id": toolCall.id,
209
- "function": {
210
- "name": toolCall.function.name,
211
- "arguments": toolCall.function.arguments,
212
- },
213
- "type": toolCall.type,
214
- }
215
- for toolCall in toolCalls
216
- ],
217
- }
218
- )
219
-
220
-
221
  def __addToolCallToMsgs(toolCall: dict):
222
  st.session_state.messages.append(
223
  {
@@ -240,9 +266,11 @@ def __processToolCalls(toolCalls):
240
  for toolCall in toolCalls:
241
  functionName = toolCall.function.name
242
  functionToCall = toolsInfo[functionName]["func"]
243
- functionArgs = json.loads(toolCall.function.arguments)
 
 
244
  functionResult = functionToCall(**functionArgs)
245
- functionResponse = functionResult["response"]
246
  responseDisplay = functionResult.get("display")
247
  pprint(f"{functionResponse=}")
248
 
@@ -274,50 +302,66 @@ def __dedupeToolCalls(toolCalls: list):
274
  return dedupedToolCalls
275
 
276
 
277
- def predict():
278
- shouldStream = False
 
 
 
 
 
 
 
 
 
279
 
280
  messagesFormatted = [{"role": "system", "content": SYSTEM_MSG}]
281
  messagesFormatted.extend(__getMessages())
282
  contextSize = countTokens(messagesFormatted)
283
- pprint(f"{contextSize=} | {MODEL}")
284
  pprint(f"{messagesFormatted=}")
285
 
286
  response = client.chat.completions.create(
287
- model=MODEL,
288
  messages=messagesFormatted,
289
- temperature=0.8,
290
  max_tokens=4000,
291
- stream=shouldStream,
292
  tools=tools
293
  )
294
- # pprint(f"llmResponse: {response}")
295
-
296
- if shouldStream:
297
- content = ""
298
- toolCall = None
299
-
300
- # for chunk in response:
301
- # chunkContent = __process_stream_chunk(chunk)
302
- # if isinstance(chunkContent, str):
303
- # content += chunkContent
304
- # yield chunkContent
305
- # elif chunkContent:
306
- # if not toolCall:
307
- # toolCall = chunkContent
308
- # else:
309
- # toolCall.function.arguments += chunkContent.function.arguments
310
-
311
- # toolCalls = [toolCall] if toolCall else []
312
- else:
313
- responseMessage = response.choices[0].message
314
- # pprint(f"{responseMessage=}")
315
- responseContent = responseMessage.content
316
- # pprint(f"{responseContent=}")
317
- if responseContent:
318
- yield responseContent
319
- toolCalls = responseMessage.tool_calls
320
- # pprint(f"{toolCalls=}")
 
 
 
 
 
 
 
321
 
322
  if toolCalls:
323
  pprint(f"{toolCalls=}")
@@ -329,7 +373,7 @@ def predict():
329
  pprint(e)
330
 
331
 
332
- st.title("EmailGenie 💌")
333
  if not (st.session_state["buttonValue"] or st.session_state["startMsg"]):
334
  st.button(START_MSG, on_click=lambda: __setStartMsg(START_MSG))
335
 
@@ -369,7 +413,7 @@ if prompt := (st.chat_input() or st.session_state["buttonValue"] or st.session_s
369
  for chunk in responseGenerator:
370
  response += chunk
371
  if __isInvalidResponse(response):
372
- pprint(f"{response=}")
373
  return
374
  responseContainer.markdown(response)
375
 
@@ -387,28 +431,6 @@ if prompt := (st.chat_input() or st.session_state["buttonValue"] or st.session_s
387
  st.session_state["buttonValue"] = optionLabel
388
  pprint(f"Selected: {optionLabel}")
389
 
390
- # responseParts = response.split(JSON_SEPARATOR)
391
-
392
- # jsonStr = None
393
- # if len(responseParts) > 1:
394
- # [response, jsonStr] = responseParts
395
-
396
- # if jsonStr:
397
- # try:
398
- # json.loads(jsonStr)
399
- # jsonObj = json.loads(jsonStr)
400
- # options = jsonObj["options"]
401
-
402
- # for option in options:
403
- # st.button(
404
- # option["label"],
405
- # key=option["id"],
406
- # on_click=lambda label=option["label"]: selectButton(label)
407
- # )
408
- # # st.code(jsonStr, language="json")
409
- # except Exception as e:
410
- # pprint(e)
411
-
412
  toolResponseDisplay = st.session_state.toolResponseDisplay
413
  st.session_state.chatHistory.append({
414
  "role": "assistant",
@@ -420,4 +442,3 @@ if prompt := (st.chat_input() or st.session_state["buttonValue"] or st.session_s
420
  "role": "assistant",
421
  "content": response,
422
  })
423
-
 
5
  import time
6
  import json
7
  import re
8
+ import random
9
+ import string
10
  from transformers import AutoTokenizer
 
11
  from tools import toolsInfo
12
 
13
  from dotenv import load_dotenv
 
19
  from openai import OpenAI
20
  client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
21
  MODEL = "gpt-4o-mini"
22
+ TOOLS_MODEL = "gpt-4o-mini"
23
  MAX_CONTEXT = 128000
24
  tokenizer = AutoTokenizer.from_pretrained("Xenova/gpt-4o")
25
  else:
 
28
  api_key=os.environ.get("GROQ_API_KEY"),
29
  )
30
  MODEL = "llama-3.1-70b-versatile"
31
+ # MODEL = "llama3-groq-70b-8192-tool-use-preview"
32
+ TOOLS_MODEL = "llama3-groq-70b-8192-tool-use-preview"
33
  MAX_CONTEXT = 8000
34
  tokenizer = AutoTokenizer.from_pretrained("Xenova/Meta-Llama-3.1-Tokenizer")
35
 
 
40
  return len(tokens)
41
 
42
 
43
+ SYSTEM_MSG = """
44
+ You are a personalized email generator for cold outreach. You take the user through the below workflow.
45
+ Keep your questions crisp. Ask only one question at a time.
46
 
47
+ # You ask about the purpose of sending email.
48
+ Give well-formatted numbered options to choose from for the email purpose.
49
+ Group the options by category as described below.
50
+ Give numbers only to the options and not categories.
51
+ Give a line break after every category name.
52
 
53
+ ##### Category: Acquiring Customers
54
+ - Lead Generation (spark interest in possible customers)
55
+ - Sales Outreach (directly ask the decision-makers)
56
+ - Partnership and Collaboration (build mutually beneficial relationships)
57
+ - Event Promotion (invite people to webinars, conferences, or other events)
58
+ - Case study or testimonial requests (ask satisfied customers for testimonials)
59
 
60
+ ##### Category: Learning and Connecting
61
+ - Networking (establish connections with industry experts)
62
+ - Market Research (gather information about target audiences or industries)
63
+ - Career Advice (seek guidance from experienced professionals)
64
+
65
+ ##### Category: Jobs and Hiring
66
+ - Job Application (apply for job openings)
67
+ - Job Referrals (ask for referrals or recommendations)
68
+ - Recruitment (reach out to potential candidates)
69
+
70
+
71
+ # You then ask sender (user) details. Whatever could be relevant for this type of email
72
+ # You ask for recipient's Industry, if it's required for this type of email
73
+ # You ask for recipient's Role in the company, if it's required for this type of email
74
+ # You ask for any other specific details required to draft this type of mail
75
+ # Once all these details are received, you save them in a Google Sheet
76
+
77
+ # You then check with the user if they can see details in the sheet.
78
+
79
+ # Once the user ackowledges, you move to the next phase of email generation.
80
+ Based on the above info, you draft 2 very different variations of email and check the user which one they're liking more.
81
+ Keep the email body in sections separated by divider "-----".
82
+ Give numbered options at the end to choose from.
83
+ Ask them if they want to finalize it. If they don't finalize, repeat doing it till the user finalizes on one.
84
+ Check with them what they would like to change in the variation.
85
+
86
+ # Once the mail is finalized. You ask the user for all the missing placeholder values to write the final mail.
87
+
88
+ # Once the placeholder values are available in the final email, you ask for the recipient email ID.
89
+
90
+ # Once you have the final mail with placeholder values and recipient exact email ID, you send the email to this id.
91
+ Dont send email until you have received a valid email from user.
92
+
93
+ # You congratulate the user and ask if he would like to save this email as a template. If he agrees, save this template in Google Sheet.
94
+
95
+ # Repeat the process for more profiles and recipients.
96
  """
97
 
98
+
99
  USER_ICON = "icons/man.png"
100
  ASSISTANT_ICON = "icons/magic-wand-1.png"
101
  TOOL_ICON = "icons/check.png"
 
171
  return True
172
 
173
  # lots of paragraphs
174
+ if len(re.findall(r'\n\n', response)) > 25:
175
  return True
176
 
177
 
 
212
 
213
 
214
  tools = [
215
+ toolsInfo["saveProfileDetailsInGSheet"]["schema"],
216
+ toolsInfo["saveTemplateInGSheet"]["schema"],
217
+ toolsInfo["sendEmail"]["schema"],
218
  ]
219
 
220
 
 
229
  width=30
230
  )
231
  with col2:
232
+ if "`" not in msg:
233
+ st.markdown(f"`{msg}`")
234
+ else:
235
+ st.markdown(msg)
 
 
 
 
236
 
237
 
238
  def __process_stream_chunk(chunk):
 
244
  return None
245
 
246
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  def __addToolCallToMsgs(toolCall: dict):
248
  st.session_state.messages.append(
249
  {
 
266
  for toolCall in toolCalls:
267
  functionName = toolCall.function.name
268
  functionToCall = toolsInfo[functionName]["func"]
269
+ functionArgsStr = toolCall.function.arguments
270
+ pprint(f"{functionName=} | {functionArgsStr=}")
271
+ functionArgs = json.loads(functionArgsStr)
272
  functionResult = functionToCall(**functionArgs)
273
+ functionResponse = functionResult.get("response")
274
  responseDisplay = functionResult.get("display")
275
  pprint(f"{functionResponse=}")
276
 
 
302
  return dedupedToolCalls
303
 
304
 
305
+ def __getRandomToolId():
306
+ return ''.join(
307
+ random.choices(
308
+ string.ascii_lowercase + string.digits,
309
+ k=4
310
+ )
311
+ )
312
+
313
+
314
+ def predict(model: str = None):
315
+ model = model or MODEL
316
 
317
  messagesFormatted = [{"role": "system", "content": SYSTEM_MSG}]
318
  messagesFormatted.extend(__getMessages())
319
  contextSize = countTokens(messagesFormatted)
320
+ pprint(f"{contextSize=} | {model}")
321
  pprint(f"{messagesFormatted=}")
322
 
323
  response = client.chat.completions.create(
324
+ model=model,
325
  messages=messagesFormatted,
326
+ temperature=0.5,
327
  max_tokens=4000,
328
+ stream=False,
329
  tools=tools
330
  )
331
+
332
+ responseMessage = response.choices[0].message
333
+ # pprint(f"{responseMessage=}")
334
+ responseContent = responseMessage.content
335
+ # pprint(f"{responseContent=}")
336
+
337
+ if responseContent and '<function=' in responseContent:
338
+ pprint("Switching to TOOLS_MODEL")
339
+ return predict(TOOLS_MODEL)
340
+
341
+ # if responseContent and responseContent.startswith('<function='):
342
+ # function_match = re.match(r'<function=(\w+)>(.*?)</+function>', responseContent)
343
+ # if function_match:
344
+ # function_name, function_args = function_match.groups()
345
+ # toolCalls = [
346
+ # {
347
+ # "id": __getRandomToolId(),
348
+ # "type": "function",
349
+ # "function": {
350
+ # "name": function_name,
351
+ # "arguments": function_args
352
+ # }
353
+ # }
354
+ # ]
355
+ # responseContent = None # Set content to None as it's a function call
356
+ # else:
357
+ # toolCalls = None
358
+ # else:
359
+ # toolCalls = responseMessage.tool_calls
360
+
361
+ if responseContent:
362
+ yield responseContent
363
+ toolCalls = responseMessage.tool_calls
364
+ # pprint(f"{toolCalls=}")
365
 
366
  if toolCalls:
367
  pprint(f"{toolCalls=}")
 
373
  pprint(e)
374
 
375
 
376
+ st.title("EmailGenie 📧🧞‍♂️")
377
  if not (st.session_state["buttonValue"] or st.session_state["startMsg"]):
378
  st.button(START_MSG, on_click=lambda: __setStartMsg(START_MSG))
379
 
 
413
  for chunk in responseGenerator:
414
  response += chunk
415
  if __isInvalidResponse(response):
416
+ pprint(f"Invalid_{response=}")
417
  return
418
  responseContainer.markdown(response)
419
 
 
431
  st.session_state["buttonValue"] = optionLabel
432
  pprint(f"Selected: {optionLabel}")
433
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  toolResponseDisplay = st.session_state.toolResponseDisplay
435
  st.session_state.chatHistory.append({
436
  "role": "assistant",
 
442
  "role": "assistant",
443
  "content": response,
444
  })
 
icons/error.png ADDED
icons/mail.png ADDED
requirements.txt CHANGED
@@ -3,4 +3,5 @@ groq
3
  transformers
4
  gradio_client
5
  oauth2client
6
- gspread
 
 
3
  transformers
4
  gradio_client
5
  oauth2client
6
+ gspread
7
+ sendgrid
tools.py CHANGED
@@ -2,22 +2,29 @@
2
  import gspread
3
  import os
4
  import json
 
 
5
 
6
- # from dotenv import load_dotenv
7
- # load_dotenv()
8
 
9
  GCP_JSON_KEY = os.environ.get("GCP_JSON_KEY")
10
  MY_EMAIL = "ashharakhlaque@gmail.com"
 
11
 
 
12
 
13
- def saveInGSheet(
14
- industry: str,
15
- role: str,
16
- profileDetails: str
 
 
 
17
  ):
18
  client = gspread.service_account_from_dict(json.loads(GCP_JSON_KEY))
19
 
20
- workBook = "Cold Outreach - User Prof"
21
  try:
22
  spreadsheet = client.open(workBook)
23
  except gspread.SpreadsheetNotFound:
@@ -30,49 +37,176 @@ def saveInGSheet(
30
  role='writer',
31
  )
32
 
33
- sheet = spreadsheet.sheet1
34
  sheet.append_row([
35
- industry,
36
- role,
37
- profileDetails
 
 
38
  ])
 
 
39
  return {
40
- "response": f"Saved in Google Sheet. [Link]({spreadsheet.url})",
41
  "display": {
42
- "text": "Saved in Google Sheet",
43
  "icon": "icons/completed-task.png",
44
  }
45
  }
46
 
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  toolsInfo = {
49
- "saveInGSheet": {
50
- "func": saveInGSheet,
51
  "schema": {
52
  "type": "function",
53
  "function": {
54
- "name": "saveInGSheet",
55
- "description": "Saves the profile details in Google Sheet",
56
  "parameters": {
57
  "type": "object",
58
  "properties": {
59
- "industry": {
 
 
 
 
60
  "type": "string",
61
- "description": "Industry of the person"
62
  },
63
- "role": {
64
  "type": "string",
65
- "description": "Role of the person in the Company"
66
  },
67
- "profileDetails": {
68
  "type": "string",
69
- "description": "Profile details of the person"
 
 
 
 
70
  }
 
71
  },
72
- "required": ["industry", "role", "profileDetails"]
73
  }
74
  }
75
  },
76
- }
77
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
 
 
2
  import gspread
3
  import os
4
  import json
5
+ from sendgrid import SendGridAPIClient
6
+ from sendgrid.helpers.mail import Mail
7
 
8
+ from dotenv import load_dotenv
9
+ load_dotenv()
10
 
11
  GCP_JSON_KEY = os.environ.get("GCP_JSON_KEY")
12
  MY_EMAIL = "ashharakhlaque@gmail.com"
13
+ TEMPLATE_COL_NAME = "Template"
14
 
15
+ sg = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY'))
16
 
17
+
18
+ def saveProfileDetailsInGSheet(
19
+ emailPurpose,
20
+ aboutYou,
21
+ recipientIndustry,
22
+ recipientRole,
23
+ specificDetails
24
  ):
25
  client = gspread.service_account_from_dict(json.loads(GCP_JSON_KEY))
26
 
27
+ workBook = "Cold Mail Outreach - User Profiles"
28
  try:
29
  spreadsheet = client.open(workBook)
30
  except gspread.SpreadsheetNotFound:
 
37
  role='writer',
38
  )
39
 
40
+ sheet = spreadsheet.worksheet("Profiles & Templates")
41
  sheet.append_row([
42
+ emailPurpose,
43
+ aboutYou,
44
+ recipientIndustry,
45
+ recipientRole,
46
+ specificDetails
47
  ])
48
+ if not (emailPurpose and aboutYou):
49
+ return {}
50
  return {
51
+ "response": "Saved profile details in Google Sheet.",
52
  "display": {
53
+ "text": f"`Saved profile details in Google Sheet` [Link]({spreadsheet.url})",
54
  "icon": "icons/completed-task.png",
55
  }
56
  }
57
 
58
 
59
+ def saveTemplateInGSheet(template: str):
60
+ client = gspread.service_account_from_dict(json.loads(GCP_JSON_KEY))
61
+
62
+ workBook = "Cold Mail Outreach - User Profiles"
63
+ try:
64
+ spreadsheet = client.open(workBook)
65
+ except gspread.SpreadsheetNotFound:
66
+ print(f"Sheet not found: {workBook}")
67
+ return {
68
+ "response": "Failed to save template. Sheet not found.",
69
+ "display": {
70
+ "text": "`Failed to save template. Sheet not found.`",
71
+ "icon": "icons/error.png",
72
+ }
73
+ }
74
+
75
+ sheet = spreadsheet.worksheet("Profiles & Templates")
76
+
77
+ headers = sheet.row_values(1)
78
+ templateColIndex = headers.index(TEMPLATE_COL_NAME) + 1
79
+ lastRow = len([row for row in sheet.get_all_values() if any(row)])
80
+
81
+ sheet.update_cell(lastRow, templateColIndex, template)
82
+
83
+ return {
84
+ "response": "Saved template in Google Sheet.",
85
+ "display": {
86
+ "text": f"`Saved template in Google Sheet` [Link]({spreadsheet.url})",
87
+ "icon": "icons/completed-task.png",
88
+ }
89
+ }
90
+
91
+
92
+ def sendEmail(
93
+ toEmail: str,
94
+ subject: str,
95
+ htmlContent: str
96
+ ):
97
+ message = Mail(
98
+ from_email=MY_EMAIL,
99
+ to_emails=toEmail,
100
+ subject=subject,
101
+ html_content=htmlContent,
102
+ )
103
+ try:
104
+ sg.send(message)
105
+ return {
106
+ "response": "Email sent",
107
+ "display": {
108
+ "text": f"`Email sent to {toEmail}`",
109
+ "icon": "icons/mail.png",
110
+ }
111
+ }
112
+ except Exception as e:
113
+ print(e.message)
114
+ return {
115
+ "response": f"Failed to send email. Error: {e.message}",
116
+ "display": {
117
+ "text": f"`Failed to send email to {toEmail}`",
118
+ "icon": "icons/error.png",
119
+ }
120
+ }
121
+
122
+
123
  toolsInfo = {
124
+ "saveProfileDetailsInGSheet": {
125
+ "func": saveProfileDetailsInGSheet,
126
  "schema": {
127
  "type": "function",
128
  "function": {
129
+ "name": "saveProfileDetailsInGSheet",
130
+ "description": "Saves the email profile details in Google Sheet",
131
  "parameters": {
132
  "type": "object",
133
  "properties": {
134
+ "emailPurpose": {
135
+ "type": "string",
136
+ "description": "Purpose of the email"
137
+ },
138
+ "aboutYou": {
139
  "type": "string",
140
+ "description": "A bit about you that's required for email content"
141
  },
142
+ "recipientIndustry": {
143
  "type": "string",
144
+ "description": "Industry of the recipient"
145
  },
146
+ "recipientRole": {
147
  "type": "string",
148
+ "description": "Recipient's role in the company"
149
+ },
150
+ "specificDetails": {
151
+ "type": "string",
152
+ "description": "Any specific details about the email"
153
  }
154
+
155
  },
156
+ "required": ["emailPurpose", "aboutYou", "recipientIndustry"]
157
  }
158
  }
159
  },
160
+ },
161
+
162
+ "saveTemplateInGSheet": {
163
+ "func": saveTemplateInGSheet,
164
+ "schema": {
165
+ "type": "function",
166
+ "function": {
167
+ "name": "saveTemplateInGSheet",
168
+ "description": "Saves the email template in Google Sheet",
169
+ "parameters": {
170
+ "type": "object",
171
+ "properties": {
172
+ "template": {
173
+ "type": "string",
174
+ "description": "Email template"
175
+ }
176
+ },
177
+ "required": ["template"]
178
+ }
179
+ }
180
+ },
181
+ },
182
+
183
+ "sendEmail": {
184
+ "func": sendEmail,
185
+ "schema": {
186
+ "type": "function",
187
+ "function": {
188
+ "name": "sendEmail",
189
+ "description": "Sends an email to the user",
190
+ "parameters": {
191
+ "type": "object",
192
+ "properties": {
193
+ "toEmail": {
194
+ "type": "string",
195
+ "description": "Email address of the recipient"
196
+ },
197
+ "subject": {
198
+ "type": "string",
199
+ "description": "Subject of the email"
200
+ },
201
+ "htmlContent": {
202
+ "type": "string",
203
+ "description": "HTML content of the email"
204
+ }
205
+ },
206
+ "required": ["toEmail", "subject", "htmlContent"]
207
+ }
208
+ }
209
+ },
210
+ },
211
 
212
+ }