Jan2000 commited on
Commit
a02ab71
·
unverified ·
1 Parent(s): a039b4d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +141 -95
app.py CHANGED
@@ -11,18 +11,40 @@ from flask import Flask, render_template, request, Response, stream_with_context
11
  import requests
12
  import docx
13
 
14
- # ================== تنظیمات لاگ ==================
15
- # فقط خطاهای حیاتی را لاگ میکنیم تا کنسول شلوغ نشود
16
- logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  app = Flask(__name__)
18
 
19
- # ================== تنظیمات کلیدها ==================
20
 
21
  GEMINI_MODEL_NAME = "gemini-2.5-flash"
22
  ALL_KEYS_STR = os.environ.get("ALL_GEMINI_API_KEYS", "")
23
- # تمیز کردن کلیدها
24
  GEMINI_API_KEYS = [key.strip() for key in ALL_KEYS_STR.split(',') if key.strip()]
25
 
 
 
 
26
  key_index_counter = 0
27
  key_lock = threading.Lock()
28
 
@@ -36,7 +58,7 @@ def get_next_key_with_index():
36
  key_index_counter = (key_index_counter + 1) % len(GEMINI_API_KEYS)
37
  return key, current_index
38
 
39
- # ================== مسیرها ==================
40
 
41
  @app.route('/')
42
  def index():
@@ -44,37 +66,46 @@ def index():
44
 
45
  @app.route('/chat', methods=['POST'])
46
  def chat():
47
- # اگر کلا کلیدی نباشد، باز هم نباید کرش کنیم
48
  if not GEMINI_API_KEYS:
49
- # شبیه سازی پاسخ متنی ربات به جای ارور سیستمی
50
- fake_response = {"choices": [{"delta": {"content": "⚠️ تنظیمات سیستم کامل نیست (کلید API یافت نشد)."}}]}
51
  return Response(f"data: {json.dumps(fake_response)}\n\n", mimetype='text/event-stream')
52
 
53
  data = request.json
54
- system_instruction = "تو چت بات هوش مصنوعی آلفا هستی. دوستانه، کوتاه و با ایموجی پاسخ بده."
 
55
  show_thoughts = data.get("show_thoughts", False)
56
 
57
- # --- پردازش پیام‌ها و فایل‌ها ---
58
  gemini_messages = []
59
  for msg in data.get("messages", []):
60
  role = "model" if msg.get("role") == "assistant" else msg.get("role")
 
61
  processed_parts = []
62
  for part in msg.get("parts", []):
63
  if part.get("text"):
64
  processed_parts.append({"text": part["text"]})
 
65
  if part.get("base64Data") and part.get("mimeType"):
66
  mime_type = part["mimeType"]
67
- # هندل کردن فایل Word
68
  if mime_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
69
  try:
70
- decoded = base64.b64decode(part["base64Data"])
71
- doc = docx.Document(io.BytesIO(decoded))
72
- text = "\n".join([p.text for p in doc.paragraphs])
73
- processed_parts.append({"text": f"محتوای فایل کاربر:\n{text}"})
74
- except:
75
- processed_parts.append({"text": "(فایل Word قابل خواندن نبود)"})
 
 
 
 
 
 
 
76
  else:
77
- processed_parts.append({"inline_data": {"mime_type": mime_type, "data": part["base64Data"]}})
78
 
79
  if processed_parts:
80
  if gemini_messages and gemini_messages[-1]["role"] == role:
@@ -82,105 +113,120 @@ def chat():
82
  else:
83
  gemini_messages.append({"role": role, "parts": processed_parts})
84
 
85
- # اگر پیامی نبود تمام کن
86
  if not any(msg['role'] == 'user' for msg in gemini_messages):
87
  return Response("data: [DONE]\n\n", mimetype='text/event-stream')
88
-
89
- # --- تابع اصلی استریم با مدیریت خطای پیشرفته ---
90
  @stream_with_context
91
- def stream_response_generator():
92
- # تعداد دفعاتی که کلا تلاش میکنیم (مثلا 3 دور کامل روی همه کلیدها)
93
- # این باعث میشه کاربر احساس نکنه سرور قطع شده، فقط فکر میکنه داره فکر میکنه
94
- MAX_GLOBAL_RETRIES = len(GEMINI_API_KEYS) * 3 if len(GEMINI_API_KEYS) > 0 else 1
 
95
 
96
- # فلگ برای اینکه بفهمیم بالاخره موفق شدیم یا نه
97
- success = False
98
 
99
- for attempt in range(MAX_GLOBAL_RETRIES):
100
  try:
101
- api_key, idx = get_next_key_with_index()
 
 
 
 
 
 
102
 
