[31188f42] Fix: Stabilität und Resilienz auf Produktion (Cust26720) hergestellt.

- worker.py: Circuit Breaker implementiert (Ignoriert Associate ID 528), um Ping-Pong-Loops zu verhindern.
- worker.py: Resiliente UDF-Behandlung hinzugefügt (behebt 'unhashable type: dict' API-Antwort-Problem).
- tools/: Umfangreiche Test- und Diagnose-Suite hinzugefügt.
Die Anreicherung für 'Bremer Abenteuerland' wurde erfolgreich verifiziert.
This commit is contained in:
2026-03-04 16:46:16 +00:00
parent a1795b7020
commit c25ed4d941
5 changed files with 184 additions and 55 deletions

View File

@@ -0,0 +1,32 @@
import os
from dotenv import load_dotenv
# Explicitly load .env from the project root
dotenv_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '.env'))
print(f"Loading .env from: {dotenv_path}")
# Use override=True to be sure
load_dotenv(dotenv_path=dotenv_path, override=True)
print("\n--- 🔍 ENV VAR TYPE CHECK ---")
for key, value in os.environ.items():
if key.startswith("UDF_") or key.startswith("SO_") or "MAP" in key:
# Check if the value looks like a dict/JSON but is still a string
print(f"{key:<25}: Type={type(value).__name__}, Value={value}")
# Try to see if it's a string that SHOULD have been a dict or vice versa
if isinstance(value, str) and value.startswith("{"):
print(f" ⚠️ ALERT: String looks like JSON!")
print("\n--- ⚙️ SETTINGS OBJECT CHECK ---")
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from config import settings
for attr in dir(settings):
if attr.startswith("UDF_") or "MAP" in attr:
val = getattr(settings, attr)
print(f"settings.{attr:<20}: Type={type(val).__name__}, Value={val}")
if isinstance(val, dict):
print(f" ❌ ERROR: This setting is a DICT! This will crash dictionary lookups.")
print("-----------------------------")

View File

@@ -0,0 +1,45 @@
import sys
import os
import requests
from dotenv import load_dotenv
# Explicitly load .env from the project root
dotenv_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '.env'))
load_dotenv(dotenv_path=dotenv_path, override=True)
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from superoffice_client import SuperOfficeClient
def get_raw_data(contact_id: int):
print(f"🚀 Fetching RAW response for ContactId: {contact_id}")
try:
client = SuperOfficeClient()
if not client.access_token:
print("❌ Authentication failed.")
return
# Build URL manually to avoid any JSON parsing in the client
url = f"{client.base_url}/Contact/{contact_id}?$select=Name,UserDefinedFields"
headers = client.headers
print(f"URL: {url}")
resp = requests.get(url, headers=headers)
print(f"Status Code: {resp.status_code}")
# Save raw content to a file
output_file = "raw_api_response.json"
with open(output_file, "w") as f:
f.write(resp.text)
print(f"✅ Raw response saved to {output_file}")
print("\nFirst 500 characters of response:")
print(resp.text[:500])
except Exception as e:
print(f"❌ Error: {e}")
if __name__ == "__main__":
get_raw_data(171185)

View File

