# import eventlet # eventlet.monkey_patch() import gevent from gevent import monkey monkey.patch_all() import json import yaml import os import random import boto3 import requests import together # Unsandbox SDK for code execution import un from flask import ( Flask, render_template, request, send_from_directory, jsonify, Response, redirect, url_for, session, ) from flask_socketio import SocketIO, emit, join_room, leave_room from flask_sqlalchemy import SQLAlchemy from sqlalchemy.exc import InvalidRequestError from models import db, Room, UserSession, Message, ActivityState, User, OTPToken app = Flask(__name__, instance_relative_config=True) app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-key-change-in-production") app.config["SQLALCHEMY_DATABASE_URI"] = ( f"sqlite:///{os.path.join(app.instance_path, 'chat.db')}" ) app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False # Enable template auto-reload to prevent stale templates during development app.config["TEMPLATES_AUTO_RELOAD"] = True db.init_app(app) from flask_migrate import Migrate migrate = Migrate(app, db) # socketio = SocketIO(app, async_mode="eventlet") socketio = SocketIO(app, async_mode="gevent") # Global dictionary to keep track of cancellation requests cancellation_requests = {} from openai import OpenAI import activity import auth # Build a list of endpoints dynamically. ENDPOINTS = [] MAX_ENDPOINTS = 1000 for i in range(MAX_ENDPOINTS): endpoint = os.environ.get(f"MODEL_ENDPOINT_{i}") if not endpoint: continue # API key is optional; if not provided, use a default. api_key = os.environ.get(f"MODEL_API_KEY_{i}", "not-needed") ENDPOINTS.append( { "base_url": endpoint, "api_key": api_key, } ) if not ENDPOINTS: raise Exception("No MODEL_ENDPOINT_x environment variables found!") # Build a dynamic model map by querying each endpoint. MODEL_CLIENT_MAP = {} SYSTEM_USERS = [] def get_client_for_endpoint(endpoint, api_key): # All providers use the OpenAI client; no endpoint URLs are hardcoded here. return OpenAI(api_key=api_key, base_url=endpoint) VISION_MODELS = [] # Populated at startup with available vision models def is_vision_model(model_name: str) -> bool: """Check if a model supports vision/image input.""" if not model_name: return False model_lower = model_name.lower() vision_indicators = ["-vl", "vl:", "vision", "gpt-4o", "gpt-4-turbo"] return any(indicator in model_lower for indicator in vision_indicators) def initialize_model_map(): global SYSTEM_USERS MODEL_CLIENT_MAP.clear() VISION_MODELS.clear() for ep_config in ENDPOINTS: base_url = ep_config["base_url"] api_key = ep_config["api_key"] client = get_client_for_endpoint(base_url, api_key) try: response = client.models.list() model_list = response.data # Assume each model object has an 'id' attribute print(f"[DEBUG] {base_url} returned models: {[m.id for m in model_list]}") except Exception as e: print(f"[WARN] Could not list models for endpoint '{base_url}': {e}") continue for m in model_list: model_id = m.id if model_id and model_id not in MODEL_CLIENT_MAP: MODEL_CLIENT_MAP[model_id] = (client, base_url) # Populate SYSTEM_USERS with dynamically loaded models. SYSTEM_USERS = list(MODEL_CLIENT_MAP.keys()) + ["system"] # Include "system" for fetched images print("Loaded models:", list(MODEL_CLIENT_MAP.keys())) # Detect and track available vision models for model_id in MODEL_CLIENT_MAP.keys(): if is_vision_model(model_id): VISION_MODELS.append(model_id) if VISION_MODELS: print(f"Vision models available: {VISION_MODELS}") else: print("No vision models available") if MODEL_CLIENT_MAP: pass else: initialize_model_map() # 4) Lookup function: get an OpenAI client for a given model name def get_client_for_model(model_name: str): """ If the model name is known, return its dedicated client. Otherwise, return None and LOL at the user when everything breaks. """ if model_name in MODEL_CLIENT_MAP: print(f"Completion Endpoint Processing: {MODEL_CLIENT_MAP[model_name][1]}") return MODEL_CLIENT_MAP[model_name][0] def extract_base64_from_img_tag(content: str) -> tuple[str, str] | None: """Extract base64 data and media type from an HTML img tag. Returns (media_type, base64_data) or None if not found. """ import re # Match data:image/TYPE;base64,DATA patterns in img src pattern = r']*src="data:image/(jpeg|png|gif|webp);base64,([^"]+)"' match = re.search(pattern, content) if match: media_type = f"image/{match.group(1)}" base64_data = match.group(2) return (media_type, base64_data) return None def extract_external_image_url(content: str) -> str | None: """Extract external image URL from an HTML img tag. Returns the URL or None if not found. """ import re # Match external URLs in img src (http/https) pattern = r']*src="(https?://[^"]+)"' match = re.search(pattern, content) if match: return match.group(1) return None # Cache for fetched external images (URL -> base64 data URL) _external_image_cache = {} # CORS proxy for ethical fetching (respects robots.txt) CORS_PROXY_URL = "https://cors-proxy.uncloseai.com/api/fetch" def escape_like_pattern(s: str) -> str: """Escape special characters for SQL LIKE patterns.""" # Escape %, _, and \ which have special meaning in LIKE return s.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_') def find_saved_base64_for_url(external_url: str, room_id: int) -> str | None: """Look up if we've already saved a base64 version of this URL in this room. Returns the data URL if found, None otherwise. """ # Check in-memory cache first if external_url in _external_image_cache: return _external_image_cache[external_url] # Look for a saved message with this URL in the alt text try: # Escape special LIKE characters in URL (%, _, \) escaped_url = escape_like_pattern(external_url) # Search for messages containing "Fetched from {external_url}" saved_msg = Message.query.filter( Message.room_id == room_id, Message.content.like(f'%alt="Fetched from {escaped_url}"%', escape='\\') ).first() if saved_msg: # Extract base64 from the saved message img_data = extract_base64_from_img_tag(saved_msg.content) if img_data: media_type, base64_data = img_data data_url = f"data:{media_type};base64,{base64_data}" # Add to memory cache for faster lookups _external_image_cache[external_url] = data_url print(f"Found saved base64 for {external_url} in message {saved_msg.id}") return data_url except Exception as e: print(f"Error looking up saved base64: {e}") return None def fetch_external_image_as_base64(image_url: str) -> str | None: """Fetch external image via CORS proxy and convert to base64. Returns data URL (data:image/...;base64,...) or None on failure. """ import base64 import httpx # Check cache first if image_url in _external_image_cache: return _external_image_cache[image_url] try: proxy_url = f"{CORS_PROXY_URL}?uri_target={image_url}" with httpx.Client(timeout=15.0) as client: response = client.get(proxy_url) if response.status_code == 403: print(f"Image blocked by robots.txt: {image_url}") return None response.raise_for_status() # Check content type content_type = response.headers.get("content-type", "") if not content_type.startswith("image/"): print(f"Not an image: {image_url} ({content_type})") return None # Convert to base64 b64_data = base64.b64encode(response.content).decode("utf-8") media_type = content_type.split(";")[0] # Remove charset if present data_url = f"data:{media_type};base64,{b64_data}" # Cache it _external_image_cache[image_url] = data_url return data_url except Exception as e: print(f"Failed to fetch external image {image_url}: {e}") return None def save_fetched_image_as_message(external_url: str, data_url: str, room_id: int) -> None: """Save a fetched external image as a new message in the database. This persists the base64 version so we don't need to fetch again. Only saves if no saved version exists for this URL in this room. """ print(f"[Vision Save] Attempting to save base64 for room {room_id}: {external_url[:60]}...") try: # Check if already saved for this URL in this room # Escape special LIKE characters in URL (%, _, \) escaped_url = escape_like_pattern(external_url) existing = Message.query.filter( Message.room_id == room_id, Message.content.like(f'%alt="Fetched from {escaped_url}"%', escape='\\') ).first() if existing: print(f"[Vision Save] Already exists as message {existing.id}") return # Create img tag with base64 data img_content = f'Fetched from {external_url}' new_message = Message( username="system", # Mark as system message content=img_content, room_id=room_id ) db.session.add(new_message) db.session.commit() print(f"[Vision Save] SUCCESS - Saved as message {new_message.id}") # Also cache it in memory _external_image_cache[external_url] = data_url except Exception as e: print(f"[Vision Save] FAILED: {e}") import traceback traceback.print_exc() db.session.rollback() def build_message_content(msg, is_vision: bool, room_id: int = None) -> dict | str: """Build message content, handling images for vision models. For vision models with images, returns multimodal content array. Otherwise returns plain text content. If room_id is provided and an external image is fetched, saves the base64 version as a new message for future use. """ if not is_vision: return msg.content # Check if this message contains a base64 image img_data = extract_base64_from_img_tag(msg.content) if img_data: media_type, base64_data = img_data # Return multimodal content with image return [ {"type": "image_url", "image_url": {"url": f"data:{media_type};base64,{base64_data}"}} ] # Check for external image URL external_url = extract_external_image_url(msg.content) if external_url: print(f"[Vision] Found external URL in message {msg.id}: {external_url[:80]}...") # First check if we already have a saved base64 version if room_id is not None: saved_data_url = find_saved_base64_for_url(external_url, room_id) if saved_data_url: print(f"[Vision] Using saved base64 for {external_url[:50]}...") return [ {"type": "image_url", "image_url": {"url": saved_data_url}} ] # Not saved yet - fetch and save print(f"[Vision] Fetching external image: {external_url[:80]}...") data_url = fetch_external_image_as_base64(external_url) if data_url: print(f"[Vision] Fetched successfully, saving to room {room_id}...") # Save the fetched image as a new message for persistence if room_id is not None: save_fetched_image_as_message(external_url, data_url, room_id) return [ {"type": "image_url", "image_url": {"url": data_url}} ] else: print(f"[Vision] Failed to fetch external image") # Plain text message return msg.content def extract_first_image_for_og(room_id: int) -> str | None: """Extract the first image URL from messages for Open Graph meta tags. Checks messages in the room for: 1. Base64 images (returns as data URL - may be large) 2. External image URLs in markdown format ![alt](url) 3. External image URLs in img tags Returns image URL/data URL or None if no image found. """ import re messages = Message.query.filter_by(room_id=room_id).order_by(Message.id.asc()).limit(50).all() for msg in messages: if not msg.content: continue # Check for base64 image if msg.is_base64_image(): img_data = extract_base64_from_img_tag(msg.content) if img_data: media_type, base64_data = img_data return f"data:{media_type};base64,{base64_data}" # Check for markdown image ![alt](url) md_img_match = re.search(r'!\[[^\]]*\]\(([^)]+)\)', msg.content) if md_img_match: url = md_img_match.group(1) if url.startswith(('http://', 'https://')): return url # Check for img tag with src img_src_match = re.search(r']+src=["\']([^"\']+)["\']', msg.content) if img_src_match: url = img_src_match.group(1) if url.startswith(('http://', 'https://')): return url return None def generate_og_description(room, max_chars: int = 500) -> str: """Generate a description for Open Graph meta tags. Uses room title if available, then extracts text from first few messages. Strips markdown/code/images and limits to max_chars. """ import re parts = [] # Start with room title if available if room and room.title: parts.append(room.title) # Get first few messages for description if room: messages = Message.query.filter_by(room_id=room.id).order_by(Message.id.asc()).limit(10).all() for msg in messages: if not msg.content: continue # Skip base64 images if msg.is_base64_image(): continue text = msg.content # Remove code blocks text = re.sub(r'```[\s\S]*?```', '', text) text = re.sub(r'`[^`]+`', '', text) # Remove markdown images text = re.sub(r'!\[[^\]]*\]\([^)]+\)', '', text) # Remove HTML tags text = re.sub(r'<[^>]+>', '', text) # Remove markdown links but keep text text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text) # Remove markdown headers text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE) # Remove bold/italic markers text = re.sub(r'\*{1,2}([^*]+)\*{1,2}', r'\1', text) text = re.sub(r'_{1,2}([^_]+)_{1,2}', r'\1', text) # Clean up whitespace text = re.sub(r'\s+', ' ', text).strip() if text: parts.append(text) # Stop if we have enough content if len(' '.join(parts)) > max_chars: break description = ' '.join(parts) # Truncate to max_chars if len(description) > max_chars: description = description[:max_chars - 3].rsplit(' ', 1)[0] + '...' return description if description else "AI-powered chat room on OpenCompletion" def get_openai_client_and_model( model_name="adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic", ): """Get OpenAI client and model name. Supports MODEL_X references (e.g., MODEL_1, MODEL_2, MODEL_3) that map to environment variables MODEL_ENDPOINT_X and MODEL_API_KEY_X. """ # Handle MODEL_X references if model_name and model_name.startswith("MODEL_"): try: model_num = model_name.split("_")[1] endpoint_key = f"MODEL_ENDPOINT_{model_num}" api_key_key = f"MODEL_API_KEY_{model_num}" endpoint = os.environ.get(endpoint_key) api_key = os.environ.get(api_key_key) if endpoint and api_key: client = get_client_for_endpoint(endpoint, api_key) # Look up actual model name from MODEL_CLIENT_MAP for this endpoint actual_model = None for model_id, (registered_client, base_url) in MODEL_CLIENT_MAP.items(): if base_url == endpoint: actual_model = model_id break if actual_model: return client, actual_model else: # Fallback: query endpoint for models if not in map yet try: response = client.models.list() if response.data: actual_model = response.data[0].id print( f"[DEBUG] Using first model from {endpoint}: {actual_model}" ) return client, actual_model except Exception as e: print(f"Warning: Could not query models from {endpoint}: {e}") # Final fallback print( f"Warning: No models found for {endpoint}, using 'model' as fallback" ) return client, "model" else: print( f"Warning: MODEL_{model_num} not configured ({endpoint_key} or {api_key_key} missing)" ) # Fall back to default model model_name = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic" except Exception as e: print(f"Warning: Failed to load {model_name}: {e}, falling back to default") model_name = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic" return get_client_for_model(model_name), model_name HELP_MESSAGE = """ **Available Commands:** - `/activity [s3_file_path]`: Start an activity from the specified S3 file path. - `/activity cancel`: Cancel the current activity. - `/activity info`: Display information about the current activity. - `/activity metadata`: Display metadata for the current activity. - `/s3 ls [s3_file_path_pattern]`: List files in S3 matching the pattern. - `/s3 load [s3_file_path]`: Load a file from S3. - `/s3 save [s3_key_path]`: Save the most recent code block from the chatroom to S3. - `/title new`: Generates a new title which reflects conversation content for the current chatroom. - `/cancel`: Cancel the most recent chat completion from streaming into the chatroom. - `/help`: Display this help message. **Interacting with AI Models:** - Select a model from the dropdown menu above the chat input. Available models are dynamically loaded from configured endpoints and include options like `gpt-4o-mini`, `llama3-70b-8192`, `anthropic.claude-3-sonnet-20240229-v1:0`, and `dall-e-3` for image generation. - Type your message and send it. The selected model will respond if it's not "None". - For image generation, select `dall-e-3` and provide a prompt (e.g., "A futuristic cityscape"). **Getting Started:** Welcome to the chatroom! Here, you can explore various AI models and engage in interactive activities. Here's how you can get started: 1. **Explore the Chatroom:** - Join a chatroom by navigating to its unique URL. You can see the list of available chatrooms on the main page. - Once inside, you can start a conversation by typing your message in the chatbox. 2. **Start an Activity:** - To begin an educational activity, use the `/activity` command followed by the path to the activity YAML file. For example: **Getting Started:** Welcome to the chatroom! Here, you can explore various AI models and engage in interactive activities. Here's how you can get started: 1. **Explore the Chatroom:** - Join a chatroom by navigating to its unique URL. You can see the list of available chatrooms on the main page. - Once inside, you can start a conversation by typing your message in the chatbox. 2. **Start an Activity:** - To begin an educational activity, use the `/activity` command followed by the path to the activity YAML file. For example: ``` /activity research/activity0.yaml ``` - The AI will guide you through the activity, providing feedback and information as you progress. 3. **Interact with AI Models:** - To interact with a specific AI model, simply type the model's command followed by your prompt. For example: ``` gpt-4 What is the capital of France? ``` - The system will process your message and provide a response from the selected model. 4. **Manage Files with S3:** - Use the `/s3` commands to load, save, or list files in your S3 bucket. For example, to list all files, use: ``` /s3 ls * ``` 5. **Get Help:** - If you need assistance or want to see a list of available commands, type `/help` to display this message. Feel free to explore and experiment with different commands and models. Enjoy your time in the chatroom! """ def get_room(room_name): """Utility function to get room from room name.""" room = Room.query.filter_by(name=room_name).first() if room: return room else: # Create a new room since it doesn't exist new_room = Room() new_room.name = room_name db.session.add(new_room) db.session.commit() return new_room def get_s3_client(): """Utility function to get the S3 client with the appropriate profile.""" if app.config.get("PROFILE_NAME"): session = boto3.Session(profile_name=app.config["PROFILE_NAME"]) s3_client = session.client("s3") else: s3_client = boto3.client("s3") return s3_client # Initialize activity module after socketio and db are configured activity.init_activity_module( app, socketio, db, { "get_room": get_room, "get_s3_client": get_s3_client, "get_openai_client_and_model": get_openai_client_and_model, "SYSTEM_USERS": SYSTEM_USERS, }, ) @app.route("/favicon.ico") def favicon(): return send_from_directory(os.path.join(app.root_path, "static"), "favicon.ico") @app.route("/") def index(): # Get statistics for homepage total_public_rooms = Room.query.filter_by(is_private=False, is_archived=False).count() total_private_rooms = 0 user = auth.get_current_user() if user: total_private_rooms = Room.query.filter_by(is_private=True, is_archived=False, owner_id=user.id).count() # Active rooms - rooms with at least one message active_public_rooms = db.session.query(Room.id).join(Message).filter( Room.is_private == False, Room.is_archived == False ).distinct().count() # Total active users (from UserSession) active_users = UserSession.query.distinct(UserSession.username).count() stats = { 'total_public_rooms': total_public_rooms, 'total_private_rooms': total_private_rooms, 'active_public_rooms': active_public_rooms, 'active_users': active_users, } return render_template("index.html", stats=stats, user=user) @app.route("/auth") def auth_page(): """Authentication page""" return render_template("auth.html") @app.route("/browse") def browse_rooms(): """Browse all rooms (public and user's private rooms)""" user = auth.get_current_user() # Get public rooms ordered by last updated public_rooms = Room.query.filter_by( is_private=False, is_archived=False ).order_by(Room.updated_at.desc()).all() # Get user's private rooms if authenticated private_rooms = [] if user: private_rooms = Room.query.filter_by( is_private=True, is_archived=False, owner_id=user.id ).order_by(Room.updated_at.desc()).all() return render_template( "browse.html", public_rooms=public_rooms, private_rooms=private_rooms, user=user ) @app.route("/models", methods=["GET"]) def get_models(): # Optionally refresh or reinitialize the model map here. # For now we simply return the keys. return jsonify({"models": list(MODEL_CLIENT_MAP.keys())}) @app.route("/vision", methods=["GET"]) def get_vision_status(): """Return vision model availability status.""" return jsonify({ "available": len(VISION_MODELS) > 0, "models": VISION_MODELS, "default": VISION_MODELS[0] if VISION_MODELS else None }) @app.route("/vision/describe", methods=["POST"]) def describe_image(): """Generate alt text description for an image using vision model.""" if not VISION_MODELS: return jsonify({"error": "No vision models available"}), 503 data = request.get_json() if not data or "image" not in data: return jsonify({"error": "Missing 'image' field (base64 data URL)"}), 400 image_url = data["image"] # Expected format: data:image/jpeg;base64,... prompt = data.get("prompt", "Describe this image in one brief sentence for use as alt text.") model_name = data.get("model", VISION_MODELS[0]) if model_name not in VISION_MODELS: return jsonify({"error": f"Model {model_name} is not a vision model"}), 400 try: client = get_client_for_model(model_name) response = client.chat.completions.create( model=model_name, messages=[{ "role": "user", "content": [ {"type": "text", "text": prompt}, {"type": "image_url", "image_url": {"url": image_url}} ] }], max_tokens=150, temperature=0.3 ) description = response.choices[0].message.content.strip() return jsonify({"description": description, "model": model_name}) except Exception as e: return jsonify({"error": str(e)}), 500 # Authentication endpoints @app.route("/auth/send-otp", methods=["POST"]) def send_otp(): """Send OTP to user's email""" data = request.get_json() email = data.get('email', '').strip().lower() if not email: return jsonify({'error': 'Email is required'}), 400 # Basic email validation if '@' not in email or '.' not in email.split('@')[1]: return jsonify({'error': 'Invalid email address'}), 400 # Create OTP token otp_token = auth.create_otp_token(email) # Send OTP via email if auth.send_otp_email(email, otp_token.otp_code): return jsonify({ 'success': True, 'message': 'OTP sent to your email', 'email': email }) else: return jsonify({'error': 'Failed to send OTP email'}), 500 @app.route("/auth/verify-otp", methods=["POST"]) def verify_otp(): """Verify OTP code and check if user exists""" data = request.get_json() email = data.get('email', '').strip().lower() otp_code = data.get('otp_code', '').strip() if not email or not otp_code: return jsonify({'error': 'Email and OTP code are required'}), 400 # Verify OTP otp_token = auth.verify_otp(email, otp_code) if not otp_token: return jsonify({'error': 'Invalid or expired OTP code'}), 400 # Check if user exists user = auth.get_or_create_user(email) if user: # Existing user - log them in auth.login_user(user) return jsonify({ 'success': True, 'needs_display_name': False, 'user': { 'email': user.email, 'display_name': user.display_name } }) else: # New user - needs to claim display name # Store email in session temporarily session['pending_email'] = email return jsonify({ 'success': True, 'needs_display_name': True, 'email': email }) @app.route("/auth/claim-name", methods=["POST"]) def claim_name(): """Claim display name for new user (after OTP verification)""" data = request.get_json() display_name = data.get('display_name', '').strip() email = session.get('pending_email') if not email: return jsonify({'error': 'No pending email verification'}), 400 if not display_name: return jsonify({'error': 'Display name is required'}), 400 # Validate display name (alphanumeric, underscores, hyphens only, 3-50 chars) import re if not re.match(r'^[a-zA-Z0-9_-]{3,50}$', display_name): return jsonify({ 'error': 'Display name must be 3-50 characters (letters, numbers, underscores, hyphens only)' }), 400 # Create user user, error = auth.create_user(email, display_name) if error: return jsonify({'error': error}), 400 # Log in user auth.login_user(user) # Clear pending email session.pop('pending_email', None) return jsonify({ 'success': True, 'user': { 'email': user.email, 'display_name': user.display_name } }) @app.route("/auth/status", methods=["GET"]) def auth_status(): """Get current authentication status""" user = auth.get_current_user() if user: return jsonify({ 'authenticated': True, 'user': { 'email': user.email, 'display_name': user.display_name } }) else: return jsonify({'authenticated': False}) @app.route("/auth/logout", methods=["POST"]) def logout(): """Log out current user""" auth.logout_user() return jsonify({'success': True}) @app.route("/profile") @auth.require_auth def profile_page(): """Profile settings page""" user = auth.get_current_user() return render_template("profile.html", user=user) @app.route("/api/check-username", methods=["GET"]) def check_username(): """Check if username is available""" username = request.args.get('username', '').strip() if not username: return jsonify({'available': False, 'error': 'Username is required'}), 400 # Validate format import re if not re.match(r'^[a-zA-Z0-9_-]{3,50}$', username): return jsonify({'available': False, 'error': 'Invalid format'}), 400 # Check if username exists existing_user = User.query.filter_by(display_name=username).first() return jsonify({'available': existing_user is None}) @app.route("/api/update-username", methods=["POST"]) @auth.require_auth def update_username(): """Update user's display name""" user = auth.get_current_user() data = request.get_json() new_username = data.get('new_username', '').strip() if not new_username: return jsonify({'error': 'Username is required'}), 400 # Validate format import re if not re.match(r'^[a-zA-Z0-9_-]{3,50}$', new_username): return jsonify({ 'error': 'Username must be 3-50 characters (letters, numbers, underscores, hyphens only)' }), 400 # Check if username is already taken existing_user = User.query.filter_by(display_name=new_username).first() if existing_user and existing_user.id != user.id: return jsonify({'error': 'Username is already taken'}), 400 # Update username user.display_name = new_username db.session.commit() return jsonify({ 'success': True, 'user': { 'email': user.email, 'display_name': user.display_name } }) @app.route("/api/activities", methods=["GET"]) def get_activities(): """Return the list of available activities.""" activities = [] if app.config.get("LOCAL_ACTIVITIES"): # List local activity files from research directory import os research_dir = "research" if os.path.exists(research_dir): for filename in sorted(os.listdir(research_dir)): if filename.endswith((".yaml", ".yml")): activities.append(f"research/{filename}") else: # For S3 activities, you would list from S3 # This is a placeholder - you'd need to implement S3 listing pass return jsonify({"activities": activities}) @app.route("/api/rooms", methods=["GET"]) def get_rooms_api(): """Get list of rooms (public or user's private rooms)""" user = auth.get_current_user() # Get public rooms public_rooms = Room.query.filter_by(is_private=False, is_archived=False).order_by(Room.id.desc()).all() # Get private rooms if authenticated private_rooms = [] if user: private_rooms = Room.query.filter_by(is_private=True, is_archived=False, owner_id=user.id).order_by(Room.id.desc()).all() return jsonify({ 'public_rooms': [{ 'id': r.id, 'name': r.name, 'title': r.title, 'active_users_count': len(r.get_active_users()) } for r in public_rooms], 'private_rooms': [{ 'id': r.id, 'name': r.name, 'title': r.title, 'active_users_count': len(r.get_active_users()) } for r in private_rooms] }) @app.route("/api/rooms/create", methods=["POST"]) def create_room_api(): """Create a new room""" user = auth.get_current_user() data = request.get_json() or {} room_name = data.get('name', '').strip() is_private = data.get('is_private', False) if not room_name: return jsonify({'error': 'Room name is required'}), 400 # Private rooms require authentication if is_private and not user: return jsonify({'error': 'Authentication required to create private rooms'}), 401 # Check if room already exists existing_room = Room.query.filter_by(name=room_name).first() if existing_room: return jsonify({'error': 'Room name already exists'}), 400 # Create room new_room = Room() new_room.name = room_name new_room.is_private = is_private new_room.owner_id = user.id if user else None db.session.add(new_room) db.session.commit() # Broadcast new room to all users so it appears in sidebar new_room_data = { 'id': new_room.id, 'name': new_room.name, 'title': new_room.title, 'is_private': new_room.is_private, 'is_new': True # Flag to indicate this is a new room, not an update } socketio.emit("update_room_list", new_room_data, room=None) return jsonify({ 'success': True, 'room': { 'id': new_room.id, 'name': new_room.name, 'is_private': new_room.is_private } }) @app.route("/api/rooms//fork", methods=["POST"]) def fork_room(room_id): """Fork a room (authenticated users can fork to private or public)""" user = auth.get_current_user() data = request.get_json() or {} make_private = data.get('private', False) # Get source room source_room = Room.query.get(room_id) if not source_room: return jsonify({'error': 'Room not found'}), 404 # Private rooms can only be forked by their owner if source_room.is_private: if not user or source_room.owner_id != user.id: return jsonify({'error': 'Cannot fork private rooms you do not own'}), 403 # Private rooms require authentication if make_private and not user: return jsonify({'error': 'Authentication required to create private rooms'}), 401 # Generate new room name base_name = f"{source_room.name}_fork" new_name = base_name counter = 1 while Room.query.filter_by(name=new_name).first(): new_name = f"{base_name}_{counter}" counter += 1 # Create forked room new_room = Room() new_room.name = new_name new_room.title = f"Fork of {source_room.title or source_room.name}" new_room.is_private = make_private new_room.owner_id = user.id if user else None new_room.forked_from_id = source_room.id db.session.add(new_room) db.session.commit() # Copy messages from source room source_messages = Message.query.filter_by(room_id=source_room.id).all() for msg in source_messages: new_msg = Message( username=msg.username, content=msg.content, room_id=new_room.id ) db.session.add(new_msg) db.session.commit() return jsonify({ 'success': True, 'room': { 'id': new_room.id, 'name': new_room.name, 'title': new_room.title, 'is_private': new_room.is_private } }) @app.route("/api/rooms//archive", methods=["POST"]) @auth.require_auth def archive_room(room_id): """Archive a room (owner only)""" user = auth.get_current_user() room = Room.query.get(room_id) if not room: return jsonify({'error': 'Room not found'}), 404 if room.owner_id != user.id: return jsonify({'error': 'Only room owner can archive rooms'}), 403 room.is_archived = True db.session.commit() return jsonify({'success': True}) @app.route("/api/rooms//delete", methods=["DELETE"]) @auth.require_auth def delete_room(room_id): """Delete a room (owner only)""" user = auth.get_current_user() room = Room.query.get(room_id) if not room: return jsonify({'error': 'Room not found'}), 404 if room.owner_id != user.id: return jsonify({'error': 'Only room owner can delete rooms'}), 403 # Delete all messages in the room Message.query.filter_by(room_id=room.id).delete() # Delete activity state if any ActivityState.query.filter_by(room_id=room.id).delete() # Delete user sessions UserSession.query.filter_by(room_id=room.id).delete() # Delete the room db.session.delete(room) db.session.commit() return jsonify({'success': True}) @app.route("/api/generate-artifact-name", methods=["POST"]) def generate_artifact_name(): """Generate a meaningful filename for an artifact using AI. Returns a 1-3 word filename with underscores based on what the code does. Respects ENABLE_CODE_GEN_FILENAMES environment variable (enabled by default). """ # Check if feature is enabled (default: true) enabled = os.environ.get("ENABLE_CODE_GEN_FILENAMES", "true").lower() == "true" if not enabled: return jsonify({"filename": "compiled_binary"}) try: data = request.get_json() code = data.get("code", "") language = data.get("language", "") if not code: return jsonify({"filename": "compiled_binary"}) # Use MODEL_1 (Hermes) to generate filename client, model = get_openai_client_and_model("MODEL_1") system_prompt = """You are a filename generator. Given code, generate a SHORT, descriptive filename that represents what the code does. Rules: - Output ONLY the filename, nothing else - Use 1-3 words maximum - Use lowercase with underscores between words (e.g., "fizzbuzz" or "hello_world" or "prime_checker") - NO file extension - NO explanations or commentary - Be specific about what the code does Examples: - Code that prints "Hello World" → "hello_world" - Code that checks for prime numbers → "prime_checker" - Code that plays FizzBuzz → "fizzbuzz" - Code that sorts an array → "array_sort" - Code that calculates factorial → "factorial" """ user_prompt = f"Language: {language}\n\nCode:\n{code}\n\nGenerate filename:" response = client.chat.completions.create( model=model, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], temperature=0.3, max_tokens=20 ) filename = response.choices[0].message.content.strip() # Clean up the filename (remove quotes, extensions, whitespace) filename = filename.strip('"\'') filename = filename.split('.')[0] # Remove any extension filename = filename.replace(' ', '_') filename = filename.lower() # Validate filename (alphanumeric and underscores only) import re if not re.match(r'^[a-z0-9_]+$', filename): filename = "compiled_binary" # Ensure it's not too long (max 50 chars) if len(filename) > 50: filename = filename[:50] return jsonify({"filename": filename}) except Exception as e: print(f"Error generating artifact name: {e}") return jsonify({"filename": "compiled_binary"}) def _unsandbox_error_response(e, log_label): """Turn an Unsandbox SDK exception into an informative JSON error. Surfaces the real upstream HTTP status + body when available instead of a flat 500, so failures are debuggable from the browser console and logs. Never serializes credentials: public/secret keys live in request headers, never in response bodies, so echoing the upstream body is safe. """ import traceback if isinstance(e, requests.exceptions.HTTPError) and e.response is not None: status = e.response.status_code body = (e.response.text or "")[:2000] print(f"{log_label}: HTTP {status} from Unsandbox: {body}") traceback.print_exc() out_status = status if 400 <= status < 600 else 502 return ( jsonify( { "error": "Unsandbox upstream error", "upstream_status": status, "upstream_body": body, } ), out_status, ) print(f"{log_label}: {e}") traceback.print_exc() return jsonify({"error": f"{log_label}: {e}"}), 500 # Unsandbox API proxy endpoints - keeps API keys server-side @app.route("/api/code/execute", methods=["POST"]) def proxy_code_execute(): """Proxy code execution requests to Unsandbox API. Keeps UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY secure on the server side. Uses official Unsandbox Python SDK for authentication and execution. Supports artifacts parameter for compiled binaries, images, etc. """ try: data = request.get_json() if not data: return jsonify({"error": "Request body required"}), 400 # Check if credentials are configured public_key = os.environ.get("UNSANDBOX_PUBLIC_KEY") secret_key = os.environ.get("UNSANDBOX_SECRET_KEY") if not public_key or not secret_key: return jsonify({"error": "Code execution not configured"}), 503 # Extract parameters from request language = data.get("language") code = data.get("code") if not language or not code: return jsonify({"error": "Language and code are required"}), 400 # Build request body with all supported parameters request_body = { "language": language, "code": code, "return_artifact": True, } # Use SDK's internal _make_request for full parameter support result = un._make_request( "POST", "/execute", public_key, secret_key, request_body ) # Return job_id from response return jsonify({"job_id": result.get("job_id")}), 200 except Exception as e: return _unsandbox_error_response(e, "Error proxying code execution") @app.route("/api/code/jobs/", methods=["GET"]) def proxy_job_status(job_id): """Proxy job status requests to Unsandbox API using SDK.""" try: # Check if credentials are configured if not os.environ.get("UNSANDBOX_PUBLIC_KEY") or not os.environ.get("UNSANDBOX_SECRET_KEY"): return jsonify({"error": "Code execution not configured"}), 503 # Use SDK's get_job method result = un.get_job(job_id) # SDK returns job status dict return jsonify(result), 200 except Exception as e: return _unsandbox_error_response(e, "Error fetching job status") @app.route("/api/code/jobs/", methods=["DELETE"]) def proxy_job_cancel(job_id): """Proxy job cancellation requests to Unsandbox API using SDK.""" try: # Check if credentials are configured if not os.environ.get("UNSANDBOX_PUBLIC_KEY") or not os.environ.get("UNSANDBOX_SECRET_KEY"): return jsonify({"error": "Code execution not configured"}), 503 # Use SDK's cancel_job method result = un.cancel_job(job_id) # SDK returns success status return jsonify(result), 200 except Exception as e: return _unsandbox_error_response(e, "Error cancelling job") @app.route("/api/fix-code", methods=["POST"]) def fix_code(): """Auto-fix code errors by asking AI to fix issues based on stderr output. Accepts code, language, stderr, and attempt number. Returns fixed code block or error message. """ try: data = request.get_json() code = data.get("code", "") language = data.get("language", "") stderr = data.get("stderr", "") exit_code = data.get("exit_code", 1) attempt = data.get("attempt", 1) if not code or not language: return jsonify({"error": "Code and language are required"}), 400 # Try MODEL_3 (Qwen Coder) first for better code fixing, fall back to MODEL_1 (Hermes) try: client, model = get_openai_client_and_model("MODEL_3") print(f"[INFO] Using MODEL_3 for code fixing: {model}") except Exception as e: print(f"[WARN] MODEL_3 not available, falling back to MODEL_1: {e}") client, model = get_openai_client_and_model("MODEL_1") print(f"[INFO] Using MODEL_1 for code fixing: {model}") system_prompt = f"""You are an expert {language} programmer and debugger. Your task is to fix code that has errors. CRITICAL RULES: - Output ONLY the fixed code, nothing else - NO explanations, NO comments about what you changed - NO markdown code fences (```), just the raw code - Preserve the original code structure and logic as much as possible - Fix ONLY the errors reported in stderr - If the error mentions missing imports/includes, add them at the top - If the error is a syntax error, fix the syntax - Keep the same variable names and overall approach The code should be immediately executable without any modifications.""" user_prompt = f"""The following {language} code has errors: ```{language} {code} ``` Error output (exit code {exit_code}): ``` {stderr} ``` Fix the code (output ONLY the corrected code, no explanations):""" response = client.chat.completions.create( model=model, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], temperature=0.2, # Low temperature for consistent fixes max_tokens=2000 ) fixed_code = response.choices[0].message.content.strip() # Clean up any markdown code fences that might have slipped through if fixed_code.startswith("```"): lines = fixed_code.split("\n") # Remove first line if it's a fence if lines[0].startswith("```"): lines = lines[1:] # Remove last line if it's a fence if lines and lines[-1].strip() == "```": lines = lines[:-1] fixed_code = "\n".join(lines) return jsonify({ "success": True, "fixed_code": fixed_code, "attempt": attempt }) except Exception as e: print(f"Error fixing code: {e}") return jsonify({"error": f"Failed to fix code: {str(e)}"}), 500 @app.route("/chat/") def chat(room_name): user = auth.get_current_user() # Get or create the room room = Room.query.filter_by(name=room_name).first() # If room doesn't exist yet, it will be created in get_room() when user joins # But check if they're trying to access a private room they don't own if room and room.is_private: if not user or room.owner_id != user.id: return "Access denied: This is a private room", 403 # Query public rooms and user's private rooms for sidebar public_rooms = Room.query.filter_by(is_private=False, is_archived=False).order_by(Room.id.desc()).all() private_rooms = [] if user: private_rooms = Room.query.filter_by(is_private=True, is_archived=False, owner_id=user.id).order_by(Room.id.desc()).all() # Use authenticated user's display name, or None (will prompt on client side) username = user.display_name if user else None # Generate Open Graph metadata for social sharing og_image = None og_description = "AI-powered chat room on OpenCompletion" og_title = f"{room_name} - OpenCompletion" if room: # Try to get first image from messages og_image = extract_first_image_for_og(room.id) og_description = generate_og_description(room) if room.title: og_title = f"{room.title} - OpenCompletion" # Pass username, rooms, room (current room), and user into the template return render_template( "chat.html", room_name=room_name, current_room=room, public_rooms=public_rooms, private_rooms=private_rooms, username=username, user=user, og_title=og_title, og_description=og_description, og_image=og_image ) @app.route("/download_chat_history", methods=["GET"]) def download_chat_history(): room_name = request.args.get("room_name") room = get_room(room_name) if not room: return jsonify({"error": "Room not found"}), 404 messages = Message.query.filter_by(room_id=room.id).all() if not messages: return jsonify({"error": "No messages found"}), 404 chat_history = [ { "role": "system" if message.username in SYSTEM_USERS else "user", "content": message.content, } for message in messages if not message.is_base64_image() ] if not chat_history: return jsonify({"error": "No valid messages found"}), 404 response = Response( response=json.dumps(chat_history, indent=2), status=200, mimetype="application/json", ) response.headers["Content-Disposition"] = f"attachment; filename={room.name}.json" return response @app.route("/download_chat_history_md", methods=["GET"]) def download_chat_history_md(): room_name = request.args.get("room_name") room = get_room(room_name) if not room: return jsonify({"error": "Room not found"}), 404 messages = Message.query.filter_by(room_id=room.id).all() if not messages: return jsonify({"error": "No messages found"}), 404 # Access system users from the existing context chat_history_md = [] toc = [] for index, message in enumerate(messages): if not message.is_base64_image(): # Correctly call the method role = "System" if message.username in SYSTEM_USERS else "User" header = f"### {role}: {message.username} (Turn {index + 1})" toc.append( f"- [{role}: {message.username} (Turn {index + 1})](#{role.lower()}-{message.username.lower().replace(' ', '-')}-turn-{index + 1})" ) chat_history_md.append(f"{header}\n\n{message.content}\n\n---\n") if not chat_history_md: return jsonify({"error": "No valid messages found"}), 404 markdown_content = ( f"# Chat History for {room.name}\n\n## Table of Contents\n" + "\n".join(toc) + "\n\n" + "\n".join(chat_history_md) ) response = Response(response=markdown_content, status=200, mimetype="text/markdown") response.headers["Content-Disposition"] = f'attachment; filename="{room.name}.md"' return response @app.route("/search") def search_page(): # Query all rooms so that newest is first. rooms = Room.query.order_by(Room.id.desc()).all() keywords = request.args.get("keywords", "") username = request.args.get("username", "guest") if not keywords: return render_template( "search.html", rooms=rooms, keywords=keywords, results=[], username=username, error="Keywords are required", ) # Call the function to search messages search_results = search_messages(keywords) # If there's exactly one search result, redirect directly to that room if len(search_results) == 1: room_result = search_results[0] room_name = room_result["room_name"] # Build the redirect URL with current parameters redirect_params = {} if username and username != "guest": redirect_params["username"] = username # Preserve other URL parameters like model, voice, etc. for param in ["model", "voice"]: value = request.args.get(param) if value: redirect_params[param] = value redirect_url = url_for("chat", room_name=room_name, **redirect_params) return redirect(redirect_url) return render_template( "search.html", rooms=rooms, keywords=keywords, results=search_results, username=username, error=None, ) def search_messages(keywords): search_results = {} # Split the keywords by spaces and sanitize keyword_list = keywords.lower().split() # Sanitize keywords to prevent SQL injection sanitized_keywords = [] for keyword in keyword_list: # Remove potentially dangerous characters and limit length sanitized_keyword = "".join( c for c in keyword if c.isalnum() or c.isspace() or c in "-_" )[:50] if sanitized_keyword.strip(): # Only add non-empty keywords sanitized_keywords.append(sanitized_keyword.strip()) if not sanitized_keywords: return {} # Search for messages containing any of the sanitized keywords using parameterized query messages = Message.query.filter( db.or_( *[Message.content.ilike(f"%{keyword}%") for keyword in sanitized_keywords] ) ).all() for message in messages: room = Room.query.get(message.room_id) if room: # Calculate the score based on the number of occurrences of all keywords score = sum( message.content.lower().count(keyword) for keyword in keyword_list ) if room.id not in search_results: search_results[room.id] = { "room_id": room.id, "room_name": room.name, "room_title": room.title, "score": 0, } search_results[room.id]["score"] += score # Convert the dictionary to a list and sort results by score in descending order search_results_list = list(search_results.values()) search_results_list.sort(key=lambda x: x["score"], reverse=True) return search_results_list # Handle user joining a room @socketio.on("join") def on_join(data): room_name = data["room_name"] username = data["username"] room = get_room(room_name) # Set owner for newly created rooms (if room has no owner and user is authenticated) if room.owner_id is None: user = auth.get_current_user() if user: room.owner_id = user.id db.session.add(room) db.session.commit() # Add the user to the active users list room.add_user(username) # Store session data in the database user_session = UserSession( session_id=request.sid, username=username, room_name=room_name, room_id=room.id ) db.session.add(user_session) db.session.commit() # Emit the active and inactive users list to the new joiner emit( "active_users", { "active_users": room.get_active_users(), "inactive_users": room.get_inactive_users(), }, room=request.sid, ) # Emit the active and inactive users list to everyone in the room emit( "active_users", { "active_users": room.get_active_users(), "inactive_users": room.get_inactive_users(), }, room=room_name, include_self=False, ) # This makes the client start listening for new events for this room. join_room(room_name) # update the title bar with the proper room title, if it exists for just this new client. if room.title: socketio.emit("update_room_title", {"title": room.title}, room=request.sid) # Fetch previous messages from the database previous_messages = Message.query.filter_by(room_id=room.id).all() # count the number of tokens in this room. total_token_count = 0 # Send the history of messages only to the newly connected client. for message in previous_messages: if not message.is_base64_image(): total_token_count += message.token_count emit( "previous_messages", { "id": message.id, "username": message.username, "content": message.content, }, room=request.sid, ) message_count = len(previous_messages) if room.title is None and message_count >= 6: room.title = gpt_generate_room_title(previous_messages) db.session.add(room) socketio.emit("update_room_title", {"title": room.title}, room=room.name) # Emit an event to update this room's title in the sidebar for all users. updated_room_data = {"id": room.id, "name": room.name, "title": room.title} socketio.emit("update_room_list", updated_room_data, room=None) # commit session & active user list and title to database. db.session.commit() # Broadcast to all clients in the room that a new user has joined. emit( "chat_message", {"id": None, "content": f"{username} has joined the room."}, room=room.name, ) emit( "chat_message", { "id": None, "content": f"Estimated {total_token_count} total tokens in conversation.", }, room=request.sid, ) # Handle user leaving a room @socketio.on("disconnect") def on_disconnect(): sid = request.sid user_session = UserSession.query.filter_by(session_id=sid).first() if user_session: room_name = user_session.room_name username = user_session.username room = Room.query.filter_by(name=room_name).first() room.remove_user(username) leave_room(room_name) # Broadcast to all clients in the room that a user has left the room. # Emit the active and inactive users list to everyone in the room emit( "active_users", { "active_users": room.get_active_users(), "inactive_users": room.get_inactive_users(), }, room=room.name, include_self=False, ) emit( "chat_message", {"id": None, "content": f"{username} has left the room."}, room=room.name, include_self=False, ) # Remove session data from the database db.session.delete(user_session) db.session.commit() @socketio.on("chat_message") def handle_message(data): room_name = data["room_name"] room = get_room(room_name) username = data["username"] message = data["message"].strip() model = data.get("model", "None") new_message = Message( username=username, content=message, room_id=room.id, ) db.session.add(new_message) # Update room's updated_at timestamp (Unix epoch) from datetime import datetime room.updated_at = int(datetime.utcnow().timestamp()) db.session.add(room) db.session.commit() emit( "chat_message", { "id": new_message.id, "username": username, "content": message, }, room=room.name, ) commands = message.splitlines() for command in commands: if command.startswith("/help"): socketio.emit( "chat_message", {"id": "tmp-1", "username": "System", "content": HELP_MESSAGE}, room=room_name, ) return if command.startswith("/activity cancel"): gevent.spawn(activity.cancel_activity, room_name, username) return if command.startswith("/activity info"): gevent.spawn(activity.display_activity_info, room_name, username) return if command.startswith("/activity metadata"): gevent.spawn(activity.display_activity_metadata, room_name, username) return if command.startswith("/activity"): s3_file_path = command.split(" ", 1)[1].strip() gevent.spawn(activity.start_activity, room_name, s3_file_path, username) return if command.startswith("/s3 ls"): s3_file_path_pattern = command.split(" ", 2)[2].strip() gevent.spawn(list_s3_files, room.name, s3_file_path_pattern, username) if command.startswith("/s3 load"): s3_file_path = command.split(" ", 2)[2].strip() gevent.spawn(load_s3_file, room_name, s3_file_path, username) if command.startswith("/s3 save"): s3_key_path = command.split(" ", 2)[2].strip() gevent.spawn(save_code_block_to_s3, room_name, s3_key_path, username) if command.startswith("/title new"): gevent.spawn(generate_new_title, room_name, username) return if command.startswith("/cancel"): gevent.spawn(cancel_generation, room_name) return activity_state = ActivityState.query.filter_by(room_id=room.id).first() if activity_state: gevent.spawn( activity.handle_activity_response, room_name, message, username, model ) return if model != "None": emit( "chat_message", {"id": None, "content": "Processing..."}, room=room.name, ) if "anthropic.claude" in model: gevent.spawn(chat_claude, username, room_name, model_name=model) if "dall-e" in model: gevent.spawn(generate_dalle_image, room_name, message, username) else: # All other models (Groq, Together, Mistral, etc.) use OpenAI client enable_thinking = data.get("enable_thinking", True) gevent.spawn( chat_gpt, username, room_name, model_name=model, enable_thinking=enable_thinking, ) @socketio.on("delete_message") def handle_delete_message(data): msg_id = data["message_id"] # Delete the message from the database message = db.session.query(Message).filter(Message.id == msg_id).one_or_none() if message: db.session.delete(message) db.session.commit() # Notify all clients in the room to remove the message from their DOM emit("message_deleted", {"message_id": msg_id}, room=data["room_name"]) @socketio.on("update_message") def handle_update_message(data): message_id = data["message_id"] new_content = data["content"] room_name = data["room_name"] # Find the message by ID message = Message.query.get(message_id) if message: # Update the message content message.content = new_content message.count_tokens() db.session.add(message) db.session.commit() # Emit an event to update the message on all clients emit( "message_updated", { "message_id": message_id, "content": new_content, "username": message.username, }, room=room_name, ) @socketio.on("get_activity_status") def handle_get_activity_status(data): """Get the current activity status for a room.""" activity.handle_get_activity_status(data) def group_consecutive_roles(messages): if not messages: return [] grouped_messages = [] current_role = messages[0]["role"] current_content = [] for message in messages: if message["role"] == current_role: current_content.append(message["content"]) else: grouped_messages.append( {"role": current_role, "content": " ".join(current_content)} ) current_role = message["role"] current_content = [message["content"]] # Append the last grouped message grouped_messages.append( {"role": current_role, "content": " ".join(current_content)} ) return grouped_messages def chat_claude( # username, room_name, model_name="anthropic.claude-3-5-sonnet-20240620-v1:0" username, room_name, model_name="anthropic.claude-3-sonnet-20240229-v1:0", ): with app.app_context(): room = get_room(room_name) # claude has a 200,000 token context window for prompts. all_messages = ( Message.query.filter_by(room_id=room.id).order_by(Message.id.desc()).all() ) chat_history = [] for msg in reversed(all_messages): if msg.is_base64_image(): continue role = "assistant" if msg.username in SYSTEM_USERS else "user" chat_history.append({"role": role, "content": msg.content}) # only claude cares about this constrant. chat_history = group_consecutive_roles(chat_history) # Initialize the Bedrock client using boto3 and profile name. if app.config.get("PROFILE_NAME"): session = boto3.Session(profile_name=app.config["PROFILE_NAME"]) client = session.client("bedrock-runtime", region_name="us-west-2") else: client = boto3.client("bedrock-runtime", region_name="us-west-2") # Define the request parameters params = { "modelId": model_name, "contentType": "application/json", "accept": "*/*", "body": json.dumps( { "messages": chat_history, "max_tokens": 4096, "temperature": 0, "top_k": 250, "top_p": 0.999, "stop_sequences": ["\n\nHuman:"], "anthropic_version": "bedrock-2023-05-31", } ).encode(), } # Process the event stream buffer = "" # save empty message, we need the ID when we chunk the response. with app.app_context(): new_message = Message(username=model_name, content=buffer, room_id=room.id) db.session.add(new_message) db.session.commit() msg_id = new_message.id try: # Invoke the model with response stream response = client.invoke_model_with_response_stream(**params)["body"] first_chunk = True for event in response: content = "" # Check if there has been a cancellation request, break if there is. if cancellation_requests.get(msg_id): del cancellation_requests[msg_id] break if "chunk" in event: chunk_data = json.loads(event["chunk"]["bytes"].decode()) if chunk_data["type"] == "content_block_delta": if chunk_data["delta"]["type"] == "text_delta": content = chunk_data["delta"]["text"] if content: buffer += content # Accumulate content if first_chunk: socketio.emit( "message_chunk", { "id": msg_id, "content": content, "username": username, "model_name": model_name, "is_first_chunk": True, }, room=room_name, ) first_chunk = False else: socketio.emit( "message_chunk", {"id": msg_id, "content": content}, room=room_name, ) socketio.sleep(0) # Force immediate handling except Exception as e: with app.app_context(): message_content = f"AWS Bedrock Error: {e}" new_message = ( db.session.query(Message).filter(Message.id == msg_id).one_or_none() ) if new_message: new_message.content = message_content new_message.count_tokens() db.session.add(new_message) db.session.commit() socketio.emit( "chat_message", { "id": msg_id, "username": model_name, "content": message_content, }, room=room_name, ) socketio.emit("delete_processing_message", msg_id, room=room_name) # exit early to avoid clobbering the error message. return None # Save the entire completion to the database with app.app_context(): new_message = ( db.session.query(Message).filter(Message.id == msg_id).one_or_none() ) if new_message: new_message.content = buffer new_message.count_tokens() db.session.add(new_message) db.session.commit() socketio.emit( "message_chunk", {"id": msg_id, "content": "", "is_complete": True}, room=room_name, ) socketio.emit("delete_processing_message", msg_id, room=room_name) def chat_gpt(username, room_name, model_name="gpt-4o-mini", enable_thinking=True): openai_client, model_name = get_openai_client_and_model(model_name) temperature = 0 limit = 20 if "gpt-4" in model_name: limit = 1000 if "o1-" in model_name: temperature = 1 if "o3-" in model_name: temperature = 1 if "o4-" in model_name: temperature = 1 # Check if this is a vision-capable model vision_enabled = is_vision_model(model_name) if vision_enabled: print(f"Vision model detected: {model_name}") with app.app_context(): room = get_room(room_name) room_id = room.id # Save room_id to avoid DetachedInstanceError later last_messages = ( Message.query.filter_by(room_id=room_id) .order_by(Message.id.desc()) .limit(limit) .all() ) chat_history = [] # Find the most recent image message ID (only this one gets base64) most_recent_image_id = None if vision_enabled: for msg in last_messages: # newest first is_image_msg = msg.is_base64_image() or extract_external_image_url(msg.content) if is_image_msg: most_recent_image_id = msg.id print(f"Vision: will include base64 for most recent image (msg {msg.id})") break # Build chat history (oldest first) - include all text, only most recent image for msg in reversed(last_messages): is_image_msg = msg.is_base64_image() or extract_external_image_url(msg.content) # Skip ALL images for non-vision models if is_image_msg and not vision_enabled: continue role = "assistant" if msg.username in SYSTEM_USERS else "user" # For vision: only include base64 for the most recent image # Older images are skipped entirely (they're just base64, no useful text) if vision_enabled and is_image_msg: if msg.id == most_recent_image_id: content = build_message_content(msg, vision_enabled, room_id=room_id) chat_history.append({"role": role, "content": content}) # Skip older image messages - they have no text context continue # Regular text message - always include chat_history.append({"role": role, "content": msg.content}) buffer = "" # Content buffer for accumulating the chunks # save empty message, we need the ID when we chunk the response. with app.app_context(): new_message = Message(username=model_name, content=buffer, room_id=room_id) db.session.add(new_message) db.session.commit() msg_id = new_message.id first_chunk = True create_kwargs = { "model": model_name, "messages": chat_history, "n": 1, "stream": True, } if "o3" not in model_name: # o3 does not support temperature at all! create_kwargs["temperature"] = temperature if not enable_thinking: # Qwen3 / vLLM-style switch to suppress chain-of-thought and save # tokens. OpenAI's hosted API rejects unknown body params, so only # send to self-hosted OpenAI-compatible endpoints; o1/o3 think # unconditionally and would 400 on this anyway. Models whose chat # template ignores the kwarg (e.g. Hermes) simply drop it. base_url = str(getattr(openai_client, "base_url", "") or "") if "api.openai.com" not in base_url: create_kwargs["extra_body"] = { "chat_template_kwargs": {"enable_thinking": False} } try: chunks = openai_client.chat.completions.create(**create_kwargs) except Exception as e: with app.app_context(): message_content = f"{model_name} Error: {e}" new_message = ( db.session.query(Message).filter(Message.id == msg_id).one_or_none() ) if new_message: new_message.content = message_content new_message.count_tokens() db.session.add(new_message) db.session.commit() socketio.emit( "chat_message", { "id": msg_id, "username": model_name, "content": message_content, }, room=room_name, ) socketio.emit("delete_processing_message", msg_id, room=room_name) # exit early to avoid clobbering the error message. return None for chunk in chunks: # Check if there has been a cancellation request, break if there is. if cancellation_requests.get(msg_id): del cancellation_requests[msg_id] break delta = chunk.choices[0].delta # Qwen3.x / Deepseek-R1 / o1: thinking streams via delta.reasoning_content, # final answer via delta.content. Forward reasoning deltas to the client # so it can lazy-create a collapsible thinking block; do NOT persist them # (transient, model-private intermediate state). reasoning = getattr(delta, "reasoning_content", None) if reasoning: socketio.emit( "message_chunk", {"id": msg_id, "reasoning_content": reasoning}, room=room_name, ) socketio.sleep(0) content = delta.content if content: buffer += content # Accumulate content if first_chunk: socketio.emit( "message_chunk", { "id": msg_id, "content": content, "username": username, "model_name": model_name, "is_first_chunk": True, }, room=room_name, ) first_chunk = False else: socketio.emit( "message_chunk", {"id": msg_id, "content": content}, room=room_name, ) socketio.sleep(0) # Force immediate handling # Save the entire completion to the database with app.app_context(): new_message = ( db.session.query(Message).filter(Message.id == msg_id).one_or_none() ) if new_message: new_message.content = buffer new_message.count_tokens() db.session.add(new_message) db.session.commit() socketio.emit( "message_chunk", {"id": msg_id, "content": "", "is_complete": True}, room=room_name, ) socketio.emit("delete_processing_message", msg_id, room=room_name) def chat_llama(username, room_name, model_name="mistral-7b-instruct-v0.2.Q3_K_L.gguf"): import llama_cpp # https://llama-cpp-python.readthedocs.io/en/latest/api-reference/ model = llama_cpp.Llama(model_name, n_gpu_layers=-1, n_ctx=32000) limit = 15 with app.app_context(): room = get_room(room_name) last_messages = ( Message.query.filter_by(room_id=room.id) .order_by(Message.id.desc()) .limit(limit) .all() ) chat_history = [ { "role": "system" if msg.username in SYSTEM_USERS else "user", "content": f"{msg.username}: {msg.content}", } for msg in reversed(last_messages) if not msg.is_base64_image() ] buffer = "" # Content buffer for accumulating the chunks # save empty message, we need the ID when we chunk the response. with app.app_context(): new_message = Message(username=model_name, content=buffer, room_id=room.id) db.session.add(new_message) db.session.commit() msg_id = new_message.id first_chunk = True try: chunks = model.create_chat_completion( messages=chat_history, stream=True, ) except Exception as e: with app.app_context(): message_content = f"LLama Error: {e}" new_message = ( db.session.query(Message).filter(Message.id == msg_id).one_or_none() ) if new_message: new_message.content = message_content new_message.count_tokens() db.session.add(new_message) db.session.commit() socketio.emit( "chat_message", { "id": msg_id, "username": model_name, "content": message_content, }, room=room_name, ) socketio.emit("delete_processing_message", msg_id, room=room_name) # exit early to avoid clobbering the error message. return None for chunk in chunks: # Check if there has been a cancellation request, break if there is. if cancellation_requests.get(msg_id): del cancellation_requests[msg_id] break delta = chunk["choices"][0]["delta"] # See OpenAI-client path above for rationale on reasoning_content. reasoning = delta.get("reasoning_content") if reasoning: socketio.emit( "message_chunk", {"id": msg_id, "reasoning_content": reasoning}, room=room_name, ) socketio.sleep(0) content = delta.get("content") if content: buffer += content # Accumulate content if first_chunk: socketio.emit( "message_chunk", { "id": msg_id, "content": content, "username": username, "model_name": model_name, "is_first_chunk": True, }, room=room_name, ) first_chunk = False else: socketio.emit( "message_chunk", {"id": msg_id, "content": content}, room=room_name, ) socketio.sleep(0) # Force immediate handling # Save the entire completion to the database with app.app_context(): new_message = ( db.session.query(Message).filter(Message.id == msg_id).one_or_none() ) if new_message: new_message.content = buffer new_message.count_tokens() db.session.add(new_message) db.session.commit() socketio.emit( "message_chunk", {"id": msg_id, "content": "", "is_complete": True}, room=room_name, ) socketio.emit("delete_processing_message", msg_id, room=room_name) def gpt_generate_room_title(messages): """ Generate a title for the room based on a list of messages. """ openai_client, model_name = get_openai_client_and_model() chat_history = [ { "role": "system" if msg.username in SYSTEM_USERS else "user", "content": f"{msg.username}: {msg.content}", } for msg in reversed(messages) if not msg.is_base64_image() ] chat_history.append( { "role": "system", "content": "return a short title for the title bar of this conversation.", } ) # Interaction with LLM to generate summary # For example, using OpenAI's GPT model response = openai_client.chat.completions.create( messages=chat_history, model=model_name, # or any appropriate model max_tokens=20, n=1, ) title = response.choices[0].message.content return title.replace('"', "") def generate_new_title(room_name, username): with app.app_context(): room = get_room(room_name) # Get the last few messages to generate a title last_messages = ( Message.query.filter_by(room_id=room.id) .order_by(Message.id.desc()) .limit(1000) # Adjust the limit as needed .all() ) # Generate the title using the messages new_title = gpt_generate_room_title(last_messages) # Update the room title in the database room.title = new_title db.session.add(room) db.session.commit() # Emit the new title to the room. socketio.emit("update_room_title", {"title": new_title}, room=room_name) # Emit an event to update this rooms title in the sidebar for all users. updated_room_data = {"id": room.id, "name": room.name, "title": room.title} socketio.emit("update_room_list", updated_room_data, room=None) # Optionally, send a confirmation message to the room confirmation_message = f"New title created: {new_title}" new_message = Message( username=username, content=confirmation_message, room_id=room.id ) db.session.add(new_message) db.session.commit() socketio.emit( "chat_message", { "id": new_message.id, "username": username, "content": confirmation_message, }, room=room_name, ) def generate_dalle_image(room_name, message, username): socketio.emit( "chat_message", {"id": None, "content": "Processing..."}, room=room_name, ) openai_client = OpenAI() # Initialize the content variable to hold either the image tag or an error message content = "" try: # Call the DALL-E 3 API to generate an image in base64 format response = openai_client.images.generate( model="dall-e-3", prompt=message, n=1, size="1024x1024", response_format="b64_json", ) # Access the base64-encoded image data image_data = response.data[0].b64_json revised_prompt = response.data[0].revised_prompt # Create an HTML img tag with the base64 data (escape user input for XSS protection) import html escaped_message = html.escape(message) escaped_prompt = html.escape(revised_prompt) content = f'{escaped_message}

