ada-web

Ada AI is a specialized coding...
Log | Files | Refs | README | LICENSE

backend.py (20513B)


      1 import os
      2 import json
      3 import datetime
      4 from flask import Flask, request, jsonify, Response, stream_with_context
      5 from flask_cors import CORS
      6 import google.generativeai as genai
      7 import firebase_admin
      8 from firebase_admin import credentials, auth, firestore
      9 
     10 # --- INIT APP & CONFIG ---
     11 app = Flask(__name__)
     12 # Allow CORS for your domain and localhost for testing
     13 CORS(app, resources={r"/api/*": {"origins": [
     14     "https://ada.amit.is-a.dev", 
     15     "https://amit.is-a.dev", 
     16     "http://127.0.0.1:5500", 
     17     "http://localhost:5000",
     18     "http://localhost:5500"
     19 ]}})
     20 
     21 # 1. Firebase Admin Init (Server-Side Security)
     22 # Ensure you have your service account json or environment variables set up in Render
     23 try:
     24     if not firebase_admin._apps:
     25         # Check for FIREBASE_CREDENTIALS environment variable (JSON string)
     26         firebase_creds_json = os.environ.get("FIREBASE_CREDENTIALS")
     27         
     28         if firebase_creds_json:
     29             # Parse JSON string from environment variable
     30             cred_dict = json.loads(firebase_creds_json)
     31             cred = credentials.Certificate(cred_dict)
     32             print("Using Firebase credentials from FIREBASE_CREDENTIALS environment variable")
     33         elif os.path.exists("firebase-adminsdk.json"):
     34             cred = credentials.Certificate("firebase-adminsdk.json")
     35             print("Using Firebase credentials from firebase-adminsdk.json file")
     36         else:
     37             # Try application default credentials
     38             cred = credentials.ApplicationDefault()
     39             print("Using Firebase Application Default Credentials")
     40         
     41         firebase_admin.initialize_app(cred)
     42         print("Firebase Admin SDK initialized successfully")
     43     db = firestore.client()
     44     print("Firestore client initialized successfully")
     45 except Exception as e:
     46     print(f"Firebase initialization error: {type(e).__name__}: {e}")
     47     print("API will run but Firestore features will not work")
     48     db = None
     49 
     50 # 2. Gemini Init
     51 GENAI_API_KEY = os.environ.get("GEMINI_API_KEY")
     52 if not GENAI_API_KEY:
     53     print("CRITICAL: GEMINI_API_KEY not found in env variables.")
     54 else:
     55     genai.configure(api_key=GENAI_API_KEY)
     56 
     57 # Generation Config
     58 generation_config = {
     59     "temperature": 0.2, # Low temp for precise coding
     60     "top_p": 0.95,
     61     "top_k": 64,
     62     "max_output_tokens": 8192,
     63     "response_mime_type": "text/plain",
     64 }
     65 
     66 # System Instruction - THE STRICT CODING GATEKEEPER
     67 SYSTEM_PROMPT = """
     68 
     69 """
     70 
     71 model = genai.GenerativeModel(
     72     model_name="models/gemini-3.1-flash-lite-preview",
     73     generation_config=generation_config,
     74     system_instruction=SYSTEM_PROMPT,
     75 )
     76 
     77 # --- HELPER: Verify Firebase Token & Check Guest Status ---
     78 def verify_token(auth_header):
     79     """
     80     Verifies the Firebase ID Token sent from the client.
     81     Returns a dictionary {'uid': str, 'is_guest': bool} if valid, None otherwise.
     82     Enhanced to detect Anonymous (Guest) users.
     83     """
     84     if not auth_header:
     85         print("No Authorization header provided")
     86         return None
     87     
     88     if not auth_header.startswith("Bearer "):
     89         print("Invalid Authorization header format (must start with 'Bearer ')")
     90         return None
     91     
     92     token = auth_header.split("Bearer ")[1]
     93     
     94     try:
     95         decoded_token = auth.verify_id_token(token)
     96         user_id = decoded_token['uid']
     97         
     98         # Check the sign-in provider to see if it's an anonymous user
     99         firebase_claim = decoded_token.get('firebase', {})
    100         provider = firebase_claim.get('sign_in_provider')
    101         is_guest = (provider == 'anonymous')
    102         
    103         return {'uid': user_id, 'is_guest': is_guest}
    104 
    105     except auth.InvalidIdTokenError:
    106         print("Invalid ID token - token is malformed or invalid")
    107         return None
    108     except auth.ExpiredIdTokenError:
    109         print("Token has expired - user needs to refresh their token")
    110         return None
    111     except auth.RevokedIdTokenError:
    112         print("Token has been revoked")
    113         return None
    114     except Exception as e:
    115         print(f"Token verification error: {type(e).__name__}: {e}")
    116         return None
    117 
    118 def get_user_ref(user_data):
    119     """
    120     Returns the Firestore Document Reference for the user.
    121     Routes guests to 'temp-users' and registered users to 'users'.
    122     """
    123     if not db:
    124         raise Exception("Database unavailable")
    125     
    126     # TRAFFIC ROUTING LOGIC
    127     collection_name = 'temp-users' if user_data['is_guest'] else 'users'
    128     return db.collection(collection_name).document(user_data['uid'])
    129 
    130 # --- ROUTES ---
    131 
    132 @app.route('/')
    133 def home():
    134     return jsonify({
    135         "status": "online",
    136         "service": "Ada AI Coding Backend",
    137         "version": "1.1.0",
    138         "model": "gemini-3.1-flash-lite-preview"
    139     })
    140 
    141 @app.route('/health')
    142 def health():
    143     """Health check endpoint for UptimeRobot and monitoring services"""
    144     return jsonify({
    145         "status": "healthy",
    146         "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat()
    147     })
    148 
    149 @app.route('/api/generate-title', methods=['POST'])
    150 def generate_title():
    151     """Generates a short 3-5 word title for the chat history."""
    152     # Verify auth even for titles to prevent abuse
    153     user_data = verify_token(request.headers.get('Authorization'))
    154     if not user_data:
    155         return jsonify({
    156             "error": "Unauthorized",
    157             "message": "Invalid or missing authentication token. Please sign in again."
    158         }), 401
    159 
    160     data = request.json
    161     message = data.get('message', '')
    162     
    163     try:
    164         title_model = genai.GenerativeModel("models/gemini-3.1-flash-lite-preview")
    165         res = title_model.generate_content(f"Summarize this coding query into a 3-5 word title: '{message}'")
    166         return jsonify({"title": res.text.strip()})
    167     except Exception as e:
    168         print(f"Error generating title: {type(e).__name__}: {e}")
    169         return jsonify({"title": "New Chat"})
    170 
    171 @app.route('/api/profile', methods=['GET'])
    172 def get_profile():
    173     """Get user profile from Firestore."""
    174     user_data = verify_token(request.headers.get('Authorization'))
    175     if not user_data:
    176         return jsonify({"error": "Unauthorized"}), 401
    177     
    178     if not db:
    179         return jsonify({"error": "Database unavailable"}), 503
    180     
    181     try:
    182         # Use helper to get correct collection path
    183         user_ref = get_user_ref(user_data)
    184         doc = user_ref.get()
    185         
    186         if doc.exists:
    187             return jsonify(doc.to_dict())
    188         else:
    189             # Create default profile
    190             default_profile = {
    191                 "uid": user_data['uid'],
    192                 "isGuest": user_data['is_guest'],
    193                 "displayName": "Guest User" if user_data['is_guest'] else "",
    194                 "email": "",
    195                 "photoURL": "",
    196                 "age": "",
    197                 "location": "",
    198                 "bio": "",
    199                 "totalChats": 0,
    200                 "totalMessages": 0,
    201                 "totalCodeSnippets": 0,
    202                 "theme": "dark",
    203                 "codeTheme": "dracula",
    204                 "fontSize": 13,
    205                 "createdAt": datetime.datetime.now(datetime.timezone.utc)
    206             }
    207             user_ref.set(default_profile)
    208             return jsonify(default_profile)
    209     except Exception as e:
    210         print(f"Error getting profile: {type(e).__name__}: {e}")
    211         return jsonify({"error": "Failed to get profile"}), 500
    212 
    213 @app.route('/api/profile', methods=['PUT'])
    214 def update_profile():
    215     """Update user profile in Firestore."""
    216     user_data = verify_token(request.headers.get('Authorization'))
    217     if not user_data:
    218         return jsonify({"error": "Unauthorized"}), 401
    219     
    220     if not db:
    221         return jsonify({"error": "Database unavailable"}), 503
    222     
    223     try:
    224         data = request.json
    225         allowed_fields = ['displayName', 'age', 'location', 'bio', 'photoURL', 'theme', 'codeTheme', 'fontSize']
    226         update_data = {k: v for k, v in data.items() if k in allowed_fields}
    227         update_data['updatedAt'] = datetime.datetime.now(datetime.timezone.utc)
    228         
    229         user_ref = get_user_ref(user_data)
    230         user_ref.update(update_data)
    231         
    232         return jsonify({"success": True, "message": "Profile updated"})
    233     except Exception as e:
    234         print(f"Error updating profile: {type(e).__name__}: {e}")
    235         return jsonify({"error": "Failed to update profile"}), 500
    236 
    237 @app.route('/api/chats', methods=['GET'])
    238 def get_chats():
    239     """Get all user's chat sessions from Firestore."""
    240     user_data = verify_token(request.headers.get('Authorization'))
    241     if not user_data:
    242         return jsonify({"error": "Unauthorized"}), 401
    243     
    244     if not db:
    245         return jsonify({"error": "Database unavailable"}), 503
    246     
    247     try:
    248         user_ref = get_user_ref(user_data)
    249         chats_ref = user_ref.collection('chats')
    250         
    251         # Order by updatedAt descending (most recent first)
    252         docs = chats_ref.order_by('updatedAt', direction='DESCENDING').stream()
    253         
    254         chats = []
    255         for doc in docs:
    256             chat_data = doc.to_dict()
    257             chat_data['id'] = doc.id
    258             # Only send metadata, not full messages
    259             chats.append({
    260                 'id': chat_data.get('id'),
    261                 'title': chat_data.get('title', 'New Chat'),
    262                 'createdAt': chat_data.get('createdAt'),
    263                 'updatedAt': chat_data.get('updatedAt'),
    264                 'isPinned': chat_data.get('isPinned', False),
    265                 'messageCount': len(chat_data.get('messages', []))
    266             })
    267         
    268         return jsonify({"chats": chats})
    269     except Exception as e:
    270         print(f"Error getting chats: {type(e).__name__}: {e}")
    271         return jsonify({"error": "Failed to get chats"}), 500
    272 
    273 @app.route('/api/chats/<chat_id>', methods=['GET'])
    274 def get_chat(chat_id):
    275     """Get specific chat session."""
    276     user_data = verify_token(request.headers.get('Authorization'))
    277     if not user_data:
    278         return jsonify({"error": "Unauthorized"}), 401
    279     
    280     if not db:
    281         return jsonify({"error": "Database unavailable"}), 503
    282     
    283     try:
    284         user_ref = get_user_ref(user_data)
    285         chat_ref = user_ref.collection('chats').document(chat_id)
    286         doc = chat_ref.get()
    287         
    288         if not doc.exists:
    289             return jsonify({"error": "Chat not found"}), 404
    290         
    291         chat_data = doc.to_dict()
    292         chat_data['id'] = doc.id
    293         return jsonify(chat_data)
    294     except Exception as e:
    295         print(f"Error getting chat: {type(e).__name__}: {e}")
    296         return jsonify({"error": "Failed to get chat"}), 500
    297 
    298 @app.route('/api/chats/<chat_id>', methods=['DELETE'])
    299 def delete_chat(chat_id):
    300     """Delete a chat session."""
    301     user_data = verify_token(request.headers.get('Authorization'))
    302     if not user_data:
    303         return jsonify({"error": "Unauthorized"}), 401
    304     
    305     if not db:
    306         return jsonify({"error": "Database unavailable"}), 503
    307     
    308     try:
    309         user_ref = get_user_ref(user_data)
    310         chat_ref = user_ref.collection('chats').document(chat_id)
    311         chat_ref.delete()
    312         return jsonify({"success": True, "message": "Chat deleted"})
    313     except Exception as e:
    314         print(f"Error deleting chat: {type(e).__name__}: {e}")
    315         return jsonify({"error": "Failed to delete chat"}), 500
    316 
    317 @app.route('/api/chats/<chat_id>/rename', methods=['PUT'])
    318 def rename_chat(chat_id):
    319     """Rename a chat session."""
    320     user_data = verify_token(request.headers.get('Authorization'))
    321     if not user_data:
    322         return jsonify({"error": "Unauthorized"}), 401
    323     
    324     if not db:
    325         return jsonify({"error": "Database unavailable"}), 503
    326     
    327     try:
    328         data = request.json
    329         new_title = data.get('title', 'New Chat')
    330         
    331         user_ref = get_user_ref(user_data)
    332         chat_ref = user_ref.collection('chats').document(chat_id)
    333         chat_ref.update({
    334             'title': new_title,
    335             'updatedAt': datetime.datetime.now(datetime.timezone.utc)
    336         })
    337         
    338         return jsonify({"success": True, "message": "Chat renamed"})
    339     except Exception as e:
    340         print(f"Error renaming chat: {type(e).__name__}: {e}")
    341         return jsonify({"error": "Failed to rename chat"}), 500
    342 
    343 @app.route('/api/chats/<chat_id>/export', methods=['GET'])
    344 def export_chat(chat_id):
    345     """Export chat as JSON."""
    346     user_data = verify_token(request.headers.get('Authorization'))
    347     if not user_data:
    348         return jsonify({"error": "Unauthorized"}), 401
    349     
    350     if not db:
    351         return jsonify({"error": "Database unavailable"}), 503
    352     
    353     try:
    354         user_ref = get_user_ref(user_data)
    355         chat_ref = user_ref.collection('chats').document(chat_id)
    356         doc = chat_ref.get()
    357         
    358         if not doc.exists:
    359             return jsonify({"error": "Chat not found"}), 404
    360         
    361         chat_data = doc.to_dict()
    362         chat_data['id'] = doc.id
    363         
    364         # Convert timestamps to ISO format for JSON serialization
    365         if 'createdAt' in chat_data:
    366             chat_data['createdAt'] = chat_data['createdAt'].isoformat() if hasattr(chat_data['createdAt'], 'isoformat') else str(chat_data['createdAt'])
    367         if 'updatedAt' in chat_data:
    368             chat_data['updatedAt'] = chat_data['updatedAt'].isoformat() if hasattr(chat_data['updatedAt'], 'isoformat') else str(chat_data['updatedAt'])
    369         
    370         for msg in chat_data.get('messages', []):
    371             if 'timestamp' in msg:
    372                 msg['timestamp'] = msg['timestamp'].isoformat() if hasattr(msg['timestamp'], 'isoformat') else str(msg['timestamp'])
    373         
    374         return jsonify(chat_data)
    375     except Exception as e:
    376         print(f"Error exporting chat: {type(e).__name__}: {e}")
    377         return jsonify({"error": "Failed to export chat"}), 500
    378 
    379 @app.route('/api/chats/<chat_id>/pin', methods=['PUT'])
    380 def pin_chat(chat_id):
    381     """Toggle pin status of a chat."""
    382     user_data = verify_token(request.headers.get('Authorization'))
    383     if not user_data:
    384         return jsonify({"error": "Unauthorized"}), 401
    385     
    386     if not db:
    387         return jsonify({"error": "Database unavailable"}), 503
    388     
    389     try:
    390         data = request.json
    391         is_pinned = data.get('isPinned', False)
    392         
    393         user_ref = get_user_ref(user_data)
    394         chat_ref = user_ref.collection('chats').document(chat_id)
    395         chat_ref.update({
    396             'isPinned': is_pinned,
    397             'updatedAt': datetime.datetime.now(datetime.timezone.utc)
    398         })
    399         
    400         return jsonify({"success": True, "isPinned": is_pinned})
    401     except Exception as e:
    402         print(f"Error pinning chat: {type(e).__name__}: {e}")
    403         return jsonify({"error": "Failed to pin chat"}), 500
    404 
    405 @app.route('/api/chat', methods=['POST'])
    406 def chat():
    407     """
    408     Main Chat Endpoint (Streaming).
    409     Headers: Authorization: Bearer <firebase_id_token>
    410     Body: { "message": str, "history": list, "codeContext": str, "fileContext": str, "sessionId": str }
    411     """
    412     # 1. Verify User & Get Identity Info
    413     user_data = verify_token(request.headers.get('Authorization'))
    414     if not user_data:
    415         return jsonify({
    416             "error": "Unauthorized",
    417             "message": "Invalid or missing authentication token. Please sign in again."
    418         }), 401
    419 
    420     data = request.json
    421     user_msg = data.get('message')
    422     history = data.get('history', []) 
    423     code_ctx = data.get('codeContext', '') 
    424     file_ctx = data.get('fileContext', '') 
    425     session_id = data.get('sessionId')
    426 
    427     if not user_msg:
    428         return jsonify({
    429             "error": "Bad Request",
    430             "message": "Message cannot be empty"
    431         }), 400
    432 
    433     # 2. Construct Prompt with Context
    434     context_str = ""
    435     if code_ctx:
    436         context_str += f"\n\n[CURRENT EDITOR CONTENT]:\n{code_ctx}\n"
    437     if file_ctx:
    438         # Handle both string and object formats
    439         if isinstance(file_ctx, dict):
    440             file_name = file_ctx.get('name', 'uploaded_file')
    441             file_content = file_ctx.get('content', '')
    442             context_str += f"\n\n[UPLOADED FILE: {file_name}]:\n{file_content}\n"
    443         else:
    444             context_str += f"\n\n[UPLOADED FILE CONTENT]:\n{file_ctx}\n"
    445     
    446     # Add History (Gemini format)
    447     chat_history = []
    448     for turn in history:
    449         # Gemini expects 'user' and 'model' roles
    450         chat_history.append({"role": "user", "parts": [turn.get('user', '')]})
    451         chat_history.append({"role": "model", "parts": [turn.get('model', '')]})
    452     
    453     try:
    454         chat_session = model.start_chat(history=chat_history)
    455     except Exception as e:
    456         print(f"Error starting chat session: {type(e).__name__}: {e}")
    457         return jsonify({
    458             "error": "Internal Server Error",
    459             "message": "Failed to initialize chat session"
    460         }), 500
    461 
    462     # 3. Stream Response
    463     def generate():
    464         final_text_acc = ""
    465         
    466         prompt_with_ctx = user_msg + context_str
    467         
    468         try:
    469             response = chat_session.send_message(prompt_with_ctx, stream=True)
    470             
    471             for chunk in response:
    472                 if chunk.text:
    473                     yield chunk.text
    474                     final_text_acc += chunk.text
    475             
    476             # 4. Save Interaction to Firestore (Specific Path)
    477             # We save this asynchronously (conceptually) after streaming is done.
    478             if session_id and db:
    479                 try:
    480                     # Get correct reference (users vs temp-users)
    481                     user_ref = get_user_ref(user_data)
    482                     doc_ref = user_ref.collection('chats').document(session_id)
    483                     
    484                     msg_data = {
    485                         "role": "user",
    486                         "content": user_msg,
    487                         "timestamp": datetime.datetime.now(datetime.timezone.utc)
    488                     }
    489                     
    490                     # We store the raw AI response. The client parses the code blocks.
    491                     ai_data = {
    492                         "role": "model",
    493                         "content": final_text_acc, 
    494                         "timestamp": datetime.datetime.now(datetime.timezone.utc)
    495                     }
    496                     
    497                     doc = doc_ref.get()
    498                     if not doc.exists:
    499                         # First message - generate title
    500                         chat_title = "New Chat"
    501                         try:
    502                             title_model = genai.GenerativeModel("models/gemini-3.1-flash-lite-preview")
    503                             title_response = title_model.generate_content(f"Summarize this coding query into a 3-5 word title: '{user_msg}'")
    504                             chat_title = title_response.text.strip()
    505                         except Exception as title_err:
    506                             print(f"⚠️  Title generation failed: {title_err}")
    507                         
    508                         doc_ref.set({
    509                             "title": chat_title,
    510                             "createdAt": datetime.datetime.now(datetime.timezone.utc),
    511                             "updatedAt": datetime.datetime.now(datetime.timezone.utc),
    512                             "userId": user_data['uid'],
    513                             "isGuest": user_data['is_guest'], # Mark for easy identification
    514                             "isPinned": False,
    515                             "messages": [msg_data, ai_data]
    516                         })
    517                     else:
    518                         doc_ref.update({
    519                             "messages": firestore.ArrayUnion([msg_data, ai_data]),
    520                             "updatedAt": datetime.datetime.now(datetime.timezone.utc)
    521                         })
    522                     print(f"Chat saved to Firestore for session: {session_id} (Guest: {user_data['is_guest']})")
    523                 except Exception as db_err:
    524                     print(f"Database Save Error: {type(db_err).__name__}: {db_err}")
    525 
    526         except Exception as e:
    527             error_msg = f"Gemini API Error: {type(e).__name__}: {str(e)}"
    528             print(error_msg)
    529             yield f"\n\nError: I encountered an issue while processing your request. Please try again."
    530 
    531     return Response(stream_with_context(generate()), mimetype='text/plain')
    532 
    533 if __name__ == '__main__':
    534     port = int(os.environ.get('PORT', 5000))
    535     # Use debug=False in production (Render)
    536     # Gunicorn will be used for production: gunicorn backend:app
    537     debug_mode = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
    538     app.run(host='0.0.0.0', port=port, debug=debug_mode)
© notamitgamer • Site Built: 2026-07-21 13:58:23 UTC • git-mirror commit: 1037f62 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror