Compare commits
240 Commits
feature/ta
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f943aea21 | |||
| 87c710a3e9 | |||
| bfbce1aad0 | |||
| 18c9ce8754 | |||
| c337e1bde1 | |||
| 444690d10c | |||
| e263df4280 | |||
| aab0499ff6 | |||
| 3fd3c5acfa | |||
| a3f79db2d2 | |||
| 8bc5f4cbb8 | |||
| 46301f9b8c | |||
| 5967f54a6f | |||
| 2c90da3ba5 | |||
| 9afe4148ba | |||
| f9e16bc8ad | |||
| 47973445dc | |||
| f6e00c97e0 | |||
| e8c751e987 | |||
| 3ee995173c | |||
| 2f8dd766cf | |||
| f1e0afe92e | |||
| fa3b139164 | |||
| 9fff5e4bde | |||
| b8e9a9c4f7 | |||
| 76f1fea4ba | |||
| 68ad818893 | |||
| 21b9d518fc | |||
| de576e2a9a | |||
| 78461b0e71 | |||
| a9b7dbaaca | |||
| 14237727b9 | |||
| 3336adf270 | |||
| 30b930f18a | |||
| d589c8ff39 | |||
| 1b7dda010d | |||
| 4ab7ee5447 | |||
| d68aa360e1 | |||
| 6d4a0564e6 | |||
| 8c136174d1 | |||
| 36252f4ea2 | |||
| 479a1cdb87 | |||
| d32e6bf7a4 | |||
| 42d9eb4660 | |||
| f38b76ffae | |||
| c467d62580 | |||
| 3a6183a85e | |||
| cb9ced2e3c | |||
| bd1657e7f4 | |||
| ff3932b3e1 | |||
| 7879b2193c | |||
| 6e0479f8bd | |||
| 44502e5b2b | |||
| 57081bf102 | |||
| 592d04a32a | |||
| ae2303b733 | |||
| efcaa57cf0 | |||
| 193b7b0e7d | |||
| 17346b3fcb | |||
|
|
262add256f | ||
|
|
6b6fe4db1f | ||
|
|
19f9ab64e6 | ||
| 0d60ad03de | |||
|
|
2e18552981 | ||
|
|
5c98d548e8 | ||
|
|
5e9c0e8d72 | ||
|
|
31dc43b62b | ||
|
|
e95aa786ba | ||
|
|
bf83fbcb65 | ||
|
|
2e33824b0b | ||
|
|
8431b9bf7d | ||
|
|
7f469a0ddf | ||
|
|
29f70a1cbb | ||
|
|
aa05f9e9b4 | ||
|
|
b8f9692eba | ||
|
|
5900ad028b | ||
|
|
31b2c24fdd | ||
|
|
4072e746cc | ||
|
|
dbc3054119 | ||
|
|
9add917f4e | ||
|
|
be62c7d0c8 | ||
|
|
42bbcd1425 | ||
| 6f558df8fa | |||
| 6144771329 | |||
| 00a524b2a9 | |||
| b446894c23 | |||
|
|
d2e9ee2a70 | ||
|
|
bb306c7717 | ||
| 05ccfbdb2d | |||
| 3f5f6a1387 | |||
| 7791100b7b | |||
| 6f70374b71 | |||
| 007755d2f6 | |||
| f5e661f9c0 | |||
| f14fcd8438 | |||
| 21d1da0a01 | |||
| 48d0191e3f | |||
|
|
82fb1a7804 | ||
|
|
275c284716 | ||
|
|
ed34b233ca | ||
|
|
4756fa3815 | ||
| 86038f0a4b | |||
| fb7347eb89 | |||
| 4eb4d033fd | |||
| a5daf1a059 | |||
| 76fae48b92 | |||
| 9316588246 | |||
| 185ec35e71 | |||
| 687cb9ef35 | |||
| 005c947dae | |||
| 1ea17695ec | |||
| 0821437407 | |||
| b236bbe29c | |||
| 0cc7309bf8 | |||
| 538e42b5ea | |||
| 2318bf322b | |||
| 395251dd9c | |||
| 938a81fd97 | |||
|
|
5d6ce40a7e | ||
| 41570534fa | |||
| ebbbcdb8ff | |||
|
|
c7a84b5a5b | ||
|
|
d28a8a2eee | ||
| 87a8a0829b | |||
| 79c3e3206b | |||
| ff62024ef7 | |||
| 3d75faea48 | |||
|
|
3e9d9eac6a | ||
|
|
dd55e080c3 | ||
|
|
9718c318f0 | ||
|
|
43733f8456 | ||
|
|
dfca6245ed | ||
|
|
c3745bd648 | ||
| e6a3a24750 | |||
| a69fd055ba | |||
| d3cc2cb3c0 | |||
| eb4e4fb2d7 | |||
| fc637cdbfd | |||
| 1862430fd8 | |||
| 6101f8c2e5 | |||
| a14ae0aa27 | |||
| 7214f7a687 | |||
| 6a63330c25 | |||
| 377271f194 | |||
| 635d35cb81 | |||
| d2e3d5f9e0 | |||
| 487031d43a | |||
| 89fa639883 | |||
| e9416f1c82 | |||
| a9e327fa26 | |||
| 4876b0317f | |||
| 030c66b258 | |||
| a00951500e | |||
| f1a960f524 | |||
| c84531ed70 | |||
| d544087ee4 | |||
| 48d79f53fb | |||
| 2c05412dfb | |||
| 66438fd6d0 | |||
| 365a92f9ac | |||
| 5f1064754c | |||
| 5b19ca31f6 | |||
| 04aa373c1a | |||
| 9ca50cbb0e | |||
| 571c125e9f | |||
| 7d94a34841 | |||
| 7215fed572 | |||
| 98c6b79086 | |||
| 64d3285320 | |||
| b2b7f7dc21 | |||
| 3a30703342 | |||
| bbafb8562c | |||
| 8bec665ac5 | |||
| 3537347e72 | |||
| 3a85514820 | |||
| 9983df54aa | |||
| 72ed7d5651 | |||
| 644553ef64 | |||
| 14eceb5d18 | |||
|
|
e24de492c7 | ||
|
|
8b5387e4cd | ||
|
|
36cb4bfbae | ||
|
|
71d384770b | ||
|
|
167f1d5b5b | ||
|
|
8205d4ecea | ||
| 457e64a930 | |||
| 8894f086c9 | |||
|
|
da9f0d1edd | ||
|
|
738cf9f847 | ||
|
|
7ee65e0b1f | ||
|
|
3a92087bb2 | ||
|
|
5d3080ba74 | ||
|
|
8cbac74b2f | ||
| 8347d5c7ae | |||
| a7cedb7180 | |||
|
|
5ad0389aaa | ||
|
|
4c391d4e81 | ||
|
|
7af388438f | ||
|
|
9e4c94ac1e | ||
|
|
9e556bace8 | ||
|
|
3a205be8eb | ||
|
|
0f0033ff9b | ||
| 14dd48b22a | |||
|
|
b8fde5ceb4 | ||
|
|
97af86e509 | ||
|
|
6ab2f10942 | ||
| 6ce3ca84eb | |||
| 6c3f033ebb | |||
| 136ed96d26 | |||
| 3f30acc9d0 | |||
| dbc1b4f2f7 | |||
| 7dc0afeed9 | |||
| 107c9dd185 | |||
| cb40d348ed | |||
| 8b3007df23 | |||
| 3510e73c62 | |||
| 2ca96d73f5 | |||
| 9caa9ecf88 | |||
| da8591ff2b | |||
| b184cf1d0f | |||
| 2e75fba71f | |||
| d3f913d3de | |||
| 59c778c602 | |||
| ea1bf6bacd | |||
| beb5f2df93 | |||
| 59ddf2690f | |||
| 94c5746c28 | |||
| fa9220e90c | |||
| 6178fbcb23 | |||
| 8c281551c8 | |||
| d418f48481 | |||
| c2b1c3d374 | |||
| 84c2af377f | |||
| ecab43ed65 | |||
| 5f3ff4a734 | |||
| 5de9486dc5 | |||
| d67245c50a | |||
| ea9bb7c40c | |||
| fff89ff012 | |||
| 5f391a2caa |
@@ -1 +1 @@
|
|||||||
{"task_id": "2f488f42-8544-81ac-a9f8-e373c4c18115", "token": "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", "session_start_time": "2026-01-26T18:39:11.157549"}
|
{"task_id": "30388f42-8544-8088-bc48-e59e9b973e91", "token": "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", "readme_path": "readme.md", "session_start_time": "2026-03-10T19:23:35.891681"}
|
||||||
41
.env.example
Normal file
41
.env.example
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# GTM Engine - Environment Configuration Template
|
||||||
|
# ==========================================
|
||||||
|
# GTM Engine - Environment Configuration
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# E-Mail Generation & Lead Engine
|
||||||
|
FEEDBACK_SERVER_BASE_URL=https://floke-ai.duckdns.org/feedback
|
||||||
|
WORDPRESS_BOOKING_URL=https://www.robo-planet.de/terminbestaetigung
|
||||||
|
MS_BOOKINGS_URL=https://outlook.office365.com/owa/calendar/IHR_BOOKINGS_NAME@robo-planet.de/bookings/
|
||||||
|
# Copy this file to .env and fill in the actual values.
|
||||||
|
|
||||||
|
# --- Core API Keys ---
|
||||||
|
GEMINI_API_KEY=AI...
|
||||||
|
OPENAI_API_KEY=sk-...
|
||||||
|
SERP_API=...
|
||||||
|
|
||||||
|
# --- Notion Integration ---
|
||||||
|
NOTION_API_KEY=ntn_...
|
||||||
|
|
||||||
|
# --- SuperOffice API Credentials ---
|
||||||
|
SO_CLIENT_ID=...
|
||||||
|
SO_CLIENT_SECRET=...
|
||||||
|
SO_REFRESH_TOKEN=...
|
||||||
|
SO_ENVIRONMENT=online3
|
||||||
|
SO_CONTEXT_IDENTIFIER=Cust...
|
||||||
|
|
||||||
|
# --- Application Settings ---
|
||||||
|
API_USER=admin
|
||||||
|
API_PASSWORD=gemini
|
||||||
|
APP_BASE_URL=http://localhost:8090
|
||||||
|
|
||||||
|
# --- Infrastructure ---
|
||||||
|
DUCKDNS_TOKEN=...
|
||||||
|
# SUBDOMAINS=floke,floke-ai... (defined in docker-compose)
|
||||||
|
|
||||||
|
# --- Feature Flags ---
|
||||||
|
ENABLE_WEBSITE_SYNC=False
|
||||||
|
|
||||||
|
# --- Advanced Mappings (Optional Overrides) ---
|
||||||
|
# VERTICAL_MAP_JSON='{"Industry": ID, ...}'
|
||||||
|
# PERSONA_MAP_JSON='{"Role": ID, ...}'
|
||||||
1
.gemini/env_old
Normal file
1
.gemini/env_old
Normal file
@@ -0,0 +1 @@
|
|||||||
|
GEMINI_API_KEY=AIzaSyBNg5yQ-dezfDs6j9DGn8qJ8SImNCGm9Ds
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -68,3 +68,7 @@ Log_from_docker/
|
|||||||
# Node.js specific
|
# Node.js specific
|
||||||
!package.json
|
!package.json
|
||||||
!package-lock.json
|
!package-lock.json
|
||||||
|
|
||||||
|
.gemini/.env
|
||||||
|
gemini_api_key.txt
|
||||||
|
*.tar.gz
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
admin:$1$RzTlC0sX$L2VQ31MyQ0Wefz1vNG7Yf1
|
admin:$6$un97dUtWx4rc/Qcr$Xo7oatwiWn8F7lPiSCHI56K3OK7k0rHztVp2kfl78Kk6juw5KTwWlwU07PGgDGY5mQiGZzDy4O0UhIVvR5HsC.
|
||||||
614
ARCHIVE_legacy_scripts/README_LEGACY.md
Normal file
614
ARCHIVE_legacy_scripts/README_LEGACY.md
Normal file
@@ -0,0 +1,614 @@
|
|||||||
|
# Legacy Dokumentation: Monolithische Python-Skripte (v2.x)
|
||||||
|
|
||||||
|
**⚠️ HINWEIS:** Diese Dokumentation ist ein Archiv der ursprünglichen `readme.md`. Sie beschreibt die alte, skript-basierte Architektur (Abschnitte 1-8). Die Dokumentation für die aktiven Systeme (Company Explorer, Market Intelligence) wurde in die jeweiligen Unterordner verschoben.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Projektübersicht & Architektur (Alt)
|
||||||
|
|
||||||
|
Dieses Projekt war ursprünglich eine modulare "Lead Enrichment Factory" auf Basis von CLI-Skripten.
|
||||||
|
|
||||||
|
### Architektur im Überblick
|
||||||
|
|
||||||
|
```text
|
||||||
|
I. DIE STEUERUNGS-EBENE (API & Ausführung)
|
||||||
|
└── app.py (Flask API Server, startet Jobs)
|
||||||
|
└── brancheneinstufung2.py (Der zentrale Orchestrator / Kommandozeile)
|
||||||
|
|
||||||
|
II. DIE KERN-PRODUKTIONSLINIE (Datenanreicherung)
|
||||||
|
└── data_processor.py (Der "Motor", führt die Arbeit aus)
|
||||||
|
├── google_sheet_handler.py (Spezialist für Google Sheets)
|
||||||
|
├── wikipedia_scraper.py (Spezialist für Wikipedia-Daten)
|
||||||
|
├── sync_manager.py (Spezialist für den D365-Abgleich)
|
||||||
|
└── helpers.py (Der "Werkzeugkasten" für alle)
|
||||||
|
|
||||||
|
III. DIE MARKETING-PRODUKTIONSLINIE (Content-Erstellung)
|
||||||
|
└── generate_marketing_text.py (Erstellt E-Mail-Texte)
|
||||||
|
└── INPUT: marketing_wissen_final.yaml (Die Wissensbasis)
|
||||||
|
|
||||||
|
IV. DIE WISSENSBASIS-FABRIK (ETL-Pipelines zur Erstellung der Wissensbasis)
|
||||||
|
├── build_knowledge_base.py (Baut die Marketing-KB aus der config.py)
|
||||||
|
├── expand_knowledge_base.py (Erweitert die Marketing-KB)
|
||||||
|
├── extract_insights.py (Baut die Marketing-KB aus Word-Dokumenten)
|
||||||
|
└── generate_knowledge_base.py (Erstellt einen Entwurf für die Marketing-KB)
|
||||||
|
|
||||||
|
V. DAS KLASSIFIZIERUNGS-SYSTEM (Job-Titel-Analyse)
|
||||||
|
├── contact_grouping.py (Klassifiziert Job-Titel)
|
||||||
|
└── knowledge_base_builder.py (Baut die Wissensbasis FÜR die Klassifizierung)
|
||||||
|
|
||||||
|
VI. DAS STANDALONE-WERKZEUG
|
||||||
|
└── company_deduplicator.py (Eigenständiger Duplikats-Check für externe und interne Listen)
|
||||||
|
|
||||||
|
IX. DAS FUNDAMENT
|
||||||
|
└── config.py (Einstellungen & Konstanten für ALLE)
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## 2. Steuerung & Ausführung (Control & Execution)
|
||||||
|
|
||||||
|
Diese Ebene bildet die Schnittstelle zur Außenwelt und startet die verschiedenen Prozesse.
|
||||||
|
|
||||||
|
### `app.py` (API Server)
|
||||||
|
|
||||||
|
#### Hauptfunktion
|
||||||
|
|
||||||
|
Das Modul `app.py` implementiert eine einfache Flask-Webanwendung, die als API-Endpunkt für das gesamte System dient. Es ermöglicht das Starten von langlaufenden Skripten (wie dem Duplikats-Check oder der Branchen-Neuklassifizierung) als asynchrone Hintergrundprozesse. Die Anwendung verwaltet diese Prozesse über eindeutige Job-IDs und bietet Endpunkte zum Starten von Aktionen und zum Abrufen des Status dieser Jobs. Dies ermöglicht eine lose Kopplung und die Steuerung des Systems durch externe Aufrufe, z.B. aus einem Frontend oder einem anderen Service.
|
||||||
|
|
||||||
|
#### Methodenbeschreibung
|
||||||
|
|
||||||
|
- `status_check()`: Ein einfacher `/status`-Endpunkt (GET), der eine "ok"-Nachricht zurückgibt. Dient als Health-Check, um zu überprüfen, ob die Flask-Anwendung läuft.
|
||||||
|
- `setup_ngrok()`: Eine Hilfsfunktion, die einen `ngrok`-Tunnel startet, um den lokalen Flask-Server über eine öffentliche URL erreichbar zu machen. Dies ist nützlich für das Testen und die Demonstration in Umgebungen ohne öffentliche IP.
|
||||||
|
- `run_script()`: Der Hauptendpunkt `/run-script` (POST), der eine Aktion entgegennimmt (z.B. "run_duplicate_check"). Basierend auf der Aktion wird das entsprechende Python-Skript (`duplicate_checker.py`, `brancheneinstufung2.py`, etc.) als separater Hintergrundprozess gestartet. Für jeden gestarteten Prozess wird eine eindeutige Job-ID generiert und eine initiale Statusdatei im `job_status`-Verzeichnis erstellt.
|
||||||
|
- `get_status()`: Ein `/get-status`-Endpunkt (GET), der den Inhalt aller im `job_status`-Verzeichnis gespeicherten JSON-Dateien ausliest. Dies ermöglicht es, den Fortschritt und den aktuellen Status aller gestarteten und abgeschlossenen Jobs abzufragen. Die Ergebnisse werden als JSON-Array zurückgegeben, wobei die neuesten Jobs zuerst aufgeführt sind.
|
||||||
|
|
||||||
|
### `brancheneinstufung2.py` (Orchestrator / Kommandozeile)
|
||||||
|
|
||||||
|
### Hauptfunktion
|
||||||
|
Das Skript `brancheneinstufung2.py` ist der zentrale Einstiegspunkt und Orchestrator für die Datenanreicherungs-Pipeline. Es dient nicht der direkten Implementierung der Verarbeitungslogik, sondern dem Parsen von Kommandozeilen-Argumenten, der Initialisierung von Handler-Klassen (`GoogleSheetHandler`, `WikipediaScraper`, `DataProcessor`, `SyncManager`) und dem Starten des vom Benutzer ausgewählten Verarbeitungsmodus. Das Skript ist hochgradig modular und delegiert die eigentliche Arbeit an die `DataProcessor`-Klasse.
|
||||||
|
|
||||||
|
### Abhängigkeiten
|
||||||
|
- **Standardbibliotheken**: `logging`, `os`, `argparse`, `time`, `datetime`.
|
||||||
|
- **Lokale Module**:
|
||||||
|
- `config.py`: Stellt globale Konfigurationsparameter und die Projektversion bereit.
|
||||||
|
- `helpers.py`: Bietet Hilfsfunktionen, z.B. für das Erstellen von Log-Dateinamen und das Initialisieren des Branchenschemas.
|
||||||
|
- `google_sheet_handler.py`: Kapselt die gesamte Kommunikation mit der Google Sheets API.
|
||||||
|
- `wikipedia_scraper.py`: Stellt Methoden zum Suchen und Parsen von Wikipedia-Artikeln bereit.
|
||||||
|
- `data_processor.py`: Enthält die Kernlogik für alle Datenverarbeitungsschritte.
|
||||||
|
- `sync_manager.py`: Implementiert die Logik zum Abgleich von CRM-Exporten mit dem Google Sheet.
|
||||||
|
|
||||||
|
### Wichtige Funktionen/Methoden
|
||||||
|
Die Logik des Skripts ist primär in der `main()`-Funktion enthalten und lässt sich in folgende Schritte unterteilen:
|
||||||
|
|
||||||
|
1. **Argumenten-Parsing**: Mittels `argparse` wird die Kommandozeile ausgewertet. Der wichtigste Parameter ist `--mode`, der den auszuführenden Prozess festlegt (z.B. `sync`, `full_run`, `reeval`, `predict_technicians`). Weitere Parameter wie `--limit` oder `--start_sheet_row` erlauben eine feingranulare Steuerung der zu verarbeitenden Datenmenge.
|
||||||
|
2. **Interaktiver Modus**: Wird kein `--mode` übergeben, startet ein interaktiver Modus, in dem der Benutzer aus einer Liste aller verfügbaren Modi auswählen kann.
|
||||||
|
3. **Initialisierung**: Basierend auf dem gewählten Modus werden die notwendigen Klassen instanziiert. Für die meisten Anreicherungs-Modi wird ein `DataProcessor`-Objekt erstellt, das wiederum Instanzen des `GoogleSheetHandler` und `WikipediaScraper` erhält. Für den `sync`-Modus wird der `SyncManager` verwendet.
|
||||||
|
4. **Modus-Dispatching**: Eine `if/elif/else`-Struktur leitet die Ausführung an die passende Methode weiter.
|
||||||
|
- **`sync` / `simulate_sync`**: Ruft den `SyncManager` auf, um einen D365-Excel-Export mit dem Google Sheet abzugleichen.
|
||||||
|
- **`full_run`**: Startet einen sequentiellen Anreicherungsprozess für Zeilen, die noch nicht verarbeitet wurden.
|
||||||
|
- **`reeval`**: Verarbeitet Zeilen, die explizit für eine Neubewertung markiert wurden.
|
||||||
|
- **`reclassify_branches`**: Führt eine Neubewertung der Branchenzuordnung für alle oder eine Teilmenge der Zeilen durch.
|
||||||
|
- **`train_technician_model` / `predict_technicians`**: Steuert das Training und die Anwendung eines Machine-Learning-Modells zur Vorhersage von Techniker-Anzahlen.
|
||||||
|
- **Dynamische Aufrufe**: Viele Modi (z.B. `wiki_verify`, `website_scraping`) werden dynamisch aufgerufen, indem Methoden des `DataProcessor`-Objekts ausgeführt werden, deren Namen dem Modus entsprechen (z.B. `process_wiki_verify`).
|
||||||
|
5. **Fehlerbehandlung und Logging**: Das gesamte Skript ist von einem `try...except...finally`-Block umschlossen, der Fehler abfängt, protokolliert und sicherstellt, dass das Logging am Ende sauber beendet wird.
|
||||||
|
|
||||||
|
## Kernlogik (data_processor.py)
|
||||||
|
|
||||||
|
Die Klasse `DataProcessor` ist das Herzstück der Anreicherungslogik und enthält alle Methoden zur schrittweisen Verarbeitung von Unternehmensdaten.
|
||||||
|
|
||||||
|
### `setup()`
|
||||||
|
- **Zweck:** Initialisiert den `DataProcessor`, indem das Ziel-Branchenschema geladen und das trainierte Machine-Learning-Modell für die Technikerschätzung in den Speicher geladen wird.
|
||||||
|
- **Input:** Keine direkten Argumente, greift aber auf die Konfigurationsdateien (`ziel_Branchenschema.csv`, `technician_decision_tree_model.pkl` etc.) zu.
|
||||||
|
- **Output:** Gibt `True` bei Erfolg zurück und setzt das interne Flag `is_setup_complete`. Bei Fehlern wird `False` zurückgegeben und das Skript beendet.
|
||||||
|
|
||||||
|
### `process_rows_sequentially(...)`
|
||||||
|
- **Zweck:** Führt den vollständigen, schrittweisen Anreicherungsprozess für einen definierten Bereich von Zeilen im Google Sheet aus. Dies ist der Hauptmodus für die initiale Verarbeitung neuer Daten.
|
||||||
|
- **Input:** `start_sheet_row` (Startzeile), `num_to_process` (Anzahl der Zeilen), sowie boolesche Flags (`process_wiki_steps`, `process_chatgpt_steps`, etc.) zur Steuerung der auszuführenden Anreicherungsschritte.
|
||||||
|
- **Output:** Die Methode hat keinen direkten Rückgabewert, aber ihr Seiteneffekt ist die Aktualisierung der verarbeiteten Zeilen im Google Sheet mit den angereicherten Daten.
|
||||||
|
|
||||||
|
### `process_reevaluation_rows(...)`
|
||||||
|
- **Zweck:** Führt den vollständigen Anreicherungsprozess gezielt für Zeilen aus, die im Google Sheet manuell mit einem 'x' in der "ReEval Flag"-Spalte markiert wurden. Vor der Neubewertung werden alle zuvor abgeleiteten Daten in der Zeile gelöscht.
|
||||||
|
- **Input:** `row_limit` (maximale Anzahl zu verarbeitender Zeilen), `clear_flag` (ob das 'x'-Flag nach der Verarbeitung entfernt werden soll) und die booleschen Flags zur Schritt-Steuerung.
|
||||||
|
- **Output:** Aktualisiert die markierten Zeilen im Google Sheet mit neu angereicherten Daten.
|
||||||
|
|
||||||
|
### `process_website_scraping(...)`
|
||||||
|
- **Zweck:** Führt einen Batch-Prozess ausschließlich für das Scrapen von Websites durch. Die Methode identifiziert Zeilen, in denen der "Website Scrape Timestamp" fehlt, und extrahiert parallel den Rohtext und die Meta-Details der jeweiligen Unternehmens-Websites.
|
||||||
|
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit` zur Eingrenzung des Verarbeitungsbereichs.
|
||||||
|
- **Output:** Schreibt den extrahierten Rohtext, Meta-Details, einen Status zur URL-Prüfung und einen Zeitstempel in die entsprechenden Spalten im Google Sheet.
|
||||||
|
|
||||||
|
### `process_summarize_website(...)`
|
||||||
|
- **Zweck:** Führt einen Batch-Prozess zur Zusammenfassung von bereits extrahierten Website-Rohtexten mittels KI durch.
|
||||||
|
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`. Die Methode verarbeitet Zeilen, bei denen der Rohtext vorhanden, aber die Zusammenfassung noch leer ist.
|
||||||
|
- **Output:** Schreibt die KI-generierte Zusammenfassung in die Spalte "Website Zusammenfassung" im Google Sheet.
|
||||||
|
|
||||||
|
### `process_branch_batch(...)` / `reclassify_all_branches(...)`
|
||||||
|
- **Zweck:** Führt eine (Neu-)Bewertung der Branchenzugehörigkeit für einen Zeilenbereich im Batch-Verfahren durch. Die Methode sammelt relevante Informationen (CRM-Daten, Website-Zusammenfassung, Wikipedia-Inhalte) und sendet sie gebündelt an die KI, um eine effiziente und konsistente Brancheneinstufung zu erhalten.
|
||||||
|
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`.
|
||||||
|
- **Output:** Aktualisiert die Spalten zur KI-basierten Brancheneinstufung ("Chat Vorschlag Branche", Konfidenz, Begründung etc.) im Google Sheet.
|
||||||
|
|
||||||
|
### `process_wiki_verify(...)`
|
||||||
|
- **Zweck:** Verifiziert in einem Batch-Prozess, ob der in einer Zeile hinterlegte Wikipedia-Artikel thematisch zum Unternehmen passt. Nutzt KI, um die Konsistenz zu prüfen und ggf. einen passenderen Artikel vorzuschlagen.
|
||||||
|
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`. Verarbeitet Zeilen, die eine Wiki-URL, aber noch keinen Verifizierungs-Timestamp haben.
|
||||||
|
- **Output:** Schreibt das Ergebnis der KI-Prüfung (OK, X), eine Begründung und ggf. einen Alternativvorschlag in die entsprechenden Spalten im Google Sheet.
|
||||||
|
|
||||||
|
### `process_find_wiki_serp(...)`
|
||||||
|
- **Zweck:** Sucht für große Unternehmen (definiert über Mindestumsatz/-mitarbeiter), bei denen noch keine Wikipedia-URL bekannt ist, über eine Google-Suche (SerpAPI) nach einem passenden Wikipedia-Artikel.
|
||||||
|
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`, `min_employees`, `min_umsatz`.
|
||||||
|
- **Output:** Wenn eine passende URL gefunden wird, wird diese in die "Wiki URL"-Spalte eingetragen und die Zeile für eine Neubewertung (`ReEval Flag`) markiert.
|
||||||
|
|
||||||
|
### `process_contact_search(...)`
|
||||||
|
- **Zweck:** Führt eine Suche nach LinkedIn-Kontakten für bestimmte Positionen (z.B. Serviceleiter, IT-Leiter) bei den Unternehmen im Sheet durch.
|
||||||
|
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`. Verarbeitet Zeilen, die noch keinen "Contact Search Timestamp" haben.
|
||||||
|
- **Output:** Schreibt die Anzahl der gefundenen Kontakte pro Kategorie in die "Linked... gefunden"-Spalten und speichert detaillierte Kontaktinformationen in einem separaten "Contacts"-Tabellenblatt.
|
||||||
|
|
||||||
|
### `train_technician_model()`
|
||||||
|
- **Zweck:** Bereitet die vorhandenen Daten auf, trainiert ein Machine-Learning-Modell (RandomForest) zur Vorhersage von Servicetechniker-Anzahlen, führt ein Hyperparameter-Tuning durch und speichert das trainierte Modell sowie den zugehörigen Imputer als `.pkl`-Dateien.
|
||||||
|
- **Input:** Keine direkten Argumente. Die Methode liest die Trainingsdaten aus dem konfigurierten Google Sheet.
|
||||||
|
- **Output:** Erstellt und speichert die `technician_decision_tree_model.pkl`- und `median_imputer.pkl`-Dateien sowie eine JSON-Datei mit den verwendeten Features.
|
||||||
|
|
||||||
|
### `process_predict_technicians(...)`
|
||||||
|
- **Zweck:** Wendet das trainierte ML-Modell auf Zeilen an, für die noch keine Techniker-Schätzung vorliegt, aber die notwendigen Input-Features (Umsatz, Mitarbeiter, Branche) vorhanden sind.
|
||||||
|
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`.
|
||||||
|
- **Output:** Schreibt den vorhergesagten Techniker-Bucket (z.B. "Techniker_Klein (0-49)") in die Spalte "Geschaetzter Techniker Bucket".
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## 3. Kern-Produktionslinie: Datenanreicherung
|
||||||
|
|
||||||
|
Dies ist das Herzstück der Fabrik, in dem die eigentliche Anreicherung der Unternehmensdaten stattfindet.
|
||||||
|
|
||||||
|
### `data_processor.py` (Der "Motor")
|
||||||
|
|
||||||
|
#### Hauptfunktion
|
||||||
|
Die Klasse `DataProcessor` ist das Herzstück der Anreicherungslogik und enthält alle Methoden zur schrittweisen Verarbeitung von Unternehmensdaten. Sie wird vom Orchestrator (`brancheneinstufung2.py`) aufgerufen und nutzt verschiedene Spezialisten-Module, um ihre Aufgaben zu erfüllen.
|
||||||
|
|
||||||
|
Die Klasse `DataProcessor` ist das Herzstück der Anreicherungslogik und enthält alle Methoden zur schrittweisen Verarbeitung von Unternehmensdaten.
|
||||||
|
|
||||||
|
### `setup()`
|
||||||
|
- **Zweck:** Initialisiert den `DataProcessor`, indem das Ziel-Branchenschema geladen und das trainierte Machine-Learning-Modell für die Technikerschätzung in den Speicher geladen wird.
|
||||||
|
- **Input:** Keine direkten Argumente, greift aber auf die Konfigurationsdateien (`ziel_Branchenschema.csv`, `technician_decision_tree_model.pkl` etc.) zu.
|
||||||
|
- **Output:** Gibt `True` bei Erfolg zurück und setzt das interne Flag `is_setup_complete`. Bei Fehlern wird `False` zurückgegeben und das Skript beendet.
|
||||||
|
|
||||||
|
### `process_rows_sequentially(...)`
|
||||||
|
- **Zweck:** Führt den vollständigen, schrittweisen Anreicherungsprozess für einen definierten Bereich von Zeilen im Google Sheet aus. Dies ist der Hauptmodus für die initiale Verarbeitung neuer Daten.
|
||||||
|
- **Input:** `start_sheet_row` (Startzeile), `num_to_process` (Anzahl der Zeilen), sowie boolesche Flags (`process_wiki_steps`, `process_chatgpt_steps`, etc.) zur Steuerung der auszuführenden Anreicherungsschritte.
|
||||||
|
- **Output:** Die Methode hat keinen direkten Rückgabewert, aber ihr Seiteneffekt ist die Aktualisierung der verarbeiteten Zeilen im Google Sheet mit den angereicherten Daten.
|
||||||
|
|
||||||
|
### `process_reevaluation_rows(...)`
|
||||||
|
- **Zweck:** Führt den vollständigen Anreicherungsprozess gezielt für Zeilen aus, die im Google Sheet manuell mit einem 'x' in der "ReEval Flag"-Spalte markiert wurden. Vor der Neubewertung werden alle zuvor abgeleiteten Daten in der Zeile gelöscht.
|
||||||
|
- **Input:** `row_limit` (maximale Anzahl zu verarbeitender Zeilen), `clear_flag` (ob das 'x'-Flag nach der Verarbeitung entfernt werden soll) und die booleschen Flags zur Schritt-Steuerung.
|
||||||
|
- **Output:** Aktualisiert die markierten Zeilen im Google Sheet mit neu angereicherten Daten.
|
||||||
|
|
||||||
|
### `process_website_scraping(...)`
|
||||||
|
- **Zweck:** Führt einen Batch-Prozess ausschließlich für das Scrapen von Websites durch. Die Methode identifiziert Zeilen, in denen der "Website Scrape Timestamp" fehlt, und extrahiert parallel den Rohtext und die Meta-Details der jeweiligen Unternehmens-Websites.
|
||||||
|
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit` zur Eingrenzung des Verarbeitungsbereichs.
|
||||||
|
- **Output:** Schreibt den extrahierten Rohtext, Meta-Details, einen Status zur URL-Prüfung und einen Zeitstempel in die entsprechenden Spalten im Google Sheet.
|
||||||
|
|
||||||
|
### `process_summarize_website(...)`
|
||||||
|
- **Zweck:** Führt einen Batch-Prozess zur Zusammenfassung von bereits extrahierten Website-Rohtexten mittels KI durch.
|
||||||
|
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`. Die Methode verarbeitet Zeilen, bei denen der Rohtext vorhanden, aber die Zusammenfassung noch leer ist.
|
||||||
|
- **Output:** Schreibt die KI-generierte Zusammenfassung in die Spalte "Website Zusammenfassung" im Google Sheet.
|
||||||
|
|
||||||
|
### `process_branch_batch(...)` / `reclassify_all_branches(...)`
|
||||||
|
- **Zweck:** Führt eine (Neu-)Bewertung der Branchenzugehörigkeit für einen Zeilenbereich im Batch-Verfahren durch. Die Methode sammelt relevante Informationen (CRM-Daten, Website-Zusammenfassung, Wikipedia-Inhalte) und sendet sie gebündelt an die KI, um eine effiziente und konsistente Brancheneinstufung zu erhalten.
|
||||||
|
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`.
|
||||||
|
- **Output:** Aktualisiert die Spalten zur KI-basierten Brancheneinstufung ("Chat Vorschlag Branche", Konfidenz, Begründung etc.) im Google Sheet.
|
||||||
|
|
||||||
|
### `process_wiki_verify(...)`
|
||||||
|
- **Zweck:** Verifiziert in einem Batch-Prozess, ob der in einer Zeile hinterlegte Wikipedia-Artikel thematisch zum Unternehmen passt. Nutzt KI, um die Konsistenz zu prüfen und ggf. einen passenderen Artikel vorzuschlagen.
|
||||||
|
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`. Verarbeitet Zeilen, die eine Wiki-URL, aber noch keinen Verifizierungs-Timestamp haben.
|
||||||
|
- **Output:** Schreibt das Ergebnis der KI-Prüfung (OK, X), eine Begründung und ggf. einen Alternativvorschlag in die entsprechenden Spalten im Google Sheet.
|
||||||
|
|
||||||
|
### `process_find_wiki_serp(...)`
|
||||||
|
- **Zweck:** Sucht für große Unternehmen (definiert über Mindestumsatz/-mitarbeiter), bei denen noch keine Wikipedia-URL bekannt ist, über eine Google-Suche (SerpAPI) nach einem passenden Wikipedia-Artikel.
|
||||||
|
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`, `min_employees`, `min_umsatz`.
|
||||||
|
- **Output:** Wenn eine passende URL gefunden wird, wird diese in die "Wiki URL"-Spalte eingetragen und die Zeile für eine Neubewertung (`ReEval Flag`) markiert.
|
||||||
|
|
||||||
|
### `process_contact_search(...)`
|
||||||
|
- **Zweck:** Führt eine Suche nach LinkedIn-Kontakten für bestimmte Positionen (z.B. Serviceleiter, IT-Leiter) bei den Unternehmen im Sheet durch.
|
||||||
|
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`. Verarbeitet Zeilen, die noch keinen "Contact Search Timestamp" haben.
|
||||||
|
- **Output:** Schreibt die Anzahl der gefundenen Kontakte pro Kategorie in die "Linked... gefunden"-Spalten und speichert detaillierte Kontaktinformationen in einem separaten "Contacts"-Tabellenblatt.
|
||||||
|
|
||||||
|
### `train_technician_model()`
|
||||||
|
- **Zweck:** Bereitet die vorhandenen Daten auf, trainiert ein Machine-Learning-Modell (RandomForest) zur Vorhersage von Servicetechniker-Anzahlen, führt ein Hyperparameter-Tuning durch und speichert das trainierte Modell sowie den zugehörigen Imputer als `.pkl`-Dateien.
|
||||||
|
- **Input:** Keine direkten Argumente. Die Methode liest die Trainingsdaten aus dem konfigurierten Google Sheet.
|
||||||
|
- **Output:** Erstellt und speichert die `technician_decision_tree_model.pkl`- und `median_imputer.pkl`-Dateien sowie eine JSON-Datei mit den verwendeten Features.
|
||||||
|
|
||||||
|
### `process_predict_technicians(...)`
|
||||||
|
- **Zweck:** Wendet das trainierte ML-Modell auf Zeilen an, für die noch keine Techniker-Schätzung vorliegt, aber die notwendigen Input-Features (Umsatz, Mitarbeiter, Branche) vorhanden sind.
|
||||||
|
- **Input:** `start_sheet_row`, `end_sheet_row`, `limit`.
|
||||||
|
- **Output:** Schreibt den vorhergesagten Techniker-Bucket (z.B. "Techniker_Klein (0-49)") in die Spalte "Geschaetzter Techniker Bucket".
|
||||||
|
|
||||||
|
### Spezialisten-Module
|
||||||
|
|
||||||
|
#### `google_sheet_handler.py` (Spezialist für Google Sheets)
|
||||||
|
|
||||||
|
##### Hauptfunktion
|
||||||
|
Das Modul `google_sheet_handler.py` dient als zentraler Wrapper für sämtliche Interaktionen mit dem Google Sheet, das als primärer Datenspeicher für das Projekt fungiert. Es abstrahiert die Komplexität der `gspread` API und stellt eine robuste, wiederverwendbare Schnittstelle für Lese- und Schreibvorgänge bereit. Die Klasse `GoogleSheetHandler` kapselt die Verbindungslogik, die Authentifizierung über Service-Accounts und bietet Methoden für spezifische Datenmanipulationen.
|
||||||
|
|
||||||
|
### Methodenbeschreibung
|
||||||
|
|
||||||
|
- `__init__(self, sheet_url=None)`: Der Konstruktor initialisiert den Handler. Er übernimmt optional eine `sheet_url`. Wenn keine URL angegeben wird, greift er auf den Wert aus der `config.py` zurück.
|
||||||
|
|
||||||
|
- `load_data(self)`: Stellt die Verbindung zum Google Sheet her (falls noch nicht geschehen) und lädt den gesamten Inhalt des Haupt-Arbeitsblatts in den internen Speicher der Klasse. Diese Methode ist mit einem Retry-Decorator versehen, um bei temporären Netzwerkproblemen robust zu sein.
|
||||||
|
|
||||||
|
- `get_sheet_as_dataframe(self, sheet_name)`: Liest ein spezifisches Arbeitsblatt (über `sheet_name` identifiziert) aus dem Google Sheet und gibt dessen Inhalt als Pandas DataFrame zurück. Dies ist nützlich für datenanalytische Aufgaben.
|
||||||
|
|
||||||
|
- `append_rows(self, sheet_name, values)`: Fügt eine oder mehrere neue Zeilen am Ende eines bestimmten Arbeitsblatts an. `values` ist dabei eine Liste von Listen (jede innere Liste repräsentiert eine Zeile).
|
||||||
|
|
||||||
|
- `clear_and_write_data(self, sheet_name, data)`: Löscht den gesamten Inhalt eines Arbeitsblatts und schreibt anschließend neue Daten hinein. Dies ist nützlich, um ein Sheet vollständig zu synchronisieren.
|
||||||
|
|
||||||
|
- `batch_update_cells(self, update_data)`: Führt eine Stapelverarbeitung von Zell-Updates durch. Diese Methode ist wesentlich performanter als einzelne Zell-Updates, da sie mehrere Änderungen in einer einzigen API-Anfrage bündelt.
|
||||||
|
|
||||||
|
- `get_main_sheet_name(self)`: Gibt den Namen des Haupt-Arbeitsblatts (typischerweise 'Tabelle1') zurück.
|
||||||
|
|
||||||
|
#### `wikipedia_scraper.py` (Spezialist für Wikipedia)
|
||||||
|
|
||||||
|
##### Hauptfunktion
|
||||||
|
Das Modul `wikipedia_scraper.py` kapselt alle Interaktionen mit Wikipedia. Seine Hauptaufgabe ist es, für ein gegebenes Unternehmen den relevantesten Wikipedia-Artikel zu finden, diesen zu validieren und anschließend strukturierte Daten wie den Unternehmenssitz, die Branche, den Umsatz und die Mitarbeiterzahl aus dem Artikel zu extrahieren. Es verwendet eine "Google-First"-Strategie, bei der die SerpAPI zur Identifizierung des wahrscheinlichsten Artikels genutzt wird, bevor eine detaillierte, faktenbasierte Validierung erfolgt.
|
||||||
|
|
||||||
|
###### Methodenbeschreibung
|
||||||
|
|
||||||
|
- `__init__(self, user_agent=None)`: Initialisiert den Scraper, setzt die Sprache für die `wikipedia`-Bibliothek (typischerweise 'de') und konfiguriert eine `requests.Session` mit einem benutzerdefinierten User-Agent für HTTP-Anfragen.
|
||||||
|
|
||||||
|
- `serp_wikipedia_lookup(self, company_name, lang='de')`: Nutzt die SerpAPI, um eine Google-Suche nach dem offiziellen Wikipedia-Artikel eines Unternehmens durchzuführen. Dies ist der erste und wichtigste Schritt, um einen Kandidaten-Artikel zu finden.
|
||||||
|
|
||||||
|
- `search_company_article(self, company_name, ...)`: Orchestriert den gesamten Such- und Validierungsprozess. Ruft zuerst `serp_wikipedia_lookup` auf, um eine URL zu erhalten. Anschließend wird der gefundene Artikel geladen und mit der internen Methode `_validate_article` auf Relevanz geprüft.
|
||||||
|
|
||||||
|
- `_validate_article(self, page, company_name, ...)`: Führt eine faktenbasierte Überprüfung eines Wikipedia-Artikels durch. Anstatt sich nur auf den Titel zu verlassen, prüft die Methode harte Kriterien wie die Übereinstimmung der Website-Domain (aus den Weblinks des Artikels) oder des Unternehmenssitzes (aus der Infobox).
|
||||||
|
|
||||||
|
- `extract_company_data(self, url_or_page)`: Die zentrale Extraktionsmethode. Nimmt eine URL oder ein `wikipedia.page`-Objekt entgegen und extrahiert daraus strukturierte Daten. Sie parst die Infobox des Artikels, um Werte für Branche, Umsatz, Mitarbeiter und Sitz zu finden, und extrahiert zusätzlich den Einleitungstext sowie die Kategorien.
|
||||||
|
|
||||||
|
- `_extract_infobox_value(self, soup, target)`: Eine interne Hilfsmethode, die gezielt nach Schlüsselwörtern (z.B. "Branche", "Umsatz") in der Infobox eines Artikels sucht und den zugehörigen Wert extrahiert und normalisiert.
|
||||||
|
|
||||||
|
- `_parse_sitz_string_detailed(self, raw_sitz_string_input)`: Eine spezialisierte Hilfsmethode, die versucht, aus dem oft unstrukturierten Textfeld für den Unternehmenssitz die Stadt und das Land zu trennen und zu normalisieren.
|
||||||
|
|
||||||
|
#### `sync_manager.py` (Spezialist für D365-Abgleich)
|
||||||
|
|
||||||
|
#### Hauptfunktion
|
||||||
|
|
||||||
|
Das Modul `sync_manager.py` ist für den robusten und intelligenten Datenabgleich zwischen einem D365 Excel-Export und dem Google Sheet verantwortlich. Es implementiert einen "Full-Sync"-Mechanismus, der neue, geänderte und potenziell zu archivierende Datensätze identifiziert. Der Manager stellt sicher, dass das Google Sheet die aktuellsten Informationen aus dem D365-Export widerspiegelt, wobei definierte Regeln für die Datenpriorisierung und Konfliktlösung angewendet werden.
|
||||||
|
|
||||||
|
#### Methodenbeschreibung
|
||||||
|
|
||||||
|
- `_normalize_text_for_comparison(self, text: str) -> str`: Eine interne Hilfsmethode, die Text normalisiert, um irrelevante Whitespace-Unterschiede zu ignorieren und so präzisere Vergleiche zu ermöglichen.
|
||||||
|
|
||||||
|
- `__init__(self, sheet_handler, d365_export_path)`: Der Konstruktor initialisiert den `SyncManager` mit einem `GoogleSheetHandler`-Objekt und dem Pfad zum D365 Excel-Export. Es definiert auch die Spaltenzuordnungen zwischen D365 und Google Sheet (`d365_to_gsheet_map`) sowie die Regeln, welche Spalten bei Konflikten Priorität haben (`d365_wins_cols`, `smart_merge_cols`).
|
||||||
|
|
||||||
|
- `_load_data(self)`: Lädt und bereitet die Daten aus dem D365 Excel-Export und dem Google Sheet vor. Diese Methode ist robust gegenüber "verschmutzten" Headern im Google Sheet und stellt sicher, dass beide Datensätze in einem konsistenten Format für den Abgleich vorliegen. Sie identifiziert auch neue und bestehende IDs.
|
||||||
|
|
||||||
|
- `run_sync(self)`: Orchestriert den gesamten Synchronisationsprozess. Nach dem Laden der Daten identifiziert es neue Accounts, aktualisiert bestehende Accounts gemäß den definierten Regeln (D365-Werte überschreiben GSheet-Werte für bestimmte Spalten, Smart-Merge für andere) und sammelt Statistiken über die durchgeführten Änderungen und Konflikte. Alle Änderungen werden in Batches an das Google Sheet gesendet.
|
||||||
|
|
||||||
|
- `debug_sync(self, debug_id=None)`: Bietet einen Debug-Modus für den Synchronisationsprozess. Ohne `debug_id` wird eine allgemeine Statistik über neue, bestehende und gelöschte IDs ausgegeben. Mit einer spezifischen `debug_id` führt es eine Tiefenanalyse für einen einzelnen Datensatz durch, zeigt Rohdaten und verarbeitete Daten und vergleicht kritische Felder.
|
||||||
|
|
||||||
|
- `simulate_sync(self, debug_id=None)`: Führt eine "Trockenlauf"-Simulation des Synchronisationsprozesses durch, ohne tatsächlich Daten im Google Sheet zu ändern. Es generiert einen detaillierten Bericht über alle potenziellen Änderungen, Updates und Konflikte, die im Falle eines echten Laufs auftreten würden. Dies ist nützlich zur Vorabprüfung und Fehleranalyse.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## 4. Marketing-Produktionslinie: Content-Erstellung
|
||||||
|
|
||||||
|
Diese Produktionslinie nutzt die angereicherten Daten, um direkt verwertbaren Marketing-Content zu erstellen.
|
||||||
|
|
||||||
|
### `generate_marketing_text.py` (Marketing Text Engine)
|
||||||
|
|
||||||
|
#### Hauptfunktion
|
||||||
|
|
||||||
|
Das Modul `generate_marketing_text.py` ist eine spezialisierte Engine zur automatischen Erstellung von hochgradig personalisierten Marketing-Texten. Es kombiniert eine strukturierte Wissensbasis (`marketing_wissen_final.yaml`) mit der Leistungsfähigkeit eines großen Sprachmodells (LLM), um für jede Kombination aus Zielbranche und Ansprechpartner-Position maßgeschneiderte Textbausteine für E-Mail-Kampagnen zu generieren. Das Skript ist so konzipiert, dass es nur neue, noch nicht existierende Textkombinationen erstellt und diese an ein Google Sheet anhängt, um die Effizienz zu maximieren.
|
||||||
|
|
||||||
|
#### Methodenbeschreibung
|
||||||
|
|
||||||
|
- `call_openai_with_retry(prompt, max_retries=3, delay=5)`: Eine robuste Wrapper-Funktion für Aufrufe an die OpenAI API. Sie implementiert eine Wiederholungslogik mit exponentiellem Backoff, um bei temporären API-Fehlern oder Netzwerkproblemen stabil zu bleiben.
|
||||||
|
|
||||||
|
- `build_prompt(branch_name, branch_data, position_name, position_data)`: Diese Funktion baut dynamisch den "Master-Prompt" für die KI zusammen. Sie integriert kontextbezogene Informationen wie die Herausforderungen (Pain Points) der Zielbranche und der spezifischen Ansprechpartner-Position. Eine wichtige Logik hierbei ist die dynamische Auswahl von Referenzkunden: Sind branchenspezifische Referenzen in der Wissensbasis vorhanden, werden diese verwendet; andernfalls greift die Funktion auf eine allgemeine Liste von Fallback-Referenzen zurück.
|
||||||
|
|
||||||
|
- `main(specific_branch=None)`: Die Haupt-Orchestrierungsfunktion des Skripts.
|
||||||
|
1. **Initialisierung:** Richtet das Logging ein, lädt API-Schlüssel und die Wissensbasis aus der YAML-Datei.
|
||||||
|
2. **Laden bestehender Texte:** Stellt eine Verbindung zum Google Sheet (`OUTPUT_SHEET_NAME`) her und lädt alle bereits generierten Textkombinationen, um doppelte Generierungen zu vermeiden.
|
||||||
|
3. **Generierungs-Loop:** Iteriert über alle möglichen Kombinationen von Branchen und Positionen aus der Wissensbasis.
|
||||||
|
4. **Überspringen-Logik:** Prüft für jede Kombination, ob sie bereits im Google Sheet vorhanden ist. Wenn ja, wird sie übersprungen.
|
||||||
|
5. **Text-Generierung:** Für neue Kombinationen wird der `build_prompt` aufgerufen, um den Prompt zu erstellen, und `call_openai_with_retry`, um die Textbausteine (Betreff, Einleitung, Referenz-Block) als JSON-Objekt zu generieren.
|
||||||
|
6. **Ergebnisse anhängen:** Alle neu generierten Texte werden gesammelt und am Ende des Prozesses in einem einzigen Batch-Aufruf an das Google Sheet angehängt, um die Anzahl der API-Aufrufe an Google zu minimieren.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## 5. ETL-Pipeline: Erstellung der Marketing-Wissensbasis
|
||||||
|
|
||||||
|
Dieser Bereich enthält alle Skripte, die als ETL-Pipelines (Extract, Transform, Load) dienen, um die zentrale Wissensbasis (`marketing_wissen.yaml`) aus verschiedenen Quellen zu erstellen und zu pflegen.
|
||||||
|
|
||||||
|
### `build_knowledge_base.py`
|
||||||
|
#### Hauptfunktion
|
||||||
|
|
||||||
|
Das Modul `build_knowledge_base.py` ist dafür verantwortlich, eine umfassende Wissensbasis für die Marketing-Text-Generierung zu erstellen. Es nutzt die in `config.py` definierten Brancheninformationen, um mittels KI für jede Branche ein detailliertes Dossier zu erstellen. Aus diesem Dossier werden dann strukturierte Daten wie eine Zusammenfassung, operative "Pain Points" und branchenspezifische Schlüsselbegriffe extrahiert. Das Endergebnis ist eine einzelne YAML-Datei (`marketing_wissen_final.yaml`), die als "Single Source of Truth" für die Textgenerierung dient.
|
||||||
|
|
||||||
|
#### Methodenbeschreibung
|
||||||
|
|
||||||
|
- `call_openai_with_retry(prompt, is_extraction=False, ...)`: Eine Wrapper-Funktion für OpenAI-API-Aufrufe, die eine Wiederholungslogik für den Fall von Fehlern implementiert. Sie kann sowohl für die Generierung von Freitext als auch für die Extraktion von strukturierten JSON-Daten konfiguriert werden.
|
||||||
|
|
||||||
|
- `generate_research_prompt(branch_name, branch_info)`: Erstellt einen Prompt für die KI, um ein detailliertes Branchen-Dossier zu generieren. Der Prompt wird mit Kontext aus der `config.py` angereichert, einschließlich der Branchendefinition und Beispielunternehmen, um eine hohe Relevanz sicherzustellen.
|
||||||
|
|
||||||
|
- `generate_extraction_prompt(dossier_content)`: Erstellt einen zweiten Prompt, der die KI anweist, aus dem zuvor generierten Dossier-Text strukturierte Informationen zu extrahieren. Der Fokus liegt hierbei auf operativen "Pain Points", die für den Außendienst relevant sind.
|
||||||
|
|
||||||
|
- `main(branches_to_process=None)`: Die Hauptfunktion, die den gesamten Prozess orchestriert:
|
||||||
|
1. **Initialisierung:** Lädt die API-Schlüssel und bereitet die Grundstruktur der Wissensbasis vor, einschließlich vordefinierter "Pain Points" für verschiedene Ansprechpartner-Positionen.
|
||||||
|
2. **Branchen-Selektion:** Verarbeitet entweder alle in `config.py` definierten Branchen oder eine spezifische Auswahl, die über Kommandozeilen-Argumente übergeben wird.
|
||||||
|
3. **Dossier-Generierung:** Für jede ausgewählte Branche wird `generate_research_prompt` aufgerufen und ein Dossier von der KI erstellt. Dieses wird zur Nachvollziehbarkeit als Textdatei im `industries`-Ordner gespeichert.
|
||||||
|
4. **Daten-Extraktion:** Das generierte Dossier wird verwendet, um mit `generate_extraction_prompt` die strukturierten Daten (Zusammenfassung, Pain Points, Schlüsselbegriffe) zu extrahieren.
|
||||||
|
5. **Zusammenführung:** Die extrahierten Daten werden zusammen mit den Referenzkunden aus der `config.py` in die Wissensbasis-Struktur eingefügt.
|
||||||
|
6. **Speichern:** Die vollständige, angereicherte Wissensbasis wird am Ende des Prozesses in die finale YAML-Datei (`marketing_wissen_final.yaml`) geschrieben.
|
||||||
|
|
||||||
|
### `expand_knowledge_base.py`
|
||||||
|
#### Hauptfunktion
|
||||||
|
|
||||||
|
Das Modul `expand_knowledge_base.py` dient dazu, eine bestehende Wissensbasis (`marketing_wissen.yaml`) gezielt zu erweitern. Es identifiziert, welche Branchen aus der zentralen Konfiguration (`config.py`) noch in der Wissensbasis fehlen, und generiert für diese fehlenden Branchen die entsprechenden Einträge. Der Prozess ist identisch mit dem von `build_knowledge_base.py`: Es wird ein KI-gestütztes Dossier erstellt, aus dem dann strukturierte Daten extrahiert und in die Wissensbasis integriert werden. Das Ergebnis wird in einer neuen, kompletten Datei (`marketing_wissen_komplett.yaml`) gespeichert.
|
||||||
|
|
||||||
|
#### Methodenbeschreibung
|
||||||
|
|
||||||
|
- `call_openai_with_retry(prompt, is_extraction=False, ...)`: Eine Wrapper-Funktion für OpenAI-API-Aufrufe mit Wiederholungslogik, die sowohl Freitext-Generierung als auch JSON-Extraktion unterstützt.
|
||||||
|
|
||||||
|
- `generate_research_prompt(branch_name)`: Erstellt einen Prompt für die KI, um ein Branchen-Dossier zu einem gegebenen Branchennamen zu erstellen.
|
||||||
|
|
||||||
|
- `generate_extraction_prompt(dossier_content)`: Erstellt einen Prompt, um aus einem generierten Dossier-Text strukturierte Daten (Zusammenfassung, Pain Points, Schlüsselbegriffe) im JSON-Format zu extrahieren.
|
||||||
|
|
||||||
|
- `main(branches_to_process=None)`: Die Hauptfunktion, die den Erweiterungsprozess steuert:
|
||||||
|
1. **Initialisierung:** Lädt API-Schlüssel und die existierende Basis-Wissensdatei (`marketing_wissen.yaml`).
|
||||||
|
2. **Delta-Ermittlung:** Vergleicht die Liste aller Branchen aus `config.py` mit den bereits in der Wissensbasis vorhandenen Branchen, um die Liste der zu bearbeitenden, fehlenden Branchen zu ermitteln.
|
||||||
|
3. **Gezielte Verarbeitung:** Iteriert ausschließlich über die fehlenden Branchen (oder eine über Kommandozeilen-Argumente spezifizierte Teilmenge).
|
||||||
|
4. **Dossier-Generierung & Extraktion:** Führt für jede neue Branche den zweistufigen Prozess aus: Zuerst wird das Dossier generiert und als Textdatei gespeichert, danach werden die strukturierten Daten extrahiert.
|
||||||
|
5. **Aktualisierung:** Fügt die neu extrahierten Daten zur in-memory-Version der Wissensbasis hinzu.
|
||||||
|
6. **Speichern:** Schreibt die erweiterte und nun vollständige Wissensbasis in eine neue Zieldatei (`marketing_wissen_komplett.yaml`).
|
||||||
|
|
||||||
|
### `extract_insights.py`
|
||||||
|
#### Hauptfunktion
|
||||||
|
|
||||||
|
Das Modul `extract_insights.py` ist ein Werkzeug zur automatisierten Erstellung einer Wissensbasis aus unstrukturierten Word-Dokumenten (`.docx`). Es liest Branchenanalysen aus einem spezifizierten Ordner, sendet deren Inhalt an eine KI und extrahiert gezielt strukturierte Informationen wie operative "Pain Points", branchenspezifische Fachbegriffe und eine Management-Zusammenfassung. Diese extrahierten Daten werden in einer einzigen, strukturierten YAML-Datei (`marketing_wissen_v1.yaml`) zusammengefasst, die als Grundlage für weitere Marketing-Automatisierungen dient.
|
||||||
|
|
||||||
|
#### Methodenbeschreibung
|
||||||
|
|
||||||
|
- `call_openai_with_retry(prompt, ...)`: Eine robuste Wrapper-Funktion für OpenAI-API-Aufrufe, die eine Wiederholungslogik bei Fehlern implementiert.
|
||||||
|
|
||||||
|
- `read_docx_content(filepath)`: Eine Hilfsfunktion, die eine `.docx`-Datei einliest und deren gesamten Textinhalt, einschließlich Absätzen und Tabellen, als einzelnen String zurückgibt.
|
||||||
|
|
||||||
|
- `extract_yaml_from_response(response_text)`: Eine wichtige Bereinigungsfunktion, die sicherstellt, dass aus der oft mit Markdown-Formatierungen (` ```yaml ... ``` `) versehenen KI-Antwort nur der reine YAML-Code extrahiert wird, um Parsing-Fehler zu vermeiden.
|
||||||
|
|
||||||
|
- `generate_extraction_prompt(content, data_to_extract)`: Erstellt hochspezialisierte Prompts für die KI. Je nachdem, welche Information extrahiert werden soll (`pain_points`, `key_terms` oder `summary`), wird der KI eine andere Rolle und ein anderer Auftrag zugewiesen, um die Qualität und Relevanz der extrahierten Daten zu maximieren.
|
||||||
|
|
||||||
|
- `main()`: Die Hauptfunktion, die den gesamten ETL-Prozess (Extract, Transform, Load) steuert:
|
||||||
|
1. **Initialisierung:** Lädt die API-Schlüssel und prüft, ob der Quellordner mit den Word-Dokumenten existiert.
|
||||||
|
2. **Dokumenten-Loop:** Iteriert über alle `.docx`-Dateien im Quellordner.
|
||||||
|
3. **Text-Extraktion:** Liest den Inhalt jedes Dokuments mit `read_docx_content`.
|
||||||
|
4. **Iterative KI-Extraktion:** Führt für jedes Dokument drei separate KI-Aufrufe durch (einen für "Pain Points", einen für "Key Terms" und einen für "Summary"), um die Genauigkeit zu erhöhen.
|
||||||
|
5. **Daten-Aggregation:** Sammelt die extrahierten und geparsten YAML-Daten für jede Branche in einer zentralen `knowledge_base`-Struktur.
|
||||||
|
6. **Speichern:** Schreibt die finale, aggregierte Wissensbasis in die `marketing_wissen_v1.yaml`-Datei.
|
||||||
|
|
||||||
|
### `generate_knowledge_base.py`
|
||||||
|
#### Hauptfunktion
|
||||||
|
|
||||||
|
Das Modul `generate_knowledge_base.py` ist ein KI-gestütztes Skript zur Erstellung eines ersten Entwurfs für eine Marketing-Wissensbasis (`marketing_wissen_entwurf.yaml`). Es generiert zwei Kernbestandteile:
|
||||||
|
|
||||||
|
1. **Branchen-Pain-Points:** Für eine vordefinierte Liste von Fokusbranchen werden die spezifischen operativen Herausforderungen im Außendienst identifiziert.
|
||||||
|
2. **Positions-Fokus:** Für eine Liste von typischen Ansprechpartner-Positionen wird deren jeweiliger strategischer Fokus in Bezug auf Serviceprozesse formuliert.
|
||||||
|
|
||||||
|
Das Skript nutzt spezialisierte Prompts, um die KI in die Rolle eines Branchenexperten oder Vertriebs-Coaches zu versetzen und so qualitativ hochwertige, relevante Inhalte zu generieren. Das Ergebnis dient als Grundlage, die manuell überprüft und verfeinert werden kann.
|
||||||
|
|
||||||
|
#### Methodenbeschreibung
|
||||||
|
|
||||||
|
- `call_openai_with_retry(prompt, ...)`: Eine Standard-Wrapper-Funktion für OpenAI-API-Aufrufe mit integrierter Wiederholungslogik, um die Stabilität bei Netzwerk- oder API-Problemen zu gewährleisten.
|
||||||
|
|
||||||
|
- `generate_pain_points_prompt(branch_name)`: Erstellt einen detaillierten Prompt, der die KI anweist, sich in die Rolle eines Top-Strategieberaters zu versetzen. Der Prompt enthält einen "Chain of Thought"-Abschnitt, der die KI anleitet, über typische Aufgaben und Probleme im Außendienst der jeweiligen Branche nachzudenken, bevor sie die finalen "Pain Points" formuliert. Das Ausgabeformat wird strikt als YAML-Liste vorgegeben.
|
||||||
|
|
||||||
|
- `generate_position_focus_prompt(position_name)`: Erstellt einen Prompt, der die KI als erfahrenen B2B-Vertriebs-Coach positioniert. Die Aufgabe ist es, einen einzigen, prägnanten Satz zu formulieren, der den Hauptfokus einer bestimmten Ansprechpartner-Rolle (z.B. CFO, IT-Leiter) zusammenfasst.
|
||||||
|
|
||||||
|
- `main()`: Die Hauptfunktion, die den gesamten Generierungsprozess steuert:
|
||||||
|
1. **Initialisierung:** Lädt die API-Schlüssel und definiert die zu bearbeitenden Fokusbranchen und Positionen.
|
||||||
|
2. **Branchen-Verarbeitung:** Iteriert durch die Liste der `FOKUS_BRANCHEN`, ruft für jede Branche `generate_pain_points_prompt` auf und sendet den Prompt an die KI. Die Antwort wird geparst und in der `knowledge_base`-Struktur gespeichert.
|
||||||
|
3. **Positions-Verarbeitung:** Iteriert durch die Liste der `POSITIONEN`, generiert mit `generate_position_focus_prompt` den entsprechenden Prompt und lässt die KI den Fokus-Satz formulieren.
|
||||||
|
4. **Speichern:** Schreibt die gesammelten Daten in die Zieldatei `marketing_wissen_entwurf.yaml`, die als Arbeitsgrundlage für die finale Wissensbasis dient.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## 6. Sub-System: Kontakt-Klassifizierung
|
||||||
|
|
||||||
|
Dies ist ein eigenständiges System innerhalb des Projekts, das sich ausschließlich mit der Analyse und Kategorisierung von Job-Titeln befasst.
|
||||||
|
|
||||||
|
### `contact_grouping.py` (Klassifizierer)
|
||||||
|
|
||||||
|
#### Hauptfunktion
|
||||||
|
|
||||||
|
Das Modul `contact_grouping.py` ist für die automatische Klassifizierung von Jobtiteln in vordefinierte Abteilungen zuständig. Es nutzt eine mehrstufige Logik, die auf einer Kombination aus regelbasierten Mappings (exakte Treffer und Keyword-Regeln) und einer KI-gestützten Klassifizierung basiert. Das Ziel ist es, die Jobtitel von Kontakten aus einem Google Sheet (`Matching_Positions`) einer passenden Abteilung zuzuordnen, wobei auch der Unternehmensbranche-Kontext berücksichtigt wird.
|
||||||
|
|
||||||
|
#### Methodenbeschreibung
|
||||||
|
|
||||||
|
- `__init__(self)`: Initialisiert die `ContactGrouper`-Klasse und bereitet die internen Variablen für die Wissensbasis vor.
|
||||||
|
|
||||||
|
- `load_knowledge_base(self)`: Lädt die zuvor erstellten Wissensbasis-Dateien (`exact_match_map.json` und `keyword_rules.json`) in den Speicher. Diese Dateien enthalten die Regeln für die regelbasierte Zuordnung. Außerdem generiert es Beispiele für den KI-Prompt aus der geladenen Wissensbasis.
|
||||||
|
|
||||||
|
- `_load_json(self, file_path)`: Eine interne Hilfsmethode zum sicheren Laden und Parsen von JSON-Dateien.
|
||||||
|
|
||||||
|
- `_normalize_text(self, text)`: Eine interne Hilfsmethode zur Normalisierung von Texten (Kleinschreibung, Leerzeichen entfernen).
|
||||||
|
|
||||||
|
- `_generate_ai_examples(self)`: Generiert einen Teil des KI-Prompts, der Beispiele für typische Jobtitel pro Abteilung enthält. Dies hilft der KI, die Klassifizierungsaufgabe besser zu verstehen.
|
||||||
|
|
||||||
|
- `_find_best_match(self, job_title, company_branch)`: Die Kernlogik für die regelbasierte Zuordnung. Sie versucht zuerst einen exakten Match des `job_title` zu finden. Wenn dies fehlschlägt, werden Keyword-Regeln angewendet, wobei die `company_branch` zur Verfeinerung der Zuordnung genutzt wird. Prioritäten der Abteilungen werden bei mehreren Treffern berücksichtigt.
|
||||||
|
|
||||||
|
- `_get_ai_classification(self, contacts_to_classify)`: Sendet eine Liste von Jobtiteln, die nicht regelbasiert zugeordnet werden konnten, an die OpenAI API zur KI-gestützten Klassifizierung. Der Prompt enthält die gültigen Abteilungen und generierte Beispiele, um die KI-Antwort zu steuern. Die Ergebnisse werden als Dictionary zurückgegeben.
|
||||||
|
|
||||||
|
- `_append_learnings_to_source(self, gsh, new_mappings_df)`: Hängt neue, von der KI erfolgreich klassifizierte Jobtitel und deren Abteilungen an das Quell-Sheet (`CRM_Jobtitles`) an, um die Wissensbasis kontinuierlich zu erweitern.
|
||||||
|
|
||||||
|
- `process_contacts(self)`: Die Hauptmethode, die den gesamten Kontakt-Klassifizierungsprozess steuert:
|
||||||
|
1. Lädt die Kontaktdaten aus dem `TARGET_SHEET_NAME` Google Sheet.
|
||||||
|
2. Führt die regelbasierte Zuordnung (`_find_best_match`) für alle Jobtitel durch.
|
||||||
|
3. Identifiziert Jobtitel, die nicht regelbasiert zugeordnet werden konnten (`DEFAULT_DEPARTMENT`).
|
||||||
|
4. Sendet diese unklassifizierten Jobtitel in Batches an die KI (`_get_ai_classification`) zur weiteren Klassifizierung.
|
||||||
|
5. Aktualisiert die `Department`-Spalte im DataFrame mit den KI-Ergebnissen.
|
||||||
|
6. Hängt neue KI-Erkenntnisse an das Lern-Quell-Sheet an (`_append_learnings_to_source`).
|
||||||
|
7. Schreibt die finalen, klassifizierten Daten zurück in das `TARGET_SHEET_NAME` Google Sheet.
|
||||||
|
|
||||||
|
### `knowledge_base_builder.py` (Wissensbasis-Ersteller für Klassifizierung)
|
||||||
|
|
||||||
|
#### Hauptfunktion
|
||||||
|
|
||||||
|
Das Modul `knowledge_base_builder.py` ist dafür verantwortlich, eine Wissensbasis für die automatische Zuordnung von Jobtiteln zu Abteilungen zu erstellen. Es verarbeitet eine Liste von Jobtiteln und deren zugehörigen Abteilungen und Branchen aus einem Google Sheet. Das Skript generiert zwei Haupt-Artefakte:
|
||||||
|
|
||||||
|
1. **Exaktes Mapping (`exact_match_map.json`):** Eine einfache Zuordnung von exakten, normalisierten Jobtiteln zu ihrer dominantesten Abteilung.
|
||||||
|
2. **Keyword-Regeln (`keyword_rules.json`):** Eine Sammlung von Regeln, die Schlüsselwörter pro Abteilung identifiziert und optional branchenspezifische Anforderungen für die Zuordnung hinzufügen, basierend auf der Häufigkeit von Branchen innerhalb der Abteilung.
|
||||||
|
|
||||||
|
Das Ziel ist es, die automatische und präzise Klassifizierung neuer, unbekannter Jobtitel zu ermöglichen.
|
||||||
|
|
||||||
|
#### Methodenbeschreibung
|
||||||
|
|
||||||
|
- `setup_logging()`: Eine Hilfsfunktion, die das Logging für das Skript konfiguriert und sicherstellt, dass alle Ausgaben sowohl in eine Datei als auch in die Konsole geschrieben werden.
|
||||||
|
|
||||||
|
- `build_knowledge_base()`: Dies ist die Hauptfunktion des Moduls, die den gesamten Prozess der Wissensbasiserstellung orchestriert:
|
||||||
|
1. **Daten laden:** Stellt eine Verbindung zum konfigurierten Google Sheet (definiert in `SOURCE_SHEET_NAME`) her und lädt die Daten in einen Pandas DataFrame.
|
||||||
|
2. **Datenbereinigung:** Entfernt Zeilen mit fehlenden oder leeren Jobtiteln/Abteilungen und normalisiert die Jobtitel für konsistente Vergleiche.
|
||||||
|
3. **Exaktes Mapping:** Gruppiert die Jobtitel nach ihrer normalisierten Form und ermittelt die am häufigsten (modal) vorkommende Abteilung, um eine direkte 1:1-Zuordnung zu erstellen. Das Ergebnis wird als JSON-Datei (`exact_match_map.json`) gespeichert.
|
||||||
|
4. **Keyword-Regeln erstellen:** Für jede Abteilung werden die häufigsten und aussagekräftigsten Schlüsselwörter aus den zugehörigen Jobtiteln extrahiert. Dabei werden eine vordefinierte Liste von Stoppwörtern und generischen Begriffen ignoriert. Diese Regeln erhalten eine Priorität basierend auf `DEPARTMENT_PRIORITIES`.
|
||||||
|
5. **Branchenspezifische Anpassung:** Optional wird geprüft, ob eine Abteilung stark mit einer bestimmten Branchengruppe (definiert in `BRANCH_GROUP_RULES`) korreliert. Wenn eine hohe Spezifität (> `BRANCH_SPECIFICITY_THRESHOLD`) und genügend Datenpunkte vorhanden sind, wird diese Branchenanforderung zur Keyword-Rule hinzugefügt. Das Ergebnis wird als JSON-Datei (`keyword_rules.json`) gespeichert.
|
||||||
|
6. **Fehlerbehandlung:** Robuste Fehlerbehandlung für Datei-I/O und Datenladen aus Google Sheets.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## 7. Standalone-Werkzeuge (Legacy)
|
||||||
|
|
||||||
|
Dieser Bereich enthält Skripte, die als eigenständige Werkzeuge für spezifische Aufgaben konzipiert waren.
|
||||||
|
|
||||||
|
### `company_deduplicator.py` (Duplikats-Check)
|
||||||
|
|
||||||
|
#### Hauptfunktion
|
||||||
|
Das Skript `company_deduplicator.py` (ehemals `duplicate_checker_old.py`) ist ein spezialisiertes Werkzeug zur Identifizierung von potenziellen Unternehmens-Duplikaten. Es operiert in zwei Modi:
|
||||||
|
|
||||||
|
1. **Externer Vergleich:** Identifiziert Duplikate zwischen einer externen Liste (`Matching_Accounts`) und der internen CRM-Liste (`CRM_Accounts`). Dies ist die ursprüngliche Funktionalität.
|
||||||
|
2. **Interne Deduplizierung:** Findet Duplikate *innerhalb* der `CRM_Accounts`-Liste, gruppiert diese und markiert sie zur weiteren Bearbeitung.
|
||||||
|
|
||||||
|
Es verwendet einen gewichteten, heuristischen Algorithmus, um Ähnlichkeiten zu bewerten und nutzt bekannte Unternehmenshierarchien (`Parent Account`), um Falsch-Positive zu reduzieren.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## 8. Projekt-Fundament (Legacy)
|
||||||
|
|
||||||
|
Diese Module stellen grundlegende Funktionen und Konfigurationen für das gesamte Projekt bereit.
|
||||||
|
|
||||||
|
### `config.py` (Zentrale Konfiguration)
|
||||||
|
|
||||||
|
#### Hauptfunktion
|
||||||
|
|
||||||
|
Das Modul `config.py` dient als zentrale Konfigurationsdatei für das gesamte Projekt "Automatisierte Unternehmensbewertung". Es bündelt alle globalen Einstellungen, Dateipfade, API-Schlüssel-Pfade, Schwellenwerte und Mappings, die von verschiedenen Modulen im Projekt verwendet werden. Durch die Zentralisierung der Konfiguration wird die Wartbarkeit und Anpassbarkeit des Systems erheblich verbessert.
|
||||||
|
|
||||||
|
#### Methodenbeschreibung
|
||||||
|
|
||||||
|
- `normalize_for_mapping(text)`: Eine Hilfsfunktion, die einen String aggressiv für Mapping-Zwecke normalisiert, indem er in Kleinbuchstaben umgewandelt, getrimmt und alle nicht-alphanumerischen Zeichen entfernt werden. Diese Funktion wird intern von der `Config`-Klasse verwendet.
|
||||||
|
|
||||||
|
- `Config` Klasse:
|
||||||
|
- **Attribute:** Enthält statische Attribute wie `VERSION`, `LANG`, `SHEET_URL`, `MAX_RETRIES`, `RETRY_DELAY`, `REQUEST_TIMEOUT`, `SIMILARITY_THRESHOLD`, `DEBUG`, `WIKIPEDIA_SEARCH_RESULTS`, `HTML_PARSER`, `TOKEN_MODEL`, `USER_AGENT`.
|
||||||
|
- **Batching & Parallelisierung:** Konfigurationen für die Batch-Verarbeitung und Parallelisierung, z.B. `PROCESSING_BATCH_SIZE`, `OPENAI_BATCH_SIZE_LIMIT`, `MAX_SCRAPING_WORKERS`.
|
||||||
|
- **Plausibilitäts-Schwellenwerte:** Definiert numerische Schwellenwerte für Plausibilitätsprüfungen von Umsatz- und Mitarbeiterzahlen, z.B. `PLAUSI_UMSATZ_MIN_WARNUNG`, `PLAUSI_RATIO_UMSATZ_PRO_MA_MIN`.
|
||||||
|
- **Länder-Codes Mapping (`COUNTRY_CODE_MAP`):** Ein Dictionary, das D365-Ländercodes in die im Google Sheet verwendeten Langformen übersetzt.
|
||||||
|
- **Branchen-Gruppen Mapping (`BRANCH_GROUP_MAPPING`):** Eine "Single Source of Truth" für alle Branchen, angereichert mit Definitionen, Beispielen und D365-Branch-Details.
|
||||||
|
- **API-Schlüssel (`API_KEYS`):** Ein Dictionary, das die geladenen API-Schlüssel speichert.
|
||||||
|
- `load_api_keys()` (Klassenmethode): Lädt API-Schlüssel aus den konfigurierten Dateipfaden (`API_KEY_FILE`, `SERP_API_KEY_FILE`, `GENDERIZE_API_KEY_FILE`) und setzt den OpenAI API-Schlüssel global.
|
||||||
|
- `_load_key_from_file(filepath)` (Statische Methode): Eine interne Hilfsfunktion zum sicheren Laden eines API-Schlüssels aus einer angegebenen Datei.
|
||||||
|
|
||||||
|
- `COLUMN_ORDER`: Eine globale Liste, die die exakte und garantierte Reihenfolge aller Spalten im Google Sheet definiert. Dies dient als "Single Source of Truth" für alle Index-Berechnungen.
|
||||||
|
|
||||||
|
- `COLUMN_MAP`: Ein globales Dictionary, das detaillierte Mappings für jede Spalte im Google Sheet bereitstellt, einschließlich des Spaltentitels (z.B. "A", "B") und des 0-basierten Index.
|
||||||
|
|
||||||
|
- **DEALFRONT AUTOMATION CONFIGURATION:** Spezifische Konfigurationen für die Dealfront-Automatisierung, einschließlich `DEALFRONT_CREDENTIALS_FILE`, `DEALFRONT_LOGIN_URL`, `DEALFRONT_TARGET_URL` und `TARGET_SEARCH_NAME`.
|
||||||
|
|
||||||
|
### `helpers.py` (Globaler Werkzeugkasten)
|
||||||
|
|
||||||
|
Diese Datei enthält eine Sammlung von globalen, wiederverwendbaren Hilfsfunktionen, die in verschiedenen Modulen des Projekts verwendet werden.
|
||||||
|
|
||||||
|
#### Decorators
|
||||||
|
|
||||||
|
##### `retry_on_failure(func)`
|
||||||
|
- **Zweck:** Ein Decorator, der eine Funktion bei bestimmten, temporären Fehlern (z.B. Netzwerkprobleme, API-Rate-Limits) automatisch mehrmals ausführt. Er verwendet eine exponentielle Backoff-Strategie, um die Wartezeit zwischen den Versuchen zu erhöhen.
|
||||||
|
- **Input:** Eine Funktion, die dekoriert werden soll.
|
||||||
|
- **Output:** Das Ergebnis der dekorierten Funktion oder löst eine Ausnahme aus, wenn alle Wiederholungsversuche fehlschlagen.
|
||||||
|
|
||||||
|
#### Logging & Token Counting
|
||||||
|
|
||||||
|
##### `token_count(text, model=None)`
|
||||||
|
- **Zweck:** Zählt die Anzahl der Tokens in einem gegebenen Text. Verwendet die `tiktoken`-Bibliothek für eine genaue Zählung, falls verfügbar, andernfalls schätzt sie die Anzahl basierend auf Leerzeichen.
|
||||||
|
- **Input:** `text` (der zu analysierende String), `model` (optional, das zu verwendende KI-Modell).
|
||||||
|
- **Output:** Eine Ganzzahl, die die Anzahl der Tokens darstellt.
|
||||||
|
|
||||||
|
##### `create_log_filename(mode)`
|
||||||
|
- **Zweck:** Erstellt einen standardisierten, zeitgestempelten Dateinamen für Log-Dateien.
|
||||||
|
- **Input:** `mode` (ein String, der den aktuellen Ausführungsmodus beschreibt, z.B. 'full_run').
|
||||||
|
- **Output:** Ein String, der den vollständigen Pfad zur Log-Datei enthält (z.B. `Log/2025-11-07_10-30_v221_Modus-full_run.txt`).
|
||||||
|
|
||||||
|
#### Text-, String- & URL-Utilities
|
||||||
|
|
||||||
|
##### `simple_normalize_url(url)`
|
||||||
|
- **Zweck:** Bereinigt und normalisiert eine URL auf ihre Kern-Domain (z.B. `https://www.beispiel.de/path` -> `beispiel.de`).
|
||||||
|
- **Input:** `url` (ein String mit einer URL).
|
||||||
|
- **Output:** Ein normalisierter Domain-String oder "k.A." bei ungültiger Eingabe.
|
||||||
|
|
||||||
|
##### `normalize_string(s)`
|
||||||
|
- **Zweck:** Standardisiert einen String, indem Umlaute (ä -> ae) und gängige Sonderzeichen ersetzt werden.
|
||||||
|
- **Input:** `s` (ein beliebiger String).
|
||||||
|
- **Output:** Der normalisierte String.
|
||||||
|
|
||||||
|
##### `clean_text(text)`
|
||||||
|
- **Zweck:** Bereinigt einen Text von unerwünschten Artefakten, die typischerweise beim Scrapen von Websites oder Wikipedia auftreten (z.B. `[1]`, `[Bearbeiten]`, überflüssige Leerzeichen).
|
||||||
|
- **Input:** `text` (ein String).
|
||||||
|
- **Output:** Der bereinigte Text.
|
||||||
|
|
||||||
|
##### `normalize_company_name(name)`
|
||||||
|
- **Zweck:** Normalisiert einen Firmennamen, indem gängige Rechtsformzusätze (GmbH, AG, etc.) und andere generische Begriffe entfernt werden, um Vergleiche zu erleichtern.
|
||||||
|
- **Input:** `name` (ein Firmenname als String).
|
||||||
|
- **Output:** Der normalisierte Firmenname in Kleinbuchstaben.
|
||||||
|
|
||||||
|
##### `extract_numeric_value(raw_value, is_umsatz=False)`
|
||||||
|
- **Zweck:** Extrahiert und normalisiert einen numerischen Wert aus einem unstrukturierten String. Kann Einheiten wie "Mio.", "Mrd." oder "Tsd." interpretieren und in einen Basiswert umrechnen.
|
||||||
|
- **Input:** `raw_value` (ein String, z.B. "ca. 250 Mio. EUR"), `is_umsatz` (ein Flag, das die Umrechnung für Umsätze steuert, z.B. Rückgabe in Millionen).
|
||||||
|
- **Output:** Ein String mit der normalisierten Zahl (z.B. "250") oder "k.A.".
|
||||||
|
|
||||||
|
#### API Wrappers & Externe Dienste
|
||||||
|
|
||||||
|
##### `call_openai_chat(...)`
|
||||||
|
- **Zweck:** Eine zentrale Wrapper-Funktion für Aufrufe an die OpenAI Chat-API. Kapselt die Authentifizierung, die Fehlerbehandlung und den eigentlichen API-Aufruf.
|
||||||
|
- **Input:** `prompt` (der an die KI gesendete Text), `temperature` (steuert die Kreativität der Antwort), `model` (das zu verwendende KI-Modell).
|
||||||
|
- **Output:** Der von der KI generierte Antwort-Text als String.
|
||||||
|
|
||||||
|
##### `summarize_website_content(raw_text, company_name)`
|
||||||
|
- **Zweck:** Nutzt die KI, um den Rohtext einer Website zu analysieren und eine strukturierte Zusammenfassung des Geschäftsmodells und des Potenzials für Field Service Management (FSM) zu erstellen.
|
||||||
|
- **Input:** `raw_text` (der Inhalt der Website), `company_name` (Name des Unternehmens).
|
||||||
|
- **Output:** Ein formatierter String, der das Geschäftsmodell, das FSM-Potenzial und Belegsätze enthält.
|
||||||
|
|
||||||
|
##### `evaluate_branche_chatgpt(...)`
|
||||||
|
- **Zweck:** Führt eine KI-basierte Brancheneinstufung für ein einzelnes Unternehmen durch. Sendet ein Unternehmensprofil und ein vordefiniertes Branchenschema an die KI.
|
||||||
|
- **Input:** `company_name`, `website_summary`, `wiki_absatz`.
|
||||||
|
- **Output:** Ein Dictionary mit der vorgeschlagenen "Branche", "Konfidenz" und "Begruendung".
|
||||||
|
|
||||||
|
##### `evaluate_branches_batch(companies_data)`
|
||||||
|
- **Zweck:** Führt die Brancheneinstufung für eine Liste von Unternehmen in einem einzigen API-Aufruf durch, um die Effizienz zu steigern.
|
||||||
|
- **Input:** `companies_data` (eine Liste von Dictionaries, die Unternehmensprofile enthalten).
|
||||||
|
- **Output:** Eine Liste von Dictionaries mit den Ergebnissen für jedes Unternehmen.
|
||||||
|
|
||||||
|
##### `generate_fsm_pitch(...)`
|
||||||
|
- **Zweck:** Generiert einen hochpersonalisierten, einleitenden Satz für eine Marketing-E-Mail (FSM-Pitch), basierend auf den gesammelten Unternehmensdaten.
|
||||||
|
- **Input:** Diverse Unternehmensdaten wie Name, Branche, Zusammenfassungen und Mitarbeiter-/Technikerzahlen.
|
||||||
|
- **Output:** Ein einzelner, prägnanter Satz als String.
|
||||||
|
|
||||||
|
##### `serp_website_lookup(company_name)`
|
||||||
|
- **Zweck:** Verwendet die SerpAPI (Google-Suche), um die offizielle Website eines Unternehmens zu finden. Filtert dabei unzuverlässige Quellen wie soziale Medien oder Nachrichtenportale heraus.
|
||||||
|
- **Input:** `company_name`.
|
||||||
|
- **Output:** Die normalisierte URL der gefundenen Website oder "k.A.".
|
||||||
|
|
||||||
|
##### `search_linkedin_contacts(...)`
|
||||||
|
- **Zweck:** Führt eine gezielte Google-Suche über die SerpAPI durch, um LinkedIn-Profile von Mitarbeitern in bestimmten Positionen (z.B. "Serviceleiter") bei einem Unternehmen zu finden.
|
||||||
|
- **Input:** `company_name`, `website`, `position_query` (z.B. "Leiter Kundendienst"), `crm_kurzform`.
|
||||||
|
- **Output:** Eine Liste von Dictionaries, die die gefundenen Kontakte mit Namen, Position und LinkedIn-URL enthalten.
|
||||||
|
|
||||||
|
#### Website Scraping & Validierung
|
||||||
|
|
||||||
|
##### `get_website_raw(url, ...)`
|
||||||
|
- **Zweck:** Lädt den reinen Textinhalt von einer Webseite. Die Funktion ist gehärtet, um mit verschiedenen Fehlern (SSL, Timeout, Connection Errors) umzugehen und versucht, Cookie-Banner intelligent zu entfernen.
|
||||||
|
- **Input:** `url` (die zu scrapende URL).
|
||||||
|
- **Output:** Der extrahierte Text der Website als String oder ein Fehlerhinweis (z.B. "k.A. (Timeout)").
|
||||||
|
|
||||||
|
##### `is_valid_wikipedia_article_url(url)`
|
||||||
|
- **Zweck:** Überprüft, ob eine gegebene URL tatsächlich auf einen existierenden Wikipedia-Artikel verweist und nicht auf eine "Seite existiert nicht"-Seite.
|
||||||
|
- **Input:** `url` (die zu prüfende Wikipedia-URL).
|
||||||
|
- **Output:** `True`, wenn der Artikel existiert, andernfalls `False`.
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# Vollständige Entwicklungs-Historie des Account-Matching-Algorithmus
|
||||||
|
|
||||||
|
Diese Datei vereint die Git-Historie der Ursprungs-Datei (`duplicate_checker.py`) und der weiterentwickelten Datei (`company_deduplicator.py`).
|
||||||
|
Da beim Umbenennen in der Vergangenheit die Historie in Git getrennt wurde, ist hier der gesamte Verlauf chronologisch dokumentiert.
|
||||||
|
|
||||||
|
## Teil 2: Weiterentwicklung & Refactoring (Nov 2025 - heute)
|
||||||
|
*Pfad: _legacy_gsheets_system/company_deduplicator.py*
|
||||||
|
|
||||||
|
2026-03-07 | d1b77fd2 | [30388f42] Infrastructure Hardening: Repaired CE/Connector DB schema, fixed frontend styling build, implemented robust echo shield in worker v2.1.1, and integrated Lead Engine into gateway.
|
||||||
|
2026-01-07 | 95634d7b | feat(company-explorer): Initial Web UI & Backend with Enrichment Flow
|
||||||
|
2025-11-09 | 00edd44b | feat: Parent Account Logik für interne Deduplizierung integriert
|
||||||
|
2025-11-09 | 37182b3a | feat: Interne Deduplizierung implementieren und Skript refaktorieren
|
||||||
|
2025-11-08 | 99867225 | feat(duplicate_checker): Verbesserte Kandidatenauswahl und Match-Priorisierung
|
||||||
|
2025-11-06 | 1dd86d8e | duplicate_checker_old.py aktualisiert
|
||||||
|
2025-11-06 | 0a729f2d | duplicate_checker_old.py aktualisiert
|
||||||
|
2025-11-06 | a67615ad | duplicate_checker_old.py aktualisiert
|
||||||
|
2025-11-06 | 2df8441b | duplicate?checker_old.py hinzugefügt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Teil 1: Ursprung & Experimente (Aug 2025 - Sep 2025)
|
||||||
|
*Pfad: ARCHIVE_legacy_scripts/duplicate_checker.py*
|
||||||
|
|
||||||
|
2026-03-07 | d1b77fd2 | [30388f42] Infrastructure Hardening: Repaired CE/Connector DB schema, fixed frontend styling build, implemented robust echo shield in worker v2.1.1, and integrated Lead Engine into gateway.
|
||||||
|
2025-09-24 | da9d97da | duplicate_checker.py aktualisiert
|
||||||
|
2025-09-24 | fa58a870 | duplicate_checker.py aktualisiert
|
||||||
|
2025-09-10 | 5fa5a292 | duplicate_checker.py aktualisiert
|
||||||
|
2025-09-10 | db696592 | duplicate_checker.py aktualisiert
|
||||||
|
2025-09-08 | ae975367 | NEU: Integration eines trainierten Machine-Learning-Modells (XGBoost) für die Match-Entscheidung
|
||||||
|
2025-09-05 | 24e32da5 | duplicate_checker.py aktualisiert
|
||||||
|
2025-09-05 | f5af3023 | duplicate_checker.py aktualisiert
|
||||||
|
2025-09-05 | 7a273bf2 | duplicate_checker.py aktualisiert
|
||||||
|
2025-09-05 | 538a0f28 | duplicate_checker.py aktualisiert
|
||||||
|
2025-09-05 | f160fc0f | duplicate_checker.py aktualisiert
|
||||||
|
2025-09-04 | 491254a8 | Feat: Matching-Logik mit gewichtetem Scoring & Interaktiv-Modus (v3.0)
|
||||||
|
2025-08-18 | 7cf23759 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-18 | b586bb3d | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-18 | 7d07a526 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-18 | 721cb39c | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-18 | af2e60f9 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-18 | 7d76a38c | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-08 | e916e4eb | feat(duplicate-checker): Quality-first++ – Domain-Gate, Location-Penalties, Smart Blocking (IDF-ligh
|
||||||
|
2025-08-08 | 56430d68 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-08 | 96ba680c | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-08 | aea5d45c | feat(duplicate-checker): quality-first Matching (Domain-Gate, Location-Penalties, Smart Blocking)
|
||||||
|
2025-08-08 | 8e80a3f7 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-08 | f420b84a | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-08 | ec56daa9 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-08 | be3f48ac | url_check nur für matching
|
||||||
|
2025-08-08 | aa4cf6ed | url check ergänzt
|
||||||
|
2025-08-06 | 4cd5dccc | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-06 | e58e493e | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-06 | 3febe145 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-06 | 63d014b0 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-06 | 4f6d51df | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-06 | 558b75f3 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-06 | e43efa44 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-06 | cfd1e8b5 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-06 | 8d717f3b | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-06 | 99dec723 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-06 | a3315eae | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-06 | b9a046bd | Add Logging
|
||||||
|
2025-08-06 | 9193ab1a | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-06 | 4aa1effe | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-06 | dfcb270a | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-06 | 6b4c8295 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-06 | 4a41ffb0 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-05 | 600b977a | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-05 | b876ea20 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-05 | 2f70e05e | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-05 | 9685bc5a | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-05 | 270a5fc0 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-05 | 7d3821ad | chat GPT version
|
||||||
|
2025-08-04 | 3a8809e0 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-04 | 38612a85 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-04 | c777d75d | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-04 | bc9591a4 | Add Logging
|
||||||
|
2025-08-04 | c0db46d2 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-03 | 7c9ee2f7 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-03 | 9cc291d5 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-03 | 40de8117 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-03 | c0cade7a | Add Logging
|
||||||
|
2025-08-03 | 940aa52b | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-01 | 05ecb012 | Rückgang zur stabilen Version
|
||||||
|
2025-08-01 | e48e44ea | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-01 | a10caa5a | revoce
|
||||||
|
2025-08-01 | add6ea53 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-01 | a58e4fc1 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-01 | 8a7426df | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-01 | 533796b6 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-01 | 4f60cc68 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-01 | a6853a2c | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-01 | 88c7ee4a | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-01 | 77852b8a | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-01 | 89ccd86f | revoce 2
|
||||||
|
2025-08-01 | 2341149c | revoce
|
||||||
|
2025-08-01 | a92da7f8 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-01 | aeda711d | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-01 | f5e28824 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-01 | 94a2dc88 | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-01 | e7c8a66f | duplicate_checker.py aktualisiert
|
||||||
|
2025-08-01 | 67b431b0 | duplicate_checker.py hinzugefügt
|
||||||
40
ARCHIVE_legacy_scripts/check_benni.py
Normal file
40
ARCHIVE_legacy_scripts/check_benni.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
DB_PATH = "companies_v3_fixed_2.db"
|
||||||
|
|
||||||
|
def check_company_33():
|
||||||
|
if not os.path.exists(DB_PATH):
|
||||||
|
print(f"❌ Database not found at {DB_PATH}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
print(f"🔍 Checking Company ID 33 (Bennis Playland)...")
|
||||||
|
# Check standard fields
|
||||||
|
cursor.execute("SELECT id, name, city, street, zip_code FROM companies WHERE id = 33")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
print(f" Standard: City='{row[2]}', Street='{row[3]}', Zip='{row[4]}'")
|
||||||
|
else:
|
||||||
|
print(" ❌ Company 33 not found in DB.")
|
||||||
|
|
||||||
|
# Check Enrichment
|
||||||
|
cursor.execute("SELECT content FROM enrichment_data WHERE company_id = 33 AND source_type = 'website_scrape'")
|
||||||
|
enrich_row = cursor.fetchone()
|
||||||
|
if enrich_row:
|
||||||
|
data = json.loads(enrich_row[0])
|
||||||
|
imp = data.get("impressum")
|
||||||
|
print(f" Impressum Data: {json.dumps(imp, indent=2) if imp else 'None'}")
|
||||||
|
else:
|
||||||
|
print(" ❌ No website_scrape found for Company 33.")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
check_company_33()
|
||||||
16
ARCHIVE_legacy_scripts/check_erding_openers.py
Normal file
16
ARCHIVE_legacy_scripts/check_erding_openers.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
DB_PATH = "/app/companies_v3_fixed_2.db"
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("SELECT name, ai_opener, ai_opener_secondary, industry_ai FROM companies WHERE name LIKE '%Erding%'")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
print(f"Company: {row[0]}")
|
||||||
|
print(f"Industry: {row[3]}")
|
||||||
|
print(f"Opener Primary: {row[1]}")
|
||||||
|
print(f"Opener Secondary: {row[2]}")
|
||||||
|
else:
|
||||||
|
print("Company not found.")
|
||||||
|
conn.close()
|
||||||
16
ARCHIVE_legacy_scripts/check_klinikum_erding.py
Normal file
16
ARCHIVE_legacy_scripts/check_klinikum_erding.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
DB_PATH = "/app/companies_v3_fixed_2.db"
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("SELECT name, ai_opener, ai_opener_secondary, industry_ai FROM companies WHERE name LIKE '%Klinikum Landkreis Erding%'")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
print(f"Company: {row[0]}")
|
||||||
|
print(f"Industry: {row[3]}")
|
||||||
|
print(f"Opener Primary: {row[1]}")
|
||||||
|
print(f"Opener Secondary: {row[2]}")
|
||||||
|
else:
|
||||||
|
print("Company not found.")
|
||||||
|
conn.close()
|
||||||
14
ARCHIVE_legacy_scripts/check_mappings.py
Normal file
14
ARCHIVE_legacy_scripts/check_mappings.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
def check_mappings():
|
||||||
|
conn = sqlite3.connect('/app/companies_v3_fixed_2.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT * FROM job_role_mappings")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
print("--- Job Role Mappings ---")
|
||||||
|
for row in rows:
|
||||||
|
print(row)
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
check_mappings()
|
||||||
25
ARCHIVE_legacy_scripts/check_matrix.py
Normal file
25
ARCHIVE_legacy_scripts/check_matrix.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add the company-explorer directory to the Python path
|
||||||
|
sys.path.append(os.path.abspath(os.path.join(os.getcwd(), 'company-explorer')))
|
||||||
|
|
||||||
|
from backend.database import SessionLocal, MarketingMatrix, Industry, Persona
|
||||||
|
import json
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
count = db.query(MarketingMatrix).count()
|
||||||
|
print(f"MarketingMatrix count: {count}")
|
||||||
|
|
||||||
|
if count > 0:
|
||||||
|
first = db.query(MarketingMatrix).first()
|
||||||
|
print(f"First entry: ID={first.id}, Industry={first.industry_id}, Persona={first.persona_id}")
|
||||||
|
else:
|
||||||
|
print("MarketingMatrix is empty.")
|
||||||
|
# Check if we have industries and personas
|
||||||
|
ind_count = db.query(Industry).count()
|
||||||
|
pers_count = db.query(Persona).count()
|
||||||
|
print(f"Industries: {ind_count}, Personas: {pers_count}")
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
23
ARCHIVE_legacy_scripts/check_matrix_indoor.py
Normal file
23
ARCHIVE_legacy_scripts/check_matrix_indoor.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
DB_PATH = "/app/companies_v3_fixed_2.db"
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT i.name, p.name, m.subject, m.intro, m.social_proof
|
||||||
|
FROM marketing_matrix m
|
||||||
|
JOIN industries i ON m.industry_id = i.id
|
||||||
|
JOIN personas p ON m.persona_id = p.id
|
||||||
|
WHERE i.name = 'Leisure - Indoor Active'
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor.execute(query)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
print(f"Industry: {row[0]} | Persona: {row[1]}")
|
||||||
|
print(f" Subject: {row[2]}")
|
||||||
|
print(f" Intro: {row[3]}")
|
||||||
|
print(f" Social Proof: {row[4]}")
|
||||||
|
print("-" * 50)
|
||||||
|
conn.close()
|
||||||
24
ARCHIVE_legacy_scripts/check_matrix_results.py
Normal file
24
ARCHIVE_legacy_scripts/check_matrix_results.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import sqlite3
|
||||||
|
import json
|
||||||
|
|
||||||
|
DB_PATH = "/app/companies_v3_fixed_2.db"
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT i.name, p.name, m.subject, m.intro, m.social_proof
|
||||||
|
FROM marketing_matrix m
|
||||||
|
JOIN industries i ON m.industry_id = i.id
|
||||||
|
JOIN personas p ON m.persona_id = p.id
|
||||||
|
WHERE i.name = 'Healthcare - Hospital'
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor.execute(query)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
print(f"Industry: {row[0]} | Persona: {row[1]}")
|
||||||
|
print(f" Subject: {row[2]}")
|
||||||
|
print(f" Intro: {row[3]}")
|
||||||
|
print(f" Social Proof: {row[4]}")
|
||||||
|
print("-" * 50)
|
||||||
|
conn.close()
|
||||||
53
ARCHIVE_legacy_scripts/check_silly_billy.py
Normal file
53
ARCHIVE_legacy_scripts/check_silly_billy.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
|
||||||
|
DB_PATH = "companies_v3_fixed_2.db"
|
||||||
|
|
||||||
|
def check_company():
|
||||||
|
if not os.path.exists(DB_PATH):
|
||||||
|
print(f"❌ Database not found at {DB_PATH}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
print(f"🔍 Searching for 'Silly Billy' in {DB_PATH}...")
|
||||||
|
cursor.execute("SELECT id, name, crm_id, ai_opener, ai_opener_secondary, city, crm_vat, status FROM companies WHERE name LIKE '%Silly Billy%'")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
print("❌ No company found matching 'Silly Billy'")
|
||||||
|
else:
|
||||||
|
for row in rows:
|
||||||
|
company_id = row[0]
|
||||||
|
print("\n✅ Company Found:")
|
||||||
|
print(f" ID: {company_id}")
|
||||||
|
print(f" Name: {row[1]}")
|
||||||
|
print(f" CRM ID: {row[2]}")
|
||||||
|
print(f" Status: {row[7]}")
|
||||||
|
print(f" City: {row[5]}")
|
||||||
|
print(f" VAT: {row[6]}")
|
||||||
|
print(f" Opener (Primary): {row[3][:50]}..." if row[3] else " Opener (Primary): None")
|
||||||
|
|
||||||
|
# Check Enrichment Data
|
||||||
|
print(f"\n 🔍 Checking Enrichment Data for ID {company_id}...")
|
||||||
|
cursor.execute("SELECT content FROM enrichment_data WHERE company_id = ? AND source_type = 'website_scrape'", (company_id,))
|
||||||
|
enrich_row = cursor.fetchone()
|
||||||
|
if enrich_row:
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
data = json.loads(enrich_row[0])
|
||||||
|
imp = data.get("impressum")
|
||||||
|
print(f" Impressum Data in Scrape: {json.dumps(imp, indent=2) if imp else 'None'}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Error parsing JSON: {e}")
|
||||||
|
else:
|
||||||
|
print(" ❌ No website_scrape enrichment data found.")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error reading DB: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
check_company()
|
||||||
31
ARCHIVE_legacy_scripts/clear_zombies.py
Normal file
31
ARCHIVE_legacy_scripts/clear_zombies.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import sqlite3
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
DB_PATH = "/app/connector_queue.db"
|
||||||
|
|
||||||
|
def clear_all_zombies():
|
||||||
|
print("🧹 Cleaning up Zombie Jobs (PROCESSING for too long)...")
|
||||||
|
# A job that is PROCESSING for more than 10 minutes is likely dead
|
||||||
|
threshold = (datetime.utcnow() - timedelta(minutes=10)).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
with sqlite3.connect(DB_PATH) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 1. Identify Zombies
|
||||||
|
cursor.execute("SELECT id, updated_at FROM jobs WHERE status = 'PROCESSING' AND updated_at < ?", (threshold,))
|
||||||
|
zombies = cursor.fetchall()
|
||||||
|
|
||||||
|
if not zombies:
|
||||||
|
print("✅ No zombies found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"🕵️ Found {len(zombies)} zombie jobs.")
|
||||||
|
for zid, updated in zombies:
|
||||||
|
print(f" - Zombie ID {zid} (Last active: {updated})")
|
||||||
|
|
||||||
|
# 2. Kill them
|
||||||
|
cursor.execute("UPDATE jobs SET status = 'FAILED', error_msg = 'Zombie cleared: Process timed out' WHERE status = 'PROCESSING' AND updated_at < ?", (threshold,))
|
||||||
|
print(f"✅ Successfully cleared {cursor.rowcount} zombie(s).")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
clear_all_zombies()
|
||||||
49
ARCHIVE_legacy_scripts/debug_connector_status.py
Normal file
49
ARCHIVE_legacy_scripts/debug_connector_status.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import sqlite3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
DB_PATH = "connector_queue.db"
|
||||||
|
|
||||||
|
def inspect_queue():
|
||||||
|
if not os.path.exists(DB_PATH):
|
||||||
|
print(f"❌ Database not found at {DB_PATH}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"🔍 Inspecting Queue: {DB_PATH}")
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Get stats
|
||||||
|
cursor.execute("SELECT status, COUNT(*) FROM jobs GROUP BY status")
|
||||||
|
stats = dict(cursor.fetchall())
|
||||||
|
print(f"\n📊 Stats: {stats}")
|
||||||
|
|
||||||
|
# Get recent jobs
|
||||||
|
print("\n📝 Last 10 Jobs:")
|
||||||
|
cursor.execute("SELECT id, event_type, status, error_msg, updated_at, payload FROM jobs ORDER BY updated_at DESC LIMIT 10")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
payload = json.loads(row['payload'])
|
||||||
|
# Try to identify entity
|
||||||
|
entity = "Unknown"
|
||||||
|
if "PrimaryKey" in payload: entity = f"ID {payload['PrimaryKey']}"
|
||||||
|
if "ContactId" in payload: entity = f"Contact {payload['ContactId']}"
|
||||||
|
|
||||||
|
print(f" - Job #{row['id']} [{row['status']}] {row['event_type']} ({entity})")
|
||||||
|
print(f" Updated: {row['updated_at']}")
|
||||||
|
if row['error_msg']:
|
||||||
|
print(f" ❌ ERROR: {row['error_msg']}")
|
||||||
|
|
||||||
|
# Print payload details relevant to syncing
|
||||||
|
if row['status'] == 'COMPLETED':
|
||||||
|
pass # Maybe less interesting if success, but user says it didn't sync
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error reading DB: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
inspect_queue()
|
||||||
71
ARCHIVE_legacy_scripts/debug_meeting.py
Normal file
71
ARCHIVE_legacy_scripts/debug_meeting.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
DB_PATH = "transcription-tool/backend/meetings.db"
|
||||||
|
MEETING_ID = 5
|
||||||
|
|
||||||
|
def debug_meeting(db_path, meeting_id):
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
print(f"ERROR: Database file not found at {db_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Get Meeting Info
|
||||||
|
cursor.execute("SELECT id, title, status, duration_seconds FROM meetings WHERE id = ?", (meeting_id,))
|
||||||
|
meeting = cursor.fetchone()
|
||||||
|
|
||||||
|
if not meeting:
|
||||||
|
print(f"ERROR: No meeting found with ID {meeting_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("--- MEETING INFO ---")
|
||||||
|
print(f"ID: {meeting[0]}")
|
||||||
|
print(f"Title: {meeting[1]}")
|
||||||
|
print(f"Status: {meeting[2]}")
|
||||||
|
print(f"Duration (s): {meeting[3]}")
|
||||||
|
print("-" * 20)
|
||||||
|
|
||||||
|
# Get Chunks
|
||||||
|
cursor.execute("SELECT id, chunk_index, json_content FROM transcript_chunks WHERE meeting_id = ? ORDER BY chunk_index", (meeting_id,))
|
||||||
|
chunks = cursor.fetchall()
|
||||||
|
|
||||||
|
print(f"--- CHUNKS FOUND: {len(chunks)} ---")
|
||||||
|
for chunk in chunks:
|
||||||
|
chunk_id, chunk_index, json_content_str = chunk
|
||||||
|
print(f"\n--- Chunk ID: {chunk_id}, Index: {chunk_index} ---")
|
||||||
|
|
||||||
|
if not json_content_str:
|
||||||
|
print(" -> JSON content is EMPTY.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
json_content = json.loads(json_content_str)
|
||||||
|
print(f" -> Number of entries: {len(json_content)}")
|
||||||
|
|
||||||
|
if json_content:
|
||||||
|
# Print first 2 and last 2 entries to check for the "Ja" loop
|
||||||
|
print(" -> First 2 entries:")
|
||||||
|
for entry in json_content[:2]:
|
||||||
|
print(f" - {entry.get('display_time')} [{entry.get('speaker')}]: {entry.get('text')[:80]}...")
|
||||||
|
|
||||||
|
if len(json_content) > 4:
|
||||||
|
print(" -> Last 2 entries:")
|
||||||
|
for entry in json_content[-2:]:
|
||||||
|
print(f" - {entry.get('display_time')} [{entry.get('speaker')}]: {entry.get('text')[:80]}...")
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(" -> ERROR: Failed to decode JSON content.")
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
finally:
|
||||||
|
if 'conn' in locals() and conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
debug_meeting(DB_PATH, MEETING_ID)
|
||||||
13
ARCHIVE_legacy_scripts/debug_paths.py
Normal file
13
ARCHIVE_legacy_scripts/debug_paths.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import os
|
||||||
|
static_path = "/frontend_static"
|
||||||
|
print(f"Path {static_path} exists: {os.path.exists(static_path)}")
|
||||||
|
if os.path.exists(static_path):
|
||||||
|
for root, dirs, files in os.walk(static_path):
|
||||||
|
for file in files:
|
||||||
|
print(os.path.join(root, file))
|
||||||
|
else:
|
||||||
|
print("Listing /app instead:")
|
||||||
|
for root, dirs, files in os.walk("/app"):
|
||||||
|
if "node_modules" in root: continue
|
||||||
|
for file in files:
|
||||||
|
print(os.path.join(root, file))
|
||||||
70
ARCHIVE_legacy_scripts/debug_transcription_raw.py
Normal file
70
ARCHIVE_legacy_scripts/debug_transcription_raw.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import sqlite3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
DB_PATH = "transcripts.db"
|
||||||
|
|
||||||
|
def inspect_latest_meeting():
|
||||||
|
if not os.path.exists(DB_PATH):
|
||||||
|
print(f"Error: Database file '{DB_PATH}' not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Get latest meeting
|
||||||
|
cursor.execute("SELECT id, title, created_at FROM meetings ORDER BY created_at DESC LIMIT 1")
|
||||||
|
meeting = cursor.fetchone()
|
||||||
|
|
||||||
|
if not meeting:
|
||||||
|
print("No meetings found in DB.")
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
meeting_id, title, created_at = meeting
|
||||||
|
print(f"--- Inspecting Latest Meeting: ID {meeting_id} ('{title}') created at {created_at} ---")
|
||||||
|
|
||||||
|
# Get chunks for this meeting
|
||||||
|
cursor.execute("SELECT id, chunk_index, raw_text, json_content FROM transcript_chunks WHERE meeting_id = ? ORDER BY chunk_index", (meeting_id,))
|
||||||
|
chunks = cursor.fetchall()
|
||||||
|
|
||||||
|
if not chunks:
|
||||||
|
print("No chunks found for this meeting.")
|
||||||
|
|
||||||
|
for chunk in chunks:
|
||||||
|
chunk_id, idx, raw_text, json_content = chunk
|
||||||
|
print(f"\n[Chunk {idx} (ID: {chunk_id})]")
|
||||||
|
|
||||||
|
print(f"Stored JSON Content (Length): {len(json.loads(json_content)) if json_content else 'None/Empty'}")
|
||||||
|
|
||||||
|
print("-" * 20 + " RAW TEXT START " + "-" * 20)
|
||||||
|
print(raw_text[:500]) # Print first 500 chars
|
||||||
|
print("..." if len(raw_text) > 500 else "")
|
||||||
|
print("-" * 20 + " RAW TEXT END " + "-" * 20)
|
||||||
|
|
||||||
|
# Try to parse manually to see error
|
||||||
|
try:
|
||||||
|
# Simulate cleaning logic from orchestrator
|
||||||
|
cleaned = raw_text.strip()
|
||||||
|
if cleaned.startswith("```json"):
|
||||||
|
cleaned = cleaned[7:]
|
||||||
|
elif cleaned.startswith("```"):
|
||||||
|
cleaned = cleaned[3:]
|
||||||
|
if cleaned.endswith("```"):
|
||||||
|
cleaned = cleaned[:-3]
|
||||||
|
cleaned = cleaned.strip()
|
||||||
|
|
||||||
|
parsed = json.loads(cleaned)
|
||||||
|
print("✅ Manual Parsing Successful!")
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"❌ Manual Parsing Failed: {e}")
|
||||||
|
# Show context around error
|
||||||
|
if hasattr(e, 'pos'):
|
||||||
|
start = max(0, e.pos - 20)
|
||||||
|
end = min(len(cleaned), e.pos + 20)
|
||||||
|
print(f" Context at error: ...{cleaned[start:end]}...")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
inspect_latest_meeting()
|
||||||
16
ARCHIVE_legacy_scripts/debug_zombie.py
Normal file
16
ARCHIVE_legacy_scripts/debug_zombie.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
|
||||||
|
DB_PATH = "/app/connector_queue.db"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(f"📊 Accessing database at {DB_PATH}")
|
||||||
|
print("📊 Listing last 20 jobs in database...")
|
||||||
|
with sqlite3.connect(DB_PATH) as conn:
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT id, status, event_type, updated_at FROM jobs ORDER BY id DESC LIMIT 20")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
for r in rows:
|
||||||
|
print(f" - Job {r['id']}: {r['status']} ({r['event_type']}) - Updated: {r['updated_at']}")
|
||||||
|
|
||||||
41
ARCHIVE_legacy_scripts/fix_benni_data.py
Normal file
41
ARCHIVE_legacy_scripts/fix_benni_data.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Setup DB
|
||||||
|
DB_PATH = "sqlite:///companies_v3_fixed_2.db"
|
||||||
|
engine = create_engine(DB_PATH)
|
||||||
|
SessionLocal = sessionmaker(bind=engine)
|
||||||
|
session = SessionLocal()
|
||||||
|
|
||||||
|
from sqlalchemy import Column, Integer, String
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Company(Base):
|
||||||
|
__tablename__ = "companies"
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
street = Column(String)
|
||||||
|
zip_code = Column(String)
|
||||||
|
|
||||||
|
def fix_benni():
|
||||||
|
company_id = 33
|
||||||
|
print(f"🔧 Fixing Address for Company ID {company_id}...")
|
||||||
|
|
||||||
|
company = session.query(Company).filter_by(id=company_id).first()
|
||||||
|
if not company:
|
||||||
|
print("❌ Company not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Hardcoded from previous check_benni.py output to be safe/fast
|
||||||
|
# "street": "Eriagstraße 58", "zip": "85053"
|
||||||
|
|
||||||
|
company.street = "Eriagstraße 58"
|
||||||
|
company.zip_code = "85053"
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
print(f"✅ Database updated: Street='{company.street}', Zip='{company.zip_code}'")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fix_benni()
|
||||||
70
ARCHIVE_legacy_scripts/fix_industry_units.py
Normal file
70
ARCHIVE_legacy_scripts/fix_industry_units.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
DB_PATH = "companies_v3_fixed_2.db"
|
||||||
|
|
||||||
|
UNIT_MAPPING = {
|
||||||
|
"Logistics - Warehouse": "m²",
|
||||||
|
"Healthcare - Hospital": "Betten",
|
||||||
|
"Infrastructure - Transport": "Passagiere",
|
||||||
|
"Leisure - Indoor Active": "m²",
|
||||||
|
"Retail - Food": "m²",
|
||||||
|
"Retail - Shopping Center": "m²",
|
||||||
|
"Hospitality - Gastronomy": "Sitzplätze",
|
||||||
|
"Leisure - Outdoor Park": "Besucher",
|
||||||
|
"Leisure - Wet & Spa": "Besucher",
|
||||||
|
"Infrastructure - Public": "Kapazität",
|
||||||
|
"Retail - Non-Food": "m²",
|
||||||
|
"Hospitality - Hotel": "Zimmer",
|
||||||
|
"Leisure - Entertainment": "Besucher",
|
||||||
|
"Healthcare - Care Home": "Plätze",
|
||||||
|
"Industry - Manufacturing": "Mitarbeiter",
|
||||||
|
"Energy - Grid & Utilities": "Kunden",
|
||||||
|
"Leisure - Fitness": "Mitglieder",
|
||||||
|
"Corporate - Campus": "Mitarbeiter",
|
||||||
|
"Energy - Solar/Wind": "MWp",
|
||||||
|
"Tech - Data Center": "Racks",
|
||||||
|
"Automotive - Dealer": "Fahrzeuge",
|
||||||
|
"Infrastructure Parking": "Stellplätze",
|
||||||
|
"Reinigungsdienstleister": "Mitarbeiter",
|
||||||
|
"Infrastructure - Communities": "Einwohner"
|
||||||
|
}
|
||||||
|
|
||||||
|
def fix_units():
|
||||||
|
print(f"Connecting to {DB_PATH}...")
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT id, name, scraper_search_term, metric_type FROM industries")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
updated_count = 0
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
ind_id, name, current_term, m_type = row
|
||||||
|
|
||||||
|
new_term = UNIT_MAPPING.get(name)
|
||||||
|
|
||||||
|
# Fallback Logic
|
||||||
|
if not new_term:
|
||||||
|
if m_type in ["AREA_IN", "AREA_OUT"]:
|
||||||
|
new_term = "m²"
|
||||||
|
else:
|
||||||
|
new_term = "Anzahl" # Generic fallback
|
||||||
|
|
||||||
|
if current_term != new_term:
|
||||||
|
print(f"Updating '{name}': '{current_term}' -> '{new_term}'")
|
||||||
|
cursor.execute("UPDATE industries SET scraper_search_term = ? WHERE id = ?", (new_term, ind_id))
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print(f"\n✅ Updated {updated_count} industries with correct units.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
conn.rollback()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fix_units()
|
||||||
23
ARCHIVE_legacy_scripts/fix_mappings_v2.py
Normal file
23
ARCHIVE_legacy_scripts/fix_mappings_v2.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
def fix_mappings():
|
||||||
|
conn = sqlite3.connect('/app/companies_v3_fixed_2.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Neue Mappings für Geschäftsleitung und Verallgemeinerung
|
||||||
|
new_rules = [
|
||||||
|
('%leitung%', 'Wirtschaftlicher Entscheider'),
|
||||||
|
('%vorstand%', 'Wirtschaftlicher Entscheider'),
|
||||||
|
('%geschäftsleitung%', 'Wirtschaftlicher Entscheider'),
|
||||||
|
('%management%', 'Wirtschaftlicher Entscheider')
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern, role in new_rules:
|
||||||
|
cursor.execute("INSERT OR REPLACE INTO job_role_mappings (pattern, role, created_at) VALUES (?, ?, '2026-02-22T15:30:00')", (pattern, role))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print("Mappings updated for Geschäftsleitung, Vorstand, Management.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fix_mappings()
|
||||||
90
ARCHIVE_legacy_scripts/fix_silly_billy_data.py
Normal file
90
ARCHIVE_legacy_scripts/fix_silly_billy_data.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Setup DB
|
||||||
|
DB_PATH = "sqlite:///companies_v3_fixed_2.db"
|
||||||
|
engine = create_engine(DB_PATH)
|
||||||
|
SessionLocal = sessionmaker(bind=engine)
|
||||||
|
session = SessionLocal()
|
||||||
|
|
||||||
|
# Import Models (Simplified for script)
|
||||||
|
from sqlalchemy import Column, Integer, String, Text, JSON
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Company(Base):
|
||||||
|
__tablename__ = "companies"
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String)
|
||||||
|
city = Column(String)
|
||||||
|
country = Column(String)
|
||||||
|
crm_vat = Column(String)
|
||||||
|
street = Column(String)
|
||||||
|
zip_code = Column(String)
|
||||||
|
|
||||||
|
class EnrichmentData(Base):
|
||||||
|
__tablename__ = "enrichment_data"
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
company_id = Column(Integer)
|
||||||
|
source_type = Column(String)
|
||||||
|
content = Column(JSON)
|
||||||
|
|
||||||
|
def fix_data():
|
||||||
|
company_id = 32
|
||||||
|
print(f"🔧 Fixing Data for Company ID {company_id}...")
|
||||||
|
|
||||||
|
company = session.query(Company).filter_by(id=company_id).first()
|
||||||
|
if not company:
|
||||||
|
print("❌ Company not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
enrichment = session.query(EnrichmentData).filter_by(
|
||||||
|
company_id=company_id, source_type="website_scrape"
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if enrichment and enrichment.content:
|
||||||
|
imp = enrichment.content.get("impressum")
|
||||||
|
if imp:
|
||||||
|
print(f"📄 Found Impressum: {imp}")
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
if imp.get("city"):
|
||||||
|
company.city = imp.get("city")
|
||||||
|
changed = True
|
||||||
|
print(f" -> Set City: {company.city}")
|
||||||
|
|
||||||
|
if imp.get("vat_id"):
|
||||||
|
company.crm_vat = imp.get("vat_id")
|
||||||
|
changed = True
|
||||||
|
print(f" -> Set VAT: {company.crm_vat}")
|
||||||
|
|
||||||
|
if imp.get("country_code"):
|
||||||
|
company.country = imp.get("country_code")
|
||||||
|
changed = True
|
||||||
|
print(f" -> Set Country: {company.country}")
|
||||||
|
|
||||||
|
if imp.get("street"):
|
||||||
|
company.street = imp.get("street")
|
||||||
|
changed = True
|
||||||
|
print(f" -> Set Street: {company.street}")
|
||||||
|
|
||||||
|
if imp.get("zip"):
|
||||||
|
company.zip_code = imp.get("zip")
|
||||||
|
changed = True
|
||||||
|
print(f" -> Set Zip: {company.zip_code}")
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
session.commit()
|
||||||
|
print("✅ Database updated.")
|
||||||
|
else:
|
||||||
|
print("ℹ️ No changes needed.")
|
||||||
|
else:
|
||||||
|
print("⚠️ No impressum data in enrichment.")
|
||||||
|
else:
|
||||||
|
print("⚠️ No enrichment data found.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fix_data()
|
||||||
30
ARCHIVE_legacy_scripts/list_all_companies.py
Normal file
30
ARCHIVE_legacy_scripts/list_all_companies.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
|
||||||
|
DB_PATH = "companies_v3_fixed_2.db"
|
||||||
|
|
||||||
|
def list_companies():
|
||||||
|
if not os.path.exists(DB_PATH):
|
||||||
|
print(f"❌ Database not found at {DB_PATH}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
print(f"🔍 Listing companies in {DB_PATH}...")
|
||||||
|
cursor.execute("SELECT id, name, crm_id, city, crm_vat FROM companies ORDER BY id DESC LIMIT 20")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
print("❌ No companies found")
|
||||||
|
else:
|
||||||
|
for row in rows:
|
||||||
|
print(f" ID: {row[0]} | Name: {row[1]} | CRM ID: {row[2]} | City: {row[3]} | VAT: {row[4]}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error reading DB: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
list_companies()
|
||||||
18
ARCHIVE_legacy_scripts/list_industries.py
Normal file
18
ARCHIVE_legacy_scripts/list_industries.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.join(os.path.dirname(__file__), "company-explorer"))
|
||||||
|
from backend.database import SessionLocal, Industry
|
||||||
|
|
||||||
|
def list_industries():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
industries = db.query(Industry.name).all()
|
||||||
|
print("Available Industries:")
|
||||||
|
for (name,) in industries:
|
||||||
|
print(f"- {name}")
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
list_industries()
|
||||||
12
ARCHIVE_legacy_scripts/list_industries_db.py
Normal file
12
ARCHIVE_legacy_scripts/list_industries_db.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
DB_PATH = "/app/companies_v3_fixed_2.db"
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("SELECT name FROM industries")
|
||||||
|
industries = cursor.fetchall()
|
||||||
|
print("Available Industries:")
|
||||||
|
for ind in industries:
|
||||||
|
print(f"- {ind[0]}")
|
||||||
|
conn.close()
|
||||||
120
ARCHIVE_legacy_scripts/market_db_manager.py
Normal file
120
ARCHIVE_legacy_scripts/market_db_manager.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import sqlite3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
DB_PATH = os.environ.get("DB_PATH", "/app/market_intelligence.db")
|
||||||
|
|
||||||
|
def get_db_connection():
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
conn = get_db_connection()
|
||||||
|
# Flexible schema: We store almost everything in a 'data' JSON column
|
||||||
|
conn.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS projects (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
data JSON NOT NULL
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def save_project(project_data):
|
||||||
|
"""
|
||||||
|
Saves a project. If 'id' exists in data, updates it. Otherwise creates new.
|
||||||
|
"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
try:
|
||||||
|
project_id = project_data.get('id')
|
||||||
|
|
||||||
|
# Extract a name for the list view (e.g. from companyName or referenceUrl)
|
||||||
|
# We assume the frontend passes a 'name' field, or we derive it.
|
||||||
|
name = project_data.get('name') or project_data.get('companyName') or "Untitled Project"
|
||||||
|
|
||||||
|
if not project_id:
|
||||||
|
# Create New
|
||||||
|
project_id = str(uuid.uuid4())
|
||||||
|
project_data['id'] = project_id
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
'INSERT INTO projects (id, name, data) VALUES (?, ?, ?)',
|
||||||
|
(project_id, name, json.dumps(project_data))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Update Existing
|
||||||
|
conn.execute(
|
||||||
|
'''UPDATE projects
|
||||||
|
SET name = ?, data = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?''',
|
||||||
|
(name, json.dumps(project_data), project_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
return {"id": project_id, "status": "saved"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def get_all_projects():
|
||||||
|
conn = get_db_connection()
|
||||||
|
projects = conn.execute('SELECT id, name, created_at, updated_at FROM projects ORDER BY updated_at DESC').fetchall()
|
||||||
|
conn.close()
|
||||||
|
return [dict(ix) for ix in projects]
|
||||||
|
|
||||||
|
def load_project(project_id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
project = conn.execute('SELECT data FROM projects WHERE id = ?', (project_id,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if project:
|
||||||
|
return json.loads(project['data'])
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete_project(project_id):
|
||||||
|
conn = get_db_connection()
|
||||||
|
try:
|
||||||
|
conn.execute('DELETE FROM projects WHERE id = ?', (project_id,))
|
||||||
|
conn.commit()
|
||||||
|
return {"status": "deleted", "id": project_id}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
# Simple CLI for Node.js bridge
|
||||||
|
# Usage: python market_db_manager.py [init|list|save|load|delete] [args...]
|
||||||
|
|
||||||
|
mode = sys.argv[1]
|
||||||
|
|
||||||
|
if mode == "init":
|
||||||
|
init_db()
|
||||||
|
print(json.dumps({"status": "initialized"}))
|
||||||
|
|
||||||
|
elif mode == "list":
|
||||||
|
print(json.dumps(get_all_projects()))
|
||||||
|
|
||||||
|
elif mode == "save":
|
||||||
|
# Data is passed as a JSON string file path to avoid command line length limits
|
||||||
|
data_file = sys.argv[2]
|
||||||
|
with open(data_file, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
print(json.dumps(save_project(data)))
|
||||||
|
|
||||||
|
elif mode == "load":
|
||||||
|
p_id = sys.argv[2]
|
||||||
|
result = load_project(p_id)
|
||||||
|
print(json.dumps(result if result else {"error": "Project not found"}))
|
||||||
|
|
||||||
|
elif mode == "delete":
|
||||||
|
p_id = sys.argv[2]
|
||||||
|
print(json.dumps(delete_project(p_id)))
|
||||||
29
ARCHIVE_legacy_scripts/migrate_opener_native.py
Normal file
29
ARCHIVE_legacy_scripts/migrate_opener_native.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
DB_PATH = "/app/companies_v3_fixed_2.db"
|
||||||
|
|
||||||
|
def migrate():
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
print(f"Checking schema in {DB_PATH}...")
|
||||||
|
cursor.execute("PRAGMA table_info(companies)")
|
||||||
|
columns = [row[1] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
if "ai_opener" in columns:
|
||||||
|
print("Column 'ai_opener' already exists. Skipping.")
|
||||||
|
else:
|
||||||
|
print("Adding column 'ai_opener' to 'companies' table...")
|
||||||
|
cursor.execute("ALTER TABLE companies ADD COLUMN ai_opener TEXT")
|
||||||
|
conn.commit()
|
||||||
|
print("✅ Migration successful.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Migration failed: {e}")
|
||||||
|
finally:
|
||||||
|
if conn: conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
migrate()
|
||||||
29
ARCHIVE_legacy_scripts/migrate_opener_secondary.py
Normal file
29
ARCHIVE_legacy_scripts/migrate_opener_secondary.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
DB_PATH = "/app/companies_v3_fixed_2.db"
|
||||||
|
|
||||||
|
def migrate():
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
print(f"Checking schema in {DB_PATH}...")
|
||||||
|
cursor.execute("PRAGMA table_info(companies)")
|
||||||
|
columns = [row[1] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
if "ai_opener_secondary" in columns:
|
||||||
|
print("Column 'ai_opener_secondary' already exists. Skipping.")
|
||||||
|
else:
|
||||||
|
print("Adding column 'ai_opener_secondary' to 'companies' table...")
|
||||||
|
cursor.execute("ALTER TABLE companies ADD COLUMN ai_opener_secondary TEXT")
|
||||||
|
conn.commit()
|
||||||
|
print("✅ Migration successful.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Migration failed: {e}")
|
||||||
|
finally:
|
||||||
|
if conn: conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
migrate()
|
||||||
30
ARCHIVE_legacy_scripts/migrate_personas_v2.py
Normal file
30
ARCHIVE_legacy_scripts/migrate_personas_v2.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
|
||||||
|
DB_PATH = "/app/companies_v3_fixed_2.db"
|
||||||
|
|
||||||
|
def migrate_personas():
|
||||||
|
print(f"Adding new columns to 'personas' table in {DB_PATH}...")
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
columns_to_add = [
|
||||||
|
("description", "TEXT"),
|
||||||
|
("convincing_arguments", "TEXT"),
|
||||||
|
("typical_positions", "TEXT"),
|
||||||
|
("kpis", "TEXT")
|
||||||
|
]
|
||||||
|
|
||||||
|
for col_name, col_type in columns_to_add:
|
||||||
|
try:
|
||||||
|
cursor.execute(f"ALTER TABLE personas ADD COLUMN {col_name} {col_type}")
|
||||||
|
print(f" Added column: {col_name}")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
print(f" Column {col_name} already exists.")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print("Migration complete.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
migrate_personas()
|
||||||
37
ARCHIVE_legacy_scripts/read_matrix_entry.py
Normal file
37
ARCHIVE_legacy_scripts/read_matrix_entry.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.join(os.path.dirname(__file__), "company-explorer"))
|
||||||
|
from backend.database import SessionLocal, Industry, Persona, MarketingMatrix
|
||||||
|
|
||||||
|
def read_specific_entry(industry_name: str, persona_name: str):
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
entry = (
|
||||||
|
db.query(MarketingMatrix)
|
||||||
|
.join(Industry)
|
||||||
|
.join(Persona)
|
||||||
|
.filter(Industry.name == industry_name, Persona.name == persona_name)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not entry:
|
||||||
|
print(f"No entry found for {industry_name} and {persona_name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("--- Generated Text ---")
|
||||||
|
print(f"Industry: {industry_name}")
|
||||||
|
print(f"Persona: {persona_name}")
|
||||||
|
print("\n[Intro]")
|
||||||
|
print(entry.intro)
|
||||||
|
print("\n[Social Proof]")
|
||||||
|
print(entry.social_proof)
|
||||||
|
print("----------------------")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
read_specific_entry("Healthcare - Hospital", "Infrastruktur-Verantwortlicher")
|
||||||
|
|
||||||
|
|
||||||
92
ARCHIVE_legacy_scripts/standalone_importer.py
Normal file
92
ARCHIVE_legacy_scripts/standalone_importer.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import csv
|
||||||
|
from collections import Counter
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# --- Standalone Configuration ---
|
||||||
|
DATABASE_URL = "sqlite:////app/companies_v3_fixed_2.db"
|
||||||
|
LOG_FILE = "/app/Log_from_docker/standalone_importer.log"
|
||||||
|
|
||||||
|
# --- Logging Setup ---
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler(LOG_FILE),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# --- SQLAlchemy Models (simplified, only what's needed) ---
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class RawJobTitle(Base):
|
||||||
|
__tablename__ = 'raw_job_titles'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
title = Column(String, unique=True, index=True)
|
||||||
|
count = Column(Integer, default=1)
|
||||||
|
source = Column(String, default="import")
|
||||||
|
is_mapped = Column(Boolean, default=False)
|
||||||
|
created_at = Column(DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
# --- Database Connection ---
|
||||||
|
engine = create_engine(DATABASE_URL)
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
def import_job_titles_standalone(file_path: str):
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
logger.info(f"Starting standalone import of job titles from {file_path}")
|
||||||
|
|
||||||
|
job_title_counts = Counter()
|
||||||
|
total_rows = 0
|
||||||
|
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
reader = csv.reader(f)
|
||||||
|
for row in reader:
|
||||||
|
if row and row[0].strip():
|
||||||
|
title = row[0].strip()
|
||||||
|
job_title_counts[title] += 1
|
||||||
|
total_rows += 1
|
||||||
|
|
||||||
|
logger.info(f"Read {total_rows} total job title entries. Found {len(job_title_counts)} unique titles.")
|
||||||
|
|
||||||
|
added_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
|
||||||
|
for title, count in job_title_counts.items():
|
||||||
|
existing_title = db.query(RawJobTitle).filter(RawJobTitle.title == title).first()
|
||||||
|
if existing_title:
|
||||||
|
if existing_title.count != count:
|
||||||
|
existing_title.count = count
|
||||||
|
updated_count += 1
|
||||||
|
else:
|
||||||
|
new_title = RawJobTitle(title=title, count=count, source="csv_import", is_mapped=False)
|
||||||
|
db.add(new_title)
|
||||||
|
added_count += 1
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
logger.info(f"Standalone import complete. Added {added_count} new unique titles, updated {updated_count} existing titles.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error during standalone job title import: {e}", exc_info=True)
|
||||||
|
db.rollback()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Standalone script to import job titles from a CSV file.")
|
||||||
|
parser.add_argument("file_path", type=str, help="Path to the CSV file containing job titles.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Ensure the log directory exists
|
||||||
|
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
|
||||||
|
|
||||||
|
import_job_titles_standalone(args.file_path)
|
||||||
22
ARCHIVE_legacy_scripts/test_api_logic.py
Normal file
22
ARCHIVE_legacy_scripts/test_api_logic.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add the company-explorer directory to the Python path
|
||||||
|
sys.path.append(os.path.abspath(os.path.join(os.getcwd(), 'company-explorer')))
|
||||||
|
|
||||||
|
from backend.database import SessionLocal, MarketingMatrix, Industry, Persona
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
query = db.query(MarketingMatrix).options(
|
||||||
|
joinedload(MarketingMatrix.industry),
|
||||||
|
joinedload(MarketingMatrix.persona)
|
||||||
|
)
|
||||||
|
entries = query.all()
|
||||||
|
print(f"Total entries: {len(entries)}")
|
||||||
|
for e in entries[:3]:
|
||||||
|
print(f"ID={e.id}, Industry={e.industry.name if e.industry else 'N/A'}, Persona={e.persona.name if e.persona else 'N/A'}")
|
||||||
|
print(f" Subject: {e.subject}")
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
98
ARCHIVE_legacy_scripts/test_company_explorer_connector.py
Normal file
98
ARCHIVE_legacy_scripts/test_company_explorer_connector.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Den Pfad anpassen, damit das Modul gefunden wird
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
from check_company_existence import check_company_existence_with_company_explorer
|
||||||
|
|
||||||
|
class TestCompanyExistenceChecker(unittest.TestCase):
|
||||||
|
|
||||||
|
@patch('check_company_existence.requests.get')
|
||||||
|
def test_company_exists_exact_match(self, mock_get):
|
||||||
|
"""Testet, ob ein exakt passendes Unternehmen korrekt als 'existent' erkannt wird."""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
"total": 1,
|
||||||
|
"items": [
|
||||||
|
{"id": 123, "name": "TestCorp"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
result = check_company_existence_with_company_explorer("TestCorp")
|
||||||
|
|
||||||
|
self.assertTrue(result["exists"])
|
||||||
|
self.assertEqual(result["company_id"], 123)
|
||||||
|
self.assertEqual(result["company_name"], "TestCorp")
|
||||||
|
|
||||||
|
@patch('check_company_existence.requests.get')
|
||||||
|
def test_company_does_not_exist(self, mock_get):
|
||||||
|
"""Testet, ob ein nicht existentes Unternehmen korrekt als 'nicht existent' erkannt wird."""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {"total": 0, "items": []}
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
result = check_company_existence_with_company_explorer("NonExistentCorp")
|
||||||
|
|
||||||
|
self.assertFalse(result["exists"])
|
||||||
|
self.assertIn("not found", result["message"])
|
||||||
|
|
||||||
|
@patch('check_company_existence.requests.get')
|
||||||
|
def test_company_partial_match_only(self, mock_get):
|
||||||
|
"""Testet den Fall, in dem die Suche Ergebnisse liefert, aber kein exakter Match dabei ist."""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
"total": 1,
|
||||||
|
"items": [
|
||||||
|
{"id": 124, "name": "TestCorp Inc"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
result = check_company_existence_with_company_explorer("TestCorp")
|
||||||
|
|
||||||
|
self.assertFalse(result["exists"])
|
||||||
|
self.assertIn("not found as an exact match", result["message"])
|
||||||
|
|
||||||
|
@patch('check_company_existence.requests.get')
|
||||||
|
def test_http_error_handling(self, mock_get):
|
||||||
|
"""Testet das Fehlerhandling bei einem HTTP 401 Unauthorized Error."""
|
||||||
|
# Importiere requests innerhalb des Test-Scopes, um den side_effect zu verwenden
|
||||||
|
import requests
|
||||||
|
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 401
|
||||||
|
mock_response.text = "Unauthorized"
|
||||||
|
# Die raise_for_status Methode muss eine Ausnahme auslösen
|
||||||
|
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("401 Client Error: Unauthorized for url")
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
result = check_company_existence_with_company_explorer("AnyCompany")
|
||||||
|
|
||||||
|
self.assertFalse(result["exists"])
|
||||||
|
self.assertIn("HTTP error occurred", result["error"])
|
||||||
|
|
||||||
|
@patch('check_company_existence.requests.get')
|
||||||
|
def test_connection_error_handling(self, mock_get):
|
||||||
|
"""Testet das Fehlerhandling bei einem Connection Error."""
|
||||||
|
# Importiere requests hier, damit die Ausnahme im Patch-Kontext ist
|
||||||
|
import requests
|
||||||
|
mock_get.side_effect = requests.exceptions.ConnectionError("Connection failed")
|
||||||
|
|
||||||
|
result = check_company_existence_with_company_explorer("AnyCompany")
|
||||||
|
|
||||||
|
self.assertFalse(result["exists"])
|
||||||
|
self.assertIn("Connection error occurred", result["error"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Füge 'requests' zum globalen Scope hinzu, damit es im Test-HTTP-Error-Handling-Test verwendet werden kann
|
||||||
|
import requests
|
||||||
|
unittest.main(argv=['first-arg-is-ignored'], exit=False)
|
||||||
31
ARCHIVE_legacy_scripts/test_explorer_connection.py
Normal file
31
ARCHIVE_legacy_scripts/test_explorer_connection.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import requests
|
||||||
|
import os
|
||||||
|
from requests.auth import HTTPBasicAuth
|
||||||
|
|
||||||
|
def test_connection(url, name):
|
||||||
|
print(f"--- Testing {name}: {url} ---")
|
||||||
|
try:
|
||||||
|
# We try the health endpoint
|
||||||
|
response = requests.get(
|
||||||
|
f"{url}/health",
|
||||||
|
auth=HTTPBasicAuth("admin", "gemini"),
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
print(f"Response: {response.text}")
|
||||||
|
return response.status_code == 200
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Path 1: Hardcoded LAN IP through Proxy
|
||||||
|
url_lan = "http://192.168.178.6:8090/ce/api"
|
||||||
|
# Path 2: Internal Docker Networking (direct)
|
||||||
|
url_docker = "http://company-explorer:8000/api"
|
||||||
|
|
||||||
|
success_lan = test_connection(url_lan, "LAN IP (Proxy)")
|
||||||
|
print("\n")
|
||||||
|
success_docker = test_connection(url_docker, "Docker Internal (Direct)")
|
||||||
|
|
||||||
|
if not success_lan and not success_docker:
|
||||||
|
print("\nFATAL: Company Explorer not reachable from this container.")
|
||||||
91
ARCHIVE_legacy_scripts/test_opener_api.py
Normal file
91
ARCHIVE_legacy_scripts/test_opener_api.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Load credentials from .env
|
||||||
|
# Simple manual parser to avoid dependency on python-dotenv
|
||||||
|
def load_env(path):
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print(f"Warning: .env file not found at {path}")
|
||||||
|
return
|
||||||
|
with open(path) as f:
|
||||||
|
for line in f:
|
||||||
|
if line.strip() and not line.startswith('#'):
|
||||||
|
key, val = line.strip().split('=', 1)
|
||||||
|
os.environ.setdefault(key, val)
|
||||||
|
|
||||||
|
load_env('/app/.env')
|
||||||
|
|
||||||
|
API_USER = os.getenv("API_USER", "admin")
|
||||||
|
API_PASS = os.getenv("API_PASSWORD", "gemini")
|
||||||
|
CE_URL = "http://127.0.0.1:8000" # Target the local container (assuming port 8000 is mapped)
|
||||||
|
TEST_CONTACT_ID = 1 # Therme Erding
|
||||||
|
|
||||||
|
def run_test():
|
||||||
|
print("🚀 STARTING API-LEVEL E2E TEXT GENERATION TEST\n")
|
||||||
|
|
||||||
|
# --- Health Check ---
|
||||||
|
print("Waiting for Company Explorer API to be ready...")
|
||||||
|
for i in range(10):
|
||||||
|
try:
|
||||||
|
health_resp = requests.get(f"{CE_URL}/api/health", auth=(API_USER, API_PASS), timeout=2)
|
||||||
|
if health_resp.status_code == 200:
|
||||||
|
print("✅ API is ready.")
|
||||||
|
break
|
||||||
|
except requests.exceptions.RequestException:
|
||||||
|
pass
|
||||||
|
if i == 9:
|
||||||
|
print("❌ API not ready after 20 seconds. Aborting.")
|
||||||
|
return False
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
scenarios = [
|
||||||
|
{"name": "Infrastructure Role", "job_title": "Facility Manager", "opener_field": "opener", "keyword": "Sicherheit"},
|
||||||
|
{"name": "Operational Role", "job_title": "Leiter Badbetrieb", "opener_field": "opener_secondary", "keyword": "Gäste"}
|
||||||
|
]
|
||||||
|
|
||||||
|
all_passed = True
|
||||||
|
for s in scenarios:
|
||||||
|
print(f"--- Testing: {s['name']} ---")
|
||||||
|
endpoint = f"{CE_URL}/api/provision/superoffice-contact"
|
||||||
|
payload = {
|
||||||
|
"so_contact_id": TEST_CONTACT_ID,
|
||||||
|
"job_title": s['job_title']
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.post(endpoint, json=payload, auth=(API_USER, API_PASS))
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
|
||||||
|
# --- Assertions ---
|
||||||
|
opener = data.get('opener')
|
||||||
|
opener_sec = data.get('opener_secondary')
|
||||||
|
|
||||||
|
assert opener, "❌ FAIL: Primary opener is missing!"
|
||||||
|
print(f" ✅ Primary Opener: '{opener}'")
|
||||||
|
|
||||||
|
assert opener_sec, "❌ FAIL: Secondary opener is missing!"
|
||||||
|
print(f" ✅ Secondary Opener: '{opener_sec}'")
|
||||||
|
|
||||||
|
target_opener_text = data.get(s['opener_field'])
|
||||||
|
assert s['keyword'].lower() in target_opener_text.lower(), f"❌ FAIL: Keyword '{s['keyword']}' not in '{s['opener_field']}'!"
|
||||||
|
print(f" ✅ Keyword '{s['keyword']}' found in correct opener.")
|
||||||
|
|
||||||
|
print(f"--- ✅ PASSED: {s['name']} ---\\n")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ TEST FAILED: {e}")
|
||||||
|
if hasattr(e, 'response') and e.response is not None:
|
||||||
|
print(f" Response: {e.response.text}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
return all_passed
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if run_test():
|
||||||
|
print("🏁 All scenarios passed successfully!")
|
||||||
|
else:
|
||||||
|
print("🔥 Some scenarios failed.")
|
||||||
|
sys.exit(1)
|
||||||
12
ARCHIVE_legacy_scripts/test_provisioning_api.py
Normal file
12
ARCHIVE_legacy_scripts/test_provisioning_api.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
url = "http://company-explorer:8000/api/provision/superoffice-contact"
|
||||||
|
payload = {"so_contact_id": 4}
|
||||||
|
auth = ("admin", "gemini")
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.post(url, json=payload, auth=auth)
|
||||||
|
print(json.dumps(resp.json(), indent=2))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
99
ARCHIVE_legacy_scripts/trading_twins_tool.py
Normal file
99
ARCHIVE_legacy_scripts/trading_twins_tool.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import json
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Ensure we can import from lead-engine
|
||||||
|
sys.path.append(os.path.join(os.path.dirname(__file__), 'lead-engine'))
|
||||||
|
try:
|
||||||
|
from trading_twins_ingest import process_leads
|
||||||
|
except ImportError:
|
||||||
|
print("Warning: Could not import trading_twins_ingest from lead-engine. Email ingestion disabled.")
|
||||||
|
process_leads = None
|
||||||
|
|
||||||
|
from company_explorer_connector import handle_company_workflow
|
||||||
|
|
||||||
|
def run_trading_twins_process(target_company_name: str):
|
||||||
|
"""
|
||||||
|
Startet den Trading Twins Prozess für ein Zielunternehmen.
|
||||||
|
Ruft den Company Explorer Workflow auf, um das Unternehmen zu finden,
|
||||||
|
zu erstellen oder anzureichern.
|
||||||
|
"""
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print(f"Starte Trading Twins Analyse für: {target_company_name}")
|
||||||
|
print(f"{'='*50}\n")
|
||||||
|
|
||||||
|
# Aufruf des Company Explorer Workflows
|
||||||
|
# Diese Funktion prüft, ob die Firma existiert.
|
||||||
|
# Wenn nicht, erstellt sie die Firma und startet die Anreicherung.
|
||||||
|
# Sie gibt am Ende die Daten aus dem Company Explorer zurück.
|
||||||
|
company_data_result = handle_company_workflow(target_company_name)
|
||||||
|
|
||||||
|
# Verarbeitung der Rückgabe (für den POC genügt eine Ausgabe)
|
||||||
|
print("\n--- Ergebnis vom Company Explorer Connector (für Trading Twins) ---")
|
||||||
|
|
||||||
|
status = company_data_result.get("status")
|
||||||
|
data = company_data_result.get("data")
|
||||||
|
|
||||||
|
if status == "error":
|
||||||
|
print(f"Ein Fehler ist aufgetreten: {company_data_result.get('message')}")
|
||||||
|
elif status == "found":
|
||||||
|
print(f"Unternehmen gefunden. ID: {data.get('id')}, Name: {data.get('name')}")
|
||||||
|
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||||
|
elif status == "created_and_enriched":
|
||||||
|
print(f"Unternehmen erstellt und Enrichment angestoßen. ID: {data.get('id')}, Name: {data.get('name')}")
|
||||||
|
print("Hinweis: Enrichment-Prozesse laufen im Hintergrund und können einige Zeit dauern, bis alle Daten verfügbar sind.")
|
||||||
|
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||||
|
elif status == "created_discovery_timeout":
|
||||||
|
print(f"Unternehmen erstellt, aber Discovery konnte keine Website finden (ID: {data.get('id')}, Name: {data.get('name')}).")
|
||||||
|
print("Der Analyse-Prozess wurde daher nicht gestartet.")
|
||||||
|
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||||
|
else:
|
||||||
|
print("Ein unerwarteter Status ist aufgetreten.")
|
||||||
|
print(json.dumps(company_data_result, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print(f"Trading Twins Analyse für {target_company_name} abgeschlossen.")
|
||||||
|
print(f"{'='*50}\n")
|
||||||
|
|
||||||
|
def run_email_ingest():
|
||||||
|
"""Starts the automated email ingestion process for Tradingtwins leads."""
|
||||||
|
if process_leads:
|
||||||
|
print("\nStarting automated email ingestion via Microsoft Graph...")
|
||||||
|
process_leads()
|
||||||
|
print("Email ingestion completed.")
|
||||||
|
else:
|
||||||
|
print("Error: Email ingestion module not available.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Simulieren der Umgebungsvariablen für diesen Testlauf, falls nicht gesetzt
|
||||||
|
if "COMPANY_EXPLORER_API_USER" not in os.environ:
|
||||||
|
os.environ["COMPANY_EXPLORER_API_USER"] = "admin"
|
||||||
|
if "COMPANY_EXPLORER_API_PASSWORD" not in os.environ:
|
||||||
|
os.environ["COMPANY_EXPLORER_API_PASSWORD"] = "gemini"
|
||||||
|
|
||||||
|
print("Trading Twins Tool - Main Menu")
|
||||||
|
print("1. Process specific company name")
|
||||||
|
print("2. Ingest leads from Email (info@robo-planet.de)")
|
||||||
|
print("3. Run demo sequence (Robo-Planet, Erding, etc.)")
|
||||||
|
|
||||||
|
choice = input("\nSelect option (1-3): ").strip()
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
name = input("Enter company name: ").strip()
|
||||||
|
if name:
|
||||||
|
run_trading_twins_process(name)
|
||||||
|
elif choice == "2":
|
||||||
|
run_email_ingest()
|
||||||
|
elif choice == "3":
|
||||||
|
# Testfall 1: Ein Unternehmen, das wahrscheinlich bereits existiert
|
||||||
|
run_trading_twins_process("Robo-Planet GmbH")
|
||||||
|
time.sleep(2)
|
||||||
|
# Testfall 1b: Ein bekanntes, real existierendes Unternehmen
|
||||||
|
run_trading_twins_process("Klinikum Landkreis Erding")
|
||||||
|
time.sleep(2)
|
||||||
|
# Testfall 2: Ein neues, eindeutiges Unternehmen
|
||||||
|
new_unique_company_name = f"Trading Twins New Target {int(time.time())}"
|
||||||
|
run_trading_twins_process(new_unique_company_name)
|
||||||
|
else:
|
||||||
|
print("Invalid choice.")
|
||||||
25
ARCHIVE_legacy_scripts/trigger_resync.py
Normal file
25
ARCHIVE_legacy_scripts/trigger_resync.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import sqlite3
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
DB_PATH = "connector_queue.db"
|
||||||
|
|
||||||
|
def trigger_resync(contact_id):
|
||||||
|
print(f"🚀 Triggering manual resync for Contact {contact_id}...")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"Event": "contact.changed",
|
||||||
|
"PrimaryKey": contact_id,
|
||||||
|
"ContactId": contact_id,
|
||||||
|
"Changes": ["UserDefinedFields", "Name"] # Dummy changes to pass filters
|
||||||
|
}
|
||||||
|
|
||||||
|
with sqlite3.connect(DB_PATH) as conn:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO jobs (event_type, payload, status) VALUES (?, ?, ?)",
|
||||||
|
("contact.changed", json.dumps(payload), 'PENDING')
|
||||||
|
)
|
||||||
|
print("✅ Job added to queue.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
trigger_resync(6) # Bennis Playland has CRM ID 6
|
||||||
13
ARCHIVE_legacy_scripts/verify_db.py
Normal file
13
ARCHIVE_legacy_scripts/verify_db.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
DB_PATH = "/app/companies_v3_fixed_2.db"
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT name, description, convincing_arguments FROM personas")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
print(f"Persona: {row[0]}")
|
||||||
|
print(f" Description: {row[1][:100]}...")
|
||||||
|
print(f" Convincing: {row[2][:100]}...")
|
||||||
|
print("-" * 20)
|
||||||
|
conn.close()
|
||||||
75
ARCHIVE_vor_migration/Fotograf.de/README.md
Normal file
75
ARCHIVE_vor_migration/Fotograf.de/README.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Archivierte Fotograf.de Tools
|
||||||
|
|
||||||
|
Dieses Verzeichnis (`ARCHIVE_vor_migration/Fotograf.de/`) enthält zwei archivierte Tools, die zuvor für die Interaktion mit `app.fotograf.de` und die Erstellung von Google Docs Teilnehmerlisten verwendet wurden.
|
||||||
|
|
||||||
|
Beide Tools sind hier isoliert und dokumentiert, um eine spätere Wiederverwendung und Überarbeitung zu erleichtern.
|
||||||
|
|
||||||
|
## 1. Fotograf.de Scraper
|
||||||
|
|
||||||
|
**Verzeichnis:** `./scraper/`
|
||||||
|
|
||||||
|
**Zweck:**
|
||||||
|
Ein Python-basiertes Skript, das die Website `app.fotograf.de` automatisiert besucht, sich anmeldet und in zwei Modi Daten extrahiert:
|
||||||
|
1. **E-Mail-Liste erstellen:** Sammelt Kontaktdaten (Käufer, E-Mail, Kindnamen, Login-URLs) und speichert sie in einer CSV-Datei (`supermailer_fertige_liste.csv`).
|
||||||
|
2. **Statistik auswerten:** Erstellt eine Statistik-CSV-Datei (`job_statistik.csv`) über Album-Käufe.
|
||||||
|
|
||||||
|
**Benötigte Dateien:**
|
||||||
|
* `./scraper/scrape_fotograf.py`: Das Hauptskript mit der gesamten Logik.
|
||||||
|
* `./scraper/fotograf_credentials.json`: **(Manuell zu erstellen!)** Diese Datei muss Ihre Login-Daten für `app.fotograf.de` im folgenden JSON-Format enthalten:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"PROFILNAME": {
|
||||||
|
"username": "IHR_BENUTZERNAME",
|
||||||
|
"password": "IHR_PASSWORT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ausführung über Docker:**
|
||||||
|
Das Tool wird in einer Docker-Umgebung ausgeführt, die Google Chrome und Selenium bereitstellt.
|
||||||
|
|
||||||
|
1. **Image bauen (einmalig oder bei Änderungen an Dockerfile/requirements.txt):**
|
||||||
|
Navigieren Sie zum Root-Verzeichnis des Hauptprojekts (`/app`) und verwenden Sie das dortige `Dockerfile.brancheneinstufung`:
|
||||||
|
```bash
|
||||||
|
cd /app
|
||||||
|
docker build -f Dockerfile.brancheneinstufung -t fotograf-scraper .
|
||||||
|
```
|
||||||
|
*(Hinweis: Das `Dockerfile.brancheneinstufung` verwendet die globale `requirements.txt` im Root-Verzeichnis, welche `selenium` enthält.)*
|
||||||
|
|
||||||
|
2. **Container starten und Skript ausführen:**
|
||||||
|
Vom Root-Verzeichnis des Hauptprojekts (`/app`) aus:
|
||||||
|
```bash
|
||||||
|
cd /app
|
||||||
|
docker run -it --rm -v "$(pwd):/app" fotograf-scraper python3 /app/ARCHIVE_vor_migration/Fotograf.de/scraper/scrape_fotograf.py
|
||||||
|
```
|
||||||
|
Das Skript fragt Sie interaktiv nach dem gewünschten Modus und der URL des Fotoauftrags.
|
||||||
|
|
||||||
|
|
||||||
|
## 2. Google Docs Teilnehmerlisten-Generator
|
||||||
|
|
||||||
|
**Verzeichnis:** `./list_generator/`
|
||||||
|
|
||||||
|
**Zweck:**
|
||||||
|
Ein Python-Skript, das CSV-Dateien einliest und daraus formatierte Teilnehmerlisten als neues Google Docs-Dokument im Google Drive erstellt. Das Tool ist interaktiv und fragt beim Start Details wie den Namen der Veranstaltung, den Einrichtungstyp und den Ausgabemodus ab.
|
||||||
|
|
||||||
|
**Benötigte Dateien:**
|
||||||
|
* `./list_generator/list_generator.py`: Das Hauptskript mit der gesamten Logik.
|
||||||
|
* `./list_generator/Namensliste.csv`: **(Manuell zu erstellen!)** Eine CSV-Datei mit den Anmeldungen für Kindergärten/Schulen.
|
||||||
|
* `./list_generator/familien.csv`: **(Manuell zu erstellen!)** Eine CSV-Datei mit den Anmeldungen für Familien-Shootings.
|
||||||
|
* `./list_generator/service_account.json`: **(Manuell zu erstellen!)** Die JSON-Datei mit den Anmeldeinformationen für den Google Cloud Service Account. Diese Datei wird benötigt, um auf Google Docs und Google Drive zuzugreifen.
|
||||||
|
|
||||||
|
**Ausführung:**
|
||||||
|
Navigieren Sie in das Verzeichnis des Tools und starten Sie es mit Python:
|
||||||
|
```bash
|
||||||
|
cd /app/ARCHIVE_vor_migration/Fotograf.de/list_generator/
|
||||||
|
python3 list_generator.py
|
||||||
|
```
|
||||||
|
*(Stellen Sie sicher, dass alle benötigten Python-Bibliotheken wie `google-api-python-client` etc. in Ihrer Umgebung installiert sind. Diese sind vermutlich über die globale `requirements.txt` im Root-Verzeichnis des Hauptprojekts verfügbar.)*
|
||||||
|
|
||||||
|
## Wichtiger Hinweis zu Credentials (Sicherheit)
|
||||||
|
|
||||||
|
Die Tools verwenden `fotograf_credentials.json` und `service_account.json` zur Authentifizierung. Diese Dateien enthalten sensitive Zugangsdaten und wurden **bewusst aus der Git-Historie entfernt** und nicht im Repository abgelegt.
|
||||||
|
|
||||||
|
**Für die Wiederinbetriebnahme müssen diese Dateien manuell im jeweiligen Tool-Verzeichnis (`./scraper/` bzw. `./list_generator/`) erstellt und mit den korrekten Zugangsdaten befüllt werden.**
|
||||||
|
|
||||||
|
**Priorität für die Überarbeitung:** Bei einer zukünftigen Überarbeitung dieser Tools ist es **zwingend erforderlich**, die Handhabung der Credentials zu verbessern. Statt fester JSON-Dateien sollten Umgebungsvariablen (`.env`) oder ein sicherer Secret Management Service verwendet werden, um die Sicherheitsstandards zu erhöhen.
|
||||||
22
ARCHIVE_vor_migration/Fotograf.de/list_generator/README.md
Normal file
22
ARCHIVE_vor_migration/Fotograf.de/list_generator/README.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Google Docs Teilnehmerlisten-Generator (Archiviert)
|
||||||
|
|
||||||
|
Dieses Verzeichnis enthält die archivierten Dateien für den "Google Docs Teilnehmerlisten-Generator".
|
||||||
|
|
||||||
|
**Zweck:**
|
||||||
|
Ein Python-Skript, das CSV-Dateien einliest und daraus formatierte Teilnehmerlisten als neues Google Docs-Dokument im Google Drive erstellt. Das Tool ist interaktiv und fragt beim Start Details wie den Namen der Veranstaltung ab.
|
||||||
|
|
||||||
|
**Zugehörige Dateien in diesem Ordner:**
|
||||||
|
* `list_generator.py`: Das Hauptskript mit der gesamten Logik.
|
||||||
|
|
||||||
|
**Manuell zu erstellende Dateien:**
|
||||||
|
Diese Dateien werden vom Skript als Input benötigt und müssen im selben Verzeichnis liegen:
|
||||||
|
* `Namensliste.csv`: Eine CSV-Datei mit den Anmeldungen für Kindergärten/Schulen.
|
||||||
|
* `familien.csv`: Eine CSV-Datei mit den Anmeldungen für Familien-Shootings.
|
||||||
|
* `service_account.json`: Die JSON-Datei mit den Anmeldeinformationen für den Google Cloud Service Account, der die Berechtigung hat, auf Google Docs und Google Drive zuzugreifen.
|
||||||
|
|
||||||
|
**Hinweis zur Ausführung:**
|
||||||
|
Das Skript wird direkt mit Python ausgeführt, z.B.:
|
||||||
|
```bash
|
||||||
|
python3 list_generator.py
|
||||||
|
```
|
||||||
|
Stellen Sie sicher, dass alle benötigten Bibliotheken (wie `google-api-python-client`, `google-auth-httplib2`, `google-auth-oauthlib`) in Ihrer Python-Umgebung installiert sind. Diese sind vermutlich in der globalen `requirements.txt` im Root-Verzeichnis des Projekts enthalten.
|
||||||
32
ARCHIVE_vor_migration/Fotograf.de/scraper/README.md
Normal file
32
ARCHIVE_vor_migration/Fotograf.de/scraper/README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Fotograf.de Scraper (Archiviert)
|
||||||
|
|
||||||
|
Dieses Verzeichnis enthält die archivierten Dateien für den "Fotograf.de Scraper".
|
||||||
|
|
||||||
|
**Zweck:**
|
||||||
|
Ein Python-basiertes Tool, das die Website `app.fotograf.de` automatisiert besucht, sich anmeldet und in zwei Modi Daten extrahiert:
|
||||||
|
1. **E-Mail-Liste erstellen:** Sammelt Kontaktdaten und speichert sie in einer CSV-Datei (`supermailer_fertige_liste.csv`).
|
||||||
|
2. **Statistik auswerten:** Erstellt eine Statistik-CSV-Datei (`job_statistik.csv`).
|
||||||
|
|
||||||
|
**Zugehörige Dateien in diesem Ordner:**
|
||||||
|
* `scrape_fotograf.py`: Das Hauptskript mit der gesamten Logik.
|
||||||
|
|
||||||
|
**Manuell zu erstellende Dateien:**
|
||||||
|
* `fotograf_credentials.json`: Diese Datei wird vom Skript benötigt und muss die Login-Daten für `app.fotograf.de` im folgenden JSON-Format enthalten:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"PROFILNAME": {
|
||||||
|
"username": "IHR_BENUTZERNAME",
|
||||||
|
"password": "IHR_PASSWORT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Externe Abhängigkeiten (befinden sich im Hauptverzeichnis des Projekts):**
|
||||||
|
* **Dockerfile:** `Dockerfile.brancheneinstufung` wurde wahrscheinlich verwendet, um ein Docker-Image für dieses Tool zu erstellen. Es installiert Google Chrome und die notwendigen Python-Pakete.
|
||||||
|
* **Python-Abhängigkeiten:** Die globale `requirements.txt` im Root-Verzeichnis enthält `selenium` und andere benötigte Bibliotheken.
|
||||||
|
|
||||||
|
**Beispielhafter `docker run`-Befehl:**
|
||||||
|
1. Bauen Sie das Image (nur einmalig): `docker build -f Dockerfile.brancheneinstufung -t fotograf-scraper .`
|
||||||
|
2. Führen Sie den Container aus: `docker run -it --rm -v "$(pwd):/app" fotograf-scraper python3 /app/ARCHIVE_vor_migration/Fotograf.de/scraper/scrape_fotograf.py`
|
||||||
|
|
||||||
|
(Pfade müssen ggf. angepasst werden, je nachdem, von wo der Befehl ausgeführt wird.)
|
||||||
0
ARCHIVE_vor_migration/Generating
Normal file
0
ARCHIVE_vor_migration/Generating
Normal file
BIN
FRITZbox7530.pdf
BIN
FRITZbox7530.pdf
Binary file not shown.
291
GEMINI.md
291
GEMINI.md
@@ -20,6 +20,97 @@ Dies ist in der Vergangenheit mehrfach passiert und hat zu massivem Datenverlust
|
|||||||
- **Git-Repository:** Dieses Projekt wird über ein Git-Repository verwaltet. Alle Änderungen am Code werden versioniert. Beachten Sie den Abschnitt "Git Workflow & Conventions" für unsere Arbeitsregeln.
|
- **Git-Repository:** Dieses Projekt wird über ein Git-Repository verwaltet. Alle Änderungen am Code werden versioniert. Beachten Sie den Abschnitt "Git Workflow & Conventions" für unsere Arbeitsregeln.
|
||||||
- **WICHTIG:** Der AI-Agent kann Änderungen committen, aber aus Sicherheitsgründen oft nicht `git push` ausführen. Bitte führen Sie `git push` manuell aus, wenn der Agent dies meldet.
|
- **WICHTIG:** Der AI-Agent kann Änderungen committen, aber aus Sicherheitsgründen oft nicht `git push` ausführen. Bitte führen Sie `git push` manuell aus, wenn der Agent dies meldet.
|
||||||
|
|
||||||
|
---
|
||||||
|
## ‼️ Aktueller Projekt-Fokus (März 2026): Migration & Stabilisierung
|
||||||
|
|
||||||
|
**Das System wurde am 07. März 2026 erfolgreich stabilisiert und für den Umzug auf die Ubuntu VM (`docker1`) vorbereitet.**
|
||||||
|
|
||||||
|
Alle kritischen Komponenten (Company Explorer, Connector, Lead Engine) sind nun funktionsfähig und resilient konfiguriert.
|
||||||
|
|
||||||
|
Alle weiteren Aufgaben für den Umzug sind hier zentralisiert:
|
||||||
|
➡️ **[`RELOCATION.md`](./RELOCATION.md)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Current Status (March 7, 2026) - STABLE & RESILIENT
|
||||||
|
|
||||||
|
Das System läuft stabil und ist für den Produktivbetrieb vorbereitet. Wesentliche Fortschritte wurden erzielt:
|
||||||
|
|
||||||
|
### 1. SuperOffice Connector (v2.1.1 - "Echo Shield")
|
||||||
|
* **Echo-Prävention:** Implementierung eines robusten "Echo Shield" im Worker. Der Worker identifiziert seine eigenen Aktionen (via `ChangedByAssociateId`) und vermeidet dadurch Endlosschleifen. Änderungen sind nur noch bei externen, relevanten Feldaktualisierungen (Name, Website, JobTitle) relevant.
|
||||||
|
* **Webhook:** Erfolgreich registriert auf `https://floke-ai.duckdns.org/connector/webhook` mit sicherer Token-Validierung.
|
||||||
|
|
||||||
|
### 2. Company Explorer (v0.7.4)
|
||||||
|
* **Datenbank:** Schema-Integrität wiederhergestellt. Fehlende Spalten (`street`, `zip_code`, `unsubscribe_token`, `strategy_briefing`) wurden mit Migrations-Skripten nachgerüstet. Keine 500er Fehler mehr.
|
||||||
|
* **Frontend:** Build-Pipeline mit PostCSS/Tailwind-Styling repariert, sodass die UI wieder einwandfrei funktioniert.
|
||||||
|
|
||||||
|
### 3. Lead Engine (Trading Twins - Voll funktionsfähig)
|
||||||
|
* **Integration:** Service erfolgreich in den Docker-Stack integriert und über Nginx unter `/lead/` und `/feedback/` erreichbar.
|
||||||
|
* **Persistent State:** Led-Daten und Job-Status werden nun zuverlässig in einer SQLite-Datenbank (`/app/data/trading_twins.db`) gespeichert.
|
||||||
|
* **Roundtrip-Funktionalität:** Der komplette Prozess (Lead -> CE -> KI -> Teams-Benachrichtigung -> E-Mail mit Kalender-Links -> Outlook-Termin) funktioniert End-to-End.
|
||||||
|
* **Fehlerbehebung (Debugging-Iterationen):
|
||||||
|
* **`sqlalchemy` & Imports:** Installation von `sqlalchemy` sichergestellt, Pfade für Module (`trading_twins`) im Docker-Build korrigiert.
|
||||||
|
* **Nginx Routing:** Konfiguration optimiert, um `/feedback/` und `/lead/` korrekt an den FastAPI-Server weiterzuleiten. Globale `auth_basic` entfernt, um öffentlichen Endpunkten den Zugriff zu ermöglichen.
|
||||||
|
* **FastAPI `root_path`:** Bereinigt, um Konflikte mit Nginx-Pfaden zu vermeiden.
|
||||||
|
* **Server Stabilität:** `uvicorn` startet nun als separater Prozess, und der `monitor.py` importiert die Module sauber.
|
||||||
|
* **API-Schlüssel:** Alle notwendigen Keys (`INFO_*`, `CAL_*`, `SERP_API`, `WEBHOOK_*`, `GEMINI_API_KEY`) werden korrekt aus `.env` an die Container gemappt.
|
||||||
|
|
||||||
|
### 5. DuckDNS & DNS Monitor
|
||||||
|
* **Erfolgreich reaktiviert:** Der DynDNS-Service läuft und aktualisiert die IP, die Netzwerk-Konnektivität ist stabil.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Git Workflow & Conventions
|
||||||
|
|
||||||
|
### Den Arbeitstag abschließen mit `#fertig`
|
||||||
|
|
||||||
|
Um einen Arbeitsschritt oder einen Task abzuschließen, verwenden Sie den Befehl `#fertig`.
|
||||||
|
|
||||||
|
**WICHTIG:** Verwenden Sie **nicht** `/fertig` oder nur `fertig`. Nur der Befehl mit der Raute (`#`) wird korrekt erkannt.
|
||||||
|
|
||||||
|
Wenn Sie `#fertig` eingeben, führt der Agent folgende Schritte aus:
|
||||||
|
1. **Analyse:** Der Agent prüft, ob seit dem letzten Commit Änderungen am Code vorgenommen wurden.
|
||||||
|
2. **Zusammenfassung:** Er generiert eine automatische Arbeitszusammenfassung basierend auf den Code-Änderungen.
|
||||||
|
3. **Status-Update:** Der Agent führt das Skript `python3 dev_session.py --report-status` im Hintergrund aus.
|
||||||
|
- Die in der aktuellen Session investierte Zeit wird berechnet und in Notion gespeichert.
|
||||||
|
- Ein neuer Statusbericht mit der Zusammenfassung wird an den Notion-Task angehängt.
|
||||||
|
- Der Status des Tasks in Notion wird auf "Done" (oder einen anderen passenden Status) gesetzt.
|
||||||
|
4. **Commit & Push:** Wenn Code-Änderungen vorhanden sind, wird ein Commit erstellt und ein `git push` interaktiv angefragt.
|
||||||
|
|
||||||
|
### ⚠️ Troubleshooting: Git `push`/`pull` Fehler in Docker-Containern
|
||||||
|
|
||||||
|
Gelegentlich kann es vorkommen, dass `git push` oder `git pull` Befehle aus dem `gemini-session` Docker-Container heraus mit Fehlern wie `Could not resolve host` oder `Failed to connect to <Gitea-Domain>` fehlschlagen, selbst wenn die externe Gitea-URL (z.B. `floke-gitea.duckdns.org`) im Host-System erreichbar ist. Dies liegt daran, dass der Docker-Container möglicherweise nicht dieselben DNS-Auflösungsmechanismen oder eine direkte Verbindung zur externen Adresse hat.
|
||||||
|
|
||||||
|
**Problem:** Standard-DNS-Auflösung und externe Hostnamen schlagen innerhalb des Docker-Containers fehl.
|
||||||
|
|
||||||
|
**Lösung:** Um eine robuste und direkte Verbindung zum Gitea-Container auf dem *selben Docker-Host* herzustellen, sollte die Git Remote URL auf die **lokale IP-Adresse des Docker-Hosts** und die **token-basierte Authentifizierung** umgestellt werden.
|
||||||
|
|
||||||
|
**Schritte zur Konfiguration:**
|
||||||
|
|
||||||
|
1. **Lokale IP des Docker-Hosts ermitteln:**
|
||||||
|
* Finden Sie die lokale IP-Adresse des Servers (z.B. Ihrer Diskstation), auf dem die Docker-Container laufen. Beispiel: `192.168.178.6`.
|
||||||
|
2. **Gitea-Token aus `.env` ermitteln:**
|
||||||
|
* Finden Sie das Gitea-Token (das im Format `<Token>` in der `.env`-Datei oder in der vorherigen `git remote -v` Ausgabe zu finden ist). Beispiel: `318c736205934dd066b6bbcb1d732931eaa7c8c4`.
|
||||||
|
3. **Git Remote URL aktualisieren:**
|
||||||
|
* Verwenden Sie den folgenden Befehl, um die Remote-URL zu aktualisieren. Ersetzen Sie `<Username>`, `<Token>` und `<Local-IP-Adresse>` durch Ihre Werte.
|
||||||
|
```bash
|
||||||
|
git remote set-url origin http://<Username>:<Token>@<Local-IP-Adresse>:3000/Floke/Brancheneinstufung2.git
|
||||||
|
```
|
||||||
|
* **Beispiel (mit Ihren Daten):**
|
||||||
|
```bash
|
||||||
|
git remote set-url origin http://Floke:318c736205934dd066b6bbcb1d732931eaa7c8c4@192.168.178.6:3000/Floke/Brancheneinstufung2.git
|
||||||
|
```
|
||||||
|
*(Hinweis: Für die interne Docker-Kommunikation ist `http` anstelle von `https` oft ausreichend und kann Probleme mit SSL-Zertifikaten vermeiden.)*
|
||||||
|
4. **Verifizierung:**
|
||||||
|
* Führen Sie `git fetch` aus, um die neue Konfiguration zu testen. Es sollte nun ohne Passwortabfrage funktionieren:
|
||||||
|
```bash
|
||||||
|
git fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
Diese Konfiguration gewährleistet eine stabile Git-Verbindung innerhalb Ihrer Docker-Umgebung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
This project is a Python-based system for automated company data enrichment and lead generation. It focuses on identifying B2B companies with high potential for robotics automation (Cleaning, Transport, Security, Service).
|
This project is a Python-based system for automated company data enrichment and lead generation. It focuses on identifying B2B companies with high potential for robotics automation (Cleaning, Transport, Security, Service).
|
||||||
@@ -62,7 +153,7 @@ The system architecture has evolved from a CLI-based toolset to a modern web app
|
|||||||
|
|
||||||
2. **The Wolfra/Greilmeier/Erding Fixes (Advanced Metric Parsing):**
|
2. **The Wolfra/Greilmeier/Erding Fixes (Advanced Metric Parsing):**
|
||||||
* **Problem:** Simple regex parsers fail on complex sentences with multiple numbers, concatenated years, or misleading prefixes.
|
* **Problem:** Simple regex parsers fail on complex sentences with multiple numbers, concatenated years, or misleading prefixes.
|
||||||
* **Solution (Hybrid Extraction & Regression Testing):**
|
* **Solution (Hybrid Extraction & Regression Testing):**
|
||||||
1. **LLM Guidance:** The LLM provides an `expected_value` (e.g., "8.000 m²").
|
1. **LLM Guidance:** The LLM provides an `expected_value` (e.g., "8.000 m²").
|
||||||
2. **Robust Python Parser (`MetricParser`):** This parser aggressively cleans the `expected_value` (stripping units like "m²") to get a numerical target. It then intelligently searches the full text for this target, ignoring other numbers (like "2" in "An 2 Standorten").
|
2. **Robust Python Parser (`MetricParser`):** This parser aggressively cleans the `expected_value` (stripping units like "m²") to get a numerical target. It then intelligently searches the full text for this target, ignoring other numbers (like "2" in "An 2 Standorten").
|
||||||
3. **Specific Bug Fixes:**
|
3. **Specific Bug Fixes:**
|
||||||
@@ -87,6 +178,17 @@ The system architecture has evolved from a CLI-based toolset to a modern web app
|
|||||||
* **Problem:** Users didn't see when a background job finished.
|
* **Problem:** Users didn't see when a background job finished.
|
||||||
* **Solution:** Implementing a polling mechanism (`setInterval`) tied to a `isProcessing` state is superior to static timeouts for long-running AI tasks.
|
* **Solution:** Implementing a polling mechanism (`setInterval`) tied to a `isProcessing` state is superior to static timeouts for long-running AI tasks.
|
||||||
|
|
||||||
|
7. **Hyper-Personalized Marketing Engine (v3.2) - "Deep Persona Injection":**
|
||||||
|
* **Problem:** Marketing texts were too generic and didn't reflect the specific psychological or operative profile of the different target roles (e.g., CFO vs. Facility Manager).
|
||||||
|
* **Solution (Deep Sync & Prompt Hardening):**
|
||||||
|
1. **Extended Schema:** Added `description`, `convincing_arguments`, and `kpis` to the `Persona` database model to store richer profile data.
|
||||||
|
2. **Notion Master Sync:** Updated the synchronization logic to pull these deep insights directly from the Notion "Personas / Roles" database.
|
||||||
|
3. **Role-Centric Prompts:** The `MarketingMatrix` generator was re-engineered to inject the persona's "Mindset" and "KPIs" into the prompt.
|
||||||
|
* **Example (Healthcare):**
|
||||||
|
- **Infrastructure Lead:** Focuses now on "IT Security", "DSGVO Compliance", and "WLAN integration".
|
||||||
|
- **Economic Buyer (CFO):** Focuses on "ROI Amortization", "Reduction of Overtime", and "Flexible Financing (RaaS)".
|
||||||
|
* **Verification:** Verified that the transition from a company-specific **Opener** (e.g., observing staff shortages at Klinikum Erding) to the **Role-specific Intro** (e.g., pitching transport robots to reduce walking distances for nursing directors) is seamless and logical.
|
||||||
|
|
||||||
## Metric Parser - Regression Tests
|
## Metric Parser - Regression Tests
|
||||||
To ensure the stability and accuracy of the metric extraction logic, a dedicated test suite (`/company-explorer/backend/tests/test_metric_parser.py`) has been created. It covers the following critical, real-world bug fixes:
|
To ensure the stability and accuracy of the metric extraction logic, a dedicated test suite (`/company-explorer/backend/tests/test_metric_parser.py`) has been created. It covers the following critical, real-world bug fixes:
|
||||||
|
|
||||||
@@ -104,8 +206,185 @@ To ensure the stability and accuracy of the metric extraction logic, a dedicated
|
|||||||
|
|
||||||
These tests are crucial for preventing regressions as the parser logic evolves.
|
These tests are crucial for preventing regressions as the parser logic evolves.
|
||||||
|
|
||||||
## Next Steps
|
## Notion Maintenance & Data Sync
|
||||||
* **Marketing Automation:** Implement the actual sending logic (or export) based on the contact status.
|
|
||||||
* **Job Role Mapping Engine:** Connect the configured patterns to the contact import/creation process to auto-assign roles.
|
Since the "Golden Record" for Industry Verticals (Pains, Gains, Products) resides in Notion, specific tools are available to read and sync this data.
|
||||||
* **Industry Classification Engine:** Connect the configured industries to the AI Analysis prompt to enforce the "Strict Mode" mapping.
|
|
||||||
* **Export:** Generate Excel/CSV enriched reports (already partially implemented via JSON export).
|
**Location:** `/app/company-explorer/backend/scripts/notion_maintenance/`
|
||||||
|
|
||||||
|
**Prerequisites:**
|
||||||
|
- Ensure `.env` is loaded with `NOTION_API_KEY` and correct DB IDs.
|
||||||
|
|
||||||
|
**Key Scripts:**
|
||||||
|
|
||||||
|
1. **`check_relations.py` (Reader - Deep):**
|
||||||
|
* **Purpose:** Reads Verticals and resolves linked Product Categories (Relation IDs -> Names). Essential for verifying the "Primary/Secondary Product" logic.
|
||||||
|
* **Usage:** `python3 check_relations.py`
|
||||||
|
|
||||||
|
2. **`update_notion_full.py` (Writer - Batch):**
|
||||||
|
* **Purpose:** Batch updates Pains and Gains for multiple verticals. Use this as a template when refining the messaging strategy.
|
||||||
|
* **Usage:** Edit the dictionary in the script, then run `python3 update_notion_full.py`.
|
||||||
|
|
||||||
|
3. **`list_notion_structure.py` (Schema Discovery):**
|
||||||
|
* **Purpose:** Lists all property keys and page titles. Use this to debug schema changes (e.g. if a column was renamed).
|
||||||
|
- **Usage:** `python3 list_notion_structure.py`
|
||||||
|
|
||||||
|
## Next Steps (Updated Feb 27, 2026)
|
||||||
|
|
||||||
|
***HINWEIS:*** *Dieser Abschnitt ist veraltet. Die aktuellen nächsten Schritte beziehen sich auf die Migrations-Vorbereitung und sind in der Datei [`RELOCATION.md`](./RELOCATION.md) dokumentiert.*
|
||||||
|
|
||||||
|
* **Notion Content:** Finalize "Pains" and "Gains" for all 25 verticals in the Notion master database.
|
||||||
|
* **Intelligence:** Run `generate_matrix.py` in the Company Explorer backend to populate the matrix for all new English vertical names.
|
||||||
|
* **Automation:** Register the production webhook (requires `admin-webhooks` rights) to enable real-time CRM sync without manual job injection.
|
||||||
|
* **Execution:** Connect the "Sending Engine" (the actual email dispatch logic) to the SuperOffice fields.
|
||||||
|
* **Monitoring:** Monitor the 'Atomic PATCH' logs in production for any 400 errors regarding field length or specific character sets.
|
||||||
|
|
||||||
|
|
||||||
|
## Company Explorer Access & Debugging
|
||||||
|
|
||||||
|
The **Company Explorer** is the central intelligence engine.
|
||||||
|
|
||||||
|
**Core Paths:**
|
||||||
|
* **Database:** `/app/companies_v3_fixed_2.db` (SQLite)
|
||||||
|
* **Backend Code:** `/app/company-explorer/backend/`
|
||||||
|
* **Logs:** `/app/logs_debug/company_explorer_debug.log`
|
||||||
|
|
||||||
|
**Accessing Data:**
|
||||||
|
To inspect live data without starting the full stack, use `sqlite3` directly or the helper scripts (if environment permits).
|
||||||
|
|
||||||
|
* **Direct SQL:** `sqlite3 /app/companies_v3_fixed_2.db "SELECT * FROM companies WHERE name LIKE '%Firma%';" `
|
||||||
|
* **Python (requires env):** The app runs in a Docker container. When debugging from outside (CLI agent), Python dependencies like `sqlalchemy` might be missing in the global scope. Prefer `sqlite3` for quick checks.
|
||||||
|
|
||||||
|
**Key Endpoints (Internal API :8000):**
|
||||||
|
* `POST /api/provision/superoffice-contact`: Triggers the text generation logic.
|
||||||
|
* `GET /api/companies/{id}`: Full company profile including enrichment data.
|
||||||
|
|
||||||
|
**Troubleshooting:**
|
||||||
|
* **"BaseModel" Error:** Usually a mix-up between Pydantic and SQLAlchemy `Base`. Check imports in `database.py`.
|
||||||
|
* **Missing Dependencies:** The CLI agent runs in `/app` but not necessarily inside the container's venv. Use standard tools (`grep`, `sqlite3`) where possible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Debugging Session (Feb 21, 2026) - Re-Stabilizing the Analysis Engine
|
||||||
|
|
||||||
|
A critical session was required to fix a series of cascading failures in the `ClassificationService`. The key takeaways are documented here to prevent future issues.
|
||||||
|
|
||||||
|
1. **The "Phantom" `NameError`:**
|
||||||
|
* **Symptom:** The application crashed with a `NameError: name 'joinedload' is not defined`, even though the import was correctly added to `classification.py`.
|
||||||
|
* **Root Cause:** The `uvicorn` server's hot-reload mechanism within the Docker container did not reliably pick up file changes made from outside the container. A simple `docker-compose restart` was insufficient to clear the process's cached state.
|
||||||
|
* **Solution:** After any significant code change, especially to imports or core logic, a forced-recreation of the container is **mandatory**.
|
||||||
|
```bash
|
||||||
|
# Correct Way to Apply Changes:
|
||||||
|
docker-compose up -d --build --force-recreate company-explorer
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **The "Invisible" Logs:**
|
||||||
|
* **Symptom:** No debug logs were being written, making it impossible to trace the execution flow.
|
||||||
|
* **Root Cause:** The `LOG_DIR` path in `/company-explorer/backend/config.py` was misconfigured (`/app/logs_debug`) and did not point to the actual, historical log directory (`/app/Log_from_docker`).
|
||||||
|
* **Solution:** Configuration paths must be treated as absolute and verified. Correcting the `LOG_DIR` path immediately resolved the issue.
|
||||||
|
|
||||||
|
3. **Inefficient Debugging Loop:**
|
||||||
|
* **Symptom:** The cycle of triggering a background job via API, waiting, and then manually checking logs was slow and inefficient.
|
||||||
|
* **Root Cause:** Lack of a tool to test the core application logic in isolation.
|
||||||
|
* **Solution:** The creation of a dedicated, interactive test script (`/company-explorer/backend/scripts/debug_single_company.py`). This script allows running the entire analysis for a single company in the foreground, providing immediate and detailed feedback. This pattern is invaluable for complex, multi-step processes and should be a standard for future development.
|
||||||
|
## Production Migration & Multi-Campaign Support (Feb 27, 2026)
|
||||||
|
|
||||||
|
The system has been fully migrated to the SuperOffice production environment (`online3.superoffice.com`, tenant `Cust26720`).
|
||||||
|
|
||||||
|
### 1. Final UDF Mappings (Production)
|
||||||
|
These ProgIDs are verified and active for the production tenant:
|
||||||
|
|
||||||
|
| Field Purpose | Entity | ProgID | Notes |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **MA Subject** | Person | `SuperOffice:19` | |
|
||||||
|
| **MA Intro** | Person | `SuperOffice:20` | |
|
||||||
|
| **MA Social Proof** | Person | `SuperOffice:21` | |
|
||||||
|
| **MA Unsubscribe** | Person | `SuperOffice:22` | URL format |
|
||||||
|
| **MA Campaign** | Person | `SuperOffice:23` | List field (uses `:DisplayText`) |
|
||||||
|
| **Vertical** | Contact | `SuperOffice:83` | List field (mapped via JSON) |
|
||||||
|
| **AI Summary** | Contact | `SuperOffice:84` | Truncated to 132 chars |
|
||||||
|
| **AI Last Update** | Contact | `SuperOffice:85` | Format: `[D:MM/DD/YYYY HH:MM:SS]` |
|
||||||
|
| **Opener Primary** | Contact | `SuperOffice:86` | |
|
||||||
|
| **Opener Secondary**| Contact | `SuperOffice:87` | |
|
||||||
|
| **Last Outreach** | Contact | `SuperOffice:88` | |
|
||||||
|
|
||||||
|
### 2. Vertical ID Mapping (Production)
|
||||||
|
The full list of 25 verticals with their internal SuperOffice IDs (List `udlist331`):
|
||||||
|
`Automotive - Dealer: 1613, Corporate - Campus: 1614, Energy - Grid & Utilities: 1615, Energy - Solar/Wind: 1616, Healthcare - Care Home: 1617, Healthcare - Hospital: 1618, Hospitality - Gastronomy: 1619, Hospitality - Hotel: 1620, Industry - Manufacturing: 1621, Infrastructure - Communities: 1622, Infrastructure - Public: 1623, Infrastructure - Transport: 1624, Infrastructure - Parking: 1625, Leisure - Entertainment: 1626, Leisure - Fitness: 1627, Leisure - Indoor Active: 1628, Leisure - Outdoor Park: 1629, Leisure - Wet & Spa: 1630, Logistics - Warehouse: 1631, Others: 1632, Reinigungsdienstleister: 1633, Retail - Food: 1634, Retail - Non-Food: 1635, Retail - Shopping Center: 1636, Tech - Data Center: 1637`.
|
||||||
|
|
||||||
|
### 3. Technical Lessons Learned (SO REST API)
|
||||||
|
|
||||||
|
1. **Atomic PATCH (Stability):** Bundling all contact updates into a single `PATCH` request to the `/Contact/{id}` endpoint is far more stable than sequential UDF updates. If one field fails (e.g. invalid property), the whole transaction might roll back or partially fail—proactive validation is key.
|
||||||
|
2. **Website Sync (`Urls` Array):** Updating the website via REST requires manipulating the `Urls` array property. Simple field assignment to `UrlAddress` fails during `PATCH`.
|
||||||
|
* *Correct Format:* `"Urls": [{"Value": "https://example.com", "Description": "AI Discovered"}]`.
|
||||||
|
3. **List Resolution (`:DisplayText`):** To get the clean string value of a list field (like Campaign Name) without extra API calls, use the pseudo-field `ProgID:DisplayText` in the `$select` parameter.
|
||||||
|
4. **Field Length Limits:** Standard SuperOffice text UDFs are limited to approx. 140-254 characters. AI-generated summaries must be truncated (e.g. 132 chars) to avoid 400 Bad Request errors.
|
||||||
|
5. **Docker `env_file` Importance:** For production, mapping individual variables in `docker-compose.yml` is error-prone. Using `env_file: .env` ensures all services stay synchronized with the latest UDF IDs and mappings.
|
||||||
|
6. **Production URL Schema:** The production API is strictly hosted on `online3.superoffice.com` (for this tenant), while OAuth remains at `online.superoffice.com`.
|
||||||
|
|
||||||
|
### 4. Campaign Trigger Logic
|
||||||
|
The `worker.py` (v1.8) now extracts the `campaign_tag` from `SuperOffice:23:DisplayText`. This tag is passed to the Company Explorer's provisioning API. If a matching entry exists in the `MarketingMatrix` for that tag, specific texts are used; otherwise, it falls back to the "standard" Kaltakquise texts.
|
||||||
|
|
||||||
|
### 5. SuperOffice Authentication (Critical Update Feb 28, 2026)
|
||||||
|
|
||||||
|
**Problem:** Authentication failures ("Invalid refresh token" or "Invalid client_id") occurred because standard `load_dotenv()` did not override stale environment variables present in the shell process.
|
||||||
|
|
||||||
|
**Solution:** Always use `load_dotenv(override=True)` in Python scripts to force loading the actual values from the `.env` file.
|
||||||
|
|
||||||
|
**Correct Authentication Pattern (Python):**
|
||||||
|
```python
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
|
||||||
|
# CRITICAL: override=True ensures we read from .env even if env vars are already set
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
client_id = os.getenv("SO_CLIENT_ID")
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Known Working Config (Production):**
|
||||||
|
* **Environment:** `online3`
|
||||||
|
* **Tenant:** `Cust26720`
|
||||||
|
* **Token Logic:** The `AuthHandler` implementation in `health_check_so.py` is the reference standard. Avoid using legacy `superoffice_client.py` without verifying it uses `override=True`.
|
||||||
|
|
||||||
|
### 6. Sales & Opportunities (Roboplanet Specifics)
|
||||||
|
|
||||||
|
When creating sales via API, specific constraints apply due to the shared tenant with Wackler:
|
||||||
|
|
||||||
|
* **SaleTypeId:** MUST be **14** (`GE:"Roboplanet Verkauf";`) to ensure the sale is assigned to the correct business unit.
|
||||||
|
* *Alternative:* ID 16 (`GE:"Roboplanet Teststellung";`) for trials.
|
||||||
|
* **Mandatory Fields:**
|
||||||
|
* `Saledate` (Estimated Date): Must be provided in ISO format (e.g., `YYYY-MM-DDTHH:MM:SSZ`).
|
||||||
|
* `Person`: Highly recommended linking to a specific person, not just the company.
|
||||||
|
* **Context:** Avoid creating sales on the parent company "Wackler Service Group" (ID 3). Always target the specific lead company.
|
||||||
|
|
||||||
|
### Analyse der SuperOffice `Sale`-Entität (März 2026)
|
||||||
|
|
||||||
|
- **Ziel:** Erstellung eines Reports, der abbildet, welche Kunden welche Produkte angeboten bekommen oder gekauft haben. Die initiale Vermutung war, dass Produktinformationen oft als Freitext-Einträge und nicht über den offiziellen Produktkatalog erfasst werden.
|
||||||
|
- **Problem:** Die Untersuchung der Datenstruktur zeigte, dass die API-Endpunkte zur Abfrage von `Quote`-Objekten (Angeboten) und `QuoteLines` (Angebotspositionen) über `Sale`-, `Contact`- oder `Project`-Beziehungen hinweg nicht zuverlässig funktionierten. Viele Abfragen resultierten in `500 Internal Server Errors` oder leeren Datenmengen, was eine direkte Verknüpfung von Verkauf zu Produkt unmöglich machte.
|
||||||
|
- **Kern-Erkenntnis (Datenstruktur):**
|
||||||
|
1. **Freitext statt strukturierter Daten:** Die Analyse eines konkreten `Sale`-Objekts (ID `342243`) bestätigte die ursprüngliche Hypothese. Produktinformationen (z.B. `2xOmnie CD-01 mit Nachlass`) werden direkt in das `Heading`-Feld (Betreff) des `Sale`-Objekts als Freitext eingetragen. Es existieren oft keine verknüpften `Quote`- oder `QuoteLine`-Entitäten.
|
||||||
|
2. **Datenqualität bei Verknüpfungen:** Eine signifikante Anzahl von `Sale`-Objekten im System weist keine Verknüpfung zu einem `Contact`-Objekt auf (`Contact: null`). Dies erschwert die automatische Zuordnung von Verkäufen zu Kunden erheblich.
|
||||||
|
- **Nächster Schritt / Lösungsweg:** Ein Skript (`/app/connector-superoffice/generate_customer_product_report.py`) wurde entwickelt, das diese Probleme adressiert. Es fragt gezielt nur `Sale`-Objekte ab, die eine gültige `Contact`-Verknüpfung besitzen (`$filter=Contact ne null`). Anschließend extrahiert es den Kundennamen und das `Heading`-Feld des Verkaufs und durchsucht letzteres nach vordefinierten Produkt-Schlüsselwörtern. Die Ergebnisse werden für die manuelle Analyse in einer CSV-Datei (`product_report.csv`) gespeichert. Dieser Ansatz ist der einzig verlässliche Weg, um die gewünschten Informationen aus dem System zu extrahieren.
|
||||||
|
|
||||||
|
### 7. Service & Tickets (Anfragen)
|
||||||
|
|
||||||
|
SuperOffice Tickets represent the support and request system. Like Sales, they are organized to allow separation between Roboplanet and Wackler.
|
||||||
|
|
||||||
|
* **Entity Name:** `ticket`
|
||||||
|
* **Roboplanet Specific Categories (CategoryId):**
|
||||||
|
* **ID 46:** `GE:"Lead Roboplanet";`
|
||||||
|
* **ID 47:** `GE:"Vertriebspartner Roboplanet";`
|
||||||
|
* **ID 48:** `GE:"Weitergabe Roboplanet";`
|
||||||
|
* **Hierarchical:** `Roboplanet/Support` (often used for technical issues).
|
||||||
|
* **Key Fields:**
|
||||||
|
* `ticketId`: Internal ID.
|
||||||
|
* `title`: The subject of the request.
|
||||||
|
* `contactId` / `personId`: Links to company and contact person.
|
||||||
|
* `ticketStatusId`: 1 (Unbearbeitet), 2 (In Arbeit), 3 (Bearbeitet).
|
||||||
|
* `ownedBy`: Often "ROBO" for Roboplanet staff.
|
||||||
|
* **Cross-Links:** Tickets can be linked to `saleId` (to track support during a sale) or `projectId`.
|
||||||
|
|
||||||
|
---
|
||||||
|
This is the core logic used to generate the company-specific opener.
|
||||||
|
|||||||
@@ -1,385 +0,0 @@
|
|||||||
Kinderhaus St. Martin Neuching Kinderfotos Erding
|
|
||||||
02. - 05.06.2025
|
|
||||||
|
|
||||||
Nachname Vorname Gruppe
|
|
||||||
----------------------------------------------------------------
|
|
||||||
Bauer Emilie Christiana Bienengruppe
|
|
||||||
Eberl Maximilian Bienengruppe
|
|
||||||
Eckinger Charly Bienengruppe
|
|
||||||
Ehrensberger Theo Bienengruppe
|
|
||||||
Gugic Vanessa Bienengruppe
|
|
||||||
Hermansdorfer Marina Bienengruppe
|
|
||||||
Kaiser Theresa Bienengruppe
|
|
||||||
Mehringer Quirin Bienengruppe
|
|
||||||
Müller Helene Bienengruppe
|
|
||||||
Nowak Juna Bienengruppe
|
|
||||||
Root Jonas Bienengruppe
|
|
||||||
Tress Anna Bienengruppe
|
|
||||||
Vass Marcell Bienengruppe
|
|
||||||
Vilgertshofer Clara Bienengruppe
|
|
||||||
Wamprechtshammer Marie Bienengruppe
|
|
||||||
|
|
||||||
15 angemeldete Kinder
|
|
||||||
|
|
||||||
Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden
|
|
||||||
Kinder an die Anmeldung erinnern.
|
|
||||||
|
|
||||||
Stand 26.05.2025 20:04 Uhr
|
|
||||||
|
|
||||||
|
|
||||||
Kinderfotos Erding
|
|
||||||
Gartenstr. 10 85445 Oberding
|
|
||||||
www.kinderfotos-erding.de
|
|
||||||
08122-8470867
|
|
||||||
|
|
||||||
--- SEITENWECHSEL / PAGE BREAK ---
|
|
||||||
|
|
||||||
Kinderhaus St. Martin Neuching Kinderfotos Erding
|
|
||||||
02. - 05.06.2025
|
|
||||||
|
|
||||||
Nachname Vorname Gruppe
|
|
||||||
---------------------------------------------------------------
|
|
||||||
Arndt Alina Eulengruppe
|
|
||||||
Ben Hiba Elias Eulengruppe
|
|
||||||
Benke Bastian Eulengruppe
|
|
||||||
Berg Tamara Eulengruppe
|
|
||||||
Berndt Mariella Eulengruppe
|
|
||||||
Brose Valentina Eulengruppe
|
|
||||||
Drexler Lukas Eulengruppe
|
|
||||||
Eisenhofer Kilian Eulengruppe
|
|
||||||
Etterer Juna Eulengruppe
|
|
||||||
Fink Sophia Eulengruppe
|
|
||||||
Fleissner Sebastian Eulengruppe
|
|
||||||
Gebhardt Anton Eulengruppe
|
|
||||||
Götz Ludwig Eulengruppe
|
|
||||||
Josef Maximilian Eulengruppe
|
|
||||||
Klimaschewski Ben Eulengruppe
|
|
||||||
Kroh Louis Eulengruppe
|
|
||||||
Lavalie Alvin Eulengruppe
|
|
||||||
Michalik Fabian Eulengruppe
|
|
||||||
Multhammer Vincent Eulengruppe
|
|
||||||
Multhammer Vincent Ludwig Eulengruppe
|
|
||||||
Reck Sophia Eulengruppe
|
|
||||||
Richter Oskar Eulengruppe
|
|
||||||
Rocha Elias Lorenzo Eulengruppe
|
|
||||||
Rudolph Anna Eulengruppe
|
|
||||||
Ternavska Kira Eulengruppe
|
|
||||||
Varadi Amelie Eulengruppe
|
|
||||||
Varadi Clara Eulengruppe
|
|
||||||
Zerr Oskar Eulengruppe
|
|
||||||
|
|
||||||
28 angemeldete Kinder
|
|
||||||
|
|
||||||
Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden
|
|
||||||
Kinder an die Anmeldung erinnern.
|
|
||||||
|
|
||||||
Stand 26.05.2025 20:04 Uhr
|
|
||||||
|
|
||||||
|
|
||||||
Kinderfotos Erding
|
|
||||||
Gartenstr. 10 85445 Oberding
|
|
||||||
www.kinderfotos-erding.de
|
|
||||||
08122-8470867
|
|
||||||
|
|
||||||
--- SEITENWECHSEL / PAGE BREAK ---
|
|
||||||
|
|
||||||
Kinderhaus St. Martin Neuching Kinderfotos Erding
|
|
||||||
02. - 05.06.2025
|
|
||||||
|
|
||||||
Nachname Vorname Gruppe
|
|
||||||
-----------------------------------------------------------------
|
|
||||||
Bauer Luka Maximilian Željko Fröschegruppe
|
|
||||||
Birladeanu Ianis Andrei Fröschegruppe
|
|
||||||
Eberhardt Valentin Fröschegruppe
|
|
||||||
Gryczuk Oliver Fröschegruppe
|
|
||||||
Kelly Ferdinand Fröschegruppe
|
|
||||||
Koburger Leonie Fröschegruppe
|
|
||||||
Kressirer Josephine Fröschegruppe
|
|
||||||
Kunz Sophie Fröschegruppe
|
|
||||||
Käsmeier Xaver Fröschegruppe
|
|
||||||
Magin Lilly Fröschegruppe
|
|
||||||
Mert Lukas Fröschegruppe
|
|
||||||
Schmitz Juna Fröschegruppe
|
|
||||||
Schütz Mirjam Fröschegruppe
|
|
||||||
Wamprechtshammer Moritz Fröschegruppe
|
|
||||||
Wimmer Annika Fröschegruppe
|
|
||||||
Zerr Paul Fröschegruppe
|
|
||||||
|
|
||||||
16 angemeldete Kinder
|
|
||||||
|
|
||||||
Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden
|
|
||||||
Kinder an die Anmeldung erinnern.
|
|
||||||
|
|
||||||
Stand 26.05.2025 20:04 Uhr
|
|
||||||
|
|
||||||
|
|
||||||
Kinderfotos Erding
|
|
||||||
Gartenstr. 10 85445 Oberding
|
|
||||||
www.kinderfotos-erding.de
|
|
||||||
08122-8470867
|
|
||||||
|
|
||||||
--- SEITENWECHSEL / PAGE BREAK ---
|
|
||||||
|
|
||||||
Kinderhaus St. Martin Neuching Kinderfotos Erding
|
|
||||||
02. - 05.06.2025
|
|
||||||
|
|
||||||
Nachname Vorname Gruppe
|
|
||||||
---------------------------------------------------------------
|
|
||||||
Albano Felicitas Hasengruppe
|
|
||||||
Ben Hiba Yassin Hasengruppe
|
|
||||||
Daiser Greta Hasengruppe
|
|
||||||
Fischer Alexander Hasengruppe
|
|
||||||
Gerlsbeck Lena Hasengruppe
|
|
||||||
Gottwalt Greta Hasengruppe
|
|
||||||
Gruber Sophia Hasengruppe
|
|
||||||
Hadzic Anelia Hasengruppe
|
|
||||||
Kundt Patrick Hasengruppe
|
|
||||||
Lechner Maximilian Hasengruppe
|
|
||||||
Numberger Benedikt Hasengruppe
|
|
||||||
Scherber Lukas Hasengruppe
|
|
||||||
Tress Leo Hasengruppe
|
|
||||||
Türe Emine Hasengruppe
|
|
||||||
Wagner Isabell Hasengruppe
|
|
||||||
|
|
||||||
15 angemeldete Kinder
|
|
||||||
|
|
||||||
Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden
|
|
||||||
Kinder an die Anmeldung erinnern.
|
|
||||||
|
|
||||||
Stand 26.05.2025 20:04 Uhr
|
|
||||||
|
|
||||||
|
|
||||||
Kinderfotos Erding
|
|
||||||
Gartenstr. 10 85445 Oberding
|
|
||||||
www.kinderfotos-erding.de
|
|
||||||
08122-8470867
|
|
||||||
|
|
||||||
--- SEITENWECHSEL / PAGE BREAK ---
|
|
||||||
|
|
||||||
Kinderhaus St. Martin Neuching Kinderfotos Erding
|
|
||||||
02. - 05.06.2025
|
|
||||||
|
|
||||||
Nachname Vorname Gruppe
|
|
||||||
-----------------------------------------------------------------
|
|
||||||
Brandl Mila Koboldegruppe
|
|
||||||
Engelking Melina Koboldegruppe
|
|
||||||
Fink Anna Koboldegruppe
|
|
||||||
Flügel Antonia Koboldegruppe
|
|
||||||
Heindl Louis Koboldegruppe
|
|
||||||
Kundt Philip Koboldegruppe
|
|
||||||
Lechner Ben Koboldegruppe
|
|
||||||
Müller Johanna Koboldegruppe
|
|
||||||
Schütz Rafaela Koboldegruppe
|
|
||||||
Seibold Marie Koboldegruppe
|
|
||||||
Seibold Sophia Koboldegruppe
|
|
||||||
Wenninger Rosa Koboldegruppe
|
|
||||||
|
|
||||||
12 angemeldete Kinder
|
|
||||||
|
|
||||||
Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden
|
|
||||||
Kinder an die Anmeldung erinnern.
|
|
||||||
|
|
||||||
Stand 26.05.2025 20:04 Uhr
|
|
||||||
|
|
||||||
|
|
||||||
Kinderfotos Erding
|
|
||||||
Gartenstr. 10 85445 Oberding
|
|
||||||
www.kinderfotos-erding.de
|
|
||||||
08122-8470867
|
|
||||||
|
|
||||||
--- SEITENWECHSEL / PAGE BREAK ---
|
|
||||||
|
|
||||||
Kinderhaus St. Martin Neuching Kinderfotos Erding
|
|
||||||
02. - 05.06.2025
|
|
||||||
|
|
||||||
Nachname Vorname Gruppe
|
|
||||||
---------------------------------------------------------------------
|
|
||||||
Brose Marie Marienkäfergruppe
|
|
||||||
Burghardt Fanny Marienkäfergruppe
|
|
||||||
Daberger Tobias Marienkäfergruppe
|
|
||||||
Grobler Emelia Marienkäfergruppe
|
|
||||||
Hamdard Faria Marienkäfergruppe
|
|
||||||
Hermansdorfer Vincent Marienkäfergruppe
|
|
||||||
Huber Helena Marienkäfergruppe
|
|
||||||
Klaric Mia Marienkäfergruppe
|
|
||||||
Kraus Viktoria Marienkäfergruppe
|
|
||||||
Kroh Maximilian Marienkäfergruppe
|
|
||||||
Schuster Korbinian Marienkäfergruppe
|
|
||||||
Wenninger Valentin Marienkäfergruppe
|
|
||||||
Zehetmaier Emma Marienkäfergruppe
|
|
||||||
|
|
||||||
13 angemeldete Kinder
|
|
||||||
|
|
||||||
Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden
|
|
||||||
Kinder an die Anmeldung erinnern.
|
|
||||||
|
|
||||||
Stand 26.05.2025 20:04 Uhr
|
|
||||||
|
|
||||||
|
|
||||||
Kinderfotos Erding
|
|
||||||
Gartenstr. 10 85445 Oberding
|
|
||||||
www.kinderfotos-erding.de
|
|
||||||
08122-8470867
|
|
||||||
|
|
||||||
--- SEITENWECHSEL / PAGE BREAK ---
|
|
||||||
|
|
||||||
Kinderhaus St. Martin Neuching Kinderfotos Erding
|
|
||||||
02. - 05.06.2025
|
|
||||||
|
|
||||||
Nachname Vorname Gruppe
|
|
||||||
-------------------------------------------------------------------
|
|
||||||
Arndt Felix Maulwürfegruppe
|
|
||||||
Beck Josef Maulwürfegruppe
|
|
||||||
Ehrensberger Zeno Maulwürfegruppe
|
|
||||||
Eisenhofer Antonia Maulwürfegruppe
|
|
||||||
Farin Constantin Maulwürfegruppe
|
|
||||||
Fink Xaver Maulwürfegruppe
|
|
||||||
Fischer Joshua Maulwürfegruppe
|
|
||||||
Flügel Johann Maulwürfegruppe
|
|
||||||
Hadzic Aylin Maulwürfegruppe
|
|
||||||
Kressirer Simon Maulwürfegruppe
|
|
||||||
Kroh Liana Maulwürfegruppe
|
|
||||||
Kugler Milena Vaiana Maulwürfegruppe
|
|
||||||
Magin Lotte Maulwürfegruppe
|
|
||||||
Michalik Sarah Maulwürfegruppe
|
|
||||||
Rocha Elias Matteo Maulwürfegruppe
|
|
||||||
Schleier Valentin Maulwürfegruppe
|
|
||||||
Sedlmeir Julia Maulwürfegruppe
|
|
||||||
Suszczewicz Adam Maulwürfegruppe
|
|
||||||
Tratnik Maja Maulwürfegruppe
|
|
||||||
Wagner Mariella Maulwürfegruppe
|
|
||||||
Winkler Clara Maulwürfegruppe
|
|
||||||
|
|
||||||
21 angemeldete Kinder
|
|
||||||
|
|
||||||
Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden
|
|
||||||
Kinder an die Anmeldung erinnern.
|
|
||||||
|
|
||||||
Stand 26.05.2025 20:04 Uhr
|
|
||||||
|
|
||||||
|
|
||||||
Kinderfotos Erding
|
|
||||||
Gartenstr. 10 85445 Oberding
|
|
||||||
www.kinderfotos-erding.de
|
|
||||||
08122-8470867
|
|
||||||
|
|
||||||
--- SEITENWECHSEL / PAGE BREAK ---
|
|
||||||
|
|
||||||
Kinderhaus St. Martin Neuching Kinderfotos Erding
|
|
||||||
02. - 05.06.2025
|
|
||||||
|
|
||||||
Nachname Vorname Gruppe
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
Birladeanu Dima Matei Schmetterlingegruppe
|
|
||||||
Eichner Hanna Schmetterlingegruppe
|
|
||||||
Gruber Johanna Schmetterlingegruppe
|
|
||||||
Gschwendtner Theresa Schmetterlingegruppe
|
|
||||||
Haberthaler Moritz Schmetterlingegruppe
|
|
||||||
Hamdard Avesta Schmetterlingegruppe
|
|
||||||
Josef Konstantin Schmetterlingegruppe
|
|
||||||
Mikołajczak Gabriela Schmetterlingegruppe
|
|
||||||
Pfaus Leo Schmetterlingegruppe
|
|
||||||
Sulejmanovic Elna Schmetterlingegruppe
|
|
||||||
|
|
||||||
10 angemeldete Kinder
|
|
||||||
|
|
||||||
Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden
|
|
||||||
Kinder an die Anmeldung erinnern.
|
|
||||||
|
|
||||||
Stand 26.05.2025 20:04 Uhr
|
|
||||||
|
|
||||||
|
|
||||||
Kinderfotos Erding
|
|
||||||
Gartenstr. 10 85445 Oberding
|
|
||||||
www.kinderfotos-erding.de
|
|
||||||
08122-8470867
|
|
||||||
|
|
||||||
--- SEITENWECHSEL / PAGE BREAK ---
|
|
||||||
|
|
||||||
Kinderhaus St. Martin Neuching Kinderfotos Erding
|
|
||||||
02. - 05.06.2025
|
|
||||||
|
|
||||||
Nachname Vorname Gruppe
|
|
||||||
-----------------------------------------------------------------
|
|
||||||
Daiser Nora Wichtelgruppe
|
|
||||||
Fabri Jule Wichtelgruppe
|
|
||||||
Fink Leonie Wichtelgruppe
|
|
||||||
Gojanaj Simon Wichtelgruppe
|
|
||||||
Haberthaler Anton Wichtelgruppe
|
|
||||||
Hoffmann Eva Wichtelgruppe
|
|
||||||
Hoffmann Eva Charlotte Wichtelgruppe
|
|
||||||
Koburger Paula Wichtelgruppe
|
|
||||||
Mair Magdalena Wichtelgruppe
|
|
||||||
Sammer Emilie Wichtelgruppe
|
|
||||||
|
|
||||||
10 angemeldete Kinder
|
|
||||||
|
|
||||||
Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden
|
|
||||||
Kinder an die Anmeldung erinnern.
|
|
||||||
|
|
||||||
Stand 26.05.2025 20:04 Uhr
|
|
||||||
|
|
||||||
|
|
||||||
Kinderfotos Erding
|
|
||||||
Gartenstr. 10 85445 Oberding
|
|
||||||
www.kinderfotos-erding.de
|
|
||||||
08122-8470867
|
|
||||||
|
|
||||||
--- SEITENWECHSEL / PAGE BREAK ---
|
|
||||||
|
|
||||||
Kinderhaus St. Martin Neuching Kinderfotos Erding
|
|
||||||
02. - 05.06.2025
|
|
||||||
|
|
||||||
Nachname Vorname Gruppe
|
|
||||||
---------------------------------------------------------------
|
|
||||||
Berndt Milena Wölfegruppe
|
|
||||||
Brose Sophia Wölfegruppe
|
|
||||||
Dressel Lucy Jolie Wölfegruppe
|
|
||||||
Etterer Nele Wölfegruppe
|
|
||||||
Fischer Benjamin Wölfegruppe
|
|
||||||
Götz Georg Wölfegruppe
|
|
||||||
Hermansdorfer Ludwig Wölfegruppe
|
|
||||||
Klimaschewski Leon Wölfegruppe
|
|
||||||
Magin Frederik Wölfegruppe
|
|
||||||
Müller Luisa Wölfegruppe
|
|
||||||
Niedermeier Lukas Wölfegruppe
|
|
||||||
|
|
||||||
11 angemeldete Kinder
|
|
||||||
|
|
||||||
Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden
|
|
||||||
Kinder an die Anmeldung erinnern.
|
|
||||||
|
|
||||||
Stand 26.05.2025 20:04 Uhr
|
|
||||||
|
|
||||||
|
|
||||||
Kinderfotos Erding
|
|
||||||
Gartenstr. 10 85445 Oberding
|
|
||||||
www.kinderfotos-erding.de
|
|
||||||
08122-8470867
|
|
||||||
|
|
||||||
--- SEITENWECHSEL / PAGE BREAK ---
|
|
||||||
|
|
||||||
Kinderhaus St. Martin Neuching Kinderfotos Erding
|
|
||||||
02. - 05.06.2025
|
|
||||||
|
|
||||||
Nachname Vorname Gruppe
|
|
||||||
------------------------------------------------------------------
|
|
||||||
Baur Leon Malouis Zwergerlgruppe
|
|
||||||
Grobler Eloise Zwergerlgruppe
|
|
||||||
Hagn Maximilian Zwergerlgruppe
|
|
||||||
Hren Jan Zwergerlgruppe
|
|
||||||
Kugler Amaya Marina Zwergerlgruppe
|
|
||||||
Schmidt Emilie Zwergerlgruppe
|
|
||||||
|
|
||||||
6 angemeldete Kinder
|
|
||||||
|
|
||||||
Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden
|
|
||||||
Kinder an die Anmeldung erinnern.
|
|
||||||
|
|
||||||
Stand 26.05.2025 20:04 Uhr
|
|
||||||
|
|
||||||
|
|
||||||
Kinderfotos Erding
|
|
||||||
Gartenstr. 10 85445 Oberding
|
|
||||||
www.kinderfotos-erding.de
|
|
||||||
08122-8470867
|
|
||||||
1757
HA_automations.yaml
1757
HA_automations.yaml
File diff suppressed because it is too large
Load Diff
@@ -1,667 +0,0 @@
|
|||||||
# Loads default set of integrations. Do not remove.
|
|
||||||
default_config:
|
|
||||||
|
|
||||||
# Load frontend themes from the themes folder
|
|
||||||
frontend:
|
|
||||||
themes: !include_dir_merge_named themes
|
|
||||||
|
|
||||||
# Text to speech
|
|
||||||
tts:
|
|
||||||
- platform: google_translate
|
|
||||||
|
|
||||||
automation: !include automations.yaml
|
|
||||||
script: !include scripts.yaml
|
|
||||||
scene: !include scenes.yaml
|
|
||||||
|
|
||||||
#Anleitung: https://book.cryd.de/books/projekte/page/hausverbrauch-strom-messen-incl-dummy-sensoren
|
|
||||||
|
|
||||||
utility_meter:
|
|
||||||
daily_upload_volume:
|
|
||||||
source: sensor.fritzbox_upload_volumen
|
|
||||||
cycle: daily
|
|
||||||
daily_download_volume:
|
|
||||||
source: sensor.fritzbox_download_volumen
|
|
||||||
cycle: daily
|
|
||||||
taeglicher_stromverbrauch:
|
|
||||||
source: sensor.stromzahler_energieverbrauch
|
|
||||||
cycle: daily
|
|
||||||
taegliche_einspeisung:
|
|
||||||
source: sensor.stromzahler_energieeinspeisung
|
|
||||||
cycle: daily
|
|
||||||
|
|
||||||
input_boolean:
|
|
||||||
manual_trigger:
|
|
||||||
name: Manual Trigger
|
|
||||||
initial: off
|
|
||||||
|
|
||||||
influxdb:
|
|
||||||
host: 127.0.0.1
|
|
||||||
#host: a0d7b954-influxdb
|
|
||||||
port: 8086
|
|
||||||
database: homeassistant
|
|
||||||
username: !secret influxdb_user
|
|
||||||
password: !secret influxdb_pw
|
|
||||||
max_retries: 3
|
|
||||||
default_measurement: state
|
|
||||||
|
|
||||||
alexa:
|
|
||||||
smart_home:
|
|
||||||
endpoint: https://api.eu.amazonalexa.com/v3/events
|
|
||||||
filter:
|
|
||||||
include_entities:
|
|
||||||
- light.living_room
|
|
||||||
- switch.kitchen
|
|
||||||
entity_config:
|
|
||||||
light.living_room:
|
|
||||||
name: "Wohnzimmer Licht"
|
|
||||||
switch.kitchen:
|
|
||||||
name: "Küchenschalter"
|
|
||||||
template:
|
|
||||||
- sensor:
|
|
||||||
- name: "Total Power3"
|
|
||||||
unique_id: "total_power_sensor3"
|
|
||||||
unit_of_measurement: "W"
|
|
||||||
device_class: power
|
|
||||||
state_class: measurement
|
|
||||||
state: >
|
|
||||||
{{
|
|
||||||
states('sensor.shelly_em3_channel_a_power') | float(0) +
|
|
||||||
states('sensor.shelly_em3_channel_b_power') | float(0) +
|
|
||||||
states('sensor.shelly_em3_channel_c_power') | float(0)
|
|
||||||
}}
|
|
||||||
- name: "Prozent Nutzung"
|
|
||||||
unique_id: "pv_prozent_nutzung"
|
|
||||||
unit_of_measurement: "%"
|
|
||||||
state: >
|
|
||||||
{% set total_power = states('sensor.total_power_v2') | float(0) + states('sensor.solaranlage_power') | float(0) %}
|
|
||||||
{% if total_power > 0 %}
|
|
||||||
{{ (100 * states('sensor.solaranlage_power') | float(0) / total_power) | round(1) }}
|
|
||||||
{% else %}
|
|
||||||
0
|
|
||||||
{% endif %}
|
|
||||||
- name: "Total Energy Use1"
|
|
||||||
unique_id: "total_energy_use1"
|
|
||||||
device_class: energy
|
|
||||||
state_class: total_increasing
|
|
||||||
unit_of_measurement: "kWh"
|
|
||||||
state: >
|
|
||||||
{{
|
|
||||||
states('sensor.shelly_em3_channel_a_energy') | float(0) +
|
|
||||||
states('sensor.shelly_em3_channel_b_energy') | float(0) +
|
|
||||||
states('sensor.shelly_em3_channel_c_energy') | float(0)
|
|
||||||
}}
|
|
||||||
- name: "Total Energy Returned1"
|
|
||||||
unique_id: "total_energy_returned1"
|
|
||||||
device_class: energy
|
|
||||||
state_class: total_increasing
|
|
||||||
unit_of_measurement: "kWh"
|
|
||||||
state: >
|
|
||||||
{{
|
|
||||||
states('sensor.shelly_em3_channel_a_energy_returned') | float(0) +
|
|
||||||
states('sensor.shelly_em3_channel_b_energy_returned') | float(0) +
|
|
||||||
states('sensor.shelly_em3_channel_c_energy_returned') | float(0)
|
|
||||||
}}
|
|
||||||
- name: "Aktuelle Solarleistung1"
|
|
||||||
unique_id: "aktuelle_solarleistung1"
|
|
||||||
unit_of_measurement: "W"
|
|
||||||
device_class: power
|
|
||||||
state_class: measurement
|
|
||||||
state: >
|
|
||||||
{{
|
|
||||||
max(0, states('sensor.esphome_web_39b3f0_charging_power_2') | float(0) -
|
|
||||||
states('sensor.esphome_web_39b3f0_discharging_power_2') | float(0) +
|
|
||||||
states('sensor.solaranlage_power') | float(0))
|
|
||||||
}}
|
|
||||||
- name: "Täglicher Stromverbrauch"
|
|
||||||
unit_of_measurement: "kWh"
|
|
||||||
state: >
|
|
||||||
{% set aktueller_wert = states('sensor.stromzahler_energieverbrauch') | float %}
|
|
||||||
{% set startwert = states('input_number.tagesstart_zaehlerstand') | float %}
|
|
||||||
{{ (aktueller_wert - startwert) | round(2) }}
|
|
||||||
- name: "Fritzbox Download Volumen"
|
|
||||||
unit_of_measurement: "MB"
|
|
||||||
state: >
|
|
||||||
{% set rate_kbps = states('sensor.fritz_box_7530_download_durchsatz') | float %}
|
|
||||||
{% set rate_kBps = rate_kbps / 8 %} # Kilobits pro Sekunde in Kilobytes umrechnen
|
|
||||||
{{ (rate_kBps * 60) / 1024 }} # Datenvolumen pro Minute in Megabyte
|
|
||||||
- name: "Fritzbox Upload Volumen"
|
|
||||||
unit_of_measurement: "MB"
|
|
||||||
state: >
|
|
||||||
{% set rate_kbps = states('sensor.fritz_box_7530_upload_durchsatz') | float %}
|
|
||||||
{% set rate_kBps = rate_kbps / 8 %} # Kilobits pro Sekunde in Kilobytes umrechnen
|
|
||||||
{{ (rate_kBps * 60) / 1024 }} # Datenvolumen pro Minute in Megabyte
|
|
||||||
- name: "Aktueller Strompreis"
|
|
||||||
state: "{{ states('input_number.strompreis') }}"
|
|
||||||
unit_of_measurement: "€/kWh"
|
|
||||||
device_class: monetary
|
|
||||||
- name: "Stromverbrauch Vortag"
|
|
||||||
unique_id: "stromverbrauch_vortag"
|
|
||||||
unit_of_measurement: "kWh"
|
|
||||||
device_class: energy
|
|
||||||
state: >
|
|
||||||
{% set stats = state_attr('sensor.taeglicher_stromverbrauch', 'last_period') %}
|
|
||||||
{{ stats | float(0) }}
|
|
||||||
- name: "Einspeisung Vortag"
|
|
||||||
unique_id: "einspeisung_vortag"
|
|
||||||
unit_of_measurement: "kWh"
|
|
||||||
device_class: energy
|
|
||||||
state: >
|
|
||||||
{% set stats = state_attr('sensor.taegliche_einspeisung', 'last_period') %}
|
|
||||||
{{ stats | float(0) }}
|
|
||||||
- name: "Generiert Vortag (Template)"
|
|
||||||
unique_id: "generiert_vortag_template"
|
|
||||||
unit_of_measurement: "kWh"
|
|
||||||
device_class: energy
|
|
||||||
state: >
|
|
||||||
{% set stats = state_attr('sensor.komplett_solarlieferung', 'last_period') %}
|
|
||||||
{{ stats | float(0) }}
|
|
||||||
- name: "Nächste Müllabholung"
|
|
||||||
state: >-
|
|
||||||
{% set today = now().date().isoformat() %}
|
|
||||||
{% for date in states.sensor.garbage.attributes.keys() | list | sort %}
|
|
||||||
{% if date >= today %}
|
|
||||||
{{ date }} - {{ states.sensor.garbage.attributes[date] }}
|
|
||||||
{% break %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
- name: "Statistik Solarerzeugung Durchschnitt"
|
|
||||||
state: "{{ now().year }}"
|
|
||||||
attributes:
|
|
||||||
data: >
|
|
||||||
{{ states('sensor.gsheet_data') }}
|
|
||||||
- name: "Solarertrag 2022"
|
|
||||||
state: "OK"
|
|
||||||
attributes:
|
|
||||||
values: >
|
|
||||||
{% set raw_data = state_attr('sensor.statistik_solarerzeugung_durchschnitt_mqtt', 'data')[1][1:] %}
|
|
||||||
{{ raw_data | map('replace', ',', '.') | map('float') | list }}
|
|
||||||
- name: "Solarertrag 2023"
|
|
||||||
state: "OK"
|
|
||||||
attributes:
|
|
||||||
values: >
|
|
||||||
{% set raw_data = state_attr('sensor.statistik_solarerzeugung_durchschnitt_mqtt', 'data')[2][1:] %}
|
|
||||||
{{ raw_data | map('replace', ',', '.') | map('float') | list }}
|
|
||||||
- name: "Solarertrag 2024"
|
|
||||||
state: "OK"
|
|
||||||
attributes:
|
|
||||||
values: >
|
|
||||||
{% set raw_data = state_attr('sensor.statistik_solarerzeugung_durchschnitt_mqtt', 'data')[3][1:] %}
|
|
||||||
{{ raw_data | map('replace', ',', '.') | map('float') | list }}
|
|
||||||
- name: "Solarertrag 2025"
|
|
||||||
state: "OK"
|
|
||||||
attributes:
|
|
||||||
values: >
|
|
||||||
{% set raw_data = state_attr('sensor.statistik_solarerzeugung_durchschnitt_mqtt', 'data')[4][1:] %}
|
|
||||||
{{ raw_data | map('replace', ',', '.') | map('float') | list }}
|
|
||||||
- name: "Solarertrag 2022 Werte"
|
|
||||||
state: "{{ state_attr('sensor.solarertrag_2022', 'values')[-1] | float(0) }}"
|
|
||||||
unit_of_measurement: "kWh" # Passen Sie die Einheit an
|
|
||||||
state_class: measurement
|
|
||||||
attributes:
|
|
||||||
alle_werte: "{{ state_attr('sensor.solarertrag_2022', 'values') }}"
|
|
||||||
|
|
||||||
- name: "Kühlschrank Letzte Aktivzeit"
|
|
||||||
unique_id: kuehlschrank_letzte_aktivzeit
|
|
||||||
unit_of_measurement: "min"
|
|
||||||
state: >
|
|
||||||
{% set aktiv_start = states.binary_sensor.kuehlschrank_laeuft.last_changed %}
|
|
||||||
{% if is_state('binary_sensor.kuehlschrank_laeuft', 'on') %}
|
|
||||||
{{ ((now() - aktiv_start).total_seconds() / 60) | round(1) }}
|
|
||||||
{% else %}
|
|
||||||
0
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
- name: "Kühlschrank Letzte Pausezeit"
|
|
||||||
unique_id: kuehlschrank_letzte_pausezeit
|
|
||||||
unit_of_measurement: "min"
|
|
||||||
state: >
|
|
||||||
{% set pause_start = states.binary_sensor.kuehlschrank_laeuft.last_changed %}
|
|
||||||
{% if is_state('binary_sensor.kuehlschrank_laeuft', 'off') %}
|
|
||||||
{{ ((now() - pause_start).total_seconds() / 60) | round(1) }}
|
|
||||||
{% else %}
|
|
||||||
0
|
|
||||||
{% endif %}
|
|
||||||
sensor:
|
|
||||||
- platform: average
|
|
||||||
name: "Durchschnittsverbrauch"
|
|
||||||
unique_id: "durchschnitt_verbrauch"
|
|
||||||
duration: 60
|
|
||||||
entities:
|
|
||||||
- sensor.total_power_v2
|
|
||||||
|
|
||||||
- platform: average
|
|
||||||
name: "Durchschnittsertrag"
|
|
||||||
unique_id: "durchschnitt_ertrag"
|
|
||||||
duration: 180
|
|
||||||
entities:
|
|
||||||
- sensor.aktuelle_solarleistung1
|
|
||||||
|
|
||||||
- platform: teamtracker
|
|
||||||
league_id: "BUND"
|
|
||||||
team_id: "MUC"
|
|
||||||
name: "Bayern2"
|
|
||||||
|
|
||||||
- platform: integration
|
|
||||||
name: Upload Volume
|
|
||||||
source: sensor.fritz_box_7530_upload_durchsatz
|
|
||||||
unit_prefix: k
|
|
||||||
round: 2
|
|
||||||
- platform: integration
|
|
||||||
name: Download Volume
|
|
||||||
source: sensor.fritz_box_7530_download_durchsatz
|
|
||||||
unit_prefix: k
|
|
||||||
round: 2
|
|
||||||
- platform: statistics
|
|
||||||
name: "Generiert Vortag (Statistik)"
|
|
||||||
entity_id: sensor.solaranlage_energy
|
|
||||||
state_characteristic: change
|
|
||||||
max_age:
|
|
||||||
days: 1
|
|
||||||
- platform: rest
|
|
||||||
name: "Google Sheets Daten"
|
|
||||||
resource: "https://script.google.com/macros/s/AKfycbz4sAiMvufOqL-gv5o7YfjaL4V0eWu9dGren_xg6pV35dE8bMyzaQckKp5WCs6ex5bbdA/exec"
|
|
||||||
scan_interval: 600 # Aktualisiert alle 10 Minuten
|
|
||||||
value_template: "{{ value_json[0] }}" # Falls erforderlich, kann dies angepasst werden
|
|
||||||
json_attributes:
|
|
||||||
- "Gesamtwerte"
|
|
||||||
- "Genergiert"
|
|
||||||
- "Einspeisung"
|
|
||||||
- "Netzverbrauch"
|
|
||||||
- "Solarverbrauch"
|
|
||||||
- "Gesamtverbrauch"
|
|
||||||
- "Durchschnittl. Nutzung"
|
|
||||||
- "Autarkiegrad"
|
|
||||||
- "Ersparnis in € / Tag"
|
|
||||||
- "Ersparnis gesamt"
|
|
||||||
- "Prozent Abgezahlt"
|
|
||||||
- "Gesamnt abgezahlt"
|
|
||||||
- platform: history_stats
|
|
||||||
name: "Kühlschrank Aktivzeit"
|
|
||||||
entity_id: binary_sensor.kuehlschrank_laeuft
|
|
||||||
state: "on"
|
|
||||||
type: time
|
|
||||||
start: "{{ now() - timedelta(hours=24) }}"
|
|
||||||
end: "{{ now() }}"
|
|
||||||
|
|
||||||
- platform: history_stats
|
|
||||||
name: "Kühlschrank Pausezeit"
|
|
||||||
entity_id: binary_sensor.kuehlschrank_laeuft
|
|
||||||
state: "off"
|
|
||||||
type: time
|
|
||||||
start: "{{ now() - timedelta(hours=24) }}"
|
|
||||||
end: "{{ now() }}"
|
|
||||||
- platform: statistics
|
|
||||||
name: "Kühlschrank Durchschnitt Aktivzeit"
|
|
||||||
entity_id: sensor.kuehlschrank_letzte_aktivzeit
|
|
||||||
state_characteristic: mean
|
|
||||||
max_age:
|
|
||||||
hours: 24
|
|
||||||
sampling_size: 10
|
|
||||||
|
|
||||||
- platform: statistics
|
|
||||||
name: "Kühlschrank Durchschnitt Pausezeit"
|
|
||||||
entity_id: sensor.kuehlschrank_letzte_pausezeit
|
|
||||||
state_characteristic: mean
|
|
||||||
max_age:
|
|
||||||
hours: 24
|
|
||||||
sampling_size: 10
|
|
||||||
|
|
||||||
input_datetime:
|
|
||||||
kuehlschrank_ende_aktiv:
|
|
||||||
name: "Ende aktive Phase"
|
|
||||||
has_time: true
|
|
||||||
kuehlschrank_ende_pause:
|
|
||||||
name: "Ende Pause Phase"
|
|
||||||
has_time: true
|
|
||||||
|
|
||||||
waste_collection_schedule:
|
|
||||||
sources:
|
|
||||||
- name: awido_de
|
|
||||||
args:
|
|
||||||
customer: Erding
|
|
||||||
city: Oberding
|
|
||||||
street: "Gartenstraße"
|
|
||||||
|
|
||||||
binary_sensor:
|
|
||||||
- platform: template
|
|
||||||
sensors:
|
|
||||||
kuehlschrank_laeuft:
|
|
||||||
friendly_name: "Kühlschrank läuft"
|
|
||||||
value_template: "{{ states('sensor.kuehlschrank_power')|float > 50 }}"
|
|
||||||
|
|
||||||
mqtt:
|
|
||||||
sensor:
|
|
||||||
- name: "Balkonkraftwerk Leistung AC"
|
|
||||||
state_topic: "inverter/hm600/ch0/P_AC"
|
|
||||||
device_class: power
|
|
||||||
unit_of_measurement: W
|
|
||||||
state_class: measurement
|
|
||||||
unique_id: "BalkonkraftwerkLeistungAC"
|
|
||||||
- name: "Balkonkraftwerk Module 1 Leistung"
|
|
||||||
state_topic: "inverter/hm600/ch1/P_DC"
|
|
||||||
device_class: power
|
|
||||||
unit_of_measurement: W
|
|
||||||
state_class: measurement
|
|
||||||
unique_id: "BalkonkraftwerkModule13Leistung"
|
|
||||||
- name: "Balkonkraftwerk Module 2 Leistung"
|
|
||||||
state_topic: "inverter/hm600/ch2/P_DC"
|
|
||||||
device_class: power
|
|
||||||
unit_of_measurement: W
|
|
||||||
state_class: measurement
|
|
||||||
unique_id: "BalkonkraftwerkModule24Leistung"
|
|
||||||
- name: "Balkonkraftwerk Temperatur"
|
|
||||||
state_topic: "inverter/hm600/ch0/Temp"
|
|
||||||
device_class: temperature
|
|
||||||
unit_of_measurement: °C
|
|
||||||
state_class: measurement
|
|
||||||
unique_id: "BalkonkraftwerkTemperatur"
|
|
||||||
- name: "Balkonkraftwerk Arbeit Tag"
|
|
||||||
state_topic: "inverter/hm600/ch0/YieldDay"
|
|
||||||
device_class: energy
|
|
||||||
unit_of_measurement: Wh
|
|
||||||
state_class: total_increasing
|
|
||||||
unique_id: "BalkonkraftwerkArbeitTag"
|
|
||||||
- name: "Balkonkraftwerk Arbeit Gesamt"
|
|
||||||
state_topic: "inverter/hm600/ch0/YieldTotal"
|
|
||||||
device_class: energy
|
|
||||||
unit_of_measurement: kWh
|
|
||||||
state_class: total_increasing
|
|
||||||
unique_id: "BalkonkraftwerkArbeitGesamt"
|
|
||||||
- name: "version"
|
|
||||||
state_topic: "inverter/version"
|
|
||||||
unique_id: "version_dtu"
|
|
||||||
- name: "Limit"
|
|
||||||
state_topic: "inverter/hm600/ch0/active_PowerLimit"
|
|
||||||
unique_id: "set_powerlimit"
|
|
||||||
- name: "Energy Akkuentladung current"
|
|
||||||
device_class: power
|
|
||||||
unit_of_measurement: "W"
|
|
||||||
state_topic: "esphome-web-39b3f0/sensor/esphome-web-39b3f0_discharging_power"
|
|
||||||
unique_id: "energy_akkuentladung"
|
|
||||||
- name: "Energy Akkuentladung total"
|
|
||||||
device_class: energy
|
|
||||||
unit_of_measurement: "kWh"
|
|
||||||
state_topic: "esphome-web-39b3f0/sensor/esphome-web-39b3f0_discharging_power"
|
|
||||||
- name: "Effizienz HM600"
|
|
||||||
unit_of_measurement: "%"
|
|
||||||
state_topic: "inverter/hm600/ch0/Efficiency"
|
|
||||||
unique_id: "effizienz_hm600"
|
|
||||||
- name: "HM600 Spannung"
|
|
||||||
unit_of_measurement: "V"
|
|
||||||
state_topic: "inverter/hm600/ch1/U_DC"
|
|
||||||
- name: "Waschmaschine Leistung"
|
|
||||||
state_topic: "shellyplus1pm-84cca8771670/status/switch:0"
|
|
||||||
value_template: "{{ value_json.apower }}"
|
|
||||||
unit_of_measurement: "W"
|
|
||||||
device_class: power
|
|
||||||
- name: "Waschmaschine Energieverbrauch"
|
|
||||||
state_topic: "shellyplus1pm-84cca8771670/status/switch:0"
|
|
||||||
value_template: "{{ value_json.aenergy.total }}"
|
|
||||||
unit_of_measurement: "kWh"
|
|
||||||
device_class: energy
|
|
||||||
- name: "Statistik Solarerzeugung Durchschnitt mqtt"
|
|
||||||
state_topic: "homeassistant/sensor/gsheet_data"
|
|
||||||
value_template: "{{ value_json.state }}"
|
|
||||||
json_attributes_topic: "homeassistant/sensor/gsheet_data"
|
|
||||||
json_attributes_template: "{{ value_json.attributes | tojson }}"
|
|
||||||
|
|
||||||
logger:
|
|
||||||
default: warning
|
|
||||||
logs:
|
|
||||||
custom_components.awtrix: warning
|
|
||||||
homeassistant.components.sensor: warning
|
|
||||||
|
|
||||||
# Nächste Abholung Restmüll
|
|
||||||
|
|
||||||
# - name: "Restmüll"
|
|
||||||
# state: '{{value.types|join(", ")}}{% if value.daysTo == 0 %} Heute{% elif value.daysTo == 1 %} Morgen{% else %} in {{value.daysTo}} Tagen{% endif %}'
|
|
||||||
# attributes:
|
|
||||||
# value_template: '{{value.types|join(", ")}}'
|
|
||||||
# unique_id: "restmuell"
|
|
||||||
# unit_of_measurement: "days"
|
|
||||||
# device_class: "timestamp"
|
|
||||||
# value_template: '{{(states.sensor.waste_collection_schedule.attributes.next_date)|as_timestamp | timestamp_local}}'
|
|
||||||
|
|
||||||
# Nächste Abholung Biotonne
|
|
||||||
|
|
||||||
# - name: "Biotonne"
|
|
||||||
# state: '{{value.types|join(", ")}}{% if value.daysTo == 0 %} Heute{% elif value.daysTo == 1 %} Morgen{% else %} in {{value.daysTo}} Tagen{% endif %}'
|
|
||||||
# attributes:
|
|
||||||
# value_template: '{{value.types|join(", ")}}'
|
|
||||||
# unique_id: "biotonne"
|
|
||||||
# unit_of_measurement: "days"
|
|
||||||
# device_class: "timestamp"
|
|
||||||
# value_template: '{{(states.sensor.waste_collection_schedule.attributes.next_date)|as_timestamp | timestamp_local}}'
|
|
||||||
|
|
||||||
##sensor:
|
|
||||||
# - platform: average
|
|
||||||
# name: 'Durchschnittsverbrauch'
|
|
||||||
# unique_id: 'durchschnitt_verbrauch'
|
|
||||||
# duration: 60
|
|
||||||
# entities:
|
|
||||||
# - sensor.total_power
|
|
||||||
# - platform: average
|
|
||||||
# name: 'Durchschnittsertrag'
|
|
||||||
# unique_id: 'durchschnitt_ertrag'
|
|
||||||
# duration: 180
|
|
||||||
# entities:
|
|
||||||
# - sensor.aktuelle_solarleistung
|
|
||||||
# - platform: teamtracker
|
|
||||||
# league_id: "BUND"
|
|
||||||
# team_id: "MUC"
|
|
||||||
# name: "Bayern2"
|
|
||||||
#
|
|
||||||
# - platform: template
|
|
||||||
# name: "Total Power"
|
|
||||||
# unique_id: "Total_Energy"
|
|
||||||
# device_class: power
|
|
||||||
# state_class: total
|
|
||||||
# unit_of_measurement: "W"
|
|
||||||
# value_template: >
|
|
||||||
# {{
|
|
||||||
# states('sensor.shelly_em3_channel_a_power')| float(0) +
|
|
||||||
# states('sensor.shelly_em3_channel_b_power')| float(0) +
|
|
||||||
# states('sensor.shelly_em3_channel_c_power')| float(0)
|
|
||||||
# }}
|
|
||||||
#
|
|
||||||
# - platform: template
|
|
||||||
# name: "Total Energy Use1"
|
|
||||||
# unique_id: "Total_Energy_Use1"
|
|
||||||
# device_class: energy
|
|
||||||
# state_class: total
|
|
||||||
# unit_of_measurement: "kWh"
|
|
||||||
# value_template: >
|
|
||||||
# {{
|
|
||||||
# states('sensor.shelly_em3_channel_a_energy')| float(0) +
|
|
||||||
# states('sensor.shelly_em3_channel_b_energy')| float(0) +
|
|
||||||
# states('sensor.shelly_em3_channel_c_energy')| float(0)
|
|
||||||
# }}
|
|
||||||
#
|
|
||||||
# - name: "Total Energy Returned1"
|
|
||||||
# unique_id: "Total_Energy_Returned1"
|
|
||||||
# device_class: energy
|
|
||||||
# state_class: total
|
|
||||||
# unit_of_measurement: "kWh"
|
|
||||||
# value_template: >
|
|
||||||
# {{
|
|
||||||
# states('sensor.shelly_em3_channel_a_energy_returned')| float(0) +
|
|
||||||
# states('sensor.shelly_em3_channel_b_energy_returned')| float(0) +
|
|
||||||
# states('sensor.shelly_em3_channel_c_energy_returned')| float(0)
|
|
||||||
# }}
|
|
||||||
#
|
|
||||||
# - name: "PV Einspeisung"
|
|
||||||
# unique_id: "pv_einspeisung"
|
|
||||||
# unit_of_measurement: "W"
|
|
||||||
# device_class: power
|
|
||||||
# value_template: "{{ states('sensor.total_power')|float if states('sensor.total_power') | int < 1 else 0 }}"
|
|
||||||
#
|
|
||||||
# - name: "PV Einspeisung negiert"
|
|
||||||
# unique_id: "pv_einspeisung_negiert"
|
|
||||||
# unit_of_measurement: "W"
|
|
||||||
# device_class: power
|
|
||||||
# value_template: "{{ states('sensor.pv_einspeisung')|float * -1 }}"
|
|
||||||
#
|
|
||||||
# - name: "Wirkungsgrad"
|
|
||||||
# unique_id: "wirkungsgrad_battery"
|
|
||||||
# unit_of_measurement: "%"
|
|
||||||
# device_class: power
|
|
||||||
# value_template: >
|
|
||||||
# {{(100 * states('sensor.solaranlage_power')| float(0) / states('sensor.esphome_web_39b3f0_discharging_power')| float(0)) | round(1) }}
|
|
||||||
#
|
|
||||||
# - name: "Prozent_Nutzung"
|
|
||||||
# unique_id: "pv_prozent_nutzung"
|
|
||||||
# unit_of_measurement: "%"
|
|
||||||
# device_class: power
|
|
||||||
# value_template: >
|
|
||||||
# {{
|
|
||||||
# (100 * states('sensor.solaranlage_power')| float(0) / (states('sensor.solaranlage_power')| float(0) + states('sensor.total_power_v2')| float(0))) | round(1)
|
|
||||||
# }}
|
|
||||||
#
|
|
||||||
# - name: "Aktuelle_Solarleistung"
|
|
||||||
# unique_id: "aktuelle-solarleistung"
|
|
||||||
# unit_of_measurement: "W"
|
|
||||||
# device_class: power
|
|
||||||
# value_template: >
|
|
||||||
# {{
|
|
||||||
# max(0, states('sensor.esphome_web_39b3f0_charging_power_2')| float(0) -
|
|
||||||
# states('sensor.esphome_web_39b3f0_discharging_power_2')| float(0) +
|
|
||||||
# states('sensor.solaranlage_power')|float(0) +
|
|
||||||
# }}
|
|
||||||
#
|
|
||||||
# //states('sensor.akku_power')|float(0)) removed from aktuelle solarleistung
|
|
||||||
#
|
|
||||||
# - name: "Summierter Ertrag"
|
|
||||||
# unique_id: "summierter_ertrag"
|
|
||||||
# unit_of_measurement: "W"
|
|
||||||
# device_class: power
|
|
||||||
# value_template: >
|
|
||||||
# {{
|
|
||||||
# states('sensor.akku_power')| float(0) +
|
|
||||||
# states('sensor.solaranlage_power')|float(0)
|
|
||||||
# }}
|
|
||||||
#
|
|
||||||
# - name: "Total Power"
|
|
||||||
# unique_id: "Total_Energy"
|
|
||||||
# device_class: power
|
|
||||||
# state_class: total
|
|
||||||
# unit_of_measurement: "W"
|
|
||||||
# value_template: >
|
|
||||||
# {{
|
|
||||||
# states('sensor.shelly_em3_channel_a_power')| float(0) +
|
|
||||||
# states('sensor.shelly_em3_channel_b_power')| float(0) +
|
|
||||||
# states('sensor.shelly_em3_channel_c_power')| float(0)
|
|
||||||
# }}
|
|
||||||
#
|
|
||||||
# - name: "Total Energy Use"
|
|
||||||
# unique_id: "Total_Energy_Use"
|
|
||||||
# device_class: energy
|
|
||||||
# state_class: total
|
|
||||||
# unit_of_measurement: "kWh"
|
|
||||||
# value_template: >
|
|
||||||
# {{
|
|
||||||
# states('sensor.shelly_em3_channel_a_energy')| float(0) +
|
|
||||||
# states('sensor.shelly_em3_channel_b_energy')| float(0) +
|
|
||||||
# states('sensor.shelly_em3_channel_c_energy')| float(0)
|
|
||||||
# }}
|
|
||||||
#
|
|
||||||
# - name: "Total Energy Returned"
|
|
||||||
# unique_id: "Total_Energy_Returned"
|
|
||||||
# device_class: energy
|
|
||||||
# state_class: total
|
|
||||||
# unit_of_measurement: "kWh"
|
|
||||||
# value_template: >
|
|
||||||
# {{
|
|
||||||
# states('sensor.shelly_em3_channel_a_energy_returned')| float(0) +
|
|
||||||
# states('sensor.shelly_em3_channel_b_energy_returned')| float(0) +
|
|
||||||
# states('sensor.shelly_em3_channel_c_energy_returned')| float(0)
|
|
||||||
# }}
|
|
||||||
#
|
|
||||||
# - name: "PV Einspeisung"
|
|
||||||
# unique_id: "pv_einspeisung"
|
|
||||||
# unit_of_measurement: "W"
|
|
||||||
# device_class: power
|
|
||||||
# value_template: "{{ states('sensor.total_power')|float if states('sensor.total_power') | int < 1 else 0 }}"
|
|
||||||
#
|
|
||||||
# - name: "PV Einspeisung negiert"
|
|
||||||
# unique_id: "pv_einspeisung_negiert"
|
|
||||||
# unit_of_measurement: "W"
|
|
||||||
# device_class: power
|
|
||||||
# value_template: "{{ states('sensor.pv_einspeisung')|float * -1 }}"
|
|
||||||
#
|
|
||||||
# - name: "Wirkungsgrad"
|
|
||||||
# unique_id: "wirkungsgrad_battery"
|
|
||||||
# unit_of_measurement: "%"
|
|
||||||
# device_class: power
|
|
||||||
# value_template: >
|
|
||||||
# {{(100 * states('sensor.solaranlage_power')| float(0) / states('sensor.esphome_web_39b3f0_discharging_power')| float(0)) | round(1) }}###
|
|
||||||
#
|
|
||||||
# - name: "Prozent_Nutzung"
|
|
||||||
# unique_id: "pv_prozent_nutzung"
|
|
||||||
# unit_of_measurement: "%"
|
|
||||||
# device_class: power
|
|
||||||
# value_template: >
|
|
||||||
# {{
|
|
||||||
# (100 * states('sensor.solaranlage_power')| float(0) / (states('sensor.solaranlage_power')| float(0) + states('sensor.total_power')| float(0))) | round(1)
|
|
||||||
# }}
|
|
||||||
#
|
|
||||||
# - name: "Aktuelle_Solarleistung"
|
|
||||||
# unique_id: "aktuelle-solarleistung"
|
|
||||||
# unit_of_measurement: "W"
|
|
||||||
# device_class: power
|
|
||||||
# value_template: >
|
|
||||||
# {{
|
|
||||||
# max(0, states('sensor.esphome_web_39b3f0_charging_power_2')| float(0) -
|
|
||||||
# states('sensor.esphome_web_39b3f0_discharging_power_2')| float(0) +
|
|
||||||
# states('sensor.solaranlage_power')|float(0) +
|
|
||||||
# states('sensor.akku_power')|float(0))
|
|
||||||
# }}
|
|
||||||
#
|
|
||||||
# - name: "Summierter Ertrag"
|
|
||||||
# unique_id: "summierter_ertrag"
|
|
||||||
# unit_of_measurement: "W"
|
|
||||||
# device_class: power
|
|
||||||
# value_template: >
|
|
||||||
# {{
|
|
||||||
# states('sensor.akku_power')| float(0) +
|
|
||||||
# states('sensor.solaranlage_power')|float(0)
|
|
||||||
# }}
|
|
||||||
|
|
||||||
# https://community.home-assistant.io/t/hoymiles-dtu-microinverters-pv/253674/21
|
|
||||||
|
|
||||||
# statistics
|
|
||||||
#- platform: statistics
|
|
||||||
# entity_id: sensor.total_power_av
|
|
||||||
# sampling_size: 20
|
|
||||||
|
|
||||||
#powercalc:
|
|
||||||
|
|
||||||
#sensor:
|
|
||||||
# - platform: powercalc
|
|
||||||
# entity_id: light.esslicht
|
|
||||||
# fixed:
|
|
||||||
# states_power:
|
|
||||||
# off: 0.4
|
|
||||||
# on: 22
|
|
||||||
|
|
||||||
# platform: template
|
|
||||||
# daily_solar_percent:
|
|
||||||
# value_template: "{{ ( 100 * states('sensor.total_power')|float / states('sensor.solaranlage_power')|float )|round(1) }}"
|
|
||||||
# unit_of_measurement: '%'
|
|
||||||
# friendly_name: Daily Solar Percentage
|
|
||||||
|
|
||||||
# ssl Configuration
|
|
||||||
# http:
|
|
||||||
# ssl_certificate: /ssl/fullchain.pem
|
|
||||||
# ssl_key: /ssl/privkey.pem
|
|
||||||
|
|
||||||
http:
|
|
||||||
use_x_forwarded_for: true
|
|
||||||
trusted_proxies:
|
|
||||||
- 127.0.0.1
|
|
||||||
- 192.168.178.6
|
|
||||||
- 172.16.0.0/12
|
|
||||||
- ::1
|
|
||||||
#ssl_certificate: "/ssl/fullchain.pem"
|
|
||||||
#ssl_key: "/ssl/privkey.pem"
|
|
||||||
homeassistant:
|
|
||||||
external_url: "https://floke-ha.duckdns.org"
|
|
||||||
260
HA_jbd_bms.yaml
260
HA_jbd_bms.yaml
@@ -1,260 +0,0 @@
|
|||||||
substitutions:
|
|
||||||
name: esphome-web-39b3f0
|
|
||||||
device_description: "Monitor and control a Xiaoxiang Battery Management System (JBD-BMS) via BLE"
|
|
||||||
external_components_source: github://syssi/esphome-jbd-bms@main
|
|
||||||
mac_address: A4:C1:37:00:86:5A
|
|
||||||
|
|
||||||
esphome:
|
|
||||||
name: ${name}
|
|
||||||
comment: ${device_description}
|
|
||||||
min_version: 2024.6.0
|
|
||||||
project:
|
|
||||||
name: "syssi.esphome-jbd-bms"
|
|
||||||
version: 2.1.0
|
|
||||||
|
|
||||||
esp32:
|
|
||||||
board: esp32dev
|
|
||||||
framework:
|
|
||||||
type: esp-idf
|
|
||||||
|
|
||||||
external_components:
|
|
||||||
- source: ${external_components_source}
|
|
||||||
refresh: 0s
|
|
||||||
|
|
||||||
wifi:
|
|
||||||
ssid: !secret wifi_ssid
|
|
||||||
password: !secret wifi_password
|
|
||||||
|
|
||||||
ota:
|
|
||||||
platform: esphome
|
|
||||||
|
|
||||||
logger:
|
|
||||||
level: DEBUG
|
|
||||||
|
|
||||||
# If you use Home Assistant please remove this `mqtt` section and uncomment the `api` component!
|
|
||||||
# The native API has many advantages over MQTT: https://esphome.io/components/api.html#advantages-over-mqtt
|
|
||||||
#mqtt:
|
|
||||||
# broker: !secret mqtt_host
|
|
||||||
# username: !secret mqtt_username
|
|
||||||
# password: !secret mqtt_password
|
|
||||||
# id: mqtt_client
|
|
||||||
|
|
||||||
# api:
|
|
||||||
|
|
||||||
esp32_ble_tracker:
|
|
||||||
scan_parameters:
|
|
||||||
active: false
|
|
||||||
|
|
||||||
ble_client:
|
|
||||||
- id: client0
|
|
||||||
mac_address: ${mac_address}
|
|
||||||
|
|
||||||
jbd_bms_ble:
|
|
||||||
- id: bms0
|
|
||||||
ble_client_id: client0
|
|
||||||
# Some Liontron BMS models require an update interval of less than 8s
|
|
||||||
update_interval: 2s
|
|
||||||
|
|
||||||
button:
|
|
||||||
- platform: jbd_bms_ble
|
|
||||||
jbd_bms_ble_id: bms0
|
|
||||||
retrieve_hardware_version:
|
|
||||||
name: "${name} retrieve hardware version"
|
|
||||||
force_soc_reset:
|
|
||||||
name: "${name} force soc reset"
|
|
||||||
|
|
||||||
binary_sensor:
|
|
||||||
- platform: jbd_bms_ble
|
|
||||||
jbd_bms_ble_id: bms0
|
|
||||||
balancing:
|
|
||||||
name: "${name} balancing"
|
|
||||||
charging:
|
|
||||||
name: "${name} charging"
|
|
||||||
discharging:
|
|
||||||
name: "${name} discharging"
|
|
||||||
online_status:
|
|
||||||
name: "${name} online status"
|
|
||||||
|
|
||||||
sensor:
|
|
||||||
- platform: jbd_bms_ble
|
|
||||||
jbd_bms_ble_id: bms0
|
|
||||||
battery_strings:
|
|
||||||
name: "${name} battery strings"
|
|
||||||
current:
|
|
||||||
name: "${name} current"
|
|
||||||
power:
|
|
||||||
name: "${name} power"
|
|
||||||
charging_power:
|
|
||||||
name: "${name} charging power"
|
|
||||||
discharging_power:
|
|
||||||
name: "${name} discharging power"
|
|
||||||
state_of_charge:
|
|
||||||
name: "${name} state of charge"
|
|
||||||
nominal_capacity:
|
|
||||||
name: "${name} nominal capacity"
|
|
||||||
charging_cycles:
|
|
||||||
name: "${name} charging cycles"
|
|
||||||
capacity_remaining:
|
|
||||||
name: "${name} capacity remaining"
|
|
||||||
battery_cycle_capacity:
|
|
||||||
name: "${name} battery cycle capacity"
|
|
||||||
total_voltage:
|
|
||||||
name: "${name} total voltage"
|
|
||||||
average_cell_voltage:
|
|
||||||
name: "${name} average cell voltage"
|
|
||||||
delta_cell_voltage:
|
|
||||||
name: "${name} delta cell voltage"
|
|
||||||
min_cell_voltage:
|
|
||||||
name: "${name} min cell voltage"
|
|
||||||
max_cell_voltage:
|
|
||||||
name: "${name} max cell voltage"
|
|
||||||
min_voltage_cell:
|
|
||||||
name: "${name} min voltage cell"
|
|
||||||
max_voltage_cell:
|
|
||||||
name: "${name} max voltage cell"
|
|
||||||
temperature_1:
|
|
||||||
name: "${name} temperature 1"
|
|
||||||
temperature_2:
|
|
||||||
name: "${name} temperature 2"
|
|
||||||
temperature_3:
|
|
||||||
name: "${name} temperature 3"
|
|
||||||
temperature_4:
|
|
||||||
name: "${name} temperature 4"
|
|
||||||
temperature_5:
|
|
||||||
name: "${name} temperature 5"
|
|
||||||
temperature_6:
|
|
||||||
name: "${name} temperature 6"
|
|
||||||
cell_voltage_1:
|
|
||||||
name: "${name} cell voltage 1"
|
|
||||||
cell_voltage_2:
|
|
||||||
name: "${name} cell voltage 2"
|
|
||||||
cell_voltage_3:
|
|
||||||
name: "${name} cell voltage 3"
|
|
||||||
cell_voltage_4:
|
|
||||||
name: "${name} cell voltage 4"
|
|
||||||
cell_voltage_5:
|
|
||||||
name: "${name} cell voltage 5"
|
|
||||||
cell_voltage_6:
|
|
||||||
name: "${name} cell voltage 6"
|
|
||||||
cell_voltage_7:
|
|
||||||
name: "${name} cell voltage 7"
|
|
||||||
cell_voltage_8:
|
|
||||||
name: "${name} cell voltage 8"
|
|
||||||
cell_voltage_9:
|
|
||||||
name: "${name} cell voltage 9"
|
|
||||||
cell_voltage_10:
|
|
||||||
name: "${name} cell voltage 10"
|
|
||||||
cell_voltage_11:
|
|
||||||
name: "${name} cell voltage 11"
|
|
||||||
cell_voltage_12:
|
|
||||||
name: "${name} cell voltage 12"
|
|
||||||
cell_voltage_13:
|
|
||||||
name: "${name} cell voltage 13"
|
|
||||||
cell_voltage_14:
|
|
||||||
name: "${name} cell voltage 14"
|
|
||||||
cell_voltage_15:
|
|
||||||
name: "${name} cell voltage 15"
|
|
||||||
cell_voltage_16:
|
|
||||||
name: "${name} cell voltage 16"
|
|
||||||
cell_voltage_17:
|
|
||||||
name: "${name} cell voltage 17"
|
|
||||||
cell_voltage_18:
|
|
||||||
name: "${name} cell voltage 18"
|
|
||||||
cell_voltage_19:
|
|
||||||
name: "${name} cell voltage 19"
|
|
||||||
cell_voltage_20:
|
|
||||||
name: "${name} cell voltage 20"
|
|
||||||
cell_voltage_21:
|
|
||||||
name: "${name} cell voltage 21"
|
|
||||||
cell_voltage_22:
|
|
||||||
name: "${name} cell voltage 22"
|
|
||||||
cell_voltage_23:
|
|
||||||
name: "${name} cell voltage 23"
|
|
||||||
cell_voltage_24:
|
|
||||||
name: "${name} cell voltage 24"
|
|
||||||
cell_voltage_25:
|
|
||||||
name: "${name} cell voltage 25"
|
|
||||||
cell_voltage_26:
|
|
||||||
name: "${name} cell voltage 26"
|
|
||||||
cell_voltage_27:
|
|
||||||
name: "${name} cell voltage 27"
|
|
||||||
cell_voltage_28:
|
|
||||||
name: "${name} cell voltage 28"
|
|
||||||
cell_voltage_29:
|
|
||||||
name: "${name} cell voltage 29"
|
|
||||||
cell_voltage_30:
|
|
||||||
name: "${name} cell voltage 30"
|
|
||||||
cell_voltage_31:
|
|
||||||
name: "${name} cell voltage 31"
|
|
||||||
cell_voltage_32:
|
|
||||||
name: "${name} cell voltage 32"
|
|
||||||
operation_status_bitmask:
|
|
||||||
name: "${name} operation status bitmask"
|
|
||||||
errors_bitmask:
|
|
||||||
name: "${name} errors bitmask"
|
|
||||||
balancer_status_bitmask:
|
|
||||||
name: "${name} balancer status bitmask"
|
|
||||||
software_version:
|
|
||||||
name: "${name} software version"
|
|
||||||
short_circuit_error_count:
|
|
||||||
name: "${name} short circuit error count"
|
|
||||||
charge_overcurrent_error_count:
|
|
||||||
name: "${name} charge overcurrent error count"
|
|
||||||
discharge_overcurrent_error_count:
|
|
||||||
name: "${name} discharge overcurrent error count"
|
|
||||||
cell_overvoltage_error_count:
|
|
||||||
name: "${name} cell overvoltage error count"
|
|
||||||
cell_undervoltage_error_count:
|
|
||||||
name: "${name} cell undervoltage error count"
|
|
||||||
charge_overtemperature_error_count:
|
|
||||||
name: "${name} charge overtemperature error count"
|
|
||||||
charge_undertemperature_error_count:
|
|
||||||
name: "${name} charge undertemperature error count"
|
|
||||||
discharge_overtemperature_error_count:
|
|
||||||
name: "${name} discharge overtemperature error count"
|
|
||||||
discharge_undertemperature_error_count:
|
|
||||||
name: "${name} discharge undertemperature error count"
|
|
||||||
battery_overvoltage_error_count:
|
|
||||||
name: "${name} battery overvoltage error count"
|
|
||||||
battery_undervoltage_error_count:
|
|
||||||
name: "${name} battery undervoltage error count"
|
|
||||||
|
|
||||||
text_sensor:
|
|
||||||
- platform: jbd_bms_ble
|
|
||||||
jbd_bms_ble_id: bms0
|
|
||||||
errors:
|
|
||||||
name: "${name} errors"
|
|
||||||
operation_status:
|
|
||||||
name: "${name} operation status"
|
|
||||||
device_model:
|
|
||||||
name: "${name} device model"
|
|
||||||
|
|
||||||
select:
|
|
||||||
- platform: jbd_bms_ble
|
|
||||||
jbd_bms_ble_id: bms0
|
|
||||||
read_eeprom_register:
|
|
||||||
name: "${name} read eeprom register"
|
|
||||||
id: read_eeprom_register0
|
|
||||||
optionsmap:
|
|
||||||
0xAA: "Error Counts"
|
|
||||||
|
|
||||||
switch:
|
|
||||||
- platform: ble_client
|
|
||||||
ble_client_id: client0
|
|
||||||
name: "${name} enable bluetooth connection"
|
|
||||||
|
|
||||||
- platform: jbd_bms_ble
|
|
||||||
jbd_bms_ble_id: bms0
|
|
||||||
charging:
|
|
||||||
name: "${name} charging"
|
|
||||||
discharging:
|
|
||||||
name: "${name} discharging"
|
|
||||||
|
|
||||||
# Uncomment this section if you want to update the error count sensors periodically
|
|
||||||
#
|
|
||||||
# interval:
|
|
||||||
# - interval: 30min
|
|
||||||
# then:
|
|
||||||
# - select.set:
|
|
||||||
# id: read_eeprom_register0
|
|
||||||
# option: "Error Counts"
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
# Migrations-Plan: Legacy GSheets -> Company Explorer (Robotics Edition v0.7.4)
|
|
||||||
|
|
||||||
**Kontext:** Neuanfang für die Branche **Robotik & Facility Management**.
|
|
||||||
**Ziel:** Ablösung von Google Sheets/CLI durch eine Web-App ("Company Explorer") mit SQLite-Backend.
|
|
||||||
|
|
||||||
## 1. Strategische Neuausrichtung
|
|
||||||
|
|
||||||
| Bereich | Alt (Legacy) | Neu (Robotics Edition) |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| **Daten-Basis** | Google Sheets | **SQLite** (Lokal, performant, filterbar). |
|
|
||||||
| **Ziel-Daten** | Allgemein / Kundenservice | **Quantifizierbares Potenzial** (z.B. 4500m² Fläche, 120 Betten). |
|
|
||||||
| **Branchen** | KI-Vorschlag (Freitext) | **Strict Mode:** Mapping auf definierte Notion-Liste (z.B. "Hotellerie", "Automotive"). |
|
|
||||||
| **Bewertung** | 0-100 Score (Vage) | **Data-Driven:** Rohwert (Scraper/Search) -> Standardisierung (Formel) -> Potenzial. |
|
|
||||||
| **Analytics** | Techniker-ML-Modell | **Deaktiviert**. Fokus auf harte Fakten. |
|
|
||||||
| **Operations** | D365 Sync (Broken) | **Excel-Import & Deduplizierung**. Fokus auf Matching externer Listen gegen Bestand. |
|
|
||||||
|
|
||||||
## 2. Architektur & Komponenten-Mapping
|
|
||||||
|
|
||||||
Das System wird in `company-explorer/` neu aufgebaut. Wir lösen Abhängigkeiten zur Root `helpers.py` auf.
|
|
||||||
|
|
||||||
### A. Core Backend (`backend/`)
|
|
||||||
|
|
||||||
| Komponente | Aufgabe & Neue Logik | Prio |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| **Database** | Ersetzt `GoogleSheetHandler`. Speichert Firmen & "Enrichment Blobs". | 1 |
|
|
||||||
| **Importer** | Ersetzt `SyncManager`. Importiert Excel-Dumps (CRM) und Event-Listen. | 1 |
|
|
||||||
| **Deduplicator** | Ersetzt `company_deduplicator.py`. **Kern-Feature:** Checkt Event-Listen gegen DB. Muss "intelligent" matchen (Name + Ort + Web). | 1 |
|
|
||||||
| **Scraper (Base)** | Extrahiert Text von Websites. Basis für alle Analysen. | 1 |
|
|
||||||
| **Classification Service** | **NEU (v0.7.0).** Zweistufige Logik: <br> 1. Strict Industry Classification. <br> 2. Metric Extraction Cascade (Web -> Wiki -> SerpAPI). | 1 |
|
|
||||||
| **Marketing Engine** | Ersetzt `generate_marketing_text.py`. Nutzt neue `marketing_wissen_robotics.yaml`. | 3 |
|
|
||||||
|
|
||||||
### B. Frontend (`frontend/`) - React
|
|
||||||
|
|
||||||
* **View 1: Der "Explorer":** DataGrid aller Firmen. Filterbar nach "Roboter-Potential" und Status.
|
|
||||||
* **View 2: Der "Inspector":** Detailansicht einer Firma. Zeigt gefundene Signale ("Hat SPA Bereich"). Manuelle Korrektur-Möglichkeit.
|
|
||||||
* **View 3: "List Matcher":** Upload einer Excel-Liste -> Anzeige von Duplikaten -> Button "Neue importieren".
|
|
||||||
* **View 4: "Settings":** Konfiguration von Branchen, Rollen und Robotik-Logik.
|
|
||||||
|
|
||||||
## 3. Umgang mit Shared Code (`helpers.py` & Co.)
|
|
||||||
|
|
||||||
Wir kapseln das neue Projekt vollständig ab ("Fork & Clean").
|
|
||||||
|
|
||||||
* **Quelle:** `helpers.py` (Root)
|
|
||||||
* **Ziel:** `company-explorer/backend/lib/core_utils.py`
|
|
||||||
* **Aktion:** Wir kopieren nur relevante Teile und ergänzen sie (z.B. `safe_eval_math`, `run_serp_search`).
|
|
||||||
|
|
||||||
## 4. Datenstruktur (SQLite Schema)
|
|
||||||
|
|
||||||
### Tabelle `companies` (Stammdaten & Analyse)
|
|
||||||
* `id` (PK)
|
|
||||||
* `name` (String)
|
|
||||||
* `website` (String)
|
|
||||||
* `crm_id` (String, nullable - Link zum D365)
|
|
||||||
* `industry_crm` (String - Die "erlaubte" Branche aus Notion)
|
|
||||||
* `city` (String)
|
|
||||||
* `country` (String - Standard: "DE" oder aus Impressum)
|
|
||||||
* `status` (Enum: NEW, IMPORTED, ENRICHED, QUALIFIED)
|
|
||||||
* **NEU (v0.7.0):**
|
|
||||||
* `calculated_metric_name` (String - z.B. "Anzahl Betten")
|
|
||||||
* `calculated_metric_value` (Float - z.B. 180)
|
|
||||||
* `calculated_metric_unit` (String - z.B. "Betten")
|
|
||||||
* `standardized_metric_value` (Float - z.B. 4500)
|
|
||||||
* `standardized_metric_unit` (String - z.B. "m²")
|
|
||||||
* `metric_source` (String - "website", "wikipedia", "serpapi")
|
|
||||||
|
|
||||||
### Tabelle `signals` (Deprecated)
|
|
||||||
* *Veraltet ab v0.7.0. Wird durch quantitative Metriken in `companies` ersetzt.*
|
|
||||||
|
|
||||||
### Tabelle `contacts` (Ansprechpartner)
|
|
||||||
* `id` (PK)
|
|
||||||
* `account_id` (FK -> companies.id)
|
|
||||||
* `gender`, `title`, `first_name`, `last_name`, `email`
|
|
||||||
* `job_title` (Visitenkarte)
|
|
||||||
* `role` (Standardisierte Rolle: "Operativer Entscheider", etc.)
|
|
||||||
* `status` (Marketing Status)
|
|
||||||
|
|
||||||
### Tabelle `industries` (Branchen-Fokus - Synced from Notion)
|
|
||||||
* `id` (PK)
|
|
||||||
* `notion_id` (String, Unique)
|
|
||||||
* `name` (String - "Vertical" in Notion)
|
|
||||||
* `description` (Text - "Definition" in Notion)
|
|
||||||
* `metric_type` (String - "Metric Type")
|
|
||||||
* `min_requirement` (Float - "Min. Requirement")
|
|
||||||
* `whale_threshold` (Float - "Whale Threshold")
|
|
||||||
* `proxy_factor` (Float - "Proxy Factor")
|
|
||||||
* `scraper_search_term` (String - "Scraper Search Term")
|
|
||||||
* `scraper_keywords` (Text - "Scraper Keywords")
|
|
||||||
* `standardization_logic` (String - "Standardization Logic")
|
|
||||||
|
|
||||||
### Tabelle `job_role_mappings` (Rollen-Logik)
|
|
||||||
* `id` (PK)
|
|
||||||
* `pattern` (String - Regex für Jobtitles)
|
|
||||||
* `role` (String - Zielrolle)
|
|
||||||
|
|
||||||
## 7. Historie & Fixes (Jan 2026)
|
|
||||||
|
|
||||||
* **[CRITICAL] v0.7.4: Service Restoration & Logic Fix (Jan 24, 2026)**
|
|
||||||
* **Summary:** Identified and resolved a critical issue where `ClassificationService` contained empty placeholder methods (`pass`), leading to "Others" classification and missing metrics.
|
|
||||||
* **Fixes Implemented:**
|
|
||||||
* **Service Restoration:** Completely re-implemented `classify_company_potential`, `_run_llm_classification_prompt`, and `_run_llm_metric_extraction_prompt` to restore AI functionality using `call_gemini_flash`.
|
|
||||||
* **Standardization Logic:** Connected the `standardization_logic` formula parser (e.g., "Values * 100m²") into the metric extraction cascade. It now correctly computes `standardized_metric_value` (e.g., 352 beds -> 35,200 m²).
|
|
||||||
* **Verification:** Confirmed end-to-end flow from "New Company" -> "Healthcare - Hospital" -> "352 Betten" -> "35.200 m²" via the UI "Play" button.
|
|
||||||
|
|
||||||
* **[STABILITY] v0.7.3: Hardening Metric Parser & Regression Testing (Jan 23, 2026)**
|
|
||||||
* **Summary:** A series of critical fixes were applied to the `MetricParser` to handle complex real-world scenarios, and a regression test suite was created to prevent future issues.
|
|
||||||
* **Specific Bug Fixes:**
|
|
||||||
* **Wolfra Bug ("802020"):** Logic to detect and remove trailing years from concatenated numbers (e.g., "Mitarbeiter: 802020" -> "80").
|
|
||||||
* **Erding Bug ("Year Prefix"):** Logic to ignore year-like prefixes appearing before the actual metric (e.g., "Seit 2022 ... 200.000 Besucher").
|
|
||||||
* **Greilmeier Bug ("Truncation"):** Removed aggressive sentence splitting on hyphens that was truncating text and causing the parser to miss numbers at the end of a phrase.
|
|
||||||
* **Expected Value Cleaning:** The parser now aggressively strips units (like "m²") from the LLM's `expected_value` to ensure it can find the correct numeric target even if the source text contains multiple numbers.
|
|
||||||
* **Regression Test Suite:** Created `/backend/tests/test_metric_parser.py` to lock in these fixes.
|
|
||||||
|
|
||||||
* **[STABILITY] v0.7.2: Robust Metric Parsing (Jan 23, 2026)**
|
|
||||||
* **Legacy Logic Restored:** Re-implemented the robust, regex-based number parsing logic (formerly in legacy helpers) as `MetricParser`.
|
|
||||||
* **German Formats:** Correctly handles "1.000" (thousands) vs "1,5" (decimal) and mixed formats.
|
|
||||||
* **Citation Cleaning:** Filters out Wikipedia citations like `[3]` and years in parentheses (e.g. "80 (2020)" -> 80).
|
|
||||||
* **Hybrid Extraction:** The ClassificationService now asks the LLM for the *text segment* and parses the number deterministically, fixing "LLM Hallucinations" (e.g. "1.005" -> 1).
|
|
||||||
|
|
||||||
* **[STABILITY] v0.7.1: AI Robustness & UI Fixes (Jan 21, 2026)**
|
|
||||||
* **SDK Stabilität:** Umstellung auf `gemini-2.0-flash` im Legacy-SDK zur Behebung von `404 Not Found` Fehlern.
|
|
||||||
* **API-Key Management:** Robustes Laden des Keys aus `/app/gemini_api_key.txt`.
|
|
||||||
* **Classification Prompt:** Schärfung auf "Best-Fit"-Entscheidungen (kein vorzeitiges "Others").
|
|
||||||
* **Scraping:** Wechsel auf `BeautifulSoup` nach Problemen mit `trafilatura`.
|
|
||||||
|
|
||||||
* **[MAJOR] v0.7.0: Quantitative Potential Analysis (Jan 20, 2026)**
|
|
||||||
* **Zweistufige Analyse:**
|
|
||||||
1. **Strict Classification:** Ordnet Firmen einer Notion-Branche zu (oder "Others").
|
|
||||||
2. **Metric Cascade:** Sucht gezielt nach der branchenspezifischen Metrik ("Scraper Search Term").
|
|
||||||
* **Fallback-Kaskade:** Website -> Wikipedia -> SerpAPI (Google Search).
|
|
||||||
* **Standardisierung:** Berechnet vergleichbare Werte (z.B. m²) aus Rohdaten mit der `Standardization Logic`.
|
|
||||||
* **Datenbank:** Erweiterung der `companies`-Tabelle um Metrik-Felder.
|
|
||||||
|
|
||||||
* **[UPGRADE] v0.6.x: Notion Integration & UI Improvements**
|
|
||||||
* **Notion SSoT:** Umstellung der Branchenverwaltung (`Industries`) auf Notion.
|
|
||||||
* **Sync Automation:** `backend/scripts/sync_notion_industries.py`.
|
|
||||||
* **Contacts Management:** Globale Kontaktliste, Bulk-Import, Marketing-Status.
|
|
||||||
* **UI Overhaul:** Light/Dark Mode, Grid View, Responsive Design.
|
|
||||||
|
|
||||||
## 8. Eingesetzte Prompts (Account-Analyse v0.7.4)
|
|
||||||
|
|
||||||
### 8.1 Strict Industry Classification
|
|
||||||
|
|
||||||
Ordnet das Unternehmen einer definierten Branche zu.
|
|
||||||
|
|
||||||
```python
|
|
||||||
prompt = f"""
|
|
||||||
Act as a strict B2B Industry Classifier.
|
|
||||||
Company: {company_name}
|
|
||||||
Context: {website_text[:3000]}
|
|
||||||
|
|
||||||
Available Industries:
|
|
||||||
{json.dumps(industry_definitions, indent=2)}
|
|
||||||
|
|
||||||
Task: Select the ONE industry that best matches the company.
|
|
||||||
If the company is a Hospital/Klinik, select 'Healthcare - Hospital'.
|
|
||||||
If none match well, select 'Others'.
|
|
||||||
|
|
||||||
Return ONLY the exact name of the industry.
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.2 Metric Extraction
|
|
||||||
|
|
||||||
Extrahiert den spezifischen Zahlenwert ("Scraper Search Term") und liefert JSON für den `MetricParser`.
|
|
||||||
|
|
||||||
```python
|
|
||||||
prompt = f"""
|
|
||||||
Extract the following metric for the company in industry '{industry_name}':
|
|
||||||
Target Metric: "{search_term}"
|
|
||||||
|
|
||||||
Source Text:
|
|
||||||
{text_content[:6000]}
|
|
||||||
|
|
||||||
Return a JSON object with:
|
|
||||||
- "raw_value": The number found (e.g. 352 or 352.0). If text says "352 Betten", extract 352. If not found, null.
|
|
||||||
- "raw_unit": The unit found (e.g. "Betten", "m²").
|
|
||||||
- "proof_text": A short quote from the text proving this value.
|
|
||||||
|
|
||||||
JSON ONLY.
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
## 9. Notion Integration (Single Source of Truth)
|
|
||||||
|
|
||||||
Das System nutzt Notion als zentrales Steuerungselement für strategische Definitionen.
|
|
||||||
|
|
||||||
### 9.1 Datenfluss
|
|
||||||
1. **Definition:** Branchen und Robotik-Kategorien werden in Notion gepflegt (Whale Thresholds, Keywords, Definitionen).
|
|
||||||
2. **Synchronisation:** Das Skript `sync_notion_industries.py` zieht die Daten via API und führt einen Upsert in die lokale SQLite-Datenbank aus.
|
|
||||||
3. **App-Nutzung:** Das Web-Interface zeigt diese Daten schreibgeschützt an. Der `ClassificationService` nutzt sie als "System-Anweisung" für das LLM.
|
|
||||||
|
|
||||||
### 9.2 Technische Details
|
|
||||||
* **Notion Token:** Muss in `/app/notion_token.txt` (Container-Pfad) hinterlegt sein.
|
|
||||||
* **DB-Mapping:** Die Zuordnung erfolgt primär über die `notion_id`, sekundär über den Namen, um Dubletten bei der Migration zu vermeiden.
|
|
||||||
|
|
||||||
## 10. Database Migration
|
|
||||||
|
|
||||||
Wenn die `industries`-Tabelle in einer bestehenden Datenbank aktualisiert werden muss (z.B. um neue Felder aus Notion zu unterstützen), darf die Datenbankdatei **nicht** gelöscht werden. Stattdessen muss das Migrations-Skript ausgeführt werden.
|
|
||||||
|
|
||||||
**Prozess:**
|
|
||||||
|
|
||||||
1. **Sicherstellen, dass die Zieldatenbank vorhanden ist:** Die `companies_v3_fixed_2.db` muss im `company-explorer`-Verzeichnis liegen (bzw. via Volume gemountet sein).
|
|
||||||
2. **Migration ausführen:** Dieser Befehl fügt die fehlenden Spalten hinzu, ohne Daten zu löschen.
|
|
||||||
```bash
|
|
||||||
docker exec -it company-explorer python3 backend/scripts/migrate_db.py
|
|
||||||
```
|
|
||||||
3. **Container neu starten:** Damit der Server das neue Schema erkennt.
|
|
||||||
```bash
|
|
||||||
docker-compose restart company-explorer
|
|
||||||
```
|
|
||||||
4. **Notion-Sync ausführen:** Um die neuen Spalten mit Daten zu befüllen.
|
|
||||||
```bash
|
|
||||||
docker exec -it company-explorer python3 backend/scripts/sync_notion_industries.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## 11. Lessons Learned (Retrospektive Jan 24, 2026)
|
|
||||||
|
|
||||||
1. **API-Routing-Reihenfolge (FastAPI):** Ein spezifischer Endpunkt (z.B. `/api/companies/export`) muss **vor** einem dynamischen Endpunkt (z.B. `/api/companies/{company_id}`) deklariert werden. Andernfalls interpretiert FastAPI "export" als eine `company_id`, was zu einem `422 Unprocessable Entity` Fehler führt.
|
|
||||||
2. **Nginx `proxy_pass` Trailing Slash:** Das Vorhandensein oder Fehlen eines `/` am Ende der `proxy_pass`-URL in Nginx ist kritisch. Für Dienste wie FastAPI, die mit einem `root_path` (z.B. `/ce`) laufen, darf **kein** Trailing Slash verwendet werden (`proxy_pass http://company-explorer:8000;`), damit der `root_path` in der an das Backend weitergeleiteten Anfrage erhalten bleibt.
|
|
||||||
3. **Docker-Datenbank-Persistenz:** Das Fehlen eines expliziten Volume-Mappings für die Datenbankdatei in `docker-compose.yml` führt dazu, dass der Container eine interne, ephemere Kopie der Datenbank verwendet. Alle Änderungen, die außerhalb des Containers an der "Host"-DB vorgenommen werden, sind für die Anwendung unsichtbar. Es ist zwingend erforderlich, ein Mapping wie `./companies_v3_fixed_2.db:/app/companies_v3_fixed_2.db` zu definieren.
|
|
||||||
4. **Code-Integrität & Platzhalter:** Es ist kritisch, bei Datei-Operationen sicherzustellen, dass keine Platzhalter (wie `pass` oder `# omitted`) in den produktiven Code gelangen. Eine "Zombie"-Datei, die äußerlich korrekt aussieht aber innerlich leer ist, kann schwer zu debuggende Logikfehler verursachen.
|
|
||||||
5. **Formel-Robustheit:** Formeln aus externen Quellen müssen vor der Auswertung bereinigt werden (Entfernung von Einheiten, Kommentaren), um Syntax-Fehler zu vermeiden.
|
|
||||||
|
|
||||||
## 12. Deployment & Access Notes
|
|
||||||
|
|
||||||
**Wichtiger Hinweis zum Deployment-Setup:**
|
|
||||||
|
|
||||||
Dieses Projekt läuft in einer Docker-Compose-Umgebung, typischerweise auf einer Synology Diskstation. Der Zugriff auf die einzelnen Microservices erfolgt über einen zentralen Nginx-Reverse-Proxy (`proxy`-Service), der auf Port `8090` des Host-Systems lauscht.
|
|
||||||
|
|
||||||
**Zugriffs-URLs für `company-explorer`:**
|
|
||||||
|
|
||||||
* **Intern (im Docker-Netzwerk):** `http://company-explorer:8000`
|
|
||||||
* **Extern (über Proxy):** `https://floke-ai.duckdns.org/ce/` (bzw. lokal `http://192.168.x.x:8090/ce/`)
|
|
||||||
|
|
||||||
**Datenbank-Persistenz:**
|
|
||||||
* Die SQLite-Datenbankdatei (`companies_v3_fixed_2.db`) muss mittels Docker-Volume-Mapping vom Host-Dateisystem in den `company-explorer`-Container gemountet werden (`./companies_v3_fixed_2.db:/app/companies_v3_fixed_2.db`). Dies stellt sicher, dass Datenänderungen persistent sind und nicht verloren gehen, wenn der Container neu gestartet oder neu erstellt wird.
|
|
||||||
378
RELOCATION.md
Normal file
378
RELOCATION.md
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
### **Anforderungsdokument (Version 3): Docker-Migration nach Ubuntu VM (`docker1`)**
|
||||||
|
|
||||||
|
**Zweck:** Verbindliche Netzwerk-Anforderungen und schrittweiser Migrationsplan für den Umzug des GTM-Engine Stacks (ca. 20 Container). Basierend auf den Stabilisierungs-Erkenntnissen vom 07.03.2026.
|
||||||
|
|
||||||
|
#### **Teil 1: Externe Port-Freigaben (Firewall)**
|
||||||
|
|
||||||
|
Diese Ports müssen auf der Firewall für den eingehenden Verkehr zur VM `10.10.81.2` geöffnet werden.
|
||||||
|
|
||||||
|
| Host-Port | Ziel-Dienst | Zugriff | Zweck |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **8090** | `gateway_proxy` | Intranet | **Zentraler Zugang.** Alle Web-Apps (Dashboard, CE, Lead). |
|
||||||
|
| **3000** | `gitea` | Intranet | Gitea Web UI. |
|
||||||
|
| **2222** | `gitea` | Intranet | Gitea Git via SSH. |
|
||||||
|
| **8003** | `connector-so` | **Public** | SuperOffice Webhook-Empfänger (SSL erforderlich!). |
|
||||||
|
| **5678** | `n8n` | **Public** | Automation Workhooks. |
|
||||||
|
| **8094** | `gtm-architect`| Intranet | GTM Architect Direct. |
|
||||||
|
| **8092** | `b2b-marketing`| Intranet | B2B Marketing Assistant Direct. |
|
||||||
|
| **8001** | `transcription`| Intranet | Transcription Tool Direct (via 8090). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Port-Mapping (docker-compose.yml)**
|
||||||
|
Die folgende Tabelle listet alle Host-Ports auf, die durch die `docker-compose.yml` gebunden werden. Diese sind die Ports, die auf `docker1` lauschen werden.
|
||||||
|
|
||||||
|
| Host-Port | Ziel-Dienst | Zweck / Notiz |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **8090** | `nginx` (Gateway) | Zentraler Zugang (Intranet) |
|
||||||
|
| **8000** | `company-explorer` | API (intern) |
|
||||||
|
| **8003** | `connector-superoffice`| Webhook (Public) |
|
||||||
|
| **8501** | `lead-engine` | UI (intern) |
|
||||||
|
| **8004** | `lead-engine` | API (intern) |
|
||||||
|
| **8092** | `b2b-marketing-assistant`| UI/API (Intranet) |
|
||||||
|
| **8093** | `content-engine` | UI/API (Intranet) |
|
||||||
|
| **8094** | `gtm-architect` | UI/API (Intranet) |
|
||||||
|
| **8096** | `heatmap-frontend` | UI (Intranet) |
|
||||||
|
| **8002** | `heatmap-backend` | API (intern) |
|
||||||
|
| **8097** | `competitor-analysis` | UI/API (Intranet) |
|
||||||
|
| **8098** | `market-intelligence` | UI/API (Intranet) |
|
||||||
|
| **8001** | `transcription-tool` | UI/API (Intranet) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### **Teil 2: Netzwerk-Architektur (Intern)**
|
||||||
|
|
||||||
|
* **DNS Resolver:** In Nginx konfiguriert (`resolver 127.0.0.11`).
|
||||||
|
* **WebSockets:** Das Gateway unterstützt `Upgrade`-Header (kritisch für Streamlit/Lead-Engine).
|
||||||
|
* **Echo-Prävention:** Der Connector (`worker.py`) identifiziert sich dynamisch. Keine manuellen ID-Einträge in `.env` nötig, solange `SO_CLIENT_ID` passt.
|
||||||
|
* **Routing:**
|
||||||
|
* `/ce/` -> `company-explorer:8000`
|
||||||
|
* `/lead/` -> `lead-engine:8501` (UI)
|
||||||
|
* `/feedback/` -> `lead-engine:8004` (API)
|
||||||
|
* `/gtm/` -> `gtm-architect:3005` (API/Frontend)
|
||||||
|
* `/b2b/` -> `b2b-marketing-assistant:3002` (API/Frontend)
|
||||||
|
* `/content/` -> `content-engine:3000` (API/Frontend)
|
||||||
|
* `/market/` -> `market-intelligence:3001` (API/Frontend)
|
||||||
|
* `/competitor/` -> `competitor-analysis:3000` (API/Frontend)
|
||||||
|
* `/heatmap/` -> `heatmap-frontend:80` (Static/Proxy)
|
||||||
|
* `/tr/` -> `transcription-tool:8001` (API/Frontend) -> **Achtung:** Benötigt expliziten `rewrite` in Nginx!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# ⚠️ Kritische Lehren (Update 08.03.2026)
|
||||||
|
|
||||||
|
Der Umzug muss zwingend diese Punkte beachten, um den "Totalausfall" zu vermeiden:
|
||||||
|
|
||||||
|
### 1. Datenbank-Schema & Volumes
|
||||||
|
**Regel:** Datenbanken werden **NIEMALS** mehr direkt auf einen Host-Pfad gemountet.
|
||||||
|
**Grund:** Permission-Errors und SQLite-Locks auf Netzwerk-Dateisystemen.
|
||||||
|
**Vorgehen:** Nutzung von benannten Volumes (`explorer_db_data`, `connector_db_data`, `lead_engine_data`, `gtm_architect_data`, `b2b_marketing_data`, `content_engine_data`, `market_intel_data`, `competitor_analysis_data`, `transcription_uploads`).
|
||||||
|
|
||||||
|
### 2. Lead Engine: Kalender-Logik (v1.4)
|
||||||
|
* **Raster:** Das System bietet Termine nur im **15-Minuten-Takt** (:00, :15, :30, :45) an.
|
||||||
|
* **Abstand:** Zwischen zwei Terminvorschlägen liegen mind. **3 Stunden** Pause.
|
||||||
|
* **AppOnly Workaround:** Termin wird im Kalender von `info@robo-planet.de` erstellt und der Mitarbeiter (`e.melcer@`) als Teilnehmer hinzugefügt.
|
||||||
|
|
||||||
|
### 3. GTM Architect & B2B Assistant: Standalone-Betrieb
|
||||||
|
* **Architektur:** Beide Apps nutzen das "Self-Contained Image" Muster. Code, Frontend-Builds (`dist/`) und `node_modules` sind fest im Image verbaut.
|
||||||
|
* **GTM Port:** 3005 intern.
|
||||||
|
* **B2B Port:** 3002 intern.
|
||||||
|
* **DB-Abhängigkeit:** Der B2B Assistant benötigt zwingend die Datei `market_db_manager.py` (wird beim Build aus dem Root kopiert).
|
||||||
|
|
||||||
|
### 4. Transcription Tool: FFmpeg & Routing
|
||||||
|
* **FFmpeg:** Muss im Image vorhanden sein (Build dauert ca. 15 Min auf Synology).
|
||||||
|
* **Pfade:** Das Tool benötigt eine `tsconfig.json` im `frontend/` Ordner für den TypeScript-Build.
|
||||||
|
* **Nginx:** Der Pfad `/tr/` muss explizit umgeschrieben werden: `rewrite ^/tr/(.*) /$1 break;`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📂 Docker Volume Migration (Der "Plug & Play" Weg)
|
||||||
|
|
||||||
|
Um die Daten (Companies, Leads, Projekte, Audio-Files) ohne Verluste umzuziehen, müssen die benannten Volumes gesichert werden.
|
||||||
|
|
||||||
|
**Auf der Synology (Quelle):**
|
||||||
|
```bash
|
||||||
|
# Backup aller kritischen Volumes in ein Archiv
|
||||||
|
docker run --rm -v explorer_db_data:/data -v $(pwd):/backup alpine tar czf /backup/explorer_data.tar.gz -C /data .
|
||||||
|
docker run --rm -v lead_engine_data:/data -v $(pwd):/backup alpine tar czf /backup/lead_data.tar.gz -C /data .
|
||||||
|
docker run --rm -v gtm_architect_data:/data -v $(pwd):/backup alpine tar czf /backup/gtm_data.tar.gz -C /data .
|
||||||
|
docker run --rm -v b2b_marketing_data:/data -v $(pwd):/backup alpine tar czf /backup/b2b_data.tar.gz -C /data .
|
||||||
|
docker run --rm -v content_engine_data:/data -v $(pwd):/backup alpine tar czf /backup/content_data.tar.gz -C /data .
|
||||||
|
docker run --rm -v market_intel_data:/data -v $(pwd):/backup alpine tar czf /backup/market_data.tar.gz -C /data .
|
||||||
|
docker run --rm -v competitor_analysis_data:/data -v $(pwd):/backup alpine tar czf /backup/competitor_data.tar.gz -C /data .
|
||||||
|
docker run --rm -v transcription_uploads:/data -v $(pwd):/backup alpine tar czf /backup/tr_uploads.tar.gz -C /data .
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Auf der Ubuntu VM (Ziel):**
|
||||||
|
1. Volumes anlegen: `docker volume create explorer_db_data` (etc.)
|
||||||
|
2. Daten wiederherstellen:
|
||||||
|
```bash
|
||||||
|
docker run --rm -v explorer_db_data:/data -v $(pwd):/backup alpine sh -c "cd /data && tar xzf /backup/explorer_data.tar.gz"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Verbindlicher Migrationsplan**
|
||||||
|
|
||||||
|
**Phase 1: Vorbereitung**
|
||||||
|
1. [ ] **`git push`** auf Synology (aktuellster Stand inkl. GTM-Integration).
|
||||||
|
2. [ ] **`.env` Datei sichern** (Vollständigkeit prüfen!).
|
||||||
|
3. [ ] **Volumes sichern** (siehe oben: `tar.gz` Erstellung).
|
||||||
|
|
||||||
|
**Phase 2: Deployment auf `docker1`**
|
||||||
|
1. [ ] Repo klonen: `git clone ... /opt/gtm-engine`
|
||||||
|
2. [ ] `.env` kopieren.
|
||||||
|
3. [ ] **Volumes restoren** (BEVOR `docker compose up` ausgeführt wird).
|
||||||
|
4. [ ] Starten: `docker compose up -d --build`
|
||||||
|
5. [ ] **Schema-Check:** `docker exec -it company-explorer python /app/fix_missing_columns.py`
|
||||||
|
|
||||||
|
**Phase 3: Verifizierung**
|
||||||
|
1. [ ] Check Kalender-Lesen: `docker exec lead-engine python /app/trading_twins/test_calendar_logic.py`
|
||||||
|
2. [ ] Check GTM Architect: `https://10.10.81.2:8090/gtm/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Anhang A: Post-Deployment Port Check**
|
||||||
|
Führen Sie dieses Skript auf einem Client-Rechner aus (nicht auf `docker1` selbst), um die Erreichbarkeit der freigegebenen Ports zu testen. Ersetzen Sie `TARGET_IP` durch `10.10.81.2`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
TARGET_IP="10.10.81.2"
|
||||||
|
PORTS_INTRANET=(8090 3000 2222 8092 8093 8094 8096 8097 8098 8001)
|
||||||
|
PORTS_PUBLIC=(8003 5678)
|
||||||
|
|
||||||
|
echo "--- Testing Intranet Ports on $TARGET_IP ---"
|
||||||
|
for port in "${PORTS_INTRANET[@]}"; do
|
||||||
|
if nc -z -w 2 $TARGET_IP $port; then
|
||||||
|
echo "✅ Port $port is OPEN"
|
||||||
|
else
|
||||||
|
echo "❌ Port $port is CLOSED"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
echo -e "\n--- Testing Public Ports on $TARGET_IP (von extern prüfen!) ---"
|
||||||
|
|
||||||
|
for port in "${PORTS_PUBLIC[@]}"; do
|
||||||
|
|
||||||
|
echo " - Bitte prüfen Sie Port $port manuell von außerhalb des Netzwerks."
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### **Teil 4: Entwicklungs- vs. Produktions-Workflow**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Dieses Kapitel definiert die verbindlichen Regeln für die Weiterentwicklung der GTM-Engine nach der Migration, um maximale Stabilität und Sicherheit des Produktivsystems zu gewährleisten.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### **1. Grundprinzipien: Getrennte Welten**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* **Regel 1: Keine Entwicklung in der Produktion.** Auf der Wackler-VM (`docker1`) wird niemals Code direkt bearbeitet, experimentiert oder getestet. Sie ist ausschließlich für den Betrieb der stabilen Softwareversion vorgesehen.
|
||||||
|
|
||||||
|
* **Regel 2: Getrennte Konfiguration.** Jede Umgebung hat ihre eigene Konfigurationsdatei.
|
||||||
|
|
||||||
|
* **Produktion (Wackler):** `.env` – Enthält alle echten API-Schlüssel und Endpunkte.
|
||||||
|
|
||||||
|
* **Entwicklung (Synology):** `.env.dev` (oder eine Kopie der `.env`) – Verwendet Test-Schlüssel, Sandbox-Endpunkte und Dummy-Werte.
|
||||||
|
|
||||||
|
* **Regel 3: Die Produktionsdatenbank ist heilig.** Die produktiven Docker-Volumes (`explorer_db_data` etc.) werden niemals für Entwicklungszwecke angetastet. Lokale Entwicklung findet gegen eine separate, lokale Test-Datenbank statt.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### **2. Umgang mit Live-Daten & Aktionen**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**a) SuperOffice Webhook**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* **Problem:** Es darf nur einen aktiven Webhook geben, um Dateninkonsistenzen zu vermeiden.
|
||||||
|
|
||||||
|
* **Lösung:**
|
||||||
|
|
||||||
|
1. **Produktion:** Der Webhook (`https://<neue-wackler-domain>/connector/webhook`) wird bei SuperOffice registriert. In der `.env` ist das `WEBHOOK_SECRET_TOKEN` gesetzt.
|
||||||
|
|
||||||
|
2. **Entwicklung:** In der `.env.dev` wird das `WEBHOOK_SECRET_TOKEN` auskommentiert (`#WEBHOOK_SECRET_TOKEN=...`). Der Connector ist somit "taub" für externe Anrufe.
|
||||||
|
|
||||||
|
3. **Testen in der Entwicklung:** Um realistische Tests durchzuführen, wird eine Umgebungsvariable `LOG_WEBHOOKS=true` im **produktiv-Connector** gesetzt. Dieser schreibt dann den Body jedes eingehenden Webhooks in die Docker-Logs. Diese JSON-Payloads können kopiert werden, um mit einem Skript (`curl`) gezielte, manuelle Tests gegen die lokale Entwicklungsumgebung zu fahren.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**b) E-Mail-Versand (Lead Engine & Co.)**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* **Problem:** Das Entwicklungssystem darf unter keinen Umständen E-Mails an echte Kunden oder Kontakte senden.
|
||||||
|
|
||||||
|
* **Lösung: Der "Safety Override"**
|
||||||
|
|
||||||
|
1. **Produktion:** Die echten MS Graph API Credentials (`INFO_...`) sind in der `.env` gesetzt.
|
||||||
|
|
||||||
|
2. **Entwicklung:** Die MS Graph Credentials in der `.env.dev` sind auskommentiert oder mit Dummy-Werten belegt.
|
||||||
|
|
||||||
|
3. **Sicherer Test-Modus:** Eine neue Umgebungsvariable `DEV_MODE_EMAIL_RECIPIENT` wird implementiert.
|
||||||
|
|
||||||
|
* Wenn diese Variable in der `.env.dev` gesetzt ist (z.B. `DEV_MODE_EMAIL_RECIPIENT=ihre.test@adresse.de`), leitet der Code **alle** ausgehenden E-Mails an diese eine Adresse um.
|
||||||
|
|
||||||
|
* **Test-Prozess:** Für einen E-Mail-Test werden in der `.env.dev` temporär die echten MS Graph Credentials eingetragen, `DEV_MODE_EMAIL_RECIPIENT` gesetzt, der Test durchgeführt und danach werden die Credentials **sofort wieder entfernt/auskommentiert**.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### **3. Deployment-Prozess: Von der Entwicklung zur Produktion**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Der Prozess, um neuen, getesteten Code sicher auf das Produktivsystem zu bringen, ist wie folgt:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
1. **Entwicklung (Synology):**
|
||||||
|
|
||||||
|
* Ein neues Feature wird entwickelt und lokal gegen die Test-Datenbank getestet.
|
||||||
|
|
||||||
|
* Änderungen werden committet und in das **lokale Gitea auf der Synology** gepusht (`git push`).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2. **Deployment (Wackler VM):**
|
||||||
|
|
||||||
|
* Verbindung zur Wackler-VM via SSH.
|
||||||
|
|
||||||
|
* Ins Projektverzeichnis navigieren (`/opt/gtm-engine`).
|
||||||
|
|
||||||
|
* Den neuesten stabilen Code vom Entwicklungs-Gitea holen: `git pull synology main`.
|
||||||
|
|
||||||
|
* Die Docker-Container mit dem neuen Code neu bauen und starten: `docker compose up -d --build`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. **Netzwerkanforderung für Deployment:**
|
||||||
|
|
||||||
|
* Für den `git pull`-Befehl muss die Wackler-VM (`10.10.81.2`) auf den **Gitea-Port (TCP 3000) der Synology-Diskstation** zugreifen können. Dies muss einmalig im Netzwerk konfiguriert werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Teil 5: Alternative - Single-Host-Setup (Dev & Prod auf einer VM)**
|
||||||
|
|
||||||
|
Dieses Szenario beschreibt den wahrscheinlicheren Fall, dass die gesamte Arbeit (Entwicklung und Produktion) auf der von der IT bereitgestellten VM (`docker1`) stattfinden muss. Der Ansatz wandelt sich von "physischer Trennung" zu **"logischer Trennung mit starker Isolation"**.
|
||||||
|
|
||||||
|
#### **1. Grundkonzept: Schalldichte Räume**
|
||||||
|
|
||||||
|
Auf der VM werden zwei komplett isolierte Umgebungen parallel betrieben. Docker ist für dieses Szenario optimiert und gewährleistet die Trennung von Code, Konfiguration, Ports und vor allem Daten.
|
||||||
|
|
||||||
|
* **Produktionsumgebung (`prod`):** Läuft stabil mit dem geprüften Code und greift auf die produktiven Daten zu.
|
||||||
|
* **Entwicklungsumgebung (`dev`):** Dient zum Entwickeln und Testen. Hat eine eigene Konfiguration und eine komplett separate Test-Datenbank.
|
||||||
|
|
||||||
|
#### **2. Implementierung**
|
||||||
|
|
||||||
|
**a) Verzeichnisstruktur**
|
||||||
|
|
||||||
|
Eine saubere Ordnerstruktur ist die Basis für die Trennung.
|
||||||
|
|
||||||
|
```
|
||||||
|
/opt/gtm-engine/
|
||||||
|
├── dev/
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ ├── .env
|
||||||
|
│ └── (kompletter Code des Projekts...)
|
||||||
|
│
|
||||||
|
└── prod/
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── .env
|
||||||
|
└── (kompletter Code des Projekts...)
|
||||||
|
```
|
||||||
|
|
||||||
|
**b) Auflösung von Port-Konflikten**
|
||||||
|
|
||||||
|
Zwei Stacks können nicht dieselben Ports nutzen. Die `dev`-Umgebung nutzt daher verschobene Ports.
|
||||||
|
|
||||||
|
| Dienst | Produktions-Port (in `prod/docker-compose.yml`) | Entwicklungs-Port (in `dev/docker-compose.yml`) |
|
||||||
|
| :--- | :---: | :---: |
|
||||||
|
| Gateway | `8090:80` | `9090:80` |
|
||||||
|
| Webhook | `8003:8003` | `9003:8003` |
|
||||||
|
| GTM | `8094:3005` | `9094:3005` |
|
||||||
|
|
||||||
|
So ist das **Produktivsystem** unter `http://10.10.81.2:8090` und das **Entwicklungssystem** unter `http://10.10.81.2:9090` erreichbar.
|
||||||
|
|
||||||
|
**c) Garantierte Datenbank-Isolation**
|
||||||
|
|
||||||
|
**Dies ist der wichtigste Punkt:** Docker stellt sicher, dass die Datenbanken niemals vermischt werden können. Docker Compose erstellt benannte Volumes mit dem Verzeichnisnamen als Präfix.
|
||||||
|
|
||||||
|
* `docker compose up` im Ordner `prod/` erzeugt das Volume `prod_explorer_db_data`.
|
||||||
|
* `docker compose up` im Ordner `dev/` erzeugt das Volume `dev_explorer_db_data`.
|
||||||
|
|
||||||
|
Diese beiden Volumes sind **vollständig voneinander isolierte Container** auf der Festplatte. Ein Zugriff von `dev` auf `prod`-Daten ist technisch unmöglich. **Ihre Anforderung, dass die Produktionsdatenbank sicher ist, wird hiermit zu 100% erfüllt.**
|
||||||
|
|
||||||
|
#### **3. Vereinfachter Workflow auf einer Maschine**
|
||||||
|
|
||||||
|
Das neue Gitea (auf Port 3000) wird zur zentralen "Source of Truth".
|
||||||
|
|
||||||
|
1. **Entwicklung:**
|
||||||
|
* Sie arbeiten im Verzeichnis `/opt/gtm-engine/dev/`.
|
||||||
|
* Sie ändern den Code und testen ihn, indem Sie den `dev`-Stack starten:
|
||||||
|
`cd /opt/gtm-engine/dev/ && docker compose up -d --build`
|
||||||
|
* Sie verifizieren die Änderungen im Browser unter `http://10.10.81.2:9090`.
|
||||||
|
* Wenn alles passt, committen Sie und pushen zum Gitea auf derselben Maschine: `git push`.
|
||||||
|
|
||||||
|
2. **Deployment (Release in die Produktion):**
|
||||||
|
* Wechseln Sie in das Produktionsverzeichnis: `cd /opt/gtm-engine/prod/`.
|
||||||
|
* Holen Sie den neuen, im `dev`-Zweig getesteten Code aus Gitea: `git pull`.
|
||||||
|
* Aktualisieren Sie den Produktions-Stack: `docker compose up -d --build`.
|
||||||
|
|
||||||
|
Dieser Prozess ist sicher, schnell und wiederholbar, da er nur aus Standard-Git- und Docker-Befehlen besteht.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **UMZUG Live - Lessons Learned (März 2026)**
|
||||||
|
|
||||||
|
Der reale Umzug von der Synology auf die Wackler-VM (`docker1`) hat gezeigt, dass die Theorie gut war, die Praxis aber spezifische Hürden bereithielt. Diese Lektionen sind kritisch für alle zukünftigen Deployments:
|
||||||
|
|
||||||
|
#### **1. Die Git-Historien-Falle (Packfile / Timeout Error)**
|
||||||
|
* **Problem:** Versuche, das Repository per `git clone` oder über die Gitea-Migrations-Funktion umzuziehen, brachen reproduzierbar mit `RPC failed; HTTP 504` oder `fatal: expected 'packfile'` ab.
|
||||||
|
* **Ursache:** Das lokale `.git` Verzeichnis war auf **1,4 Gigabyte** angewachsen, da in der Vergangenheit versehentlich große MP3-Dateien committet wurden. Auch nach dem Löschen verbleiben diese in der Historie und blockieren den Transfer über Proxies.
|
||||||
|
* **Lösung:** Radikale Bereinigung der Historie via `git filter-branch` (oder `git filter-repo`) für den Ordner `uploads_audio/`, gefolgt von einer aggressiven Garbage Collection (`git gc --prune=now --aggressive`).
|
||||||
|
* **Ergebnis:** Schrumpfung des Repos von 1,4 GB auf **130 MB**.
|
||||||
|
* **Lesson:** Bei Git-Transfer-Fehlern zuerst die Größe des `.git` Ordners prüfen.
|
||||||
|
|
||||||
|
#### **2. Der "Filter-Branch" Nebeneffekt (.env Schutz)**
|
||||||
|
* **Problem:** Nach der Git-Bereinigung schienen ungetrackte Dateien (wie `.env` und `volume_backups/`) plötzlich verschwunden.
|
||||||
|
* **Ursache:** `git filter-branch` erzwingt einen harten Reset auf den bereinigten Stand. Dateien, die in der `.gitignore` stehen, werden dabei oft aus dem Arbeitsverzeichnis entfernt.
|
||||||
|
* **Lösung:** Wiederherstellung aus einem zuvor erstellten `.tar.gz`-Backup des gesamten Projektverzeichnisses.
|
||||||
|
* **Lesson:** Führe niemals tiefe Git-Eingriffe ohne ein physisches Dateibackup der ungetrackten Dateien durch.
|
||||||
|
|
||||||
|
#### **3. Das "Transit-Repo" Muster für Secrets**
|
||||||
|
* **Problem:** Sicherer Transfer der `.env` und Datenbanken ohne Einchecken in das Haupt-Repo und ohne Zugriff auf externe Transfer-Dienste (Firewall-Blockade).
|
||||||
|
* **Lösung:** Erstellung eines temporären, privaten Gitea-Repositorys namens `transit` auf dem Ziel-System. Hochladen des verschlüsselten Backups über die Gitea-Weboberfläche und anschließendes Klonen/Entpacken auf der VM. Danach sofortige Löschung des `transit` Repos.
|
||||||
|
* **Lesson:** Gitea ist das beste Transit-Medium für große Files hinter restriktiven Firewalls.
|
||||||
|
|
||||||
|
#### **4. Docker Build Context & Berechtigungen**
|
||||||
|
* **Problem:** `docker compose up --build` brach ab mit: `failed to solve: error from sender: open /volume_backups: permission denied`.
|
||||||
|
* **Ursache:** Docker versucht beim Bauen das gesamte Verzeichnis einzulesen. Ordner, die dem `root` User gehören (z.B. durch Gemini-Aktionen erstellt), blockieren den Prozess.
|
||||||
|
* **Lösung:**
|
||||||
|
1. Berechtigungen korrigieren: `sudo chown -R $USER:$USER volume_backups/`
|
||||||
|
2. Ordner in der `.dockerignore` ausschließen, damit er gar nicht erst eingelesen wird.
|
||||||
|
* **Lesson:** Alles, was nicht in ein Container-Image gehört, **muss** in die `.dockerignore`.
|
||||||
|
|
||||||
|
#### **5. Vite & TypeScript Build-Tücken**
|
||||||
|
* **Problem:** Der Frontend-Build schlug fehl, weil die `tsconfig.json` im Container nicht gefunden wurde.
|
||||||
|
* **Ursache:** `COPY . .` verhält sich in komplexen Verzeichnisstrukturen manchmal unvorhersehbar.
|
||||||
|
* **Lösung:** Explizites Kopieren aller Konfigurationsdateien im Dockerfile: `COPY tsconfig.json tsconfig.node.json tsconfig.app.json vite.config.ts ./` direkt vor dem Build-Schritt.
|
||||||
|
* **Lesson:** Verlasse dich beim Build nicht auf Wildcards; kopiere essenzielle Config-Files explizit.
|
||||||
|
|
||||||
|
|
||||||
@@ -57,6 +57,8 @@ const App: React.FC = () => {
|
|||||||
const [generationStep, setGenerationStep] = useState<number>(0); // 0: idle, 1-6: step X is complete
|
const [generationStep, setGenerationStep] = useState<number>(0); // 0: idle, 1-6: step X is complete
|
||||||
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
|
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
|
||||||
const [batchStatus, setBatchStatus] = useState<{ current: number; total: number; industry: string } | null>(null);
|
const [batchStatus, setBatchStatus] = useState<{ current: number; total: number; industry: string } | null>(null);
|
||||||
|
const [isEnriching, setIsEnriching] = useState<boolean>(false);
|
||||||
|
|
||||||
|
|
||||||
// Project Persistence
|
// Project Persistence
|
||||||
const [projectId, setProjectId] = useState<string | null>(null);
|
const [projectId, setProjectId] = useState<string | null>(null);
|
||||||
@@ -69,6 +71,43 @@ const App: React.FC = () => {
|
|||||||
const STEP_TITLES = t.stepTitles;
|
const STEP_TITLES = t.stepTitles;
|
||||||
const STEP_KEYS: (keyof AnalysisData)[] = ['offer', 'targetGroups', 'personas', 'painPoints', 'gains', 'messages', 'customerJourney'];
|
const STEP_KEYS: (keyof AnalysisData)[] = ['offer', 'targetGroups', 'personas', 'painPoints', 'gains', 'messages', 'customerJourney'];
|
||||||
|
|
||||||
|
const handleEnrichRow = async (productName: string, productUrl?: string) => {
|
||||||
|
setIsEnriching(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/enrich-product`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
productName,
|
||||||
|
productUrl,
|
||||||
|
language: inputData.language
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.details || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const newRow = await response.json();
|
||||||
|
setAnalysisData(prev => {
|
||||||
|
const currentOffer = prev.offer || { headers: [], rows: [], summary: [] };
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
offer: {
|
||||||
|
...currentOffer,
|
||||||
|
rows: [...currentOffer.rows, newRow]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setError(e instanceof Error ? `Fehler beim Anreichern: ${e.message}` : 'Unbekannter Fehler beim Anreichern.');
|
||||||
|
} finally {
|
||||||
|
setIsEnriching(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// --- AUTO-SAVE EFFECT ---
|
// --- AUTO-SAVE EFFECT ---
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (generationStep === 0 || !inputData.companyUrl) return;
|
if (generationStep === 0 || !inputData.companyUrl) return;
|
||||||
@@ -507,9 +546,10 @@ const App: React.FC = () => {
|
|||||||
const canAdd = ['offer', 'targetGroups'].includes(stepKey);
|
const canAdd = ['offer', 'targetGroups'].includes(stepKey);
|
||||||
const canDelete = ['offer', 'targetGroups', 'personas'].includes(stepKey);
|
const canDelete = ['offer', 'targetGroups', 'personas'].includes(stepKey);
|
||||||
|
|
||||||
const handleManualAdd = (newRow: string[]) => {
|
const handleManualAdd = () => {
|
||||||
|
const newEmptyRow = Array(step.headers.length).fill('');
|
||||||
const currentRows = step.rows || [];
|
const currentRows = step.rows || [];
|
||||||
handleDataChange(stepKey, { ...step, rows: [...currentRows, newRow] });
|
handleDataChange(stepKey, { ...step, rows: [...currentRows, newEmptyRow] });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -521,8 +561,8 @@ const App: React.FC = () => {
|
|||||||
rows={step.rows}
|
rows={step.rows}
|
||||||
onDataChange={(newRows) => handleDataChange(stepKey, { ...step, rows: newRows })}
|
onDataChange={(newRows) => handleDataChange(stepKey, { ...step, rows: newRows })}
|
||||||
canAddRows={canAdd}
|
canAddRows={canAdd}
|
||||||
onEnrichRow={canAdd ? handleManualAdd : undefined}
|
onEnrichRow={stepKey === 'offer' ? handleEnrichRow : handleManualAdd}
|
||||||
isEnriching={false}
|
isEnriching={isEnriching}
|
||||||
canDeleteRows={canDelete}
|
canDeleteRows={canDelete}
|
||||||
onRestart={() => handleStepRestart(stepKey)}
|
onRestart={() => handleStepRestart(stepKey)}
|
||||||
t={t}
|
t={t}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user