279 lines
8.4 KiB
Python
279 lines
8.4 KiB
Python
import string
|
|
from collections.abc import Callable
|
|
from typing import Any, cast
|
|
|
|
from arcade_salesforce.constants import ASSOCIATION_REFERENCE_FIELDS, GLOBALLY_IGNORED_FIELDS
|
|
from arcade_salesforce.enums import SalesforceObject
|
|
|
|
|
|
def remove_fields_globally_ignored(clean_func: Callable[[dict], dict]) -> Callable[[dict], dict]:
|
|
"""Remove fields that are irrelevant for our tools' responses.
|
|
|
|
The main purpose of cleaning and removing unnecessary fields from data is to make our
|
|
responses more concise and save on LLM tokens/context window.
|
|
"""
|
|
|
|
def global_cleaner(data: dict) -> dict:
|
|
cleaned_data = {}
|
|
for key, value in data.items():
|
|
if key in GLOBALLY_IGNORED_FIELDS or value is None:
|
|
continue
|
|
|
|
if isinstance(value, dict):
|
|
cleaned_data[key] = global_cleaner(value)
|
|
|
|
elif isinstance(value, list | tuple | set):
|
|
cleaned_items = []
|
|
for item in value:
|
|
if isinstance(item, dict):
|
|
cleaned_items.append(global_cleaner(item))
|
|
else:
|
|
cleaned_items.append(item)
|
|
cleaned_data[key] = cleaned_items # type: ignore[assignment]
|
|
else:
|
|
cleaned_data[key] = value
|
|
return cleaned_data
|
|
|
|
def wrapper(data: dict) -> dict:
|
|
return global_cleaner(clean_func(data))
|
|
|
|
return wrapper
|
|
|
|
|
|
def clean_object_data(data: dict) -> dict:
|
|
"""Clean Salesforce object dictionary based on the object type."""
|
|
|
|
obj_type = data["attributes"]["type"]
|
|
if obj_type == SalesforceObject.ACCOUNT.value:
|
|
return clean_account_data(data)
|
|
elif obj_type == SalesforceObject.CONTACT.value:
|
|
return clean_contact_data(data)
|
|
elif obj_type == SalesforceObject.LEAD.value:
|
|
return clean_lead_data(data)
|
|
elif obj_type == SalesforceObject.NOTE.value:
|
|
return clean_note_data(data)
|
|
elif obj_type == SalesforceObject.TASK.value:
|
|
return clean_task_data(data)
|
|
elif obj_type == SalesforceObject.USER.value:
|
|
return clean_user_data(data)
|
|
raise ValueError(f"Unknown object type: '{obj_type}' in object: {data}")
|
|
|
|
|
|
@remove_fields_globally_ignored
|
|
def clean_account_data(data: dict) -> dict:
|
|
data["AccountType"] = data["Type"]
|
|
del data["Type"]
|
|
data["ObjectType"] = SalesforceObject.ACCOUNT.value
|
|
ignore_fields = [
|
|
"BillingCity",
|
|
"BillingCountry",
|
|
"BillingCountryCode",
|
|
"BillingPostalCode",
|
|
"BillingState",
|
|
"BillingStateCode",
|
|
"BillingStreet",
|
|
"LastActivityDate",
|
|
"ShippingCity",
|
|
"ShippingCountry",
|
|
"ShippingCountryCode",
|
|
"ShippingPostalCode",
|
|
"ShippingState",
|
|
"ShippingStateCode",
|
|
"ShippingStreet",
|
|
]
|
|
return {k: v for k, v in data.items() if v is not None and k not in ignore_fields}
|
|
|
|
|
|
@remove_fields_globally_ignored
|
|
def clean_contact_data(data: dict) -> dict:
|
|
data["ObjectType"] = SalesforceObject.CONTACT.value
|
|
ignore_fields = ["IsEmailBounced", "IsPriorityRecord"]
|
|
return {k: v for k, v in data.items() if v is not None and k not in ignore_fields}
|
|
|
|
|
|
@remove_fields_globally_ignored
|
|
def clean_lead_data(data: dict) -> dict:
|
|
data["ObjectType"] = SalesforceObject.LEAD.value
|
|
return data
|
|
|
|
|
|
@remove_fields_globally_ignored
|
|
def clean_opportunity_data(data: dict) -> dict:
|
|
data["ObjectType"] = SalesforceObject.OPPORTUNITY.value
|
|
data["Amount"] = {
|
|
"Value": data["Amount"],
|
|
"ClosingProbability": data["Probability"]
|
|
if not isinstance(data["Probability"], int | float)
|
|
else data["Probability"] / 100,
|
|
"ExpectedRevenue": data["ExpectedRevenue"],
|
|
}
|
|
del data["Probability"]
|
|
del data["ExpectedRevenue"]
|
|
return data
|
|
|
|
|
|
@remove_fields_globally_ignored
|
|
def clean_note_data(data: dict) -> dict:
|
|
data["ObjectType"] = SalesforceObject.NOTE.value
|
|
return data
|
|
|
|
|
|
@remove_fields_globally_ignored
|
|
def clean_task_data(data: dict) -> dict:
|
|
data["ObjectType"] = data["TaskSubtype"]
|
|
data["AssociatedToWhom"] = data["WhoId"]
|
|
ignore_fields = [
|
|
"IsArchived",
|
|
"IsClosed",
|
|
"IsDeleted",
|
|
"IsHighPriority",
|
|
"IsRecurrence",
|
|
"IsReminderSet",
|
|
"TaskSubtype",
|
|
"WhoId",
|
|
]
|
|
|
|
if data["ObjectType"] == SalesforceObject.EMAIL.value:
|
|
data["Email"] = format_email(data["Description"])
|
|
del data["ActivityDate"]
|
|
del data["CompletedDateTime"]
|
|
del data["Description"]
|
|
del data["Subject"]
|
|
del data["OwnerId"]
|
|
del data["Priority"]
|
|
del data["Status"]
|
|
|
|
return {k: v for k, v in data.items() if v is not None and k not in ignore_fields}
|
|
|
|
|
|
@remove_fields_globally_ignored
|
|
def clean_user_data(data: dict) -> dict:
|
|
data["ObjectType"] = SalesforceObject.USER.value
|
|
ignore_fields = [
|
|
"attributes",
|
|
"CleanStatus",
|
|
"LastReferencedDate",
|
|
"LastViewedDate",
|
|
"SystemModstamp",
|
|
]
|
|
return {k: v for k, v in data.items() if v is not None and k not in ignore_fields}
|
|
|
|
|
|
def get_ids_referenced(*data_objects: dict) -> set[str]:
|
|
"""Build a set of IDs referenced in a list of dictionaries.
|
|
|
|
Args:
|
|
data_objects: The list of dictionaries to search for referenced IDs.
|
|
|
|
Returns:
|
|
The set of IDs referenced in the dictionaries.
|
|
"""
|
|
referenced_ids = set()
|
|
for data in data_objects:
|
|
for field in ASSOCIATION_REFERENCE_FIELDS:
|
|
if field in data:
|
|
referenced_ids.add(data[field])
|
|
return referenced_ids
|
|
|
|
|
|
def expand_associations(data: dict, objects_by_id: dict) -> dict:
|
|
"""Expand a Salesforce object referenced by ID with metadata about the object.
|
|
|
|
Args:
|
|
data: The dictionary to expand.
|
|
objects_by_id: The dictionary of objects metadata by ID.
|
|
|
|
Returns:
|
|
The expanded dictionary.
|
|
"""
|
|
for field in ASSOCIATION_REFERENCE_FIELDS:
|
|
if field not in data:
|
|
continue
|
|
|
|
associated_object = objects_by_id.get(data[field])
|
|
|
|
if isinstance(associated_object, dict):
|
|
del data[field]
|
|
data[field.rstrip("Id")] = simplified_object_data(associated_object)
|
|
|
|
return data
|
|
|
|
|
|
def simplified_object_data(data: dict) -> dict:
|
|
return {
|
|
"Id": data["Id"],
|
|
"Name": data["Name"],
|
|
"ObjectType": get_object_type(data),
|
|
}
|
|
|
|
|
|
def get_object_type(data: dict) -> str:
|
|
obj_type = data.get("ObjectType") or data["attributes"]["type"]
|
|
return cast(str, obj_type)
|
|
|
|
|
|
def build_soql_query(query: str, **kwargs: Any) -> str:
|
|
"""Build a SOQL query with sanitized arguments.
|
|
|
|
Args:
|
|
query: The SOQL query to build.
|
|
**kwargs: The arguments to sanitize and format into the query.
|
|
|
|
Returns:
|
|
The SOQL query with the arguments formatted.
|
|
"""
|
|
return query.format(**{key: sanitize_soql_argument(value) for key, value in kwargs.items()})
|
|
|
|
|
|
def sanitize_soql_argument(value: Any) -> str:
|
|
"""Sanitize an argument for a SOQL query.
|
|
|
|
The goal is to prevent SQL injection, since Salesforce does not support parameterized queries.
|
|
This function will only accept ASCII letters and digits in the value.
|
|
|
|
Args:
|
|
value: The value to sanitize.
|
|
|
|
Returns:
|
|
The sanitized value.
|
|
"""
|
|
allowed_chars = string.ascii_letters + string.digits
|
|
if not isinstance(value, str):
|
|
value = str(value)
|
|
return "".join([char for char in value if char in allowed_chars])
|
|
|
|
|
|
def format_email(description: str) -> dict:
|
|
"""Turns a Salesforce email description string into a more readable dictionary.
|
|
|
|
Args:
|
|
description: The email description to format.
|
|
|
|
Returns:
|
|
The formatted email as a dictionary.
|
|
"""
|
|
email = {
|
|
"To": description.split("To:")[1].split("\n")[0].strip(),
|
|
"CC": description.split("CC:")[1].split("\n")[0].strip(),
|
|
"BCC": description.split("BCC:")[1].split("\n")[0].strip(),
|
|
"Attachment": description.split("Attachment:")[1].split("\n")[0].strip(),
|
|
"Subject": description.split("Subject:")[1].split("\n")[0].strip(),
|
|
"Body": description.split("Body:\n")[1].strip(),
|
|
}
|
|
|
|
if email["Attachment"] == "--none--":
|
|
del email["Attachment"]
|
|
|
|
return email
|
|
|
|
|
|
def remove_none_values(data: dict) -> dict:
|
|
"""Remove None values from a dictionary.
|
|
|
|
Args:
|
|
data: The dictionary to clean.
|
|
|
|
Returns:
|
|
The cleaned dictionary.
|
|
"""
|
|
return {k: v for k, v in data.items() if v is not None}
|