gateway/gwserver/connector_aichat_openai.py
2025-03-25 00:05:01 +01:00

302 lines
No EOL
12 KiB
Python

import os
import json
import logging
import httpx
import base64
import mimetypes
from typing import Dict, Any, List, Optional
from fastapi import HTTPException
import configload as configload
# Logger konfigurieren
logger = logging.getLogger(__name__)
# Konfigurationsdaten laden
def load_config_data():
config = configload.load_config()
return {
"api_key": config.get('Connector_AiOpenai', 'API_KEY'),
"api_url": config.get('Connector_AiOpenai', 'API_URL'),
"model_name": config.get('Connector_AiOpenai', 'MODEL_NAME'),
"temperature": float(config.get('Connector_AiOpenai', 'TEMPERATURE')),
"max_tokens": int(config.get('Connector_AiOpenai', 'MAX_TOKENS'))
}
class ChatService:
"""
Connector für die Kommunikation mit der OpenAI API.
"""
def __init__(self):
# Konfiguration laden
self.config = load_config_data()
self.api_key = self.config["api_key"]
self.api_url = self.config["api_url"]
self.model_name = self.config["model_name"]
# HttpClient für API-Aufrufe
self.http_client = httpx.AsyncClient(
timeout=120.0, # Längeres Timeout für komplexe Anfragen
headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
)
logger.info(f"OpenAI Connector initialisiert mit Modell: {self.model_name}")
async def call_api(self, messages: List[Dict[str, Any]], temperature: float = None, max_tokens: int = None) -> Dict[str, Any]:
"""
Ruft die OpenAI API mit den gegebenen Nachrichten auf.
Args:
messages: Liste von Nachrichten im OpenAI-Format (role, content)
temperature: Temperatur für die Antwortgenerierung (0.0-1.0)
max_tokens: Maximale Anzahl der Token in der Antwort
Returns:
Die Antwort der OpenAI API
Raises:
HTTPException: Bei Fehlern in der API-Kommunikation
"""
try:
# Verwende Parameter aus der Konfiguration, falls keine überschrieben wurden
if temperature is None:
temperature = self.config.get("temperature", 0.2)
if max_tokens is None:
max_tokens = self.config.get("max_tokens", 2000)
payload = {
"model": self.model_name,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens
}
response = await self.http_client.post(
self.api_url,
json=payload
)
if response.status_code != 200:
logger.error(f"OpenAI API-Fehler: {response.status_code} - {response.text}")
raise HTTPException(status_code=500, detail="Fehler bei der Kommunikation mit OpenAI API")
return response.json()
except Exception as e:
logger.error(f"Fehler beim Aufruf der OpenAI API: {str(e)}")
raise HTTPException(status_code=500, detail=f"Fehler beim Aufruf der OpenAI API: {str(e)}")
def prepare_file_message_content(self, prompt_text: str, file_paths: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Bereitet eine Nachricht mit Dateien für OpenAI API vor.
Args:
prompt_text: Der Text-Prompt
file_paths: Liste von Dateipfaden mit Metadaten (Dict mit id, name, type, path)
Returns:
Eine für OpenAI-API formatierte content-Liste
"""
message_content = [
{
"type": "text",
"text": prompt_text
}
]
# Füge Dateien als Base64-Anhänge hinzu
for file_info in file_paths:
file_path = file_info.get("path", "")
if file_path and os.path.exists(file_path):
try:
# Datei als Base64 codieren
with open(file_path, "rb") as f:
file_data = f.read()
base64_data = base64.b64encode(file_data).decode('utf-8')
# MIME-Typ bestimmen
mime_type, _ = mimetypes.guess_type(file_path)
if not mime_type:
mime_type = "application/octet-stream"
# Bei OpenAI werden Bilder anders behandelt als bei Anthropic
if mime_type.startswith("image/"):
message_content.append({
"type": "image_url",
"image_url": {
"url": f"data:{mime_type};base64,{base64_data}"
}
})
else:
# Für nicht-Bilder werden sie als Textbeschreibung hinzugefügt
message_content.append({
"type": "text",
"text": f"[Datei: {file_info.get('name', 'Unbekannt')} (Typ: {mime_type})]"
})
logger.info(f"Datei {file_info.get('name', 'Unbekannt')} als Anhang hinzugefügt")
except Exception as e:
logger.error(f"Fehler beim Hinzufügen der Datei {file_info.get('name', 'Unbekannt')}: {str(e)}")
return message_content
def parse_filedata(self, file_paths: List[Dict[str, Any]], prompt_text: str = "", file_contents: Dict[str, str] = None) -> Dict[str, Any]:
"""
Bereitet Dateien für die OpenAI API vor und erstellt ein einheitliches Message-Objekt.
Args:
file_paths: Liste von Dateipfaden mit Metadaten (Dict mit id, name, type, path)
prompt_text: Der Text-Prompt, der zusammen mit den Dateien gesendet werden soll
file_contents: Optional vorgelesene Dateiinhalte als Dict[file_id, content]
Returns:
Ein standardisiertes Message-Objekt, das für beide API-Typen verwendet werden kann
"""
# Basisstruktur für die Nachricht in OpenAI-Format
message = {
"role": "user",
"content": []
}
# Text-Prompt hinzufügen
if prompt_text:
message["content"].append({
"type": "text",
"text": prompt_text
})
# Dateien als Anhänge hinzufügen
for file_info in file_paths:
file_path = file_info.get("path", "")
file_name = file_info.get("name", "")
file_id = file_info.get("id", "")
# Prüfen, ob Dateiinhalt bereits vorhanden ist
if file_contents and file_id in file_contents:
# Bereits verarbeiteten Inhalt verwenden
message["content"].append({
"type": "text",
"text": f"Datei: {file_name}\n{file_contents[file_id]}"
})
logger.info(f"Vorverarbeiteter Inhalt für Datei {file_name} verwendet")
continue
# Sonst Datei direkt verarbeiten
if file_path and os.path.exists(file_path):
try:
# Datei einlesen
with open(file_path, "rb") as f:
file_data = f.read()
# MIME-Typ bestimmen mit python-magic, wenn verfügbar
try:
import magic
mime_type = magic.from_buffer(file_data, mime=True)
except ImportError:
# Fallback auf mimetypes, wenn python-magic nicht verfügbar ist
mime_type, _ = mimetypes.guess_type(file_path)
if not mime_type:
# Fallback auf Dateierweiterung
extension = os.path.splitext(file_path)[1].lower()[1:]
mime_type = get_mime_type_from_extension(extension)
# Content-Type bestimmen für OpenAI
if mime_type.startswith("image/"):
# Bild als Base64 kodieren
base64_data = base64.b64encode(file_data).decode('utf-8')
message["content"].append({
"type": "image_url",
"image_url": {
"url": f"data:{mime_type};base64,{base64_data}"
}
})
else:
# Für nicht-Bild-Dateien als Text hinzufügen
try:
# Textdateien direkt als Text extrahieren
if mime_type in ["text/plain", "text/csv", "application/json", "text/html", "application/xml"]:
message["content"].append({
"type": "text",
"text": f"[Datei: {file_name}]\n{file_data.decode('utf-8', errors='replace')}"
})
else:
# Bei Binärdateien nur einen Hinweis einfügen
message["content"].append({
"type": "text",
"text": f"[Datei: {file_name} (Typ: {mime_type}) ist verfügbar, aber kann nicht direkt angezeigt werden]"
})
except UnicodeDecodeError:
# Bei Decodierungsfehlern nur einen Hinweis einfügen
message["content"].append({
"type": "text",
"text": f"[Datei: {file_name} (Typ: {mime_type}) ist binär und kann nicht direkt angezeigt werden]"
})
logger.info(f"Datei {file_name} zum OpenAI-Nachrichtenobjekt hinzugefügt")
except Exception as e:
logger.error(f"Fehler beim Hinzufügen der Datei {file_name}: {str(e)}")
message["content"].append({
"type": "text",
"text": f"[Fehler beim Laden der Datei {file_name}: {str(e)}]"
})
else:
# Datei nicht gefunden - Hinweis einfügen
message["content"].append({
"type": "text",
"text": f"[Datei {file_name} nicht verfügbar]"
})
# Prüfe, ob wir ein leeres content-Array haben und wandle es in einen String um
if not message["content"]:
message["content"] = prompt_text or ""
elif len(message["content"]) == 1 and message["content"][0]["type"] == "text":
# Wenn nur ein Text-Element vorhanden ist, vereinfachen wir die Struktur
message["content"] = message["content"][0]["text"]
return message
async def close(self):
"""Schließt den HTTP-Client beim Beenden der Anwendung"""
await self.http_client.aclose()
def get_mime_type_from_extension(extension: str) -> str:
"""
Bestimmt den MIME-Typ basierend auf der Dateiendung.
Args:
extension: Die Dateiendung ohne Punkt
Returns:
Der entsprechende MIME-Typ
"""
extension_to_mime = {
"pdf": "application/pdf",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"doc": "application/msword",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"xls": "application/vnd.ms-excel",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"ppt": "application/vnd.ms-powerpoint",
"csv": "text/csv",
"txt": "text/plain",
"json": "application/json",
"xml": "application/xml",
"html": "text/html",
"htm": "text/html",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"png": "image/png",
"gif": "image/gif",
"webp": "image/webp",
"svg": "image/svg+xml"
}
return extension_to_mime.get(extension, "application/octet-stream")