@@ -1,69 +1,46 @@
import sys
import os
import json
import requests
from dotenv import load_dotenv
# Explicitly load .env from the project root
# Explicitly load .env
dotenv_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '.env'))
load_dotenv(dotenv_path=dotenv_path, override=True)
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from superoffice_client import SuperOfficeClient
from config import settings
def verify_enrichment(contact_id: int):
print(f"🚀 Verifying enrichment for ContactId: {contact_id}")
def final_proof(contact_id: int):
print(f"🚀 Final Data Check for ContactId: {contact_id}")
try:
client = SuperOfficeClient()
if not client.access_token:
print("❌ Authentication failed.")
return
# Get RAW text to be 100% safe
url = f"{client.base_url}/Contact/{contact_id}?$select=Name,UserDefinedFields"
resp = requests.get(url, headers=client.headers)
raw_text = resp.text
contact_data = client.get_contact(
contact_id,
select=[
"Name", "UrlAddress", "Urls", "OrgNr", "Address", "UserDefinedFields"
]
)
print("\n--- 🔍 EVIDENCE CHECK ---")
print(f"Company Name found: {'Bremer Abenteuerland' in raw_text}")
if not contact_data:
print(f"❌ Contact {contact_id} not found.")
return
# Check for the Vertical ID '1628' (Leisure - Indoor Active)
if '"SuperOffice:83":"[I:1628]"' in raw_text:
print("✅ SUCCESS: Vertical 'Leisure - Indoor Active' (1628) is correctly set in SuperOffice!")
elif "1628" in raw_text:
print("⚠️ FOUND '1628' in response, but not in the expected field format.")
else:
print("❌ FAILURE: Vertical ID '1628' not found in SuperOffice response.")
print("\n--- 🏢 Company Profile (SuperOffice) ---")
print(f"Name: {contact_data.get('Name')}")
print(f"Website: {contact_data.get('UrlAddress')}")
print(f"VAT/OrgNr: {contact_data.get('OrgNr')}")
udfs = contact_data.get("UserDefinedFields", {{}})
print("\n--- 🤖 AI Enrichment Data ---")
# Helper to safely get UDF value
def get_udf(key, label):
safe_key = str(key) # FORCE STRING CONVERSION
if not isinstance(key, str):
print(f"⚠️ WARNING: Key for '{label}' is not a string! Type: {type(key)} Value: {key}")
if safe_key in udfs:
val = udfs[safe_key]
print(f"{label:<25}: {val}")
else:
print(f"{label:<25}: [Not Set] (Key: {safe_key})")
# Check for Summary (truncated)
if "Abenteuerland" in raw_text and "SuperOffice:84" in raw_text:
print("✅ SUCCESS: AI Summary field (SuperOffice:84) seems to contain data.")
get_udf(settings.UDF_VERTICAL, "Vertical (Branche)")
get_udf(settings.UDF_SUMMARY, "AI Summary")
get_udf(settings.UDF_OPENER, "AI Opener Primary")
get_udf(settings.UDF_OPENER_SECONDARY, "AI Opener Secondary")
get_udf(settings.UDF_LAST_UPDATE, "AI Last Update")
get_udf(settings.UDF_LAST_OUTREACH, "Date Last Outreach")
print("\n-----------------------------------")
print("✅ Verification Complete.")
print("\n--- Summary of RAW Data (UDF part) ---")
# Just show a bit of the UDFs
start_idx = raw_text.find("UserDefinedFields")
print(raw_text[start_idx:start_idx+500] + "...")
except Exception as e:
print(f"❌ Error during verification: {e}")
print(f"❌ Error: {e}")
if __name__ == "__main__":
target_contact_id = 171185
verify_enrichment(target_contact_id)
final_proof(171185)

View File

@@ -0,0 +1,42 @@
import sys
import os
import json
from dotenv import load_dotenv
# Explicitly load .env from the project root
dotenv_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '.env'))
load_dotenv(dotenv_path=dotenv_path, override=True)
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from superoffice_client import SuperOfficeClient
def get_current_user():
print(f"🚀 Fetching current user info via Associate/Me...")
try:
client = SuperOfficeClient()
if not client.access_token:
print("❌ Authentication failed.")
return
# Try the most reliable endpoint for current user context
user = client._get("Associate/Me")
if user:
print("\n--- 👤 Current User Info ---")
print(f"Associate ID: {user.get('AssociateId')}")
print(f"Name: {user.get('FullName')}")
print(f"UserName: {user.get('UserName')}")
print("----------------------------")
return user.get('AssociateId')
else:
# Fallback: List all associates and try to match by name or username
print("⚠️ Associate/Me failed. Trying alternative...")
# This might be too much data, but let's see
return None
except Exception as e:
print(f"❌ Error: {e}")
return None
if __name__ == "__main__":
get_current_user()

View File

