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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +98 -142
app.py CHANGED
@@ -11,40 +11,18 @@ 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):
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 تنظیم نشده است!")
47
-
48
  key_index_counter = 0
49
  key_lock = threading.Lock()
50
 
@@ -52,19 +30,13 @@ def get_next_key_with_index():
52
  global key_index_counter
53
  with key_lock:
54
  if not GEMINI_API_KEYS:
55
- raise ValueError("لیست کلیدهای API خالی است.")
56
  current_index = key_index_counter
57
  key = GEMINI_API_KEYS[current_index]
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():
@@ -72,40 +44,37 @@ def index():
72
 
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
81
- system_instruction = "تو چت بات هوش مصنوعی آلفا هستی و توسط برنامه هوش مصنوعی آلفا توسعه داده شدی. کمی با کاربران باحال و دوستانه صحبت کن و از ایموجی‌ها استفاده کن. همیشه پاسخ‌هایت را به زبان فارسی و یا هر زبانی که کاربر صحبت میکنه ارائه بده."
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")
89
-
90
  processed_parts = []
91
  for part in msg.get("parts", []):
92
  if part.get("text"):
93
  processed_parts.append({"text": part["text"]})
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
 
110
  if processed_parts:
111
  if gemini_messages and gemini_messages[-1]["role"] == role:
@@ -113,116 +82,103 @@ def chat():
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
  @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 = {
136
- "contents": gemini_messages,
137
- "systemInstruction": {"parts": [{"text": system_instruction}]},
138
- "tools": [{"google_search": {}}],
139
- "generationConfig": {
140
- "temperature": 0.7,
141
- }
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))
 
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
 
 
30
  global key_index_counter
31
  with key_lock:
32
  if not GEMINI_API_KEYS:
33
+ return None, -1
34
  current_index = key_index_counter
35
  key = GEMINI_API_KEYS[current_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
 
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
  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))