├── resources └── logo.png ├── force-app ├── main │ └── default │ │ ├── pages │ │ ├── sessionId.page │ │ └── sessionId.page-meta.xml │ │ ├── contentassets │ │ ├── myorgbutlertransparent_720.asset │ │ └── myorgbutlertransparent_720.asset-meta.xml │ │ ├── classes │ │ ├── SessionId.cls-meta.xml │ │ ├── CallGitHubApi.cls-meta.xml │ │ ├── CallRestApi.cls-meta.xml │ │ ├── CallToolingApi.cls-meta.xml │ │ ├── SearchWeb.cls-meta.xml │ │ ├── AgentMemory.cls-meta.xml │ │ ├── CallMetadataApi.cls-meta.xml │ │ ├── CustomSettings.cls-meta.xml │ │ ├── SearchWeb_Test.cls-meta.xml │ │ ├── AgentMemory_Test.cls-meta.xml │ │ ├── CallGitHubApi_Test.cls-meta.xml │ │ ├── CallMetadataApi_Test.cls-meta.xml │ │ ├── CallRestApi_Test.cls-meta.xml │ │ ├── CallToolingApi_Test.cls-meta.xml │ │ ├── CreatePlantUmlUrl.cls-meta.xml │ │ ├── ExploreOrgSchema.cls-meta.xml │ │ ├── QueryRecordsWithSoql.cls-meta.xml │ │ ├── SessionId.cls │ │ ├── CreatePlantUmlUrl_Test.cls-meta.xml │ │ ├── ExploreOrgSchema_Test.cls-meta.xml │ │ ├── LoadCustomInstructions.cls-meta.xml │ │ ├── QueryRecordsWithSoql_Test.cls-meta.xml │ │ ├── RetrieveVectorizeChunks.cls-meta.xml │ │ ├── StoreCustomInstruction.cls-meta.xml │ │ ├── LoadCustomInstructions_Test.cls-meta.xml │ │ ├── NightlyMemoryConsolidation.cls-meta.xml │ │ ├── RetrieveVectorizeChunks_Test.cls-meta.xml │ │ ├── StoreCustomInstruction_Test.cls-meta.xml │ │ ├── CallRestApi_Test.cls │ │ ├── CallGitHubApi_Test.cls │ │ ├── CreatePlantUmlUrl_Test.cls │ │ ├── CustomSettings.cls │ │ ├── SearchWeb_Test.cls │ │ ├── CallMetadataApi_Test.cls │ │ ├── CreatePlantUmlUrl.cls │ │ ├── LoadCustomInstructions_Test.cls │ │ ├── NightlyMemoryConsolidation.cls │ │ ├── SearchWeb.cls │ │ ├── StoreCustomInstruction.cls │ │ ├── QueryRecordsWithSoql.cls │ │ ├── StoreCustomInstruction_Test.cls │ │ ├── CallGitHubApi.cls │ │ ├── RetrieveVectorizeChunks_Test.cls │ │ ├── CallRestApi.cls │ │ ├── LoadCustomInstructions.cls │ │ ├── AgentMemory_Test.cls │ │ ├── QueryRecordsWithSoql_Test.cls │ │ ├── ExploreOrgSchema_Test.cls │ │ ├── AgentMemory.cls │ │ ├── CallMetadataApi.cls │ │ ├── CallToolingApi_Test.cls │ │ ├── CallToolingApi.cls │ │ └── RetrieveVectorizeChunks.cls │ │ ├── tabs │ │ └── Memory__c.tab-meta.xml │ │ ├── genAiFunctions │ │ ├── LoadCustomInstructions │ │ │ ├── input │ │ │ │ └── schema.json │ │ │ ├── output │ │ │ │ └── schema.json │ │ │ └── LoadCustomInstructions.genAiFunction-meta.xml │ │ ├── QueryRecordsWithSoql │ │ │ ├── input │ │ │ │ └── schema.json │ │ │ ├── output │ │ │ │ └── schema.json │ │ │ └── QueryRecordsWithSoql.genAiFunction-meta.xml │ │ ├── SearchVectorDatabase │ │ │ ├── input │ │ │ │ └── schema.json │ │ │ ├── output │ │ │ │ └── schema.json │ │ │ └── SearchVectorDatabase.genAiFunction-meta.xml │ │ ├── SearchWeb │ │ │ ├── input │ │ │ │ └── schema.json │ │ │ ├── output │ │ │ │ └── schema.json │ │ │ └── SearchWeb.genAiFunction-meta.xml │ │ ├── CallRestApi │ │ │ ├── output │ │ │ │ └── schema.json │ │ │ ├── input │ │ │ │ └── schema.json │ │ │ └── CallRestApi.genAiFunction-meta.xml │ │ ├── CallMetadataApi │ │ │ ├── output │ │ │ │ └── schema.json │ │ │ ├── CallMetadataApi.genAiFunction-meta.xml │ │ │ └── input │ │ │ │ └── schema.json │ │ ├── CallToolingApi │ │ │ ├── output │ │ │ │ └── schema.json │ │ │ ├── CallToolingApi.genAiFunction-meta.xml │ │ │ └── input │ │ │ │ └── schema.json │ │ ├── CallGitHubApi │ │ │ ├── output │ │ │ │ └── schema.json │ │ │ ├── CallGitHubApi.genAiFunction-meta.xml │ │ │ └── input │ │ │ │ └── schema.json │ │ ├── CreatePlantUmlUrl │ │ │ ├── input │ │ │ │ └── schema.json │ │ │ ├── output │ │ │ │ └── schema.json │ │ │ └── CreatePlantUmlUrl.genAiFunction-meta.xml │ │ ├── ExploreOrgSchema │ │ │ ├── output │ │ │ │ └── schema.json │ │ │ ├── ExploreOrgSchema.genAiFunction-meta.xml │ │ │ └── input │ │ │ │ └── schema.json │ │ ├── AnswerWithCurrentFile │ │ │ ├── output │ │ │ │ └── schema.json │ │ │ ├── AnswerWithCurrentFile.genAiFunction-meta.xml │ │ │ └── input │ │ │ │ └── schema.json │ │ ├── AnswerWithRelatedFiles │ │ │ ├── output │ │ │ │ └── schema.json │ │ │ ├── AnswerWithRelatedFiles.genAiFunction-meta.xml │ │ │ └── input │ │ │ │ └── schema.json │ │ └── StoreCustomInstruction │ │ │ ├── output │ │ │ └── schema.json │ │ │ ├── input │ │ │ └── schema.json │ │ │ └── StoreCustomInstruction.genAiFunction-meta.xml │ │ ├── objects │ │ ├── CustomSetting__c │ │ │ ├── CustomSetting__c.object-meta.xml │ │ │ └── fields │ │ │ │ └── Value__c.field-meta.xml │ │ └── Memory__c │ │ │ ├── listViews │ │ │ └── All.listView-meta.xml │ │ │ ├── fields │ │ │ ├── IsShared__c.field-meta.xml │ │ │ └── Content__c.field-meta.xml │ │ │ └── Memory__c.object-meta.xml │ │ ├── externalCredentials │ │ ├── TavilyApi.externalCredential-meta.xml │ │ ├── VectorizeApi.externalCredential-meta.xml │ │ └── GitHubApi.externalCredential-meta.xml │ │ ├── cspTrustedSites │ │ ├── Google.cspTrustedSite-meta.xml │ │ ├── Atlassian.cspTrustedSite-meta.xml │ │ ├── Github.cspTrustedSite-meta.xml │ │ └── PlantUml.cspTrustedSite-meta.xml │ │ ├── namedCredentials │ │ ├── GitHubApi.namedCredential-meta.xml │ │ ├── TavilyApi.namedCredential-meta.xml │ │ └── VectorizeApi.namedCredential-meta.xml │ │ ├── genAiPromptTemplates │ │ ├── ConsolidateMemory.genAiPromptTemplate-meta.xml │ │ ├── AnswerFromFile.genAiPromptTemplate-meta.xml │ │ ├── AnswerFromVectorizeChunks.genAiPromptTemplate-meta.xml │ │ └── AnswerFromRelatedFiles.genAiPromptTemplate-meta.xml │ │ ├── layouts │ │ └── Memory__c-Memory Layout.layout-meta.xml │ │ ├── permissionsets │ │ └── MyOrgButlerUser.permissionset-meta.xml │ │ └── genAiPlugins │ │ ├── AnswerWithInternet.genAiPlugin-meta.xml │ │ ├── EditData.genAiPlugin-meta.xml │ │ ├── AnswerWithFiles.genAiPlugin-meta.xml │ │ └── AnswerWithData.genAiPlugin-meta.xml └── jfwberg │ └── lightweight-soap-util │ └── package │ ├── core │ ├── classes │ │ ├── Wsdl.cls-meta.xml │ │ └── XmlWriter.cls-meta.xml │ └── pages │ │ ├── getSoapApiSessionId.page-meta.xml │ │ └── getSoapApiSessionId.page │ ├── wsdl │ └── classes │ │ ├── ApxWsdl.cls-meta.xml │ │ └── MdtWsdl.cls-meta.xml │ └── test │ └── classes │ ├── ApxWsdlTest.cls-meta.xml │ ├── MdtWsdlTest.cls-meta.xml │ ├── WsdlTest.cls-meta.xml │ ├── XmlWriterTest.cls-meta.xml │ ├── ApxWsdlTest.cls │ └── XmlWriterTest.cls ├── scripts ├── config.sh ├── create-package-version.sh └── create-scratch-org.sh ├── code-analyzer.yaml ├── unpackaged └── main │ └── default │ ├── permissionsets │ └── AgentAccess.permissionset-meta.xml │ └── bots │ └── MyOrgButler │ └── v1.botVersion-meta.xml ├── .forceignore ├── config └── project-scratch-def.json ├── LICENSE ├── .gitignore ├── code-analyzer-cleancode.csv ├── sfdx-project.json └── README.md /resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquivalabs/my-org-butler/HEAD/resources/logo.png -------------------------------------------------------------------------------- /force-app/main/default/pages/sessionId.page: -------------------------------------------------------------------------------- 1 | {!$Api.Session_ID} -------------------------------------------------------------------------------- /scripts/config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DEV_HUB_ALIAS="DevHubAquiva" 3 | PACKAGE_NAME="my-org-butler" 4 | SCRATCH_ORG_ALIAS="my-org-butler_DEV" -------------------------------------------------------------------------------- /force-app/main/default/contentassets/myorgbutlertransparent_720.asset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquivalabs/my-org-butler/HEAD/force-app/main/default/contentassets/myorgbutlertransparent_720.asset -------------------------------------------------------------------------------- /force-app/main/default/classes/SessionId.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 59.0 4 | Active 5 | -------------------------------------------------------------------------------- /force-app/main/default/classes/CallGitHubApi.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 63.0 4 | Active 5 | -------------------------------------------------------------------------------- /force-app/main/default/classes/CallRestApi.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 58.0 4 | Active 5 | -------------------------------------------------------------------------------- /force-app/main/default/classes/CallToolingApi.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64.0 4 | Active 5 | -------------------------------------------------------------------------------- /force-app/main/default/classes/SearchWeb.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/AgentMemory.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/CallMetadataApi.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/CustomSettings.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/SearchWeb_Test.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/pages/sessionId.page-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 59.0 4 | 5 | -------------------------------------------------------------------------------- /force-app/main/default/classes/AgentMemory_Test.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 63.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/CallGitHubApi_Test.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/CallMetadataApi_Test.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/CallRestApi_Test.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/CallToolingApi_Test.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/CreatePlantUmlUrl.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 63.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/ExploreOrgSchema.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 63.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/QueryRecordsWithSoql.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/SessionId.cls: -------------------------------------------------------------------------------- 1 | public with sharing class SessionId { 2 | public String asString() { 3 | return (Test.isRunningTest()) ? UserInfo.getSessionId() : Page.sessionId.getContent().toString().substring(15); 4 | } 5 | } -------------------------------------------------------------------------------- /force-app/main/default/tabs/Memory__c.tab-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | Custom55: Books 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/CreatePlantUmlUrl_Test.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 63.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/ExploreOrgSchema_Test.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 63.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/LoadCustomInstructions.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/QueryRecordsWithSoql_Test.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/RetrieveVectorizeChunks.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/StoreCustomInstruction.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/jfwberg/lightweight-soap-util/package/core/classes/Wsdl.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62.0 4 | Active 5 | -------------------------------------------------------------------------------- /force-app/main/default/classes/LoadCustomInstructions_Test.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/NightlyMemoryConsolidation.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/RetrieveVectorizeChunks_Test.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/StoreCustomInstruction_Test.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/jfwberg/lightweight-soap-util/package/core/classes/XmlWriter.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62.0 4 | Active 5 | -------------------------------------------------------------------------------- /force-app/jfwberg/lightweight-soap-util/package/wsdl/classes/ApxWsdl.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62.0 4 | Active 5 | -------------------------------------------------------------------------------- /force-app/jfwberg/lightweight-soap-util/package/wsdl/classes/MdtWsdl.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62.0 4 | Active 5 | -------------------------------------------------------------------------------- /force-app/jfwberg/lightweight-soap-util/package/test/classes/ApxWsdlTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62.0 4 | Active 5 | -------------------------------------------------------------------------------- /force-app/jfwberg/lightweight-soap-util/package/test/classes/MdtWsdlTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62.0 4 | Active 5 | -------------------------------------------------------------------------------- /force-app/jfwberg/lightweight-soap-util/package/test/classes/WsdlTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/jfwberg/lightweight-soap-util/package/test/classes/XmlWriterTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/jfwberg/lightweight-soap-util/package/core/pages/getSoapApiSessionId.page-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62.0 4 | 5 | -------------------------------------------------------------------------------- /force-app/jfwberg/lightweight-soap-util/package/core/pages/getSoapApiSessionId.page: -------------------------------------------------------------------------------- 1 | {!$Api.Session_ID} -------------------------------------------------------------------------------- /code-analyzer.yaml: -------------------------------------------------------------------------------- 1 | engines: 2 | pmd: 3 | custom_rulesets: ['pmd-ruleset.xml'] 4 | file_extensions: { 5 | "apex":[ 6 | ".cls", 7 | ".trigger" 8 | ], 9 | "visualforce":[ 10 | ".page", 11 | ".component" 12 | ], 13 | "xml":[ 14 | ".xml" 15 | ] 16 | } -------------------------------------------------------------------------------- /unpackaged/main/default/permissionsets/AgentAccess.permissionset-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MyOrgButler 5 | true 6 | 7 | false 8 | 9 | 10 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/LoadCustomInstructions/input/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "unevaluatedProperties" : false, 3 | "properties" : { 4 | "ignored" : { 5 | "title" : "ignored", 6 | "description" : "no input", 7 | "lightning:type" : "lightning__textType", 8 | "lightning:isPII" : false, 9 | "copilotAction:isUserInput" : false 10 | } 11 | }, 12 | "lightning:type" : "lightning__objectType" 13 | } -------------------------------------------------------------------------------- /force-app/main/default/objects/CustomSetting__c/CustomSetting__c.object-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | List 4 | Generic key-value configuration store for application settings 5 | false 6 | 7 | Public 8 | 9 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Memory__c/listViews/All.listView-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | All 4 | NAME 5 | Content__c 6 | IsShared__c 7 | CREATEDBY_USER 8 | CREATED_DATE 9 | Everything 10 | 11 | 12 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/QueryRecordsWithSoql/input/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "required" : [ "dynamicSoqlQuery" ], 3 | "unevaluatedProperties" : false, 4 | "properties" : { 5 | "dynamicSoqlQuery" : { 6 | "title" : "SOQL Query", 7 | "description" : "Query to be executed as Dynamic SOQL", 8 | "lightning:type" : "lightning__textType", 9 | "lightning:isPII" : false, 10 | "copilotAction:isUserInput" : false 11 | } 12 | }, 13 | "lightning:type" : "lightning__objectType" 14 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/SearchVectorDatabase/input/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "required" : [ "content" ], 3 | "unevaluatedProperties" : false, 4 | "properties" : { 5 | "content" : { 6 | "title" : "Question", 7 | "description" : "The question to search for in the company knowledge base.", 8 | "lightning:type" : "lightning__textType", 9 | "lightning:isPII" : false, 10 | "copilotAction:isUserInput" : false 11 | } 12 | }, 13 | "lightning:type" : "lightning__objectType" 14 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/SearchWeb/input/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "required" : [ "content" ], 3 | "unevaluatedProperties" : false, 4 | "properties" : { 5 | "content" : { 6 | "title" : "Query", 7 | "description" : "A natural language search query to search the web for and get an answer for.", 8 | "lightning:type" : "lightning__textType", 9 | "lightning:isPII" : false, 10 | "copilotAction:isUserInput" : false 11 | } 12 | }, 13 | "lightning:type" : "lightning__objectType" 14 | } -------------------------------------------------------------------------------- /force-app/main/default/objects/CustomSetting__c/fields/Value__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Value__c 4 | The configuration value 5 | false 6 | 7 | 255 8 | true 9 | false 10 | Text 11 | false 12 | 13 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/CallRestApi/output/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "unevaluatedProperties" : false, 3 | "properties" : { 4 | "responseBody" : { 5 | "title" : "Response Body", 6 | "description" : "Raw HTTP Response to be interpreted for Errors or Success", 7 | "lightning:type" : "lightning__textType", 8 | "lightning:isPII" : false, 9 | "copilotAction:isDisplayable" : false, 10 | "copilotAction:isUsedByPlanner" : true 11 | } 12 | }, 13 | "lightning:type" : "lightning__objectType" 14 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/CallMetadataApi/output/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "unevaluatedProperties" : false, 3 | "properties" : { 4 | "responseBody" : { 5 | "title" : "Response Body", 6 | "description" : "Raw HTTP Response to be interpreted for Errors or Success", 7 | "lightning:type" : "lightning__textType", 8 | "lightning:isPII" : false, 9 | "copilotAction:isDisplayable" : false, 10 | "copilotAction:isUsedByPlanner" : true 11 | } 12 | }, 13 | "lightning:type" : "lightning__objectType" 14 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/CallToolingApi/output/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "unevaluatedProperties" : false, 3 | "properties" : { 4 | "responseBody" : { 5 | "title" : "Response Body", 6 | "description" : "Raw HTTP Response to be interpreted for Errors or Success", 7 | "lightning:type" : "lightning__textType", 8 | "lightning:isPII" : false, 9 | "copilotAction:isDisplayable" : false, 10 | "copilotAction:isUsedByPlanner" : true 11 | } 12 | }, 13 | "lightning:type" : "lightning__objectType" 14 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/SearchWeb/output/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "unevaluatedProperties" : false, 3 | "properties" : { 4 | "answer" : { 5 | "title" : "Answer", 6 | "description" : "A Natural Language Answer for the Query generated after doing a Web Search.", 7 | "lightning:type" : "lightning__textType", 8 | "lightning:isPII" : false, 9 | "copilotAction:isDisplayable" : false, 10 | "copilotAction:isUsedByPlanner" : true 11 | } 12 | }, 13 | "lightning:type" : "lightning__objectType" 14 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/CallGitHubApi/output/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "unevaluatedProperties" : false, 3 | "properties" : { 4 | "responseBody" : { 5 | "title" : "Response Body", 6 | "description" : "Raw HTTP Response from GitHub API to be interpreted for success or errors", 7 | "lightning:type" : "lightning__textType", 8 | "lightning:isPII" : false, 9 | "copilotAction:isDisplayable" : false, 10 | "copilotAction:isUsedByPlanner" : true 11 | } 12 | }, 13 | "lightning:type" : "lightning__objectType" 14 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/CreatePlantUmlUrl/input/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "required" : [ "plantUmlText" ], 3 | "unevaluatedProperties" : false, 4 | "properties" : { 5 | "plantUmlText" : { 6 | "title" : "PlantUML Text", 7 | "description" : "The PlantUML diagram definition in text format (e.g., @startuml\\nAlice->Bob : hello\\n@enduml)", 8 | "lightning:type" : "lightning__textType", 9 | "lightning:isPII" : false, 10 | "copilotAction:isUserInput" : false 11 | } 12 | }, 13 | "lightning:type" : "lightning__objectType" 14 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/ExploreOrgSchema/output/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "unevaluatedProperties" : false, 3 | "properties" : { 4 | "response" : { 5 | "title" : "Response as JSON", 6 | "description" : "Serialized JSON output of schema exploration", 7 | "lightning:type" : "lightning__textType", 8 | "lightning:isPII" : false, 9 | "copilotAction:isDisplayable" : false, 10 | "copilotAction:isUsedByPlanner" : true, 11 | "copilotAction:useHydratedPrompt" : false 12 | } 13 | }, 14 | "lightning:type" : "lightning__objectType" 15 | } -------------------------------------------------------------------------------- /force-app/main/default/objects/Memory__c/fields/IsShared__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | IsShared__c 4 | false 5 | When checked, this memory is shared with all users in the organization. Otherwise, it is private to the creating user. 6 | 7 | false 8 | false 9 | Checkbox 10 | 11 | -------------------------------------------------------------------------------- /.forceignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status 2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm 3 | 4 | package.xml 5 | **appMenu 6 | **appSwitcher 7 | **setting 8 | **/profiles/* 9 | **/emailservices/* 10 | **/applications/standard__* 11 | 12 | # LWC configuration files 13 | **/jsconfig.json 14 | **/.eslintrc.json 15 | 16 | # LWC Jest 17 | **/__tests__/** 18 | **/tsconfig.json 19 | 20 | **/*.ts 21 | 22 | unsupported/ 23 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/CreatePlantUmlUrl/output/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "unevaluatedProperties" : false, 3 | "properties" : { 4 | "encodedImageUrl" : { 5 | "title" : "Diagram Image URL", 6 | "description" : "URL to the rendered PlantUML diagram image", 7 | "lightning:type" : "lightning__textType", 8 | "lightning:isPII" : false, 9 | "copilotAction:isDisplayable" : false, 10 | "copilotAction:isUsedByPlanner" : true, 11 | "copilotAction:useHydratedPrompt" : false 12 | } 13 | }, 14 | "lightning:type" : "lightning__objectType" 15 | } -------------------------------------------------------------------------------- /force-app/main/default/externalCredentials/TavilyApi.externalCredential-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Custom 4 | 5 | ApiKey 6 | ApiKey 7 | NamedPrincipal 8 | 1 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /force-app/main/default/externalCredentials/VectorizeApi.externalCredential-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Custom 4 | 5 | Token 6 | Token 7 | NamedPrincipal 8 | 1 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Memory__c/fields/Content__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Content__c 4 | The detailed instruction or preference content that the AI agent should remember and apply in future interactions. 5 | 6 | 32768 7 | false 8 | false 9 | LongTextArea 10 | 5 11 | 12 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/AnswerWithCurrentFile/output/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "unevaluatedProperties" : false, 3 | "properties" : { 4 | "promptResponse" : { 5 | "title" : "Prompt Response", 6 | "description" : "Answer based on attachments from Opportunity and its Account.", 7 | "lightning:type" : "lightning__textType", 8 | "lightning:isPII" : false, 9 | "copilotAction:isDisplayable" : false, 10 | "copilotAction:isUsedByPlanner" : true, 11 | "copilotAction:useHydratedPrompt" : false 12 | } 13 | }, 14 | "lightning:type" : "lightning__objectType" 15 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/AnswerWithRelatedFiles/output/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "unevaluatedProperties" : false, 3 | "properties" : { 4 | "promptResponse" : { 5 | "title" : "Prompt Response", 6 | "description" : "Answers based on attachments from Opportunity and its Account.", 7 | "lightning:type" : "lightning__textType", 8 | "lightning:isPII" : false, 9 | "copilotAction:isDisplayable" : false, 10 | "copilotAction:isUsedByPlanner" : true, 11 | "copilotAction:useHydratedPrompt" : false 12 | } 13 | }, 14 | "lightning:type" : "lightning__objectType" 15 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/SearchVectorDatabase/output/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "unevaluatedProperties" : false, 3 | "properties" : { 4 | "content" : { 5 | "title" : "Answer", 6 | "description" : "AI-generated answer based on company knowledge sources. Use this as SOLE response. Dont reformulate and also dont format as quote. JUST RETURN THIS!", 7 | "lightning:type" : "lightning__textType", 8 | "lightning:isPII" : false, 9 | "copilotAction:isDisplayable" : false, 10 | "copilotAction:isUsedByPlanner" : true 11 | } 12 | }, 13 | "lightning:type" : "lightning__objectType" 14 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/QueryRecordsWithSoql/output/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "unevaluatedProperties" : false, 3 | "properties" : { 4 | "resultOrException" : { 5 | "title" : "Result or Exception JSON", 6 | "description" : "JSON array of result records or aggregate results on success, or exception object on error", 7 | "lightning:type" : "lightning__textType", 8 | "lightning:isPII" : false, 9 | "copilotAction:isDisplayable" : false, 10 | "copilotAction:isUsedByPlanner" : true, 11 | "copilotAction:useHydratedPrompt" : false 12 | } 13 | }, 14 | "lightning:type" : "lightning__objectType" 15 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/LoadCustomInstructions/output/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "unevaluatedProperties" : false, 3 | "properties" : { 4 | "customInstructions" : { 5 | "title" : "Custom Instructions", 6 | "description" : "Formatted string containing user context information including user details, timezone, organization info, current date/time, and user-specific communication preferences", 7 | "lightning:type" : "lightning__textType", 8 | "lightning:isPII" : true, 9 | "copilotAction:isDisplayable" : false, 10 | "copilotAction:isUsedByPlanner" : true 11 | } 12 | }, 13 | "lightning:type" : "lightning__objectType" 14 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/StoreCustomInstruction/output/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "unevaluatedProperties" : false, 3 | "properties" : { 4 | "customInstructions" : { 5 | "title" : "Custom Instructions", 6 | "description" : "Updated formatted string containing user context information including user details, timezone, organization info, current date/time, and user-specific instructions from memory records", 7 | "lightning:type" : "lightning__textType", 8 | "lightning:isPII" : true, 9 | "copilotAction:isDisplayable" : false, 10 | "copilotAction:isUsedByPlanner" : true 11 | } 12 | }, 13 | "lightning:type" : "lightning__objectType" 14 | } -------------------------------------------------------------------------------- /force-app/main/default/contentassets/myorgbutlertransparent_720.asset-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | en_US 5 | myorgbutlertransparent_720 6 | 7 | 8 | VIEWER 9 | 10 | 11 | 12 | 13 | 1 14 | myorgbutler-transparent_720.png 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /force-app/main/default/classes/CallRestApi_Test.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class CallRestApi_Test { 3 | 4 | @IsTest 5 | private static void execute() { 6 | // Setup 7 | new HttpMock() 8 | .get('/services/rest').body('{ value : 3}').statusCodeOk() 9 | .mock(); 10 | 11 | CallRestApi.Input input = new CallRestApi.Input(); 12 | input.httpMethod = 'GET'; 13 | input.urlIncludingParams = '/services/rest'; 14 | input.requestBody = null; 15 | 16 | // Exercise 17 | List response = CallRestApi.execute(new List{ input }); 18 | 19 | // Verify 20 | Assert.areEqual('{ value : 3}', response[0].responseBody); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /force-app/main/default/cspTrustedSites/Google.cspTrustedSite-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | false 5 | All 6 | *.google.com 7 | true 8 | false 9 | false 10 | false 11 | true 12 | false 13 | false 14 | 15 | -------------------------------------------------------------------------------- /force-app/main/default/cspTrustedSites/Atlassian.cspTrustedSite-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | false 5 | All 6 | *.atlassian.net 7 | true 8 | false 9 | false 10 | false 11 | true 12 | false 13 | false 14 | 15 | -------------------------------------------------------------------------------- /force-app/main/default/cspTrustedSites/Github.cspTrustedSite-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | false 5 | All 6 | https://github.com 7 | true 8 | false 9 | false 10 | false 11 | true 12 | false 13 | false 14 | 15 | -------------------------------------------------------------------------------- /force-app/main/default/cspTrustedSites/PlantUml.cspTrustedSite-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | false 5 | All 6 | *.plantuml.com 7 | true 8 | false 9 | false 10 | false 11 | true 12 | false 13 | false 14 | 15 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/CallGitHubApi/CallGitHubApi.genAiFunction-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Use this action to interact with GitHub repositories through the GitHub API. Supports operations like listing, creating, and managing issues, pull requests, and repositories. 4 | CallGitHubApi 5 | CallGitHubApi 6 | apex 7 | false 8 | true 9 | CallGitHubApi 10 | MyOrgButler: Call GitHub API 11 | 12 | -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "my-org-butler_DEV", 3 | "edition": "Partner Developer", 4 | "hasSampleData": true, 5 | "features": ["EnableSetPasswordInApi","Einstein1AIPlatform", "Chatbot"], 6 | "settings": { 7 | "lightningExperienceSettings": { 8 | "enableS1DesktopEnabled": true 9 | }, 10 | "mobileSettings": { 11 | "enableS1EncryptedStoragePref2": false 12 | }, 13 | "EinsteinGptSettings" : { 14 | "enableEinsteinGptPlatform": true 15 | }, 16 | "botSettings": { 17 | "enableBots": true 18 | }, 19 | "deploymentSettings": { 20 | "doesSkipAsyncApexValidation": true 21 | }, 22 | "einsteinGptSettings": { 23 | "enableEinsteinGptPlatform": true 24 | }, 25 | "agentPlatformSettings": { 26 | "enableAgentPlatform": true 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/CallGitHubApi_Test.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class CallGitHubApi_Test { 3 | 4 | @IsTest 5 | private static void execute() { 6 | // Setup 7 | new HttpMock() 8 | .get('/repos/owner/repo').body('{ "name": "repo", "full_name": "owner/repo" }').statusCodeOk() 9 | .mock(); 10 | 11 | CallGitHubApi.Input input = new CallGitHubApi.Input(); 12 | input.urlIncludingParams = '/repos/owner/repo'; 13 | input.httpMethod = 'GET'; 14 | input.requestBody = null; 15 | 16 | // Exercise 17 | List response = CallGitHubApi.execute(new List{ input }); 18 | 19 | // Verify 20 | Assert.areEqual('{ "name": "repo", "full_name": "owner/repo" }', response[0].responseBody); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /force-app/main/default/classes/CreatePlantUmlUrl_Test.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class CreatePlantUmlUrl_Test { 3 | 4 | @IsTest 5 | private static void execute() { 6 | // Setup 7 | CreatePlantUmlUrl.Input input = new CreatePlantUmlUrl.Input(); 8 | input.plantUmlText = '@startuml\nAlice->Bob : I am using hex\n@enduml'; 9 | 10 | // Exercise 11 | List results = 12 | CreatePlantUmlUrl.execute(new List{ input }); 13 | 14 | // Verify 15 | Assert.areEqual(1, results.size()); 16 | 17 | String expectedUrl = 'http://www.plantuml.com/plantuml/png/~h407374617274756d6c0a416c6963652d3e426f62203a204920616d207573696e67206865780a40656e64756d6c'; 18 | Assert.areEqual(expectedUrl, results[0].encodedImageUrl); 19 | } 20 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/CustomSettings.cls: -------------------------------------------------------------------------------- 1 | // Note: Small stateless helper - no constructor or state needed, static access is cleaner 2 | @SuppressWarnings('PMD.PreferRealObjectsOverStaticHelpers') 3 | public with sharing class CustomSettings { 4 | 5 | public static String valueFor(String settingName) { 6 | CustomSetting__c setting = CustomSetting__c.getValues(settingName); 7 | 8 | if (setting == null) { 9 | throw new ApplicationException('Custom Setting not found: ' + settingName); 10 | } 11 | 12 | return setting.Value__c; 13 | } 14 | 15 | @TestVisible 16 | private static void mock(String settingName, String value) { 17 | CustomSetting__c setting = new CustomSetting__c( 18 | Name = settingName, 19 | Value__c = value 20 | ); 21 | upsert as user setting; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/CallMetadataApi/CallMetadataApi.genAiFunction-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Use this action to read and create Metadata that cannot be handled by the Salesforce tooling API. 4 | When in doubt or the action returns an error use the SearchWeb action to find out which API support that type or activity. 5 | CallMetadataApi 6 | CallMetadataApi 7 | apex 8 | false 9 | true 10 | CallMetadataApi 11 | MyOrgButler: Call Metadata API 12 | 13 | -------------------------------------------------------------------------------- /force-app/main/default/externalCredentials/GitHubApi.externalCredential-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Custom 4 | 5 | ApiKey 6 | ApiKey 7 | NamedPrincipal 8 | 1 9 | 10 | 11 | DefaultGroup 12 | Authorization 13 | AuthHeader 14 | Bearer {!$Credential.GitHubApi.ApiKey} 15 | 1 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /force-app/main/default/namedCredentials/GitHubApi.namedCredential-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | true 5 | Enabled 6 | false 7 | 8 | 9 | Url 10 | Url 11 | https://api.github.com 12 | 13 | 14 | GitHubApi 15 | ExternalCredential 16 | Authentication 17 | 18 | SecuredEndpoint 19 | -------------------------------------------------------------------------------- /force-app/main/default/namedCredentials/TavilyApi.namedCredential-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | false 5 | Enabled 6 | false 7 | 8 | 9 | Url 10 | Url 11 | https://api.tavily.com 12 | 13 | 14 | TavilyApi 15 | ExternalCredential 16 | Authentication 17 | 18 | SecuredEndpoint 19 | 20 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/SearchWeb/SearchWeb.genAiFunction-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | - Use this helper action whenever you need information that you dont have. 4 | - It leverage the Tavily API which does a natural language query to the web and return with a single natural language answer. 5 | SearchWeb 6 | SearchWeb 7 | apex 8 | false 9 | true 10 | SearchWeb 11 | 12 | 13 | output_answer 14 | answer 15 | output 16 | 17 | MyOrgButler: Search the Web 18 | 19 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/AnswerWithCurrentFile/AnswerWithCurrentFile.genAiFunction-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | - Use the content and metadata of the current file to answer. 4 | - Respond concisely—prefer TL;DR followed by detail when summarizing. 5 | - If the file doesn’t contain the answer, say so clearly. 6 | - Allow follow-up questions by reusing earlier context in rewritten user input. 7 | AnswerWithCurrentFile 8 | AnswerFromFile 9 | generatePromptResponse 10 | false 11 | true 12 | AnswerWithCurrentFile 13 | Answer questions with current file 14 | Digesting file content 15 | 16 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/ExploreOrgSchema/ExploreOrgSchema.genAiFunction-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | This action helps you explore Custom Objects, Fields, and Relationships in your org. Use the "summary" scope to discover available objects, "relationships" to see how an object connects to others, and "details" for a full list of fields and picklist values on a specific object. Only use "details" or "relationships" when you are sure the object exists. 4 | ExploreOrgSchema 5 | ExploreOrgSchema 6 | apex 7 | false 8 | true 9 | ExploreOrgSchema 10 | My Org Butler: Explore Org Schema 11 | 12 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/AnswerWithRelatedFiles/AnswerWithRelatedFiles.genAiFunction-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | - Use attachments from the Opportunity and its Account to answer. 4 | - Prioritize relevance and avoid overexplaining. 5 | - If no file contains the answer, respond transparently. 6 | - Allow follow-up questions by reusing earlier context in rewritten Input:userQuestion 7 | AnswerWithRelatedFiles 8 | AnswerFromRelatedFiles 9 | generatePromptResponse 10 | false 11 | true 12 | AnswerWithRelatedFiles 13 | Answer questions with related files 14 | Digesting content of files… 15 | 16 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/ExploreOrgSchema/input/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "required" : [ "scope" ], 3 | "unevaluatedProperties" : false, 4 | "properties" : { 5 | "objectFilter" : { 6 | "title" : "Object API Names", 7 | "description" : "Comma-separated list of object API names to investigate (used with 'details' and 'relationships' scopes).", 8 | "lightning:type" : "lightning__textType", 9 | "lightning:isPII" : false, 10 | "copilotAction:isUserInput" : false 11 | }, 12 | "namespaceFilter" : { 13 | "title" : "Namespace Filter", 14 | "description" : "Optional namespace prefix to limit objects by package", 15 | "lightning:type" : "lightning__textType", 16 | "lightning:isPII" : false, 17 | "copilotAction:isUserInput" : false 18 | }, 19 | "scope" : { 20 | "title" : "Scope", 21 | "description" : "summary, details, relationships", 22 | "lightning:type" : "lightning__textType", 23 | "lightning:isPII" : false, 24 | "copilotAction:isUserInput" : false 25 | } 26 | }, 27 | "lightning:type" : "lightning__objectType" 28 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Aquiva Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used for Git repositories to specify intentionally untracked files that Git should ignore. 2 | # If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore 3 | # For useful gitignore templates see: https://github.com/github/gitignore 4 | 5 | # Salesforce cache 6 | .sfdx/ 7 | .sf/ 8 | .localdevserver/ 9 | 10 | #Snyk cache 11 | .dccache 12 | 13 | # IDEs 14 | .idea/ 15 | .vscode/ 16 | .vim-force.com/ 17 | IlluminatedCloud/ 18 | projectFilesBackup/ 19 | # LWC VSCode autocomplete 20 | **/lwc/jsconfig.json 21 | 22 | # Logs 23 | logs 24 | *.log 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # Dependency directories 30 | node_modules/ 31 | 32 | # Eslint cache 33 | .eslintcache 34 | 35 | # MacOS system files 36 | .DS_Store 37 | 38 | # Windows system files 39 | Thumbs.db 40 | ehthumbs.db 41 | [Dd]esktop.ini 42 | $RECYCLE.BIN/ 43 | .prettierignore 44 | .prettierrc 45 | package-lock.json 46 | *.iml 47 | 48 | mdapi/ 49 | /.idea/ 50 | .pmdCache 51 | 52 | # Exclude the metadata retrieval staging area 53 | retrieval-staging/ 54 | 55 | # Environment variables 56 | .env -------------------------------------------------------------------------------- /force-app/main/default/classes/SearchWeb_Test.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class SearchWeb_Test { 3 | 4 | @IsTest 5 | private static void happyPath() { 6 | 7 | // Setup 8 | SearchWeb.Answer expected = answerWith('Donald Trump was elected in November 2024.'); 9 | new HttpMock() 10 | .post('/search').body(expected).statusCodeOk() 11 | .mock(); 12 | 13 | SearchWeb.Query query = new SearchWeb.Query(); 14 | query.content = 'Who is the 47th US president?'; 15 | 16 | // Exercise 17 | Test.startTest(); 18 | List response = SearchWeb.execute(new List{ query }); 19 | Test.stopTest(); 20 | 21 | // Verify 22 | Assert.areEqual(1, response.size()); 23 | SearchWeb.Answer actual = response[0]; 24 | Assert.areEqual(expected.answer, actual.answer); 25 | } 26 | 27 | // HELPER 28 | 29 | private static SearchWeb.Answer answerWith(String content) { 30 | SearchWeb.Answer result = new SearchWeb.Answer(); 31 | result.answer = content; 32 | return result; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/CallGitHubApi/input/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "required" : [ "httpMethod", "urlIncludingParams" ], 3 | "unevaluatedProperties" : false, 4 | "properties" : { 5 | "httpMethod" : { 6 | "title" : "HTTP Method", 7 | "description" : "The HTTP method to use: GET, POST, PUT, PATCH, DELETE", 8 | "lightning:type" : "lightning__textType", 9 | "lightning:isPII" : false, 10 | "copilotAction:isUserInput" : false 11 | }, 12 | "requestBody" : { 13 | "title" : "Request Body", 14 | "description" : "JSON payload for POST/PUT/PATCH requests. Required for creating/updating resources.", 15 | "lightning:type" : "lightning__textType", 16 | "lightning:isPII" : false, 17 | "copilotAction:isUserInput" : false 18 | }, 19 | "urlIncludingParams" : { 20 | "title" : "GitHub API Endpoint", 21 | "description" : "The GitHub API endpoint path (e.g., /repos/{owner}/{repo}/issues)", 22 | "lightning:type" : "lightning__textType", 23 | "lightning:isPII" : false, 24 | "copilotAction:isUserInput" : false 25 | } 26 | }, 27 | "lightning:type" : "lightning__objectType" 28 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/CallRestApi/input/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "required" : [ "httpMethod", "urlIncludingParams" ], 3 | "unevaluatedProperties" : false, 4 | "properties" : { 5 | "httpMethod" : { 6 | "title" : "HTTP Method", 7 | "description" : "The HTTP method to use: GET, POST, PUT, PATCH, DELETE", 8 | "lightning:type" : "lightning__textType", 9 | "lightning:isPII" : false, 10 | "copilotAction:isUserInput" : false 11 | }, 12 | "requestBody" : { 13 | "title" : "Request Body", 14 | "description" : "Payload data to be sent in the request body. Can be empty if there is no body.", 15 | "lightning:type" : "lightning__textType", 16 | "lightning:isPII" : false, 17 | "copilotAction:isUserInput" : false 18 | }, 19 | "urlIncludingParams" : { 20 | "title" : "Endpoint URL", 21 | "description" : "The URL fragment of the REST endpoint starting with /. The Orgs base URL will be added", 22 | "lightning:type" : "lightning__textType", 23 | "lightning:isPII" : false, 24 | "copilotAction:isUserInput" : false 25 | } 26 | }, 27 | "lightning:type" : "lightning__objectType" 28 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/AnswerWithRelatedFiles/input/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "required" : [ "Input:userQuestion", "Input:opportunity" ], 3 | "unevaluatedProperties" : false, 4 | "properties" : { 5 | "Input:userQuestion" : { 6 | "title" : "userQuestion", 7 | "description" : "This is the original question from the user.\\nYou may rewrite it slightly to include implicit context from a previous question and answer, especially if the user asks a follow-up or refers back to previous content (e.g., \\\"and what about pricing?\\\").\\nOnly do this when it helps clarify intent and improves the response.", 8 | "lightning:type" : "lightning__textType", 9 | "lightning:isPII" : false, 10 | "copilotAction:isUserInput" : false 11 | }, 12 | "Input:opportunity" : { 13 | "title" : "opportunity", 14 | "description" : "The currentRecord opportunity", 15 | "lightning:type" : "lightning__recordInfoType", 16 | "lightning:sObjectInfo" : { 17 | "apiName" : "Opportunity" 18 | }, 19 | "lightning:isPII" : false, 20 | "copilotAction:isUserInput" : false 21 | } 22 | }, 23 | "lightning:type" : "lightning__objectType" 24 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/StoreCustomInstruction/input/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "required" : [ "summary", "instruction", "isShared" ], 3 | "unevaluatedProperties" : false, 4 | "properties" : { 5 | "summary" : { 6 | "title" : "Summary", 7 | "description" : "Brief summary of the learned preference or behavior pattern (max 80 characters)", 8 | "lightning:type" : "lightning__textType", 9 | "lightning:isPII" : false, 10 | "copilotAction:isUserInput" : true 11 | }, 12 | "instruction" : { 13 | "title" : "Instruction", 14 | "description" : "Contextual instruction with situation trigger and specific guidance. Include context like 'When user asks about projects, query Salesforce data using...' (max 255 characters)", 15 | "lightning:type" : "lightning__textType", 16 | "lightning:isPII" : false, 17 | "copilotAction:isUserInput" : true 18 | }, 19 | "isShared" : { 20 | "title" : "Is Shared", 21 | "description" : "Set to true to share this instruction with other users. Optional.", 22 | "lightning:type" : "lightning__booleanType", 23 | "copilotAction:isUserInput" : true 24 | } 25 | }, 26 | "lightning:type" : "lightning__objectType" 27 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/CallMetadataApi_Test.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class CallMetadataApi_Test { 3 | 4 | @IsTest 5 | private static void happyPath() { 6 | 7 | // Setup 8 | Exception unexpectedException = null; 9 | mockResponse(); 10 | 11 | CallMetadataApi.MetadataDescription metadata = new CallMetadataApi.MetadataDescription(); 12 | metadata.soapAction = 'createMetadata'; 13 | metadata.typeName = 'RemoteSiteSetting'; 14 | metadata.attributesJson = '{ "fullName" : "Spiegel", "url" : "https://www.spiegel.de", "isActive" : true }'; 15 | 16 | // Exercise 17 | try { 18 | CallMetadataApi.execute(new List{ metadata }); 19 | } 20 | catch(Exception ex) { 21 | unexpectedException = ex; 22 | } 23 | 24 | 25 | // Verify 26 | Assert.isNull(unexpectedException); 27 | } 28 | 29 | // HELPER 30 | 31 | private static void mockResponse() { 32 | HttpResponse response = new HttpResponse(); 33 | response.setStatusCode(200); 34 | response.setBody('asdf'); 35 | 36 | CallMetadataApi.mockedResponse = response; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/CreatePlantUmlUrl/CreatePlantUmlUrl.genAiFunction-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | - Use this action to convert PlantUML diagram text into an image URL 4 | - This action does not invent the diagram text but only encoded it into an URL 5 | - Render as an HTML A link with target="_blank" to the image. 6 | - The URL encoding this action uses only works with smaller diagrams. If you get passed a large diagram feel free to reduce its size by removing irrelevant detail like redundant or overly detailed field lists. 7 | - Maximum URL length: 2048 characters 8 | - Syntax must start with @startuml and end with @enduml 9 | - Use UTF-8 encoding for special characters 10 | CreatePlantUmlUrl 11 | CreatePlantUmlUrl 12 | apex 13 | false 14 | true 15 | CreatePlantUmlUrl 16 | MyOrgButler: Create PlantUML URL 17 | 18 | -------------------------------------------------------------------------------- /force-app/main/default/classes/CreatePlantUmlUrl.cls: -------------------------------------------------------------------------------- 1 | // PMD False Positives: Agentforce requires Global 2 | @SuppressWarnings('PMD.AvoidGlobalModifier') 3 | global with sharing class CreatePlantUmlUrl { 4 | private static final String PLANTUML_URL_PREFIX = 'http://www.plantuml.com/plantuml/png/~h'; 5 | 6 | @InvocableMethod(label='MyOrgButler: Render PlantUML Diagram' description='Converts PlantUML text notation into a rendered diagram image URL') 7 | global static List execute(List inputs) { 8 | Output output = new Output(); 9 | 10 | Blob textBlob = Blob.valueOf(inputs[0].plantUmlText); 11 | output.encodedImageUrl = PLANTUML_URL_PREFIX + EncodingUtil.convertToHex(textBlob).toLowerCase(); 12 | 13 | return new List{ output }; 14 | } 15 | 16 | // INNER 17 | 18 | global class Input { 19 | @InvocableVariable(label='PlantUML Text' description='The PlantUML diagram definition in text format (e.g., @startuml\\nAlice->Bob : hello\\n@enduml)' required=true) 20 | global String plantUmlText; 21 | } 22 | 23 | global class Output { 24 | @InvocableVariable(label='Diagram Image URL' description='URL to the rendered PlantUML diagram image' required=true) 25 | global String encodedImageUrl; 26 | } 27 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/CallRestApi/CallRestApi.genAiFunction-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Use this action to create or modify SObject records in the org. The Action is very generic so that also other APIs that the Salesforce REST API could be called. 4 | 5 | **IMPORTANT: Do NOT use this action for querying/reading data! Always use QueryRecordsWithSoql for any SOQL queries instead.** 6 | 7 | This action is intended for: 8 | - Creating new records (POST requests) 9 | - Updating existing records (PATCH requests) 10 | - Deleting records (DELETE requests) 11 | - Other REST API operations that modify data 12 | 13 | For querying data, always use the dedicated QueryRecordsWithSoql action which handles both regular queries and aggregate queries properly. 14 | CallRestApi 15 | CallRestApi 16 | apex 17 | false 18 | true 19 | CallRestApi 20 | MyOrgButler: Call REST API 21 | 22 | -------------------------------------------------------------------------------- /force-app/main/default/namedCredentials/VectorizeApi.namedCredential-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | true 5 | Enabled 6 | true 7 | 8 | 9 | Url 10 | Url 11 | https://api.vectorize.io 12 | 13 | 14 | VectorizeApi 15 | ExternalCredential 16 | Authentication 17 | 18 | 19 | Authorization 20 | HttpHeader 21 | {!$Credential.VectorizeApi.Token} 22 | 1 23 | 24 | SecuredEndpoint 25 | 26 | -------------------------------------------------------------------------------- /force-app/main/default/classes/LoadCustomInstructions_Test.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class LoadCustomInstructions_Test { 3 | 4 | @IsTest 5 | private static void execute() { 6 | 7 | // Setup 8 | insert new Memory__c(Content__c = 'Test shared preference', IsShared__c = true); 9 | insert new Memory__c(Content__c = 'Test user preference', IsShared__c = false); 10 | 11 | // Exercise 12 | List response = LoadCustomInstructions.execute(new List()); 13 | 14 | // Verify 15 | Assert.isNotNull(response); 16 | Assert.areEqual(1, response.size()); 17 | 18 | LoadCustomInstructions.CustomInstructions output = response[0]; 19 | Assert.isNotNull(output.customInstructions); 20 | Assert.isTrue(output.customInstructions.contains(System.UserInfo.getUserId())); 21 | Assert.isTrue(output.customInstructions.contains(System.UserInfo.getFirstName())); 22 | Assert.isTrue(output.customInstructions.contains(System.UserInfo.getLastName())); 23 | Assert.isTrue(output.customInstructions.contains(System.UserInfo.getOrganizationId())); 24 | Assert.isTrue(output.customInstructions.contains(URL.getOrgDomainUrl().toExternalForm())); 25 | Assert.isTrue(output.customInstructions.contains('- Test shared preference')); 26 | Assert.isTrue(output.customInstructions.contains('- Test user preference')); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/AnswerWithCurrentFile/input/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "required" : [ "Input:file", "Input:userQuestion" ], 3 | "unevaluatedProperties" : false, 4 | "properties" : { 5 | "Input:file" : { 6 | "title" : "file", 7 | "description" : "the current file/attachment", 8 | "lightning:type" : "lightning__recordInfoType", 9 | "lightning:sObjectInfo" : { 10 | "apiName" : "ContentDocument" 11 | }, 12 | "lightning:isPII" : false, 13 | "copilotAction:isUserInput" : false 14 | }, 15 | "Input:userQuestion" : { 16 | "title" : "userQuestion", 17 | "description" : "This is the original question from the user.\nYou may rewrite it slightly to include implicit context from a previous question and answer, especially if the user asks a follow-up or refers back to previous content (e.g., \"and what about pricing?\").\nOnly do this when it helps clarify intent and improves the response.\n", 18 | "lightning:type" : "lightning__textType", 19 | "lightning:isPII" : false, 20 | "copilotAction:isUserInput" : false 21 | }, 22 | "outputLanguage" : { 23 | "title" : "Prompt Operation output language", 24 | "description" : "Select Prompt Operation output language", 25 | "lightning:type" : "lightning__textType", 26 | "lightning:isPII" : false, 27 | "copilotAction:isUserInput" : false 28 | } 29 | }, 30 | "lightning:type" : "lightning__objectType" 31 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/CallToolingApi/CallToolingApi.genAiFunction-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | - Use this Action to read and create Metadata that can be handled by the Tooling API 4 | - When in doubt or the action returns an error use the SearchWeb action to find out which API support that type or activity. 5 | 6 | Supported Actions: 7 | - query: SOQL queries against Tooling API objects 8 | - create: Single record creation 9 | - update: Modify existing records 10 | - delete: Remove development artifacts 11 | 12 | Key Object Types: 13 | - ApexClass: code, status, body, symbols 14 | - CustomField: metadata, relationships, validation 15 | - CustomObject: fields, validation rules, layouts 16 | - ValidationRule: errorMessage, active, metadata 17 | - ApexTrigger: code, status, usageBeforeUpdate 18 | - Layout: layout items, buttons, related lists 19 | - WorkflowRule: criteria, actions, timeTriggered 20 | CallToolingApi 21 | CallToolingApi 22 | apex 23 | false 24 | false 25 | CallToolingApi 26 | MyOrgButler: Call Tooling API 27 | 28 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/CallMetadataApi/input/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "required" : [ "soapAction" ], 3 | "unevaluatedProperties" : false, 4 | "properties" : { 5 | "attributesJson" : { 6 | "title" : "Attributes JSON", 7 | "description" : "A JSON with all relevant attributes needed to create this metadata type.", 8 | "lightning:type" : "lightning__textType", 9 | "lightning:isPII" : false, 10 | "copilotAction:isUserInput" : false 11 | }, 12 | "fullName" : { 13 | "title" : "Metadata Full Name", 14 | "description" : "Some Actions need a Full Name of the existing Metadata record", 15 | "lightning:type" : "lightning__textType", 16 | "lightning:isPII" : false, 17 | "copilotAction:isUserInput" : false 18 | }, 19 | "soapAction" : { 20 | "title" : "SOAP Action", 21 | "description" : "One of the allowed SOAP Action types: describeMetadata,listMetadata,readMetadata,createMetadata,updateMetadata,upsertMetadata,deleteMetadata", 22 | "lightning:type" : "lightning__textType", 23 | "lightning:isPII" : false, 24 | "copilotAction:isUserInput" : false 25 | }, 26 | "typeName" : { 27 | "title" : "Metadata Type Name", 28 | "description" : "The official Salesforce Metadata Type Name", 29 | "lightning:type" : "lightning__textType", 30 | "lightning:isPII" : false, 31 | "copilotAction:isUserInput" : false 32 | } 33 | }, 34 | "lightning:type" : "lightning__objectType" 35 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/NightlyMemoryConsolidation.cls: -------------------------------------------------------------------------------- 1 | // PMD False Positives: Package Subscribers need to schedule this class 2 | @SuppressWarnings('PMD.AvoidGlobalModifier') 3 | global with sharing class NightlyMemoryConsolidation implements Database.Batchable, Schedulable { 4 | 5 | private static final String CONSOLIDATED_NAME = 'CONSOLIDATED_MEMORY'; 6 | 7 | // GLOBAL 8 | 9 | global void execute(SchedulableContext context) { 10 | Database.executeBatch(new NightlyMemoryConsolidation(), 1); 11 | } 12 | 13 | // PUBLIC 14 | 15 | public Iterable start(Database.BatchableContext context) { 16 | Set usersWithUnconsolidatedMemory = new Set(); 17 | 18 | for(Memory__c memory : [SELECT CreatedById FROM Memory__c 19 | WHERE Name != :CONSOLIDATED_NAME AND IsShared__c = false 20 | WITH SYSTEM_MODE]) { 21 | usersWithUnconsolidatedMemory.add(memory.CreatedById); 22 | } 23 | 24 | return usersWithUnconsolidatedMemory; 25 | } 26 | 27 | public void execute(Database.BatchableContext context, List userIds) { 28 | for(Id userId : userIds) { 29 | new AgentMemory(userId).consolidate(); 30 | } 31 | } 32 | 33 | public void finish(Database.BatchableContext context) { 34 | // Consolidate shared memories in one pass 35 | new AgentMemory(null).consolidate(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /scripts/create-package-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source `dirname $0`/config.sh 3 | 4 | execute() { 5 | $@ || exit 6 | } 7 | 8 | echo "Set default devhub user" 9 | execute sf config set target-dev-hub=$DEV_HUB_ALIAS 10 | 11 | echo "List existing package versions" 12 | sf package version list -p "$PACKAGE_NAME" --concise 13 | 14 | echo "Create new package version" 15 | PACKAGE_VERSION_OUTPUT=$(sf package version create -p "$PACKAGE_NAME" --installation-key-bypass --wait 40 --code-coverage -f config/project-scratch-def.json --json) 16 | if [ $? -ne 0 ]; then 17 | echo "Error: Failed to create package version" 18 | echo "$PACKAGE_VERSION_OUTPUT" 19 | exit 1 20 | fi 21 | 22 | PACKAGE_VERSION=$(echo "$PACKAGE_VERSION_OUTPUT" | jq -r '.result.SubscriberPackageVersionId // empty') 23 | if [ -z "$PACKAGE_VERSION" ] || [ "$PACKAGE_VERSION" = "null" ]; then 24 | echo "Error: Failed to extract package version ID" 25 | echo "Output: $PACKAGE_VERSION_OUTPUT" 26 | exit 1 27 | fi 28 | 29 | echo "Promote package version $PACKAGE_VERSION" 30 | execute sf package version promote -p "$PACKAGE_VERSION" -n 31 | 32 | echo "Install from: /packaging/installPackage.apexp?p0=$PACKAGE_VERSION" 33 | 34 | if [ -n "$QA_ORG_ALIAS" ]; then 35 | if [ -n "$QA_ORG_URL" ]; then 36 | echo "Authenticate QA Org" 37 | echo "$QA_ORG_URL" | sf org login sfdx-url --sfdx-url-stdin -a "$QA_ORG_ALIAS" 38 | fi 39 | 40 | echo "Install in QA Org" 41 | execute sf package install -p "$PACKAGE_VERSION" -o "$QA_ORG_ALIAS" -b 30 -w 40 -r 42 | fi -------------------------------------------------------------------------------- /force-app/main/default/classes/SearchWeb.cls: -------------------------------------------------------------------------------- 1 | // PMD False Positives: Agentforce requires Global 2 | @SuppressWarnings('PMD.AvoidGlobalModifier') 3 | global with sharing class SearchWeb { 4 | 5 | @InvocableMethod(label='MyOrgButler: Search Web' description='Performs a web search using the Tavily API and returns the answer.') 6 | global static List execute(List queries) { 7 | HttpRequest request = new HttpRequest(); 8 | request.setEndpoint('callout:aquiva_os__TavilyApi/search'); 9 | request.setMethod('POST'); 10 | request.setHeader('Content-Type', 'application/json'); 11 | 12 | Map body = new Map{ 13 | 'query' => queries[0].content, 14 | 'api_key' => '{!$Credential.TavilyApi.ApiKey}', 15 | 'include_answer' => true 16 | }; 17 | request.setBody( JSON.serialize(body) ); 18 | 19 | HttpResponse response = new Http().send(request); 20 | Answer answer = (Answer) JSON.deserialize(response.getBody(), Answer.class); 21 | return new List{ answer }; 22 | 23 | } 24 | 25 | // INNER 26 | 27 | global class Query { 28 | @InvocableVariable(label='Query' description='A natural language search query to search the web for and get an answer for.') 29 | global String content; 30 | } 31 | 32 | global class Answer { 33 | @InvocableVariable(label='Answer' description='The Natural Language Answer for the Query generated after doing a Web Search.') 34 | global String answer; 35 | } 36 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/CallToolingApi/input/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "required" : [ "action" ], 3 | "unevaluatedProperties" : false, 4 | "properties" : { 5 | "action" : { 6 | "title" : "Action", 7 | "description" : "The Tooling API action to perform: query, create, update, delete", 8 | "lightning:type" : "lightning__textType", 9 | "lightning:isPII" : false, 10 | "copilotAction:isUserInput" : false 11 | }, 12 | "objectType" : { 13 | "title" : "Object Type", 14 | "description" : "The Tooling API object type (e.g., ApexClass, CustomField)", 15 | "lightning:type" : "lightning__textType", 16 | "lightning:isPII" : false, 17 | "copilotAction:isUserInput" : false 18 | }, 19 | "recordId" : { 20 | "title" : "Record Id", 21 | "description" : "Id of the record to update or delete", 22 | "lightning:type" : "lightning__textType", 23 | "lightning:isPII" : false, 24 | "copilotAction:isUserInput" : false 25 | }, 26 | "recordJson" : { 27 | "title" : "Record JSON", 28 | "description" : "JSON representation of the record to create or update", 29 | "lightning:type" : "lightning__textType", 30 | "lightning:isPII" : false, 31 | "copilotAction:isUserInput" : false 32 | }, 33 | "soql" : { 34 | "title" : "SOQL Query", 35 | "description" : "The SOQL query to execute (for query action)", 36 | "lightning:type" : "lightning__textType", 37 | "lightning:isPII" : false, 38 | "copilotAction:isUserInput" : false 39 | } 40 | }, 41 | "lightning:type" : "lightning__objectType" 42 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/StoreCustomInstruction.cls: -------------------------------------------------------------------------------- 1 | // PMD False Positives: Agentforce requires Global 2 | @SuppressWarnings('PMD.AvoidGlobalModifier') 3 | global with sharing class StoreCustomInstruction { 4 | 5 | @InvocableMethod(label='MyOrgButler: Store Custom Instruction' description='Updates user context with learned preferences and instructions for future AI agent interactions.') 6 | global static List execute(List input) { 7 | Memory__c memory = new Memory__c(); 8 | memory.Name = input[0].summary.left(80); 9 | memory.Content__c = input[0].instruction.left(255); 10 | if(input[0].isShared != null) { 11 | memory.IsShared__c = input[0].isShared; 12 | } 13 | 14 | insert as system memory; 15 | 16 | // Note: Return updated user info including the new memory 17 | return LoadCustomInstructions.execute(new List()); 18 | } 19 | 20 | // INNER 21 | 22 | global class Input { 23 | @InvocableVariable(label='Summary' description='Brief summary of the learned preference or behavior (max 80 characters)') 24 | global String summary; 25 | 26 | @InvocableVariable(label='Instruction' description='Contextual instruction for future behavior. Include the context (e.g. "When user asks about projects, query Salesforce data using...") and specific guidance (max 255 characters)') 27 | global String instruction; 28 | 29 | @InvocableVariable(label='Is Shared' description='Set to true to share this memory with other users in the org.' required='false') 30 | global Boolean isShared = false; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /force-app/main/default/classes/QueryRecordsWithSoql.cls: -------------------------------------------------------------------------------- 1 | // PMD False Positives: Agentforce requires Global 2 | @SuppressWarnings('PMD.AvoidGlobalModifier') 3 | global with sharing class QueryRecordsWithSoql { 4 | 5 | @InvocableMethod(label='MyOrgButler: Run SOQL Queries' description='Run a single SOQL Query and return a list of records or errors') 6 | global static List execute(List input) { 7 | Output output = new Output(); 8 | 9 | try { 10 | List result = Database.query(input[0].dynamicSoqlQuery, AccessLevel.USER_MODE); 11 | 12 | String jsonResult = JSON.serialize(result); 13 | String cleanJson = jsonResult.replaceAll('(\\"url\\":\\")/services/data/v[0-9.]+/sobjects/[^/]+/', '$1/'); 14 | 15 | output.resultOrException = cleanJson; 16 | } 17 | catch(Exception ex) { 18 | Map exceptionInfo = new Map{ 19 | 'message' => ex.getMessage(), 20 | 'typeName' => ex.getTypeName(), 21 | 'stackTrace' => ex.getStackTraceString() 22 | }; 23 | output.resultOrException = JSON.serialize(exceptionInfo); 24 | } 25 | 26 | return new List{ output }; 27 | } 28 | 29 | // INNER 30 | 31 | global class Input { 32 | @InvocableVariable(label='SOQL Query' description='Query to be executed as Dynamic SOQL' required=true) 33 | global String dynamicSoqlQuery; 34 | } 35 | 36 | global class Output { 37 | @InvocableVariable(label='Result or Exception JSON' description='JSON array of result records or aggregate results on success, or exception object on error') 38 | global String resultOrException; 39 | } 40 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/StoreCustomInstruction_Test.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class StoreCustomInstruction_Test { 3 | 4 | @IsTest 5 | private static void execute() { 6 | 7 | // Exercise 8 | StoreCustomInstruction.Input input = new StoreCustomInstruction.Input(); 9 | input.summary = 'User prefers tables over lists'; 10 | input.instruction = 'When user asks about projects, query Salesforce data and format results as tables instead of bullet lists for better readability'; 11 | 12 | List response = StoreCustomInstruction.execute(new List{ input }); 13 | 14 | // Verify 15 | Assert.isNotNull(response); 16 | Assert.areEqual(1, response.size()); 17 | 18 | LoadCustomInstructions.CustomInstructions output = response[0]; 19 | Assert.isNotNull(output.customInstructions); 20 | Assert.isTrue(output.customInstructions.contains('When user asks about projects')); 21 | } 22 | 23 | 24 | @IsTest 25 | private static void executeShared() { 26 | 27 | // Exercise 28 | StoreCustomInstruction.Input input = new StoreCustomInstruction.Input(); 29 | input.summary = 'User prefers tables over lists'; 30 | input.instruction = 'When user asks about projects, query Salesforce data and format results as tables instead of bullet lists for better readability'; 31 | input.isShared = true; 32 | 33 | StoreCustomInstruction.execute(new List{ input }); 34 | 35 | 36 | // Verify 37 | Memory__c memory = [SELECT IsShared__c FROM Memory__c LIMIT 1]; 38 | Assert.areEqual(true, memory.IsShared__c); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /force-app/main/default/classes/CallGitHubApi.cls: -------------------------------------------------------------------------------- 1 | // Note: Agentforce requires Global 2 | @SuppressWarnings('PMD.AvoidGlobalModifier') 3 | global with sharing class CallGitHubApi { 4 | 5 | @InvocableMethod(label='MyOrgButler: Call GitHub API' description='Call the GitHub API using the GitHub Named Credential') 6 | global static List execute(List requests) { 7 | Input req = requests[0]; 8 | HttpRequest request = new HttpRequest(); 9 | request.setEndpoint('callout:aquiva_os__GitHubApi' + req.urlIncludingParams); 10 | request.setMethod(req.httpMethod); 11 | request.setHeader('Content-Type', 'application/json'); 12 | String body = req.requestBody; 13 | if (String.isNotEmpty(body)) { 14 | request.setBody(body); 15 | } 16 | HttpResponse response = new Http().send(request); 17 | return new List{ new Output(response.getBody()) }; 18 | } 19 | 20 | global class Input { 21 | @InvocableVariable(label='Endpoint URL' description='The GitHub API endpoint URL to call, starting with / and including any query parameters' required=true) 22 | global String urlIncludingParams; 23 | 24 | @InvocableVariable(label='HTTP Method' description='The HTTP method to use (GET, POST, PATCH, DELETE)' required=true) 25 | global String httpMethod; 26 | 27 | @InvocableVariable(label='Request Body' description='JSON payload for POST or PATCH requests' required=false) 28 | global String requestBody; 29 | } 30 | 31 | global class Output { 32 | @InvocableVariable( 33 | label='Response Body' 34 | description='Raw HTTP Response to be interpreted for Errors or Success' 35 | required=true 36 | ) 37 | global String responseBody; 38 | 39 | public Output(String body) { 40 | responseBody = body; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /scripts/create-scratch-org.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source `dirname $0`/config.sh 3 | 4 | execute() { 5 | $@ || exit 6 | } 7 | 8 | echo "Updating tools" 9 | npm install --global @salesforce/cli 10 | sf plugins update 11 | 12 | if [ -z "$DEV_HUB_URL" ]; then 13 | echo "set default devhub user" 14 | execute sf config set target-dev-hub=$DEV_HUB_ALIAS 15 | 16 | echo "Deleting old scratch org" 17 | sf org delete scratch --no-prompt --target-org $SCRATCH_ORG_ALIAS 18 | fi 19 | 20 | echo "Creating scratch org" 21 | execute sf org create scratch --alias $SCRATCH_ORG_ALIAS --set-default --definition-file ./config/project-scratch-def.json --duration-days 30 22 | 23 | echo "Make sure Org user is english" 24 | sf data update record --sobject User --where "Name='User User'" --values "Languagelocalekey=en_US" 25 | 26 | echo "Enabling Prompt Builder" 27 | execute sf org assign permset --name EinsteinGPTPromptTemplateManager --name AgentPlatformBuilder 28 | 29 | echo "Installing dependencies" 30 | execute sf package install --package "app-foundations@LATEST" --publish-wait 3 --wait 10 31 | 32 | echo "Pushing changes to scratch org" 33 | execute sf project deploy start --source-dir force-app 34 | 35 | echo "Running Tests" 36 | sf apex run test --test-level RunLocalTests --wait 30 --code-coverage --result-format human 37 | #sf agent test run --api-name RegressionSuite --wait 10 38 | 39 | echo "Pushing unpackaged changes to scratch org" 40 | execute sf project deploy start --source-dir unpackaged 41 | 42 | echo "Assigning permissions" 43 | execute sf org assign permset --name MyOrgButlerUser --name AgentAccess 44 | 45 | echo "Running SFX Scanner with Security, AppExchange and Coding Standards" 46 | sf code-analyzer run --rule-selector Recommended:Security, AppExchange --output-file code-analyzer-security.csv 47 | sf code-analyzer run --rule-selector PMD:OpinionatedSalesforce --output-file code-analyzer-cleancode.csv --target force-app/main/default -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/LoadCustomInstructions/LoadCustomInstructions.genAiFunction-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | - Use this to load user context information and stored custom instructions 4 | - Returns user details (ID, name, email, language, timezone), organization information (ID, base URL), and current date/time 5 | - Also returns any custom instructions the user has asked you to remember from previous conversations 6 | - These stored instructions include user preferences, corrections, specific ways they want you to behave, AND crucially how to use tools and when to use them 7 | - Custom instructions are essential for proper tool usage, communication style, and operational preferences 8 | - Use this information to personalize responses, follow the user's preferred communication style, and apply their specific tool usage patterns 9 | - User and time information is needed for creating user-filtered queries or handling relative time expressions like "last week" or "my accounts" 10 | - The Org Base URL is needed when constructing links to records or pages 11 | LoadCustomInstructions 12 | LoadCustomInstructions 13 | apex 14 | false 15 | true 16 | LoadCustomInstructions 17 | 18 | 19 | output_customInstructions 20 | customInstructions 21 | output 22 | 23 | MyOrgButler: Load Custom Instructions 24 | 25 | -------------------------------------------------------------------------------- /force-app/main/default/classes/RetrieveVectorizeChunks_Test.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class RetrieveVectorizeChunks_Test { 3 | 4 | @TestSetup 5 | static void setup() { 6 | CustomSettings.mock('VectorizeOrgId', 'ORG'); 7 | CustomSettings.mock('VectorizePipelineId', 'PIPELINE'); 8 | } 9 | 10 | @IsTest 11 | private static void execute() { 12 | 13 | // Setup 14 | new HttpMock() 15 | .post('/v1/org/ORG/pipelines/PIPELINE/retrieval') 16 | .body('{"documents": []}') 17 | .mock(); 18 | 19 | RetrieveVectorizeChunks.Question question = new RetrieveVectorizeChunks.Question('What is the company policy?'); 20 | 21 | // Exercise 22 | Test.startTest(); 23 | List results = RetrieveVectorizeChunks.execute(new List{ question }); 24 | Test.stopTest(); 25 | 26 | // Verify 27 | Assert.areEqual(1, results.size()); 28 | Assert.isNotNull(results[0].content); 29 | Assert.isTrue(results[0].content.contains('Mock answer for testing')); 30 | } 31 | 32 | @IsTest 33 | private static void apiError() { 34 | 35 | // Setup 36 | ApplicationException expectedException = null; 37 | 38 | new HttpMock() 39 | .post('/v1/org/ORG/pipelines/PIPELINE/retrieval') 40 | .statusCode(500) 41 | .body('Internal Server Error') 42 | .mock(); 43 | 44 | RetrieveVectorizeChunks.Question question = new RetrieveVectorizeChunks.Question('What is the company policy?'); 45 | 46 | // Exercise 47 | Test.startTest(); 48 | try { 49 | RetrieveVectorizeChunks.execute(new List{ question }); 50 | } 51 | catch(ApplicationException ex) { 52 | expectedException = ex; 53 | } 54 | Test.stopTest(); 55 | 56 | 57 | // Verify 58 | Assert.isNotNull(expectedException); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/StoreCustomInstruction/StoreCustomInstruction.genAiFunction-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | - Use this when the user corrects you, expresses dissatisfaction with your response, or asks you to remember something for future conversations 4 | - Store user preferences, corrections, and specific instructions about how they want you to behave, communicate, AND use tools 5 | - Custom instructions cover everything: communication style, tool usage patterns, when to use specific tools, operational preferences 6 | - Examples: user wants tables instead of lists, prefers more technical detail, wants specific formatting, dislikes certain approaches, prefers specific tools for certain tasks 7 | - The summary should briefly describe what to remember (max 80 characters): "User prefers tables over lists" 8 | - The instruction should specify when and how to apply this: "When showing data results, format as tables instead of bullet points" 9 | - After storing, acknowledge: "I'll remember that preference for future interactions" and immediately apply the new instruction 10 | - This creates persistent memory that improves your responses in all future conversations with this user 11 | StoreCustomInstruction 12 | StoreCustomInstruction 13 | apex 14 | false 15 | true 16 | StoreCustomInstruction 17 | 18 | 19 | output_customInstructions 20 | customInstructions 21 | output 22 | 23 | MyOrgButler: Store Custom Instruction 24 | 25 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/SearchVectorDatabase/SearchVectorDatabase.genAiFunction-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | - Use this function to search your company's internal knowledge base and get accurate answers based on internal documents and policies. 4 | - This function searches through vectorized company documents, policies, procedures, and other internal knowledge sources. 5 | - It returns AI-generated answers that are grounded in your organization's actual documentation. 6 | - Use this when you need information from internal company sources rather than the public web. 7 | 8 | IMPORTANT: When calling this function, do NOT simply pass the user's last utterance as the question. Instead: 9 | 1. Analyze the entire conversation context and previous exchanges 10 | 2. Reformulate the question to be self-contained and contextually complete 11 | 3. Include relevant background information from the conversation history 12 | 4. Ensure the question clearly states what information is being sought 13 | 5. Make the question specific enough for effective vector search 14 | 15 | For example: 16 | - If user asks "What about remote work?" after discussing employee policies, reformulate to "What are the company's remote work policies and procedures?" 17 | - If user asks "How do I do that?" after discussing expense reports, reformulate to "What is the process for submitting expense reports?" 18 | - Always provide context so the search can find the most relevant internal documents. 19 | SearchVectorDatabase 20 | RetrieveVectorizeChunks 21 | apex 22 | false 23 | true 24 | SearchVectorDatabase 25 | MyOrgButler: Search Vector Database 26 | 27 | -------------------------------------------------------------------------------- /force-app/main/default/genAiFunctions/QueryRecordsWithSoql/QueryRecordsWithSoql.genAiFunction-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | - Use this action to query records from Standard and Custom objects. 4 | - The action takes a SOQL query string and returns JSON containing either: 5 | * On success: JSON array of result records (for regular queries) or aggregate results (for GROUP BY queries) 6 | * On error: JSON object representing the exception with message, type, and stack trace 7 | - Handle both regular SOQL queries (SELECT fields FROM object) and aggregate queries (SELECT COUNT(), SUM(), etc. with GROUP BY) 8 | - If the query is invalid, parse the exception JSON to understand the error and retry with a corrected query 9 | - Use the error details to fix the SOQL query and retry a few times before returning an error to the user 10 | - If you cannot fix the issue on your own, you can use the SearchWeb action to search for possible remedies 11 | - Its ok to create SOQL queries for well known Salesforce Standard objects. But never guess the names of Custom Objects and fields. Always use the Explore_Org_Schema tool to find out the right object, field and relationship names. 12 | 13 | Output Formatting: 14 | - The raw JSON output is not shown to users - you must parse and format it appropriately 15 | - Present data in tables, lists, or summaries as appropriate for the context 16 | - Compress large datasets by showing key highlights, top results, or summaries 17 | - Include record links when displaying individual records 18 | - For aggregate queries, explain what the numbers represent 19 | - If there are errors, explain them in user-friendly language 20 | QueryRecordsWithSoql 21 | QueryRecordsWithSoql 22 | apex 23 | false 24 | true 25 | QueryRecordsWithSoql 26 | MyOrgButler: Query Records with SOQL 27 | Querying records now. 28 | 29 | -------------------------------------------------------------------------------- /force-app/main/default/classes/CallRestApi.cls: -------------------------------------------------------------------------------- 1 | // PMD False Positives: 2 | // - Calling regular Salesforce APIs can be done with Session Id 3 | // - Agentforce requires Global 4 | @SuppressWarnings('PMD.AvoidGlobalModifier,PMD.ApexSuggestUsingNamedCred') 5 | global with sharing class CallRestApi { 6 | 7 | @InvocableMethod(label='MyOrgButler: Call Salesforce API' description='Call an official public Salesforce API REST endpoint') 8 | global static List execute(List input) { 9 | HttpRequest request = new HttpRequest(); 10 | request.setHeader('Authorization', 'Bearer ' + new SessionId().asString()); 11 | request.setHeader('Content-Type', 'application/json'); 12 | request.setEndpoint(Url.getOrgDomainURL().toExternalForm() + input[0].urlIncludingParams); 13 | request.setMethod(input[0].httpMethod); 14 | 15 | String body = input[0].requestBody; 16 | if(String.isNotEmpty(body)) { 17 | request.setBody(body); 18 | } 19 | 20 | HttpResponse response = new Http().send(request); 21 | return new List{ new Output(response.getBody()) }; 22 | } 23 | 24 | // INNER 25 | 26 | global class Input { 27 | @InvocableVariable( 28 | label='Endpoint URL' 29 | description='The URL fragment of the REST endpoint starting with /. The Orgs base URL will be added' 30 | required=true) 31 | global String urlIncludingParams; 32 | 33 | @InvocableVariable( 34 | label='HTTP Method' 35 | description='The HTTP method to use' 36 | required=true) 37 | global String httpMethod; 38 | 39 | @InvocableVariable( 40 | label='Request Body' 41 | description='Payload data to be sent in the request body. Can be null if there is no body.' 42 | required=false) 43 | global String requestBody; 44 | } 45 | 46 | global class Output { 47 | @InvocableVariable( 48 | label='Response Body' 49 | description='Raw HTTP Response to be interpreted for Errors or Success' 50 | required=true) 51 | global String responseBody; 52 | 53 | public Output(String body) { 54 | responseBody = body; 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/LoadCustomInstructions.cls: -------------------------------------------------------------------------------- 1 | // PMD False Positives: Agentforce requires Global 2 | @SuppressWarnings('PMD.AvoidGlobalModifier') 3 | global with sharing class LoadCustomInstructions { 4 | 5 | @InvocableMethod(label='MyOrgButler: Load Custom Instructions' description='Provides a formatted string with user context information including user details, time, date, and org information for AI agents.') 6 | global static List execute(List ignored) { 7 | return new List{ getCustomInstructions() }; 8 | } 9 | 10 | // PRIVATE 11 | 12 | private static CustomInstructions getCustomInstructions() { 13 | List lines = new List(); 14 | lines.add('\n### Basic User Info'); 15 | lines.add('- User Record Id: ' + System.UserInfo.getUserId()); 16 | lines.add('- First name: ' + System.UserInfo.getFirstName()); 17 | lines.add('- Last name: ' + System.UserInfo.getLastName()); 18 | lines.add('- Email address: ' + System.UserInfo.getUserEmail()); 19 | lines.add('- Language: ' + System.UserInfo.getLanguage()); 20 | lines.add('- Timezone in this org: ' + System.UserInfo.getTimeZone().getID()); 21 | lines.add('- Current date and time: ' + System.now().format('E, dd MMM yyyy HH:mm:ss z')); 22 | lines.add('- Base URL for constructing links in this org: ' + URL.getOrgDomainUrl().toExternalForm()); 23 | lines.add('- Salesforce organization ID: ' + System.UserInfo.getOrganizationId()); 24 | 25 | for (Memory__c memory : [SELECT Content__c FROM Memory__c 26 | WHERE IsShared__c = true OR CreatedById = :System.UserInfo.getUserId() 27 | WITH SYSTEM_MODE 28 | ORDER BY IsShared__c DESC, CreatedDate DESC 29 | ]) { 30 | lines.add('- ' + memory.Content__c.trim()); 31 | } 32 | 33 | CustomInstructions result = new CustomInstructions(); 34 | result.customInstructions = String.join(lines, '\n'); 35 | return result; 36 | } 37 | 38 | // INNER 39 | 40 | global class CustomInstructions { 41 | @InvocableVariable(label='Custom Instructions' description='Formatted string containing all user context information for AI agents.') 42 | global String customInstructions; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /force-app/main/default/genAiPromptTemplates/ConsolidateMemory.genAiPromptTemplate-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | CNSyYi7XBrJXfPt263fnNSJ26Xsc+rcfRzrE+DtAFyo=_2 4 | Merge all new memory entries into a single, compact summary—removing duplicates, resolving conflicts (keeping later entries), and packing related details together. 5 | ConsolidateMemory 6 | Consolidate Memory 7 | 8 | You are an intelligent assistant that merges memories into a structured summary. 9 | 10 | Current consolidated memory (may be empty): 11 | --- 12 | {!$Input:consolidatedMemory} 13 | --- 14 | 15 | New memory entries since last consolidation: 16 | --- 17 | {!$Input:newMemory} 18 | --- 19 | 20 | Create a consolidated memory that: 21 | 1. Merges existing and new memories into one coherent text 22 | 2. Organizes into 2-4 clear sections with simple headings 23 | 3. Uses bullet points or concise sentences 24 | 4. Removes duplicates and keeps latest versions of conflicting information 25 | 5. Is compact while preserving important facts 26 | 27 | IMPORTANT: Return ONLY the consolidated memory content. Do NOT include any meta-instructions, rules, or phrases like "Memory Consolidation Rules" in your response. 28 | 29 | 30 | consolidatedMemory 31 | primitive://String 32 | consolidatedMemory 33 | Input:consolidatedMemory 34 | true 35 | 36 | 37 | newMemory 38 | primitive://String 39 | newMemory 40 | Input:newMemory 41 | true 42 | 43 | sfdc_ai__DefaultGPT4OmniMini 44 | Published 45 | CNSyYi7XBrJXfPt263fnNSJ26Xsc+rcfRzrE+DtAFyo=_2 46 | 47 | einstein_gpt__flex 48 | Global 49 | 50 | -------------------------------------------------------------------------------- /force-app/main/default/genAiPromptTemplates/AnswerFromFile.genAiPromptTemplate-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Oq+z2t7jnhqpqemOhnh1UFWAMUvqqyi3bFjeWFcN3tQ=_1 4 | AnswerFromFile 5 | Answer questions about this file 6 | 7 | You're a business-savvy assistant specialized in analyzing Salesforce files like contracts, proposals, or SOWs. 8 | 9 | A user has asked a question about the file you're currently focused on. 10 | 11 | <FILE-METADATA> 12 | {!$RecordSnapshot:file.Snapshot} 13 | </FILE-METADATA> 14 | 15 | 16 | <QUESTION> 17 |  {!$Input:userQuestion} 18 | </QUESTION> 19 | 20 | ### Answering Instructions 21 | 22 | - Use only the file's content and metadata. 23 | - If the file doesn't contain the requested info, say so clearly. 24 | - If a summary is requested: 25 |  - Start with a TL;DR (1–2 lines) 26 |  - Then follow with a more detailed explanation 27 | - Avoid overly long answers unless explicitly asked 28 | - Keep responses concise to allow space for follow-ups 29 | 30 | 31 | file 32 | SOBJECT://ContentDocument 33 | file 34 | Input:file 35 | true 36 | 37 | 38 | userQuestion 39 | primitive://String 40 | userQuestion 41 | Input:userQuestion 42 | true 43 | 44 | sfdc_ai__DefaultOpenAIGPT4OmniMini 45 | Published 46 | 47 | invocable://getDataForGrounding 48 | 49 | primitive://String 50 | true 51 | recordId 52 | {!$Input:file.Id} 53 | 54 | RecordSnapshot:file 55 | 56 | Oq+z2t7jnhqpqemOhnh1UFWAMUvqqyi3bFjeWFcN3tQ=_1 57 | 58 | einstein_gpt__flex 59 | Global 60 | 61 | -------------------------------------------------------------------------------- /force-app/main/default/classes/AgentMemory_Test.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class AgentMemory_Test { 3 | 4 | @IsTest 5 | private static void consolidateWithNoMemory() { 6 | 7 | // Exercise 8 | new AgentMemory().consolidate(); 9 | 10 | // Verify 11 | List memories = [SELECT Id FROM Memory__c]; 12 | Assert.areEqual(0, memories.size()); 13 | } 14 | 15 | @IsTest 16 | private static void consolidateWithNewMemory() { 17 | 18 | // Setup 19 | insert new List{ 20 | new Memory__c(Name = 'Preference 1', Content__c = 'User likes tables'), 21 | new Memory__c(Name = 'Preference 2', Content__c = 'User prefers JSON format') 22 | }; 23 | 24 | // Exercise 25 | new AgentMemory().consolidate(); 26 | 27 | // Verify 28 | List memories = [SELECT Name, Content__c FROM Memory__c]; 29 | Assert.areEqual(1, memories.size()); 30 | Assert.areEqual('CONSOLIDATED_MEMORY', memories[0].Name); 31 | Assert.isNotNull(memories[0].Content__c); 32 | Assert.isTrue(memories[0].Content__c.length() > 0); 33 | } 34 | 35 | 36 | @IsTest 37 | private static void consolidateWithExistingConsolidated() { 38 | 39 | // Setup 40 | insert new List{ 41 | new Memory__c(Name = 'CONSOLIDATED_MEMORY', Content__c = 'Existing consolidated memory'), 42 | new Memory__c(Name = 'New Preference', Content__c = 'User wants detailed responses') 43 | }; 44 | 45 | // Exercise 46 | new AgentMemory().consolidate(); 47 | 48 | // Verify 49 | List memories = [SELECT Name, Content__c FROM Memory__c]; 50 | Assert.areEqual(1, memories.size()); 51 | Assert.areEqual('CONSOLIDATED_MEMORY', memories[0].Name); 52 | Assert.isNotNull(memories[0].Content__c); 53 | Assert.isTrue(memories[0].Content__c.length() > 0); 54 | } 55 | 56 | 57 | @IsTest 58 | private static void consolidateSharedMemory() { 59 | 60 | // Setup 61 | insert new List{ 62 | new Memory__c(Name = 'Shared 1', Content__c = 'S1', IsShared__c = true), 63 | new Memory__c(Name = 'Shared 2', Content__c = 'S2', IsShared__c = true) 64 | }; 65 | 66 | // Exercise 67 | new AgentMemory((Id)null).consolidate(); 68 | 69 | // Verify 70 | List memories = [SELECT Name, Content__c FROM Memory__c]; 71 | Assert.areEqual(1, memories.size()); 72 | Assert.areEqual('CONSOLIDATED_MEMORY', memories[0].Name); 73 | Assert.isNotNull(memories[0].Content__c); 74 | Assert.isTrue(memories[0].Content__c.length() > 0); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /force-app/main/default/layouts/Memory__c-Memory Layout.layout-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Submit 4 | 5 | false 6 | false 7 | true 8 | 9 | 10 | 11 | Required 12 | Name 13 | 14 | 15 | Edit 16 | Content__c 17 | 18 | 19 | Edit 20 | IsShared__c 21 | 22 | 23 | 24 | 25 | Edit 26 | OwnerId 27 | 28 | 29 | 30 | 31 | 32 | false 33 | false 34 | true 35 | 36 | 37 | 38 | Readonly 39 | CreatedById 40 | 41 | 42 | 43 | 44 | Readonly 45 | LastModifiedById 46 | 47 | 48 | 49 | 50 | 51 | true 52 | false 53 | true 54 | 55 | 56 | 57 | 58 | 59 | 60 | false 61 | false 62 | false 63 | false 64 | false 65 | 66 | 00hdh000000xK5x 67 | 4 68 | 0 69 | Default 70 | 71 | 72 | -------------------------------------------------------------------------------- /force-app/main/default/classes/QueryRecordsWithSoql_Test.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class QueryRecordsWithSoql_Test { 3 | 4 | 5 | @IsTest 6 | static void execute() { 7 | 8 | // Setup 9 | Account a = new Account(Name='AcmeCorp'); 10 | insert a; 11 | 12 | // Exercise 13 | QueryRecordsWithSoql.Input input = new QueryRecordsWithSoql.Input(); 14 | input.dynamicSoqlQuery = 'SELECT Name FROM Account'; 15 | 16 | List result = QueryRecordsWithSoql.execute( 17 | new List{ input } 18 | ); 19 | 20 | // Verify 21 | Assert.areEqual(1, result.size()); 22 | Assert.isTrue(String.isNotBlank(result[0].resultOrException)); 23 | 24 | List records = (List) JSON.deserializeUntyped(result[0].resultOrException); 25 | Assert.areEqual(1, records.size()); 26 | } 27 | 28 | @IsTest 29 | static void handleQueryError() { 30 | // Setup & Exercise 31 | QueryRecordsWithSoql.Input input = new QueryRecordsWithSoql.Input(); 32 | input.dynamicSoqlQuery = 'SELECT InvalidField FROM Account'; 33 | 34 | List result = QueryRecordsWithSoql.execute( 35 | new List{ input } 36 | ); 37 | 38 | // Verify 39 | Assert.areEqual(1, result.size()); 40 | QueryRecordsWithSoql.Output output = result[0]; 41 | Assert.isTrue(String.isNotBlank(output.resultOrException)); 42 | 43 | Map exceptionResult = (Map) JSON.deserializeUntyped(output.resultOrException); 44 | Assert.isTrue(exceptionResult.containsKey('message')); 45 | Assert.isTrue(exceptionResult.containsKey('typeName')); 46 | Assert.isTrue(((String)exceptionResult.get('message')).contains('InvalidField')); 47 | } 48 | 49 | @IsTest 50 | static void executeAggregateQuery() { 51 | // Setup 52 | Account a1 = new Account(Name='AcmeCorp', Type='Customer'); 53 | Account a2 = new Account(Name='GlobalCorp', Type='Customer'); 54 | insert new List{ a1, a2 }; 55 | 56 | // Exercise - Aggregate query with GROUP BY 57 | QueryRecordsWithSoql.Input input = new QueryRecordsWithSoql.Input(); 58 | input.dynamicSoqlQuery = 'SELECT Type, COUNT(Id) recordCount FROM Account GROUP BY Type'; 59 | 60 | List result = QueryRecordsWithSoql.execute( 61 | new List{ input } 62 | ); 63 | 64 | // Verify 65 | Assert.areEqual(1, result.size()); 66 | Assert.isTrue(String.isNotBlank(result[0].resultOrException)); 67 | 68 | List aggregateResults = (List) JSON.deserializeUntyped(result[0].resultOrException); 69 | Assert.areEqual(1, aggregateResults.size()); 70 | 71 | Map firstResult = (Map) aggregateResults[0]; 72 | Assert.isTrue(firstResult.containsKey('Type')); 73 | Assert.isTrue(firstResult.containsKey('recordCount')); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /code-analyzer-cleancode.csv: -------------------------------------------------------------------------------- 1 | "rule","engine","severity","tags","file","startLine","startColumn","endLine","endColumn","message","resources" 2 | "CheckIfProperFalsePositive","pmd",4,"Recommended,OpinionatedSalesforce,Apex,Custom","force-app/main/default/classes/CallGitHubApi.cls",2,19,2,44,"Only False Positives can be supressed and need a proper comment.", 3 | "CheckIfProperFalsePositive","pmd",4,"Recommended,OpinionatedSalesforce,Apex,Custom","force-app/main/default/classes/CallMetadataApi.cls",2,19,2,44,"Only False Positives can be supressed and need a proper comment.", 4 | "CheckIfProperFalsePositive","pmd",4,"Recommended,OpinionatedSalesforce,Apex,Custom","force-app/main/default/classes/CallRestApi.cls",4,19,4,74,"Only False Positives can be supressed and need a proper comment.", 5 | "CheckIfProperFalsePositive","pmd",4,"Recommended,OpinionatedSalesforce,Apex,Custom","force-app/main/default/classes/CallToolingApi.cls",2,19,2,75,"Only False Positives can be supressed and need a proper comment.", 6 | "CheckIfProperFalsePositive","pmd",4,"Recommended,OpinionatedSalesforce,Apex,Custom","force-app/main/default/classes/CreatePlantUmlUrl.cls",2,19,2,44,"Only False Positives can be supressed and need a proper comment.", 7 | "CheckIfProperFalsePositive","pmd",4,"Recommended,OpinionatedSalesforce,Apex,Custom","force-app/main/default/classes/CustomSettings.cls",2,19,2,59,"Only False Positives can be supressed and need a proper comment.", 8 | "CheckIfProperFalsePositive","pmd",4,"Recommended,OpinionatedSalesforce,Apex,Custom","force-app/main/default/classes/ExploreOrgSchema.cls",2,19,2,44,"Only False Positives can be supressed and need a proper comment.", 9 | "CognitiveComplexity","pmd",3,"Recommended,Design,Apex,OpinionatedSalesforce","force-app/main/default/classes/ExploreOrgSchema.cls",3,21,317,2,"The class 'ExploreOrgSchema' has a total cognitive complexity of 55 (highest 8), current threshold is 50","https://docs.pmd-code.org/pmd-doc-7.18.0/pmd_rules_apex_design.html#cognitivecomplexity" 10 | "CheckIfProperFalsePositive","pmd",4,"Recommended,OpinionatedSalesforce,Apex,Custom","force-app/main/default/classes/ExploreOrgSchema.cls",210,23,210,51,"Only False Positives can be supressed and need a proper comment.", 11 | "CheckIfProperFalsePositive","pmd",4,"Recommended,OpinionatedSalesforce,Apex,Custom","force-app/main/default/classes/LoadCustomInstructions.cls",2,19,2,44,"Only False Positives can be supressed and need a proper comment.", 12 | "CheckIfProperFalsePositive","pmd",4,"Recommended,OpinionatedSalesforce,Apex,Custom","force-app/main/default/classes/NightlyMemoryConsolidation.cls",2,19,2,44,"Only False Positives can be supressed and need a proper comment.", 13 | "CheckIfProperFalsePositive","pmd",4,"Recommended,OpinionatedSalesforce,Apex,Custom","force-app/main/default/classes/QueryRecordsWithSoql.cls",2,19,2,44,"Only False Positives can be supressed and need a proper comment.", 14 | "CheckIfProperFalsePositive","pmd",4,"Recommended,OpinionatedSalesforce,Apex,Custom","force-app/main/default/classes/RetrieveVectorizeChunks.cls",2,19,2,44,"Only False Positives can be supressed and need a proper comment.", 15 | "CheckIfProperFalsePositive","pmd",4,"Recommended,OpinionatedSalesforce,Apex,Custom","force-app/main/default/classes/SearchWeb.cls",2,19,2,44,"Only False Positives can be supressed and need a proper comment.", 16 | "CheckIfProperFalsePositive","pmd",4,"Recommended,OpinionatedSalesforce,Apex,Custom","force-app/main/default/classes/StoreCustomInstruction.cls",2,19,2,44,"Only False Positives can be supressed and need a proper comment.", 17 | -------------------------------------------------------------------------------- /force-app/main/default/classes/ExploreOrgSchema_Test.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class ExploreOrgSchema_Test { 3 | 4 | @IsTest 5 | private static void summary() { 6 | 7 | // Setup 8 | mockResponse(); 9 | 10 | ExploreOrgSchema.Input input = new ExploreOrgSchema.Input(); 11 | input.scope = 'summary'; 12 | 13 | // Exercise 14 | List result = ExploreOrgSchema.execute(new List{ input }); 15 | 16 | // Verify 17 | Assert.areEqual(1, result.size()); 18 | String response = result[0].response; 19 | Assert.isTrue(response.contains('aquiva_os__Memory__c')); 20 | Assert.isTrue(response.contains('MyOrgButler Memory')); 21 | } 22 | 23 | 24 | @IsTest 25 | private static void summaryWithNamespaceFilter() { 26 | 27 | // Setup 28 | mockResponse(); 29 | 30 | ExploreOrgSchema.Input input = new ExploreOrgSchema.Input(); 31 | input.scope = 'summary'; 32 | input.namespaceFilter = 'aquiva_os'; 33 | 34 | // Exercise 35 | List result = ExploreOrgSchema.execute(new List{ input }); 36 | 37 | // Verify 38 | Assert.areEqual(1, result.size()); 39 | String response = result[0].response; 40 | Assert.isTrue(response.contains('aquiva_os__Memory__c')); 41 | Assert.isTrue(response.contains('MyOrgButler Memory')); 42 | } 43 | 44 | 45 | @IsTest 46 | private static void details() { 47 | 48 | // Setup 49 | mockResponse(); 50 | 51 | ExploreOrgSchema.Input input = new ExploreOrgSchema.Input(); 52 | input.scope = 'details'; 53 | input.objectFilter = 'aquiva_os__Memory__c'; 54 | 55 | // Exercise 56 | List result = ExploreOrgSchema.execute(new List{ input }); 57 | 58 | // Verify 59 | Assert.areEqual(1, result.size()); 60 | String response = result[0].response; 61 | Assert.isTrue(response.contains('aquiva_os__Memory__c')); 62 | Assert.isTrue(response.contains('MyOrgButler Memory')); 63 | Assert.isTrue(response.contains('Content__c')); 64 | Assert.isTrue(response.contains('Content')); 65 | } 66 | 67 | 68 | @IsTest 69 | private static void relationships() { 70 | 71 | // Setup 72 | mockResponse(); 73 | 74 | ExploreOrgSchema.Input input = new ExploreOrgSchema.Input(); 75 | input.scope = 'relationships'; 76 | input.objectFilter = 'aquiva_os__Memory__c'; 77 | 78 | // Exercise 79 | List result = ExploreOrgSchema.execute(new List{ input }); 80 | 81 | // Verify 82 | Assert.areEqual(1, result.size()); 83 | String response = result[0].response; 84 | Assert.isTrue(response.contains('aquiva_os__Memory__c')); 85 | Assert.isTrue(response.contains('MyOrgButler Memory')); 86 | Assert.isTrue(response.contains('childRelationships')); 87 | Assert.isTrue(response.contains('parentRelationships')); 88 | } 89 | 90 | // HELPER 91 | 92 | private static void mockResponse() { 93 | HttpResponse response = new HttpResponse(); 94 | response.setStatusCode(200); 95 | response.setBody('{"records":[{"DeveloperName":"aquiva_os__Memory__c","Description":"MyOrgButler Memory"}]}'); 96 | 97 | CallToolingApi.mockedResponse = response; 98 | } 99 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiPromptTemplates/AnswerFromVectorizeChunks.genAiPromptTemplate-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | q7MyTfwMuUCCWfGws2IzSJ9TWeku2X0yTr1X0oEMjzM=_4 4 | AnswerFromVectorizeChunks 5 | Answer From Vectorize Chunks 6 | 7 | You are an expert assistant helping users find information from our company knowledge base. Your task is to provide accurate, helpful answers to the users <USER_QUESTION> based solely on the retrieved information in <RETRIEVED_CHUNKS> 8 | 9 | <INSTRUCTIONS> 10 | - Answer the user&apos;s question using ONLY the information provided in the retrieved chunks. 11 | - Do not use technical jargon like &quot;based on the retrieved chunks&quot; or &quot;according to the provided information&quot; in your answer. 12 | - When you use information from a source, introduce it naturally. For example: &quot;According to [source name], ...&quot; or &quot;The [source name] document states that...&quot;. 13 | - Always include a markdown link to the `sourceUrl` immediately after the source name, like this: `[source name](sourceUrl)`. 14 | - If the retrieved chunks don&apos;t contain enough information to fully answer the question, clearly state what information is missing. 15 | - If no relevant information is found, state that you cannot answer based on the available information. 16 | </INSTRUCTIONS> 17 | 18 | <SAMPLE_OUTPUT> 19 | Here is an example of the data format you will receive: 20 | 21 | ```json 22 | [ 23 | { 24 | &quot;text&quot;: &quot;Our standard support hours are Monday to Friday, 9 AM to 5 PM.&quot;, 25 | &quot;source&quot;: &quot;Support Policy&quot;, 26 | &quot;sourceUrl&quot;: &quot;https://example.com/support-policy&quot; 27 | }, 28 | { 29 | &quot;text&quot;: &quot;For urgent issues, a premium support plan offers 24/7 assistance.&quot;, 30 | &quot;source&quot;: &quot;Premium Support SLA&quot;, 31 | &quot;sourceUrl&quot;: &quot;https://example.com/premium-sla&quot; 32 | } 33 | ] 34 | ``` 35 | 36 | could produce this answer: 37 | 38 | "According to the [Support Policy](https://example.com/support-policy), standard support hours are Monday to Friday, 9 AM to 5 PM. The [Premium Support SLA](https://example.com/premium-sla) document states that for urgent issues, a premium support plan offers 24/7 assistance."" 39 | </SAMPLE_OUTPUT> 40 | 41 | <USER_QUESTION> 42 | {!$Input:userQuestion} 43 | </USER_QUESTION> 44 | 45 | <RETRIEVED_CHUNKS> 46 | {!$Input:retrievedChunks} 47 | </RETRIEVED_CHUNKS> 48 | 49 | 50 | retrievedChunks 51 | primitive://String 52 | retrievedChunks 53 | Input:retrievedChunks 54 | true 55 | 56 | 57 | userQuestion 58 | primitive://String 59 | userQuestion 60 | Input:userQuestion 61 | true 62 | 63 | sfdc_ai__DefaultVertexAIGemini25Flash001 64 | Published 65 | q7MyTfwMuUCCWfGws2IzSJ9TWeku2X0yTr1X0oEMjzM=_4 66 | 67 | einstein_gpt__flex 68 | Global 69 | 70 | -------------------------------------------------------------------------------- /force-app/main/default/classes/AgentMemory.cls: -------------------------------------------------------------------------------- 1 | public with sharing class AgentMemory { 2 | 3 | private static final String CONSOLIDATED_NAME = 'CONSOLIDATED_MEMORY'; 4 | 5 | private Id userId; 6 | private Memory__c consolidated = new Memory__c(); 7 | private List newMemory = new List(); 8 | 9 | // CTOR 10 | 11 | public AgentMemory() { 12 | this(UserInfo.getUserId()); 13 | } 14 | 15 | public AgentMemory(Id userId) { 16 | this.userId = userId; 17 | } 18 | 19 | // PUBLIC 20 | 21 | public void consolidate() { 22 | queryMemories(); 23 | 24 | if(newMemory.size() > 0) { 25 | String content = performConsolidation(); 26 | updateConsolidated(content); 27 | delete newMemory; 28 | } 29 | } 30 | 31 | // PRIVATE 32 | 33 | private void queryMemories() { 34 | for(Memory__c memory : (List)Database.query(memorySoql(), System.AccessLevel.SYSTEM_MODE)) { 35 | if(memory.Name == CONSOLIDATED_NAME) { 36 | consolidated = memory; 37 | } 38 | else { 39 | newMemory.add(memory); 40 | } 41 | } 42 | } 43 | 44 | private String memorySoql() { 45 | String filter = (userId == null) ? 'IsShared__c = true' : 'CreatedById = :userId'; 46 | return 'SELECT Id, Name, Content__c, CreatedDate FROM Memory__c WHERE ' + filter + ' ORDER BY CreatedDate ASC'; 47 | } 48 | 49 | private String performConsolidation() { 50 | String existing = consolidated != null ? consolidated.Content__c : ''; 51 | existing = (existing != null) ? existing.left(10000) : ''; // Limit to 10k characters to avoid API limits 52 | 53 | String newMemories = buildMemoryText(); 54 | newMemories = newMemories.left(10000); // Limit to 10k characters to avoid API limits 55 | 56 | String result = callPrompt(existing, newMemories); 57 | return result; 58 | } 59 | 60 | private String buildMemoryText() { 61 | List lines = new List(); 62 | 63 | for(Memory__c memory : newMemory) { 64 | lines.add(memory.Name + ' : ' + memory.Content__c); 65 | } 66 | 67 | return String.join(lines, '\n'); 68 | } 69 | 70 | private String callPrompt(String consolidated, String newMemories) { 71 | String result = 'Mock response'; 72 | 73 | if(!Test.isRunningTest()) { 74 | ConnectApi.WrappedValue consolidatedMemoryValue = new ConnectApi.WrappedValue(); 75 | consolidatedMemoryValue.value = consolidated; 76 | 77 | ConnectApi.WrappedValue newMemoryValue = new ConnectApi.WrappedValue(); 78 | newMemoryValue.value = newMemories; 79 | 80 | ConnectApi.EinsteinPromptTemplateGenerationsInput input = new ConnectApi.EinsteinPromptTemplateGenerationsInput(); 81 | input.additionalConfig = new ConnectApi.EinsteinLlmAdditionalConfigInput(); 82 | input.additionalConfig.applicationName = 'PromptBuilderPreview'; 83 | input.isPreview = false; 84 | input.inputParams = new Map{ 85 | 'Input:consolidatedMemory' => consolidatedMemoryValue, 86 | 'Input:newMemory' => newMemoryValue 87 | }; 88 | 89 | ConnectApi.EinsteinPromptTemplateGenerationsRepresentation output = 90 | ConnectApi.EinsteinLLM.generateMessagesForPromptTemplate('ConsolidateMemory', input); 91 | 92 | result = output.generations[0].text; 93 | } 94 | 95 | return result; 96 | } 97 | 98 | private void updateConsolidated(String content) { 99 | consolidated.Name = CONSOLIDATED_NAME; 100 | consolidated.Content__c = content.left(32768); 101 | upsert consolidated; 102 | } 103 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/CallMetadataApi.cls: -------------------------------------------------------------------------------- 1 | // PMD False Positives: Agentforce requires Global 2 | @SuppressWarnings('PMD.AvoidGlobalModifier') 3 | global with sharing class CallMetadataApi { 4 | 5 | public static HttpResponse mockedResponse = null; 6 | 7 | @InvocableMethod(label='MyOrgButler: Use Metadata API' description='Describe, list, reads, creates, updates, renames or deletes metadata using an Apex Wrapper lib. Most actions require a Metadata type given its name and attribute map as JSON. Uses an MDAPI Apex wrapper.') 8 | global static List execute(List descriptions) { 9 | List result = new List(); 10 | 11 | for(MetadataDescription metadata : descriptions) { 12 | try { 13 | Wsdl action = actionFor(metadata) 14 | .setMockResponse(mockedResponse) 15 | .call() 16 | .handleErrors(); 17 | 18 | result.add(new Output( action.getResponse()?.getBody() )); 19 | } 20 | catch(Wsdl.SoapApiException ex) { 21 | result.add(new Output( ex.getMessage() )); 22 | } 23 | } 24 | 25 | return result; 26 | } 27 | 28 | // PRIVATE 29 | 30 | private static Wsdl actionFor(MetadataDescription metadata) { 31 | Wsdl result = null; 32 | 33 | switch on metadata.soapAction { 34 | when 'describeMetadata' { 35 | result = new MdtWsdl(metadata.soapAction); 36 | } 37 | when 'listMetadata' { 38 | result = new MdtWsdl(metadata.soapAction) 39 | .setAllOrNone(true) 40 | .addListMetadata(metadata.typeName); 41 | } 42 | when 'readMetadata' { 43 | result = new MdtWsdl(metadata.soapAction) 44 | .setItemMetadataType(metadata.typeName) 45 | .addItemMetadataFullName(metadata.fullName); 46 | } 47 | when 'createMetadata', 'updateMetadata', 'upsertMetadata' { 48 | result = new MdtWsdl(metadata.soapAction) 49 | .addCredMetadata(metadata.typeName, jsonToMap(metadata.attributesJson)); 50 | } 51 | when 'deleteMetadata' { 52 | result = new MdtWsdl(metadata.soapAction) 53 | .setItemMetadataType(metadata.typeName) 54 | .addItemMetadataFullName(metadata.fullName); 55 | } 56 | } 57 | 58 | return result; 59 | } 60 | 61 | 62 | private static Map jsonToMap(String jsonString) { 63 | return (Map) JSON.deserializeUntyped(jsonString); 64 | } 65 | 66 | // INNER 67 | 68 | global class MetadataDescription { 69 | @InvocableVariable(label='SOAP Action' description='One of the allowed SOAP Action types: describeMetadata,listMetadata,readMetadata,createMetadata,updateMetadata,upsertMetadata,deleteMetadata' required=true) 70 | global String soapAction; 71 | 72 | @InvocableVariable(label='Metadata Type Name' description='The official Salesforce Metadata Type Name' required=false) 73 | global String typeName; 74 | 75 | @InvocableVariable(label='Attributes JSON' description='A JSON with all relevant attributes needed to create this metadata type.' required=false) 76 | global String attributesJson; 77 | 78 | @InvocableVariable(label='Metadata Full Name' description='Some Actions need a Full Name of the existing Metadata record' required=false) 79 | global String fullName; 80 | } 81 | 82 | 83 | global class Output { 84 | @InvocableVariable(label='Response Body' description='Raw HTTP Response to be interpreted for Errors or Success') 85 | global String responseBody; 86 | 87 | public Output(String body) { 88 | responseBody = body; 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/CallToolingApi_Test.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class CallToolingApi_Test { 3 | 4 | @IsTest 5 | private static void queryAction() { 6 | // Setup 7 | Exception unexpectedException = null; 8 | mockResponse('{"records":[{"Id":"01p....","Name":"TestClass"}]}'); 9 | 10 | CallToolingApi.ToolingDescription tooling = new CallToolingApi.ToolingDescription(); 11 | tooling.action = 'query'; 12 | tooling.soql = 'SELECT Id, Name FROM ApexClass'; 13 | 14 | // Exercise 15 | try { 16 | CallToolingApi.execute(new List{ tooling }); 17 | } 18 | catch(Exception ex) { 19 | unexpectedException = ex; 20 | } 21 | 22 | // Verify 23 | Assert.isNull(unexpectedException); 24 | } 25 | 26 | 27 | @IsTest 28 | private static void createAction() { 29 | // Setup 30 | Exception unexpectedException = null; 31 | mockResponse('{"id":"01p....","success":true}'); 32 | 33 | CallToolingApi.ToolingDescription tooling = new CallToolingApi.ToolingDescription(); 34 | tooling.action = 'create'; 35 | tooling.objectType = 'ApexClass'; 36 | tooling.recordJson = '{ "Name" : "TestClass", "Body" : "public class TestClass {}" }'; 37 | 38 | // Exercise 39 | try { 40 | CallToolingApi.execute(new List{ tooling }); 41 | } 42 | catch(Exception ex) { 43 | unexpectedException = ex; 44 | } 45 | 46 | // Verify 47 | Assert.isNull(unexpectedException); 48 | } 49 | 50 | 51 | @IsTest 52 | private static void updateAction() { 53 | // Setup 54 | Exception unexpectedException = null; 55 | mockResponse('{"id":"01p....","success":true}'); 56 | 57 | CallToolingApi.ToolingDescription tooling = new CallToolingApi.ToolingDescription(); 58 | tooling.action = 'update'; 59 | tooling.objectType = 'ApexClass'; 60 | tooling.recordId = '01p....'; 61 | tooling.recordJson = '{ "Body" : "public class TestClass { }" }'; 62 | 63 | // Exercise 64 | try { 65 | CallToolingApi.execute(new List{ tooling }); 66 | } 67 | catch(Exception ex) { 68 | unexpectedException = ex; 69 | } 70 | 71 | // Verify 72 | Assert.isNull(unexpectedException); 73 | } 74 | 75 | 76 | @IsTest 77 | private static void deleteAction() { 78 | // Setup 79 | Exception unexpectedException = null; 80 | mockResponse(''); 81 | 82 | CallToolingApi.ToolingDescription tooling = new CallToolingApi.ToolingDescription(); 83 | tooling.action = 'delete'; 84 | tooling.objectType = 'ApexClass'; 85 | tooling.recordId = '01p....'; 86 | 87 | // Exercise 88 | try { 89 | CallToolingApi.execute(new List{ tooling }); 90 | } 91 | catch(Exception ex) { 92 | unexpectedException = ex; 93 | } 94 | 95 | // Verify 96 | Assert.isNull(unexpectedException); 97 | } 98 | 99 | 100 | @IsTest 101 | private static void invalidAction() { 102 | // Setup 103 | Exception expectedException = null; 104 | 105 | CallToolingApi.ToolingDescription tooling = new CallToolingApi.ToolingDescription(); 106 | tooling.action = 'invalid'; 107 | 108 | // Exercise 109 | try { 110 | CallToolingApi.execute(new List{ tooling }); 111 | } 112 | catch(Exception ex) { 113 | expectedException = ex; 114 | } 115 | 116 | // Verify 117 | Assert.isNotNull(expectedException); 118 | Assert.isTrue(expectedException.getMessage().contains('Unsupported Tooling API action')); 119 | } 120 | 121 | // HELPER 122 | 123 | private static void mockResponse(String body) { 124 | HttpResponse response = new HttpResponse(); 125 | response.setStatusCode(200); 126 | response.setBody(body); 127 | 128 | CallToolingApi.mockedResponse = response; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /force-app/main/default/permissionsets/MyOrgButlerUser.permissionset-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Grants access to MyOrgButler AI agent capabilities including memory management, API integrations, and knowledge search features. 4 | 5 | CallGitHubApi 6 | true 7 | 8 | 9 | CallMetadataApi 10 | true 11 | 12 | 13 | CallRestApi 14 | true 15 | 16 | 17 | CallToolingApi 18 | true 19 | 20 | 21 | CreatePlantUmlUrl 22 | true 23 | 24 | 25 | ExploreOrgSchema 26 | true 27 | 28 | 29 | LoadCustomInstructions 30 | true 31 | 32 | 33 | QueryRecordsWithSoql 34 | true 35 | 36 | 37 | RetrieveVectorizeChunks 38 | true 39 | 40 | 41 | SearchWeb 42 | true 43 | 44 | 45 | SessionId 46 | true 47 | 48 | 49 | StoreCustomInstruction 50 | true 51 | 52 | 53 | true 54 | CustomSetting__c 55 | 56 | 57 | true 58 | aquiva_os__GitHubApi-ApiKey 59 | 60 | 61 | true 62 | aquiva_os__TavilyApi-ApiKey 63 | 64 | 65 | true 66 | aquiva_os__VectorizeApi-Token 67 | 68 | 69 | false 70 | Memory__c.Content__c 71 | true 72 | 73 | 74 | false 75 | Memory__c.IsShared__c 76 | true 77 | 78 | false 79 | 80 | 81 | false 82 | false 83 | false 84 | true 85 | false 86 | Memory__c 87 | false 88 | false 89 | 90 | 91 | false 92 | false 93 | false 94 | true 95 | false 96 | UserExternalCredential 97 | false 98 | false 99 | 100 | 101 | Memory__c 102 | Available 103 | 104 | 105 | true 106 | ExecutePromptTemplates 107 | 108 | 109 | -------------------------------------------------------------------------------- /force-app/main/default/genAiPromptTemplates/AnswerFromRelatedFiles.genAiPromptTemplate-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Oq+z2t7jnhqpqemOhnh1UFWAMUvqqyi3bFjeWFcN3tQ=_1 4 | AnswerFromRelatedFiles 5 | Answer questions with related files 6 | 7 | You're an expert Sales Assistant with deep knowledge of Sales Cloud, CRM processes,  8 | and sales-related documentation. 9 | 10 | A Salesforce user has asked a question about this opportunity and its related files. 11 | 12 | <QUESTION> 13 | {!$Input:userQuestion} 14 | </QUESTION> 15 | 16 | <OPPORTUNITY> 17 | {!$RecordSnapshot:opportunity.Snapshot} 18 | </OPPORTUNITY> 19 | 20 | <ATTACHMENTS> 21 | {!$RelatedList:opportunity.CombinedAttachments.Records} 22 | {!$RelatedList:opportunity.Account.CombinedAttachments.Records} 23 | </ATTACHMENTS> 24 | 25 | ### Answering Instructions 26 | 27 | - Answer using only the content of attached files on the Opportunity and its Account. 28 | - If no relevant file content is available, respond clearly. 29 | - If the user asks for a summary: 30 |  - Start with a TL;DR (1–2 lines) 31 |  - Then give a longer explanation 32 | - Stay concise unless more detail is requested 33 | - Encourage follow-up questions by keeping answers focused 34 | 35 | 36 | 37 | opportunity 38 | SOBJECT://Opportunity 39 | opportunity 40 | Input:opportunity 41 | true 42 | 43 | 44 | userQuestion 45 | primitive://String 46 | userQuestion 47 | Input:userQuestion 48 | true 49 | 50 | sfdc_ai__DefaultGPT41Mini 51 | Published 52 | 53 | invocable://getRelatedList 54 | 55 | primitive://String 56 | true 57 | parentRecordId 58 | {!$Input:opportunity.Id} 59 | 60 | 61 | primitive://String 62 | true 63 | relatedListName 64 | CombinedAttachments 65 | 66 | RelatedList:opportunity.CombinedAttachments.Records 67 | 68 | 69 | invocable://getRelatedList 70 | 71 | primitive://String 72 | true 73 | parentRecordId 74 | {!$Input:opportunity.Account.Id} 75 | 76 | 77 | primitive://String 78 | true 79 | relatedListName 80 | CombinedAttachments 81 | 82 | RelatedList:opportunity.Account.CombinedAttachments.Records 83 | 84 | 85 | invocable://getDataForGrounding 86 | 87 | primitive://String 88 | true 89 | recordId 90 | {!$Input:opportunity.Id} 91 | 92 | RecordSnapshot:opportunity 93 | 94 | Oq+z2t7jnhqpqemOhnh1UFWAMUvqqyi3bFjeWFcN3tQ=_1 95 | 96 | einstein_gpt__flex 97 | Global 98 | 99 | -------------------------------------------------------------------------------- /force-app/jfwberg/lightweight-soap-util/package/test/classes/ApxWsdlTest.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Justus van den Berg (jfwberg@gmail.com) 3 | * @date November 2024 4 | * @copyright (c) 2024 Justus van den Berg 5 | * @license MIT (See LICENSE file in the project root) 6 | * @description Test class for the Apex Wsdl class 7 | */ 8 | @IsTest 9 | @SuppressWarnings('PMD.ApexAssertionsShouldIncludeMessage') 10 | private with sharing class ApxWsdlTest { 11 | 12 | /** 13 | * APEX WSDL - EXECUTE ANONYMOUS APEX EXAMPLE 14 | */ 15 | @IsTest 16 | static void testExecuteAnonymous(){ 17 | 18 | // Test with myDomain URL + Session Id 19 | Wsdl apexWsdl = new ApxWsdl(URL.getOrgDomainUrl().toExternalForm(), Wsdl.VAL_BEARER_TOKEN_HIDDEN_MESSAGE , 'executeAnonymous') 20 | .setCode('System.debug(12345);') 21 | .setLogLevel('DEBUG') 22 | .setLogCategory('APEX_CODE') 23 | .setupRequest() 24 | ; 25 | 26 | // Check the body is as expected 27 | Assert.areEqual( 28 | '' + Wsdl.VAL_BEARER_TOKEN_HIDDEN_MESSAGE + 'APEX_CODEDEBUGNONESystem.debug(12345);', 29 | apexWsdl.getRequest().getBody() 30 | ); 31 | } 32 | 33 | 34 | /** 35 | * APEX WSDL - COMPILE CLASSES EXAMPLE 36 | */ 37 | @IsTest 38 | static void testCompileClasses(){ 39 | 40 | // Test with named credential 41 | Wsdl apexWsdl = new ApxWsdl('testNamedCredential','compileClasses') 42 | .addScript('public class demo{}') 43 | .setupRequest() 44 | ; 45 | 46 | // Check the body is as expected 47 | Assert.areEqual( 48 | '' + Wsdl.VAL_NC_TOKEN_MERGE_VARIABLE + 'APEX_CODEERRORNONEpublic class demo{}', 49 | apexWsdl.getRequest().getBody() 50 | ); 51 | } 52 | 53 | 54 | /** 55 | * APEX WSDL - COMPILE TRIGGERS EXAMPLE 56 | */ 57 | @IsTest 58 | static void testCompileTriggers(){ 59 | 60 | // Test with no session Id and no my domain Url, so connect to self 61 | Wsdl apexWsdl = new ApxWsdl('compileTriggers') 62 | .addScript('trigger UserTrigger on User( before insert){}') 63 | .setupRequest() 64 | ; 65 | 66 | // Check the body is as expected 67 | Assert.areEqual( 68 | '' + Wsdl.VAL_BEARER_TOKEN_HIDDEN_MESSAGE + 'APEX_CODEERRORNONEtrigger UserTrigger on User( before insert){}', 69 | apexWsdl.getRequest().getBody() 70 | ); 71 | } 72 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/CallToolingApi.cls: -------------------------------------------------------------------------------- 1 | // Note: Agentforce requires Global; session auth needed to call own org's Tooling API as current user 2 | @SuppressWarnings('PMD.AvoidGlobalModifier, PMD.ApexSuggestUsingNamedCred') 3 | global with sharing class CallToolingApi { 4 | 5 | public static HttpResponse mockedResponse = null; 6 | 7 | @InvocableMethod(label='MyOrgButler: Use Tooling API' description='Query, create, update, or delete records using the Tooling API. Supports SOQL queries and CRUD operations on Tooling API objects.') 8 | global static List execute(List descriptions) { 9 | List result = new List(); 10 | 11 | for(ToolingDescription tooling : descriptions) { 12 | HttpResponse response = mockedResponse != null ? mockedResponse : makeRequest(tooling); 13 | result.add(new Output(response.getBody())); 14 | } 15 | 16 | return result; 17 | } 18 | 19 | // PUBLIC 20 | 21 | public static List> query(String soql) { 22 | List> results = new List>(); 23 | 24 | ToolingDescription tooling = new ToolingDescription(); 25 | tooling.action = 'query'; 26 | tooling.soql = soql; 27 | 28 | List outputs = execute(new List{tooling}); 29 | String responseBody = outputs[0].responseBody; 30 | 31 | Map responseMap = (Map)JSON.deserializeUntyped(responseBody); 32 | if(responseMap.containsKey('records')) { 33 | List records = (List)responseMap.get('records'); 34 | for(Object record : records) { 35 | results.add((Map)record); 36 | } 37 | } 38 | 39 | return results; 40 | } 41 | 42 | // PRIVATE 43 | 44 | private static HttpResponse makeRequest(ToolingDescription tooling) { 45 | HttpRequest request = new HttpRequest(); 46 | request.setHeader('Authorization', 'Bearer ' + new SessionId().asString()); 47 | request.setHeader('Content-Type', 'application/json'); 48 | 49 | String baseUrl = URL.getOrgDomainUrl().toExternalForm() + '/services/data/v63.0/tooling'; 50 | 51 | switch on tooling.action { 52 | when 'query' { 53 | request.setMethod('GET'); 54 | request.setEndpoint(baseUrl + '/query/?q=' + EncodingUtil.urlEncode(tooling.soql, 'UTF-8')); 55 | } 56 | when 'create' { 57 | request.setMethod('POST'); 58 | request.setEndpoint(baseUrl + '/sobjects/' + tooling.objectType); 59 | request.setBody(tooling.recordJson); 60 | } 61 | when 'update' { 62 | request.setMethod('PATCH'); 63 | request.setEndpoint(baseUrl + '/sobjects/' + tooling.objectType + '/' + tooling.recordId); 64 | request.setBody(tooling.recordJson); 65 | } 66 | when 'delete' { 67 | request.setMethod('DELETE'); 68 | request.setEndpoint(baseUrl + '/sobjects/' + tooling.objectType + '/' + tooling.recordId); 69 | } 70 | when else { 71 | throw new ApplicationException('Unsupported Tooling API action: ' + tooling.action); 72 | } 73 | } 74 | 75 | return new Http().send(request); 76 | } 77 | 78 | 79 | // INNER 80 | 81 | global class ToolingDescription { 82 | @InvocableVariable(label='Action' description='The Tooling API action to perform: query, create, update, delete' required=true) 83 | global String action; 84 | 85 | @InvocableVariable(label='Object Type' description='The Tooling API object type (e.g., ApexClass, CustomField)' required=false) 86 | global String objectType; 87 | 88 | @InvocableVariable(label='SOQL Query' description='The SOQL query to execute (for query action)' required=false) 89 | global String soql; 90 | 91 | @InvocableVariable(label='Record JSON' description='JSON representation of the record to create or update' required=false) 92 | global String recordJson; 93 | 94 | @InvocableVariable(label='Record Id' description='Id of the record to update or delete' required=false) 95 | public String recordId; 96 | } 97 | 98 | global class Output { 99 | @InvocableVariable(label='Response Body' description='Raw HTTP Response to be interpreted for Errors or Success') 100 | global String responseBody; 101 | 102 | public Output(String body) { 103 | responseBody = body; 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "retrieval-staging", 5 | "default": true 6 | }, 7 | { 8 | "path": "unpackaged", 9 | "default": false 10 | }, 11 | { 12 | "path": "force-app", 13 | "default": false, 14 | "package": "my-org-butler", 15 | "versionName": "December '25", 16 | "versionNumber": "2.21.0.NEXT", 17 | "versionDescription": "", 18 | "packageMetadataAccess": { 19 | "permissionSets": [ 20 | "EinsteinGPTPromptTemplateManager" 21 | ] 22 | }, 23 | "dependencies": [ 24 | { 25 | "package": "app-foundations@LATEST" 26 | } 27 | ] 28 | } 29 | ], 30 | "name": "my-org-butler", 31 | "namespace": "aquiva_os", 32 | "sfdcLoginUrl": "https://login.salesforce.com", 33 | "sourceApiVersion": "65.0", 34 | "packageAliases": { 35 | "app-foundations@LATEST": "04tVI000000L3ZBYA0", 36 | "my-org-butler": "0HoVI0000002h2T0AQ", 37 | "my-org-butler@1.1.0-1": "04tVI0000002nxVYAQ", 38 | "my-org-butler@1.2.0-2": "04tVI0000002o3xYAA", 39 | "my-org-butler@1.3.0-1": "04tVI0000002ojtYAA", 40 | "my-org-butler@1.4.0-1": "04tVI0000002oojYAA", 41 | "my-org-butler@1.5.0-1": "04tVI0000002oqLYAQ", 42 | "my-org-butler@1.6.0-1": "04tVI0000002ovBYAQ", 43 | "my-org-butler@1.7.0-1": "04tVI0000002p6TYAQ", 44 | "my-org-butler@1.8.0-1": "04tVI0000002pBJYAY", 45 | "my-org-butler@1.9.0-1": "04tVI0000002pXtYAI", 46 | "my-org-butler@1.10.0-1": "04tVI0000002pw5YAA", 47 | "my-org-butler@1.11.0-1": "04tVI0000002qk5YAA", 48 | "my-org-butler@1.12.0-2": "04tVI000000337pYAA", 49 | "my-org-butler@1.13.0-1": "04tVI000000339RYAQ", 50 | "my-org-butler@1.14.0-1": "04tVI00000033CfYAI", 51 | "my-org-butler@1.15.0-1": "04tVI00000036LpYAI", 52 | "my-org-butler@1.16.0-1": "04tVI00000037ZdYAI", 53 | "my-org-butler@1.17.0-1": "04tVI00000037eTYAQ", 54 | "my-org-butler@1.18.0-1": "04tVI00000039OXYAY", 55 | "my-org-butler@1.19.0-1": "04tVI0000003CSrYAM", 56 | "my-org-butler@1.20.0-1": "04tVI0000003u45YAA", 57 | "my-org-butler@1.21.0-1": "04tVI00000040KnYAI", 58 | "my-org-butler@1.22.0-1": "04tVI0000004CQnYAM", 59 | "my-org-butler@1.23.0-2": "04tVI0000004Y4jYAE", 60 | "my-org-butler@1.26.0-1": "04tVI0000004YBBYA2", 61 | "my-org-butler@1.27.0-1": "04tVI0000004YntYAE", 62 | "my-org-butler@1.27.0-2": "04tVI0000004YvxYAE", 63 | "my-org-butler@1.28.0-1": "04tVI0000004YxZYAU", 64 | "my-org-butler@1.29.0-1": "04tVI0000004f2zYAA", 65 | "my-org-butler@1.30.0-1": "04tVI0000004lQ9YAI", 66 | "my-org-butler@1.31.0-1": "04tVI0000005RpBYAU", 67 | "my-org-butler@1.32.0-1": "04tVI0000006o5NYAQ", 68 | "my-org-butler@1.32.0-2": "04tVI000000E7XtYAK", 69 | "my-org-butler@1.33.0-1": "04tVI000000E7eLYAS", 70 | "my-org-butler@1.33.0-2": "04tVI000000E7jBYAS", 71 | "my-org-butler@1.34.0-1": "04tVI000000ETtNYAW", 72 | "my-org-butler@1.35.0-1": "04tVI000000EUFxYAO", 73 | "my-org-butler@1.36.0-1": "04tVI000000EUizYAG", 74 | "my-org-butler@1.37.0-1": "04tVI000000EWT3YAO", 75 | "my-org-butler@1.40.0-1": "04tVI000000EWrFYAW", 76 | "my-org-butler@1.41.0-1": "04tVI000000EXddYAG", 77 | "my-org-butler@1.42.0-1": "04tVI000000HZpNYAW", 78 | "my-org-butler@1.43.0-1": "04tVI000000HbxdYAC", 79 | "my-org-butler@1.44.0-1": "04tVI000000HkcjYAC", 80 | "my-org-butler@1.45.0-1": "04tVI000000HohxYAC", 81 | "my-org-butler@1.46.0-1": "04tVI000000L48fYAC", 82 | "my-org-butler@1.47.0-1": "04tVI000000L4jlYAC", 83 | "my-org-butler@1.48.0-1": "04tVI000000LLhNYAW", 84 | "my-org-butler@1.49.0-1": "04tVI000000LLkbYAG", 85 | "my-org-butler@1.50.0-1": "04tVI000000LWw9YAG", 86 | "my-org-butler@1.51.0-1": "04tVI000000MgDVYA0", 87 | "my-org-butler@2.0.0-1": "04tVI000000MgqDYAS", 88 | "my-org-butler@2.1.0-1": "04tVI000000MhSvYAK", 89 | "my-org-butler@2.2.0-1": "04tVI000000N10DYAS", 90 | "my-org-butler@2.3.0-1": "04tVI000000NLerYAG", 91 | "my-org-butler@2.4.0-1": "04tVI000000NM1RYAW", 92 | "my-org-butler@2.5.0-1": "04tVI000000NM4fYAG", 93 | "my-org-butler@2.6.0-1": "04tVI000000NTULYA4", 94 | "my-org-butler@2.7.0-1": "04tVI000000NV1VYAW", 95 | "my-org-butler@2.8.0-1": "04tVI000000NVBBYA4", 96 | "my-org-butler@2.9.0-1": "04tVI000000NXxlYAG", 97 | "my-org-butler@2.10.0-1": "04tVI000000O16DYAS", 98 | "my-org-butler@2.11.0-1": "04tVI000000OLjFYAW", 99 | "my-org-butler@2.12.0-1": "04tVI000000Oa0jYAC", 100 | "my-org-butler@2.13.0-1": "04tVI000000OavBYAS", 101 | "my-org-butler@2.14.0-1": "04tVI000000Ob01YAC", 102 | "my-org-butler@2.15.0-1": "04tVI000000Ob1dYAC", 103 | "my-org-butler@2.16.0-1": "04tVI000000OcLtYAK", 104 | "my-org-butler@2.17.0-1": "04tVI000000OfV3YAK", 105 | "my-org-butler@2.18.0-1": "04tVI000000QBpRYAW", 106 | "my-org-butler@2.19.0-1": "04tVI000000S8FZYA0", 107 | "my-org-butler@2.20.0-1": "04tVI000000S8NdYAK" 108 | } 109 | } -------------------------------------------------------------------------------- /force-app/main/default/genAiPlugins/AnswerWithInternet.genAiPlugin-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | This topic handles user requests that require searching the web for information not available in Salesforce or internal sources. 5 | AnswerWithInternet 6 | 7 | SearchWeb 8 | 9 | 10 | LoadCustomInstructions 11 | 12 | 13 | StoreCustomInstruction 14 | 15 | 16 | ## GENERAL INSTRUCTIONS 17 | 18 | - Ensure the web search results are relevant and directly address the user's inquiry. 19 | - If multiple sources provide conflicting information, present the most reliable sources and indicate any discrepancies. 20 | - If the requested information is not found in Salesforce or internal sources, perform a web search to find the necessary details. 21 | - Provide a summary of the information found on the web, including the source. 22 | instruction_generalins0 23 | en_US 24 | instruction_generalins0 25 | 0 26 | 27 | 28 | ## PROACTIVE LEARNING OF USER PREFERENCES 29 | 30 | Your goal is to learn user preferences to improve future interactions. Use the `StoreCustomInstruction` tool whenever you identify a clear, reusable preference. 31 | 32 | **### Triggers for Learning (When to store an instruction):** 33 | 34 | * **Explicit Correction:** When the user directly corrects you (e.g., "No, use the 'Revenue' field, not 'Amount'"). 35 | * **Implicit Refinement:** When the user refines a request right after you respond, implying your first attempt was incomplete (e.g., You show a list of Opportunities, and they immediately say, "Show that sorted by close date"). This implies a sorting preference. 36 | * **Stated Preference:** When the user explicitly states a general preference (e.g., "From now on, always show my reports in a table," or "I always want to see the contact's phone number when you list contacts"). 37 | * **Format Preference:** When a user asks for a specific format (e.g., "Can you put that in a table?"). 38 | 39 | **### How to Create an Instruction:** 40 | 41 | * **Be Specific:** The instruction should be a clear, concise directive. 42 | * **Use a Condition-Action Format:** Frame the instruction as `Condition -> Action`. This makes it easier to apply later. 43 | * **Focus on Reusable Preferences:** Do NOT learn temporary context (e.g., "The user is asking about 'Acme Corp'"). DO learn permanent rules (e.g., "When showing Accounts, always include the 'Industry' field"). 44 | * **Shared or Personal Instructions?** If the context doesn't make it explicit, ask the user if they have everybody benefit from this instruction. 45 | 46 | **### Examples of Good Instructions to Store:** 47 | 48 | * `User asks for projects -> Always sort results by the 'Go_Live_Date__c' field descending.` 49 | * `Querying Opportunities -> Always include the 'NextStep' field in the results.` 50 | * `Displaying lists of records -> Format the output as a markdown table.` 51 | instruction_learningfeedback1 52 | en_US 53 | instruction_learningfeedback1 54 | 0 55 | 56 | 57 | ## APPLYING LEARNED PREFERENCES 58 | 59 | **IMPORTANT:** Before generating a response or calling a tool, you MUST review and adhere to the following learned user preferences. These instructions override your default behavior. 60 | 61 | These preferences were stored from previous conversations to personalize your assistance. Apply them whenever the specified condition is met. 62 | 63 | {!customInstructions} 64 | 65 | If a user's instruction in the current turn conflicts with a learned preference, **the current instruction takes priority.** 66 | instruction_customins2 67 | en_US 68 | instruction_customins2 69 | 0 70 | 71 | en_US 72 | AnswerWithInternet 73 | MyOrgButler: Answer question using the internet 74 | Topic 75 | Your job is only to search the web for information that is not available in Salesforce or internal sources and provide relevant results to the user. 76 | 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## My Org Butler 2 | 3 | My Org Butler is a showcase for building Agentforce solutions that help Salesforce users with their daily work. Using natural language, it answers questions about data, metadata, and configuration, and can perform tasks like creating records, making configuration changes, or notifying other people. 4 | 5 | > ⚠️ **Looking for the custom OpenAI version?** 6 | > 7 | > This is the **Agentforce-only version** that's simple to set up and use. If you're looking for the version that uses OpenAI with custom UI components, please check out the [`openai-agentforce-hybrid`](../../tree/openai-agentforce-hybrid) branch. 8 | 9 | 10 | ### Show me a demo 11 | 12 | [![Agentforce Demo](http://img.youtube.com/vi/_pz1rgWpDXU/hqdefault.jpg)](https://youtu.be/LuLv77KKRIo?si=-ya1wjyksBH2jZbG&t=656 "Agentforce Version") 13 | 14 | 15 | Find all of Aquiva's My Org Butler related video demos at this [YouTube playlist](https://youtube.com/playlist?list=PL9OHq276T12G1Ngw5m05KZyY5jwF5qMLD&si=rmKXPGRYhw4Bxgs9). 16 | 17 | --- 18 | 19 | ### Getting started 20 | 21 | Follow these steps to get My Org Butler running in your org: 22 | 23 | 1. **Install Prerequisite** - Install the [latest version](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tVI000000L3ZBYA0) of the [App Foundations](https://github.com/aquivalabs/app-foundations) package (prerequisite) 24 | 25 | 2. **Install My Org Butler 2.20** - [Production](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tVI000000S8NdYAK) or [Sandbox](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tVI000000S8NdYAK) 26 | 27 | 3. **Enable Agentforce** in your org (if not already enabled) 28 | 29 | 4. **Create an Agent from Template** - After installing the package, create a new agent from the `My Org Butler` template. See [Salesforce Help](https://help.salesforce.com/s/articleView?id=ai.agent_employee_agent_setup.htm&type=5) for instructions. 30 | 31 | 5. **Turn on Optional Features** (choose which capabilities you want to enable): 32 | 33 | **Turn on GitHub Integration:** 34 | 1. Get a [GitHub Personal Access Token](https://github.com/settings/tokens) 35 | 2. Go to **Setup → Named Credentials → `GitHubApi`** 36 | 3. Click on the linked External Credential `GitHubApi` 37 | 4. Create a New Named Principal with parameter name `ApiKey` 38 | 5. Enter your GitHub token as the value 39 | 40 | **Turn on Web Search:** 41 | 1. Get a [free Tavily API key](https://tavily.com/) 42 | 2. Go to **Setup → Named Credentials → `TavilyApi`** 43 | 3. Click on the linked External Credential `TavilyApi` 44 | 4. Create a New Named Principal with parameter name `ApiKey` 45 | 5. Enter your Tavily API key as the value 46 | 47 | **Turn on External Knowledge Search:** 48 | 1. Create a free account at [Vectorize.io](https://platform.vectorize.io/) 49 | 2. Build a RAG pipeline with your company documents (wikis, documentation, etc.) 50 | 3. Note your **Organization ID** and **Pipeline ID** from the Vectorize dashboard 51 | 4. Get your **API token** from the Vectorize backend 52 | 5. Go to **Setup → Named Credentials → `VectorizeApi`** 53 | 6. Click on the linked External Credential `VectorizeApi` 54 | 7. Create a New Named Principal with parameter name `Token` 55 | 8. Enter your Vectorize API token as the value 56 | 9. Go to **Setup → Custom Settings → Custom Setting → Manage** 57 | 10. Create a new record with Name: `VectorizeOrgId`, Value: your Organization ID 58 | 11. Create another record with Name: `VectorizePipelineId`, Value: your Pipeline ID 59 | 60 | That's it! The Butler will be available in your Agentforce sidebar and can now search your external knowledge sources. 61 | 62 | ### What it does 63 | 64 | Built entirely on Agentforce with 9 functions and 5 plugins. The functions handle Salesforce-specific tasks like querying data, calling APIs, and managing metadata. Agentforce provides the AI reasoning and conversation management. 65 | 66 | **Data & Records:** 67 | - Answer questions about your org's data, metadata and settings 68 | - Create and update records (delete not supported) 69 | - Query complex data relationships and generate reports 70 | 71 | **Metadata & Configuration:** 72 | - Explain Salesforce metadata components and configurations 73 | - Create and modify Apex classes, Flows, and custom objects 74 | - Analyze validation rules, custom fields, and object relationships 75 | - Generate PlantUML diagrams for data models and processes 76 | 77 | **GitHub Integration:** 78 | - Manage GitHub repositories, issues, and pull requests 79 | - View commit history and compare code changes 80 | - Search repositories and track development progress 81 | 82 | **Advanced Capabilities:** 83 | - Search the web for additional information using Tavily API 84 | - Search your company's external knowledge base using Vectorize (wikis, docs, etc.) 85 | - Create visual diagrams and documentation 86 | 87 | 88 | ### Development 89 | 90 | Want to customize or extend the Butler? 91 | 92 | 1. Clone this repo 93 | 2. Replace `aquiva_os` namespace with your own (or remove it) 94 | 3. Create a scratch org: `./scripts/create-scratch-org.sh` 95 | 4. Make your changes 96 | 5. Create a package and versions: `./scripts/create-package.sh` 97 | -------------------------------------------------------------------------------- /force-app/main/default/classes/RetrieveVectorizeChunks.cls: -------------------------------------------------------------------------------- 1 | // PMD False Positives: Agentforce requires Global 2 | @SuppressWarnings('PMD.AvoidGlobalModifier') 3 | global with sharing class RetrieveVectorizeChunks { 4 | 5 | @InvocableMethod(label='MyOrgButler: Search External Knowledge' description='Use Vector Search to find relevant internal document chunks to answer users question.') 6 | global static List execute(List questions) { 7 | Answer result = new Answer(); 8 | result.content = getAnswer(questions[0].content); 9 | 10 | return new List{ result }; 11 | } 12 | 13 | // PRIVATE 14 | 15 | private static String getAnswer(String userQuestion) { 16 | List chunks = retrieveFromVectorize(userQuestion); 17 | return generateAnswer(userQuestion, chunks); 18 | } 19 | 20 | private static List retrieveFromVectorize(String query) { 21 | HttpRequest request = new HttpRequest(); 22 | request.setEndpoint('callout:aquiva_os__VectorizeApi' + getEndpoint()); 23 | request.setMethod('POST'); 24 | request.setHeader('Content-Type', 'application/json'); 25 | 26 | Map body = new Map{ 27 | 'question' => query, 28 | 'rerank' => true, 29 | 'numResults' => 10 30 | }; 31 | request.setBody(JSON.serialize(body)); 32 | 33 | HttpResponse response = new Http().send(request); 34 | 35 | if (response.getStatusCode() != 200) { 36 | throw new ApplicationException('External knowledge search failed: ' + response.getStatus()); 37 | } 38 | 39 | return toChunks(response); 40 | } 41 | 42 | private static List toChunks(HttpResponse response) { 43 | List result = new List(); 44 | 45 | Map responseData = (Map) JSON.deserializeUntyped(response.getBody()); 46 | List documents = (List) responseData.get('documents'); 47 | 48 | if(documents != null) { 49 | for(Object doc : documents) { 50 | Map docMap = (Map) doc; 51 | 52 | Chunk chunk = new Chunk(); 53 | chunk.text = (String) docMap.get('text'); 54 | chunk.source = (String) docMap.get('source_display_name'); 55 | chunk.sourceUrl = getSourceUrl(docMap); 56 | chunk.relevancy = (Decimal) docMap.get('relevancy'); 57 | 58 | result.add(chunk); 59 | } 60 | } 61 | 62 | return result; 63 | } 64 | 65 | private static String getSourceUrl(Map docMap) { 66 | Map urlByOrigin = new Map{ 67 | 'google-drive' => 'https://docs.google.com/file/d/', 68 | 'confluence-bucket' => 'https://aquiva.atlassian.net/wiki/spaces/STAN/pages/' 69 | }; 70 | 71 | String origin = (String) docMap.get('origin'); 72 | String sourceId = (String) docMap.get('source'); 73 | 74 | return urlByOrigin.get(origin) + sourceId; 75 | } 76 | 77 | private static String generateAnswer(String userQuestion, List chunks) { 78 | String result = 'Mock answer for testing: ' + userQuestion; 79 | 80 | if(!Test.isRunningTest()) { 81 | ConnectApi.WrappedValue questionParam = new ConnectApi.WrappedValue(); 82 | questionParam.value = userQuestion; 83 | 84 | ConnectApi.WrappedValue chunksParam = new ConnectApi.WrappedValue(); 85 | chunksParam.value = JSON.serialize(chunks); 86 | 87 | ConnectApi.EinsteinPromptTemplateGenerationsInput input = new ConnectApi.EinsteinPromptTemplateGenerationsInput(); 88 | input.additionalConfig = new ConnectApi.EinsteinLlmAdditionalConfigInput(); 89 | input.additionalConfig.applicationName = 'PromptBuilderPreview'; 90 | input.isPreview = false; 91 | input.inputParams = new Map{ 92 | 'Input:userQuestion' => questionParam, 93 | 'Input:retrievedChunks' => chunksParam 94 | }; 95 | 96 | ConnectApi.EinsteinPromptTemplateGenerationsRepresentation output = 97 | ConnectApi.EinsteinLLM.generateMessagesForPromptTemplate('AnswerFromVectorizeChunks', input); 98 | 99 | result = output.generations[0].text; 100 | } 101 | 102 | return result; 103 | } 104 | 105 | @TestVisible 106 | private static String getEndpoint() { 107 | String orgId = CustomSettings.valueFor('VectorizeOrgId'); 108 | String pipelineId = CustomSettings.valueFor('VectorizePipelineId'); 109 | 110 | return '/v1/org/' + orgId + '/pipelines/' + pipelineId + '/retrieval'; 111 | } 112 | 113 | // INNER 114 | 115 | private class Chunk { 116 | public String text; 117 | public String source; 118 | public String sourceUrl; 119 | public Decimal relevancy; 120 | } 121 | 122 | global class Question { 123 | @InvocableVariable(label='Question' description='The question to search for in the company knowledge base.') 124 | global String content; 125 | 126 | global Question(String content) { 127 | this.content = content; 128 | } 129 | } 130 | 131 | global class Answer { 132 | @InvocableVariable(label='Answer' description='AI-generated answer based on company knowledge sources.') 133 | global String content; 134 | } 135 | } -------------------------------------------------------------------------------- /unpackaged/main/default/bots/MyOrgButler/v1.botVersion-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | v1 4 | false 5 | 6 | 7 | 8 | Hi, I’m Agentforce! I use AI to search trusted sources, and more. Ask me “What else can you do?” to see how I can simplify your workday. How can I help? 9 | 446e92b4-3f6b-4ad8-bd83-a553e293a7d4 10 | 11 | 943cc656-5988-4d56-a2f2-b11860b44126 12 | Message 13 | 14 | 15 | 0e6dc268-6bce-4b1b-a28e-abc310e23653 16 | Wait 17 | 18 | Welcome 19 | false 20 | 21 | false 22 | 23 | 24 | 25 | 26 | Something went wrong. Try again. 27 | 83065b4f-62e1-41d3-9bf8-9084f4293c35 28 | 29 | 258bd88b-94e6-4ed3-9db9-65db77b88af5 30 | Message 31 | 32 | 33 | a29671c8-7dc1-4b4c-9bae-37bac84823cb 34 | Wait 35 | 36 | Error_Handling 37 | false 38 | 39 | false 40 | 41 | false 42 | Acme Corp. 43 | 44 | MyOrgButler 45 | 46 | 47 | Text 48 | Salesforce Application Name. 49 | currentAppName 50 | true 51 | 52 | External 53 | 54 | 55 | Text 56 | The API name of the Salesforce object (such as Account or Opportunity) associated with the record the user wants to interact with. Do not use this if the user is already talking about another object in the conversation. 57 | currentObjectApiName 58 | true 59 | 60 | External 61 | 62 | 63 | Text 64 | Type of Salesforce Page. 65 | currentPageType 66 | true 67 | 68 | External 69 | 70 | 71 | Text 72 | The ID of the record on the user's screen. It may not relate to the user's input. Only use this if the user input mentions 'this', 'current', 'the record', etc. If in doubt, don't use it. 73 | currentRecordId 74 | true 75 | 76 | External 77 | 78 | 79 | Text 80 | Information and Preferences of the Current User 81 | customInstructions 82 | true 83 | 84 | External 85 | 86 | 87 | Text 88 | This variable may also be referred to as VerifiedCustomerId 89 | VerifiedCustomerId 90 | false 91 | 92 | Internal 93 | 94 | Welcome 95 | false 96 | false 97 | false 98 | false 99 | Your personal Salesforce butler that helps with daily work using natural language. Answers questions about data, metadata, and configuration, and can perform tasks like creating records or making configuration changes. 100 | false 101 | false 102 | false 103 | false 104 | Casual 105 | 106 | -------------------------------------------------------------------------------- /force-app/main/default/genAiPlugins/EditData.genAiPlugin-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | Use this topic when the user requests to create new records, update existing records, or perform other data manipulation operations within their Salesforce org. 5 | EditData 6 | 7 | LoadCustomInstructions 8 | 9 | 10 | StoreCustomInstruction 11 | 12 | 13 | ExploreOrgSchema 14 | 15 | 16 | CallRestApi 17 | 18 | 19 | ## GENERAL INSTRUCTIONS 20 | 21 | 1. Clearly identify which object(s) the user wants to create or modify data for. 22 | 3. If unsure about the object structure, use the Explore_Org_Schema action to verify object and field names. 23 | 4. For data queries, always use QueryRecordsWithSoql - never use CallRestApi for queries. 24 | 5. For data creation: 25 | - Gather all necessary field values from the user's request 26 | - If information is missing, ask for clarification 27 | - Use the CallRestApi action to create the record 28 | - Provide confirmation with a link to the new record 29 | 6. For data updates: 30 | - First use QueryRecordsWithSoql to query the existing record(s) to understand current state 31 | - Confirm which fields should be modified 32 | - Use the CallRestApi action to update the record 33 | - Provide confirmation with details of what was changed 34 | 7. For enrichment requests (e.g., "populate fields with web data"): 35 | - Use the SearchWeb action to gather the required information 36 | - Format the data appropriately for Salesforce fields 37 | - Use the CallRestApi action to update with the enriched data 38 | 8. Always provide a clear success or error message after the operation. 39 | 9. For bulk operations, consider performing them in batches and provide progress updates. 40 | 10. If you encounter errors, try to interpret them, fix the issue, and retry before returning an error to the user. 41 | instruction_generalins0 42 | en_US 43 | instruction_generalins0 44 | 0 45 | 46 | 47 | ## PROACTIVE LEARNING OF USER PREFERENCES 48 | 49 | Your goal is to learn user preferences to improve future interactions. Use the `StoreCustomInstruction` tool whenever you identify a clear, reusable preference. 50 | 51 | **### Triggers for Learning (When to store an instruction):** 52 | 53 | * **Explicit Correction:** When the user directly corrects you (e.g., "No, use the 'Revenue' field, not 'Amount'"). 54 | * **Implicit Refinement:** When the user refines a request right after you respond, implying your first attempt was incomplete (e.g., You show a list of Opportunities, and they immediately say, "Show that sorted by close date"). This implies a sorting preference. 55 | * **Stated Preference:** When the user explicitly states a general preference (e.g., "From now on, always show my reports in a table," or "I always want to see the contact's phone number when you list contacts"). 56 | * **Format Preference:** When a user asks for a specific format (e.g., "Can you put that in a table?"). 57 | 58 | **### How to Create an Instruction:** 59 | 60 | * **Be Specific:** The instruction should be a clear, concise directive. 61 | * **Use a Condition-Action Format:** Frame the instruction as `Condition -> Action`. This makes it easier to apply later. 62 | * **Focus on Reusable Preferences:** Do NOT learn temporary context (e.g., "The user is asking about 'Acme Corp'"). DO learn permanent rules (e.g., "When showing Accounts, always include the 'Industry' field"). 63 | * **Shared or Personal Instructions?** If the context doesn't make it explicit, ask the user if they have everybody benefit from this instruction. 64 | 65 | **### Examples of Good Instructions to Store:** 66 | 67 | * `User asks for projects -> Always sort results by the 'Go_Live_Date__c' field descending.` 68 | * `Querying Opportunities -> Always include the 'NextStep' field in the results.` 69 | * `Displaying lists of records -> Format the output as a markdown table.` 70 | instruction_learningfeedback1 71 | en_US 72 | instruction_learningfeedback1 73 | 0 74 | 75 | 76 | ## APPLYING LEARNED PREFERENCES 77 | 78 | **IMPORTANT:** Before generating a response or calling a tool, you MUST review and adhere to the following learned user preferences. These instructions override your default behavior. 79 | 80 | These preferences were stored from previous conversations to personalize your assistance. Apply them whenever the specified condition is met. 81 | 82 | {!customInstructions} 83 | 84 | If a user's instruction in the current turn conflicts with a learned preference, **the current instruction takes priority.** 85 | instruction_customins2 86 | en_US 87 | instruction_customins2 88 | 0 89 | 90 | en_US 91 | EditData 92 | MyOrgButler: Edit Data 93 | Topic 94 | Use this topic when the user requests to create new records, update existing records, or perform other data manipulation operations within their Salesforce org. 95 | 96 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Memory__c/Memory__c.object-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Accept 5 | Default 6 | 7 | 8 | Accept 9 | Large 10 | Default 11 | 12 | 13 | Accept 14 | Small 15 | Default 16 | 17 | 18 | CancelEdit 19 | Default 20 | 21 | 22 | CancelEdit 23 | Large 24 | Default 25 | 26 | 27 | CancelEdit 28 | Small 29 | Default 30 | 31 | 32 | Clone 33 | Default 34 | 35 | 36 | Clone 37 | Large 38 | Default 39 | 40 | 41 | Clone 42 | Small 43 | Default 44 | 45 | 46 | Delete 47 | Default 48 | 49 | 50 | Delete 51 | Large 52 | Default 53 | 54 | 55 | Delete 56 | Small 57 | Default 58 | 59 | 60 | Edit 61 | Default 62 | 63 | 64 | Edit 65 | Large 66 | Default 67 | 68 | 69 | Edit 70 | Small 71 | Default 72 | 73 | 74 | List 75 | Default 76 | 77 | 78 | List 79 | Large 80 | Default 81 | 82 | 83 | List 84 | Small 85 | Default 86 | 87 | 88 | New 89 | Default 90 | 91 | 92 | New 93 | Large 94 | Default 95 | 96 | 97 | New 98 | Small 99 | Default 100 | 101 | 102 | SaveEdit 103 | Default 104 | 105 | 106 | SaveEdit 107 | Large 108 | Default 109 | 110 | 111 | SaveEdit 112 | Small 113 | Default 114 | 115 | 116 | Tab 117 | Default 118 | 119 | 120 | Tab 121 | Large 122 | Default 123 | 124 | 125 | Tab 126 | Small 127 | Default 128 | 129 | 130 | View 131 | Default 132 | 133 | 134 | View 135 | Large 136 | Default 137 | 138 | 139 | View 140 | Small 141 | Default 142 | 143 | false 144 | SYSTEM 145 | Deployed 146 | Stores learned preferences and context for AI agent personalization. Memories can be user-specific or shared across the organization. 147 | true 148 | true 149 | false 150 | true 151 | false 152 | true 153 | false 154 | true 155 | true 156 | Private 157 | 158 | 159 | 160 | false 161 | Text 162 | 163 | MyOrgButler Memory 164 | 165 | Private 166 | Public 167 | 168 | -------------------------------------------------------------------------------- /force-app/main/default/genAiPlugins/AnswerWithFiles.genAiPlugin-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | This topic enables answering user questions based on files and attachments stored directly on Salesforce records. It supports traversing from child records (e.g., Opportunity) to related parents (e.g., Account) to locate relevant documents such as contracts, MSAs, NDAs, or quotes. 5 | AnswerWithFiles 6 | 7 | AnswerWithRelatedFiles 8 | 9 | 10 | AnswerWithCurrentFile 11 | 12 | 13 | EmployeeCopilot__GetRecordDetails 14 | 15 | 16 | EmployeeCopilot__IdentifyRecordByName 17 | 18 | 19 | LoadCustomInstructions 20 | 21 | 22 | StoreCustomInstruction 23 | 24 | 25 | ### Purpose 26 | Answer questions based on actual file content stored on Salesforce records. 27 | 28 | ### File Discovery 29 | - Search files and attachments on the current record (e.g., Opportunity) 30 | - Also include related parent records (e.g., Account) 31 | 32 | ### Question Types 33 | - File lookup: “Is there a signed NDA?” 34 | - File content: “What are the payment terms in the MSA?” 35 | - Summaries: “Can you summarize the contract?” 36 | 37 | ### Behavior 38 | - If no file is found, ask the user to clarify or respond accordingly. 39 | - Be concise. TL;DR first for summaries. 40 | - Support follow-up questions by maintaining conversational context. 41 | instruction_purposeans0 42 | en_US 43 | instruction_purposeans0 44 | 0 45 | 46 | 47 | ## SHARING INSTRUCTIONS 48 | 49 | * If the user explicitly says to share the instruction (e.g., "Share this with everyone"), set `isShared` to `true`. 50 | * If the user's intent is clearly for a broad audience (e.g., "All users should see reports this way"), set `isShared` to `true`. 51 | * If you are unsure whether the instruction should be shared, **ask the user**: "Should I remember this just for you, or for everyone?" 52 | instruction_sharingmem0 53 | en_US 54 | instruction_sharingmem0 55 | 0 56 | 57 | 58 | ## PROACTIVE LEARNING OF USER PREFERENCES 59 | 60 | Your goal is to learn user preferences to improve future interactions. Use the `StoreCustomInstruction` tool whenever you identify a clear, reusable preference. 61 | 62 | **### Triggers for Learning (When to store an instruction):** 63 | 64 | * **Explicit Correction:** When the user directly corrects you (e.g., "No, use the 'Revenue' field, not 'Amount'"). 65 | * **Implicit Refinement:** When the user refines a request right after you respond, implying your first attempt was incomplete (e.g., You show a list of Opportunities, and they immediately say, "Show that sorted by close date"). This implies a sorting preference. 66 | * **Stated Preference:** When the user explicitly states a general preference (e.g., "From now on, always show my reports in a table," or "I always want to see the contact's phone number when you list contacts"). 67 | * **Format Preference:** When a user asks for a specific format (e.g., "Can you put that in a table?"). 68 | 69 | **### How to Create an Instruction:** 70 | 71 | * **Be Specific:** The instruction should be a clear, concise directive. 72 | * **Use a Condition-Action Format:** Frame the instruction as `Condition -> Action`. This makes it easier to apply later. 73 | * **Focus on Reusable Preferences:** Do NOT learn temporary context (e.g., "The user is asking about 'Acme Corp'"). DO learn permanent rules (e.g., "When showing Accounts, always include the 'Industry' field"). 74 | * **Shared or Personal Instructions?** If the context doesn't make it explicit, ask the user if they have everybody benefit from this instruction. 75 | 76 | **### Examples of Good Instructions to Store:** 77 | 78 | * `User asks for projects -> Always sort results by the 'Go_Live_Date__c' field descending.` 79 | * `Querying Opportunities -> Always include the 'NextStep' field in the results.` 80 | * `Displaying lists of records -> Format the output as a markdown table.` 81 | instruction_learningfeedback1 82 | en_US 83 | instruction_learningfeedback1 84 | 0 85 | 86 | en_US 87 | AnswerWithFiles 88 | MyOrgButler: Answer questions with file content 89 | Topic 90 | Use this topic when the user wants to understand the content of a file or verify whether a document exists on a Salesforce record. Typical questions might involve asking whether an NDA or MSA has been signed, what’s included in a contract, or details from a quote or proposal stored as an attachment. 91 | 92 | This topic is appropriate when: 93 | 94 | - The user is interacting with a record (e.g., Opportunity) and refers to a file, attachment, or document 95 | - The question requires reading or summarizing file content 96 | - The question implies document lookup, even indirectly (“Do we have an NDA on file?”) 97 | 98 | Do not use this topic if: 99 | - The question refers to external sources like Confluence, G-Drive, or embedded content 100 | - The user is asking about general record fields or metadata (use a data or metadata topic instead) 101 | 102 | -------------------------------------------------------------------------------- /force-app/main/default/genAiPlugins/AnswerWithData.genAiPlugin-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | Helps users understand and analyze their Salesforce data through data model exploration and SOQL queries. Combines understanding of object relationships with data analysis capabilities. 5 | AnswerWithData 6 | 7 | LoadCustomInstructions 8 | 9 | 10 | StoreCustomInstruction 11 | 12 | 13 | ExploreOrgSchema 14 | 15 | 16 | QueryRecordsWithSoql 17 | 18 | 19 | ## GENERAL INSTRUCTIONS 20 | 21 | Follow this methodical approach for data analysis: 22 | 23 | 1. **Explore Smart**: Use ExploreOrgSchema with "summary" scope to find matching objects: 24 | - If user mentions "Project", look for all objects containing "Project" in name or label 25 | - If multiple objects match, try the most likely candidate first based on context 26 | - Only ask for clarification if initial attempts fail 27 | 28 | 2. **Verify Fields**: Use ExploreOrgSchema with "details" scope to understand: 29 | - Available fields and their types 30 | - Picklist values for status fields 31 | - Relationship fields for lookups 32 | 33 | 3. **Query Data**: Use QueryRecords function with appropriate mode: 34 | - For Standard objects: Use direct SOQL mode when schema is well-known 35 | - For Custom objects/apps: Use prompt mode with templates for schema-aware queries 36 | - Include user intent and conversation context in prompt mode 37 | - Parse error responses and retry with corrected queries 38 | 39 | 5. **Handle No Results**: 40 | - Try alternative field names automatically 41 | - Explore related objects if primary object has no matches 42 | - Use prompt mode for complex custom object queries 43 | - Only ask for help when all reasonable attempts are exhausted 44 | 45 | 6. **Present Results**: Provide clear summaries with record links when data is found. 46 | 47 | **Extensibility**: Admins can enhance this topic by: 48 | - Creating custom prompt templates for specific apps (PSA, etc.) 49 | - Adding app-specific keywords to trigger prompt mode 50 | - Including domain knowledge in prompt templates for better SOQL generation 51 | 52 | **Key Principle**: Work intelligently in the background - use prompt templates for custom schemas. 53 | instruction_generalins0 54 | en_US 55 | instruction_generalins0 56 | 0 57 | 58 | 59 | ## PROACTIVE LEARNING OF USER PREFERENCES 60 | 61 | Your goal is to learn user preferences to improve future interactions. Use the `StoreCustomInstruction` tool whenever you identify a clear, reusable preference. 62 | 63 | **### Triggers for Learning (When to store an instruction):** 64 | 65 | * **Explicit Correction:** When the user directly corrects you (e.g., "No, use the 'Revenue' field, not 'Amount'"). 66 | * **Implicit Refinement:** When the user refines a request right after you respond, implying your first attempt was incomplete (e.g., You show a list of Opportunities, and they immediately say, "Show that sorted by close date"). This implies a sorting preference. 67 | * **Stated Preference:** When the user explicitly states a general preference (e.g., "From now on, always show my reports in a table," or "I always want to see the contact's phone number when you list contacts"). 68 | * **Format Preference:** When a user asks for a specific format (e.g., "Can you put that in a table?"). 69 | 70 | **### How to Create an Instruction:** 71 | 72 | * **Be Specific:** The instruction should be a clear, concise directive. 73 | * **Use a Condition-Action Format:** Frame the instruction as `Condition -> Action`. This makes it easier to apply later. 74 | * **Focus on Reusable Preferences:** Do NOT learn temporary context (e.g., "The user is asking about 'Acme Corp'"). DO learn permanent rules (e.g., "When showing Accounts, always include the 'Industry' field"). 75 | * **Shared or Personal Instructions?** If the context doesn't make it explicit, ask the user if they have everybody benefit from this instruction. 76 | 77 | * `User asks for projects -> Always sort results by the 'Go_Live_Date__c' field descending.` 78 | * `Querying Opportunities -> Always include the 'NextStep' field in the results.` 79 | * `Displaying lists of records -> Format the output as a markdown table.` 80 | instruction_learningfeedback1 81 | en_US 82 | instruction_learningfeedback1 83 | 0 84 | 85 | 86 | ## APPLYING LEARNED PREFERENCES 87 | 88 | **IMPORTANT:** Before generating a response or calling a tool, you MUST review and adhere to the following learned user preferences. These instructions override your default behavior. 89 | 90 | These preferences were stored from previous conversations to personalize your assistance. Apply them whenever the specified condition is met. 91 | 92 | {!customInstructions} 93 | 94 | If a user's instruction in the current turn conflicts with a learned preference, **the current instruction takes priority.** 95 | instruction_customins2 96 | en_US 97 | instruction_customins2 98 | 0 99 | 100 | en_US 101 | AnswerWithData 102 | MyOrgButler: Anwer questions with Data 103 | Topic 104 | Use this topic when the user implicitly ask questions about data record in Standard Salesforce apps or apps installed in the org. 105 | 106 | -------------------------------------------------------------------------------- /force-app/jfwberg/lightweight-soap-util/package/test/classes/XmlWriterTest.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Justus van den Berg (jfwberg@gmail.com) 3 | * @date November 2024 4 | * @copyright (c) 2024 Justus van den Berg 5 | * @license MIT (See LICENSE file in the project root) 6 | * @description Test class for the XmlWriter class 7 | */ 8 | @IsTest 9 | private with sharing class XmlWriterTest { 10 | 11 | @IsTest 12 | static void testXmlFromObjectMap(){ 13 | // Assertion variables 14 | String result; 15 | 16 | // Run test 17 | Test.startTest(); 18 | 19 | // Test data 20 | Map connectedAppMetadataMap = new Map{ 21 | 'ConnectedApp' => new Map { 22 | 'fullName' => 'Current_Org', 23 | 'label' => 'Current Org', 24 | 'description' => 'A connected App to the current org\'s for the use of API that are used by Lightning (Web) components', 25 | 'contactEmail' => 'info@aloha-workshop.com', 26 | 'oauthConfig' => new Map { 27 | 'callbackUrl' => 'http://localhost', 28 | 'certificate' => '[CERT_DATA]', 29 | 'isAdminApproved' => true, 30 | 'isConsumerSecretOptional' => false, 31 | 'isIntrospectAllTokens' => false, 32 | 'scopes' => new Object[]{ 33 | 'Api', 34 | 'RefreshToken' 35 | } 36 | }, 37 | 'oauthPolicy' => new Map{ 38 | 'ipRelaxation' => 'ENFORCE', 39 | 'refreshTokenPolicy'=> 'infinite' 40 | }, 41 | 'profileName' => new Object[]{ 42 | 'System Administrator' 43 | }, 44 | 'permissionSetName' => new Object[]{ 45 | 'Current Org' 46 | }, 47 | 'ipRanges' => new Object[]{ 48 | new Map{ 49 | 'start' =>'10.0.0.0', 50 | 'end' =>'10.255.255.255', 51 | 'description' => 'Salesforce Internal IP Address range, needs white listed for calling REST Api' 52 | } 53 | }, 54 | 'attributes' => new Object[]{ 55 | new Map{ 56 | 'key' =>'validationKey', 57 | 'formula' =>'\'EMTX1\'' 58 | }, 59 | new Map{ 60 | 'key' =>'verificationKey', 61 | 'formula' =>'\'EMTX2\'' 62 | } 63 | } 64 | } 65 | }; 66 | 67 | // Existing or New writer example 68 | XmlStreamWriter w = new XmlStreamWriter(); 69 | XmlWriter.write(null, w, connectedAppMetadataMap); 70 | result = w.getXmlString(); 71 | 72 | Test.stopTest(); 73 | 74 | // Validate expected output 75 | Assert.areEqual( 76 | 'Current_OrgA connected App to the current org\'s for the use of API that are used by Lightning (Web) componentsinfo@aloha-workshop.comhttp://localhost[CERT_DATA]truefalsefalseApiRefreshTokenENFORCEinfiniteSystem AdministratorCurrent Org10.0.0.010.255.255.255Salesforce Internal IP Address range, needs white listed for calling REST ApivalidationKey\'EMTX1\'verificationKey\'EMTX2\'', 77 | result, 78 | 'Unexpected XML string' 79 | ); 80 | } 81 | 82 | 83 | @IsTest 84 | static void testXmlFromObjectList(){ 85 | 86 | // Assertion variables 87 | String result; 88 | 89 | Test.startTest(); 90 | 91 | // List holding the sObject types 92 | Object[] objectMap = new Object[]{}; 93 | 94 | // Populate sObject type map 95 | for(String sObjectType : new String[]{'Account','Case','Contact','Opportunity'}){ 96 | objectMap.add( 97 | new Map{ 98 | 'sObjectType' => sObjectType 99 | } 100 | ); 101 | } 102 | 103 | Test.stopTest(); 104 | 105 | // Existing or New writer example 106 | XmlStreamWriter w = new XmlStreamWriter(); 107 | XmlWriter.write(null, w, objectMap); 108 | result = w.getXmlString(); 109 | 110 | // Validate expected output 111 | Assert.areEqual( 112 | 'AccountCaseContactOpportunity', 113 | result, 114 | 'Unexpected XML string' 115 | ); 116 | } 117 | 118 | 119 | @IsTest 120 | static void testXmlFromObjectListWtihNamespace(){ 121 | 122 | // Assertion variables 123 | String result; 124 | 125 | Test.startTest(); 126 | 127 | // List holding the sObject types 128 | Object[] objectMap = new Object[]{}; 129 | 130 | // Populate sObject type map 131 | for(String sObjectType : new String[]{'Account','Case','Contact','Opportunity'}){ 132 | objectMap.add( 133 | new Map{ 134 | 'sObjectType' => sObjectType 135 | } 136 | ); 137 | } 138 | 139 | // Write stream with namespace 140 | XmlStreamWriter w = new XmlStreamWriter(); 141 | XmlWriter.write('dns', w, objectMap); 142 | result = w.getXmlString(); 143 | 144 | Test.stopTest(); 145 | 146 | // Validate expected output 147 | Assert.areEqual( 148 | 'AccountCaseContactOpportunity', 149 | result, 150 | 'Unexpected XML string' 151 | ); 152 | } 153 | } --------------------------------------------------------------------------------