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:
2026-03-08 08:46:25 +00:00
parent 57081bf102
commit 44502e5b2b
16 changed files with 451 additions and 136 deletions

View File

@@ -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"

View File

@@ -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

View 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()