|
|
|
|
|
|
|
|
import os |
|
|
import json |
|
|
import logging |
|
|
import threading |
|
|
import base64 |
|
|
import io |
|
|
import time |
|
|
from flask import Flask, render_template, request, Response, stream_with_context |
|
|
import requests |
|
|
import docx |
|
|
|
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s') |
|
|
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()] |
|
|
|
|
|
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: |
|
|
return None, -1 |
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
@app.route('/') |
|
|
def index(): |
|
|
return render_template('index.html') |
|
|
|
|
|
@app.route('/chat', methods=['POST']) |
|
|
def chat(): |
|
|
|
|
|
if not GEMINI_API_KEYS: |
|
|
|
|
|
fake_response = {"choices": [{"delta": {"content": "⚠️ تنظیمات سیستم کامل نیست (کلید API یافت نشد)."}}]} |
|
|
return Response(f"data: {json.dumps(fake_response)}\n\n", 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 = base64.b64decode(part["base64Data"]) |
|
|
doc = docx.Document(io.BytesIO(decoded)) |
|
|
text = "\n".join([p.text for p in doc.paragraphs]) |
|
|
processed_parts.append({"text": f"محتوای فایل کاربر:\n{text}"}) |
|
|
except: |
|
|
processed_parts.append({"text": "(فایل Word قابل خواندن نبود)"}) |
|
|
else: |
|
|
processed_parts.append({"inline_data": {"mime_type": mime_type, "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') |
|
|
|
|
|
|
|
|
@stream_with_context |
|
|
def stream_response_generator(): |
|
|
|
|
|
|
|
|
MAX_GLOBAL_RETRIES = len(GEMINI_API_KEYS) * 3 if len(GEMINI_API_KEYS) > 0 else 1 |
|
|
|
|
|
|
|
|
success = False |
|
|
|
|
|
for attempt in range(MAX_GLOBAL_RETRIES): |
|
|
try: |
|
|
api_key, idx = get_next_key_with_index() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
response = requests.post( |
|
|
f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL_NAME}:streamGenerateContent?key={api_key}&alt=sse", |
|
|
json={ |
|
|
"contents": gemini_messages, |
|
|
"systemInstruction": {"parts": [{"text": system_instruction}]}, |
|
|
"generationConfig": { |
|
|
"temperature": 0.7, |
|
|
"thinking_config": {"include_thoughts": True} if show_thoughts else {} |
|
|
} |
|
|
}, |
|
|
stream=True, |
|
|
timeout=(5, 100) |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
if response.status_code != 200: |
|
|
response.close() |
|
|
continue |
|
|
|
|
|
|
|
|
|
|
|
line_iter = response.iter_lines() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
first_chunk_found = False |
|
|
|
|
|
for line in line_iter: |
|
|
if line: |
|
|
decoded_line = line.decode('utf-8') |
|
|
if decoded_line.startswith('data: '): |
|
|
try: |
|
|
json_data = json.loads(decoded_line[6:]) |
|
|
|
|
|
|
|
|
first_chunk_found = True |
|
|
|
|
|
|
|
|
|
|
|
success = True |
|
|
|
|
|
|
|
|
parts = json_data.get("candidates", [{}])[0].get("content", {}).get("parts", []) |
|
|
for part in parts: |
|
|
if part.get("text"): |
|
|
is_thought = part.get("thought") is True |
|
|
if show_thoughts and is_thought: |
|
|
yield f"data: {json.dumps({'type': 'thought', 'content': part['text']})}\n\n" |
|
|
elif not is_thought: |
|
|
yield f"data: {json.dumps({'choices': [{'delta': {'content': part['text']}}]})}\n\n" |
|
|
|
|
|
except: |
|
|
pass |
|
|
|
|
|
|
|
|
if success: |
|
|
return |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
|
|
|
|
|
|
logging.error(f"Retry {attempt}: {e}") |
|
|
time.sleep(0.2) |
|
|
continue |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not success: |
|
|
fallback_msg = "🤔 سیستم کمی شلوغ است و نتوانستم پاسخ را کامل کنم. لطفاً دوباره تلاش کنید یا فایلتان را بررسی کنید." |
|
|
yield f"data: {json.dumps({'choices': [{'delta': {'content': fallback_msg}}]})}\n\n" |
|
|
|
|
|
return Response(stream_response_generator(), mimetype='text/event-stream') |
|
|
|
|
|
if __name__ == '__main__': |
|
|
app.run(debug=True, host='0.0.0.0', port=os.environ.get("PORT", 7860)) |
|
|
|
|
|
|
|
|
|