# myapp/projects/projects_routes.py import datetime import os # Needed for checking environment variables (e.g., for OpenAI key) import logging from flask import request, jsonify, current_app, has_app_context # Flask utilities from bson.objectid import ObjectId, InvalidId # For MongoDB ObjectIds from collections import defaultdict # May be used in helper logic from functools import wraps # Import wraps for dummy decorator # --- Local Blueprint Import --- from . import bp # Import the 'bp' instance defined in the local __init__.py # --- Shared Extensions and Utilities Imports --- try: from ..extensions import mongo # Import the initialized PyMongo instance # Import utilities from the parent 'myapp/utils.py' from ..utils import token_required, generate_passkey except ImportError: # Fallback or error handling if imports fail print("Warning: Could not import mongo, token_required, or generate_passkey in projects/projects_routes.py.") mongo = None generate_passkey = lambda: "error_generating_passkey" # Dummy function # Define a dummy decorator if token_required is missing def token_required(f): @wraps(f) def wrapper(*args, **kwargs): print("ERROR: token_required decorator is not available!") return jsonify({"message": "Server configuration error: Missing authentication utility."}), 500 return wrapper # --- Schema Imports --- try: # Import the relevant schemas defined in schemas.py from ..schemas import ( ProjectCreateSchema, ProjectUpdateSchema, ProjectSchema, ProjectListSchema # Use ProjectListSchema for the list endpoint ) from marshmallow import ValidationError except ImportError: print("Warning: Could not import Project schemas or ValidationError in projects/projects_routes.py.") ProjectCreateSchema = None ProjectUpdateSchema = None ProjectSchema = None ProjectListSchema = None ValidationError = None # --- Celery Task Import --- # IMPORTANT: Assumes the project root directory ('your_fullstack_project/') is in PYTHONPATH try: from backend_flask.celery_worker.celery_app import async_recalc_project_keywords except ModuleNotFoundError: print("Warning: Could not import 'async_recalc_project_keywords' from 'celery_worker'. Ensure project root is in PYTHONPATH.") # Define a dummy task function to prevent NameError if Celery isn't set up def _dummy_celery_task(*args, **kwargs): task_name = args[0] if args else 'dummy_task' print(f"ERROR: Celery task {task_name} not available!") class DummyTask: def __init__(self, name): self.__name__ = name def delay(self, *a, **kw): print(f"ERROR: Tried to call delay() on dummy task {self.__name__}") pass return DummyTask(task_name) async_recalc_project_keywords = _dummy_celery_task('async_recalc_project_keywords') # --- Dialog Helper Import --- # Import the helper function from the sibling 'dialog' blueprint's routes module try: # Assumes the function is defined in myapp/dialog/dialog_routes.py from ..dialog.dialog_routes import generate_knowledge_base_message except ImportError: print("Warning: Could not import 'generate_knowledge_base_message' from dialog blueprint.") # Define a dummy function generate_knowledge_base_message = lambda pid: "Error: Knowledge base function not available." # --- External Lib Imports (for summarize_project) --- # Import conditionally to avoid errors if not installed try: import google.generativeai as genai from google.api_core import exceptions as google_exceptions except ImportError: print("Warning: google.generativeai not installed. Project summarization will fail.") genai = None google_exceptions = None # --- Helper to get logger safely --- def _get_logger(): if has_app_context(): return current_app.logger return logging.getLogger(__name__) # Note: Routes use paths relative to the '/api/projects' prefix defined in __init__.py. @bp.route('/', methods=['POST']) # Path relative to prefix @token_required def create_project(current_user): """ Create a new project for the authenticated user. Uses ProjectCreateSchema for input validation. Expects 'name' and optional 'topic', 'description' in JSON payload. Generates a unique passkey for the project. """ logger = _get_logger() # Validate user object from token if not current_user or not current_user.get("_id"): logger.error("Invalid current_user object received in create_project") return jsonify({"message": "Internal authorization error."}), 500 try: user_id = ObjectId(current_user["_id"]) except (InvalidId, TypeError) as e: logger.error(f"User ID conversion error in create_project: {e}") return jsonify({"message": "Invalid user ID format in token."}), 400 # Check dependencies if not mongo: return jsonify({"message": "Database connection not available."}), 500 if not ProjectCreateSchema or not ValidationError: return jsonify({"message": "Server configuration error: Schema unavailable."}), 500 # Get and validate JSON data using the schema json_data = request.get_json() or {} schema = ProjectCreateSchema() try: validated_data = schema.load(json_data) except ValidationError as err: logger.warning(f"Create project validation failed: {err.messages}") return jsonify(err.messages), 422 # Return validation errors # Extract validated data name = validated_data['name'] # Required field topic = validated_data.get('topic', "") # Optional field from schema description = validated_data.get('description', "") # Optional field from schema try: # Generate a passkey for potential sharing/joining later passkey = generate_passkey() db = mongo.db # Use imported mongo instance # Prepare project document data now = datetime.datetime.now(datetime.timezone.utc) project_data = { "ownerId": user_id, "collaborators": [], # Initially empty collaborator list "passkey": passkey, # Store the generated passkey "name": name.strip(), # Use validated and trimmed name "topic": topic, "description": description, "summary": "", # Initial empty summary "keywords": [], # Initial empty keywords "lastActivityBy": user_id, # Owner is the last active initially "createdAt": now, "updatedAt": now } # Insert the new project document result = db.projects.insert_one(project_data) project_id = str(result.inserted_id) # Return success response with project ID and passkey return jsonify({ "message": "Project created successfully.", "project_id": project_id, "passkey": passkey # Return passkey so owner knows it }), 201 # 201 Created status code except Exception as e: # Log the detailed error for debugging logger.error(f"Error creating project for user {user_id}: {e}", exc_info=True) # Return a generic error message to the client return jsonify({"message": "An error occurred while creating the project."}), 500 @bp.route('/', methods=['GET']) # Path relative to prefix @token_required def get_projects(current_user): """ Retrieve a summary list (ID, name, updatedAt) of projects where the authenticated user is either the owner or a collaborator. Uses ProjectListSchema for output serialization. Sorted by last update time descending. """ logger = _get_logger() # Validate user object from token if not current_user or not current_user.get("_id"): return jsonify({"message": "Internal authorization error."}), 500 try: user_id = ObjectId(current_user["_id"]) except (InvalidId, TypeError) as e: logger.error(f"User ID conversion error in get_projects: {e}") return jsonify({"message": "Invalid user ID format in token."}), 400 # Check dependencies if not mongo: return jsonify({"message": "Database connection not available."}), 500 if not ProjectListSchema: return jsonify({"message": "Server configuration error: Schema unavailable."}), 500 try: db = mongo.db # Query for projects owned by or collaborated on by the user projects_cursor = db.projects.find( { "$or": [ {"ownerId": user_id}, {"collaborators": user_id} # Check if user ID is in the collaborators array ] }, # Projection: only retrieve fields needed by the ProjectListSchema {"name": 1, "updatedAt": 1, "_id": 1} ).sort("updatedAt", -1) # Sort by most recently updated project_docs = list(projects_cursor) # Convert cursor to list # --- Serialize results using the schema --- output_schema = ProjectListSchema(many=True) # Schema handles ObjectId and datetime conversion serialized_result = output_schema.dump(project_docs) # Return the serialized list of project summaries return jsonify({"projects": serialized_result}), 200 except Exception as e: logger.error(f"Error fetching projects for user {user_id}: {e}", exc_info=True) # Use a generic error message for the client return jsonify({"message": "An error occurred while fetching projects."}), 500 @bp.route('/', methods=['GET']) # Path relative to prefix @token_required def get_project_detail(current_user, project_id): """ Retrieve detailed information for a specific project by its ID. Uses ProjectSchema for output serialization. Verifies user access (owner or collaborator). """ logger = _get_logger() # Validate user object from token if not current_user or not current_user.get("_id"): return jsonify({"message": "Internal authorization error."}), 500 try: user_id = ObjectId(current_user["_id"]) except (InvalidId, TypeError) as e: logger.error(f"User ID conversion error in get_project_detail: {e}") return jsonify({"message": "Invalid user ID format in token."}), 400 # Check dependencies if not mongo: return jsonify({"message": "Database connection not available."}), 500 if not ProjectSchema: return jsonify({"message": "Server configuration error: Schema unavailable."}), 500 try: db = mongo.db # Validate the provided project ID format try: obj_project_id = ObjectId(project_id) except InvalidId: return jsonify({"message": "Invalid project ID format."}), 400 # Find the project by ID project_doc = db.projects.find_one({"_id": obj_project_id}) if not project_doc: return jsonify({"message": "Project not found."}), 404 # 404 Not Found # Verify ownership or collaboration status for access control owner_id = project_doc.get("ownerId") collaborators = project_doc.get("collaborators", []) if not owner_id: # Check for data integrity logger.error(f"Project {project_id} is missing ownerId.") return jsonify({"message": "Project data integrity issue."}), 500 if owner_id != user_id and user_id not in collaborators: return jsonify({"message": "Access denied to this project."}), 403 # 403 Forbidden # --- Serialize results using the schema --- output_schema = ProjectSchema() # Schema handles ObjectId, datetime, nested keywords, and field selection serialized_result = output_schema.dump(project_doc) return jsonify(serialized_result), 200 except Exception as e: logger.error(f"Error fetching project detail for {project_id}: {e}", exc_info=True) return jsonify({"message": "An error occurred while fetching project details."}), 500 @bp.route('/', methods=['PUT']) # Path relative to prefix @token_required def update_project(current_user, project_id): """ Update details of an existing project. Uses ProjectUpdateSchema for input validation. Only allows updating specific fields: name, collaborators, topic, description, keywords. Requires the authenticated user to be the project owner. Returns the updated project details using ProjectSchema. """ logger = _get_logger() # Validate user object from token if not current_user or not current_user.get("_id"): return jsonify({"message": "Internal authorization error."}), 500 try: user_id = ObjectId(current_user["_id"]) except (InvalidId, TypeError) as e: logger.error(f"User ID conversion error in update_project: {e}") return jsonify({"message": "Invalid user ID format in token."}), 400 # Check dependencies if not mongo: return jsonify({"message": "Database connection not available."}), 500 if not ProjectUpdateSchema or not ProjectSchema or not ValidationError: return jsonify({"message": "Server configuration error: Schema unavailable."}), 500 # Get and validate JSON data using the schema json_data = request.get_json() or {} schema = ProjectUpdateSchema() try: # Load validates allowed fields and their types (like collaborators list of strings) validated_data = schema.load(json_data) except ValidationError as err: logger.warning(f"Update project validation failed: {err.messages}") return jsonify(err.messages), 422 # If validation passed but no valid fields were provided if not validated_data: return jsonify({"message": "No valid fields provided for update."}), 400 try: db = mongo.db # Validate project ID format try: obj_project_id = ObjectId(project_id) except InvalidId: return jsonify({"message": "Invalid project ID format."}), 400 # Find the project project = db.projects.find_one({"_id": obj_project_id}, {"ownerId": 1}) # Fetch ownerId for check if not project: return jsonify({"message": "Project not found."}), 404 # Verify ownership for update permission owner_id = project.get("ownerId") if not owner_id: logger.error(f"Project {project_id} is missing ownerId during update.") return jsonify({"message": "Project data integrity issue."}), 500 if owner_id != user_id: return jsonify({"message": "Only the project owner can update this project."}), 403 # --- Prepare Update Fields based on validated data --- update_fields = {} # Convert collaborator strings back to ObjectIds if present if "collaborators" in validated_data: try: update_fields["collaborators"] = [ObjectId(cid) for cid in validated_data["collaborators"]] # Optional: Add check here to ensure collaborator IDs exist and are not the owner except (InvalidId, TypeError): # This should ideally be caught by schema validation if using _validate_object_id return jsonify({"message": "Invalid collaborator ID format received."}), 400 # Copy other validated fields directly for field in ["name", "topic", "description", "keywords"]: if field in validated_data: update_fields[field] = validated_data[field] # Always update the 'updatedAt' timestamp update_fields["updatedAt"] = datetime.datetime.now(datetime.timezone.utc) # Note: lastActivityBy is NOT updated here. # Perform the update operation result = db.projects.update_one({"_id": obj_project_id}, {"$set": update_fields}) # Check if the update was successful if result.matched_count == 1: # Retrieve the updated project document to return it updated_project_doc = db.projects.find_one({"_id": obj_project_id}) if updated_project_doc: # Serialize the updated document using the detail schema output_schema = ProjectSchema() serialized_project = output_schema.dump(updated_project_doc) return jsonify({"message": "Project updated successfully.", "project": serialized_project}), 200 else: logger.warning(f"Project {project_id} updated but could not be retrieved.") return jsonify({"message": "Project updated successfully, but failed to retrieve updated data."}), 200 else: # Matched count was 0 return jsonify({"message": "Project update failed (document not found)."}), 404 except Exception as e: logger.error(f"Error updating project {project_id}: {e}", exc_info=True) return jsonify({"message": "An error occurred while updating the project."}), 500 @bp.route('/', methods=['DELETE']) # Path relative to prefix @token_required def delete_project(current_user, project_id): """ Delete a project and cascade deletion of associated URLs, activity logs, and dialogs. Requires the authenticated user to be the project owner. (No schema needed for input/output here) """ logger = _get_logger() # Validate user object from token if not current_user or not current_user.get("_id"): return jsonify({"message": "Internal authorization error."}), 500 try: user_id = ObjectId(current_user["_id"]) except (InvalidId, TypeError) as e: logger.error(f"User ID conversion error in delete_project: {e}") return jsonify({"message": "Invalid user ID format in token."}), 400 if not mongo: return jsonify({"message": "Database connection not available."}), 500 try: db = mongo.db # Validate project ID format try: obj_project_id = ObjectId(project_id) except InvalidId: return jsonify({"message": "Invalid project ID format."}), 400 # Find the project project = db.projects.find_one({"_id": obj_project_id}, {"ownerId": 1}) if not project: return jsonify({"message": "Project not found."}), 404 # Verify ownership for delete permission owner_id = project.get("ownerId") if not owner_id: logger.error(f"Project {project_id} is missing ownerId during delete.") return jsonify({"message": "Project data integrity issue."}), 500 if owner_id != user_id: return jsonify({"message": "Only the project owner can delete this project."}), 403 # --- Perform Deletions (Consider Transactions if available/needed) --- # 1. Delete the project document itself delete_project_result = db.projects.delete_one({"_id": obj_project_id}) if delete_project_result.deleted_count == 0: logger.warning(f"Project {project_id} found but delete_one removed 0 documents.") return jsonify({"message": "Project deletion failed (already deleted?)."}), 404 # 2. Cascade delete associated URLs delete_urls_result = db.urls.delete_many({"projectId": obj_project_id}) logger.info(f"Deleted {delete_urls_result.deleted_count} URLs for project {project_id}") # 3. Cascade delete associated activity logs delete_activity_result = db.project_activity.delete_many({"projectId": obj_project_id}) logger.info(f"Deleted {delete_activity_result.deleted_count} activity logs for project {project_id}") # 4. Cascade delete associated dialog sessions delete_dialog_result = db.dialog_activity.delete_many({"projectId": obj_project_id}) logger.info(f"Deleted {delete_dialog_result.deleted_count} dialog sessions for project {project_id}") # --- End Deletions --- return jsonify({"message": "Project and associated data deleted successfully."}), 200 # 200 OK or 204 No Content except Exception as e: logger.error(f"Error deleting project {project_id}: {e}", exc_info=True) return jsonify({"message": "An error occurred while deleting the project."}), 500 @bp.route('//info', methods=['GET']) # Path relative to prefix @token_required def get_project_info(current_user, project_id): """ Retrieve basic informational fields for a project. Uses ProjectSchema for output serialization (implicitly selects fields). Verifies user access (owner or collaborator). """ logger = _get_logger() # Validate user object from token if not current_user or not current_user.get("_id"): return jsonify({"message": "Internal authorization error."}), 500 try: user_id = ObjectId(current_user["_id"]) except (InvalidId, TypeError) as e: logger.error(f"User ID conversion error in get_project_info: {e}") return jsonify({"message": "Invalid user ID format in token."}), 400 # Check dependencies if not mongo: return jsonify({"message": "Database connection not available."}), 500 if not ProjectSchema: return jsonify({"message": "Server configuration error: Schema unavailable."}), 500 try: db = mongo.db # Validate project ID format try: obj_project_id = ObjectId(project_id) except InvalidId: return jsonify({"message": "Invalid project ID format."}), 400 # Find the project, projecting only necessary fields + access control fields # Schema will handle final field selection for output project_doc = db.projects.find_one( {"_id": obj_project_id} # Fetch full doc for schema dump # {"name": 1, "topic": 1, "description": 1, "keywords": 1, "summary": 1, "ownerId": 1, "collaborators": 1} ) if not project_doc: return jsonify({"message": "Project not found."}), 404 # Verify access owner_id = project_doc.get("ownerId") collaborators = project_doc.get("collaborators", []) if not owner_id: logger.error(f"Project {project_id} is missing ownerId in get_project_info.") return jsonify({"message": "Project data integrity issue."}), 500 if owner_id != user_id and user_id not in collaborators: return jsonify({"message": "Access denied to this project's info."}), 403 # --- Serialize using ProjectSchema --- # The schema definition controls which fields are included in the output output_schema = ProjectSchema() serialized_result = output_schema.dump(project_doc) # The ProjectSchema includes more than just the 'info' fields, # adjust schema or create ProjectInfoSchema if only specific fields are desired. # For now, returning the standard ProjectSchema output. return jsonify(serialized_result), 200 except Exception as e: logger.error(f"Error getting project info for {project_id}: {e}", exc_info=True) return jsonify({"message": "An error occurred while retrieving project info."}), 500 @bp.route('//recalc_keywords', methods=['PUT']) # Path relative to prefix @token_required def recalc_project_keywords(current_user, project_id): """ Triggers an asynchronous Celery task to recalculate project keywords. Verifies user access (owner or collaborator). (No schema needed for input/output here) """ logger = _get_logger() # Validate user object from token if not current_user or not current_user.get("_id"): return jsonify({"message": "Internal authorization error."}), 500 try: user_id = ObjectId(current_user["_id"]) user_id_str = str(user_id) # Keep string version for Celery task if needed except (InvalidId, TypeError) as e: logger.error(f"User ID conversion error in recalc_project_keywords: {e}") return jsonify({"message": "Invalid user ID format in token."}), 400 if not mongo: return jsonify({"message": "Database connection not available."}), 500 try: db = mongo.db # Validate project ID format try: obj_project_id = ObjectId(project_id) except InvalidId: return jsonify({"message": "Invalid project ID format."}), 400 # Verify project exists and user has access before queueing task project = db.projects.find_one( {"_id": obj_project_id}, {"ownerId": 1, "collaborators": 1} # Only fetch fields needed for access check ) if not project: return jsonify({"message": "Project not found."}), 404 owner_id = project.get("ownerId") collaborators = project.get("collaborators", []) if not owner_id: logger.error(f"Project {project_id} is missing ownerId in recalc_keywords.") return jsonify({"message": "Project data integrity issue."}), 500 if owner_id != user_id and user_id not in collaborators: return jsonify({"message": "Access denied to trigger keyword recalculation for this project."}), 403 # --- Queue the Celery Task --- try: # Call the .delay() method on the imported Celery task task_result = async_recalc_project_keywords.delay(project_id, user_id_str) logger.info(f"Queued keyword recalc task {task_result.id} for project {project_id}") # Return 202 Accepted status code to indicate task was queued return jsonify({"message": "Project keywords recalculation task queued successfully."}), 202 except NameError: logger.error("Celery task 'async_recalc_project_keywords' is not defined or imported correctly.") return jsonify({"message": "Server configuration error: Keyword recalculation feature unavailable."}), 500 except Exception as e: # Catch errors related to Celery connection or queueing logger.error(f"Error queueing recalc keywords task for project {project_id}: {e}", exc_info=True) return jsonify({"message": "An error occurred while queueing the keywords recalculation task."}), 500 except Exception as e: # Catch general errors before task queueing logger.error(f"Error in recalc_project_keywords endpoint for project {project_id}: {e}", exc_info=True) return jsonify({"message": "An internal error occurred before queueing the task."}), 500 @bp.route('//summarize', methods=['PUT']) # Path relative to prefix @token_required def summarize_project(current_user, project_id): """ Generates a summary for the project using its associated URL knowledge base and an external LLM (Gemini). Updates the project's summary field. Requires the user to have a selected Gemini API key configured. Verifies user access (owner or collaborator). (No schema needed for input, output is summary string) """ logger = _get_logger() # Validate user object from token if not current_user or not current_user.get("_id"): return jsonify({"message": "Internal authorization error."}), 500 try: user_id = ObjectId(current_user["_id"]) except (InvalidId, TypeError) as e: logger.error(f"User ID conversion error in summarize_project: {e}") return jsonify({"message": "Invalid user ID format in token."}), 400 # Check dependencies if not mongo: return jsonify({"message": "Database connection not available."}), 500 if not genai or not google_exceptions: return jsonify({"message": "Gemini API library not available."}), 500 try: db = mongo.db # Validate project ID format try: obj_project_id = ObjectId(project_id) except InvalidId: return jsonify({"message": "Invalid project ID format."}), 400 # Verify project exists and user has access project = db.projects.find_one( {"_id": obj_project_id}, {"ownerId": 1, "collaborators": 1} # Only fetch fields needed for access check ) if not project: return jsonify({"message": "Project not found."}), 404 owner_id = project.get("ownerId") collaborators = project.get("collaborators", []) if not owner_id: logger.error(f"Project {project_id} is missing ownerId in summarize_project.") return jsonify({"message": "Project data integrity issue."}), 500 if owner_id != user_id and user_id not in collaborators: return jsonify({"message": "Access denied to summarize this project."}), 403 # --- Check for User's Gemini API Key --- api_doc = db.api_list.find_one({"uid": user_id, "selected": True, "name": "Gemini"}) if not (api_doc and api_doc.get("key")): return jsonify({"message": "Summarization requires a selected Gemini API key. Please configure it in API Keys."}), 400 # 400 Bad Request - missing prereq gemini_key = api_doc.get("key") # --- Generate Knowledge Base and Prompt --- # Use the imported helper function from the dialog blueprint kb_message = generate_knowledge_base_message(obj_project_id) # Pass ObjectId if not kb_message or kb_message.startswith("Error:") : # Handle error from helper logger.warning(f"Knowledge base generation failed or was empty for project {project_id}. KB: {kb_message}") kb_message = "No external knowledge base content available for this project." # Fallback # Construct the prompt for Gemini prompt = ( f"You are an expert research assistant tasked with summarizing a project. " f"Below is the external knowledge base compiled from websites associated with this project.\n\n" f"--- External Knowledge Base ---\n{kb_message}\n--- End Knowledge Base ---\n\n" f"Based ONLY on the provided knowledge base (do not use external information), please generate a concise and comprehensive summary " f"of the project's main focus, key topics, and potential research directions. Aim for approximately 300 words, maximum 400 words." ) # --- Call Gemini API --- summary_text = "[Summary generation failed]" # Default try: genai.configure(api_key=gemini_key) # Use the constant defined earlier or get from config model = genai.GenerativeModel(current_app.config["GEMINI_MODEL_NAME"]) gemini_input = [{"role": "user", "parts": [{"text": prompt}]}] # Consider adding safety settings if needed llm_response = model.generate_content(gemini_input) # Extract text, handling potential blocks try: summary_text = llm_response.text except ValueError: logger.warning(f"Gemini response for project {project_id} summary may have been blocked. Feedback: {llm_response.prompt_feedback}") summary_text = "[Summary generation blocked or failed]" except google_exceptions.PermissionDenied as ex: logger.warning(f"Gemini Permission Denied for user {user_id} during summarization: {ex}") return jsonify({"message": "Gemini API Error: Invalid API key or insufficient permissions."}), 403 except google_exceptions.ResourceExhausted as ex: logger.warning(f"Gemini Resource Exhausted for user {user_id} during summarization: {ex}") return jsonify({"message": "Gemini API Error: Rate limit or quota exceeded."}), 429 except google_exceptions.GoogleAPIError as ex: logger.error(f"Gemini API communication error during summarization for project {project_id}: {ex}", exc_info=True) return jsonify({"message": "An error occurred while communicating with the Gemini API."}), 503 except Exception as e: logger.error(f"Unexpected error during Gemini call setup/execution for project {project_id} summary: {e}", exc_info=True) return jsonify({"message": "Internal server error during LLM communication."}), 500 # Check if the summary is empty after potential blocking if not summary_text or summary_text == "[Summary generation blocked or failed]": return jsonify({"message": "Failed to generate summary (LLM returned empty or blocked response)."}), 500 # --- Update Project Summary in DB --- try: update_result = db.projects.update_one( {"_id": obj_project_id}, {"$set": {"summary": summary_text, "updatedAt": datetime.datetime.now(datetime.timezone.utc)}} ) if update_result.matched_count == 0: # Project deleted between find and update? logger.warning(f"Project {project_id} not found during summary update.") return jsonify({"message": "Project not found while saving summary."}), 404 # Return success response with the generated summary return jsonify({"message": "Project summary generated and saved successfully.", "summary": summary_text}), 200 except Exception as e: logger.error(f"Error updating project summary in DB for {project_id}: {e}", exc_info=True) # Inform user summary was generated but not saved return jsonify({"message": "Summary generated but failed to save to project.", "summary": summary_text}), 500 except Exception as e: # Catch-all for errors before API call or DB update logger.error(f"Error in summarize_project endpoint for project {project_id}: {e}", exc_info=True) return jsonify({"message": "An internal error occurred during project summarization."}), 500