103
- # تنظیمات تایم اوت:
104
- # 5 ثانیه برای اتصال (اگر کلید خراب بود سریع رد شو)
105
- # 100 ثانیه برای خواندن (اگر فایل سنگین بود صبر کن)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  response = requests.post(
107
- f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL_NAME}:streamGenerateContent?key={api_key}&alt=sse",
108
- json={
109
- "contents": gemini_messages,
110
- "systemInstruction": {"parts": [{"text": system_instruction}]},
111
- "generationConfig": {
112
- "temperature": 0.7,
113
- "thinking_config": {"include_thoughts": True} if show_thoughts else {}
114
- }
115
- },
116
- stream=True,
117
- timeout=(5, 100)
118
  )
119
-
120
- # اگر کد 200 نبود، یعنی این کلید مشکل داره.
121
- # نکته مهم: اینجا هیچ چیزی به کاربر نمیفرستیم. `continue` میکنیم تا بره کلید بعدی.
122
  if response.status_code != 200:
 
123
  response.close()
124
- continue
125
 
126
- # حالا چک میکنیم که آیا واقعا دیتایی میاد؟
127
- # این خط iterator رو میگیره اما هنوز دانلود نکرده
128
- line_iter = response.iter_lines()
129
-
130
- # سعی میکنیم اولین خط رو بگیریم.
131
- # اگر اینجا ارور بده (مثل 502 وسط کار)، میره توی except و کلید بعدی رو تست میکنه
132
- # پس کاربر هنوز چیزی ندیده.
133
- first_chunk_found = False
134
 
135
- for line in line_iter:
 
 
 
136
  if line:
137
  decoded_line = line.decode('utf-8')
138
  if decoded_line.startswith('data: '):
139
  try:
140
- json_data = json.loads(decoded_line[6:])
141
- # اگر جیسون ولید بود، یعنی اتصال درسته.
142
- # حالا شروع میکنیم به فرستادن به کاربر
143
- first_chunk_found = True
144
-
145
- # اینجا دیگه تسلیم میشیم و شروع میکنیم به ارسال به کلاینت
146
- # چون مطمئن شدیم این کلید سالمه
147
- success = True
148
 
149
- # پردازش معمولی
150
- parts = json_data.get("candidates", [{}])[0].get("content", {}).get("parts", [])
151
  for part in parts:
152
- if part.get("text"):
153
- is_thought = part.get("thought") is True
154
- if show_thoughts and is_thought:
155
- yield f"data: {json.dumps({'type': 'thought', 'content': part['text']})}\n\n"
156
- elif not is_thought:
157
- yield f"data: {json.dumps({'choices': [{'delta': {'content': part['text']}}]})}\n\n"
158
-
159
- except:
160
- pass # ایگنور کردن خطاهای جزئی جیسون
161
-
162
- # اگر حلقه تمام شد و ما دیتایی فرستادیم، یعنی کار تمامه
163
- if success:
164
- return # خروج از کل تابع (پایان استریم)
 
 
 
 
 
 
 
 
165
 
 
166
  except Exception as e:
167
- # هر خطایی (قطع نت، تایم اوت، ارور گوگل)
168
- # فقط لاگ کن و برو کلید بعدی
169
- # کاربر چیزی حس نمیکنه، فقط اسپینر میچرخه
170
- logging.error(f"Retry {attempt}: {e}")
171
- time.sleep(0.2) # وقفه کوتاه
172
- continue
173
-
174
- # === اگر به اینجا رسیدیم یعنی تمام کلیدها تست شدند و هیچکدام کار نکردند ===
175
- # به جای اینکه ارور قرمز بفرستیم، یک متن معمولی میفرستیم
176
- # این باعث میشه کاربر فکر کنه ربات جواب داده ولی نتونسته
177
- if not success:
178
- fallback_msg = "🤔 سیستم کمی شلوغ است و نتوانستم پاسخ را کامل کنم. لطفاً دوباره تلاش کنید یا فایلتان را بررسی کنید."
179
- yield f"data: {json.dumps({'choices': [{'delta': {'content': fallback_msg}}]})}\n\n"
180
-
181
- return Response(stream_response_generator(), mimetype='text/event-stream')
 
 
 
182
 
183
  if __name__ == '__main__':
 
 
184
  app.run(debug=True, host='0.0.0.0', port=os.environ.get("PORT", 7860))
185
 
186
  # --- END OF FILE app.py ---
 
11
  import requests
12
  import docx
13
 
