Saturday 11 May 2024

Formatted Output from Copilot Custom Actions


Image generated by DALL-E 3 based on a prompt from Bob Buzzard

Introduction

If you've seen any of the demos of Einstein Copilot, you'll notice that sometimes the responses are nicely formatted using the Lightning Design System, while other times they are simply text on a darker background - e.g. from the TrailblazerDX keynote, the pipeline information is text:


While the contacts are shown as a formatted list, with fields specific to the solution:


This isn't particularly well documented, so it's a matter of trial and error to figure out what works and what doesn't.

Text Responses

Text responses came out the same regardless of what I tried - the text that I return appears on a dark background. If I format it as JSON or CSV, it comes out in JSON or CSV format, even if I included instructions to display as a list of label/value pairs. 

The same goes for HTML markup - it's taken as text and shown to the user. Using \n for a line break works, but aside from that what you return from your custom action is what you see on the screen.

Custom Class

Returning custom class instances introduce a little more formatting. You can only return a single "value" from your custom action, but this can contain a list of custom class instances and Copilot will render each of the properties in it's own "text box". In the example below I return a single instance of my custom class Output, but this contains a list of instances another custom class. These in turn which contain the fields from Task records (as using the Tasks directly throws errors) :

public class Output
{
    @InvocableVariable
    public List<CustomRec> recs;
}

public class CustomRec
{
    @InvocableVariable
    public String ident;
        
    @InvocableVariable
    public String subject;
        
    @InvocableVariable
    public String description;
        
    @InvocableVariable
    public String activityDate;

    public CustomRec(Task task)
    {
        this.ident=(null!=task.id?'ID : ' + task.Id:null);
        this.subject=(null!=task.Subject?'Subject: ' + task.Subject:null);
        this.description=(null!=task.Description?'Description :\n' + task.Description:null);
        this.activityDate=(null!=task.activityDate?'Due Date : ' + task.activityDate:null);
    }
}

Copilot displays the properties from each record, although there isn't any separation between records. There's also a wrinkle in there I wasn't expecting - the properties are displayed in alphabetical order : activityDate, description, ident, subject. While unexpected, this does give me a way to order the properties if I need to:


sObject Records

As before, I can only return a single item from a custom action, so if I want to send back a list of records I have to wrap them in an containing class:

public class Output
{
    @InvocableVariable
    public List<Opportunity> opportunities;

    public Output()
    {
        opportunities=new List<Opportunity>();
    }
}

And this comes out very nicely, with a card for each record:


sObject Records and more!

This is the one that I'm most pleased about - it allows me to display records and some commentary about each record, while still retaining the Lightning Design System formatting for the record itself.

Once again I'm returning a custom class containing a list of other custom classes, but this time it's a wrapper class containing an sObject record and some additional information:

public class Output
{
    @InvocableVariable
    public List<OpportunityWrapper> opportunities;

    public Output()
    {
        opportunities=new List<OpportunityWrapper>();
    }
}

public class OpportunityWrapper
{
    @InvocableVariable
    public Opportunity opp;

    @InvocableVariable
    public String message;

    @InvocableVariable
    public String trailer;
        
    @InvocableVariable
    public String zzzSeparator='-----------------------------';

    public OpportunityWrapper(Opportunity opp, String message, String trailer)
    {
        this.opp=opp;
        this.message=message;
        this.trailer=trailer;
    }
}

I really didn't expect this to work, but it did!


I've taken advantage of the fact that the properties are displayed in alphabetical order to display a message about how near the close date is, then the record, then some commission information, followed by a rather ugly separator. I think if I was going to use this in production I'd have all the message above or below the record so that I didn't have to put a clunky separator in, but I wanted to see if I above and below would work.

Conclusion

I don't think this is too bad, given that Copilot is at its heart a text based tool. Hopefully in time we'll be able to apply more formatting to text output, but at least sObject records are styled for the Lightning Experience. Worst case, we all end up polluting our Salesforce org with a bunch of fake records that are just used as carriers for Copilot responses! 

Related Posts



Sunday 5 May 2024

Einstein Copilot - AI + (most of your) Data + CRM?

Image created by DALL-E 3 based on a prompt from Bob Buzzard

Introduction

