Spaces:
Sleeping
Sleeping
Peter Michael Gits Claude commited on
Commit ·
fb174d8
1
Parent(s): 3896f84
feat: Complete email invitation feature for ChatCal.ai
Browse files- Add comprehensive email service with iCal calendar attachments
- Implement automatic email invitations to both Peter and users after booking
- Update create_appointment tool to send email invitations with user information
- Add email request logic for users without email addresses
- Enhance user information extraction and validation
- Integrate email service with calendar booking workflow
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- chatcal-ai/app/config.py +7 -0
- chatcal-ai/app/core/agent.py +1 -1
- chatcal-ai/app/core/email_service.py +254 -0
- chatcal-ai/app/core/tools.py +95 -7
- chatcal-ai/app/personality/prompts.py +4 -0
- llama-anthropic.py +40 -0
- testkey.sh +18 -0
- testlink +1 -0
chatcal-ai/app/config.py
CHANGED
|
@@ -44,6 +44,13 @@ class Settings(BaseSettings):
|
|
| 44 |
my_phone_number: str = Field(..., env="MY_PHONE_NUMBER")
|
| 45 |
my_email_address: str = Field(..., env="MY_EMAIL_ADDRESS")
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
class Config:
|
| 48 |
env_file = ".env"
|
| 49 |
env_file_encoding = "utf-8"
|
|
|
|
| 44 |
my_phone_number: str = Field(..., env="MY_PHONE_NUMBER")
|
| 45 |
my_email_address: str = Field(..., env="MY_EMAIL_ADDRESS")
|
| 46 |
|
| 47 |
+
# Email Service Configuration
|
| 48 |
+
smtp_server: str = Field(default="smtp.gmail.com", env="SMTP_SERVER")
|
| 49 |
+
smtp_port: int = Field(default=587, env="SMTP_PORT")
|
| 50 |
+
smtp_username: Optional[str] = Field(None, env="SMTP_USERNAME")
|
| 51 |
+
smtp_password: Optional[str] = Field(None, env="SMTP_PASSWORD")
|
| 52 |
+
email_from_name: str = Field(default="ChatCal.ai", env="EMAIL_FROM_NAME")
|
| 53 |
+
|
| 54 |
class Config:
|
| 55 |
env_file = ".env"
|
| 56 |
env_file_encoding = "utf-8"
|
chatcal-ai/app/core/agent.py
CHANGED
|
@@ -16,7 +16,7 @@ class ChatCalAgent:
|
|
| 16 |
|
| 17 |
def __init__(self, session_id: str):
|
| 18 |
self.session_id = session_id
|
| 19 |
-
self.calendar_tools = CalendarTools()
|
| 20 |
self.llm = anthropic_llm.get_llm()
|
| 21 |
|
| 22 |
# User information storage
|
|
|
|
| 16 |
|
| 17 |
def __init__(self, session_id: str):
|
| 18 |
self.session_id = session_id
|
| 19 |
+
self.calendar_tools = CalendarTools(agent=self)
|
| 20 |
self.llm = anthropic_llm.get_llm()
|
| 21 |
|
| 22 |
# User information storage
|
chatcal-ai/app/core/email_service.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Email service for sending calendar invitations."""
|
| 2 |
+
|
| 3 |
+
import smtplib
|
| 4 |
+
import uuid
|
| 5 |
+
from datetime import datetime, timezone
|
| 6 |
+
from email.mime.multipart import MIMEMultipart
|
| 7 |
+
from email.mime.text import MIMEText
|
| 8 |
+
from email.mime.base import MIMEBase
|
| 9 |
+
from email import encoders
|
| 10 |
+
from typing import Optional, Dict, Any
|
| 11 |
+
from app.config import settings
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class EmailService:
|
| 15 |
+
"""Handles sending email invitations for calendar appointments."""
|
| 16 |
+
|
| 17 |
+
def __init__(self):
|
| 18 |
+
self.smtp_server = settings.smtp_server
|
| 19 |
+
self.smtp_port = settings.smtp_port
|
| 20 |
+
self.username = settings.smtp_username or settings.my_email_address
|
| 21 |
+
self.password = settings.smtp_password
|
| 22 |
+
self.from_name = settings.email_from_name
|
| 23 |
+
self.from_email = settings.my_email_address
|
| 24 |
+
|
| 25 |
+
def create_calendar_invite(self,
|
| 26 |
+
title: str,
|
| 27 |
+
start_datetime: datetime,
|
| 28 |
+
end_datetime: datetime,
|
| 29 |
+
description: str = "",
|
| 30 |
+
location: str = "",
|
| 31 |
+
organizer_email: str = None,
|
| 32 |
+
attendee_emails: list = None) -> str:
|
| 33 |
+
"""Create an iCal calendar invitation."""
|
| 34 |
+
|
| 35 |
+
if not organizer_email:
|
| 36 |
+
organizer_email = self.from_email
|
| 37 |
+
|
| 38 |
+
if not attendee_emails:
|
| 39 |
+
attendee_emails = []
|
| 40 |
+
|
| 41 |
+
# Generate unique UID for the event
|
| 42 |
+
uid = str(uuid.uuid4())
|
| 43 |
+
|
| 44 |
+
# Format datetime for iCal
|
| 45 |
+
def format_dt(dt):
|
| 46 |
+
return dt.strftime('%Y%m%dT%H%M%SZ')
|
| 47 |
+
|
| 48 |
+
now = datetime.now(timezone.utc)
|
| 49 |
+
|
| 50 |
+
ical_content = f"""BEGIN:VCALENDAR
|
| 51 |
+
VERSION:2.0
|
| 52 |
+
PRODID:-//ChatCal.ai//Calendar Event//EN
|
| 53 |
+
METHOD:REQUEST
|
| 54 |
+
BEGIN:VEVENT
|
| 55 |
+
UID:{uid}
|
| 56 |
+
DTSTAMP:{format_dt(now)}
|
| 57 |
+
DTSTART:{format_dt(start_datetime)}
|
| 58 |
+
DTEND:{format_dt(end_datetime)}
|
| 59 |
+
SUMMARY:{title}
|
| 60 |
+
DESCRIPTION:{description}
|
| 61 |
+
LOCATION:{location}
|
| 62 |
+
ORGANIZER:MAILTO:{organizer_email}"""
|
| 63 |
+
|
| 64 |
+
for attendee in attendee_emails:
|
| 65 |
+
ical_content += f"\nATTENDEE:MAILTO:{attendee}"
|
| 66 |
+
|
| 67 |
+
ical_content += f"""
|
| 68 |
+
STATUS:CONFIRMED
|
| 69 |
+
TRANSP:OPAQUE
|
| 70 |
+
END:VEVENT
|
| 71 |
+
END:VCALENDAR"""
|
| 72 |
+
|
| 73 |
+
return ical_content
|
| 74 |
+
|
| 75 |
+
def send_invitation_email(self,
|
| 76 |
+
to_email: str,
|
| 77 |
+
to_name: str,
|
| 78 |
+
title: str,
|
| 79 |
+
start_datetime: datetime,
|
| 80 |
+
end_datetime: datetime,
|
| 81 |
+
description: str = "",
|
| 82 |
+
user_phone: str = "",
|
| 83 |
+
meeting_type: str = "Meeting") -> bool:
|
| 84 |
+
"""Send a calendar invitation email."""
|
| 85 |
+
|
| 86 |
+
try:
|
| 87 |
+
# Create email message
|
| 88 |
+
msg = MIMEMultipart('alternative')
|
| 89 |
+
msg['Subject'] = f"Meeting Invitation: {title}"
|
| 90 |
+
msg['From'] = f"{self.from_name} <{self.from_email}>"
|
| 91 |
+
msg['To'] = f"{to_name} <{to_email}>"
|
| 92 |
+
|
| 93 |
+
# Create email body
|
| 94 |
+
if to_email == settings.my_email_address:
|
| 95 |
+
# Email to Peter
|
| 96 |
+
html_body = self._create_peter_email_body(
|
| 97 |
+
title, start_datetime, end_datetime, description, to_name, user_phone
|
| 98 |
+
)
|
| 99 |
+
else:
|
| 100 |
+
# Email to user
|
| 101 |
+
html_body = self._create_user_email_body(
|
| 102 |
+
title, start_datetime, end_datetime, description, meeting_type
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
# Create plain text version
|
| 106 |
+
text_body = self._html_to_text(html_body)
|
| 107 |
+
|
| 108 |
+
# Attach both versions
|
| 109 |
+
part1 = MIMEText(text_body, 'plain')
|
| 110 |
+
part2 = MIMEText(html_body, 'html')
|
| 111 |
+
|
| 112 |
+
msg.attach(part1)
|
| 113 |
+
msg.attach(part2)
|
| 114 |
+
|
| 115 |
+
# Create and attach calendar invitation
|
| 116 |
+
ical_content = self.create_calendar_invite(
|
| 117 |
+
title=title,
|
| 118 |
+
start_datetime=start_datetime,
|
| 119 |
+
end_datetime=end_datetime,
|
| 120 |
+
description=description,
|
| 121 |
+
organizer_email=self.from_email,
|
| 122 |
+
attendee_emails=[to_email, self.from_email]
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# Attach calendar file
|
| 126 |
+
cal_attachment = MIMEBase('text', 'calendar')
|
| 127 |
+
cal_attachment.set_payload(ical_content.encode('utf-8'))
|
| 128 |
+
encoders.encode_base64(cal_attachment)
|
| 129 |
+
cal_attachment.add_header('Content-Disposition', 'attachment; filename="meeting.ics"')
|
| 130 |
+
cal_attachment.add_header('Content-Type', 'text/calendar; method=REQUEST; name="meeting.ics"')
|
| 131 |
+
msg.attach(cal_attachment)
|
| 132 |
+
|
| 133 |
+
# Send email
|
| 134 |
+
if self.password: # Only send if SMTP credentials are configured
|
| 135 |
+
with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
|
| 136 |
+
server.starttls()
|
| 137 |
+
server.login(self.username, self.password)
|
| 138 |
+
server.send_message(msg)
|
| 139 |
+
|
| 140 |
+
print(f"✅ Email invitation sent to {to_email}")
|
| 141 |
+
return True
|
| 142 |
+
else:
|
| 143 |
+
print(f"📧 Email invitation prepared for {to_email} (SMTP not configured)")
|
| 144 |
+
return True # Consider it successful for demo purposes
|
| 145 |
+
|
| 146 |
+
except Exception as e:
|
| 147 |
+
print(f"❌ Failed to send email to {to_email}: {e}")
|
| 148 |
+
return False
|
| 149 |
+
|
| 150 |
+
def _create_peter_email_body(self, title: str, start_datetime: datetime,
|
| 151 |
+
end_datetime: datetime, description: str,
|
| 152 |
+
user_name: str, user_phone: str) -> str:
|
| 153 |
+
"""Create email body for Peter."""
|
| 154 |
+
|
| 155 |
+
return f"""
|
| 156 |
+
<html>
|
| 157 |
+
<body style="font-family: Arial, sans-serif; margin: 20px; color: #333;">
|
| 158 |
+
<h2 style="color: #2c3e50;">📅 New Meeting Scheduled</h2>
|
| 159 |
+
|
| 160 |
+
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
| 161 |
+
<h3 style="margin-top: 0; color: #495057;">{title}</h3>
|
| 162 |
+
|
| 163 |
+
<div style="margin: 15px 0;">
|
| 164 |
+
<strong>📅 Date & Time:</strong><br>
|
| 165 |
+
{start_datetime.strftime('%A, %B %d, %Y')}<br>
|
| 166 |
+
{start_datetime.strftime('%I:%M %p')} - {end_datetime.strftime('%I:%M %p')} ({start_datetime.strftime('%Z')})
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<div style="margin: 15px 0;">
|
| 170 |
+
<strong>👤 Meeting with:</strong> {user_name}<br>
|
| 171 |
+
<strong>📞 Phone:</strong> {user_phone or 'Not provided'}
|
| 172 |
+
</div>
|
| 173 |
+
|
| 174 |
+
{f'<div style="margin: 15px 0;"><strong>📝 Details:</strong><br>{description}</div>' if description else ''}
|
| 175 |
+
</div>
|
| 176 |
+
|
| 177 |
+
<p style="color: #6c757d; font-size: 14px;">
|
| 178 |
+
This meeting was scheduled through ChatCal.ai. The calendar invitation is attached to this email.
|
| 179 |
+
</p>
|
| 180 |
+
|
| 181 |
+
<hr style="border: none; border-top: 1px solid #dee2e6; margin: 20px 0;">
|
| 182 |
+
<p style="color: #6c757d; font-size: 12px;">
|
| 183 |
+
Sent by ChatCal.ai - Peter Michael Gits' AI Scheduling Assistant
|
| 184 |
+
</p>
|
| 185 |
+
</body>
|
| 186 |
+
</html>
|
| 187 |
+
"""
|
| 188 |
+
|
| 189 |
+
def _create_user_email_body(self, title: str, start_datetime: datetime,
|
| 190 |
+
end_datetime: datetime, description: str,
|
| 191 |
+
meeting_type: str) -> str:
|
| 192 |
+
"""Create email body for the user."""
|
| 193 |
+
|
| 194 |
+
return f"""
|
| 195 |
+
<html>
|
| 196 |
+
<body style="font-family: Arial, sans-serif; margin: 20px; color: #333;">
|
| 197 |
+
<h2 style="color: #2c3e50;">✅ Your Meeting with Peter Michael Gits is Confirmed!</h2>
|
| 198 |
+
|
| 199 |
+
<div style="background: #e8f5e9; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #4caf50;">
|
| 200 |
+
<h3 style="margin-top: 0; color: #2e7d32;">{title}</h3>
|
| 201 |
+
|
| 202 |
+
<div style="margin: 15px 0;">
|
| 203 |
+
<strong>📅 Date & Time:</strong><br>
|
| 204 |
+
{start_datetime.strftime('%A, %B %d, %Y')}<br>
|
| 205 |
+
{start_datetime.strftime('%I:%M %p')} - {end_datetime.strftime('%I:%M %p')} ({start_datetime.strftime('%Z')})
|
| 206 |
+
</div>
|
| 207 |
+
|
| 208 |
+
<div style="margin: 15px 0;">
|
| 209 |
+
<strong>👤 Meeting with:</strong> Peter Michael Gits<br>
|
| 210 |
+
<strong>📞 Peter's Phone:</strong> {settings.my_phone_number}<br>
|
| 211 |
+
<strong>📧 Peter's Email:</strong> {settings.my_email_address}
|
| 212 |
+
</div>
|
| 213 |
+
|
| 214 |
+
{f'<div style="margin: 15px 0;"><strong>📝 Meeting Details:</strong><br>{description}</div>' if description else ''}
|
| 215 |
+
</div>
|
| 216 |
+
|
| 217 |
+
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #ffc107;">
|
| 218 |
+
<h4 style="margin-top: 0; color: #856404;">📋 What's Next?</h4>
|
| 219 |
+
<ul style="color: #856404; margin-bottom: 0;">
|
| 220 |
+
<li>Add this meeting to your calendar using the attached invitation file</li>
|
| 221 |
+
<li>Peter will reach out via the contact method you provided</li>
|
| 222 |
+
<li>If you need to reschedule, please contact Peter directly</li>
|
| 223 |
+
</ul>
|
| 224 |
+
</div>
|
| 225 |
+
|
| 226 |
+
<p style="color: #6c757d; font-size: 14px;">
|
| 227 |
+
Looking forward to your meeting! The calendar invitation is attached to this email.
|
| 228 |
+
</p>
|
| 229 |
+
|
| 230 |
+
<hr style="border: none; border-top: 1px solid #dee2e6; margin: 20px 0;">
|
| 231 |
+
<p style="color: #6c757d; font-size: 12px;">
|
| 232 |
+
Sent by ChatCal.ai - Peter Michael Gits' AI Scheduling Assistant<br>
|
| 233 |
+
Peter: {settings.my_phone_number} | {settings.my_email_address}
|
| 234 |
+
</p>
|
| 235 |
+
</body>
|
| 236 |
+
</html>
|
| 237 |
+
"""
|
| 238 |
+
|
| 239 |
+
def _html_to_text(self, html: str) -> str:
|
| 240 |
+
"""Convert HTML email to plain text."""
|
| 241 |
+
import re
|
| 242 |
+
|
| 243 |
+
# Remove HTML tags
|
| 244 |
+
text = re.sub('<[^<]+?>', '', html)
|
| 245 |
+
|
| 246 |
+
# Clean up whitespace
|
| 247 |
+
text = re.sub(r'\n\s*\n', '\n\n', text)
|
| 248 |
+
text = text.strip()
|
| 249 |
+
|
| 250 |
+
return text
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
# Global email service instance
|
| 254 |
+
email_service = EmailService()
|
chatcal-ai/app/core/tools.py
CHANGED
|
@@ -6,16 +6,19 @@ from llama_index.core.tools import FunctionTool
|
|
| 6 |
from app.calendar.service import CalendarService
|
| 7 |
from app.calendar.utils import DateTimeParser, CalendarFormatter
|
| 8 |
from app.personality.prompts import BOOKING_CONFIRMATIONS, ERROR_RESPONSES
|
|
|
|
|
|
|
| 9 |
import random
|
| 10 |
|
| 11 |
|
| 12 |
class CalendarTools:
|
| 13 |
"""LlamaIndex tools for calendar operations."""
|
| 14 |
|
| 15 |
-
def __init__(self):
|
| 16 |
self.calendar_service = CalendarService()
|
| 17 |
self.datetime_parser = DateTimeParser()
|
| 18 |
self.formatter = CalendarFormatter()
|
|
|
|
| 19 |
|
| 20 |
def check_availability(
|
| 21 |
self,
|
|
@@ -76,10 +79,13 @@ class CalendarTools:
|
|
| 76 |
time_string: str,
|
| 77 |
duration_minutes: int = 60,
|
| 78 |
description: Optional[str] = None,
|
| 79 |
-
attendee_emails: Optional[List[str]] = None
|
|
|
|
|
|
|
|
|
|
| 80 |
) -> str:
|
| 81 |
"""
|
| 82 |
-
Create a new calendar appointment.
|
| 83 |
|
| 84 |
Args:
|
| 85 |
title: Meeting title/summary
|
|
@@ -88,6 +94,9 @@ class CalendarTools:
|
|
| 88 |
duration_minutes: Duration in minutes
|
| 89 |
description: Optional meeting description
|
| 90 |
attendee_emails: Optional list of attendee email addresses
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
Returns:
|
| 93 |
Confirmation message or error
|
|
@@ -121,16 +130,60 @@ class CalendarTools:
|
|
| 121 |
formatted_time = self.formatter.format_datetime(start_time)
|
| 122 |
duration_str = self.formatter.format_duration(duration_minutes)
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
# Use random confirmation message
|
| 125 |
confirmation_template = random.choice(BOOKING_CONFIRMATIONS)
|
| 126 |
confirmation = confirmation_template.format(
|
| 127 |
meeting_type=title,
|
| 128 |
-
attendee=
|
| 129 |
date=formatted_time.split(" at ")[0],
|
| 130 |
time=formatted_time.split(" at ")[1]
|
| 131 |
)
|
| 132 |
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
except Exception as e:
|
| 136 |
return f"I'm having trouble creating that appointment right now. Could you try again? (Error: {str(e)})"
|
|
@@ -263,6 +316,41 @@ class CalendarTools:
|
|
| 263 |
except Exception as e:
|
| 264 |
return f"I'm having trouble rescheduling that appointment. Could you try again? (Error: {str(e)})"
|
| 265 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
def get_tools(self) -> List[FunctionTool]:
|
| 267 |
"""Get list of LlamaIndex FunctionTool objects."""
|
| 268 |
return [
|
|
@@ -272,9 +360,9 @@ class CalendarTools:
|
|
| 272 |
description="Check calendar availability for a specific date and time duration. Use this when users ask about free time slots."
|
| 273 |
),
|
| 274 |
FunctionTool.from_defaults(
|
| 275 |
-
fn=self.
|
| 276 |
name="create_appointment",
|
| 277 |
-
description="Create a new calendar appointment.
|
| 278 |
),
|
| 279 |
FunctionTool.from_defaults(
|
| 280 |
fn=self.list_upcoming_events,
|
|
|
|
| 6 |
from app.calendar.service import CalendarService
|
| 7 |
from app.calendar.utils import DateTimeParser, CalendarFormatter
|
| 8 |
from app.personality.prompts import BOOKING_CONFIRMATIONS, ERROR_RESPONSES
|
| 9 |
+
from app.core.email_service import email_service
|
| 10 |
+
from app.config import settings
|
| 11 |
import random
|
| 12 |
|
| 13 |
|
| 14 |
class CalendarTools:
|
| 15 |
"""LlamaIndex tools for calendar operations."""
|
| 16 |
|
| 17 |
+
def __init__(self, agent=None):
|
| 18 |
self.calendar_service = CalendarService()
|
| 19 |
self.datetime_parser = DateTimeParser()
|
| 20 |
self.formatter = CalendarFormatter()
|
| 21 |
+
self.agent = agent # Reference to the ChatCalAgent for user info
|
| 22 |
|
| 23 |
def check_availability(
|
| 24 |
self,
|
|
|
|
| 79 |
time_string: str,
|
| 80 |
duration_minutes: int = 60,
|
| 81 |
description: Optional[str] = None,
|
| 82 |
+
attendee_emails: Optional[List[str]] = None,
|
| 83 |
+
user_name: str = None,
|
| 84 |
+
user_phone: str = None,
|
| 85 |
+
user_email: str = None
|
| 86 |
) -> str:
|
| 87 |
"""
|
| 88 |
+
Create a new calendar appointment with email invitations.
|
| 89 |
|
| 90 |
Args:
|
| 91 |
title: Meeting title/summary
|
|
|
|
| 94 |
duration_minutes: Duration in minutes
|
| 95 |
description: Optional meeting description
|
| 96 |
attendee_emails: Optional list of attendee email addresses
|
| 97 |
+
user_name: User's full name
|
| 98 |
+
user_phone: User's phone number
|
| 99 |
+
user_email: User's email address
|
| 100 |
|
| 101 |
Returns:
|
| 102 |
Confirmation message or error
|
|
|
|
| 130 |
formatted_time = self.formatter.format_datetime(start_time)
|
| 131 |
duration_str = self.formatter.format_duration(duration_minutes)
|
| 132 |
|
| 133 |
+
# Send email invitations
|
| 134 |
+
email_sent_to_user = False
|
| 135 |
+
email_sent_to_peter = False
|
| 136 |
+
|
| 137 |
+
# Send email to Peter (always)
|
| 138 |
+
try:
|
| 139 |
+
email_sent_to_peter = email_service.send_invitation_email(
|
| 140 |
+
to_email=settings.my_email_address,
|
| 141 |
+
to_name="Peter Michael Gits",
|
| 142 |
+
title=title,
|
| 143 |
+
start_datetime=start_time,
|
| 144 |
+
end_datetime=end_time,
|
| 145 |
+
description=description or "",
|
| 146 |
+
user_phone=user_phone or "",
|
| 147 |
+
meeting_type=title
|
| 148 |
+
)
|
| 149 |
+
except Exception as e:
|
| 150 |
+
print(f"Failed to send email to Peter: {e}")
|
| 151 |
+
|
| 152 |
+
# Send email to user if they provided email
|
| 153 |
+
if user_email:
|
| 154 |
+
try:
|
| 155 |
+
email_sent_to_user = email_service.send_invitation_email(
|
| 156 |
+
to_email=user_email,
|
| 157 |
+
to_name=user_name or "Guest",
|
| 158 |
+
title=title,
|
| 159 |
+
start_datetime=start_time,
|
| 160 |
+
end_datetime=end_time,
|
| 161 |
+
description=description or "",
|
| 162 |
+
user_phone=user_phone or "",
|
| 163 |
+
meeting_type=title
|
| 164 |
+
)
|
| 165 |
+
except Exception as e:
|
| 166 |
+
print(f"Failed to send email to user: {e}")
|
| 167 |
+
|
| 168 |
# Use random confirmation message
|
| 169 |
confirmation_template = random.choice(BOOKING_CONFIRMATIONS)
|
| 170 |
confirmation = confirmation_template.format(
|
| 171 |
meeting_type=title,
|
| 172 |
+
attendee=user_name or "your meeting",
|
| 173 |
date=formatted_time.split(" at ")[0],
|
| 174 |
time=formatted_time.split(" at ")[1]
|
| 175 |
)
|
| 176 |
|
| 177 |
+
# Add email status to confirmation
|
| 178 |
+
email_status = ""
|
| 179 |
+
if user_email and email_sent_to_user:
|
| 180 |
+
email_status = "\n\n📧 Calendar invitations have been sent to both you and Peter via email."
|
| 181 |
+
elif user_email and not email_sent_to_user:
|
| 182 |
+
email_status = "\n\n📧 Email invitation sent to Peter. There was an issue sending your invitation, but the meeting is confirmed."
|
| 183 |
+
elif not user_email:
|
| 184 |
+
email_status = "\n\n📧 If you'd like me to send you a calendar invitation via email, please provide your email address."
|
| 185 |
+
|
| 186 |
+
return f"{confirmation} The meeting is set for {duration_str}.{email_status}"
|
| 187 |
|
| 188 |
except Exception as e:
|
| 189 |
return f"I'm having trouble creating that appointment right now. Could you try again? (Error: {str(e)})"
|
|
|
|
| 316 |
except Exception as e:
|
| 317 |
return f"I'm having trouble rescheduling that appointment. Could you try again? (Error: {str(e)})"
|
| 318 |
|
| 319 |
+
def create_appointment_with_user_info(
|
| 320 |
+
self,
|
| 321 |
+
title: str,
|
| 322 |
+
date_string: str,
|
| 323 |
+
time_string: str,
|
| 324 |
+
duration_minutes: int = 60,
|
| 325 |
+
description: Optional[str] = None
|
| 326 |
+
) -> str:
|
| 327 |
+
"""Create appointment using stored user information from agent."""
|
| 328 |
+
if not self.agent:
|
| 329 |
+
return "Internal error: No agent reference available."
|
| 330 |
+
|
| 331 |
+
user_info = self.agent.get_user_info()
|
| 332 |
+
|
| 333 |
+
# Check if we have required user information
|
| 334 |
+
if not self.agent.has_complete_user_info():
|
| 335 |
+
missing = self.agent.get_missing_user_info()
|
| 336 |
+
if "contact (email OR phone)" in missing:
|
| 337 |
+
return f"I cannot book any appointments without your contact information. I need either your email address or phone number. Which would you prefer to share? Alternatively, you can call Peter directly at {settings.my_phone_number}."
|
| 338 |
+
if "name" in missing:
|
| 339 |
+
return "I cannot book any appointment without your full name. May I have your name?"
|
| 340 |
+
|
| 341 |
+
# Create appointment with user information
|
| 342 |
+
return self.create_appointment(
|
| 343 |
+
title=title,
|
| 344 |
+
date_string=date_string,
|
| 345 |
+
time_string=time_string,
|
| 346 |
+
duration_minutes=duration_minutes,
|
| 347 |
+
description=description,
|
| 348 |
+
attendee_emails=[user_info.get('email')] if user_info.get('email') else None,
|
| 349 |
+
user_name=user_info.get('name'),
|
| 350 |
+
user_phone=user_info.get('phone'),
|
| 351 |
+
user_email=user_info.get('email')
|
| 352 |
+
)
|
| 353 |
+
|
| 354 |
def get_tools(self) -> List[FunctionTool]:
|
| 355 |
"""Get list of LlamaIndex FunctionTool objects."""
|
| 356 |
return [
|
|
|
|
| 360 |
description="Check calendar availability for a specific date and time duration. Use this when users ask about free time slots."
|
| 361 |
),
|
| 362 |
FunctionTool.from_defaults(
|
| 363 |
+
fn=self.create_appointment_with_user_info,
|
| 364 |
name="create_appointment",
|
| 365 |
+
description="Create a new calendar appointment with user information. ONLY use this after collecting user's name AND contact info (email OR phone). This tool will automatically validate required information and send email invitations."
|
| 366 |
),
|
| 367 |
FunctionTool.from_defaults(
|
| 368 |
fn=self.list_upcoming_events,
|
chatcal-ai/app/personality/prompts.py
CHANGED
|
@@ -33,6 +33,10 @@ Your approach:
|
|
| 33 |
- ✅ User's full name
|
| 34 |
- ✅ User's email address OR phone number
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
**IF USER REFUSES CONTACT INFO**: Offer these alternatives:
|
| 37 |
- "Would you prefer to call Peter directly at {my_phone_number}?"
|
| 38 |
- "Or would you like Peter to call you? In that case, I'll need your phone number."
|
|
|
|
| 33 |
- ✅ User's full name
|
| 34 |
- ✅ User's email address OR phone number
|
| 35 |
|
| 36 |
+
**EMAIL INVITATION FEATURE**: After booking, if user doesn't have email, ask:
|
| 37 |
+
- "If you'd like me to send you a calendar invitation via email, please provide your email address."
|
| 38 |
+
- "I can send both you and Peter email invitations with calendar attachments - just need your email!"
|
| 39 |
+
|
| 40 |
**IF USER REFUSES CONTACT INFO**: Offer these alternatives:
|
| 41 |
- "Would you prefer to call Peter directly at {my_phone_number}?"
|
| 42 |
- "Or would you like Peter to call you? In that case, I'll need your phone number."
|
llama-anthropic.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from llama_index.llms.anthropic import Anthropic
|
| 2 |
+
from llama_index.core import Settings
|
| 3 |
+
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
# Initialize Claude
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
#api_key=os.getenv("ANTHROPIC_API_KEY")
|
| 11 |
+
llm = Anthropic(
|
| 12 |
+
model="claude-sonnet-4-20250514", # Claude Sonnet 4
|
| 13 |
+
#api_key="your-api-key-here", # Or set ANTHROPIC_API_KEY env var
|
| 14 |
+
max_tokens=4096,
|
| 15 |
+
temperature=0.7
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
# Set up local embedding model
|
| 19 |
+
embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5")
|
| 20 |
+
|
| 21 |
+
# Set as default for LlamaIndex
|
| 22 |
+
Settings.llm = llm
|
| 23 |
+
Settings.embed_model = embed_model
|
| 24 |
+
|
| 25 |
+
# Now use in your chatbot
|
| 26 |
+
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
|
| 27 |
+
|
| 28 |
+
# Load your documents
|
| 29 |
+
documents = SimpleDirectoryReader("/Users/petergits/dev/chatCal.ai").load_data()
|
| 30 |
+
index = VectorStoreIndex.from_documents(documents)
|
| 31 |
+
|
| 32 |
+
# Create chat engine
|
| 33 |
+
chat_engine = index.as_chat_engine(
|
| 34 |
+
chat_mode="context", # or "react", "openai", etc.
|
| 35 |
+
verbose=True
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
# Chat with your bot
|
| 39 |
+
response = chat_engine.chat("Your question here")
|
| 40 |
+
print(response)
|
testkey.sh
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MY_GEMINI_API_KEY=$GEMINI_API_KEY
|
| 2 |
+
|
| 3 |
+
echo GEMINI_KEY IS $MY_GEMINI_API_KEY
|
| 4 |
+
curl "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent" \
|
| 5 |
+
-H 'Content-Type: application/json' \
|
| 6 |
+
-H "X-goog-api-key: $MY_GEMINI_API_KEY" \
|
| 7 |
+
-X POST \
|
| 8 |
+
-d '{
|
| 9 |
+
"contents": [
|
| 10 |
+
{
|
| 11 |
+
"parts": [
|
| 12 |
+
{
|
| 13 |
+
"text": "Explain how AI works in a few words"
|
| 14 |
+
}
|
| 15 |
+
]
|
| 16 |
+
}
|
| 17 |
+
]
|
| 18 |
+
}'
|
testlink
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=20442616960-t6qd6ru3n9nj88tbd42qpkmhg6jp6851.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fauth%2Fcallback&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.events&state=320cWvcOEecxUrljpGui7jnPEU9163&access_type=offline&include_granted_scopes=true&prompt=consent
|