14
+ # ================== بخش تنظیمات لاگ‌نویسی ==================
15
+
16
+ class NoGrpcFilter(logging.Filter):
17
+ def filter(self, record):
18
+ return not record.getMessage().startswith('ALTS creds ignored.')
19
+
20
+ def setup_logging():
21
+ log_format = '[%(asctime)s] [%(levelname)s]: %(message)s'
22
+ date_format = '%Y-%m-%d %H:%M:%S'
23
+ formatter = logging.Formatter(log_format, datefmt=date_format)
24
+
25
+ root_logger = logging.getLogger()
26
+ if root_logger.hasHandlers():
27
+ root_logger.handlers.clear()
28
+
29
+ console_handler = logging.StreamHandler()
30
+ console_handler.setFormatter(formatter)
31
+ console_handler.addFilter(NoGrpcFilter())
32
+
33
+ root_logger.addHandler(console_handler)
34
+ root_logger.setLevel(logging.INFO)
35
+
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 در Secrets تنظیم نشده است! (ALL_GEMINI_API_KEYS)")
47
+
48
  key_index_counter = 0
49
  key_lock = threading.Lock()
50
 
 
58
  key_index_counter = (key_index_counter + 1) % len(GEMINI_API_KEYS)
59
  return key, current_index
60
 
61
+ # ================== پایان بخش پیکربندی ====================
62
 
63
  @app.route('/')
64
  def index():
 
66
 
67
  @app.route('/chat', methods=['POST'])
68
  def chat():
69
+ # اگر کلیدی تعریف نشده باشد، به جای ارور، یک پیام متنی معمولی میفرستیم
70
  if not GEMINI_API_KEYS:
71
+ fake_response = {"choices": [{"delta": {"content": "⚠️ تنظیمات سرور کامل نیست (کلید API یافت نشد)."}}]}
 
72
  return Response(f"data: {json.dumps(fake_response)}\n\n", mimetype='text/event-stream')
73
 
74
  data = request.json
75
+ system_instruction = "تو چت بات هوش مصنوعی آلفا هستی و توسط برنامه هوش مصنوعی آلفا توسعه داده شدی. کمی با کاربران باحال و دوستانه صحبت کن و از ایموجی‌ها استفاده کن. همیشه پاسخ‌هایت را به زبان فارسی و یا هر زبانی که کاربر صحبت میکنه ارائه بده."
76
+
77
  show_thoughts = data.get("show_thoughts", False)
78
 
79
+ # === بخش پردازش پیام‌ها و فایل DOCX (بدون تغییر) ===
80
  gemini_messages = []
81
  for msg in data.get("messages", []):
82
  role = "model" if msg.get("role") == "assistant" else msg.get("role")
83
+
84
  processed_parts = []
85
  for part in msg.get("parts", []):
86
  if part.get("text"):
87
  processed_parts.append({"text": part["text"]})
88
+
89
  if part.get("base64Data") and part.get("mimeType"):
90
  mime_type = part["mimeType"]
91
+
92
  if mime_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
93
  try:
94
+ decoded_data = base64.b64decode(part["base64Data"])
95
+ file_stream = io.BytesIO(decoded_data)
96
+ document = docx.Document(file_stream)
97
+ full_text = "\n".join([para.text for para in document.paragraphs])
98
+
99
+ final_text_part = f"کاربر یک فایل Word آپلود کرد. محتوای متنی آن به شرح زیر است:\n\n---\n\n{full_text}\n\n---"
100
+ processed_parts.append({"text": final_text_part})
101
+ logging.info("فایل DOCX با موفقیت پردازش و متن آن استخراج شد.")
102
+
103
+ except Exception as e:
104
+ logging.error(f"خطا در پردازش فایل DOCX: {e}")
105
+ processed_parts.append({"text": "[خطا: امکان پردازش فایل Word وجود نداشت.]"})
106
+
107
  else:
108
+ processed_parts.append({"inline_data": {"mime_type": part["mimeType"], "data": part["base64Data"]}})
109
 
110
  if processed_parts:
111
  if gemini_messages and gemini_messages[-1]["role"] == role:
 
113
  else:
114
  gemini_messages.append({"role": role, "parts": processed_parts})
115
 
 
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
+ # === تابع اصلی استریم با مکانیزم تلاش مجدد مخفی ===
120
  @stream_with_context
121
+ def stream_response():
122
+ # تعداد کلیدها را میگیریم
123
+ num_keys = len(GEMINI_API_KEYS)
124
+ # تعداد تلاش‌ها: حداقل 3 برابر تعداد کلیدها تلاش میکنیم تا مطمئن شویم
125
+ max_attempts = max(num_keys * 3, 5)
126
 
127
+ success_flag = False
 
128
 
129
+ for attempt in range(max_attempts):
130
  try:
131
+ api_key, key_index = get_next_key_with_index()
132
+ # اگر کلیدی نبود (خیلی بعید)
133
+ if not api_key: break
134
+
135
+ logging.info(f"تلاش شماره {attempt + 1} با کلید ایندکس {key_index}...")
136
+
137
+ api_endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL_NAME}:streamGenerateContent?key={api_key}&alt=sse"
138
 
