Этап 8: Поддержка российских LLM
This commit is contained in:
parent
0a294e89fc
commit
a5db7abdd0
|
|
@ -28,6 +28,7 @@ STT_MODEL=vosk
|
||||||
# Russian LLM (optional)
|
# Russian LLM (optional)
|
||||||
GIGACHAT_CREDENTIALS=
|
GIGACHAT_CREDENTIALS=
|
||||||
YANDEX_API_KEY=
|
YANDEX_API_KEY=
|
||||||
|
YANDEX_FOLDER_ID=
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
DATABASE_URL=sqlite+aiosqlite:///./valera.db
|
DATABASE_URL=sqlite+aiosqlite:///./valera.db
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ class Settings(BaseSettings):
|
||||||
|
|
||||||
gigachat_credentials: Optional[str] = None
|
gigachat_credentials: Optional[str] = None
|
||||||
yandex_api_key: Optional[str] = None
|
yandex_api_key: Optional[str] = None
|
||||||
|
yandex_folder_id: Optional[str] = None
|
||||||
|
|
||||||
database_url: str = "sqlite+aiosqlite:///./valera.db"
|
database_url: str = "sqlite+aiosqlite:///./valera.db"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,12 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"/help - Показать эту справку\n"
|
"/help - Показать эту справку\n"
|
||||||
"/mode confirm - Режим с подтверждением\n"
|
"/mode confirm - Режим с подтверждением\n"
|
||||||
"/mode auto - Автономный режим\n"
|
"/mode auto - Автономный режим\n"
|
||||||
"/use qwen|open - Выбрать инструмент\n"
|
"/use qwen|open|gigachat|yandex - Выбрать инструмент\n"
|
||||||
"/cancel - Отменить текущее действие\n"
|
"/cancel - Отменить текущее действие\n"
|
||||||
"/qwen <текст> - Задать вопрос qwen-code\n"
|
"/qwen <текст> - Задать вопрос qwen-code\n"
|
||||||
"/open <текст> - Задать вопрос opencode\n"
|
"/open <текст> - Задать вопрос opencode\n"
|
||||||
|
"/gigachat <текст> - Задать вопрос Gigachat\n"
|
||||||
|
"/yandex <текст> - Задать вопрос YandexGPT\n"
|
||||||
"/forget - Очистить историю чата\n"
|
"/forget - Очистить историю чата\n"
|
||||||
"/remind <текст> <время> - Создать напоминание\n"
|
"/remind <текст> <время> - Создать напоминание\n"
|
||||||
"/stt on|off - Включить/выключить распознавание речи\n\n"
|
"/stt on|off - Включить/выключить распознавание речи\n\n"
|
||||||
|
|
@ -79,16 +81,21 @@ async def mode_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
async def use_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def use_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
if not context.args:
|
if not context.args:
|
||||||
current = orchestrator.get_default_tool()
|
current = orchestrator.get_default_tool()
|
||||||
await update.message.reply_text(f"Текущий инструмент: {current}")
|
available = ", ".join(orchestrator.get_available_tools())
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"Текущий инструмент: {current}\n"
|
||||||
|
f"Доступные: {available}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
tool = context.args[0].lower()
|
tool = context.args[0].lower()
|
||||||
if tool in ["qwen", "open"]:
|
if tool in ["qwen", "open", "gigachat", "yandex"]:
|
||||||
tool = "qwen" if tool == "qwen" else "opencode"
|
tool_map = {"qwen": "qwen", "open": "opencode", "gigachat": "gigachat", "yandex": "yandex"}
|
||||||
|
tool = tool_map.get(tool, tool)
|
||||||
orchestrator.set_default_tool(tool)
|
orchestrator.set_default_tool(tool)
|
||||||
await update.message.reply_text(f"Инструмент изменён на {tool}")
|
await update.message.reply_text(f"Инструмент изменён на {tool}")
|
||||||
else:
|
else:
|
||||||
await update.message.reply_text("Использование: /use qwen | open")
|
await update.message.reply_text("Использование: /use qwen | open | gigachat | yandex")
|
||||||
|
|
||||||
|
|
||||||
async def cancel_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def cancel_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
|
@ -224,6 +231,72 @@ async def open_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
await execute_tool_query(update, "opencode", prompt)
|
await execute_tool_query(update, "opencode", prompt)
|
||||||
|
|
||||||
|
|
||||||
|
async def gigachat_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
prompt = " ".join(context.args)
|
||||||
|
if not prompt:
|
||||||
|
await update.message.reply_text("Использование: /gigachat <текст>")
|
||||||
|
return
|
||||||
|
|
||||||
|
chat_id = update.effective_chat.id
|
||||||
|
mode = chat_state.get_mode(chat_id)
|
||||||
|
|
||||||
|
if mode == ChatMode.CONFIRM:
|
||||||
|
keyboard = [
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("Да", callback_data="confirm_yes"),
|
||||||
|
InlineKeyboardButton("Нет", callback_data="confirm_no")
|
||||||
|
]
|
||||||
|
]
|
||||||
|
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||||
|
|
||||||
|
chat_state.set_waiting_confirmation(chat_id, True, {
|
||||||
|
"type": "tool",
|
||||||
|
"tool": "gigachat",
|
||||||
|
"prompt": prompt
|
||||||
|
})
|
||||||
|
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"Выполнить запрос к Gigachat?\n\n{prompt[:200]}...",
|
||||||
|
reply_markup=reply_markup
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await update.message.reply_text("Думаю...")
|
||||||
|
await execute_tool_query(update, "gigachat", prompt)
|
||||||
|
|
||||||
|
|
||||||
|
async def yandex_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
prompt = " ".join(context.args)
|
||||||
|
if not prompt:
|
||||||
|
await update.message.reply_text("Использование: /yandex <текст>")
|
||||||
|
return
|
||||||
|
|
||||||
|
chat_id = update.effective_chat.id
|
||||||
|
mode = chat_state.get_mode(chat_id)
|
||||||
|
|
||||||
|
if mode == ChatMode.CONFIRM:
|
||||||
|
keyboard = [
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("Да", callback_data="confirm_yes"),
|
||||||
|
InlineKeyboardButton("Нет", callback_data="confirm_no")
|
||||||
|
]
|
||||||
|
]
|
||||||
|
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||||
|
|
||||||
|
chat_state.set_waiting_confirmation(chat_id, True, {
|
||||||
|
"type": "tool",
|
||||||
|
"tool": "yandex",
|
||||||
|
"prompt": prompt
|
||||||
|
})
|
||||||
|
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"Выполнить запрос к YandexGPT?\n\n{prompt[:200]}...",
|
||||||
|
reply_markup=reply_markup
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await update.message.reply_text("Думаю...")
|
||||||
|
await execute_tool_query(update, "yandex", prompt)
|
||||||
|
|
||||||
|
|
||||||
async def forget_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def forget_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
chat_id = update.effective_chat.id
|
chat_id = update.effective_chat.id
|
||||||
orchestrator.memory.clear_chat(chat_id)
|
orchestrator.memory.clear_chat(chat_id)
|
||||||
|
|
@ -349,6 +422,8 @@ def main():
|
||||||
application.add_handler(CommandHandler("stt", stt_command))
|
application.add_handler(CommandHandler("stt", stt_command))
|
||||||
application.add_handler(CommandHandler("qwen", qwen_command))
|
application.add_handler(CommandHandler("qwen", qwen_command))
|
||||||
application.add_handler(CommandHandler("open", open_command))
|
application.add_handler(CommandHandler("open", open_command))
|
||||||
|
application.add_handler(CommandHandler("gigachat", gigachat_command))
|
||||||
|
application.add_handler(CommandHandler("yandex", yandex_command))
|
||||||
application.add_handler(CommandHandler("forget", forget_command))
|
application.add_handler(CommandHandler("forget", forget_command))
|
||||||
application.add_handler(CommandHandler("remind", remind_command))
|
application.add_handler(CommandHandler("remind", remind_command))
|
||||||
application.add_handler(CallbackQueryHandler(confirm_callback))
|
application.add_handler(CallbackQueryHandler(confirm_callback))
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import logging
|
||||||
from typing import Tuple, Optional
|
from typing import Tuple, Optional
|
||||||
from config.config import get_settings
|
from config.config import get_settings
|
||||||
from src.tools.tool_runner import ToolRunner
|
from src.tools.tool_runner import ToolRunner
|
||||||
|
from src.tools.russian_llm import GigachatProvider, YandexGPTProvider
|
||||||
from src.memory.memory import Memory
|
from src.memory.memory import Memory
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -19,8 +20,12 @@ class Orchestrator:
|
||||||
self.default_tool = settings.default_tool
|
self.default_tool = settings.default_tool
|
||||||
self.tool_limits = {
|
self.tool_limits = {
|
||||||
"qwen": {"failed": 0, "max_failures": 3},
|
"qwen": {"failed": 0, "max_failures": 3},
|
||||||
"opencode": {"failed": 0, "max_failures": 3}
|
"opencode": {"failed": 0, "max_failures": 3},
|
||||||
|
"gigachat": {"failed": 0, "max_failures": 3},
|
||||||
|
"yandex": {"failed": 0, "max_failures": 3}
|
||||||
}
|
}
|
||||||
|
self.gigachat = GigachatProvider()
|
||||||
|
self.yandex = YandexGPTProvider()
|
||||||
|
|
||||||
def _check_rate_limit_error(self, result: str) -> bool:
|
def _check_rate_limit_error(self, result: str) -> bool:
|
||||||
rate_limit_keywords = [
|
rate_limit_keywords = [
|
||||||
|
|
@ -45,6 +50,11 @@ class Orchestrator:
|
||||||
|
|
||||||
full_prompt = self._build_prompt(prompt, chat_id)
|
full_prompt = self._build_prompt(prompt, chat_id)
|
||||||
|
|
||||||
|
if selected_tool == "gigachat":
|
||||||
|
result, success = await self.gigachat.ask(full_prompt)
|
||||||
|
elif selected_tool == "yandex":
|
||||||
|
result, success = await self.yandex.ask(full_prompt)
|
||||||
|
else:
|
||||||
result, success = await self.tool_runner.run_tool(selected_tool, full_prompt)
|
result, success = await self.tool_runner.run_tool(selected_tool, full_prompt)
|
||||||
|
|
||||||
if not success and self._check_rate_limit_error(result):
|
if not success and self._check_rate_limit_error(result):
|
||||||
|
|
@ -68,9 +78,17 @@ class Orchestrator:
|
||||||
return result, success
|
return result, success
|
||||||
|
|
||||||
def set_default_tool(self, tool: str):
|
def set_default_tool(self, tool: str):
|
||||||
if tool in ["qwen", "opencode"]:
|
if tool in ["qwen", "opencode", "gigachat", "yandex"]:
|
||||||
self.default_tool = tool
|
self.default_tool = tool
|
||||||
logger.info(f"Инструмент по умолчанию изменён на {tool}")
|
logger.info(f"Инструмент по умолчанию изменён на {tool}")
|
||||||
|
|
||||||
def get_default_tool(self) -> str:
|
def get_default_tool(self) -> str:
|
||||||
return self.default_tool
|
return self.default_tool
|
||||||
|
|
||||||
|
def get_available_tools(self) -> list:
|
||||||
|
tools = ["qwen", "opencode"]
|
||||||
|
if settings.gigachat_credentials:
|
||||||
|
tools.append("gigachat")
|
||||||
|
if settings.yandex_api_key:
|
||||||
|
tools.append("yandex")
|
||||||
|
return tools
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
from typing import Tuple, Optional
|
||||||
|
from config.config import get_settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
|
||||||
|
class GigachatProvider:
|
||||||
|
def __init__(self):
|
||||||
|
self.credentials = settings.gigachat_credentials
|
||||||
|
self.api_url = "https://ngw.devices.sberbank.ru:9443/api/v2"
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
def _get_token(self) -> Optional[str]:
|
||||||
|
if not self.credentials:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
self.api_url + "/oauth",
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"RqUID": "valera-bot",
|
||||||
|
"Authorization": f"Basic {self.credentials}"
|
||||||
|
},
|
||||||
|
data={"scope": "GIGACHAT_API_PERS"},
|
||||||
|
verify=False
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json().get("access_token")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка получения токена Gigachat: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def ask(self, prompt: str) -> Tuple[str, bool]:
|
||||||
|
if not self.credentials:
|
||||||
|
return "Gigachat не настроен. Укажите GIGACHAT_CREDENTIALS в .env", False
|
||||||
|
|
||||||
|
self.token = self._get_token()
|
||||||
|
if not self.token:
|
||||||
|
return "Не удалось получить токен Gigachat", False
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
self.api_url + "/chat/completions",
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Authorization": f"Bearer {self.token}"
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"model": "GigaChat",
|
||||||
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
|
"temperature": 0.7
|
||||||
|
},
|
||||||
|
verify=False,
|
||||||
|
timeout=60
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
return result["choices"][0]["message"]["content"], True
|
||||||
|
else:
|
||||||
|
return f"Ошибка API: {response.status_code}", False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка запроса к Gigachat: {e}")
|
||||||
|
return f"Ошибка: {str(e)}", False
|
||||||
|
|
||||||
|
|
||||||
|
class YandexGPTProvider:
|
||||||
|
def __init__(self):
|
||||||
|
self.api_key = settings.yandex_api_key
|
||||||
|
self.folder_id = settings.yandex_folder_id
|
||||||
|
|
||||||
|
async def ask(self, prompt: str) -> Tuple[str, bool]:
|
||||||
|
if not self.api_key:
|
||||||
|
return "YandexGPT не настроен. Укажите YANDEX_API_KEY в .env", False
|
||||||
|
|
||||||
|
try:
|
||||||
|
url = "https://llm.api.cloud.yandex.net/foundationModels/v1/completion"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Api-Key {self.api_key}"
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"modelUri": f"gpt://{self.folder_id}/yandexgpt",
|
||||||
|
"completionOptions": {
|
||||||
|
"stream": False,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"maxTokens": 2000
|
||||||
|
},
|
||||||
|
"messages": [
|
||||||
|
{"role": "user", "text": prompt}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(url, json=data, headers=headers, timeout=60)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
return result["result"]["alternatives"][0]["message"]["text"], True
|
||||||
|
else:
|
||||||
|
return f"Ошибка API: {response.status_code}", False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка запроса к YandexGPT: {e}")
|
||||||
|
return f"Ошибка: {str(e)}", False
|
||||||
Loading…
Reference in New Issue