Jan2000 commited on
Commit
b53a67a
·
unverified ·
1 Parent(s): 63617fd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +103 -117
app.py CHANGED
@@ -6,11 +6,12 @@ import logging
6
  import threading
7
  import base64
8
  import io
9
- from flask import Flask, render_template, request, Response
 
10
  import requests
11
  import docx
12
 
13
- # ================== بخش تنظیمات لاگ‌نویسی (بدون تغییر) ==================
14
 
15
  class NoGrpcFilter(logging.Filter):
16
  def filter(self, record):
@@ -35,14 +36,14 @@ def setup_logging():
35
  setup_logging()
36
  app = Flask(__name__)
37
 
38
- # ================== بخش پیکربندی Gemini (با تغییر) ==================
39
 
40
  GEMINI_MODEL_NAME = "gemini-2.5-flash"
41
  ALL_KEYS_STR = os.environ.get("ALL_GEMINI_API_KEYS", "")
42
  GEMINI_API_KEYS = [key.strip() for key in ALL_KEYS_STR.split(',') if key.strip()]
43
 
44
  if not GEMINI_API_KEYS:
45
- logging.critical("هشدار: هیچ کلید API برای Gemini در Secrets تنظیم نشده است! (ALL_GEMINI_API_KEYS)")
46
 
47
  key_index_counter = 0
48
  key_lock = threading.Lock()
@@ -57,15 +58,13 @@ def get_next_key_with_index():
57
  key_index_counter = (key_index_counter + 1) % len(GEMINI_API_KEYS)
58
  return key, current_index
59
 
60
- # *** START: OPTIMIZED TIMEOUT SETTINGS ***
61
- # مهلت زمانی برای برقراری اتصال اولیه با سرور گوگل (اگر اینترنت قطع باشد یا سرور دان باشد سریع سوئیچ میکند)
62
- STREAM_CONNECT_TIMEOUT = 10
63
- # مهلت زمانی برای انتظار پردازش و دریافت پاسخ.
64
- # برای فایل‌ها بسیار مهم است که این عدد بالا باشد (مثلا 100 ثانیه) تا وسط پردازش فایل قطع نکند.
65
- STREAM_READ_TIMEOUT = 100
66
- # *** END: OPTIMIZED TIMEOUT SETTINGS ***
67
 
68
- # ================== پایان بخش پیکربندی ====================
69
 
70
  @app.route('/')
71
  def index():
@@ -74,7 +73,8 @@ def index():
74
  @app.route('/chat', methods=['POST'])
75
  def chat():
76
  if not GEMINI_API_KEYS:
77
- error_payload = {"type": "error", "message": "خطای سرور: هیچ کلید API پیکربندی نشده است."}
 
78
  return Response(f"data: {json.dumps(error_payload)}\n\n", status=500, mimetype='text/event-stream')
79
 
80
  data = request.json
@@ -82,7 +82,7 @@ def chat():
82
 
83
  show_thoughts = data.get("show_thoughts", False)
84
 
85
- # بخش پردازش پیام‌ها و فایل DOCX
86
  gemini_messages = []
87
  for msg in data.get("messages", []):
88
  role = "model" if msg.get("role") == "assistant" else msg.get("role")
@@ -94,22 +94,16 @@ def chat():
94
 
95
  if part.get("base64Data") and part.get("mimeType"):
96
  mime_type = part["mimeType"]
97
-
98
  if mime_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
99
  try:
100
  decoded_data = base64.b64decode(part["base64Data"])
101
  file_stream = io.BytesIO(decoded_data)
102
  document = docx.Document(file_stream)
103
  full_text = "\n".join([para.text for para in document.paragraphs])
104
-
105
- final_text_part = f"کاربر یک فایل Word آپلود کرد. محتوای متنی آن به شرح زیر است:\n\n---\n\n{full_text}\n\n---"
106
  processed_parts.append({"text": final_text_part})
