716 lines
33 KiB
Python
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
|
|
|