Einstein Copilot from Salesforce went GA around a week ago (25th April 2024, for future readers), which changes my attitude to it. When things are in preview/pilot/beta, I'll take a bit of additional effort tracking down issues if it means I get my hands on things earlier. Once a feature goes GA though, the gloves are off - real money is changing hands to use these features so we hold them to a higher standard.

As the title of this post suggests, my first disappointment post-GA was that I can't use all my data across Generative AI - specifically Tasks and Events, part of the Salesforce Activities feature. This won't come as a surprise to old hands in the Salesforce ecosystem - hands up those who remember using text fields to capture IDs because custom lookups weren't available! More recently, they still aren't supported by the User Interface API, which means some of the standard LWC functionality won't work for them, and it's slightly concerning that an idea to add this capability has been open for 3+ years without even a comment from Salesforce. Yes, Activities are really Tasks, Events and Calendars on each others shoulders in a long overcoat, but this was the choice Salesforce made and they need to own it

There are workarounds for most areas of the platform where support for Activities is not as complete as it could be, but that isn't the case for the first issue that I encountered.

Prompt Template Parameters

Activities can't be specified as parameters to Prompt Builder. The docs suggest they should be available, as they are standard objects:

My use case was I wanted to send an Event record to an LLM, which contained details of a meeting that had taken place with a customer. The record includes the intended outcome, notes from the meeting, and any comments from company attendees after they've had a short period of time to reflect.  The LLM would then recommend the best course of action based on the meeting.This would then be used to create a Copilot action, so my users didn't could focus on doing the next step rather than figuring it out.

Searching for Event in the resource picker gave me pause for thought:


Maybe it's under Activity/Activities? Sadly not:


For the sake of completeness I also checked for Task - same result:


Now this doesn't mean that Activities can't be used with Prompt Builder - I can access the Open Activities and Activity History if I specify another object like Opportunity as the resource:



As this is the only way to specify a record to a Prompt Template, there aren't any workarounds to handle this, or none that don't suck anyway.

I could pass the Who and What information to the template and try to figure out the Event - for example, if the Event was with a Contact to talk about an Opportunity, I could pass the IDs for those in and use Apex/Flow grounding to retrieve the Events that match these IDs and hope I can figure out which one the user is interested in. If there are a number of them, or the user doesn't want something simple like the last one, I'm highly likely to pick the wrong one and give them unwanted advice about it.

I could make the users populate a lookup to a custom object and then pass the ID of that into the Prompt Template. The downside to this is my users have to do extra work in order to make the AI assistant useful - the exact opposite of how it's supposed to work. I'd also have to do a data fix to create custom object records for the recent history of Events.

Rendering Lists of Activities

One of the nice features of Copilot is if my custom action returns a list of sobjects in the correct format, it will apply Lightning Design System formatting:


So I decided to create a custom action that retrieves the Tasks that I should be focusing on - this has a bunch of rules in the action to determine which Tasks are actually important, as anyone who creates a Task for me will naturally mark it as Urgent and High Priority.

I'm able to retrieve the list of Tasks, but Copilot can't render them:


and check out that error message - Copilot fails to render the information provided and suggests you talk to your admin about it! I predict frustration all round!

Undeterred, I changed my code to return a list of custom Apex class instances that mirrored a task, so a property for Id, a property for Subject etc. Slightly better, in that it briefly rendered some text and then errored again.

After quite a bit of trial and error I was able to get a list of custom Apex class instances to render - I had to either remove the Id of the task, or put it into a String property that had any name other than Id. It looked awful by comparison, and I had no way to generate a link to any of the Tasks, as I'd had to put a false nose and dark glasses on the Id. The user could copy and paste the Id, but it seemed like the introduction of an AI assistant was once again leading to more work for the User, not less. So here there is a workaround, but it's not a great user experience.

My suspicion regarding this problem is that if Copilot can detect that there is either a Task or the Id of a Task in the data, it tries to pull some information about the Task sObject and then errors. Maybe via the User Interface API, given this is rendering on the screen, which doesn't support Tasks.

Conclusion

I'm really surprised at this gap, as Tasks and Events are pretty fundamental to Customer Relationship Management. It's either quite the oversight or, left out because the APIs don't support them in. the hope that nobody notices. It's all well and good having every web and commerce interaction with your customer in Data Cloud, but if you can't ask questions of your standard CRM objects, then we really aren't getting the AI + Data + CRM that we've been promised will change our lives.

Related Posts