Spaces:
Sleeping
Sleeping
| # app.py | |
| import os | |
| import typing as t | |
| import gradio as gr | |
| from openai import OpenAI, AsyncOpenAI | |
| VLLM_BASE_URL = os.getenv("VLLM_BASE_URL") | |
| VLLM_MODEL = os.getenv("VLLM_MODEL", "avibe") | |
| OPENAI_API_KEY = "" | |
| def _build_messages( | |
| user_message: str, | |
| history: list[tuple[str, str]] | None, | |
| system_prompt: str | None, | |
| ) -> list[dict[str, str]]: | |
| """Конвертация истории gr.ChatInterface в формат OpenAI Chat API.""" | |
| messages: list[dict[str, str]] = [] | |
| if system_prompt: | |
| messages.append({"role": "system", "content": system_prompt}) | |
| if history: | |
| for user_turn, assistant_turn in history: | |
| if user_turn: | |
| messages.append({"role": "user", "content": user_turn}) | |
| if assistant_turn: | |
| messages.append({"role": "assistant", "content": assistant_turn}) | |
| messages.append({"role": "user", "content": user_message}) | |
| return messages | |
| def _parse_stop_sequences(stop_sequences_raw: str | None) -> list[str] | None: | |
| """Парсит стоп-последовательности из строки (по запятым или переводам строк).""" | |
| if not stop_sequences_raw: | |
| return None | |
| raw = stop_sequences_raw.replace("\r", "\n").replace("\n", ",") | |
| items = [s.strip() for s in raw.split(",")] | |
| result = [s for s in items if s] | |
| return result or None | |
| def _create_client(base_url: str | None, api_key: str | None): | |
| """Синхронный клиент OpenAI-совместимого API (если понадобится).""" | |
| effective_api_key = api_key or os.environ.get("OPENAI_API_KEY") or "EMPTY_KEY" | |
| kwargs: dict[str, t.Any] = {"api_key": effective_api_key} | |
| if base_url: | |
| kwargs["base_url"] = base_url | |
| return OpenAI(**kwargs) | |
| def _create_async_client(base_url: str | None, api_key: str | None): | |
| """Асинхронный клиент OpenAI-совместимого API (vLLM/SGLang/TGI совместим).""" | |
| effective_api_key = api_key or os.environ.get("OPENAI_API_KEY") or "EMPTY_KEY" | |
| kwargs: dict[str, t.Any] = {"api_key": effective_api_key} | |
| if base_url: | |
| kwargs["base_url"] = base_url | |
| return AsyncOpenAI(**kwargs) | |
| async def generate_response( | |
| user_message: str, | |
| history: list[tuple[str, str]] | None, | |
| temperature: float, | |
| top_p: float, | |
| max_tokens: int | float | None, | |
| system_prompt: str, | |
| ): | |
| """ | |
| Генерация ответа с потоковой выдачей через OpenAI-совместимый vLLM/SGLang/TGI. | |
| Это async-генератор: Gradio ChatInterface будет отображать промежуточный текст | |
| по мере поступления токенов (streaming). См. доки ChatInterface. | |
| """ | |
| try: | |
| client = _create_async_client(base_url=VLLM_BASE_URL, api_key=OPENAI_API_KEY) | |
| messages = _build_messages( | |
| user_message=user_message, | |
| history=history, | |
| system_prompt=system_prompt, | |
| ) | |
| # Безопасная обработка max_tokens | |
| max_new_tokens: int | None = None | |
| if isinstance(max_tokens, (int, float)) and max_tokens > 0: | |
| max_new_tokens = int(max_tokens) | |
| create_kwargs: dict[str, t.Any] = { | |
| "model": VLLM_MODEL, | |
| "messages": messages, | |
| "temperature": float(temperature), | |
| "top_p": float(top_p), | |
| "max_tokens": max_new_tokens, | |
| "stream": True, # просим потоковую выдачу | |
| } | |
| # Для AsyncOpenAI при stream=True возвращается асинхронный поток событий | |
| stream = await client.chat.completions.create(**create_kwargs) | |
| accumulated_text = "" | |
| async for chunk in stream: | |
| try: | |
| # стандартный путь для openai>=1.** c Chat Completions | |
| delta = chunk.choices[0].delta | |
| content_piece = getattr(delta, "content", None) | |
| if content_piece: | |
| accumulated_text += content_piece | |
| # возвращаем на каждом шаге наращиваемую строку | |
| yield accumulated_text | |
| except Exception: | |
| # запасной путь для несовпадающих реализаций стриминга | |
| choice0 = chunk.choices[0] | |
| content_piece = None | |
| if hasattr(choice0, "delta") and isinstance(choice0.delta, dict): | |
| content_piece = choice0.delta.get("content") | |
| elif hasattr(choice0, "message") and isinstance(choice0.message, dict): | |
| content_piece = choice0.message.get("content") | |
| if content_piece: | |
| accumulated_text += content_piece | |
| yield accumulated_text | |
| # Если поток завершился без текста — вернём пустую строку | |
| if not accumulated_text: | |
| yield "" | |
| except Exception as e: | |
| # Показываем текст ошибки прямо в чате | |
| yield f"Ошибка: {e}" | |
| def build_demo() -> gr.Blocks: | |
| """Собираем интерфейс Gradio ChatInterface с дополнительными контролами.""" | |
| with gr.Blocks(title="vLLM Chat (Gradio)") as demo: | |
| gr.Markdown("### Тестим версию avibe после этапа GRPO") | |
| gr.ChatInterface( | |
| fn=generate_response, | |
| submit_btn="Отправить", | |
| additional_inputs=[ | |
| gr.Slider( | |
| minimum=0.0, maximum=2.0, value=0.7, step=0.01, label="temperature" | |
| ), | |
| gr.Slider( | |
| minimum=0.0, maximum=1.0, value=0.95, step=0.01, label="top_p" | |
| ), | |
| gr.Slider( | |
| minimum=1, maximum=8192, value=512, step=1, label="max_tokens" | |
| ), | |
| gr.Textbox( | |
| label="system prompt", | |
| placeholder="Вы можете задать стиль и поведение ассистента...", | |
| lines=3, | |
| ), | |
| ], | |
| ) | |
| return demo | |
| def main() -> None: | |
| """Точка входа. На HF Spaces НЕ указываем порт/хост и не используем share=True.""" | |
| demo = build_demo() | |
| # demo.queue() # можно включить, если ожидается высокая конкуррентность | |
| demo.launch(debug=True) | |
| if __name__ == "__main__": | |
| main() | |