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 ---