| import asyncio |
| import codecs |
| import hashlib |
| import os |
| from datetime import datetime |
| import json |
| import uuid |
| |
| from typing import Dict, Any, Optional |
| from fastapi import HTTPException |
| import ssl |
| import httpx |
| from httpx import ConnectError, TransportError |
| from starlette import status |
|
|
| from core.config import get_settings |
| from core.logger import setup_logger |
| from core.models import ChatRequest |
| from core.sqlite_store import SQLiteConnectionPool, DatabaseManager |
|
|
| settings = get_settings() |
| logger = setup_logger(__name__) |
|
|
| def decode_unicode_escape(s): |
| |
| if isinstance(s, dict): |
| return s |
| |
| if not isinstance(s, (str, bytes)): |
| s = str(s) |
| |
| if isinstance(s, str): |
| s = s.encode('utf-8') |
| return codecs.decode(s, 'unicode_escape') |
|
|
| FIREBASE_API_KEY = settings.FIREBASE_API_KEY |
| async def refresh_token_via_rest(refresh_token): |
| |
| url = f"https://securetoken.googleapis.com/v1/token?key={FIREBASE_API_KEY}" |
|
|
| payload = { |
| 'grant_type': 'refresh_token', |
| 'refresh_token': refresh_token |
| } |
|
|
| try: |
| async with httpx.AsyncClient() as client: |
| response = await client.post(url, json=payload) |
| if response.status_code == 200: |
| data = response.json() |
| print(json.dumps(data, indent=2)) |
| |
| |
| |
| |
| |
| return data['id_token'] |
| else: |
| print(f"刷新失败: {response.text}") |
| return None |
| except Exception as e: |
| print(f"请求异常: {e}") |
| return None |
|
|
|
|
| async def sign_in_with_idp(): |
| url = "https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp" |
|
|
| |
| params = { |
| "key": FIREBASE_API_KEY |
| } |
|
|
| |
| headers = { |
| "X-Client-Version": "Node/JsCore/10.5.2/FirebaseCore-web", |
| "X-Firebase-gmpid": "1:252179682924:web:9c80c6a32cb4682cbfaa49", |
| "Content-Type": "application/json", |
| "User-Agent": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" |
| } |
|
|
| |
| data = { |
| "requestUri": "http://localhost", |
| "returnSecureToken": True, |
| "postBody": f"&id_token={settings.AUTHORIZATION_TOKEN}&providerId=google.com" |
| } |
| print("Request Headers:", json.dumps(headers, indent=2)) |
| print("Request Body:", json.dumps(data, indent=2)) |
| print("Request params:", json.dumps(params, indent=2)) |
|
|
| async with httpx.AsyncClient() as client: |
| response = await client.post( |
| url, |
| params=params, |
| headers=headers, |
| json=data |
| ) |
| |
| if response.status_code == 200: |
| return response.json() |
| else: |
| raise Exception(f"Request failed with status code: {response.status_code}") |
|
|
| async def handle_firebase_response(response) -> str: |
| try: |
| |
| if isinstance(response, dict): |
| print(json.dumps(response, indent=2)) |
| if response.get('error', {}).get('code') == 400: |
| print("Invalid id_token in IdP response") |
| |
| if 'refreshToken' in response: |
| os.environ["REFRESH_TOKEN"] = response['refreshToken'] |
| if 'idToken' in response: |
| return response['idToken'] |
| else: |
| raise ValueError("dict case Response does not contain idToken") |
|
|
| |
| elif hasattr(response, 'status_code'): |
| if response.status_code == 200: |
| data = response.json() |
| print(data) |
| |
| if 'refreshToken' in data: |
| os.environ["REFRESH_TOKEN"] = data['refreshToken'] |
| if 'idToken' in data: |
| return data['idToken'] |
| else: |
| raise ValueError("response case Response does not contain idToken") |
|
|
| |
| elif response.status_code == 400: |
| error_data = response.json() |
| raise ValueError(f"Bad Request: {error_data.get('error', {}).get('message', 'Unknown error')}") |
| elif response.status_code == 401: |
| raise ValueError("Unauthorized: Invalid credentials") |
| elif response.status_code == 403: |
| raise ValueError("Forbidden: Insufficient permissions") |
| elif response.status_code == 404: |
| raise ValueError("Not Found: Resource doesn't exist") |
| else: |
| raise ValueError(f"Unexpected status code: {response.status_code}") |
|
|
| else: |
| raise ValueError(f"Unexpected response type: {type(response)}") |
|
|
| except json.JSONDecodeError: |
| raise ValueError("Invalid JSON response") |
| except Exception as e: |
| raise ValueError(f"Error processing response: {str(e)}") |
|
|
| |
| def _sha256_hash(text): |
| sha256 = hashlib.sha256() |
| sha256.update(text.encode('utf-8')) |
| return sha256.hexdigest() |
|
|
| |
| def sha256_hash_messages(messages): |
| |
| message_data = [str(msg['content']) for msg in messages if msg['role'] == "user"] |
| print("Filtered contents:", message_data) |
| json_str = json.dumps(message_data, sort_keys=True) |
| print("JSON string:", json_str) |
| return hashlib.sha256(json_str.encode('utf-8')).hexdigest() |
|
|
|
|
| def create_chat_completion_data( |
| content: str, model: str, timestamp: int, finish_reason: Optional[str] = None |
| ) -> Dict[str, Any]: |
| return { |
| "id": f"chatcmpl-{uuid.uuid4()}", |
| "object": "chat.completion.chunk", |
| "created": timestamp, |
| "model": model, |
| "choices": [ |
| { |
| "index": 0, |
| "delta": {"content": content, "role": "assistant"}, |
| "finish_reason": finish_reason, |
| } |
| ], |
| "usage": None, |
| } |
| pool = SQLiteConnectionPool('~/tmp/merlin-sqlite.db', max_connections=5) |
| db = DatabaseManager(pool) |
| async def process_streaming_response(request: ChatRequest, app_secret: str): |
| |
| ssl_context = ssl.create_default_context() |
| ssl_context.check_hostname = True |
| ssl_context.verify_mode = ssl.CERT_REQUIRED |
| |
| |
| |
| |
| |
| previous_messages = [ |
| msg.model_dump(include={'role', 'content'}) |
| for msg in request.messages[:-1] |
| if msg.role == "user" |
| ] |
| |
| |
| |
| message = [dict(msg) for msg in request.messages] |
| if not previous_messages: |
| chat_id = str(uuid.uuid4()) |
| child_id = str(uuid.uuid4()) |
| parent_id = "root" |
| |
| db.insert_context_record(app_secret, chat_id, child_id, sha256_hash_messages(message)) |
| print("Insert new context record: hash=", sha256_hash_messages(message)) |
| else: |
| sha256_hash = sha256_hash_messages(previous_messages) |
| print("func get_context_record_by_sha256_hash SHA-256 hash:", sha256_hash) |
| context_record = db.get_context_record_by_sha256_hash(sha256_hash) |
| chat_id = context_record['chat_id'] |
| child_id = str(uuid.uuid4()) |
| parent_id = context_record['parent_id'] |
| json_data = { |
| "attachments": [], |
| "chatId": chat_id, |
| "language": "AUTO", |
| "message": { |
| "childId": child_id, |
| "content": request.messages[-1].content, |
| "context": "", |
| "id": str(uuid.uuid4()), |
| "parentId": parent_id |
| }, |
| "metadata": { |
| "largeContext": False, |
| "merlinMagic": False, |
| "proFinderMode": False, |
| "webAccess": False |
| }, |
| "mode": "UNIFIED_CHAT", |
| |
| "model": request.model |
| } |
| async with httpx.AsyncClient( |
| verify=ssl_context, |
| |
| |
| ) as client: |
| try: |
| request_headers = {**settings.HEADERS, 'authorization': f"Bearer {os.getenv('TOKEN', '')}"} |
|
|
| print("Request Headers:", json.dumps(request_headers, indent=2)) |
| print("Request Body:", json.dumps(json_data, indent=2)) |
| async with client.stream( |
| "POST", |
| f"https://arcane.getmerlin.in/v1/thread/unified", |
| headers=request_headers, |
| json=json_data, |
| timeout=100, |
| ) as response: |
| response.raise_for_status() |
| timestamp = int(datetime.now().timestamp()) |
| async for line in response.aiter_lines(): |
| |
| if line and line.startswith("data: "): |
| try: |
| data_str = line[6:] |
| |
| json_data = json.loads(data_str) |
| |
| |
| |
| |
| |
| |
| if 'type' in json_data and json_data['type'] == 'USAGE_LIMIT_REACHED_FREE': |
| content = json_data['message'] |
| print(content, end='', flush=True) |
| yield f"data: {json.dumps(create_chat_completion_data(content, request.model, timestamp))}\n\n" |
| |
| if 'data' in json_data and 'eventType' in json_data['data'] and json_data['data']['eventType'] == 'DONE': |
| await response.aclose() |
| break |
| |
| |
| |
| |
| |
| |
| |
| if 'data' in json_data and 'content' in json_data['data'] and json_data['data']['content'] != '': |
| content = json_data['data']['content'] |
| |
| print(content, end='', flush=True) |
| yield f"data: {json.dumps(create_chat_completion_data(content, request.model, timestamp))}\n\n" |
|
|
| except json.JSONDecodeError as e: |
| print(f"JSON解析错误: {e}") |
| print(f"原始数据: {line}") |
| continue |
|
|
| yield f"data: {json.dumps(create_chat_completion_data('', request.model, timestamp, 'stop'))}\n\n" |
| yield "data: [DONE]\n\n" |
| |
| db.update_context_record_by_chat_id(app_secret, chat_id, child_id, sha256_hash_messages(message)) |
| except ConnectError as e: |
| logger.error(f"Connection error details: {str(e)}") |
| raise HTTPException( |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, |
| detail="Service temporarily unavailable. Please try again later." |
| ) |
| except TransportError as e: |
| logger.error(f"Transport error details: {str(e)}") |
| raise HTTPException( |
| status_code=status.HTTP_502_BAD_GATEWAY, |
| detail="Network transport error occurred." |
| ) |
| except httpx.HTTPStatusError as e: |
| |
| logger.error(f"HTTP error occurred: {e}") |
| raise HTTPException(status_code=e.response.status_code, detail=str(e)) |
| except httpx.RequestError as e: |
| logger.error(f"Error occurred during request: {e}") |
| raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) |
| finally: |
| await response.aclose() |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|