From aa44d3b3dba88c435524de0e49d9615d908b5555 Mon Sep 17 00:00:00 2001 From: Mohith Shrivastava Date: Wed, 6 May 2026 10:25:40 -0400 Subject: [PATCH 01/14] feat: add service agent recipe with DX pipeline for agent user management Promotes the CustomerServiceAgent from future_recipes to a new force-app-service/ package directory typed as AgentforceServiceAgent. Adds cross-platform Node.js scripts to create the org-specific agent user and inject it into .agent files at deploy time, with a pre-commit hook to restore the placeholder before commits. CI workflows updated to deploy service agents sequentially after employee agents. Closes #58 --- .github/workflows/ci-pr.yml | 14 +- .github/workflows/ci.yml | 14 +- .husky/pre-commit | 1 + AGENTS.md | 6 +- README.md | 27 ++ bin/clean-service-agent.js | 58 +++++ bin/setup-service-agent.js | 113 +++++++++ force-app-service/README.md | 22 ++ .../CustomerServiceAgent.agent | 3 +- .../CustomerServiceAgent.bundle-meta.xml | 0 .../classes/CustomerServiceAgentFlowTest.cls | 0 .../CustomerServiceAgentFlowTest.cls-meta.xml | 0 .../classes/IssueClassifier.cls | 0 .../classes/IssueClassifier.cls-meta.xml | 0 .../classes/IssueClassifierTest.cls | 0 .../classes/IssueClassifierTest.cls-meta.xml | 0 .../flows/CreateCase.flow-meta.xml | 0 .../flows/EscalateCase.flow-meta.xml | 0 .../flows/FetchCustomer.flow-meta.xml | 0 .../flows/SearchKnowledgeBase.flow-meta.xml | 0 .../SendSatisfactionSurvey.flow-meta.xml | 0 .../flows/UpdateCase.flow-meta.xml | 0 .../Escalation_Reason__c.field-meta.xml | 0 .../fields/Customer_ID__c.field-meta.xml | 0 .../fields/Loyalty_Tier__c.field-meta.xml | 0 .../Survey_Log__c.object-meta.xml | 0 .../fields/Case__c.field-meta.xml | 0 .../fields/Sent_Date__c.field-meta.xml | 0 .../customerServiceAgent/README.md | 240 ------------------ package.json | 3 +- sfdx-project.json | 4 + 31 files changed, 255 insertions(+), 250 deletions(-) create mode 100644 bin/clean-service-agent.js create mode 100644 bin/setup-service-agent.js create mode 100644 force-app-service/README.md rename {force-app/future_recipes/customerServiceAgent => force-app-service}/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent (99%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.bundle-meta.xml (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/classes/CustomerServiceAgentFlowTest.cls (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/classes/CustomerServiceAgentFlowTest.cls-meta.xml (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/classes/IssueClassifier.cls (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/classes/IssueClassifier.cls-meta.xml (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/classes/IssueClassifierTest.cls (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/classes/IssueClassifierTest.cls-meta.xml (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/flows/CreateCase.flow-meta.xml (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/flows/EscalateCase.flow-meta.xml (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/flows/FetchCustomer.flow-meta.xml (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/flows/SearchKnowledgeBase.flow-meta.xml (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/flows/SendSatisfactionSurvey.flow-meta.xml (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/flows/UpdateCase.flow-meta.xml (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/objects/Case/fields/Escalation_Reason__c.field-meta.xml (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/objects/Contact/fields/Customer_ID__c.field-meta.xml (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/objects/Contact/fields/Loyalty_Tier__c.field-meta.xml (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/objects/Survey_Log__c/Survey_Log__c.object-meta.xml (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/objects/Survey_Log__c/fields/Case__c.field-meta.xml (100%) rename {force-app/future_recipes/customerServiceAgent => force-app-service}/objects/Survey_Log__c/fields/Sent_Date__c.field-meta.xml (100%) delete mode 100644 force-app/future_recipes/customerServiceAgent/README.md diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 6ea5b54..18d6c9d 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -180,9 +180,9 @@ jobs: - name: 'Assign Manage Prompt Template permission set' run: sf org assign permset -n EinsteinGPTPromptTemplateManager - # Deploy source to scratch org - - name: 'Push source to scratch org' - run: sf project deploy start --dev-debug + # Deploy employee agent source to scratch org + - name: 'Deploy employee agent recipes' + run: sf project deploy start --source-dir force-app --dev-debug # Assign permission sets - name: 'Assign permission sets to default user' @@ -194,6 +194,14 @@ jobs: - name: 'Import sample data' run: sf data tree import -p ./data/data-plan.json + # Create agent user for service agent deployment + - name: 'Create agent user for service agent' + run: node bin/setup-service-agent.js --target-org scratch-org + + # Deploy service agent source to scratch org + - name: 'Deploy service agent recipes' + run: sf project deploy start --source-dir force-app-service --dev-debug + # Run Apex tests in scratch org - name: 'Run Apex tests' run: sf apex test run -c -r human -d ./tests/apex -w 20 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5485f94..1d0e0d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,9 +120,9 @@ jobs: - name: 'Assign Manage Prompt Template permission set' run: sf org assign permset -n EinsteinGPTPromptTemplateManager - # Deploy source to scratch org - - name: 'Push source to scratch org' - run: sf project deploy start --dev-debug + # Deploy employee agent source to scratch org + - name: 'Deploy employee agent recipes' + run: sf project deploy start --source-dir force-app --dev-debug # Assign permission sets - name: 'Assign permission sets to default user' @@ -134,6 +134,14 @@ jobs: - name: 'Import sample data' run: sf data tree import -p ./data/data-plan.json + # Create agent user for service agent deployment + - name: 'Create agent user for service agent' + run: node bin/setup-service-agent.js --target-org scratch-org + + # Deploy service agent source to scratch org + - name: 'Deploy service agent recipes' + run: sf project deploy start --source-dir force-app-service --dev-debug + # Run Apex tests in scratch org - name: 'Run Apex tests' run: sf apex test run -c -r human -d ./tests/apex -w 20 diff --git a/.husky/pre-commit b/.husky/pre-commit index 2601a3b..e80854f 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,2 @@ +node bin/clean-service-agent.js npm run precommit \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 5dc1f9f..d857384 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,8 @@ Detailed rules and guidelines are in the `.airules/` directory. Read the relevan ## Project Layout -- **Agent scripts:** `force-app/**/aiAuthoringBundles/**/*.agent` -- **Apex services:** `force-app/**/classes/*.cls` +- **Employee Agent scripts:** `force-app/**/aiAuthoringBundles/**/*.agent` +- **Service Agent scripts:** `force-app-service/**/aiAuthoringBundles/**/*.agent` +- **Apex services:** `force-app/**/classes/*.cls` and `force-app-service/classes/*.cls` - **Recipes:** each subdirectory under `force-app/main/` is a self-contained recipe +- **Service Agent recipes:** `force-app-service/` contains service agent examples that require a `default_agent_user` diff --git a/README.md b/README.md index 339712b..81f42a8 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,30 @@ If you don't have an org yet, you can sign up for a free [Developer Edition Org] sf data import tree --plan data/data-plan.json ``` +1. **(Service Agent recipes)** Install npm dependencies. This is required for the setup script that creates and configures the agent user: + + ```bash + npm install + ``` + +1. **(Service Agent recipes)** Create the agent user and prepare the service agent metadata for deployment. Service Agents (unlike Employee Agents) require a `default_agent_user` — an org-specific user that the agent runs as. This script creates that user and injects it into all `.agent` files under `force-app-service/`: + + ```bash + npm run setup:service-agent + ``` + + If deploying to a specific org (not your default), pass the target org alias: + + ```bash + npm run setup:service-agent -- --target-org my-org-alias + ``` + +1. **(Service Agent recipes)** Deploy the service agent metadata: + + ```bash + sf project deploy start --source-dir force-app-service + ``` + 1. Open your org with the **Agentforce Studio** app displayed: ```bash @@ -87,6 +111,9 @@ If you don't have an org yet, you can sign up for a free [Developer Edition Org] > [!TIP] > **Agentforce Studio** can be reached from the App Launcher. From there, click **View All** then select the **Agentforce Studio** app. +> [!NOTE] +> **What is a Service Agent?** Unlike Employee Agents (which run as the logged-in user), Service Agents are external-facing agents that run under a dedicated agent user. This user is org-specific, which is why the `npm run setup:service-agent` step is needed to dynamically create and configure it before deployment. + **Post installation:** when working with the recipes, assign the **Agent Script Recipes Data** permission set to your agent user to avoid access issues. ## Optional Installation Instructions diff --git a/bin/clean-service-agent.js b/bin/clean-service-agent.js new file mode 100644 index 0000000..33d7e10 --- /dev/null +++ b/bin/clean-service-agent.js @@ -0,0 +1,58 @@ +#!/usr/bin/env node + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const SERVICE_AGENT_DIR = path.resolve(__dirname, '..', 'force-app-service'); +const PLACEHOLDER = '__AGENT_USER_PLACEHOLDER__'; +const AGENT_USER_REGEX = + /default_agent_user:\s*"(?!__AGENT_USER_PLACEHOLDER__).+"/g; +const REPLACEMENT = 'default_agent_user: "__AGENT_USER_PLACEHOLDER__"'; + +function findAgentFiles(dir) { + const results = []; + + function walk(currentDir) { + const entries = fs.readdirSync(currentDir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(currentDir, entry.name); + if (entry.isDirectory()) { + walk(fullPath); + } else if (entry.name.endsWith('.agent')) { + results.push(fullPath); + } + } + } + + walk(dir); + return results; +} + +if (!fs.existsSync(SERVICE_AGENT_DIR)) { + process.exit(0); +} + +const agentFiles = findAgentFiles(SERVICE_AGENT_DIR); +let restoredCount = 0; + +for (const filePath of agentFiles) { + let content = fs.readFileSync(filePath, 'utf8'); + + if (AGENT_USER_REGEX.test(content)) { + content = content.replace(AGENT_USER_REGEX, REPLACEMENT); + fs.writeFileSync(filePath, content, 'utf8'); + execSync(`git add "${filePath}"`); + const relativePath = path.relative(SERVICE_AGENT_DIR, filePath); + console.log(`Restored placeholder in: ${relativePath}`); + restoredCount++; + } +} + +if (restoredCount > 0) { + console.log( + `\nRestored ${PLACEHOLDER} in ${restoredCount} file(s) before commit.` + ); +} diff --git a/bin/setup-service-agent.js b/bin/setup-service-agent.js new file mode 100644 index 0000000..baf78a7 --- /dev/null +++ b/bin/setup-service-agent.js @@ -0,0 +1,113 @@ +#!/usr/bin/env node + +'use strict'; + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const SERVICE_AGENT_DIR = path.resolve(__dirname, '..', 'force-app-service'); +const PLACEHOLDER = '__AGENT_USER_PLACEHOLDER__'; + +function parseArgs() { + const args = process.argv.slice(2); + let targetOrg = null; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--target-org' && args[i + 1]) { + targetOrg = args[i + 1]; + i++; + } + } + + return { targetOrg }; +} + +function findAgentFiles(dir) { + const results = []; + + function walk(currentDir) { + const entries = fs.readdirSync(currentDir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(currentDir, entry.name); + if (entry.isDirectory()) { + walk(fullPath); + } else if (entry.name.endsWith('.agent')) { + results.push(fullPath); + } + } + } + + walk(dir); + return results; +} + +function createAgentUser(targetOrg) { + const orgFlag = targetOrg ? ` -o ${targetOrg}` : ''; + const cmd = `sf org create agent-user${orgFlag} --json`; + + console.log(`Running: ${cmd}`); + + try { + const output = execSync(cmd, { encoding: 'utf8' }); + const result = JSON.parse(output); + + if (result.status !== 0) { + console.error('Failed to create agent user:', result.message); + process.exit(1); + } + + return result.result.username; + } catch (error) { + console.error('Error creating agent user:', error.message); + process.exit(1); + } +} + +function replacePlaceholders(username) { + if (!fs.existsSync(SERVICE_AGENT_DIR)) { + console.error( + `Service agent directory not found: ${SERVICE_AGENT_DIR}` + ); + process.exit(1); + } + + const agentFiles = findAgentFiles(SERVICE_AGENT_DIR); + + if (agentFiles.length === 0) { + console.error('No .agent files found in force-app-service/'); + process.exit(1); + } + + let replacedCount = 0; + + for (const filePath of agentFiles) { + let content = fs.readFileSync(filePath, 'utf8'); + + if (content.includes(PLACEHOLDER)) { + content = content.replace(PLACEHOLDER, username); + fs.writeFileSync(filePath, content, 'utf8'); + console.log( + `Updated: ${path.relative(SERVICE_AGENT_DIR, filePath)}` + ); + replacedCount++; + } + } + + if (replacedCount === 0) { + console.warn( + `Warning: No files contained the placeholder "${PLACEHOLDER}". They may have already been replaced.` + ); + } else { + console.log( + `\nReplaced placeholder in ${replacedCount} file(s) with agent user: ${username}` + ); + console.log( + '\nReminder: Do not commit the modified agent file(s). The pre-commit hook will restore the placeholder automatically.' + ); + } +} + +const { targetOrg } = parseArgs(); +const username = createAgentUser(targetOrg); +replacePlaceholders(username); diff --git a/force-app-service/README.md b/force-app-service/README.md new file mode 100644 index 0000000..e74d04d --- /dev/null +++ b/force-app-service/README.md @@ -0,0 +1,22 @@ +# Service Agent Recipes + +This directory contains Agentforce **Service Agent** examples. Service Agents are external-facing agents (customer support, self-service portals) that run under a dedicated agent user rather than the logged-in user. + +## Key Difference from Employee Agents + +Service Agents require a `default_agent_user` field in the agent config. This value is org-specific (it contains the org ID), so it cannot be committed to source control. Instead, a placeholder token (`__AGENT_USER_PLACEHOLDER__`) is used in the `.agent` files and replaced at deploy time. + +## Deployment + +See the root [README](../README.md#installing-the-app-using-a-developer-edition-org) for full deployment steps. The key additional steps for service agents are: + +1. `npm run setup:service-agent` — creates the agent user and replaces the placeholder +2. `sf project deploy start --source-dir force-app-service` — deploys the service agent metadata + +A pre-commit hook automatically restores the placeholder before any commit so org-specific values are never pushed to the repository. + +## Recipes + +| Recipe | Description | +| ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| [CustomerServiceAgent](aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) | Complete customer service agent with issue classification, knowledge base resolution, case management, escalation workflows, and satisfaction surveys | diff --git a/force-app/future_recipes/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent b/force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent similarity index 99% rename from force-app/future_recipes/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent rename to force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent index d760883..e51a0ac 100644 --- a/force-app/future_recipes/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent +++ b/force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent @@ -4,7 +4,8 @@ config: developer_name: "CustomerService_Agent" agent_label: "Customer Service Agent" - agent_type: "AgentforceEmployeeAgent" + agent_type: "AgentforceServiceAgent" + default_agent_user: "__AGENT_USER_PLACEHOLDER__" description: "Complete customer service agent with issue classification, resolution workflows, and escalation" variables: diff --git a/force-app/future_recipes/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.bundle-meta.xml b/force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.bundle-meta.xml similarity index 100% rename from force-app/future_recipes/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.bundle-meta.xml rename to force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.bundle-meta.xml diff --git a/force-app/future_recipes/customerServiceAgent/classes/CustomerServiceAgentFlowTest.cls b/force-app-service/classes/CustomerServiceAgentFlowTest.cls similarity index 100% rename from force-app/future_recipes/customerServiceAgent/classes/CustomerServiceAgentFlowTest.cls rename to force-app-service/classes/CustomerServiceAgentFlowTest.cls diff --git a/force-app/future_recipes/customerServiceAgent/classes/CustomerServiceAgentFlowTest.cls-meta.xml b/force-app-service/classes/CustomerServiceAgentFlowTest.cls-meta.xml similarity index 100% rename from force-app/future_recipes/customerServiceAgent/classes/CustomerServiceAgentFlowTest.cls-meta.xml rename to force-app-service/classes/CustomerServiceAgentFlowTest.cls-meta.xml diff --git a/force-app/future_recipes/customerServiceAgent/classes/IssueClassifier.cls b/force-app-service/classes/IssueClassifier.cls similarity index 100% rename from force-app/future_recipes/customerServiceAgent/classes/IssueClassifier.cls rename to force-app-service/classes/IssueClassifier.cls diff --git a/force-app/future_recipes/customerServiceAgent/classes/IssueClassifier.cls-meta.xml b/force-app-service/classes/IssueClassifier.cls-meta.xml similarity index 100% rename from force-app/future_recipes/customerServiceAgent/classes/IssueClassifier.cls-meta.xml rename to force-app-service/classes/IssueClassifier.cls-meta.xml diff --git a/force-app/future_recipes/customerServiceAgent/classes/IssueClassifierTest.cls b/force-app-service/classes/IssueClassifierTest.cls similarity index 100% rename from force-app/future_recipes/customerServiceAgent/classes/IssueClassifierTest.cls rename to force-app-service/classes/IssueClassifierTest.cls diff --git a/force-app/future_recipes/customerServiceAgent/classes/IssueClassifierTest.cls-meta.xml b/force-app-service/classes/IssueClassifierTest.cls-meta.xml similarity index 100% rename from force-app/future_recipes/customerServiceAgent/classes/IssueClassifierTest.cls-meta.xml rename to force-app-service/classes/IssueClassifierTest.cls-meta.xml diff --git a/force-app/future_recipes/customerServiceAgent/flows/CreateCase.flow-meta.xml b/force-app-service/flows/CreateCase.flow-meta.xml similarity index 100% rename from force-app/future_recipes/customerServiceAgent/flows/CreateCase.flow-meta.xml rename to force-app-service/flows/CreateCase.flow-meta.xml diff --git a/force-app/future_recipes/customerServiceAgent/flows/EscalateCase.flow-meta.xml b/force-app-service/flows/EscalateCase.flow-meta.xml similarity index 100% rename from force-app/future_recipes/customerServiceAgent/flows/EscalateCase.flow-meta.xml rename to force-app-service/flows/EscalateCase.flow-meta.xml diff --git a/force-app/future_recipes/customerServiceAgent/flows/FetchCustomer.flow-meta.xml b/force-app-service/flows/FetchCustomer.flow-meta.xml similarity index 100% rename from force-app/future_recipes/customerServiceAgent/flows/FetchCustomer.flow-meta.xml rename to force-app-service/flows/FetchCustomer.flow-meta.xml diff --git a/force-app/future_recipes/customerServiceAgent/flows/SearchKnowledgeBase.flow-meta.xml b/force-app-service/flows/SearchKnowledgeBase.flow-meta.xml similarity index 100% rename from force-app/future_recipes/customerServiceAgent/flows/SearchKnowledgeBase.flow-meta.xml rename to force-app-service/flows/SearchKnowledgeBase.flow-meta.xml diff --git a/force-app/future_recipes/customerServiceAgent/flows/SendSatisfactionSurvey.flow-meta.xml b/force-app-service/flows/SendSatisfactionSurvey.flow-meta.xml similarity index 100% rename from force-app/future_recipes/customerServiceAgent/flows/SendSatisfactionSurvey.flow-meta.xml rename to force-app-service/flows/SendSatisfactionSurvey.flow-meta.xml diff --git a/force-app/future_recipes/customerServiceAgent/flows/UpdateCase.flow-meta.xml b/force-app-service/flows/UpdateCase.flow-meta.xml similarity index 100% rename from force-app/future_recipes/customerServiceAgent/flows/UpdateCase.flow-meta.xml rename to force-app-service/flows/UpdateCase.flow-meta.xml diff --git a/force-app/future_recipes/customerServiceAgent/objects/Case/fields/Escalation_Reason__c.field-meta.xml b/force-app-service/objects/Case/fields/Escalation_Reason__c.field-meta.xml similarity index 100% rename from force-app/future_recipes/customerServiceAgent/objects/Case/fields/Escalation_Reason__c.field-meta.xml rename to force-app-service/objects/Case/fields/Escalation_Reason__c.field-meta.xml diff --git a/force-app/future_recipes/customerServiceAgent/objects/Contact/fields/Customer_ID__c.field-meta.xml b/force-app-service/objects/Contact/fields/Customer_ID__c.field-meta.xml similarity index 100% rename from force-app/future_recipes/customerServiceAgent/objects/Contact/fields/Customer_ID__c.field-meta.xml rename to force-app-service/objects/Contact/fields/Customer_ID__c.field-meta.xml diff --git a/force-app/future_recipes/customerServiceAgent/objects/Contact/fields/Loyalty_Tier__c.field-meta.xml b/force-app-service/objects/Contact/fields/Loyalty_Tier__c.field-meta.xml similarity index 100% rename from force-app/future_recipes/customerServiceAgent/objects/Contact/fields/Loyalty_Tier__c.field-meta.xml rename to force-app-service/objects/Contact/fields/Loyalty_Tier__c.field-meta.xml diff --git a/force-app/future_recipes/customerServiceAgent/objects/Survey_Log__c/Survey_Log__c.object-meta.xml b/force-app-service/objects/Survey_Log__c/Survey_Log__c.object-meta.xml similarity index 100% rename from force-app/future_recipes/customerServiceAgent/objects/Survey_Log__c/Survey_Log__c.object-meta.xml rename to force-app-service/objects/Survey_Log__c/Survey_Log__c.object-meta.xml diff --git a/force-app/future_recipes/customerServiceAgent/objects/Survey_Log__c/fields/Case__c.field-meta.xml b/force-app-service/objects/Survey_Log__c/fields/Case__c.field-meta.xml similarity index 100% rename from force-app/future_recipes/customerServiceAgent/objects/Survey_Log__c/fields/Case__c.field-meta.xml rename to force-app-service/objects/Survey_Log__c/fields/Case__c.field-meta.xml diff --git a/force-app/future_recipes/customerServiceAgent/objects/Survey_Log__c/fields/Sent_Date__c.field-meta.xml b/force-app-service/objects/Survey_Log__c/fields/Sent_Date__c.field-meta.xml similarity index 100% rename from force-app/future_recipes/customerServiceAgent/objects/Survey_Log__c/fields/Sent_Date__c.field-meta.xml rename to force-app-service/objects/Survey_Log__c/fields/Sent_Date__c.field-meta.xml diff --git a/force-app/future_recipes/customerServiceAgent/README.md b/force-app/future_recipes/customerServiceAgent/README.md deleted file mode 100644 index 918044d..0000000 --- a/force-app/future_recipes/customerServiceAgent/README.md +++ /dev/null @@ -1,240 +0,0 @@ -# CustomerServiceAgent - -## Overview - -A **complete real-world customer service agent** demonstrating a production-ready implementation. This comprehensive example combines issue classification, knowledge base search, case management, escalation patterns, and customer satisfaction surveys. - -## Agent Flow - -```mermaid -%%{init: {'theme':'neutral'}}%% -graph TD - A[Customer Arrives] --> B[Subagent: Triage] - B --> C[Fetch Customer & Classify Issue] - C --> D[Subagent: Resolution] - D --> E[Search Knowledge Base] - E --> F{KB Article Found?} - F -->|Yes| G[Subagent: Solution Presentation] - F -->|No| H[Subagent: Escalation] - G --> I[Present Solution] - I --> J{Resolved?} - J -->|Yes| K[Subagent: Closure] - J -->|No| H - H --> L[Escalate to Specialist] - L --> K - K --> M[Send Satisfaction Survey] - M --> N[End] -``` - -## Key Concepts - -- **Issue Classification**: Automatically categorize customer issues and assign priority -- **Knowledge Base Integration**: Dynamic search for solutions based on issue description -- **Case Management**: Create and update support cases throughout the lifecycle -- **Escalation Patterns**: Seamlessly transfer complex issues to human specialists -- **Customer Satisfaction**: Integrated feedback collection upon closure - -## How It Works - -### Subagent: Triage - -The entry point for the conversation. It identifies the customer and classifies their issue. - -```agentscript -subagent triage: - actions: - fetch_customer: - description: "Fetch customer information" - # ... - classify_issue: - description: "Classify customer issue type" - # ... - - reasoning: - actions: - # Classify the issue - classify_customer_issue: @actions.classify_issue - available when @variables.issue_description and not @variables.issue_type - # ... - transition to @subagent.resolution -``` - -### Subagent: Resolution - -Attempts to find a solution using the Knowledge Base and initiates case tracking. - -```agentscript -subagent resolution: - actions: - search_knowledge_base: - description: "Search knowledge base for solutions" - # ... - create_case: - description: "Create support case" - # ... - - reasoning: - actions: - # Search knowledge base - find_solution: @actions.search_knowledge_base - available when @variables.issue_type and not @variables.kb_article_found - # ... - - # If KB solution found, present it - present_solution: @utils.transition to @subagent.solution_presentation - available when @variables.kb_article_found - - # If no KB solution, escalate - escalate_to_specialist: @utils.transition to @subagent.escalation - available when not @variables.kb_article_found -``` - -### Subagent: Solution Presentation - -Presents the found Knowledge Base article to the user and asks for confirmation. - -```agentscript -subagent solution_presentation: - reasoning: - instructions:-> - | I found a solution for your {!@variables.issue_type} issue! - {!@variables.kb_article_content} - Does this resolve your issue? - - actions: - mark_resolved: @actions.update_case - # ... - transition to @subagent.closure - - need_more_help: @utils.transition to @subagent.escalation -``` - -### Subagent: Escalation - -Handles scenarios where the bot cannot resolve the issue or the user requests a human. - -```agentscript -subagent escalation: - reasoning: - instructions:-> - | This issue requires specialist attention. - if @variables.issue_priority == "high": - | Due to the high priority, I'm escalating this immediately. - - actions: - escalate_issue: @actions.escalate_case - with specialist_team=@variables.issue_type - # ... - transition to @subagent.closure -``` - -## Key Code Snippets - -### Variables for Complete Workflow - -```agentscript -variables: - # Customer information - customer_id: mutable string = "" - description: "Unique identifier for the customer" - customer_name: mutable string = "" - description: "Full name of the customer" - customer_email: mutable string = "" - description: "Email address of the customer" - customer_tier: mutable string = "standard" - description: "Customer loyalty tier (standard, premium, enterprise)" - - # Issue tracking - issue_type: mutable string = "" - description: "Category of the customer's issue (e.g., billing, technical)" - issue_description: mutable string = "" - description: "Detailed description of the issue provided by the customer" - issue_priority: mutable string = "medium" - description: "Priority level of the issue (low, medium, high, urgent)" - issue_resolved: mutable boolean = False - description: "Flag indicating if the issue has been resolved" - - # Case management - case_id: mutable string = "" - description: "Salesforce Case ID associated with the interaction" - case_status: mutable string = "new" - description: "Current status of the case (new, in_progress, resolved, escalated, closed)" - - # Knowledge base - kb_article_found: mutable boolean = False - description: "Flag indicating if a relevant KB article was found" - kb_article_content: mutable string = "" - description: "Content of the found KB article" - - # Escalation - requires_escalation: mutable boolean = False - description: "Flag indicating if the issue needs escalation to a human agent" - escalation_reason: mutable string = "" - description: "Reason for escalation" - - # Resolution tracking - resolution_steps: mutable object = {} - description: "List of steps taken to resolve the issue" - customer_satisfied: mutable boolean = False - description: "Flag indicating customer satisfaction with the resolution" -``` - -## Try It Out - -### Example: Successful KB Resolution - -```text -Agent: Welcome to Customer Service! Please describe the issue you're experiencing. - -User: I can't reset my password. - -[Agent classifies issue as "Account Access"] -[Agent searches KB and finds article] -[Agent creates Case] - -Agent: I found a solution for your Account Access issue! - [KB Article Content: Go to settings > security > reset password...] - Does this resolve your issue? - -User: Yes, thanks. - -[Agent updates Case to "Resolved"] - -Agent: ✅ Great! Your issue has been resolved. - Case ID: 500xx000000abc1 - I'm sending a satisfaction survey to your email. -``` - -### Example: Escalation Flow - -```text -Agent: Welcome to Customer Service! Please describe the issue you're experiencing. - -User: I have a complex billing error that needs a manager. - -[Agent classifies issue as "Billing", Priority "High"] -[Agent searches KB... No direct solution found or User rejects KB] - -Agent: This issue requires specialist attention. - Due to the high priority, I'm escalating this immediately. - Creating escalation... - -[Agent escalates case to "Billing" team] - -Agent: Your case (500xx000000abc2) has been escalated to our specialist team. - They will contact you within 4 hours. -``` - -## Features Demonstrated - -1. **Context Engineering**: Hydrating customer data (`before_reasoning`) before interaction. -2. **Chaining Actions**: Combining KB search and Case creation in a single logical step. -3. **Conditional Transitions**: Routing between subagents based on variables (`kb_article_found`, `issue_resolved`). -4. **Variable Descriptions**: providing semantic meaning to variables for better planner performance. - -## Best Practices Demonstrated - -✅ **Context Engineering** - Hydrating data before instructions run. -✅ **Subagent Separation** - Distinct subagents for Triage, Resolution, and Escalation. -✅ **Variable Descriptions** - Clear descriptions helping the LLM understand variable purpose. -✅ **Deterministic Transitions** - Explicit `@utils.transition` calls for state management. diff --git a/package.json b/package.json index dd06df3..8885a64 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "prettier:verify": "prettier --check \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", "prepare": "husky || true", "precommit": "lint-staged", - "validate:agents": "node bin/validate-agents.js" + "validate:agents": "node bin/validate-agents.js", + "setup:service-agent": "node bin/setup-service-agent.js" }, "lint-staged": { "**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}": [ diff --git a/sfdx-project.json b/sfdx-project.json index e02b252..eec1619 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -6,6 +6,10 @@ "package": "AgentScriptRecipes", "versionName": "Spring '26", "versionNumber": "66.0.0.NEXT" + }, + { + "path": "force-app-service", + "default": false } ], "name": "agent-script-recipes", From 6b395ebe0030be86ce0d6f77f08de03e8bcbfca2 Mon Sep 17 00:00:00 2001 From: Mohith Shrivastava Date: Wed, 6 May 2026 10:42:55 -0400 Subject: [PATCH 02/14] feat: assign Agent_Script_Recipes_Data permset to agent user in CI and install scripts The service agent user needs the data permset to access recipe objects. Previously this was a vague post-install note. Now it's automated in CI workflows, install scripts (sh + bat), and documented as an explicit README step with the exact command. --- .github/workflows/ci-pr.yml | 6 ++++++ .github/workflows/ci.yml | 6 ++++++ README.md | 10 ++++++++-- bin/install-scratch.bat | 21 +++++++++++++++++---- bin/install-scratch.sh | 17 +++++++++++++++-- 5 files changed, 52 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 18d6c9d..0a91282 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -198,6 +198,12 @@ jobs: - name: 'Create agent user for service agent' run: node bin/setup-service-agent.js --target-org scratch-org + # Assign permission set to agent user + - name: 'Assign permission set to agent user' + run: | + agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) + sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of "$agent_user" -o scratch-org + # Deploy service agent source to scratch org - name: 'Deploy service agent recipes' run: sf project deploy start --source-dir force-app-service --dev-debug diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d0e0d9..b707cfe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,6 +138,12 @@ jobs: - name: 'Create agent user for service agent' run: node bin/setup-service-agent.js --target-org scratch-org + # Assign permission set to agent user + - name: 'Assign permission set to agent user' + run: | + agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) + sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of "$agent_user" -o scratch-org + # Deploy service agent source to scratch org - name: 'Deploy service agent recipes' run: sf project deploy start --source-dir force-app-service --dev-debug diff --git a/README.md b/README.md index 81f42a8..24e368d 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,14 @@ If you don't have an org yet, you can sign up for a free [Developer Edition Org] sf project deploy start --source-dir force-app-service ``` +1. **(Service Agent recipes)** Assign the required permission set to the agent user so the service agent can access recipe data: + + ```bash + sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of + ``` + + Replace `` with the username printed by the `setup:service-agent` script in the previous step. + 1. Open your org with the **Agentforce Studio** app displayed: ```bash @@ -114,8 +122,6 @@ If you don't have an org yet, you can sign up for a free [Developer Edition Org] > [!NOTE] > **What is a Service Agent?** Unlike Employee Agents (which run as the logged-in user), Service Agents are external-facing agents that run under a dedicated agent user. This user is org-specific, which is why the `npm run setup:service-agent` step is needed to dynamically create and configure it before deployment. -**Post installation:** when working with the recipes, assign the **Agent Script Recipes Data** permission set to your agent user to avoid access issues. - ## Optional Installation Instructions This repository contains several files that are relevant if you want to integrate modern web development tools into your Salesforce development processes or into your continuous integration/continuous deployment processes. diff --git a/bin/install-scratch.bat b/bin/install-scratch.bat index 1259697..d0f87e9 100755 --- a/bin/install-scratch.bat +++ b/bin/install-scratch.bat @@ -23,8 +23,8 @@ cmd.exe /c sf org assign permset -n EinsteinGPTPromptTemplateManager call :checkForError @echo: -echo Pushing source... -cmd.exe /c sf project deploy start +echo Deploying employee agent recipes... +cmd.exe /c sf project deploy start --source-dir force-app call :checkForError @echo: @@ -36,11 +36,24 @@ cmd.exe /c sf org assign permset -n Agent_Script_Recipes_App call :checkForError @echo: +echo Importing sample data... +cmd.exe /c sf data tree import -p data/data-plan.json call :checkForError @echo: -echo Importing sample data... -cmd.exe /c sf data tree import -p data/data-plan.json +echo Creating agent user for service agent... +cmd.exe /c node bin/setup-service-agent.js +call :checkForError +@echo: + +echo Assigning permission set to agent user... +for /f "tokens=*" %%a in ('node -e "const fs=require(\"fs\"),p=require(\"path\");const f=fs.readFileSync(p.resolve(\"force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent\"),\"utf8\");const m=f.match(/default_agent_user:\s*\"([^_][^\"]+)\"/);if(m)process.stdout.write(m[1])"') do set AGENT_USER=%%a +cmd.exe /c sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of %AGENT_USER% +call :checkForError +@echo: + +echo Deploying service agent recipes... +cmd.exe /c sf project deploy start --source-dir force-app-service call :checkForError @echo: diff --git a/bin/install-scratch.sh b/bin/install-scratch.sh index 6ec98ab..185ee00 100755 --- a/bin/install-scratch.sh +++ b/bin/install-scratch.sh @@ -22,8 +22,8 @@ echo "Assigning Manage Prompt Templates permission set..." sf org assign permset -n EinsteinGPTPromptTemplateManager && \ echo "" && \ -echo "Pushing source..." && \ -sf project deploy start && \ +echo "Deploying employee agent recipes..." && \ +sf project deploy start --source-dir force-app && \ echo "" && \ echo "Assigning Agent Script permission sets..." && \ @@ -35,6 +35,19 @@ echo "Importing sample data..." && \ sf data import tree --plan data/data-plan.json && \ echo "" && \ +echo "Creating agent user for service agent..." && \ +node bin/setup-service-agent.js && \ +echo "" && \ + +echo "Assigning permission set to agent user..." && \ +agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) && \ +sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of "$agent_user" && \ +echo "" && \ + +echo "Deploying service agent recipes..." && \ +sf project deploy start --source-dir force-app-service && \ +echo "" && \ + echo "Opening org..." && \ sf org open -p lightning/n/standard-AgentforceStudio && \ echo "" From 8d220525b0fc98d6427e60f5fbc652181a79d39d Mon Sep 17 00:00:00 2001 From: Mohith Shrivastava Date: Wed, 6 May 2026 10:58:48 -0400 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20simplify=20CustomerServiceAgent?= =?UTF-8?q?=20recipe=20=E2=80=94=20remove=20before=5Freasoning,=20reduce?= =?UTF-8?q?=20to=203=20subagents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recipe now focuses on demonstrating the Service Agent differentiator (agent_type + default_agent_user) with a clean 3-subagent flow: router → triage → resolution. Drops unused update_case and send_satisfaction_survey actions along with their backing metadata. --- force-app-service/README.md | 6 +- .../CustomerServiceAgent.agent | 443 +++--------------- .../classes/CustomerServiceAgentFlowTest.cls | 90 ---- .../SendSatisfactionSurvey.flow-meta.xml | 79 ---- .../flows/UpdateCase.flow-meta.xml | 87 ---- .../Survey_Log__c.object-meta.xml | 13 - .../fields/Case__c.field-meta.xml | 9 - .../fields/Sent_Date__c.field-meta.xml | 6 - 8 files changed, 74 insertions(+), 659 deletions(-) delete mode 100644 force-app-service/flows/SendSatisfactionSurvey.flow-meta.xml delete mode 100644 force-app-service/flows/UpdateCase.flow-meta.xml delete mode 100644 force-app-service/objects/Survey_Log__c/Survey_Log__c.object-meta.xml delete mode 100644 force-app-service/objects/Survey_Log__c/fields/Case__c.field-meta.xml delete mode 100644 force-app-service/objects/Survey_Log__c/fields/Sent_Date__c.field-meta.xml diff --git a/force-app-service/README.md b/force-app-service/README.md index e74d04d..bce7d4d 100644 --- a/force-app-service/README.md +++ b/force-app-service/README.md @@ -17,6 +17,6 @@ A pre-commit hook automatically restores the placeholder before any commit so or ## Recipes -| Recipe | Description | -| ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| [CustomerServiceAgent](aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) | Complete customer service agent with issue classification, knowledge base resolution, case management, escalation workflows, and satisfaction surveys | +| Recipe | Description | +| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| [CustomerServiceAgent](aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) | Service agent that classifies issues, searches the knowledge base, and creates or escalates cases | diff --git a/force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent b/force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent index e51a0ac..0bb1a91 100644 --- a/force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent +++ b/force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent @@ -1,261 +1,131 @@ # CustomerServiceAgent -# Complete real-world customer service agent +# Service Agent recipe: demonstrates agent_type + default_agent_user config config: developer_name: "CustomerService_Agent" agent_label: "Customer Service Agent" agent_type: "AgentforceServiceAgent" default_agent_user: "__AGENT_USER_PLACEHOLDER__" - description: "Complete customer service agent with issue classification, resolution workflows, and escalation" + description: "Customer service agent that classifies issues, searches the knowledge base, and creates or escalates cases" variables: - # Customer information customer_id: mutable string = "" description: "Unique identifier for the customer" customer_name: mutable string = "" description: "Full name of the customer" - customer_email: mutable string = "" - description: "Email address of the customer" customer_tier: mutable string = "standard" description: "Customer loyalty tier (standard, premium, enterprise)" - - # Issue tracking - issue_type: mutable string = "" - description: "Category of the customer's issue (e.g., billing, technical)" issue_description: mutable string = "" description: "Detailed description of the issue provided by the customer" + issue_type: mutable string = "" + description: "Category of the customer's issue (billing, technical, account, product)" issue_priority: mutable string = "medium" description: "Priority level of the issue (low, medium, high, urgent)" - issue_resolved: mutable boolean = False - description: "Flag indicating if the issue has been resolved" - - # Case management - case_id: mutable string = "" - description: "Salesforce Case ID associated with the interaction" - case_status: mutable string = "new" - description: "Current status of the case (new, in_progress, resolved, escalated, closed)" - - # Knowledge base kb_article_found: mutable boolean = False - description: "Flag indicating if a relevant KB article was found" - kb_article_content: mutable string = "" - description: "Content of the found KB article" - - # Escalation - requires_escalation: mutable boolean = False - description: "Flag indicating if the issue needs escalation to a human agent" - escalation_reason: mutable string = "" - description: "Reason for escalation" - - # Resolution tracking - resolution_steps: mutable object = {} - description: "List of steps taken to resolve the issue" - customer_satisfied: mutable boolean = False - description: "Flag indicating customer satisfaction with the resolution" + description: "Whether a relevant knowledge base article was found" system: - messages: - welcome: "Welcome to Customer Service! I'm here to help resolve your issue quickly." - error: "I encountered an issue processing your request. Please try again or contact support." - - instructions: "You are a customer service agent. Classify issues, provide solutions from the knowledge base, create cases, and escalate when needed. Be empathetic, professional, and solution-oriented." + instructions: "You are a customer service agent. Identify the customer, classify their issue, search the knowledge base for solutions, and create or escalate cases as needed. Be empathetic and solution-oriented." start_agent agent_router: - description: "Welcome customers and begin issue triage and resolution" - + description: "Welcome customers and route to triage" + reasoning: instructions:| - Select the tool that best matches the user's message and conversation history. If it's unclear, make your best guess. + Greet the customer and begin the triage process. actions: begin_triage: @utils.transition to @subagent.triage description: "Start the customer service triage process" -# SUBAGENT 1: Initial Triage +# SUBAGENT 1: Triage subagent triage: - description: "Initial customer interaction and issue triage" + description: "Identify the customer, classify their issue, and search the knowledge base" actions: fetch_customer: - description: "Fetch customer information" + description: "Fetch customer information by their ID" inputs: customer_id: string - description: "The unique identifier of the customer to fetch information for" + description: "The unique identifier of the customer" is_required: True outputs: name: string description: "Customer's full name" email: string - description: "Customer's email address for communication" + description: "Customer's email address" tier: string - description: "Customer tier level (standard, premium, enterprise) determining service priority" + description: "Customer tier level (standard, premium, enterprise)" target: "flow://FetchCustomer" classify_issue: - description: "Classify customer issue type" + description: "Classify the customer's issue type and priority" inputs: issue_description: string - description: "The customer's description of their issue or problem" + description: "The customer's description of their problem" is_required: True outputs: issue_type: string - description: "Classified issue type (e.g., billing, technical, account, product)" + description: "Classified issue type (billing, technical, account, product)" priority: string - description: "Issue priority level (low, medium, high, urgent) based on classification" - suggested_kb_articles: list[string] - description: "List of relevant knowledge base article IDs that may help resolve the issue" + description: "Issue priority level (low, medium, high, urgent)" target: "apex://IssueClassifier" search_knowledge_base: - description: "Search knowledge base for solutions" + description: "Search the knowledge base for relevant solutions" inputs: issue_type: string - description: "The classified type of issue to search solutions for" + description: "The classified type of issue to search for" is_required: True keywords: string - description: "Keywords extracted from the issue description for refined search" + description: "Keywords from the issue description for search" is_required: True outputs: articles: list[string] - description: "List of knowledge base article objects matching the search criteria" + description: "List of matching knowledge base articles" top_article: object - description: "The most relevant knowledge base article object with solution content" + description: "The most relevant article with solution content" target: "flow://SearchKnowledgeBase" - create_case: - description: "Create support case" - inputs: - customer_id: string - description: "The unique identifier of the customer the case is for" - is_required: True - subject: string - description: "Brief subject line summarizing the case" - is_required: True - case_description: string - description: "Detailed description of the customer's issue" - is_required: True - priority: string - description: "Priority level of the case (low, medium, high, urgent)" - is_required: True - issue_type: string - description: "The classified type of issue (billing, technical, account, product)" - is_required: True - outputs: - case_id: string - description: "Unique identifier assigned to the newly created case" - case_number: string - description: "Human-readable case number for customer reference" - target: "flow://CreateCase" - - update_case: - description: "Update case status" - inputs: - case_id: string - description: "The unique identifier of the case to update" - is_required: True - status: string - description: "New status for the case (new, in_progress, resolved, escalated, closed)" - is_required: True - notes: string - description: "Notes or comments about the case update" - is_required: False - outputs: - updated: boolean - description: "Indicates whether the case was updated successfully" - target: "flow://UpdateCase" - - escalate_case: - description: "Escalate case to specialist" - inputs: - case_id: string - description: "The unique identifier of the case to escalate" - is_required: True - reason: string - description: "Reason for escalation (e.g., complex issue, customer request, policy exception)" - is_required: True - specialist_team: string - description: "The specialist team to escalate to based on issue type" - is_required: True - outputs: - escalated: boolean - description: "Indicates whether the case was escalated successfully" - specialist_assigned: string - description: "Name or ID of the specialist assigned to handle the escalated case" - target: "flow://EscalateCase" - - send_satisfaction_survey: - description: "Send customer satisfaction survey" - inputs: - customer_email: string - description: "Email address to send the satisfaction survey to" - is_required: True - case_id: string - description: "The case ID to associate with the survey response" - is_required: True - outputs: - sent: boolean - description: "Indicates whether the satisfaction survey was sent successfully" - target: "flow://SendSatisfactionSurvey" - - - # Load customer data if available - before_reasoning: - if @variables.customer_id and not @variables.customer_name: - run @actions.fetch_customer - with customer_id=@variables.customer_id - set @variables.customer_name = @outputs.name - set @variables.customer_email = @outputs.email - set @variables.customer_tier = @outputs.tier - reasoning: instructions:-> if @variables.customer_name: - | Hello {!@variables.customer_name}! 👋 + | Hello {!@variables.customer_name}! How can I help you today? if not @variables.customer_name: - | Hello! Welcome to Customer Service. 👋 - - | I'm here to help resolve your issue quickly. - + | Hello! Welcome to Customer Service. How can I help you today? + if not @variables.issue_description: | Please describe the issue you're experiencing. - if @variables.issue_description and not @variables.issue_type: - | Let me classify your issue... actions: - # Classify the issue + identify_customer: @actions.fetch_customer + available when @variables.customer_id and not @variables.customer_name + with customer_id=@variables.customer_id + set @variables.customer_name = @outputs.name + set @variables.customer_tier = @outputs.tier + classify_customer_issue: @actions.classify_issue available when @variables.issue_description and not @variables.issue_type with issue_description=@variables.issue_description set @variables.issue_type = @outputs.issue_type set @variables.issue_priority = @outputs.priority - # Transition to resolution workflow + + search_kb: @actions.search_knowledge_base + available when @variables.issue_type and not @variables.kb_article_found + with issue_type=@variables.issue_type + with keywords=@variables.issue_description + set @variables.kb_article_found = True transition to @subagent.resolution -# SUBAGENT 2: Resolution Workflow +# SUBAGENT 2: Resolution subagent resolution: - description: "Attempt to resolve issue using knowledge base" + description: "Create a case or escalate based on knowledge base results" actions: - search_knowledge_base: - description: "Search knowledge base for solutions" - inputs: - issue_type: string - description: "The classified type of issue to search solutions for" - is_required: True - keywords: string - description: "Keywords extracted from the issue description for refined search" - is_required: True - outputs: - articles: list[string] - description: "List of knowledge base article objects matching the search criteria" - top_article: object - description: "The most relevant knowledge base article object with solution content" - target: "flow://SearchKnowledgeBase" - create_case: - description: "Create support case" + description: "Create a support case for the customer" inputs: customer_id: string - description: "The unique identifier of the customer the case is for" + description: "The unique identifier of the customer" is_required: True subject: string description: "Brief subject line summarizing the case" @@ -264,230 +134,59 @@ subagent resolution: description: "Detailed description of the customer's issue" is_required: True priority: string - description: "Priority level of the case (low, medium, high, urgent)" + description: "Priority level (low, medium, high, urgent)" is_required: True issue_type: string - description: "The classified type of issue (billing, technical, account, product)" + description: "The classified issue type (billing, technical, account, product)" is_required: True outputs: case_id: string - description: "Unique identifier assigned to the newly created case" + description: "Unique identifier of the created case" case_number: string - description: "Human-readable case number for customer reference" + description: "Human-readable case number for reference" target: "flow://CreateCase" - update_case: - description: "Update case status" - inputs: - case_id: string - description: "The unique identifier of the case to update" - is_required: True - status: string - description: "New status for the case (new, in_progress, resolved, escalated, closed)" - is_required: True - notes: string - description: "Notes or comments about the case update" - is_required: False - outputs: - updated: boolean - description: "Indicates whether the case was updated successfully" - target: "flow://UpdateCase" - - reasoning: - instructions:-> - | Issue Classification: - - Type: {!@variables.issue_type} - - Priority: {!@variables.issue_priority} - - if @variables.customer_tier == "premium": - | (Premium customer - prioritizing resolution) - - | Searching knowledge base for solutions... - - actions: - # Search knowledge base - find_solution: @actions.search_knowledge_base - available when @variables.issue_type and not @variables.kb_article_found - with issue_type=@variables.issue_type - with keywords=@variables.issue_description - set @variables.kb_article_found = True - set @variables.kb_article_content = @outputs.top_article - - # Create case for tracking - run @actions.create_case - with customer_id=@variables.customer_id - with subject="{!@variables.issue_type}: {!@variables.issue_description}" - with case_description=@variables.issue_description - with priority=@variables.issue_priority - with issue_type=@variables.issue_type - set @variables.case_id = @outputs.case_id - set @variables.case_status = "in_progress" - - # If KB solution found, present it - present_solution: @utils.transition to @subagent.solution_presentation - available when @variables.kb_article_found - - # If no KB solution, escalate - escalate_to_specialist: @utils.transition to @subagent.escalation - available when not @variables.kb_article_found - -# SUBAGENT 3: Solution Presentation -subagent solution_presentation: - description: "Present solution and verify resolution" - - actions: - update_case: - description: "Update case status" - inputs: - case_id: string - description: "The unique identifier of the case to update" - is_required: True - status: string - description: "New status for the case (new, in_progress, resolved, escalated, closed)" - is_required: True - notes: string - description: "Notes or comments about the case update" - is_required: False - outputs: - updated: boolean - description: "Indicates whether the case was updated successfully" - target: "flow://UpdateCase" - - reasoning: - instructions:-> - | I found a solution for your {!@variables.issue_type} issue! - - {!@variables.kb_article_content} - - Does this resolve your issue? - - actions: - # Mark resolved - mark_resolved: @actions.update_case - with case_id=@variables.case_id - with status="resolved" - with notes="Resolved using KB article" - set @variables.issue_resolved = True - set @variables.case_status = "resolved" - # Transition to closure - transition to @subagent.closure - - # Need more help - escalate - need_more_help: @utils.transition to @subagent.escalation - -# SUBAGENT 4: Escalation -subagent escalation: - description: "Escalate complex issues to specialists" - - actions: escalate_case: - description: "Escalate case to specialist" + description: "Escalate a complex issue to a specialist team" inputs: case_id: string description: "The unique identifier of the case to escalate" is_required: True reason: string - description: "Reason for escalation (e.g., complex issue, customer request, policy exception)" + description: "Reason for escalation" is_required: True specialist_team: string - description: "The specialist team to escalate to based on issue type" + description: "The specialist team to escalate to" is_required: True outputs: escalated: boolean - description: "Indicates whether the case was escalated successfully" + description: "Whether the case was escalated successfully" specialist_assigned: string - description: "Name or ID of the specialist assigned to handle the escalated case" + description: "Name of the assigned specialist" target: "flow://EscalateCase" - update_case: - description: "Update case status" - inputs: - case_id: string - description: "The unique identifier of the case to update" - is_required: True - status: string - description: "New status for the case (new, in_progress, resolved, escalated, closed)" - is_required: True - notes: string - description: "Notes or comments about the case update" - is_required: False - outputs: - updated: boolean - description: "Indicates whether the case was updated successfully" - target: "flow://UpdateCase" - reasoning: instructions:-> - | This issue requires specialist attention. - - if @variables.issue_priority == "high" or @variables.issue_priority == "urgent": - | Due to the high priority, I'm escalating this immediately. - if @variables.customer_tier == "premium": - | As a premium customer, connecting you with a senior specialist. - - | Creating escalation... - - actions: - escalate_issue: @actions.escalate_case - with case_id=@variables.case_id - with reason=@variables.escalation_reason - with specialist_team=@variables.issue_type - set @variables.requires_escalation = True - - # Update case - run @actions.update_case - with case_id=@variables.case_id - with status="escalated" - with notes="Escalated to {!@variables.issue_type} team" - set @variables.case_status = "escalated" - - # Transition to closure - transition to @subagent.closure + if @variables.kb_article_found: + | I found a knowledge base article that may help resolve your {!@variables.issue_type} issue. + Let me create a case to track this. + if not @variables.kb_article_found: + | I couldn't find a matching solution. Let me escalate this to a specialist. -# SUBAGENT 5: Closure -subagent closure: - description: "Close conversation and collect feedback" - - actions: - send_satisfaction_survey: - description: "Send customer satisfaction survey" - inputs: - customer_email: string - description: "Email address to send the satisfaction survey to" - is_required: True - case_id: string - description: "The case ID to associate with the survey response" - is_required: True - outputs: - sent: boolean - description: "Indicates whether the satisfaction survey was sent successfully" - target: "flow://SendSatisfactionSurvey" - - reasoning: - instructions:-> - if @variables.issue_resolved: - | ✅ Great! Your issue has been resolved. - Case ID: {!@variables.case_id} - - I'm sending a satisfaction survey to {!@variables.customer_email}. - if @variables.requires_escalation: - | Your case ({!@variables.case_id}) has been escalated to our specialist team. - They will contact you within: - if @variables.issue_priority == "urgent": - | - 1 hour - if @variables.issue_priority == "high": - | - 4 hours - if @variables.issue_priority == "medium": - | - 24 hours - - | Is there anything else I can help you with? + if @variables.customer_tier == "premium": + | (Priority handling for premium customer) actions: - # Send satisfaction survey - send_survey: @actions.send_satisfaction_survey - available when @variables.issue_resolved - with customer_email=@variables.customer_email - with case_id=@variables.case_id + create_support_case: @actions.create_case + available when @variables.kb_article_found + with customer_id=@variables.customer_id + with subject="{!@variables.issue_type}: {!@variables.issue_description}" + with case_description=@variables.issue_description + with priority=@variables.issue_priority + with issue_type=@variables.issue_type - # Start new issue - new_issue: @utils.transition to @subagent.triage + escalate_to_specialist: @actions.escalate_case + available when not @variables.kb_article_found + with case_id="" + with reason="No knowledge base solution found for {!@variables.issue_type} issue" + with specialist_team=@variables.issue_type diff --git a/force-app-service/classes/CustomerServiceAgentFlowTest.cls b/force-app-service/classes/CustomerServiceAgentFlowTest.cls index 6138acf..a51ad7a 100644 --- a/force-app-service/classes/CustomerServiceAgentFlowTest.cls +++ b/force-app-service/classes/CustomerServiceAgentFlowTest.cls @@ -90,54 +90,6 @@ private class CustomerServiceAgentFlowTest { Assert.areEqual('Technical', createdCase.Type, 'Type should match'); } - @IsTest - static void testUpdateCaseSuccess() { - // Create a test case first - Contact testContact = [ - SELECT Id - FROM Contact - WHERE Customer_ID__c = 'CUST-12345' - LIMIT 1 - ]; - Case testCase = new Case( - Subject = 'Original Subject', - Description = 'Original Description', - ContactId = testContact.Id, - Status = 'New' - ); - insert testCase; - - // Set input variables for the flow - Map inputs = new Map(); - inputs.put('case_id', testCase.Id); - inputs.put('status', 'In Progress'); - inputs.put('notes', 'Updated notes'); - - // Run the flow - Test.startTest(); - Flow.Interview.UpdateCase flowInterview = new Flow.Interview.UpdateCase( - inputs - ); - flowInterview.start(); - Test.stopTest(); - - // Verify the expected outcome - Boolean updated = (Boolean) flowInterview.getVariableValue('updated'); - Assert.areEqual(true, updated, 'Update should be successful'); - - // Verify case was updated - Case updatedCase = [ - SELECT Id, Status - FROM Case - WHERE Id = :testCase.Id - ]; - Assert.areEqual( - 'In Progress', - updatedCase.Status, - 'Status should be updated' - ); - } - @IsTest static void testSearchKnowledgeBase() { // Set input variables for the flow @@ -221,46 +173,4 @@ private class CustomerServiceAgentFlowTest { 'Status should be Escalated' ); } - - @IsTest - static void testSendSatisfactionSurvey() { - // Create a test case first - Contact testContact = [ - SELECT Id - FROM Contact - WHERE Customer_ID__c = 'CUST-12345' - LIMIT 1 - ]; - Case testCase = new Case( - Subject = 'Test Case', - ContactId = testContact.Id, - Status = 'Closed' - ); - insert testCase; - - // Set input variables for the flow - Map inputs = new Map(); - inputs.put('case_id', testCase.Id); - inputs.put('customer_email', 'test@example.com'); - - // Run the flow - Test.startTest(); - Flow.Interview.SendSatisfactionSurvey flowInterview = new Flow.Interview.SendSatisfactionSurvey( - inputs - ); - flowInterview.start(); - Test.stopTest(); - - // Verify the expected outcome - Boolean sent = (Boolean) flowInterview.getVariableValue('sent'); - Assert.areEqual(true, sent, 'Survey should be sent'); - - // Verify survey log was created - List surveyLogs = [ - SELECT Id - FROM ASR_Survey_Log__c - WHERE Case__c = :testCase.Id - ]; - Assert.areEqual(1, surveyLogs.size(), 'Survey log should be created'); - } } diff --git a/force-app-service/flows/SendSatisfactionSurvey.flow-meta.xml b/force-app-service/flows/SendSatisfactionSurvey.flow-meta.xml deleted file mode 100644 index c9c48bb..0000000 --- a/force-app-service/flows/SendSatisfactionSurvey.flow-meta.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - 66.0 - Default - SendSatisfactionSurvey {!$Flow.CurrentDateTime} - - - BuilderType - - LightningFlowBuilder - - - AutoLaunchedFlow - - 50 - 0 - - Create_Survey_Log - - - Active - - Create_Survey_Log - - 176 - 134 - - Assign_Result - - - Case__c - - case_id - - - - Sent_Date__c - - $Flow.CurrentDateTime - - - ASR_Survey_Log__c - - - Assign_Result - - 176 - 242 - - sent - Assign - - true - - - - - customer_email - String - false - true - false - - - case_id - String - false - true - false - - - sent - Boolean - false - false - true - - diff --git a/force-app-service/flows/UpdateCase.flow-meta.xml b/force-app-service/flows/UpdateCase.flow-meta.xml deleted file mode 100644 index fc8ee53..0000000 --- a/force-app-service/flows/UpdateCase.flow-meta.xml +++ /dev/null @@ -1,87 +0,0 @@ - - - 66.0 - Default - UpdateCase {!$Flow.CurrentDateTime} - - - BuilderType - - LightningFlowBuilder - - - AutoLaunchedFlow - - 50 - 0 - - Update_Case_Record - - - Active - - Update_Case_Record - - 176 - 134 - - Assign_Result - - and - - Id - EqualTo - - case_id - - - - Status - - status - - - Case - - - Assign_Result - - 176 - 242 - - updated - Assign - - true - - - - - case_id - String - false - true - false - - - status - String - false - true - false - - - notes - String - false - true - false - - - updated - Boolean - false - false - true - - diff --git a/force-app-service/objects/Survey_Log__c/Survey_Log__c.object-meta.xml b/force-app-service/objects/Survey_Log__c/Survey_Log__c.object-meta.xml deleted file mode 100644 index dc7c22d..0000000 --- a/force-app-service/objects/Survey_Log__c/Survey_Log__c.object-meta.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - Deployed - Survey Log for Customer Service Agent - - Survey Logs - ReadWrite - - - AutoNumber - SL-{0000} - - diff --git a/force-app-service/objects/Survey_Log__c/fields/Case__c.field-meta.xml b/force-app-service/objects/Survey_Log__c/fields/Case__c.field-meta.xml deleted file mode 100644 index 8d2884c..0000000 --- a/force-app-service/objects/Survey_Log__c/fields/Case__c.field-meta.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - Case__c - - Lookup - Case - Survey Logs - Survey_Logs - diff --git a/force-app-service/objects/Survey_Log__c/fields/Sent_Date__c.field-meta.xml b/force-app-service/objects/Survey_Log__c/fields/Sent_Date__c.field-meta.xml deleted file mode 100644 index 8c94faa..0000000 --- a/force-app-service/objects/Survey_Log__c/fields/Sent_Date__c.field-meta.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - Sent_Date__c - - DateTime - From 06271cc7c98dfb10d32edadb7cf876c6270f4801 Mon Sep 17 00:00:00 2001 From: Mohith Shrivastava Date: Wed, 6 May 2026 11:01:01 -0400 Subject: [PATCH 04/14] fix: align agent_router instructions with repo-wide pattern --- .../CustomerServiceAgent/CustomerServiceAgent.agent | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent b/force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent index 0bb1a91..f9df8fd 100644 --- a/force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent +++ b/force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent @@ -32,7 +32,7 @@ start_agent agent_router: reasoning: instructions:| - Greet the customer and begin the triage process. + Select the tool that best matches the user's message and conversation history. If it's unclear, make your best guess. actions: begin_triage: @utils.transition to @subagent.triage description: "Start the customer service triage process" From 6901ec1c07088426112eafb8dfd9496c93b573ae Mon Sep 17 00:00:00 2001 From: Mohith Shrivastava Date: Wed, 6 May 2026 11:05:20 -0400 Subject: [PATCH 05/14] feat: reorganize force-app-service into recipe subfolder with README Moves all CustomerServiceAgent metadata into force-app-service/customerServiceAgent/ to match the per-recipe folder structure used in force-app/main/. Adds a detailed recipe README and updates all path references in CI, install scripts, and AGENTS.md. --- .github/workflows/ci-pr.yml | 2 +- .github/workflows/ci.yml | 2 +- AGENTS.md | 2 +- bin/install-scratch.bat | 2 +- bin/install-scratch.sh | 2 +- force-app-service/README.md | 6 +- .../customerServiceAgent/README.md | 194 ++++++++++++++++++ .../CustomerServiceAgent.agent | 0 .../CustomerServiceAgent.bundle-meta.xml | 0 .../classes/CustomerServiceAgentFlowTest.cls | 0 .../CustomerServiceAgentFlowTest.cls-meta.xml | 0 .../classes/IssueClassifier.cls | 0 .../classes/IssueClassifier.cls-meta.xml | 0 .../classes/IssueClassifierTest.cls | 0 .../classes/IssueClassifierTest.cls-meta.xml | 0 .../flows/CreateCase.flow-meta.xml | 0 .../flows/EscalateCase.flow-meta.xml | 0 .../flows/FetchCustomer.flow-meta.xml | 0 .../flows/SearchKnowledgeBase.flow-meta.xml | 0 .../Escalation_Reason__c.field-meta.xml | 0 .../fields/Customer_ID__c.field-meta.xml | 0 .../fields/Loyalty_Tier__c.field-meta.xml | 0 22 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 force-app-service/customerServiceAgent/README.md rename force-app-service/{ => customerServiceAgent}/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent (100%) rename force-app-service/{ => customerServiceAgent}/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.bundle-meta.xml (100%) rename force-app-service/{ => customerServiceAgent}/classes/CustomerServiceAgentFlowTest.cls (100%) rename force-app-service/{ => customerServiceAgent}/classes/CustomerServiceAgentFlowTest.cls-meta.xml (100%) rename force-app-service/{ => customerServiceAgent}/classes/IssueClassifier.cls (100%) rename force-app-service/{ => customerServiceAgent}/classes/IssueClassifier.cls-meta.xml (100%) rename force-app-service/{ => customerServiceAgent}/classes/IssueClassifierTest.cls (100%) rename force-app-service/{ => customerServiceAgent}/classes/IssueClassifierTest.cls-meta.xml (100%) rename force-app-service/{ => customerServiceAgent}/flows/CreateCase.flow-meta.xml (100%) rename force-app-service/{ => customerServiceAgent}/flows/EscalateCase.flow-meta.xml (100%) rename force-app-service/{ => customerServiceAgent}/flows/FetchCustomer.flow-meta.xml (100%) rename force-app-service/{ => customerServiceAgent}/flows/SearchKnowledgeBase.flow-meta.xml (100%) rename force-app-service/{ => customerServiceAgent}/objects/Case/fields/Escalation_Reason__c.field-meta.xml (100%) rename force-app-service/{ => customerServiceAgent}/objects/Contact/fields/Customer_ID__c.field-meta.xml (100%) rename force-app-service/{ => customerServiceAgent}/objects/Contact/fields/Loyalty_Tier__c.field-meta.xml (100%) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 0a91282..bb06f49 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -201,7 +201,7 @@ jobs: # Assign permission set to agent user - name: 'Assign permission set to agent user' run: | - agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) + agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of "$agent_user" -o scratch-org # Deploy service agent source to scratch org diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b707cfe..046c3f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -141,7 +141,7 @@ jobs: # Assign permission set to agent user - name: 'Assign permission set to agent user' run: | - agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) + agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of "$agent_user" -o scratch-org # Deploy service agent source to scratch org diff --git a/AGENTS.md b/AGENTS.md index d857384..6f4d73a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,6 @@ Detailed rules and guidelines are in the `.airules/` directory. Read the relevan - **Employee Agent scripts:** `force-app/**/aiAuthoringBundles/**/*.agent` - **Service Agent scripts:** `force-app-service/**/aiAuthoringBundles/**/*.agent` -- **Apex services:** `force-app/**/classes/*.cls` and `force-app-service/classes/*.cls` +- **Apex services:** `force-app/**/classes/*.cls` and `force-app-service/**/classes/*.cls` - **Recipes:** each subdirectory under `force-app/main/` is a self-contained recipe - **Service Agent recipes:** `force-app-service/` contains service agent examples that require a `default_agent_user` diff --git a/bin/install-scratch.bat b/bin/install-scratch.bat index d0f87e9..2bdd0ce 100755 --- a/bin/install-scratch.bat +++ b/bin/install-scratch.bat @@ -47,7 +47,7 @@ call :checkForError @echo: echo Assigning permission set to agent user... -for /f "tokens=*" %%a in ('node -e "const fs=require(\"fs\"),p=require(\"path\");const f=fs.readFileSync(p.resolve(\"force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent\"),\"utf8\");const m=f.match(/default_agent_user:\s*\"([^_][^\"]+)\"/);if(m)process.stdout.write(m[1])"') do set AGENT_USER=%%a +for /f "tokens=*" %%a in ('node -e "const fs=require(\"fs\"),p=require(\"path\");const f=fs.readFileSync(p.resolve(\"force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent\"),\"utf8\");const m=f.match(/default_agent_user:\s*\"([^_][^\"]+)\"/);if(m)process.stdout.write(m[1])"') do set AGENT_USER=%%a cmd.exe /c sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of %AGENT_USER% call :checkForError @echo: diff --git a/bin/install-scratch.sh b/bin/install-scratch.sh index 185ee00..aab8afc 100755 --- a/bin/install-scratch.sh +++ b/bin/install-scratch.sh @@ -40,7 +40,7 @@ node bin/setup-service-agent.js && \ echo "" && \ echo "Assigning permission set to agent user..." && \ -agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) && \ +agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) && \ sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of "$agent_user" && \ echo "" && \ diff --git a/force-app-service/README.md b/force-app-service/README.md index bce7d4d..7a2a26a 100644 --- a/force-app-service/README.md +++ b/force-app-service/README.md @@ -17,6 +17,6 @@ A pre-commit hook automatically restores the placeholder before any commit so or ## Recipes -| Recipe | Description | -| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | -| [CustomerServiceAgent](aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) | Service agent that classifies issues, searches the knowledge base, and creates or escalates cases | +| Recipe | Description | +| --------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| [CustomerServiceAgent](customerServiceAgent/) | Service agent that classifies issues, searches the knowledge base, and creates or escalates cases | diff --git a/force-app-service/customerServiceAgent/README.md b/force-app-service/customerServiceAgent/README.md new file mode 100644 index 0000000..4c2213e --- /dev/null +++ b/force-app-service/customerServiceAgent/README.md @@ -0,0 +1,194 @@ +# CustomerServiceAgent + +## Overview + +This recipe demonstrates how to build an **Agentforce Service Agent** — an external-facing agent that runs under a dedicated agent user rather than the logged-in user. The key differentiator from Employee Agents is the `agent_type: "AgentforceServiceAgent"` and `default_agent_user` configuration, which requires an org-specific deployment pipeline. + +## Agent Flow + +```mermaid +%%{init: {'theme':'neutral'}}%% +graph TD + A[Start] --> B[start_agent: agent_router] + B --> C[Transition to triage] + C --> D[triage subagent] + D --> E{Customer ID known?} + E -->|Yes| F[fetch_customer] + E -->|No| G[Ask for issue] + F --> G + G --> H[classify_issue] + H --> I[search_knowledge_base] + I --> J[Transition to resolution] + J --> K[resolution subagent] + K --> L{KB article found?} + L -->|Yes| M[create_case] + L -->|No| N[escalate_case] +``` + +## Key Concepts + +- **Service Agent config**: `agent_type: "AgentforceServiceAgent"` and `default_agent_user` field +- **Placeholder-based deployment**: Source uses `__AGENT_USER_PLACEHOLDER__` replaced at deploy time +- **Subagent transitions**: Router → triage → resolution flow +- **Mixed action targets**: Flows (`flow://`) and Apex (`apex://`) in the same agent + +## How It Works + +### Service Agent Configuration + +Unlike Employee Agents, Service Agents require a dedicated user to run as: + +```agentscript +config: + developer_name: "CustomerService_Agent" + agent_label: "Customer Service Agent" + agent_type: "AgentforceServiceAgent" + default_agent_user: "__AGENT_USER_PLACEHOLDER__" +``` + +The placeholder is replaced by `npm run setup:service-agent` before deployment. + +### Triage Subagent + +Handles customer identification, issue classification, and knowledge base search: + +```agentscript +subagent triage: + actions: + fetch_customer: ... + target: "flow://FetchCustomer" + + classify_issue: ... + target: "apex://IssueClassifier" + + search_knowledge_base: ... + target: "flow://SearchKnowledgeBase" + + reasoning: + actions: + identify_customer: @actions.fetch_customer + available when @variables.customer_id and not @variables.customer_name + + classify_customer_issue: @actions.classify_issue + available when @variables.issue_description and not @variables.issue_type + + search_kb: @actions.search_knowledge_base + available when @variables.issue_type and not @variables.kb_article_found + transition to @subagent.resolution +``` + +### Resolution Subagent + +Routes to case creation or escalation based on KB results: + +```agentscript +subagent resolution: + reasoning: + actions: + create_support_case: @actions.create_case + available when @variables.kb_article_found + + escalate_to_specialist: @actions.escalate_case + available when not @variables.kb_article_found +``` + +## Try It Out + +### Example: Issue Resolved via Knowledge Base + +```text +Agent: Hello! Welcome to Customer Service. How can I help you today? + +User: My account is CUST-12345. I can't reset my password. + +[fetch_customer → name="Customer", tier="Premium"] +[classify_issue → type="technical", priority="high"] +[search_knowledge_base → kb_article_found=True] +[transitions to resolution] + +Agent: I found a knowledge base article that may help resolve your technical issue. + Let me create a case to track this. + +[create_case → case_id="500xx...", case_number="00001234"] + +Agent: I've created case 00001234 for your password reset issue. + Here's the solution from our knowledge base: ... +``` + +### Example: Issue Escalated + +```text +Agent: Hello! Welcome to Customer Service. How can I help you today? + +User: I'm being charged incorrectly on my enterprise contract. + +[classify_issue → type="billing", priority="high"] +[search_knowledge_base → kb_article_found=False (no match)] +[transitions to resolution] + +Agent: I couldn't find a matching solution. Let me escalate this to a specialist. + +[escalate_case → escalated=True, specialist_assigned="Billing Support"] + +Agent: I've escalated your billing issue to our specialist team. + They will reach out to you shortly. +``` + +## Deployment + +This recipe requires additional setup compared to Employee Agent recipes: + +1. `npm run setup:service-agent` — creates the agent user and replaces the placeholder in all `.agent` files +2. `sf project deploy start --source-dir force-app-service` — deploys the metadata +3. `sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of ` — grants data access + +A pre-commit hook automatically restores the placeholder before commits. + +## What's Next + +- **MultiSubagentNavigation**: More complex multi-subagent routing patterns +- **OpenGateRouter**: Deterministic gate-based routing with authentication +- **ActionChaining**: Sequential action execution patterns + +## Testing + +### Stub Flow Behavior + +The included flows return hardcoded values for testing: + +| Flow | Always Returns | +| --------------------- | --------------------------------------------------------------- | +| `FetchCustomer` | `name="Customer"`, `email="test@example.com"`, `tier="Premium"` | +| `SearchKnowledgeBase` | `articles=["..."]`, `top_article={...}` | +| `CreateCase` | `case_id="500..."`, `case_number="00001234"` | +| `EscalateCase` | `escalated=true`, `specialist_assigned="Technical Support"` | + +### Test 1: Happy Path — KB Resolution + +```text +User: My account is CUST-12345. I can't log in. + +Agent: [fetches customer, classifies issue, searches KB, creates case] +``` + +**Expected**: All three triage actions fire in sequence, then transitions to resolution and creates a case. + +### Test 2: Escalation Path — No KB Match + +```text +User: I need a custom enterprise pricing adjustment. + +Agent: [classifies issue, searches KB (no match), escalates] +``` + +**Expected**: After KB search returns no relevant articles, the agent escalates instead of creating a standard case. + +### Test 3: Unknown Customer + +```text +User: I need help with my billing. + +Agent: [classifies issue directly, skips fetch_customer since no customer_id] +``` + +**Expected**: `fetch_customer` is not called because `customer_id` is empty. Agent proceeds with classification. diff --git a/force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent b/force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent similarity index 100% rename from force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent rename to force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent diff --git a/force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.bundle-meta.xml b/force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.bundle-meta.xml similarity index 100% rename from force-app-service/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.bundle-meta.xml rename to force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.bundle-meta.xml diff --git a/force-app-service/classes/CustomerServiceAgentFlowTest.cls b/force-app-service/customerServiceAgent/classes/CustomerServiceAgentFlowTest.cls similarity index 100% rename from force-app-service/classes/CustomerServiceAgentFlowTest.cls rename to force-app-service/customerServiceAgent/classes/CustomerServiceAgentFlowTest.cls diff --git a/force-app-service/classes/CustomerServiceAgentFlowTest.cls-meta.xml b/force-app-service/customerServiceAgent/classes/CustomerServiceAgentFlowTest.cls-meta.xml similarity index 100% rename from force-app-service/classes/CustomerServiceAgentFlowTest.cls-meta.xml rename to force-app-service/customerServiceAgent/classes/CustomerServiceAgentFlowTest.cls-meta.xml diff --git a/force-app-service/classes/IssueClassifier.cls b/force-app-service/customerServiceAgent/classes/IssueClassifier.cls similarity index 100% rename from force-app-service/classes/IssueClassifier.cls rename to force-app-service/customerServiceAgent/classes/IssueClassifier.cls diff --git a/force-app-service/classes/IssueClassifier.cls-meta.xml b/force-app-service/customerServiceAgent/classes/IssueClassifier.cls-meta.xml similarity index 100% rename from force-app-service/classes/IssueClassifier.cls-meta.xml rename to force-app-service/customerServiceAgent/classes/IssueClassifier.cls-meta.xml diff --git a/force-app-service/classes/IssueClassifierTest.cls b/force-app-service/customerServiceAgent/classes/IssueClassifierTest.cls similarity index 100% rename from force-app-service/classes/IssueClassifierTest.cls rename to force-app-service/customerServiceAgent/classes/IssueClassifierTest.cls diff --git a/force-app-service/classes/IssueClassifierTest.cls-meta.xml b/force-app-service/customerServiceAgent/classes/IssueClassifierTest.cls-meta.xml similarity index 100% rename from force-app-service/classes/IssueClassifierTest.cls-meta.xml rename to force-app-service/customerServiceAgent/classes/IssueClassifierTest.cls-meta.xml diff --git a/force-app-service/flows/CreateCase.flow-meta.xml b/force-app-service/customerServiceAgent/flows/CreateCase.flow-meta.xml similarity index 100% rename from force-app-service/flows/CreateCase.flow-meta.xml rename to force-app-service/customerServiceAgent/flows/CreateCase.flow-meta.xml diff --git a/force-app-service/flows/EscalateCase.flow-meta.xml b/force-app-service/customerServiceAgent/flows/EscalateCase.flow-meta.xml similarity index 100% rename from force-app-service/flows/EscalateCase.flow-meta.xml rename to force-app-service/customerServiceAgent/flows/EscalateCase.flow-meta.xml diff --git a/force-app-service/flows/FetchCustomer.flow-meta.xml b/force-app-service/customerServiceAgent/flows/FetchCustomer.flow-meta.xml similarity index 100% rename from force-app-service/flows/FetchCustomer.flow-meta.xml rename to force-app-service/customerServiceAgent/flows/FetchCustomer.flow-meta.xml diff --git a/force-app-service/flows/SearchKnowledgeBase.flow-meta.xml b/force-app-service/customerServiceAgent/flows/SearchKnowledgeBase.flow-meta.xml similarity index 100% rename from force-app-service/flows/SearchKnowledgeBase.flow-meta.xml rename to force-app-service/customerServiceAgent/flows/SearchKnowledgeBase.flow-meta.xml diff --git a/force-app-service/objects/Case/fields/Escalation_Reason__c.field-meta.xml b/force-app-service/customerServiceAgent/objects/Case/fields/Escalation_Reason__c.field-meta.xml similarity index 100% rename from force-app-service/objects/Case/fields/Escalation_Reason__c.field-meta.xml rename to force-app-service/customerServiceAgent/objects/Case/fields/Escalation_Reason__c.field-meta.xml diff --git a/force-app-service/objects/Contact/fields/Customer_ID__c.field-meta.xml b/force-app-service/customerServiceAgent/objects/Contact/fields/Customer_ID__c.field-meta.xml similarity index 100% rename from force-app-service/objects/Contact/fields/Customer_ID__c.field-meta.xml rename to force-app-service/customerServiceAgent/objects/Contact/fields/Customer_ID__c.field-meta.xml diff --git a/force-app-service/objects/Contact/fields/Loyalty_Tier__c.field-meta.xml b/force-app-service/customerServiceAgent/objects/Contact/fields/Loyalty_Tier__c.field-meta.xml similarity index 100% rename from force-app-service/objects/Contact/fields/Loyalty_Tier__c.field-meta.xml rename to force-app-service/customerServiceAgent/objects/Contact/fields/Loyalty_Tier__c.field-meta.xml From a4a9a4c55e24136c8ce981fb49a298f9737ce2ab Mon Sep 17 00:00:00 2001 From: Mohith Shrivastava Date: Wed, 6 May 2026 11:39:17 -0400 Subject: [PATCH 06/14] fix: add @utils.setVariables to populate variables from conversation Without collect_info, the available when guards on all triage actions stayed permanently locked because nothing wrote user input into variables. The LLM now calls collect_info first to extract customer_id and issue_description, which unlocks downstream actions. --- .../CustomerServiceAgent/CustomerServiceAgent.agent | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent b/force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent index f9df8fd..5f920d3 100644 --- a/force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent +++ b/force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent @@ -82,7 +82,7 @@ subagent triage: outputs: articles: list[string] description: "List of matching knowledge base articles" - top_article: object + top_article: string description: "The most relevant article with solution content" target: "flow://SearchKnowledgeBase" @@ -94,9 +94,15 @@ subagent triage: | Hello! Welcome to Customer Service. How can I help you today? if not @variables.issue_description: - | Please describe the issue you're experiencing. + | First, use {!@actions.collect_info} to save the customer ID and issue description from the conversation. If the user has not yet described their issue, ask them to do so. actions: + collect_info: @utils.setVariables + description: "Save the customer ID and issue description from the conversation" + available when not @variables.issue_description + with customer_id=... + with issue_description=... + identify_customer: @actions.fetch_customer available when @variables.customer_id and not @variables.customer_name with customer_id=@variables.customer_id From e96e21049cea2e50e24dd3e8f79531a43a395567 Mon Sep 17 00:00:00 2001 From: Mohith Shrivastava Date: Wed, 6 May 2026 14:37:29 -0400 Subject: [PATCH 07/14] feat: feedback from testing --- .../CustomerServiceAgent.agent | 69 +++--- .../classes/IssueClassifier.cls | 46 +++- .../classes/IssueClassifierTest.cls | 232 ++++++------------ .../flows/CreateCase.flow-meta.xml | 130 +++------- .../flows/EscalateCase.flow-meta.xml | 102 +++----- .../flows/FetchCustomer.flow-meta.xml | 74 +++--- .../flows/SearchKnowledgeBase.flow-meta.xml | 60 +++-- ...Script_Recipes_Data.permissionset-meta.xml | 14 ++ 8 files changed, 296 insertions(+), 431 deletions(-) diff --git a/force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent b/force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent index 5f920d3..1b9ecc4 100644 --- a/force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent +++ b/force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent @@ -23,6 +23,8 @@ variables: description: "Priority level of the issue (low, medium, high, urgent)" kb_article_found: mutable boolean = False description: "Whether a relevant knowledge base article was found" + kb_article_content: mutable string = "" + description: "Content of the top knowledge base article found" system: instructions: "You are a customer service agent. Identify the customer, classify their issue, search the knowledge base for solutions, and create or escalate cases as needed. Be empathetic and solution-oriented." @@ -60,11 +62,11 @@ subagent triage: classify_issue: description: "Classify the customer's issue type and priority" inputs: - issue_description: string + issueDescription: string description: "The customer's description of their problem" is_required: True outputs: - issue_type: string + issueType: string description: "Classified issue type (billing, technical, account, product)" priority: string description: "Issue priority level (low, medium, high, urgent)" @@ -88,13 +90,32 @@ subagent triage: reasoning: instructions:-> + if @variables.issue_description and @variables.customer_id and not @variables.customer_name: + run @actions.fetch_customer + with customer_id=@variables.customer_id + set @variables.customer_name = @outputs.name + set @variables.customer_tier = @outputs.tier + if @variables.issue_description and not @variables.issue_type: + run @actions.classify_issue + with issueDescription=@variables.issue_description + set @variables.issue_type = @outputs.issueType + set @variables.issue_priority = @outputs.priority + if @variables.issue_type and not @variables.kb_article_found: + run @actions.search_knowledge_base + with issue_type=@variables.issue_type + with keywords=@variables.issue_description + set @variables.kb_article_found = True + set @variables.kb_article_content = @outputs.top_article + if @variables.customer_name: | Hello {!@variables.customer_name}! How can I help you today? if not @variables.customer_name: | Hello! Welcome to Customer Service. How can I help you today? if not @variables.issue_description: - | First, use {!@actions.collect_info} to save the customer ID and issue description from the conversation. If the user has not yet described their issue, ask them to do so. + | Use {!@actions.collect_info} to save whatever the user provided. Extract customer_id if mentioned (otherwise leave empty) and issue_description from the conversation. If the user has not described any issue at all, ask them to do so. + if @variables.kb_article_found: + | Triage is complete. Inform the customer their issue has been identified and you are resolving it. actions: collect_info: @utils.setVariables @@ -103,24 +124,9 @@ subagent triage: with customer_id=... with issue_description=... - identify_customer: @actions.fetch_customer - available when @variables.customer_id and not @variables.customer_name - with customer_id=@variables.customer_id - set @variables.customer_name = @outputs.name - set @variables.customer_tier = @outputs.tier - - classify_customer_issue: @actions.classify_issue - available when @variables.issue_description and not @variables.issue_type - with issue_description=@variables.issue_description - set @variables.issue_type = @outputs.issue_type - set @variables.issue_priority = @outputs.priority - - search_kb: @actions.search_knowledge_base - available when @variables.issue_type and not @variables.kb_article_found - with issue_type=@variables.issue_type - with keywords=@variables.issue_description - set @variables.kb_article_found = True - transition to @subagent.resolution + after_reasoning: + if @variables.kb_article_found: + transition to @subagent.resolution # SUBAGENT 2: Resolution subagent resolution: @@ -130,24 +136,10 @@ subagent resolution: create_case: description: "Create a support case for the customer" inputs: - customer_id: string - description: "The unique identifier of the customer" - is_required: True subject: string description: "Brief subject line summarizing the case" is_required: True - case_description: string - description: "Detailed description of the customer's issue" - is_required: True - priority: string - description: "Priority level (low, medium, high, urgent)" - is_required: True - issue_type: string - description: "The classified issue type (billing, technical, account, product)" - is_required: True outputs: - case_id: string - description: "Unique identifier of the created case" case_number: string description: "Human-readable case number for reference" target: "flow://CreateCase" @@ -175,7 +167,8 @@ subagent resolution: instructions:-> if @variables.kb_article_found: | I found a knowledge base article that may help resolve your {!@variables.issue_type} issue. - Let me create a case to track this. + Share this solution with the customer: {!@variables.kb_article_content} + Also create a case to track this. if not @variables.kb_article_found: | I couldn't find a matching solution. Let me escalate this to a specialist. @@ -185,11 +178,7 @@ subagent resolution: actions: create_support_case: @actions.create_case available when @variables.kb_article_found - with customer_id=@variables.customer_id with subject="{!@variables.issue_type}: {!@variables.issue_description}" - with case_description=@variables.issue_description - with priority=@variables.issue_priority - with issue_type=@variables.issue_type escalate_to_specialist: @actions.escalate_case available when not @variables.kb_article_found diff --git a/force-app-service/customerServiceAgent/classes/IssueClassifier.cls b/force-app-service/customerServiceAgent/classes/IssueClassifier.cls index 1bffd50..7d7f661 100644 --- a/force-app-service/customerServiceAgent/classes/IssueClassifier.cls +++ b/force-app-service/customerServiceAgent/classes/IssueClassifier.cls @@ -4,41 +4,63 @@ public with sharing class IssueClassifier { /** * @description Classifies the issue based on description - * @param issueDescriptions List of issue descriptions to classify + * @param requests List of IssueRequest objects * @return List of IssueResult objects with classification details */ @InvocableMethod( label='Classify Issue' description='Classifies the issue based on description' ) - public static List classify(List issueDescriptions) { + public static List classify(List requests) { List results = new List(); - for (String description : issueDescriptions) { + for (IssueRequest req : requests) { + String description = req.issueDescription != null + ? req.issueDescription + : ''; IssueResult res = new IssueResult(); - res.issueType = 'General'; - res.priority = 'Medium'; - res.suggestedKbArticles = new List{ - 'Article 1', - 'Article 2' - }; + res.issueType = 'account'; + res.priority = 'medium'; if ( description.containsIgnoreCase('urgent') || description.containsIgnoreCase('critical') ) { - res.priority = 'High'; + res.priority = 'high'; } if ( description.containsIgnoreCase('login') || description.containsIgnoreCase('password') ) { - res.issueType = 'Login Issue'; + res.issueType = 'technical'; + } + if ( + description.containsIgnoreCase('bill') || + description.containsIgnoreCase('charge') || + description.containsIgnoreCase('invoice') || + description.containsIgnoreCase('pricing') + ) { + res.issueType = 'billing'; + } + if ( + description.containsIgnoreCase('feature') || + description.containsIgnoreCase('product') || + description.containsIgnoreCase('update') + ) { + res.issueType = 'product'; } results.add(res); } return results; } + /** + * @description Input class for issue classification + */ + public class IssueRequest { + @InvocableVariable(required=true) + public String issueDescription; + } + /** * @description Result class for issue classification */ @@ -47,7 +69,5 @@ public with sharing class IssueClassifier { public String issueType; @InvocableVariable public String priority; - @InvocableVariable - public List suggestedKbArticles; } } diff --git a/force-app-service/customerServiceAgent/classes/IssueClassifierTest.cls b/force-app-service/customerServiceAgent/classes/IssueClassifierTest.cls index 0898926..e28ad9c 100644 --- a/force-app-service/customerServiceAgent/classes/IssueClassifierTest.cls +++ b/force-app-service/customerServiceAgent/classes/IssueClassifierTest.cls @@ -5,273 +5,189 @@ @IsTest private class IssueClassifierTest { @IsTest - static void testClassifyGeneralIssue() { - // Test with general issue description - List descriptions = new List{ - 'I need help with my account' - }; + static void testClassifyAccountIssue() { + IssueClassifier.IssueRequest req = new IssueClassifier.IssueRequest(); + req.issueDescription = 'I need help with my account'; - // Execute test Test.startTest(); List results = IssueClassifier.classify( - descriptions + new List{ req } ); Test.stopTest(); - // Verify results Assert.areEqual(1, results.size(), 'Should return one result'); Assert.areEqual( - 'General', + 'account', results[0].issueType, - 'Issue type should be General' + 'Default issue type should be account' ); Assert.areEqual( - 'Medium', + 'medium', results[0].priority, - 'Priority should be Medium' - ); - Assert.areNotEqual( - null, - results[0].suggestedKbArticles, - 'KB articles should be provided' - ); - Assert.areEqual( - 2, - results[0].suggestedKbArticles.size(), - 'Should have two KB articles' + 'Default priority should be medium' ); } @IsTest static void testClassifyUrgentIssue() { - // Test with urgent keyword - List descriptions = new List{ - 'This is an urgent matter that needs immediate attention' - }; + IssueClassifier.IssueRequest req = new IssueClassifier.IssueRequest(); + req.issueDescription = 'This is an urgent matter that needs immediate attention'; - // Execute test Test.startTest(); List results = IssueClassifier.classify( - descriptions + new List{ req } ); Test.stopTest(); - // Verify priority is set to High - Assert.areEqual(1, results.size(), 'Should return one result'); Assert.areEqual( - 'High', + 'high', results[0].priority, - 'Priority should be High for urgent issue' + 'Priority should be high for urgent issue' ); Assert.areEqual( - 'General', + 'account', results[0].issueType, - 'Issue type should remain General' + 'Issue type should remain account' ); } @IsTest - static void testClassifyCriticalIssue() { - // Test with critical keyword - List descriptions = new List{ - 'This is a critical system failure' - }; + static void testClassifyTechnicalIssue() { + IssueClassifier.IssueRequest req = new IssueClassifier.IssueRequest(); + req.issueDescription = 'I cannot login to my account'; - // Execute test Test.startTest(); List results = IssueClassifier.classify( - descriptions + new List{ req } ); Test.stopTest(); - // Verify priority is set to High - Assert.areEqual(1, results.size(), 'Should return one result'); Assert.areEqual( - 'High', + 'technical', + results[0].issueType, + 'Login issues should be technical' + ); + Assert.areEqual( + 'medium', results[0].priority, - 'Priority should be High for critical issue' + 'Priority should be medium' ); } @IsTest - static void testClassifyLoginIssue() { - // Test with login keyword - List descriptions = new List{ - 'I cannot login to my account' - }; + static void testClassifyPasswordIssue() { + IssueClassifier.IssueRequest req = new IssueClassifier.IssueRequest(); + req.issueDescription = 'I forgot my password and need to reset it'; - // Execute test Test.startTest(); List results = IssueClassifier.classify( - descriptions + new List{ req } ); Test.stopTest(); - // Verify issue type is Login Issue - Assert.areEqual(1, results.size(), 'Should return one result'); Assert.areEqual( - 'Login Issue', + 'technical', results[0].issueType, - 'Issue type should be Login Issue' - ); - Assert.areEqual( - 'Medium', - results[0].priority, - 'Priority should be Medium' + 'Password issues should be technical' ); } @IsTest - static void testClassifyPasswordIssue() { - // Test with password keyword - List descriptions = new List{ - 'I forgot my password and need to reset it' - }; + static void testClassifyBillingIssue() { + IssueClassifier.IssueRequest req = new IssueClassifier.IssueRequest(); + req.issueDescription = 'I was incorrectly charged on my bill'; - // Execute test Test.startTest(); List results = IssueClassifier.classify( - descriptions + new List{ req } ); Test.stopTest(); - // Verify issue type is Login Issue - Assert.areEqual(1, results.size(), 'Should return one result'); Assert.areEqual( - 'Login Issue', + 'billing', results[0].issueType, - 'Issue type should be Login Issue' + 'Billing keywords should classify as billing' ); } @IsTest - static void testClassifyUrgentLoginIssue() { - // Test with both urgent and login keywords - List descriptions = new List{ - 'This is an urgent login problem that needs immediate attention' - }; + static void testClassifyProductIssue() { + IssueClassifier.IssueRequest req = new IssueClassifier.IssueRequest(); + req.issueDescription = 'I have a question about a product feature'; - // Execute test Test.startTest(); List results = IssueClassifier.classify( - descriptions + new List{ req } ); Test.stopTest(); - // Verify both conditions are met - Assert.areEqual(1, results.size(), 'Should return one result'); Assert.areEqual( - 'Login Issue', + 'product', results[0].issueType, - 'Issue type should be Login Issue' - ); - Assert.areEqual( - 'High', - results[0].priority, - 'Priority should be High for urgent issue' + 'Product keywords should classify as product' ); } @IsTest - static void testClassifyBulkIssues() { - // Test bulk processing with multiple descriptions - List descriptions = new List{ - 'General question about my account', - 'URGENT: System is down', - 'I cannot login', - 'Critical security breach detected', - 'Password reset needed' - }; + static void testClassifyNullDescription() { + IssueClassifier.IssueRequest req = new IssueClassifier.IssueRequest(); + req.issueDescription = null; - // Execute test Test.startTest(); List results = IssueClassifier.classify( - descriptions + new List{ req } ); Test.stopTest(); - // Verify all issues were classified - Assert.areEqual(5, results.size(), 'Should return five results'); - - // Verify first issue (General) Assert.areEqual( - 'General', + 'account', results[0].issueType, - 'First issue should be General' + 'Null description should default to account' ); Assert.areEqual( - 'Medium', + 'medium', results[0].priority, - 'First issue priority should be Medium' - ); - - // Verify second issue (Urgent) - Assert.areEqual( - 'High', - results[1].priority, - 'Second issue priority should be High' + 'Null description should default to medium' ); + } - // Verify third issue (Login) - Assert.areEqual( - 'Login Issue', - results[2].issueType, - 'Third issue should be Login Issue' - ); + @IsTest + static void testClassifyBulkIssues() { + List requests = new List(); - // Verify fourth issue (Critical) - Assert.areEqual( - 'High', - results[3].priority, - 'Fourth issue priority should be High' - ); + IssueClassifier.IssueRequest req1 = new IssueClassifier.IssueRequest(); + req1.issueDescription = 'General question about my account'; + requests.add(req1); - // Verify fifth issue (Password) - Assert.areEqual( - 'Login Issue', - results[4].issueType, - 'Fifth issue should be Login Issue' - ); - } + IssueClassifier.IssueRequest req2 = new IssueClassifier.IssueRequest(); + req2.issueDescription = 'URGENT: System is down'; + requests.add(req2); - @IsTest - static void testClassifyCaseInsensitive() { - // Test case-insensitive matching - List descriptions = new List{ - 'URGENT issue', - 'CRITICAL problem', - 'LOGIN error', - 'PASSWORD reset' - }; + IssueClassifier.IssueRequest req3 = new IssueClassifier.IssueRequest(); + req3.issueDescription = 'I cannot login'; + requests.add(req3); - // Execute test Test.startTest(); List results = IssueClassifier.classify( - descriptions + requests ); Test.stopTest(); - // Verify case-insensitive matching works - Assert.areEqual(4, results.size(), 'Should return four results'); + Assert.areEqual(3, results.size(), 'Should return three results'); Assert.areEqual( - 'High', - results[0].priority, - 'URGENT should be detected' + 'account', + results[0].issueType, + 'First should be account' ); Assert.areEqual( - 'High', + 'high', results[1].priority, - 'CRITICAL should be detected' + 'Second should be high priority' ); Assert.areEqual( - 'Login Issue', + 'technical', results[2].issueType, - 'LOGIN should be detected' - ); - Assert.areEqual( - 'Login Issue', - results[3].issueType, - 'PASSWORD should be detected' + 'Third should be technical' ); } } diff --git a/force-app-service/customerServiceAgent/flows/CreateCase.flow-meta.xml b/force-app-service/customerServiceAgent/flows/CreateCase.flow-meta.xml index 6c125ee..3d16d8b 100644 --- a/force-app-service/customerServiceAgent/flows/CreateCase.flow-meta.xml +++ b/force-app-service/customerServiceAgent/flows/CreateCase.flow-meta.xml @@ -1,8 +1,23 @@ - + - 66.0 + 60.0 + false + + Assign_Output + + 176 + 134 + + case_number + Assign + + 00001234 + + + + Stub flow for Create Case Default - CreateCase {!$Flow.CurrentDateTime} + Create Case {!$Flow.CurrentDateTime} BuilderType @@ -10,81 +25,27 @@ LightningFlowBuilder + + CanvasMode + + AUTO_LAYOUT_CANVAS + + AutoLaunchedFlow 50 0 - Get_Contact + Assign_Output Active - - Get_Contact - - 176 - 134 - false - - Create_Case - - and - - Customer_ID__c - EqualTo - - customer_id - - - true - Contact - true - - - Create_Case - - 176 - 242 - case_id - - Subject - - subject - - - - Description - - case_description - - - - Priority - - priority - - - - Type - - issue_type - - - - ContactId - - Get_Contact.Id - - - Case - - customer_id + case_number String false - true - false + false + true subject @@ -93,39 +54,4 @@ true false - - case_description - String - false - true - false - - - priority - String - false - true - false - - - issue_type - String - false - true - false - - - case_id - String - false - false - true - - - case_number - String - false - false - true - diff --git a/force-app-service/customerServiceAgent/flows/EscalateCase.flow-meta.xml b/force-app-service/customerServiceAgent/flows/EscalateCase.flow-meta.xml index 14e3cea..33837bb 100644 --- a/force-app-service/customerServiceAgent/flows/EscalateCase.flow-meta.xml +++ b/force-app-service/customerServiceAgent/flows/EscalateCase.flow-meta.xml @@ -1,59 +1,12 @@ - + 66.0 - Default - EscalateCase {!$Flow.CurrentDateTime} - - - BuilderType - - LightningFlowBuilder - - - AutoLaunchedFlow - - 50 - 0 - - Update_Case_Escalation - - - Active - - Update_Case_Escalation - - 176 - 134 - - Assign_Result - - and - - Id - EqualTo - - case_id - - - - Status - - Escalated - - - - Escalation_Reason__c - - reason - - - Case - + false Assign_Result - 176 - 242 + 0 + 0 escalated Assign @@ -65,10 +18,35 @@ specialist_assigned Assign - specialist_team + Technical Support + Stub flow for Escalate Case + Default + EscalateCase {!$Flow.CurrentDateTime} + + + BuilderType + + LightningFlowBuilder + + + + CanvasMode + + AUTO_LAYOUT_CANVAS + + + AutoLaunchedFlow + + 0 + 0 + + Assign_Result + + + Draft case_id String @@ -77,31 +55,31 @@ false - reason - String + escalated + Boolean false - true - false + false + true - specialist_team + reason String false true false - escalated - Boolean + specialist_assigned + String false false true - specialist_assigned + specialist_team String false - false - true + true + false diff --git a/force-app-service/customerServiceAgent/flows/FetchCustomer.flow-meta.xml b/force-app-service/customerServiceAgent/flows/FetchCustomer.flow-meta.xml index d650c0c..4772786 100644 --- a/force-app-service/customerServiceAgent/flows/FetchCustomer.flow-meta.xml +++ b/force-app-service/customerServiceAgent/flows/FetchCustomer.flow-meta.xml @@ -1,6 +1,35 @@ - + 66.0 + false + + Mock_Customer + + 0 + 0 + + name + Assign + + Customer + + + + email + Assign + + test@example.com + + + + tier + Assign + + Premium + + + + Stub flow for Fetch Customer Default FetchCustomer {!$Flow.CurrentDateTime} @@ -10,44 +39,21 @@ LightningFlowBuilder + + CanvasMode + + AUTO_LAYOUT_CANVAS + + AutoLaunchedFlow - 50 + 0 0 - Get_Contact + Mock_Customer Active - - Get_Contact - - 176 - 134 - false - and - - Customer_ID__c - EqualTo - - customer_id - - - Contact - - name - LastName - - - email - Email - - - tier - Loyalty_Tier__c - - customer_id String @@ -56,14 +62,14 @@ false - name + email String false false true - email + name String false false diff --git a/force-app-service/customerServiceAgent/flows/SearchKnowledgeBase.flow-meta.xml b/force-app-service/customerServiceAgent/flows/SearchKnowledgeBase.flow-meta.xml index 7c3ef32..6cb3b34 100644 --- a/force-app-service/customerServiceAgent/flows/SearchKnowledgeBase.flow-meta.xml +++ b/force-app-service/customerServiceAgent/flows/SearchKnowledgeBase.flow-meta.xml @@ -1,6 +1,29 @@ - + 66.0 + false + + Mock_Results + + 0 + 0 + + articles + Add + + Article 1: How to Reset Password + + + + top_article + Assign + + How to Reset Password: Go to Settings > Security > Reset Password. Click the reset link sent to your email. + + + + Stub flow for Search Knowledge Base Default SearchKnowledgeBase {!$Flow.CurrentDateTime} @@ -11,28 +34,28 @@ LightningFlowBuilder + + CanvasMode + + AUTO_LAYOUT_CANVAS + + AutoLaunchedFlow - 50 + 0 0 Mock_Results Active - - Mock_Results - - 176 - 134 - - articles - Add - - Article 1: How to Reset Password - - - + + articles + String + true + false + true + issue_type String @@ -47,13 +70,6 @@ true false - - articles - String - true - false - true - top_article String diff --git a/force-app/main/shared/permissionsets/Agent_Script_Recipes_Data.permissionset-meta.xml b/force-app/main/shared/permissionsets/Agent_Script_Recipes_Data.permissionset-meta.xml index 764906e..57c6c58 100644 --- a/force-app/main/shared/permissionsets/Agent_Script_Recipes_Data.permissionset-meta.xml +++ b/force-app/main/shared/permissionsets/Agent_Script_Recipes_Data.permissionset-meta.xml @@ -1,5 +1,9 @@ + + IssueClassifier + true + PaymentGatewayController true @@ -343,6 +347,11 @@ Contact.AccountId true + + true + Contact.Customer_ID__c + true + true Contact.Lifetime_Value__c @@ -353,6 +362,11 @@ Contact.Loyalty_Status__c true + + true + Contact.Loyalty_Tier__c + true + true Contact.User_ID__c From 9d22dbbc681880d5399a8760ffa1688a34cd97fe Mon Sep 17 00:00:00 2001 From: Mohith Shrivastava Date: Wed, 6 May 2026 14:54:31 -0400 Subject: [PATCH 08/14] fix: remove --dev-debug from CI deploy commands --- .github/workflows/ci-pr.yml | 4 ++-- .github/workflows/ci.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index bb06f49..d6fdcb2 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -182,7 +182,7 @@ jobs: # Deploy employee agent source to scratch org - name: 'Deploy employee agent recipes' - run: sf project deploy start --source-dir force-app --dev-debug + run: sf project deploy start --source-dir force-app # Assign permission sets - name: 'Assign permission sets to default user' @@ -206,7 +206,7 @@ jobs: # Deploy service agent source to scratch org - name: 'Deploy service agent recipes' - run: sf project deploy start --source-dir force-app-service --dev-debug + run: sf project deploy start --source-dir force-app-service # Run Apex tests in scratch org - name: 'Run Apex tests' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 046c3f8..54f430a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,7 +122,7 @@ jobs: # Deploy employee agent source to scratch org - name: 'Deploy employee agent recipes' - run: sf project deploy start --source-dir force-app --dev-debug + run: sf project deploy start --source-dir force-app # Assign permission sets - name: 'Assign permission sets to default user' @@ -146,7 +146,7 @@ jobs: # Deploy service agent source to scratch org - name: 'Deploy service agent recipes' - run: sf project deploy start --source-dir force-app-service --dev-debug + run: sf project deploy start --source-dir force-app-service # Run Apex tests in scratch org - name: 'Run Apex tests' From fbbc001c89c302e6dd0b69d7c704865a8984b05f Mon Sep 17 00:00:00 2001 From: Mohith Shrivastava Date: Wed, 6 May 2026 15:20:15 -0400 Subject: [PATCH 09/14] docs: remove parenthetical examples from service agent README --- force-app-service/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/force-app-service/README.md b/force-app-service/README.md index 7a2a26a..e3b67d5 100644 --- a/force-app-service/README.md +++ b/force-app-service/README.md @@ -1,6 +1,6 @@ # Service Agent Recipes -This directory contains Agentforce **Service Agent** examples. Service Agents are external-facing agents (customer support, self-service portals) that run under a dedicated agent user rather than the logged-in user. +This directory contains Agentforce **Service Agent** examples. Service Agents are external-facing agents that run under a dedicated agent user rather than the logged-in user. ## Key Difference from Employee Agents From d6733271370c7b4ae7a3f0b848de293d501513ba Mon Sep 17 00:00:00 2001 From: Mohith Shrivastava Date: Wed, 6 May 2026 22:45:07 -0400 Subject: [PATCH 10/14] fix: extract service agent permission set to avoid cross-directory deploy failure Move IssueClassifier class access and Contact.Customer_ID__c, Contact.Loyalty_Tier__c, Case.Escalation_Reason__c field permissions into a dedicated Customer_Service_Agent_Data permission set inside force-app-service. This eliminates the deploy error where the shared permission set in force-app referenced fields that only exist in force-app-service. --- .github/workflows/ci-pr.yml | 10 +++++-- .github/workflows/ci.yml | 10 +++++-- README.md | 3 ++- bin/install-scratch.bat | 7 ++++- bin/install-scratch.sh | 6 ++++- .../customerServiceAgent/README.md | 2 +- ..._Service_Agent_Data.permissionset-meta.xml | 26 +++++++++++++++++++ ...Script_Recipes_Data.permissionset-meta.xml | 14 ---------- 8 files changed, 56 insertions(+), 22 deletions(-) create mode 100644 force-app-service/customerServiceAgent/permissionsets/Customer_Service_Agent_Data.permissionset-meta.xml diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index d6fdcb2..ea3aeb1 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -198,8 +198,8 @@ jobs: - name: 'Create agent user for service agent' run: node bin/setup-service-agent.js --target-org scratch-org - # Assign permission set to agent user - - name: 'Assign permission set to agent user' + # Assign base permission set to agent user + - name: 'Assign base permission set to agent user' run: | agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of "$agent_user" -o scratch-org @@ -208,6 +208,12 @@ jobs: - name: 'Deploy service agent recipes' run: sf project deploy start --source-dir force-app-service + # Assign service agent permission set (deployed with force-app-service) + - name: 'Assign service agent permission set to agent user' + run: | + agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) + sf org assign permset -n Customer_Service_Agent_Data --on-behalf-of "$agent_user" -o scratch-org + # Run Apex tests in scratch org - name: 'Run Apex tests' run: sf apex test run -c -r human -d ./tests/apex -w 20 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54f430a..17cf879 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,8 +138,8 @@ jobs: - name: 'Create agent user for service agent' run: node bin/setup-service-agent.js --target-org scratch-org - # Assign permission set to agent user - - name: 'Assign permission set to agent user' + # Assign base permission set to agent user + - name: 'Assign base permission set to agent user' run: | agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of "$agent_user" -o scratch-org @@ -148,6 +148,12 @@ jobs: - name: 'Deploy service agent recipes' run: sf project deploy start --source-dir force-app-service + # Assign service agent permission set (deployed with force-app-service) + - name: 'Assign service agent permission set to agent user' + run: | + agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) + sf org assign permset -n Customer_Service_Agent_Data --on-behalf-of "$agent_user" -o scratch-org + # Run Apex tests in scratch org - name: 'Run Apex tests' run: sf apex test run -c -r human -d ./tests/apex -w 20 diff --git a/README.md b/README.md index 24e368d..e94b8aa 100644 --- a/README.md +++ b/README.md @@ -102,10 +102,11 @@ If you don't have an org yet, you can sign up for a free [Developer Edition Org] sf project deploy start --source-dir force-app-service ``` -1. **(Service Agent recipes)** Assign the required permission set to the agent user so the service agent can access recipe data: +1. **(Service Agent recipes)** Assign the required permission sets to the agent user so the service agent can access recipe data: ```bash sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of + sf org assign permset -n Customer_Service_Agent_Data --on-behalf-of ``` Replace `` with the username printed by the `setup:service-agent` script in the previous step. diff --git a/bin/install-scratch.bat b/bin/install-scratch.bat index 2bdd0ce..c0ae1ca 100755 --- a/bin/install-scratch.bat +++ b/bin/install-scratch.bat @@ -46,7 +46,7 @@ cmd.exe /c node bin/setup-service-agent.js call :checkForError @echo: -echo Assigning permission set to agent user... +echo Assigning base permission set to agent user... for /f "tokens=*" %%a in ('node -e "const fs=require(\"fs\"),p=require(\"path\");const f=fs.readFileSync(p.resolve(\"force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent\"),\"utf8\");const m=f.match(/default_agent_user:\s*\"([^_][^\"]+)\"/);if(m)process.stdout.write(m[1])"') do set AGENT_USER=%%a cmd.exe /c sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of %AGENT_USER% call :checkForError @@ -57,6 +57,11 @@ cmd.exe /c sf project deploy start --source-dir force-app-service call :checkForError @echo: +echo Assigning service agent permission set... +cmd.exe /c sf org assign permset -n Customer_Service_Agent_Data --on-behalf-of %AGENT_USER% +call :checkForError +@echo: + rem Report install success if no error @echo: if ["%errorlevel%"]==["0"] ( diff --git a/bin/install-scratch.sh b/bin/install-scratch.sh index aab8afc..c787354 100755 --- a/bin/install-scratch.sh +++ b/bin/install-scratch.sh @@ -39,7 +39,7 @@ echo "Creating agent user for service agent..." && \ node bin/setup-service-agent.js && \ echo "" && \ -echo "Assigning permission set to agent user..." && \ +echo "Assigning base permission set to agent user..." && \ agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) && \ sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of "$agent_user" && \ echo "" && \ @@ -48,6 +48,10 @@ echo "Deploying service agent recipes..." && \ sf project deploy start --source-dir force-app-service && \ echo "" && \ +echo "Assigning service agent permission set..." && \ +sf org assign permset -n Customer_Service_Agent_Data --on-behalf-of "$agent_user" && \ +echo "" && \ + echo "Opening org..." && \ sf org open -p lightning/n/standard-AgentforceStudio && \ echo "" diff --git a/force-app-service/customerServiceAgent/README.md b/force-app-service/customerServiceAgent/README.md index 4c2213e..dd5d16a 100644 --- a/force-app-service/customerServiceAgent/README.md +++ b/force-app-service/customerServiceAgent/README.md @@ -140,7 +140,7 @@ This recipe requires additional setup compared to Employee Agent recipes: 1. `npm run setup:service-agent` — creates the agent user and replaces the placeholder in all `.agent` files 2. `sf project deploy start --source-dir force-app-service` — deploys the metadata -3. `sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of ` — grants data access +3. `sf org assign permset -n Customer_Service_Agent_Data --on-behalf-of ` — grants service agent data access A pre-commit hook automatically restores the placeholder before commits. diff --git a/force-app-service/customerServiceAgent/permissionsets/Customer_Service_Agent_Data.permissionset-meta.xml b/force-app-service/customerServiceAgent/permissionsets/Customer_Service_Agent_Data.permissionset-meta.xml new file mode 100644 index 0000000..2342571 --- /dev/null +++ b/force-app-service/customerServiceAgent/permissionsets/Customer_Service_Agent_Data.permissionset-meta.xml @@ -0,0 +1,26 @@ + + + + IssueClassifier + true + + Grants access to custom fields and classes used by the Customer Service Agent recipe. + + true + Case.Escalation_Reason__c + true + + + true + Contact.Customer_ID__c + true + + + true + Contact.Loyalty_Tier__c + true + + false + + diff --git a/force-app/main/shared/permissionsets/Agent_Script_Recipes_Data.permissionset-meta.xml b/force-app/main/shared/permissionsets/Agent_Script_Recipes_Data.permissionset-meta.xml index 57c6c58..764906e 100644 --- a/force-app/main/shared/permissionsets/Agent_Script_Recipes_Data.permissionset-meta.xml +++ b/force-app/main/shared/permissionsets/Agent_Script_Recipes_Data.permissionset-meta.xml @@ -1,9 +1,5 @@ - - IssueClassifier - true - PaymentGatewayController true @@ -347,11 +343,6 @@ Contact.AccountId true - - true - Contact.Customer_ID__c - true - true Contact.Lifetime_Value__c @@ -362,11 +353,6 @@ Contact.Loyalty_Status__c true - - true - Contact.Loyalty_Tier__c - true - true Contact.User_ID__c From 560e2f6701a5d3f7658d5d79e55492d7dceaaa6d Mon Sep 17 00:00:00 2001 From: Mohith Shrivastava Date: Thu, 7 May 2026 00:07:52 -0400 Subject: [PATCH 11/14] refactor: move node scripts to scripts/ and output agent username via stdout - Move node scripts (setup-service-agent, clean-service-agent, validate-agents) from bin/ to scripts/ - setup-service-agent now writes username to stdout and logs to stderr, eliminating the brittle grep against .agent files in CI - CI steps capture username via GITHUB_OUTPUT step outputs - Add clean:service-agent npm script - Activate EscalateCase flow - Remove .vscode bin exclusion --- .github/workflows/ci-pr.yml | 13 +++++------ .github/workflows/ci.yml | 13 +++++------ .husky/pre-commit | 2 +- bin/install-scratch.bat | 3 +-- bin/install-scratch.sh | 3 +-- .../flows/EscalateCase.flow-meta.xml | 2 +- package.json | 5 ++-- {bin => scripts}/clean-service-agent.js | 0 {bin => scripts}/setup-service-agent.js | 23 ++++++++++++------- {bin => scripts}/validate-agents.js | 2 +- 10 files changed, 35 insertions(+), 31 deletions(-) rename {bin => scripts}/clean-service-agent.js (100%) rename {bin => scripts}/setup-service-agent.js (88%) rename {bin => scripts}/validate-agents.js (99%) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index ea3aeb1..71a4402 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -196,13 +196,14 @@ jobs: # Create agent user for service agent deployment - name: 'Create agent user for service agent' - run: node bin/setup-service-agent.js --target-org scratch-org + id: service-agent-user + run: | + agent_user=$(node scripts/setup-service-agent.js --target-org scratch-org) + echo "username=$agent_user" >> "$GITHUB_OUTPUT" # Assign base permission set to agent user - name: 'Assign base permission set to agent user' - run: | - agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) - sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of "$agent_user" -o scratch-org + run: sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of "${{ steps.service-agent-user.outputs.username }}" -o scratch-org # Deploy service agent source to scratch org - name: 'Deploy service agent recipes' @@ -210,9 +211,7 @@ jobs: # Assign service agent permission set (deployed with force-app-service) - name: 'Assign service agent permission set to agent user' - run: | - agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) - sf org assign permset -n Customer_Service_Agent_Data --on-behalf-of "$agent_user" -o scratch-org + run: sf org assign permset -n Customer_Service_Agent_Data --on-behalf-of "${{ steps.service-agent-user.outputs.username }}" -o scratch-org # Run Apex tests in scratch org - name: 'Run Apex tests' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17cf879..af757e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,13 +136,14 @@ jobs: # Create agent user for service agent deployment - name: 'Create agent user for service agent' - run: node bin/setup-service-agent.js --target-org scratch-org + id: service-agent-user + run: | + agent_user=$(node scripts/setup-service-agent.js --target-org scratch-org) + echo "username=$agent_user" >> "$GITHUB_OUTPUT" # Assign base permission set to agent user - name: 'Assign base permission set to agent user' - run: | - agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) - sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of "$agent_user" -o scratch-org + run: sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of "${{ steps.service-agent-user.outputs.username }}" -o scratch-org # Deploy service agent source to scratch org - name: 'Deploy service agent recipes' @@ -150,9 +151,7 @@ jobs: # Assign service agent permission set (deployed with force-app-service) - name: 'Assign service agent permission set to agent user' - run: | - agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) - sf org assign permset -n Customer_Service_Agent_Data --on-behalf-of "$agent_user" -o scratch-org + run: sf org assign permset -n Customer_Service_Agent_Data --on-behalf-of "${{ steps.service-agent-user.outputs.username }}" -o scratch-org # Run Apex tests in scratch org - name: 'Run Apex tests' diff --git a/.husky/pre-commit b/.husky/pre-commit index e80854f..581ec3c 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,2 @@ -node bin/clean-service-agent.js +node scripts/clean-service-agent.js npm run precommit \ No newline at end of file diff --git a/bin/install-scratch.bat b/bin/install-scratch.bat index c0ae1ca..3f0ca46 100755 --- a/bin/install-scratch.bat +++ b/bin/install-scratch.bat @@ -42,12 +42,11 @@ call :checkForError @echo: echo Creating agent user for service agent... -cmd.exe /c node bin/setup-service-agent.js +for /f "tokens=*" %%a in ('node scripts/setup-service-agent.js') do set AGENT_USER=%%a call :checkForError @echo: echo Assigning base permission set to agent user... -for /f "tokens=*" %%a in ('node -e "const fs=require(\"fs\"),p=require(\"path\");const f=fs.readFileSync(p.resolve(\"force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent\"),\"utf8\");const m=f.match(/default_agent_user:\s*\"([^_][^\"]+)\"/);if(m)process.stdout.write(m[1])"') do set AGENT_USER=%%a cmd.exe /c sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of %AGENT_USER% call :checkForError @echo: diff --git a/bin/install-scratch.sh b/bin/install-scratch.sh index c787354..7f557b7 100755 --- a/bin/install-scratch.sh +++ b/bin/install-scratch.sh @@ -36,11 +36,10 @@ sf data import tree --plan data/data-plan.json && \ echo "" && \ echo "Creating agent user for service agent..." && \ -node bin/setup-service-agent.js && \ +agent_user=$(node scripts/setup-service-agent.js) && \ echo "" && \ echo "Assigning base permission set to agent user..." && \ -agent_user=$(grep -oP 'default_agent_user:\s*"\K[^"]+' force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent) && \ sf org assign permset -n Agent_Script_Recipes_Data --on-behalf-of "$agent_user" && \ echo "" && \ diff --git a/force-app-service/customerServiceAgent/flows/EscalateCase.flow-meta.xml b/force-app-service/customerServiceAgent/flows/EscalateCase.flow-meta.xml index 33837bb..32b4981 100644 --- a/force-app-service/customerServiceAgent/flows/EscalateCase.flow-meta.xml +++ b/force-app-service/customerServiceAgent/flows/EscalateCase.flow-meta.xml @@ -46,7 +46,7 @@ Assign_Result - Draft + Active case_id String diff --git a/package.json b/package.json index 8885a64..9a15c27 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,9 @@ "prettier:verify": "prettier --check \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", "prepare": "husky || true", "precommit": "lint-staged", - "validate:agents": "node bin/validate-agents.js", - "setup:service-agent": "node bin/setup-service-agent.js" + "validate:agents": "node scripts/validate-agents.js", + "setup:service-agent": "node scripts/setup-service-agent.js", + "clean:service-agent": "node scripts/clean-service-agent.js" }, "lint-staged": { "**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}": [ diff --git a/bin/clean-service-agent.js b/scripts/clean-service-agent.js similarity index 100% rename from bin/clean-service-agent.js rename to scripts/clean-service-agent.js diff --git a/bin/setup-service-agent.js b/scripts/setup-service-agent.js similarity index 88% rename from bin/setup-service-agent.js rename to scripts/setup-service-agent.js index baf78a7..fa6845e 100644 --- a/bin/setup-service-agent.js +++ b/scripts/setup-service-agent.js @@ -42,14 +42,21 @@ function findAgentFiles(dir) { return results; } +function log(msg) { + process.stderr.write(msg + '\n'); +} + function createAgentUser(targetOrg) { const orgFlag = targetOrg ? ` -o ${targetOrg}` : ''; const cmd = `sf org create agent-user${orgFlag} --json`; - console.log(`Running: ${cmd}`); + log(`Running: ${cmd}`); try { - const output = execSync(cmd, { encoding: 'utf8' }); + const output = execSync(cmd, { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'] + }); const result = JSON.parse(output); if (result.status !== 0) { @@ -87,22 +94,20 @@ function replacePlaceholders(username) { if (content.includes(PLACEHOLDER)) { content = content.replace(PLACEHOLDER, username); fs.writeFileSync(filePath, content, 'utf8'); - console.log( - `Updated: ${path.relative(SERVICE_AGENT_DIR, filePath)}` - ); + log(`Updated: ${path.relative(SERVICE_AGENT_DIR, filePath)}`); replacedCount++; } } if (replacedCount === 0) { - console.warn( + log( `Warning: No files contained the placeholder "${PLACEHOLDER}". They may have already been replaced.` ); } else { - console.log( + log( `\nReplaced placeholder in ${replacedCount} file(s) with agent user: ${username}` ); - console.log( + log( '\nReminder: Do not commit the modified agent file(s). The pre-commit hook will restore the placeholder automatically.' ); } @@ -111,3 +116,5 @@ function replacePlaceholders(username) { const { targetOrg } = parseArgs(); const username = createAgentUser(targetOrg); replacePlaceholders(username); + +process.stdout.write(username); diff --git a/bin/validate-agents.js b/scripts/validate-agents.js similarity index 99% rename from bin/validate-agents.js rename to scripts/validate-agents.js index d5dcb45..0d9e351 100644 --- a/bin/validate-agents.js +++ b/scripts/validate-agents.js @@ -4,7 +4,7 @@ // // Usage: // npm run validate:agents -// node bin/validate-agents.js [--files path1 path2 ...] +// node scripts/validate-agents.js [--files path1 path2 ...] 'use strict'; From a0f496a98f03e4903bf4903878cd56fef085d47b Mon Sep 17 00:00:00 2001 From: Mohith Shrivastava Date: Thu, 7 May 2026 00:34:19 -0400 Subject: [PATCH 12/14] fix: align CustomerServiceAgentFlowTest with stub flow signatures CreateCase only accepts subject and outputs case_number (not case_id). EscalateCase is a stub that returns hardcoded outputs without DML. --- .../classes/CustomerServiceAgentFlowTest.cls | 61 ++----------------- 1 file changed, 6 insertions(+), 55 deletions(-) diff --git a/force-app-service/customerServiceAgent/classes/CustomerServiceAgentFlowTest.cls b/force-app-service/customerServiceAgent/classes/CustomerServiceAgentFlowTest.cls index a51ad7a..e999a3b 100644 --- a/force-app-service/customerServiceAgent/classes/CustomerServiceAgentFlowTest.cls +++ b/force-app-service/customerServiceAgent/classes/CustomerServiceAgentFlowTest.cls @@ -50,15 +50,9 @@ private class CustomerServiceAgentFlowTest { @IsTest static void testCreateCaseSuccess() { - // Set input variables for the flow Map inputs = new Map(); - inputs.put('customer_id', 'CUST-12345'); inputs.put('subject', 'Test Case Subject'); - inputs.put('case_description', 'Test Case Description'); - inputs.put('priority', 'High'); - inputs.put('issue_type', 'Technical'); - // Run the flow Test.startTest(); Flow.Interview.CreateCase flowInterview = new Flow.Interview.CreateCase( inputs @@ -66,28 +60,14 @@ private class CustomerServiceAgentFlowTest { flowInterview.start(); Test.stopTest(); - // Verify the expected outcome - String caseId = (String) flowInterview.getVariableValue('case_id'); - Assert.areNotEqual(null, caseId, 'Case ID should be returned'); - - // Verify case was created - Case createdCase = [ - SELECT Id, Subject, Description, Priority, Type, ContactId - FROM Case - WHERE Id = :caseId - ]; - Assert.areEqual( - 'Test Case Subject', - createdCase.Subject, - 'Subject should match' + String caseNumber = (String) flowInterview.getVariableValue( + 'case_number' ); Assert.areEqual( - 'Test Case Description', - createdCase.Description, - 'Description should match' + '00001234', + caseNumber, + 'Case number should be returned' ); - Assert.areEqual('High', createdCase.Priority, 'Priority should match'); - Assert.areEqual('Technical', createdCase.Type, 'Type should match'); } @IsTest @@ -119,27 +99,11 @@ private class CustomerServiceAgentFlowTest { @IsTest static void testEscalateCase() { - // Create a test case first - Contact testContact = [ - SELECT Id - FROM Contact - WHERE Customer_ID__c = 'CUST-12345' - LIMIT 1 - ]; - Case testCase = new Case( - Subject = 'Test Case', - ContactId = testContact.Id, - Status = 'New' - ); - insert testCase; - - // Set input variables for the flow Map inputs = new Map(); - inputs.put('case_id', testCase.Id); + inputs.put('case_id', '500000000000001'); inputs.put('reason', 'Complex technical issue'); inputs.put('specialist_team', 'Technical Support'); - // Run the flow Test.startTest(); Flow.Interview.EscalateCase flowInterview = new Flow.Interview.EscalateCase( inputs @@ -147,7 +111,6 @@ private class CustomerServiceAgentFlowTest { flowInterview.start(); Test.stopTest(); - // Verify the expected outcome Boolean escalated = (Boolean) flowInterview.getVariableValue( 'escalated' ); @@ -160,17 +123,5 @@ private class CustomerServiceAgentFlowTest { specialistAssigned, 'Specialist team should be assigned' ); - - // Verify case was updated - Case escalatedCase = [ - SELECT Id, Status - FROM Case - WHERE Id = :testCase.Id - ]; - Assert.areEqual( - 'Escalated', - escalatedCase.Status, - 'Status should be Escalated' - ); } } From e6c4bfbfa7f94d391260922c27275fe33c8cf1c7 Mon Sep 17 00:00:00 2001 From: Mohith Shrivastava Date: Thu, 7 May 2026 23:26:37 -0400 Subject: [PATCH 13/14] fix: address feedback on escalations not working --- .../customerServiceAgent/README.md | 84 +++++++++---------- .../CustomerServiceAgent.agent | 13 +-- .../flows/SearchKnowledgeBase.flow-meta.xml | 43 +++++++++- 3 files changed, 89 insertions(+), 51 deletions(-) diff --git a/force-app-service/customerServiceAgent/README.md b/force-app-service/customerServiceAgent/README.md index dd5d16a..d7a32ef 100644 --- a/force-app-service/customerServiceAgent/README.md +++ b/force-app-service/customerServiceAgent/README.md @@ -94,7 +94,7 @@ subagent resolution: ## Try It Out -### Example: Issue Resolved via Knowledge Base +### Example 1: Happy Path — KB Resolution ```text Agent: Hello! Welcome to Customer Service. How can I help you today? @@ -115,18 +115,21 @@ Agent: I've created case 00001234 for your password reset issue. Here's the solution from our knowledge base: ... ``` -### Example: Issue Escalated +**What to verify**: All three triage actions fire in sequence (`fetch_customer` → `classify_issue` → `search_knowledge_base`), then the agent transitions to the resolution subagent and creates a case. + +### Example 2: Escalation Path — No KB Match ```text Agent: Hello! Welcome to Customer Service. How can I help you today? -User: I'm being charged incorrectly on my enterprise contract. +User: I need a custom enterprise pricing adjustment. [classify_issue → type="billing", priority="high"] [search_knowledge_base → kb_article_found=False (no match)] [transitions to resolution] -Agent: I couldn't find a matching solution. Let me escalate this to a specialist. +Agent: I couldn't find a matching solution in our knowledge base. + Let me escalate this to a specialist who can help. [escalate_case → escalated=True, specialist_assigned="Billing Support"] @@ -134,6 +137,31 @@ Agent: I've escalated your billing issue to our specialist team. They will reach out to you shortly. ``` +**What to verify**: After KB search returns no relevant articles, the agent escalates via `escalate_case` instead of creating a standard case. Note that `fetch_customer` is skipped because no customer ID was provided. + +### Example 3: Unknown Customer — Direct Classification + +```text +Agent: Hello! Welcome to Customer Service. How can I help you today? + +User: I need help with my billing. I was charged twice for last month's subscription. + +[classify_issue → type="billing", priority="medium"] +[search_knowledge_base → kb_article_found=False (no match)] +[transitions to resolution] + +Agent: I couldn't find a matching solution in our knowledge base. + Let me escalate this to a specialist. + +[escalate_case → escalated=True, specialist_assigned="Technical Support"] + +Agent: I am escalating your billing issue to a specialist who can review + your account and resolve the double charge. You will be contacted + soon with an update. +``` + +**What to verify**: `fetch_customer` is never called because no `customer_id` was provided. The agent proceeds directly to classification and escalation in a single turn. + ## Deployment This recipe requires additional setup compared to Employee Agent recipes: @@ -150,45 +178,13 @@ A pre-commit hook automatically restores the placeholder before commits. - **OpenGateRouter**: Deterministic gate-based routing with authentication - **ActionChaining**: Sequential action execution patterns -## Testing - -### Stub Flow Behavior - -The included flows return hardcoded values for testing: +## Stub Flow Behavior -| Flow | Always Returns | -| --------------------- | --------------------------------------------------------------- | -| `FetchCustomer` | `name="Customer"`, `email="test@example.com"`, `tier="Premium"` | -| `SearchKnowledgeBase` | `articles=["..."]`, `top_article={...}` | -| `CreateCase` | `case_id="500..."`, `case_number="00001234"` | -| `EscalateCase` | `escalated=true`, `specialist_assigned="Technical Support"` | - -### Test 1: Happy Path — KB Resolution - -```text -User: My account is CUST-12345. I can't log in. - -Agent: [fetches customer, classifies issue, searches KB, creates case] -``` - -**Expected**: All three triage actions fire in sequence, then transitions to resolution and creates a case. - -### Test 2: Escalation Path — No KB Match - -```text -User: I need a custom enterprise pricing adjustment. - -Agent: [classifies issue, searches KB (no match), escalates] -``` - -**Expected**: After KB search returns no relevant articles, the agent escalates instead of creating a standard case. - -### Test 3: Unknown Customer - -```text -User: I need help with my billing. - -Agent: [classifies issue directly, skips fetch_customer since no customer_id] -``` +The included flows return hardcoded values so you can try the agent without real data: -**Expected**: `fetch_customer` is not called because `customer_id` is empty. Agent proceeds with classification. +| Flow | Returns | +| --------------------- | ------------------------------------------------------------------------------ | +| `FetchCustomer` | `name="Customer"`, `email="test@example.com"`, `tier="Premium"` | +| `SearchKnowledgeBase` | If `issue_type="technical"`: articles + top_article. Otherwise: empty results. | +| `CreateCase` | `case_number="00001234"` | +| `EscalateCase` | `escalated=true`, `specialist_assigned="Technical Support"` | diff --git a/force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent b/force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent index 1b9ecc4..4926428 100644 --- a/force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent +++ b/force-app-service/customerServiceAgent/aiAuthoringBundles/CustomerServiceAgent/CustomerServiceAgent.agent @@ -25,6 +25,8 @@ variables: description: "Whether a relevant knowledge base article was found" kb_article_content: mutable string = "" description: "Content of the top knowledge base article found" + kb_search_complete: mutable boolean = False + description: "Whether the knowledge base search has been executed" system: instructions: "You are a customer service agent. Identify the customer, classify their issue, search the knowledge base for solutions, and create or escalate cases as needed. Be empathetic and solution-oriented." @@ -100,12 +102,13 @@ subagent triage: with issueDescription=@variables.issue_description set @variables.issue_type = @outputs.issueType set @variables.issue_priority = @outputs.priority - if @variables.issue_type and not @variables.kb_article_found: + if @variables.issue_type and not @variables.kb_search_complete: run @actions.search_knowledge_base with issue_type=@variables.issue_type with keywords=@variables.issue_description - set @variables.kb_article_found = True set @variables.kb_article_content = @outputs.top_article + set @variables.kb_article_found = @outputs.top_article != "" + set @variables.kb_search_complete = True if @variables.customer_name: | Hello {!@variables.customer_name}! How can I help you today? @@ -113,8 +116,8 @@ subagent triage: | Hello! Welcome to Customer Service. How can I help you today? if not @variables.issue_description: - | Use {!@actions.collect_info} to save whatever the user provided. Extract customer_id if mentioned (otherwise leave empty) and issue_description from the conversation. If the user has not described any issue at all, ask them to do so. - if @variables.kb_article_found: + | You MUST call {!@actions.collect_info} immediately. Extract customer_id if mentioned (otherwise leave empty) and issue_description from the user's messages. Any mention of a problem or topic counts as an issue description — save it even if brief. Never ask clarifying questions before calling collect_info. + if @variables.kb_search_complete: | Triage is complete. Inform the customer their issue has been identified and you are resolving it. actions: @@ -125,7 +128,7 @@ subagent triage: with issue_description=... after_reasoning: - if @variables.kb_article_found: + if @variables.kb_search_complete: transition to @subagent.resolution # SUBAGENT 2: Resolution diff --git a/force-app-service/customerServiceAgent/flows/SearchKnowledgeBase.flow-meta.xml b/force-app-service/customerServiceAgent/flows/SearchKnowledgeBase.flow-meta.xml index 6cb3b34..c9b8598 100644 --- a/force-app-service/customerServiceAgent/flows/SearchKnowledgeBase.flow-meta.xml +++ b/force-app-service/customerServiceAgent/flows/SearchKnowledgeBase.flow-meta.xml @@ -2,6 +2,31 @@ 66.0 false + + Check_Issue_Type + + 0 + 0 + + No_Results + + No Match + + Is_Technical + and + + issue_type + EqualTo + + technical + + + + Mock_Results + + + + Mock_Results @@ -23,7 +48,21 @@ - Stub flow for Search Knowledge Base + + No_Results + + 0 + 0 + + top_article + Assign + + + + + + Stub flow for Search Knowledge Base — returns articles only for technical issues Default SearchKnowledgeBase {!$Flow.CurrentDateTime} @@ -45,7 +84,7 @@ 0 0 - Mock_Results + Check_Issue_Type Active From ecd909e53a9f84305ead9fc77ec63525dafbf6ff Mon Sep 17 00:00:00 2001 From: Mohith Shrivastava Date: Mon, 11 May 2026 07:51:19 -0400 Subject: [PATCH 14/14] Update API version from 60.0 to 66.0 --- .../customerServiceAgent/flows/CreateCase.flow-meta.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/force-app-service/customerServiceAgent/flows/CreateCase.flow-meta.xml b/force-app-service/customerServiceAgent/flows/CreateCase.flow-meta.xml index 3d16d8b..3aa0647 100644 --- a/force-app-service/customerServiceAgent/flows/CreateCase.flow-meta.xml +++ b/force-app-service/customerServiceAgent/flows/CreateCase.flow-meta.xml @@ -1,6 +1,6 @@ - 60.0 + 66.0 false Assign_Output