feat(xai-service): implement xAI Files & Collections service for document synchronization
This commit is contained in:
177
services/xai_service.py
Normal file
177
services/xai_service.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""xAI Files & Collections Service"""
|
||||
import os
|
||||
import aiohttp
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
XAI_FILES_URL = "https://api.x.ai"
|
||||
XAI_MANAGEMENT_URL = "https://management-api.x.ai"
|
||||
|
||||
|
||||
class XAIService:
|
||||
"""
|
||||
Client für xAI Files API und Collections Management API.
|
||||
|
||||
Benötigte Umgebungsvariablen:
|
||||
- XAI_API_KEY – regulärer API-Key für File-Uploads (api.x.ai)
|
||||
- XAI_MANAGEMENT_KEY – Management-API-Key für Collection-Operationen (management-api.x.ai)
|
||||
"""
|
||||
|
||||
def __init__(self, ctx=None):
|
||||
self.api_key = os.getenv('XAI_API_KEY', '')
|
||||
self.management_key = os.getenv('XAI_MANAGEMENT_KEY', '')
|
||||
self.ctx = ctx
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
if not self.api_key:
|
||||
raise ValueError("XAI_API_KEY not configured in environment")
|
||||
if not self.management_key:
|
||||
raise ValueError("XAI_MANAGEMENT_KEY not configured in environment")
|
||||
|
||||
def _log(self, msg: str, level: str = 'info') -> None:
|
||||
if self.ctx:
|
||||
getattr(self.ctx.logger, level, self.ctx.logger.info)(msg)
|
||||
else:
|
||||
getattr(logger, level, logger.info)(msg)
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=120)
|
||||
)
|
||||
return self._session
|
||||
|
||||
async def close(self) -> None:
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
|
||||
async def upload_file(
|
||||
self,
|
||||
file_content: bytes,
|
||||
filename: str,
|
||||
mime_type: str = 'application/octet-stream'
|
||||
) -> str:
|
||||
"""
|
||||
Lädt eine Datei zur xAI Files API hoch (multipart/form-data).
|
||||
|
||||
POST https://api.x.ai/v1/files
|
||||
|
||||
Returns:
|
||||
xAI file_id (str)
|
||||
|
||||
Raises:
|
||||
RuntimeError: bei HTTP-Fehler oder fehlendem file_id in der Antwort
|
||||
"""
|
||||
self._log(f"📤 Uploading {len(file_content)} bytes to xAI: {filename}")
|
||||
|
||||
session = await self._get_session()
|
||||
url = f"{XAI_FILES_URL}/v1/files"
|
||||
headers = {"Authorization": f"Bearer {self.api_key}"}
|
||||
|
||||
form = aiohttp.FormData()
|
||||
form.add_field('file', file_content, filename=filename, content_type=mime_type)
|
||||
|
||||
async with session.post(url, data=form, headers=headers) as response:
|
||||
try:
|
||||
data = await response.json()
|
||||
except Exception:
|
||||
raw = await response.text()
|
||||
data = {"_raw": raw}
|
||||
|
||||
if response.status not in (200, 201):
|
||||
raise RuntimeError(
|
||||
f"xAI file upload failed ({response.status}): {data}"
|
||||
)
|
||||
|
||||
file_id = data.get('id') or data.get('file_id')
|
||||
if not file_id:
|
||||
raise RuntimeError(
|
||||
f"No file_id in xAI upload response: {data}"
|
||||
)
|
||||
|
||||
self._log(f"✅ xAI file uploaded: {file_id}")
|
||||
return file_id
|
||||
|
||||
async def add_to_collection(self, collection_id: str, file_id: str) -> None:
|
||||
"""
|
||||
Fügt eine Datei einer xAI-Collection hinzu.
|
||||
|
||||
POST https://management-api.x.ai/v1/collections/{collection_id}/documents/{file_id}
|
||||
|
||||
Raises:
|
||||
RuntimeError: bei HTTP-Fehler
|
||||
"""
|
||||
self._log(f"📚 Adding file {file_id} to collection {collection_id}")
|
||||
|
||||
session = await self._get_session()
|
||||
url = f"{XAI_MANAGEMENT_URL}/v1/collections/{collection_id}/documents/{file_id}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.management_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
async with session.post(url, headers=headers) as response:
|
||||
if response.status not in (200, 201):
|
||||
raw = await response.text()
|
||||
raise RuntimeError(
|
||||
f"Failed to add file to collection {collection_id} ({response.status}): {raw}"
|
||||
)
|
||||
|
||||
self._log(f"✅ File {file_id} added to collection {collection_id}")
|
||||
|
||||
async def remove_from_collection(self, collection_id: str, file_id: str) -> None:
|
||||
"""
|
||||
Entfernt eine Datei aus einer xAI-Collection.
|
||||
Die Datei selbst wird NICHT gelöscht – sie kann in anderen Collections sein.
|
||||
|
||||
DELETE https://management-api.x.ai/v1/collections/{collection_id}/documents/{file_id}
|
||||
|
||||
Raises:
|
||||
RuntimeError: bei HTTP-Fehler
|
||||
"""
|
||||
self._log(f"🗑️ Removing file {file_id} from collection {collection_id}")
|
||||
|
||||
session = await self._get_session()
|
||||
url = f"{XAI_MANAGEMENT_URL}/v1/collections/{collection_id}/documents/{file_id}"
|
||||
headers = {"Authorization": f"Bearer {self.management_key}"}
|
||||
|
||||
async with session.delete(url, headers=headers) as response:
|
||||
if response.status not in (200, 204):
|
||||
raw = await response.text()
|
||||
raise RuntimeError(
|
||||
f"Failed to remove file from collection {collection_id} ({response.status}): {raw}"
|
||||
)
|
||||
|
||||
self._log(f"✅ File {file_id} removed from collection {collection_id}")
|
||||
|
||||
async def add_to_collections(self, collection_ids: List[str], file_id: str) -> List[str]:
|
||||
"""
|
||||
Fügt eine Datei zu mehreren Collections hinzu.
|
||||
|
||||
Returns:
|
||||
Liste der erfolgreich hinzugefügten Collection-IDs
|
||||
"""
|
||||
added = []
|
||||
for collection_id in collection_ids:
|
||||
try:
|
||||
await self.add_to_collection(collection_id, file_id)
|
||||
added.append(collection_id)
|
||||
except Exception as e:
|
||||
self._log(
|
||||
f"⚠️ Fehler beim Hinzufügen zu Collection {collection_id}: {e}",
|
||||
level='warn'
|
||||
)
|
||||
return added
|
||||
|
||||
async def remove_from_collections(self, collection_ids: List[str], file_id: str) -> None:
|
||||
"""Entfernt eine Datei aus mehreren Collections (ignoriert Fehler pro Collection)."""
|
||||
for collection_id in collection_ids:
|
||||
try:
|
||||
await self.remove_from_collection(collection_id, file_id)
|
||||
except Exception as e:
|
||||
self._log(
|
||||
f"⚠️ Fehler beim Entfernen aus Collection {collection_id}: {e}",
|
||||
level='warn'
|
||||
)
|
||||
Reference in New Issue
Block a user