From fe3cf81bc7824c2f51cc7f8aa1cea4085a2ff4ed Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Sat, 10 May 2025 18:09:53 +0200 Subject: [PATCH] "Add/update or fix?" --- __pycache__/app.cpython-313.pyc | Bin 79657 -> 84254 bytes app.py | 222 ++++++++++++++++++++++++-------- 2 files changed, 166 insertions(+), 56 deletions(-) diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index 6b1dbbcda68796025e9fd213551e4c5a08accc13..eccc8672783171380eda85f6423c6dc2cce7cbf3 100644 GIT binary patch delta 16939 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 delta 12943 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 diff --git a/app.py b/app.py index c965712..d368134 100644 --- a/app.py +++ b/app.py @@ -122,6 +122,62 @@ socketio = SocketIO(app) migrate = Migrate(app, db) +# Automatische Datenbankinitialisierung +@app.before_first_request +def initialize_app(): + """Initialisierung der Anwendung beim ersten Request""" + print("Initialisierung der Anwendung...") + with app.app_context(): + # Prüfen, ob die Datenbank existiert und initialisiert ist + try: + # Prüfen, ob Tabellen existieren + db.create_all() + + # Prüfen, ob Stammdaten vorhanden sind + if User.query.count() == 0: + print("Erstelle Standardbenutzer...") + create_default_users() + + if Category.query.count() == 0: + print("Erstelle Standardkategorien...") + create_default_categories() + + if MindMapNode.query.count() == 0 and Category.query.count() > 0: + print("Erstelle Beispiel-Mindmap...") + create_sample_mindmap() + + print("Datenbank wurde erfolgreich initialisiert.") + except Exception as e: + import traceback + print(f"Fehler bei der Datenbankinitialisierung: {e}") + print(traceback.format_exc()) + +def create_default_users(): + """Erstellt Standardbenutzer für die Anwendung""" + users = [ + { + 'username': 'admin', + 'email': 'admin@example.com', + 'password': 'admin', + 'role': 'admin' + }, + { + 'username': 'user', + 'email': 'user@example.com', + 'password': 'user', + 'role': 'user' + } + ] + + for user_data in users: + password = user_data.pop('password') + user = User(**user_data) + user.set_password(password) + db.session.add(user) + + db.session.commit() + print(f"{len(users)} Benutzer wurden erstellt.") + def create_default_categories(): """Erstellt die Standardkategorien für die Mindmap""" # Hauptkategorien @@ -1569,67 +1625,121 @@ def chat_with_assistant(): def check_database_query(user_message): """ - Überprüft, ob die Benutzeranfrage nach Datenbankinformationen sucht und extrahiert - relevante Daten aus der Datenbank. + Prüft, ob die Benutzeranfrage nach Datenbankinformationen sucht + und gibt relevante Informationen aus der Datenbank zurück. + + Returns: + (bool, str): Tupel aus (ist_db_anfrage, db_antwort) """ - context = [] + user_message = user_message.lower() - # Prüfen auf verschiedene Datenbankabfragemuster - if any(keyword in user_message.lower() for keyword in ['gedanken', 'thought', 'beitrag', 'inhalt']): - # Suche nach relevanten Gedanken - thoughts = Thought.query.filter( - db.or_( - Thought.title.ilike(f'%{word}%') for word in user_message.split() - if len(word) > 3 # Nur längere Wörter zur Suche verwenden - ) - ).limit(5).all() + # Schlüsselwörter, die auf eine Datenbankanfrage hindeuten + db_keywords = [ + 'wie viele', 'wieviele', 'anzahl', 'liste', 'zeige', 'zeig mir', 'datenbank', + 'statistik', 'stats', 'benutzer', 'gedanken', 'kategorien', 'mindmap', + 'neueste', 'kürzlich', 'gespeichert', 'zähle', 'suche nach', 'finde' + ] + + # Überprüfen, ob die Nachricht eine Datenbankanfrage sein könnte + is_db_query = any(keyword in user_message for keyword in db_keywords) + + if not is_db_query: + return False, "" + + # Datenbank-Antwort vorbereiten + response = "Hier sind Informationen aus der Datenbank:\n\n" + + try: + # 1. Benutzer-Statistiken + if any(kw in user_message for kw in ['benutzer', 'user', 'nutzer']): + user_count = User.query.count() + active_users = User.query.filter_by(is_active=True).count() + admins = User.query.filter_by(role='admin').count() + + response += f"**Benutzer-Statistiken:**\n" + response += f"- Gesamt: {user_count} Benutzer\n" + response += f"- Aktiv: {active_users} Benutzer\n" + response += f"- Administratoren: {admins} Benutzer\n\n" - if thoughts: - context.append("Relevante Gedanken:") - for thought in thoughts: - context.append(f"- Titel: {thought.title}") - context.append(f" Zusammenfassung: {thought.abstract if thought.abstract else 'Keine Zusammenfassung verfügbar'}") - context.append(f" Keywords: {thought.keywords if thought.keywords else 'Keine Keywords verfügbar'}") - context.append("") - - if any(keyword in user_message.lower() for keyword in ['kategorie', 'category', 'themengebiet', 'bereich']): - # Suche nach Kategorien - categories = Category.query.filter( - db.or_( - Category.name.ilike(f'%{word}%') for word in user_message.split() - if len(word) > 3 - ) - ).limit(5).all() + # 2. Gedanken-Statistiken + if any(kw in user_message for kw in ['gedanken', 'thought', 'inhalt']): + thought_count = Thought.query.count() + + if thought_count > 0: + avg_rating = db.session.query(func.avg(ThoughtRating.relevance_score)).scalar() or 0 + avg_rating = round(avg_rating, 1) + + recent_thoughts = Thought.query.order_by(Thought.created_at.desc()).limit(5).all() + + response += f"**Gedanken-Statistiken:**\n" + response += f"- Gesamt: {thought_count} Gedanken\n" + response += f"- Durchschnittliche Bewertung: {avg_rating}/5\n\n" + + if recent_thoughts: + response += "**Neueste Gedanken:**\n" + for thought in recent_thoughts: + response += f"- {thought.title} (von {thought.author.username})\n" + response += "\n" + else: + response += "Es sind noch keine Gedanken in der Datenbank gespeichert.\n\n" - if categories: - context.append("Relevante Kategorien:") - for category in categories: - context.append(f"- Name: {category.name}") - context.append(f" Beschreibung: {category.description}") - context.append("") - - if any(keyword in user_message.lower() for keyword in ['mindmap', 'karte', 'visualisierung']): - # Suche nach öffentlichen Mindmaps - mindmap_nodes = MindMapNode.query.filter( - db.and_( - MindMapNode.is_public == True, - db.or_( - MindMapNode.name.ilike(f'%{word}%') for word in user_message.split() - if len(word) > 3 - ) - ) - ).limit(5).all() + # 3. Kategorien-Statistiken + if any(kw in user_message for kw in ['kategorie', 'category', 'thema']): + category_count = Category.query.filter_by(parent_id=None).count() + subcategory_count = Category.query.filter(Category.parent_id != None).count() + + response += f"**Kategorien-Statistiken:**\n" + response += f"- Hauptkategorien: {category_count}\n" + response += f"- Unterkategorien: {subcategory_count}\n\n" + + main_categories = Category.query.filter_by(parent_id=None).all() + if main_categories: + response += "**Hauptkategorien:**\n" + for category in main_categories: + subcats = Category.query.filter_by(parent_id=category.id).count() + response += f"- {category.name} ({subcats} Unterkategorien)\n" + response += "\n" - if mindmap_nodes: - context.append("Relevante Mindmap-Knoten:") - for node in mindmap_nodes: - context.append(f"- Name: {node.name}") - context.append(f" Beschreibung: {node.description if node.description else 'Keine Beschreibung verfügbar'}") - if node.category: - context.append(f" Kategorie: {node.category.name}") - context.append("") - - return "\n".join(context) if context else "" + # 4. Mindmap-Statistiken + if any(kw in user_message for kw in ['mindmap', 'karte', 'map', 'knoten']): + public_nodes = MindMapNode.query.count() + user_mindmaps = UserMindmap.query.count() + + response += f"**Mindmap-Statistiken:**\n" + response += f"- Öffentliche Knoten: {public_nodes}\n" + response += f"- Benutzerdefinierte Mindmaps: {user_mindmaps}\n\n" + + if user_mindmaps > 0: + recent_mindmaps = UserMindmap.query.order_by(UserMindmap.created_at.desc()).limit(3).all() + + if recent_mindmaps: + response += "**Neueste Mindmaps:**\n" + for mindmap in recent_mindmaps: + response += f"- {mindmap.name} (von {mindmap.user.username})\n" + response += "\n" + + # Wenn keine spezifischen Daten angefordert wurden, allgemeine Übersicht geben + if not any(kw in user_message for kw in ['benutzer', 'user', 'gedanken', 'thought', 'kategorie', 'category', 'mindmap']): + users = User.query.count() + thoughts = Thought.query.count() + categories = Category.query.count() + nodes = MindMapNode.query.count() + user_maps = UserMindmap.query.count() + + response += f"**Übersicht über die Datenbank:**\n" + response += f"- Benutzer: {users}\n" + response += f"- Gedanken: {thoughts}\n" + response += f"- Kategorien: {categories}\n" + response += f"- Mindmap-Knoten: {nodes}\n" + response += f"- Benutzerdefinierte Mindmaps: {user_maps}\n" + + return True, response + + except Exception as e: + import traceback + print(f"Fehler bei der Datenbankabfrage: {e}") + print(traceback.format_exc()) + return True, "Es ist ein Fehler bei der Abfrage der Datenbank aufgetreten. Bitte versuchen Sie es später erneut." @app.route('/search') def search_thoughts_page():