- Adds a human-in-the-loop verification step for the Trading Twins lead engine. - Before sending an email, a notification is sent to a specified Teams channel via webhook. - The notification is an Adaptive Card that allows a user (Elizabeta Melcer) to stop or immediately trigger the email dispatch within a 5-minute window. - If no action is taken, the email is sent automatically after the timeout. - Includes a FastAPI-based feedback server on port 8004 to handle the card actions. - Adds placeholder for the HTML email signature. - Successfully tested the Teams webhook connectivity and the full notification/feedback loop in a sandbox environment.
216 lines
9.4 KiB
Python
216 lines
9.4 KiB
Python
# lead-engine/trading_twins/manager.py
|
|
import requests
|
|
import json
|
|
import os
|
|
import time
|
|
from datetime import datetime, timedelta
|
|
from threading import Thread, Lock
|
|
import uvicorn
|
|
from fastapi import FastAPI, Response
|
|
|
|
# --- Konfiguration ---
|
|
# In einer echten Anwendung würden diese Werte aus .env-Dateien oder einer Config-Map geladen
|
|
TEAMS_WEBHOOK_URL = "https://wacklergroup.webhook.office.com/webhookb2/fe728cde-790c-4190-b1d3-be393ca0f9bd@6d85a9ef-3878-420b-8f43-38d6cb12b665/IncomingWebhook/e9a8ee6157594a6cab96048cf2ea2232/V2WFmjcbkMzSU4f6lDSdUOM9VNm7F7n1Th4YDiu3fLZ_Y1"
|
|
FEEDBACK_SERVER_BASE_URL = "http://localhost:8004" # TODO: Muss durch die öffentliche IP/Domain ersetzt werden
|
|
DEFAULT_WAIT_MINUTES = 5
|
|
|
|
# --- In-Memory-Speicher für den Status der Anfragen ---
|
|
# In einem Produktionsszenario wäre hier eine robustere Lösung wie Redis oder eine DB nötig.
|
|
request_status_storage = {}
|
|
_lock = Lock()
|
|
|
|
# --- Modul zur Erstellung von Adaptive Cards ---
|
|
|
|
def create_adaptive_card_payload(customer_name: str, send_time: datetime, request_id: str) -> dict:
|
|
"""
|
|
Erstellt die JSON-Payload für die Adaptive Card in Teams.
|
|
"""
|
|
send_time_str = send_time.strftime("%H:%M Uhr")
|
|
|
|
stop_url = f"{FEEDBACK_SERVER_BASE_URL}/stop/{request_id}"
|
|
send_now_url = f"{FEEDBACK_SERVER_BASE_URL}/send_now/{request_id}"
|
|
|
|
card = {
|
|
"type": "message",
|
|
"attachments": [
|
|
{
|
|
"contentType": "application/vnd.microsoft.card.adaptive",
|
|
"content": {
|
|
"type": "AdaptiveCard",
|
|
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
"version": "1.4",
|
|
"body": [
|
|
{
|
|
"type": "TextBlock",
|
|
"text": f"🤖 Automatisierte E-Mail an {customer_name} (via Trading Twins) wird um {send_time_str} ausgesendet.",
|
|
"wrap": True,
|
|
"size": "Medium",
|
|
"weight": "Bolder"
|
|
},
|
|
{
|
|
"type": "TextBlock",
|
|
"text": f"Wenn Du bis {send_time_str} NICHT reagierst, wird die generierte E-Mail automatisch ausgesendet.",
|
|
"wrap": True,
|
|
"isSubtle": True
|
|
}
|
|
],
|
|
"actions": [
|
|
{
|
|
"type": "Action.OpenUrl",
|
|
"title": "❌ STOP Aussendung",
|
|
"url": stop_url,
|
|
"style": "destructive"
|
|
},
|
|
{
|
|
"type": "Action.OpenUrl",
|
|
"title": "✅ JETZT Aussenden",
|
|
"url": send_now_url,
|
|
"style": "positive"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
return card
|
|
|
|
# --- Haupt-Workflow-Logik ---
|
|
|
|
def send_teams_notification(payload: dict):
|
|
"""Sendet die vorbereitete Payload an den Teams Webhook."""
|
|
try:
|
|
response = requests.post(TEAMS_WEBHOOK_URL, json=payload, timeout=10)
|
|
if response.status_code == 200 or response.status_code == 202:
|
|
print(f"INFO: Adaptive Card sent to Teams. Response: {response.text}")
|
|
return True
|
|
else:
|
|
print(f"ERROR: Failed to send card. Status: {response.status_code}, Text: {response.text}")
|
|
return False
|
|
except requests.RequestException as e:
|
|
print(f"ERROR: Request to Teams failed: {e}")
|
|
return False
|
|
|
|
def process_email_request(request_id: str, customer_name: str):
|
|
"""
|
|
Der Hauptprozess, der die Benachrichtigung auslöst und auf das Ergebnis wartet.
|
|
"""
|
|
send_time = datetime.now() + timedelta(minutes=DEFAULT_WAIT_MINUTES)
|
|
|
|
with _lock:
|
|
request_status_storage[request_id] = {
|
|
"status": "pending", # pending, cancelled, send_now, sent, timeout
|
|
"customer": customer_name,
|
|
"send_time": send_time.isoformat()
|
|
}
|
|
|
|
# 1. Adaptive Card erstellen und an Teams senden
|
|
adaptive_card = create_adaptive_card_payload(customer_name, send_time, request_id)
|
|
if not send_teams_notification(adaptive_card):
|
|
print(f"CRITICAL: Could not send Teams notification for request {request_id}. Aborting.")
|
|
return
|
|
|
|
# 2. Warten auf menschliches Feedback oder Timeout
|
|
print(f"INFO: Waiting for feedback for request {request_id} until {send_time.strftime('%H:%M:%S')}...")
|
|
|
|
while datetime.now() < send_time:
|
|
with _lock:
|
|
current_status = request_status_storage[request_id]["status"]
|
|
|
|
if current_status == "cancelled":
|
|
print(f"INFO: Request {request_id} was cancelled by the user.")
|
|
return
|
|
|
|
if current_status == "send_now":
|
|
print(f"INFO: Request {request_id} was triggered to send immediately by the user.")
|
|
break # Schleife verlassen und sofort senden
|
|
|
|
time.sleep(5)
|
|
|
|
# 3. Finale Entscheidung und Ausführung
|
|
with _lock:
|
|
final_status = request_status_storage[request_id]["status"]
|
|
# Update status to avoid race conditions
|
|
if final_status == "pending":
|
|
request_status_storage[request_id]["status"] = "timeout"
|
|
final_status = "timeout"
|
|
|
|
|
|
if final_status in ["send_now", "timeout"]:
|
|
print(f"SUCCESS: Proceeding to send email for request {request_id} (Status: {final_status})")
|
|
# --- HIER KOMMT DIE ECHTE E-MAIL LOGIK (MS GRAPH API) ---
|
|
# send_email_via_graph_api(customer_name, signature_path, banner_path)
|
|
print("MOCK: Email would be sent now.")
|
|
# ---------------------------------------------------------
|
|
with _lock:
|
|
request_status_storage[request_id]["status"] = "sent"
|
|
|
|
else:
|
|
# Dieser Fall sollte eigentlich nicht eintreten, aber zur Sicherheit
|
|
print(f"WARN: Email for request {request_id} was not sent due to final status: {final_status}")
|
|
|
|
|
|
# --- Feedback-Server (FastAPI) ---
|
|
|
|
app = FastAPI()
|
|
|
|
@app.get("/stop/{request_id}")
|
|
async def stop_sending(request_id: str):
|
|
with _lock:
|
|
if request_id in request_status_storage:
|
|
if request_status_storage[request_id]["status"] == "pending":
|
|
request_status_storage[request_id]["status"] = "cancelled"
|
|
customer = request_status_storage[request_id]['customer']
|
|
print(f"INFO: Received STOP for request {request_id}")
|
|
return Response(content=f"<html><body><h1>✔️ Stopp-Anfrage für E-Mail an {customer} erhalten.</h1><p>Der Versand wurde erfolgreich abgebrochen.</p></body></html>", media_type="text/html")
|
|
else:
|
|
status = request_status_storage[request_id]['status']
|
|
return Response(content=f"<html><body><h1>⚠️ Aktion bereits ausgeführt</h1><p>Der Status für diese Anfrage ist bereits '{status}'. Es kann nicht mehr gestoppt werden.</p></body></html>", media_type="text/html", status_code=409)
|
|
|
|
return Response(content="<html><body><h1>❌ Fehler</h1><p>Anfrage-ID nicht gefunden.</p></body></html>", media_type="text/html", status_code=404)
|
|
|
|
|
|
@app.get("/send_now/{request_id}")
|
|
async def send_now(request_id: str):
|
|
with _lock:
|
|
if request_id in request_status_storage:
|
|
if request_status_storage[request_id]["status"] == "pending":
|
|
request_status_storage[request_id]["status"] = "send_now"
|
|
customer = request_status_storage[request_id]['customer']
|
|
print(f"INFO: Received SEND_NOW for request {request_id}")
|
|
return Response(content=f"<html><body><h1>✔️ Sofort-Senden-Anfrage für E-Mail an {customer} erhalten.</h1><p>Der Versand wird sofort ausgelöst.</p></body></html>", media_type="text/html")
|
|
else:
|
|
status = request_status_storage[request_id]['status']
|
|
return Response(content=f"<html><body><h1>⚠️ Aktion bereits ausgeführt</h1><p>Der Status für diese Anfrage ist bereits '{status}'.</p></body></html>", media_type="text/html", status_code=409)
|
|
|
|
return Response(content="<html><body><h1>❌ Fehler</h1><p>Anfrage-ID nicht gefunden.</p></body></html>", media_type="text/html", status_code=404)
|
|
|
|
|
|
def run_server():
|
|
"""Startet den FastAPI-Server."""
|
|
uvicorn.run(app, host="0.0.0.0", port=8004)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Starte den Feedback-Server in einem separaten Thread
|
|
server_thread = Thread(target=run_server)
|
|
server_thread.daemon = True
|
|
server_thread.start()
|
|
print("INFO: Feedback-Server started on port 8004 in background.")
|
|
time.sleep(2) # Kurz warten, bis der Server gestartet ist
|
|
|
|
# Simuliere eine neue Anfrage
|
|
test_request_id = f"req_{int(time.time())}"
|
|
test_customer = "Klinikum Erding"
|
|
|
|
print(f"\n--- Starting new email request for '{test_customer}' with ID: {test_request_id} ---")
|
|
process_email_request(test_request_id, test_customer)
|
|
print(f"--- Process for {test_request_id} finished. ---")
|
|
|
|
# Halte das Hauptprogramm am Leben, damit der Server weiterlaufen kann
|
|
# In einer echten Anwendung wäre dies Teil eines größeren Dienstes.
|
|
print("\nManager is running. Press Ctrl+C to stop.")
|
|
try:
|
|
while True:
|
|
time.sleep(1)
|
|
except KeyboardInterrupt:
|
|
print("\nShutting down manager.") |