{escaped_prompt}

' except Exception as e: # Set the content to an error message content = f"Error generating image: {e}" # Store the content in the database and emit to the frontend with app.app_context(): room = get_room(room_name) new_message = Message( username=username, content=content, # Store the img tag or error message as the content room_id=room.id, # Make sure you have the room ID available ) db.session.add(new_message) db.session.commit() # Emit the message with the content to the frontend socketio.emit( "chat_message", {"id": new_message.id, "username": username, "content": content}, room=room_name, ) def find_most_recent_code_block(room_name): with app.app_context(): # Get the room object from the database room = get_room(room_name) if not room: return None # Room not found # Get the most recent message for the room latest_message = ( Message.query.filter_by(room_id=room.id) .order_by(Message.id.desc()) .offset(1) .first() ) if latest_message: # Split the message content into lines lines = latest_message.content.split("\n") # Initialize variables to store the code block code_block_lines = [] code_block_started = False for line in lines: # Check if the line starts with a code block fence if line.startswith("```"): # If we've already started capturing, this fence ends the block if code_block_started: break else: # Start capturing from the next line code_block_started = True continue elif code_block_started: # If we're inside a code block, capture the line code_block_lines.append(line) # Join the captured lines to form the code block content code_block_content = "\n".join(code_block_lines) return code_block_content # No code block found in the latest message return None def save_code_block_to_s3(room_name, s3_key_path, username): # Initialize the S3 client s3_client = get_s3_client() # Assuming the bucket name is set in an environment variable bucket_name = os.environ.get("S3_BUCKET_NAME") # Find the most recent code block code_block_content = find_most_recent_code_block(room_name) # Initialize a variable to hold the message content message_content = "" if code_block_content: try: # Save the code block content to S3 s3_client.put_object( Bucket=bucket_name, Key=s3_key_path, Body=code_block_content ) # Set the success message content message_content = f"Code block saved to S3 at {s3_key_path}" except Exception as e: # Set the error message content if S3 save fails message_content = f"Error saving file to S3: {e}" else: # Set the error message content if no code block is found message_content = "No code block found to save to S3." # Save the message to the database and emit to the frontend with app.app_context(): # Get the room object from the database room = get_room(room_name) if room: # Create a new message object new_message = Message( username=username, content=message_content, room_id=room.id ) # Add the new message to the session and commit db.session.add(new_message) db.session.commit() # Emit the message to the frontend with the new message ID socketio.emit( "chat_message", { "id": new_message.id, "username": username, "content": message_content, }, room=room_name, ) def load_s3_file(room_name, s3_file_path, username): # Initialize the S3 client s3_client = get_s3_client() # Assuming the bucket name is set in an environment variable bucket_name = os.environ.get("S3_BUCKET_NAME") # Initialize message content variable message_content = "" try: # Retrieve the file content from S3 response = s3_client.get_object(Bucket=bucket_name, Key=s3_file_path) file_content = response["Body"].read().decode("utf-8") # Format the file content as a code block message_content = f"```\n{file_content}\n```" except Exception as e: # Handle errors (e.g., file not found, access denied) message_content = f"Error loading file from S3: {e}" # Save the message to the database and emit to the chatroom with app.app_context(): room = get_room(room_name) new_message = Message( username=username, content=message_content, room_id=room.id, ) db.session.add(new_message) db.session.commit() # Emit the message to the chatroom with the message ID socketio.emit( "chat_message", { "id": new_message.id, "username": username, "content": message_content, }, room=room_name, ) def list_s3_files(room_name, s3_file_path_pattern, username): import fnmatch from datetime import timezone # Initialize the S3 client s3_client = get_s3_client() # Assuming the bucket name is set in an environment variable bucket_name = os.environ.get("S3_BUCKET_NAME") # Initialize the list to hold all file information files = [] # Initialize the pagination token continuation_token = None # Loop to handle pagination while True: # List objects in the S3 bucket with pagination support list_kwargs = { "Bucket": bucket_name, } if continuation_token: list_kwargs["ContinuationToken"] = continuation_token response = s3_client.list_objects_v2(**list_kwargs) # Process the current page of results for obj in response.get("Contents", []): key = obj["Key"] if s3_file_path_pattern == "*" or fnmatch.fnmatch( key, s3_file_path_pattern ): size = obj["Size"] last_modified = obj["LastModified"] # Convert last_modified to a timezone-aware datetime object last_modified = ( last_modified.replace(tzinfo=timezone.utc) .astimezone(tz=None) .strftime("%Y-%m-%d %H:%M:%S %Z") ) files.append( f"{key} (Size: {size} bytes, Last Modified: {last_modified})" ) # Check if there are more pages if response.get("IsTruncated"): continuation_token = response.get("NextContinuationToken") else: break # No more pages # Format the message content with the list of files and metadata message_content = ( "```\n" + "\n".join(files) + "\n```" if files else "No files found." ) # Save the message to the database and emit to the chatroom with app.app_context(): room = Room.query.filter_by(name=room_name).first() if room: new_message = Message( username=username, content=message_content, room_id=room.id, ) db.session.add(new_message) db.session.commit() # Emit the message to the chatroom with the message ID socketio.emit( "chat_message", { "id": new_message.id, "username": username, "content": message_content, }, room=room_name, ) def cancel_generation(room_name): with app.app_context(): room = get_room(room_name) # Get the most recent message for the room that is being generated latest_message = ( Message.query.filter_by(room_id=room.id) .order_by(Message.id.desc()) .offset(1) .first() ) if latest_message: # Set the cancellation request for the given message ID cancellation_requests[latest_message.id] = True # Optionally, inform the user that the generation has been canceled socketio.emit( "chat_message", { "id": None, "username": "System", "content": f"Generation for message ID {latest_message.id} has been canceled.", }, room=room_name, ) if __name__ == "__main__": import argparse parser = argparse.ArgumentParser( description="Run the SocketIO application with optional configurations." ) parser.add_argument("--profile", help="AWS profile name", default=None) parser.add_argument( "--local-activities", action="store_true", help="Use local activity files instead of S3", ) parser.add_argument( "--port", type=int, default=5001, help="Port number to run the SocketIO server on (default: 5001)", ) args = parser.parse_args() # Set profile_name and other configurations as global attributes of the app object app.config["PROFILE_NAME"] = args.profile app.config["LOCAL_ACTIVITIES"] = args.local_activities # Run the SocketIO server with the specified port # Disable reloader to avoid gevent fork compatibility issues socketio.run(app, host="0.0.0.0", port=args.port, use_reloader=False)