@salesforce/afv-skills 1.31.0 → 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/agentforce-bot-upgrade/SKILL.md +217 -0
- package/skills/agentforce-bot-upgrade/references/extraction-blueprint.md +242 -0
- package/skills/agentforce-bot-upgrade/references/generate-agent-spec.md +128 -0
- package/skills/agentforce-bot-upgrade/references/handoff-output-format.md +208 -0
- package/skills/agentforce-bot-upgrade/references/input-contract.md +101 -0
- package/skills/agentforce-bot-upgrade/references/mapping-rules.md +113 -0
- package/skills/agentforce-bot-upgrade/references/planner-workflow-reference.md +78 -0
- package/skills/agentforce-bot-upgrade/references/post-conversion-enhancements-reference.md +67 -0
- package/skills/agentforce-bot-upgrade/references/quality-checklist.md +33 -0
- package/skills/agentforce-bot-upgrade/references/sf-cli-bot-reference.md +116 -0
- package/skills/dx-devops-work-item-manage/SKILL.md +270 -0
- package/skills/dx-devops-work-item-manage/examples/common-workflows.md +479 -0
- package/skills/dx-devops-work-item-manage/references/cli-commands.md +476 -0
- package/skills/dx-org-manage/SKILL.md +8 -5
- package/skills/dx-org-trial-expiration-check/SKILL.md +239 -0
- package/skills/dx-org-trial-expiration-check/scripts/check_expiration.sh +588 -0
- package/skills/experience-ui-bundle-app-coordinate/SKILL.md +12 -9
- package/skills/experience-ui-bundle-custom-app-generate/SKILL.md +29 -5
- package/skills/experience-ui-bundle-custom-app-generate/references/configure-metadata-custom-application.md +1 -1
- package/skills/experience-ui-bundle-custom-app-generate/scripts/resolve-uibundle-path.sh +30 -0
- package/skills/experience-ui-bundle-features-generate/SKILL.md +86 -11
- package/skills/experience-ui-bundle-features-generate/references/conflict-resolution-schema.json +20 -0
- package/skills/experience-ui-bundle-features-generate/scripts/verify-react-bundle.sh +14 -0
- package/skills/experience-ui-bundle-frontend-generate/SKILL.md +6 -1
- package/skills/experience-ui-bundle-frontend-generate/references/component.md +34 -1
- package/skills/experience-ui-bundle-frontend-generate/references/page.md +1 -1
- package/skills/experience-ui-bundle-project-generate/SKILL.md +111 -0
- package/skills/experience-ui-bundle-project-generate/scripts/flatten-project.mjs +33 -0
- package/skills/platform-custom-lightning-type-generate/SKILL.md +9 -14
- package/skills/platform-custom-lightning-type-generate/references/widget-rendition.md +27 -111
- package/skills/platform-lightning-type-widget-coordinate/SKILL.md +214 -0
- package/skills/platform-lightning-type-widget-coordinate/examples/existing-lightning-type-with-widget-prompt.md +80 -0
- package/skills/platform-lightning-type-widget-coordinate/examples/new-lightning-type-with-widget-prompt.md +75 -0
- package/skills/platform-lightning-type-widget-coordinate/references/build-plan-format.md +67 -0
- package/skills/platform-lightning-type-widget-coordinate/references/lightning-type-discovery.md +124 -0
- package/skills/platform-lightning-type-widget-coordinate/references/validation-gates.md +130 -0
- package/skills/platform-sharing-owd-configure/SKILL.md +142 -0
- package/skills/platform-sharing-owd-configure/examples/get_owd_output.md +38 -0
- package/skills/platform-sharing-owd-configure/examples/update_owd_output.md +44 -0
- package/skills/platform-sharing-owd-configure/references/access_levels.md +49 -0
- package/skills/platform-sharing-owd-configure/references/metadata_api_approach.md +69 -0
- package/skills/platform-sharing-rules-generate/SKILL.md +89 -19
- package/skills/platform-sharing-rules-generate/examples/test-cases.md +381 -0
- package/skills/platform-widget-generate/SKILL.md +241 -0
- package/skills/platform-widget-generate/examples/conditional.json +73 -0
- package/skills/platform-widget-generate/examples/list-with-foreach.json +66 -0
- package/skills/platform-widget-generate/examples/single-object.json +64 -0
- package/skills/platform-widget-generate/references/schema-from-lightning-type.md +76 -0
- package/skills/platform-widget-generate/references/widget-bundle-layout.md +116 -0
- package/skills/platform-widget-generate/references/widget-meta-directives.md +130 -0
- package/skills/sales-agentforce-pipeline-management-configure/SKILL.md +268 -0
- package/skills/sales-agentforce-pipeline-management-configure/assets/field-completion-template.genAiPromptTemplate-meta.xml +141 -0
- package/skills/sales-agentforce-pipeline-management-configure/assets/pipeline_management_flow.flow-meta.xml +548 -0
- package/skills/sales-agentforce-pipeline-management-configure/assets/sales_management_agent.agent +2938 -0
- package/skills/sales-agentforce-pipeline-management-configure/assets/sales_management_agent.bundle-meta.xml +4 -0
- package/skills/sales-agentforce-pipeline-management-configure/assets/sfdx-project.json +12 -0
- package/skills/sales-agentforce-pipeline-management-configure/assets/stage-descriptions/bant.txt +11 -0
- package/skills/sales-agentforce-pipeline-management-configure/assets/stage-descriptions/meddic.txt +11 -0
- package/skills/sales-agentforce-pipeline-management-configure/assets/stage-descriptions/spiced.txt +11 -0
- package/skills/sales-agentforce-pipeline-management-configure/examples/custom-prompt-instructions.md +317 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/admin-communication.md +355 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/agent-creation.md +383 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/auth-and-cli.md +146 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/automation-matrix.md +23 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/autonomous-updates.md +215 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/canonical-agent-path.md +75 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/data-sources.md +155 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/field-completion-prompt-template.md +120 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/field-completion-prompts.md +149 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/flags.md +13 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/flow-clone-from-template.md +447 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/metadata-inventory.md +330 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/opportunity-stages.md +246 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/repair-diagnostics.md +265 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/setup-order.md +263 -0
- package/skills/sales-agentforce-pipeline-management-configure/references/soap-api-enablement.md +528 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/CHANGES.md +180 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/README.md +56 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/add-field-suggestion.sh +679 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/create-agent.sh +302 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/create-flow.sh +250 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/define-agent-access.sh +430 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/deploy-settings.sh +77 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/enable-deal-agent.sh +241 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/enable-prerequisites.sh +538 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/flow-debug-and-verify.sh +385 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/retrieve-settings.sh +131 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/setup-all.sh +2791 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/agent-bundle-publish.sh +189 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/agent-detection.sh +239 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/flow-builder.sh +238 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/psg.sh +48 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/soap.sh +140 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/stage-descriptions.sh +448 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/shared/test-opp.sh +166 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/verify-all.sh +636 -0
- package/skills/sales-agentforce-pipeline-management-configure/scripts/verify-prompt-generation.sh +346 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Add an AI field-update suggestion for an Opportunity text field.
|
|
4
|
+
#
|
|
5
|
+
# WHEN to use this: to add a field AFTER initial setup. Greenfield setup is
|
|
6
|
+
# field-selection-driven — run setup-all.sh with --fields "<...>" to build the
|
|
7
|
+
# flow with the desired field set from the start (setup-all.sh calls this script
|
|
8
|
+
# internally with --skip-flow to create/activate each selected field's template,
|
|
9
|
+
# then wires the whole set via shared/flow-builder.sh build_field_collection). Reach
|
|
10
|
+
# for this script directly only to add a NEW field to an already-configured org:
|
|
11
|
+
# it creates/activates the template AND wires the field into the live flow.
|
|
12
|
+
#
|
|
13
|
+
# WHAT this does (the three things a field needs to produce suggestions):
|
|
14
|
+
# 1. Creates + ACTIVATES a field-completion (type einstein_gpt__fieldCompletion)
|
|
15
|
+
# GenAiPromptTemplate for the field, from the canonical asset
|
|
16
|
+
# assets/field-completion-template.genAiPromptTemplate-meta.xml.
|
|
17
|
+
# 2. Adds the field's API name to the ACTIVE suggestion flow's
|
|
18
|
+
# OpportunityFields collection (Process_Field_Update_Suggestions),
|
|
19
|
+
# idempotently, so getOrExecFieldUpdtSuggestion asks for it.
|
|
20
|
+
# 3. Verifies the field is accessible (FLS) — warns if not.
|
|
21
|
+
#
|
|
22
|
+
# WHY this is hardened (no retry loops — the failures this AVOIDS):
|
|
23
|
+
# - Template activation is DETERMINISTIC. GenAiPromptTemplate IS retrievable
|
|
24
|
+
# via CLI for user-created templates, so we deploy (Published, no version
|
|
25
|
+
# identifiers), RETRIEVE the platform-generated <versionIdentifier>, set it
|
|
26
|
+
# as <activeVersionIdentifier>, and redeploy. We never guess the identifier.
|
|
27
|
+
# - The flow edit is XML-aware (python3 ElementTree, namespace-registered),
|
|
28
|
+
# not sed. It inserts a <assignmentItems> block ONLY if the field is absent
|
|
29
|
+
# (idempotent), preserves every sales_pipe_mgmt__* namespace reference, and
|
|
30
|
+
# keeps <status>Active</status> so the running schedule is never deactivated.
|
|
31
|
+
# - Verification uses the SYNCHRONOUS /generations endpoint (proves the
|
|
32
|
+
# template resolves + grounds now), not the async AiGenActionItem schedule
|
|
33
|
+
# path. The scheduled suggestions land in AiGenActionItem (~2-3 min after
|
|
34
|
+
# the daily run; poll >=240s); that guidance is printed, not blocked on.
|
|
35
|
+
#
|
|
36
|
+
# Usage:
|
|
37
|
+
# ./add-field-suggestion.sh <org-alias> <FieldApiName> [options]
|
|
38
|
+
#
|
|
39
|
+
# Options:
|
|
40
|
+
# --label "<text>" Field label for prompt text (default: from describe)
|
|
41
|
+
# --instruction "<text>" One-line field-specific extraction guidance
|
|
42
|
+
# --goal "<text>" One phrase for "You must think about <goal>"
|
|
43
|
+
# --name <DevName> GenAiPromptTemplate developerName (default: Recommend<Field>forOpp)
|
|
44
|
+
# --verify-with-note Seed a ContentNote on a sample opp + run /generations
|
|
45
|
+
# --note "<text>" Body of the seeded verification note (used with
|
|
46
|
+
# --verify-with-note). Pass a note tailored to this
|
|
47
|
+
# field's goal/instruction so generation exercises the
|
|
48
|
+
# customized prompt. Default: a generic multi-signal note.
|
|
49
|
+
# --opp <OppId> Opportunity to use for --verify-with-note (default: first open opp)
|
|
50
|
+
# --skip-flow Create/activate the template only; do not edit the flow
|
|
51
|
+
# --force Regenerate + redeploy the template even if it is
|
|
52
|
+
# already active (mints a new template version)
|
|
53
|
+
#
|
|
54
|
+
# OOTB fields (currently only NextStep): two modes
|
|
55
|
+
# The flow no longer ships every OOTB field — a field only generates suggestions
|
|
56
|
+
# once it is wired into the flow's OpportunityFields collection. For a field that
|
|
57
|
+
# ships with a managed field-completion prompt, this script has two modes:
|
|
58
|
+
#
|
|
59
|
+
# 1. WIRE-ONLY (no --goal/--instruction): keep the curated OOTB prompt exactly as
|
|
60
|
+
# shipped and just wire the field into the flow. Nothing is deployed under a
|
|
61
|
+
# custom template name; Steps 2-3 are skipped. This is the common way to add
|
|
62
|
+
# NextStep post-setup when it was not selected during initial setup.
|
|
63
|
+
# 2. OVERRIDE (with --goal and/or --instruction): deploy an OVERRIDE of the managed
|
|
64
|
+
# template (<overrideSource>), which the platform records as a NEW VERSION of
|
|
65
|
+
# that managed template — not a separate prompt. This is the only supported way
|
|
66
|
+
# to customize the OOTB goal/instruction.
|
|
67
|
+
#
|
|
68
|
+
# Either way Step 4 runs its idempotent flow wiring to guarantee the field is in
|
|
69
|
+
# OpportunityFields. Passing --skip-flow with an OOTB field and no --goal/
|
|
70
|
+
# --instruction is a no-op and is rejected. StageName is a picklist and is not
|
|
71
|
+
# eligible for either mode.
|
|
72
|
+
#
|
|
73
|
+
# Re-run safety: the flow edit (Step 4) is fully idempotent. The template
|
|
74
|
+
# deploy (Steps 2-3) is NOT — a redeploy mints a NEW template version each run.
|
|
75
|
+
# To avoid churning versions on an already-configured field, this script
|
|
76
|
+
# SKIPS Steps 2-3 when the template already exists and is active, unless
|
|
77
|
+
# --force is passed. The flow wiring and verification still run.
|
|
78
|
+
|
|
79
|
+
set -euo pipefail
|
|
80
|
+
|
|
81
|
+
# sf CLI can emit ANSI color codes inside --json stdout, breaking jq parsing.
|
|
82
|
+
# 2>/dev/null only strips stderr; these env vars are the reliable fix (see PM notes).
|
|
83
|
+
export NO_COLOR=1
|
|
84
|
+
export FORCE_COLOR=0
|
|
85
|
+
|
|
86
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
87
|
+
ASSET_TEMPLATE="$SCRIPT_DIR/../assets/field-completion-template.genAiPromptTemplate-meta.xml"
|
|
88
|
+
FLOW_API_NAME="Process_Field_Update_Suggestions"
|
|
89
|
+
TEMPLATE_DIR="force-app/main/default/genAiPromptTemplates"
|
|
90
|
+
FLOW_DIR="force-app/main/default/flows"
|
|
91
|
+
# Metadata is authored at 67.0 (see sfdx-project.json below), but the Einstein
|
|
92
|
+
# prompt-template REST endpoints are pinned to v64.0 — the version verified to
|
|
93
|
+
# return the /generations and /prompt-templates shapes this script parses, and
|
|
94
|
+
# the version the sibling setup scripts use. Deliberate split; do not "upgrade".
|
|
95
|
+
API_VERSION="v64.0"
|
|
96
|
+
# Hard cap on the number of Opportunity fields the suggestion flow may manage,
|
|
97
|
+
# OOTB fields (NextStep, StageName) inclusive. Pipeline Management supports at
|
|
98
|
+
# most this many managed fields; the flow-wiring step below refuses to exceed it.
|
|
99
|
+
MAX_SUGGESTION_FIELDS=5
|
|
100
|
+
|
|
101
|
+
# OOTB fields that already ship with a MANAGED field-completion prompt template
|
|
102
|
+
# (sales_pipe_mgmt namespace). You cannot edit a managed template, so customizing
|
|
103
|
+
# one of these fields' goal/instruction is done by deploying an OVERRIDE — a
|
|
104
|
+
# fieldCompletion template whose <overrideSource> points at the managed template.
|
|
105
|
+
# The platform records the override as a NEW VERSION of the managed template
|
|
106
|
+
# (verified on a live org), NOT as a separate prompt. Overriding only happens when
|
|
107
|
+
# the caller explicitly passes --goal/--instruction; otherwise the OOTB prompt is
|
|
108
|
+
# left untouched. Only NextStep is listed: StageName is a picklist and is rejected
|
|
109
|
+
# by the eligibility gate below, so it is intentionally NOT overridable here.
|
|
110
|
+
declare -A OOTB_MANAGED_TEMPLATE=(
|
|
111
|
+
[NextStep]="sales_pipe_mgmt__RecommendNextStepforOpp"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# --- Helpers (mirror setup-all.sh / define-agent-access.sh conventions) ---
|
|
115
|
+
log_pass() { echo " [PASS] $1"; }
|
|
116
|
+
log_warn() { echo " [WARN] $1"; }
|
|
117
|
+
log_fail() { echo " [FAIL] $1"; }
|
|
118
|
+
log_info() { echo " [....] $1"; }
|
|
119
|
+
log_try() { echo " [TRY ] $1"; }
|
|
120
|
+
|
|
121
|
+
die() { log_fail "$1"; exit 1; }
|
|
122
|
+
|
|
123
|
+
# --- Arg parsing ---
|
|
124
|
+
ORG_ALIAS="${1:-}"
|
|
125
|
+
FIELD_API="${2:-}"
|
|
126
|
+
FIELD_LABEL=""
|
|
127
|
+
FIELD_INSTRUCTION=""
|
|
128
|
+
FIELD_GOAL=""
|
|
129
|
+
DEV_NAME=""
|
|
130
|
+
VERIFY_WITH_NOTE=false
|
|
131
|
+
OPP_ID=""
|
|
132
|
+
NOTE_TEXT="" # --note "<text>": caller-supplied verification note body (used with --verify-with-note)
|
|
133
|
+
SKIP_FLOW=false
|
|
134
|
+
FORCE=false
|
|
135
|
+
|
|
136
|
+
if [[ -z "$ORG_ALIAS" || -z "$FIELD_API" ]]; then
|
|
137
|
+
echo "Usage: $0 <org-alias> <FieldApiName> [--label TEXT] [--instruction TEXT] [--goal TEXT] [--name DevName] [--verify-with-note] [--note TEXT] [--opp OppId] [--skip-flow] [--force]"
|
|
138
|
+
exit 1
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
shift 2 || true
|
|
142
|
+
# Require a value for value-taking flags (else a trailing bare flag would make
|
|
143
|
+
# `shift 2` fail and abort under `set -e` with no message).
|
|
144
|
+
need_val() { [[ $# -ge 2 ]] || die "Option '$1' requires a value."; }
|
|
145
|
+
while [[ $# -gt 0 ]]; do
|
|
146
|
+
case "$1" in
|
|
147
|
+
--label) need_val "$@"; FIELD_LABEL="$2"; shift 2 ;;
|
|
148
|
+
--instruction) need_val "$@"; FIELD_INSTRUCTION="$2"; shift 2 ;;
|
|
149
|
+
--goal) need_val "$@"; FIELD_GOAL="$2"; shift 2 ;;
|
|
150
|
+
--name) need_val "$@"; DEV_NAME="$2"; shift 2 ;;
|
|
151
|
+
--opp) need_val "$@"; OPP_ID="$2"; shift 2 ;;
|
|
152
|
+
--note) need_val "$@"; NOTE_TEXT="$2"; shift 2 ;;
|
|
153
|
+
--verify-with-note) VERIFY_WITH_NOTE=true; shift ;;
|
|
154
|
+
--skip-flow) SKIP_FLOW=true; shift ;;
|
|
155
|
+
--force) FORCE=true; shift ;;
|
|
156
|
+
*) die "Unknown option: $1" ;;
|
|
157
|
+
esac
|
|
158
|
+
done
|
|
159
|
+
|
|
160
|
+
command -v python3 >/dev/null 2>&1 || die "python3 is required for the XML-aware flow edit."
|
|
161
|
+
[[ -f "$ASSET_TEMPLATE" ]] || die "Canonical template asset not found: $ASSET_TEMPLATE"
|
|
162
|
+
|
|
163
|
+
# Ensure a project context exists for deploy/retrieve.
|
|
164
|
+
if [[ ! -f "sfdx-project.json" ]]; then
|
|
165
|
+
cat > sfdx-project.json << 'EOF'
|
|
166
|
+
{"packageDirectories": [{"path": "force-app", "default": true}], "sourceApiVersion": "67.0"}
|
|
167
|
+
EOF
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
echo "Add Field Suggestion: $FIELD_API (org: $ORG_ALIAS)"
|
|
171
|
+
echo ""
|
|
172
|
+
|
|
173
|
+
# ============================================================
|
|
174
|
+
# Step 1 — Validate field eligibility (describe)
|
|
175
|
+
# ============================================================
|
|
176
|
+
echo "Step 1: Validate field eligibility"
|
|
177
|
+
|
|
178
|
+
DESCRIBE=$(sf sobject describe --sobject Opportunity --target-org "$ORG_ALIAS" --json 2>/dev/null) \
|
|
179
|
+
|| die "Could not describe Opportunity on org '$ORG_ALIAS'. Check auth."
|
|
180
|
+
|
|
181
|
+
FIELD_JSON=$(echo "$DESCRIBE" | jq -c --arg f "$FIELD_API" '.result.fields[] | select(.name == $f)')
|
|
182
|
+
if [[ -z "$FIELD_JSON" ]]; then
|
|
183
|
+
# `describe` only returns fields the running user can SEE. A freshly created
|
|
184
|
+
# custom field has NO field-level security, so it exists but is invisible here.
|
|
185
|
+
# Distinguish "doesn't exist" from "no FLS" via the Tooling API, and tell the
|
|
186
|
+
# user the exact fix — so this never turns into blind retries.
|
|
187
|
+
EXISTS=$(sf data query -q "SELECT QualifiedApiName FROM FieldDefinition WHERE EntityDefinition.QualifiedApiName='Opportunity' AND QualifiedApiName='${FIELD_API}'" \
|
|
188
|
+
--target-org "$ORG_ALIAS" --use-tooling-api --json 2>/dev/null | jq -r '.result.records[0].QualifiedApiName // empty')
|
|
189
|
+
if [[ -n "$EXISTS" ]]; then
|
|
190
|
+
ADMIN_PS=$(sf data query -q "SELECT Id FROM PermissionSet WHERE IsOwnedByProfile=true AND Profile.Name='System Administrator' LIMIT 1" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].Id // empty')
|
|
191
|
+
echo ""
|
|
192
|
+
log_fail "Field '$FIELD_API' exists but is not visible to you — it has no field-level security yet."
|
|
193
|
+
echo " A newly created custom field is invisible to 'describe' until FLS is granted."
|
|
194
|
+
echo " Grant read/edit to your admin permission set, then re-run this script:"
|
|
195
|
+
echo ""
|
|
196
|
+
echo " sf data create record --sobject FieldPermissions \\"
|
|
197
|
+
echo " --values \"ParentId='${ADMIN_PS:-<AdminPermSetId>}' SobjectType='Opportunity' Field='Opportunity.${FIELD_API}' PermissionsRead=true PermissionsEdit=true\" \\"
|
|
198
|
+
echo " --target-org $ORG_ALIAS"
|
|
199
|
+
echo ""
|
|
200
|
+
exit 1
|
|
201
|
+
fi
|
|
202
|
+
die "Field '$FIELD_API' not found on Opportunity (no such field in FieldDefinition)."
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
F_TYPE=$(echo "$FIELD_JSON" | jq -r '.type')
|
|
206
|
+
F_LEN=$(echo "$FIELD_JSON" | jq -r '.length')
|
|
207
|
+
F_UPDATEABLE=$(echo "$FIELD_JSON" | jq -r '.updateable')
|
|
208
|
+
F_CALCULATED=$(echo "$FIELD_JSON" | jq -r '.calculated')
|
|
209
|
+
F_HTML=$(echo "$FIELD_JSON" | jq -r '.htmlFormatted')
|
|
210
|
+
F_LABEL_DESCRIBED=$(echo "$FIELD_JSON" | jq -r '.label')
|
|
211
|
+
|
|
212
|
+
# Eligibility: plain Text (type=string) or Text Area <=255 (type=textarea), updateable,
|
|
213
|
+
# not calculated, not rich text (htmlFormatted), length <= 255.
|
|
214
|
+
# - Text Area (255) -> type=textarea, htmlFormatted=false, length<=255 ✅
|
|
215
|
+
# - Long Text Area -> type=textarea, length>255 ❌ (length gate)
|
|
216
|
+
# - Rich Text Area -> type=textarea, htmlFormatted=true ❌ (html gate)
|
|
217
|
+
if [[ "$F_TYPE" != "string" && "$F_TYPE" != "textarea" ]]; then
|
|
218
|
+
die "Field '$FIELD_API' is type '$F_TYPE'. Only plain Text (type=string) or Text Area <=255 (type=textarea) fields are supported (not richtext/longtextarea/picklist/formula/date). Suggestions are capped at the field length."
|
|
219
|
+
fi
|
|
220
|
+
[[ "$F_HTML" == "true" ]] && die "Field '$FIELD_API' is a Rich Text Area (htmlFormatted). Not supported — use a plain Text or Text Area (<=255) field."
|
|
221
|
+
[[ "$F_UPDATEABLE" == "true" ]] || die "Field '$FIELD_API' is not updateable."
|
|
222
|
+
[[ "$F_CALCULATED" == "false" ]] || die "Field '$FIELD_API' is calculated (formula). Not supported."
|
|
223
|
+
if [[ "$F_LEN" -gt 255 ]]; then
|
|
224
|
+
die "Field '$FIELD_API' length is $F_LEN (>255 — Long Text Area). Not supported; use a Text or Text Area field of 255 or fewer characters."
|
|
225
|
+
fi
|
|
226
|
+
log_pass "Eligible: type=$F_TYPE, length=$F_LEN, updateable=$F_UPDATEABLE"
|
|
227
|
+
|
|
228
|
+
# OOTB field detection. If this field already ships with a managed field-completion
|
|
229
|
+
# template (only NextStep today), we NEVER author a fresh standalone template. Two
|
|
230
|
+
# modes, decided by whether the caller passed --goal/--instruction:
|
|
231
|
+
# * WIRE_ONLY (no --goal/--instruction): keep the curated OOTB prompt untouched and
|
|
232
|
+
# just wire the field into the flow. The flow no longer ships every OOTB field, so
|
|
233
|
+
# wiring NextStep post-setup is a legitimate, common action. Nothing is deployed.
|
|
234
|
+
# * OVERRIDE (--goal/--instruction given): author an OVERRIDE of the managed template
|
|
235
|
+
# (<overrideSource>), which the platform records as a NEW VERSION of the managed
|
|
236
|
+
# template. This is the ONLY supported way to change the OOTB goal/instruction.
|
|
237
|
+
# We require an explicit --goal/--instruction before overriding: doing it with the
|
|
238
|
+
# generic defaults would silently replace a curated OOTB prompt with something weaker.
|
|
239
|
+
OVERRIDE_SOURCE=""
|
|
240
|
+
WIRE_ONLY=false
|
|
241
|
+
if [[ -n "${OOTB_MANAGED_TEMPLATE[$FIELD_API]:-}" ]]; then
|
|
242
|
+
if [[ -z "$FIELD_GOAL" && -z "$FIELD_INSTRUCTION" ]]; then
|
|
243
|
+
if [[ "$SKIP_FLOW" == "true" ]]; then
|
|
244
|
+
die "'$FIELD_API' ships with a curated OOTB managed prompt and no --goal/--instruction was given, so there is no template to deploy — and --skip-flow means there is nothing to wire either. Nothing to do. To customize the prompt pass --goal/--instruction; to wire the field into the flow drop --skip-flow."
|
|
245
|
+
fi
|
|
246
|
+
WIRE_ONLY=true
|
|
247
|
+
log_info "OOTB wire-only mode: '$FIELD_API' keeps its curated OOTB prompt; wiring it into the flow (idempotent). Pass --goal/--instruction to also customize the prompt."
|
|
248
|
+
else
|
|
249
|
+
OVERRIDE_SOURCE="${OOTB_MANAGED_TEMPLATE[$FIELD_API]}"
|
|
250
|
+
log_info "OOTB override mode: '$FIELD_API' overrides managed template '$OVERRIDE_SOURCE' (deploys as a new version)."
|
|
251
|
+
fi
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
# Defaults derived from describe.
|
|
255
|
+
[[ -z "$FIELD_LABEL" ]] && FIELD_LABEL="$F_LABEL_DESCRIBED"
|
|
256
|
+
[[ -z "$DEV_NAME" ]] && DEV_NAME="Recommend$(echo "$FIELD_API" | sed 's/__c$//' | sed 's/[^A-Za-z0-9]//g')forOpp"
|
|
257
|
+
# developerName must start with a letter and be alphanumeric (Recommend<...>forOpp
|
|
258
|
+
# always satisfies this, but a custom --name could not).
|
|
259
|
+
[[ "$DEV_NAME" =~ ^[A-Za-z][A-Za-z0-9]*$ ]] || die "Template dev name '$DEV_NAME' is invalid (must start with a letter, alphanumeric only). Pass a valid --name."
|
|
260
|
+
[[ -z "$FIELD_GOAL" ]] && FIELD_GOAL="what the most appropriate value of this field is, based on the latest conversation signals."
|
|
261
|
+
[[ -z "$FIELD_INSTRUCTION" ]] && FIELD_INSTRUCTION="Extract only what is explicitly supported by the data sources."
|
|
262
|
+
CHAR_CAP="$F_LEN"
|
|
263
|
+
|
|
264
|
+
# FLS check for the agent user (non-blocking warn).
|
|
265
|
+
AGENT_USER=$(sf data query -q "SELECT Username FROM User WHERE Username LIKE 'salesmanagementagentuser@%'" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].Username // empty')
|
|
266
|
+
if [[ -n "$AGENT_USER" ]]; then
|
|
267
|
+
log_info "Agent user: $AGENT_USER (ensure it has edit FLS on $FIELD_API for autonomous updates)"
|
|
268
|
+
else
|
|
269
|
+
log_warn "Could not resolve the agent user; verify $FIELD_API is accessible to the agent's PSG."
|
|
270
|
+
fi
|
|
271
|
+
echo ""
|
|
272
|
+
|
|
273
|
+
# ============================================================
|
|
274
|
+
# Pre-check — is the template already active? (idempotency guard for #7)
|
|
275
|
+
# A redeploy round-trip (Steps 2-3) mints a NEW template version every run, so
|
|
276
|
+
# skip it when the template is already active and --force was not passed. Uses
|
|
277
|
+
# the same prompt-templates discovery endpoint Step 3 uses to confirm activation.
|
|
278
|
+
# ============================================================
|
|
279
|
+
# The name to look up in the /einstein/prompt-templates endpoint. An OVERRIDE folds
|
|
280
|
+
# into the managed template's record, so its own (un-namespaced) DeveloperName never
|
|
281
|
+
# appears there — the managed name does. Use the managed name for active-state reads
|
|
282
|
+
# in override mode, and the template's own name otherwise.
|
|
283
|
+
# WIRE_ONLY keeps the managed OOTB template (nothing deployed under DEV_NAME), so
|
|
284
|
+
# active-state reads and /generations must target the managed template's name.
|
|
285
|
+
if [[ "$WIRE_ONLY" == "true" ]]; then
|
|
286
|
+
TEMPLATE_QUERY_NAME="${OOTB_MANAGED_TEMPLATE[$FIELD_API]}"
|
|
287
|
+
else
|
|
288
|
+
TEMPLATE_QUERY_NAME="${OVERRIDE_SOURCE:-$DEV_NAME}"
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
TEMPLATE_ALREADY_ACTIVE=false
|
|
292
|
+
# Skip the idempotency shortcut in override mode: the managed template is ALWAYS
|
|
293
|
+
# active OOTB, so a query would always read "active" and wrongly skip the override
|
|
294
|
+
# deploy. An override is an explicit, opt-in action (--goal/--instruction), so always
|
|
295
|
+
# (re)deploy it when requested; --force is only meaningful for standalone templates.
|
|
296
|
+
if [[ "$FORCE" != "true" && -z "$OVERRIDE_SOURCE" && "$WIRE_ONLY" != "true" ]]; then
|
|
297
|
+
EXISTING_ACTIVE=$(sf api request rest "/services/data/${API_VERSION}/einstein/prompt-templates" --target-org "$ORG_ALIAS" 2>/dev/null \
|
|
298
|
+
| jq -r --arg n "$DEV_NAME" '.promptRecords[]? | select(.fields.DeveloperName.value==$n) | .fields.IsActive.value' 2>/dev/null | head -1)
|
|
299
|
+
if [[ "$EXISTING_ACTIVE" == "true" ]]; then
|
|
300
|
+
TEMPLATE_ALREADY_ACTIVE=true
|
|
301
|
+
fi
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
if [[ "$WIRE_ONLY" == "true" ]]; then
|
|
305
|
+
echo "Steps 2-3: SKIPPED — '$FIELD_API' keeps its curated OOTB managed prompt (wire-only mode)."
|
|
306
|
+
log_info "No template deployed. Pass --goal/--instruction to deploy an override of the managed prompt."
|
|
307
|
+
echo ""
|
|
308
|
+
elif [[ "$TEMPLATE_ALREADY_ACTIVE" == "true" ]]; then
|
|
309
|
+
echo "Steps 2-3: SKIPPED — template '$DEV_NAME' already exists and is ACTIVE."
|
|
310
|
+
log_info "Not redeploying (would mint a new version). Pass --force to regenerate + redeploy."
|
|
311
|
+
echo ""
|
|
312
|
+
else
|
|
313
|
+
|
|
314
|
+
# ============================================================
|
|
315
|
+
# Step 2 — Generate template XML from canonical asset
|
|
316
|
+
# ============================================================
|
|
317
|
+
echo "Step 2: Generate template XML ($DEV_NAME)"
|
|
318
|
+
mkdir -p "$TEMPLATE_DIR"
|
|
319
|
+
TEMPLATE_FILE="$TEMPLATE_DIR/${DEV_NAME}.genAiPromptTemplate-meta.xml"
|
|
320
|
+
MASTER_LABEL="Recommend ${FIELD_LABEL} for Opportunity"
|
|
321
|
+
|
|
322
|
+
# Substitute placeholders with python3 (safe with arbitrary text; no sed-escaping traps).
|
|
323
|
+
FIELD_API="$FIELD_API" FIELD_LABEL="$FIELD_LABEL" DEV_NAME="$DEV_NAME" \
|
|
324
|
+
MASTER_LABEL="$MASTER_LABEL" CHAR_CAP="$CHAR_CAP" FIELD_GOAL="$FIELD_GOAL" \
|
|
325
|
+
FIELD_INSTRUCTION="$FIELD_INSTRUCTION" OVERRIDE_SOURCE="$OVERRIDE_SOURCE" \
|
|
326
|
+
ASSET="$ASSET_TEMPLATE" OUT="$TEMPLATE_FILE" \
|
|
327
|
+
python3 - << 'PYEOF'
|
|
328
|
+
import os, re, xml.sax.saxutils as sx
|
|
329
|
+
src = open(os.environ["ASSET"], encoding="utf-8").read()
|
|
330
|
+
# Drop the leading XML comment (guidance only) so the deployed file is clean.
|
|
331
|
+
src = re.sub(r"<!--.*?-->\s*", "", src, count=1, flags=re.DOTALL)
|
|
332
|
+
repl = {
|
|
333
|
+
"@@DEVNAME@@": os.environ["DEV_NAME"],
|
|
334
|
+
"@@MASTERLABEL@@": os.environ["MASTER_LABEL"],
|
|
335
|
+
"@@FIELD@@": os.environ["FIELD_API"],
|
|
336
|
+
"@@FIELDLABEL@@": os.environ["FIELD_LABEL"],
|
|
337
|
+
"@@CHARCAP@@": os.environ["CHAR_CAP"],
|
|
338
|
+
"@@FIELD_GOAL@@": os.environ["FIELD_GOAL"],
|
|
339
|
+
"@@FIELD_INSTRUCTION@@": os.environ["FIELD_INSTRUCTION"],
|
|
340
|
+
}
|
|
341
|
+
for k, v in repl.items():
|
|
342
|
+
# Escape XML special chars in substituted values (they live inside element text).
|
|
343
|
+
src = src.replace(k, sx.escape(v))
|
|
344
|
+
# OOTB override mode: the canonical asset ships <overridable>false</overridable>.
|
|
345
|
+
# To register as an override of a managed template, replace that element with
|
|
346
|
+
# <overrideSource>NAMESPACE__ManagedTemplate</overrideSource>. The platform then
|
|
347
|
+
# treats this deploy as a new version of that managed template rather than a new
|
|
348
|
+
# standalone prompt (verified on a live org).
|
|
349
|
+
ovr = os.environ.get("OVERRIDE_SOURCE", "")
|
|
350
|
+
if ovr:
|
|
351
|
+
new_src, n = re.subn(
|
|
352
|
+
r"<overridable>[^<]*</overridable>",
|
|
353
|
+
"<overrideSource>%s</overrideSource>" % sx.escape(ovr),
|
|
354
|
+
src, count=1)
|
|
355
|
+
if n != 1:
|
|
356
|
+
raise SystemExit("expected exactly one <overridable> element to replace for override mode; found %d" % n)
|
|
357
|
+
src = new_src
|
|
358
|
+
open(os.environ["OUT"], "w", encoding="utf-8").write(src)
|
|
359
|
+
print(" wrote " + os.environ["OUT"] + ((" (override of %s)" % ovr) if ovr else ""))
|
|
360
|
+
PYEOF
|
|
361
|
+
|
|
362
|
+
grep -q "@@" "$TEMPLATE_FILE" && die "Unsubstituted placeholder remains in $TEMPLATE_FILE"
|
|
363
|
+
log_pass "Template XML generated (SuggestedNewValue cap = $CHAR_CAP chars)"
|
|
364
|
+
echo ""
|
|
365
|
+
|
|
366
|
+
# ============================================================
|
|
367
|
+
# Step 3 — Deterministic deploy + activate (retrieve round-trip)
|
|
368
|
+
# ============================================================
|
|
369
|
+
echo "Step 3: Deploy and activate template (deterministic round-trip)"
|
|
370
|
+
|
|
371
|
+
log_try "Deploy (status=Published, no version identifiers)"
|
|
372
|
+
DEPLOY1=$(sf project deploy start --source-dir "$TEMPLATE_FILE" --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
|
|
373
|
+
if ! echo "$DEPLOY1" | jq -e '.status == 0' >/dev/null 2>&1; then
|
|
374
|
+
ERR=$(echo "$DEPLOY1" | jq -r '.result.details.componentFailures[0].problem // .message // "unknown"' 2>/dev/null)
|
|
375
|
+
die "Initial template deploy failed: $ERR"
|
|
376
|
+
fi
|
|
377
|
+
log_pass "Deployed (inactive — no active version yet)"
|
|
378
|
+
|
|
379
|
+
log_try "Retrieve platform-generated versionIdentifier"
|
|
380
|
+
sf project retrieve start --metadata "GenAiPromptTemplate:${DEV_NAME}" --target-org "$ORG_ALIAS" --json 2>/dev/null >/dev/null \
|
|
381
|
+
|| die "Retrieve failed — cannot read versionIdentifier."
|
|
382
|
+
|
|
383
|
+
# Parse the versionIdentifier as the direct child of <templateVersions> (not a
|
|
384
|
+
# file-wide grep) so a future multi-version template can't hand us the wrong id.
|
|
385
|
+
# A freshly created template has exactly one version; assert that.
|
|
386
|
+
VERSION_ID=$(python3 - "$TEMPLATE_FILE" << 'PYEOF'
|
|
387
|
+
import sys, xml.etree.ElementTree as ET
|
|
388
|
+
NS = "http://soap.sforce.com/2006/04/metadata"; q = lambda t: "{%s}%s" % (NS, t)
|
|
389
|
+
root = ET.parse(sys.argv[1]).getroot()
|
|
390
|
+
ids = [tv.findtext(q("versionIdentifier")) for tv in root.findall(q("templateVersions"))]
|
|
391
|
+
ids = [i for i in ids if i]
|
|
392
|
+
if len(ids) == 1:
|
|
393
|
+
print(ids[0])
|
|
394
|
+
elif len(ids) > 1:
|
|
395
|
+
# Unexpected for a new template; take the last (newest) and note it on stderr.
|
|
396
|
+
sys.stderr.write("multiple versionIdentifiers found; using the last\n"); print(ids[-1])
|
|
397
|
+
PYEOF
|
|
398
|
+
)
|
|
399
|
+
[[ -n "$VERSION_ID" ]] || die "No <versionIdentifier> found after retrieve. Template may not have deployed as a version."
|
|
400
|
+
log_pass "versionIdentifier = $VERSION_ID"
|
|
401
|
+
|
|
402
|
+
# Set activeVersionIdentifier (idempotent: replace if present, else insert after <overridable>).
|
|
403
|
+
if grep -q "<activeVersionIdentifier>" "$TEMPLATE_FILE"; then
|
|
404
|
+
VERSION_ID="$VERSION_ID" python3 - "$TEMPLATE_FILE" << 'PYEOF'
|
|
405
|
+
import os, re, sys
|
|
406
|
+
p = sys.argv[1]; s = open(p).read()
|
|
407
|
+
s = re.sub(r"<activeVersionIdentifier>[^<]*</activeVersionIdentifier>",
|
|
408
|
+
"<activeVersionIdentifier>%s</activeVersionIdentifier>" % os.environ["VERSION_ID"], s)
|
|
409
|
+
open(p, "w").write(s)
|
|
410
|
+
PYEOF
|
|
411
|
+
else
|
|
412
|
+
VERSION_ID="$VERSION_ID" python3 - "$TEMPLATE_FILE" << 'PYEOF'
|
|
413
|
+
import os, re, sys
|
|
414
|
+
p = sys.argv[1]; s = open(p).read()
|
|
415
|
+
tag = " <activeVersionIdentifier>%s</activeVersionIdentifier>\n" % os.environ["VERSION_ID"]
|
|
416
|
+
# Insert right after the opening <GenAiPromptTemplate ...> line.
|
|
417
|
+
s = re.sub(r"(<GenAiPromptTemplate[^>]*>\n)", r"\1" + tag, s, count=1)
|
|
418
|
+
open(p, "w").write(s)
|
|
419
|
+
PYEOF
|
|
420
|
+
fi
|
|
421
|
+
|
|
422
|
+
log_try "Redeploy to activate"
|
|
423
|
+
DEPLOY2=$(sf project deploy start --source-dir "$TEMPLATE_FILE" --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
|
|
424
|
+
if ! echo "$DEPLOY2" | jq -e '.status == 0' >/dev/null 2>&1; then
|
|
425
|
+
ERR=$(echo "$DEPLOY2" | jq -r '.result.details.componentFailures[0].problem // .message // "unknown"' 2>/dev/null)
|
|
426
|
+
die "Activation redeploy failed: $ERR"
|
|
427
|
+
fi
|
|
428
|
+
|
|
429
|
+
# Confirm active via the prompt-templates discovery endpoint. In override mode this
|
|
430
|
+
# reads the MANAGED template's record (the override folds into it), which is expected
|
|
431
|
+
# to stay active — the successful redeploy above is the real proof the override took.
|
|
432
|
+
IS_ACTIVE=$(sf api request rest "/services/data/${API_VERSION}/einstein/prompt-templates" --target-org "$ORG_ALIAS" 2>/dev/null \
|
|
433
|
+
| jq -r --arg n "$TEMPLATE_QUERY_NAME" '.promptRecords[]? | select(.fields.DeveloperName.value==$n) | .fields.IsActive.value' 2>/dev/null | head -1)
|
|
434
|
+
if [[ "$IS_ACTIVE" == "true" ]]; then
|
|
435
|
+
if [[ -n "$OVERRIDE_SOURCE" ]]; then
|
|
436
|
+
log_pass "Override deployed — '$OVERRIDE_SOURCE' is ACTIVE with the new version"
|
|
437
|
+
else
|
|
438
|
+
log_pass "Template '$DEV_NAME' is ACTIVE"
|
|
439
|
+
fi
|
|
440
|
+
else
|
|
441
|
+
log_warn "Template deployed but active-state read returned '$IS_ACTIVE'. Verify in Prompt Builder."
|
|
442
|
+
fi
|
|
443
|
+
echo ""
|
|
444
|
+
|
|
445
|
+
fi # end template deploy/activate block (skipped when already active without --force)
|
|
446
|
+
|
|
447
|
+
# ============================================================
|
|
448
|
+
# Step 4 — Register the field in the active flow (idempotent, XML-aware)
|
|
449
|
+
# ============================================================
|
|
450
|
+
if [[ "$SKIP_FLOW" == "true" ]]; then
|
|
451
|
+
log_info "Skipping flow edit (--skip-flow). Field will NOT generate scheduled suggestions until added to $FLOW_API_NAME."
|
|
452
|
+
echo ""
|
|
453
|
+
else
|
|
454
|
+
echo "Step 4: Register field in the active suggestion flow"
|
|
455
|
+
|
|
456
|
+
FLOW_STATE=$(sf data query -q "SELECT ApiName, IsActive FROM FlowDefinitionView WHERE ApiName='${FLOW_API_NAME}'" --target-org "$ORG_ALIAS" --json 2>/dev/null)
|
|
457
|
+
FLOW_EXISTS=$(echo "$FLOW_STATE" | jq -r '.result.records[0].ApiName // empty')
|
|
458
|
+
FLOW_ACTIVE=$(echo "$FLOW_STATE" | jq -r '.result.records[0].IsActive // empty')
|
|
459
|
+
|
|
460
|
+
if [[ -z "$FLOW_EXISTS" ]]; then
|
|
461
|
+
log_warn "Flow '$FLOW_API_NAME' not found. Run create-flow.sh first, then re-run with --skip-flow already applied."
|
|
462
|
+
else
|
|
463
|
+
if [[ "$FLOW_ACTIVE" != "true" ]]; then
|
|
464
|
+
log_warn "Flow '$FLOW_API_NAME' is INACTIVE. It must be Active for suggestions to run (create-flow.sh)."
|
|
465
|
+
fi
|
|
466
|
+
|
|
467
|
+
log_try "Retrieve flow"
|
|
468
|
+
mkdir -p "$FLOW_DIR"
|
|
469
|
+
sf project retrieve start --metadata "Flow:${FLOW_API_NAME}" --target-org "$ORG_ALIAS" --json 2>/dev/null >/dev/null \
|
|
470
|
+
|| die "Could not retrieve flow '$FLOW_API_NAME'."
|
|
471
|
+
FLOW_FILE="${FLOW_DIR}/${FLOW_API_NAME}.flow-meta.xml"
|
|
472
|
+
[[ -f "$FLOW_FILE" ]] || die "Flow file not present after retrieve: $FLOW_FILE"
|
|
473
|
+
|
|
474
|
+
# Enforce the field cap BEFORE wiring. Count the fields currently wired in the
|
|
475
|
+
# AddOppFieldsToCollection assignment. If the field is already present, adding is
|
|
476
|
+
# a no-op (handled below) and does not count against the cap. If it is new and the
|
|
477
|
+
# flow is already at MAX_SUGGESTION_FIELDS, refuse — the flow can manage no more.
|
|
478
|
+
CAP_RC=0
|
|
479
|
+
CAP_OUT=$(FIELD_API="$FIELD_API" MAX_FIELDS="$MAX_SUGGESTION_FIELDS" python3 - "$FLOW_FILE" << 'PYEOF'
|
|
480
|
+
import os, sys
|
|
481
|
+
import xml.etree.ElementTree as ET
|
|
482
|
+
|
|
483
|
+
NS = "http://soap.sforce.com/2006/04/metadata"
|
|
484
|
+
q = lambda t: "{%s}%s" % (NS, t)
|
|
485
|
+
|
|
486
|
+
path = sys.argv[1]
|
|
487
|
+
field = os.environ["FIELD_API"]
|
|
488
|
+
cap = int(os.environ["MAX_FIELDS"])
|
|
489
|
+
root = ET.parse(path).getroot()
|
|
490
|
+
|
|
491
|
+
target = None
|
|
492
|
+
for a in root.findall(q("assignments")):
|
|
493
|
+
name = a.find(q("name"))
|
|
494
|
+
if name is not None and name.text == "AddOppFieldsToCollection":
|
|
495
|
+
target = a
|
|
496
|
+
break
|
|
497
|
+
if target is None:
|
|
498
|
+
sys.stderr.write("AddOppFieldsToCollection assignment not found\n")
|
|
499
|
+
sys.exit(3)
|
|
500
|
+
|
|
501
|
+
fields = []
|
|
502
|
+
for item in target.findall(q("assignmentItems")):
|
|
503
|
+
sv = item.find(q("value") + "/" + q("stringValue"))
|
|
504
|
+
if sv is not None and sv.text:
|
|
505
|
+
fields.append(sv.text)
|
|
506
|
+
|
|
507
|
+
# Field already wired -> adding it is idempotent, never breaches the cap.
|
|
508
|
+
if field in fields:
|
|
509
|
+
sys.exit(0)
|
|
510
|
+
# New field would push past the cap -> block. Print the wired set for the message.
|
|
511
|
+
if len(fields) >= cap:
|
|
512
|
+
sys.stdout.write(", ".join(fields))
|
|
513
|
+
sys.exit(4)
|
|
514
|
+
sys.exit(0)
|
|
515
|
+
PYEOF
|
|
516
|
+
) || CAP_RC=$?
|
|
517
|
+
if [[ "$CAP_RC" == "4" ]]; then
|
|
518
|
+
die "Field cap reached: the suggestion flow already wires $MAX_SUGGESTION_FIELDS fields (${CAP_OUT}). Pipeline Management supports at most $MAX_SUGGESTION_FIELDS managed Opportunity fields. Remove one before adding '$FIELD_API'."
|
|
519
|
+
elif [[ "$CAP_RC" == "3" ]]; then
|
|
520
|
+
die "Could not locate AddOppFieldsToCollection in flow '$FLOW_API_NAME' — cannot verify the field cap."
|
|
521
|
+
fi
|
|
522
|
+
|
|
523
|
+
# XML-aware idempotent insert: add a <assignmentItems> for the field into the
|
|
524
|
+
# AddOppFieldsToCollection assignment ONLY if not already present. Preserves
|
|
525
|
+
# namespace + all other content. Exits 2 = already present (no-op).
|
|
526
|
+
RC=0
|
|
527
|
+
FIELD_API="$FIELD_API" python3 - "$FLOW_FILE" << 'PYEOF' || RC=$?
|
|
528
|
+
import os, sys
|
|
529
|
+
import xml.etree.ElementTree as ET
|
|
530
|
+
|
|
531
|
+
NS = "http://soap.sforce.com/2006/04/metadata"
|
|
532
|
+
ET.register_namespace("", NS) # keep default namespace, no ns0: prefixes
|
|
533
|
+
q = lambda t: "{%s}%s" % (NS, t)
|
|
534
|
+
|
|
535
|
+
path = sys.argv[1]
|
|
536
|
+
field = os.environ["FIELD_API"]
|
|
537
|
+
tree = ET.parse(path)
|
|
538
|
+
root = tree.getroot()
|
|
539
|
+
|
|
540
|
+
# Find the AddOppFieldsToCollection assignment.
|
|
541
|
+
target = None
|
|
542
|
+
for a in root.findall(q("assignments")):
|
|
543
|
+
name = a.find(q("name"))
|
|
544
|
+
if name is not None and name.text == "AddOppFieldsToCollection":
|
|
545
|
+
target = a
|
|
546
|
+
break
|
|
547
|
+
if target is None:
|
|
548
|
+
sys.stderr.write("AddOppFieldsToCollection assignment not found\n")
|
|
549
|
+
sys.exit(3)
|
|
550
|
+
|
|
551
|
+
# Already present? (idempotent)
|
|
552
|
+
for item in target.findall(q("assignmentItems")):
|
|
553
|
+
sv = item.find(q("value") + "/" + q("stringValue"))
|
|
554
|
+
if sv is not None and sv.text == field:
|
|
555
|
+
print(" already present: %s" % field)
|
|
556
|
+
sys.exit(2)
|
|
557
|
+
|
|
558
|
+
# Build a new <assignmentItems> matching the existing shape.
|
|
559
|
+
item = ET.SubElement(target, q("assignmentItems"))
|
|
560
|
+
ET.SubElement(item, q("assignToReference")).text = "OpportunityFields"
|
|
561
|
+
ET.SubElement(item, q("operator")).text = "Add"
|
|
562
|
+
val = ET.SubElement(item, q("value"))
|
|
563
|
+
ET.SubElement(val, q("stringValue")).text = field
|
|
564
|
+
|
|
565
|
+
# assignmentItems must precede <connector>; move the new one before it if needed.
|
|
566
|
+
children = list(target)
|
|
567
|
+
conn = target.find(q("connector"))
|
|
568
|
+
if conn is not None:
|
|
569
|
+
target.remove(item)
|
|
570
|
+
idx = children.index(conn)
|
|
571
|
+
target.insert(idx, item)
|
|
572
|
+
|
|
573
|
+
tree.write(path, encoding="UTF-8", xml_declaration=True)
|
|
574
|
+
print(" inserted: %s" % field)
|
|
575
|
+
sys.exit(0)
|
|
576
|
+
PYEOF
|
|
577
|
+
if [[ "$RC" == "2" ]]; then
|
|
578
|
+
log_pass "Field already in flow collection — no change (idempotent)"
|
|
579
|
+
elif [[ "$RC" == "0" ]]; then
|
|
580
|
+
log_try "Deploy updated flow (status preserved)"
|
|
581
|
+
DEPLOYF=$(sf project deploy start --metadata "Flow:${FLOW_API_NAME}" --target-org "$ORG_ALIAS" --json 2>/dev/null || echo '{"status":1}')
|
|
582
|
+
if echo "$DEPLOYF" | jq -e '.status == 0' >/dev/null 2>&1; then
|
|
583
|
+
log_pass "Flow updated — '$FIELD_API' added to OpportunityFields"
|
|
584
|
+
else
|
|
585
|
+
ERR=$(echo "$DEPLOYF" | jq -r '.result.details.componentFailures[0].problem // .message // "unknown"' 2>/dev/null)
|
|
586
|
+
die "Flow deploy failed: $ERR (existing suggestions unaffected — flow not changed on failure)"
|
|
587
|
+
fi
|
|
588
|
+
else
|
|
589
|
+
die "Flow edit helper failed (rc=$RC)."
|
|
590
|
+
fi
|
|
591
|
+
fi
|
|
592
|
+
echo ""
|
|
593
|
+
fi
|
|
594
|
+
|
|
595
|
+
# ============================================================
|
|
596
|
+
# Step 5 — Optional synchronous verification via /generations
|
|
597
|
+
# ============================================================
|
|
598
|
+
if [[ "$VERIFY_WITH_NOTE" == "true" ]]; then
|
|
599
|
+
echo "Step 5: Verify with a seeded note (synchronous /generations)"
|
|
600
|
+
|
|
601
|
+
if [[ -z "$OPP_ID" ]]; then
|
|
602
|
+
# Match the pipeline's own eligibility filter (IsClosed=false AND CloseDate
|
|
603
|
+
# within +90 days) so we don't verify against an opp the scheduled flow would
|
|
604
|
+
# skip — that would surface a misleading "no generation" false negative.
|
|
605
|
+
OPP_ID=$(sf data query -q "SELECT Id FROM Opportunity WHERE IsClosed=false AND CloseDate>=TODAY AND CloseDate<=NEXT_N_DAYS:90 ORDER BY LastModifiedDate DESC LIMIT 1" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.records[0].Id // empty')
|
|
606
|
+
[[ -n "$OPP_ID" ]] || die "No eligible open Opportunity found (IsClosed=false, CloseDate within +90 days); pass --opp <OppId>."
|
|
607
|
+
fi
|
|
608
|
+
log_info "Opportunity: $OPP_ID"
|
|
609
|
+
|
|
610
|
+
# A caller (e.g. the agent-driven prompt tune-loop) can pass --note with a body
|
|
611
|
+
# tailored to THIS field's goal/instruction, so the seeded grounding signal
|
|
612
|
+
# actually exercises the customized prompt. Absent --note, fall back to a generic
|
|
613
|
+
# note that mentions a few common signals.
|
|
614
|
+
if [[ -n "$NOTE_TEXT" ]]; then
|
|
615
|
+
NOTE_BODY="$NOTE_TEXT"
|
|
616
|
+
else
|
|
617
|
+
NOTE_BODY="Verification note for ${FIELD_LABEL}: Customer flagged budget pressure, a competing vendor, and a timeline slip during the last call."
|
|
618
|
+
fi
|
|
619
|
+
# macOS/BSD `base64` line-wraps at 76 cols (embeds newlines); GNU's `-w0` isn't
|
|
620
|
+
# portable. `tr -d '\n'` strips wraps on both so the value stays single-line and
|
|
621
|
+
# doesn't break the --values string below.
|
|
622
|
+
NOTE_B64=$(printf '%s' "$NOTE_BODY" | base64 | tr -d '\n')
|
|
623
|
+
|
|
624
|
+
log_try "Create ContentNote + link to opportunity"
|
|
625
|
+
# Note-seeding is best-effort: `|| true` keeps a create failure from tripping
|
|
626
|
+
# `set -e` so it degrades to the log_warn fallback instead of aborting the run.
|
|
627
|
+
NOTE_ID=$(sf data create record --sobject ContentNote --values "Title='FieldSuggestion Verify ${DEV_NAME}' Content='${NOTE_B64}'" --target-org "$ORG_ALIAS" --json 2>/dev/null | jq -r '.result.id // empty' || true)
|
|
628
|
+
if [[ -n "$NOTE_ID" ]]; then
|
|
629
|
+
# ContentNote shares the 069 key prefix with ContentDocument — the note Id IS
|
|
630
|
+
# the ContentDocumentId, so link directly. (The ContentVersion→LatestVersionId
|
|
631
|
+
# subquery returns null on some orgs, silently dropping the link.)
|
|
632
|
+
if sf data create record --sobject ContentDocumentLink --values "ContentDocumentId='${NOTE_ID}' LinkedEntityId='${OPP_ID}' ShareType='V' Visibility='AllUsers'" --target-org "$ORG_ALIAS" --json 2>/dev/null >/dev/null; then
|
|
633
|
+
log_pass "Seeded note $NOTE_ID and linked to $OPP_ID"
|
|
634
|
+
else
|
|
635
|
+
log_warn "Note $NOTE_ID created but linking to $OPP_ID failed; generation will run against existing grounding data only."
|
|
636
|
+
fi
|
|
637
|
+
else
|
|
638
|
+
log_warn "Could not seed note; running generation against existing grounding data."
|
|
639
|
+
fi
|
|
640
|
+
|
|
641
|
+
log_try "POST /generations"
|
|
642
|
+
# In override mode, generate against the MANAGED template name — the override
|
|
643
|
+
# deployed as its new active version, so this exercises exactly the prompt the
|
|
644
|
+
# scheduled pipeline will run. The override's own dev name is not a generatable
|
|
645
|
+
# template (it has no standalone record).
|
|
646
|
+
GEN=$(sf api request rest "/services/data/${API_VERSION}/einstein/prompt-templates/${TEMPLATE_QUERY_NAME}/generations" \
|
|
647
|
+
--method POST --target-org "$ORG_ALIAS" \
|
|
648
|
+
--body "{\"isPreview\":false,\"inputParams\":{\"valueMap\":{\"Input:Opportunity\":{\"value\":{\"id\":\"${OPP_ID}\"}}}},\"additionalConfig\":{\"numGenerations\":1,\"temperature\":0,\"applicationName\":\"PromptTemplateGenerationsInvocable\"}}" 2>/dev/null || echo '{}')
|
|
649
|
+
GEN_TEXT=$(echo "$GEN" | jq -r '.generations[0].text // empty')
|
|
650
|
+
if [[ -n "$GEN_TEXT" ]]; then
|
|
651
|
+
log_pass "Generation succeeded:"
|
|
652
|
+
echo "$GEN_TEXT"
|
|
653
|
+
else
|
|
654
|
+
log_warn "No generation text returned. Response:"
|
|
655
|
+
echo "$GEN" | jq -r '.[0].message // .message // .' 2>/dev/null | head -5
|
|
656
|
+
fi
|
|
657
|
+
echo ""
|
|
658
|
+
fi
|
|
659
|
+
|
|
660
|
+
# ============================================================
|
|
661
|
+
# Summary
|
|
662
|
+
# ============================================================
|
|
663
|
+
echo "Done: $FIELD_API"
|
|
664
|
+
if [[ "$WIRE_ONLY" == "true" ]]; then
|
|
665
|
+
echo " Template : ${OOTB_MANAGED_TEMPLATE[$FIELD_API]} (curated OOTB prompt, unchanged)"
|
|
666
|
+
elif [[ -n "$OVERRIDE_SOURCE" ]]; then
|
|
667
|
+
echo " Template : override of $OVERRIDE_SOURCE (new active version; OOTB prompt customized)"
|
|
668
|
+
else
|
|
669
|
+
echo " Template : $DEV_NAME (active)"
|
|
670
|
+
fi
|
|
671
|
+
if [[ "$SKIP_FLOW" != "true" ]]; then
|
|
672
|
+
echo " Flow : $FIELD_API registered in $FLOW_API_NAME OpportunityFields (idempotent)"
|
|
673
|
+
fi
|
|
674
|
+
echo ""
|
|
675
|
+
echo "Scheduled suggestions land in the AiGenActionItem object (async, ~2-3 min after the daily run)."
|
|
676
|
+
echo "To confirm the scheduled path (NOT the synchronous test above):"
|
|
677
|
+
echo " 1. Baseline: sf data query -q \"SELECT COUNT() FROM AiGenActionItem WHERE CreatedDate=TODAY\" --target-org $ORG_ALIAS"
|
|
678
|
+
echo " 2. Wait >= 240s after the flow's scheduled run (WhatId is not SOQL-queryable — use the COUNT delta)."
|
|
679
|
+
echo " 3. Re-count and compare. New rows = suggestions generated."
|