Esmaill1 commited on
Commit ·
8c16fc3
1
Parent(s): b2c9746
Feat: Add PostgreSQL-backed chat memory using Neon DB
Browse files- bot.py +103 -7
- requirements.txt +1 -0
bot.py
CHANGED
|
@@ -5,6 +5,8 @@ import logging
|
|
| 5 |
import socket
|
| 6 |
import time
|
| 7 |
from io import BytesIO
|
|
|
|
|
|
|
| 8 |
from dotenv import load_dotenv
|
| 9 |
from telegram import Update
|
| 10 |
from telegram.ext import Application, MessageHandler, filters, ContextTypes
|
|
@@ -29,6 +31,79 @@ OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434")
|
|
| 29 |
OLLAMA_API_KEY = os.getenv("OLLAMA_API_KEY", "") # Optional: for cloud/authenticated services
|
| 30 |
VISION_MODEL = os.getenv("VISION_MODEL", "llava") # Model for image analysis
|
| 31 |
CHAT_MODEL = os.getenv("CHAT_MODEL", "mistral") # Model for quiz generation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
|
| 34 |
async def extract_text_from_image(image_bytes: bytes) -> str:
|
|
@@ -325,8 +400,8 @@ async def handle_file(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
| 325 |
await processing_msg.edit_text(f"❌ حصل مشكلة: {str(e)[:100]}")
|
| 326 |
|
| 327 |
|
| 328 |
-
async def generate_chat_response(text: str) -> str:
|
| 329 |
-
"""Generate a conversational response using Ollama API."""
|
| 330 |
system_prompt = """You are the AI Quiz Bot Assistant. 🧠
|
| 331 |
Your goal is to be friendly, helpful, and encourage users to study.
|
| 332 |
- If the user greets you, reply warmly in the same language (Arabic or English).
|
|
@@ -334,13 +409,22 @@ Your goal is to be friendly, helpful, and encourage users to study.
|
|
| 334 |
- Be concise and fun.
|
| 335 |
- You are NOT generating a quiz right now, just chatting."""
|
| 336 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
try:
|
| 338 |
payload = {
|
| 339 |
"model": CHAT_MODEL,
|
| 340 |
-
"messages":
|
| 341 |
-
{"role": "system", "content": system_prompt},
|
| 342 |
-
{"role": "user", "content": text}
|
| 343 |
-
],
|
| 344 |
"stream": False
|
| 345 |
}
|
| 346 |
|
|
@@ -374,9 +458,18 @@ async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
| 374 |
|
| 375 |
# If text is short (< 50 chars), treat it as conversation
|
| 376 |
if len(text.strip()) < 50:
|
|
|
|
|
|
|
|
|
|
| 377 |
# Show typing indicator
|
| 378 |
await context.bot.send_chat_action(chat_id=chat_id, action="typing")
|
| 379 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
await message.reply_text(response)
|
| 381 |
return
|
| 382 |
|
|
@@ -513,6 +606,9 @@ def wait_for_network():
|
|
| 513 |
|
| 514 |
def main():
|
| 515 |
"""Start the bot."""
|
|
|
|
|
|
|
|
|
|
| 516 |
# Fix for running in background thread: Ensure an event loop exists
|
| 517 |
try:
|
| 518 |
loop = asyncio.get_event_loop()
|
|
|
|
| 5 |
import socket
|
| 6 |
import time
|
| 7 |
from io import BytesIO
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
import psycopg2
|
| 10 |
from dotenv import load_dotenv
|
| 11 |
from telegram import Update
|
| 12 |
from telegram.ext import Application, MessageHandler, filters, ContextTypes
|
|
|
|
| 31 |
OLLAMA_API_KEY = os.getenv("OLLAMA_API_KEY", "") # Optional: for cloud/authenticated services
|
| 32 |
VISION_MODEL = os.getenv("VISION_MODEL", "llava") # Model for image analysis
|
| 33 |
CHAT_MODEL = os.getenv("CHAT_MODEL", "mistral") # Model for quiz generation
|
| 34 |
+
DATABASE_URL = os.getenv("DATABASE_URL") # Neon connection string
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def init_db():
|
| 38 |
+
"""Initialize the database table."""
|
| 39 |
+
if not DATABASE_URL:
|
| 40 |
+
logger.warning("DATABASE_URL not set. Chat memory will be disabled.")
|
| 41 |
+
return
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
conn = psycopg2.connect(DATABASE_URL)
|
| 45 |
+
cur = conn.cursor()
|
| 46 |
+
cur.execute("""
|
| 47 |
+
CREATE TABLE IF NOT EXISTS chat_history (
|
| 48 |
+
id SERIAL PRIMARY KEY,
|
| 49 |
+
chat_id BIGINT NOT NULL,
|
| 50 |
+
role VARCHAR(10) NOT NULL,
|
| 51 |
+
content TEXT NOT NULL,
|
| 52 |
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 53 |
+
);
|
| 54 |
+
""")
|
| 55 |
+
conn.commit()
|
| 56 |
+
cur.close()
|
| 57 |
+
conn.close()
|
| 58 |
+
logger.info("Database initialized successfully.")
|
| 59 |
+
except Exception as e:
|
| 60 |
+
logger.error(f"Error initializing database: {e}")
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def save_chat_message(chat_id: int, role: str, content: str):
|
| 64 |
+
"""Save a chat message to the database."""
|
| 65 |
+
if not DATABASE_URL:
|
| 66 |
+
return
|
| 67 |
+
|
| 68 |
+
try:
|
| 69 |
+
conn = psycopg2.connect(DATABASE_URL)
|
| 70 |
+
cur = conn.cursor()
|
| 71 |
+
cur.execute(
|
| 72 |
+
"INSERT INTO chat_history (chat_id, role, content) VALUES (%s, %s, %s)",
|
| 73 |
+
(chat_id, role, content)
|
| 74 |
+
)
|
| 75 |
+
conn.commit()
|
| 76 |
+
cur.close()
|
| 77 |
+
conn.close()
|
| 78 |
+
except Exception as e:
|
| 79 |
+
logger.error(f"Error saving chat message: {e}")
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def get_chat_history(chat_id: int, limit: int = 10):
|
| 83 |
+
"""Get recent chat history for a chat_id."""
|
| 84 |
+
if not DATABASE_URL:
|
| 85 |
+
return []
|
| 86 |
+
|
| 87 |
+
try:
|
| 88 |
+
conn = psycopg2.connect(DATABASE_URL)
|
| 89 |
+
cur = conn.cursor()
|
| 90 |
+
cur.execute(
|
| 91 |
+
"""
|
| 92 |
+
SELECT role, content FROM chat_history
|
| 93 |
+
WHERE chat_id = %s
|
| 94 |
+
ORDER BY timestamp DESC
|
| 95 |
+
LIMIT %s
|
| 96 |
+
""",
|
| 97 |
+
(chat_id, limit)
|
| 98 |
+
)
|
| 99 |
+
rows = cur.fetchall()
|
| 100 |
+
cur.close()
|
| 101 |
+
conn.close()
|
| 102 |
+
# Return reversed list (oldest to newest)
|
| 103 |
+
return [{"role": row[0], "content": row[1]} for row in rows][::-1]
|
| 104 |
+
except Exception as e:
|
| 105 |
+
logger.error(f"Error getting chat history: {e}")
|
| 106 |
+
return []
|
| 107 |
|
| 108 |
|
| 109 |
async def extract_text_from_image(image_bytes: bytes) -> str:
|
|
|
|
| 400 |
await processing_msg.edit_text(f"❌ حصل مشكلة: {str(e)[:100]}")
|
| 401 |
|
| 402 |
|
| 403 |
+
async def generate_chat_response(chat_id: int, text: str) -> str:
|
| 404 |
+
"""Generate a conversational response using Ollama API with history."""
|
| 405 |
system_prompt = """You are the AI Quiz Bot Assistant. 🧠
|
| 406 |
Your goal is to be friendly, helpful, and encourage users to study.
|
| 407 |
- If the user greets you, reply warmly in the same language (Arabic or English).
|
|
|
|
| 409 |
- Be concise and fun.
|
| 410 |
- You are NOT generating a quiz right now, just chatting."""
|
| 411 |
|
| 412 |
+
# Get recent history (limit 6 messages for context)
|
| 413 |
+
history = get_chat_history(chat_id, limit=6)
|
| 414 |
+
|
| 415 |
+
messages = [{"role": "system", "content": system_prompt}]
|
| 416 |
+
|
| 417 |
+
# Add history
|
| 418 |
+
for msg in history:
|
| 419 |
+
messages.append(msg)
|
| 420 |
+
|
| 421 |
+
# Add current user message
|
| 422 |
+
messages.append({"role": "user", "content": text})
|
| 423 |
+
|
| 424 |
try:
|
| 425 |
payload = {
|
| 426 |
"model": CHAT_MODEL,
|
| 427 |
+
"messages": messages,
|
|
|
|
|
|
|
|
|
|
| 428 |
"stream": False
|
| 429 |
}
|
| 430 |
|
|
|
|
| 458 |
|
| 459 |
# If text is short (< 50 chars), treat it as conversation
|
| 460 |
if len(text.strip()) < 50:
|
| 461 |
+
# Save user message to memory
|
| 462 |
+
save_chat_message(chat_id, "user", text)
|
| 463 |
+
|
| 464 |
# Show typing indicator
|
| 465 |
await context.bot.send_chat_action(chat_id=chat_id, action="typing")
|
| 466 |
+
|
| 467 |
+
# Generate response with history
|
| 468 |
+
response = await generate_chat_response(chat_id, text)
|
| 469 |
+
|
| 470 |
+
# Save bot response to memory
|
| 471 |
+
save_chat_message(chat_id, "assistant", response)
|
| 472 |
+
|
| 473 |
await message.reply_text(response)
|
| 474 |
return
|
| 475 |
|
|
|
|
| 606 |
|
| 607 |
def main():
|
| 608 |
"""Start the bot."""
|
| 609 |
+
# Initialize database
|
| 610 |
+
init_db()
|
| 611 |
+
|
| 612 |
# Fix for running in background thread: Ensure an event loop exists
|
| 613 |
try:
|
| 614 |
loop = asyncio.get_event_loop()
|
requirements.txt
CHANGED
|
@@ -4,3 +4,4 @@ python-dotenv>=1.0.0
|
|
| 4 |
httpx>=0.24.0
|
| 5 |
fastapi
|
| 6 |
uvicorn
|
|
|
|
|
|
| 4 |
httpx>=0.24.0
|
| 5 |
fastapi
|
| 6 |
uvicorn
|
| 7 |
+
psycopg2-binary
|