107
- logging.info("فایل DOCX با موفقیت پردازش و متن آن استخراج شد.")
108
-
109
- except Exception as e:
110
- logging.error(f"خطا در پردازش فایل DOCX: {e}")
111
- processed_parts.append({"text": "[خطا: امکان پردازش فایل Word وجود نداشت.]"})
112
-
113
  else:
114
  processed_parts.append({"inline_data": {"mime_type": part["mimeType"], "data": part["base64Data"]}})
115
 
@@ -122,17 +116,20 @@ def chat():
122
  if not any(msg['role'] == 'user' for msg in gemini_messages):
123
  return Response("data: [DONE]\n\n", mimetype='text/event-stream')
124
 
 
125
  def stream_response():
126
- last_error = None
127
- # تلاش برای همه کلیدها تا زمانی که یکی جواب دهد
128
- # ما یک کپی از لیست کلیدها میگیریم تا مطمئن شویم حلقه به اندازه تعداد کلیدها اجرا میشود
129
- attempts = len(GEMINI_API_KEYS)
130
-
131
- for i in range(attempts):
 
132
  try:
 
133
  api_key, key_index = get_next_key_with_index()
134
- logging.info(f"تلاش {i+1} از {attempts}: ارسال درخواست با کلید شماره {key_index + 1}...")
135
-
136
  api_endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL_NAME}:streamGenerateContent?key={api_key}&alt=sse"
137
 
138
  payload = {
@@ -145,100 +142,89 @@ def chat():
145
  }
146
 
147
  if show_thoughts:
148
- payload["generationConfig"]["thinking_config"] = {
149
- "include_thoughts": True
150
- }
151
-
152
- # استفاده از تنظیمات Timeout بهینه شده
153
- # connect: زمان اتصال به سرور (سریع)
154
- # read: زمان انتظار برای پردازش فایل و پاسخ (طولانی)
155
- with requests.post(api_endpoint, json=payload, stream=True, timeout=(STREAM_CONNECT_TIMEOUT, STREAM_READ_TIMEOUT)) as response:
156
-
157
- # بررسی خطاهای HTTP
158
- if response.status_code != 200:
159
- logging.warning(f"خطای سرور با کلید {key_index + 1}: کد {response.status_code} - متن: {response.text}")
160
- # اگر محدودیت درخواست یا خطای سرور بود، برو کلید بعدی
161
- if response.status_code in [429, 500, 502, 503, 504]:
162
- last_error = f"HTTP {response.status_code}"
163
- continue # پرش سریع به کلید بعدی
164
- elif response.status_code == 403:
165
- last_error = "Invalid API Key"
166
- continue # پرش سریع
167
- else:
168
- response.raise_for_status() # برای سایر خطاها
169
-
170
- logging.info(f"اتصال با کلید شماره {key_index + 1} برقرار شد. در حال دریافت داده...")
171
-
172
- # اگر به اینجا رسیدیم یعنی اتصال موفق بوده و استریم شروع شده
173
- # حالا داده‌ها را به کلاینت می‌فرستیم
174
- has_sent_data = False
175
- for line in response.iter_lines():
176
- if line:
177
- decoded_line = line.decode('utf-8')
178
- if decoded_line.startswith('data: '):
179
- try:
180
- chunk_data = json.loads(decoded_line[6:])
181
- parts = chunk_data.get("candidates", [{}])[0].get("content", {}).get("parts", [])
 
 
 
 
 
 
182
 
183
- for part in parts:
184
- if "text" not in part or not part["text"]:
185
- continue
 
 
 
 
 
186
 
