File size: 11,593 Bytes
34917ad
 
 
 
 
 
 
 
 
96e7af4
 
 
 
 
34917ad
 
 
 
 
 
 
 
 
 
 
96e7af4
 
 
 
34917ad
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96e7af4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34917ad
96e7af4
 
34917ad
 
96e7af4
 
 
34917ad
96e7af4
 
 
 
 
 
 
 
 
 
 
 
34917ad
96e7af4
 
34917ad
96e7af4
 
34917ad
96e7af4
34917ad
96e7af4
 
 
34917ad
96e7af4
34917ad
96e7af4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
import os
import json
import re
import datetime
from google.oauth2 import service_account
from googleapiclient.discovery import build
import gradio as gr
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
from huggingface_hub import login

# Login to Hugging Face if token is provided (for accessing gated models)
if os.getenv("HF_TOKEN"):
    login(os.getenv("HF_TOKEN"))

# Google Calendar API setup with Service Account
SCOPES = ['https://www.googleapis.com/auth/calendar']
# Calendar ID - use your calendar ID here
CALENDAR_ID = os.getenv('CALENDAR_ID', '26f5856049fab3d6648a2f1dea57c70370de6bc1629a5182be1511b0e75d11d3@group.calendar.google.com')

# Load Llama 3.1 model 
MODEL_ID = "meta-llama/Llama-3.1-8B-Instruct"

def get_calendar_service():
    """Set up Google Calendar service using service account"""
    # Load service account info from environment
    service_account_info = json.loads(os.getenv('SERVICE_ACCOUNT_INFO', '{}'))
    credentials = service_account.Credentials.from_service_account_info(
        service_account_info, scopes=SCOPES)
    
    service = build('calendar', 'v3', credentials=credentials)
    return service

def format_time(time_str):
    """Format time input to ensure 24-hour format"""
    # Handle AM/PM format
    time_str = time_str.strip().upper()
    is_pm = 'PM' in time_str
    
    # Remove AM/PM
    time_str = time_str.replace('AM', '').replace('PM', '').strip()
    
    # Parse hours and minutes
    if ':' in time_str:
        parts = time_str.split(':')
        hours = int(parts[0])
        minutes = int(parts[1]) if len(parts) > 1 else 0
    else:
        hours = int(time_str)
        minutes = 0
    
    # Convert to 24-hour format if needed
    if is_pm and hours < 12:
        hours += 12
    elif not is_pm and hours == 12:
        hours = 0
    
    # Return formatted time
    return f"{hours:02d}:{minutes:02d}"

def add_event_to_calendar(name, date, time_str, duration_minutes=60):
    """Add an event to Google Calendar using Indian time zone"""
    service = get_calendar_service()
    
    # Format time properly
    formatted_time = format_time(time_str)
    print(f"Input time: {time_str}, Formatted time: {formatted_time}")
    
    # For debugging - show the date and time being used
    print(f"Using date: {date}, time: {formatted_time}")
    
    # Create event
    event = {
        'summary': f"Appointment with {name}",
        'description': f"Meeting with {name}",
        'start': {
            'dateTime': f"{date}T{formatted_time}:00",
            'timeZone': 'Asia/Kolkata',  # Indian Standard Time
        },
        'end': {
            'dateTime': f"{date}T{formatted_time}:00",  # Will add duration below
            'timeZone': 'Asia/Kolkata',  # Indian Standard Time
        },
    }
    
    # Calculate end time properly in the same time zone
    start_dt = datetime.datetime.fromisoformat(f"{date}T{formatted_time}:00")
    end_dt = start_dt + datetime.timedelta(minutes=duration_minutes)
    event['end']['dateTime'] = end_dt.isoformat()
    
    print(f"Event start: {event['start']['dateTime']} {event['start']['timeZone']}")
    print(f"Event end: {event['end']['dateTime']} {event['end']['timeZone']}")
    
    try:
        # Add to calendar with detailed error handling
        event = service.events().insert(calendarId=CALENDAR_ID, body=event).execute()
        print(f"Event created successfully: {event.get('htmlLink')}")
        # Return True instead of the link to indicate success
        return True
    except Exception as e:
        print(f"Error creating event: {str(e)}")
        print(f"Calendar ID: {CALENDAR_ID}")
        print(f"Event details: {json.dumps(event, indent=2)}")
        raise

def extract_function_call(text):
    """Extract function call parameters from Llama's response text"""
    # Look for JSON-like structure in the response
    json_pattern = r'```json\s*({.*?})\s*```'
    matches = re.findall(json_pattern, text, re.DOTALL)
    
    if matches:
        try:
            return json.loads(matches[0])
        except json.JSONDecodeError:
            pass
    
    # Try to find a pattern like {"name": "John", "date": "2025-05-10", "time": "14:30"}
    json_pattern = r'{.*?"name".*?:.*?"(.*?)".*?"date".*?:.*?"(.*?)".*?"time".*?:.*?"(.*?)".*?}'
    matches = re.findall(json_pattern, text, re.DOTALL)
    
    if matches and len(matches[0]) == 3:
        name, date, time = matches[0]
        return {"name": name, "date": date, "time": time}
    
    # If no JSON structure is found, try to extract individual fields
    name_match = re.search(r'name["\s:]+([^",]+)', text, re.IGNORECASE)
    date_match = re.search(r'date["\s:]+([^",]+)', text, re.IGNORECASE)
    time_match = re.search(r'time["\s:]+([^",]+)', text, re.IGNORECASE)
    
    result = {}
    if name_match:
        result["name"] = name_match.group(1).strip()
    if date_match:
        result["date"] = date_match.group(1).strip()
    if time_match:
        result["time"] = time_match.group(1).strip()
    
    return result if result else None

