|
|
|
@@ -63,14 +63,31 @@ class XAIService:
|
|
|
|
Raises:
|
|
|
|
Raises:
|
|
|
|
RuntimeError: bei HTTP-Fehler oder fehlendem file_id in der Antwort
|
|
|
|
RuntimeError: bei HTTP-Fehler oder fehlendem file_id in der Antwort
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
self._log(f"📤 Uploading {len(file_content)} bytes to xAI: {filename}")
|
|
|
|
# Normalize MIME type: xAI needs correct Content-Type for proper processing
|
|
|
|
|
|
|
|
# If generic octet-stream but file is clearly a PDF, fix it
|
|
|
|
|
|
|
|
if mime_type == 'application/octet-stream' and filename.lower().endswith('.pdf'):
|
|
|
|
|
|
|
|
mime_type = 'application/pdf'
|
|
|
|
|
|
|
|
self._log(f"⚠️ Corrected MIME type to application/pdf for {filename}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self._log(f"📤 Uploading {len(file_content)} bytes to xAI: {filename} ({mime_type})")
|
|
|
|
|
|
|
|
|
|
|
|
session = await self._get_session()
|
|
|
|
session = await self._get_session()
|
|
|
|
url = f"{XAI_FILES_URL}/v1/files"
|
|
|
|
url = f"{XAI_FILES_URL}/v1/files"
|
|
|
|
headers = {"Authorization": f"Bearer {self.api_key}"}
|
|
|
|
headers = {"Authorization": f"Bearer {self.api_key}"}
|
|
|
|
|
|
|
|
|
|
|
|
form = aiohttp.FormData()
|
|
|
|
# Create multipart form with explicit UTF-8 filename encoding
|
|
|
|
form.add_field('file', file_content, filename=filename, content_type=mime_type)
|
|
|
|
# aiohttp automatically URL-encodes filenames with special chars,
|
|
|
|
|
|
|
|
# but xAI expects raw UTF-8 in the filename parameter
|
|
|
|
|
|
|
|
form = aiohttp.FormData(quote_fields=False)
|
|
|
|
|
|
|
|
form.add_field(
|
|
|
|
|
|
|
|
'file',
|
|
|
|
|
|
|
|
file_content,
|
|
|
|
|
|
|
|
filename=filename,
|
|
|
|
|
|
|
|
content_type=mime_type
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
# CRITICAL: purpose="file_search" enables proper PDF processing
|
|
|
|
|
|
|
|
# Without this, xAI throws "internal error" on complex PDFs
|
|
|
|
|
|
|
|
form.add_field('purpose', 'file_search')
|
|
|
|
|
|
|
|
|
|
|
|
async with session.post(url, data=form, headers=headers) as response:
|
|
|
|
async with session.post(url, data=form, headers=headers) as response:
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
@@ -95,9 +112,12 @@ class XAIService:
|
|
|
|
|
|
|
|
|
|
|
|
async def add_to_collection(self, collection_id: str, file_id: str) -> None:
|
|
|
|
async def add_to_collection(self, collection_id: str, file_id: str) -> None:
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Fügt eine Datei einer xAI-Collection hinzu.
|
|
|
|
Fügt eine Datei einer xAI-Collection (Vector Store) hinzu.
|
|
|
|
|
|
|
|
|
|
|
|
POST https://management-api.x.ai/v1/collections/{collection_id}/documents/{file_id}
|
|
|
|
POST https://api.x.ai/v1/vector_stores/{vector_store_id}/files
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Uses the OpenAI-compatible API pattern for adding files to vector stores.
|
|
|
|
|
|
|
|
This triggers proper indexing and processing.
|
|
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
Raises:
|
|
|
|
RuntimeError: bei HTTP-Fehler
|
|
|
|
RuntimeError: bei HTTP-Fehler
|
|
|
|
@@ -105,13 +125,16 @@ class XAIService:
|
|
|
|
self._log(f"📚 Adding file {file_id} to collection {collection_id}")
|
|
|
|
self._log(f"📚 Adding file {file_id} to collection {collection_id}")
|
|
|
|
|
|
|
|
|
|
|
|
session = await self._get_session()
|
|
|
|
session = await self._get_session()
|
|
|
|
url = f"{XAI_MANAGEMENT_URL}/v1/collections/{collection_id}/documents/{file_id}"
|
|
|
|
# Use the OpenAI-compatible endpoint (not management API)
|
|
|
|
|
|
|
|
url = f"{XAI_FILES_URL}/v1/vector_stores/{collection_id}/files"
|
|
|
|
headers = {
|
|
|
|
headers = {
|
|
|
|
"Authorization": f"Bearer {self.management_key}",
|
|
|
|
"Authorization": f"Bearer {self.api_key}",
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async with session.post(url, headers=headers) as response:
|
|
|
|
payload = {"file_id": file_id}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async with session.post(url, json=payload, headers=headers) as response:
|
|
|
|
if response.status not in (200, 201):
|
|
|
|
if response.status not in (200, 201):
|
|
|
|
raw = await response.text()
|
|
|
|
raw = await response.text()
|
|
|
|
raise RuntimeError(
|
|
|
|
raise RuntimeError(
|
|
|
|
|