alphabagibagi commited on
Commit
06939f3
·
verified ·
1 Parent(s): 0a56392

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +15 -0
  2. chat_bot.py +709 -0
  3. requirements.txt +6 -0
  4. start_chat.py +3 -0
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ COPY . .
10
+
11
+ # Make startup script executable
12
+ RUN chmod +x start.sh
13
+
14
+ # Run both bot and admin console
15
+ CMD ["./start_chat.sh"]
chat_bot.py ADDED
@@ -0,0 +1,709 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import requests
4
+ import base64
5
+ import io
6
+ import re
7
+ import asyncio
8
+ from datetime import datetime, timezone
9
+ from dotenv import load_dotenv
10
+ from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
11
+ from telegram.ext import Application, CommandHandler, CallbackQueryHandler, MessageHandler, filters, ContextTypes
12
+ from supabase import create_client, Client
13
+
14
+ # Load environment variables
15
+ load_dotenv()
16
+
17
+ # Configuration
18
+ TELEGRAM_TOKEN = os.getenv("TELEGRAM_CHAT_BOT_TOKEN")
19
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
20
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY")
21
+ POLLINATIONS_KEY = os.getenv("POLLINATIONS_KEY", "")
22
+
23
+ # Pollinations API Endpoint
24
+ POLLINATIONS_CHAT_URL = "https://gen.pollinations.ai/v1/chat/completions"
25
+
26
+ # Streaming Configuration
27
+ STREAM_UPDATE_INTERVAL = 0.5 # Update message every 0.5 seconds
28
+ MIN_CHUNK_SIZE = 50 # Minimum characters before updating
29
+
30
+ # Available Models
31
+ AVAILABLE_MODELS = {
32
+ "openai": {
33
+ "name": "openai",
34
+ "display": "🧠 GPT-5.1",
35
+ "description": "Text generation, Web search",
36
+ "supports_vision": False
37
+ }
38
+ }
39
+
40
+ DEFAULT_MODEL = "openai"
41
+
42
+ # Logging setup
43
+ logging.basicConfig(
44
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
45
+ )
46
+ logger = logging.getLogger(__name__)
47
+
48
+ # Supabase Client
49
+ supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
50
+
51
+
52
+ async def get_or_create_user(user, chat_id: int) -> str:
53
+ """Get user UUID from database, or create if not exists."""
54
+ try:
55
+ logger.info(f"Getting or creating user for chat_id: {chat_id}")
56
+
57
+ user_res = supabase.table("telegram_users").select("id, bots_joined").eq("chat_id", chat_id).execute()
58
+
59
+ if user_res.data:
60
+ user_data = user_res.data[0]
61
+ user_uuid = user_data['id']
62
+ bots_joined = user_data.get('bots_joined') or []
63
+
64
+ logger.info(f"User found: {user_uuid}")
65
+
66
+ if 'chat_bot' not in bots_joined:
67
+ bots_joined.append('chat_bot')
68
+ supabase.table("telegram_users").update({
69
+ "bots_joined": bots_joined
70
+ }).eq("id", user_uuid).execute()
71
+ logger.info(f"Updated bots_joined for user {user_uuid}")
72
+
73
+ return user_uuid
74
+ else:
75
+ logger.info(f"User not found, creating new user for chat_id: {chat_id}")
76
+
77
+ data = {
78
+ "p_chat_id": chat_id,
79
+ "p_username": user.username,
80
+ "p_first_name": user.first_name,
81
+ "p_last_name": user.last_name,
82
+ "p_language_code": user.language_code,
83
+ "p_is_bot": user.is_bot
84
+ }
85
+ supabase.rpc("upsert_telegram_user", data).execute()
86
+
87
+ user_res = supabase.table("telegram_users").select("id").eq("chat_id", chat_id).execute()
88
+ user_uuid = user_res.data[0]['id']
89
+
90
+ logger.info(f"New user created: {user_uuid}")
91
+
92
+ supabase.table("telegram_users").update({
93
+ "bots_joined": ['chat_bot']
94
+ }).eq("id", user_uuid).execute()
95
+
96
+ return user_uuid
97
+
98
+ except Exception as e:
99
+ logger.error(f"Error get_or_create_user: {e}")
100
+ import traceback
101
+ logger.error(f"Traceback: {traceback.format_exc()}")
102
+ return None
103
+
104
+
105
+ async def get_chat_history(user_uuid: str, limit: int = 20):
106
+ """Retrieve chat history from database."""
107
+ try:
108
+ logger.info(f"Fetching chat history for user: {user_uuid}, limit: {limit}")
109
+
110
+ response = supabase.table("chat_history")\
111
+ .select("role, content, image_url")\
112
+ .eq("user_id", user_uuid)\
113
+ .order("created_at", desc=True)\
114
+ .limit(limit)\
115
+ .execute()
116
+
117
+ history_count = len(response.data) if response.data else 0
118
+ logger.info(f"Retrieved {history_count} messages from history")
119
+
120
+ return response.data[::-1] if response.data else []
121
+
122
+ except Exception as e:
123
+ logger.error(f"Error fetching history: {e}")
124
+ import traceback
125
+ logger.error(f"Traceback: {traceback.format_exc()}")
126
+ return []
127
+
128
+
129
+ async def save_chat_message(user_uuid: str, role: str, content: str, model_used: str = None, image_url: str = None):
130
+ """Save a message to chat history."""
131
+ try:
132
+ data = {
133
+ "user_id": user_uuid,
134
+ "role": role,
135
+ "content": content,
136
+ "model_used": model_used,
137
+ "image_url": image_url
138
+ }
139
+ logger.info(f"Attempting to save message - User: {user_uuid}, Role: {role}, Content length: {len(content) if content else 0}")
140
+
141
+ result = supabase.table("chat_history").insert(data).execute()
142
+
143
+ logger.info(f"Message saved successfully - ID: {result.data[0]['id'] if result.data else 'unknown'}")
144
+ return True
145
+
146
+ except Exception as e:
147
+ logger.error(f"Error saving message: {e}")
148
+ logger.error(f"Data attempted: user_id={user_uuid}, role={role}, model_used={model_used}")
149
+ import traceback
150
+ logger.error(f"Traceback: {traceback.format_exc()}")
151
+ return False
152
+
153
+
154
+ def build_messages_payload(history: list, new_message: str, image_url: str = None):
155
+ """Build messages array for Pollinations API."""
156
+ messages = []
157
+
158
+ for msg in history:
159
+ content_parts = []
160
+
161
+ if msg.get('content'):
162
+ content_parts.append({
163
+ "type": "text",
164
+ "text": msg['content']
165
+ })
166
+
167
+ if msg.get('image_url'):
168
+ content_parts.append({
169
+ "type": "image_url",
170
+ "image_url": {"url": msg['image_url']}
171
+ })
172
+
173
+ messages.append({
174
+ "role": msg['role'],
175
+ "content": content_parts if len(content_parts) > 1 else msg['content']
176
+ })
177
+
178
+ if image_url:
179
+ new_content = [
180
+ {"type": "text", "text": new_message},
181
+ {"type": "image_url", "image_url": {"url": image_url}}
182
+ ]
183
+ else:
184
+ new_content = new_message
185
+
186
+ messages.append({
187
+ "role": "user",
188
+ "content": new_content
189
+ })
190
+
191
+ return messages
192
+
193
+
194
+ def clean_markdown_for_telegram(text: str) -> str:
195
+ """Convert markdown to HTML for better Telegram compatibility."""
196
+
197
+ text = re.sub(r'###\s+(.+)', r'<b>\1</b>', text)
198
+ text = re.sub(r'##\s+(.+)', r'<b>\1</b>', text)
199
+ text = re.sub(r'#\s+(.+)', r'<b>\1</b>', text)
200
+
201
+ text = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', text)
202
+
203
+ BULLET_PLACEHOLDER = "___BULLET___"
204
+ text = re.sub(r'^\*\s', BULLET_PLACEHOLDER, text, flags=re.MULTILINE)
205
+ text = re.sub(r'\*([^\*\n]+?)\*', r'<i>\1</i>', text)
206
+ text = text.replace(BULLET_PLACEHOLDER, '• ')
207
+
208
+ text = re.sub(r'`([^`]+)`', r'<code>\1</code>', text)
209
+
210
+ text = re.sub(r'```(\w+)?\n([\s\S]+?)\n```', r'<pre>\2</pre>', text)
211
+
212
+ return text
213
+
214
+
215
+ async def call_pollinations_api_streaming(messages: list, model: str, callback):
216
+ """
217
+ Call Pollinations API with streaming support.
218
+
219
+ Args:
220
+ messages: List of message objects
221
+ model: Model name to use
222
+ callback: Async function to call with each chunk of text
223
+
224
+ Returns:
225
+ Complete text response
226
+ """
227
+ try:
228
+ headers = {
229
+ "Content-Type": "application/json"
230
+ }
231
+
232
+ if POLLINATIONS_KEY:
233
+ headers["Authorization"] = f"Bearer {POLLINATIONS_KEY}"
234
+
235
+ payload = {
236
+ "model": model,
237
+ "messages": messages,
238
+ "stream": True # Enable streaming
239
+ }
240
+
241
+ logger.info(f"Calling Pollinations API (streaming) with model: {model}")
242
+
243
+ # Use requests with stream=True
244
+ response = requests.post(
245
+ POLLINATIONS_CHAT_URL,
246
+ json=payload,
247
+ headers=headers,
248
+ stream=True,
249
+ timeout=60
250
+ )
251
+
252
+ response.raise_for_status()
253
+
254
+ full_text = ""
255
+
256
+ # Process streaming response
257
+ for line in response.iter_lines():
258
+ if line:
259
+ line = line.decode('utf-8')
260
+
261
+ # SSE format: data: {...}
262
+ if line.startswith('data: '):
263
+ data_str = line[6:] # Remove 'data: ' prefix
264
+
265
+ # Check for end of stream
266
+ if data_str.strip() == '[DONE]':
267
+ break
268
+
269
+ try:
270
+ import json
271
+ data = json.loads(data_str)
272
+
273
+ # Extract content from delta
274
+ if 'choices' in data and len(data['choices']) > 0:
275
+ delta = data['choices'][0].get('delta', {})
276
+ content = delta.get('content', '')
277
+
278
+ if content:
279
+ full_text += content
280
+ # Call callback with accumulated text
281
+ await callback(full_text)
282
+
283
+ except json.JSONDecodeError:
284
+ # Skip malformed JSON
285
+ continue
286
+
287
+ logger.info(f"Streaming complete. Total length: {len(full_text)}")
288
+ return full_text
289
+
290
+ except requests.exceptions.RequestException as e:
291
+ logger.error(f"API request error: {e}")
292
+ return None
293
+ except Exception as e:
294
+ logger.error(f"Error in streaming API call: {e}")
295
+ import traceback
296
+ logger.error(f"Traceback: {traceback.format_exc()}")
297
+ return None
298
+
299
+
300
+ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
301
+ """Handle /start command."""
302
+ user = update.effective_user
303
+ chat_id = update.effective_chat.id
304
+
305
+ user_uuid = await get_or_create_user(user, chat_id)
306
+
307
+ welcome_text = (
308
+ "**Welcome to Icebox AI Chat!**\n\n"
309
+ "I'm an AI assistant that can help you:\n"
310
+ "• Answer questions\n"
311
+ "• Write and summarize text\n"
312
+ "• Search the web\n\n"
313
+ "```\n"
314
+ "📟 How to start:\n"
315
+ "Just send any message.\n\n"
316
+ "Examples:\n"
317
+ "> Give me business ideas\n"
318
+ "> Summarize this article\n"
319
+ "> Find the latest AI news\n"
320
+ "```\n\n"
321
+ "**⚙️ Commands:**\n"
322
+ "• /help — show help\n"
323
+ "• /model — model info\n"
324
+ "• /clear — delete chat history"
325
+ )
326
+
327
+ keyboard = [
328
+ [InlineKeyboardButton("⚙️ Model Settings", callback_data="select_model")],
329
+ [InlineKeyboardButton("🗑️ Clear Chat History", callback_data="clear_history")],
330
+ [InlineKeyboardButton("🏞️ Create Image", url="https://t.me/iceboxai_bot")]
331
+ ]
332
+ reply_markup = InlineKeyboardMarkup(keyboard)
333
+
334
+ await update.message.reply_text(welcome_text, reply_markup=reply_markup, parse_mode="Markdown")
335
+
336
+
337
+ async def model_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
338
+ """Handle /model command."""
339
+ current_model = context.user_data.get('model', DEFAULT_MODEL)
340
+ model_info = AVAILABLE_MODELS.get(current_model, AVAILABLE_MODELS[DEFAULT_MODEL])
341
+
342
+ text = f"""⚙️ **Current Model Information**
343
+
344
+ **Name:** {model_info['display']}
345
+ **Description:** {model_info['description']}
346
+ **Vision Support:** {"Yes ✅" if model_info['supports_vision'] else "No ❌"}
347
+ **Streaming:** Enabled ⚡"""
348
+
349
+ keyboard = [
350
+ [InlineKeyboardButton("🔄 Change Model", callback_data="select_model")],
351
+ [InlineKeyboardButton("« Back", callback_data="back_to_chat")]
352
+ ]
353
+ reply_markup = InlineKeyboardMarkup(keyboard)
354
+
355
+ await update.message.reply_text(text, reply_markup=reply_markup, parse_mode="Markdown")
356
+
357
+
358
+ async def handle_model_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
359
+ """Handle model selection callbacks."""
360
+ query = update.callback_query
361
+ await query.answer()
362
+
363
+ data = query.data
364
+
365
+ if data == "show_help":
366
+ current_model = context.user_data.get('model', DEFAULT_MODEL)
367
+ model_info = AVAILABLE_MODELS.get(current_model, AVAILABLE_MODELS[DEFAULT_MODEL])
368
+
369
+ help_text = f"""📚 **Icebox AI Chat Help**
370
+
371
+ **Available commands:**
372
+ /start - Start the bot
373
+ /help - Show this help
374
+ /image - Generate images with AI
375
+ /model - View model information
376
+ /clear - Clear chat history
377
+
378
+ **How to use:**
379
+ Send a text message to ask questions. The bot remembers conversation context and responses stream in real-time.
380
+
381
+ **Current model:** {model_info['display']}
382
+ **Vision support:** {"Yes ✅" if model_info['supports_vision'] else "No ❌"}
383
+ **Streaming:** Enabled ⚡
384
+
385
+ **Tips:**
386
+ • Chat history is saved, so the AI remembers previous conversations
387
+ • Use /clear to start a new conversation without previous context
388
+ • Watch responses appear in real-time as the AI generates them!"""
389
+
390
+ keyboard = [
391
+ [InlineKeyboardButton("« Back", callback_data="back_to_chat")]
392
+ ]
393
+ reply_markup = InlineKeyboardMarkup(keyboard)
394
+
395
+ await query.edit_message_text(help_text, reply_markup=reply_markup, parse_mode="Markdown")
396
+
397
+ elif data == "select_model":
398
+ keyboard = []
399
+ current_model = context.user_data.get('model', DEFAULT_MODEL)
400
+
401
+ for model_key, model_info in AVAILABLE_MODELS.items():
402
+ checkmark = "✓ " if model_key == current_model else ""
403
+ button_text = f"{checkmark}{model_info['display']}"
404
+ keyboard.append([InlineKeyboardButton(button_text, callback_data=f"model_{model_key}")])
405
+
406
+ keyboard.append([InlineKeyboardButton("« Back", callback_data="back_to_chat")])
407
+
408
+ reply_markup = InlineKeyboardMarkup(keyboard)
409
+ await query.edit_message_text("⚙️ **Select Model:**", reply_markup=reply_markup, parse_mode="Markdown")
410
+
411
+ elif data.startswith("model_"):
412
+ model_key = data.replace("model_", "")
413
+ context.user_data['model'] = model_key
414
+
415
+ model_info = AVAILABLE_MODELS[model_key]
416
+
417
+ text = f"""✅ **Model Updated**
418
+
419
+ Now using: {model_info['display']}
420
+
421
+ {model_info['description']}"""
422
+
423
+ keyboard = [
424
+ [InlineKeyboardButton("⚙️ Model Settings", callback_data="select_model")],
425
+ [InlineKeyboardButton("« Back to Chat", callback_data="back_to_chat")]
426
+ ]
427
+ reply_markup = InlineKeyboardMarkup(keyboard)
428
+
429
+ await query.edit_message_text(text, reply_markup=reply_markup, parse_mode="Markdown")
430
+
431
+ elif data == "back_to_chat":
432
+ current_model = context.user_data.get('model', DEFAULT_MODEL)
433
+ model_info = AVAILABLE_MODELS.get(current_model, AVAILABLE_MODELS[DEFAULT_MODEL])
434
+
435
+ text = f"""🤖 **Icebox AI Chat**
436
+
437
+ Active model: {model_info['display']}
438
+ Streaming: Enabled ⚡
439
+
440
+ Send me a message to start chatting.
441
+
442
+ **Commands:**
443
+ /help - Show help & available commands
444
+ /image - Generate images with AI
445
+ /model - View model information
446
+ /clear - Clear chat history"""
447
+
448
+ keyboard = [
449
+ [InlineKeyboardButton("📚 Help", callback_data="show_help")],
450
+ [InlineKeyboardButton("🗑️ Clear Chat History", callback_data="clear_history")]
451
+ ]
452
+ reply_markup = InlineKeyboardMarkup(keyboard)
453
+
454
+ await query.edit_message_text(text, reply_markup=reply_markup, parse_mode="Markdown")
455
+
456
+ elif data == "clear_history":
457
+ chat_id = update.effective_chat.id
458
+
459
+ try:
460
+ user_res = supabase.table("telegram_users").select("id").eq("chat_id", chat_id).execute()
461
+ if user_res.data:
462
+ user_uuid = user_res.data[0]['id']
463
+ supabase.table("chat_history").delete().eq("user_id", user_uuid).execute()
464
+
465
+ await query.edit_message_text(
466
+ "🗑️ **Chat history cleared!**\n\nSend a message to start a new conversation.",
467
+ parse_mode="Markdown"
468
+ )
469
+ else:
470
+ await query.edit_message_text("Error: User not found.")
471
+ except Exception as e:
472
+ logger.error(f"Error clearing history: {e}")
473
+ await query.edit_message_text("Error: Failed to clear chat history.")
474
+
475
+
476
+ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
477
+ """Handle incoming text messages with streaming response."""
478
+ user = update.effective_user
479
+ chat_id = update.effective_chat.id
480
+ message_text = update.message.text
481
+
482
+ logger.info(f"Received message from chat_id: {chat_id}, user: {user.username}")
483
+
484
+ # Get or create user
485
+ user_uuid = await get_or_create_user(user, chat_id)
486
+ if not user_uuid:
487
+ logger.error(f"Failed to get user_uuid for chat_id: {chat_id}")
488
+ await update.message.reply_text("Error: Unable to identify user.")
489
+ return
490
+
491
+ logger.info(f"User UUID: {user_uuid}")
492
+
493
+ # Get current model
494
+ current_model = context.user_data.get('model', DEFAULT_MODEL)
495
+ logger.info(f"Using model: {current_model}")
496
+
497
+ # Send initial typing action
498
+ await context.bot.send_chat_action(chat_id=chat_id, action="typing")
499
+
500
+ # Get chat history
501
+ history = await get_chat_history(user_uuid, limit=20)
502
+
503
+ # Build messages payload
504
+ messages = build_messages_payload(history, message_text)
505
+
506
+ # Send initial placeholder message
507
+ reply_message = await update.message.reply_text("⏳ _Thinking..._", parse_mode="Markdown")
508
+
509
+ # Variables for streaming updates
510
+ last_update_time = asyncio.get_event_loop().time()
511
+ last_text = ""
512
+ update_lock = asyncio.Lock()
513
+
514
+ async def update_message(text):
515
+ """Callback to update message with streamed content."""
516
+ nonlocal last_update_time, last_text
517
+
518
+ current_time = asyncio.get_event_loop().time()
519
+ time_since_update = current_time - last_update_time
520
+ text_diff = len(text) - len(last_text)
521
+
522
+ # Update if enough time passed OR enough new text accumulated
523
+ should_update = (
524
+ time_since_update >= STREAM_UPDATE_INTERVAL or
525
+ text_diff >= MIN_CHUNK_SIZE
526
+ )
527
+
528
+ if should_update:
529
+ async with update_lock:
530
+ try:
531
+ # Clean and format text
532
+ cleaned_text = clean_markdown_for_telegram(text)
533
+
534
+ # Add typing indicator
535
+ display_text = cleaned_text + " ▌"
536
+
537
+ # Update message (with rate limiting protection)
538
+ if display_text != last_text:
539
+ await reply_message.edit_text(display_text, parse_mode="HTML")
540
+ last_text = display_text
541
+ last_update_time = current_time
542
+
543
+ # Keep showing typing action
544
+ await context.bot.send_chat_action(chat_id=chat_id, action="typing")
545
+
546
+ except Exception as e:
547
+ # Ignore update errors (like message not modified)
548
+ if "Message is not modified" not in str(e):
549
+ logger.warning(f"Error updating message: {e}")
550
+
551
+ # Call streaming API
552
+ ai_reply = await call_pollinations_api_streaming(messages, current_model, update_message)
553
+
554
+ if ai_reply:
555
+ logger.info(f"Received complete AI reply, length: {len(ai_reply)}")
556
+
557
+ # Save user message
558
+ await save_chat_message(user_uuid, "user", message_text, model_used=current_model)
559
+
560
+ # Save AI reply
561
+ await save_chat_message(user_uuid, "assistant", ai_reply, model_used=current_model)
562
+
563
+ # Final update - remove typing indicator
564
+ ai_reply_cleaned = clean_markdown_for_telegram(ai_reply)
565
+
566
+ try:
567
+ # Send final version without typing indicator
568
+ if len(ai_reply_cleaned) > 4096:
569
+ # Delete placeholder and send in chunks
570
+ await reply_message.delete()
571
+ for i in range(0, len(ai_reply_cleaned), 4096):
572
+ chunk = ai_reply_cleaned[i:i+4096]
573
+ try:
574
+ await update.message.reply_text(chunk, parse_mode="HTML")
575
+ except Exception as e:
576
+ logger.warning(f"HTML parse error, sending as plain text: {e}")
577
+ await update.message.reply_text(chunk)
578
+ else:
579
+ await reply_message.edit_text(ai_reply_cleaned, parse_mode="HTML")
580
+ except Exception as e:
581
+ logger.warning(f"Error in final update: {e}")
582
+ try:
583
+ await reply_message.edit_text(ai_reply_cleaned)
584
+ except:
585
+ pass
586
+ else:
587
+ logger.error("No AI reply received from API")
588
+ await reply_message.edit_text(
589
+ "⚠️ Sorry, an error occurred while contacting the AI. Please try again."
590
+ )
591
+
592
+
593
+ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
594
+ """Handle /help command."""
595
+ current_model = context.user_data.get('model', DEFAULT_MODEL)
596
+ model_info = AVAILABLE_MODELS.get(current_model, AVAILABLE_MODELS[DEFAULT_MODEL])
597
+
598
+ help_text = f"""📚 **Icebox AI Chat Help**
599
+
600
+ **Available commands:**
601
+ • /start - Start the bot
602
+ • /model - View model info
603
+ • /image - Generate AI images
604
+ • /clear - Clear chat history
605
+ • /help - Show this help
606
+
607
+ **How to use:**
608
+ Send a text message to ask questions. The bot remembers conversation context and responses stream in real-time ⚡
609
+
610
+ **Current model:** {model_info['display']}
611
+ **Vision support:** {"Yes ✅" if model_info['supports_vision'] else "No ❌"}
612
+ **Streaming:** Enabled ⚡
613
+
614
+ **Tips:**
615
+ • Chat history is saved, so the AI remembers previous conversations
616
+ • Use /clear to start a new conversation without previous context
617
+ • Watch responses appear in real-time as the AI generates them!
618
+ """
619
+
620
+ await update.message.reply_text(help_text, parse_mode="Markdown")
621
+
622
+
623
+ async def clear_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
624
+ """Handle /clear command to clear chat history."""
625
+ chat_id = update.effective_chat.id
626
+
627
+ try:
628
+ user_res = supabase.table("telegram_users").select("id").eq("chat_id", chat_id).execute()
629
+ if user_res.data:
630
+ user_uuid = user_res.data[0]['id']
631
+ supabase.table("chat_history").delete().eq("user_id", user_uuid).execute()
632
+
633
+ await update.message.reply_text(
634
+ "🗑️ **Chat history cleared!**\n\nSend a message to start a new conversation.",
635
+ parse_mode="Markdown"
636
+ )
637
+ else:
638
+ await update.message.reply_text("Error: User not found. Please type /start first.")
639
+ except Exception as e:
640
+ logger.error(f"Error clearing history: {e}")
641
+ await update.message.reply_text("Error: Failed to clear chat history.")
642
+
643
+
644
+ async def image_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
645
+ """Handle /image command - redirect to image generation bot."""
646
+ image_text = """🏞️ **Generate AI Images**
647
+
648
+ To create AI-generated images, please use our dedicated image bot:
649
+
650
+ 👉 @iceboxai_bot
651
+
652
+ Just start a chat with that bot and describe the image you want to generate!"""
653
+
654
+ await update.message.reply_text(image_text, parse_mode="Markdown")
655
+
656
+
657
+
658
+ def main():
659
+ """Start the chat bot."""
660
+ if not TELEGRAM_TOKEN:
661
+ print("Error: TELEGRAM_CHAT_BOT_TOKEN not found in environment variables.")
662
+ return
663
+
664
+ if not SUPABASE_URL or not SUPABASE_KEY:
665
+ print("Error: SUPABASE_URL or SUPABASE_KEY not found.")
666
+ return
667
+
668
+ base_url = os.getenv("TELEGRAM_API_BASE_URL")
669
+ base_file_url = os.getenv("TELEGRAM_API_FILE_URL")
670
+
671
+ # Increase timeout significantly for slow proxies (n8n/etc)
672
+ from telegram.request import HTTPXRequest
673
+ request_config = HTTPXRequest(
674
+ connect_timeout=30.0,
675
+ read_timeout=60.0,
676
+ write_timeout=30.0,
677
+ pool_timeout=30.0
678
+ )
679
+
680
+ builder = Application.builder().token(TELEGRAM_TOKEN).request(request_config)
681
+
682
+ if base_url:
683
+ builder.base_url(base_url)
684
+ print(f"Using Custom API URL: {base_url}")
685
+
686
+ if base_file_url:
687
+ builder.base_file_url(base_file_url)
688
+
689
+ application = builder.build()
690
+
691
+ # Command handlers
692
+ application.add_handler(CommandHandler("start", start))
693
+ application.add_handler(CommandHandler("model", model_command))
694
+ application.add_handler(CommandHandler("help", help_command))
695
+ application.add_handler(CommandHandler("clear", clear_command))
696
+ application.add_handler(CommandHandler("image", image_command))
697
+
698
+ # Callback query handler
699
+ application.add_handler(CallbackQueryHandler(handle_model_callback))
700
+
701
+ # Message handlers
702
+ application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text_message))
703
+
704
+ print("Icebox AI Chat Bot (Streaming Enabled) is running...")
705
+ application.run_polling()
706
+
707
+
708
+ if __name__ == "__main__":
709
+ main()
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ python-telegram-bot
2
+ supabase
3
+ python-dotenv
4
+ flask
5
+ requests
6
+ APScheduler>=3.10.0
start_chat.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ #!/bin/bash
2
+ # Start the Admin Dashboard
3
+ python chat_bot.py