139
+ payload = {
140
+ "contents": gemini_messages,
141
+ "systemInstruction": {"parts": [{"text": system_instruction}]},
142
+ "tools": [{"google_search": {}}],
143
+ "generationConfig": {
144
+ "temperature": 0.7,
145
+ }
146
+ }
147
+
148
+ if show_thoughts:
149
+ payload["generationConfig"]["thinking_config"] = {
150
+ "include_thoughts": True
151
+ }
152
+
153
+ # *** تنظیمات Timeout بسیار مهم ***
154
+ # Connect (5): زمان وصل شدن به گوگل. اگر 5 ثانیه طول کشید یعنی شبکه خرابه یا کلید گیر کرده -> سریع قطع کن برو بعدی
155
+ # Read (120): زمان انتظار برای جواب. چون فایل داری، باید زیاد باشه (120 ثانیه) تا وسط پردازش قطع نکنه.
156
  response = requests.post(
157
+ api_endpoint,
158
+ json=payload,
159
+ stream=True,
160
+ timeout=(5, 120)
 
 
 
 
 
 
 
161
  )
162
+
163
+ # اگر کد وضعیت 200 نبود (یعنی 429، 500، 403 و...)
 
164
  if response.status_code != 200:
165
+ logging.warning(f"کلید {key_index} پاسخ نداد (کد {response.status_code}). تلاش بعدی...")
166
  response.close()
167
+ continue # بدون هیچ حرفی برو سراغ کلید بعدی (کاربر چیزی نمیفهمه)
168
 
169
+ # اگر کد 200 بود، حالا چک میکنیم استریم دیتا داره یا نه
170
+ line_iterator = response.iter_lines()
 
 
 
 
 
 
171
 
172
+ # یک متغیر برای اینکه بفهمیم آیا واقعا دیتایی فرستادیم یا نه
173
+ data_sent_in_this_attempt = False
174
+
175
+ for line in line_iterator:
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
+ # به محض اینکه اولین داده سالم رسید، یعنی موفق شدیم
188
+ data_sent_in_this_attempt = True
189
+ success_flag = True
190
+
191
+ is_a_thought = part.get("thought") is True
192
+ if show_thoughts and is_a_thought:
193
+ thought_payload = {"type": "thought", "content": part["text"]}
194
+ yield f"data: {json.dumps(thought_payload)}\n\n"
195
+ elif not is_a_thought:
196
+ sse_payload = {"choices": [{"delta": {"content": part["text"]}}]}
197
+ yield f"data: {json.dumps(sse_payload)}\n\n"
198
+
199
+ except (json.JSONDecodeError, IndexError, KeyError):
200
+ continue
201
+
202
+ # اگر حلقه تمام شد و ما دیتایی فرستاده بودیم، یعنی کار تمام است و موفق بودیم
203
+ if success_flag:
204
+ return # خروج کامل
205
 
206
+ # مدیریت تمام خطاها (Timeout, ConnectionError, ...)
207
  except Exception as e:
208
+ logging.error(f"خطای داخلی در تلاش {attempt+1}: {e}")
209
+ # اینجا یک وقفه کوتاه 0.5 ثانیه‌ای میدهیم که CPU درگیر نشود و سریع میریم کلید بعدی
210
+ time.sleep(0.5)
211
+ continue # برو تلاش بعدی (بدون اینکه به کاربر ارور بدی)
212
+
213
+ # === بخش نهایی (فقط اگر همه تلاش‌ها شکست خورد) ===
214
+ # اگر بعد از مثلا 30 بار تلاش (بسته به تعداد کلید) هیچکدام کار نکرد:
215
+ # به جای ارسال Error Payload که باعث نمایش خطای قرمز میشه،
216
+ # یک پیام متنی معمولی از طرف ربات میفرستیم.
217
+ if not success_flag:
218
+ logging.critical("تمام کلیدها ناموفق بودند.")
219
+ fallback_message = "متاسفانه شبکه من کمی کند شده و نتوانستم پاسخ را دریافت کنم. لطفاً دوباره دکمه ارسال را بزنید 🔄"
220
+ # فرمت پیام دقیقا مثل پاسخ عادی ربات است
221
+ fallback_payload = {"choices": [{"delta": {"content": fallback_message}}]}
222
+ yield f"data: {json.dumps(fallback_payload)}\n\n"
223
+
224
+ # استفاده از stream_with_context برای حفظ کانتکست درخواست در طول حلقه طولانی
225
+ return Response(stream_response(), mimetype='text/event-stream')
226
 
227
  if __name__ == '__main__':
228
+ if GEMINI_API_KEYS:
229
+ logging.info(f"سیستم در حالت توسعه شروع به کار کرد. تعداد {len(GEMINI_API_KEYS)} کلید شناسایی شد.")
230
  app.run(debug=True, host='0.0.0.0', port=os.environ.get("PORT", 7860))
231
 
232
  # --- END OF FILE app.py ---