@salesforce/afv-skills 1.67.0 → 1.68.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/README.md +55 -5
- package/package.json +1 -1
- package/skills/agentforce-offline-experimentation-run/SKILL.md +14 -22
- package/skills/agentforce-offline-experimentation-run/scripts/bt_compare.py +81 -0
- package/skills/agentforce-offline-experimentation-run/scripts/bt_fetch.py +358 -0
- package/skills/agentforce-offline-experimentation-run/scripts/bt_patch_agent.py +110 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/__init__.py +34 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/agentpatch.py +483 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/agentsrc.py +565 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/cache.py +95 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/compare.py +617 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/fetch.py +1012 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/judge.py +148 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/messages.py +92 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/patch.py +327 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/redact.py +40 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/results.py +288 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/scoring.py +340 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/sfap.py +238 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/splits.py +136 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/stdm.py +98 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/tokens.py +173 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/units.py +199 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/versions.py +194 -0
- package/skills/agentforce-offline-experimentation-run/scripts/build_report.py +366 -378
- package/skills/agentforce-offline-experimentation-run/scripts/resolve-token.sh +14 -4
- package/skills/agentforce-offline-experimentation-run/scripts/sfap_replay.py +472 -796
- package/skills/dx-lifecycle-center/SKILL.md +5 -2
- package/skills/dx-lifecycle-center/assets/widgets/connect-connected/connect-connected.json +65 -0
- package/skills/dx-lifecycle-center/assets/widgets/connect-connected/schema.json +32 -0
- package/skills/dx-lifecycle-center/assets/widgets/connect-sign-in/connect-sign-in.json +57 -0
- package/skills/dx-lifecycle-center/assets/widgets/connect-sign-in/schema.json +21 -0
- package/skills/dx-lifecycle-center/examples/workflows.md +20 -6
- package/skills/dx-lifecycle-center/references/connect-org.md +21 -2
- package/skills/dx-lifecycle-center/references/deploy.md +2 -2
- package/skills/dx-lifecycle-center/references/list-orgs.md +1 -1
- package/skills/dx-lifecycle-center/scripts/card-policy.jq +71 -0
- package/skills/dx-lifecycle-center/scripts/fill-widget.jq +29 -0
- package/skills/dx-lifecycle-center/scripts/widget.sh +28 -0
- package/skills/dx-org-devhub-configure/SKILL.md +85 -84
- package/skills/dx-org-devhub-configure/examples/status-output.md +25 -2
- package/skills/dx-org-devhub-configure/scripts/devhub.sh +265 -140
- package/skills/dx-org-devhub-configure/scripts/test-devhub.sh +100 -15
- package/skills/dx-packaging-move-to-2gp-migrate/SKILL.md +89 -0
- package/skills/dx-packaging-move-to-2gp-migrate/references/api-operations.md +164 -0
- package/skills/dx-scratch-def-configure/SKILL.md +163 -0
- package/skills/dx-scratch-def-configure/examples/confirm-card.md +53 -0
- package/skills/dx-scratch-def-configure/examples/report-recipes.md +100 -0
- package/skills/dx-scratch-def-configure/references/features.json +3270 -0
- package/skills/dx-scratch-def-configure/references/project-scratch-def.schema.json +1841 -0
- package/skills/dx-scratch-def-configure/references/settings-map.json +682 -0
- package/skills/dx-scratch-def-configure/scripts/lib_scratch_def.py +1306 -0
- package/skills/dx-scratch-def-configure/scripts/requirements.txt +3 -0
- package/skills/dx-scratch-def-configure/scripts/scrape_config_values.py +848 -0
- package/skills/education-cloud-domain-configure/SKILL.md +1 -1
- package/skills/education-cloud-multi-campus-configure/SKILL.md +1 -1
- package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +1 -1
- package/skills/experience-design-validate/SKILL.md +1 -1
- package/skills/experience-search-coordinate/SKILL.md +18 -12
- package/skills/experience-search-coordinate/references/content-route.md +79 -1
- package/skills/field-service-data-capture-migrate/SKILL.md +1 -1
- package/skills/platform-apex-generate/SKILL.md +14 -11
- package/skills/platform-apex-test-generate/SKILL.md +2 -1
- package/skills/platform-datamask-run/SKILL.md +52 -71
- package/skills/platform-datamask-run/references/api-surface.md +10 -4
- package/skills/platform-datamask-run/references/policy-authoring.md +56 -7
- package/skills/platform-datamask-run/references/run-and-abort.md +17 -10
- package/skills/platform-datamask-run/scripts/poll-job.sh +15 -25
- package/skills/platform-dataspace-access-configure/SKILL.md +19 -1
- package/skills/platform-metadata-discovery-get/SKILL.md +185 -0
- package/skills/platform-metadata-discovery-get/references/api-map.md +25 -0
- package/skills/platform-metadata-discovery-get/references/h360-routes.md +30 -0
- package/skills/platform-metadata-discovery-get/references/scoped-soql.md +52 -0
- package/skills/platform-metadata-grounding-get/SKILL.md +170 -0
- package/skills/platform-policy-rule-generate/SKILL.md +84 -232
- package/skills/platform-policy-rule-generate/references/condition-model.md +47 -0
- package/skills/platform-policy-rule-generate/references/condition-templates.md +89 -0
- package/skills/platform-policy-rule-generate/references/conjunction-compiler.md +25 -0
- package/skills/platform-policy-rule-generate/references/conjunction-expression.md +22 -0
- package/skills/platform-policy-rule-generate/references/consumer-all.md +9 -0
- package/skills/platform-policy-rule-generate/references/consumer-datacloud-ui.md +91 -0
- package/skills/platform-policy-rule-generate/references/deploy-errors.md +24 -36
- package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/package.xml +12 -0
- package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/policyRuleDefinitionSets/ExistsJoinRLS.policyRuleDefinitionSet +6 -0
- package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/policyRuleDefinitions/ExistsJoinRLS_Rule_1.policyRuleDefinition +91 -0
- package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/package.xml +12 -0
- package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/policyRuleDefinitionSets/FieldReadDeny.policyRuleDefinitionSet +6 -0
- package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/policyRuleDefinitions/FieldReadDeny_Rule_1.policyRuleDefinition +72 -0
- package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/package.xml +12 -0
- package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/policyRuleDefinitionSets/LastCharactersFieldMasking.policyRuleDefinitionSet +6 -0
- package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/policyRuleDefinitions/LastCharactersFieldMasking_Rule_1.policyRuleDefinition +77 -0
- package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/package.xml +12 -0
- package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/policyRuleDefinitionSets/PrincipalFieldMatchRLS.policyRuleDefinitionSet +6 -0
- package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/policyRuleDefinitions/PrincipalFieldMatchRLS_Rule_1.policyRuleDefinition +77 -0
- package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/package.xml +12 -0
- package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/policyRuleDefinitionSets/TaggedObjectAccess.policyRuleDefinitionSet +4 -0
- package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/policyRuleDefinitions/TaggedObjectAccess_Rule_1.policyRuleDefinition +65 -0
- package/skills/platform-policy-rule-generate/references/headless-transport.md +87 -0
- package/skills/platform-policy-rule-generate/references/policy-schema-full.md +17 -10
- package/skills/platform-policy-rule-generate/references/policy-shapes.md +74 -0
- package/skills/platform-policy-rule-generate/references/principal-expression.md +31 -0
- package/skills/platform-policy-rule-generate/references/reference-resolution.md +51 -0
- package/skills/platform-policy-rule-generate/references/resource-expression.md +48 -0
- package/skills/platform-policy-rule-generate/references/runtime-context.md +9 -0
- package/skills/platform-policy-rule-generate/references/templates-access.md +11 -9
- package/skills/platform-policy-rule-generate/references/templates-advanced.md +4 -2
- package/skills/platform-policy-rule-generate/references/templates-record.md +37 -22
- package/skills/platform-policy-rule-generate/references/templates-transform.md +10 -8
- package/skills/platform-policy-rule-generate/references/templates.md +2 -2
- package/skills/platform-policy-rule-generate/references/ui-compatibility.md +35 -17
- package/skills/platform-report-generate/SKILL.md +9 -4
- package/skills/platform-report-generate/references/chart-types.md +26 -2
- package/skills/platform-report-generate/references/column-names.md +43 -0
- package/skills/platform-report-generate/references/errors-and-troubleshooting.md +1 -1
- package/skills/platform-report-generate/references/verification-checklist.md +10 -2
- package/skills/service-digital-engagement-channel-configure/SKILL.md +7 -5
- package/skills/service-digital-engagement-channel-configure/assets/messaging_channel_template.xml +33 -0
- package/skills/service-digital-engagement-channel-configure/references/channel_settings.md +110 -14
package/README.md
CHANGED
|
@@ -1,15 +1,52 @@
|
|
|
1
|
-
# Salesforce Skills Library
|
|
1
|
+
# Salesforce Skills and Plugins Library
|
|
2
2
|
|
|
3
|
-
This repository provides
|
|
3
|
+
This repository provides Salesforce agent skills, plugins, and sample apps for building applications. It includes skills for Agentforce agents, Lightning apps, Flow, Apex, SOQL, Lightning Web Components (LWC), UI bundles, objects and fields, permission sets, and related areas. The [builder plugins](plugins/builder/) package skills with additional capabilities such as agents, hooks, commands, and Model Context Protocol (MCP) servers. Each client loads the components supported by that plugin's manifest.
|
|
4
4
|
|
|
5
|
-
The skills are contributed by Salesforce and the broader community.
|
|
5
|
+
The skills are contributed by Salesforce and the broader community. The skills library is optimized for Agentforce Vibes and can be used with any AI tool that supports skills. For guided Salesforce development, start with the [Salesforce Development plugin README](plugins/builder/salesforce-development/README.md) and the [Salesforce Development plugin documentation](https://developer.salesforce.com/docs/platform/salesforce-skills-plugins/guide/salesforce-development-overview.html).
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## See the development workflow
|
|
8
|
+
|
|
9
|
+
Go from a natural-language request to an app deployed in Salesforce. These short demos
|
|
10
|
+
were recorded with Salesforce Development 2.5.0 and show a terminal workflow followed by the finished app.
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
| Demo | Length | Watch or download |
|
|
15
|
+
|---|---:|---|
|
|
16
|
+
| Claude Code: Lightning app | 35 seconds | [GIF](docs/media/development-demos/claude-salesforce-development-2.5.0.gif) · [MP4](docs/media/development-demos/claude-salesforce-development-2.5.0.mp4) |
|
|
17
|
+
| Codex CLI: Lightning app | 30 seconds | [GIF](docs/media/development-demos/codex-salesforce-development-2.5.0.gif) · [MP4](docs/media/development-demos/codex-salesforce-development-2.5.0.mp4) |
|
|
18
|
+
| Claude Code: internal React app | 32 seconds | [GIF](docs/media/development-demos/claude-react-salesforce-development-2.5.0.gif) · [MP4](docs/media/development-demos/claude-react-salesforce-development-2.5.0.mp4) |
|
|
19
|
+
|
|
20
|
+
The Codex Lightning demo also uses Experience LWC 0.3.0; the React demo uses
|
|
21
|
+
Experience React 0.2.1. The clips combine real terminal excerpts with captured views
|
|
22
|
+
of the deployed apps. Accelerated terminal sections are labeled. All Account data
|
|
23
|
+
shown is fictional. See the [recording notes](docs/media/development-demos/README.md).
|
|
24
|
+
|
|
25
|
+
<details>
|
|
26
|
+
<summary>Preview Codex CLI and React</summary>
|
|
27
|
+
|
|
28
|
+
### Codex CLI: Lightning app
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
### Claude Code: internal React app
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
</details>
|
|
37
|
+
|
|
38
|
+
> ⚠️ **Expect frequent changes.** The Salesforce skills and plugins library is evolving rapidly as we refine patterns and incorporate feedback. Skills and plugins may be renamed, restructured, or removed between releases — they do not follow the same stability guarantees as GA platform APIs. If you’ve forked or synced the repository, be prepared for upstream changes that may conflict with local modifications. This repository is always the source of truth.
|
|
8
39
|
|
|
9
40
|
## 🗂️ Structure
|
|
10
41
|
|
|
11
42
|
```
|
|
12
43
|
sf-skills/
|
|
44
|
+
├── .claude-plugin/
|
|
45
|
+
│ └── marketplace.json # Salesforce plugin marketplace catalog
|
|
46
|
+
├── plugins/
|
|
47
|
+
│ └── builder/ # Plugins for Salesforce development
|
|
48
|
+
│ ├── salesforce-development/
|
|
49
|
+
│ └── ...
|
|
13
50
|
├── skills/ # Directory-based executable workflows
|
|
14
51
|
│ ├── platform-apex-generate/
|
|
15
52
|
│ ├── platform-custom-object-generate/
|
|
@@ -28,7 +65,20 @@ sf-skills/
|
|
|
28
65
|
| **Tool** | **Usage** |
|
|
29
66
|
|----------|-------------|
|
|
30
67
|
| **Agentforce Vibes** | Skills are auto-installed and auto-updated |
|
|
31
|
-
| **
|
|
68
|
+
| **Claude Code — Salesforce Development plugin** | Follow the [Claude Code installation guide](https://developer.salesforce.com/docs/platform/salesforce-skills-plugins/guide/install-salesforce-development.html) for skills, guided workflows, and integrated tools |
|
|
69
|
+
| **Codex — Salesforce Development plugin** | Follow [Codex setup and requirements](plugins/builder/salesforce-development/README.md#codex-lifecycle-support); use a release containing the Codex manifests and Codex CLI 0.145.0 or later |
|
|
70
|
+
| **OpenCode, Claude Code, Codex, Cursor, [more](https://agentskills.io/) — standalone skills** | `npx skills add forcedotcom/sf-skills` |
|
|
71
|
+
|
|
72
|
+
## 🔌 Plugins
|
|
73
|
+
|
|
74
|
+
Plugins bundle skills with other components that the coding client installs and loads together. The Salesforce Development plugin provides shared workflows for building, testing, and deploying on the Salesforce Platform, with client-specific commands, hooks, and MCP configuration. Claude Code agents and hooks aren't automatically available in other clients. Specialized builder plugins add capabilities for areas such as Agentforce, code quality, LWC, and React development.
|
|
75
|
+
|
|
76
|
+
- [Salesforce marketplace manifest](.claude-plugin/marketplace.json) — available plugins, descriptions, and source paths
|
|
77
|
+
- [Salesforce Development plugin README](plugins/builder/salesforce-development/README.md) — prerequisites, quick start, and component inventory
|
|
78
|
+
- [Salesforce Development plugin documentation](https://developer.salesforce.com/docs/platform/salesforce-skills-plugins/guide/salesforce-development-overview.html) — installation, project and org setup, workflows, and troubleshooting; use the plugin README for Codex-specific setup
|
|
79
|
+
- [Claude Code plugin documentation](https://code.claude.com/docs/en/plugins) — plugin components and how plugins work
|
|
80
|
+
|
|
81
|
+
Check the [plugin changelog](plugins/builder/salesforce-development/CHANGELOG.md) and installed version for release availability. Other clients, including Cursor, can use standalone skills where supported; full plugin support depends on the client and plugin manifest.
|
|
32
82
|
|
|
33
83
|
## 📦 Samples
|
|
34
84
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/afv-skills",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.68.0",
|
|
4
4
|
"description": "Salesforce's curated collection of agent skills for building applications. Optimized for Agentforce Vibes, compatible with all AI tools.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"files": [
|
|
@@ -24,14 +24,13 @@ metadata:
|
|
|
24
24
|
|
|
25
25
|
## When to Use This Skill
|
|
26
26
|
|
|
27
|
-
Execute this workflow when a user wants to test how a different LLM model would handle existing Agentforce agent conversations. Fetches session traces from Data Cloud STDM, replays prompts via the SFAP AI Gateway REST API with structured tool calling, scores across four metrics (topic agreement, function name accuracy, function argument accuracy via LLM-as-judge, text response quality via LLM-as-judge), and produces
|
|
27
|
+
Execute this workflow when a user wants to test how a different LLM model would handle existing Agentforce agent conversations. Fetches session traces from Data Cloud STDM, replays prompts via the SFAP AI Gateway REST API with structured tool calling, scores across four metrics (topic agreement, function name accuracy, function argument accuracy via LLM-as-judge, text response quality via LLM-as-judge), and produces a run directory with units.jsonl, results.jsonl, summary.json, and an HTML report. Requires Data Cloud with Agentforce Activity data streaming enabled, an authenticated org, and a target model ID (llmgateway__ prefix for SFAP replay). Estimated runtime: 10-30 minutes for 10 sessions.
|
|
28
28
|
|
|
29
29
|
## Workflow
|
|
30
30
|
|
|
31
|
-
discover-data-space → verify-stdm-availability → discover-agents → list-available-models (if user needs help choosing) → find-sessions → get-conversation-details → get-llm-step-details → obtain-sfap-token → replay-turn-prompts
|
|
31
|
+
discover-data-space → verify-stdm-availability → discover-agents → list-available-models (if user needs help choosing) → find-sessions → get-conversation-details → get-llm-step-details → obtain-sfap-token → replay-turn-prompts (scoring happens inline) → generate-run-results → generate-html-report.
|
|
32
32
|
WORKFLOW GATE: Do not proceed past Phase 0 until org alias (auto-discover from authenticated orgs), agent API name (auto-discover from STDM), and target model ID (ask user) are all confirmed.
|
|
33
33
|
SFAP JWT expires after ~2 hours. For 20+ session replays, refresh the token between batches. Each SFAP replay call takes ~1-3 seconds; keep under 20 sessions for reasonable wall-clock time (~10-30 minutes).
|
|
34
|
-
The scoring and judging steps (Phase 3) can run in parallel once replay completes. Exact-match scoring is instant; LLM judge calls add ~1-2 seconds per turn.
|
|
35
34
|
|
|
36
35
|
## Critical Constraints
|
|
37
36
|
|
|
@@ -61,8 +60,8 @@ Operations grouped by purpose. Use these as the building blocks for the workflow
|
|
|
61
60
|
| `score-exact-matches` | read | proposed | — | `replay-turn-prompts` |
|
|
62
61
|
| `judge-function-args` | read | proposed | — | `score-exact-matches`, `obtain-sfap-token` |
|
|
63
62
|
| `judge-text-responses` | read | proposed | — | `replay-turn-prompts`, `obtain-sfap-token` |
|
|
64
|
-
| `generate-
|
|
65
|
-
| `generate-html-report` | write | proposed | — | `generate-
|
|
63
|
+
| `generate-run-results` | write | proposed | — | `score-exact-matches`, `judge-function-args`, `judge-text-responses` |
|
|
64
|
+
| `generate-html-report` | write | proposed | — | `generate-run-results` |
|
|
66
65
|
|
|
67
66
|
### Dependency graph
|
|
68
67
|
|
|
@@ -80,7 +79,7 @@ graph TD
|
|
|
80
79
|
score_exact_matches["score-exact-matches (read)"]
|
|
81
80
|
judge_function_args["judge-function-args (read)"]
|
|
82
81
|
judge_text_responses["judge-text-responses (read)"]
|
|
83
|
-
|
|
82
|
+
generate_run_results["generate-run-results (write)"]
|
|
84
83
|
generate_html_report["generate-html-report (write)"]
|
|
85
84
|
discover_data_space --> verify_stdm_availability
|
|
86
85
|
verify_stdm_availability --> discover_agents
|
|
@@ -95,10 +94,10 @@ graph TD
|
|
|
95
94
|
obtain_sfap_token --> judge_function_args
|
|
96
95
|
replay_turn_prompts --> judge_text_responses
|
|
97
96
|
obtain_sfap_token --> judge_text_responses
|
|
98
|
-
score_exact_matches -->
|
|
99
|
-
judge_function_args -->
|
|
100
|
-
judge_text_responses -->
|
|
101
|
-
|
|
97
|
+
score_exact_matches --> generate_run_results
|
|
98
|
+
judge_function_args --> generate_run_results
|
|
99
|
+
judge_text_responses --> generate_run_results
|
|
100
|
+
generate_run_results --> generate_html_report
|
|
102
101
|
```
|
|
103
102
|
|
|
104
103
|
### Read operations
|
|
@@ -194,29 +193,22 @@ LLM-as-judge scoring of text responses for turns that end with text output
|
|
|
194
193
|
|
|
195
194
|
### Write operations
|
|
196
195
|
|
|
197
|
-
#### `generate-
|
|
196
|
+
#### `generate-run-results`
|
|
198
197
|
|
|
199
|
-
|
|
198
|
+
sfap_replay.py produces the run directory with units.jsonl (one line per replay unit with request and golden), results.jsonl (one line per scored result), and summary.json (run config, model, counts, aggregate scores). Scoring happens inline during replay — no separate scoring step needed.
|
|
200
199
|
|
|
201
|
-
**Status:** `proposed`
|
|
202
|
-
|
|
203
|
-
**Inputs:**
|
|
204
200
|
|
|
205
|
-
|
|
201
|
+
**Status:** `proposed`
|
|
206
202
|
|
|
207
203
|
**Depends on:** `score-exact-matches`, `judge-function-args`, `judge-text-responses`
|
|
208
204
|
|
|
209
205
|
#### `generate-html-report`
|
|
210
206
|
|
|
211
|
-
Build a self-contained HTML analysis report with
|
|
207
|
+
Build a self-contained HTML analysis report with scorecards and failure breakdown
|
|
212
208
|
|
|
213
209
|
**Status:** `proposed`
|
|
214
210
|
|
|
215
|
-
**
|
|
216
|
-
|
|
217
|
-
- `html_output` *(`String`)*
|
|
218
|
-
|
|
219
|
-
**Depends on:** `generate-csv-report`
|
|
211
|
+
**Depends on:** `generate-run-results`
|
|
220
212
|
|
|
221
213
|
### Other operations
|
|
222
214
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Compare two replay runs of the same units, unit by unit, against the noise between repeats.
|
|
3
|
+
|
|
4
|
+
python3 scripts/bt_compare.py runs/base runs/cand [--noise-from runs/v0] [--out FILE]
|
|
5
|
+
|
|
6
|
+
Each unit x repeat scored in both runs is a pair. Per group (overall, step kind, node, golden tool)
|
|
7
|
+
the delta is in units: the sum of each unit's cand - base score, averaged over its repeats. Per
|
|
8
|
+
failure bucket, the net is the pairs that scored lower and ended in it, minus those that scored
|
|
9
|
+
higher and left it. A metric's tolerance is the largest difference on it between two repeats of
|
|
10
|
+
the noise run (default: BASE), at least 1 unit; a delta beyond it is a regression or a gain. A
|
|
11
|
+
bootstrap over sessions gives a 95% interval of the mean unit delta, for reading only. A failure
|
|
12
|
+
drops the pair from both runs, except a no answer: a timeout where the other run answered another
|
|
13
|
+
request or model scores 0, in the bucket no_answer.
|
|
14
|
+
|
|
15
|
+
Exit 0: no metric got worse beyond its tolerance. Exit 1: some did. Exit 2: the runs are not
|
|
16
|
+
comparable (other units, judge, k, seed, steps, router nodes, equivalent tools, timeout or btlib
|
|
17
|
+
code; or more than 10% infra errors, or more than 10% no answers, in a run). --force compares them
|
|
18
|
+
anyway, on the units both share. Exit 2 also when a dir is not a finished run, or bt_compare
|
|
19
|
+
fails: never a verdict.
|
|
20
|
+
|
|
21
|
+
Writes the comparison to --out (default CAND/compare.json, bt.compare/1). It lists the units that
|
|
22
|
+
moved by unit id, so keep it with the run dirs, out of git.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
import traceback
|
|
29
|
+
|
|
30
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
31
|
+
|
|
32
|
+
from btlib import compare as comparelib # noqa: E402
|
|
33
|
+
from btlib import results # noqa: E402
|
|
34
|
+
from btlib.redact import redact # noqa: E402
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_args(argv=None):
|
|
38
|
+
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
39
|
+
p.add_argument("base", help="run dir of the baseline (sfap_replay.py --out-dir)")
|
|
40
|
+
p.add_argument("cand", help="run dir of the candidate, on the same units")
|
|
41
|
+
p.add_argument("--noise-from", metavar="DIR",
|
|
42
|
+
help="run dir whose repeats set the tolerances (default: BASE); same units, judge, steps, "
|
|
43
|
+
"router nodes, equivalent tools and btlib code as BASE")
|
|
44
|
+
p.add_argument("--out", metavar="FILE", help="where to write the comparison (default CAND/compare.json)")
|
|
45
|
+
p.add_argument("--force", action="store_true",
|
|
46
|
+
help="compare runs that are not comparable, on the units both share (recorded in the output)")
|
|
47
|
+
p.add_argument("--bootstrap-samples", type=int, default=comparelib.BOOTSTRAP_SAMPLES, metavar="N",
|
|
48
|
+
help="session resamples for the 95%% intervals (default %(default)s; 0: none)")
|
|
49
|
+
args = p.parse_args(argv)
|
|
50
|
+
if args.bootstrap_samples < 0:
|
|
51
|
+
p.error("--bootstrap-samples must be >= 0")
|
|
52
|
+
return args
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main(argv=None):
|
|
56
|
+
args = parse_args(argv)
|
|
57
|
+
out = args.out or os.path.join(args.cand, "compare.json")
|
|
58
|
+
try:
|
|
59
|
+
res = comparelib.compare(args.base, args.cand, args.noise_from, force=args.force,
|
|
60
|
+
samples=args.bootstrap_samples)
|
|
61
|
+
results.write_json(out, res) # 0600: unit ids name sessions
|
|
62
|
+
lines = comparelib.report_lines(res)
|
|
63
|
+
except comparelib.CompareError as e:
|
|
64
|
+
print(f"error: {redact(str(e))}", file=sys.stderr)
|
|
65
|
+
return 2
|
|
66
|
+
except OSError as e:
|
|
67
|
+
print(f"error: cannot write {out}: {e.strerror or e}", file=sys.stderr)
|
|
68
|
+
return 2
|
|
69
|
+
except Exception as e: # a bug, or a run dir it did not foresee: exit 1 would read as a regression
|
|
70
|
+
frame = traceback.extract_tb(e.__traceback__)[-1]
|
|
71
|
+
print(f"error: bt_compare failed ({type(e).__name__} at {os.path.basename(frame.filename)}:{frame.lineno}); "
|
|
72
|
+
"no verdict (exit 2)", file=sys.stderr)
|
|
73
|
+
return 2
|
|
74
|
+
for line in lines:
|
|
75
|
+
print(redact(line))
|
|
76
|
+
print(f"comparison: {out}")
|
|
77
|
+
return comparelib.exit_code(res)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
sys.exit(main())
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fetch an agent's STDM sessions with read-only anonymous Apex: nothing is deployed.
|
|
3
|
+
|
|
4
|
+
python3 scripts/bt_fetch.py --org <alias> --probe [--agent NAME --start DATE --end DATE]
|
|
5
|
+
python3 scripts/bt_fetch.py --org <alias> --agent GPT41_Shopper_Agent \\
|
|
6
|
+
--start 2026-09-01 --end 2026-10-08 --out-dir runs/fetch-shop [--exclude-sessions FILE]
|
|
7
|
+
python3 scripts/sfap_replay.py --details runs/fetch-shop/details.json ...
|
|
8
|
+
|
|
9
|
+
Every query runs with `sf apex run --json` in --org, the org that holds the STDM data (with Data
|
|
10
|
+
Cloud One, not the agent's own org), in --dataspace. Each generated script is checked to be
|
|
11
|
+
read-only before it runs (SELECT queries only: no DML, no callouts) and is kept in OUT/apex/.
|
|
12
|
+
|
|
13
|
+
A data space that names the STDM DMOs and fields its own way (not ssot__AiAgentSession__dlm,
|
|
14
|
+
ssot__Id__c, ...) takes --stdm-names FILE, a bt.stdm_names/1 names map (references/replay-
|
|
15
|
+
procedure.md 1.2): the queries are sent in its names. Without it, nothing is renamed.
|
|
16
|
+
|
|
17
|
+
--probe counts the rows of each STDM DMO (and the agent's participant rows, and the sessions in
|
|
18
|
+
the window, when given) and prints each error as the org words it: run it first, and quote it
|
|
19
|
+
in an access request. With --stdm-names it also runs every query a fetch runs, with keys no row
|
|
20
|
+
has, and names each that fails.
|
|
21
|
+
|
|
22
|
+
The fetch finds the agent's sessions started in [--start, --end] with at least one turn, keeps
|
|
23
|
+
the --channels wanted (by default all but Builder*, Agent Builder's test traffic), drops the
|
|
24
|
+
sessions of --exclude-sessions and, with --split, those of the other split, then takes up to
|
|
25
|
+
--max-sessions in salted-hash order (or --order recent) and fetches them, --batch-size sessions
|
|
26
|
+
per Apex run.
|
|
27
|
+
An interrupted fetch rerun into the same --out-dir does not fetch its finished batches again.
|
|
28
|
+
|
|
29
|
+
Outputs in --out-dir (0700, files 0600): details.json (ConversationData, the --details input of
|
|
30
|
+
sfap_replay.py), sessions.json (SessionSummary), fetch.json (bt.fetch/1: parameters, counts,
|
|
31
|
+
digests; no session ids), apex/ (the scripts run). details.json holds user utterances and the
|
|
32
|
+
scripts hold session ids: keep them out of git.
|
|
33
|
+
|
|
34
|
+
--emit-kit DIR writes the same fetch as a kit for someone who can read the STDM data: this script,
|
|
35
|
+
the modules it runs, run_kit.sh with the parameters set (and the --stdm-names file), and a README.
|
|
36
|
+
Its out/ comes back.
|
|
37
|
+
|
|
38
|
+
Exit 0: done. 1: the fetch failed, or --probe found a required DMO unreadable (or, with
|
|
39
|
+
--stdm-names, a query that fails). 2: usage.
|
|
40
|
+
130: interrupted (rerun into the same --out-dir to go on).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import argparse
|
|
44
|
+
import hashlib
|
|
45
|
+
import json
|
|
46
|
+
import os
|
|
47
|
+
import shlex
|
|
48
|
+
import shutil
|
|
49
|
+
import sys
|
|
50
|
+
import tempfile
|
|
51
|
+
import traceback
|
|
52
|
+
from datetime import datetime, timezone
|
|
53
|
+
|
|
54
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
55
|
+
|
|
56
|
+
from btlib import SCHEMA_KIT # noqa: E402
|
|
57
|
+
from btlib import fetch as fetchlib # noqa: E402
|
|
58
|
+
from btlib import splits as splitlib # noqa: E402
|
|
59
|
+
|
|
60
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
61
|
+
KIT_MODULES = ("__init__.py", "fetch.py", "redact.py", "splits.py", "stdm.py") # all bt_fetch.py imports
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse_args(argv=None):
|
|
65
|
+
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
66
|
+
mode = p.add_mutually_exclusive_group()
|
|
67
|
+
mode.add_argument("--probe", action="store_true", help="count the rows each STDM DMO gives, print the errors")
|
|
68
|
+
mode.add_argument("--emit-kit", metavar="DIR", help="write this fetch as a kit for someone with STDM access")
|
|
69
|
+
p.add_argument("--org", help="alias or username of the org holding the STDM data")
|
|
70
|
+
p.add_argument("--dataspace", default="default", help="Data Cloud data space (default %(default)s)")
|
|
71
|
+
p.add_argument("--stdm-names", metavar="FILE",
|
|
72
|
+
help="the data space's own DMO and field names, a bt.stdm_names/1 file (default: the "
|
|
73
|
+
"ssot__ names, as they are)")
|
|
74
|
+
p.add_argument("--agent", help="the agent's API name (participant ssot__AiAgentApiName__c)")
|
|
75
|
+
p.add_argument("--start", help="sessions started from this date or ISO 8601 time (UTC)")
|
|
76
|
+
p.add_argument("--end", help="... up to this one; a date alone includes its whole day")
|
|
77
|
+
p.add_argument("--out-dir", metavar="DIR", help="where the fetch goes (created 0700; must not hold a fetch)")
|
|
78
|
+
p.add_argument("--max-sessions", type=int, default=fetchlib.MAX_SESSIONS,
|
|
79
|
+
help="sessions to fetch at most (default %(default)s)")
|
|
80
|
+
p.add_argument("--channels", help="comma-separated channel names to keep (default: all but Builder*)")
|
|
81
|
+
p.add_argument("--exclude-sessions", metavar="FILE",
|
|
82
|
+
help="never fetch these sessions: a file that names them, e.g. one ID per line, a "
|
|
83
|
+
"units.jsonl, a sessions or details JSON, a replay CSV (references/splits.md)")
|
|
84
|
+
p.add_argument("--val-fraction", type=float, metavar="F", help="session split as sfap_replay.py labels it")
|
|
85
|
+
p.add_argument("--split", choices=splitlib.SPLITS, help="fetch only this split (needs --val-fraction)")
|
|
86
|
+
p.add_argument("--split-salt", default=splitlib.DEFAULT_SALT, help="salt of the split (default %(default)s)")
|
|
87
|
+
p.add_argument("--order", choices=("hash", "recent"), default="hash",
|
|
88
|
+
help="which sessions come first when there are more than --max-sessions: a salted hash "
|
|
89
|
+
"(an even sample, stable when the window grows) or the newest (default %(default)s)")
|
|
90
|
+
p.add_argument("--sample-salt", default=fetchlib.SAMPLE_SALT, help="salt of --order hash (default %(default)s)")
|
|
91
|
+
p.add_argument("--batch-size", type=int, default=fetchlib.BATCH_SIZE,
|
|
92
|
+
help="sessions per Apex run, halved when a run hits a limit (default %(default)s)")
|
|
93
|
+
p.add_argument("--timeout", type=int, default=fetchlib.APEX_TIMEOUT, help="seconds per Apex run")
|
|
94
|
+
p.add_argument("--dry-run", action="store_true", help="find and select the sessions, print the counts, write nothing")
|
|
95
|
+
p.add_argument("--overwrite", action="store_true", help="replace an earlier fetch in --out-dir")
|
|
96
|
+
args = p.parse_args(argv)
|
|
97
|
+
if not args.emit_kit and not args.org:
|
|
98
|
+
p.error("--org is required (the org whose STDM data to read)")
|
|
99
|
+
if not args.probe:
|
|
100
|
+
missing = [f"--{n}" for n in ("agent", "start", "end") if not getattr(args, n)]
|
|
101
|
+
if missing:
|
|
102
|
+
p.error(" ".join(missing) + " required")
|
|
103
|
+
if not (args.emit_kit or args.dry_run or args.out_dir):
|
|
104
|
+
p.error("--out-dir is required (or --dry-run)")
|
|
105
|
+
if args.val_fraction is not None and not 0 < args.val_fraction < 1:
|
|
106
|
+
p.error("--val-fraction must be in (0, 1)")
|
|
107
|
+
if args.split and args.val_fraction is None:
|
|
108
|
+
p.error("--split needs --val-fraction (and the --split-salt of the units)")
|
|
109
|
+
if args.max_sessions < 1 or args.batch_size < 1 or args.timeout < 1:
|
|
110
|
+
p.error("--max-sessions, --batch-size and --timeout must be >= 1")
|
|
111
|
+
args.channels = [c.strip() for c in args.channels.split(",") if c.strip()] if args.channels else None
|
|
112
|
+
return args
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def read_excluded(path):
|
|
116
|
+
if not path:
|
|
117
|
+
return frozenset()
|
|
118
|
+
try:
|
|
119
|
+
return frozenset(splitlib.read_session_ids(path))
|
|
120
|
+
except (OSError, splitlib.SplitError) as e:
|
|
121
|
+
raise fetchlib.FetchError(f"--exclude-sessions {path}: {e}") from None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def read_names(path):
|
|
125
|
+
"""The StdmNames of --stdm-names, or None: the default names."""
|
|
126
|
+
return fetchlib.load_names(path) if path else None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def run_probe(args):
|
|
130
|
+
names = read_names(args.stdm_names)
|
|
131
|
+
with tempfile.TemporaryDirectory(prefix="bt_fetch_") as tmp:
|
|
132
|
+
f = fetchlib.Fetcher(args.org, os.path.join(tmp, "apex"), args.dataspace, timeout=args.timeout, names=names)
|
|
133
|
+
got = fetchlib.probe(f, args.agent, args.start, args.end)
|
|
134
|
+
print(f"STDM probe of {args.org}, data space {args.dataspace} (row counts, read-only):")
|
|
135
|
+
labels = {"agent": f"participant rows of {args.agent}", "window": "sessions started in the window"}
|
|
136
|
+
if names is not None:
|
|
137
|
+
print(f"names: {names.source} (sha256 {names.sha256})" + (f": {names.about}" if names.about else ""))
|
|
138
|
+
for key, table in fetchlib.PROBE_DMOS.items(): # table: the default name, quoted
|
|
139
|
+
labels[key] = f"{key} ({names.dmo(table[1:-1])})"
|
|
140
|
+
labels.update((key, "query: " + what) for key, what in fetchlib.SHAPES.items())
|
|
141
|
+
width = max(len(labels.get(key, key)) for key in got)
|
|
142
|
+
for key, res in got.items():
|
|
143
|
+
print(f" {labels.get(key, key):<{width}} " + (f"{res['rows']}" if "rows" in res else f"ERROR {res['error']}"))
|
|
144
|
+
unreadable = [k for k in fetchlib.REQUIRED_DMOS if "error" in got[k]]
|
|
145
|
+
if unreadable:
|
|
146
|
+
print(f"not readable: {', '.join(unreadable)}. A fetch needs them; quote the errors above in an access "
|
|
147
|
+
"request, or hand someone with access a kit (--emit-kit).")
|
|
148
|
+
return 1
|
|
149
|
+
if names is None:
|
|
150
|
+
print("the DMOs a fetch reads are readable")
|
|
151
|
+
return 0
|
|
152
|
+
failed = [what for key, what in fetchlib.SHAPES.items() if "error" in got[key]]
|
|
153
|
+
if failed:
|
|
154
|
+
print(f"these queries fail with the names of {names.source}: {', '.join(failed)}. The errors above "
|
|
155
|
+
"name what the data space lacks; fix the names map, and probe again.")
|
|
156
|
+
return 1
|
|
157
|
+
print("the DMOs a fetch reads are readable, and every query a fetch runs works with these names")
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def prepare_out_dir(out_dir, overwrite):
|
|
162
|
+
if os.path.isfile(os.path.join(out_dir, "fetch.json")) and not overwrite:
|
|
163
|
+
raise fetchlib.FetchError(f"{out_dir} already holds a fetch (fetch.json); use a new --out-dir or --overwrite")
|
|
164
|
+
if not os.path.isdir(out_dir):
|
|
165
|
+
os.makedirs(out_dir)
|
|
166
|
+
os.chmod(out_dir, 0o700) # user utterances inside
|
|
167
|
+
elif overwrite: # the old fetch's files go first: one that stops early must not pass for it
|
|
168
|
+
fetchlib.clear_outputs(out_dir)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def run_fetch(args):
|
|
172
|
+
exclude = read_excluded(args.exclude_sessions)
|
|
173
|
+
names = read_names(args.stdm_names)
|
|
174
|
+
kw = dict(channels=args.channels, exclude=exclude, split=args.split, val_fraction=args.val_fraction,
|
|
175
|
+
split_salt=args.split_salt, order=args.order, sample_salt=args.sample_salt,
|
|
176
|
+
max_sessions=args.max_sessions, batch_size=args.batch_size, log=print,
|
|
177
|
+
params={"exclude_sessions": os.path.basename(args.exclude_sessions) if args.exclude_sessions else None,
|
|
178
|
+
"batch_size": args.batch_size})
|
|
179
|
+
if args.dry_run:
|
|
180
|
+
with tempfile.TemporaryDirectory(prefix="bt_fetch_") as tmp:
|
|
181
|
+
f = fetchlib.Fetcher(args.org, os.path.join(tmp, "apex"), args.dataspace, timeout=args.timeout,
|
|
182
|
+
names=names)
|
|
183
|
+
fetchlib.fetch(f, args.agent, args.start, args.end, dry_run=True, **kw)
|
|
184
|
+
print("dry run: nothing fetched")
|
|
185
|
+
return 0
|
|
186
|
+
prepare_out_dir(args.out_dir, args.overwrite)
|
|
187
|
+
f = fetchlib.Fetcher(args.org, os.path.join(args.out_dir, "apex"), args.dataspace, timeout=args.timeout,
|
|
188
|
+
names=names)
|
|
189
|
+
m = fetchlib.fetch(f, args.agent, args.start, args.end, out_dir=args.out_dir, **kw)
|
|
190
|
+
c = m["counts"]
|
|
191
|
+
print(f"fetched {c['fetched']} sessions, {c['turns']} turns, {c['llm_steps']} LLM steps in {c['apex_runs']} "
|
|
192
|
+
f"Apex runs: {os.path.join(args.out_dir, 'details.json')}")
|
|
193
|
+
if os.path.isfile(os.path.join(HERE, "sfap_replay.py")):
|
|
194
|
+
print(f"next: python3 {os.path.join(HERE, 'sfap_replay.py')} --details "
|
|
195
|
+
f"{os.path.join(args.out_dir, 'details.json')} ...")
|
|
196
|
+
else: # a kit
|
|
197
|
+
print(f"send back the whole {args.out_dir} dir (README.md)")
|
|
198
|
+
return 0
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
_RUN_KIT = """#!/usr/bin/env bash
|
|
202
|
+
# Fetch the STDM sessions of {agent} for a backtest, with read-only anonymous Apex (README.md).
|
|
203
|
+
# Usage: ./run_kit.sh <org-alias> [data-space] [more bt_fetch.py flags: --probe, --dry-run, --overwrite]
|
|
204
|
+
set -euo pipefail
|
|
205
|
+
here="$(cd "$(dirname "$0")" && pwd)"
|
|
206
|
+
if [ $# -lt 1 ]; then
|
|
207
|
+
echo "usage: $0 <org-alias> [data-space] [--probe | --dry-run | --overwrite]" >&2
|
|
208
|
+
exit 2
|
|
209
|
+
fi
|
|
210
|
+
org="$1"; shift
|
|
211
|
+
dataspace={dataspace}
|
|
212
|
+
if [ $# -gt 0 ] && [ "${{1#-}}" = "$1" ]; then dataspace="$1"; shift; fi
|
|
213
|
+
exec python3 "$here/bt_fetch.py" --org "$org" --dataspace "$dataspace" \\
|
|
214
|
+
{flags} \\
|
|
215
|
+
--out-dir "$here/out" "$@"
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
_README = """# STDM fetch kit: {agent}
|
|
219
|
+
|
|
220
|
+
Fetches up to {max_sessions} Agentforce sessions of the agent `{agent}`, started from {start} to
|
|
221
|
+
{end} (UTC), from the Session Tracing data (STDM) of a Data Cloud data space. They are the
|
|
222
|
+
input of an offline backtest. Made by `bt_fetch.py --emit-kit` (code sha {code_sha}).
|
|
223
|
+
|
|
224
|
+
## What it does
|
|
225
|
+
|
|
226
|
+
`run_kit.sh` runs `bt_fetch.py`, which writes small anonymous Apex scripts and runs each with
|
|
227
|
+
`sf apex run --json --file <script> --target-org <your org>`. They only read:
|
|
228
|
+
|
|
229
|
+
- SELECT queries through `ConnectApi.CdpQuery.queryAnsiSqlV2` on the DMOs
|
|
230
|
+
{session}, {participant}, {interaction},
|
|
231
|
+
{message} and {step} (and a row count of
|
|
232
|
+
{generation} and {gateway} with `--probe`);
|
|
233
|
+
- a SOQL query of GenAiPlannerDefinition (Id, DeveloperName, MasterLabel), only when no session
|
|
234
|
+
names the agent.
|
|
235
|
+
|
|
236
|
+
There is no DML, no callout, nothing is deployed and nothing is changed in the org. Each script
|
|
237
|
+
is checked before it runs (`btlib/fetch.py`, `check_read_only`: only the words those reads need,
|
|
238
|
+
and SELECT statements only) and stays in `out/apex/` for you to read.
|
|
239
|
+
|
|
240
|
+
{names_note}## Run it
|
|
241
|
+
|
|
242
|
+
Needs python3 (3.9 or later) and the Salesforce CLI `sf`, logged in to the org that holds the
|
|
243
|
+
STDM data, as a user who can query its data space.
|
|
244
|
+
|
|
245
|
+
./run_kit.sh <org-alias> --probe # can this user read the STDM DMOs? (counts only)
|
|
246
|
+
./run_kit.sh <org-alias> --dry-run # how many sessions would come (counts only)
|
|
247
|
+
./run_kit.sh <org-alias> # the fetch, into out/
|
|
248
|
+
./run_kit.sh <org-alias> <data-space> ... # another data space than {dataspace}
|
|
249
|
+
|
|
250
|
+
{exclude_note}## What to send back
|
|
251
|
+
|
|
252
|
+
The whole `out/` dir: `details.json` (the conversations), `sessions.json`, `fetch.json` (what was
|
|
253
|
+
asked and the counts) and `apex/` (the scripts that ran). The conversations hold what users
|
|
254
|
+
wrote: share them only the way your team shares production transcripts.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
_NAMES_NOTE = """## Names
|
|
258
|
+
|
|
259
|
+
This data space names the STDM DMOs and fields its own way. The queries are sent in the names
|
|
260
|
+
of `stdm_names.json` (sha256 {sha256}){user_agent}.{about} `--probe` also runs every query
|
|
261
|
+
the fetch runs, with keys no row has, and names each that fails.
|
|
262
|
+
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def emit_kit(args):
|
|
267
|
+
"""A self-contained copy of this fetch; returns the kit dir."""
|
|
268
|
+
kit = args.emit_kit
|
|
269
|
+
if os.path.exists(kit) and (not os.path.isdir(kit) or os.listdir(kit)):
|
|
270
|
+
raise fetchlib.FetchError(f"{kit} exists and is not an empty dir")
|
|
271
|
+
fetchlib.check_name(args.agent)
|
|
272
|
+
fetchlib.check_name(args.dataspace, "data space name")
|
|
273
|
+
start, end = fetchlib.window_bound(args.start), fetchlib.window_bound(args.end, end=True)
|
|
274
|
+
exclude = read_excluded(args.exclude_sessions)
|
|
275
|
+
names = read_names(args.stdm_names)
|
|
276
|
+
os.makedirs(os.path.join(kit, "btlib"), exist_ok=True)
|
|
277
|
+
os.chmod(kit, 0o700)
|
|
278
|
+
files = {"bt_fetch.py": os.path.join(HERE, "bt_fetch.py")}
|
|
279
|
+
files.update({"btlib/" + name: os.path.join(HERE, "btlib", name) for name in KIT_MODULES})
|
|
280
|
+
if names is not None:
|
|
281
|
+
files["stdm_names.json"] = args.stdm_names
|
|
282
|
+
for rel, src in files.items():
|
|
283
|
+
shutil.copyfile(src, os.path.join(kit, rel))
|
|
284
|
+
if names is not None:
|
|
285
|
+
os.chmod(os.path.join(kit, "stdm_names.json"), 0o600) # data, like the kit's other files
|
|
286
|
+
flags =["--agent", args.agent, "--start", start, "--end", end, "--max-sessions", str(args.max_sessions),
|
|
287
|
+
"--batch-size", str(args.batch_size), "--order", args.order, "--sample-salt", args.sample_salt]
|
|
288
|
+
if args.channels:
|
|
289
|
+
flags += ["--channels", ",".join(args.channels)]
|
|
290
|
+
if args.split:
|
|
291
|
+
flags += ["--val-fraction", repr(args.val_fraction), "--split", args.split, "--split-salt", args.split_salt]
|
|
292
|
+
texts = {"run_kit.sh": _RUN_KIT.format(agent=args.agent, dataspace=shlex.quote(args.dataspace),
|
|
293
|
+
flags=" ".join(shlex.quote(x) for x in flags)
|
|
294
|
+
+ (' --stdm-names "$here/stdm_names.json"' if names else "")
|
|
295
|
+
+ (' --exclude-sessions "$here/exclude_sessions.txt"' if exclude else ""))}
|
|
296
|
+
if exclude: # the ids are the org's own, and the kit goes to people who can read its sessions
|
|
297
|
+
texts["exclude_sessions.txt"] = ("# sessions this fetch must not return (already in the backtest)\n"
|
|
298
|
+
+ "".join(s + "\n" for s in sorted(exclude)))
|
|
299
|
+
dmos = {key: table[1:-1] for key, table in fetchlib.PROBE_DMOS.items()} # the default names, unquoted
|
|
300
|
+
names_note = ""
|
|
301
|
+
if names is not None:
|
|
302
|
+
dmos = {key: names.dmo(name) for key, name in dmos.items()}
|
|
303
|
+
about = names.about + ("" if names.about.endswith((".", "!", "?")) else ".") if names.about else ""
|
|
304
|
+
names_note = _NAMES_NOTE.format(
|
|
305
|
+
sha256=names.sha256, about=f" It says: {about}" if about else "",
|
|
306
|
+
user_agent=f", each with the userAgent `{names.user_agent}`" if names.user_agent else "")
|
|
307
|
+
texts["README.md"] = _README.format(
|
|
308
|
+
agent=args.agent, max_sessions=args.max_sessions, start=start, end=end, dataspace=args.dataspace,
|
|
309
|
+
code_sha=fetchlib.code_sha(), names_note=names_note, **dmos,
|
|
310
|
+
exclude_note=(f"`exclude_sessions.txt` lists {len(exclude)} sessions the backtest already has; the fetch "
|
|
311
|
+
"skips them.\n\n" if exclude else ""))
|
|
312
|
+
for rel, text in texts.items():
|
|
313
|
+
fetchlib.write_private(os.path.join(kit, rel), text)
|
|
314
|
+
os.chmod(os.path.join(kit, "run_kit.sh"), 0o700)
|
|
315
|
+
shas = {}
|
|
316
|
+
for rel in sorted(list(files) + list(texts)):
|
|
317
|
+
with open(os.path.join(kit, rel), "rb") as fh:
|
|
318
|
+
shas[rel] = hashlib.sha256(fh.read()).hexdigest()[:16]
|
|
319
|
+
manifest = {"schema": SCHEMA_KIT, "created": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
320
|
+
"params": {"agent": args.agent, "window": [start, end], "dataspace": args.dataspace,
|
|
321
|
+
"max_sessions": args.max_sessions, "batch_size": args.batch_size,
|
|
322
|
+
"channels": args.channels, "order": args.order, "sample_salt": args.sample_salt,
|
|
323
|
+
"split": None if not args.split else {"split": args.split, "val_fraction": args.val_fraction,
|
|
324
|
+
"salt": args.split_salt},
|
|
325
|
+
"excluded_ids": len(exclude)},
|
|
326
|
+
"code": {"fetch_sha256": fetchlib.code_sha()}, "files": shas}
|
|
327
|
+
if names is not None:
|
|
328
|
+
manifest["params"]["stdm_names"] = names.manifest()
|
|
329
|
+
fetchlib.write_private(os.path.join(kit, "kit.json"), json.dumps(manifest, indent=2) + "\n")
|
|
330
|
+
print(f"kit: {kit} ({len(shas) + 1} files); they run ./run_kit.sh <org-alias> and send back {kit}/out/")
|
|
331
|
+
return kit
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def main(argv=None):
|
|
335
|
+
args = parse_args(argv)
|
|
336
|
+
try:
|
|
337
|
+
if args.emit_kit:
|
|
338
|
+
emit_kit(args)
|
|
339
|
+
return 0
|
|
340
|
+
return run_probe(args) if args.probe else run_fetch(args)
|
|
341
|
+
except fetchlib.FetchError as e:
|
|
342
|
+
print(f"error: {fetchlib.mask_ids(str(e))}", file=sys.stderr)
|
|
343
|
+
return 1
|
|
344
|
+
except OSError as e:
|
|
345
|
+
print(f"error: {fetchlib.mask_ids(str(e))}", file=sys.stderr)
|
|
346
|
+
return 1
|
|
347
|
+
except KeyboardInterrupt:
|
|
348
|
+
print("interrupted: rerun into the same --out-dir to go on from the finished batches", file=sys.stderr)
|
|
349
|
+
return 130
|
|
350
|
+
except Exception as e: # a bug, or an answer it did not foresee: no traceback (it can quote session data)
|
|
351
|
+
frame = traceback.extract_tb(e.__traceback__)[-1]
|
|
352
|
+
print(f"error: bt_fetch failed ({type(e).__name__} at {os.path.basename(frame.filename)}:{frame.lineno}: "
|
|
353
|
+
f"{fetchlib.mask_ids(str(e))[:200]})", file=sys.stderr)
|
|
354
|
+
return 1
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
if __name__ == "__main__":
|
|
358
|
+
sys.exit(main())
|