from flask import Flask, render_template, request, jsonify, Response, stream_with_context from flask_cors import CORS import json import random import datetime from typing import Dict, List, Any, Optional from google import genai from google.genai import types from dotenv import load_dotenv import os import logging import sys # Configure logging for production logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.StreamHandler(sys.stdout) ] ) logger = logging.getLogger(__name__) # Load environment variables from .env file load_dotenv() app = Flask(__name__) CORS(app) # Disable Flask debug mode in production app.config['DEBUG'] = False app.config['PROPAGATE_EXCEPTIONS'] = True # Configure Gemini API # Configure Gemini API api_key = os.getenv('GEMINI_API_KEY', '').strip() if not api_key: logger.error("GEMINI_API_KEY not found in environment variables!") # Don't crash on startup - handle gracefully in routes client = None else: logger.info(f"API Key loaded successfully (length: {len(api_key)} characters)") try: client = genai.Client(api_key=api_key) logger.info("Gemini API configured successfully") except Exception as e: logger.error(f"Failed to configure Gemini API: {str(e)}") client = None # Global Data Storage APPOINTMENTS: List[Dict] = [] # Ultra-optimized System Prompt SYSTEM_PROMPT = '''Pare AI for interior/exterior. Products: INNOV+ (waterproof), INNOV2+ (3D walls), DURA+ (exterior), EASY+ (easy install), LUXE (premium), Acoustic (sound). Rules: Products only. English/Hindi. Hours: 9-18. JSON: {"type":"message","content":"text","language":"en"} Tools: book_appointment(name,address,date,time), cancel_appointment(id) Brief answers only.''' MODEL_NAME = "gemini-2.0-flash" GENERATION_CONFIG = types.GenerateContentConfig( temperature=0.2, top_p=0.8, top_k=5, max_output_tokens=100, ) # Tool Functions def validate_time_slot(time: str) -> tuple[bool, str]: """Validate if time slot is within business hours and in correct format""" try: # Parse time hour, minute = map(int, time.split(':')) # Check if minutes are 00 (only full hours allowed) if minute != 0: return False, "Appointments are only available on the hour (e.g., 9:00, 10:00, 11:00). Please choose a valid slot." # Check business hours (9 AM to 6 PM) if hour < 9 or hour > 18: return False, "Appointments are available only from 9:00 AM to 6:00 PM. Please choose a valid slot." return True, "Valid time slot" except: return False, "Invalid time format. Please use HH:MM format (e.g., 10:00, 14:00)." def book_appointment(name: str, address: str, date: str, time: str) -> Dict[str, Any]: """Book a new appointment with validation""" # Normalize date format to YYYY-MM-DD try: # Try parsing different date formats if '-' in date: parts = date.split('-') if len(parts[0]) == 4: # Already YYYY-MM-DD normalized_date = date else: # DD-MM-YYYY format normalized_date = f"{parts[2]}-{parts[1]}-{parts[0]}" else: normalized_date = date except: return { "status": "error", "message": "Invalid date format. Please use YYYY-MM-DD format." } # Validate time slot is_valid, message = validate_time_slot(time) if not is_valid: return { "status": "error", "message": message } # Check if slot is already booked for apt in APPOINTMENTS: if apt["date"] == normalized_date and apt["time"] == time: return { "status": "error", "message": f"Time slot {time} on {normalized_date} is already booked. Please choose another time." } # Create appointment apt_id = f"APT-{random.randint(1000, 9999)}" appointment_data = { "status": "confirmed", "appointment_id": apt_id, "name": name, "address": address, "date": normalized_date, # Store in YYYY-MM-DD format "time": time, "created_at": datetime.datetime.now().isoformat() } APPOINTMENTS.append(appointment_data) return appointment_data def cancel_appointment(appointment_id: str) -> Dict[str, Any]: """Cancel an existing appointment""" global APPOINTMENTS original_count = len(APPOINTMENTS) APPOINTMENTS = [a for a in APPOINTMENTS if a["appointment_id"] != appointment_id] if len(APPOINTMENTS) < original_count: return { "status": "cancelled", "appointment_id": appointment_id, "message": "Appointment successfully cancelled" } else: return { "status": "not_found", "appointment_id": appointment_id, "message": "Appointment not found" } def view_appointments() -> List[Dict[str, Any]]: """View all appointments""" return APPOINTMENTS def process_tool_call(response_text: str) -> Optional[Dict[str, Any]]: """Process tool calls from the AI response""" try: if response_text.startswith('```json') and response_text.endswith('```'): response_text = response_text.lstrip('```json').rstrip('```').strip() response_json = json.loads(response_text) if response_json.get("type") == "tool_call": function_name = response_json.get("name") args = response_json.get("args", {}) if function_name == "book_appointment": result = book_appointment( name=args.get("name"), address=args.get("address"), date=args.get("date"), time=args.get("time") ) # Check if booking failed due to validation if result.get("status") == "error": return { "type": "message", "content": result.get("message"), "language": "en" } return { "type": "appointment_result", "status": "confirmed", "details": result } elif function_name == "cancel_appointment": result = cancel_appointment( appointment_id=args.get("appointment_id") ) return { "type": "appointment_result", "status": result["status"], "details": result } return response_json except json.JSONDecodeError: return { "type": "error", "content": "Invalid response format", "raw_response": response_text } def chat(user_input: str) -> Dict[str, Any]: """Main chat function using new google-genai SDK""" if not client: return {"type": "error", "content": "API key not configured. Please set GEMINI_API_KEY.", "language": "en"} try: logger.info(f"Processing chat request: {user_input[:50]}...") full_prompt = f"{SYSTEM_PROMPT}\n\nUser: {user_input}\n\nAssistant:" response = client.models.generate_content( model=MODEL_NAME, contents=full_prompt, config=GENERATION_CONFIG, ) if not response.text: logger.warning("Empty response from Gemini") return { "type": "error", "content": "No response generated. Please try again.", "language": "en" } response_text = response.text.strip() processed_response = process_tool_call(response_text) logger.info("Chat request processed successfully") return processed_response except Exception as e: error_msg = str(e) logger.error(f"Chat error: {error_msg}") if "429" in error_msg or "quota" in error_msg.lower(): return {"type": "error", "content": "API quota exceeded. Please wait and try again.", "language": "en"} elif "timeout" in error_msg.lower() or "deadline" in error_msg.lower(): return {"type": "error", "content": "Response too slow. Try a shorter question.", "language": "en"} else: return {"type": "error", "content": f"Error: {error_msg}", "language": "en"} def chat_stream(user_input: str): """Streaming chat function using new google-genai SDK""" if not client: yield f"data: {json.dumps({'done': True, 'result': {'type': 'error', 'content': 'API key not configured. Please set GEMINI_API_KEY.', 'language': 'en'}})}\n\n" return try: logger.info(f"Processing streaming chat request: {user_input[:50]}...") full_prompt = f"{SYSTEM_PROMPT}\n\nUser: {user_input}\n\nAssistant:" accumulated_text = "" for chunk in client.models.generate_content_stream( model=MODEL_NAME, contents=full_prompt, config=GENERATION_CONFIG, ): if chunk.text: accumulated_text += chunk.text yield f"data: {json.dumps({'chunk': chunk.text})}\n\n" if accumulated_text: processed = process_tool_call(accumulated_text) yield f"data: {json.dumps({'done': True, 'result': processed})}\n\n" logger.info("Streaming chat request completed successfully") else: yield f"data: {json.dumps({'done': True, 'result': {'type': 'error', 'content': 'No response generated.', 'language': 'en'}})}\n\n" logger.warning("Streaming chat generated no response") except Exception as e: error_msg = str(e) logger.error(f"Streaming chat error: {error_msg}") if "429" in error_msg or "quota" in error_msg.lower(): content = "API quota exceeded. Please wait and try again." elif "timeout" in error_msg.lower() or "deadline" in error_msg.lower(): content = "Response too slow. Try a shorter question." else: content = f"Error: {error_msg}" yield f"data: {json.dumps({'done': True, 'result': {'type': 'error', 'content': content, 'language': 'en'}})}\n\n" # Routes @app.route('/') def index(): logger.info("Index page accessed") return render_template('index.html') @app.route('/health') def health_check(): """Health check endpoint""" return jsonify({ "status": "healthy", "service": "Pare AI Chatbot", "api_configured": client is not None, "timestamp": datetime.datetime.now().isoformat() }), 200 @app.route('/api/chat', methods=['POST']) def api_chat(): try: data = request.json user_message = data.get('message', '') use_streaming = data.get('stream', False) if not user_message: return jsonify({"type": "error", "content": "Message is required"}), 400 if use_streaming: return Response( stream_with_context(chat_stream(user_message)), mimetype='text/event-stream' ) else: response = chat(user_message) return jsonify(response) except Exception as e: logger.error(f"API chat error: {str(e)}") return jsonify({ "type": "error", "content": "An error occurred processing your request" }), 500 @app.route('/api/appointments', methods=['GET']) def api_appointments(): try: return jsonify({"appointments": view_appointments()}) except Exception as e: logger.error(f"API appointments error: {str(e)}") return jsonify({"error": "Failed to fetch appointments"}), 500 @app.route('/api/appointments/book', methods=['POST']) def api_book_appointment(): try: data = request.json result = book_appointment( name=data.get('name'), address=data.get('address'), date=data.get('date'), time=data.get('time') ) # Return appropriate status code if result.get('status') == 'error': return jsonify(result), 400 logger.info(f"Appointment booked: {result.get('appointment_id')}") return jsonify(result) except Exception as e: logger.error(f"API book appointment error: {str(e)}") return jsonify({"status": "error", "message": "Failed to book appointment"}), 500 @app.route('/api/appointments/cancel', methods=['POST']) def api_cancel_appointment(): try: data = request.json result = cancel_appointment(appointment_id=data.get('appointment_id')) logger.info(f"Appointment cancelled: {data.get('appointment_id')}") return jsonify(result) except Exception as e: logger.error(f"API cancel appointment error: {str(e)}") return jsonify({"status": "error", "message": "Failed to cancel appointment"}), 500 # Error handlers @app.errorhandler(404) def not_found(e): return jsonify({"error": "Resource not found"}), 404 @app.errorhandler(500) def internal_error(e): logger.error(f"Internal server error: {str(e)}") return jsonify({"error": "Internal server error"}), 500 if __name__ == '__main__': # Hugging Face Spaces requires port 7860 # Render uses $PORT env variable # Local dev defaults to 5000 port = int(os.getenv('PORT', 7860)) logger.info(f"Starting Pare AI Chatbot on port {port}") logger.info(f"Environment: {'Production' if not app.config['DEBUG'] else 'Development'}") app.run(host='0.0.0.0', port=port, debug=False)