diff --git a/gwserver/auth.py b/gwserver/auth.py index e0238edf..393f0e12 100644 --- a/gwserver/auth.py +++ b/gwserver/auth.py @@ -11,9 +11,9 @@ import configload # Get Config Data config=configload.load_config() -SECRET_KEY = config.get('Access', 'SECRET_KEY') -ALGORITHM = config.get('Access', 'ALGORITHM') -ACCESS_TOKEN_EXPIRE_MINUTES = int(config.get('Access', 'ACCESS_TOKEN_EXPIRE_MINUTES')) +SECRET_KEY = config.get('Auth', 'SECRET_KEY') +ALGORITHM = config.get('Auth', 'ALGORITHM') +ACCESS_TOKEN_EXPIRE_MINUTES = int(config.get('Auth', 'ACCESS_TOKEN_EXPIRE_MINUTES')) # OAuth2 Setup diff --git a/gwserver/config.ini b/gwserver/config.ini index 1f0bcfba..53de56f6 100644 --- a/gwserver/config.ini +++ b/gwserver/config.ini @@ -1,20 +1,31 @@ -[Application] -DEBUG = True -UPLOAD_DIR = ./_uploads -RESULTS_DIR = ./_results - -[Access] +[Auth] SECRET_KEY = dein-geheimer-schlüssel ALGORITHM = HS256 ACCESS_TOKEN_EXPIRE_MINUTES = 300 -[OpenAI] +[Module_AgentserviceInterface] +DEBUG = True +UPLOAD_DIR = ./_uploads +RESULTS_DIR = ./_results +MAX_HISTORY = 50 +AI_PROVIDER = anthropic # Mögliche Werte: "openai" oder "anthropic" + +[Connector_AiOpenai] API_KEY = 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 API_URL = https://api.openai.com/v1/chat/completions MODEL_NAME = gpt-4o +TEMPERATURE = 0.2 +MAX_TOKENS = 2000 -[WebScraping] +[Connector_AiWebscraping] TIMEOUT = 10 MAX_URLS = 3 MAX_CONTENT_LENGTH = 3000 -USER_AGENT = Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 \ No newline at end of file +USER_AGENT = Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 + +[Connector_AiAnthropic] +API_KEY = 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 +API_URL = https://api.anthropic.com/v1/messages +MODEL_NAME = claude-3-opus-20240229 +TEMPERATURE = 0.2 +MAX_TOKENS = 2000 diff --git a/gwserver/connector_aichat_anthropic.py b/gwserver/connector_aichat_anthropic.py new file mode 100644 index 00000000..976ca893 --- /dev/null +++ b/gwserver/connector_aichat_anthropic.py @@ -0,0 +1,273 @@ +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_AiAnthropic', 'API_KEY'), + "api_url": config.get('Connector_AiAnthropic', 'API_URL', fallback="https://api.anthropic.com/v1/messages"), + "model_name": config.get('Connector_AiAnthropic', 'MODEL_NAME', fallback="claude-3-opus-20240229"), + "temperature": float(config.get('Connector_AiAnthropic', 'TEMPERATURE', fallback="0.2")), + "max_tokens": int(config.get('Connector_AiAnthropic', 'MAX_TOKENS', fallback="2000")) + } + +class ChatService: + """ + Connector für die Kommunikation mit der Anthropic 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={ + "x-api-key": self.api_key, + "anthropic-version": "2023-06-01", # Anthropic API Version + "Content-Type": "application/json" + } + ) + + logger.info(f"Anthropic 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 Anthropic 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 umgewandelt ins OpenAI-Format + + Raises: + HTTPException: Bei Fehlern in der API-Kommunikation + """ + try: + # OpenAI-Format in Anthropic-Format umwandeln + formatted_messages = self._convert_to_anthropic_format(messages) + + # 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) + + # Anthropic API Payload erstellen + payload = { + "model": self.model_name, + "messages": formatted_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"Anthropic API-Fehler: {response.status_code} - {response.text}") + raise HTTPException(status_code=500, detail="Fehler bei der Kommunikation mit Anthropic API") + + # Antwort im Anthropic-Format in OpenAI-Format umwandeln + anthropic_response = response.json() + openai_formatted_response = self._convert_to_openai_format(anthropic_response) + + return openai_formatted_response + + except Exception as e: + logger.error(f"Fehler beim Aufruf der Anthropic API: {str(e)}") + raise HTTPException(status_code=500, detail=f"Fehler beim Aufruf der Anthropic API: {str(e)}") + + def _convert_to_anthropic_format(self, openai_messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """ + Konvertiert Nachrichten vom OpenAI-Format ins Anthropic-Format. + + OpenAI verwendet: + [{"role": "system", "content": "..."}, + {"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."}] + + Anthropic verwendet: + [{"role": "user", "content": "..."}, + {"role": "assistant", "content": "..."}] + + Anmerkung: Anthropic hat kein direktes System-Message-Äquivalent, + daher fügen wir System-Nachrichten in die erste User-Nachricht ein. + """ + anthropic_messages = [] + system_content = "" + + # Extrahiere zuerst alle System-Nachrichten + for msg in openai_messages: + if msg.get("role") == "system": + system_content += msg.get("content", "") + "\n\n" + + # Konvertiere die restlichen Nachrichten + for i, msg in enumerate(openai_messages): + role = msg.get("role") + content = msg.get("content", "") + + # System-Nachrichten überspringen (bereits extrahiert) + if role == "system": + continue + + # Für die erste User-Nachricht: System-Inhalte voranstellen, falls vorhanden + if role == "user" and system_content and not any(m.get("role") == "user" for m in anthropic_messages): + if isinstance(content, str): + content = system_content + content + elif isinstance(content, list): + # Wenn content ein Array ist (für Multimodal-Nachrichten) + text_parts = [] + for part in content: + if part.get("type") == "text": + text_parts.append(part) + + if text_parts: + text_parts[0]["text"] = system_content + text_parts[0].get("text", "") + + # Anthropic unterstützt nur "user" und "assistant" als Rollen + if role not in ["user", "assistant"]: + role = "user" + + anthropic_messages.append({"role": role, "content": content}) + + return anthropic_messages + + def _convert_to_openai_format(self, anthropic_response: Dict[str, Any]) -> Dict[str, Any]: + """ + Konvertiert eine Antwort vom Anthropic-Format ins OpenAI-Format. + + Anthropic gibt zurück: + { + "id": "msg_...", + "content": [{"type": "text", "text": "Antworttext"}], + "model": "claude-...", + ... + } + + OpenAI gibt zurück: + { + "id": "chatcmpl-...", + "object": "chat.completion", + "choices": [ + { + "message": { + "role": "assistant", + "content": "Antworttext" + }, + "index": 0, + "finish_reason": "stop" + } + ], + "model": "gpt-...", + ... + } + """ + # Extrahiere Inhalt aus Anthropic-Antwort + content = "" + if "content" in anthropic_response: + if isinstance(anthropic_response["content"], list): + # Inhalt ist eine Liste von Teilen (bei neueren API-Versionen) + for part in anthropic_response["content"]: + if part.get("type") == "text": + content += part.get("text", "") + else: + # Direkter Inhalt als String (bei älteren API-Versionen) + content = anthropic_response["content"] + + # Erstelle OpenAI-formatierte Antwort + return { + "id": anthropic_response.get("id", ""), + "object": "chat.completion", + "created": anthropic_response.get("created", 0), + "model": anthropic_response.get("model", self.model_name), + "choices": [ + { + "message": { + "role": "assistant", + "content": content + }, + "index": 0, + "finish_reason": "stop" + } + ] + } + + 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 Anthropic 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 Anthropic-API formatierte content-Liste + """ + message_content = [ + { + "type": "text", + "text": prompt_text + } + ] + + # Füge Dateien als 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" + + # Content-Type bestimmen (image oder document) + content_type = "image" if mime_type.startswith("image/") else "document" + + # Füge die Datei als Anhang hinzu + message_content.append({ + "type": content_type, + "source": { + "type": "base64", + "media_type": mime_type, + "data": base64_data + } + }) + + logger.info(f"Datei {file_info.get('name', 'Unbekannt')} als {content_type} 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 + + async def close(self): + """Schließt den HTTP-Client beim Beenden der Anwendung""" + await self.http_client.aclose() \ No newline at end of file diff --git a/gwserver/connector_aichat_openai.py b/gwserver/connector_aichat_openai.py new file mode 100644 index 00000000..6aa27337 --- /dev/null +++ b/gwserver/connector_aichat_openai.py @@ -0,0 +1,146 @@ +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', fallback="https://api.openai.com/v1/chat/completions"), + "model_name": config.get('Connector_AiOpenai', 'MODEL_NAME', fallback="gpt-4o"), + "temperature": float(config.get('Connector_AiOpenai', 'TEMPERATURE', fallback="0.2")), + "max_tokens": int(config.get('Connector_AiOpenai', 'MAX_TOKENS', fallback="2000")) + } + +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" + + # Füge die Datei als Anhang hinzu + message_content.append({ + "type": "file", + "source": { + "type": "base64", + "media_type": mime_type, + "data": base64_data + } + }) + + 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 + + async def close(self): + """Schließt den HTTP-Client beim Beenden der Anwendung""" + await self.http_client.aclose() \ No newline at end of file diff --git a/gwserver/connector_aiweb_webscraping.py b/gwserver/connector_aiweb_webscraping.py new file mode 100644 index 00000000..20fdd561 --- /dev/null +++ b/gwserver/connector_aiweb_webscraping.py @@ -0,0 +1,189 @@ +import logging +import re +import requests +from typing import List, Dict, Any, Optional +from bs4 import BeautifulSoup +import json +import os +import configload as configload + + +# Logger konfigurieren +logger = logging.getLogger(__name__) + +# Konfigurationsdaten laden +def load_config_data(): + config = configload.load_config() + return { + "timeout": config.get('Connector_AiWebscraping', 'TIMEOUT'), + "max_urls": config.get('Connector_AiWebscraping', 'MAX_URLS'), + "max_content_length": config.get('Connector_AiWebscraping', 'MAX_CONTENT_LENGTH'), + "user_agent": config.get('Connector_AiWebscraping', 'USER_AGENT') + } + +class WebScrapingService: + """ + Connector für Web-Scraping-Funktionalitäten. + """ + + def __init__(self): + + # Konfiguration laden + self.config = load_config_data() + + logger.info(f"WebScraping Connector initialisiert mit Timeout: {self.timeout}s") + + def scrape_url(self, url: str) -> str: + """ + Scrapt den Inhalt einer URL und extrahiert den relevanten Text. + + Args: + url: Die zu scrapende URL + + Returns: + Der extrahierte Inhalt + + Raises: + Exception: Bei Fehlern im Scraping-Prozess + """ + headers = { + 'User-Agent': self.user_agent + } + + try: + response = requests.get(url, headers=headers, timeout=self.timeout) + response.raise_for_status() + + soup = BeautifulSoup(response.text, 'html.parser') + + # Entferne Skripte, Styles und andere unwichtige Elemente + for script in soup(["script", "style", "meta", "noscript", "iframe"]): + script.extract() + + # Extrahiere den Hauptinhalt + main_content = "" + + # Versuche, Hauptcontainer zu finden (häufige IDs und Klassen) + main_elements = soup.select('main, #main, .main, #content, .content, article, .article, .post, #post') + + if main_elements: + # Nehme den ersten gefundenen Hauptcontainer + main_content = main_elements[0].get_text(separator='\n', strip=True) + else: + # Falls kein Hauptcontainer gefunden, nehme den Body-Text + main_content = soup.body.get_text(separator='\n', strip=True) + + # Bereinige den Text (entferne mehrfache Leerzeilen etc.) + lines = [line.strip() for line in main_content.split('\n') if line.strip()] + main_content = '\n'.join(lines) + + # Begrenze die Länge + if len(main_content) > self.max_content_length: + main_content = main_content[:self.max_content_length] + "...\n[Inhalt gekürzt]" + + return main_content + + except Exception as e: + logger.error(f"Fehler beim Scrapen von {url}: {str(e)}") + raise Exception(f"Fehler beim Scrapen von {url}: {str(e)}") + + def extract_urls(self, text: str) -> List[str]: + """ + Extrahiert URLs aus einem Text. + + Args: + text: Der zu analysierende Text + + Returns: + Liste der gefundenen URLs + """ + # Einfacher URL-Extraktions-Regex + url_pattern = re.compile(r'https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+[/\w\.-]*(?:\?\S+)?') + return url_pattern.findall(text) + + def extract_keywords(self, text: str) -> str: + """ + Extrahiert Schlüsselwörter aus einem Text. + + Args: + text: Der zu analysierende Text + + Returns: + Extrahierte Schlüsselwörter als String + """ + # Einfache Implementierung - in der Praxis könntest du NLP verwenden + words = text.split() + # Filtere kurze Wörter und häufige Stopwörter + stopwords = ["einen", "einer", "eines", "keine", "nicht", "diese", "dieses", "zwischen", + "und", "oder", "aber", "denn", "wenn", "weil", "obwohl", "während", "für", + "mit", "von", "aus", "nach", "bei", "über", "unter", "durch", "gegen"] + keywords = [w for w in words if len(w) > 4 and w.lower() not in stopwords] + return " ".join(keywords[:5]) # Begrenze auf 5 Keywords + + async def search_web(self, query: str) -> str: + """ + Simuliert eine Websuche mit den gegebenen Schlüsselwörtern. + + Args: + query: Suchbegriffe + + Returns: + Ergebnisse der Suche (simuliert) + """ + # HINWEIS: Dies ist eine Simulation! In einer echten Anwendung + # würdest du Google Custom Search API, SerpAPI oder ähnliches verwenden + + # Für eine echte Implementierung: + # - Google Custom Search API: https://developers.google.com/custom-search/v1/overview + # - SerpAPI: https://serpapi.com/ + # - Oder ähnliche Dienste + + return f"Hinweis: Dies ist eine Demo-Implementierung ohne echte Websuche. In der Produktion würde hier der Agent tatsächlich nach '{query}' suchen." + + async def scrape_web_data(self, prompt: str) -> str: + """ + Führt Web-Scraping basierend auf dem Prompt durch + + Args: + prompt: Der Benutzer-Prompt + + Returns: + Gescrapte Webdaten als Text + """ + try: + # Extrahiere mögliche Schlüsselwörter oder URLs aus dem Prompt + keywords = self.extract_keywords(prompt) + urls = self.extract_urls(prompt) + + results = [] + + # Falls direkte URLs im Prompt enthalten sind + if urls: + logger.info(f"Gefundene URLs: {', '.join(urls[:self.max_urls])}") + for url in urls[:self.max_urls]: # Begrenze auf max_urls + try: + logger.info(f"Scrape URL: {url}") + content = self.scrape_url(url) + if content: + results.append(f"## Inhalt von {url}\n{content}") + logger.info(f"Scraping von {url} erfolgreich") + except Exception as e: + logger.error(f"Fehler beim Scrapen von {url}: {e}") + + # Falls keine URLs, versuche Suche mit Schlüsselwörtern + elif keywords: + logger.info(f"Verwende Keywords für Suche: {keywords}") + search_results = await self.search_web(keywords) + if search_results: + results.append(f"## Suchergebnisse für: {keywords}\n{search_results}") + logger.info("Suche abgeschlossen") + + if results: + return "\n\n".join(results) + + logger.warning("Keine relevanten Web-Daten gefunden") + return "Keine relevanten Web-Daten gefunden." + + except Exception as e: + logger.error(f"Fehler beim Web-Scraping: {e}") + return f"Web-Scraping konnte nicht durchgeführt werden: {str(e)}" diff --git a/gwserver/connector_db_json.py b/gwserver/connector_db_json.py index f80076a4..dd0b91d6 100644 --- a/gwserver/connector_db_json.py +++ b/gwserver/connector_db_json.py @@ -43,9 +43,45 @@ class JSONDatabaseConnector: # Cache für geladene Daten self._tables_cache = {} + # System-Tabelle initialisieren + self._system_table_name = "_system" + self._initialize_system_table() + logger.info(f"JSONDatabaseConnector initialisiert für Verzeichnis: {db_folder}") logger.info(f"Kontext: mandate_id={mandate_id}, user_id={user_id}") + def _initialize_system_table(self): + """Initialisiert die System-Tabelle, falls sie noch nicht existiert.""" + system_table_path = self._get_table_path(self._system_table_name) + if not os.path.exists(system_table_path): + empty_system_table = {} + self._save_system_table(empty_system_table) + logger.info(f"System-Tabelle initialisiert in {system_table_path}") + + def _load_system_table(self) -> Dict[str, int]: + """Lädt die System-Tabelle mit den initialen IDs.""" + system_table_path = self._get_table_path(self._system_table_name) + try: + if os.path.exists(system_table_path): + with open(system_table_path, 'r', encoding='utf-8') as f: + return json.load(f) + else: + return {} + except Exception as e: + logger.error(f"Fehler beim Laden der System-Tabelle: {e}") + return {} + + def _save_system_table(self, data: Dict[str, int]) -> bool: + """Speichert die System-Tabelle mit den initialen IDs.""" + system_table_path = self._get_table_path(self._system_table_name) + try: + with open(system_table_path, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + return True + except Exception as e: + logger.error(f"Fehler beim Speichern der System-Tabelle: {e}") + return False + def _get_table_path(self, table: str) -> str: """Gibt den vollständigen Pfad zu einer Tabellendatei zurück""" return os.path.join(self.db_folder, f"{table}.json") @@ -54,6 +90,10 @@ class JSONDatabaseConnector: """Lädt eine Tabelle aus der entsprechenden JSON-Datei""" path = self._get_table_path(table) + # Wenn die Tabelle die System-Tabelle ist, lade sie direkt + if table == self._system_table_name: + return [] # Die System-Tabelle wird nicht wie normale Tabellen behandelt + # Wenn die Tabelle bereits im Cache ist, verwende den Cache if table in self._tables_cache: logger.info(f"Lade Tabelle {table} aus Cache") @@ -66,6 +106,14 @@ class JSONDatabaseConnector: with open(path, 'r', encoding='utf-8') as f: data = json.load(f) self._tables_cache[table] = data + + # Wenn Daten geladen wurden und noch keine initiale ID registriert ist, + # registriere die ID des ersten Datensatzes (falls vorhanden) + if data and not self.has_initial_id(table): + if "id" in data[0]: + self.register_initial_id(table, data[0]["id"]) + logger.info(f"Initiale ID {data[0]['id']} für Tabelle {table} nachträglich registriert") + return data else: # Wenn die Datei nicht existiert, erstelle eine leere Tabelle @@ -79,6 +127,10 @@ class JSONDatabaseConnector: def _save_table(self, table: str, data: List[Dict[str, Any]]) -> bool: """Speichert eine Tabelle in der entsprechenden JSON-Datei""" + # Die System-Tabelle wird speziell behandelt + if table == self._system_table_name: + return False + path = self._get_table_path(table) try: with open(path, 'w', encoding='utf-8') as f: @@ -167,7 +219,7 @@ class JSONDatabaseConnector: try: for filename in os.listdir(self.db_folder): - if filename.endswith('.json'): + if filename.endswith('.json') and not filename.startswith('_'): table_name = filename[:-5] # Entferne die .json-Endung tables.append(table_name) except Exception as e: @@ -293,6 +345,18 @@ class JSONDatabaseConnector: if "user_id" not in record_data: record_data["user_id"] = self.user_id + # Bestimme die nächste ID, falls nicht vorhanden + if "id" not in record_data: + next_id = 1 + if data: + next_id = max(record["id"] for record in data if "id" in record) + 1 + record_data["id"] = next_id + + # Wenn die Tabelle leer ist und eine System-ID registriert werden soll + if not data: + self.register_initial_id(table, record_data["id"]) + logger.info(f"Initiale ID {record_data['id']} für Tabelle {table} registriert") + # Füge den neuen Datensatz hinzu data.append(record_data) @@ -316,6 +380,12 @@ class JSONDatabaseConnector: # Lade die Tabellendaten data = self._load_table(table) + # Prüfe, ob es sich um die initiale ID handelt + initial_id = self.get_initial_id(table) + if initial_id is not None and initial_id == record_id: + logger.warning(f"Versuch, den initialen Datensatz mit ID {record_id} aus Tabelle {table} zu löschen, wurde verhindert") + return False + # Suche den Datensatz for i, record in enumerate(data): if "id" in record and record["id"] == record_id: @@ -354,6 +424,11 @@ class JSONDatabaseConnector: if "mandate_id" in record and record["mandate_id"] != self.mandate_id: raise ValueError("Not your mandate") + # Verhindere Änderung der ID bei initialem Datensatz + initial_id = self.get_initial_id(table) + if initial_id is not None and initial_id == record_id and "id" in record_data and record_data["id"] != record_id: + raise ValueError(f"Die ID des initialen Datensatzes in Tabelle {table} kann nicht geändert werden") + # Aktualisiere den Datensatz for key, value in record_data.items(): data[i][key] = value @@ -365,4 +440,72 @@ class JSONDatabaseConnector: raise ValueError(f"Fehler beim Aktualisieren des Datensatzes in Tabelle {table}") # Datensatz nicht gefunden - raise ValueError(f"Datensatz mit ID {record_id} nicht gefunden in Tabelle {table}") \ No newline at end of file + raise ValueError(f"Datensatz mit ID {record_id} nicht gefunden in Tabelle {table}") + + # System-Tabellen-Funktionen + + def register_initial_id(self, table: str, initial_id: int) -> bool: + """ + Registriert die initiale ID für eine Tabelle. + + Args: + table: Name der Tabelle + initial_id: Die initiale ID + + Returns: + True bei Erfolg, False bei Fehler + """ + try: + # Lade die aktuelle System-Tabelle + system_data = self._load_system_table() + + # Nur registrieren, wenn noch nicht vorhanden + if table not in system_data: + system_data[table] = initial_id + success = self._save_system_table(system_data) + if success: + logger.info(f"Initiale ID {initial_id} für Tabelle {table} registriert") + return success + return True # Wenn bereits vorhanden, ist das kein Fehler + except Exception as e: + logger.error(f"Fehler beim Registrieren der initialen ID für Tabelle {table}: {e}") + return False + + def get_initial_id(self, table: str) -> Optional[int]: + """ + Gibt die initiale ID für eine Tabelle zurück. + + Args: + table: Name der Tabelle + + Returns: + Die initiale ID oder None, wenn nicht vorhanden + """ + system_data = self._load_system_table() + initial_id = system_data.get(table) + if initial_id is None: + logger.debug(f"Keine initiale ID für Tabelle {table} gefunden") + return initial_id + + def has_initial_id(self, table: str) -> bool: + """ + Prüft, ob eine initiale ID für eine Tabelle registriert ist. + + Args: + table: Name der Tabelle + + Returns: + True, wenn eine initiale ID registriert ist, sonst False + """ + system_data = self._load_system_table() + return table in system_data + + def get_all_initial_ids(self) -> Dict[str, int]: + """ + Gibt alle registrierten initialen IDs zurück. + + Returns: + Dictionary mit Tabellennamen als Schlüssel und initialen IDs als Werte + """ + system_data = self._load_system_table() + return system_data.copy() # Kopie zurückgeben, um das Original zu schützen \ No newline at end of file diff --git a/gwserver/connector_db_mysql.py b/gwserver/connector_db_mysql.py index c942bfc0..3a5e7b2e 100644 --- a/gwserver/connector_db_mysql.py +++ b/gwserver/connector_db_mysql.py @@ -43,6 +43,10 @@ class MySQLDatabaseConnector: # Stelle Verbindung zur Datenbank her self.connection = self._create_connection() + # System-Tabelle initialisieren + self._system_table_name = "_system" + self._initialize_system_table() + logger.info(f"MySQLDatabaseConnector initialisiert für Datenbank: {db_name}") logger.info(f"Kontext: mandate_id={mandate_id}, user_id={user_id}") @@ -62,6 +66,39 @@ class MySQLDatabaseConnector: logger.error(f"Fehler bei der Verbindung zu MySQL: {e}") raise + def _initialize_system_table(self): + """Initialisiert die System-Tabelle, falls sie noch nicht existiert.""" + cursor = None + try: + cursor = self.connection.cursor() + + # Prüfe, ob die System-Tabelle existiert + cursor.execute(f""" + SELECT COUNT(*) + FROM information_schema.tables + WHERE table_schema = '{self.db_name}' + AND table_name = '{self._system_table_name}' + """) + + if cursor.fetchone()[0] == 0: + # Erstelle die System-Tabelle + cursor.execute(f""" + CREATE TABLE {self._system_table_name} ( + table_name VARCHAR(255) PRIMARY KEY, + initial_id INT NOT NULL + ) + """) + self.connection.commit() + logger.info(f"System-Tabelle '{self._system_table_name}' erstellt") + except Error as e: + logger.error(f"Fehler beim Initialisieren der System-Tabelle: {e}") + if self.connection.is_connected(): + self.connection.rollback() + raise + finally: + if cursor and cursor.is_connected(): + cursor.close() + def _execute_query(self, query: str, params: tuple = None): """Führt eine SQL-Abfrage aus""" cursor = None @@ -175,6 +212,7 @@ class MySQLDatabaseConnector: SELECT table_name FROM information_schema.tables WHERE table_schema = %s + AND table_name NOT LIKE '\_%' """ try: @@ -331,9 +369,21 @@ class MySQLDatabaseConnector: """ try: + # Prüfe zuerst, ob die Tabelle leer ist + check_query = f""" + SELECT COUNT(*) as count FROM {table} + """ + count_result = self._execute_select(check_query) + is_empty = count_result[0]["count"] == 0 + # Führe die Abfrage aus und erhalte die ID des neuen Datensatzes new_id = self._execute_insert(query, values) + # Wenn die Tabelle vorher leer war, registriere die neue ID als initiale ID + if is_empty and new_id: + self.register_initial_id(table, new_id) + logger.info(f"Initiale ID {new_id} für Tabelle {table} registriert") + # Füge die ID zum Datensatz hinzu, falls eine zurückgegeben wurde if new_id: record_data["id"] = new_id @@ -354,6 +404,12 @@ class MySQLDatabaseConnector: Returns: True bei Erfolg, False bei Fehler """ + # Prüfe, ob es sich um die initiale ID handelt + initial_id = self.get_initial_id(table) + if initial_id is not None and initial_id == record_id: + logger.warning(f"Versuch, den initialen Datensatz mit ID {record_id} aus Tabelle {table} zu löschen, wurde verhindert") + return False + # Prüfe zuerst, ob der Datensatz zum aktuellen Mandanten gehört check_query = f""" SELECT mandate_id FROM {table} WHERE id = %s @@ -393,6 +449,11 @@ class MySQLDatabaseConnector: Returns: Der aktualisierte Datensatz """ + # Prüfe, ob es sich um die initiale ID handelt und die ID geändert werden soll + initial_id = self.get_initial_id(table) + if initial_id is not None and initial_id == record_id and "id" in record_data and record_data["id"] != record_id: + raise ValueError(f"Die ID des initialen Datensatzes in Tabelle {table} kann nicht geändert werden") + # Prüfe zuerst, ob der Datensatz zum aktuellen Mandanten gehört check_query = f""" SELECT mandate_id FROM {table} WHERE id = %s @@ -447,6 +508,125 @@ class MySQLDatabaseConnector: logger.error(f"Fehler beim Aktualisieren des Datensatzes in Tabelle {table}: {e}") raise + # System-Tabellen-Funktionen + + def register_initial_id(self, table: str, initial_id: int) -> bool: + """ + Registriert die initiale ID für eine Tabelle. + + Args: + table: Name der Tabelle + initial_id: Die initiale ID + + Returns: + True bei Erfolg, False bei Fehler + """ + try: + # Prüfe zuerst, ob bereits eine initiale ID für diese Tabelle registriert ist + check_query = f""" + SELECT COUNT(*) as count + FROM {self._system_table_name} + WHERE table_name = %s + """ + + result = self._execute_select(check_query, (table,)) + + if result and result[0]["count"] > 0: + # Bereits registriert + return True + + # Registriere die initiale ID + insert_query = f""" + INSERT INTO {self._system_table_name} (table_name, initial_id) + VALUES (%s, %s) + """ + + self._execute_insert(insert_query, (table, initial_id)) + logger.info(f"Initiale ID {initial_id} für Tabelle {table} registriert") + + return True + except Exception as e: + logger.error(f"Fehler beim Registrieren der initialen ID für Tabelle {table}: {e}") + return False + + def get_initial_id(self, table: str) -> Optional[int]: + """ + Gibt die initiale ID für eine Tabelle zurück. + + Args: + table: Name der Tabelle + + Returns: + Die initiale ID oder None, wenn nicht vorhanden + """ + try: + query = f""" + SELECT initial_id + FROM {self._system_table_name} + WHERE table_name = %s + """ + + result = self._execute_select(query, (table,)) + + if result and len(result) > 0: + return result[0]["initial_id"] + + return None + except Exception as e: + logger.error(f"Fehler beim Abrufen der initialen ID für Tabelle {table}: {e}") + return None + + def has_initial_id(self, table: str) -> bool: + """ + Prüft, ob eine initiale ID für eine Tabelle registriert ist. + + Args: + table: Name der Tabelle + + Returns: + True, wenn eine initiale ID registriert ist, sonst False + """ + try: + query = f""" + SELECT COUNT(*) as count + FROM {self._system_table_name} + WHERE table_name = %s + """ + + result = self._execute_select(query, (table,)) + + if result and len(result) > 0: + return result[0]["count"] > 0 + + return False + except Exception as e: + logger.error(f"Fehler beim Prüfen der initialen ID für Tabelle {table}: {e}") + return False + + def get_all_initial_ids(self) -> Dict[str, int]: + """ + Gibt alle registrierten initialen IDs zurück. + + Returns: + Dictionary mit Tabellennamen als Schlüssel und initialen IDs als Werte + """ + try: + query = f""" + SELECT table_name, initial_id + FROM {self._system_table_name} + """ + + result = self._execute_select(query) + + initial_ids = {} + for row in result: + initial_ids[row["table_name"]] = row["initial_id"] + + return initial_ids + except Exception as e: + logger.error(f"Fehler beim Abrufen aller initialen IDs: {e}") + return {} + def close(self): """Schließt die Datenbankverbindung""" if hasattr(self, 'connection') and self.connection.is_connected(): diff --git a/gwserver/modules/agentservice_interface.py b/gwserver/modules/agentservice_interface.py index 764a9438..88b2dc5c 100644 --- a/gwserver/modules/agentservice_interface.py +++ b/gwserver/modules/agentservice_interface.py @@ -3,39 +3,27 @@ import uuid import os import json import logging -import re +import base64 +import mimetypes from typing import List, Dict, Any, Optional from datetime import datetime -import httpx from fastapi import HTTPException -import requests -from bs4 import BeautifulSoup import configload as configload - # Logger konfigurieren logger = logging.getLogger(__name__) # Konfigurationsdaten laden def load_config_data(): - config=configload.load_config() + config = configload.load_config() result = { - "openai": { - "api_key": config.get('OpenAI', 'API_KEY'), - "api_url": config.get('OpenAI', 'API_URL'), - "model_name": config.get('OpenAI', 'MODEL_NAME') - }, "application": { - "debug": config.get('Application', 'DEBUG'), - "upload_dir": config.get('Application', 'UPLOAD_DIR'), - "results_dir": config.get('Application', 'RESULTS_DIR') - }, - "webscraping": { - "timeout": config.get('WebScraping', 'TIMEOUT'), - "max_urls": config.get('WebScraping', 'MAX_URLS'), - "max_content_length": config.get('WebScraping', 'MAX_CONTENT_LENGTH'), - "user_agent": config.get('WebScraping', 'USER_AGENT') + "debug": config.get('Module_AgentserviceInterface', 'DEBUG'), + "upload_dir": config.get('Module_AgentserviceInterface', 'UPLOAD_DIR'), + "results_dir": config.get('Module_AgentserviceInterface', 'RESULTS_DIR'), + "max_history": config.get('Module_AgentserviceInterface', 'MAX_HISTORY'), + "ai_provider": config.get('Module_AgentserviceInterface', 'AI_PROVIDER', fallback="openai") # Standard ist OpenAI } } # Debug-Modus einstellen @@ -48,7 +36,7 @@ def load_config_data(): class AgentService: """ - Service für die Verwaltung und Ausführung von Multi-Agent-Workflows mit OpenAI GPT-4o. + Service für die Verwaltung und Ausführung von Multi-Agent-Workflows mit verschiedenen Modellen. """ def __init__(self, mandate_id: int = None, user_id: int = None): @@ -69,33 +57,35 @@ class AgentService: # Verzeichnisse aus der Konfiguration übernehmen self.results_dir = self.config["application"]["results_dir"] self.upload_dir = self.config["application"]["upload_dir"] + self.max_history = int(self.config["application"]["max_history"]) - # Web-Scraping-Konfiguration - self.scraping_timeout = int(self.config["webscraping"]["timeout"]) - self.scraping_max_urls = int(self.config["webscraping"]["max_urls"]) - self.scraping_max_content_length = int(self.config["webscraping"]["max_content_length"]) - self.scraping_user_agent = self.config["webscraping"]["user_agent"] + # AI Provider aus der Konfiguration übernehmen + self.ai_provider = self.config["application"]["ai_provider"].lower() # Verzeichnisse erstellen os.makedirs(self.results_dir, exist_ok=True) os.makedirs(self.upload_dir, exist_ok=True) + # Connector-Instanzen initialisieren + if self.ai_provider == "anthropic": + import connector_aichat_anthropic as service_aichat + self.service_aichat = service_aichat.ChatService() + logger.info("Anthropic AI Provider wird verwendet") + else: + import connector_aichat_openai as service_aichat + self.service_aichat = service_aichat.ChatService() + logger.info("OpenAI AI Provider wird verwendet") + + import connector_aiweb_webscraping as service_aiscrap + self.service_aiscrap = service_aiscrap.WebScrapingService() + logger.info(f"AgentService initialisiert mit:") - logger.info(f" - Modell: {self.config['openai']['model_name']}") + logger.info(f" - AI Provider: {self.ai_provider}") logger.info(f" - Ergebnisverzeichnis: {self.results_dir}") logger.info(f" - Upload-Verzeichnis: {self.upload_dir}") # Workflow-Speicher self.workflows = {} - - # 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.config['openai']['api_key']}", - "Content-Type": "application/json" - } - ) async def execute_workflow( self, @@ -106,7 +96,9 @@ class AgentService: ) -> str: """ Führt einen Workflow mit den angegebenen Agenten und Dateien aus. - Verwendet OpenAI GPT-4o für die Verarbeitung der Anfragen. + + Anstatt die Agenten der Reihe nach abzuarbeiten, wird ein AI-Moderator verwendet, + der die Agenten basierend auf ihren Fähigkeiten und bisherigen Antworten steuert. """ logger.info(f"Starte Workflow {workflow_id} mit {len(agents)} Agenten und {len(files)} Dateien") @@ -243,99 +235,292 @@ class AgentService: self.workflows[workflow_id]["progress"] = 0.1 - # Verarbeitung pro Agent ausführen - for i, agent in enumerate(agents): + # Initialisiere den Chatverlauf für den Agenten-Dialog + chat_history = [] + + # Erstelle das Nachrichtenobjekt für die initialen Dateien und den Prompt + message_content = self.service_aichat.prepare_file_message_content(prompt, file_contexts) + + # Füge Dateien als Base64-Anhänge hinzu + for file in file_contexts: + 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" + + # Füge die Datei als Anhang hinzu + message_content.append({ + "type": "file", + "source": { + "type": "base64", + "media_type": mime_type, + "data": base64_data + } + }) + except Exception as e: + logger.error(f"Fehler beim Hinzufügen der Datei {file['name']} als Anhang: {str(e)}") + + # Nachrichtenobjekt erstellen + initial_message = { + "role": "user", + "content": message_content + } + + # Initialen Prompt zum Chatverlauf hinzufügen + chat_history.append(initial_message) + + # Initialisiere die verfügbaren Agenten mit ihren Fähigkeiten + available_agents = {} + for agent in agents: agent_id = agent["id"] agent_name = agent["name"] agent_type = agent["type"] + agent_capabilities = agent.get("capabilities", "") - self.workflows[workflow_id]["agent_statuses"][agent_id] = "running" - self._add_log( - workflow_id, - f"Agent '{agent_name}' beginnt mit der Verarbeitung...", - "start", - agent_id, - agent_name - ) + available_agents[agent_id] = { + "id": agent_id, + "name": agent_name, + "type": agent_type, + "capabilities": agent_capabilities, + "used": False + } - # Fortschritt aktualisieren - self.workflows[workflow_id]["progress"] = 0.1 + (i + 1) * (0.8 / len(agents)) * 0.5 + # Initialisiere den Status + self.workflows[workflow_id]["agent_statuses"][agent_id] = "pending" + + # Initialisiere die Moderator-Rolle - Fester Teil + moderator_prompt_base = """ + Du bist der Moderator eines Multi-Agent-Systems. Deine Aufgabe ist es, die Zusammenarbeit zwischen verschiedenen spezialisierten Agenten zu koordinieren, um die Anfrage des Benutzers bestmöglich zu erfüllen. + + Du sollst: + 1. Die Anfrage des Benutzers verstehen und analysieren + 2. Den am besten geeigneten Agenten basierend auf seinen Fähigkeiten auswählen + 3. Die Antworten der Agenten überwachen und bewerten + 4. Falls nötig, weitere Agenten hinzuziehen, um die Anfrage vollständig zu bearbeiten + 5. Den Workflow beenden, wenn die Anfrage vollständig erfüllt wurde + + Für jeden Schritt sollst du begründen, warum du einen bestimmten Agenten auswählst, und zusammenfassen, was bisher erreicht wurde. + """ + + # Dynamischer Teil - Verfügbare Agenten aus den tatsächlich vorhandenen Agenten + agents_description = "Verfügbare Agenten:\n" + for agent_id, agent in available_agents.items(): + agents_description += f"- {agent['name']} (Typ: {agent['type']}): {agent['capabilities']}\n" + + moderator_prompt_end = """ + Beende den Workflow, wenn die Aufgabe erfüllt ist oder keine weiteren Agenten zur Bearbeitung beitragen können. + """ + + # Kombiniere alle Teile + moderator_system_prompt = moderator_prompt_base + "\n" + agents_description + "\n" + moderator_prompt_end + + # Starte den Workflow mit dem Moderator + self._add_log(workflow_id, "Starte Agenten-Tischrunde mit Moderator", "info") + + # Maximale Anzahl der Runden zur Vermeidung endloser Schleifen + max_rounds = 12 + current_round = 0 + workflow_complete = False + + while current_round < max_rounds and not workflow_complete: + current_round += 1 + self._add_log(workflow_id, f"Starte Runde {current_round}", "info") + # Der Moderator wählt den nächsten Agenten aus + moderator_prompt = { + "role": "system", + "content": moderator_system_prompt + } + + # Kopie des Chatverlaufs für den Moderator erstellen + moderator_chat = [moderator_prompt] + chat_history[-self.max_history:] + + # Füge eine Zusammenfassung der verfügbaren Agenten hinzu + agent_info = "Verfügbare Agenten:\n" + for agent_id, agent in available_agents.items(): + status = "✓ Bereits verwendet" if agent["used"] else "✗ Noch nicht verwendet" + agent_info += f"- {agent['name']} (Typ: {agent['type']}): {agent['capabilities']}\n Status: {status}\n" + + moderator_chat.append({ + "role": "system", + "content": agent_info + "\nWähle den nächsten Agenten aus oder beende den Workflow, wenn die Aufgabe erfüllt ist." + }) + + # Moderator trifft die Entscheidung try: - # Agent-spezifische Anweisungen erstellen - agent_instructions = self._get_agent_instructions(agent_type) + moderator_decision = await self.service_aichat.call_api(moderator_chat) + moderator_text = moderator_decision["choices"][0]["message"]["content"] - # Vollständige Anfrage an OpenAI erstellen - full_prompt = f""" - # Aufgabe - {prompt} + # Füge die Entscheidung des Moderators zum Chatverlauf hinzu + chat_history.append({ + "role": "assistant", + "content": f"[Moderator] {moderator_text}" + }) - # Dateikontexte - {file_context_text} + # Log der Moderator-Entscheidung + self._add_log(workflow_id, f"Moderator-Entscheidung: {moderator_text[:100]}...", "info") - # Agent-Rolle - Du bist ein {agent_name} ({agent_type}). + # Prüfe, ob der Workflow beendet werden soll + if any(phrase in moderator_text.lower() for phrase in ["workflow beenden", "aufgabe erfüllt", "beende den workflow", "workflow abschließen"]): + self._add_log(workflow_id, "Moderator hat den Workflow beendet", "success") + workflow_complete = True + break - {agent_instructions} + # Extrahiere den ausgewählten Agenten + selected_agent_id = None - Bitte analysiere die Daten und beantworte die Anfrage gemäß deiner Rolle. - """ + # Versuche, den ausgewählten Agenten aus dem Text zu extrahieren + for agent_id, agent in available_agents.items(): + if agent["name"] in moderator_text or f"Agent {agent_id}" in moderator_text: + selected_agent_id = agent_id + break - # Für Web-Scraper: Führe Web-Scraping durch - if agent_type == "scraper": - self._add_log(workflow_id, "Führe Web-Scraping durch...", "info", agent_id, agent_name) - web_data = await self._scrape_web_data(workflow_id, prompt) - if web_data: - full_prompt += f"\n\n# Gescrapte Web-Daten\n{web_data}" - self._add_log(workflow_id, "Web-Scraping abgeschlossen", "info", agent_id, agent_name) + if not selected_agent_id: + self._add_log(workflow_id, "Moderator konnte keinen Agenten identifizieren", "warning") + # Wähle den ersten nicht verwendeten Agenten + for agent_id, agent in available_agents.items(): + if not agent["used"]: + selected_agent_id = agent_id + break + + # Wenn alle Agenten bereits verwendet wurden, wähle den Initialisierungs-Agenten + if not selected_agent_id: + for agent_id, agent in available_agents.items(): + if agent["type"] == "initialisierung": + selected_agent_id = agent_id + break + + # Als letztes Mittel wähle einfach den ersten Agenten + if not selected_agent_id and available_agents: + selected_agent_id = list(available_agents.keys())[0] - # API-Anfrage an OpenAI senden - response = await self._call_openai_api(full_prompt) + if selected_agent_id: + # Agenten aus der Liste markieren + selected_agent = available_agents[selected_agent_id] + selected_agent["used"] = True + + # Agenten-Status aktualisieren + self.workflows[workflow_id]["agent_statuses"][selected_agent_id] = "running" + self._add_log( + workflow_id, + f"Agent '{selected_agent['name']}' beginnt mit der Verarbeitung...", + "start", + selected_agent_id, + selected_agent['name'] + ) + + # Agent-spezifische Anweisungen erstellen + agent_instructions = self._get_agent_instructions(selected_agent["type"]) + + # Agent-Prompt erstellen + agent_prompt = { + "role": "system", + "content": f""" + # Aufgabe + Du bist ein spezialisierter Agent vom Typ {selected_agent['type']} mit dem Namen {selected_agent['name']}. + + {agent_instructions} + + Bitte analysiere den Chatverlauf und die Dateien und beantworte die Anfrage gemäß deiner Rolle. + + Ausgabeformat: + [Agent: {selected_agent['name']}] + Deine Antwort... + """ + } + + # Kopie des Chatverlaufs für den Agenten erstellen + agent_chat = [agent_prompt] + chat_history[-self.max_history:] + + # Falls der Agent ein Webscraper ist und Scraping notwendig ist + if selected_agent["type"] == "scraper": + self._add_log(workflow_id, "Führe Web-Scraping durch...", "info", selected_agent_id, selected_agent["name"]) + web_data = await self.service_aiscrap.scrape_web_data(prompt) + if web_data: + agent_chat.append({ + "role": "system", + "content": f"# Gescrapte Web-Daten\n{web_data}" + }) + self._add_log(workflow_id, "Web-Scraping abgeschlossen", "info", selected_agent_id, selected_agent["name"]) + + # Agent führt seinen Teil aus + try: + agent_response = await self.service_aichat.call_api(agent_chat) + agent_text = agent_response["choices"][0]["message"]["content"] + + # Füge die Antwort des Agenten zum Chatverlauf hinzu + chat_history.append({ + "role": "assistant", + "content": agent_text + }) + + # Agent-Ergebnis erstellen + result = self._create_agent_result( + workflow_id, + selected_agent, + len(self.workflows[workflow_id]["results"]), + prompt, + file_contexts, + agent_text + ) + self.workflows[workflow_id]["results"].append(result) + + self._add_log( + workflow_id, + f"Agent '{selected_agent['name']}' hat die Verarbeitung abgeschlossen", + "complete", + selected_agent_id, + selected_agent["name"] + ) + + # Agenten-Status aktualisieren + self.workflows[workflow_id]["agent_statuses"][selected_agent_id] = "completed" + + except Exception as e: + logger.error(f"Fehler bei der Ausführung von Agent '{selected_agent['name']}': {str(e)}") + self._add_log( + workflow_id, + f"Fehler bei der Ausführung: {str(e)}", + "error", + selected_agent_id, + selected_agent["name"] + ) + self.workflows[workflow_id]["agent_statuses"][selected_agent_id] = "failed" + + # Füge die Fehlermeldung zum Chatverlauf hinzu + chat_history.append({ + "role": "assistant", + "content": f"[Fehler bei Agent '{selected_agent['name']}']: {str(e)}" + }) + else: + self._add_log(workflow_id, "Kein Agent ausgewählt. Beende Workflow.", "warning") + workflow_complete = True - # Ergebnis extrahieren und speichern - result_content = response.get("choices", [{}])[0].get("message", {}).get("content", "Keine Antwort erhalten") - - # Agent-Ergebnis erstellen - result = self._create_agent_result(workflow_id, agent, i, prompt, file_contexts, result_content) - self.workflows[workflow_id]["results"].append(result) - - self._add_log( - workflow_id, - f"Agent '{agent_name}' hat die Verarbeitung abgeschlossen", - "complete", - agent_id, - agent_name - ) + # Fortschritt aktualisieren + self.workflows[workflow_id]["progress"] = min(0.9, 0.1 + (current_round / max_rounds) * 0.8) except Exception as e: - logger.error(f"Fehler bei der Ausführung von Agent '{agent_name}': {str(e)}") - self._add_log( - workflow_id, - f"Fehler bei der Ausführung: {str(e)}", - "error", - agent_id, - agent_name - ) - self.workflows[workflow_id]["agent_statuses"][agent_id] = "failed" - continue - - self.workflows[workflow_id]["agent_statuses"][agent_id] = "completed" - self.workflows[workflow_id]["progress"] = 0.1 + (i + 1) * (0.8 / len(agents)) - - # Kurze Pause zwischen Agent-Aufrufen - await asyncio.sleep(1) + logger.error(f"Fehler in der Moderator-Phase: {str(e)}") + self._add_log(workflow_id, f"Fehler in der Moderator-Phase: {str(e)}", "error") + break - # Workflow-Status prüfen - failed_agents = [a for a, s in self.workflows[workflow_id]["agent_statuses"].items() if s == "failed"] - - if failed_agents: - self._add_log(workflow_id, f"{len(failed_agents)} Agent(s) sind fehlgeschlagen", "error") - self._add_log(workflow_id, "Workflow mit Fehlern beendet", "error") - self.workflows[workflow_id]["status"] = "failed" - else: - self._add_log(workflow_id, "Alle Agenten haben ihre Aufgaben abgeschlossen", "info") - self._add_log(workflow_id, "Workflow erfolgreich beendet", "success") + # Workflow abschließen + if workflow_complete: self.workflows[workflow_id]["status"] = "completed" + self._add_log(workflow_id, "Workflow erfolgreich beendet", "success") + elif current_round >= max_rounds: + self.workflows[workflow_id]["status"] = "completed" + self._add_log(workflow_id, f"Workflow nach {max_rounds} Runden automatisch beendet", "info") + else: + self.workflows[workflow_id]["status"] = "failed" + self._add_log(workflow_id, "Workflow mit Fehlern beendet", "error") self.workflows[workflow_id]["progress"] = 1.0 self.workflows[workflow_id]["completed_at"] = datetime.now().isoformat() @@ -343,401 +528,4 @@ class AgentService: # Speichere Ergebnisse in Datei für spätere Verwendung self._save_workflow_results(workflow_id) - return workflow_id - - # Die übrigen Methoden bleiben unverändert - # ... - - async def _scrape_web_data(self, workflow_id: str, prompt: str) -> str: - """ - Führt Web-Scraping basierend auf dem Prompt durch - """ - try: - # Extrahiere mögliche Schlüsselwörter oder URLs aus dem Prompt - keywords = self._extract_keywords(prompt) - urls = self._extract_urls(prompt) - - results = [] - - # Falls direkte URLs im Prompt enthalten sind - if urls: - self._add_log(workflow_id, f"Gefundene URLs: {', '.join(urls[:self.scraping_max_urls])}", "info") - for url in urls[:self.scraping_max_urls]: # Begrenze auf max_urls (Standard: 3) - try: - self._add_log(workflow_id, f"Scrape URL: {url}", "info") - content = self._scrape_url(url) - if content: - results.append(f"## Inhalt von {url}\n{content}") - self._add_log(workflow_id, f"Scraping von {url} erfolgreich", "info") - except Exception as e: - logger.error(f"Fehler beim Scrapen von {url}: {e}") - self._add_log(workflow_id, f"Fehler beim Scrapen von {url}: {e}", "error") - - # Falls keine URLs, versuche Suche mit Schlüsselwörtern - elif keywords: - self._add_log(workflow_id, f"Verwende Keywords für Suche: {keywords}", "info") - search_results = self._search_web(keywords) - if search_results: - results.append(f"## Suchergebnisse für: {keywords}\n{search_results}") - self._add_log(workflow_id, "Suche abgeschlossen", "info") - - if results: - return "\n\n".join(results) - - self._add_log(workflow_id, "Keine relevanten Web-Daten gefunden", "warning") - return "Keine relevanten Web-Daten gefunden." - - except Exception as e: - logger.error(f"Fehler beim Web-Scraping: {e}") - self._add_log(workflow_id, f"Fehler beim Web-Scraping: {e}", "error") - return f"Web-Scraping konnte nicht durchgeführt werden: {str(e)}" - - def _extract_keywords(self, text: str) -> str: - """Extrahiert Schlüsselwörter aus dem Text""" - # Einfache Implementierung - in der Praxis könntest du NLP verwenden - words = text.split() - # Filtere kurze Wörter und häufige Stopwörter - stopwords = ["einen", "einer", "eines", "keine", "nicht", "diese", "dieses", "zwischen", - "und", "oder", "aber", "denn", "wenn", "weil", "obwohl", "während", "für", - "mit", "von", "aus", "nach", "bei", "über", "unter", "durch", "gegen"] - keywords = [w for w in words if len(w) > 4 and w.lower() not in stopwords] - return " ".join(keywords[:5]) # Begrenze auf 5 Keywords - - def _extract_urls(self, text: str) -> List[str]: - """Extrahiert URLs aus dem Text""" - # Einfacher URL-Extraktions-Regex - url_pattern = re.compile(r'https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+[/\w\.-]*(?:\?\S+)?') - return url_pattern.findall(text) - - def _scrape_url(self, url: str) -> str: - """Scrapt den Inhalt einer URL""" - headers = { - 'User-Agent': self.scraping_user_agent - } - - response = requests.get(url, headers=headers, timeout=self.scraping_timeout) - response.raise_for_status() - - soup = BeautifulSoup(response.text, 'html.parser') - - # Entferne Skripte, Styles und andere unwichtige Elemente - for script in soup(["script", "style", "meta", "noscript", "iframe"]): - script.extract() - - # Extrahiere den Hauptinhalt - main_content = "" - - # Versuche, Hauptcontainer zu finden (häufige IDs und Klassen) - main_elements = soup.select('main, #main, .main, #content, .content, article, .article, .post, #post') - - if main_elements: - # Nehme den ersten gefundenen Hauptcontainer - main_content = main_elements[0].get_text(separator='\n', strip=True) - else: - # Falls kein Hauptcontainer gefunden, nehme den Body-Text - main_content = soup.body.get_text(separator='\n', strip=True) - - # Bereinige den Text (entferne mehrfache Leerzeilen etc.) - lines = [line.strip() for line in main_content.split('\n') if line.strip()] - main_content = '\n'.join(lines) - - # Begrenze die Länge - if len(main_content) > self.scraping_max_content_length: - main_content = main_content[:self.scraping_max_content_length] + "...\n[Inhalt gekürzt]" - - return main_content - - def _search_web(self, query: str) -> str: - """ - Simuliert eine Websuche (in einer vollständigen Implementierung - würdest du hier eine echte Suchmaschinen-API verwenden) - """ - # HINWEIS: Dies ist eine Simulation! In einer echten Anwendung - # würdest du Google Custom Search API, SerpAPI oder ähnliches verwenden - - # Für eine echte Implementierung: - # - Google Custom Search API: https://developers.google.com/custom-search/v1/overview - # - SerpAPI: https://serpapi.com/ - # - Oder ähnliche Dienste - - return f"Hinweis: Dies ist eine Demo-Implementierung ohne echte Websuche. In der Produktion würde hier der Agent tatsächlich nach '{query}' suchen." - - async def _call_openai_api(self, prompt: str) -> Dict[str, Any]: - """Ruft die OpenAI API auf und gibt die Antwort zurück""" - try: - payload = { - "model": self.config["openai"]["model_name"], - "messages": [ - {"role": "system", "content": "Du bist ein spezialisierter Agent in einem Multi-Agent-System zur Datenanalyse und -verarbeitung."}, - {"role": "user", "content": prompt} - ], - "temperature": 0.2, # Niedrige Temperatur für konsistentere Ergebnisse - "max_tokens": 2000 - } - - response = await self.http_client.post( - self.config["openai"]["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 _get_agent_instructions(self, agent_type: str) -> str: - """ - Gibt agententypspezifische Anweisungen zurück, die aus der agents.json geladen werden. - Falls die Datei nicht existiert oder der Agententyp nicht gefunden wird, - werden Standard-Anweisungen zurückgegeben. - """ - try: - # Pfad zur agents.json-Datei - agents_file = os.path.join(os.path.dirname(__file__), 'data', 'agents.json') - - # Überprüfen, ob die Datei existiert - if not os.path.exists(agents_file): - logger.warning(f"Agents-Definitionen nicht gefunden: {agents_file}") - return self._get_default_agent_instructions(agent_type) - - # Datei lesen - with open(agents_file, 'r', encoding='utf-8') as f: - agents_data = json.load(f) - - # Nach dem Agententyp suchen - for agent in agents_data: - if agent.get("type") == agent_type: - # Anweisungen zurückgeben, wenn vorhanden - instructions = agent.get("instructions") - if instructions: - logger.debug(f"Anweisungen für Agent-Typ '{agent_type}' aus agents.json geladen") - return instructions - - # Wenn kein passender Agent gefunden wurde, Standardanweisungen verwenden - logger.warning(f"Keine Anweisungen für Agent-Typ '{agent_type}' in agents.json gefunden") - return self._get_default_agent_instructions(agent_type) - - except Exception as e: - logger.error(f"Fehler beim Laden der Agent-Anweisungen aus agents.json: {e}") - return self._get_default_agent_instructions(agent_type) - - def _get_default_agent_instructions(self, agent_type: str) -> str: - """ - Gibt Standard-Anweisungen für einen Agententyp zurück, - wenn keine spezifischen Anweisungen in der agents.json gefunden wurden. - """ - default_instructions = { - "analyzer": """ - Als Datenanalyse-Agent ist es deine Aufgabe, die bereitgestellten Daten zu analysieren und wichtige Erkenntnisse zu extrahieren. - - Folge diesen Anweisungen zur Analyse der Dateien: - 1. Lese und verstehe den Inhalt der bereitgestellten Dateien gründlich - 2. Identifiziere welchen Datentyp jede Datei enthält (z.B. Zeitreihendaten, kategorische Daten, Text) - 3. Wenn es sich um tabellarische Daten handelt: - - Identifiziere Muster, Trends und Anomalien - - Berechne relevante statistische Kennzahlen (Mittelwerte, Mediane, Standardabweichungen) - - Suche nach Korrelationen zwischen verschiedenen Spalten - - Identifiziere Ausreißer und ungewöhnliche Datenpunkte - 4. Wenn es sich um Textdaten handelt: - - Analysiere Schlüsselthemen und -begriffe - - Identifiziere Stimmung und Tonalität, wenn relevant - - Extrahiere zentrale Aussagen und Schlussfolgerungen - 5. Erstelle eine strukturierte Zusammenfassung deiner Erkenntnisse - 6. Gib konkrete, datengestützte Empfehlungen, wenn möglich - - In deiner Antwort: - - Beginne mit einer kurzen Übersicht der analysierten Daten - - Struktur - - Strukturiere deine Erkenntnisse klar mit Überschriften und Aufzählungen - - Füge quantitative Erkenntnisse ein, wo immer möglich - - Schließe mit einer Zusammenfassung der wichtigsten Punkte ab - """, - "visualizer": """ - Als Visualisierungs-Agent ist es deine Aufgabe, die Daten visuell zu beschreiben. - - Beschreibe, welche Art von Visualisierungen für die Daten sinnvoll wären - - Erkläre das Layout und die Komponenten der Visualisierung - - Beschreibe, wie die Daten grafisch dargestellt werden sollten - - Erkläre, welche Erkenntnisse aus dieser Visualisierung gewonnen werden können - """, - "writer": """ - Als Text-Generator ist es deine Aufgabe, verständliche Berichte und Zusammenfassungen zu erstellen. - - Fasse die wichtigsten Erkenntnisse aus den Daten zusammen - - Strukturiere den Bericht klar und prägnant - - Verwende eine sachliche und professionelle Sprache - - Formuliere konkrete Empfehlungen basierend auf den Daten - - Nutze Markdown für bessere Formatierung - """, - "scraper": """ - Als Web-Scraper-Agent ist es deine Aufgabe, Webseiten zu durchsuchen und relevante Informationen zu extrahieren. - - Folge diesen Anweisungen: - 1. Analysiere die bereitgestellten Web-Daten sorgfältig - 2. Extrahiere die wichtigsten Informationen und Fakten aus den Webinhalten - 3. Organisiere die Informationen in klare Kategorien - 4. Identifiziere Trends, Muster und wichtige Erkenntnisse - 5. Vergleiche die Informationen aus verschiedenen Quellen, wenn verfügbar - 6. Fasse die gefundenen Informationen prägnant zusammen - 7. Erkenne mögliche Einschränkungen oder Verzerrungen in den Quellen - - In deiner Antwort: - - Beginne mit einer Zusammenfassung der gefundenen Informationen - - Strukturiere die extrahierten Daten in logische Abschnitte - - Hebe wichtige Fakten und Zahlen hervor - - Gib Quellenhinweise an - - Formuliere Schlussfolgerungen basierend auf den gesammelten Daten - """ - } - - return default_instructions.get(agent_type, "Du bist ein Datenverarbeitungs-Agent. Analysiere die gegebenen Informationen und liefere relevante Erkenntnisse.") - - def _add_log( - self, - workflow_id: str, - message: str, - log_type: str, - agent_id: Optional[str] = None, - agent_name: Optional[str] = None - ) -> None: - """Fügt einen Protokolleintrag zum Workflow hinzu""" - log_entry = { - "id": f"log_{uuid.uuid4()}", - "mandate_id": self.mandate_id, - "user_id": self.user_id, - "message": message, - "type": log_type, - "timestamp": datetime.now().isoformat(), - "agent_id": agent_id, - "agent_name": agent_name - } - - workflow = self.workflows.get(workflow_id) - if workflow: - workflow["logs"].append(log_entry) - logger.info(f"Workflow {workflow_id}: {message}") - - def _create_agent_result( - self, - workflow_id: str, - agent: Dict[str, Any], - index: int, - prompt: str, - file_contexts: List[Dict[str, Any]], - content: str - ) -> Dict[str, Any]: - """Erstellt ein Ergebnisobjekt basierend auf dem Agententyp und der API-Antwort""" - agent_type = agent["type"] - agent_id = agent["id"] - agent_name = agent["name"] - - # Grundlegende Ergebnisstruktur - result = { - "id": f"result_{workflow_id}_{index}", - "mandate_id": self.mandate_id, - "user_id": self.user_id, - "agent_id": agent_id, - "agent_name": agent_name, - "timestamp": datetime.now().isoformat(), - "type": "text", # Standardtyp - "metadata": { - "files_processed": [file["name"] for file in file_contexts], - "prompt": prompt - } - } - - # Titel und Inhalt basierend auf dem Agententyp anpassen - if agent_type == "analyzer": - result.update({ - "title": "Datenanalyse-Ergebnis", - "content": content, - }) - elif agent_type == "visualizer": - result.update({ - "title": "Visualisierungsvorschlag", - "content": content, - "type": "chart" # Auch wenn kein echtes Diagramm, markieren wir es als solches - }) - elif agent_type == "writer": - result.update({ - "title": "Zusammenfassung und Empfehlungen", - "content": content, - }) - elif agent_type == "scraper": - result.update({ - "title": "Web-Recherche Ergebnisse", - "content": content, - }) - else: - result.update({ - "title": f"Ergebnis von {agent_name}", - "content": content, - }) - - return result - - def _save_workflow_results(self, workflow_id: str) -> None: - """Speichert die Workflow-Ergebnisse in einer Datei""" - workflow = self.workflows.get(workflow_id) - if workflow: - try: - file_path = os.path.join(self.results_dir, f"workflow_{workflow_id}.json") - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(workflow, f, indent=2, ensure_ascii=False) - logger.info(f"Workflow-Ergebnisse gespeichert: {file_path}") - except Exception as e: - logger.error(f"Fehler beim Speichern der Workflow-Ergebnisse: {e}") - - def get_workflow_status(self, workflow_id: str) -> Optional[Dict[str, Any]]: - """Gibt den Status eines Workflows zurück""" - workflow = self.workflows.get(workflow_id) - if not workflow: - return None - - return { - "id": workflow["id"], - "mandate_id": workflow.get("mandate_id"), - "user_id": workflow.get("user_id"), - "status": workflow["status"], - "progress": workflow["progress"], - "started_at": workflow["started_at"], - "completed_at": workflow["completed_at"], - "agent_statuses": workflow["agent_statuses"] - } - - def get_workflow_logs(self, workflow_id: str) -> Optional[List[Dict[str, Any]]]: - """Gibt die Protokolle eines Workflows zurück""" - workflow = self.workflows.get(workflow_id) - if not workflow: - return None - - return workflow["logs"] - - def get_workflow_results(self, workflow_id: str) -> Optional[List[Dict[str, Any]]]: - """Gibt die Ergebnisse eines Workflows zurück""" - workflow = self.workflows.get(workflow_id) - if not workflow: - return None - - return workflow["results"] - - async def close(self): - """Schließt den HTTP-Client beim Beenden der Anwendung""" - await self.http_client.aclose() - - -# Singleton-Factory für AgentService-Instanzen pro Kontext -_agent_service_instances = {} - -def get_agentservice_interface(mandate_id: int = None, user_id: int = None) -> AgentService: - """ - Gibt eine AgentService-Instanz für den angegebenen Kontext zurück. - Wiederverwendet bestehende Instanzen. - """ - context_key = f"{mandate_id}_{user_id}" - if context_key not in _agent_service_instances: - _agent_service_instances[context_key] = AgentService(mandate_id, user_id) - return _agent_service_instances[context_key] \ No newline at end of file + return workflow_id \ No newline at end of file diff --git a/gwserver/modules/gateway_interface.py b/gwserver/modules/gateway_interface.py index 6cc1509b..42c7e257 100644 --- a/gwserver/modules/gateway_interface.py +++ b/gwserver/modules/gateway_interface.py @@ -4,7 +4,6 @@ from typing import Dict, Any, List, Optional, Union import importlib from passlib.context import CryptContext from connector_db_json import JSONDatabaseConnector -from modules.gateway_model import User, UserInDB, Token, Mandate logger = logging.getLogger(__name__) @@ -29,8 +28,8 @@ class GatewayInterface: user_id: ID des aktuellen Benutzers (optional) """ # Bei der Initialisierung kann der Kontext leer sein - self.mandate_id = mandate_id if mandate_id is not None else 1 # Root-Mandant als Standard - self.user_id = user_id if user_id is not None else 1 # Admin-Benutzer als Standard + self.mandate_id = mandate_id + self.user_id = user_id # Datenverzeichnis self.data_folder = "_database_gateway" @@ -48,10 +47,36 @@ class GatewayInterface: # Konnektor erstellen self.db = JSONDatabaseConnector( db_folder=self.data_folder, - mandate_id=self.mandate_id, - user_id=self.user_id + mandate_id=self.mandate_id if self.mandate_id is not None else 0, + user_id=self.user_id if self.user_id is not None else 0 ) + # Hole die ID des Root-Mandanten + initial_mandate_id = self.get_initial_id("mandates") + + # Aktualisiere den Mandanten-Kontext, falls nötig + if self.mandate_id is None and initial_mandate_id is not None: + self.mandate_id = initial_mandate_id + # Konnektor mit korrektem Kontext neu erstellen + self.db = JSONDatabaseConnector( + db_folder=self.data_folder, + mandate_id=self.mandate_id, + user_id=self.user_id if self.user_id is not None else 0 + ) + + # Hole die ID des Admin-Benutzers + initial_user_id = self.get_initial_id("users") + + # Aktualisiere den Benutzer-Kontext, falls nötig + if self.user_id is None and initial_user_id is not None: + self.user_id = initial_user_id + # Konnektor mit korrektem Kontext neu erstellen + self.db = JSONDatabaseConnector( + db_folder=self.data_folder, + mandate_id=self.mandate_id, + user_id=self.user_id + ) + # Datenbank initialisieren, falls nötig self._initialize_database() @@ -68,13 +93,21 @@ class GatewayInterface: logger.info("Erstelle Root-Mandant") root_mandate = { - "id": 1, "name": "Root", "language": "de" } - self.db.record_create("mandates", root_mandate) - logger.info("Root-Mandant wurde erstellt") + created_mandate = self.db.record_create("mandates", root_mandate) + logger.info(f"Root-Mandant wurde erstellt mit ID {created_mandate['id']}") + + # Aktualisiere den Mandanten-Kontext + self.mandate_id = created_mandate['id'] + # Konnektor mit korrektem Kontext neu erstellen + self.db = JSONDatabaseConnector( + db_folder=self.data_folder, + mandate_id=self.mandate_id, + user_id=self.user_id if self.user_id is not None else 0 + ) # Prüfe, ob Benutzer existieren users = self.db.get_recordset("users") @@ -84,8 +117,7 @@ class GatewayInterface: logger.info("Erstelle Admin-Benutzer") admin_user = { - "id": 1, - "mandate_id": 1, # Root-Mandant + "mandate_id": self.mandate_id, "username": "admin", "email": "admin@example.com", "full_name": "Administrator", @@ -95,8 +127,29 @@ class GatewayInterface: "hashed_password": self._get_password_hash("admin") # In der Produktion ein sicheres Passwort verwenden! } - self.db.record_create("users", admin_user) - logger.info("Admin-Benutzer wurde erstellt") + created_user = self.db.record_create("users", admin_user) + logger.info(f"Admin-Benutzer wurde erstellt mit ID {created_user['id']}") + + # Aktualisiere den Benutzer-Kontext + self.user_id = created_user['id'] + # Konnektor mit korrektem Kontext neu erstellen + self.db = JSONDatabaseConnector( + db_folder=self.data_folder, + mandate_id=self.mandate_id, + user_id=self.user_id + ) + + def get_initial_id(self, table: str) -> Optional[int]: + """ + Gibt die initiale ID für eine Tabelle zurück. + + Args: + table: Name der Tabelle + + Returns: + Die initiale ID oder None, wenn nicht vorhanden + """ + return self.db.get_initial_id(table) def _get_password_hash(self, password: str) -> str: """Erstellt einen Hash für ein Passwort""" @@ -126,14 +179,7 @@ class GatewayInterface: def create_mandate(self, name: str, language: str = "de") -> Dict[str, Any]: """Erstellt einen neuen Mandanten""" - # Bestimme die nächste ID - mandates = self.db.get_recordset("mandates") - next_id = 1 - if mandates: - next_id = max(mandate["id"] for mandate in mandates) + 1 - mandate_data = { - "id": next_id, "name": name, "language": language } @@ -179,9 +225,10 @@ class GatewayInterface: if not mandate: return False - # Root-Mandant darf nicht gelöscht werden - if mandate_id == 1: - logger.warning("Versuch, den Root-Mandanten zu löschen, wurde verhindert") + # Prüfe, ob es der initiale Mandant ist + initial_mandate_id = self.get_initial_id("mandates") + if initial_mandate_id is not None and mandate_id == initial_mandate_id: + logger.warning(f"Versuch, den Root-Mandanten zu löschen, wurde verhindert") return False # Finde alle Benutzer des Mandanten @@ -277,24 +324,17 @@ class GatewayInterface: if existing_user: raise ValueError(f"Benutzer '{username}' existiert bereits") - # Bestimme die nächste ID - users = self.db.get_recordset("users") - next_id = 1 - if users: - next_id = max(user["id"] for user in users) + 1 - # Verwende den übergebenen mandate_id oder den aktuellen Kontext user_mandate_id = mandate_id if mandate_id is not None else self.mandate_id user_data = { - "id": next_id, "mandate_id": user_mandate_id, "username": username, "email": email, "full_name": full_name, "disabled": disabled, "language": language, - "privilege": privilege, # Neue Eigenschaft + "privilege": privilege, "hashed_password": self._get_password_hash(password) } @@ -447,8 +487,9 @@ class GatewayInterface: if not users: return False - # Root-Admin (ID 1) darf nicht gelöscht werden - if user_id == 1: + # Prüfe, ob es der initiale Benutzer ist + initial_user_id = self.get_initial_id("users") + if initial_user_id is not None and user_id == initial_user_id: logger.warning("Versuch, den Root-Admin zu löschen, wurde verhindert") return False diff --git a/gwserver/modules/lucydom_interface.py b/gwserver/modules/lucydom_interface.py index 113b00e7..ca374c3a 100644 --- a/gwserver/modules/lucydom_interface.py +++ b/gwserver/modules/lucydom_interface.py @@ -61,21 +61,32 @@ class LucyDOMInterface: # Erstelle den Default Workspace default_workspace = { - "id": 1, "mandate_id": self.mandate_id, "user_id": self.user_id, "name": "Default Workspace", "created_at": self._get_current_timestamp() } - self.db.record_create("workspaces", default_workspace) - logger.info("Default Workspace wurde erstellt") + created_workspace = self.db.record_create("workspaces", default_workspace) + logger.info(f"Default Workspace wurde erstellt mit ID {created_workspace['id']}") def _get_current_timestamp(self) -> str: """Gibt den aktuellen Zeitstempel im ISO-Format zurück""" from datetime import datetime return datetime.now().isoformat() + def get_initial_id(self, table: str) -> Optional[int]: + """ + Gibt die initiale ID für eine Tabelle zurück. + + Args: + table: Name der Tabelle + + Returns: + Die initiale ID oder None, wenn nicht vorhanden + """ + return self.db.get_initial_id(table) + # Workspace-Methoden def get_all_workspaces(self) -> List[Dict[str, Any]]: @@ -91,14 +102,7 @@ class LucyDOMInterface: def create_workspace(self, name: str) -> Dict[str, Any]: """Erstellt einen neuen Workspace""" - # Bestimme die nächste ID - workspaces = self.db.get_recordset("workspaces") - next_id = 1 - if workspaces: - next_id = max(workspace["id"] for workspace in workspaces) + 1 - workspace_data = { - "id": next_id, "mandate_id": self.mandate_id, "user_id": self.user_id, "name": name, @@ -125,11 +129,7 @@ class LucyDOMInterface: # Daten für die Aktualisierung vorbereiten workspace_data = { - "id": workspace_id, - "mandate_id": self.mandate_id, - "user_id": self.user_id, - "name": name, - "created_at": workspace.get("created_at") + "name": name } # Workspace aktualisieren @@ -141,6 +141,12 @@ class LucyDOMInterface: Returns: True, wenn der Workspace erfolgreich gelöscht wurde, sonst False """ + # Prüfen, ob es der initiale Workspace ist + initial_workspace_id = self.get_initial_id("workspaces") + if initial_workspace_id is not None and workspace_id == initial_workspace_id: + logger.warning("Versuch, den Default Workspace zu löschen, wurde verhindert") + return False + return self.db.record_delete("workspaces", workspace_id) # Agent-Methoden @@ -163,14 +169,7 @@ class LucyDOMInterface: def create_agent(self, name: str, agent_type: str, workspace_id: int, capabilities: str = None, description: str = None) -> Dict[str, Any]: """Erstellt einen neuen Agenten""" - # Bestimme die nächste ID - agents = self.db.get_recordset("agents") - next_id = 1 - if agents: - next_id = max(agent["id"] for agent in agents) + 1 - agent_data = { - "id": next_id, "mandate_id": self.mandate_id, "user_id": self.user_id, "name": name, @@ -205,9 +204,6 @@ class LucyDOMInterface: # Daten für die Aktualisierung vorbereiten agent_data = { - "id": agent_id, - "mandate_id": self.mandate_id, - "user_id": self.user_id, "name": name, "type": agent_type, "workspace_id": workspace_id, @@ -248,14 +244,7 @@ class LucyDOMInterface: def create_file(self, name: str, file_type: str, content_type: str = None, size: int = None, path: str = None) -> Dict[str, Any]: """Erstellt einen neuen Dateieintrag""" - # Bestimme die nächste ID - files = self.db.get_recordset("files") - next_id = 1 - if files: - next_id = max(file["id"] for file in files) + 1 - file_data = { - "id": next_id, "mandate_id": self.mandate_id, "user_id": self.user_id, "name": name, @@ -290,17 +279,18 @@ class LucyDOMInterface: return None # Daten für die Aktualisierung vorbereiten - file_data = { - "id": file_id, - "mandate_id": self.mandate_id, - "user_id": self.user_id, - "name": name if name is not None else file.get("name"), - "type": file_type if file_type is not None else file.get("type"), - "content_type": content_type if content_type is not None else file.get("content_type"), - "size": size if size is not None else file.get("size"), - "path": path if path is not None else file.get("path"), - "upload_date": file.get("upload_date") - } + file_data = {} + + if name is not None: + file_data["name"] = name + if file_type is not None: + file_data["type"] = file_type + if content_type is not None: + file_data["content_type"] = content_type + if size is not None: + file_data["size"] = size + if path is not None: + file_data["path"] = path # Datei aktualisieren return self.db.record_modify("files", file_id, file_data) @@ -328,14 +318,7 @@ class LucyDOMInterface: def create_prompt(self, content: str, workspace_id: int) -> Dict[str, Any]: """Erstellt einen neuen Prompt""" - # Bestimme die nächste ID - prompts = self.db.get_recordset("prompts") - next_id = 1 - if prompts: - next_id = max(prompt["id"] for prompt in prompts) + 1 - prompt_data = { - "id": next_id, "mandate_id": self.mandate_id, "user_id": self.user_id, "content": content, @@ -363,14 +346,12 @@ class LucyDOMInterface: return None # Daten für die Aktualisierung vorbereiten - prompt_data = { - "id": prompt_id, - "mandate_id": self.mandate_id, - "user_id": self.user_id, - "content": content if content is not None else prompt.get("content"), - "workspace_id": workspace_id if workspace_id is not None else prompt.get("workspace_id"), - "created_at": prompt.get("created_at") - } + prompt_data = {} + + if content is not None: + prompt_data["content"] = content + if workspace_id is not None: + prompt_data["workspace_id"] = workspace_id # Prompt aktualisieren return self.db.record_modify("prompts", prompt_id, prompt_data) @@ -401,5 +382,5 @@ def get_lucydom_interface(mandate_id: int = 0, user_id: int = 0) -> LucyDOMInter _lucydom_interfaces[context_key] = LucyDOMInterface(mandate_id, user_id) return _lucydom_interfaces[context_key] -#Init +# Init get_lucydom_interface() \ No newline at end of file diff --git a/gwserver/routes/prompt.py b/gwserver/routes/prompt.py index c069164b..fadd5002 100644 --- a/gwserver/routes/prompt.py +++ b/gwserver/routes/prompt.py @@ -85,4 +85,78 @@ async def get_prompt( detail=f"Prompt mit ID {prompt_id} nicht gefunden" ) - return prompt \ No newline at end of file + return prompt + + +@router.put("/{prompt_id}", response_model=Dict[str, Any]) +async def update_prompt( + prompt_id: int, + prompt_data: Dict[str, Any] = Body(...), + current_user: Dict[str, Any] = Depends(get_current_active_user) +): + """Einen bestehenden Prompt aktualisieren""" + mandate_id, user_id = await get_user_context(current_user) + + # LucyDOM-Interface mit Benutzerkontext initialisieren + lucy_interface = get_lucydom_interface(mandate_id, user_id) + + # Prüfe, ob der Prompt existiert + existing_prompt = lucy_interface.get_prompt(prompt_id) + if not existing_prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Prompt mit ID {prompt_id} nicht gefunden" + ) + + # Wenn workspace_id vorhanden ist, prüfe, ob der Workspace existiert + workspace_id = prompt_data.get("workspace_id") + if workspace_id: + workspace = lucy_interface.get_workspace(workspace_id) + if not workspace: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Workspace mit ID {workspace_id} nicht gefunden" + ) + + updated_prompt = lucy_interface.update_prompt( + prompt_id=prompt_id, + content=prompt_data.get("content"), + workspace_id=workspace_id + ) + + if not updated_prompt: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Fehler beim Aktualisieren des Prompts" + ) + + return updated_prompt + + +@router.delete("/{prompt_id}", response_model=Dict[str, Any]) +async def delete_prompt( + prompt_id: int, + current_user: Dict[str, Any] = Depends(get_current_active_user) +): + """Einen Prompt löschen""" + mandate_id, user_id = await get_user_context(current_user) + + # LucyDOM-Interface mit Benutzerkontext initialisieren + lucy_interface = get_lucydom_interface(mandate_id, user_id) + + # Prüfe, ob der Prompt existiert + existing_prompt = lucy_interface.get_prompt(prompt_id) + if not existing_prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Prompt mit ID {prompt_id} nicht gefunden" + ) + + success = lucy_interface.delete_prompt(prompt_id) + if not success: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Fehler beim Löschen des Prompts" + ) + + return {"message": f"Prompt mit ID {prompt_id} wurde erfolgreich gelöscht"} \ No newline at end of file diff --git a/gwserver/routes/workspace.py b/gwserver/routes/workspace.py index c252e190..babe8e15 100644 --- a/gwserver/routes/workspace.py +++ b/gwserver/routes/workspace.py @@ -59,4 +59,75 @@ async def create_workspace( lucy_interface = get_lucydom_interface(mandate_id, user_id) new_workspace = lucy_interface.create_workspace(name=workspace.get("name", "Neuer Workspace")) - return new_workspace \ No newline at end of file + return new_workspace + + +@router.put("/{workspace_id}", response_model=Dict[str, Any]) +async def update_workspace( + workspace_id: int, + workspace: Dict[str, Any] = Body(...), + current_user: Dict[str, Any] = Depends(get_current_active_user) +): + """Einen bestehenden Workspace aktualisieren""" + mandate_id, user_id = await get_user_context(current_user) + + # LucyDOM-Interface mit Benutzerkontext initialisieren + lucy_interface = get_lucydom_interface(mandate_id, user_id) + + # Prüfe, ob der Workspace existiert + existing_workspace = lucy_interface.get_workspace(workspace_id) + if not existing_workspace: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Workspace mit ID {workspace_id} nicht gefunden" + ) + + updated_workspace = lucy_interface.update_workspace( + workspace_id=workspace_id, + name=workspace.get("name", existing_workspace.get("name")) + ) + + if not updated_workspace: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Fehler beim Aktualisieren des Workspaces" + ) + + return updated_workspace + + +@router.delete("/{workspace_id}", response_model=Dict[str, Any]) +async def delete_workspace( + workspace_id: int, + current_user: Dict[str, Any] = Depends(get_current_active_user) +): + """Einen Workspace löschen""" + mandate_id, user_id = await get_user_context(current_user) + + # LucyDOM-Interface mit Benutzerkontext initialisieren + lucy_interface = get_lucydom_interface(mandate_id, user_id) + + # Prüfe, ob der Workspace existiert + existing_workspace = lucy_interface.get_workspace(workspace_id) + if not existing_workspace: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Workspace mit ID {workspace_id} nicht gefunden" + ) + + # Prüfe, ob es der initiale Workspace ist + initial_workspace_id = lucy_interface.get_initial_id("workspaces") + if initial_workspace_id == workspace_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Der Default Workspace kann nicht gelöscht werden" + ) + + success = lucy_interface.delete_workspace(workspace_id) + if not success: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Fehler beim Löschen des Workspaces" + ) + + return {"message": f"Workspace mit ID {workspace_id} wurde erfolgreich gelöscht"} \ No newline at end of file diff --git a/gwserver/specification.txt b/gwserver/specification.txt index c88d142f..c7706bdf 100644 --- a/gwserver/specification.txt +++ b/gwserver/specification.txt @@ -1,3 +1,79 @@ +.......................... Tasks + + +Kannst du mir bitte folgende anpassungen am frontend code machen: + +Bei der Ausgabe eines datensatzes ans frontend formular soll die ID immer als schreibgeschützt mitgegeben werden. Es wird nie eine ID gesetzt bei Create. + +Im Frontend soll im generischen Formular "generic-entity.js" für ein neues Objekt die ID entweder hidden oder schreibgeschützt sein. die ID wird nicht benötigt, sondern wird erst mit dem speichern in der datenbank erstellt. d.h. nach dem speichern in der datenbank werden die daten der entsprechenden tabelle neu geladen. + +In den Einstellungen des Frontends soll die Sprache des aktiven benutzers gemäss den Listenoptionen in den "...model.py" angepasst werden können. die sprache gilt dann auch für die Attributnamen in einem Formularfeld im "generic-entity.js". eine sprachänderung zieht somit eine anpassung des Users über das API nach sich, indem die Sprache in der Datenbank angepasst wird. + +Chat mit Instant message - auch inputs geben während der ausführung + +Sprache spezialisierten + +Admin Seite mit CRUD für User Mgmt und Mandate Management, generisch + + +--------------------------- OPEN + + + +----------------------- done + + +Kannst du mir bitte code struktur und logik das 'agentservice_interface.py' anpsssen und die code struktur zur besseren wartung und weiterenwticklung verbessern: + +1. die anbindung der ai-modelle mit den entsprechenden config-daten und den funktionsaufrufen in separate dateien auslagern ("connector_ai_openai","connector_ai_webscraping"). im 'agentservice_interface.py' die connector module bei der initialisierung importieren und vorbereiten. + +2. den agenten-chat 'execute_workflow' nicht in der reihenfolge der agents ausführen, sondern als tischrunde der agents.das heisst ein AI moderator moderiert die agenten autonom und ruft anhand der produzierten antworten und der eigenschaften der agentss den jeweils nächsten geeigneten agenten anhand der 'capabilities' auf, nachdem ein agent seine antwort geliefert hat. +der initiale prompt mit den zugehörigen files und dem chatverlauf im 'LogEntry' mit den n letzten Datensätzen (n wird aus dem Config file aus der variablen Application.MAX_HISTORY gelesen) wird in ein 'message'-objekt als dictionary transformiert, welches so aussieht: + message = { + "role": "user", #--> statisch, immer so + "content": [ #--> liste der Files + { + "type": "text", + "text": prompt_text + }, + { + "type": content_type, # --> diese funktion integrieren wir später + "source": { + "type": "base64", + "media_type": mime_type, + "data": base64_file # --> hier das dateiname der jeweiligen datei + } + }, + { + "type": "text", + "text": LogEntries # --> hier die LogEinträge als Textpaket + } + ] + } +wenn der AI moderator der Meinung ist, dass die aufgabe erfüllt ist, beendet er den workflow. + + +3. initialisierungsset: beantwortet Anfragen direkt mit dem hinterlegten KI Modell, welche keine spezialisierten Agenten benötigen. Dies ist die Generierung von Text, Code, Strukturen, die Analyse von Files, Graphiken erstellen, etc. +(Agent) Organisator: Dieser analysiert den User Prompt und strukturiert die auszuführenden Aufräge sowie die nötigen zu liefernden Resultate +(Agent) Entwickler: Dieser entwickelt python code im Auftrag der anderen Agents und führt ihn anschliessend aus +(Agent) Webscrape: Ein Agent, welcher webscraping durchführt. Dieser nutzt die Funktion '_scrape_url', um eine Webseite zu scannen und den Inhalt zurückzugeben. Er kann auch den Entwickler beauftragen, einen Code zu generieren, welcher die funktion _scrape_url mit einer logik (z.B. iterativ oder batch-mässig) ausführt +(Prompt): Kannst Du mir ein paar initiale Prompts für die folgenden Fragebereiche vorbereiten, welche ausgewählt werden können: +. Web Research +. Analyse +. Protokoll +. Design + + +4. Kannst Du bitte die fehlenden CRUD Methoden in den modulen "workspaces" und "prompts" ergänzen. Ich glaube, es fehlen Post und Delete. + + +5. Datenbank-Management verbessern: In den zwei Modulen "gateway_interface.py" und "lucydom_interface" finden keine Manipulationen oder Referenzierungen mit ID's statt. Die ID's für einen neuen Datensatz werden nur in "connector_....py" modulen vergeben. Jeder datensatz hat eine unique id. in den modulen "...interface.py" werden keine id's generiert. die abfrage für die id=1 wird ersetzt mit der funktion 'get_initial_id', welche weiter unten erklärt ist. +Dazu bitte die Module anpassen und in den Modulen "connector...py" eine system-tabelle ergänzen, welche sich merkt, welche ID der erste datensatz jeder tabelle hat, denn dieser ist der jeweilige system-datensatz. dann eine funktion 'get_initial_id' erfassen, welche in den modulen Modulen "gateway_interface.py" und "lucydom_interface" aufgerufen werden kann, um die id des initialen datensatzes pro tabelle abzufragen. + + + + + der gateway funktioniert noch nicht ganz. kannst mir bitte die module prüfen und besser stukturieren? @@ -67,82 +143,3 @@ agent_service.py: Umbenennen in "interface_agentservice.py" - Ziel soll es sein, dass die Endpunkte-Codestruktur maximal schlank und übersichtlich ist, also auch die Strukturierung und Gruppierung der Endpunkte - - -Summary of the Restructuring -I've completely restructured the gateway code according to your specifications. Here's what I've done: -1. Model Files - -model_gateway.py: Created this file for user, auth, and mandate models. -model_lucydom.py: Renamed from models.py with all application-specific models. -Enhanced both model files to include labels with multi-language support for all classes and attributes. -Changed all IDs from string to long integers as requested. -Made all objects tenant-capable by adding mandate_id and user_id to each model. - -2. Database Connector - -connector_db_json.py: Created a generic connector that provides standard database operations (get_tables, get_fields, get_schema, etc.). -Added automatic filtering by mandate_id for data isolation. -Added proper error handling for case when a user tries to access data from another mandate. - -3. Interface Files - -interface_lucydom.py: Created an interface for the LucyDOM application logic. -interface_gateway.py: Created an interface for the authentication and user management. -interface_agentservice.py: Renamed from agent_service.py with better structure. -Added proper context handling (mandate_id and user_id) throughout all interfaces. - -4. Authentication - -auth.py: Centralized all authentication logic from app.py into this file. -Added better token handling with mandate context. - -5. Main Application (app.py) - -Completely restructured to use the new interfaces. -Simplified endpoint implementations by leveraging the interface functionality. -Made all endpoints context-aware (tenant-aware). -Added new endpoints for mandate management. - -6. Overall Structure - -Improved the code structure to be more modular and maintainable. -Made the system properly tenant-aware at all levels. -Enhanced error handling and logging throughout the codebase. -Made database initialization logic more robust. - -Key Improvements - -Better Separation of Concerns: Clear distinction between models, data access, and business logic. -Multi-Tenancy: All operations now respect tenant boundaries. -Enhanced Type Safety: More explicit typing and model definitions. -Improved Maintainability: Modular code structure makes it easier to maintain and extend. -Centralized Authentication: All auth logic in one place for better security. - -This structure will allow for easier future expansion and maintenance of the system. - - - - - -.......................... Tasks - - - - -Kannst du mir bitte folgende anpassungen am code machen: - -Beim gateway_model.py "User" das Attribut "role" ergänzen mit allen Details. Dies ist die Berechtigungsrolle, sie kann eine dieser Optionen sein: [root,admin,user]. Wenn kein User vorhanden ist, wird ein User mit der Rolle "root" erzeugt. dieser wird dann auch gleich als aktiver user genutzt. - -Im Datenmodell den Datentyp "Lookup" oder ähnlich ergänzen, für Felder mit Auswahl, so wie die Auswahlobjekte für Berechtigungsrolle als Beispiel - -Im "lucydom_interface.py" einen default workspace erstellen, falls keiner vorhanden ist. dieser soll mit den credentials des angemendeten users stattfinden. - -unique id's bei datenobjekten in einer tabelle sicherstellen. über alle datensätze ist eine id eindeutig über mandanten hinweg. das management der id's soll im Connector "connector_bd_json.py" erfolgen. In den "...interface.py" dies rausnehmen. wenn ein neues datenobjekt erstellt wird, so erhält es die nächste verfügbare id. hier soll sichergestellt werden, dass bei parallelen funktionsaufrufen von mehreren Users nicht eine id doppelt gesetzt wird. - -Bei der Ausgabe eines datensatzes soll die ID immer als schreibgeschützt mitgegeben werden. - -Im Frontend soll im generischen Formular "generic-entity.js" für ein neues Objekt die ID entweder hidden oder schreibgeschützt sein. die ID wird nicht benötigt, sondern wird erst mit dem speichern in der datenbank erstellt. d.h. nach dem speichern in der datenbank werden die daten der entsprechenden tabelle neu geladen. - -In den Einstellungen des Frontends soll die Sprache des aktiven benutzers gemäss den Listenoptionen in den "...model.py" angepasst werden können. die sprache gilt dann auch für die Attributnamen in einem Formularfeld im "generic-entity.js". eine sprachänderung zieht somit eine anpassung des Users über das API nach sich, indem die Sprache in der Datenbank angepasst wird. - diff --git a/gwserver/xxxxx_ant_call.py b/gwserver/xxxxx_ant_call.py new file mode 100644 index 00000000..7b2233b2 --- /dev/null +++ b/gwserver/xxxxx_ant_call.py @@ -0,0 +1,122 @@ +from anthropic import Anthropic +import base64 +import magic +import os +from typing import Dict, Any, Union, List + +def create_message_with_document(file_path: str, prompt_text: str = "Bitte analysiere dieses Dokument:") -> Dict[str, Any]: + """ + Erstellt ein Message-Objekt für die Anthropic API, das ein Dokument enthält. + + Args: + file_path: Pfad zur Datei + prompt_text: Text, der zusammen mit dem Dokument gesendet werden soll + + Returns: + Ein Message-Objekt für die Anthropic API + """ + # Datei einlesen und als Base64 kodieren + with open(file_path, "rb") as file: + file_content = file.read() + base64_file = base64.b64encode(file_content).decode('utf-8') + + # Mime-Typ der Datei mit python-magic erkennen + mime_type = magic.from_buffer(file_content, mime=True) + + # Fallback auf Dateiendung, wenn magic keine klare Erkennung liefert + if mime_type == "application/octet-stream": + extension = os.path.splitext(file_path)[1].lower()[1:] + mime_type = get_mime_type_from_extension(extension) + + # Message-Objekt erstellen + content_type, message_structure = determine_content_structure(mime_type) + + message = { + "role": "user", + "content": [ + { + "type": "text", + "text": prompt_text + }, + { + "type": content_type, + "source": { + "type": "base64", + "media_type": mime_type, + "data": base64_file + } + } + ] + } + + return message + +def determine_content_structure(mime_type: str) -> tuple[str, str]: + """ + Bestimmt den richtigen content_type und die Nachrichtenstruktur basierend auf dem MIME-Typ. + + Args: + mime_type: Der MIME-Typ der Datei + + Returns: + Tuple mit (content_type, message_structure) + """ + # Bildtypen + if mime_type.startswith("image/"): + return "image", "image" + + # Dokumenttypen + document_types = [ + "application/pdf", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", # docx + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", # xlsx + "application/vnd.openxmlformats-officedocument.presentationml.presentation", # pptx + "application/vnd.ms-excel", + "application/vnd.ms-powerpoint", + "application/msword", + "text/csv", + "text/plain", + "application/json", + "application/xml", + "text/html" + ] + + if any(mime_type.startswith(dt) for dt in document_types) or mime_type in document_types: + return "document", "document" + + # Fallback für unbekannte Typen + return "document", "document" + +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") diff --git a/requirements.txt b/requirements.txt index 8e2ea14c..fe13668a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -58,6 +58,8 @@ aiofiles>=23.1.0 # Async file operations # AI and NLP openai>=0.27.4 # OpenAI API client +anthropic +python-magic #nltk>=3.8.1 # Natural Language Toolkit #scikit-learn>=1.2.2 # For machine learning utilities #spacy>=3.5.2 # For advanced NLP