|
|
|
|
|
|
|
|
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_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 |
|
|
|
|
|
|
|
|
|
|
|
STREAM_START_TIMEOUT = 4 |
|
|
|
|
|
|
|
|
STREAM_READ_TIMEOUT = 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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) |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
with requests.post(api_endpoint, json=payload, stream=True, timeout=(STREAM_START_TIMEOUT, STREAM_READ_TIMEOUT)) as response: |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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)) |
|
|
|
|
|
|