arcade-mcp/toolkits/salesforce/arcade_salesforce/utils.py
Renato Byrro e1c293ee4f
Salesforce toolkit (#360)
Co-authored-by: Eric Gustin <34000337+EricGustin@users.noreply.github.com>
2025-04-18 14:28:27 -03:00

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}