Files
motia-iii/services/xai_service.py

178 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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'
)