SyedBasitAbbas commited on
Commit
7c26280
·
verified ·
1 Parent(s): e917ec8

Upload 14 files

Browse files
.env ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ CAL_API_KEY=cal_live_c51069c635cb79e8ddb92bd38c85887c
2
+ CAL_USERNAME=fiwyve-cliplat
3
+ OPENAI_API_KEY=sk-vIC99U09XnsUcghERnIdhdTtznAGLcldxZzR2SjqAUT3BlbkFJ_DAvTZPRBjLBKbjhZm79nH8xliS0PYjic-mTx1BNwA
4
+ GOOGLE_API_KEY=AIzaSyCO3jJkzNlgkcc_egBIKlelgberVozSON0
__pycache__/c_booking_controller.cpython-312.pyc ADDED
Binary file (12.6 kB). View file
 
__pycache__/c_langgraph_flow.cpython-312.pyc ADDED
Binary file (5.49 kB). View file
 
__pycache__/c_langgraph_flow_new.cpython-312.pyc ADDED
Binary file (11.5 kB). View file
 
__pycache__/improved_cal_booking.cpython-312.pyc ADDED
Binary file (9.53 kB). View file
 
c_booking_controller.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Streamlit interface for the barber booking system."""
2
+
3
+ import streamlit as st
4
+ from c_langgraph_flow_new import run_booking_flow, SERVICE_MAPPING
5
+ import os
6
+ from dotenv import load_dotenv
7
+
8
+ # Load environment variables
9
+ load_dotenv()
10
+
11
+ def initialize_session():
12
+ """Initialize the session state with default values."""
13
+ if "messages" not in st.session_state:
14
+ st.session_state.messages = []
15
+ # Add initial greeting
16
+ st.session_state.messages.append({
17
+ "role": "assistant",
18
+ "content": "Hello! Welcome to our barber shop. I'll help you book an appointment. Could you please tell me your name?"
19
+ })
20
+
21
+ if "booking_info" not in st.session_state:
22
+ st.session_state.booking_info = {
23
+ "current_node": "Greeting",
24
+ "booking_info": {},
25
+ "response": "",
26
+ "messages": []
27
+ }
28
+
29
+ def main():
30
+ st.set_page_config(page_title="AI Barber Booking", page_icon="💇‍♂️")
31
+ st.title("💇‍♂️ AI Barber Booking Assistant")
32
+
33
+ # Initialize session
34
+ initialize_session()
35
+
36
+ # Display current state (for debugging)
37
+ with st.sidebar:
38
+ st.subheader("Debug Information")
39
+ st.write("Current State:", st.session_state.booking_info.get("current_node", "Unknown"))
40
+ st.write("\nBooking Info:", st.session_state.booking_info.get("booking_info", {}))
41
+ if st.checkbox("Show Full Debug Info"):
42
+ st.json(st.session_state.booking_info)
43
+
44
+ # Display service cards
45
+ st.markdown("### Our Services")
46
+ cols = st.columns(len(SERVICE_MAPPING))
47
+ for col, (service_name, details) in zip(cols, SERVICE_MAPPING.items()):
48
+ with col:
49
+ st.markdown(
50
+ f"""
51
+ <div style="padding: 10px; border-radius: 10px; border: 1px solid #ddd; text-align: center;">
52
+ <h4>{service_name.title()}</h4>
53
+ <p>${details['price']}<br>{details['duration']} min</p>
54
+ </div>
55
+ """,
56
+ unsafe_allow_html=True
57
+ )
58
+
59
+ # Chat interface
60
+ st.markdown("### Chat with our Booking Assistant")
61
+
62
+ # Display chat history
63
+ for message in st.session_state.messages:
64
+ with st.chat_message(message["role"]):
65
+ st.markdown(message["content"])
66
+
67
+ # Get user input
68
+ if prompt := st.chat_input("Type your message here..."):
69
+ if prompt.strip() == "":
70
+ st.warning("Please enter a valid message.")
71
+ return
72
+
73
+ # Add user message to chat history
74
+ st.session_state.messages.append({"role": "user", "content": prompt})
75
+ with st.chat_message("user"):
76
+ st.markdown(prompt)
77
+
78
+ try:
79
+ # Update booking_info messages
80
+ st.session_state.booking_info["messages"] = st.session_state.messages.copy()
81
+
82
+ # Process user input through LangGraph flow
83
+ with st.spinner("Processing your request..."):
84
+ new_state = run_booking_flow(prompt, st.session_state.booking_info)
85
+
86
+ # Update session state
87
+ st.session_state.booking_info = new_state
88
+
89
+ # Add assistant response to chat history
90
+ if new_state.get("response"):
91
+ st.session_state.messages.append({
92
+ "role": "assistant",
93
+ "content": new_state["response"]
94
+ })
95
+ with st.chat_message("assistant"):
96
+ st.markdown(new_state["response"])
97
+
98
+ # Force streamlit to rerun
99
+ st.rerun()
100
+
101
+ except Exception as e:
102
+ st.error(f"An error occurred: {str(e)}")
103
+ print(f"Error in main loop: {str(e)}")
104
+
105
+ if __name__ == "__main__":
106
+ main()
c_langgraph_flow_new.py ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Main flow controller for the barber booking system."""
2
+
3
+ import json
4
+ from typing import Dict, Any, Union, TypedDict
5
+ from langchain.schema import HumanMessage, SystemMessage
6
+ from langchain_core.messages import AIMessage
7
+ import os
8
+ from dotenv import load_dotenv
9
+ import google.generativeai as genai
10
+
11
+ from config.services import SERVICE_MAPPING
12
+ from config.prompts import BOOKING_FLOW
13
+ from config.validation import (
14
+ validate_email, validate_phone, validate_name, format_phone,
15
+ validate_booking_info, get_closest_service
16
+ )
17
+
18
+ # Load environment variables
19
+ load_dotenv()
20
+
21
+ # Initialize Gemini
22
+ def get_llm():
23
+ api_key = os.getenv("GOOGLE_API_KEY")
24
+ if not api_key:
25
+ raise ValueError("GOOGLE_API_KEY environment variable is not set")
26
+
27
+ try:
28
+ genai.configure(api_key=api_key)
29
+ model = genai.GenerativeModel('gemini-pro')
30
+ return model
31
+ except Exception as e:
32
+ print(f"Error initializing Gemini: {str(e)}")
33
+ raise
34
+
35
+ try:
36
+ llm = get_llm()
37
+ except Exception as e:
38
+ print(f"Error initializing LLM: {str(e)}")
39
+ raise
40
+
41
+ # Type definition for booking state
42
+ class BookingState(TypedDict):
43
+ current_node: str
44
+ booking_info: Dict[str, Any]
45
+ response: str
46
+ messages: list
47
+
48
+ def handle_greeting(message: str, booking_info: Dict) -> Dict:
49
+ """Handle the greeting state."""
50
+ if validate_name(message):
51
+ name = message.strip().title()
52
+ return {
53
+ "current_node": "ServiceSelection",
54
+ "booking_info": {"name": name},
55
+ "response": f"Hi {name}, welcome to our barbershop! Here are our services:"
56
+ }
57
+ return {
58
+ "current_node": "Greeting",
59
+ "booking_info": booking_info,
60
+ "response": "Could you please tell me your name?"
61
+ }
62
+
63
+ def handle_service_selection(message: str, booking_info: Dict) -> Dict:
64
+ """Handle the service selection state."""
65
+ message = message.lower().strip()
66
+
67
+ # Check for cancellation
68
+ if message in ["no", "cancel", "actually no", "nevermind"]:
69
+ return {
70
+ "current_node": "ServiceSelection",
71
+ "booking_info": booking_info,
72
+ "response": "No problem. Which service would you like to book? We offer haircut, beard trim, and full service."
73
+ }
74
+
75
+ # Check for multiple services
76
+ if "and" in message or "," in message:
77
+ return {
78
+ "current_node": "ServiceSelection",
79
+ "booking_info": booking_info,
80
+ "response": "I see you're interested in multiple services. I recommend booking our full service which includes both haircut and beard trim. Would you like that?"
81
+ }
82
+
83
+ # Check for completion keywords
84
+ if message in ["no", "nothing", "nothing else", "that's all"]:
85
+ if "service" in booking_info:
86
+ return {
87
+ "current_node": "ShowTopSlots",
88
+ "booking_info": booking_info,
89
+ "response": "Great! Let me show you our available time slots."
90
+ }
91
+
92
+ # Try to match service
93
+ service = get_closest_service(message)
94
+ if service:
95
+ booking_info["service"] = service
96
+ return {
97
+ "current_node": "ShowTopSlots",
98
+ "booking_info": booking_info,
99
+ "response": f"Perfect! Let me show you our available time slots for your {service}."
100
+ }
101
+
102
+ return {
103
+ "current_node": "ServiceSelection",
104
+ "booking_info": booking_info,
105
+ "response": "Which service would you like to book? We offer haircut, beard trim, and full service."
106
+ }
107
+
108
+ def handle_time_selection(message: str, booking_info: Dict) -> Dict:
109
+ """Handle the time selection state."""
110
+ time_slots = ["9:00 AM", "10:00 AM", "11:00 AM", "2:00 PM", "3:00 PM"]
111
+ message = message.upper().strip()
112
+
113
+ # Check for cancellation or change
114
+ if message.lower() in ["cancel", "change", "different time", "other time"]:
115
+ slots_text = "\n".join([f"- {slot}" for slot in time_slots])
116
+ return {
117
+ "current_node": "TimeSelection",
118
+ "booking_info": booking_info,
119
+ "response": f"Here are all our available times:\n{slots_text}\nWhich time works best for you?"
120
+ }
121
+
122
+ # Handle relative time references
123
+ relative_times = {
124
+ "MORNING": ["9:00 AM", "10:00 AM", "11:00 AM"],
125
+ "AFTERNOON": ["2:00 PM", "3:00 PM"],
126
+ "EARLY": ["9:00 AM", "10:00 AM"],
127
+ "LATE": ["2:00 PM", "3:00 PM"],
128
+ "FIRST": ["9:00 AM"],
129
+ "LAST": ["3:00 PM"]
130
+ }
131
+
132
+ for keyword, options in relative_times.items():
133
+ if keyword in message:
134
+ slots_text = "\n".join([f"- {slot}" for slot in options])
135
+ return {
136
+ "current_node": "TimeSelection",
137
+ "booking_info": booking_info,
138
+ "response": f"Here are the {keyword.lower()} slots:\n{slots_text}\nWhich specific time would you prefer?"
139
+ }
140
+
141
+ # Normalize input
142
+ message = message.replace(":", "").replace(" ", "")
143
+
144
+ # Map variations to standard format
145
+ time_map = {
146
+ "9": "9:00 AM", "9AM": "9:00 AM", "900": "9:00 AM", "900AM": "9:00 AM", "NINE": "9:00 AM",
147
+ "10": "10:00 AM", "10AM": "10:00 AM", "1000": "10:00 AM", "1000AM": "10:00 AM", "TEN": "10:00 AM",
148
+ "11": "11:00 AM", "11AM": "11:00 AM", "1100": "11:00 AM", "1100AM": "11:00 AM", "ELEVEN": "11:00 AM",
149
+ "2": "2:00 PM", "2PM": "2:00 PM", "200": "2:00 PM", "200PM": "2:00 PM", "TWO": "2:00 PM",
150
+ "3": "3:00 PM", "3PM": "3:00 PM", "300": "3:00 PM", "300PM": "3:00 PM", "THREE": "3:00 PM"
151
+ }
152
+
153
+ selected_time = time_map.get(message)
154
+ if selected_time and selected_time in time_slots:
155
+ booking_info["time_slot"] = selected_time
156
+ return {
157
+ "current_node": "CustomerInfo",
158
+ "booking_info": booking_info,
159
+ "response": f"Perfect! I'll book you for {selected_time}. What's your email address?"
160
+ }
161
+
162
+ # If time not found, show all slots
163
+ slots_text = "\n".join([f"- {slot}" for slot in time_slots])
164
+ return {
165
+ "current_node": "TimeSelection",
166
+ "booking_info": booking_info,
167
+ "response": f"I couldn't understand that time. Please select from these available times:\n{slots_text}"
168
+ }
169
+
170
+ def handle_customer_info(message: str, booking_info: Dict) -> Dict:
171
+ """Handle the customer information state."""
172
+ if "email" not in booking_info:
173
+ if validate_email(message):
174
+ booking_info["email"] = message
175
+ return {
176
+ "current_node": "CustomerInfo",
177
+ "booking_info": booking_info,
178
+ "response": "Great! Now, what's your phone number for appointment reminders?"
179
+ }
180
+ return {
181
+ "current_node": "CustomerInfo",
182
+ "booking_info": booking_info,
183
+ "response": "Please provide a valid email address (e.g., name@example.com)."
184
+ }
185
+
186
+ if "phone" not in booking_info:
187
+ if validate_phone(message):
188
+ booking_info["phone"] = format_phone(message)
189
+ return {
190
+ "current_node": "Confirmation",
191
+ "booking_info": booking_info,
192
+ "response": "Perfect! Let me confirm your booking details..."
193
+ }
194
+ return {
195
+ "current_node": "CustomerInfo",
196
+ "booking_info": booking_info,
197
+ "response": "Please provide a valid phone number (e.g., 1234567890)."
198
+ }
199
+
200
+ return {
201
+ "current_node": "CustomerInfo",
202
+ "booking_info": booking_info,
203
+ "response": "I need your contact information. What's your email address?"
204
+ }
205
+
206
+ def handle_confirmation(message: str, booking_info: Dict) -> Dict:
207
+ """Handle the confirmation state."""
208
+ message = message.lower().strip()
209
+
210
+ # Format booking summary
211
+ service = booking_info.get("service", "")
212
+ price = SERVICE_MAPPING[service]["price"] if service else 0
213
+ duration = SERVICE_MAPPING[service]["duration"] if service else 0
214
+
215
+ summary = f"""Here's your booking summary:
216
+ - Name: {booking_info.get('name', '')}
217
+ - Service: {service.title()} (${price}, {duration} min)
218
+ - Time: {booking_info.get('time_slot', '')}
219
+ - Email: {booking_info.get('email', '')}
220
+ - Phone: {booking_info.get('phone', '')}
221
+
222
+ Is this correct? Please confirm (yes/no)."""
223
+
224
+ if message in ["yes", "correct", "confirm", "y"]:
225
+ return {
226
+ "current_node": "BookingComplete",
227
+ "booking_info": {**booking_info, "confirmation": True},
228
+ "response": "Great! Your appointment has been confirmed. We'll send you a confirmation email shortly."
229
+ }
230
+
231
+ if message in ["no", "wrong", "incorrect", "n"]:
232
+ return {
233
+ "current_node": "ServiceSelection",
234
+ "booking_info": {"name": booking_info.get("name", "")},
235
+ "response": "I understand. Let's start over with the service selection."
236
+ }
237
+
238
+ return {
239
+ "current_node": "Confirmation",
240
+ "booking_info": booking_info,
241
+ "response": summary
242
+ }
243
+
244
+ def process_node(state: Dict) -> Dict:
245
+ """Process the current node in the booking flow."""
246
+ try:
247
+ current_node = state["current_node"]
248
+ booking_info = state.get("booking_info", {})
249
+ messages = state.get("messages", [])
250
+
251
+ # Get the last user message
252
+ last_message = ""
253
+ if messages:
254
+ last_msg = messages[-1]
255
+ if isinstance(last_msg, dict):
256
+ last_message = last_msg.get("content", "")
257
+ elif isinstance(last_msg, (HumanMessage, AIMessage, SystemMessage)):
258
+ last_message = last_msg.content
259
+ else:
260
+ last_message = str(last_msg)
261
+
262
+ # Handle each state
263
+ if current_node == "Greeting":
264
+ new_state = handle_greeting(last_message, booking_info)
265
+ elif current_node == "ServiceSelection":
266
+ new_state = handle_service_selection(last_message, booking_info)
267
+ elif current_node == "ShowTopSlots":
268
+ time_slots = ["9:00 AM", "10:00 AM", "11:00 AM", "2:00 PM", "3:00 PM"]
269
+ slots_text = "\n".join([f"- {slot}" for slot in time_slots])
270
+ new_state = {
271
+ "current_node": "TimeSelection",
272
+ "booking_info": booking_info,
273
+ "response": f"Here are our available time slots:\n{slots_text}\n\nWhich time works best for you?"
274
+ }
275
+ elif current_node == "TimeSelection":
276
+ new_state = handle_time_selection(last_message, booking_info)
277
+ elif current_node == "CustomerInfo":
278
+ new_state = handle_customer_info(last_message, booking_info)
279
+ elif current_node == "Confirmation":
280
+ new_state = handle_confirmation(last_message, booking_info)
281
+ elif current_node == "BookingComplete":
282
+ new_state = {
283
+ "current_node": "Farewell",
284
+ "booking_info": booking_info,
285
+ "response": "Thank you for booking with us! We look forward to seeing you soon."
286
+ }
287
+ else:
288
+ new_state = {
289
+ "current_node": "Greeting",
290
+ "booking_info": {},
291
+ "response": "Hello! Welcome to our barber shop. Could you please tell me your name?"
292
+ }
293
+
294
+ return {**new_state, "messages": messages}
295
+
296
+ except Exception as e:
297
+ print(f"Error in process_node: {str(e)}")
298
+ return {
299
+ "current_node": state.get("current_node", "Greeting"),
300
+ "booking_info": state.get("booking_info", {}),
301
+ "response": "I encountered an error. Could you please rephrase that?",
302
+ "messages": messages
303
+ }
304
+
305
+ def run_booking_flow(user_input: str, state: Dict[str, Any] = None) -> Dict[str, Any]:
306
+ """Run the booking workflow."""
307
+ if state is None:
308
+ state = {
309
+ "current_node": "Greeting",
310
+ "booking_info": {},
311
+ "response": "",
312
+ "messages": []
313
+ }
314
+
315
+ try:
316
+ # Process the current node
317
+ new_state = process_node(state)
318
+ return new_state
319
+
320
+ except Exception as e:
321
+ print(f"Error in run_booking_flow: {str(e)}")
322
+ return {
323
+ "current_node": "Greeting",
324
+ "booking_info": {},
325
+ "response": "I encountered an error. Let's start over.",
326
+ "messages": []
327
+ }
328
+
329
+ # Export necessary components
330
+ __all__ = ['run_booking_flow', 'SERVICE_MAPPING']
config/__pycache__/prompts.cpython-312.pyc ADDED
Binary file (5.06 kB). View file
 