187
- has_sent_data = True
188
- is_a_thought = part.get("thought") is True
189
- if show_thoughts and is_a_thought:
190
- thought_payload = {"type": "thought", "content": part["text"]}
191
- yield f"data: {json.dumps(thought_payload)}\n\n"
192
- elif not is_a_thought:
193
- sse_payload = {"choices": [{"delta": {"content": part["text"]}}]}
194
- yield f"data: {json.dumps(sse_payload)}\n\n"
195
- except (json.JSONDecodeError, IndexError, KeyError):
196
- continue
197
-
198
- # اگر حلقه تمام شد و دیتایی ارسال شد، یعنی موفقیت آمیز بوده
199
- if has_sent_data:
200
- logging.info(f"پاسخ کامل با کلید {key_index + 1} ارسال شد.")
201
- return # خروج از کل تابع و پایان کار
202
- else:
203
- # اگر هیچ دیتایی نیامد اما کد 200 بود (مثلا پاسخ خالی)، باز هم موفقیت حساب میشه
204
- return
205
-
206
- except requests.exceptions.ConnectTimeout:
207
- logging.warning(f"Timeout در اتصال با کلید {key_index + 1}. تلاش فوری با کلید بعدی...")
208
- last_error = "Connection Timeout"
209
- continue # تلاش با کلید بعدی
210
-
211
- except requests.exceptions.ReadTimeout:
212
- # اگر ReadTimeout رخ داد یعنی گوگل خیلی طولش داده، اما معمولا با 100 ثانیه رخ نمیده.
213
- # اگر رخ داد یعنی سرور پاسخگو نیست، پس کلید بعدی.
214
- logging.warning(f"ReadTimeout (عدم پاسخگویی طولانی) با کلید {key_index + 1}. تلاش با کلید بعدی...")
215
- last_error = "Read Timeout"
216
- continue
217
-
218
- except requests.exceptions.ConnectionError:
219
- logging.warning(f"خطای شبکه (ConnectionError) با کلید {key_index + 1}. تلاش با کلید بعدی...")
220
- last_error = "Network Error"
221
- continue
222
-
223
  except Exception as e:
224
- logging.error(f"خطای پیش‌بینی نشده با کلید {key_index + 1}: {e}")
225
- last_error = str(e)
226
- # برای خطاهای ناشناخته هم به کلید بعدی شانس میدهیم
227
- continue
228
-
229
- # اگر حلقه تمام شد و هیچ کلیدی موفق نشد:
230
- error_message = "متاسفانه سرور پاسخگو نیست. لطفا دقایقی دیگر تلاش کنید."
231
- if last_error:
232
- logging.critical(f"تمام تلاش‌ها ناموفق بود. آخرین خطا: {last_error}")
 
233
 
234
- error_payload = {"type": "error", "message": error_message}
235
- yield f"data: {json.dumps(error_payload)}\n\n"
 
 
236
 
237
  return Response(stream_response(), mimetype='text/event-stream')
238
 
239
  if __name__ == '__main__':
240
- if GEMINI_API_KEYS:
241
- logging.info(f"سیستم شروع به کار کرد. تعداد {len(GEMINI_API_KEYS)} کلید شناسایی شد.")
242
  app.run(debug=True, host='0.0.0.0', port=os.environ.get("PORT", 7860))
243
 
244
  # --- END OF FILE app.py ---
 
6
  import threading
7
  import base64
8
  import io
9
+ import time
10
+ from flask import Flask, render_template, request, Response, stream_with_context
11
  import requests
12
  import docx
13
 
14
+ # ================== بخش تنظیمات لاگ‌نویسی ==================
15
 
16
  class NoGrpcFilter(logging.Filter):
17
  def filter(self, record):
 
36
  setup_logging()
37
  app = Flask(__name__)
38
 
39
+ # ================== بخش پیکربندی Gemini ==================
40
 
41
  GEMINI_MODEL_NAME = "gemini-2.5-flash"
42
  ALL_KEYS_STR = os.environ.get("ALL_GEMINI_API_KEYS", "")
43
  GEMINI_API_KEYS = [key.strip() for key in ALL_KEYS_STR.split(',') if key.strip()]
44
 
45
  if not GEMINI_API_KEYS:
46
+ logging.critical("هشدار: هیچ کلید API برای Gemini تنظیم نشده است!")
47
 
48
  key_index_counter = 0
49
  key_lock = threading.Lock()
 
58
  key_index_counter = (key_index_counter + 1) % len(GEMINI_API_KEYS)
59
  return key, current_index
60
 
