added RAG, Multiuser, TG bot
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
from aiogram import Router
|
||||
|
||||
from bot.handlers.auth import router as auth_router
|
||||
from bot.handlers.chat import router as chat_router
|
||||
from bot.handlers.commands import router as commands_router
|
||||
from bot.handlers.start import router as start_router
|
||||
|
||||
router = Router()
|
||||
router.include_router(start_router)
|
||||
router.include_router(auth_router)
|
||||
router.include_router(commands_router)
|
||||
router.include_router(chat_router)
|
||||
@@ -0,0 +1,126 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message
|
||||
|
||||
from bot.access import access_denied_text, is_allowed
|
||||
from bot.config import Settings
|
||||
from bot.filters import NotLinked
|
||||
from bot.ha_client import HaApiError, HaClient
|
||||
from bot.storage import Storage
|
||||
|
||||
router = Router()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _link_token(message: Message, settings: Settings, storage: Storage, token: str) -> None:
|
||||
if not message.from_user:
|
||||
return
|
||||
|
||||
telegram_id = message.from_user.id
|
||||
client = HaClient(settings.ha_api_base_url)
|
||||
|
||||
try:
|
||||
login_result = await client.login(token)
|
||||
except HaApiError as exc:
|
||||
await message.answer(f"Не удалось привязать токен: {exc}")
|
||||
return
|
||||
except Exception:
|
||||
logger.exception("Login failed for telegram_id=%s", telegram_id)
|
||||
await message.answer("Ошибка соединения с Home Assistant. Проверь HA_API_BASE_URL на сервере бота.")
|
||||
return
|
||||
|
||||
api_token = str(login_result.get("token") or token).strip()
|
||||
user_info = login_result.get("user") or {}
|
||||
ha_user_id = int(user_info.get("id") or 0)
|
||||
display_name = str(user_info.get("display_name") or user_info.get("username") or "")
|
||||
username = str(user_info.get("username") or "")
|
||||
|
||||
ha_client = HaClient(settings.ha_api_base_url, api_token)
|
||||
try:
|
||||
session_id = await ha_client.find_or_create_telegram_session()
|
||||
except Exception:
|
||||
logger.exception("Session setup failed for telegram_id=%s", telegram_id)
|
||||
await message.answer("Токен принят, но не удалось создать чат-сессию на сервере.")
|
||||
return
|
||||
|
||||
await storage.link_user(
|
||||
telegram_id=telegram_id,
|
||||
api_token=api_token,
|
||||
ha_user_id=ha_user_id,
|
||||
display_name=display_name,
|
||||
username=username,
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
name = display_name or username or f"user #{ha_user_id}"
|
||||
await message.answer(
|
||||
f"Готово! Привязан аккаунт: {name}\n"
|
||||
f"Чат-сессия Telegram: #{session_id}\n\n"
|
||||
"Рекомендую удалить сообщение с токеном из истории чата.\n"
|
||||
"Можешь писать ассистенту обычными сообщениями."
|
||||
)
|
||||
|
||||
|
||||
@router.message(Command("logout"))
|
||||
async def cmd_logout(message: Message, settings: Settings, storage: Storage) -> None:
|
||||
if not is_allowed(message, settings):
|
||||
await message.answer(access_denied_text())
|
||||
return
|
||||
if not message.from_user:
|
||||
return
|
||||
|
||||
removed = await storage.unlink_user(message.from_user.id)
|
||||
if removed:
|
||||
await message.answer("API-токен отвязан. Чтобы снова пользоваться ботом, отправь новый токен.")
|
||||
else:
|
||||
await message.answer("Токен не был привязан.")
|
||||
|
||||
|
||||
@router.message(Command("whoami"))
|
||||
async def cmd_whoami(message: Message, settings: Settings, storage: Storage) -> None:
|
||||
if not is_allowed(message, settings):
|
||||
await message.answer(access_denied_text())
|
||||
return
|
||||
if not message.from_user:
|
||||
return
|
||||
|
||||
linked = await storage.get_user(message.from_user.id)
|
||||
if not linked:
|
||||
await message.answer("Токен не привязан. Отправь API-токен или /start.")
|
||||
return
|
||||
|
||||
client = HaClient(settings.ha_api_base_url, linked.api_token)
|
||||
try:
|
||||
me = await client.me()
|
||||
user = me.get("user") or {}
|
||||
name = user.get("display_name") or user.get("username") or linked.display_name
|
||||
username = user.get("username") or linked.username
|
||||
await message.answer(
|
||||
f"Home Assistant: {name} (@{username})\n"
|
||||
f"HA user id: {linked.ha_user_id}\n"
|
||||
f"Telegram-сессия: #{linked.session_id}"
|
||||
)
|
||||
except Exception:
|
||||
await message.answer(
|
||||
f"Привязан локально: {linked.display_name or linked.username}\n"
|
||||
f"Telegram-сессия: #{linked.session_id}\n"
|
||||
"(Не удалось проверить токен на сервере — возможно, он отозван.)"
|
||||
)
|
||||
|
||||
|
||||
@router.message(F.text & ~F.text.startswith("/"), NotLinked())
|
||||
async def handle_possible_token(message: Message, settings: Settings, storage: Storage) -> None:
|
||||
if not is_allowed(message, settings):
|
||||
return
|
||||
if not message.from_user or not message.text:
|
||||
return
|
||||
|
||||
token = message.text.strip()
|
||||
if len(token) < 8:
|
||||
return
|
||||
|
||||
await _link_token(message, settings, storage, token)
|
||||
@@ -0,0 +1,129 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.enums import ChatAction
|
||||
from aiogram.types import Message
|
||||
|
||||
from bot.access import is_allowed
|
||||
from bot.config import Settings
|
||||
from bot.filters import IsLinked
|
||||
from bot.ha_client import HaClient
|
||||
from bot.sse import SseChunk
|
||||
from bot.notify_worker import advance_cursors, send_text
|
||||
from bot.storage import LinkedUser, Storage
|
||||
|
||||
router = Router()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_generation_locks: dict[int, asyncio.Lock] = {}
|
||||
|
||||
|
||||
def _user_lock(telegram_id: int) -> asyncio.Lock:
|
||||
if telegram_id not in _generation_locks:
|
||||
_generation_locks[telegram_id] = asyncio.Lock()
|
||||
return _generation_locks[telegram_id]
|
||||
|
||||
|
||||
async def _iter_stream(stream: AsyncIterator[SseChunk]) -> AsyncIterator[SseChunk]:
|
||||
async for chunk in stream:
|
||||
yield chunk
|
||||
|
||||
|
||||
async def _run_chat_stream(
|
||||
message: Message,
|
||||
settings: Settings,
|
||||
storage: Storage,
|
||||
linked: LinkedUser,
|
||||
stream: AsyncIterator[SseChunk],
|
||||
) -> None:
|
||||
accumulated = ""
|
||||
|
||||
async for chunk in stream:
|
||||
if chunk.event == "status":
|
||||
await message.bot.send_chat_action(message.chat.id, ChatAction.TYPING)
|
||||
elif chunk.event == "token":
|
||||
piece = str(chunk.data.get("content") or "")
|
||||
if piece:
|
||||
accumulated += piece
|
||||
elif chunk.event == "notice":
|
||||
content = str(chunk.data.get("content") or "").strip()
|
||||
if content:
|
||||
await send_text(message.bot, message.chat.id, content)
|
||||
elif chunk.event == "error":
|
||||
err = str(chunk.data.get("message") or "Ошибка генерации")
|
||||
await message.answer(err)
|
||||
return
|
||||
elif chunk.event == "done":
|
||||
break
|
||||
|
||||
if accumulated.strip():
|
||||
parts = _split_for_edit_or_send(accumulated)
|
||||
for part in parts:
|
||||
await message.answer(part)
|
||||
|
||||
client = HaClient(settings.ha_api_base_url, linked.api_token)
|
||||
try:
|
||||
await advance_cursors(storage, client, linked)
|
||||
except Exception:
|
||||
logger.exception("Failed to advance cursors for telegram_id=%s", linked.telegram_id)
|
||||
|
||||
|
||||
def _split_for_edit_or_send(text: str, limit: int = 4096) -> list[str]:
|
||||
if len(text) <= limit:
|
||||
return [text]
|
||||
parts: list[str] = []
|
||||
remaining = text
|
||||
while remaining:
|
||||
if len(remaining) <= limit:
|
||||
parts.append(remaining)
|
||||
break
|
||||
cut = remaining.rfind("\n", 0, limit)
|
||||
if cut <= 0:
|
||||
cut = limit
|
||||
parts.append(remaining[:cut])
|
||||
remaining = remaining[cut:].lstrip("\n")
|
||||
return parts
|
||||
|
||||
|
||||
@router.message(F.text & ~F.text.startswith("/"), IsLinked())
|
||||
async def handle_chat_message(message: Message, settings: Settings, storage: Storage) -> None:
|
||||
if not is_allowed(message, settings):
|
||||
return
|
||||
if not message.from_user or not message.text:
|
||||
return
|
||||
|
||||
linked = await storage.get_user(message.from_user.id)
|
||||
if not linked:
|
||||
return
|
||||
|
||||
lock = _user_lock(message.from_user.id)
|
||||
if lock.locked():
|
||||
await message.answer("Подожди, предыдущий ответ ещё генерируется.")
|
||||
return
|
||||
|
||||
async with lock:
|
||||
client = HaClient(settings.ha_api_base_url, linked.api_token)
|
||||
content = message.text.strip()
|
||||
if not content:
|
||||
return
|
||||
|
||||
try:
|
||||
status = await client.generation_status(linked.session_id)
|
||||
if status.get("active"):
|
||||
stream = client.stream_generation(linked.session_id)
|
||||
else:
|
||||
stream = client.send_message_stream(linked.session_id, content)
|
||||
except Exception as exc:
|
||||
logger.exception("Failed to start chat for telegram_id=%s", message.from_user.id)
|
||||
await message.answer(f"Ошибка связи с Home Assistant: {exc}")
|
||||
return
|
||||
|
||||
try:
|
||||
await _run_chat_stream(message, settings, storage, linked, _iter_stream(stream))
|
||||
except Exception as exc:
|
||||
logger.exception("Chat stream failed for telegram_id=%s", message.from_user.id)
|
||||
await message.answer(f"Ошибка при получении ответа: {exc}")
|
||||
@@ -0,0 +1,33 @@
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message
|
||||
|
||||
from bot.access import access_denied_text, is_allowed
|
||||
from bot.config import Settings
|
||||
from bot.ha_client import HaClient
|
||||
from bot.storage import Storage
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.message(Command("newchat"))
|
||||
async def cmd_newchat(message: Message, settings: Settings, storage: Storage) -> None:
|
||||
if not is_allowed(message, settings):
|
||||
await message.answer(access_denied_text())
|
||||
return
|
||||
if not message.from_user:
|
||||
return
|
||||
|
||||
linked = await storage.get_user(message.from_user.id)
|
||||
if not linked:
|
||||
await message.answer("Сначала привяжи API-токен (/start).")
|
||||
return
|
||||
|
||||
client = HaClient(settings.ha_api_base_url, linked.api_token)
|
||||
try:
|
||||
session = await client.create_session("Telegram")
|
||||
session_id = int(session["id"])
|
||||
await storage.set_session_id(message.from_user.id, session_id)
|
||||
await message.answer(f"Новая сессия создана: #{session_id}. История с прошлой сессии здесь не продолжается.")
|
||||
except Exception as exc:
|
||||
await message.answer(f"Не удалось создать сессию: {exc}")
|
||||
@@ -0,0 +1,44 @@
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message
|
||||
|
||||
from bot.access import access_denied_text, is_allowed
|
||||
from bot.config import Settings
|
||||
|
||||
router = Router()
|
||||
|
||||
HELP_TEXT = """Команды:
|
||||
/start — приветствие и привязка
|
||||
/help — эта справка
|
||||
/whoami — текущий пользователь Home Assistant
|
||||
/logout — отвязать API-токен
|
||||
/newchat — новая сессия чата в Telegram
|
||||
|
||||
Обычный текст — сообщение домашнему ассистенту.
|
||||
|
||||
Чтобы привязать аккаунт, отправь API-токен одним сообщением (из Settings → Пользователи на домашнем сервере). После привязки лучше удалить сообщение с токеном."""
|
||||
|
||||
|
||||
@router.message(Command("start"))
|
||||
async def cmd_start(message: Message, settings: Settings) -> None:
|
||||
if not is_allowed(message, settings):
|
||||
await message.answer(access_denied_text())
|
||||
return
|
||||
|
||||
await message.answer(
|
||||
"Привет! Я мост к домашнему ассистенту Home Assistant.\n\n"
|
||||
"Отправь API-токен одним сообщением, чтобы привязать аккаунт. "
|
||||
"Токен можно создать в веб-интерфейсе (Settings → Пользователи) "
|
||||
"или через create_user.py на сервере.\n\n"
|
||||
"После привязки все оповещения (напоминания, помидоро и т.д.) "
|
||||
"будут дублироваться сюда.\n\n"
|
||||
"Справка: /help"
|
||||
)
|
||||
|
||||
|
||||
@router.message(Command("help"))
|
||||
async def cmd_help(message: Message, settings: Settings) -> None:
|
||||
if not is_allowed(message, settings):
|
||||
await message.answer(access_denied_text())
|
||||
return
|
||||
await message.answer(HELP_TEXT)
|
||||
Reference in New Issue
Block a user