File size: 11,816 Bytes
59b269a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# --- START OF FILE app.py ---
import os
import json
import logging
import threading
import base64
import io
from flask import Flask, render_template, request, Response
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:
raise ValueError("لیست کلیدهای API خالی است.")
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
# *** START: MODIFIED - افزودن ثابتهای جدید برای مهلت زمانی ***
# مهلت زمانی (به ثانیه) برای شروع دریافت پاسخ از سرور
STREAM_START_TIMEOUT = 4
# مهلت زمانی (به ثانیه) برای دریافت هر قطعه جدید از داده در حین استریم
# اگر در این مدت داده جدیدی نرسد، اتصال قطع و با کلید بعدی تلاش میشود
STREAM_READ_TIMEOUT = 15
# *** END: MODIFIED ***
# ================== پایان بخش پیکربندی ====================
@app.route('/')
def index():
return render_template('index.html')
@app.route('/chat', methods=['POST'])
def chat():
if not GEMINI_API_KEYS:
error_payload = {"type": "error", "message": "خطای سرور: هیچ کلید API پیکربندی نشده است."}
return Response(f"data: {json.dumps(error_payload)}\n\n", status=500, 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')
def stream_response():
last_error = None
for _ in range(len(GEMINI_API_KEYS)):
try:
api_key, key_index = get_next_key_with_index()
logging.info(f"تلاش برای ارسال درخواست با کلید شماره {key_index + 1}...")
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
}
# *** START: MODIFIED - استفاده از Timeout تفکیک شده ***
# timeout اول برای اتصال اولیه، timeout دوم برای فاصله بین دریافت دادهها
with requests.post(api_endpoint, json=payload, stream=True, timeout=(STREAM_START_TIMEOUT, STREAM_READ_TIMEOUT)) as response:
# *** END: MODIFIED ***
if response.status_code == 429:
logging.warning(f"کلید شماره {key_index + 1} سهمیه آن تمام شده است. در حال تلاش با کلید بعدی...")
last_error = "Rate limit exceeded"
continue
response.raise_for_status()
logging.info(f"اتصال با کلید شماره {key_index + 1} موفقیتآمیز بود. در حال استریم پاسخ...")
# بخش استریم پاسخ (بدون تغییر)
for line in response.iter_lines():
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
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
logging.info(f"استریم با کلید شماره {key_index + 1} به پایان رسید.")
return
# *** START: MODIFIED - افزودن ReadTimeout به مدیریت خطا ***
except (requests.exceptions.Timeout, requests.exceptions.ReadTimeout) as e:
if isinstance(e, requests.exceptions.ReadTimeout):
logging.warning(f"استریم با کلید شماره {key_index + 1} به دلیل عدم دریافت داده جدید متوقف شد (ReadTimeout). در حال تلاش با کلید بعدی...")
last_error = f"ReadTimeout after {STREAM_READ_TIMEOUT} seconds of inactivity"
else:
logging.warning(f"مهلت زمانی برای اتصال با کلید شماره {key_index + 1} به پایان رسید (ConnectTimeout). در حال تلاش با کلید بعدی...")
last_error = f"ConnectTimeout after {STREAM_START_TIMEOUT} seconds"
continue
# *** END: MODIFIED ***
except requests.exceptions.HTTPError as e:
if e.response.status_code == 403:
logging.warning(f"کلید شماره {key_index + 1} نامعتبر است (Permission Denied). در حال تلاش با کلید بعدی...")
last_error = e
continue
else:
logging.error(f"خطای HTTP پیشبینی نشده در حین تلاش با کلید {key_index + 1}: {e}")
last_error = e
break
except Exception as e:
logging.error(f"خطای پیشبینی نشده در حین تلاش با کلید {key_index + 1}: {e}")
last_error = e
break
error_message = f"متاسفانه تمام کلیدهای API موجود نامعتبر هستند یا سهمیه آنها به پایان رسیده است. لطفا بعدا تلاش کنید.\n\nآخرین خطا: {str(last_error)}"
logging.critical("هیچکدام از کلیدهای Gemini کار نکردند.")
error_payload = {"type": "error", "message": error_message}
yield f"data: {json.dumps(error_payload)}\n\n"
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 --- |