diff --git a/backend/ai.py b/backend/ai.py
index cb7a562..406eba1 100644
--- a/backend/ai.py
+++ b/backend/ai.py
@@ -22,69 +22,76 @@
INITIAL_BACKOFF_SECONDS = 35 # Free tier asks to retry after ~35s
+def _difficulty_badge(difficulty: str) -> str:
+ badges = {"Easy": "🟢 Easy", "Medium": "🟡 Medium", "Hard": "🔴 Hard"}
+ return badges.get(difficulty, f"⚪ {difficulty}")
+
+
def _build_prompt(problem, current_time: str) -> str:
"""
- Build the prompt string to send to Gemini AI.
+ Builds a structured prompt for Gemini AI using LeetCode problem details,
+ solution code, author information, and optional custom instructions.
Args:
- problem: LeetCode problem object containing title, description, code and author
- current_time: Current timestamp string
+ problem: Object containing the LeetCode problem title, description,
+ code, author, difficulty, and custom prompt.
+ current_time (str): Timestamp used in the generated blog footer.
Returns:
str: Formatted prompt string for Gemini AI
"""
+ badge = _difficulty_badge(getattr(problem, "difficulty", None) or "Unknown")
custom_instructions = ""
default_prompt = f"""
- You are a professional technical writer and competitive programmer.
-
- Generate a highly engaging, beginner-friendly Dev.to blog post about a LeetCode problem.
-
- Author Account: {problem.author}
- Publishing Time: {current_time}
- Title: {problem.title}
-
- Problem Description:
- {problem.description}
-
- Solution Code:
- {problem.code}
-
- Strictly follow this structure:
- 1. Title (Use an engaging # Title instead of YAML)
- 2. Problem Explanation (explain it simply, as if to a beginner)
- 3. Intuition (the "aha!" moment)
- 4. Approach (step-by-step logic)
- 5. Code (formatted clearly inside markdown code blocks, specify language if obvious)
- 6. Time & Space Complexity Analysis
- 7. Key Takeaways
- 8. Submission Details (MUST include the Author Account [{problem.author}] and the Time Published [{current_time}] in a concluding footnote)
-
- CRITICAL INSTRUCTIONS:
- - DO NOT wrap the output in ```markdown or ``` tags. Return raw markdown text.
- - DO NOT output YAML frontmatter (no --- blocks).
- - TABLE FORMATTING (STRICT RULES):
- - If you use a Markdown table, it MUST be perfectly formatted to render correctly.
- - Each row (header, separator, or data) MUST start with `|` and end with `|`.
- - A table row MUST be on exactly ONE single line. DO NOT use line breaks inside rows.
- - The header row, separator row (e.g., `|---|---|`), and all data rows MUST have the EXACT same number of columns.
- - CELL CONTENT: If a cell contains a bitwise OR operator `|` or any pipe character, you MUST escape it as `\\|` (e.g., `(a \\| b)`). Failing to escape pipes inside cells will break the table structure.
- - Ensure the separator line is continuous (no line breaks) and uses at least 3 dashes per column.
- - Always provide an EMPTY LINE before and after the table to ensure correct rendering.
- """
+You are a professional technical writer and competitive programmer.
+
+Generate a highly engaging, beginner-friendly Dev.to blog post about a LeetCode problem.
+
+Author Account: {problem.author}
+Publishing Time: {current_time}
+Title: {problem.title}
+Difficulty: {badge}
+
+Problem Description:
+{problem.description}
+
+Solution Code:
+{problem.code}
+
+Strictly follow this structure:
+1. Title (Use an engaging # Title instead of YAML)
+2. Difficulty Badge — render it prominently right below the title as: **Difficulty:** {badge}
+3. Problem Explanation (explain it simply, as if to a beginner)
+4. Intuition (the "aha!" moment)
+5. Approach (step-by-step logic)
+6. Code (formatted clearly inside markdown code blocks, specify language if obvious)
+7. Time & Space Complexity Analysis
+8. Key Takeaways
+9. Submission Details (MUST include the Author Account [{problem.author}] and the Time Published [{current_time}] in a concluding footnote)
+
+CRITICAL INSTRUCTIONS:
+- DO NOT wrap the output in ```markdown or ``` tags. Return raw markdown text.
+- DO NOT output YAML frontmatter (no --- blocks).
+- TABLE FORMATTING (STRICT RULES):
+ - If you use a Markdown table, it MUST be perfectly formatted to render correctly.
+ - Each row (header, separator, or data) MUST start with `|` and end with `|`.
+ - A table row MUST be on exactly ONE single line. DO NOT use line breaks inside rows.
+ - The header row, separator row (e.g., `|---|---|`), and all data rows MUST have the EXACT same number of columns.
+ - CELL CONTENT: If a cell contains a bitwise OR operator `|` or any pipe character, you MUST escape it as `\\|` (e.g., `(a \\| b)`). Failing to escape pipes inside cells will break the table structure.
+ - Ensure the separator line is continuous (no line breaks) and uses at least 3 dashes per column.
+ - Always provide an EMPTY LINE before and after the table to ensure correct rendering.
+"""
if hasattr(problem, "custom_prompt") and problem.custom_prompt:
- cleaned_custom_prompt = problem.custom_prompt.strip()
- if cleaned_custom_prompt:
+ cleaned = problem.custom_prompt.strip()
+ if cleaned:
custom_instructions = f"""
- Additional User Prompt Preferences:
- {cleaned_custom_prompt}
- """
+Additional User Prompt Preferences:
+{cleaned}
+"""
- return f"""
- {default_prompt}
- {custom_instructions}
- """
+ return f"{default_prompt}{custom_instructions}"
def _clean_response(text: str) -> str:
diff --git a/backend/alerts/init__.py b/backend/alerts/init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/backend/alerts/scheduler.py b/backend/alerts/scheduler.py
deleted file mode 100644
index 33e661e..0000000
--- a/backend/alerts/scheduler.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from time import timezone
-
-from alerts.progress_checker import check_unsolved_users
-from services.reminder_scheduler import BackgroundScheduler, start_scheduler
-
-scheduler = BackgroundScheduler()
-
-# Check daily at 11:00 PM IST
-scheduler.add_job(
- check_unsolved_users, "cron", hour=23, minute=0, timezone=timezone("Asia/Kolkata")
-)
-
-scheduler.start()
-start_scheduler()
diff --git a/backend/devto.py b/backend/devto.py
index 0cca656..2861c80 100644
--- a/backend/devto.py
+++ b/backend/devto.py
@@ -1,11 +1,9 @@
import asyncio
import os
-import time
from dataclasses import dataclass
from typing import Any
import httpx
-import requests
from dotenv import load_dotenv
load_dotenv()
@@ -42,7 +40,7 @@ class PublisherError(Exception):
class BasePublisher:
platform = "base"
- def publish(
+ async def publish(
self,
title: str,
content: str,
@@ -54,7 +52,7 @@ def publish(
raise NotImplementedError
@staticmethod
- def _post_with_retries(
+ async def _post_with_retries(
url: str,
*,
headers: dict[str, str],
@@ -62,26 +60,31 @@ def _post_with_retries(
platform: str,
retries: int = 2,
) -> dict[str, Any]:
- for attempt in range(retries + 1):
- try:
- response = requests.post(url, headers=headers, json=payload, timeout=20)
- if response.status_code in (200, 201):
- return response.json()
- if attempt == retries:
- raise PublisherError(
- f"{platform} API Error {response.status_code}: {response.text}"
+ async with httpx.AsyncClient() as client:
+ for attempt in range(retries + 1):
+ try:
+ response = await client.post(
+ url, headers=headers, json=payload, timeout=20.0
)
- except requests.RequestException as exc:
- if attempt == retries:
- raise PublisherError(f"{platform} network error: {exc}") from exc
- time.sleep(1)
- raise PublisherError(f"{platform} API request failed.")
+ if response.status_code in (200, 201):
+ return response.json()
+ if attempt == retries:
+ raise PublisherError(
+ f"{platform} API Error {response.status_code}: {response.text}"
+ )
+ except httpx.RequestError as exc:
+ if attempt == retries:
+ raise PublisherError(
+ f"{platform} network error: {exc}"
+ ) from exc
+ await asyncio.sleep(1)
+ raise PublisherError(f"{platform} API request failed.")
class DevToPublisher(BasePublisher):
platform = "devto"
- def publish(
+ async def publish(
self,
title: str,
content: str,
@@ -96,7 +99,7 @@ def publish(
"Dev.to API key missing. Add it in Settings > Integrations."
)
- response = self._post_with_retries(
+ response = await self._post_with_retries(
"https://dev.to/api/articles",
headers={
"api-key": api_key,
@@ -196,7 +199,7 @@ async def publish(
class MediumPublisher(BasePublisher):
platform = "medium"
- def publish(
+ async def publish(
self,
title: str,
content: str,
@@ -213,7 +216,7 @@ def publish(
"Medium publishing requires MEDIUM_TOKEN and MEDIUM_USER_ID."
)
- response = self._post_with_retries(
+ response = await self._post_with_retries(
f"https://api.medium.com/v1/users/{user_id}/posts",
headers={
"Authorization": f"Bearer {token}",
@@ -241,7 +244,7 @@ def publish(
class WebhookPublisher(BasePublisher):
platform = "webhook"
- def publish(
+ async def publish(
self,
title: str,
content: str,
@@ -254,7 +257,7 @@ def publish(
if not webhook_url:
raise PublisherError("Personal blog publishing requires BLOG_WEBHOOK_URL.")
- response = self._post_with_retries(
+ response = await self._post_with_retries(
webhook_url,
headers={"Content-Type": "application/json"},
payload={
@@ -343,14 +346,10 @@ async def publish_to_platforms(
return [result.as_dict() for result in results]
-def post_to_platform(title: str, content: str) -> dict[str, Any]:
+async def post_to_platform(title: str, content: str) -> dict[str, Any]:
"""Backward-compatible Dev.to-only wrapper used by older integrations."""
- result = DevToPublisher().publish(
- title,
- content,
- tags=DEFAULT_TAGS,
- published=True,
- )
- if result.status != "success":
- raise Exception(result.message or "Dev.to publishing failed.")
- return result.response or result.as_dict()
+ results = await publish_to_platforms(title, content, platforms=["devto"])
+ first = results[0]
+ if first["status"] != "success":
+ raise Exception(first.get("message", "Dev.to publishing failed."))
+ return first.get("response", first)
diff --git a/backend/main.py b/backend/main.py
index 3d21f8c..c550589 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -12,6 +12,7 @@
import uvicorn
from dotenv import load_dotenv
from fastapi import Depends, FastAPI, Header, HTTPException, Query, status
+from fastapi.concurrency import run_in_threadpool
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
@@ -21,7 +22,7 @@
from ai_core.blog_generator import generate_blog
from devto import publish_to_platforms
from models.reminder import PublishRecord
-from services.reminder_scheduler import start_scheduler
+from services.scheduler_service import start_scheduler
from social import share_to_platforms
load_dotenv()
@@ -38,6 +39,13 @@ async def lifespan(app: FastAPI):
except Exception as e:
print(f"Reminder scheduler failed to start: {e}")
yield
+ try:
+ from services.scheduler_service import scheduler
+ if scheduler.running:
+ scheduler.shutdown()
+ print("Reminder scheduler shut down successfully.")
+ except Exception as e:
+ print(f"Failed to shut down scheduler: {e}")
app = FastAPI(title="LeetLog AI", version="1.0.0", lifespan=lifespan)
@@ -357,6 +365,7 @@ async def update_integration_settings(
+
# -----------------------------
# Health Check
# -----------------------------
@@ -403,7 +412,7 @@ async def create_blog(
user_settings = await _settings_for_user(current_user["id"]) if current_user else {}
try:
- blog_content = generate_blog(problem, credentials=user_settings)
+ blog_content = await run_in_threadpool(generate_blog, problem, credentials=user_settings)
except Exception as e:
return {"status": "error", "message": f"AI provider failure: {str(e)}"}
@@ -595,8 +604,7 @@ def test_whatsapp():
try:
import os
- from alerts.twilio_service import send_whatsapp_message
-
+ from services.twilio_service import send_whatsapp_message
phone = os.getenv("TEST_PHONE_NUMBER")
if not phone:
return {
@@ -621,8 +629,8 @@ def test_call():
try:
import os
- from alerts.elevenlabs_service import generate_audio, generate_message
- from alerts.twilio_service import make_call
+ from services.elevenlabs_service import generate_audio, generate_message
+ from services.twilio_service import make_call
message = generate_message("Vansh")
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 288345f..8a996cf 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -2,8 +2,9 @@ fastapi
uvicorn[standard]
pydantic
google-genai
-httpx
+openai
requests
+httpx
python-dotenv
twilio
elevenlabs
@@ -12,8 +13,7 @@ celery[redis]
motor
pytz
pymongo
-tweepy
-openai
+tweepy==4.14.0
# --- Test & Dev Dependencies ---
pytest
diff --git a/backend/ruff.toml b/backend/ruff.toml
index 21b06a3..62a741f 100644
--- a/backend/ruff.toml
+++ b/backend/ruff.toml
@@ -6,4 +6,4 @@ select = ["E", "F", "W", "I"]
ignore = ["E501"]
[lint.isort]
-known-first-party = ["ai", "devto", "services", "alerts", "models", "utils"]
+known-first-party = ["ai", "devto", "services", "models"]
diff --git a/backend/services/call_service.py b/backend/services/call_service.py
deleted file mode 100644
index c702c74..0000000
--- a/backend/services/call_service.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import os
-
-from twilio.rest import Client
-
-client = Client(os.getenv("TWILIO_ACCOUNT_SID"), os.getenv("TWILIO_AUTH_TOKEN"))
-
-
-def make_call(to_number: str, audio_url: str = None, text_to_say: str = None):
- twilio_number = os.getenv("TWILIO_PHONE_NUMBER", "")
- from_number = twilio_number.replace("whatsapp:", "") if twilio_number else ""
-
- if not from_number:
- raise ValueError("TWILIO_PHONE_NUMBER is not set in environment variables.")
-
- if audio_url:
- twiml = f"