config/__pycache__/services.cpython-312.pyc ADDED
Binary file (625 Bytes). View file
 
config/__pycache__/validation.cpython-312.pyc ADDED
Binary file (3.94 kB). View file
 
config/prompts.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Prompts and flow configuration for the barber booking system."""
2
+
3
+ BOOKING_FLOW = {
4
+ "Greeting": {
5
+ "prompt": """You are a friendly barber shop assistant. Your task is to get the customer's name.
6
+
7
+ Current message: "{user_input}"
8
+ Current booking info: {booking_info}
9
+
10
+ Rules:
11
+ 1. Accept any greeting or name input
12
+ 2. If input length >= 2, treat it as a name
13
+ 3. Store name in title case
14
+ 4. After getting name, show services list
15
+
16
+ Available services:
17
+ {services_list}
18
+
19
+ Output format (JSON):
20
+ {{
21
+ "next_node": "ServiceSelection",
22
+ "booking_info": {{"name": "User Name"}},
23
+ "response": "Your response message"
24
+ }}""",
25
+ "next_states": ["ServiceSelection"],
26
+ "required_info": ["name"]
27
+ },
28
+ "ServiceSelection": {
29
+ "prompt": """You are helping select a service.
30
+
31
+ Current message: "{user_input}"
32
+ Current booking info: {booking_info}
33
+ Previous messages: {messages}
34
+
35
+ Available services:
36
+ {services_list}
37
+
38
+ Rules:
39
+ 1. Direct matches: "haircut", "beard trim", "full service"
40
+ 2. Partial matches:
41
+ - haircut: "hair", "cut"
42
+ - beard trim: "beard", "trim"
43
+ - full service: "full", "complete", "both"
44
+ 3. Once service is selected, move to ShowTopSlots
45
+ 4. If user says "no" or "nothing else", assume they're done selecting and move to ShowTopSlots
46
+
47
+ Output format (JSON):
48
+ {{
49
+ "next_node": "ShowTopSlots",
50
+ "booking_info": {{"service": "selected_service"}},
51
+ "response": "Great! Let me show you our available time slots for your [service]."
52
+ }}""",
53
+ "next_states": ["ShowTopSlots"],
54
+ "required_info": ["service"]
55
+ },
56
+ "ShowTopSlots": {
57
+ "prompt": """You are showing available time slots.
58
+
59
+ Current message: "{user_input}"
60
+ Current booking info: {booking_info}
61
+
62
+ Rules:
63
+ 1. Show these time slots:
64
+ - 9:00 AM
65
+ - 10:00 AM
66
+ - 11:00 AM
67
+ - 2:00 PM
68
+ - 3:00 PM
69
+ 2. Ask user to select a preferred time
70
+ 3. Include service duration and price
71
+
72
+ Output format (JSON):
73
+ {{
74
+ "next_node": "TimeSelection",
75
+ "booking_info": {booking_info},
76
+ "response": "Here are our available time slots for your {booking_info[service]}:\\n- 9:00 AM\\n- 10:00 AM\\n- 11:00 AM\\n- 2:00 PM\\n- 3:00 PM\\n\\nWhich time works best for you?"
77
+ }}""",
78
+ "next_states": ["TimeSelection"],
79
+ "required_info": []
80
+ },
81
+ "TimeSelection": {
82
+ "prompt": """You are helping select a time slot.
83
+
84
+ Current message: "{user_input}"
85
+ Current booking info: {booking_info}
86
+
87
+ Available slots:
88
+ - 9:00 AM
89
+ - 10:00 AM
90
+ - 11:00 AM
91
+ - 2:00 PM
92
+ - 3:00 PM
93
+
94
+ Rules:
95
+ 1. Match user's input to available slots
96
+ 2. Accept variations (e.g., "9", "9am", "9:00", "9 am")
97
+ 3. If valid slot selected, move to CustomerInfo
98
+ 4. If invalid, stay in TimeSelection and show slots again
99
+
100
+ Output format (JSON):
101
+ {{
102
+ "next_node": "CustomerInfo" if valid_slot else "TimeSelection",
103
+ "booking_info": {{"time_slot": "selected_time"}} if valid_slot,
104
+ "response": "Perfect! I'll book you for [time]. What's your email address?" if valid_slot else "Please select from these times:\\n- 9:00 AM\\n- 10:00 AM\\n- 11:00 AM\\n- 2:00 PM\\n- 3:00 PM"
105
+ }}""",
106
+ "next_states": ["CustomerInfo"],
107
+ "required_info": ["time_slot"]
108
+ },
109
+ "CustomerInfo": {
110
+ "prompt": """You are collecting customer contact information.
111
+
112
+ Current message: "{user_input}"
113
+ Current booking info: {booking_info}
114
+
115
+ Rules:
116
+ 1. First collect email
117
+ 2. Then collect phone
118
+ 3. Validate format of each
119
+ 4. Move to Confirmation when both collected
120
+
121
+ Output format (JSON):
122
+ {{
123
+ "next_node": "Confirmation" if have_all_info else "CustomerInfo",
124
+ "booking_info": {{
125
+ "email": "user@email.com" if valid_email,
126
+ "phone": "+1234567890" if valid_phone
127
+ }},
128
+ "response": "Your response asking for missing info or confirming"
129
+ }}""",
130
+ "next_states": ["Confirmation"],
131
+ "required_info": ["email", "phone"]
132
+ },
133
+ "Confirmation": {
134
+ "prompt": """You are confirming the booking details.
135
+
136
+ Current message: "{user_input}"
137
+ Current booking info: {booking_info}
138
+
139
+ Rules:
140
+ 1. Show full booking summary:
141
+ - Name
142
+ - Service
143
+ - Time
144
+ - Email
145
+ - Phone
146
+ 2. Ask for confirmation
147
+ 3. Accept yes/no response
148
+
149
+ Output format (JSON):
150
+ {{
151
+ "next_node": "BookingComplete" if confirmed else "ServiceSelection",
152
+ "booking_info": {{"confirmation": true/false}},
153
+ "response": "Your confirmation message or start over message"
154
+ }}""",
155
+ "next_states": ["BookingComplete", "ServiceSelection"],
156
+ "required_info": ["confirmation"]
157
+ },
158
+ "BookingComplete": {
159
+ "prompt": """You are completing the booking.
160
+
161
+ Current message: "{user_input}"
162
+ Current booking info: {booking_info}
163
+
164
+ Rules:
165
+ 1. Confirm booking details
166
+ 2. Provide booking reference
167
+ 3. Thank the customer
168
+
169
+ Output format (JSON):
170
+ {{
171
+ "next_node": "Farewell",
172
+ "booking_info": {booking_info},
173
+ "response": "Your thank you message with booking details"
174
+ }}""",
175
+ "next_states": ["Farewell"],
176
+ "required_info": []
177
+ },
178
+ "Farewell": {
179
+ "prompt": """You are saying goodbye.
180
+
181
+ Current message: "{user_input}"
182
+ Current booking info: {booking_info}
183
+
184
+ Rules:
185
+ 1. Thank the customer
186
+ 2. Provide contact info if needed
187
+ 3. Say goodbye
188
+
189
+ Output format (JSON):
190
+ {{
191
+ "next_node": "Farewell",
192
+ "booking_info": {booking_info},
193
+ "response": "Your goodbye message"
194
+ }}""",
195
+ "next_states": [],
196
+ "required_info": []
197
+ }
198
+ }
config/services.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Service configuration for the barber booking system."""
2
+
3
+ SERVICE_MAPPING = {
4
+ "haircut": {
5
+ "price": 25,
6
+ "duration": 35,
7
+ "description": "Classic haircut service including wash and style"
8
+ },
9
+ "beard trim": {
10
+ "price": 15,
11
+ "duration": 25,
12
+ "description": "Professional beard trimming and shaping"
13
+ },
14
+ "full service": {
15
+ "price": 35,
16
+ "duration": 60,
17
+ "description": "Complete package including haircut, beard trim, and styling"
18
+ }
19
+ }
config/validation.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Validation utilities for the barber booking system."""
2
+
3
+ import re
4
+ from typing import Tuple, Dict, Optional
5
+
6
+ def validate_email(email: str) -> bool:
7
+ """Validate email format."""
8
+ pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
9
+ return bool(re.match(pattern, email))
10
+
11
+ def validate_phone(phone: str) -> bool:
12
+ """Validate phone number format."""
13
+ pattern = r'^\+?[1-9]\d{1,14}$'
14
+ return bool(re.match(pattern, phone))
15
+
16
+ def validate_name(name: str) -> bool:
17
+ """Validate customer name."""
18
+ words = name.strip().split()
19
+ return len(words) >= 1 and all(len(word) >= 2 for word in words)
20
+
21
+ def format_phone(phone: str) -> str:
22
+ """Format phone number to international format."""
23
+ if not phone.startswith('+'):
24
+ return f"+{phone.lstrip('0')}"
25
+ return phone
26
+
27
+ def validate_booking_info(current_node: str, booking_info: Dict) -> Tuple[bool, str]:
28
+ """Validate required booking information for the current node."""
29
+ if current_node == "ShowTopSlots" and not booking_info.get("name"):
30
+ return False, "I need your name first. What's your name?"
31
+ elif current_node == "CollectEmail" and not booking_info.get("service"):
32
+ return False, "Please select a service first."
33
+ elif current_node == "CollectPhone" and not booking_info.get("email"):
34
+ return False, "I need your email for the confirmation. What's your email address?"
35
+ elif current_node == "Confirmation" and not booking_info.get("phone"):
36
+ return False, "What's your phone number for appointment reminders?"
37
+ return True, ""
38
+
39
+ def get_closest_service(query: str) -> Optional[str]:
40
+ """Find the closest matching service based on the user's input."""
41
+ query = query.lower().strip()
42
+
43
+ # Direct matches
44
+ if query in ["haircut", "beard trim", "full service"]:
45
+ return query
46
+
47
+ # Handle variations and partial matches
48
+ if any(word in query for word in ["hair", "cut", "haircut"]):
49
+ return "haircut"
50
+ elif any(word in query for word in ["beard", "trim", "beardtrim"]):
51
+ return "beard trim"
52
+ elif any(word in query for word in ["full", "complete", "both", "all"]):
53
+ return "full service"
54
+
55
+ return None
improved_cal_booking.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from datetime import datetime, timedelta
3
+ import requests
4
+ import json
5
+ from typing import Dict, List, Optional, Union
6
+ from dotenv import load_dotenv
7
+
8
+ class ImprovedCalBookingSystem:
9
+ def __init__(self, api_key: Optional[str] = None, username: Optional[str] = None):
10
+ """Initialize the booking system with API credentials"""
11
+ # Load from environment if not provided
12
+ if api_key is None:
13
+ load_dotenv()
14
+ api_key = os.getenv('CAL_API_KEY')
15
+ if username is None:
16
+ load_dotenv()
17
+ username = os.getenv('CAL_USERNAME')
18
+
19
+ if not api_key or not username:
20
+ raise ValueError("CAL_API_KEY and CAL_USERNAME are required")
21
+
22
+ self.api_key = api_key
23
+ self.username = username
24
+ self.base_url = "https://api.cal.com/v1"
25
+ self.headers = {"Content-Type": "application/json"}
26
+ self.params = {"apiKey": self.api_key}
27
+
28
+ def get_event_types(self) -> List[Dict]:
29
+ """Get all available event types"""
30
+ try:
31
+ response = requests.get(
32
+ f"{self.base_url}/event-types",
33
+ headers=self.headers,
34
+ params=self.params
35
+ )
36
+ response.raise_for_status()
37
+ return response.json()
38
+ except requests.RequestException as e:
39
+ print(f"Error fetching event types: {e}")
40
+ if hasattr(e.response, 'text'):
41
+ print(f"Response: {e.response.text}")
42
+ return []
43
+
44
+ def get_available_slots(
45
+ self,
46
+ event_type_id: int,
47
+ date: Optional[datetime] = None,
48
+ days_ahead: int = 7
49
+ ) -> List[str]:
50
+ """Get available time slots for a specific event type"""
51
+ if date is None:
52
+ # Start from tomorrow to avoid "booking in the past" errors
53
+ date = datetime.now() + timedelta(days=1)
54
+ date = date.replace(hour=0, minute=0, second=0, microsecond=0)
55
+
56
+ try:
57
+ availability_params = {
58
+ **self.params,
59
+ "username": self.username,
60
+ "eventTypeId": event_type_id,
61
+ "dateFrom": date.strftime("%Y-%m-%d"),
62
+ "dateTo": (date + timedelta(days=days_ahead)).strftime("%Y-%m-%d"),
63
+ "duration": 30
64
+ }
65
+
66
+ response = requests.get(
67
+ f"{self.base_url}/availability",
68
+ headers=self.headers,
69
+ params=availability_params
70
+ )
71
+ response.raise_for_status()
72
+ availability_data = response.json()
73
+
74
+ # Generate available slots based on working hours
75
+ available_slots = []
76
+ date_ranges = availability_data.get("dateRanges", [])
77
+ busy_times = availability_data.get("busy", [])
78
+
79
+ # Convert busy times to datetime objects
80
+ busy_periods = []
81
+ for busy in busy_times:
82
+ start = datetime.fromisoformat(busy["start"].replace("Z", "+00:00"))
83
+ end = datetime.fromisoformat(busy["end"].replace("Z", "+00:00"))
84
+ busy_periods.append((start, end))
85
+
86
+ # Generate slots for each date range
87
+ for date_range in date_ranges:
88
+ range_start = datetime.fromisoformat(date_range["start"].replace("Z", "+00:00"))
89
+ range_end = datetime.fromisoformat(date_range["end"].replace("Z", "+00:00"))
90
+
91
+ current_slot = range_start
92
+ while current_slot + timedelta(minutes=30) <= range_end:
93
+ slot_end = current_slot + timedelta(minutes=30)
94
+
95
+ # Check if slot is available
96
+ is_available = True
97
+ for busy_start, busy_end in busy_periods:
98
+ if (current_slot >= busy_start and current_slot < busy_end) or \
99
+ (slot_end > busy_start and slot_end <= busy_end):
100
+ is_available = False
101
+ break
102
+
103
+ if is_available:
104
+ available_slots.append(current_slot.isoformat())
105
+
106
+ current_slot = slot_end
107
+
108
+ return available_slots
109
+
110
+ except requests.RequestException as e:
111
+ print(f"Error fetching available slots: {e}")
112
+ if hasattr(e.response, 'text'):
113
+ print(f"Response: {e.response.text}")
114
+ return []
115
+
116
+ def book_appointment(
117
+ self,
118
+ event_type_id: int,
119
+ customer: Dict[str, str],
120
+ start_time: str,
121
+ notes: str = "",
122
+ location: str = ""
123
+ ) -> Dict[str, Union[bool, Dict]]:
124
+ """Book an appointment with the provided details"""
125
+ try:
126
+ # Format the booking data according to Cal.com's API requirements
127
+ booking_data = {
128
+ "eventTypeId": event_type_id,
129
+ "start": start_time,
130
+ "email": customer["email"],
131
+ "name": customer["name"],
132
+ "timeZone": "UTC",
133
+ "language": "en",
134
+ "guests": [],
135
+ "notes": notes,
136
+ "location": location,
137
+ "smsReminderNumber": customer.get("phone", ""),
138
+ "rescheduleReason": "",
139
+ "customInputs": [],
140
+ "metadata": {} # Required field
141
+ }
142
+
143
+ response = requests.post(
144
+ f"{self.base_url}/bookings",
145
+ headers=self.headers,
146
+ params=self.params,
147
+ json=booking_data
148
+ )
149
+ response.raise_for_status()
150
+ return {"success": True, "booking": response.json()}
151
+
152
+ except requests.RequestException as e:
153
+ print(f"Error creating booking: {e}")
154
+ if hasattr(e.response, 'text'):
155
+ print(f"Response: {e.response.text}")
156
+ return {"success": False, "message": str(e)}
157
+
158
+ def get_booking_details(self, booking_id: str) -> Optional[Dict]:
159
+ """Get details of a specific booking"""
160
+ try:
161
+ response = requests.get(
162
+ f"{self.base_url}/bookings/{booking_id}",
163
+ headers=self.headers,
164
+ params=self.params
165
+ )
166
+ response.raise_for_status()
167
+ return response.json()
168
+ except requests.RequestException as e:
169
+ print(f"Error fetching booking details: {e}")
170
+ return None
171
+
172
+ def cancel_booking(self, booking_id: str, reason: str = "") -> bool:
173
+ """Cancel a booking"""
174
+ try:
175
+ response = requests.delete(
176
+ f"{self.base_url}/bookings/{booking_id}",
177
+ headers=self.headers,
178
+ params={**self.params, "reason": reason}
179
+ )
180
+ response.raise_for_status()
181
+ return True
182
+ except requests.RequestException as e:
183
+ print(f"Error canceling booking: {e}")
184
+ return False
185
+
186
+ def reschedule_booking(
187
+ self,
188
+ booking_id: str,
189
+ new_start_time: str,
190
+ reason: str = ""
191
+ ) -> Dict[str, Union[bool, Dict]]:
192
+ """Reschedule a booking to a new time"""
193
+ try:
194
+ reschedule_data = {
195
+ "start": new_start_time,
196
+ "reason": reason
197
+ }
198
+
199
+ response = requests.patch(
200
+ f"{self.base_url}/bookings/{booking_id}",
201
+ headers=self.headers,
202
+ params=self.params,
203
+ json=reschedule_data
204
+ )
205
+ response.raise_for_status()
206
+ return {"success": True, "booking": response.json()}
207
+ except requests.RequestException as e:
208
+ print(f"Error rescheduling booking: {e}")
209
+ return {"success": False, "message": str(e)}