* Delete legacy from bot * Clear old models * Единый http клиент * РАГ полечен
This commit is contained in:
-172
@@ -1,172 +0,0 @@
|
||||
import json
|
||||
from typing import Optional, AsyncIterator
|
||||
import httpx
|
||||
from tg_bot.config.settings import settings
|
||||
|
||||
|
||||
class DeepSeekAPIError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DeepSeekClient:
|
||||
|
||||
def __init__(self, api_key: str | None = None, api_url: str | None = None):
|
||||
self.api_key = api_key or settings.DEEPSEEK_API_KEY
|
||||
self.api_url = api_url or settings.DEEPSEEK_API_URL
|
||||
self.timeout = 60.0
|
||||
|
||||
def _get_headers(self) -> dict[str, str]:
|
||||
if not self.api_key:
|
||||
raise DeepSeekAPIError("API key not set")
|
||||
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}"
|
||||
}
|
||||
|
||||
async def chat_completion(
|
||||
self,
|
||||
messages: list[dict[str, str]],
|
||||
model: str = "deepseek-chat",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
stream: bool = False
|
||||
) -> dict:
|
||||
if not self.api_key:
|
||||
return {
|
||||
"content": "API key not configured",
|
||||
"usage": {
|
||||
"prompt_tokens": 0,
|
||||
"completion_tokens": 0,
|
||||
"total_tokens": 0
|
||||
}
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"stream": stream
|
||||
}
|
||||
|
||||
if max_tokens is not None:
|
||||
payload["max_tokens"] = max_tokens
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.post(
|
||||
self.api_url,
|
||||
headers=self._get_headers(),
|
||||
json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
|
||||
if "choices" in data and len(data["choices"]) > 0:
|
||||
content = data["choices"][0]["message"]["content"]
|
||||
else:
|
||||
raise DeepSeekAPIError("Invalid response format")
|
||||
|
||||
usage = data.get("usage", {})
|
||||
|
||||
return {
|
||||
"content": content,
|
||||
"usage": {
|
||||
"prompt_tokens": usage.get("prompt_tokens", 0),
|
||||
"completion_tokens": usage.get("completion_tokens", 0),
|
||||
"total_tokens": usage.get("total_tokens", 0)
|
||||
}
|
||||
}
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
error_msg = f"API error: {e.response.status_code}"
|
||||
try:
|
||||
error_data = e.response.json()
|
||||
if "error" in error_data:
|
||||
error_msg = error_data['error'].get('message', error_msg)
|
||||
except:
|
||||
pass
|
||||
raise DeepSeekAPIError(error_msg) from e
|
||||
except httpx.RequestError as e:
|
||||
raise DeepSeekAPIError(f"Connection error: {str(e)}") from e
|
||||
except Exception as e:
|
||||
raise DeepSeekAPIError(str(e)) from e
|
||||
|
||||
async def stream_chat_completion(
|
||||
self,
|
||||
messages: list[dict[str, str]],
|
||||
model: str = "deepseek-chat",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None
|
||||
) -> AsyncIterator[str]:
|
||||
if not self.api_key:
|
||||
yield "API key not configured"
|
||||
return
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"stream": True
|
||||
}
|
||||
|
||||
if max_tokens is not None:
|
||||
payload["max_tokens"] = max_tokens
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
async with client.stream(
|
||||
"POST",
|
||||
self.api_url,
|
||||
headers=self._get_headers(),
|
||||
json=payload
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
|
||||
async for line in response.aiter_lines():
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
if line.startswith("data: "):
|
||||
line = line[6:]
|
||||
|
||||
if line.strip() == "[DONE]":
|
||||
break
|
||||
|
||||
try:
|
||||
data = json.loads(line)
|
||||
|
||||
if "choices" in data and len(data["choices"]) > 0:
|
||||
delta = data["choices"][0].get("delta", {})
|
||||
content = delta.get("content", "")
|
||||
if content:
|
||||
yield content
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
error_msg = f"API error: {e.response.status_code}"
|
||||
try:
|
||||
error_data = e.response.json()
|
||||
if "error" in error_data:
|
||||
error_msg = error_data['error'].get('message', error_msg)
|
||||
except:
|
||||
pass
|
||||
raise DeepSeekAPIError(error_msg) from e
|
||||
except httpx.RequestError as e:
|
||||
raise DeepSeekAPIError(f"Connection error: {str(e)}") from e
|
||||
except Exception as e:
|
||||
raise DeepSeekAPIError(str(e)) from e
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
if not self.api_key:
|
||||
return False
|
||||
|
||||
try:
|
||||
test_messages = [{"role": "user", "content": "test"}]
|
||||
await self.chat_completion(test_messages, max_tokens=1)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -1,87 +1,23 @@
|
||||
"""HTTP client utilities for making requests to the backend API"""
|
||||
import aiohttp
|
||||
from typing import Optional
|
||||
import ssl
|
||||
import os
|
||||
|
||||
|
||||
def get_windows_host_ip() -> Optional[str]:
|
||||
"""
|
||||
Get the Windows host IP address when running in WSL.
|
||||
In WSL2, the Windows host IP is typically the first nameserver in /etc/resolv.conf.
|
||||
"""
|
||||
try:
|
||||
if os.path.exists("/etc/resolv.conf"):
|
||||
with open("/etc/resolv.conf", "r") as f:
|
||||
for line in f:
|
||||
if line.startswith("nameserver"):
|
||||
ip = line.split()[1]
|
||||
if ip not in ["127.0.0.1", "127.0.0.53"] and not ip.startswith("fe80"):
|
||||
return ip
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def normalize_backend_url(url: str) -> str:
|
||||
"""
|
||||
Normalize backend URL for better compatibility, especially on WSL and Docker.
|
||||
"""
|
||||
if not ("localhost" in url or "127.0.0.1" in url):
|
||||
return url
|
||||
if os.path.exists("/.dockerenv"):
|
||||
print(f"Warning: Running in Docker but URL contains localhost: {url}")
|
||||
print("Please set BACKEND_URL environment variable in docker-compose.yml to use Docker service name (e.g., http://backend:8000/api/v1)")
|
||||
return url.replace("localhost", "127.0.0.1")
|
||||
try:
|
||||
if os.path.exists("/proc/version"):
|
||||
with open("/proc/version", "r") as f:
|
||||
version_content = f.read().lower()
|
||||
if "microsoft" in version_content:
|
||||
windows_ip = get_windows_host_ip()
|
||||
if windows_ip:
|
||||
if "localhost" in url or "127.0.0.1" in url:
|
||||
url = url.replace("localhost", windows_ip).replace("127.0.0.1", windows_ip)
|
||||
print(f"WSL detected: Using Windows host IP {windows_ip} for backend connection")
|
||||
return url
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not detect WSL environment: {e}")
|
||||
|
||||
if url.startswith("http://localhost") or url.startswith("https://localhost"):
|
||||
return url.replace("localhost", "127.0.0.1")
|
||||
return url
|
||||
|
||||
|
||||
def create_http_session(timeout: Optional[aiohttp.ClientTimeout] = None) -> aiohttp.ClientSession:
|
||||
"""
|
||||
Create a configured aiohttp ClientSession for backend API requests.
|
||||
|
||||
Args:
|
||||
timeout: Optional timeout configuration. Defaults to 30 seconds total timeout.
|
||||
|
||||
Returns:
|
||||
Configured aiohttp.ClientSession
|
||||
Создаем сессию для запросов к бэку
|
||||
"""
|
||||
if timeout is None:
|
||||
timeout = aiohttp.ClientTimeout(total=30, connect=10)
|
||||
|
||||
connector = aiohttp.TCPConnector(
|
||||
ssl=False,
|
||||
limit=100,
|
||||
limit_per_host=30,
|
||||
force_close=True,
|
||||
enable_cleanup_closed=True
|
||||
limit_per_host=30
|
||||
)
|
||||
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
return aiohttp.ClientSession(
|
||||
connector=connector,
|
||||
timeout=timeout,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ from aiogram.filters import Command, StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
import aiohttp
|
||||
from tg_bot.config.settings import settings
|
||||
from tg_bot.infrastructure.http_client import create_http_session
|
||||
from tg_bot.infrastructure.telegram.states.collection_states import (
|
||||
CollectionAccessStates,
|
||||
CollectionEditStates
|
||||
@@ -14,7 +15,7 @@ router = Router()
|
||||
|
||||
async def get_user_collections(telegram_id: str):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with create_http_session() as session:
|
||||
async with session.get(
|
||||
f"{settings.BACKEND_URL}/collections/",
|
||||
headers={"X-Telegram-ID": telegram_id}
|
||||
@@ -33,7 +34,7 @@ async def get_collection_documents(collection_id: str, telegram_id: str):
|
||||
url = f"{settings.BACKEND_URL}/documents/collection/{collection_id}"
|
||||
print(f"DEBUG get_collection_documents: URL={url}, collection_id={collection_id}, telegram_id={telegram_id}")
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with create_http_session() as session:
|
||||
async with session.get(
|
||||
url,
|
||||
headers={"X-Telegram-ID": telegram_id}
|
||||
@@ -57,7 +58,7 @@ async def get_collection_documents(collection_id: str, telegram_id: str):
|
||||
|
||||
async def search_in_collection(collection_id: str, query: str, telegram_id: str):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with create_http_session() as session:
|
||||
async with session.get(
|
||||
f"{settings.BACKEND_URL}/documents/collection/{collection_id}",
|
||||
params={"search": query},
|
||||
@@ -78,7 +79,7 @@ async def get_collection_info(collection_id: str, telegram_id: str):
|
||||
url = f"{settings.BACKEND_URL}/collections/{collection_id}"
|
||||
print(f"DEBUG get_collection_info: URL={url}, collection_id={collection_id}, telegram_id={telegram_id}")
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with create_http_session() as session:
|
||||
async with session.get(
|
||||
url,
|
||||
headers={"X-Telegram-ID": telegram_id}
|
||||
@@ -103,7 +104,7 @@ async def get_collection_info(collection_id: str, telegram_id: str):
|
||||
async def get_collection_access_list(collection_id: str, telegram_id: str):
|
||||
"""Получить список пользователей с доступом к коллекции"""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with create_http_session() as session:
|
||||
async with session.get(
|
||||
f"{settings.BACKEND_URL}/collections/{collection_id}/access",
|
||||
headers={"X-Telegram-ID": telegram_id}
|
||||
@@ -122,7 +123,7 @@ async def grant_collection_access(collection_id: str, telegram_id: str, owner_te
|
||||
url = f"{settings.BACKEND_URL}/collections/{collection_id}/access/telegram/{telegram_id}"
|
||||
print(f"DEBUG grant_collection_access: URL={url}, target_telegram_id={telegram_id}, owner_telegram_id={owner_telegram_id}")
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with create_http_session() as session:
|
||||
async with session.post(
|
||||
url,
|
||||
headers={"X-Telegram-ID": owner_telegram_id}
|
||||
@@ -145,7 +146,7 @@ async def grant_collection_access(collection_id: str, telegram_id: str, owner_te
|
||||
async def revoke_collection_access(collection_id: str, telegram_id: str, owner_telegram_id: str):
|
||||
"""Отозвать доступ к коллекции"""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with create_http_session() as session:
|
||||
async with session.delete(
|
||||
f"{settings.BACKEND_URL}/collections/{collection_id}/access/telegram/{telegram_id}",
|
||||
headers={"X-Telegram-ID": owner_telegram_id}
|
||||
@@ -281,7 +282,7 @@ async def show_collection_menu(callback: CallbackQuery):
|
||||
collection_name = collection_info.get("name", "Коллекция")
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with create_http_session() as session:
|
||||
async with session.get(
|
||||
f"{settings.BACKEND_URL}/users/telegram/{telegram_id}"
|
||||
) as response:
|
||||
@@ -673,7 +674,7 @@ async def process_edit_collection_description(message: Message, state: FSMContex
|
||||
if new_description:
|
||||
update_data["description"] = new_description
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with create_http_session() as session:
|
||||
async with session.put(
|
||||
f"{settings.BACKEND_URL}/collections/{collection_id}",
|
||||
json=update_data,
|
||||
|
||||
@@ -7,6 +7,7 @@ from aiogram.filters import StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
import aiohttp
|
||||
from tg_bot.config.settings import settings
|
||||
from tg_bot.infrastructure.http_client import create_http_session
|
||||
from tg_bot.infrastructure.telegram.states.collection_states import (
|
||||
DocumentEditStates,
|
||||
DocumentUploadStates
|
||||
@@ -18,7 +19,7 @@ router = Router()
|
||||
async def get_document_info(document_id: str, telegram_id: str):
|
||||
"""Получить информацию о документе"""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with create_http_session() as session:
|
||||
async with session.get(
|
||||
f"{settings.BACKEND_URL}/documents/{document_id}",
|
||||
headers={"X-Telegram-ID": telegram_id}
|
||||
@@ -34,7 +35,7 @@ async def get_document_info(document_id: str, telegram_id: str):
|
||||
async def delete_document(document_id: str, telegram_id: str):
|
||||
"""Удалить документ"""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with create_http_session() as session:
|
||||
async with session.delete(
|
||||
f"{settings.BACKEND_URL}/documents/{document_id}",
|
||||
headers={"X-Telegram-ID": telegram_id}
|
||||
@@ -54,7 +55,7 @@ async def update_document(document_id: str, telegram_id: str, title: str = None,
|
||||
if content:
|
||||
update_data["content"] = content
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with create_http_session() as session:
|
||||
async with session.put(
|
||||
f"{settings.BACKEND_URL}/documents/{document_id}",
|
||||
json=update_data,
|
||||
@@ -71,7 +72,7 @@ async def update_document(document_id: str, telegram_id: str, title: str = None,
|
||||
async def upload_document_to_collection(collection_id: str, file_data: bytes, filename: str, telegram_id: str):
|
||||
"""Загрузить документ в коллекцию"""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with create_http_session() as session:
|
||||
form_data = aiohttp.FormData()
|
||||
form_data.add_field('file', file_data, filename=filename, content_type='application/octet-stream')
|
||||
|
||||
@@ -120,7 +121,7 @@ async def view_document(callback: CallbackQuery):
|
||||
response += "\n\n<i>...</i>"
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with create_http_session() as session:
|
||||
async with session.get(
|
||||
f"{settings.BACKEND_URL}/collections/{collection_id}",
|
||||
headers={"X-Telegram-ID": telegram_id}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from aiogram import Router, types
|
||||
from aiogram.types import Message
|
||||
from datetime import datetime
|
||||
import aiohttp
|
||||
from tg_bot.config.settings import settings
|
||||
from tg_bot.domain.services.user_service import UserService, User
|
||||
from tg_bot.application.services.rag_service import RAGService
|
||||
@@ -60,12 +58,7 @@ async def process_premium_question(message: Message, user: User, question_text:
|
||||
answer = rag_result.get("answer", "Извините, не удалось сгенерировать ответ.")
|
||||
sources = rag_result.get("sources", [])
|
||||
|
||||
await save_conversation_to_backend(
|
||||
str(message.from_user.id),
|
||||
question_text,
|
||||
answer,
|
||||
sources
|
||||
)
|
||||
# Беседа уже сохранена в бэкенде через API /rag/question
|
||||
|
||||
response = (
|
||||
f"<b>Ваш вопрос:</b>\n"
|
||||
@@ -74,18 +67,10 @@ async def process_premium_question(message: Message, user: User, question_text:
|
||||
)
|
||||
|
||||
if sources:
|
||||
response += f"<b>Источники из коллекций:</b>\n"
|
||||
collections_used = {}
|
||||
for source in sources[:5]:
|
||||
collection_name = source.get('collection', 'Неизвестно')
|
||||
if collection_name not in collections_used:
|
||||
collections_used[collection_name] = []
|
||||
collections_used[collection_name].append(source.get('title', 'Без названия'))
|
||||
|
||||
for i, (collection_name, titles) in enumerate(collections_used.items(), 1):
|
||||
response += f"{i}. <b>Коллекция:</b> {collection_name}\n"
|
||||
for title in titles[:2]:
|
||||
response += f" • {title}\n"
|
||||
response += f"<b>Источники:</b>\n"
|
||||
for idx, source in enumerate(sources[:5], 1):
|
||||
title = source.get('title', 'Без названия')
|
||||
response += f"{idx}. {title}\n"
|
||||
response += "\n<i>Используйте /mycollections для просмотра всех коллекций</i>\n\n"
|
||||
|
||||
response += (
|
||||
@@ -122,12 +107,7 @@ async def process_free_question(message: Message, user: User, question_text: str
|
||||
answer = rag_result.get("answer", "Извините, не удалось сгенерировать ответ.")
|
||||
sources = rag_result.get("sources", [])
|
||||
|
||||
await save_conversation_to_backend(
|
||||
str(message.from_user.id),
|
||||
question_text,
|
||||
answer,
|
||||
sources
|
||||
)
|
||||
# Уже все сохранили через /rag/question
|
||||
|
||||
response = (
|
||||
f"<b>Ваш вопрос:</b>\n"
|
||||
@@ -136,18 +116,10 @@ async def process_free_question(message: Message, user: User, question_text: str
|
||||
)
|
||||
|
||||
if sources:
|
||||
response += f"<b>Источники из коллекций:</b>\n"
|
||||
collections_used = {}
|
||||
for source in sources[:5]:
|
||||
collection_name = source.get('collection', 'Неизвестно')
|
||||
if collection_name not in collections_used:
|
||||
collections_used[collection_name] = []
|
||||
collections_used[collection_name].append(source.get('title', 'Без названия'))
|
||||
|
||||
for i, (collection_name, titles) in enumerate(collections_used.items(), 1):
|
||||
response += f"{i}. <b>Коллекция:</b> {collection_name}\n"
|
||||
for title in titles[:2]:
|
||||
response += f" • {title}\n"
|
||||
response += f"<b>Источники:</b>\n"
|
||||
for idx, source in enumerate(sources[:5], 1):
|
||||
title = source.get('title', 'Без названия')
|
||||
response += f"{idx}. {title}\n"
|
||||
response += "\n<i>Используйте /mycollections для просмотра всех коллекций</i>\n\n"
|
||||
|
||||
response += (
|
||||
@@ -176,83 +148,7 @@ async def process_free_question(message: Message, user: User, question_text: str
|
||||
await message.answer(response, parse_mode="HTML")
|
||||
|
||||
|
||||
async def save_conversation_to_backend(telegram_id: str, question: str, answer: str, sources: list):
|
||||
try:
|
||||
from tg_bot.config.settings import settings
|
||||
backend_url = settings.BACKEND_URL
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
f"{backend_url}/users/telegram/{telegram_id}"
|
||||
) as user_response:
|
||||
if user_response.status != 200:
|
||||
return
|
||||
user_data = await user_response.json()
|
||||
user_uuid = user_data.get("user_id")
|
||||
|
||||
async with session.get(
|
||||
f"{backend_url}/collections/",
|
||||
headers={"X-Telegram-ID": telegram_id}
|
||||
) as collections_response:
|
||||
collections = []
|
||||
if collections_response.status == 200:
|
||||
collections = await collections_response.json()
|
||||
|
||||
collection_id = None
|
||||
if collections:
|
||||
collection_id = collections[0].get("collection_id")
|
||||
else:
|
||||
async with session.post(
|
||||
f"{backend_url}/collections",
|
||||
json={
|
||||
"name": "Основная коллекция",
|
||||
"description": "Коллекция по умолчанию",
|
||||
"is_public": False
|
||||
},
|
||||
headers={"X-Telegram-ID": telegram_id}
|
||||
) as create_collection_response:
|
||||
if create_collection_response.status in [200, 201]:
|
||||
collection_data = await create_collection_response.json()
|
||||
collection_id = collection_data.get("collection_id")
|
||||
|
||||
if not collection_id:
|
||||
return
|
||||
|
||||
async with session.post(
|
||||
f"{backend_url}/conversations",
|
||||
json={"collection_id": str(collection_id)},
|
||||
headers={"X-Telegram-ID": telegram_id}
|
||||
) as conversation_response:
|
||||
if conversation_response.status not in [200, 201]:
|
||||
return
|
||||
conversation_data = await conversation_response.json()
|
||||
conversation_id = conversation_data.get("conversation_id")
|
||||
|
||||
if not conversation_id:
|
||||
return
|
||||
|
||||
await session.post(
|
||||
f"{backend_url}/messages",
|
||||
json={
|
||||
"conversation_id": str(conversation_id),
|
||||
"content": question,
|
||||
"role": "user"
|
||||
},
|
||||
headers={"X-Telegram-ID": telegram_id}
|
||||
)
|
||||
|
||||
await session.post(
|
||||
f"{backend_url}/messages",
|
||||
json={
|
||||
"conversation_id": str(conversation_id),
|
||||
"content": answer,
|
||||
"role": "assistant",
|
||||
"sources": {"documents": sources}
|
||||
},
|
||||
headers={"X-Telegram-ID": telegram_id}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error saving conversation: {e}")
|
||||
#Сново сохраняется в /rag/question
|
||||
|
||||
|
||||
async def handle_limit_exceeded(message: Message, user: User):
|
||||
|
||||
Reference in New Issue
Block a user