Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 55 additions & 48 deletions backend/ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Empty file removed backend/alerts/init__.py
Empty file.
3 changes: 0 additions & 3 deletions backend/alerts/scheduler.py

This file was deleted.

65 changes: 32 additions & 33 deletions backend/devto.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -42,7 +40,7 @@ class PublisherError(Exception):
class BasePublisher:
platform = "base"

def publish(
async def publish(
self,
title: str,
content: str,
Expand All @@ -54,34 +52,39 @@ def publish(
raise NotImplementedError

@staticmethod
def _post_with_retries(
async def _post_with_retries(
url: str,
*,
headers: dict[str, str],
payload: dict[str, Any],
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,
Expand All @@ -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,
Expand Down Expand Up @@ -196,7 +199,7 @@ async def publish(
class MediumPublisher(BasePublisher):
platform = "medium"

def publish(
async def publish(
self,
title: str,
content: str,
Expand All @@ -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}",
Expand Down Expand Up @@ -241,7 +244,7 @@ def publish(
class WebhookPublisher(BasePublisher):
platform = "webhook"

def publish(
async def publish(
self,
title: str,
content: str,
Expand All @@ -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={
Expand Down Expand Up @@ -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)
27 changes: 16 additions & 11 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -37,6 +38,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)

Expand Down Expand Up @@ -88,6 +96,7 @@ class ReminderPreference(BaseModel):
is_opted_in: bool = True



class AuthCredentials(BaseModel):
name: str | None = None
email: str
Expand Down Expand Up @@ -347,6 +356,7 @@ async def update_integration_settings(




# -----------------------------
# Health Check
# -----------------------------
Expand Down Expand Up @@ -394,7 +404,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",
Expand Down Expand Up @@ -566,7 +576,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 {"status": "error", "message": "TEST_PHONE_NUMBER is not set in environment."}
Expand All @@ -580,8 +590,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")

Expand Down Expand Up @@ -631,9 +641,4 @@ async def unsubscribe(data: dict):
# Run Server
# -----------------------------
if __name__ == "__main__":
uvicorn.run(
"main:app",
host="0.0.0.0",
port=10000,
reload=True
)
uvicorn.run("main:app", host="0.0.0.0", port=10000, reload=True)
6 changes: 3 additions & 3 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ fastapi
uvicorn[standard]
pydantic
google-genai
httpx
openai
requests
httpx
python-dotenv
twilio
elevenlabs
Expand All @@ -12,8 +13,7 @@ celery[redis]
motor
pytz
pymongo
tweepy
openai
tweepy==4.14.0

# --- Test & Dev Dependencies ---
pytest
Expand Down
2 changes: 1 addition & 1 deletion backend/ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Loading
Loading