61
+ # تنظیمات زمانی حیاتی
62
+ # اتصال اولیه: سریع قطع کن اگر وصل نشد (5 ثانیه)
63
+ # خواندن دیتا: صبر زیاد برای پردازش فایل‌ها (120 ثانیه)
64
+ STREAM_CONNECT_TIMEOUT = 5
65
+ STREAM_READ_TIMEOUT = 120
 
 
66
 
67
+ # ================== پایان پیکربندی ====================
68
 
69
  @app.route('/')
70
  def index():
 
73
  @app.route('/chat', methods=['POST'])
74
  def chat():
75
  if not GEMINI_API_KEYS:
76
+ # اگر هیچ کلیدی کلا وجود نداشت، چاره‌ای جز خطا نیست
77
+ error_payload = {"type": "error", "message": "خطای تنظیمات سرور: کلید API یافت نشد."}
78
  return Response(f"data: {json.dumps(error_payload)}\n\n", status=500, mimetype='text/event-stream')
79
 
80
  data = request.json
 
82
 
83
  show_thoughts = data.get("show_thoughts", False)
84
 
85
+ # --- بخش پردازش پیام‌ها و فایل (بدون تغییر) ---
86
  gemini_messages = []
87
  for msg in data.get("messages", []):
88
  role = "model" if msg.get("role") == "assistant" else msg.get("role")
 
94
 
95
  if part.get("base64Data") and part.get("mimeType"):
96
  mime_type = part["mimeType"]
 
97
  if mime_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
98
  try:
99
  decoded_data = base64.b64decode(part["base64Data"])
100
  file_stream = io.BytesIO(decoded_data)
101
  document = docx.Document(file_stream)
102
  full_text = "\n".join([para.text for para in document.paragraphs])
103
+ final_text_part = f"کاربر یک فایل Word آپلود کرد. محتوای متنی آن:\n\n---\n\n{full_text}\n\n---"
 
104
  processed_parts.append({"text": final_text_part})
105
+ except Exception:
106
+ processed_parts.append({"text": "[خطا در خواندن فایل Word]"})
 
 
 
 
107
  else:
108
  processed_parts.append({"inline_data": {"mime_type": part["mimeType"], "data": part["base64Data"]}})
109
 
 
116
  if not any(msg['role'] == 'user' for msg in gemini_messages):
117
  return Response("data: [DONE]\n\n", mimetype='text/event-stream')
118
 
119
+ @stream_with_context
120
  def stream_response():
121
+ # تعداد تلاش‌ها برابر با تعداد کلیدهاست (یک دور کامل روی همه کلیدها)
122
+ max_attempts = len(GEMINI_API_KEYS)
123
+ # اگر تعداد کلیدها کم بود، حداقل 3 بار تلاش کن (با تکرار کلیدها)
124
+ if max_attempts < 3:
125
+ max_attempts = 3
126
+
127
+ for attempt in range(max_attempts):
128
  try:
129
+ # انتخاب کلید
130
  api_key, key_index = get_next_key_with_index()
131
+
132
+ # ساخت درخواست
133
  api_endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL_NAME}:streamGenerateContent?key={api_key}&alt=sse"
134
 
135
  payload = {
 
142
  }
143
 
144
  if show_thoughts:
