Add multi-provider AI config safeguards
This commit is contained in:
29
.env.example
29
.env.example
@@ -1,5 +1,32 @@
|
|||||||
PERPLEXITY_API_KEY=your_perplexity_api_key_here
|
# Оставьте незакомментированным только один AI API KEY.
|
||||||
|
# Если одновременно указать несколько AI ключей, колонка выдаст ошибку.
|
||||||
|
AI_PROVIDER=
|
||||||
|
|
||||||
|
# Perplexity
|
||||||
|
# PERPLEXITY_API_KEY=your_perplexity_api_key_here
|
||||||
PERPLEXITY_MODEL=llama-3.1-sonar-small-128k-chat
|
PERPLEXITY_MODEL=llama-3.1-sonar-small-128k-chat
|
||||||
|
PERPLEXITY_API_URL=https://api.perplexity.ai/chat/completions
|
||||||
|
|
||||||
|
# OpenAI
|
||||||
|
# OPENAI_API_KEY=your_openai_api_key_here
|
||||||
|
OPENAI_MODEL=gpt-4o-mini
|
||||||
|
OPENAI_API_URL=https://api.openai.com/v1/chat/completions
|
||||||
|
|
||||||
|
# Gemini
|
||||||
|
# GEMINI_API_KEY=your_gemini_api_key_here
|
||||||
|
GEMINI_MODEL=gemini-2.5-flash
|
||||||
|
GEMINI_API_URL=https://generativelanguage.googleapis.com/v1beta/openai/chat/completions
|
||||||
|
|
||||||
|
# Z.ai
|
||||||
|
# ZAI_API_KEY=your_zai_api_key_here
|
||||||
|
ZAI_MODEL=glm-5
|
||||||
|
ZAI_API_URL=https://api.z.ai/api/paas/v4/chat/completions
|
||||||
|
|
||||||
|
# Anthropic Claude
|
||||||
|
# ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
||||||
|
ANTHROPIC_MODEL=claude-sonnet-4-20250514
|
||||||
|
ANTHROPIC_API_URL=https://api.anthropic.com/v1/messages
|
||||||
|
ANTHROPIC_API_VERSION=2023-06-01
|
||||||
DEEPGRAM_API_KEY=your_deepgram_api_key_here
|
DEEPGRAM_API_KEY=your_deepgram_api_key_here
|
||||||
PORCUPINE_ACCESS_KEY=your_porcupine_access_key_here
|
PORCUPINE_ACCESS_KEY=your_porcupine_access_key_here
|
||||||
PORCUPINE_SENSITIVITY=0.8
|
PORCUPINE_SENSITIVITY=0.8
|
||||||
|
|||||||
31
README.md
31
README.md
@@ -24,7 +24,7 @@
|
|||||||
- Follow-up окно 4 секунды после ответа: если пользователь молчит, ассистент возвращается к ожиданию wake word.
|
- Follow-up окно 4 секунды после ответа: если пользователь молчит, ассистент возвращается к ожиданию wake word.
|
||||||
- Распознавание речи через Deepgram (WebSocket, VAD, fast stop).
|
- Распознавание речи через Deepgram (WebSocket, VAD, fast stop).
|
||||||
- Озвучка через Silero TTS (RU + EN, с прерыванием по wake word).
|
- Озвучка через Silero TTS (RU + EN, с прерыванием по wake word).
|
||||||
- AI-диалог через Perplexity API со streaming-ответом и контекстом.
|
- AI-диалог через Perplexity, OpenAI, Gemini, Z.ai и Anthropic Claude API со streaming-ответом и контекстом.
|
||||||
- Перевод RU -> EN и EN -> RU.
|
- Перевод RU -> EN и EN -> RU.
|
||||||
- Погода: текущий прогноз по городу по умолчанию или по названию города.
|
- Погода: текущий прогноз по городу по умолчанию или по названию города.
|
||||||
- Таймеры, будильники (включая будни/выходные), секундомеры.
|
- Таймеры, будильники (включая будни/выходные), секундомеры.
|
||||||
@@ -73,11 +73,19 @@ cp .env.example .env
|
|||||||
Минимально обязательные переменные:
|
Минимально обязательные переменные:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
PERPLEXITY_API_KEY=...
|
AI_PROVIDER= # опционально; можно оставить пустым
|
||||||
|
# Раскомментируйте только один AI API KEY:
|
||||||
|
# PERPLEXITY_API_KEY=...
|
||||||
|
# OPENAI_API_KEY=...
|
||||||
|
# GEMINI_API_KEY=...
|
||||||
|
# ZAI_API_KEY=...
|
||||||
|
# ANTHROPIC_API_KEY=...
|
||||||
DEEPGRAM_API_KEY=...
|
DEEPGRAM_API_KEY=...
|
||||||
PORCUPINE_ACCESS_KEY=...
|
PORCUPINE_ACCESS_KEY=...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Если одновременно оставить несколько AI API key, ассистент вернет ошибку: он не будет выбирать провайдера наугад.
|
||||||
|
|
||||||
### 4) Запуск
|
### 4) Запуск
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -92,8 +100,23 @@ python run.py
|
|||||||
|
|
||||||
| Переменная | Обязательно | По умолчанию | Назначение |
|
| Переменная | Обязательно | По умолчанию | Назначение |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `PERPLEXITY_API_KEY` | Да | - | Ключ Perplexity API |
|
| `AI_PROVIDER` | Нет | `perplexity` | Опциональный провайдер AI (`perplexity`, `openai`, `gemini`, `zai`, `anthropic`; также понимает `claude`) |
|
||||||
|
| `PERPLEXITY_API_KEY` | Да* | - | Ключ Perplexity API (*если выбран Perplexity и только этот AI ключ активен) |
|
||||||
| `PERPLEXITY_MODEL` | Нет | `llama-3.1-sonar-small-128k-chat` | Модель Perplexity |
|
| `PERPLEXITY_MODEL` | Нет | `llama-3.1-sonar-small-128k-chat` | Модель Perplexity |
|
||||||
|
| `PERPLEXITY_API_URL` | Нет | `https://api.perplexity.ai/chat/completions` | Endpoint Perplexity Chat Completions |
|
||||||
|
| `OPENAI_API_KEY` | Да* | - | Ключ OpenAI API (*если выбран OpenAI и только этот AI ключ активен) |
|
||||||
|
| `OPENAI_MODEL` | Нет | `gpt-4o-mini` | Модель OpenAI |
|
||||||
|
| `OPENAI_API_URL` | Нет | `https://api.openai.com/v1/chat/completions` | Endpoint OpenAI Chat Completions |
|
||||||
|
| `GEMINI_API_KEY` | Да* | - | Ключ Google Gemini API (*если выбран Gemini и только этот AI ключ активен) |
|
||||||
|
| `GEMINI_MODEL` | Нет | `gemini-2.5-flash` | Модель Gemini |
|
||||||
|
| `GEMINI_API_URL` | Нет | `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions` | OpenAI-compatible endpoint Gemini |
|
||||||
|
| `ZAI_API_KEY` | Да* | - | Ключ Z.ai API (*если выбран Z.ai и только этот AI ключ активен) |
|
||||||
|
| `ZAI_MODEL` | Нет | `glm-5` | Модель Z.ai |
|
||||||
|
| `ZAI_API_URL` | Нет | `https://api.z.ai/api/paas/v4/chat/completions` | Endpoint Z.ai Chat Completions |
|
||||||
|
| `ANTHROPIC_API_KEY` | Да* | - | Ключ Anthropic API (*если выбран Anthropic и только этот AI ключ активен) |
|
||||||
|
| `ANTHROPIC_MODEL` | Нет | `claude-sonnet-4-20250514` | Модель Claude |
|
||||||
|
| `ANTHROPIC_API_URL` | Нет | `https://api.anthropic.com/v1/messages` | Endpoint Anthropic Messages API |
|
||||||
|
| `ANTHROPIC_API_VERSION` | Нет | `2023-06-01` | Версия Anthropic API |
|
||||||
| `DEEPGRAM_API_KEY` | Да | - | Ключ Deepgram STT |
|
| `DEEPGRAM_API_KEY` | Да | - | Ключ Deepgram STT |
|
||||||
| `PORCUPINE_ACCESS_KEY` | Да | - | Ключ PicoVoice Porcupine |
|
| `PORCUPINE_ACCESS_KEY` | Да | - | Ключ PicoVoice Porcupine |
|
||||||
| `PORCUPINE_SENSITIVITY` | Нет | `0.8` | Чувствительность wake word |
|
| `PORCUPINE_SENSITIVITY` | Нет | `0.8` | Чувствительность wake word |
|
||||||
@@ -121,6 +144,8 @@ python run.py
|
|||||||
| Игра | `Давай сыграем в города` |
|
| Игра | `Давай сыграем в города` |
|
||||||
| Управление диалогом | `Повтори`, `Стоп`, `Хватит` |
|
| Управление диалогом | `Повтори`, `Стоп`, `Хватит` |
|
||||||
|
|
||||||
|
Память текущего диалога, история сообщений и `ROLE_JSON` системной роли сохраняются для всех поддерживаемых AI-провайдеров.
|
||||||
|
|
||||||
## Полезные команды
|
## Полезные команды
|
||||||
|
|
||||||
| Команда | Что делает |
|
| Команда | Что делает |
|
||||||
|
|||||||
432
app/core/ai.py
432
app/core/ai.py
@@ -1,11 +1,33 @@
|
|||||||
"""AI module for Perplexity API integration."""
|
"""AI module with pluggable providers."""
|
||||||
|
|
||||||
# Модуль общения с искусственным интеллектом (Perplexity API).
|
# Модуль общения с искусственным интеллектом.
|
||||||
# Обрабатывает запросы пользователя и переводы.
|
# Обрабатывает запросы пользователя и переводы через выбранный API-провайдер.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import re
|
|
||||||
from .config import PERPLEXITY_API_KEY, PERPLEXITY_MODEL, PERPLEXITY_API_URL
|
from .config import (
|
||||||
|
AI_PROVIDER,
|
||||||
|
ANTHROPIC_API_KEY,
|
||||||
|
ANTHROPIC_API_URL,
|
||||||
|
ANTHROPIC_API_VERSION,
|
||||||
|
ANTHROPIC_MODEL,
|
||||||
|
GEMINI_API_KEY,
|
||||||
|
GEMINI_API_URL,
|
||||||
|
GEMINI_MODEL,
|
||||||
|
OPENAI_API_KEY,
|
||||||
|
OPENAI_API_URL,
|
||||||
|
OPENAI_MODEL,
|
||||||
|
PERPLEXITY_API_KEY,
|
||||||
|
PERPLEXITY_API_URL,
|
||||||
|
PERPLEXITY_MODEL,
|
||||||
|
ZAI_API_KEY,
|
||||||
|
ZAI_API_URL,
|
||||||
|
ZAI_MODEL,
|
||||||
|
)
|
||||||
|
|
||||||
_HTTP = requests.Session()
|
_HTTP = requests.Session()
|
||||||
|
|
||||||
@@ -32,10 +54,311 @@ No explanations, no quotes, no comments.
|
|||||||
Separate variants with " / " (space slash space).
|
Separate variants with " / " (space slash space).
|
||||||
Keep the translation максимально кратким и естественным, без лишних слов."""
|
Keep the translation максимально кратким и естественным, без лишних слов."""
|
||||||
|
|
||||||
|
_PROVIDER_ALIASES = {
|
||||||
|
"": "perplexity",
|
||||||
|
"anthropic": "anthropic",
|
||||||
|
"claude": "anthropic",
|
||||||
|
"claude_anthropic": "anthropic",
|
||||||
|
"gemini": "gemini",
|
||||||
|
"google": "gemini",
|
||||||
|
"openai": "openai",
|
||||||
|
"perplexity": "perplexity",
|
||||||
|
"z.ai": "zai",
|
||||||
|
"z-ai": "zai",
|
||||||
|
"z_ai": "zai",
|
||||||
|
"zai": "zai",
|
||||||
|
}
|
||||||
|
|
||||||
|
_PROVIDER_SETTINGS = {
|
||||||
|
"perplexity": {
|
||||||
|
"provider": "perplexity",
|
||||||
|
"protocol": "openai_compatible",
|
||||||
|
"api_key": PERPLEXITY_API_KEY,
|
||||||
|
"model": PERPLEXITY_MODEL,
|
||||||
|
"api_url": PERPLEXITY_API_URL,
|
||||||
|
"name": "Perplexity",
|
||||||
|
"key_var": "PERPLEXITY_API_KEY",
|
||||||
|
"model_var": "PERPLEXITY_MODEL",
|
||||||
|
},
|
||||||
|
"openai": {
|
||||||
|
"provider": "openai",
|
||||||
|
"protocol": "openai_compatible",
|
||||||
|
"api_key": OPENAI_API_KEY,
|
||||||
|
"model": OPENAI_MODEL,
|
||||||
|
"api_url": OPENAI_API_URL,
|
||||||
|
"name": "OpenAI",
|
||||||
|
"key_var": "OPENAI_API_KEY",
|
||||||
|
"model_var": "OPENAI_MODEL",
|
||||||
|
},
|
||||||
|
"gemini": {
|
||||||
|
"provider": "gemini",
|
||||||
|
"protocol": "openai_compatible",
|
||||||
|
"api_key": GEMINI_API_KEY,
|
||||||
|
"model": GEMINI_MODEL,
|
||||||
|
"api_url": GEMINI_API_URL,
|
||||||
|
"name": "Gemini",
|
||||||
|
"key_var": "GEMINI_API_KEY",
|
||||||
|
"model_var": "GEMINI_MODEL",
|
||||||
|
},
|
||||||
|
"zai": {
|
||||||
|
"provider": "zai",
|
||||||
|
"protocol": "openai_compatible",
|
||||||
|
"api_key": ZAI_API_KEY,
|
||||||
|
"model": ZAI_MODEL,
|
||||||
|
"api_url": ZAI_API_URL,
|
||||||
|
"name": "Z.ai",
|
||||||
|
"key_var": "ZAI_API_KEY",
|
||||||
|
"model_var": "ZAI_MODEL",
|
||||||
|
"extra_headers": {
|
||||||
|
"Accept-Language": "en-US,en",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"anthropic": {
|
||||||
|
"provider": "anthropic",
|
||||||
|
"protocol": "anthropic",
|
||||||
|
"api_key": ANTHROPIC_API_KEY,
|
||||||
|
"model": ANTHROPIC_MODEL,
|
||||||
|
"api_url": ANTHROPIC_API_URL,
|
||||||
|
"api_version": ANTHROPIC_API_VERSION,
|
||||||
|
"name": "Anthropic Claude",
|
||||||
|
"key_var": "ANTHROPIC_API_KEY",
|
||||||
|
"model_var": "ANTHROPIC_MODEL",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _has_active_api_key(value) -> bool:
|
||||||
|
return bool(str(value or "").strip())
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_provider_name(provider_name: str) -> str:
|
||||||
|
normalized = (provider_name or "").strip().lower()
|
||||||
|
return _PROVIDER_ALIASES.get(normalized, normalized)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_provider_settings():
|
||||||
|
configured = [
|
||||||
|
cfg
|
||||||
|
for cfg in _PROVIDER_SETTINGS.values()
|
||||||
|
if _has_active_api_key(cfg.get("api_key"))
|
||||||
|
]
|
||||||
|
if len(configured) == 1:
|
||||||
|
cfg = configured[0]
|
||||||
|
requested = _normalize_provider_name(AI_PROVIDER)
|
||||||
|
if requested and requested in _PROVIDER_SETTINGS and requested != cfg["provider"]:
|
||||||
|
print(
|
||||||
|
f"⚠️ AI_PROVIDER={AI_PROVIDER!r} не совпадает с единственным "
|
||||||
|
f"активным ключом {cfg['name']}. Используем {cfg['name']}."
|
||||||
|
)
|
||||||
|
return cfg, None
|
||||||
|
|
||||||
|
if len(configured) > 1:
|
||||||
|
names = ", ".join(cfg["name"] for cfg in configured)
|
||||||
|
return None, (
|
||||||
|
"Обнаружено несколько AI API ключей. Оставьте незакомментированным "
|
||||||
|
f"только один ключ. Сейчас активны: {names}. "
|
||||||
|
"Колонка не знает, какой AI использовать."
|
||||||
|
)
|
||||||
|
|
||||||
|
provider = _normalize_provider_name(AI_PROVIDER)
|
||||||
|
cfg = _PROVIDER_SETTINGS.get(provider)
|
||||||
|
if cfg:
|
||||||
|
return cfg, None
|
||||||
|
|
||||||
|
supported = ", ".join(sorted(_PROVIDER_SETTINGS))
|
||||||
|
print(
|
||||||
|
f"⚠️ Неизвестный AI_PROVIDER={AI_PROVIDER!r}, используем Perplexity. "
|
||||||
|
f"Поддерживаются: {supported}."
|
||||||
|
)
|
||||||
|
return _PROVIDER_SETTINGS["perplexity"], None
|
||||||
|
|
||||||
|
|
||||||
|
def _content_to_text(content) -> str:
|
||||||
|
if isinstance(content, str):
|
||||||
|
return content
|
||||||
|
if isinstance(content, list):
|
||||||
|
parts = []
|
||||||
|
for item in content:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
text = item.get("text")
|
||||||
|
if text:
|
||||||
|
parts.append(str(text))
|
||||||
|
elif item is not None:
|
||||||
|
parts.append(str(item))
|
||||||
|
return "".join(parts)
|
||||||
|
if content is None:
|
||||||
|
return ""
|
||||||
|
return str(content)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_provider_config_error(cfg) -> Optional[str]:
|
||||||
|
if not cfg:
|
||||||
|
return "Не настроен AI-провайдер. Проверьте файл .env."
|
||||||
|
if not cfg["api_key"]:
|
||||||
|
return f"Не настроен {cfg['key_var']}. Проверьте файл .env."
|
||||||
|
if not cfg["model"]:
|
||||||
|
return f"Не настроен {cfg['model_var']}. Проверьте файл .env."
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_headers(cfg):
|
||||||
|
if cfg["protocol"] == "anthropic":
|
||||||
|
return {
|
||||||
|
"x-api-key": cfg["api_key"],
|
||||||
|
"anthropic-version": cfg["api_version"],
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {cfg['api_key']}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
headers.update(cfg.get("extra_headers") or {})
|
||||||
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
def _split_system_messages(messages):
|
||||||
|
system_parts = []
|
||||||
|
chat_messages = []
|
||||||
|
|
||||||
|
for message in messages:
|
||||||
|
role = str(message.get("role") or "").strip().lower()
|
||||||
|
content = _content_to_text(message.get("content"))
|
||||||
|
if role == "system":
|
||||||
|
if content:
|
||||||
|
system_parts.append(content)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if role not in ("user", "assistant"):
|
||||||
|
role = "user"
|
||||||
|
chat_messages.append({"role": role, "content": content})
|
||||||
|
|
||||||
|
return "\n\n".join(system_parts), chat_messages
|
||||||
|
|
||||||
|
|
||||||
|
def _build_payload(cfg, messages, max_tokens, temperature, stream):
|
||||||
|
if cfg["protocol"] == "anthropic":
|
||||||
|
system_prompt, chat_messages = _split_system_messages(messages)
|
||||||
|
payload = {
|
||||||
|
"model": cfg["model"],
|
||||||
|
"messages": chat_messages,
|
||||||
|
"max_tokens": max_tokens,
|
||||||
|
"temperature": temperature,
|
||||||
|
"stream": stream,
|
||||||
|
}
|
||||||
|
if system_prompt:
|
||||||
|
payload["system"] = system_prompt
|
||||||
|
return payload
|
||||||
|
|
||||||
|
return {
|
||||||
|
"model": cfg["model"],
|
||||||
|
"messages": messages,
|
||||||
|
"max_tokens": max_tokens,
|
||||||
|
"temperature": temperature,
|
||||||
|
"stream": stream,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_openai_compatible_content(data: dict) -> str:
|
||||||
|
choices = data.get("choices") or []
|
||||||
|
if not choices:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
message = choices[0].get("message") or {}
|
||||||
|
return _content_to_text(message.get("content"))
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_anthropic_content(data: dict) -> str:
|
||||||
|
return _content_to_text(data.get("content"))
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_response_content(cfg, data: dict) -> str:
|
||||||
|
if cfg["protocol"] == "anthropic":
|
||||||
|
return _extract_anthropic_content(data)
|
||||||
|
return _extract_openai_compatible_content(data)
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_openai_compatible_stream(response):
|
||||||
|
for line in response.iter_lines(decode_unicode=True):
|
||||||
|
if not line or not line.startswith("data:"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
data_str = line[5:].strip()
|
||||||
|
if data_str == "[DONE]":
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
data_json = json.loads(data_str)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
choices = data_json.get("choices") or []
|
||||||
|
if not choices:
|
||||||
|
continue
|
||||||
|
|
||||||
|
delta = choices[0].get("delta") or {}
|
||||||
|
content = delta.get("content", "")
|
||||||
|
if isinstance(content, str):
|
||||||
|
if content:
|
||||||
|
yield content
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(content, list):
|
||||||
|
for item in content:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
text = item.get("text")
|
||||||
|
if text:
|
||||||
|
yield str(text)
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_anthropic_stream(response):
|
||||||
|
for line in response.iter_lines(decode_unicode=True):
|
||||||
|
if not line or not line.startswith("data:"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
data_str = line[5:].strip()
|
||||||
|
if data_str == "[DONE]":
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
data_json = json.loads(data_str)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
chunk_type = data_json.get("type")
|
||||||
|
if chunk_type == "content_block_start":
|
||||||
|
content_block = data_json.get("content_block") or {}
|
||||||
|
text = content_block.get("text")
|
||||||
|
if text:
|
||||||
|
yield str(text)
|
||||||
|
elif chunk_type == "content_block_delta":
|
||||||
|
delta = data_json.get("delta") or {}
|
||||||
|
text = delta.get("text")
|
||||||
|
if text:
|
||||||
|
yield str(text)
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_stream_chunks(cfg, response):
|
||||||
|
if cfg["protocol"] == "anthropic":
|
||||||
|
yield from _iter_anthropic_stream(response)
|
||||||
|
return
|
||||||
|
|
||||||
|
yield from _iter_openai_compatible_stream(response)
|
||||||
|
|
||||||
|
|
||||||
|
def _log_request_exception(cfg, error: Exception):
|
||||||
|
details = ""
|
||||||
|
response = getattr(error, "response", None)
|
||||||
|
if response is not None:
|
||||||
|
body = (response.text or "").strip()
|
||||||
|
if body:
|
||||||
|
details = f" | body={body[:400]}"
|
||||||
|
print(f"❌ Ошибка API ({cfg['name']}): {error}{details}")
|
||||||
|
|
||||||
|
|
||||||
def _send_request(messages, max_tokens, temperature, error_text):
|
def _send_request(messages, max_tokens, temperature, error_text):
|
||||||
"""
|
"""
|
||||||
Внутренняя функция для отправки HTTP-запроса к Perplexity API.
|
Внутренняя функция для отправки HTTP-запроса к выбранному AI-провайдеру.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
messages: Список сообщений (история чата).
|
messages: Список сообщений (история чата).
|
||||||
@@ -43,34 +366,33 @@ def _send_request(messages, max_tokens, temperature, error_text):
|
|||||||
temperature: "Креативность" (0.2 - строго, 1.0 - креативно).
|
temperature: "Креативность" (0.2 - строго, 1.0 - креативно).
|
||||||
error_text: Текст ошибки для пользователя в случае сбоя.
|
error_text: Текст ошибки для пользователя в случае сбоя.
|
||||||
"""
|
"""
|
||||||
if not PERPLEXITY_API_KEY:
|
cfg, selection_error = _get_provider_settings()
|
||||||
return "Не настроен PERPLEXITY_API_KEY. Проверьте файл .env."
|
if selection_error:
|
||||||
headers = {
|
return selection_error
|
||||||
"Authorization": f"Bearer {PERPLEXITY_API_KEY}",
|
config_error = _get_provider_config_error(cfg)
|
||||||
"Content-Type": "application/json",
|
if config_error:
|
||||||
}
|
return config_error
|
||||||
payload = {
|
|
||||||
"model": PERPLEXITY_MODEL,
|
|
||||||
"messages": messages,
|
|
||||||
"max_tokens": max_tokens,
|
|
||||||
"temperature": temperature,
|
|
||||||
"stream": False # Убираем стриминг для более быстрого ответа
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = _HTTP.post(
|
response = _HTTP.post(
|
||||||
PERPLEXITY_API_URL, headers=headers, json=payload, timeout=15 # Уменьшаем таймаут
|
cfg["api_url"],
|
||||||
|
headers=_build_headers(cfg),
|
||||||
|
json=_build_payload(cfg, messages, max_tokens, temperature, stream=False),
|
||||||
|
timeout=15,
|
||||||
)
|
)
|
||||||
response.raise_for_status() # Проверка на ошибки HTTP (4xx, 5xx)
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
return data["choices"][0]["message"]["content"]
|
content = _extract_response_content(cfg, data)
|
||||||
|
if not content:
|
||||||
|
return "Не удалось обработать ответ от AI."
|
||||||
|
return content
|
||||||
except requests.exceptions.Timeout:
|
except requests.exceptions.Timeout:
|
||||||
return "Извините, сервер не отвечает. Попробуйте позже."
|
return f"Извините, сервер {cfg['name']} не отвечает. Попробуйте позже."
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as error:
|
||||||
print(f"❌ Ошибка API: {e}")
|
_log_request_exception(cfg, error)
|
||||||
return error_text
|
return error_text
|
||||||
except (KeyError, IndexError) as e:
|
except Exception as error:
|
||||||
print(f"❌ Ошибка парсинга ответа: {e}")
|
print(f"❌ Ошибка парсинга ответа ({cfg['name']}): {error}")
|
||||||
return "Не удалось обработать ответ от AI."
|
return "Не удалось обработать ответ от AI."
|
||||||
|
|
||||||
|
|
||||||
@@ -109,8 +431,13 @@ def ask_ai_stream(messages_history: list):
|
|||||||
"""
|
"""
|
||||||
Generator that yields chunks of the AI response as they arrive.
|
Generator that yields chunks of the AI response as they arrive.
|
||||||
"""
|
"""
|
||||||
if not PERPLEXITY_API_KEY:
|
cfg, selection_error = _get_provider_settings()
|
||||||
yield "Не настроен ключ PERPLEXITY_API_KEY. Проверьте файл .env."
|
if selection_error:
|
||||||
|
yield selection_error
|
||||||
|
return
|
||||||
|
config_error = _get_provider_config_error(cfg)
|
||||||
|
if config_error:
|
||||||
|
yield config_error
|
||||||
return
|
return
|
||||||
if not messages_history:
|
if not messages_history:
|
||||||
yield "Извините, я не расслышал вашу команду."
|
yield "Извините, я не расслышал вашу команду."
|
||||||
@@ -122,46 +449,29 @@ def ask_ai_stream(messages_history: list):
|
|||||||
if msg["role"] == "user":
|
if msg["role"] == "user":
|
||||||
last_user_message = msg["content"]
|
last_user_message = msg["content"]
|
||||||
break
|
break
|
||||||
print(f"🤖 Запрос к AI (Stream): {last_user_message}")
|
print(f"🤖 Запрос к AI ({cfg['name']}, Stream): {last_user_message}")
|
||||||
|
|
||||||
messages = [{"role": "system", "content": SYSTEM_PROMPT}] + list(messages_history)
|
messages = [{"role": "system", "content": SYSTEM_PROMPT}] + list(messages_history)
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {PERPLEXITY_API_KEY}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
payload = {
|
|
||||||
"model": PERPLEXITY_MODEL,
|
|
||||||
"messages": messages,
|
|
||||||
"max_tokens": 500,
|
|
||||||
"temperature": 1.0,
|
|
||||||
"stream": True, # Enable streaming
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = _HTTP.post(
|
response = _HTTP.post(
|
||||||
PERPLEXITY_API_URL, headers=headers, json=payload, timeout=15, stream=True # Уменьшаем таймаут
|
cfg["api_url"],
|
||||||
|
headers=_build_headers(cfg),
|
||||||
|
json=_build_payload(cfg, messages, 500, 1.0, stream=True),
|
||||||
|
timeout=15,
|
||||||
|
stream=True,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
import json
|
for chunk in _iter_stream_chunks(cfg, response):
|
||||||
|
yield chunk
|
||||||
for line in response.iter_lines(decode_unicode=True):
|
except requests.exceptions.Timeout:
|
||||||
if line:
|
yield f"Извините, сервер {cfg['name']} не отвечает. Попробуйте позже."
|
||||||
line_text = line
|
except requests.exceptions.RequestException as error:
|
||||||
if line_text.startswith("data: "):
|
_log_request_exception(cfg, error)
|
||||||
data_str = line_text[6:] # Skip "data: "
|
yield "Произошла ошибка связи."
|
||||||
if data_str == "[DONE]":
|
except Exception as error:
|
||||||
break
|
print(f"❌ Streaming Error ({cfg['name']}): {error}")
|
||||||
try:
|
|
||||||
data_json = json.loads(data_str)
|
|
||||||
content = data_json["choices"][0]["delta"].get("content", "")
|
|
||||||
if content:
|
|
||||||
yield content
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
continue
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Streaming Error: {e}")
|
|
||||||
yield "Произошла ошибка связи."
|
yield "Произошла ошибка связи."
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,47 @@ BASE_DIR = Path(__file__).resolve().parents[2]
|
|||||||
# Загружаем переменные из файла .env в корневом каталоге
|
# Загружаем переменные из файла .env в корневом каталоге
|
||||||
load_dotenv(BASE_DIR / ".env")
|
load_dotenv(BASE_DIR / ".env")
|
||||||
|
|
||||||
# --- Настройки AI (Perplexity) ---
|
# --- Настройки AI ---
|
||||||
# API ключ для доступа к нейросети
|
# AI_PROVIDER опционален. Приоритет у единственного активного AI API key.
|
||||||
|
# Если активных ключей несколько, AI-модуль вернет ошибку конфигурации.
|
||||||
|
AI_PROVIDER = os.getenv("AI_PROVIDER", "perplexity").strip().lower()
|
||||||
|
|
||||||
|
# Perplexity
|
||||||
PERPLEXITY_API_KEY = os.getenv("PERPLEXITY_API_KEY")
|
PERPLEXITY_API_KEY = os.getenv("PERPLEXITY_API_KEY")
|
||||||
# Модель, которую будем использовать (по умолчанию llama-3.1-sonar-small-128k-chat)
|
|
||||||
PERPLEXITY_MODEL = os.getenv("PERPLEXITY_MODEL", "llama-3.1-sonar-small-128k-chat")
|
PERPLEXITY_MODEL = os.getenv("PERPLEXITY_MODEL", "llama-3.1-sonar-small-128k-chat")
|
||||||
PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions"
|
PERPLEXITY_API_URL = os.getenv(
|
||||||
|
"PERPLEXITY_API_URL", "https://api.perplexity.ai/chat/completions"
|
||||||
|
)
|
||||||
|
|
||||||
|
# OpenAI
|
||||||
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||||
|
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
|
||||||
|
OPENAI_API_URL = os.getenv(
|
||||||
|
"OPENAI_API_URL", "https://api.openai.com/v1/chat/completions"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Gemini (через официальный OpenAI-compatible endpoint Google)
|
||||||
|
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
||||||
|
GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
|
||||||
|
GEMINI_API_URL = os.getenv(
|
||||||
|
"GEMINI_API_URL",
|
||||||
|
"https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Z.ai
|
||||||
|
ZAI_API_KEY = os.getenv("ZAI_API_KEY")
|
||||||
|
ZAI_MODEL = os.getenv("ZAI_MODEL", "glm-5")
|
||||||
|
ZAI_API_URL = os.getenv(
|
||||||
|
"ZAI_API_URL", "https://api.z.ai/api/paas/v4/chat/completions"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Anthropic Claude
|
||||||
|
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
|
||||||
|
ANTHROPIC_MODEL = os.getenv("ANTHROPIC_MODEL", "claude-sonnet-4-20250514")
|
||||||
|
ANTHROPIC_API_URL = os.getenv(
|
||||||
|
"ANTHROPIC_API_URL", "https://api.anthropic.com/v1/messages"
|
||||||
|
)
|
||||||
|
ANTHROPIC_API_VERSION = os.getenv("ANTHROPIC_API_VERSION", "2023-06-01")
|
||||||
|
|
||||||
# --- Настройки распознавания речи (Deepgram) ---
|
# --- Настройки распознавания речи (Deepgram) ---
|
||||||
# Ключ для облачного STT (Speech-to-Text)
|
# Ключ для облачного STT (Speech-to-Text)
|
||||||
|
|||||||
Reference in New Issue
Block a user