Chat / app.py
Sada10's picture
Update app.py
a02ab71 unverified
raw
history blame
12 kB
# --- START OF FILE app.py ---
import os
import json
import logging
import threading
import base64
import io
import time
from flask import Flask, render_template, request, Response, stream_with_context
import requests
import docx
# ================== بخش تنظیمات لاگ‌نویسی ==================
class NoGrpcFilter(logging.Filter):
def filter(self, record):
return not record.getMessage().startswith('ALTS creds ignored.')
def setup_logging():
log_format = '[%(asctime)s] [%(levelname)s]: %(message)s'
date_format = '%Y-%m-%d %H:%M:%S'
formatter = logging.Formatter(log_format, datefmt=date_format)
root_logger = logging.getLogger()
if root_logger.hasHandlers():
root_logger.handlers.clear()
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
console_handler.addFilter(NoGrpcFilter())
root_logger.addHandler(console_handler)
root_logger.setLevel(logging.INFO)
setup_logging()
app = Flask(__name__)
# ================== بخش پیکربندی Gemini ==================
GEMINI_MODEL_NAME = "gemini-2.5-flash"
ALL_KEYS_STR = os.environ.get("ALL_GEMINI_API_KEYS", "")
GEMINI_API_KEYS = [key.strip() for key in ALL_KEYS_STR.split(',') if key.strip()]
if not GEMINI_API_KEYS:
logging.critical("هشدار: هیچ کلید API برای Gemini در Secrets تنظیم نشده است! (ALL_GEMINI_API_KEYS)")
key_index_counter = 0
key_lock = threading.Lock()
def get_next_key_with_index():
global key_index_counter
with key_lock:
if not GEMINI_API_KEYS:
return None, -1
current_index = key_index_counter
key = GEMINI_API_KEYS[current_index]
key_index_counter = (key_index_counter + 1) % len(GEMINI_API_KEYS)
return key, current_index
# ================== پایان بخش پیکربندی ====================
@app.route('/')
def index():
return render_template('index.html')
@app.route('/chat', methods=['POST'])
def chat():
# اگر کلیدی تعریف نشده باشد، به جای ارور، یک پیام متنی معمولی میفرستیم
if not GEMINI_API_KEYS:
fake_response = {"choices": [{"delta": {"content": "⚠️ تنظیمات سرور کامل نیست (کلید API یافت نشد)."}}]}
return Response(f"data: {json.dumps(fake_response)}\n\n", mimetype='text/event-stream')
data = request.json
system_instruction = "تو چت بات هوش مصنوعی آلفا هستی و توسط برنامه هوش مصنوعی آلفا توسعه داده شدی. کمی با کاربران باحال و دوستانه صحبت کن و از ایموجی‌ها استفاده کن. همیشه پاسخ‌هایت را به زبان فارسی و یا هر زبانی که کاربر صحبت میکنه ارائه بده."
show_thoughts = data.get("show_thoughts", False)
# === بخش پردازش پیام‌ها و فایل DOCX (بدون تغییر) ===
gemini_messages = []
for msg in data.get("messages", []):
role = "model" if msg.get("role") == "assistant" else msg.get("role")
processed_parts = []
for part in msg.get("parts", []):
if part.get("text"):
processed_parts.append({"text": part["text"]})
if part.get("base64Data") and part.get("mimeType"):
mime_type = part["mimeType"]
if mime_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
try:
decoded_data = base64.b64decode(part["base64Data"])
file_stream = io.BytesIO(decoded_data)
document = docx.Document(file_stream)
full_text = "\n".join([para.text for para in document.paragraphs])
final_text_part = f"کاربر یک فایل Word آپلود کرد. محتوای متنی آن به شرح زیر است:\n\n---\n\n{full_text}\n\n---"
processed_parts.append({"text": final_text_part})
logging.info("فایل DOCX با موفقیت پردازش و متن آن استخراج شد.")
except Exception as e:
logging.error(f"خطا در پردازش فایل DOCX: {e}")
processed_parts.append({"text": "[خطا: امکان پردازش فایل Word وجود نداشت.]"})
else:
processed_parts.append({"inline_data": {"mime_type": part["mimeType"], "data": part["base64Data"]}})
if processed_parts:
if gemini_messages and gemini_messages[-1]["role"] == role:
gemini_messages[-1]["parts"].extend(processed_parts)
else:
gemini_messages.append({"role": role, "parts": processed_parts})
if not any(msg['role'] == 'user' for msg in gemini_messages):
return Response("data: [DONE]\n\n", mimetype='text/event-stream')
# === تابع اصلی استریم با مکانیزم تلاش مجدد مخفی ===
@stream_with_context
def stream_response():
# تعداد کلیدها را میگیریم
num_keys = len(GEMINI_API_KEYS)
# تعداد تلاش‌ها: حداقل 3 برابر تعداد کلیدها تلاش میکنیم تا مطمئن شویم
max_attempts = max(num_keys * 3, 5)
success_flag = False
for attempt in range(max_attempts):
try:
api_key, key_index = get_next_key_with_index()
# اگر کلیدی نبود (خیلی بعید)
if not api_key: break
logging.info(f"تلاش شماره {attempt + 1} با کلید ایندکس {key_index}...")
api_endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL_NAME}:streamGenerateContent?key={api_key}&alt=sse"
payload = {
"contents": gemini_messages,
"systemInstruction": {"parts": [{"text": system_instruction}]},
"tools": [{"google_search": {}}],
"generationConfig": {
"temperature": 0.7,
}
}
if show_thoughts:
payload["generationConfig"]["thinking_config"] = {
"include_thoughts": True
}
# *** تنظیمات Timeout بسیار مهم ***
# Connect (5): زمان وصل شدن به گوگل. اگر 5 ثانیه طول کشید یعنی شبکه خرابه یا کلید گیر کرده -> سریع قطع کن برو بعدی
# Read (120): زمان انتظار برای جواب. چون فایل داری، باید زیاد باشه (120 ثانیه) تا وسط پردازش قطع نکنه.
response = requests.post(
api_endpoint,
json=payload,
stream=True,
timeout=(5, 120)
)
# اگر کد وضعیت 200 نبود (یعنی 429، 500، 403 و...)
if response.status_code != 200:
logging.warning(f"کلید {key_index} پاسخ نداد (کد {response.status_code}). تلاش بعدی...")
response.close()
continue # بدون هیچ حرفی برو سراغ کلید بعدی (کاربر چیزی نمیفهمه)
# اگر کد 200 بود، حالا چک میکنیم استریم دیتا داره یا نه
line_iterator = response.iter_lines()
# یک متغیر برای اینکه بفهمیم آیا واقعا دیتایی فرستادیم یا نه
data_sent_in_this_attempt = False
for line in line_iterator:
if line:
decoded_line = line.decode('utf-8')
if decoded_line.startswith('data: '):
try:
chunk_data = json.loads(decoded_line[6:])
parts = chunk_data.get("candidates", [{}])[0].get("content", {}).get("parts", [])
for part in parts:
if "text" not in part or not part["text"]:
continue
# به محض اینکه اولین داده سالم رسید، یعنی موفق شدیم
data_sent_in_this_attempt = True
success_flag = True
is_a_thought = part.get("thought") is True
if show_thoughts and is_a_thought:
thought_payload = {"type": "thought", "content": part["text"]}
yield f"data: {json.dumps(thought_payload)}\n\n"
elif not is_a_thought:
sse_payload = {"choices": [{"delta": {"content": part["text"]}}]}
yield f"data: {json.dumps(sse_payload)}\n\n"
except (json.JSONDecodeError, IndexError, KeyError):
continue
# اگر حلقه تمام شد و ما دیتایی فرستاده بودیم، یعنی کار تمام است و موفق بودیم
if success_flag:
return # خروج کامل
# مدیریت تمام خطاها (Timeout, ConnectionError, ...)
except Exception as e:
logging.error(f"خطای داخلی در تلاش {attempt+1}: {e}")
# اینجا یک وقفه کوتاه 0.5 ثانیه‌ای میدهیم که CPU درگیر نشود و سریع میریم کلید بعدی
time.sleep(0.5)
continue # برو تلاش بعدی (بدون اینکه به کاربر ارور بدی)
# === بخش نهایی (فقط اگر همه تلاش‌ها شکست خورد) ===
# اگر بعد از مثلا 30 بار تلاش (بسته به تعداد کلید) هیچکدام کار نکرد:
# به جای ارسال Error Payload که باعث نمایش خطای قرمز میشه،
# یک پیام متنی معمولی از طرف ربات میفرستیم.
if not success_flag:
logging.critical("تمام کلیدها ناموفق بودند.")
fallback_message = "متاسفانه شبکه من کمی کند شده و نتوانستم پاسخ را دریافت کنم. لطفاً دوباره دکمه ارسال را بزنید 🔄"
# فرمت پیام دقیقا مثل پاسخ عادی ربات است
fallback_payload = {"choices": [{"delta": {"content": fallback_message}}]}
yield f"data: {json.dumps(fallback_payload)}\n\n"
# استفاده از stream_with_context برای حفظ کانتکست درخواست در طول حلقه طولانی
return Response(stream_response(), mimetype='text/event-stream')
if __name__ == '__main__':
if GEMINI_API_KEYS:
logging.info(f"سیستم در حالت توسعه شروع به کار کرد. تعداد {len(GEMINI_API_KEYS)} کلید شناسایی شد.")
app.run(debug=True, host='0.0.0.0', port=os.environ.get("PORT", 7860))
# --- END OF FILE app.py ---