145
+ payload["generationConfig"]["thinking_config"] = {"include_thoughts": True}
146
+
147
+ logging.info(f"تلاش {attempt+1}: استفاده از کلید {key_index + 1}...")
148
+
149
+ # ارسال درخواست به گوگل
150
+ # stream=True یعنی پاسخ را تکه تکه بگیر
151
+ # timeout=(Connect, Read)
152
+ response = requests.post(
153
+ api_endpoint,
154
+ json=payload,
155
+ stream=True,
156
+ timeout=(STREAM_CONNECT_TIMEOUT, STREAM_READ_TIMEOUT)
157
+ )
158
+
159
+ # اگر وضعیت 200 نبود، یعنی این کلید مشکل دارد.
160
+ # Exception ایجاد میکنیم تا برود به بخش except و کلید بعدی را تست کند
161
+ if response.status_code != 200:
162
+ logging.warning(f"کلید {key_index + 1} خطا داد: {response.status_code}")
163
+ response.close()
164
+ continue # برو به کلید بعدی
165
+
166
+ # ترفند اصلی: ساخت Iterator
167
+ # ما سعی میکنیم "اولین خط" پاسخ را بگیریم.
168
+ # اگر اینجا خطا بدهد یعنی هنوز چیزی به کاربر نفرستادیم، پس میتونیم سوییچ کنیم.
169
+ line_iterator = response.iter_lines()
170
+
171
+ # اینجا با yield from ما عملاً استریم را به کلاینت وصل میکنیم
172
+ # اگر وسط استریم قطع شود کاری نمیتوان کرد، اما مهم شروعش است.
173
+ data_received = False
174
+ for line in line_iterator:
175
+ if line:
176
+ decoded_line = line.decode('utf-8')
177
+ if decoded_line.startswith('data: '):
178
+ try:
179
+ chunk_data = json.loads(decoded_line[6:])
180
+ parts = chunk_data.get("candidates", [{}])[0].get("content", {}).get("parts", [])
181
+
182
+ for part in parts:
183
+ if "text" not in part or not part["text"]:
184
+ continue
185
 
186
+ # به محض اینکه اولین داده سالم رسید، یعنی اتصال موفق بوده
187
+ data_received = True
188
+
189
+ is_thought = part.get("thought") is True
190
+ if show_thoughts and is_thought:
191
+ yield f"data: {json.dumps({'type': 'thought', 'content': part['text']})}\n\n"
192
+ elif not is_thought:
193
+ yield f"data: {json.dumps({'choices': [{'delta': {'content': part['text']}}]})}\n\n"
194
 
195
+ except Exception:
196
+ continue
197
+
198
+ # اگر حلقه تمام شد و دیتایی ارسال شد، کار تمام است
199
+ if data_received:
200
+ logging.info(f"پاسخ با موفقیت با کلید {key_index + 1} تکمیل شد.")
201
+ return
202
+
203
+ # اگر ریسپانس 200 بود ولی دیتایی نداشت (خیلی بعید)، باز هم یعنی موفق بوده
204
+ # اما اگر خالی بودنش به خاطر خطا بود، شاید بهتر باشد ادامه دهیم.
205
+ # اینجا فرض را بر اتمام موفق میگذاریم.
206
+ return
207
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  except Exception as e:
209
+ # هر خطایی رخ داد (تایم اوت، شبکه، قطعی، فیلتر)
210
+ # لاگ کن و برو دور بعدی حلقه (کلید بعدی)
211
+ logging.error(f"خطا در کلید {key_index + 1}: {e} -- تلاش مجدد با کلید دیگر...")
212
+ time.sleep(0.5) # مکث کوتاه برای جلوگیری از اسپم سریع
213
+ continue
214
+
215
+ # === اگر از حلقه خارج شدیم یعنی همه کلیدها تست شدند و هیچکدام کار نکردند ===
216
+ # فقط در این حالت نهایی مجبوریم یک پیام به کاربر بدهیم که بفهمد تمام شده
217
+ # اما سعی میکنیم پیام سیستمی نباشد.
218
+ # یا میتوانیم یک پیام [DONE] بفرستیم که انگار تمام شده (بدون خطا)
219
 
220
+ logging.critical("تمام کلیدها شکست خوردند.")
221
+ # اینجا یک پیام خطای نرم میفرستیم که کاربر فکر نکند سرور خراب است
222
+ final_err = {"type": "error", "message": "شبکه شلوغ است. لطفا مجددا دکمه ارسال را بزنید."}
223
+ yield f"data: {json.dumps(final_err)}\n\n"
224
 
225
  return Response(stream_response(), mimetype='text/event-stream')
226
 
227
  if __name__ == '__main__':
 
 
228
  app.run(debug=True, host='0.0.0.0', port=os.environ.get("PORT", 7860))
229
 
230
  # --- END OF FILE app.py ---