Update flask_Character.py
Browse files- flask_Character.py +29 -69
flask_Character.py
CHANGED
|
@@ -35,12 +35,14 @@ from bson import ObjectId
|
|
| 35 |
MONGO_URI = "mongodb+srv://precison9:P1LhtFknkT75yg5L@cluster0.isuwpef.mongodb.net"
|
| 36 |
DB_NAME = "email_assistant_db"
|
| 37 |
EXTRACTED_EMAILS_COLLECTION = "extracted_emails"
|
| 38 |
-
GENERATED_REPLIES_COLLECTION = "generated_replies"
|
| 39 |
|
| 40 |
# Global variables for MongoDB client and collections
|
| 41 |
client: Optional[MongoClient] = None
|
| 42 |
db: Optional[Any] = None
|
| 43 |
extracted_emails_collection: Optional[Any] = None
|
|
|
|
|
|
|
| 44 |
generated_replies_collection: Optional[Any] = None
|
| 45 |
|
| 46 |
# --- Pydantic ObjectId Handling ---
|
|
@@ -158,7 +160,7 @@ class GenerateReplyRequest(BaseModel):
|
|
| 158 |
emoji: str = Field("Auto", examples=["Auto", "None", "Occasional", "Frequent"])
|
| 159 |
|
| 160 |
class GeneratedReplyData(BaseModel):
|
| 161 |
-
# Use PyObjectId for the _id field
|
| 162 |
id: Optional[PyObjectId] = Field(alias="_id", default=None)
|
| 163 |
original_email_text: str
|
| 164 |
generated_reply_text: str
|
|
@@ -180,11 +182,11 @@ class GeneratedReplyData(BaseModel):
|
|
| 180 |
data["_id"] = str(data["_id"])
|
| 181 |
return data
|
| 182 |
|
| 183 |
-
#
|
| 184 |
class GenerateReplyResponse(BaseModel):
|
| 185 |
reply: str = Field(..., description="The AI-generated reply text.")
|
| 186 |
-
stored_id
|
| 187 |
-
|
| 188 |
|
| 189 |
# --- Query Models for GET Endpoints ---
|
| 190 |
class ExtractedEmailQuery(BaseModel):
|
|
@@ -221,7 +223,7 @@ def extract_last_json_block(text: str) -> Optional[str]:
|
|
| 221 |
|
| 222 |
def parse_date(date_str: Optional[str], current_date: date) -> Optional[date]:
|
| 223 |
"""
|
| 224 |
-
Parses a date string, handling 'today', 'tomorrow', and
|
| 225 |
Returns None if input is None or cannot be parsed into a valid date.
|
| 226 |
"""
|
| 227 |
if not date_str:
|
|
@@ -311,16 +313,16 @@ Here is the required JSON schema for each category:
|
|
| 311 |
Each Appointment object must have:
|
| 312 |
- `title` (string, short, meaningful title in Italian based on the meeting's purpose)
|
| 313 |
- `description` (string, summary of the meeting's goal)
|
| 314 |
-
- `start_date` (string,
|
| 315 |
- `start_time` (string, optional, e.g., "10:30 AM", null if not present)
|
| 316 |
-
- `end_date` (string,
|
| 317 |
- `end_time` (string, optional, e.g., "11:00 AM", null if not present)
|
| 318 |
|
| 319 |
- **tasks**: List of Task objects.
|
| 320 |
Each Task object must have:
|
| 321 |
- `task_title` (string, short summary of action item)
|
| 322 |
- `task_description` (string, more detailed explanation)
|
| 323 |
-
- `due_date` (string,
|
| 324 |
|
| 325 |
---
|
| 326 |
|
|
@@ -358,7 +360,7 @@ def _generate_response_internal(
|
|
| 358 |
if not email_text:
|
| 359 |
print(f"[{datetime.now()}] _generate_response_internal: Email text is empty.")
|
| 360 |
return "Cannot generate reply for empty email text."
|
| 361 |
-
|
| 362 |
try:
|
| 363 |
llm = ChatGroq(model="meta-llama/llama-4-scout-17b-16e-instruct", temperature=0.7, max_tokens=800, groq_api_key=api_key)
|
| 364 |
prompt_template_str="""
|
|
@@ -390,7 +392,7 @@ def _generate_response_internal(
|
|
| 390 |
traceback.print_exc() # Print full traceback to logs
|
| 391 |
raise # Re-raise the exception so it can be caught by handle_single_reply_request
|
| 392 |
|
| 393 |
-
# --- Batching
|
| 394 |
MAX_BATCH_SIZE = 20
|
| 395 |
BATCH_TIMEOUT = 0.5 # seconds (Adjust based on expected LLM response time and desired latency)
|
| 396 |
|
|
@@ -400,45 +402,16 @@ reply_queue_condition = asyncio.Condition(lock=reply_queue_lock)
|
|
| 400 |
batch_processor_task: Optional[asyncio.Task] = None
|
| 401 |
|
| 402 |
|
| 403 |
-
# --- Batch Processor and Handler ---
|
| 404 |
async def handle_single_reply_request(request_data: GenerateReplyRequest, future: asyncio.Future):
|
| 405 |
-
"""Handles a single request:
|
| 406 |
print(f"[{datetime.now()}] Handle single reply: Starting for email_text_start='{request_data.email_text[:50]}'...")
|
| 407 |
if future.cancelled():
|
| 408 |
print(f"[{datetime.now()}] Handle single reply: Future cancelled. Aborting.")
|
| 409 |
return
|
| 410 |
try:
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
if not future.done():
|
| 414 |
-
future.set_exception(HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Database service not available for caching/storage."))
|
| 415 |
-
return
|
| 416 |
-
|
| 417 |
-
cache_query = {
|
| 418 |
-
"original_email_text": request_data.email_text,
|
| 419 |
-
"language": request_data.language,
|
| 420 |
-
"length": request_data.length,
|
| 421 |
-
"style": request_data.style,
|
| 422 |
-
"tone": request_data.tone,
|
| 423 |
-
"emoji": request_data.emoji,
|
| 424 |
-
}
|
| 425 |
-
print(f"[{datetime.now()}] Handle single reply: Checking cache for reply...")
|
| 426 |
-
# Use await asyncio.to_thread for blocking MongoDB operations
|
| 427 |
-
cached_reply_doc = await asyncio.to_thread(generated_replies_collection.find_one, cache_query)
|
| 428 |
-
|
| 429 |
-
if cached_reply_doc:
|
| 430 |
-
print(f"[{datetime.now()}] Handle single reply: Reply found in cache. ID: {str(cached_reply_doc['_id'])}")
|
| 431 |
-
response = {
|
| 432 |
-
"reply": cached_reply_doc["generated_reply_text"],
|
| 433 |
-
"stored_id": str(cached_reply_doc["_id"]),
|
| 434 |
-
"cached": True
|
| 435 |
-
}
|
| 436 |
-
if not future.done():
|
| 437 |
-
future.set_result(response)
|
| 438 |
-
print(f"[{datetime.now()}] Handle single reply: Cache result set on future.")
|
| 439 |
-
return
|
| 440 |
-
|
| 441 |
-
print(f"[{datetime.now()}] Handle single reply: Reply not in cache. Calling LLM...")
|
| 442 |
reply_content = await asyncio.to_thread(
|
| 443 |
_generate_response_internal,
|
| 444 |
request_data.email_text,
|
|
@@ -451,26 +424,10 @@ async def handle_single_reply_request(request_data: GenerateReplyRequest, future
|
|
| 451 |
)
|
| 452 |
print(f"[{datetime.now()}] Handle single reply: LLM call completed. Reply length: {len(reply_content)}.")
|
| 453 |
|
| 454 |
-
|
| 455 |
-
original_email_text=request_data.email_text,
|
| 456 |
-
generated_reply_text=reply_content,
|
| 457 |
-
language=request_data.language,
|
| 458 |
-
length=request_data.length,
|
| 459 |
-
style=request_data.style,
|
| 460 |
-
tone=request_data.tone,
|
| 461 |
-
emoji=request_data.emoji
|
| 462 |
-
)
|
| 463 |
-
print(f"[{datetime.now()}] Handle single reply: Storing reply in DB...")
|
| 464 |
-
# Use model_dump for Pydantic v2
|
| 465 |
-
reply_data_dict = reply_data_to_store.model_dump(by_alias=True, exclude_none=True, exclude={'id'})
|
| 466 |
-
|
| 467 |
-
insert_result = await asyncio.to_thread(generated_replies_collection.insert_one, reply_data_dict)
|
| 468 |
-
stored_id = str(insert_result.inserted_id)
|
| 469 |
-
print(f"[{datetime.now()}] Handle single reply: Reply stored in DB. ID: {stored_id}")
|
| 470 |
-
|
| 471 |
final_response = {
|
| 472 |
"reply": reply_content,
|
| 473 |
-
"stored_id":
|
| 474 |
"cached": False
|
| 475 |
}
|
| 476 |
if not future.done():
|
|
@@ -585,6 +542,7 @@ async def startup_event():
|
|
| 585 |
client.admin.command('ping') # Test connection
|
| 586 |
db = client[DB_NAME]
|
| 587 |
extracted_emails_collection = db[EXTRACTED_EMAILS_COLLECTION]
|
|
|
|
| 588 |
generated_replies_collection = db[GENERATED_REPLIES_COLLECTION]
|
| 589 |
print(f"[{datetime.now()}] Successfully connected to MongoDB: {DB_NAME}")
|
| 590 |
|
|
@@ -739,16 +697,17 @@ async def extract_email_data_excel(request: ProcessEmailRequest):
|
|
| 739 |
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Excel functionality is currently disabled.")
|
| 740 |
|
| 741 |
|
| 742 |
-
@app.post("/generate-reply", response_model=GenerateReplyResponse, summary="Generate a smart reply to an email (batched
|
| 743 |
async def generate_email_reply(request: GenerateReplyRequest):
|
| 744 |
"""
|
| 745 |
Generates an intelligent email reply based on specified parameters (language, length, style, tone, emoji).
|
| 746 |
-
Uses a batch processing system
|
| 747 |
"""
|
| 748 |
print(f"[{datetime.now()}] /generate-reply: Received request.")
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
|
|
|
| 752 |
|
| 753 |
future = asyncio.Future()
|
| 754 |
current_time = asyncio.get_event_loop().time()
|
|
@@ -759,8 +718,8 @@ async def generate_email_reply(request: GenerateReplyRequest):
|
|
| 759 |
print(f"[{datetime.now()}] /generate-reply: Request added to queue, notifying batch processor. Queue size: {len(reply_request_queue)}")
|
| 760 |
|
| 761 |
try:
|
| 762 |
-
# Debugging:
|
| 763 |
-
client_timeout = BATCH_TIMEOUT +
|
| 764 |
print(f"[{datetime.now()}] /generate-reply: Waiting for future result with timeout {client_timeout}s.")
|
| 765 |
result = await asyncio.wait_for(future, timeout=client_timeout)
|
| 766 |
print(f"[{datetime.now()}] /generate-reply: Future result received. Returning data.")
|
|
@@ -832,6 +791,7 @@ async def query_extracted_emails_endpoint(query_params: ExtractedEmailQuery = De
|
|
| 832 |
@app.get("/query-generated-replies", response_model=List[GeneratedReplyData], summary="Query generated replies from MongoDB")
|
| 833 |
async def query_generated_replies_endpoint(query_params: GeneratedReplyQuery = Depends()):
|
| 834 |
print(f"[{datetime.now()}] /query-generated-replies: Received request with params: {query_params.model_dump_json()}")
|
|
|
|
| 835 |
if generated_replies_collection is None:
|
| 836 |
print(f"[{datetime.now()}] /query-generated-replies: MongoDB collection is None.")
|
| 837 |
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="MongoDB not available for querying generated replies.")
|
|
|
|
| 35 |
MONGO_URI = "mongodb+srv://precison9:P1LhtFknkT75yg5L@cluster0.isuwpef.mongodb.net"
|
| 36 |
DB_NAME = "email_assistant_db"
|
| 37 |
EXTRACTED_EMAILS_COLLECTION = "extracted_emails"
|
| 38 |
+
GENERATED_REPLIES_COLLECTION = "generated_replies" # Still defined, but not used by generate-reply logic
|
| 39 |
|
| 40 |
# Global variables for MongoDB client and collections
|
| 41 |
client: Optional[MongoClient] = None
|
| 42 |
db: Optional[Any] = None
|
| 43 |
extracted_emails_collection: Optional[Any] = None
|
| 44 |
+
# generated_replies_collection is no longer needed for /generate-reply logic,
|
| 45 |
+
# but kept for /query-generated-replies endpoint if that's still desired.
|
| 46 |
generated_replies_collection: Optional[Any] = None
|
| 47 |
|
| 48 |
# --- Pydantic ObjectId Handling ---
|
|
|
|
| 160 |
emoji: str = Field("Auto", examples=["Auto", "None", "Occasional", "Frequent"])
|
| 161 |
|
| 162 |
class GeneratedReplyData(BaseModel):
|
| 163 |
+
# Use PyObjectId for the _id field (This model is now only used for the query endpoint)
|
| 164 |
id: Optional[PyObjectId] = Field(alias="_id", default=None)
|
| 165 |
original_email_text: str
|
| 166 |
generated_reply_text: str
|
|
|
|
| 182 |
data["_id"] = str(data["_id"])
|
| 183 |
return data
|
| 184 |
|
| 185 |
+
# Response Model for /generate-reply endpoint (simplified)
|
| 186 |
class GenerateReplyResponse(BaseModel):
|
| 187 |
reply: str = Field(..., description="The AI-generated reply text.")
|
| 188 |
+
# 'stored_id' and 'cached' are removed as caching/storage is removed
|
| 189 |
+
# from the main generate-reply logic.
|
| 190 |
|
| 191 |
# --- Query Models for GET Endpoints ---
|
| 192 |
class ExtractedEmailQuery(BaseModel):
|
|
|
|
| 223 |
|
| 224 |
def parse_date(date_str: Optional[str], current_date: date) -> Optional[date]:
|
| 225 |
"""
|
| 226 |
+
Parses a date string, handling 'today', 'tomorrow', and APAC-MM-DD format.
|
| 227 |
Returns None if input is None or cannot be parsed into a valid date.
|
| 228 |
"""
|
| 229 |
if not date_str:
|
|
|
|
| 313 |
Each Appointment object must have:
|
| 314 |
- `title` (string, short, meaningful title in Italian based on the meeting's purpose)
|
| 315 |
- `description` (string, summary of the meeting's goal)
|
| 316 |
+
- `start_date` (string, APAC-MM-DD. If not explicitly mentioned, use "{prompt_today_str}" for "today", or "{prompt_tomorrow_str}" for "tomorrow")
|
| 317 |
- `start_time` (string, optional, e.g., "10:30 AM", null if not present)
|
| 318 |
+
- `end_date` (string, APAC-MM-DD, optional, null if unknown or not applicable)
|
| 319 |
- `end_time` (string, optional, e.g., "11:00 AM", null if not present)
|
| 320 |
|
| 321 |
- **tasks**: List of Task objects.
|
| 322 |
Each Task object must have:
|
| 323 |
- `task_title` (string, short summary of action item)
|
| 324 |
- `task_description` (string, more detailed explanation)
|
| 325 |
+
- `due_date` (string, APAC-MM-DD. Infer from context, e.g., "entro domani" becomes "{prompt_tomorrow_str}", "today" becomes "{prompt_today_str}")
|
| 326 |
|
| 327 |
---
|
| 328 |
|
|
|
|
| 360 |
if not email_text:
|
| 361 |
print(f"[{datetime.now()}] _generate_response_internal: Email text is empty.")
|
| 362 |
return "Cannot generate reply for empty email text."
|
| 363 |
+
|
| 364 |
try:
|
| 365 |
llm = ChatGroq(model="meta-llama/llama-4-scout-17b-16e-instruct", temperature=0.7, max_tokens=800, groq_api_key=api_key)
|
| 366 |
prompt_template_str="""
|
|
|
|
| 392 |
traceback.print_exc() # Print full traceback to logs
|
| 393 |
raise # Re-raise the exception so it can be caught by handle_single_reply_request
|
| 394 |
|
| 395 |
+
# --- Batching Configuration (Caching/Storage logic removed) ---
|
| 396 |
MAX_BATCH_SIZE = 20
|
| 397 |
BATCH_TIMEOUT = 0.5 # seconds (Adjust based on expected LLM response time and desired latency)
|
| 398 |
|
|
|
|
| 402 |
batch_processor_task: Optional[asyncio.Task] = None
|
| 403 |
|
| 404 |
|
| 405 |
+
# --- Batch Processor and Handler (Simplified) ---
|
| 406 |
async def handle_single_reply_request(request_data: GenerateReplyRequest, future: asyncio.Future):
|
| 407 |
+
"""Handles a single request: calls LLM, and sets future with the reply."""
|
| 408 |
print(f"[{datetime.now()}] Handle single reply: Starting for email_text_start='{request_data.email_text[:50]}'...")
|
| 409 |
if future.cancelled():
|
| 410 |
print(f"[{datetime.now()}] Handle single reply: Future cancelled. Aborting.")
|
| 411 |
return
|
| 412 |
try:
|
| 413 |
+
# Directly call LLM (no cache check or storage)
|
| 414 |
+
print(f"[{datetime.now()}] Handle single reply: Calling LLM for reply generation...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
reply_content = await asyncio.to_thread(
|
| 416 |
_generate_response_internal,
|
| 417 |
request_data.email_text,
|
|
|
|
| 424 |
)
|
| 425 |
print(f"[{datetime.now()}] Handle single reply: LLM call completed. Reply length: {len(reply_content)}.")
|
| 426 |
|
| 427 |
+
# Simplified response as no storage/cache ID
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
final_response = {
|
| 429 |
"reply": reply_content,
|
| 430 |
+
"stored_id": "N/A - Caching disabled", # Indicate that ID is not available
|
| 431 |
"cached": False
|
| 432 |
}
|
| 433 |
if not future.done():
|
|
|
|
| 542 |
client.admin.command('ping') # Test connection
|
| 543 |
db = client[DB_NAME]
|
| 544 |
extracted_emails_collection = db[EXTRACTED_EMAILS_COLLECTION]
|
| 545 |
+
# Keep generated_replies_collection definition if /query-generated-replies is still desired
|
| 546 |
generated_replies_collection = db[GENERATED_REPLIES_COLLECTION]
|
| 547 |
print(f"[{datetime.now()}] Successfully connected to MongoDB: {DB_NAME}")
|
| 548 |
|
|
|
|
| 697 |
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Excel functionality is currently disabled.")
|
| 698 |
|
| 699 |
|
| 700 |
+
@app.post("/generate-reply", response_model=GenerateReplyResponse, summary="Generate a smart reply to an email (batched)")
|
| 701 |
async def generate_email_reply(request: GenerateReplyRequest):
|
| 702 |
"""
|
| 703 |
Generates an intelligent email reply based on specified parameters (language, length, style, tone, emoji).
|
| 704 |
+
Uses a batch processing system. Caching and database storage for replies are disabled.
|
| 705 |
"""
|
| 706 |
print(f"[{datetime.now()}] /generate-reply: Received request.")
|
| 707 |
+
# generated_replies_collection check is no longer relevant for this endpoint's logic
|
| 708 |
+
if batch_processor_task is None or reply_queue_condition is None:
|
| 709 |
+
print(f"[{datetime.now()}] /generate-reply: Service not fully initialized. batch_task={batch_processor_task is not None}, queue_cond={reply_queue_condition is not None}")
|
| 710 |
+
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Reply generation service not fully initialized. Check server logs for batch processor issues.")
|
| 711 |
|
| 712 |
future = asyncio.Future()
|
| 713 |
current_time = asyncio.get_event_loop().time()
|
|
|
|
| 718 |
print(f"[{datetime.now()}] /generate-reply: Request added to queue, notifying batch processor. Queue size: {len(reply_request_queue)}")
|
| 719 |
|
| 720 |
try:
|
| 721 |
+
# Debugging: Use a very long timeout for now to ensure server-side logs complete
|
| 722 |
+
client_timeout = BATCH_TIMEOUT + 300.0 # 5 minutes (0.5s batch + 300s buffer)
|
| 723 |
print(f"[{datetime.now()}] /generate-reply: Waiting for future result with timeout {client_timeout}s.")
|
| 724 |
result = await asyncio.wait_for(future, timeout=client_timeout)
|
| 725 |
print(f"[{datetime.now()}] /generate-reply: Future result received. Returning data.")
|
|
|
|
| 791 |
@app.get("/query-generated-replies", response_model=List[GeneratedReplyData], summary="Query generated replies from MongoDB")
|
| 792 |
async def query_generated_replies_endpoint(query_params: GeneratedReplyQuery = Depends()):
|
| 793 |
print(f"[{datetime.now()}] /query-generated-replies: Received request with params: {query_params.model_dump_json()}")
|
| 794 |
+
# This endpoint still relies on `generated_replies_collection`
|
| 795 |
if generated_replies_collection is None:
|
| 796 |
print(f"[{datetime.now()}] /query-generated-replies: MongoDB collection is None.")
|
| 797 |
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="MongoDB not available for querying generated replies.")
|