From bf2664b58894e7edfe34c8dcbaaddeb7c7a657c4 Mon Sep 17 00:00:00 2001 From: Isabella Oren Date: Sun, 24 May 2026 18:37:53 +0300 Subject: [PATCH] Implement telegram integration for task creation and event extraction, plus saving to db --- .env.example | 4 +- app/main.py | 7 ++- app/models/__init__.py | 3 ++ app/models/task_input.py | 25 +++++++++ app/services/task_input_service.py | 28 ++++++++++ app/services/telegram_bot.py | 86 ++++++++++++++++++++++++++++++ 6 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 app/models/__init__.py create mode 100644 app/models/task_input.py create mode 100644 app/services/task_input_service.py create mode 100644 app/services/telegram_bot.py diff --git a/.env.example b/.env.example index 2a3255c..6492bb1 100644 --- a/.env.example +++ b/.env.example @@ -8,4 +8,6 @@ POSTGRES_DB=myapp_dev POSTGRES_HOST=db POSTGRES_PORT=5432 DEBUG=false -LOG_LEVEL=INFO \ No newline at end of file +LOG_LEVEL=INFO +TELEGRAM_BOT_TOKEN=your_bot_token_here +TELEGRAM_WEBHOOK_SECRET=some-long-random-secret \ No newline at end of file diff --git a/app/main.py b/app/main.py index 77a5f8f..041f871 100644 --- a/app/main.py +++ b/app/main.py @@ -3,7 +3,8 @@ import logging from contextlib import asynccontextmanager from app.config import settings -from app.database import engine +from app.database import engine, Base +from app.models import TaskInput logger = logging.getLogger(__name__) @@ -26,6 +27,10 @@ async def lifespan(app: FastAPI): app = FastAPI(lifespan=lifespan) +@app.on_event("startup") +async def on_startup(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) # CORS if needed during development app.add_middleware( diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..682cd40 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,3 @@ +from app.models.task_input import TaskInput + +__all__ = ["TaskInput"] \ No newline at end of file diff --git a/app/models/task_input.py b/app/models/task_input.py new file mode 100644 index 0000000..323c836 --- /dev/null +++ b/app/models/task_input.py @@ -0,0 +1,25 @@ +from datetime import datetime +from sqlalchemy import DateTime, Integer, String, Text, JSON +from sqlalchemy.orm import Mapped, mapped_column + +from app.database import Base + + +class TaskInput(Base): + __tablename__ = "task_inputs" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) + + source: Mapped[str] = mapped_column(String(50), nullable=False) + original_text: Mapped[str] = mapped_column(Text, nullable=False) + + extracted_event: Mapped[dict] = mapped_column(JSON, nullable=False) + + telegram_chat_id: Mapped[str | None] = mapped_column(String(100), nullable=True) + telegram_message_id: Mapped[str | None] = mapped_column(String(100), nullable=True) + + created_at: Mapped[datetime] = mapped_column( + DateTime, + default=datetime.utcnow, + nullable=False, + ) \ No newline at end of file diff --git a/app/services/task_input_service.py b/app/services/task_input_service.py new file mode 100644 index 0000000..35971cf --- /dev/null +++ b/app/services/task_input_service.py @@ -0,0 +1,28 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.task_input import TaskInput + + +async def save_task_input( + db: AsyncSession, + source: str, + original_text: str, + extracted_event: dict, + telegram_chat_id: str | None = None, + telegram_message_id: str | None = None, +) -> TaskInput: + + task_input = TaskInput( + source=source, + original_text=original_text, + extracted_event=extracted_event, + telegram_chat_id=telegram_chat_id, + telegram_message_id=telegram_message_id, + ) + + db.add(task_input) + + await db.commit() + await db.refresh(task_input) + + return task_input \ No newline at end of file diff --git a/app/services/telegram_bot.py b/app/services/telegram_bot.py new file mode 100644 index 0000000..86979a9 --- /dev/null +++ b/app/services/telegram_bot.py @@ -0,0 +1,86 @@ +import os +import logging + +from dotenv import load_dotenv +from telegram import Update +from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, ContextTypes, filters +from app.services.event_extractor import extract_event_from_text +from app.services.openrouter_client import OpenRouterError +from sqlalchemy.ext.asyncio import AsyncSession + +from app.database import AsyncSessionLocal +from app.services.task_input_service import save_task_input + +load_dotenv() + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + await update.message.reply_text( + "Hi! Send me a task and I’ll turn it into a TaskCraft event draft." + ) + + +async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + message_text = update.message.text + timezone = "Asia/Jerusalem" + + await update.message.reply_text("Processing task... ⏳") + + try: + event = await extract_event_from_text(message_text, timezone) + + async with AsyncSessionLocal() as db: + await save_task_input( + db=db, + source="telegram", + original_text=message_text, + extracted_event=event, + telegram_chat_id=str(update.effective_chat.id), + telegram_message_id=str(update.message.message_id), + ) + response = f""" +✅ Event Draft Created + +📌 Title: {event["title"]} +🕒 Start: {event["start_at"]} +🕔 End: {event["end_at"]} +📅 All day: {event["all_day"]} +🎯 Confidence: {event["confidence"]} + +📝 Notes: +{event["notes"]} +""".strip() + + if event["missing_info"]: + response += "\n\n❓ Missing info:\n" + "\n".join( + f"- {item}" for item in event["missing_info"] + ) + + await update.message.reply_text(response) + + except OpenRouterError as e: + await update.message.reply_text(f"❌ OpenRouter failed:\n{str(e)}") + + except Exception as e: + await update.message.reply_text(f"❌ Something went wrong:\n{str(e)}") + +def run_bot() -> None: + token = os.getenv("TELEGRAM_BOT_TOKEN") + + if not token: + raise RuntimeError("Missing TELEGRAM_BOT_TOKEN in .env") + + app = ApplicationBuilder().token(token).build() + + app.add_handler(CommandHandler("start", start)) + app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)) + + logger.info("Starting Telegram bot with polling...") + app.run_polling() + + +if __name__ == "__main__": + run_bot() \ No newline at end of file