Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1 @@
# Salesforce DX Project: Next Steps

Now that you’ve created a Salesforce DX project, what’s next? Here are some documentation resources to get you started.

## How Do You Plan to Deploy Your Changes?

Do you want to deploy a set of changes, or create a self-contained application? Choose a [development model](https://developer.salesforce.com/tools/vscode/en/user-guide/development-models).

## Configure Your Salesforce DX Project

The `sfdx-project.json` file contains useful configuration information for your project. See [Salesforce DX Project Configuration](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_config.htm) in the _Salesforce DX Developer Guide_ for details about this file.

## Read All About It

- [Salesforce Extensions Documentation](https://developer.salesforce.com/tools/vscode/)
- [Salesforce CLI Setup Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_intro.htm)
- [Salesforce DX Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_intro.htm)
- [Salesforce CLI Command Reference](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference.htm)
38 changes: 38 additions & 0 deletions force-app/main/default/classes/AI_Case_Routing.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* AI_Case_Routing is a Salesforce Apex class that extends AI_MsOpenAiCompletion and implements the IAI_ChatCompletion interface.
* It is designed to determine the most probable country of origin for a case by leveraging AI chat completion services,
* using the language and content of the case description as input. The class handles API communication, error management,
* and logging, providing a concrete implementation for routing cases based on AI-driven language analysis.
*/
public with sharing class AI_Case_Routing extends AI_MsOpenAiCompletion implements IAI_ChatCompletion{

public class AI_Case_RoutingException extends Exception {}

public Map<String,String> getChatCompletion(Map<String,Object> inputs){
//Guardrails
if (inputs == null || inputs.isEmpty() || !inputs.containsKey('settings') || !inputs.containsKey('prompt')) {
throw new AI_Case_RoutingException('Invalid input data for chat completion');
}
// Set context variables
super.init(inputs);
// Build request
HttpRequest request = super.buildHttpRequest();
Http http = new Http();
HttpResponse response;
try {
response = http.send(request);
// Parse the response and return the content attribute
String jsonResponse = super.parseResponse(response);
returnMap.put('probableCountry',jsonResponse);
// Create log
logAiEvent(response);
return returnMap;

} catch (Exception e) {
Logger.error('AI Case routing GEN AI Api error',e);
Logger.saveLog();
Logger.flushBuffer();
throw new AI_Case_RoutingException('Impossible to get chat completion from the API: '+e.getMessage());
}
}
}
5 changes: 5 additions & 0 deletions force-app/main/default/classes/AI_Case_Routing.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>64.0</apiVersion>
<status>Active</status>
</ApexClass>
68 changes: 68 additions & 0 deletions force-app/main/default/classes/AI_Case_RoutingTest.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
@isTest
public with sharing class AI_Case_RoutingTest {

private static PromptSetting__mdt getSettings(String psDeveloperName) {
return [SELECT Id, DeveloperName, Instructions__c, Named_Credential__c,
ApexClassName__c,Temprature__c, Top_P__c, Frequency_Penalty__c,
Presence_Penalty__c, Max_Tokens__c
FROM PromptSetting__mdt
WHERE DeveloperName = :psDeveloperName
LIMIT 1];
}

@isTest
static void testValidInputs() {
PromptSetting__mdt ps = getSettings('Country_identification');
Map<String, Object> inputs = new Map<String, Object>{
'settings' => ps,
'prompt' => 'This is a test prompt for Case Routing'
};

// Set mock callout class
String mockJsonResponse = '{"choices":[{"message":{"content":"FR"}}]}';
MockHttpResponses mockResponse = new MockHttpResponses(200, 'OK', mockJsonResponse);
mockResponse.setHeaders(new Map<String, String>{'Content-Type' => 'application/json'});
Test.setMock(HttpCalloutMock.class, mockResponse);
Test.startTest();
AI_Case_Routing aiCaseRouting = new AI_Case_Routing();
Map<String, String> result = aiCaseRouting.getChatCompletion(inputs);
Test.stopTest();
// Assert
Assert.areEqual('FR', result.get('probableCountry'), 'Invalid response from AI Case Routing from country determination');
}

@isTest
static void apiCallErrorTest() {
PromptSetting__mdt ps = getSettings('Country_identification');
Map<String, Object> inputs = new Map<String, Object>{
'settings' => ps,
'prompt' => 'This is a test prompt for Case Routing'
};

// Set mock callout class
//this String will cause a deserialization error
String invalidJsonResponse = '{"choices":[{"message":{"content":"Bad Request"}}}';
MockHttpResponses mockResponse = new MockHttpResponses(400, 'Bad Request', invalidJsonResponse);
mockResponse.setHeaders(new Map<String, String>{'Content-Type' => 'application/json'});
Test.setMock(HttpCalloutMock.class, mockResponse);

AI_Case_Routing aiCaseRouting = new AI_Case_Routing();
try {
Map<String, String> result = aiCaseRouting.getChatCompletion(inputs);
} catch (AI_Case_Routing.AI_Case_RoutingException e) {
// Assert that the exception is thrown
Assert.isTrue(e.getMessage().startsWith('Impossible to get chat completion from the API'), 'Incorrect error thrown');
}
}

@isTest
static void testInvalidInputs() {
// Test with null inputs
AI_Case_Routing aiCaseRouting = new AI_Case_Routing();
try {
aiCaseRouting.getChatCompletion(null);
} catch (AI_Case_Routing.AI_Case_RoutingException e) {
Assert.areEqual('Invalid input data for chat completion', e.getMessage(), 'Incorrect error thrown for null inputs');
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>64.0</apiVersion>
<status>Active</status>
</ApexClass>
38 changes: 38 additions & 0 deletions force-app/main/default/classes/AI_Case_Summary.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*This class is designed for the specific use case of case summarization.
* It extends the base functionality provided by the parent class, allowing for
* customized behavior tailored to summarizing Salesforce Case records.
* Additionally, it implements an interface to ensure that all required methods
* for case summarization are present and correctly structured.
*/
public with sharing class AI_Case_Summary extends AI_MsOpenAiCompletion implements IAI_ChatCompletion {

public class AI_Case_SummaryException extends Exception {}

public Map<String,String> getChatCompletion(Map<String,Object> inputs){
//Guardrails
if (inputs == null || inputs.isEmpty() || !inputs.containsKey('settings') || !inputs.containsKey('prompt')) {
throw new AI_Case_SummaryException('Invalid input data for chat completion');
}
// Set context variables
super.init(inputs);
// Build request
HttpRequest request = super.buildHttpRequest();
Http http = new Http();
HttpResponse response;
try {
response = http.send(request);
// Parse the response and return the content attribute
String jsonResponse = super.parseResponse(response);
returnMap.put('caseSummary',jsonResponse);
// Create log
logAiEvent(response);
return returnMap;

} catch (Exception e) {
Logger.error('AI Case Summarry GEN AI Api error',e);
Logger.saveLog();
Logger.flushBuffer();
throw new AI_Case_SummaryException('Impossible to get chat completion from the API: '+e.getMessage());
}
}
}
5 changes: 5 additions & 0 deletions force-app/main/default/classes/AI_Case_Summary.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>64.0</apiVersion>
<status>Active</status>
</ApexClass>
70 changes: 70 additions & 0 deletions force-app/main/default/classes/AI_Case_SummaryTest.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
@isTest
public with sharing class AI_Case_SummaryTest {


private static PromptSetting__mdt getSettings(String psDeveloperName) {
return [SELECT Id, DeveloperName, Instructions__c, Named_Credential__c,
ApexClassName__c,Temprature__c, Top_P__c, Frequency_Penalty__c,
Presence_Penalty__c, Max_Tokens__c
FROM PromptSetting__mdt
WHERE DeveloperName = :psDeveloperName
LIMIT 1];
}

@isTest
static void testValidInputs() {
PromptSetting__mdt ps = getSettings('Case_Summary_Comprehensive');
Map<String, Object> inputs = new Map<String, Object>{
'settings' => ps,
'prompt' => 'This is a test prompt for Case Summary'
};

// Set mock callout class
String mockJsonResponse = '{"choices":[{"message":{"content":"mockResponse"}}]}';
MockHttpResponses mockResponse = new MockHttpResponses(200, 'OK', mockJsonResponse);
mockResponse.setHeaders(new Map<String, String>{'Content-Type' => 'application/json'});
Test.setMock(HttpCalloutMock.class, mockResponse);

// Act
AI_Case_Summary aiCaseSummary = new AI_Case_Summary();
Map<String, String> result = aiCaseSummary.getChatCompletion(inputs);

// Assert
Assert.areEqual('mockResponse', result.get('caseSummary'), 'Invalid response from AI Case Summary comprehensive');
}

@isTest
static void apiCallErrorTest() {
PromptSetting__mdt ps = getSettings('Case_Summary_Comprehensive');
Map<String, Object> inputs = new Map<String, Object>{
'settings' => ps,
'prompt' => 'This is a test prompt for Case Summary'
};

// Set mock callout class
//this String will cause a deserialization error
String invalidJsonResponse = '{"choices":[{"message":{"content":"Bad Request"}}}';
MockHttpResponses mockResponse = new MockHttpResponses(400, 'Bad Request', invalidJsonResponse);
mockResponse.setHeaders(new Map<String, String>{'Content-Type' => 'application/json'});
Test.setMock(HttpCalloutMock.class, mockResponse);

AI_Case_Summary aiCaseSummary = new AI_Case_Summary();
try {
Map<String, String> result = aiCaseSummary.getChatCompletion(inputs);
} catch (AI_Case_Summary.AI_Case_SummaryException e) {
// Assert that the exception is thrown
Assert.isTrue(e.getMessage().startsWith('Impossible to get chat completion from the API'), 'Incorrect error thrown');
}
}

@isTest
static void testInvalidInputs() {
// Test with null inputs
AI_Case_Summary aiCaseSummary = new AI_Case_Summary();
try {
aiCaseSummary.getChatCompletion(null);
} catch (AI_Case_Summary.AI_Case_SummaryException e) {
Assert.areEqual('Invalid input data for chat completion', e.getMessage(), 'Incorrect error thrown for null inputs');
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>64.0</apiVersion>
<status>Active</status>
</ApexClass>
73 changes: 73 additions & 0 deletions force-app/main/default/classes/AI_ContactJobTitle.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*current implementation is not meant to be invoked from Flow but from apex trigger
* @summary
* This class extends the AI_MsOpenAiCompletion abstract class and implements the IAI_ChatCompletion interface.
* It is designed to determine and assign the Marketing Job Title for Contact records by leveraging the OpenAI API.
* The class validates AI-generated job titles against the allowed picklist values and ensures only valid titles
* are returned for each Contact.
*/
public with sharing class AI_ContactJobTitle extends AI_MsOpenAiCompletion implements IAI_ChatCompletion {

private Boolean errorLogged = false;
public List<String> allowedContactJobTitles;

public List<String> getAllowedContactJobTitles() {
if (allowedContactJobTitles == null) {
allowedContactJobTitles = new List<String>();
Schema.DescribeFieldResult fieldDescribe = Contact.Marketing_Job_Title__c.getDescribe();
List<Schema.PicklistEntry> pickEntryList = fieldDescribe.getPicklistValues();
for (Schema.PicklistEntry pickEntry : pickEntryList) {
allowedContactJobTitles.add(pickEntry.getValue());
}
}
return allowedContactJobTitles;
}


public Map<String,String> getChatCompletion(Map<String,Object> inputs){
//Guardrails
if (inputs == null || inputs.isEmpty() || !inputs.containsKey('settings') || !inputs.containsKey('prompt')) {
Logger.error('AI Contact Job Title error: Invalid input data for AI chat completion');
Logger.debug('Inputs: ' + inputs);
Logger.saveLog();
Logger.flushBuffer();
}
//Set context variables
super.init(inputs);
HttpRequest request = super.buildHttpRequest();
Http http = new Http();
HttpResponse response;
try {
response = http.send(request);
// Parse the response and return the content attribute
String jsonResponse = super.parseResponse(response);
//expected output is list of {"SF Contact Id": "jobTitle"}
//verify that the Id is a valid Contact Id and the job title returned by AI is a valid picklist value as well
Map<String, Object> parsedResponse = (Map<String, Object>) JSON.deserializeUntyped(jsonResponse);
List<String> validContactJobTitles = getAllowedContactJobTitles();
Set<String> contIdSet = parsedResponse.keySet();
for(String key:contIdSet){
Id contId = (Id) key;
String jobTitle = (String) parsedResponse.get(key);
if(contId.getSobjectType() == Contact.SObjectType && validContactJobTitles.contains(jobTitle)){
returnMap.put(key, jobTitle);
} else {
Logger.error('AI Contact Job Title error: Invalid Contact Id or Invalid Job Title');
Logger.debug('Id: ' + key);
Logger.debug('Job Title: ' + parsedResponse.get(key));
errorLogged = true;
}
}
if(errorLogged){
Logger.saveLog();
Logger.flushBuffer();
}
// Create log
logAiEvent(response);
} catch (Exception e) {
Logger.error('AI Contact job title GEN AI Api error',e);
Logger.saveLog();
Logger.flushBuffer();
}
return returnMap;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>64.0</apiVersion>
<status>Active</status>
</ApexClass>
50 changes: 50 additions & 0 deletions force-app/main/default/classes/AI_ContactJobTitleTest.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
@isTest
public with sharing class AI_ContactJobTitleTest {

@TestSetup
static void makeData(){
Contact cont = (Contact) new SObjectBuilder(Contact.SObjectType)
.put(Contact.Title, 'Tester')
.create().getRecord();
}

static Contact createContactWithJobTitle(){
return (Contact) new SObjectBuilder(Contact.SObjectType)
.put(Contact.Title, 'Purchasing')
.create().getRecord();
}

@isTest
static void insertContactWithJobTitleTest(){
//this test method does not contain assertion because the Mock response should contain the Contact Id
//but the Id is not known beforehand so we cannot assert the Marketing title is updated asynchronously
//The mock response contains teh Contact Id of a nexisting contact in the database
//the test method updateContactJobTitleTest will contain the assertion
Contact con = [select Id from Contact limit 1];
String conId = con.Id;
String aiResponse = '{"choices":[{"message":{"content":"{\\"' + conId + '\\":\\"Purchasing Manager\\"}"}}]}';
MockHttpResponses mockResponse = new MockHttpResponses(200, 'OK', aiResponse);
Test.startTest();
ContactTriggerHandler.forceExecuteTest = true;
Contact objContact = createContactWithJobTitle();
Test.stopTest();
}


@isTest
static void updateContactJobTitleTest(){
Contact objContact = [select Id, Title from Contact limit 1];
String conId = objContact.Id;
String aiResponse = '{"choices":[{"message":{"content":"{\\"' + conId + '\\":\\"Purchasing Manager\\"}"}}]}';
MockHttpResponses mockResponse = new MockHttpResponses(200, 'OK', aiResponse);
objContact.Title = 'Purchase';
Test.setMock(HttpCalloutMock.class, mockResponse);
Test.startTest();
ContactTriggerHandler.forceExecuteTest = true;
update objContact;
ContactTriggerHandler.forceExecuteTest = false;
Test.stopTest();
Contact updatedContact = [Select id,Marketing_Job_Title__c from contact where Id = :objContact.Id];
Assert.areEqual('Purchasing Manager', updatedContact.Marketing_Job_Title__c, 'Contact job title did not updated');
}
}
Loading