Compare commits
4 Commits
088c665783
...
f3e4694f4f
| Author | SHA1 | Date | |
|---|---|---|---|
| f3e4694f4f | |||
| 391b74a6af | |||
| 7c686ad093 | |||
| 7e81c8778f |
@@ -1 +1 @@
|
|||||||
{"task_id": "31188f42-8544-80fa-8051-cef82ce7e4d3", "token": "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", "session_start_time": "2026-02-24T12:18:39.379752"}
|
{"task_id": "31188f42-8544-8074-bad3-d3e1b9b4051f", "token": "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", "session_start_time": "2026-02-24T13:10:01.388800"}
|
||||||
@@ -185,6 +185,21 @@ Die Simulation von E-Mails via Terminen (Appointments) erforderte Workarounds f
|
|||||||
3. **Rollen-Dynamik:**
|
3. **Rollen-Dynamik:**
|
||||||
* Um zu verhindern, dass alte Rollen (z.B. "Infrastruktur") nach einer Beförderung/Änderung in SuperOffice "kleben" bleiben, führt das System nun bei jeder Namens- oder Funktionsänderung einen **Rollen-Reset** durch.
|
* Um zu verhindern, dass alte Rollen (z.B. "Infrastruktur") nach einer Beförderung/Änderung in SuperOffice "kleben" bleiben, führt das System nun bei jeder Namens- oder Funktionsänderung einen **Rollen-Reset** durch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Lessons Learned: API Optimization & Certification (Feb 24, 2026)
|
||||||
|
|
||||||
|
Um die Zertifizierung für den SuperOffice App Store zu erhalten, mussten kritische Performance-Optimierungen durchgeführt werden.
|
||||||
|
|
||||||
|
1. **Die `getAllRows`-Falle:**
|
||||||
|
* **Problem:** SuperOffice monierte in der Validierung API-Calls wie `getAllRows` (implizit oft durch Abfragen ganzer Objekte ohne Filter), die unnötige Last verursachen.
|
||||||
|
* **Lösung:** Implementierung von **OData `$select`**. Wir fordern nun strikt nur die Felder an, die wir wirklich benötigen (z.B. `get_person(id, select=['JobTitle', 'UserDefinedFields'])`).
|
||||||
|
* **Wichtig:** Niemals pauschal `get_person()` aufrufen, wenn nur die Rolle geprüft werden soll.
|
||||||
|
|
||||||
|
2. **NullReference bei `$select`:**
|
||||||
|
* **Problem:** Wenn `$select` genutzt wird, gibt SuperOffice nicht angeforderte komplexe Objekte (wie `Contact` in `Person`) als `null` zurück. Der Zugriff `person['Contact']['ContactId']` führt dann zum Crash.
|
||||||
|
* **Lösung:** Robuste Checks (`if contact_obj and isinstance(contact_obj, dict)`) und primäre Nutzung der IDs direkt aus dem Webhook-Payload (`FieldValues`), um API-Calls komplett zu vermeiden.
|
||||||
|
|
||||||
## Appendix: The "First Sentence" Prompt
|
## Appendix: The "First Sentence" Prompt
|
||||||
This is the core logic used to generate the company-specific opener.
|
This is the core logic used to generate the company-specific opener.
|
||||||
|
|
||||||
|
|||||||
@@ -114,11 +114,17 @@ class SuperOfficeClient:
|
|||||||
|
|
||||||
# --- Convenience Wrappers ---
|
# --- Convenience Wrappers ---
|
||||||
|
|
||||||
def get_person(self, person_id):
|
def get_person(self, person_id, select: list = None):
|
||||||
return self._get(f"Person/{person_id}")
|
endpoint = f"Person/{person_id}"
|
||||||
|
if select:
|
||||||
|
endpoint += f"?$select={','.join(select)}"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
def get_contact(self, contact_id):
|
def get_contact(self, contact_id, select: list = None):
|
||||||
return self._get(f"Contact/{contact_id}")
|
endpoint = f"Contact/{contact_id}"
|
||||||
|
if select:
|
||||||
|
endpoint += f"?$select={','.join(select)}"
|
||||||
|
return self._get(endpoint)
|
||||||
|
|
||||||
def search(self, query_string: str):
|
def search(self, query_string: str):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ sys.path.append(connector_dir)
|
|||||||
# Note: backend.app needs to be importable. If backend is a package.
|
# Note: backend.app needs to be importable. If backend is a package.
|
||||||
try:
|
try:
|
||||||
from backend.app import app, get_db
|
from backend.app import app, get_db
|
||||||
from backend.database import Base, Industry, Persona, MarketingMatrix, JobRoleMapping, Company, Contact, init_db
|
from backend.database import Base, Industry, Persona, MarketingMatrix, JobRolePattern, Company, Contact, init_db
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Try alternate import if running from root
|
# Try alternate import if running from root
|
||||||
sys.path.append(os.path.abspath("company-explorer"))
|
sys.path.append(os.path.abspath("company-explorer"))
|
||||||
from backend.app import app, get_db
|
from backend.app import app, get_db
|
||||||
from backend.database import Base, Industry, Persona, MarketingMatrix, JobRoleMapping, Company, Contact, init_db
|
from backend.database import Base, Industry, Persona, MarketingMatrix, JobRolePattern, Company, Contact, init_db
|
||||||
|
|
||||||
# Import Worker Logic
|
# Import Worker Logic
|
||||||
from worker import process_job
|
from worker import process_job
|
||||||
@@ -56,10 +56,10 @@ class MockSuperOfficeClient:
|
|||||||
self.contacts = {} # id -> data
|
self.contacts = {} # id -> data
|
||||||
self.persons = {} # id -> data
|
self.persons = {} # id -> data
|
||||||
|
|
||||||
def get_contact(self, contact_id):
|
def get_contact(self, contact_id, select=None):
|
||||||
return self.contacts.get(int(contact_id))
|
return self.contacts.get(int(contact_id))
|
||||||
|
|
||||||
def get_person(self, person_id):
|
def get_person(self, person_id, select=None):
|
||||||
return self.persons.get(int(person_id))
|
return self.persons.get(int(person_id))
|
||||||
|
|
||||||
def update_entity_udfs(self, entity_id, entity_type, udfs):
|
def update_entity_udfs(self, entity_id, entity_type, udfs):
|
||||||
@@ -152,7 +152,7 @@ class TestE2EFlow(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
db.add(matrix2)
|
db.add(matrix2)
|
||||||
|
|
||||||
mapping = JobRoleMapping(pattern="%Head of Operations%", role="Operativer Entscheider")
|
mapping = JobRolePattern(pattern_value="Head of Operations", role="Operativer Entscheider", pattern_type="exact")
|
||||||
db.add(mapping)
|
db.add(mapping)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -30,25 +30,48 @@ def process_job(job, so_client: SuperOfficeClient):
|
|||||||
contact_id = None
|
contact_id = None
|
||||||
job_title = payload.get("JobTitle")
|
job_title = payload.get("JobTitle")
|
||||||
|
|
||||||
|
# Try getting IDs from FieldValues (more reliable for Webhooks)
|
||||||
|
field_values = payload.get("FieldValues", {})
|
||||||
|
if "person_id" in field_values:
|
||||||
|
person_id = int(field_values["person_id"])
|
||||||
|
if "contact_id" in field_values:
|
||||||
|
contact_id = int(field_values["contact_id"])
|
||||||
|
if "title" in field_values and not job_title:
|
||||||
|
job_title = field_values["title"]
|
||||||
|
|
||||||
|
# Fallback to older payload structure if not found
|
||||||
|
if not person_id:
|
||||||
if "PersonId" in payload:
|
if "PersonId" in payload:
|
||||||
person_id = int(payload["PersonId"])
|
person_id = int(payload["PersonId"])
|
||||||
elif "PrimaryKey" in payload and "person" in event_low:
|
elif "PrimaryKey" in payload and "person" in event_low:
|
||||||
person_id = int(payload["PrimaryKey"])
|
person_id = int(payload["PrimaryKey"])
|
||||||
|
|
||||||
|
if not contact_id:
|
||||||
if "ContactId" in payload:
|
if "ContactId" in payload:
|
||||||
contact_id = int(payload["ContactId"])
|
contact_id = int(payload["ContactId"])
|
||||||
elif "PrimaryKey" in payload and "contact" in event_low:
|
elif "PrimaryKey" in payload and "contact" in event_low:
|
||||||
contact_id = int(payload["PrimaryKey"])
|
contact_id = int(payload["PrimaryKey"])
|
||||||
|
|
||||||
# Fallback/Deep Lookup & Fetch JobTitle if missing
|
# Fallback/Deep Lookup & Fetch JobTitle if missing
|
||||||
if person_id:
|
# Only fetch if we are missing critical info AND have a person_id
|
||||||
|
if person_id and (not job_title or not contact_id):
|
||||||
try:
|
try:
|
||||||
person_details = so_client.get_person(person_id)
|
person_details = so_client.get_person(
|
||||||
|
person_id,
|
||||||
|
select=["JobTitle", "Title", "Contact/ContactId", "FirstName", "LastName", "UserDefinedFields", "Position"]
|
||||||
|
)
|
||||||
if person_details:
|
if person_details:
|
||||||
if not job_title:
|
if not job_title:
|
||||||
job_title = person_details.get("JobTitle") or person_details.get("Title")
|
job_title = person_details.get("JobTitle") or person_details.get("Title")
|
||||||
if not contact_id and "Contact" in person_details:
|
|
||||||
contact_id = person_details["Contact"].get("ContactId")
|
# Robust extraction of ContactId
|
||||||
|
if not contact_id:
|
||||||
|
contact_obj = person_details.get("Contact")
|
||||||
|
if contact_obj and isinstance(contact_obj, dict):
|
||||||
|
contact_id = contact_obj.get("ContactId")
|
||||||
|
elif "ContactId" in person_details: # Sometimes flat
|
||||||
|
contact_id = person_details.get("ContactId")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to fetch person details for {person_id}: {e}")
|
logger.warning(f"Failed to fetch person details for {person_id}: {e}")
|
||||||
|
|
||||||
@@ -96,7 +119,7 @@ def process_job(job, so_client: SuperOfficeClient):
|
|||||||
if "contact" in event_low and not person_id:
|
if "contact" in event_low and not person_id:
|
||||||
logger.info(f"Company event detected. Triggering cascade for all persons of Contact {contact_id}.")
|
logger.info(f"Company event detected. Triggering cascade for all persons of Contact {contact_id}.")
|
||||||
try:
|
try:
|
||||||
persons = so_client.search(f"Person?$filter=contact/contactId eq {contact_id}")
|
persons = so_client.search(f"Person?$select=PersonId&$filter=contact/contactId eq {contact_id}")
|
||||||
if persons:
|
if persons:
|
||||||
q = JobQueue()
|
q = JobQueue()
|
||||||
for p in persons:
|
for p in persons:
|
||||||
@@ -114,7 +137,10 @@ def process_job(job, so_client: SuperOfficeClient):
|
|||||||
contact_details = None
|
contact_details = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
contact_details = so_client.get_contact(contact_id)
|
contact_details = so_client.get_contact(
|
||||||
|
contact_id,
|
||||||
|
select=["Name", "UrlAddress", "Urls", "UserDefinedFields", "Address", "OrgNr"]
|
||||||
|
)
|
||||||
if not contact_details:
|
if not contact_details:
|
||||||
raise ValueError(f"Contact {contact_id} not found (API returned None)")
|
raise ValueError(f"Contact {contact_id} not found (API returned None)")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user