180 lines
6.6 KiB
Python
180 lines
6.6 KiB
Python
|
|
"""
|
|||
|
|
HTTP клиент для загрузки документов в Open WebUI API
|
|||
|
|
Поддержка метаданных user_id и access_groups для изоляции доступа
|
|||
|
|
"""
|
|||
|
|
import logging
|
|||
|
|
from typing import Dict, Optional, List
|
|||
|
|
import requests
|
|||
|
|
from requests.exceptions import RequestException, Timeout
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class OpenWebUIClient:
|
|||
|
|
"""Клиент для работы с Open WebUI API"""
|
|||
|
|
|
|||
|
|
def __init__(self, api_url: str, api_key: str, timeout: int = 300):
|
|||
|
|
"""
|
|||
|
|
Инициализация клиента Open WebUI API
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
api_url: Базовый URL API (например, https://odo.iieasy.ru/api/v1)
|
|||
|
|
api_key: API ключ из Open WebUI Settings -> Account -> API Keys
|
|||
|
|
timeout: Таймаут для запросов (секунды)
|
|||
|
|
"""
|
|||
|
|
self.api_url = api_url.rstrip("/")
|
|||
|
|
self.api_key = api_key
|
|||
|
|
self.timeout = timeout
|
|||
|
|
self.session = requests.Session()
|
|||
|
|
self.session.headers.update({
|
|||
|
|
"Authorization": f"Bearer {api_key}",
|
|||
|
|
"User-Agent": "iiEasy-Nextcloud-Sync/1.0"
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
def upload_document(
|
|||
|
|
self,
|
|||
|
|
file_content: bytes,
|
|||
|
|
filename: str,
|
|||
|
|
user_id: Optional[str] = None,
|
|||
|
|
access_groups: Optional[List[str]] = None,
|
|||
|
|
metadata: Optional[Dict] = None
|
|||
|
|
) -> Dict:
|
|||
|
|
"""
|
|||
|
|
Загрузка документа в Open WebUI через API
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
file_content: Содержимое файла в виде bytes
|
|||
|
|
filename: Имя файла
|
|||
|
|
user_id: ID пользователя Authentik для изоляции доступа
|
|||
|
|
access_groups: Список групп доступа (для совместного доступа)
|
|||
|
|
metadata: Дополнительные метаданные (source, path и т.д.)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Ответ API в виде словаря
|
|||
|
|
|
|||
|
|
Raises:
|
|||
|
|
RequestException: При ошибках загрузки
|
|||
|
|
"""
|
|||
|
|
url = f"{self.api_url}/documents"
|
|||
|
|
|
|||
|
|
# Подготовка данных для multipart/form-data
|
|||
|
|
files = {
|
|||
|
|
"file": (filename, file_content)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Метаданные передаются через form-data или JSON
|
|||
|
|
data = {}
|
|||
|
|
|
|||
|
|
if user_id:
|
|||
|
|
data["user_id"] = user_id
|
|||
|
|
|
|||
|
|
if access_groups:
|
|||
|
|
# Если API поддерживает список групп, передаем как JSON строку или отдельные поля
|
|||
|
|
data["access_groups"] = ",".join(access_groups) if isinstance(access_groups, list) else access_groups
|
|||
|
|
|
|||
|
|
if metadata:
|
|||
|
|
# Добавляем дополнительные метаданные
|
|||
|
|
for key, value in metadata.items():
|
|||
|
|
if key not in data:
|
|||
|
|
data[key] = str(value)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
logger.info(f"Загрузка документа {filename} (размер: {len(file_content)} bytes)")
|
|||
|
|
|
|||
|
|
response = self.session.post(
|
|||
|
|
url,
|
|||
|
|
files=files,
|
|||
|
|
data=data,
|
|||
|
|
timeout=self.timeout
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
response.raise_for_status()
|
|||
|
|
|
|||
|
|
result = response.json() if response.content else {}
|
|||
|
|
logger.info(f"Документ {filename} успешно загружен. ID: {result.get('id', 'unknown')}")
|
|||
|
|
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
except Timeout:
|
|||
|
|
logger.error(f"Таймаут при загрузке документа {filename}")
|
|||
|
|
raise
|
|||
|
|
except RequestException as e:
|
|||
|
|
logger.error(f"Ошибка при загрузке документа {filename}: {e}")
|
|||
|
|
if hasattr(e.response, 'text'):
|
|||
|
|
logger.error(f"Ответ сервера: {e.response.text}")
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
def upload_document_with_text(
|
|||
|
|
self,
|
|||
|
|
text_content: str,
|
|||
|
|
filename: str,
|
|||
|
|
user_id: Optional[str] = None,
|
|||
|
|
access_groups: Optional[List[str]] = None,
|
|||
|
|
metadata: Optional[Dict] = None
|
|||
|
|
) -> Dict:
|
|||
|
|
"""
|
|||
|
|
Загрузка текстового документа (например, извлеченного из PDF)
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
text_content: Текст документа
|
|||
|
|
filename: Имя файла (будет изменено на .txt если нужно)
|
|||
|
|
user_id: ID пользователя Authentik
|
|||
|
|
access_groups: Список групп доступа
|
|||
|
|
metadata: Дополнительные метаданные
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Ответ API
|
|||
|
|
"""
|
|||
|
|
# Убеждаемся, что файл имеет расширение .txt
|
|||
|
|
if not filename.endswith('.txt'):
|
|||
|
|
filename = filename.rsplit('.', 1)[0] + '.txt'
|
|||
|
|
|
|||
|
|
file_content = text_content.encode('utf-8')
|
|||
|
|
return self.upload_document(
|
|||
|
|
file_content=file_content,
|
|||
|
|
filename=filename,
|
|||
|
|
user_id=user_id,
|
|||
|
|
access_groups=access_groups,
|
|||
|
|
metadata=metadata
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def check_document_exists(self, document_id: str) -> bool:
|
|||
|
|
"""
|
|||
|
|
Проверка существования документа по ID
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
document_id: ID документа
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
True если документ существует
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
url = f"{self.api_url}/documents/{document_id}"
|
|||
|
|
response = self.session.get(url, timeout=30)
|
|||
|
|
return response.status_code == 200
|
|||
|
|
except RequestException:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def delete_document(self, document_id: str) -> bool:
|
|||
|
|
"""
|
|||
|
|
Удаление документа по ID
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
document_id: ID документа
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
True если удаление успешно
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
url = f"{self.api_url}/documents/{document_id}"
|
|||
|
|
response = self.session.delete(url, timeout=30)
|
|||
|
|
response.raise_for_status()
|
|||
|
|
return True
|
|||
|
|
except RequestException as e:
|
|||
|
|
logger.error(f"Ошибка при удалении документа {document_id}: {e}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def close(self):
|
|||
|
|
"""Закрытие сессии"""
|
|||
|
|
self.session.close()
|