2025-06-09 17:53:19 +08:00

716 lines
33 KiB
Python

# 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('/<string:project_id>', 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('/<string:project_id>', 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('/<string:project_id>', 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('/<string:project_id>/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('/<string:project_id>/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('/<string:project_id>/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