Stabilize Lead Engine calendar logic (v1.4) and integrate GTM Architect, B2B Assistant, and Transcription Tool into Docker stack [30388f42]
This commit is contained in:
@@ -1,49 +1,38 @@
|
||||
# Lead Engine: Multi-Source Automation v1.3 [31988f42]
|
||||
# Lead Engine: Multi-Source Automation v1.4 [31988f42]
|
||||
|
||||
## 🚀 Übersicht
|
||||
Die **Lead Engine** ist ein spezialisiertes Modul zur autonomen Verarbeitung von B2B-Anfragen aus verschiedenen Quellen. Sie fungiert als Brücke zwischen dem E-Mail-Postfach und dem **Company Explorer**, um innerhalb von Minuten hochgradig personalisierte Antwort-Entwürfe auf "Human Expert Level" zu generieren.
|
||||
Die **Lead Engine** ist ein spezialisiertes Modul zur autonomen Verarbeitung von B2B-Anfragen. Sie fungiert als Brücke zwischen dem E-Mail-Postfach und dem **Company Explorer**, um innerhalb von Minuten hochgradig personalisierte Antwort-Entwürfe auf "Human Expert Level" zu generieren.
|
||||
|
||||
## 🛠 Hauptfunktionen
|
||||
|
||||
### 1. Intelligenter E-Mail Ingest
|
||||
* **Multi-Source:** Überwacht das Postfach `info@robo-planet.de` via **Microsoft Graph API** auf verschiedene Lead-Typen.
|
||||
* **Filter & Routing:** Erkennt und unterscheidet Anfragen von **TradingTwins** und dem **Roboplanet-Kontaktformular**.
|
||||
* **Parsing:** Spezialisierte HTML-Parser extrahieren für jede Quelle strukturierte Daten (Firma, Kontakt, Bedarf, etc.).
|
||||
* **Multi-Source:** Überwacht das Postfach `info@robo-planet.de` via **Microsoft Graph API**.
|
||||
* **Filter & Routing:** Unterscheidet Anfragen von **TradingTwins** und dem **Kontaktformular**.
|
||||
* **Parsing:** Spezialisierte HTML-Parser extrahieren strukturierte Daten (Firma, Kontakt, Bedarf).
|
||||
|
||||
### 2. Contact Research (LinkedIn Lookup)
|
||||
* **Automatisierung:** Sucht via **SerpAPI** und **Gemini 2.0 Flash** nach der beruflichen Position des Ansprechpartners.
|
||||
* **Ergebnis:** Identifiziert Rollen wie "CFO", "Mitglied der Klinikleitung" oder "Facharzt", um den Tonfall der Antwort perfekt anzupassen.
|
||||
* **Automatisierung:** Sucht via **SerpAPI** und **Gemini 2.0 Flash** nach der beruflichen Position.
|
||||
* **Ergebnis:** Identifiziert Rollen (z.B. "CFO"), um den Tonfall anzupassen.
|
||||
|
||||
### 3. Company Explorer Sync & Monitoring
|
||||
* **Integration:** Legt Accounts und Kontakte automatisch im CE an.
|
||||
* **Monitor:** Ein Hintergrund-Prozess (`monitor.py`) überwacht asynchron den Status der KI-Analyse im CE.
|
||||
* **Daten-Pull:** Sobald die Analyse (Branche, Dossier) fertig ist, werden die Daten in die lokale Lead-Datenbank übernommen.
|
||||
* **Monitor:** Hintergrund-Prozess (`monitor.py`) überwacht den Analyse-Status.
|
||||
* **Daten-Pull:** Übernimmt Branche und Dossier in die lokale Lead-Datenbank.
|
||||
|
||||
### 4. Expert Response Generator
|
||||
* **KI-Engine:** Nutzt Gemini 2.0 Flash zur Erstellung von E-Mail-Entwürfen.
|
||||
* **Kontext:** Kombiniert Lead-Daten (Fläche) + CE-Daten (Dossier) + Matrix-Argumente (Pains/Gains).
|
||||
* **Persistente Entwürfe:** Generierte E-Mail-Entwürfe werden direkt beim Lead gespeichert und bleiben erhalten.
|
||||
* **KI-Engine:** Gemini 2.0 Flash erstellt E-Mail-Entwürfe.
|
||||
* **Kontext:** Kombiniert Lead-Daten + CE-Daten + Matrix-Argumente (Pains/Gains).
|
||||
|
||||
### 5. UI & Qualitätskontrolle
|
||||
* **Visuelle Unterscheidung:** Klare Kennzeichnung der Lead-Quelle (z.B. 🌐 für Website, 🤝 für Partner) in der Übersicht.
|
||||
* **Status-Tracking:** Visueller Indikator (🆕/✅) für den Synchronisations-Status mit dem Company Explorer.
|
||||
* **Low-Quality-Warnung:** Visuelle Kennzeichnung (⚠️) von Leads mit Free-Mail-Adressen oder ohne Firmennamen direkt in der Übersicht.
|
||||
|
||||
### 6. Trading Twins Autopilot (PRODUKTIV v2.0)
|
||||
### 5. Trading Twins Autopilot (PRODUKTIV v2.1)
|
||||
Der vollautomatische "Zero Touch" Workflow für Trading Twins Anfragen.
|
||||
|
||||
* **Human-in-the-Loop:** Vor Versand erhält Elizabeta Melcer eine Teams-Nachricht ("Approve/Deny") via Adaptive Card.
|
||||
* **Feedback-Server:** Ein integrierter FastAPI-Server (Port 8004) verarbeitet die Klicks aus Teams und gibt sofortiges visuelles Feedback.
|
||||
* **Direct Calendar Booking (Eigener Service):**
|
||||
* **Problem:** MS Bookings API lässt sich nicht per Application Permission steuern (Erstellung verboten).
|
||||
* **Lösung:** Wir haben einen eigenen Micro-Booking-Service gebaut.
|
||||
* **Ablauf:** Das System prüft echte freie Slots im Kalender von `e.melcer` (via Graph API).
|
||||
* **E-Mail:** Der Kunde erhält eine E-Mail mit zwei konkreten Terminvorschlägen (Links).
|
||||
* **Buchung:** Klick auf einen Link -> Server bestätigt -> **Echte Outlook-Kalendereinladung** wird automatisch von `info@` versendet.
|
||||
* **Technologie:**
|
||||
* **Teams Webhook:** Für interaktive "Adaptive Cards".
|
||||
* **Graph API:** Für E-Mail-Versand (`info@`) und Kalender-Check (`e.melcer`).
|
||||
* **Orchestrator (`manager.py`):** Steuert den Ablauf (Lead -> CE -> Teams -> Timer -> Mail -> Booking).
|
||||
* **Human-in-the-Loop:** Elizabeta Melcer erhält eine Teams-Nachricht ("Approve/Deny").
|
||||
* **Feedback-Server:** Ein integrierter FastAPI-Server (Port 8004) verarbeitet Klicks.
|
||||
* **Direct Calendar Booking (Micro-Service):**
|
||||
* **Logik:** Prüft den Kalender von `e.melcer` auf **echte Verfügbarkeit**.
|
||||
* **Raster:** Termine starten nur im **15-Minuten-Takt** (:00, :15, :30, :45).
|
||||
* **Abstand:** Bietet zwei Termine an, mit ca. **3 Stunden Pause** dazwischen.
|
||||
* **Buchung:** Klick auf Link -> Server erstellt Outlook-Termin von `info@` mit `e.melcer` als Teilnehmer.
|
||||
|
||||
## 🏗 Architektur
|
||||
|
||||
@@ -52,63 +41,55 @@ Der vollautomatische "Zero Touch" Workflow für Trading Twins Anfragen.
|
||||
├── app.py # Streamlit Web-Interface
|
||||
├── trading_twins_ingest.py # E-Mail Importer (Graph API)
|
||||
├── monitor.py # Monitor + Trigger für Orchestrator
|
||||
├── trading_twins/ # [NEU] Autopilot Modul
|
||||
│ ├── manager.py # Orchestrator, FastAPI Server, Graph API Logic
|
||||
│ ├── signature.html # HTML-Signatur für E-Mails
|
||||
│ └── debug_bookings_only.py # Diagnose-Tool (Legacy)
|
||||
├── db.py # Lokale Lead-Datenbank
|
||||
└── data/ # DB-Storage
|
||||
├── trading_twins/ # Autopilot Modul
|
||||
│ ├── manager.py # Orchestrator, FastAPI, Graph API Logic
|
||||
│ ├── test_calendar_logic.py # Interner Test für Kalender-Zugriff
|
||||
│ └── signature.html # HTML-Signatur
|
||||
└── db.py # Lokale SQLite Lead-Datenbank
|
||||
```
|
||||
|
||||
## 🚨 Lessons Learned & Troubleshooting (Critical)
|
||||
## 🚨 Lessons Learned & Critical Fixes
|
||||
|
||||
### 1. Microsoft Bookings API Falle
|
||||
* **Problem:** Wir wollten `Bookings.Manage.All` nutzen, um eine Buchungsseite für `info@` zu erstellen.
|
||||
* **Fehler:** `403 Forbidden` ("Api Business.Create does not support the token type: App") und `500 Internal Server Error` (bei `GET`).
|
||||
* **Erkenntnis:** Eine App (Service Principal) kann zwar Bookings *verwalten*, aber **nicht initial erstellen**. Die erste Seite muss zwingend manuell oder per Delegated-User angelegt werden. Zudem erfordert der Zugriff oft eine User-Lizenz, die Service Principals nicht haben.
|
||||
* **Lösung:** Umstieg auf **Direct Calendar Booking** (Graph API `Calendar.ReadWrite`). Wir schreiben Termine direkt in den Outlook-Kalender, statt über die Bookings-Schicht zu gehen. Das ist robuster und voll automatisierbar.
|
||||
### 1. Microsoft Graph API: Kalender-Zugriff
|
||||
* **Problem:** `debug_calendar.py` scheiterte oft mit `Invalid parameter`.
|
||||
* **Ursache:** URL-Encoding von Zeitstempeln (`+` wurde zu Leerzeichen) und Mikrosekunden (7 Stellen statt 6).
|
||||
* **Lösung:** Nutzung von `requests(params=...)` und Abschneiden der Mikrosekunden.
|
||||
* **Endpoint:** `/users/{email}/calendar/getSchedule` (POST) ist robuster als `/calendarView` (GET).
|
||||
|
||||
### 4. Exchange AppOnly AccessPolicy
|
||||
* **Problem:** Trotz globaler `Calendars.ReadWrite` Berechtigung schlug das Erstellen von Terminen im Kalender von `e.melcer@` fehl (`403 Forbidden: Blocked by tenant configured AppOnly AccessPolicy settings`).
|
||||
* **Erkenntnis:** Viele Organisationen schränken per Policy ein, auf welche Postfächer eine App zugreifen darf. Ein Zugriff auf "fremde" Postfächer ist oft standardmäßig gesperrt.
|
||||
* **Lösung:** Der Termin wird im **eigenen Kalender** des Service-Accounts (`info@robo-planet.de`) erstellt. Der zuständige Mitarbeiter (`e.melcer@`) wird als **erforderlicher Teilnehmer** hinzugefügt. Dies umgeht die Policy-Sperre und stellt sicher, dass der Mitarbeiter den Termin in seinem Kalender sieht und das Teams-Meeting voll steuern kann.
|
||||
### 2. Exchange AppOnly AccessPolicy (Buchungs-Workaround)
|
||||
* **Problem:** `Calendars.ReadWrite` erlaubt einer App oft nicht, Termine in *fremden* Kalendern (`e.melcer@`) zu erstellen (`403 Forbidden`).
|
||||
* **Lösung:** Der Termin wird im **eigenen Kalender** des Service-Accounts (`info@`) erstellt. Der Mitarbeiter (`e.melcer@`) wird als **Teilnehmer** hinzugefügt. Das umgeht die Policy.
|
||||
|
||||
## 🚀 Inbetriebnahme (Docker)
|
||||
### 3. Docker Environment Variables
|
||||
* **Problem:** Skripte im Container fanden Credentials nicht, obwohl sie in `.env` standen.
|
||||
* **Lösung:** Explizites `load_dotenv` ist in Standalone-Skripten (`test_*.py`) nötig. Im Hauptprozess (`manager.py`) reicht `os.getenv`, solange Docker Compose die Vars korrekt durchreicht.
|
||||
|
||||
## 🚀 Inbetriebnahme (Docker)
|
||||
|
||||
Die Lead Engine ist als Service in der zentralen `docker-compose.yml` integriert.
|
||||
## 🚀 Inbetriebnahme
|
||||
|
||||
```bash
|
||||
# Neustart des Dienstes nach Code-Änderungen
|
||||
# Neustart des Dienstes
|
||||
docker-compose up -d --build --force-recreate lead-engine
|
||||
|
||||
# Manueller Test (intern)
|
||||
docker exec lead-engine python /app/trading_twins/test_calendar_logic.py
|
||||
```
|
||||
|
||||
**Zugriff:** `https://floke-ai.duckdns.org/lead/` (Passwortgeschützt)
|
||||
**Feedback API:** `https://floke-ai.duckdns.org/feedback/` (Öffentlich)
|
||||
|
||||
## 📝 Credentials (.env)
|
||||
|
||||
Für den Betrieb sind folgende Variablen in der zentralen `.env` zwingend erforderlich:
|
||||
|
||||
```env
|
||||
# App 1: Info-Postfach (Schreiben)
|
||||
# Info-Postfach (App 1 - Schreiben)
|
||||
INFO_Application_ID=...
|
||||
INFO_Tenant_ID=...
|
||||
INFO_Secret=...
|
||||
|
||||
# App 2: E.Melcer Kalender (Lesen)
|
||||
# E.Melcer Kalender (App 2 - Lesen)
|
||||
CAL_APPID=...
|
||||
CAL_TENNANT_ID=...
|
||||
CAL_SECRET=...
|
||||
|
||||
# Teams
|
||||
# URLs
|
||||
TEAMS_WEBHOOK_URL=...
|
||||
|
||||
# Public URL
|
||||
FEEDBACK_SERVER_BASE_URL=https://floke-ai.duckdns.org/feedback
|
||||
```
|
||||
|
||||
---
|
||||
*Dokumentationsstand: 5. März 2026*
|
||||
*Task: [31988f42]*
|
||||
```
|
||||
@@ -8,6 +8,10 @@ from threading import Thread, Lock
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Response, BackgroundTasks
|
||||
import msal
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from /app/.env
|
||||
load_dotenv(dotenv_path="/app/.env", override=True)
|
||||
|
||||
# --- Zeitzonen-Konfiguration ---
|
||||
TZ_BERLIN = ZoneInfo("Europe/Berlin")
|
||||
@@ -60,10 +64,15 @@ def check_calendar_availability():
|
||||
"availabilityViewInterval": 60 # Check availability in 1-hour blocks
|
||||
}
|
||||
|
||||
url = f"{GRAPH_API_ENDPOINT}/users/{TARGET_EMAIL}/calendarView?startDateTime={start_time.isoformat()}&endDateTime={end_time.isoformat()}&$top=5"
|
||||
url = f"{GRAPH_API_ENDPOINT}/users/{TARGET_EMAIL}/calendarView"
|
||||
params = {
|
||||
"startDateTime": start_time.isoformat(),
|
||||
"endDateTime": end_time.isoformat(),
|
||||
"$top": 5
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
response = requests.get(url, headers=headers, params=params)
|
||||
if response.status_code == 200:
|
||||
events = response.json().get("value", [])
|
||||
if not events:
|
||||
@@ -75,6 +84,12 @@ def check_calendar_availability():
|
||||
subject = event.get('subject', 'No Subject')
|
||||
start = event.get('start', {}).get('dateTime')
|
||||
if start:
|
||||
# Fix for 7-digit microseconds from Graph API (e.g. 2026-03-09T17:00:00.0000000)
|
||||
if "." in start:
|
||||
main_part, frac_part = start.split(".")
|
||||
# Truncate to 6 digits max or remove if empty
|
||||
start = f"{main_part}.{frac_part[:6]}"
|
||||
|
||||
dt_obj = datetime.fromisoformat(start.replace('Z', '+00:00')).astimezone(TZ_BERLIN)
|
||||
start_formatted = dt_obj.strftime('%A, %d.%m.%Y um %H:%M Uhr')
|
||||
else: start_formatted = "N/A"
|
||||
|
||||
@@ -47,21 +47,66 @@ def get_access_token(client_id, client_secret, tenant_id):
|
||||
return result.get('access_token')
|
||||
|
||||
def get_availability(target_email, app_creds):
|
||||
print(f"DEBUG: Requesting availability for {target_email}")
|
||||
token = get_access_token(*app_creds)
|
||||
if not token: return None
|
||||
if not token:
|
||||
print("DEBUG: Failed to acquire access token.")
|
||||
return None
|
||||
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json", "Prefer": 'outlook.timezone="Europe/Berlin"'}
|
||||
start_time = datetime.now(TZ_BERLIN).replace(hour=0, minute=0, second=0)
|
||||
start_time = datetime.now(TZ_BERLIN).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_time = start_time + timedelta(days=3)
|
||||
payload = {"schedules": [target_email], "startTime": {"dateTime": start_time.isoformat()}, "endTime": {"dateTime": end_time.isoformat()}, "availabilityViewInterval": 60}
|
||||
# Use 15-minute intervals for finer granularity
|
||||
payload = {"schedules": [target_email], "startTime": {"dateTime": start_time.isoformat()}, "endTime": {"dateTime": end_time.isoformat()}, "availabilityViewInterval": 15}
|
||||
|
||||
try:
|
||||
r = requests.post(f"{GRAPH_API_ENDPOINT}/users/{target_email}/calendar/getSchedule", headers=headers, json=payload)
|
||||
if r.status_code == 200: return start_time, r.json()['value'][0].get('availabilityView', ''), 60
|
||||
except: pass
|
||||
url = f"{GRAPH_API_ENDPOINT}/users/{target_email}/calendar/getSchedule"
|
||||
r = requests.post(url, headers=headers, json=payload)
|
||||
print(f"DEBUG: API Status Code: {r.status_code}")
|
||||
|
||||
if r.status_code == 200:
|
||||
view = r.json()['value'][0].get('availabilityView', '')
|
||||
print(f"DEBUG: Availability View received (Length: {len(view)})")
|
||||
return start_time, view, 15
|
||||
else:
|
||||
print(f"DEBUG: API Error Response: {r.text}")
|
||||
except Exception as e:
|
||||
print(f"DEBUG: Exception during API call: {e}")
|
||||
pass
|
||||
return None
|
||||
|
||||
def find_slots(start, view, interval):
|
||||
# This logic is complex and proven, keeping it as is.
|
||||
return [datetime.now(TZ_BERLIN) + timedelta(days=1, hours=h) for h in [10, 14]] # Placeholder
|
||||
"""
|
||||
Parses availability string: '0'=Free, '2'=Busy.
|
||||
Returns 2 free slots (start times) within business hours (09:00 - 16:30),
|
||||
excluding weekends (Sat/Sun), with approx. 3 hours distance between them.
|
||||
"""
|
||||
slots = []
|
||||
first_slot = None
|
||||
|
||||
# Iterate through the view string
|
||||
for i, status in enumerate(view):
|
||||
if status == '0': # '0' means Free
|
||||
slot_time = start + timedelta(minutes=i * interval)
|
||||
|
||||
# Constraints:
|
||||
# 1. Mon-Fri only
|
||||
# 2. Business hours (09:00 - 16:30)
|
||||
# 3. Future only
|
||||
if slot_time.weekday() < 5 and (9 <= slot_time.hour < 17) and slot_time > datetime.now(TZ_BERLIN):
|
||||
# Max start time 16:30
|
||||
if slot_time.hour == 16 and slot_time.minute > 30:
|
||||
continue
|
||||
|
||||
if first_slot is None:
|
||||
first_slot = slot_time
|
||||
slots.append(first_slot)
|
||||
else:
|
||||
# Second slot should be at least 3 hours after the first
|
||||
if slot_time >= first_slot + timedelta(hours=3):
|
||||
slots.append(slot_time)
|
||||
break
|
||||
return slots
|
||||
|
||||
def create_calendar_invite(lead_email, company, start_time):
|
||||
catchall = os.getenv("EMAIL_CATCHALL"); lead_email = catchall if catchall else lead_email
|
||||
|
||||
88
lead-engine/trading_twins/test_calendar_logic.py
Normal file
88
lead-engine/trading_twins/test_calendar_logic.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# lead-engine/trading_twins/test_calendar_logic.py
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
from dotenv import load_dotenv
|
||||
import msal
|
||||
import requests
|
||||
|
||||
# Load environment variables from the root .env
|
||||
load_dotenv(dotenv_path="/app/.env", override=True)
|
||||
|
||||
# Pfad anpassen, damit wir manager importieren können
|
||||
sys.path.append('/app')
|
||||
|
||||
from trading_twins.manager import get_availability, find_slots
|
||||
|
||||
# Re-import variables to ensure we see what's loaded
|
||||
CAL_APPID = os.getenv("CAL_APPID")
|
||||
CAL_SECRET = os.getenv("CAL_SECRET")
|
||||
CAL_TENNANT_ID = os.getenv("CAL_TENNANT_ID")
|
||||
|
||||
TZ_BERLIN = ZoneInfo("Europe/Berlin")
|
||||
|
||||
def test_internal():
|
||||
target = "e.melcer@robo-planet.de"
|
||||
print(f"🔍 Teste Kalender-Logik für {target}...")
|
||||
|
||||
# Debug Token Acquisition
|
||||
print("🔑 Authentifiziere mit MS Graph...")
|
||||
authority = f"https://login.microsoftonline.com/{CAL_TENNANT_ID}"
|
||||
app_msal = msal.ConfidentialClientApplication(client_id=CAL_APPID, authority=authority, client_credential=CAL_SECRET)
|
||||
result = app_msal.acquire_token_silent([".default"], account=None)
|
||||
if not result:
|
||||
print(" ... hole neues Token ...")
|
||||
result = app_msal.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
|
||||
|
||||
if "access_token" in result:
|
||||
print("✅ Token erhalten.")
|
||||
token = result['access_token']
|
||||
else:
|
||||
print(f"❌ Token-Fehler: {result.get('error')}")
|
||||
print(f"❌ Beschreibung: {result.get('error_description')}")
|
||||
return
|
||||
|
||||
# Debug API Call
|
||||
print("📡 Frage Kalender ab...")
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json", "Prefer": 'outlook.timezone="Europe/Berlin"'}
|
||||
start_time = datetime.now(TZ_BERLIN).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_time = start_time + timedelta(days=3)
|
||||
|
||||
payload = {
|
||||
"schedules": [target],
|
||||
"startTime": {"dateTime": start_time.isoformat(), "timeZone": "Europe/Berlin"},
|
||||
"endTime": {"dateTime": end_time.isoformat(), "timeZone": "Europe/Berlin"},
|
||||
"availabilityViewInterval": 15
|
||||
}
|
||||
|
||||
import requests
|
||||
try:
|
||||
url = f"https://graph.microsoft.com/v1.0/users/{target}/calendar/getSchedule"
|
||||
r = requests.post(url, headers=headers, json=payload)
|
||||
|
||||
print(f"📡 API Status: {r.status_code}")
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
# print(f"DEBUG RAW: {data}")
|
||||
schedule = data['value'][0]
|
||||
view = schedule.get('availabilityView', '')
|
||||
print(f"✅ Verfügbarkeit (View Länge: {len(view)})")
|
||||
|
||||
# Test Slot Finding
|
||||
slots = find_slots(start_time, view, 15)
|
||||
if slots:
|
||||
print(f"✅ {len(slots)} Slots gefunden:")
|
||||
for s in slots:
|
||||
print(f" 📅 {s.strftime('%A, %d.%m.%Y um %H:%M')}")
|
||||
else:
|
||||
print("⚠️ Keine Slots gefunden (Logik korrekt, aber Kalender voll?)")
|
||||
else:
|
||||
print(f"❌ API Fehler: {r.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Exception beim API Call: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_internal()
|
||||
Reference in New Issue
Block a user