Update code via agent code generation

This commit is contained in:
Automated Action 2025-06-27 16:41:30 +00:00
parent e4b129ff31
commit 7446415eec
24 changed files with 1003 additions and 0 deletions

41
alembic.ini Normal file
View File

@ -0,0 +1,41 @@
[alembic]
script_location = alembic
prepend_sys_path = .
version_path_separator = os
sqlalchemy.url = sqlite:////app/storage/db/db.sqlite
[post_write_hooks]
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

55
alembic/env.py Normal file
View File

@ -0,0 +1,55 @@
import os
import sys
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
sys.path = ['', '..'] + sys.path[1:]
from app.db.base import Base
from app.models.user import User
from app.models.task import Task
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def run_migrations_offline() -> None:
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
alembic/script.py.mako Normal file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,63 @@
"""Initial migration
Revision ID: 001
Revises:
Create Date: 2024-01-01 00:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '001'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('hashed_password', sa.String(), nullable=False),
sa.Column('full_name', sa.String(), nullable=True),
sa.Column('phone_number', sa.String(), nullable=True),
sa.Column('whatsapp_number', sa.String(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
op.create_table('tasks',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('status', sa.Enum('PENDING', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED', name='taskstatus'), nullable=True),
sa.Column('priority', sa.Enum('LOW', 'MEDIUM', 'HIGH', 'URGENT', name='taskpriority'), nullable=True),
sa.Column('due_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('scheduled_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('ai_suggested_time', sa.DateTime(timezone=True), nullable=True),
sa.Column('whatsapp_reminder_sent', sa.Boolean(), nullable=True),
sa.Column('whatsapp_completion_sent', sa.Boolean(), nullable=True),
sa.Column('owner_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_tasks_id'), 'tasks', ['id'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_tasks_id'), table_name='tasks')
op.drop_table('tasks')
op.drop_index(op.f('ix_users_id'), table_name='users')
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_table('users')
# ### end Alembic commands ###

0
app/__init__.py Normal file
View File

0
app/api/__init__.py Normal file
View File

53
app/api/auth.py Normal file
View File

@ -0,0 +1,53 @@
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.models.user import User
from app.api.schemas import UserCreate, UserResponse, UserLogin, Token
from app.core.security import verify_password, get_password_hash, create_access_token
from app.core.config import settings
router = APIRouter()
@router.post("/register", response_model=UserResponse)
def register_user(user: UserCreate, db: Session = Depends(get_db)):
db_user = db.query(User).filter(User.email == user.email).first()
if db_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
hashed_password = get_password_hash(user.password)
db_user = User(
email=user.email,
hashed_password=hashed_password,
full_name=user.full_name,
phone_number=user.phone_number,
whatsapp_number=user.whatsapp_number
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
@router.post("/login", response_model=Token)
def login_user(user_credentials: UserLogin, db: Session = Depends(get_db)):
user = db.query(User).filter(User.email == user_credentials.email).first()
if not user or not verify_password(user_credentials.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.email}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}

76
app/api/schemas.py Normal file
View File

@ -0,0 +1,76 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, EmailStr
from app.models.task import TaskStatus, TaskPriority
class UserBase(BaseModel):
email: EmailStr
full_name: Optional[str] = None
phone_number: Optional[str] = None
whatsapp_number: Optional[str] = None
class UserCreate(UserBase):
password: str
class UserResponse(UserBase):
id: int
is_active: bool
created_at: datetime
class Config:
from_attributes = True
class UserLogin(BaseModel):
email: EmailStr
password: str
class Token(BaseModel):
access_token: str
token_type: str
class TaskBase(BaseModel):
title: str
description: Optional[str] = None
priority: TaskPriority = TaskPriority.MEDIUM
due_date: Optional[datetime] = None
scheduled_at: Optional[datetime] = None
class TaskCreate(TaskBase):
pass
class TaskUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
priority: Optional[TaskPriority] = None
status: Optional[TaskStatus] = None
due_date: Optional[datetime] = None
scheduled_at: Optional[datetime] = None
class TaskResponse(TaskBase):
id: int
status: TaskStatus
ai_suggested_time: Optional[datetime] = None
whatsapp_reminder_sent: bool
whatsapp_completion_sent: bool
owner_id: int
created_at: datetime
updated_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
class Config:
from_attributes = True
class HealthResponse(BaseModel):
status: str
timestamp: datetime
version: str

165
app/api/tasks.py Normal file
View File

@ -0,0 +1,165 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from datetime import datetime
from app.db.session import get_db
from app.models.user import User
from app.models.task import Task, TaskStatus
from app.api.schemas import TaskCreate, TaskUpdate, TaskResponse
from app.core.auth import get_current_user
from app.services.ai_service import suggest_optimal_time
from app.services.whatsapp_service import send_task_reminder
router = APIRouter()
@router.post("/", response_model=TaskResponse)
def create_task(
task: TaskCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
ai_suggested_time = None
if task.due_date and not task.scheduled_at:
ai_suggested_time = suggest_optimal_time(task.due_date, task.priority)
db_task = Task(
title=task.title,
description=task.description,
priority=task.priority,
due_date=task.due_date,
scheduled_at=task.scheduled_at,
ai_suggested_time=ai_suggested_time,
owner_id=current_user.id
)
db.add(db_task)
db.commit()
db.refresh(db_task)
return db_task
@router.get("/", response_model=List[TaskResponse])
def get_tasks(
skip: int = 0,
limit: int = 100,
status: TaskStatus = None,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
query = db.query(Task).filter(Task.owner_id == current_user.id)
if status:
query = query.filter(Task.status == status)
tasks = query.offset(skip).limit(limit).all()
return tasks
@router.get("/{task_id}", response_model=TaskResponse)
def get_task(
task_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
task = db.query(Task).filter(
Task.id == task_id,
Task.owner_id == current_user.id
).first()
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found"
)
return task
@router.put("/{task_id}", response_model=TaskResponse)
def update_task(
task_id: int,
task_update: TaskUpdate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
task = db.query(Task).filter(
Task.id == task_id,
Task.owner_id == current_user.id
).first()
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found"
)
update_data = task_update.dict(exclude_unset=True)
if task_update.status == TaskStatus.COMPLETED and task.status != TaskStatus.COMPLETED:
update_data["completed_at"] = datetime.utcnow()
for field, value in update_data.items():
setattr(task, field, value)
db.commit()
db.refresh(task)
return task
@router.delete("/{task_id}")
def delete_task(
task_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
task = db.query(Task).filter(
Task.id == task_id,
Task.owner_id == current_user.id
).first()
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found"
)
db.delete(task)
db.commit()
return {"message": "Task deleted successfully"}
@router.post("/{task_id}/send-reminder")
def send_reminder(
task_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
task = db.query(Task).filter(
Task.id == task_id,
Task.owner_id == current_user.id
).first()
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found"
)
if not current_user.whatsapp_number:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="WhatsApp number not configured"
)
success = send_task_reminder(current_user.whatsapp_number, task)
if success:
task.whatsapp_reminder_sent = True
db.commit()
return {"message": "Reminder sent successfully"}
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to send reminder"
)

0
app/core/__init__.py Normal file
View File

31
app/core/auth.py Normal file
View File

@ -0,0 +1,31 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.models.user import User
from app.core.security import verify_token
security = HTTPBearer()
def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db)
) -> User:
payload = verify_token(credentials.credentials)
email = payload.get("sub")
user = db.query(User).filter(User.email == email).first()
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found"
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
return user

23
app/core/config.py Normal file
View File

@ -0,0 +1,23 @@
import os
from pydantic import BaseSettings
class Settings(BaseSettings):
SECRET_KEY: str = os.getenv("SECRET_KEY", "your-secret-key-here")
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
TWILIO_ACCOUNT_SID: str = os.getenv("TWILIO_ACCOUNT_SID", "")
TWILIO_AUTH_TOKEN: str = os.getenv("TWILIO_AUTH_TOKEN", "")
TWILIO_WHATSAPP_FROM: str = os.getenv("TWILIO_WHATSAPP_FROM", "")
OPENAI_API_KEY: str = os.getenv("OPENAI_API_KEY", "")
PROJECT_NAME: str = "WhatsApp AI Task Scheduling Service"
PROJECT_VERSION: str = "1.0.0"
class Config:
env_file = ".env"
settings = Settings()

46
app/core/security.py Normal file
View File

@ -0,0 +1,46 @@
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import HTTPException, status
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def verify_token(token: str) -> dict:
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
email: str = payload.get("sub")
if email is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return payload
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)

0
app/db/__init__.py Normal file
View File

3
app/db/base.py Normal file
View File

@ -0,0 +1,3 @@
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

22
app/db/session.py Normal file
View File

@ -0,0 +1,22 @@
from pathlib import Path
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
DB_DIR = Path("/app") / "storage" / "db"
DB_DIR.mkdir(parents=True, exist_ok=True)
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_DIR}/db.sqlite"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

0
app/models/__init__.py Normal file
View File

40
app/models/task.py Normal file
View File

@ -0,0 +1,40 @@
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, ForeignKey, Enum
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from app.db.base import Base
import enum
class TaskStatus(str, enum.Enum):
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
CANCELLED = "cancelled"
class TaskPriority(str, enum.Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
URGENT = "urgent"
class Task(Base):
__tablename__ = "tasks"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, nullable=False)
description = Column(Text, nullable=True)
status = Column(Enum(TaskStatus), default=TaskStatus.PENDING)
priority = Column(Enum(TaskPriority), default=TaskPriority.MEDIUM)
due_date = Column(DateTime(timezone=True), nullable=True)
scheduled_at = Column(DateTime(timezone=True), nullable=True)
ai_suggested_time = Column(DateTime(timezone=True), nullable=True)
whatsapp_reminder_sent = Column(Boolean, default=False)
whatsapp_completion_sent = Column(Boolean, default=False)
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
completed_at = Column(DateTime(timezone=True), nullable=True)
owner = relationship("User", back_populates="tasks")

20
app/models/user.py Normal file
View File

@ -0,0 +1,20 @@
from sqlalchemy import Column, Integer, String, DateTime, Boolean
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from app.db.base import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
full_name = Column(String, nullable=True)
phone_number = Column(String, nullable=True)
whatsapp_number = Column(String, nullable=True)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
tasks = relationship("Task", back_populates="owner")

0
app/services/__init__.py Normal file
View File

133
app/services/ai_service.py Normal file
View File

@ -0,0 +1,133 @@
import logging
from datetime import datetime, timedelta
from typing import Optional, List
import openai
from app.core.config import settings
from app.models.task import TaskPriority
logger = logging.getLogger(__name__)
class AIService:
def __init__(self):
if settings.OPENAI_API_KEY:
openai.api_key = settings.OPENAI_API_KEY
self.client = openai.OpenAI(api_key=settings.OPENAI_API_KEY)
else:
self.client = None
logger.warning("OpenAI API key not configured. AI functionality disabled.")
def suggest_optimal_time(self, due_date: datetime, priority: TaskPriority) -> Optional[datetime]:
if not self.client:
return self._fallback_scheduling(due_date, priority)
try:
current_time = datetime.utcnow()
time_until_due = due_date - current_time
if time_until_due.total_seconds() <= 0:
return current_time + timedelta(hours=1)
prompt = f"""
Based on task management best practices, suggest an optimal time to work on a task with the following details:
Current time: {current_time.strftime('%Y-%m-%d %H:%M:%S')}
Due date: {due_date.strftime('%Y-%m-%d %H:%M:%S')}
Priority: {priority.value}
Time available: {time_until_due.days} days, {time_until_due.seconds // 3600} hours
Consider:
- Task priority (urgent tasks should be scheduled sooner)
- Optimal work hours (9 AM - 5 PM on weekdays)
- Buffer time before due date
- Work-life balance
Respond with only the suggested datetime in format: YYYY-MM-DD HH:MM:SS
"""
response = self.client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "You are a task scheduling AI assistant."},
{"role": "user", "content": prompt}
],
max_tokens=50,
temperature=0.3
)
suggested_time_str = response.choices[0].message.content.strip()
suggested_time = datetime.strptime(suggested_time_str, '%Y-%m-%d %H:%M:%S')
if suggested_time < current_time:
suggested_time = current_time + timedelta(hours=1)
elif suggested_time > due_date:
suggested_time = due_date - timedelta(hours=2)
return suggested_time
except Exception as e:
logger.error(f"AI scheduling failed: {str(e)}")
return self._fallback_scheduling(due_date, priority)
def _fallback_scheduling(self, due_date: datetime, priority: TaskPriority) -> datetime:
current_time = datetime.utcnow()
time_until_due = due_date - current_time
if priority == TaskPriority.URGENT:
return current_time + timedelta(hours=1)
elif priority == TaskPriority.HIGH:
return current_time + timedelta(hours=min(6, time_until_due.total_seconds() // 3600 // 4))
elif priority == TaskPriority.MEDIUM:
return current_time + timedelta(hours=min(24, time_until_due.total_seconds() // 3600 // 2))
else: # LOW priority
return current_time + timedelta(hours=min(48, time_until_due.total_seconds() // 3600 * 0.75))
def generate_task_insights(self, tasks: List) -> str:
if not self.client:
return "AI insights not available. Please configure OpenAI API key."
try:
task_summary = []
for task in tasks:
task_summary.append(f"- {task.title} (Priority: {task.priority.value}, Status: {task.status.value})")
prompt = f"""
Analyze the following task list and provide productivity insights:
{chr(10).join(task_summary)}
Provide insights on:
1. Task distribution by priority
2. Potential bottlenecks
3. Productivity recommendations
4. Time management suggestions
Keep the response concise and actionable (max 200 words).
"""
response = self.client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "You are a productivity analysis AI assistant."},
{"role": "user", "content": prompt}
],
max_tokens=250,
temperature=0.7
)
return response.choices[0].message.content.strip()
except Exception as e:
logger.error(f"AI insights generation failed: {str(e)}")
return "Unable to generate insights at this time."
ai_service = AIService()
def suggest_optimal_time(due_date: datetime, priority: TaskPriority) -> Optional[datetime]:
return ai_service.suggest_optimal_time(due_date, priority)
def generate_task_insights(tasks: List) -> str:
return ai_service.generate_task_insights(tasks)

View File

@ -0,0 +1,129 @@
import logging
from datetime import datetime, timedelta
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from sqlalchemy.orm import Session
from app.db.session import SessionLocal
from app.models.task import Task, TaskStatus
from app.models.user import User
from app.services.whatsapp_service import send_task_reminder, send_task_overdue
logger = logging.getLogger(__name__)
class SchedulerService:
def __init__(self):
self.scheduler = BackgroundScheduler()
self.scheduler.start()
self._setup_jobs()
def _setup_jobs(self):
self.scheduler.add_job(
func=self.send_reminders,
trigger=CronTrigger(hour=9, minute=0), # Daily at 9 AM
id='daily_reminders',
name='Send daily task reminders',
replace_existing=True
)
self.scheduler.add_job(
func=self.check_overdue_tasks,
trigger=CronTrigger(hour=18, minute=0), # Daily at 6 PM
id='overdue_check',
name='Check for overdue tasks',
replace_existing=True
)
def send_reminders(self):
logger.info("Starting daily reminder job")
db: Session = SessionLocal()
try:
# Get tasks scheduled for today that haven't been reminded
today = datetime.utcnow().date()
tomorrow = today + timedelta(days=1)
tasks = db.query(Task).join(User).filter(
Task.status.in_([TaskStatus.PENDING, TaskStatus.IN_PROGRESS]),
Task.scheduled_at >= datetime.combine(today, datetime.min.time()),
Task.scheduled_at < datetime.combine(tomorrow, datetime.min.time()),
Task.whatsapp_reminder_sent == False,
User.whatsapp_number.isnot(None)
).all()
sent_count = 0
for task in tasks:
if send_task_reminder(task.owner.whatsapp_number, task):
task.whatsapp_reminder_sent = True
sent_count += 1
db.commit()
logger.info(f"Sent {sent_count} reminders")
except Exception as e:
logger.error(f"Error in reminder job: {str(e)}")
db.rollback()
finally:
db.close()
def check_overdue_tasks(self):
logger.info("Starting overdue task check")
db: Session = SessionLocal()
try:
now = datetime.utcnow()
overdue_tasks = db.query(Task).join(User).filter(
Task.status.in_([TaskStatus.PENDING, TaskStatus.IN_PROGRESS]),
Task.due_date < now,
User.whatsapp_number.isnot(None)
).all()
sent_count = 0
for task in overdue_tasks:
if send_task_overdue(task.owner.whatsapp_number, task):
sent_count += 1
logger.info(f"Sent {sent_count} overdue notifications")
except Exception as e:
logger.error(f"Error in overdue check job: {str(e)}")
finally:
db.close()
def schedule_task_reminder(self, task_id: int, remind_at: datetime):
job_id = f"task_reminder_{task_id}"
self.scheduler.add_job(
func=self._send_individual_reminder,
trigger='date',
run_date=remind_at,
args=[task_id],
id=job_id,
name=f'Reminder for task {task_id}',
replace_existing=True
)
def _send_individual_reminder(self, task_id: int):
db: Session = SessionLocal()
try:
task = db.query(Task).join(User).filter(
Task.id == task_id,
User.whatsapp_number.isnot(None)
).first()
if task and task.status in [TaskStatus.PENDING, TaskStatus.IN_PROGRESS]:
send_task_reminder(task.owner.whatsapp_number, task)
task.whatsapp_reminder_sent = True
db.commit()
except Exception as e:
logger.error(f"Error sending individual reminder for task {task_id}: {str(e)}")
finally:
db.close()
def shutdown(self):
self.scheduler.shutdown()
scheduler_service = SchedulerService()

View File

@ -0,0 +1,64 @@
import logging
from typing import Optional
from twilio.rest import Client
from twilio.base.exceptions import TwilioException
from app.core.config import settings
from app.models.task import Task
logger = logging.getLogger(__name__)
class WhatsAppService:
def __init__(self):
if settings.TWILIO_ACCOUNT_SID and settings.TWILIO_AUTH_TOKEN:
self.client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)
else:
self.client = None
logger.warning("Twilio credentials not configured. WhatsApp functionality disabled.")
def send_message(self, to_number: str, message: str) -> bool:
if not self.client:
logger.error("WhatsApp service not configured")
return False
try:
# Ensure phone number is in WhatsApp format
if not to_number.startswith("whatsapp:"):
to_number = f"whatsapp:{to_number}"
message = self.client.messages.create(
body=message,
from_=settings.TWILIO_WHATSAPP_FROM,
to=to_number
)
logger.info(f"WhatsApp message sent successfully. SID: {message.sid}")
return True
except TwilioException as e:
logger.error(f"Failed to send WhatsApp message: {str(e)}")
return False
except Exception as e:
logger.error(f"Unexpected error sending WhatsApp message: {str(e)}")
return False
whatsapp_service = WhatsAppService()
def send_task_reminder(phone_number: str, task: Task) -> bool:
due_info = f" (Due: {task.due_date.strftime('%Y-%m-%d %H:%M')})" if task.due_date else ""
message = f"🔔 Task Reminder: {task.title}\n\n{task.description or 'No description provided'}{due_info}\n\nPriority: {task.priority.value.upper()}"
return whatsapp_service.send_message(phone_number, message)
def send_task_completion(phone_number: str, task: Task) -> bool:
message = f"✅ Task Completed: {task.title}\n\nCongratulations! You've successfully completed this task."
return whatsapp_service.send_message(phone_number, message)
def send_task_overdue(phone_number: str, task: Task) -> bool:
overdue_days = (task.due_date - task.created_at).days if task.due_date else 0
message = f"⚠️ Task Overdue: {task.title}\n\nThis task is overdue by {overdue_days} days. Please complete it as soon as possible.\n\nPriority: {task.priority.value.upper()}"
return whatsapp_service.send_message(phone_number, message)

15
requirements.txt Normal file
View File

@ -0,0 +1,15 @@
fastapi==0.104.1
uvicorn==0.24.0
sqlalchemy==2.0.23
alembic==1.13.0
pydantic==2.5.0
python-multipart==0.0.6
passlib==1.7.4
python-jose==3.3.0
bcrypt==4.1.2
httpx==0.25.2
python-dotenv==1.0.0
apscheduler==3.10.4
openai==1.3.7
twilio==8.10.3
ruff==0.1.6