786 lines
No EOL
32 KiB
Python
786 lines
No EOL
32 KiB
Python
"""
|
|
Datenanalyst-Agent für die Analyse und Interpretation von Daten.
|
|
Angepasst für die neue chat.py Architektur und chat_registry.py.
|
|
"""
|
|
|
|
import logging
|
|
import json
|
|
import re
|
|
import uuid
|
|
import io
|
|
import base64
|
|
from typing import Dict, Any, List, Optional
|
|
import pandas as pd
|
|
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
import seaborn as sns
|
|
|
|
from modules.chat_registry import AgentBase
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class AgentAnalyst(AgentBase):
|
|
"""Agent für die Analyse und Interpretation von Daten"""
|
|
|
|
def __init__(self):
|
|
"""Initialisiert den Datenanalyse-Agent"""
|
|
super().__init__()
|
|
self.name = "Data Analyst"
|
|
self.capabilities = "data_analysis,pattern_recognition,statistics,visualization,data_interpretation"
|
|
self.result_format = "AnalysisReport"
|
|
|
|
# Visualisierungseinstellungen
|
|
self.plt_style = 'seaborn-v0_8-whitegrid'
|
|
self.default_figsize = (10, 6)
|
|
self.chart_dpi = 100
|
|
plt.style.use(self.plt_style)
|
|
|
|
def get_agent_info(self) -> Dict[str, Any]:
|
|
"""Gibt Agent-Informationen für die Registry zurück"""
|
|
info = super().get_config()
|
|
info.update({
|
|
"metadata": {
|
|
"supported_formats": ["csv", "xlsx", "json", "text"],
|
|
"analysis_types": ["statistical", "trend", "comparative", "predictive", "clustering", "general"],
|
|
"visualization_types": ["bar", "line", "scatter", "histogram", "box", "heatmap", "pie"]
|
|
}
|
|
})
|
|
return info
|
|
|
|
async def process_message(self, message: Dict[str, Any], context: Dict[str, Any] = None) -> Dict[str, Any]:
|
|
"""
|
|
Verarbeitet eine Nachricht und führt Datenanalyse durch.
|
|
|
|
Args:
|
|
message: Eingabenachricht
|
|
context: Optionaler Kontext
|
|
|
|
Returns:
|
|
Antwortnachricht mit Analyseergebnissen
|
|
"""
|
|
# Workflow-ID aus Kontext oder Nachricht extrahieren
|
|
workflow_id = context.get("workflow_id") if context else message.get("workflow_id", "unknown")
|
|
|
|
# Antwortstruktur erstellen
|
|
response = {
|
|
"role": "assistant",
|
|
"content": "",
|
|
"agent_name": self.name,
|
|
"result_format": self.result_format,
|
|
"workflow_id": workflow_id,
|
|
"documents": []
|
|
}
|
|
|
|
try:
|
|
# Aufgabe aus Nachricht extrahieren
|
|
task = message.get("content", "")
|
|
|
|
# Angehängte Dokumente verarbeiten und Daten extrahieren
|
|
document_context = ""
|
|
data_frames = {}
|
|
|
|
if message.get("documents"):
|
|
logger.info("Verarbeite Dokumente für die Analyse")
|
|
document_context, data_frames = await self._process_and_extract_data(message)
|
|
|
|
# Prüfen, ob wir analysierbare Inhalte haben
|
|
have_analyzable_content = len(data_frames) > 0 or (task and len(task.strip()) > 10)
|
|
|
|
if not have_analyzable_content:
|
|
# Warnmeldung, wenn keine analysierbaren Inhalte vorhanden sind
|
|
if message.get("documents"):
|
|
analysis_content = "## Datenanalyse-Bericht\n\nIch konnte keine verarbeitbaren Daten in den bereitgestellten Dokumenten finden. Bitte stellen Sie sicher, dass Sie CSV-, Excel- oder andere Datendateien in einem Format beifügen, das ich analysieren kann."
|
|
else:
|
|
analysis_content = "## Datenanalyse-Bericht\n\nEs wurden keine Daten oder ausreichenden Textinhalte für die Analyse bereitgestellt. Bitte stellen Sie Text für die Analyse bereit oder fügen Sie Datendateien bei, die ich analysieren kann."
|
|
|
|
response["content"] = analysis_content
|
|
return response
|
|
|
|
# Analysetyp bestimmen und Analyse durchführen
|
|
analysis_type = self._determine_analysis_type(task)
|
|
logger.info(f"Führe {analysis_type}-Analyse durch")
|
|
|
|
# Prompt mit Dokumentkontext erweitern
|
|
enhanced_prompt = self._create_enhanced_prompt(message, document_context, context)
|
|
|
|
# Visualisierungsdokumente generieren, falls Daten vorhanden sind
|
|
visualization_documents = []
|
|
if data_frames:
|
|
logger.info(f"Generiere Visualisierungen für {len(data_frames)} Datensätze")
|
|
visualization_documents = self._generate_visualizations(data_frames, analysis_type, workflow_id, task)
|
|
|
|
# Visualisierungen zur Antwort hinzufügen
|
|
response["documents"].extend(visualization_documents)
|
|
|
|
# Analyse mit Datenerkenntnissen generieren, falls Datenrahmen vorhanden sind
|
|
analysis_content = ""
|
|
if data_frames:
|
|
# Datenerkenntnisse extrahieren
|
|
data_insights = self._extract_data_insights(data_frames)
|
|
|
|
# Erkenntnisse zum Prompt hinzufügen
|
|
enhanced_prompt += f"\n\n=== DATENERKENNTNISSE ===\n{data_insights}"
|
|
|
|
# Analyse mit Datenerkenntnissen generieren
|
|
analysis_content = await self._generate_analysis(enhanced_prompt, analysis_type)
|
|
|
|
# Verweise auf die Visualisierungsdokumente einfügen
|
|
if visualization_documents:
|
|
viz_references = "\n\n## Visualisierungen\n\n"
|
|
viz_references += "Die folgenden Visualisierungen wurden erstellt, um die Daten besser zu verstehen:\n\n"
|
|
|
|
for i, doc in enumerate(visualization_documents, 1):
|
|
doc_source = doc.get("source", {})
|
|
doc_name = doc_source.get("name", f"Visualisierung {i}")
|
|
viz_references += f"{i}. **{doc_name}** - Als angehängtes Dokument verfügbar\n"
|
|
|
|
analysis_content += viz_references
|
|
else:
|
|
# Analyse basierend nur auf Text, wenn keine Datenrahmen vorhanden sind
|
|
logger.info("Keine Datenrahmen verfügbar, analysiere Textinhalt")
|
|
analysis_content = await self._generate_analysis(enhanced_prompt, analysis_type)
|
|
|
|
# Inhalt in der Antwort setzen
|
|
response["content"] = analysis_content
|
|
|
|
return response
|
|
|
|
except Exception as e:
|
|
error_msg = f"Fehler bei der Datenanalyse: {str(e)}"
|
|
logger.error(error_msg)
|
|
response["content"] = f"## Fehler bei der Datenanalyse\n\n{error_msg}"
|
|
return response
|
|
|
|
def _create_enhanced_prompt(self, message: Dict[str, Any], document_context: str, context: Dict[str, Any] = None) -> str:
|
|
"""
|
|
Erstellt einen erweiterten Prompt für die Analyse, der alle verfügbaren Inhalte integriert.
|
|
|
|
Args:
|
|
message: Die ursprüngliche Nachricht
|
|
document_context: Aus Dokumenten extrahierter Kontext
|
|
context: Optionaler zusätzlicher Kontext
|
|
|
|
Returns:
|
|
Erweiterter Prompt für die Analyse
|
|
"""
|
|
# Originale Aufgabe/Prompt abrufen
|
|
task = message.get("content", "")
|
|
|
|
# Mit Aufgabe beginnen
|
|
enhanced_prompt = f"ANALYSEAUFGABE:\n{task}"
|
|
|
|
# Dokumentkontext hinzufügen, falls vorhanden
|
|
if document_context:
|
|
enhanced_prompt += f"\n\n=== DOKUMENTINHALT ===\n{document_context}"
|
|
else:
|
|
# Wenn kein Dokumentinhalt vorhanden ist, ausdrücklich darauf hinweisen, dass wir den Textinhalt direkt analysieren
|
|
enhanced_prompt += "\n\nEs wurden keine Datendateien bereitgestellt. Führe eine Analyse des Textinhalts selbst durch."
|
|
|
|
return enhanced_prompt
|
|
|
|
async def _process_and_extract_data(self, message: Dict[str, Any]) -> tuple:
|
|
"""
|
|
Verarbeitet Dokumente und extrahiert strukturierte Daten.
|
|
|
|
Args:
|
|
message: Eingabenachricht mit Dokumenten
|
|
|
|
Returns:
|
|
Tuple aus (document_context, data_frames_dict)
|
|
"""
|
|
document_context = ""
|
|
data_frames = {}
|
|
|
|
if not message.get("documents"):
|
|
return document_context, data_frames
|
|
|
|
# Dokumenttext extrahieren
|
|
document_context = self._extract_document_text(message)
|
|
|
|
# Datendateien identifizieren und verarbeiten (CSV, Excel usw.)
|
|
for document in message.get("documents", []):
|
|
source = document.get("source", {})
|
|
filename = source.get("name", "")
|
|
|
|
# Überspringen, wenn keine erkennbare Datendatei
|
|
if not self._is_data_file(filename):
|
|
continue
|
|
|
|
try:
|
|
# Dateiinhalt aus Dokumentinhalten extrahieren
|
|
file_content = None
|
|
for content in document.get("contents", []):
|
|
if content.get("type") == "text":
|
|
file_content = content.get("text", "")
|
|
break
|
|
|
|
# Nach Dateityp verarbeiten
|
|
if filename.lower().endswith('.csv') and file_content:
|
|
df = pd.read_csv(io.StringIO(file_content))
|
|
df = self._preprocess_dataframe(df)
|
|
data_frames[filename] = df
|
|
|
|
elif filename.lower().endswith(('.xlsx', '.xls')) and file_content:
|
|
# XLS-Dateien können nicht direkt aus Text verarbeitet werden
|
|
logger.warning(f"Excel-Datei {filename} kann nicht direkt aus Textinhalt verarbeitet werden")
|
|
|
|
elif filename.lower().endswith('.json') and file_content:
|
|
try:
|
|
data = json.loads(file_content)
|
|
if isinstance(data, list):
|
|
df = pd.DataFrame(data)
|
|
elif isinstance(data, dict):
|
|
if any(isinstance(v, list) for v in data.values()):
|
|
for key, value in data.items():
|
|
if isinstance(value, list) and len(value) > 0:
|
|
df = pd.DataFrame(value)
|
|
break
|
|
else:
|
|
continue
|
|
else:
|
|
df = pd.DataFrame([data])
|
|
else:
|
|
continue
|
|
|
|
df = self._preprocess_dataframe(df)
|
|
data_frames[filename] = df
|
|
except:
|
|
logger.error(f"Fehler beim Verarbeiten der JSON-Datei {filename}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Verarbeiten der Datei {filename}: {str(e)}")
|
|
|
|
return document_context, data_frames
|
|
|
|
def _is_data_file(self, filename: str) -> bool:
|
|
"""Prüft, ob eine Datei eine verarbeitbare Datendatei ist"""
|
|
if filename.lower().endswith(('.csv', '.xlsx', '.xls', '.json')):
|
|
return True
|
|
return False
|
|
|
|
def _preprocess_dataframe(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
"""Führt grundlegende Vorverarbeitung für einen DataFrame durch"""
|
|
if df.empty:
|
|
return df
|
|
|
|
# Vollständig leere Zeilen und Spalten entfernen
|
|
df = df.dropna(how='all')
|
|
df = df.dropna(axis=1, how='all')
|
|
|
|
# Stringkonvertierung zu numerischen Werten, wo angemessen
|
|
for col in df.columns:
|
|
# Überspringen, wenn bereits numerisch
|
|
if pd.api.types.is_numeric_dtype(df[col]):
|
|
continue
|
|
|
|
# Überspringen, wenn überwiegend nicht-numerische Strings
|
|
if df[col].dtype == 'object':
|
|
# Prüfen, ob mehr als 80% der Nicht-NA-Werte numerisch sein könnten
|
|
non_na_values = df[col].dropna()
|
|
if len(non_na_values) == 0:
|
|
continue
|
|
|
|
# Versuch der Konvertierung zu numerischen Werten
|
|
numeric_count = pd.to_numeric(non_na_values, errors='coerce').notna().sum()
|
|
if numeric_count / len(non_na_values) > 0.8:
|
|
# Mehr als 80% können in numerische Werte konvertiert werden
|
|
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
|
|
return df
|
|
|
|
def _extract_document_text(self, message: Dict[str, Any]) -> str:
|
|
"""
|
|
Extrahiert Text aus Dokumenten.
|
|
|
|
Args:
|
|
message: Eingabenachricht mit Dokumenten
|
|
|
|
Returns:
|
|
Extrahierter Text
|
|
"""
|
|
text_content = ""
|
|
for document in message.get("documents", []):
|
|
source = document.get("source", {})
|
|
name = source.get("name", "unnamed")
|
|
|
|
text_content += f"\n\n--- {name} ---\n"
|
|
|
|
for content in document.get("contents", []):
|
|
if content.get("type") == "text":
|
|
text_content += content.get("text", "")
|
|
|
|
return text_content
|
|
|
|
def _determine_analysis_type(self, task: str) -> str:
|
|
"""
|
|
Bestimmt den Analysetyp basierend auf der Aufgabe.
|
|
|
|
Args:
|
|
task: Die Analyseaufgabe
|
|
|
|
Returns:
|
|
Analysetyp
|
|
"""
|
|
task_lower = task.lower()
|
|
|
|
# Prüfen auf statistische Analyse
|
|
if any(term in task_lower for term in ["statistik", "statistical", "mittelwert", "mean", "median", "varianz"]):
|
|
return "statistical"
|
|
|
|
# Prüfen auf Trend-Analyse
|
|
elif any(term in task_lower for term in ["trend", "pattern", "zeitreihe", "time series", "historisch"]):
|
|
return "trend"
|
|
|
|
# Prüfen auf vergleichende Analyse
|
|
elif any(term in task_lower for term in ["vergleich", "compare", "comparison", "versus", "vs", "unterschied"]):
|
|
return "comparative"
|
|
|
|
# Prüfen auf prädiktive Analyse
|
|
elif any(term in task_lower for term in ["vorhersage", "predict", "forecast", "zukunft", "future"]):
|
|
return "predictive"
|
|
|
|
# Prüfen auf Clustering oder Kategorisierung
|
|
elif any(term in task_lower for term in ["cluster", "segment", "kategorisieren", "classify"]):
|
|
return "clustering"
|
|
|
|
# Standard: allgemeine Analyse
|
|
else:
|
|
return "general"
|
|
|
|
def _extract_data_insights(self, data_frames: Dict[str, pd.DataFrame]) -> str:
|
|
"""
|
|
Extrahiert grundlegende Erkenntnisse aus DataFrames.
|
|
|
|
Args:
|
|
data_frames: Dictionary von DataFrames
|
|
|
|
Returns:
|
|
Extrahierte Erkenntnisse als Text
|
|
"""
|
|
insights = []
|
|
|
|
for name, df in data_frames.items():
|
|
if df.empty:
|
|
continue
|
|
|
|
insight = f"Datensatz: {name}\n"
|
|
insight += f"Form: {df.shape[0]} Zeilen, {df.shape[1]} Spalten\n"
|
|
insight += f"Spalten: {', '.join(df.columns.tolist())}\n"
|
|
|
|
# Grundlegende Statistiken für numerische Spalten
|
|
numeric_cols = df.select_dtypes(include=['number']).columns
|
|
if len(numeric_cols) > 0:
|
|
insight += "Statistiken für numerische Spalten:\n"
|
|
for col in numeric_cols[:5]: # Auf die ersten 5 Spalten begrenzen
|
|
stats = df[col].describe()
|
|
insight += f" {col}: min={stats['min']:.2f}, max={stats['max']:.2f}, mean={stats['mean']:.2f}, median={df[col].median():.2f}\n"
|
|
|
|
# Kategoriale Spaltenwerte
|
|
cat_cols = df.select_dtypes(include=['object', 'category']).columns
|
|
if len(cat_cols) > 0:
|
|
insight += "Kategoriale Spalten:\n"
|
|
for col in cat_cols[:3]: # Auf die ersten 3 Spalten begrenzen
|
|
# Top 3 Werte abrufen
|
|
top_values = df[col].value_counts().head(3)
|
|
vals_str = ", ".join([f"{val} ({count})" for val, count in top_values.items()])
|
|
insight += f" {col}: {df[col].nunique()} eindeutige Werte. Häufigste Werte: {vals_str}\n"
|
|
|
|
insights.append(insight)
|
|
|
|
return "\n\n".join(insights)
|
|
|
|
def _generate_visualizations(self, data_frames: Dict[str, pd.DataFrame], analysis_type: str,
|
|
workflow_id: str, task: str) -> List[Dict[str, Any]]:
|
|
"""
|
|
Generiert passende Visualisierungen basierend auf Daten und Analysetyp.
|
|
|
|
Args:
|
|
data_frames: Dictionary von zu visualisierenden DataFrames
|
|
analysis_type: Durchzuführender Analysetyp
|
|
workflow_id: Workflow-ID
|
|
task: Ursprüngliche Aufgabenbeschreibung
|
|
|
|
Returns:
|
|
Liste von Visualisierungsdokumentobjekten
|
|
"""
|
|
documents = []
|
|
|
|
for name, df in data_frames.items():
|
|
if df.empty or df.shape[0] < 2:
|
|
continue # Leere oder einzeilige DataFrames überspringen
|
|
|
|
# Verschiedene Visualisierungen basierend auf dem Analysetyp erzeugen
|
|
if analysis_type == "statistical":
|
|
viz_docs = self._create_statistical_visualizations(df, name)
|
|
documents.extend(viz_docs)
|
|
|
|
elif analysis_type == "trend":
|
|
viz_docs = self._create_trend_visualizations(df, name)
|
|
documents.extend(viz_docs)
|
|
|
|
elif analysis_type == "comparative":
|
|
viz_docs = self._create_comparative_visualizations(df, name)
|
|
documents.extend(viz_docs)
|
|
|
|
else: # Allgemeine Analyse
|
|
viz_docs = self._create_general_visualizations(df, name)
|
|
documents.extend(viz_docs)
|
|
|
|
return documents
|
|
|
|
def _create_statistical_visualizations(self, df: pd.DataFrame, name: str) -> List[Dict[str, Any]]:
|
|
"""Erstellt statistische Visualisierungen für einen DataFrame"""
|
|
documents = []
|
|
|
|
# 1. Verteilungs-/Histogramm-Plots für numerische Spalten
|
|
numeric_cols = df.select_dtypes(include=['number']).columns[:3] # Auf erste 3 begrenzen
|
|
if len(numeric_cols) > 0:
|
|
plt.figure(figsize=(12, 4 * len(numeric_cols)))
|
|
|
|
for i, col in enumerate(numeric_cols, 1):
|
|
plt.subplot(len(numeric_cols), 1, i)
|
|
sns.histplot(df[col].dropna(), kde=True)
|
|
plt.title(f'Verteilung von {col}')
|
|
plt.tight_layout()
|
|
|
|
# Abbildung speichern
|
|
img_data = self._get_figure_as_base64()
|
|
plt.close()
|
|
|
|
# Dokument erstellen
|
|
doc_id = f"viz_stat_dist_{uuid.uuid4()}"
|
|
doc = {
|
|
"id": doc_id,
|
|
"source": {
|
|
"type": "generated",
|
|
"id": doc_id,
|
|
"name": f"Statistische Verteilungen - {name}",
|
|
"content_type": "image/png"
|
|
},
|
|
"contents": [{
|
|
"type": "image",
|
|
"data": img_data,
|
|
"format": "base64"
|
|
}]
|
|
}
|
|
documents.append(doc)
|
|
|
|
# 2. Box-Plots für numerische Spalten
|
|
if len(numeric_cols) > 0:
|
|
plt.figure(figsize=(12, 8))
|
|
sns.boxplot(data=df[numeric_cols])
|
|
plt.title(f'Box-Plots der numerischen Variablen in {name}')
|
|
plt.xticks(rotation=45)
|
|
plt.tight_layout()
|
|
|
|
# Abbildung speichern
|
|
img_data = self._get_figure_as_base64()
|
|
plt.close()
|
|
|
|
# Dokument erstellen
|
|
doc_id = f"viz_stat_box_{uuid.uuid4()}"
|
|
doc = {
|
|
"id": doc_id,
|
|
"source": {
|
|
"type": "generated",
|
|
"id": doc_id,
|
|
"name": f"Box-Plots - {name}",
|
|
"content_type": "image/png"
|
|
},
|
|
"contents": [{
|
|
"type": "image",
|
|
"data": img_data,
|
|
"format": "base64"
|
|
}]
|
|
}
|
|
documents.append(doc)
|
|
|
|
return documents
|
|
|
|
def _create_trend_visualizations(self, df: pd.DataFrame, name: str) -> List[Dict[str, Any]]:
|
|
"""Erstellt Trend-Visualisierungen für einen DataFrame"""
|
|
documents = []
|
|
|
|
# Numerische Spalten für die Darstellung verwenden
|
|
numeric_cols = df.select_dtypes(include=['number']).columns[:2] # Auf erste 2 begrenzen
|
|
|
|
if len(numeric_cols) > 0:
|
|
plt.figure(figsize=(12, 6))
|
|
|
|
for col in numeric_cols:
|
|
plt.plot(df.index, df[col], marker='o', label=col)
|
|
|
|
plt.title(f'Trendsicht von {", ".join(numeric_cols)} - {name}')
|
|
plt.legend()
|
|
plt.tight_layout()
|
|
|
|
# Abbildung speichern
|
|
img_data = self._get_figure_as_base64()
|
|
plt.close()
|
|
|
|
# Dokument erstellen
|
|
doc_id = f"viz_trend_{uuid.uuid4()}"
|
|
doc = {
|
|
"id": doc_id,
|
|
"source": {
|
|
"type": "generated",
|
|
"id": doc_id,
|
|
"name": f"Trendanalyse - {name}",
|
|
"content_type": "image/png"
|
|
},
|
|
"contents": [{
|
|
"type": "image",
|
|
"data": img_data,
|
|
"format": "base64"
|
|
}]
|
|
}
|
|
documents.append(doc)
|
|
|
|
return documents
|
|
|
|
def _create_comparative_visualizations(self, df: pd.DataFrame, name: str) -> List[Dict[str, Any]]:
|
|
"""Erstellt vergleichende Visualisierungen für einen DataFrame"""
|
|
documents = []
|
|
|
|
# 1. Kategoriale Spalten für Gruppierung suchen
|
|
cat_cols = df.select_dtypes(include=['object', 'category']).columns
|
|
|
|
if len(cat_cols) > 0:
|
|
# Erste kategoriale Spalte mit angemessener Anzahl eindeutiger Werte verwenden
|
|
groupby_col = None
|
|
for col in cat_cols:
|
|
unique_count = df[col].nunique()
|
|
if 2 <= unique_count <= 10: # Angemessene Anzahl von Kategorien
|
|
groupby_col = col
|
|
break
|
|
|
|
if groupby_col:
|
|
# Numerische Spalten für den Vergleich über Gruppen hinweg suchen
|
|
numeric_cols = df.select_dtypes(include=['number']).columns[:3] # Auf erste 3 begrenzen
|
|
|
|
if len(numeric_cols) > 0:
|
|
# 1. Balkendiagramm, das Mittelwerte vergleicht
|
|
plt.figure(figsize=(12, 6))
|
|
mean_by_group = df.groupby(groupby_col)[numeric_cols].mean()
|
|
mean_by_group.plot(kind='bar')
|
|
plt.title(f'Vergleich der Mittelwerte nach {groupby_col} - {name}')
|
|
plt.xticks(rotation=45)
|
|
plt.tight_layout()
|
|
|
|
# Abbildung speichern
|
|
img_data = self._get_figure_as_base64()
|
|
plt.close()
|
|
|
|
# Dokument erstellen
|
|
doc_id = f"viz_comp_bar_{uuid.uuid4()}"
|
|
doc = {
|
|
"id": doc_id,
|
|
"source": {
|
|
"type": "generated",
|
|
"id": doc_id,
|
|
"name": f"Mittelwertvergleich nach {groupby_col} - {name}",
|
|
"content_type": "image/png"
|
|
},
|
|
"contents": [{
|
|
"type": "image",
|
|
"data": img_data,
|
|
"format": "base64"
|
|
}]
|
|
}
|
|
documents.append(doc)
|
|
|
|
# 2. Streudiagramm für den Vergleich zweier numerischer Variablen
|
|
numeric_cols = df.select_dtypes(include=['number']).columns
|
|
if len(numeric_cols) >= 2:
|
|
plt.figure(figsize=(10, 8))
|
|
# Erste beiden numerischen Spalten als Feature und Ziel verwenden
|
|
x_col, y_col = numeric_cols[0], numeric_cols[1]
|
|
|
|
plt.scatter(df[x_col], df[y_col])
|
|
plt.title(f'Vergleich von {x_col} vs {y_col} - {name}')
|
|
plt.xlabel(x_col)
|
|
plt.ylabel(y_col)
|
|
plt.tight_layout()
|
|
|
|
# Abbildung speichern
|
|
img_data = self._get_figure_as_base64()
|
|
plt.close()
|
|
|
|
# Dokument erstellen
|
|
doc_id = f"viz_comp_scatter_{uuid.uuid4()}"
|
|
doc = {
|
|
"id": doc_id,
|
|
"source": {
|
|
"type": "generated",
|
|
"id": doc_id,
|
|
"name": f"Variablenvergleich - {name}",
|
|
"content_type": "image/png"
|
|
},
|
|
"contents": [{
|
|
"type": "image",
|
|
"data": img_data,
|
|
"format": "base64"
|
|
}]
|
|
}
|
|
documents.append(doc)
|
|
|
|
return documents
|
|
|
|
def _create_general_visualizations(self, df: pd.DataFrame, name: str) -> List[Dict[str, Any]]:
|
|
"""Erstellt allgemeine Visualisierungen für einen DataFrame"""
|
|
documents = []
|
|
|
|
# 1. Datenübersicht: Numerische Zusammenfassung
|
|
numeric_cols = df.select_dtypes(include=['number']).columns
|
|
if len(numeric_cols) > 0:
|
|
# Balkendiagramm der Mittelwerte für numerische Spalten erstellen
|
|
plt.figure(figsize=(12, 6))
|
|
means = df[numeric_cols].mean().sort_values()
|
|
means.plot(kind='bar')
|
|
plt.title(f'Mittelwerte der numerischen Variablen - {name}')
|
|
plt.xticks(rotation=45)
|
|
plt.tight_layout()
|
|
|
|
# Abbildung speichern
|
|
img_data = self._get_figure_as_base64()
|
|
plt.close()
|
|
|
|
# Dokument erstellen
|
|
doc_id = f"viz_gen_means_{uuid.uuid4()}"
|
|
doc = {
|
|
"id": doc_id,
|
|
"source": {
|
|
"type": "generated",
|
|
"id": doc_id,
|
|
"name": f"Zusammenfassung numerischer Variablen - {name}",
|
|
"content_type": "image/png"
|
|
},
|
|
"contents": [{
|
|
"type": "image",
|
|
"data": img_data,
|
|
"format": "base64"
|
|
}]
|
|
}
|
|
documents.append(doc)
|
|
|
|
# 2. Übersicht über kategoriale Daten
|
|
cat_cols = df.select_dtypes(include=['object', 'category']).columns
|
|
if len(cat_cols) > 0:
|
|
# Erste kategoriale Spalte mit angemessener Kardinalität auswählen
|
|
for col in cat_cols:
|
|
if df[col].nunique() <= 10: # Angemessene Anzahl von Kategorien
|
|
plt.figure(figsize=(10, 6))
|
|
value_counts = df[col].value_counts().sort_values(ascending=False)
|
|
value_counts.plot(kind='bar')
|
|
plt.title(f'Verteilung von {col} - {name}')
|
|
plt.xticks(rotation=45)
|
|
plt.tight_layout()
|
|
|
|
# Abbildung speichern
|
|
img_data = self._get_figure_as_base64()
|
|
plt.close()
|
|
|
|
# Dokument erstellen
|
|
doc_id = f"viz_gen_cat_{uuid.uuid4()}"
|
|
doc = {
|
|
"id": doc_id,
|
|
"source": {
|
|
"type": "generated",
|
|
"id": doc_id,
|
|
"name": f"Kategoriale Verteilung - {name}",
|
|
"content_type": "image/png"
|
|
},
|
|
"contents": [{
|
|
"type": "image",
|
|
"data": img_data,
|
|
"format": "base64"
|
|
}]
|
|
}
|
|
documents.append(doc)
|
|
break # Nur die erste geeignete Spalte verwenden
|
|
|
|
return documents
|
|
|
|
def _get_figure_as_base64(self) -> str:
|
|
"""Konvertiert aktuelle matplotlib-Abbildung in base64-String"""
|
|
buffer = io.BytesIO()
|
|
plt.savefig(buffer, format='png', dpi=self.chart_dpi)
|
|
buffer.seek(0)
|
|
image_png = buffer.getvalue()
|
|
buffer.close()
|
|
|
|
# Zu base64 konvertieren
|
|
image_base64 = base64.b64encode(image_png).decode('utf-8')
|
|
return image_base64
|
|
|
|
async def _generate_analysis(self, prompt: str, analysis_type: str) -> str:
|
|
"""
|
|
Generiert eine Analyse basierend auf Prompt und Analysetyp.
|
|
|
|
Args:
|
|
prompt: Der Analyseprompt
|
|
analysis_type: Analysetyp
|
|
|
|
Returns:
|
|
Generierte Analyse
|
|
"""
|
|
if not self.ai_service:
|
|
logging.warning("KI-Service nicht verfügbar für Analysegenerierung")
|
|
return f"## Datenanalyse ({analysis_type})\n\nAnalyse konnte nicht generiert werden: KI-Service nicht verfügbar."
|
|
|
|
# Spezialisierten Prompt basierend auf Analysetyp erstellen
|
|
system_prompt = f"""
|
|
Du bist ein spezialisierter Datenanalyst, der auf {analysis_type}-Analysen fokussiert ist.
|
|
|
|
Erstelle eine detaillierte Analyse der bereitgestellten Daten und/oder Textinhalte.
|
|
Deine Analyse sollte folgendes enthalten:
|
|
1. Eine Zusammenfassung der Daten/Inhalte
|
|
2. Wichtige Erkenntnisse und Einsichten
|
|
3. Stützende Belege und Berechnungen
|
|
4. Klare Schlussfolgerungen
|
|
5. Empfehlungen, wo angemessen
|
|
|
|
Formatiere die Analyse in Markdown mit geeigneten Überschriften, Listen und Tabellen.
|
|
"""
|
|
|
|
# Bestimmen, ob dies eine datenbasierte oder textbasierte Analyse ist
|
|
is_data_analysis = "DATENERKENNTNISSE" in prompt
|
|
|
|
# Prompt mit analysespezifischen Anweisungen erweitern
|
|
if is_data_analysis:
|
|
enhanced_prompt = f"""
|
|
Generiere eine detaillierte {analysis_type}-Analyse basierend auf den folgenden Daten:
|
|
|
|
{prompt}
|
|
"""
|
|
else:
|
|
# Anweisungen für textbasierte Analyse
|
|
enhanced_prompt = f"""
|
|
Generiere eine detaillierte {analysis_type}-Analyse des folgenden Textinhalts:
|
|
|
|
{prompt}
|
|
"""
|
|
|
|
try:
|
|
content = await self.ai_service.call_api([
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": enhanced_prompt}
|
|
])
|
|
|
|
# Sicherstellen, dass es einen Titel am Anfang gibt
|
|
if not content.strip().startswith("# "):
|
|
content = f"# {analysis_type.capitalize()}-Analyse\n\n{content}"
|
|
|
|
return content
|
|
except Exception as e:
|
|
return f"# {analysis_type.capitalize()}-Analyse\n\nFehler bei der Analysegenerierung: {str(e)}"
|
|
|
|
# Singleton-Instanz
|
|
_analyst_agent = None
|
|
|
|
def get_analyst_agent():
|
|
"""Gibt eine Singleton-Instanz des Analyst-Agenten zurück"""
|
|
global _analyst_agent
|
|
if _analyst_agent is None:
|
|
_analyst_agent = AgentAnalyst()
|
|
return _analyst_agent |