Initial Commit
This commit is contained in:
13
backend_flask/myapp/projects/__init__.py
Normal file
13
backend_flask/myapp/projects/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# myapp/projects/__init__.py
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
# Define the Blueprint instance for the projects module.
|
||||
# 'projects' is the unique name for this blueprint.
|
||||
# url_prefix='/api/projects' will be prepended to all routes defined in this blueprint.
|
||||
bp = Blueprint('projects', __name__, url_prefix='/api/projects')
|
||||
|
||||
# Import the routes module.
|
||||
# This connects the routes defined in routes.py to the 'bp' instance.
|
||||
# This import MUST come AFTER the Blueprint 'bp' is defined.
|
||||
from . import projects_routes
|
||||
715
backend_flask/myapp/projects/projects_routes.py
Normal file
715
backend_flask/myapp/projects/projects_routes.py
Normal file
@@ -0,0 +1,715 @@
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user