def process_with_llama(user_input, conversation_history, llm_pipeline):
    """Process user input with Llama 3.1 model, handling function calling"""
    try:
        # Build conversation context with function calling instructions
        function_description = """
        You have access to the following function:
        
        book_appointment
        Description: Book an appointment in Google Calendar
        Parameters:
        - name: string, Name of the person for the appointment
        - date: string, Date of appointment in YYYY-MM-DD format
        - time: string, Time of appointment (e.g., '2:30 PM', '14:30')
        
        When you need to book an appointment, output the function call in JSON format like this:
        ```json
        {"name": "John Doe", "date": "2025-05-10", "time": "14:30"}
        ```
        """
        
        # Create a prompt that includes conversation history and function description
        prompt = "You are an appointment booking assistant for Indian users. "
        prompt += "You help book appointments in Google Calendar using Indian Standard Time. "
        prompt += function_description
        
        # Add conversation history to the prompt
        for message in conversation_history:
            if message["role"] == "user":
                prompt += f"\n\nUser: {message['content']}"
            elif message["role"] == "assistant":
                prompt += f"\n\nAssistant: {message['content']}"
        
        # Add the current user message
        prompt += f"\n\nUser: {user_input}\n\nAssistant:"
        
        # Generate response from Llama
        response = llm_pipeline(prompt, max_new_tokens=1024, do_sample=True, temperature=0.1)
        llama_response = response[0]['generated_text'][len(prompt):].strip()
        
        # Check if Llama wants to call a function
        function_args = extract_function_call(llama_response)
        
        if function_args and "name" in function_args and "date" in function_args and "time" in function_args:
            print(f"Function arguments from Llama: {json.dumps(function_args, indent=2)}")
            
            # Add to calendar
            try:
                # Call the function but ignore the return value (we don't need the link)
                add_event_to_calendar(
                    function_args["name"], 
                    function_args["date"], 
                    function_args["time"]
                )
                
                # Construct a response that confirms booking but doesn't include a link
                final_response = f"Great! I've booked an appointment for {function_args['name']} on {function_args['date']} at {function_args['time']} (Indian Standard Time). The appointment has been added to your calendar."
                
            except Exception as e:
                final_response = f"I attempted to book an appointment, but encountered an error: {str(e)}"
                
            # Update conversation history
            conversation_history.append({"role": "user", "content": user_input})
            conversation_history.append({"role": "assistant", "content": final_response})
            
            return final_response, conversation_history
        else:
            # No function call detected, just return Llama's response
            conversation_history.append({"role": "user", "content": user_input})
            conversation_history.append({"role": "assistant", "content": llama_response})
            
            return llama_response, conversation_history
    
    except Exception as e:
        print(f"Error in process_with_llama: {str(e)}")
        return f"Error: {str(e)}", conversation_history

# System prompt for conversation
system_prompt = """You are an appointment booking assistant for Indian users.
When someone asks to book an appointment, collect:

1. Their name
2. The date (in YYYY-MM-DD format)
3. The time (in either 12-hour format like '2:30 PM' or 24-hour format like '14:30')

All appointments are in Indian Standard Time (IST).

If any information is missing, ask for it politely. Once you have all details, use the
book_appointment function to add it to the calendar.

IMPORTANT: After booking an appointment, simply confirm the details. Do not include
any links or mention viewing the appointment details. The user does not need to click
any links to view their appointment.

IMPORTANT: Make sure to interpret times correctly. If a user says '2 PM' or just '2',
this likely means 2:00 PM (14:00) in 24-hour format."""

# Initialize model and pipeline
def load_model_and_pipeline():
    model = AutoModelForCausalLM.from_pretrained(
        MODEL_ID,
        torch_dtype=torch.bfloat16,
        device_map="auto",
        low_cpu_mem_usage=True
    )
    
    tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
    
    # Create text generation pipeline
    llm_pipeline = pipeline(
        "text-generation",
        model=model,
        tokenizer=tokenizer,
        return_full_text=True,
        max_new_tokens=1024
    )
    
    return llm_pipeline

# Initialize conversation history with system prompt
conversation_history = [{"role": "system", "content": system_prompt}]

# Load model and pipeline at startup
llm_pipe = load_model_and_pipeline()

# Create Gradio interface
with gr.Blocks(title="Calendar Booking Assistant") as demo:
    gr.Markdown("# Indian Time Zone Appointment Booking with Llama 3.1")
    gr.Markdown("Say something like 'Book an appointment for John on May 10th at 2pm'")
    
    # Chat interface
    chatbot = gr.Chatbot()
    msg = gr.Textbox(placeholder="Type your message here...", label="Message")
    clear = gr.Button("Clear Chat")
    
    # State for conversation history
    state = gr.State(conversation_history)
    
    # Handle user input
    def user_input(message, history, conv_history):
        if message.strip() == "":
            return "", history, conv_history
        
        # Get response from Llama
        response, updated_conv_history = process_with_llama(message, conv_history, llm_pipe)
        
        # Update chat display
        history.append((message, response))
        
        return "", history, updated_conv_history
    
    # Connect components
    msg.submit(user_input, [msg, chatbot, state], [msg, chatbot, state])
    clear.click(lambda: ([], [{"role": "system", "content": system_prompt}]), None, [chatbot, state])

# Launch the app
if __name__ == "__main__":
    demo.launch()