Initial Commit
This commit is contained in:
13
backend_flask/myapp/dialog/__init__.py
Normal file
13
backend_flask/myapp/dialog/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# myapp/dialog/__init__.py
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
# Define the Blueprint instance for the dialog module.
|
||||
# 'dialog' is the unique name for this blueprint.
|
||||
# url_prefix='/api/dialog' will be prepended to all routes defined in this blueprint.
|
||||
bp = Blueprint('dialog', __name__, url_prefix='/api/dialog')
|
||||
|
||||
# Import the routes module.
|
||||
# This connects the routes defined in routes.py (including helper functions) to the 'bp' instance.
|
||||
# This import MUST come AFTER the Blueprint 'bp' is defined.
|
||||
from . import dialog_routes
|
||||
787
backend_flask/myapp/dialog/dialog_routes.py
Normal file
787
backend_flask/myapp/dialog/dialog_routes.py
Normal file
@@ -0,0 +1,787 @@
|
||||
# myapp/dialog/dialog_routes.py
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import logging
|
||||
from flask import request, jsonify, current_app, has_app_context # Flask utilities
|
||||
from bson.objectid import ObjectId, InvalidId # For MongoDB ObjectIds
|
||||
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
|
||||
from ..utils import token_required # Import the authentication decorator
|
||||
except ImportError:
|
||||
# Fallback or error handling if imports fail
|
||||
print("Warning: Could not import mongo or token_required in dialog/dialog_routes.py.")
|
||||
mongo = None
|
||||
# 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 (
|
||||
DialogCreateSchema, DialogSendMessageSchema,
|
||||
DialogSchema, DialogSummarySchema
|
||||
)
|
||||
from marshmallow import ValidationError
|
||||
except ImportError:
|
||||
print("Warning: Could not import Dialog schemas or ValidationError in dialog/dialog_routes.py.")
|
||||
DialogCreateSchema = None
|
||||
DialogSendMessageSchema = None
|
||||
DialogSchema = None
|
||||
DialogSummarySchema = None
|
||||
ValidationError = None # Define ValidationError as None if import fails
|
||||
|
||||
|
||||
# --- External API and Langchain Imports ---
|
||||
# Keep these imports conditional to avoid errors if libraries are 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. Gemini functionality will fail.")
|
||||
genai = None
|
||||
google_exceptions = None
|
||||
|
||||
try:
|
||||
from langchain_community.embeddings import OpenAIEmbeddings
|
||||
from langchain_community.vectorstores import FAISS
|
||||
from langchain.docstore.document import Document
|
||||
except ImportError:
|
||||
print("Warning: Langchain components not installed. Vector store functionality will fail.")
|
||||
OpenAIEmbeddings = None
|
||||
FAISS = None
|
||||
Document = None
|
||||
|
||||
# --- Constants ---
|
||||
# Adjust model name if needed, potentially move to config
|
||||
MAX_HISTORY_MESSAGES = 20 # Max messages to keep in history for context (if applicable, not used in current Gemini call)
|
||||
|
||||
# --- Helper to get logger safely ---
|
||||
def _get_logger():
|
||||
if has_app_context():
|
||||
return current_app.logger
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
|
||||
##################################################
|
||||
# Helper Functions (kept within this module)
|
||||
##################################################
|
||||
|
||||
# --- Knowledge Base Helpers ---
|
||||
# (generate_knowledge_base_message, update_project_knowledge, process_api_response_and_update_knowledge - unchanged from previous version)
|
||||
def generate_knowledge_base_message(project_id):
|
||||
"""
|
||||
Retrieves all URL documents for the given project and formats them
|
||||
into a single knowledge base message string for context.
|
||||
|
||||
Args:
|
||||
project_id (ObjectId): The ObjectId of the project.
|
||||
|
||||
Returns:
|
||||
str: A formatted string containing knowledge base entries, or an empty string on error.
|
||||
Returns "No external knowledge available." if no URLs are found.
|
||||
"""
|
||||
logger = _get_logger()
|
||||
if not mongo:
|
||||
logger.error("Mongo extension not available in generate_knowledge_base_message.")
|
||||
return "" # Cannot proceed without DB connection
|
||||
|
||||
try:
|
||||
# Ensure project_id is ObjectId
|
||||
if not isinstance(project_id, ObjectId):
|
||||
project_id = ObjectId(project_id)
|
||||
|
||||
# Find all URL documents linked to the project ID
|
||||
urls_cursor = mongo.db.urls.find({"projectId": project_id})
|
||||
knowledge_entries = []
|
||||
for doc in urls_cursor:
|
||||
# Format keywords with percentages
|
||||
keywords_list = [f"{kw.get('word', '')}({kw.get('percentage', 'N/A')}%)" for kw in doc.get("keywords", [])]
|
||||
# Aggregate relevant fields into a string entry
|
||||
aggregated = (
|
||||
f"Title: {doc.get('title', 'N/A')}\n"
|
||||
f"URL: {doc.get('url', 'N/A')}\n"
|
||||
# f"Starred: {doc.get('starred', False)}\n" # Optionally include starred status
|
||||
f"Note: {doc.get('note', 'N/A')}\n"
|
||||
f"Keywords: {', '.join(keywords_list) if keywords_list else 'N/A'}\n"
|
||||
f"Summary: {doc.get('summary', 'N/A')}"
|
||||
)
|
||||
knowledge_entries.append(aggregated)
|
||||
|
||||
# Handle case where no URLs are found
|
||||
if not knowledge_entries:
|
||||
return "No external knowledge available for this project."
|
||||
|
||||
# Combine entries and truncate if necessary
|
||||
combined = "\n\n---\n\n".join(knowledge_entries)
|
||||
# Use Flask config for max length if available, otherwise default
|
||||
max_length = current_app.config.get('KNOWLEDGE_BASE_MAX_LENGTH', 4000) if has_app_context() else 4000
|
||||
if len(combined) > max_length:
|
||||
combined = combined[:max_length] + " ... [truncated]"
|
||||
return combined
|
||||
|
||||
except InvalidId:
|
||||
logger.error(f"Invalid project_id format passed to generate_knowledge_base_message: {project_id}")
|
||||
return "Error: Invalid project identifier."
|
||||
except AttributeError:
|
||||
logger.error("PyMongo extension not initialized or available.")
|
||||
return "Error: Database configuration issue."
|
||||
except Exception as e:
|
||||
# Log the error with project ID for easier debugging
|
||||
logger.error(f"Error generating knowledge base message for project {project_id}: {e}", exc_info=True)
|
||||
return "" # Return empty string on generic error
|
||||
|
||||
|
||||
def update_project_knowledge(project_id):
|
||||
"""
|
||||
Updates the project's 'summary' field with a condensed version of its knowledge base.
|
||||
This acts as a cache or snapshot for quick reference.
|
||||
|
||||
Args:
|
||||
project_id (ObjectId): The ObjectId of the project.
|
||||
"""
|
||||
logger = _get_logger()
|
||||
if not mongo:
|
||||
logger.error("Mongo extension not available in update_project_knowledge.")
|
||||
return
|
||||
try:
|
||||
# Ensure project_id is ObjectId
|
||||
if not isinstance(project_id, ObjectId):
|
||||
project_id = ObjectId(project_id)
|
||||
|
||||
knowledge_message = generate_knowledge_base_message(project_id)
|
||||
# Condense the message for storage (e.g., first 1000 chars)
|
||||
condensed = knowledge_message[:1000] if len(knowledge_message) > 1000 else knowledge_message
|
||||
|
||||
# Update the project document in the 'projects' collection
|
||||
mongo.db.projects.update_one(
|
||||
{"_id": project_id},
|
||||
{"$set": {"summary": condensed, "updatedAt": datetime.datetime.now(datetime.timezone.utc)}}
|
||||
)
|
||||
except InvalidId:
|
||||
logger.error(f"Invalid project_id format passed to update_project_knowledge: {project_id}")
|
||||
except AttributeError:
|
||||
logger.error("PyMongo extension not initialized or available.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating project knowledge cache for {project_id}: {e}", exc_info=True)
|
||||
|
||||
|
||||
def process_api_response_and_update_knowledge(api_response, project_id):
|
||||
"""
|
||||
Placeholder function to process LLM responses. Currently updates project knowledge cache.
|
||||
"""
|
||||
# For now, simply update the cached summary in the project document
|
||||
update_project_knowledge(project_id)
|
||||
# Future enhancements could go here
|
||||
|
||||
|
||||
# --- Vector Store Helpers ---
|
||||
def build_vector_knowledge_base(project_id, query, k=3):
|
||||
"""
|
||||
Builds a vector index (FAISS) from project URL content and retrieves top-k relevant documents.
|
||||
|
||||
Args:
|
||||
project_id (ObjectId): The ObjectId of the project.
|
||||
query (str): The user query for similarity search.
|
||||
k (int): The number of top similar documents to retrieve.
|
||||
|
||||
Returns:
|
||||
List[Document]: A list of LangChain Document objects, or an empty list on error/no data.
|
||||
"""
|
||||
logger = _get_logger()
|
||||
# Check if necessary components are available
|
||||
if not mongo or not OpenAIEmbeddings or not FAISS or not Document:
|
||||
logger.error("Missing dependencies (Mongo, Langchain) for build_vector_knowledge_base.")
|
||||
return []
|
||||
|
||||
try:
|
||||
# Ensure project_id is ObjectId
|
||||
if not isinstance(project_id, ObjectId):
|
||||
project_id = ObjectId(project_id)
|
||||
|
||||
# Fetch URL documents from MongoDB
|
||||
urls_cursor = mongo.db.urls.find({"projectId": project_id})
|
||||
texts = []
|
||||
metadatas = []
|
||||
|
||||
for doc in urls_cursor:
|
||||
# Aggregate text content for embedding
|
||||
keywords_list = [f"{kw.get('word', '')}({kw.get('percentage', 'N/A')}%)" for kw in doc.get("keywords", [])]
|
||||
aggregated = (
|
||||
f"Title: {doc.get('title', 'N/A')}\n"
|
||||
f"URL: {doc.get('url', 'N/A')}\n"
|
||||
# f"Starred: {doc.get('starred', False)}\n" # Optionally include more fields
|
||||
f"Note: {doc.get('note', 'N/A')}\n"
|
||||
f"Keywords: {', '.join(keywords_list) if keywords_list else 'N/A'}\n"
|
||||
f"Summary: {doc.get('summary', 'N/A')}"
|
||||
)
|
||||
texts.append(aggregated)
|
||||
# Store relevant metadata alongside the text
|
||||
metadatas.append({"url": doc.get("url", ""), "title": doc.get("title", ""), "doc_id": str(doc["_id"])})
|
||||
|
||||
# If no text content found, return empty list
|
||||
if not texts:
|
||||
logger.info(f"No URL text content found for project {project_id} to build vector base.")
|
||||
return []
|
||||
|
||||
# Initialize embeddings model (ensure OPENAI_API_KEY is set in environment or config)
|
||||
try:
|
||||
# Check if OPENAI_API_KEY exists (more robust check)
|
||||
openai_api_key = os.environ.get("OPENAI_API_KEY") or (current_app.config.get("OPENAI_API_KEY") if has_app_context() else None)
|
||||
if not openai_api_key:
|
||||
raise ValueError("OPENAI_API_KEY environment variable or Flask config not set.")
|
||||
embeddings = OpenAIEmbeddings(openai_api_key=openai_api_key)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize OpenAIEmbeddings: {e}. Check API key.", exc_info=False) # Avoid logging key
|
||||
return []
|
||||
|
||||
|
||||
# Build FAISS vector store from the texts and metadata
|
||||
vectorstore = FAISS.from_texts(texts=texts, embedding=embeddings, metadatas=metadatas)
|
||||
|
||||
# Perform similarity search
|
||||
docs = vectorstore.similarity_search(query, k=k)
|
||||
return docs
|
||||
|
||||
except InvalidId:
|
||||
logger.error(f"Invalid project_id format passed to build_vector_knowledge_base: {project_id}")
|
||||
return []
|
||||
except AttributeError:
|
||||
logger.error("PyMongo or Langchain components not initialized or available.")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Error building vector knowledge base for project {project_id}: {e}", exc_info=True)
|
||||
return [] # Return empty list on error
|
||||
|
||||
|
||||
def build_vector_based_prompt_with_knowledge(user_message, project_id):
|
||||
"""
|
||||
Constructs a prompt for the LLM, incorporating context from vector search results.
|
||||
|
||||
Args:
|
||||
user_message (str): The user's latest message/query.
|
||||
project_id (ObjectId): The ObjectId of the project.
|
||||
|
||||
Returns:
|
||||
str: The formatted prompt string including retrieved knowledge.
|
||||
"""
|
||||
# Retrieve top 3 relevant documents using vector search
|
||||
retrieved_docs = build_vector_knowledge_base(project_id, user_message, k=3)
|
||||
|
||||
# Format the retrieved knowledge for inclusion in the prompt
|
||||
if retrieved_docs:
|
||||
# Join the page_content of each retrieved LangChain Document
|
||||
knowledge_text = "\n\n---\n\n".join([f"Source URL: {doc.metadata.get('url', 'N/A')}\n{doc.page_content}" for doc in retrieved_docs])
|
||||
else:
|
||||
knowledge_text = "No relevant external knowledge found via vector search for this query."
|
||||
|
||||
# Construct the final prompt with instructions, knowledge, and user query
|
||||
# Make prompt more specific about using ONLY the provided knowledge
|
||||
prompt = (
|
||||
"You are an expert research assistant. Analyze the following retrieved documents, which contain information "
|
||||
"(titles, URLs, notes, keywords, summaries) from websites related to the current research project. "
|
||||
"Base your response *only* on this provided information and the user's query.\n\n"
|
||||
"Common user questions might involve:\n"
|
||||
"- Summarizing key topics from the retrieved documents.\n"
|
||||
"- Suggesting research directions based *only* on the retrieved documents.\n"
|
||||
"- Recommending specific URLs *from the retrieved documents* that are most relevant.\n"
|
||||
"- Identifying potentially redundant information *within the retrieved documents*.\n\n"
|
||||
"--- Relevant Retrieved Knowledge ---\n"
|
||||
f"{knowledge_text}\n"
|
||||
"--- End Retrieved Knowledge ---\n\n"
|
||||
"User Query:\n"
|
||||
f"{user_message}\n\n"
|
||||
"Based strictly on the retrieved knowledge and the user query, provide your analysis and recommendations:"
|
||||
)
|
||||
return prompt
|
||||
|
||||
|
||||
# --- Gemini Message Formatting Helper (Not currently used by send_dialog_message) ---
|
||||
def format_messages_for_gemini(db_messages, max_history=MAX_HISTORY_MESSAGES):
|
||||
"""
|
||||
Converts dialog history from DB format to Gemini API format.
|
||||
Handles role mapping ('system' -> 'model') and ensures role alternation.
|
||||
"""
|
||||
logger = _get_logger()
|
||||
contents = []
|
||||
last_role = None
|
||||
recent_messages = db_messages[-max_history:] # Get the most recent messages
|
||||
|
||||
for msg in recent_messages:
|
||||
gemini_role = "model" if msg.get("role") == "system" else "user"
|
||||
if gemini_role == last_role:
|
||||
logger.warning(f"Skipping consecutive message of role '{gemini_role}' in formatting.")
|
||||
continue
|
||||
contents.append({
|
||||
"role": gemini_role,
|
||||
"parts": [{"text": msg.get("content", "")}]
|
||||
})
|
||||
last_role = gemini_role
|
||||
|
||||
if contents and contents[-1]["role"] != "user":
|
||||
logger.warning("Formatted history for Gemini does not end with a 'user' message.")
|
||||
return contents
|
||||
|
||||
|
||||
##################################################
|
||||
# Dialog API Endpoints
|
||||
##################################################
|
||||
|
||||
# Note: Routes use paths relative to the '/api/dialog' prefix.
|
||||
|
||||
@bp.route('/', methods=['POST']) # Path relative to prefix
|
||||
@token_required
|
||||
def create_dialog_session(current_user):
|
||||
"""
|
||||
Creates a new dialog session associated with a project.
|
||||
Uses DialogCreateSchema for input validation.
|
||||
Expects JSON: { "projectId": "<ObjectId_string>", "sessionId": "<optional_string>", "startMessage": "<optional_string>" }
|
||||
Determines the LLM provider based on the user's selected API key.
|
||||
"""
|
||||
logger = _get_logger()
|
||||
# Validate user
|
||||
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 create_dialog_session: {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 DialogCreateSchema 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 = DialogCreateSchema()
|
||||
try:
|
||||
validated_data = schema.load(json_data)
|
||||
except ValidationError as err:
|
||||
logger.warning(f"Create dialog session validation failed: {err.messages}")
|
||||
return jsonify(err.messages), 422 # Return validation errors
|
||||
|
||||
# Extract validated data
|
||||
project_id_str = validated_data['projectId'] # Already validated as ObjectId string by schema
|
||||
session_id = validated_data.get("sessionId", "") # Optional
|
||||
start_message = validated_data.get("startMessage", "").strip() # Optional
|
||||
|
||||
try:
|
||||
# Convert project ID
|
||||
project_obj_id = ObjectId(project_id_str) # Conversion should succeed due to schema validation
|
||||
|
||||
# Find the user's selected API key
|
||||
db = mongo.db
|
||||
selected_api = db.api_list.find_one({"uid": user_id, "selected": True})
|
||||
if not selected_api:
|
||||
return jsonify({"message": "User has no selected API provider. Please select one in API Keys."}), 400
|
||||
|
||||
provider = selected_api.get("name")
|
||||
api_key_exists = bool(selected_api.get("key")) # Check if key value exists
|
||||
|
||||
# Validate provider and key presence
|
||||
allowed_providers = ["Gemini", "Deepseek", "Chatgpt"] # Consider from config
|
||||
if provider not in allowed_providers:
|
||||
return jsonify({"message": f"Selected provider '{provider}' is not supported."}), 400
|
||||
if not api_key_exists:
|
||||
return jsonify({"message": f"API key value missing for selected provider '{provider}'."}), 400
|
||||
|
||||
# Verify project exists and user has access
|
||||
project = db.projects.find_one({"_id": project_obj_id}, {"ownerId": 1, "collaborators": 1})
|
||||
if not project:
|
||||
return jsonify({"message": "Project not found."}), 404
|
||||
owner_id = project.get("ownerId")
|
||||
collaborators = project.get("collaborators", [])
|
||||
if owner_id != user_id and user_id not in collaborators:
|
||||
return jsonify({"message": "Access denied to the specified project."}), 403
|
||||
|
||||
# Prepare initial messages if startMessage exists
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
messages_array = []
|
||||
if start_message:
|
||||
messages_array.append({
|
||||
"role": "user",
|
||||
"content": start_message,
|
||||
"timestamp": now # Store timestamp for messages
|
||||
})
|
||||
|
||||
# Prepare the new dialog document
|
||||
dialog_doc = {
|
||||
"uid": user_id,
|
||||
"projectId": project_obj_id,
|
||||
"provider": provider, # Store the provider used for this session
|
||||
"sessionStartedAt": now,
|
||||
"sessionEndedAt": None, # Mark as null initially
|
||||
"messages": messages_array
|
||||
}
|
||||
if session_id: dialog_doc["sessionId"] = session_id
|
||||
|
||||
# Insert the new dialog session
|
||||
result = db.dialog_activity.insert_one(dialog_doc)
|
||||
|
||||
# Return success response with the new dialog ID
|
||||
return jsonify({
|
||||
"message": "Dialog session created successfully.",
|
||||
"dialog_id": str(result.inserted_id)
|
||||
}), 201
|
||||
|
||||
except KeyError: # Should be caught by initial user_id check
|
||||
logger.error(f"User ID (_id) not found in token payload for create_dialog_session.")
|
||||
return jsonify({"message": "Authentication token is invalid or missing user ID."}), 401
|
||||
except AttributeError:
|
||||
logger.error("PyMongo extension not initialized or attached correctly.")
|
||||
return jsonify({"message": "Database configuration error."}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating dialog session for user {current_user.get('_id', 'UNKNOWN')}: {e}", exc_info=True)
|
||||
return jsonify({"message": "Internal server error creating dialog session."}), 500
|
||||
|
||||
|
||||
@bp.route('/<string:dialog_id>/send', methods=['POST']) # Path relative to prefix
|
||||
@token_required
|
||||
def send_dialog_message(current_user, dialog_id):
|
||||
"""
|
||||
Sends a user message within a specific dialog session.
|
||||
Uses DialogSendMessageSchema for input validation.
|
||||
Expects JSON: { "content": "User's message text" }
|
||||
Retrieves context using vector search, builds a prompt, calls the LLM (Gemini),
|
||||
and stores the conversation turn in the dialog history.
|
||||
"""
|
||||
logger = _get_logger()
|
||||
# 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
|
||||
if not DialogSendMessageSchema or not ValidationError:
|
||||
return jsonify({"message": "Server configuration error: Schema unavailable."}), 500
|
||||
|
||||
try:
|
||||
# Validate IDs
|
||||
user_id_str = str(current_user.get("_id"))
|
||||
if not user_id_str: return jsonify({"message": "Missing user ID in token."}), 400
|
||||
try:
|
||||
user_id = ObjectId(user_id_str)
|
||||
dialog_obj_id = ObjectId(dialog_id)
|
||||
except InvalidId:
|
||||
return jsonify({"message": "Invalid user or dialog ID format."}), 400
|
||||
|
||||
# Get and validate user message content using schema
|
||||
json_data = request.get_json() or {}
|
||||
schema = DialogSendMessageSchema()
|
||||
try:
|
||||
validated_data = schema.load(json_data)
|
||||
except ValidationError as err:
|
||||
logger.warning(f"Send dialog message validation failed: {err.messages}")
|
||||
return jsonify(err.messages), 422
|
||||
|
||||
content = validated_data['content'] # Use validated content
|
||||
|
||||
# --- Retrieve Dialog and API Key ---
|
||||
db = mongo.db
|
||||
dialog_doc = db.dialog_activity.find_one({"_id": dialog_obj_id, "uid": user_id})
|
||||
if not dialog_doc: return jsonify({"message": "Dialog session not found or access denied."}), 404
|
||||
if dialog_doc.get("sessionEndedAt"): return jsonify({"message": "This dialog session has ended."}), 409 # 409 Conflict
|
||||
|
||||
provider = dialog_doc.get("provider")
|
||||
if provider != "Gemini": # This endpoint currently only supports Gemini
|
||||
return jsonify({"message": f"This endpoint only supports 'Gemini', but session provider is '{provider}'."}), 400
|
||||
|
||||
# Find the active Gemini API key for the user
|
||||
api_doc = db.api_list.find_one({"uid": user_id, "name": "Gemini", "selected": True})
|
||||
if not (api_doc and api_doc.get("key")):
|
||||
logger.error(f"No valid Gemini key found or selected for user {user_id} during send message.")
|
||||
return jsonify({"message": "Gemini API key not configured or selected."}), 400
|
||||
gemini_key = api_doc["key"]
|
||||
|
||||
# --- Build Prompt with Vector Knowledge ---
|
||||
project_id = dialog_doc.get("projectId")
|
||||
if not project_id or not isinstance(project_id, ObjectId):
|
||||
logger.error(f"Dialog {dialog_id} is missing valid projectId.")
|
||||
return jsonify({"message": "Internal error: Project reference missing."}), 500
|
||||
|
||||
# This builds the prompt incorporating vector search results
|
||||
detailed_prompt = build_vector_based_prompt_with_knowledge(content, project_id)
|
||||
|
||||
# Prepare history for Gemini (currently just the detailed prompt as a single user turn)
|
||||
gemini_history = [{"role": "user", "parts": [{"text": detailed_prompt}]}]
|
||||
|
||||
# --- Call Gemini API ---
|
||||
llm_response_text = "[LLM Call Skipped/Failed]" # Default response text
|
||||
try:
|
||||
genai.configure(api_key=gemini_key)
|
||||
model = genai.GenerativeModel(current_app.config["GEMINI_MODEL_NAME"])
|
||||
# Consider adding generation_config and safety_settings from Flask config
|
||||
llm_response = model.generate_content(gemini_history)
|
||||
|
||||
# Extract text, handling potential blocks or empty responses
|
||||
try:
|
||||
llm_response_text = llm_response.text
|
||||
except ValueError:
|
||||
logger.warning(f"Gemini response for dialog {dialog_id} may have been blocked or empty. Feedback: {llm_response.prompt_feedback}")
|
||||
llm_response_text = "[Response blocked by safety filters or returned no text content]"
|
||||
|
||||
except google_exceptions.PermissionDenied as ex:
|
||||
logger.warning(f"Gemini Permission Denied for user {user_id}: {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}: {ex}")
|
||||
return jsonify({"message": "Gemini API Error: Rate limit or quota exceeded."}), 429
|
||||
except google_exceptions.GoogleAPIError as ex: # Catch other Google API errors
|
||||
logger.error(f"Gemini API communication error for user {user_id}: {ex}", exc_info=True)
|
||||
return jsonify({"message": "An error occurred while communicating with the Gemini API."}), 503 # 503 Service Unavailable
|
||||
except Exception as e: # Catch potential genai configuration errors etc.
|
||||
logger.error(f"Unexpected error during Gemini call setup or execution for user {user_id}: {e}", exc_info=True)
|
||||
return jsonify({"message": "Internal server error during LLM communication."}), 500
|
||||
|
||||
|
||||
# --- Process Response and Update DB ---
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
user_msg_entry = {"role": "user", "content": content, "timestamp": now}
|
||||
system_msg_entry = {"role": "system", "content": llm_response_text, "timestamp": now} # Use same timestamp for pair
|
||||
|
||||
# Add both messages to the dialog history in MongoDB atomically
|
||||
update_res = db.dialog_activity.update_one(
|
||||
{"_id": dialog_obj_id},
|
||||
{"$push": {"messages": {"$each": [user_msg_entry, system_msg_entry]}}}
|
||||
)
|
||||
if update_res.modified_count != 1:
|
||||
logger.warning(f"Dialog {dialog_id} DB update failed after LLM call (modified_count={update_res.modified_count}).")
|
||||
# Decide if this should be an error response to the user
|
||||
|
||||
# Process the response (e.g., update cached knowledge)
|
||||
process_api_response_and_update_knowledge(llm_response_text, project_id)
|
||||
|
||||
# Return the LLM's response text to the client
|
||||
return jsonify({"message": "LLM response received.", "llmResponse": llm_response_text}), 200
|
||||
|
||||
except KeyError: # Should be caught by initial user_id check
|
||||
logger.error(f"User ID (_id) not found in token payload for send_dialog_message.")
|
||||
return jsonify({"message": "Authentication token is invalid or missing user ID."}), 401
|
||||
except AttributeError:
|
||||
logger.error("PyMongo or other extension not initialized correctly.")
|
||||
return jsonify({"message": "Server configuration error."}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in send_dialog_message for dialog {dialog_id}: {e}", exc_info=True)
|
||||
return jsonify({"message": "Internal server error processing message."}), 500
|
||||
|
||||
|
||||
@bp.route('/', methods=['GET']) # Path relative to prefix
|
||||
@token_required
|
||||
def list_dialog_sessions(current_user):
|
||||
"""
|
||||
Lists dialog sessions for the authenticated user.
|
||||
Uses DialogSummarySchema for output serialization.
|
||||
Supports filtering by 'projectId' query parameter.
|
||||
Excludes the 'messages' array for brevity.
|
||||
"""
|
||||
logger = _get_logger()
|
||||
# Check dependencies
|
||||
if not mongo: return jsonify({"message": "Database connection not available."}), 500
|
||||
if not DialogSummarySchema: return jsonify({"message": "Server configuration error: Schema unavailable."}), 500
|
||||
|
||||
try:
|
||||
# Validate user ID
|
||||
user_id_str = str(current_user.get("_id"))
|
||||
if not user_id_str: return jsonify({"message": "Missing user ID in token."}), 400
|
||||
try:
|
||||
user_id = ObjectId(user_id_str)
|
||||
except InvalidId:
|
||||
return jsonify({"message": "Invalid user ID format in token."}), 400
|
||||
|
||||
# Base query for the user's dialogs
|
||||
query = {"uid": user_id}
|
||||
|
||||
# Add projectId filter if provided in query parameters
|
||||
project_id_str = request.args.get("projectId")
|
||||
if project_id_str:
|
||||
try:
|
||||
project_obj_id = ObjectId(project_id_str)
|
||||
query["projectId"] = project_obj_id
|
||||
except InvalidId:
|
||||
return jsonify({"message": "Invalid projectId format in query parameter."}), 400
|
||||
|
||||
# Fetch dialogs, excluding the messages field, sort by start time descending
|
||||
db = mongo.db
|
||||
cursor = db.dialog_activity.find(
|
||||
query,
|
||||
{"messages": 0} # Projection to exclude messages
|
||||
).sort("sessionStartedAt", -1)
|
||||
|
||||
dialog_docs = list(cursor) # Convert cursor to list
|
||||
|
||||
# --- Serialize results using the schema ---
|
||||
output_schema = DialogSummarySchema(many=True)
|
||||
# Schema handles ObjectId and datetime conversion, and field exclusion
|
||||
serialized_result = output_schema.dump(dialog_docs)
|
||||
|
||||
return jsonify({"dialogs": serialized_result}), 200
|
||||
|
||||
except KeyError: # Should be caught by initial user_id check
|
||||
logger.error(f"User ID (_id) not found in token payload for list_dialog_sessions.")
|
||||
return jsonify({"message": "Authentication token is invalid or missing user ID."}), 401
|
||||
except AttributeError:
|
||||
logger.error("PyMongo extension not initialized or attached correctly.")
|
||||
return jsonify({"message": "Database configuration error."}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing dialogs for user {current_user.get('_id', 'UNKNOWN')}: {e}", exc_info=True)
|
||||
return jsonify({"message": "Internal server error listing dialog sessions."}), 500
|
||||
|
||||
|
||||
@bp.route('/<string:dialog_id>', methods=['GET']) # Path relative to prefix
|
||||
@token_required
|
||||
def get_dialog_session(current_user, dialog_id):
|
||||
"""
|
||||
Retrieves the full details of a specific dialog session, including messages.
|
||||
Uses DialogSchema for output serialization. Verifies ownership.
|
||||
"""
|
||||
logger = _get_logger()
|
||||
# Check dependencies
|
||||
if not mongo: return jsonify({"message": "Database connection not available."}), 500
|
||||
if not DialogSchema: return jsonify({"message": "Server configuration error: Schema unavailable."}), 500
|
||||
|
||||
try:
|
||||
# Validate IDs
|
||||
user_id_str = str(current_user.get("_id"))
|
||||
if not user_id_str: return jsonify({"message": "Missing user ID in token."}), 400
|
||||
try:
|
||||
user_id = ObjectId(user_id_str)
|
||||
dial_obj_id = ObjectId(dialog_id)
|
||||
except InvalidId:
|
||||
return jsonify({"message": "Invalid user or dialog ID format."}), 400
|
||||
|
||||
# Find the specific dialog owned by the user
|
||||
db = mongo.db
|
||||
doc = db.dialog_activity.find_one({"_id": dial_obj_id, "uid": user_id})
|
||||
if not doc:
|
||||
return jsonify({"message": "Dialog session not found or access denied."}), 404
|
||||
|
||||
# --- Serialize results using the schema ---
|
||||
output_schema = DialogSchema()
|
||||
# Schema handles ObjectId, datetime, and nested message formatting
|
||||
serialized_result = output_schema.dump(doc)
|
||||
|
||||
return jsonify(serialized_result), 200
|
||||
|
||||
except KeyError: # Should be caught by initial user_id check
|
||||
logger.error(f"User ID (_id) not found in token payload for get_dialog_session.")
|
||||
return jsonify({"message": "Authentication token is invalid or missing user ID."}), 401
|
||||
except AttributeError:
|
||||
logger.error("PyMongo extension not initialized or attached correctly.")
|
||||
return jsonify({"message": "Database configuration error."}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving dialog {dialog_id} for user {current_user.get('_id', 'UNKNOWN')}: {e}", exc_info=True)
|
||||
return jsonify({"message": "Internal server error retrieving dialog session."}), 500
|
||||
|
||||
|
||||
@bp.route('/<string:dialog_id>/end', methods=['PUT']) # Path relative to prefix
|
||||
@token_required
|
||||
def end_dialog_session(current_user, dialog_id):
|
||||
"""
|
||||
Marks a dialog session as ended by setting the 'sessionEndedAt' timestamp.
|
||||
Prevents ending an already ended session. Verifies ownership.
|
||||
(No schema needed for input/output here)
|
||||
"""
|
||||
logger = _get_logger()
|
||||
if not mongo: return jsonify({"message": "Database connection not available."}), 500
|
||||
|
||||
try:
|
||||
# Validate IDs
|
||||
user_id_str = str(current_user.get("_id"))
|
||||
if not user_id_str: return jsonify({"message": "Missing user ID in token."}), 400
|
||||
try:
|
||||
user_id = ObjectId(user_id_str)
|
||||
dial_obj_id = ObjectId(dialog_id)
|
||||
except InvalidId:
|
||||
return jsonify({"message": "Invalid user or dialog ID format."}), 400
|
||||
|
||||
# Check if session exists, belongs to user, and is not already ended
|
||||
db = mongo.db
|
||||
existing_doc = db.dialog_activity.find_one({"_id": dial_obj_id, "uid": user_id}, {"sessionEndedAt": 1})
|
||||
if not existing_doc:
|
||||
return jsonify({"message": "Dialog session not found or access denied."}), 404
|
||||
if existing_doc.get("sessionEndedAt") is not None:
|
||||
# 409 Conflict - the session is already in the 'ended' state
|
||||
return jsonify({"message": "Dialog session has already been ended."}), 409
|
||||
|
||||
# Update the document to set the end time
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
result = db.dialog_activity.update_one(
|
||||
{"_id": dial_obj_id, "uid": user_id, "sessionEndedAt": None}, # Ensure it's not already ended atomically
|
||||
{"$set": {"sessionEndedAt": now}}
|
||||
)
|
||||
|
||||
# Check if the update was successful
|
||||
if result.modified_count == 1:
|
||||
return jsonify({"message": "Dialog session marked as ended."}), 200
|
||||
elif result.matched_count == 1 and result.modified_count == 0:
|
||||
# This could happen if the session was ended between find_one and update_one (race condition)
|
||||
logger.warning(f"Dialog {dialog_id} was already ended before update (race condition?).")
|
||||
return jsonify({"message": "Dialog session was already ended."}), 409
|
||||
else: # matched_count == 0 (shouldn't happen if find_one worked unless deleted concurrently)
|
||||
logger.warning(f"Dialog {dialog_id} matched 0 for ending update.")
|
||||
return jsonify({"message": "Dialog session not found or already ended."}), 404
|
||||
|
||||
|
||||
except KeyError: # Should be caught by initial user_id check
|
||||
logger.error(f"User ID (_id) not found in token payload for end_dialog_session.")
|
||||
return jsonify({"message": "Authentication token is invalid or missing user ID."}), 401
|
||||
except AttributeError:
|
||||
logger.error("PyMongo extension not initialized or attached correctly.")
|
||||
return jsonify({"message": "Database configuration error."}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Error ending dialog {dialog_id} for user {current_user.get('_id', 'UNKNOWN')}: {e}", exc_info=True)
|
||||
return jsonify({"message": "Internal server error ending dialog session."}), 500
|
||||
|
||||
|
||||
@bp.route('/<string:dialog_id>', methods=['DELETE']) # Path relative to prefix
|
||||
@token_required
|
||||
def delete_dialog_session(current_user, dialog_id):
|
||||
"""
|
||||
Deletes an entire dialog session document. Verifies ownership.
|
||||
(No schema needed for input/output here)
|
||||
"""
|
||||
logger = _get_logger()
|
||||
if not mongo: return jsonify({"message": "Database connection not available."}), 500
|
||||
|
||||
try:
|
||||
# Validate IDs
|
||||
user_id_str = str(current_user.get("_id"))
|
||||
if not user_id_str: return jsonify({"message": "Missing user ID in token."}), 400
|
||||
try:
|
||||
user_id = ObjectId(user_id_str)
|
||||
dial_obj_id = ObjectId(dialog_id)
|
||||
except InvalidId:
|
||||
return jsonify({"message": "Invalid user or dialog ID format."}), 400
|
||||
|
||||
# Perform deletion, ensuring the user owns the dialog
|
||||
db = mongo.db
|
||||
result = db.dialog_activity.delete_one({"_id": dial_obj_id, "uid": user_id})
|
||||
|
||||
# Check if a document was deleted
|
||||
if result.deleted_count == 1:
|
||||
return jsonify({"message": "Dialog session deleted successfully."}), 200 # 200 OK or 204 No Content
|
||||
else:
|
||||
# If deleted_count is 0, the document either didn't exist or didn't belong to the user
|
||||
return jsonify({"message": "Dialog session not found or access denied."}), 404
|
||||
|
||||
except KeyError: # Should be caught by initial user_id check
|
||||
logger.error(f"User ID (_id) not found in token payload for delete_dialog_session.")
|
||||
return jsonify({"message": "Authentication token is invalid or missing user ID."}), 401
|
||||
except AttributeError:
|
||||
logger.error("PyMongo extension not initialized or attached correctly.")
|
||||
return jsonify({"message": "Database configuration error."}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting dialog {dialog_id} for user {current_user.get('_id', 'UNKNOWN')}: {e}", exc_info=True)
|
||||
return jsonify({"message": "Internal server error deleting dialog session."}), 500
|
||||
|
||||
Reference in New Issue
Block a user