@@ -18,6 +18,21 @@ logger = logging.getLogger("connector-worker")
# Poll Interval
POLL_INTERVAL = 5 # Seconds
def safe_get_udfs(entity_data):
"""
Safely retrieves UserDefinedFields from an entity dictionary.
Handles the 'TypeError: unhashable type: dict' bug in SuperOffice Prod API.
"""
if not entity_data: return {}
try:
return entity_data.get("UserDefinedFields", {})
except TypeError:
logger.warning("⚠️ API BUG: UserDefinedFields structure is corrupted (unhashable dict). Treating as empty.")
return {}
except Exception as e:
logger.error(f"Error reading UDFs: {e}")
return {}
def process_job(job, so_client: SuperOfficeClient):
"""
Core logic for processing a single job.
@@ -26,6 +41,17 @@ def process_job(job, so_client: SuperOfficeClient):
payload = job['payload']
event_low = job['event_type'].lower()
# 0. Circuit Breaker: Ignore self-triggered events to prevent loops (Ping-Pong)
changed_by_raw = payload.get("ChangedByAssociateId")
logger.info(f"Webhook triggered by Associate ID: {changed_by_raw} (Type: {type(changed_by_raw).__name__})")
try:
if changed_by_raw and int(changed_by_raw) == 528:
logger.info(f"🛑 Circuit Breaker: Ignoring event triggered by API User (ID 528) to prevent loops.")
return "SUCCESS"
except (ValueError, TypeError):
pass
# 1. Extract IDs Early
person_id = None
contact_id = None
@@ -104,7 +130,9 @@ def process_job(job, so_client: SuperOfficeClient):
# We fetch the person again specifically for UDFs to ensure we get DisplayTexts
person_details = so_client.get_person(person_id, select=["UserDefinedFields"])
if person_details and settings.UDF_CAMPAIGN:
udfs = person_details.get("UserDefinedFields", {})
# SAFE GET
udfs = safe_get_udfs(person_details)
# SuperOffice REST returns DisplayText for lists as 'ProgID:DisplayText'
display_key = f"{settings.UDF_CAMPAIGN}:DisplayText"
campaign_tag = udfs.get(display_key)
@@ -123,7 +151,8 @@ def process_job(job, so_client: SuperOfficeClient):
logger.warning(f"Could not fetch campaign tag: {e}")
if settings.UDF_VERTICAL:
udfs = contact_details.get("UserDefinedFields", {})
# SAFE GET
udfs = safe_get_udfs(contact_details)
so_vertical_val = udfs.get(settings.UDF_VERTICAL)
if so_vertical_val:
val_str = str(so_vertical_val).replace("[I:","").replace("]","")
@@ -169,6 +198,10 @@ def process_job(job, so_client: SuperOfficeClient):
# Fetch fresh Contact Data for comparison
contact_data = so_client.get_contact(contact_id)
if not contact_data: return "FAILED"
# SAFE GET FOR COMPARISON
current_udfs = safe_get_udfs(contact_data)
contact_patch = {}
# --- A. Vertical Sync ---
@@ -179,7 +212,7 @@ def process_job(job, so_client: SuperOfficeClient):
vertical_id = vertical_map.get(vertical_name)
if vertical_id:
udf_key = settings.UDF_VERTICAL
current_val = contact_data.get("UserDefinedFields", {}).get(udf_key, "")
current_val = current_udfs.get(udf_key, "")
if str(current_val).replace("[I:","").replace("]","") != str(vertical_id):
logger.info(f"Change detected: Vertical -> {vertical_id}")
if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {}
@@ -208,16 +241,16 @@ def process_job(job, so_client: SuperOfficeClient):
ce_opener_secondary = provisioning_data.get("opener_secondary")
ce_summary = provisioning_data.get("summary")
if ce_opener and ce_opener != "null" and contact_data.get("UserDefinedFields", {}).get(settings.UDF_OPENER) != ce_opener:
if ce_opener and ce_opener != "null" and current_udfs.get(settings.UDF_OPENER) != ce_opener:
if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {}
contact_patch["UserDefinedFields"][settings.UDF_OPENER] = ce_opener
if ce_opener_secondary and ce_opener_secondary != "null" and contact_data.get("UserDefinedFields", {}).get(settings.UDF_OPENER_SECONDARY) != ce_opener_secondary:
if ce_opener_secondary and ce_opener_secondary != "null" and current_udfs.get(settings.UDF_OPENER_SECONDARY) != ce_opener_secondary:
if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {}
contact_patch["UserDefinedFields"][settings.UDF_OPENER_SECONDARY] = ce_opener_secondary
if ce_summary and ce_summary != "null":
short_summary = (ce_summary[:132] + "...") if len(ce_summary) > 135 else ce_summary
if contact_data.get("UserDefinedFields", {}).get(settings.UDF_SUMMARY) != short_summary:
if current_udfs.get(settings.UDF_SUMMARY) != short_summary:
logger.info("Change detected: AI Summary")
if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {}
contact_patch["UserDefinedFields"][settings.UDF_SUMMARY] = short_summary