prikoll / app.py
anon4ik's picture
Update app.py
2ac2728 verified
from contextlib import asynccontextmanager
from datetime import datetime
from json import dumps, loads
from logging import DEBUG, Formatter, INFO, StreamHandler, WARNING, getLogger
from os import environ
from pathlib import Path
from random import randint
from typing import AsyncGenerator
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from fastapi import Depends, FastAPI, HTTPException, Header, Request
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, StreamingResponse
from httpx import AsyncClient
from patchright.async_api import Page, async_playwright
from starlette.responses import Response
screenshot_path = Path(__file__).parent / 'screenshot.jpeg'
token_path = Path(__file__).parent / 'token.json'
AUTH_URL = 'https://chat.reka.ai/bff/auth/login'
GET_TOKEN_URL = 'https://chat.reka.ai/bff/auth/access_token'
EMAIL_INPUT = 'input#username'
PASSWRD_INPUT = 'input#password'
SUBMIT_BUTTON = 'button[type="submit"][name="action"][value="default"]:not([data-provider])'
REKA_CHAT_PAGE = 'https://chat.reka.ai/chat'
EMAIL = str(environ.get('EMAIL')).strip()
PASSWRD = str(environ.get('PASSWORD')).strip()
UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0'
BROWSER_TIMEOUT_SECONDS = 15
API_TOKEN = str(environ.get('API_TOKEN')).strip()
REKA_API_URL = 'https://chat.reka.ai/api/chat'
logger = getLogger('REKA_API')
logger.setLevel(DEBUG)
handler = StreamHandler()
handler.setLevel(INFO)
formatter = Formatter('%(asctime)s | %(levelname)s : %(message)s', datefmt='%d.%m.%Y %H:%M:%S')
handler.setFormatter(formatter)
logger.addHandler(handler)
getLogger('httpx').setLevel(WARNING)
logger.info('инициализация приложения...')
async def make_screenshot(page: Page):
await page.screenshot(type='jpeg', path=screenshot_path.resolve().as_posix(), quality=85, full_page=True)
logger.info('скриншот создан')
async def refresh_token():
max_timeout = BROWSER_TIMEOUT_SECONDS * 1000
json_data = {'accessToken': None}
async with async_playwright() as playwright:
logger.info('запуск браузера')
browser = await playwright.chromium.launch(headless=True, args=['--disable-blink-features=AutomationControlled'])
context = await browser.new_context(
color_scheme='dark',
ignore_https_errors=True,
locale='en-US',
user_agent=UA,
no_viewport=True,
)
context.set_default_timeout(max_timeout)
context.set_default_navigation_timeout(max_timeout)
page = await context.new_page()
page.set_default_timeout(max_timeout)
page.set_default_navigation_timeout(max_timeout)
try:
logger.info('открытие страницы авторизации')
await page.goto(AUTH_URL, wait_until='networkidle')
await make_screenshot(page)
logger.info('заполнение формы авторизации')
await page.locator(EMAIL_INPUT).type(EMAIL)
await page.locator(PASSWRD_INPUT).type(PASSWRD)
await make_screenshot(page)
await page.locator(SUBMIT_BUTTON).click()
logger.info('переход на страницу чата')
await page.wait_for_url(REKA_CHAT_PAGE)
await make_screenshot(page)
logger.info('получение токена')
await page.goto(GET_TOKEN_URL, wait_until='domcontentloaded')
await make_screenshot(page)
json_text = await page.evaluate("document.querySelector('pre').textContent")
json_data = loads(json_text)
await make_screenshot(page)
finally:
await page.close()
await context.close()
await browser.close()
logger.info('работа браузера завершена')
return json_data
async def get_token():
token_data = await refresh_token()
if token_data.get('accessToken'):
token_path.write_text(dumps(token_data))
logger.info('токен reka получен')
else:
raise logger.error('токен reka не был получен')
def reka_headers() -> dict[str, str]:
token = None
if token_path.exists():
token = loads(token_path.read_text()).get('accessToken')
return {
'authorization': f'Bearer {token}',
'content-type': 'application/json',
'user-agent': UA
}
def convert_openai_to_reka(messages: list[dict]) -> list[dict]:
reka_messages = [{'type': 'human', 'text': '👋'}]
skip_next = False
logger.debug('конвертация сообщений в формат reka')
for i, message in enumerate(messages):
if skip_next:
skip_next = False
continue
if message['role'] in ['user', 'assistant']:
content = message['content']
if isinstance(content, list):
text_content = ''
image_url = None
for part in content:
if part['type'] == 'text':
text_content += part['text'] + ' '
elif part['type'] == 'image_url':
image_url = part['image_url']['url']
reka_message = {
'type': 'human' if message['role'] == 'user' else 'model',
'text': text_content.strip()
}
if image_url:
reka_message['image_url'] = image_url
reka_message['media_type'] = 'image'
reka_messages.append(reka_message)
else:
reka_messages.append({
'type': 'human' if message['role'] == 'user' else 'model',
'text': content
})
elif message['role'] == 'system':
if i + 1 < len(messages) and messages[i + 1]['role'] == 'user':
combined_text = '[SYSTEM: ' + message['content'] + '] ' + messages[i + 1]['content']
reka_messages.append({
'type': 'human',
'text': combined_text
})
skip_next = True
else:
reka_messages.append({
'type': 'human',
'text': '[SYSTEM: ' + message['content'] + ']'
})
return reka_messages
def format_part(current_text: str, previous_text: str, finish_reason: str):
logger.debug(f'форматирование сообщения: {current_text}')
return f"data: {dumps({
'id': 'chatcmpl-0',
'object': 'chat.completion.chunk',
'created': int(datetime.now().timestamp()),
'model': 'reka-core',
'system_fingerprint': 'fp_67802d9a6d',
'choices': [{
'index': 0,
'delta': {'content': current_text[len(previous_text):]},
'finish_reason': finish_reason
}]}, ensure_ascii=False)}\n\n"
async def fetch_reka_stream(data: dict) -> AsyncGenerator[str, None]:
logger.info('запрос к reka и стриминг ответа')
async with AsyncClient() as client:
response = await client.post(REKA_API_URL, headers=reka_headers(), json=data, timeout=None)
previous_text = ''
async for line in response.aiter_lines():
logger.debug(line)
if line.startswith('{"detail":'):
yield format_part('ОШИБКА: ' + loads(line).get('detail'), previous_text, 'error')
break
if line.startswith('data:'):
event_data = loads(line[5:])
current_text = event_data['text']
sep_index = current_text.find('<sep')
finish_reason = None
if sep_index != -1:
finish_reason = 'stop'
current_text = current_text[:sep_index].rstrip()
if current_text.endswith('\n\n<'):
current_text = current_text[:-3]
if current_text != previous_text:
yield format_part(current_text, previous_text, finish_reason)
previous_text = current_text
if finish_reason == 'stop':
break
async def periodic_get_token(scheduler: AsyncIOScheduler):
logger.info('запуск задачи периодического обновления токена')
scheduler.add_job(
get_token,
trigger='interval',
hours=24,
next_run_time=datetime.now(),
misfire_grace_time=3600
)
@asynccontextmanager
async def app_lifespan(_) -> AsyncGenerator:
logger.info('запуск приложения')
scheduler = AsyncIOScheduler()
await periodic_get_token(scheduler)
try:
logger.info('запуск переодических задач')
scheduler.start()
logger.info('старт API')
yield
finally:
scheduler.shutdown()
logger.info('приложение завершено')
app = FastAPI(lifespan=app_lifespan, title='Reka_API')
banned_endpoints = [
'/openapi.json',
'/docs',
'/docs/oauth2-redirect',
'swagger_ui_redirect',
'/redoc',
]
@app.middleware('http')
async def block_banned_endpoints(request: Request, call_next):
logger.debug(f'получен запрос: {request.url.path}')
if request.url.path in banned_endpoints:
logger.warning(f'запрещенный endpoint: {request.url.path}')
return Response(status_code=403)
response = await call_next(request)
return response
def verify_token(authorization: str = Header(...)):
if authorization is None:
logger.warning('попытка доступа без заголовков авторизации')
raise HTTPException(status_code=401, detail='эм... нужен пролапс')
try:
scheme, token = authorization.split()
if scheme.lower() != 'bearer':
logger.warning('попытка доступа с неверным типом авторизации')
raise HTTPException(status_code=401, detail='пролапс не того вида...')
if token != API_TOKEN:
logger.warning('попытка доступа с неверным токеном')
raise HTTPException(status_code=401, detail='пролапс неверный...')
except ValueError:
logger.warning('попытка доступа с неверным типом авторизации')
raise HTTPException(status_code=401, detail='а где пролапс?')
@app.get('/')
async def root():
return HTMLResponse('ну пролапс, ну и что', status_code=200)
@app.post('/api/chat/completions')
@app.post('/api/v1/chat/completions')
async def chat_completions(request: Request, token: str = Depends(verify_token)):
logger.debug('запрос `completions`')
data = await request.json()
messages = data.get('messages', [])
reka_messages = convert_openai_to_reka(messages)
reka_data = {
'conversation_history': reka_messages,
'stream': True,
'use_search_engine': False,
'use_code_interpreter': False,
'model_name': 'reka-core',
'random_seed': randint(0, 2 ** 32 - 1)
}
return StreamingResponse(fetch_reka_stream(reka_data), media_type='text/event-stream')
@app.get('/api/models')
@app.get('/api/v1/models')
async def models():
logger.debug('запрос `models`')
return JSONResponse({
'object': 'list',
'data': [{'id': 'reka-core', 'object': 'model', 'created': int(datetime.now().timestamp()), 'owned_by': 'reka.ai'}]
}, status_code=200, media_type='application/json')
@app.get('/api/update_token')
async def update_token(token: str = Depends(verify_token)):
logger.info('запрос `update_token`')
task = get_token()
return JSONResponse({'status': 'обновление токена запущено'}, status_code=200, media_type='application/json')
@app.get('/api/show_last_screen')
async def show_last_screen(token: str = Depends(verify_token)):
logger.info('запрос `show_last_screen`')
return FileResponse(screenshot_path.resolve().as_posix(), media_type='image/jpeg', status_code=200)
@app.get('/api')
@app.get('/api/info', response_class=HTMLResponse)
async def info():
logger.debug('запрос `info`')
return HTMLResponse(content='''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Reka Reverse Proxy Endpoints</title>
<style>
body {font-family: monospace; background-color: #202020; color:#bfbcb9;}
.locked::before {content: '🔐'; margin-right: 5px;}
.unlocked::before {content: '🔓'; margin-right: 5px;}
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
const url = `${window.location.protocol}//${window.location.host}`;
const endpoints = [
{ type: 'locked', path: '/api/v1/chat/completions' },
{ type: 'locked', path: '/api/chat/completions' },
{ type: 'unlocked', path: '/api/v1/models' },
{ type: 'unlocked', path: '/api/models' },
{ type: 'locked', path: '/api/update_token' },
{ type: 'locked', path: '/api/show_last_screen' }
];
const listContainer = document.getElementById('endpoints-list');
endpoints.forEach(({ type, path }) => {
const listItem = document.createElement('li');
listItem.className = type;
listItem.textContent = `${url}${path}`;
listContainer.appendChild(listItem);
});
});
</script>
</head>
<body>
<h2>эндпоинты:</h2>
<ul id="endpoints-list"></ul>
</body>
</html>''', status_code=200)
if __name__ == '__main__':
from uvicorn import run as uvicorn_run
logger.info('запуск сервера uvicorn')
uvicorn_run(app, host='0.0.0.0', port=7860)