tag:blogger.com,1999:blog-61401990228602803342024-03-19T08:48:24.269+00:00Bob Buzzard BlogBob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.comBlogger391125tag:blogger.com,1999:blog-6140199022860280334.post-2497928198405976532024-03-16T12:36:00.001+00:002024-03-16T12:36:17.099+00:00Einstein Prompt Grounding with Apex<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEieouwTxjK6mxS7zcspqE2rlghoHY1zx2Ultx4UfK5vmDZJ8L21o2aLgHIKPDDFuc3zgFRAfo_u99biNaHh2kOjb0pQaWs62gZhHxlwU-SU5GBDG4R1se6jdwheTMCk0l25voBpjull_ZJ1C9m6B_eqntSH0DEuk3ONRmBjagHeYjR1qaYZf8wyrYgj1lQV/s1024/DALL%C2%B7E%202024-03-16%2012.33.52%20-%20An%20image%20depicting%20a%20programmer%20creating%20a%20prompt%20for%20a%20large%20language%20model,%20including%20grounding%20merge%20fields.%20The%20scene%20shows%20a%20focused%20individual%20a%20copy.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1024" data-original-width="1024" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEieouwTxjK6mxS7zcspqE2rlghoHY1zx2Ultx4UfK5vmDZJ8L21o2aLgHIKPDDFuc3zgFRAfo_u99biNaHh2kOjb0pQaWs62gZhHxlwU-SU5GBDG4R1se6jdwheTMCk0l25voBpjull_ZJ1C9m6B_eqntSH0DEuk3ONRmBjagHeYjR1qaYZf8wyrYgj1lQV/s320/DALL%C2%B7E%202024-03-16%2012.33.52%20-%20An%20image%20depicting%20a%20programmer%20creating%20a%20prompt%20for%20a%20large%20language%20model,%20including%20grounding%20merge%20fields.%20The%20scene%20shows%20a%20focused%20individual%20a%20copy.jpg" width="320" /></a></div><div style="text-align: center;"><span style="font-size: x-small;"><i>Image generated by DALL-E 3 in response to a prompt from Bob Buzzard</i></span></div><h3 style="text-align: left;">Introduction</h3>
<p>
A couple of weeks ago I published my initial experience and thoughts on
<a href="https://bobbuzzard.blogspot.com/2024/03/hands-on-with-prompt-builder.html" target="_blank">Einstein Prompt Builder</a>, and one of my findings was that I couldn't get grounding with Apex to work,
even using the example code in the help. Thanks to some help from
<a href="https://www.linkedin.com/in/claudiomoraes/" target="_blank">Claudio Moraes</a>
from the Salesforce Partner team, that is no longer the case and I can pull
information into my prompt via an Apex class.
</p>
<h3 style="text-align: left;">Example Scenario</h3>
<p>
The prompt I've created is following up on a lead where we have a pretty good
idea what product they are interested in. Using standard prompting I can get
the user to select the product in questions, but I also want to include the
standard price to give the lead an idea of what it might cost them. I can pull
the PricebookEntries related list from the product into the prompt, but that
is all or nothing, whereas I just want the a single entry from the standard
price book.
</p>
<h3 style="text-align: left;">The Apex Code</h3>
<p>
In order to be considered for use in a Sales Email Prompt Template, I need a
method with the following signature:
</p>
<pre class="brush:java; toolbar: true">@InvocableMethod(label='Price for Lead'
description='Gets the price of a product for a lead'
CapabilityType='PromptTemplateType://einstein_gpt__salesEmail')
public static List<Response> getProductStandardPrices(List<Request> requests)
</pre>
<p>The key aspects of this are:</p>
<p><b>It must be an invocable method</b></p>
<p>
Which simply means it needs the @InvocableMethod annotation. Without this, the
Apex class will never be considered by Prompt Builder.
</p>
<p>
<b>It has to have the CapabilityType attribute on the @InvocableMethod
annotation</b>
</p>
<p>
This identifies the type of Prompt Template that the method can be used with.
As this is for a Sales Email, I specify :<br /><br />
PromptTemplateType://einstein_gpt__salesEmail
</p>
<p><b>It must take a list of Request class instances</b></p>
<p>Request is an inner class that I have to define, with the format:</p>
<pre class="brush: java; toolbar: true"> public class Request {
@InvocableVariable(required=true)
public User Sender;
@InvocableVariable(required=true)
public Lead Recipient;
@InvocableVariable(required=true)
public Product2 RelatedObject;
}
</pre>
<p>
The variable names must be exactly as shown here - including the
capitalisation (thanks Claudio!). These match the properties of the Prompt
Template.
</p>
<p><b>It must return a list of Response class instances</b></p>
<p>Again, Response is an inner class that I define, with the format:</p>
<pre class="brush:java; toolbar: true"> public class Response {
@InvocableVariable(required=true)
public String prompt;
}
</pre>
<p><br /></p>
<p>The body of the method itself is pretty straightforward:</p>
<pre class="brush: java; toolbar: true">@InvocableMethod(label='Price for Lead'
description='Gets the price of a product for a lead'
CapabilityType='PromptTemplateType://einstein_gpt__salesEmail')
public static List<Response> getProductStandardPrices(List<Request> requests)
{
Request req=requests[0];
Id productId=req.RelatedObject.Id;
Response resp=new Response();
PricebookEntry entry=[
select Id, UnitPrice, CurrencyISOCode from PricebookEntry
where Product2.Id=:productId
and Pricebook2.IsStandard = true];
resp.prompt=entry.CurrencyISOCode + ' ' + entry.UnitPrice;
return new List<Response>{resp};
}
</pre>
<p>
Note that while the method has to take a list of requests and return a list of
responses, there's only ever one element present.
</p>
<h3 style="text-align: left;">Putting it Together</h3>
<p>
If I deploy my class and it doesn't show up in Prompt Builder because I've got
the capitalisation wrong, it doesn't appear to be possible to recover it.
Instead, I have to delete the class and try the next attempt with a new one
(thanks again, Claudio!).
</p>
<p>
Once I have the correct breed of chicken and the right colour chalk for the
pentangle, Apex appears in the resource picker :
</p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWMwFEh-KBlF1Hfn663TGNEl4TlNklGF1xz3rTnc2PcCS2VYN61rACMlv-uVzIGeAtp9OlV_O-QXxmEJdPsMZL23Y659yarga51PZu_nkzKQJ5nWCev_441Mh0Ahlmo-Rlx3ZQeDtOyXc1ZTXYWGhcsoI0mjcpUkVbQcFjO8itWNBqH_FCfqud9qy48gzf/s582/Screenshot%202024-03-16%20at%2012.11.17.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="486" data-original-width="582" height="267" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWMwFEh-KBlF1Hfn663TGNEl4TlNklGF1xz3rTnc2PcCS2VYN61rACMlv-uVzIGeAtp9OlV_O-QXxmEJdPsMZL23Y659yarga51PZu_nkzKQJ5nWCev_441Mh0Ahlmo-Rlx3ZQeDtOyXc1ZTXYWGhcsoI0mjcpUkVbQcFjO8itWNBqH_FCfqud9qy48gzf/s320/Screenshot%202024-03-16%20at%2012.11.17.png" width="320" /></a>
</div>
<br />
<p>
And clicking into the Apex option shows me the name of my enclosing class and
the label attribute from the @InvocableMethod annotation :
</p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgC2NnaVrtWIUc2FQu0zlHLFbqSMzjdaIPyh7Z2UybtH46qXrMunLX64SeBmPm58jueM474xEDoldoHJ0ndjP20SaIt303HuJE24YQhJJ9jqj2-KFXftgOquxMFZOGoUBPr8BMkh7CVPGEjrJrVkf590uW63Qr0Cz4P1nx-vSRze1LyjBBMav63XY6l8rYz/s578/Screenshot%202024-03-16%20at%2012.11.24.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="268" data-original-width="578" height="148" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgC2NnaVrtWIUc2FQu0zlHLFbqSMzjdaIPyh7Z2UybtH46qXrMunLX64SeBmPm58jueM474xEDoldoHJ0ndjP20SaIt303HuJE24YQhJJ9jqj2-KFXftgOquxMFZOGoUBPr8BMkh7CVPGEjrJrVkf590uW63Qr0Cz4P1nx-vSRze1LyjBBMav63XY6l8rYz/s320/Screenshot%202024-03-16%20at%2012.11.24.png" width="320" /></a>
</div>
<p>
And selecting this adds it to my prompt, helpfully highlighted to show me that
it is a reference rather than plaintext :
</p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj7fQXTbgAjf8DZuMOmd_WCDtYHHSbD3FqRoTaGxzvqmMNIorT_sGF-qNVXCV2yYHrA1SKV9dFSxnHmdLlqszkl0cGs0JCrEmWi-Gs7huONMUXlxP3z16v4-7dAkTwmlwmrlZZNzecRGZnbglZonnr7480O6p9DxWPKF_oLJxylCEUF_R4CJGyqiGSOetXQ/s486/Screenshot%202024-03-16%20at%2012.11.42.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="80" data-original-width="486" height="53" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj7fQXTbgAjf8DZuMOmd_WCDtYHHSbD3FqRoTaGxzvqmMNIorT_sGF-qNVXCV2yYHrA1SKV9dFSxnHmdLlqszkl0cGs0JCrEmWi-Gs7huONMUXlxP3z16v4-7dAkTwmlwmrlZZNzecRGZnbglZonnr7480O6p9DxWPKF_oLJxylCEUF_R4CJGyqiGSOetXQ/s320/Screenshot%202024-03-16%20at%2012.11.42.png" width="320" /></a>
</div>
<h3 style="text-align: left;">The Prompt and Response</h3>
<div>
<p>
I've added this to a pretty simple prompt - I doubt it would make much
difference to a potential customer, but the point of this blog is to show
the Apex, not what makes a great prompt! Note that as I didn't ground the
prompt with any details of the user, they are expected to add that
themselves :
</p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzEbYGKhaGLwjZcbMbQLWWsrSPCi9PlrM-VMvpZrK9BbuQY7P6KRIswhndUmxbDObJe8RxDHQBt_yuZlBrrdCXIYeuWxiwTYBHwvPmns8egUJONjaSAvPGPfDpu6EHfVllSPNTquphAEPVfM64u7NU4FSSAWzKN9NFrvzfDE6pi0KGMlHOHEOg1GRFkCp_/s1398/Screenshot%202024-03-16%20at%2012.23.30.png" style="margin-left: 1em; margin-right: 1em;"><img border="1" data-original-height="314" data-original-width="1398" height="144" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzEbYGKhaGLwjZcbMbQLWWsrSPCi9PlrM-VMvpZrK9BbuQY7P6KRIswhndUmxbDObJe8RxDHQBt_yuZlBrrdCXIYeuWxiwTYBHwvPmns8egUJONjaSAvPGPfDpu6EHfVllSPNTquphAEPVfM64u7NU4FSSAWzKN9NFrvzfDE6pi0KGMlHOHEOg1GRFkCp_/w640-h144/Screenshot%202024-03-16%20at%2012.23.30.png" width="640" /></a>
</div>
<p><br /></p><p>and the response from the LLM:</p>
<p>
<span class="slds-float_left slds-p-right_xx-small"><b>Subject:</b></span>
</p>
<p class="subject" lwc-52boff8tlld="">
<span style="font-family: courier;">
Thank You for Your Interest in Solar Panels
</span>
</p>
<p>
<span lwc-52boff8tlld=""><b>Body:</b></span>
</p>
<p class="body" lwc-52boff8tlld="">
<span style="font-family: courier;"> Dear Lauren Reynolds, </span>
</p>
<p class="body" lwc-52boff8tlld="">
<span style="font-family: courier;">Thank you for your interest in our solar panels. We are pleased to share
the price with you. The cost for a single panel is USD 1725.00. Please
note that discounts are available for larger orders, but this price
already reflects an excellent value for such a high-quality
product. </span>
</p>
<p class="body" lwc-52boff8tlld="">
<span style="font-family: courier;">We hope this information is helpful to you, and we look forward to the
possibility of working with you. </span>
</p>
<p class="body" lwc-52boff8tlld="">
<span style="font-family: courier;">Should you have any further questions or require additional details,
please do not hesitate to reach out. </span>
</p>
<p class="body" lwc-52boff8tlld="">
<span style="font-family: courier;">Warm regards, <br />[Your Name] <br />Credera Sales Team</span>
</p>
<p><br /></p>
<h3 style="text-align: left;">More Information</h3>
<p></p>
<ul style="text-align: left;">
<li><a href="https://bobbuzzard.blogspot.com/2024/03/hands-on-with-prompt-builder.html" target="_blank">Hands On with Prompt Builder</a></li><li>
<a href="https://help.salesforce.com/s/articleView?language=en_US&id=sf.prompt_builder_add_apex_sales_email.htm&type=5" target="_blank">Salesforce Help for Prompt Builder</a>
</li>
<li>
<a href="https://admin.salesforce.com/blog/2024/the-ultimate-guide-to-prompt-builder-spring-24" target="_blank">Salesforce Admins Ultimate Guide to Prompt Builder</a>
</li>
</ul>
<p></p>
<div>
<a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a>
</div>
<p></p>
<div>
<div><br /></div>
<div>
<br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe>
</div>
</div>
<p><br /></p>
<p><br /></p>
</div>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-81056422768935481602024-03-10T18:27:00.001+00:002024-03-10T18:27:11.549+00:00Hands on with Salesforce Copilot<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjJ0vZJFdi-uk5kf0bXmriH1hq7XTeVQABZAEBo-uERK5gSuisE87PMCT7qcFIDkxeDMAlzh4FaKSKosisbSS75RJdu-T7395hwP76X70LSr1RtKOtRlWFYvLkcF-KTI-8e62-WAyU_5alSnztF9Z4jkzCfspEjJMUdaTIZmU7EMlOZ9OBFdbkHbG4nIAd2/s1024/DALL%C2%B7E%202024-03-10%2018.25.14%20-%20An%20image%20of%20a%20sales%20representative%20working%20with%20an%20AI%20copilot%20chatbot,%20where%20the%20chatbot%20interface%20occupies%20about%20a%20third%20of%20the%20screen%20and%20prominentl%20copy.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1024" data-original-width="1024" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjJ0vZJFdi-uk5kf0bXmriH1hq7XTeVQABZAEBo-uERK5gSuisE87PMCT7qcFIDkxeDMAlzh4FaKSKosisbSS75RJdu-T7395hwP76X70LSr1RtKOtRlWFYvLkcF-KTI-8e62-WAyU_5alSnztF9Z4jkzCfspEjJMUdaTIZmU7EMlOZ9OBFdbkHbG4nIAd2/s320/DALL%C2%B7E%202024-03-10%2018.25.14%20-%20An%20image%20of%20a%20sales%20representative%20working%20with%20an%20AI%20copilot%20chatbot,%20where%20the%20chatbot%20interface%20occupies%20about%20a%20third%20of%20the%20screen%20and%20prominentl%20copy.jpg" width="320" /></a></div><h3 style="text-align: center;"><span style="font-size: x-small;"><span style="font-weight: normal;"><i>Image generated by DALL-E 3 based on a prompt from Bob Buzzard</i></span></span></h3><h3 style="text-align: left;">Introduction
</h3>
<p>
Copilot from Salesforce entered beta at the end of Feb '24, without too much
of a build-up I think it's fair to say. Given how quiet the likes of Marc
Benioff were on the socials, I was half-expecting it to be delayed until
nearer to the Summer release. I'm very pleased that wasn't the case, but not
as pleased as I was to get access to it.
</p>
<p>Setup</p>
<p>
Much like
<a href="https://bobbuzzard.blogspot.com/2024/03/hands-on-with-prompt-builder.html" target="_blank">Prompt Builder</a>, enabling copilot is pretty simple :
</p>
<p></p>
<ul style="text-align: left;">
<li>
Go to Setup -> Einstein Generative AI -> Einstein Setup and
toggle it on
</li>
<li>
The go to Setup -> Einstein Generative AI -> Einstein Copilot Studio
(Beta) -> Einstein Copilots and toggle that on.
</li>
</ul>
<div>
Copilot is then enabled in your org. It's not active though - you have to head
into Builder and click the Activate button to see it in the user interface.
You can play around with it pretty well inside the Builder though, so don't
activate until you are happy.
</div>
<div><br /></div>
<h3 style="text-align: left;">Accessing</h3>
<div><br /></div>
<div>
Once you've activated your copilot, you'll see a new Enstein icon up at the
top right of the user interface - looks a bit Heisenberg-esque to me, which I
think is awesome:
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjOeB2abG4J7uRN-X4_D2tsZ52U7B5U6hqhqflJCBOvbLYC2HtgmCVHms-S4WUB3HA7pj_3DTK-0zAnZigHIdAIxNcp1bJeIDNu5DiaNP5b4EgvY6XGa0u7LtIhRQMF_QlMl0pDU8bM9gH9kHMS-XJVLJf1WbtnxABC4CwUnONM4sEZamvW7Pg74JRTvnSH/s648/Screenshot%202024-03-10%20at%2017.22.16.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="102" data-original-width="648" height="50" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjOeB2abG4J7uRN-X4_D2tsZ52U7B5U6hqhqflJCBOvbLYC2HtgmCVHms-S4WUB3HA7pj_3DTK-0zAnZigHIdAIxNcp1bJeIDNu5DiaNP5b4EgvY6XGa0u7LtIhRQMF_QlMl0pDU8bM9gH9kHMS-XJVLJf1WbtnxABC4CwUnONM4sEZamvW7Pg74JRTvnSH/s320/Screenshot%202024-03-10%20at%2017.22.16.png" width="320" /></a>
</div>
<br />
<div>
Clicking this opens copilot in a new sidebar, moving the current screen
contents over to the left rather than overlaying them - it's the little
things:
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3n6wfzS0j167XZEmR2vSVAaoXJwSLHrl996V2R9FEBaBKI0CZZM1O9maHNd58WUTBe1nD34LlZivZ1SjYdbSIC0SR07HwkM2mlXgNxUCpefXr8WYmC3Z1MFrWEZFEn_XrPCjOoruMM41lIOyewL9PGAiXJ40cIB1xEYp0_NFGbWhd4a3anr7l8T0alTku/s792/Screenshot%202024-03-10%20at%2017.27.36.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="706" data-original-width="792" height="285" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3n6wfzS0j167XZEmR2vSVAaoXJwSLHrl996V2R9FEBaBKI0CZZM1O9maHNd58WUTBe1nD34LlZivZ1SjYdbSIC0SR07HwkM2mlXgNxUCpefXr8WYmC3Z1MFrWEZFEn_XrPCjOoruMM41lIOyewL9PGAiXJ40cIB1xEYp0_NFGbWhd4a3anr7l8T0alTku/s320/Screenshot%202024-03-10%20at%2017.27.36.png" width="320" /></a>
</div>
<br />
<div>
One key point to note - copilot doesn't always have the context you are
working in - the screengrab above is copilot opening while I'm viewing an
opportunity. It's tempting to think that I can just refer to 'this
opportunity' and it will pick up the details from my current context/URL, but
that doesn't happen - instead it says 'Sure, which opportunity is this?"
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj9bFMHl7oS8rDE1UDMOENygpQ4AjcvhHqRshlTM_OcleAxocvlYtFTOrgKBc8GIVPFr9LM7DkN4HvFBgmuQfZaVxV6kvFnV3MrTLz-2-3KpxZRWqnspD4uyzfq0nhjLIgnUOiEf4v2vDArQZ0Uqwd6o6xxvnqMl4g-vZJH6ZY-Yk0nwTIXSzmlYyTWH0vC/s758/Screenshot%202024-03-10%20at%2017.29.16.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="490" data-original-width="758" height="207" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj9bFMHl7oS8rDE1UDMOENygpQ4AjcvhHqRshlTM_OcleAxocvlYtFTOrgKBc8GIVPFr9LM7DkN4HvFBgmuQfZaVxV6kvFnV3MrTLz-2-3KpxZRWqnspD4uyzfq0nhjLIgnUOiEf4v2vDArQZ0Uqwd6o6xxvnqMl4g-vZJH6ZY-Yk0nwTIXSzmlYyTWH0vC/s320/Screenshot%202024-03-10%20at%2017.29.16.png" width="320" /></a>
</div>
<br />
<div>
As an aside, it also really wants me to use the name of the record - if I try
the Id, it tells me that nothing was found in the query. This might seem
limited, but I think it'sactually working exactly as intended. There's an
action in this copilot configuration that retrieves a record and it requires
the record name. If I want to use Ids instead, I need a different action.
</div>
<div><br /></div>
<div>
Back to context though, if I click the Summarize Opportunity button, it does
know about the opportunity and gives me the details :
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmvtyKCUzzNJYeLEW0XAQywHVLsmZoGgr25ApydHOQ5XC-mFh7rmOgsQZFPn97The3AFX8CQSUHdQk2xuZQjP32Pwfd8BAw3r4wvL6brwaN3YKbyZN2WegWjtAFtuij2N7gfrkj9UaRgdJmnSIq-nvyWGC4Y_Rbgd6z5h2AeDHqvuZSaMZ0OJ0FSZ9gzIi/s740/Screenshot%202024-03-10%20at%2017.37.06.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="740" data-original-width="626" height="388" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmvtyKCUzzNJYeLEW0XAQywHVLsmZoGgr25ApydHOQ5XC-mFh7rmOgsQZFPn97The3AFX8CQSUHdQk2xuZQjP32Pwfd8BAw3r4wvL6brwaN3YKbyZN2WegWjtAFtuij2N7gfrkj9UaRgdJmnSIq-nvyWGC4Y_Rbgd6z5h2AeDHqvuZSaMZ0OJ0FSZ9gzIi/w329-h388/Screenshot%202024-03-10%20at%2017.37.06.png" width="329" /></a>
</div>
<br />
<h3 style="text-align: left;">Making a Request</h3>
<div><br /></div>
<div>
As always with generative AI, the quality of the response has a pretty direct
correlation with the quality of the prompt. Just vaguely asking for help
doesn't go well:
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhhs44oIJCZ33C36DPcwgt5cpnBZa79XFy_C6uyvQIz0G6rrS3RpmKDkT0EmIuuqt_L3QON_o_GfdsYmkaAAzJoQSZKeuLOP6o4gnsj3YYK2vax2BVdGuB6OxFuwaX1AgrfLipiM1FgeqNFEYZgOEFEbMvXG_QPXcpsrndzv-nN8hV1Vq9U64vs2FzJBxj/s620/Screenshot%202024-03-10%20at%2017.42.39.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="286" data-original-width="620" height="160" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhhs44oIJCZ33C36DPcwgt5cpnBZa79XFy_C6uyvQIz0G6rrS3RpmKDkT0EmIuuqt_L3QON_o_GfdsYmkaAAzJoQSZKeuLOP6o4gnsj3YYK2vax2BVdGuB6OxFuwaX1AgrfLipiM1FgeqNFEYZgOEFEbMvXG_QPXcpsrndzv-nN8hV1Vq9U64vs2FzJBxj/w347-h160/Screenshot%202024-03-10%20at%2017.42.39.png" width="347" /></a>
</div>
<br />
<div>
I then tried a request that I thought had enough detail, but in hindsight
lacked a little clarity:
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjC8nOVUYql9yDufZVef1LkuYFYE-j7eI5AekmJQFyfF8Xt6z2rJ1P2RNaCZb62vsdoa8tPgLul7giv6QMFPq-5e7AYnqzHC2TWvhleytgv_mirwjPnbaLVnIDK-w8MfyrmCEVYoSvV4NHF6zn_qs8wpISbKLFkNdR4tSVsSs5gHzdZDtqWhwaDxWqWXaO4/s614/Screenshot%202024-03-10%20at%2017.43.53.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="276" data-original-width="614" height="161" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjC8nOVUYql9yDufZVef1LkuYFYE-j7eI5AekmJQFyfF8Xt6z2rJ1P2RNaCZb62vsdoa8tPgLul7giv6QMFPq-5e7AYnqzHC2TWvhleytgv_mirwjPnbaLVnIDK-w8MfyrmCEVYoSvV4NHF6zn_qs8wpISbKLFkNdR4tSVsSs5gHzdZDtqWhwaDxWqWXaO4/w358-h161/Screenshot%202024-03-10%20at%2017.43.53.png" width="358" /></a>
</div>
<br />
<div>
Third time's the charm though, and now I have a few accounts to focus on:
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgpi0ygYJj_ueBkmo8TpJfuhxHpGAX3m4Sbm6KwVHAaQZAXpx23l2A_heVTOVnxpXbZcSUxjY6cc7ehNBrnOqpoa9l-BVRZCSFgkLN5aUonJ4bNPnyFiNNet8jA9FCBUI0L09NeDjn8P4HjiBqNGTCt7DGmlaCjIIINx9P_du5LKvm_SFr8UbmnuFpMhAK-/s804/Screenshot%202024-03-10%20at%2017.45.46.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="804" data-original-width="614" height="421" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgpi0ygYJj_ueBkmo8TpJfuhxHpGAX3m4Sbm6KwVHAaQZAXpx23l2A_heVTOVnxpXbZcSUxjY6cc7ehNBrnOqpoa9l-BVRZCSFgkLN5aUonJ4bNPnyFiNNet8jA9FCBUI0L09NeDjn8P4HjiBqNGTCt7DGmlaCjIIINx9P_du5LKvm_SFr8UbmnuFpMhAK-/w321-h421/Screenshot%202024-03-10%20at%2017.45.46.png" width="321" /></a>
</div>
<br />
<div><br /></div>
<h3 style="text-align: left;">Extending</h3>
<div><br /></div>
<div>
The standard functionality is all well and good, but I'm always interested in
how I can extend and customise. There was a bit of trial and error involved,
as the docs aren't perfect yet, but remember this is a beta feature.
</div>
<div><br /></div>
<div>
I was trying to think of something that wasn't in place already, and after a
short while it struck me - there's just not enough poetry in enterprise
software nowadays, so an action to write a haiku about an account was the
obvious, if not the only, choice.
</div>
<div><br /></div>
<div>
This will be a Prompt Template action, so the first job is to define the
prompt:
</div>
<div><br /></div>
<blockquote style="border: none; margin: 0px 0px 0px 40px; padding: 0px; text-align: left;">
<div>
<div>
<span style="font-family: courier;">You are a world renowned haiku writer, who has a side gig as a CRM
administrator.</span>
</div>
</div>
<div>
<div>
<span style="font-family: courier;">Write a haiku based for the account {!$Input:Account.Name} which
operates in the {!$Input:Account.Industry} industry. </span>
</div>
</div>
<div>
<div>
<span style="font-family: courier;">The haiku should not include any PII.</span>
</div>
</div>
</blockquote>
<div><br /></div>
<div>Then the copilot action: </div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmT96TWOQizC7_7QLLHJC-YpwNFVjU5Rgr-E8GTonAX5RISwlAras9mthOPH2TuLpYqZGbAquH56L4j8FPgSGE5o4BBgKcpp-nX-2G5Gwj8Rgih4dRoWQZydYqdgr80aE3tcIIKw4Az9yk8YKZxsGnGp-amq6em0p5N3QioerScrFpfU6AY9gLFMvzHsl-/s1532/Screenshot%202024-03-10%20at%2017.50.20.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1092" data-original-width="1532" height="475" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmT96TWOQizC7_7QLLHJC-YpwNFVjU5Rgr-E8GTonAX5RISwlAras9mthOPH2TuLpYqZGbAquH56L4j8FPgSGE5o4BBgKcpp-nX-2G5Gwj8Rgih4dRoWQZydYqdgr80aE3tcIIKw4Az9yk8YKZxsGnGp-amq6em0p5N3QioerScrFpfU6AY9gLFMvzHsl-/w669-h475/Screenshot%202024-03-10%20at%2017.50.20.png" width="669" /></a>
</div>
<div><br /></div>
The important parts of the action are :
<div><br /></div>
<div>
<ul style="text-align: left;">
<li>
<b>Copilot Action Instructions </b>- this is used by the AI to determine
whether an action is appropriate for a plan. The more detailed the better,
although in this case there probably won't be multiple actions competing
for the job of writing the haiku.
</li>
<li>
<b>Require user confirmation</b> - tick this if any data is going to be
changed.
</li>
<li>
<b>Account Instructions </b>- these are the instructions for the input
named <b>Account</b> that I've specified in the prompt. I've identified
which fields I'm interested in, the type, and that it's required. I'm not
asking the user to enter any data and there's no PII to mask as I'm just
using the Name and Industry fields, which is public information
</li>
<li>
<b>Prompt Response Instructions</b> - here I've defined what type of
response, but most importantly ticked <b>Show in conversation</b>. Without
this nothing will be returned to the user.
</li>
</ul>
<div>
Once this action is added to my copilot, I can add writing a haiku to a
request and get the user experience I've been craving:
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgbA8hY-2TQPg3utyO1lllgzQC5Onp9k9kXo6HqlaJW2kvbhl-hSM4610Tx0F0gbMeBuS3zufwd3h2OOyPQhqfNKVEbH3WU2s25RKr76BU6cGri9fW0fNHXGzlaHBx1SHUp5ENI7eVQ30TyY2wttbhMqwf8I-Ie4PT7T0234wn1jLfy1jJUzWCdILSfwYDU/s930/Screenshot%202024-02-27%20at%2018.15.49.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="828" data-original-width="930" height="328" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgbA8hY-2TQPg3utyO1lllgzQC5Onp9k9kXo6HqlaJW2kvbhl-hSM4610Tx0F0gbMeBuS3zufwd3h2OOyPQhqfNKVEbH3WU2s25RKr76BU6cGri9fW0fNHXGzlaHBx1SHUp5ENI7eVQ30TyY2wttbhMqwf8I-Ie4PT7T0234wn1jLfy1jJUzWCdILSfwYDU/w368-h328/Screenshot%202024-02-27%20at%2018.15.49.png" width="368" /></a>
</div>
<div class="separator" style="clear: both; text-align: center;"><br /></div><h3 style="text-align: left;">
More Information</h3>
<div><br /></div>
<div>
<ul style="text-align: left;">
<li>
<a href="https://help.salesforce.com/s/articleView?id=sf.copilot_intro.htm&type=5" target="_blank">Salesforce Help for Einstein Copilot</a>
</li>
<li>
<a href="https://trailhead.salesforce.com/content/learn/trails/build-ai-assistants-with-einstein-copilot" target="_blank">Get Started with Einstein Copilot Trail</a>
</li>
<li>
<a href="https://www.salesforce.com/plus/experience/trailblazerdx_2024/series/trailblazerdx_2024_highlights/episode/episode-s1e3" target="_blank">TrailblazerDX Keynote</a>
- some great demos in here!
</li>
<li>
<a href="https://bobbuzzard.blogspot.com/2024/03/hands-on-with-prompt-builder.html" target="_blank">Hands on with Prompt Builder</a>
</li>
</ul>
<p></p>
<div>
<a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a>
</div>
<p></p>
<div>
<div><br /></div>
<div>
<br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe>
</div>
</div>
<p><br /></p>
<p><br /></p>
</div>
<p></p>
</div>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-60540603467917303822024-03-02T16:13:00.000+00:002024-03-02T16:13:42.223+00:00Hands on with Prompt Builder<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiAVb4mKKlmrIYuIqa93tO7Wmnxz2GaJDXjhLkfW_iQtehn6VLcPUVClrWPra6v_jyTfJSriyt_ioNYSHkMptcFrEN72EcgcIKrAKAMXKPf_jmmfUj2QM4xXtg-wnaeK9JWlduqrNHsvhZKSOkUJ1rY6dhczB5zpJyawH7q77XtVxWaBSag9O_je6-oMS5M/s1024/DALL%C2%B7E%202024-03-02%2012.43.58%20-%20An%20image%20of%20a%20salesman%20whose%20communication%20abilities%20are%20being%20augmented%20by%20artificial%20intelligence.%20The%20salesman%20is%20depicted%20interacting%20with%20a%20custo%20copy.jpg" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1024" data-original-width="1024" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiAVb4mKKlmrIYuIqa93tO7Wmnxz2GaJDXjhLkfW_iQtehn6VLcPUVClrWPra6v_jyTfJSriyt_ioNYSHkMptcFrEN72EcgcIKrAKAMXKPf_jmmfUj2QM4xXtg-wnaeK9JWlduqrNHsvhZKSOkUJ1rY6dhczB5zpJyawH7q77XtVxWaBSag9O_je6-oMS5M/s320/DALL%C2%B7E%202024-03-02%2012.43.58%20-%20An%20image%20of%20a%20salesman%20whose%20communication%20abilities%20are%20being%20augmented%20by%20artificial%20intelligence.%20The%20salesman%20is%20depicted%20interacting%20with%20a%20custo%20copy.jpg" width="320" /></a></div><div style="text-align: center;"><i><span style="font-size: x-small;">Image generated by DALL-E 3 based on a prompt created by Bob Buzzard</span></i></div><h3 style="text-align: left;">Introduction</h3>
<p>
Unless you've been living under a rock, you'll have seen some big news from
Salesforce in the Generative AI space this week (29th Feb 2024, for future
readers) - Prompt Builder is Generally Available and Copilot is in Beta.
</p>
<p>
I've been lucky enough to get my hands on both of these new features and have
an initial dig around, so continue reading to find out how I got on with
Prompt Builder. Like it's General Availability, my hands on experience with
Copilot is coming soon!
</p>
<h3 style="text-align: left;">Setup</h3>
<p>Setting up Prompt Builder is very straightforward:</p>
<div>
<div>
<ol style="text-align: left;">
<li>
Navigate to
<b>Setup -> Einstein Generative AI -> Einstein Setup </b>and
toggle it on
</li>
<li>Assign the Prompt Builder Manager/User permission sets</li>
<li>
Reload Setup, then navigate to
<b>Setup -> Einstein Generative AI -> Prompt Builder</b> and
start prompting.<br /><br /></li>
</ol>
</div>
</div>
<h3 style="text-align: left;">Creating Prompts</h3>
<div><br /></div><div>
I've mostly played with the Sales Emails prompts, aside from my Copilot
experiments (see below). My initial use case was follow up emails for
attendees of one of my World Famous Release Webinars, so I created a new
custom object of Webinar and added a few fields that I wanted to pull into my
prompt - the usual name and description, but also a couple of fields to
capture the URLs of additional resources I wanted to send. I also added a Call
to Action - this is a field to capture what I was hoping the attendees would
do after attending - try out a couple of the features, for example.
</div>
<div><br /></div>
<div>
Once I'd added the Webinar custom object, it didn't appear immediately in the
prompt builder as a selectable Related Object. After checking the docs to make
sure I hadn't missed a checkbox to make it available, I decided it must just
be a timing thing - maybe the objects needed reindexing or the like. Then
</div>
<div>
I found that I couldn't create any prompts for a period of time - not sure how
long, but it felt like 30-40 minutes - during that time the Related Object
selector wouldn't populate at all. Then clearly whatever had been happening in
the background finished, and I could select my new Webinar object.
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiBlWceJ9He7zLESSQ3wf88I3wRiEythxjJLipGG-VA67Jkq21yC86ixAoQo5ARSs-bAUGgSzxg0WTii7VMQvs155eNVQS-Q_1Ak8i_ja4-x0f5IzEXk2R6R_i_rPNBS-ghRNbATyR_Z4k1nLdFFNzD9RB46b4ntZMVbMe21rxS7yGlt-AtbDs5JFWiKp0z/s1620/Screenshot%202024-03-02%20at%2011.22.52.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1038" data-original-width="1620" height="410" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiBlWceJ9He7zLESSQ3wf88I3wRiEythxjJLipGG-VA67Jkq21yC86ixAoQo5ARSs-bAUGgSzxg0WTii7VMQvs155eNVQS-Q_1Ak8i_ja4-x0f5IzEXk2R6R_i_rPNBS-ghRNbATyR_Z4k1nLdFFNzD9RB46b4ntZMVbMe21rxS7yGlt-AtbDs5JFWiKp0z/w640-h410/Screenshot%202024-03-02%20at%2011.22.52.png" width="640" /></a>
</div>
<br />
<div><br /></div>
<div><br /></div>
<div>
My prompt is loosely based on the standard follow up email, but includes
specific instructions and fields related to my webinar :
</div>
<p></p>
<blockquote style="border: none; margin: 0px 0px 0px 40px; padding: 0px; text-align: left;"><pre style="text-align: left; white-space: pre-wrap;">You are a Sales Executive and your name is {!$Input:Sender.Name} from an organization called {!$Input:Sender.CompanyName}. Your prospect is {!$Input:Recipient.Name}, who is the {!$Input:Recipient.Title}, from the company called {!$Input:Recipient.Company}.Your prospect {!$Input:Recipient.Name} attended a Salesforce release webinar that your company hosted.</pre><pre style="text-align: left; white-space: pre-wrap;">When I ask you to generate a follow-up email to your prospect you must strictly follow my instructions below.</pre><pre style="text-align: left; white-space: pre-wrap;">Instructions: """</pre><pre style="text-align: left; white-space: pre-wrap;">The salutation must only contain the recipient's first name.</pre><pre style="text-align: left; white-space: pre-wrap;">You must strictly not use "I hope this email finds you well" ,"I hope this email finds you doing well", or any other variation that expresses interest in the recipient's well-being to open the email.</pre><pre style="text-align: left; white-space: pre-wrap;">Create a follow-up email conveying that you are following up on their attendance of {!$Input:Webinar__c.Name} to see if they need any additional information or support.</pre><pre style="text-align: left; white-space: pre-wrap;">Mention the webinar, the number of attendees from {!$Input:Webinar__c.Attendee_Count__c} and include a summary of the {!$Input:Webinar__c.Description__c}. Express the hope that it was useful for them and that they will follow the call to action identified at {!$Input:Webinar__c.Call_to_Action__c}</pre><pre style="text-align: left; white-space: pre-wrap;">End the email with details of the two additional resources that are available : {!$Input:Webinar__c.Resource_1__c} , which is {!$Input:Webinar__c.Resource_1_Detail__c} and {!$Input:Webinar__c.Resource_2__c}, which is {!$Input:Webinar__c.Resource_2_Detail__c}</pre><pre style="text-align: left; white-space: pre-wrap;">Finish by indirectly encouraging your prospect {!$Input:Recipient.Name} to respond to your email by asking them if they need any further information, have questions or require assistance.</pre><pre style="text-align: left; white-space: pre-wrap;">Generate a subject line that can increase open rate using words and content related to the email body.</pre><pre style="text-align: left; white-space: pre-wrap;">"""</pre><pre style="text-align: left; white-space: pre-wrap;"></pre><pre style="text-align: left; white-space: pre-wrap;">Now generate the follow-up email to your prospect.</pre></blockquote>
<h3 style="text-align: left;">Grounding the Prompt</h3>
<p>
I then provide a couple of sample records to test the prompt out. The Lead
record is one that came with the org and will probably be familiar to anyone
that has used a Salesforce Developer Edition before - Lauren Bailey from
Omega, Inc. The webinar record is as follows:
</p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjNtUaEjat4X6j6Z9Ynj_J06CGjetFU68Wviy6BesFvOq7z8S7ZIIM4H9sNcMvBEYKle8z-pJhCDxBgUbIroMlPVAEFxK5rICAlwNZV9xvzGQArc5qc4DVAP2RKkTUsZ1Qo1puUOPwPQbZhWv8f92XJcQvrZRAmBQwnfqKVWcyqoWnIemTf7SAXFCg31cTZ/s1214/Screenshot%202024-03-02%20at%2012.10.49.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="646" data-original-width="1214" height="340" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjNtUaEjat4X6j6Z9Ynj_J06CGjetFU68Wviy6BesFvOq7z8S7ZIIM4H9sNcMvBEYKle8z-pJhCDxBgUbIroMlPVAEFxK5rICAlwNZV9xvzGQArc5qc4DVAP2RKkTUsZ1Qo1puUOPwPQbZhWv8f92XJcQvrZRAmBQwnfqKVWcyqoWnIemTf7SAXFCg31cTZ/w640-h340/Screenshot%202024-03-02%20at%2012.10.49.png" width="640" /></a>
</div>
The resolution section is useful when you are grounding the prompt with a number
of inputs - it shows the expanded merge fields so you can see exactly what
you'll be sending the AI model (although sensitive data will be masked by the
<a href="https://bobbuzzard.blogspot.com/2023/11/the-einstein-trust-layer-must-become.html" target="_blank">Einstein Trust Layer</a>, obviously):
<div>
<div></div>
<blockquote>
<div><span style="font-family: verdana;">
You are a Sales Executive and your name is Keir Bowden from an
organization called Credera. Your prospect is Lauren Bailey, who is the
Director of Services, from the company called Omega,Inc.Your prospect
Lauren Bailey attended a Salesforce release webinar that your company
hosted.
</span></div>
<div><span style="font-family: verdana;"><br /></span></div>
<div><span style="font-family: verdana;">
When I ask you to generate a follow-up email to your prospect you must
strictly follow my instructions below.
</span></div>
<div><span style="font-family: verdana;"><br /></span></div>
<div><span style="font-family: verdana;">Instructions: """</span></div>
<div><span style="font-family: verdana;">The salutation must only contain the recipient's first name.</span></div>
<div><span style="font-family: verdana;">
You must strictly not use "I hope this email finds you well" ,"I hope this
email finds you doing well", or any other variation that expresses
interest in the recipient's well-being to open the email.
</span></div>
<div><span style="font-family: verdana;"><br /></span></div>
<div><span style="font-family: verdana;">
Create a follow-up email conveying that you are following up on their
attendance of Spring 24 Release Webinar to see if they need any additional
information or support.
</span></div>
<div><span style="font-family: verdana;"><br /></span></div>
<div><span style="font-family: verdana;">
Mention the webinar, the number of attendees from 150 and include a
summary of the Learn about the key new features and enhancements in the
Spring 24 release of Salesforce.. Express the hope that it was useful for
them and that they will follow the call to action identified at Enable
dynamic forms and add related objects to your records home pages.
</span></div>
<div><span style="font-family: verdana;"><br /></span></div>
<div><span style="font-family: verdana;">
End the email with details of the two additional resources that are
available :
https://help.salesforce.com/s/articleView?id=release-notes.salesforce_release_notes.htm&language=en_US&release=248&type=5
, which is Salesforce Release Notes and
https://www.youtube.com/watch?v=c4nKiYlQ6EU, which is Webinar Recording
</span></div>
<div><span style="font-family: verdana;"><br /></span></div>
<div><span style="font-family: verdana;">
Finish by indirectly encouraging your prospect Lauren Bailey to respond to
your email by asking them if they need any further information, have
questions or require assistance.
</span></div>
<div><span style="font-family: verdana;"><br /></span></div>
<div><span style="font-family: verdana;">
Generate a subject line that can increase open rate using words and
content related to the email body.
</span></div>
<div><span style="font-family: verdana;">"""</span></div>
<div><span style="font-family: verdana;"><br /></span></div>
<div><span style="font-family: verdana;">Now generate the follow-up email to your prospect.</span></div>
<div></div>
</blockquote>
<div>
I found that my original calls to action, even though they looked good in
the record, didn't work very well when included in the prompt, so always
have a look at the resolution the first couple of times.
</div><div><br /></div>
<h3 style="text-align: left;">The Response</h3>
<div><br /></div><div>
The response from OpenAI GPT 3.5 Turbo was pretty good I thought, although
maybe a little brusque - I'll probably rework it to a slightly more chatty
version, but it's a great start for a small amount of work:</div>
<div>
<div><span style="font-family: verdana;"><b></b></span></div>
<blockquote>
<div><span style="font-family: verdana;"><b>Subject</b>:</span></div>
<div><span style="font-family: verdana;">
Enhance Your Salesforce Experience with Spring 24 Release Insights</span></div>
<div><span style="font-family: verdana;"><br /></span></div>
<div><span style="font-family: verdana;"><b>Body</b>:</span></div>
<div><span style="font-family: verdana;">Hello Lauren,</span></div>
<div><span style="font-family: verdana;"><br /></span></div>
<div><span style="font-family: verdana;">
I trust you found the Spring 24 Release Webinar insightful. With 150
attendees, we hope you gained valuable insights into the key new
features and enhancements in the Salesforce Spring 24 release. The
webinar highlighted how to enable dynamic forms and add related objects
to your records home pages.
</span></div>
<div><span style="font-family: verdana;"><br /></span></div>
<div><span style="font-family: verdana;">
For further exploration, two additional resources are available:
</span></div>
<div><span style="font-family: verdana;">
- Salesforce Release Notes:
https://help.salesforce.com/s/articleView?id=release-notes.salesforce_release_notes.htm&language=en_US&release=248&type=5
</span></div>
<div><span style="font-family: verdana;">
- Webinar Recording: https://www.youtube.com/watch?v=c4nKiYlQ6EU
</span></div>
<div><span style="font-family: verdana;"><br /></span></div>
<div><span style="font-family: verdana;">
Should you require any additional information, have questions, or need
assistance, please feel free to reach out.
</span></div>
<div><span style="font-family: verdana;"><br /></span></div>
<div><span style="font-family: verdana;">Best regards,</span></div>
<div><span style="font-family: verdana;">Keir Bowden</span></div>
<div><span style="font-family: verdana;">Credera</span></div>
</blockquote>
<div></div>
</div>
<h3 style="text-align: left;">Grounding with Apex/Flow</h3>
<p>
I can see I'll be doing a lot of grounding with pro or low code - if I'm
sending an email regarding a product I'll probably want to include a rate
card and maybe some contractual information for example. Grounding
with Flow is very straightforward, and once you choose a Template Triggered
Prompt Flow and pick the Sales Emails capability, there's a nice screen to
define the object types you are using :
</p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj6cs4B8mVU0ji4_hS3YqK2e6pMOf9oadLYQ8t5xXpjUWV6_8YY6SMJ7fK8YYq9KDET9wtEKBpN3Gi77mGQO3wPgi1Li5-JBK7OuW_2Iy5TDLxQ7Th5DSUyA8r0UbUWtib7YyApMP-BQ69_G_yQC7cCM2c7yjSBc7PgE3kW4A_z2xDoe9ntasw3hqllKY3d/s1240/Screenshot%202024-03-02%20at%2012.27.22.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1106" data-original-width="1240" height="570" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj6cs4B8mVU0ji4_hS3YqK2e6pMOf9oadLYQ8t5xXpjUWV6_8YY6SMJ7fK8YYq9KDET9wtEKBpN3Gi77mGQO3wPgi1Li5-JBK7OuW_2Iy5TDLxQ7Th5DSUyA8r0UbUWtib7YyApMP-BQ69_G_yQC7cCM2c7yjSBc7PgE3kW4A_z2xDoe9ntasw3hqllKY3d/w640-h570/Screenshot%202024-03-02%20at%2012.27.22.png" width="640" /></a>
</div>
<br />
<p>
Apex is a little different - you define a class with an invocable method
that defines the capability, then define Request and Response inner classes
to manage the input and output parameters:
</p>
<pre class="brush: java; toolbar: true">@InvocableMethod(label='Price for Lead' description='Gets the price of a product for a lead'
CapabilityType='PromptTemplateType://einstein_gpt__salesEmail')
public static List<Response> getProductStandardPrices(List<Request> requests) </pre><pre class="brush: java; toolbar: true"><span> </span><span> </span>--- --- ---</pre><pre class="brush: java; toolbar: true">public class Request {
@InvocableVariable(required=true)
public User sender;
@InvocableVariable(required=true)
public Lead recipient;
@InvocableVariable(required=true)
public Webinar__c relatedObject;
}
public class Response {
@InvocableVariable(required=true)
public String Prompt;
}</pre>
<p>Apex is also a little different in that I couldn't get it to work! Even after copy/pasting the example in the Salesforce help, it wouldn't show up in the Resources selector. I'm sure this will be resolved soon, and as Prompt Builder has been one of Salesforce's more rapid features to go GA, I'm okay with their being a few glitches. As I've <a href="https://bobbuzzard.blogspot.com/2024/02/zip-handling-in-apex-spring-24.html" target="_blank">written before</a>, I'll take a bit of additional effort tracking down issues if it means I get my hands on things earlier.</p><h3 style="text-align: left;">More Information</h3><p></p><ul style="text-align: left;"><li><a href="https://help.salesforce.com/s/articleView?language=en_US&id=sf.prompt_builder_add_apex_sales_email.htm&type=5" target="_blank">Salesforce Help for Prompt Builder</a></li><li><a href="https://admin.salesforce.com/blog/2024/the-ultimate-guide-to-prompt-builder-spring-24" target="_blank">Salesforce Admins Ultimate Guide to Prompt Builder</a></li></ul><p></p><div><a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a></div><p></p><div><div><br /></div><div><br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe></div></div><p><br /></p>
<p><br /></p>
</div>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-76255536013950242602024-02-25T16:31:00.004+00:002024-02-25T16:31:58.332+00:00Zip Handling in Apex, Spring '24 Developer Preview<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjnlhm1J2CB_gtlEvi7dTKeIHsYPbQROkWWzMSsKcUO4xedZ1rvAaDVnvnqD38uCOuAGfKM73IMFbhUTda6w4GqXjonI0buwEjwAxXWjjiElae68-9C1c127Y18zIF8t9a8Nt2rJ06uGFE5993-dEYQShOa7o5pDcVlCvNwXcs0-wKBR_DDNJxVkN73-f5J/s1024/DALL%C2%B7E%202024-02-25%2016.24.48%20-%20Create%20an%20image%20representing%20the%20concept%20of%20file%20compression.%20The%20main%20element%20is%20a%20box%20labeled%20'Files'%20that%20is%20being%20visually%20compressed,%20symbolizing%20copy.jpg" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1024" data-original-width="1024" height="200" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjnlhm1J2CB_gtlEvi7dTKeIHsYPbQROkWWzMSsKcUO4xedZ1rvAaDVnvnqD38uCOuAGfKM73IMFbhUTda6w4GqXjonI0buwEjwAxXWjjiElae68-9C1c127Y18zIF8t9a8Nt2rJ06uGFE5993-dEYQShOa7o5pDcVlCvNwXcs0-wKBR_DDNJxVkN73-f5J/w200-h200/DALL%C2%B7E%202024-02-25%2016.24.48%20-%20Create%20an%20image%20representing%20the%20concept%20of%20file%20compression.%20The%20main%20element%20is%20a%20box%20labeled%20'Files'%20that%20is%20being%20visually%20compressed,%20symbolizing%20copy.jpg" width="200" /></a>
</div>
<div style="text-align: center;">
<span style="font-size: x-small;"><i>Image generated by DALL-E 3, based on a prompt by Bob Buzzard</i></span>
</div>
<h3 style="text-align: left;">Introduction</h3>
<p>
The Spring '24 release of Salesforce had some great new features and
improvements, but for me some of the Beta/Developer Preview features were more
interesting - for example the
<a href="https://bobbuzzard.blogspot.com/2024/01/scratch-org-snapshots-in-spring-24.html" target="_blank">scratch org snapshots beta</a>
that I wrote about a few weeks ago.
</p>
<p>
I've been developing on the Salesforce platform for close to 16 years now, and
I've lost count of the number of different approaches that I've taken to
handle zip files. Sometimes on the server, sometimes on the client in
JavaScript, but always using custom solutions that didn't perform
fantastically well. The new functionality that is native to Apex won't solve
this problem - governor limits still apply - that fact that it isn't
implemented in Apex itself should mean that a little more processing can be
wrung out of the transaction.
</p>
<h3 style="text-align: left;">Trying it Out</h3>
<p>
This is a departure from previous developer preview functionality that I've
tried in the past, as it's available in scratch orgs only. I'm onside with
this approach as it feels like it will allow the Apex development team to be
more agile and give us access to changes earlier. Anything that does go awry
will disappear in a maximum of 30 days, so there won't be loads of developer
editions left behind needing to be patched up. I'll take a bit of additional
effort tracking down issues if it means I get my hands on things earlier.
</p>
<p>
You need to enable support for zip file processing via the features property
in your scratch org definition, specifying ZipSupportInApex :
</p>
<pre> "features": ["EnableSetPasswordInApi", "ZipSupportInApex"],
</pre>
<p>Then create a new scratch org and you are off to the races.</p>
<h3 style="text-align: left;">What Worked</h3>
<p>
Creating a zip file and then extracting it works great - I did this through
some execute anonymous Apex and was pleasantly surprised there were no further
hoops to jump through. As this is in developer preview I didn't expend a huge
amount of effort, just adding a single entry created from a String via a Blob:
</p>
<pre class="brush:java; tooblar: true">public void SimpleZip()
{
Compression.ZipWriter writer=new Compression.ZipWriter();
writer.addEntry('zs.csv', Blob.valueOf('Name, Title\nKeir Bowden, CEO'));
Blob zipFile=writer.getArchive();
ContentVersion contentVer = new ContentVersion(
VersionData =zipFile,
Title = 'zipsample.zip',
Description = 'Zip Sample',
PathOnClient = '/zipsample.zip'
);
insert contentVer;
}
</pre>
<p>Once I'd done this, I could see the zip and download it to extract files:</p>
<p></p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjpb_kj_JkrQX3tJto5lQARFLIvXQCk0lVNOgrCvWy-ryTx14xlg4PPqiahboouzdcieaf_VJc5aCZRn2VAcYIMQoMH6jNLqQ8DCyU-_7ry8ufbYqxmeW8Sn9nQvFoDiCwYI_4HZusEz9IiWE227LWbKfkaIwlC_3ZTKwwTztp-F2WK-dNXrgyNhfRM7Y7z/s1220/zip%20screenshot%201.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="180" data-original-width="1220" height="84" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjpb_kj_JkrQX3tJto5lQARFLIvXQCk0lVNOgrCvWy-ryTx14xlg4PPqiahboouzdcieaf_VJc5aCZRn2VAcYIMQoMH6jNLqQ8DCyU-_7ry8ufbYqxmeW8Sn9nQvFoDiCwYI_4HZusEz9IiWE227LWbKfkaIwlC_3ZTKwwTztp-F2WK-dNXrgyNhfRM7Y7z/w571-h84/zip%20screenshot%201.png" width="571" /></a>
</div>
<p></p>
<p>And to extract the file from the zip and debug the contents in Apex :</p>
<pre class="brush: java; toolbar: true">public void SimpleExtract()
{
ContentVersion contentVer=[select Id, VersionData, Title, Description, PathOnClient
from ContentVersion
where Title='zipsample.zip'];
Blob zipBody = contentVer.VersionData;
Compression.ZipReader reader = new Compression.ZipReader(zipBody);
Compression.ZipEntry csvEntry = reader.getEntry('zs.csv');
Blob csv = reader.extract(csvEntry);
System.debug('CSV body = ' + csv.toString());
}</pre>
<p>Gave me the contents of the file that I'd created from my String :</p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvrs3yW0rV0_xWcfEhyHf5LzIKBchoFQcrQ0m9HqTALkGwBMB-qOYslA-qpRNI9RRFLGuU1NnXpuo3HOP6KqAXcAQcFRYOLx31p-5cTmoLJ3D8rNNKs0wBKobJBAgHVv7Pnuo4RwJfEzMXHOwcQqj3gEXCe_zJTEnUV6jjeI6xK5T49Ulj3m57l39ujYiz/s193/zip%20screenshot%2023.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="44" data-original-width="193" height="55" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvrs3yW0rV0_xWcfEhyHf5LzIKBchoFQcrQ0m9HqTALkGwBMB-qOYslA-qpRNI9RRFLGuU1NnXpuo3HOP6KqAXcAQcFRYOLx31p-5cTmoLJ3D8rNNKs0wBKobJBAgHVv7Pnuo4RwJfEzMXHOwcQqj3gEXCe_zJTEnUV6jjeI6xK5T49Ulj3m57l39ujYiz/w238-h55/zip%20screenshot%2023.png" width="238" /></a>
</div>
<div class="separator" style="clear: both; text-align: center;"><br /></div>
<h3 style="text-align: left;">What Didn't Work</h3>
<div>
The demo that I was actually trying to put together was the ability to choose
a zip file from the content library and inspect the contents via the UI.
Selecting the file was all well and good, but once I tried to extract the
details of the files and wrap them in an inner class, I was out of luck.
</div><div><br /></div>
<div>
In case it was something about my processing of the entries, I tried just
debugging them :
</div>
<pre class="brush: java; toolbar: true">@AuraEnabled(cacheable=true)
public static List<Entry> GetZipEntries(Id contentVersionId)
{
List<Entry> entries=new List<Entry>();
try
{
ContentVersion contentVer=[select Id, VersionData, Title, Description, PathOnClient
from ContentVersion
where Id=:contentVersionId];
Blob zipBody = contentVer.VersionData;
Compression.ZipReader reader = new Compression.ZipReader(zipBody);
for (Compression.ZipEntry entry : reader.getEntries())
{
System.debug('Entry = ' + entry.getName());
}
}
catch (Exception e)
{
System.debug(e);
}
return entries;
}
</pre>
<div>
but this failed in the same, odd way. No exceptions, no errors, just an exit
of the server side call when I invoked ZipReader.getZipEntries().
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgYJuFPqi2KgraX-Mrmiq_ZwsviJzu86-9t1SnGoEQBq85SdlFveejEBr2-nyIXADEwGtnESx1oow9TY4ZM1IkzPza2Yy8AbP84QzbyV7DJRNT75iXLvIjc4FWkxsp0jkCXdbr3KFX7l4gHu4q1rnjw_eSnApKsac95W2vNHAx0mAV9EfCUxgVLvyjQZBc3/s1034/Screenshot%202024-02-24%20at%2011.55.58.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="392" data-original-width="1034" height="243" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgYJuFPqi2KgraX-Mrmiq_ZwsviJzu86-9t1SnGoEQBq85SdlFveejEBr2-nyIXADEwGtnESx1oow9TY4ZM1IkzPza2Yy8AbP84QzbyV7DJRNT75iXLvIjc4FWkxsp0jkCXdbr3KFX7l4gHu4q1rnjw_eSnApKsac95W2vNHAx0mAV9EfCUxgVLvyjQZBc3/w640-h243/Screenshot%202024-02-24%20at%2011.55.58.png" width="640" /></a>
</div>
<div><br /></div>
<div><br /></div>
<div>
In case it was something Lightning Components related I dusted off my
Visualforce skills and tried a custom controller :
</div>
<pre class="brush: java; toolbar: true">public List<Entry> getEntries()
{
if (null==this.entries)
{
entries=new List<Entry>();
String cvId=ApexPages.currentPage().getParameters().get('cvid');
System.debug('Id = ' + cvId);
ContentVersion contentVer=[select Id, VersionData, Title, Description, PathOnClient
from ContentVersion
where Id=:cvId];
Blob zipBody = contentVer.VersionData;
Compression.ZipReader reader = new Compression.ZipReader(zipBody);
for (Compression.ZipEntry zipEntry : reader.getEntries())
{
System.debug('Processing entry');
Entry entry=new Entry();
entry.name=zipEntry.getName();
entry.method=zipEntry.getMethod().name();
entry.lastModifiedTime=zipEntry.getLastModifiedTime();
entries.add(entry);
}
}
return entries;
}
</pre>
<div>
This was slightly more successful, in that it very occasionally showed the
list of entries after a hard reload of the page. Most of the time though, it
behaved the same and just gave up when I executed ZipReader.getEntries().
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgnrq1V05UFIOFUw_FO4CRV4h-Q981i_j5syJoM4g4pv3RoWzuPW44JdNI4EvjFsvgg1pCwLXa63r7D3LVVG4Txxy1kR-MxW7hFPWllwqCaICqTKsuSe_Q-j3WFahsomn6EaMArzG13mkP4ZHfEsK6Wm_F_izkVbEOOTin8J0LO7_7SIPRWegYFeVXkaFaP/s1092/Screenshot%202024-02-24%20at%2011.54.10.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="510" data-original-width="1092" height="298" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgnrq1V05UFIOFUw_FO4CRV4h-Q981i_j5syJoM4g4pv3RoWzuPW44JdNI4EvjFsvgg1pCwLXa63r7D3LVVG4Txxy1kR-MxW7hFPWllwqCaICqTKsuSe_Q-j3WFahsomn6EaMArzG13mkP4ZHfEsK6Wm_F_izkVbEOOTin8J0LO7_7SIPRWegYFeVXkaFaP/w640-h298/Screenshot%202024-02-24%20at%2011.54.10.png" width="640" /></a>
</div>
<br />
<div><br /></div>
<div>
Just in case it was something about my zip file, which was an old bootstrap
library I had lying around, I copied the same code into a new class and
invoked that using execute anonymous.</div><div><br /></div><div>That worked fine:</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgOkVDq0dcYBeCeWTFhtyN3I2bs9Ne9sBF2tP0pAvBp8T5lThtebPBMCvz0xu7nVAYf0QdD_NRXp6RrG9jMGxninp3EjF4XZXGdOiBDmDKxB_TGmAabzbrUHfY6_cjqDHi3CBOg_rkFjIIBVRoLtvCRRFwMMg4EGDjY3jqwC_voXZzbqBY6SPvW3ocKP4wr/s1840/Screenshot%202024-02-25%20at%2016.13.46.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="974" data-original-width="1840" height="355" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgOkVDq0dcYBeCeWTFhtyN3I2bs9Ne9sBF2tP0pAvBp8T5lThtebPBMCvz0xu7nVAYf0QdD_NRXp6RrG9jMGxninp3EjF4XZXGdOiBDmDKxB_TGmAabzbrUHfY6_cjqDHi3CBOg_rkFjIIBVRoLtvCRRFwMMg4EGDjY3jqwC_voXZzbqBY6SPvW3ocKP4wr/w673-h355/Screenshot%202024-02-25%20at%2016.13.46.png" width="673" /></a>
</div>
<br />
<div><br /></div><div>
So it does appear there is an issue accessing the contents of a zip file in a
transaction that originated from a custom UI, or something along those lines
anyway. I couldn't find anything in the docs to indicate I shouldn't be trying this, but it is in developer preview so I'd expect a few glitches. As a wise man once said,
"I'll take a bit of additional effort tracking down issues if it means I get
my hands on things earlier.".
</div>
<h3 style="text-align: left;">More Information</h3>
<div>
<ul style="text-align: left;">
<li>
<a href="https://help.salesforce.com/s/articleView?id=release-notes.rn_apex_compression.htm&release=248&type=5" target="_blank">Spring '24 Release Notes for this feature</a>
</li>
<li>
<a href="https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_namespace_compression.htm" target="_blank">Compression Namespace in the Apex Reference Guide</a>
</li>
</ul>
<div>
<div>
<a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a>
</div>
<div>
<div><br /></div>
<div>
<br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe>
</div>
</div>
</div>
</div>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-87059196901683838532024-02-11T12:18:00.002+00:002024-02-11T12:18:45.072+00:00Apex Null Coalescing Operator<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFDANBvVjiBIi8102PbbTO3kiDMN4JSuQxaibINX7POemQNDypDGtp0PMg-LN3rKszMK8j4g99nVhgQ74e6Kg-UbclS0FPFlVqWcx5VWbX3x0v92PxOOXYnqGMEgAJRXkI0A5NhzP0YdJNEDI_vZIfox3cRhMvhxgds4ZmtddbQ9kakXEB8LZUX5gYadLx/s1024/DALL%C2%B7E%202024-02-11%2012.10.20%20-%20An%20image%20showing%20the%20concept%20of%20a%20null%20coalescing%20operator%20(__)%20compacting%20some%20computer%20code.%20The%20image%20should%20depict%20a%20visual%20metaphor%20for%20the%20null%20%20copy.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1024" data-original-width="1024" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFDANBvVjiBIi8102PbbTO3kiDMN4JSuQxaibINX7POemQNDypDGtp0PMg-LN3rKszMK8j4g99nVhgQ74e6Kg-UbclS0FPFlVqWcx5VWbX3x0v92PxOOXYnqGMEgAJRXkI0A5NhzP0YdJNEDI_vZIfox3cRhMvhxgds4ZmtddbQ9kakXEB8LZUX5gYadLx/s320/DALL%C2%B7E%202024-02-11%2012.10.20%20-%20An%20image%20showing%20the%20concept%20of%20a%20null%20coalescing%20operator%20(__)%20compacting%20some%20computer%20code.%20The%20image%20should%20depict%20a%20visual%20metaphor%20for%20the%20null%20%20copy.jpg" width="320" /></a></div><div style="text-align: center;"><i>Image generated by DALL-E 3 based on about 10 prompts by Bob Buzzard</i></div><h3 style="text-align: left;">Introduction</h3>
<p>
The Spring 24 release of Salesforce bring a new operator to Apex - the null
coalescing operator. It makes me very happy when these kinds of operators make
their way to Apex, as it feels like we're a real programming language!
</p>
<p>
The null coalescing operator consists of an operand, two question marks, and
another operand:
</p>
<pre class="brush: java; toolbar: true"> operand1 ?? operand2
</pre>
<p>
When the expressions is evaluated, if operand 1 is not null that will be the
result, otherwise operand2 will be the result. Note that this isn't a
mechanism for avoiding a null result, as if operand1 is null, operand2 will be
the result <b>even if it is null.</b>
</p>
<h3 style="text-align: left;">Using the Operator</h3>
<p>
The key use case for the null coalescing operator is removing explicit, and
possibly lengthy, null checks. The obvious one is falling back on a default
value, so instead of :
</p>
<pre class="brush: java; toolbar: true">public List<Contact> getContactsForAccount(Account account)
{
List<Contact> contacts=contactsForAccount.get(account.Id);
if (null==contacts)
{
contacts=new List<Contact>();
}
return contacts;
}
</pre>
<p>we can write:</p>
<pre class="brush: java; toolbar: true">public List<Contact> getContactsForAccountNew(Account account)
{
List<Contact> contacts=contactsForAccount.get(account.Id) ??
new List<Contact>();
return contacts;
}
</pre>
<p>
Much more compact, but that's not all - there's another aspect to this that is
good and bad in equal measure. As there is now a single line evaluating the
list of contacts, a single test will give 100% coverage of the line regardless
of whether the first or second operand is as the result. For some developers
this is good - one less scenario needed for test coverage. For others
(including me) this is bad - if both scenarios aren't tested then we don't
know how it will behave when the missing scenario happens in production.
</p>
<p>
Actually there's a third aspect to this too - the right hand operand isn't
evaluated if the left hand is non-null, so it's efficient and devoid of
unexpected side effects.
</p>
<p>
You can also chain them. so if you have five potential candidates for an
opportunity value to return, you can write:
</p>
<pre class="brush:java; toolbar: true"> Opportunity opp=opp1 ?? opp2 ?? opp3 ?? opp4 ?? opp5;
</pre>
<p>Safe in the knowledge that the first non-null value will be used. </p>
<p>
You also don't have to use objects or primitives as the operands, but watch
out for the loss of clarity if you start to put too much working into a single
expression. For example, I don't think this is the clearest way to
extract a title given an Id that might refer to a Contact or a Lead:
</p>
<pre class="brush: java; toolbar: true">return [select Title from Contact where Email=:email][0].Title ??
[select Title from Lead where Email=:email limit 1].Title ??
'No contact/lead found matching email';Al
</pre>
<p>Always think about those that come after you!</p>
<p>
Use to pull a collection from a map or add the new collection in if it's not
present. Instead of
</p>
<h3 style="text-align: left;">Apples with Apples</h3>
<p>
The operands must both be the same type, so you can't have the first operand
being a Lead and the second being a Contact unless they can be converted to a
common type - e.g. an sObject. By the time you introduce casting though,
I'd say you've lost most of the benefit due to some ugly code:
</p>
<pre class="brush: java; toolbar: true">Contact contactCand=new Contact();
Lead leadCand;
String name = (String) ((sobject) leadCand ?? (sobject) contactCand).get('Name');
</pre>
<p>
You can also compare operands that can be promoted to a common type by the
compiler, for example if you have some inheritance at play:
</p>
<pre class="brush: java; toolbar: true" style="text-align: left;">public virtual class First
{
public String name {get; set;}
}
public class Second extends First
{
}
----------
First first=new First();
Second second=new Second();
String name=(first ?? second).name;
</pre>
<h3>More Information</h3><p></p><ul style="text-align: left;"><li><a href="https://help.salesforce.com/s/articleView?id=release-notes.rn_apex_NullCoalescingOper.htm&release=248&type=5" target="_blank">Spring 24 Release Notes for the operator</a></li><li><a href="https://en.wikipedia.org/wiki/Null_coalescing_operator" target="_blank">Wikipedia, with examples from many different languages</a></li></ul><div><div><a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a></div><div><div><br /></div><div><br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe></div></div></div><p></p>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-22430514054149445002024-01-14T17:00:00.004+00:002024-02-05T08:44:11.162+00:00Scratch Org Snapshots in Spring '24<p><i>Note: This feature is in beta in Spring '24. Like all other betas, this functionality may never go GA and may disappear at any time. Caveat emptor.</i></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjm6JSW5p4J-sI7HWYek2useiNb5sJVifT_5eCujXxNE9dfwphI5gtz6ej3-ePeufZqv9yAsaNEZQk7gDCt2VhaiUXgWKNgbud_hLydoXe76RFG8EtCuGVedG7r4d7wecgF40N1fF8ZQHv8BlQLligHa1UP-f-yNb_FhDr_4CBelpX2-Xie24GaVCGRsip9/s1024/DALL%C2%B7E%202024-01-14%2016.58.04%20-%20An%20image%20set%20in%20the%20early%20days%20of%20photography,%20depicting%20a%20scene%20where%20a%20photographer%20is%20in%20the%20midst%20of%20capturing%20a%20photograph%20of%20a%20computer.%20The%20pho.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1024" data-original-width="1024" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjm6JSW5p4J-sI7HWYek2useiNb5sJVifT_5eCujXxNE9dfwphI5gtz6ej3-ePeufZqv9yAsaNEZQk7gDCt2VhaiUXgWKNgbud_hLydoXe76RFG8EtCuGVedG7r4d7wecgF40N1fF8ZQHv8BlQLligHa1UP-f-yNb_FhDr_4CBelpX2-Xie24GaVCGRsip9/s320/DALL%C2%B7E%202024-01-14%2016.58.04%20-%20An%20image%20set%20in%20the%20early%20days%20of%20photography,%20depicting%20a%20scene%20where%20a%20photographer%20is%20in%20the%20midst%20of%20capturing%20a%20photograph%20of%20a%20computer.%20The%20pho.png" width="320" /></a></div><div style="text-align: center;"><i><span style="font-size: x-small;">Image generated by DALL-E 3 based on a prompt by Bob Buzzard</span></i></div><h3 style="text-align: left;">Introduction</h3><p>The Spring '24 release of Salesforce moves the scratch org snapshot functionality into beta. I've been waiting to get my hands on this and so far it hasn't disappointed.</p><p>Whenever we get new features of this nature, I like to reflect on how far we've come in Salesforce land. In this case I was testing with the codebase of our BrightMEDIA accelerator, and when we started building this in mid-2014 (aka nearly a decade ago), we typically allowed a week to get a new developer set up. We had to spin up a Developer Edition, raise a bunch of tickets to get various features enabled and increase the Apex character limit, install a number of packages, carry out a number of manual setup steps, deploy the code and assign permission sets. For whatever reason, no two Developer Editions appeared to have the same setup, so typically the deployment was an iterative process where we discovered what was missing or off instead of on by default. Then they'd go through and set up some standing data to be able to work in the org. </p><p>Fast forward to the end of 2023 and I have a node script that creates a scratch org, installs the packages, deploys the code, loads the standing data and produces a ready to go development environment in around 30 minutes. I'm always interested in speeding things up though!</p><h3 style="text-align: left;">Creating a Snapshot</h3><p>Thanks to a pre-release environment that I've also had for a decade, I have a pre-release dev hub which meant I could enable the beta before the Spring '24 release goes live. Then I assigned myself the appropriate object permissions for Org Snapshots and I was ready to create. </p><p>I set up my scratch org using my existing script, which creates an org with the following applied:</p><p></p><ul style="text-align: left;"><li>Four managed packages</li><li>Approximately 9,000 metadata components</li><li>Approximately 2,000 records</li></ul><div>Creating a snapshot of this org took 11 minutes, which I must admit was quite a bit faster than I was expecting.</div><div><br /></div><h3 style="text-align: left;">Using the Snapshot</h3><div><br /></div><div>This started off with a bit of a challenge, in that attempting to use the snapshot kept giving the error that the snapshot wasn't Active, but listing the Dev Hub snapshots showed that it was indeed Active. I spent a while searching through the CLI Github issues list and the snapshot pilot Trailblazer group, but it seemed like I was the lucky one who got to experience this first. This was quite soon after the pre-release had gone live, so I figured it might be a simple bug and played the waiting game.</div><div><br /></div><div>About 7 hours later my masterful inactivity was rewarded, as my snapshot sprang to life and I was able to run the commend to create an org from it. In fairness, it might have started working 10 minutes later, but it was around 7 hours later I had the time to try it out again.</div><div><br /></div><div>The even better news was that creating a scratch org from the snapshot took 6 minutes - an 80% saving on the 30 minute creation time for my script. The org was flawless too - all the metadata and data was there.</div><div><br /></div><h3 style="text-align: left;">The End of Sandboxes?</h3><div><br /></div><div>So does this mean that we can all create scratch org snapshots rather than sandboxes going forward? They even contain data, so maybe we can do away with full or partial copy sandboxes too. I don't think so, for a few reasons.</div><div><br /></div><h4 style="text-align: left;">Lifespan</h4><div><br /></div><div>Scratch orgs and org snapshots, have a 30 day lifespan. From a developer perspective this is fine - we treat these orgs and disposable and typically create a fresh one when we start a new piece of work. That isn't necessarily the case for orgs used for training, QA, integration testing or testing against a new release. It's particularly unsuitable for pre-production environments which mirror production - imagine having to recreate all your test integrations at the start of every month!</div><div><br /></div><h4 style="text-align: left;">Storage</h4><div><br /></div><div>Scratch orgs and org snapshots are limited to 200Mb of data. Again, probably fine for many development tasks, but again likely to be too small for training, pre-production and test environments that are indicative of production. </div><div><br /></div><h4 style="text-align: left;">Licenses</h4><div><br /></div><div>Sandboxes replicate your production org licenses, so all of your users can have access. Scratch orgs are a much more restrictively licensed, usually somewhere between 1 and 10 seats per feature. When we were adding community (now Experience Cloud) features to BrightMEDIA, we had the princely sum of 1 partner community license available in our scratch orgs - you'd have to be quite brave to promote to production with that kind of limitation on your testing!</div><div><br /></div><h4 style="text-align: left;">Completeness of Version Controlled Metadata</h4><div><br /></div><div>This is where developer/developer pro sandboxes will retain their usefulness once scratch org snapshots are live. Some organisations with large, mature Salesforce orgs won't have all their metadata in version control, because why would they invest the time and money to do that when they don't need to. They'll likely have Apex, flows, lightning components, and maybe some second generation packages in version control, but things like sharing rules, report and dashboard folders, duplicate rules that are managed by administrators probably won't. Yes this is a sweeping generalisation, but you get the general idea. Being able to create a guaranteed replication of production to work in will be an important capability for years to to come in my view. That said, they'll probably become less used as time goes on and maybe scratch org snapshots get longer lifespans.</div><div><br /></div><div>So not a sandbox killer, but that was never the intention. For those of us with a very source-centric development approach however, this is another great addition to the developer toolbelt.</div><div><br /></div><h3 style="text-align: left;">More Information</h3><div><ul style="text-align: left;"><li><a href="https://help.salesforce.com/s/articleView?id=release-notes.rn_dev_environments_scratch_org_snapshots.htm&release=248&type=5" target="_blank">Release Notes (Preview)</a></li><li><a href="https://developer.salesforce.com/docs/atlas.en-us.248.0.sfdx_dev.meta/sfdx_dev/sfdx_dev_snapshots_intro.htm" target="_blank">Scratch Org Snapshots Developer Guide (Beta)</a><br /><br /></li></ul></div><p></p><div><a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a></div><div><div><br /></div><div><br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe></div></div>Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-10959276703621470362024-01-07T17:13:00.002+00:002024-01-07T17:13:36.487+00:00Breaking Batch<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhYXrqZCkARNd9HvU6nMycMd7XQnvI3iDGiKahEI_T_6_H-eoFgKbSB-6P8xmEjxKJBB6QIo2o4m_k7TLkWHt_hzwBJRlqPq62PYFdkQ_3ESUVP1azKOXNiSgcDCOvCHTbWIDXRDe39nullW5HBvPUwlO44PH6vfAYC1KKQ8g9IL_pxpDX-GZXZJIp2P0ga/s1024/DALL%C2%B7E%202024-01-07%2017.11.23%20-%20A%20picture%20of%20a%20character%20resembling%20Heisenberg%20from%20'Breaking%20Bad',%20sat%20at%20a%20computer.%20The%20character,%20inspired%20by%20Heisenberg,%20is%20depicted%20with%20distinc.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1024" data-original-width="1024" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhYXrqZCkARNd9HvU6nMycMd7XQnvI3iDGiKahEI_T_6_H-eoFgKbSB-6P8xmEjxKJBB6QIo2o4m_k7TLkWHt_hzwBJRlqPq62PYFdkQ_3ESUVP1azKOXNiSgcDCOvCHTbWIDXRDe39nullW5HBvPUwlO44PH6vfAYC1KKQ8g9IL_pxpDX-GZXZJIp2P0ga/s320/DALL%C2%B7E%202024-01-07%2017.11.23%20-%20A%20picture%20of%20a%20character%20resembling%20Heisenberg%20from%20'Breaking%20Bad',%20sat%20at%20a%20computer.%20The%20character,%20inspired%20by%20Heisenberg,%20is%20depicted%20with%20distinc.png" width="320" /></a></div><div style="text-align: center;"><i><span style="font-size: x-small;">Image generated by DALL-E 3 from a prompt by Bob Buzzard</span></i></div><h3 style="text-align: left;">Introduction</h3>
<div><br /></div>
<div>
In my last blog post (<a href="https://bobbuzzard.blogspot.com/2023/12/a-tale-of-two-contains-methods.html" target="_blank">A Tale of Two Contains Methods</a>) I mentioned that I'd spent quite a bit of December taking part in
<a href="https://adventofcode.com/" target="_blank">Advent of Code</a>.
Each day there were two challenges - a (relatively) straightforward one, that
could potentially be brute forced, and an extended version where brute forcing
would take days so using the a more thoughtful approach was required. As I was
tackling these challenges using Apex, brute forcing wasn't really an option,
so my solution typically involved building structures of complex objects in
memory in order to be able to process them quickly. Pretty much every extended
version required batch Apex to handle the volumes, and in a few cases the
(relatively) straightforward one did too.
</div>
<div><br /></div>
<div>
The combination of the complex object structure and batch Apex threw up some
interesting errors, so I decided to blog about one of these. A couple of
things to note:
</div>
<div>
<ul style="text-align: left;">
<li>
This isn't a moan about batch Apex - I was using it in a way that I'm
pretty sure it wasn't intended for, and there was a simple workaround
</li>
<li>
By complex object I just mean one that is made up of primitives, simple(r)
objects and collections - it doesn't mean it was a particularly difficult
structure to comprehend or change.
</li>
</ul>
</div>
<div><br /></div>
<h3 style="text-align: left;">The Challenge</h3>
<div><br /></div>
<div>
(Some of the challenge detail has been removed for clarity - you can see it in
its full glory
<a href="https://adventofcode.com/2023/day/22" target="_blank">here</a>)
</div>
<div>
Part 1 of the challenge in question was around bricks of varying length in a
3-dimensional structure (essentially a large cube) that had landed on top of
each other like a weird Jenga puzzle. Based on the starting coordinate and
dimensions of each brick, I needed to figure out how the bricks were supported
in the structure.
</div>
<div><br /></div>
<div>
The approach I took was to represent a brick as an object and hold two
associated collections for each Brick instance:
</div>
<div>
<ul style="text-align: left;">
<li>
Supporters - these are the Bricks that are directly beneath this Brick and
in contact with it.
</li>
<li>
Supporting - these are the Bricks that this brick is directly beneath and
supporting.
</li>
</ul>
<div>
The answer I had to calculate to complete the challenge was number of bricks
that I could remove without causing any other bricks to fall. This could be
accomplished by iterating the bricks and adding up all of those where all of
the Supporting bricks are also supported by others.
</div>
</div>
<div><br /></div>
<div>
Part 2 was to find sum of the bricks that would fall if each of the bricks
were removed. With the structure that I had in place, this was actually quite
simple. I iterated the bricks, found all of the Supporting entries where that
brick was the only Supporter, and then found all of their Supporting entries
where they were the only Supporter and so on until I reached the end. This
would definitely need batch Apex though, as there were 1,500 bricks in the
actual challenge input.
</div>
<div><br /></div>
<div>
Each challenge includes a small example with the workings and answers - 6
bricks in this case - so I was able to test my batch Apex before
executing with the larger volume of data.
</div>
<div><br /></div>
<div>My Brick class was as follows:</div>
<pre class="brush:java; toolbar: true">public class Brick
{
public String brickNo;
public Point3d startPoint;
public Point3d endPoint;
public Integer width;
public Integer depth;
public Integer height;
public Set<Brick> supporters=new Set<Brick>();
public Set<Brick> supporting=new Set<Brick>();
public Integer totalSupporters=0;
}
</pre>
<div>
The start method of the Batch class converted the input into a collection of
Bricks and then returned a collection of Integers, one per Brick. I
implemented Database.Stateful so that the collection of Bricks was
available across each execute method, and then processed the Bricks who's
brickNo appeared in the scope. Essentially I'd broken up my iteration of the
Bricks across a number of transactions, while ensuring I only had to build the
Bricks structure once at the start.
</div>
<div><br /></div>
<div>
When I ran this with the example, it worked fine and gave me the correct
answer.
</div>
<div><br /></div>
<h3 style="text-align: left;">The Problem</h3>
<div><br /></div>
<div>
I then fired it off with the (much larger) challenge input, and was initially
pleased to see that I was able to build the in-memory structure without
running into any issues around heap or CPU. Sadly this pleasant sensation was
short lived, as the first batch that executed generated the following output:
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj68iUGA6baVvPVTx0hG7Hxpcu8CqP5WPwpjGxVJx6zkUMhrd5U0wl7et6ZRlfIjmYyt45rtgvvq4xZawyrBMx0J0rZoow0t6kQs-zHuGa_zo8fbn-WZwJ8VCAdRn8zhtTfk-V0oiPUImJCU4Vit351stZeTj5772eNjQVWxXXWINAbtlglKdJFmziEdpT4/s1266/Screenshot%202024-01-07%20at%2013.08.10.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="752" data-original-width="1266" height="238" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj68iUGA6baVvPVTx0hG7Hxpcu8CqP5WPwpjGxVJx6zkUMhrd5U0wl7et6ZRlfIjmYyt45rtgvvq4xZawyrBMx0J0rZoow0t6kQs-zHuGa_zo8fbn-WZwJ8VCAdRn8zhtTfk-V0oiPUImJCU4Vit351stZeTj5772eNjQVWxXXWINAbtlglKdJFmziEdpT4/w400-h238/Screenshot%202024-01-07%20at%2013.08.10.png" width="400" /></a>
</div>
<br />
<div>
Based on the debug that I had in the class, it was clear that the batch job
was failing before it was getting to any of my code. After some binary chop
style debugging, where retried the batch with various parts of the code
commented out, it turned out that the issue was my collections:
</div>
<pre class="brush:java; toolbar: true"> public Set<Brick> supporters;
public Set<Brick> supporting;
</pre>
<div><br /></div>
<div>
As I already had the full collection of Bricks stored in a Map keyed by
brickNo, turning these into sets of Strings and storing the brickNo rather
than a reference to the Brick itself didn't need much in terms ot changes to
the code, and allowed the batch to complete without issue.
</div>
<div><br /></div>
<div>
So why were Sets of Strings okay by Sets of Bricks not? Once I was into a
large cube with 1,500 bricks in it, it looked like the sets got pretty big. As
the Bricks were stored in an instance variable, they were part of the state of
the batch and thus de/serialised for each batch processed. Obviously I'm not
privy to exactly how the batch processing in Apex works, but I'd imagine that
serialising ended up with a pretty huge structure with a lot of repetition, as
the same Brick instances were expanded many times as part of the
Supporters and Supporting collections. Deserialising this structure clearly
proved too much, hence the internal error.
</div>
<div><br /></div>
<h3 style="text-align: left;">In Conclusion</h3>
<div><br /></div>
<div>
As mentioned earlier, this isn't intended to throw shade on batch Apex.
Storing large collections of complex objects that contain collections of other
complex objects so they can be accessed across transactions really isn't a
valid use case. This kind of information belongs in the database rather than
in the batch class, while Database.stateful is more appropriate for managing
things like running totals.
</div>
<div><br /></div>
<div>
This is one of the reasons that I really enjoyed taking on Advent of Code with
Apex - I'm trying to solve problems that (a) I'd never encounter in a customer implementation and (b) the Salesforce platform is really not suited to handling.</div><div><br /></div><div>This was also a lesson in the need to test with indicative data - everything worked fine with the small amount of test data I had available, but once I hit the real data the flaws were revealed!</div><div><br /></div><h3 style="text-align: left;">Related Posts</h3><div><br /></div><div><ul style="text-align: left;"><li><a href="https://bobbuzzard.blogspot.com/2023/12/a-tale-of-two-contains-methods.html" target="_blank">A Tale of Two Contains Methods</a></li><li><a href="https://bobbuzzard.blogspot.com/2021/10/transaction-boundaries-in-salesforce.html" target="_blank">Transaction Boundaries in Salesforce</a></li></ul><div><div><a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a></div><div><div><br /></div><div><br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe></div></div></div></div>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-55033990394187592172023-12-28T17:24:00.002+00:002023-12-28T17:24:23.303+00:00A Tale of Two Contains Methods<div>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFe2TkDB5YTF-O-qoEfWDsTgfLm1oh-07xN4DD4oSyHfCzyLP5UJ_jwQR81uSIubAfGCEtab9HXZa-W8ta5swMx95oK-26bTLNsOLSP9gu_P2ADAzJhZOMGtIJooaHR82rCC2KYk8ShVXslVkb7RUeINwqc_Rm-P0Jy6Urf18XWILebNdVm1FQZQBxojmA/s1792/twomethods.webp" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1024" data-original-width="1792" height="183" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFe2TkDB5YTF-O-qoEfWDsTgfLm1oh-07xN4DD4oSyHfCzyLP5UJ_jwQR81uSIubAfGCEtab9HXZa-W8ta5swMx95oK-26bTLNsOLSP9gu_P2ADAzJhZOMGtIJooaHR82rCC2KYk8ShVXslVkb7RUeINwqc_Rm-P0Jy6Urf18XWILebNdVm1FQZQBxojmA/s320/twomethods.webp" width="320" /></a></div><div style="text-align: center;"><i><span style="font-size: x-small;"><br />Image generated by DALL-E 3 from a prompt by Bob Buzzard </span></i></div><h3 style="text-align: left;"><br /></h3><h3 style="text-align: left;">Introduction</h3>
<div><br /></div>
</div>
<div>
<div>
Eagle-eyed regular readers of this blog may have noticed that I dropped away
during the build up to Christmas this year, and there was a good reason for
this. I was taking part in
<a href="https://adventofcode.com/" target="_blank">Advent of Code</a>,
having been introduced to it by our
<a href="https://www.credera.co.uk/" target="_blank">Credera</a> brethren. A
challenge a day for 25 days - two a day in fact, as if/when you solve the
first, it gets tweaked to make it harder. </div><div><br /></div><div>I opted to use Apex to take on the
challenges, which ensured that I couldn't brute force any solutions, and
that I typically had to switch to a batch/asynchronous approach when I
finally ran out of CPU or heap. This didn't always help by the way - a few
times I had to give up after encountering variants of "the batch class is too
large" error. A few other times I had to accept solving one challenge of
the two when I couldn't figure out how to even start on the second! I ended
up with 39/50 successes though, which didn't seem like a bad return for a
constrained language. It was extremely enjoyable, but be aware that this can
easily soak up all of your spare time and more, and may not make you popular
at home!</div><div><br /></div>
<div>
A common theme of the challenge was walking a route through a grid and
keeping track of the tiles that I'd encountered before, usually with some
additional information around which path I was following, the direction I'd
moved in and how many steps I'd taken in a direction. If it was net new then
I needed to add it to a collection, but I also needed to order the
collection, as I was looking for the shortest or longest route possible, so
they needed to end up in a List.</div>
<div><br /></div>
<h3 style="text-align: left;">A Tale of Two Contains Methods</h3>
<div><br /></div>
<div>
<div style="text-align: center;">
<b style="font-size: large;"><i><span style="color: #999999;">It was the best of methods, it was the worst of methods</span></i></b>
</div>
<div><br /></div>
</div>
</div>
<div>One difference between Apex and some other languages I've worked with in the past is the <a href="https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_list.htm#apex_System_List_contains" target="_blank">contains method on the List class</a> - this handy helper returns true if the list contains the element passed in to it. This saves me from either iterating the list each time I consider adding an element, or maintaining a separate "lookup" collection - typically a Set that matches the list and I'd check if the element was in there first.</div><div><br /></div><div>I used the List contains method in my first attempt on one of the challenges,
and found that I had to quickly go to batch apex. In order to walk the path I
was carrying out a
<a href="https://en.wikipedia.org/wiki/Breadth-first_search" target="_blank">breadth-first search</a>, adding every possible option for each step to a queue as a complex object,
but always processing the shortest option first. Once the queue got to around
3,000 elements (complex objects), I found that I could only process a few of
them before breaching the 60,000 millisecond CPU limit, and I was looking at
an extremely large set of batches that would likely take multiple hours to
complete. After a bit of digging it looked like the check/add to the
queue of steps wasn't scaling overly well, so I switched back to maintaining a
separate lookup Set and using the
<a href="https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_set.htm#apex_System_Set_contains" target="_blank">Set contains method</a>
to determine if I'd seen it before. Once I did this, the CPU use dropped to
the point where I could complete the whole thing in 2-3 batches, which I did.
<div><br /></div>
<div>
I was somewhat taken aback by this, as I'd assumed that the List contains
method would be using an efficient mechanism under the hood and would
perform well regardless of the size of the list/complexity of the object.
This turns out not to be the case, but that's really my fault for assuming -
there's nothing in the docs to suggest that it will be doing anything of the
sort.<br />
<div><br /></div>
<div>
Now that Advent of Code has completed, I've had the time to run some
numbers on the CPU consumption of each of these contains methods (hence
the witty title, with apologies to Charles Dickens of course), and present the results.
</div>
<div><br /></div>
<div>
<h3 style="text-align: left;">The Methodology</h3><div><br /></div>
<div>
I have defined a (not particularly) complex object so that there's a bit
of work involved to determine if another object matches it:
</div>
<div><br /></div>
<pre class="brush: java; toolbar: true">public class ContainsObject
{
public String stringRep;
public Integer intRep;
public Long square;
public DateTime timestamp;
public ContainsObject(Integer idx)
{
intRep=idx;
stringRep=''+idx;
square=idx*idx;
timestamp=System.now();
}
}
</pre>
<div><br /></div>
<div>
I then add two thousand of these to a List, checking each one to see if
I've seen it before. The CPU consumed is captured for every 100 elements
and gives the following results:
</div>
<div><br /></div>
<pre> <b>Count</b> <b>CPU</b>
0 - 100 76
400 - 500 97
900 -1000 169
1400 - 1500 460
1900 - 2000 582
<br /></pre>from this I can deduce that there isn't anything particularly efficient going on under the hood - as the size of the List increases, so does the time taken to check and add 100 elements. In the 1900-2000 range the average is over 5ms per check/insert, which is quite a chunk for a couple of statements.</div><div><br /></div><div>Switching to the List and lookup Set approach, I create a Set of the complex objects to mirror the contents of the List, but without any ordering, that I can use for the check part. If the element isn't present in the Set, I add it to both the Set and the List.</div><div><br /></div><div>Executing this for the same number of complex objects gives:</div><div><br /></div><div><pre> <b>Count</b> <b>CPU</b>
0 - 100 4
400 - 500 6
900 -1000 5
1400 - 1500 4
1900 - 2000 7
<br /></pre></div>
This is much more the kind of result I want to see - the performance isn't really changing regardless of the size of the Set, and while the final hundred takes slightly longer than the first hundred, the average is 0.07ms, which leaves me plenty of CPU to play with in the rest of the transaction.</div><div><br /></div><h3 style="text-align: left;">No Downside?</h3><div><br /></div><div>As always, there's no such thing as a free lunch - the fact that I have to maintain another collection for speedy lookup does incur a heap penalty. It is a pretty cheap lunch though, as I'm only holding references to the objects stored in the List rather than copies, so the 2,000 entries in the Set consume another 8kb of heap. This feels like a pretty decent trade off to me, but if your transaction has plenty of spare CPU and is butting up against the heap limit, you'll likely feel different.</div><div><br /></div><h3 style="text-align: left;">Related Posts</h3><div><ul><li><a href="https://bobbuzzard.blogspot.com/2023/09/apex-comparator-in-winter-24.html" target="_blank">Apex Comparator in Spring 24</a></li><li><a href="https://bobbuzzard.blogspot.com/2022/05/the-cpu-effects-of-sorting.html" target="_blank">The CPU Effects of Sorting</a></li><li><a href="https://bobbuzzard.blogspot.com/2022/02/flow-collection-filter-element-in.html" target="_blank">Flow Collection Filter Element in Spring 22</a></li><li><a href="http://bobbuzzard.blogspot.com/2021/05/the-impact-of-systemdebug.html" target="_blank">The Impact of System.debug</a></li><li><a href="http://bobbuzzard.blogspot.com/2021/01/salesforce-flows-triggers-and-cpu-one.html" target="_blank">Salesforce Flows, Triggers and CPU - One. More. Time.</a></li></ul></div>
</div>
<p></p>
<div>
<a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a>
</div>
<div>
<div>
<div>
<div><br /></div>
<div>
<br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe>
</div>
</div>
<div class="separator" style="clear: both; text-align: center;"><br /></div>
</div>
</div>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-44503329018546670212023-11-11T17:08:00.001+00:002023-11-11T17:08:08.994+00:00OpenAI GPTs - Meet Bob Buzzard 2.0<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihvKaMn4bvcuz926P8s1LzDZQR6V7JBDOfRYDBMADdbrndEP-3Kw7cGtinE-wiEnENCvrhkgNP5Q3bSe32QnN5qvVCA6RZgCMFr9Q44S_5jv7hguDJJ6d30UydAUJzvAMCAjzuFi6VN32_urNHWPCwoeADw68wSF39dQQenknrKmKEKjkOKK9JaYlyhjSA/s1058/Screenshot%202023-11-11%20at%2016.31.35.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="546" data-original-width="1058" height="165" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihvKaMn4bvcuz926P8s1LzDZQR6V7JBDOfRYDBMADdbrndEP-3Kw7cGtinE-wiEnENCvrhkgNP5Q3bSe32QnN5qvVCA6RZgCMFr9Q44S_5jv7hguDJJ6d30UydAUJzvAMCAjzuFi6VN32_urNHWPCwoeADw68wSF39dQQenknrKmKEKjkOKK9JaYlyhjSA/s320/Screenshot%202023-11-11%20at%2016.31.35.png" width="320" /></a></div><div><br /></div><div><br /></div><h3 style="text-align: left;">Introduction</h3><div><br /></div>During <a href="https://devday.openai.com/" target="_blank">OpenAI DevDay</a>, the concept of <a href="https://openai.com/blog/introducing-gpts" target="_blank">custom GPTs</a> was launched - Chat GPT with a bunch of preset instructions to target a specific problem domain, additional capabilities such as browsing the web, and extra knowledge in terms of information that may not be available on the web. <div><br /></div><div>In order to create and use GPTs, you need to be a <a href="https://help.openai.com/en/articles/6950777-what-is-chatgpt-plus" target="_blank">ChatGPT Plus</a> subscriber at $20/month, although in the UK there's VAT to be added so it works out around £20/month. This also gives priority access to new features, the latest models and tools, faster response times and access even at peak times. I signed up just to try out GPTs though, as they looked like a world of fun.</div><div><br /></div><h3 style="text-align: left;">The Replicant</h3><div><br /></div><div>My first custom GPT is my replicant - Bob Buzzard 2.0. A GPT that has been pointed at most of my public and some of my private information. Instructed to respond as I would, you can expect irreverent or sarcastic responses as the mood takes (AI) me. Obviously very focused on Salesforce, and keen on Apex code. </div><div><br /></div><div>Right now you'll need to be a ChatGPT Plus user to access custom GPTs, but if you are you can find Bob Buzzard 2.0 at : <a href="https://chat.openai.com/g/g-DOVc9phwC-bob-buzzard-2-0">https://chat.openai.com/g/g-DOVc9phwC-bob-buzzard-2-0</a> Here's a snippet of a response from my digital twin regarding the impact of log messages on CPU - something I've <a href="https://bobbuzzard.blogspot.com/2021/05/the-impact-of-systemdebug.html" target="_blank">investigated in detail in the past</a> :</div><div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2rOHIQar3fJBdO7Xtq442ED1238WJLAmScDHdBBaM8XEE3uL6gdNx41WfYndoN6RN83qlHRTVzyakPkToI1LHyznzgC36eYMHVZ3q9f_hRQ0EeFI97zprjjfFD-oDnqAi2nlAyvg4Gd7mOAW9JFNUUUOF9CbO2X8Tk67T-nNcxMFVHkhHwNOdFU-1U914/s1580/Screenshot%202023-11-11%20at%2016.34.34.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="600" data-original-width="1580" height="244" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2rOHIQar3fJBdO7Xtq442ED1238WJLAmScDHdBBaM8XEE3uL6gdNx41WfYndoN6RN83qlHRTVzyakPkToI1LHyznzgC36eYMHVZ3q9f_hRQ0EeFI97zprjjfFD-oDnqAi2nlAyvg4Gd7mOAW9JFNUUUOF9CbO2X8Tk67T-nNcxMFVHkhHwNOdFU-1U914/w640-h244/Screenshot%202023-11-11%20at%2016.34.34.png" width="640" /></a></div><div><br /></div><h3 style="text-align: left;">Creating GPTs</h3><div><br /></div><div>This is incredibly simple - you just navigate to the <a href="https://chat.openai.com/create" target="_blank">create page</a> and tell it in natural language how you want it to behave, define the skills, point it at additional web sites or upload additional information. It's easy and requires no technical knowledge, which does make me wonder why they announced it at developer day given there's no development needed, but lets not tilt at that windmill.</div><div><br /></div><h3 style="text-align: left;">A Couple of Warnings</h3><div><br /></div><div>First, remember that any private information that you upload to a GPT won't necessarily remain private. If you don't instruct your custom GPT to keep instructions and material private, it will happy share them on request. </div><div><br /></div><div>Second, I've given the replicant a mischievous side - from time to time it will just gainsay your original decisions when you ask for help with specific problems, maybe suggesting you have picked the wrong Salesforce technology, or telling you to bin it all off and use another vendor. Think of this as your reminder that a human should always be involved in any decision making based on advice from AI.</div><div><br /></div><h3 style="text-align: left;">I'm Going to be Rich?</h3><div><br /></div><div>Something else that was announced at Developer Day was revenue sharing - if people use Bob Buzzard 2.0 I'll get a slice of the pie. So does this mean I'm going to be rich? Like always, almost certainly not. As you just click a button and answer questions to create a GPT, there will be millions of them before too long. They are so easy to create that something a service like Salesforce development advice, with the vast amount of content already in the public domain, will be extremely competitive - an extremely crowded marketplace of similar products means everybody earns nothing.</div><div><br /></div><div>That said, I think this is something that genuine creatives will be able to earn with. Rather than having their work used to train models that are can then be used to produce highly derivative works for close to free, they can create their own GPT and at least stand a chance of getting paid. Whether the earnings will be worth it we don't yet know, although history suggests the platform providers will keep everything they can. </div><div><br /></div><div><h3>Related Information</h3><div><ul><li><a href="https://bobbuzzard.blogspot.com/2023/11/the-einstein-trust-layer-must-become.html" target="_blank">The Einstein Trust Layer must become the Einstein Trust Platform</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/10/einstein-sales-emails.html" target="_blank">Einstein Sales Emails</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/08/salesforce-cli-open-ai-plug-in-function.html" target="_blank">Salesforce CLI OpenAI Plug-in - Function Calling</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-cli-open-ai-plug-in.html" target="_blank">Salesforce CLI OpenAI Plug-In - Generating Records</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-gpt-its-alive.html" target="_blank">Salesforce GPT - It's Alive!</a></li><li><a href="https://www.youtube.com/watch?v=uq6sVscJ52E" target="_blank">Get AI Ready - Prompt Engineering (BrightGen Webinar Recording)</a></li><li><a href="https://youtu.be/5M3U3fbV9zg" target="_blank">Get AI Ready - Prepare for AI Cloud (BrightGen webinar recording)</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-cli-openai-plug-in.html" target="_blank">Salesforce CLI OpenAI Plug-in</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-world-tour-london-ai-day.html" target="_blank">Salesforce World Tour London : AI Day</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/07/predictions-for-digital-life-in-2035.html" target="_blank">Predictions for Digital Life in 2035</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/03/einstein-gpt-rise-of-machines.html" target="_blank">Einstein GPT - Rise of the Machines?</a></li></ul><div><div><a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a></div><div><div><div><br /></div><div><br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe></div></div><div class="separator" style="clear: both; text-align: center;"><br /></div></div></div></div></div>Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-73507651477148859142023-11-04T12:07:00.002+00:002023-11-04T12:07:42.786+00:00The Einstein Trust Layer must become the Einstein Trust Platform<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitQQ0S29iGMyO8M4Y6DvHswl0IPK0p0oR2bbxYlipQAHhQcN4Fpco6j1NCAV7aF8WWvf-7MLaJE0iLeu6kAm7y3Yc8kJ_EakGr6rY3U9IJ2_68yccjwvyGMXX3EFavSntB9PiuOK112xgqLKCBYBb4-ZWhNc7OdsJzt244a92-y4rNXQ3YAk89B_S5e8BT/s1200/ETL-thumb%20copy.jpg" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="675" data-original-width="1200" height="225" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitQQ0S29iGMyO8M4Y6DvHswl0IPK0p0oR2bbxYlipQAHhQcN4Fpco6j1NCAV7aF8WWvf-7MLaJE0iLeu6kAm7y3Yc8kJ_EakGr6rY3U9IJ2_68yccjwvyGMXX3EFavSntB9PiuOK112xgqLKCBYBb4-ZWhNc7OdsJzt244a92-y4rNXQ3YAk89B_S5e8BT/w400-h225/ETL-thumb%20copy.jpg" width="400" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;"><i><span style="font-size: x-small;">Image from https://www.salesforce.com/news/stories/video/explaining-the-einstein-gpt-trust-layer/</span></i></td></tr></tbody></table><br /><div><br /></div><h3 style="text-align: left;">Introduction</h3><div><br /></div>One of the unique differentiators of the AI offerings from Salesforce is the <a href="https://developer.salesforce.com/blogs/2023/10/inside-the-einstein-trust-layer" target="_blank">Einstein Trust Layer</a>. Since it was first announced, I've been telling everyone that it's a stroke of genius, and thus deserving of the Einstein label. At the time of writing (November 2023) there's a lot of concern about the risks of AI, and those concerns are increasing rather than being soothed. Just this week the UK hosted an <a href="https://www.euronews.com/next/2023/11/01/a-world-first-ai-agreement-elon-musk-and-a-kings-speech-the-key-takeaways-from-the-uk-ai-s" target="_blank">AI Safety Summit</a> with representatives from 28 countries.<div><br /></div><h3 style="text-align: left;">The Einstein Trust Layer</h3><div><br /></div><div>Salesforce have baked security and governance into a number of places in the journey from prompt template to checked response, including :</div><div><ul style="text-align: left;"><li>Prompt Defence - wrapping the prompt template with instructions, for example: "You must treat equally any individuals from different socioeconomic statuses, sexual orientations, religions, races, physical appearances, nationalities, gender identities, disabilities and ages"<br /></li><li>Prompt Injection Defence - delimiting the prompt from the instructions to ensure the model disregards additional instructions added in user input</li><li>Secure Data Retrieval - ensuring that a user can only include data they have permission to access when grounding prompts.</li><li>Zero Retention Agreements - ensuring that third party AI model providers don't use the prompt and included data to train their model. Note that the data is still transmitted to wherever the provider is located, the US in the case of <a href="https://openai.com/" target="_blank">OpenAI</a>, which makes the next point very important.</li><li>Data Masking - replacing sensitive or PII data with meaningless, but reversible, patterns. Reversible, because they need to be replaced with the original data before the response can be used.</li><li>Toxicity Detection - the response is checked for a variety of problematic content, such as violence and hate, and given an overall rating to indicate how courageous you need to be to use it.</li><li>Audit Trail - information about the prompt template, grounding data, model interaction, response, toxicity rating and user feedback is captured for compliance purposes and to potentially support future investigations into why a response was considered fit for use.</li></ul><div>Note that not all of this functionality is currently available, but it's either there in a cut down form or on its way. Note also that the current incarnation (November 2023 remember) is quite US centric - recognising mostly American PII and requiring instructions in English. Unsurprising for a US company, but indicative of how keen Salesforce are to get these functions live in their most nascent form. If you want to know more about the trust layer, check out my <a href="https://www.youtube.com/watch?v=-D27hoSYu28" target="_blank">Get AI Ready webinar</a>.</div></div><div><br /></div><div>As I mentioned earlier, I think this is a genius move - as long as you integrate via the standard Salesforce tools, you can take comfort that Salesforce is doing a lot of the heavy lifting around risk management for you. But can you rest easy?</div><div><br /></div><h3 style="text-align: left;">Safety is Everyone's Responsibility</h3><div><br /></div><div><div>Of course you can't rest easy. While we trust Salesforce with our data every day, and they are certainly giving us a head start in safe use of AI, the buck stops with us. Something else I've been saying to anyone who will listen is that we should trust Salesforce, but it can't be blind trust. We know quite a lot about how the Einstein Trust Layer works, but we have to be certain that it is applying the rules that we want in place, rather than a set of generic rules that doesn't quite cover what we need. One-size-doesn't-quite-fit-all if you will. </div><div><br /></div><h3 style="text-align: left;">The Layer must become the Platform</h3><div><br /></div><div>And this brings me to the matter at hand of this post - the Trust Layer needs to become the Trust Platform that we can configure and extend to satisfy the unique requirements of our businesses. In no particular order, we need to be able to :</div><div><ul style="text-align: left;"><li>Define our own rules and patterns for data masking</li><li>Create our own toxicity topics, and adjust the overall ratings based on our own rules</li><li>Add our own defensive instructions to the prompts. <br />Yes, I know we'll be able to do this on a prompt by prompt basis, but I'm talking about company standard instructions that need to be added to all prompts. It will get tedious to manually add these to every prompt, and even more tedious to update them all manually when minor changes are required.</li><li>Include additional information in the audit logs</li></ul><div>and much more - plugins that carry out additional risk mitigation that isn't currently part of the Salesforce "stack". Feels like there's an AppExchange opportunity here too!</div></div><div><br /></div><div>Once we have this, we'll be able to say we are using AI responsibly, to the best of our ability and current knowledge at any rate.</div><div><br /></div></div><div><h3>Related Information</h3><div><ul><li><a href="https://bobbuzzard.blogspot.com/2023/10/einstein-sales-emails.html" target="_blank">Einstein Sales Emails</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/08/salesforce-cli-open-ai-plug-in-function.html" target="_blank">Salesforce CLI OpenAI Plug-in - Function Calling</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-cli-open-ai-plug-in.html" target="_blank">Salesforce CLI OpenAI Plug-In - Generating Records</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-gpt-its-alive.html" target="_blank">Salesforce GPT - It's Alive!</a></li><li><a href="https://www.youtube.com/watch?v=uq6sVscJ52E" target="_blank">Get AI Ready - Prompt Engineering (BrightGen Webinar Recording)</a></li><li><a href="https://youtu.be/5M3U3fbV9zg" target="_blank">Get AI Ready - Prepare for AI Cloud (BrightGen webinar recording)</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-cli-openai-plug-in.html" target="_blank">Salesforce CLI OpenAI Plug-in</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-world-tour-london-ai-day.html" target="_blank">Salesforce World Tour London : AI Day</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/07/predictions-for-digital-life-in-2035.html" target="_blank">Predictions for Digital Life in 2035</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/03/einstein-gpt-rise-of-machines.html" target="_blank">Einstein GPT - Rise of the Machines?</a></li></ul><div><div><a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a></div><div><div><div><br /></div><div><br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe></div></div><div class="separator" style="clear: both; text-align: center;"><br /></div></div></div></div></div>Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-1404621815136978002023-10-14T12:22:00.004+01:002023-10-14T12:39:18.172+01:00Einstein Sales Emails<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjfgYEDCfMoxLlLB51-kw8r0GNzn7KgKKhJhXC2sxD-HgKnGgtnRsmqCZe8LNbCUkNFKScmY1I3aYk79Ff4PoUYfLGTwUOgrc31W7oNtWnC8UAk_ujr59QJWr-xsV9FSV_iPyybZEy9ofopZATnllXQO2uWx1WEfSHkO4rgUWQP4flGTmUMyWG5JURCXql1/s768/einstein.jpeg" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="768" data-original-width="768" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjfgYEDCfMoxLlLB51-kw8r0GNzn7KgKKhJhXC2sxD-HgKnGgtnRsmqCZe8LNbCUkNFKScmY1I3aYk79Ff4PoUYfLGTwUOgrc31W7oNtWnC8UAk_ujr59QJWr-xsV9FSV_iPyybZEy9ofopZATnllXQO2uWx1WEfSHkO4rgUWQP4flGTmUMyWG5JURCXql1/s320/einstein.jpeg" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><br /></div><div style="text-align: center;"><i><span style="font-size: x-small;">Image created by StableDiffusion 2.1 based on a prompt by Bob Buzzard</span></i></div><div style="text-align: center;"><i><span style="font-size: x-small;"><br /></span></i></div><h3 style="text-align: left;">Introduction</h3><p>
Sales GPT went GA in July 2023, and then went through a couple of "blink and
you'll miss it" renames, before it's was rolled into (October 2023, so it
might have changed by the time you read this!) Einstein for Sales. From the
Generative AI perspective, this consists of a couple of features - call
summaries, and the subject of this post - Sales Emails.
</p>
<h3 style="text-align: left;">Turning it On</h3>
<p>
This feature is pretty simply to enable - first turn on Einstein for Sales in
Setup:
</p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2Cp3JZRRFgUESwTdOLJDyBKB8jyKv8GDTKQAyxSgKxXfwdvQwM1BWFQXiL6vWeFiu1NPQJ9s9gmHLqqdH6Im1fNvUg3s0Vc0wUHHdtvOKlJxk3JYNN4mMApBIun4XVi5eK4uWoyNlwKLvHh2NyoRRlfr6X0eWsqln7PQs9e18TxvQSZ7gEhXn3SC7LJKF/s1886/Screenshot%202023-10-14%20at%2011.33.16.png" style="margin-left: 1em; margin-right: 1em;"><img border="1" data-original-height="898" data-original-width="1886" height="304" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2Cp3JZRRFgUESwTdOLJDyBKB8jyKv8GDTKQAyxSgKxXfwdvQwM1BWFQXiL6vWeFiu1NPQJ9s9gmHLqqdH6Im1fNvUg3s0Vc0wUHHdtvOKlJxk3JYNN4mMApBIun4XVi5eK4uWoyNlwKLvHh2NyoRRlfr6X0eWsqln7PQs9e18TxvQSZ7gEhXn3SC7LJKF/w640-h304/Screenshot%202023-10-14%20at%2011.33.16.png" width="640" /></a>
</div>
<div><br /></div>
Then assign yourself the permission set:
<div>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiPWn24abIS9SJ0VJg1XSUwlPYR2DJ62xp4_vcrUcu9EQRS872b052zGL7KDLWjfOko3EMPesdsgxmTUG7AISKDoTDkzbPgt3lfJRm7rYi3OSGHt5pNz1A-m9QkJB26YlIFefZV55B3IEaD8pO2zkIHKQ9wIciw9w9hHAkSL2R8Nxh8ptHgFqtcHkhgix-6/s2204/Screenshot%202023-10-14%20at%2011.35.05.png" style="margin-left: 1em; margin-right: 1em;"><img border="1" data-original-height="594" data-original-width="2204" height="172" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiPWn24abIS9SJ0VJg1XSUwlPYR2DJ62xp4_vcrUcu9EQRS872b052zGL7KDLWjfOko3EMPesdsgxmTUG7AISKDoTDkzbPgt3lfJRm7rYi3OSGHt5pNz1A-m9QkJB26YlIFefZV55B3IEaD8pO2zkIHKQ9wIciw9w9hHAkSL2R8Nxh8ptHgFqtcHkhgix-6/w640-h172/Screenshot%202023-10-14%20at%2011.35.05.png" width="640" /></a>
</div>
<br />
<h3 style="text-align: left;">Creating Emails</h3>
<div><br /></div>
<div>
Once the setup is complete, opening the Email Composer shows a shiny new
button to draft with Einstein:
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjN7hn1h-VXCN_kJy1p1lpbqVdjY010bFlA_ULhAxTbA5R4EAGo0FbHlTAAbuZnlAyordRAHY69d5cSOE0lZ1qZwterQIRE1jIaA3acCLNLfd6i3kd4I5g3aLFXwmcuX_wyBdX_hjHZMp4Q2grjYsfk3sbOEOJvbYzeoJuE8zPgiX8B3s_ZZW0EA3cQd2nJ/s982/Screenshot%202023-10-14%20at%2011.47.45.png" style="margin-left: 1em; margin-right: 1em;"><img border="1" data-original-height="974" data-original-width="982" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjN7hn1h-VXCN_kJy1p1lpbqVdjY010bFlA_ULhAxTbA5R4EAGo0FbHlTAAbuZnlAyordRAHY69d5cSOE0lZ1qZwterQIRE1jIaA3acCLNLfd6i3kd4I5g3aLFXwmcuX_wyBdX_hjHZMp4Q2grjYsfk3sbOEOJvbYzeoJuE8zPgiX8B3s_ZZW0EA3cQd2nJ/w400-h396/Screenshot%202023-10-14%20at%2011.47.45.png" width="400" /></a>
</div>
<br />
<div class="separator" style="clear: both; text-align: center;"><br /></div>
<div>
In this case I'm sending the email from the Al Miller contact record, and
I've selected Al's account - Advanced Communications - from the
dropdown/search widget at the bottom. This will be used to ground the prompt
that is sent to OpenAI, to include any relevant details from the records in
the email.
</div>
<div><br /></div>
<div>
Clicking the Draft with Einstein button offers me a choice of 5
pre-configured prompts - note that Salesforce doesn't yet offer the
capability to create your own prompts, although that is definitely coming
soon.
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjeoZl1F8Y9Siyx8G3VNj8kGedBunKh7s96hddcv1HlhvBZNRGhz4Yy0OHbskXVLNenEpO38ABcL56e5nkmXKBeFWDsZSehSySp3ftzaHwpFKCqN11iQfSkXII8ym04QeQUHelMNTTGv0I7MLhjIYLcMr6ZtEc9MRV27JRkY-gKIjyFowrT_FnxlPtVq6XP/s496/Screenshot%202023-10-14%20at%2011.50.48.png" style="margin-left: 1em; margin-right: 1em;"><img border="1" data-original-height="466" data-original-width="496" height="301" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjeoZl1F8Y9Siyx8G3VNj8kGedBunKh7s96hddcv1HlhvBZNRGhz4Yy0OHbskXVLNenEpO38ABcL56e5nkmXKBeFWDsZSehSySp3ftzaHwpFKCqN11iQfSkXII8ym04QeQUHelMNTTGv0I7MLhjIYLcMr6ZtEc9MRV27JRkY-gKIjyFowrT_FnxlPtVq6XP/s320/Screenshot%202023-10-14%20at%2011.50.48.png" width="320" /></a>
</div>
<div><br /></div>
<div><br /></div>
<div>
Since GA this feature has been improved with the ability to include product
information, so once I choose the type of prompt - Send a Meeting Invite in
this case - I have the option to choose a product to refer to.
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg8Q0AyjtP5NKojVoJESS6uiNQ-jK8Ktr4bYED5XeydY4VuuLBdXzzml8Wxu9eZEkdE-J0Hpld85BvtepNZEGB9OmQ-8QmVVBsGKm2f79lyYqdB9ECDWZx5wS8PmSguJIAcw5_Hf-dJZImdTPAnGQV9UT3ew4Qw5oOw3Rmzet440jM1tTTLGrtUiJNNIABM/s1890/Screenshot%202023-10-14%20at%2011.53.25.png" style="margin-left: 1em; margin-right: 1em;"><img border="1" data-original-height="806" data-original-width="1890" height="272" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg8Q0AyjtP5NKojVoJESS6uiNQ-jK8Ktr4bYED5XeydY4VuuLBdXzzml8Wxu9eZEkdE-J0Hpld85BvtepNZEGB9OmQ-8QmVVBsGKm2f79lyYqdB9ECDWZx5wS8PmSguJIAcw5_Hf-dJZImdTPAnGQV9UT3ew4Qw5oOw3Rmzet440jM1tTTLGrtUiJNNIABM/w640-h272/Screenshot%202023-10-14%20at%2011.53.25.png" width="640" /></a>
</div>
<br />
<div>
Once I choose a product, the Name and Description is pulled from the
record, but I can add more information that might be relevant for this
Contact - the words with the red border below. Note that there's a
limited number of characters allowed here - I was within 5-10 of the limit.
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBSo7z4TUjqmI7Ofbm2iAJfpkOH28B3SsGcuZld2QAcQBXDIxOvebygd96q21PKeNfhSXAuZeTkofyYCUFvr01AlqlYnmp7gEHFKzKYQ2pozV7o5QSjtA80pLGGUQfMPzzxZGAsX78dF1wzZlr7JkRFjeyOch6gz-gFBj-_g6jvnx3dz5P-p75tBXUiqTH/s1892/Screenshot%202023-10-14%20at%2011.57.25.png" style="margin-left: 1em; margin-right: 1em;"><img border="1" data-original-height="716" data-original-width="1892" height="242" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBSo7z4TUjqmI7Ofbm2iAJfpkOH28B3SsGcuZld2QAcQBXDIxOvebygd96q21PKeNfhSXAuZeTkofyYCUFvr01AlqlYnmp7gEHFKzKYQ2pozV7o5QSjtA80pLGGUQfMPzzxZGAsX78dF1wzZlr7JkRFjeyOch6gz-gFBj-_g6jvnx3dz5P-p75tBXUiqTH/w640-h242/Screenshot%202023-10-14%20at%2011.57.25.png" width="640" /></a>
</div>
<br />
<div class="separator" style="clear: both; text-align: center;"><br /></div>
<div>
Clicking the Continue button starts the process of pulling relevant
information from the related account to ground the prompt, adding the
guardrails to ensure the response is non-toxic, and validating the response
before offering it to me:<br />
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgGI3AIeI-E0D0AwNRY1jwDX1KUjhbDUYABPNTRWRZ9GO6yejmSld0P3xuxBNyalK4W5g0CBu_ygqcqsgmffCrPF0chAruc-9WWi1NBIIxB3jUFWtx4aPACB1otFH-Zsu4RoWWc86pAIWG4mYxXlPU1nfz9R3Ardh1QL98bdrEuJdhk108bJ1nHEX977aIK/s1890/Screenshot%202023-10-14%20at%2012.00.18.png" style="margin-left: 1em; margin-right: 1em;"><img border="1" data-original-height="1110" data-original-width="1890" height="439" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgGI3AIeI-E0D0AwNRY1jwDX1KUjhbDUYABPNTRWRZ9GO6yejmSld0P3xuxBNyalK4W5g0CBu_ygqcqsgmffCrPF0chAruc-9WWi1NBIIxB3jUFWtx4aPACB1otFH-Zsu4RoWWc86pAIWG4mYxXlPU1nfz9R3Ardh1QL98bdrEuJdhk108bJ1nHEX977aIK/w746-h439/Screenshot%202023-10-14%20at%2012.00.18.png" width="746" /></a>
</div>
<br />
<div>
You can see where the grounding information has been used in the response -
Al's name, role and company appear in the second paragraph, and the product
information (including my added info) is in the third to try to entice Al to
bite at a meeting.
</div>
<div><br /></div>
<div>
If I don't care for this response I can edit and tweak it, or click the
button again to get a new response:
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWyduMjLUhXvusmb0DMvqzQO1SdbsFc1Wv3PxrHKGNBEUrahqoY7ZRRBWbHiiCuWuT1AL1ex20ZT7vVpKj0GT8PUKE-j4RNrhMFhtvnzumQCmXJ8gBXN6TINLADCe1A6G434ParqrA48bsoH1NbAfGLSBXD33IWaIxbq3Ar9LwwM6-bCSaXEe6ADDHQrtv/s1886/Screenshot%202023-10-14%20at%2012.07.42.png" style="margin-left: 1em; margin-right: 1em;"><img border="1" data-original-height="506" data-original-width="1886" height="184" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWyduMjLUhXvusmb0DMvqzQO1SdbsFc1Wv3PxrHKGNBEUrahqoY7ZRRBWbHiiCuWuT1AL1ex20ZT7vVpKj0GT8PUKE-j4RNrhMFhtvnzumQCmXJ8gBXN6TINLADCe1A6G434ParqrA48bsoH1NbAfGLSBXD33IWaIxbq3Ar9LwwM6-bCSaXEe6ADDHQrtv/w692-h184/Screenshot%202023-10-14%20at%2012.07.42.png" width="692" /></a>
</div>
<br />
<h3 style="text-align: left;">Gotchas</h3>
<div><br /></div>
<div>
Note that this just adds the next response under the previous one, as you
can see by the 'Best regards' at the top of the screenshot. If I don't want
to use a response, it's up to me to delete it. Make sure to check the entire
content before sending, as this it would be pretty embarrassing to let one
go out with 4-5 different emails in it! Note also that I'm expected to add
the date and time that I want to meet, to replace the
</div>
<div><br /></div>
<div> <b>[Customize: DATE AND TIME]</b></div>
<div><br /></div>
<div>
I can't help thinking that we'll all start receiving emails with these
placeholders still there, much like at the moment when merge fields go bad!</div><div><br /></div>
<h3 style="text-align: left;">Related Information</h3>
<div><div><ul><li><a href="https://bobbuzzard.blogspot.com/2023/08/salesforce-cli-open-ai-plug-in-function.html" target="_blank">Salesforce CLI OpenAI Plug-in - Function Calling</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-cli-open-ai-plug-in.html" target="_blank">Salesforce CLI OpenAI Plug-In - Generating Records</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-gpt-its-alive.html" target="_blank">Salesforce GPT - It's Alive!</a></li><li><a href="https://www.youtube.com/watch?v=uq6sVscJ52E" target="_blank">Get AI Ready - Prompt Engineering (BrightGen Webinar Recording)</a></li><li><a href="https://youtu.be/5M3U3fbV9zg" target="_blank">Get AI Ready - Prepare for AI Cloud (BrightGen webinar recording)</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-cli-openai-plug-in.html" target="_blank">Salesforce CLI OpenAI Plug-in</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-world-tour-london-ai-day.html" target="_blank">Salesforce World Tour London : AI Day</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/07/predictions-for-digital-life-in-2035.html" target="_blank">Predictions for Digital Life in 2035</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/03/einstein-gpt-rise-of-machines.html" target="_blank">Einstein GPT - Rise of the Machines?</a></li></ul><div><div><a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a></div><div><div><div><br /></div><div><br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe></div></div><div class="separator" style="clear: both; text-align: center;"><br /></div></div></div></div><p></p></div>
</div>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com1tag:blogger.com,1999:blog-6140199022860280334.post-28039275909254789672023-09-30T17:08:00.003+01:002023-09-30T17:22:17.062+01:00Apex Comparator in Winter 24<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg6OA4Cnf-HyGvxt7LqjOxwckBiXISR2ltbzqgV6gLxehDaN-Uc6BDQqKH8zr9u61_IufMbnt05CVCAWI4Na1MF7AROFxCPBJmmFVzpHq2vZrItb-B-rI6LWDC-0FKaISoBKUAL3u0LYoijvYmq1B2u4ADtKMmIKdh-9NX90PcIjux69aGzgw5Udr9pQ7Wk/s768/sort.jpeg" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="768" data-original-width="768" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg6OA4Cnf-HyGvxt7LqjOxwckBiXISR2ltbzqgV6gLxehDaN-Uc6BDQqKH8zr9u61_IufMbnt05CVCAWI4Na1MF7AROFxCPBJmmFVzpHq2vZrItb-B-rI6LWDC-0FKaISoBKUAL3u0LYoijvYmq1B2u4ADtKMmIKdh-9NX90PcIjux69aGzgw5Udr9pQ7Wk/s320/sort.jpeg" width="320" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;"><i>Image generated by Stable Diffusion from a prompt by Bob Buzzard</i></td></tr></tbody></table><h3>Introduction</h3>
<p>
The Winter 24 release of Salesforce introduces a few new Apex features,
including one that I'm very pleased to see - the Comparator interface. Simply
put, this new interface allows the List.sort() method to take a parameter that
determines the sort order of the elements in the List.
</p>
<h3 style="text-align: left;">The Problem</h3>
<p>
Now this might not sound like a big change, but it simplifies the support of
sorting Lists quite a bit. The way we used to have to do it was for the items
in the List to implement the Comparable interface. I've written loads of
custom Apex classes over the years that implement this, and it's very
straightforward - here's an example from a class that retrieves the code
coverage values for all Apex classes in an org and displays them in order of
coverage with the lowest covered (problem!) classes first :
</p>
<pre class="brush:java; toolbar: true">public Integer compareTo(Object compareTo)
{
CoverageRecord that=(CoverageRecord) compareTo;
return this.getPercentage()-that.getPercentage();
}
</pre>
In this case, implementing
<span style="font-family: courier;">compareTo</span> isn't any real overhead -
I've created a custom class that contains a whole bunch of information about the
coverage for an Apex class - total lines, lines covered etc, so an extra method
with a couple of lines of code isn't a big deal. It's a little less convenient
if I need to sort a class from an App Exchange package - in that case I'll need
to create a new class from scratch to wrap the packaged class and implement the
method. If we assume that my coverage class is now in a package, it would look
something like :
<div>
<pre class="brush:java; toolbar: true">public class CoverageWrapper
{
public BBCOVERAGE__CoverageRecord coverage {get; set;}
public Integer compareTo(Object compareTo)
{
BBCOVERAGE__CoverageRecord that=(BBCOVERAGE__CoverageRecord) compareTo;
return this.coverage.getPercentage()-that.getPercentage();
}
}
</pre>
<p>
Slightly less convenient - I now have a whole new class to maintain to be
able to sort, and I have to store all the elements of the list in a
CoverageWrapper rather than their original CoverageRecord. Again, not a huge
amount of overhead but it gets a bit samey if I'm doing a lot of this kind
of thing.
</p>
<p>
Much the same thing applies if I want to sort sObjects - I need to create a
wrapper class and turn my list of sObjects into a list of the wrapper class
before I can sort it. All those CPU cycles gone forever!
</p>
<h3 style="text-align: left;">The Solution</h3>
<p>
This all changes in Winter 24 with the Comparator interface. I still need to
create a class that implements an interface - the Comparator in this case :</p>
<pre class="brush:java; toolbar: true">public with sharing class CoverageComparator implements Comparator<CoverageRecord>
{
public Integer compare(CoverageRecord one, CoverageRecord tother)
{
return one.getPercentage()-tother.getPercentage();
}
}
</pre>
<p>but I don't need to wrap the class/sObject that I am processing in this
class and create a new list. Instead I call the new sort() method that takes
a Comparator parameter:
</p>
<pre class="brush:java; toolbar: true">List<CoverageRecord> coverageRecords;
...
CoverageComparator covComp=new CoverageComparator();
coverageRecords.sort(covComp);
</pre>
<h3 style="text-align: left;">CPU Impact</h3><div><br /></div><div>Regular readers of this blog will know that I'm always interested in the impact of changes on CPU time. In enterprise implementations CPU limits are something that I run up against again and again, so if a new feature improves this I want to know!\</div><div><br /></div><div>I used my usual methodology to test this - execute anonymous with most logging turned off and Apex at the error level, executing the same code three times and taking the average.</div><div><br /></div><div>For each test I created the records before capturing the CPU time, then sorted the list. First using a comparator on the list of CoverageRecord objects:</div>
</div>
<pre class="brush:java; toolbar: true">List<CoverageRecord> covRecs=TestData.CreateRecords(100);
Integer startCpu=Limits.getCpuTime();
CoverageComparator covComp=new CoverageComparator();
covRecs.sort(covComp);
Integer stopCpu=Limits.getCpuTime();
System.debug(LoggingLevel.ERROR,
'CPU for comparator = ' + (stopCpu-startCpu));
</pre>
<p>And secondly wrapping them with classes that implement Comparable:</p>
<pre class="brush: java; toolbar: true">List<CoverageRecord> covRecs=TestData.CreateRecords(100);
Integer startCpu=Limits.getCpuTime();
List<CoverageWrapper> wrappers=new List<CoverageWrapper>();
for (CoverageRecord covRec : covRecs)
{
CoverageWrapper wrapper=new CoverageWrapper();
wrapper.coverage=covRec;
wrappers.add(wrapper);
}
wrappers.sort();
Integer stopCpu=Limits.getCpuTime();
System.debug(LoggingLevel.ERROR,
'CPU for comparable = ' + (stopCpu-startCpu));
</pre>
<p>The results were broadly what I was expecting, as sorting a list in place is always going to be quicker than iterating it, wrapping the members, and then sorting, but it's always good to see the numbers:</p><p></p><ul style="text-align: left;"><li>For 100 records, Comparator took 9 milliseconds versus 11 milliseconds for wrapping</li><li>For 1,000 records, Comparator took 126 milliseconds versus 150 for wrapping</li><li>For 10,000 records, Comparator took 1844 millseconds versus 2058 for wrapping</li></ul><div>So once you get up to decent sized lists, there's a 10% difference, well worth saving. </div><div><br /></div><h3 style="text-align: left;">Columbo Close</h3><div><br /></div><div>Just one more thing ... 1844 milliseconds is still quite a chunk of the CPU limit. This is because my original implementation of the CoverageRecord calculates the percentage on demand, based on stored total and covered lines. Clearly in a large list this is being called a lot. I then re-ran the 10,000 test with a new implementation - CoverageRecordCachePercent - which stores the percentage after calculating, which knocked 300 milliseconds, or 15%, off the time. I'm sure that converting again to something that calculated the percentage once the total and covered lines were known and set it as a public property would reduce it further. Your regular reminder that even the smallest method can have an impact if it's being called a large number of times!</div><div><br /></div><h3 style="text-align: left;">More Information</h3><div><ul style="text-align: left;"><li><a href="https://github.com/keirbowden/Winter24" target="_blank">Github repository with the sample code</a></li><li><a href="https://help.salesforce.com/s/articleView?id=release-notes.rn_apex_comparator_collator_interface.htm&release=246&type=5" target="_blank">Salesforce Release Notes for the Comparator interface</a></li><li><a href="http://bobbuzzard.blogspot.com/2022/05/the-cpu-effects-of-sorting.html" target="_blank">The CPU Effects of Sorting</a></li></ul></div><p></p><div><a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a></div><div><div><div><div><br /></div><div><br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe></div></div><div class="separator" style="clear: both; text-align: center;"><br /></div></div></div>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-49481137511929856172023-08-06T16:06:00.004+01:002023-08-06T16:08:08.306+01:00Salesforce CLI Open AI Plug-in - Function Calling<p>
<span></span></p><div class="separator" style="clear: both; color: #cc0000; text-align: center;"><span><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjabQpQImnTu_pO7vBd5cpAQraFQCtr8-x1KIw7u9-Y0QQsI-rS3NcLQcSQE5tR7iBThKUGKMDhG5KWt2avFnotm_RB_XsA-InJr71JoqKz_2PhTVFY5UheMN2aP3DfAP_dUE7Os8FqVsbXlZmngpvbB5sIRh-wmQhmWXLGK_Ypz2mKIZf18qNGPJILzSoX/s512/orgdata.jpeg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="512" data-original-width="512" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjabQpQImnTu_pO7vBd5cpAQraFQCtr8-x1KIw7u9-Y0QQsI-rS3NcLQcSQE5tR7iBThKUGKMDhG5KWt2avFnotm_RB_XsA-InJr71JoqKz_2PhTVFY5UheMN2aP3DfAP_dUE7Os8FqVsbXlZmngpvbB5sIRh-wmQhmWXLGK_Ypz2mKIZf18qNGPJILzSoX/s320/orgdata.jpeg" width="320" /></a></span></div><span><i><div style="text-align: center;"><i><span style="font-size: x-small;">Image generated by Stable Diffusion online, based on a prompt by Bob Buzzard</span></i></div></i></span><p></p><p><span style="color: #cc0000;"><b>WARNING: The new command covered in this blog extracts information from
your Salesforce database and sends it to OpenAI in the US to provide
additional grounding to a prompt - only use this with test/fake data, as
there is no attempt at masking or restricting field access.</b></span>
</p>
<h3 style="text-align: left;">Introduction</h3>
<p>
Back in June 2023, only a couple of months ago on the calendar but a lifetime
in generative AI product releases,
<a href="https://openai.com/blog/function-calling-and-other-api-updates" target="_blank">OpenAI announced the availability of function calling</a>. Now that my plug-in is integrated with gpt-3.5+, this is now something I
can use, but what value does it add?
</p>
<p>
The short version - this allows the model to progress past it's training data
and request more information to satisfy a prompt.
</p>
<p>
The longer version. As we all know, the data used to train gpt-3.5 cut off at
September 2021, so often the response to a prompt will warn you that things
may have changed. With function calling, when you prompt the model you also
tell it about any functions that you have available that it can use to
retrieve additional information. If the functions don't help it will return
the response as usual, but if they would it will return function calls for you
to make and pass back to it. Note that the model doesn't call the functions,
it tells you which functions to call and the parameters to pass and expects
you to make the decision as to whether you should call them, which is as it
should be.
</p>
<h3 style="text-align: left;">The Plug-in Command</h3>
<p>
In the
<a href="https://www.npmjs.com/package/bbai" target="_blank">latest version (1.2.2) of my plug-in</a>, there's a new command that gives the model access to a function to pull
data from Salesforce if needed. The function simply takes a query and returns
the result as a JSON formatted string:
</p>
<pre class="brush: javascript; toolbar: true">const queryData = async (query: string): Promise<string> => {
const authInfo = await AuthInfo.create({ username: flags.username });
const connection = await Connection.create({ authInfo });
const result = await connection.query<{ Name: string; Id: string }>(query);
return JSON.stringify(result);
}
</pre>
<p>
When the command is executed, the request to the Chat Completion API includes
the prompt supplied by the user, and details of the function:
</p>
<pre class="brush: javascript; toolbar: true">const functions: ChatCompletionFunctions[] = [
{
name: 'queryData',
description: 'A function to extract records from Salesforce via a SOQL query',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The SOQL query to execute',
},
},
required: ['query'],
}
}
];
</pre>
<p>
Note that my function isn't targeting any specific objects, nor does it have
any logic to figure out what might be needed based on the user's prompt - it
simply executes a query and returns the results, which might be an error or an
empty data set.
</p>
<p>I execute this command as follows:</p>
<p>
<span style="font-family: courier; font-size: x-small;">> sf bbai org data -t "Create an email introducing GenePoint, a
new account we are tracking in Salesforce. Include the industry and number
of employee details from our Salesforce database" -u
keir.bowden@googlemail.com</span>
</p>
<p>
Note that I have to tell the prompt that the GenePoint account details can be
found in our Salesforce database - if I don't do that it won't see any value
in the function. I've also provided the description of a couple of fields that
I want it to extract, and finally I've supplied the username that I'll connect
to Salesforce with if the model asks me to run a query.
</p>
<p>
When the model responds, it will indicate if it wants me to execute the
function by specifying the finish_reason as 'function_call', and adding the
details in the function_call property of the message:
</p>
<pre class="brush: javascript; toolbar: true">{
index: 0,
message: {
role: 'assistant',
content: null,
function_call: {
name: 'queryData',
arguments: '{\n' +
` "query": "SELECT Industry, NumberOfEmployees FROM Account
WHERE Name = 'GenePoint' LIMIT 1"\n`
}'
}
},
finish_reason: 'function_call'
}</pre>
<p>
In this case it wants me to call queryData with a query parameter of a SOQL
query string that extracts details of the GenePoint account. I execute this,
then add the results of the query to the prompt message and retry my request.
This time I get a response with the email containing the details I wanted:
</p>
<p>
<span style="font-family: courier; font-size: x-small;">Subject: Introducing GenePoint - A Promising Addition to Our
Portfolio</span>
</p>
<p><span style="font-family: courier; font-size: x-small;">Dear Team,</span></p>
<p>
<span style="font-family: courier; font-size: x-small;">I hope this email finds you well. I am excited to introduce you to our
newest account, GenePoint, which we have recently started tracking in our
Salesforce database. GenePoint is a biotechnology company that shows immense
potential in its field. Allow me to provide you with some important details
about this account.</span>
</p>
<p>
<span style="font-family: courier; font-size: x-small;">Industry: Biotechnology</span>
</p>
<p>
<span style="font-family: courier; font-size: x-small;">Number of Employees: 265</span>
</p>
<h3 style="text-align: left;">Conclusion</h3>
<p>
I think this is very cool because I haven't had to inspect the prompt in any way to
decide to extract information from Salesforce. The model has been given a very
basic function and knows (with some nudging, for sure) when it is appropriate
to call it and, more importantly, the query that needs to be run to extract
the details the user requested.
</p>
<p>
Right now I've only told it about a single function, so it's either going to
call that or nothing, but I can easily imagine a collection of functions that
provide access to many internal systems. This allows the final request to be
grounded with a huge amount of relevant data, leading to a highly accurate and
targeted response.
</p>
<p>
Once again a reminder that this could result in sensitive or personally
identifiable information being sent to the OpenAI API in the US to
be processed, so while it's fun to try out you really don't want to go any
where near your production data with this.
</p>
<p>More Information</p>
<div>
<ul style="text-align: left;">
<li><a href="https://www.npmjs.com/package/bbai" target="_blank">The plug-in on NPMJS</a></li><li><a href="https://github.com/keirbowden/bbaiplugin" target="_blank">The plug-in source on Github</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-cli-open-ai-plug-in.html" target="_blank">Salesforce CLI OpenAI Plug-In - Generating Records</a></li><li>
<a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-gpt-its-alive.html" target="_blank">Salesforce GPT - It's Alive!</a>
</li>
<li>
<a href="https://youtu.be/5M3U3fbV9zg" target="_blank">Get AI Ready - Prepare for AI Cloud (BrightGen webinar recording)</a>
</li>
<li>
<a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-cli-openai-plug-in.html" target="_blank">Salesforce CLI OpenAI Plug-in</a>
</li>
<li>
<a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-world-tour-london-ai-day.html" target="_blank">Salesforce World Tour London : AI Day</a>
</li>
<li>
<a href="https://bobbuzzard.blogspot.com/2023/07/predictions-for-digital-life-in-2035.html" target="_blank">Predictions for Digital Life in 2035</a>
</li>
<li>
<a href="https://bobbuzzard.blogspot.com/2023/03/einstein-gpt-rise-of-machines.html" target="_blank">Einstein GPT - Rise of the Machines?</a>
</li>
</ul>
</div>
<p></p>
<div>
<a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a>
</div>
<div>
<div><br /></div>
<div>
<br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe>
</div>
</div>
<div class="separator" style="clear: both; text-align: center;"><br /></div>
<p><br /></p>
<p><br /></p>
<p><br /></p>
<p><br /></p>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-12787267121584530062023-07-30T17:05:00.003+01:002023-07-30T17:05:31.081+01:00Salesforce CLI Open AI Plug-in - Generating Records<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiAj5KRsNhMzE5SmTcBAec1cFejMbDgi5KrMNTdwvD8SsyGmRLQFmznwRBIzVe7FessJar_m3O9P9mMQEEcHYsWKHrdfgAL7DT1kSjlCdgFbUicYZONzVzzZB-rWoHgwmIchVwGs_0vm5SV5us4Jlljgd4nVvBobstDc_yaXMK4kDz-05XCVCWNzAMQGMll/s768/testdata.jpeg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="768" data-original-width="768" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiAj5KRsNhMzE5SmTcBAec1cFejMbDgi5KrMNTdwvD8SsyGmRLQFmznwRBIzVe7FessJar_m3O9P9mMQEEcHYsWKHrdfgAL7DT1kSjlCdgFbUicYZONzVzzZB-rWoHgwmIchVwGs_0vm5SV5us4Jlljgd4nVvBobstDc_yaXMK4kDz-05XCVCWNzAMQGMll/s320/testdata.jpeg" width="320" /></a></div><div style="text-align: center;"><i><span style="font-size: x-small;"><br />Image generated by Stable Diffusion 2.1, based on a prompt from Bob Buzzard</span></i></div><h3 style="text-align: left;">Introduction</h3>
<p>
After the go-live of
<a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-gpt-its-alive.html" target="_blank">Service and Sales GPT</a>, I felt that I had to revisit my
<a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-cli-openai-plug-in.html" target="_blank">Salesforce CLI Open AI Plug-in</a>
and connect it up to the GPT 4 Large Language Model. I didn't succeed in this
quest, as while I am a paying customer of OpenAI, I haven't satisfied the
requirement of making a successful payment of at least $1. The API function
I'm hitting,
<a href="https://platform.openai.com/docs/api-reference/chat/create" target="_blank">Create Chat Completion</a>, supports gpt-3.5-turbo and the gpt-4 variants, so once I've racked up
enough cost using the earlier models I can switch over by changing one
parameter. My current spending looks like it will take me a few months to get
there, but such is life with competitively priced APIs.
</p>
<h3 style="text-align: left;">The Use Case</h3>
<p>
The first incarnation of the plug-in asks the model to describe Apex, CLI or
Salesforce concepts, but I wanted something that was more of a tool than a
content generator, so I decided on creating test records. The new command
takes parameters listing the field names, the desired output format, and the
number of records required, and folds these into the messages passed to the
API function. Like the Completion API, the interface is very simple:
</p>
<pre class="toolbar: true; brush: javascript">const response = await openai.createChatCompletion({
model: 'gpt-3.5-turbo',
messages,
temperature: 1,
max_tokens: maxTokens,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
});
result = (response.data.choices[0].message?.content as string);
</pre>
<p>There's a few more parameters than the Completion API:</p>
<p></p>
<ul style="text-align: left;">
<li>
<b>model</b> - the Large Language Model that I send the request to. Right
now I've hardcoded this to the latest I can access
</li>
<li>
<b>messages</b> - the collection of messages to send. The messages build on
each other, and each message has a <b>content</b> (the instruction/request)
and a <b>role</b> (where the instruction is being sent). This allows me to
separate the instructions to the model (when the <b>role</b> is
<b>assistant</b>, I'm giving it constraints about how to behave) from the
request (when the <b>role</b> is <b>user</b>, this is the task/request I'm
asking it to carry out).
</li>
<li>
<b>max_tokens</b> is the maximum number of tokens (approximately 4
characters of text) that my request combined with the response can be. I've
set this to 3,500, which is approaching the limit of the gpt-3.5 model. If
you have a lot of fields you'll have to generate a smaller number of records
to avoid breaching this. I was able to create 50 records with 4-5 fields
inside this limit, but your mileage may vary.
</li>
<li>
<b>temperature</b> and <b>top_p</b> guide the model as to whether I want
precise or creative responses.
</li>
<li>
<b>frequency_penalty</b> and <b>presence_penalty</b> indicate whether I want
the model to continually focus on tokens if they are repeated, or focus on
new information.
</li>
</ul>
<p></p>
<p>
As this is an asynchronous API, I await the response, then pick the first
element in the choices array.
</p>
<p>
Here's a few executions to show it in action - linebreaks have been added to
the commands to aid legibility - remove these if you copy/paste the commands.
</p>
<pre>> sf bbai data testdata -f 'Index (count starting at 1), Name (Text, product name),
Amount (Number), CloseDate (Date yyyy-mm-dd),
StageName (One of these values : Negotiating, Closed Lost, Closed Won)'
-r csv
Here are the records you requested
Index,Name,Amount,CloseDate,StageName
1,Product A,1000,2022-01-15,Closed Lost
2,Product B,2500,2022-02-28,Closed Won
3,Product C,500,2022-03-10,Closed Lost
4,Product D,800,2022-04-05,Closed Won
5,Product E,1500,2022-05-20,Negotiating
</pre>
<br />
<pre>> sf bbai data testdata -f 'FirstName (Text), LastName (Text), Company (Text),
Email (Email), Rating__c (1-10)'
-n 4 -r json
Here are the records you requested
[
{
"FirstName": "John",
"LastName": "Doe",
"Company": "ABC Inc.",
"Email": "john.doe@example.com",
"Rating__c": 8
},
{
"FirstName": "Jane",
"LastName": "Smith",
"Company": "XYZ Corp.",
"Email": "jane.smith@example.com",
"Rating__c": 5
},
{
"FirstName": "Michael",
"LastName": "Johnson",
"Company": "123 Co.",
"Email": "michael.johnson@example.com",
"Rating__c": 9
},
{
"FirstName": "Sarah",
"LastName": "Williams",
"Company": "Acme Ltd.",
"Email": "sarah.williams@example.com",
"Rating__c": 7
}
]</pre>
<p>There's a few interesting points to note here:</p>
<p></p>
<ul style="text-align: left;">
<li>
Formatting field data is conversational - e.g. when I use
<span style="font-family: courier; font-size: x-small;">Date yyyy-mm-dd</span> the model knows that I want the date in ISO8601 format. For picklist
values, I just tell it 'One of these values' and it does the rest.
</li>
<li>
In the messages I asked it to generate realistic data, and while it's very
good at this for<span style="font-family: courier; font-size: x-small;">
First Name, Last Name, Email, Company</span>, it's not when told a
<span style="font-family: courier; font-size: x-small;">Name</span> field should be a
<span style="font-family: courier; font-size: x-small;">product name</span>,
just giving me
<span style="font-family: courier; font-size: x-small;">Product A, Product B</span>
etc.
</li>
<li>
It sometimes takes it a couple of requests to generate the output in a
format suitable for dropping into a file - I'm guessing this is because I
instruct the model and make the request in a single API call.
</li>
<li>
I've generated probably close to 500 records while testing this, and that
has cost me the princely sum of $0.04. If you want to play around with the
GPT models, it really is dirt cheap.
</li>
</ul>
<div>
The final point I'll make, as I did in the last post, is how simple the code
is. All the effort went into the messages to ask the model to generate the
data in the correct format, not to include additional information that it was
responding to the request, to generate realistic data. Truly the key
programming language for Generative AI is the supported language that you
speak - English in my case!
</div>
<div><br /></div>
<div>As before, you can install the plug-in via :</div>
<pre>> sf plugins install bbai</pre>
<div> or if you have already installed it, upgrade via : </div>
<pre>> sf plugins update</pre>
<h3 style="text-align: left;">More Information</h3>
<div>
<ul style="text-align: left;">
<li><a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-gpt-its-alive.html" target="_blank">Salesforce GPT - It's Alive!</a></li><li><a href="https://youtu.be/5M3U3fbV9zg" target="_blank">Get AI Ready - Prepare for AI Cloud (BrightGen webinar recording)</a></li><li>
<a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-cli-openai-plug-in.html" target="_blank">Salesforce CLI OpenAI Plug-in</a></li>
<li>
<a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-world-tour-london-ai-day.html" target="_blank">Salesforce World Tour London : AI Day</a>
</li>
<li>
<a href="https://bobbuzzard.blogspot.com/2023/07/predictions-for-digital-life-in-2035.html" target="_blank">Predictions for Digital Life in 2035</a></li>
<li>
<a href="https://bobbuzzard.blogspot.com/2023/03/einstein-gpt-rise-of-machines.html" target="_blank">Einstein GPT - Rise of the Machines?</a></li></ul></div>
<p></p>
<div>
<a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a>
</div>
<div>
<div><br /></div>
<div>
<br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe>
</div>
</div>
<div class="separator" style="clear: both; text-align: center;"><br /></div>
<p><br /></p>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-34149424858494300592023-07-22T15:11:00.005+01:002023-07-23T12:51:42.585+01:00Salesforce GPT - It's Alive!<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto;"><tbody><tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg59YGAxLYCtPOEOJ6OA1CjK26M5oIayNyrBhpNV9k3c4XMXUmFGRFwbvvx0E1z4--IpJweyiouFPAgvwqJKOeZHJcYn3pn26kuujP74sowVbU16WkIOMzZuuKCZTdESwn1nQivxFJ_Gnq-LQXag0huSyh_6aZYtZzQpQNpkWWTyZO1Sb3Ox8RdpqNaISkL/s768/alive.jpeg" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="768" data-original-width="768" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg59YGAxLYCtPOEOJ6OA1CjK26M5oIayNyrBhpNV9k3c4XMXUmFGRFwbvvx0E1z4--IpJweyiouFPAgvwqJKOeZHJcYn3pn26kuujP74sowVbU16WkIOMzZuuKCZTdESwn1nQivxFJ_Gnq-LQXag0huSyh_6aZYtZzQpQNpkWWTyZO1Sb3Ox8RdpqNaISkL/s320/alive.jpeg" width="320" /></a></td></tr><tr><td class="tr-caption" style="text-align: center;"><i>Image generated by Stable Diffusion 2.1 in response to a prompt by Bob Buzzard</i></td></tr></tbody></table><br /><h3 style="text-align: left;">Introduction</h3>
<p>
This week (19th July) the first of the Salesforce AI Cloud/Einstein GPT
applications became generally available. You can read the full announcement on
the
<a href="https://www.salesforce.com/uk/news/stories/sales-gpt-service-gpt-ga/" target="_blank">Salesforce news site</a>, but it's great to see something tangible in the hands of customers after
the wave of marketing over the last few months. Its a relatively limited
amount of functionality to start with, but I prefer that to waiting another 9
months for everything to be fully built out. GA is better than finished in
this case!
</p>
<h3 style="text-align: left;">What we know so far</h3>
<p>
We know that it's only available to Unlimited Edition, which already includes
the existing Einstein AI features. This seems to be becoming the standard
approach for Salesforce - Meetings, for example, was originally Performance
and Unlimited Edition only, but is now available for all editions with Sales
Cloud. It's a good way of keeping the numbers down without having to evaluate
every application, and it's likely to include those customers that are running
a lot of their business on Salesforce and thus will get the most value.
</p>
<p>
We know that it's (initially) a limited set of features that look to be mostly
relying on external generative AI systems rather than LLMs trained on your
data. The features called out in the announcement include:
</p>
<p></p>
<ul style="text-align: left;">
<li>
Service replies - personalised responses grounded in relevant, real time
data sources. To be fair, this could be a model trained on your data, but
the term grounded implies that it's an external request to something like
Open AI GPT with additional context pulled from Salesforce.
</li>
<li>
Work Summaries - wrap ups of service cases and customer engagements. The
kind of thing that Claude from Anthropic is very good at. These can then be
turned into Knowledge Articles, assuming there was anything that could be
re-used or repurposed for future customer calls.
</li>
<li>
Sales Emails - personalised and data-informed emails created for you, again
sounding very much like a grounded response from something like OpenAI.
</li>
</ul>
<div>
This looks like a smart move by Salesforce, as they can make generative AI
available to customers without having to build out the infrastructure to host
their own models - something that might present a challenge, given the demand
for GPUs across the industry.
</div>
<div><br /></div>
<div>
We know it will include the <a href="https://www.salesforce.com/uk/news/press-releases/2023/06/12/ai-cloud-news/" target="_blank">Einstein GPT Trust Layer</a>. This is probably the
biggest benefit - you could create your own integration with any of these
external services, but you'd have to build all the protections in yourself,
and the UI that allow admins to configure them.
</div>
<div><br /></div>
<div>
We don't know what pricing to expect when it becomes available outside of
Unlimited Edition, but given that it's included with Einstein there, it may
well be included in that SKU for Enterprise Edition, which is $50/user/month
for each of Sales and Service Cloud Einstein.
</div>
<div><br /></div>
<div>
We know it includes "a limited number of credits', which I'm pretty sure was
defined as 8,000 in one of the webinars I watched. This sounds like a lot, but
we don't know what a credit is, so it might not be. If it's requests, that is
quite a few. If it's tokens, not so much - testing my <a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-cli-openai-plug-in.html" target="_blank">Salesforce CLI integration with OpenAI</a> used around 6,000 tokens for not very many requests. Still, if you
built your own integration with any of these tools you'd have to pay for
usage, so there's no reason to expect it to be included when going via
Salesforce, especially as I'm sure different customers will have wildly
different usage. Those 6,000 tokens also cost me around 12 cents, so hopefully
purchasing extra credits won't break the bank!
</div>
<div><br /></div>
<div>
We also know, based on the Service Cloud GPT webinar on 19th July, that we'll
be able to add our own rules around PII/sensitive data detection in prompts.
It seemed highly likely that would be the case, but good to have it confirmed.
</div>
<div><br /></div>
<div>
Finally, we know this is just the beginning of the GPT GAs. Dreamforce is less
than 2 months away and there will be some big AInnouncements for sure.]
</div>
<h3 style="text-align: left;">More Information</h3>
<div>
<ul style="text-align: left;">
<li><a href="https://youtu.be/5M3U3fbV9zg" target="_blank">Get AI Ready - Prepare for AI Cloud (BrightGen webinar recording)</a></li><li>
<a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-cli-openai-plug-in.html" target="_blank">Salesforce CLI OpenAI Plug-in</a></li>
<li>
<a href="https://bobbuzzard.blogspot.com/2023/07/salesforce-world-tour-london-ai-day.html" target="_blank">Salesforce World Tour London : AI Day</a>
</li>
<li>
<a href="https://bobbuzzard.blogspot.com/2023/07/predictions-for-digital-life-in-2035.html" target="_blank">Predictions for Digital Life in 2035</a></li>
<li>
<a href="https://bobbuzzard.blogspot.com/2023/03/einstein-gpt-rise-of-machines.html" target="_blank">Einstein GPT - Rise of the Machines?</a></li></ul></div>
<p></p>
<div>
<a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a>
</div>
<div>
<div><br /></div>
<div>
<br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe>
</div>
</div>
<div class="separator" style="clear: both; text-align: center;"><br /></div>
<p><br /></p>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-27037747558290946792023-07-16T16:39:00.001+01:002023-07-23T12:51:23.964+01:00Salesforce CLI OpenAI Plug-in<h3 style="text-align: left;">
<div class="separator" style="clear: both; text-align: center;">
<a
href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj6CBgtkgdJFE_kVkCZypLL_nZENqDOuJllNGS7oZ2IrUv9ibC-Ysq4_iRDhyhQjv0o0M43K7OhdImV9aoI1MxJ_KoUTA81gZfZutKvl40FQxj6bTOJ22eZ_ZGtZRBFMTk86p6rPXrnIy3W8kdYJw6nQotbTXdNbBrW1vzhg6PL8d2wwMWfspKHcPgtx5AM/s768/plugin.jpeg"
style="margin-left: 1em; margin-right: 1em;"
><img
border="0"
data-original-height="768"
data-original-width="768"
height="320"
src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj6CBgtkgdJFE_kVkCZypLL_nZENqDOuJllNGS7oZ2IrUv9ibC-Ysq4_iRDhyhQjv0o0M43K7OhdImV9aoI1MxJ_KoUTA81gZfZutKvl40FQxj6bTOJ22eZ_ZGtZRBFMTk86p6rPXrnIy3W8kdYJw6nQotbTXdNbBrW1vzhg6PL8d2wwMWfspKHcPgtx5AM/s320/plugin.jpeg"
width="320"
/></a>
</div>
<div class="separator" style="clear: both; text-align: center;"><br /></div>
<div style="text-align: center;">
<span style="font-size: x-small;"
><span style="font-weight: 400;"
><i
>Image generated using StableDiffusion 2.1 via
https://huggingface.co/spaces/stabilityai/stable-diffusion</i
></span
>
</span>
</div>
</h3>
<h3 style="text-align: left;">Introduction</h3>
<p>
I've finally found time to have a play around with the OpenAI API, and was
delighted to find that it has a
<a
href="https://platform.openai.com/docs/libraries/node-js-library"
target="_blank"
>NodeJS library</a
>, so I can interact with it via Typescript or JavaScript rather than learning
Python. Not that I have any issue with learning Python, but it's not something
that is that useful for my day to day work and thus not something I want to
invest effort in right now.
</p>
<p>
As I've said many times in the past, anyone who knows me knows that I love a
Salesforce CLI Plug-in, and that is where my mind immediately went once I'd
cloned their
<a href="https://github.com/openai/openai-node" target="_blank"
>Github repo</a
>
and been through the (very) quick overview.
</p>
<h3 style="text-align: left;">The Plug-In</h3>
<p>
I wasn't really interested in surfacing a chat bot via the Salesforce CLI -
for a start, it wouldn't really add anything over and above the public OpenAI
<a href="https://chat.openai.com/" target="_blank">chat page</a>.
</p>
<p>
The first area I investigated was asking it to review Apex classes and call
out any inefficiencies or divergence from best practice. While this was
entertaining, and found some real issues, it was also wildly inaccurate (and
probably something my Evil Co-Worker will move forward with to mess with
everyone).
</p>
<p>
I then built out a couple of commands to generate titles and abstracts for
conference sessions based on Salesforce development concepts, but as the
Dreamforce call for papers is complete for this year, that didn't seem worth
the effort.
</p>
<p>
I landed on a set of explainer commands that would ask the AI model to explain
something to help a more junior developer:
</p>
<p></p>
<ul style="text-align: left;">
<li>
<b> apex</b> - explain a topic in the context of the Apex programming
language
</li>
<li><b>cli</b> - explain a Salesforce CLI command, along with an example</li>
<li>
<b> salesforce</b> - explain a Salesforce topic using appropriate language
for a specific audience - admins, devs, execs etc.
</li>
</ul>
<div>Accessing the OpenAI API is very straightforward:</div>
<div><br /></div>
<div><b>Install the OpenAI Package</b></div>
<div><br /></div>
<div>
>
<span style="font-family: courier; font-size: x-small;"
>npm install openai</span
>
</div>
<div><br /></div>
<div><b>Create the API instance</b></div>
<pre class="brush: javascript; toolbar: true">
import { Configuration, OpenAIApi } from 'openai';
const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);
</pre>
<div>
Note that the API Key comes from the OPENAI_API_KEY environment variable - if
you want to try out the plug-in yourself you'll need an OpenAI account and
your own key.
</div>
<div><br /></div>
<div><b>Create the Completion</b></div>
<div><br /></div>
<div>
This is where the prompt is passed to the model so that it can generate a
response:
</div>
<pre class="brush: javascript, toolbar: true">
const completion = await openai.createCompletion({
model: 'text-davinci-003',
max_tokens: maxTokens,
prompt,
temperature
});
result = (completion.data.choices[0].text as string);
</pre>
<div>
Note that the completions API is legacy and received it's final update in July
2023. I chose this simply because it's cheap. When you are writing a new
plug-in connecting to a third party system you get a lot of stuff wrong and I
was keen not to rack up too large a bill while I was learning! The Chat
Completions API is slightly different, in that it receives a list of messages
and can call functions with results. It's not dramatically different though,
so I felt anything I learned applied to both.
</div>
<div><br /></div>
<div>
I moved this into a shared function, hence most of the call is parameterised.
The createCompletion function can take many parameters, but here's the
explanation of those that I've used:
</div>
<div>
<ul style="text-align: left;">
<li><b>model</b> - the large language model to use</li>
<li>
<b>max_tokens</b> - the maximum number of tokens to generate the response.
A token is typically thought to be around 4 characters, so will sometimes
represent words and sometimes parts of words. Input and output token
counts are how you get charged for API use, so you want to keep them down
where you can. The function defaults to 16 which in practice is fairly
useless for a response to a human, so I've typically upped it to 256
</li>
<li><b>prompt</b> - the question/task that I've set the model</li>
<li>
<b>temperature</b> - a value between 0 and 2.0 that indicates how accurate
a response I want. I find that 0.6 works pretty well. If you change this,
do it in small increments, as I asked it to review an Apex class with a
temperature of 1.5 and got pretty much gibberish.
</li>
</ul>
<div>
And that's about it. The rest of the code is available at the
<a href="https://github.com/keirbowden/bbaiplugin" target="_blank"
>Github repository</a
> - it's mostly standard CLI Plug-In stuff of handling flags and
displaying the response.
</div>
</div>
<h3 style="text-align: left;">The Plug-In In Action</h3>
<div>
Once I've installed the plug-in from
<a href="https://www.npmjs.com/package/bbai" target="_blank">NPM</a>, using
the following command :
</div>
<div><br /></div>
<div>
>
<span style="font-family: courier; font-size: x-small;"
>sf plugins install bbai</span
>
</div>
<div><br /></div>
<div>
I can ask for some explanations. First, explain the Salesforce concept of
groups. As I don't provide details of the audience, it defaults to
programmers:
</div>
<div><br /></div>
<div>
<p
class="p1"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"
>> sf bbai explain salesforce -t groups</span
>
</p>
<p
class="p1"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"
>Here are the results</span
>
</p>
<p
class="p2"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px; min-height: 13px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"></span
><br />
</p>
<p
class="p2"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px; min-height: 13px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"></span
><br />
</p>
<p
class="p1"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"
>Groups in Salesforce are a way to organize and manage users in the
system. They allow you to categorize users based on their roles and
responsibilities, allowing for more streamlined access to information. For
example, you can create a group for all the programmers within your
organization, allowing them to access the same resources, documents, and
tasks. Additionally, groups can be used to assign tasks, send messages,
and create reports. By creating groups, you can quickly and easily manage
permissions, tasks, and data access for your programming team.</span
>
</p>
</div>
<div><br /></div>
<div>Now the same command for an exec audience - notice the difference?</div>
<div><br /></div>
<div>
<p
class="p1"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"
>> sf bbai explain salesforce -t groups -s executive</span
>
</p>
<p
class="p1"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"
>Here are the results</span
>
</p>
<p
class="p2"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px; min-height: 13px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"></span
><br />
</p>
<p
class="p2"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px; min-height: 13px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"></span
><br />
</p>
<p
class="p1"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"
>Groups in Salesforce allow organizations to manage and control access to
records and data. Groups are used to assign users to roles within the
organization and to grant them access to the appropriate records. By using
groups, organizations can easily assign the same set of permissions to
multiple users, allowing them to quickly and easily manage access rights.
This makes it easier to manage user permissions, and also helps to ensure
that users only have access to the data and records they need to do their
job. Groups also streamline the process of granting and revoking access
rights, making it easier for organizations to stay secure and
compliant.</span
>
</p>
</div>
<div><br /></div>
<div>
And now I want to start working with some of these concepts, but I don't
understand how to get my code into the instance via the CLI:
</div>
<div><br /></div>
<div>
<p
class="p1"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"
><span class="Apple-converted-space">> </span>sf bbai explain cli
-c "source push"<span class="Apple-converted-space"
> </span
></span
>
</p>
<p
class="p1"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"
>Here are the results</span
>
</p>
<p
class="p2"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px; min-height: 13px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"></span
><br />
</p>
<p
class="p2"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px; min-height: 13px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"></span
><br />
</p>
<p
class="p1"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"
>Salesforce CLI command source push is used to deploy source from the
local environment (e.g. a developer’s machine) to a Salesforce org. This
command is useful in a DevOps process, as it enables developers to quickly
and easily deploy their code changes to a Salesforce org for testing and
deployment.</span
>
</p>
<p
class="p2"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px; min-height: 13px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"></span
><br />
</p>
<p
class="p1"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"
>Example Execution:<span class="Apple-converted-space"> </span></span
>
</p>
<p
class="p1"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"
>sfdx force:source:push --targetusername devorg --sourcepath
/path/to/source/directory/</span
>
</p>
<p
class="p2"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px; min-height: 13px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"></span
><br />
</p>
<p
class="p1"
style="font-family: Menlo; font-feature-settings: normal; font-kerning: auto; font-optical-sizing: auto; font-size: 11px; font-stretch: normal; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-numeric: normal; font-variation-settings: normal; line-height: normal; margin: 0px;"
>
<span class="s1" style="font-variant-ligatures: no-common-ligatures;"
>The command above will deploy all source code in the specified directory
to the org with the username devorg.</span
>
</p>
</div>
<h3 style="text-align: left;">Conclusions</h3>
<p>
Something that really stood out in this was the simplicity of the integration
- it's literally a few lines of code. Which is as it should be - with
generative AI, English is the programming language so most of the effort
should go into the prompt rather than getting set up to send the prompt.
Looking ahead a few years, I can see this being a game changer for integrating
systems. Rather than starting with a book of API specifications that you need
to adhere to, there will be a single entry point where you send conversational
requests to a model trained on the specific data. "Return the id and all name
and address fields of all the contacts we have that work for financial
services customers in JSON format", without needing to develop that specific
interface.
</p>
<h3 style="text-align: left;">The Cost</h3>
<p>
This was top of mind for me during this experiment. I've seen situations in
the past where request based charging really racked up during testing. OpenAI
allows you to set a hard and soft limit, so that you can't get into that
situation, but it also charges by the token for both input and output. When
you aren't that familiar with how many tokens might be in a 2-3 line prompt.
After building my plug-in, with the usual number of mistakes, and testing it
for several hours, I'd used the princely sum of $0.16.
</p>
<p>
While that might sound like there's nothing to worry about, but the latest
models are more expensive per token and can handle significantly more tokens
both for input and output, so you may find that things stack up quickly. I've
seen news stories lauding the ability to send an entire book to a generative
AI as context, but no mention of how much you'll be charged to do that!
</p>
<h3 style="text-align: left;">More Information</h3>
<p></p>
<ul style="text-align: left;">
<li>
<a href="https://github.com/keirbowden/bbaiplugin" target="_blank"
>Github repository for the plug-in</a
>
</li>
<li>
<a href="https://www.npmjs.com/package/bbai" target="_blank"
>Plug-in landing page on NPMJS</a
>
</li>
<li>
<a
href="https://platform.openai.com/docs/introduction/overview"
target="_blank"
>OpenAI API Documentation</a
>
</li>
<li>
<a
href="https://bobbuzzard.blogspot.com/2023/07/salesforce-world-tour-london-ai-day.html"
target="_blank"
>Salesforce World Tour London : AI Day</a
>
</li>
<li>
<a
href="https://bobbuzzard.blogspot.com/2023/07/predictions-for-digital-life-in-2035.html"
target="_blank"
>Predictions for Digital Life in 2035</a
>
</li>
<li>
<a
href="https://bobbuzzard.blogspot.com/2023/03/einstein-gpt-rise-of-machines.html"
target="_blank"
>Einstein GPT - Rise of the Machines?</a
>
</li>
</ul>
<div><br /></div>
<p></p>
<div>
<a
class="twitter-follow-button"
data-show-count="false"
href="https://twitter.com/bob_buzzard"
style='font-family: "lucida grande";'
>Follow @bob_buzzard</a
>
</div>
<div>
<div><br /></div>
<div>
<br class="Apple-interchange-newline" /><iframe
frameborder="0"
height="320"
scrolling="no"
src="https://bobbuzz.substack.com/embed"
style="background: white; border: 1px solid #EEE;"
width="480"
></iframe>
</div>
</div>
<div class="separator" style="clear: both; text-align: center;"><br /></div>
<p><br /></p>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-59276574157242142302023-07-09T16:02:00.000+01:002023-07-09T16:02:00.024+01:00Salesforce World Tour London : AI Day<h1 style="text-align: left;">AI Day London</h1><div class="separator" style="clear: both; text-align: center;"><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjQeu6Ehdk9YhWo4p5cSArzQxDIVk3Ye3RK7nXNiO587RqJQiU0NioTSVCdGCTMjPLGLC97GX9dvdQKsP9bYETXarQrtOAM4VlAz-pDVoCj98gkD3VVi6wUuYRgpjTiu8JLGYvObrqCmqUCtBW7_PfQHOAeGP0sMgXF4rvJQKvbxTSHM7oeXj4sXolY2qo1/s768/aiday.jpeg" style="margin-left: 1em; margin-right: 1em;"><img alt="A steampunk image of a machine with a brain." border="0" data-original-height="768" data-original-width="768" height="200" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjQeu6Ehdk9YhWo4p5cSArzQxDIVk3Ye3RK7nXNiO587RqJQiU0NioTSVCdGCTMjPLGLC97GX9dvdQKsP9bYETXarQrtOAM4VlAz-pDVoCj98gkD3VVi6wUuYRgpjTiu8JLGYvObrqCmqUCtBW7_PfQHOAeGP0sMgXF4rvJQKvbxTSHM7oeXj4sXolY2qo1/w200-h200/aiday.jpeg" title="A steampunk image of a machine with a brain." width="200" /></a></div><br /></div><div style="text-align: center;"><i><span style="font-size: x-small;">Image created using Stable Diffusion 2.1</span></i></div><p>It's been just over a week since Salesforce World Tour : AI Day, and I'll wager less than two weeks since the events team found it it was going to be rebranded AI Day! Kudos to them though, it was seamless and if you weren't following the tech news you'd never have guessed!</p><p>Those of us attending the UK event experienced great timing this year. The fact that it happened a few weeks after the launch of <a href="https://aibusiness.com/nlp/salesforce-launches-ai-cloud-to-bring-generative-ai-to-the-enterprise-" target="_blank">AI Cloud in New York</a> means that cutting edge features have just reached demo state well outside of the usual release cycle. For example, we got the first public demo of Prompt Studio, which is the kind of thing that would typically land at Dreamforce in San Francisco, rather than a World Tour event on the other side of the world. I'm also fairly sure that we wouldn't have been treated to a deep dive in machine learning from Patrick Stokes if he wasn't leading the AI technical charge. </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtEAj-ZdnJN85H-2paDzSQwoFnjcrbC5on_43D-xDyT_yyG5_ezs36CX1lcXFI0Wa1smn2Ast-xksfVIkaEFh01l5XKtb2mSPaPB59ZYziyZAK8heBT2kQxuxpmfJgSOO7YyQuc4bGMIHEiGM4O04KBi7MpP5gCVzRK8M3nPx9XrqvjKPj2L7FnXH3IbMn/s1628/Screenshot%202023-07-09%20at%2015.48.52.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1094" data-original-width="1628" height="430" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtEAj-ZdnJN85H-2paDzSQwoFnjcrbC5on_43D-xDyT_yyG5_ezs36CX1lcXFI0Wa1smn2Ast-xksfVIkaEFh01l5XKtb2mSPaPB59ZYziyZAK8heBT2kQxuxpmfJgSOO7YyQuc4bGMIHEiGM4O04KBi7MpP5gCVzRK8M3nPx9XrqvjKPj2L7FnXH3IbMn/w640-h430/Screenshot%202023-07-09%20at%2015.48.52.png" width="640" /></a></div><p>Spare a thought for the poor execs who are suddenly having to trip out a lot more than usual at this time of year, but if you want to position yourself as a leader in the AI space, you need some representation from Head Office. Bad news for them, but good news for us.</p><p>Prompt Studio looks good from what we saw in a short demo in a keynote. As expected, Salesforce are taking a cautious approach with guardrails around not just the AI, but who can create the prompts that drive the AI. There's some chatter on the socials that every admin is now a prompt engineer, but that feels like a simplistic view to me. Marketing and Legal are a couple of departments that will be very interested in contributing to and signing off prompts, rather than viewing it as regular configuration. Any admin who does become a prompt engineer could be looking at a rather lucrative career though, as that role is <a href="https://time.com/6272103/ai-prompt-engineer-job/" target="_blank">currently paying up to $335k/year, although it isn't expected to be around that long</a>.</p><p>We also saw several other demos of GPT-as-an-Assistant across Sales, Service, Slack and Platform. This last one was particularly interesting for me, covering as it did automatic generation of Apex code from descriptive comments. This represents a significant change for us developers from the usual enhancements on the development side - typically we are keen to learn the detail of the new features so that we can start customising Salesforce with them. With GPT it's closer to handing off our work to a junior colleague - explaining what we want, waiting for it to appear and then reviewing it to see how well it matches our requirements. And, if history is anything to go by, realising that there was a whole layer of complexity that we forgot to mention!</p><p>One constant attendee at the London events is Kavindra Patel, VP of Trailblazer Engagement at Salesforce. I always try to catch up with him, and this year was no different. In a shocking turn of events, he let me in on the secret that there's quite a focus on AI at Salesforce right now!</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEimrk98KycJ1ybls0bNPCR0Ayt5RngEAPLpNwc7_kGV3e8SXzyyUH4ZSkILxHxqdSgirfjX5asUvb2euV5bpOKpkkV2ofp0mKOE9gkH9xrh-0pYKh06LqXx4RF_7oHEL5qYThW-LPoVhl7DKjacygKGyL4efuuvrDqmBjQmSF5lJ5hBXxkxSROwXxQxYMRt/s3093/IMG_8776.heic" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1973" data-original-width="3093" height="204" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEimrk98KycJ1ybls0bNPCR0Ayt5RngEAPLpNwc7_kGV3e8SXzyyUH4ZSkILxHxqdSgirfjX5asUvb2euV5bpOKpkkV2ofp0mKOE9gkH9xrh-0pYKh06LqXx4RF_7oHEL5qYThW-LPoVhl7DKjacygKGyL4efuuvrDqmBjQmSF5lJ5hBXxkxSROwXxQxYMRt/s320/IMG_8776.heic" width="320" /></a></div><p>And, of course, we had the Golden Hoodie award. This year it went to my fellow MVP and co-organiser of the London Salesforce Developer group, Todd Halfpenny, seen here preparing for a duel with Zahra Bahrololoumi CBE, CEO of Salesforce UK&I.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjLYmdPwzn3-BmStc6MkAaG94GuB_SaGmAadFEqiFzEvKOj0sZwaNE4xMfK5pHo56zbDNIv_RqneV4BScmDJB43ChEhpd7KVau46rFvfdv9gCmVMybEaoJ3l2rkCxXFSrU-0rZtpHpf7KbxwLBgj5n0q_VS4TufDr8w6pBPqOSp-eu5x704WowFcJy69FHT/s4032/todd.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="3024" data-original-width="4032" height="240" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjLYmdPwzn3-BmStc6MkAaG94GuB_SaGmAadFEqiFzEvKOj0sZwaNE4xMfK5pHo56zbDNIv_RqneV4BScmDJB43ChEhpd7KVau46rFvfdv9gCmVMybEaoJ3l2rkCxXFSrU-0rZtpHpf7KbxwLBgj5n0q_VS4TufDr8w6pBPqOSp-eu5x704WowFcJy69FHT/s320/todd.jpg" width="320" /></a></div><p>You can catch up with the World Tour keynote, and various other sessions, on <a href="https://www.salesforce.com/plus/experience/world_tour/series/world_tour_london:_ai_day" target="_blank">Salesforce+</a>. If you weren't able to attend in person I'd highly recommend it, as it is a good overview of where Salesforce will be in a few months once it's all built out. If you are interested in AI in general or AI Cloud in particular, but not sure where to start, <a href="https://brightgen.com/brightgen-events/get-ai-ready-brightgen-webinar/" target="_blank">join me on 19th July</a> when I'll be doling out advice about preparing for the rise of the machines. </p><h3>Additional Information</h3><div><ul><li><a href="https://bobbuzzard.blogspot.com/2023/07/predictions-for-digital-life-in-2035.html" target="_blank">Predictions for Digital Life in 2023</a></li><li><a href="https://bobbuzzard.blogspot.com/2023/03/einstein-gpt-rise-of-machines.html" target="_blank">Einstein GPT - Rise of the Machines?</a></li></ul><div><br /></div></div><p></p><div><a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a></div><div><div><br /></div><div><br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe></div></div><div class="separator" style="clear: both; text-align: center;"><br /></div><br />Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-60863772081127648952023-07-04T09:51:00.002+01:002023-07-04T09:51:39.321+01:00Predictions for Digital Life in 2035<h2 style="text-align: left;">Predictions for Digital Life in 2035</h2><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi9cY-2HRopZWcEFHurPx0quZs8bR7GkW7K7ESPwpLzB6BtQc695HhKSrD5I0MQm3hBdfco98CqtI5NgD4DdacQ2natm3CfT-LuUSdKP6SONcVcLDtutjQiyFMYgeLl67rmIACh8E66Mez2RhnznGwPLu9z3nfNVaZ-B1WXZdujKCoi6XSumqPitmA4MbEm/s666/heaven.jpeg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="500" data-original-width="666" height="240" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi9cY-2HRopZWcEFHurPx0quZs8bR7GkW7K7ESPwpLzB6BtQc695HhKSrD5I0MQm3hBdfco98CqtI5NgD4DdacQ2natm3CfT-LuUSdKP6SONcVcLDtutjQiyFMYgeLl67rmIACh8E66Mez2RhnznGwPLu9z3nfNVaZ-B1WXZdujKCoi6XSumqPitmA4MbEm/s320/heaven.jpeg" width="320" /></a></div><br /><div>Pew Research Center recently published <a href="https://www.pewresearch.org/internet/wp-content/uploads/sites/9/2023/06/PI_2023.06.21_Best-Worst-Digital-Life_2035_FINAL.pdf" target="_blank">the results from their 16th "Future of the Internet"</a> research that aggregates experts' opinions on important digital issues. </div><div><br /></div><div>As you can imagine much of the focus was on Generative and other Artificial Intelligence applications. A fair number of the respondents were enthusianxious about the changes, with 42% being equally excited and concerned about what they expect to see, 37% being more concerned than excited. and only 18% more excited than concerned. Clearly the pessimists are currently in charge with a whopping 79% being as-or-more-concerned than excited. </div><p>The concerns will again come as no surprise to anyone who has been following the AI news and opinion over the last year or two:</p><p></p><ul style="text-align: left;"><li>That the motivation will be for profit and power, leading to data collection aimed at controlling or coercing behaviour with ethics as an afterthought</li><li>Loss of privacy and jobs, leading to a rise in poverty and reduction in dignity</li><li>Human knowledge drowning in an ocean of meaningless or flat out wrong information generated by or using AI</li><li>Health impact as tech encourages us to become even more isolated from humans, or feeds our worst paranoia</li><li>Government and regulation being unable to keep up with the pace of change, with an end game of autonomous weapons and cyber warfare being waged by machines without human oversight. Essentially the first steps towards Terminators.</li></ul><div>It's not all bad news though, with benefits expected to include:</div><div><ul style="text-align: left;"><li>Enhancements in healthcare, education, fitness, nutrition, entertainment, transportation and energy. Our digital assistants will free us up from much of the drudgery, leaving us more time to enjoy these improved offerings, which will of course be entirely integrated and friction-free.</li><li>Increased amplification when people speak up for their human rights, and easy collaboration with others who wish to mobilise to demand the same. Access to data and better communication tools will help people live better and safer lives around the globe.</li><li>Improved digital literacy, with the desire that this will see the return of trusted news and information sources. Failing that, at least an assurance that information is factual and verified.</li><li>A regulatory environment that promotes pro-social activities and minimises anti-social ones. </li></ul><div>One thing that jumps out at me from the list of negatives and positives is how much more detailed the Orwellian nightmares are - many of those that are concerned have really thought those concerns all the way through to the bitter end!</div><div><br /></div><div>It's a relatively lengthy report at 230 odd pages, but an interesting read, and at times entertaining (especially the more extreme views, as always). An awful lot of those canvassed expect most technological innovations to be a double-edged sword - Howard Rheingold, author of "<a href="https://www.amazon.co.uk/Virtual-Community-Homesteading-Electronic-Frontier/dp/0262681218" target="_blank">The Virtual Community</a>", advises us to ask of any new technology 'What would 4chan do with it?'. Jonathan Grudin, Affiliate Professor of Information Science at the University of Washington, paints a picture of "<b>a Sorcerer’s Apprentice’s army of feverishly acting brooms with no sorcerer around to stop
them.</b>" as the sheer scale of digital activities, and the incredible speeds at which they are carried out, far outpaces our ability to verify and correct, so we stand by helplessly and watch.</div></div><div><br /></div><div>Louis Rosenbery, CEO and Chief Scientist at <a href="https://unanimous.ai/" target="_blank">Unanimous AI</a>, predicts that we'll be in full Star Trek mode by 2035, as keyboards, mice, touchscreen input and flat screen display output are swept aside by conversational interfaces. Like Captain Kirk, we'll just ask the computer to carry out a task in plain language (although for some reason Kirk still needed humans to change the velocity and direction of the Enterprise, and patch through a call, so maybe our jobs are safe after all).</div><div><br /></div><div>If you have a couple of hours to spare, it's well worth a read. My main takeaway is that even those who are positive are sounding a note of caution equally loudly, and I think that's an appropriate view. It dovetails nicely with most of the work that I do in my professional life - I'm not so much looking for things that will work correctly, as a lot of that is fairly obvious. Instead I'm looking for problems - what won't work, are there unintended side effects, is it future-proof, will it scale? It's easy to get caught up in the hype and hoopla of an emerging technology like Generative AI, and let the FOMO trick you into doing something rash. With any new technology, always take a step back, a deep breath, and evaluate with cold eyes.</div><div><br /></div><div>Will 2035 be digital heaven or digital hell? I think, like always, it will be six of one, half a dozen of the other - a constant battle that leads to an uneasy balance.</div><div><br /></div><h3 style="text-align: left;">Additional Information</h3><div><ul style="text-align: left;"><li><a href="https://bobbuzzard.blogspot.com/2023/03/einstein-gpt-rise-of-machines.html" target="_blank">Einstein GPT - Rise of the Machines?</a></li><li><a href="https://www.pewresearch.org/internet/wp-content/uploads/sites/9/2023/06/PI_2023.06.21_Best-Worst-Digital-Life_2035_FINAL.pdf" target="_blank">As AI Spreads, Experts Predict the Best and Worst Changes in Digital Life by 2035, Pew Research Center</a></li></ul><div><br /></div></div><p></p><div><a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a></div><div><div><br /></div><div><br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe></div></div><p></p><div><br /></div><div><br /></div><p></p>Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-47798914022122401472023-05-14T12:16:00.002+01:002023-05-14T12:16:52.244+01:00Light DOM Scoped Slots in Summer 23<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiX3DE03tewhK9CRRbzxdnwPzbyUeS5jhVvMFV7LsWAkBS88sDVmDqcq3L37vIiiCWEYjcHh9BsBozXCIKP0pf8HJ1fcQt_1hh3p2P5f0g8wk-RH5A76m9tBsNHI6bFiivpec3O85SF33_S9FWbGz8k8j34q4i7ETL5bitTouLD7xFRRn7L53Q_7EPnUg/s612/slots.jpeg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="459" data-original-width="612" height="240" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiX3DE03tewhK9CRRbzxdnwPzbyUeS5jhVvMFV7LsWAkBS88sDVmDqcq3L37vIiiCWEYjcHh9BsBozXCIKP0pf8HJ1fcQt_1hh3p2P5f0g8wk-RH5A76m9tBsNHI6bFiivpec3O85SF33_S9FWbGz8k8j34q4i7ETL5bitTouLD7xFRRn7L53Q_7EPnUg/s320/slots.jpeg" width="320" /></a></div><h3 style="text-align: left;">Introduction</h3>
<p>
It's that time again - another Salesforce release is a few weeks away, the
preview release notes are out, and preview scratch orgs can be spun up to try
out some of the new features. There's a few changes around Lightning
Components in the upcoming release, and the first one that caught my eye was
the concept of scoped slots, made possible by the general availability of the
light DOM.
</p>
<p>
The light DOM is what we used to call the DOM before web components came along
- the standard Document Object Model representing the structure of a web page
that is visible and accessible to any JavaScript that cares to look. The light
aspect comes from the fact that web components by default use shadow DOM - a
hidden DOM structure attached to the regular DOM that only the component can
see.
</p>
<p>
Scoped slots are an inversion of the usual slot behaviour seen with Lightning
Web Components. The standard case allows a parent component to pass markup
into a child component to render in specific locations. Scoped slots turns
this on its head and allows child components to pass information up to parent
components that can be rendered in a specific slot. All of which sounds very
cool, but at first glance it seemed a solution looking for a problem. The
example in the release notes didn't do much to change this view, as they
showed a child iterating a list and the parent rendering the contents of the
list item. Given that the parent can iterate the list just as easily as the
child, I couldn't see the value that was added.
</p>
<p>
One thing I've found with Lightning Web Components is a lot of the new
features have equivalents in another JavaScript framework - Vue.js seems to be
the prime candidate at the moment, and sure enough there's the concept of
scoped slots there. Unfortunately the various blogs that I read on this topic
didn't help my understanding enormously - when you aren't familiar with the
framework then the examples aren't always helpful. What I did take away from
this is the value in scoped slots is to separate the generation of the data
from the rendering, so that it is available for re-use. The release notes
example didn't really provide this as the list iteration is the same
regardless of whether you do it in the parent or child, so I needed to figure
out another use case.
</p>
<h3 style="text-align: left;">The Sample</h3>
<p>
Simple list iteration is easy, but what about the case where I have a
JavaScript object created from an Apex Map and want to iterate that? I can't
do this in markup, instead I have to create a list of properties from the
object. This feels like a situation where a child component that can handle
any conversion and iteration required and simply send the properties back to
the parent.
</p>
<p>
My component that handles this is called mapIterator, and when it receives an
object via an @api property, it creates a list from it:
</p>
<pre class="brush: javascript; toolbar: true">set objMap(value) {
this._objMap=value;
if (this._objMap) {
this.values=[];
for (let key in this._objMap) {
this.values.push(this._objMap[key]);
}
}
}
</pre>
<p>
and the HTML markup simply iterates this and makes each entry available to the
parent via the <b>lwc:slot-bind</b> directive, remembering to render in light
DOM mode:
</p>
<pre class="brush: html; toolbar: true"><template lwc:render-mode="light">
<template for:each={values} for:item="value">
<slot key={value} lwc:slot-bind={value}></slot>
</template>
</template>
</pre>
<p>
I have several parent components that make use of this - one to generate a
simple list of accounts, one to generate a list of account cards, and one to
generate a list of opportunity cards. The mapIterator provides the conversion
and iteration of the object properties to each of this with no changes
required. You can find all of these at the
<a href="https://github.com/keirbowden/Summer23" target="_blank">Github repository</a>, but here's the markup from the simple list :
</p>
<pre class="brush: html; toolbar: true"><c-map-iterator obj-map={accountsMap}>
<template lwc:slot-data="value">
<div class="slds-p-left_small slds-p-bottom_xx-small">
<strong>ID:</strong> {value.Id}
</div>
<div class="slds-p-left_large slds-p-bottom_small">
<strong>Name:</strong>{value.Name}
</div>
</template>
</c-map-iterator></pre>
<p>
Access to the element from the iterator is provided by the
<b>lwc:slot-data</b> directive, and as you can see the parent handles all the
presentation side of things.
</p>
<h3 style="text-align: left;">Conclusion</h3>
<p>
There is definite value here in separating the conversion/iteration from the
rendering of the content. That said, I think you might run into issues if you
are rendering the iterated elements using markup that must be a direct
descendant of a containing element. In that case you won't be able to separate
everything, as you get the child element markup in the way. I think there
you'd likely need specialisations of the iterator that also renders the
container, which won't feel quite as clean.
</p>
<h3 style="text-align: left;">More Information</h3>
<p></p>
<ul style="text-align: left;">
<li>
<a href="https://help.salesforce.com/s/articleView?id=release-notes.rn_lwc_scoped_slots.htm&release=244&type=5" target="_blank">Salesforce release notes for this feature</a>
</li>
<li>
<a href="https://github.com/keirbowden/Summer23" target="_blank">Github repository</a>
</li>
</ul>
<div><br /></div>
<div>
<a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a>
</div>
<div><br /></div>
<div>
<br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe>
</div>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-23248323229662124822023-04-30T12:18:00.002+01:002023-04-30T12:24:15.408+01:00Reactive Flow Components in Spring 23<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJI82Uo8JCZ5InyQ5yRwVv0MD4yc3mQu8FWEBT-XQA6dA2QcGR7JZF3BxgaKMbhTrbRn90-CrT9yyHqtM7p8A2w8wdbPtqygB8JC6oC1GnmRxRer63w9rPOjavNmBuVd5IAyxO6KxnWxJEU-WdAJ2I6M8c9q2c8WEEpfhM2octbHhCc6BEvXhgIcRUuA/s512/react.jpeg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="487" data-original-width="512" height="304" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJI82Uo8JCZ5InyQ5yRwVv0MD4yc3mQu8FWEBT-XQA6dA2QcGR7JZF3BxgaKMbhTrbRn90-CrT9yyHqtM7p8A2w8wdbPtqygB8JC6oC1GnmRxRer63w9rPOjavNmBuVd5IAyxO6KxnWxJEU-WdAJ2I6M8c9q2c8WEEpfhM2octbHhCc6BEvXhgIcRUuA/s320/react.jpeg" width="320" /></a></div><h3 style="text-align: left;"><br /></h3><h3 style="text-align: left;">Introduction</h3>
<p>
A typical scenarios where I favour lightning pages over flows is where a
user's decisions around field values need to have a major impact on the rest
of the elements being displayed, and where a wizard interface would slow the
user down too much. A great example of this is our BrightMEDIA order booking
page. As the user inputs information about the type of advert, where it needs
to appear, what size it is and when it appears, the price is dynamically
recalculated and displayed. The user typically has the customer on the phone,
and there may be a few cycles of tweaking the details to hit a price point -
what happens if we make it a bit smaller, move it to another section etc.
Paging back and forward through the wizard wouldn't be a great experience,
along with the need to keep saving the order as navigation between pages takes
place.
</p>
<p>
With the Spring 23 release of Salesforce, it looks like in future flow will
become a candidate for this kind of user interface with the advent of
the <a href="https://help.salesforce.com/s/articleView?id=release-notes.rn_automate_flow_reactivity_beta.htm&release=242&type=5" target="_blank">Reactive Flow Components</a> beta. Simply put, you can wire the input of one component up to the
output of another component, and react when changes occur. </p>
<p>
As this is a beta, you'll have to opt-in to it for any org where you want to
use reactive components by checking the box in the Automation Settings
</p>
<h3 style="text-align: left;">The Sample</h3>
<p>
A lot of the demos around reactive anything tends to be a table of accounts
that updates when the user inputs the desired number of entries. That's
exactly what I started with to get a handle on how this works, but it's not
overly interesting, so I decided to return to our BrightMEDIA accelerator for
inspiration.
</p>
<p>
One of the more complex Lightning Components we have in the order booking page
is the data picker. This can render as a drop down or a calendar, with dates
available for selection rendered in specific styles depending on whether
capacity is available, if they are contiguous with a selected start date, and
many other rules.
</p>
<p>So for my sample I have a couple of custom Lightning Web Components:</p>
<p></p>
<ul style="text-align: left;">
<li>
A Publication Picker - this is a simple choice style component, but I've had
to go custom as the standard choice component doesn't yet have reactive
support. Each publication has a checkbox that indicates if it publishes on
weekends as well as weekdays.
</li>
<li>
A Date Picker - this is a calendar component that by default displays a 7
day week, but if the selected Publication doesn't publish on weekends,
switches to a 5 day week.
</li>
</ul>
<div>
I create a simple screen flow and add a two column screen component, with my
two custom components:
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjczC0EG7O4tQACEwpA-hgzlKJNflu_WUNnezAImEV6e5YR1RII-SFLiniiP5SeS52RJxxMK4X-lsg05Hw9N8l80_TbFD4i2Z44Dhczo85vZk-yJisH5ysk8wmQcuXu1jnwqgozqk9szkDfGYo4nIdSVLJPeQCD6a2dxoRWYc75_QEVM0UoquUxKeDJAQ/s1040/Screenshot%202023-04-30%20at%2011.57.21.png" style="margin-left: 1em; margin-right: 1em;"><img border="1" data-original-height="732" data-original-width="1040" height="450" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjczC0EG7O4tQACEwpA-hgzlKJNflu_WUNnezAImEV6e5YR1RII-SFLiniiP5SeS52RJxxMK4X-lsg05Hw9N8l80_TbFD4i2Z44Dhczo85vZk-yJisH5ysk8wmQcuXu1jnwqgozqk9szkDfGYo4nIdSVLJPeQCD6a2dxoRWYc75_QEVM0UoquUxKeDJAQ/w640-h450/Screenshot%202023-04-30%20at%2011.57.21.png" width="640" /></a>
</div>
<br />
<div><br /></div>
<div>
The Publication Picker defines an output property named publishesWeekends,
which is set to the value from the selected Publication. I wire this to the
weekends input property on the datePicker:
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg-xqo5QHDZB0LU7ajYlW-BoBC9-xnGp1AZpieK3DNJtXHada_-sV5kQbx9y_NbMCi94iPRzKN54tO18uWqIpd-Cs3Xa-obIyudBS0UNCvvHJAxNjRlPg5XhufEWL45SLd20VqLCZDNqqHjSoJTm-m9kGMDIUytfPeXLbn4ouMyiQoA-w2OEKRfaZfhHg/s622/Screenshot%202023-04-30%20at%2011.59.34.png" style="margin-left: 1em; margin-right: 1em;"><img border="1" data-original-height="366" data-original-width="622" height="235" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg-xqo5QHDZB0LU7ajYlW-BoBC9-xnGp1AZpieK3DNJtXHada_-sV5kQbx9y_NbMCi94iPRzKN54tO18uWqIpd-Cs3Xa-obIyudBS0UNCvvHJAxNjRlPg5XhufEWL45SLd20VqLCZDNqqHjSoJTm-m9kGMDIUytfPeXLbn4ouMyiQoA-w2OEKRfaZfhHg/w400-h235/Screenshot%202023-04-30%20at%2011.59.34.png" width="400" /></a>
</div>
<br />
<div>
When I run the flow, as I switch my choices between Publications, the Date
Picker changes how it renders to reflect 7 days a week publishing:
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhXrLY1XCSwhu0sXRiSq_QD3d-tnXZ74RjO1zeeE16YR_uYReB6Xlft5DhlfudLe8byJiFH4IBYbHLG8bF3grzCS9ogna5cHLODYo5OoMC04wPn44Nsm0ZWAloJn9kzQHFpfI5f6swkjNXNuAGJRJmMgBk0l2zGsaNMVkGHLISmPRsuAscpsdw8MPc-NA/s1438/Screenshot%202023-04-30%20at%2012.02.09.png" style="margin-left: 1em; margin-right: 1em;"><img border="1" data-original-height="864" data-original-width="1438" height="384" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhXrLY1XCSwhu0sXRiSq_QD3d-tnXZ74RjO1zeeE16YR_uYReB6Xlft5DhlfudLe8byJiFH4IBYbHLG8bF3grzCS9ogna5cHLODYo5OoMC04wPn44Nsm0ZWAloJn9kzQHFpfI5f6swkjNXNuAGJRJmMgBk0l2zGsaNMVkGHLISmPRsuAscpsdw8MPc-NA/w640-h384/Screenshot%202023-04-30%20at%2012.02.09.png" width="640" /></a>
</div>
<br />
<div>or 5 days a week:</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh6LbHAWtESGedVaqaCmoj82W4s7kjiROB4MsYEw2rRX9pxdh1rbv50Xkdq6PKy4aE9F7yh1QtQXthAOQS20tPNZd8Zp7cDIPWoiWHUhNr_Ixn6bStHP-IRGB6iB9KvNFRNj1s7iZ3E-LEvLnaJfIctmkNYMY7N7awUsDUhsnT002Tf2NK0UEmSkWY0SQ/s1408/Screenshot%202023-04-30%20at%2012.02.18.png" style="margin-left: 1em; margin-right: 1em;"><img border="1" data-original-height="874" data-original-width="1408" height="398" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh6LbHAWtESGedVaqaCmoj82W4s7kjiROB4MsYEw2rRX9pxdh1rbv50Xkdq6PKy4aE9F7yh1QtQXthAOQS20tPNZd8Zp7cDIPWoiWHUhNr_Ixn6bStHP-IRGB6iB9KvNFRNj1s7iZ3E-LEvLnaJfIctmkNYMY7N7awUsDUhsnT002Tf2NK0UEmSkWY0SQ/w640-h398/Screenshot%202023-04-30%20at%2012.02.18.png" width="640" /></a>
</div>
<div><br /></div><div>It's early days for this, but definitely a step in the right direction. This feels like an area where you'll benefit from developer support to create truly reactive experiences, as this will really pop with custom components that can change their styling based on user selections. </div><div><br /></div>
<h3 style="text-align: left;">The Code</h3>
<div><br /></div><div>
You can find the
<a href="https://github.com/keirbowden/ReactiveFlowSpring23" target="_blank">code behind the sample in Github</a>. Spin up a scratch org, deploy the code, create a couple of publications and
run the flow. Don't forget to opt in to the beta as outlined above, or
like me you'll sit there scratching your head trying to figure out what you've
broken :)</div>
<div>More Information</div>
<div>
<ul style="text-align: left;">
<li>
<a href="https://help.salesforce.com/s/articleView?language=en_us&id=release-notes.rn_automate_flow_reactivity_beta.htm&type=5&release=242" target="_blank">Salesforce Release Notes for this feature</a>
</li>
<li>
<a href="https://unofficialsf.com/reactive-screen-component-demonstration/" target="_blank">Reactive Screen Demo from Eric Smith at UnofficialSF</a>
</li>
</ul>
<div><br /></div>
</div>
<p></p>
<div>
<a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a>
</div>
<div><br /></div>
<div>
<br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe>
</div>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-163616785080166892023-04-15T16:34:00.002+01:002023-04-15T16:34:21.733+01:00BrightSIGN: Updating a Record when a Signature is Captured<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjAxPe9-JwB5IbtpxQpZLfJ0gRyfmIfg-J6AYHkvMDAtRM8XS0K4CYSGBLpHgV8bneTsoeEm0q-L7otQILZkgnPNthKcsCHGz0_D2zhIEOlNBz5zAsKUxY4TUzEoSXc6cH7QyQ8X4t5vhGh6aLBoltoIdj5-VP8xOBUwrmFqQJOfyd3o2LjCP5_tBUeFg/s754/update.jpeg" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="500" data-original-width="754" height="212" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjAxPe9-JwB5IbtpxQpZLfJ0gRyfmIfg-J6AYHkvMDAtRM8XS0K4CYSGBLpHgV8bneTsoeEm0q-L7otQILZkgnPNthKcsCHGz0_D2zhIEOlNBz5zAsKUxY4TUzEoSXc6cH7QyQ8X4t5vhGh6aLBoltoIdj5-VP8xOBUwrmFqQJOfyd3o2LjCP5_tBUeFg/s320/update.jpeg" width="320" /></a></div><br /><p>It's been a while since I've had the time to work on the <a href="https://appexchange.salesforce.com/listingDetail?listingId=a0N30000000q5XOEAY&tab=e" target="_blank">BrightSIGN</a> (formerly
known as Signature Capture), but I had a couple of days off this week and what
better way to spend them than a bit of front end coding.</p>
<h3 style="text-align: left;">The Problem</h3>
<p>
A feature requested by a user posting a <a href="https://appexchange.salesforce.com/listingDetail?listingId=a0N30000000q5XOEAY&tab=r" target="_blank">review on the app exchange</a> was the
ability to update a field on the record that the signature image is attached
to. There are a number of ways to achieve this, but one thing was for sure -
this wasn't going into the package. Dynamically generating update statements
to change records in the subscriber org is the kind of thing that really slows
down the security review!
</p>
<p>
The scenario I wanted to satisfy was a <a href="https://trailhead.salesforce.com/content/learn/modules/lightning_app_builder" target="_blank">Lightning App Builder</a> page that would
show a BrightSIGN component, and once the user had saved the signature image,
update the record and hide the BrightSIGN component so they weren't asked to
sign multiple times.
</p>
<p>
I was originally thinking that I'd go the route of a trigger on
Attachment/ContentDocument, depending on how the component was configured, but
I decided against this for a couple of reasons -
</p>
<p></p>
<ol style="text-align: left;">
<li>
I'd have to make some assumptions about the name of the attachment/content
document in order to determine that it had been created by BrightSIGN, which
would lead to false positives if attachments/documents with the same name
were used elsewhere
</li>
<li>
I'd be detached from the front end and unable to signal to the container
that the record had been updated - this is key when I want to conditionally
render parts of the page depending on whether the record has been 'signed'
or not.
</li>
</ol>
<h3 style="text-align: left;">The Solution</h3>
<div><br /></div>
<div>
I ended up writing very little code to satisfy this requirement, as I was able
to leverage the existing SignatureCaptured event and the force:recordData
standard component. In the example scenario I'm setting a field with the
API name 'Signature_Captured__c' on an Account record once the user has
'signed' it.
</div>
<div><br /></div>
<div>
To start with, I created a Lightning App Builder record page for Accounts,
which I dropped a BrightSIGN component onto. This component is conditionally
rendered and only appears if the Signature_Captured__c checkbox is not set :
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhU3Wfix2D64icf5zg66erepP4HCJSVd4DJWf7xQNhB-qOeSkfEiz_o70HOqufQxU8XU_sOeXFTUYim3ZOnvo2JtPyp8JF8t2ur9gkAmKjakKvCy9z6w0E1Fq_E3-qSRgJfDeD4Zpw7yNoJbueWfKYFZjYtsV55KJSd7hhZ8yfL6GhBe2EB08XbgNLh5w/s626/Screenshot%202023-04-01%20at%2017.03.41.png" style="margin-left: 1em; margin-right: 1em;"><img border="1" data-original-height="434" data-original-width="626" height="222" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhU3Wfix2D64icf5zg66erepP4HCJSVd4DJWf7xQNhB-qOeSkfEiz_o70HOqufQxU8XU_sOeXFTUYim3ZOnvo2JtPyp8JF8t2ur9gkAmKjakKvCy9z6w0E1Fq_E3-qSRgJfDeD4Zpw7yNoJbueWfKYFZjYtsV55KJSd7hhZ8yfL6GhBe2EB08XbgNLh5w/s320/Screenshot%202023-04-01%20at%2017.03.41.png" width="320" /></a>
</div>
<div class="separator" style="clear: both; text-align: center;"><br /></div>
<br />
<div>
I then created a new local component - SigCapUpdateRecord - which retrieves
the field that I'm interested in for the current record using the
force:recordData component, thus saving me writing any Apex code :
</div>
<pre class="brush: html; toolbar:true"><force:recordData aura:id="recordUpdater"
recordId="{!v.recordId}"
fields="Signature_Captured__c"
targetFields="{!v.theRecordFields}"
targetError="{!v.recordLoadError}"
mode="EDIT"
/>
</pre>
<p>This component also has a handler for the SignatureCaptured event:</p>
<pre class="brush: html; toolbar: true"><aura:handler event="BGSIGCAP:SignatureCapturedEvt" action="{!c.handleCaptured}"/>
</pre>
<p>
Thus when the user saves the signature image, the handleCaptured method from
my component's controller is invoked. This sets the Signature_Captured__c
field and updates the record using the force:recordData component, again
avoiding me having to write any Apex code. Once the update is complete, the
view is refreshed to notify the container that it's changed and allow any
conditional rendering to be re-evaluated:
</p>
<pre class="brush: javascript; toolbar: true">handleCaptured : function(component, event, helper)
{
component.get('v.theRecordFields').Signature_Captured__c=true;
component.find("recordUpdater").saveRecord(function(saveResult) {
var resultsToast = $A.get("e.force:showToast");
if (saveResult.state === "SUCCESS" || saveResult.state === "DRAFT") {
resultsToast.setParams({
"type":"success",
"title": "Record Signed",
"message": "The record whas been marked as signed."
});
resultsToast.fire();
$A.get("e.force:refreshView").fire();
}
// error handling
});
}
</pre>
<p>You can see a very brief video of this below :</p>
<iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" frameborder="0" height="315" src="https://www.youtube.com/embed/_eLytC4IqVA" title="YouTube video player" width="560"></iframe>
<div><br /></div>
<h3 style="text-align: left;">The Code</h3>
<div><br /></div>
<div>
You can find the code in the
<a href="https://github.com/keirbowden/sigcapapp" target="_blank">BrightSIGN Samples repository</a> - look for the <a href="https://github.com/keirbowden/sigcapapp/tree/master/force-app/main/default/aura/SigCapUpdateRecord" target="_blank">SigCapUpdateRecord</a> component. Note that you will need to
either deploy the samples or manually create a Signature_Captured__c field on
the Account sObject in order to try this out.
</div>
<div><br /></div>
<h3 style="text-align: left;">Related Posts</h3>
<div>
<ul>
<li>
<a href="https://bobbuzzard.blogspot.com/2019/10/auto-completing-signature-capture-flow.html" target="_blank">Auto-completing a Signature Capture Flow</a>
</li>
<li>
<a href="https://bobbuzzard.blogspot.com/2018/06/adding-signature-capture-to-lightning.html" target="_blank">Adding Signature Capture to a Lightning Flow</a>
</li>
<li>
<a href="http://bobbuzzard.blogspot.com/2016/11/lightning-component-actions-with_10.html" rel="noopener" target="_blank">Lightning Component Actions with Signature Capture Part 2</a>
</li>
<li>
<a href="https://bobbuzzard.blogspot.co.uk/2016/11/lightning-component-actions-with.html" rel="noopener" target="_blank">Lightning Component Actions with Signature Capture</a>
</li>
</ul>
</div>
<div><br /></div>
<div>
<a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a>
</div>
<div><br /></div>
<div>
<br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe>
</div>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-78071056263191479092023-04-01T07:52:00.003+01:002023-04-01T07:52:43.230+01:00Artificial Indolence - Your Automated Accomplice<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQm4XcuJi8enX-Onxny27zxDt1utwJw7d3fxd60jVp6IeCy9U1jQ6zG6gUG_A0jioHe9jvDkXd1iYEnsXQcDtrsIpEgWg_-eq533UKoTxovNbq7NJZautHcL058yQmyn9q7I-_KbSK_KhRphkVMwQRDxHUnKo2iETyJvt3RAptp1jwwrQMfap9qUS2mw/s666/indolent.jpeg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="375" data-original-width="666" height="180" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQm4XcuJi8enX-Onxny27zxDt1utwJw7d3fxd60jVp6IeCy9U1jQ6zG6gUG_A0jioHe9jvDkXd1iYEnsXQcDtrsIpEgWg_-eq533UKoTxovNbq7NJZautHcL058yQmyn9q7I-_KbSK_KhRphkVMwQRDxHUnKo2iETyJvt3RAptp1jwwrQMfap9qUS2mw/s320/indolent.jpeg" width="320" /></a></div><br /><div><br /></div>
<p>
Hot on the heels of the Einstein GPT launch at TrailblazerDX, it gives me
great pleasure to announce Artificial Indolence. If you are lazy, workshy, or
just plain incompetent, Artificial Indolence can help you to hang on to your
job while underperforming, far longer than anyone would reasonably expect.
</p>
<p>
Artificial Indolence is a general purpose tool (GPT), with use cases across
the enterprise, including:
</p>
<h3 style="text-align: left;">Case Work Deflector</h3>
<p>
The lazy service rep's worst nightmare - in spite of the best efforts of
Einstein, the customer was able to raise a case and it's been assigned to you.
Drawing on a training set of over a billion work-dodging communications,
Artificial Indolence presents you with a tailored response that clearly
explains why you are the worst person to be assigned work for the specific
customer and problem. Clearly calling out your lack of ability and experience,
it will be obvious to any manager that this can only end in disaster. If
you've chosen the Communication Bus add-on, it will also identify a range of
your peers who the work would be better assigned to and throw them under the
bus.
</p>
<h3 style="text-align: left;">Closed Lost Justifier</h3>
<p>
Our most successful feature to date, and perfect for the sales rep who just
can't be bothered. When your lack of effort inevitably leads to an Opportunity
being marked Closed Lost, Artificial Indolence generates a plausible reason
why the stars didn't align in spite of your herculean efforts.
</p>
<h3 style="text-align: left;">Activity Delayer</h3>
<p>
Rather than disappointing your customer all at once by not completing a job on
time, drip feed the disappointment over weeks if not months with Artificial
Indolence Activity Delayer - the customer will receive confusing
communications outside working hours, and with any luck will blame themselves
for not responding. If you've sprung for the Meeting Avoidance add-on,
Indolence will automatically schedule meetings when most of the invitees have
conflicts - the meeting will never happen and it's because everyone other than
you is too busy!
</p>
<h3 style="text-align: left;">Coverage Concealer</h3>
<p>
One for the developers who aren't interested in writing unit tests -
Artificial Indolence will generate huge classes with 100% test coverage to
bring the org coverage up to 75%. As these classes are based on genuine
examples found in other orgs, they serve a real purpose (unless the examples
were also generated by Indolence of course). Good enough to convince
Salesforce to increase your Apex character limit, or three months free!
</p>
<p><br /></p>
<p>
As befits it's purpose, Artificial Indolence is right now a collection of
smoke, mirrors, mock ups and brochureware. Stay tuned for details of the pilot
program that nobody will be accepted on to.
</p>
<p><br /></p>
<p><br /></p>
<div>
<a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a>
</div>
<div><br /></div>
<div>
<br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe>
</div>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-64227500175608957292023-03-29T18:47:00.001+01:002023-03-29T18:47:30.128+01:00Lightning Web Component References in Spring '23<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjqiA0LIZ2BAm1gh8M5uNaAcZ4DznLgDOJ1Id8VcwWl0ScIZBbZUZlg8RGp-hNhmnb3sLuysR7YLCNg66bBi_QafO-tHMPbCorhAbWxN6lsCM4lf1M9tbHiEpyj57hNZAgIrvThHo19HnbO9a0sl09_VXXdupP9taPYVW2WQ1YryruO6XjgtRvLgo7zfA/s823/reference.jpeg" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="500" data-original-width="823" height="194" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjqiA0LIZ2BAm1gh8M5uNaAcZ4DznLgDOJ1Id8VcwWl0ScIZBbZUZlg8RGp-hNhmnb3sLuysR7YLCNg66bBi_QafO-tHMPbCorhAbWxN6lsCM4lf1M9tbHiEpyj57hNZAgIrvThHo19HnbO9a0sl09_VXXdupP9taPYVW2WQ1YryruO6XjgtRvLgo7zfA/s320/reference.jpeg" width="320" /></a></div><h3 style="text-align: left;">Introduction</h3>
<p>
Another week, another post about new LWC directives in 'Spring 23 - this time
the lwc:ref directive. As the name suggests, this provides a reference to the
component, one that can be used in JavaScript to access the element with a
minimum of fuss.
</p>
<h3 style="text-align: left;">Usage</h3>
<p>
To define a reference to a component, simply specify the lwc:ref directive in
the HTML:
</p>
<pre class="brush:html; toolbar: true"><h1 class="slds-var-m-bottom_small" lwc:ref="Heading1">Directives</h1>
</pre>
<p>and to access the element in JavaScript via the reference:</p>
<pre class="brush: javascript; toolbar: true">const headingEle=this.refs.Heading1;
</pre>
<p>
As this.refs is an object, you can also retrieve a reference based on a
variable:
</p>
<pre class="brush: javascript; toolbar: true">const refVal='Heading1';
...
const chosenEle=this.refs[refval];</pre>
<h3 style="text-align: left;">Notes/Gotchas</h3>
The reference value in the HTML markup must be a string literal - you can't use
properties to dynamically generate the reference. This has the knock-on effect
that you can't use it in loops to generate a unique reference for each element.
If you try, you'll get an error similar to the following when you deploy or push your
source:<br />
<pre> LWC1158:
Invalid lwc:ref usage on element "<li>". lwc:ref cannot be used inside
for:each or an iterator.<br /></pre>
<p></p>
<p>
Hopefully this will change in a future release, as it's a key use case that
has been a struggle since Visualforce days.
</p>
<p>
You can, however, define multiple references with the same name - in this case
the last one defined will win. I'm surprised that this isn't blocked by the
platform, as it feels like a really easy mistake to make and one that I'm sure
I'll encounter a lot over the next couple of years!
</p>
<p>
If you extract a reference that doesn't exist, in true JavaScript fashion it will return
null rather than indicating an error. Always check your reference resolved to
an element before taking any further action!
</p>
<h3 style="text-align: left;">Why Use Refs</h3>
<div>
So why should we use references, when we can already provide a reference via a
<a href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes" target="_blank">data attribute</a>, and dynamically generate the value to boot? There are a few good reasons:
</div>
<div>
<ul style="text-align: left;">
<li>
Clarity. A data attribute is a generic mechanism for storing
additional information on an HTML element - information that is useful to
the application. In order to determine the purpose of this information, I
need to examine the JavaScript to figure out how it is used. A reference,
on the other hand, has a single purpose - to provide a way to locate the
element based on the reference value - no need to look at any JavaScript. As mentioned above, I have to compromise on this clarity and fall back on
data attributes to dynamically generate a value, but hopefully that will
be coming soon.<br /><br />
</li>
<li>
Cleanliness. Data attributes appear in the rendered HTML, while references
don't. This also has the benefit that you aren't exposing something you
rely on to possible external interference or for someone else's code to rely on, thus creating an unexpected dependency.<br /><br />
</li>
<li>Performance - my opinion! I'd also imagine that it's more performant to use references than executing selector queries - the this.refs object is built once, applies to the current
template only, and I then extract elements anywhere in my JavaScript with no
additional effort. Using selectors I have to execute a query each time I need an element, and I might need multiple selectors to find disparate
elements by their attributes. This is the case in <a href="http://Vue.js">Vue.js</a> which has it's
own <a href="https://vuejs.org/guide/essentials/template-refs.html" target="_blank">references implementation</a>, so I'd imagine LWC is similar.
</li>
</ul>
</div>
<h3 style="text-align: left;">Related Posts</h3>
<div>
<ul style="text-align: left;">
<li>
<a href="https://developer.salesforce.com/docs/component-library/documentation/en/lwc/create_components_dom_work" target="_blank">lwc:ref official documentation (scroll down to the Refs section)</a>
</li>
<li>
<a href="https://bobbuzzard.blogspot.com/2023/02/conditional-lwc-directives-in-spring-23.html" target="_blank">Conditional LWC Directives in Spring '23</a>
</li>
</ul>
</div>
<p><br /></p>
<div>
<a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a>
</div>
<div><br /></div>
<div>
<br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe>
</div>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-72285939307664734172023-03-17T18:11:00.001+00:002023-03-17T18:11:16.929+00:00Einstein GPT - Rise of the Machines?<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgaQWEmx2KGDcLiNsreagw82BLbfObkJW0UeQIZO64iJ88ML45Y_2lPuI71DOg9g_VmAe2Nx0M3_WWqUqDK4RgV8_QB192Bjo2I8vxDnyx0bJxj9D9EQzKgtsA1BxzQsEj55uH_4uWoWtdgJs1OgOmaaBJ3aMLq7LsOS_cj2wtseR8K21M3P9-0fzbY9w/s888/terminator3.jpeg" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="499" data-original-width="888" height="180" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgaQWEmx2KGDcLiNsreagw82BLbfObkJW0UeQIZO64iJ88ML45Y_2lPuI71DOg9g_VmAe2Nx0M3_WWqUqDK4RgV8_QB192Bjo2I8vxDnyx0bJxj9D9EQzKgtsA1BxzQsEj55uH_4uWoWtdgJs1OgOmaaBJ3aMLq7LsOS_cj2wtseR8K21M3P9-0fzbY9w/s320/terminator3.jpeg" width="320" /></a>
</div>
<p>
The very first computer magazine that I bought, way back in February 1981, was
an issue of Personal Computer World with a story entitled "<a href="http://www.tebbo.com/archive/pw8102.htm" target="_blank">THE LAST ONE</a>". This was a revolutionary system where you described what you wanted the
program to do in plain English and it generated the code for you. This was
being sensationalised as the end of programmers as we know them, yet I've
managed a fruitful career in the 4 decades since.
</p>
<p>
Plain English was doing a lot of heavy lifting here, and the reality was that
you chose options from menus to build a flowchart that eventually generated
BASIC code, but it was pretty cool for it's time. Fast forward 42 years and
with Salesforce announcing <a href="https://www.salesforce.com/news/press-releases/2023/03/07/einstein-generative-ai/" target="_blank">Einstein GPT</a> at TrailblazerDX 23, it appears we'll be able to describe our requirements in a plain text comment and the class will be generated for us. So does this finally mean the end of programmers as we know them?
Once again I think that would be a premature conclusion to jump to.
</p>
<p>
If you'll permit me another stroll down memory lane, I'll use a documentary on
factory automation to explain why. This was a ceramics factory that produced,
among other things, mugs. It was the job of the final worker on the assembly
line to take the mug and place it in a cardboard box for shipping. The company
had invested in automation and he'd been put on notice that he was to be made
redundant as part of this push to automate all the things. The slight downside
was that the robot that picked the mug off the assembly line and put it in the
box wasn't initially very accurate. it was knocking mugs off the conveyor when
it tried to pick them up, and often dropped them when swinging round to the
cardboard box, resulting in huge breakage numbers. This worker was then
retained to stand near the robot and catch the items that it knocked over or
dropped! The robot was continually being adjusted and adapted to do a better
job, so he was essentially acting as it's minder until it got smart enough not
to need him, at which point he'd be let go.
</p>
<p>
I think that we are in the same situation with Einstein GPT (and all the other
GPTs) - we can ask them to create something, they will have a go, but you
still need a human there to check that they got it right and fix what they got
wrong. For simple classes with a method or two, it will make a good fist of
it, and the same for the test classes, but as the complexity rises it's likely
to need more human intervention. Human guardrails if you will, ensuring things
stay heading in the right direction.
</p>
<p>
What Einstein GPT will give us is very useful starting points, rather like
templates. Just like templates of old this will remove the need for us to
continually rewrite the same few lines of boilerplate code to declare a class,
except that these templates will be ever evolving based on what is already
present in our codebase, and generated on demand based on what we can
articulate (for regular classes) or what a class currently looks like (for
test classes). For simple requirements they may be a very good first cut that
just needs a bit of polishing.
</p>
<p>
A real time and money saver if most of the code written is relatively
straightforward, and a potentially huge productivity boost for junior
developers, as long as it gets it right most of the time. Experienced
developers should also get a productivity improvement - they'll focus more of
their time on the hard stuff rather than setting up the basics.
</p>
<p>
So will Einstein GPT take the jobs of Salesforce developers? I think
not.
</p>
<p>
Salesforce Developers that use Einstein GPT may take the jobs of Salesforce
Developers who don't, especially if the majority of the development work is
low complexity.
</p>
<div><br /></div>
<div>
<a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a>
</div>
<div><br /></div>
<div>
<br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe>
</div>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com0tag:blogger.com,1999:blog-6140199022860280334.post-32699623297234534162023-03-11T16:41:00.000+00:002023-03-11T16:41:00.848+00:00Apex Roadmap: User Mode with Permission Sets<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiDs6UY2gw3DNsX13H9arHAPa8q1fYY7OOhzxofZz9zmg5YcclSPWI8G6EQldb1ljIPrBjWR6vRk2onaorSjq3ZX9R91bXNhp-8DRWkqFxMAccsyy-OyAagRiuLzlG5jBM3Ge_orWvJ91xLfwMDUmZ9Eko3tirIFIaZP-mDYb4VDoDaFOywlr-ZNicYEw/s670/permission.jpeg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="372" data-original-width="670" height="178" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiDs6UY2gw3DNsX13H9arHAPa8q1fYY7OOhzxofZz9zmg5YcclSPWI8G6EQldb1ljIPrBjWR6vRk2onaorSjq3ZX9R91bXNhp-8DRWkqFxMAccsyy-OyAagRiuLzlG5jBM3Ge_orWvJ91xLfwMDUmZ9Eko3tirIFIaZP-mDYb4VDoDaFOywlr-ZNicYEw/s320/permission.jpeg" width="320" /></a></div>
<p>
As has been the case since the first TrailheaDX in 2016, the stars didn't
align so that I could attend in person. Luckily a lot of the content is
already up on Salesforce+, including a very interesting
<a href="https://www.salesforce.com/plus/experience/trailblazerdx_2023/series/Developers_for_TrailblazerDX_2023/episode/episode-s1e1" target="_blank">session on the Apex roadmap</a>
from
<a href="https://twitter.com/FishOfPrey" target="_blank">Daniel Ballinger</a>
and
<a href="https://twitter.com/ca_peterson" target="_blank">Chris Peterson</a>.
One feature that is currently in development really stood out for me - User
Mode with Permission Sets.
</p>
<h3 style="text-align: left;">User Mode</h3>
<p>
User Mode went GA in the Spring 23 release of Salesforce, and is a real
benefit for those of us with app exchange packages containing Apex code. For
those who haven't attempted this and the associated security review, here's a
brief summary of what you are up against with the previous tooling:
</p>
<p></p>
<ul style="text-align: left;">
<li>
You need to respect the CRUD and FLS permissions set by the subscriber org
(the Salesforce instance into which the package has been installed)
administrator. This is of course quite right, otherwise your package could
just help itself to confidential information that it has no business
accessing.
</li>
<li>
There isn't much support in the platform for this, so you need to either
create your own methods to check this for each object and field, or use one
of the myriad third party offerings.
</li>
</ul>
<div>
I've always been in agreement with this governance being enforced in the
security review, and also of the strong opinion that if this is a requirement
it should be baked into the platform, rather than pushed back on everyone that
is developing a package with code. There must be tens of thousands of lines of
code in all the packages out there, all re-solving the same problem.
</div>
<div><br /></div>
<div>
There were previous attempts to solve this, but they all had compromises. User
Mode genuinely takes the transaction out of system mode so that all configured
permissions apply for the database operation. If the database operation
attempts to use fields or objects that aren't accessible, errors are returned
for every disallowed item.
</div>
<div><br /></div>
<div>
All well and good when you are respecting the admin's decision about access,
but what happens if access to objects and fields managed by the package is
required in order for the package to function correctly? You could put the
user through death by a thousand cuts, allowing them to get slightly further
each time before an error is thrown and they request additional permissions
from their admin, or you can execute your database operations in System
Mode. This is quite the nuclear option though, as it opens up access to
every object and field in the instance. If all you need is to get at the value
for a single checkbox, it's an extremely large hammer to crack a small nut.
</div><div><br /></div>
<h3 style="text-align: left;">User Mode with Permission Sets</h3>
<div><br /></div><div>
User Mode with Permission Sets allows you to select an appropriate sized
hammer for the nut in question. Rather than elevating the user's access to
everything, everywhere, you can give them the minimal amount of additional
privilege required for your package to function correctly, just for the
database operations that it is needed. To continue the example of needing
access to a single checkbox value, you'd also package a permission set that
grants access to just that field and execute the database operation in user
mode plus that permission set. Much better from a security and governance
perspective.
</div>
<div><br /></div>
<div>
Here's a code snippet snatched from the session - note that as this is still
in development the syntax is likely to change, but it gives a sense of the
direction of travel:
</div>
<div><br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiTipvNX55qoSh6CPJwgLRdzMrZyexQKNI7knVpk_8BpO21a5EpAxiK7lwtpfnhJ_d7o9BRiMhCPnhAN6DdbET3JuvvaLSEGftrPeKMi8xuetLF11T9h_556et6Vb8H6nb3G1RFNssktbYhs28YwTR0f7UCHh5ztXExYJfGnu2b0DGsGSXWrAl3b983ag/s1094/Screenshot%202023-03-11%20at%2016.30.50.png" style="margin-left: 1em; margin-right: 1em;"><img border="1" data-original-height="516" data-original-width="1094" height="302" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiTipvNX55qoSh6CPJwgLRdzMrZyexQKNI7knVpk_8BpO21a5EpAxiK7lwtpfnhJ_d7o9BRiMhCPnhAN6DdbET3JuvvaLSEGftrPeKMi8xuetLF11T9h_556et6Vb8H6nb3G1RFNssktbYhs28YwTR0f7UCHh5ztXExYJfGnu2b0DGsGSXWrAl3b983ag/w640-h302/Screenshot%202023-03-11%20at%2016.30.50.png" width="640" /></a>
</div>
<br />
<div><br /></div>
<div>
Packages aren't the only use case for this though, limiting the data that your
Apex code has access to is good practice regardless of whether it's going into
a package or being deployed directly to an org. Sadly it wouldn't stop my Evil
Co-Worker, as when they have access to the code they can simply change it to
run in System Mode in all cases. It will limit the impact of simple mistakes
though, and keep your Information Security department a little happier.
</div><div><br /></div>
<h3 style="text-align: left;">More Information</h3><div><br /></div><div><ul style="text-align: left;"><li><a href="https://www.salesforce.com/plus/experience/trailblazerdx_2023/series/Developers_for_TrailblazerDX_2023/episode/episode-s1e1" target="_blank">TDX 23 Apex roadmap session on Salesforce+</a>
</li>
<li>
<a href="https://speakerdeck.com/fishofprey/pdf-tdx-2023-apex-what-s-new-and-what-s-coming" target="_blank">Slide deck from the session</a>
</li>
<li>
<a href="https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_enforce_usermode.htm" rel="nofollow">User Mode documentation</a>
</li>
</ul>
</div>
<div><br /></div>
<div>
<a class="twitter-follow-button" data-show-count="false" href="https://twitter.com/bob_buzzard" style="font-family: "lucida grande";">Follow @bob_buzzard</a>
</div>
<div><br /></div>
<div>
<br class="Apple-interchange-newline" /><iframe frameborder="0" height="320" scrolling="no" src="https://bobbuzz.substack.com/embed" style="background: white; border: 1px solid #EEE;" width="480"></iframe>
</div>
Bob Buzzardhttp://www.blogger.com/profile/07815472992351343395noreply@blogger.com1